@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,398 @@
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
+ const runtimeDir = process.env.AGENT_INFRA_RUNTIME_DIR;
42
+ if (!token || !generation || !channelDir || !statusDir || !runtimeDir) {
43
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
44
+ }
45
+ return { token, generation, channelDir, statusDir, runtimeDir };
46
+ }
47
+ function verifyRuntime(runtimeDir) {
48
+ let stat;
49
+ try {
50
+ stat = fs.lstatSync(runtimeDir);
51
+ }
52
+ catch {
53
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
54
+ }
55
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
56
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
57
+ }
58
+ const probe = path.join(runtimeDir, `.controller-${process.pid}-${crypto.randomUUID()}.probe`);
59
+ try {
60
+ fs.writeFileSync(probe, 'runtime-probe\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 });
61
+ if (fs.readFileSync(probe, 'utf8') !== 'runtime-probe\n') {
62
+ throw new Error('probe mismatch');
63
+ }
64
+ }
65
+ catch {
66
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
67
+ }
68
+ finally {
69
+ fs.rmSync(probe, { force: true });
70
+ }
71
+ }
72
+ function verifyControl(taskId, control) {
73
+ verifyRuntime(control.runtimeDir);
74
+ let status = null;
75
+ let lastError = null;
76
+ for (let attempt = 0; attempt < 5; attempt += 1) {
77
+ try {
78
+ status = readSandboxControlStatus(control.statusDir);
79
+ break;
80
+ }
81
+ catch (error) {
82
+ lastError = error;
83
+ if (attempt < 4)
84
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
85
+ }
86
+ }
87
+ if (!status) {
88
+ throw new Error(`CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE: ${String(lastError)}`);
89
+ }
90
+ if (status.generation !== control.generation
91
+ || status.state !== 'healthy'
92
+ || Date.now() - status.updatedAt > 1_500) {
93
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE');
94
+ }
95
+ const response = requestSandboxControl({
96
+ family: 'task-orchestration',
97
+ args: [taskId, 'status'],
98
+ ...control,
99
+ timeoutMs: 30_000
100
+ });
101
+ let payload = {};
102
+ try {
103
+ payload = JSON.parse(response.stdout);
104
+ }
105
+ catch {
106
+ throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
107
+ }
108
+ if (payload.taskId !== taskId)
109
+ throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
110
+ }
111
+ function detectedCodexVersion() {
112
+ const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
113
+ const version = /codex-cli\s+(\d+\.\d+\.\d+)/u.exec(result.stdout ?? '')?.[1];
114
+ if (result.status !== 0 || !version)
115
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNAVAILABLE');
116
+ return version;
117
+ }
118
+ function sanitizedConfig(source, destination, sourceHome, targetHome) {
119
+ if (!fs.existsSync(source))
120
+ return [];
121
+ const parsed = toml.parse(fs.readFileSync(source, 'utf8'));
122
+ const allowed = new Set([
123
+ 'model',
124
+ 'model_provider',
125
+ 'model_providers',
126
+ 'model_reasoning_effort',
127
+ 'model_auto_compact_token_limit',
128
+ 'preferred_auth_method'
129
+ ]);
130
+ const output = Object.fromEntries(Object.entries(parsed).filter(([key]) => allowed.has(key)));
131
+ const catalog = parsed.model_catalog_json;
132
+ if (typeof catalog === 'string' && catalog.trim()) {
133
+ const resolved = path.resolve(sourceHome, catalog.replace(/^~[/\\]/u, ''));
134
+ const relative = path.relative(sourceHome, resolved);
135
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
136
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONFIG_PATH_INVALID');
137
+ }
138
+ const target = path.join(targetHome, 'model-catalogs', path.basename(resolved));
139
+ copyRegular(resolved, target, 0o600);
140
+ output.model_catalog_json = target;
141
+ }
142
+ fs.writeFileSync(destination, `${toml.stringify(output)}\n`, { mode: 0o600 });
143
+ const providerEnvironment = new Set();
144
+ const providers = output.model_providers;
145
+ if (providers && typeof providers === 'object' && !Array.isArray(providers)) {
146
+ for (const provider of Object.values(providers)) {
147
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider))
148
+ continue;
149
+ for (const key of ['env_key', 'envKey']) {
150
+ const name = provider[key];
151
+ if (typeof name === 'string' && /^[A-Z][A-Z0-9_]{1,127}$/u.test(name)) {
152
+ providerEnvironment.add(name);
153
+ }
154
+ }
155
+ }
156
+ }
157
+ return Object.freeze([...providerEnvironment]);
158
+ }
159
+ function isolatedEnvironment(home, shimDir, contextPath, taskId, control, providerEnvironment, sourceEnvironment) {
160
+ const inherited = new Set([
161
+ 'LANG', 'LC_ALL', 'TERM', 'COLORTERM',
162
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
163
+ 'http_proxy', 'https_proxy', 'no_proxy',
164
+ 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
165
+ 'CODEX_CI',
166
+ ...providerEnvironment
167
+ ]);
168
+ const env = {};
169
+ for (const key of inherited) {
170
+ if (sourceEnvironment[key] !== undefined)
171
+ env[key] = sourceEnvironment[key];
172
+ }
173
+ return {
174
+ ...env,
175
+ HOME: home,
176
+ CODEX_HOME: home,
177
+ PATH: `${shimDir}${path.delimiter}${sourceEnvironment.PATH ?? ''}`,
178
+ AGENT_INFRA_TASK_ID: taskId,
179
+ AGENT_INFRA_CONTROL_TOKEN: control.token,
180
+ AGENT_INFRA_CONTROL_GENERATION: control.generation,
181
+ AGENT_INFRA_CONTROL_DIR: control.channelDir,
182
+ AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
183
+ AGENT_INFRA_RUNTIME_DIR: control.runtimeDir,
184
+ AGENT_INFRA_CODEX_CONTROLLER_CONTEXT: contextPath
185
+ };
186
+ }
187
+ function shellQuote(value) {
188
+ return `'${value.replaceAll("'", "'\\''")}'`;
189
+ }
190
+ function prepareCodexSandboxController(input, options = {}) {
191
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
192
+ const sourceHome = path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'));
193
+ const control = options.control ?? controlFromEnvironment();
194
+ (options.verifyTaskBinding ?? verifyControl)(input.taskId, control);
195
+ const version = (options.codexVersion ?? detectedCodexVersion)();
196
+ if (!semver.gte(version, '0.147.0'))
197
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNSUPPORTED');
198
+ const runtimeRoot = path.resolve(options.temporaryRoot
199
+ ?? path.join(os.tmpdir(), 'agent-infra-codex-controllers'));
200
+ fs.mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
201
+ fs.chmodSync(runtimeRoot, 0o700);
202
+ const key = crypto.createHash('sha256')
203
+ .update(`${input.taskId}\0${control.generation}`)
204
+ .digest('hex');
205
+ const leasePath = path.join(runtimeRoot, `${key}.lease.json`);
206
+ if (fs.existsSync(leasePath)) {
207
+ const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
208
+ if (Number.isSafeInteger(lease.pid) && typeof lease.startTime === 'number'
209
+ && getProcessStartTime(lease.pid) === lease.startTime) {
210
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUSY');
211
+ }
212
+ fs.unlinkSync(leasePath);
213
+ }
214
+ const parentStartTime = getProcessStartTime(process.pid);
215
+ if (!parentStartTime)
216
+ throw new Error('CODEX_SANDBOX_CONTROLLER_PROCESS_IDENTITY_INVALID');
217
+ const instanceDigest = crypto.randomBytes(32).toString('hex');
218
+ fs.writeFileSync(leasePath, `${JSON.stringify({
219
+ version: 1,
220
+ pid: process.pid,
221
+ startTime: parentStartTime,
222
+ taskId: input.taskId,
223
+ generation: control.generation,
224
+ instanceDigest
225
+ })}\n`, { mode: 0o600, flag: 'wx' });
226
+ const home = fs.mkdtempSync(path.join(runtimeRoot, `${key}-`));
227
+ fs.chmodSync(home, 0o700);
228
+ let cleaned = false;
229
+ const cleanup = () => {
230
+ if (cleaned)
231
+ return;
232
+ cleaned = true;
233
+ const relative = path.relative(runtimeRoot, home);
234
+ if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
235
+ fs.rmSync(home, { recursive: true, force: true });
236
+ }
237
+ try {
238
+ const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
239
+ if (lease.instanceDigest === instanceDigest)
240
+ fs.unlinkSync(leasePath);
241
+ }
242
+ catch {
243
+ // A missing or replaced lease is owned by another cleanup path.
244
+ }
245
+ };
246
+ try {
247
+ const auth = path.join(sourceHome, 'auth.json');
248
+ if (fs.existsSync(auth))
249
+ copyRegular(auth, path.join(home, 'auth.json'), 0o600);
250
+ const providerEnvironment = sanitizedConfig(path.join(sourceHome, 'config.toml'), path.join(home, 'config.toml'), sourceHome, home);
251
+ const hooks = path.join(repoRoot, '.codex', 'hooks.json');
252
+ const executor = path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-executor.toml');
253
+ const reviewer = path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-reviewer.toml');
254
+ copyRegular(hooks, path.join(home, 'hooks.json'), 0o600);
255
+ copyRegular(executor, path.join(home, 'agents', path.basename(executor)), 0o600);
256
+ copyRegular(reviewer, path.join(home, 'agents', path.basename(reviewer)), 0o600);
257
+ if (crypto.createHash('sha256').update(fs.readFileSync(path.join(home, 'hooks.json'))).digest('hex')
258
+ !== crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex')
259
+ || digestFiles([
260
+ path.join(home, 'agents', path.basename(executor)),
261
+ path.join(home, 'agents', path.basename(reviewer))
262
+ ]) !== digestFiles([executor, reviewer])) {
263
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
264
+ }
265
+ const buildIdentity = computeLifecycleBuildIdentity(repoRoot);
266
+ const now = (options.now ?? Date.now)();
267
+ const context = Object.freeze({
268
+ version: 1,
269
+ taskId: input.taskId,
270
+ controlGeneration: control.generation,
271
+ controllerInstanceDigest: instanceDigest,
272
+ parentPid: process.pid,
273
+ parentStartTime,
274
+ issuedAt: now,
275
+ expiresAt: now + CONTEXT_TTL_MS,
276
+ buildIdentity,
277
+ hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex'),
278
+ lifecycleProfilesHash: digestFiles([executor, reviewer])
279
+ });
280
+ const contextPath = path.join(home, 'controller-context.json');
281
+ fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
282
+ const shimDir = path.join(home, 'bin');
283
+ fs.mkdirSync(shimDir, { mode: 0o700 });
284
+ const internalCli = path.resolve(process.argv[1] ?? path.join(repoRoot, 'bin', 'internal-cli.ts'));
285
+ const source = internalCli.endsWith('.ts')
286
+ ? `#!/bin/sh\nexec ${shellQuote(process.execPath)} --experimental-strip-types ${shellQuote(internalCli)} "$@"\n`
287
+ : `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(internalCli)} "$@"\n`;
288
+ fs.writeFileSync(path.join(shimDir, 'agent-infra-internal'), source, { mode: 0o700 });
289
+ const policy = [
290
+ input.executorModel ? `--executor-model ${input.executorModel}` : '',
291
+ input.executorReasoningEffort ? `--executor-reasoning-effort ${input.executorReasoningEffort}` : '',
292
+ input.reviewerModel ? `--reviewer-model ${input.reviewerModel}` : '',
293
+ input.reviewerReasoningEffort ? `--reviewer-reasoning-effort ${input.reviewerReasoningEffort}` : ''
294
+ ].filter(Boolean).join(' ');
295
+ const prompt = `$run-task ${input.taskRef}${policy ? ` ${policy}` : ''}`;
296
+ const args = Object.freeze([
297
+ 'exec',
298
+ '--enable', 'hooks',
299
+ '--enable', 'multi_agent',
300
+ '--dangerously-bypass-hook-trust',
301
+ '--dangerously-bypass-approvals-and-sandbox',
302
+ '--json',
303
+ '-C', repoRoot,
304
+ prompt
305
+ ]);
306
+ const env = isolatedEnvironment(home, shimDir, contextPath, input.taskId, control, providerEnvironment, options.environment ?? process.env);
307
+ return Object.freeze({
308
+ command: 'codex',
309
+ args,
310
+ env,
311
+ home,
312
+ contextPath,
313
+ context,
314
+ cleanup
315
+ });
316
+ }
317
+ catch (error) {
318
+ cleanup();
319
+ throw error;
320
+ }
321
+ }
322
+ function verifyCodexSandboxControllerContext(contextPath, taskId, options = {}) {
323
+ const stat = fs.lstatSync(contextPath);
324
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
325
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
326
+ }
327
+ const context = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
328
+ const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
329
+ if (context.version !== 1
330
+ || context.taskId !== taskId
331
+ || context.controlGeneration !== generation
332
+ || context.expiresAt < (options.now ?? Date.now())
333
+ || getProcessStartTime(context.parentPid) !== context.parentStartTime
334
+ || !isProcessAlive(context.parentPid)) {
335
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
336
+ }
337
+ const current = computeLifecycleBuildIdentity(options.repoRoot ?? process.cwd());
338
+ if (JSON.stringify(current) !== JSON.stringify(context.buildIdentity)) {
339
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
340
+ }
341
+ return Object.freeze(context);
342
+ }
343
+ async function runCodexSandboxController(input, options = {}) {
344
+ const prepared = prepareCodexSandboxController(input, options);
345
+ try {
346
+ const child = spawn(prepared.command, [...prepared.args], {
347
+ cwd: options.repoRoot ?? process.cwd(),
348
+ env: prepared.env,
349
+ stdio: 'inherit',
350
+ detached: process.platform !== 'win32'
351
+ });
352
+ let forceKill;
353
+ const terminate = () => {
354
+ if (child.pid && process.platform !== 'win32') {
355
+ try {
356
+ process.kill(-child.pid, 'SIGTERM');
357
+ }
358
+ catch {
359
+ child.kill('SIGTERM');
360
+ }
361
+ }
362
+ else
363
+ child.kill('SIGTERM');
364
+ forceKill = setTimeout(() => {
365
+ if (child.pid && process.platform !== 'win32') {
366
+ try {
367
+ process.kill(-child.pid, 'SIGKILL');
368
+ }
369
+ catch {
370
+ child.kill('SIGKILL');
371
+ }
372
+ }
373
+ else
374
+ child.kill('SIGKILL');
375
+ }, 5_000);
376
+ forceKill.unref();
377
+ };
378
+ process.once('SIGINT', terminate);
379
+ process.once('SIGTERM', terminate);
380
+ try {
381
+ return await new Promise((resolve, reject) => {
382
+ child.once('error', reject);
383
+ child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
384
+ });
385
+ }
386
+ finally {
387
+ if (forceKill)
388
+ clearTimeout(forceKill);
389
+ process.off('SIGINT', terminate);
390
+ process.off('SIGTERM', terminate);
391
+ }
392
+ }
393
+ finally {
394
+ prepared.cleanup();
395
+ }
396
+ }
397
+ export { prepareCodexSandboxController, runCodexSandboxController, verifyCodexSandboxControllerContext };
398
+ //# sourceMappingURL=sandbox-controller.js.map
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { createCodexLifecycleState, expireCodexLifecycleState, reduceCodexLifecycleEvent } from './evidence.js';
5
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
5
6
  const LOCK_RETRY_MS = 10;
6
7
  const LOCK_TIMEOUT_MS = 1_000;
7
8
  const LOCK_STALE_MS = 30_000;
@@ -14,10 +15,12 @@ function readRecord(file) {
14
15
  || !Number.isSafeInteger(value.revision)
15
16
  || value.revision < 1
16
17
  || !value.state
17
- || value.state.schemaVersion !== 1) {
18
+ || value.state.schemaVersion !== 1
19
+ || (value.spawnObservedAt != null && (typeof value.spawnObservedAt !== 'string'
20
+ || !Number.isFinite(Date.parse(value.spawnObservedAt))))) {
18
21
  throw new Error(`Codex lifecycle record '${path.basename(file)}' is invalid`);
19
22
  }
20
- return value;
23
+ return Object.freeze({ ...value, spawnObservedAt: value.spawnObservedAt ?? null });
21
24
  }
22
25
  function recordFiles(root) {
23
26
  if (!fs.existsSync(root))
@@ -26,6 +29,15 @@ function recordFiles(root) {
26
29
  .filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
27
30
  .map((name) => path.join(root, name));
28
31
  }
32
+ function hasActiveCodexLifecycleEvidence(root, query) {
33
+ return recordFiles(root).map(readRecord).some((record) => {
34
+ const evidence = record.state.startEvidence;
35
+ return record.consumer === null
36
+ && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status)
37
+ && evidence?.nativeAgent === query.nativeAgent
38
+ && evidence.hookDefinitionHash === query.hookDefinitionHash;
39
+ });
40
+ }
29
41
  function writeRecord(file, record, expectedRevision) {
30
42
  const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
31
43
  fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, {
@@ -47,7 +59,7 @@ function writeRecord(file, record, expectedRevision) {
47
59
  }
48
60
  }
49
61
  function createCodexLifecycleStore(options) {
50
- const root = options.root ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-lifecycle');
62
+ const root = options.root ?? resolveAgentRuntimeStoreRoot({ store: 'lifecycle' });
51
63
  const now = options.now ?? (() => new Date().toISOString());
52
64
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
53
65
  fs.chmodSync(root, 0o700);
@@ -121,6 +133,7 @@ function createCodexLifecycleStore(options) {
121
133
  function apply(event) {
122
134
  return withWriteLock(() => {
123
135
  const file = locate(event);
136
+ const observedAt = now();
124
137
  const current = fs.existsSync(file)
125
138
  ? readRecord(file)
126
139
  : Object.freeze({
@@ -129,7 +142,8 @@ function createCodexLifecycleStore(options) {
129
142
  state: createCodexLifecycleState(options.cliVersion),
130
143
  consumer: null,
131
144
  consumedAt: null,
132
- updatedAt: now()
145
+ spawnObservedAt: null,
146
+ updatedAt: observedAt
133
147
  });
134
148
  if (current.consumer)
135
149
  throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
@@ -140,7 +154,10 @@ function createCodexLifecycleStore(options) {
140
154
  state: nextState,
141
155
  consumer: null,
142
156
  consumedAt: null,
143
- updatedAt: now()
157
+ spawnObservedAt: event.type === 'hook-spawn' && current.revision === 0
158
+ ? observedAt
159
+ : current.spawnObservedAt,
160
+ updatedAt: observedAt
144
161
  });
145
162
  writeRecord(file, next, current.revision);
146
163
  return Object.freeze({ path: file, revision: next.revision, state: next.state });
@@ -251,5 +268,5 @@ function createCodexLifecycleStore(options) {
251
268
  }
252
269
  return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
253
270
  }
254
- export { createCodexLifecycleStore };
271
+ export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
255
272
  //# sourceMappingURL=store.js.map
@@ -2,25 +2,44 @@ import { VERSION } from './version.js';
2
2
  import { parseTaskScope } from './task/command-options.js';
3
3
  import { applyHumanDecision } from './task/decision-intents.js';
4
4
  import { canonicalTimestamp } from './task/write.js';
5
+ import { consumeHumanOverride, failureId } from './task/human-override.js';
6
+ import { resolveTaskRef } from './task/resolve-ref.js';
7
+ import { TaskExecutionLockError, withTaskExecutionLock } from './task/task-execution-lock.js';
5
8
  function parseDecisionParts(parts) {
6
9
  const decision = [];
7
10
  let needsImplementation;
11
+ let overrideTicket;
12
+ let overrideTarget;
13
+ let overrideScope;
8
14
  for (let index = 0; index < parts.length; index += 1) {
9
- if (parts[index] !== '--needs-implementation') {
10
- decision.push(parts[index]);
11
- continue;
15
+ const part = parts[index];
16
+ if (part === '--needs-implementation') {
17
+ if (needsImplementation !== undefined)
18
+ throw new Error("duplicate option '--needs-implementation'");
19
+ const value = parts[++index];
20
+ if (value !== 'true' && value !== 'false') {
21
+ throw new Error("--needs-implementation must be 'true' or 'false'");
22
+ }
23
+ needsImplementation = value === 'true';
12
24
  }
13
- if (needsImplementation !== undefined)
14
- throw new Error("duplicate option '--needs-implementation'");
15
- const value = parts[++index];
16
- if (value !== 'true' && value !== 'false') {
17
- throw new Error("--needs-implementation must be 'true' or 'false'");
25
+ else if (part === '--override-ticket' || part === '--override-target' || part === '--override-scope') {
26
+ const value = parts[++index];
27
+ if (!value)
28
+ throw new Error(`${part} requires a value`);
29
+ if (part === '--override-ticket')
30
+ overrideTicket = value;
31
+ else if (part === '--override-target')
32
+ overrideTarget = value;
33
+ else
34
+ overrideScope = value;
35
+ }
36
+ else {
37
+ decision.push(parts[index]);
18
38
  }
19
- needsImplementation = value === 'true';
20
39
  }
21
40
  if (decision.length === 0)
22
41
  throw new Error('decision content is required');
23
- return { decision: decision.join(' '), needsImplementation };
42
+ return { decision: decision.join(' '), needsImplementation, overrideTicket, overrideTarget, overrideScope };
24
43
  }
25
44
  export async function decide(args, options = {}) {
26
45
  let scope;
@@ -79,19 +98,54 @@ export async function decide(args, options = {}) {
79
98
  try {
80
99
  const parsedDecision = parseDecisionParts(decisionParts);
81
100
  const now = (options.now ?? canonicalTimestamp)();
82
- const result = applyHumanDecision({
101
+ const request = {
83
102
  taskRef, selector, decision: parsedDecision.decision,
84
103
  needsImplementation: parsedDecision.needsImplementation
85
- }, {
104
+ };
105
+ const writeOptions = {
86
106
  repoRoot: options.repoRoot,
87
107
  metadataProvider: () => ({ timestamp: now, agentInfraVersion: options.version ?? VERSION })
88
- });
108
+ };
109
+ const resolved = taskRef ? resolveTaskRef(taskRef, { repoRoot: options.repoRoot }) : null;
110
+ let result;
111
+ const execute = () => {
112
+ let current = applyHumanDecision(request, writeOptions);
113
+ if (current.status !== 'failed' || !parsedDecision.overrideTicket)
114
+ return current;
115
+ if (!parsedDecision.overrideTarget || !parsedDecision.overrideScope)
116
+ throw new Error('override ticket requires --override-target and --override-scope');
117
+ const consumed = consumeHumanOverride({
118
+ taskRef: resolved?.ok ? resolved.taskId : String(taskRef ?? ''),
119
+ ticketId: parsedDecision.overrideTicket,
120
+ failureId: failureId('decision-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
121
+ target: parsedDecision.overrideTarget,
122
+ scope: parsedDecision.overrideScope
123
+ }, {
124
+ ...writeOptions,
125
+ effectExecutor: (capability) => {
126
+ const retried = applyHumanDecision(request, { ...writeOptions, manualOverride: capability });
127
+ current = retried;
128
+ return retried.status === 'failed' || retried.status === 'planned'
129
+ ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no decision effect was committed' : `${retried.error?.code ?? 'DECISION_FAILED'}: ${retried.error?.message ?? 'manual decision effect failed'}` }
130
+ : null;
131
+ }
132
+ });
133
+ if (consumed.status === 'failed')
134
+ throw new Error(consumed.error.message);
135
+ return current;
136
+ };
137
+ result = resolved?.ok
138
+ ? withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-decision', execute)
139
+ : execute();
89
140
  if (result.error)
90
141
  throw new Error(result.error.message);
91
142
  return 0;
92
143
  }
93
144
  catch (error) {
94
- process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
145
+ const message = error instanceof TaskExecutionLockError
146
+ ? `${error.code}: ${error.message}`
147
+ : error instanceof Error ? error.message : String(error);
148
+ process.stderr.write(`Error: ${message}\n`);
95
149
  return 1;
96
150
  }
97
151
  }
@@ -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 };