@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
@@ -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",
@@ -6,36 +6,32 @@ import path from 'node:path';
6
6
  import semver from 'semver';
7
7
  import * as toml from 'smol-toml';
8
8
 
9
- import { requestSandboxControl } from '../../../sandbox/control/client.ts';
9
+ import {
10
+ requestCodexControllerClose,
11
+ requestCodexControllerOpen,
12
+ requestCodexControllerVerify,
13
+ requestSandboxControl
14
+ } from '../../../sandbox/control/client.ts';
10
15
  import { readSandboxControlStatus } from '../../../sandbox/control/state.ts';
11
- import { getProcessStartTime, isProcessAlive } from '../../../server/process-state.ts';
16
+ import { getProcessStartTime, type ProcessIdentity } from '../../../server/process-state.ts';
12
17
  import { computeLifecycleBuildIdentity } from './build-identity.ts';
13
- import type { LifecycleBuildIdentity } from './build-identity.ts';
18
+ import {
19
+ contextFromControllerLease,
20
+ controllerProofFromContext,
21
+ verifyCodexSandboxControllerContext as verifyContextFile,
22
+ writeCodexSandboxControllerContext,
23
+ type CodexSandboxControllerContextV2
24
+ } from './controller-context.ts';
14
25
 
15
26
  type ControllerControl = Readonly<{
16
27
  token: string;
17
28
  generation: string;
18
29
  channelDir: string;
19
30
  statusDir: string;
20
- }>;
21
-
22
- type CodexSandboxControllerContext = Readonly<{
23
- version: 1;
24
- taskId: string;
25
- controlGeneration: string;
26
- controllerInstanceDigest: string;
27
- parentPid: number;
28
- parentStartTime: number;
29
- issuedAt: number;
30
- expiresAt: number;
31
- buildIdentity: LifecycleBuildIdentity;
32
- hookDefinitionHash: string;
33
- lifecycleProfilesHash: string;
31
+ runtimeDir: string;
34
32
  }>;
35
33
 
36
34
  type ControllerInput = Readonly<{
37
- taskId: string;
38
- taskRef: string;
39
35
  executorModel?: string;
40
36
  executorReasoningEffort?: string;
41
37
  reviewerModel?: string;
@@ -50,6 +46,8 @@ type ControllerOptions = Readonly<{
50
46
  now?: () => number;
51
47
  codexVersion?: () => string;
52
48
  verifyTaskBinding?: (taskId: string, control: ControllerControl) => void;
49
+ openController?: typeof requestCodexControllerOpen;
50
+ closeController?: typeof requestCodexControllerClose;
53
51
  environment?: NodeJS.ProcessEnv;
54
52
  }>;
55
53
 
@@ -59,11 +57,18 @@ type PreparedCodexSandboxController = Readonly<{
59
57
  env: NodeJS.ProcessEnv;
60
58
  home: string;
61
59
  contextPath: string;
62
- context: CodexSandboxControllerContext;
60
+ context: CodexSandboxControllerContextV2;
63
61
  cleanup: () => void;
64
62
  }>;
65
63
 
66
- const CONTEXT_TTL_MS = 4 * 60 * 60 * 1_000;
64
+ type VerifyContextOptions = Readonly<{
65
+ repoRoot?: string;
66
+ now?: number;
67
+ generation?: string;
68
+ probeProcess?: (identity: ProcessIdentity) => 'alive' | 'dead' | 'unknown';
69
+ control?: ControllerControl;
70
+ requestControllerVerify?: typeof requestCodexControllerVerify;
71
+ }>;
67
72
 
68
73
  function digestFiles(files: readonly string[]): string {
69
74
  const hash = crypto.createHash('sha256');
@@ -95,13 +100,38 @@ function controlFromEnvironment(): ControllerControl {
95
100
  const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
96
101
  const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
97
102
  const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
98
- if (!token || !generation || !channelDir || !statusDir) {
103
+ const runtimeDir = process.env.AGENT_INFRA_RUNTIME_DIR;
104
+ if (!token || !generation || !channelDir || !statusDir || !runtimeDir) {
99
105
  throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
100
106
  }
101
- return { token, generation, channelDir, statusDir };
107
+ return { token, generation, channelDir, statusDir, runtimeDir };
108
+ }
109
+
110
+ function verifyRuntime(runtimeDir: string): void {
111
+ let stat: fs.Stats;
112
+ try {
113
+ stat = fs.lstatSync(runtimeDir);
114
+ } catch {
115
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
116
+ }
117
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
118
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
119
+ }
120
+ const probe = path.join(runtimeDir, `.controller-${process.pid}-${crypto.randomUUID()}.probe`);
121
+ try {
122
+ fs.writeFileSync(probe, 'runtime-probe\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 });
123
+ if (fs.readFileSync(probe, 'utf8') !== 'runtime-probe\n') {
124
+ throw new Error('probe mismatch');
125
+ }
126
+ } catch {
127
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
128
+ } finally {
129
+ fs.rmSync(probe, { force: true });
130
+ }
102
131
  }
103
132
 
104
133
  function verifyControl(taskId: string, control: ControllerControl): void {
134
+ verifyRuntime(control.runtimeDir);
105
135
  let status: ReturnType<typeof readSandboxControlStatus> | null = null;
106
136
  let lastError: unknown = null;
107
137
  for (let attempt = 0; attempt < 5; attempt += 1) {
@@ -136,6 +166,30 @@ function verifyControl(taskId: string, control: ControllerControl): void {
136
166
  if (payload.taskId !== taskId) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
137
167
  }
138
168
 
169
+ function verifyCodexSandboxControllerContext(
170
+ contextPath: string,
171
+ options: VerifyContextOptions = {}
172
+ ): CodexSandboxControllerContextV2 {
173
+ const control = options.control ?? controlFromEnvironment();
174
+ const context = verifyContextFile(contextPath, {
175
+ repoRoot: options.repoRoot,
176
+ now: options.now,
177
+ generation: control.generation,
178
+ probeProcess: options.probeProcess
179
+ });
180
+ const verified = (options.requestControllerVerify ?? requestCodexControllerVerify)({
181
+ controllerProof: controllerProofFromContext(context),
182
+ ...control,
183
+ timeoutMs: 30_000
184
+ });
185
+ if (verified.binding.taskId !== context.taskId
186
+ || verified.binding.controlGeneration !== context.controlGeneration
187
+ || verified.binding.controllerInstanceDigest !== context.controllerInstanceDigest) {
188
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
189
+ }
190
+ return context;
191
+ }
192
+
139
193
  function detectedCodexVersion(): string {
140
194
  const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
141
195
  const version = /codex-cli\s+(\d+\.\d+\.\d+)/u.exec(result.stdout ?? '')?.[1];
@@ -214,6 +268,7 @@ function isolatedEnvironment(
214
268
  AGENT_INFRA_CONTROL_GENERATION: control.generation,
215
269
  AGENT_INFRA_CONTROL_DIR: control.channelDir,
216
270
  AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
271
+ AGENT_INFRA_RUNTIME_DIR: control.runtimeDir,
217
272
  AGENT_INFRA_CODEX_CONTROLLER_CONTEXT: contextPath
218
273
  };
219
274
  }
@@ -229,7 +284,6 @@ function prepareCodexSandboxController(
229
284
  const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
230
285
  const sourceHome = path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'));
231
286
  const control = options.control ?? controlFromEnvironment();
232
- (options.verifyTaskBinding ?? verifyControl)(input.taskId, control);
233
287
  const version = (options.codexVersion ?? detectedCodexVersion)();
234
288
  if (!semver.gte(version, '0.147.0')) throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNSUPPORTED');
235
289
 
@@ -238,46 +292,33 @@ function prepareCodexSandboxController(
238
292
  fs.mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
239
293
  fs.chmodSync(runtimeRoot, 0o700);
240
294
  const key = crypto.createHash('sha256')
241
- .update(`${input.taskId}\0${control.generation}`)
295
+ .update(`${process.pid}\0${control.generation}`)
242
296
  .digest('hex');
243
- const leasePath = path.join(runtimeRoot, `${key}.lease.json`);
244
- if (fs.existsSync(leasePath)) {
245
- const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { pid?: number; startTime?: number };
246
- if (Number.isSafeInteger(lease.pid) && typeof lease.startTime === 'number'
247
- && getProcessStartTime(lease.pid!) === lease.startTime) {
248
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUSY');
249
- }
250
- fs.unlinkSync(leasePath);
251
- }
252
-
253
297
  const parentStartTime = getProcessStartTime(process.pid);
254
298
  if (!parentStartTime) throw new Error('CODEX_SANDBOX_CONTROLLER_PROCESS_IDENTITY_INVALID');
255
- const instanceDigest = crypto.randomBytes(32).toString('hex');
256
- fs.writeFileSync(leasePath, `${JSON.stringify({
257
- version: 1,
258
- pid: process.pid,
259
- startTime: parentStartTime,
260
- taskId: input.taskId,
261
- generation: control.generation,
262
- instanceDigest
263
- })}\n`, { mode: 0o600, flag: 'wx' });
264
-
265
299
  const home = fs.mkdtempSync(path.join(runtimeRoot, `${key}-`));
266
300
  fs.chmodSync(home, 0o700);
267
301
  let cleaned = false;
302
+ let context: CodexSandboxControllerContextV2 | null = null;
268
303
  const cleanup = () => {
269
304
  if (cleaned) return;
270
305
  cleaned = true;
306
+ if (context) {
307
+ try {
308
+ (options.closeController ?? requestCodexControllerClose)({
309
+ controllerProcess: context.controllerProcess,
310
+ controllerProof: controllerProofFromContext(context),
311
+ ...control,
312
+ timeoutMs: 30_000
313
+ });
314
+ } catch {
315
+ // The wrapper is exiting; dead-process recovery safely handles an unknown close result.
316
+ }
317
+ }
271
318
  const relative = path.relative(runtimeRoot, home);
272
319
  if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
273
320
  fs.rmSync(home, { recursive: true, force: true });
274
321
  }
275
- try {
276
- const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { instanceDigest?: string };
277
- if (lease.instanceDigest === instanceDigest) fs.unlinkSync(leasePath);
278
- } catch {
279
- // A missing or replaced lease is owned by another cleanup path.
280
- }
281
322
  };
282
323
 
283
324
  try {
@@ -306,22 +347,25 @@ function prepareCodexSandboxController(
306
347
  }
307
348
 
308
349
  const buildIdentity = computeLifecycleBuildIdentity(repoRoot);
309
- const now = (options.now ?? Date.now)();
310
- const context: CodexSandboxControllerContext = Object.freeze({
311
- version: 1,
312
- taskId: input.taskId,
313
- controlGeneration: control.generation,
314
- controllerInstanceDigest: instanceDigest,
315
- parentPid: process.pid,
316
- parentStartTime,
317
- issuedAt: now,
318
- expiresAt: now + CONTEXT_TTL_MS,
319
- buildIdentity,
350
+ const opened = (options.openController ?? requestCodexControllerOpen)({
351
+ controllerProcess: { pid: process.pid, startTime: parentStartTime },
352
+ ...control,
353
+ timeoutMs: 30_000
354
+ });
355
+ const taskId = opened.lease.taskId;
356
+ context = contextFromControllerLease(opened.lease, {
320
357
  hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex'),
321
358
  lifecycleProfilesHash: digestFiles([executor, reviewer])
322
359
  });
360
+ if (opened.lease.controlGeneration !== control.generation
361
+ || opened.lease.controllerProcess.pid !== process.pid
362
+ || opened.lease.controllerProcess.startTime !== parentStartTime
363
+ || JSON.stringify(opened.lease.buildIdentity) !== JSON.stringify(buildIdentity)) {
364
+ throw new Error('SANDBOX_CONTROL_RESULT_INVALID');
365
+ }
366
+ (options.verifyTaskBinding ?? verifyControl)(taskId, control);
323
367
  const contextPath = path.join(home, 'controller-context.json');
324
- fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
368
+ writeCodexSandboxControllerContext(contextPath, context);
325
369
 
326
370
  const shimDir = path.join(home, 'bin');
327
371
  fs.mkdirSync(shimDir, { mode: 0o700 });
@@ -337,7 +381,7 @@ function prepareCodexSandboxController(
337
381
  input.reviewerModel ? `--reviewer-model ${input.reviewerModel}` : '',
338
382
  input.reviewerReasoningEffort ? `--reviewer-reasoning-effort ${input.reviewerReasoningEffort}` : ''
339
383
  ].filter(Boolean).join(' ');
340
- const prompt = `$run-task ${input.taskRef}${policy ? ` ${policy}` : ''}`;
384
+ const prompt = `$run-task ${taskId}${policy ? ` ${policy}` : ''}`;
341
385
  const args = Object.freeze([
342
386
  'exec',
343
387
  '--enable', 'hooks',
@@ -352,7 +396,7 @@ function prepareCodexSandboxController(
352
396
  home,
353
397
  shimDir,
354
398
  contextPath,
355
- input.taskId,
399
+ taskId,
356
400
  control,
357
401
  providerEnvironment,
358
402
  options.environment ?? process.env
@@ -372,32 +416,6 @@ function prepareCodexSandboxController(
372
416
  }
373
417
  }
374
418
 
375
- function verifyCodexSandboxControllerContext(
376
- contextPath: string,
377
- taskId: string,
378
- options: Readonly<{ repoRoot?: string; now?: number; generation?: string }> = {}
379
- ): CodexSandboxControllerContext {
380
- const stat = fs.lstatSync(contextPath);
381
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
382
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
383
- }
384
- const context = JSON.parse(fs.readFileSync(contextPath, 'utf8')) as CodexSandboxControllerContext;
385
- const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
386
- if (context.version !== 1
387
- || context.taskId !== taskId
388
- || context.controlGeneration !== generation
389
- || context.expiresAt < (options.now ?? Date.now())
390
- || getProcessStartTime(context.parentPid) !== context.parentStartTime
391
- || !isProcessAlive(context.parentPid)) {
392
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
393
- }
394
- const current = computeLifecycleBuildIdentity(options.repoRoot ?? process.cwd());
395
- if (JSON.stringify(current) !== JSON.stringify(context.buildIdentity)) {
396
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
397
- }
398
- return Object.freeze(context);
399
- }
400
-
401
419
  async function runCodexSandboxController(
402
420
  input: ControllerInput,
403
421
  options: ControllerOptions = {}
@@ -442,10 +460,11 @@ async function runCodexSandboxController(
442
460
  export {
443
461
  prepareCodexSandboxController,
444
462
  runCodexSandboxController,
445
- verifyCodexSandboxControllerContext
463
+ verifyCodexSandboxControllerContext,
464
+ controllerProofFromContext
446
465
  };
447
466
  export type {
448
- CodexSandboxControllerContext,
467
+ CodexSandboxControllerContextV2 as CodexSandboxControllerContext,
449
468
  ControllerControl,
450
469
  ControllerInput,
451
470
  ControllerOptions,
@@ -11,6 +11,7 @@ import type {
11
11
  CodexLifecycleEvent,
12
12
  CodexLifecycleState
13
13
  } from './evidence.ts';
14
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
14
15
 
15
16
  type StoredCodexLifecycle = Readonly<{
16
17
  schemaVersion: 1;
@@ -105,7 +106,7 @@ function writeRecord(file: string, record: StoredCodexLifecycle, expectedRevisio
105
106
  }
106
107
 
107
108
  function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
108
- const root = options.root ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-lifecycle');
109
+ const root = options.root ?? resolveAgentRuntimeStoreRoot({ store: 'lifecycle' });
109
110
  const now = options.now ?? (() => new Date().toISOString());
110
111
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
111
112
  fs.chmodSync(root, 0o700);