@fitlab-ai/agent-infra 0.9.6 → 0.9.8

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 (178) hide show
  1. package/dist/bin/internal-cli.js +10 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
  8. package/dist/lib/decide.js +68 -14
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -3
  11. package/dist/lib/init.js +1 -1
  12. package/dist/lib/internal/codex-lifecycle.js +69 -1
  13. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  14. package/dist/lib/internal/release-workflow.js +1 -3
  15. package/dist/lib/internal/task-activity.js +57 -7
  16. package/dist/lib/internal/task-event.js +57 -4
  17. package/dist/lib/internal/task-ledger.js +59 -5
  18. package/dist/lib/internal/task-lifecycle.js +59 -8
  19. package/dist/lib/internal/task-orchestration.js +48 -8
  20. package/dist/lib/internal/task-override.js +131 -0
  21. package/dist/lib/internal/task-review.js +64 -6
  22. package/dist/lib/internal/task-verify.js +65 -3
  23. package/dist/lib/internal/task-warning.js +57 -4
  24. package/dist/lib/platform/context.js +6 -4
  25. package/dist/lib/platform/github-client.js +55 -2
  26. package/dist/lib/platform/pr-summary.js +5 -3
  27. package/dist/lib/process-data/checkpoint.js +319 -0
  28. package/dist/lib/process-data/index.js +128 -60
  29. package/dist/lib/process-data/materialize.js +84 -0
  30. package/dist/lib/process-data/normalize.js +34 -1
  31. package/dist/lib/process-data/reconcile.js +55 -1
  32. package/dist/lib/process-data/sources.js +422 -1
  33. package/dist/lib/process-data/store.js +341 -11
  34. package/dist/lib/runtime/agent-runtime.js +56 -0
  35. package/dist/lib/sandbox/commands/create.js +394 -264
  36. package/dist/lib/sandbox/commands/rm.js +64 -15
  37. package/dist/lib/sandbox/commands/start.js +4 -3
  38. package/dist/lib/sandbox/control/container-identity.js +64 -0
  39. package/dist/lib/sandbox/control/executor.js +23 -9
  40. package/dist/lib/sandbox/control/lifecycle.js +590 -61
  41. package/dist/lib/sandbox/control/protocol.js +7 -0
  42. package/dist/lib/sandbox/control/server.js +202 -26
  43. package/dist/lib/sandbox/control/state.js +51 -25
  44. package/dist/lib/sandbox/control/timing.js +8 -0
  45. package/dist/lib/sandbox/engine.js +14 -7
  46. package/dist/lib/sandbox/mounts.js +7 -0
  47. package/dist/lib/sandbox/recovery.js +135 -50
  48. package/dist/lib/sandbox/workspace-view.js +59 -20
  49. package/dist/lib/server/daemon.js +1 -1
  50. package/dist/lib/server/process-control.js +11 -1
  51. package/dist/lib/server/process-state.js +107 -20
  52. package/dist/lib/task/activity-intent.js +4 -1
  53. package/dist/lib/task/artifact-lifecycle.js +82 -11
  54. package/dist/lib/task/artifact-receipts.js +128 -0
  55. package/dist/lib/task/codex-orchestration.js +81 -6
  56. package/dist/lib/task/commands/validate.js +1 -1
  57. package/dist/lib/task/create.js +10 -2
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +120 -17
  60. package/dist/lib/task/events.js +148 -7
  61. package/dist/lib/task/guard-override.js +31 -0
  62. package/dist/lib/task/human-override.js +1240 -0
  63. package/dist/lib/task/ledger-intents.js +6 -3
  64. package/dist/lib/task/ledger.js +13 -1
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/orchestration.js +293 -18
  67. package/dist/lib/task/review-finalization.js +3 -2
  68. package/dist/lib/task/sections.js +5 -1
  69. package/dist/lib/task/task-execution-lock.js +5 -4
  70. package/dist/lib/task/verification-engine.js +80 -22
  71. package/dist/lib/task/verification.js +13 -1
  72. package/dist/lib/task/workflow-warning-intents.js +4 -2
  73. package/dist/lib/task/write.js +2 -1
  74. package/dist/lifecycle-build-manifest.json +6 -0
  75. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  76. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
  77. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  78. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  80. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
  82. package/lib/decide.ts +59 -14
  83. package/lib/defaults.json +10 -2
  84. package/lib/git/workflow.ts +25 -2
  85. package/lib/init.ts +1 -1
  86. package/lib/internal/codex-lifecycle.ts +68 -1
  87. package/lib/internal/codex-sandbox-controller.ts +109 -0
  88. package/lib/internal/release-workflow.ts +1 -3
  89. package/lib/internal/task-activity.ts +46 -6
  90. package/lib/internal/task-event.ts +53 -4
  91. package/lib/internal/task-ledger.ts +48 -4
  92. package/lib/internal/task-lifecycle.ts +56 -12
  93. package/lib/internal/task-orchestration.ts +43 -8
  94. package/lib/internal/task-override.ts +138 -0
  95. package/lib/internal/task-review.ts +51 -6
  96. package/lib/internal/task-verify.ts +52 -3
  97. package/lib/internal/task-warning.ts +44 -3
  98. package/lib/platform/context.ts +8 -5
  99. package/lib/platform/github-client.ts +61 -3
  100. package/lib/platform/pr-summary.ts +10 -3
  101. package/lib/process-data/checkpoint.ts +335 -0
  102. package/lib/process-data/index.ts +112 -60
  103. package/lib/process-data/materialize.ts +83 -0
  104. package/lib/process-data/normalize.ts +29 -1
  105. package/lib/process-data/reconcile.ts +64 -2
  106. package/lib/process-data/sources.ts +449 -2
  107. package/lib/process-data/store.ts +304 -11
  108. package/lib/process-data/types.ts +124 -2
  109. package/lib/runtime/agent-runtime.ts +76 -0
  110. package/lib/sandbox/commands/create.ts +139 -6
  111. package/lib/sandbox/commands/rm.ts +64 -14
  112. package/lib/sandbox/commands/start.ts +4 -3
  113. package/lib/sandbox/config.ts +2 -1
  114. package/lib/sandbox/control/container-identity.ts +93 -0
  115. package/lib/sandbox/control/executor.ts +24 -12
  116. package/lib/sandbox/control/lifecycle.ts +685 -67
  117. package/lib/sandbox/control/protocol.ts +35 -6
  118. package/lib/sandbox/control/server.ts +184 -24
  119. package/lib/sandbox/control/state.ts +67 -33
  120. package/lib/sandbox/control/timing.ts +12 -0
  121. package/lib/sandbox/engine.ts +20 -9
  122. package/lib/sandbox/mounts.ts +8 -0
  123. package/lib/sandbox/recovery.ts +159 -49
  124. package/lib/sandbox/workspace-view.ts +73 -22
  125. package/lib/server/daemon.ts +1 -1
  126. package/lib/server/process-control.ts +13 -3
  127. package/lib/server/process-state.ts +129 -27
  128. package/lib/task/activity-intent.ts +4 -2
  129. package/lib/task/artifact-lifecycle.ts +73 -10
  130. package/lib/task/artifact-receipts.ts +161 -0
  131. package/lib/task/codex-orchestration.ts +91 -5
  132. package/lib/task/commands/validate.ts +1 -1
  133. package/lib/task/create.ts +11 -2
  134. package/lib/task/decision-intents.ts +4 -2
  135. package/lib/task/delegation-receipts.ts +205 -25
  136. package/lib/task/events.ts +132 -6
  137. package/lib/task/guard-override.ts +50 -0
  138. package/lib/task/human-override.ts +1503 -0
  139. package/lib/task/ledger-intents.ts +6 -3
  140. package/lib/task/ledger.ts +16 -1
  141. package/lib/task/lifecycle.ts +71 -14
  142. package/lib/task/orchestration.ts +443 -22
  143. package/lib/task/review-finalization.ts +6 -2
  144. package/lib/task/sections.ts +6 -2
  145. package/lib/task/task-execution-lock.ts +11 -9
  146. package/lib/task/verification-engine.ts +93 -32
  147. package/lib/task/verification.ts +15 -1
  148. package/lib/task/workflow-warning-intents.ts +4 -2
  149. package/lib/task/write.ts +4 -1
  150. package/package.json +1 -1
  151. package/templates/.agents/README.en.md +7 -0
  152. package/templates/.agents/README.zh-CN.md +5 -0
  153. package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
  154. package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
  155. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
  156. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  157. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  158. package/templates/.agents/rules/review-handshake.en.md +3 -1
  159. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  160. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  161. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  162. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  163. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  164. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  165. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  166. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  167. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  168. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  169. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  170. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  171. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  172. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  173. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  174. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
  175. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
  176. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  177. package/templates/.agents/workspace/README.en.md +0 -26
  178. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -9,6 +9,9 @@ import type {
9
9
  RepairAction,
10
10
  RestCollectionEvidence,
11
11
  SnapshotManifest,
12
+ SnapshotManifestV1,
13
+ SnapshotManifestV2,
14
+ SnapshotOperations,
12
15
  StoredObjectEvidence
13
16
  } from './types.ts';
14
17
 
@@ -28,6 +31,18 @@ type PublishInput = {
28
31
  repairs: RepairAction[];
29
32
  };
30
33
 
34
+ type PublishV2Input = PublishInput & {
35
+ snapshotKind: 'base' | 'delta';
36
+ parentSnapshotId: string | null;
37
+ checkpointBefore: string | null;
38
+ watermark: string;
39
+ window: SnapshotManifestV2['window'];
40
+ observation: SnapshotManifestV2['observation'];
41
+ coverage: SnapshotManifestV2['coverage'];
42
+ reconciliation: 'incremental' | 'full';
43
+ operations: SnapshotOperations;
44
+ };
45
+
31
46
  type VerificationResult = {
32
47
  ok: boolean;
33
48
  snapshotId: string;
@@ -82,12 +97,32 @@ function canonicalJsonBytes(value: JsonValue | unknown): Buffer {
82
97
  }
83
98
 
84
99
  function ensurePrivateDirectory(directory: string): void {
85
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
86
- if (process.platform !== 'win32') fs.chmodSync(directory, 0o700);
100
+ const missing: string[] = [];
101
+ let current = path.resolve(directory);
102
+ while (!fs.existsSync(current)) {
103
+ missing.push(current);
104
+ const parent = path.dirname(current);
105
+ if (parent === current) throw new Error(`cannot create directory tree: ${directory}`);
106
+ current = parent;
107
+ }
108
+ for (const target of missing.reverse()) {
109
+ fs.mkdirSync(target, { mode: 0o700 });
110
+ if (process.platform !== 'win32') fs.chmodSync(target, 0o700);
111
+ fsyncDirectory(path.dirname(target));
112
+ }
113
+ }
114
+
115
+ function fsyncDirectory(directory: string): void {
116
+ if (process.platform === 'win32') return;
117
+ const fd = fs.openSync(directory, 'r');
118
+ try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
87
119
  }
88
120
 
89
121
  function writeExclusive(filePath: string, bytes: Buffer): boolean {
90
- ensurePrivateDirectory(path.dirname(filePath));
122
+ const directory = path.dirname(filePath);
123
+ const directoryExisted = fs.existsSync(directory);
124
+ ensurePrivateDirectory(directory);
125
+ if (!directoryExisted) fsyncDirectory(path.dirname(directory));
91
126
  try {
92
127
  const fd = fs.openSync(filePath, 'wx', 0o600);
93
128
  try {
@@ -96,6 +131,7 @@ function writeExclusive(filePath: string, bytes: Buffer): boolean {
96
131
  } finally {
97
132
  fs.closeSync(fd);
98
133
  }
134
+ fsyncDirectory(directory);
99
135
  return true;
100
136
  } catch (error) {
101
137
  if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
@@ -133,11 +169,17 @@ function jsonLine(value: JsonValue | unknown): string {
133
169
  }
134
170
 
135
171
  function writePrivateFile(filePath: string, content: string | Buffer): void {
136
- fs.writeFileSync(filePath, content, { mode: 0o600, flag: 'wx' });
172
+ const fd = fs.openSync(filePath, 'wx', 0o600);
173
+ try {
174
+ fs.writeFileSync(fd, content);
175
+ fs.fsyncSync(fd);
176
+ } finally {
177
+ fs.closeSync(fd);
178
+ }
137
179
  if (process.platform !== 'win32') fs.chmodSync(filePath, 0o600);
138
180
  }
139
181
 
140
- function manifestDigest(manifest: Omit<SnapshotManifest, 'manifestSha256'>): string {
182
+ function manifestDigest(manifest: Record<string, unknown>): string {
141
183
  return sha256(canonicalJsonBytes(manifest));
142
184
  }
143
185
 
@@ -163,7 +205,7 @@ function publishSnapshot(root: string, input: PublishInput): { snapshotId: strin
163
205
  const identityDigest = sha256(canonicalJsonBytes(identity));
164
206
  const stamp = input.observedTo.replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
165
207
  const snapshotId = `${stamp}-${identityDigest.slice(0, 12)}`;
166
- const partialManifest: Omit<SnapshotManifest, 'manifestSha256'> = {
208
+ const partialManifest: Omit<SnapshotManifestV1, 'manifestSha256'> = {
167
209
  schema: 'raw-manifest/v1',
168
210
  snapshotId,
169
211
  ...identity,
@@ -200,9 +242,79 @@ function publishSnapshot(root: string, input: PublishInput): { snapshotId: strin
200
242
  const existing = fs.readFileSync(path.join(destination, 'manifest.json'), 'utf8');
201
243
  if (existing !== jsonLine(manifest)) throw new Error(`snapshot collision: ${snapshotId}`);
202
244
  fs.rmSync(staging, { recursive: true, force: true });
245
+ fsyncDirectory(stagingRoot);
203
246
  return { snapshotId, path: destination, created: false };
204
247
  }
248
+ fsyncDirectory(staging);
205
249
  fs.renameSync(staging, destination);
250
+ fsyncDirectory(stagingRoot);
251
+ fsyncDirectory(path.dirname(destination));
252
+ return { snapshotId, path: destination, created: true };
253
+ } catch (error) {
254
+ fs.rmSync(staging, { recursive: true, force: true });
255
+ throw error;
256
+ }
257
+ }
258
+
259
+ function publishSnapshotV2(root: string, input: PublishV2Input): { snapshotId: string; path: string; created: boolean } {
260
+ ensurePrivateDirectory(root);
261
+ ensurePrivateDirectory(path.join(root, 'repairs'));
262
+ const date = new Date(input.observedTo);
263
+ if (!Number.isFinite(date.getTime())) throw new Error('observedTo must be an ISO timestamp');
264
+ const identity = {
265
+ scope: 'github' as const,
266
+ repository: input.repository,
267
+ observedFrom: input.observedFrom,
268
+ observedTo: input.observedTo,
269
+ objects: input.objects,
270
+ endpoints: input.endpoints,
271
+ excerptsEnabled: false,
272
+ dispositionCounts: dispositionCounts(input.objects),
273
+ recordCount: input.records.length,
274
+ findingCount: input.quality.length,
275
+ repairCount: input.repairs.length,
276
+ snapshotKind: input.snapshotKind,
277
+ parentSnapshotId: input.parentSnapshotId,
278
+ checkpointBefore: input.checkpointBefore,
279
+ watermark: input.watermark,
280
+ window: input.window,
281
+ observation: input.observation,
282
+ coverage: input.coverage,
283
+ reconciliation: input.reconciliation,
284
+ operations: input.operations
285
+ };
286
+ const snapshotId = `${input.watermark.replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z')}-${sha256(canonicalJsonBytes(identity)).slice(0, 12)}`;
287
+ const partialManifest = {
288
+ schema: 'raw-manifest/v2' as const,
289
+ snapshotId,
290
+ ...identity,
291
+ observedFrom: input.observedFrom,
292
+ observedTo: input.observedTo,
293
+ privacyPolicyVersion: 'process-data-privacy/v1' as const
294
+ };
295
+ const manifest: SnapshotManifestV2 = { ...partialManifest, manifestSha256: manifestDigest(partialManifest) };
296
+ const stagingRoot = path.join(root, '.staging');
297
+ ensurePrivateDirectory(stagingRoot);
298
+ const staging = fs.mkdtempSync(path.join(stagingRoot, `${snapshotId}-`));
299
+ if (process.platform !== 'win32') fs.chmodSync(staging, 0o700);
300
+ try {
301
+ writePrivateFile(path.join(staging, 'manifest.json'), jsonLine(manifest));
302
+ writePrivateFile(path.join(staging, 'normalized.jsonl'), input.records.map((record) => jsonLine({ schema: 'normalized/v2', ...record })).join(''));
303
+ writePrivateFile(path.join(staging, 'quality.json'), jsonLine({ schema: 'quality/v1', findings: input.quality }));
304
+ writePrivateFile(path.join(staging, 'repair-manifest.jsonl'), input.repairs.map((repair) => jsonLine({ schema: 'repair/v1', ...repair })).join(''));
305
+ const destination = path.join(root, 'snapshots', String(date.getUTCFullYear()), String(date.getUTCMonth() + 1).padStart(2, '0'), String(date.getUTCDate()).padStart(2, '0'), snapshotId);
306
+ ensurePrivateDirectory(path.dirname(destination));
307
+ if (fs.existsSync(destination)) {
308
+ const existing = fs.readFileSync(path.join(destination, 'manifest.json'), 'utf8');
309
+ if (existing !== jsonLine(manifest)) throw new Error(`snapshot collision: ${snapshotId}`);
310
+ fs.rmSync(staging, { recursive: true, force: true });
311
+ fsyncDirectory(stagingRoot);
312
+ return { snapshotId, path: destination, created: false };
313
+ }
314
+ fsyncDirectory(staging);
315
+ fs.renameSync(staging, destination);
316
+ fsyncDirectory(stagingRoot);
317
+ fsyncDirectory(path.dirname(destination));
206
318
  return { snapshotId, path: destination, created: true };
207
319
  } catch (error) {
208
320
  fs.rmSync(staging, { recursive: true, force: true });
@@ -226,7 +338,181 @@ function findSnapshot(root: string, snapshotId: string): string | null {
226
338
  return null;
227
339
  }
228
340
 
229
- function verifySnapshot(root: string, snapshotId: string): VerificationResult {
341
+ function verifyV2Snapshot(root: string, snapshotId: string, snapshotPath: string, manifest: SnapshotManifestV2, seen: Set<string>): VerificationResult {
342
+ const errors: string[] = [];
343
+ if (seen.has(snapshotId)) return { ok: false, snapshotId, snapshotPath, errors: ['snapshot lineage cycle detected'], manifest };
344
+ seen.add(snapshotId);
345
+ if (manifest.snapshotId !== snapshotId) errors.push('snapshot identity mismatch');
346
+ if (manifest.scope !== 'github') errors.push('v2 snapshot scope must be github');
347
+ if (manifest.snapshotKind === 'base' && manifest.parentSnapshotId !== null) errors.push('base snapshot must not have a parent');
348
+ if (manifest.snapshotKind === 'delta' && !manifest.parentSnapshotId) errors.push('delta snapshot must have a parent');
349
+ if (manifest.checkpointBefore !== manifest.parentSnapshotId) errors.push('checkpoint and parent lineage mismatch');
350
+ if (manifest.parentSnapshotId) {
351
+ const parentPath = findSnapshot(root, manifest.parentSnapshotId);
352
+ if (!parentPath) errors.push('parent snapshot is missing');
353
+ else {
354
+ const parentVerification = verifySnapshot(root, manifest.parentSnapshotId, seen);
355
+ if (!parentVerification.ok || !parentVerification.manifest) {
356
+ errors.push(`parent snapshot is invalid: ${parentVerification.errors.join('; ')}`);
357
+ } else if (parentVerification.manifest.schema !== 'raw-manifest/v2') {
358
+ errors.push('v2 snapshot parent is not v2');
359
+ } else {
360
+ const parent = parentVerification.manifest;
361
+ if (parent.repository !== manifest.repository) errors.push('parent repository mismatch');
362
+ if (new Date(parent.watermark).getTime() >= new Date(manifest.watermark).getTime()) errors.push('snapshot watermark is not monotonic');
363
+ }
364
+ }
365
+ }
366
+ if (manifest.watermark !== manifest.window.toExclusive) errors.push('watermark and window cutoff mismatch');
367
+ const watermark = new Date(manifest.watermark);
368
+ const from = manifest.window.fromInclusive ? new Date(manifest.window.fromInclusive) : null;
369
+ const queryAfter = manifest.window.queryAfter ? new Date(manifest.window.queryAfter) : null;
370
+ if (!Number.isFinite(watermark.getTime()) || (from && !Number.isFinite(from.getTime())) || (queryAfter && !Number.isFinite(queryAfter.getTime()))) {
371
+ errors.push('v2 observation window contains an invalid timestamp');
372
+ }
373
+ if (from && queryAfter && queryAfter.getTime() !== from.getTime() - 1000) errors.push('queryAfter does not equal watermark minus one second');
374
+ if (!from && queryAfter) errors.push('base/full window cannot have queryAfter without fromInclusive');
375
+ const strictSinceEndpoints = manifest.endpoints.filter((endpoint) => endpoint.queryMode === 'strict-since');
376
+ const expectedQueryAfter = from && Number.isFinite(from.getTime()) ? new Date(from.getTime() - 1000).toISOString() : null;
377
+ if (manifest.snapshotKind === 'base' && from) errors.push('base snapshot must not have fromInclusive');
378
+ if (queryAfter && strictSinceEndpoints.length === 0) errors.push('queryAfter requires a strict-since endpoint');
379
+ if (strictSinceEndpoints.length > 0 && (manifest.snapshotKind !== 'delta' || manifest.reconciliation !== 'incremental')) {
380
+ errors.push('strict-since is only valid for incremental delta snapshots');
381
+ }
382
+ if (strictSinceEndpoints.length > 0 && manifest.window.queryAfter !== expectedQueryAfter) {
383
+ errors.push('incremental strict-since queryAfter proof is invalid');
384
+ }
385
+ if (manifest.coverage.mode !== 'boundary-reread-with-full-reconcile' || manifest.coverage.absoluteCompleteness !== false) errors.push('coverage declaration is invalid');
386
+ const preflight = new Date(manifest.observation.preflightDate);
387
+ if (!Number.isFinite(preflight.getTime())) errors.push('preflight Date is invalid');
388
+ for (const responseDate of manifest.observation.responseDates) {
389
+ const parsed = new Date(responseDate);
390
+ if (!Number.isFinite(parsed.getTime()) || parsed.getTime() < preflight.getTime()) errors.push('response Date evidence is invalid');
391
+ }
392
+ for (const endpoint of manifest.endpoints) {
393
+ if (!endpoint.queryMode) errors.push(`endpoint query mode missing: ${endpoint.endpoint}`);
394
+ if (endpoint.queryMode !== 'strict-since' && endpoint.queryMode !== 'full-enumeration') {
395
+ errors.push(`endpoint query mode is invalid: ${endpoint.endpoint}`);
396
+ } else if (endpoint.queryMode === 'strict-since') {
397
+ if (endpoint.requestedSince !== expectedQueryAfter) errors.push(`endpoint since proof mismatch: ${endpoint.endpoint}`);
398
+ } else if (endpoint.requestedSince !== undefined) {
399
+ errors.push(`full-enumeration endpoint has requestedSince: ${endpoint.endpoint}`);
400
+ }
401
+ for (const page of endpoint.pages) {
402
+ if (page.queryMode !== 'strict-since' && page.queryMode !== 'full-enumeration') {
403
+ errors.push(`page query mode is invalid: ${endpoint.endpoint}#${page.index}`);
404
+ }
405
+ if (page.queryMode !== endpoint.queryMode) errors.push(`page query mode mismatch: ${endpoint.endpoint}#${page.index}`);
406
+ if (endpoint.queryMode === 'strict-since') {
407
+ if (page.requestedSince !== expectedQueryAfter) errors.push(`page since proof mismatch: ${endpoint.endpoint}#${page.index}`);
408
+ } else if (page.requestedSince !== undefined) {
409
+ errors.push(`full-enumeration page has requestedSince: ${endpoint.endpoint}#${page.index}`);
410
+ }
411
+ if (page.responseDate && new Date(page.responseDate).getTime() < preflight.getTime()) errors.push(`page Date precedes preflight: ${endpoint.endpoint}#${page.index}`);
412
+ }
413
+ }
414
+ for (const endpoint of manifest.endpoints) {
415
+ for (const page of endpoint.pages) {
416
+ const matches = manifest.objects.filter((object) => object.role === 'page-evidence'
417
+ && object.endpoint === endpoint.endpoint
418
+ && object.page === page.index
419
+ && object.sha256 === page.canonicalSha256);
420
+ if (matches.length !== 1) {
421
+ errors.push(`page evidence CAS reference is missing or ambiguous: ${endpoint.endpoint}#${page.index}`);
422
+ continue;
423
+ }
424
+ const object = matches[0]!;
425
+ if (object.queryMode !== 'strict-since' && object.queryMode !== 'full-enumeration') {
426
+ errors.push(`page evidence query mode is invalid: ${endpoint.endpoint}#${page.index}`);
427
+ }
428
+ if (object.queryMode !== page.queryMode
429
+ || object.requestedSince !== page.requestedSince
430
+ || object.responseDate !== page.responseDate) {
431
+ errors.push(`page evidence metadata mismatch: ${endpoint.endpoint}#${page.index}`);
432
+ }
433
+ if (endpoint.queryMode === 'strict-since' && object.requestedSince !== expectedQueryAfter) {
434
+ errors.push(`page evidence since proof mismatch: ${endpoint.endpoint}#${page.index}`);
435
+ }
436
+ }
437
+ }
438
+ const { manifestSha256, ...partial } = manifest;
439
+ if (manifestDigest(partial) !== manifestSha256) errors.push('manifest hash mismatch');
440
+ const checkMode = (target: string, expected: number, label: string) => {
441
+ if (process.platform === 'win32' || !fs.existsSync(target)) return;
442
+ const actual = fs.statSync(target).mode & 0o777;
443
+ if (actual !== expected) errors.push(`${label} permission mismatch: expected ${expected.toString(8)}, got ${actual.toString(8)}`);
444
+ };
445
+ checkMode(snapshotPath, 0o700, 'snapshot directory');
446
+ for (const file of ['manifest.json', 'normalized.jsonl', 'quality.json', 'repair-manifest.jsonl']) checkMode(path.join(snapshotPath, file), 0o600, file);
447
+ const store = createObjectStore(root);
448
+ const normalizedEvidence: Array<{ resourceIdentity: string; resourceSha256: string; pageSha256: string }> = [];
449
+ try {
450
+ const lines = fs.readFileSync(path.join(snapshotPath, 'normalized.jsonl'), 'utf8').split('\n').filter(Boolean);
451
+ const ids = new Set<string>();
452
+ const operationCounts: SnapshotOperations = { upsert: 0, supersede: 0, tombstone: 0, unavailable: 0 };
453
+ for (const line of lines) {
454
+ const record = JSON.parse(line) as {
455
+ schema?: string;
456
+ recordId?: string;
457
+ resourceIdentity?: string;
458
+ sourceSha256?: string;
459
+ operation?: keyof SnapshotOperations;
460
+ evidence?: Array<{ resourceIdentity?: string; resourceSha256?: string; pageSha256?: string }>;
461
+ };
462
+ if (record.schema !== 'normalized/v2' || typeof record.recordId !== 'string') errors.push('normalized v2 record schema is invalid');
463
+ if (typeof record.recordId === 'string' && ids.has(record.recordId)) errors.push(`duplicate normalized record: ${record.recordId}`);
464
+ if (typeof record.recordId === 'string') ids.add(record.recordId);
465
+ if (!record.operation || !(record.operation in operationCounts)) errors.push('normalized v2 operation is invalid');
466
+ else operationCounts[record.operation] += 1;
467
+ if ((record.operation === 'upsert' || record.operation === 'supersede')
468
+ && (!Array.isArray(record.evidence) || record.evidence.length !== 1)) {
469
+ errors.push(`resource evidence is required for ${record.operation}: ${record.recordId ?? 'unknown'}`);
470
+ }
471
+ for (const evidence of record.evidence ?? []) {
472
+ if (typeof evidence.resourceIdentity === 'string' && typeof evidence.resourceSha256 === 'string' && typeof evidence.pageSha256 === 'string') {
473
+ if (record.resourceIdentity !== evidence.resourceIdentity || record.sourceSha256 !== evidence.resourceSha256) {
474
+ errors.push(`normalized v2 evidence does not match record source: ${record.recordId ?? 'unknown'}`);
475
+ }
476
+ normalizedEvidence.push({ resourceIdentity: evidence.resourceIdentity, resourceSha256: evidence.resourceSha256, pageSha256: evidence.pageSha256 });
477
+ } else {
478
+ errors.push('normalized v2 evidence reference is invalid');
479
+ }
480
+ }
481
+ }
482
+ if (lines.length !== manifest.recordCount) errors.push('normalized record count mismatch');
483
+ if (canonicalJsonBytes(operationCounts).toString('utf8') !== canonicalJsonBytes(manifest.operations).toString('utf8')) errors.push('operation count mismatch');
484
+ const quality = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'quality.json'), 'utf8')) as { schema?: string; findings?: unknown[] };
485
+ if (quality.schema !== 'quality/v1' || !Array.isArray(quality.findings)) errors.push('quality schema is invalid');
486
+ else if (quality.findings.length !== manifest.findingCount) errors.push('quality finding count mismatch');
487
+ const repairLines = fs.readFileSync(path.join(snapshotPath, 'repair-manifest.jsonl'), 'utf8').split('\n').filter(Boolean);
488
+ if (repairLines.length !== manifest.repairCount) errors.push('repair manifest count mismatch');
489
+ for (const line of repairLines) if ((JSON.parse(line) as { schema?: string }).schema !== 'repair/v1') errors.push('repair manifest schema is invalid');
490
+ } catch {
491
+ errors.push('snapshot derived files are missing or invalid');
492
+ }
493
+ for (const object of manifest.objects) {
494
+ if (object.role !== 'page-evidence' && object.role !== 'resource') errors.push(`object role is invalid: ${object.sourceIdentity}`);
495
+ if (object.disposition?.state && object.disposition.state !== 'included') continue;
496
+ try {
497
+ const objectPath = store.pathFor(object.sha256);
498
+ const bytes = fs.readFileSync(objectPath);
499
+ if (sha256(bytes) !== object.sha256 || bytes.length !== object.bytes) errors.push(`object integrity mismatch: ${object.sourceIdentity}`);
500
+ } catch {
501
+ errors.push(`object missing: ${object.sourceIdentity}`);
502
+ }
503
+ }
504
+ for (const evidence of normalizedEvidence) {
505
+ const resources = manifest.objects.filter((object) => object.role === 'resource'
506
+ && object.resourceIdentity === evidence.resourceIdentity
507
+ && object.sha256 === evidence.resourceSha256);
508
+ const pages = manifest.objects.filter((object) => object.role === 'page-evidence' && object.sha256 === evidence.pageSha256);
509
+ if (resources.length !== 1) errors.push(`resource evidence reference is missing or ambiguous: ${evidence.resourceIdentity}`);
510
+ if (pages.length === 0) errors.push(`resource page evidence reference is missing: ${evidence.resourceIdentity}`);
511
+ }
512
+ return { ok: errors.length === 0, snapshotId, snapshotPath, errors, manifest };
513
+ }
514
+
515
+ function verifySnapshot(root: string, snapshotId: string, seen: Set<string> = new Set()): VerificationResult {
230
516
  const snapshotPath = findSnapshot(root, snapshotId);
231
517
  if (!snapshotPath) return { ok: false, snapshotId, snapshotPath: null, errors: ['snapshot not found'] };
232
518
  const errors: string[] = [];
@@ -239,13 +525,19 @@ function verifySnapshot(root: string, snapshotId: string): VerificationResult {
239
525
  for (const file of ['manifest.json', 'normalized.jsonl', 'quality.json', 'repair-manifest.jsonl']) {
240
526
  checkMode(path.join(snapshotPath, file), 0o600, file);
241
527
  }
242
- let manifest: SnapshotManifest;
528
+ let rawManifest: { schema?: unknown };
243
529
  try {
244
- manifest = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'manifest.json'), 'utf8')) as SnapshotManifest;
530
+ rawManifest = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'manifest.json'), 'utf8')) as { schema?: unknown };
245
531
  } catch {
246
532
  return { ok: false, snapshotId, snapshotPath, errors: ['manifest is invalid JSON'] };
247
533
  }
248
- if (manifest.schema !== 'raw-manifest/v1') errors.push(`unsupported manifest schema: ${manifest.schema}`);
534
+ if (rawManifest.schema === 'raw-manifest/v2') {
535
+ return verifyV2Snapshot(root, snapshotId, snapshotPath, rawManifest as SnapshotManifestV2, seen);
536
+ }
537
+ if (rawManifest.schema !== 'raw-manifest/v1') {
538
+ return { ok: false, snapshotId, snapshotPath, errors: [`unsupported manifest schema: ${String(rawManifest.schema)}`] };
539
+ }
540
+ const manifest = rawManifest as SnapshotManifestV1;
249
541
  if (manifest.snapshotId !== snapshotId) errors.push('snapshot identity mismatch');
250
542
  if (typeof manifest.excerptsEnabled !== 'boolean') errors.push('manifest excerpts opt-in is invalid');
251
543
  if (!manifest.dispositionCounts || canonicalJsonBytes(manifest.dispositionCounts).toString('utf8') !== canonicalJsonBytes(dispositionCounts(manifest.objects)).toString('utf8')) {
@@ -309,7 +601,8 @@ export {
309
601
  createObjectStore,
310
602
  findSnapshot,
311
603
  publishSnapshot,
604
+ publishSnapshotV2,
312
605
  sha256,
313
606
  verifySnapshot
314
607
  };
315
- export type { ObjectStore, ObjectWrite, PublishInput, VerificationResult };
608
+ export type { ObjectStore, ObjectWrite, PublishInput, PublishV2Input, VerificationResult };
@@ -7,6 +7,9 @@ type Availability<T> =
7
7
 
8
8
  type SourceKind = 'local-file' | 'github-rest' | 'operational-report' | 'structured-telemetry';
9
9
 
10
+ type CaptureObjectRole = 'page-evidence' | 'resource';
11
+ type EndpointQueryMode = 'strict-since' | 'full-enumeration';
12
+
10
13
  type PrivacyDisposition =
11
14
  | { state: 'included' }
12
15
  | { state: 'excluded-sensitive'; ruleId: string }
@@ -19,14 +22,37 @@ type CapturedObject = {
19
22
  bytes: number;
20
23
  content?: string;
21
24
  disposition?: PrivacyDisposition;
25
+ role?: CaptureObjectRole;
26
+ resourceIdentity?: string;
27
+ routeNumber?: number;
28
+ eventTime?: string;
29
+ endpoint?: string;
30
+ page?: number;
31
+ queryMode?: EndpointQueryMode;
32
+ requestedSince?: string;
33
+ responseDate?: string;
34
+ pageSha256?: string;
35
+ parentIdentity?: string;
22
36
  };
23
37
 
24
38
  type StoredObjectEvidence = Omit<CapturedObject, 'content'>;
25
39
 
40
+ type ResourceEvidenceRef = {
41
+ resourceIdentity: string;
42
+ resourceSha256: string;
43
+ pageSha256: string;
44
+ };
45
+
26
46
  type RestPageEvidence = {
27
47
  index: number;
28
48
  itemCount: number;
29
49
  canonicalSha256: string;
50
+ queryMode?: EndpointQueryMode;
51
+ requestedSince?: string;
52
+ responseDate?: string;
53
+ overlapItemCount?: number;
54
+ acceptedItemCount?: number;
55
+ deferredItemCount?: number;
30
56
  };
31
57
 
32
58
  type RestCollectionEvidence = {
@@ -36,6 +62,8 @@ type RestCollectionEvidence = {
36
62
  itemCount: number;
37
63
  termination: 'short-page';
38
64
  pages: RestPageEvidence[];
65
+ queryMode?: EndpointQueryMode;
66
+ requestedSince?: string;
39
67
  };
40
68
 
41
69
  type NormalizedKind =
@@ -58,6 +86,10 @@ type NormalizedRecord = {
58
86
  binding?: string;
59
87
  observedAt?: Availability<string>;
60
88
  data?: JsonValue;
89
+ resourceIdentity?: string;
90
+ parentIdentity?: string;
91
+ evidence?: ResourceEvidenceRef[];
92
+ operation?: 'upsert' | 'supersede' | 'tombstone' | 'unavailable' | 'unchanged';
61
93
  };
62
94
 
63
95
  type QualityCategory =
@@ -88,10 +120,12 @@ type RepairAction = {
88
120
  preconditionSha256: string;
89
121
  };
90
122
 
91
- type SnapshotManifest = {
123
+ type SnapshotScope = 'all' | 'local' | 'github';
124
+
125
+ type SnapshotManifestV1 = {
92
126
  schema: 'raw-manifest/v1';
93
127
  snapshotId: string;
94
- scope: 'all' | 'local' | 'github';
128
+ scope: SnapshotScope;
95
129
  repository: string;
96
130
  observedFrom: string;
97
131
  observedTo: string;
@@ -106,6 +140,80 @@ type SnapshotManifest = {
106
140
  manifestSha256: string;
107
141
  };
108
142
 
143
+ type SnapshotWindow = {
144
+ fromInclusive: string | null;
145
+ queryAfter: string | null;
146
+ toExclusive: string;
147
+ precision: 'second';
148
+ };
149
+
150
+ type ObservationEvidence = {
151
+ cutoffSource: 'github-response-date';
152
+ preflightDate: string;
153
+ responseDates: string[];
154
+ };
155
+
156
+ type CoverageEvidence = {
157
+ mode: 'boundary-reread-with-full-reconcile';
158
+ absoluteCompleteness: false;
159
+ };
160
+
161
+ type SnapshotOperations = {
162
+ upsert: number;
163
+ supersede: number;
164
+ tombstone: number;
165
+ unavailable: number;
166
+ };
167
+
168
+ type SnapshotManifestV2 = Omit<SnapshotManifestV1, 'schema' | 'observedFrom' | 'observedTo'> & {
169
+ schema: 'raw-manifest/v2';
170
+ snapshotKind: 'base' | 'delta';
171
+ parentSnapshotId: string | null;
172
+ checkpointBefore: string | null;
173
+ watermark: string;
174
+ window: SnapshotWindow;
175
+ observation: ObservationEvidence;
176
+ coverage: CoverageEvidence;
177
+ reconciliation: 'incremental' | 'full';
178
+ operations: SnapshotOperations;
179
+ observedFrom: string;
180
+ observedTo: string;
181
+ };
182
+
183
+ type SnapshotManifest = SnapshotManifestV1 | SnapshotManifestV2;
184
+
185
+ type CheckpointOwner = {
186
+ schema: 'checkpoint-owner/v1';
187
+ ownerId: string;
188
+ pid: number;
189
+ host: string;
190
+ processStartToken: string;
191
+ createdAt: string;
192
+ };
193
+
194
+ type GitHubCheckpoint = {
195
+ schema: 'github-checkpoint/v1';
196
+ repository: string;
197
+ snapshotId: string;
198
+ watermark: string;
199
+ manifestSha256: string;
200
+ committedAt: string;
201
+ };
202
+
203
+ type DeltaOperation = {
204
+ operation: 'upsert' | 'supersede' | 'tombstone' | 'unavailable';
205
+ resourceIdentity: string;
206
+ previousSha256?: string;
207
+ resourceSha256?: string;
208
+ reason?: string;
209
+ };
210
+
211
+ type RecoveryReport = {
212
+ recovered: boolean;
213
+ quarantined: string[];
214
+ reason?: string;
215
+ };
216
+
109
217
  type ProcessDataError = {
110
218
  code: string;
111
219
  message: string;
@@ -127,9 +235,23 @@ export type {
127
235
  QualityCategory,
128
236
  QualityFinding,
129
237
  RepairAction,
238
+ CaptureObjectRole,
239
+ CheckpointOwner,
240
+ CoverageEvidence,
241
+ DeltaOperation,
242
+ EndpointQueryMode,
243
+ GitHubCheckpoint,
244
+ ObservationEvidence,
245
+ RecoveryReport,
246
+ ResourceEvidenceRef,
130
247
  RestCollectionEvidence,
131
248
  RestPageEvidence,
132
249
  SnapshotManifest,
250
+ SnapshotManifestV1,
251
+ SnapshotManifestV2,
252
+ SnapshotOperations,
253
+ SnapshotScope,
254
+ SnapshotWindow,
133
255
  SourceKind,
134
256
  StoredObjectEvidence
135
257
  };
@@ -0,0 +1,76 @@
1
+ import path from 'node:path';
2
+
3
+ export type AgentRuntimeStore = 'capabilities' | 'lifecycle';
4
+
5
+ type RuntimeResolutionOptions = Readonly<{
6
+ repoRoot?: string;
7
+ env?: NodeJS.ProcessEnv;
8
+ explicitRoot?: string;
9
+ client?: string;
10
+ store?: AgentRuntimeStore;
11
+ }>;
12
+
13
+ const CLIENT_NAMESPACE = /^[a-z][a-z0-9-]{0,31}$/u;
14
+
15
+ function runtimeError(code: string, message: string): Error {
16
+ const error = new Error(`${code}: ${message}`);
17
+ error.name = code;
18
+ return error;
19
+ }
20
+
21
+ function boundControlContext(env: NodeJS.ProcessEnv): boolean {
22
+ return [
23
+ 'AGENT_INFRA_CONTROL_TOKEN',
24
+ 'AGENT_INFRA_CONTROL_GENERATION',
25
+ 'AGENT_INFRA_EXECUTOR_MANIFEST',
26
+ 'AGENT_INFRA_CODEX_CONTROLLER_CONTEXT'
27
+ ].some((key) => typeof env[key] === 'string' && env[key]!.length > 0);
28
+ }
29
+
30
+ function clientNamespace(client: string): string {
31
+ if (!CLIENT_NAMESPACE.test(client)) {
32
+ throw runtimeError('AGENT_INFRA_RUNTIME_CLIENT_INVALID', `invalid runtime client namespace '${client}'`);
33
+ }
34
+ return client;
35
+ }
36
+
37
+ export function resolveAgentRuntimeRoot(options: Readonly<{
38
+ repoRoot?: string;
39
+ env?: NodeJS.ProcessEnv;
40
+ }> = {}): string {
41
+ const env = options.env ?? process.env;
42
+ const configured = env.AGENT_INFRA_RUNTIME_DIR;
43
+ if (configured !== undefined && configured.length > 0) {
44
+ if (!path.isAbsolute(configured)) {
45
+ throw runtimeError('AGENT_INFRA_RUNTIME_DIR_INVALID', 'runtime directory must be absolute');
46
+ }
47
+ return path.resolve(configured);
48
+ }
49
+ if (boundControlContext(env)) {
50
+ throw runtimeError(
51
+ 'AGENT_INFRA_RUNTIME_DIR_REQUIRED',
52
+ 'task-bound control context requires AGENT_INFRA_RUNTIME_DIR'
53
+ );
54
+ }
55
+ return path.join(path.resolve(options.repoRoot ?? process.cwd()), '.agents', 'workspace', '.runtime');
56
+ }
57
+
58
+ export function resolveAgentRuntimeStoreRoot(options: RuntimeResolutionOptions = {}): string {
59
+ const client = clientNamespace(options.client ?? 'codex');
60
+ if (options.explicitRoot) return path.resolve(options.explicitRoot);
61
+ const env = options.env ?? process.env;
62
+ const runtimeRoot = resolveAgentRuntimeRoot({ repoRoot: options.repoRoot, env });
63
+ if (env.AGENT_INFRA_RUNTIME_DIR) {
64
+ if (!options.store) {
65
+ throw runtimeError('AGENT_INFRA_RUNTIME_STORE_INVALID', 'store kind is required for task-bound runtime');
66
+ }
67
+ return path.join(runtimeRoot, 'clients', client, options.store);
68
+ }
69
+ if (client === 'codex' && options.store) {
70
+ return path.join(runtimeRoot, `codex-${options.store}`);
71
+ }
72
+ if (!options.store) {
73
+ throw runtimeError('AGENT_INFRA_RUNTIME_STORE_INVALID', 'store kind is required');
74
+ }
75
+ return path.join(runtimeRoot, 'clients', client, options.store);
76
+ }