@fitlab-ai/agent-infra 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/bin/internal-cli.js +10 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
  8. package/dist/lib/decide.js +68 -14
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -3
  11. package/dist/lib/init.js +1 -1
  12. package/dist/lib/internal/codex-lifecycle.js +69 -1
  13. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  14. package/dist/lib/internal/release-workflow.js +1 -3
  15. package/dist/lib/internal/task-activity.js +57 -7
  16. package/dist/lib/internal/task-event.js +57 -4
  17. package/dist/lib/internal/task-ledger.js +59 -5
  18. package/dist/lib/internal/task-lifecycle.js +59 -8
  19. package/dist/lib/internal/task-orchestration.js +48 -8
  20. package/dist/lib/internal/task-override.js +131 -0
  21. package/dist/lib/internal/task-review.js +64 -6
  22. package/dist/lib/internal/task-verify.js +65 -3
  23. package/dist/lib/internal/task-warning.js +57 -4
  24. package/dist/lib/platform/context.js +6 -4
  25. package/dist/lib/platform/github-client.js +55 -2
  26. package/dist/lib/platform/pr-summary.js +5 -3
  27. package/dist/lib/process-data/checkpoint.js +319 -0
  28. package/dist/lib/process-data/index.js +128 -60
  29. package/dist/lib/process-data/materialize.js +84 -0
  30. package/dist/lib/process-data/normalize.js +34 -1
  31. package/dist/lib/process-data/reconcile.js +55 -1
  32. package/dist/lib/process-data/sources.js +422 -1
  33. package/dist/lib/process-data/store.js +341 -11
  34. package/dist/lib/runtime/agent-runtime.js +56 -0
  35. package/dist/lib/sandbox/commands/create.js +394 -264
  36. package/dist/lib/sandbox/commands/rm.js +64 -15
  37. package/dist/lib/sandbox/commands/start.js +4 -3
  38. package/dist/lib/sandbox/control/container-identity.js +64 -0
  39. package/dist/lib/sandbox/control/executor.js +23 -9
  40. package/dist/lib/sandbox/control/lifecycle.js +590 -61
  41. package/dist/lib/sandbox/control/protocol.js +7 -0
  42. package/dist/lib/sandbox/control/server.js +202 -26
  43. package/dist/lib/sandbox/control/state.js +51 -25
  44. package/dist/lib/sandbox/control/timing.js +8 -0
  45. package/dist/lib/sandbox/engine.js +14 -7
  46. package/dist/lib/sandbox/mounts.js +7 -0
  47. package/dist/lib/sandbox/recovery.js +135 -50
  48. package/dist/lib/sandbox/workspace-view.js +59 -20
  49. package/dist/lib/server/daemon.js +1 -1
  50. package/dist/lib/server/process-control.js +11 -1
  51. package/dist/lib/server/process-state.js +107 -20
  52. package/dist/lib/task/activity-intent.js +4 -1
  53. package/dist/lib/task/artifact-lifecycle.js +82 -11
  54. package/dist/lib/task/artifact-receipts.js +128 -0
  55. package/dist/lib/task/codex-orchestration.js +81 -6
  56. package/dist/lib/task/commands/validate.js +1 -1
  57. package/dist/lib/task/create.js +10 -2
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +120 -17
  60. package/dist/lib/task/events.js +148 -7
  61. package/dist/lib/task/guard-override.js +31 -0
  62. package/dist/lib/task/human-override.js +1240 -0
  63. package/dist/lib/task/ledger-intents.js +6 -3
  64. package/dist/lib/task/ledger.js +13 -1
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/orchestration.js +293 -18
  67. package/dist/lib/task/review-finalization.js +3 -2
  68. package/dist/lib/task/sections.js +5 -1
  69. package/dist/lib/task/task-execution-lock.js +5 -4
  70. package/dist/lib/task/verification-engine.js +80 -22
  71. package/dist/lib/task/verification.js +13 -1
  72. package/dist/lib/task/workflow-warning-intents.js +4 -2
  73. package/dist/lib/task/write.js +2 -1
  74. package/dist/lifecycle-build-manifest.json +6 -0
  75. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  76. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
  77. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  78. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  80. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
  82. package/lib/decide.ts +59 -14
  83. package/lib/defaults.json +10 -2
  84. package/lib/git/workflow.ts +25 -2
  85. package/lib/init.ts +1 -1
  86. package/lib/internal/codex-lifecycle.ts +68 -1
  87. package/lib/internal/codex-sandbox-controller.ts +109 -0
  88. package/lib/internal/release-workflow.ts +1 -3
  89. package/lib/internal/task-activity.ts +46 -6
  90. package/lib/internal/task-event.ts +53 -4
  91. package/lib/internal/task-ledger.ts +48 -4
  92. package/lib/internal/task-lifecycle.ts +56 -12
  93. package/lib/internal/task-orchestration.ts +43 -8
  94. package/lib/internal/task-override.ts +138 -0
  95. package/lib/internal/task-review.ts +51 -6
  96. package/lib/internal/task-verify.ts +52 -3
  97. package/lib/internal/task-warning.ts +44 -3
  98. package/lib/platform/context.ts +8 -5
  99. package/lib/platform/github-client.ts +61 -3
  100. package/lib/platform/pr-summary.ts +10 -3
  101. package/lib/process-data/checkpoint.ts +335 -0
  102. package/lib/process-data/index.ts +112 -60
  103. package/lib/process-data/materialize.ts +83 -0
  104. package/lib/process-data/normalize.ts +29 -1
  105. package/lib/process-data/reconcile.ts +64 -2
  106. package/lib/process-data/sources.ts +449 -2
  107. package/lib/process-data/store.ts +304 -11
  108. package/lib/process-data/types.ts +124 -2
  109. package/lib/runtime/agent-runtime.ts +76 -0
  110. package/lib/sandbox/commands/create.ts +139 -6
  111. package/lib/sandbox/commands/rm.ts +64 -14
  112. package/lib/sandbox/commands/start.ts +4 -3
  113. package/lib/sandbox/config.ts +2 -1
  114. package/lib/sandbox/control/container-identity.ts +93 -0
  115. package/lib/sandbox/control/executor.ts +24 -12
  116. package/lib/sandbox/control/lifecycle.ts +685 -67
  117. package/lib/sandbox/control/protocol.ts +35 -6
  118. package/lib/sandbox/control/server.ts +184 -24
  119. package/lib/sandbox/control/state.ts +67 -33
  120. package/lib/sandbox/control/timing.ts +12 -0
  121. package/lib/sandbox/engine.ts +20 -9
  122. package/lib/sandbox/mounts.ts +8 -0
  123. package/lib/sandbox/recovery.ts +159 -49
  124. package/lib/sandbox/workspace-view.ts +73 -22
  125. package/lib/server/daemon.ts +1 -1
  126. package/lib/server/process-control.ts +13 -3
  127. package/lib/server/process-state.ts +129 -27
  128. package/lib/task/activity-intent.ts +4 -2
  129. package/lib/task/artifact-lifecycle.ts +73 -10
  130. package/lib/task/artifact-receipts.ts +161 -0
  131. package/lib/task/codex-orchestration.ts +91 -5
  132. package/lib/task/commands/validate.ts +1 -1
  133. package/lib/task/create.ts +11 -2
  134. package/lib/task/decision-intents.ts +4 -2
  135. package/lib/task/delegation-receipts.ts +205 -25
  136. package/lib/task/events.ts +132 -6
  137. package/lib/task/guard-override.ts +50 -0
  138. package/lib/task/human-override.ts +1503 -0
  139. package/lib/task/ledger-intents.ts +6 -3
  140. package/lib/task/ledger.ts +16 -1
  141. package/lib/task/lifecycle.ts +71 -14
  142. package/lib/task/orchestration.ts +443 -22
  143. package/lib/task/review-finalization.ts +6 -2
  144. package/lib/task/sections.ts +6 -2
  145. package/lib/task/task-execution-lock.ts +11 -9
  146. package/lib/task/verification-engine.ts +93 -32
  147. package/lib/task/verification.ts +15 -1
  148. package/lib/task/workflow-warning-intents.ts +4 -2
  149. package/lib/task/write.ts +4 -1
  150. package/package.json +1 -1
  151. package/templates/.agents/README.en.md +7 -0
  152. package/templates/.agents/README.zh-CN.md +5 -0
  153. package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
  154. package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
  155. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
  156. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  157. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  158. package/templates/.agents/rules/review-handshake.en.md +3 -1
  159. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  160. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  161. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  162. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  163. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  164. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  165. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  166. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  167. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  168. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  169. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  170. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  171. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  172. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  173. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  174. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
  175. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
  176. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  177. package/templates/.agents/workspace/README.en.md +0 -26
  178. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -0,0 +1,480 @@
1
+ import crypto from 'node:crypto';
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import semver from 'semver';
7
+ import * as toml from 'smol-toml';
8
+
9
+ import { requestSandboxControl } from '../../../sandbox/control/client.ts';
10
+ import { readSandboxControlStatus } from '../../../sandbox/control/state.ts';
11
+ import { getProcessStartTime, isProcessAlive } from '../../../server/process-state.ts';
12
+ import { computeLifecycleBuildIdentity } from './build-identity.ts';
13
+ import type { LifecycleBuildIdentity } from './build-identity.ts';
14
+
15
+ type ControllerControl = Readonly<{
16
+ token: string;
17
+ generation: string;
18
+ channelDir: string;
19
+ statusDir: string;
20
+ runtimeDir: string;
21
+ }>;
22
+
23
+ type CodexSandboxControllerContext = Readonly<{
24
+ version: 1;
25
+ taskId: string;
26
+ controlGeneration: string;
27
+ controllerInstanceDigest: string;
28
+ parentPid: number;
29
+ parentStartTime: number;
30
+ issuedAt: number;
31
+ expiresAt: number;
32
+ buildIdentity: LifecycleBuildIdentity;
33
+ hookDefinitionHash: string;
34
+ lifecycleProfilesHash: string;
35
+ }>;
36
+
37
+ type ControllerInput = Readonly<{
38
+ taskId: string;
39
+ taskRef: string;
40
+ executorModel?: string;
41
+ executorReasoningEffort?: string;
42
+ reviewerModel?: string;
43
+ reviewerReasoningEffort?: string;
44
+ }>;
45
+
46
+ type ControllerOptions = Readonly<{
47
+ repoRoot?: string;
48
+ codexHome?: string;
49
+ temporaryRoot?: string;
50
+ control?: ControllerControl;
51
+ now?: () => number;
52
+ codexVersion?: () => string;
53
+ verifyTaskBinding?: (taskId: string, control: ControllerControl) => void;
54
+ environment?: NodeJS.ProcessEnv;
55
+ }>;
56
+
57
+ type PreparedCodexSandboxController = Readonly<{
58
+ command: 'codex';
59
+ args: readonly string[];
60
+ env: NodeJS.ProcessEnv;
61
+ home: string;
62
+ contextPath: string;
63
+ context: CodexSandboxControllerContext;
64
+ cleanup: () => void;
65
+ }>;
66
+
67
+ const CONTEXT_TTL_MS = 4 * 60 * 60 * 1_000;
68
+
69
+ function digestFiles(files: readonly string[]): string {
70
+ const hash = crypto.createHash('sha256');
71
+ for (const file of [...files].sort()) {
72
+ const stat = fs.lstatSync(file);
73
+ if (!stat.isFile() || stat.isSymbolicLink()) {
74
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
75
+ }
76
+ hash.update(path.basename(file));
77
+ hash.update('\0');
78
+ hash.update(fs.readFileSync(file));
79
+ hash.update('\0');
80
+ }
81
+ return hash.digest('hex');
82
+ }
83
+
84
+ function copyRegular(source: string, destination: string, mode: number): void {
85
+ const stat = fs.lstatSync(source);
86
+ if (!stat.isFile() || stat.isSymbolicLink()) {
87
+ throw new Error('CODEX_SANDBOX_CONTROLLER_INPUT_INVALID');
88
+ }
89
+ fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
90
+ fs.copyFileSync(source, destination);
91
+ fs.chmodSync(destination, mode);
92
+ }
93
+
94
+ function controlFromEnvironment(): ControllerControl {
95
+ const token = process.env.AGENT_INFRA_CONTROL_TOKEN;
96
+ const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
97
+ const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
98
+ const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
99
+ const runtimeDir = process.env.AGENT_INFRA_RUNTIME_DIR;
100
+ if (!token || !generation || !channelDir || !statusDir || !runtimeDir) {
101
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
102
+ }
103
+ return { token, generation, channelDir, statusDir, runtimeDir };
104
+ }
105
+
106
+ function verifyRuntime(runtimeDir: string): void {
107
+ let stat: fs.Stats;
108
+ try {
109
+ stat = fs.lstatSync(runtimeDir);
110
+ } catch {
111
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
112
+ }
113
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
114
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
115
+ }
116
+ const probe = path.join(runtimeDir, `.controller-${process.pid}-${crypto.randomUUID()}.probe`);
117
+ try {
118
+ fs.writeFileSync(probe, 'runtime-probe\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 });
119
+ if (fs.readFileSync(probe, 'utf8') !== 'runtime-probe\n') {
120
+ throw new Error('probe mismatch');
121
+ }
122
+ } catch {
123
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
124
+ } finally {
125
+ fs.rmSync(probe, { force: true });
126
+ }
127
+ }
128
+
129
+ function verifyControl(taskId: string, control: ControllerControl): void {
130
+ verifyRuntime(control.runtimeDir);
131
+ let status: ReturnType<typeof readSandboxControlStatus> | null = null;
132
+ let lastError: unknown = null;
133
+ for (let attempt = 0; attempt < 5; attempt += 1) {
134
+ try {
135
+ status = readSandboxControlStatus(control.statusDir);
136
+ break;
137
+ } catch (error) {
138
+ lastError = error;
139
+ if (attempt < 4) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
140
+ }
141
+ }
142
+ if (!status) {
143
+ throw new Error(`CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE: ${String(lastError)}`);
144
+ }
145
+ if (status.generation !== control.generation
146
+ || status.state !== 'healthy'
147
+ || Date.now() - status.updatedAt > 1_500) {
148
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE');
149
+ }
150
+ const response = requestSandboxControl({
151
+ family: 'task-orchestration',
152
+ args: [taskId, 'status'],
153
+ ...control,
154
+ timeoutMs: 30_000
155
+ });
156
+ let payload: { taskId?: unknown } = {};
157
+ try {
158
+ payload = JSON.parse(response.stdout) as { taskId?: unknown };
159
+ } catch {
160
+ throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
161
+ }
162
+ if (payload.taskId !== taskId) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
163
+ }
164
+
165
+ function detectedCodexVersion(): string {
166
+ const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
167
+ const version = /codex-cli\s+(\d+\.\d+\.\d+)/u.exec(result.stdout ?? '')?.[1];
168
+ if (result.status !== 0 || !version) throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNAVAILABLE');
169
+ return version;
170
+ }
171
+
172
+ function sanitizedConfig(source: string, destination: string, sourceHome: string, targetHome: string): readonly string[] {
173
+ if (!fs.existsSync(source)) return [];
174
+ const parsed = toml.parse(fs.readFileSync(source, 'utf8')) as Record<string, unknown>;
175
+ const allowed = new Set([
176
+ 'model',
177
+ 'model_provider',
178
+ 'model_providers',
179
+ 'model_reasoning_effort',
180
+ 'model_auto_compact_token_limit',
181
+ 'preferred_auth_method'
182
+ ]);
183
+ const output = Object.fromEntries(Object.entries(parsed).filter(([key]) => allowed.has(key)));
184
+ const catalog = parsed.model_catalog_json;
185
+ if (typeof catalog === 'string' && catalog.trim()) {
186
+ const resolved = path.resolve(sourceHome, catalog.replace(/^~[/\\]/u, ''));
187
+ const relative = path.relative(sourceHome, resolved);
188
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
189
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONFIG_PATH_INVALID');
190
+ }
191
+ const target = path.join(targetHome, 'model-catalogs', path.basename(resolved));
192
+ copyRegular(resolved, target, 0o600);
193
+ output.model_catalog_json = target;
194
+ }
195
+ fs.writeFileSync(destination, `${toml.stringify(output)}\n`, { mode: 0o600 });
196
+ const providerEnvironment = new Set<string>();
197
+ const providers = output.model_providers;
198
+ if (providers && typeof providers === 'object' && !Array.isArray(providers)) {
199
+ for (const provider of Object.values(providers)) {
200
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider)) continue;
201
+ for (const key of ['env_key', 'envKey']) {
202
+ const name = (provider as Record<string, unknown>)[key];
203
+ if (typeof name === 'string' && /^[A-Z][A-Z0-9_]{1,127}$/u.test(name)) {
204
+ providerEnvironment.add(name);
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return Object.freeze([...providerEnvironment]);
210
+ }
211
+
212
+ function isolatedEnvironment(
213
+ home: string,
214
+ shimDir: string,
215
+ contextPath: string,
216
+ taskId: string,
217
+ control: ControllerControl,
218
+ providerEnvironment: readonly string[],
219
+ sourceEnvironment: NodeJS.ProcessEnv
220
+ ): NodeJS.ProcessEnv {
221
+ const inherited = new Set([
222
+ 'LANG', 'LC_ALL', 'TERM', 'COLORTERM',
223
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
224
+ 'http_proxy', 'https_proxy', 'no_proxy',
225
+ 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
226
+ 'CODEX_CI',
227
+ ...providerEnvironment
228
+ ]);
229
+ const env: NodeJS.ProcessEnv = {};
230
+ for (const key of inherited) {
231
+ if (sourceEnvironment[key] !== undefined) env[key] = sourceEnvironment[key];
232
+ }
233
+ return {
234
+ ...env,
235
+ HOME: home,
236
+ CODEX_HOME: home,
237
+ PATH: `${shimDir}${path.delimiter}${sourceEnvironment.PATH ?? ''}`,
238
+ AGENT_INFRA_TASK_ID: taskId,
239
+ AGENT_INFRA_CONTROL_TOKEN: control.token,
240
+ AGENT_INFRA_CONTROL_GENERATION: control.generation,
241
+ AGENT_INFRA_CONTROL_DIR: control.channelDir,
242
+ AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
243
+ AGENT_INFRA_RUNTIME_DIR: control.runtimeDir,
244
+ AGENT_INFRA_CODEX_CONTROLLER_CONTEXT: contextPath
245
+ };
246
+ }
247
+
248
+ function shellQuote(value: string): string {
249
+ return `'${value.replaceAll("'", "'\\''")}'`;
250
+ }
251
+
252
+ function prepareCodexSandboxController(
253
+ input: ControllerInput,
254
+ options: ControllerOptions = {}
255
+ ): PreparedCodexSandboxController {
256
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
257
+ const sourceHome = path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'));
258
+ const control = options.control ?? controlFromEnvironment();
259
+ (options.verifyTaskBinding ?? verifyControl)(input.taskId, control);
260
+ const version = (options.codexVersion ?? detectedCodexVersion)();
261
+ if (!semver.gte(version, '0.147.0')) throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNSUPPORTED');
262
+
263
+ const runtimeRoot = path.resolve(options.temporaryRoot
264
+ ?? path.join(os.tmpdir(), 'agent-infra-codex-controllers'));
265
+ fs.mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
266
+ fs.chmodSync(runtimeRoot, 0o700);
267
+ const key = crypto.createHash('sha256')
268
+ .update(`${input.taskId}\0${control.generation}`)
269
+ .digest('hex');
270
+ const leasePath = path.join(runtimeRoot, `${key}.lease.json`);
271
+ if (fs.existsSync(leasePath)) {
272
+ const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { pid?: number; startTime?: number };
273
+ if (Number.isSafeInteger(lease.pid) && typeof lease.startTime === 'number'
274
+ && getProcessStartTime(lease.pid!) === lease.startTime) {
275
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUSY');
276
+ }
277
+ fs.unlinkSync(leasePath);
278
+ }
279
+
280
+ const parentStartTime = getProcessStartTime(process.pid);
281
+ if (!parentStartTime) throw new Error('CODEX_SANDBOX_CONTROLLER_PROCESS_IDENTITY_INVALID');
282
+ const instanceDigest = crypto.randomBytes(32).toString('hex');
283
+ fs.writeFileSync(leasePath, `${JSON.stringify({
284
+ version: 1,
285
+ pid: process.pid,
286
+ startTime: parentStartTime,
287
+ taskId: input.taskId,
288
+ generation: control.generation,
289
+ instanceDigest
290
+ })}\n`, { mode: 0o600, flag: 'wx' });
291
+
292
+ const home = fs.mkdtempSync(path.join(runtimeRoot, `${key}-`));
293
+ fs.chmodSync(home, 0o700);
294
+ let cleaned = false;
295
+ const cleanup = () => {
296
+ if (cleaned) return;
297
+ cleaned = true;
298
+ const relative = path.relative(runtimeRoot, home);
299
+ if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
300
+ fs.rmSync(home, { recursive: true, force: true });
301
+ }
302
+ try {
303
+ const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { instanceDigest?: string };
304
+ if (lease.instanceDigest === instanceDigest) fs.unlinkSync(leasePath);
305
+ } catch {
306
+ // A missing or replaced lease is owned by another cleanup path.
307
+ }
308
+ };
309
+
310
+ try {
311
+ const auth = path.join(sourceHome, 'auth.json');
312
+ if (fs.existsSync(auth)) copyRegular(auth, path.join(home, 'auth.json'), 0o600);
313
+ const providerEnvironment = sanitizedConfig(
314
+ path.join(sourceHome, 'config.toml'),
315
+ path.join(home, 'config.toml'),
316
+ sourceHome,
317
+ home
318
+ );
319
+
320
+ const hooks = path.join(repoRoot, '.codex', 'hooks.json');
321
+ const executor = path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-executor.toml');
322
+ const reviewer = path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-reviewer.toml');
323
+ copyRegular(hooks, path.join(home, 'hooks.json'), 0o600);
324
+ copyRegular(executor, path.join(home, 'agents', path.basename(executor)), 0o600);
325
+ copyRegular(reviewer, path.join(home, 'agents', path.basename(reviewer)), 0o600);
326
+ if (crypto.createHash('sha256').update(fs.readFileSync(path.join(home, 'hooks.json'))).digest('hex')
327
+ !== crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex')
328
+ || digestFiles([
329
+ path.join(home, 'agents', path.basename(executor)),
330
+ path.join(home, 'agents', path.basename(reviewer))
331
+ ]) !== digestFiles([executor, reviewer])) {
332
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
333
+ }
334
+
335
+ const buildIdentity = computeLifecycleBuildIdentity(repoRoot);
336
+ const now = (options.now ?? Date.now)();
337
+ const context: CodexSandboxControllerContext = Object.freeze({
338
+ version: 1,
339
+ taskId: input.taskId,
340
+ controlGeneration: control.generation,
341
+ controllerInstanceDigest: instanceDigest,
342
+ parentPid: process.pid,
343
+ parentStartTime,
344
+ issuedAt: now,
345
+ expiresAt: now + CONTEXT_TTL_MS,
346
+ buildIdentity,
347
+ hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex'),
348
+ lifecycleProfilesHash: digestFiles([executor, reviewer])
349
+ });
350
+ const contextPath = path.join(home, 'controller-context.json');
351
+ fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
352
+
353
+ const shimDir = path.join(home, 'bin');
354
+ fs.mkdirSync(shimDir, { mode: 0o700 });
355
+ const internalCli = path.resolve(process.argv[1] ?? path.join(repoRoot, 'bin', 'internal-cli.ts'));
356
+ const source = internalCli.endsWith('.ts')
357
+ ? `#!/bin/sh\nexec ${shellQuote(process.execPath)} --experimental-strip-types ${shellQuote(internalCli)} "$@"\n`
358
+ : `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(internalCli)} "$@"\n`;
359
+ fs.writeFileSync(path.join(shimDir, 'agent-infra-internal'), source, { mode: 0o700 });
360
+
361
+ const policy = [
362
+ input.executorModel ? `--executor-model ${input.executorModel}` : '',
363
+ input.executorReasoningEffort ? `--executor-reasoning-effort ${input.executorReasoningEffort}` : '',
364
+ input.reviewerModel ? `--reviewer-model ${input.reviewerModel}` : '',
365
+ input.reviewerReasoningEffort ? `--reviewer-reasoning-effort ${input.reviewerReasoningEffort}` : ''
366
+ ].filter(Boolean).join(' ');
367
+ const prompt = `$run-task ${input.taskRef}${policy ? ` ${policy}` : ''}`;
368
+ const args = Object.freeze([
369
+ 'exec',
370
+ '--enable', 'hooks',
371
+ '--enable', 'multi_agent',
372
+ '--dangerously-bypass-hook-trust',
373
+ '--dangerously-bypass-approvals-and-sandbox',
374
+ '--json',
375
+ '-C', repoRoot,
376
+ prompt
377
+ ]);
378
+ const env = isolatedEnvironment(
379
+ home,
380
+ shimDir,
381
+ contextPath,
382
+ input.taskId,
383
+ control,
384
+ providerEnvironment,
385
+ options.environment ?? process.env
386
+ );
387
+ return Object.freeze({
388
+ command: 'codex' as const,
389
+ args,
390
+ env,
391
+ home,
392
+ contextPath,
393
+ context,
394
+ cleanup
395
+ });
396
+ } catch (error) {
397
+ cleanup();
398
+ throw error;
399
+ }
400
+ }
401
+
402
+ function verifyCodexSandboxControllerContext(
403
+ contextPath: string,
404
+ taskId: string,
405
+ options: Readonly<{ repoRoot?: string; now?: number; generation?: string }> = {}
406
+ ): CodexSandboxControllerContext {
407
+ const stat = fs.lstatSync(contextPath);
408
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
409
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
410
+ }
411
+ const context = JSON.parse(fs.readFileSync(contextPath, 'utf8')) as CodexSandboxControllerContext;
412
+ const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
413
+ if (context.version !== 1
414
+ || context.taskId !== taskId
415
+ || context.controlGeneration !== generation
416
+ || context.expiresAt < (options.now ?? Date.now())
417
+ || getProcessStartTime(context.parentPid) !== context.parentStartTime
418
+ || !isProcessAlive(context.parentPid)) {
419
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
420
+ }
421
+ const current = computeLifecycleBuildIdentity(options.repoRoot ?? process.cwd());
422
+ if (JSON.stringify(current) !== JSON.stringify(context.buildIdentity)) {
423
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
424
+ }
425
+ return Object.freeze(context);
426
+ }
427
+
428
+ async function runCodexSandboxController(
429
+ input: ControllerInput,
430
+ options: ControllerOptions = {}
431
+ ): Promise<number> {
432
+ const prepared = prepareCodexSandboxController(input, options);
433
+ try {
434
+ const child = spawn(prepared.command, [...prepared.args], {
435
+ cwd: options.repoRoot ?? process.cwd(),
436
+ env: prepared.env,
437
+ stdio: 'inherit',
438
+ detached: process.platform !== 'win32'
439
+ });
440
+ let forceKill: NodeJS.Timeout | undefined;
441
+ const terminate = () => {
442
+ if (child.pid && process.platform !== 'win32') {
443
+ try { process.kill(-child.pid, 'SIGTERM'); } catch { child.kill('SIGTERM'); }
444
+ } else child.kill('SIGTERM');
445
+ forceKill = setTimeout(() => {
446
+ if (child.pid && process.platform !== 'win32') {
447
+ try { process.kill(-child.pid, 'SIGKILL'); } catch { child.kill('SIGKILL'); }
448
+ } else child.kill('SIGKILL');
449
+ }, 5_000);
450
+ forceKill.unref();
451
+ };
452
+ process.once('SIGINT', terminate);
453
+ process.once('SIGTERM', terminate);
454
+ try {
455
+ return await new Promise<number>((resolve, reject) => {
456
+ child.once('error', reject);
457
+ child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
458
+ });
459
+ } finally {
460
+ if (forceKill) clearTimeout(forceKill);
461
+ process.off('SIGINT', terminate);
462
+ process.off('SIGTERM', terminate);
463
+ }
464
+ } finally {
465
+ prepared.cleanup();
466
+ }
467
+ }
468
+
469
+ export {
470
+ prepareCodexSandboxController,
471
+ runCodexSandboxController,
472
+ verifyCodexSandboxControllerContext
473
+ };
474
+ export type {
475
+ CodexSandboxControllerContext,
476
+ ControllerControl,
477
+ ControllerInput,
478
+ ControllerOptions,
479
+ PreparedCodexSandboxController
480
+ };
@@ -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;
@@ -18,6 +19,7 @@ type StoredCodexLifecycle = Readonly<{
18
19
  state: CodexLifecycleState;
19
20
  consumer: string | null;
20
21
  consumedAt: string | null;
22
+ spawnObservedAt: string | null;
21
23
  updatedAt: string;
22
24
  }>;
23
25
 
@@ -33,6 +35,11 @@ type CodexLifecycleStoreResult = Readonly<{
33
35
  state: CodexLifecycleState;
34
36
  }>;
35
37
 
38
+ type ActiveCodexLifecycleEvidenceQuery = Readonly<{
39
+ nativeAgent: string;
40
+ hookDefinitionHash: string;
41
+ }>;
42
+
36
43
  const LOCK_RETRY_MS = 10;
37
44
  const LOCK_TIMEOUT_MS = 1_000;
38
45
  const LOCK_STALE_MS = 30_000;
@@ -49,10 +56,14 @@ function readRecord(file: string): StoredCodexLifecycle {
49
56
  || value.revision < 1
50
57
  || !value.state
51
58
  || value.state.schemaVersion !== 1
59
+ || (value.spawnObservedAt != null && (
60
+ typeof value.spawnObservedAt !== 'string'
61
+ || !Number.isFinite(Date.parse(value.spawnObservedAt))
62
+ ))
52
63
  ) {
53
64
  throw new Error(`Codex lifecycle record '${path.basename(file)}' is invalid`);
54
65
  }
55
- return value;
66
+ return Object.freeze({ ...value, spawnObservedAt: value.spawnObservedAt ?? null });
56
67
  }
57
68
 
58
69
  function recordFiles(root: string): string[] {
@@ -62,6 +73,19 @@ function recordFiles(root: string): string[] {
62
73
  .map((name) => path.join(root, name));
63
74
  }
64
75
 
76
+ function hasActiveCodexLifecycleEvidence(
77
+ root: string,
78
+ query: ActiveCodexLifecycleEvidenceQuery
79
+ ): boolean {
80
+ return recordFiles(root).map(readRecord).some((record) => {
81
+ const evidence = record.state.startEvidence;
82
+ return record.consumer === null
83
+ && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status)
84
+ && evidence?.nativeAgent === query.nativeAgent
85
+ && evidence.hookDefinitionHash === query.hookDefinitionHash;
86
+ });
87
+ }
88
+
65
89
  function writeRecord(file: string, record: StoredCodexLifecycle, expectedRevision: number): void {
66
90
  const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
67
91
  fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, {
@@ -82,7 +106,7 @@ function writeRecord(file: string, record: StoredCodexLifecycle, expectedRevisio
82
106
  }
83
107
 
84
108
  function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
85
- const root = options.root ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-lifecycle');
109
+ const root = options.root ?? resolveAgentRuntimeStoreRoot({ store: 'lifecycle' });
86
110
  const now = options.now ?? (() => new Date().toISOString());
87
111
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
88
112
  fs.chmodSync(root, 0o700);
@@ -154,6 +178,7 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
154
178
  function apply(event: CodexLifecycleEvent): CodexLifecycleStoreResult {
155
179
  return withWriteLock(() => {
156
180
  const file = locate(event);
181
+ const observedAt = now();
157
182
  const current = fs.existsSync(file)
158
183
  ? readRecord(file)
159
184
  : Object.freeze({
@@ -162,7 +187,8 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
162
187
  state: createCodexLifecycleState(options.cliVersion),
163
188
  consumer: null,
164
189
  consumedAt: null,
165
- updatedAt: now()
190
+ spawnObservedAt: null,
191
+ updatedAt: observedAt
166
192
  });
167
193
  if (current.consumer) throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
168
194
  const nextState = reduceCodexLifecycleEvent(current.state, event);
@@ -172,7 +198,10 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
172
198
  state: nextState,
173
199
  consumer: null,
174
200
  consumedAt: null,
175
- updatedAt: now()
201
+ spawnObservedAt: event.type === 'hook-spawn' && current.revision === 0
202
+ ? observedAt
203
+ : current.spawnObservedAt,
204
+ updatedAt: observedAt
176
205
  });
177
206
  writeRecord(file, next, current.revision);
178
207
  return Object.freeze({ path: file, revision: next.revision, state: next.state });
@@ -290,8 +319,9 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
290
319
  return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
291
320
  }
292
321
 
293
- export { createCodexLifecycleStore };
322
+ export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
294
323
  export type {
324
+ ActiveCodexLifecycleEvidenceQuery,
295
325
  CodexLifecycleStoreOptions,
296
326
  CodexLifecycleStoreResult,
297
327
  StoredCodexLifecycle