AIP-17: RUNNER.md — shared process boundary block
A composable schema block defining the `runner` field — engine (in-process / subprocess / sandbox), optional container image, declarative dependency needs, and resource limits — reused by every manifest format that runs code. Permissions (secrets, network) and IO are defined elsewhere; this block scopes only to the process boundary.
| Field | Value |
|---|---|
| AIP | 17 |
| Title | RUNNER.md — shared process boundary block |
| Status | Draft |
| Type | Schema |
| Domain | runner.sh |
| Requires | AIP-1, AIP-2, AIP-7 |
| Resources | ./resources/aip-17 — SKILL.md, ADAPTER.md, RUNNER.schema.json |
Abstract
This AIP defines the runner schema block — engine (where the
process runs), image (which container template), needs
(declarative dependency requirements), and limits (resource
caps) — and the defineRunner(...) standard signature that
consumes it.
runner describes a single concern: the process boundary that
hosts the code. It does NOT cover what code runs (see
AIP-26 CODE.md), what data flows in/out
(AIP-16 IO.md), what env vars / secrets the body
sees (AIP-19 SECRETS.md), or what hosts it can reach
(top-level network block).
Earlier drafts of this AIP bundled all five concerns under a single
runtime block. The 2026-04-30 revision narrows the scope: each
concern is now its own block at the manifest top level. The legacy
runtime shape is preprocessed to the new layout for backward
compatibility.
This is a schema-block AIP, not a file format users author
directly. There is no RUNNER.md file checked into a repo. The
block is referenced by other manifest specs via JSON Schema $ref.
Motivation
Tools, workflows, and forthcoming runnable manifest types all face the same question: how should the host execute the body? That question alone has four sub-axes:
- Where: in-process vs subprocess vs container.
- What image: which container template provides the OS-level dependencies the body needs.
- What deps: which language packages and native binaries the body declares as required at startup.
- How much: CPU, memory, wall-clock caps.
Stuffing permissions (env, fs, network) into the same block — as
the legacy runtime shape did — confused the boundary. Permissions
are what the body is allowed to do; the runner is the process
that hosts it. Splitting clarifies both: a host can pick a
different runner without renegotiating permissions, and a permission
audit doesn't have to wade through engine choices.
Design principles
-
Subprocess by default. A manifest without an explicit
runnerblock is treated asengine: "subprocess"— the lightest isolation that still puts the body in its own process. -
Privilege is host-controlled. The manifest requests; the host decides. A request for
engine: "in-process"from an untrusted source MUST be silently downgraded tosubprocess. The downgrade MUST be observable (logged + flagged in the resolved handle). -
Image is declarative or auto-picked. The manifest MAY name an
image:explicitly (e.g.mcp-node-server) or leave it unspecified — in which case the host picks the lightest image matchingneeds:. Auto-pick is deterministic given the same needs + same registry. -
Needs are pull, not push.
needs.nativedeclares what the body needs (weasyprint,ffmpeg); the host figures out where that lives (apt package, brew formula, pre-baked image). The manifest doesn't sayapt-get install. -
Limits are advisory but enforceable. Hosts SHOULD enforce
timeout_ms/memory_mb/cpu_mswhen the underlying isolation primitive supports it. Hosts that can't enforce a cap SHOULD warn at registration. -
Single concern. This block does NOT mention env, fs, network, secrets, or IO. Those live in their own blocks. Bodies that mix isolation requests with permission requests should look at AIP-19 (secrets), AIP-16 (IO/files), and the top-level
networkblock.
Specification
The runner block
runner:
engine: sandbox # "subprocess" (default) | "sandbox" | "in-process"
image: mcp-node-server # optional; sandbox engine only — auto-picked when omitted
needs: # optional; declarative dependency requirements
language: node # "node" | "python" | "multi"
native: [weasyprint] # OS packages (Debian/Ubuntu apt names by default)
npm: [stripe@^11.0.0] # added on top of bundle's package.json
pip: [requests==2.31.0] # added on top of bundle's requirements.txt
limits: # optional; resource caps
memory_mb: 1024
timeout_ms: 60000
cpu_ms: 30000runner.engine
| Value | Meaning |
|---|---|
"subprocess" (default) | Run the body in an isolated host-local subprocess. Lightweight: shares the host kernel, isolation enforced via OS primitives (Node --permission, bwrap, etc.). Suitable for pure-language tools that don't need native deps or child_process. |
"sandbox" | Run the body in a real container provisioned through the host's sandbox driver (E2B, Modal, Fly Machines, …). Full Linux runtime — child_process, native deps, free /tmp, network gating enforced by the driver. Use when the body needs npm install, system binaries, or capabilities outside the host's permission model. |
"in-process" | Run the body in the host's own process via dynamic import. Reserved for trusted, host-shipped code. The host MUST refuse in-process for any manifest whose source originates outside the host's trust boundary; the downgrade is silent (→ subprocess) and logged. |
runner.image
Optional. Names a sandbox template id from the host's registry. Only
meaningful when engine: "sandbox". When omitted, the host
auto-selects via the algorithm in AIP-26 § Cache invalidation
- the host's template registry: pick the lightest template that
satisfies all
needs.nativepackages andneeds.language.
The reference Guilde implementation maintains an open registry at
packages/mcp/core/src/sandbox/registry.ts with templates including
mcp-node-server, mcp-python-server, mcp-browser-server, plus
custom templates teams publish.
Hosts MAY refuse manifests requesting unknown image ids — the error SHOULD list available templates so authors can correct.
runner.needs
Declarative dependency requirements. Hosts SHOULD use these to (a) auto-pick an image, (b) install missing dependencies at cold-start, and (c) surface them in trust UIs.
| Field | Type | Meaning |
|---|---|---|
language | "node" | "python" | "multi" | The primary language runtime the body uses. Hosts MAY refuse manifests where language doesn't match the inferred extension of run (AIP-26). |
native | string[] | OS-level package names. Default convention: Debian/Ubuntu apt names. Hosts running on other distros MAY map names if reliable, otherwise refuse. |
npm | string[] | Additional npm packages. Format: <name>@<semver>. Installed via npm install AFTER npm ci (lockfile-driven baseline). |
pip | string[] | Additional pip packages. Format: <name>==<version> recommended. Installed via pip install --user. |
needs is purely advisory for engine: "in-process" and engine: "subprocess" — the host trusts the bundle's own dependency
manifests (package.json, requirements.txt) for those engines. For
engine: "sandbox", hosts MUST honour needs at cold-start.
runner.limits
| Field | Default | Meaning |
|---|---|---|
memory_mb | host default (RECOMMENDED 256) | Hard memory cap when the isolation primitive supports it. |
timeout_ms | host default (RECOMMENDED 600000 = 10 min) | Wall-clock cap on the run. The host kills the process past this. |
cpu_ms | host default (no cap) | CPU-time cap. Hosts that can't enforce it ignore the field. |
Hosts MAY add fields for their isolation backend (e.g.
vcpus, ephemeral_storage_mb); portable manifests SHOULD stay
within the listed set.
Defaults
| Missing | Treated as |
|---|---|
runner (whole block) | engine: "subprocess", no image, no needs, host's default limits |
runner.image | auto-pick via needs + host registry |
runner.needs | language inferred from run extension; no extra packages |
runner.limits | host's default caps |
The defineRunner standard signature
Every implementation that consumes the runner block MUST expose a function whose signature matches the contract below.
defineRunner (TypeScript notation, normative)
defineRunner(definition: RunnerDefinition): RunnerHandle
interface RunnerDefinition {
engine?: "in-process" | "subprocess" | "sandbox"
image?: string
needs?: {
language?: "node" | "python" | "multi"
native?: string[]
npm?: string[]
pip?: string[]
}
limits?: {
memory_mb?: number
timeout_ms?: number
cpu_ms?: number
}
}
interface RunnerHandle {
engine: "in-process" | "subprocess" | "sandbox"
image: string | null // resolved (auto-pick or explicit)
needs: {
language: "node" | "python" | "multi"
native: string[] // never undefined; defaults to []
npm: string[]
pip: string[]
}
limits: {
memory_mb: number
timeout_ms: number
cpu_ms: number
}
/**
* Apply the host's downgrade rule. Pass the manifest's source
* origin; if it's untrusted (workspace, ai-draft, …) and the
* declared engine is "in-process", returns a copy with engine
* forced to "subprocess" and `downgraded: true`.
*/
resolveForOrigin(origin: string): RunnerHandle & { downgraded: boolean }
}Conformance rules
-
Canonical name. The export MUST be named
defineRunner. -
Defaults applied at the boundary. A
RunnerHandlereturned bydefineRunnerMUST have every field populated — empty arrays, default caps, resolved image (ornullfor non-sandbox engines). Bodies and adapters MUST NOT have to re-default. -
Downgrade is host policy, not manifest choice. A manifest that requests
in-processfrom an untrusted origin MUST be downgraded tosubprocess. The downgrade MUST be observable (via thedowngradedflag and a warning log). -
Image resolution at handle time. When
engine: "sandbox"andimage:is omitted, the host MUST resolve to a concrete image id in the handle (deterministic given needs + registry). Whenengine: "subprocess"orengine: "in-process",image:isnullin the handle. -
No I/O at module load. Same rule as
defineCode/defineIO/defineTool— the module containingdefineRunner(...)MUST be safely importable without side effects.
Implementer's guide
For step-by-step guidance on building a defineRunner implementation
across engines (Node --permission, E2B, bwrap, microVM), see
./resources/aip-17/draft/ADAPTER.md.
Compatibility
Legacy runtime block (pre-2026-04-30)
The previous draft of this AIP combined engine + env + fs + network
- resources under a single
runtime:block. Hosts MUST accept the legacy shape and preprocess it to the new layout:
runtime.mode → runner.engine
("sandbox" → "subprocess", the legacy
value mapped to host-local subprocess
isolation; new "sandbox" value reserved
for real-container engine)
runtime.env: ["X"] → secrets: { X: { vault: x.toLowerCase().replace(/_/g, "-") } }
(per AIP-19 binding contract)
runtime.fs.read → inputsFiles (per AIP-16, key-by-key migration)
runtime.fs.write → outputsFiles (per AIP-16)
runtime.network.egress → network.egress (top-level, no `network:` wrapper if absent)
runtime.cpu_ms_max → runner.limits.cpu_ms
runtime.memory_mb_max → runner.limits.memory_mb
runtime.timeout_ms → runner.limits.timeout_msHosts SHOULD warn (not error) on legacy shape during the deprecation window, then remove the preprocessor in a future revision.
With AIP-26 CODE
runner resolves the process boundary; AIP-26 resolves the bundle
contents. Both blocks live at the manifest top level, independent.
A code-workspace declares its own default runner; consuming tools
inherit and MAY override field-by-field.
With AIP-19 SECRETS
The legacy runtime.env allowlist moves to the AIP-19 secrets:
binding block. runner no longer mentions credentials — that's a
permission concern, not a process-boundary concern.
With AIP-16 IO
The legacy runtime.fs.{read,write} move to inputsFiles /
outputsFiles (AIP-16) for per-run staging semantics. Persistent
mounts are not part of the AIP family — workspace tools are
stateless across calls by design.
Security considerations
-
engine: "in-process"from untrusted source. Silent downgrade is the default; hosts that want to also AUDIT the attempt SHOULD emit a high-severity log when the downgrade fires. -
Image registry trust. Custom images MUST be reviewed before being added to the host registry — they run with the body's privileges. The registry itself is the trust boundary, not individual manifests.
-
needs.nativeinjection. A manifest that declares a misleadingnative: [...]could pull packages the trust UI doesn't surface. Hosts SHOULD list the resolvednative:packages in the trust UI alongside other permissions. -
Resource cap enforcement. A host that accepts
limits.timeout_ms: 60000but cannot enforce it lets a runaway body burn host resources. Hosts SHOULD log a warning at registration when caps are unenforceable. -
Cross-engine privilege drift. A manifest that switches
engine: "sandbox"→"subprocess"between revisions changes the trust profile (different isolation guarantees, different fs semantics). Hosts SHOULD treat any change toengineas cause for re-review.
Open questions
-
Engine taxonomy beyond 3. Future engines (microVM, WASM, edge workers) — keep the enum closed and add per-engine, or open the enum with host-specific values?
-
Resource caps per language.
memory_mbfor Node maps to--max-old-space-size; for Python it's vaguer. Whether to keep one number or split per-language. -
Image inheritance. When a code-workspace declares an image and a tool that uses it declares another, who wins? Current draft: tool overrides workspace; should be normative.
-
needs.systemvsneeds.native. Whether to split apt-style packages from generic system requirements (e.g. "needs a writable /tmp larger than 100 MB"). Probably YAGNI for v1.
Composition pattern (inline | ref | file)
Like every composable block in the AIP series (STORAGE, SANDBOX,
SECRETS, IDENTITY-ref, CODE), runner accepts three forms when
embedded in a parent manifest:
# Inline — the block defined directly in the parent
runner:
inline:
engine: subprocess
needs: { language: python }
# Ref — a registry-resolvable identifier
runner:
ref: "@agentik/runners/python-3.12"
# File — a workspace-relative path to a sibling RUNNER.md
runner:
file: "./RUNNER.md"A consumer (e.g. CODE.md, DRIVER.md) MAY use any of the three.
Validators MUST accept all three and resolve at use-time. Inline
and the standalone RUNNER.md file form share the same frontmatter
schema (this AIP).
See also
- AIP-14 — TOOL.md — primary consumer
- AIP-15 — WORKFLOW.md — primary consumer
- AIP-16 — IO.md — sibling block (data flow)
- AIP-19 — SECRETS.md — sibling block (env/secret binding)
- AIP-23 — IDENTITY.md — sibling block (identity-ref)
- AIP-26 — CODE.md — sibling block (bundle composition)
- AIP-30 — DRIVER.md — primary consumer (binding)
- AIP-37 — LIFECYCLE.md — event vocabulary referenced by lifecycle hooks
- AIP-7 — governance, approval, audit
./RUNNER.schema.json— schema validator./ADAPTER.md— implementer's guide
Resources
Supporting artifacts for AIP-17. Links open the file on GitHub — markdown and JSON render natively in GitHub's viewer. Browse the full resource tree →
AIP-16: IO.md — shared input/output schema blocks
A composable schema block defining `inputs`, `outputs`, `inputsFiles`, and `outputsFiles` — the data-shape primitives reused by every manifest format that needs to declare what flows in and out of a runnable unit.
AIP-18: COLLECTION.md — collections/v1 (typed collections + items)
A composable primitive pack that lets any AIP define domain-extensible item types as on-disk schema files (`COLLECTION.md`) instantiated by markdown records (`ITEM.md`), so future workspace AIPs (work, knowledge, companies) compose on a shared type system instead of inventing their own.