@tangle-network/agent-provider-tangle 0.14.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/dist/tangle-capabilities.js +9 -1
- package/dist/tangle-create-options.d.ts +4 -4
- package/dist/tangle-create-options.js +17 -44
- package/dist/tangle-environment-session.js +0 -1
- package/dist/tangle-environment.js +0 -1
- package/dist/tangle-interaction-response.d.ts +0 -1
- package/dist/tangle-interaction-response.js +9 -7
- package/dist/tangle-prompt.d.ts +1 -1
- package/dist/tangle-prompt.js +2 -0
- package/dist/tangle-provider.js +3 -3
- package/dist/tangle-types.d.ts +2 -0
- package/dist/tangle-workspace-branching.js +85 -8
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# @tangle-network/agent-provider-tangle
|
|
2
2
|
|
|
3
3
|
Wraps `@tangle-network/sandbox` as an `AgentEnvironmentProvider`.
|
|
4
|
-
The peer range is `>=0.34.
|
|
5
|
-
The floor is 0.34.
|
|
6
|
-
lookup, and cleanup operations added to that SDK.
|
|
4
|
+
The peer range is `>=0.34.4 <1.0.0`, and this package is developed and tested against 0.34.4.
|
|
5
|
+
The floor is 0.34.4 because exact interactive attachment needs the host receiver, and workspace branching needs keyed snapshot, fork, lookup, and cleanup operations.
|
|
7
6
|
The provider fails closed when the configured backend or its catalog entry cannot be read.
|
|
8
7
|
Newer SDKs may also provide `getBackend()` as a lookup over the same catalog.
|
|
9
8
|
|
|
@@ -72,7 +72,15 @@ export function defaultTangleSandboxCapabilities(harness) {
|
|
|
72
72
|
eventIdentity: true,
|
|
73
73
|
cancellationIdempotency: true,
|
|
74
74
|
},
|
|
75
|
-
workspace: {
|
|
75
|
+
workspace: {
|
|
76
|
+
read: true,
|
|
77
|
+
write: true,
|
|
78
|
+
exec: true,
|
|
79
|
+
git: false,
|
|
80
|
+
upload: true,
|
|
81
|
+
download: true,
|
|
82
|
+
cwdBases: { repository: true, host: false },
|
|
83
|
+
},
|
|
76
84
|
// The complete Sandbox SDK surface backs this contract. Narrowing below
|
|
77
85
|
// removes every flag when any recovery or cleanup method is absent.
|
|
78
86
|
branching: {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { BackendType, CreateSandboxOptions } from "@tangle-network/sandbox";
|
|
2
|
-
import type { CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
|
|
3
|
-
export declare function sandboxOptionsFromCreateInput(input: CreateAgentEnvironmentInput, defaultBackend: BackendType): CreateSandboxOptions;
|
|
2
|
+
import type { CreateAgentEnvironmentInput, WorkspaceRequest } from "@tangle-network/agent-interface/environment-provider";
|
|
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
|
-
export declare function assertNoInlineSecretValues(input: CreateAgentEnvironmentInput): void;
|
|
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. */
|
|
7
7
|
export declare function assertMappedSecretNames(options: CreateSandboxOptions): void;
|
|
8
|
-
export declare function assertCreateInputShape(input: CreateAgentEnvironmentInput):
|
|
8
|
+
export declare function assertCreateInputShape(input: CreateAgentEnvironmentInput, parsedWorkspace?: WorkspaceRequest): WorkspaceRequest | undefined;
|
|
9
9
|
export declare function assertMappedCreateOptions(options: CreateSandboxOptions): void;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { WorkspaceRequestSchema, workspaceCwdPathForBase, } from "@tangle-network/agent-interface/environment-provider";
|
|
1
2
|
import { assertBoundedJson, boundedIdentifier, boundedString, MAX_ARRAY_LENGTH, MAX_MAP_ENTRIES, } from "./tangle-contract-safety.js";
|
|
2
3
|
import { sandboxResourcesFromResourceRequest } from "./tangle-resources.js";
|
|
3
|
-
export function sandboxOptionsFromCreateInput(input, defaultBackend) {
|
|
4
|
-
assertCreateInputShape(input);
|
|
5
|
-
assertNoInlineSecretValues(input);
|
|
4
|
+
export function sandboxOptionsFromCreateInput(input, defaultBackend, parsedWorkspace) {
|
|
5
|
+
const workspace = assertCreateInputShape(input, parsedWorkspace) ?? {};
|
|
6
|
+
assertNoInlineSecretValues(input, workspace);
|
|
6
7
|
if (input.providerOptions && Object.keys(input.providerOptions).length > 0) {
|
|
7
8
|
throw new Error("Tangle create providerOptions are not supported");
|
|
8
9
|
}
|
|
@@ -19,23 +20,10 @@ export function sandboxOptionsFromCreateInput(input, defaultBackend) {
|
|
|
19
20
|
boundedIdentifier(input.backend, "Tangle backend");
|
|
20
21
|
if (input.env !== undefined)
|
|
21
22
|
assertStringRecord(input.env, "Tangle");
|
|
22
|
-
const workspace = input.workspace ?? {};
|
|
23
23
|
if (workspace.providerOptions && Object.keys(workspace.providerOptions).length > 0) {
|
|
24
24
|
throw new Error("Tangle workspace providerOptions are not supported");
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
assertBoundedRecord(workspace.providerOptions, "Tangle workspace providerOptions");
|
|
28
|
-
if (workspace.environment !== undefined) {
|
|
29
|
-
boundedIdentifier(workspace.environment, "Tangle workspace environment");
|
|
30
|
-
}
|
|
31
|
-
if (workspace.image !== undefined)
|
|
32
|
-
boundedString(workspace.image, "Tangle workspace image");
|
|
33
|
-
if (workspace.cwd !== undefined)
|
|
34
|
-
boundedString(workspace.cwd, "Tangle workspace cwd");
|
|
35
|
-
if (workspace.repoUrl !== undefined)
|
|
36
|
-
boundedString(workspace.repoUrl, "Tangle repository URL");
|
|
37
|
-
if (workspace.gitRef !== undefined)
|
|
38
|
-
boundedIdentifier(workspace.gitRef, "Tangle git ref");
|
|
26
|
+
const workspaceCwd = workspaceCwdPathForBase(workspace.cwd, "repository", "Tangle");
|
|
39
27
|
if (input.resources?.providerOptions && Object.keys(input.resources.providerOptions).length > 0) {
|
|
40
28
|
throw new Error("Tangle resource providerOptions are not supported");
|
|
41
29
|
}
|
|
@@ -44,24 +32,16 @@ export function sandboxOptionsFromCreateInput(input, defaultBackend) {
|
|
|
44
32
|
if (input.idempotencyKey !== undefined) {
|
|
45
33
|
boundedIdentifier(input.idempotencyKey, "Tangle idempotency key");
|
|
46
34
|
}
|
|
47
|
-
if (input.workspace?.cwd === "")
|
|
48
|
-
throw new Error("Tangle workspace cwd cannot be empty");
|
|
49
|
-
if (workspace.image === "")
|
|
50
|
-
throw new Error("Tangle workspace image cannot be empty");
|
|
51
|
-
if (workspace.repoUrl === "")
|
|
52
|
-
throw new Error("Tangle repository URL cannot be empty");
|
|
53
35
|
const resources = sandboxResourcesFromResourceRequest(input.resources);
|
|
54
|
-
if (workspace.environment !== undefined && workspace.image !== undefined) {
|
|
55
|
-
throw new Error("Tangle workspace cannot specify both environment and image");
|
|
56
|
-
}
|
|
57
36
|
// Sandbox injects secrets by name from its own store. Accepting a name/value
|
|
58
37
|
// record and dropping it would create an environment with no credentials and
|
|
59
38
|
// no error, surfacing later as an unexplained tool failure.
|
|
60
39
|
const environment = workspace.image ?? workspace.environment;
|
|
61
40
|
const base = {};
|
|
62
|
-
|
|
41
|
+
const mapped = {
|
|
63
42
|
...base,
|
|
64
43
|
...(environment !== undefined ? { environment } : {}),
|
|
44
|
+
...(workspaceCwd === undefined ? {} : { cwd: workspaceCwd }),
|
|
65
45
|
...(workspace.repoUrl
|
|
66
46
|
? {
|
|
67
47
|
git: {
|
|
@@ -82,9 +62,10 @@ export function sandboxOptionsFromCreateInput(input, defaultBackend) {
|
|
|
82
62
|
profile: inlineAgentProfile(input.profile),
|
|
83
63
|
},
|
|
84
64
|
};
|
|
65
|
+
return mapped;
|
|
85
66
|
}
|
|
86
67
|
/** Reject value-bearing secret maps before any custom mapper can drop them. */
|
|
87
|
-
export function assertNoInlineSecretValues(input) {
|
|
68
|
+
export function assertNoInlineSecretValues(input, parsedWorkspace) {
|
|
88
69
|
if (input.providerOptions !== undefined) {
|
|
89
70
|
if (!input.providerOptions || typeof input.providerOptions !== "object" || Array.isArray(input.providerOptions)) {
|
|
90
71
|
throw new Error("Tangle create providerOptions must be a JSON object");
|
|
@@ -95,11 +76,8 @@ export function assertNoInlineSecretValues(input) {
|
|
|
95
76
|
}
|
|
96
77
|
}
|
|
97
78
|
if (input.workspace?.providerOptions !== undefined) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
assertBoundedJson(input.workspace.providerOptions);
|
|
102
|
-
if (Object.keys(input.workspace.providerOptions).length > 0) {
|
|
79
|
+
const workspace = parsedWorkspace ?? WorkspaceRequestSchema.parse(input.workspace);
|
|
80
|
+
if (workspace.providerOptions && Object.keys(workspace.providerOptions).length > 0) {
|
|
103
81
|
throw new Error("Tangle workspace providerOptions are not supported");
|
|
104
82
|
}
|
|
105
83
|
}
|
|
@@ -136,7 +114,7 @@ export function assertMappedSecretNames(options) {
|
|
|
136
114
|
boundedIdentifier(secret, "Tangle mapped secret name");
|
|
137
115
|
}
|
|
138
116
|
}
|
|
139
|
-
export function assertCreateInputShape(input) {
|
|
117
|
+
export function assertCreateInputShape(input, parsedWorkspace) {
|
|
140
118
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
141
119
|
throw new Error("Tangle create input must be an object");
|
|
142
120
|
}
|
|
@@ -166,16 +144,6 @@ export function assertCreateInputShape(input) {
|
|
|
166
144
|
}
|
|
167
145
|
assertBoundedJson(input.profile);
|
|
168
146
|
}
|
|
169
|
-
if (input.workspace !== undefined) {
|
|
170
|
-
if (!input.workspace || typeof input.workspace !== "object" || Array.isArray(input.workspace)) {
|
|
171
|
-
throw new Error("Tangle workspace must be an object");
|
|
172
|
-
}
|
|
173
|
-
const workspaceKeys = new Set(Object.keys(input.workspace));
|
|
174
|
-
for (const key of ["environment", "image", "repoUrl", "gitRef", "cwd", "providerOptions"])
|
|
175
|
-
workspaceKeys.delete(key);
|
|
176
|
-
if (workspaceKeys.size > 0)
|
|
177
|
-
throw new Error("Tangle workspace contains unsupported fields");
|
|
178
|
-
}
|
|
179
147
|
if (input.resources !== undefined) {
|
|
180
148
|
if (!input.resources || typeof input.resources !== "object" || Array.isArray(input.resources)) {
|
|
181
149
|
throw new Error("Tangle resources must be an object");
|
|
@@ -186,6 +154,11 @@ export function assertCreateInputShape(input) {
|
|
|
186
154
|
if (resourceKeys.size > 0)
|
|
187
155
|
throw new Error("Tangle resources contain unsupported fields");
|
|
188
156
|
}
|
|
157
|
+
if (input.workspace === undefined)
|
|
158
|
+
return undefined;
|
|
159
|
+
const workspace = parsedWorkspace ?? WorkspaceRequestSchema.parse(input.workspace);
|
|
160
|
+
workspaceCwdPathForBase(workspace.cwd, "repository", "Tangle");
|
|
161
|
+
return workspace;
|
|
189
162
|
}
|
|
190
163
|
export function assertMappedCreateOptions(options) {
|
|
191
164
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
@@ -20,20 +20,22 @@ function acknowledge(command, status, extras = {}) {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* The coordinates this environment can answer for
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* The coordinates this environment can answer for before the command reaches
|
|
24
|
+
* the deployment. Tangle hosts a backend adapter inside the sandbox, so the
|
|
25
|
+
* interaction binding's provider names that inner adapter (for example,
|
|
26
|
+
* `opencode`) rather than this outer provider. The deployment compares that
|
|
27
|
+
* provider with its durable interaction record. Environment and session are
|
|
28
|
+
* still checked here because the Sandbox SDK rejects those coordinates before
|
|
29
|
+
* it can return an acknowledgement.
|
|
27
30
|
*/
|
|
28
31
|
function foreignBinding(command, options) {
|
|
29
32
|
const binding = command.binding;
|
|
30
|
-
if (binding.
|
|
31
|
-
binding.environmentId === options.environmentId &&
|
|
33
|
+
if (binding.environmentId === options.environmentId &&
|
|
32
34
|
binding.sessionId === options.sessionId) {
|
|
33
35
|
return undefined;
|
|
34
36
|
}
|
|
35
37
|
return acknowledge(command, "binding_mismatch", {
|
|
36
|
-
message: `response command binding names another
|
|
38
|
+
message: `response command binding names another environment or session than ${options.environmentId}/${options.sessionId}`,
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
/**
|
package/dist/tangle-prompt.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare function promptOptionsFromTurnInput(input: AgentTurnInput, target
|
|
|
8
8
|
environmentId: string;
|
|
9
9
|
sessionId?: string;
|
|
10
10
|
}): PromptOptions;
|
|
11
|
-
type SandboxRunStatus = "success" | "failed" | "blocked_on_approval" | "awaiting_question" | "awaiting_plan_decision";
|
|
11
|
+
type SandboxRunStatus = "success" | "failed" | "blocked_on_approval" | "awaiting_question" | "awaiting_interaction" | "awaiting_plan_decision";
|
|
12
12
|
type ValidatedSandboxPromptResult = Record<string, unknown> & {
|
|
13
13
|
success: boolean;
|
|
14
14
|
status: SandboxRunStatus;
|
package/dist/tangle-prompt.js
CHANGED
|
@@ -97,6 +97,7 @@ export function validatedSandboxPromptResult(result) {
|
|
|
97
97
|
"failed",
|
|
98
98
|
"blocked_on_approval",
|
|
99
99
|
"awaiting_question",
|
|
100
|
+
"awaiting_interaction",
|
|
100
101
|
"awaiting_plan_decision",
|
|
101
102
|
]);
|
|
102
103
|
if (typeof record.status !== "string" ||
|
|
@@ -134,6 +135,7 @@ export function validatedSandboxPromptResult(result) {
|
|
|
134
135
|
const AWAITING_STATUSES = new Set([
|
|
135
136
|
"blocked_on_approval",
|
|
136
137
|
"awaiting_question",
|
|
138
|
+
"awaiting_interaction",
|
|
137
139
|
"awaiting_plan_decision",
|
|
138
140
|
]);
|
|
139
141
|
export function agentTurnResultFromPromptRecord(record, options = {}) {
|
package/dist/tangle-provider.js
CHANGED
|
@@ -43,9 +43,9 @@ export function createTangleProvider(options) {
|
|
|
43
43
|
const resolveCapabilities = async () => narrowedProviderCapabilities(await resolveDeclaredCapabilities());
|
|
44
44
|
const createRecords = new Map();
|
|
45
45
|
const createEnvironment = async (input) => {
|
|
46
|
-
assertCreateInputShape(input);
|
|
46
|
+
const parsedWorkspace = assertCreateInputShape(input);
|
|
47
47
|
input.signal?.throwIfAborted();
|
|
48
|
-
assertNoInlineSecretValues(input);
|
|
48
|
+
assertNoInlineSecretValues(input, parsedWorkspace);
|
|
49
49
|
if (input.providerOptions && Object.keys(input.providerOptions).length > 0) {
|
|
50
50
|
throw new Error("Tangle create providerOptions are not supported");
|
|
51
51
|
}
|
|
@@ -55,7 +55,7 @@ export function createTangleProvider(options) {
|
|
|
55
55
|
const declaredCapabilities = await resolveDeclaredCapabilities();
|
|
56
56
|
narrowedProviderCapabilities(declaredCapabilities);
|
|
57
57
|
const createOptions = options.mapCreateInput?.(input) ??
|
|
58
|
-
sandboxOptionsFromCreateInput(input, options.defaultBackend ?? "opencode");
|
|
58
|
+
sandboxOptionsFromCreateInput(input, options.defaultBackend ?? "opencode", parsedWorkspace);
|
|
59
59
|
assertMappedCreateOptions(createOptions);
|
|
60
60
|
if (input.idempotencyKey !== undefined &&
|
|
61
61
|
createOptions.idempotencyKey !== input.idempotencyKey) {
|
package/dist/tangle-types.d.ts
CHANGED
|
@@ -202,6 +202,8 @@ export interface SandboxWorkspaceOperationLookupLike {
|
|
|
202
202
|
outcome: "found" | "not_found" | "conflict" | "unknown";
|
|
203
203
|
kind: "checkpoint" | "fork";
|
|
204
204
|
state?: "pending" | "succeeded" | "failed";
|
|
205
|
+
/** The durable operation result, when the service records one. */
|
|
206
|
+
result?: Record<string, unknown>;
|
|
205
207
|
}
|
|
206
208
|
/** Fan-out acknowledgement returned by SandboxInstance.fork(). */
|
|
207
209
|
export interface SandboxForkAcknowledgementLike {
|
|
@@ -116,7 +116,7 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
116
116
|
message: "Sandbox fork child identity is incomplete",
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
const environment = await environmentFromChild(recovered.marker.request, child, provider, child.createdAt, options.confidentialAttestationVerifier, signal);
|
|
119
|
+
const environment = await environmentFromChild(recovered.marker.request, child, provider, recovered.createdAt ?? child.createdAt, options.confidentialAttestationVerifier, signal);
|
|
120
120
|
if (!environment) {
|
|
121
121
|
return {
|
|
122
122
|
state: "undecided",
|
|
@@ -699,6 +699,22 @@ function validForkResult(result) {
|
|
|
699
699
|
result.materializedCount === result.children.length &&
|
|
700
700
|
typeof result.complete === "boolean");
|
|
701
701
|
}
|
|
702
|
+
function validOperationRecord(value) {
|
|
703
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
704
|
+
}
|
|
705
|
+
function validOperationDate(value) {
|
|
706
|
+
return ((typeof value === "string" || value instanceof Date) && validDate(value));
|
|
707
|
+
}
|
|
708
|
+
function validSnapshotOperationResult(value) {
|
|
709
|
+
return (validOperationRecord(value) &&
|
|
710
|
+
safeIdentifier(value.snapshotId) !== undefined &&
|
|
711
|
+
validOperationDate(value.createdAt));
|
|
712
|
+
}
|
|
713
|
+
function validForkOperationChildResult(value) {
|
|
714
|
+
return (validOperationRecord(value) &&
|
|
715
|
+
safeIdentifier(value.sandboxId ?? value.id) !== undefined &&
|
|
716
|
+
validOperationDate(value.createdAt));
|
|
717
|
+
}
|
|
702
718
|
function checkpointMarkerTags(request) {
|
|
703
719
|
const marker = {
|
|
704
720
|
version: 1,
|
|
@@ -755,22 +771,30 @@ function forkMarkerMetadata(request) {
|
|
|
755
771
|
* A marker only names a candidate resource. Nothing is returned to a caller
|
|
756
772
|
* until the ledger reports the operation succeeded.
|
|
757
773
|
*/
|
|
758
|
-
async function
|
|
774
|
+
async function checkpointOperationLookup(box, marker, signal) {
|
|
759
775
|
const lookup = await awaitWithSignal(box.getSnapshotOperation?.(marker.idempotencyKey, {
|
|
760
776
|
tags: marker.legacy
|
|
761
777
|
? legacyCheckpointMarkerTags(marker.request)
|
|
762
778
|
: checkpointMarkerTags(marker.request),
|
|
763
779
|
}), signal);
|
|
780
|
+
return lookup;
|
|
781
|
+
}
|
|
782
|
+
async function checkpointOperationSucceeded(box, marker, signal) {
|
|
783
|
+
const lookup = await checkpointOperationLookup(box, marker, signal);
|
|
764
784
|
return (lookup?.outcome === "found" &&
|
|
765
785
|
lookup.kind === "checkpoint" &&
|
|
766
786
|
lookup.state === "succeeded");
|
|
767
787
|
}
|
|
768
788
|
/** The fork equivalent of {@link checkpointOperationSucceeded}. */
|
|
769
|
-
async function
|
|
789
|
+
async function forkOperationLookup(box, marker, signal) {
|
|
770
790
|
const lookup = await awaitWithSignal(box.getForkOperation?.(marker.idempotencyKey, {
|
|
771
791
|
count: 1,
|
|
772
792
|
metadata: forkMarkerMetadata(marker.request),
|
|
773
793
|
}), signal);
|
|
794
|
+
return lookup;
|
|
795
|
+
}
|
|
796
|
+
async function forkOperationSucceeded(box, marker, signal) {
|
|
797
|
+
const lookup = await forkOperationLookup(box, marker, signal);
|
|
774
798
|
return (lookup?.outcome === "found" &&
|
|
775
799
|
lookup.kind === "fork" &&
|
|
776
800
|
lookup.state === "succeeded");
|
|
@@ -818,8 +842,14 @@ async function findCheckpointByKey(box, key, signal) {
|
|
|
818
842
|
if (!marker)
|
|
819
843
|
continue;
|
|
820
844
|
try {
|
|
821
|
-
|
|
822
|
-
|
|
845
|
+
const lookup = await checkpointOperationLookup(box, marker, signal);
|
|
846
|
+
if (lookup?.outcome === "found" &&
|
|
847
|
+
lookup.kind === "checkpoint" &&
|
|
848
|
+
lookup.state === "succeeded") {
|
|
849
|
+
const authoritative = snapshotFromOperationResult(snapshot, lookup);
|
|
850
|
+
if (authoritative === undefined)
|
|
851
|
+
return undefined;
|
|
852
|
+
return { snapshot: authoritative, marker };
|
|
823
853
|
}
|
|
824
854
|
unresolved = true;
|
|
825
855
|
}
|
|
@@ -830,6 +860,23 @@ async function findCheckpointByKey(box, key, signal) {
|
|
|
830
860
|
}
|
|
831
861
|
return unresolved ? undefined : null;
|
|
832
862
|
}
|
|
863
|
+
/**
|
|
864
|
+
* Prefer the durable operation result over inventory metadata.
|
|
865
|
+
*
|
|
866
|
+
* Snapshot inventory and the operation ledger can expose different creation
|
|
867
|
+
* timestamps. The ledger result is the acknowledgement returned by the
|
|
868
|
+
* idempotent operation, so recovery must rebuild the exact checkpoint ref
|
|
869
|
+
* from it when the service provides that result.
|
|
870
|
+
*/
|
|
871
|
+
function snapshotFromOperationResult(snapshot, lookup) {
|
|
872
|
+
if (lookup.result === undefined)
|
|
873
|
+
return snapshot;
|
|
874
|
+
if (!validSnapshotOperationResult(lookup.result) ||
|
|
875
|
+
lookup.result.snapshotId !== snapshot.snapshotId) {
|
|
876
|
+
return undefined;
|
|
877
|
+
}
|
|
878
|
+
return { ...snapshot, createdAt: lookup.result.createdAt };
|
|
879
|
+
}
|
|
833
880
|
/**
|
|
834
881
|
* Confirm that one snapshot id is a settled checkpoint this provider created.
|
|
835
882
|
*
|
|
@@ -877,8 +924,15 @@ async function findForkByKey(client, box, provider, key, signal) {
|
|
|
877
924
|
let unresolved = false;
|
|
878
925
|
for (const candidate of candidates) {
|
|
879
926
|
try {
|
|
880
|
-
|
|
881
|
-
|
|
927
|
+
const lookup = await forkOperationLookup(box, candidate.marker, signal);
|
|
928
|
+
if (lookup?.outcome === "found" &&
|
|
929
|
+
lookup.kind === "fork" &&
|
|
930
|
+
lookup.state === "succeeded") {
|
|
931
|
+
const authoritative = childFromOperationResult(candidate.child, lookup);
|
|
932
|
+
if (authoritative === undefined)
|
|
933
|
+
return undefined;
|
|
934
|
+
return { ...authoritative, marker: candidate.marker };
|
|
935
|
+
}
|
|
882
936
|
unresolved = true;
|
|
883
937
|
}
|
|
884
938
|
catch {
|
|
@@ -888,6 +942,29 @@ async function findForkByKey(client, box, provider, key, signal) {
|
|
|
888
942
|
}
|
|
889
943
|
return unresolved ? undefined : null;
|
|
890
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* Prefer the durable fork result over account-inventory metadata.
|
|
947
|
+
*
|
|
948
|
+
* Fork inventory can report a child timestamp from a later registry read. The
|
|
949
|
+
* operation ledger stores the original child acknowledgement, which is the
|
|
950
|
+
* stable value required to replay one exact fork reference after a restart.
|
|
951
|
+
*/
|
|
952
|
+
function childFromOperationResult(child, lookup) {
|
|
953
|
+
if (lookup.result === undefined) {
|
|
954
|
+
return { child, createdAt: child.createdAt };
|
|
955
|
+
}
|
|
956
|
+
const result = lookup.result;
|
|
957
|
+
if (!validOperationRecord(result))
|
|
958
|
+
return undefined;
|
|
959
|
+
const children = result.children;
|
|
960
|
+
if (!Array.isArray(children))
|
|
961
|
+
return undefined;
|
|
962
|
+
const operationChild = children.find((candidate) => validForkOperationChildResult(candidate) &&
|
|
963
|
+
(candidate.sandboxId ?? candidate.id) === child.id);
|
|
964
|
+
if (!operationChild)
|
|
965
|
+
return undefined;
|
|
966
|
+
return { child, createdAt: operationChild.createdAt };
|
|
967
|
+
}
|
|
891
968
|
async function findForkChildById(client, box, provider, id, signal) {
|
|
892
969
|
try {
|
|
893
970
|
if (typeof client.get !== "function")
|
|
@@ -1229,7 +1306,7 @@ async function forkConflictFromRemote(client, box, provider, request, verifier,
|
|
|
1229
1306
|
const child = await completeForkChild(client, recovered.child, signal);
|
|
1230
1307
|
if (!child)
|
|
1231
1308
|
return undefined;
|
|
1232
|
-
const environment = await environmentFromChild(recovered.marker.request, child, provider, child.createdAt, verifier, signal);
|
|
1309
|
+
const environment = await environmentFromChild(recovered.marker.request, child, provider, recovered.createdAt ?? child.createdAt, verifier, signal);
|
|
1233
1310
|
return environment === undefined
|
|
1234
1311
|
? undefined
|
|
1235
1312
|
: forkSuccess(request, environment, "replayed");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-tangle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for Tangle sandboxes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -89,10 +89,10 @@
|
|
|
89
89
|
"LICENSE"
|
|
90
90
|
],
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@tangle-network/agent-interface": "^
|
|
92
|
+
"@tangle-network/agent-interface": "^2.0.0"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@tangle-network/sandbox": ">=0.34.
|
|
95
|
+
"@tangle-network/sandbox": ">=0.34.6 <1.0.0"
|
|
96
96
|
},
|
|
97
97
|
"peerDependenciesMeta": {
|
|
98
98
|
"@tangle-network/sandbox": {
|
|
@@ -101,12 +101,12 @@
|
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
103
|
"@tangle-network/agent-eval": "0.170.0",
|
|
104
|
-
"@tangle-network/agent-runtime": "0.
|
|
105
|
-
"@tangle-network/sandbox": "0.34.
|
|
104
|
+
"@tangle-network/agent-runtime": "0.184.0",
|
|
105
|
+
"@tangle-network/sandbox": "0.34.6",
|
|
106
106
|
"@types/node": "26.4.0",
|
|
107
107
|
"typescript": "7.0.2",
|
|
108
108
|
"vitest": "4.1.11",
|
|
109
|
-
"@tangle-network/agent-provider-testkit": "0.8.
|
|
109
|
+
"@tangle-network/agent-provider-testkit": "0.8.6"
|
|
110
110
|
},
|
|
111
111
|
"scripts": {
|
|
112
112
|
"build": "tsc -p tsconfig.json",
|