@tangle-network/agent-provider-tangle 0.5.0 → 0.6.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.
Files changed (41) hide show
  1. package/dist/exact-process.d.ts +1 -4
  2. package/dist/exact-process.js +123 -206
  3. package/dist/index.d.ts +4 -126
  4. package/dist/index.js +3 -687
  5. package/dist/tangle-capabilities.d.ts +39 -0
  6. package/dist/tangle-capabilities.js +140 -0
  7. package/dist/tangle-contract-safety.d.ts +19 -0
  8. package/dist/tangle-contract-safety.js +240 -0
  9. package/dist/tangle-create-options.d.ts +9 -0
  10. package/dist/tangle-create-options.js +243 -0
  11. package/dist/tangle-environment-control.d.ts +6 -0
  12. package/dist/tangle-environment-control.js +50 -0
  13. package/dist/tangle-environment-dispatch.d.ts +3 -0
  14. package/dist/tangle-environment-dispatch.js +60 -0
  15. package/dist/tangle-environment-session.d.ts +4 -0
  16. package/dist/tangle-environment-session.js +156 -0
  17. package/dist/tangle-environment-validation.d.ts +11 -0
  18. package/dist/tangle-environment-validation.js +63 -0
  19. package/dist/tangle-environment-values.d.ts +8 -0
  20. package/dist/tangle-environment-values.js +84 -0
  21. package/dist/tangle-environment.d.ts +3 -0
  22. package/dist/tangle-environment.js +216 -0
  23. package/dist/tangle-events.d.ts +6 -0
  24. package/dist/tangle-events.js +111 -0
  25. package/dist/tangle-exact-process-environment.d.ts +3 -0
  26. package/dist/tangle-exact-process-environment.js +184 -0
  27. package/dist/tangle-exact-process-runtime.d.ts +5 -0
  28. package/dist/tangle-exact-process-runtime.js +150 -0
  29. package/dist/tangle-exact-process-validation.d.ts +17 -0
  30. package/dist/tangle-exact-process-validation.js +123 -0
  31. package/dist/tangle-prompt.d.ts +24 -0
  32. package/dist/tangle-prompt.js +166 -0
  33. package/dist/tangle-provider.d.ts +3 -0
  34. package/dist/tangle-provider.js +192 -0
  35. package/dist/tangle-result-values.d.ts +5 -0
  36. package/dist/tangle-result-values.js +94 -0
  37. package/dist/tangle-session-control.d.ts +7 -0
  38. package/dist/tangle-session-control.js +89 -0
  39. package/dist/tangle-types.d.ts +141 -0
  40. package/dist/tangle-types.js +1 -0
  41. package/package.json +39 -3
@@ -0,0 +1,84 @@
1
+ import { assertBoundedJson } from "./tangle-contract-safety.js";
2
+ const MAX_IDENTIFIER_LENGTH = 512;
3
+ export function nonEmptyString(value) {
4
+ return typeof value === "string" &&
5
+ value.length > 0 &&
6
+ value.length <= MAX_IDENTIFIER_LENGTH &&
7
+ value.trim() === value
8
+ ? value
9
+ : undefined;
10
+ }
11
+ export function optionalNonEmptyString(value, label) {
12
+ if (value === undefined)
13
+ return undefined;
14
+ if (typeof value !== "string" ||
15
+ value.length === 0 ||
16
+ value.length > MAX_IDENTIFIER_LENGTH ||
17
+ value.trim() !== value) {
18
+ throw new Error(`${label} must be a non-empty string`);
19
+ }
20
+ return value;
21
+ }
22
+ export function checkpointIdFromResult(result) {
23
+ assertBoundedJson(result);
24
+ const record = result && typeof result === "object" ? result : {};
25
+ const id = record.checkpointId ?? record.id;
26
+ if (typeof id !== "string" ||
27
+ id.length === 0 ||
28
+ id.length > MAX_IDENTIFIER_LENGTH ||
29
+ id.trim() !== id) {
30
+ throw new Error("sandbox checkpoint returned no checkpoint id");
31
+ }
32
+ return id;
33
+ }
34
+ export function placementInfoFromLoopPlacement(placement, box) {
35
+ if (!placement || typeof placement !== "object") {
36
+ return { kind: "sandbox", sandboxId: boundedId(box.id, "sandbox id") };
37
+ }
38
+ const record = placement;
39
+ if (record.kind !== "sandbox" && record.kind !== "fleet") {
40
+ throw new Error("Tangle placement returned an unsupported kind");
41
+ }
42
+ return {
43
+ kind: record.kind === "fleet" ? "fleet" : "sandbox",
44
+ sandboxId: record.kind === "fleet"
45
+ ? undefined
46
+ : boundedOptionalId(record.sandboxId, "sandbox id") ??
47
+ boundedId(box.id, "sandbox id"),
48
+ ...(record.kind === "fleet"
49
+ ? { fleetId: boundedId(record.fleetId, "fleet id") }
50
+ : {}),
51
+ ...(record.machineId !== undefined
52
+ ? { machineId: boundedId(record.machineId, "machine id") }
53
+ : {}),
54
+ ...(record.region !== undefined
55
+ ? { region: boundedId(record.region, "region") }
56
+ : {}),
57
+ };
58
+ }
59
+ function boundedId(value, label) {
60
+ if (typeof value !== "string" ||
61
+ value.length === 0 ||
62
+ value.length > MAX_IDENTIFIER_LENGTH ||
63
+ value.trim() !== value) {
64
+ throw new Error(`${label} must be a bounded non-empty string`);
65
+ }
66
+ return value;
67
+ }
68
+ function boundedOptionalId(value, label) {
69
+ return value === undefined ? undefined : boundedId(value, label);
70
+ }
71
+ export function statusFromUnknown(status) {
72
+ if (status === "pending" || status === "provisioning" || status === "running")
73
+ return status;
74
+ if (status === "stopped" || status === "failed" || status === "expired")
75
+ return status;
76
+ if (status === "completed" || status === "cancelled")
77
+ return "stopped";
78
+ return "unknown";
79
+ }
80
+ export function sessionStatusFromUnknown(status) {
81
+ if (status === "completed" || status === "cancelled")
82
+ return status;
83
+ return statusFromUnknown(status);
84
+ }
@@ -0,0 +1,3 @@
1
+ import type { AgentEnvironment, AgentEnvironmentCapabilities } from "@tangle-network/agent-interface/environment-provider";
2
+ import type { SandboxClientLike, SandboxInstanceLike } from "./tangle-types.js";
3
+ export declare function sandboxInstanceAsEnvironment(box: SandboxInstanceLike, providerName: string, client: SandboxClientLike, declaredCapabilities: AgentEnvironmentCapabilities): AgentEnvironment;
@@ -0,0 +1,216 @@
1
+ import { AgentTurnInputSchema } from "@tangle-network/agent-interface";
2
+ import { environmentEventFromSandboxEvent } from "./tangle-events.js";
3
+ import { executionIdFromTurnInput, promptFromTurnInput, promptOptionsFromTurnInput, } from "./tangle-prompt.js";
4
+ import { resolveRetainedSessionControlRef } from "./tangle-session-control.js";
5
+ import { checkpointIdFromResult, placementInfoFromLoopPlacement, statusFromUnknown, } from "./tangle-environment-values.js";
6
+ import { execResultFromSandboxExecResult } from "./tangle-result-values.js";
7
+ import { capabilitiesForSandbox, sandboxCapabilitySupport } from "./tangle-capabilities.js";
8
+ import { attachCleanupHandle, awaitWithSignal, assertBoundedJson, boundedIdentifier, boundedString, } from "./tangle-contract-safety.js";
9
+ import { assertCheckpointOptions, assertCheckpointRef, assertExecOptions, assertForkOptions, assertOptionKeys, } from "./tangle-environment-validation.js";
10
+ import { interruptExecutionAfterAbort, } from "./tangle-environment-control.js";
11
+ import { dispatchEnvironmentRun } from "./tangle-environment-dispatch.js";
12
+ import { sandboxSessionAsAgentSession } from "./tangle-environment-session.js";
13
+ export function sandboxInstanceAsEnvironment(box, providerName, client, declaredCapabilities) {
14
+ const environmentId = boundedIdentifier(box.id, "Tangle environment id");
15
+ boundedIdentifier(providerName, "Tangle provider name");
16
+ if (box.metadata !== undefined) {
17
+ if (!box.metadata || typeof box.metadata !== "object" || Array.isArray(box.metadata)) {
18
+ throw new Error("Tangle environment metadata must be a JSON object");
19
+ }
20
+ assertBoundedJson(box.metadata);
21
+ }
22
+ const support = sandboxCapabilitySupport(box, client);
23
+ const capabilities = capabilitiesForSandbox(declaredCapabilities, support);
24
+ return {
25
+ id: environmentId,
26
+ provider: providerName,
27
+ ...(box.name ? { name: boundedString(box.name, "Tangle environment name") } : {}),
28
+ async status(options) {
29
+ assertOptionKeys(options, ["signal"], "Tangle environment status");
30
+ await awaitWithSignal(box.refresh?.(options), options?.signal);
31
+ return statusFromUnknown(box.status);
32
+ },
33
+ async *stream(input) {
34
+ AgentTurnInputSchema.parse(input);
35
+ input.signal?.throwIfAborted();
36
+ const expectedExecutionId = executionIdFromTurnInput(input);
37
+ const expectedSessionId = input.sessionId ?? input.controlRef?.sessionId;
38
+ const iterator = box.streamPrompt(promptFromTurnInput(input), promptOptionsFromTurnInput(input, {
39
+ provider: providerName,
40
+ environmentId,
41
+ }))[Symbol.asyncIterator]();
42
+ let completed = false;
43
+ try {
44
+ while (true) {
45
+ const next = await awaitWithSignal(iterator.next(), input.signal);
46
+ if (next.done) {
47
+ completed = true;
48
+ break;
49
+ }
50
+ input.signal?.throwIfAborted();
51
+ const converted = environmentEventFromSandboxEvent(next.value, {
52
+ executionId: expectedExecutionId,
53
+ sessionId: expectedSessionId,
54
+ });
55
+ input.signal?.throwIfAborted();
56
+ yield converted;
57
+ }
58
+ }
59
+ catch (error) {
60
+ if (input.signal?.aborted &&
61
+ expectedSessionId !== undefined &&
62
+ expectedExecutionId !== undefined) {
63
+ void interruptExecutionAfterAbort(box, expectedSessionId, expectedExecutionId);
64
+ }
65
+ throw error;
66
+ }
67
+ finally {
68
+ if (!completed) {
69
+ void Promise.resolve(iterator.return?.()).catch(() => undefined);
70
+ }
71
+ }
72
+ input.signal?.throwIfAborted();
73
+ },
74
+ ...(capabilities.streaming.detach && box.dispatchPrompt
75
+ ? { dispatch: dispatchEnvironmentRun(box, providerName, environmentId) }
76
+ : {}),
77
+ ...((capabilities.sessions.continue || capabilities.streaming.replay || capabilities.streaming.detach) &&
78
+ box.session
79
+ ? {
80
+ session(id, options) {
81
+ boundedIdentifier(id, "Tangle session id");
82
+ assertOptionKeys(options, ["controlRef", "signal"], "Tangle session");
83
+ options?.signal?.throwIfAborted();
84
+ const session = box.session?.(id, options?.signal ? { signal: options.signal } : undefined);
85
+ if (!session)
86
+ throw new Error("sandbox session(id) returned undefined");
87
+ options?.signal?.throwIfAborted();
88
+ boundedIdentifier(session.id, "Tangle session id");
89
+ if (session.id !== id) {
90
+ throw new Error("sandbox session(id) returned an unrelated session");
91
+ }
92
+ return sandboxSessionAsAgentSession(session, resolveRetainedSessionControlRef(options?.controlRef, id, providerName, environmentId), providerName, environmentId);
93
+ },
94
+ }
95
+ : {}),
96
+ ...(capabilities.workspace.read && box.read
97
+ ? {
98
+ async read(path, options) {
99
+ boundedString(path, "Tangle path");
100
+ assertOptionKeys(options, ["sessionId", "signal"], "Tangle read");
101
+ if (options?.sessionId !== undefined)
102
+ boundedIdentifier(options.sessionId, "Tangle read session id");
103
+ options?.signal?.throwIfAborted();
104
+ const content = await awaitWithSignal(box.read?.(path, options), options?.signal);
105
+ options?.signal?.throwIfAborted();
106
+ return boundedString(content, "Tangle file content");
107
+ },
108
+ }
109
+ : {}),
110
+ ...(capabilities.workspace.write && box.write
111
+ ? {
112
+ async write(path, content, options) {
113
+ boundedString(path, "Tangle path");
114
+ boundedString(content, "Tangle file content");
115
+ assertOptionKeys(options, ["sessionId", "signal"], "Tangle write");
116
+ if (options?.sessionId !== undefined)
117
+ boundedIdentifier(options.sessionId, "Tangle write session id");
118
+ options?.signal?.throwIfAborted();
119
+ await awaitWithSignal(box.write?.(path, content, options), options?.signal);
120
+ options?.signal?.throwIfAborted();
121
+ },
122
+ }
123
+ : {}),
124
+ ...(capabilities.workspace.exec && box.exec
125
+ ? {
126
+ async exec(command, options) {
127
+ boundedString(command, "Tangle command");
128
+ assertExecOptions(options);
129
+ options?.signal?.throwIfAborted();
130
+ const result = await awaitWithSignal(box.exec?.(command, options), options?.signal);
131
+ options?.signal?.throwIfAborted();
132
+ return execResultFromSandboxExecResult(result);
133
+ },
134
+ }
135
+ : {}),
136
+ ...(capabilities.branching.checkpoint && box.checkpoint
137
+ ? {
138
+ async checkpoint(options) {
139
+ assertCheckpointOptions(options);
140
+ options?.signal?.throwIfAborted();
141
+ const result = await awaitWithSignal(box.checkpoint?.(options), options?.signal);
142
+ options?.signal?.throwIfAborted();
143
+ return { id: checkpointIdFromResult(result), provider: providerName };
144
+ },
145
+ }
146
+ : {}),
147
+ ...(capabilities.branching.fork && box.fork
148
+ ? {
149
+ async fork(checkpoint, options) {
150
+ assertCheckpointRef(checkpoint);
151
+ assertForkOptions(options);
152
+ if (checkpoint.provider !== undefined && checkpoint.provider !== providerName) {
153
+ throw new Error("Tangle fork checkpoint belongs to another provider");
154
+ }
155
+ boundedIdentifier(checkpoint.id, "Tangle checkpoint id");
156
+ options?.signal?.throwIfAborted();
157
+ const forked = await awaitWithSignal(box.fork?.(checkpoint.id, options), options?.signal);
158
+ if (!forked)
159
+ throw new Error("sandbox fork returned no environment");
160
+ try {
161
+ options?.signal?.throwIfAborted();
162
+ if (boundedIdentifier(forked.id, "Tangle fork environment id") === environmentId) {
163
+ throw new Error("Tangle fork returned the source environment");
164
+ }
165
+ return sandboxInstanceAsEnvironment(forked, providerName, client, capabilities);
166
+ }
167
+ catch (error) {
168
+ if (!forked.delete) {
169
+ const baseError = error instanceof Error ? error : new Error(String(error));
170
+ attachCleanupHandle(baseError, forked);
171
+ throw baseError;
172
+ }
173
+ try {
174
+ await forked.delete();
175
+ }
176
+ catch (cleanupError) {
177
+ const combined = new AggregateError([error, cleanupError], "Tangle fork validation and cleanup both failed");
178
+ attachCleanupHandle(combined, forked, cleanupError);
179
+ throw combined;
180
+ }
181
+ throw error;
182
+ }
183
+ },
184
+ }
185
+ : {}),
186
+ ...(capabilities.placement
187
+ ? {
188
+ async placement(options) {
189
+ assertOptionKeys(options, ["signal"], "Tangle placement");
190
+ options?.signal?.throwIfAborted();
191
+ const placement = await awaitWithSignal(Promise.resolve(client.describePlacement?.(box)), options?.signal);
192
+ options?.signal?.throwIfAborted();
193
+ return placementInfoFromLoopPlacement(placement, box);
194
+ },
195
+ }
196
+ : {}),
197
+ async refresh(options) {
198
+ assertOptionKeys(options, ["signal"], "Tangle refresh");
199
+ options?.signal?.throwIfAborted();
200
+ await awaitWithSignal(box.refresh?.(options), options?.signal);
201
+ options?.signal?.throwIfAborted();
202
+ },
203
+ ...(support.destroy
204
+ ? {
205
+ async destroy(options) {
206
+ if (!box.delete)
207
+ throw new Error("Tangle sandbox client cannot delete this environment");
208
+ assertOptionKeys(options, ["signal"], "Tangle destroy");
209
+ options?.signal?.throwIfAborted();
210
+ await awaitWithSignal(box.delete(options), options?.signal);
211
+ options?.signal?.throwIfAborted();
212
+ },
213
+ }
214
+ : {}),
215
+ };
216
+ }
@@ -0,0 +1,6 @@
1
+ import type { SandboxEvent } from "@tangle-network/sandbox";
2
+ import type { AgentEnvironmentEvent } from "@tangle-network/agent-interface/environment-provider";
3
+ export declare function environmentEventFromSandboxEvent(event: SandboxEvent, expected?: {
4
+ executionId?: string;
5
+ sessionId?: string;
6
+ }): AgentEnvironmentEvent;
@@ -0,0 +1,111 @@
1
+ import { assertBoundedJson } from "./tangle-contract-safety.js";
2
+ import { optionalNonEmptyString } from "./tangle-environment-values.js";
3
+ import { tokenUsageFromData } from "./tangle-result-values.js";
4
+ export function environmentEventFromSandboxEvent(event, expected = {}) {
5
+ if (!event || typeof event !== "object") {
6
+ throw new Error("Tangle Sandbox emitted a non-object event");
7
+ }
8
+ const record = event;
9
+ if (typeof record.type !== "string" || record.type.length === 0) {
10
+ throw new Error("Tangle Sandbox event omitted its type");
11
+ }
12
+ if (record.type.length > 512 || record.type.trim() !== record.type) {
13
+ throw new Error("Tangle Sandbox event type exceeded its bound");
14
+ }
15
+ if (!record.data ||
16
+ typeof record.data !== "object" ||
17
+ Array.isArray(record.data)) {
18
+ throw new Error("Tangle Sandbox event omitted its object data");
19
+ }
20
+ if (record.id !== undefined &&
21
+ (typeof record.id !== "string" ||
22
+ record.id.length === 0 ||
23
+ record.id.length > 512 ||
24
+ record.id.trim() !== record.id)) {
25
+ throw new Error("Tangle Sandbox event contained an invalid event id");
26
+ }
27
+ const data = record.data;
28
+ assertBoundedRecord(data);
29
+ assertBoundedJson(record);
30
+ if (Object.prototype.hasOwnProperty.call(data, "contextTransferReceipt")) {
31
+ throw new Error("Tangle Sandbox emitted an unsolicited context transfer receipt");
32
+ }
33
+ const eventExecutionId = optionalNonEmptyString(data.executionId, "Tangle Sandbox event executionId");
34
+ const eventSessionId = optionalNonEmptyString(data.sessionId, "Tangle Sandbox event sessionId");
35
+ if (expected.executionId !== undefined &&
36
+ (eventExecutionId === undefined || eventExecutionId !== expected.executionId)) {
37
+ throw new Error("Tangle exact session event identified a different executionId");
38
+ }
39
+ if (expected.sessionId !== undefined &&
40
+ (eventSessionId === undefined || eventSessionId !== expected.sessionId)) {
41
+ throw new Error("Tangle exact session event identified a different sessionId");
42
+ }
43
+ const usage = tokenUsageFromData(data);
44
+ return {
45
+ type: record.type,
46
+ data,
47
+ ...(typeof record.id === "string" ? { id: record.id } : {}),
48
+ // Absent rather than zeroed: an event that reported no usage must not
49
+ // contribute a total to whatever sums these events.
50
+ ...(usage ? { usage } : {}),
51
+ providerEvent: event,
52
+ };
53
+ }
54
+ function assertBoundedRecord(value) {
55
+ if (Object.keys(value).length > 256) {
56
+ throw new Error("Tangle Sandbox event data has too many fields");
57
+ }
58
+ const pending = [
59
+ { value, depth: 0 },
60
+ ];
61
+ const ancestors = new Set();
62
+ let nodes = 0;
63
+ while (pending.length > 0) {
64
+ const item = pending.pop();
65
+ if (!item)
66
+ continue;
67
+ nodes += 1;
68
+ if (nodes > 8_192)
69
+ throw new Error("Tangle Sandbox event data has too many JSON nodes");
70
+ const current = item.value;
71
+ if (item.leave) {
72
+ ancestors.delete(current);
73
+ continue;
74
+ }
75
+ if (current === null || typeof current === "boolean")
76
+ continue;
77
+ if (typeof current === "string" || typeof current === "number") {
78
+ if (typeof current === "string" && current.length > 16_384) {
79
+ throw new Error("Tangle Sandbox event data exceeded its string bound");
80
+ }
81
+ if (typeof current === "number" && !Number.isFinite(current)) {
82
+ throw new Error("Tangle Sandbox event data contained a non-finite number");
83
+ }
84
+ continue;
85
+ }
86
+ if (typeof current !== "object" || item.depth >= 16 || ancestors.has(current)) {
87
+ throw new Error("Tangle Sandbox event data exceeded its JSON bound");
88
+ }
89
+ ancestors.add(current);
90
+ pending.push({ value: current, depth: item.depth, leave: true });
91
+ if (Array.isArray(current)) {
92
+ if (current.length > 1_024) {
93
+ throw new Error("Tangle Sandbox event data has too many array entries");
94
+ }
95
+ for (const entry of current)
96
+ pending.push({ value: entry, depth: item.depth + 1 });
97
+ continue;
98
+ }
99
+ if (Object.getPrototypeOf(current) !== Object.prototype && Object.getPrototypeOf(current) !== null) {
100
+ throw new Error("Tangle Sandbox event data must be plain JSON");
101
+ }
102
+ const keys = Object.keys(current);
103
+ if (keys.length > 256)
104
+ throw new Error("Tangle Sandbox event map is too large");
105
+ for (const key of keys) {
106
+ if (key.length > 512)
107
+ throw new Error("Tangle Sandbox event key is too long");
108
+ pending.push({ value: current[key], depth: item.depth + 1 });
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,3 @@
1
+ import type { AgentExactProcessEnvironment } from "@tangle-network/agent-interface/environment-provider";
2
+ import type { SandboxInstanceLike } from "./tangle-types.js";
3
+ export declare function sandboxInstanceAsExactProcessEnvironment(box: SandboxInstanceLike, providerName: string): AgentExactProcessEnvironment;
@@ -0,0 +1,184 @@
1
+ import { attachCleanupHandle, awaitWithSignal, MAX_EXACT_FILE_BYTES, MAX_LIST_RESULTS, } from "./tangle-contract-safety.js";
2
+ import { exactProcessStatusFromSandbox, sandboxProcessAsExactProcess, validateExactProcessLaunch, } from "./tangle-exact-process-runtime.js";
3
+ import { assertAbsoluteFilePath, assertFileOptions, assertSignalOptions, } from "./tangle-exact-process-validation.js";
4
+ export function sandboxInstanceAsExactProcessEnvironment(box, providerName) {
5
+ if (!box.fs ||
6
+ box.fs.supportsWriteMode !== true ||
7
+ !box.process ||
8
+ !box.delete) {
9
+ throw new Error("Tangle sandbox does not expose exact files, processes, and deletion");
10
+ }
11
+ const process = box.process;
12
+ const fs = box.fs;
13
+ const destroy = box.delete.bind(box);
14
+ return {
15
+ id: box.id,
16
+ provider: providerName,
17
+ ...(box.metadata ? { metadata: box.metadata } : {}),
18
+ process: {
19
+ async list(options = {}) {
20
+ assertSignalOptions(options, "Tangle exact process list");
21
+ options.signal?.throwIfAborted();
22
+ const statuses = await awaitWithSignal(process.list(), options.signal);
23
+ options.signal?.throwIfAborted();
24
+ if (!Array.isArray(statuses) || statuses.length > MAX_LIST_RESULTS) {
25
+ throw new Error("Tangle exact process status list exceeded its result bound");
26
+ }
27
+ return statuses.map(exactProcessStatusFromSandbox);
28
+ },
29
+ async get(pid, options = {}) {
30
+ if (!Number.isSafeInteger(pid) || pid < 1) {
31
+ throw new Error("Tangle exact process pid is invalid");
32
+ }
33
+ assertSignalOptions(options, "Tangle exact process get");
34
+ options.signal?.throwIfAborted();
35
+ const handle = await awaitWithSignal(process.get(pid), options.signal);
36
+ options.signal?.throwIfAborted();
37
+ if (!handle || handle.pid !== pid)
38
+ return null;
39
+ return sandboxProcessAsExactProcess(handle);
40
+ },
41
+ async spawn(launch, operation = {}) {
42
+ assertSignalOptions(operation, "Tangle exact process spawn");
43
+ operation.signal?.throwIfAborted();
44
+ validateExactProcessLaunch(launch);
45
+ let handle;
46
+ const spawnPromise = process.spawnExact(launch.executable, launch.args, {
47
+ cwd: launch.cwd,
48
+ env: { ...launch.env },
49
+ inheritEnv: false,
50
+ ...(launch.stdin === undefined ? {} : { stdin: launch.stdin }),
51
+ timeoutMs: launch.timeoutMs,
52
+ ...(operation.signal ? { signal: operation.signal } : {}),
53
+ });
54
+ try {
55
+ handle = await awaitWithSignal(spawnPromise, operation.signal);
56
+ operation.signal?.throwIfAborted();
57
+ return sandboxProcessAsExactProcess(handle);
58
+ }
59
+ catch (error) {
60
+ if (!handle && operation.signal?.aborted) {
61
+ void spawnPromise
62
+ .then(async (lateHandle) => {
63
+ try {
64
+ await lateHandle.kill("SIGKILL", { tree: true });
65
+ }
66
+ catch (cleanupError) {
67
+ attachCleanupHandle(error, lateHandle, cleanupError);
68
+ }
69
+ })
70
+ .catch((lateError) => attachCleanupHandle(error, undefined, lateError));
71
+ throw error;
72
+ }
73
+ if (!handle)
74
+ throw error;
75
+ try {
76
+ await handle.kill("SIGKILL", { tree: true });
77
+ }
78
+ catch (cleanupError) {
79
+ throw new AggregateError([error, cleanupError], "Tangle exact process spawn and cleanup both failed");
80
+ }
81
+ throw error;
82
+ }
83
+ },
84
+ },
85
+ async writeFile(path, bytes, options) {
86
+ assertFileOptions(options, "Tangle exact process write");
87
+ options.signal?.throwIfAborted();
88
+ assertAbsoluteFilePath(path);
89
+ if (!(bytes instanceof Uint8Array)) {
90
+ throw new Error("Tangle exact process write requires Uint8Array bytes");
91
+ }
92
+ if (bytes.byteLength > MAX_EXACT_FILE_BYTES) {
93
+ throw new Error("Tangle exact process write exceeds its byte bound");
94
+ }
95
+ if (!Number.isSafeInteger(options.mode) ||
96
+ options.mode < 0 ||
97
+ options.mode > 0o7777) {
98
+ throw new Error("Tangle exact process file mode must be between 0 and 07777");
99
+ }
100
+ await awaitWithSignal(fs.write(path, Buffer.from(bytes).toString("base64"), {
101
+ encoding: "base64",
102
+ mode: options.mode,
103
+ }), options.signal);
104
+ options.signal?.throwIfAborted();
105
+ },
106
+ async readFile(path, options) {
107
+ assertFileOptions(options, "Tangle exact process read");
108
+ options.signal?.throwIfAborted();
109
+ assertAbsoluteFilePath(path);
110
+ if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1) {
111
+ throw new Error("Tangle exact process maxBytes must be a positive integer");
112
+ }
113
+ if (options.maxBytes > MAX_EXACT_FILE_BYTES) {
114
+ throw new Error("Tangle exact process maxBytes exceeds its bound");
115
+ }
116
+ const stat = await awaitWithSignal(fs.stat(path), options.signal);
117
+ options.signal?.throwIfAborted();
118
+ if (!stat ||
119
+ typeof stat !== "object" ||
120
+ typeof stat.isFile !== "boolean" ||
121
+ !Number.isSafeInteger(stat.size) ||
122
+ stat.size < 0 ||
123
+ stat.size > MAX_EXACT_FILE_BYTES) {
124
+ throw new Error("Tangle exact process file stat returned an invalid size");
125
+ }
126
+ if (!stat.isFile) {
127
+ throw new Error("Tangle exact process path is not a regular file");
128
+ }
129
+ if (stat.size > options.maxBytes) {
130
+ throw new Error("Tangle exact process file exceeds maxBytes");
131
+ }
132
+ const result = await awaitWithSignal(fs.readBatch([path], { encoding: "base64" }), options.signal);
133
+ options.signal?.throwIfAborted();
134
+ if (!result ||
135
+ !Array.isArray(result.files) ||
136
+ result.files.length > 1 ||
137
+ !Array.isArray(result.errors) ||
138
+ result.errors.length > 256) {
139
+ throw new Error("Tangle exact process file read returned an invalid result");
140
+ }
141
+ for (const error of result.errors) {
142
+ if (!error ||
143
+ typeof error !== "object" ||
144
+ typeof error.path !== "string" ||
145
+ error.path.length > 512 ||
146
+ typeof error.error !== "string" ||
147
+ error.error.length > 16_384 ||
148
+ (error.code !== undefined &&
149
+ (typeof error.code !== "string" || error.code.length > 512))) {
150
+ throw new Error("Tangle exact process file read returned an invalid error");
151
+ }
152
+ }
153
+ const file = result.files[0];
154
+ if (result.errors.length !== 0 ||
155
+ result.files.length !== 1 ||
156
+ !file ||
157
+ file.path !== path ||
158
+ file.encoding !== "base64" ||
159
+ typeof file.content !== "string" ||
160
+ !Number.isSafeInteger(file.size) ||
161
+ file.size < 0 ||
162
+ file.size > MAX_EXACT_FILE_BYTES ||
163
+ file.content.length > Math.ceil((MAX_EXACT_FILE_BYTES / 3) * 4) ||
164
+ file.content.length % 4 !== 0 ||
165
+ !/^[A-Za-z0-9+/]*={0,2}$/.test(file.content)) {
166
+ throw new Error(result.errors[0]?.error ??
167
+ "Tangle exact process file read returned an invalid result");
168
+ }
169
+ const bytes = Uint8Array.from(Buffer.from(file.content, "base64"));
170
+ if (bytes.byteLength !== file.size ||
171
+ bytes.byteLength !== stat.size ||
172
+ bytes.byteLength > options.maxBytes) {
173
+ throw new Error("Tangle exact process file read violated its byte bound");
174
+ }
175
+ return bytes;
176
+ },
177
+ async destroy(options = {}) {
178
+ assertSignalOptions(options, "Tangle exact process destroy");
179
+ options.signal?.throwIfAborted();
180
+ await awaitWithSignal(destroy(), options.signal);
181
+ options.signal?.throwIfAborted();
182
+ },
183
+ };
184
+ }
@@ -0,0 +1,5 @@
1
+ import type { AgentExactProcess, AgentExactProcessLaunch, AgentExactProcessStatus } from "@tangle-network/agent-interface/environment-provider";
2
+ import type { SandboxProcessLike, SandboxProcessStatusLike } from "./tangle-types.js";
3
+ export declare function validateExactProcessLaunch(input: AgentExactProcessLaunch): void;
4
+ export declare function sandboxProcessAsExactProcess(process: SandboxProcessLike): AgentExactProcess;
5
+ export declare function exactProcessStatusFromSandbox(status: SandboxProcessStatusLike): AgentExactProcessStatus;