@tangle-network/agent-provider-cli-bridge 2.0.3 → 2.0.5
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/README.md +18 -0
- package/dist/sandbox/box.d.ts +62 -0
- package/dist/sandbox/box.js +70 -0
- package/dist/sandbox/image.d.ts +9 -0
- package/dist/sandbox/image.js +31 -0
- package/dist/sandbox/index.d.ts +6 -0
- package/dist/sandbox/index.js +4 -0
- package/dist/sandbox/provider.d.ts +23 -0
- package/dist/sandbox/provider.js +472 -0
- package/dist/sandbox/refusal.d.ts +23 -0
- package/dist/sandbox/refusal.js +159 -0
- package/dist/sandbox/vendor.d.ts +105 -0
- package/dist/sandbox/vendor.js +1 -0
- package/package.json +23 -4
package/README.md
CHANGED
|
@@ -195,3 +195,21 @@ Those five surfaces are never claimed in the capability document and always carr
|
|
|
195
195
|
The usage of one run is summed across its events and attributed to that run's execution id, which the observation carries as the provenance source.
|
|
196
196
|
An environment rebuilt with `provider.get(id)` has measured nothing yet, so it reports the usage surface as unavailable until a turn completes through it.
|
|
197
197
|
The endpoint holds only a scheme, a host, and an explicit port; userinfo, path, and query are dropped, and the bearer beside the base URL is never read.
|
|
198
|
+
|
|
199
|
+
## Cloud sandboxes (`./sandbox`)
|
|
200
|
+
|
|
201
|
+
`@tangle-network/agent-provider-cli-bridge/sandbox` turns a vendor sandbox that runs CLI Bridge into an `AgentEnvironmentProvider`.
|
|
202
|
+
The vendor adapter implements `BridgeSandboxVendor`: `create`, `connect`, `list`, and per sandbox `exec`, `spawn`, `read`, `write`, `endpoint`, `status` and `destroy`.
|
|
203
|
+
`createBridgeSandboxProvider({ vendor, router: { key } })` does the rest:
|
|
204
|
+
|
|
205
|
+
- It refuses every create field the vendor cannot honor with an `EnvironmentProviderError` that names the field, before any vendor call, and validates the profile with the materializer the Bridge in the box uses.
|
|
206
|
+
- It creates the sandbox from the box image with a finite lifetime and a run tag, starts the Bridge with a random bearer and the per-run Router key on that process only, waits for the selected backend to report ready, checks the harness CLI against the registry's pin, clones `workspace.repoUrl`, and creates the environment through `createCliBridgeProvider` at the exposed URL.
|
|
207
|
+
- Any failure after the vendor create destroys the sandbox before the error surfaces; a destroy that fails carries the sandbox id as `cleanupHandle`.
|
|
208
|
+
- Turns run through the Bridge. `turn.timeoutMs` cancels the run and ends the stream with a failed `result` that names the timeout, an abort rejects with `AbortError`, and a second turn on a busy environment is refused with `conflict`.
|
|
209
|
+
- Events carry `execution.started` with the declared model and a terminal `result` with the model the harness reported serving. The Bridge's dollar figure is the harness's catalog estimate, so events omit `usage.cost`.
|
|
210
|
+
|
|
211
|
+
The box image is `renderBoxDockerfile()`: Node, CLI Bridge at the commit in `BOX_PINS`, and each harness CLI at the version `@tangle-network/cli-agent-registry` pins.
|
|
212
|
+
A harness joins `BOX_HARNESSES` only after a live turn through the Router completed in a box; `claude-code` is the proven row.
|
|
213
|
+
Build the image for a cloud with `pnpm build:sandbox-image --target e2b|daytona|docker` from the repository root.
|
|
214
|
+
`@tangle-network/agent-provider-testkit` runs the live profile conformance suite against the result.
|
|
215
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { HarnessType } from "@tangle-network/agent-interface";
|
|
2
|
+
/**
|
|
3
|
+
* The box: one image every cloud runs, holding Node, CLI Bridge and the pinned
|
|
4
|
+
* harness CLIs. The Bridge listens on {@link BRIDGE_PORT}, materializes the
|
|
5
|
+
* exact profile and starts the selected harness exactly as it does for Braid
|
|
6
|
+
* on a local machine.
|
|
7
|
+
*/
|
|
8
|
+
export declare const BRIDGE_PORT = 3344;
|
|
9
|
+
/** The box user's home. */
|
|
10
|
+
export declare const BOX_HOME = "/home/node";
|
|
11
|
+
/** The default workspace directory; `workspace.cwd` with `base: "repository"` resolves under it. */
|
|
12
|
+
export declare const BOX_WORKSPACE_DIR = "/home/node/workspace";
|
|
13
|
+
export declare const BRIDGE_DATA_DIR = "/home/node/.cli-bridge";
|
|
14
|
+
export declare const BOX_NODE_IMAGE = "node:24.22.1-trixie-slim";
|
|
15
|
+
/**
|
|
16
|
+
* The CLI Bridge source revision the box installs, reviewed as a commit.
|
|
17
|
+
* `agentProfileMaterialize` is the exact version of the planner that revision
|
|
18
|
+
* pins; {@link createBridgeSandboxProvider} plans with the same version, and
|
|
19
|
+
* the image build refuses a revision whose pin differs.
|
|
20
|
+
*/
|
|
21
|
+
export declare const BOX_PINS: {
|
|
22
|
+
readonly cliBridgeRepository: "drewstone/cli-bridge";
|
|
23
|
+
readonly cliBridgeCommit: "263f700dc5ca8b7b6e53b986d376102e0d82b42f";
|
|
24
|
+
readonly agentProfileMaterialize: "1.1.4";
|
|
25
|
+
};
|
|
26
|
+
/** The E2B template and Daytona snapshot name `pnpm build:sandbox-image` publishes for these pins. */
|
|
27
|
+
export declare const BOX_TEMPLATE_NAME: string;
|
|
28
|
+
export interface BoxRouter {
|
|
29
|
+
/** Router root URL, without a path. */
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
/** The per-run capped Router key. */
|
|
32
|
+
key: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A harness the box carries and routes through the Tangle Router.
|
|
36
|
+
*
|
|
37
|
+
* A harness joins this table only after a live turn through the Router
|
|
38
|
+
* completed in a box, so the declared set is the proven set.
|
|
39
|
+
*/
|
|
40
|
+
export interface BoxHarness {
|
|
41
|
+
readonly harness: HarnessType;
|
|
42
|
+
/** The Bridge backend name in `BRIDGE_BACKENDS`. */
|
|
43
|
+
readonly bridgeBackend: string;
|
|
44
|
+
/** The npm package that installs the CLI. */
|
|
45
|
+
readonly npmPackage: string;
|
|
46
|
+
/** The CLI binary; `<bin> --version` prints the pinned version first. */
|
|
47
|
+
readonly bin: string;
|
|
48
|
+
/** Environment that points this harness at the Router, set on the Bridge process only. */
|
|
49
|
+
routerEnv(router: BoxRouter): Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
export declare const BOX_HARNESSES: Readonly<Partial<Record<HarnessType, BoxHarness>>>;
|
|
52
|
+
export declare function boxHarness(harness: string | undefined): BoxHarness | undefined;
|
|
53
|
+
/** The CLI version the harness registry pins; the box image installs exactly this. */
|
|
54
|
+
export declare function boxHarnessVersion(harness: HarnessType): string;
|
|
55
|
+
/** The command that starts the Bridge; its settings come from the process environment. */
|
|
56
|
+
export declare const BRIDGE_START_COMMAND = "cli-bridge";
|
|
57
|
+
/**
|
|
58
|
+
* The Bridge process environment. `bearer` guards the exposed port; the Router
|
|
59
|
+
* variables reach the harness through the Bridge's child environment and
|
|
60
|
+
* appear in no command line or file.
|
|
61
|
+
*/
|
|
62
|
+
export declare function bridgeProcessEnv(harness: BoxHarness, bearer: string, router: BoxRouter): Record<string, string>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { NATIVE_SESSION_CONTRACTS } from "@tangle-network/cli-agent-registry/native-session-contracts";
|
|
2
|
+
/**
|
|
3
|
+
* The box: one image every cloud runs, holding Node, CLI Bridge and the pinned
|
|
4
|
+
* harness CLIs. The Bridge listens on {@link BRIDGE_PORT}, materializes the
|
|
5
|
+
* exact profile and starts the selected harness exactly as it does for Braid
|
|
6
|
+
* on a local machine.
|
|
7
|
+
*/
|
|
8
|
+
export const BRIDGE_PORT = 3344;
|
|
9
|
+
/** The box user's home. */
|
|
10
|
+
export const BOX_HOME = "/home/node";
|
|
11
|
+
/** The default workspace directory; `workspace.cwd` with `base: "repository"` resolves under it. */
|
|
12
|
+
export const BOX_WORKSPACE_DIR = `${BOX_HOME}/workspace`;
|
|
13
|
+
export const BRIDGE_DATA_DIR = `${BOX_HOME}/.cli-bridge`;
|
|
14
|
+
export const BOX_NODE_IMAGE = "node:24.22.1-trixie-slim";
|
|
15
|
+
/**
|
|
16
|
+
* The CLI Bridge source revision the box installs, reviewed as a commit.
|
|
17
|
+
* `agentProfileMaterialize` is the exact version of the planner that revision
|
|
18
|
+
* pins; {@link createBridgeSandboxProvider} plans with the same version, and
|
|
19
|
+
* the image build refuses a revision whose pin differs.
|
|
20
|
+
*/
|
|
21
|
+
export const BOX_PINS = {
|
|
22
|
+
cliBridgeRepository: "drewstone/cli-bridge",
|
|
23
|
+
cliBridgeCommit: "263f700dc5ca8b7b6e53b986d376102e0d82b42f",
|
|
24
|
+
agentProfileMaterialize: "1.1.4",
|
|
25
|
+
};
|
|
26
|
+
/** The E2B template and Daytona snapshot name `pnpm build:sandbox-image` publishes for these pins. */
|
|
27
|
+
export const BOX_TEMPLATE_NAME = `tangle-cli-bridge-box-${BOX_PINS.cliBridgeCommit.slice(0, 12)}`;
|
|
28
|
+
export const BOX_HARNESSES = {
|
|
29
|
+
"claude-code": {
|
|
30
|
+
harness: "claude-code",
|
|
31
|
+
bridgeBackend: "claude",
|
|
32
|
+
npmPackage: "@anthropic-ai/claude-code",
|
|
33
|
+
bin: "claude",
|
|
34
|
+
// The Router authenticates a Bearer token. ANTHROPIC_API_KEY sends x-api-key, which it rejects.
|
|
35
|
+
routerEnv: (router) => ({
|
|
36
|
+
ANTHROPIC_BASE_URL: router.baseUrl,
|
|
37
|
+
ANTHROPIC_AUTH_TOKEN: router.key,
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
export function boxHarness(harness) {
|
|
42
|
+
return harness === undefined ? undefined : BOX_HARNESSES[harness];
|
|
43
|
+
}
|
|
44
|
+
/** The CLI version the harness registry pins; the box image installs exactly this. */
|
|
45
|
+
export function boxHarnessVersion(harness) {
|
|
46
|
+
const contract = NATIVE_SESSION_CONTRACTS[harness];
|
|
47
|
+
if (contract?.cliVersion === undefined) {
|
|
48
|
+
throw new Error(`cli-agent-registry pins no CLI version for harness ${harness}`);
|
|
49
|
+
}
|
|
50
|
+
return contract.cliVersion;
|
|
51
|
+
}
|
|
52
|
+
/** The command that starts the Bridge; its settings come from the process environment. */
|
|
53
|
+
export const BRIDGE_START_COMMAND = "cli-bridge";
|
|
54
|
+
/**
|
|
55
|
+
* The Bridge process environment. `bearer` guards the exposed port; the Router
|
|
56
|
+
* variables reach the harness through the Bridge's child environment and
|
|
57
|
+
* appear in no command line or file.
|
|
58
|
+
*/
|
|
59
|
+
export function bridgeProcessEnv(harness, bearer, router) {
|
|
60
|
+
return {
|
|
61
|
+
BRIDGE_HOST: "0.0.0.0",
|
|
62
|
+
BRIDGE_PORT: String(BRIDGE_PORT),
|
|
63
|
+
BRIDGE_BEARER: bearer,
|
|
64
|
+
BRIDGE_BACKENDS: harness.bridgeBackend,
|
|
65
|
+
BRIDGE_DATA_DIR: BRIDGE_DATA_DIR,
|
|
66
|
+
BRIDGE_JAIL_MODE: "off",
|
|
67
|
+
HOME: BOX_HOME,
|
|
68
|
+
...harness.routerEnv(router),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -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_TEMPLATE_NAME, 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_TEMPLATE_NAME, 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,472 @@
|
|
|
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 { BOX_WORKSPACE_DIR, BRIDGE_PORT, BRIDGE_START_COMMAND, bridgeProcessEnv, boxHarness, boxHarnessVersion, } from "./box.js";
|
|
9
|
+
import { assertProfileValid, declarableHarnesses, plannerLowers, refuseUnsupportedCreateInput, resolveBoxHarness, validateBoxProfile } from "./refusal.js";
|
|
10
|
+
export const BRIDGE_RUN_TAG = "tangle-bridge-run";
|
|
11
|
+
const DEFAULT_LIFETIME_MS = 2 * 60 * 60 * 1000;
|
|
12
|
+
const DEFAULT_READY_TIMEOUT_MS = 120_000;
|
|
13
|
+
const UNSUPPORTED_TURN_FIELDS = [
|
|
14
|
+
"contextTransfer",
|
|
15
|
+
"nativeContinuation",
|
|
16
|
+
"nativeResume",
|
|
17
|
+
"interactions",
|
|
18
|
+
"controlRef",
|
|
19
|
+
"lastEventId",
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* An {@link AgentEnvironmentProvider} for a vendor cloud: each environment is a
|
|
23
|
+
* vendor sandbox created from the box image, running CLI Bridge, and every turn
|
|
24
|
+
* is delegated to {@link createCliBridgeProvider} at the sandbox's exposed URL.
|
|
25
|
+
*/
|
|
26
|
+
export function createBridgeSandboxProvider(options) {
|
|
27
|
+
const { vendor } = options;
|
|
28
|
+
const declared = options.harnesses ?? declarableHarnesses();
|
|
29
|
+
const supported = new Set(declarableHarnesses());
|
|
30
|
+
for (const harness of declared) {
|
|
31
|
+
if (!supported.has(harness))
|
|
32
|
+
throw new Error(`harness ${harness} is not in the box harness table`);
|
|
33
|
+
}
|
|
34
|
+
const routerBaseUrl = (options.router.baseUrl ?? TANGLE_ROUTER_DEFAULT_ROOT_URL).replace(/\/+$/, "");
|
|
35
|
+
const lifetimeMs = options.lifetimeMs ?? DEFAULT_LIFETIME_MS;
|
|
36
|
+
const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
37
|
+
const providerCapabilities = (harness) => environmentCapabilities(defaultCliBridgeCapabilities(harness), vendor, harness === undefined ? false : plannerLowers(harness, "hooks"));
|
|
38
|
+
async function create(input) {
|
|
39
|
+
input.signal?.throwIfAborted();
|
|
40
|
+
refuseUnsupportedCreateInput(input, vendor.support);
|
|
41
|
+
const profile = input.profile;
|
|
42
|
+
const validation = validateBoxProfile(profile, input.backend, declared);
|
|
43
|
+
assertProfileValid(validation);
|
|
44
|
+
const harness = resolveBoxHarness(profile, input.backend, declared);
|
|
45
|
+
const box = boxHarness(harness);
|
|
46
|
+
const runTag = randomUUID();
|
|
47
|
+
const bearer = randomBytes(32).toString("hex");
|
|
48
|
+
const routerKey = typeof options.router.key === "function" ? await options.router.key() : options.router.key;
|
|
49
|
+
const secrets = [bearer, routerKey];
|
|
50
|
+
const workspaceDir = repositoryCwd(input);
|
|
51
|
+
let sandbox;
|
|
52
|
+
let dispatcher;
|
|
53
|
+
try {
|
|
54
|
+
sandbox = await vendor.create({
|
|
55
|
+
...(input.name === undefined ? {} : { name: input.name }),
|
|
56
|
+
metadata: { ...stringMetadata(input.metadata), [BRIDGE_RUN_TAG]: runTag },
|
|
57
|
+
env: input.env ?? {},
|
|
58
|
+
lifetimeMs,
|
|
59
|
+
...(input.workspace?.environment === undefined ? {} : { environment: input.workspace.environment }),
|
|
60
|
+
...(input.workspace?.image === undefined ? {} : { image: input.workspace.image }),
|
|
61
|
+
...(input.resources === undefined ? {} : { resources: input.resources }),
|
|
62
|
+
...(input.egress === undefined ? {} : { egress: egressFor(input, routerBaseUrl) }),
|
|
63
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
64
|
+
});
|
|
65
|
+
input.signal?.throwIfAborted();
|
|
66
|
+
await sandbox.spawn(BRIDGE_START_COMMAND, {
|
|
67
|
+
cwd: BOX_WORKSPACE_DIR,
|
|
68
|
+
env: bridgeProcessEnv(box, bearer, { baseUrl: routerBaseUrl, key: routerKey }),
|
|
69
|
+
});
|
|
70
|
+
const endpoint = await sandbox.endpoint(BRIDGE_PORT, signalOption(input.signal));
|
|
71
|
+
dispatcher = new Agent({ headersTimeout: 0, bodyTimeout: 0 });
|
|
72
|
+
const bridgeFetch = vendorFetch(endpoint.headers ?? {}, dispatcher);
|
|
73
|
+
await waitForBridge(endpoint.url, bearer, bridgeFetch, box.bridgeBackend, readyTimeoutMs, input.signal, secrets);
|
|
74
|
+
await assertHarnessVersion(sandbox, harness, input.signal);
|
|
75
|
+
await cloneRepository(sandbox, input, secrets);
|
|
76
|
+
await ensureDirectory(sandbox, workspaceDir);
|
|
77
|
+
const bridge = createCliBridgeProvider({
|
|
78
|
+
baseUrl: endpoint.url,
|
|
79
|
+
bearerToken: bearer,
|
|
80
|
+
fetch: bridgeFetch,
|
|
81
|
+
name: vendor.name,
|
|
82
|
+
});
|
|
83
|
+
const { workspace: _workspace, egress: _egress, resources: _resources, ...forwarded } = input;
|
|
84
|
+
const inner = await bridge.create({
|
|
85
|
+
...forwarded,
|
|
86
|
+
backend: harness,
|
|
87
|
+
requestedId: sandbox.id,
|
|
88
|
+
workspace: { cwd: { base: "host", path: workspaceDir } },
|
|
89
|
+
});
|
|
90
|
+
input.signal?.throwIfAborted();
|
|
91
|
+
return wrapEnvironment({ inner, sandbox, vendor, harness, profile, workspaceDir, dispatcher, capabilities: environmentCapabilities(inner.capabilities ?? providerCapabilities(harness), vendor, plannerLowers(harness, "hooks")), input });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
await dispatcher?.close().catch(() => undefined);
|
|
95
|
+
throw await failAfterProvision(error, sandbox, vendor.name, secrets);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
name: vendor.name,
|
|
100
|
+
capabilities: () => providerCapabilities(declared.length === 1 ? declared[0] : undefined),
|
|
101
|
+
validateProfile(ref) {
|
|
102
|
+
if (typeof ref === "string") {
|
|
103
|
+
return { ok: false, issues: [{ level: "error", code: "named_profile_refused", path: "profile", message: `named profile ${ref} is unsupported; pass an inline AgentProfile` }] };
|
|
104
|
+
}
|
|
105
|
+
return validateBoxProfile(ref, undefined, declared);
|
|
106
|
+
},
|
|
107
|
+
create,
|
|
108
|
+
async get(id, getOptions) {
|
|
109
|
+
assertGetOptionsSupported(getOptions);
|
|
110
|
+
AgentEnvironmentGetOptionsSchema.parse(getOptions ?? {});
|
|
111
|
+
getOptions?.signal?.throwIfAborted();
|
|
112
|
+
const sandbox = await vendor.connect(id, signalOption(getOptions?.signal));
|
|
113
|
+
if (sandbox === null)
|
|
114
|
+
return null;
|
|
115
|
+
if (sandbox.id !== id) {
|
|
116
|
+
throw new EnvironmentProviderError("transport", `vendor returned sandbox ${sandbox.id} for ${id}`, { provider: vendor.name });
|
|
117
|
+
}
|
|
118
|
+
return reconstructedEnvironment(sandbox, vendor, providerCapabilities());
|
|
119
|
+
},
|
|
120
|
+
async list(query, listOptions) {
|
|
121
|
+
const summaries = await vendor.list({
|
|
122
|
+
...(query?.name === undefined ? {} : { name: query.name }),
|
|
123
|
+
...(query?.metadata === undefined ? {} : { metadata: stringMetadata(query.metadata) }),
|
|
124
|
+
}, signalOption(listOptions?.signal));
|
|
125
|
+
return summaries.map((summary) => ({
|
|
126
|
+
id: summary.id,
|
|
127
|
+
provider: vendor.name,
|
|
128
|
+
status: summary.status,
|
|
129
|
+
...(summary.name === undefined ? {} : { name: summary.name }),
|
|
130
|
+
metadata: summary.metadata,
|
|
131
|
+
}));
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function signalOption(signal) {
|
|
136
|
+
return signal === undefined ? {} : { signal };
|
|
137
|
+
}
|
|
138
|
+
function stringMetadata(metadata) {
|
|
139
|
+
const out = {};
|
|
140
|
+
for (const [key, value] of Object.entries(metadata ?? {})) {
|
|
141
|
+
if (typeof value !== "string") {
|
|
142
|
+
throw new EnvironmentProviderError("refused", `vendor tags hold string values; ${key} is not a string`, { path: `metadata.${key}` });
|
|
143
|
+
}
|
|
144
|
+
out[key] = value;
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function repositoryCwd(input) {
|
|
149
|
+
const cwd = input.workspace?.cwd;
|
|
150
|
+
return cwd === undefined || cwd.path === "." ? BOX_WORKSPACE_DIR : posix.join(BOX_WORKSPACE_DIR, cwd.path);
|
|
151
|
+
}
|
|
152
|
+
function egressFor(input, routerBaseUrl) {
|
|
153
|
+
const policy = input.egress;
|
|
154
|
+
const routerHost = new URL(routerBaseUrl).host;
|
|
155
|
+
const allowed = policy.mode === "strict"
|
|
156
|
+
? (policy.allowDomains ?? []).map((entry) => (typeof entry === "string" ? entry : entry.domain))
|
|
157
|
+
: [];
|
|
158
|
+
return { mode: policy.mode, allowDomains: policy.mode === "strict" ? [...new Set([...allowed, routerHost])] : allowed };
|
|
159
|
+
}
|
|
160
|
+
/** Requests to the Bridge carry the vendor gateway's headers and no timeouts, because a turn streams for minutes. */
|
|
161
|
+
function vendorFetch(headers, dispatcher) {
|
|
162
|
+
return ((input, init) => undiciFetch(input, {
|
|
163
|
+
...init,
|
|
164
|
+
headers: { ...headers, ...init?.headers },
|
|
165
|
+
dispatcher,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
async function waitForBridge(baseUrl, bearer, bridgeFetch, backend, timeoutMs, signal, secrets) {
|
|
169
|
+
const deadline = Date.now() + timeoutMs;
|
|
170
|
+
let last = "no response";
|
|
171
|
+
while (Date.now() < deadline) {
|
|
172
|
+
signal?.throwIfAborted();
|
|
173
|
+
try {
|
|
174
|
+
const response = await bridgeFetch(`${baseUrl.replace(/\/+$/, "")}/health`, {
|
|
175
|
+
headers: { authorization: `Bearer ${bearer}` },
|
|
176
|
+
signal: AbortSignal.timeout(5_000),
|
|
177
|
+
});
|
|
178
|
+
if (response.ok) {
|
|
179
|
+
const health = (await response.json());
|
|
180
|
+
const state = health.backends?.find((entry) => entry.name !== undefined && (entry.name === backend || entry.name.startsWith(backend)))?.state;
|
|
181
|
+
if (state === "ready")
|
|
182
|
+
return;
|
|
183
|
+
if (state !== undefined && state !== "probing") {
|
|
184
|
+
throw new EnvironmentProviderError("materialization_failed", `cli-bridge reports the ${backend} backend ${state}`, { secrets });
|
|
185
|
+
}
|
|
186
|
+
last = `backend ${state ?? "absent"}`;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
last = `health status ${response.status}`;
|
|
190
|
+
if (response.status === 401 || response.status === 403) {
|
|
191
|
+
throw new EnvironmentProviderError("auth_failed", `cli-bridge health answered ${response.status}`, { status: response.status, secrets });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (error instanceof EnvironmentProviderError || signal?.aborted)
|
|
197
|
+
throw error;
|
|
198
|
+
last = error instanceof Error ? error.message : String(error);
|
|
199
|
+
}
|
|
200
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
201
|
+
}
|
|
202
|
+
throw new EnvironmentProviderError("provision_failed", `cli-bridge was not ready within ${timeoutMs} ms (${last})`, { secrets });
|
|
203
|
+
}
|
|
204
|
+
async function assertHarnessVersion(sandbox, harness, signal) {
|
|
205
|
+
const box = boxHarness(harness);
|
|
206
|
+
const pinned = boxHarnessVersion(harness);
|
|
207
|
+
const result = await sandbox.exec(`${box.bin} --version`, { ...signalOption(signal), timeoutMs: 30_000 });
|
|
208
|
+
if (result.exitCode !== 0 || result.stdout.trim().split(/\s+/)[0] !== pinned) {
|
|
209
|
+
throw new EnvironmentProviderError("materialization_failed", `${box.bin} --version reported "${result.stdout.trim() || result.stderr.trim()}" (exit ${result.exitCode}); the registry pins ${pinned}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function cloneRepository(sandbox, input, secrets) {
|
|
213
|
+
const repoUrl = input.workspace?.repoUrl;
|
|
214
|
+
if (repoUrl === undefined)
|
|
215
|
+
return;
|
|
216
|
+
const gitRef = input.workspace?.gitRef;
|
|
217
|
+
const quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
218
|
+
const command = [
|
|
219
|
+
`rm -rf ${quote(BOX_WORKSPACE_DIR)}/.git`,
|
|
220
|
+
`git clone ${quote(repoUrl)} ${quote(BOX_WORKSPACE_DIR)}.clone`,
|
|
221
|
+
...(gitRef === undefined ? [] : [`git -C ${quote(BOX_WORKSPACE_DIR)}.clone checkout ${quote(gitRef)}`]),
|
|
222
|
+
`cp -a ${quote(BOX_WORKSPACE_DIR)}.clone/. ${quote(BOX_WORKSPACE_DIR)}/ && rm -rf ${quote(BOX_WORKSPACE_DIR)}.clone`,
|
|
223
|
+
].join(" && ");
|
|
224
|
+
const result = await sandbox.exec(command, { timeoutMs: 600_000, ...signalOption(input.signal) });
|
|
225
|
+
if (result.exitCode !== 0) {
|
|
226
|
+
throw new EnvironmentProviderError("provision_failed", `cloning ${repoUrl} failed (exit ${result.exitCode}): ${result.stderr.trim().slice(0, 400)}`, { path: "workspace.repoUrl", secrets });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function ensureDirectory(sandbox, directory) {
|
|
230
|
+
const result = await sandbox.exec(`mkdir -p '${directory.replace(/'/g, `'\\''`)}'`, { timeoutMs: 30_000 });
|
|
231
|
+
if (result.exitCode !== 0) {
|
|
232
|
+
throw new EnvironmentProviderError("provision_failed", `cannot create workspace directory ${directory}: ${result.stderr.trim().slice(0, 200)}`, { path: "workspace.cwd" });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Destroy the sandbox a failed create left behind; a destroy that fails names the sandbox as a cleanup handle. */
|
|
236
|
+
async function failAfterProvision(error, sandbox, provider, secrets) {
|
|
237
|
+
const original = error instanceof EnvironmentProviderError || isAbort(error) ? error : new EnvironmentProviderError("provision_failed", error instanceof Error ? error.message : String(error), { cause: error, provider, secrets });
|
|
238
|
+
if (sandbox === undefined)
|
|
239
|
+
return original;
|
|
240
|
+
try {
|
|
241
|
+
await sandbox.destroy();
|
|
242
|
+
return original;
|
|
243
|
+
}
|
|
244
|
+
catch (cleanup) {
|
|
245
|
+
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 } });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function isAbort(error) {
|
|
249
|
+
return error instanceof Error && error.name === "AbortError";
|
|
250
|
+
}
|
|
251
|
+
function assertGetOptionsSupported(options) {
|
|
252
|
+
if (options === undefined || options === null)
|
|
253
|
+
return;
|
|
254
|
+
for (const key of ["readOnly", "proxySecrets", "proxySecretParents", "expectedProxySecretGeneration", "requireInPlace", "contextTransfer"]) {
|
|
255
|
+
if (key in options) {
|
|
256
|
+
throw new EnvironmentProviderError("refused", `get does not honor ${key}`, { path: key });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** The document for one environment: what the Bridge claims, plus the vendor sandbox's workspace and placement. */
|
|
261
|
+
function environmentCapabilities(bridge, vendor, hooks) {
|
|
262
|
+
const { retainedControl: _retained, nativeContinuation: _native, contextTransfer: _transfer, interactions: _interactions, observation: _observation, ...base } = bridge;
|
|
263
|
+
return {
|
|
264
|
+
...base,
|
|
265
|
+
profile: { ...base.profile, validation: true, hooks: hooks === true },
|
|
266
|
+
workspace: { read: true, write: true, exec: true, git: false, upload: false, download: false, cwdBases: { repository: true, host: false } },
|
|
267
|
+
placement: true,
|
|
268
|
+
create: {
|
|
269
|
+
durableIdempotency: false,
|
|
270
|
+
...(vendor.support.egress.length === 0 ? {} : { egress: [...vendor.support.egress] }),
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function sandboxOperations(sandbox, vendor, workspaceDir) {
|
|
275
|
+
const resolve = (path) => (path.startsWith("/") ? path : posix.join(workspaceDir, path));
|
|
276
|
+
return {
|
|
277
|
+
read: (path, options) => sandbox.read(resolve(path), options),
|
|
278
|
+
write: (path, content, options) => sandbox.write(resolve(path), content, options),
|
|
279
|
+
exec: (command, options) => sandbox.exec(command, { ...options, cwd: options?.cwd === undefined ? workspaceDir : resolve(options.cwd) }),
|
|
280
|
+
placement: async () => ({
|
|
281
|
+
kind: "provider",
|
|
282
|
+
sandboxId: sandbox.id,
|
|
283
|
+
...(sandbox.region === undefined ? {} : { region: sandbox.region }),
|
|
284
|
+
providerMetadata: { vendor: vendor.name },
|
|
285
|
+
}),
|
|
286
|
+
async status(options) {
|
|
287
|
+
return sandbox.status(options);
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function wrapEnvironment(args) {
|
|
292
|
+
const { inner, sandbox, vendor, workspaceDir } = args;
|
|
293
|
+
const operations = sandboxOperations(sandbox, vendor, workspaceDir);
|
|
294
|
+
const createDigest = canonicalAgentProfileDigest(args.profile);
|
|
295
|
+
let busy = false;
|
|
296
|
+
let destroyPromise;
|
|
297
|
+
const guard = (turn) => {
|
|
298
|
+
const result = AgentTurnInputSchema.safeParse(turn);
|
|
299
|
+
if (!result.success) {
|
|
300
|
+
const issue = result.error.issues[0];
|
|
301
|
+
throw new EnvironmentProviderError("invalid_input", issue?.message ?? "invalid turn input", { path: `turn.${(issue?.path ?? []).join(".")}`.replace(/\.$/, ""), cause: result.error });
|
|
302
|
+
}
|
|
303
|
+
const parsed = result.data;
|
|
304
|
+
for (const field of UNSUPPORTED_TURN_FIELDS) {
|
|
305
|
+
if (parsed[field] !== undefined) {
|
|
306
|
+
throw new EnvironmentProviderError("refused", `this environment does not honor turn.${field}`, { path: `turn.${field}` });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const turnProfile = parsed.profile ?? parsed.providerOptions?.backend?.profile;
|
|
310
|
+
if (turnProfile !== undefined && canonicalAgentProfileDigest(turnProfile) !== createDigest) {
|
|
311
|
+
throw new EnvironmentProviderError("refused", "a turn profile that differs from the create profile is refused, never run under the old profile", { path: "turn.profile" });
|
|
312
|
+
}
|
|
313
|
+
if (parsed.model !== undefined) {
|
|
314
|
+
const support = harnessModelSupportFor(args.harness, parsed.model);
|
|
315
|
+
if (!support.supported)
|
|
316
|
+
throw new EnvironmentProviderError("refused", support.message, { path: "turn.model" });
|
|
317
|
+
}
|
|
318
|
+
return turn;
|
|
319
|
+
};
|
|
320
|
+
const exclusive = async function* (turn) {
|
|
321
|
+
const checked = guard(turn);
|
|
322
|
+
if (checked.detach === true) {
|
|
323
|
+
throw new EnvironmentProviderError("refused", "stream does not honor detach; use dispatch", { path: "turn.detach" });
|
|
324
|
+
}
|
|
325
|
+
if (busy)
|
|
326
|
+
throw new EnvironmentProviderError("conflict", "a turn is already running on this environment", { provider: vendor.name });
|
|
327
|
+
busy = true;
|
|
328
|
+
try {
|
|
329
|
+
yield* projectTurn(inner, checked, checked.model ?? args.profile.model?.default);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
// An aborted turn rejects with AbortError whatever the Bridge says about confirming the cancel; the Bridge's error stays as the cause.
|
|
333
|
+
if (checked.signal?.aborted === true && !isAbort(error)) {
|
|
334
|
+
throw new DOMException("turn aborted", { name: "AbortError", cause: error });
|
|
335
|
+
}
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
busy = false;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
return {
|
|
343
|
+
id: sandbox.id,
|
|
344
|
+
provider: vendor.name,
|
|
345
|
+
creation: "created",
|
|
346
|
+
...(inner.name === undefined ? {} : { name: inner.name }),
|
|
347
|
+
capabilities: args.capabilities,
|
|
348
|
+
status: operations.status,
|
|
349
|
+
stream: exclusive,
|
|
350
|
+
...(inner.session === undefined ? {} : { session: (id, options) => inner.session(id, options) }),
|
|
351
|
+
...(inner.dispatch === undefined
|
|
352
|
+
? {}
|
|
353
|
+
: {
|
|
354
|
+
async dispatch(turn) {
|
|
355
|
+
const checked = guard(turn);
|
|
356
|
+
if (busy)
|
|
357
|
+
throw new EnvironmentProviderError("conflict", "a turn is already running on this environment", { provider: vendor.name });
|
|
358
|
+
return inner.dispatch(checked);
|
|
359
|
+
},
|
|
360
|
+
}),
|
|
361
|
+
read: operations.read,
|
|
362
|
+
write: operations.write,
|
|
363
|
+
exec: operations.exec,
|
|
364
|
+
placement: operations.placement,
|
|
365
|
+
destroy(options) {
|
|
366
|
+
destroyPromise ??= (async () => {
|
|
367
|
+
try {
|
|
368
|
+
await inner.destroy?.(options).catch(() => undefined);
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
await sandbox.destroy(options);
|
|
372
|
+
await args.dispatcher.close().catch(() => undefined);
|
|
373
|
+
}
|
|
374
|
+
})().catch((error) => {
|
|
375
|
+
destroyPromise = undefined;
|
|
376
|
+
throw error;
|
|
377
|
+
});
|
|
378
|
+
return destroyPromise;
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const isTerminalResult = (event) => event.type === "result";
|
|
383
|
+
/** The Bridge's own dollar figure is the harness's catalog estimate, never a receipt, so no event carries it. */
|
|
384
|
+
function withoutHarnessCost(event) {
|
|
385
|
+
if (event.usage?.cost === undefined)
|
|
386
|
+
return event;
|
|
387
|
+
const { cost: _cost, ...usage } = event.usage;
|
|
388
|
+
return { ...event, usage };
|
|
389
|
+
}
|
|
390
|
+
/** The model the harness reported serving, read from the Bridge's terminal frame. */
|
|
391
|
+
function reportedModel(event) {
|
|
392
|
+
const models = event.data.provider_models;
|
|
393
|
+
const reported = event.data.provider_model ?? (Array.isArray(models) ? models[0] : undefined);
|
|
394
|
+
return typeof reported === "string" ? reported : undefined;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* One turn as the contract states it: an `execution.started` event naming the
|
|
398
|
+
* declared model, the Bridge's events without harness dollar estimates, a
|
|
399
|
+
* terminal `result` that names the model the harness reported serving, and
|
|
400
|
+
* `turn.timeoutMs` ending the run (the Bridge cancels it, and a failed terminal
|
|
401
|
+
* event names the timeout and carries the usage observed so far).
|
|
402
|
+
*/
|
|
403
|
+
async function* projectTurn(inner, turn, declaredModel) {
|
|
404
|
+
const { timeoutMs, ...rest } = turn;
|
|
405
|
+
yield {
|
|
406
|
+
type: "execution.started",
|
|
407
|
+
data: { ...(rest.sessionId === undefined ? {} : { sessionId: rest.sessionId }), effectiveBackend: { ...(declaredModel === undefined ? {} : { model: declaredModel }), source: "profile" } },
|
|
408
|
+
};
|
|
409
|
+
const timeout = new AbortController();
|
|
410
|
+
let timedOut = false;
|
|
411
|
+
const timer = timeoutMs === undefined ? undefined : setTimeout(() => {
|
|
412
|
+
timedOut = true;
|
|
413
|
+
timeout.abort(new DOMException(`turn exceeded ${timeoutMs} ms`, "TimeoutError"));
|
|
414
|
+
}, timeoutMs);
|
|
415
|
+
const signal = rest.signal === undefined ? timeout.signal : AbortSignal.any([rest.signal, timeout.signal]);
|
|
416
|
+
let lastUsage;
|
|
417
|
+
try {
|
|
418
|
+
for await (const raw of inner.stream({ ...rest, signal })) {
|
|
419
|
+
const event = withoutHarnessCost(raw);
|
|
420
|
+
if (event.usage !== undefined)
|
|
421
|
+
lastUsage = event;
|
|
422
|
+
if (!isTerminalResult(event)) {
|
|
423
|
+
yield event;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const served = reportedModel(event);
|
|
427
|
+
yield served === undefined ? event : { ...event, data: { ...event.data, effectiveBackend: { model: served, source: "harness" } } };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
if (!timedOut)
|
|
432
|
+
throw error;
|
|
433
|
+
yield {
|
|
434
|
+
type: "result",
|
|
435
|
+
data: { status: "failed", error: `turn timed out after ${timeoutMs} ms`, errorCode: "timeout", finalText: "" },
|
|
436
|
+
...(lastUsage?.usage === undefined ? {} : { usage: lastUsage.usage, usageMode: lastUsage.usageMode ?? "cumulative" }),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
if (timer !== undefined)
|
|
441
|
+
clearTimeout(timer);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/** An environment rebuilt by `get`: it reaches the sandbox but starts no turn. */
|
|
445
|
+
function reconstructedEnvironment(sandbox, vendor, providerCapabilities) {
|
|
446
|
+
const operations = sandboxOperations(sandbox, vendor, BOX_WORKSPACE_DIR);
|
|
447
|
+
const refuse = () => {
|
|
448
|
+
throw new EnvironmentProviderError("refused", "a reconstructed environment starts no turn; create one", { path: "turn", provider: vendor.name });
|
|
449
|
+
};
|
|
450
|
+
let destroyPromise;
|
|
451
|
+
return {
|
|
452
|
+
id: sandbox.id,
|
|
453
|
+
provider: vendor.name,
|
|
454
|
+
capabilities: providerCapabilities,
|
|
455
|
+
status: operations.status,
|
|
456
|
+
// eslint-disable-next-line require-yield
|
|
457
|
+
stream: async function* () {
|
|
458
|
+
refuse();
|
|
459
|
+
},
|
|
460
|
+
read: operations.read,
|
|
461
|
+
write: operations.write,
|
|
462
|
+
exec: operations.exec,
|
|
463
|
+
placement: operations.placement,
|
|
464
|
+
destroy(options) {
|
|
465
|
+
destroyPromise ??= sandbox.destroy(options).catch((error) => {
|
|
466
|
+
destroyPromise = undefined;
|
|
467
|
+
throw error;
|
|
468
|
+
});
|
|
469
|
+
return destroyPromise;
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { harnessSystemPromptIntents, } from "@tangle-network/agent-interface";
|
|
2
|
+
import { EnvironmentProviderError } from "@tangle-network/agent-interface/environment-provider";
|
|
3
|
+
import { materializeProfile } from "@tangle-network/agent-profile-materialize";
|
|
4
|
+
import { harnessModelSupportFor } from "@tangle-network/cli-agent-registry";
|
|
5
|
+
import { BOX_HARNESSES, boxHarness } from "./box.js";
|
|
6
|
+
/** A profile axis no workspace control enforces here, with the reason it is refused. */
|
|
7
|
+
const REFUSED_PROFILE_AXES = [
|
|
8
|
+
{ path: "profile.confidential", present: (p) => p.confidential !== undefined, reason: "confidential execution needs placement and attestation this provider does not offer" },
|
|
9
|
+
{ path: "profile.seats", present: (p) => p.seats !== undefined, reason: "an ordered subscription chain is not implemented; this provider runs one Router-routed model" },
|
|
10
|
+
{ path: "profile.connections", present: (p) => (p.connections?.length ?? 0) > 0, reason: "connections must be resolved to MCP servers before create" },
|
|
11
|
+
{ path: "profile.proxySecrets", present: (p) => p.proxySecrets !== undefined, reason: "proxy-held credentials are not supported" },
|
|
12
|
+
{ path: "profile.resources.knowledge", present: (p) => Array.isArray(Reflect.get(p.resources ?? {}, "knowledge")) && Reflect.get(p.resources ?? {}, "knowledge").length > 0, reason: "knowledge must be resolved by the runtime before materialization" },
|
|
13
|
+
{ path: "profile.model.maxVisibleOutputTokens", present: (p) => p.model?.maxVisibleOutputTokens !== undefined, reason: "no workspace control enforces an output token cap" },
|
|
14
|
+
{ path: "profile.model.maxReasoningTokens", present: (p) => p.model?.maxReasoningTokens !== undefined, reason: "no workspace control enforces a reasoning token cap" },
|
|
15
|
+
{ path: "profile.model.maxTotalOutputTokens", present: (p) => p.model?.maxTotalOutputTokens !== undefined, reason: "no workspace control enforces a total output token cap" },
|
|
16
|
+
{ path: "profile.extensions", present: (p) => Object.values(p.extensions ?? {}).some((value) => value !== undefined), reason: "no backend extension namespace is implemented" },
|
|
17
|
+
];
|
|
18
|
+
function metadataCredentialSource(profile) {
|
|
19
|
+
const metadata = profile.model?.metadata;
|
|
20
|
+
return metadata !== null && typeof metadata === "object" ? metadata.credentialSource : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** The harness a request selects, or the refusal that names why none can be. */
|
|
23
|
+
export function resolveBoxHarness(profile, backend, declared) {
|
|
24
|
+
const selected = profile.harness ?? backend;
|
|
25
|
+
if (selected === undefined) {
|
|
26
|
+
throw new EnvironmentProviderError("refused", "a profile or backend must name a harness; a prompt is never run as a shell command", { path: "profile.harness" });
|
|
27
|
+
}
|
|
28
|
+
if (profile.harness !== undefined && backend !== undefined && profile.harness !== backend) {
|
|
29
|
+
throw new EnvironmentProviderError("refused", `backend ${backend} conflicts with profile.harness ${profile.harness}`, { path: "backend" });
|
|
30
|
+
}
|
|
31
|
+
if (!declared.includes(selected) || boxHarness(selected) === undefined) {
|
|
32
|
+
throw new EnvironmentProviderError("refused", `harness ${selected} is not in this provider's declared set (${declared.join(", ")})`, { path: backend === undefined ? "profile.harness" : "backend" });
|
|
33
|
+
}
|
|
34
|
+
return selected;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate a profile against what the box can run, without any sandbox call.
|
|
38
|
+
*
|
|
39
|
+
* It plans with the same materializer the Bridge in the box uses, so a
|
|
40
|
+
* dimension the planner cannot lower is refused here with its own reason.
|
|
41
|
+
*/
|
|
42
|
+
export function validateBoxProfile(profile, backend, declared) {
|
|
43
|
+
const issues = [];
|
|
44
|
+
const error = (code, path, message) => {
|
|
45
|
+
issues.push({ level: "error", code, path, message });
|
|
46
|
+
};
|
|
47
|
+
let harness;
|
|
48
|
+
try {
|
|
49
|
+
harness = resolveBoxHarness(profile, backend, declared);
|
|
50
|
+
}
|
|
51
|
+
catch (caught) {
|
|
52
|
+
const refusal = caught;
|
|
53
|
+
error("harness_refused", refusal.path ?? "profile.harness", refusal.message);
|
|
54
|
+
}
|
|
55
|
+
for (const axis of REFUSED_PROFILE_AXES) {
|
|
56
|
+
if (axis.present(profile))
|
|
57
|
+
error("axis_refused", axis.path, axis.reason);
|
|
58
|
+
}
|
|
59
|
+
if (metadataCredentialSource(profile) === "subscription") {
|
|
60
|
+
error("subscription_refused", "profile.model.metadata.credentialSource", "subscription credentials are not delivered into a cloud box; the model routes through the Router, and a subscription profile is never run on a managed key");
|
|
61
|
+
}
|
|
62
|
+
if (harness !== undefined) {
|
|
63
|
+
const intents = harnessSystemPromptIntents(harness);
|
|
64
|
+
if (profile.prompt?.systemPrompt !== undefined && !intents.replace)
|
|
65
|
+
error("system_prompt_refused", "profile.prompt.systemPrompt", `${harness} cannot replace its system prompt`);
|
|
66
|
+
if (profile.prompt?.appendSystemPrompt !== undefined && !intents.append)
|
|
67
|
+
error("system_prompt_refused", "profile.prompt.appendSystemPrompt", `${harness} cannot append to its system prompt`);
|
|
68
|
+
const model = profile.model?.default;
|
|
69
|
+
if (model !== undefined) {
|
|
70
|
+
const support = harnessModelSupportFor(harness, { provider: profile.model?.provider, model });
|
|
71
|
+
if (!support.supported)
|
|
72
|
+
error("model_refused", "profile.model.default", support.message);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const plan = materializeProfile(profile, harness);
|
|
76
|
+
for (const entry of plan.unsupported)
|
|
77
|
+
error("unsupported", `profile.${entry.dimension}`, entry.reason);
|
|
78
|
+
}
|
|
79
|
+
catch (caught) {
|
|
80
|
+
error("materialization_refused", "profile", caught instanceof Error ? caught.message : String(caught));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { ok: !issues.some((issue) => issue.level === "error"), issues };
|
|
84
|
+
}
|
|
85
|
+
/** Throw the first refusal of a validation result as a typed error that names its path. */
|
|
86
|
+
export function assertProfileValid(result) {
|
|
87
|
+
const first = result.issues.find((issue) => issue.level === "error");
|
|
88
|
+
if (first !== undefined) {
|
|
89
|
+
throw new EnvironmentProviderError("refused", first.message, { ...(first.path === undefined ? {} : { path: first.path }) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Refuse every create-input field this provider cannot honor, before any
|
|
94
|
+
* vendor call. A field is never accepted and dropped.
|
|
95
|
+
*/
|
|
96
|
+
export function refuseUnsupportedCreateInput(input, support) {
|
|
97
|
+
const refuse = (path, message) => {
|
|
98
|
+
throw new EnvironmentProviderError("refused", message, { path });
|
|
99
|
+
};
|
|
100
|
+
if (typeof input.profile === "string")
|
|
101
|
+
refuse("profile", `named profile ${input.profile} is unsupported; pass an inline AgentProfile`);
|
|
102
|
+
if (input.requestedId !== undefined)
|
|
103
|
+
refuse("requestedId", "the vendor issues sandbox ids");
|
|
104
|
+
if (input.runtimeAttachments !== undefined)
|
|
105
|
+
refuse("runtimeAttachments", "runtime MCP attachments are not supported");
|
|
106
|
+
if (input.idempotencyKey !== undefined)
|
|
107
|
+
refuse("idempotencyKey", "durable keyed create is not implemented");
|
|
108
|
+
if (input.billingOwner !== undefined)
|
|
109
|
+
refuse("billingOwner", "billing owners are not supported");
|
|
110
|
+
if (input.secrets !== undefined)
|
|
111
|
+
refuse("secrets", "secrets are not supported; pass env");
|
|
112
|
+
if (input.proxySecrets !== undefined)
|
|
113
|
+
refuse("proxySecrets", "proxy secrets are not supported");
|
|
114
|
+
if (input.proxySecretParents !== undefined)
|
|
115
|
+
refuse("proxySecretParents", "proxy secret parents are not supported");
|
|
116
|
+
if (input.providerOptions !== undefined && Object.keys(input.providerOptions).length > 0)
|
|
117
|
+
refuse("providerOptions", "no provider option is implemented");
|
|
118
|
+
const workspace = input.workspace;
|
|
119
|
+
if (workspace !== undefined) {
|
|
120
|
+
if (workspace.snapshot !== undefined)
|
|
121
|
+
refuse("workspace.snapshot", "workspace snapshots are not supported");
|
|
122
|
+
if (workspace.checkpoint !== undefined)
|
|
123
|
+
refuse("workspace.checkpoint", "workspace checkpoints are not supported");
|
|
124
|
+
if (workspace.cwd?.base === "host")
|
|
125
|
+
refuse("workspace.cwd", "a host cwd is refused; use base \"repository\"");
|
|
126
|
+
if (workspace.providerOptions !== undefined && Object.keys(workspace.providerOptions).length > 0)
|
|
127
|
+
refuse("workspace.providerOptions", "no workspace provider option is implemented");
|
|
128
|
+
if (workspace.environment !== undefined && workspace.image !== undefined)
|
|
129
|
+
refuse("workspace.image", "workspace cannot specify both environment and image");
|
|
130
|
+
if (workspace.environment !== undefined && !support.environment)
|
|
131
|
+
refuse("workspace.environment", "this vendor selects no template or snapshot");
|
|
132
|
+
if (workspace.image !== undefined && !support.image)
|
|
133
|
+
refuse("workspace.image", "this vendor selects no image");
|
|
134
|
+
}
|
|
135
|
+
if (input.resources !== undefined) {
|
|
136
|
+
if (!support.resources)
|
|
137
|
+
refuse("resources", "this vendor sets no resources at create");
|
|
138
|
+
if (input.resources.providerOptions !== undefined)
|
|
139
|
+
refuse("resources.providerOptions", "no resource provider option is implemented");
|
|
140
|
+
}
|
|
141
|
+
if (input.egress !== undefined && !support.egress.includes(input.egress.mode)) {
|
|
142
|
+
refuse("egress", `egress mode ${input.egress.mode} is not enforced by this vendor`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** The harnesses this module can declare. */
|
|
146
|
+
export function declarableHarnesses() {
|
|
147
|
+
return Object.keys(BOX_HARNESSES);
|
|
148
|
+
}
|
|
149
|
+
/** Whether the shared planner lowers this profile dimension for the harness. */
|
|
150
|
+
export function plannerLowers(harness, dimension) {
|
|
151
|
+
const probe = { hooks: { PostToolUse: [{ command: "true" }] } };
|
|
152
|
+
try {
|
|
153
|
+
const plan = materializeProfile(probe, harness);
|
|
154
|
+
return !plan.unsupported.some((entry) => entry.dimension === dimension);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { AgentEnvironmentEgressMode, AgentEnvironmentStatus, ExecRequest, ExecResult, ResourceRequest } from "@tangle-network/agent-interface/environment-provider";
|
|
2
|
+
/**
|
|
3
|
+
* What a cloud sandbox must expose so a CLI Bridge can run inside it.
|
|
4
|
+
*
|
|
5
|
+
* A vendor adapter is only this: create the sandbox from the box image, run
|
|
6
|
+
* commands in it, expose the Bridge port, report status, destroy it. Profile
|
|
7
|
+
* materialization, turns, events, usage, cancellation and refusals all belong
|
|
8
|
+
* to the Bridge and to {@link createBridgeSandboxProvider}.
|
|
9
|
+
*/
|
|
10
|
+
export interface BridgeSandbox {
|
|
11
|
+
/** The vendor's own sandbox id. It becomes `AgentEnvironment.id`. */
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly region?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Run a command to completion as the box user.
|
|
16
|
+
*
|
|
17
|
+
* A non-zero exit is a result, never a throw. `signal` and `timeoutMs` end
|
|
18
|
+
* the remote command itself, not only the client call.
|
|
19
|
+
*/
|
|
20
|
+
exec(command: string, options?: ExecRequest): Promise<ExecResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Start a process that outlives this call, with `env` set on that process
|
|
23
|
+
* only. The Bridge runs this way, so its credentials never enter the sandbox
|
|
24
|
+
* default environment or any command line.
|
|
25
|
+
*/
|
|
26
|
+
spawn(command: string, options: {
|
|
27
|
+
cwd?: string;
|
|
28
|
+
env: Record<string, string>;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
/** Exact bytes written by `write`; a relative path resolves against the workspace directory. */
|
|
31
|
+
read(path: string, options?: {
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}): Promise<string>;
|
|
34
|
+
/** Creates parent directories. */
|
|
35
|
+
write(path: string, content: string, options?: {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
/** The URL that reaches `port` inside the sandbox, plus any headers the vendor's gateway requires. */
|
|
39
|
+
endpoint(port: number, options?: {
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
url: string;
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
}>;
|
|
45
|
+
/** The vendor's current lifecycle state; `stopped` or `expired` after destroy or expiry. */
|
|
46
|
+
status(options?: {
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}): Promise<AgentEnvironmentStatus>;
|
|
49
|
+
/** Delete the sandbox. Resolves when it is already gone. */
|
|
50
|
+
destroy(options?: {
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export interface BridgeSandboxCreateRequest {
|
|
55
|
+
name?: string;
|
|
56
|
+
/** Tags the vendor stores with the sandbox, string values only. Always carries the run tag. */
|
|
57
|
+
metadata: Record<string, string>;
|
|
58
|
+
/** Caller environment for the sandbox's default exec environment. */
|
|
59
|
+
env: Record<string, string>;
|
|
60
|
+
/** Finite lifetime the vendor enforces; longer than any turn the caller may run. */
|
|
61
|
+
lifetimeMs: number;
|
|
62
|
+
/** Vendor template, snapshot or image the caller chose; absent selects the published box image. */
|
|
63
|
+
environment?: string;
|
|
64
|
+
image?: string;
|
|
65
|
+
resources?: ResourceRequest;
|
|
66
|
+
egress?: {
|
|
67
|
+
mode: AgentEnvironmentEgressMode;
|
|
68
|
+
allowDomains: readonly string[];
|
|
69
|
+
};
|
|
70
|
+
/** Aborting must cancel the vendor create and destroy a sandbox that arrives late. */
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
}
|
|
73
|
+
export interface BridgeSandboxSummary {
|
|
74
|
+
id: string;
|
|
75
|
+
status: AgentEnvironmentStatus;
|
|
76
|
+
name?: string;
|
|
77
|
+
metadata: Record<string, string>;
|
|
78
|
+
}
|
|
79
|
+
/** Create-input fields a vendor honors beyond the always-honored ones. Everything else is refused. */
|
|
80
|
+
export interface BridgeVendorSupport {
|
|
81
|
+
/** `workspace.environment` selects a vendor template or snapshot. */
|
|
82
|
+
environment: boolean;
|
|
83
|
+
/** `workspace.image` selects a vendor image. */
|
|
84
|
+
image: boolean;
|
|
85
|
+
resources: boolean;
|
|
86
|
+
/** Egress modes the vendor enforces; absent modes are refused. */
|
|
87
|
+
egress: readonly AgentEnvironmentEgressMode[];
|
|
88
|
+
}
|
|
89
|
+
export interface BridgeSandboxVendor {
|
|
90
|
+
/** Provider name reported on every environment, for example `e2b`. */
|
|
91
|
+
readonly name: string;
|
|
92
|
+
readonly support: BridgeVendorSupport;
|
|
93
|
+
create(request: BridgeSandboxCreateRequest): Promise<BridgeSandbox>;
|
|
94
|
+
/** `null` only when the vendor definitively reports no such sandbox; transport failures throw. */
|
|
95
|
+
connect(id: string, options?: {
|
|
96
|
+
signal?: AbortSignal;
|
|
97
|
+
}): Promise<BridgeSandbox | null>;
|
|
98
|
+
/** Sandboxes whose tags contain every entry of `metadata`, with their true status. */
|
|
99
|
+
list(query: {
|
|
100
|
+
name?: string;
|
|
101
|
+
metadata?: Record<string, string>;
|
|
102
|
+
}, options?: {
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
}): Promise<BridgeSandboxSummary[]>;
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-cli-bridge",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for cli-bridge retained sessions and one-shot turns",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"import": "./dist/index.js",
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./sandbox": {
|
|
19
|
+
"import": "./dist/sandbox/index.js",
|
|
20
|
+
"types": "./dist/sandbox/index.d.ts",
|
|
21
|
+
"default": "./dist/sandbox/index.js"
|
|
17
22
|
}
|
|
18
23
|
},
|
|
19
24
|
"repository": {
|
|
@@ -60,15 +65,29 @@
|
|
|
60
65
|
"dist/transport.js",
|
|
61
66
|
"dist/wire.d.ts",
|
|
62
67
|
"dist/wire.js",
|
|
68
|
+
"dist/sandbox/index.d.ts",
|
|
69
|
+
"dist/sandbox/index.js",
|
|
70
|
+
"dist/sandbox/box.d.ts",
|
|
71
|
+
"dist/sandbox/box.js",
|
|
72
|
+
"dist/sandbox/image.d.ts",
|
|
73
|
+
"dist/sandbox/image.js",
|
|
74
|
+
"dist/sandbox/provider.d.ts",
|
|
75
|
+
"dist/sandbox/provider.js",
|
|
76
|
+
"dist/sandbox/refusal.d.ts",
|
|
77
|
+
"dist/sandbox/refusal.js",
|
|
78
|
+
"dist/sandbox/vendor.d.ts",
|
|
79
|
+
"dist/sandbox/vendor.js",
|
|
63
80
|
"README.md",
|
|
64
81
|
"LICENSE"
|
|
65
82
|
],
|
|
66
83
|
"dependencies": {
|
|
67
|
-
"@tangle-network/agent-interface": "^5.10.
|
|
68
|
-
"undici": "8.12.3"
|
|
84
|
+
"@tangle-network/agent-interface": "^5.10.12",
|
|
85
|
+
"undici": "8.12.3",
|
|
86
|
+
"@tangle-network/agent-profile-materialize": "1.1.4",
|
|
87
|
+
"@tangle-network/cli-agent-registry": "^0.14.0"
|
|
69
88
|
},
|
|
70
89
|
"devDependencies": {
|
|
71
|
-
"@tangle-network/agent-provider-testkit": "0.12.
|
|
90
|
+
"@tangle-network/agent-provider-testkit": "0.12.4",
|
|
72
91
|
"@types/node": "26.6.5",
|
|
73
92
|
"typescript": "6.0.3",
|
|
74
93
|
"vitest": "5.0.5"
|