@fitlab-ai/agent-infra 0.11.1 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +2 -171
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +7 -2
- package/dist/lib/internal/task-review.js +5 -5
- package/dist/lib/internal/task-validate.js +0 -6
- package/dist/lib/platform/comment-safety.js +7 -1
- package/dist/lib/platform/github-provider.js +13 -3
- package/dist/lib/platform/issue-comments.js +93 -15
- package/dist/lib/platform/pr-summary.js +7 -2
- package/dist/lib/platform/provider-contract.d.ts +2 -0
- package/dist/lib/platform/provider-validation.js +7 -2
- package/dist/lib/platform/verification-sync.js +19 -2
- package/dist/lib/sandbox/control/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -117
- package/dist/lib/sandbox/control/finalization-response.js +8 -2
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +29 -206
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +2 -1
- package/dist/lib/task/artifact-command.js +6 -6
- package/dist/lib/task/codex-orchestration.js +10 -208
- package/dist/lib/task/commit-operation.js +3 -18
- package/dist/lib/task/control-authority.js +35 -44
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/delegation-receipts.js +16 -78
- package/dist/lib/task/events.js +89 -237
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +208 -57
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/lifecycle.js +1 -1
- package/dist/lib/task/local-artifact-finalization.js +25 -242
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +12 -24
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +46 -211
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +1 -185
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +8 -4
- package/lib/internal/task-review.ts +5 -5
- package/lib/internal/task-validate.ts +0 -5
- package/lib/platform/comment-safety.ts +8 -0
- package/lib/platform/github-provider.ts +11 -2
- package/lib/platform/issue-comments.ts +102 -17
- package/lib/platform/pr-summary.ts +10 -2
- package/lib/platform/provider-contract.ts +2 -0
- package/lib/platform/provider-validation.ts +6 -2
- package/lib/platform/verification-sync.ts +19 -2
- package/lib/sandbox/control/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -125
- package/lib/sandbox/control/finalization-response.ts +8 -2
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +30 -220
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +2 -1
- package/lib/task/artifact-command.ts +6 -7
- package/lib/task/codex-orchestration.ts +8 -241
- package/lib/task/commit-operation.ts +3 -23
- package/lib/task/control-authority.ts +39 -55
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/delegation-receipts.ts +14 -81
- package/lib/task/events.ts +106 -255
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +221 -65
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/lifecycle.ts +2 -2
- package/lib/task/local-artifact-finalization.ts +23 -281
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +30 -53
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +45 -193
- package/package.json +2 -2
- package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
- package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -157
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -18
- package/dist/lib/task/workflow-dispatch.js +0 -161
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -183
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -19
- package/lib/task/workflow-dispatch.ts +0 -171
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { reconcileTaskInvalidation } from './invalidation-command.js';
|
|
2
|
-
import { applyLedgerIntent } from './ledger-intents.js';
|
|
3
|
-
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
4
|
-
import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
|
|
5
|
-
function failed(code, message) {
|
|
6
|
-
return { status: 'failed', changed: false, error: { code, message } };
|
|
7
|
-
}
|
|
8
|
-
function isFailure(value) {
|
|
9
|
-
return Boolean(value) && typeof value === 'object' && value.status === 'failed';
|
|
10
|
-
}
|
|
11
|
-
function options(args, allowed) {
|
|
12
|
-
const values = {};
|
|
13
|
-
for (let index = 2; index < args.length; index += 1) {
|
|
14
|
-
const flag = args[index];
|
|
15
|
-
if (flag === '--dry-run') {
|
|
16
|
-
if (!Object.hasOwn(allowed, flag) || values[flag] !== undefined)
|
|
17
|
-
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', `invalid option '${flag}'`);
|
|
18
|
-
values[flag] = true;
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
const key = allowed[flag];
|
|
22
|
-
if (!key || values[flag] !== undefined)
|
|
23
|
-
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', `invalid option '${flag}'`);
|
|
24
|
-
const value = args[++index];
|
|
25
|
-
if (value === undefined || value.startsWith('--'))
|
|
26
|
-
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', `option '${flag}' requires a value`);
|
|
27
|
-
if (flag === '--needs-implementation' && value !== 'true' && value !== 'false') {
|
|
28
|
-
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', "option '--needs-implementation' must be true or false");
|
|
29
|
-
}
|
|
30
|
-
values[flag] = value;
|
|
31
|
-
}
|
|
32
|
-
return values;
|
|
33
|
-
}
|
|
34
|
-
function value(values, flag) {
|
|
35
|
-
const candidate = values[flag];
|
|
36
|
-
return typeof candidate === 'string' ? candidate : undefined;
|
|
37
|
-
}
|
|
38
|
-
function required(values, flags) {
|
|
39
|
-
const missing = flags.find((flag) => value(values, flag) === undefined);
|
|
40
|
-
return missing ? failed('TASK_WORKFLOW_PAYLOAD_INVALID', `option '${missing}' is required`) : null;
|
|
41
|
-
}
|
|
42
|
-
function resultForLock(error) {
|
|
43
|
-
if (error instanceof TaskExecutionLockError)
|
|
44
|
-
return failed(error.code, error.message);
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
function ledgerIntent(operation, args) {
|
|
48
|
-
const [taskRef, kind] = args;
|
|
49
|
-
const mappings = {
|
|
50
|
-
'ledger-finding-response': {
|
|
51
|
-
kind: 'finding-respond', required: ['--id', '--round', '--status', '--evidence'],
|
|
52
|
-
flags: { '--id': 'id', '--round': 'round', '--status': 'status', '--evidence': 'evidence', '--dry-run': 'dryRun' }
|
|
53
|
-
},
|
|
54
|
-
'ledger-finding-review': {
|
|
55
|
-
kind: 'finding-review', required: ['--id', '--status', '--evidence'],
|
|
56
|
-
flags: { '--id': 'id', '--status': 'status', '--evidence': 'evidence', '--needs-implementation': 'needsImplementation', '--dry-run': 'dryRun' }
|
|
57
|
-
},
|
|
58
|
-
'ledger-finding-upsert': {
|
|
59
|
-
kind: 'finding-upsert', required: ['--stage', '--review-artifact', '--ordinal', '--severity', '--evidence'],
|
|
60
|
-
flags: { '--stage': 'stage', '--review-artifact': 'reviewArtifact', '--ordinal': 'ordinal', '--severity': 'severity', '--evidence': 'evidence', '--dry-run': 'dryRun' }
|
|
61
|
-
},
|
|
62
|
-
'decision-next-id': { kind: 'decision-next-id', required: [], flags: {} },
|
|
63
|
-
'decision-upsert': {
|
|
64
|
-
kind: 'decision-upsert', required: ['--id', '--stage', '--artifact'],
|
|
65
|
-
flags: { '--id': 'id', '--stage': 'stage', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation', '--dry-run': 'dryRun' }
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
const mapping = mappings[operation];
|
|
69
|
-
if (!mapping || !taskRef || kind === undefined)
|
|
70
|
-
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow ledger request is invalid');
|
|
71
|
-
const values = options(args, mapping.flags);
|
|
72
|
-
if (isFailure(values))
|
|
73
|
-
return values;
|
|
74
|
-
const missing = required(values, mapping.required);
|
|
75
|
-
if (missing)
|
|
76
|
-
return missing;
|
|
77
|
-
const dryRun = values['--dry-run'] === true;
|
|
78
|
-
if (mapping.kind === 'decision-next-id')
|
|
79
|
-
return { kind: mapping.kind, taskRef };
|
|
80
|
-
if (mapping.kind === 'finding-respond')
|
|
81
|
-
return {
|
|
82
|
-
kind: mapping.kind, taskRef, id: value(values, '--id'), round: Number(value(values, '--round')),
|
|
83
|
-
status: value(values, '--status'), evidence: value(values, '--evidence'), ...(dryRun ? { dryRun } : {})
|
|
84
|
-
};
|
|
85
|
-
if (mapping.kind === 'finding-review')
|
|
86
|
-
return {
|
|
87
|
-
kind: mapping.kind, taskRef, id: value(values, '--id'), status: value(values, '--status'),
|
|
88
|
-
evidence: value(values, '--evidence'), ...(value(values, '--needs-implementation') === 'true' ? { needsImplementation: true } : {}),
|
|
89
|
-
...(dryRun ? { dryRun } : {})
|
|
90
|
-
};
|
|
91
|
-
if (mapping.kind === 'finding-upsert')
|
|
92
|
-
return {
|
|
93
|
-
kind: mapping.kind, taskRef, stage: value(values, '--stage'), reviewArtifact: value(values, '--review-artifact'),
|
|
94
|
-
ordinal: Number(value(values, '--ordinal')), severity: value(values, '--severity'), evidence: value(values, '--evidence'),
|
|
95
|
-
...(dryRun ? { dryRun } : {})
|
|
96
|
-
};
|
|
97
|
-
return {
|
|
98
|
-
kind: mapping.kind, taskRef, id: value(values, '--id'), stage: value(values, '--stage'),
|
|
99
|
-
artifact: value(values, '--artifact'), ...(value(values, '--needs-implementation') === 'true' ? { needsImplementation: true } : {}),
|
|
100
|
-
...(dryRun ? { dryRun } : {})
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
function warningIntent(args) {
|
|
104
|
-
const [taskRef, kind] = args;
|
|
105
|
-
if (!taskRef || kind !== 'add')
|
|
106
|
-
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow warning request is invalid');
|
|
107
|
-
const values = options(args, {
|
|
108
|
-
'--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
|
|
109
|
-
'--message': 'message', '--action': 'action', '--dry-run': 'dryRun'
|
|
110
|
-
});
|
|
111
|
-
if (isFailure(values))
|
|
112
|
-
return values;
|
|
113
|
-
const missing = required(values, ['--step', '--severity', '--code', '--target', '--message', '--action']);
|
|
114
|
-
if (missing)
|
|
115
|
-
return missing;
|
|
116
|
-
return {
|
|
117
|
-
kind: 'add', taskRef, step: value(values, '--step'), severity: value(values, '--severity'),
|
|
118
|
-
code: value(values, '--code'), target: value(values, '--target'), message: value(values, '--message'),
|
|
119
|
-
action: value(values, '--action'), ...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* The caller has already selected and authorized the execution boundary.
|
|
124
|
-
* This function deliberately invokes task domains in that process: starting a
|
|
125
|
-
* second CLI process would discard the broker binding and could misclassify a
|
|
126
|
-
* sandbox process with cleared environment variables as direct-host.
|
|
127
|
-
*/
|
|
128
|
-
export function dispatchWorkflowCommand(repoRoot, operation, args) {
|
|
129
|
-
const taskRef = args[0];
|
|
130
|
-
if (!taskRef)
|
|
131
|
-
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task ref is required');
|
|
132
|
-
try {
|
|
133
|
-
if (operation === 'invalidation-reconcile') {
|
|
134
|
-
const values = options(args, { '--max-targets': 'maxTargets', '--dry-run': 'dryRun' });
|
|
135
|
-
if (isFailure(values))
|
|
136
|
-
return values;
|
|
137
|
-
const maxTargets = value(values, '--max-targets');
|
|
138
|
-
if (maxTargets !== undefined && (!Number.isSafeInteger(Number(maxTargets)) || Number(maxTargets) < 1)) {
|
|
139
|
-
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', '--max-targets must be a positive integer');
|
|
140
|
-
}
|
|
141
|
-
return withTaskExecutionLock(repoRoot, taskRef, 'task-invalidation.reconcile', () => reconcileTaskInvalidation(taskRef, {
|
|
142
|
-
repoRoot, ...(maxTargets === undefined ? {} : { maxTargets: Number(maxTargets) }),
|
|
143
|
-
...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
144
|
-
}));
|
|
145
|
-
}
|
|
146
|
-
if (operation === 'warning-add') {
|
|
147
|
-
const intent = warningIntent(args);
|
|
148
|
-
if (isFailure(intent))
|
|
149
|
-
return intent;
|
|
150
|
-
return withTaskExecutionLock(repoRoot, taskRef, 'task-warning.add', () => applyWorkflowWarningIntent(intent, { repoRoot }));
|
|
151
|
-
}
|
|
152
|
-
const intent = ledgerIntent(operation, args);
|
|
153
|
-
if (isFailure(intent))
|
|
154
|
-
return intent;
|
|
155
|
-
return withTaskExecutionLock(repoRoot, taskRef, `task-ledger.${intent.kind}`, () => applyLedgerIntent(intent, { repoRoot }));
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
return resultForLock(error);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
//# sourceMappingURL=workflow-dispatch.js.map
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
|
|
5
|
-
import { readStableFile, SecureFileError, writeAtomicFile } from '../../task/secure-fs.ts';
|
|
6
|
-
|
|
7
|
-
import { TASK_WORKFLOW_COMMANDS, TASK_WORKFLOW_OPERATIONS, type TaskWorkflowOperation, type WorkflowCommand } from '../../task/workflow-command.ts';
|
|
8
|
-
|
|
9
|
-
export type TaskWorkflowRequest = Readonly<{
|
|
10
|
-
version: 1;
|
|
11
|
-
id: string;
|
|
12
|
-
taskId: string;
|
|
13
|
-
generation: string;
|
|
14
|
-
operation: TaskWorkflowOperation;
|
|
15
|
-
args: readonly string[];
|
|
16
|
-
}>;
|
|
17
|
-
|
|
18
|
-
export function validateTaskWorkflowRequest(value: unknown): TaskWorkflowRequest {
|
|
19
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('TASK_WORKFLOW_REQUEST_INVALID');
|
|
20
|
-
const request = value as TaskWorkflowRequest;
|
|
21
|
-
if (Object.keys(request).sort().join(',') !== 'args,generation,id,operation,taskId,version'
|
|
22
|
-
|| request.version !== 1 || typeof request.id !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(request.id)
|
|
23
|
-
|| typeof request.taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/u.test(request.taskId)
|
|
24
|
-
|| typeof request.generation !== 'string' || !request.generation
|
|
25
|
-
|| !Object.hasOwn(TASK_WORKFLOW_COMMANDS, request.operation)
|
|
26
|
-
|| !Array.isArray(request.args) || request.args.length < 2 || request.args.length > 128
|
|
27
|
-
|| !request.args.every((arg) => typeof arg === 'string' && !/[\r\n]/u.test(arg))
|
|
28
|
-
|| request.args[0] !== request.taskId) throw new Error('TASK_WORKFLOW_REQUEST_INVALID');
|
|
29
|
-
const [, selector] = TASK_WORKFLOW_COMMANDS[request.operation];
|
|
30
|
-
if (selector !== null && request.args[1] !== selector) throw new Error('TASK_WORKFLOW_REQUEST_INVALID');
|
|
31
|
-
return request;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Preserve argv verbatim; the domain command owns option parsing and validation. */
|
|
35
|
-
export function createTaskWorkflowRequest(
|
|
36
|
-
command: WorkflowCommand | undefined,
|
|
37
|
-
args: readonly string[],
|
|
38
|
-
taskId: string,
|
|
39
|
-
generation: string
|
|
40
|
-
): TaskWorkflowRequest {
|
|
41
|
-
const operation = TASK_WORKFLOW_OPERATIONS.find((key) => {
|
|
42
|
-
const [name, selector] = TASK_WORKFLOW_COMMANDS[key];
|
|
43
|
-
return name === command && (selector === null || selector === args[1]);
|
|
44
|
-
});
|
|
45
|
-
if (!operation) throw new Error('TASK_WORKFLOW_OPERATION_UNSUPPORTED');
|
|
46
|
-
return validateTaskWorkflowRequest({ version: 1, id: randomUUID(), taskId, generation, operation, args: [...args] });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function canonicalArtifactName(value: string): boolean {
|
|
50
|
-
return /^(?:analysis|plan|code|review-analysis|review-plan|review-code)(?:-r[1-9]\d*)?\.md$/u.test(value)
|
|
51
|
-
&& path.basename(value) === value;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function assertTaskArtifactDirectory(directory: string): string {
|
|
55
|
-
const resolved = path.resolve(directory);
|
|
56
|
-
let stat: fs.Stats;
|
|
57
|
-
try {
|
|
58
|
-
stat = fs.lstatSync(resolved);
|
|
59
|
-
} catch {
|
|
60
|
-
throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'task directory is unavailable');
|
|
61
|
-
}
|
|
62
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
63
|
-
throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'task directory must be a real directory');
|
|
64
|
-
}
|
|
65
|
-
return resolved;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function readTaskArtifact(
|
|
69
|
-
taskDir: string,
|
|
70
|
-
input: Readonly<{
|
|
71
|
-
artifact: string;
|
|
72
|
-
expectedSha256?: string;
|
|
73
|
-
}>
|
|
74
|
-
): Promise<Readonly<{ artifact: string; bytes: Buffer; sha256: string }>> {
|
|
75
|
-
if (!canonicalArtifactName(input.artifact)) throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'artifact must be a canonical top-level basename');
|
|
76
|
-
const root = assertTaskArtifactDirectory(taskDir);
|
|
77
|
-
const candidate = path.join(root, input.artifact);
|
|
78
|
-
if (path.dirname(candidate) !== root) throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'artifact escapes task directory');
|
|
79
|
-
const stable = await readStableFile(candidate, {
|
|
80
|
-
maxBytes: 1024 * 1024,
|
|
81
|
-
...(input.expectedSha256 ? { expectedSha256: input.expectedSha256 } : {})
|
|
82
|
-
});
|
|
83
|
-
return { artifact: input.artifact, bytes: stable.bytes, sha256: stable.sha256 };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export async function writeTaskArtifact(
|
|
87
|
-
taskDir: string,
|
|
88
|
-
artifact: Readonly<{ artifact: string; bytes: Buffer; expectedSha256?: string }>
|
|
89
|
-
): Promise<void> {
|
|
90
|
-
if (!canonicalArtifactName(artifact.artifact)) throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'artifact must be a canonical top-level basename');
|
|
91
|
-
const root = assertTaskArtifactDirectory(taskDir);
|
|
92
|
-
const target = path.join(root, artifact.artifact);
|
|
93
|
-
if (path.dirname(target) !== root) throw new SecureFileError('TASK_ARTIFACT_WRITE_DENIED', 'artifact escapes task directory');
|
|
94
|
-
await writeAtomicFile(target, artifact.bytes, 0o600, artifact.expectedSha256);
|
|
95
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { TASK_WORKFLOW_COMMANDS } from '../../task/workflow-command.ts';
|
|
2
|
-
|
|
3
|
-
import { parseArtifactCommand, executeArtifactCommand } from '../../task/artifact-command.ts';
|
|
4
|
-
import { parseReviewCommand } from '../../task/review-command.ts';
|
|
5
|
-
import { prepareLocalArtifact, preflightLocalArtifact, commitLocalArtifactProvenance } from '../../task/local-artifact-finalization.ts';
|
|
6
|
-
import {
|
|
7
|
-
prepareReviewSummaryCandidate,
|
|
8
|
-
commitReviewSummaryProvenance,
|
|
9
|
-
preflightReviewSummary
|
|
10
|
-
} from '../../task/review-finalization.ts';
|
|
11
|
-
import { readArtifactRecoveryIntent } from '../../task/artifact-repair-intent.ts';
|
|
12
|
-
import { withTaskExecutionLock } from '../../task/task-execution-lock.ts';
|
|
13
|
-
import { dispatchWorkflowCommand } from '../../task/workflow-dispatch.ts';
|
|
14
|
-
import { assertSandboxTaskSource } from '../workspace-view.ts';
|
|
15
|
-
import { appendDiagnosticAudit } from './audit.ts';
|
|
16
|
-
import {
|
|
17
|
-
readTaskArtifact, type TaskWorkflowRequest
|
|
18
|
-
} from './task-workflow.ts';
|
|
19
|
-
import type { SandboxControlManifest } from './protocol.ts';
|
|
20
|
-
import type { SandboxControlExecutionResult } from './executor.ts';
|
|
21
|
-
import type { LifecycleRecoveryAttestationV1 } from '../../task/control-authority.ts';
|
|
22
|
-
import { applyTaskEvent } from '../../task/events.ts';
|
|
23
|
-
import { parseTaskEventRequest } from '../../internal/task-event.ts';
|
|
24
|
-
|
|
25
|
-
export type TaskWorkflowFaultWindow =
|
|
26
|
-
| 'before-call'
|
|
27
|
-
| 'before-domain-write'
|
|
28
|
-
| 'after-atomic-rename'
|
|
29
|
-
| 'before-result-return';
|
|
30
|
-
|
|
31
|
-
export type TaskWorkflowExecutionOptions = Readonly<{
|
|
32
|
-
/** Deterministic test seam; production callers leave this unset. */
|
|
33
|
-
faultWindow?: TaskWorkflowFaultWindow;
|
|
34
|
-
}>;
|
|
35
|
-
|
|
36
|
-
function executionResult(result: Record<string, unknown>): SandboxControlExecutionResult {
|
|
37
|
-
return { exitCode: result.status === 'failed' || result.status === 'refused' ? 1 : 0, stdout: `${JSON.stringify(result)}\n`, stderr: '' };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** The broker already authorized this host executor; domain operations own validation. */
|
|
41
|
-
export async function executeTaskWorkflow(
|
|
42
|
-
manifest: SandboxControlManifest,
|
|
43
|
-
request: TaskWorkflowRequest,
|
|
44
|
-
lifecycleRecoveryAttestation: LifecycleRecoveryAttestationV1 | null = null,
|
|
45
|
-
options: TaskWorkflowExecutionOptions = {}
|
|
46
|
-
): Promise<SandboxControlExecutionResult> {
|
|
47
|
-
let publicationCommitted = false;
|
|
48
|
-
let recoveryObservation: Readonly<{
|
|
49
|
-
family: 'analysis' | 'plan' | 'code' | 'review-analysis' | 'review-plan' | 'review-code';
|
|
50
|
-
artifact: string;
|
|
51
|
-
wasPassed: boolean;
|
|
52
|
-
}> | null = null;
|
|
53
|
-
const observeRecoveryPublication = (): void => {
|
|
54
|
-
if (!recoveryObservation || recoveryObservation.wasPassed) return;
|
|
55
|
-
const intent = readArtifactRecoveryIntent(
|
|
56
|
-
manifest.repoRoot,
|
|
57
|
-
request.taskId,
|
|
58
|
-
recoveryObservation.family,
|
|
59
|
-
recoveryObservation.artifact
|
|
60
|
-
);
|
|
61
|
-
publicationCommitted ||= intent?.state === 'preflight-ready'
|
|
62
|
-
|| intent?.state === 'preflight-commit-started'
|
|
63
|
-
|| intent?.state === 'preflight-passed'
|
|
64
|
-
|| intent?.state === 'passed'
|
|
65
|
-
|| intent?.state === 'commit-started';
|
|
66
|
-
};
|
|
67
|
-
let faultTriggered = false;
|
|
68
|
-
const fault = (window: TaskWorkflowFaultWindow): void => {
|
|
69
|
-
if (faultTriggered || options.faultWindow !== window) return;
|
|
70
|
-
faultTriggered = true;
|
|
71
|
-
throw new Error(`TASK_WORKFLOW_FAULT_INJECTED:${window}`);
|
|
72
|
-
};
|
|
73
|
-
const response = (result: Record<string, unknown>): SandboxControlExecutionResult => {
|
|
74
|
-
fault('before-result-return');
|
|
75
|
-
return executionResult(result);
|
|
76
|
-
};
|
|
77
|
-
try {
|
|
78
|
-
fault('before-call');
|
|
79
|
-
const taskDir = assertSandboxTaskSource(manifest.repoRoot, request.taskId);
|
|
80
|
-
const [command] = TASK_WORKFLOW_COMMANDS[request.operation];
|
|
81
|
-
if (request.operation === 'event') {
|
|
82
|
-
const eventRequest = parseTaskEventRequest(request.args);
|
|
83
|
-
const boundEventRequest = lifecycleRecoveryAttestation && eventRequest.requestId === undefined
|
|
84
|
-
? { ...eventRequest, requestId: lifecycleRecoveryAttestation.lifecycleRequestId }
|
|
85
|
-
: eventRequest;
|
|
86
|
-
fault('before-domain-write');
|
|
87
|
-
const result = applyTaskEvent(boundEventRequest, {
|
|
88
|
-
repoRoot: manifest.repoRoot,
|
|
89
|
-
lifecycleRecoveryAttestation,
|
|
90
|
-
deferLifecycleRecoveryConsumption: true
|
|
91
|
-
});
|
|
92
|
-
publicationCommitted = result.changed;
|
|
93
|
-
fault('after-atomic-rename');
|
|
94
|
-
return response(result);
|
|
95
|
-
}
|
|
96
|
-
if (command !== 'task-artifact' && command !== 'task-review') {
|
|
97
|
-
// The broker owns this short-lived executor; no global service or worker
|
|
98
|
-
// credential participates in the command's authority or recovery path.
|
|
99
|
-
fault('before-domain-write');
|
|
100
|
-
const result = dispatchWorkflowCommand(manifest.repoRoot, request.operation, request.args);
|
|
101
|
-
publicationCommitted = result.changed === true;
|
|
102
|
-
fault('after-atomic-rename');
|
|
103
|
-
return response(result);
|
|
104
|
-
}
|
|
105
|
-
const input = command === 'task-artifact' ? parseArtifactCommand(request.args) : parseReviewCommand(request.args);
|
|
106
|
-
if (command === 'task-artifact' && 'operation' in input && input.operation === 'inspect') {
|
|
107
|
-
fault('before-domain-write');
|
|
108
|
-
const result = executeArtifactCommand(input, { repoRoot: manifest.repoRoot });
|
|
109
|
-
publicationCommitted = result.changed === true;
|
|
110
|
-
fault('after-atomic-rename');
|
|
111
|
-
return response(result);
|
|
112
|
-
}
|
|
113
|
-
if ('operation' in input && input.operation === 'init') {
|
|
114
|
-
fault('before-domain-write');
|
|
115
|
-
const result = executeArtifactCommand(input, { repoRoot: manifest.repoRoot, artifactDir: taskDir });
|
|
116
|
-
publicationCommitted = result.changed === true;
|
|
117
|
-
fault('after-atomic-rename');
|
|
118
|
-
return response(result);
|
|
119
|
-
}
|
|
120
|
-
return await withTaskExecutionLock(manifest.repoRoot, request.taskId, `sandbox-control.${request.operation}`, async () => {
|
|
121
|
-
const artifact = await readTaskArtifact(taskDir, { artifact: input.artifact });
|
|
122
|
-
const content = artifact.bytes.toString('utf8');
|
|
123
|
-
fault('before-domain-write');
|
|
124
|
-
let result: Record<string, unknown>;
|
|
125
|
-
if ('operation' in input) {
|
|
126
|
-
const { family } = input;
|
|
127
|
-
if (family !== 'analysis' && family !== 'plan' && family !== 'code') throw new Error('ARTIFACT_IDENTITY_INVALID');
|
|
128
|
-
const local = { taskRef: request.taskId, family, artifact: input.artifact, repoRoot: manifest.repoRoot, recoveryId: input.recoveryId, lockAlreadyHeld: true } as const;
|
|
129
|
-
if (input.operation === 'preflight') {
|
|
130
|
-
// Preflight only seals a verified generation. It must not publish the
|
|
131
|
-
// formal artifact, set publicationStarted, or append finalizer audit.
|
|
132
|
-
const result = preflightLocalArtifact(local);
|
|
133
|
-
recoveryObservation = { family, artifact: input.artifact, wasPassed: false };
|
|
134
|
-
observeRecoveryPublication();
|
|
135
|
-
return response(result);
|
|
136
|
-
}
|
|
137
|
-
recoveryObservation = {
|
|
138
|
-
family,
|
|
139
|
-
artifact: input.artifact,
|
|
140
|
-
wasPassed: readArtifactRecoveryIntent(manifest.repoRoot, request.taskId, family, input.artifact)?.state === 'passed'
|
|
141
|
-
};
|
|
142
|
-
const prepared = prepareLocalArtifact(local, content, lifecycleRecoveryAttestation ?? undefined);
|
|
143
|
-
observeRecoveryPublication();
|
|
144
|
-
const commitOptions = { afterPublish: () => fault('after-atomic-rename') };
|
|
145
|
-
if (prepared.result.status === 'failed') return executionResult(commitLocalArtifactProvenance(prepared, commitOptions));
|
|
146
|
-
result = commitLocalArtifactProvenance(prepared, commitOptions);
|
|
147
|
-
} else {
|
|
148
|
-
if (input.overrideTicket) throw new Error('TASK_WORKFLOW_OVERRIDE_UNSUPPORTED');
|
|
149
|
-
const family = `review-${input.stage}` as 'review-analysis' | 'review-plan' | 'review-code';
|
|
150
|
-
if (request.operation === 'review-preflight') {
|
|
151
|
-
const result = preflightReviewSummary(input, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true });
|
|
152
|
-
recoveryObservation = { family, artifact: input.artifact, wasPassed: false };
|
|
153
|
-
observeRecoveryPublication();
|
|
154
|
-
return response(result);
|
|
155
|
-
}
|
|
156
|
-
recoveryObservation = {
|
|
157
|
-
family,
|
|
158
|
-
artifact: input.artifact,
|
|
159
|
-
wasPassed: readArtifactRecoveryIntent(manifest.repoRoot, request.taskId, family, input.artifact)?.state === 'passed'
|
|
160
|
-
};
|
|
161
|
-
const prepared = prepareReviewSummaryCandidate(input, content, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true, startRecovery: true });
|
|
162
|
-
observeRecoveryPublication();
|
|
163
|
-
if (input.dryRun || prepared.result.status === 'planned') return executionResult(prepared.result);
|
|
164
|
-
const commitOptions = { afterPublish: () => fault('after-atomic-rename') };
|
|
165
|
-
if (prepared.result.status === 'failed') return executionResult(commitReviewSummaryProvenance(prepared, manifest.repoRoot, commitOptions));
|
|
166
|
-
result = commitReviewSummaryProvenance(prepared, manifest.repoRoot, commitOptions);
|
|
167
|
-
}
|
|
168
|
-
observeRecoveryPublication();
|
|
169
|
-
publicationCommitted ||= result.changed === true;
|
|
170
|
-
if (result.status !== 'failed') appendDiagnosticAudit(manifest, 'task-workflow-artifact-finalized', {
|
|
171
|
-
requestId: request.id, sandboxTaskId: request.taskId, workflowOperation: request.operation,
|
|
172
|
-
artifact: input.artifact, sha256: result.artifactSha256 as string, semanticDigest: result.semanticDigest as string
|
|
173
|
-
});
|
|
174
|
-
fault('after-atomic-rename');
|
|
175
|
-
return response(result);
|
|
176
|
-
});
|
|
177
|
-
} catch (error) {
|
|
178
|
-
observeRecoveryPublication();
|
|
179
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
-
const code = (error as { code?: string })?.code ?? /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'TASK_WORKFLOW_REQUEST_INVALID';
|
|
181
|
-
return executionResult({ status: 'failed', changed: publicationCommitted ? null : false, error: { code, message } });
|
|
182
|
-
}
|
|
183
|
-
}
|