@sandboxrouter/sdk 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +271 -62
  2. package/dist/bench.js +4 -4
  3. package/dist/catalog.d.ts +232 -3
  4. package/dist/catalog.js +569 -14
  5. package/dist/check.d.ts +28 -0
  6. package/dist/check.js +185 -0
  7. package/dist/cli.js +149 -12
  8. package/dist/computers.d.ts +55 -13
  9. package/dist/computers.js +132 -6
  10. package/dist/conformance.d.ts +46 -0
  11. package/dist/conformance.js +65 -0
  12. package/dist/cost.d.ts +67 -0
  13. package/dist/cost.js +153 -0
  14. package/dist/gc.d.ts +29 -0
  15. package/dist/gc.js +54 -0
  16. package/dist/index.d.ts +34 -2
  17. package/dist/index.js +34 -2
  18. package/dist/leaderboard.d.ts +6 -0
  19. package/dist/leaderboard.js +5 -4
  20. package/dist/ledger.d.ts +38 -0
  21. package/dist/ledger.js +89 -0
  22. package/dist/providers/agent37.d.ts +40 -0
  23. package/dist/providers/agent37.js +224 -0
  24. package/dist/providers/agentcore.d.ts +71 -0
  25. package/dist/providers/agentcore.js +446 -0
  26. package/dist/providers/alibaba.d.ts +18 -0
  27. package/dist/providers/alibaba.js +106 -0
  28. package/dist/providers/azure.d.ts +43 -0
  29. package/dist/providers/azure.js +330 -0
  30. package/dist/providers/beam.d.ts +11 -0
  31. package/dist/providers/beam.js +179 -0
  32. package/dist/providers/blaxel.d.ts +3 -1
  33. package/dist/providers/blaxel.js +32 -22
  34. package/dist/providers/boat.d.ts +25 -0
  35. package/dist/providers/boat.js +263 -0
  36. package/dist/providers/cloudflare.d.ts +30 -0
  37. package/dist/providers/cloudflare.js +152 -0
  38. package/dist/providers/codesandbox.d.ts +59 -0
  39. package/dist/providers/codesandbox.js +287 -0
  40. package/dist/providers/daytona.d.ts +25 -4
  41. package/dist/providers/daytona.js +449 -28
  42. package/dist/providers/dedalus.d.ts +12 -0
  43. package/dist/providers/dedalus.js +244 -0
  44. package/dist/providers/deno.d.ts +50 -0
  45. package/dist/providers/deno.js +170 -0
  46. package/dist/providers/docker.d.ts +17 -0
  47. package/dist/providers/docker.js +154 -0
  48. package/dist/providers/e2b.d.ts +70 -3
  49. package/dist/providers/e2b.js +376 -8
  50. package/dist/providers/exedev.d.ts +23 -0
  51. package/dist/providers/exedev.js +290 -0
  52. package/dist/providers/freestyle.d.ts +49 -0
  53. package/dist/providers/freestyle.js +261 -0
  54. package/dist/providers/hopx.d.ts +15 -0
  55. package/dist/providers/hopx.js +177 -0
  56. package/dist/providers/instabox.d.ts +76 -0
  57. package/dist/providers/instabox.js +360 -0
  58. package/dist/providers/koyeb.d.ts +71 -0
  59. package/dist/providers/koyeb.js +379 -0
  60. package/dist/providers/lambda.d.ts +51 -0
  61. package/dist/providers/lambda.js +467 -0
  62. package/dist/providers/langsmith.d.ts +16 -0
  63. package/dist/providers/langsmith.js +149 -0
  64. package/dist/providers/limrun.d.ts +77 -0
  65. package/dist/providers/limrun.js +626 -0
  66. package/dist/providers/maritime.d.ts +5 -1
  67. package/dist/providers/maritime.js +39 -8
  68. package/dist/providers/mobilerun.d.ts +44 -0
  69. package/dist/providers/mobilerun.js +417 -0
  70. package/dist/providers/modal.js +12 -4
  71. package/dist/providers/morph.js +12 -4
  72. package/dist/providers/northflank.d.ts +21 -0
  73. package/dist/providers/northflank.js +259 -0
  74. package/dist/providers/novita.d.ts +12 -0
  75. package/dist/providers/novita.js +81 -0
  76. package/dist/providers/openscale.d.ts +17 -0
  77. package/dist/providers/openscale.js +87 -0
  78. package/dist/providers/orgo.d.ts +45 -0
  79. package/dist/providers/orgo.js +414 -0
  80. package/dist/providers/sail.d.ts +32 -0
  81. package/dist/providers/sail.js +372 -0
  82. package/dist/providers/superserve.d.ts +25 -0
  83. package/dist/providers/superserve.js +337 -0
  84. package/dist/providers/tensorlake.js +17 -14
  85. package/dist/providers/upstash.d.ts +30 -0
  86. package/dist/providers/upstash.js +258 -0
  87. package/dist/providers/vercel.js +14 -3
  88. package/dist/providers/workspaces.d.ts +60 -0
  89. package/dist/providers/workspaces.js +662 -0
  90. package/dist/router.d.ts +36 -5
  91. package/dist/router.js +174 -25
  92. package/dist/server.d.ts +1 -1
  93. package/dist/server.js +22 -3
  94. package/dist/types.d.ts +18 -6
  95. package/dist/util.d.ts +28 -1
  96. package/dist/util.js +56 -7
  97. package/package.json +106 -17
package/dist/index.js CHANGED
@@ -1,7 +1,11 @@
1
- export { Router, RoutedSandbox, defaultProviders, parseId } from "./router.js";
1
+ export { checkProvider, CHECK_STEPS } from "./check.js";
2
+ export { Ledger, defaultLedgerFile } from "./ledger.js";
3
+ export { conformanceTable, loadConformance } from "./conformance.js";
4
+ export { gc, listOpen, isGone } from "./gc.js";
5
+ export { Router, RoutedSandbox, defaultProviders, defaultComputerProviders, parseId } from "./router.js";
2
6
  export { CATALOG, hourlyCost, unfit, info } from "./catalog.js";
3
7
  export { createApp } from "./server.js";
4
- export { RoutedComputer, createComputer, } from "./computers.js";
8
+ export { RoutedComputer, createComputer, planComputer, computerCatalog, computerHourly, COMPUTER_ENTRIES, } from "./computers.js";
5
9
  export { bench, benchMarkdown } from "./bench.js";
6
10
  export { aggregate, loadReports, makeReport, renderLeaderboard } from "./leaderboard.js";
7
11
  export { MaritimeProvider } from "./providers/maritime.js";
@@ -14,4 +18,32 @@ export { SpritesProvider } from "./providers/sprites.js";
14
18
  export { BlaxelProvider } from "./providers/blaxel.js";
15
19
  export { TensorlakeProvider } from "./providers/tensorlake.js";
16
20
  export { RunloopProvider } from "./providers/runloop.js";
21
+ export { DockerProvider } from "./providers/docker.js";
22
+ export { HopxProvider } from "./providers/hopx.js";
23
+ export { CloudflareProvider } from "./providers/cloudflare.js";
24
+ export { NorthflankProvider } from "./providers/northflank.js";
25
+ export { BoatProvider } from "./providers/boat.js";
26
+ export { ExeDevProvider } from "./providers/exedev.js";
27
+ export { AgentCoreProvider } from "./providers/agentcore.js";
28
+ export { AzureSessionsProvider } from "./providers/azure.js";
29
+ export { LambdaMicrovmProvider, writeLambdaMicrovmBundle } from "./providers/lambda.js";
30
+ export { AlibabaProvider } from "./providers/alibaba.js";
31
+ export { BeamProvider } from "./providers/beam.js";
32
+ export { CodeSandboxProvider } from "./providers/codesandbox.js";
33
+ export { DenoProvider } from "./providers/deno.js";
34
+ export { FreestyleProvider } from "./providers/freestyle.js";
35
+ export { KoyebProvider } from "./providers/koyeb.js";
36
+ export { DedalusProvider } from "./providers/dedalus.js";
37
+ export { OpenScaleProvider } from "./providers/openscale.js";
38
+ export { SuperserveProvider } from "./providers/superserve.js";
39
+ export { SailProvider } from "./providers/sail.js";
40
+ export { InstaboxProvider, INSTABOX_SIZES, instaboxSize } from "./providers/instabox.js";
41
+ export { Agent37Provider, AGENT37_SHAPES, agent37Shape } from "./providers/agent37.js";
42
+ export { NovitaProvider } from "./providers/novita.js";
43
+ export { LangSmithProvider, langsmithSize } from "./providers/langsmith.js";
44
+ export { UpstashProvider } from "./providers/upstash.js";
45
+ export { OrgoProvider } from "./providers/orgo.js";
46
+ export { MobilerunProvider } from "./providers/mobilerun.js";
47
+ export { LimrunProvider } from "./providers/limrun.js";
48
+ export { WorkspacesAgentProvider } from "./providers/workspaces.js";
17
49
  export * from "./types.js";
@@ -31,7 +31,13 @@ export interface LeaderRow {
31
31
  medianCreateMs?: number;
32
32
  medianExecMs?: number;
33
33
  successRate: number;
34
+ /** List price for the size the bench runs it at (`size`). */
34
35
  hourlyUsd: number;
36
+ /** The size bench runs: 2 vCPU / 4 GB, or the provider's fixed default size. */
37
+ size: {
38
+ vcpu: number;
39
+ memoryGb: number;
40
+ };
35
41
  }
36
42
  /** Median of each submission's medians, so one heavy submitter can't skew a provider. */
37
43
  export declare function aggregate(reports: BenchReport[]): LeaderRow[];
@@ -1,6 +1,6 @@
1
1
  import { readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { CATALOG, hourlyCost } from "./catalog.js";
3
+ import { CATALOG, fittingSpec, hourlyCost } from "./catalog.js";
4
4
  import { median } from "./bench-store.js";
5
5
  export function makeReport(rows, opts) {
6
6
  return {
@@ -55,7 +55,8 @@ export function aggregate(reports) {
55
55
  medianCreateMs: median(rs.map((r) => r.medianCreateMs).filter((n) => typeof n === "number")),
56
56
  medianExecMs: median(rs.map((r) => r.medianExecMs).filter((n) => typeof n === "number")),
57
57
  successRate: runs ? ok / runs : 0,
58
- hourlyUsd: hourlyCost(id),
58
+ hourlyUsd: hourlyCost(id, fittingSpec(id)),
59
+ size: fittingSpec(id),
59
60
  };
60
61
  });
61
62
  return rows.sort((a, b) => (a.medianCreateMs ?? Infinity) - (b.medianCreateMs ?? Infinity));
@@ -81,7 +82,7 @@ export function renderLeaderboard(reports) {
81
82
  <th scope="row">${esc(r.name)}</th>
82
83
  ${bar(r.medianCreateMs, maxCreate, r.medianCreateMs === undefined ? "" : `${r.medianCreateMs.toLocaleString("en-US")} ms`, `${r.name}: median cold start ${r.medianCreateMs} ms across ${r.submissions} submission${r.submissions === 1 ? "" : "s"}`)}
83
84
  <td class="num">${r.medianExecMs === undefined ? "n/a" : `${r.medianExecMs} ms`}</td>
84
- ${bar(r.hourlyUsd, maxPrice, `$${r.hourlyUsd.toFixed(3)}`, `${r.name}: $${r.hourlyUsd.toFixed(4)} per hour at list price, 2 vCPU / 4 GB`)}
85
+ ${bar(r.hourlyUsd, maxPrice, `$${r.hourlyUsd.toFixed(3)}`, `${r.name}: $${r.hourlyUsd.toFixed(4)} per hour at list price, ${r.size.vcpu} vCPU / ${r.size.memoryGb} GB`)}
85
86
  <td class="num">${(r.successRate * 100).toFixed(0)}%</td>
86
87
  <td class="num muted">${r.submissions}</td>
87
88
  </tr>`)
@@ -154,7 +155,7 @@ a { color: inherit; }
154
155
  <table>
155
156
  <thead><tr>
156
157
  <th>#</th><th>Provider</th><th>Cold start (median)</th><th>Exec round trip</th>
157
- <th>List price, 2 vCPU / 4 GB</th><th>Success</th><th>Submissions</th>
158
+ <th>List price (size run)</th><th>Success</th><th>Submissions</th>
158
159
  </tr></thead>
159
160
  <tbody>
160
161
  ${body || `<tr><td colspan="7" class="muted">No results yet. Run <code>npx @sandboxrouter/sdk bench --share</code> and open a PR.</td></tr>`}
@@ -0,0 +1,38 @@
1
+ import type { ProviderId } from "./types.js";
2
+ /** One sandbox the router started and hasn't seen killed. */
3
+ export interface LedgerEntry {
4
+ /** Routed id, `provider:nativeId`. */
5
+ id: string;
6
+ provider: ProviderId;
7
+ /** ISO time the create returned. */
8
+ createdAt: string;
9
+ /** ISO time its requested lifetime (spec.timeoutMinutes) runs out. */
10
+ expiresAt: string;
11
+ /** Estimated list price per hour while it runs. */
12
+ usdHour: number;
13
+ }
14
+ /**
15
+ * Where the default ledger lives: $SANDBOXROUTER_HOME/sandboxes.jsonl, else ~/.sandboxrouter/.
16
+ * Returns null when SANDBOXROUTER_LEDGER=off.
17
+ */
18
+ export declare function defaultLedgerFile(): string | null;
19
+ /**
20
+ * A local record of every sandbox this machine's routers created, so a crashed script's sandboxes
21
+ * can still be found and killed (`sandboxrouter ls` / `gc`). Append-only JSON lines, so several
22
+ * processes can write at once; `compact()` rewrites it with only the open entries.
23
+ * Writing never throws: a ledger problem must not break creating or killing a sandbox.
24
+ */
25
+ export declare class Ledger {
26
+ readonly file: string;
27
+ private warned;
28
+ constructor(file: string);
29
+ open(e: LedgerEntry): void;
30
+ close(id: string, why?: string): void;
31
+ /** Open entries, oldest first. */
32
+ list(): LedgerEntry[];
33
+ /** Rewrite the file with only the open entries. */
34
+ compact(): void;
35
+ private lines;
36
+ private append;
37
+ private warn;
38
+ }
package/dist/ledger.js ADDED
@@ -0,0 +1,89 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ /**
5
+ * Where the default ledger lives: $SANDBOXROUTER_HOME/sandboxes.jsonl, else ~/.sandboxrouter/.
6
+ * Returns null when SANDBOXROUTER_LEDGER=off.
7
+ */
8
+ export function defaultLedgerFile() {
9
+ if (/^(off|0|false|no)$/i.test(process.env.SANDBOXROUTER_LEDGER ?? ""))
10
+ return null;
11
+ return join(process.env.SANDBOXROUTER_HOME || join(homedir(), ".sandboxrouter"), "sandboxes.jsonl");
12
+ }
13
+ /**
14
+ * A local record of every sandbox this machine's routers created, so a crashed script's sandboxes
15
+ * can still be found and killed (`sandboxrouter ls` / `gc`). Append-only JSON lines, so several
16
+ * processes can write at once; `compact()` rewrites it with only the open entries.
17
+ * Writing never throws: a ledger problem must not break creating or killing a sandbox.
18
+ */
19
+ export class Ledger {
20
+ file;
21
+ warned = false;
22
+ constructor(file) {
23
+ this.file = file;
24
+ }
25
+ open(e) {
26
+ this.append({ op: "open", ...e });
27
+ }
28
+ close(id, why = "killed") {
29
+ this.append({ op: "close", id, at: new Date().toISOString(), why });
30
+ }
31
+ /** Open entries, oldest first. */
32
+ list() {
33
+ const open = new Map();
34
+ for (const line of this.lines()) {
35
+ if (line.op === "open") {
36
+ const { op: _op, ...e } = line;
37
+ open.set(e.id, e);
38
+ }
39
+ else
40
+ open.delete(line.id);
41
+ }
42
+ return [...open.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
43
+ }
44
+ /** Rewrite the file with only the open entries. */
45
+ compact() {
46
+ const keep = this.list().map((e) => JSON.stringify({ op: "open", ...e })).join("\n");
47
+ try {
48
+ const tmp = `${this.file}.${process.pid}.tmp`;
49
+ writeFileSync(tmp, keep ? `${keep}\n` : "", { mode: 0o600 });
50
+ renameSync(tmp, this.file);
51
+ }
52
+ catch (err) {
53
+ this.warn(err);
54
+ }
55
+ }
56
+ lines() {
57
+ if (!existsSync(this.file))
58
+ return [];
59
+ const out = [];
60
+ for (const raw of readFileSync(this.file, "utf8").split("\n")) {
61
+ if (!raw.trim())
62
+ continue;
63
+ try {
64
+ const l = JSON.parse(raw);
65
+ if ((l?.op === "open" || l?.op === "close") && typeof l.id === "string")
66
+ out.push(l);
67
+ }
68
+ catch {
69
+ // A line cut short by a crash: skip it.
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+ append(line) {
75
+ try {
76
+ mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
77
+ appendFileSync(this.file, `${JSON.stringify(line)}\n`, { mode: 0o600 });
78
+ }
79
+ catch (err) {
80
+ this.warn(err);
81
+ }
82
+ }
83
+ warn(err) {
84
+ if (this.warned)
85
+ return;
86
+ this.warned = true;
87
+ console.warn(`sandboxrouter: couldn't write the sandbox ledger at ${this.file}: ${err.message}`);
88
+ }
89
+ }
@@ -0,0 +1,40 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ /** Default-type shapes from https://www.agent37.com/docs/agents-api/instance-sizing (2026-11-05). */
3
+ export declare const AGENT37_SHAPES: readonly [{
4
+ readonly vcpu: 2;
5
+ readonly memoryGb: 4;
6
+ readonly diskGb: 4;
7
+ readonly usdMonth: 4.76;
8
+ readonly usdHour: number;
9
+ }, {
10
+ readonly vcpu: 4;
11
+ readonly memoryGb: 8;
12
+ readonly diskGb: 6;
13
+ readonly usdMonth: 9.34;
14
+ readonly usdHour: number;
15
+ }, {
16
+ readonly vcpu: 4;
17
+ readonly memoryGb: 16;
18
+ readonly diskGb: 12;
19
+ readonly usdMonth: 15.48;
20
+ readonly usdHour: number;
21
+ }, {
22
+ readonly vcpu: 4;
23
+ readonly memoryGb: 32;
24
+ readonly diskGb: 24;
25
+ readonly usdMonth: 27.76;
26
+ readonly usdHour: number;
27
+ }];
28
+ /** Smallest shape with at least this much CPU and memory, or undefined. */
29
+ export declare function agent37Shape(vcpu: number, memoryGb: number): (typeof AGENT37_SHAPES)[number] | undefined;
30
+ export declare class Agent37Provider implements Provider {
31
+ private apiKey;
32
+ private baseUrl;
33
+ readonly id: "agent37";
34
+ constructor(apiKey?: string | undefined, baseUrl?: string);
35
+ configured(): boolean;
36
+ /** @internal */
37
+ api<T>(method: string, path: string, json?: unknown, signal?: AbortSignal): Promise<T>;
38
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
39
+ connect(nativeId: string): Promise<ProviderSandbox>;
40
+ }
@@ -0,0 +1,224 @@
1
+ import { ProviderError } from "../types.js";
2
+ import { dirname, shq, toBytes, wrapCommand } from "../util.js";
3
+ /**
4
+ * Agent37 (YC F26) instances over plain REST. gVisor containers that bill per minute while
5
+ * running and only for disk while asleep. Fixed shapes, at most 4 vCPU. The workspace wallet is
6
+ * prepaid, and create needs one day of the instance's running cost in it (402 otherwise).
7
+ *
8
+ * https://api.agent37.com, Authorization: Bearer <AGENT37_API_KEY> (sk_live_...):
9
+ * POST /v1/instances {template, resources:{cpu, memory}, auto_sleep, idle_timeout_seconds, env, metadata}
10
+ * 201 with the instance once status is "running"
11
+ * GET /v1/instances/:id -> {id, status, status_reason, url, ...}
12
+ * POST /v1/instances/:id/start, POST /v1/instances/:id/sleep, DELETE /v1/instances/:id
13
+ * POST /v1/instances/:id/exec {command} -> {exit_code, stdout, stderr, truncated}
14
+ * command runs under sh -c; body max 128 KB; stdout and stderr capped at 512 KB each;
15
+ * the call waits up to 780 s and the command keeps running if the call gives up.
16
+ * Status: provisioning, running, stopping, stopped, starting, restarting, updating, sleeping,
17
+ * waking, failed, deleting, deleted. Errors are {error:{code, message}}.
18
+ * The files endpoints live on agent templates only, not agent37-base, so files go through exec as
19
+ * base64, in chunks that fit the request and output caps. exec has no cwd/env/timeout fields, so
20
+ * those are added to the shell command. No streaming: output arrives when the command ends.
21
+ * Docs: https://www.agent37.com/docs/agents-api/overview, .../instances/create, .../exec,
22
+ * .../instance-sizing, .../instances/sleep, .../instances/object
23
+ */
24
+ const ID = "agent37";
25
+ const READY_TIMEOUT_MS = 5 * 60_000;
26
+ /** Base64 characters per write request, under the 128 KB body cap. */
27
+ const WRITE_CHUNK_B64 = 96 * 1024;
28
+ /** Raw bytes per read request; base64 of this stays under the 512 KB stdout cap. */
29
+ const READ_CHUNK = 256 * 1024;
30
+ /** Default-type shapes from https://www.agent37.com/docs/agents-api/instance-sizing (2026-11-05). */
31
+ export const AGENT37_SHAPES = [
32
+ // usdHour = (monthly price at default disk - disk at $0.09/GB-month) / 730 h
33
+ { vcpu: 2, memoryGb: 4, diskGb: 4, usdMonth: 4.76, usdHour: 4.4 / 730 },
34
+ { vcpu: 4, memoryGb: 8, diskGb: 6, usdMonth: 9.34, usdHour: 8.8 / 730 },
35
+ { vcpu: 4, memoryGb: 16, diskGb: 12, usdMonth: 15.48, usdHour: 14.4 / 730 },
36
+ { vcpu: 4, memoryGb: 32, diskGb: 24, usdMonth: 27.76, usdHour: 25.6 / 730 },
37
+ ];
38
+ /** Smallest shape with at least this much CPU and memory, or undefined. */
39
+ export function agent37Shape(vcpu, memoryGb) {
40
+ return AGENT37_SHAPES.find((s) => s.vcpu >= vcpu && s.memoryGb >= memoryGb);
41
+ }
42
+ export class Agent37Provider {
43
+ apiKey;
44
+ baseUrl;
45
+ id = ID;
46
+ constructor(apiKey = process.env.AGENT37_API_KEY, baseUrl = process.env.AGENT37_API_URL ?? "https://api.agent37.com") {
47
+ this.apiKey = apiKey;
48
+ this.baseUrl = baseUrl;
49
+ }
50
+ configured() {
51
+ return !!this.apiKey;
52
+ }
53
+ /** @internal */
54
+ async api(method, path, json, signal) {
55
+ if (!this.apiKey)
56
+ throw new ProviderError(ID, "AGENT37_API_KEY is not set");
57
+ const res = await fetch(`${this.baseUrl}${path}`, {
58
+ method,
59
+ headers: { Authorization: `Bearer ${this.apiKey}`, ...(json !== undefined ? { "Content-Type": "application/json" } : {}) },
60
+ body: json !== undefined ? JSON.stringify(json) : undefined,
61
+ signal,
62
+ });
63
+ const text = await res.text();
64
+ if (!res.ok) {
65
+ let detail = text.slice(0, 300);
66
+ try {
67
+ const j = JSON.parse(text);
68
+ const e = j?.error;
69
+ detail = typeof e === "string" ? e : e?.code ? `${e.code}${e.message ? `: ${e.message}` : ""}` : (j?.message ?? detail);
70
+ }
71
+ catch {
72
+ /* plain-text body */
73
+ }
74
+ throw new ProviderError(ID, `${method} ${path} -> ${res.status} ${detail}`, res.status);
75
+ }
76
+ return (text ? JSON.parse(text) : {});
77
+ }
78
+ async create(spec) {
79
+ const vcpu = spec.vcpu ?? 2;
80
+ const memoryGb = spec.memoryGb ?? 4;
81
+ const shape = agent37Shape(vcpu, memoryGb);
82
+ if (!shape)
83
+ throw new ProviderError(ID, `no Agent37 shape has ${vcpu} vCPU / ${memoryGb} GB (max 4 vCPU / 32 GB)`);
84
+ const idle = Math.min(86_400, Math.max(300, Math.round((spec.timeoutMinutes ?? 30) * 60)));
85
+ const inst = await this.api("POST", "/v1/instances", {
86
+ template: spec.image?.agent37 ?? "agent37-base",
87
+ resources: { cpu: shape.vcpu, memory: shape.memoryGb },
88
+ // Sleep (disk-only billing) once idle for the requested lifetime, so a leaked box stops costing compute.
89
+ auto_sleep: true,
90
+ idle_timeout_seconds: idle,
91
+ ...(spec.env && Object.keys(spec.env).length ? { env: spec.env } : {}),
92
+ ...(spec.metadata ? { metadata: spec.metadata } : {}),
93
+ });
94
+ if (!inst.id)
95
+ throw new ProviderError(ID, "create returned no id");
96
+ const box = new Agent37Box(this, inst.id);
97
+ try {
98
+ if (inst.status !== "running")
99
+ await box.waitRunning();
100
+ }
101
+ catch (err) {
102
+ await box.kill().catch(() => { });
103
+ throw err;
104
+ }
105
+ return box;
106
+ }
107
+ async connect(nativeId) {
108
+ const box = new Agent37Box(this, nativeId);
109
+ await box.waitRunning();
110
+ return box;
111
+ }
112
+ }
113
+ class Agent37Box {
114
+ p;
115
+ nativeId;
116
+ constructor(p, nativeId) {
117
+ this.p = p;
118
+ this.nativeId = nativeId;
119
+ }
120
+ get path() {
121
+ return `/v1/instances/${encodeURIComponent(this.nativeId)}`;
122
+ }
123
+ /** Poll until running, starting the instance if it sleeps or is stopped. */
124
+ async waitRunning() {
125
+ const deadline = Date.now() + READY_TIMEOUT_MS;
126
+ let started = false;
127
+ for (;;) {
128
+ const inst = await this.p.api("GET", this.path);
129
+ const status = inst.status ?? "";
130
+ if (status === "running")
131
+ return;
132
+ if (status === "failed" || status === "deleting" || status === "deleted") {
133
+ throw new ProviderError(ID, `instance ${this.nativeId} is ${status}${inst.status_reason ? `: ${inst.status_reason}` : ""}`, status === "failed" ? undefined : 404);
134
+ }
135
+ if ((status === "sleeping" || status === "stopped") && !started) {
136
+ await this.p.api("POST", `${this.path}/start`);
137
+ started = true;
138
+ }
139
+ if (Date.now() > deadline)
140
+ throw new ProviderError(ID, `instance ${this.nativeId} not running after ${READY_TIMEOUT_MS / 1000}s (${status || "unknown"})`);
141
+ await sleep(500);
142
+ }
143
+ }
144
+ async call(command, waitSeconds) {
145
+ const send = () => this.p.api("POST", `${this.path}/exec`, { command }, AbortSignal.timeout(waitSeconds * 1000));
146
+ try {
147
+ return await send();
148
+ }
149
+ catch (err) {
150
+ // A sleeping instance may refuse exec until started.
151
+ if (err instanceof ProviderError && err.status === 400 && /sleep|stopp/i.test(err.message)) {
152
+ await this.waitRunning();
153
+ return send();
154
+ }
155
+ throw err;
156
+ }
157
+ }
158
+ /** The exec API has no timeout field, so the command runs under `timeout` when the image has it. */
159
+ async exec(cmd, opts = {}) {
160
+ const t0 = Date.now();
161
+ const secs = opts.timeoutSeconds ?? 60;
162
+ const inner = wrapCommand(cmd, { cwd: opts.cwd, env: opts.env });
163
+ const guarded = `if command -v timeout >/dev/null 2>&1; then exec timeout -k 5 ${secs} /bin/sh -c ${shq(inner)}; else exec /bin/sh -c ${shq(inner)}; fi`;
164
+ let r;
165
+ try {
166
+ r = await this.call(guarded, Math.min(secs + 20, 780));
167
+ }
168
+ catch (err) {
169
+ if (err?.name === "TimeoutError")
170
+ throw new ProviderError(ID, `command timed out after ${secs}s`);
171
+ throw err;
172
+ }
173
+ const durationMs = Date.now() - t0;
174
+ const exitCode = r.exit_code ?? -1;
175
+ if (durationMs >= secs * 1000 && [124, 137, 143].includes(exitCode))
176
+ throw new ProviderError(ID, `command timed out after ${secs}s`);
177
+ const stdout = r.stdout ?? "";
178
+ const stderr = r.stderr ?? "";
179
+ if (stdout)
180
+ opts.onStdout?.(stdout);
181
+ if (stderr)
182
+ opts.onStderr?.(stderr);
183
+ return { exitCode, stdout, stderr, durationMs };
184
+ }
185
+ async writeFile(path, content) {
186
+ const b64 = Buffer.from(toBytes(content)).toString("base64");
187
+ const tmp = `${path}.sbr-part`;
188
+ let first = true;
189
+ for (let i = 0; first || i < b64.length; i += WRITE_CHUNK_B64) {
190
+ const part = b64.slice(i, i + WRITE_CHUNK_B64);
191
+ const cmd = `${first ? `mkdir -p ${shq(dirname(path))} && ` : ""}printf %s ${shq(part)} | base64 -d ${first ? ">" : ">>"} ${shq(tmp)}`;
192
+ const r = await this.call(cmd, 120);
193
+ if (r.exit_code !== 0)
194
+ throw new ProviderError(ID, `write ${path}: ${r.stderr?.trim() || `exit ${r.exit_code}`}`);
195
+ first = false;
196
+ }
197
+ const r = await this.call(`mv -f ${shq(tmp)} ${shq(path)}`, 60);
198
+ if (r.exit_code !== 0)
199
+ throw new ProviderError(ID, `write ${path}: ${r.stderr?.trim() || `exit ${r.exit_code}`}`);
200
+ }
201
+ async readFile(path) {
202
+ const size = await this.call(`wc -c < ${shq(path)}`, 60);
203
+ if (size.exit_code !== 0)
204
+ throw new ProviderError(ID, `read ${path}: ${size.stderr?.trim() || "no such file"}`, 404);
205
+ const total = Number((size.stdout ?? "").trim());
206
+ const parts = [];
207
+ for (let off = 0; off < total; off += READ_CHUNK) {
208
+ const r = await this.call(`tail -c +${off + 1} ${shq(path)} | head -c ${READ_CHUNK} | base64`, 120);
209
+ if (r.exit_code !== 0)
210
+ throw new ProviderError(ID, `read ${path}: ${r.stderr?.trim() || `exit ${r.exit_code}`}`);
211
+ parts.push(Buffer.from((r.stdout ?? "").replace(/\s+/g, ""), "base64"));
212
+ }
213
+ return new Uint8Array(Buffer.concat(parts));
214
+ }
215
+ async pause() {
216
+ await this.p.api("POST", `${this.path}/sleep`);
217
+ }
218
+ async kill() {
219
+ await this.p.api("DELETE", this.path);
220
+ }
221
+ }
222
+ function sleep(ms) {
223
+ return new Promise((r) => setTimeout(r, ms));
224
+ }
@@ -0,0 +1,71 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ export interface AwsCredentials {
3
+ accessKeyId: string;
4
+ secretAccessKey: string;
5
+ sessionToken?: string;
6
+ }
7
+ /** True when the default AWS chain plausibly has credentials (env keys, a profile, or a container/web identity). */
8
+ export declare function awsCredentialsHinted(env?: NodeJS.ProcessEnv): boolean;
9
+ /** SigV4-signs and sends JSON requests for one AWS service in one region. */
10
+ export declare class AwsClient {
11
+ readonly provider: string;
12
+ readonly service: string;
13
+ readonly region: string;
14
+ private credentials?;
15
+ private signer?;
16
+ constructor(provider: string, service: string, region: string, credentials?: AwsCredentials | (() => Promise<AwsCredentials>) | undefined);
17
+ private getSigner;
18
+ /** Signs and sends; returns the response as-is (callers check status). */
19
+ send(method: string, url: URL, opts?: {
20
+ json?: unknown;
21
+ headers?: Record<string, string>;
22
+ timeoutMs?: number;
23
+ }): Promise<Response>;
24
+ /** send() + parse JSON, throwing ProviderError (with the AWS error name) on non-2xx. */
25
+ json<T>(method: string, url: URL, opts?: {
26
+ json?: unknown;
27
+ headers?: Record<string, string>;
28
+ timeoutMs?: number;
29
+ }): Promise<T>;
30
+ }
31
+ export declare function awsFail(provider: string, res: Response, what: string): Promise<never>;
32
+ export interface EventMessage {
33
+ headers: Record<string, string | number | boolean | Uint8Array>;
34
+ payload: Uint8Array;
35
+ }
36
+ /**
37
+ * Decodes the AWS event stream binary framing: [total len u32][headers len u32][prelude crc u32]
38
+ * [headers][payload][message crc u32]. Header value types per the spec (7 = string).
39
+ */
40
+ export declare function readEventStream(res: Response): AsyncGenerator<EventMessage>;
41
+ /** Encodes one event stream frame (string headers only). Used by tests' mocks. */
42
+ export declare function encodeEvent(headers: Record<string, string>, payload: Uint8Array | string): Buffer;
43
+ export interface AgentCoreOptions {
44
+ region?: string;
45
+ /** Code interpreter id (default aws.codeinterpreter.v1, the AWS-managed one). */
46
+ codeInterpreterId?: string;
47
+ /** Override the data-plane base URL (tests). Default https://bedrock-agentcore.{region}.amazonaws.com. */
48
+ endpoint?: string;
49
+ /** Static credentials or a provider; default is the standard AWS chain. */
50
+ credentials?: AwsCredentials | (() => Promise<AwsCredentials>);
51
+ }
52
+ export declare class AgentCoreProvider implements Provider {
53
+ readonly id: "agentcore";
54
+ readonly region?: string;
55
+ readonly interpreter: string;
56
+ private readonly opts;
57
+ private client?;
58
+ constructor(opts?: AgentCoreOptions);
59
+ /**
60
+ * Opt-in: AWS credentials alone are common, so the router only uses AgentCore when you name an
61
+ * interpreter (AGENTCORE_CODE_INTERPRETER_ID=aws.codeinterpreter.v1 for the managed one) or
62
+ * construct the provider yourself.
63
+ */
64
+ configured(): boolean;
65
+ /** @internal */
66
+ aws(): AwsClient;
67
+ /** @internal */
68
+ url(path: string, query?: Record<string, string>): URL;
69
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
70
+ connect(nativeId: string): Promise<ProviderSandbox>;
71
+ }