@fitlab-ai/agent-infra 0.11.10 → 0.11.11
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/lib/agent-clients/adapter.js +20 -0
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
- package/dist/lib/agent-clients/adapters/codex.js +4 -0
- package/dist/lib/internal/manual-validation.js +2 -2
- package/dist/lib/internal/platform-pr.js +4 -5
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-create.js +15 -1
- package/dist/lib/internal/task-finalization.js +1 -3
- package/dist/lib/internal/task-operation-registry.js +4 -138
- package/dist/lib/platform/pr-summary.js +45 -8
- package/dist/lib/platform/pull-requests.js +79 -14
- package/dist/lib/platform/verification-sync.js +17 -14
- package/dist/lib/sandbox/commands/create.js +18 -2
- package/dist/lib/sandbox/commands/enter.js +47 -33
- package/dist/lib/sandbox/control/client.js +46 -12
- package/dist/lib/sandbox/control/executor.js +4 -11
- package/dist/lib/sandbox/control/host-recovery.js +218 -0
- package/dist/lib/sandbox/control/protocol.js +2 -3
- package/dist/lib/sandbox/control/server.js +42 -2
- package/dist/lib/sandbox/environment.js +150 -0
- package/dist/lib/sandbox/recovery.js +8 -0
- package/dist/lib/sandbox/workspace-identity.js +0 -14
- package/dist/lib/sandbox/workspace-view.js +33 -6
- package/dist/lib/task/artifact-command.js +7 -2
- package/dist/lib/task/artifact-lifecycle.js +2 -1
- package/dist/lib/task/capabilities.js +4 -1
- package/dist/lib/task/control-authority.js +0 -2
- package/dist/lib/task/events.js +1 -1
- package/dist/lib/task/finalization.js +37 -54
- package/dist/lib/task/frontmatter.js +24 -1
- package/dist/lib/task/ledger-intents.js +2 -1
- package/dist/lib/task/platform-operation-journal.js +12 -0
- package/dist/lib/task/platform-operation-recovery.js +14 -7
- package/dist/lib/task/resolve-ref.js +8 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +22 -0
- package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
- package/lib/agent-clients/adapters/codex.ts +4 -0
- package/lib/internal/manual-validation.ts +2 -2
- package/lib/internal/platform-pr.ts +3 -4
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-create.ts +15 -2
- package/lib/internal/task-finalization.ts +1 -3
- package/lib/internal/task-operation-registry.ts +4 -150
- package/lib/platform/pr-summary.ts +43 -7
- package/lib/platform/pull-requests.ts +74 -13
- package/lib/platform/verification-sync.ts +18 -14
- package/lib/sandbox/commands/create.ts +24 -2
- package/lib/sandbox/commands/enter.ts +42 -39
- package/lib/sandbox/control/client.ts +52 -15
- package/lib/sandbox/control/executor.ts +5 -12
- package/lib/sandbox/control/host-recovery.ts +195 -0
- package/lib/sandbox/control/protocol.ts +4 -5
- package/lib/sandbox/control/server.ts +42 -1
- package/lib/sandbox/environment.ts +162 -0
- package/lib/sandbox/recovery.ts +10 -0
- package/lib/sandbox/tool-types.ts +1 -0
- package/lib/sandbox/workspace-identity.ts +0 -26
- package/lib/sandbox/workspace-view.ts +33 -8
- package/lib/task/artifact-command.ts +9 -2
- package/lib/task/artifact-lifecycle.ts +2 -1
- package/lib/task/capabilities.ts +5 -2
- package/lib/task/control-authority.ts +0 -4
- package/lib/task/events.ts +1 -1
- package/lib/task/finalization.ts +34 -51
- package/lib/task/frontmatter.ts +25 -1
- package/lib/task/ledger-intents.ts +2 -1
- package/lib/task/platform-operation-journal.ts +29 -2
- package/lib/task/platform-operation-recovery.ts +12 -6
- package/lib/task/resolve-ref.ts +8 -3
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +1 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
- package/templates/.agents/rules/review-handshake.en.md +3 -0
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +4 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +4 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/dist/lib/task/finalization-handoff.js +0 -93
- package/lib/task/finalization-handoff.ts +0 -105
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readSandboxControlManifest } from './lifecycle.js';
|
|
5
|
+
import { identityDigest } from './identity-sentinel.js';
|
|
6
|
+
import { readSandboxControlPayload, readSandboxControlResultEvidence } from './state.js';
|
|
7
|
+
import { taskCreateOutputUnavailableResult } from '../../task/create-service.js';
|
|
8
|
+
const REQUEST_ID = /^[a-f0-9-]{16,64}$/u;
|
|
9
|
+
function unknown(requestId) {
|
|
10
|
+
return {
|
|
11
|
+
version: 2, id: requestId, phase: 'rejected', exitCode: null, stdout: '', stderr: '',
|
|
12
|
+
error: { code: 'SANDBOX_CONTROL_RESULT_UNKNOWN', message: 'SANDBOX_CONTROL_RESULT_UNKNOWN: recovery evidence is unavailable', retryable: false },
|
|
13
|
+
outputState: 'unavailable', payload: null
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function manifestDigest(manifest) {
|
|
17
|
+
return identityDigest({
|
|
18
|
+
version: 1, mode: manifest.mode, taskId: manifest.taskId,
|
|
19
|
+
generation: manifest.generation, controlRootId: manifest.controlRootId
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function managedManifestPaths(managedRoot) {
|
|
23
|
+
if (!fs.existsSync(managedRoot))
|
|
24
|
+
return [];
|
|
25
|
+
const rootStat = fs.lstatSync(managedRoot);
|
|
26
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
27
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
28
|
+
const result = [];
|
|
29
|
+
for (const container of fs.readdirSync(managedRoot, { withFileTypes: true })) {
|
|
30
|
+
if (!container.isDirectory() || container.isSymbolicLink())
|
|
31
|
+
continue;
|
|
32
|
+
const containerPath = path.join(managedRoot, container.name);
|
|
33
|
+
for (const identity of fs.readdirSync(containerPath, { withFileTypes: true })) {
|
|
34
|
+
if (!identity.isDirectory() || identity.isSymbolicLink() || !/^[a-f0-9]{16}$/u.test(identity.name))
|
|
35
|
+
continue;
|
|
36
|
+
result.push(path.join(containerPath, identity.name, 'manifest.json'));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function assertDirectory(directory) {
|
|
42
|
+
const stat = fs.lstatSync(directory);
|
|
43
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
44
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
45
|
+
}
|
|
46
|
+
function readAudit(manifest) {
|
|
47
|
+
const root = path.dirname(path.resolve(manifest.publicStatusDir));
|
|
48
|
+
const records = [];
|
|
49
|
+
for (const file of [path.join(root, 'audit.ndjson.1'), path.join(root, 'audit.ndjson')]) {
|
|
50
|
+
if (!fs.existsSync(file))
|
|
51
|
+
continue;
|
|
52
|
+
const stat = fs.lstatSync(file);
|
|
53
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
54
|
+
throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
|
|
55
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
|
|
56
|
+
let value;
|
|
57
|
+
try {
|
|
58
|
+
value = JSON.parse(line);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
|
|
62
|
+
}
|
|
63
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
64
|
+
throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
|
|
65
|
+
records.push(value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return records;
|
|
69
|
+
}
|
|
70
|
+
function auditProof(manifest, requestId) {
|
|
71
|
+
const digest = manifestDigest(manifest);
|
|
72
|
+
const related = readAudit(manifest).filter((record) => record.requestId === requestId);
|
|
73
|
+
for (const record of related) {
|
|
74
|
+
if (record.generation !== manifest.generation || record.identityDigest !== digest) {
|
|
75
|
+
throw new Error('SANDBOX_CONTROL_AUDIT_IDENTITY_MISMATCH');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!related.some((record) => record.event === 'accepted-authorized'
|
|
79
|
+
&& record.version === 2 && record.phase === 'accepted-authorized'))
|
|
80
|
+
return [];
|
|
81
|
+
return related;
|
|
82
|
+
}
|
|
83
|
+
function publishedEvidence(records, generation) {
|
|
84
|
+
const publications = records.filter((record) => record.event === 'executor-result-published');
|
|
85
|
+
let selected = null;
|
|
86
|
+
let selectedSummary = null;
|
|
87
|
+
for (const record of publications) {
|
|
88
|
+
if (record.version !== 2 || record.requestGeneration !== generation
|
|
89
|
+
|| !Number.isSafeInteger(record.exitCode)
|
|
90
|
+
|| !Number.isSafeInteger(record.outputBytes) || !Number.isSafeInteger(record.errorBytes)
|
|
91
|
+
|| typeof record.outputDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.outputDigest)
|
|
92
|
+
|| typeof record.errorDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.errorDigest)) {
|
|
93
|
+
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
94
|
+
}
|
|
95
|
+
const summary = JSON.stringify([
|
|
96
|
+
record.requestFamily ?? null, record.requestGeneration, record.exitCode,
|
|
97
|
+
record.outputBytes, record.errorBytes, record.outputDigest, record.errorDigest
|
|
98
|
+
]);
|
|
99
|
+
if (selectedSummary !== null && summary !== selectedSummary) {
|
|
100
|
+
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_CONFLICT');
|
|
101
|
+
}
|
|
102
|
+
selected ??= record;
|
|
103
|
+
selectedSummary ??= summary;
|
|
104
|
+
}
|
|
105
|
+
return selected;
|
|
106
|
+
}
|
|
107
|
+
function verifiedResponse(manifest, requestId, records) {
|
|
108
|
+
assertDirectory(manifest.channelDir);
|
|
109
|
+
const responsesDir = path.join(manifest.channelDir, 'responses');
|
|
110
|
+
assertDirectory(responsesDir);
|
|
111
|
+
const responsePath = path.join(responsesDir, `${requestId}.json`);
|
|
112
|
+
if (!fs.existsSync(responsePath))
|
|
113
|
+
return null;
|
|
114
|
+
const responseStat = fs.lstatSync(responsePath);
|
|
115
|
+
if (!responseStat.isFile() || responseStat.isSymbolicLink())
|
|
116
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
117
|
+
const raw = fs.readFileSync(responsePath, 'utf8');
|
|
118
|
+
let response;
|
|
119
|
+
try {
|
|
120
|
+
response = JSON.parse(raw);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
124
|
+
}
|
|
125
|
+
if (!response || response.version !== 2 || response.id !== requestId)
|
|
126
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
127
|
+
if (response.phase === 'accepted')
|
|
128
|
+
return null;
|
|
129
|
+
if (!['completed', 'rejected'].includes(response.phase))
|
|
130
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
131
|
+
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
|
|
132
|
+
return unknown(requestId);
|
|
133
|
+
const evidencePath = path.join(manifest.processingDir, requestId, 'result.json');
|
|
134
|
+
const published = publishedEvidence(records, manifest.generation);
|
|
135
|
+
if (!published)
|
|
136
|
+
return unknown(requestId);
|
|
137
|
+
let evidence = null;
|
|
138
|
+
if (fs.existsSync(evidencePath)) {
|
|
139
|
+
evidence = readSandboxControlResultEvidence(evidencePath);
|
|
140
|
+
if (evidence.id !== requestId || evidence.generation !== manifest.generation
|
|
141
|
+
|| evidence.exitCode !== published.exitCode || evidence.stdoutBytes !== published.outputBytes
|
|
142
|
+
|| evidence.stderrBytes !== published.errorBytes || evidence.stdoutSha256 !== published.outputDigest
|
|
143
|
+
|| evidence.stderrSha256 !== published.errorDigest)
|
|
144
|
+
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
145
|
+
}
|
|
146
|
+
if (response.phase !== 'completed' || response.exitCode !== published.exitCode || response.error !== null) {
|
|
147
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
148
|
+
}
|
|
149
|
+
if (response.outputState === undefined) {
|
|
150
|
+
const stdout = response.stdout;
|
|
151
|
+
const stderr = response.stderr;
|
|
152
|
+
if (response.payload !== undefined
|
|
153
|
+
|| Buffer.byteLength(stdout, 'utf8') !== published.outputBytes
|
|
154
|
+
|| Buffer.byteLength(stderr, 'utf8') !== published.errorBytes
|
|
155
|
+
|| createHash('sha256').update(stdout, 'utf8').digest('hex') !== published.outputDigest
|
|
156
|
+
|| createHash('sha256').update(stderr, 'utf8').digest('hex') !== published.errorDigest) {
|
|
157
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
158
|
+
}
|
|
159
|
+
return response;
|
|
160
|
+
}
|
|
161
|
+
if (response.outputState === 'available') {
|
|
162
|
+
const payload = readSandboxControlPayload(path.join(manifest.channelDir, 'responses', `${requestId}.payload.json`));
|
|
163
|
+
if (payload.id !== requestId || payload.generation !== manifest.generation
|
|
164
|
+
|| payload.stdoutBytes !== published.outputBytes || payload.stderrBytes !== published.errorBytes
|
|
165
|
+
|| payload.stdoutSha256 !== published.outputDigest || payload.stderrSha256 !== published.errorDigest
|
|
166
|
+
|| response.payload?.id !== payload.id || response.payload?.generation !== payload.generation
|
|
167
|
+
|| response.payload?.stdoutBytes !== payload.stdoutBytes || response.payload?.stderrBytes !== payload.stderrBytes
|
|
168
|
+
|| response.payload?.stdoutSha256 !== payload.stdoutSha256 || response.payload?.stderrSha256 !== payload.stderrSha256) {
|
|
169
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
170
|
+
}
|
|
171
|
+
return { ...response, stdout: payload.stdout, stderr: payload.stderr };
|
|
172
|
+
}
|
|
173
|
+
const expectedStdout = published.requestFamily === 'task-create'
|
|
174
|
+
? `${JSON.stringify(taskCreateOutputUnavailableResult(requestId))}\n`
|
|
175
|
+
: '';
|
|
176
|
+
if (response.outputState !== 'unavailable' || response.payload !== null
|
|
177
|
+
|| response.stdout !== expectedStdout || response.stderr !== 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: broker restarted after executor completion\n'
|
|
178
|
+
&& response.stderr !== 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: output payload was not retained\n') {
|
|
179
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
180
|
+
}
|
|
181
|
+
return response;
|
|
182
|
+
}
|
|
183
|
+
export function recoverSandboxControlFromHost(requestId, params) {
|
|
184
|
+
if (!REQUEST_ID.test(requestId))
|
|
185
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
|
|
186
|
+
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
187
|
+
let candidates = [];
|
|
188
|
+
while (Date.now() < deadline) {
|
|
189
|
+
candidates = [];
|
|
190
|
+
for (const manifestPath of managedManifestPaths(params.managedRoot)) {
|
|
191
|
+
let manifest;
|
|
192
|
+
try {
|
|
193
|
+
manifest = readSandboxControlManifest(manifestPath);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
197
|
+
}
|
|
198
|
+
if (manifest.project !== path.basename(path.resolve(params.managedRoot)) || !manifest.generation) {
|
|
199
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
200
|
+
}
|
|
201
|
+
const records = auditProof(manifest, requestId);
|
|
202
|
+
if (records.length)
|
|
203
|
+
candidates.push({ manifest, records });
|
|
204
|
+
}
|
|
205
|
+
if (candidates.length > 1)
|
|
206
|
+
throw new Error('SANDBOX_CONTROL_RECOVERY_AMBIGUOUS');
|
|
207
|
+
if (candidates.length === 1) {
|
|
208
|
+
const response = verifiedResponse(candidates[0].manifest, requestId, candidates[0].records);
|
|
209
|
+
if (response)
|
|
210
|
+
return response;
|
|
211
|
+
}
|
|
212
|
+
const remainingMs = deadline - Date.now();
|
|
213
|
+
if (remainingMs > 0)
|
|
214
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(25, remainingMs));
|
|
215
|
+
}
|
|
216
|
+
return unknown(requestId);
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=host-recovery.js.map
|
|
@@ -111,7 +111,7 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
|
|
|
111
111
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
|
|
112
112
|
}
|
|
113
113
|
const request = value;
|
|
114
|
-
if (request.version !==
|
|
114
|
+
if (request.version !== 4 || typeof request.id !== 'string'
|
|
115
115
|
|| !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
|
|
116
116
|
|| typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
|
|
117
117
|
|| !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt))
|
|
@@ -158,12 +158,11 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
|
|
|
158
158
|
return request;
|
|
159
159
|
}
|
|
160
160
|
if (request.family === 'task-finalization') {
|
|
161
|
-
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', '
|
|
161
|
+
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
162
162
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
163
163
|
|| request.operation !== 'complete'
|
|
164
164
|
|| !Array.isArray(request.args) || request.args.length !== 0
|
|
165
165
|
|| typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
|
|
166
|
-
|| typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
|
|
167
166
|
|| request.controllerProcess !== null || request.controllerProof !== null) {
|
|
168
167
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
|
|
169
168
|
}
|
|
@@ -20,8 +20,35 @@ import { nextSandboxControlBackoff } from './timing.js';
|
|
|
20
20
|
import { readTaskFinalizationReceipt } from '../../task/finalization.js';
|
|
21
21
|
import { readCodexControllerRegistration } from './controller-registration.js';
|
|
22
22
|
import { validateSandboxControlIdentity } from './identity-sentinel.js';
|
|
23
|
-
import { mergeSandboxTaskView, taskViewForManifest } from './task-view.js';
|
|
23
|
+
import { mergeSandboxTaskView, taskViewAfterFinalization, taskViewForManifest } from './task-view.js';
|
|
24
24
|
import { taskCreateOutputUnavailableResult } from '../../task/create-service.js';
|
|
25
|
+
async function taskViewForFinalizationResult(manifest, request, result) {
|
|
26
|
+
if (request.family !== 'task-finalization' || result?.exitCode !== 0 || !manifest.taskId)
|
|
27
|
+
return null;
|
|
28
|
+
let receipt = null;
|
|
29
|
+
for (let attempt = 0; attempt < 100 && !receipt; attempt += 1) {
|
|
30
|
+
try {
|
|
31
|
+
receipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// A missing or temporarily moving task directory is retried before publishing completion.
|
|
35
|
+
}
|
|
36
|
+
if (!receipt)
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
38
|
+
}
|
|
39
|
+
const finalized = taskViewAfterFinalization({
|
|
40
|
+
taskId: manifest.taskId,
|
|
41
|
+
generation: manifest.generation,
|
|
42
|
+
requestId: request.id,
|
|
43
|
+
receipt
|
|
44
|
+
});
|
|
45
|
+
if (finalized.state === 'unknown')
|
|
46
|
+
return finalized;
|
|
47
|
+
const canonical = taskViewForManifest({ ...manifest, receipt });
|
|
48
|
+
return canonical.state === 'current' && canonical.observedSource === 'completed'
|
|
49
|
+
? canonical
|
|
50
|
+
: finalized;
|
|
51
|
+
}
|
|
25
52
|
function appendBrokerAudit(manifest, event, fields = {}) {
|
|
26
53
|
appendDiagnosticAudit(manifest, event, { source: 'broker', ...fields });
|
|
27
54
|
}
|
|
@@ -722,6 +749,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
722
749
|
// A fresh control root has no previous task-view projection.
|
|
723
750
|
}
|
|
724
751
|
let lastState = '';
|
|
752
|
+
let lastTaskView = '';
|
|
725
753
|
let lastStatusAt = 0;
|
|
726
754
|
let nextBindingCheckAt = 0;
|
|
727
755
|
let nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
|
|
@@ -838,6 +866,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
838
866
|
settledExecution = active;
|
|
839
867
|
active = null;
|
|
840
868
|
}
|
|
869
|
+
const finalizationView = settledExecution
|
|
870
|
+
? await taskViewForFinalizationResult(manifest, settledExecution.request, settledExecution.result)
|
|
871
|
+
: null;
|
|
872
|
+
if (finalizationView)
|
|
873
|
+
taskView = finalizationView;
|
|
841
874
|
let reasonCode = null;
|
|
842
875
|
try {
|
|
843
876
|
if (!brokerOwns())
|
|
@@ -881,9 +914,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
881
914
|
}
|
|
882
915
|
const state = reasonCode ? 'parked' : active ? 'busy' : 'healthy';
|
|
883
916
|
const stateKey = `${state}:${reasonCode ?? ''}:${active?.request.id ?? ''}`;
|
|
884
|
-
|
|
917
|
+
const taskViewKey = JSON.stringify(taskView);
|
|
918
|
+
if (brokerOwns() && (stateKey !== lastState || taskViewKey !== lastTaskView || now - lastStatusAt >= timing.controlTickMs)) {
|
|
885
919
|
writeSandboxControlStatus(manifest, broker, state, reasonCode, active?.request.id ?? null, now, taskView);
|
|
886
920
|
lastStatusAt = now;
|
|
921
|
+
lastTaskView = taskViewKey;
|
|
887
922
|
}
|
|
888
923
|
if (brokerOwns() && stateKey !== lastState) {
|
|
889
924
|
appendBrokerAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
|
|
@@ -1156,6 +1191,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
1156
1191
|
}
|
|
1157
1192
|
}
|
|
1158
1193
|
if (owned && active.result && active.resultEvidenceWritten) {
|
|
1194
|
+
const finalizationView = await taskViewForFinalizationResult(manifest, active.request, active.result);
|
|
1195
|
+
if (finalizationView) {
|
|
1196
|
+
taskView = finalizationView;
|
|
1197
|
+
writeSandboxControlStatus(manifest, broker, 'healthy', null, null, Date.now(), taskView);
|
|
1198
|
+
}
|
|
1159
1199
|
if (publishExecutionResult(manifest, active.request, active.result, brokerOwns)) {
|
|
1160
1200
|
if (brokerOwns()) {
|
|
1161
1201
|
removeAcceptedResponse(manifest, active.request.id);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
|
|
4
|
+
export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
|
|
5
|
+
const TASK_MARKER_KEYS = [
|
|
6
|
+
'AGENT_INFRA_SANDBOX',
|
|
7
|
+
'AGENT_INFRA_TASK_ID',
|
|
8
|
+
'AGENT_INFRA_CONTROL_TOKEN',
|
|
9
|
+
'AGENT_INFRA_CONTROL_GENERATION',
|
|
10
|
+
'AGENT_INFRA_CONTROL_DIR',
|
|
11
|
+
'AGENT_INFRA_CONTROL_STATUS_DIR',
|
|
12
|
+
'AGENT_INFRA_RUNTIME_DIR'
|
|
13
|
+
];
|
|
14
|
+
const TASK_CONTROL_MARKER_KEYS = [
|
|
15
|
+
'AGENT_INFRA_CONTROL_TOKEN',
|
|
16
|
+
'AGENT_INFRA_CONTROL_GENERATION',
|
|
17
|
+
'AGENT_INFRA_CONTROL_DIR',
|
|
18
|
+
'AGENT_INFRA_CONTROL_STATUS_DIR'
|
|
19
|
+
];
|
|
20
|
+
const TASK_CONTROL_CONFIG_KEYS = [
|
|
21
|
+
...TASK_CONTROL_MARKER_KEYS,
|
|
22
|
+
'AGENT_INFRA_CONTROL_ROOT_ID'
|
|
23
|
+
];
|
|
24
|
+
function nativeDirectoryProbe(candidate) {
|
|
25
|
+
if (process.platform === 'linux') {
|
|
26
|
+
try {
|
|
27
|
+
// Linux sandboxes use this fixed mount as a host/sandbox trust anchor.
|
|
28
|
+
// Do not let a preload replace the ordinary node:fs probe and select host routing.
|
|
29
|
+
const binding = process.binding('fs');
|
|
30
|
+
if (!Function.prototype.toString.call(binding.internalModuleStat).includes('[native code]'))
|
|
31
|
+
return 'unknown';
|
|
32
|
+
const result = binding.internalModuleStat(candidate);
|
|
33
|
+
if (result === 1)
|
|
34
|
+
return 'present';
|
|
35
|
+
if (result === -2)
|
|
36
|
+
return 'absent';
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.lstatSync(candidate);
|
|
45
|
+
return stat.isDirectory() && !stat.isSymbolicLink() ? 'present' : 'unknown';
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error && typeof error === 'object' && 'code' in error
|
|
49
|
+
&& (error.code === 'ENOENT' || error.code === 'ENOTDIR'))
|
|
50
|
+
return 'absent';
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function sandboxTaskMarkerState(env) {
|
|
55
|
+
const present = (key) => Boolean(env[key]);
|
|
56
|
+
const sandbox = env.AGENT_INFRA_SANDBOX === '1';
|
|
57
|
+
const taskId = present('AGENT_INFRA_TASK_ID');
|
|
58
|
+
const controls = TASK_CONTROL_MARKER_KEYS.every(present);
|
|
59
|
+
const runtime = present('AGENT_INFRA_RUNTIME_DIR');
|
|
60
|
+
const any = TASK_MARKER_KEYS.some(present);
|
|
61
|
+
if (!any)
|
|
62
|
+
return 'none';
|
|
63
|
+
if (!sandbox || !controls)
|
|
64
|
+
return 'incomplete';
|
|
65
|
+
if (taskId && runtime)
|
|
66
|
+
return 'task-bound';
|
|
67
|
+
if (!taskId && !runtime)
|
|
68
|
+
return 'branch-only';
|
|
69
|
+
return 'incomplete';
|
|
70
|
+
}
|
|
71
|
+
export function hasTaskBoundMarker(env = process.env) {
|
|
72
|
+
return sandboxTaskMarkerState(env) === 'task-bound';
|
|
73
|
+
}
|
|
74
|
+
export function hasSandboxControlMarker(env = process.env) {
|
|
75
|
+
return TASK_CONTROL_MARKER_KEYS.some((key) => Boolean(env[key]));
|
|
76
|
+
}
|
|
77
|
+
export function resolveSandboxControlTransport(env = process.env, options = {}) {
|
|
78
|
+
const markerState = sandboxTaskMarkerState(env);
|
|
79
|
+
const hasAnyMarker = markerState !== 'none'
|
|
80
|
+
|| Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
|
|
81
|
+
|| Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
|
|
82
|
+
const fixedStatusDir = options.statusMountPath ?? SANDBOX_CONTROL_STATUS_MOUNT;
|
|
83
|
+
const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
|
|
84
|
+
? nativeDirectoryProbe(fixedStatusDir) : 'absent';
|
|
85
|
+
if (fixedStatusProbe === 'unknown') {
|
|
86
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
|
|
87
|
+
}
|
|
88
|
+
const fixedStatusMounted = fixedStatusProbe === 'present';
|
|
89
|
+
if (!hasAnyMarker) {
|
|
90
|
+
return fixedStatusMounted
|
|
91
|
+
? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' }
|
|
92
|
+
: { kind: 'direct-host', reasonCode: null };
|
|
93
|
+
}
|
|
94
|
+
if (markerState === 'incomplete') {
|
|
95
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
96
|
+
}
|
|
97
|
+
const hasCompleteConfig = TASK_CONTROL_CONFIG_KEYS.every((key) => Boolean(env[key]));
|
|
98
|
+
if (!hasCompleteConfig) {
|
|
99
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
100
|
+
}
|
|
101
|
+
const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
102
|
+
const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
|
|
103
|
+
? nativeDirectoryProbe(configuredStatusDir) : 'absent';
|
|
104
|
+
if (configuredStatusProbe === 'unknown') {
|
|
105
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
|
|
106
|
+
}
|
|
107
|
+
const statusDir = configuredStatusDir && path.isAbsolute(configuredStatusDir)
|
|
108
|
+
? configuredStatusProbe === 'present' ? configuredStatusDir : null
|
|
109
|
+
: fixedStatusMounted ? fixedStatusDir : null;
|
|
110
|
+
const statusMounted = statusDir !== null;
|
|
111
|
+
if (env.AGENT_INFRA_EXECUTOR_MANIFEST || env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING) {
|
|
112
|
+
return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
|
|
113
|
+
}
|
|
114
|
+
const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
|
|
115
|
+
const runtime = Boolean(env.AGENT_INFRA_RUNTIME_DIR);
|
|
116
|
+
if (statusMounted) {
|
|
117
|
+
let sentinel;
|
|
118
|
+
try {
|
|
119
|
+
sentinel = readSandboxControlIdentitySentinel(statusDir);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : '';
|
|
123
|
+
return { kind: 'fail-closed', reasonCode: message.endsWith('MISSING')
|
|
124
|
+
? 'SANDBOX_CONTROL_IDENTITY_MISSING'
|
|
125
|
+
: 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
|
|
126
|
+
}
|
|
127
|
+
if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
|
|
128
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
|
|
129
|
+
}
|
|
130
|
+
if (sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
|
|
131
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
|
|
132
|
+
}
|
|
133
|
+
if (sentinel.mode === 'task-bound'
|
|
134
|
+
&& (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
|
|
135
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
|
|
136
|
+
}
|
|
137
|
+
if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
|
|
138
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
|
|
139
|
+
}
|
|
140
|
+
return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
|
|
141
|
+
}
|
|
142
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' };
|
|
143
|
+
}
|
|
144
|
+
export function isSandbox(env = process.env, options = {}) {
|
|
145
|
+
const decision = resolveSandboxControlTransport(env, options);
|
|
146
|
+
if (decision.kind === 'fail-closed')
|
|
147
|
+
throw new Error(decision.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID');
|
|
148
|
+
return decision.kind !== 'direct-host';
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=environment.js.map
|
|
@@ -567,6 +567,13 @@ function expectedMounts(params) {
|
|
|
567
567
|
hostPaths: toolConfigDirCandidates(tool, config.project, branch),
|
|
568
568
|
expectedRW: true
|
|
569
569
|
}));
|
|
570
|
+
const hostState = tools.flatMap((tool) => (tool.hostStateMounts ?? []).map(({ hostSubdir, containerSubpath }) => ({
|
|
571
|
+
path: path.posix.join(tool.containerMount, containerSubpath),
|
|
572
|
+
expectedType: 'bind',
|
|
573
|
+
hostPaths: toolConfigDirCandidates(tool, config.project, branch)
|
|
574
|
+
.map((candidate) => path.join(candidate, hostSubdir)),
|
|
575
|
+
expectedRW: true
|
|
576
|
+
})));
|
|
570
577
|
const staging = tools.flatMap((tool) => (tool.tmpfs?.seed ?? []).flatMap((seedEntry, index) => {
|
|
571
578
|
const stagingPath = `/run/agent-infra/tmpfs-seeds/${tool.id}/${index}`;
|
|
572
579
|
if (!actualMounts.has(stagingPath))
|
|
@@ -583,6 +590,7 @@ function expectedMounts(params) {
|
|
|
583
590
|
...core,
|
|
584
591
|
...persistentTools,
|
|
585
592
|
...live,
|
|
593
|
+
...hostState,
|
|
586
594
|
...staging,
|
|
587
595
|
...tools
|
|
588
596
|
.filter((tool) => tool.tmpfs)
|
|
@@ -79,20 +79,6 @@ export function resolveSandboxTarget(requestedRef, repoRoot) {
|
|
|
79
79
|
}
|
|
80
80
|
return { requestedRef, branch: requestedRef, workspace: { mode: 'branch-only' } };
|
|
81
81
|
}
|
|
82
|
-
export function resolveSandboxReentryContext(params) {
|
|
83
|
-
if (params.target.workspace.mode !== 'branch-only'
|
|
84
|
-
|| params.containerWorkspace.mode !== 'task-bound') {
|
|
85
|
-
return { workspace: params.target.workspace, reentry: 'standard' };
|
|
86
|
-
}
|
|
87
|
-
const task = resolveTaskWorkspace(params.containerWorkspace.taskId, params.repoRoot);
|
|
88
|
-
if (task.state !== 'completed' || task.branch !== params.target.branch) {
|
|
89
|
-
return { workspace: params.target.workspace, reentry: 'standard' };
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
workspace: { mode: 'task-bound', taskId: task.taskId },
|
|
93
|
-
reentry: 'completed'
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
82
|
export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}) {
|
|
97
83
|
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
98
84
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { resolveTaskWorkspace } from './task-resolver.js';
|
|
4
5
|
import { createSandboxControlIdentitySentinel, writeSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
|
|
5
6
|
export const SANDBOX_WORKSPACE_VIEW_STATES = Object.freeze([
|
|
6
7
|
'active',
|
|
@@ -73,15 +74,38 @@ function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot)
|
|
|
73
74
|
export function assertSandboxTaskSource(repoRoot, taskId) {
|
|
74
75
|
if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
75
76
|
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
76
|
-
const
|
|
77
|
-
|
|
77
|
+
const activeSource = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
78
|
+
try {
|
|
79
|
+
const stat = fs.lstatSync(activeSource);
|
|
80
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
81
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${activeSource} must be a real directory`);
|
|
82
|
+
}
|
|
83
|
+
const canonical = fs.realpathSync.native(activeSource);
|
|
84
|
+
const activeRoot = fs.realpathSync.native(path.dirname(activeSource));
|
|
85
|
+
const workspaceRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
|
|
86
|
+
const relative = path.relative(workspaceRoot, canonical);
|
|
87
|
+
if (path.dirname(canonical) !== activeRoot || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
88
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${activeSource} escapes the task workspace`);
|
|
89
|
+
}
|
|
90
|
+
return canonical;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code !== 'ENOENT')
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
const task = resolveTaskWorkspace(taskId, repoRoot);
|
|
97
|
+
const source = task.state === 'archive'
|
|
98
|
+
? path.dirname(path.dirname(task.taskMd))
|
|
99
|
+
: path.dirname(task.taskMd);
|
|
78
100
|
const stat = fs.lstatSync(source);
|
|
79
101
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
80
102
|
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} must be a real directory`);
|
|
81
103
|
}
|
|
82
104
|
const canonical = fs.realpathSync.native(source);
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
const workspaceRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
|
|
106
|
+
const relative = path.relative(workspaceRoot, canonical);
|
|
107
|
+
if (path.basename(canonical) !== taskId || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
108
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the task workspace`);
|
|
85
109
|
}
|
|
86
110
|
return canonical;
|
|
87
111
|
}
|
|
@@ -114,10 +138,13 @@ export function materializeSandboxWorkspaceView(params) {
|
|
|
114
138
|
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
115
139
|
}
|
|
116
140
|
const active = path.join(root, 'active');
|
|
141
|
+
const shortId = params.identity.mode === 'task-bound' && 'shortId' in params.identity
|
|
142
|
+
? params.identity.shortId
|
|
143
|
+
: null;
|
|
117
144
|
const registry = {
|
|
118
145
|
version: 1,
|
|
119
|
-
ids: params.identity.mode === 'task-bound'
|
|
120
|
-
? { [
|
|
146
|
+
ids: shortId !== null && params.identity.mode === 'task-bound'
|
|
147
|
+
? { [shortId]: params.identity.taskId }
|
|
121
148
|
: {}
|
|
122
149
|
};
|
|
123
150
|
fs.writeFileSync(path.join(active, '.short-ids.json'), `${JSON.stringify(registry)}\n`, {
|
|
@@ -14,7 +14,7 @@ export function parseArtifactCommand(args) {
|
|
|
14
14
|
}
|
|
15
15
|
const fields = {};
|
|
16
16
|
const allowed = {
|
|
17
|
-
inspect: ['--family', '--source-finding', '--source-artifact', '--source-sha256'],
|
|
17
|
+
inspect: ['--family', '--reason-code', '--source-finding', '--source-artifact', '--source-sha256'],
|
|
18
18
|
init: ['--family', '--artifact', '--locale'],
|
|
19
19
|
'preflight': ['--family', '--artifact'],
|
|
20
20
|
'finalize-local': ['--family', '--artifact']
|
|
@@ -37,9 +37,13 @@ export function parseArtifactCommand(args) {
|
|
|
37
37
|
const locale = fields['--locale'];
|
|
38
38
|
if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en')
|
|
39
39
|
throw new Error("option '--locale' must be 'zh-CN' or 'en'");
|
|
40
|
+
const reasonCode = fields['--reason-code'];
|
|
41
|
+
if (reasonCode !== undefined && !['user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'manual-review-supplement', 'retry', 'validation-rerun'].includes(reasonCode)) {
|
|
42
|
+
throw new Error("option '--reason-code' is invalid");
|
|
43
|
+
}
|
|
40
44
|
return {
|
|
41
45
|
taskRef, operation: operation, family: fields['--family'] ?? '',
|
|
42
|
-
artifact: fields['--artifact'] ?? '', locale,
|
|
46
|
+
artifact: fields['--artifact'] ?? '', locale, reasonCode: reasonCode,
|
|
43
47
|
sourceFinding: fields['--source-finding'], sourceArtifact: fields['--source-artifact'],
|
|
44
48
|
sourceSha256: fields['--source-sha256']
|
|
45
49
|
};
|
|
@@ -51,6 +55,7 @@ export function executeArtifactCommand(command, options = {}) {
|
|
|
51
55
|
if (operation === 'inspect') {
|
|
52
56
|
const result = resolveArtifactContext(taskRef, family, {
|
|
53
57
|
repoRoot: options.repoRoot,
|
|
58
|
+
reasonCode: command.reasonCode,
|
|
54
59
|
sourceFinding: command.sourceFinding,
|
|
55
60
|
sourceArtifact: command.sourceArtifact,
|
|
56
61
|
sourceSha256: command.sourceSha256
|
|
@@ -292,7 +292,8 @@ const OPTIONAL_CONTEXT = {
|
|
|
292
292
|
function hasSelectionEvidence(content, family, options, context, pathState) {
|
|
293
293
|
if (options.sourceFinding && options.sourceArtifact && options.sourceSha256)
|
|
294
294
|
return true;
|
|
295
|
-
if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt'
|
|
295
|
+
if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt'
|
|
296
|
+
|| family === 'review-code' && options.reasonCode === 'manual-review-supplement')
|
|
296
297
|
return true;
|
|
297
298
|
if ((family === 'plan' || family === 'code') && context.latest) {
|
|
298
299
|
const latestInputs = Object.fromEntries(context.inputs.map((input) => [input.family, input.name]));
|
|
@@ -17,7 +17,7 @@ import { inspectArtifactDirectory } from './artifact-lifecycle.js';
|
|
|
17
17
|
import { executionInputsMatchLatest as executionReceiptsMatchLatest } from './execution-inputs.js';
|
|
18
18
|
const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
|
|
19
19
|
const REASONS = new Set([
|
|
20
|
-
'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'
|
|
20
|
+
'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'manual-review-supplement', 'retry', 'validation-rerun'
|
|
21
21
|
]);
|
|
22
22
|
function deny(reasonCode, ...evidence) {
|
|
23
23
|
return { allowed: false, reasonCode, evidence };
|
|
@@ -40,6 +40,9 @@ function canStart(action, facts, trigger) {
|
|
|
40
40
|
return deny('TRIGGER_REQUEST_ID_REQUIRED');
|
|
41
41
|
if (!REASONS.has(trigger.reasonCode))
|
|
42
42
|
return deny('TRIGGER_REASON_INVALID');
|
|
43
|
+
if (trigger.reasonCode === 'manual-review-supplement' && (action !== 'review-code' || trigger.initiator !== 'human')) {
|
|
44
|
+
return deny('MANUAL_REVIEW_SUPPLEMENT_REQUIRES_HUMAN_REVIEW_CODE');
|
|
45
|
+
}
|
|
43
46
|
if (trigger.sourceArtifact && !trigger.sourceSha256)
|
|
44
47
|
return deny('SOURCE_ARTIFACT_HASH_REQUIRED');
|
|
45
48
|
if (trigger.sourceSha256 && !trigger.sourceArtifact)
|