@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.
- package/dist/bin/internal-cli.js +5 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -3
- package/dist/lib/internal/codex-lifecycle.js +69 -1
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/task-orchestration.js +37 -5
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/sandbox/commands/create.js +103 -51
- package/dist/lib/sandbox/commands/rm.js +64 -15
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +13 -7
- package/dist/lib/sandbox/control/lifecycle.js +238 -29
- package/dist/lib/sandbox/control/protocol.js +7 -0
- package/dist/lib/sandbox/control/server.js +202 -26
- package/dist/lib/sandbox/control/state.js +25 -15
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/mounts.js +5 -13
- package/dist/lib/sandbox/recovery.js +59 -42
- package/dist/lib/sandbox/workspace-view.js +57 -40
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +11 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +82 -11
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +81 -6
- package/dist/lib/task/commands/validate.js +1 -1
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +84 -2
- package/dist/lib/task/events.js +139 -2
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +235 -14
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +40 -8
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -2
- package/lib/internal/codex-lifecycle.ts +68 -1
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/task-orchestration.ts +31 -4
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/sandbox/commands/create.ts +56 -7
- package/lib/sandbox/commands/rm.ts +64 -14
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +14 -10
- package/lib/sandbox/control/lifecycle.ts +264 -33
- package/lib/sandbox/control/protocol.ts +31 -5
- package/lib/sandbox/control/server.ts +184 -24
- package/lib/sandbox/control/state.ts +34 -17
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/mounts.ts +5 -15
- package/lib/sandbox/recovery.ts +71 -42
- package/lib/sandbox/workspace-view.ts +61 -40
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +13 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +73 -10
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +91 -5
- package/lib/task/commands/validate.ts +1 -1
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +160 -8
- package/lib/task/events.ts +122 -1
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +356 -17
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +51 -16
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.agents/workspace/README.en.md +0 -26
- 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`, {
|
|
@@ -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
|
-
|
|
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,7 +197,10 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
172
197
|
state: nextState,
|
|
173
198
|
consumer: null,
|
|
174
199
|
consumedAt: null,
|
|
175
|
-
|
|
200
|
+
spawnObservedAt: event.type === 'hook-spawn' && current.revision === 0
|
|
201
|
+
? observedAt
|
|
202
|
+
: current.spawnObservedAt,
|
|
203
|
+
updatedAt: observedAt
|
|
176
204
|
});
|
|
177
205
|
writeRecord(file, next, current.revision);
|
|
178
206
|
return Object.freeze({ path: file, revision: next.revision, state: next.state });
|
|
@@ -290,8 +318,9 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
290
318
|
return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
|
|
291
319
|
}
|
|
292
320
|
|
|
293
|
-
export { createCodexLifecycleStore };
|
|
321
|
+
export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
|
|
294
322
|
export type {
|
|
323
|
+
ActiveCodexLifecycleEvidenceQuery,
|
|
295
324
|
CodexLifecycleStoreOptions,
|
|
296
325
|
CodexLifecycleStoreResult,
|
|
297
326
|
StoredCodexLifecycle
|
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
|
}
|
package/lib/git/workflow.ts
CHANGED
|
@@ -20,6 +20,10 @@ function value(runner: GitRunner, cwd: string, args: readonly string[], trim = t
|
|
|
20
20
|
return result.status === 0 ? (trim ? result.stdout.trim() : result.stdout.replace(/\n$/, '')) : null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function nulPaths(output: string): string[] {
|
|
24
|
+
return output.split('\0').filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
function cleanMessage(message: string): string {
|
|
24
28
|
return message.replace(/(https?:\/\/)[^\s/@:]+(?::[^\s/@]*)?@/gi, '$1***@').trim();
|
|
25
29
|
}
|
|
@@ -68,8 +72,27 @@ function commitExplicitPaths(input: { cwd: string; paths: readonly string[]; mes
|
|
|
68
72
|
const selected = new Set(input.paths);
|
|
69
73
|
const unrelatedStaged = before.snapshot.staged.filter((candidate) => !selected.has(candidate));
|
|
70
74
|
if (unrelatedStaged.length > 0) return { status: 'failed' as const, changed: false, snapshot: before.snapshot, operations: [], error: { code: 'GIT_STAGED_SCOPE_MISMATCH', message: `Unrelated staged paths: ${unrelatedStaged.join(', ')}` } };
|
|
71
|
-
const
|
|
72
|
-
|
|
75
|
+
const stageFailure = (message: string) => ({ status: 'failed' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'failed' as const, message }], error: { code: 'GIT_STAGE_FAILED', message } });
|
|
76
|
+
const indexed = run(runner, input.cwd, ['ls-files', '-z', '--', ...input.paths]);
|
|
77
|
+
if (indexed.status !== 0) return stageFailure(indexed.stderr.trim());
|
|
78
|
+
const headed = run(runner, input.cwd, ['ls-tree', '-r', '--name-only', '-z', 'HEAD', '--', ...input.paths]);
|
|
79
|
+
if (headed.status !== 0) return stageFailure(headed.stderr.trim());
|
|
80
|
+
const indexedPaths = new Set(nulPaths(indexed.stdout));
|
|
81
|
+
const headPaths = new Set(nulPaths(headed.stdout));
|
|
82
|
+
const trackedPaths: string[] = [];
|
|
83
|
+
const addPaths: string[] = [];
|
|
84
|
+
for (const candidate of input.paths) {
|
|
85
|
+
if (indexedPaths.has(candidate)) trackedPaths.push(candidate);
|
|
86
|
+
else if (!headPaths.has(candidate) || fs.existsSync(path.join(input.cwd, candidate))) addPaths.push(candidate);
|
|
87
|
+
}
|
|
88
|
+
if (trackedPaths.length > 0) {
|
|
89
|
+
const update = run(runner, input.cwd, ['add', '-u', '--', ...trackedPaths]);
|
|
90
|
+
if (update.status !== 0) 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) return stageFailure(add.stderr.trim());
|
|
95
|
+
}
|
|
73
96
|
const staged = run(runner, input.cwd, ['diff', '--cached', '--quiet', '--']);
|
|
74
97
|
if (staged.status === 0) return { status: 'no-op' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'no-op' as const }], error: null };
|
|
75
98
|
if (input.expectedTree) {
|