@fitlab-ai/agent-infra 0.9.5 → 0.9.7

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 (172) hide show
  1. package/dist/bin/internal-cli.js +8 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
  8. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -4
  11. package/dist/lib/internal/codex-lifecycle.js +169 -3
  12. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  13. package/dist/lib/internal/release-workflow.js +108 -11
  14. package/dist/lib/internal/sandbox-control.js +43 -6
  15. package/dist/lib/internal/task-create.js +12 -4
  16. package/dist/lib/internal/task-orchestration.js +40 -7
  17. package/dist/lib/platform/context.js +6 -4
  18. package/dist/lib/platform/github-client.js +55 -2
  19. package/dist/lib/platform/issue-comments.js +2 -1
  20. package/dist/lib/process-data/checkpoint.js +319 -0
  21. package/dist/lib/process-data/index.js +128 -60
  22. package/dist/lib/process-data/materialize.js +84 -0
  23. package/dist/lib/process-data/normalize.js +34 -1
  24. package/dist/lib/process-data/reconcile.js +55 -1
  25. package/dist/lib/process-data/sources.js +422 -1
  26. package/dist/lib/process-data/store.js +341 -11
  27. package/dist/lib/release/workflow.js +29 -21
  28. package/dist/lib/sandbox/commands/create.js +111 -60
  29. package/dist/lib/sandbox/commands/prune.js +40 -16
  30. package/dist/lib/sandbox/commands/rm.js +372 -94
  31. package/dist/lib/sandbox/commands/start.js +4 -3
  32. package/dist/lib/sandbox/control/client.js +138 -22
  33. package/dist/lib/sandbox/control/container-identity.js +64 -0
  34. package/dist/lib/sandbox/control/executor.js +142 -0
  35. package/dist/lib/sandbox/control/lifecycle.js +557 -0
  36. package/dist/lib/sandbox/control/protocol.js +69 -26
  37. package/dist/lib/sandbox/control/server.js +395 -99
  38. package/dist/lib/sandbox/control/state.js +191 -0
  39. package/dist/lib/sandbox/control/timing.js +8 -0
  40. package/dist/lib/sandbox/index.js +2 -2
  41. package/dist/lib/sandbox/managed-fs.js +14 -2
  42. package/dist/lib/sandbox/mounts.js +22 -19
  43. package/dist/lib/sandbox/recovery.js +284 -50
  44. package/dist/lib/sandbox/workspace-view.js +90 -10
  45. package/dist/lib/sandbox/worktree-safety.js +196 -0
  46. package/dist/lib/server/daemon.js +1 -1
  47. package/dist/lib/server/process-control.js +17 -1
  48. package/dist/lib/server/process-state.js +57 -20
  49. package/dist/lib/task/artifact-lifecycle.js +87 -15
  50. package/dist/lib/task/artifact-receipts.js +128 -0
  51. package/dist/lib/task/codex-orchestration.js +236 -0
  52. package/dist/lib/task/commands/status.js +3 -0
  53. package/dist/lib/task/commands/validate.js +289 -0
  54. package/dist/lib/task/commit-finalization.js +36 -9
  55. package/dist/lib/task/create.js +10 -2
  56. package/dist/lib/task/delegation-receipts.js +109 -2
  57. package/dist/lib/task/events.js +149 -6
  58. package/dist/lib/task/finalization-artifacts.js +2 -1
  59. package/dist/lib/task/implementation-inputs.js +1 -7
  60. package/dist/lib/task/index.js +9 -0
  61. package/dist/lib/task/ledger.js +13 -1
  62. package/dist/lib/task/orchestration.js +365 -15
  63. package/dist/lib/task/sections.js +5 -1
  64. package/dist/lib/task/task-execution-lock.js +5 -4
  65. package/dist/lib/task/verification-engine.js +78 -14
  66. package/dist/lib/task/verification.js +1 -0
  67. package/dist/lifecycle-build-manifest.json +6 -0
  68. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
  69. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  70. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  71. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  72. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
  75. package/lib/agent-clients/adapters/codex.ts +1 -1
  76. package/lib/defaults.json +10 -2
  77. package/lib/git/workflow.ts +25 -3
  78. package/lib/internal/codex-lifecycle.ts +175 -4
  79. package/lib/internal/codex-sandbox-controller.ts +109 -0
  80. package/lib/internal/release-workflow.ts +107 -11
  81. package/lib/internal/sandbox-control.ts +38 -6
  82. package/lib/internal/task-create.ts +16 -4
  83. package/lib/internal/task-orchestration.ts +34 -7
  84. package/lib/platform/context.ts +8 -5
  85. package/lib/platform/github-client.ts +61 -3
  86. package/lib/platform/issue-comments.ts +2 -1
  87. package/lib/process-data/checkpoint.ts +335 -0
  88. package/lib/process-data/index.ts +112 -60
  89. package/lib/process-data/materialize.ts +83 -0
  90. package/lib/process-data/normalize.ts +29 -1
  91. package/lib/process-data/reconcile.ts +64 -2
  92. package/lib/process-data/sources.ts +449 -2
  93. package/lib/process-data/store.ts +304 -11
  94. package/lib/process-data/types.ts +124 -2
  95. package/lib/release/workflow.ts +69 -20
  96. package/lib/sandbox/commands/create.ts +70 -25
  97. package/lib/sandbox/commands/prune.ts +53 -15
  98. package/lib/sandbox/commands/rm.ts +423 -108
  99. package/lib/sandbox/commands/start.ts +4 -3
  100. package/lib/sandbox/control/client.ts +152 -28
  101. package/lib/sandbox/control/container-identity.ts +93 -0
  102. package/lib/sandbox/control/executor.ts +167 -0
  103. package/lib/sandbox/control/lifecycle.ts +573 -0
  104. package/lib/sandbox/control/protocol.ts +121 -68
  105. package/lib/sandbox/control/server.ts +398 -109
  106. package/lib/sandbox/control/state.ts +213 -0
  107. package/lib/sandbox/control/timing.ts +12 -0
  108. package/lib/sandbox/index.ts +2 -2
  109. package/lib/sandbox/managed-fs.ts +17 -2
  110. package/lib/sandbox/mounts.ts +16 -8
  111. package/lib/sandbox/recovery.ts +335 -65
  112. package/lib/sandbox/workspace-view.ts +102 -11
  113. package/lib/sandbox/worktree-safety.ts +234 -0
  114. package/lib/server/daemon.ts +1 -1
  115. package/lib/server/process-control.ts +24 -3
  116. package/lib/server/process-state.ts +82 -27
  117. package/lib/task/artifact-lifecycle.ts +78 -14
  118. package/lib/task/artifact-receipts.ts +161 -0
  119. package/lib/task/codex-orchestration.ts +319 -0
  120. package/lib/task/commands/status.ts +2 -0
  121. package/lib/task/commands/validate.ts +282 -0
  122. package/lib/task/commit-finalization.ts +52 -9
  123. package/lib/task/create.ts +11 -2
  124. package/lib/task/delegation-receipts.ts +208 -7
  125. package/lib/task/events.ts +131 -5
  126. package/lib/task/finalization-artifacts.ts +2 -1
  127. package/lib/task/implementation-inputs.ts +1 -5
  128. package/lib/task/index.ts +8 -0
  129. package/lib/task/ledger.ts +16 -1
  130. package/lib/task/orchestration.ts +552 -18
  131. package/lib/task/sections.ts +6 -2
  132. package/lib/task/task-execution-lock.ts +11 -9
  133. package/lib/task/verification-engine.ts +93 -24
  134. package/lib/task/verification.ts +2 -1
  135. package/package.json +1 -1
  136. package/templates/.agents/README.en.md +7 -0
  137. package/templates/.agents/README.zh-CN.md +5 -0
  138. package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
  139. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  140. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  141. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  142. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  143. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  144. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  145. package/templates/.agents/rules/review-handshake.en.md +3 -1
  146. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  147. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  148. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  149. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  150. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  151. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  152. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  153. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  154. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  155. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
  156. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
  157. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  158. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  159. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
  160. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  163. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
  164. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
  165. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  166. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  167. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  168. package/templates/.codex/hooks.json +8 -8
  169. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  170. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
  171. package/templates/.agents/workspace/README.en.md +0 -26
  172. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -0,0 +1,453 @@
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
+ }>;
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;
34
+ }>;
35
+
36
+ type ControllerInput = Readonly<{
37
+ taskId: string;
38
+ taskRef: string;
39
+ executorModel?: string;
40
+ executorReasoningEffort?: string;
41
+ reviewerModel?: string;
42
+ reviewerReasoningEffort?: string;
43
+ }>;
44
+
45
+ type ControllerOptions = Readonly<{
46
+ repoRoot?: string;
47
+ codexHome?: string;
48
+ temporaryRoot?: string;
49
+ control?: ControllerControl;
50
+ now?: () => number;
51
+ codexVersion?: () => string;
52
+ verifyTaskBinding?: (taskId: string, control: ControllerControl) => void;
53
+ environment?: NodeJS.ProcessEnv;
54
+ }>;
55
+
56
+ type PreparedCodexSandboxController = Readonly<{
57
+ command: 'codex';
58
+ args: readonly string[];
59
+ env: NodeJS.ProcessEnv;
60
+ home: string;
61
+ contextPath: string;
62
+ context: CodexSandboxControllerContext;
63
+ cleanup: () => void;
64
+ }>;
65
+
66
+ const CONTEXT_TTL_MS = 4 * 60 * 60 * 1_000;
67
+
68
+ function digestFiles(files: readonly string[]): string {
69
+ const hash = crypto.createHash('sha256');
70
+ for (const file of [...files].sort()) {
71
+ const stat = fs.lstatSync(file);
72
+ if (!stat.isFile() || stat.isSymbolicLink()) {
73
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
74
+ }
75
+ hash.update(path.basename(file));
76
+ hash.update('\0');
77
+ hash.update(fs.readFileSync(file));
78
+ hash.update('\0');
79
+ }
80
+ return hash.digest('hex');
81
+ }
82
+
83
+ function copyRegular(source: string, destination: string, mode: number): void {
84
+ const stat = fs.lstatSync(source);
85
+ if (!stat.isFile() || stat.isSymbolicLink()) {
86
+ throw new Error('CODEX_SANDBOX_CONTROLLER_INPUT_INVALID');
87
+ }
88
+ fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
89
+ fs.copyFileSync(source, destination);
90
+ fs.chmodSync(destination, mode);
91
+ }
92
+
93
+ function controlFromEnvironment(): ControllerControl {
94
+ const token = process.env.AGENT_INFRA_CONTROL_TOKEN;
95
+ const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
96
+ const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
97
+ const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
98
+ if (!token || !generation || !channelDir || !statusDir) {
99
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
100
+ }
101
+ return { token, generation, channelDir, statusDir };
102
+ }
103
+
104
+ function verifyControl(taskId: string, control: ControllerControl): void {
105
+ let status: ReturnType<typeof readSandboxControlStatus> | null = null;
106
+ let lastError: unknown = null;
107
+ for (let attempt = 0; attempt < 5; attempt += 1) {
108
+ try {
109
+ status = readSandboxControlStatus(control.statusDir);
110
+ break;
111
+ } catch (error) {
112
+ lastError = error;
113
+ if (attempt < 4) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
114
+ }
115
+ }
116
+ if (!status) {
117
+ throw new Error(`CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE: ${String(lastError)}`);
118
+ }
119
+ if (status.generation !== control.generation
120
+ || status.state !== 'healthy'
121
+ || Date.now() - status.updatedAt > 1_500) {
122
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE');
123
+ }
124
+ const response = requestSandboxControl({
125
+ family: 'task-orchestration',
126
+ args: [taskId, 'status'],
127
+ ...control,
128
+ timeoutMs: 30_000
129
+ });
130
+ let payload: { taskId?: unknown } = {};
131
+ try {
132
+ payload = JSON.parse(response.stdout) as { taskId?: unknown };
133
+ } catch {
134
+ throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
135
+ }
136
+ if (payload.taskId !== taskId) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
137
+ }
138
+
139
+ function detectedCodexVersion(): string {
140
+ const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
141
+ const version = /codex-cli\s+(\d+\.\d+\.\d+)/u.exec(result.stdout ?? '')?.[1];
142
+ if (result.status !== 0 || !version) throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNAVAILABLE');
143
+ return version;
144
+ }
145
+
146
+ function sanitizedConfig(source: string, destination: string, sourceHome: string, targetHome: string): readonly string[] {
147
+ if (!fs.existsSync(source)) return [];
148
+ const parsed = toml.parse(fs.readFileSync(source, 'utf8')) as Record<string, unknown>;
149
+ const allowed = new Set([
150
+ 'model',
151
+ 'model_provider',
152
+ 'model_providers',
153
+ 'model_reasoning_effort',
154
+ 'model_auto_compact_token_limit',
155
+ 'preferred_auth_method'
156
+ ]);
157
+ const output = Object.fromEntries(Object.entries(parsed).filter(([key]) => allowed.has(key)));
158
+ const catalog = parsed.model_catalog_json;
159
+ if (typeof catalog === 'string' && catalog.trim()) {
160
+ const resolved = path.resolve(sourceHome, catalog.replace(/^~[/\\]/u, ''));
161
+ const relative = path.relative(sourceHome, resolved);
162
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
163
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONFIG_PATH_INVALID');
164
+ }
165
+ const target = path.join(targetHome, 'model-catalogs', path.basename(resolved));
166
+ copyRegular(resolved, target, 0o600);
167
+ output.model_catalog_json = target;
168
+ }
169
+ fs.writeFileSync(destination, `${toml.stringify(output)}\n`, { mode: 0o600 });
170
+ const providerEnvironment = new Set<string>();
171
+ const providers = output.model_providers;
172
+ if (providers && typeof providers === 'object' && !Array.isArray(providers)) {
173
+ for (const provider of Object.values(providers)) {
174
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider)) continue;
175
+ for (const key of ['env_key', 'envKey']) {
176
+ const name = (provider as Record<string, unknown>)[key];
177
+ if (typeof name === 'string' && /^[A-Z][A-Z0-9_]{1,127}$/u.test(name)) {
178
+ providerEnvironment.add(name);
179
+ }
180
+ }
181
+ }
182
+ }
183
+ return Object.freeze([...providerEnvironment]);
184
+ }
185
+
186
+ function isolatedEnvironment(
187
+ home: string,
188
+ shimDir: string,
189
+ contextPath: string,
190
+ taskId: string,
191
+ control: ControllerControl,
192
+ providerEnvironment: readonly string[],
193
+ sourceEnvironment: NodeJS.ProcessEnv
194
+ ): NodeJS.ProcessEnv {
195
+ const inherited = new Set([
196
+ 'LANG', 'LC_ALL', 'TERM', 'COLORTERM',
197
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
198
+ 'http_proxy', 'https_proxy', 'no_proxy',
199
+ 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
200
+ 'CODEX_CI',
201
+ ...providerEnvironment
202
+ ]);
203
+ const env: NodeJS.ProcessEnv = {};
204
+ for (const key of inherited) {
205
+ if (sourceEnvironment[key] !== undefined) env[key] = sourceEnvironment[key];
206
+ }
207
+ return {
208
+ ...env,
209
+ HOME: home,
210
+ CODEX_HOME: home,
211
+ PATH: `${shimDir}${path.delimiter}${sourceEnvironment.PATH ?? ''}`,
212
+ AGENT_INFRA_TASK_ID: taskId,
213
+ AGENT_INFRA_CONTROL_TOKEN: control.token,
214
+ AGENT_INFRA_CONTROL_GENERATION: control.generation,
215
+ AGENT_INFRA_CONTROL_DIR: control.channelDir,
216
+ AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
217
+ AGENT_INFRA_CODEX_CONTROLLER_CONTEXT: contextPath
218
+ };
219
+ }
220
+
221
+ function shellQuote(value: string): string {
222
+ return `'${value.replaceAll("'", "'\\''")}'`;
223
+ }
224
+
225
+ function prepareCodexSandboxController(
226
+ input: ControllerInput,
227
+ options: ControllerOptions = {}
228
+ ): PreparedCodexSandboxController {
229
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
230
+ const sourceHome = path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'));
231
+ const control = options.control ?? controlFromEnvironment();
232
+ (options.verifyTaskBinding ?? verifyControl)(input.taskId, control);
233
+ const version = (options.codexVersion ?? detectedCodexVersion)();
234
+ if (!semver.gte(version, '0.147.0')) throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNSUPPORTED');
235
+
236
+ const runtimeRoot = path.resolve(options.temporaryRoot
237
+ ?? path.join(os.tmpdir(), 'agent-infra-codex-controllers'));
238
+ fs.mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
239
+ fs.chmodSync(runtimeRoot, 0o700);
240
+ const key = crypto.createHash('sha256')
241
+ .update(`${input.taskId}\0${control.generation}`)
242
+ .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
+ const parentStartTime = getProcessStartTime(process.pid);
254
+ 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
+ const home = fs.mkdtempSync(path.join(runtimeRoot, `${key}-`));
266
+ fs.chmodSync(home, 0o700);
267
+ let cleaned = false;
268
+ const cleanup = () => {
269
+ if (cleaned) return;
270
+ cleaned = true;
271
+ const relative = path.relative(runtimeRoot, home);
272
+ if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
273
+ fs.rmSync(home, { recursive: true, force: true });
274
+ }
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
+ };
282
+
283
+ try {
284
+ const auth = path.join(sourceHome, 'auth.json');
285
+ if (fs.existsSync(auth)) copyRegular(auth, path.join(home, 'auth.json'), 0o600);
286
+ const providerEnvironment = sanitizedConfig(
287
+ path.join(sourceHome, 'config.toml'),
288
+ path.join(home, 'config.toml'),
289
+ sourceHome,
290
+ home
291
+ );
292
+
293
+ const hooks = path.join(repoRoot, '.codex', 'hooks.json');
294
+ const executor = path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-executor.toml');
295
+ const reviewer = path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-reviewer.toml');
296
+ copyRegular(hooks, path.join(home, 'hooks.json'), 0o600);
297
+ copyRegular(executor, path.join(home, 'agents', path.basename(executor)), 0o600);
298
+ copyRegular(reviewer, path.join(home, 'agents', path.basename(reviewer)), 0o600);
299
+ if (crypto.createHash('sha256').update(fs.readFileSync(path.join(home, 'hooks.json'))).digest('hex')
300
+ !== crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex')
301
+ || digestFiles([
302
+ path.join(home, 'agents', path.basename(executor)),
303
+ path.join(home, 'agents', path.basename(reviewer))
304
+ ]) !== digestFiles([executor, reviewer])) {
305
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
306
+ }
307
+
308
+ 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,
320
+ hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex'),
321
+ lifecycleProfilesHash: digestFiles([executor, reviewer])
322
+ });
323
+ const contextPath = path.join(home, 'controller-context.json');
324
+ fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
325
+
326
+ const shimDir = path.join(home, 'bin');
327
+ fs.mkdirSync(shimDir, { mode: 0o700 });
328
+ const internalCli = path.resolve(process.argv[1] ?? path.join(repoRoot, 'bin', 'internal-cli.ts'));
329
+ const source = internalCli.endsWith('.ts')
330
+ ? `#!/bin/sh\nexec ${shellQuote(process.execPath)} --experimental-strip-types ${shellQuote(internalCli)} "$@"\n`
331
+ : `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(internalCli)} "$@"\n`;
332
+ fs.writeFileSync(path.join(shimDir, 'agent-infra-internal'), source, { mode: 0o700 });
333
+
334
+ const policy = [
335
+ input.executorModel ? `--executor-model ${input.executorModel}` : '',
336
+ input.executorReasoningEffort ? `--executor-reasoning-effort ${input.executorReasoningEffort}` : '',
337
+ input.reviewerModel ? `--reviewer-model ${input.reviewerModel}` : '',
338
+ input.reviewerReasoningEffort ? `--reviewer-reasoning-effort ${input.reviewerReasoningEffort}` : ''
339
+ ].filter(Boolean).join(' ');
340
+ const prompt = `$run-task ${input.taskRef}${policy ? ` ${policy}` : ''}`;
341
+ const args = Object.freeze([
342
+ 'exec',
343
+ '--enable', 'hooks',
344
+ '--enable', 'multi_agent',
345
+ '--dangerously-bypass-hook-trust',
346
+ '--dangerously-bypass-approvals-and-sandbox',
347
+ '--json',
348
+ '-C', repoRoot,
349
+ prompt
350
+ ]);
351
+ const env = isolatedEnvironment(
352
+ home,
353
+ shimDir,
354
+ contextPath,
355
+ input.taskId,
356
+ control,
357
+ providerEnvironment,
358
+ options.environment ?? process.env
359
+ );
360
+ return Object.freeze({
361
+ command: 'codex' as const,
362
+ args,
363
+ env,
364
+ home,
365
+ contextPath,
366
+ context,
367
+ cleanup
368
+ });
369
+ } catch (error) {
370
+ cleanup();
371
+ throw error;
372
+ }
373
+ }
374
+
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
+ async function runCodexSandboxController(
402
+ input: ControllerInput,
403
+ options: ControllerOptions = {}
404
+ ): Promise<number> {
405
+ const prepared = prepareCodexSandboxController(input, options);
406
+ try {
407
+ const child = spawn(prepared.command, [...prepared.args], {
408
+ cwd: options.repoRoot ?? process.cwd(),
409
+ env: prepared.env,
410
+ stdio: 'inherit',
411
+ detached: process.platform !== 'win32'
412
+ });
413
+ let forceKill: NodeJS.Timeout | undefined;
414
+ const terminate = () => {
415
+ if (child.pid && process.platform !== 'win32') {
416
+ try { process.kill(-child.pid, 'SIGTERM'); } catch { child.kill('SIGTERM'); }
417
+ } else child.kill('SIGTERM');
418
+ forceKill = setTimeout(() => {
419
+ if (child.pid && process.platform !== 'win32') {
420
+ try { process.kill(-child.pid, 'SIGKILL'); } catch { child.kill('SIGKILL'); }
421
+ } else child.kill('SIGKILL');
422
+ }, 5_000);
423
+ forceKill.unref();
424
+ };
425
+ process.once('SIGINT', terminate);
426
+ process.once('SIGTERM', terminate);
427
+ try {
428
+ return await new Promise<number>((resolve, reject) => {
429
+ child.once('error', reject);
430
+ child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
431
+ });
432
+ } finally {
433
+ if (forceKill) clearTimeout(forceKill);
434
+ process.off('SIGINT', terminate);
435
+ process.off('SIGTERM', terminate);
436
+ }
437
+ } finally {
438
+ prepared.cleanup();
439
+ }
440
+ }
441
+
442
+ export {
443
+ prepareCodexSandboxController,
444
+ runCodexSandboxController,
445
+ verifyCodexSandboxControllerContext
446
+ };
447
+ export type {
448
+ CodexSandboxControllerContext,
449
+ ControllerControl,
450
+ ControllerInput,
451
+ ControllerOptions,
452
+ PreparedCodexSandboxController
453
+ };
@@ -18,6 +18,7 @@ type StoredCodexLifecycle = Readonly<{
18
18
  state: CodexLifecycleState;
19
19
  consumer: string | null;
20
20
  consumedAt: string | null;
21
+ spawnObservedAt: string | null;
21
22
  updatedAt: string;
22
23
  }>;
23
24
 
@@ -33,6 +34,11 @@ type CodexLifecycleStoreResult = Readonly<{
33
34
  state: CodexLifecycleState;
34
35
  }>;
35
36
 
37
+ type ActiveCodexLifecycleEvidenceQuery = Readonly<{
38
+ nativeAgent: string;
39
+ hookDefinitionHash: string;
40
+ }>;
41
+
36
42
  const LOCK_RETRY_MS = 10;
37
43
  const LOCK_TIMEOUT_MS = 1_000;
38
44
  const LOCK_STALE_MS = 30_000;
@@ -49,10 +55,14 @@ function readRecord(file: string): StoredCodexLifecycle {
49
55
  || value.revision < 1
50
56
  || !value.state
51
57
  || value.state.schemaVersion !== 1
58
+ || (value.spawnObservedAt != null && (
59
+ typeof value.spawnObservedAt !== 'string'
60
+ || !Number.isFinite(Date.parse(value.spawnObservedAt))
61
+ ))
52
62
  ) {
53
63
  throw new Error(`Codex lifecycle record '${path.basename(file)}' is invalid`);
54
64
  }
55
- return value;
65
+ return Object.freeze({ ...value, spawnObservedAt: value.spawnObservedAt ?? null });
56
66
  }
57
67
 
58
68
  function recordFiles(root: string): string[] {
@@ -62,6 +72,19 @@ function recordFiles(root: string): string[] {
62
72
  .map((name) => path.join(root, name));
63
73
  }
64
74
 
75
+ function hasActiveCodexLifecycleEvidence(
76
+ root: string,
77
+ query: ActiveCodexLifecycleEvidenceQuery
78
+ ): boolean {
79
+ return recordFiles(root).map(readRecord).some((record) => {
80
+ const evidence = record.state.startEvidence;
81
+ return record.consumer === null
82
+ && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status)
83
+ && evidence?.nativeAgent === query.nativeAgent
84
+ && evidence.hookDefinitionHash === query.hookDefinitionHash;
85
+ });
86
+ }
87
+
65
88
  function writeRecord(file: string, record: StoredCodexLifecycle, expectedRevision: number): void {
66
89
  const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
67
90
  fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, {
@@ -131,7 +154,7 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
131
154
  if (event.type === 'hook-child') {
132
155
  const matches = recordFiles(root).filter((file) => {
133
156
  const state = readRecord(file).state;
134
- return state.spawn?.sessionId === event.sessionId
157
+ return state.spawn?.sessionId === event.parentThreadId
135
158
  && state.spawn.nativeAgent === event.nativeAgent
136
159
  && (!state.child || state.child.childThreadId === event.childThreadId);
137
160
  });
@@ -154,6 +177,7 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
154
177
  function apply(event: CodexLifecycleEvent): CodexLifecycleStoreResult {
155
178
  return withWriteLock(() => {
156
179
  const file = locate(event);
180
+ const observedAt = now();
157
181
  const current = fs.existsSync(file)
158
182
  ? readRecord(file)
159
183
  : Object.freeze({
@@ -162,7 +186,8 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
162
186
  state: createCodexLifecycleState(options.cliVersion),
163
187
  consumer: null,
164
188
  consumedAt: null,
165
- updatedAt: now()
189
+ spawnObservedAt: null,
190
+ updatedAt: observedAt
166
191
  });
167
192
  if (current.consumer) throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
168
193
  const nextState = reduceCodexLifecycleEvent(current.state, event);
@@ -172,6 +197,35 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
172
197
  state: nextState,
173
198
  consumer: null,
174
199
  consumedAt: null,
200
+ spawnObservedAt: event.type === 'hook-spawn' && current.revision === 0
201
+ ? observedAt
202
+ : current.spawnObservedAt,
203
+ updatedAt: observedAt
204
+ });
205
+ writeRecord(file, next, current.revision);
206
+ return Object.freeze({ path: file, revision: next.revision, state: next.state });
207
+ });
208
+ }
209
+
210
+ function applyToSpawn(
211
+ identity: Readonly<{ sessionId: string; turnId: string; toolUseId: string }>,
212
+ event: Extract<CodexLifecycleEvent, { type: 'hook-child' }>
213
+ ): CodexLifecycleStoreResult {
214
+ return withWriteLock(() => {
215
+ const file = path.join(root, `${digest(`${identity.sessionId}\0${identity.turnId}\0${identity.toolUseId}`)}.json`);
216
+ if (!fs.existsSync(file)) throw new Error('Codex lifecycle spawn identity was not found');
217
+ const current = readRecord(file);
218
+ if (current.consumer) throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
219
+ if (
220
+ current.state.spawn?.sessionId !== identity.sessionId
221
+ || current.state.spawn.turnId !== identity.turnId
222
+ || current.state.spawn.toolUseId !== identity.toolUseId
223
+ ) throw new Error('Codex lifecycle spawn identity does not match the stored event');
224
+ const nextState = reduceCodexLifecycleEvent(current.state, event);
225
+ const next = Object.freeze({
226
+ ...current,
227
+ revision: current.revision + 1,
228
+ state: nextState,
175
229
  updatedAt: now()
176
230
  });
177
231
  writeRecord(file, next, current.revision);
@@ -179,6 +233,19 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
179
233
  });
180
234
  }
181
235
 
236
+ function findByParent(parentThreadId: string): readonly StoredCodexLifecycle[] {
237
+ const matches = recordFiles(root)
238
+ .map(readRecord)
239
+ .filter((record) => record.state.startEvidence?.parentThreadId === parentThreadId
240
+ && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status));
241
+ const unconsumed = matches.filter((record) => !record.consumer);
242
+ if (unconsumed.length > 1) throw new Error(`Codex lifecycle parent '${parentThreadId}' has ambiguous active children`);
243
+ if (unconsumed.length === 1) return Object.freeze(unconsumed);
244
+ return Object.freeze(matches
245
+ .filter((record) => record.consumer && record.state.status === 'stop-ready')
246
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)));
247
+ }
248
+
182
249
  function read(childThreadId: string): StoredCodexLifecycle {
183
250
  const matches = findByChild(childThreadId);
184
251
  if (matches.length !== 1) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
@@ -196,7 +263,16 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
196
263
  if (matches.length !== 1) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
197
264
  const file = matches[0]!;
198
265
  const current = readRecord(file);
199
- if (current.consumer) throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
266
+ if (current.consumer) {
267
+ if (current.consumer !== consumer) {
268
+ throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
269
+ }
270
+ if (
271
+ expectedHookDefinitionHash
272
+ && current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash
273
+ ) throw new Error('Codex lifecycle hook definition hash is stale');
274
+ return current;
275
+ }
200
276
  if (current.state.status !== 'stop-ready') throw new Error('Codex lifecycle evidence is not stop-ready');
201
277
  if (
202
278
  expectedHookDefinitionHash
@@ -239,11 +315,12 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
239
315
  });
240
316
  }
241
317
 
242
- return Object.freeze({ root, apply, consume, expireBefore, read });
318
+ return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
243
319
  }
244
320
 
245
- export { createCodexLifecycleStore };
321
+ export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
246
322
  export type {
323
+ ActiveCodexLifecycleEvidenceQuery,
247
324
  CodexLifecycleStoreOptions,
248
325
  CodexLifecycleStoreResult,
249
326
  StoredCodexLifecycle
@@ -15,7 +15,7 @@ const codexAdapter = defineAgentClientAdapter({
15
15
  commands: { level: 'integrated' },
16
16
  hooks: { level: 'integrated' },
17
17
  subagents: { level: 'experimental' },
18
- orchestration: { level: 'unsupported' },
18
+ orchestration: { level: 'experimental' },
19
19
  sandbox: { level: 'integrated' },
20
20
  verification: { level: 'compatible' }
21
21
  },
package/lib/defaults.json CHANGED
@@ -56,7 +56,6 @@
56
56
  ".agents/skills/",
57
57
  ".agents/templates/",
58
58
  ".agents/workflows/",
59
- ".agents/workspace/README.md",
60
59
  ".git-hooks/check-large-files.cjs",
61
60
  ".git-hooks/check-version-format.sh",
62
61
  ".github/scripts/",
@@ -84,6 +83,15 @@
84
83
  ".git-hooks/pre-commit",
85
84
  ".gitignore"
86
85
  ],
87
- "ejected": []
86
+ "ejected": [],
87
+ "retiredManaged": [
88
+ {
89
+ "path": ".agents/workspace/README.md",
90
+ "templateHashes": [
91
+ "sha256:967b27ca4008154c5abf99a1df728242f80ed3ace9bd98eb2828bc1c76af826c",
92
+ "sha256:15aa21942cf2a1396d27834f1d3d723b61a46f8d14600b706f39f3bebc21aea3"
93
+ ]
94
+ }
95
+ ]
88
96
  }
89
97
  }