@agent-relay/sandbox-router

中文文档
0.6.0发布于 2026/09/21TypeScriptES Module零依赖近 30 天下载 3070

Credential-first sandbox construction and provider routing for agent workloads.

原始使用文档
文档目录12 节
  1. V1 boundary
  2. Control flow
  3. Credential-first construction
  4. Provider credential readiness
  5. Capacity measurement and spillover
  6. Basic construction
  7. Registration and provisioning
  8. Routing-signal seam
  9. Placement, fallback, and identity guardrails
  10. Failure semantics
  11. Public-base gaps (not copied here)
  12. Development

Agent Relay Sandbox Router

Credential-first SDK and routing substrate for sandbox workloads. It constructs and selects among configured provider pools while delegating every runtime operation to @agent-relay/sandbox. This repository does not fork, copy, or implement provider runtime adapters.

V1 boundary

The router owns:

  • provider-declared sandbox capability profiles, including account-scoped maximum lifetime, and positive/negative requirements;
  • a stable provider registry interface;
  • requirements filtering before ranking;
  • deterministic cost, latency, reliability, and balanced ranking;
  • bounded acquisition-time fallback for explicitly classified, proven pre-allocation failures (three attempts by default, ten maximum);
  • read-only account capacity measurement and explicitly ordered capacity spillover;
  • stable sbx_… locator ids, labels, and a durable provider-mapping seam;
  • private process-manifest validation and deterministic startup/teardown plans;
  • typed, sanitized routing errors and structured observability events;
  • best-effort cleanup when persistence fails after a sandbox was acquired.

Credentials arrive only as explicit arguments and remain in runtime closures. The router does not read provider keys from environment variables or files. It does not own provider SDK adapters, secret storage, billing, metering, REST handlers, database infrastructure, or cloud policy. The in-memory registry and mapping store are test/local references, not production storage.

The process-manifest scaffold is also intentionally private composition logic. It makes useful-node readiness, history-safe teardown, and evidence-gated idle policy explicit without changing the public runtime port. See docs/process-manifest-design.md.

The credential-first entrypoint supports Daytona, E2B, Vercel, Freestyle, Agent37, and hosted Microsandbox through @agent-relay/sandbox >=0.1.10. Cloud already consumes the routing package; its environment/secret lookup, snapshot selection, logger, safety configuration, and signed Microsandbox bridge remain caller-owned.

This is a provider-pool ranker for the cloud acquisition boundary. It is not a fleet/node scheduler. Requests selecting fleet, a concrete node affinity, or no sandbox are refused with RoutingNotApplicableError, leaving Factory's existing fleet placement authority and consumer-local execution untouched.

Control flow

SandboxRequest
      │
      ├─ validate request and attempt bound
      ├─ filter provider-declared correctness/trust requirements
      ├─ resolve request-scoped operational signals for survivors
      ├─ filter availability and time-to-ready budget
      ├─ deterministically rank eligible pools
      └─ attempt at most N providers
             │
             ├─ SandboxRuntime.launch(...)
             ├─ allocate sbx_<32 hex>
             ├─ persist sbx id ↔ labels ↔ provider id ↔ provider sandbox id
             └─ return only the Agent Relay id

Provider registration order never breaks a scoring tie; stable provider id lexicographic order does. Balanced ranking normalizes the current eligible set and applies 35% cost, 25% selected latency objective, and 40% completion-within-budget reliability. Capabilities are never probed and are never part of a "more is better" score: registrations declare them, requests may require or forbid them, and the router trusts those declarations.

Credential-first construction

Install @agent-relay/sandbox-router and @agent-relay/sandbox@^0.1.10, plus only the SDKs for providers you use. Daytona uses @daytonaio/sdk in >=0.180.0 <0.206.0; E2B uses e2b in >=2.35.0 <3.0.0. The sandbox package lists the optional SDK requirements for Vercel, Freestyle, and Microsandbox. Agent37 uses its HTTP adapter.

Provider credential readiness

Trusted backends can supply provisioning: { hasAvailableCredentials(providerId) } to either SandboxRouter or createSandboxRouter. This tenant/environment-scoped port reads existing credential readiness and returns only a boolean. It never creates accounts or returns credentials. Account setup must complete before acquisition; omitting the port preserves existing behavior.

Both provision() and provisionDetached() exclude an unavailable binding before resolving operational signals, ranking or reading capacity. A thrown error or a non-boolean result aborts acquisition with a sanitized ProvisioningAvailabilityError. Ready credentials do not imply available capacity: enabled capacity routing still refuses unknown or exhausted capacity. Readiness gates new allocation; it does not prevent cleanup of an already acquired sandbox.

Capacity measurement and spillover

Each builtin registration has a CapacityReader.getCapacity() method. Call await router.getCapacity() to read all configured accounts without creating or starting a sandbox. Results are either { status: "known", resources } or { status: "unknown", reason }. Each resource reports used, limit, unit, headroom, and the configured pool's allocation per launch (required). CPU uses vCPU, memory/disk use MiB, and instance quotas use sandbox slots.

Enable admission checks with an explicit ordered allowlist:

const router = createSandboxRouter({
  providers: {
    daytona: {
      apiKey: daytonaKey,
      snapshot: snapshotName,
      capacity: {
        limits: { cpu: 250 }, // This organization's verified cap, not a universal default.
        demand: { cpu: 2 },   // Actual allocation of the configured snapshot.
      },
    },
    microsandbox: {
      apiKey: microboxKey, image: microboxImage, homeDir: "/root",
      capacity: { limits: { sandboxes: verifiedMicroboxSlotLimit } },
    },
  },
  capacity: { providerOrder: ["daytona", "microsandbox"], timeoutMs: 5000 },
});
const measurements = await router.getCapacity(); // Only reads inventory.

The router applies capabilities, resource requirements, exact providerId, and operational eligibility first, then uses providerOrder in place of strategy ranking. Unlisted providers cannot be selected. Before each launch it checks fresh capacity against the greater of the configured allocation and request requirements. Every reported dimension must fit. Full, unknown, malformed, failed, or timed-out readers are skipped, without consuming a launch attempt. If none has room, ProviderCapacityExhaustedError carries code PROVIDER_CAPACITY_EXHAUSTED and sanitized UNKNOWN/EXHAUSTED rejections. The same behavior applies to provisionDetached. An exact provider constraint never spills outside that provider. acquisitionFallback: "forbidden" limits actual launches to one; capacity skips perform no acquisition.

ProviderBuiltin measurementRequired caller limit
DaytonaComplete organization inventory, summed across all states/workspacesCPU and/or memory/disk, with matching per-launch demand; memory/disk inputs are GiB
E2BPaginated running sandbox inventory for the supplied API keyRunning sandbox concurrency
Microsandbox / MicroboxPaginated inventory across all states, using the supplied hosted backendTotal account sandbox slots
Agent37Account instance inventory across all statesTotal account instance slots
VercelExplicit unknown: NOT_SUPPORTED; the adapter list is prefix-scopedSupply a custom account-wide reader
FreestyleExplicit unknown: NOT_SUPPORTED; the generic count is a placeholderSupply a custom account-wide reader

Inventory alone does not reveal an account quota. Missing verified limits produce unknown: LIMIT_NOT_CONFIGURED, never zero or an inferred pricing-tier cap. Daytona also returns unknown when snapshot demand is missing. Its conservative all-state inventory sum preserves the cloud helper's semantics; it may overestimate currently reserved compute. Only configured quota dimensions are measured, so configure every limit you need to enforce. Capacity is a point-in-time observation, not a reservation: concurrent callers can race, and provider create failures retain the existing outcome-aware fallback policy. Reads have a five-second default deadline; caller readers should cancel their own underlying I/O when needed. No cache or credential state is shared across router instances.

The constructor accepts a custom { getCapacity: async () => ... } in a provider's capacity field for quota APIs or deployments outside these builtin surfaces. A custom reader must report account-wide usage and actual launch demand. For direct registry construction, attach it as ProviderDefinition.capacity. Existing callers that omit the router-level capacity policy keep their existing strategy-based routing behavior; measurement is still available.

summarizeDaytonaCapacity and parseDaytonaCapacityLimits are ported from cloud's helper. The display summary retains warning/critical thresholds, organization totals, workspace-scoped idle details, limit provenance, and provisioningCheck: "not_covered". Admission uses strict measurement validation rather than the display helper's numeric coercion. See also the Daytona list API and E2B listing contract.

Run node scripts/verify-capacity-mutations.mjs to prove the spillover and per-provider assertions reject deliberate regressions. The runner uses fake credentials and local fixtures, restores each mutation, and requires the restored tests to pass.

Basic construction

import { createSandboxRouter } from "@agent-relay/sandbox-router";

const router = createSandboxRouter({
  providers: {
    daytona: {
      apiKey: daytonaKey,
      createParams: { autoStopInterval: 5, autoDeleteInterval: 0 },
    },
    e2b: { apiKey: e2bKey },
  },
});

const sandbox = await router.provision({
  launch: { labels: { requestedBy: "my-application" } },
});
try {
  const result = await router.runScript(sandbox.id, { command: "echo hello" });
  console.log(result.output);
} finally {
  await router.destroy(sandbox.id);
}

createSandboxRouter is synchronous; provider adapters and optional SDKs load on first operation. The returned router extends SandboxRouter, with runScript(id, options) and destroy(id) resolving the provider through the stored mapping. destroy tolerates an already-deleted provider sandbox. createProviderRuntime(provider, options) exposes the same credential-safe runtime construction for callers that maintain their own registry.

ProviderRequired explicit configurationConvenience defaults
DaytonaapiKeyAPI https://app.daytona.io/api, target us, home /home/daytona
E2BapiKeyTemplate base
Verceltoken, teamId, projectId, defaultHomeDir, namePrefixAdapter defaults
FreestyleapiKey, defaultHomeDir, namePrefix, persistenceAdapter defaults
Agent37apiKey, baseUrl, defaultHomeDirAdapter defaults
MicrosandboxapiKey, homeDir, imageHosted backend; optional url

Override Daytona's home directory when selecting a custom snapshot. E2B's base template is for basic workloads; a custom template must be supplied for application-specific binaries. Microsandbox profile files and local execution and host-local snapshots are excluded from this credential-first surface.

Provider entries also accept capabilities and signals. Without explicit metadata, the factory advertises only synchronous execution and file transfer; resource limits are zero and region/image/isolation/durability claims are empty. Thus requests requiring those properties are refused until you declare the configured pool's envelope. Default signals are equal ranking placeholders, not measured price, latency, reliability, or live health. Supply signals or resolveRoutingSignals to make informed multi-provider selections.

Mappings default to memory. Supply mappings for durable, cross-process identity. Provider keys never enter the mapping, observability events, or the serializable runtime facade. Provider SDK failures, including construction, lookup, execution and destruction, become ProviderRuntimeError with only provider and operation; original messages, stacks and causes are discarded. A failure classifier on this surface receives that sanitized error, so it must not infer a safe retry from missing provider details. Acquisition failures stay outcome-unknown and terminal by default.

The original new SandboxRouter({ registry, mappings, ... }) and routeProviders(request, providers) signatures remain unchanged. The explicit runtime path still supports caller-owned adapters and provider error classifiers.

Registration and provisioning

import {
  InMemoryProviderRegistry,
  SandboxRouter,
  asProviderId,
} from "@agent-relay/sandbox-router";

const registry = new InMemoryProviderRegistry([
  {
    id: asProviderId("primary-eu"),
    runtime: externallyConstructedRuntime,
    capabilities: externallyDeclaredCapabilities,
    signals: {
      estimatedCost: 0.12,
      coldStartLatencyMs: 850,
      readyLatencyMs: 12_000,
      completionWithinBudgetRate: 0.999,
      available: true,
    },
  },
]);

const router = new SandboxRouter({
  registry,
  mappings: durableMappingStore,
  observer: structuredEventSink,
  classifyProviderFailure: (error, provider) => {
    if (isProvenPreAllocationCapacityFailure(error, provider)) {
      return { code: "PROVIDER_CAPACITY_PRE_ALLOCATION", retryable: true };
    }
    return { code: "PROVIDER_LAUNCH_OUTCOME_UNKNOWN", retryable: false };
  },
});

const sandbox = await router.provision({
  resources: { cpu: 4, memoryMb: 8192 },
  requires: {
    requiredCapabilities: ["pty", "publicPorts"],
    forbiddenCapabilities: ["nestedUserNamespaces"],
    image: { identity: "agent-v4", requiredBinaries: ["node"] },
    timeout: {
      semantics: ["command-lifetime"],
      maxReadyLatencyMs: 30_000,
      requiredSandboxLifetimeMs: 30 * 60_000,
      completionBudgetMs: 1_800_000,
    },
  },
  region: { preferred: ["eu-west"], required: true },
  routing: {
    strategy: "balanced",
    latencyObjective: "ready",
    acquisitionFallback: "allowed",
    maxAcquisitionAttempts: 3,
  },
});

Runtime construction is intentionally external. A configured runtime may hold its provider authorization internally, but credentials never enter a provider definition, request, routing event, mapping, or router error.

Maximum sandbox lifetime is a correctness constraint, not a ranking signal. Each registered pool may declare capabilities.timeout.maxSandboxLifetimeMs for the provider account behind that runtime. A finite value is an inclusive ceiling; null explicitly means unbounded; omission means the account limit is unknown. When a request supplies requires.timeout.requiredSandboxLifetimeMs, unknown and insufficient pools are rejected with SANDBOX_LIFETIME before ranking or acquisition. Requests without a lifetime requirement remain backward compatible with registrations that omit the declaration.

The classifier in this example represents caller-owned provider knowledge; isProvenPreAllocationCapacityFailure must only return true when the provider guarantees that allocation did not occur. Omitting the classifier is safer: all launch failures are terminal by default.

Routing-signal seam

ProviderRoutingSignals are comparable selection inputs, not charges or usage records. estimatedCost means the expected cost of the relevant acquisition, while coldStartLatencyMs and readyLatencyMs deliberately separate SDK response from actual workload readiness. completionWithinBudgetRate counts hangs as failures instead of treating only returned errors as unreliable. A caller can supply resolveRoutingSignals(provider, request) to incorporate live health or its own workload knowledge—for example, cold acquire plus destroy multiplied by an expected step count. That resolver stays outside billing and metering.

Placement, fallback, and identity guardrails

  • Fallback exists only inside provision, before any exec, lease, or stateful operation. The credential-first execution path resolves the original provider mapping and never reroutes resident work. Leased/resident callers set acquisitionFallback: "forbidden".
  • acquisitionFallback: "allowed" and an attempt limit do not make a launch error retryable. Advancing to another provider additionally requires an explicit caller classifier to return retryable: true for a narrowly proven pre-allocation failure, such as provider capacity rejection before creation.
  • Provider capability is declared, not inferred from method presence. The vocabulary includes image/binary identity, egress class, timeout semantics and granularity, provider-account lifetime, state durability, ports/tailnet, long-running process/PID/PTY/restart needs, unattended enrollment, and required restrictions.
  • sbx_… is a provider-independent locator, not workload identity. It coexists with labels. Durable state-directory identity, Factory invocationId, and PID tuples remain owned by their existing lifecycle systems and are not replaced.
  • placement.authority: "none" | "fleet" and nodeAffinity explicitly bypass provider routing; this library never becomes a second fleet scheduler.

Failure semantics

  • Requirements or availability failures never call a provider.
  • Generic or unclassified launch failures are outcome-unknown and terminal by default because a provider may have allocated remotely before throwing.
  • Only a caller-supplied classifier can authorize fallback by returning retryable: true; callers must reserve it for narrowly proven pre-allocation failures. Retryable failures advance only until the acquisition attempt bound.
  • Classifiers expose only sanitized codes; terminal failures raise ProviderProvisioningError and emit an attempt-failed event with retryable: false.
  • Raw provider error messages are not emitted or copied into exhaustion data.
  • Mapping-store failure after acquisition is not a provider failure: the router attempts to destroy that sandbox, emits whether cleanup succeeded, and stops.
  • Observer failures never affect selection, acquisition fallback, or provisioning.

Public-base gaps (not copied here)

These belong in @agent-relay/sandbox or its provider adapters if required:

  1. Portable launch requirements: SandboxRuntime.launch accepts only name/env/labels/timeout, not resources, region, or isolation. V1 therefore treats each registration as a preconfigured provider pool and filters using its declared envelope; it does not inject unsupported fields. Passing portable requirements into provider creation needs a public-port decision.
  2. Partial-allocation cleanup: SandboxRuntime.launch returns a handle only on success. If an adapter allocates remotely and then throws, the router has no cleanup handle. Leak-safe recovery for that case must be guaranteed by the adapter or added to the public port. The router therefore treats an unclassified launch exception as terminal rather than risking a duplicate allocation through automatic fallback.
  3. Portable declared capability vocabulary: the public outer runtime descriptor covers a few operational methods, not image identity, binaries, egress, timeout semantics, provider-account lifetime, state durability, trust restrictions, regions, resources, network, or GPU envelopes. The commercial registry owns these provider declarations for now. They must not be implemented through probes; present-but-no-op methods have already proved that inference unsafe.
  4. Optional peer declaration surface: the public package declarations reference Daytona peer types even for an adapter-free type-only consumer. This package uses skipLibCheck; the public base should isolate optional provider declaration imports.

Development

Requires Node.js 20 or newer.

npm install
npm run build
npm run typecheck
npm test

Behavioral tests are named in must-fire/must-not-fire pairs so both the trigger and its guard are visible. The package is private and has no publish script.

The credential-first regression proof can be reproduced with node scripts/verify-tenant0-mutations.mjs. It temporarily mutates and restores source, asserts each negative control fails, then runs the clean regression suite. Run it without concurrent source edits.

The opt-in live proof is npm run build followed by node scripts/prove-tenant0-daytona.mjs. It reads Daytona credentials from ~/Projects/AgentWorkforce/cloud/.env at point of use, provisions a bounded sandbox, verifies command stdout, destroys it, and audits its unique labels. It writes only a sanitized transcript under .workflow-artifacts/tenant0-0906/.

版本与发布记录

@agent-relay/sandbox-router 共发布 7 个版本,首次发布于 2026/08/25;过去 12 个月发布了 7 个版本。

版本发布日期距今
0.6.0latest2026/09/211 个月前
0.5.02026/09/161 个月前
0.4.02026/09/072 个月前
0.3.02026/09/012 个月前
0.2.12026/08/292 个月前
0.2.02026/08/252 个月前

关于 @agent-relay/sandbox-router 的常见问题

@agent-relay/sandbox-router 是什么?

Credential-first sandbox construction and provider routing for agent workloads.

如何安装 @agent-relay/sandbox-router?

在项目目录运行 npm install @agent-relay/sandbox-router,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@agent-relay/sandbox(^0.1.16)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@agent-relay/sandbox-router 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@agent-relay/sandbox-router 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@agent-relay/sandbox-router 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@agent-relay/sandbox-router 可以商用吗?

@agent-relay/sandbox-router 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@agent-relay/sandbox-router 的下载量有多少?

根据 npm 官方下载统计,@agent-relay/sandbox-router 在 2026/10/05 – 2026/11/03 这 30 天内被下载 3,070 次,日均约 102 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@agent-relay/sandbox-router 还在维护吗?

最新版本 0.6.0 发布于 2026/09/21(1 个月前);至今共发布 7 个版本,其中过去 12 个月发布了 7 个;首次发布于 2026/08/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。