@fitlab-ai/agent-infra 0.9.6 → 0.9.7

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 (127) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
  7. package/dist/lib/defaults.json +10 -2
  8. package/dist/lib/git/workflow.js +30 -3
  9. package/dist/lib/internal/codex-lifecycle.js +69 -1
  10. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  11. package/dist/lib/internal/task-orchestration.js +37 -5
  12. package/dist/lib/platform/context.js +6 -4
  13. package/dist/lib/platform/github-client.js +55 -2
  14. package/dist/lib/process-data/checkpoint.js +319 -0
  15. package/dist/lib/process-data/index.js +128 -60
  16. package/dist/lib/process-data/materialize.js +84 -0
  17. package/dist/lib/process-data/normalize.js +34 -1
  18. package/dist/lib/process-data/reconcile.js +55 -1
  19. package/dist/lib/process-data/sources.js +422 -1
  20. package/dist/lib/process-data/store.js +341 -11
  21. package/dist/lib/sandbox/commands/create.js +103 -51
  22. package/dist/lib/sandbox/commands/rm.js +64 -15
  23. package/dist/lib/sandbox/commands/start.js +4 -3
  24. package/dist/lib/sandbox/control/container-identity.js +64 -0
  25. package/dist/lib/sandbox/control/executor.js +13 -7
  26. package/dist/lib/sandbox/control/lifecycle.js +238 -29
  27. package/dist/lib/sandbox/control/protocol.js +7 -0
  28. package/dist/lib/sandbox/control/server.js +202 -26
  29. package/dist/lib/sandbox/control/state.js +25 -15
  30. package/dist/lib/sandbox/control/timing.js +8 -0
  31. package/dist/lib/sandbox/mounts.js +5 -13
  32. package/dist/lib/sandbox/recovery.js +59 -42
  33. package/dist/lib/sandbox/workspace-view.js +57 -40
  34. package/dist/lib/server/daemon.js +1 -1
  35. package/dist/lib/server/process-control.js +11 -1
  36. package/dist/lib/server/process-state.js +57 -20
  37. package/dist/lib/task/artifact-lifecycle.js +82 -11
  38. package/dist/lib/task/artifact-receipts.js +128 -0
  39. package/dist/lib/task/codex-orchestration.js +81 -6
  40. package/dist/lib/task/commands/validate.js +1 -1
  41. package/dist/lib/task/create.js +10 -2
  42. package/dist/lib/task/delegation-receipts.js +84 -2
  43. package/dist/lib/task/events.js +139 -2
  44. package/dist/lib/task/ledger.js +13 -1
  45. package/dist/lib/task/orchestration.js +235 -14
  46. package/dist/lib/task/sections.js +5 -1
  47. package/dist/lib/task/task-execution-lock.js +5 -4
  48. package/dist/lib/task/verification-engine.js +40 -8
  49. package/dist/lifecycle-build-manifest.json +6 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
  51. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  52. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  53. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
  56. package/lib/defaults.json +10 -2
  57. package/lib/git/workflow.ts +25 -2
  58. package/lib/internal/codex-lifecycle.ts +68 -1
  59. package/lib/internal/codex-sandbox-controller.ts +109 -0
  60. package/lib/internal/task-orchestration.ts +31 -4
  61. package/lib/platform/context.ts +8 -5
  62. package/lib/platform/github-client.ts +61 -3
  63. package/lib/process-data/checkpoint.ts +335 -0
  64. package/lib/process-data/index.ts +112 -60
  65. package/lib/process-data/materialize.ts +83 -0
  66. package/lib/process-data/normalize.ts +29 -1
  67. package/lib/process-data/reconcile.ts +64 -2
  68. package/lib/process-data/sources.ts +449 -2
  69. package/lib/process-data/store.ts +304 -11
  70. package/lib/process-data/types.ts +124 -2
  71. package/lib/sandbox/commands/create.ts +56 -7
  72. package/lib/sandbox/commands/rm.ts +64 -14
  73. package/lib/sandbox/commands/start.ts +4 -3
  74. package/lib/sandbox/control/container-identity.ts +93 -0
  75. package/lib/sandbox/control/executor.ts +14 -10
  76. package/lib/sandbox/control/lifecycle.ts +264 -33
  77. package/lib/sandbox/control/protocol.ts +31 -5
  78. package/lib/sandbox/control/server.ts +184 -24
  79. package/lib/sandbox/control/state.ts +34 -17
  80. package/lib/sandbox/control/timing.ts +12 -0
  81. package/lib/sandbox/mounts.ts +5 -15
  82. package/lib/sandbox/recovery.ts +71 -42
  83. package/lib/sandbox/workspace-view.ts +61 -40
  84. package/lib/server/daemon.ts +1 -1
  85. package/lib/server/process-control.ts +13 -3
  86. package/lib/server/process-state.ts +82 -27
  87. package/lib/task/artifact-lifecycle.ts +73 -10
  88. package/lib/task/artifact-receipts.ts +161 -0
  89. package/lib/task/codex-orchestration.ts +91 -5
  90. package/lib/task/commands/validate.ts +1 -1
  91. package/lib/task/create.ts +11 -2
  92. package/lib/task/delegation-receipts.ts +160 -8
  93. package/lib/task/events.ts +122 -1
  94. package/lib/task/ledger.ts +16 -1
  95. package/lib/task/orchestration.ts +356 -17
  96. package/lib/task/sections.ts +6 -2
  97. package/lib/task/task-execution-lock.ts +11 -9
  98. package/lib/task/verification-engine.ts +51 -16
  99. package/package.json +1 -1
  100. package/templates/.agents/README.en.md +7 -0
  101. package/templates/.agents/README.zh-CN.md +5 -0
  102. package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  104. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  105. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  106. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  107. package/templates/.agents/rules/review-handshake.en.md +3 -1
  108. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  109. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  110. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  113. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  114. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  115. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  116. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  117. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  118. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  119. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  121. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  122. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  123. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
  124. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
  125. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  126. package/templates/.agents/workspace/README.en.md +0 -26
  127. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -47,12 +47,39 @@ function canonicalJsonBytes(value) {
47
47
  return Buffer.from(canonicalString(value, new Set()), 'utf8');
48
48
  }
49
49
  function ensurePrivateDirectory(directory) {
50
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
51
- if (process.platform !== 'win32')
52
- fs.chmodSync(directory, 0o700);
50
+ const missing = [];
51
+ let current = path.resolve(directory);
52
+ while (!fs.existsSync(current)) {
53
+ missing.push(current);
54
+ const parent = path.dirname(current);
55
+ if (parent === current)
56
+ throw new Error(`cannot create directory tree: ${directory}`);
57
+ current = parent;
58
+ }
59
+ for (const target of missing.reverse()) {
60
+ fs.mkdirSync(target, { mode: 0o700 });
61
+ if (process.platform !== 'win32')
62
+ fs.chmodSync(target, 0o700);
63
+ fsyncDirectory(path.dirname(target));
64
+ }
65
+ }
66
+ function fsyncDirectory(directory) {
67
+ if (process.platform === 'win32')
68
+ return;
69
+ const fd = fs.openSync(directory, 'r');
70
+ try {
71
+ fs.fsyncSync(fd);
72
+ }
73
+ finally {
74
+ fs.closeSync(fd);
75
+ }
53
76
  }
54
77
  function writeExclusive(filePath, bytes) {
55
- ensurePrivateDirectory(path.dirname(filePath));
78
+ const directory = path.dirname(filePath);
79
+ const directoryExisted = fs.existsSync(directory);
80
+ ensurePrivateDirectory(directory);
81
+ if (!directoryExisted)
82
+ fsyncDirectory(path.dirname(directory));
56
83
  try {
57
84
  const fd = fs.openSync(filePath, 'wx', 0o600);
58
85
  try {
@@ -62,6 +89,7 @@ function writeExclusive(filePath, bytes) {
62
89
  finally {
63
90
  fs.closeSync(fd);
64
91
  }
92
+ fsyncDirectory(directory);
65
93
  return true;
66
94
  }
67
95
  catch (error) {
@@ -100,7 +128,14 @@ function jsonLine(value) {
100
128
  return `${canonicalJsonBytes(value).toString('utf8')}\n`;
101
129
  }
102
130
  function writePrivateFile(filePath, content) {
103
- fs.writeFileSync(filePath, content, { mode: 0o600, flag: 'wx' });
131
+ const fd = fs.openSync(filePath, 'wx', 0o600);
132
+ try {
133
+ fs.writeFileSync(fd, content);
134
+ fs.fsyncSync(fd);
135
+ }
136
+ finally {
137
+ fs.closeSync(fd);
138
+ }
104
139
  if (process.platform !== 'win32')
105
140
  fs.chmodSync(filePath, 0o600);
106
141
  }
@@ -153,9 +188,82 @@ function publishSnapshot(root, input) {
153
188
  if (existing !== jsonLine(manifest))
154
189
  throw new Error(`snapshot collision: ${snapshotId}`);
155
190
  fs.rmSync(staging, { recursive: true, force: true });
191
+ fsyncDirectory(stagingRoot);
192
+ return { snapshotId, path: destination, created: false };
193
+ }
194
+ fsyncDirectory(staging);
195
+ fs.renameSync(staging, destination);
196
+ fsyncDirectory(stagingRoot);
197
+ fsyncDirectory(path.dirname(destination));
198
+ return { snapshotId, path: destination, created: true };
199
+ }
200
+ catch (error) {
201
+ fs.rmSync(staging, { recursive: true, force: true });
202
+ throw error;
203
+ }
204
+ }
205
+ function publishSnapshotV2(root, input) {
206
+ ensurePrivateDirectory(root);
207
+ ensurePrivateDirectory(path.join(root, 'repairs'));
208
+ const date = new Date(input.observedTo);
209
+ if (!Number.isFinite(date.getTime()))
210
+ throw new Error('observedTo must be an ISO timestamp');
211
+ const identity = {
212
+ scope: 'github',
213
+ repository: input.repository,
214
+ observedFrom: input.observedFrom,
215
+ observedTo: input.observedTo,
216
+ objects: input.objects,
217
+ endpoints: input.endpoints,
218
+ excerptsEnabled: false,
219
+ dispositionCounts: dispositionCounts(input.objects),
220
+ recordCount: input.records.length,
221
+ findingCount: input.quality.length,
222
+ repairCount: input.repairs.length,
223
+ snapshotKind: input.snapshotKind,
224
+ parentSnapshotId: input.parentSnapshotId,
225
+ checkpointBefore: input.checkpointBefore,
226
+ watermark: input.watermark,
227
+ window: input.window,
228
+ observation: input.observation,
229
+ coverage: input.coverage,
230
+ reconciliation: input.reconciliation,
231
+ operations: input.operations
232
+ };
233
+ const snapshotId = `${input.watermark.replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z')}-${sha256(canonicalJsonBytes(identity)).slice(0, 12)}`;
234
+ const partialManifest = {
235
+ schema: 'raw-manifest/v2',
236
+ snapshotId,
237
+ ...identity,
238
+ observedFrom: input.observedFrom,
239
+ observedTo: input.observedTo,
240
+ privacyPolicyVersion: 'process-data-privacy/v1'
241
+ };
242
+ const manifest = { ...partialManifest, manifestSha256: manifestDigest(partialManifest) };
243
+ const stagingRoot = path.join(root, '.staging');
244
+ ensurePrivateDirectory(stagingRoot);
245
+ const staging = fs.mkdtempSync(path.join(stagingRoot, `${snapshotId}-`));
246
+ if (process.platform !== 'win32')
247
+ fs.chmodSync(staging, 0o700);
248
+ try {
249
+ writePrivateFile(path.join(staging, 'manifest.json'), jsonLine(manifest));
250
+ writePrivateFile(path.join(staging, 'normalized.jsonl'), input.records.map((record) => jsonLine({ schema: 'normalized/v2', ...record })).join(''));
251
+ writePrivateFile(path.join(staging, 'quality.json'), jsonLine({ schema: 'quality/v1', findings: input.quality }));
252
+ writePrivateFile(path.join(staging, 'repair-manifest.jsonl'), input.repairs.map((repair) => jsonLine({ schema: 'repair/v1', ...repair })).join(''));
253
+ const destination = path.join(root, 'snapshots', String(date.getUTCFullYear()), String(date.getUTCMonth() + 1).padStart(2, '0'), String(date.getUTCDate()).padStart(2, '0'), snapshotId);
254
+ ensurePrivateDirectory(path.dirname(destination));
255
+ if (fs.existsSync(destination)) {
256
+ const existing = fs.readFileSync(path.join(destination, 'manifest.json'), 'utf8');
257
+ if (existing !== jsonLine(manifest))
258
+ throw new Error(`snapshot collision: ${snapshotId}`);
259
+ fs.rmSync(staging, { recursive: true, force: true });
260
+ fsyncDirectory(stagingRoot);
156
261
  return { snapshotId, path: destination, created: false };
157
262
  }
263
+ fsyncDirectory(staging);
158
264
  fs.renameSync(staging, destination);
265
+ fsyncDirectory(stagingRoot);
266
+ fsyncDirectory(path.dirname(destination));
159
267
  return { snapshotId, path: destination, created: true };
160
268
  }
161
269
  catch (error) {
@@ -181,7 +289,224 @@ function findSnapshot(root, snapshotId) {
181
289
  }
182
290
  return null;
183
291
  }
184
- function verifySnapshot(root, snapshotId) {
292
+ function verifyV2Snapshot(root, snapshotId, snapshotPath, manifest, seen) {
293
+ const errors = [];
294
+ if (seen.has(snapshotId))
295
+ return { ok: false, snapshotId, snapshotPath, errors: ['snapshot lineage cycle detected'], manifest };
296
+ seen.add(snapshotId);
297
+ if (manifest.snapshotId !== snapshotId)
298
+ errors.push('snapshot identity mismatch');
299
+ if (manifest.scope !== 'github')
300
+ errors.push('v2 snapshot scope must be github');
301
+ if (manifest.snapshotKind === 'base' && manifest.parentSnapshotId !== null)
302
+ errors.push('base snapshot must not have a parent');
303
+ if (manifest.snapshotKind === 'delta' && !manifest.parentSnapshotId)
304
+ errors.push('delta snapshot must have a parent');
305
+ if (manifest.checkpointBefore !== manifest.parentSnapshotId)
306
+ errors.push('checkpoint and parent lineage mismatch');
307
+ if (manifest.parentSnapshotId) {
308
+ const parentPath = findSnapshot(root, manifest.parentSnapshotId);
309
+ if (!parentPath)
310
+ errors.push('parent snapshot is missing');
311
+ else {
312
+ const parentVerification = verifySnapshot(root, manifest.parentSnapshotId, seen);
313
+ if (!parentVerification.ok || !parentVerification.manifest) {
314
+ errors.push(`parent snapshot is invalid: ${parentVerification.errors.join('; ')}`);
315
+ }
316
+ else if (parentVerification.manifest.schema !== 'raw-manifest/v2') {
317
+ errors.push('v2 snapshot parent is not v2');
318
+ }
319
+ else {
320
+ const parent = parentVerification.manifest;
321
+ if (parent.repository !== manifest.repository)
322
+ errors.push('parent repository mismatch');
323
+ if (new Date(parent.watermark).getTime() >= new Date(manifest.watermark).getTime())
324
+ errors.push('snapshot watermark is not monotonic');
325
+ }
326
+ }
327
+ }
328
+ if (manifest.watermark !== manifest.window.toExclusive)
329
+ errors.push('watermark and window cutoff mismatch');
330
+ const watermark = new Date(manifest.watermark);
331
+ const from = manifest.window.fromInclusive ? new Date(manifest.window.fromInclusive) : null;
332
+ const queryAfter = manifest.window.queryAfter ? new Date(manifest.window.queryAfter) : null;
333
+ if (!Number.isFinite(watermark.getTime()) || (from && !Number.isFinite(from.getTime())) || (queryAfter && !Number.isFinite(queryAfter.getTime()))) {
334
+ errors.push('v2 observation window contains an invalid timestamp');
335
+ }
336
+ if (from && queryAfter && queryAfter.getTime() !== from.getTime() - 1000)
337
+ errors.push('queryAfter does not equal watermark minus one second');
338
+ if (!from && queryAfter)
339
+ errors.push('base/full window cannot have queryAfter without fromInclusive');
340
+ const strictSinceEndpoints = manifest.endpoints.filter((endpoint) => endpoint.queryMode === 'strict-since');
341
+ const expectedQueryAfter = from && Number.isFinite(from.getTime()) ? new Date(from.getTime() - 1000).toISOString() : null;
342
+ if (manifest.snapshotKind === 'base' && from)
343
+ errors.push('base snapshot must not have fromInclusive');
344
+ if (queryAfter && strictSinceEndpoints.length === 0)
345
+ errors.push('queryAfter requires a strict-since endpoint');
346
+ if (strictSinceEndpoints.length > 0 && (manifest.snapshotKind !== 'delta' || manifest.reconciliation !== 'incremental')) {
347
+ errors.push('strict-since is only valid for incremental delta snapshots');
348
+ }
349
+ if (strictSinceEndpoints.length > 0 && manifest.window.queryAfter !== expectedQueryAfter) {
350
+ errors.push('incremental strict-since queryAfter proof is invalid');
351
+ }
352
+ if (manifest.coverage.mode !== 'boundary-reread-with-full-reconcile' || manifest.coverage.absoluteCompleteness !== false)
353
+ errors.push('coverage declaration is invalid');
354
+ const preflight = new Date(manifest.observation.preflightDate);
355
+ if (!Number.isFinite(preflight.getTime()))
356
+ errors.push('preflight Date is invalid');
357
+ for (const responseDate of manifest.observation.responseDates) {
358
+ const parsed = new Date(responseDate);
359
+ if (!Number.isFinite(parsed.getTime()) || parsed.getTime() < preflight.getTime())
360
+ errors.push('response Date evidence is invalid');
361
+ }
362
+ for (const endpoint of manifest.endpoints) {
363
+ if (!endpoint.queryMode)
364
+ errors.push(`endpoint query mode missing: ${endpoint.endpoint}`);
365
+ if (endpoint.queryMode !== 'strict-since' && endpoint.queryMode !== 'full-enumeration') {
366
+ errors.push(`endpoint query mode is invalid: ${endpoint.endpoint}`);
367
+ }
368
+ else if (endpoint.queryMode === 'strict-since') {
369
+ if (endpoint.requestedSince !== expectedQueryAfter)
370
+ errors.push(`endpoint since proof mismatch: ${endpoint.endpoint}`);
371
+ }
372
+ else if (endpoint.requestedSince !== undefined) {
373
+ errors.push(`full-enumeration endpoint has requestedSince: ${endpoint.endpoint}`);
374
+ }
375
+ for (const page of endpoint.pages) {
376
+ if (page.queryMode !== 'strict-since' && page.queryMode !== 'full-enumeration') {
377
+ errors.push(`page query mode is invalid: ${endpoint.endpoint}#${page.index}`);
378
+ }
379
+ if (page.queryMode !== endpoint.queryMode)
380
+ errors.push(`page query mode mismatch: ${endpoint.endpoint}#${page.index}`);
381
+ if (endpoint.queryMode === 'strict-since') {
382
+ if (page.requestedSince !== expectedQueryAfter)
383
+ errors.push(`page since proof mismatch: ${endpoint.endpoint}#${page.index}`);
384
+ }
385
+ else if (page.requestedSince !== undefined) {
386
+ errors.push(`full-enumeration page has requestedSince: ${endpoint.endpoint}#${page.index}`);
387
+ }
388
+ if (page.responseDate && new Date(page.responseDate).getTime() < preflight.getTime())
389
+ errors.push(`page Date precedes preflight: ${endpoint.endpoint}#${page.index}`);
390
+ }
391
+ }
392
+ for (const endpoint of manifest.endpoints) {
393
+ for (const page of endpoint.pages) {
394
+ const matches = manifest.objects.filter((object) => object.role === 'page-evidence'
395
+ && object.endpoint === endpoint.endpoint
396
+ && object.page === page.index
397
+ && object.sha256 === page.canonicalSha256);
398
+ if (matches.length !== 1) {
399
+ errors.push(`page evidence CAS reference is missing or ambiguous: ${endpoint.endpoint}#${page.index}`);
400
+ continue;
401
+ }
402
+ const object = matches[0];
403
+ if (object.queryMode !== 'strict-since' && object.queryMode !== 'full-enumeration') {
404
+ errors.push(`page evidence query mode is invalid: ${endpoint.endpoint}#${page.index}`);
405
+ }
406
+ if (object.queryMode !== page.queryMode
407
+ || object.requestedSince !== page.requestedSince
408
+ || object.responseDate !== page.responseDate) {
409
+ errors.push(`page evidence metadata mismatch: ${endpoint.endpoint}#${page.index}`);
410
+ }
411
+ if (endpoint.queryMode === 'strict-since' && object.requestedSince !== expectedQueryAfter) {
412
+ errors.push(`page evidence since proof mismatch: ${endpoint.endpoint}#${page.index}`);
413
+ }
414
+ }
415
+ }
416
+ const { manifestSha256, ...partial } = manifest;
417
+ if (manifestDigest(partial) !== manifestSha256)
418
+ errors.push('manifest hash mismatch');
419
+ const checkMode = (target, expected, label) => {
420
+ if (process.platform === 'win32' || !fs.existsSync(target))
421
+ return;
422
+ const actual = fs.statSync(target).mode & 0o777;
423
+ if (actual !== expected)
424
+ errors.push(`${label} permission mismatch: expected ${expected.toString(8)}, got ${actual.toString(8)}`);
425
+ };
426
+ checkMode(snapshotPath, 0o700, 'snapshot directory');
427
+ for (const file of ['manifest.json', 'normalized.jsonl', 'quality.json', 'repair-manifest.jsonl'])
428
+ checkMode(path.join(snapshotPath, file), 0o600, file);
429
+ const store = createObjectStore(root);
430
+ const normalizedEvidence = [];
431
+ try {
432
+ const lines = fs.readFileSync(path.join(snapshotPath, 'normalized.jsonl'), 'utf8').split('\n').filter(Boolean);
433
+ const ids = new Set();
434
+ const operationCounts = { upsert: 0, supersede: 0, tombstone: 0, unavailable: 0 };
435
+ for (const line of lines) {
436
+ const record = JSON.parse(line);
437
+ if (record.schema !== 'normalized/v2' || typeof record.recordId !== 'string')
438
+ errors.push('normalized v2 record schema is invalid');
439
+ if (typeof record.recordId === 'string' && ids.has(record.recordId))
440
+ errors.push(`duplicate normalized record: ${record.recordId}`);
441
+ if (typeof record.recordId === 'string')
442
+ ids.add(record.recordId);
443
+ if (!record.operation || !(record.operation in operationCounts))
444
+ errors.push('normalized v2 operation is invalid');
445
+ else
446
+ operationCounts[record.operation] += 1;
447
+ if ((record.operation === 'upsert' || record.operation === 'supersede')
448
+ && (!Array.isArray(record.evidence) || record.evidence.length !== 1)) {
449
+ errors.push(`resource evidence is required for ${record.operation}: ${record.recordId ?? 'unknown'}`);
450
+ }
451
+ for (const evidence of record.evidence ?? []) {
452
+ if (typeof evidence.resourceIdentity === 'string' && typeof evidence.resourceSha256 === 'string' && typeof evidence.pageSha256 === 'string') {
453
+ if (record.resourceIdentity !== evidence.resourceIdentity || record.sourceSha256 !== evidence.resourceSha256) {
454
+ errors.push(`normalized v2 evidence does not match record source: ${record.recordId ?? 'unknown'}`);
455
+ }
456
+ normalizedEvidence.push({ resourceIdentity: evidence.resourceIdentity, resourceSha256: evidence.resourceSha256, pageSha256: evidence.pageSha256 });
457
+ }
458
+ else {
459
+ errors.push('normalized v2 evidence reference is invalid');
460
+ }
461
+ }
462
+ }
463
+ if (lines.length !== manifest.recordCount)
464
+ errors.push('normalized record count mismatch');
465
+ if (canonicalJsonBytes(operationCounts).toString('utf8') !== canonicalJsonBytes(manifest.operations).toString('utf8'))
466
+ errors.push('operation count mismatch');
467
+ const quality = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'quality.json'), 'utf8'));
468
+ if (quality.schema !== 'quality/v1' || !Array.isArray(quality.findings))
469
+ errors.push('quality schema is invalid');
470
+ else if (quality.findings.length !== manifest.findingCount)
471
+ errors.push('quality finding count mismatch');
472
+ const repairLines = fs.readFileSync(path.join(snapshotPath, 'repair-manifest.jsonl'), 'utf8').split('\n').filter(Boolean);
473
+ if (repairLines.length !== manifest.repairCount)
474
+ errors.push('repair manifest count mismatch');
475
+ for (const line of repairLines)
476
+ if (JSON.parse(line).schema !== 'repair/v1')
477
+ errors.push('repair manifest schema is invalid');
478
+ }
479
+ catch {
480
+ errors.push('snapshot derived files are missing or invalid');
481
+ }
482
+ for (const object of manifest.objects) {
483
+ if (object.role !== 'page-evidence' && object.role !== 'resource')
484
+ errors.push(`object role is invalid: ${object.sourceIdentity}`);
485
+ if (object.disposition?.state && object.disposition.state !== 'included')
486
+ continue;
487
+ try {
488
+ const objectPath = store.pathFor(object.sha256);
489
+ const bytes = fs.readFileSync(objectPath);
490
+ if (sha256(bytes) !== object.sha256 || bytes.length !== object.bytes)
491
+ errors.push(`object integrity mismatch: ${object.sourceIdentity}`);
492
+ }
493
+ catch {
494
+ errors.push(`object missing: ${object.sourceIdentity}`);
495
+ }
496
+ }
497
+ for (const evidence of normalizedEvidence) {
498
+ const resources = manifest.objects.filter((object) => object.role === 'resource'
499
+ && object.resourceIdentity === evidence.resourceIdentity
500
+ && object.sha256 === evidence.resourceSha256);
501
+ const pages = manifest.objects.filter((object) => object.role === 'page-evidence' && object.sha256 === evidence.pageSha256);
502
+ if (resources.length !== 1)
503
+ errors.push(`resource evidence reference is missing or ambiguous: ${evidence.resourceIdentity}`);
504
+ if (pages.length === 0)
505
+ errors.push(`resource page evidence reference is missing: ${evidence.resourceIdentity}`);
506
+ }
507
+ return { ok: errors.length === 0, snapshotId, snapshotPath, errors, manifest };
508
+ }
509
+ function verifySnapshot(root, snapshotId, seen = new Set()) {
185
510
  const snapshotPath = findSnapshot(root, snapshotId);
186
511
  if (!snapshotPath)
187
512
  return { ok: false, snapshotId, snapshotPath: null, errors: ['snapshot not found'] };
@@ -197,15 +522,20 @@ function verifySnapshot(root, snapshotId) {
197
522
  for (const file of ['manifest.json', 'normalized.jsonl', 'quality.json', 'repair-manifest.jsonl']) {
198
523
  checkMode(path.join(snapshotPath, file), 0o600, file);
199
524
  }
200
- let manifest;
525
+ let rawManifest;
201
526
  try {
202
- manifest = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'manifest.json'), 'utf8'));
527
+ rawManifest = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'manifest.json'), 'utf8'));
203
528
  }
204
529
  catch {
205
530
  return { ok: false, snapshotId, snapshotPath, errors: ['manifest is invalid JSON'] };
206
531
  }
207
- if (manifest.schema !== 'raw-manifest/v1')
208
- errors.push(`unsupported manifest schema: ${manifest.schema}`);
532
+ if (rawManifest.schema === 'raw-manifest/v2') {
533
+ return verifyV2Snapshot(root, snapshotId, snapshotPath, rawManifest, seen);
534
+ }
535
+ if (rawManifest.schema !== 'raw-manifest/v1') {
536
+ return { ok: false, snapshotId, snapshotPath, errors: [`unsupported manifest schema: ${String(rawManifest.schema)}`] };
537
+ }
538
+ const manifest = rawManifest;
209
539
  if (manifest.snapshotId !== snapshotId)
210
540
  errors.push('snapshot identity mismatch');
211
541
  if (typeof manifest.excerptsEnabled !== 'boolean')
@@ -278,5 +608,5 @@ function verifySnapshot(root, snapshotId) {
278
608
  }
279
609
  return { ok: errors.length === 0, snapshotId, snapshotPath, errors, manifest };
280
610
  }
281
- export { canonicalJsonBytes, createObjectStore, findSnapshot, publishSnapshot, sha256, verifySnapshot };
611
+ export { canonicalJsonBytes, createObjectStore, findSnapshot, publishSnapshot, publishSnapshotV2, sha256, verifySnapshot };
282
612
  //# sourceMappingURL=store.js.map
@@ -19,7 +19,7 @@ import { resolveTools, tmpfsSeedStagingPath, tmpfsSeedTargetPath, toolConfigDirC
19
19
  import { assertFreshSandboxReady, hydrateTmpfsSeedEntries, prepareTmpfsMounts, startSandboxControlBroker } from '../recovery.js';
20
20
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
21
21
  import { sandboxCoreBindMounts } from '../mounts.js';
22
- import { assertSandboxTaskSource, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets } from '../workspace-view.js';
22
+ import { assertSandboxTaskSource, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTarget, } from '../workspace-view.js';
23
23
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
24
24
  import { validateSelinuxDisableEnv } from '../engines/selinux.js';
25
25
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
@@ -960,7 +960,7 @@ export async function create(args) {
960
960
  container,
961
961
  identity: target.workspace
962
962
  });
963
- prepareSandboxWorkspaceMountTargets(worktree);
963
+ prepareSandboxWorkspaceMountTarget(worktree);
964
964
  const control = materializeSandboxControl({
965
965
  base: effectiveConfig.controlBase,
966
966
  repoRoot: effectiveConfig.repoRoot,
@@ -968,7 +968,8 @@ export async function create(args) {
968
968
  project: effectiveConfig.project,
969
969
  container,
970
970
  branch,
971
- identity: target.workspace
971
+ identity: target.workspace,
972
+ engine
972
973
  });
973
974
  hostShellConfig = prepareHostShellConfig({
974
975
  home: effectiveConfig.home,
@@ -1028,54 +1029,105 @@ export async function create(args) {
1028
1029
  : [];
1029
1030
  const hostTz = detectHostTimezone();
1030
1031
  const tzFlags = hostTz ? ['-e', `TZ=${hostTz}`] : [];
1031
- runEngineTaskCommand(engine, 'docker', [
1032
- 'run',
1033
- '-d',
1034
- '--name',
1035
- container,
1036
- '--hostname',
1037
- `${effectiveConfig.project}-sandbox`,
1038
- '--label',
1039
- sandboxLabel(effectiveConfig),
1040
- '--label',
1041
- `${sandboxBranchLabel(effectiveConfig)}=${branch}`,
1042
- '--label',
1043
- `${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
1044
- ...(target.workspace.mode === 'task-bound'
1045
- ? [
1046
- '--label',
1047
- `${sandboxTaskIdLabel(effectiveConfig)}=${target.workspace.taskId}`,
1048
- '-e',
1049
- `AGENT_INFRA_TASK_ID=${target.workspace.taskId}`
1050
- ]
1051
- : []),
1052
- '-e',
1053
- `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1054
- '-e',
1055
- `AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
1056
- '-e',
1057
- 'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
1058
- '-e',
1059
- 'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
1060
- '--label',
1061
- `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1062
- ...coreVolumes,
1063
- ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
1064
- '-v',
1065
- volumeArg(engine, path.join(effectiveConfig.repoRoot, '.git'), `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`),
1066
- ...dotfilesMount,
1067
- ...toolVolumes,
1068
- ...tmpfsArgs,
1069
- ...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
1070
- ...liveMountVolumes,
1071
- ...envFile.dockerArgs,
1072
- ...tzFlags,
1073
- '-w',
1074
- '/workspace',
1075
- effectiveConfig.imageName
1076
- ]);
1077
- await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
1078
- createdTmpfsSeedPlan = tmpfsSeedPlan;
1032
+ let createdContainerRef = null;
1033
+ try {
1034
+ const dockerRunId = runEngineTaskCommand(engine, 'docker', [
1035
+ 'run',
1036
+ '-d',
1037
+ '--init',
1038
+ '--name',
1039
+ container,
1040
+ '--hostname',
1041
+ `${effectiveConfig.project}-sandbox`,
1042
+ '--label',
1043
+ sandboxLabel(effectiveConfig),
1044
+ '--label',
1045
+ `${sandboxBranchLabel(effectiveConfig)}=${branch}`,
1046
+ '--label',
1047
+ `${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
1048
+ ...(target.workspace.mode === 'task-bound'
1049
+ ? [
1050
+ '--label',
1051
+ `${sandboxTaskIdLabel(effectiveConfig)}=${target.workspace.taskId}`,
1052
+ '-e',
1053
+ `AGENT_INFRA_TASK_ID=${target.workspace.taskId}`
1054
+ ]
1055
+ : []),
1056
+ '-e',
1057
+ `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1058
+ '-e',
1059
+ `AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
1060
+ '-e',
1061
+ 'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
1062
+ '-e',
1063
+ 'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
1064
+ '--label',
1065
+ `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1066
+ ...coreVolumes,
1067
+ ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
1068
+ '-v',
1069
+ volumeArg(engine, path.join(effectiveConfig.repoRoot, '.git'), `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`),
1070
+ ...dotfilesMount,
1071
+ ...toolVolumes,
1072
+ ...tmpfsArgs,
1073
+ ...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
1074
+ ...liveMountVolumes,
1075
+ ...envFile.dockerArgs,
1076
+ ...tzFlags,
1077
+ '-w',
1078
+ '/workspace',
1079
+ effectiveConfig.imageName
1080
+ ]);
1081
+ createdContainerRef = dockerRunId.trim();
1082
+ const containerId = runEngineTaskCommand(engine, 'docker', [
1083
+ 'inspect', '--format', '{{.Id}}', dockerRunId.trim()
1084
+ ]).trim();
1085
+ if (!containerId)
1086
+ throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
1087
+ const rawContainerLabels = runEngineTaskCommand(engine, 'docker', [
1088
+ 'inspect', '--format', '{{json .Config.Labels}}', containerId
1089
+ ]).trim();
1090
+ let inspectedLabels;
1091
+ try {
1092
+ inspectedLabels = JSON.parse(rawContainerLabels);
1093
+ }
1094
+ catch {
1095
+ throw new Error('SANDBOX_CONTROL_CONTAINER_LABELS_INVALID');
1096
+ }
1097
+ if (!inspectedLabels || typeof inspectedLabels !== 'object' || Array.isArray(inspectedLabels)) {
1098
+ throw new Error('SANDBOX_CONTROL_CONTAINER_LABELS_INVALID');
1099
+ }
1100
+ const expectedLabels = {
1101
+ [sandboxLabel(effectiveConfig)]: '',
1102
+ [sandboxBranchLabel(effectiveConfig)]: branch,
1103
+ [sandboxWorkspaceModeLabel(effectiveConfig)]: target.workspace.mode,
1104
+ [sandboxRuntimeCapabilityLabel(effectiveConfig)]: capabilityPlan.runtimeSignature,
1105
+ ...(target.workspace.mode === 'task-bound'
1106
+ ? { [sandboxTaskIdLabel(effectiveConfig)]: target.workspace.taskId }
1107
+ : {})
1108
+ };
1109
+ const labels = inspectedLabels;
1110
+ for (const [key, expected] of Object.entries(expectedLabels)) {
1111
+ if (labels[key] !== expected)
1112
+ throw new Error('SANDBOX_CONTROL_CONTAINER_IDENTITY_MISMATCH');
1113
+ }
1114
+ finalizeSandboxControlManifest(control, {
1115
+ engine,
1116
+ id: containerId,
1117
+ labels: expectedLabels
1118
+ });
1119
+ await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
1120
+ createdTmpfsSeedPlan = tmpfsSeedPlan;
1121
+ }
1122
+ catch (error) {
1123
+ if (createdContainerRef) {
1124
+ runSafeEngine(engine, 'docker', ['stop', createdContainerRef]);
1125
+ runSafeEngine(engine, 'docker', ['rm', createdContainerRef]);
1126
+ }
1127
+ removeDirRecursive(control.root);
1128
+ removeDirRecursive(workspaceView.root);
1129
+ throw error;
1130
+ }
1079
1131
  }
1080
1132
  finally {
1081
1133
  envFile.cleanup();