@tangle-network/agent-provider-tangle 1.1.0 → 1.1.2
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 +3 -0
- package/dist/tangle-capabilities.js +4 -1
- package/dist/tangle-environment.js +27 -1
- package/dist/tangle-exact-process-environment.js +19 -1
- package/dist/tangle-readiness.d.ts +11 -11
- package/dist/tangle-readiness.js +11 -11
- package/dist/tangle-workspace-branching.js +18 -1
- package/package.json +17 -26
package/README.md
CHANGED
|
@@ -241,6 +241,9 @@ if (checkpoint?.status === "created" || checkpoint?.status === "replayed") {
|
|
|
241
241
|
### Confidential forks
|
|
242
242
|
|
|
243
243
|
Sandbox returns raw TEE evidence, not a verified claim.
|
|
244
|
+
The deployed Tangle job does not carry snapshot restore or sealed-persistence inputs.
|
|
245
|
+
The provider therefore refuses new confidential workspace forks before creation.
|
|
246
|
+
Existing confidential fork records still require a matching TEE report and an external verifier before they become trusted.
|
|
244
247
|
Pass `confidentialAttestationVerifier` to `createTangleProvider` to connect a
|
|
245
248
|
trusted provider-key and measurement verifier.
|
|
246
249
|
The callback receives the raw report, the expected environment binding, and
|
|
@@ -86,6 +86,8 @@ export function defaultTangleSandboxCapabilities(harness) {
|
|
|
86
86
|
branching: {
|
|
87
87
|
checkpoint: true,
|
|
88
88
|
fork: true,
|
|
89
|
+
// The deployed Tangle job does not carry snapshot restore inputs.
|
|
90
|
+
confidential: false,
|
|
89
91
|
retrySafe: true,
|
|
90
92
|
lookup: true,
|
|
91
93
|
cleanup: true,
|
|
@@ -361,11 +363,12 @@ export function narrowedTangleCapabilities(declared, support, deployment, option
|
|
|
361
363
|
// An incomplete Sandbox surface clears every branching flag together. A
|
|
362
364
|
// partial claim would let a caller start an operation it cannot recover.
|
|
363
365
|
branching: support.workspaceBranching
|
|
364
|
-
? { ...declaredBranching }
|
|
366
|
+
? { ...declaredBranching, confidential: false }
|
|
365
367
|
: {
|
|
366
368
|
...declaredBranching,
|
|
367
369
|
checkpoint: false,
|
|
368
370
|
fork: false,
|
|
371
|
+
confidential: false,
|
|
369
372
|
...(declaredBranching.retrySafe !== undefined ? { retrySafe: false } : {}),
|
|
370
373
|
...(declaredBranching.lookup !== undefined ? { lookup: false } : {}),
|
|
371
374
|
...(declaredBranching.cleanup !== undefined ? { cleanup: false } : {}),
|
|
@@ -71,6 +71,8 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
71
71
|
// Usage is measured per execution, so the log collects what runs through
|
|
72
72
|
// this handle and the observation reports the newest record it holds.
|
|
73
73
|
const usageLog = createExecutionUsageLog();
|
|
74
|
+
// The single destroy this handle performs, once it has been asked for.
|
|
75
|
+
let destruction;
|
|
74
76
|
const terminals = capabilities.interactiveTerminal?.attach === true
|
|
75
77
|
? createTangleTerminalRegistry(box)
|
|
76
78
|
: undefined;
|
|
@@ -310,7 +312,31 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
310
312
|
throw new Error("Tangle sandbox client cannot delete this environment");
|
|
311
313
|
assertOptionKeys(options, ["signal"], "Tangle destroy");
|
|
312
314
|
options?.signal?.throwIfAborted();
|
|
313
|
-
|
|
315
|
+
// One environment has more than one owner of its end. A runtime
|
|
316
|
+
// that destroys on settle AND tears the executor down calls this
|
|
317
|
+
// twice for the same box, and the platform refuses the second
|
|
318
|
+
// DELETE with "A sandbox lifecycle operation is already in
|
|
319
|
+
// progress" while the first one's cleanup still holds the
|
|
320
|
+
// sandbox's lifecycle lease. Measured 2026-09-01 on
|
|
321
|
+
// `sandbox-b525e04a551b`: of two concurrent deletes one answered
|
|
322
|
+
// 200 `destroyed` and the other 409, and a third delete 0.4 s
|
|
323
|
+
// after the 200 was still refused. That refusal cost two
|
|
324
|
+
// completed agent turns their result
|
|
325
|
+
// (tangle-network/agent-sdk#280), because the second destroy ran
|
|
326
|
+
// in a caller's `finally` and its throw replaced the run's own
|
|
327
|
+
// outcome.
|
|
328
|
+
//
|
|
329
|
+
// So destroy is answered once per environment and every later
|
|
330
|
+
// caller joins that answer. This is not a retry and hides no
|
|
331
|
+
// platform error: a delete that fails is not remembered, and the
|
|
332
|
+
// next caller issues a real one.
|
|
333
|
+
destruction ??= awaitWithSignal(box.delete(options), options?.signal)
|
|
334
|
+
.then(() => undefined)
|
|
335
|
+
.catch((error) => {
|
|
336
|
+
destruction = undefined;
|
|
337
|
+
throw error;
|
|
338
|
+
});
|
|
339
|
+
await awaitWithSignal(destruction, options?.signal);
|
|
314
340
|
options?.signal?.throwIfAborted();
|
|
315
341
|
},
|
|
316
342
|
}
|
|
@@ -11,6 +11,9 @@ export function sandboxInstanceAsExactProcessEnvironment(box, providerName) {
|
|
|
11
11
|
const process = box.process;
|
|
12
12
|
const fs = box.fs;
|
|
13
13
|
const destroy = box.delete.bind(box);
|
|
14
|
+
// The same single delete this environment performs, once it is asked for.
|
|
15
|
+
// See the destroy() below for why one is all the platform accepts.
|
|
16
|
+
let destruction;
|
|
14
17
|
return {
|
|
15
18
|
id: box.id,
|
|
16
19
|
provider: providerName,
|
|
@@ -177,7 +180,22 @@ export function sandboxInstanceAsExactProcessEnvironment(box, providerName) {
|
|
|
177
180
|
async destroy(options = {}) {
|
|
178
181
|
assertSignalOptions(options, "Tangle exact process destroy");
|
|
179
182
|
options.signal?.throwIfAborted();
|
|
180
|
-
|
|
183
|
+
// Deleting one sandbox twice is refused, not repeated: the platform
|
|
184
|
+
// holds a per-sandbox lifecycle lease past the first DELETE's own
|
|
185
|
+
// response while its deferred cleanup runs, so a second DELETE inside
|
|
186
|
+
// that window answers "A sandbox lifecycle operation is already in
|
|
187
|
+
// progress". Measured 2026-09-01 (issue #280) on the streaming
|
|
188
|
+
// environment, which has the same shape and the same platform route.
|
|
189
|
+
// This environment is destroyed once and later callers join that
|
|
190
|
+
// answer; a delete that fails is not remembered, so nothing hides a
|
|
191
|
+
// real error.
|
|
192
|
+
destruction ??= awaitWithSignal(destroy(), options.signal)
|
|
193
|
+
.then(() => undefined)
|
|
194
|
+
.catch((error) => {
|
|
195
|
+
destruction = undefined;
|
|
196
|
+
throw error;
|
|
197
|
+
});
|
|
198
|
+
await awaitWithSignal(destruction, options.signal);
|
|
181
199
|
options.signal?.throwIfAborted();
|
|
182
200
|
},
|
|
183
201
|
};
|
|
@@ -14,18 +14,18 @@ export declare const DEFAULT_TANGLE_READY_TIMEOUT_MS = 120000;
|
|
|
14
14
|
* A response that already reports `running` skips that wait, and `running`
|
|
15
15
|
* alone is not usable: the SDK's own `waitFor` treats the target as reached
|
|
16
16
|
* only when `filesystemIncarnationReadiness` is `ready`, because the box's
|
|
17
|
-
* filesystem is still being built until then.
|
|
18
|
-
* that
|
|
19
|
-
* on it, and the first turn lands on that lock.
|
|
17
|
+
* filesystem is still being built until then. An environment composed before
|
|
18
|
+
* that is an environment that cannot accept a turn.
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
20
|
+
* WHAT THIS WAIT IS NOT FOR. An earlier version of this comment claimed the
|
|
21
|
+
* wait also closed the platform's "A sandbox lifecycle operation is already in
|
|
22
|
+
* progress", and that claim is withdrawn: measured 2026-09-01 (issue #280),
|
|
23
|
+
* that refusal comes from `DELETE /v1/sandboxes/:id`, not from a turn. The
|
|
24
|
+
* platform guards exactly three routes with the per-sandbox lifecycle lease —
|
|
25
|
+
* resume, stop and delete — and the runtime path a prompt takes is not one of
|
|
26
|
+
* them, so no readiness wait here can prevent it and none is trying to. The
|
|
27
|
+
* collision is answered where it happens, in `destroy()`
|
|
28
|
+
* (`tangle-environment.ts`).
|
|
29
29
|
*
|
|
30
30
|
* Readiness also decides what the environment can claim. Composing an
|
|
31
31
|
* environment reads the sandbox's deployment capability document once, and a
|
package/dist/tangle-readiness.js
CHANGED
|
@@ -15,18 +15,18 @@ export const DEFAULT_TANGLE_READY_TIMEOUT_MS = 120_000;
|
|
|
15
15
|
* A response that already reports `running` skips that wait, and `running`
|
|
16
16
|
* alone is not usable: the SDK's own `waitFor` treats the target as reached
|
|
17
17
|
* only when `filesystemIncarnationReadiness` is `ready`, because the box's
|
|
18
|
-
* filesystem is still being built until then.
|
|
19
|
-
* that
|
|
20
|
-
* on it, and the first turn lands on that lock.
|
|
18
|
+
* filesystem is still being built until then. An environment composed before
|
|
19
|
+
* that is an environment that cannot accept a turn.
|
|
21
20
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
21
|
+
* WHAT THIS WAIT IS NOT FOR. An earlier version of this comment claimed the
|
|
22
|
+
* wait also closed the platform's "A sandbox lifecycle operation is already in
|
|
23
|
+
* progress", and that claim is withdrawn: measured 2026-09-01 (issue #280),
|
|
24
|
+
* that refusal comes from `DELETE /v1/sandboxes/:id`, not from a turn. The
|
|
25
|
+
* platform guards exactly three routes with the per-sandbox lifecycle lease —
|
|
26
|
+
* resume, stop and delete — and the runtime path a prompt takes is not one of
|
|
27
|
+
* them, so no readiness wait here can prevent it and none is trying to. The
|
|
28
|
+
* collision is answered where it happens, in `destroy()`
|
|
29
|
+
* (`tangle-environment.ts`).
|
|
30
30
|
*
|
|
31
31
|
* Readiness also decides what the environment can claim. Composing an
|
|
32
32
|
* environment reads the sandbox's deployment capability document once, and a
|
|
@@ -16,6 +16,18 @@ const MAX_MARKER_TAG_LENGTH = 128;
|
|
|
16
16
|
const MARKER_CHUNK_SIZE = 80;
|
|
17
17
|
const LEGACY_MARKER_CHUNK_SIZE = 240;
|
|
18
18
|
const MAX_MARKER_CHUNKS = 512;
|
|
19
|
+
const TANGLE_ATTESTATION_NONCE_PATTERN = /^(?:0x)?[0-9a-fA-F]{64}(?:[0-9a-fA-F]{64})?$/;
|
|
20
|
+
function confidentialForkRefusal(request) {
|
|
21
|
+
const confidential = request.confidential;
|
|
22
|
+
if (confidential?.requested !== true)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (confidential.nonce === undefined ||
|
|
25
|
+
!TANGLE_ATTESTATION_NONCE_PATTERN.test(confidential.nonce))
|
|
26
|
+
return "Tangle confidential forks require a 32-64 byte hexadecimal attestation nonce";
|
|
27
|
+
if (confidential.sealed === true)
|
|
28
|
+
return "Tangle confidential forks cannot prove sealed no-persistence because the deployed job does not carry that requirement";
|
|
29
|
+
return "Tangle confidential workspace forks are unavailable because the deployed job does not carry snapshot restore inputs";
|
|
30
|
+
}
|
|
19
31
|
/**
|
|
20
32
|
* Carry the verifier only when the caller supplied one. Every option type that
|
|
21
33
|
* accepts it is exact-optional, so an explicit `undefined` is not an absent key.
|
|
@@ -293,6 +305,10 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
293
305
|
if (known.state === "known") {
|
|
294
306
|
return forkSuccess(request, known.record.environment, "replayed");
|
|
295
307
|
}
|
|
308
|
+
const confidentialRefusal = confidentialForkRefusal(request);
|
|
309
|
+
if (confidentialRefusal !== undefined) {
|
|
310
|
+
return forkUnknown(request, confidentialRefusal, false);
|
|
311
|
+
}
|
|
296
312
|
const checkpoint = [...checkpoints.values()].some((record) => canonicalCandidateDigest(record.checkpoint) ===
|
|
297
313
|
canonicalCandidateDigest(request.checkpoint))
|
|
298
314
|
? true
|
|
@@ -624,6 +640,7 @@ async function confidentialAttestationForChild(request, child, provider, verifie
|
|
|
624
640
|
const material = {
|
|
625
641
|
provider,
|
|
626
642
|
requested: true,
|
|
643
|
+
tee: response.attestation.tee_type,
|
|
627
644
|
nonce: confidential.nonce,
|
|
628
645
|
measurement,
|
|
629
646
|
environmentId: child.id,
|
|
@@ -681,7 +698,7 @@ async function confidentialAttestationForChild(request, child, provider, verifie
|
|
|
681
698
|
}
|
|
682
699
|
function validTeeReport(report) {
|
|
683
700
|
return (!!report &&
|
|
684
|
-
|
|
701
|
+
safeIdentifier(report.tee_type) !== undefined &&
|
|
685
702
|
Array.isArray(report.evidence) &&
|
|
686
703
|
report.evidence.length <= MAX_TEE_EVIDENCE_BYTES &&
|
|
687
704
|
report.evidence.every((value) => Number.isInteger(value) && value >= 0 && value <= 255) &&
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-tangle",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for Tangle sandboxes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"main": "dist/index.js",
|
|
8
|
-
"types": "
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
|
-
"types": "./
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
"repository": {
|
|
@@ -19,16 +20,7 @@
|
|
|
19
20
|
},
|
|
20
21
|
"publishConfig": {
|
|
21
22
|
"access": "public",
|
|
22
|
-
"registry": "https://registry.npmjs.org"
|
|
23
|
-
"main": "./dist/index.js",
|
|
24
|
-
"types": "./dist/index.d.ts",
|
|
25
|
-
"exports": {
|
|
26
|
-
".": {
|
|
27
|
-
"import": "./dist/index.js",
|
|
28
|
-
"types": "./dist/index.d.ts",
|
|
29
|
-
"default": "./dist/index.js"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
23
|
+
"registry": "https://registry.npmjs.org"
|
|
32
24
|
},
|
|
33
25
|
"files": [
|
|
34
26
|
"dist/index.d.ts",
|
|
@@ -98,15 +90,8 @@
|
|
|
98
90
|
"README.md",
|
|
99
91
|
"LICENSE"
|
|
100
92
|
],
|
|
101
|
-
"scripts": {
|
|
102
|
-
"build": "tsc -p tsconfig.json",
|
|
103
|
-
"check-types": "tsc --noEmit",
|
|
104
|
-
"clean": "rm -rf dist",
|
|
105
|
-
"prepublishOnly": "pnpm run build",
|
|
106
|
-
"test": "vitest run src"
|
|
107
|
-
},
|
|
108
93
|
"dependencies": {
|
|
109
|
-
"@tangle-network/agent-interface": "^2.
|
|
94
|
+
"@tangle-network/agent-interface": "^2.3.0"
|
|
110
95
|
},
|
|
111
96
|
"peerDependencies": {
|
|
112
97
|
"@tangle-network/sandbox": ">=0.34.6 <1.0.0"
|
|
@@ -118,11 +103,17 @@
|
|
|
118
103
|
},
|
|
119
104
|
"devDependencies": {
|
|
120
105
|
"@tangle-network/agent-eval": "0.170.0",
|
|
121
|
-
"@tangle-network/agent-provider-testkit": "workspace:*",
|
|
122
106
|
"@tangle-network/agent-runtime": "0.184.0",
|
|
123
107
|
"@tangle-network/sandbox": "0.34.6",
|
|
124
|
-
"@types/node": "
|
|
108
|
+
"@types/node": "26.4.0",
|
|
125
109
|
"typescript": "7.0.2",
|
|
126
|
-
"vitest": "
|
|
110
|
+
"vitest": "4.1.11",
|
|
111
|
+
"@tangle-network/agent-provider-testkit": "0.9.0"
|
|
112
|
+
},
|
|
113
|
+
"scripts": {
|
|
114
|
+
"build": "tsc -p tsconfig.json",
|
|
115
|
+
"check-types": "tsc --noEmit",
|
|
116
|
+
"clean": "rm -rf dist",
|
|
117
|
+
"test": "vitest run src"
|
|
127
118
|
}
|
|
128
|
-
}
|
|
119
|
+
}
|