@fitlab-ai/agent-infra 0.9.8 → 0.9.9

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 (133) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +6 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +56 -67
  10. package/dist/lib/git/workflow.js +10 -1
  11. package/dist/lib/internal/codex-lifecycle.js +19 -5
  12. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  13. package/dist/lib/internal/git-workflow.js +5 -7
  14. package/dist/lib/internal/platform-pr.js +20 -7
  15. package/dist/lib/internal/release-workflow.js +2 -1
  16. package/dist/lib/internal/sandbox-control.js +79 -2
  17. package/dist/lib/internal/task-finalization.js +54 -0
  18. package/dist/lib/internal/task-orchestration.js +156 -291
  19. package/dist/lib/platform/pr-summary.js +48 -10
  20. package/dist/lib/platform/pull-requests.js +205 -41
  21. package/dist/lib/sandbox/commands/rm.js +316 -55
  22. package/dist/lib/sandbox/control/client.js +190 -6
  23. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  24. package/dist/lib/sandbox/control/executor.js +137 -5
  25. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  26. package/dist/lib/sandbox/control/protocol.js +67 -6
  27. package/dist/lib/sandbox/index.js +5 -3
  28. package/dist/lib/sandbox/managed-fs.js +5 -0
  29. package/dist/lib/sandbox/task-resolver.js +50 -6
  30. package/dist/lib/sandbox/workspace-identity.js +27 -0
  31. package/dist/lib/sandbox/worktree-safety.js +215 -2
  32. package/dist/lib/task/codex-orchestration.js +158 -33
  33. package/dist/lib/task/commands/status.js +59 -1
  34. package/dist/lib/task/commit-operation.js +335 -0
  35. package/dist/lib/task/commit-policy.js +18 -0
  36. package/dist/lib/task/delegation-receipts.js +243 -2
  37. package/dist/lib/task/finalization.js +567 -0
  38. package/dist/lib/task/operation-outcome.js +33 -0
  39. package/dist/lib/task/orchestration.js +187 -761
  40. package/dist/lib/task/short-id.js +59 -1
  41. package/dist/lib/task/task-execution-lock.js +6 -1
  42. package/dist/lib/task/verification-engine.js +113 -60
  43. package/dist/lib/task/verification.js +2 -1
  44. package/dist/lib/task/workflow-warning-intents.js +50 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  47. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  49. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  51. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  52. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +83 -91
  53. package/lib/git/workflow.ts +11 -2
  54. package/lib/internal/codex-lifecycle.ts +18 -5
  55. package/lib/internal/codex-sandbox-controller.ts +6 -12
  56. package/lib/internal/git-workflow.ts +5 -6
  57. package/lib/internal/platform-pr.ts +14 -7
  58. package/lib/internal/release-workflow.ts +2 -1
  59. package/lib/internal/sandbox-control.ts +92 -2
  60. package/lib/internal/task-finalization.ts +68 -0
  61. package/lib/internal/task-orchestration.ts +20 -103
  62. package/lib/platform/pr-summary.ts +55 -11
  63. package/lib/platform/pull-requests.ts +211 -41
  64. package/lib/sandbox/commands/rm.ts +413 -61
  65. package/lib/sandbox/control/client.ts +255 -6
  66. package/lib/sandbox/control/controller-registration.ts +347 -0
  67. package/lib/sandbox/control/executor.ts +163 -5
  68. package/lib/sandbox/control/lifecycle.ts +2 -1
  69. package/lib/sandbox/control/protocol.ts +80 -7
  70. package/lib/sandbox/index.ts +5 -3
  71. package/lib/sandbox/managed-fs.ts +5 -0
  72. package/lib/sandbox/task-resolver.ts +58 -7
  73. package/lib/sandbox/workspace-identity.ts +50 -2
  74. package/lib/sandbox/workspace-view.ts +3 -3
  75. package/lib/sandbox/worktree-safety.ts +236 -2
  76. package/lib/task/codex-orchestration.ts +167 -33
  77. package/lib/task/commands/status.ts +78 -1
  78. package/lib/task/commit-operation.ts +430 -0
  79. package/lib/task/commit-policy.ts +30 -0
  80. package/lib/task/delegation-receipts.ts +258 -1
  81. package/lib/task/finalization.ts +719 -0
  82. package/lib/task/operation-outcome.ts +48 -0
  83. package/lib/task/orchestration.ts +217 -982
  84. package/lib/task/short-id.ts +65 -1
  85. package/lib/task/task-execution-lock.ts +10 -1
  86. package/lib/task/verification-engine.ts +118 -63
  87. package/lib/task/verification.ts +3 -2
  88. package/lib/task/workflow-warning-intents.ts +57 -2
  89. package/package.json +2 -2
  90. package/templates/.agents/QUICKSTART.en.md +2 -2
  91. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  92. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  95. package/templates/.agents/rules/task-management.en.md +2 -2
  96. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  97. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  98. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  99. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  100. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  101. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  102. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  103. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  104. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  105. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  106. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  107. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  108. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  109. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  111. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  112. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  113. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  114. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  115. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  116. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  117. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  118. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  119. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  120. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  121. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  122. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  123. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  124. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  125. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  126. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  127. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  128. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  129. package/templates/.codex/hooks.json +4 -4
  130. package/dist/lib/task/commit-finalization.js +0 -305
  131. package/dist/lib/task/commit-intent.js +0 -165
  132. package/lib/task/commit-finalization.ts +0 -419
  133. package/lib/task/commit-intent.ts +0 -230
@@ -2,7 +2,12 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
- import { verifyLifecycleBuildIdentity } from './build-identity.ts';
5
+ import {
6
+ isCanonicalLifecyclePackageVersion,
7
+ isLifecycleProtocolVersion,
8
+ LIFECYCLE_PROTOCOL_VERSION,
9
+ verifyLifecycleBuildIdentity
10
+ } from './build-identity.ts';
6
11
  import type { LifecycleBuildIdentity } from './build-identity.ts';
7
12
  import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
8
13
 
@@ -11,6 +16,36 @@ type CodexControllerBinding = Readonly<{
11
16
  instanceDigest: string;
12
17
  controlGeneration: string;
13
18
  }>;
19
+ type CodexCapabilityProjection = Readonly<
20
+ | { kind: 'absent' }
21
+ | { kind: 'protocol-version'; value: typeof LIFECYCLE_PROTOCOL_VERSION }
22
+ | { kind: 'semver'; value: string }
23
+ | { kind: 'digest-prefix'; value: string }
24
+ | { kind: 'presence'; present: boolean }
25
+ >;
26
+ type CodexCapabilityProvenanceField = Readonly<{
27
+ matches: boolean;
28
+ expected: CodexCapabilityProjection;
29
+ actual: CodexCapabilityProjection;
30
+ }>;
31
+ type CodexCapabilityProvenanceDetail = Readonly<{
32
+ kind: 'codex-capability-provenance-mismatch';
33
+ version: 1;
34
+ fields: Readonly<{
35
+ taskId: CodexCapabilityProvenanceField;
36
+ hookDefinitionHash: CodexCapabilityProvenanceField;
37
+ buildIdentity: Readonly<{
38
+ protocolVersion: CodexCapabilityProvenanceField;
39
+ packageVersion: CodexCapabilityProvenanceField;
40
+ internalExecutableBuildHash: CodexCapabilityProvenanceField;
41
+ lifecycleContractHash: CodexCapabilityProvenanceField;
42
+ }>;
43
+ controller: Readonly<{
44
+ instanceDigest: CodexCapabilityProvenanceField;
45
+ controlGeneration: CodexCapabilityProvenanceField;
46
+ }>;
47
+ }>;
48
+ }>;
14
49
  type CodexCapabilityRecord = Readonly<{
15
50
  schemaVersion: 1;
16
51
  revision: number;
@@ -45,23 +80,222 @@ const DEFAULT_MAX_RECORDS = 128;
45
80
  const LOCK_RETRY_MS = 10;
46
81
  const LOCK_TIMEOUT_MS = 1_000;
47
82
  const LOCK_STALE_MS = 30_000;
83
+ const DETAIL_DOMAIN = 'codex-capability-provenance/v1';
84
+ const TASK_ID_PATTERN = /^TASK-[0-9]{8}-[0-9]{6}$/u;
85
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
86
+ const DIGEST_PREFIX_PATTERN = /^sha256:[0-9a-f]{16}$/u;
48
87
 
49
88
  function digest(value: string): string {
50
89
  return crypto.createHash('sha256').update(value).digest('hex');
51
90
  }
52
91
 
53
- function capabilityError(code: string, message: string): Error {
92
+ function capabilityError(code: string, message: string, detail?: CodexCapabilityProvenanceDetail): Error {
54
93
  const error = new Error(`${code}: ${message}`);
55
94
  error.name = code;
95
+ if (detail) (error as Error & { detail: CodexCapabilityProvenanceDetail }).detail = detail;
56
96
  return error;
57
97
  }
58
98
 
99
+ function objectValue(value: unknown, key: string): unknown {
100
+ if (!value || typeof value !== 'object') return undefined;
101
+ return (value as Record<string, unknown>)[key];
102
+ }
103
+
104
+ function digestPrefix(fieldName: string, value: string): CodexCapabilityProjection {
105
+ return {
106
+ kind: 'digest-prefix',
107
+ value: `sha256:${crypto.createHash('sha256')
108
+ .update(`${DETAIL_DOMAIN}\0${fieldName}\0${value}`)
109
+ .digest('hex')
110
+ .slice(0, 16)}`
111
+ };
112
+ }
113
+
114
+ function projectDigest(fieldName: string, value: unknown, pattern: RegExp): CodexCapabilityProjection {
115
+ return typeof value === 'string' && pattern.test(value) ? digestPrefix(fieldName, value) : { kind: 'absent' };
116
+ }
117
+
118
+ function projectTaskId(value: unknown): CodexCapabilityProjection {
119
+ return projectDigest('taskId', value, TASK_ID_PATTERN);
120
+ }
121
+
122
+ function projectHash(fieldName: string, value: unknown): CodexCapabilityProjection {
123
+ return projectDigest(fieldName, value, SHA256_PATTERN);
124
+ }
125
+
126
+ function projectProtocol(value: unknown): CodexCapabilityProjection {
127
+ return isLifecycleProtocolVersion(value)
128
+ ? { kind: 'protocol-version', value: LIFECYCLE_PROTOCOL_VERSION }
129
+ : { kind: 'absent' };
130
+ }
131
+
132
+ function projectPackageVersion(value: unknown): CodexCapabilityProjection {
133
+ return isCanonicalLifecyclePackageVersion(value)
134
+ ? { kind: 'semver', value }
135
+ : { kind: 'absent' };
136
+ }
137
+
138
+ function projectPresence(value: unknown): CodexCapabilityProjection {
139
+ return { kind: 'presence', present: Boolean(value && typeof value === 'object') };
140
+ }
141
+
142
+ function field(matches: boolean, expected: CodexCapabilityProjection, actual: CodexCapabilityProjection): CodexCapabilityProvenanceField {
143
+ return { matches, expected, actual };
144
+ }
145
+
146
+ function provenanceDetail(
147
+ record: CodexCapabilityRecord,
148
+ expected: Readonly<{
149
+ taskId: string;
150
+ hookDefinitionHash: string;
151
+ buildIdentity: LifecycleBuildIdentity;
152
+ controller?: CodexControllerBinding;
153
+ }>
154
+ ): CodexCapabilityProvenanceDetail {
155
+ const actualBuild = record.buildIdentity;
156
+ const expectedBuild = expected.buildIdentity;
157
+ const actualController = record.controller;
158
+ const expectedController = expected.controller ?? null;
159
+ return {
160
+ kind: 'codex-capability-provenance-mismatch',
161
+ version: 1,
162
+ fields: {
163
+ taskId: field(
164
+ record.taskId === expected.taskId,
165
+ projectTaskId(expected.taskId),
166
+ projectTaskId(record.taskId)
167
+ ),
168
+ hookDefinitionHash: field(
169
+ record.hookDefinitionHash === expected.hookDefinitionHash,
170
+ projectHash('hookDefinitionHash', expected.hookDefinitionHash),
171
+ projectHash('hookDefinitionHash', record.hookDefinitionHash)
172
+ ),
173
+ buildIdentity: {
174
+ protocolVersion: field(
175
+ objectValue(actualBuild, 'protocolVersion') === objectValue(expectedBuild, 'protocolVersion'),
176
+ projectProtocol(objectValue(expectedBuild, 'protocolVersion')),
177
+ projectProtocol(objectValue(actualBuild, 'protocolVersion'))
178
+ ),
179
+ packageVersion: field(
180
+ objectValue(actualBuild, 'packageVersion') === objectValue(expectedBuild, 'packageVersion'),
181
+ projectPackageVersion(objectValue(expectedBuild, 'packageVersion')),
182
+ projectPackageVersion(objectValue(actualBuild, 'packageVersion'))
183
+ ),
184
+ internalExecutableBuildHash: field(
185
+ objectValue(actualBuild, 'internalExecutableBuildHash') === objectValue(expectedBuild, 'internalExecutableBuildHash'),
186
+ projectHash('buildIdentity.internalExecutableBuildHash', objectValue(expectedBuild, 'internalExecutableBuildHash')),
187
+ projectHash('buildIdentity.internalExecutableBuildHash', objectValue(actualBuild, 'internalExecutableBuildHash'))
188
+ ),
189
+ lifecycleContractHash: field(
190
+ objectValue(actualBuild, 'lifecycleContractHash') === objectValue(expectedBuild, 'lifecycleContractHash'),
191
+ projectHash('buildIdentity.lifecycleContractHash', objectValue(expectedBuild, 'lifecycleContractHash')),
192
+ projectHash('buildIdentity.lifecycleContractHash', objectValue(actualBuild, 'lifecycleContractHash'))
193
+ )
194
+ },
195
+ controller: {
196
+ instanceDigest: field(
197
+ sameControllerField(actualController, expectedController, 'instanceDigest'),
198
+ projectPresence(objectValue(expectedController, 'instanceDigest') === undefined ? null : expectedController),
199
+ projectPresence(objectValue(actualController, 'instanceDigest') === undefined ? null : actualController)
200
+ ),
201
+ controlGeneration: field(
202
+ sameControllerField(actualController, expectedController, 'controlGeneration'),
203
+ projectPresence(objectValue(expectedController, 'controlGeneration') === undefined ? null : expectedController),
204
+ projectPresence(objectValue(actualController, 'controlGeneration') === undefined ? null : actualController)
205
+ )
206
+ }
207
+ }
208
+ };
209
+ }
210
+
211
+ function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
212
+ return Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
213
+ }
214
+
215
+ function isCodexControllerBinding(value: unknown): value is CodexControllerBinding {
216
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
217
+ const record = value as Record<string, unknown>;
218
+ return exactKeys(record, ['instanceDigest', 'controlGeneration'])
219
+ && typeof record.instanceDigest === 'string'
220
+ && typeof record.controlGeneration === 'string';
221
+ }
222
+
223
+ function isProjection(value: unknown): value is CodexCapabilityProjection {
224
+ if (!value || typeof value !== 'object') return false;
225
+ const record = value as Record<string, unknown>;
226
+ if (record.kind === 'absent') return exactKeys(record, ['kind']);
227
+ if (record.kind === 'protocol-version') {
228
+ return exactKeys(record, ['kind', 'value']) && isLifecycleProtocolVersion(record.value);
229
+ }
230
+ if (record.kind === 'semver') {
231
+ return exactKeys(record, ['kind', 'value']) && isCanonicalLifecyclePackageVersion(record.value);
232
+ }
233
+ if (record.kind === 'digest-prefix') {
234
+ return exactKeys(record, ['kind', 'value'])
235
+ && typeof record.value === 'string'
236
+ && DIGEST_PREFIX_PATTERN.test(record.value);
237
+ }
238
+ if (record.kind === 'presence') {
239
+ return exactKeys(record, ['kind', 'present']) && typeof record.present === 'boolean';
240
+ }
241
+ return false;
242
+ }
243
+
244
+ function isProvenanceField(value: unknown): value is CodexCapabilityProvenanceField {
245
+ if (!value || typeof value !== 'object') return false;
246
+ const record = value as Record<string, unknown>;
247
+ return exactKeys(record, ['matches', 'expected', 'actual'])
248
+ && typeof record.matches === 'boolean'
249
+ && isProjection(record.expected)
250
+ && isProjection(record.actual);
251
+ }
252
+
253
+ function isCodexCapabilityProvenanceDetail(value: unknown): value is CodexCapabilityProvenanceDetail {
254
+ if (!value || typeof value !== 'object') return false;
255
+ const record = value as Record<string, unknown>;
256
+ if (!exactKeys(record, ['kind', 'version', 'fields'])
257
+ || record.kind !== 'codex-capability-provenance-mismatch'
258
+ || record.version !== 1
259
+ || !record.fields || typeof record.fields !== 'object') return false;
260
+ const fields = record.fields as Record<string, unknown>;
261
+ if (!exactKeys(fields, ['taskId', 'hookDefinitionHash', 'buildIdentity', 'controller'])
262
+ || !isProvenanceField(fields.taskId)
263
+ || !isProvenanceField(fields.hookDefinitionHash)) return false;
264
+ const buildIdentity = fields.buildIdentity;
265
+ const controller = fields.controller;
266
+ if (!buildIdentity || typeof buildIdentity !== 'object' || !controller || typeof controller !== 'object') return false;
267
+ const build = buildIdentity as Record<string, unknown>;
268
+ const binding = controller as Record<string, unknown>;
269
+ return exactKeys(build, ['protocolVersion', 'packageVersion', 'internalExecutableBuildHash', 'lifecycleContractHash'])
270
+ && exactKeys(binding, ['instanceDigest', 'controlGeneration'])
271
+ && isProvenanceField(build.protocolVersion)
272
+ && isProvenanceField(build.packageVersion)
273
+ && isProvenanceField(build.internalExecutableBuildHash)
274
+ && isProvenanceField(build.lifecycleContractHash)
275
+ && isProvenanceField(binding.instanceDigest)
276
+ && isProvenanceField(binding.controlGeneration);
277
+ }
278
+
279
+ function sameControllerField(
280
+ left: unknown,
281
+ right: unknown,
282
+ key: keyof CodexControllerBinding
283
+ ): boolean {
284
+ if (left === null || right === null) return left === null && right === null;
285
+ return isCodexControllerBinding(left)
286
+ && isCodexControllerBinding(right)
287
+ && left[key] === right[key];
288
+ }
289
+
59
290
  function sameController(
60
- left: CodexControllerBinding | null,
61
- right: CodexControllerBinding | null
291
+ left: unknown,
292
+ right: unknown
62
293
  ): boolean {
63
- return left?.instanceDigest === right?.instanceDigest
64
- && left?.controlGeneration === right?.controlGeneration;
294
+ if (left === null || right === null) return left === null && right === null;
295
+ return isCodexControllerBinding(left)
296
+ && isCodexControllerBinding(right)
297
+ && left.instanceDigest === right.instanceDigest
298
+ && left.controlGeneration === right.controlGeneration;
65
299
  }
66
300
 
67
301
  function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
@@ -275,27 +509,54 @@ function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
275
509
  return next;
276
510
  }
277
511
 
278
- function consume(token: string, expected: Readonly<{
512
+ function validateRecord(record: CodexCapabilityRecord, expected: Readonly<{
279
513
  taskId: string;
280
514
  hookDefinitionHash: string;
281
515
  buildIdentity: LifecycleBuildIdentity;
282
516
  controller?: CodexControllerBinding;
283
517
  }>): CodexCapabilityRecord {
284
- sweep();
285
- const { file, record } = loadActive(token);
286
518
  if (record.status === 'consumed') {
287
519
  throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
288
520
  }
289
521
  if (record.status !== 'attested') {
290
522
  throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
291
523
  }
292
- const identity = verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity);
524
+ const identity = record.buildIdentity && typeof record.buildIdentity === 'object'
525
+ ? verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity)
526
+ : { ok: false };
293
527
  if (!identity.ok
294
528
  || record.taskId !== expected.taskId
295
529
  || record.hookDefinitionHash !== expected.hookDefinitionHash
296
530
  || !sameController(record.controller, expected.controller ?? null)) {
297
- throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability consumption identity does not match');
531
+ throw capabilityError(
532
+ 'CODEX_CAPABILITY_PROVENANCE_MISMATCH',
533
+ 'capability consumption identity does not match',
534
+ provenanceDetail(record, expected)
535
+ );
298
536
  }
537
+ return record;
538
+ }
539
+
540
+ function validate(token: string, expected: Readonly<{
541
+ taskId: string;
542
+ hookDefinitionHash: string;
543
+ buildIdentity: LifecycleBuildIdentity;
544
+ controller?: CodexControllerBinding;
545
+ }>): CodexCapabilityRecord {
546
+ sweep();
547
+ const { record } = loadActive(token);
548
+ return validateRecord(record, expected);
549
+ }
550
+
551
+ function consume(token: string, expected: Readonly<{
552
+ taskId: string;
553
+ hookDefinitionHash: string;
554
+ buildIdentity: LifecycleBuildIdentity;
555
+ controller?: CodexControllerBinding;
556
+ }>): CodexCapabilityRecord {
557
+ sweep();
558
+ const { file, record } = loadActive(token);
559
+ validateRecord(record, expected);
299
560
  const next: CodexCapabilityRecord = Object.freeze({
300
561
  ...record,
301
562
  revision: record.revision + 1,
@@ -310,13 +571,16 @@ function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
310
571
  return loadActive(token).record;
311
572
  }
312
573
 
313
- return Object.freeze({ arm, attest, consume, inspect, sweep });
574
+ return Object.freeze({ arm, attest, consume, inspect, validate, sweep });
314
575
  }
315
576
 
316
577
  export { createCodexCapabilityStore };
317
578
  export type {
318
579
  CapabilityStatus,
580
+ CodexCapabilityProjection,
581
+ CodexCapabilityProvenanceDetail,
319
582
  CodexCapabilityRecord,
320
583
  CodexCapabilityStoreOptions,
321
584
  CodexControllerBinding
322
585
  };
586
+ export { isCodexCapabilityProvenanceDetail };
@@ -0,0 +1,180 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import { getProcessIdentityState, type ProcessIdentity } from '../../../server/process-state.ts';
6
+ import type { CodexControllerLeaseProofV1, CodexControllerLeaseV1 } from '../../../sandbox/control/controller-registration.ts';
7
+ import { computeLifecycleBuildIdentity, type LifecycleBuildIdentity } from './build-identity.ts';
8
+
9
+ const HEX_256 = /^[a-f0-9]{64}$/u;
10
+
11
+ export type CodexSandboxControllerContextV2 = Readonly<{
12
+ version: 2;
13
+ taskId: string;
14
+ controlGeneration: string;
15
+ controllerInstanceDigest: string;
16
+ controllerProcess: ProcessIdentity;
17
+ controllerLease: Readonly<{ version: 1; leaseId: string; leaseSecret: string }>;
18
+ issuedAt: number;
19
+ expiresAt: number;
20
+ buildIdentity: LifecycleBuildIdentity;
21
+ hookDefinitionHash: string;
22
+ lifecycleProfilesHash: string;
23
+ }>;
24
+
25
+ function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
26
+ return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
27
+ }
28
+
29
+ function digestProfiles(repoRoot: string): string {
30
+ const files = [
31
+ path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-executor.toml'),
32
+ path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-reviewer.toml')
33
+ ];
34
+ const hash = crypto.createHash('sha256');
35
+ for (const file of files.sort()) {
36
+ const stat = fs.lstatSync(file);
37
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
38
+ hash.update(path.basename(file));
39
+ hash.update('\0');
40
+ hash.update(fs.readFileSync(file));
41
+ hash.update('\0');
42
+ }
43
+ return hash.digest('hex');
44
+ }
45
+
46
+ function validProcess(value: unknown): value is ProcessIdentity {
47
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
48
+ const identity = value as Record<string, unknown>;
49
+ return exactKeys(identity, ['pid', 'startTime'])
50
+ && Number.isSafeInteger(identity.pid) && (identity.pid as number) > 0
51
+ && Number.isSafeInteger(identity.startTime) && (identity.startTime as number) >= 0;
52
+ }
53
+
54
+ function validBuildIdentity(value: unknown): value is LifecycleBuildIdentity {
55
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
56
+ const identity = value as Record<string, unknown>;
57
+ return exactKeys(identity, [
58
+ 'internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'
59
+ ])
60
+ && identity.protocolVersion === 3
61
+ && typeof identity.packageVersion === 'string' && identity.packageVersion.length > 0
62
+ && typeof identity.internalExecutableBuildHash === 'string' && HEX_256.test(identity.internalExecutableBuildHash)
63
+ && typeof identity.lifecycleContractHash === 'string' && HEX_256.test(identity.lifecycleContractHash);
64
+ }
65
+
66
+ function validateContext(value: unknown): CodexSandboxControllerContextV2 {
67
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
68
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
69
+ }
70
+ const context = value as Record<string, unknown>;
71
+ const lease = context.controllerLease as Record<string, unknown> | null;
72
+ if (!exactKeys(context, [
73
+ 'buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerLease',
74
+ 'controllerProcess', 'expiresAt', 'hookDefinitionHash', 'issuedAt',
75
+ 'lifecycleProfilesHash', 'taskId', 'version'
76
+ ])
77
+ || context.version !== 2
78
+ || typeof context.taskId !== 'string' || context.taskId.length === 0
79
+ || typeof context.controlGeneration !== 'string' || context.controlGeneration.length === 0
80
+ || typeof context.controllerInstanceDigest !== 'string' || !HEX_256.test(context.controllerInstanceDigest)
81
+ || !validProcess(context.controllerProcess)
82
+ || !lease || !exactKeys(lease, ['leaseId', 'leaseSecret', 'version'])
83
+ || lease.version !== 1
84
+ || typeof lease.leaseId !== 'string' || !HEX_256.test(lease.leaseId)
85
+ || typeof lease.leaseSecret !== 'string' || !HEX_256.test(lease.leaseSecret)
86
+ || !Number.isSafeInteger(context.issuedAt)
87
+ || !Number.isSafeInteger(context.expiresAt)
88
+ || (context.expiresAt as number) <= (context.issuedAt as number)
89
+ || !validBuildIdentity(context.buildIdentity)
90
+ || typeof context.hookDefinitionHash !== 'string' || !HEX_256.test(context.hookDefinitionHash)
91
+ || typeof context.lifecycleProfilesHash !== 'string' || !HEX_256.test(context.lifecycleProfilesHash)) {
92
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
93
+ }
94
+ return context as unknown as CodexSandboxControllerContextV2;
95
+ }
96
+
97
+ export function contextFromControllerLease(
98
+ lease: CodexControllerLeaseV1,
99
+ hashes: Readonly<{ hookDefinitionHash: string; lifecycleProfilesHash: string }>
100
+ ): CodexSandboxControllerContextV2 {
101
+ return Object.freeze({
102
+ version: 2,
103
+ taskId: lease.taskId,
104
+ controlGeneration: lease.controlGeneration,
105
+ controllerInstanceDigest: lease.controllerInstanceDigest,
106
+ controllerProcess: lease.controllerProcess,
107
+ controllerLease: Object.freeze({ version: 1, leaseId: lease.leaseId, leaseSecret: lease.leaseSecret }),
108
+ issuedAt: lease.issuedAt,
109
+ expiresAt: lease.expiresAt,
110
+ buildIdentity: lease.buildIdentity,
111
+ hookDefinitionHash: hashes.hookDefinitionHash,
112
+ lifecycleProfilesHash: hashes.lifecycleProfilesHash
113
+ });
114
+ }
115
+
116
+ export function writeCodexSandboxControllerContext(
117
+ contextPath: string,
118
+ context: CodexSandboxControllerContextV2
119
+ ): void {
120
+ validateContext(context);
121
+ fs.mkdirSync(path.dirname(contextPath), { recursive: true, mode: 0o700 });
122
+ const temporary = path.join(path.dirname(contextPath), `.${path.basename(contextPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
123
+ try {
124
+ fs.writeFileSync(temporary, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
125
+ fs.chmodSync(temporary, 0o600);
126
+ fs.renameSync(temporary, contextPath);
127
+ } finally {
128
+ fs.rmSync(temporary, { force: true });
129
+ }
130
+ }
131
+
132
+ export function verifyCodexSandboxControllerContext(
133
+ contextPath: string,
134
+ options: Readonly<{
135
+ repoRoot?: string;
136
+ now?: number;
137
+ generation?: string;
138
+ probeProcess?: (identity: ProcessIdentity) => 'alive' | 'dead' | 'unknown';
139
+ }> = {}
140
+ ): CodexSandboxControllerContextV2 {
141
+ let stat: fs.Stats;
142
+ try {
143
+ stat = fs.lstatSync(contextPath);
144
+ } catch {
145
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
146
+ }
147
+ if (!stat.isFile() || stat.isSymbolicLink()
148
+ || (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
149
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
150
+ }
151
+ let context: CodexSandboxControllerContextV2;
152
+ try {
153
+ context = validateContext(JSON.parse(fs.readFileSync(contextPath, 'utf8')));
154
+ } catch {
155
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
156
+ }
157
+ const repoRoot = options.repoRoot ?? process.cwd();
158
+ const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
159
+ const state = (options.probeProcess ?? getProcessIdentityState)(context.controllerProcess);
160
+ if (context.controlGeneration !== generation
161
+ || context.expiresAt <= (options.now ?? Date.now())
162
+ || state !== 'alive') {
163
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
164
+ }
165
+ if (JSON.stringify(computeLifecycleBuildIdentity(repoRoot)) !== JSON.stringify(context.buildIdentity)
166
+ || crypto.createHash('sha256').update(fs.readFileSync(path.join(repoRoot, '.codex', 'hooks.json'))).digest('hex') !== context.hookDefinitionHash
167
+ || digestProfiles(repoRoot) !== context.lifecycleProfilesHash) {
168
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
169
+ }
170
+ return Object.freeze(context);
171
+ }
172
+
173
+ export function controllerProofFromContext(context: CodexSandboxControllerContextV2): CodexControllerLeaseProofV1 {
174
+ return Object.freeze({
175
+ version: 1,
176
+ leaseId: context.controllerLease.leaseId,
177
+ leaseSecret: context.controllerLease.leaseSecret,
178
+ controllerProcess: context.controllerProcess
179
+ });
180
+ }
@@ -147,6 +147,16 @@ function same(left: unknown, right: unknown): boolean {
147
147
  return JSON.stringify(left) === JSON.stringify(right);
148
148
  }
149
149
 
150
+ function sameChildCorrelation(
151
+ left: Extract<CodexLifecycleEvent, { type: 'hook-child' }>,
152
+ right: Extract<CodexLifecycleEvent, { type: 'hook-child' }>
153
+ ): boolean {
154
+ return left.sessionId === right.sessionId
155
+ && left.childThreadId === right.childThreadId
156
+ && left.parentThreadId === right.parentThreadId
157
+ && left.nativeAgent === right.nativeAgent;
158
+ }
159
+
150
160
  function requiredStrings(event: CodexLifecycleEvent): boolean {
151
161
  return Object.entries(event).every(([key, value]) =>
152
162
  key === 'forkedFromId'
@@ -286,6 +296,15 @@ function reduceCodexLifecycleEvent(
286
296
  const existing = state[key];
287
297
  if (existing) {
288
298
  if (same(existing, event)) return state;
299
+ if (
300
+ existing.type === 'hook-child'
301
+ && event.type === 'hook-child'
302
+ && sameChildCorrelation(existing, event)
303
+ && (existing.source === 'parent-rollout') !== (event.source === 'parent-rollout')
304
+ ) {
305
+ if (existing.source !== 'parent-rollout') return state;
306
+ return derive(Object.freeze({ ...state, child: Object.freeze({ ...event }) }));
307
+ }
289
308
  return invalid(state, 'CODEX_EVIDENCE_REPLAY_CONFLICT', `conflicting '${event.type}' event`);
290
309
  }
291
310
  return derive(Object.freeze({ ...state, [key]: Object.freeze({ ...event }) }));
@@ -3,6 +3,7 @@
3
3
  "bin/internal-cli.ts",
4
4
  "lib/internal/codex-lifecycle.ts",
5
5
  "lib/internal/codex-sandbox-controller.ts",
6
+ "lib/internal/sandbox-control.ts",
6
7
  "lib/internal/task-orchestration.ts",
7
8
  "lib/task/codex-orchestration.ts",
8
9
  "lib/task/delegation-receipts.ts",
@@ -10,10 +11,17 @@
10
11
  "lib/agent-clients/adapters/codex-lifecycle/build-identity.ts",
11
12
  "lib/agent-clients/adapters/codex-lifecycle/capability-store.ts",
12
13
  "lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts",
14
+ "lib/agent-clients/adapters/codex-lifecycle/controller-context.ts",
13
15
  "lib/agent-clients/adapters/codex-lifecycle/app-server.ts",
14
16
  "lib/agent-clients/adapters/codex-lifecycle/evidence.ts",
15
17
  "lib/agent-clients/adapters/codex-lifecycle/store.ts",
16
- "lib/agent-clients/adapters/codex-lifecycle/manifest-files.json"
18
+ "lib/agent-clients/adapters/codex-lifecycle/manifest-files.json",
19
+ "lib/sandbox/control/protocol.ts",
20
+ "lib/sandbox/control/client.ts",
21
+ "lib/sandbox/control/executor.ts",
22
+ "lib/sandbox/control/controller-registration.ts",
23
+ "lib/server/process-state.ts",
24
+ "lib/sandbox/shell.ts"
17
25
  ],
18
26
  "contractFiles": [
19
27
  ".codex/hooks.json",