# zidane/contexts

> Execution contexts, the environments where an agent runs shell commands and reads and writes files.

Import: `import { … } from 'zidane/contexts'`. Source: [src/contexts/index.ts](https://github.com/Tahul/zidane/blob/main/src/contexts/index.ts).

Use this entry point to choose or build the place where tool calls run. It holds the `ExecutionContext` interface that every backend implements, the in-process `createProcessContext`, and the helpers for the optional OS sandbox. Docker, E2B and Daytona have their own subpaths, so their SDKs load only when you import them. See [Execution contexts](https://zidane.si/docs/execution-contexts.md) for the guide.

```ts
import { createProcessContext } from 'zidane/contexts'

const context = createProcessContext({ cwd: process.cwd() })
const handle = await context.spawn()
const result = await context.exec(handle, 'echo hello')
console.log(result.stdout, result.exitCode)
await context.destroy(handle)
```

## Functions

### `createProcessContext`

Function.

Creates an execution context that runs commands and file operations in the current process.
No isolation unless `config.isolation` asks for an OS sandbox. This is the default context.

```ts
function createProcessContext(config?: SpawnConfig): ExecutionContext
```

**Parameters**

- `config?` (`SpawnConfig`): Default spawn settings, such as `cwd`, file containment root and isolation.

**Returns** `ExecutionContext`

**Example**

```ts
import { createAgent } from 'zidane'
import { createProcessContext } from 'zidane/contexts'
import { basic } from 'zidane/presets'
import { anthropic } from 'zidane/providers/anthropic'

const agent = createAgent({
  ...basic,
  provider: anthropic(),
  execution: createProcessContext({ cwd: process.cwd() }),
})
```

Source: [src/contexts/process.ts:42](https://github.com/Tahul/zidane/blob/main/src/contexts/process.ts#L42)

### `entryPortOf`

Function.

The context's own entry port for `handle`, else one over its commands.

```ts
function entryPortOf(context: ExecutionContext, handle: ExecutionHandle): EntryPort
```

**Parameters**

- `context` (`ExecutionContext`): The execution context.
- `handle` (`ExecutionHandle`): The handle the operations run in.

**Returns** `EntryPort`

Source: [src/contexts/entries.ts:179](https://github.com/Tahul/zidane/blob/main/src/contexts/entries.ts#L179)

### `execEntryPort`

Function.

An [`EntryPort`](https://zidane.si/docs/reference/contexts.md#EntryPort) over a context's `exec` (POSIX `sh`), its files written with the context's `writeFile`.

```ts
function execEntryPort(
  context: Pick<ExecutionContext, 'exec' | 'writeFile'>,
  handle: ExecutionHandle,
): EntryPort
```

**Parameters**

- `context` (`Pick<ExecutionContext, 'exec' | 'writeFile'>`): The context: its commands and its file write.
- `handle` (`ExecutionHandle`): The handle to run them in.

**Returns** `EntryPort`

Source: [src/contexts/entries.ts:130](https://github.com/Tahul/zidane/blob/main/src/contexts/entries.ts#L130)

### `isolationAvailability`

Function.

Whether the sandbox can actually be enforced on this host. Callers that
requested isolation and get `ok: false` MUST fail closed (refuse to run
the command) — silently running unsandboxed would turn a security
setting into a placebo.

```ts
function isolationAvailability(
  platform?: Platform,
  env?: ProcessEnv,
): IsolationAvailability
```

**Parameters**

- `platform?` (`Platform`, default `process.platform`): Platform to check.
- `env?` (`ProcessEnv`, default `process.env`): Environment used to look up `bwrap` on `PATH`.

**Returns** `IsolationAvailability`

Source: [src/contexts/isolation.ts:135](https://github.com/Tahul/zidane/blob/main/src/contexts/isolation.ts#L135)

### `listOwnedProcesses`

Function.

The processes registered now.

```ts
function listOwnedProcesses(): OwnedProcess[]
```

**Returns** `OwnedProcess[]`

Source: [src/contexts/owned-processes.ts:43](https://github.com/Tahul/zidane/blob/main/src/contexts/owned-processes.ts#L43)

### `listSessionProcesses`

Function.

The processes registered now.

```ts
function listSessionProcesses(): SessionProcess[]
```

**Returns** `SessionProcess[]`

Source: [src/contexts/session-processes.ts:41](https://github.com/Tahul/zidane/blob/main/src/contexts/session-processes.ts#L41)

### `nodeEntryPort`

Function.

An [`EntryPort`](https://zidane.si/docs/reference/contexts.md#EntryPort) on this machine's filesystem.

```ts
function nodeEntryPort(options: NodeEntryPortOptions): EntryPort
```

**Parameters**

- `options` (`NodeEntryPortOptions`): How paths resolve, what may change, and where removed entries go.

**Returns** `EntryPort`

Source: [src/contexts/entries.ts:42](https://github.com/Tahul/zidane/blob/main/src/contexts/entries.ts#L42)

### `parseAllowWriteSetting`

Function.

Parse the user-facing "extra writable paths" setting: comma- or
newline-separated, `~` expanded, relative entries resolved against `cwd`,
blanks dropped.

```ts
function parseAllowWriteSetting(raw: string, cwd: string, home?: string): string[]
```

**Parameters**

- `raw` (`string`): Comma- or newline-separated paths.
- `cwd` (`string`): Base for relative entries.
- `home?` (`string`): Home directory that `~` expands to.

**Returns** `string[]`

Source: [src/contexts/isolation.ts:175](https://github.com/Tahul/zidane/blob/main/src/contexts/isolation.ts#L175)

### `registerSessionProcess`

Function.

Records `pid` as started for `sessionId`.

```ts
function registerSessionProcess(pid: number, sessionId: string): () => void
```

**Parameters**

- `pid` (`number`): The child's pid.
- `sessionId` (`string`): The session's id on this host.

**Returns** `() => void`: Removes the record; idempotent, and a no-op once the pid was registered again.

Source: [src/contexts/session-processes.ts:31](https://github.com/Tahul/zidane/blob/main/src/contexts/session-processes.ts#L31)

### `resolveDetachedTasksCapability`

Function.

Effective [`DetachedTasksCapability`](https://zidane.si/docs/reference/contexts.md#DetachedTasksCapability) of a context, with the
backward-compatible inference for contexts that predate the field.

```ts
function resolveDetachedTasksCapability(
  context: ExecutionContext,
): DetachedTasksCapability
```

**Parameters**

- `context` (`ExecutionContext`): Execution context to inspect.

**Returns** `DetachedTasksCapability`

Source: [src/contexts/types.ts:80](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L80)

### `resolveWritableRoots`

Function.

The set of directories a sandboxed command may write to: the workspace
(`cwd`), the system temp dirs, git's common dir when `cwd` is a linked
worktree, plus any user-configured extras. All canonicalized.

```ts
function resolveWritableRoots(
  cwd: string,
  options?: ResolveWritableRootsOptions,
): string[]
```

**Parameters**

- `cwd` (`string`): Workspace directory.
- `options?` (`ResolveWritableRootsOptions`, default `{}`): Extra roots and overrides for the environment, platform and git lookup.

**Returns** `string[]`

Source: [src/contexts/isolation.ts:321](https://github.com/Tahul/zidane/blob/main/src/contexts/isolation.ts#L321)

### `resolveWritableRootsAsync`

Function.

Async twin of [`resolveWritableRoots`](https://zidane.si/docs/reference/contexts.md#resolveWritableRoots) for hosts that must not block (the daemon).

```ts
function resolveWritableRootsAsync(
  cwd: string,
  options?: Omit<ResolveWritableRootsOptions, 'gitCommonDir'> & { gitCommonDir?: (cwd: string) => Promise<string | null> },
): Promise<string[]>
```

**Parameters**

- `cwd` (`string`): Workspace directory.
- `options?` (`Omit<ResolveWritableRootsOptions, 'gitCommonDir'> & { gitCommonDir?: (cwd: string) => Promise<string | null> }`, default `{}`): Extra roots and overrides for the environment and platform, with an async git lookup.

**Returns** `Promise<string[]>`

Source: [src/contexts/isolation.ts:331](https://github.com/Tahul/zidane/blob/main/src/contexts/isolation.ts#L331)

## Interfaces

### `ContextCapabilities`

Interface.

Features an execution context supports. Consumers check these before relying on a feature.

```ts
interface ContextCapabilities
```

**Properties**

- `detachedTasks?` (`DetachedTasksCapability`): Background-task lifetime guarantee. Optional for backward
  compatibility — when absent, callers infer `'process-lifetime'`
  if the context implements `execBackground`, `'none'` otherwise
  (see [`resolveDetachedTasksCapability`](https://zidane.si/docs/reference/contexts.md#resolveDetachedTasksCapability)).
- `filesystem` (`boolean`): Can read/write files in a workspace
- `glob?` (`boolean`): Can enumerate glob matches through `ExecutionContext.listFiles(..., { glob: true })`.
- `gpu` (`boolean`): Has GPU access
- `isolated?` (`boolean`): Shell commands run under an ENFORCED OS-level sandbox (macOS Seatbelt /
  Linux bwrap) — see `SpawnConfig.isolation`. `false` when isolation was
  not requested AND when it was requested but the backend is unavailable
  (that context fails every command closed instead). Absent / `false` =
  commands do not run sandboxed.
- `network` (`boolean`): Can make outbound network requests
- `shell` (`boolean`): Can execute shell commands
- `stream?` (`boolean`): Can open a bidirectional byte stream to a long-lived process through
  `ExecutionContext.execStream`. Absent / `false` = the context
  only supports the buffered `ExecutionContext.exec` and (maybe)
  the write-to-a-log-file `ExecutionContext.execBackground`.

  Consumers that NEED live stdin (stdio MCP servers routed into the
  context) must check this before calling; the seam is optional so
  contexts backed by a request/response-only API stay valid.

Source: [src/contexts/types.ts:35](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L35)

### `ContextHardening`

Interface.

Opt-in container hardening for the docker context. Every field defaults to
"unset" so an omitted `hardening` (or omitted field) leaves the container
unrestricted.

```ts
interface ContextHardening
```

**Properties**

- `dropAllCapabilities?` (`boolean`): Drop all Linux capabilities (`CapDrop: ['ALL']`). Strong isolation, but
  breaks images needing capabilities (e.g. binding low ports, `ping`).
  Default: `false`.
- `noNewPrivileges?` (`boolean`): Set `no-new-privileges` so processes can't gain privileges via setuid
  binaries. Low blast radius; safe for most workloads. Default: `false`.
- `pidsLimit?` (`number`): Max number of processes (`PidsLimit`) — caps fork-bombs. Default: unset
  (no limit). A few hundred is usually plenty for an agent sandbox.
- `readonlyRootfs?` (`boolean`): Mount the container root filesystem read-only. Pair with writable mounts
  for scratch space. Default: `false`.

Source: [src/contexts/types.ts:325](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L325)

### `ContextMount`

Interface.

A host directory bind-mounted into an execution context.

```ts
interface ContextMount
```

**Properties**

- `readonly?` (`boolean`): Mount read-only. Defaults to false for Docker's native bind behavior.
- `shared?` (`boolean`): Apply the SELinux shared label (`:z`) so the host user and the container
  user can both read/write the mount (docker context only). No-op on
  non-SELinux hosts. Combine with `SpawnConfig.user` to avoid root-owned
  files leaking onto the host. Mutually exclusive with `readonly`.
- `source` (`string`): Absolute host path to mount.
- `target` (`string`): Absolute path inside the execution context.

Source: [src/contexts/types.ts:350](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L350)

### `EntryPort`

Interface.

The few file operations every entry action is built from
(`createEntryOps`, `zidane/chat`), paths resolved like the context's
other file methods. None of them overwrites: a target is checked missing
first, and the operations refuse one that appeared since.

```ts
interface EntryPort
```

**Properties**

- `copy` (`(from: string, to: string) => Promise<void>`): Copies `from` (a folder whole, symlinks as links) to `to` (missing; its folder exists).
- `emptyDirs` (`(path: string, limit: number) => Promise<string[]>`): Empty folders under folder `path` (`.git` and `node_modules` skipped), relative to it, at most `limit`.
- `list` (`(path: string) => Promise<string[]>`): The names in folder `path`.
- `mkdir` (`(path: string) => Promise<void>`): Creates folder `path` and its missing parents.
- `move` (`(from: string, to: string) => Promise<void>`): Renames `from` to `to` (missing; its folder exists).
- `remove` (`(path: string) => Promise<void>`): Removes `path` (a folder whole).
- `stat` (`(path: string) => Promise<EntryKind | null>`): What `path` is, `null` when nothing is there. Never follows a final symlink.
- `write` (`(path: string, bytes: Uint8Array) => Promise<void>`): Writes a new file (its folder exists).

Source: [src/contexts/types.ts:122](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L122)

### `ExecOptions`

Interface.

Options of `ExecutionContext.exec`.

```ts
interface ExecOptions
```

**Properties**

- `cwd?` (`string`): Folder to run in, resolved against the handle's. Default: the handle's folder.
- `env?` (`Record<string, string>`): Variables added to the command's environment.
- `onOutput?` (`(stream: 'stdout' | 'stderr', text: string) => void`): Output as it arrives, decoded per stream. A chunk can end mid-line.
- `signal?` (`AbortSignal`): Kills the command (its whole process group, in the process context) when aborted.
- `timeout?` (`number`): Seconds. Default 30.

Source: [src/contexts/types.ts:374](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L374)

### `ExecResult`

Interface.

Result of a buffered command.

```ts
interface ExecResult
```

**Properties**

- `exitCode` (`number`): Process exit code. `0` means success.
- `stderr` (`string`): Captured standard error.
- `stdout` (`string`): Captured standard output.

Source: [src/contexts/types.ts:104](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L104)

### `ExecutionContext`

Interface.

An environment where an agent runs commands and reads and writes files: the host process, a Docker container or a remote sandbox.

```ts
interface ExecutionContext
```

**Properties**

- `capabilities` (`ContextCapabilities`, readonly): What this context supports
- `destroy` (`(handle: ExecutionHandle) => Promise<void>`): Destroy the execution environment and clean up resources
- `entries?` (`(handle: ExecutionHandle) => EntryPort`): The context's own entry operations (create, move, copy, remove) for
  `handle`, with paths resolved like every other file method. Optional:
  without it, `entryPortOf` (`./entries`) runs POSIX commands through
  `exec` (`mkdir -p`, `mv`, `cp -RP`, `rm -rf`), which every shell
  context can do. Implement it where a native call is cheaper or safer
  (in-process fs, a machine link).
- `exec` (`(handle: ExecutionHandle, command: string, options?: ExecOptions) => Promise<ExecResult>`): Execute a shell command in the context.

  `signal` propagates abort all the way down to the underlying child
  process — the implementation is expected to wire it into whatever
  spawn primitive it uses so the OS receives a SIGTERM (or equivalent)
  when the caller aborts. Without it, a cancelled tool returns its
  cancellation marker to the model but the underlying process keeps
  running in the background, orphaning compute / locks / IO. Pass
  `ctx.signal` from a tool body to inherit the per-call + run-level
  abort union; pass a freshly-built one for a host-driven kill.

  Implementations are free to ignore `signal` (the contract degrades
  gracefully — the process simply won't be killed), but the process
  context honors it.

  `timeout` is in SECONDS (default 30) — every
  implementation converts internally (process ×1000 to ms, docker wraps
  with coreutils `timeout`, sandbox SDKs take seconds/ms per their API).
  Documented here because the unit is invisible at call sites and a
  caller assuming milliseconds would set an instant-kill timeout.

  `onOutput` hears the output as it arrives, decoded, before the result
  settles (a UI showing a running command). Optional for implementations:
  one that cannot stream calls it never, and the result is the same.
- `execBackground?` (`(handle: ExecutionHandle, command: string, options: { cwd?: string; env?: Record<string, string>; maxOutputBytes?: number; onExit?: (info: TaskExitInfo) => void; onStall?: (info: TaskStallInfo) => void; outputDir: string; stallTimeoutMs?: number; terminal?: BackgroundTerminalOptions }) => Promise<TaskHandle>`): Start a process in the background. Settles as soon as `spawn` returns
  — does NOT wait for the child to exit. Stdout + stderr stream
  interleaved to the file at the returned `outputPath`. The caller
  (typically the agent) reads incremental output via the regular
  `ExecutionContext.readFile` seam.

  Optional — contexts without background support (some remote sandboxes)
  just don't implement it. The shell tool surfaces a clean
  "background mode is not supported in this execution context" error
  when this is undefined.

  `onExit` is called once when the child terminates (natural, killed,
  or error). The same instance that called `execBackground` is the
  exclusive owner of the callback — it's not a multi-cast bus. Hosts
  wire this to the agent's pending-notification queue so the model
  gets a `<task-notification>` on its next turn.

  `onExit` is OPTIONAL: remote / durable contexts may have no legal
  way to push a callback from a timer back into the host (a replay
  journal, for instance, forbids out-of-band writes). Such contexts
  record the exit in their registry; the agent loop reconciles
  by polling `ExecutionContext.listBackground` at run
  boundaries, where a host journal wrapper is legal (the
  `background:reconcile` hook is the journalable seam).

  See `docs/RUN_IN_BACKGROUND.md` for the broader design contract
  (file location, replay semantics, suppression rules).
- `execStream?` (`(handle: ExecutionHandle, command: string, options?: { cwd?: string; env?: Record<string, string>; signal?: AbortSignal }) => Promise<StreamHandle>`): Open a LIVE bidirectional byte stream to a long-lived process in the
  context. The third execution mode, alongside the buffered `exec`
  (request/response) and `execBackground` (fire-and-forget into a log
  file): neither of those can carry a protocol that interleaves reads
  and writes over the child's lifetime.

  Motivating consumer: stdio MCP servers. An MCP server is an ordinary
  process doing `fs.readFileSync` / `fs.writeFile` against whatever
  filesystem it was spawned on. Spawned on the host (the default — see
  `McpServerConfig.scope`) it sees the HOST filesystem while the agent's
  own tools see the context's, so the two disagree about what a path
  means. Routing the server through this seam puts both on the same
  filesystem.

  Optional — a context backed by a request/response-only API can't
  implement it. Advertise support via `capabilities.stream`; callers
  check that (or `execStream !== undefined`) and degrade with a clear
  error rather than silently falling back to a host spawn, which would
  reintroduce exactly the split this seam exists to close.

  IMPLEMENTOR CONTRACT — the shape is deliberately the SUPERSET of what
  the backing APIs can do, because they do not agree:

  - `stdout` / `stderr` are byte streams, never pre-decoded strings.
    A provider SDK that hands back `string` has already destroyed any
    multibyte codepoint split across a chunk boundary; the caller
    needs the raw bytes to run its own streaming decoder.
  - `stderr` is OPTIONAL because a PTY-backed context physically
    cannot separate it — one terminal, one stream. Such contexts
    redirect the child's stderr to a file; they may then surface it
    here out-of-band (the Daytona context polls that file back), but
    they are not obliged to, so consumers must tolerate its absence.
  - A PTY-backed context MUST also neutralise terminal semantics
    before the child's first byte (echo of everything written to
    stdin, `\n`→`\r\n` rewriting, and the \~4096-byte canonical-mode
    line cap that arbitrary protocol frames exceed). In practice:
    launch through `stty raw -echo; exec <cmd> 2><file>`.
  - `exited` resolves with the exit code. On a PTY the child is an
    interactive shell that never exits on its own, so `kill()` is
    the normal termination path, not an abnormal one.
  - Providers that impose a default command timeout (E2B and Daytona
    both default to 60s) MUST disable or maximise it here. A stream
    opened by this method is expected to live for the whole session.
- `fileSize?` (`(handle: ExecutionHandle, path: string) => Promise<number | null>`): Cheap size probe for a file in the context's filesystem, in bytes.
  `null` = unknowable (missing file, not a regular file, probe failed,
  or containment rejected the path — a probe must not leak even the
  size of files outside the workspace root).

  Exists because `readFile` has no partial-read form: tools that must
  materialize a whole file (read_file's text path, edit/multi_edit,
  grep's no-rg fallback) consult this first to refuse multi-GB targets
  instead of OOMing the HOST — the read lands in host memory no matter
  where the file lives. Per-context posture:

  - `process` — implements it (local stat, µs).
  - `docker`  — implements it via `wc -c` through its exec; its
    `readFile` gets transport headroom above the tool ceiling so
    the probe (tool policy), not the 10MB exec cap, decides read
    limits — same experience as every other context.
  - `sandbox` (and the e2b / daytona providers behind it) —
    implements it via `wc -c` through the provider's exec: one extra
    round-trip per whole-read tool call, paid so a multi-GB sandbox
    file is refused BEFORE the SDK transfers it into host memory.

  KNOWN RESIDUAL (sandbox providers): this guards the `readFile` path
  only. Their `exec` output is buffered inside the provider SDKs
  (e2b `commands.run`, daytona `executeCommand`), which we cannot cap
  from this side — a command that streams GBs to stdout within its
  timeout window can still balloon host memory. Docker doesn't share
  the residual (we own its transport; see `output-cap.ts`). A shell
  wrapper (`| head -c`) was considered and rejected: preserving exit
  codes, stderr separation, and timeout semantics through a pipe for
  EVERY command is fragile, and the timeout already time-bounds the
  exposure.
- `getMappedPort?` (`(handle: ExecutionHandle, containerPort: number) => Promise<number | null>`): Resolve the host-side port that a container port was published on
  (docker context only, and only when the matching entry was created
  with `host` omitted in `SpawnConfig.ports`).

  Resolves to `null` if the container port isn't published. Other
  contexts (process, sandbox) don't implement this; they don't have
  a port-mapping concept.
- `killBackground?` (`(handle: ExecutionHandle, taskId: string) => Promise<TaskExitInfo | null>`): SIGTERM the whole process group of a running background task.
  Idempotent — second call returns `null` (or the cached exit info).
  Resolves once the process has exited AND its output stream has
  been flushed + closed.

  `null` return on miss (unknown id, already cleaned up) so the
  shell_kill tool can surface a clean "no such task" message
  without throwing.
- `listBackground?` (`(handle: ExecutionHandle) => Promise<readonly TaskEntry[]>`): Snapshot of every task in the context's registry — running AND
  terminated (entries remain until the next context destroy, so
  the model can still read output of exited tasks).
- `listFiles` (`(handle: ExecutionHandle, path: string, options?: ListFilesOptions) => Promise<ListFilesEntry[]>`): List files in the context filesystem.

  Plain paths list immediate directory entries. Paths that
  contain glob metacharacters (`*`, `?`, `[]`, `{}`) request matched file
  enumeration inside the target context. Contexts that cannot support
  globbing should throw a clear error rather than silently returning no
  matches. `metadata` is best-effort and only required when the context can
  provide it without expensive per-file shell calls.
- `openTerminal?` (`(handle: ExecutionHandle, options: TerminalOptions) => Promise<TerminalProcess>`): Open an interactive terminal (a PTY running the user's login shell) in
  the context: on the host, in the container, in the sandbox. For people,
  not protocols: the opposite of `execStream`, which defeats every
  terminal semantic this one keeps (echo, line editing, job control,
  window size).

  Optional: a context without a PTY primitive (or a runtime without one:
  the local contexts need Bun) leaves it undefined, and callers offer no
  terminal rather than falling back to a host shell.
- `readFile` (`(handle: ExecutionHandle, path: string) => Promise<string>`): Read a file from the context's filesystem
- `readFileBinary?` (`(handle: ExecutionHandle, path: string) => Promise<Uint8Array<ArrayBufferLike>>`): Read a file from the context's filesystem as raw bytes.

  Used by `read_file` to dispatch image / binary files into the multimodal
  `ToolResultContent[]` route. Optional — when not implemented, the tool
  falls back to `base64 < path` via the `exec` seam, which works in any
  shell-capable context. Implementations that already have a native
  binary read (in-process `fs.readFile` without encoding, container API,
  sandbox SDK) should override for the latency win.
- `reassignBackgroundTasks?` (`(fromHandle: ExecutionHandle, toHandle: ExecutionHandle, newOnExit?: (info: TaskExitInfo) => void, newOnStall?: (info: TaskStallInfo) => void) => Promise<readonly TaskEntry[]>`): Transfer ownership of every still-running task from `fromHandle`
  to `toHandle`. Used by the spawn tool to "promote" a subagent's
  background tasks up to the parent's handle so they outlive the
  subagent's destroy() — matching shell semantics, where a `&`-ed
  command outlives the parent process.

  With `fromHandle === toHandle` ownership stays put and only the
  callbacks are rebound: a host that keeps one handle across agent
  rebuilds (`chat/execution-cache.ts`) hands the running tasks to the
  freshly built agent this way.

  Side effects:

  - The task's `handleId` is rewritten, so subsequent
    `listBackground(toHandle)` / `killBackground(toHandle, …)` /
    `destroy(toHandle)` see it (and `fromHandle`-scoped operations
    don't).
  - When `newOnExit` / `newOnStall` are provided, the task's OWNER
    callbacks are REPLACED with them. Critical: they were captured
    against the spawning agent's hook bus, which is about to be
    destroyed; without rewiring, the new owner never learns when the
    task terminates or stalls. Pass closures that fire the new owner's
    `background:exit` / `background:stall` hooks. In-flight
    `killBackground` / `waitBackground` resolvers are not owner
    callbacks and keep firing.

  Returns the entries that were actually reassigned (running tasks
  only; terminated ones stay where they are). Implementations that
  don't support reassignment can leave this undefined; the caller
  (spawn.ts) falls back to the current behavior of killing child
  tasks at subagent shutdown.
- `reconnectTerminal?` (`(handle: ExecutionHandle, id: string, options: TerminalOptions) => Promise<TerminalProcess | null>`): Re-attach to a terminal opened earlier, by its `TerminalProcess.id`,
  after the host lost the connection (daemon restart, paused sandbox).
  Resolves `null` when it is gone. Only contexts whose terminals outlive
  the host (remote sandboxes) implement it.
- `spawn` (`(config?: SpawnConfig) => Promise<ExecutionHandle>`): Spawn a new execution environment
- `type` (`ContextType`, readonly): Context type identifier
- `waitBackground?` (`(handle: ExecutionHandle, taskId: string, options?: { signal?: AbortSignal; timeoutMs?: number }) => Promise<TaskExitInfo | null>`): Block until a background task terminates, then resolve with its
  exit info. Resolves immediately for already-terminated tasks.

  Returns `null` when:

  - the task id is unknown (or owned by another handle — same
    isolation contract as `killBackground`), OR
  - `timeoutMs` elapsed / `signal` aborted before the task exited.

  Callers that need to distinguish "unknown" from "still running"
  should consult `listBackground` first (the `wait_task` tool does).

  This is the injectable wait seam for the `wait_task` tool:
  in-process contexts implement it on the existing `onExit`
  machinery; durable hosts implement it as an awakeable park
  (runner fires task-exit over their bridge → resolveAwakeable).
- `writeFile` (`(handle: ExecutionHandle, path: string, content: string | Buffer<ArrayBufferLike>, encoding?: BufferEncoding) => Promise<void>`): Write a file to the context's filesystem.

  `encoding` applies only when `content` is a string (how its bytes are
  produced; defaults to utf-8) and is ignored for a Buffer, which is
  written verbatim.

Source: [src/contexts/types.ts:388](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L388)

### `ExecutionHandle`

Interface.

Opaque handle to a running execution context instance

```ts
interface ExecutionHandle
```

**Properties**

- `cwd` (`string`): Working directory within the context
- `id` (`string`): Identifier of the spawned instance, unique within its context.
- `type` (`ContextType`): Type of the context that created the handle.

Source: [src/contexts/types.ts:90](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L90)

### `IsolationPolicy`

Interface.

Policy for an OS-level sandbox around shell commands.

```ts
interface IsolationPolicy
```

**Properties**

- `network` (`'allow' | 'deny'`): `'allow'` = unrestricted network (the default: `bun install`, `git
  fetch`, `gh` keep working). `'deny'` = no network access. Without network
  isolation a compromised command can still exfiltrate what it can read —
  the setting description says so.
- `unreadableDirs?` (`string[]`): Directories hidden from the sandboxed command (read AND write denied).
  Defaults to `DEFAULT_UNREADABLE_DIRS` when omitted.
- `writableRoots` (`string[]`): Directories the sandboxed command may write to (create, modify,
  delete). Absolute paths; callers should pass them through
  [`resolveWritableRoots`](https://zidane.si/docs/reference/contexts.md#resolveWritableRoots) so symlinked locations (`/tmp` →
  `/private/tmp` on macOS) match what the kernel sees.

Source: [src/contexts/isolation.ts:37](https://github.com/Tahul/zidane/blob/main/src/contexts/isolation.ts#L37)

### `ListFileMetadata`

Interface.

A listed file with optional metadata.

```ts
interface ListFileMetadata
```

**Properties**

- `mtimeMs?` (`number`): Last modification time, in milliseconds since the epoch.
- `path` (`string`): Path of the file.
- `size?` (`number`): Size in bytes.

Source: [src/contexts/types.ts:142](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L142)

### `ListFilesOptions`

Interface.

Options for `ExecutionContext.listFiles`.

```ts
interface ListFilesOptions
```

**Properties**

- `gitignore?` (`boolean`): Skip git-ignored files when globbing. Default `true`. Contexts that can
  resolve git's ignore rules (e.g. the local process context) honor it; the
  rest treat it as a no-op. Set `false` to match every file, ignored or not.
- `glob?` (`boolean`): Treat `path` as a glob match pattern instead of a literal directory path.
- `limit?` (`number`): Maximum number of entries to return.
- `metadata?` (`boolean`): Return [`ListFileMetadata`](https://zidane.si/docs/reference/contexts.md#ListFileMetadata) objects instead of plain paths.
- `signal?` (`AbortSignal`): Abort the listing early — forwarded from the calling tool's `ctx.signal`.

Source: [src/contexts/types.ts:155](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L155)

### `NodeEntryPortOptions`

Interface.

Options of [`nodeEntryPort`](https://zidane.si/docs/reference/contexts.md#nodeEntryPort).

```ts
interface NodeEntryPortOptions
```

**Properties**

- `entry?` (`(path: string) => Promise<string>`): Same, for the entry itself, its own link never followed (stat, move, remove, a target to create). Default: `resolve`.
- `resolve` (`(path: string) => Promise<string>`): A path as the caller names it → the absolute path to act on; throws when it is out of bounds.
- `trash?` (`(absolute: string) => Promise<void>`): Removes `absolute` recoverably (the system trash). Default: removed for good.
- `writable?` (`(absolute: string, path: string) => Promise<void>`): Throws when the caller may not change `absolute` (a sandbox's writable roots). Default: anything goes.

Source: [src/contexts/entries.ts:26](https://github.com/Tahul/zidane/blob/main/src/contexts/entries.ts#L26)

### `OwnedProcess`

Interface.

One process the app started for itself.

```ts
interface OwnedProcess
```

**Properties**

- `pid` (`number`): The child's pid.
- `role` (`OwnedProcessRole`): What the app started it for.

Source: [src/contexts/owned-processes.ts:17](https://github.com/Tahul/zidane/blob/main/src/contexts/owned-processes.ts#L17)

### `SessionProcess`

Interface.

One process a session started.

```ts
interface SessionProcess
```

**Properties**

- `pid` (`number`): The child's pid.
- `sessionId` (`string`): The session it runs for, by its id on this host.

Source: [src/contexts/session-processes.ts:15](https://github.com/Tahul/zidane/blob/main/src/contexts/session-processes.ts#L15)

### `SpawnConfig`

Interface.

Configuration for `ExecutionContext.spawn`. Not every context honors every field.

```ts
interface SpawnConfig
```

**Properties**

- `cwd?` (`string`): Working directory (created if it doesn't exist)
- `destroyGraceMs?` (`number`): How long `destroy()` waits for a SIGTERM'd background task to settle
  before escalating to SIGKILL and abandoning the wait (process context
  only). Bounds `agent.destroy()` against tasks that trap SIGTERM or
  whose stdio pipes are held open by escaped grandchildren.

  Default: `5000`.
- `env?` (`Record<string, string>`): Environment variables
- `hardening?` (`ContextHardening`): Container hardening options (docker context only).

  All fields are opt-in and OFF by default to preserve existing behavior
  (containers run with the image's default user/capabilities). Enable them
  for untrusted workloads. `dropAllCapabilities` and a non-root `user`
  (see `SpawnConfig.user`) can break images that expect root or
  specific capabilities, so they are not applied unless requested.
- `image?` (`string`): Docker image (only for 'docker' context)
- `inheritEnv?` (`boolean`): Whether spawned shells inherit the parent `process.env` (process context).

  Default `true` (preserves existing behavior — children see the parent
  environment including anything Bun auto-loaded from `.env`). Set `false`
  for untrusted workloads so secrets in the parent environment aren't
  readable by tool commands; only `env` / per-call env plus a minimal base
  (`PATH`, `HOME`, `SHELL`, `LANG`, `LC_ALL`, `USER`, `TERM`, `TMPDIR`) are
  passed through.
- `isolation?` (`IsolationPolicy`): OS-level sandboxing of shell commands (process context only). When set,
  every `exec` / `execStream` / `execBackground` runs `/bin/sh -c` under
  macOS `sandbox-exec` or Linux `bwrap`: filesystem writes are limited to
  `writableRoots`, credential directories are hidden, and network is
  `allow`ed or `deny`ed as a whole. `writeFile` applies the same
  `writableRoots` check in-process (reads are unrestricted — this is the
  Claude Code / Codex boundary, not a full jail; use `workspaceRoot` for
  read containment).

  Fails closed: when the backend is unavailable on the host, every command
  returns exit 127 with the reason instead of running unsandboxed.

  Build the policy with `resolveWritableRoots` from `contexts/isolation`.
- `labels?` (`Record<string, string>`): Docker labels to attach to the container (docker context only).

  Useful for ownership tracking — callers can sweep abandoned
  containers (e.g. from a crashed parent process) by filtering on
  a label they own: `docker ps -aq --filter label=my-app=true`.
- `limits?` (`{ cpu?: string; memory?: number; timeout?: number }`): Resource limits
- `mounts?` (`ContextMount[]`): Host paths mounted into the context (only for 'docker' context today)
- `name?` (`string`): Docker container name prefix (only for 'docker' context)
- `network?` (`string`): User-defined Docker network to join (docker context only).

  Defaults to Docker's default bridge. Use a user network when you
  need multiple sibling containers (e.g. agent + database + dev
  server) to discover each other by name.
- `ports?` (`{ container: number; host?: number; proto?: 'tcp' | 'udp' }[]`): Publish container ports on the host (docker context only).

  Each entry maps a container port to either an explicit host port or
  (when `host` is omitted) a Docker-assigned random port. Retrieve the
  actual host port at runtime via `getMappedPort(handle, containerPort)`
  on the docker context.
- `sandbox?` (`{ [key: string]: unknown; apiKey?: string; provider: string; sandboxId?: string }`): Sandbox provider config (only for 'sandbox' context)
- `sessionId?` (`string`): The session this context runs commands for (process context only): each
  command it starts (`exec`, `execStream`, `execBackground`) is recorded in
  the session-process registry (`listSessionProcesses`) while it runs.
- `user?` (`string`): UID/GID the container should run as (docker context only).

  Accepts the same forms Docker's `--user` does: `uid`, `uid:gid`, or a
  named user that exists in the image. Default is the image's default
  user (typically root). Setting this to the host user's `uid:gid`
  avoids the EACCES-on-cleanup problem when sharing a workspace via
  a `shared` mount.
- `workspaceRoot?` (`string`): Optional workspace-containment root for the file methods (`readFile`,
  `writeFile`, `listFiles`) of the process context.

  When set, a resolved path that escapes this root (via absolute path,
  `..`, or a symlink pointing outside) is rejected before any I/O. Default
  unset = no containment (back-compat; the agent can read/write anywhere
  the host process can). Set this for untrusted workloads to confine file
  tools to a directory. Shell commands are not covered — use a sandbox /
  docker context for full isolation.

Source: [src/contexts/types.ts:177](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L177)

### `StreamHandle`

Interface.

A live bidirectional stream to a process in the context. Returned by
`ExecutionContext.execStream`; see that method for the full
implementor contract (why the streams are bytes, why `stderr` is
optional, what PTY-backed contexts must do before the first byte).

Ownership: the caller owns the lifetime. Nothing reaps this handle
automatically, so `kill()` belongs in the consumer's teardown path —
for MCP that is the transport's `close()`, reached via the agent's
`mcpConnection.close()` during `destroy()`.

```ts
interface StreamHandle
```

**Properties**

- `exited` (`Promise<number>`): Resolves with the exit code when the child terminates, including
  termination via `kill`. Never rejects.
- `kill` (`() => Promise<void>`): Terminate the child. Idempotent — killing a dead child is a no-op.
- `pid` (`number`): Process id inside the context — NOT meaningful on the host.
- `stderr?` (`AsyncIterable<Uint8Array<ArrayBufferLike>, any, any>`): Child's stderr, when the implementation can provide it separately.
  Present on pipe-backed contexts. A PTY-backed context cannot split it
  from the terminal stream, so it redirects stderr to a file and either
  surfaces it here out-of-band or leaves this unset — expect a lower
  latency guarantee there than on stdout.
- `stdout` (`AsyncIterable<Uint8Array<ArrayBufferLike>>`): Child's stdout as raw byte chunks, in emit order.

  CONSUMER OBLIGATION: drain this (and `stderr`) for the stream's
  lifetime. Nothing here is bounded, and the consequence of ignoring it
  varies by backend and by host runtime, none of it good:

  - `process` / `docker` hand back real OS pipes. Under Node an unread
    stream stops draining the fd and the child BLOCKS once the pipe
    fills (\~64 KiB) — a deadlock, not a slowdown. Under Bun the runtime
    buffers eagerly instead, so it presents as unbounded host memory
    (measured: 32 MiB of unread stderr never blocked the child).
  - `e2b` / `daytona` buffer in a `StreamQueue`, which grows without
    limit. It cannot do otherwise: both SDKs push output through a
    synchronous `(data) => void` callback, so there is no channel to
    apply backpressure, and dropping bytes would desynchronise a framed
    protocol permanently. The queue logs a one-shot warning once the
    unread backlog passes 8 MiB so a stalled consumer is diagnosable.

  `createContextStdioTransport` satisfies this for both streams.
- `write` (`(data: Uint8Array) => Promise<void>`): Write to the child's stdin. Resolves once the bytes are handed off.

Source: [src/contexts/types.ts:857](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L857)

### `TaskEntry`

Interface.

One row in `listBackground`'s snapshot. Living entries (status `'running'`)
have `exitCode` / `signal` / `endedAt` unset; terminated entries carry the
same data `TaskExitInfo` returned at exit time.

```ts
interface TaskEntry
```

**Properties**

- `bytesWritten` (`number`): Total bytes written to the output file so far — useful for "task X has produced N KB" UX hints.
- `command` (`string`): Command the task runs.
- `cwd` (`string`): Working directory of the task.
- `endedAt?` (`number`): Set when the task terminated. Lets pull-based consumers (the agent's
  run-boundary reconcile) derive `durationMs = endedAt - startedAt`
  without depending on when the snapshot was taken.
- `exitCode?` (`number`): Exit code. Set once the task terminated.
- `outputPath` (`string`): Absolute path of the log file holding the task output.
- `pid` (`number`): Process id.
- `signal?` (`Signals`): Signal that terminated the process, when there was one.
- `startedAt` (`number`): Start time, in milliseconds since the epoch.
- `status` (`BackgroundTaskStatus`): Current state of the task.
- `taskId` (`string`): Id of the task, as in `TaskHandle.taskId`.

Source: [src/contexts/types.ts:933](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L933)

### `TaskExitInfo`

Interface.

Fired exactly once per task when the child process terminates, via
`execBackground`'s `onExit` callback. The agent layer translates this
into a queued `<task-notification>` for the next turn.

`signal` is set when the child was terminated by a signal (e.g.
SIGTERM from our own kill-tree, SIGKILL from oom-killer); absent on
natural exit. `exitCode` is `128 + signal-number` on signal-killed
children, matching POSIX shell conventions — `143` for SIGTERM, etc.

```ts
interface TaskExitInfo
```

**Properties**

- `command` (`string`): The original command string the model invoked — useful for telemetry / banner summary.
- `durationMs` (`number`): `Date.now()` delta between spawn and exit.
- `exitCode` (`number`): Exit code of the process.
- `outputPath` (`string`): Absolute path of the log file holding the task output.
- `signal?` (`Signals`): Signal that terminated the process, when there was one.
- `status` (`'exited' | 'killed'`): How the task ended.
- `taskId` (`string`): Id of the task, as in `TaskHandle.taskId`.

Source: [src/contexts/types.ts:911](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L911)

### `TaskHandle`

Interface.

Returned by `execBackground` once the process has spawned — the handle the model and
the framework use to refer to the task until it terminates.

`outputPath` is an absolute path to the log file the context is
appending stdout + stderr to (interleaved by emit order). The model
reads it via the normal `read_file` tool; no special tool is required.

```ts
interface TaskHandle
```

**Properties**

- `outputPath` (`string`): Absolute path to the log file the context is streaming output into.
- `pid` (`number`): OS pid of the spawned shell wrapper (process-group leader on POSIX).
- `taskId` (`string`): Stable id minted by the context — typically `bash_<n>` for
  `ProcessContext`. Sequential within a single context instance,
  resets when a new context is constructed. Forwarded to `killBackground` /
  `listBackground` and stamped into every `<task-notification>` block.
- `terminal?` (`boolean`): True when the task runs in a terminal (`execBackground`'s `terminal` option was honored).

Source: [src/contexts/types.ts:830](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L830)

### `TaskStallInfo`

Interface.

Fired by the optional stall watchdog (see `execBackground`'s
`stallTimeoutMs`) when a running task has produced no output for the
configured window. The process is still alive — this is a signal, not
a state transition.

```ts
interface TaskStallInfo
```

**Properties**

- `bytesWritten` (`number`): Total bytes the task has written so far.
- `command` (`string`): Command the task runs.
- `outputPath` (`string`): Absolute path of the log file holding the task output.
- `stalledForMs` (`number`): Milliseconds since the last output chunk (>= the configured window).
- `taskId` (`string`): Id of the stalled task.

Source: [src/contexts/types.ts:968](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L968)

## Types

### `BackgroundTaskStatus`

Type.

Lifecycle status of a background task.

- `'running'` — process is still live; `exitCode` / `signal` unset.
- `'exited'` — process terminated on its own (clean or non-zero exit).
- `'killed'` — the host issued `killBackground` (SIGTERM to the group).

The status is a coarse-grained signal; the exit code carries the
fine-grained detail (e.g. `143 = SIGTERM` for `'killed'`).

```ts
type BackgroundTaskStatus = 'running' | 'exited' | 'killed'
```

Source: [src/contexts/types.ts:820](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L820)

### `ContextType`

Type.

Kind of execution context: `process`, `docker` or `sandbox`.

```ts
type ContextType = 'process' | 'docker' | 'sandbox'
```

Source: [src/contexts/types.ts:371](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L371)

### `DetachedTasksCapability`

Type.

Lifetime guarantee of background tasks started through
`ExecutionContext.execBackground`.

- `'none'` — the context cannot detach tasks at all.
- `'process-lifetime'` — tasks live as long as the HOST process; a
  crash/restart of the host orphans or kills them (`ProcessContext`).
- `'durable'` — tasks live on a remote runner and survive host process
  death (remote execution contexts driven by durable runtimes).

Durable-execution runtimes consult this to decide whether backgrounding
is safe to expose (`disableBackgroundTasks: 'non-durable'`, the default
in replay mode): a `'durable'` context keeps its tasks across worker
crashes, so there is no reason to strip the capability from the model.

```ts
type DetachedTasksCapability = 'none' | 'process-lifetime' | 'durable'
```

Source: [src/contexts/types.ts:32](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L32)

### `EntryKind`

Type.

What a path is: a file (a symlink counts as one, never followed) or a folder.

```ts
type EntryKind = 'file' | 'dir'
```

Source: [src/contexts/types.ts:114](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L114)

### `IsolationBackend`

Type.

Sandbox backend: macOS Seatbelt or Linux bubblewrap.

```ts
type IsolationBackend = 'seatbelt' | 'bwrap'
```

Source: [src/contexts/isolation.ts:60](https://github.com/Tahul/zidane/blob/main/src/contexts/isolation.ts#L60)

### `ListFilesEntry`

Type.

One listing entry: a plain path, or a [`ListFileMetadata`](https://zidane.si/docs/reference/contexts.md#ListFileMetadata) object when metadata was requested.

```ts
type ListFilesEntry = string | ListFileMetadata
```

Source: [src/contexts/types.ts:152](https://github.com/Tahul/zidane/blob/main/src/contexts/types.ts#L152)

### `OwnedProcessRole`

Type.

Why the app started the process.

```ts
type OwnedProcessRole = 'mcp' | 'tool'
```

Source: [src/contexts/owned-processes.ts:14](https://github.com/Tahul/zidane/blob/main/src/contexts/owned-processes.ts#L14)

## Re-exports

These names are documented on the entry point that owns them.

- [zidane/contexts/sandbox](https://zidane.si/docs/reference/contexts-sandbox.md): [`createSandboxContext`](https://zidane.si/docs/reference/contexts-sandbox.md#createSandboxContext), [`SandboxProvider`](https://zidane.si/docs/reference/contexts-sandbox.md#SandboxProvider)
