@fitlab-ai/agent-infra 0.9.4 → 0.9.5
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +10 -0
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
- package/dist/lib/agent-clients/adapters/codex.js +2 -2
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +202 -0
- package/dist/lib/internal/task-create.js +48 -0
- package/dist/lib/internal/task-orchestration.js +68 -29
- package/dist/lib/sandbox/commands/create.js +1 -0
- package/dist/lib/sandbox/control/client.js +29 -15
- package/dist/lib/sandbox/control/protocol.js +23 -4
- package/dist/lib/sandbox/control/server.js +53 -18
- package/dist/lib/sandbox/workspace-view.js +8 -2
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/commit-finalization.js +14 -3
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/orchestration.js +151 -15
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
- package/lib/agent-clients/adapters/codex.ts +2 -2
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +204 -0
- package/lib/internal/task-create.ts +49 -0
- package/lib/internal/task-orchestration.ts +54 -28
- package/lib/sandbox/commands/create.ts +1 -0
- package/lib/sandbox/control/client.ts +42 -18
- package/lib/sandbox/control/protocol.ts +43 -8
- package/lib/sandbox/control/server.ts +60 -22
- package/lib/sandbox/workspace-view.ts +9 -2
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/commit-finalization.ts +23 -3
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/orchestration.ts +182 -18
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.codex/hooks.json +50 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createCodexLifecycleState, expireCodexLifecycleState, reduceCodexLifecycleEvent } from './evidence.js';
|
|
5
|
+
const LOCK_RETRY_MS = 10;
|
|
6
|
+
const LOCK_TIMEOUT_MS = 1_000;
|
|
7
|
+
const LOCK_STALE_MS = 30_000;
|
|
8
|
+
function digest(value) {
|
|
9
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
function readRecord(file) {
|
|
12
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
13
|
+
if (value.schemaVersion !== 1
|
|
14
|
+
|| !Number.isSafeInteger(value.revision)
|
|
15
|
+
|| value.revision < 1
|
|
16
|
+
|| !value.state
|
|
17
|
+
|| value.state.schemaVersion !== 1) {
|
|
18
|
+
throw new Error(`Codex lifecycle record '${path.basename(file)}' is invalid`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function recordFiles(root) {
|
|
23
|
+
if (!fs.existsSync(root))
|
|
24
|
+
return [];
|
|
25
|
+
return fs.readdirSync(root)
|
|
26
|
+
.filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
|
|
27
|
+
.map((name) => path.join(root, name));
|
|
28
|
+
}
|
|
29
|
+
function writeRecord(file, record, expectedRevision) {
|
|
30
|
+
const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
31
|
+
fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
flag: 'wx',
|
|
34
|
+
mode: 0o600
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
const actualRevision = fs.existsSync(file) ? readRecord(file).revision : 0;
|
|
38
|
+
if (actualRevision !== expectedRevision) {
|
|
39
|
+
throw new Error(`Codex lifecycle revision changed from ${expectedRevision} to ${actualRevision}`);
|
|
40
|
+
}
|
|
41
|
+
fs.renameSync(temp, file);
|
|
42
|
+
fs.chmodSync(file, 0o600);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (fs.existsSync(temp))
|
|
46
|
+
fs.unlinkSync(temp);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function createCodexLifecycleStore(options) {
|
|
50
|
+
const root = options.root ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-lifecycle');
|
|
51
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
52
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
53
|
+
fs.chmodSync(root, 0o700);
|
|
54
|
+
function withWriteLock(operation) {
|
|
55
|
+
const lock = path.join(root, '.write.lock');
|
|
56
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
57
|
+
let descriptor = null;
|
|
58
|
+
while (descriptor === null) {
|
|
59
|
+
try {
|
|
60
|
+
descriptor = fs.openSync(lock, 'wx', 0o600);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code !== 'EEXIST')
|
|
64
|
+
throw error;
|
|
65
|
+
try {
|
|
66
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
67
|
+
fs.unlinkSync(lock);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (lockError) {
|
|
72
|
+
if (lockError.code === 'ENOENT')
|
|
73
|
+
continue;
|
|
74
|
+
throw lockError;
|
|
75
|
+
}
|
|
76
|
+
if (Date.now() >= deadline)
|
|
77
|
+
throw new Error('Codex lifecycle store is busy');
|
|
78
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return operation();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
fs.closeSync(descriptor);
|
|
86
|
+
fs.unlinkSync(lock);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function findByChild(childThreadId) {
|
|
90
|
+
return recordFiles(root).filter((file) => {
|
|
91
|
+
const child = readRecord(file).state.child;
|
|
92
|
+
return child?.childThreadId === childThreadId;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function locate(event) {
|
|
96
|
+
if (event.type === 'hook-spawn') {
|
|
97
|
+
return path.join(root, `${digest(`${event.sessionId}\0${event.turnId}\0${event.toolUseId}`)}.json`);
|
|
98
|
+
}
|
|
99
|
+
if (event.type === 'hook-child') {
|
|
100
|
+
const matches = recordFiles(root).filter((file) => {
|
|
101
|
+
const state = readRecord(file).state;
|
|
102
|
+
return state.spawn?.sessionId === event.sessionId
|
|
103
|
+
&& state.spawn.nativeAgent === event.nativeAgent
|
|
104
|
+
&& (!state.child || state.child.childThreadId === event.childThreadId);
|
|
105
|
+
});
|
|
106
|
+
if (matches.length !== 1) {
|
|
107
|
+
throw new Error(matches.length === 0
|
|
108
|
+
? 'Codex lifecycle parent session and agent correlation was not found'
|
|
109
|
+
: 'Codex lifecycle parent session and agent correlation is ambiguous');
|
|
110
|
+
}
|
|
111
|
+
return matches[0];
|
|
112
|
+
}
|
|
113
|
+
const matches = findByChild(event.childThreadId);
|
|
114
|
+
if (matches.length !== 1) {
|
|
115
|
+
throw new Error(matches.length === 0
|
|
116
|
+
? `Codex lifecycle child '${event.childThreadId}' was not found`
|
|
117
|
+
: `Codex lifecycle child '${event.childThreadId}' is ambiguous`);
|
|
118
|
+
}
|
|
119
|
+
return matches[0];
|
|
120
|
+
}
|
|
121
|
+
function apply(event) {
|
|
122
|
+
return withWriteLock(() => {
|
|
123
|
+
const file = locate(event);
|
|
124
|
+
const current = fs.existsSync(file)
|
|
125
|
+
? readRecord(file)
|
|
126
|
+
: Object.freeze({
|
|
127
|
+
schemaVersion: 1,
|
|
128
|
+
revision: 0,
|
|
129
|
+
state: createCodexLifecycleState(options.cliVersion),
|
|
130
|
+
consumer: null,
|
|
131
|
+
consumedAt: null,
|
|
132
|
+
updatedAt: now()
|
|
133
|
+
});
|
|
134
|
+
if (current.consumer)
|
|
135
|
+
throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
|
|
136
|
+
const nextState = reduceCodexLifecycleEvent(current.state, event);
|
|
137
|
+
const next = Object.freeze({
|
|
138
|
+
schemaVersion: 1,
|
|
139
|
+
revision: current.revision + 1,
|
|
140
|
+
state: nextState,
|
|
141
|
+
consumer: null,
|
|
142
|
+
consumedAt: null,
|
|
143
|
+
updatedAt: now()
|
|
144
|
+
});
|
|
145
|
+
writeRecord(file, next, current.revision);
|
|
146
|
+
return Object.freeze({ path: file, revision: next.revision, state: next.state });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function read(childThreadId) {
|
|
150
|
+
const matches = findByChild(childThreadId);
|
|
151
|
+
if (matches.length !== 1)
|
|
152
|
+
throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
|
|
153
|
+
return readRecord(matches[0]);
|
|
154
|
+
}
|
|
155
|
+
function consume(childThreadId, consumer, expectedHookDefinitionHash) {
|
|
156
|
+
if (!consumer.trim())
|
|
157
|
+
throw new Error('Codex lifecycle consumer is required');
|
|
158
|
+
return withWriteLock(() => {
|
|
159
|
+
const matches = findByChild(childThreadId);
|
|
160
|
+
if (matches.length !== 1)
|
|
161
|
+
throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
|
|
162
|
+
const file = matches[0];
|
|
163
|
+
const current = readRecord(file);
|
|
164
|
+
if (current.consumer)
|
|
165
|
+
throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
|
|
166
|
+
if (current.state.status !== 'stop-ready')
|
|
167
|
+
throw new Error('Codex lifecycle evidence is not stop-ready');
|
|
168
|
+
if (expectedHookDefinitionHash
|
|
169
|
+
&& current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash)
|
|
170
|
+
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
171
|
+
const next = Object.freeze({
|
|
172
|
+
...current,
|
|
173
|
+
revision: current.revision + 1,
|
|
174
|
+
consumer,
|
|
175
|
+
consumedAt: now(),
|
|
176
|
+
updatedAt: now()
|
|
177
|
+
});
|
|
178
|
+
writeRecord(file, next, current.revision);
|
|
179
|
+
return next;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function expireBefore(cutoff) {
|
|
183
|
+
return withWriteLock(() => {
|
|
184
|
+
let changed = 0;
|
|
185
|
+
for (const file of recordFiles(root)) {
|
|
186
|
+
const current = readRecord(file);
|
|
187
|
+
if (current.updatedAt >= cutoff)
|
|
188
|
+
continue;
|
|
189
|
+
if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
|
|
190
|
+
fs.unlinkSync(file);
|
|
191
|
+
changed += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const expiredState = expireCodexLifecycleState(current.state);
|
|
195
|
+
if (expiredState === current.state)
|
|
196
|
+
continue;
|
|
197
|
+
writeRecord(file, Object.freeze({
|
|
198
|
+
...current,
|
|
199
|
+
revision: current.revision + 1,
|
|
200
|
+
state: expiredState,
|
|
201
|
+
updatedAt: now()
|
|
202
|
+
}), current.revision);
|
|
203
|
+
changed += 1;
|
|
204
|
+
}
|
|
205
|
+
return changed;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return Object.freeze({ root, apply, consume, expireBefore, read });
|
|
209
|
+
}
|
|
210
|
+
export { createCodexLifecycleStore };
|
|
211
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -21,8 +21,8 @@ const codexAdapter = defineAgentClientAdapter({
|
|
|
21
21
|
guidance: 'Use the host model picker for the complete model and reasoning-effort catalog.'
|
|
22
22
|
},
|
|
23
23
|
delegationEvidence: {
|
|
24
|
-
actualModel: '
|
|
25
|
-
actualReasoningEffort: '
|
|
24
|
+
actualModel: 'app-server',
|
|
25
|
+
actualReasoningEffort: 'app-server'
|
|
26
26
|
},
|
|
27
27
|
project: {
|
|
28
28
|
ownedPathPrefixes: ['.codex/'],
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function normalizeCanonicalPath(value, platform = process.platform) {
|
|
5
|
+
return platform === 'win32'
|
|
6
|
+
? path.win32.normalize(value).replaceAll('/', '\\').toLowerCase()
|
|
7
|
+
: path.posix.normalize(value);
|
|
8
|
+
}
|
|
9
|
+
function sameFilesystemEntry(left, right, platform = process.platform) {
|
|
10
|
+
const leftReal = fs.realpathSync.native(left);
|
|
11
|
+
const rightReal = fs.realpathSync.native(right);
|
|
12
|
+
const leftStat = fs.statSync(leftReal, { bigint: true });
|
|
13
|
+
const rightStat = fs.statSync(rightReal, { bigint: true });
|
|
14
|
+
if (leftStat.ino !== 0n && rightStat.ino !== 0n) {
|
|
15
|
+
return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
|
|
16
|
+
}
|
|
17
|
+
return normalizeCanonicalPath(leftReal, platform) === normalizeCanonicalPath(rightReal, platform);
|
|
18
|
+
}
|
|
19
|
+
function git(root, args) {
|
|
20
|
+
return execFileSync('git', args, {
|
|
21
|
+
cwd: root,
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
24
|
+
}).trim();
|
|
25
|
+
}
|
|
26
|
+
function resolveGitWorktreeIdentity(worktreeRoot) {
|
|
27
|
+
try {
|
|
28
|
+
const canonical = fs.realpathSync.native(worktreeRoot);
|
|
29
|
+
const topLevel = fs.realpathSync.native(git(canonical, ['rev-parse', '--show-toplevel']));
|
|
30
|
+
if (!sameFilesystemEntry(canonical, topLevel)) {
|
|
31
|
+
throw new Error('worktree root is not the Git top-level directory');
|
|
32
|
+
}
|
|
33
|
+
const commonDir = fs.realpathSync.native(git(canonical, [
|
|
34
|
+
'rev-parse', '--path-format=absolute', '--git-common-dir'
|
|
35
|
+
]));
|
|
36
|
+
const branch = git(canonical, ['symbolic-ref', '--short', 'HEAD']);
|
|
37
|
+
return { worktreeRoot: canonical, commonDir, branch };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
+
throw new Error(`SANDBOX_CONTROL_WORKTREE_INVALID: ${message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function assertGitWorktreeBinding(stateRoot, worktreeRoot, expectedBranch) {
|
|
45
|
+
const state = resolveGitWorktreeIdentity(stateRoot);
|
|
46
|
+
const worktree = resolveGitWorktreeIdentity(worktreeRoot);
|
|
47
|
+
if (!sameFilesystemEntry(state.commonDir, worktree.commonDir)) {
|
|
48
|
+
throw new Error('SANDBOX_CONTROL_WORKTREE_INVALID: state and worktree roots have different Git common directories');
|
|
49
|
+
}
|
|
50
|
+
if (worktree.branch !== expectedBranch) {
|
|
51
|
+
throw new Error(`SANDBOX_CONTROL_WORKTREE_INVALID: expected branch '${expectedBranch}', got '${worktree.branch}'`);
|
|
52
|
+
}
|
|
53
|
+
return worktree;
|
|
54
|
+
}
|
|
55
|
+
function assertGitRepositoryBinding(stateRoot, worktreeRoot) {
|
|
56
|
+
const state = resolveGitWorktreeIdentity(stateRoot);
|
|
57
|
+
const worktree = resolveGitWorktreeIdentity(worktreeRoot);
|
|
58
|
+
if (!sameFilesystemEntry(state.commonDir, worktree.commonDir)) {
|
|
59
|
+
throw new Error('SANDBOX_CONTROL_WORKTREE_INVALID: state and worktree roots have different Git common directories');
|
|
60
|
+
}
|
|
61
|
+
return worktree;
|
|
62
|
+
}
|
|
63
|
+
export { assertGitWorktreeBinding, assertGitRepositoryBinding, normalizeCanonicalPath, resolveGitWorktreeIdentity, sameFilesystemEntry };
|
|
64
|
+
//# sourceMappingURL=worktree-identity.js.map
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread } from '../agent-clients/adapters/codex-lifecycle/app-server.js';
|
|
6
|
+
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.js';
|
|
7
|
+
const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
|
|
8
|
+
const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
|
|
9
|
+
function output(value) {
|
|
10
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
11
|
+
}
|
|
12
|
+
function failure(code, message) {
|
|
13
|
+
output({ status: 'failed', changed: false, evidence: null, diagnostics: [], error: { code, message } });
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
16
|
+
function parse(args) {
|
|
17
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
18
|
+
process.stdout.write(USAGE);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const operation = args[0];
|
|
22
|
+
if (!operation) {
|
|
23
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'operation is required');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const allowed = {
|
|
27
|
+
'hook-event': ['--event'],
|
|
28
|
+
'resolve-start': ['--child-id'],
|
|
29
|
+
'resolve-stop': ['--child-id'],
|
|
30
|
+
preflight: ['--format', '--session-id', '--turn-id', '--tool-use-id'],
|
|
31
|
+
consume: ['--child-id', '--consumer']
|
|
32
|
+
}[operation];
|
|
33
|
+
if (!allowed) {
|
|
34
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'unknown operation');
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const values = {};
|
|
38
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
39
|
+
const flag = args[index];
|
|
40
|
+
if (!allowed.includes(flag) || Object.hasOwn(values, flag)) {
|
|
41
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `invalid or duplicate option '${flag}'`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const value = args[++index];
|
|
45
|
+
if (!value || value.startsWith('--')) {
|
|
46
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `option '${flag}' requires a value`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
values[flag] = value;
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({ operation, values: Object.freeze(values) });
|
|
52
|
+
}
|
|
53
|
+
function cliVersion() {
|
|
54
|
+
const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
|
|
55
|
+
const version = /codex-cli\s+(\d+\.\d+\.\d+)/.exec(result.stdout ?? '')?.[1];
|
|
56
|
+
if (result.status !== 0 || !version)
|
|
57
|
+
throw new Error('Codex CLI version is unavailable');
|
|
58
|
+
return version;
|
|
59
|
+
}
|
|
60
|
+
function hookDefinitionHash() {
|
|
61
|
+
const file = path.join(process.cwd(), '.codex', 'hooks.json');
|
|
62
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
63
|
+
}
|
|
64
|
+
async function readStdin() {
|
|
65
|
+
let input = '';
|
|
66
|
+
process.stdin.setEncoding('utf8');
|
|
67
|
+
for await (const chunk of process.stdin) {
|
|
68
|
+
input += chunk;
|
|
69
|
+
if (Buffer.byteLength(input) > 64 * 1024)
|
|
70
|
+
throw new Error('Codex lifecycle input exceeds 64 KiB');
|
|
71
|
+
}
|
|
72
|
+
return JSON.parse(input || '{}');
|
|
73
|
+
}
|
|
74
|
+
function recordFromPayload(phase, payload) {
|
|
75
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
76
|
+
throw new Error('Codex lifecycle hook payload must be an object');
|
|
77
|
+
}
|
|
78
|
+
const value = payload;
|
|
79
|
+
const text = (key) => typeof value[key] === 'string' ? value[key] : '';
|
|
80
|
+
if (!MANAGED_AGENT.test(text('nativeAgent')))
|
|
81
|
+
return null;
|
|
82
|
+
if (phase === 'pre-tool')
|
|
83
|
+
return {
|
|
84
|
+
type: 'hook-spawn',
|
|
85
|
+
sessionId: text('sessionId'),
|
|
86
|
+
turnId: text('turnId'),
|
|
87
|
+
toolUseId: text('toolUseId'),
|
|
88
|
+
nativeAgent: text('nativeAgent'),
|
|
89
|
+
...(text('requestedModel') ? { requestedModel: text('requestedModel') } : {}),
|
|
90
|
+
...(text('requestedReasoningEffort') ? { requestedReasoningEffort: text('requestedReasoningEffort') } : {}),
|
|
91
|
+
hookDefinitionHash: text('hookDefinitionHash')
|
|
92
|
+
};
|
|
93
|
+
if (phase === 'subagent-start')
|
|
94
|
+
return {
|
|
95
|
+
type: 'hook-child',
|
|
96
|
+
sessionId: text('sessionId'),
|
|
97
|
+
turnId: text('turnId'),
|
|
98
|
+
childThreadId: text('childThreadId'),
|
|
99
|
+
nativeAgent: text('nativeAgent')
|
|
100
|
+
};
|
|
101
|
+
if (phase === 'subagent-stop')
|
|
102
|
+
return {
|
|
103
|
+
type: 'hook-stop',
|
|
104
|
+
sessionId: text('sessionId'),
|
|
105
|
+
turnId: text('turnId'),
|
|
106
|
+
childThreadId: text('childThreadId'),
|
|
107
|
+
nativeAgent: text('nativeAgent')
|
|
108
|
+
};
|
|
109
|
+
if (phase === 'post-tool')
|
|
110
|
+
return null;
|
|
111
|
+
throw new Error(`unknown Codex lifecycle hook phase '${phase}'`);
|
|
112
|
+
}
|
|
113
|
+
async function codexLifecycle(args = []) {
|
|
114
|
+
const parsed = parse(args);
|
|
115
|
+
if (!parsed || process.exitCode)
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
if (parsed.operation === 'preflight') {
|
|
119
|
+
const format = parsed.values['--format'] ?? 'json';
|
|
120
|
+
if (!['text', 'json'].includes(format))
|
|
121
|
+
throw new Error("format must be 'text' or 'json'");
|
|
122
|
+
const runtimeFields = [
|
|
123
|
+
parsed.values['--session-id'],
|
|
124
|
+
parsed.values['--turn-id'],
|
|
125
|
+
parsed.values['--tool-use-id']
|
|
126
|
+
];
|
|
127
|
+
if (runtimeFields.some(Boolean) && !runtimeFields.every(Boolean)) {
|
|
128
|
+
throw new Error('preflight runtime identity requires --session-id, --turn-id, and --tool-use-id together');
|
|
129
|
+
}
|
|
130
|
+
const runtimeIdentity = runtimeFields.every(Boolean) ? {
|
|
131
|
+
sessionId: runtimeFields[0],
|
|
132
|
+
turnId: runtimeFields[1],
|
|
133
|
+
toolUseId: runtimeFields[2]
|
|
134
|
+
} : undefined;
|
|
135
|
+
const result = await preflightCodexLifecycleEvidence(process.cwd(), runtimeIdentity);
|
|
136
|
+
if (format === 'text') {
|
|
137
|
+
process.stdout.write(`Codex lifecycle static preflight: ready\nCLI: ${result.cliVersion}\nRuntime hook liveness: ${result.runtimeLiveness ? 'observed' : 'not-yet-observed'}\n`);
|
|
138
|
+
}
|
|
139
|
+
else
|
|
140
|
+
output({ status: 'ready', changed: false, evidence: result, diagnostics: result.diagnostics, error: null });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const store = createCodexLifecycleStore({ cliVersion: cliVersion() });
|
|
144
|
+
if (parsed.operation === 'hook-event') {
|
|
145
|
+
const phase = parsed.values['--event'];
|
|
146
|
+
if (!phase || !['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop'].includes(phase)) {
|
|
147
|
+
throw new Error('hook-event requires a known --event');
|
|
148
|
+
}
|
|
149
|
+
const event = recordFromPayload(phase, await readStdin());
|
|
150
|
+
if (!event) {
|
|
151
|
+
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const result = store.apply(event);
|
|
155
|
+
output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
|
|
156
|
+
if (result.state.status === 'invalid')
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const childThreadId = parsed.values['--child-id'];
|
|
161
|
+
if (!childThreadId)
|
|
162
|
+
throw new Error(`operation '${parsed.operation}' requires --child-id`);
|
|
163
|
+
if (parsed.operation === 'resolve-start') {
|
|
164
|
+
if (store.read(childThreadId).state.spawn?.hookDefinitionHash !== hookDefinitionHash()) {
|
|
165
|
+
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
166
|
+
}
|
|
167
|
+
const resolved = await resolveCodexThread(childThreadId);
|
|
168
|
+
let latest = store.apply(resolved.resolution.thread);
|
|
169
|
+
for (const reroute of resolved.reroutes)
|
|
170
|
+
latest = store.apply(reroute);
|
|
171
|
+
latest = store.apply(resolved.resolution.settings);
|
|
172
|
+
output({ status: latest.state.status, changed: true, evidence: latest.state.startEvidence, diagnostics: resolved.diagnostics, error: latest.state.error });
|
|
173
|
+
if (latest.state.status !== 'start-ready')
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (parsed.operation === 'resolve-stop') {
|
|
178
|
+
const terminal = await resolveCodexTerminal(childThreadId);
|
|
179
|
+
const latest = store.apply(terminal);
|
|
180
|
+
if (latest.state.status !== 'stop-ready') {
|
|
181
|
+
throw new Error(`Codex lifecycle stop evidence is not ready (status=${latest.state.status})`);
|
|
182
|
+
}
|
|
183
|
+
output({ status: latest.state.status, changed: true, evidence: latest.state.stopEvidence, diagnostics: [], error: latest.state.error });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (parsed.operation === 'consume') {
|
|
187
|
+
const consumer = parsed.values['--consumer'];
|
|
188
|
+
if (!consumer)
|
|
189
|
+
throw new Error('consume requires --consumer');
|
|
190
|
+
const consumed = store.consume(childThreadId, consumer, hookDefinitionHash());
|
|
191
|
+
output({ status: 'consumed', changed: true, evidence: {
|
|
192
|
+
start: consumed.state.startEvidence,
|
|
193
|
+
stop: consumed.state.stopEvidence
|
|
194
|
+
}, diagnostics: [], error: null });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
failure('CODEX_LIFECYCLE_FAILED', error instanceof Error ? error.message : String(error));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export { codexLifecycle };
|
|
202
|
+
//# sourceMappingURL=codex-lifecycle.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { requestSandboxTaskCreate } from '../sandbox/control/client.js';
|
|
4
|
+
import { SANDBOX_CONTROL_MAX_BYTES } from '../sandbox/control/protocol.js';
|
|
5
|
+
import { validateTaskCreateCandidate } from '../task/create.js';
|
|
6
|
+
import { createTask } from '../task/create-service.js';
|
|
7
|
+
function failed(code, message) {
|
|
8
|
+
return {
|
|
9
|
+
status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
|
|
10
|
+
operations: [], warnings: [], error: { code, message, retryable: false }
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function output(result) {
|
|
14
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
15
|
+
process.exitCode = result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0;
|
|
16
|
+
}
|
|
17
|
+
function taskCreate(args) {
|
|
18
|
+
const inputIndex = args.indexOf('--input');
|
|
19
|
+
const input = inputIndex >= 0 ? args[inputIndex + 1] : undefined;
|
|
20
|
+
if (!input || args.length !== 2 || inputIndex !== 0) {
|
|
21
|
+
output(failed('TASK_CREATE_PAYLOAD_INVALID', 'Usage: agent-infra-internal task-create --input <candidate.json>'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const inputPath = path.resolve(input);
|
|
26
|
+
const stat = fs.lstatSync(inputPath);
|
|
27
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
28
|
+
throw new Error('TASK_CREATE_INPUT_INVALID: input must be a regular file');
|
|
29
|
+
if (stat.size > SANDBOX_CONTROL_MAX_BYTES)
|
|
30
|
+
throw new Error('TASK_CREATE_INPUT_TOO_LARGE: input exceeds the control limit');
|
|
31
|
+
const candidate = validateTaskCreateCandidate(JSON.parse(fs.readFileSync(inputPath, 'utf8')));
|
|
32
|
+
if (process.env.AGENT_INFRA_CONTROL_TOKEN) {
|
|
33
|
+
const response = requestSandboxTaskCreate({ candidate });
|
|
34
|
+
process.stdout.write(response.stdout);
|
|
35
|
+
process.stderr.write(response.stderr);
|
|
36
|
+
process.exitCode = response.exitCode;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
output(createTask(candidate, { repoRoot: process.cwd() }));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
|
|
44
|
+
output(failed(code, message));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export { taskCreate };
|
|
48
|
+
//# sourceMappingURL=task-create.js.map
|