Skip to content

Tool Runtime Security & Isolation

Document ID: TRT-005
File Path: docs/07-tool-runtime/security-isolation.md
Version: 1.1.0
Status: Draft — the network-isolation section (§5) reflects the current implementation; other sections (filesystem, secrets, tenant isolation) are still directional
Owner: AI Platform Team
Last Updated: 2026-07-03


This document defines the security model of the Tool Runtime: the threat model, how untrusted code is contained, how network and filesystem access are constrained, how secrets are injected, and how tenants are isolated.

Tools are the platform’s most dangerous surface — they run arbitrary, often third-party code. The Runtime treats every tool as untrusted by default and enforces least privilege end to end.


ThreatMitigation
Malicious tool codeSandboxing; default-deny network/fs; ephemeral execution
Sandbox escapeStrong backends (gVisor/microVM) for untrusted; hardened nodes
Data exfiltrationEgress allowlists; no ambient credentials; output size caps
Cross-tenant accessPer-tenant pools, namespaces, and storage; no reuse across tenants
Secret theftIn-memory injection; never logged; zeroed on teardown
Resource exhaustion (DoS)Hard CPU/mem/disk/time/PID limits; fair scheduling
Supply-chain attackImage signing + provenance verification
Privilege escalationNon-root execution; dropped capabilities; read-only system paths

Every tool carries a trust class that drives isolation strength:

ClassSourceMinimum backend
First-partyBuilt by the platform teamnative / wasm
VerifiedThird-party, reviewed + signedcontainer / gVisor
UntrustedUnreviewed / user-suppliedgVisor / microVM

Tenant policy sets a floor: it may require a stronger backend than the tool’s class implies, never weaker (see Sandbox Runtime §3).


Before execution, the Permission Engine evaluates the caller’s grants against the tool’s required permissions (Tool Framework §26–28) through the Policy Engine:

Caller grants ∩ Tool required permissions ⊆ Policy allow ⇒ execute
else ⇒ forbidden

Authorization is fail-closed: any evaluation error denies execution.


Default policy is deny-all egress. Tools declare required destinations (Tool Framework §34), enforced by the Runtime:

  • An egress proxy / network policy allows only declared hosts/ports.
  • DNS is restricted to allowed domains (prevents DNS exfiltration).
  • No inbound connectivity to sandboxes.
  • Per-tenant egress is metered; anomalous volume raises alerts.
  • Untrusted tools route egress through an inspecting proxy.
network:
default: deny
outbound_allow:
- api.example.com:443
dns_allow:
- api.example.com
inbound: deny

Implemented: wovyr-toolsEgressProxy is a host-side HTTP CONNECT tunnel enforcing the host allow-list; the container backend points a sandboxed workload at it via HTTPS_PROXY. That alone was only cooperative — a workload that ignored the env var and dialed out directly had full --network bridge connectivity underneath it (the “L3 egress bypass” gap noted in earlier revisions of this platform). This is now closed: before the real command ever runs, the host attaches to the container’s network namespace via nsenter and applies an iptables default-deny to its OUTPUT chain, allowing only loopback and the egress proxy’s address (egress_lockdown) — so ignoring HTTPS_PROXY now reaches nothing. The container starts running an inert placeholder and only receives the real command via docker exec once the lockdown is confirmed in place, so there is no window where untrusted code runs before the restriction applies. Linux/Docker-specific (needs nsenter + iptables on the host); fails closed if either is unavailable. Not yet extended to Podman (its network inspect output shape differs) or to DNS-level restriction (the lockdown allows only the literal egress-proxy IP, so DNS is moot for this path — the container needs no DNS lookup to reach it).

5.1 The native backend’s isolation is scoped, not universal (SEC-404)

Section titled “5.1 The native backend’s isolation is scoped, not universal (SEC-404)”

§5’s egress-proxy + iptables/nsenter lockdown describes the container/gVisor path (verified/untrusted trust classes, or an operator-set policy floor). The native backend — first-party shell/code_execute runs, the default trust class — historically enforced only resource limits (timeout, output cap, setrlimit/Job Object): no filesystem confinement beyond the run’s working directory, and no network isolation at all. On the default cross-platform path, “sandboxed tools” was accurate for resource limits and false for confinement — a native run could read ~/.wovyr/kms/root.key and exfiltrate it over the open network.

The native path now has a confinement floor, not parity with the container path:

  • Linux, with working unprivileged user+network namespaces: a native run is confined to a deny-all egress namespace (unshare --map-root-user --net, no interfaces configured — no route out). Probed once per process (NativeSandbox::network_isolation_available).
  • Windows/macOS, and hardened Linux kernels with unprivileged namespaces disabled: there is no native egress mechanism. A native run there is unsandboxed for network access. This is never silent: it proceeds only as an explicitly-acknowledged operator choice (the CLI/local trusted context, or WOVYR_ALLOW_UNSANDBOXED_NATIVE=1 on a hosted deployment) — logged loudly on every such run — or the tool call is refused (PermissionDenied) if unacknowledged.
  • Filesystem confinement on the native path remains a documented gap on every platform: the run’s working directory scopes relative paths, but nothing prevents an absolute path outside it, or a symlink escape. Cross-platform parity to the Linux+Docker container path is an explicit non-goal for the native backend (an untrusted or filesystem-sensitive run should select the container/gVisor backend instead, via trust classification or a policy floor — see §3).

5.2 Privileged builtins need an explicit opt-in under --local (SBX-305)

Section titled “5.2 Privileged builtins need an explicit opt-in under --local (SBX-305)”

§5.1’s confinement floor governs how a native tool call runs. This section governs whether the privileged tools are registered at all for a local CLI run.

shell, fs_write, and code_execute are not default builtins (ToolRegistry::with_builtins()); they are added only by with_privileged_builtins(). A hosted server requires the operator to set WOVYR_ENABLE_SHELL_TOOL=1 before shell appears at all (SEC-301). The CLI’s --local paths used to call with_privileged_builtins() unconditionally, treating “the operator typed --local” as the acknowledgement. That is materially weaker than §5.1’s floor, and it could not distinguish the two cases it was applied to identically:

  • a single-operator trusted workstation, where full host access to your own machine is the documented, accepted design; and
  • a shared, CI, or multi-tenant host, where the same command grants full host access to whatever a model decides to do, for anyone who can supply a manifest.

The 2026-07-27 internal red-team assessment confirmed the consequence concretely: a real model, driven only by an agent manifest that listed the shell tool, read a host file outside the run’s working directory, with nothing but a WARN line in the way.

Both privileged registration paths now require an explicit signal:

CommandOpt-in
wovyr agents run --local--allow-privileged-tools
wovyr workflows run --local--allow-privileged-tools
wovyr workflows approve / signal / tickWOVYR_LOCAL_PRIVILEGED=1 (no flag — these resume an existing execution)

WOVYR_LOCAL_PRIVILEGED=1 also satisfies the two run commands, for a session that needs privileged tools throughout. Absent any signal, a --local run gets exactly the safe builtins (echo, fs_read, http_get) — the same set the hosted server defaults to.

Fail closed, not fail quiet. A manifest or definition that names a privileged tool is rejected before anything runs, with an error naming the flag. Without that, the run would still fail (resolve_tools rejects a tool missing from the registry) but with a bare “unknown tool” message that reads like a typo and gives no hint that an opt-in exists. The workflow-side check inspects for_each/map bodies too, since a fan-out’s per-item activity template is where a privileged tool would otherwise hide.

This flag is a scope decision, not a sandbox. Enabling it does not weaken §5.1 — an enabled shell call still goes through the same confinement floor and still logs the same loud warning on an unsandboxed native run. It is meant for a host where you trust everything the agent may decide to do. On a shared host, prefer trust classification (§3) so the call selects a container/gVisor backend instead.


  • Fresh, isolated root per execution; only declared paths mounted (Tool Framework §35).
  • Workspace is per-execution scratch, wiped on teardown.
  • System paths mounted read-only; no host path is ever bind-mounted into untrusted sandboxes.
  • Output is read back through a bounded channel (max_output_bytes), not by the host reading sandbox disk.

Tools never see raw long-lived credentials. The Secret Injector:

1. Resolve secret references from the tool's grants (Secret Vault)
2. Mint short-lived, scoped credentials where possible
3. Inject into the sandbox via in-memory env / tmpfs (never persistent disk)
4. Zero secrets on teardown; never write them to logs or audit

Aligned with Tool Framework §29–30. Secrets are scoped to the single execution and revoked after.


  • Workers are pooled per trust class, and sandboxes are never reused across tenants (see Worker Pool §3).
  • Scratch, network namespaces, and caches are tenant-scoped.
  • Result caches and execution records are tenant-partitioned.
  • Cross-tenant isolation is a hard requirement, verified by tests (zero leakage), per Tool Framework §70.

For third-party tools and images:

  • Artifacts are content-addressed and pulled by digest.
  • Signatures are verified (e.g. Sigstore-style) before a tool may run.
  • Provenance/SBOM is recorded; tools failing verification are quarantined.
  • A tool’s manifest, permissions, and image are pinned per version so a published version cannot silently change.

  • Tools run as an unprivileged user inside the sandbox.
  • Linux capabilities are dropped to the minimum; no-new-privileges is set.
  • seccomp/syscall filtering restricts the syscall surface (especially under gVisor/microVM).

Every execution writes a tamper-evident audit record:

{
"execution_id": "exec_01H...",
"tenant": "acme", "principal": "agent:order-assistant",
"tool": "http.request", "version": "1.2.0",
"input_hash": "sha256:...", "egress": ["api.example.com"],
"sandbox": "gvisor", "status": "succeeded",
"secrets_used": ["ref:example-api-key"],
"timestamp": "2026-06-27T10:00:00Z"
}

Inputs are hashed (not stored raw) unless policy requires retention; secrets are referenced, never valued.


RequirementTarget
Authorization decision< 5 ms p95
Secret injection< 5 ms p95
Cross-tenant leakage0 (hard)
Egress policy enforcement100% of executions



VersionDateDescription
1.2.02026-07-25SEC-404: new §5.1 scopes the “sandboxed tools” claim precisely for the native backend — a Linux deny-all egress floor via unprivileged netns, an explicit operator-acknowledgement-or-refusal path on Windows/macOS (never a silent unsandboxed run), and filesystem confinement on the native path named as a documented gap.
1.1.02026-07-03§5 Network Isolation: closed the “L3 egress bypass” gap — wovyr-tools’ container backend now applies a host-side iptables default-deny (via nsenter into the container’s network namespace, before the real command runs) restricting OUTPUT to loopback + the egress proxy’s address, so a workload ignoring HTTPS_PROXY no longer reaches anything. Linux/Docker-specific; not yet extended to Podman. Not run against a live Docker/nsenter/iptables environment in the authoring session — flagged for verification on first real use
1.0.02026-06-27Initial Tool Runtime Security & Isolation specification