@fitlab-ai/agent-infra 0.9.6 → 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 (127) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
  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/sandbox-controller.js +370 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
  7. package/dist/lib/defaults.json +10 -2
  8. package/dist/lib/git/workflow.js +30 -3
  9. package/dist/lib/internal/codex-lifecycle.js +69 -1
  10. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  11. package/dist/lib/internal/task-orchestration.js +37 -5
  12. package/dist/lib/platform/context.js +6 -4
  13. package/dist/lib/platform/github-client.js +55 -2
  14. package/dist/lib/process-data/checkpoint.js +319 -0
  15. package/dist/lib/process-data/index.js +128 -60
  16. package/dist/lib/process-data/materialize.js +84 -0
  17. package/dist/lib/process-data/normalize.js +34 -1
  18. package/dist/lib/process-data/reconcile.js +55 -1
  19. package/dist/lib/process-data/sources.js +422 -1
  20. package/dist/lib/process-data/store.js +341 -11
  21. package/dist/lib/sandbox/commands/create.js +103 -51
  22. package/dist/lib/sandbox/commands/rm.js +64 -15
  23. package/dist/lib/sandbox/commands/start.js +4 -3
  24. package/dist/lib/sandbox/control/container-identity.js +64 -0
  25. package/dist/lib/sandbox/control/executor.js +13 -7
  26. package/dist/lib/sandbox/control/lifecycle.js +238 -29
  27. package/dist/lib/sandbox/control/protocol.js +7 -0
  28. package/dist/lib/sandbox/control/server.js +202 -26
  29. package/dist/lib/sandbox/control/state.js +25 -15
  30. package/dist/lib/sandbox/control/timing.js +8 -0
  31. package/dist/lib/sandbox/mounts.js +5 -13
  32. package/dist/lib/sandbox/recovery.js +59 -42
  33. package/dist/lib/sandbox/workspace-view.js +57 -40
  34. package/dist/lib/server/daemon.js +1 -1
  35. package/dist/lib/server/process-control.js +11 -1
  36. package/dist/lib/server/process-state.js +57 -20
  37. package/dist/lib/task/artifact-lifecycle.js +82 -11
  38. package/dist/lib/task/artifact-receipts.js +128 -0
  39. package/dist/lib/task/codex-orchestration.js +81 -6
  40. package/dist/lib/task/commands/validate.js +1 -1
  41. package/dist/lib/task/create.js +10 -2
  42. package/dist/lib/task/delegation-receipts.js +84 -2
  43. package/dist/lib/task/events.js +139 -2
  44. package/dist/lib/task/ledger.js +13 -1
  45. package/dist/lib/task/orchestration.js +235 -14
  46. package/dist/lib/task/sections.js +5 -1
  47. package/dist/lib/task/task-execution-lock.js +5 -4
  48. package/dist/lib/task/verification-engine.js +40 -8
  49. package/dist/lifecycle-build-manifest.json +6 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
  51. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  52. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  53. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
  56. package/lib/defaults.json +10 -2
  57. package/lib/git/workflow.ts +25 -2
  58. package/lib/internal/codex-lifecycle.ts +68 -1
  59. package/lib/internal/codex-sandbox-controller.ts +109 -0
  60. package/lib/internal/task-orchestration.ts +31 -4
  61. package/lib/platform/context.ts +8 -5
  62. package/lib/platform/github-client.ts +61 -3
  63. package/lib/process-data/checkpoint.ts +335 -0
  64. package/lib/process-data/index.ts +112 -60
  65. package/lib/process-data/materialize.ts +83 -0
  66. package/lib/process-data/normalize.ts +29 -1
  67. package/lib/process-data/reconcile.ts +64 -2
  68. package/lib/process-data/sources.ts +449 -2
  69. package/lib/process-data/store.ts +304 -11
  70. package/lib/process-data/types.ts +124 -2
  71. package/lib/sandbox/commands/create.ts +56 -7
  72. package/lib/sandbox/commands/rm.ts +64 -14
  73. package/lib/sandbox/commands/start.ts +4 -3
  74. package/lib/sandbox/control/container-identity.ts +93 -0
  75. package/lib/sandbox/control/executor.ts +14 -10
  76. package/lib/sandbox/control/lifecycle.ts +264 -33
  77. package/lib/sandbox/control/protocol.ts +31 -5
  78. package/lib/sandbox/control/server.ts +184 -24
  79. package/lib/sandbox/control/state.ts +34 -17
  80. package/lib/sandbox/control/timing.ts +12 -0
  81. package/lib/sandbox/mounts.ts +5 -15
  82. package/lib/sandbox/recovery.ts +71 -42
  83. package/lib/sandbox/workspace-view.ts +61 -40
  84. package/lib/server/daemon.ts +1 -1
  85. package/lib/server/process-control.ts +13 -3
  86. package/lib/server/process-state.ts +82 -27
  87. package/lib/task/artifact-lifecycle.ts +73 -10
  88. package/lib/task/artifact-receipts.ts +161 -0
  89. package/lib/task/codex-orchestration.ts +91 -5
  90. package/lib/task/commands/validate.ts +1 -1
  91. package/lib/task/create.ts +11 -2
  92. package/lib/task/delegation-receipts.ts +160 -8
  93. package/lib/task/events.ts +122 -1
  94. package/lib/task/ledger.ts +16 -1
  95. package/lib/task/orchestration.ts +356 -17
  96. package/lib/task/sections.ts +6 -2
  97. package/lib/task/task-execution-lock.ts +11 -9
  98. package/lib/task/verification-engine.ts +51 -16
  99. package/package.json +1 -1
  100. package/templates/.agents/README.en.md +7 -0
  101. package/templates/.agents/README.zh-CN.md +5 -0
  102. package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  104. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  105. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  106. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  107. package/templates/.agents/rules/review-handshake.en.md +3 -1
  108. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  109. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  110. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  113. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  114. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  115. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  116. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  117. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  118. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  119. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  121. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  122. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  123. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
  124. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
  125. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  126. package/templates/.agents/workspace/README.en.md +0 -26
  127. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -0,0 +1,370 @@
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
+ import { requestSandboxControl } from '../../../sandbox/control/client.js';
9
+ import { readSandboxControlStatus } from '../../../sandbox/control/state.js';
10
+ import { getProcessStartTime, isProcessAlive } from '../../../server/process-state.js';
11
+ import { computeLifecycleBuildIdentity } from './build-identity.js';
12
+ const CONTEXT_TTL_MS = 4 * 60 * 60 * 1_000;
13
+ function digestFiles(files) {
14
+ const hash = crypto.createHash('sha256');
15
+ for (const file of [...files].sort()) {
16
+ const stat = fs.lstatSync(file);
17
+ if (!stat.isFile() || stat.isSymbolicLink()) {
18
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
19
+ }
20
+ hash.update(path.basename(file));
21
+ hash.update('\0');
22
+ hash.update(fs.readFileSync(file));
23
+ hash.update('\0');
24
+ }
25
+ return hash.digest('hex');
26
+ }
27
+ function copyRegular(source, destination, mode) {
28
+ const stat = fs.lstatSync(source);
29
+ if (!stat.isFile() || stat.isSymbolicLink()) {
30
+ throw new Error('CODEX_SANDBOX_CONTROLLER_INPUT_INVALID');
31
+ }
32
+ fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
33
+ fs.copyFileSync(source, destination);
34
+ fs.chmodSync(destination, mode);
35
+ }
36
+ function controlFromEnvironment() {
37
+ const token = process.env.AGENT_INFRA_CONTROL_TOKEN;
38
+ const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
39
+ const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
40
+ const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
41
+ if (!token || !generation || !channelDir || !statusDir) {
42
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
43
+ }
44
+ return { token, generation, channelDir, statusDir };
45
+ }
46
+ function verifyControl(taskId, control) {
47
+ let status = null;
48
+ let lastError = null;
49
+ for (let attempt = 0; attempt < 5; attempt += 1) {
50
+ try {
51
+ status = readSandboxControlStatus(control.statusDir);
52
+ break;
53
+ }
54
+ catch (error) {
55
+ lastError = error;
56
+ if (attempt < 4)
57
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
58
+ }
59
+ }
60
+ if (!status) {
61
+ throw new Error(`CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE: ${String(lastError)}`);
62
+ }
63
+ if (status.generation !== control.generation
64
+ || status.state !== 'healthy'
65
+ || Date.now() - status.updatedAt > 1_500) {
66
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE');
67
+ }
68
+ const response = requestSandboxControl({
69
+ family: 'task-orchestration',
70
+ args: [taskId, 'status'],
71
+ ...control,
72
+ timeoutMs: 30_000
73
+ });
74
+ let payload = {};
75
+ try {
76
+ payload = JSON.parse(response.stdout);
77
+ }
78
+ catch {
79
+ throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
80
+ }
81
+ if (payload.taskId !== taskId)
82
+ throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
83
+ }
84
+ function detectedCodexVersion() {
85
+ const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
86
+ const version = /codex-cli\s+(\d+\.\d+\.\d+)/u.exec(result.stdout ?? '')?.[1];
87
+ if (result.status !== 0 || !version)
88
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNAVAILABLE');
89
+ return version;
90
+ }
91
+ function sanitizedConfig(source, destination, sourceHome, targetHome) {
92
+ if (!fs.existsSync(source))
93
+ return [];
94
+ const parsed = toml.parse(fs.readFileSync(source, 'utf8'));
95
+ const allowed = new Set([
96
+ 'model',
97
+ 'model_provider',
98
+ 'model_providers',
99
+ 'model_reasoning_effort',
100
+ 'model_auto_compact_token_limit',
101
+ 'preferred_auth_method'
102
+ ]);
103
+ const output = Object.fromEntries(Object.entries(parsed).filter(([key]) => allowed.has(key)));
104
+ const catalog = parsed.model_catalog_json;
105
+ if (typeof catalog === 'string' && catalog.trim()) {
106
+ const resolved = path.resolve(sourceHome, catalog.replace(/^~[/\\]/u, ''));
107
+ const relative = path.relative(sourceHome, resolved);
108
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
109
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONFIG_PATH_INVALID');
110
+ }
111
+ const target = path.join(targetHome, 'model-catalogs', path.basename(resolved));
112
+ copyRegular(resolved, target, 0o600);
113
+ output.model_catalog_json = target;
114
+ }
115
+ fs.writeFileSync(destination, `${toml.stringify(output)}\n`, { mode: 0o600 });
116
+ const providerEnvironment = new Set();
117
+ const providers = output.model_providers;
118
+ if (providers && typeof providers === 'object' && !Array.isArray(providers)) {
119
+ for (const provider of Object.values(providers)) {
120
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider))
121
+ continue;
122
+ for (const key of ['env_key', 'envKey']) {
123
+ const name = provider[key];
124
+ if (typeof name === 'string' && /^[A-Z][A-Z0-9_]{1,127}$/u.test(name)) {
125
+ providerEnvironment.add(name);
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return Object.freeze([...providerEnvironment]);
131
+ }
132
+ function isolatedEnvironment(home, shimDir, contextPath, taskId, control, providerEnvironment, sourceEnvironment) {
133
+ const inherited = new Set([
134
+ 'LANG', 'LC_ALL', 'TERM', 'COLORTERM',
135
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
136
+ 'http_proxy', 'https_proxy', 'no_proxy',
137
+ 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
138
+ 'CODEX_CI',
139
+ ...providerEnvironment
140
+ ]);
141
+ const env = {};
142
+ for (const key of inherited) {
143
+ if (sourceEnvironment[key] !== undefined)
144
+ env[key] = sourceEnvironment[key];
145
+ }
146
+ return {
147
+ ...env,
148
+ HOME: home,
149
+ CODEX_HOME: home,
150
+ PATH: `${shimDir}${path.delimiter}${sourceEnvironment.PATH ?? ''}`,
151
+ AGENT_INFRA_TASK_ID: taskId,
152
+ AGENT_INFRA_CONTROL_TOKEN: control.token,
153
+ AGENT_INFRA_CONTROL_GENERATION: control.generation,
154
+ AGENT_INFRA_CONTROL_DIR: control.channelDir,
155
+ AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
156
+ AGENT_INFRA_CODEX_CONTROLLER_CONTEXT: contextPath
157
+ };
158
+ }
159
+ function shellQuote(value) {
160
+ return `'${value.replaceAll("'", "'\\''")}'`;
161
+ }
162
+ function prepareCodexSandboxController(input, options = {}) {
163
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
164
+ const sourceHome = path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'));
165
+ const control = options.control ?? controlFromEnvironment();
166
+ (options.verifyTaskBinding ?? verifyControl)(input.taskId, control);
167
+ const version = (options.codexVersion ?? detectedCodexVersion)();
168
+ if (!semver.gte(version, '0.147.0'))
169
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNSUPPORTED');
170
+ const runtimeRoot = path.resolve(options.temporaryRoot
171
+ ?? path.join(os.tmpdir(), 'agent-infra-codex-controllers'));
172
+ fs.mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
173
+ fs.chmodSync(runtimeRoot, 0o700);
174
+ const key = crypto.createHash('sha256')
175
+ .update(`${input.taskId}\0${control.generation}`)
176
+ .digest('hex');
177
+ const leasePath = path.join(runtimeRoot, `${key}.lease.json`);
178
+ if (fs.existsSync(leasePath)) {
179
+ const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
180
+ if (Number.isSafeInteger(lease.pid) && typeof lease.startTime === 'number'
181
+ && getProcessStartTime(lease.pid) === lease.startTime) {
182
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUSY');
183
+ }
184
+ fs.unlinkSync(leasePath);
185
+ }
186
+ const parentStartTime = getProcessStartTime(process.pid);
187
+ if (!parentStartTime)
188
+ throw new Error('CODEX_SANDBOX_CONTROLLER_PROCESS_IDENTITY_INVALID');
189
+ const instanceDigest = crypto.randomBytes(32).toString('hex');
190
+ fs.writeFileSync(leasePath, `${JSON.stringify({
191
+ version: 1,
192
+ pid: process.pid,
193
+ startTime: parentStartTime,
194
+ taskId: input.taskId,
195
+ generation: control.generation,
196
+ instanceDigest
197
+ })}\n`, { mode: 0o600, flag: 'wx' });
198
+ const home = fs.mkdtempSync(path.join(runtimeRoot, `${key}-`));
199
+ fs.chmodSync(home, 0o700);
200
+ let cleaned = false;
201
+ const cleanup = () => {
202
+ if (cleaned)
203
+ return;
204
+ cleaned = true;
205
+ const relative = path.relative(runtimeRoot, home);
206
+ if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
207
+ fs.rmSync(home, { recursive: true, force: true });
208
+ }
209
+ try {
210
+ const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
211
+ if (lease.instanceDigest === instanceDigest)
212
+ fs.unlinkSync(leasePath);
213
+ }
214
+ catch {
215
+ // A missing or replaced lease is owned by another cleanup path.
216
+ }
217
+ };
218
+ try {
219
+ const auth = path.join(sourceHome, 'auth.json');
220
+ if (fs.existsSync(auth))
221
+ copyRegular(auth, path.join(home, 'auth.json'), 0o600);
222
+ const providerEnvironment = sanitizedConfig(path.join(sourceHome, 'config.toml'), path.join(home, 'config.toml'), sourceHome, home);
223
+ const hooks = path.join(repoRoot, '.codex', 'hooks.json');
224
+ const executor = path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-executor.toml');
225
+ const reviewer = path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-reviewer.toml');
226
+ copyRegular(hooks, path.join(home, 'hooks.json'), 0o600);
227
+ copyRegular(executor, path.join(home, 'agents', path.basename(executor)), 0o600);
228
+ copyRegular(reviewer, path.join(home, 'agents', path.basename(reviewer)), 0o600);
229
+ if (crypto.createHash('sha256').update(fs.readFileSync(path.join(home, 'hooks.json'))).digest('hex')
230
+ !== crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex')
231
+ || digestFiles([
232
+ path.join(home, 'agents', path.basename(executor)),
233
+ path.join(home, 'agents', path.basename(reviewer))
234
+ ]) !== digestFiles([executor, reviewer])) {
235
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
236
+ }
237
+ const buildIdentity = computeLifecycleBuildIdentity(repoRoot);
238
+ const now = (options.now ?? Date.now)();
239
+ const context = Object.freeze({
240
+ version: 1,
241
+ taskId: input.taskId,
242
+ controlGeneration: control.generation,
243
+ controllerInstanceDigest: instanceDigest,
244
+ parentPid: process.pid,
245
+ parentStartTime,
246
+ issuedAt: now,
247
+ expiresAt: now + CONTEXT_TTL_MS,
248
+ buildIdentity,
249
+ hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex'),
250
+ lifecycleProfilesHash: digestFiles([executor, reviewer])
251
+ });
252
+ const contextPath = path.join(home, 'controller-context.json');
253
+ fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
254
+ const shimDir = path.join(home, 'bin');
255
+ fs.mkdirSync(shimDir, { mode: 0o700 });
256
+ const internalCli = path.resolve(process.argv[1] ?? path.join(repoRoot, 'bin', 'internal-cli.ts'));
257
+ const source = internalCli.endsWith('.ts')
258
+ ? `#!/bin/sh\nexec ${shellQuote(process.execPath)} --experimental-strip-types ${shellQuote(internalCli)} "$@"\n`
259
+ : `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(internalCli)} "$@"\n`;
260
+ fs.writeFileSync(path.join(shimDir, 'agent-infra-internal'), source, { mode: 0o700 });
261
+ const policy = [
262
+ input.executorModel ? `--executor-model ${input.executorModel}` : '',
263
+ input.executorReasoningEffort ? `--executor-reasoning-effort ${input.executorReasoningEffort}` : '',
264
+ input.reviewerModel ? `--reviewer-model ${input.reviewerModel}` : '',
265
+ input.reviewerReasoningEffort ? `--reviewer-reasoning-effort ${input.reviewerReasoningEffort}` : ''
266
+ ].filter(Boolean).join(' ');
267
+ const prompt = `$run-task ${input.taskRef}${policy ? ` ${policy}` : ''}`;
268
+ const args = Object.freeze([
269
+ 'exec',
270
+ '--enable', 'hooks',
271
+ '--enable', 'multi_agent',
272
+ '--dangerously-bypass-hook-trust',
273
+ '--dangerously-bypass-approvals-and-sandbox',
274
+ '--json',
275
+ '-C', repoRoot,
276
+ prompt
277
+ ]);
278
+ const env = isolatedEnvironment(home, shimDir, contextPath, input.taskId, control, providerEnvironment, options.environment ?? process.env);
279
+ return Object.freeze({
280
+ command: 'codex',
281
+ args,
282
+ env,
283
+ home,
284
+ contextPath,
285
+ context,
286
+ cleanup
287
+ });
288
+ }
289
+ catch (error) {
290
+ cleanup();
291
+ throw error;
292
+ }
293
+ }
294
+ function verifyCodexSandboxControllerContext(contextPath, taskId, options = {}) {
295
+ const stat = fs.lstatSync(contextPath);
296
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
297
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
298
+ }
299
+ const context = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
300
+ const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
301
+ if (context.version !== 1
302
+ || context.taskId !== taskId
303
+ || context.controlGeneration !== generation
304
+ || context.expiresAt < (options.now ?? Date.now())
305
+ || getProcessStartTime(context.parentPid) !== context.parentStartTime
306
+ || !isProcessAlive(context.parentPid)) {
307
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
308
+ }
309
+ const current = computeLifecycleBuildIdentity(options.repoRoot ?? process.cwd());
310
+ if (JSON.stringify(current) !== JSON.stringify(context.buildIdentity)) {
311
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
312
+ }
313
+ return Object.freeze(context);
314
+ }
315
+ async function runCodexSandboxController(input, options = {}) {
316
+ const prepared = prepareCodexSandboxController(input, options);
317
+ try {
318
+ const child = spawn(prepared.command, [...prepared.args], {
319
+ cwd: options.repoRoot ?? process.cwd(),
320
+ env: prepared.env,
321
+ stdio: 'inherit',
322
+ detached: process.platform !== 'win32'
323
+ });
324
+ let forceKill;
325
+ const terminate = () => {
326
+ if (child.pid && process.platform !== 'win32') {
327
+ try {
328
+ process.kill(-child.pid, 'SIGTERM');
329
+ }
330
+ catch {
331
+ child.kill('SIGTERM');
332
+ }
333
+ }
334
+ else
335
+ child.kill('SIGTERM');
336
+ forceKill = setTimeout(() => {
337
+ if (child.pid && process.platform !== 'win32') {
338
+ try {
339
+ process.kill(-child.pid, 'SIGKILL');
340
+ }
341
+ catch {
342
+ child.kill('SIGKILL');
343
+ }
344
+ }
345
+ else
346
+ child.kill('SIGKILL');
347
+ }, 5_000);
348
+ forceKill.unref();
349
+ };
350
+ process.once('SIGINT', terminate);
351
+ process.once('SIGTERM', terminate);
352
+ try {
353
+ return await new Promise((resolve, reject) => {
354
+ child.once('error', reject);
355
+ child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
356
+ });
357
+ }
358
+ finally {
359
+ if (forceKill)
360
+ clearTimeout(forceKill);
361
+ process.off('SIGINT', terminate);
362
+ process.off('SIGTERM', terminate);
363
+ }
364
+ }
365
+ finally {
366
+ prepared.cleanup();
367
+ }
368
+ }
369
+ export { prepareCodexSandboxController, runCodexSandboxController, verifyCodexSandboxControllerContext };
370
+ //# sourceMappingURL=sandbox-controller.js.map
@@ -14,10 +14,12 @@ function readRecord(file) {
14
14
  || !Number.isSafeInteger(value.revision)
15
15
  || value.revision < 1
16
16
  || !value.state
17
- || value.state.schemaVersion !== 1) {
17
+ || value.state.schemaVersion !== 1
18
+ || (value.spawnObservedAt != null && (typeof value.spawnObservedAt !== 'string'
19
+ || !Number.isFinite(Date.parse(value.spawnObservedAt))))) {
18
20
  throw new Error(`Codex lifecycle record '${path.basename(file)}' is invalid`);
19
21
  }
20
- return value;
22
+ return Object.freeze({ ...value, spawnObservedAt: value.spawnObservedAt ?? null });
21
23
  }
22
24
  function recordFiles(root) {
23
25
  if (!fs.existsSync(root))
@@ -26,6 +28,15 @@ function recordFiles(root) {
26
28
  .filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
27
29
  .map((name) => path.join(root, name));
28
30
  }
31
+ function hasActiveCodexLifecycleEvidence(root, query) {
32
+ return recordFiles(root).map(readRecord).some((record) => {
33
+ const evidence = record.state.startEvidence;
34
+ return record.consumer === null
35
+ && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status)
36
+ && evidence?.nativeAgent === query.nativeAgent
37
+ && evidence.hookDefinitionHash === query.hookDefinitionHash;
38
+ });
39
+ }
29
40
  function writeRecord(file, record, expectedRevision) {
30
41
  const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
31
42
  fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, {
@@ -121,6 +132,7 @@ function createCodexLifecycleStore(options) {
121
132
  function apply(event) {
122
133
  return withWriteLock(() => {
123
134
  const file = locate(event);
135
+ const observedAt = now();
124
136
  const current = fs.existsSync(file)
125
137
  ? readRecord(file)
126
138
  : Object.freeze({
@@ -129,7 +141,8 @@ function createCodexLifecycleStore(options) {
129
141
  state: createCodexLifecycleState(options.cliVersion),
130
142
  consumer: null,
131
143
  consumedAt: null,
132
- updatedAt: now()
144
+ spawnObservedAt: null,
145
+ updatedAt: observedAt
133
146
  });
134
147
  if (current.consumer)
135
148
  throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
@@ -140,7 +153,10 @@ function createCodexLifecycleStore(options) {
140
153
  state: nextState,
141
154
  consumer: null,
142
155
  consumedAt: null,
143
- updatedAt: now()
156
+ spawnObservedAt: event.type === 'hook-spawn' && current.revision === 0
157
+ ? observedAt
158
+ : current.spawnObservedAt,
159
+ updatedAt: observedAt
144
160
  });
145
161
  writeRecord(file, next, current.revision);
146
162
  return Object.freeze({ path: file, revision: next.revision, state: next.state });
@@ -251,5 +267,5 @@ function createCodexLifecycleStore(options) {
251
267
  }
252
268
  return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
253
269
  }
254
- export { createCodexLifecycleStore };
270
+ export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
255
271
  //# sourceMappingURL=store.js.map
@@ -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
  }
@@ -12,6 +12,9 @@ function value(runner, cwd, args, trim = true) {
12
12
  const result = run(runner, cwd, args);
13
13
  return result.status === 0 ? (trim ? result.stdout.trim() : result.stdout.replace(/\n$/, '')) : null;
14
14
  }
15
+ function nulPaths(output) {
16
+ return output.split('\0').filter(Boolean);
17
+ }
15
18
  function cleanMessage(message) {
16
19
  return message.replace(/(https?:\/\/)[^\s/@:]+(?::[^\s/@]*)?@/gi, '$1***@').trim();
17
20
  }
@@ -64,9 +67,33 @@ function commitExplicitPaths(input, runner = defaultRunner) {
64
67
  const unrelatedStaged = before.snapshot.staged.filter((candidate) => !selected.has(candidate));
65
68
  if (unrelatedStaged.length > 0)
66
69
  return { status: 'failed', changed: false, snapshot: before.snapshot, operations: [], error: { code: 'GIT_STAGED_SCOPE_MISMATCH', message: `Unrelated staged paths: ${unrelatedStaged.join(', ')}` } };
67
- const add = run(runner, input.cwd, ['add', '--', ...input.paths]);
68
- if (add.status !== 0)
69
- return { status: 'failed', changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'failed', message: add.stderr.trim() }], error: { code: 'GIT_STAGE_FAILED', message: add.stderr.trim() } };
70
+ const stageFailure = (message) => ({ status: 'failed', changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'failed', message }], error: { code: 'GIT_STAGE_FAILED', message } });
71
+ const indexed = run(runner, input.cwd, ['ls-files', '-z', '--', ...input.paths]);
72
+ if (indexed.status !== 0)
73
+ return stageFailure(indexed.stderr.trim());
74
+ const headed = run(runner, input.cwd, ['ls-tree', '-r', '--name-only', '-z', 'HEAD', '--', ...input.paths]);
75
+ if (headed.status !== 0)
76
+ return stageFailure(headed.stderr.trim());
77
+ const indexedPaths = new Set(nulPaths(indexed.stdout));
78
+ const headPaths = new Set(nulPaths(headed.stdout));
79
+ const trackedPaths = [];
80
+ const addPaths = [];
81
+ for (const candidate of input.paths) {
82
+ if (indexedPaths.has(candidate))
83
+ trackedPaths.push(candidate);
84
+ else if (!headPaths.has(candidate) || fs.existsSync(path.join(input.cwd, candidate)))
85
+ addPaths.push(candidate);
86
+ }
87
+ if (trackedPaths.length > 0) {
88
+ const update = run(runner, input.cwd, ['add', '-u', '--', ...trackedPaths]);
89
+ if (update.status !== 0)
90
+ return stageFailure(update.stderr.trim());
91
+ }
92
+ if (addPaths.length > 0) {
93
+ const add = run(runner, input.cwd, ['add', '--', ...addPaths]);
94
+ if (add.status !== 0)
95
+ return stageFailure(add.stderr.trim());
96
+ }
70
97
  const staged = run(runner, input.cwd, ['diff', '--cached', '--quiet', '--']);
71
98
  if (staged.status === 0)
72
99
  return { status: 'no-op', changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'no-op' }], error: null };
@@ -4,8 +4,12 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread } from '../agent-clients/adapters/codex-lifecycle/app-server.js';
6
6
  import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.js';
7
+ import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.js';
8
+ import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
9
+ import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
10
+ import { resolveTaskRef } from '../task/resolve-ref.js';
7
11
  import { activateCodexOrchestrationDelegation, activateCodexSpawnDelegation, reconcileCodexOrchestrationDelegation, sealCodexOrchestrationDelegation, sealCodexParentDelegation } from '../task/codex-orchestration.js';
8
- const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
12
+ const USAGE = 'Usage: agent-infra-internal codex-lifecycle <capability-arm|hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
9
13
  const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
10
14
  function output(value) {
11
15
  process.stdout.write(`${JSON.stringify(value)}\n`);
@@ -34,6 +38,7 @@ function parse(args) {
34
38
  return null;
35
39
  }
36
40
  const allowed = {
41
+ 'capability-arm': ['--task-id'],
37
42
  'hook-event': ['--event', '--bridge'],
38
43
  'resolve-start': ['--child-id'],
39
44
  'resolve-stop': ['--child-id'],
@@ -71,6 +76,16 @@ function hookDefinitionHash() {
71
76
  const file = path.join(process.cwd(), '.codex', 'hooks.json');
72
77
  return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
73
78
  }
79
+ function controllerBinding(taskId) {
80
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
81
+ if (!contextPath)
82
+ return undefined;
83
+ const context = verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot: process.cwd() });
84
+ return {
85
+ instanceDigest: context.controllerInstanceDigest,
86
+ controlGeneration: context.controlGeneration
87
+ };
88
+ }
74
89
  async function readStdin() {
75
90
  let input = '';
76
91
  process.stdin.setEncoding('utf8');
@@ -156,6 +171,29 @@ async function codexLifecycle(args = []) {
156
171
  output({ status: 'ready', changed: false, evidence: result, diagnostics: result.diagnostics, error: null });
157
172
  return;
158
173
  }
174
+ if (parsed.operation === 'capability-arm') {
175
+ const taskId = parsed.values['--task-id'];
176
+ if (!taskId)
177
+ throw new Error('capability-arm requires --task-id');
178
+ const resolved = resolveTaskRef(taskId, { repoRoot: process.cwd() });
179
+ if (!resolved.ok)
180
+ throw new Error(`${resolved.code}: ${resolved.message}`);
181
+ const armed = createCodexCapabilityStore().arm({
182
+ taskId: resolved.taskId,
183
+ buildIdentity: computeLifecycleBuildIdentity(process.cwd()),
184
+ controller: controllerBinding(resolved.taskId)
185
+ });
186
+ output({
187
+ status: 'armed',
188
+ changed: true,
189
+ capabilityToken: armed.token,
190
+ marker: armed.marker,
191
+ expiresAt: armed.expiresAt,
192
+ buildIdentity: armed.buildIdentity,
193
+ error: null
194
+ });
195
+ return;
196
+ }
159
197
  const store = createCodexLifecycleStore({ cliVersion: cliVersion() });
160
198
  if (parsed.operation === 'hook-event') {
161
199
  const phase = parsed.values['--event'];
@@ -166,6 +204,36 @@ async function codexLifecycle(args = []) {
166
204
  throw new Error("hook-event --bridge must be 'true'");
167
205
  }
168
206
  const payload = await readStdin();
207
+ if (phase === 'post-tool') {
208
+ const capabilityToken = payloadText(payload, 'capabilityToken');
209
+ if (capabilityToken) {
210
+ const capabilityStore = createCodexCapabilityStore();
211
+ const armed = capabilityStore.inspect(capabilityToken);
212
+ const capability = createCodexCapabilityStore().attest({
213
+ token: capabilityToken,
214
+ sessionId: payloadText(payload, 'sessionId'),
215
+ turnId: payloadText(payload, 'turnId'),
216
+ toolUseId: payloadText(payload, 'toolUseId'),
217
+ hookDefinitionHash: payloadText(payload, 'hookDefinitionHash'),
218
+ buildIdentity: computeLifecycleBuildIdentity(process.cwd()),
219
+ controller: controllerBinding(armed.taskId)
220
+ });
221
+ output({
222
+ status: capability.status,
223
+ changed: true,
224
+ evidence: {
225
+ revision: capability.revision,
226
+ sessionId: capability.sessionId,
227
+ turnId: capability.turnId,
228
+ toolUseId: capability.toolUseId,
229
+ expiresAt: capability.expiresAt
230
+ },
231
+ diagnostics: [],
232
+ error: null
233
+ });
234
+ return;
235
+ }
236
+ }
169
237
  if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
170
238
  const toolName = payloadText(payload, 'toolName');
171
239
  if (toolName === 'collaborationspawn_agent') {