@tangle-network/agent-provider-tangle 1.1.4 → 1.1.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 CHANGED
@@ -36,7 +36,9 @@ const provider = createTangleProvider({
36
36
  readyTimeoutMs: 180_000,
37
37
  });
38
38
 
39
- const environment = await provider.create({ profile: { name: "worker" } });
39
+ const environment = await provider.create({
40
+ profile: { name: "worker", harness: "codex" },
41
+ });
40
42
  for await (const event of environment.stream({ prompt: "run the task" })) {
41
43
  // The sandbox is running. No caller-side wait, poll, or retry stands here.
42
44
  }
@@ -50,6 +52,14 @@ A client that offers neither cannot prove readiness, so the sandbox it returned
50
52
  The adapter reads the platform's answer back rather than trusting the wait to have resolved for the right reason.
51
53
  A create call returns only a sandbox that reports `running`: `failed`, `stopped`, and `expired` are refused with the status named, and so is a wait that resolves while the sandbox still reports `provisioning`.
52
54
 
55
+ The default create mapping selects `create()`'s `backend`, then the provider's `defaultBackend`, then the inline profile's `harness`.
56
+ An inline profile therefore carries its own backend unless the caller records an execution override.
57
+ Profiles without a harness retain the provider's legacy OpenCode fallback.
58
+ A configured `mapCreateInput` owns the Sandbox options and may select another backend or profile.
59
+ The created environment's capabilities use that mapping's backend; provider capabilities describe the configured default.
60
+ `get(id)` reads the running sandbox's backend status.
61
+ If that status is unavailable, the reconstructed environment advertises no backend-specific capabilities.
62
+
53
63
  ## Per-turn backend options
54
64
 
55
65
  `AgentTurnInput.providerOptions.backend` reaches `PromptOptions.backend` on the Sandbox prompt call.
@@ -1,6 +1,6 @@
1
- import type { BackendType, CreateSandboxOptions } from "@tangle-network/sandbox";
1
+ import { type BackendType, type CreateSandboxOptions } from "@tangle-network/sandbox";
2
2
  import type { CreateAgentEnvironmentInput, WorkspaceRequest } from "@tangle-network/agent-interface/environment-provider";
3
- export declare function sandboxOptionsFromCreateInput(input: CreateAgentEnvironmentInput, defaultBackend: BackendType, parsedWorkspace?: WorkspaceRequest): CreateSandboxOptions;
3
+ export declare function sandboxOptionsFromCreateInput(input: CreateAgentEnvironmentInput, defaultBackend?: BackendType, parsedWorkspace?: WorkspaceRequest): CreateSandboxOptions;
4
4
  /** Reject value-bearing secret maps before any custom mapper can drop them. */
5
5
  export declare function assertNoInlineSecretValues(input: CreateAgentEnvironmentInput, parsedWorkspace?: WorkspaceRequest): void;
6
6
  /** A custom mapper must not smuggle a value map into the Sandbox request. */
@@ -1,8 +1,13 @@
1
+ import { parseBackendType, } from "@tangle-network/sandbox";
1
2
  import { AgentEnvironmentEgressPolicySchema, WorkspaceRequestSchema, workspaceCwdPathForBase, } from "@tangle-network/agent-interface/environment-provider";
2
3
  import { assertBoundedJson, boundedIdentifier, boundedString, MAX_ARRAY_LENGTH, MAX_MAP_ENTRIES, } from "./tangle-contract-safety.js";
3
4
  import { sandboxResourcesFromResourceRequest } from "./tangle-resources.js";
4
5
  export function sandboxOptionsFromCreateInput(input, defaultBackend, parsedWorkspace) {
5
6
  const workspace = assertCreateInputShape(input, parsedWorkspace) ?? {};
7
+ const profile = inlineAgentProfile(input.profile);
8
+ const backend = input.backend === undefined
9
+ ? parseBackendType(defaultBackend ?? profile.harness ?? "opencode")
10
+ : input.backend;
6
11
  assertNoInlineSecretValues(input, workspace);
7
12
  if (input.providerOptions && Object.keys(input.providerOptions).length > 0) {
8
13
  throw new Error("Tangle create providerOptions are not supported");
@@ -63,8 +68,8 @@ export function sandboxOptionsFromCreateInput(input, defaultBackend, parsedWorks
63
68
  ...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }),
64
69
  backend: {
65
70
  ...(base.backend ?? {}),
66
- type: (input.backend ?? defaultBackend),
67
- profile: inlineAgentProfile(input.profile),
71
+ type: backend,
72
+ profile,
68
73
  },
69
74
  };
70
75
  return mapped;
@@ -1,4 +1,5 @@
1
1
  import { AgentEnvironmentCapabilitiesSchema, createAgentEnvironmentWithIdempotency, } from "@tangle-network/agent-interface/environment-provider";
2
+ import { deepFreeze, harnessTypeSchema } from "@tangle-network/agent-interface";
2
3
  import { createTangleExactProcessProvider, } from "./exact-process.js";
3
4
  import { capabilitiesForClient, defaultTangleSandboxCapabilities, narrowTangleCapabilitiesToBackend, } from "./tangle-capabilities.js";
4
5
  import { sandboxInstanceAsEnvironment } from "./tangle-environment.js";
@@ -23,18 +24,19 @@ export function createTangleProvider(options) {
23
24
  readyTimeoutMs,
24
25
  })
25
26
  : undefined;
26
- const resolveDeclaredCapabilities = async () => {
27
+ const resolveDeclaredCapabilities = async (backendType) => {
28
+ const harness = harnessTypeSchema.safeParse(backendType);
27
29
  const configured = options.capabilities
28
30
  ? typeof options.capabilities === "function"
29
31
  ? await options.capabilities()
30
32
  : options.capabilities
31
- : defaultTangleSandboxCapabilities();
33
+ : defaultTangleSandboxCapabilities(harness.success ? harness.data : undefined);
32
34
  if (!exactProcess && configured.exactProcess) {
33
35
  throw new Error("Tangle capabilities cannot advertise exactProcess without exactProcess configuration");
34
36
  }
35
37
  const backend = configured.interactions === undefined
36
38
  ? undefined
37
- : await resolveDefaultBackend(options.client, options.defaultBackend);
39
+ : await resolveBackend(options.client, backendType);
38
40
  const narrowed = narrowTangleCapabilitiesToBackend(configured, backend);
39
41
  return exactProcess
40
42
  ? {
@@ -46,7 +48,7 @@ export function createTangleProvider(options) {
46
48
  // Provider-boundary document: client-stage facts only. It also validates
47
49
  // the configured document, so create() and get() call it before any effect.
48
50
  const narrowedProviderCapabilities = (declared) => AgentEnvironmentCapabilitiesSchema.parse(capabilitiesForClient(declared, options.client, confidentialVerifierOption(options.confidentialAttestationVerifier)));
49
- const resolveCapabilities = async () => narrowedProviderCapabilities(await resolveDeclaredCapabilities());
51
+ const resolveCapabilities = async () => narrowedProviderCapabilities(await resolveDeclaredCapabilities(options.defaultBackend));
50
52
  const createRecords = new Map();
51
53
  const createEnvironment = async (input) => {
52
54
  const parsedWorkspace = assertCreateInputShape(input);
@@ -55,19 +57,19 @@ export function createTangleProvider(options) {
55
57
  if (input.providerOptions && Object.keys(input.providerOptions).length > 0) {
56
58
  throw new Error("Tangle create providerOptions are not supported");
57
59
  }
58
- // The sandbox stage narrows from the declared document, not the
59
- // provider-boundary one: the client stage cannot observe box-scoped
60
- // facts, so measured instance facts must decide them per sandbox.
61
- const declaredCapabilities = await resolveDeclaredCapabilities();
62
- narrowedProviderCapabilities(declaredCapabilities);
63
- const createOptions = options.mapCreateInput?.(input) ??
64
- sandboxOptionsFromCreateInput(input, options.defaultBackend ?? "opencode", parsedWorkspace);
65
- assertMappedCreateOptions(createOptions);
60
+ const mappedOptions = options.mapCreateInput?.(input) ??
61
+ sandboxOptionsFromCreateInput(input, options.defaultBackend, parsedWorkspace);
62
+ assertMappedCreateOptions(mappedOptions);
63
+ const createOptions = deepFreeze(structuredClone(mappedOptions));
66
64
  if (input.idempotencyKey !== undefined &&
67
65
  createOptions.idempotencyKey !== input.idempotencyKey) {
68
66
  throw new Error("Tangle mapped create options must preserve input idempotencyKey");
69
67
  }
70
68
  assertMappedSecretNames(createOptions);
69
+ // Backend selection belongs to the create projection. The provider-wide
70
+ // default cannot describe a sandbox that this projection routes elsewhere.
71
+ const declaredCapabilities = await resolveDeclaredCapabilities(createOptions.backend?.type);
72
+ narrowedProviderCapabilities(declaredCapabilities);
71
73
  input.signal?.throwIfAborted();
72
74
  const createPromise = options.client.create(createOptions, input.signal ? { signal: input.signal } : undefined);
73
75
  let box;
@@ -155,21 +157,43 @@ export function createTangleProvider(options) {
155
157
  ...(workspaceBranching === undefined ? {} : { workspaceBranching }),
156
158
  capabilities: resolveCapabilities,
157
159
  ...(options.validateProfile ? { validateProfile: options.validateProfile } : {}),
158
- create(input) {
159
- return createAgentEnvironmentWithIdempotency(createRecords, input, () => createEnvironment(input));
160
+ async create(input) {
161
+ const { signal, ...material } = input;
162
+ signal?.throwIfAborted();
163
+ assertCreateInputShape(input);
164
+ assertNoInlineSecretValues(input);
165
+ for (const value of Object.values(material)) {
166
+ if (value !== undefined)
167
+ assertBoundedJson(value);
168
+ }
169
+ const acceptedInput = Object.freeze({
170
+ ...deepFreeze(structuredClone(material)),
171
+ ...(signal === undefined ? {} : { signal }),
172
+ });
173
+ return createAgentEnvironmentWithIdempotency(createRecords, acceptedInput, createEnvironment);
160
174
  },
161
175
  ...(options.client.get
162
176
  ? {
163
177
  async get(id, operation) {
164
178
  assertProviderOperationOptions(operation, "Tangle get");
165
179
  boundedIdentifier(id, "Tangle environment id");
166
- const declaredCapabilities = await resolveDeclaredCapabilities();
167
- narrowedProviderCapabilities(declaredCapabilities);
180
+ await resolveCapabilities();
168
181
  operation?.signal?.throwIfAborted();
169
182
  const box = await awaitWithSignal(options.client.get?.(id, operation), operation?.signal);
170
183
  operation?.signal?.throwIfAborted();
171
184
  if (!box || boundedIdentifier(box.id, "Tangle environment id") !== id)
172
185
  return null;
186
+ let backendType;
187
+ if (box.status === "running" && box.backend) {
188
+ try {
189
+ const status = await awaitWithSignal(box.backend.status(), operation?.signal);
190
+ backendType = boundedIdentifier(status.type, "Tangle current backend");
191
+ }
192
+ catch {
193
+ operation?.signal?.throwIfAborted();
194
+ }
195
+ }
196
+ const declaredCapabilities = await resolveDeclaredCapabilities(backendType);
173
197
  return await sandboxInstanceAsEnvironment(box, providerName, options.client, declaredCapabilities, operation?.signal ? { signal: operation.signal } : undefined, confidentialVerifierOption(options.confidentialAttestationVerifier));
174
198
  },
175
199
  }
@@ -246,16 +270,16 @@ export function createTangleProvider(options) {
246
270
  : {}),
247
271
  };
248
272
  }
249
- async function resolveDefaultBackend(client, defaultBackend) {
250
- if (defaultBackend === undefined)
273
+ async function resolveBackend(client, backendType) {
274
+ if (backendType === undefined)
251
275
  return undefined;
252
276
  try {
253
277
  if (client.getBackend)
254
- return await client.getBackend(defaultBackend);
278
+ return await client.getBackend(backendType);
255
279
  if (!client.listBackends)
256
280
  return undefined;
257
281
  const catalog = await client.listBackends();
258
- return catalog.backends.find((backend) => backend.type === defaultBackend);
282
+ return catalog.backends.find((backend) => backend.type === backendType);
259
283
  }
260
284
  catch {
261
285
  return undefined;
@@ -391,6 +391,12 @@ export interface SandboxInstanceLike {
391
391
  name?: string;
392
392
  status?: unknown;
393
393
  metadata?: Record<string, unknown>;
394
+ /** Current backend reported by this sandbox's runtime. */
395
+ backend?: {
396
+ status(): Promise<{
397
+ type: string;
398
+ }>;
399
+ };
394
400
  /** Network location of the sandbox runtime, without its bearer. */
395
401
  connection?: SandboxConnectionLike;
396
402
  /** When the platform retires this sandbox, when it set a lifetime. */
@@ -578,9 +584,11 @@ export interface SandboxInteractionCommandResultLike {
578
584
  export interface TangleProviderOptions {
579
585
  client: SandboxClientLike;
580
586
  name?: string;
587
+ /** Overrides an inline profile's harness when create() names no backend. */
581
588
  defaultBackend?: BackendType;
582
589
  capabilities?: AgentEnvironmentCapabilities | (() => AgentEnvironmentCapabilities | Promise<AgentEnvironmentCapabilities>);
583
590
  validateProfile?: AgentEnvironmentProvider["validateProfile"];
591
+ /** Replaces the default mapping, including its backend and profile selection. */
584
592
  mapCreateInput?: (input: CreateAgentEnvironmentInput) => CreateSandboxOptions;
585
593
  exactProcess?: TangleExactProcessOptions;
586
594
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-tangle",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "AgentEnvironmentProvider adapter for Tangle sandboxes",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -91,7 +91,7 @@
91
91
  "LICENSE"
92
92
  ],
93
93
  "dependencies": {
94
- "@tangle-network/agent-interface": "^2.3.0"
94
+ "@tangle-network/agent-interface": "^2.3.1"
95
95
  },
96
96
  "peerDependencies": {
97
97
  "@tangle-network/sandbox": ">=0.34.6 <1.0.0"