@fitlab-ai/agent-infra 0.9.7 → 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 (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. package/lib/task/commit-intent.ts +0 -230
package/README.md CHANGED
@@ -182,7 +182,7 @@ The most-used lifecycle commands, in delivery order. The command prefix varies b
182
182
  | `code-task` → `review-code` | Implement and test, then run a structured code review |
183
183
  | `commit` → `create-pr` → `complete-task` | Commit, open a PR, and archive the task |
184
184
 
185
- Start with one atomic role policy, for example: `$run-task 42 --executor-model <id> --executor-reasoning-effort <value> --reviewer-model <id> --reviewer-reasoning-effort <value>`. Both roles may use the same model. With no explicit policy, `run-task` reads the current Agent Client's optional `agentClients[].orchestration`; if neither source is complete, it shows the host model-selection guidance before creating any run. Re-entry uses the persisted v2 policy.
185
+ Start with one atomic role policy, for example: `$run-task 42 --executor-model <id> --executor-reasoning-effort <value> --reviewer-model <id> --reviewer-reasoning-effort <value>`. Both roles may use the same model. With no explicit policy, `run-task` reads the current Agent Client's optional `agentClients[].orchestration`; if neither source is complete, it shows the host model-selection guidance before creating any run. Re-entry uses the persisted run policy. Finish or clear active runs before upgrading agent-infra; unsupported disk state fails closed without rewriting.
186
186
 
187
187
  See the full catalog — task status, release, security, and project-maintenance skills — in [Built-in AI Skills](./docs/en/skills.md).
188
188
 
package/README.zh-CN.md CHANGED
@@ -181,7 +181,7 @@ ai agent-client configure
181
181
  | `code-task` → `review-code` | 实现并测试,再执行结构化代码审查 |
182
182
  | `commit` → `create-pr` → `complete-task` | 提交、创建 PR、归档任务 |
183
183
 
184
- 启动时可提供原子角色策略,例如:`$run-task 42 --executor-model <id> --executor-reasoning-effort <值> --reviewer-model <id> --reviewer-reasoning-effort <值>`。两个角色可以使用同一模型。完全没有显式策略时,`run-task` 读取当前 Agent Client 可选的 `agentClients[].orchestration`;两个来源都不完整时,会在创建任何 run 前展示宿主模型选择指引。重入使用已持久化的 v2 策略。
184
+ 启动时可提供原子角色策略,例如:`$run-task 42 --executor-model <id> --executor-reasoning-effort <值> --reviewer-model <id> --reviewer-reasoning-effort <值>`。两个角色可以使用同一模型。完全没有显式策略时,`run-task` 读取当前 Agent Client 可选的 `agentClients[].orchestration`;两个来源都不完整时,会在创建任何 run 前展示宿主模型选择指引。重入使用已持久化的 run 策略。升级 agent-infra 前必须完成或清空 active run;不受支持的磁盘状态会失败关闭且不改写。
185
185
 
186
186
  完整清单(任务状态、发布、安全、项目维护等 skill)见 [内置 AI Skills](./docs/zh-CN/skills.md)。
187
187
 
@@ -6,7 +6,7 @@ if (major < 22 || (major === 22 && minor < 9)) {
6
6
  }
7
7
  const command = process.argv[2] || '';
8
8
  if (process.env.AGENT_INFRA_CONTROL_TOKEN
9
- && (command === 'task-lifecycle' || command === 'task-orchestration')) {
9
+ && (command === 'task-lifecycle' || command === 'task-orchestration' || command === 'task-finalization')) {
10
10
  const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
11
11
  await sandboxControl(['client', command, ...process.argv.slice(3)]);
12
12
  }
@@ -132,6 +132,16 @@ else
132
132
  taskLifecycle(process.argv.slice(3));
133
133
  break;
134
134
  }
135
+ case 'task-finalization': {
136
+ const { taskFinalization } = await import('../lib/internal/task-finalization.js');
137
+ taskFinalization(process.argv.slice(3));
138
+ break;
139
+ }
140
+ case 'task-override': {
141
+ const { taskOverride } = await import('../lib/internal/task-override.js');
142
+ taskOverride(process.argv.slice(3));
143
+ break;
144
+ }
135
145
  case 'task-short-id': {
136
146
  const { taskShortId } = await import('../lib/internal/task-short-id.js');
137
147
  taskShortId(process.argv.slice(3));
@@ -11,7 +11,7 @@ const claudeCodeAdapter = defineAgentClientAdapter({
11
11
  commands: { level: 'integrated' },
12
12
  hooks: { level: 'integrated' },
13
13
  subagents: { level: 'experimental' },
14
- orchestration: { level: 'unsupported' },
14
+ orchestration: { level: 'experimental' },
15
15
  sandbox: { level: 'integrated' },
16
16
  verification: { level: 'compatible' }
17
17
  },
@@ -21,8 +21,8 @@ const claudeCodeAdapter = defineAgentClientAdapter({
21
21
  guidance: 'Use the host model picker for the complete model and reasoning-effort catalog.'
22
22
  },
23
23
  delegationEvidence: {
24
- actualModel: 'unavailable',
25
- actualReasoningEffort: 'unavailable'
24
+ actualModel: 'host-event',
25
+ actualReasoningEffort: 'spawn-ack'
26
26
  },
27
27
  project: {
28
28
  ownedPathPrefixes: ['.claude/'],
@@ -4,6 +4,7 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import semver from 'semver';
7
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
7
8
  const LIFECYCLE_HOOKS = Object.freeze([
8
9
  Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
9
10
  Object.freeze({ event: 'PostToolUse', matcher: '', phase: 'post-tool' }),
@@ -12,6 +13,10 @@ const LIFECYCLE_HOOKS = Object.freeze([
12
13
  ]);
13
14
  const DEFAULT_ROLLOUT_READ_ATTEMPTS = 81;
14
15
  const DEFAULT_ROLLOUT_RETRY_MS = 100;
16
+ const DEFAULT_THREAD_READ_ATTEMPTS = 21;
17
+ const DEFAULT_THREAD_RETRY_MS = 100;
18
+ const DEFAULT_TERMINAL_READ_ATTEMPTS = 21;
19
+ const DEFAULT_TERMINAL_RETRY_MS = 100;
15
20
  function object(value) {
16
21
  return value && typeof value === 'object' && !Array.isArray(value)
17
22
  ? value
@@ -61,16 +66,20 @@ function resolveCodexSpawnedChild(transcriptPath, expected) {
61
66
  throw new Error('Codex parent rollout spawn identity does not match the hook event');
62
67
  const activities = records.filter((record) => {
63
68
  const payload = object(record?.payload);
69
+ const item = object(payload?.item);
64
70
  return record?.type === 'event_msg'
65
- && payload?.type === 'sub_agent_activity'
66
- && payload.event_id === expected.toolUseId
67
- && payload.kind === 'started'
68
- && payload.agent_path === `/root/${expected.taskName}`
69
- && nonEmpty(payload.agent_thread_id);
71
+ && payload?.type === 'item_completed'
72
+ && payload.thread_id === expected.sessionId
73
+ && payload.turn_id === expected.turnId
74
+ && item?.type === 'SubAgentActivity'
75
+ && item.id === expected.toolUseId
76
+ && item.kind === 'started'
77
+ && item.agent_path === `/root/${expected.taskName}`
78
+ && nonEmpty(item.agent_thread_id);
70
79
  });
71
80
  if (activities.length !== 1)
72
81
  throw new Error('Codex parent rollout child activity was not found uniquely');
73
- return nonEmpty(object(activities[0]?.payload)?.agent_thread_id);
82
+ return nonEmpty(object(object(activities[0]?.payload)?.item)?.agent_thread_id);
74
83
  }
75
84
  function validateCodexLifecycleHookConfig(value) {
76
85
  const hooks = object(object(value)?.hooks);
@@ -85,7 +94,7 @@ function validateCodexLifecycleHookConfig(value) {
85
94
  return entry.hooks.some((hookValue) => {
86
95
  const hook = object(hookValue);
87
96
  return hook?.type === 'command'
88
- && hook.timeout === 15
97
+ && hook.timeout === 30
89
98
  && hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
90
99
  });
91
100
  });
@@ -409,14 +418,26 @@ async function resolveCodexThread(childThreadId, options = {}) {
409
418
  const readThread = object(readResult?.thread);
410
419
  if (nonEmpty(readThread?.id) !== childThreadId)
411
420
  throw new Error('Codex App Server returned the wrong child thread');
412
- let resolution;
421
+ let rolloutRecords;
413
422
  try {
414
- const rolloutRecords = await resolveCodexRolloutRecords(readResult, childThreadId, options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS, options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS);
415
- resolution = parseCodexThreadResolution(readResult, rolloutRecords);
423
+ rolloutRecords = await resolveCodexRolloutRecords(readResult, childThreadId, options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS, options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS);
416
424
  }
417
425
  catch {
418
426
  throw new Error('Codex App Server rollout metadata is unavailable');
419
427
  }
428
+ let resolution = parseCodexThreadResolution(readResult, rolloutRecords);
429
+ const threadReadAttempts = options.threadReadAttempts ?? DEFAULT_THREAD_READ_ATTEMPTS;
430
+ for (let attempt = 1; resolution.thread.forkedFromId !== null && attempt < threadReadAttempts; attempt += 1) {
431
+ await new Promise((resolve) => setTimeout(resolve, options.threadRetryMs ?? DEFAULT_THREAD_RETRY_MS));
432
+ const refreshed = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
433
+ if (nonEmpty(object(refreshed?.thread)?.id) !== childThreadId) {
434
+ throw new Error('Codex App Server returned the wrong child thread');
435
+ }
436
+ resolution = parseCodexThreadResolution(refreshed, rolloutRecords);
437
+ }
438
+ if (resolution.thread.forkedFromId !== null) {
439
+ throw new Error('Codex App Server fresh thread metadata is unavailable');
440
+ }
420
441
  const reroutes = transport.notifications
421
442
  .filter((entry) => entry.method === 'model/rerouted')
422
443
  .map((entry) => parseCodexModelReroute(entry.params))
@@ -433,21 +454,38 @@ async function resolveCodexThread(childThreadId, options = {}) {
433
454
  transport.close();
434
455
  }
435
456
  }
436
- async function resolveCodexTerminal(childThreadId, options = {}) {
457
+ async function resolveCodexTerminal(childThreadId, expectedTurnIdOrOptions = {}, terminalOptions = {}) {
458
+ const expectedTurnId = typeof expectedTurnIdOrOptions === 'string' ? expectedTurnIdOrOptions : null;
459
+ const options = typeof expectedTurnIdOrOptions === 'string' ? terminalOptions : expectedTurnIdOrOptions;
437
460
  const transport = new CodexAppServerTransport(options);
438
461
  transport.start();
439
462
  try {
440
463
  await transport.initialize();
441
- const result = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: true }));
442
- const thread = object(result?.thread);
443
- if (nonEmpty(thread?.id) !== childThreadId)
444
- throw new Error('Codex App Server returned the wrong child thread');
445
- const turns = Array.isArray(thread?.turns) ? thread.turns : [];
446
- const turn = turns.at(-1);
447
- const turnValue = object(turn);
448
- if (!turnValue || turnValue.status === 'inProgress')
449
- throw new Error('CODEX_TURN_NOT_TERMINAL');
450
- return parseCodexTurnCompleted({ threadId: childThreadId, turn });
464
+ const attempts = expectedTurnId ? options.terminalReadAttempts ?? DEFAULT_TERMINAL_READ_ATTEMPTS : 1;
465
+ let abnormal = null;
466
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
467
+ const result = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: true }));
468
+ const thread = object(result?.thread);
469
+ if (nonEmpty(thread?.id) !== childThreadId)
470
+ throw new Error('Codex App Server returned the wrong child thread');
471
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
472
+ const turn = expectedTurnId
473
+ ? turns.find((candidate) => object(candidate)?.id === expectedTurnId)
474
+ : turns.at(-1);
475
+ const turnValue = object(turn);
476
+ if (turnValue && turnValue.status !== 'inProgress') {
477
+ const terminal = parseCodexTurnCompleted({ threadId: childThreadId, turn });
478
+ if (terminal.status === 'completed')
479
+ return terminal;
480
+ abnormal = terminal;
481
+ }
482
+ if (attempt + 1 < attempts) {
483
+ await new Promise((resolve) => setTimeout(resolve, options.terminalRetryMs ?? DEFAULT_TERMINAL_RETRY_MS));
484
+ }
485
+ }
486
+ if (abnormal)
487
+ return abnormal;
488
+ throw new Error('CODEX_TURN_NOT_TERMINAL');
451
489
  }
452
490
  finally {
453
491
  transport.close();
@@ -482,7 +520,7 @@ async function preflightCodexLifecycleEvidence(repoRoot = process.cwd(), runtime
482
520
  fs.rmSync(schemaDir, { recursive: true, force: true });
483
521
  }
484
522
  const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
485
- const runtimeRoot = path.join(repoRoot, '.agents', 'workspace', '.runtime', 'codex-lifecycle');
523
+ const runtimeRoot = resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' });
486
524
  const runtimeLiveness = hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, runtimeIdentity);
487
525
  const transport = new CodexAppServerTransport();
488
526
  transport.start();
@@ -2,10 +2,17 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import semver from 'semver';
5
6
  const LIFECYCLE_PROTOCOL_VERSION = 3;
6
7
  function exactText(value) {
7
8
  return typeof value === 'string' && value.trim() === value && value.length > 0;
8
9
  }
10
+ function isCanonicalLifecyclePackageVersion(value) {
11
+ return typeof value === 'string' && semver.valid(value) === value;
12
+ }
13
+ function isLifecycleProtocolVersion(value) {
14
+ return value === LIFECYCLE_PROTOCOL_VERSION;
15
+ }
9
16
  function normalizeRelativePath(value) {
10
17
  return value.replaceAll('\\', '/');
11
18
  }
@@ -158,5 +165,5 @@ function verifyLifecycleBuildIdentity(expected, actual) {
158
165
  }
159
166
  return { ok: true, code: null, message: null };
160
167
  }
161
- export { LIFECYCLE_PROTOCOL_VERSION, computeLifecycleBuildIdentity, readLifecycleManifestFiles, resolveLifecycleExecutableFiles, verifyLifecycleBuildIdentity };
168
+ export { LIFECYCLE_PROTOCOL_VERSION, computeLifecycleBuildIdentity, isCanonicalLifecyclePackageVersion, isLifecycleProtocolVersion, readLifecycleManifestFiles, resolveLifecycleExecutableFiles, verifyLifecycleBuildIdentity };
162
169
  //# sourceMappingURL=build-identity.js.map
@@ -1,28 +1,180 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { verifyLifecycleBuildIdentity } from './build-identity.js';
4
+ import { isCanonicalLifecyclePackageVersion, isLifecycleProtocolVersion, LIFECYCLE_PROTOCOL_VERSION, verifyLifecycleBuildIdentity } from './build-identity.js';
5
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
5
6
  const DEFAULT_TTL_MS = 30_000;
6
7
  const DEFAULT_TOMBSTONE_MS = 86_400_000;
7
8
  const DEFAULT_MAX_RECORDS = 128;
8
9
  const LOCK_RETRY_MS = 10;
9
10
  const LOCK_TIMEOUT_MS = 1_000;
10
11
  const LOCK_STALE_MS = 30_000;
12
+ const DETAIL_DOMAIN = 'codex-capability-provenance/v1';
13
+ const TASK_ID_PATTERN = /^TASK-[0-9]{8}-[0-9]{6}$/u;
14
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
15
+ const DIGEST_PREFIX_PATTERN = /^sha256:[0-9a-f]{16}$/u;
11
16
  function digest(value) {
12
17
  return crypto.createHash('sha256').update(value).digest('hex');
13
18
  }
14
- function capabilityError(code, message) {
19
+ function capabilityError(code, message, detail) {
15
20
  const error = new Error(`${code}: ${message}`);
16
21
  error.name = code;
22
+ if (detail)
23
+ error.detail = detail;
17
24
  return error;
18
25
  }
26
+ function objectValue(value, key) {
27
+ if (!value || typeof value !== 'object')
28
+ return undefined;
29
+ return value[key];
30
+ }
31
+ function digestPrefix(fieldName, value) {
32
+ return {
33
+ kind: 'digest-prefix',
34
+ value: `sha256:${crypto.createHash('sha256')
35
+ .update(`${DETAIL_DOMAIN}\0${fieldName}\0${value}`)
36
+ .digest('hex')
37
+ .slice(0, 16)}`
38
+ };
39
+ }
40
+ function projectDigest(fieldName, value, pattern) {
41
+ return typeof value === 'string' && pattern.test(value) ? digestPrefix(fieldName, value) : { kind: 'absent' };
42
+ }
43
+ function projectTaskId(value) {
44
+ return projectDigest('taskId', value, TASK_ID_PATTERN);
45
+ }
46
+ function projectHash(fieldName, value) {
47
+ return projectDigest(fieldName, value, SHA256_PATTERN);
48
+ }
49
+ function projectProtocol(value) {
50
+ return isLifecycleProtocolVersion(value)
51
+ ? { kind: 'protocol-version', value: LIFECYCLE_PROTOCOL_VERSION }
52
+ : { kind: 'absent' };
53
+ }
54
+ function projectPackageVersion(value) {
55
+ return isCanonicalLifecyclePackageVersion(value)
56
+ ? { kind: 'semver', value }
57
+ : { kind: 'absent' };
58
+ }
59
+ function projectPresence(value) {
60
+ return { kind: 'presence', present: Boolean(value && typeof value === 'object') };
61
+ }
62
+ function field(matches, expected, actual) {
63
+ return { matches, expected, actual };
64
+ }
65
+ function provenanceDetail(record, expected) {
66
+ const actualBuild = record.buildIdentity;
67
+ const expectedBuild = expected.buildIdentity;
68
+ const actualController = record.controller;
69
+ const expectedController = expected.controller ?? null;
70
+ return {
71
+ kind: 'codex-capability-provenance-mismatch',
72
+ version: 1,
73
+ fields: {
74
+ taskId: field(record.taskId === expected.taskId, projectTaskId(expected.taskId), projectTaskId(record.taskId)),
75
+ hookDefinitionHash: field(record.hookDefinitionHash === expected.hookDefinitionHash, projectHash('hookDefinitionHash', expected.hookDefinitionHash), projectHash('hookDefinitionHash', record.hookDefinitionHash)),
76
+ buildIdentity: {
77
+ protocolVersion: field(objectValue(actualBuild, 'protocolVersion') === objectValue(expectedBuild, 'protocolVersion'), projectProtocol(objectValue(expectedBuild, 'protocolVersion')), projectProtocol(objectValue(actualBuild, 'protocolVersion'))),
78
+ packageVersion: field(objectValue(actualBuild, 'packageVersion') === objectValue(expectedBuild, 'packageVersion'), projectPackageVersion(objectValue(expectedBuild, 'packageVersion')), projectPackageVersion(objectValue(actualBuild, 'packageVersion'))),
79
+ internalExecutableBuildHash: field(objectValue(actualBuild, 'internalExecutableBuildHash') === objectValue(expectedBuild, 'internalExecutableBuildHash'), projectHash('buildIdentity.internalExecutableBuildHash', objectValue(expectedBuild, 'internalExecutableBuildHash')), projectHash('buildIdentity.internalExecutableBuildHash', objectValue(actualBuild, 'internalExecutableBuildHash'))),
80
+ lifecycleContractHash: field(objectValue(actualBuild, 'lifecycleContractHash') === objectValue(expectedBuild, 'lifecycleContractHash'), projectHash('buildIdentity.lifecycleContractHash', objectValue(expectedBuild, 'lifecycleContractHash')), projectHash('buildIdentity.lifecycleContractHash', objectValue(actualBuild, 'lifecycleContractHash')))
81
+ },
82
+ controller: {
83
+ instanceDigest: field(sameControllerField(actualController, expectedController, 'instanceDigest'), projectPresence(objectValue(expectedController, 'instanceDigest') === undefined ? null : expectedController), projectPresence(objectValue(actualController, 'instanceDigest') === undefined ? null : actualController)),
84
+ controlGeneration: field(sameControllerField(actualController, expectedController, 'controlGeneration'), projectPresence(objectValue(expectedController, 'controlGeneration') === undefined ? null : expectedController), projectPresence(objectValue(actualController, 'controlGeneration') === undefined ? null : actualController))
85
+ }
86
+ }
87
+ };
88
+ }
89
+ function exactKeys(value, keys) {
90
+ return Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
91
+ }
92
+ function isCodexControllerBinding(value) {
93
+ if (!value || typeof value !== 'object' || Array.isArray(value))
94
+ return false;
95
+ const record = value;
96
+ return exactKeys(record, ['instanceDigest', 'controlGeneration'])
97
+ && typeof record.instanceDigest === 'string'
98
+ && typeof record.controlGeneration === 'string';
99
+ }
100
+ function isProjection(value) {
101
+ if (!value || typeof value !== 'object')
102
+ return false;
103
+ const record = value;
104
+ if (record.kind === 'absent')
105
+ return exactKeys(record, ['kind']);
106
+ if (record.kind === 'protocol-version') {
107
+ return exactKeys(record, ['kind', 'value']) && isLifecycleProtocolVersion(record.value);
108
+ }
109
+ if (record.kind === 'semver') {
110
+ return exactKeys(record, ['kind', 'value']) && isCanonicalLifecyclePackageVersion(record.value);
111
+ }
112
+ if (record.kind === 'digest-prefix') {
113
+ return exactKeys(record, ['kind', 'value'])
114
+ && typeof record.value === 'string'
115
+ && DIGEST_PREFIX_PATTERN.test(record.value);
116
+ }
117
+ if (record.kind === 'presence') {
118
+ return exactKeys(record, ['kind', 'present']) && typeof record.present === 'boolean';
119
+ }
120
+ return false;
121
+ }
122
+ function isProvenanceField(value) {
123
+ if (!value || typeof value !== 'object')
124
+ return false;
125
+ const record = value;
126
+ return exactKeys(record, ['matches', 'expected', 'actual'])
127
+ && typeof record.matches === 'boolean'
128
+ && isProjection(record.expected)
129
+ && isProjection(record.actual);
130
+ }
131
+ function isCodexCapabilityProvenanceDetail(value) {
132
+ if (!value || typeof value !== 'object')
133
+ return false;
134
+ const record = value;
135
+ if (!exactKeys(record, ['kind', 'version', 'fields'])
136
+ || record.kind !== 'codex-capability-provenance-mismatch'
137
+ || record.version !== 1
138
+ || !record.fields || typeof record.fields !== 'object')
139
+ return false;
140
+ const fields = record.fields;
141
+ if (!exactKeys(fields, ['taskId', 'hookDefinitionHash', 'buildIdentity', 'controller'])
142
+ || !isProvenanceField(fields.taskId)
143
+ || !isProvenanceField(fields.hookDefinitionHash))
144
+ return false;
145
+ const buildIdentity = fields.buildIdentity;
146
+ const controller = fields.controller;
147
+ if (!buildIdentity || typeof buildIdentity !== 'object' || !controller || typeof controller !== 'object')
148
+ return false;
149
+ const build = buildIdentity;
150
+ const binding = controller;
151
+ return exactKeys(build, ['protocolVersion', 'packageVersion', 'internalExecutableBuildHash', 'lifecycleContractHash'])
152
+ && exactKeys(binding, ['instanceDigest', 'controlGeneration'])
153
+ && isProvenanceField(build.protocolVersion)
154
+ && isProvenanceField(build.packageVersion)
155
+ && isProvenanceField(build.internalExecutableBuildHash)
156
+ && isProvenanceField(build.lifecycleContractHash)
157
+ && isProvenanceField(binding.instanceDigest)
158
+ && isProvenanceField(binding.controlGeneration);
159
+ }
160
+ function sameControllerField(left, right, key) {
161
+ if (left === null || right === null)
162
+ return left === null && right === null;
163
+ return isCodexControllerBinding(left)
164
+ && isCodexControllerBinding(right)
165
+ && left[key] === right[key];
166
+ }
19
167
  function sameController(left, right) {
20
- return left?.instanceDigest === right?.instanceDigest
21
- && left?.controlGeneration === right?.controlGeneration;
168
+ if (left === null || right === null)
169
+ return left === null && right === null;
170
+ return isCodexControllerBinding(left)
171
+ && isCodexControllerBinding(right)
172
+ && left.instanceDigest === right.instanceDigest
173
+ && left.controlGeneration === right.controlGeneration;
22
174
  }
23
175
  function createCodexCapabilityStore(options = {}) {
24
176
  const root = options.root
25
- ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-capabilities');
177
+ ?? resolveAgentRuntimeStoreRoot({ store: 'capabilities' });
26
178
  const now = options.now ?? Date.now;
27
179
  const createToken = options.token ?? (() => crypto.randomBytes(32).toString('base64url'));
28
180
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
@@ -210,22 +362,33 @@ function createCodexCapabilityStore(options = {}) {
210
362
  write(file, next, record.revision);
211
363
  return next;
212
364
  }
213
- function consume(token, expected) {
214
- sweep();
215
- const { file, record } = loadActive(token);
365
+ function validateRecord(record, expected) {
216
366
  if (record.status === 'consumed') {
217
367
  throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
218
368
  }
219
369
  if (record.status !== 'attested') {
220
370
  throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
221
371
  }
222
- const identity = verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity);
372
+ const identity = record.buildIdentity && typeof record.buildIdentity === 'object'
373
+ ? verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity)
374
+ : { ok: false };
223
375
  if (!identity.ok
224
376
  || record.taskId !== expected.taskId
225
377
  || record.hookDefinitionHash !== expected.hookDefinitionHash
226
378
  || !sameController(record.controller, expected.controller ?? null)) {
227
- throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability consumption identity does not match');
379
+ throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability consumption identity does not match', provenanceDetail(record, expected));
228
380
  }
381
+ return record;
382
+ }
383
+ function validate(token, expected) {
384
+ sweep();
385
+ const { record } = loadActive(token);
386
+ return validateRecord(record, expected);
387
+ }
388
+ function consume(token, expected) {
389
+ sweep();
390
+ const { file, record } = loadActive(token);
391
+ validateRecord(record, expected);
229
392
  const next = Object.freeze({
230
393
  ...record,
231
394
  revision: record.revision + 1,
@@ -238,7 +401,8 @@ function createCodexCapabilityStore(options = {}) {
238
401
  function inspect(token) {
239
402
  return loadActive(token).record;
240
403
  }
241
- return Object.freeze({ arm, attest, consume, inspect, sweep });
404
+ return Object.freeze({ arm, attest, consume, inspect, validate, sweep });
242
405
  }
243
406
  export { createCodexCapabilityStore };
407
+ export { isCodexCapabilityProvenanceDetail };
244
408
  //# sourceMappingURL=capability-store.js.map
@@ -0,0 +1,147 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { getProcessIdentityState } from '../../../server/process-state.js';
5
+ import { computeLifecycleBuildIdentity } from './build-identity.js';
6
+ const HEX_256 = /^[a-f0-9]{64}$/u;
7
+ function exactKeys(value, keys) {
8
+ return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
9
+ }
10
+ function digestProfiles(repoRoot) {
11
+ const files = [
12
+ path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-executor.toml'),
13
+ path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-reviewer.toml')
14
+ ];
15
+ const hash = crypto.createHash('sha256');
16
+ for (const file of files.sort()) {
17
+ const stat = fs.lstatSync(file);
18
+ if (!stat.isFile() || stat.isSymbolicLink())
19
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
20
+ hash.update(path.basename(file));
21
+ hash.update('\0');
22
+ hash.update(fs.readFileSync(file));
23
+ hash.update('\0');
24
+ }
25
+ return hash.digest('hex');
26
+ }
27
+ function validProcess(value) {
28
+ if (!value || typeof value !== 'object' || Array.isArray(value))
29
+ return false;
30
+ const identity = value;
31
+ return exactKeys(identity, ['pid', 'startTime'])
32
+ && Number.isSafeInteger(identity.pid) && identity.pid > 0
33
+ && Number.isSafeInteger(identity.startTime) && identity.startTime >= 0;
34
+ }
35
+ function validBuildIdentity(value) {
36
+ if (!value || typeof value !== 'object' || Array.isArray(value))
37
+ return false;
38
+ const identity = value;
39
+ return exactKeys(identity, [
40
+ 'internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'
41
+ ])
42
+ && identity.protocolVersion === 3
43
+ && typeof identity.packageVersion === 'string' && identity.packageVersion.length > 0
44
+ && typeof identity.internalExecutableBuildHash === 'string' && HEX_256.test(identity.internalExecutableBuildHash)
45
+ && typeof identity.lifecycleContractHash === 'string' && HEX_256.test(identity.lifecycleContractHash);
46
+ }
47
+ function validateContext(value) {
48
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
49
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
50
+ }
51
+ const context = value;
52
+ const lease = context.controllerLease;
53
+ if (!exactKeys(context, [
54
+ 'buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerLease',
55
+ 'controllerProcess', 'expiresAt', 'hookDefinitionHash', 'issuedAt',
56
+ 'lifecycleProfilesHash', 'taskId', 'version'
57
+ ])
58
+ || context.version !== 2
59
+ || typeof context.taskId !== 'string' || context.taskId.length === 0
60
+ || typeof context.controlGeneration !== 'string' || context.controlGeneration.length === 0
61
+ || typeof context.controllerInstanceDigest !== 'string' || !HEX_256.test(context.controllerInstanceDigest)
62
+ || !validProcess(context.controllerProcess)
63
+ || !lease || !exactKeys(lease, ['leaseId', 'leaseSecret', 'version'])
64
+ || lease.version !== 1
65
+ || typeof lease.leaseId !== 'string' || !HEX_256.test(lease.leaseId)
66
+ || typeof lease.leaseSecret !== 'string' || !HEX_256.test(lease.leaseSecret)
67
+ || !Number.isSafeInteger(context.issuedAt)
68
+ || !Number.isSafeInteger(context.expiresAt)
69
+ || context.expiresAt <= context.issuedAt
70
+ || !validBuildIdentity(context.buildIdentity)
71
+ || typeof context.hookDefinitionHash !== 'string' || !HEX_256.test(context.hookDefinitionHash)
72
+ || typeof context.lifecycleProfilesHash !== 'string' || !HEX_256.test(context.lifecycleProfilesHash)) {
73
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
74
+ }
75
+ return context;
76
+ }
77
+ export function contextFromControllerLease(lease, hashes) {
78
+ return Object.freeze({
79
+ version: 2,
80
+ taskId: lease.taskId,
81
+ controlGeneration: lease.controlGeneration,
82
+ controllerInstanceDigest: lease.controllerInstanceDigest,
83
+ controllerProcess: lease.controllerProcess,
84
+ controllerLease: Object.freeze({ version: 1, leaseId: lease.leaseId, leaseSecret: lease.leaseSecret }),
85
+ issuedAt: lease.issuedAt,
86
+ expiresAt: lease.expiresAt,
87
+ buildIdentity: lease.buildIdentity,
88
+ hookDefinitionHash: hashes.hookDefinitionHash,
89
+ lifecycleProfilesHash: hashes.lifecycleProfilesHash
90
+ });
91
+ }
92
+ export function writeCodexSandboxControllerContext(contextPath, context) {
93
+ validateContext(context);
94
+ fs.mkdirSync(path.dirname(contextPath), { recursive: true, mode: 0o700 });
95
+ const temporary = path.join(path.dirname(contextPath), `.${path.basename(contextPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
96
+ try {
97
+ fs.writeFileSync(temporary, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
98
+ fs.chmodSync(temporary, 0o600);
99
+ fs.renameSync(temporary, contextPath);
100
+ }
101
+ finally {
102
+ fs.rmSync(temporary, { force: true });
103
+ }
104
+ }
105
+ export function verifyCodexSandboxControllerContext(contextPath, options = {}) {
106
+ let stat;
107
+ try {
108
+ stat = fs.lstatSync(contextPath);
109
+ }
110
+ catch {
111
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
112
+ }
113
+ if (!stat.isFile() || stat.isSymbolicLink()
114
+ || (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
115
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
116
+ }
117
+ let context;
118
+ try {
119
+ context = validateContext(JSON.parse(fs.readFileSync(contextPath, 'utf8')));
120
+ }
121
+ catch {
122
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
123
+ }
124
+ const repoRoot = options.repoRoot ?? process.cwd();
125
+ const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
126
+ const state = (options.probeProcess ?? getProcessIdentityState)(context.controllerProcess);
127
+ if (context.controlGeneration !== generation
128
+ || context.expiresAt <= (options.now ?? Date.now())
129
+ || state !== 'alive') {
130
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
131
+ }
132
+ if (JSON.stringify(computeLifecycleBuildIdentity(repoRoot)) !== JSON.stringify(context.buildIdentity)
133
+ || crypto.createHash('sha256').update(fs.readFileSync(path.join(repoRoot, '.codex', 'hooks.json'))).digest('hex') !== context.hookDefinitionHash
134
+ || digestProfiles(repoRoot) !== context.lifecycleProfilesHash) {
135
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
136
+ }
137
+ return Object.freeze(context);
138
+ }
139
+ export function controllerProofFromContext(context) {
140
+ return Object.freeze({
141
+ version: 1,
142
+ leaseId: context.controllerLease.leaseId,
143
+ leaseSecret: context.controllerLease.leaseSecret,
144
+ controllerProcess: context.controllerProcess
145
+ });
146
+ }
147
+ //# sourceMappingURL=controller-context.js.map
@@ -29,6 +29,12 @@ function invalid(state, code, message) {
29
29
  function same(left, right) {
30
30
  return JSON.stringify(left) === JSON.stringify(right);
31
31
  }
32
+ function sameChildCorrelation(left, right) {
33
+ return left.sessionId === right.sessionId
34
+ && left.childThreadId === right.childThreadId
35
+ && left.parentThreadId === right.parentThreadId
36
+ && left.nativeAgent === right.nativeAgent;
37
+ }
32
38
  function requiredStrings(event) {
33
39
  return Object.entries(event).every(([key, value]) => key === 'forkedFromId'
34
40
  || (typeof value !== 'string' || value.trim() !== ''));
@@ -152,6 +158,14 @@ function reduceCodexLifecycleEvent(state, event) {
152
158
  if (existing) {
153
159
  if (same(existing, event))
154
160
  return state;
161
+ if (existing.type === 'hook-child'
162
+ && event.type === 'hook-child'
163
+ && sameChildCorrelation(existing, event)
164
+ && (existing.source === 'parent-rollout') !== (event.source === 'parent-rollout')) {
165
+ if (existing.source !== 'parent-rollout')
166
+ return state;
167
+ return derive(Object.freeze({ ...state, child: Object.freeze({ ...event }) }));
168
+ }
155
169
  return invalid(state, 'CODEX_EVIDENCE_REPLAY_CONFLICT', `conflicting '${event.type}' event`);
156
170
  }
157
171
  return derive(Object.freeze({ ...state, [key]: Object.freeze({ ...event }) }));