{"_id":"@agnx/mcp","name":"@agnx/mcp","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@agnx/mcp","version":"0.1.0","description":"MCP client integration for agnx runs","license":"MIT","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"dependencies":{"@agnx/core":"0.1.0","@modelcontextprotocol/client":"^2.0.0","effect":"^3.17.13","ipaddr.js":"^2.5.0","undici":"^7.29.0"},"engines":{"node":">=22.19.0"},"publishConfig":{"access":"public"},"_id":"@agnx/mcp@0.1.0","_integrity":"sha512-Mp2T2V3SO9umVUFA8HrHV+fsADIIl4/ITIe+0T7C9Q5kVNIARdeWD22h+T3/0yQoVLXm+4pksvZv0Jc+QfLujw==","_resolved":"/Users/callavicka/Desktop/Programming/agnx/dist/release/npm/agnx-mcp-0.1.0.tgz","_from":"file:/Users/callavicka/Desktop/Programming/agnx/dist/release/npm/agnx-mcp-0.1.0.tgz","_nodeVersion":"24.13.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-Mp2T2V3SO9umVUFA8HrHV+fsADIIl4/ITIe+0T7C9Q5kVNIARdeWD22h+T3/0yQoVLXm+4pksvZv0Jc+QfLujw==","shasum":"1e8f3c1bb48c805741395eec569c0a791fdbe19f","tarball":"https://registry.npmjs.org/@agnx/mcp/-/mcp-0.1.0.tgz","fileCount":12,"unpackedSize":98019,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEBkh6MPb4U6iNMrUb0I7P9kh8kYt9GP6w7ZCrV3NHnyAiEA5bOnTu2jsDlzn/zasJWRgfOIsMG+CgL4Fgl0dYScF/k="}]},"_npmUser":{"name":"cal_l","email":"cal@assertlabs.dev"},"directories":{},"maintainers":[{"name":"cal_l","email":"cal@assertlabs.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mcp_0.1.0_1788909539223_0.6452494400166959"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-08T23:18:59.031Z","0.1.0":"2026-09-08T23:18:59.356Z","modified":"2026-09-08T23:18:59.557Z"},"maintainers":[{"name":"cal_l","email":"cal@assertlabs.dev"}],"description":"MCP client integration for agnx runs","license":"MIT","readme":"# agnx\n\n`agnx` exposes harness, model, and sandbox providers through one HTTP endpoint. Configuration\nis grouped by subsystem: each provider owns its authentication, portable settings, and a\n`providerOptions` escape hatch for settings that only its SDK understands.\n\n## Packages\n\nThe repository is a pnpm workspace with six independently consumable packages:\n\n- `@agnx/protocol` contains the dependency-light Effect Schemas for request configuration, run\n  events, public and authorized run responses, run listings, and HTTP authentication fields. It\n  is the wire contract shared by servers and clients and has no dependency on core.\n- `@agnx/core` contains service contracts, run state, provider implementations, and\n  transport-independent execution. `executeRun(configuration)` only asks Effect for `Harness`\n  and `Sandbox`; callers choose the layers that satisfy them. `RunStore` is likewise an interface,\n  with `RunStoreLive` provided as the current in-memory implementation.\n- `@agnx/mcp` adapts remote MCP servers to core's generic `ToolCatalog`. It owns the MCP SDK,\n  Streamable HTTP client, SSRF protections, tool discovery, and tool-call implementation. Core\n  has no dependency on this package.\n- `@agnx/http` owns JSON and header decoding, authentication-header precedence, run routes, and\n  SSE encoding. It exports `ServerLive` plus an injectable `RunLayerFactory`; an application can\n  host the API without the agnx CLI and can replace the bundled provider composition.\n- `@agnx/client` is an Effect-first HTTP client. It validates inputs and responses through\n  `@agnx/protocol`, exposes a reconnecting SSE stream, handles run tokens and follow-ups, lists\n  run listings, and downloads retained sandbox paths.\n- `agnx` is the Node entry point, published unscoped so `npx agnx serve` works. It creates the\n  HTTP server, loads namespaced defaults from JSON/environment configuration, and composes the\n  storage and server layers. The npm package and the SEA are built from the same esbuild bundle.\n\nThe server dependency direction is `cli -> http -> core`, with HTTP using MCP as an adapter and\nboth core and HTTP consuming protocol. The client depends only on protocol and Effect. Core imports\nnone of the transport adapters. The CLI provides `RunLayerFactoryLive`, which selects the bundled\nimplementations for each resolved run. A non-HTTP consumer can import `executeRun`, `Harness`,\n`Sandbox`, and `ToolCatalog` from `@agnx/core`, provide its own Effect layers, and execute a run\ninline. Consumers that want MCP can compose the exported layers from `@agnx/mcp`. Bundled\nimplementations are exports, not dependencies of the orchestration effect.\n\n`@agnx/protocol` and `@agnx/client` support Node 20 and newer, and also run in browsers (see\n[Browser clients and CORS](#browser-clients-and-cors)). The server-side packages require\nNode 22.19 or newer, matching Pi and Undici's actual runtime floor. Node 26 is required only when\nbuilding the standalone SEA; `.nvmrc`, the SEA build script, the Docker build, and the CI SEA job\ncontinue to select and enforce it.\n\n## Quick start\n\nWith Node 22.19 or newer and Docker running locally, no sandbox account is needed:\n\n```sh\nnpx agnx serve --port 8080\n```\n\n```sh\ncurl -N localhost:8080/v1/runs \\\n  -H 'content-type: application/json' \\\n  -H \"x-agnx-model-token: $ANTHROPIC_API_KEY\" \\\n  -d '{\n    \"prompt\": \"Run the test suite and fix any failures\",\n    \"contents\": { \"repo\": { \"url\": \"https://github.com/acme/project.git\" } },\n    \"model\": { \"provider\": \"anthropic\", \"model\": \"claude-sonnet-4-5\" },\n    \"sandbox\": { \"provider\": \"docker\" }\n  }'\n```\n\nThe `docker` sandbox provider runs each turn in a container on the host's Docker daemon, using\nthe `agnx-sandbox` image published with each release. It is enabled automatically because the\nserver listens on `127.0.0.1` by default; see [Docker sandboxes](#docker-sandboxes) for the rules\nwhen a server is reachable from other machines. Swap `sandbox` for a hosted provider such as\n`{ \"provider\": \"daytona\" }` plus an `x-agnx-sandbox-token` header to run the same request off-host.\n\n## Run request\n\n```json\n{\n  \"prompt\": \"Inspect the repository\",\n  \"systemPrompt\": {\n    \"append\": \"Keep changes focused and explain important tradeoffs.\"\n  },\n  \"run\": { \"ttlMs\": 3600000 },\n  \"execution\": { \"cwd\": \"/tmp/agnx\", \"timeoutMs\": 600000 },\n  \"validation\": { \"command\": \"pnpm test\", \"maxAttempts\": 3 },\n  \"harness\": { \"provider\": \"pi\" },\n  \"model\": {\n    \"provider\": \"anthropic\",\n    \"model\": \"claude-sonnet-4-5\",\n    \"authentication\": { \"token\": \"...\" },\n    \"thinkingLevel\": \"high\",\n    \"providerOptions\": {}\n  },\n  \"contents\": {\n    \"repo\": {\n      \"url\": \"https://github.com/acme/project.git\",\n      \"access\": \"write\",\n      \"authentication\": { \"token\": \"...\" }\n    }\n  },\n  \"sandbox\": {\n    \"provider\": \"daytona\",\n    \"authentication\": { \"token\": \"...\" },\n    \"image\": \"ubuntu:22.04\",\n    \"size\": \"medium\",\n    \"env\": { \"NODE_ENV\": \"test\" },\n    \"providerOptions\": {\n      \"target\": \"us\",\n      \"autoStopMinutes\": 15,\n      \"ephemeral\": true\n    }\n  }\n}\n```\n\nThe generic sandbox sizes currently map as follows:\n\n| Size     | CPU | Memory |\n| -------- | --: | -----: |\n| `small`  |   1 |  2 GiB |\n| `medium` |   2 |  4 GiB |\n| `large`  |   4 |  8 GiB |\n\nVercel derives memory from its fixed 2 GiB per-vCPU allocation. Daytona and Runloop receive both\nCPU and memory; Runloop uses an exact custom resource request so `large` remains 4 CPU / 8 GiB.\nE2B resources are fixed by the selected template, so its strict schema rejects `sandbox.size`\nrather than silently ignoring it. For E2B, `sandbox.image` is the template name or ID. For\nRunloop, it is a blueprint name; use `sandbox.providerOptions.blueprintId` for an exact blueprint\nID. Provider-only settings live under `providerOptions`; this keeps portable settings at the\nnamespace root without hiding capabilities unique to a provider.\n\n## Core imports\n\nThe root `@agnx/core` entry exposes execution, shared configuration, events, and service contracts.\nConcrete provider exports live on subpaths:\n\n```ts\nimport { ConfigurationLive, executeRun } from '@agnx/core'\nimport { vercelSandboxProvider, VercelSandboxParams } from '@agnx/core/sandboxes/vercel'\nimport { PiHarness } from '@agnx/core/harnesses/pi'\n```\n\nThe other provider paths are `/sandboxes/daytona`, `/sandboxes/e2b`, `/sandboxes/runloop`, and\n`/sandboxes/docker`.\nProvider checkpoint codecs, provider configuration types, and provider helpers are exported there.\nLower-level APIs are grouped under `/configuration`, `/storage`, `/runs`, `/sandboxes`,\n`/harnesses`, and `/repositories`. The bundled provider selectors remain available from the root.\nThis organizes the public API; it does not guarantee that unused SDKs are excluded from loading\nor installation. Core remains a private workspace package pending publication work.\n\n## System prompt\n\n`systemPrompt` customizes the harness system prompt independently from the user-facing `prompt`:\n\n```json\n{\n  \"prompt\": \"Implement the requested feature\",\n  \"systemPrompt\": {\n    \"override\": \"You are an expert TypeScript coding agent.\",\n    \"append\": \"Run the test suite before reporting completion.\"\n  }\n}\n```\n\nUse `override` to replace the harness default, `append` to retain the default and add instructions,\nor both to replace the default and then append another section. At least one field is required and\nwhitespace-only values are rejected by the shared Effect Schema. The namespace works in POST data,\ninline core calls, and server configuration defaults. Pi maps it to its resource loader; custom\nharness implementations receive the same portable value in `Harness.createSession`.\n\nChild runs do not inherit the parent's system-prompt configuration; a caller that wants the same\ncustomization must send it again. An omitted value uses the server default. There is deliberately\nno `null` or empty form that clears a server default back to the harness default. Request\n`override` and `append` fields merge individually over server defaults, so supplying only one does\nnot accidentally remove the other.\n\nFor hosted deployments, `AGNX_SYSTEM_PROMPT_OVERRIDE` and `AGNX_SYSTEM_PROMPT_APPEND` provide the\nequivalent server defaults. Like other non-authentication configuration, these values are not read\nfrom request headers.\n\n## Client SDK\n\nThe client is an Effect service with an injectable `AgnxTransport`. `AgnxClientLive` supplies the\nstandard `fetch` transport; provide `AgnxClientLayer` and your own transport layer for tests,\ninstrumentation, retries, or a nonstandard runtime.\n\n```ts\nimport { Effect, Redacted } from 'effect'\nimport { AgnxClient, AgnxClientLive } from '@agnx/client'\n\nconst program = Effect.gen(function* () {\n  const client = yield* AgnxClient\n  const run = yield* client.start(\n    {\n      prompt: 'Inspect the repository and run its tests',\n      model: { provider: 'openai', model: 'gpt-5.4-mini' },\n      sandbox: { provider: 'daytona' },\n    },\n    {\n      credentialOverrides: {\n        model: { authentication: { token: process.env.OPENAI_API_KEY! } },\n        sandbox: { authentication: { token: process.env.DAYTONA_API_KEY! } },\n      },\n    },\n  )\n\n  // Persist both values if this process must reconnect later.\n  console.log(run.id, run.runToken && Redacted.value(run.runToken))\n  return yield* run.wait({ timeoutMs: 600_000 })\n})\n\nawait Effect.runPromise(\n  program.pipe(\n    Effect.provide(\n      AgnxClientLive({\n        baseUrl: 'http://localhost:3500',\n        reconnect: { delayMs: 1_000 },\n        retry: { maxAttempts: 3, initialDelayMs: 250, maxDelayMs: 2_000 },\n      }),\n    ),\n  ),\n)\n```\n\nEach client generates one 256-bit token by default and reuses it for its lifetime. Persist\n`client.runToken` to reconnect from another client. Select\n`runAuthentication: { mode: 'provided', token }` to reuse a persisted token, or\n`runAuthentication: { mode: 'ephemeral' }` when reconnection, follow-ups, run listings, and downloads\nare not needed. `run.events` consumes the initial SSE response and, if it ends before a terminal\nevent, reconnects to the authenticated event stream with the last received SSE event ID. This\nreplays any missed events and resumes the live tail without polling; `run.wait()` consumes that\nsame stream. Ephemeral runs cannot reconnect because the server intentionally stores no token for\nthem. `run.followUp()` injects `parentRunId` and reuses the token while leaving provider and\nsystem-prompt configuration explicit for the new run. The client retries idempotent reads on\ntransport errors and HTTP 429/502/503/504. Run creation is retried on those same failures only\nwhen `start(..., { idempotencyKey })` is supplied; every attempt reuses its original run token,\nkey, and body. HTTP 409 conflicts are never retried.\n`reconnect` and `retry` are validated client configuration.\n\nCallers that do not otherwise use Effect can use `makePromiseAgnxClient`. Its run handle exposes\nthe SSE events as an `AsyncIterable` and wraps the same operations in Promises. Both APIs also\nprovide `run(id)` (a local handle constructor) and `listRuns()` (handles with listing snapshots); each run handle also has `cancel()`. The Effect streaming methods expose an Effect\n`Stream`, while the Promise facade exposes an `AsyncIterable`. The shorter `file` and `archive`\nmethods are convenience APIs that collect the response into a `Uint8Array`, which works in Node\nand browsers alike; Node callers can wrap it with `Buffer.from(data)`. All\nsuccessful JSON and SSE payloads are decoded with the shared protocol schemas; an incompatible\nserver response is a typed `AgnxProtocolError`, and non-success HTTP responses become\n`AgnxHttpError` with the server's stable error code.\n\n## Authentication and precedence\n\nUse these headers for credentials:\n\n- `X-Agnx-Model-Token`\n- `X-Agnx-Sandbox-Token`\n- `X-Agnx-Git-Token` for private HTTPS repositories\n\nAuthentication can instead be supplied inside the subsystem it belongs to:\n\n```json\n{\n  \"model\": {\n    \"provider\": \"anthropic\",\n    \"model\": \"claude-sonnet-4-5\",\n    \"authentication\": { \"token\": \"...\" }\n  },\n  \"sandbox\": {\n    \"provider\": \"vercel\",\n    \"authentication\": { \"token\": \"...\" },\n    \"providerOptions\": {\n      \"teamId\": \"team_...\",\n      \"projectId\": \"prj_...\"\n    }\n  },\n  \"contents\": {\n    \"repo\": {\n      \"url\": \"https://github.com/acme/project.git\",\n      \"access\": \"write\",\n      \"authentication\": { \"token\": \"...\" }\n    }\n  }\n}\n```\n\nVercel requires `sandbox.authentication.token` and public `teamId` / `projectId` values under\n`sandbox.providerOptions`. The IDs belong in request bodies or configuration files; they are\nnot credential headers. Authentication defaults to header, request body, then host configuration,\nsubject to the host's source policy.\nDefaults only cross an override boundary when the provider (or repository URL) is unchanged, so\na default Anthropic token cannot accidentally be sent to OpenAI. Resolved authentication values\nuse Effect's `Redacted` type before they reach provider layers.\n\nSandbox token fallback also requires matching connection options (`apiUrl`, `baseUrl`, `domain`,\nand `sandboxUrl`). If a request selects a different connection, supply credentials explicitly in\nthe body or headers. Validation errors omit submitted values, and provider error messages redact\nthe resolved authentication fields before they become run or tool errors. Empty credential headers,\nincluding dynamic MCP token headers, are invalid rather than falling back to server credentials.\n\n## Idempotent submission\n\nSend an optional `Idempotency-Key` header alongside `X-Agnx-Run-Token` on `POST /v1/runs`.\nKeys contain 1–128 characters from `A-Z a-z 0-9 . _ ~ -`. Use a distinct key for each logical\nsubmission, including follow-ups.\n\n| Same run token and key        | Result                                                                        |\n| ----------------------------- | ----------------------------------------------------------------------------- |\n| No retained submission        | Atomically create and start one run                                           |\n| Same supplied parameters      | HTTP 200, same `X-Agnx-Run-Id`, replay stored events and follow the live tail |\n| Different supplied parameters | HTTP 409 with `error: \"idempotency_conflict\"`; original run is unchanged      |\n\nMatching retries work for pending, running, completed, failed, timed-out, and cancelled runs.\nReplay starts at the beginning unless `Last-Event-ID` is supplied. It uses the same cursor and\nreplay/live-tail implementation as `GET /v1/runs/:id/events/stream`, and closes at the terminal\nevent. A retry never launches another agent or repeats publication.\n\nComparison uses the supplied JSON body and credential overrides, with object keys sorted.\nArray order and field presence matter; explicit credential changes (including moving credentials\nbetween the body and headers) conflict even if the effective configuration would be equivalent.\nTransport details such as `Last-Event-ID` and the server admission token do not affect comparison.\nMatching retries reuse the original execution without resolving current defaults or revalidating\nits parent. Configuration and credentials resolved for the original execution stay unchanged.\n\nReservations store only token-keyed, versioned HMAC digests of the key and supplied inputs.\nNeither the key nor request bodies/credentials are retained in the reservation or echoed in\nconflict responses. Reservation and run creation are atomic; initial background execution is\nregistered before sending response headers. Storage errors return 503 rather than starting a\nreplacement run.\n\nFor the Promise client:\n\n```ts\nconst run = await client.start(request, {\n  idempotencyKey: 'fix-parser-attempt-1',\n  runAuthentication: { mode: 'provided', token: savedRunToken },\n})\n```\n\nThe Effect client accepts the same options. Generated run tokens are reused during automatic\nretries within one `start` call. To retry across separate calls or client restarts, persist and\nsupply your own run token and idempotency key before the first submission.\n\nKeys are scoped to a run token, and reservations expire with their run. After expiry, the same\nkey can create a new run. Current memory stores lose reservations on server restart; this is not\ncrash-safe exactly-once execution. Durable adapters must atomically implement\n`RunStore.createOrGet` with the reservation and run in one commit. `findSubmission` is an early\nlookup, not a replacement for the atomic check. Tokenless requests reject `Idempotency-Key`\nwith HTTP 400 and retain no state.\n\n## Repository workflows\n\nUse `contents.repo.ref` for the starting branch, tag, or commit (the remote's default branch when\nomitted), and `branch` for the working branch:\n\n```json\n{\n  \"contents\": {\n    \"repo\": {\n      \"url\": \"https://github.com/acme/project.git\",\n      \"ref\": \"main\",\n      \"branch\": \"agnx/fix-parser\",\n      \"access\": \"push\"\n    }\n  }\n}\n```\n\n| `access`         | Credential handling                                                                      | Remote behavior                                                                   |\n| ---------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |\n| `read` (default) | Temporary clone/fetch authentication, removed before init hooks and the agent run        | No automatic publishing; local edits and commits are allowed                      |\n| `push`           | Authentication stays on the agnx host; only Git bundles enter or leave the agent sandbox | Agnx commits local changes and pushes only to the designated branch               |\n| `write`          | Credentials remain available inside the sandbox                                          | Unrestricted credential use within the key's permissions; no automatic publishing |\n\nFor `push`, `branch` is required. The repository URL must use HTTPS without embedded credentials,\nquery parameters, or a fragment. Supply authentication through `X-Agnx-Git-Token`,\n`contents.repo.authentication.token`, or matching server defaults using the usual precedence.\nGit credentials remain optional for public repository reads.\n\nAfter the agent, configured validation, and finish hook succeed, agnx stages tracked changes,\ndeletions, and non-ignored untracked files. If needed, it creates a commit named `Apply agnx changes`\nas `agnx <agnx@localhost>`; existing agent commits are preserved. It then sends one explicit,\nnon-forced branch push. Tags, other branches, branch deletions, and PR operations are never\npublished by this workflow. A divergent destination fails with `repository_push_conflict`;\nremote permission/policy failures produce `repository_publish_failed`. An unchanged commit can\nbe published again safely. `branch` in `read`/`write` mode only selects the initial working branch.\n\nThe host uses a fresh bare repository with isolated Git configuration and disabled hooks for every\nauthenticated operation. Sandbox remote URLs, hooks, tags, push configuration, and credential\nhelpers cannot redirect scoped publishing. The agent receives a system-prompt explanation of the\nchosen policy. `write` intentionally makes its credential accessible to arbitrary agent commands;\n`branch` does not constrain that mode.\n\nScoped publishing requires **Git installed on the agnx host** (included in the Docker image).\nGit must also be available inside the sandbox. Checkout requires an existing starting commit;\nbundle transfers are limited to 100 MiB. Submodules and Git LFS content are not automatically\nmaterialized or published. Hosts can replace the `RepositoryPublisher` Effect service for another\ntrusted implementation.\n\nA successful push emits `RepositoryPublished` with the destination branch and commit SHA during\nthe `repository_publish` phase. Publication is a remote side effect: subsequent checkpoint/storage\nfailure does not undo it. A connection failure during push may leave its outcome uncertain; inspect\nthe destination before retrying.\n\nContinuation inherits the checkout; it does not re-clone or reset the branch. When a repository is\nspecified on a child request, its URL, access policy, ref, and branch must match the retained policy.\nSupply credentials again or through matching defaults for a child that should publish. Omitting\nrepository settings skips automatic publishing while retaining the original policy. Start a new\nroot run to change policy, particularly when moving from unrestricted `write` to scoped `push`.\n\n## MCP tools\n\nRemote MCP servers can contribute tools to the run-scoped `ToolCatalog`:\n\n```json\n{\n  \"mcp\": {\n    \"servers\": {\n      \"github\": {\n        \"transport\": {\n          \"type\": \"streamableHttp\",\n          \"url\": \"https://mcp.example.com/mcp\"\n        },\n        \"authentication\": { \"token\": \"...\" },\n        \"tools\": { \"allow\": [\"get_issue\", \"search_issues\"] },\n        \"providerOptions\": {\n          \"connectTimeoutMs\": 10000,\n          \"callTimeoutMs\": 60000,\n          \"maxRequestBytes\": 1048576,\n          \"maxResponseBytes\": 4194304\n        }\n      }\n    }\n  }\n}\n```\n\nThe equivalent authentication header is `X-Agnx-MCP-Github-Token`. Server IDs use lowercase\nletters, digits, underscores, and hyphens, and determine the dynamic header name. Header tokens\ntake precedence over body tokens. Discovered tools are exposed as `mcp__github__get_issue`; names\nthat are not portable across model providers are normalized and given a stable hash suffix.\n\nThe HTTP package materializes one scoped `@agnx/mcp` catalog for each run and provides it to the\nharness. Core itself depends only on the generic `ToolCatalog` service, so an inline consumer can\nprovide another catalog without using MCP or HTTP. The current MCP adapter supports tool discovery\nand tool calls over Streamable HTTP; resources, prompts, and stdio are intentionally out of scope.\n\nMCP endpoint URLs may target any public HTTPS host; an allowlist is not required. Before opening\na connection, agnx resolves every A and AAAA answer and rejects the server if any answer is not\nglobally routable. It pins the validated addresses into the connection's DNS lookup, which closes\nthe usual DNS-rebinding gap, and rejects redirects and cross-origin transport requests. Request\nand response sizes and connection/call durations are bounded by the options above.\n\nApplication-level filtering is not a complete network security boundary. Production deployments\nshould also deny private, link-local, metadata-service, and other internal destinations at the\negress firewall or proxy. That defense protects against bugs in this process or its HTTP stack\nwithout requiring a per-server allowlist.\n\n## Working directory and validation\n\n`execution.cwd` is the single working directory for the run and defaults to `/tmp/agnx`. agnx\ncreates it before setup and clones `contents.repo` directly into it; there is no separate repository\npath. Initialization and finish hooks, harness sessions and tools, and validation commands all use\nthe same directory. Set `execution.cwd` when a different absolute path is appropriate for the\nsandbox image.\n\nValidation is optional. When configured, agnx runs `validation.command` after the initial agent\nresponse. A failure is sent back to the same harness session so the agent can correct its work.\n`validation.maxAttempts` defaults to 3; exhausting the attempts produces a `RunFailed` event\nwith code `validation_failed`.\n\nCore does not select a harness or sandbox provider. The CLI decodes file and environment defaults\nthrough the same `ConfigurationDefaults` schema and passes them to `ConfigurationLive(defaults)`.\nIt uses the same subsystem namespaces as the POST body. Request namespaces override injected\ndefaults; authentication fields additionally support header overrides.\n\n## Server configuration\n\n`agnx serve` accepts a JSON or YAML configuration file containing the normal namespaced run defaults\nplus a `server` namespace. Unknown fields and provider-specific configuration mistakes prevent the\nserver from starting.\n\n```sh\nagnx serve --config /etc/agnx/config.json\n```\n\nSee [`examples/agnx.config.json`](examples/agnx.config.json) for a complete Daytona example. The\nfile path may instead come from `AGNX_CONFIG_FILE`. `AGNX_CONFIG_JSON` supplies a JSON object that\nis deeply merged over the file, which is useful on platforms that cannot mount configuration\nfiles.\n\nFiles ending in `.yaml` or `.yml` (case-insensitive) use YAML 1.2, files ending in `.jsonc` accept\ncomments and trailing commas, and other file names use strict JSON. All formats use identical\nschemas, defaults, and override precedence. `AGNX_CONFIG_JSON` remains strict JSON. YAML supports\ncomments and multiline strings; duplicate keys, multiple documents, custom tags, and aliases are\nrejected. Quote values that must be strings, such as numeric-looking credentials.\n\nA JSON Schema for the file is published at\n[`https://agnx.dev/schema/agnx.config.json`](https://agnx.dev/schema/agnx.config.json). It is\ncompiled from the same Effect schema the CLI enforces, so editors validate and complete exactly\nwhat `agnx serve` accepts. Reference it with a top-level `\"$schema\"` key in JSON or JSONC, or with\na first-line modeline comment in YAML for editors using the YAML language server:\n\n```yaml\n# yaml-language-server: $schema=https://agnx.dev/schema/agnx.config.json\n```\n\nTop-level `$schema` and `$comment` keys are ignored by the loader; every other unknown key is still\nan error. `pnpm schema` regenerates the published file (and the copies the docs site renders) from the source schema and\n`pnpm schema:check` (part of `release:check`) fails when the committed copy is stale. Field\ndescriptions come from `description` annotations on the schemas in `@agnx/protocol`, core, and the\nCLI, so documenting a field once documents it in every editor.\n\nSee [the YAML example](examples/agnx.config.yaml), or use:\n\n```yaml\nserver:\n  port: 8080\n  memory:\n    maxRunEvents: 10000\nharness:\n  provider: pi\nsandbox:\n  provider: daytona\nsystemPrompt:\n  append: |\n    Explain the changes you made.\n    Include validation results.\n```\n\nCommon scalar and secret environment overrides are:\n\n- `AGNX_HOST`, `AGNX_PORT`, or the conventional `PORT`\n- `AGNX_SERVER_TOKEN` and `AGNX_CORS_ORIGINS`\n- `AGNX_HARNESS_PROVIDER`\n- `AGNX_SYSTEM_PROMPT_OVERRIDE` and `AGNX_SYSTEM_PROMPT_APPEND`\n- `AGNX_MODEL_PROVIDER`, `AGNX_MODEL_ID`, `AGNX_MODEL_TOKEN`, and\n  `AGNX_MODEL_THINKING_LEVEL`\n- `AGNX_SANDBOX_PROVIDER`, `AGNX_SANDBOX_TOKEN`, `AGNX_SANDBOX_PROVIDER_TEAM_ID`,\n  `AGNX_SANDBOX_PROVIDER_PROJECT_ID`, `AGNX_SANDBOX_IMAGE`, and `AGNX_SANDBOX_SIZE`\n- `AGNX_GIT_TOKEN`, `AGNX_GIT_ACCESS` (`read`, `push`, or `write`), `AGNX_GIT_REF`, and `AGNX_GIT_BRANCH`\n- `AGNX_EXECUTION_CWD`, `AGNX_EXECUTION_TIMEOUT_MS`, and `AGNX_RUN_TTL_MS`\n\n`AGNX_SANDBOX_DOCKER_ENABLED`, `AGNX_SANDBOX_DOCKER_COMMAND`, and `AGNX_SANDBOX_DOCKER_NETWORK`\nconfigure the docker provider; see [Sandbox provider policy](#sandbox-provider-policy).\n`AGNX_MODEL_API_KEY` is accepted as a compatibility alias for `AGNX_MODEL_TOKEN`.\n`DAYTONA_API_KEY`, `VERCEL_TOKEN`, `E2B_API_KEY`, and `RUNLOOP_API_KEY` are conventional\nsandbox-token aliases for the selected provider. Vercel also accepts `VERCEL_TEAM_ID` and\n`VERCEL_PROJECT_ID`. Model aliases include `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`\n(or `GOOGLE_API_KEY`), `GROQ_API_KEY`, `XAI_API_KEY`, `OPENROUTER_API_KEY`, `MISTRAL_API_KEY`, and\n`CEREBRAS_API_KEY`; only the selected model provider's aliases are consulted. Explicit agnx token\nvariables win over conventional aliases.\n\nProvider connection bindings are `DAYTONA_API_URL` (or `DAYTONA_SERVER_URL`), `DAYTONA_TARGET`,\n`E2B_DOMAIN`, `E2B_API_URL`, `E2B_SANDBOX_URL`, and `RUNLOOP_BASE_URL`. SDK connection defaults\nare passed explicitly, so SDK environment fallbacks cannot bypass agnx's source policy. Daytona's\ndefault target is `us`; E2B's default sandbox gateway is `https://sandbox.e2b.app`.\n\nBy default, HTTP fields override environment fields, which override file fields. Dedicated environment\nvariables override `AGNX_CONFIG_JSON` within the environment source. Credential headers override\nbody credentials within HTTP. CLI `--host` / `--port` take precedence for those host settings.\nPi is the default harness in core as well as the CLI; the CLI additionally defaults to Daytona.\nThe server listens on `127.0.0.1` unless `server.host`, `AGNX_HOST`, or `--host` says otherwise;\nthe container image sets `AGNX_HOST=0.0.0.0`.\n\n### Sandbox provider policy\n\n`server.sandbox.providers` decides which bundled sandbox providers a run may select. Every hosted\nprovider is enabled by default because callers pay for those sandboxes with their own credentials.\nThe `docker` provider is different: it runs containers on the agnx host with no caller credential,\nso it is enabled by default only when the server listens on a loopback address. Anywhere else it\nmust be switched on explicitly, and any provider can be switched off:\n\n```yaml\nserver:\n  host: 0.0.0.0\n  sandbox:\n    providers:\n      docker:\n        enabled: true # Required when host is not loopback.\n      vercel:\n        enabled: false\n```\n\n`AGNX_SANDBOX_DOCKER_ENABLED=true` or `false` sets the same flag from the environment. A request\nthat selects a disabled provider fails configuration validation with\n`The docker sandbox provider is not enabled on this server`.\n\nObjects merge per field across sources; arrays replace. A request may therefore supply only\n`model: { model: \"another-model\" }`. Changing a provider drops its inherited credentials and\nprovider-specific settings. Changing a repository or MCP URL drops inherited credentials and\nsettings; changing a sandbox endpoint drops inherited authentication. Explicit request credentials\nremain usable for a newly selected destination. A request's MCP server map selects active servers;\nan empty map disables them.\n\nOptional host-only source rules use the same dot-separated configuration paths:\n\n```yaml\nmodel:\n  provider: openai\n  model: gpt-5\nsandbox:\n  provider: vercel\n  providerOptions:\n    teamId: team_123\n    projectId: prj_123\nconfiguration:\n  sources: [http, env, file] # Highest precedence first.\n  rules:\n    model.provider:\n      sources: [file]\n    model.model:\n      env: MY_DEFAULT_MODEL\n    sandbox:\n      sources: [env, file] # Reject sandbox overrides in HTTP bodies and headers.\n    sandbox.authentication.token:\n      sources: [env]\n      env: MY_VERCEL_TOKEN\n```\n\nRules inherit namespace source restrictions, and a more specific rule can replace that source list.\nAn explicit `env` name replaces all standard aliases for that field. Omitting `env` keeps standard\nbindings. Remove `env` from `sources` to disable environment values; remove `http` to reject HTTP\noverrides with a configuration error, including equal-value overrides. Allowed lower-priority HTTP\nvalues remain fallbacks. Unknown configuration paths, unknown sources, and duplicate sources are\nrejected. Custom numeric and boolean bindings are parsed before schema validation; arrays use JSON.\n\n`configuration` can only appear in a file, never an HTTP request or `AGNX_CONFIG_JSON`. Globally\nomitting `env` also skips `AGNX_CONFIG_JSON` unless a field rule re-enables that source. `--config`\nand `AGNX_CONFIG_FILE` locate the file before its policy can be read; use `--config` to choose that\nbootstrap path explicitly. Embedded core reads no process environment: pass host sources through\n`makeConfiguration(defaults, { policy, environment })` or `ConfigurationLive` explicitly.\n\nLocked credentials remain bound to their original provider and endpoint. An allowed request that\nchanges either must provide its own credentials; if credential overrides are locked too, the run\nfails configuration validation. Pin the provider and connection options when callers should not\nchoose another destination.\n\n### Public and private servers\n\nThe server is open when `server.authentication.token` and `AGNX_SERVER_TOKEN` are absent. This is\nthe public BYOK mode: callers provide the underlying model, sandbox, Git, and MCP credentials with\ntheir requests.\n\nSetting `AGNX_SERVER_TOKEN` or `server.authentication.token` enables private mode. The token must\ncontain at least 32 URL-safe characters. Every `/v1/*` request must then include:\n\n```text\nAuthorization: Bearer <server-token>\n```\n\nThis admission token is independent of `X-Agnx-Run-Token`, which continues to authorize private\nrun history and run listings. `/health` and `/ready` deliberately remain public for orchestration\nchecks. agnx does not implement rate limiting in v0.1; public hosts should enforce it at their\nreverse proxy, load balancer, or platform edge.\nPrivate deployments can place model and sandbox credentials in the environment or config file so\ntrusted callers only need the server token; secrets should normally be injected through the\nenvironment rather than committed to a configuration file.\n\n### Browser clients and CORS\n\n`@agnx/client` uses only web-standard APIs (`fetch`, `ReadableStream`, `TextDecoder`, and Web\nCrypto), so it runs in browsers as well as Node. Because it sends custom headers such as\n`X-Agnx-Run-Token` and `Authorization`, every browser request needs a CORS preflight, and the\nserver refuses cross-origin browser access until `server.cors` names the allowed origins:\n\n```yaml\nserver:\n  cors:\n    allowedOrigins:\n      - https://app.example.com\n      - http://localhost:5173\n```\n\n`AGNX_CORS_ORIGINS` accepts the same list as comma-separated values. Use `[\"*\"]` to allow every\norigin, which is safe for open BYOK servers because agnx never relies on cookies and never sets\n`Access-Control-Allow-Credentials`. Origins must match what browsers send in the `Origin` header:\na scheme and host without a path or trailing slash.\n\nPreflight requests are answered before authentication, so private servers work from browsers\nwithout exposing anything beyond the CORS headers. The middleware allows `GET` and `POST`,\nreflects whatever request headers the browser asks for (so dynamic MCP credential headers work),\nand exposes `X-Agnx-Run-Id` and `Content-Disposition` so the client can read run IDs and download\nfilenames. Optional `allowedHeaders`, `exposedHeaders`, and `maxAgeSeconds` tighten or extend\nthose defaults. Hosts composing the exported layers without the CLI enable the same behaviour by\nproviding `serverCors(configuration)` from `@agnx/http`; without it the app runs with CORS off.\n\n## Execution timeout\n\nThe complete run—including sandbox creation, repository setup, harness execution, validation,\nand finish hooks—is limited to 10 minutes by default. Set `execution.timeoutMs` in the request\nor configuration defaults to override it. Request configuration takes precedence over server\ndefaults. Provider-specific timeout options remain separate because they control SDK requests\nor sandbox lifetime rather than the overall agnx run.\n\n## Run events\n\nSuccessful `POST /v1/runs` requests return a `text/event-stream` response. Every event belongs to\none provider-independent `RunEvent` schema shared by the server, stored history, and both client\nAPIs. Upgrade the protocol/client packages together with the server: older strict decoders reject\nevent types they do not recognize. Pi translates its session events inside its adapter; SDK messages and checkpoint objects\nare not part of the public protocol.\n\n| Events                                                                       | Contents                                                                                                                                                                                                                   |\n| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `RunStarted`                                                                 | Harness execution is starting; provisioning/setup phases can precede it.                                                                                                                                                   |\n| `RunPhaseStarted`, `RunPhaseCompleted`                                       | Sandbox provisioning/restoration, model configuration, MCP tools, setup, session creation, finish, and harness/sandbox checkpointing. Completion reports `succeeded`, `failed`, or `cancelled`.                            |\n| `AgentTurnStarted`, `AgentTurnCompleted`                                     | Model/tool-loop turns, correlated by `turnId`; completion includes available token usage and USD cost estimates.                                                                                                           |\n| `AssistantMessageStarted`, `AssistantTextDelta`, `AssistantMessageCompleted` | Messages correlated by `messageId` and `turnId`; deltas include a `contentIndex` and `kind` (`text` or harness-exposed `reasoning`). Completion carries final text, a normalized finish reason, and available token usage. |\n| `ToolCallStarted`, `ToolCallUpdated`, `ToolCallCompleted`                    | Tool name, `toolCallId`, `turnId`, JSON arguments at start, text output snapshots during execution, and final output with `isError`. This includes sandbox commands and MCP tools.                                         |\n| `RepositoryPublished`                                                        | Scoped publication succeeded; includes the destination branch and commit SHA.                                                                                                                                              |\n| `FileChanged`                                                                | Successful writes/edits reported by the harness's file tools. Paths use the tool's path convention; this is not a filesystem watcher and does not detect arbitrary shell writes.                                           |\n| `ValidationStarted`, `ValidationCompleted`, `ValidationRetryScheduled`       | Command and attempt limits; exit code/stdout/stderr; the next attempt announced before the corrective agent prompt.                                                                                                        |\n| `AgentRetryScheduled`, `AgentRetryCompleted`                                 | Harness-managed model retries, including attempt limits, delay, and outcome.                                                                                                                                               |\n| `ContextCompactionStarted`, `ContextCompactionCompleted`                     | Context compaction reason, outcome, whether the harness will retry, and available usage.                                                                                                                                   |\n| `RunCompleted`, `RunFailed`, `RunCancelled`, `RunTimedOut`                   | Terminal outcome and aggregate usage when available. Completion contains final output; failure contains a stable code and message.                                                                                         |\n\nAppend assistant deltas per `(messageId, contentIndex, kind)`. Treat tool updates as replacement\nsnapshots, not append-only deltas; providers that buffer command output may only report it when\nthe command finishes. IDs are scoped to the run, including repeated prompts for validation.\n\nTurn completion carries optional `usage`: `inputTokens`, `outputTokens`, `cacheReadTokens`,\n`cacheWriteTokens`, `totalTokens`, optional `reasoningTokens`, and optional `cost`.\nReasoning tokens are a subset of output tokens. Costs contain `currency: \"USD\"`, `input`,\n`output`, `cacheRead`, `cacheWrite`, and `total` amounts in dollars. Pi supplies SDK model-pricing\nestimates, not invoices; sandbox and tool charges are excluded. Pi's all-zero pricing is omitted\nbecause it can mean unknown pricing.\n\nTerminal events carry aggregate `usage` for this execution, including validation retries and\nreported compaction usage. They prefer turn usage over the same turn's message usage to avoid\ndouble-counting; completed messages provide a fallback if a turn never finishes. Parent history\nand SSE replay do not increase totals. Failed, timed-out, and cancelled runs include whatever\nusage was reported before termination.\n\nTotals include `usageReports` (reports with token data), `missingUsageReports` (observed turns,\nfallback messages, or compactions without token data), and `costReports` (reports with pricing).\nThese are reporting coverage counts, not billing call counts. Unreported work cannot be counted;\npartial totals may understate actual usage. If no token data is available, `usage` is absent;\nif no pricing is available, `cost` is absent. The same final report is available through SSE,\nevent history, and the client's `wait()` result. No usage limits are enforced.\n\nOnly the five existing lifecycle event types affect run status. All new events are nonterminal;\na failed tool, failed validation attempt, or completed assistant message does not imply the run\nhas finished. Use `isTerminalRunEvent` rather than interpreting message/tool completion as run\ncompletion. Pi's agent-end event is not translated into `RunCompleted`: validation, finish hooks,\nand checkpointing can still follow.\n\n```text\nid: 1\nevent: RunPhaseStarted\ndata: {\"_tag\":\"RunPhaseStarted\",\"phase\":\"sandbox\"}\n\nid: 2\nevent: RunPhaseCompleted\ndata: {\"_tag\":\"RunPhaseCompleted\",\"phase\":\"sandbox\",\"status\":\"succeeded\"}\n```\n\nOther harnesses can emit these events through core's optional `RunEvents` service. The Pi adapter\nsubscribes only when an observer exists, copies mutable SDK data, serializes delivery, drains\npending events before returning from a prompt, and unsubscribes on success or failure. agnx uses\n`withRunPhase` for its own operations. No observer is required for inline execution.\n\nProgress is stored and replayed with the same monotonically increasing SSE IDs as terminal events.\nClosing the initial SSE connection does not cancel the run. Publication preserves the same order\nin storage and the response stream, including concurrent provisioning phases. History and response\nqueues are bounded by host memory protection (see below); this does not introduce\ndurable storage. Event consumers should treat tool arguments, outputs, and assistant text as\npotentially sensitive task data. Raw provider diagnostics, signatures, binary tool payloads, and\ncheckpoint entries are deliberately omitted.\n\n## Memory protection\n\nConfigure host limits in the JSON or YAML configuration file under `server.memory`. These settings are\nalso supported in the server's `AGNX_CONFIG_JSON` overlay, using the usual per-field precedence.\nThey are not run defaults and cannot be overridden in HTTP requests.\n\nThe defaults are:\n\n```json\n{\n  \"server\": {\n    \"memory\": {\n      \"maxValidationOutputCharacters\": 1024,\n      \"maxEventBytes\": 262144,\n      \"maxRunEventBytes\": 8388608,\n      \"maxRunEvents\": 10000,\n      \"maxRetainedRuns\": 256,\n      \"maxStoreEventBytes\": 67108864,\n      \"maxBufferedEvents\": 256,\n      \"maxBufferedBytes\": 2097152,\n      \"maxCheckpointBytes\": 8388608,\n      \"maxCheckpointStoreBytes\": 67108864,\n      \"maxSandboxAccessBytes\": 262144,\n      \"maxSandboxAccessStoreBytes\": 8388608\n    }\n  }\n}\n```\n\nAll values must be positive safe integers. Byte limits use a conservative in-memory payload\nestimate (including UTF-16 strings, containers, and private credential values), not serialized\nJSON length or a hard process RSS ceiling. Nesting beyond 64 levels is rejected before copying.\n\n- `maxEventBytes`, `maxRunEventBytes`, and `maxRunEvents` bound each event and the cumulative\n  events of one execution, including tokenless runs. Overflow stops execution with\n  `RunFailed(resource_limit_exceeded)`. Output is not silently truncated.\n- `maxRetainedRuns` caps each memory store's entry count. `maxStoreEventBytes` caps retained\n  history across runs, including a 4 KiB terminal reserve per unfinished run. The reserve permits\n  one small capacity-failure event beyond the normal per-run limits, with an ordinary replay\n  cursor. New retained submissions return HTTP 503 with `resource_limit_exceeded` when capacity\n  is exhausted; existing idempotent retries can still replay. Unexpired records are never evicted\n  to make room. Expired entries are reclaimed during store operations.\n- `maxBufferedEvents` and `maxBufferedBytes` cap each initial SSE delivery buffer and Pi's\n  callback backlog. A slow SSE consumer is disconnected without a fabricated terminal event:\n  retained runs continue and clients reconnect using their last received cursor. Tokenless\n  executions cancel when the response scope closes. Replay readers consume the stored log\n  directly; coalesced wakeups carry no event payloads. A Pi callback overflow aborts the prompt\n  and fails the run explicitly.\n- `maxValidationOutputCharacters` bounds each stdout/stderr preview in terminal validation\n  summaries. Longer output is marked `outputTruncated`; full accepted diagnostics remain in history.\n- Checkpoint and sandbox-access limits cap individual records and their respective stores.\n  Writes that exceed capacity fail atomically without replacing existing records. Failure to\n  retain a checkpoint can occur after agent work or repository publication has happened;\n  those external actions are not rolled back.\n\nEmbedded hosts can provide the core `MemoryProtection` service. Durable adapters must define\ntheir own storage-capacity policy. These bounds protect agnx's retained data and delivery queues;\nprovider SDK allocations, request bodies, concurrent executions/connections, and downloaded\nfiles can require additional host or provider limits. Token/cost budgets and an admission queue\nremain deferred.\n\n## Run storage\n\nEvery accepted run is registered in an in-memory `RunStore` before its event stream starts. The\nresponse includes its UUID in `X-Agnx-Run-Id`. Each run is a node with an optional parent and a\n`rootRunId`; roots and their descendants form resumable run trees. Recording an event atomically\nappends it to the immutable history and derives one of these statuses: `pending`, `running`,\n`completed`, `cancelled`, `failed`, or `timed_out`.\n\nAnyone with a run ID may read its non-sensitive snapshot:\n\n```text\nGET /v1/runs/:id\n```\n\n```json\n{\n  \"id\": \"e7a72a68-3ca9-4c75-a17c-9672da9dd22c\",\n  \"rootRunId\": \"e7a72a68-3ca9-4c75-a17c-9672da9dd22c\",\n  \"status\": \"running\",\n  \"resumability\": \"none\",\n  \"createdAt\": 1788055200000,\n  \"updatedAt\": 1788055201000\n}\n```\n\nClients that need access to stored event history may generate 32 random bytes, encode them as\nunpadded base64url, and send the resulting 43-character value when creating the run:\n\n```text\nX-Agnx-Run-Token: <client-generated token>\n```\n\nClients that do not consume SSE can retrieve a compact final result:\n\n```text\nGET /v1/runs/:id/result\nX-Agnx-Run-Token: <client-generated-secret>\n```\n\nThis returns the run metadata plus `result: null` while unfinished, or `result` containing the\nsame terminal event as SSE and event history. It does not include the event log. Both pending\nand terminal responses use HTTP 200; missing, expired, or unauthorized runs return 404.\nTokenless runs have no stored result and must consume the terminal stream event.\nPublic status remains metadata-only: output, validation diagnostics, and repository details\nrequire the run token.\n\n```ts\n// Both Promise and Effect clients expose these methods.\nconst snapshot = await client.run(savedRunId).result()\n// For a handle returned by start(), use: await run.result()\nif (snapshot.result?._tag === 'RunCompleted') {\n  console.log(snapshot.result.output)\n  console.log(snapshot.result.usage)\n  console.log(snapshot.result.validation)\n  console.log(snapshot.result.repository)\n}\n```\n\nTerminal reports include, when available:\n\n- `output` on success and aggregate `usage` as before.\n- `validation`: `status` (`passed`, `failed`, or `incomplete`), `attempts` started,\n  `maxAttempts`, and the latest attempt's `exitCode`, `stdout`, and `stderr`. Diagnostics are\n  previews bounded by `server.memory.maxValidationOutputCharacters` (default 1024 per stream);\n  `outputTruncated` identifies shortened output. Validation that never started is omitted.\n- `repository`: the branch and commit confirmed by agnx's scoped publisher. Unrestricted\n  agent Git operations are not inferred as publication.\n- `detailsOmitted: true` if summary details cannot fit the configured event limit or a\n  capacity-failure reserve. Consult retained history for the accepted underlying events.\n\nSummaries include this execution only, including validation retries. Cancellation and timeouts\ncan report incomplete validation. A failure after publishing still reports that publication:\nexternal effects are not rolled back. The JSON endpoint reads the committed terminal event;\nreplaying or polling does not re-execute work or recalculate usage.\n\nThe same header authorizes `GET /v1/runs/:id/events` and `GET /v1/runs`. The listing returns a flat `{ runs: PublicRun[] }` response with lineage IDs and resumability. Invalid or mismatched tokens and unknown run IDs all produce the same `404` response.\n\nTo replay missed events and continue following an active run, open the authenticated SSE endpoint\nand send the last event ID received, using the standard SSE reconnection header:\n\n```text\nGET /v1/runs/:id/events/stream\nX-Agnx-Run-Token: <client-generated token>\nLast-Event-ID: 12\n```\n\nThe response first replays stored events after that cursor, then remains open for newly recorded\nevents and closes after a terminal event. Omitting `Last-Event-ID` replays from the beginning. The\n`RunStore` owns this replay-and-tail operation so a future durable implementation can use Redis\nStreams or another append-only log without changing the HTTP or client contracts. The bundled\nin-memory store uses an Effect `PubSub` for live notification and the stored event history for\nreplay.\n\nAn active run may be interrupted with the same token. Cancellation interrupts its Effect fiber,\nruns scoped provider cleanup, records `RunCancelled`, and returns the resulting public snapshot:\n\n```text\nPOST /v1/runs/:id/cancel\nX-Agnx-Run-Token: <client-generated token>\n```\n\nCompleted retained runs expose their sandbox filesystem through the same token:\n\n```text\nGET /v1/runs/:id/files?path=dist/report.pdf\nGET /v1/runs/:id/archive?path=/tmp/results\n```\n\nRelative paths resolve from the run's `execution.cwd`; absolute paths may address any ordinary\nlocation inside the sandbox. The file endpoint returns raw bytes as an attachment. The archive\nendpoint creates a temporary gzip-compressed tarball in a sandbox restored from the terminal\ncheckpoint, streams it, and deletes the temporary sandbox and archive when the response finishes\nor disconnects. Both endpoints require a completed, fully retained run and return `410` after its\nstate expires. Downloads preserve backpressure and are limited to 100 MiB without buffering on the\nagnx server. Paths never address the agnx server host filesystem.\n\nWhen a root request includes this token, agnx retains its paired Pi and sandbox terminal state for\n`run.ttlMs`, which defaults to one hour. Pi stores a cloned message checkpoint. Vercel stores\nan expiring filesystem snapshot; Daytona stops the sandbox, configures provider-side auto-delete,\nand retains its ID as an immutable fork source. Run metadata uses the same expiration. The\nconfigured TTL begins when terminal state is committed and reads do not extend it.\n\nCreate a child by making the normal request with the same token and an explicit parent:\n\n```json\n{\n  \"prompt\": \"Now add tests\",\n  \"run\": { \"parentRunId\": \"e7a72a68-3ca9-4c75-a17c-9672da9dd22c\" },\n  \"harness\": { \"provider\": \"pi\" },\n  \"model\": {\n    \"provider\": \"anthropic\",\n    \"model\": \"claude-sonnet-4-5\",\n    \"authentication\": { \"token\": \"...\" }\n  },\n  \"sandbox\": {\n    \"provider\": \"daytona\",\n    \"authentication\": { \"token\": \"...\" }\n  }\n}\n```\n\nThe child gets an independent fork of both checkpoints. It inherits the root retention policy;\ncredentials must be supplied again when creating the child. Harness and sandbox providers must\nmatch the parent. A missing or expired parent returns `parent_run_not_found`; an unfinished or\nnon-resumable parent is rejected rather than silently starting from an empty state.\n\n`parentRunId` inherits sandbox state, while the child request supplies its desired sandbox\nconfiguration. Vercel snapshots support new size, environment, timeout, and port settings; their\nimage, team, and project cannot change. Daytona forks support new size, environment, auto-stop,\nauto-delete, and client request-timeout settings; image, ephemeral mode, API endpoint, and target\ncannot change. E2B uses a native independent fork and permits new sandbox/request timeouts and\ncheckpoint memory policy; template, environment, traffic security, internet access, and API\nendpoints cannot change. Runloop creates an independent Devbox from a disk snapshot and permits\nnew size, environment, keep-alive, retry, and request-timeout settings; blueprint, API endpoint,\nand architecture cannot change. Runloop continuation preserves disk state, while harness messages\nremain preserved by the separate harness checkpoint; running processes are not carried into the\nchild. Unsupported changes terminate the run with\n`sandbox_fork_configuration_unsupported` and name every incompatible request field.\n\nWithout `X-Agnx-Run-Token`, no run history, checkpoints, or sandbox credentials are retained.\nThe response ID is only a correlation ID and events carry no replay cursors. Disconnecting cancels\nthe scoped execution. Agent and sandbox resources are released at the end of the\nrun. Supplying `run.ttlMs` without a token is an error. The current `RunStore` and\n`RunStateStore` implementations are process-local. `SandboxAccessStore` also retains the redacted\nsandbox provider credential in memory for the same TTL so that download requests can reopen the\ncheckpoint without asking for provider authentication again. TTL is therefore a maximum cache\nlifetime rather than a durability guarantee: restarting agnx loses resumability and downloads.\nAll stores are injectable Effect services. A durable implementation also needs stable checkpoint\nfingerprints and an atomic completion contract; typed storage errors, authoritative cursors, and\nversioned state/credential codecs are available to adapters. Replacing the\nthree memory layers alone is insufficient. See [state management triage](docs/state-management-triage.md). All lookup\nresponses use `Cache-Control: no-store`; the events endpoint currently returns a point-in-time\nreplay rather than following new events.\n\n## Sandbox provider options\n\nThe current Pi adapter has no model or harness provider options. `model.providerOptions` and\n`harness.providerOptions` may be omitted or `{}`; nonempty objects are rejected instead of silently\nignored. Use `model.thinkingLevel` and the portable `systemPrompt` namespace for their supported\nsettings. Sandbox and MCP options are explicitly typed and forwarded by their respective adapters.\n\n- Daytona `sandbox.providerOptions`: `apiUrl`, `target`, `requestTimeoutMs`, `autoStopMinutes`,\n  `autoDeleteMinutes`, and `ephemeral`. `autoDeleteMinutes` must be positive and defaults to 60 as\n  a provider-side cleanup backstop; a retained run replaces it with the run TTL.\n- Vercel `sandbox.providerOptions`: `timeoutMs` and `ports`.\n- E2B `sandbox.providerOptions`: `domain`, `apiUrl`, `sandboxUrl`, `requestTimeoutMs`, `timeoutMs`,\n  `secure`, `allowInternetAccess`, and `keepMemory`. Retained E2B sandboxes are paused with a\n  provider timeout and resumed only long enough to create an independent native fork.\n- Runloop `sandbox.providerOptions`: `blueprintId`, `baseUrl`, `requestTimeoutMs`, `maxRetries`,\n  `keepAliveSeconds`, and `architecture`. `sandbox.image` and `blueprintId` are mutually exclusive.\n  Runloop has no provider-side snapshot TTL, so agnx tags snapshots with their expiration, schedules\n  deletion in the live process, and opportunistically reaps expired managed snapshots on later\n  Runloop use. After a server restart with no Runloop traffic, deletion is therefore delayed until\n  the next Runloop request; a durable scheduler is needed for a strict idle-restart guarantee.\n- Docker `sandbox.providerOptions`: `command`, `network`, and `pull`. There is no\n  `sandbox.authentication`; see [Docker sandboxes](#docker-sandboxes).\n\n### Docker sandboxes\n\nThe `docker` provider drives a Docker-compatible CLI on the agnx host: `docker run` creates a\ndetached container from `sandbox.image`, `docker exec` runs commands, files move through\n`docker exec` as well, and `docker commit` produces the checkpoint image that retained runs and\nforks restore from. `sandbox.size` maps to `--cpus` and `--memory`, `sandbox.env` to `--env`, and\n`providerOptions.network` to `--network` (`none` disables network access). `providerOptions.command`\nselects another compatible CLI such as `podman` or `nerdctl` (`AGNX_SANDBOX_DOCKER_COMMAND`), and\n`providerOptions.pull` sets the pull policy for the initial image. Containers start with `--init`\nso exec'd processes are reaped; Podman needs `catatonit` installed for that flag. The default image is\n`docker.io/assertlabs/agnx-sandbox:<agnx version>`, built from [`images/sandbox`](images/sandbox) and\npublished to Docker Hub as the first step of each release; it contains git, bash, tar, file, curl,\nand CA certificates and runs as a non-root user. Any image with those tools works as `sandbox.image`.\n\nCheckpoints are committed images on the same daemon, labelled `dev.agnx.sandbox.checkpoint` with\ntheir expiration. They can only be restored by an agnx process talking to that daemon, and a fork\ncannot change `sandbox.image`, `sandbox.env`, or `providerOptions.network`. agnx does not prune\nexpired checkpoint images on its own; remove them with\n`docker image prune --filter label=dev.agnx.sandbox.checkpoint=true`. Sandbox containers carry the\n`dev.agnx.sandbox=true` label and are removed when their run ends.\n\nContainers share the host kernel and daemon, so this is a convenience for development and trusted\nsingle-user hosts rather than isolation for untrusted callers. The published server image has no\ncontainer CLI and cannot use this provider; run `agnx serve` directly on a machine with Docker.\n\nConfiguration and provider checkpoints are decoded with Effect Schema at their core boundaries,\nincluding inline JavaScript calls. Decoding is strict about excess properties, so placing a\nDaytona option under Vercel (or vice versa) is an explicit configuration error rather than a\nsilently ignored field. Each resumable provider defines a tagged checkpoint schema using the\nshared `providerCheckpointSchema` envelope.\n\nRun `pnpm test`, `pnpm typecheck`, and `pnpm lint` to verify changes.\n\nService and interface function members use `readonly foo: (...) => ...`, including optional\ncallbacks, rather than method signatures. The lint rule `typescript/method-signature-style`\nenforces property syntax. Prefer small helpers for repeated behavior; keep SDK-specific option\nmapping and lifecycle logic in each provider. Run `pnpm fmt:check` to check formatting.\n\n## Live end-to-end test\n\nThe canonical live test exercises the HTTP API against Daytona and the configured model provider.\nIt verifies readiness, authenticated cancellation, public in-progress status, private event access,\ncontinuation after the initial SSE client disconnects, a child run forked from the completed root,\ninherited sandbox files, and the authorized run listing. It also downloads a root-run file and a\nchild-run tar archive from their retained terminal sandboxes.\n\nConfigure these values in the ignored `.env` file or export them in the shell:\n\n```sh\nDAYTONA_API_KEY=...\nAGNX_MODEL_PROVIDER=openai\nAGNX_MODEL_ID=gpt-5.4-mini\nAGNX_MODEL_API_KEY=...\n```\n\nOptional Daytona settings are `DAYTONA_API_URL` and `DAYTONA_TARGET`. Build and start agnx in one\nterminal:\n\n```sh\npnpm build:sea\n./dist/agnx serve --port 8787\n```\n\nThen run the test from another terminal:\n\n```sh\npnpm test:e2e\n```\n\nThe test generates its own client run token and never prints it. It uses a two-minute run TTL\nby default and creates a Daytona root sandbox, a forked child, and a short-lived snapshot. To test\nan existing server or adjust timing, set `AGNX_E2E_BASE_URL`, `AGNX_E2E_RUN_TIMEOUT_MS`,\n`AGNX_E2E_REQUEST_TIMEOUT_MS`, or `AGNX_E2E_SESSION_TTL_MS`.\n\nIf the server requires admission authentication, set `AGNX_SERVER_TOKEN` for the test as well.\nSet `AGNX_E2E_USE_SERVER_CREDENTIALS=1` to omit provider credential headers and verify that the\nserver's configured model and sandbox credentials are used. `AGNX_E2E_SANDBOX_PROVIDER=docker`\nruns the same lifecycle against the local Docker daemon with no sandbox credential; set\n`AGNX_SANDBOX_IMAGE` to a locally built sandbox image to avoid pulling the release image.\n\n## Container images\n\nBoth images live under `images/`. `images/server/Dockerfile` builds the Node 26 SEA in one stage\nand copies only the executable, CA certificates, and the health-check client into a non-root\nDebian runtime image; its build context is the repository root and `.env` files are excluded.\n`images/sandbox/Dockerfile` is the default image for the `docker` sandbox provider and builds from\nits own directory. Releases publish them to Docker Hub as `docker.io/assertlabs/agnx` and\n`docker.io/assertlabs/agnx-sandbox`, tagged with the version and `latest`.\n\n```sh\ndocker build --file images/server/Dockerfile --tag agnx .\ndocker run --rm \\\n  --publish 8080:8080 \\\n  --env-file .env \\\n  --env AGNX_CONFIG_FILE=/etc/agnx/config.json \\\n  --mount type=bind,src=\"$PWD/examples/agnx.config.json\",dst=/etc/agnx/config.json,readonly \\\n  agnx\n```\n\nThe server image behaves like a conventional server image: it sets `AGNX_HOST=0.0.0.0`, reads\n`PORT` (default `8080`), exposes that port, checks `/ready`, and runs `agnx serve` as its default\ncommand. Configuration should be mounted or injected at runtime rather than baked into the image.\nBecause it listens on every interface and ships no container CLI, the `docker` sandbox provider\nis not available from this image; use a hosted provider there.\n\nA single replica can use the bundled in-memory stores, with the understood limitation that runs\nand resumable Pi state are lost when the container is restarted. Multiple replicas or restart-safe\nretained runs require external stores plus the checkpoint, commit, and execution-ownership contracts\ndescribed in the [state management triage](docs/state-management-triage.md).\n\n## Standalone executable\n\nagnx builds as a Node 26 Single Executable Application (SEA). With nvm installed:\n\n```sh\nnvm use\npnpm install\npnpm build\n./dist/agnx --help\n```\n\nThe build typechecks the project, bundles the TypeScript entry point and all dependencies with\nesbuild, and passes that ESM bundle to Node's built-in `--build-sea` command. The result is the\nsingle executable `dist/agnx` (`dist/agnx.exe` on Windows); Node does not need to be installed on\nthe destination system. macOS builds are ad-hoc signed automatically. Release binaries should\nstill be built on each target operating system and signed with the appropriate distribution\nidentity where applicable. See the\n[Node SEA documentation](https://nodejs.org/api/single-executable-applications.html) for platform\nand signing details.\n\n## Website and docs\n\n`apps/web` is the public site for `agnx.dev` and `apps/docs` is the stub for `docs.agnx.dev`.\nBoth are TanStack Start applications on Solid 2.0 with hand-written CSS and no database, and both\nare workspace projects so `pnpm typecheck`, `pnpm lint`, and `pnpm fmt:check` cover them. Start\neither locally with `pnpm dev:web` or `pnpm dev:docs`; `pnpm build:web` and `pnpm build:docs`\nproduce a Nitro `.output/` directory that Vercel picks up without extra configuration when the\nproject root directory is set to the app folder. Public links are centralized in\n`apps/web/src/site.ts` and currently use placeholders for the repository, npm, and container image.\nThe server Docker build installs only the `packages/*` projects, so the apps do not affect the\nimage.\n\n## Release build\n\n`pnpm release:check` runs the required local verification. `pnpm build:npm` compiles the five\nlibrary packages (`@agnx/protocol`, `@agnx/client`, `@agnx/core`, `@agnx/mcp`, `@agnx/http`) with\ntsc, rewrites their export maps to the compiled output, pins workspace dependencies to the release\nversion, and produces their tarballs plus the bundled `agnx` CLI tarball. `pnpm build:sea:artifact`\ncreates a platform SEA archive plus SHA-256 checksum, and `pnpm build:release` runs both after\nverification. `pnpm release:version` checks that every package, the CLI's `--version` literal, and\nthe docker provider's pinned sandbox image (`docker.io/assertlabs/agnx-sandbox:<version>`) agree. GitHub\nActions (`.github/workflows/ci.yml`) never publishes: it runs `pnpm release:check`, then\n`pnpm release:publish:dry` so every push proves both container images build and every npm tarball\npacks and smoke-tests, and it builds the SEA on Linux and macOS as downloadable artifacts. See\n`docs/release-acceptance.md` for the acceptance matrix. `scripts/release/packages.mjs` is the single\nlist of rel","readmeFilename":"README.md","_rev":"1-c0305d58b02e6b96fe5c92c56ca10f74"}