@tangle-network/agent-provider-cli-bridge 2.0.2 → 2.0.4

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.
@@ -0,0 +1,9 @@
1
+ import type { HarnessType } from "@tangle-network/agent-interface";
2
+ /**
3
+ * The box image as one Dockerfile.
4
+ *
5
+ * Every cloud builds from this text: E2B and Daytona build their template and
6
+ * image from it, and a local Docker build of it is the same box. Harness
7
+ * versions come from the registry that owns the pins.
8
+ */
9
+ export declare function renderBoxDockerfile(harnesses?: readonly HarnessType[]): string;
@@ -0,0 +1,31 @@
1
+ import { BOX_HARNESSES, BOX_HOME, BOX_NODE_IMAGE, BOX_PINS, BOX_WORKSPACE_DIR, boxHarnessVersion } from "./box.js";
2
+ /**
3
+ * The box image as one Dockerfile.
4
+ *
5
+ * Every cloud builds from this text: E2B and Daytona build their template and
6
+ * image from it, and a local Docker build of it is the same box. Harness
7
+ * versions come from the registry that owns the pins.
8
+ */
9
+ export function renderBoxDockerfile(harnesses = Object.keys(BOX_HARNESSES)) {
10
+ const installs = harnesses.map((name) => {
11
+ const entry = BOX_HARNESSES[name];
12
+ if (entry === undefined)
13
+ throw new Error(`harness ${name} is not in the box harness table`);
14
+ return { entry, version: boxHarnessVersion(name) };
15
+ });
16
+ const bridge = `https://github.com/${BOX_PINS.cliBridgeRepository}/archive/${BOX_PINS.cliBridgeCommit}.tar.gz`;
17
+ const packages = installs.map(({ entry, version }) => `${entry.npmPackage}@${version}`);
18
+ const probes = installs.map(({ entry, version }) => `test "$(${entry.bin} --version | cut -d' ' -f1)" = "${version}"`);
19
+ return [
20
+ `FROM ${BOX_NODE_IMAGE}`,
21
+ "RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates python3 make g++ ripgrep procps \\",
22
+ " && rm -rf /var/lib/apt/lists/*",
23
+ `RUN npm install -g ${[...packages, bridge].join(" ")} \\`,
24
+ ` && ${[...probes, "command -v cli-bridge >/dev/null"].join(" \\\n && ")}`,
25
+ `RUN mkdir -p ${BOX_WORKSPACE_DIR} && chown -R node:node ${BOX_HOME}`,
26
+ "USER node",
27
+ `ENV HOME=${BOX_HOME}`,
28
+ `WORKDIR ${BOX_WORKSPACE_DIR}`,
29
+ "",
30
+ ].join("\n");
31
+ }
@@ -0,0 +1,6 @@
1
+ export { createBridgeSandboxProvider, BRIDGE_RUN_TAG } from "./provider.js";
2
+ export type { BridgeSandboxProviderOptions } from "./provider.js";
3
+ export type { BridgeSandbox, BridgeSandboxCreateRequest, BridgeSandboxSummary, BridgeSandboxVendor, BridgeVendorSupport, } from "./vendor.js";
4
+ export { validateBoxProfile } from "./refusal.js";
5
+ export { BOX_HARNESSES, BOX_HOME, BOX_PINS, BOX_WORKSPACE_DIR, BRIDGE_PORT, boxHarnessVersion, } from "./box.js";
6
+ export { renderBoxDockerfile } from "./image.js";
@@ -0,0 +1,4 @@
1
+ export { createBridgeSandboxProvider, BRIDGE_RUN_TAG } from "./provider.js";
2
+ export { validateBoxProfile } from "./refusal.js";
3
+ export { BOX_HARNESSES, BOX_HOME, BOX_PINS, BOX_WORKSPACE_DIR, BRIDGE_PORT, boxHarnessVersion, } from "./box.js";
4
+ export { renderBoxDockerfile } from "./image.js";
@@ -0,0 +1,23 @@
1
+ import { type HarnessType } from "@tangle-network/agent-interface";
2
+ import { type AgentEnvironmentProvider } from "@tangle-network/agent-interface/environment-provider";
3
+ import type { BridgeSandboxVendor } from "./vendor.js";
4
+ export declare const BRIDGE_RUN_TAG = "tangle-bridge-run";
5
+ export interface BridgeSandboxProviderOptions {
6
+ vendor: BridgeSandboxVendor;
7
+ /** The Router that serves the model, with the per-run capped key. The key reaches the Bridge process only. */
8
+ router: {
9
+ key: string | (() => string | Promise<string>);
10
+ baseUrl?: string;
11
+ };
12
+ /** Harnesses this provider accepts. Defaults to every harness in the box table. */
13
+ harnesses?: readonly HarnessType[];
14
+ /** Finite sandbox lifetime the vendor enforces. Must exceed the longest turn plus capture. */
15
+ lifetimeMs?: number;
16
+ readyTimeoutMs?: number;
17
+ }
18
+ /**
19
+ * An {@link AgentEnvironmentProvider} for a vendor cloud: each environment is a
20
+ * vendor sandbox created from the box image, running CLI Bridge, and every turn
21
+ * is delegated to {@link createCliBridgeProvider} at the sandbox's exposed URL.
22
+ */
23
+ export declare function createBridgeSandboxProvider(options: BridgeSandboxProviderOptions): AgentEnvironmentProvider;
@@ -0,0 +1,473 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { posix } from "node:path";
3
+ import { Agent, fetch as undiciFetch } from "undici";
4
+ import { canonicalAgentProfileDigest, TANGLE_ROUTER_DEFAULT_ROOT_URL, } from "@tangle-network/agent-interface";
5
+ import { AgentEnvironmentGetOptionsSchema, AgentTurnInputSchema, EnvironmentProviderError, } from "@tangle-network/agent-interface/environment-provider";
6
+ import { harnessModelSupportFor } from "@tangle-network/cli-agent-registry";
7
+ import { createCliBridgeProvider, defaultCliBridgeCapabilities } from "../index.js";
8
+ import { plannerLowers } from "./refusal.js";
9
+ import { BOX_WORKSPACE_DIR, BRIDGE_PORT, BRIDGE_START_COMMAND, bridgeProcessEnv, boxHarness, boxHarnessVersion, } from "./box.js";
10
+ import { assertProfileValid, declarableHarnesses, refuseUnsupportedCreateInput, resolveBoxHarness, validateBoxProfile } from "./refusal.js";
11
+ export const BRIDGE_RUN_TAG = "tangle-bridge-run";
12
+ const DEFAULT_LIFETIME_MS = 2 * 60 * 60 * 1000;
13
+ const DEFAULT_READY_TIMEOUT_MS = 120_000;
14
+ const UNSUPPORTED_TURN_FIELDS = [
15
+ "contextTransfer",
16
+ "nativeContinuation",
17
+ "nativeResume",
18
+ "interactions",
19
+ "controlRef",
20
+ "lastEventId",
21
+ ];
22
+ /**
23
+ * An {@link AgentEnvironmentProvider} for a vendor cloud: each environment is a
24
+ * vendor sandbox created from the box image, running CLI Bridge, and every turn
25
+ * is delegated to {@link createCliBridgeProvider} at the sandbox's exposed URL.
26
+ */
27
+ export function createBridgeSandboxProvider(options) {
28
+ const { vendor } = options;
29
+ const declared = options.harnesses ?? declarableHarnesses();
30
+ const supported = new Set(declarableHarnesses());
31
+ for (const harness of declared) {
32
+ if (!supported.has(harness))
33
+ throw new Error(`harness ${harness} is not in the box harness table`);
34
+ }
35
+ const routerBaseUrl = (options.router.baseUrl ?? TANGLE_ROUTER_DEFAULT_ROOT_URL).replace(/\/+$/, "");
36
+ const lifetimeMs = options.lifetimeMs ?? DEFAULT_LIFETIME_MS;
37
+ const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
38
+ const providerCapabilities = (harness) => environmentCapabilities(defaultCliBridgeCapabilities(harness), vendor, harness === undefined ? false : plannerLowers(harness, "hooks"));
39
+ async function create(input) {
40
+ input.signal?.throwIfAborted();
41
+ refuseUnsupportedCreateInput(input, vendor.support);
42
+ const profile = input.profile;
43
+ const validation = validateBoxProfile(profile, input.backend, declared);
44
+ assertProfileValid(validation);
45
+ const harness = resolveBoxHarness(profile, input.backend, declared);
46
+ const box = boxHarness(harness);
47
+ const runTag = randomUUID();
48
+ const bearer = randomBytes(32).toString("hex");
49
+ const routerKey = typeof options.router.key === "function" ? await options.router.key() : options.router.key;
50
+ const secrets = [bearer, routerKey];
51
+ const workspaceDir = repositoryCwd(input);
52
+ let sandbox;
53
+ let dispatcher;
54
+ try {
55
+ sandbox = await vendor.create({
56
+ ...(input.name === undefined ? {} : { name: input.name }),
57
+ metadata: { ...stringMetadata(input.metadata), [BRIDGE_RUN_TAG]: runTag },
58
+ env: input.env ?? {},
59
+ lifetimeMs,
60
+ ...(input.workspace?.environment === undefined ? {} : { environment: input.workspace.environment }),
61
+ ...(input.workspace?.image === undefined ? {} : { image: input.workspace.image }),
62
+ ...(input.resources === undefined ? {} : { resources: input.resources }),
63
+ ...(input.egress === undefined ? {} : { egress: egressFor(input, routerBaseUrl) }),
64
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
65
+ });
66
+ input.signal?.throwIfAborted();
67
+ await sandbox.spawn(BRIDGE_START_COMMAND, {
68
+ cwd: BOX_WORKSPACE_DIR,
69
+ env: bridgeProcessEnv(box, bearer, { baseUrl: routerBaseUrl, key: routerKey }),
70
+ });
71
+ const endpoint = await sandbox.endpoint(BRIDGE_PORT, signalOption(input.signal));
72
+ dispatcher = new Agent({ headersTimeout: 0, bodyTimeout: 0 });
73
+ const bridgeFetch = vendorFetch(endpoint.headers ?? {}, dispatcher);
74
+ await waitForBridge(endpoint.url, bearer, bridgeFetch, box.bridgeBackend, readyTimeoutMs, input.signal, secrets);
75
+ await assertHarnessVersion(sandbox, harness, input.signal);
76
+ await cloneRepository(sandbox, input, secrets);
77
+ await ensureDirectory(sandbox, workspaceDir);
78
+ const bridge = createCliBridgeProvider({
79
+ baseUrl: endpoint.url,
80
+ bearerToken: bearer,
81
+ fetch: bridgeFetch,
82
+ name: vendor.name,
83
+ });
84
+ const { workspace: _workspace, egress: _egress, resources: _resources, ...forwarded } = input;
85
+ const inner = await bridge.create({
86
+ ...forwarded,
87
+ backend: harness,
88
+ requestedId: sandbox.id,
89
+ workspace: { cwd: { base: "host", path: workspaceDir } },
90
+ });
91
+ input.signal?.throwIfAborted();
92
+ return wrapEnvironment({ inner, sandbox, vendor, harness, profile, workspaceDir, dispatcher, capabilities: environmentCapabilities(inner.capabilities ?? providerCapabilities(harness), vendor, plannerLowers(harness, "hooks")), input });
93
+ }
94
+ catch (error) {
95
+ await dispatcher?.close().catch(() => undefined);
96
+ throw await failAfterProvision(error, sandbox, vendor.name, secrets);
97
+ }
98
+ }
99
+ return {
100
+ name: vendor.name,
101
+ capabilities: () => providerCapabilities(declared.length === 1 ? declared[0] : undefined),
102
+ validateProfile(ref) {
103
+ if (typeof ref === "string") {
104
+ return { ok: false, issues: [{ level: "error", code: "named_profile_refused", path: "profile", message: `named profile ${ref} is unsupported; pass an inline AgentProfile` }] };
105
+ }
106
+ return validateBoxProfile(ref, undefined, declared);
107
+ },
108
+ create,
109
+ async get(id, getOptions) {
110
+ assertGetOptionsSupported(getOptions);
111
+ AgentEnvironmentGetOptionsSchema.parse(getOptions ?? {});
112
+ getOptions?.signal?.throwIfAborted();
113
+ const sandbox = await vendor.connect(id, signalOption(getOptions?.signal));
114
+ if (sandbox === null)
115
+ return null;
116
+ if (sandbox.id !== id) {
117
+ throw new EnvironmentProviderError("transport", `vendor returned sandbox ${sandbox.id} for ${id}`, { provider: vendor.name });
118
+ }
119
+ return reconstructedEnvironment(sandbox, vendor, providerCapabilities());
120
+ },
121
+ async list(query, listOptions) {
122
+ const summaries = await vendor.list({
123
+ ...(query?.name === undefined ? {} : { name: query.name }),
124
+ ...(query?.metadata === undefined ? {} : { metadata: stringMetadata(query.metadata) }),
125
+ }, signalOption(listOptions?.signal));
126
+ return summaries.map((summary) => ({
127
+ id: summary.id,
128
+ provider: vendor.name,
129
+ status: summary.status,
130
+ ...(summary.name === undefined ? {} : { name: summary.name }),
131
+ metadata: summary.metadata,
132
+ }));
133
+ },
134
+ };
135
+ }
136
+ function signalOption(signal) {
137
+ return signal === undefined ? {} : { signal };
138
+ }
139
+ function stringMetadata(metadata) {
140
+ const out = {};
141
+ for (const [key, value] of Object.entries(metadata ?? {})) {
142
+ if (typeof value !== "string") {
143
+ throw new EnvironmentProviderError("refused", `vendor tags hold string values; ${key} is not a string`, { path: `metadata.${key}` });
144
+ }
145
+ out[key] = value;
146
+ }
147
+ return out;
148
+ }
149
+ function repositoryCwd(input) {
150
+ const cwd = input.workspace?.cwd;
151
+ return cwd === undefined || cwd.path === "." ? BOX_WORKSPACE_DIR : posix.join(BOX_WORKSPACE_DIR, cwd.path);
152
+ }
153
+ function egressFor(input, routerBaseUrl) {
154
+ const policy = input.egress;
155
+ const routerHost = new URL(routerBaseUrl).host;
156
+ const allowed = policy.mode === "strict"
157
+ ? (policy.allowDomains ?? []).map((entry) => (typeof entry === "string" ? entry : entry.domain))
158
+ : [];
159
+ return { mode: policy.mode, allowDomains: policy.mode === "strict" ? [...new Set([...allowed, routerHost])] : allowed };
160
+ }
161
+ /** Requests to the Bridge carry the vendor gateway's headers and no timeouts, because a turn streams for minutes. */
162
+ function vendorFetch(headers, dispatcher) {
163
+ return ((input, init) => undiciFetch(input, {
164
+ ...init,
165
+ headers: { ...headers, ...init?.headers },
166
+ dispatcher,
167
+ }));
168
+ }
169
+ async function waitForBridge(baseUrl, bearer, bridgeFetch, backend, timeoutMs, signal, secrets) {
170
+ const deadline = Date.now() + timeoutMs;
171
+ let last = "no response";
172
+ while (Date.now() < deadline) {
173
+ signal?.throwIfAborted();
174
+ try {
175
+ const response = await bridgeFetch(`${baseUrl.replace(/\/+$/, "")}/health`, {
176
+ headers: { authorization: `Bearer ${bearer}` },
177
+ signal: AbortSignal.timeout(5_000),
178
+ });
179
+ if (response.ok) {
180
+ const health = (await response.json());
181
+ const state = health.backends?.find((entry) => entry.name !== undefined && (entry.name === backend || entry.name.startsWith(backend)))?.state;
182
+ if (state === "ready")
183
+ return;
184
+ if (state !== undefined && state !== "probing") {
185
+ throw new EnvironmentProviderError("materialization_failed", `cli-bridge reports the ${backend} backend ${state}`, { secrets });
186
+ }
187
+ last = `backend ${state ?? "absent"}`;
188
+ }
189
+ else {
190
+ last = `health status ${response.status}`;
191
+ if (response.status === 401 || response.status === 403) {
192
+ throw new EnvironmentProviderError("auth_failed", `cli-bridge health answered ${response.status}`, { status: response.status, secrets });
193
+ }
194
+ }
195
+ }
196
+ catch (error) {
197
+ if (error instanceof EnvironmentProviderError || signal?.aborted)
198
+ throw error;
199
+ last = error instanceof Error ? error.message : String(error);
200
+ }
201
+ await new Promise((resolve) => setTimeout(resolve, 500));
202
+ }
203
+ throw new EnvironmentProviderError("provision_failed", `cli-bridge was not ready within ${timeoutMs} ms (${last})`, { secrets });
204
+ }
205
+ async function assertHarnessVersion(sandbox, harness, signal) {
206
+ const box = boxHarness(harness);
207
+ const pinned = boxHarnessVersion(harness);
208
+ const result = await sandbox.exec(`${box.bin} --version`, { ...signalOption(signal), timeoutMs: 30_000 });
209
+ if (result.exitCode !== 0 || result.stdout.trim().split(/\s+/)[0] !== pinned) {
210
+ throw new EnvironmentProviderError("materialization_failed", `${box.bin} --version reported "${result.stdout.trim() || result.stderr.trim()}" (exit ${result.exitCode}); the registry pins ${pinned}`);
211
+ }
212
+ }
213
+ async function cloneRepository(sandbox, input, secrets) {
214
+ const repoUrl = input.workspace?.repoUrl;
215
+ if (repoUrl === undefined)
216
+ return;
217
+ const gitRef = input.workspace?.gitRef;
218
+ const quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
219
+ const command = [
220
+ `rm -rf ${quote(BOX_WORKSPACE_DIR)}/.git`,
221
+ `git clone ${quote(repoUrl)} ${quote(BOX_WORKSPACE_DIR)}.clone`,
222
+ ...(gitRef === undefined ? [] : [`git -C ${quote(BOX_WORKSPACE_DIR)}.clone checkout ${quote(gitRef)}`]),
223
+ `cp -a ${quote(BOX_WORKSPACE_DIR)}.clone/. ${quote(BOX_WORKSPACE_DIR)}/ && rm -rf ${quote(BOX_WORKSPACE_DIR)}.clone`,
224
+ ].join(" && ");
225
+ const result = await sandbox.exec(command, { timeoutMs: 600_000, ...signalOption(input.signal) });
226
+ if (result.exitCode !== 0) {
227
+ throw new EnvironmentProviderError("provision_failed", `cloning ${repoUrl} failed (exit ${result.exitCode}): ${result.stderr.trim().slice(0, 400)}`, { path: "workspace.repoUrl", secrets });
228
+ }
229
+ }
230
+ async function ensureDirectory(sandbox, directory) {
231
+ const result = await sandbox.exec(`mkdir -p '${directory.replace(/'/g, `'\\''`)}'`, { timeoutMs: 30_000 });
232
+ if (result.exitCode !== 0) {
233
+ throw new EnvironmentProviderError("provision_failed", `cannot create workspace directory ${directory}: ${result.stderr.trim().slice(0, 200)}`, { path: "workspace.cwd" });
234
+ }
235
+ }
236
+ /** Destroy the sandbox a failed create left behind; a destroy that fails names the sandbox as a cleanup handle. */
237
+ async function failAfterProvision(error, sandbox, provider, secrets) {
238
+ const original = error instanceof EnvironmentProviderError || isAbort(error) ? error : new EnvironmentProviderError("provision_failed", error instanceof Error ? error.message : String(error), { cause: error, provider, secrets });
239
+ if (sandbox === undefined)
240
+ return original;
241
+ try {
242
+ await sandbox.destroy();
243
+ return original;
244
+ }
245
+ catch (cleanup) {
246
+ return new EnvironmentProviderError("cleanup_failed", `create failed (${original instanceof Error ? original.message : String(original)}) and sandbox ${sandbox.id} could not be destroyed`, { cause: new AggregateError([original, cleanup]), provider, secrets, cleanupHandle: { environmentId: sandbox.id, provider } });
247
+ }
248
+ }
249
+ function isAbort(error) {
250
+ return error instanceof Error && error.name === "AbortError";
251
+ }
252
+ function assertGetOptionsSupported(options) {
253
+ if (options === undefined || options === null)
254
+ return;
255
+ for (const key of ["readOnly", "proxySecrets", "proxySecretParents", "expectedProxySecretGeneration", "requireInPlace", "contextTransfer"]) {
256
+ if (key in options) {
257
+ throw new EnvironmentProviderError("refused", `get does not honor ${key}`, { path: key });
258
+ }
259
+ }
260
+ }
261
+ /** The document for one environment: what the Bridge claims, plus the vendor sandbox's workspace and placement. */
262
+ function environmentCapabilities(bridge, vendor, hooks) {
263
+ const { retainedControl: _retained, nativeContinuation: _native, contextTransfer: _transfer, interactions: _interactions, observation: _observation, ...base } = bridge;
264
+ return {
265
+ ...base,
266
+ profile: { ...base.profile, validation: true, hooks: hooks === true },
267
+ workspace: { read: true, write: true, exec: true, git: false, upload: false, download: false, cwdBases: { repository: true, host: false } },
268
+ placement: true,
269
+ create: {
270
+ durableIdempotency: false,
271
+ ...(vendor.support.egress.length === 0 ? {} : { egress: [...vendor.support.egress] }),
272
+ },
273
+ };
274
+ }
275
+ function sandboxOperations(sandbox, vendor, workspaceDir) {
276
+ const resolve = (path) => (path.startsWith("/") ? path : posix.join(workspaceDir, path));
277
+ return {
278
+ read: (path, options) => sandbox.read(resolve(path), options),
279
+ write: (path, content, options) => sandbox.write(resolve(path), content, options),
280
+ exec: (command, options) => sandbox.exec(command, { ...options, cwd: options?.cwd === undefined ? workspaceDir : resolve(options.cwd) }),
281
+ placement: async () => ({
282
+ kind: "provider",
283
+ sandboxId: sandbox.id,
284
+ ...(sandbox.region === undefined ? {} : { region: sandbox.region }),
285
+ providerMetadata: { vendor: vendor.name },
286
+ }),
287
+ async status(options) {
288
+ return sandbox.status(options);
289
+ },
290
+ };
291
+ }
292
+ function wrapEnvironment(args) {
293
+ const { inner, sandbox, vendor, workspaceDir } = args;
294
+ const operations = sandboxOperations(sandbox, vendor, workspaceDir);
295
+ const createDigest = canonicalAgentProfileDigest(args.profile);
296
+ let busy = false;
297
+ let destroyPromise;
298
+ const guard = (turn) => {
299
+ const result = AgentTurnInputSchema.safeParse(turn);
300
+ if (!result.success) {
301
+ const issue = result.error.issues[0];
302
+ throw new EnvironmentProviderError("invalid_input", issue?.message ?? "invalid turn input", { path: `turn.${(issue?.path ?? []).join(".")}`.replace(/\.$/, ""), cause: result.error });
303
+ }
304
+ const parsed = result.data;
305
+ for (const field of UNSUPPORTED_TURN_FIELDS) {
306
+ if (parsed[field] !== undefined) {
307
+ throw new EnvironmentProviderError("refused", `this environment does not honor turn.${field}`, { path: `turn.${field}` });
308
+ }
309
+ }
310
+ const turnProfile = parsed.profile ?? parsed.providerOptions?.backend?.profile;
311
+ if (turnProfile !== undefined && canonicalAgentProfileDigest(turnProfile) !== createDigest) {
312
+ throw new EnvironmentProviderError("refused", "a turn profile that differs from the create profile is refused, never run under the old profile", { path: "turn.profile" });
313
+ }
314
+ if (parsed.model !== undefined) {
315
+ const support = harnessModelSupportFor(args.harness, parsed.model);
316
+ if (!support.supported)
317
+ throw new EnvironmentProviderError("refused", support.message, { path: "turn.model" });
318
+ }
319
+ return turn;
320
+ };
321
+ const exclusive = async function* (turn) {
322
+ const checked = guard(turn);
323
+ if (checked.detach === true) {
324
+ throw new EnvironmentProviderError("refused", "stream does not honor detach; use dispatch", { path: "turn.detach" });
325
+ }
326
+ if (busy)
327
+ throw new EnvironmentProviderError("conflict", "a turn is already running on this environment", { provider: vendor.name });
328
+ busy = true;
329
+ try {
330
+ yield* projectTurn(inner, checked, checked.model ?? args.profile.model?.default);
331
+ }
332
+ catch (error) {
333
+ // An aborted turn rejects with AbortError whatever the Bridge says about confirming the cancel; the Bridge's error stays as the cause.
334
+ if (checked.signal?.aborted === true && !isAbort(error)) {
335
+ throw new DOMException("turn aborted", { name: "AbortError", cause: error });
336
+ }
337
+ throw error;
338
+ }
339
+ finally {
340
+ busy = false;
341
+ }
342
+ };
343
+ return {
344
+ id: sandbox.id,
345
+ provider: vendor.name,
346
+ creation: "created",
347
+ ...(inner.name === undefined ? {} : { name: inner.name }),
348
+ capabilities: args.capabilities,
349
+ status: operations.status,
350
+ stream: exclusive,
351
+ ...(inner.session === undefined ? {} : { session: (id, options) => inner.session(id, options) }),
352
+ ...(inner.dispatch === undefined
353
+ ? {}
354
+ : {
355
+ async dispatch(turn) {
356
+ const checked = guard(turn);
357
+ if (busy)
358
+ throw new EnvironmentProviderError("conflict", "a turn is already running on this environment", { provider: vendor.name });
359
+ return inner.dispatch(checked);
360
+ },
361
+ }),
362
+ read: operations.read,
363
+ write: operations.write,
364
+ exec: operations.exec,
365
+ placement: operations.placement,
366
+ destroy(options) {
367
+ destroyPromise ??= (async () => {
368
+ try {
369
+ await inner.destroy?.(options).catch(() => undefined);
370
+ }
371
+ finally {
372
+ await sandbox.destroy(options);
373
+ await args.dispatcher.close().catch(() => undefined);
374
+ }
375
+ })().catch((error) => {
376
+ destroyPromise = undefined;
377
+ throw error;
378
+ });
379
+ return destroyPromise;
380
+ },
381
+ };
382
+ }
383
+ const isTerminalResult = (event) => event.type === "result";
384
+ /** The Bridge's own dollar figure is the harness's catalog estimate, never a receipt, so no event carries it. */
385
+ function withoutHarnessCost(event) {
386
+ if (event.usage?.cost === undefined)
387
+ return event;
388
+ const { cost: _cost, ...usage } = event.usage;
389
+ return { ...event, usage };
390
+ }
391
+ /** The model the harness reported serving, read from the Bridge's terminal frame. */
392
+ function reportedModel(event) {
393
+ const models = event.data.provider_models;
394
+ const reported = event.data.provider_model ?? (Array.isArray(models) ? models[0] : undefined);
395
+ return typeof reported === "string" ? reported : undefined;
396
+ }
397
+ /**
398
+ * One turn as the contract states it: an `execution.started` event naming the
399
+ * declared model, the Bridge's events without harness dollar estimates, a
400
+ * terminal `result` that names the model the harness reported serving, and
401
+ * `turn.timeoutMs` ending the run (the Bridge cancels it, and a failed terminal
402
+ * event names the timeout and carries the usage observed so far).
403
+ */
404
+ async function* projectTurn(inner, turn, declaredModel) {
405
+ const { timeoutMs, ...rest } = turn;
406
+ yield {
407
+ type: "execution.started",
408
+ data: { ...(rest.sessionId === undefined ? {} : { sessionId: rest.sessionId }), effectiveBackend: { ...(declaredModel === undefined ? {} : { model: declaredModel }), source: "profile" } },
409
+ };
410
+ const timeout = new AbortController();
411
+ let timedOut = false;
412
+ const timer = timeoutMs === undefined ? undefined : setTimeout(() => {
413
+ timedOut = true;
414
+ timeout.abort(new DOMException(`turn exceeded ${timeoutMs} ms`, "TimeoutError"));
415
+ }, timeoutMs);
416
+ const signal = rest.signal === undefined ? timeout.signal : AbortSignal.any([rest.signal, timeout.signal]);
417
+ let lastUsage;
418
+ try {
419
+ for await (const raw of inner.stream({ ...rest, signal })) {
420
+ const event = withoutHarnessCost(raw);
421
+ if (event.usage !== undefined)
422
+ lastUsage = event;
423
+ if (!isTerminalResult(event)) {
424
+ yield event;
425
+ continue;
426
+ }
427
+ const served = reportedModel(event);
428
+ yield served === undefined ? event : { ...event, data: { ...event.data, effectiveBackend: { model: served, source: "harness" } } };
429
+ }
430
+ }
431
+ catch (error) {
432
+ if (!timedOut)
433
+ throw error;
434
+ yield {
435
+ type: "result",
436
+ data: { status: "failed", error: `turn timed out after ${timeoutMs} ms`, errorCode: "timeout", finalText: "" },
437
+ ...(lastUsage?.usage === undefined ? {} : { usage: lastUsage.usage, usageMode: lastUsage.usageMode ?? "cumulative" }),
438
+ };
439
+ }
440
+ finally {
441
+ if (timer !== undefined)
442
+ clearTimeout(timer);
443
+ }
444
+ }
445
+ /** An environment rebuilt by `get`: it reaches the sandbox but starts no turn. */
446
+ function reconstructedEnvironment(sandbox, vendor, providerCapabilities) {
447
+ const operations = sandboxOperations(sandbox, vendor, BOX_WORKSPACE_DIR);
448
+ const refuse = () => {
449
+ throw new EnvironmentProviderError("refused", "a reconstructed environment starts no turn; create one", { path: "turn", provider: vendor.name });
450
+ };
451
+ let destroyPromise;
452
+ return {
453
+ id: sandbox.id,
454
+ provider: vendor.name,
455
+ capabilities: providerCapabilities,
456
+ status: operations.status,
457
+ // eslint-disable-next-line require-yield
458
+ stream: async function* () {
459
+ refuse();
460
+ },
461
+ read: operations.read,
462
+ write: operations.write,
463
+ exec: operations.exec,
464
+ placement: operations.placement,
465
+ destroy(options) {
466
+ destroyPromise ??= sandbox.destroy(options).catch((error) => {
467
+ destroyPromise = undefined;
468
+ throw error;
469
+ });
470
+ return destroyPromise;
471
+ },
472
+ };
473
+ }
@@ -0,0 +1,23 @@
1
+ import { type AgentProfile, type AgentProfileValidationResult, type HarnessType } from "@tangle-network/agent-interface";
2
+ import type { CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
3
+ import type { BridgeVendorSupport } from "./vendor.js";
4
+ /** The harness a request selects, or the refusal that names why none can be. */
5
+ export declare function resolveBoxHarness(profile: AgentProfile, backend: string | undefined, declared: readonly HarnessType[]): HarnessType;
6
+ /**
7
+ * Validate a profile against what the box can run, without any sandbox call.
8
+ *
9
+ * It plans with the same materializer the Bridge in the box uses, so a
10
+ * dimension the planner cannot lower is refused here with its own reason.
11
+ */
12
+ export declare function validateBoxProfile(profile: AgentProfile, backend: string | undefined, declared: readonly HarnessType[]): AgentProfileValidationResult;
13
+ /** Throw the first refusal of a validation result as a typed error that names its path. */
14
+ export declare function assertProfileValid(result: AgentProfileValidationResult): void;
15
+ /**
16
+ * Refuse every create-input field this provider cannot honor, before any
17
+ * vendor call. A field is never accepted and dropped.
18
+ */
19
+ export declare function refuseUnsupportedCreateInput(input: CreateAgentEnvironmentInput, support: BridgeVendorSupport): void;
20
+ /** The harnesses this module can declare. */
21
+ export declare function declarableHarnesses(): HarnessType[];
22
+ /** Whether the shared planner lowers this profile dimension for the harness. */
23
+ export declare function plannerLowers(harness: HarnessType, dimension: "hooks"): boolean;