@kingsoftcloud/agentkit-harness-node 0.0.0-stage → 0.0.1

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 (151) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +1 -0
  3. package/README.md +124 -2
  4. package/THIRD_PARTY_NOTICES.md +66 -0
  5. package/ZCode-LICENSE +202 -0
  6. package/dist/index.mjs +4871 -0
  7. package/dist/native-command-plan.LICENSE.txt +221 -0
  8. package/dist/native-command-plan.json +16 -0
  9. package/dist/native-command-plan.mjs +8 -0
  10. package/dist/rpc-entry.mjs +5682 -0
  11. package/package.json +169 -4
  12. package/src/native/a2a-receipt-source.mjs +19 -0
  13. package/src/native/a2a-tools.mjs +517 -0
  14. package/src/native/activity-status.mjs +30 -0
  15. package/src/native/artifact-codec.mjs +481 -0
  16. package/src/native/artifact-store.mjs +1162 -0
  17. package/src/native/assistant-stream.mjs +76 -0
  18. package/src/native/background-command-port.mjs +95 -0
  19. package/src/native/browser-action-observation.mjs +43 -0
  20. package/src/native/browser-availability.mjs +22 -0
  21. package/src/native/browser-canvas-metrics.mjs +71 -0
  22. package/src/native/browser-dom.mjs +193 -0
  23. package/src/native/browser-playwright.mjs +1249 -0
  24. package/src/native/browser-vision.mjs +369 -0
  25. package/src/native/browser-workspace-policy.mjs +22 -0
  26. package/src/native/browser-workspace-protocol.mjs +95 -0
  27. package/src/native/browser-workspace-server.mjs +164 -0
  28. package/src/native/browser-workspace.mjs +355 -0
  29. package/src/native/captured-source.mjs +3 -0
  30. package/src/native/child-budget-allocation.mjs +87 -0
  31. package/src/native/child-capabilities.mjs +35 -0
  32. package/src/native/child-command-resource.mjs +20 -0
  33. package/src/native/child-history.mjs +188 -0
  34. package/src/native/child-merge.mjs +98 -0
  35. package/src/native/child-model-surface.mjs +19 -0
  36. package/src/native/child-observation.mjs +100 -0
  37. package/src/native/child-owner.mjs +620 -0
  38. package/src/native/child-publication.mjs +76 -0
  39. package/src/native/child-readers.mjs +366 -0
  40. package/src/native/child-tools.mjs +46 -0
  41. package/src/native/child-workspace-binding.mjs +29 -0
  42. package/src/native/child-workspace-tools.mjs +1 -0
  43. package/src/native/child-workspaces.mjs +655 -0
  44. package/src/native/children.mjs +365 -0
  45. package/src/native/command-plan-worker.mjs +14 -0
  46. package/src/native/command-plan.mjs +31 -0
  47. package/src/native/command-progress.mjs +64 -0
  48. package/src/native/command-shell.mjs +9 -0
  49. package/src/native/command-workspace-observation.mjs +36 -0
  50. package/src/native/component-tools.mjs +280 -0
  51. package/src/native/context-counter.mjs +133 -0
  52. package/src/native/context-markers.mjs +51 -0
  53. package/src/native/context-profile.mjs +72 -0
  54. package/src/native/context-status.mjs +27 -0
  55. package/src/native/conversation-command-control.mjs +133 -0
  56. package/src/native/delivery-timeline.mjs +39 -0
  57. package/src/native/dsh-background-ownership.mjs +102 -0
  58. package/src/native/dsh-managed-ownership.mjs +23 -0
  59. package/src/native/dsh-turn-contract.mjs +3 -0
  60. package/src/native/dsh-turn-ownership.mjs +54 -0
  61. package/src/native/dsh-turn-runtime.mjs +498 -0
  62. package/src/native/e2b-background-supervisor.mjs +347 -0
  63. package/src/native/e2b-background.mjs +368 -0
  64. package/src/native/e2b-command-state.mjs +40 -0
  65. package/src/native/e2b-command-tools.mjs +486 -0
  66. package/src/native/e2b-sandbox-owner.mjs +227 -0
  67. package/src/native/hook-executor.mjs +249 -0
  68. package/src/native/instruction-assembly.mjs +60 -0
  69. package/src/native/instruction-context.mjs +54 -0
  70. package/src/native/local-background-supervisor.mjs +108 -0
  71. package/src/native/local-background.mjs +207 -0
  72. package/src/native/local-command-evidence.mjs +57 -0
  73. package/src/native/local-command-policy-worker.mjs +197 -0
  74. package/src/native/local-command-policy.mjs +46 -0
  75. package/src/native/local-command-state.mjs +177 -0
  76. package/src/native/local-command-tools.mjs +302 -0
  77. package/src/native/local-execution-profile.mjs +36 -0
  78. package/src/native/local-output-policy.mjs +92 -0
  79. package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
  80. package/src/native/local-process-bin/darwin-x64/launcher +0 -0
  81. package/src/native/local-process-bin/linux-arm64/launcher +0 -0
  82. package/src/native/local-process-bin/linux-x64/launcher +0 -0
  83. package/src/native/local-process-launcher.c +183 -0
  84. package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
  85. package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
  86. package/src/native/local-process-licenses/NOTICE +21 -0
  87. package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
  88. package/src/native/local-process-manifest.json +32 -0
  89. package/src/native/local-process.mjs +246 -0
  90. package/src/native/local-resource-profile.mjs +32 -0
  91. package/src/native/memory-candidate.mjs +121 -0
  92. package/src/native/memory-extraction-run.mjs +60 -0
  93. package/src/native/memory-extraction-tools.mjs +116 -0
  94. package/src/native/memory-recall.mjs +65 -0
  95. package/src/native/memory-store.mjs +334 -0
  96. package/src/native/memory-tools.mjs +329 -0
  97. package/src/native/model-anthropic-cache.mjs +49 -0
  98. package/src/native/model-anthropic-tool-search.mjs +85 -0
  99. package/src/native/model-anthropic.mjs +753 -0
  100. package/src/native/model-chat-usage.mjs +24 -0
  101. package/src/native/model-inline-reasoning.mjs +111 -0
  102. package/src/native/model-media.mjs +174 -0
  103. package/src/native/model-normalize.mjs +228 -0
  104. package/src/native/model-openai-chat.mjs +561 -0
  105. package/src/native/model-port.mjs +361 -0
  106. package/src/native/model-provider-compat.mjs +129 -0
  107. package/src/native/model-replay.mjs +153 -0
  108. package/src/native/model-request.mjs +15 -0
  109. package/src/native/model-responses.mjs +1205 -0
  110. package/src/native/model-sse.mjs +171 -0
  111. package/src/native/model-surface-host.mjs +27 -0
  112. package/src/native/model-tool-search-policy.mjs +16 -0
  113. package/src/native/model-transport-observation.mjs +27 -0
  114. package/src/native/owned-child.mjs +114 -0
  115. package/src/native/owned-command.mjs +106 -0
  116. package/src/native/python-syntax-worker.mjs +60 -0
  117. package/src/native/python-syntax.mjs +50 -0
  118. package/src/native/read-interruption.mjs +16 -0
  119. package/src/native/result-stream.mjs +63 -0
  120. package/src/native/ruff-ast.mjs +95 -0
  121. package/src/native/run-observation.mjs +128 -0
  122. package/src/native/session-delivery-journal.mjs +96 -0
  123. package/src/native/session-delivery.mjs +434 -0
  124. package/src/native/shell-analysis.mjs +50 -0
  125. package/src/native/skill-disclosure.mjs +404 -0
  126. package/src/native/skill-events.mjs +164 -0
  127. package/src/native/skill-tools.mjs +813 -0
  128. package/src/native/source-page.mjs +73 -0
  129. package/src/native/sqlite-session-inbox.mjs +42 -0
  130. package/src/native/sqlite-store.mjs +617 -0
  131. package/src/native/tool-authority.mjs +99 -0
  132. package/src/native/tool-guidance.mjs +12 -0
  133. package/src/native/tool-observation.mjs +75 -0
  134. package/src/native/tool-states.mjs +14 -0
  135. package/src/native/verification-authority.mjs +37 -0
  136. package/src/native/web-tools.mjs +643 -0
  137. package/src/native/workspace-checkpoints.mjs +312 -0
  138. package/src/native/workspace-context.mjs +61 -0
  139. package/src/native/workspace-display.mjs +39 -0
  140. package/src/native/workspace-edit-match.mjs +19 -0
  141. package/src/native/workspace-edit-worker.mjs +15 -0
  142. package/src/native/workspace-editing.mjs +447 -0
  143. package/src/native/workspace-image.mjs +72 -0
  144. package/src/native/workspace-instructions.mjs +107 -0
  145. package/src/native/workspace-mutation-lock.mjs +74 -0
  146. package/src/native/workspace-patch-worker.mjs +17 -0
  147. package/src/native/workspace-patch.mjs +17 -0
  148. package/src/native/workspace-paths.mjs +225 -0
  149. package/src/native/workspace-state.mjs +272 -0
  150. package/src/native/workspace-subject-observer.mjs +84 -0
  151. package/src/native/workspace-tools.mjs +1740 -0
@@ -0,0 +1,368 @@
1
+ // Host-owned durable background commands for the E2B sandbox backend (Node port).
2
+ //
3
+ // Reference: ksadk/sandbox/e2b_background.py @355059d. Mirrors the Node local-background
4
+ // contract (owner/scope/session/workspace isolation, UTF-8 byte cursors, terminal
5
+ // states, unknown+evidenceIncomplete) for a REMOTE sandbox whose command outlives
6
+ // any single client. A detached supervisor (a fork child, not a Python subprocess)
7
+ // holds a cross-process lock and appends output to host files; a new host process
8
+ // reads the same files and checks the lock for liveness.
9
+ //
10
+ // Production wires the installed E2B SDK via a strictly injected transport interface.
11
+ // The reference's unverified remote-replay promises
12
+ // are NOT claimed here: a reconnect gap is conservatively unknown+evidenceIncomplete.
13
+ //
14
+ // Authority is synchronous === true; cleanup awaits a resolved === true.
15
+ // No new launch/stop/model/ask on read. The started backend decides the backend.
16
+ import { spawn } from 'node:child_process';
17
+ import { randomUUID } from 'node:crypto';
18
+ import { existsSync, realpathSync, renameSync, rmSync } from 'node:fs';
19
+ import { dirname, join, resolve } from 'node:path';
20
+ import { homedir } from 'node:os';
21
+ import { copy } from '@kingsoftcloud/agentkit-harness-core';
22
+ import { LOCAL_BACKGROUND_CONTRACT, TERMINAL, LocalCommandError, check, createPrivate,
23
+ digest, lockUntil, privateDirectory, readAll, readWindow, syncDirectory,
24
+ tryLock, unknownRecord, validId, writeRecord, delay, readE2BRecord, atomicPrivate } from './e2b-command-state.mjs';
25
+
26
+ export const E2B_BACKGROUND_CONTRACT = 'agentkit.e2b-background-command/v1';
27
+ export const RECOVERY_CLAIM_TTL_MS = 30000;
28
+ // Public deterministic locators let the Host persist a task before SDK entry.
29
+ export const e2bCommandOwner = (workspaceRoot, sessionId) => digest([realpathSync(workspaceRoot), 'e2b', sessionId]);
30
+ export function e2bCommandId(owner, operationKey) {
31
+ check(typeof operationKey === 'string' && !!operationKey.trim(), 'invalid_background_operation_key');
32
+ return digest([owner, operationKey]);
33
+ }
34
+ const supervisor = new URL('./e2b-background-supervisor.mjs', import.meta.url).href;
35
+ const validSandboxId = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value);
36
+ // A strict confirmed-true: the value must be exactly true, not a truthy Promise.
37
+ const isConfirmed = value => {
38
+ if (value && typeof value.then === 'function') Promise.resolve(value).catch(() => {});
39
+ return value === true;
40
+ };
41
+ const freeze = value => {
42
+ if (value && typeof value === 'object') { Object.freeze(value); for (const child of Object.values(value)) freeze(child); }
43
+ return value;
44
+ };
45
+
46
+ const commandRequest = ({ command, cwd = null, env = null, timeoutSeconds }) => {
47
+ check(typeof command === 'string' && !!command.trim(), 'invalid_background_command');
48
+ check(cwd === null || typeof cwd === 'string', 'invalid_background_command');
49
+ check(env === null || (typeof env === 'object' && !Array.isArray(env)
50
+ && Object.entries(env).every(([k, v]) => typeof k === 'string' && typeof v === 'string')), 'invalid_background_command');
51
+ check(Number.isSafeInteger(timeoutSeconds) && timeoutSeconds >= 0, 'invalid_background_command');
52
+ return { command, cwd, env, timeoutSeconds };
53
+ };
54
+
55
+ /** One host-owned handle to a remote E2B background command. commandId is the only
56
+ * identity exposed to the model/native layer; the remote pid stays internal. Output
57
+ * is read from host files via UTF-8 cursors. Liveness is cross-process: the detached
58
+ * supervisor holds supervisor.sqlite for its life. */
59
+ export class E2BBackgroundCommand {
60
+ constructor(store, directory, { commandId }) {
61
+ check(store instanceof E2BCommandStore, 'invalid_e2b_background_store');
62
+ this._store = store; this.directory = directory; this.commandId = commandId;
63
+ readE2BRecord(directory, store.owner, commandId); // Refuse a handle from another session/root.
64
+ Object.freeze(this);
65
+ }
66
+ _record() { return readE2BRecord(this.directory, this._store.owner, this.commandId); }
67
+ get timeoutSeconds() { return Math.floor(this._record().timeoutMs / 1000); }
68
+ get processId() {
69
+ const pid = this._record().supervisorPid;
70
+ check(Number.isSafeInteger(pid) && pid > 0, 'background_supervisor_identity_unavailable');
71
+ return pid; // Diagnostic locator only; never used to signal.
72
+ }
73
+ readOutput({ stdoutCursor = 0, stderrCursor = 0, maxBytes = 65536 } = {}) {
74
+ check(Number.isSafeInteger(maxBytes) && maxBytes >= 4 && maxBytes <= 65536, 'invalid_background_output_window');
75
+ // tryLock returns a release fn iff the lock was FREE (acquired) = no live
76
+ // supervisor; null iff a live supervisor holds it. Probe and release immediately.
77
+ const probe = tryLock(join(this.directory, 'supervisor.sqlite'));
78
+ let record;
79
+ try {
80
+ record = this._record();
81
+ if (probe && !TERMINAL.has(record.status)) {
82
+ if (record.status !== 'starting' || record.supervisorPid !== undefined || Date.now() - record.createdAt > 5000)
83
+ record = { ...record, status: 'unknown', exitCode: null, evidenceIncomplete: true, outcomeReason: 'supervisor_lost' };
84
+ }
85
+ } finally { if (probe) probe(); }
86
+ const stdout = readWindow(join(this.directory, 'stdout'), stdoutCursor, maxBytes);
87
+ const stderr = readWindow(join(this.directory, 'stderr'), stderrCursor, maxBytes);
88
+ return { status: record.status, stdout: stdout.text, stderr: stderr.text,
89
+ stdoutCursor: stdout.cursor, stderrCursor: stderr.cursor,
90
+ exitCode: TERMINAL.has(record.status) ? record.exitCode : null,
91
+ cancelRequested: existsSync(join(this.directory, 'cancel')),
92
+ outputMayBeTruncated: stdout.hasMore || stderr.hasMore,
93
+ evidenceIncomplete: record.evidenceIncomplete === true, observedExitCode: record.observedExitCode ?? null,
94
+ outcomeReason: record.outcomeReason ?? '' };
95
+ }
96
+ requestCancel() {
97
+ const output = this.readOutput();
98
+ if (!TERMINAL.has(output.status)) createPrivate(join(this.directory, 'cancel'));
99
+ return this.readOutput();
100
+ }
101
+ async wait({ timeoutMs } = {}) {
102
+ const started = performance.now();
103
+ let output;
104
+ while (!TERMINAL.has((output = this.readOutput()).status)) {
105
+ if (Number.isSafeInteger(timeoutMs) && performance.now() - started >= timeoutMs)
106
+ throw new LocalCommandError('background_command_still_running');
107
+ await delay(50);
108
+ }
109
+ check(output.status !== 'unknown', 'background_command_outcome_unknown');
110
+ return { stdout: readAll(join(this.directory, 'stdout')), stderr: readAll(join(this.directory, 'stderr')),
111
+ exitCode: output.exitCode };
112
+ }
113
+ }
114
+
115
+ /** Host-owned command records for one session, backed by detached supervisors that
116
+ * drive an injected remote transport. The frozen sandbox.json (sandboxId + owner)
117
+ * is the authority for read/stop/recover; a foreign-owned sandbox is refused.
118
+ *
119
+ * Authority contract: `authority` is a STRICT synchronous predicate returning
120
+ * exactly `true` (not a truthy Promise) to confirm authority over an existing
121
+ * sandbox. `killSandbox` may be asynchronous; cleanup requires its resolved
122
+ * value to equal `true`. `authority === null` means NO authority is proven:
123
+ * recover/close refuse with zero side effects.
124
+ *
125
+ * `launchAuthorized` (default true) is the outer host's declaration that this store
126
+ * may launch a first command (the first launch has no existing sandbox to prove
127
+ * authority over; the outer host authorizes it). Without it, start() refuses. */
128
+ export class E2BCommandStore {
129
+ constructor({ stateRoot, workspaceRoot, sessionId, transport = null, transportModule = null,
130
+ transportConfig = null, authority = null, killSandbox = null, launchAuthorized = true,
131
+ sandboxId = null }) {
132
+ check(process.platform === 'darwin' || process.platform === 'linux', 'unsupported_e2b_background_platform');
133
+ check(typeof stateRoot === 'string' && !!stateRoot && typeof workspaceRoot === 'string' && !!workspaceRoot
134
+ && typeof sessionId === 'string' && !!sessionId.trim(), 'invalid_background_command_scope');
135
+ check((transport && typeof transport.start === 'function' && typeof transport.reconnect === 'function'
136
+ && typeof transport.kill === 'function') || typeof transportModule === 'string', 'invalid_e2b_background_transport');
137
+ check(authority === null || typeof authority === 'function', 'invalid_e2b_background_authority');
138
+ check(killSandbox === null || typeof killSandbox === 'function', 'invalid_e2b_background_kill_sandbox');
139
+ check(sandboxId === null || validSandboxId(sandboxId), 'invalid_e2b_background_sandbox_id');
140
+ check(typeof launchAuthorized === 'boolean', 'invalid_e2b_launch_authority');
141
+ // Resolve stateRoot through the filesystem. The workspace-containment check
142
+ // runs BEFORE any directory creation so a stateRoot inside the workspace is
143
+ // rejected with zero side effects. resolve() does not follow symlinks;
144
+ // privateDirectory validates ancestors (no symlink) and creates the root.
145
+ const resolved = resolve(stateRoot === '~' ? homedir() : stateRoot.startsWith('~/') ? join(homedir(), stateRoot.slice(2)) : stateRoot);
146
+ const workspace = realpathSync(workspaceRoot);
147
+ // Pre-creation check on the resolved path: reject root === workspace or root
148
+ // inside workspace (a stateRoot inside the workspace is never created).
149
+ check(resolved !== workspace && !resolved.startsWith(workspace + '/'), 'background_state_inside_workspace');
150
+ privateDirectory(dirname(resolved), false);
151
+ privateDirectory(resolved, true); // create the root (0o700, no symlink ancestors)
152
+ const root = realpathSync(resolved);
153
+ // Post-creation realpath check: a symlink in the resolved path that landed
154
+ // inside the workspace is still rejected.
155
+ check(root !== workspace && !root.startsWith(workspace + '/'), 'background_state_inside_workspace');
156
+ this.owner = e2bCommandOwner(workspace, sessionId);
157
+ this.directory = join(root, 'commands', this.owner);
158
+ this._sandboxRoot = join(root, 'sandboxes', this.owner);
159
+ this.sessionId = sessionId;
160
+ this._transport = transport; this._transportModule = transportModule; this._transportConfig = freeze(copy(transportConfig));
161
+ this._authority = authority; this._killSandbox = killSandbox;
162
+ this._launchAuthorized = !!launchAuthorized;
163
+ this._boundSandboxId = sandboxId; // proven by owner.ensure before store creation
164
+ privateDirectory(this.directory, true); privateDirectory(this._sandboxRoot, true);
165
+ if (sandboxId !== null) {
166
+ const release = tryLock(join(this._sandboxRoot, 'binding.sqlite'));
167
+ check(release, 'e2b_background_binding_busy');
168
+ try {
169
+ if (this._hasSandboxState()) {
170
+ const frozen = this._frozenSandbox();
171
+ check(frozen?.owner === this.owner && frozen.sandboxId === sandboxId, 'e2b_background_binding_changed');
172
+ } else {
173
+ atomicPrivate(this._sandboxFile(), JSON.stringify({ sandboxId, owner: this.owner, createdAt: Date.now() }) + '\n');
174
+ }
175
+ } finally { release(); }
176
+ }
177
+ Object.freeze(this);
178
+ }
179
+ commandId(operationKey) {
180
+ return e2bCommandId(this.owner, operationKey);
181
+ }
182
+ connect(commandId) {
183
+ check(validId(commandId), 'invalid_background_command_identity');
184
+ const directory = join(this.directory, commandId);
185
+ check(existsSync(join(directory, 'record.json')), 'background_command_not_owned');
186
+ return new E2BBackgroundCommand(this, directory, { commandId });
187
+ }
188
+ _sandboxFile() { return join(this._sandboxRoot, 'sandbox.json'); }
189
+ _hasSandboxState() { return existsSync(this._sandboxFile()); }
190
+ _frozenSandbox() {
191
+ if (!this._hasSandboxState()) return null;
192
+ try { const record = JSON.parse(readAll(this._sandboxFile())); return record && typeof record === 'object' ? record : null; }
193
+ catch { return null; }
194
+ }
195
+ _authorityProven() {
196
+ // The frozen sandbox.json owner must match this session's owner key, AND a
197
+ // host-supplied authority predicate must return EXACTLY true (strict, not a
198
+ // truthy Promise). authority === null → not proven → refuse. This is the gate
199
+ // for operations that TOUCH an existing sandbox (recover/close).
200
+ const frozen = this._frozenSandbox();
201
+ if (frozen === null) return false;
202
+ if (frozen.owner !== this.owner || !validSandboxId(frozen.sandboxId)
203
+ || this._boundSandboxId !== null && frozen.sandboxId !== this._boundSandboxId) return false;
204
+ const sealed = digest(frozen);
205
+ try {
206
+ return this._authority !== null && isConfirmed(this._authority(frozen))
207
+ && digest(frozen) === sealed && digest(this._frozenSandbox()) === sealed;
208
+ } catch { return false; }
209
+ }
210
+ _assertAuthority() {
211
+ // A genuine first launch (no sandbox.json) is allowed only if the outer host
212
+ // declared launchAuthorized (the first launch has no existing sandbox to prove
213
+ // authority over). An existing sandbox requires proven authority.
214
+ if (!this._hasSandboxState()) {
215
+ check(this._launchAuthorized, 'e2b_background_launch_not_authorized');
216
+ return;
217
+ }
218
+ check(this._authorityProven(), 'e2b_background_authority_unprovable');
219
+ }
220
+ recordSandboxIdForTask(taskId) {
221
+ if (!validId(taskId)) return null;
222
+ const directory = join(this.directory, taskId);
223
+ if (!existsSync(join(directory, 'record.json'))) return null;
224
+ try { const rec = readE2BRecord(directory, this.owner, taskId); return rec.sandboxId && validSandboxId(rec.sandboxId) ? rec.sandboxId : null; }
225
+ catch { return null; }
226
+ }
227
+ connectOrRecover(commandId) {
228
+ const handle = this.connect(commandId);
229
+ const record = handle._record();
230
+ if (TERMINAL.has(record.status)) return handle; // terminal → no authority needed
231
+ if (!record.pid || !record.sandboxId) return handle;
232
+ this._assertAuthority(); // recovery touches an existing sandbox
233
+ // tryLock: release fn = free (supervisor dead) → recover; null = held (alive).
234
+ const probe = tryLock(join(handle.directory, 'supervisor.sqlite'));
235
+ if (probe) { probe(); return this.recover(commandId); }
236
+ return handle; // supervisor alive
237
+ }
238
+ async start({ operationKey = randomUUID(), assertCurrent = () => true, ...input }) {
239
+ check(typeof assertCurrent === 'function', 'e2b_background_guard_invalid');
240
+ const request = commandRequest(input), commandId = this.commandId(operationKey), requestDigest = digest(request);
241
+ const release = await lockUntil(join(this.directory, 'launch.sqlite'), performance.now() + 5000);
242
+ try {
243
+ check(isConfirmed(assertCurrent()), 'e2b_background_authority_lost');
244
+ this._assertAuthority();
245
+ const directory = join(this.directory, commandId);
246
+ if (existsSync(directory)) {
247
+ const handle = this.connect(commandId);
248
+ check(readE2BRecord(directory, this.owner, commandId).requestDigest === requestDigest, 'background_operation_arguments_changed');
249
+ return handle; // An existing/unknown operation is never launched again.
250
+ }
251
+ const record = { schemaVersion: LOCAL_BACKGROUND_CONTRACT, owner: this.owner, commandId, requestDigest,
252
+ status: 'starting', createdAt: Date.now(), timeoutMs: request.timeoutSeconds * 1000, exitCode: null,
253
+ backend: 'e2b', generation: 0 };
254
+ const preparing = join(this.directory, `.preparing-${randomUUID()}`);
255
+ try {
256
+ privateDirectory(preparing, true);
257
+ for (const file of ['stdout', 'stderr', 'supervisor.sqlite']) createPrivate(join(preparing, file));
258
+ writeRecord(preparing, record);
259
+ renameSync(preparing, directory); syncDirectory(dirname(directory));
260
+ } finally { if (existsSync(preparing)) rmSync(preparing, { recursive: true, force: true }); }
261
+ const handle = this.connect(commandId);
262
+ check(isConfirmed(assertCurrent()), 'e2b_background_authority_lost');
263
+ this._spawnSupervisor(handle, { mode: 'start', generation: 0, request });
264
+ return handle;
265
+ } finally { release(); }
266
+ }
267
+ async recover(commandId) {
268
+ const handle = this.connect(commandId);
269
+ let record = handle._record();
270
+ if (TERMINAL.has(record.status)) return handle;
271
+ if (!record.pid || !record.sandboxId) return handle; // Cannot reconnect without a remote identity.
272
+ check(this._authorityProven(), 'e2b_background_authority_unprovable');
273
+ const release = await lockUntil(join(handle.directory, 'recover.sqlite'), performance.now() + 5000);
274
+ try {
275
+ record = handle._record();
276
+ if (TERMINAL.has(record.status)) return handle;
277
+ const probe = tryLock(join(handle.directory, 'supervisor.sqlite'));
278
+ if (probe) probe();
279
+ else return handle; // live supervisor owns it; nothing to recover.
280
+ const claimedGen = record.recoveryClaimGeneration;
281
+ if (Number.isSafeInteger(claimedGen)) {
282
+ const claimAge = Date.now() - (record.recoveryClaimAt ?? 0);
283
+ if (claimAge < RECOVERY_CLAIM_TTL_MS) return handle; // durable claim prevents a second spawn
284
+ record.staleClaimFloor = claimedGen;
285
+ delete record.recoveryClaimGeneration; delete record.recoveryClaimAt;
286
+ }
287
+ const priorGen = record.generation ?? 0, priorClaim = record.recoveryClaimGeneration ?? 0, staleFloor = record.staleClaimFloor ?? 0;
288
+ const generation = Math.max(priorGen, priorClaim, staleFloor) + 1;
289
+ record.recoveryClaimGeneration = generation; record.recoveryClaimAt = Date.now();
290
+ delete record.staleClaimFloor;
291
+ writeRecord(handle.directory, record);
292
+ this._spawnSupervisor(handle, { mode: 'reconnect', generation, request: null });
293
+ return handle;
294
+ } finally { release(); }
295
+ }
296
+ _spawnSupervisor(handle, { mode, generation, request }) {
297
+ // The frozen sandboxId: prefer the proven bound sandboxId from owner.ensure
298
+ // (set at store construction); fall back to sandbox.json if it exists. This
299
+ // ensures the first launch (no sandbox.json yet) still passes a trusted
300
+ // sandboxId to transport.start — the real SDK forbids a null sandboxId.
301
+ const frozen = this._frozenSandbox();
302
+ const fromJson = frozen && frozen.owner === this.owner && validSandboxId(frozen.sandboxId) ? frozen.sandboxId : null;
303
+ const frozenSandboxId = this._boundSandboxId ?? fromJson;
304
+ const config = { mode, directory: handle.directory, sandboxRoot: this._sandboxRoot, owner: this.owner,
305
+ commandId: handle.commandId, generation, request, frozenSandboxId,
306
+ transportModule: this._transportModule, transportConfig: this._transportConfig };
307
+ // In-process transport (tests): run the supervisor directly with the injected
308
+ // transport, so a closure-based fake transport works. Production uses the
309
+ // detached child with a transportModule the child imports.
310
+ if (this._transport) {
311
+ config.transport = this._transport;
312
+ import('./e2b-background-supervisor.mjs').then(({ supervise }) =>
313
+ supervise(config, this._transport).catch(() => {
314
+ // Generation-fenced fallback: only settle unknown if this supervisor is
315
+ // still current. A newer reclaim or terminal must not be clobbered.
316
+ try {
317
+ const dir = handle.directory, owner = this.owner, cid = handle.commandId, gen = config.generation ?? 0;
318
+ const rec = readE2BRecord(dir, owner, cid);
319
+ if (!['completed', 'failed', 'canceled', 'timed_out', 'output_limit', 'unknown'].includes(rec.status)
320
+ && (rec.generation ?? 0) <= gen
321
+ && !(Number.isSafeInteger(rec.recoveryClaimGeneration) && rec.recoveryClaimGeneration > gen))
322
+ unknownRecord(dir, rec, 'supervisor_in_process_failed');
323
+ } catch {}
324
+ }));
325
+ return;
326
+ }
327
+ let child;
328
+ try {
329
+ child = spawn(process.execPath, ['--input-type=module', '-e',
330
+ `import(${JSON.stringify(supervisor)}).then(m => m.runSupervisor()).catch(() => process.exit(1))`],
331
+ { detached: true, stdio: ['pipe', 'ignore', 'ignore'], env: { ...process.env, NODE_NO_WARNINGS: '1' } });
332
+ } catch { unknownRecord(handle.directory, handle._record(), 'supervisor_launch_failed'); return; }
333
+ child.once('error', () => { const probe = tryLock(join(handle.directory, 'supervisor.sqlite'));
334
+ try { if (probe) { probe(); const current = handle._record(); if (current.status === 'starting') unknownRecord(handle.directory, current, 'supervisor_launch_failed'); } }
335
+ catch {} finally { /* probe already released if acquired */ } });
336
+ child.stdin.on('error', () => {});
337
+ child.stdin.end(JSON.stringify(config)); child.unref();
338
+ }
339
+ async close() {
340
+ // Kill the persistent sandbox. ASYNC contract: killSandbox may be sync or
341
+ // async (returning a Promise<boolean>); close() awaits it and checks the
342
+ // RESOLVED value === true. A Promise itself (truthy) is never accepted — it
343
+ // must resolve to exactly true. authority remains a strict synchronous ===true
344
+ // predicate. Zero side effects without proven authority.
345
+ if (this._killSandbox === null) return false;
346
+ const frozen = this._frozenSandbox();
347
+ if (!frozen || frozen.owner !== this.owner || !validSandboxId(frozen.sandboxId)) return false;
348
+ const sealed = digest(frozen);
349
+ if (!this._authorityProven() || digest(this._frozenSandbox()) !== sealed) return false;
350
+ try { return await Promise.resolve(this._killSandbox(frozen.sandboxId)) === true; }
351
+ catch { return false; }
352
+ }
353
+ }
354
+
355
+ /** Reader for createBackgroundCommandPort's canonical, owned launch receipts.
356
+ * No launch/cancel/record transition; never selects a backend from the environment. */
357
+ export function createE2BBackgroundReader(store) {
358
+ check(store instanceof E2BCommandStore, 'invalid_e2b_background_store');
359
+ return Object.freeze(async request => {
360
+ request.signal.throwIfAborted();
361
+ check(request.sessionId === store.sessionId, 'background_reader_owner_mismatch');
362
+ check(request.ref?.taskId === store.commandId(request.ref?.operationKey), 'background_reader_operation_mismatch');
363
+ check(request.waitSeconds === 0, 'background_observation_must_not_wait');
364
+ const result = store.connect(request.ref.taskId).readOutput({ stdoutCursor: request.stdoutCursor,
365
+ stderrCursor: request.stderrCursor, maxBytes: request.maxBytes });
366
+ request.signal.throwIfAborted(); return result;
367
+ });
368
+ }
@@ -0,0 +1,40 @@
1
+ // E2B background command state helpers. Reuses the verified local-command-state
2
+ // private-file/lock/cursor helpers but validates E2B terminal semantics
3
+ // (cancelled/timed_out with a null exit code from a remote kill), which the strict
4
+ // local_process readRecord exit-code conventions (124/122) do not allow.
5
+ import { constants, closeSync, fstatSync, readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { LOCAL_BACKGROUND_CONTRACT, TERMINAL, LocalCommandError, check, openPrivate,
8
+ privateDirectory, validId } from './local-command-state.mjs';
9
+
10
+ export { LOCAL_BACKGROUND_CONTRACT, TERMINAL, LocalCommandError, check, createPrivate,
11
+ digest, lockUntil, privateDirectory, readAll, readWindow, syncDirectory, tryLock,
12
+ unknownRecord, validId, writeRecord, delay, openPrivate, atomicPrivate } from './local-command-state.mjs';
13
+
14
+ export function readE2BRecord(directory, owner, commandId) {
15
+ privateDirectory(directory);
16
+ const fd = openPrivate(join(directory, 'record.json'), constants.O_RDONLY);
17
+ let record;
18
+ try {
19
+ check(fstatSync(fd).size <= 65536, 'invalid_background_record');
20
+ const text = readFileSync(fd, 'utf8');
21
+ try { record = JSON.parse(text); } catch { throw new LocalCommandError('invalid_background_record'); }
22
+ } finally { closeSync(fd); }
23
+ check(record && record.schemaVersion === LOCAL_BACKGROUND_CONTRACT && record.owner === owner
24
+ && record.commandId === commandId && validId(owner) && validId(commandId) && validId(record.requestDigest)
25
+ && record.backend === 'e2b' && Number.isSafeInteger(record.generation) && record.generation >= 0
26
+ && ['starting', 'running', ...TERMINAL].includes(record.status)
27
+ && Number.isSafeInteger(record.createdAt) && record.createdAt > 0
28
+ && Number.isSafeInteger(record.timeoutMs) && record.timeoutMs >= 0
29
+ && (record.exitCode === null || Number.isSafeInteger(record.exitCode))
30
+ && (!['completed', 'failed'].includes(record.status) || Number.isSafeInteger(record.exitCode))
31
+ && (record.pid === undefined || Number.isSafeInteger(record.pid) && record.pid > 0)
32
+ && (record.sandboxId === undefined || typeof record.sandboxId === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(record.sandboxId))
33
+ && (record.recoveryClaimGeneration === undefined || Number.isSafeInteger(record.recoveryClaimGeneration)
34
+ && record.recoveryClaimGeneration > record.generation && Number.isSafeInteger(record.recoveryClaimAt) && record.recoveryClaimAt > 0)
35
+ && (record.supervisorPid === undefined || Number.isSafeInteger(record.supervisorPid) && record.supervisorPid > 0)
36
+ && (record.evidenceIncomplete === undefined || typeof record.evidenceIncomplete === 'boolean')
37
+ && (record.observedExitCode === undefined || record.observedExitCode === null || Number.isSafeInteger(record.observedExitCode)),
38
+ 'invalid_background_record');
39
+ return record;
40
+ }