@sema-agent/server 7.49.0 → 7.51.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 (63) hide show
  1. package/USAGE.md +39 -0
  2. package/dist/boot/device-lane.d.ts +79 -0
  3. package/dist/boot/device-lane.js +63 -0
  4. package/dist/boot/engine-lease.d.ts +95 -0
  5. package/dist/boot/engine-lease.js +56 -0
  6. package/dist/boot/execution-env.d.ts +48 -2
  7. package/dist/boot/execution-env.js +57 -5
  8. package/dist/boot/resolve-spec.d.ts +3 -0
  9. package/dist/boot/resolve-spec.js +12 -9
  10. package/dist/boot/session-faces.d.ts +5 -0
  11. package/dist/boot/session-faces.js +4 -1
  12. package/dist/boot/shutdown.d.ts +10 -0
  13. package/dist/boot/shutdown.js +19 -1
  14. package/dist/boot/stores.js +9 -0
  15. package/dist/config-center/types.d.ts +10 -3
  16. package/dist/config-invariants.d.ts +2 -2
  17. package/dist/config-invariants.js +18 -0
  18. package/dist/config-types.d.ts +32 -1
  19. package/dist/config.js +69 -10
  20. package/dist/device-enrollment.d.ts +125 -0
  21. package/dist/device-enrollment.js +156 -0
  22. package/dist/device-store.d.ts +385 -0
  23. package/dist/device-store.js +407 -0
  24. package/dist/device-ws-hub.d.ts +182 -0
  25. package/dist/device-ws-hub.js +1012 -0
  26. package/dist/device-ws-protocol.d.ts +429 -0
  27. package/dist/device-ws-protocol.js +464 -0
  28. package/dist/env-facts.d.ts +4 -1
  29. package/dist/env-facts.js +1 -0
  30. package/dist/execution-lane-caps.d.ts +152 -0
  31. package/dist/execution-lane-caps.js +166 -0
  32. package/dist/fleet/subagent-tail-bus.d.ts +1 -1
  33. package/dist/fleet/subagent-tail-bus.js +10 -0
  34. package/dist/http/routes/approvals-assistant.js +1 -1
  35. package/dist/http/routes/capabilities.js +7 -2
  36. package/dist/http/routes/sessions.js +1 -1
  37. package/dist/http/routes/tasks.js +12 -6
  38. package/dist/http/send.d.ts +14 -0
  39. package/dist/http/send.js +5 -0
  40. package/dist/http/server.d.ts +19 -0
  41. package/dist/http/server.js +21 -2
  42. package/dist/http/wire-types.d.ts +1 -1
  43. package/dist/leader/wire.js +4 -2
  44. package/dist/main.js +10 -3
  45. package/dist/orchestration/hardened-vm-runner.d.ts +7 -0
  46. package/dist/orchestration/hardened-vm-runner.js +11 -1
  47. package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
  48. package/dist/plugins/device-store-sql.d.ts +130 -0
  49. package/dist/plugins/device-store-sql.js +574 -0
  50. package/dist/plugins/remote-env-device.d.ts +271 -0
  51. package/dist/plugins/remote-env-device.js +727 -0
  52. package/dist/plugins/remote-scratchpad.js +1 -1
  53. package/dist/plugins/store-backend.d.ts +9 -0
  54. package/dist/plugins/store-backend.js +3 -0
  55. package/dist/plugins/usage-window-store-sql.d.ts +2 -2
  56. package/dist/plugins/usage-window-store-sql.js +17 -6
  57. package/dist/task-cwd.d.ts +25 -0
  58. package/dist/task-cwd.js +3 -0
  59. package/dist/task-settings.js +3 -1
  60. package/dist/trace/core-keyset-guard.d.ts +3 -3
  61. package/dist/trace/engine-notice-wire.d.ts +1 -1
  62. package/dist/trace/engine-notice-wire.js +1 -0
  63. package/package.json +3 -3
@@ -0,0 +1,727 @@
1
+ import { posix } from "node:path";
2
+ import { ExecutionError, FileError, RemoteExecutionError, } from "@sema-agent/core";
3
+ import { DEVICE_ERROR_CODES, DEVICE_RATE_LIMIT_CODE, } from "../device-store.js";
4
+ export const DEVICE_PROVIDER = "device";
5
+ const ok = (value) => ({ ok: true, value });
6
+ const STREAM_PENDING_MAX_BYTES = 2 * 1024 * 1024;
7
+ const STREAM_PENDING_MAX_ENTRIES = 8192;
8
+ const VERIFY_FIRST = "may have already executed on the target; verify its effect before re-running";
9
+ export const DEVICE_OUTCOME_TO_FILE_ERROR = {
10
+ aborted: "aborted",
11
+ already_exists: "already_exists",
12
+ auth_failed: "unknown",
13
+ callback_error: "unknown",
14
+ invalid: "invalid",
15
+ is_directory: "is_directory",
16
+ not_directory: "not_directory",
17
+ not_found: "not_found",
18
+ not_supported: "not_supported",
19
+ outcome_unknown: "outcome_unknown",
20
+ permission_denied: "permission_denied",
21
+ precondition_failed: "precondition_failed",
22
+ shell_unavailable: "unknown",
23
+ spawn_error: "unknown",
24
+ suspended: "unknown",
25
+ target_unavailable: "transport_lost",
26
+ timeout: "timeout",
27
+ transport_lost: "transport_lost",
28
+ unknown: "unknown",
29
+ };
30
+ export const DEVICE_OUTCOME_TO_EXEC_ERROR = {
31
+ aborted: "aborted",
32
+ already_exists: "unknown",
33
+ auth_failed: "auth_failed",
34
+ callback_error: "callback_error",
35
+ invalid: "unknown",
36
+ is_directory: "unknown",
37
+ not_directory: "unknown",
38
+ not_found: "unknown",
39
+ not_supported: "shell_unavailable",
40
+ outcome_unknown: "outcome_unknown",
41
+ permission_denied: "unknown",
42
+ precondition_failed: "unknown",
43
+ shell_unavailable: "shell_unavailable",
44
+ spawn_error: "spawn_error",
45
+ suspended: "suspended",
46
+ target_unavailable: "target_unavailable",
47
+ timeout: "timeout",
48
+ transport_lost: "transport_lost",
49
+ unknown: "unknown",
50
+ };
51
+ export const DEVICE_REJECT_TO_EXEC_ERROR = {
52
+ "device.not_bound": "target_unavailable",
53
+ "device.offline": "target_unavailable",
54
+ "device.busy": "target_unavailable",
55
+ "device.attached_elsewhere": "target_unavailable",
56
+ "device.claim_conflict": "target_unavailable",
57
+ "device.provider_mismatch": "auth_failed",
58
+ "device.identity_mismatch": "auth_failed",
59
+ "device.revoked": "auth_failed",
60
+ "device.enrollment_invalid": "auth_failed",
61
+ "not_found.device": "auth_failed",
62
+ "limit.rate_exceeded": "target_unavailable",
63
+ };
64
+ export const DEVICE_REJECT_TO_FILE_ERROR = {
65
+ "device.not_bound": "transport_lost",
66
+ "device.offline": "transport_lost",
67
+ "device.busy": "transport_lost",
68
+ "device.attached_elsewhere": "transport_lost",
69
+ "device.claim_conflict": "transport_lost",
70
+ "device.provider_mismatch": "permission_denied",
71
+ "device.identity_mismatch": "permission_denied",
72
+ "device.revoked": "permission_denied",
73
+ "device.enrollment_invalid": "permission_denied",
74
+ "not_found.device": "permission_denied",
75
+ "limit.rate_exceeded": "transport_lost",
76
+ };
77
+ const DEVICE_REJECT_TO_REMOTE_ERROR = {
78
+ "device.not_bound": "connect_failed",
79
+ "device.offline": "connect_failed",
80
+ "device.busy": "connect_failed",
81
+ "device.attached_elsewhere": "connect_failed",
82
+ "device.claim_conflict": "auth_failed",
83
+ "device.provider_mismatch": "auth_failed",
84
+ "device.identity_mismatch": "auth_failed",
85
+ "device.revoked": "auth_failed",
86
+ "device.enrollment_invalid": "auth_failed",
87
+ "not_found.device": "auth_failed",
88
+ "limit.rate_exceeded": "connect_failed",
89
+ };
90
+ function rejectText(reject) {
91
+ return `[${reject.code}] ${reject.message}`;
92
+ }
93
+ function assertPosixDevicePlatform(os, deviceId) {
94
+ switch (os) {
95
+ case "darwin":
96
+ case "linux":
97
+ return;
98
+ }
99
+ throw new Error(`device lane: device ${deviceId} declares a platform this adapter has no path flavor for (${String(os)}) — win32 is a follow-on (v2 §4.2 AR-R1-8)`);
100
+ }
101
+ export class RemoteDeviceExecutionEnv {
102
+ cfg;
103
+ capabilities = { isolation: false, suspendable: false };
104
+ externalContentTarget = true;
105
+ hostLocalPaths = false;
106
+ cwd;
107
+ readLink;
108
+ writeFileExclusive;
109
+ writeFileGuarded;
110
+ connected = false;
111
+ destroyed = false;
112
+ constructor(cfg) {
113
+ this.cfg = cfg;
114
+ this.cwd = cfg.cwd;
115
+ if (cfg.supports?.has("readLink") === true)
116
+ this.readLink = (p, s) => this.doReadLink(p, s);
117
+ if (cfg.supports?.has("writeFileExclusive") === true)
118
+ this.writeFileExclusive = (p, c, s) => this.doWrite("writeFileExclusive", p, c, undefined, s);
119
+ if (cfg.supports?.has("writeFileGuarded") === true)
120
+ this.writeFileGuarded = (p, c, e, s) => this.doGuardedWrite(p, c, e, s);
121
+ }
122
+ guardFaceSnapshot() {
123
+ const out = new Set();
124
+ if (this.writeFileGuarded !== undefined)
125
+ out.add("writeFileGuarded");
126
+ if (this.writeFileExclusive !== undefined)
127
+ out.add("writeFileExclusive");
128
+ return out;
129
+ }
130
+ guardFaceDrift() {
131
+ const now = this.cfg.hub.declaredSupports(this.cfg.deviceId);
132
+ if (now === undefined)
133
+ return [];
134
+ const minted = this.guardFaceSnapshot();
135
+ return ["writeFileGuarded", "writeFileExclusive"].filter((k) => now.has(k) && !minted.has(k));
136
+ }
137
+ absentGuardFaces() {
138
+ const minted = this.guardFaceSnapshot();
139
+ return ["writeFileGuarded", "writeFileExclusive"].filter((k) => !minted.has(k));
140
+ }
141
+ driftError(gained, absPath) {
142
+ return new FileError("not_supported", `[device.capability_drift] the connected device now declares protective write primitives (${gained.join(", ")}) that this environment was minted without — ` +
143
+ "performing a NON-atomic write here is exactly the silent downgrade guarded writes exist to prevent (#157). Refusing; the per-task factory mints a correctly-shaped env on the next task.", absPath);
144
+ }
145
+ resolve(p) {
146
+ return posix.isAbsolute(p) ? posix.normalize(p) : posix.normalize(posix.join(this.cwd, p));
147
+ }
148
+ async absolutePath(p) {
149
+ return ok(this.resolve(p));
150
+ }
151
+ async joinPath(parts) {
152
+ return ok(posix.join(...parts));
153
+ }
154
+ workspaceHandle() {
155
+ return {
156
+ sandboxId: `${this.cfg.deviceId}#${this.cwd}`,
157
+ provider: DEVICE_PROVIDER,
158
+ mountPath: this.cwd,
159
+ deviceId: this.cfg.deviceId,
160
+ restoreMode: "park_only",
161
+ };
162
+ }
163
+ async connect(config) {
164
+ if (this.destroyed)
165
+ return { ok: false, error: new RemoteExecutionError("connect_failed", "device env destroyed — create a fresh env instead of reviving") };
166
+ if (config?.abortSignal?.aborted === true)
167
+ return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
168
+ const admitted = await this.cfg.enrollment.admitSession({ rootSessionId: this.cfg.rootSessionId, requester: this.cfg.owner });
169
+ if (!admitted.ok) {
170
+ return { ok: false, error: new RemoteExecutionError(DEVICE_REJECT_TO_REMOTE_ERROR[admitted.reject.code], rejectText(admitted.reject)) };
171
+ }
172
+ if (admitted.value.deviceId !== this.cfg.deviceId) {
173
+ return {
174
+ ok: false,
175
+ error: new RemoteExecutionError("auth_failed", `[${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] this session is now bound to a different device than the one this environment was minted for — refusing to silently re-target (device lane identity, v2 §4.5)`),
176
+ };
177
+ }
178
+ {
179
+ const gained = this.guardFaceDrift();
180
+ if (gained.length > 0) {
181
+ return {
182
+ ok: false,
183
+ error: new RemoteExecutionError("connect_failed", `[device.capability_drift] this environment was minted while the device's protective write primitives (${gained.join(", ")}) were not observable, ` +
184
+ "so they are absent from its filesystem face — core would read that absence as \"the backend has no such primitive\" and fall back to a NON-atomic write, " +
185
+ "silently re-opening the TOCTOU window guarded writes exist to close (#157). Refusing this connect; the per-task factory will mint a correctly-shaped env on retry."),
186
+ };
187
+ }
188
+ }
189
+ this.connected = true;
190
+ return ok(this.workspaceHandle());
191
+ }
192
+ async reconnect(_sessionToken) {
193
+ this.connected = false;
194
+ return this.connect();
195
+ }
196
+ async suspendVM(_options) {
197
+ return this.unsupported("suspendVM");
198
+ }
199
+ async resumeVM(_snapshotId, _options) {
200
+ return this.unsupported("resumeVM");
201
+ }
202
+ async postResumeInit() {
203
+ return this.unsupported("postResumeInit");
204
+ }
205
+ unsupported(op) {
206
+ return {
207
+ ok: false,
208
+ error: new RemoteExecutionError("unsupported", `${op} is not supported on a personal device (real hardware — not snapshotable; capabilities.suspendable=false)`),
209
+ };
210
+ }
211
+ async destroy() {
212
+ this.connected = false;
213
+ this.destroyed = true;
214
+ }
215
+ async cleanup() {
216
+ return this.destroy();
217
+ }
218
+ shellGate() {
219
+ if (this.destroyed)
220
+ return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
221
+ if (!this.connected)
222
+ return { ok: false, error: new ExecutionError("shell_unavailable", "device env not connected — call connect() first") };
223
+ return undefined;
224
+ }
225
+ fsGate(p) {
226
+ if (this.destroyed)
227
+ return { ok: false, error: new FileError("unknown", "execution env already destroyed", this.resolve(p)) };
228
+ if (!this.connected)
229
+ return { ok: false, error: new FileError("unknown", "device env not connected — call connect() first", this.resolve(p)) };
230
+ return undefined;
231
+ }
232
+ timeoutMsFor(seconds) {
233
+ return seconds != null && Number.isFinite(seconds) && seconds > 0 ? Math.ceil(seconds * 1000) : this.cfg.execTimeoutMs;
234
+ }
235
+ dispatch(kind, args, opts) {
236
+ const input = {
237
+ rootSessionId: this.cfg.rootSessionId,
238
+ requester: this.cfg.owner,
239
+ kind,
240
+ args,
241
+ timeoutMs: opts.timeoutMs,
242
+ ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
243
+ ...(opts.shellEnv === undefined ? {} : { shellEnv: opts.shellEnv }),
244
+ ...(opts.payload === undefined ? {} : { payload: opts.payload }),
245
+ ...(opts.onChunk === undefined ? {} : { onChunk: opts.onChunk }),
246
+ ...(opts.signal === undefined ? {} : { signal: opts.signal }),
247
+ ...(opts.refuseIfDeclared === undefined || opts.refuseIfDeclared.length === 0 ? {} : { refuseIfDeclared: opts.refuseIfDeclared }),
248
+ };
249
+ return this.cfg.hub.dispatch(input);
250
+ }
251
+ fileOutcome(res, absPath, take) {
252
+ switch (res.kind) {
253
+ case "rejected":
254
+ return { ok: false, error: new FileError(DEVICE_REJECT_TO_FILE_ERROR[res.reject.code], rejectText(res.reject), absPath) };
255
+ case "never_started":
256
+ case "settled": {
257
+ const o = res.outcome;
258
+ if (!o.ok)
259
+ return { ok: false, error: new FileError(DEVICE_OUTCOME_TO_FILE_ERROR[o.errorCode], o.message, absPath) };
260
+ return take(o);
261
+ }
262
+ }
263
+ }
264
+ wrongOutcome(want, got, absPath) {
265
+ return { ok: false, error: new FileError("unknown", `device returned a "${got}" outcome for an operation that requires "${want}" — protocol violation`, absPath) };
266
+ }
267
+ async exec(command, options) {
268
+ const gated = this.shellGate();
269
+ if (gated)
270
+ return gated;
271
+ let stdout = "";
272
+ let stderr = "";
273
+ const res = await this.dispatch("exec", { command }, {
274
+ timeoutMs: this.timeoutMsFor(options?.timeout),
275
+ ...(options?.abortSignal === undefined ? {} : { signal: options.abortSignal }),
276
+ cwd: options?.cwd === undefined ? this.cwd : this.resolve(options.cwd),
277
+ ...(mergedEnv(this.cfg.shellEnv, options?.env) === undefined ? {} : { shellEnv: mergedEnv(this.cfg.shellEnv, options?.env) }),
278
+ onChunk: (c) => {
279
+ if (c.kind === "stdout") {
280
+ const s = c.data?.toString("utf8") ?? "";
281
+ stdout += s;
282
+ options?.onStdout?.(s);
283
+ }
284
+ else if (c.kind === "stderr") {
285
+ const s = c.data?.toString("utf8") ?? "";
286
+ stderr += s;
287
+ options?.onStderr?.(s);
288
+ }
289
+ },
290
+ });
291
+ switch (res.kind) {
292
+ case "rejected":
293
+ return { ok: false, error: new ExecutionError(DEVICE_REJECT_TO_EXEC_ERROR[res.reject.code], rejectText(res.reject)) };
294
+ case "never_started":
295
+ case "settled": {
296
+ const o = res.outcome;
297
+ if (!o.ok) {
298
+ return { ok: false, error: new ExecutionError(DEVICE_OUTCOME_TO_EXEC_ERROR[o.errorCode], o.message, undefined, { stdout, stderr }) };
299
+ }
300
+ if (o.kind === "cancelled") {
301
+ return { ok: false, error: new ExecutionError("aborted", `the device reported the command as ${o.state} after a cancel`, undefined, { stdout, stderr }) };
302
+ }
303
+ if (o.kind !== "exec")
304
+ return { ok: false, error: new ExecutionError("unknown", `device returned a "${o.kind}" outcome for an exec — protocol violation`) };
305
+ return ok({ stdout: markTruncated(stdout, o.truncated?.stdout), stderr: markTruncated(stderr, o.truncated?.stderr), exitCode: o.exitCode });
306
+ }
307
+ }
308
+ }
309
+ async *execStream(command, options) {
310
+ if (this.destroyed)
311
+ throw new RemoteExecutionError("connect_failed", "execution env already destroyed");
312
+ if (!this.connected)
313
+ throw new RemoteExecutionError("connect_failed", "device env not connected — call connect() first");
314
+ const queue = [];
315
+ let queuedBytes = 0;
316
+ let done = false;
317
+ let failure;
318
+ let localOverflow;
319
+ let accepting = true;
320
+ let droppedAfterClose = 0;
321
+ const closeQueue = () => {
322
+ if (!accepting)
323
+ return;
324
+ accepting = false;
325
+ queue.length = 0;
326
+ queuedBytes = 0;
327
+ };
328
+ let wake;
329
+ const signalReady = () => {
330
+ const w = wake;
331
+ wake = undefined;
332
+ w?.();
333
+ };
334
+ const inner = new AbortController();
335
+ let onOuterAbort;
336
+ if (options?.signal) {
337
+ if (options.signal.aborted)
338
+ inner.abort();
339
+ else {
340
+ onOuterAbort = () => inner.abort();
341
+ options.signal.addEventListener("abort", onOuterAbort, { once: true });
342
+ }
343
+ }
344
+ const settled = this.dispatch("execStream", { command }, {
345
+ timeoutMs: this.timeoutMsFor(options?.timeout),
346
+ signal: inner.signal,
347
+ cwd: options?.cwd === undefined ? this.cwd : this.resolve(options.cwd),
348
+ ...(mergedEnv(this.cfg.shellEnv, options?.env) === undefined ? {} : { shellEnv: mergedEnv(this.cfg.shellEnv, options?.env) }),
349
+ onChunk: (c) => {
350
+ if (!accepting) {
351
+ if (droppedAfterClose === 0) {
352
+ this.cfg.logger?.warn("device_exec_stream_chunks_dropped_after_close", {
353
+ deviceId: this.cfg.deviceId,
354
+ note: "the stream was refused (buffer overflow) or its consumer left; the remote command has been cancelled but the device keeps sending until its own grace elapses — these chunks are dropped instead of accumulating",
355
+ });
356
+ }
357
+ droppedAfterClose += 1;
358
+ return;
359
+ }
360
+ if (c.kind === "stdout" || c.kind === "stderr") {
361
+ const cost = c.data?.length ?? 0;
362
+ queue.push({ chunk: { type: c.kind, data: c.data?.toString("utf8") ?? "" }, cost });
363
+ queuedBytes += cost;
364
+ }
365
+ else if (c.kind === "exit") {
366
+ queue.push({ chunk: { type: "exit", exitCode: c.exitCode ?? 0 }, cost: 0 });
367
+ }
368
+ else {
369
+ this.cfg.logger?.warn("device_exec_stream_unexpected_chunk_kind", { deviceId: this.cfg.deviceId, kind: c.kind });
370
+ }
371
+ if (queuedBytes > STREAM_PENDING_MAX_BYTES || queue.length > STREAM_PENDING_MAX_ENTRIES) {
372
+ localOverflow ??= new RemoteExecutionError("unknown", `device execStream pending stream buffer exceeded its bound (${queuedBytes} bytes / ${queue.length} entries; caps ${STREAM_PENDING_MAX_BYTES} bytes / ${STREAM_PENDING_MAX_ENTRIES} entries) ` +
373
+ "with an un-drained consumer — the remote command was cancelled and this stream is refused rather than silently truncated");
374
+ closeQueue();
375
+ inner.abort();
376
+ }
377
+ signalReady();
378
+ },
379
+ });
380
+ void settled.then((res) => {
381
+ failure ??= streamFailure(res);
382
+ done = true;
383
+ signalReady();
384
+ }, (err) => {
385
+ failure ??= new RemoteExecutionError("unknown", err instanceof Error ? err.message : String(err));
386
+ done = true;
387
+ signalReady();
388
+ });
389
+ try {
390
+ for (;;) {
391
+ if (localOverflow)
392
+ throw localOverflow;
393
+ while (queue.length > 0) {
394
+ const item = queue.shift();
395
+ queuedBytes -= item.cost;
396
+ if (failure && item.chunk.type === "exit")
397
+ continue;
398
+ yield item.chunk;
399
+ if (localOverflow)
400
+ throw localOverflow;
401
+ }
402
+ if (failure)
403
+ throw failure;
404
+ if (done)
405
+ break;
406
+ await new Promise((r) => {
407
+ wake = r;
408
+ });
409
+ }
410
+ while (queue.length > 0) {
411
+ const item = queue.shift();
412
+ queuedBytes -= item.cost;
413
+ yield item.chunk;
414
+ }
415
+ }
416
+ finally {
417
+ closeQueue();
418
+ inner.abort();
419
+ if (onOuterAbort)
420
+ options?.signal?.removeEventListener("abort", onOuterAbort);
421
+ void settled.catch(() => undefined);
422
+ }
423
+ }
424
+ async exists(p, abortSignal) {
425
+ const gated = this.fsGate(p);
426
+ if (gated)
427
+ return gated;
428
+ const abs = this.resolve(p);
429
+ const res = await this.dispatch("exists", { path: abs }, { timeoutMs: this.cfg.execTimeoutMs, ...(abortSignal === undefined ? {} : { signal: abortSignal }) });
430
+ return this.fileOutcome(res, abs, (o) => {
431
+ if (o.kind !== "stat")
432
+ return this.wrongOutcome("stat", o.kind, abs);
433
+ if (typeof o.existsValue !== "boolean")
434
+ return { ok: false, error: new FileError("unknown", "device returned a stat outcome without an existsValue — protocol violation", abs) };
435
+ return ok(o.existsValue);
436
+ });
437
+ }
438
+ async canonicalPath(p, abortSignal) {
439
+ const gated = this.fsGate(p);
440
+ if (gated)
441
+ return gated;
442
+ const abs = this.resolve(p);
443
+ const res = await this.dispatch("canonicalPath", { path: abs }, { timeoutMs: this.cfg.execTimeoutMs, ...(abortSignal === undefined ? {} : { signal: abortSignal }) });
444
+ return this.fileOutcome(res, abs, (o) => {
445
+ if (o.kind !== "stat")
446
+ return this.wrongOutcome("stat", o.kind, abs);
447
+ if (typeof o.canonicalPath !== "string")
448
+ return { ok: false, error: new FileError("unknown", "device returned a stat outcome without a canonicalPath — protocol violation", abs) };
449
+ return ok(o.canonicalPath);
450
+ });
451
+ }
452
+ async fileInfo(p, abortSignal) {
453
+ const gated = this.fsGate(p);
454
+ if (gated)
455
+ return gated;
456
+ const abs = this.resolve(p);
457
+ const res = await this.dispatch("fileInfo", { path: abs }, { timeoutMs: this.cfg.execTimeoutMs, ...(abortSignal === undefined ? {} : { signal: abortSignal }) });
458
+ return this.fileOutcome(res, abs, (o) => {
459
+ if (o.kind !== "stat")
460
+ return this.wrongOutcome("stat", o.kind, abs);
461
+ if (!o.info)
462
+ return { ok: false, error: new FileError("unknown", "device returned a stat outcome without info — protocol violation", abs) };
463
+ return ok(toFileInfo(o.info));
464
+ });
465
+ }
466
+ async doReadLink(p, abortSignal) {
467
+ const gated = this.fsGate(p);
468
+ if (gated)
469
+ return gated;
470
+ const abs = this.resolve(p);
471
+ const res = await this.dispatch("readLink", { path: abs }, { timeoutMs: this.cfg.execTimeoutMs, ...(abortSignal === undefined ? {} : { signal: abortSignal }) });
472
+ return this.fileOutcome(res, abs, (o) => {
473
+ if (o.kind !== "stat")
474
+ return this.wrongOutcome("stat", o.kind, abs);
475
+ if (o.linkTarget === null)
476
+ return { ok: false, error: new FileError("invalid", "not a symlink", abs) };
477
+ if (typeof o.linkTarget !== "string")
478
+ return { ok: false, error: new FileError("unknown", "device returned a stat outcome without a linkTarget — protocol violation", abs) };
479
+ return ok(o.linkTarget);
480
+ });
481
+ }
482
+ async readBinaryFile(p, abortSignal) {
483
+ const gated = this.fsGate(p);
484
+ if (gated)
485
+ return gated;
486
+ const abs = this.resolve(p);
487
+ const parts = [];
488
+ const res = await this.dispatch("readBinaryFile", { path: abs }, {
489
+ timeoutMs: this.cfg.execTimeoutMs,
490
+ ...(abortSignal === undefined ? {} : { signal: abortSignal }),
491
+ onChunk: (c) => {
492
+ if (c.kind === "data" && c.data)
493
+ parts.push(c.data);
494
+ },
495
+ });
496
+ return this.fileOutcome(res, abs, (o) => (o.kind === "read" ? ok(new Uint8Array(Buffer.concat(parts))) : this.wrongOutcome("read", o.kind, abs)));
497
+ }
498
+ async readTextFile(p, abortSignal) {
499
+ const gated = this.fsGate(p);
500
+ if (gated)
501
+ return gated;
502
+ const abs = this.resolve(p);
503
+ const parts = [];
504
+ const res = await this.dispatch("readTextFile", { path: abs }, {
505
+ timeoutMs: this.cfg.execTimeoutMs,
506
+ ...(abortSignal === undefined ? {} : { signal: abortSignal }),
507
+ onChunk: (c) => {
508
+ if (c.kind === "data" && c.data)
509
+ parts.push(c.data);
510
+ },
511
+ });
512
+ return this.fileOutcome(res, abs, (o) => (o.kind === "read" ? ok(Buffer.concat(parts).toString("utf8")) : this.wrongOutcome("read", o.kind, abs)));
513
+ }
514
+ async readTextLines(p, options) {
515
+ const gated = this.fsGate(p);
516
+ if (gated)
517
+ return gated;
518
+ const abs = this.resolve(p);
519
+ const parts = [];
520
+ const res = await this.dispatch("readTextLines", { path: abs, ...(options?.maxLines === undefined ? {} : { limit: options.maxLines }) }, {
521
+ timeoutMs: this.cfg.execTimeoutMs,
522
+ ...(options?.abortSignal === undefined ? {} : { signal: options.abortSignal }),
523
+ onChunk: (c) => {
524
+ if (c.kind === "data" && c.data)
525
+ parts.push(c.data);
526
+ },
527
+ });
528
+ return this.fileOutcome(res, abs, (o) => {
529
+ if (o.kind !== "read")
530
+ return this.wrongOutcome("read", o.kind, abs);
531
+ const lines = Buffer.concat(parts).toString("utf8").split(/\r?\n/);
532
+ if (lines.length > 0 && lines[lines.length - 1] === "")
533
+ lines.pop();
534
+ return ok(options?.maxLines != null ? lines.slice(0, options.maxLines) : lines);
535
+ });
536
+ }
537
+ async listDir(p, abortSignal) {
538
+ const gated = this.fsGate(p);
539
+ if (gated)
540
+ return gated;
541
+ const abs = this.resolve(p);
542
+ const res = await this.dispatch("listDir", { path: abs }, { timeoutMs: this.cfg.execTimeoutMs, ...(abortSignal === undefined ? {} : { signal: abortSignal }) });
543
+ return this.fileOutcome(res, abs, (o) => {
544
+ if (o.kind !== "stat")
545
+ return this.wrongOutcome("stat", o.kind, abs);
546
+ if (!o.entries)
547
+ return { ok: false, error: new FileError("unknown", "device returned a stat outcome without entries — protocol violation", abs) };
548
+ return ok(o.entries.map(toFileInfo));
549
+ });
550
+ }
551
+ async writeFile(p, content, abortSignal) {
552
+ return this.doWrite("writeFile", p, content, undefined, abortSignal);
553
+ }
554
+ async appendFile(p, content, abortSignal) {
555
+ return this.doWrite("appendFile", p, content, undefined, abortSignal);
556
+ }
557
+ async doWrite(kind, p, content, _unused, abortSignal) {
558
+ const gated = this.fsGate(p);
559
+ if (gated)
560
+ return gated;
561
+ const abs = this.resolve(p);
562
+ if (kind !== "writeFileExclusive") {
563
+ const gained = this.guardFaceDrift();
564
+ if (gained.length > 0)
565
+ return { ok: false, error: this.driftError(gained, abs) };
566
+ }
567
+ const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
568
+ const res = await this.dispatch(kind, { path: abs, contentLen: bytes.length, sha256: "" }, {
569
+ timeoutMs: this.cfg.execTimeoutMs,
570
+ payload: bytes,
571
+ ...(abortSignal === undefined ? {} : { signal: abortSignal }),
572
+ ...(kind === "writeFileExclusive" ? {} : { refuseIfDeclared: this.absentGuardFaces() }),
573
+ });
574
+ return this.fileOutcome(res, abs, (o) => {
575
+ if (kind === "appendFile")
576
+ return o.kind === "void" ? ok(undefined) : this.wrongOutcome("void", o.kind, abs);
577
+ return o.kind === "write" ? ok(undefined) : this.wrongOutcome("write", o.kind, abs);
578
+ });
579
+ }
580
+ async doGuardedWrite(p, content, expect, abortSignal) {
581
+ const gated = this.fsGate(p);
582
+ if (gated)
583
+ return gated;
584
+ const abs = this.resolve(p);
585
+ const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
586
+ const res = await this.dispatch("writeFileGuarded", { path: abs, contentLen: bytes.length, sha256: "", expect: toWireExpectation(expect) }, { timeoutMs: this.cfg.execTimeoutMs, payload: bytes, ...(abortSignal === undefined ? {} : { signal: abortSignal }) });
587
+ return this.fileOutcome(res, abs, (o) => {
588
+ if (o.kind !== "write")
589
+ return this.wrongOutcome("write", o.kind, abs);
590
+ return ok({
591
+ canonicalPath: o.canonicalPath,
592
+ ...(o.inode === undefined ? {} : { fileId: o.inode }),
593
+ created: o.created ?? this.warnMissingCreated(abs),
594
+ });
595
+ });
596
+ }
597
+ warnMissingCreated(abs) {
598
+ this.cfg.logger?.warn("device_write_receipt_missing_created", {
599
+ deviceId: this.cfg.deviceId,
600
+ path: abs,
601
+ note: "the device's write receipt omitted `created`; core's WriteReceipt has no 'unknown' spelling, so the weaker side (false) is reported — the executor should send it",
602
+ });
603
+ return false;
604
+ }
605
+ async createDir(p, options) {
606
+ const gated = this.fsGate(p);
607
+ if (gated)
608
+ return gated;
609
+ const abs = this.resolve(p);
610
+ const res = await this.dispatch("createDir", { path: abs, recursive: options?.recursive ?? true }, { timeoutMs: this.cfg.execTimeoutMs, ...(options?.abortSignal === undefined ? {} : { signal: options.abortSignal }) });
611
+ return this.fileOutcome(res, abs, (o) => (o.kind === "void" ? ok(undefined) : this.wrongOutcome("void", o.kind, abs)));
612
+ }
613
+ async remove(p, options) {
614
+ const gated = this.fsGate(p);
615
+ if (gated)
616
+ return gated;
617
+ const abs = this.resolve(p);
618
+ const res = await this.dispatch("remove", { path: abs, recursive: options?.recursive ?? false }, { timeoutMs: this.cfg.execTimeoutMs, ...(options?.abortSignal === undefined ? {} : { signal: options.abortSignal }) });
619
+ return this.fileOutcome(res, abs, (o) => (o.kind === "void" ? ok(undefined) : this.wrongOutcome("void", o.kind, abs)));
620
+ }
621
+ async createTempDir(prefix = "tmp-", abortSignal) {
622
+ const gated = this.fsGate(this.cwd);
623
+ if (gated)
624
+ return gated;
625
+ const res = await this.dispatch("createTempDir", { prefix }, { timeoutMs: this.cfg.execTimeoutMs, ...(abortSignal === undefined ? {} : { signal: abortSignal }) });
626
+ return this.fileOutcome(res, this.cwd, (o) => (o.kind === "write" ? ok(o.canonicalPath) : this.wrongOutcome("write", o.kind, this.cwd)));
627
+ }
628
+ async createTempFile(options) {
629
+ const gated = this.fsGate(this.cwd);
630
+ if (gated)
631
+ return gated;
632
+ const res = await this.dispatch("createTempFile", { ...(options?.prefix === undefined ? {} : { prefix: options.prefix }), ...(options?.suffix === undefined ? {} : { suffix: options.suffix }) }, { timeoutMs: this.cfg.execTimeoutMs, ...(options?.abortSignal === undefined ? {} : { signal: options.abortSignal }) });
633
+ return this.fileOutcome(res, this.cwd, (o) => (o.kind === "write" ? ok(o.canonicalPath) : this.wrongOutcome("write", o.kind, this.cwd)));
634
+ }
635
+ }
636
+ function mergedEnv(taskEnv, commandEnv) {
637
+ if (!taskEnv && !commandEnv)
638
+ return undefined;
639
+ return { ...(taskEnv ?? {}), ...(commandEnv ?? {}) };
640
+ }
641
+ function markTruncated(text, truncated) {
642
+ return truncated === true ? `${text}\n[output truncated by the device executor]` : text;
643
+ }
644
+ function toFileInfo(w) {
645
+ return { name: posix.basename(w.path), path: w.path, kind: w.kind === "dir" ? "directory" : w.kind === "symlink" ? "symlink" : "file", size: w.size, mtimeMs: w.mtimeMs };
646
+ }
647
+ function toWireExpectation(e) {
648
+ return {
649
+ canonicalPath: e.canonicalPath,
650
+ ...(e.fileId === undefined ? {} : { fileId: e.fileId }),
651
+ ...(e.exclusive === undefined ? {} : { exclusive: e.exclusive }),
652
+ ...(e.noFollow === undefined ? {} : { noFollow: e.noFollow }),
653
+ };
654
+ }
655
+ function streamFailure(res) {
656
+ switch (res.kind) {
657
+ case "rejected":
658
+ return new RemoteExecutionError(DEVICE_REJECT_TO_REMOTE_ERROR[res.reject.code], rejectText(res.reject));
659
+ case "never_started":
660
+ case "settled": {
661
+ const o = res.outcome;
662
+ if (o.ok)
663
+ return undefined;
664
+ return new RemoteExecutionError(OUTCOME_TO_REMOTE_ERROR[o.errorCode], o.message);
665
+ }
666
+ }
667
+ }
668
+ const OUTCOME_TO_REMOTE_ERROR = {
669
+ aborted: "aborted",
670
+ already_exists: "unknown",
671
+ auth_failed: "auth_failed",
672
+ callback_error: "unknown",
673
+ invalid: "unknown",
674
+ is_directory: "unknown",
675
+ not_directory: "unknown",
676
+ not_found: "unknown",
677
+ not_supported: "unsupported",
678
+ outcome_unknown: "outcome_unknown",
679
+ permission_denied: "unknown",
680
+ precondition_failed: "unknown",
681
+ shell_unavailable: "connect_failed",
682
+ spawn_error: "connect_failed",
683
+ suspended: "suspended",
684
+ target_unavailable: "connect_failed",
685
+ timeout: "timeout",
686
+ transport_lost: "transport_lost",
687
+ unknown: "unknown",
688
+ };
689
+ export function deviceExecutionEnvFactory(deps) {
690
+ const execTimeoutMs = deps.execTimeoutMs ?? 1_800_000;
691
+ return async (ctx) => {
692
+ const rootSessionId = ctx.placementRootSessionId;
693
+ if (typeof rootSessionId !== "string" || rootSessionId.length === 0) {
694
+ throw new Error("device lane: ExecutionEnvFactoryContext.placementRootSessionId is empty [config.placement_root_invalid] — " +
695
+ "the device placement is keyed on the run tree's placement root; defaulting to the child sessionId would read as a NEW placement (v2 §4.3.2 / core design/380 O1①)");
696
+ }
697
+ const binding = await deps.store.getSessionBinding(rootSessionId);
698
+ if (!binding) {
699
+ throw new Error(`device lane: no device binding for placement root ${rootSessionId} [${DEVICE_ERROR_CODES.NOT_BOUND}] — ` +
700
+ "the submit-time admission chain must bind the root session to a device before a run reaches the factory (v2 §4.3.2)");
701
+ }
702
+ const view = await deps.store.readDeviceForAdmission(binding.deviceId, binding.owner);
703
+ if (!view) {
704
+ throw new Error(`device lane: the bound device is gone or not owned by the binding's principal [${DEVICE_ERROR_CODES.NOT_FOUND}]`);
705
+ }
706
+ if (view.row.status === "revoked") {
707
+ throw new Error(`device lane: the bound device has been revoked [${DEVICE_ERROR_CODES.REVOKED}] — revocation is terminal; re-enrolling mints a NEW deviceId (v2 §8-R4)`);
708
+ }
709
+ assertPosixDevicePlatform(view.row.platformOs, view.row.deviceId);
710
+ return new RemoteDeviceExecutionEnv({
711
+ hub: deps.hub,
712
+ enrollment: deps.enrollment,
713
+ rootSessionId,
714
+ deviceId: binding.deviceId,
715
+ owner: binding.owner,
716
+ cwd: effectiveDeviceCwd(deps.perSessionCwd?.get(ctx.sessionId), ctx.parentCwd, view.row),
717
+ ...(deps.perSessionShellEnv?.get(ctx.sessionId) === undefined ? {} : { shellEnv: deps.perSessionShellEnv.get(ctx.sessionId) }),
718
+ execTimeoutMs,
719
+ supports: deps.hub.declaredSupports(binding.deviceId),
720
+ ...(deps.logger === undefined ? {} : { logger: deps.logger }),
721
+ });
722
+ };
723
+ }
724
+ export function effectiveDeviceCwd(sessionCwd, parentCwd, row) {
725
+ return sessionCwd ?? parentCwd ?? row.workspaceRoot;
726
+ }
727
+ //# sourceMappingURL=remote-env-device.js.map