@fitlab-ai/agent-infra 0.9.15 → 0.11.0
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 +2 -6
- package/README.zh-CN.md +2 -6
- package/bin/internal-cli.sh +0 -13
- package/dist/bin/internal-cli.js +11 -49
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/fs/durable-write.js +2 -0
- package/dist/lib/internal/cli-route-inventory.js +3 -4
- package/dist/lib/internal/manual-validation.js +17 -8
- package/dist/lib/internal/platform-comment.js +60 -2
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-lifecycle.js +4 -4
- package/dist/lib/internal/task-operation-registry.js +22 -13
- package/dist/lib/internal/task-orchestration.js +2 -2
- package/dist/lib/internal/task-review.js +12 -7
- package/dist/lib/platform/activity-recovery.js +159 -0
- package/dist/lib/platform/issue-comments.js +89 -31
- package/dist/lib/platform/issues.js +1 -1
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-summary.js +64 -32
- package/dist/lib/platform/pull-requests.js +3 -3
- package/dist/lib/platform/task-comment-projection.js +70 -0
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +45 -7
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/sandbox/commands/rm.js +21 -2
- package/dist/lib/sandbox/control/executor.js +4 -2
- package/dist/lib/sandbox/control/server.js +29 -9
- package/dist/lib/sandbox/control/state.js +7 -1
- package/dist/lib/sandbox/control/task-workflow.js +1 -1
- package/dist/lib/sandbox/control/workflow-executor.js +87 -35
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/removal.js +116 -1011
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +11 -30
- package/dist/lib/task/artifact-operations.js +17 -231
- package/dist/lib/task/artifact-recovery.js +476 -0
- package/dist/lib/task/artifact-repair-intent.js +90 -39
- package/dist/lib/task/commit-operation.js +31 -4
- package/dist/lib/task/control-authority.js +39 -8
- package/dist/lib/task/control-recovery.js +14 -2
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +165 -23
- package/dist/lib/task/human-override.js +5 -8
- package/dist/lib/task/intermediate-cleanup.js +91 -8
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +12 -3
- package/dist/lib/task/local-artifact-finalization.js +212 -123
- package/dist/lib/task/manual-validation-receipt.js +5 -1
- package/dist/lib/task/orchestration.js +52 -2
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-command.js +3 -2
- package/dist/lib/task/review-finalization.js +294 -74
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +3 -14
- package/dist/lib/task/workflow-command.js +2 -1
- package/dist/lib/task/workflow-dispatch.js +161 -0
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +1 -0
- package/lib/internal/cli-route-inventory.ts +3 -4
- package/lib/internal/manual-validation.ts +17 -8
- package/lib/internal/platform-comment.ts +53 -2
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +1 -1
- package/lib/internal/task-lifecycle.ts +5 -4
- package/lib/internal/task-operation-registry.ts +22 -13
- package/lib/internal/task-orchestration.ts +1 -1
- package/lib/internal/task-review.ts +11 -7
- package/lib/platform/activity-recovery.ts +179 -0
- package/lib/platform/issue-comments.ts +105 -30
- package/lib/platform/issues.ts +1 -1
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-summary.ts +64 -32
- package/lib/platform/pull-requests.ts +3 -3
- package/lib/platform/task-comment-projection.ts +79 -0
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +48 -11
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/sandbox/commands/rm.ts +34 -2
- package/lib/sandbox/control/executor.ts +4 -2
- package/lib/sandbox/control/protocol.ts +6 -1
- package/lib/sandbox/control/server.ts +30 -10
- package/lib/sandbox/control/state.ts +9 -1
- package/lib/sandbox/control/task-workflow.ts +1 -1
- package/lib/sandbox/control/workflow-executor.ts +106 -35
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/removal.ts +126 -1128
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +12 -29
- package/lib/task/artifact-operations.ts +22 -252
- package/lib/task/artifact-recovery.ts +572 -0
- package/lib/task/artifact-repair-intent.ts +126 -51
- package/lib/task/commit-operation.ts +35 -4
- package/lib/task/control-authority.ts +48 -15
- package/lib/task/control-recovery.ts +13 -1
- package/lib/task/create-service.ts +1 -10
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +188 -24
- package/lib/task/human-override.ts +5 -8
- package/lib/task/intermediate-cleanup.ts +66 -12
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +13 -4
- package/lib/task/local-artifact-finalization.ts +249 -131
- package/lib/task/manual-validation-receipt.ts +5 -1
- package/lib/task/orchestration.ts +65 -0
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-command.ts +2 -1
- package/lib/task/review-finalization.ts +298 -84
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +3 -17
- package/lib/task/verification-types.ts +0 -2
- package/lib/task/workflow-command.ts +2 -1
- package/lib/task/workflow-dispatch.ts +171 -0
- package/lib/version.ts +1 -15
- package/package.json +1 -1
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +12 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +13 -5
- package/templates/.agents/rules/local-artifact-repair.en.md +47 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +45 -36
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.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/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +11 -6
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +11 -6
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -4
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
- package/dist/lib/host-control/audit.js +0 -19
- package/dist/lib/host-control/client.js +0 -142
- package/dist/lib/host-control/command.js +0 -49
- package/dist/lib/host-control/path.js +0 -160
- package/dist/lib/host-control/server.js +0 -159
- package/dist/lib/host-control/service.js +0 -112
- package/dist/lib/internal/host-control.js +0 -28
- package/lib/host-control/audit.ts +0 -22
- package/lib/host-control/client.ts +0 -172
- package/lib/host-control/command.ts +0 -60
- package/lib/host-control/path.ts +0 -167
- package/lib/host-control/server.ts +0 -180
- package/lib/host-control/service.ts +0 -133
- package/lib/internal/host-control.ts +0 -28
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const SNAPSHOT_FRONTMATTER = new Set([
|
|
4
|
+
'id', 'type', 'branch', 'workflow', 'status', 'created_at', 'updated_at',
|
|
5
|
+
'priority', 'effort', 'start_date', 'target_date', 'current_step', 'assigned_to',
|
|
6
|
+
'platform_issue_identity', 'delivery_remote', 'delivery_base_ref', 'pr_delivery_fact'
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
type TaskCommentProjection = {
|
|
10
|
+
content: string;
|
|
11
|
+
byteLength: number;
|
|
12
|
+
sha256: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PROCESS_SECTION_ALIASES = new Set([
|
|
16
|
+
'活动日志', 'activity log',
|
|
17
|
+
'产物生命周期收据', 'artifact lifecycle receipts',
|
|
18
|
+
'审查分歧账本', 'review dispute ledger', 'review disagreement ledger',
|
|
19
|
+
'人工裁决', '人工裁决待办', 'human decisions', 'human decision pending', 'human decisions pending',
|
|
20
|
+
'实现输入', 'implementation input', 'implementation inputs',
|
|
21
|
+
'工作流告警', 'workflow warning', 'workflow warnings', 'warnings',
|
|
22
|
+
'返工意图', 'rework intent', 'rework intents',
|
|
23
|
+
'产物失效记录', 'artifact invalidation', 'artifact invalidation records',
|
|
24
|
+
'实现备注', 'implementation notes',
|
|
25
|
+
'审查反馈', 'review feedback'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function normalize(content: string): string {
|
|
29
|
+
return content.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function splitFrontmatter(content: string): { frontmatter: string; body: string } {
|
|
33
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
34
|
+
if (!match) throw new Error('task comment projection requires task frontmatter');
|
|
35
|
+
return { frontmatter: match[1]!, body: content.slice(match[0].length) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function projectFrontmatter(frontmatter: string): string {
|
|
39
|
+
const lines = frontmatter.replace(/\r\n/g, '\n').split('\n');
|
|
40
|
+
const selected = lines.filter((line) => {
|
|
41
|
+
if (!line.trim() || line.trimStart().startsWith('#')) return false;
|
|
42
|
+
const match = /^([^:\s][^:]*):/.exec(line);
|
|
43
|
+
return Boolean(match && SNAPSHOT_FRONTMATTER.has(match[1]!.trim()));
|
|
44
|
+
});
|
|
45
|
+
if (!selected.some((line) => /^id:/.test(line))) throw new Error('task comment projection requires frontmatter id');
|
|
46
|
+
return selected.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function projectBody(body: string): string {
|
|
50
|
+
const lines = body.replace(/\r\n/g, '\n').split('\n');
|
|
51
|
+
const kept: string[] = [];
|
|
52
|
+
let excludedLevel: number | null = null;
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
55
|
+
if (heading) {
|
|
56
|
+
const level = heading[1]!.length;
|
|
57
|
+
if (excludedLevel !== null && level <= excludedLevel) excludedLevel = null;
|
|
58
|
+
if (PROCESS_SECTION_ALIASES.has(heading[2]!.trim().toLowerCase())) {
|
|
59
|
+
excludedLevel = level;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (excludedLevel === null) kept.push(line);
|
|
64
|
+
}
|
|
65
|
+
return kept.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function projectTaskComment(content: string): TaskCommentProjection {
|
|
69
|
+
const split = splitFrontmatter(content);
|
|
70
|
+
const projected = normalize(`---\n${projectFrontmatter(split.frontmatter)}\n---\n\n${projectBody(split.body)}`);
|
|
71
|
+
return {
|
|
72
|
+
content: projected,
|
|
73
|
+
byteLength: Buffer.byteLength(projected, 'utf8'),
|
|
74
|
+
sha256: createHash('sha256').update(projected, 'utf8').digest('hex')
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { projectTaskComment };
|
|
79
|
+
export type { TaskCommentProjection };
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResourceIdentity } from './resource-identity.ts';
|
|
2
2
|
import { parseResourceIdentity } from './resource-identity.ts';
|
|
3
|
-
import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.ts';
|
|
4
3
|
import { parse as parseYaml } from 'yaml';
|
|
5
4
|
|
|
6
|
-
function taskIssueIdentity(
|
|
7
|
-
frontmatter: Record<string, string | number | boolean | null>,
|
|
8
|
-
_declaration?: ProviderIdentityDeclaration,
|
|
9
|
-
runtimeVersion = VERSION
|
|
10
|
-
): ResourceIdentity | null {
|
|
5
|
+
function taskIssueIdentity(frontmatter: Record<string, string | number | boolean | null>): ResourceIdentity | null {
|
|
11
6
|
const serialized = frontmatter.platform_issue_identity;
|
|
12
7
|
if (typeof serialized === 'string' && serialized.trim()) {
|
|
13
8
|
try {
|
|
@@ -20,13 +15,7 @@ function taskIssueIdentity(
|
|
|
20
15
|
return null;
|
|
21
16
|
}
|
|
22
17
|
}
|
|
23
|
-
|
|
24
|
-
const number = typeof legacy === 'number' ? legacy : typeof legacy === 'string' && /^[1-9]\d*$/u.test(legacy) ? Number(legacy) : NaN;
|
|
25
|
-
// TODO(compat): Remove the numeric issue_number fallback before the first stable v1.0.0 release.
|
|
26
|
-
if (Number.isSafeInteger(number) && number > 0 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
|
|
27
|
-
throw legacyCompatibilityError('issue_number', runtimeVersion);
|
|
28
|
-
}
|
|
29
|
-
return Number.isSafeInteger(number) && number > 0 ? { kind: 'number', value: number } : null;
|
|
18
|
+
return null;
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
function taskIssueIdentityError(error: unknown): { code: string; message: string } {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseActivityRecoveryMetadata } from './activity-recovery.ts';
|
|
2
|
+
|
|
3
|
+
type RecoveryRemoteComment = { body: string; user?: { login?: string } };
|
|
4
|
+
|
|
5
|
+
function normalized(body: string): string {
|
|
6
|
+
return body.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function decodeRenderedText(value: string): string {
|
|
10
|
+
return value
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, "'")
|
|
15
|
+
.replace(/&/g, '&');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function taskSnapshot(taskId: string, body: string): string {
|
|
19
|
+
const marker = `<!-- sync-issue:${taskId}:task -->`;
|
|
20
|
+
if (normalized(body).split('\n', 1)[0] !== marker) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot marker is invalid');
|
|
21
|
+
const frontmatter = body.match(/<details><summary>[^<]*<\/summary>\n\n```yaml\n---\n([\s\S]*?)\n---\n```\n\n<\/details>/);
|
|
22
|
+
if (!frontmatter) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot frontmatter is invalid');
|
|
23
|
+
const afterDetails = (frontmatter.index ?? 0) + frontmatter[0].length;
|
|
24
|
+
const footer = body.lastIndexOf('\n\n---\n*由 ');
|
|
25
|
+
if (footer <= afterDetails) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot envelope is invalid');
|
|
26
|
+
const projectedBody = body.slice(afterDetails, footer).replace(/^\n+|\n+$/g, '');
|
|
27
|
+
return `---\n${decodeRenderedText(frontmatter[1]!)}\n---\n\n${decodeRenderedText(projectedBody)}\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function activityLine(entry: { time: string; step: string; agent: string; note: string }): string {
|
|
31
|
+
return `- ${entry.time} — **${entry.step}** by ${entry.agent} — ${entry.note}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function recoverTaskFromComments(input: { taskId: string; comments: readonly RecoveryRemoteComment[] }): string {
|
|
35
|
+
const marker = `<!-- sync-issue:${input.taskId}:task -->`;
|
|
36
|
+
const snapshots = input.comments.filter((comment) => normalized(comment.body).split('\n', 1)[0] === marker);
|
|
37
|
+
if (snapshots.length !== 1) throw new Error('RECOVERY_EVIDENCE_MISSING: exactly one task snapshot comment is required');
|
|
38
|
+
const author = snapshots[0]!.user?.login;
|
|
39
|
+
if (!author) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot author is unavailable');
|
|
40
|
+
const content = taskSnapshot(input.taskId, normalized(snapshots[0]!.body));
|
|
41
|
+
const entries = new Map<string, { time: string; step: string; agent: string; note: string }>();
|
|
42
|
+
for (const comment of input.comments) {
|
|
43
|
+
if (comment.user?.login !== author) continue;
|
|
44
|
+
const record = parseActivityRecoveryMetadata(comment.body);
|
|
45
|
+
if (!record) continue;
|
|
46
|
+
if (record.taskId !== input.taskId) throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata task identity is invalid');
|
|
47
|
+
for (const entry of record.entries) entries.set(JSON.stringify(entry), entry);
|
|
48
|
+
}
|
|
49
|
+
const restored = [...entries.values()].sort((left, right) => left.time.localeCompare(right.time));
|
|
50
|
+
return `${content.replace(/\n+$/, '')}\n\n## 活动日志\n\n${restored.map(activityLine).join('\n')}\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { recoverTaskFromComments };
|
|
54
|
+
export type { RecoveryRemoteComment };
|
|
@@ -12,6 +12,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.ts";
|
|
|
12
12
|
import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
|
|
13
13
|
import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.ts";
|
|
14
14
|
import { taskIssueIdentity } from "./task-identities.ts";
|
|
15
|
+
import { renderTaskCommentResult } from "./issue-comments.ts";
|
|
15
16
|
import {
|
|
16
17
|
CONTROL_MARKER_PATTERN,
|
|
17
18
|
canonicalizeCommentBody,
|
|
@@ -113,8 +114,8 @@ async function buildSyncContext({ taskDir, config, artifactFile }: any, shared:
|
|
|
113
114
|
}
|
|
114
115
|
const prIdentity = fact.status === "valid" && fact.fact.state === "bound" ? fact.fact.identity.resource : null;
|
|
115
116
|
const prNumber = resourceIdentityNumber(prIdentity);
|
|
116
|
-
if (config.when === "
|
|
117
|
-
return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no
|
|
117
|
+
if (config.when === "platform_issue_identity_exists" && !issueIdentity) {
|
|
118
|
+
return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no platform_issue_identity") };
|
|
118
119
|
}
|
|
119
120
|
if (config.when === "pr_fact_bound" && !prIdentity) {
|
|
120
121
|
return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no verified bound pull request") };
|
|
@@ -541,7 +542,6 @@ function checkTaskCommentContent(context: any, remoteData: any, shared: Verifica
|
|
|
541
542
|
if (!context.config.verify_task_comment_content) {
|
|
542
543
|
return null;
|
|
543
544
|
}
|
|
544
|
-
|
|
545
545
|
const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
|
|
546
546
|
const comment = findCommentByMarker(remoteData.comments, taskMarker);
|
|
547
547
|
if (!comment) {
|
|
@@ -551,14 +551,20 @@ function checkTaskCommentContent(context: any, remoteData: any, shared: Verifica
|
|
|
551
551
|
);
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
554
|
+
let renderedTask: { body: string; byteLength: number };
|
|
555
|
+
try {
|
|
556
|
+
renderedTask = renderTaskCommentResult(
|
|
557
|
+
context.task.content,
|
|
558
|
+
context.task.metadata.id,
|
|
559
|
+
loadProjectLanguage(shared)
|
|
559
560
|
);
|
|
561
|
+
} catch {
|
|
562
|
+
return shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed");
|
|
560
563
|
}
|
|
561
|
-
|
|
564
|
+
if (renderedTask.byteLength > 60_000) {
|
|
565
|
+
return shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed");
|
|
566
|
+
}
|
|
567
|
+
const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
|
|
562
568
|
const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
|
|
563
569
|
|
|
564
570
|
if (expectedBody === commentBody) {
|
|
@@ -829,7 +835,7 @@ function extractCommentBody(commentBody: any): any {
|
|
|
829
835
|
start += 1;
|
|
830
836
|
}
|
|
831
837
|
|
|
832
|
-
if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start]!.trim())) {
|
|
838
|
+
if (start < lines.length && (/^> \*\*.+\*\* · .+$/.test(lines[start]!.trim()) || /^> 任务同步 · .+$/.test(lines[start]!.trim()))) {
|
|
833
839
|
start += 1;
|
|
834
840
|
}
|
|
835
841
|
|
|
@@ -837,6 +843,16 @@ function extractCommentBody(commentBody: any): any {
|
|
|
837
843
|
start += 1;
|
|
838
844
|
}
|
|
839
845
|
|
|
846
|
+
if (lines[start]?.trim() === "<details><summary>恢复元数据</summary>") {
|
|
847
|
+
const metadataEnd = lines.indexOf("</details>", start + 1);
|
|
848
|
+
if (metadataEnd >= 0) {
|
|
849
|
+
start = metadataEnd + 1;
|
|
850
|
+
while (start < lines.length && lines[start]!.trim() === "") {
|
|
851
|
+
start += 1;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
840
856
|
let end = lines.length;
|
|
841
857
|
for (let index = lines.length - 1; index >= start; index -= 1) {
|
|
842
858
|
const trimmed = lines[index]!.trim();
|
|
@@ -1038,7 +1054,9 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
|
|
|
1038
1054
|
if (!baseRef) {
|
|
1039
1055
|
return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
|
|
1040
1056
|
}
|
|
1041
|
-
const
|
|
1057
|
+
const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
|
|
1058
|
+
const gitCwd = branch ? worktreeForBranch(shared.repoRoot, branch) ?? taskDir : taskDir;
|
|
1059
|
+
const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
|
|
1042
1060
|
if (!changedFilesResult.ok) {
|
|
1043
1061
|
return { ...changedFilesResult, type: "network_error" };
|
|
1044
1062
|
}
|
|
@@ -1065,6 +1083,25 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
|
|
|
1065
1083
|
return { ok: true, labels: planned.target, mode: "mapped" };
|
|
1066
1084
|
}
|
|
1067
1085
|
|
|
1086
|
+
function worktreeForBranch(repositoryRoot: string, branch: string): string | null {
|
|
1087
|
+
try {
|
|
1088
|
+
const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
1089
|
+
cwd: repositoryRoot,
|
|
1090
|
+
encoding: "utf8",
|
|
1091
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1092
|
+
}).trim().split(/\r?\n\r?\n/);
|
|
1093
|
+
const expectedRef = `refs/heads/${branch}`;
|
|
1094
|
+
for (const record of records) {
|
|
1095
|
+
const worktree = /^worktree (.+)$/m.exec(record)?.[1];
|
|
1096
|
+
const branchRef = /^branch (.+)$/m.exec(record)?.[1];
|
|
1097
|
+
if (worktree && branchRef === expectedRef) return worktree;
|
|
1098
|
+
}
|
|
1099
|
+
} catch {
|
|
1100
|
+
// Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
|
|
1101
|
+
}
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1068
1105
|
function loadInLabelMapping(shared: VerificationShared): any {
|
|
1069
1106
|
const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
|
|
1070
1107
|
if (!fs.existsSync(configPath)) {
|
|
@@ -37,11 +37,6 @@ export class HostResolutionError extends Error {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function toPositiveNumber(value: unknown): number | null {
|
|
41
|
-
const n = Number(value);
|
|
42
|
-
return Number.isInteger(n) && n > 0 ? n : null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
40
|
/**
|
|
46
41
|
* Parse the PR body for closing-issue references. Handles the conventional
|
|
47
42
|
* `Closes #1` / `Fixes: #2, #3` shapes (case-insensitive, comma/space
|
|
@@ -68,7 +63,7 @@ export function extractClosingIssueNumbers(body: string): number[] {
|
|
|
68
63
|
/**
|
|
69
64
|
* Scan `.agents/workspace/active/{task-id}/task.md` files for tasks bound to the
|
|
70
65
|
* PR number or to one of the PR's closing issues. A task is emitted at most once;
|
|
71
|
-
* a verified fact identity hit takes priority over the issue-
|
|
66
|
+
* a verified fact identity hit takes priority over the issue-identity reverse lookup.
|
|
72
67
|
*/
|
|
73
68
|
export function collectHostCandidates(input: {
|
|
74
69
|
prNumber?: number;
|
|
@@ -97,7 +92,7 @@ export function collectHostCandidates(input: {
|
|
|
97
92
|
let frontmatter: ReturnType<typeof parseTypedTaskFrontmatter>;
|
|
98
93
|
try { frontmatter = parseTypedTaskFrontmatter(content); } catch { continue; }
|
|
99
94
|
const issueIdentity = taskIssueIdentity(frontmatter);
|
|
100
|
-
const issueNumber = resourceIdentityNumber(issueIdentity)
|
|
95
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
101
96
|
const fact = readPrDeliveryFact(frontmatter);
|
|
102
97
|
const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
|
|
103
98
|
const prNumber = resourceIdentityNumber(prIdentity);
|
|
@@ -209,8 +204,8 @@ function isTrusted(head: HeadState): boolean {
|
|
|
209
204
|
/**
|
|
210
205
|
* Derive the S1(a) trust factor `taskIssueMatches` (PL-3) mechanically from
|
|
211
206
|
* resolved data: a unique host is trusted on the issue dimension only when the
|
|
212
|
-
* task's bound
|
|
213
|
-
* hosts and tasks without a
|
|
207
|
+
* task's bound issue identity is among the PR's closing issues. Ambiguous/none
|
|
208
|
+
* hosts and tasks without a numeric issue identity cannot be confirmed -> false
|
|
214
209
|
* (fail closed to S2/audit). `resolve-host` emits this value so the prompt layer
|
|
215
210
|
* does not derive the trust factor ad hoc (CD-2).
|
|
216
211
|
*/
|
|
@@ -2,12 +2,41 @@ import { parseArgs } from 'node:util';
|
|
|
2
2
|
import { loadConfig } from '../config.ts';
|
|
3
3
|
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
|
|
4
4
|
import { resolveSandboxCleanupTarget } from '../workspace-identity.ts';
|
|
5
|
+
import { detectEngine } from '../engine.ts';
|
|
6
|
+
import {
|
|
7
|
+
sandboxBranchLabel,
|
|
8
|
+
sandboxLabel,
|
|
9
|
+
sandboxTaskIdLabel,
|
|
10
|
+
sandboxWorkspaceModeLabel
|
|
11
|
+
} from '../constants.ts';
|
|
12
|
+
import { fetchSandboxRows } from './list-running.ts';
|
|
5
13
|
import { rmOne, rmPurge, rmUnbound } from '../removal.ts';
|
|
6
14
|
|
|
7
15
|
const USAGE = `Usage:
|
|
8
16
|
ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
|
|
9
|
-
ai sandbox rm --unbound [--dry-run] [--yes] Remove
|
|
17
|
+
ai sandbox rm --unbound [--dry-run] [--yes] Remove discovered task-bound and branch-only sandboxes
|
|
10
18
|
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
19
|
+
|
|
20
|
+
function resolveMissingTaskSandboxBranch(taskId: string, config: ReturnType<typeof loadConfig>): string | null {
|
|
21
|
+
const engine = detectEngine(config);
|
|
22
|
+
const listed = fetchSandboxRows(
|
|
23
|
+
engine,
|
|
24
|
+
sandboxLabel(config),
|
|
25
|
+
sandboxBranchLabel(config),
|
|
26
|
+
{ mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
|
|
27
|
+
);
|
|
28
|
+
const branches = new Set(
|
|
29
|
+
[...listed.running, ...listed.nonRunning]
|
|
30
|
+
.filter((row) => row.workspaceMode === 'task-bound' && row.taskId === taskId && row.branch)
|
|
31
|
+
.map((row) => row.branch)
|
|
32
|
+
);
|
|
33
|
+
if (branches.size === 0) return null;
|
|
34
|
+
if (branches.size > 1) {
|
|
35
|
+
throw new Error(`SANDBOX_CLEANUP_TASK_ID_AMBIGUOUS: task ${taskId} matches multiple sandbox branches`);
|
|
36
|
+
}
|
|
37
|
+
return branches.values().next().value ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
11
40
|
export async function rm(args: string[]): Promise<void> {
|
|
12
41
|
const { values, positionals } = parseArgs({
|
|
13
42
|
args,
|
|
@@ -64,6 +93,9 @@ export async function rm(args: string[]): Promise<void> {
|
|
|
64
93
|
return;
|
|
65
94
|
}
|
|
66
95
|
|
|
67
|
-
const cleanupTarget = resolveSandboxCleanupTarget(positionals[0] ?? '', config.repoRoot
|
|
96
|
+
const cleanupTarget = resolveSandboxCleanupTarget(positionals[0] ?? '', config.repoRoot, {
|
|
97
|
+
allowProtected: true,
|
|
98
|
+
resolveMissingTask: (taskId) => resolveMissingTaskSandboxBranch(taskId, config)
|
|
99
|
+
});
|
|
68
100
|
await rmOne(config, tools, cleanupTarget.branch, { cleanupTarget });
|
|
69
101
|
}
|
|
@@ -188,7 +188,9 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
188
188
|
owner: gateOwner,
|
|
189
189
|
requestId: params.request.id,
|
|
190
190
|
operationId: authority?.operationId ?? params.request.id,
|
|
191
|
-
phase: authority?.phase ?? (params.request.family === 'task-workflow'
|
|
191
|
+
phase: authority?.phase ?? (params.request.family === 'task-workflow'
|
|
192
|
+
&& params.request.workflow?.operation === 'artifact-preflight' ? 'artifact.preflight'
|
|
193
|
+
: params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
|
|
192
194
|
authority
|
|
193
195
|
} satisfies SandboxControlExecutorGateV2);
|
|
194
196
|
},
|
|
@@ -235,7 +237,7 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<Readonly<{ owner
|
|
|
235
237
|
return;
|
|
236
238
|
}
|
|
237
239
|
if (typeof value.requestId !== 'string' || typeof value.operationId !== 'string'
|
|
238
|
-
|| !['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
|
|
240
|
+
|| !['orchestration.prepare', 'artifact.preflight', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
|
|
239
241
|
|| (value.authority !== null && value.authority !== undefined && (() => {
|
|
240
242
|
try { validateLifecycleRecoveryAttestation(value.authority); return false; } catch { return true; }
|
|
241
243
|
})())) {
|
|
@@ -62,7 +62,7 @@ export type SandboxControlExecutorGateV2 = Readonly<{
|
|
|
62
62
|
owner: SandboxControlBrokerOwner;
|
|
63
63
|
requestId: string;
|
|
64
64
|
operationId: string;
|
|
65
|
-
phase: 'orchestration.prepare' | 'artifact.finalize-local' | 'task-event.completed';
|
|
65
|
+
phase: 'orchestration.prepare' | 'artifact.preflight' | 'artifact.finalize-local' | 'task-event.completed';
|
|
66
66
|
authority: LifecycleRecoveryAttestationV1 | null;
|
|
67
67
|
}>;
|
|
68
68
|
type RequestBase = Readonly<{
|
|
@@ -131,6 +131,11 @@ export type SandboxControlResponse = Readonly<{
|
|
|
131
131
|
outputState?: 'available' | 'unavailable';
|
|
132
132
|
payload?: SandboxControlPayloadReference | null;
|
|
133
133
|
}>;
|
|
134
|
+
export type SandboxControlRecoveryWarning = Readonly<{
|
|
135
|
+
code: string;
|
|
136
|
+
message: string;
|
|
137
|
+
action: string;
|
|
138
|
+
}>;
|
|
134
139
|
export type SandboxControlStatus = Readonly<{
|
|
135
140
|
version: 3; generation: string; broker: ProcessIdentity & { brokerId: string };
|
|
136
141
|
state: 'starting' | 'healthy' | 'busy' | 'parked'; reasonCode: string | null;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
SANDBOX_CONTROL_MAX_RESPONSE_BYTES,
|
|
13
13
|
SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES,
|
|
14
14
|
type SandboxControlManifest,
|
|
15
|
+
type SandboxControlRecoveryWarning,
|
|
15
16
|
type SandboxControlRequest,
|
|
16
17
|
type SandboxControlResponse,
|
|
17
18
|
type SandboxControlTimingPolicy
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES
|
|
63
64
|
} from '../../task/control-recovery.ts';
|
|
64
65
|
import { readRun } from '../../task/orchestration.ts';
|
|
66
|
+
import { readLifecycleRecoveryDomainEvidence } from '../../task/lifecycle-recovery.ts';
|
|
65
67
|
import { captureRepositorySnapshot } from '../../task/workspace-snapshot.ts';
|
|
66
68
|
import { parseTypedTaskFrontmatter } from '../../task/frontmatter.ts';
|
|
67
69
|
import { readLifecycleJournalEvidence } from '../../task/lifecycle.ts';
|
|
@@ -368,11 +370,12 @@ function outputMatchesEvidence(output: string, bytes: number, sha256: string): b
|
|
|
368
370
|
&& createHash('sha256').update(output, 'utf8').digest('hex') === sha256;
|
|
369
371
|
}
|
|
370
372
|
|
|
371
|
-
function genericRecoveryResponse(
|
|
373
|
+
export function genericRecoveryResponse(
|
|
372
374
|
request: SandboxControlRequest,
|
|
373
375
|
exitCode: number,
|
|
374
376
|
payload: ReturnType<typeof readSandboxControlPayload> | null,
|
|
375
|
-
cause: 'publish' | 'recovery' = 'recovery'
|
|
377
|
+
cause: 'publish' | 'recovery' = 'recovery',
|
|
378
|
+
warning: SandboxControlRecoveryWarning | null = null
|
|
376
379
|
): SandboxControlResponse {
|
|
377
380
|
const outputUnavailable = request.family === 'task-create' && !payload;
|
|
378
381
|
return {
|
|
@@ -381,9 +384,11 @@ function genericRecoveryResponse(
|
|
|
381
384
|
phase: 'completed',
|
|
382
385
|
exitCode,
|
|
383
386
|
stdout: outputUnavailable ? `${JSON.stringify(taskCreateOutputUnavailableResult(request.id))}\n` : '',
|
|
384
|
-
stderr: payload ? '' :
|
|
385
|
-
?
|
|
386
|
-
:
|
|
387
|
+
stderr: payload ? '' : warning
|
|
388
|
+
? `${warning.code}: ${warning.message}\nAction: ${warning.action}\n`
|
|
389
|
+
: cause === 'publish'
|
|
390
|
+
? 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: output payload was not retained\n'
|
|
391
|
+
: 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: broker restarted after executor completion\n',
|
|
387
392
|
error: null,
|
|
388
393
|
outputState: payload ? 'available' : 'unavailable',
|
|
389
394
|
payload: payload ? payloadReference(payload) : null
|
|
@@ -504,6 +509,21 @@ function readRecoveryDomain(
|
|
|
504
509
|
}
|
|
505
510
|
|
|
506
511
|
if (operation.family === 'task-lifecycle') {
|
|
512
|
+
if (operation.intent === 'recover-started') {
|
|
513
|
+
try {
|
|
514
|
+
if (!('args' in request)) return { domain: null };
|
|
515
|
+
const parsed = parseTaskControlOperation('task-lifecycle', request.args);
|
|
516
|
+
if (parsed.family !== 'task-lifecycle' || parsed.request.intent !== 'recover-started') {
|
|
517
|
+
return { domain: null };
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
domain: readLifecycleRecoveryDomainEvidence(manifest.repoRoot, parsed.request, terminalResult),
|
|
521
|
+
journal: readLifecycleJournalEvidence(manifest.repoRoot, taskRef!)
|
|
522
|
+
};
|
|
523
|
+
} catch {
|
|
524
|
+
return { domain: null };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
507
527
|
if (!taskRef || !terminalResult.targetState) return { domain: null };
|
|
508
528
|
const journal = readLifecycleJournalEvidence(manifest.repoRoot, taskRef);
|
|
509
529
|
try {
|
|
@@ -577,7 +597,7 @@ function readCommittedCriticalPhases(manifest: SandboxControlManifest, requestId
|
|
|
577
597
|
});
|
|
578
598
|
}
|
|
579
599
|
|
|
580
|
-
function recoveryResponse(
|
|
600
|
+
export function recoveryResponse(
|
|
581
601
|
manifest: SandboxControlManifest,
|
|
582
602
|
manifestPath: string,
|
|
583
603
|
request: SandboxControlRequest,
|
|
@@ -637,7 +657,7 @@ function recoveryResponse(
|
|
|
637
657
|
if (finalization?.status !== 'matched') return null;
|
|
638
658
|
return finalization.response ?? null;
|
|
639
659
|
}
|
|
640
|
-
return genericRecoveryResponse(request, evidence.exitCode, payload);
|
|
660
|
+
return genericRecoveryResponse(request, evidence.exitCode, payload, 'recovery', terminalResult.warning ?? null);
|
|
641
661
|
}
|
|
642
662
|
|
|
643
663
|
function terminalMatchesEvidence(
|
|
@@ -681,7 +701,7 @@ function terminalMatchesEvidence(
|
|
|
681
701
|
if (response.outputState === 'unavailable') {
|
|
682
702
|
const causes = ['recovery', 'publish'] as const;
|
|
683
703
|
const valid = causes.some((cause) => JSON.stringify(response)
|
|
684
|
-
=== JSON.stringify(genericRecoveryResponse(request, evidence.exitCode, null, cause)));
|
|
704
|
+
=== JSON.stringify(genericRecoveryResponse(request, evidence.exitCode, null, cause, terminalResult?.warning ?? null)));
|
|
685
705
|
return { valid, payloadReferenced: false };
|
|
686
706
|
}
|
|
687
707
|
return {
|
|
@@ -700,7 +720,7 @@ function publishExecutionResult(
|
|
|
700
720
|
brokerOwns: () => boolean
|
|
701
721
|
): boolean {
|
|
702
722
|
const normalized = sanitizeSandboxControlResult(manifest, result);
|
|
703
|
-
writeSandboxControlTerminalResult(manifest, {
|
|
723
|
+
const terminalResult = writeSandboxControlTerminalResult(manifest, {
|
|
704
724
|
id: request.id,
|
|
705
725
|
family: request.family,
|
|
706
726
|
operation: operationKey(request, normalized.stdout)
|
|
@@ -737,7 +757,7 @@ function publishExecutionResult(
|
|
|
737
757
|
} catch {
|
|
738
758
|
payload = null;
|
|
739
759
|
}
|
|
740
|
-
terminal = genericRecoveryResponse(request, normalized.exitCode, payload, 'publish');
|
|
760
|
+
terminal = genericRecoveryResponse(request, normalized.exitCode, payload, 'publish', terminalResult.warning ?? null);
|
|
741
761
|
}
|
|
742
762
|
}
|
|
743
763
|
if (!brokerOwns()) return false;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isCompletionEvidence, type CleanCompletionEvidence } from '../../task/orchestration.ts';
|
|
2
2
|
import { parseControlOutput } from '../../task/control-recovery.ts';
|
|
3
|
+
import { isRecoveryWarning } from '../../task/recovery-warning.ts';
|
|
3
4
|
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import fs from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
@@ -14,6 +15,7 @@ import type {
|
|
|
14
15
|
SandboxControlPayload,
|
|
15
16
|
SandboxControlReservation,
|
|
16
17
|
SandboxControlResultEvidence,
|
|
18
|
+
SandboxControlRecoveryWarning,
|
|
17
19
|
SandboxControlStatus
|
|
18
20
|
} from './protocol.ts';
|
|
19
21
|
import {
|
|
@@ -113,6 +115,7 @@ export type SandboxControlTerminalResult = Readonly<{
|
|
|
113
115
|
status: string;
|
|
114
116
|
changed: boolean | null;
|
|
115
117
|
completionEvidence: CleanCompletionEvidence | null;
|
|
118
|
+
warning?: SandboxControlRecoveryWarning | null;
|
|
116
119
|
}>;
|
|
117
120
|
|
|
118
121
|
export function terminalResultPath(manifest: SandboxControlManifest, requestId: string): string {
|
|
@@ -130,6 +133,7 @@ export function createSandboxControlTerminalResult(
|
|
|
130
133
|
? nested.run as Record<string, unknown> : null;
|
|
131
134
|
const rawCompletion = nested.completionEvidence ?? run?.completionEvidence;
|
|
132
135
|
const operation = request.operation ?? (typeof nested.intent === 'string' ? nested.intent : null) ?? '';
|
|
136
|
+
const warning = isRecoveryWarning(nested.warning) ? nested.warning : null;
|
|
133
137
|
return {
|
|
134
138
|
version: 1,
|
|
135
139
|
requestId: request.id,
|
|
@@ -139,7 +143,8 @@ export function createSandboxControlTerminalResult(
|
|
|
139
143
|
targetState: typeof nested.targetState === 'string' ? nested.targetState : null,
|
|
140
144
|
status: typeof nested.status === 'string' ? nested.status : 'completed',
|
|
141
145
|
changed: typeof nested.changed === 'boolean' ? nested.changed : null,
|
|
142
|
-
completionEvidence: isCompletionEvidence(rawCompletion) ? rawCompletion : null
|
|
146
|
+
completionEvidence: isCompletionEvidence(rawCompletion) ? rawCompletion : null,
|
|
147
|
+
...(warning ? { warning } : {})
|
|
143
148
|
};
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -173,6 +178,9 @@ export function readSandboxControlTerminalResult(filePath: string): SandboxContr
|
|
|
173
178
|
if (value.completionEvidence !== null && !isCompletionEvidence(value.completionEvidence)) {
|
|
174
179
|
throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
|
|
175
180
|
}
|
|
181
|
+
if (value.warning !== undefined && value.warning !== null && !isRecoveryWarning(value.warning)) {
|
|
182
|
+
throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
|
|
183
|
+
}
|
|
176
184
|
if (value.changed === undefined) {
|
|
177
185
|
throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
|
|
178
186
|
}
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
|
|
5
|
-
import { readStableFile, SecureFileError, writeAtomicFile } from '../../
|
|
5
|
+
import { readStableFile, SecureFileError, writeAtomicFile } from '../../task/secure-fs.ts';
|
|
6
6
|
|
|
7
7
|
import { TASK_WORKFLOW_COMMANDS, TASK_WORKFLOW_OPERATIONS, type TaskWorkflowOperation, type WorkflowCommand } from '../../task/workflow-command.ts';
|
|
8
8
|
|