Skip to main content

maf-sandbox-otel

PyPI Python License

Experimental. This package is early-stage (pre-1.0, Development Status :: 4 - Beta) — its API may change or be removed in a future release without notice. Importing it emits a one-time MafSandboxOtelExperimentalWarning; suppress it with warnings.filterwarnings("ignore", category=maf_sandbox_otel.MafSandboxOtelExperimentalWarning) once you've read the notice.

OpenTelemetry records of what a sandbox did — which conversation was served what posture, which host tools a guest called and under whose authority, what crossed the boundary and with what integrity label, and what became of each sandbox on its way out — the delete addressed to one key, and the purge that clears a whole conversation. See the limits below for what it still does not see.

maf-sandbox reports these as events on an observer seam and records nothing itself. This package is one observer: it turns each event into a log record and a span, and the countable ones into a metric. A store read has no duration of its own, so its span is a single instant. Every signal goes through the providers you give the constructor, so records you route to a security pipeline do not also land on the application's trace. It depends on maf-sandbox and the OpenTelemetry API, and on nothing else — no backend, no agent framework, no SDK.

pip install maf-sandbox-otel

Process audit logs

The registry observer receives bounded process snapshots around supervised runs and every process cleanup attempt. This package exports sandbox.process.snapshot, sandbox.process.observed and sandbox.process.cleanup logs even when traces are sampled out. Per-process logs include PID, user IDs, ancestry, start ticks, state, resource usage and attribution. Enable record_sensitive_data=True on the audit observer to include commands, argv, usernames and paths; the default redacts those fields. Collection failures and truncation are explicit. Snapshots are guest-observed evidence and cannot establish that a sandbox is completely clean.

Wiring

There are two registration points because there are two host-policy objects, and a host that wires one records only that half.

from maf_sandbox import HostToolRegistry, SandboxRouter
from maf_sandbox_otel import OpenTelemetrySandboxObserver

observer = OpenTelemetrySandboxObserver()

router = SandboxRouter([backend], observer=observer)
registry = HostToolRegistry(observer=observer)

collect_outputs is neither — it is a function a kind calls per collection, so a kind that reports its file landings passes the observer and the key as arguments.

Each provider argument defaults to the global one, so with nothing else configured these records land beside the application's own traces:

observer = OpenTelemetrySandboxObserver(
    logger_provider=security_logs,   # a SIEM pipeline, its own exporter and retention
    tracer_provider=None,            # spans stay with the application's traces
)

Splitting them is usually what a security record wants. A SIEM does not want the application's trace sampling applied to it, and an application's trace store does not want a year of egress records. The three providers are independent, so a deployment can move the logs and leave the spans where they were.

What is recorded

Event Span Metric
A sandbox was served, or refused sandbox.acquire maf_sandbox.sandbox.acquires
A guest called back into the host sandbox.host_tool_call maf_sandbox.host_tool.calls, .response_bytes
A call read a file out of the host's store sandbox.files_in (one instant, no duration) maf_sandbox.store.file_reads
A collection landed artifacts in a sink sandbox.files_out maf_sandbox.outputs.landed_files, .landed_bytes
One backend answered one disposal sandbox.dispose maf_sandbox.sandbox.disposals
One backend answered one conversation's purge sandbox.purge maf_sandbox.scope.purges, .purged_sandboxes
A backend reported what its egress enforcement decided sandbox.egress maf_sandbox.egress.decisions
A sandboxed tool call ended sandbox.call maf_sandbox.call.duration

Every event also emits a log record, and that is the one a security pipeline should keep: it does not depend on anything else being instrumented, and it survives a trace sampler that discarded the span. Attributes are under maf_sandbox.*, so they select cleanly out of a pipeline carrying everyone else's.

What crosses, and what does not

Shape and policy always; content only when asked. A sandbox's posture — the egress mode and its allowlist, the isolation rung, the capabilities, the sealed host-tool surface and the authority it carries, the integrity label, the counts, the sizes, the outcome — is what a security question is asked in, and a guest chooses none of it. Names and sentences are the other half: an artifact name is written by the model, a host-tool refusal quotes a bounded copy of what the guest asked for, and a store file name is the host's own vocabulary about its own data. Those cross only under record_sensitive_data=True, which mirrors the agent framework's switch of the same name and is off by default.

A SandboxKey is the column almost every record joins on, so it cannot simply be dropped. It is hashed by default — stable across processes, so grouping still works, and not reversible by reading. It is not a secret: an id drawn from a small space can be recovered by hashing the candidates, and a deployment that needs the key withheld from a pipeline should not send it rather than trust this.

sandbox.purge is the one record with no key on it, because core's event has none: a backend answers a purge with a count rather than with the sandboxes it removed. It carries maf_sandbox.sandbox.conversation — the same hash of (scope, thread_id) every keyed record carries beside its key — so the record that says a conversation was cleaned up groups with that conversation's own.

The call id is the one part of a key recorded in the clear. The framework generates it per call, it is drawn from nobody's vocabulary, and it is what names the folder a per_call sink lands that call's artifacts in — so hashing it would cost the correlation a landing record exists for and protect nothing.

Two attributes carry a call id, and they are not the same column. maf_sandbox.sandbox.call_id is the key's own, and a key carries one only where the workload runs a sandbox per call. maf_sandbox.call.id is which tool call the record came from, and it is on every record that has one — so it, and not the key, is what separates two calls in flight on one conversation. They hold the same string at IsolationScope.CALL and only there. sandbox.call always carries it, because that is the record the others join to; sandbox.egress never does, because a drain covers a window between two removals and the decisions in it span whatever calls happened meanwhile.

Two limits worth knowing before you rely on it

Egress is recorded by the backends that enforce it themselves, and by no others. sandbox.acquire carries the mode and the allowlist a sandbox was served under; sandbox.egress carries what its enforcer then decided, which is a different question and the one an incident is asked in. The docker and wslc backends answer it — they run the proxy, so they read its ALLOW/DENY lines back before the container holding them goes. ACAS does not: it enforces in the service, and the deny reason never leaves the guest. Do not read an absence of sandbox.egress as an absence of traffic. The acquire record carries maf_sandbox.backend.observes_egress for exactly this, and it is the attribute that separates a sandbox nobody watched from a sandbox that reached nothing.

The events of one call are siblings, not children of sandbox.call. Every event arrives after the work it describes, and the call's own event arrives last, so there is no moment at which this package could open a parent for the others to nest under. Each is parented to whatever span is current where it arrives instead — the agent framework's execute_tool span — and sandbox.call carries the total the caller waited for. Buffering them into a real tree would need per-call state that a cancellation could leak. That holds for a tool body that awaits nothing too, even though its record arrives on a worker thread: the framework dispatches with asyncio.to_thread from inside the span, and that copies the context the current span lives in, so the span crosses with the body. A test reproduces that dispatch rather than describing it.

Cost

An observer is called synchronously inside the call it records — on the event loop's task, or on the worker thread a synchronous tool body runs on — so this package does no I/O: it hands each record to the OpenTelemetry API and returns. It keeps no per-call state, so two calls reaching one observer at once share nothing to race over; the OpenTelemetry tracer, meter and logger it holds are thread-safe by that API's own contract. With no SDK installed at all, the API's no-op implementations answer and the cost is a few attribute dictionaries per call.

Whether export blocks the call is your SDK configuration, not this package. A BatchSpanProcessor and a BatchLogRecordProcessor hand off to their own thread, which is what keeps a slow collector away from a sandbox call. The Simple* processors call the exporter synchronously, inside span.end() and logger.emit() — so configured that way, a network exporter blocks the call for as long as the export takes. Use the batch processors where call latency matters; the simple ones are for tests, which is what this package's own suite uses them for.

A failure here never reaches the call — maf-sandbox contains whatever an observer does and logs it. With two deliberate exceptions: SystemExit and KeyboardInterrupt are the host's own control flow rather than a recorder failing, so core lets them through, including when one arrives as a leaf of an exception group. Nothing in this package raises them, but an exporter or a provider you configure can, and if it does the call goes down with it.

Release files for maf-sandbox-otel 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for maf-sandbox-otel 0.5.1
File Size Uploaded
maf_sandbox_otel-0.5.1.tar.gz 20.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for maf-sandbox-otel 0.5.1
File Interpreter ABI Platform
maf_sandbox_otel-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 43.3 kB

Release files / maf_sandbox_otel-0.5.1.tar.gz

Download URL maf_sandbox_otel-0.5.1.tar.gz
Size 20.5 kB
Tags Source
SHA-256 checksum
How to use checksums
7f4960229779133133d6532c43f58ada75c6ce7b19d2bf99ad3df0e6876287a8
BLAKE2b-256 checksum
How to use checksums
e4e85eadc17285949688ccd4318a30af9c31928ad903d5675d4bcbc63606459f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.

Transparency log

Release files / maf_sandbox_otel-0.5.1-py3-none-any.whl

Download URL maf_sandbox_otel-0.5.1-py3-none-any.whl
Size 22.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5d564e9b59e419af8fd1e74f004cb0922a73a24499ed499b81fe8cd14dc7a0a3
BLAKE2b-256 checksum
How to use checksums
97696208e7b74d67e3ce21276559824b069cf5a1bc3b2272f68fb6426b9a3cfa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.

Transparency log

Release history Release notifications | RSS feed

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

This release

0.5.1 This release

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page