{"_id":"@agent-relay/sandbox-router","_rev":"6-33bc2a8bfaf441631baaf7ea3c270019","name":"@agent-relay/sandbox-router","dist-tags":{"latest":"0.5.0"},"versions":{"0.1.0":{"name":"@agent-relay/sandbox-router","version":"0.1.0","_id":"@agent-relay/sandbox-router@0.1.0","maintainers":[{"name":"willwashburn","email":"will@agentrelay.com"},{"name":"khaliqgant","email":"khaliqgant@gmail.com"}],"dist":{"shasum":"9e10c4543da97ea20a3561549bcfb40c26a9076d","tarball":"https://registry.npmjs.org/@agent-relay/sandbox-router/-/sandbox-router-0.1.0.tgz","fileCount":46,"integrity":"sha512-Q7LvDNjgiVALXGlLNT7fZ08joT19tMNOgk8CL3CQvsq4401EJfWqvk8iuZBVQmTzbOTdxn8EFS8I366YBNM4Iw==","signatures":[{"sig":"MEUCIENuvhzU6hW6T0zGktftrgyXCosoUaVxwU4KNryT434qAiEA0mWoz8rCbO/3Nkrfiq9sLUldH0BIkkt+QYd+H8j/g6Y=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":192843},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"cde43474967c896ecf60ec4b0b600caf7d22b4ff","scripts":{"demo":"tsx demo/src/main.ts","test":"node --test --import tsx \"src/**/*.test.ts\" \"demo/src/**/*.test.ts\"","build":"tsc -p tsconfig.build.json","typecheck":"tsc -p tsconfig.json --noEmit","demo:offline":"tsx demo/src/main.ts --no-live","demo:relayfile":"tsx demo/src/relayfile/run-local.ts","prepublishOnly":"npm run build","typecheck:demo":"tsc -p tsconfig.demo.json"},"_npmUser":{"name":"khaliqgant","email":"khaliqgant@gmail.com"},"_npmVersion":"10.9.8","description":"Private provider-routing substrate for Agent Relay sandboxes.","directories":{},"_nodeVersion":"22.23.2","dependencies":{"@agent-relay/sandbox":"0.1.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"e2b":"^2.45.0","tsx":"^4.20.5","typescript":"^5.9.2","@types/node":"^22.17.2","@daytonaio/sdk":"^0.180.0"},"_npmOperationalInternal":{"tmp":"tmp/sandbox-router_0.1.0_1787642887202_0.035347146572972665","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@agent-relay/sandbox-router","version":"0.2.0","_id":"@agent-relay/sandbox-router@0.2.0","maintainers":[{"name":"willwashburn","email":"will@agentrelay.com"},{"name":"khaliqgant","email":"khaliqgant@gmail.com"}],"dist":{"shasum":"700cd19dc285e76a3c56d32eb6642f5d40e7a3d7","tarball":"https://registry.npmjs.org/@agent-relay/sandbox-router/-/sandbox-router-0.2.0.tgz","fileCount":46,"integrity":"sha512-b2UQtKKIIUi8Wc0wZKkk2mj5bRXZ3C24oaXw2GJXIi4QeoKPh5ram3AxhwfEMQ9M/955NoTfIPQqCkDrQ7anXg==","signatures":[{"sig":"MEYCIQDvUt05FD5zlvCv8NldBGDF6qDLJx1cgfzPazU7mxwAgAIhAJ8EJATP5hVK/5gvNyM1/ZN1pvs+Me78GCzSFTwgmjvZ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDwbUxRFGoTzQB5uE5eCYte/oXhoeDQ+8brhlF/UbdNEAIgDiJGjEbzoWyjcfGensYysojlMJrzby7TqksprqW8E4g=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":192980},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"a007d55e1cb7f4f4a2cb4fd43006555591a43d89","scripts":{"demo":"tsx demo/src/main.ts","test":"node --test --import tsx \"src/**/*.test.ts\" \"demo/src/**/*.test.ts\"","build":"tsc -p tsconfig.build.json","prepack":"npm run build && node scripts/assert-pack-contents.mjs","typecheck":"tsc -p tsconfig.json --noEmit","pack:verify":"node scripts/assert-pack-contents.mjs","demo:offline":"tsx demo/src/main.ts --no-live","demo:relayfile":"tsx demo/src/relayfile/run-local.ts","typecheck:demo":"tsc -p tsconfig.demo.json"},"_npmUser":{"name":"khaliqgant","email":"khaliqgant@gmail.com"},"_npmVersion":"10.9.8","description":"Private provider-routing substrate for Agent Relay sandboxes.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"e2b":"^2.45.0","tsx":"^4.20.5","typescript":"^5.9.2","@types/node":"^22.17.2","@daytonaio/sdk":"^0.180.0","@agent-relay/sandbox":"0.1.10"},"peerDependencies":{"@agent-relay/sandbox":"^0.1.2"},"_npmOperationalInternal":{"tmp":"tmp/sandbox-router_0.2.0_1787647915361_0.7514140245269454","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@agent-relay/sandbox-router","version":"0.2.1","_id":"@agent-relay/sandbox-router@0.2.1","maintainers":[{"name":"willwashburn","email":"will@agentrelay.com"},{"name":"khaliqgant","email":"khaliqgant@gmail.com"}],"dist":{"shasum":"8990183535e5aaad87a56d51fe792ca5f69ba4e4","tarball":"https://registry.npmjs.org/@agent-relay/sandbox-router/-/sandbox-router-0.2.1.tgz","fileCount":46,"integrity":"sha512-RdpnoAxmJSJOxTNDRI6W0VBZji16p3W76AuU9j1x9vitY/AOr3zlm9C+B6HFzYHhvRBbm3DQUfOlmXqy+pKpVA==","signatures":[{"sig":"MEYCIQD5LuGxlZgrN94svgMllmYUEaXVhGYvclC+16mcPy+1NQIhAOSDfJtMAUjjPgaKF50LInVJ2lvgOoi8xMH++mlwXV8c","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIDFHyL0mQvHQ9Ywc018VDMnFZkoEyhtg7/75FopPRJ8xAiEAujZJAKxzDTEaY+rJg0+34JZpErwPxPvv37SAiZa/xtc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":201321},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"3b11cbefd0ec363dcbe2c3c6b8d468d3736141d0","scripts":{"demo":"tsx demo/src/main.ts","test":"node --test --import tsx \"src/**/*.test.ts\" \"demo/src/**/*.test.ts\"","build":"tsc -p tsconfig.build.json","prepack":"npm run build && node scripts/assert-pack-contents.mjs","typecheck":"tsc -p tsconfig.json --noEmit","pack:verify":"node scripts/assert-pack-contents.mjs","demo:offline":"tsx demo/src/main.ts --no-live","demo:relayfile":"tsx demo/src/relayfile/run-local.ts","typecheck:demo":"tsc -p tsconfig.demo.json"},"_npmUser":{"name":"khaliqgant","email":"khaliqgant@gmail.com"},"_npmVersion":"10.9.8","description":"Private provider-routing substrate for Agent Relay sandboxes.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"e2b":"^2.45.0","tsx":"^4.20.5","typescript":"^5.9.2","@types/node":"^22.17.2","@daytonaio/sdk":"^0.180.0","@agent-relay/sandbox":"0.1.10"},"peerDependencies":{"@agent-relay/sandbox":"^0.1.2"},"_npmOperationalInternal":{"tmp":"tmp/sandbox-router_0.2.1_1788013434000_0.4684080094058143","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@agent-relay/sandbox-router","version":"0.3.0","_id":"@agent-relay/sandbox-router@0.3.0","maintainers":[{"name":"willwashburn","email":"will@agentrelay.com"},{"name":"khaliqgant","email":"khaliqgant@gmail.com"}],"dist":{"shasum":"b434f88a6392163d5632fdf74a8a8fbf736ab699","tarball":"https://registry.npmjs.org/@agent-relay/sandbox-router/-/sandbox-router-0.3.0.tgz","fileCount":46,"integrity":"sha512-RGBW+0DUW9R6ICVdLq1fBqzhBvFIHDbqJzPzjvnSsCHwuOIzytiWsSM9vyWkZo/Lc2DUAiX8c7i2XhG+qgTkYw==","signatures":[{"sig":"MEQCIAioemwuauog5UPr84JMY7ahZEoVkfJ4IhZjQdoBFmiYAiBIdkKgP5IJoL4pSZxW917IWdMhLWwmikKRt3EyrOjElg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDAjtttCNPYqSWYyxES6UnEMYOyNoYZaUWHqbsYsjnX2wIhAJJhOEnzLo0C9/s3ReTCVcZJgTx29dUeFubIyC0mLkLw","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":202554},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"9499216bf59ab8ac16fed1781d3095d5cd9b2e0b","scripts":{"demo":"tsx demo/src/main.ts","test":"node --test --import tsx \"src/**/*.test.ts\" \"demo/src/**/*.test.ts\" \"flows/sandbox-matrix/probes/*.test.mjs\"","build":"tsc -p tsconfig.build.json","prepack":"npm run build && node scripts/assert-pack-contents.mjs","typecheck":"tsc -p tsconfig.json --noEmit","pack:verify":"node scripts/assert-pack-contents.mjs","demo:offline":"tsx demo/src/main.ts --no-live","demo:relayfile":"tsx demo/src/relayfile/run-local.ts","typecheck:demo":"tsc -p tsconfig.demo.json"},"_npmUser":{"name":"khaliqgant","email":"khaliqgant@gmail.com"},"_npmVersion":"10.9.8","description":"Private provider-routing substrate for Agent Relay sandboxes.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"e2b":"^2.45.0","tsx":"^4.20.5","typescript":"^5.9.2","@types/node":"^22.17.2","@daytonaio/sdk":"^0.180.0","@agent-relay/sandbox":"0.1.10"},"peerDependencies":{"@agent-relay/sandbox":"^0.1.2"},"_npmOperationalInternal":{"tmp":"tmp/sandbox-router_0.3.0_1788254922687_0.940534524705265","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@agent-relay/sandbox-router","version":"0.4.0","_id":"@agent-relay/sandbox-router@0.4.0","maintainers":[{"name":"willwashburn","email":"will@agentrelay.com"},{"name":"khaliqgant","email":"khaliqgant@gmail.com"}],"dist":{"shasum":"f5ff7c6834010f55be412c0b21d45a351510fc76","tarball":"https://registry.npmjs.org/@agent-relay/sandbox-router/-/sandbox-router-0.4.0.tgz","fileCount":66,"integrity":"sha512-7nnLzg4BmoyzE2Yx7WbaAKaQqCfCXhvJsveWJYJfqTIv2CLnkWGnnUCUXpNwZI1aC5kdtIGkam5HS/QL4wJPOA==","signatures":[{"sig":"MEUCIQCREeU0SMLV4P1bNJG33AWYsyRG4bOfcoQbhrGSEoLJzAIgf4OBBBzE0zgBNneQxy5P5VS8d8ckf/ZJiJ3NEBanHcI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIEncawXnajHlcl45JCrwHB6SmHMEONvnJEPUK7rZdnZRAiAl8QpvqBUdAv6jbtRpxaTYtbJDGUQ+4IyJNgcYIEfpkA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":282182},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"b5d6f1b33f790752114420f611a1605b78513b65","scripts":{"demo":"tsx demo/src/main.ts","test":"node --test --import tsx \"src/**/*.test.ts\" \"demo/src/**/*.test.ts\" \"flows/sandbox-matrix/probes/*.test.mjs\"","build":"tsc -p tsconfig.build.json","prepack":"npm run build && node scripts/assert-pack-contents.mjs","typecheck":"tsc -p tsconfig.json --noEmit","pack:verify":"node scripts/assert-pack-contents.mjs","demo:offline":"tsx demo/src/main.ts --no-live","demo:relayfile":"tsx demo/src/relayfile/run-local.ts","typecheck:demo":"tsc -p tsconfig.demo.json"},"_npmUser":{"name":"khaliqgant","email":"khaliqgant@gmail.com"},"_npmVersion":"10.9.8","description":"Credential-first sandbox construction and provider routing for agent workloads.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"e2b":"^2.45.0","tsx":"^4.20.5","typescript":"^5.9.2","@types/node":"^22.17.2","@daytonaio/sdk":"^0.180.0","@relayflows/cli":"1.0.7","@relayflows/core":"1.0.7","@agent-relay/sandbox":"0.1.10"},"peerDependencies":{"@daytonaio/sdk":">=0.180.0 <0.206.0","@agent-relay/sandbox":"^0.1.10"},"peerDependenciesMeta":{"@daytonaio/sdk":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sandbox-router_0.4.0_1788773878984_0.9709730929323526","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"_id":"@agent-relay/sandbox-router@0.5.0","dist":{"shasum":"ed32684393454b58cf1b10ed1235286d7bba8862","tarball":"https://registry.npmjs.org/@agent-relay/sandbox-router/-/sandbox-router-0.5.0.tgz","fileCount":70,"integrity":"sha512-oc25G9CL8xOUTQIMlVxHLkxfP6+dILQ13HhhcDsLQv+EG5ZGfyrdoFVOQ3iJUVlWxFyxP9BQExqWQYyKKjgalA==","signatures":[{"sig":"MEYCIQDHX3q7ZYkbVzxNBlL9DrNXfRcVej9acYgV5ASxdfpv2QIhAJ3Rf6U5dyixyfMnng0S5+VxjbaqRcuIQ9ZsPUSrvLmO","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDmQkZ4DY8lzxy40aZiAYo/QsKK6IVR8eLe8HoEaEHpPwIgMW07eCa8l7iMRr0CJzzz9Hd7obkewre29sTtDIBHkis="}],"unpackedSize":289913},"main":"dist/index.js","name":"@agent-relay/sandbox-router","type":"module","types":"dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"876622bc67a28a4416123d16531e6ecff67becb1","scripts":{"demo":"tsx demo/src/main.ts","test":"node --test --import tsx \"src/**/*.test.ts\" \"demo/src/**/*.test.ts\" \"flows/sandbox-matrix/probes/*.test.mjs\"","build":"tsc -p tsconfig.build.json","prepack":"npm run build && node scripts/assert-pack-contents.mjs","typecheck":"tsc -p tsconfig.json --noEmit","pack:verify":"node scripts/assert-pack-contents.mjs","demo:offline":"tsx demo/src/main.ts --no-live","demo:relayfile":"tsx demo/src/relayfile/run-local.ts","typecheck:demo":"tsc -p tsconfig.demo.json"},"version":"0.5.0","_npmUser":{"name":"khaliqgant","email":"khaliqgant@gmail.com"},"_npmVersion":"10.9.8","description":"Credential-first sandbox construction and provider routing for agent workloads.","directories":{},"maintainers":[{"name":"willwashburn","email":"will@agentrelay.com"},{"name":"khaliqgant","email":"khaliqgant@gmail.com"}],"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"e2b":"^2.45.0","tsx":"^4.20.5","typescript":"^5.9.2","@types/node":"^22.17.2","@daytonaio/sdk":"^0.180.0","@relayflows/cli":"1.0.7","@relayflows/core":"1.0.7","@agent-relay/sandbox":"0.1.16"},"peerDependencies":{"@daytonaio/sdk":">=0.180.0 <0.206.0","@agent-relay/sandbox":"^0.1.16"},"peerDependenciesMeta":{"@daytonaio/sdk":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sandbox-router_0.5.0_1789570721027_0.19342136866625026"}}},"time":{"created":"2026-08-25T07:28:07.025Z","modified":"2026-09-16T14:58:41.422Z","0.1.0":"2026-08-25T07:28:07.348Z","0.2.0":"2026-08-25T08:51:55.451Z","0.2.1":"2026-08-29T14:23:54.101Z","0.3.0":"2026-09-01T09:28:42.777Z","0.4.0":"2026-09-07T09:37:59.077Z","0.5.0":"2026-09-16T14:58:41.114Z"},"description":"Credential-first sandbox construction and provider routing for agent workloads.","maintainers":[{"name":"willwashburn","email":"will@agentrelay.com"},{"name":"khaliqgant","email":"khaliqgant@gmail.com"}],"readme":"# Agent Relay Sandbox Router\n\nCredential-first SDK and routing substrate for sandbox workloads.\nIt constructs and selects among configured provider pools while delegating every runtime\noperation to [`@agent-relay/sandbox`](https://github.com/AgentWorkforce/sandbox).\nThis repository does not fork, copy, or implement provider runtime adapters.\n\n## V1 boundary\n\nThe router owns:\n\n- provider-declared sandbox capability profiles and positive/negative requirements;\n- a stable provider registry interface;\n- requirements filtering before ranking;\n- deterministic `cost`, `latency`, `reliability`, and `balanced` ranking;\n- bounded acquisition-time fallback for explicitly classified, proven\n  pre-allocation failures (three attempts by default, ten maximum);\n- read-only account capacity measurement and explicitly ordered capacity spillover;\n- stable `sbx_…` locator ids, labels, and a durable provider-mapping seam;\n- private process-manifest validation and deterministic startup/teardown plans;\n- typed, sanitized routing errors and structured observability events;\n- best-effort cleanup when persistence fails after a sandbox was acquired.\n\nCredentials arrive only as explicit arguments and remain in runtime closures.\nThe router does not read provider keys from environment variables or files.\nIt does **not** own provider SDK adapters, secret storage, billing, metering, REST\nhandlers, database infrastructure, or cloud policy.\nThe in-memory registry and mapping store are test/local references, not\nproduction storage.\n\nThe process-manifest scaffold is also intentionally private composition logic.\nIt makes useful-node readiness, history-safe teardown, and evidence-gated idle\npolicy explicit without changing the public runtime port. See\n[`docs/process-manifest-design.md`](docs/process-manifest-design.md).\n\nThe credential-first entrypoint supports Daytona, E2B, Vercel, Freestyle,\nAgent37, and hosted Microsandbox through `@agent-relay/sandbox >=0.1.10`.\nCloud already consumes the routing package; its environment/secret lookup,\nsnapshot selection, logger, safety configuration, and signed Microsandbox\nbridge remain caller-owned.\n\nThis is a provider-pool ranker for the cloud acquisition boundary. It is not a\nfleet/node scheduler. Requests selecting `fleet`, a concrete node affinity, or\nno sandbox are refused with `RoutingNotApplicableError`, leaving Factory's\nexisting fleet placement authority and consumer-local execution untouched.\n\n## Control flow\n\n```text\nSandboxRequest\n      │\n      ├─ validate request and attempt bound\n      ├─ filter provider-declared correctness/trust requirements\n      ├─ resolve request-scoped operational signals for survivors\n      ├─ filter availability and time-to-ready budget\n      ├─ deterministically rank eligible pools\n      └─ attempt at most N providers\n             │\n             ├─ SandboxRuntime.launch(...)\n             ├─ allocate sbx_<32 hex>\n             ├─ persist sbx id ↔ labels ↔ provider id ↔ provider sandbox id\n             └─ return only the Agent Relay id\n```\n\nProvider registration order never breaks a scoring tie; stable provider id\nlexicographic order does. Balanced ranking normalizes the current eligible set\nand applies 35% cost, 25% selected latency objective, and 40%\ncompletion-within-budget reliability. Capabilities are never probed and are\nnever part of a \"more is better\" score: registrations declare them, requests\nmay require or forbid them, and the router trusts those declarations.\n\n## Credential-first construction\n\nInstall `@agent-relay/sandbox-router` and `@agent-relay/sandbox@^0.1.10`, plus\nonly the SDKs for providers you use. Daytona uses `@daytonaio/sdk` in\n`>=0.180.0 <0.206.0`; E2B uses `e2b` in `>=2.35.0 <3.0.0`. The sandbox package\nlists the optional SDK requirements for Vercel, Freestyle, and Microsandbox.\nAgent37 uses its HTTP adapter.\n\n### Provider credential readiness\n\nTrusted backends can supply `provisioning: { hasAvailableCredentials(providerId) }`\nto either `SandboxRouter` or `createSandboxRouter`. This tenant/environment-scoped\nport reads existing credential readiness and returns only a boolean. It never\ncreates accounts or returns credentials. Account setup must complete before\nacquisition; omitting the port preserves existing behavior.\n\nBoth `provision()` and `provisionDetached()` exclude an unavailable binding before\nresolving operational signals, ranking or reading capacity. A thrown error or a\nnon-boolean result aborts acquisition with a sanitized `ProvisioningAvailabilityError`.\nReady credentials do not imply available capacity: enabled capacity routing still\nrefuses unknown or exhausted capacity. Readiness gates new allocation; it does\nnot prevent cleanup of an already acquired sandbox.\n\n### Capacity measurement and spillover\n\nEach builtin registration has a `CapacityReader.getCapacity()` method. Call\n`await router.getCapacity()` to read all configured accounts without creating\nor starting a sandbox. Results are either `{ status: \"known\", resources }` or\n`{ status: \"unknown\", reason }`. Each resource reports `used`, `limit`, `unit`,\n`headroom`, and the configured pool's allocation per launch (`required`). CPU\nuses vCPU, memory/disk use MiB, and instance quotas use sandbox slots.\n\nEnable admission checks with an explicit ordered allowlist:\n\n```ts\nconst router = createSandboxRouter({\n  providers: {\n    daytona: {\n      apiKey: daytonaKey,\n      snapshot: snapshotName,\n      capacity: {\n        limits: { cpu: 250 }, // This organization's verified cap, not a universal default.\n        demand: { cpu: 2 },   // Actual allocation of the configured snapshot.\n      },\n    },\n    microsandbox: {\n      apiKey: microboxKey, image: microboxImage, homeDir: \"/root\",\n      capacity: { limits: { sandboxes: verifiedMicroboxSlotLimit } },\n    },\n  },\n  capacity: { providerOrder: [\"daytona\", \"microsandbox\"], timeoutMs: 5000 },\n});\nconst measurements = await router.getCapacity(); // Only reads inventory.\n```\n\nThe router applies capabilities, resource requirements, exact `providerId`, and\noperational eligibility first, then uses `providerOrder` in place of strategy\nranking. Unlisted providers cannot be selected. Before each launch it checks\nfresh capacity against the greater of the configured allocation and request\nrequirements. Every reported dimension must fit. Full, unknown, malformed,\nfailed, or timed-out readers are skipped, without consuming a launch attempt.\nIf none has room, `ProviderCapacityExhaustedError` carries code\n`PROVIDER_CAPACITY_EXHAUSTED` and sanitized `UNKNOWN`/`EXHAUSTED` rejections.\nThe same behavior applies to `provisionDetached`. An exact provider constraint\nnever spills outside that provider. `acquisitionFallback: \"forbidden\"` limits\nactual launches to one; capacity skips perform no acquisition.\n\n| Provider | Builtin measurement | Required caller limit |\n| --- | --- | --- |\n| Daytona | Complete organization inventory, summed across all states/workspaces | CPU and/or memory/disk, with matching per-launch demand; memory/disk inputs are GiB |\n| E2B | Paginated running sandbox inventory for the supplied API key | Running sandbox concurrency |\n| Microsandbox / Microbox | Paginated inventory across all states, using the supplied hosted backend | Total account sandbox slots |\n| Agent37 | Account instance inventory across all states | Total account instance slots |\n| Vercel | Explicit `unknown: NOT_SUPPORTED`; the adapter list is prefix-scoped | Supply a custom account-wide reader |\n| Freestyle | Explicit `unknown: NOT_SUPPORTED`; the generic count is a placeholder | Supply a custom account-wide reader |\n\nInventory alone does not reveal an account quota. Missing verified limits\nproduce `unknown: LIMIT_NOT_CONFIGURED`, never zero or an inferred pricing-tier\ncap. Daytona also returns unknown when snapshot demand is missing. Its\nconservative all-state inventory sum preserves the cloud helper's semantics;\nit may overestimate currently reserved compute. Only configured quota dimensions\nare measured, so configure every limit you need to enforce. Capacity is a\npoint-in-time observation, not a reservation: concurrent callers can race, and\nprovider create failures retain the existing outcome-aware fallback policy.\nReads have a five-second default deadline; caller readers should cancel their\nown underlying I/O when needed. No cache or credential state is shared across\nrouter instances.\n\nThe constructor accepts a custom `{ getCapacity: async () => ... }` in a\nprovider's `capacity` field for quota APIs or deployments outside these builtin\nsurfaces. A custom reader must report account-wide usage and actual launch\ndemand. For direct registry construction, attach it as `ProviderDefinition.capacity`.\nExisting callers that omit the router-level `capacity` policy keep their\nexisting strategy-based routing behavior; measurement is still available.\n\n`summarizeDaytonaCapacity` and `parseDaytonaCapacityLimits` are ported from\n[cloud's helper](https://github.com/AgentWorkforce/cloud/blob/main/packages/web/lib/fleet/daytona-capacity.ts).\nThe display summary retains warning/critical thresholds, organization totals,\nworkspace-scoped idle details, limit provenance, and `provisioningCheck:\n\"not_covered\"`. Admission uses strict measurement validation rather than the\ndisplay helper's numeric coercion. See also the\n[Daytona list API](https://www.daytona.io/docs/en/typescript-sdk/daytona/#list)\nand [E2B listing contract](https://github.com/e2b-dev/E2B/blob/main/packages/js-sdk/src/sandbox/sandboxApi.ts).\n\nRun `node scripts/verify-capacity-mutations.mjs` to prove the spillover and\nper-provider assertions reject deliberate regressions. The runner uses fake\ncredentials and local fixtures, restores each mutation, and requires the\nrestored tests to pass.\n\n### Basic construction\n\n```ts\nimport { createSandboxRouter } from \"@agent-relay/sandbox-router\";\n\nconst router = createSandboxRouter({\n  providers: {\n    daytona: {\n      apiKey: daytonaKey,\n      createParams: { autoStopInterval: 5, autoDeleteInterval: 0 },\n    },\n    e2b: { apiKey: e2bKey },\n  },\n});\n\nconst sandbox = await router.provision({\n  launch: { labels: { requestedBy: \"my-application\" } },\n});\ntry {\n  const result = await router.runScript(sandbox.id, { command: \"echo hello\" });\n  console.log(result.output);\n} finally {\n  await router.destroy(sandbox.id);\n}\n```\n\n`createSandboxRouter` is synchronous; provider adapters and optional SDKs load\non first operation. The returned router extends `SandboxRouter`, with\n`runScript(id, options)` and `destroy(id)` resolving the provider through the\nstored mapping. `destroy` tolerates an already-deleted provider sandbox.\n`createProviderRuntime(provider, options)` exposes the same credential-safe\nruntime construction for callers that maintain their own registry.\n\n| Provider | Required explicit configuration | Convenience defaults |\n| --- | --- | --- |\n| Daytona | `apiKey` | API `https://app.daytona.io/api`, target `us`, home `/home/daytona` |\n| E2B | `apiKey` | Template `base` |\n| Vercel | `token`, `teamId`, `projectId`, `defaultHomeDir`, `namePrefix` | Adapter defaults |\n| Freestyle | `apiKey`, `defaultHomeDir`, `namePrefix`, `persistence` | Adapter defaults |\n| Agent37 | `apiKey`, `baseUrl`, `defaultHomeDir` | Adapter defaults |\n| Microsandbox | `apiKey`, `homeDir`, `image` | Hosted backend; optional `url` |\n\nOverride Daytona's home directory when selecting a custom snapshot. E2B's\n`base` template is for basic workloads; a custom template must be supplied for\napplication-specific binaries. Microsandbox profile files and local execution\nand host-local snapshots are excluded from this credential-first surface.\n\nProvider entries also accept `capabilities` and `signals`. Without explicit\nmetadata, the factory advertises only synchronous execution and file transfer;\nresource limits are zero and region/image/isolation/durability claims are empty.\nThus requests requiring those properties are refused until you declare the\nconfigured pool's envelope. Default signals are equal ranking placeholders,\nnot measured price, latency, reliability, or live health. Supply signals or\n`resolveRoutingSignals` to make informed multi-provider selections.\n\nMappings default to memory. Supply `mappings` for durable, cross-process\nidentity. Provider keys never enter the mapping, observability events, or the\nserializable runtime facade. Provider SDK failures, including construction,\nlookup, execution and destruction, become `ProviderRuntimeError` with only\nprovider and operation; original messages, stacks and causes are discarded.\nA failure classifier on this surface receives that sanitized error, so it must\nnot infer a safe retry from missing provider details. Acquisition failures stay\noutcome-unknown and terminal by default.\n\nThe original `new SandboxRouter({ registry, mappings, ... })` and\n`routeProviders(request, providers)` signatures remain unchanged. The explicit\nruntime path still supports caller-owned adapters and provider error classifiers.\n\n## Registration and provisioning\n\n```ts\nimport {\n  InMemoryProviderRegistry,\n  SandboxRouter,\n  asProviderId,\n} from \"@agent-relay/sandbox-router\";\n\nconst registry = new InMemoryProviderRegistry([\n  {\n    id: asProviderId(\"primary-eu\"),\n    runtime: externallyConstructedRuntime,\n    capabilities: externallyDeclaredCapabilities,\n    signals: {\n      estimatedCost: 0.12,\n      coldStartLatencyMs: 850,\n      readyLatencyMs: 12_000,\n      completionWithinBudgetRate: 0.999,\n      available: true,\n    },\n  },\n]);\n\nconst router = new SandboxRouter({\n  registry,\n  mappings: durableMappingStore,\n  observer: structuredEventSink,\n  classifyProviderFailure: (error, provider) => {\n    if (isProvenPreAllocationCapacityFailure(error, provider)) {\n      return { code: \"PROVIDER_CAPACITY_PRE_ALLOCATION\", retryable: true };\n    }\n    return { code: \"PROVIDER_LAUNCH_OUTCOME_UNKNOWN\", retryable: false };\n  },\n});\n\nconst sandbox = await router.provision({\n  resources: { cpu: 4, memoryMb: 8192 },\n  requires: {\n    requiredCapabilities: [\"pty\", \"publicPorts\"],\n    forbiddenCapabilities: [\"nestedUserNamespaces\"],\n    image: { identity: \"agent-v4\", requiredBinaries: [\"node\"] },\n    timeout: {\n      semantics: [\"command-lifetime\"],\n      maxReadyLatencyMs: 30_000,\n      completionBudgetMs: 1_800_000,\n    },\n  },\n  region: { preferred: [\"eu-west\"], required: true },\n  routing: {\n    strategy: \"balanced\",\n    latencyObjective: \"ready\",\n    acquisitionFallback: \"allowed\",\n    maxAcquisitionAttempts: 3,\n  },\n});\n```\n\nRuntime construction is intentionally external. A configured runtime may hold\nits provider authorization internally, but credentials never enter a provider\ndefinition, request, routing event, mapping, or router error.\n\nThe classifier in this example represents caller-owned provider knowledge;\n`isProvenPreAllocationCapacityFailure` must only return true when the provider\nguarantees that allocation did not occur. Omitting the classifier is safer: all\nlaunch failures are terminal by default.\n\n## Routing-signal seam\n\n`ProviderRoutingSignals` are comparable selection inputs, not charges or usage\nrecords. `estimatedCost` means the expected cost of the relevant acquisition,\nwhile `coldStartLatencyMs` and `readyLatencyMs` deliberately separate SDK\nresponse from actual workload readiness. `completionWithinBudgetRate` counts\nhangs as failures instead of treating only returned errors as unreliable. A caller can supply\n`resolveRoutingSignals(provider, request)` to incorporate live health or its own\nworkload knowledge—for example, cold acquire plus destroy multiplied by an\nexpected step count. That resolver stays outside billing and metering.\n\n## Placement, fallback, and identity guardrails\n\n- Fallback exists only inside `provision`, before any exec, lease, or stateful\n  operation. The credential-first execution path resolves the original provider\n  mapping and never reroutes resident work. Leased/resident callers set\n  `acquisitionFallback: \"forbidden\"`.\n- `acquisitionFallback: \"allowed\"` and an attempt limit do not make a launch\n  error retryable. Advancing to another provider additionally requires an\n  explicit caller classifier to return `retryable: true` for a narrowly proven\n  pre-allocation failure, such as provider capacity rejection before creation.\n- Provider capability is declared, not inferred from method presence. The\n  vocabulary includes image/binary identity, egress class, timeout semantics\n  and granularity, state durability, ports/tailnet, long-running process/PID/\n  PTY/restart needs, unattended enrollment, and required restrictions.\n- `sbx_…` is a provider-independent locator, not workload identity. It coexists\n  with labels. Durable state-directory identity, Factory `invocationId`, and PID\n  tuples remain owned by their existing lifecycle systems and are not replaced.\n- `placement.authority: \"none\" | \"fleet\"` and `nodeAffinity` explicitly bypass\n  provider routing; this library never becomes a second fleet scheduler.\n\n## Failure semantics\n\n- Requirements or availability failures never call a provider.\n- Generic or unclassified launch failures are outcome-unknown and terminal by\n  default because a provider may have allocated remotely before throwing.\n- Only a caller-supplied classifier can authorize fallback by returning\n  `retryable: true`; callers must reserve it for narrowly proven pre-allocation\n  failures. Retryable failures advance only until the acquisition attempt bound.\n- Classifiers expose only sanitized codes; terminal failures raise\n  `ProviderProvisioningError` and emit an attempt-failed event with\n  `retryable: false`.\n- Raw provider error messages are not emitted or copied into exhaustion data.\n- Mapping-store failure after acquisition is not a provider failure: the router\n  attempts to destroy that sandbox, emits whether cleanup succeeded, and stops.\n- Observer failures never affect selection, acquisition fallback, or provisioning.\n\n## Public-base gaps (not copied here)\n\nThese belong in `@agent-relay/sandbox` or its provider adapters if required:\n\n1. **Portable launch requirements:** `SandboxRuntime.launch` accepts only\n   name/env/labels/timeout, not resources, region, or isolation. V1 therefore\n   treats each registration as a preconfigured provider pool and filters using\n   its declared envelope; it does not inject unsupported fields. Passing\n   portable requirements into provider creation needs a public-port decision.\n2. **Partial-allocation cleanup:** `SandboxRuntime.launch` returns a handle only\n   on success. If an adapter allocates remotely and then throws, the router has\n   no cleanup handle. Leak-safe recovery for that case must be guaranteed by the\n   adapter or added to the public port. The router therefore treats an\n   unclassified launch exception as terminal rather than risking a duplicate\n   allocation through automatic fallback.\n3. **Portable declared capability vocabulary:** the public outer runtime\n   descriptor covers a few operational methods, not image identity, binaries,\n   egress, timeout semantics, state durability, trust restrictions, regions,\n   resources, network, or GPU envelopes. The commercial registry owns these\n   provider declarations for now. They must not be implemented through probes;\n   present-but-no-op methods have already proved that inference unsafe.\n4. **Optional peer declaration surface:** the public package declarations\n   reference Daytona peer types even for an adapter-free type-only consumer.\n   This package uses `skipLibCheck`; the public base should isolate optional\n   provider declaration imports.\n\n## Development\n\nRequires Node.js 20 or newer.\n\n```bash\nnpm install\nnpm run build\nnpm run typecheck\nnpm test\n```\n\nBehavioral tests are named in must-fire/must-not-fire pairs so both the trigger\nand its guard are visible. The package is private and has no publish script.\n\nThe credential-first regression proof can be reproduced with\n`node scripts/verify-tenant0-mutations.mjs`. It temporarily mutates and restores\nsource, asserts each negative control fails, then runs the clean regression suite.\nRun it without concurrent source edits.\n\nThe opt-in live proof is `npm run build` followed by\n`node scripts/prove-tenant0-daytona.mjs`. It reads Daytona credentials from\n`~/Projects/AgentWorkforce/cloud/.env` at point of use, provisions a bounded\nsandbox, verifies command stdout, destroys it, and audits its unique labels.\nIt writes only a sanitized transcript under `.workflow-artifacts/tenant0-0906/`.\n","readmeFilename":"README.md"}