@tanstack/ai-sandbox-cloudflare 0.1.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.
- package/dist/esm/agent.d.ts +30 -0
- package/dist/esm/agent.js +25 -0
- package/dist/esm/agent.js.map +1 -0
- package/dist/esm/chat-coordinator.d.ts +75 -0
- package/dist/esm/chat-coordinator.js +135 -0
- package/dist/esm/chat-coordinator.js.map +1 -0
- package/dist/esm/container-coordinator.d.ts +114 -0
- package/dist/esm/container-coordinator.js +256 -0
- package/dist/esm/container-coordinator.js.map +1 -0
- package/dist/esm/coordinator.d.ts +68 -0
- package/dist/esm/coordinator.js +188 -0
- package/dist/esm/coordinator.js.map +1 -0
- package/dist/esm/factory.d.ts +80 -0
- package/dist/esm/factory.js +69 -0
- package/dist/esm/factory.js.map +1 -0
- package/dist/esm/handle.d.ts +23 -0
- package/dist/esm/handle.js +208 -0
- package/dist/esm/handle.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/preview-tool.d.ts +31 -0
- package/dist/esm/preview-tool.js +37 -0
- package/dist/esm/preview-tool.js.map +1 -0
- package/dist/esm/protocol.d.ts +42 -0
- package/dist/esm/protocol.js +64 -0
- package/dist/esm/protocol.js.map +1 -0
- package/dist/esm/provider.d.ts +31 -0
- package/dist/esm/provider.js +65 -0
- package/dist/esm/provider.js.map +1 -0
- package/dist/esm/public-host.d.ts +67 -0
- package/dist/esm/public-host.js +49 -0
- package/dist/esm/public-host.js.map +1 -0
- package/dist/esm/run-log-do.d.ts +25 -0
- package/dist/esm/run-log-do.js +122 -0
- package/dist/esm/run-log-do.js.map +1 -0
- package/dist/esm/runner.d.ts +32 -0
- package/dist/esm/runner.js +107 -0
- package/dist/esm/runner.js.map +1 -0
- package/dist/esm/web-crypto.d.ts +11 -0
- package/dist/esm/web-crypto.js +18 -0
- package/dist/esm/web-crypto.js.map +1 -0
- package/dist/esm/worker.d.ts +8 -0
- package/dist/esm/worker.js +83 -0
- package/dist/esm/worker.js.map +1 -0
- package/package.json +74 -0
- package/src/agent.ts +66 -0
- package/src/chat-coordinator.ts +253 -0
- package/src/container-coordinator.ts +437 -0
- package/src/coordinator.ts +338 -0
- package/src/factory.ts +225 -0
- package/src/handle.ts +292 -0
- package/src/index.ts +5 -0
- package/src/preview-tool.ts +110 -0
- package/src/protocol.ts +171 -0
- package/src/provider.ts +111 -0
- package/src/public-host.ts +121 -0
- package/src/run-log-do.ts +171 -0
- package/src/runner.ts +226 -0
- package/src/web-crypto.ts +31 -0
- package/src/worker.ts +173 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function isLoopbackHost(host) {
|
|
2
|
+
const name = host.split(":")[0];
|
|
3
|
+
return name === "localhost" || name === "127.0.0.1" || name === "0.0.0.0";
|
|
4
|
+
}
|
|
5
|
+
function portOf(host, fallback) {
|
|
6
|
+
const colon = host.indexOf(":");
|
|
7
|
+
return colon === -1 ? fallback : host.slice(colon + 1);
|
|
8
|
+
}
|
|
9
|
+
function originForHost(host) {
|
|
10
|
+
const name = host.split(":")[0];
|
|
11
|
+
const scheme = isLoopbackHost(host) || name === "host.docker.internal" ? "http" : "https";
|
|
12
|
+
return `${scheme}://${host}`;
|
|
13
|
+
}
|
|
14
|
+
function resolveBridgeOrigin(env, input) {
|
|
15
|
+
const configured = env.PUBLIC_HOSTNAME?.trim();
|
|
16
|
+
if (configured) return originForHost(configured);
|
|
17
|
+
const host = input.publicHost;
|
|
18
|
+
if (!host) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"sandbox agent: no bridge host available — set PUBLIC_HOSTNAME, or run behind Cloudflare so the Worker can derive it from the trigger request."
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (isLoopbackHost(host)) {
|
|
24
|
+
return `http://host.docker.internal:${portOf(host, "3001")}`;
|
|
25
|
+
}
|
|
26
|
+
return originForHost(host);
|
|
27
|
+
}
|
|
28
|
+
function resolvePreviewHost(env, input) {
|
|
29
|
+
const configured = env.PREVIEW_HOSTNAME?.trim();
|
|
30
|
+
if (configured) return configured;
|
|
31
|
+
const host = input.publicHost;
|
|
32
|
+
if (!host) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"sandbox agent: no preview host available — set PREVIEW_HOSTNAME to a custom domain with a wildcard route."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (isLoopbackHost(host)) return host;
|
|
38
|
+
if (host.endsWith(".workers.dev")) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"sandbox agent: preview URLs need a custom domain with wildcard DNS — *.workers.dev has no wildcard subdomains. Set PREVIEW_HOSTNAME to your custom domain and add a `*.<domain>` route to the Worker."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return host;
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
resolveBridgeOrigin,
|
|
47
|
+
resolvePreviewHost
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=public-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-host.js","sources":["../../src/public-host.ts"],"sourcesContent":["/**\n * Host resolution for the two DISTINCT public surfaces the sandbox layer exposes.\n * Kept in its own (Workers-free) module so it stays pure and unit-testable.\n *\n * These were once a single `PUBLIC_HOSTNAME`, but they have different reachers and\n * therefore different correct values:\n *\n * - **Bridge / tool-exec** — the off-isolate CONTAINER calls back into the Worker\n * (`/_bridge`, `/tool-exec`). It must reach the Worker, so locally that's\n * `host.docker.internal` (the container can't reach the host's `localhost`).\n * - **Preview** — the BROWSER opens an `exposePort` URL that `proxyToSandbox`\n * routes into the container. It needs WILDCARD DNS, so locally that's\n * `*.localhost` (browsers resolve it to loopback with zero setup) and in\n * production a CUSTOM DOMAIN (`*.workers.dev` has no wildcard subdomains).\n */\n\n/** Hostnames that mean \"this machine\" (the loopback the container can't reach). */\nfunction isLoopbackHost(host: string): boolean {\n const name = host.split(':')[0]\n return name === 'localhost' || name === '127.0.0.1' || name === '0.0.0.0'\n}\n\n/** The port portion of a `host[:port]`, or `fallback` when none is present. */\nfunction portOf(host: string, fallback: string): string {\n const colon = host.indexOf(':')\n return colon === -1 ? fallback : host.slice(colon + 1)\n}\n\n/** `http://` for local hosts (loopback / host.docker.internal), `https://` else. */\nfunction originForHost(host: string): string {\n const name = host.split(':')[0]\n const scheme =\n isLoopbackHost(host) || name === 'host.docker.internal' ? 'http' : 'https'\n return `${scheme}://${host}`\n}\n\n/**\n * Resolve the ORIGIN the off-isolate sandbox CONTAINER uses to call back into the\n * Worker — the MCP tool-bridge (`/_bridge`) and host-tool execution (`/tool-exec`).\n * Returns a full origin (scheme + host + optional port), e.g.\n * `http://host.docker.internal:3001` locally or `https://app.example.com` deployed.\n *\n * `PUBLIC_HOSTNAME` wins when set; otherwise we derive from the host the trigger\n * request arrived on (`input.publicHost`).\n *\n * ── Why a callback hostname is unavoidable ──────────────────────────────────────\n * The container is SEPARATE compute from the Worker isolate; it can only reach the\n * Worker over the network, so the callback URL must be an absolute host.\n *\n * ── Why request-derivation is SAFE on Cloudflare ────────────────────────────────\n * On a generic Node server the `Host` header is attacker-controlled and trusting it\n * is a Host-injection / token-exfil vector (the per-run bearer token rides this\n * URL). Not so behind Cloudflare: the edge dispatches a request to your Worker only\n * when its hostname matches a route you OWN, so `input.publicHost` is always one of\n * your own hostnames — never an attacker's.\n *\n * ── Local dev: localhost → host.docker.internal ─────────────────────────────────\n * Locally the trigger arrives on `localhost`, which the container CANNOT reach\n * (that's the container's own loopback). So we rewrite it to `host.docker.internal`\n * (the Docker host gateway), keeping the port, over `http`. This removes the need\n * for a dev tunnel for the bridge entirely.\n */\nexport function resolveBridgeOrigin(\n env: { PUBLIC_HOSTNAME?: string },\n input: { publicHost?: string },\n): string {\n const configured = env.PUBLIC_HOSTNAME?.trim()\n if (configured) return originForHost(configured)\n const host = input.publicHost\n if (!host) {\n throw new Error(\n 'sandbox agent: no bridge host available — set PUBLIC_HOSTNAME, or run ' +\n 'behind Cloudflare so the Worker can derive it from the trigger request.',\n )\n }\n // Local dev: the container reaches the host machine via the Docker host gateway.\n if (isLoopbackHost(host)) {\n return `http://host.docker.internal:${portOf(host, '3001')}`\n }\n return originForHost(host)\n}\n\n/**\n * Resolve the HOST passed to `exposePort` for browser-facing preview URLs (the app\n * the agent builds). Returns a bare host (the `@cloudflare/sandbox` SDK builds the\n * `<port>-<id>-<token>.<host>` URL + scheme itself).\n *\n * `PREVIEW_HOSTNAME` wins when set; otherwise we derive from the trigger request.\n *\n * Preview URLs require WILDCARD DNS, which constrains the value:\n * - **Local** → `localhost:<port>`. The SDK's localhost path yields\n * `http://<port>-<id>-<token>.localhost:<port>`, which browsers resolve to\n * loopback with no DNS setup — so previews work locally with no tunnel.\n * - **Deployed** → a CUSTOM DOMAIN with a `*.<domain>` route. `*.workers.dev` has\n * no wildcard subdomains (the SDK's `exposePort` throws on it), so we throw a\n * clear error pointing at `PREVIEW_HOSTNAME` rather than letting the run fail\n * deep in the agent.\n */\nexport function resolvePreviewHost(\n env: { PREVIEW_HOSTNAME?: string },\n input: { publicHost?: string },\n): string {\n const configured = env.PREVIEW_HOSTNAME?.trim()\n if (configured) return configured\n const host = input.publicHost\n if (!host) {\n throw new Error(\n 'sandbox agent: no preview host available — set PREVIEW_HOSTNAME to a ' +\n 'custom domain with a wildcard route.',\n )\n }\n if (isLoopbackHost(host)) return host\n if (host.endsWith('.workers.dev')) {\n throw new Error(\n 'sandbox agent: preview URLs need a custom domain with wildcard DNS — ' +\n '*.workers.dev has no wildcard subdomains. Set PREVIEW_HOSTNAME to your ' +\n 'custom domain and add a `*.<domain>` route to the Worker.',\n )\n }\n return host\n}\n"],"names":[],"mappings":"AAiBA,SAAS,eAAe,MAAuB;AAC7C,QAAM,OAAO,KAAK,MAAM,GAAG,EAAE,CAAC;AAC9B,SAAO,SAAS,eAAe,SAAS,eAAe,SAAS;AAClE;AAGA,SAAS,OAAO,MAAc,UAA0B;AACtD,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,SAAO,UAAU,KAAK,WAAW,KAAK,MAAM,QAAQ,CAAC;AACvD;AAGA,SAAS,cAAc,MAAsB;AAC3C,QAAM,OAAO,KAAK,MAAM,GAAG,EAAE,CAAC;AAC9B,QAAM,SACJ,eAAe,IAAI,KAAK,SAAS,yBAAyB,SAAS;AACrE,SAAO,GAAG,MAAM,MAAM,IAAI;AAC5B;AA4BO,SAAS,oBACd,KACA,OACQ;AACR,QAAM,aAAa,IAAI,iBAAiB,KAAA;AACxC,MAAI,WAAY,QAAO,cAAc,UAAU;AAC/C,QAAM,OAAO,MAAM;AACnB,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAGJ;AAEA,MAAI,eAAe,IAAI,GAAG;AACxB,WAAO,+BAA+B,OAAO,MAAM,MAAM,CAAC;AAAA,EAC5D;AACA,SAAO,cAAc,IAAI;AAC3B;AAkBO,SAAS,mBACd,KACA,OACQ;AACR,QAAM,aAAa,IAAI,kBAAkB,KAAA;AACzC,MAAI,WAAY,QAAO;AACvB,QAAM,OAAO,MAAM;AACnB,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAGJ;AACA,MAAI,eAAe,IAAI,EAAG,QAAO;AACjC,MAAI,KAAK,SAAS,cAAc,GAAG;AACjC,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAIJ;AACA,SAAO;AACT;"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RunError, RunEvent, RunEventLog, RunEventLogReadOptions, RunRecord, TerminalRunStatus } from '@tanstack/ai-sandbox';
|
|
2
|
+
import { StreamChunk } from '@tanstack/ai';
|
|
3
|
+
export declare class DurableObjectRunEventLog implements RunEventLog {
|
|
4
|
+
private readonly storage;
|
|
5
|
+
/** Per-run wake-ups for live-tailing readers on THIS instance. */
|
|
6
|
+
private readonly waiters;
|
|
7
|
+
constructor(storage: DurableObjectStorage);
|
|
8
|
+
private require;
|
|
9
|
+
/** Wake (and clear) every reader blocked on this run. */
|
|
10
|
+
private wake;
|
|
11
|
+
open(input: {
|
|
12
|
+
runId: string;
|
|
13
|
+
threadId?: string;
|
|
14
|
+
}): Promise<RunRecord>;
|
|
15
|
+
append(runId: string, chunk: StreamChunk): Promise<number>;
|
|
16
|
+
finish(runId: string, status: TerminalRunStatus, error?: RunError): Promise<void>;
|
|
17
|
+
get(runId: string): Promise<RunRecord | null>;
|
|
18
|
+
read(runId: string, options?: RunEventLogReadOptions): AsyncIterable<RunEvent>;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve when an append/finish wakes this run, the signal aborts, or the
|
|
21
|
+
* fallback poll fires (the poll lets a reader that outlived its in-memory
|
|
22
|
+
* waiter — e.g. after the appending instance was evicted — keep progressing).
|
|
23
|
+
*/
|
|
24
|
+
private waitForChange;
|
|
25
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isTerminalRunStatus } from "@tanstack/ai-sandbox";
|
|
2
|
+
const TAIL_POLL_MS = 250;
|
|
3
|
+
const recKey = (runId) => `rec:${runId}`;
|
|
4
|
+
const evtKey = (runId, seq) => `evt:${runId}:${String(seq).padStart(8, "0")}`;
|
|
5
|
+
const evtPrefix = (runId) => `evt:${runId}:`;
|
|
6
|
+
class DurableObjectRunEventLog {
|
|
7
|
+
constructor(storage) {
|
|
8
|
+
this.storage = storage;
|
|
9
|
+
}
|
|
10
|
+
storage;
|
|
11
|
+
/** Per-run wake-ups for live-tailing readers on THIS instance. */
|
|
12
|
+
waiters = /* @__PURE__ */ new Map();
|
|
13
|
+
async require(runId) {
|
|
14
|
+
const record = await this.storage.get(recKey(runId));
|
|
15
|
+
if (!record) throw new Error(`run-log: unknown runId "${runId}"`);
|
|
16
|
+
return record;
|
|
17
|
+
}
|
|
18
|
+
/** Wake (and clear) every reader blocked on this run. */
|
|
19
|
+
wake(runId) {
|
|
20
|
+
const set = this.waiters.get(runId);
|
|
21
|
+
if (!set) return;
|
|
22
|
+
const pending = [...set];
|
|
23
|
+
set.clear();
|
|
24
|
+
for (const resolve of pending) resolve();
|
|
25
|
+
}
|
|
26
|
+
async open(input) {
|
|
27
|
+
const existing = await this.storage.get(recKey(input.runId));
|
|
28
|
+
if (existing) return existing;
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const record = {
|
|
31
|
+
runId: input.runId,
|
|
32
|
+
...input.threadId !== void 0 ? { threadId: input.threadId } : {},
|
|
33
|
+
status: "running",
|
|
34
|
+
lastSeq: -1,
|
|
35
|
+
createdAt: now,
|
|
36
|
+
updatedAt: now
|
|
37
|
+
};
|
|
38
|
+
await this.storage.put(recKey(input.runId), record);
|
|
39
|
+
return record;
|
|
40
|
+
}
|
|
41
|
+
async append(runId, chunk) {
|
|
42
|
+
const record = await this.require(runId);
|
|
43
|
+
if (isTerminalRunStatus(record.status)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`run-log: cannot append to terminal run "${runId}" (status=${record.status})`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const seq = record.lastSeq + 1;
|
|
49
|
+
const next = { ...record, lastSeq: seq, updatedAt: Date.now() };
|
|
50
|
+
await this.storage.transaction(async (txn) => {
|
|
51
|
+
await txn.put(evtKey(runId, seq), chunk);
|
|
52
|
+
await txn.put(recKey(runId), next);
|
|
53
|
+
});
|
|
54
|
+
this.wake(runId);
|
|
55
|
+
return seq;
|
|
56
|
+
}
|
|
57
|
+
async finish(runId, status, error) {
|
|
58
|
+
const record = await this.require(runId);
|
|
59
|
+
if (isTerminalRunStatus(record.status)) return;
|
|
60
|
+
const next = {
|
|
61
|
+
...record,
|
|
62
|
+
status,
|
|
63
|
+
...error !== void 0 ? { error } : {},
|
|
64
|
+
updatedAt: Date.now()
|
|
65
|
+
};
|
|
66
|
+
await this.storage.put(recKey(runId), next);
|
|
67
|
+
this.wake(runId);
|
|
68
|
+
}
|
|
69
|
+
async get(runId) {
|
|
70
|
+
return await this.storage.get(recKey(runId)) ?? null;
|
|
71
|
+
}
|
|
72
|
+
async *read(runId, options) {
|
|
73
|
+
await this.require(runId);
|
|
74
|
+
const signal = options?.signal;
|
|
75
|
+
let cursor = options?.fromSeq ?? -1;
|
|
76
|
+
while (!signal?.aborted) {
|
|
77
|
+
const record = await this.require(runId);
|
|
78
|
+
if (cursor < record.lastSeq) {
|
|
79
|
+
const events = await this.storage.list({
|
|
80
|
+
prefix: evtPrefix(runId),
|
|
81
|
+
start: evtKey(runId, cursor + 1)
|
|
82
|
+
});
|
|
83
|
+
for (const [, chunk] of events) {
|
|
84
|
+
cursor += 1;
|
|
85
|
+
yield { seq: cursor, chunk };
|
|
86
|
+
if (signal?.aborted) return;
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (isTerminalRunStatus(record.status)) return;
|
|
91
|
+
await this.waitForChange(runId, signal);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve when an append/finish wakes this run, the signal aborts, or the
|
|
96
|
+
* fallback poll fires (the poll lets a reader that outlived its in-memory
|
|
97
|
+
* waiter — e.g. after the appending instance was evicted — keep progressing).
|
|
98
|
+
*/
|
|
99
|
+
waitForChange(runId, signal) {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
let set = this.waiters.get(runId);
|
|
102
|
+
if (!set) {
|
|
103
|
+
set = /* @__PURE__ */ new Set();
|
|
104
|
+
this.waiters.set(runId, set);
|
|
105
|
+
}
|
|
106
|
+
const localSet = set;
|
|
107
|
+
const wake = () => {
|
|
108
|
+
localSet.delete(wake);
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
if (signal) signal.removeEventListener("abort", wake);
|
|
111
|
+
resolve();
|
|
112
|
+
};
|
|
113
|
+
const timer = setTimeout(wake, TAIL_POLL_MS);
|
|
114
|
+
localSet.add(wake);
|
|
115
|
+
if (signal) signal.addEventListener("abort", wake, { once: true });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
DurableObjectRunEventLog
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=run-log-do.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-log-do.js","sources":["../../src/run-log-do.ts"],"sourcesContent":["/**\n * A durable {@link RunEventLog} backed by Durable Object storage — the storage\n * half of the serverless/edge run model. The coordinator appends every\n * {@link StreamChunk} the agent emits under a monotonic `seq`; clients tail from\n * a cursor. Because events are PERSISTED (not held in a caller's open stream), a\n * reconnecting tab, a dropped WebSocket, or a coordinator that hibernated\n * between chunks all resume cleanly: replay everything after the client's\n * `lastSeq`, then live-tail to terminal.\n *\n * Mirrors {@link InMemoryRunEventLog} from `@tanstack/ai-sandbox` exactly.\n * Storage layout (keys scoped by `runId` so one DO can host many runs):\n * - `rec:<runId>` → the {@link RunRecord}\n * - `evt:<runId>:<seq8>` → the chunk for that seq (seq zero-padded to 8 digits\n * so `list({ prefix })` returns events in seq order).\n *\n * The live-tail wake-up (the in-memory waiter set) is per-INSTANCE; if the\n * instance is evicted mid-run, a reader re-reads the persisted backlog and the\n * `TAIL_POLL_MS` fallback poll keeps it progressing. No event is ever lost.\n *\n * NOTE: Workers-runtime code — compiles against `@cloudflare/workers-types`.\n */\nimport { isTerminalRunStatus } from '@tanstack/ai-sandbox'\nimport type {\n RunError,\n RunEvent,\n RunEventLog,\n RunEventLogReadOptions,\n RunRecord,\n TerminalRunStatus,\n} from '@tanstack/ai-sandbox'\nimport type { StreamChunk } from '@tanstack/ai'\n\n/** How long a post-eviction reader waits before re-polling storage (ms). */\nconst TAIL_POLL_MS = 250\n\nconst recKey = (runId: string): string => `rec:${runId}`\nconst evtKey = (runId: string, seq: number): string =>\n `evt:${runId}:${String(seq).padStart(8, '0')}`\nconst evtPrefix = (runId: string): string => `evt:${runId}:`\n\nexport class DurableObjectRunEventLog implements RunEventLog {\n /** Per-run wake-ups for live-tailing readers on THIS instance. */\n private readonly waiters = new Map<string, Set<() => void>>()\n\n constructor(private readonly storage: DurableObjectStorage) {}\n\n private async require(runId: string): Promise<RunRecord> {\n const record = await this.storage.get<RunRecord>(recKey(runId))\n if (!record) throw new Error(`run-log: unknown runId \"${runId}\"`)\n return record\n }\n\n /** Wake (and clear) every reader blocked on this run. */\n private wake(runId: string): void {\n const set = this.waiters.get(runId)\n if (!set) return\n const pending = [...set]\n set.clear()\n for (const resolve of pending) resolve()\n }\n\n async open(input: { runId: string; threadId?: string }): Promise<RunRecord> {\n const existing = await this.storage.get<RunRecord>(recKey(input.runId))\n if (existing) return existing\n const now = Date.now()\n const record: RunRecord = {\n runId: input.runId,\n ...(input.threadId !== undefined ? { threadId: input.threadId } : {}),\n status: 'running',\n lastSeq: -1,\n createdAt: now,\n updatedAt: now,\n }\n await this.storage.put(recKey(input.runId), record)\n return record\n }\n\n async append(runId: string, chunk: StreamChunk): Promise<number> {\n const record = await this.require(runId)\n if (isTerminalRunStatus(record.status)) {\n throw new Error(\n `run-log: cannot append to terminal run \"${runId}\" (status=${record.status})`,\n )\n }\n const seq = record.lastSeq + 1\n const next: RunRecord = { ...record, lastSeq: seq, updatedAt: Date.now() }\n // One transaction so the appended event and its bumped record commit\n // together — a reader never sees a lastSeq pointing at a missing event.\n await this.storage.transaction(async (txn) => {\n await txn.put(evtKey(runId, seq), chunk)\n await txn.put(recKey(runId), next)\n })\n this.wake(runId)\n return seq\n }\n\n async finish(\n runId: string,\n status: TerminalRunStatus,\n error?: RunError,\n ): Promise<void> {\n const record = await this.require(runId)\n if (isTerminalRunStatus(record.status)) return\n const next: RunRecord = {\n ...record,\n status,\n ...(error !== undefined ? { error } : {}),\n updatedAt: Date.now(),\n }\n await this.storage.put(recKey(runId), next)\n this.wake(runId)\n }\n\n async get(runId: string): Promise<RunRecord | null> {\n return (await this.storage.get<RunRecord>(recKey(runId))) ?? null\n }\n\n async *read(\n runId: string,\n options?: RunEventLogReadOptions,\n ): AsyncIterable<RunEvent> {\n await this.require(runId)\n const signal = options?.signal\n let cursor = options?.fromSeq ?? -1\n\n while (!signal?.aborted) {\n const record = await this.require(runId)\n // Drain the persisted backlog after the cursor in seq order. The\n // zero-padded keys make the prefix list naturally ordered.\n if (cursor < record.lastSeq) {\n const events = await this.storage.list<StreamChunk>({\n prefix: evtPrefix(runId),\n start: evtKey(runId, cursor + 1),\n })\n for (const [, chunk] of events) {\n cursor += 1\n yield { seq: cursor, chunk }\n if (signal?.aborted) return\n }\n continue\n }\n if (isTerminalRunStatus(record.status)) return\n await this.waitForChange(runId, signal)\n }\n }\n\n /**\n * Resolve when an append/finish wakes this run, the signal aborts, or the\n * fallback poll fires (the poll lets a reader that outlived its in-memory\n * waiter — e.g. after the appending instance was evicted — keep progressing).\n */\n private waitForChange(runId: string, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve) => {\n let set = this.waiters.get(runId)\n if (!set) {\n set = new Set()\n this.waiters.set(runId, set)\n }\n const localSet = set\n const wake = (): void => {\n localSet.delete(wake)\n clearTimeout(timer)\n if (signal) signal.removeEventListener('abort', wake)\n resolve()\n }\n const timer = setTimeout(wake, TAIL_POLL_MS)\n localSet.add(wake)\n if (signal) signal.addEventListener('abort', wake, { once: true })\n })\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
import { AnyTextAdapter } from '@tanstack/ai';
|
|
3
|
+
import { HarnessId } from './protocol.js';
|
|
4
|
+
/** The `{ harness, model }` the caller maps to a concrete `*Text` adapter. */
|
|
5
|
+
export interface ResolveAdapterInput {
|
|
6
|
+
harness: HarnessId;
|
|
7
|
+
model: string;
|
|
8
|
+
}
|
|
9
|
+
/** Options for {@link runInContainerHarness}. */
|
|
10
|
+
export interface RunInContainerHarnessOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Build the text adapter `chat()` runs for one request's `{ harness, model }`.
|
|
13
|
+
* The app supplies this so the package doesn't depend on every adapter package
|
|
14
|
+
* — e.g. `({ model }) => claudeCodeText(model)`.
|
|
15
|
+
*/
|
|
16
|
+
resolveAdapter: (input: ResolveAdapterInput) => AnyTextAdapter;
|
|
17
|
+
/** Port to listen on. Defaults to `RUNNER_PORT` env, then `8080`. */
|
|
18
|
+
port?: number;
|
|
19
|
+
}
|
|
20
|
+
/** What {@link runInContainerHarness} returns: the listening `node:http` server. */
|
|
21
|
+
export interface ContainerHarnessServer {
|
|
22
|
+
/** The underlying `node:http` server (already `listen()`ing). */
|
|
23
|
+
server: Server;
|
|
24
|
+
/** The port it is listening on. */
|
|
25
|
+
port: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start the in-container harness runner: a `node:http` server with `GET /health`
|
|
29
|
+
* and `POST /run`. Call this as the container's program; the app supplies only
|
|
30
|
+
* `resolveAdapter`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runInContainerHarness(options: RunInContainerHarnessOptions): ContainerHarnessServer;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { EventType, chat } from "@tanstack/ai";
|
|
3
|
+
import { defineSandbox, withSandbox, remoteToolStubs, httpRemoteToolExecutor, defineWorkspace, createSecrets } from "@tanstack/ai-sandbox";
|
|
4
|
+
import { localProcessSandbox } from "@tanstack/ai-sandbox-local-process";
|
|
5
|
+
import { parseContainerRunRequest } from "./protocol.js";
|
|
6
|
+
function readBody(req) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
let body = "";
|
|
9
|
+
req.setEncoding("utf8");
|
|
10
|
+
req.on("data", (chunk) => {
|
|
11
|
+
body += chunk;
|
|
12
|
+
});
|
|
13
|
+
req.on("end", () => resolve(body));
|
|
14
|
+
req.on("error", reject);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function reconstituteWorkspace(workspace) {
|
|
18
|
+
if (workspace.secrets === void 0) return workspace;
|
|
19
|
+
const names = Object.keys(workspace.secrets);
|
|
20
|
+
if (names.length === 0) return workspace;
|
|
21
|
+
const values = {};
|
|
22
|
+
for (const name of names) {
|
|
23
|
+
const value = process.env[name];
|
|
24
|
+
if (value === void 0 || value === "") {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`runInContainerHarness: secret "${name}" is not set in the container env`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
values[name] = value;
|
|
30
|
+
}
|
|
31
|
+
return defineWorkspace({ ...workspace, secrets: createSecrets(values) });
|
|
32
|
+
}
|
|
33
|
+
function runAgent(request, resolveAdapter) {
|
|
34
|
+
const sandbox = defineSandbox({
|
|
35
|
+
// The container IS the host: no isolation, just run on its own filesystem.
|
|
36
|
+
id: "colocated-in-container",
|
|
37
|
+
provider: localProcessSandbox(),
|
|
38
|
+
// Honor the app's workspace (source / setup / skills / …), with the secrets
|
|
39
|
+
// re-resolved from the container env.
|
|
40
|
+
workspace: reconstituteWorkspace(request.workspace)
|
|
41
|
+
});
|
|
42
|
+
return chat({
|
|
43
|
+
threadId: request.threadId,
|
|
44
|
+
adapter: resolveAdapter({
|
|
45
|
+
harness: request.harness,
|
|
46
|
+
model: request.model
|
|
47
|
+
}),
|
|
48
|
+
messages: request.messages,
|
|
49
|
+
stream: true,
|
|
50
|
+
// Rebuild the DO's host tools as stubs whose execute() POSTs back to the DO.
|
|
51
|
+
// The adapter bridges them over the in-container localhost MCP transport.
|
|
52
|
+
tools: remoteToolStubs(
|
|
53
|
+
request.toolDescriptors,
|
|
54
|
+
httpRemoteToolExecutor(request.toolExecUrl, request.toolExecToken)
|
|
55
|
+
),
|
|
56
|
+
// Provide the in-container local-process sandbox handle the adapter needs.
|
|
57
|
+
middleware: [withSandbox(sandbox)]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function handleRun(req, res, resolveAdapter) {
|
|
61
|
+
const parsed = JSON.parse(await readBody(req));
|
|
62
|
+
const request = parseContainerRunRequest(parsed);
|
|
63
|
+
res.writeHead(200, {
|
|
64
|
+
"content-type": "application/x-ndjson",
|
|
65
|
+
"cache-control": "no-cache"
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
for await (const chunk of runAgent(request, resolveAdapter)) {
|
|
69
|
+
res.write(`${JSON.stringify(chunk)}
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
res.write(`${JSON.stringify({ type: EventType.RUN_ERROR, message })}
|
|
75
|
+
`);
|
|
76
|
+
} finally {
|
|
77
|
+
res.end();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function runInContainerHarness(options) {
|
|
81
|
+
const port = options.port ?? Number.parseInt(process.env.RUNNER_PORT ?? "8080", 10);
|
|
82
|
+
const server = createServer((req, res) => {
|
|
83
|
+
if (req.method === "POST" && req.url === "/run") {
|
|
84
|
+
handleRun(req, res, options.resolveAdapter).catch((error) => {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
if (!res.headersSent) {
|
|
87
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
88
|
+
}
|
|
89
|
+
res.end(message);
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
94
|
+
res.writeHead(200).end("ok");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
res.writeHead(404).end("not found");
|
|
98
|
+
});
|
|
99
|
+
server.listen(port, () => {
|
|
100
|
+
console.log(`[container-runner] listening on :${port}`);
|
|
101
|
+
});
|
|
102
|
+
return { server, port };
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
runInContainerHarness
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sources":["../../src/runner.ts"],"sourcesContent":["/**\n * `runInContainerHarness` — the IN-CONTAINER harness runner, shipped from the\n * package so a co-located app's container program is a single function call.\n *\n * This is the heart of the CO-LOCATED model: the agent harness loop AND its MCP\n * tool-bridge run HERE, on the container's own localhost. The Durable Object\n * outside never calls `chat()`; it POSTs `/run` to this server and reads the\n * NDJSON stream back.\n *\n * DO ── POST /run {messages, harness, model, workspace, toolDescriptors,\n * toolExecUrl, toolExecToken} ──▶ THIS\n * THIS ── NDJSON stream of StreamChunk ──────────────────────────────────▶ DO\n *\n * It is a tiny `node:http` server (NODE/container side — NOT Workers; it uses\n * `localProcessSandbox`). On `POST /run` it validates the {@link\n * ContainerRunRequest}, builds `chat()` with the in-container `local-process`\n * sandbox and the adapter the CALLER resolves, and streams each {@link\n * StreamChunk} back as NDJSON (one JSON object per line).\n *\n * Why the MCP bridge is genuinely in-container: the in-container sandbox is\n * `localProcessSandbox()` — the container IS the host — so the harness adapter\n * serves its tool-bridge over the container's own `localhost` and feeds the\n * prompt over NATIVE writable stdin (no file-redirect; the bridge URL/token\n * never leave the container). The MCP protocol never crosses the network.\n *\n * The ONE thing that still crosses back to the DO is host-tool EXECUTION: each\n * tool rebuilt by {@link remoteToolStubs} delegates its `execute()` to {@link\n * httpRemoteToolExecutor}, which POSTs `{ name, args }` (bearer-gated) to the\n * DO's `toolExecUrl`:\n *\n * agent → in-container MCP bridge → stub.execute → httpRemoteToolExecutor → DO\n *\n * The app supplies only `resolveAdapter` — which `*Text` adapter to build for a\n * given `{ harness, model }`. The server + `chat()` wiring lives here, so the\n * package doesn't depend on every adapter package.\n *\n * NOTE: container-side Node code — compiles against the real TanStack AI types;\n * not runtime-verified in this repo (no container build in CI).\n */\nimport { createServer } from 'node:http'\nimport { EventType, chat } from '@tanstack/ai'\nimport {\n createSecrets,\n defineSandbox,\n defineWorkspace,\n httpRemoteToolExecutor,\n remoteToolStubs,\n withSandbox,\n} from '@tanstack/ai-sandbox'\nimport { localProcessSandbox } from '@tanstack/ai-sandbox-local-process'\nimport { parseContainerRunRequest } from './protocol'\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http'\nimport type { AnyTextAdapter, StreamChunk } from '@tanstack/ai'\nimport type { WorkspaceDefinition } from '@tanstack/ai-sandbox'\nimport type { ContainerRunRequest, HarnessId } from './protocol'\n\n/** The `{ harness, model }` the caller maps to a concrete `*Text` adapter. */\nexport interface ResolveAdapterInput {\n harness: HarnessId\n model: string\n}\n\n/** Options for {@link runInContainerHarness}. */\nexport interface RunInContainerHarnessOptions {\n /**\n * Build the text adapter `chat()` runs for one request's `{ harness, model }`.\n * The app supplies this so the package doesn't depend on every adapter package\n * — e.g. `({ model }) => claudeCodeText(model)`.\n */\n resolveAdapter: (input: ResolveAdapterInput) => AnyTextAdapter\n /** Port to listen on. Defaults to `RUNNER_PORT` env, then `8080`. */\n port?: number\n}\n\n/** What {@link runInContainerHarness} returns: the listening `node:http` server. */\nexport interface ContainerHarnessServer {\n /** The underlying `node:http` server (already `listen()`ing). */\n server: Server\n /** The port it is listening on. */\n port: number\n}\n\n/** Read a request body fully into a string (small JSON payloads only). */\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n let body = ''\n req.setEncoding('utf8')\n req.on('data', (chunk: string) => {\n body += chunk\n })\n req.on('end', () => resolve(body))\n req.on('error', reject)\n })\n}\n\n/**\n * Rebuild the request's workspace with a real `createSecrets`, pulling each\n * referenced secret's VALUE from the container env. Secret values never cross\n * the `POST /run` boundary (`createSecrets` stores them under a non-enumerable\n * symbol, so serializing the workspace carries only the names) — the DO injects\n * them into the container env via `sandbox.setEnvVars`, and we reconstitute them\n * here. A referenced secret missing from the env is a hard error, never a silent\n * keyless run.\n */\nfunction reconstituteWorkspace(\n workspace: WorkspaceDefinition,\n): WorkspaceDefinition {\n if (workspace.secrets === undefined) return workspace\n const names = Object.keys(workspace.secrets)\n if (names.length === 0) return workspace\n const values: Record<string, string> = {}\n for (const name of names) {\n const value = process.env[name]\n if (value === undefined || value === '') {\n throw new Error(\n `runInContainerHarness: secret \"${name}\" is not set in the container env`,\n )\n }\n values[name] = value\n }\n return defineWorkspace({ ...workspace, secrets: createSecrets(values) })\n}\n\n/**\n * Build the `chat()` stream that runs the harness on THIS container via the\n * `local-process` sandbox. The agent's `chat()` tools are stubs that delegate\n * back to the DO; everything else (the harness loop, the MCP bridge, stdin)\n * stays on localhost.\n */\nfunction runAgent(\n request: ContainerRunRequest,\n resolveAdapter: (input: ResolveAdapterInput) => AnyTextAdapter,\n): AsyncIterable<StreamChunk> {\n const sandbox = defineSandbox({\n // The container IS the host: no isolation, just run on its own filesystem.\n id: 'colocated-in-container',\n provider: localProcessSandbox(),\n // Honor the app's workspace (source / setup / skills / …), with the secrets\n // re-resolved from the container env.\n workspace: reconstituteWorkspace(request.workspace),\n })\n\n // `stream: true` (no outputSchema) makes chat() return AsyncIterable<StreamChunk>.\n return chat({\n threadId: request.threadId,\n adapter: resolveAdapter({\n harness: request.harness,\n model: request.model,\n }),\n messages: request.messages,\n stream: true,\n // Rebuild the DO's host tools as stubs whose execute() POSTs back to the DO.\n // The adapter bridges them over the in-container localhost MCP transport.\n tools: remoteToolStubs(\n request.toolDescriptors,\n httpRemoteToolExecutor(request.toolExecUrl, request.toolExecToken),\n ),\n // Provide the in-container local-process sandbox handle the adapter needs.\n middleware: [withSandbox(sandbox)],\n })\n}\n\n/** Stream the agent's chunks to the response as NDJSON, one object per line. */\nasync function handleRun(\n req: IncomingMessage,\n res: ServerResponse,\n resolveAdapter: (input: ResolveAdapterInput) => AnyTextAdapter,\n): Promise<void> {\n const parsed: unknown = JSON.parse(await readBody(req))\n const request = parseContainerRunRequest(parsed)\n res.writeHead(200, {\n 'content-type': 'application/x-ndjson',\n 'cache-control': 'no-cache',\n })\n // The DO appends each line to its durable run-log; here we are the producer,\n // so we surface a mid-stream failure as a terminal RUN_ERROR line the DO will\n // append + finish on, never a silently truncated stream.\n try {\n for await (const chunk of runAgent(request, resolveAdapter)) {\n res.write(`${JSON.stringify(chunk)}\\n`)\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n res.write(`${JSON.stringify({ type: EventType.RUN_ERROR, message })}\\n`)\n } finally {\n res.end()\n }\n}\n\n/**\n * Start the in-container harness runner: a `node:http` server with `GET /health`\n * and `POST /run`. Call this as the container's program; the app supplies only\n * `resolveAdapter`.\n */\nexport function runInContainerHarness(\n options: RunInContainerHarnessOptions,\n): ContainerHarnessServer {\n const port =\n options.port ?? Number.parseInt(process.env.RUNNER_PORT ?? '8080', 10)\n\n const server = createServer((req, res) => {\n if (req.method === 'POST' && req.url === '/run') {\n handleRun(req, res, options.resolveAdapter).catch((error: unknown) => {\n // A failure BEFORE we start streaming (e.g. a malformed body) is a 400 —\n // surfaced, never swallowed.\n const message = error instanceof Error ? error.message : String(error)\n if (!res.headersSent) {\n res.writeHead(400, { 'content-type': 'text/plain' })\n }\n res.end(message)\n })\n return\n }\n if (req.method === 'GET' && req.url === '/health') {\n res.writeHead(200).end('ok')\n return\n }\n res.writeHead(404).end('not found')\n })\n\n server.listen(port, () => {\n console.log(`[container-runner] listening on :${port}`)\n })\n\n return { server, port }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Crypto helpers for the Workers runtime, where `node:crypto` is
|
|
3
|
+
* unavailable. The sandbox layer's `timingSafeBearerEqual` is node-based; this
|
|
4
|
+
* is the equivalent for a Worker / Durable Object.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Constant-time check of an `Authorization: Bearer <token>` header against the
|
|
8
|
+
* expected token. A length mismatch returns false early (token length is not
|
|
9
|
+
* secret); the equal-length comparison is timing-safe.
|
|
10
|
+
*/
|
|
11
|
+
export declare function timingSafeBearerEqualWeb(header: string | undefined, token: string): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function timingSafeBearerEqualWeb(header, token) {
|
|
2
|
+
if (header === void 0) return false;
|
|
3
|
+
const a = new TextEncoder().encode(header);
|
|
4
|
+
const b = new TextEncoder().encode(`Bearer ${token}`);
|
|
5
|
+
if (a.length !== b.length) return false;
|
|
6
|
+
let diff = 0;
|
|
7
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
8
|
+
const ai = a[i];
|
|
9
|
+
const bi = b[i];
|
|
10
|
+
if (ai === void 0 || bi === void 0) return false;
|
|
11
|
+
diff |= ai ^ bi;
|
|
12
|
+
}
|
|
13
|
+
return diff === 0;
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
timingSafeBearerEqualWeb
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=web-crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-crypto.js","sources":["../../src/web-crypto.ts"],"sourcesContent":["/**\n * Web Crypto helpers for the Workers runtime, where `node:crypto` is\n * unavailable. The sandbox layer's `timingSafeBearerEqual` is node-based; this\n * is the equivalent for a Worker / Durable Object.\n */\n\n/**\n * Constant-time check of an `Authorization: Bearer <token>` header against the\n * expected token. A length mismatch returns false early (token length is not\n * secret); the equal-length comparison is timing-safe.\n */\nexport function timingSafeBearerEqualWeb(\n header: string | undefined,\n token: string,\n): boolean {\n if (header === undefined) return false\n const a = new TextEncoder().encode(header)\n const b = new TextEncoder().encode(`Bearer ${token}`)\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i += 1) {\n const ai = a[i]\n const bi = b[i]\n // In-bounds by construction (i < a.length === b.length); the guard satisfies\n // `noUncheckedIndexedAccess` without a non-null assertion and treats any\n // impossible out-of-bounds read as \"not equal\".\n if (ai === undefined || bi === undefined) return false\n diff |= ai ^ bi\n }\n return diff === 0\n}\n"],"names":[],"mappings":"AAWO,SAAS,yBACd,QACA,OACS;AACT,MAAI,WAAW,OAAW,QAAO;AACjC,QAAM,IAAI,IAAI,cAAc,OAAO,MAAM;AACzC,QAAM,IAAI,IAAI,YAAA,EAAc,OAAO,UAAU,KAAK,EAAE;AACpD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,GAAG;AACpC,UAAM,KAAK,EAAE,CAAC;AACd,UAAM,KAAK,EAAE,CAAC;AAId,QAAI,OAAO,UAAa,OAAO,OAAW,QAAO;AACjD,YAAQ,KAAK;AAAA,EACf;AACA,SAAO,SAAS;AAClB;"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SandboxCoordinator } from './coordinator.js';
|
|
2
|
+
/** Resolve the coordinator DO that owns a thread's runs. */
|
|
3
|
+
export type ResolveCoordinator<TEnv> = (env: TEnv, threadId: string) => DurableObjectStub<SandboxCoordinator<TEnv>>;
|
|
4
|
+
/**
|
|
5
|
+
* Build the Worker fetch handler. `resolveCoordinator` maps `(env, threadId)` to
|
|
6
|
+
* the DO stub that owns that thread's runs.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createSandboxAgentWorker<TEnv>(resolveCoordinator: ResolveCoordinator<TEnv>): ExportedHandler<TEnv>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { proxyToSandbox } from "@cloudflare/sandbox";
|
|
2
|
+
function parseCreateRunBody(value) {
|
|
3
|
+
if (value === null || typeof value !== "object") {
|
|
4
|
+
throw new Error("body must be a JSON object");
|
|
5
|
+
}
|
|
6
|
+
if (!("threadId" in value) || typeof value.threadId !== "string" || value.threadId === "") {
|
|
7
|
+
throw new Error("body.threadId must be a non-empty string");
|
|
8
|
+
}
|
|
9
|
+
if (!("messages" in value) || !Array.isArray(value.messages) || value.messages.length === 0) {
|
|
10
|
+
throw new Error("body.messages must be a non-empty array");
|
|
11
|
+
}
|
|
12
|
+
for (const message of value.messages) {
|
|
13
|
+
if (message === null || typeof message !== "object") {
|
|
14
|
+
throw new Error("each message must be an object");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
let metadata;
|
|
18
|
+
if ("metadata" in value && value.metadata !== void 0) {
|
|
19
|
+
if (!isRecord(value.metadata)) {
|
|
20
|
+
throw new Error("body.metadata must be an object");
|
|
21
|
+
}
|
|
22
|
+
metadata = value.metadata;
|
|
23
|
+
}
|
|
24
|
+
return { threadId: value.threadId, messages: value.messages, metadata };
|
|
25
|
+
}
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function hasSandboxBinding(env) {
|
|
30
|
+
return env !== null && typeof env === "object" && "Sandbox" in env && env.Sandbox !== void 0;
|
|
31
|
+
}
|
|
32
|
+
function jsonResponse(body, status = 200) {
|
|
33
|
+
return new Response(JSON.stringify(body), {
|
|
34
|
+
status,
|
|
35
|
+
headers: { "content-type": "application/json" }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function createSandboxAgentWorker(resolveCoordinator) {
|
|
39
|
+
return {
|
|
40
|
+
async fetch(request, env) {
|
|
41
|
+
if (hasSandboxBinding(env)) {
|
|
42
|
+
const proxied = await proxyToSandbox(request, env);
|
|
43
|
+
if (proxied) return proxied;
|
|
44
|
+
}
|
|
45
|
+
const url = new URL(request.url);
|
|
46
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
47
|
+
if (request.method === "POST" && parts.length === 1 && parts[0] === "runs") {
|
|
48
|
+
let body;
|
|
49
|
+
try {
|
|
50
|
+
body = parseCreateRunBody(await request.json());
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
return jsonResponse({ error: message }, 400);
|
|
54
|
+
}
|
|
55
|
+
const runId = crypto.randomUUID();
|
|
56
|
+
const input = {
|
|
57
|
+
runId,
|
|
58
|
+
threadId: body.threadId,
|
|
59
|
+
messages: body.messages,
|
|
60
|
+
// The host this request arrived on. Coordinators derive the container's
|
|
61
|
+
// callback hosts from it when `PUBLIC_HOSTNAME`/`PREVIEW_HOSTNAME` are
|
|
62
|
+
// unset. On Cloudflare this is safe to trust — the edge only routes
|
|
63
|
+
// hostnames you own to your Worker. See `resolveBridgeOrigin` /
|
|
64
|
+
// `resolvePreviewHost`.
|
|
65
|
+
publicHost: url.host,
|
|
66
|
+
// Forwarded verbatim to the app's resolvers (e.g. the chosen harness).
|
|
67
|
+
metadata: body.metadata
|
|
68
|
+
};
|
|
69
|
+
await resolveCoordinator(env, body.threadId).startRun(input);
|
|
70
|
+
return jsonResponse({ runId }, 202);
|
|
71
|
+
}
|
|
72
|
+
const threadId = url.searchParams.get("threadId");
|
|
73
|
+
if (threadId !== null) {
|
|
74
|
+
return resolveCoordinator(env, threadId).fetch(request);
|
|
75
|
+
}
|
|
76
|
+
return jsonResponse({ error: "threadId query param required" }, 400);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
createSandboxAgentWorker
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sources":["../../src/worker.ts"],"sourcesContent":["/**\n * `createSandboxAgentWorker` — the STATELESS trigger + router Worker for the\n * serverless/edge agent run model, factored so an app never writes it by hand.\n *\n * It never drives a run; it forwards to the owning {@link SandboxCoordinator}\n * Durable Object and returns immediately:\n *\n * POST /runs → coordinator.startRun(...) → `202 { runId }` (the\n * Worker invocation ENDS here; it does NOT wait for\n * the agent run — that is the whole point).\n * * (with ?threadId) → forward to coordinator.fetch(request), which the\n * base handles for `/runs/:id`, `/runs/:id/stream`,\n * and a subclass handles for `/_bridge`/`/tool-exec`.\n *\n * The coordinator that owns a thread's runs is resolved by the caller-supplied\n * `resolveCoordinator(env, threadId)` — usually a DO addressed by `threadId`, so\n * every event for a conversation lands in one coordinator and the sandbox is\n * reused per thread. {@link createCloudflareSandboxAgent} supplies a resolver\n * that uses the `RUN_COORDINATOR` binding.\n *\n * NOTE: Workers-runtime code — compiles against the real Cloudflare + TanStack\n * AI types; not runtime-verified in this repo (no Workers runtime here).\n */\nimport { proxyToSandbox } from '@cloudflare/sandbox'\nimport type { SandboxCoordinator, StartRunInput } from './coordinator'\nimport type { ModelMessage } from '@tanstack/ai'\nimport type { Sandbox } from '@cloudflare/sandbox'\n\n/** Resolve the coordinator DO that owns a thread's runs. */\nexport type ResolveCoordinator<TEnv> = (\n env: TEnv,\n threadId: string,\n) => DurableObjectStub<SandboxCoordinator<TEnv>>\n\n/** Body of `POST /runs`. */\ninterface CreateRunBody {\n threadId: string\n messages: Array<ModelMessage>\n /** Forwarded verbatim to the app's resolvers — see {@link StartRunInput.metadata}. */\n metadata?: Record<string, unknown>\n}\n\n/** Narrow the parsed JSON body without casting (project rule: no `as`). */\nfunction parseCreateRunBody(value: unknown): CreateRunBody {\n if (value === null || typeof value !== 'object') {\n throw new Error('body must be a JSON object')\n }\n if (\n !('threadId' in value) ||\n typeof value.threadId !== 'string' ||\n value.threadId === ''\n ) {\n throw new Error('body.threadId must be a non-empty string')\n }\n if (\n !('messages' in value) ||\n !Array.isArray(value.messages) ||\n value.messages.length === 0\n ) {\n throw new Error('body.messages must be a non-empty array')\n }\n // The chat engine validates message shape; we only assert it is an array of\n // objects here so the request fails fast with a clear 400 on garbage input.\n for (const message of value.messages) {\n if (message === null || typeof message !== 'object') {\n throw new Error('each message must be an object')\n }\n }\n // Optional free-form pass-through (app-validated). Must be an object if present.\n let metadata: Record<string, unknown> | undefined\n if ('metadata' in value && value.metadata !== undefined) {\n if (!isRecord(value.metadata)) {\n throw new Error('body.metadata must be an object')\n }\n metadata = value.metadata\n }\n return { threadId: value.threadId, messages: value.messages, metadata }\n}\n\n/** A JSON object — narrows `unknown` to `Record<string, unknown>` cast-free. */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\n/** A Worker env that carries the Sandbox DO namespace `proxyToSandbox` needs. */\ninterface SandboxBindingEnv {\n Sandbox: DurableObjectNamespace<Sandbox>\n}\n\n/** Narrow an env to one with a Sandbox binding (so previews can be proxied). */\nfunction hasSandboxBinding<TEnv>(env: TEnv): env is TEnv & SandboxBindingEnv {\n return (\n env !== null &&\n typeof env === 'object' &&\n 'Sandbox' in env &&\n env.Sandbox !== undefined\n )\n}\n\nfunction jsonResponse(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\n/**\n * Build the Worker fetch handler. `resolveCoordinator` maps `(env, threadId)` to\n * the DO stub that owns that thread's runs.\n */\nexport function createSandboxAgentWorker<TEnv>(\n resolveCoordinator: ResolveCoordinator<TEnv>,\n): ExportedHandler<TEnv> {\n return {\n async fetch(request: Request, env: TEnv): Promise<Response> {\n // Preview-port traffic for exposed sandbox ports is routed by hostname; let\n // the sandbox runtime claim those requests before our app routes run. Only\n // possible when the env actually carries the Sandbox binding.\n if (hasSandboxBinding(env)) {\n const proxied = await proxyToSandbox(request, env)\n if (proxied) return proxied\n }\n\n const url = new URL(request.url)\n const parts = url.pathname.split('/').filter(Boolean)\n\n // POST /runs — trigger a run, return 202 immediately.\n if (\n request.method === 'POST' &&\n parts.length === 1 &&\n parts[0] === 'runs'\n ) {\n let body: CreateRunBody\n try {\n body = parseCreateRunBody(await request.json())\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return jsonResponse({ error: message }, 400)\n }\n const runId = crypto.randomUUID()\n const input: StartRunInput = {\n runId,\n threadId: body.threadId,\n messages: body.messages,\n // The host this request arrived on. Coordinators derive the container's\n // callback hosts from it when `PUBLIC_HOSTNAME`/`PREVIEW_HOSTNAME` are\n // unset. On Cloudflare this is safe to trust — the edge only routes\n // hostnames you own to your Worker. See `resolveBridgeOrigin` /\n // `resolvePreviewHost`.\n publicHost: url.host,\n // Forwarded verbatim to the app's resolvers (e.g. the chosen harness).\n metadata: body.metadata,\n }\n // RPC into the coordinator. `startRun` registers the run and returns\n // immediately under `ctx.waitUntil`; we do NOT await the agent loop.\n await resolveCoordinator(env, body.threadId).startRun(input)\n return jsonResponse({ runId }, 202)\n }\n\n // Everything else for a run needs the owning coordinator, addressed by the\n // `threadId` query the Worker carries so it never reads run state itself.\n // The base coordinator routes `/runs/:id` + `/runs/:id/stream`, and a\n // subclass routes `/_bridge/:runId` (DO-drives) or `/tool-exec/:runId`\n // (co-located) — all reachable through one forward.\n const threadId = url.searchParams.get('threadId')\n if (threadId !== null) {\n return resolveCoordinator(env, threadId).fetch(request)\n }\n\n return jsonResponse({ error: 'threadId query param required' }, 400)\n },\n }\n}\n"],"names":[],"mappings":";AA2CA,SAAS,mBAAmB,OAA+B;AACzD,MAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AACA,MACE,EAAE,cAAc,UAChB,OAAO,MAAM,aAAa,YAC1B,MAAM,aAAa,IACnB;AACA,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AACA,MACE,EAAE,cAAc,UAChB,CAAC,MAAM,QAAQ,MAAM,QAAQ,KAC7B,MAAM,SAAS,WAAW,GAC1B;AACA,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAGA,aAAW,WAAW,MAAM,UAAU;AACpC,QAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;AACnD,YAAM,IAAI,MAAM,gCAAgC;AAAA,IAClD;AAAA,EACF;AAEA,MAAI;AACJ,MAAI,cAAc,SAAS,MAAM,aAAa,QAAW;AACvD,QAAI,CAAC,SAAS,MAAM,QAAQ,GAAG;AAC7B,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AACA,eAAW,MAAM;AAAA,EACnB;AACA,SAAO,EAAE,UAAU,MAAM,UAAU,UAAU,MAAM,UAAU,SAAA;AAC/D;AAGA,SAAS,SAAS,OAAkD;AAClE,SAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAQA,SAAS,kBAAwB,KAA4C;AAC3E,SACE,QAAQ,QACR,OAAO,QAAQ,YACf,aAAa,OACb,IAAI,YAAY;AAEpB;AAEA,SAAS,aAAa,MAAe,SAAS,KAAe;AAC3D,SAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;AAAA,IACxC;AAAA,IACA,SAAS,EAAE,gBAAgB,mBAAA;AAAA,EAAmB,CAC/C;AACH;AAMO,SAAS,yBACd,oBACuB;AACvB,SAAO;AAAA,IACL,MAAM,MAAM,SAAkB,KAA8B;AAI1D,UAAI,kBAAkB,GAAG,GAAG;AAC1B,cAAM,UAAU,MAAM,eAAe,SAAS,GAAG;AACjD,YAAI,QAAS,QAAO;AAAA,MACtB;AAEA,YAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,YAAM,QAAQ,IAAI,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AAGpD,UACE,QAAQ,WAAW,UACnB,MAAM,WAAW,KACjB,MAAM,CAAC,MAAM,QACb;AACA,YAAI;AACJ,YAAI;AACF,iBAAO,mBAAmB,MAAM,QAAQ,KAAA,CAAM;AAAA,QAChD,SAAS,OAAO;AACd,gBAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,iBAAO,aAAa,EAAE,OAAO,QAAA,GAAW,GAAG;AAAA,QAC7C;AACA,cAAM,QAAQ,OAAO,WAAA;AACrB,cAAM,QAAuB;AAAA,UAC3B;AAAA,UACA,UAAU,KAAK;AAAA,UACf,UAAU,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMf,YAAY,IAAI;AAAA;AAAA,UAEhB,UAAU,KAAK;AAAA,QAAA;AAIjB,cAAM,mBAAmB,KAAK,KAAK,QAAQ,EAAE,SAAS,KAAK;AAC3D,eAAO,aAAa,EAAE,MAAA,GAAS,GAAG;AAAA,MACpC;AAOA,YAAM,WAAW,IAAI,aAAa,IAAI,UAAU;AAChD,UAAI,aAAa,MAAM;AACrB,eAAO,mBAAmB,KAAK,QAAQ,EAAE,MAAM,OAAO;AAAA,MACxD;AAEA,aAAO,aAAa,EAAE,OAAO,gCAAA,GAAmC,GAAG;AAAA,IACrE;AAAA,EAAA;AAEJ;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/ai-sandbox-cloudflare",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cloudflare sandbox provider for TanStack AI — run harness adapters inside Cloudflare Containers (edge) through the uniform SandboxHandle.",
|
|
5
|
+
"author": "",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TanStack/ai.git",
|
|
10
|
+
"directory": "packages/ai-sandbox-cloudflare"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"tanstack",
|
|
15
|
+
"sandbox",
|
|
16
|
+
"cloudflare",
|
|
17
|
+
"workers",
|
|
18
|
+
"edge",
|
|
19
|
+
"harness",
|
|
20
|
+
"agent"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"module": "./dist/esm/index.js",
|
|
24
|
+
"types": "./dist/esm/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/esm/index.d.ts",
|
|
28
|
+
"import": "./dist/esm/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./agent": {
|
|
31
|
+
"types": "./dist/esm/agent.d.ts",
|
|
32
|
+
"import": "./dist/esm/agent.js"
|
|
33
|
+
},
|
|
34
|
+
"./runner": {
|
|
35
|
+
"types": "./dist/esm/runner.d.ts",
|
|
36
|
+
"import": "./dist/esm/runner.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"src"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "vite build",
|
|
45
|
+
"clean": "premove ./build ./dist",
|
|
46
|
+
"lint:fix": "eslint ./src --fix",
|
|
47
|
+
"test:build": "publint --strict",
|
|
48
|
+
"test:eslint": "eslint ./src",
|
|
49
|
+
"test:lib": "vitest",
|
|
50
|
+
"test:lib:dev": "pnpm test:lib --watch",
|
|
51
|
+
"test:types": "tsc"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@cloudflare/sandbox": "^0.12.1"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@tanstack/ai": "workspace:^",
|
|
58
|
+
"@tanstack/ai-sandbox": "workspace:^",
|
|
59
|
+
"@tanstack/ai-sandbox-local-process": "workspace:^",
|
|
60
|
+
"zod": "^4.0.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@cloudflare/workers-types": "^4.20260317.1",
|
|
64
|
+
"@tanstack/ai": "workspace:*",
|
|
65
|
+
"@tanstack/ai-sandbox": "workspace:*",
|
|
66
|
+
"@tanstack/ai-sandbox-local-process": "workspace:*",
|
|
67
|
+
"@types/node": "^24.10.1",
|
|
68
|
+
"@vitest/coverage-v8": "4.0.14",
|
|
69
|
+
"zod": "^4.2.0"
|
|
70
|
+
},
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public"
|
|
73
|
+
}
|
|
74
|
+
}
|