@opengeni/runtime 2.5.2 → 2.5.3-canary.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 (49) hide show
  1. package/dist/{agent-Q2VNH5FV.js → agent-RJQLKFIW.js} +2 -2
  2. package/dist/{chunk-LGLJ5PVD.js → chunk-46BFUGFL.js} +2622 -1368
  3. package/dist/chunk-46BFUGFL.js.map +1 -0
  4. package/dist/{chunk-CD22I76N.js → chunk-FHGLYLBL.js} +2 -2
  5. package/dist/{chunk-25B3EQRW.js → chunk-UNICCRW5.js} +4 -4
  6. package/dist/{chunk-GJC7H2FC.js → chunk-VUEWS4GI.js} +494 -95
  7. package/dist/chunk-VUEWS4GI.js.map +1 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +22 -4
  10. package/dist/{localSources-Z5IJIIRV.js → localSources-PVFCAPN2.js} +3 -3
  11. package/dist/mcp-operation-dispatch.d.ts +45 -0
  12. package/dist/mcp-operation-observation.d.ts +137 -0
  13. package/dist/operational-instructions.d.ts +1 -1
  14. package/dist/sandbox/archive-spool.d.ts +14 -0
  15. package/dist/sandbox/host-archive-spool.d.ts +19 -0
  16. package/dist/sandbox/index.d.ts +5 -1
  17. package/dist/sandbox/index.js +7 -3
  18. package/dist/sandbox/routing/backend-resolver.d.ts +1 -0
  19. package/dist/sandbox/selfhosted/file-transfer.d.ts +20 -0
  20. package/dist/sandbox/selfhosted/session.d.ts +7 -0
  21. package/dist/sandbox/workspace-archive.d.ts +7 -0
  22. package/dist/workspace-skills.d.ts +4 -0
  23. package/dist/workspace-tool-gateway.js +4 -4
  24. package/package.json +11 -11
  25. package/src/bundled_default_skills/document-parsing/SKILL.md +36 -0
  26. package/src/bundled_site_skills/opengeni-sites/SKILL.md +1 -1
  27. package/src/index.ts +231 -45
  28. package/src/lazy-tool-transport.ts +2 -0
  29. package/src/mcp-operation-dispatch.ts +144 -0
  30. package/src/mcp-operation-observation.ts +148 -0
  31. package/src/operational-instructions.ts +1 -1
  32. package/src/runtime-skills.ts +7 -2
  33. package/src/sandbox/archive-spool.ts +16 -0
  34. package/src/sandbox/host-archive-spool.ts +1040 -0
  35. package/src/sandbox/index.ts +292 -203
  36. package/src/sandbox/routing/backend-resolver.ts +2 -0
  37. package/src/sandbox/selfhosted/control-rpc.ts +22 -0
  38. package/src/sandbox/selfhosted/fault-rendering.ts +9 -0
  39. package/src/sandbox/selfhosted/file-transfer.ts +160 -0
  40. package/src/sandbox/selfhosted/op-testing.ts +3 -0
  41. package/src/sandbox/selfhosted/session.ts +194 -5
  42. package/src/sandbox/workspace-archive.ts +54 -1
  43. package/src/workspace-skills.ts +171 -31
  44. package/dist/chunk-GJC7H2FC.js.map +0 -1
  45. package/dist/chunk-LGLJ5PVD.js.map +0 -1
  46. /package/dist/{agent-Q2VNH5FV.js.map → agent-RJQLKFIW.js.map} +0 -0
  47. /package/dist/{chunk-CD22I76N.js.map → chunk-FHGLYLBL.js.map} +0 -0
  48. /package/dist/{chunk-25B3EQRW.js.map → chunk-UNICCRW5.js.map} +0 -0
  49. /package/dist/{localSources-Z5IJIIRV.js.map → localSources-PVFCAPN2.js.map} +0 -0
@@ -0,0 +1,160 @@
1
+ import { blake3 } from "@noble/hashes/blake3";
2
+ import { bytesToHex } from "@noble/hashes/utils";
3
+ import { ControlRequest, ControlResponse, OpState } from "@opengeni/agent-proto";
4
+ import { agentErrorToControlError } from "./control-rpc";
5
+
6
+ // Below the protocol's 512 KiB chunk maximum, leaving control-envelope headroom.
7
+ export const SELFHOSTED_FILE_CHUNK_BYTES = 256 * 1024;
8
+
9
+ export function fileContentDigest(content: Uint8Array): string {
10
+ return bytesToHex(blake3(content));
11
+ }
12
+
13
+ export class FileTransferOutcomeError extends Error {
14
+ readonly retryable = false;
15
+ constructor(
16
+ readonly operationId: string,
17
+ readonly phase: "begin" | "upload" | "verify",
18
+ cause: unknown,
19
+ readonly observationError?: unknown,
20
+ ) {
21
+ const detail = cause instanceof Error ? cause.message : "unknown failure";
22
+ const observation =
23
+ observationError instanceof Error ? ` Outcome check: ${observationError.message}` : "";
24
+ super(
25
+ `File transfer ${operationId} ${phase} failed: ${detail}.${observation} No mutation was replayed; inspect the destination before another edit.`,
26
+ { cause },
27
+ );
28
+ this.name = "FileTransferOutcomeError";
29
+ }
30
+ }
31
+
32
+ /** The caller must reauthorize every request and keep the exact physical route.
33
+ * No mutation is automatically replayed, including after an ambiguous reply. */
34
+ export async function transferEditorFile(input: {
35
+ path: string;
36
+ content: Uint8Array;
37
+ baseContent?: Uint8Array;
38
+ request: (requestId: string, op: NonNullable<ControlRequest["op"]>) => Promise<ControlResponse>;
39
+ }): Promise<{ recovered: boolean }> {
40
+ const opId = `fsw-${crypto.randomUUID()}`;
41
+ const digest = fileContentDigest(input.content);
42
+ const checked = (reply: ControlResponse) => {
43
+ if (reply.error) throw agentErrorToControlError(reply.error, reply.requestId);
44
+ if (!reply.result) throw new Error("The machine omitted the file-transfer result.");
45
+ return reply.result;
46
+ };
47
+ const send = async (requestId: string, op: NonNullable<ControlRequest["op"]>) => {
48
+ const reply = await input.request(requestId, op);
49
+ if (reply.requestId !== requestId)
50
+ throw new Error("The machine returned a mismatched file-transfer response identity.");
51
+ return checked(reply);
52
+ };
53
+ const isVerified = (observed: NonNullable<ControlResponse["result"]>): boolean =>
54
+ !(
55
+ observed.$case !== "opStatus" ||
56
+ observed.opStatus.opId !== opId ||
57
+ observed.opStatus.state !== OpState.OP_STATE_COMPLETE ||
58
+ observed.opStatus.exit?.exitCode !== 0 ||
59
+ observed.opStatus.exit.cancelled ||
60
+ observed.opStatus.exit.timedOut ||
61
+ observed.opStatus.exit.failureCode ||
62
+ observed.opStatus.exit.digests.content !== digest ||
63
+ observed.opStatus.exit.totals.content !== String(input.content.byteLength)
64
+ );
65
+ const verifyOutcome = async (cancelRunning = false): Promise<void> => {
66
+ const observed = await send(crypto.randomUUID(), { $case: "opQuery", opQuery: { opId } });
67
+ if (isVerified(observed)) return;
68
+ if (
69
+ cancelRunning &&
70
+ observed.$case === "opStatus" &&
71
+ observed.opStatus.opId === opId &&
72
+ observed.opStatus.state === OpState.OP_STATE_RUNNING
73
+ ) {
74
+ // This explicit transfer is being abandoned. Cancel only its known live
75
+ // identity through the same reauthorized callback; never sweep or expire
76
+ // other work. A commit racing cancellation must retain its real receipt.
77
+ const cancelled = await send(crypto.randomUUID(), { $case: "opCancel", opCancel: { opId } });
78
+ if (isVerified(cancelled)) return;
79
+ if (
80
+ cancelled.$case === "opStatus" &&
81
+ cancelled.opStatus.opId === opId &&
82
+ cancelled.opStatus.state === OpState.OP_STATE_COMPLETE &&
83
+ cancelled.opStatus.exit?.cancelled
84
+ ) {
85
+ throw new Error("Cancellation of the abandoned transfer's private staging was verified.");
86
+ }
87
+ }
88
+ throw new Error(
89
+ "The file-transfer outcome is not verified; the operation was not replayed. Inspect the destination before another edit.",
90
+ );
91
+ };
92
+ let phase: "begin" | "upload" | "verify" = "begin";
93
+ try {
94
+ const begun = await send(opId, {
95
+ $case: "opStart",
96
+ opStart: {
97
+ op: {
98
+ $case: "fsWrite",
99
+ fsWrite: {
100
+ path: input.path,
101
+ createParents: true,
102
+ mode: 0,
103
+ expectedBaseDigest:
104
+ input.baseContent === undefined ? "" : fileContentDigest(input.baseContent),
105
+ expectedAbsent: input.baseContent === undefined,
106
+ contentDigest: digest,
107
+ contentSize: String(input.content.byteLength),
108
+ },
109
+ },
110
+ windowBytes: "0",
111
+ deadlineMs: "0",
112
+ originId: "",
113
+ },
114
+ });
115
+ if (
116
+ begun.$case !== "opStart" ||
117
+ !begun.opStart.accepted ||
118
+ begun.opStart.status?.opId !== opId
119
+ ) {
120
+ throw new Error("The machine did not accept the exact file-transfer operation.");
121
+ }
122
+
123
+ phase = "upload";
124
+ let offset = 0;
125
+ let seq = 0;
126
+ do {
127
+ const end = Math.min(input.content.byteLength, offset + SELFHOSTED_FILE_CHUNK_BYTES);
128
+ const reply = await send(crypto.randomUUID(), {
129
+ $case: "writeChunk",
130
+ writeChunk: {
131
+ opId,
132
+ seq: String(seq),
133
+ offset: String(offset),
134
+ bytes: input.content.subarray(offset, end),
135
+ last: end === input.content.byteLength,
136
+ },
137
+ });
138
+ if (reply.$case !== "writeChunk" || reply.writeChunk.seq !== String(seq)) {
139
+ throw new Error(
140
+ "The machine returned a mismatched file-transfer acknowledgment; no chunk was replayed.",
141
+ );
142
+ }
143
+ offset = end;
144
+ seq += 1;
145
+ } while (offset < input.content.byteLength);
146
+ phase = "verify";
147
+ await verifyOutcome();
148
+ } catch (error) {
149
+ if (phase === "verify") throw new FileTransferOutcomeError(opId, phase, error);
150
+ // An acknowledgment can be lost after commit. Observe this exact operation
151
+ // once under current authority; never restart it or resend a chunk.
152
+ try {
153
+ await verifyOutcome(true);
154
+ return { recovered: true };
155
+ } catch (observationError) {
156
+ throw new FileTransferOutcomeError(opId, phase, error, observationError);
157
+ }
158
+ }
159
+ return { recovered: false };
160
+ }
@@ -459,6 +459,7 @@ export class FakeOpRunner implements ControlRpc {
459
459
  opId,
460
460
  state: complete ? OpState.OP_STATE_COMPLETE : OpState.OP_STATE_RUNNING,
461
461
  nextSeq: complete ? (run.exitSeq + 1n).toString() : "1",
462
+ writeOffset: "0",
462
463
  exit: complete ? run.exit : undefined,
463
464
  lostReason: OpLostReason.OP_LOST_REASON_UNSPECIFIED,
464
465
  };
@@ -617,6 +618,7 @@ function lostStatus(opId: string): OpStatus {
617
618
  return {
618
619
  opId,
619
620
  state: OpState.OP_STATE_LOST,
621
+ writeOffset: "0",
620
622
  nextSeq: "0",
621
623
  exit: undefined,
622
624
  lostReason: OpLostReason.OP_LOST_REASON_EVICTED,
@@ -627,6 +629,7 @@ function cancelledStatus(opId: string): OpStatus {
627
629
  return {
628
630
  opId,
629
631
  state: OpState.OP_STATE_COMPLETE,
632
+ writeOffset: "0",
630
633
  nextSeq: "1",
631
634
  exit: {
632
635
  exitCode: -1,
@@ -31,6 +31,11 @@ import {
31
31
  type StreamChannel,
32
32
  } from "@opengeni/agent-proto";
33
33
  import { DESKTOP_STREAM_PORT } from "@opengeni/contracts";
34
+ import {
35
+ SELFHOSTED_FILE_CHUNK_BYTES,
36
+ fileContentDigest,
37
+ transferEditorFile,
38
+ } from "./file-transfer";
34
39
  // `Manifest` from the ALLOWED sandbox-leaf entrypoint (`@openai/agents/sandbox`
35
40
  // re-exports `@openai/agents-core/sandbox`, which exports the Manifest class) —
36
41
  // NOT the agent-loop `@openai/agents` root the sandbox leaf forbids. The live
@@ -262,6 +267,7 @@ export interface SelfhostedBackgroundCommandAdoption {
262
267
  /** Exact command-admission snapshot. A worker resolves this immediately before
263
268
  * every exec/Git admission; retries and already-started operations retain it. */
264
269
  export interface SelfhostedOperationAdmission {
270
+ transactionalFsWriteSupported?: boolean;
265
271
  /** Workspace segment of the physical agent/relay route. Personal machines may
266
272
  * originate in a different same-organization workspace than the session. */
267
273
  workspaceId?: string;
@@ -284,6 +290,8 @@ export interface SelfhostedOperationAdmission {
284
290
  }
285
291
 
286
292
  export interface SelfhostedSessionDeps {
293
+ /** Exact live daemon capability; absent preserves legacy small-file writes. */
294
+ transactionalFsWriteSupported?: boolean;
287
295
  /** Authorization/session workspace. */
288
296
  workspaceId: string;
289
297
  /** Physical control-plane workspace. Defaults to workspaceId for legacy and
@@ -488,6 +496,7 @@ export class SelfhostedSession {
488
496
  private readonly resourcePolicy: OperationResourcePolicy | undefined;
489
497
  private readonly resourcePolicySupported: boolean;
490
498
  private readonly operationCpuQuotaSupported: boolean;
499
+ private readonly transactionalFsWriteSupported: boolean;
491
500
  private readonly defaultOpStream: SelfhostedOpStreamDeps | undefined;
492
501
  private readonly connectionInstanceId: string;
493
502
  private readonly resolveOperationAdmission:
@@ -560,6 +569,7 @@ export class SelfhostedSession {
560
569
  this.resourcePolicy = normalizeOperationResourcePolicy(deps.operationResourcePolicy);
561
570
  this.resourcePolicySupported = deps.operationResourcePolicySupported === true;
562
571
  this.operationCpuQuotaSupported = deps.operationCpuQuotaSupported === true;
572
+ this.transactionalFsWriteSupported = deps.transactionalFsWriteSupported === true;
563
573
  this.defaultOpStream = deps.opStream;
564
574
  this.connectionInstanceId = deps.connectionInstanceId;
565
575
  this.resolveOperationAdmission = deps.resolveOperationAdmission;
@@ -577,6 +587,7 @@ export class SelfhostedSession {
577
587
  subject: this.subject,
578
588
  resourcePolicy: this.resourcePolicy,
579
589
  opStream: deps.opStream,
590
+ transactionalFsWriteSupported: this.transactionalFsWriteSupported,
580
591
  })
581
592
  : undefined;
582
593
  // A valid Manifest whose root is the machine's actual effective root. There
@@ -644,6 +655,7 @@ export class SelfhostedSession {
644
655
  }
645
656
 
646
657
  private async admitOperation(commandPolicy: boolean): Promise<{
658
+ transactionalFsWriteSupported: boolean;
647
659
  controlWorkspaceId: string;
648
660
  connectionInstanceId: string;
649
661
  subject: string;
@@ -664,6 +676,7 @@ export class SelfhostedSession {
664
676
  subject: this.subject,
665
677
  resourcePolicy: commandPolicy ? this.resourcePolicy : undefined,
666
678
  opStream: this.defaultOpStream,
679
+ transactionalFsWriteSupported: this.transactionalFsWriteSupported,
667
680
  };
668
681
  }
669
682
 
@@ -698,6 +711,7 @@ export class SelfhostedSession {
698
711
  ),
699
712
  resourcePolicy: commandPolicy ? resourcePolicy : undefined,
700
713
  opStream: resolved.opStream,
714
+ transactionalFsWriteSupported: resolved.transactionalFsWriteSupported === true,
701
715
  };
702
716
  }
703
717
 
@@ -879,7 +893,9 @@ export class SelfhostedSession {
879
893
  machineId: this.agentId,
880
894
  faultClass: selfhostedFaultClass(error),
881
895
  // Known only on a PAYLOAD_TOO_LARGE fault (the agent's encoded_bytes detail).
882
- ...(Number.isFinite(encoded) ? { replyBytes: encoded } : {}),
896
+ ...(Number.isFinite(encoded) && error.detail.direction !== "request"
897
+ ? { replyBytes: encoded }
898
+ : {}),
883
899
  });
884
900
  // Fold the retry count into the DRAINING copy when we actually retried, so
885
901
  // the surfaced message reads "…retried N times first…".
@@ -1324,8 +1340,169 @@ export class SelfhostedSession {
1324
1340
  const pathExists = (path: string): Promise<boolean> => this.pathExists(path, runAs);
1325
1341
  const readText = async (path: string): Promise<string> =>
1326
1342
  decoder.decode(await this.readFile({ path, ...(runAs ? { runAs } : {}) }));
1327
- const writeText = async (path: string, content: string): Promise<void> => {
1328
- await this.writeFile({ path, content, createParents: true });
1343
+ const writeText = async (
1344
+ path: string,
1345
+ content: string,
1346
+ base?: string | (() => Promise<string | undefined>),
1347
+ ): Promise<((source: string, sourceBase: string) => Promise<void>) | undefined> => {
1348
+ const bytes = encoder.encode(content);
1349
+ if (bytes.byteLength <= SELFHOSTED_FILE_CHUNK_BYTES) {
1350
+ await this.writeFile({ path, content, createParents: true });
1351
+ return;
1352
+ }
1353
+ const initial = await this.admitOperation(false);
1354
+ if (!initial.transactionalFsWriteSupported) {
1355
+ // Older agents may have a larger negotiated message budget. Preserve
1356
+ // their existing writes; a real size rejection remains a typed fault.
1357
+ await this.writeFile({ path, content, createParents: true });
1358
+ return;
1359
+ }
1360
+ if (runAs) {
1361
+ throw new SelfhostedControlError({
1362
+ code: ErrorCode.ERROR_CODE_UNSUPPORTED,
1363
+ message:
1364
+ "Transactional file editing does not support runAs impersonation; no file was written.",
1365
+ reason: null,
1366
+ retryable: false,
1367
+ });
1368
+ }
1369
+ const requestTransfer = async (
1370
+ requestId: string,
1371
+ op: NonNullable<ControlRequest["op"]>,
1372
+ ): Promise<ControlResponse> => {
1373
+ const startedAt = Date.now();
1374
+ try {
1375
+ const current = await this.admitOperation(false);
1376
+ if (
1377
+ !current.transactionalFsWriteSupported ||
1378
+ current.subject !== initial.subject ||
1379
+ current.connectionInstanceId !== initial.connectionInstanceId
1380
+ ) {
1381
+ throw new Error(
1382
+ "The authorized machine connection changed during the file transfer; the operation was not replayed.",
1383
+ );
1384
+ }
1385
+ const response = await this.controlRpc.request(
1386
+ current.subject,
1387
+ {
1388
+ requestId,
1389
+ epoch: this.epoch,
1390
+ op,
1391
+ resourcePolicy: undefined,
1392
+ },
1393
+ { timeoutMs: this.timeoutMs },
1394
+ );
1395
+ if (response.requestId !== requestId)
1396
+ throw new Error("The machine returned a mismatched file-transfer response identity.");
1397
+ const error = response.error
1398
+ ? agentErrorToControlError(response.error, requestId)
1399
+ : undefined;
1400
+ this.emitOp({
1401
+ op: op.$case,
1402
+ outcome: error || !response.result ? "failed" : "ok",
1403
+ healed: false,
1404
+ retries: 0,
1405
+ durationMs: Date.now() - startedAt,
1406
+ machineId: this.agentId,
1407
+ ...(error
1408
+ ? { code: error.code, reason: error.reason, faultClass: selfhostedFaultClass(error) }
1409
+ : {}),
1410
+ });
1411
+ return response;
1412
+ } catch (error) {
1413
+ this.emitOp({
1414
+ op: op.$case,
1415
+ outcome: "failed",
1416
+ healed: false,
1417
+ retries: 0,
1418
+ durationMs: Date.now() - startedAt,
1419
+ machineId: this.agentId,
1420
+ ...(error instanceof SelfhostedControlError
1421
+ ? { code: error.code, reason: error.reason, faultClass: selfhostedFaultClass(error) }
1422
+ : {}),
1423
+ });
1424
+ throw error;
1425
+ }
1426
+ };
1427
+ const resolvedBase = typeof base === "function" ? await base() : base;
1428
+ const transferStartedAt = Date.now();
1429
+ try {
1430
+ const result = await transferEditorFile({
1431
+ path: resolveConnectedMachinePath(this.workspaceRoot, path),
1432
+ content: bytes,
1433
+ ...(resolvedBase === undefined ? {} : { baseContent: encoder.encode(resolvedBase) }),
1434
+ request: requestTransfer,
1435
+ });
1436
+ // Unlike physical request observations, this is a verified file outcome.
1437
+ this.emitOp({
1438
+ op: "fsWrite",
1439
+ outcome: "ok",
1440
+ healed: result.recovered,
1441
+ retries: 0,
1442
+ durationMs: Date.now() - transferStartedAt,
1443
+ machineId: this.agentId,
1444
+ });
1445
+ } catch (error) {
1446
+ this.emitOp({
1447
+ op: "fsWrite",
1448
+ outcome: "failed",
1449
+ healed: false,
1450
+ retries: 0,
1451
+ durationMs: Date.now() - transferStartedAt,
1452
+ machineId: this.agentId,
1453
+ });
1454
+ throw error;
1455
+ }
1456
+ return async (source, sourceBase) => {
1457
+ // A move is verified destination replacement followed by source cleanup,
1458
+ // not an atomic rename. Never retarget cleanup after a runner change.
1459
+ // The source check and deletion are not CAS against unrelated writers.
1460
+ try {
1461
+ const sourcePath = resolveConnectedMachinePath(this.workspaceRoot, source);
1462
+ const sourceBytes: Uint8Array[] = [];
1463
+ let offset = 0;
1464
+ let total = Infinity;
1465
+ while (offset < total) {
1466
+ const reply = await requestTransfer(crypto.randomUUID(), {
1467
+ $case: "fsRead",
1468
+ fsRead: {
1469
+ path: sourcePath,
1470
+ offset: String(offset),
1471
+ length: String(SELFHOSTED_FILE_CHUNK_BYTES),
1472
+ },
1473
+ });
1474
+ if (reply.error) throw agentErrorToControlError(reply.error);
1475
+ if (reply.result?.$case !== "fsRead")
1476
+ throw new Error("Source verification response missing");
1477
+ const part = reply.result.fsRead;
1478
+ total = safeWireSize(part.totalSize, "source file size");
1479
+ if (!part.content.length && offset < total)
1480
+ throw new Error("Source verification was incomplete");
1481
+ sourceBytes.push(part.content);
1482
+ offset += part.content.length;
1483
+ }
1484
+ const actual = new Uint8Array(offset);
1485
+ let cursor = 0;
1486
+ for (const chunk of sourceBytes) {
1487
+ actual.set(chunk, cursor);
1488
+ cursor += chunk.length;
1489
+ }
1490
+ if (fileContentDigest(actual) !== fileContentDigest(encoder.encode(sourceBase)))
1491
+ throw new Error("Source changed before move cleanup");
1492
+ const reply = await requestTransfer(crypto.randomUUID(), {
1493
+ $case: "fsRemove",
1494
+ fsRemove: { path: sourcePath, recursive: false },
1495
+ });
1496
+ if (reply.error) throw agentErrorToControlError(reply.error);
1497
+ if (reply.result?.$case !== "fsRemove")
1498
+ throw new Error("Source cleanup response missing");
1499
+ } catch (cause) {
1500
+ throw new Error(
1501
+ "The destination edit was verified, but source cleanup was not verified. The move was not replayed; inspect both paths.",
1502
+ { cause },
1503
+ );
1504
+ }
1505
+ };
1329
1506
  };
1330
1507
  const deletePath = async (path: string): Promise<void> => {
1331
1508
  const result = await this.call({
@@ -1351,9 +1528,15 @@ export class SelfhostedSession {
1351
1528
  const current = await readText(operation.path);
1352
1529
  const next = applyDiff(current, operation.diff);
1353
1530
  const destination = operation.moveTo ?? operation.path;
1354
- await writeText(destination, next);
1531
+ const destinationBase =
1532
+ destination === operation.path
1533
+ ? current
1534
+ : async () =>
1535
+ (await pathExists(destination)) ? await readText(destination) : undefined;
1536
+ const cleanup = await writeText(destination, next, destinationBase);
1355
1537
  if (operation.moveTo && destination !== operation.path) {
1356
- await deletePath(operation.path);
1538
+ if (cleanup) await cleanup(operation.path, current);
1539
+ else await deletePath(operation.path);
1357
1540
  }
1358
1541
  return {};
1359
1542
  },
@@ -1708,6 +1891,7 @@ export class SelfhostedSandboxClient {
1708
1891
  private readonly operationResourcePolicy: SelfhostedOperationResourcePolicy | undefined;
1709
1892
  private readonly operationResourcePolicySupported: boolean | undefined;
1710
1893
  private readonly operationCpuQuotaSupported: boolean | undefined;
1894
+ private readonly transactionalFsWriteSupported: boolean | undefined;
1711
1895
  private readonly resolveOperationAdmission:
1712
1896
  | (() => Promise<SelfhostedOperationAdmission | null>)
1713
1897
  | undefined;
@@ -1748,6 +1932,7 @@ export class SelfhostedSandboxClient {
1748
1932
  operationResourcePolicy?: SelfhostedOperationResourcePolicy;
1749
1933
  operationResourcePolicySupported?: boolean;
1750
1934
  operationCpuQuotaSupported?: boolean;
1935
+ transactionalFsWriteSupported?: boolean;
1751
1936
  resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
1752
1937
  adoptBackgroundCommand?: SelfhostedSessionDeps["adoptBackgroundCommand"];
1753
1938
  settleBackgroundCommand?: SelfhostedSessionDeps["settleBackgroundCommand"];
@@ -1779,6 +1964,7 @@ export class SelfhostedSandboxClient {
1779
1964
  this.operationResourcePolicy = opts.operationResourcePolicy;
1780
1965
  this.operationResourcePolicySupported = opts.operationResourcePolicySupported;
1781
1966
  this.operationCpuQuotaSupported = opts.operationCpuQuotaSupported;
1967
+ this.transactionalFsWriteSupported = opts.transactionalFsWriteSupported;
1782
1968
  this.resolveOperationAdmission = opts.resolveOperationAdmission;
1783
1969
  this.adoptBackgroundCommand = opts.adoptBackgroundCommand;
1784
1970
  this.settleBackgroundCommand = opts.settleBackgroundCommand;
@@ -1822,6 +2008,7 @@ export class SelfhostedSandboxClient {
1822
2008
  ...(this.operationCpuQuotaSupported !== undefined
1823
2009
  ? { operationCpuQuotaSupported: this.operationCpuQuotaSupported }
1824
2010
  : {}),
2011
+ transactionalFsWriteSupported: this.transactionalFsWriteSupported === true,
1825
2012
  ...(this.resolveOperationAdmission !== undefined
1826
2013
  ? { resolveOperationAdmission: this.resolveOperationAdmission }
1827
2014
  : {}),
@@ -1941,6 +2128,7 @@ export interface SelfhostedSessionBuild {
1941
2128
  operationResourcePolicySupported?: boolean;
1942
2129
  /** Exact live CPU enforcement capability paired with the initial snapshot. */
1943
2130
  operationCpuQuotaSupported?: boolean;
2131
+ transactionalFsWriteSupported?: boolean;
1944
2132
  /** Live last-boundary operation admission resolver; see SelfhostedSessionDeps. */
1945
2133
  resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
1946
2134
  adoptBackgroundCommand?: SelfhostedSessionDeps["adoptBackgroundCommand"];
@@ -1996,6 +2184,7 @@ export async function buildSelfhostedBackendSession(
1996
2184
  ...(deps.operationCpuQuotaSupported !== undefined
1997
2185
  ? { operationCpuQuotaSupported: deps.operationCpuQuotaSupported }
1998
2186
  : {}),
2187
+ transactionalFsWriteSupported: deps.transactionalFsWriteSupported === true,
1999
2188
  ...(deps.resolveOperationAdmission !== undefined
2000
2189
  ? { resolveOperationAdmission: deps.resolveOperationAdmission }
2001
2190
  : {}),
@@ -14,6 +14,8 @@ import {
14
14
  type WorkspaceTreeFingerprint,
15
15
  } from "@opengeni/contracts";
16
16
  import { withSandboxProviderCapture } from "./provider-operation-gate";
17
+ import type { VerifiedHostWorkspaceArchive } from "./archive-spool";
18
+ import { captureHostWorkspaceArchive, fingerprintHostWorkspace } from "./host-archive-spool";
17
19
 
18
20
  export {
19
21
  WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION,
@@ -36,6 +38,55 @@ export type VerifiedWorkspaceArchive = {
36
38
  nativeSnapshot?: NativeSnapshotRef;
37
39
  };
38
40
 
41
+ export type VerifiedWorkspaceArchivePayload =
42
+ | VerifiedWorkspaceArchive
43
+ | VerifiedHostWorkspaceArchive;
44
+
45
+ /** Host-backed portable archives can go directly to object storage without an
46
+ * intermediate whole-document string or buffer. Existing inline/provider APIs
47
+ * retain their compatibility path when disk-backed publication is unavailable. */
48
+ export async function captureWorkspaceArchiveForStorage(
49
+ session: unknown,
50
+ capturedAtMs: number,
51
+ options: WorkspaceArchiveCaptureOptions,
52
+ objectStorageAvailable: boolean,
53
+ ): Promise<VerifiedWorkspaceArchivePayload> {
54
+ const target = session as WorkspaceSession;
55
+ const root = hostBackedWorkspaceRoot(target);
56
+ if (
57
+ process.platform !== "linux" ||
58
+ !root ||
59
+ !objectStorageAvailable ||
60
+ options.strategy === "portable_tar"
61
+ ) {
62
+ return await captureVerifiedWorkspaceArchive(session, capturedAtMs, options);
63
+ }
64
+ return await withSandboxProviderCapture(session, async () => {
65
+ const { spool, workspace } = await captureHostWorkspaceArchive(
66
+ root,
67
+ workspaceFingerprintExcludes(target),
68
+ );
69
+ return {
70
+ kind: "host_spool" as const,
71
+ spool,
72
+ descriptor: {
73
+ version: 1 as const,
74
+ revision: `wa1:${String(capturedAtMs).padStart(13, "0")}:${spool.sha256}`,
75
+ archiveSha256: spool.sha256,
76
+ archiveBytes: spool.byteSize,
77
+ capturedAt: new Date(capturedAtMs).toISOString(),
78
+ workspace,
79
+ },
80
+ };
81
+ });
82
+ }
83
+
84
+ export async function disposeWorkspaceArchive(
85
+ archive: VerifiedWorkspaceArchivePayload | undefined,
86
+ ): Promise<void> {
87
+ if (archive?.kind === "host_spool") await archive.spool.dispose();
88
+ }
89
+
39
90
  export type WorkspaceArchiveIntegrityCode =
40
91
  | "archive_metadata_missing"
41
92
  | "archive_metadata_invalid"
@@ -789,7 +840,9 @@ export async function verifyRestoredWorkspace(
789
840
  "selected host-backed workspace archive was restored by a non-host-backed provider",
790
841
  );
791
842
  }
792
- return fingerprintHostBackedWorkspace(root, workspaceFingerprintExcludes(target));
843
+ return process.platform === "linux"
844
+ ? fingerprintHostWorkspace(root, workspaceFingerprintExcludes(target))
845
+ : fingerprintHostBackedWorkspace(root, workspaceFingerprintExcludes(target));
793
846
  })()
794
847
  : await fingerprintRemoteWorkspace(target);
795
848
  if (!fingerprintsEqual(actual, descriptor.workspace)) {