@fitlab-ai/agent-infra 0.9.14 → 0.9.15
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 +10 -0
- package/README.zh-CN.md +10 -0
- package/bin/internal-cli.sh +45 -0
- package/dist/bin/internal-cli.js +93 -30
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
- package/dist/lib/defaults.json +103 -103
- package/dist/lib/fs/durable-write.js +37 -0
- package/dist/lib/fs/file-lock.js +111 -0
- package/dist/lib/host-control/audit.js +19 -0
- package/dist/lib/host-control/client.js +142 -0
- package/dist/lib/host-control/command.js +49 -0
- package/dist/lib/host-control/path.js +160 -0
- package/dist/lib/host-control/secure-fs.js +140 -0
- package/dist/lib/host-control/server.js +159 -0
- package/dist/lib/host-control/service.js +112 -0
- package/dist/lib/init.js +11 -2
- package/dist/lib/internal/cli-route-inventory.js +3 -1
- package/dist/lib/internal/codex-lifecycle.js +6 -6
- package/dist/lib/internal/demo-promotion.js +322 -0
- package/dist/lib/internal/demo-transcript.js +454 -0
- package/dist/lib/internal/host-control.js +28 -0
- package/dist/lib/internal/manual-validation.js +322 -0
- package/dist/lib/internal/platform-pr.js +18 -4
- package/dist/lib/internal/release-workflow.js +63 -84
- package/dist/lib/internal/sandbox-control.js +149 -2
- package/dist/lib/internal/task-artifact.js +15 -262
- package/dist/lib/internal/task-create.js +6 -5
- package/dist/lib/internal/task-event.js +37 -37
- package/dist/lib/internal/task-operation-registry.js +101 -3
- package/dist/lib/internal/task-review.js +8 -53
- package/dist/lib/internal/task-validate.js +30 -10
- package/dist/lib/owned-path.js +33 -0
- package/dist/lib/platform/check-status.js +11 -0
- package/dist/lib/platform/comment-safety.js +4 -1
- package/dist/lib/platform/completion-backfill.js +1 -1
- package/dist/lib/platform/github-data.js +241 -0
- package/dist/lib/platform/github-provider.js +15 -20
- package/dist/lib/platform/issue-comments.js +2 -2
- package/dist/lib/platform/issues.js +1 -179
- package/dist/lib/platform/pr-checks.js +12 -65
- package/dist/lib/platform/pr-summary.js +55 -3
- package/dist/lib/platform/pull-requests.js +2 -105
- package/dist/lib/platform/snapshots.js +2 -0
- package/dist/lib/platform/verification-sync.js +127 -142
- package/dist/lib/release/workflow.js +1 -1
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/rm.js +3 -1514
- package/dist/lib/sandbox/control/audit.js +203 -0
- package/dist/lib/sandbox/control/client.js +93 -54
- package/dist/lib/sandbox/control/executor.js +129 -65
- package/dist/lib/sandbox/control/finalization-response.js +27 -0
- package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
- package/dist/lib/sandbox/control/lifecycle.js +36 -2
- package/dist/lib/sandbox/control/native-file-lock.js +6 -98
- package/dist/lib/sandbox/control/protocol.js +55 -4
- package/dist/lib/sandbox/control/server.js +491 -120
- package/dist/lib/sandbox/control/state.js +58 -0
- package/dist/lib/sandbox/control/task-workflow.js +74 -0
- package/dist/lib/sandbox/control/workflow-executor.js +99 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/recovery.js +28 -4
- package/dist/lib/sandbox/removal.js +1717 -0
- package/dist/lib/sandbox/task-cleanup.js +217 -0
- package/dist/lib/sandbox/workspace-view.js +13 -14
- package/dist/lib/task/activity-intent.js +2 -1
- package/dist/lib/task/activity-log.js +30 -24
- package/dist/lib/task/artifact-command.js +101 -0
- package/dist/lib/task/artifact-lifecycle.js +30 -28
- package/dist/lib/task/artifact-name.js +36 -0
- package/dist/lib/task/artifact-operations.js +23 -69
- package/dist/lib/task/artifact-receipts.js +3 -15
- package/dist/lib/task/artifact-repair-intent.js +87 -16
- package/dist/lib/task/artifact-schema.js +6 -14
- package/dist/lib/task/capabilities.js +12 -19
- package/dist/lib/task/codex-orchestration.js +71 -25
- package/dist/lib/task/commit-identity.js +28 -0
- package/dist/lib/task/commit-operation.js +2 -16
- package/dist/lib/task/control-authority.js +371 -5
- package/dist/lib/task/control-recovery.js +128 -0
- package/dist/lib/task/create.js +62 -9
- package/dist/lib/task/decision-details.js +2 -46
- package/dist/lib/task/events.js +120 -9
- package/dist/lib/task/finalization-state.js +9 -0
- package/dist/lib/task/human-override.js +3 -6
- package/dist/lib/task/implementation-inputs.js +3 -3
- package/dist/lib/task/intermediate-cleanup.js +383 -0
- package/dist/lib/task/invalidation.js +18 -65
- package/dist/lib/task/ledger-intents.js +7 -22
- package/dist/lib/task/ledger.js +1 -3
- package/dist/lib/task/lifecycle.js +19 -1
- package/dist/lib/task/local-artifact-finalization.js +132 -157
- package/dist/lib/task/manual-validation-completion.js +26 -0
- package/dist/lib/task/manual-validation-evidence.js +130 -0
- package/dist/lib/task/manual-validation-receipt.js +101 -0
- package/dist/lib/task/manual-validation-shared.js +40 -0
- package/dist/lib/task/manual-validation-transaction.js +243 -0
- package/dist/lib/task/markdown.js +47 -0
- package/dist/lib/task/orchestration.js +14 -17
- package/dist/lib/task/qualification-audit.js +20 -34
- package/dist/lib/task/review-artifacts.js +2 -18
- package/dist/lib/task/review-command.js +25 -0
- package/dist/lib/task/review-finalization.js +125 -193
- package/dist/lib/task/review-fingerprint.js +2 -2
- package/dist/lib/task/sections.js +34 -56
- package/dist/lib/task/verification-engine.js +116 -323
- package/dist/lib/task/verification.js +1 -1
- package/dist/lib/task/workflow-command.js +17 -0
- package/dist/lib/task/workflow-warning-intents.js +0 -16
- package/dist/lib/task/workflow-warnings.js +28 -2
- package/dist/lib/update.js +2 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
- package/lib/fs/durable-write.ts +29 -0
- package/lib/fs/file-lock.ts +113 -0
- package/lib/host-control/audit.ts +22 -0
- package/lib/host-control/client.ts +172 -0
- package/lib/host-control/command.ts +60 -0
- package/lib/host-control/path.ts +167 -0
- package/lib/host-control/secure-fs.ts +135 -0
- package/lib/host-control/server.ts +180 -0
- package/lib/host-control/service.ts +133 -0
- package/lib/init.ts +15 -2
- package/lib/internal/cli-route-inventory.ts +3 -1
- package/lib/internal/codex-lifecycle.ts +6 -6
- package/lib/internal/demo-promotion.ts +354 -0
- package/lib/internal/demo-transcript.ts +419 -0
- package/lib/internal/host-control.ts +28 -0
- package/lib/internal/manual-validation.ts +306 -0
- package/lib/internal/platform-pr.ts +15 -4
- package/lib/internal/release-workflow.ts +74 -82
- package/lib/internal/sandbox-control.ts +162 -1
- package/lib/internal/task-artifact.ts +13 -216
- package/lib/internal/task-create.ts +5 -5
- package/lib/internal/task-event.ts +20 -11
- package/lib/internal/task-operation-registry.ts +111 -3
- package/lib/internal/task-review.ts +6 -51
- package/lib/internal/task-validate.ts +26 -10
- package/lib/owned-path.ts +26 -0
- package/lib/platform/check-status.ts +9 -0
- package/lib/platform/comment-safety.ts +5 -0
- package/lib/platform/completion-backfill.ts +1 -1
- package/lib/platform/github-data.ts +322 -0
- package/lib/platform/github-provider.ts +15 -20
- package/lib/platform/issue-comments.ts +3 -4
- package/lib/platform/issues.ts +4 -245
- package/lib/platform/pr-checks.ts +13 -77
- package/lib/platform/pr-summary.ts +61 -5
- package/lib/platform/pull-requests.ts +3 -170
- package/lib/platform/snapshots.ts +53 -0
- package/lib/platform/verification-sync.ts +145 -155
- package/lib/release/workflow.ts +3 -3
- package/lib/sandbox/commands/create.ts +7 -6
- package/lib/sandbox/commands/rm.ts +3 -1909
- package/lib/sandbox/control/audit.ts +253 -0
- package/lib/sandbox/control/client.ts +109 -66
- package/lib/sandbox/control/executor.ts +134 -74
- package/lib/sandbox/control/finalization-response.ts +29 -0
- package/lib/sandbox/control/identity-sentinel.ts +137 -0
- package/lib/sandbox/control/lifecycle.ts +45 -3
- package/lib/sandbox/control/native-file-lock.ts +6 -107
- package/lib/sandbox/control/protocol.ts +75 -6
- package/lib/sandbox/control/server.ts +538 -123
- package/lib/sandbox/control/state.ts +78 -0
- package/lib/sandbox/control/task-workflow.ts +95 -0
- package/lib/sandbox/control/workflow-executor.ts +102 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/recovery.ts +28 -5
- package/lib/sandbox/removal.ts +2144 -0
- package/lib/sandbox/task-cleanup.ts +209 -0
- package/lib/sandbox/workspace-view.ts +17 -15
- package/lib/task/activity-intent.ts +2 -2
- package/lib/task/activity-log.ts +27 -19
- package/lib/task/artifact-command.ts +104 -0
- package/lib/task/artifact-lifecycle.ts +31 -26
- package/lib/task/artifact-name.ts +55 -0
- package/lib/task/artifact-operations.ts +22 -68
- package/lib/task/artifact-receipts.ts +3 -14
- package/lib/task/artifact-repair-intent.ts +99 -18
- package/lib/task/artifact-schema.ts +5 -34
- package/lib/task/capabilities.ts +13 -22
- package/lib/task/codex-orchestration.ts +85 -24
- package/lib/task/commit-identity.ts +31 -0
- package/lib/task/commit-operation.ts +2 -16
- package/lib/task/control-authority.ts +502 -5
- package/lib/task/control-recovery.ts +174 -0
- package/lib/task/create.ts +63 -8
- package/lib/task/decision-details.ts +3 -71
- package/lib/task/events.ts +131 -10
- package/lib/task/finalization-state.ts +11 -0
- package/lib/task/human-override.ts +3 -8
- package/lib/task/implementation-inputs.ts +3 -3
- package/lib/task/intermediate-cleanup.ts +455 -0
- package/lib/task/invalidation.ts +18 -62
- package/lib/task/ledger-intents.ts +7 -24
- package/lib/task/ledger.ts +1 -3
- package/lib/task/lifecycle.ts +17 -1
- package/lib/task/local-artifact-finalization.ts +148 -182
- package/lib/task/manual-validation-completion.ts +40 -0
- package/lib/task/manual-validation-evidence.ts +209 -0
- package/lib/task/manual-validation-receipt.ts +152 -0
- package/lib/task/manual-validation-shared.ts +50 -0
- package/lib/task/manual-validation-transaction.ts +304 -0
- package/lib/task/markdown.ts +70 -0
- package/lib/task/orchestration.ts +19 -19
- package/lib/task/qualification-audit.ts +22 -33
- package/lib/task/review-artifacts.ts +1 -20
- package/lib/task/review-command.ts +23 -0
- package/lib/task/review-finalization.ts +131 -259
- package/lib/task/review-fingerprint.ts +2 -2
- package/lib/task/sections.ts +33 -54
- package/lib/task/verification-engine.ts +110 -366
- package/lib/task/verification-types.ts +17 -0
- package/lib/task/verification.ts +1 -1
- package/lib/task/workflow-command.ts +18 -0
- package/lib/task/workflow-warning-intents.ts +0 -13
- package/lib/task/workflow-warnings.ts +21 -2
- package/lib/update.ts +6 -0
- package/package.json +6 -6
- package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
- package/templates/.agents/rules/testing-discipline.en.md +2 -0
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
- package/templates/.agents/scripts/package.json +3 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/release/SKILL.en.md +7 -3
- package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
- 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/skills/test/SKILL.en.md +17 -6
- package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
- package/templates/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/dist/lib/platform/adapters.js +0 -56
- package/lib/platform/adapters.ts +0 -197
|
@@ -1,29 +1,47 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { inspectDecisionDetailDuplicates
|
|
3
|
-
import {
|
|
2
|
+
import { inspectDecisionDetailDuplicates } from './decision-details.js';
|
|
3
|
+
import { scanVisibleMarkdown } from './markdown.js';
|
|
4
|
+
import { parseArtifactName } from './artifact-name.js';
|
|
5
|
+
import { hasOpenArtifactRound, validateArtifactPublication, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
4
6
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
5
7
|
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
|
|
6
8
|
import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.js';
|
|
7
9
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
8
10
|
import { readArtifactRepairIntent, writeArtifactRepairIntent } from './artifact-repair-intent.js';
|
|
11
|
+
import { consumeLifecycleRecoveryAttestation, lifecycleRecoveryAttestationDigest, validateLifecycleRecoveryAttestation } from './control-authority.js';
|
|
9
12
|
const LOCAL_ARTIFACT_REQUIRED_SECTIONS = {
|
|
10
13
|
analysis: getArtifactSchema('analysis').sections.map((section) => section.headings.zh),
|
|
11
14
|
plan: getArtifactSchema('plan').sections.map((section) => section.headings.zh),
|
|
12
15
|
code: getArtifactSchema('code').sections.map((section) => section.headings.zh)
|
|
13
16
|
};
|
|
14
|
-
function
|
|
15
|
-
return
|
|
17
|
+
function authorityDigest(authority) {
|
|
18
|
+
return lifecycleRecoveryAttestationDigest(authority);
|
|
16
19
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
20
|
+
function validFinalizerAuthority(authority, request, taskId) {
|
|
21
|
+
if (!authority)
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
validateLifecycleRecoveryAttestation(authority);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return authority.version === 1
|
|
30
|
+
&& authority.phase === 'artifact.finalize-local'
|
|
31
|
+
&& authority.taskId === taskId
|
|
32
|
+
&& authority.family === request.family
|
|
33
|
+
&& authority.artifact === request.artifact
|
|
34
|
+
&& authority.round === (parseArtifactName(request.artifact)?.round ?? 0)
|
|
35
|
+
&& authority.requestId.length > 0
|
|
36
|
+
&& authority.operationId.length > 0;
|
|
19
37
|
}
|
|
20
38
|
function consumeLocalArtifactFinalizationIntent(repoRoot, intent) {
|
|
21
39
|
if (intent.state === 'consumed')
|
|
22
40
|
return intent;
|
|
23
41
|
if (intent.state !== 'passed')
|
|
24
42
|
throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: only passed provenance can be consumed');
|
|
25
|
-
const consumed = { ...intent, state: 'consumed' };
|
|
26
|
-
|
|
43
|
+
const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
|
|
44
|
+
writeArtifactRepairIntent(repoRoot, consumed, { expected: intent });
|
|
27
45
|
return consumed;
|
|
28
46
|
}
|
|
29
47
|
function localDiagnosticCode(code) {
|
|
@@ -120,180 +138,137 @@ function failedFinalization(request, error, extra = {}) {
|
|
|
120
138
|
repairable: false, diagnostics: [], error, ...extra
|
|
121
139
|
};
|
|
122
140
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
artifactSha256,
|
|
128
|
-
semanticDigest: semanticDigestValue,
|
|
129
|
-
diagnostics: [{ code, message, repairable: false, line: null }]
|
|
141
|
+
/** Read and validate once; callers publish this content before committing provenance. */
|
|
142
|
+
function prepareLocalArtifact(request, candidate, authority) {
|
|
143
|
+
const failed = (code, message) => ({
|
|
144
|
+
result: failedFinalization(request, { code, message }), content: candidate ?? ''
|
|
130
145
|
});
|
|
131
|
-
}
|
|
132
|
-
function finalizeLocalArtifact(request) {
|
|
133
146
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: request.repoRoot });
|
|
134
147
|
if (!resolved.ok)
|
|
135
|
-
return
|
|
148
|
+
return failed(resolved.code, resolved.message);
|
|
136
149
|
const parsed = parseArtifactName(request.artifact);
|
|
137
|
-
if (!parsed || parsed.family !== request.family)
|
|
138
|
-
return
|
|
139
|
-
code: 'ARTIFACT_IDENTITY_INVALID',
|
|
140
|
-
message: `artifact '${request.artifact}' does not match ${request.family}`
|
|
141
|
-
}, { taskId: resolved.taskId, taskDir: resolved.taskDir });
|
|
142
|
-
}
|
|
143
|
-
const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
|
|
144
|
-
if (!validated.ok) {
|
|
145
|
-
return failedFinalization(request, validated.error, { taskId: resolved.taskId, taskDir: resolved.taskDir });
|
|
146
|
-
}
|
|
150
|
+
if (!parsed || parsed.family !== request.family)
|
|
151
|
+
return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
|
|
147
152
|
let content;
|
|
148
|
-
try {
|
|
149
|
-
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
return failedFinalization(request, { code: 'ARTIFACT_NOT_READABLE', message: String(error) }, {
|
|
153
|
-
taskId: resolved.taskId, taskDir: resolved.taskDir
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
153
|
let taskContent;
|
|
157
154
|
try {
|
|
158
155
|
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
156
|
+
if (candidate !== undefined) {
|
|
157
|
+
const error = validateArtifactPublication(resolved.taskDir, request.family, request.artifact);
|
|
158
|
+
if (error)
|
|
159
|
+
return failed(error.code, error.message);
|
|
160
|
+
if (resolved.state !== 'active' || !hasOpenArtifactRound(taskContent, request.family, parsed.round)) {
|
|
161
|
+
return failed('ARTIFACT_IDENTITY_INVALID', 'candidate must match one open started round in an active task');
|
|
162
|
+
}
|
|
163
|
+
content = candidate;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
|
|
167
|
+
if (!validated.ok)
|
|
168
|
+
return failed(validated.error.code, validated.error.message);
|
|
169
|
+
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
170
|
+
}
|
|
159
171
|
}
|
|
160
172
|
catch (error) {
|
|
161
|
-
return
|
|
162
|
-
taskId: resolved.taskId, taskDir: resolved.taskDir
|
|
163
|
-
});
|
|
173
|
+
return failed('ARTIFACT_NOT_READABLE', String(error));
|
|
164
174
|
}
|
|
165
|
-
const
|
|
166
|
-
family: request.family,
|
|
167
|
-
requiredSections: request.requiredSections,
|
|
168
|
-
taskContent,
|
|
169
|
-
artifact: request.artifact
|
|
175
|
+
const validation = validateLocalArtifact(content, {
|
|
176
|
+
family: request.family, requiredSections: request.requiredSections, taskContent, artifact: request.artifact
|
|
170
177
|
});
|
|
171
178
|
const artifactSha256 = sha256Content(content);
|
|
179
|
+
const { semanticDigest, repairable, diagnostics } = validation;
|
|
180
|
+
const result = failedFinalization(request, {
|
|
181
|
+
code: 'LOCAL_ARTIFACT_INVALID', message: diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
182
|
+
}, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest, repairable, diagnostics });
|
|
183
|
+
const preparedBase = { result, content, repoRoot: resolved.repoRoot };
|
|
184
|
+
if (authority !== undefined && !validFinalizerAuthority(authority, request, resolved.taskId)) {
|
|
185
|
+
return {
|
|
186
|
+
...preparedBase,
|
|
187
|
+
result: { ...result, repairable: false, error: {
|
|
188
|
+
code: 'LOCAL_EXECUTION_AUTHORITY_INVALID',
|
|
189
|
+
message: 'finalizer authority does not match the artifact tuple'
|
|
190
|
+
}, diagnostics: [] }
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const reject = (code, message) => ({
|
|
194
|
+
...prepared, result: { ...result, repairable: false, error: { code, message },
|
|
195
|
+
diagnostics: [{ code, message, repairable: false, line: null }] }
|
|
196
|
+
});
|
|
172
197
|
let intent;
|
|
173
198
|
try {
|
|
174
|
-
intent =
|
|
199
|
+
intent = readArtifactRepairIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
|
|
175
200
|
}
|
|
176
201
|
catch (error) {
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
message: error instanceof Error ? error.message : String(error)
|
|
180
|
-
}, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
|
|
202
|
+
return { ...preparedBase, result: { ...result, repairable: false, diagnostics: [],
|
|
203
|
+
error: { code: 'LOCAL_FINALIZATION_INTENT_INVALID', message: String(error) } } };
|
|
181
204
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (!intent) {
|
|
187
|
-
try {
|
|
188
|
-
writeLocalArtifactFinalizationIntent(resolved.repoRoot, {
|
|
189
|
-
version: 1,
|
|
190
|
-
taskId: resolved.taskId,
|
|
191
|
-
family: request.family,
|
|
192
|
-
artifact: request.artifact,
|
|
193
|
-
state: 'awaiting-repair',
|
|
194
|
-
baselineSemanticDigest: result.semanticDigest,
|
|
195
|
-
artifactSha256,
|
|
196
|
-
semanticDigest: result.semanticDigest
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
catch (error) {
|
|
200
|
-
return failedFinalization(request, {
|
|
201
|
-
code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED',
|
|
202
|
-
message: error instanceof Error ? error.message : String(error)
|
|
203
|
-
}, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return {
|
|
207
|
-
status: 'failed',
|
|
208
|
-
changed: false,
|
|
209
|
-
taskId: resolved.taskId,
|
|
210
|
-
taskDir: resolved.taskDir,
|
|
211
|
-
family: request.family,
|
|
212
|
-
artifact: request.artifact,
|
|
213
|
-
artifactSha256,
|
|
214
|
-
semanticDigest: result.semanticDigest,
|
|
215
|
-
repairable: true,
|
|
216
|
-
diagnostics: result.diagnostics,
|
|
217
|
-
error: {
|
|
218
|
-
code: 'LOCAL_ARTIFACT_INVALID',
|
|
219
|
-
message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
if (!result.ok) {
|
|
224
|
-
return {
|
|
225
|
-
status: 'failed',
|
|
226
|
-
changed: false,
|
|
227
|
-
taskId: resolved.taskId,
|
|
228
|
-
taskDir: resolved.taskDir,
|
|
229
|
-
family: request.family,
|
|
230
|
-
artifact: request.artifact,
|
|
231
|
-
artifactSha256,
|
|
232
|
-
semanticDigest: result.semanticDigest,
|
|
233
|
-
repairable: false,
|
|
234
|
-
diagnostics: result.diagnostics,
|
|
235
|
-
error: {
|
|
236
|
-
code: 'LOCAL_ARTIFACT_INVALID',
|
|
237
|
-
message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
if (intent?.state === 'awaiting-repair' && intent.baselineSemanticDigest !== result.semanticDigest) {
|
|
242
|
-
return provenanceFailure(request, resolved, content, artifactSha256, result.semanticDigest, 'LOCAL_REPAIR_BASELINE_MISMATCH', 'the repaired artifact semantic digest does not match the recorded repair baseline');
|
|
205
|
+
const prepared = { ...preparedBase, expectedIntent: intent };
|
|
206
|
+
if (intent && intent.recoveryOperationId !== null
|
|
207
|
+
&& (!authority || intent.recoveryOperationId !== authority.operationId)) {
|
|
208
|
+
return reject('LOCAL_REPAIR_PROVENANCE_CONFLICT', 'local repair provenance is owned by another recovery operation');
|
|
243
209
|
}
|
|
244
|
-
if (
|
|
245
|
-
&& (intent.
|
|
246
|
-
|
|
210
|
+
if (repairable) {
|
|
211
|
+
if (intent && (intent.state !== 'awaiting-repair' || intent.baselineSemanticDigest !== semanticDigest)) {
|
|
212
|
+
return reject('LOCAL_REPAIR_PROVENANCE_CONFLICT', 'a different local repair baseline is already recorded for this artifact');
|
|
213
|
+
}
|
|
214
|
+
if (intent && !authority)
|
|
215
|
+
return prepared;
|
|
247
216
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
};
|
|
217
|
+
else {
|
|
218
|
+
if (!validation.ok)
|
|
219
|
+
return prepared;
|
|
220
|
+
if (intent?.state === 'awaiting-repair' && intent.baselineSemanticDigest !== semanticDigest) {
|
|
221
|
+
return reject('LOCAL_REPAIR_BASELINE_MISMATCH', 'the repaired artifact semantic digest does not match the recorded repair baseline');
|
|
222
|
+
}
|
|
223
|
+
if ((intent?.state === 'passed' || intent?.state === 'consumed')
|
|
224
|
+
&& (intent.artifactSha256 !== artifactSha256 || intent.semanticDigest !== semanticDigest)) {
|
|
225
|
+
return reject('LOCAL_REPAIR_PROVENANCE_CONFLICT', 'the artifact changed after its finalization provenance was recorded');
|
|
226
|
+
}
|
|
227
|
+
prepared.result = { ...result, status: 'passed', error: null };
|
|
228
|
+
if (intent?.state === 'consumed')
|
|
229
|
+
return prepared;
|
|
262
230
|
}
|
|
231
|
+
const timestamp = Date.now();
|
|
232
|
+
return { ...prepared, provenance: {
|
|
233
|
+
version: 2, taskId: resolved.taskId, family: request.family, artifact: request.artifact,
|
|
234
|
+
state: repairable ? 'awaiting-repair' : 'passed',
|
|
235
|
+
baselineSemanticDigest: repairable ? semanticDigest : intent?.baselineSemanticDigest ?? null,
|
|
236
|
+
artifactSha256, semanticDigest,
|
|
237
|
+
recoveryOperationId: authority?.operationId ?? intent?.recoveryOperationId ?? null,
|
|
238
|
+
phase: authority?.phase ?? intent?.phase ?? null,
|
|
239
|
+
authorityDigest: authority ? authorityDigest(authority) : intent?.authorityDigest ?? null,
|
|
240
|
+
requestId: authority?.lifecycleRequestId ?? intent?.requestId ?? `local-finalize:${resolved.taskId}`,
|
|
241
|
+
createdAt: intent?.createdAt ?? timestamp,
|
|
242
|
+
updatedAt: timestamp
|
|
243
|
+
} };
|
|
244
|
+
}
|
|
245
|
+
function commitLocalArtifactProvenance(prepared) {
|
|
263
246
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
artifact: request.artifact,
|
|
269
|
-
state: 'passed',
|
|
270
|
-
baselineSemanticDigest: intent?.baselineSemanticDigest ?? null,
|
|
271
|
-
artifactSha256,
|
|
272
|
-
semanticDigest: result.semanticDigest
|
|
273
|
-
});
|
|
247
|
+
if (prepared.provenance) {
|
|
248
|
+
writeArtifactRepairIntent(prepared.repoRoot, prepared.provenance, { expected: prepared.expectedIntent ?? null });
|
|
249
|
+
}
|
|
250
|
+
return prepared.result;
|
|
274
251
|
}
|
|
275
252
|
catch (error) {
|
|
276
|
-
return
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
|
|
253
|
+
return { ...prepared.result, status: 'failed', error: {
|
|
254
|
+
code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED', message: String(error)
|
|
255
|
+
} };
|
|
280
256
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
artifact: request.artifact,
|
|
288
|
-
artifactSha256,
|
|
289
|
-
semanticDigest: result.semanticDigest,
|
|
290
|
-
repairable: result.repairable,
|
|
291
|
-
diagnostics: result.diagnostics,
|
|
292
|
-
error: result.ok ? null : {
|
|
293
|
-
code: 'LOCAL_ARTIFACT_INVALID',
|
|
294
|
-
message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
257
|
+
}
|
|
258
|
+
function finalizeLocalArtifact(request, authority, options = {}) {
|
|
259
|
+
const result = commitLocalArtifactProvenance(prepareLocalArtifact(request, undefined, authority));
|
|
260
|
+
if (authority && !options.deferLifecycleRecoveryConsumption && result.status === 'passed') {
|
|
261
|
+
try {
|
|
262
|
+
consumeLifecycleRecoveryAttestation(authority);
|
|
295
263
|
}
|
|
296
|
-
|
|
264
|
+
catch (error) {
|
|
265
|
+
return { ...result, status: 'failed', error: {
|
|
266
|
+
code: 'LOCAL_EXECUTION_AUTHORITY_CONSUME_FAILED',
|
|
267
|
+
message: error instanceof Error ? error.message : String(error)
|
|
268
|
+
} };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
297
272
|
}
|
|
298
|
-
export { LOCAL_ARTIFACT_REQUIRED_SECTIONS, consumeLocalArtifactFinalizationIntent, finalizeLocalArtifact, readLocalArtifactFinalizationIntent, canonicalSemanticDigest as semanticDigest, sha256Content, validateLocalArtifact };
|
|
273
|
+
export { LOCAL_ARTIFACT_REQUIRED_SECTIONS, consumeLocalArtifactFinalizationIntent, finalizeLocalArtifact, prepareLocalArtifact, commitLocalArtifactProvenance, readArtifactRepairIntent as readLocalArtifactFinalizationIntent, canonicalSemanticDigest as semanticDigest, sha256Content, validateLocalArtifact };
|
|
299
274
|
//# sourceMappingURL=local-artifact-finalization.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readManualValidationReceipt } from './manual-validation-receipt.js';
|
|
2
|
+
import { readManualValidationTransaction } from './manual-validation-transaction.js';
|
|
3
|
+
function readManualValidationCompletion(taskDir, expected = {}) {
|
|
4
|
+
const { receiptDigest: expectedReceiptDigest, ...receiptIdentity } = expected;
|
|
5
|
+
const receipt = readManualValidationReceipt(taskDir, receiptIdentity);
|
|
6
|
+
if (!receipt.ok)
|
|
7
|
+
return receipt;
|
|
8
|
+
if (expectedReceiptDigest !== undefined && receipt.value.receiptDigest !== expectedReceiptDigest) {
|
|
9
|
+
return { ok: false, error: { code: 'MANUAL_VALIDATION_RECEIPT_IDENTITY_MISMATCH', message: 'receipt digest does not match the expected completion' } };
|
|
10
|
+
}
|
|
11
|
+
const transaction = readManualValidationTransaction(taskDir, {
|
|
12
|
+
transactionId: receipt.value.transactionId,
|
|
13
|
+
taskId: receipt.value.taskId,
|
|
14
|
+
prNumber: receipt.value.prNumber,
|
|
15
|
+
prHeadSha: receipt.value.prHeadSha,
|
|
16
|
+
artifact: receipt.value.artifact
|
|
17
|
+
});
|
|
18
|
+
if (!transaction.ok)
|
|
19
|
+
return transaction;
|
|
20
|
+
if (transaction.value.committedReceipt !== receipt.value.receiptDigest) {
|
|
21
|
+
return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'completion transaction is not bound to the canonical receipt' } };
|
|
22
|
+
}
|
|
23
|
+
return { ok: true, value: { receipt: receipt.value, transaction: transaction.value } };
|
|
24
|
+
}
|
|
25
|
+
export { readManualValidationCompletion };
|
|
26
|
+
//# sourceMappingURL=manual-validation-completion.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { MANUAL_VALIDATION_SHA40 as SHA40, MANUAL_VALIDATION_TASK_ID as TASK_ID, exactKeys, isRecord, validTimestamp } from './manual-validation-shared.js';
|
|
4
|
+
const MANUAL_VALIDATION_EVIDENCE_SCHEMA = 'agent-infra/branch-only-validation-evidence';
|
|
5
|
+
const MANUAL_VALIDATION_EVIDENCE_VERSION = 1;
|
|
6
|
+
function invalid(message) {
|
|
7
|
+
return { ok: false, error: { code: 'MANUAL_VALIDATION_EVIDENCE_INVALID', message } };
|
|
8
|
+
}
|
|
9
|
+
function validateShape(value) {
|
|
10
|
+
if (!isRecord(value))
|
|
11
|
+
return invalid('evidence must be a JSON object');
|
|
12
|
+
const topError = exactKeys(value, ['schema', 'version', 'mode', 'taskId', 'branch', 'commit', 'recoverable', 'source', 'result'], 'evidence');
|
|
13
|
+
if (topError)
|
|
14
|
+
return invalid(topError);
|
|
15
|
+
if (value.schema !== MANUAL_VALIDATION_EVIDENCE_SCHEMA || value.version !== MANUAL_VALIDATION_EVIDENCE_VERSION) {
|
|
16
|
+
return invalid('evidence schema or version is unsupported');
|
|
17
|
+
}
|
|
18
|
+
if (value.mode !== 'task-bound' && value.mode !== 'branch-only')
|
|
19
|
+
return invalid('mode must be task-bound or branch-only');
|
|
20
|
+
if (value.taskId !== null && (typeof value.taskId !== 'string' || !TASK_ID.test(value.taskId)))
|
|
21
|
+
return invalid('taskId must be a TASK identifier or null');
|
|
22
|
+
if (typeof value.branch !== 'string' || !value.branch.trim() || /[\r\n]/u.test(value.branch))
|
|
23
|
+
return invalid('branch must be a non-empty single line');
|
|
24
|
+
if (typeof value.commit !== 'string' || !SHA40.test(value.commit))
|
|
25
|
+
return invalid('commit must be a 40-character lowercase hexadecimal SHA');
|
|
26
|
+
if (typeof value.recoverable !== 'boolean')
|
|
27
|
+
return invalid('recoverable must be boolean');
|
|
28
|
+
if (value.source !== 'run-manual-validation')
|
|
29
|
+
return invalid('source is unsupported');
|
|
30
|
+
if (value.mode === 'branch-only' && (value.taskId !== null || value.recoverable !== false))
|
|
31
|
+
return invalid('branch-only evidence must be non-recoverable and task-free');
|
|
32
|
+
if (value.mode === 'task-bound' && (value.taskId === null || value.recoverable !== true))
|
|
33
|
+
return invalid('task-bound evidence must identify a recoverable task');
|
|
34
|
+
if (!isRecord(value.result))
|
|
35
|
+
return invalid('result must be a JSON object');
|
|
36
|
+
const result = value.result;
|
|
37
|
+
const resultError = exactKeys(result, ['scope', 'command', 'startedAt', 'completedAt', 'exitCode', 'signal', 'cleanup'], 'result');
|
|
38
|
+
if (resultError)
|
|
39
|
+
return invalid(resultError);
|
|
40
|
+
if (result.scope !== 'snapshot' && result.scope !== 'inplace')
|
|
41
|
+
return invalid('result.scope is invalid');
|
|
42
|
+
if (typeof result.command !== 'string' || !result.command || /[\\/\r\n]/u.test(result.command))
|
|
43
|
+
return invalid('result.command must be a basename');
|
|
44
|
+
if (!validTimestamp(result.startedAt) || !validTimestamp(result.completedAt) || Date.parse(result.completedAt) < Date.parse(result.startedAt))
|
|
45
|
+
return invalid('result timestamps are invalid');
|
|
46
|
+
const exitCode = result.exitCode;
|
|
47
|
+
if (!Number.isSafeInteger(exitCode) || exitCode < 0)
|
|
48
|
+
return invalid('result.exitCode must be a non-negative integer');
|
|
49
|
+
if (result.signal !== null && (typeof result.signal !== 'string' || !result.signal || /[\r\n]/u.test(result.signal)))
|
|
50
|
+
return invalid('result.signal must be a string or null');
|
|
51
|
+
if (!['completed', 'pending', 'failed', 'not-created'].includes(result.cleanup))
|
|
52
|
+
return invalid('result.cleanup is invalid');
|
|
53
|
+
return { ok: true, value: value };
|
|
54
|
+
}
|
|
55
|
+
function createManualValidationEvidence(input) {
|
|
56
|
+
const value = {
|
|
57
|
+
schema: MANUAL_VALIDATION_EVIDENCE_SCHEMA,
|
|
58
|
+
version: MANUAL_VALIDATION_EVIDENCE_VERSION,
|
|
59
|
+
mode: input.mode,
|
|
60
|
+
taskId: input.taskId,
|
|
61
|
+
branch: input.branch,
|
|
62
|
+
commit: input.commit,
|
|
63
|
+
recoverable: input.recoverable,
|
|
64
|
+
source: 'run-manual-validation',
|
|
65
|
+
result: {
|
|
66
|
+
scope: input.scope,
|
|
67
|
+
command: input.command,
|
|
68
|
+
startedAt: input.startedAt,
|
|
69
|
+
completedAt: input.completedAt,
|
|
70
|
+
exitCode: input.exitCode,
|
|
71
|
+
signal: input.signal,
|
|
72
|
+
cleanup: input.cleanup
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const checked = validateShape(value);
|
|
76
|
+
if (!checked.ok)
|
|
77
|
+
throw new Error(`${checked.error.code}: ${checked.error.message}`);
|
|
78
|
+
return checked.value;
|
|
79
|
+
}
|
|
80
|
+
function manualValidationEvidenceSummary(evidence) {
|
|
81
|
+
return {
|
|
82
|
+
version: 1,
|
|
83
|
+
taskId: evidence.taskId,
|
|
84
|
+
branch: evidence.branch,
|
|
85
|
+
scope: evidence.result.scope,
|
|
86
|
+
commit: evidence.commit,
|
|
87
|
+
command: evidence.result.command,
|
|
88
|
+
startedAt: evidence.result.startedAt,
|
|
89
|
+
completedAt: evidence.result.completedAt,
|
|
90
|
+
exitCode: evidence.result.exitCode,
|
|
91
|
+
signal: evidence.result.signal,
|
|
92
|
+
cleanup: evidence.result.cleanup
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function validateManualValidationEvidence(value, expected) {
|
|
96
|
+
const shape = validateShape(value);
|
|
97
|
+
if (!shape.ok)
|
|
98
|
+
return shape;
|
|
99
|
+
const evidence = shape.value;
|
|
100
|
+
if (evidence.result.exitCode !== 0 || evidence.result.signal !== null || evidence.result.cleanup !== 'completed') {
|
|
101
|
+
return { ok: false, error: { code: 'MANUAL_VALIDATION_EVIDENCE_UNSUCCESSFUL', message: 'validation must exit 0 without a signal and complete cleanup' } };
|
|
102
|
+
}
|
|
103
|
+
if (expected) {
|
|
104
|
+
if (evidence.taskId !== expected.taskId || evidence.branch !== expected.branch) {
|
|
105
|
+
return { ok: false, error: { code: 'MANUAL_VALIDATION_EVIDENCE_IDENTITY_MISMATCH', message: 'evidence task or branch does not match the expected identity' } };
|
|
106
|
+
}
|
|
107
|
+
if (expected.commit !== undefined && evidence.commit !== expected.commit) {
|
|
108
|
+
return { ok: false, error: { code: 'MANUAL_VALIDATION_EVIDENCE_STALE', message: 'evidence commit does not match the current head' } };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return shape;
|
|
112
|
+
}
|
|
113
|
+
function readManualValidationEvidence(file, expected) {
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const code = error.code === 'ENOENT'
|
|
120
|
+
? 'MANUAL_VALIDATION_EVIDENCE_MISSING'
|
|
121
|
+
: 'MANUAL_VALIDATION_EVIDENCE_INVALID';
|
|
122
|
+
return { ok: false, error: { code, message: error instanceof Error ? error.message : String(error) } };
|
|
123
|
+
}
|
|
124
|
+
return validateManualValidationEvidence(parsed, expected);
|
|
125
|
+
}
|
|
126
|
+
function manualValidationEvidenceDigest(evidence) {
|
|
127
|
+
return createHash('sha256').update(JSON.stringify(evidence), 'utf8').digest('hex');
|
|
128
|
+
}
|
|
129
|
+
export { MANUAL_VALIDATION_EVIDENCE_SCHEMA, MANUAL_VALIDATION_EVIDENCE_VERSION, createManualValidationEvidence, manualValidationEvidenceSummary, manualValidationEvidenceDigest, readManualValidationEvidence, validateManualValidationEvidence };
|
|
130
|
+
//# sourceMappingURL=manual-validation-evidence.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { MANUAL_VALIDATION_ARTIFACT as ARTIFACT, MANUAL_VALIDATION_SHA40 as SHA40, MANUAL_VALIDATION_SHA64 as SHA64, MANUAL_VALIDATION_TASK_ID as TASK_ID, exactKeys, isRecord, validTimestamp, writeJsonAtomic } from './manual-validation-shared.js';
|
|
5
|
+
const MANUAL_VALIDATION_RECEIPT_SCHEMA = 'agent-infra/manual-validation-receipt';
|
|
6
|
+
const MANUAL_VALIDATION_RECEIPT_VERSION = 1;
|
|
7
|
+
function invalid(message) {
|
|
8
|
+
return { ok: false, error: { code: 'MANUAL_VALIDATION_RECEIPT_INVALID', message } };
|
|
9
|
+
}
|
|
10
|
+
function canonicalPayload(receipt) {
|
|
11
|
+
return {
|
|
12
|
+
schema: MANUAL_VALIDATION_RECEIPT_SCHEMA,
|
|
13
|
+
version: MANUAL_VALIDATION_RECEIPT_VERSION,
|
|
14
|
+
transactionId: receipt.transactionId,
|
|
15
|
+
taskId: receipt.taskId,
|
|
16
|
+
prNumber: receipt.prNumber,
|
|
17
|
+
prHeadSha: receipt.prHeadSha,
|
|
18
|
+
evidenceDigest: receipt.evidenceDigest,
|
|
19
|
+
artifact: receipt.artifact,
|
|
20
|
+
artifactSha256: receipt.artifactSha256,
|
|
21
|
+
pendingSummaryDigest: receipt.pendingSummaryDigest,
|
|
22
|
+
finalSummaryDigest: receipt.finalSummaryDigest,
|
|
23
|
+
committedAt: receipt.committedAt
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function manualValidationReceiptDigest(receipt) {
|
|
27
|
+
return createHash('sha256').update(JSON.stringify(canonicalPayload(receipt)), 'utf8').digest('hex');
|
|
28
|
+
}
|
|
29
|
+
function manualValidationFinalSummaryDigest(body) {
|
|
30
|
+
const heading = /^###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)\s*$/mu.exec(body);
|
|
31
|
+
const projection = heading ? body.slice(heading.index).replace(/\s+$/u, '') : body;
|
|
32
|
+
const preimage = projection.replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
|
|
33
|
+
return createHash('sha256').update(preimage, 'utf8').digest('hex');
|
|
34
|
+
}
|
|
35
|
+
function manualValidationFinalSummaryProjectionMatches(body, receipt) {
|
|
36
|
+
const identity = `transaction=${receipt.transactionId}; receipt=${receipt.receiptDigest}; evidence=${receipt.evidenceDigest}; head=${receipt.prHeadSha}`;
|
|
37
|
+
return body.includes(identity) && manualValidationFinalSummaryDigest(body) === receipt.finalSummaryDigest;
|
|
38
|
+
}
|
|
39
|
+
function validateManualValidationReceipt(value, expected) {
|
|
40
|
+
if (!isRecord(value))
|
|
41
|
+
return invalid('receipt must be a JSON object');
|
|
42
|
+
const keys = ['schema', 'version', 'transactionId', 'taskId', 'prNumber', 'prHeadSha', 'evidenceDigest', 'artifact', 'artifactSha256', 'pendingSummaryDigest', 'finalSummaryDigest', 'committedAt', 'receiptDigest'];
|
|
43
|
+
const keyError = exactKeys(value, keys, 'receipt');
|
|
44
|
+
if (keyError)
|
|
45
|
+
return invalid(keyError);
|
|
46
|
+
if (value.schema !== MANUAL_VALIDATION_RECEIPT_SCHEMA || value.version !== MANUAL_VALIDATION_RECEIPT_VERSION)
|
|
47
|
+
return invalid('receipt schema or version is unsupported');
|
|
48
|
+
if (typeof value.transactionId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.transactionId))
|
|
49
|
+
return invalid('transactionId is invalid');
|
|
50
|
+
if (typeof value.taskId !== 'string' || !TASK_ID.test(value.taskId))
|
|
51
|
+
return invalid('taskId is invalid');
|
|
52
|
+
const prNumber = value.prNumber;
|
|
53
|
+
if (!Number.isSafeInteger(prNumber) || prNumber <= 0)
|
|
54
|
+
return invalid('prNumber is invalid');
|
|
55
|
+
for (const [name, pattern] of [['prHeadSha', SHA40], ['evidenceDigest', SHA64], ['artifactSha256', SHA64], ['pendingSummaryDigest', SHA64], ['finalSummaryDigest', SHA64], ['receiptDigest', SHA64]]) {
|
|
56
|
+
if (typeof value[name] !== 'string' || !pattern.test(value[name]))
|
|
57
|
+
return invalid(`${name} is invalid`);
|
|
58
|
+
}
|
|
59
|
+
if (typeof value.artifact !== 'string' || !ARTIFACT.test(value.artifact))
|
|
60
|
+
return invalid('artifact is not a canonical manual-validation artifact');
|
|
61
|
+
if (!validTimestamp(value.committedAt))
|
|
62
|
+
return invalid('committedAt is invalid');
|
|
63
|
+
const receipt = value;
|
|
64
|
+
if (manualValidationReceiptDigest(receipt) !== receipt.receiptDigest)
|
|
65
|
+
return invalid('receiptDigest does not match receipt contents');
|
|
66
|
+
if (expected && Object.entries(expected).some(([key, expectedValue]) => expectedValue !== undefined && receipt[key] !== expectedValue)) {
|
|
67
|
+
return { ok: false, error: { code: 'MANUAL_VALIDATION_RECEIPT_IDENTITY_MISMATCH', message: 'receipt identity does not match the expected transaction' } };
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, value: receipt };
|
|
70
|
+
}
|
|
71
|
+
function createManualValidationReceipt(input) {
|
|
72
|
+
const payload = canonicalPayload(input);
|
|
73
|
+
const receipt = { ...payload, receiptDigest: manualValidationReceiptDigest(input) };
|
|
74
|
+
const checked = validateManualValidationReceipt(receipt);
|
|
75
|
+
if (!checked.ok)
|
|
76
|
+
throw new Error(`${checked.error.code}: ${checked.error.message}`);
|
|
77
|
+
return checked.value;
|
|
78
|
+
}
|
|
79
|
+
function manualValidationReceiptPath(taskDir) {
|
|
80
|
+
return path.join(taskDir, '.manual-validation', 'receipt.json');
|
|
81
|
+
}
|
|
82
|
+
function writeManualValidationReceiptAtomic(taskDir, receipt) {
|
|
83
|
+
const checked = validateManualValidationReceipt(receipt);
|
|
84
|
+
if (!checked.ok)
|
|
85
|
+
throw new Error(`${checked.error.code}: ${checked.error.message}`);
|
|
86
|
+
const target = manualValidationReceiptPath(taskDir);
|
|
87
|
+
return writeJsonAtomic(target, receipt);
|
|
88
|
+
}
|
|
89
|
+
function readManualValidationReceipt(taskDir, expected) {
|
|
90
|
+
const file = manualValidationReceiptPath(taskDir);
|
|
91
|
+
let value;
|
|
92
|
+
try {
|
|
93
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return { ok: false, error: { code: error.code === 'ENOENT' ? 'MANUAL_VALIDATION_RECEIPT_MISSING' : 'MANUAL_VALIDATION_RECEIPT_INVALID', message: error instanceof Error ? error.message : String(error) } };
|
|
97
|
+
}
|
|
98
|
+
return validateManualValidationReceipt(value, expected);
|
|
99
|
+
}
|
|
100
|
+
export { MANUAL_VALIDATION_RECEIPT_SCHEMA, MANUAL_VALIDATION_RECEIPT_VERSION, createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, manualValidationReceiptDigest, manualValidationReceiptPath, readManualValidationReceipt, validateManualValidationReceipt, writeManualValidationReceiptAtomic };
|
|
101
|
+
//# sourceMappingURL=manual-validation-receipt.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
const MANUAL_VALIDATION_TASK_ID = /^TASK-\d{8}-\d{6}$/u;
|
|
5
|
+
const MANUAL_VALIDATION_SHA40 = /^[a-f0-9]{40}$/u;
|
|
6
|
+
const MANUAL_VALIDATION_SHA64 = /^[a-f0-9]{64}$/u;
|
|
7
|
+
const MANUAL_VALIDATION_ARTIFACT = /^manual-validation(?:-r[2-9]|-r[1-9]\d+)?.md$/u;
|
|
8
|
+
const MANUAL_VALIDATION_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function exactKeys(value, keys, label) {
|
|
13
|
+
const allowed = new Set(keys);
|
|
14
|
+
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
15
|
+
if (unknown)
|
|
16
|
+
return `${label} contains unknown field '${unknown}'`;
|
|
17
|
+
const missing = keys.find((key) => !Object.hasOwn(value, key));
|
|
18
|
+
return missing ? `${label} is missing '${missing}'` : null;
|
|
19
|
+
}
|
|
20
|
+
function validTimestamp(value) {
|
|
21
|
+
return typeof value === 'string' && MANUAL_VALIDATION_TIMESTAMP.test(value) && Number.isFinite(Date.parse(value));
|
|
22
|
+
}
|
|
23
|
+
function writeJsonAtomic(file, value) {
|
|
24
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
25
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
26
|
+
try {
|
|
27
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
28
|
+
fs.renameSync(temporary, file);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
try {
|
|
32
|
+
fs.unlinkSync(temporary);
|
|
33
|
+
}
|
|
34
|
+
catch { /* best effort cleanup of our own temp file */ }
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
return file;
|
|
38
|
+
}
|
|
39
|
+
export { MANUAL_VALIDATION_ARTIFACT, MANUAL_VALIDATION_SHA40, MANUAL_VALIDATION_SHA64, MANUAL_VALIDATION_TASK_ID, MANUAL_VALIDATION_TIMESTAMP, exactKeys, isRecord, validTimestamp, writeJsonAtomic };
|
|
40
|
+
//# sourceMappingURL=manual-validation-shared.js.map
|