@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
|
|
2
|
-
import { abortCommitIntent, activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, completeCommitIntent, pauseOrchestration, prepareOrchestrationDelegation, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, statusCommitIntent, statusOrchestration } from '../task/orchestration.js';
|
|
2
|
+
import { abortCommitIntent, activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, completeCommitIntent, pauseOrchestration, prepareOrchestrationDelegation, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt } from '../task/orchestration.js';
|
|
3
3
|
import { isAgentClientId } from '../agent-clients/types.js';
|
|
4
|
-
const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|hook-stop|advance|pause|status|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-status> [options]\n';
|
|
4
|
+
const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|hook-stop|advance|pause|status|commit-start|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-terminate|commit-status> [options]\n';
|
|
5
5
|
function usageFailure(message) {
|
|
6
6
|
process.stdout.write(`${JSON.stringify({
|
|
7
7
|
status: 'failed', changed: false,
|
|
@@ -22,7 +22,7 @@ function taskOrchestration(args = []) {
|
|
|
22
22
|
const [taskRef, intent] = args;
|
|
23
23
|
if (![
|
|
24
24
|
'begin-or-resume', 'route', 'prepare', 'hook-start', 'hook-stop', 'advance', 'pause', 'status',
|
|
25
|
-
'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-status'
|
|
25
|
+
'commit-start', 'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-terminate', 'commit-status'
|
|
26
26
|
].includes(intent)) {
|
|
27
27
|
usageFailure(`unknown intent '${intent}'`);
|
|
28
28
|
return;
|
|
@@ -48,7 +48,8 @@ function taskOrchestration(args = []) {
|
|
|
48
48
|
'--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
|
|
49
49
|
'--model-fallback-reason', '--reasoning-effort-fallback-reason',
|
|
50
50
|
'--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent',
|
|
51
|
-
'--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
|
|
51
|
+
'--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head',
|
|
52
|
+
'--attempt', '--git-worktree-root'
|
|
52
53
|
].includes(flag)) {
|
|
53
54
|
usageFailure(`unknown option '${flag}'`);
|
|
54
55
|
return;
|
|
@@ -66,12 +67,14 @@ function taskOrchestration(args = []) {
|
|
|
66
67
|
values[flag] = value;
|
|
67
68
|
}
|
|
68
69
|
const commitIntentFlags = {
|
|
69
|
-
'commit-
|
|
70
|
-
'commit-
|
|
71
|
-
'commit-
|
|
72
|
-
'commit-
|
|
73
|
-
'commit-
|
|
74
|
-
'commit-
|
|
70
|
+
'commit-start': ['--agent', '--git-worktree-root'],
|
|
71
|
+
'commit-begin': ['--agent', '--baseline-head', '--attempt', '--orchestrated', '--git-worktree-root'],
|
|
72
|
+
'commit-checkpoint': ['--token', '--kind', '--head', '--remote', '--ref', '--git-worktree-root'],
|
|
73
|
+
'commit-complete': ['--token', '--agent', '--git-worktree-root'],
|
|
74
|
+
'commit-recover': ['--agent', '--git-worktree-root'],
|
|
75
|
+
'commit-abort': ['--token', '--expected-head', '--git-worktree-root'],
|
|
76
|
+
'commit-terminate': ['--attempt', '--agent', '--code', '--git-worktree-root'],
|
|
77
|
+
'commit-status': ['--git-worktree-root']
|
|
75
78
|
};
|
|
76
79
|
if (intent in commitIntentFlags) {
|
|
77
80
|
const allowed = commitIntentFlags[intent];
|
|
@@ -83,7 +86,7 @@ function taskOrchestration(args = []) {
|
|
|
83
86
|
}
|
|
84
87
|
else {
|
|
85
88
|
const commitOnly = [...seen].find((flag) => [
|
|
86
|
-
'--orchestrated', '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
|
|
89
|
+
'--orchestrated', '--baseline-head', '--attempt', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
|
|
87
90
|
].includes(flag));
|
|
88
91
|
if (commitOnly) {
|
|
89
92
|
usageFailure(`intent '${intent}' does not accept '${commitOnly}'`);
|
|
@@ -91,13 +94,29 @@ function taskOrchestration(args = []) {
|
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
const requireValues = (flags) => flags.find((flag) => values[flag] === undefined);
|
|
97
|
+
const coreOptions = values['--git-worktree-root'] === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { gitWorktreeRoot: values['--git-worktree-root'] };
|
|
94
100
|
if (values['--client'] !== undefined && !isAgentClientId(values['--client'])) {
|
|
95
101
|
usageFailure(`unknown client '${values['--client']}'`);
|
|
96
102
|
return;
|
|
97
103
|
}
|
|
98
104
|
let result;
|
|
99
|
-
if (intent === 'commit-
|
|
100
|
-
const missing = requireValues(['--agent'
|
|
105
|
+
if (intent === 'commit-start') {
|
|
106
|
+
const missing = requireValues(['--agent']);
|
|
107
|
+
if (missing) {
|
|
108
|
+
usageFailure(`intent 'commit-start' requires '${missing}'`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const agent = normalizeAgentToken(values['--agent']);
|
|
112
|
+
if (!agent) {
|
|
113
|
+
usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
result = startCommitAttempt(taskRef, { agent }, coreOptions);
|
|
117
|
+
}
|
|
118
|
+
else if (intent === 'commit-begin') {
|
|
119
|
+
const missing = requireValues(['--agent', '--baseline-head', '--attempt']);
|
|
101
120
|
if (missing) {
|
|
102
121
|
usageFailure(`intent 'commit-begin' requires '${missing}'`);
|
|
103
122
|
return;
|
|
@@ -108,8 +127,8 @@ function taskOrchestration(args = []) {
|
|
|
108
127
|
return;
|
|
109
128
|
}
|
|
110
129
|
result = beginCommitIntent(taskRef, {
|
|
111
|
-
agent, orchestrated, baselineHead: values['--baseline-head']
|
|
112
|
-
});
|
|
130
|
+
agent, orchestrated, baselineHead: values['--baseline-head'], attempt: values['--attempt']
|
|
131
|
+
}, coreOptions);
|
|
113
132
|
}
|
|
114
133
|
else if (intent === 'commit-checkpoint') {
|
|
115
134
|
const missing = requireValues(['--token', '--kind', '--head']);
|
|
@@ -135,7 +154,7 @@ function taskOrchestration(args = []) {
|
|
|
135
154
|
result = checkpointCommitIntent(taskRef, {
|
|
136
155
|
token: values['--token'], kind: values['--kind'],
|
|
137
156
|
head: values['--head'], remote: values['--remote'], ref: values['--ref']
|
|
138
|
-
});
|
|
157
|
+
}, coreOptions);
|
|
139
158
|
}
|
|
140
159
|
else if (intent === 'commit-complete') {
|
|
141
160
|
const missing = requireValues(['--token', '--agent']);
|
|
@@ -148,7 +167,7 @@ function taskOrchestration(args = []) {
|
|
|
148
167
|
usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
|
|
149
168
|
return;
|
|
150
169
|
}
|
|
151
|
-
result = completeCommitIntent(taskRef, { token: values['--token'], agent });
|
|
170
|
+
result = completeCommitIntent(taskRef, { token: values['--token'], agent }, coreOptions);
|
|
152
171
|
}
|
|
153
172
|
else if (intent === 'commit-recover') {
|
|
154
173
|
const missing = requireValues(['--agent']);
|
|
@@ -161,7 +180,7 @@ function taskOrchestration(args = []) {
|
|
|
161
180
|
usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
|
|
162
181
|
return;
|
|
163
182
|
}
|
|
164
|
-
result = recoverCommitIntent(taskRef, { agent });
|
|
183
|
+
result = recoverCommitIntent(taskRef, { agent }, coreOptions);
|
|
165
184
|
}
|
|
166
185
|
else if (intent === 'commit-abort') {
|
|
167
186
|
const missing = requireValues(['--token', '--expected-head']);
|
|
@@ -169,10 +188,29 @@ function taskOrchestration(args = []) {
|
|
|
169
188
|
usageFailure(`intent 'commit-abort' requires '${missing}'`);
|
|
170
189
|
return;
|
|
171
190
|
}
|
|
172
|
-
result = abortCommitIntent(taskRef, { token: values['--token'], expectedHead: values['--expected-head'] });
|
|
191
|
+
result = abortCommitIntent(taskRef, { token: values['--token'], expectedHead: values['--expected-head'] }, coreOptions);
|
|
192
|
+
}
|
|
193
|
+
else if (intent === 'commit-terminate') {
|
|
194
|
+
const missing = requireValues(['--attempt', '--agent', '--code']);
|
|
195
|
+
if (missing) {
|
|
196
|
+
usageFailure(`intent 'commit-terminate' requires '${missing}'`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const agent = normalizeAgentToken(values['--agent']);
|
|
200
|
+
if (!agent) {
|
|
201
|
+
usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!/^[A-Z][A-Z0-9_]{2,63}$/.test(values['--code'])) {
|
|
205
|
+
usageFailure('--code must be a stable uppercase identifier');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
result = terminateCommitAttempt(taskRef, {
|
|
209
|
+
attempt: values['--attempt'], agent, code: values['--code']
|
|
210
|
+
}, coreOptions);
|
|
173
211
|
}
|
|
174
212
|
else if (intent === 'commit-status') {
|
|
175
|
-
result = statusCommitIntent(taskRef);
|
|
213
|
+
result = statusCommitIntent(taskRef, coreOptions);
|
|
176
214
|
}
|
|
177
215
|
else if (intent === 'begin-or-resume') {
|
|
178
216
|
const missing = requireValues(['--client']);
|
|
@@ -195,6 +233,7 @@ function taskOrchestration(args = []) {
|
|
|
195
233
|
return;
|
|
196
234
|
}
|
|
197
235
|
result = beginOrResumeOrchestration(taskRef, {
|
|
236
|
+
...coreOptions,
|
|
198
237
|
maxSteps,
|
|
199
238
|
client: values['--client'],
|
|
200
239
|
modelPolicy: hasAnyPolicy ? {
|
|
@@ -210,10 +249,10 @@ function taskOrchestration(args = []) {
|
|
|
210
249
|
});
|
|
211
250
|
}
|
|
212
251
|
else if (intent === 'route') {
|
|
213
|
-
result = routeOrchestration(taskRef);
|
|
252
|
+
result = routeOrchestration(taskRef, coreOptions);
|
|
214
253
|
}
|
|
215
254
|
else if (intent === 'status') {
|
|
216
|
-
result = statusOrchestration(taskRef);
|
|
255
|
+
result = statusOrchestration(taskRef, coreOptions);
|
|
217
256
|
}
|
|
218
257
|
else if (intent === 'prepare') {
|
|
219
258
|
const missing = requireValues(['--client']);
|
|
@@ -225,7 +264,7 @@ function taskOrchestration(args = []) {
|
|
|
225
264
|
client: values['--client'],
|
|
226
265
|
requestedModel: values['--requested-model'],
|
|
227
266
|
requestedReasoningEffort: values['--requested-reasoning-effort']
|
|
228
|
-
});
|
|
267
|
+
}, coreOptions);
|
|
229
268
|
}
|
|
230
269
|
else if (intent === 'hook-start') {
|
|
231
270
|
const missing = requireValues([
|
|
@@ -245,8 +284,8 @@ function taskOrchestration(args = []) {
|
|
|
245
284
|
reasoningEffortFallbackReason: values['--reasoning-effort-fallback-reason']
|
|
246
285
|
};
|
|
247
286
|
result = taskRef === 'auto'
|
|
248
|
-
? activateMatchingOrchestrationDelegation(values['--client'], event)
|
|
249
|
-
: activateOrchestrationDelegation(taskRef, event);
|
|
287
|
+
? activateMatchingOrchestrationDelegation(values['--client'], event, coreOptions)
|
|
288
|
+
: activateOrchestrationDelegation(taskRef, event, coreOptions);
|
|
250
289
|
}
|
|
251
290
|
else if (intent === 'hook-stop') {
|
|
252
291
|
if (taskRef === 'auto') {
|
|
@@ -257,7 +296,7 @@ function taskOrchestration(args = []) {
|
|
|
257
296
|
}
|
|
258
297
|
result = sealMatchingOrchestrationDelegation(values['--client'], {
|
|
259
298
|
nativeAgent: values['--native-agent'], childId: values['--child-id']
|
|
260
|
-
});
|
|
299
|
+
}, coreOptions);
|
|
261
300
|
}
|
|
262
301
|
else {
|
|
263
302
|
const missing = requireValues(['--child-id', '--exit-code', '--after-fingerprint']);
|
|
@@ -274,11 +313,11 @@ function taskOrchestration(args = []) {
|
|
|
274
313
|
childId: values['--child-id'], exitCode,
|
|
275
314
|
afterFingerprint: values['--after-fingerprint'],
|
|
276
315
|
changedPaths: values['--changed-paths'] ? values['--changed-paths'].split(',').filter(Boolean) : []
|
|
277
|
-
});
|
|
316
|
+
}, coreOptions);
|
|
278
317
|
}
|
|
279
318
|
}
|
|
280
319
|
else if (intent === 'advance') {
|
|
281
|
-
result = advanceOrchestration(taskRef);
|
|
320
|
+
result = advanceOrchestration(taskRef, coreOptions);
|
|
282
321
|
}
|
|
283
322
|
else {
|
|
284
323
|
const missing = requireValues(['--code', '--message', '--recoverable']);
|
|
@@ -290,7 +329,7 @@ function taskOrchestration(args = []) {
|
|
|
290
329
|
usageFailure('--recoverable must be true or false');
|
|
291
330
|
return;
|
|
292
331
|
}
|
|
293
|
-
result = pauseOrchestration(taskRef, values['--code'], values['--message'], values['--recoverable'] === 'true');
|
|
332
|
+
result = pauseOrchestration(taskRef, values['--code'], values['--message'], values['--recoverable'] === 'true', coreOptions);
|
|
294
333
|
}
|
|
295
334
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
296
335
|
if (result.status === 'failed')
|
|
@@ -965,6 +965,7 @@ export async function create(args) {
|
|
|
965
965
|
const control = materializeSandboxControl({
|
|
966
966
|
base: effectiveConfig.controlBase,
|
|
967
967
|
repoRoot: effectiveConfig.repoRoot,
|
|
968
|
+
worktreeRoot: worktree,
|
|
968
969
|
project: effectiveConfig.project,
|
|
969
970
|
container,
|
|
970
971
|
branch,
|
|
@@ -5,25 +5,17 @@ import { SANDBOX_CONTROL_MAX_BYTES, isSandboxControlFamily } from './protocol.js
|
|
|
5
5
|
function sleep(milliseconds) {
|
|
6
6
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
if (!isSandboxControlFamily(params.family)) {
|
|
10
|
-
throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
|
|
11
|
-
}
|
|
8
|
+
function exchangeSandboxControl(request, params) {
|
|
12
9
|
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
13
|
-
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
14
|
-
if (!token)
|
|
15
|
-
throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
16
|
-
const requestsDir = path.join(channelDir, 'requests');
|
|
17
|
-
const responsesDir = path.join(channelDir, 'responses');
|
|
18
|
-
const id = randomUUID();
|
|
19
|
-
const request = { version: 1, id, token, family: params.family, args: params.args };
|
|
20
10
|
const encoded = `${JSON.stringify(request)}\n`;
|
|
21
11
|
if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
22
12
|
throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
|
|
23
13
|
}
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
14
|
+
const requestsDir = path.join(channelDir, 'requests');
|
|
15
|
+
const responsesDir = path.join(channelDir, 'responses');
|
|
16
|
+
const temporary = path.join(requestsDir, `.${request.id}.tmp`);
|
|
17
|
+
const requestPath = path.join(requestsDir, `${request.id}.json`);
|
|
18
|
+
const responsePath = path.join(responsesDir, `${request.id}.json`);
|
|
27
19
|
fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
|
|
28
20
|
fs.renameSync(temporary, requestPath);
|
|
29
21
|
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
@@ -32,7 +24,7 @@ export function requestSandboxControl(params) {
|
|
|
32
24
|
const raw = fs.readFileSync(responsePath, 'utf8');
|
|
33
25
|
fs.unlinkSync(responsePath);
|
|
34
26
|
const response = JSON.parse(raw);
|
|
35
|
-
if (response.version !== 1 || response.id !== id || !Number.isInteger(response.exitCode)) {
|
|
27
|
+
if (response.version !== 1 || response.id !== request.id || !Number.isInteger(response.exitCode)) {
|
|
36
28
|
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID: broker response is invalid');
|
|
37
29
|
}
|
|
38
30
|
return response;
|
|
@@ -41,4 +33,26 @@ export function requestSandboxControl(params) {
|
|
|
41
33
|
}
|
|
42
34
|
throw new Error(`SANDBOX_CONTROL_TIMEOUT: broker did not respond within ${params.timeoutMs ?? 30_000}ms`);
|
|
43
35
|
}
|
|
36
|
+
export function requestSandboxControl(params) {
|
|
37
|
+
if (!isSandboxControlFamily(params.family)) {
|
|
38
|
+
throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
|
|
39
|
+
}
|
|
40
|
+
if (params.family === 'task-create')
|
|
41
|
+
throw new Error("SANDBOX_CONTROL_COMMAND_DENIED: 'task-create' requires a typed candidate");
|
|
42
|
+
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
43
|
+
if (!token)
|
|
44
|
+
throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
45
|
+
const id = randomUUID();
|
|
46
|
+
const request = { version: 1, id, token, family: params.family, args: params.args };
|
|
47
|
+
return exchangeSandboxControl(request, params);
|
|
48
|
+
}
|
|
49
|
+
export function requestSandboxTaskCreate(params) {
|
|
50
|
+
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
51
|
+
if (!token)
|
|
52
|
+
throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
53
|
+
const request = {
|
|
54
|
+
version: 1, id: randomUUID(), token, family: 'task-create', candidate: params.candidate
|
|
55
|
+
};
|
|
56
|
+
return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
|
|
57
|
+
}
|
|
44
58
|
//# sourceMappingURL=client.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { validateTaskCreateCandidate } from '../../task/create.js';
|
|
1
2
|
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
2
|
-
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration'];
|
|
3
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'];
|
|
3
4
|
export function isSandboxControlFamily(value) {
|
|
4
5
|
return SANDBOX_CONTROL_FAMILIES.includes(value);
|
|
5
6
|
}
|
|
@@ -13,7 +14,21 @@ export function validateSandboxControlRequest(value, manifest) {
|
|
|
13
14
|
|| !/^[a-f0-9-]{16,64}$/.test(request.id)
|
|
14
15
|
|| request.token !== manifest.token
|
|
15
16
|
|| typeof request.family !== 'string'
|
|
16
|
-
|| !isSandboxControlFamily(request.family)
|
|
17
|
+
|| !isSandboxControlFamily(request.family)) {
|
|
18
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
19
|
+
}
|
|
20
|
+
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
21
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
|
|
22
|
+
}
|
|
23
|
+
if (request.family === 'task-create') {
|
|
24
|
+
const keys = Object.keys(request).sort().join(',');
|
|
25
|
+
if (keys !== ['candidate', 'family', 'id', 'token', 'version'].sort().join(',')) {
|
|
26
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
27
|
+
}
|
|
28
|
+
return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
|
|
29
|
+
}
|
|
30
|
+
const keys = Object.keys(request).sort().join(',');
|
|
31
|
+
if (keys !== ['args', 'family', 'id', 'token', 'version'].sort().join(',')
|
|
17
32
|
|| !Array.isArray(request.args)
|
|
18
33
|
|| !request.args.every((arg) => typeof arg === 'string')) {
|
|
19
34
|
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
@@ -21,12 +36,16 @@ export function validateSandboxControlRequest(value, manifest) {
|
|
|
21
36
|
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
22
37
|
throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: branch-only sandboxes cannot coordinate tasks');
|
|
23
38
|
}
|
|
24
|
-
if (
|
|
25
|
-
|
|
39
|
+
if (request.family === 'task-orchestration'
|
|
40
|
+
&& request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
|
|
41
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: worktree binding is reserved for the control broker');
|
|
26
42
|
}
|
|
27
43
|
return request;
|
|
28
44
|
}
|
|
29
45
|
export function bindSandboxControlTask(request, taskId) {
|
|
46
|
+
if (request.family === 'task-create') {
|
|
47
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: task-create requests do not bind a current task');
|
|
48
|
+
}
|
|
30
49
|
if (request.args.length === 0) {
|
|
31
50
|
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
|
|
32
51
|
}
|
|
@@ -2,14 +2,19 @@ import { spawnSync } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { getProcessStartTime } from '../../server/process-state.js';
|
|
5
|
+
import { createTask } from '../../task/create-service.js';
|
|
6
|
+
import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
|
|
5
7
|
import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
|
|
6
8
|
function readManifest(manifestPath) {
|
|
7
9
|
const stat = fs.lstatSync(manifestPath);
|
|
8
10
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
9
11
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
10
12
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
11
|
-
if (manifest.version !==
|
|
12
|
-
|
|
13
|
+
if (manifest.version !== 2) {
|
|
14
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 2; recreate the sandbox');
|
|
15
|
+
}
|
|
16
|
+
if (typeof manifest.repoRoot !== 'string'
|
|
17
|
+
|| typeof manifest.worktreeRoot !== 'string'
|
|
13
18
|
|| typeof manifest.channelDir !== 'string'
|
|
14
19
|
|| typeof manifest.token !== 'string')
|
|
15
20
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
@@ -31,8 +36,23 @@ function writeResponse(directory, response) {
|
|
|
31
36
|
fs.writeFileSync(temporary, `${JSON.stringify(response)}\n`, { mode: 0o600, flag: 'wx' });
|
|
32
37
|
fs.renameSync(temporary, path.join(directory, `${response.id}.json`));
|
|
33
38
|
}
|
|
39
|
+
export function sandboxControlSafeEnv(env = process.env) {
|
|
40
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
|
|
41
|
+
}
|
|
42
|
+
function consumeRequest(directory, id) {
|
|
43
|
+
try {
|
|
44
|
+
fs.writeFileSync(path.join(directory, id), '', { flag: 'wx', mode: 0o600 });
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code === 'EEXIST') {
|
|
48
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
34
53
|
export function serveSandboxControl(manifestPath, signal = new AbortController().signal) {
|
|
35
54
|
const manifest = readManifest(manifestPath);
|
|
55
|
+
assertGitWorktreeBinding(manifest.repoRoot, manifest.worktreeRoot, manifest.branch);
|
|
36
56
|
const brokerPath = path.join(path.dirname(manifestPath), 'broker.json');
|
|
37
57
|
const startTime = getProcessStartTime(process.pid);
|
|
38
58
|
if (!startTime)
|
|
@@ -45,16 +65,19 @@ export function serveSandboxControl(manifestPath, signal = new AbortController()
|
|
|
45
65
|
})}\n`, { mode: 0o600, flag: 'wx' });
|
|
46
66
|
const requestsDir = path.join(manifest.channelDir, 'requests');
|
|
47
67
|
const responsesDir = path.join(manifest.channelDir, 'responses');
|
|
68
|
+
const consumedDir = path.join(path.dirname(manifestPath), 'consumed');
|
|
48
69
|
assertRealDirectory(manifest.channelDir);
|
|
49
70
|
fs.mkdirSync(requestsDir, { recursive: true, mode: 0o700 });
|
|
50
71
|
fs.mkdirSync(responsesDir, { recursive: true, mode: 0o700 });
|
|
72
|
+
fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
|
|
51
73
|
assertRealDirectory(requestsDir, manifest.channelDir);
|
|
52
74
|
assertRealDirectory(responsesDir, manifest.channelDir);
|
|
53
|
-
|
|
75
|
+
assertRealDirectory(consumedDir, path.dirname(manifestPath));
|
|
54
76
|
while (!signal.aborted) {
|
|
55
77
|
const current = readManifest(manifestPath);
|
|
56
78
|
if (current.token !== manifest.token)
|
|
57
79
|
return;
|
|
80
|
+
assertGitWorktreeBinding(current.repoRoot, current.worktreeRoot, current.branch);
|
|
58
81
|
assertRealDirectory(requestsDir, manifest.channelDir);
|
|
59
82
|
assertRealDirectory(responsesDir, manifest.channelDir);
|
|
60
83
|
for (const name of fs.readdirSync(requestsDir).sort()) {
|
|
@@ -69,21 +92,33 @@ export function serveSandboxControl(manifestPath, signal = new AbortController()
|
|
|
69
92
|
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
|
|
70
93
|
const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(requestPath, 'utf8')), manifest);
|
|
71
94
|
id = request.id;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
consumeRequest(consumedDir, id);
|
|
96
|
+
if (request.family === 'task-create') {
|
|
97
|
+
const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
|
|
98
|
+
response = {
|
|
99
|
+
version: 1, id,
|
|
100
|
+
exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
|
|
101
|
+
stdout: `${JSON.stringify(result)}\n`, stderr: ''
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const boundArgs = bindSandboxControlTask(request, manifest.taskId);
|
|
106
|
+
if (request.family === 'task-orchestration') {
|
|
107
|
+
boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
|
|
108
|
+
}
|
|
109
|
+
const result = spawnSync(process.execPath, ['--experimental-strip-types', '--no-warnings', process.argv[1], request.family, ...boundArgs], {
|
|
110
|
+
cwd: manifest.repoRoot,
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
env: sandboxControlSafeEnv()
|
|
113
|
+
});
|
|
114
|
+
response = {
|
|
115
|
+
version: 1,
|
|
116
|
+
id,
|
|
117
|
+
exitCode: result.status ?? 1,
|
|
118
|
+
stdout: result.stdout ?? '',
|
|
119
|
+
stderr: result.stderr ?? ''
|
|
120
|
+
};
|
|
121
|
+
}
|
|
87
122
|
}
|
|
88
123
|
catch (error) {
|
|
89
124
|
response = {
|
|
@@ -79,6 +79,10 @@ export function materializeSandboxWorkspaceView(params) {
|
|
|
79
79
|
export function materializeSandboxControl(params) {
|
|
80
80
|
const { root, channelDir, manifestPath } = sandboxControlPaths(params);
|
|
81
81
|
assertSafeDirectory(root, path.resolve(params.base));
|
|
82
|
+
const consumedDir = path.join(root, 'consumed');
|
|
83
|
+
assertSafeDirectory(consumedDir, root);
|
|
84
|
+
fs.rmSync(consumedDir, { recursive: true, force: true });
|
|
85
|
+
fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
|
|
82
86
|
for (const queue of ['requests', 'responses']) {
|
|
83
87
|
const directory = path.join(channelDir, queue);
|
|
84
88
|
assertSafeDirectory(directory, root);
|
|
@@ -86,9 +90,11 @@ export function materializeSandboxControl(params) {
|
|
|
86
90
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
87
91
|
}
|
|
88
92
|
const token = randomBytes(32).toString('hex');
|
|
93
|
+
const repoRoot = fs.realpathSync.native(params.repoRoot);
|
|
89
94
|
const manifest = {
|
|
90
|
-
version:
|
|
91
|
-
repoRoot
|
|
95
|
+
version: 2,
|
|
96
|
+
repoRoot,
|
|
97
|
+
worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
|
|
92
98
|
project: params.project,
|
|
93
99
|
container: params.container,
|
|
94
100
|
branch: params.branch,
|
|
@@ -2,6 +2,9 @@ const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
|
|
|
2
2
|
const NEXT_H2_RE = /^##\s/;
|
|
3
3
|
const ENTRY_RE = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
|
|
4
4
|
const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
|
|
5
|
+
const ABORTED_SUFFIX_RE = /\s*\[aborted\]\s*$/;
|
|
6
|
+
const ATTEMPT_RE = /(?:^|;\s*)attempt=([A-Za-z0-9-]{8,64})(?:;|$)/;
|
|
7
|
+
const COMMIT_STARTED_RE = /^started; attempt=([A-Za-z0-9-]{8,64}); baseline=([a-f0-9]{40,64}); agent=([a-z-]+)$/;
|
|
5
8
|
function parseActivityLog(content) {
|
|
6
9
|
const section = locateActivityLog(content);
|
|
7
10
|
return section ? { sectionFound: true, entries: section.entries } : { sectionFound: false, entries: [] };
|
|
@@ -34,28 +37,52 @@ function appendActivityEntry(section, entry) {
|
|
|
34
37
|
const line = `- ${entry.time} — **${entry.step}** by ${entry.agent} — ${entry.note}`;
|
|
35
38
|
return section.body ? `${section.body}\n${line}` : line;
|
|
36
39
|
}
|
|
40
|
+
function activityAttempt(note) {
|
|
41
|
+
return ATTEMPT_RE.exec(note)?.[1] ?? null;
|
|
42
|
+
}
|
|
43
|
+
function parseCommitAttemptStarted(note) {
|
|
44
|
+
const match = COMMIT_STARTED_RE.exec(note);
|
|
45
|
+
return match ? { attempt: match[1], baseline: match[2], agent: match[3] } : null;
|
|
46
|
+
}
|
|
47
|
+
function commitAttemptStartedNote(attempt) {
|
|
48
|
+
return `started; attempt=${attempt.attempt}; baseline=${attempt.baseline}; agent=${attempt.agent}`;
|
|
49
|
+
}
|
|
37
50
|
function pairEntries(entries) {
|
|
38
51
|
const rows = [];
|
|
39
52
|
const open = new Map();
|
|
40
53
|
for (const entry of entries) {
|
|
41
54
|
const started = STARTED_SUFFIX_RE.test(entry.step);
|
|
42
|
-
const
|
|
55
|
+
const aborted = ABORTED_SUFFIX_RE.test(entry.step);
|
|
56
|
+
const base = entry.step.replace(STARTED_SUFFIX_RE, '').replace(ABORTED_SUFFIX_RE, '');
|
|
43
57
|
if (started) {
|
|
44
|
-
const
|
|
58
|
+
const attempt = activityAttempt(entry.note);
|
|
59
|
+
const row = {
|
|
60
|
+
step: base, agent: entry.agent, started: entry.time, done: '', note: entry.note,
|
|
61
|
+
...(attempt ? { attempt } : {})
|
|
62
|
+
};
|
|
45
63
|
rows.push(row);
|
|
46
64
|
const queue = open.get(base) ?? [];
|
|
47
65
|
queue.push(row);
|
|
48
66
|
open.set(base, queue);
|
|
49
67
|
}
|
|
50
68
|
else {
|
|
51
|
-
const
|
|
69
|
+
const queue = open.get(base) ?? [];
|
|
70
|
+
const terminalAttempt = aborted ? activityAttempt(entry.note) : null;
|
|
71
|
+
const pendingIndex = terminalAttempt === null
|
|
72
|
+
? 0
|
|
73
|
+
: queue.findIndex((row) => row.attempt === terminalAttempt);
|
|
74
|
+
const pending = pendingIndex >= 0 ? queue.splice(pendingIndex, 1)[0] : undefined;
|
|
52
75
|
if (pending)
|
|
53
76
|
Object.assign(pending, { done: entry.time, agent: entry.agent, note: entry.note });
|
|
54
77
|
else
|
|
55
|
-
rows.push({
|
|
78
|
+
rows.push({
|
|
79
|
+
step: aborted ? entry.step : base,
|
|
80
|
+
agent: entry.agent, started: '', done: entry.time, note: entry.note,
|
|
81
|
+
...(terminalAttempt ? { attempt: terminalAttempt } : {})
|
|
82
|
+
});
|
|
56
83
|
}
|
|
57
84
|
}
|
|
58
85
|
return rows;
|
|
59
86
|
}
|
|
60
|
-
export {
|
|
87
|
+
export { activityAttempt, appendActivityEntry, commitAttemptStartedNote, locateActivityLog, pairEntries, parseActivityLog, parseCommitAttemptStarted };
|
|
61
88
|
//# sourceMappingURL=activity-log.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { locateActivityLog, appendActivityEntry, pairEntries } from './activity-log.js';
|
|
4
|
+
import { locateActivityLog, appendActivityEntry, pairEntries, parseCommitAttemptStarted } from './activity-log.js';
|
|
5
5
|
import { commitIntentPath, digest, readCommitIntent } from './commit-intent.js';
|
|
6
6
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
7
7
|
import { parseReviewSummary } from './review-artifacts.js';
|
|
8
|
-
import { extractReviewBaseline, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact } from './review-fingerprint.js';
|
|
8
|
+
import { extractReviewBaseline, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, parseReviewedGitTree } from './review-fingerprint.js';
|
|
9
9
|
const SHA_RE = /^[a-f0-9]{40,64}$/;
|
|
10
10
|
function git(repoRoot, args) {
|
|
11
11
|
return execFileSync('git', args, {
|
|
@@ -38,6 +38,7 @@ function outcome(disposition, overrides = {}) {
|
|
|
38
38
|
needsLog: false,
|
|
39
39
|
intent: null,
|
|
40
40
|
intentDigest: null,
|
|
41
|
+
attempt: null,
|
|
41
42
|
...overrides
|
|
42
43
|
};
|
|
43
44
|
}
|
|
@@ -63,6 +64,16 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
63
64
|
const openRows = commitRows.filter((row) => row.started !== '' && row.done === '');
|
|
64
65
|
const file = commitIntentPath(taskDir);
|
|
65
66
|
if (!fs.existsSync(file)) {
|
|
67
|
+
if (openRows.length === 1) {
|
|
68
|
+
const attempt = parseCommitAttemptStarted(openRows[0].note);
|
|
69
|
+
if (attempt && attempt.baseline === currentHead) {
|
|
70
|
+
return outcome('retryable-start', {
|
|
71
|
+
message: 'a structured Commit attempt can be safely reused',
|
|
72
|
+
currentHead,
|
|
73
|
+
attempt
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
66
77
|
if (openRows.length > 0) {
|
|
67
78
|
return outcome('orphaned-start', {
|
|
68
79
|
code: 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
|
|
@@ -132,7 +143,7 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
132
143
|
const summary = parseReviewSummary(reviewContent);
|
|
133
144
|
const reviewBaseline = extractReviewBaseline(reviewContent);
|
|
134
145
|
const reviewTree = extractReviewedSnapshotTree(reviewContent);
|
|
135
|
-
if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !
|
|
146
|
+
if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !parseReviewedGitTree(reviewTree)) {
|
|
136
147
|
return invalid('authoritative review-code evidence is not a valid approved snapshot', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
|
|
137
148
|
}
|
|
138
149
|
const committedTree = git(repoRoot, ['rev-parse', `${intent.committedHead}^{tree}`]);
|