pi-background-tasks 2.5.0 → 2.6.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/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
- package/README.md +70 -25
- package/TESTING.md +42 -2
- package/TEST_PLAN.md +7 -4
- package/dist/extensions/anthropic-attribution-child.js +4 -0
- package/dist/extensions/anthropic-attribution-child.js.map +1 -0
- package/dist/extensions/anthropic-attribution.js +107 -0
- package/dist/extensions/anthropic-attribution.js.map +1 -0
- package/dist/extensions/background-tasks.js +2 -0
- package/dist/extensions/background-tasks.js.map +1 -0
- package/dist/extensions/delegate-child.js +2 -0
- package/dist/extensions/delegate-child.js.map +1 -0
- package/dist/extensions/fusion-child.js +2 -0
- package/dist/extensions/fusion-child.js.map +1 -0
- package/dist/package.json +5 -0
- package/dist/src/core/anthropic-attribution-path.js +24 -0
- package/dist/src/core/anthropic-attribution-path.js.map +1 -0
- package/dist/src/core/anthropic-attribution.js +2247 -0
- package/dist/src/core/anthropic-attribution.js.map +1 -0
- package/dist/src/core/attested-pi-contract.js +5 -0
- package/dist/src/core/attested-pi-contract.js.map +1 -0
- package/dist/src/core/attested-pi-run.js +749 -0
- package/dist/src/core/attested-pi-run.js.map +1 -0
- package/dist/src/core/canonical-json.js +19 -0
- package/dist/src/core/canonical-json.js.map +1 -0
- package/dist/src/core/common.js +787 -0
- package/dist/src/core/common.js.map +1 -0
- package/dist/src/core/config.js +78 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/context/parent-snapshot.js +75 -0
- package/dist/src/core/context/parent-snapshot.js.map +1 -0
- package/dist/src/core/context/token-budget.js +631 -0
- package/dist/src/core/context/token-budget.js.map +1 -0
- package/dist/src/core/context/visible-conversation-v2.js +390 -0
- package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
- package/dist/src/core/delegate/artifacts.js +357 -0
- package/dist/src/core/delegate/artifacts.js.map +1 -0
- package/dist/src/core/delegate/budget.js +266 -0
- package/dist/src/core/delegate/budget.js.map +1 -0
- package/dist/src/core/delegate/facade-contract.js +7 -0
- package/dist/src/core/delegate/facade-contract.js.map +1 -0
- package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
- package/dist/src/core/delegate/hook-contract.js +124 -0
- package/dist/src/core/delegate/hook-contract.js.map +1 -0
- package/dist/src/core/delegate/launch.js +366 -0
- package/dist/src/core/delegate/launch.js.map +1 -0
- package/dist/src/core/delegate/result-package.js +293 -0
- package/dist/src/core/delegate/result-package.js.map +1 -0
- package/dist/src/core/delegate/runner.js +398 -0
- package/dist/src/core/delegate/runner.js.map +1 -0
- package/dist/src/core/delegate/seed.js +334 -0
- package/dist/src/core/delegate/seed.js.map +1 -0
- package/dist/src/core/delegate/types.js +103 -0
- package/dist/src/core/delegate/types.js.map +1 -0
- package/dist/src/core/durable-fs.js +372 -0
- package/dist/src/core/durable-fs.js.map +1 -0
- package/dist/src/core/extension-api.js +382 -0
- package/dist/src/core/extension-api.js.map +1 -0
- package/dist/src/core/fusion/artifacts.js +654 -0
- package/dist/src/core/fusion/artifacts.js.map +1 -0
- package/dist/src/core/fusion/budget.js +949 -0
- package/dist/src/core/fusion/budget.js.map +1 -0
- package/dist/src/core/fusion/child-protocol.js +176 -0
- package/dist/src/core/fusion/child-protocol.js.map +1 -0
- package/dist/src/core/fusion/claude-cache.js +156 -0
- package/dist/src/core/fusion/claude-cache.js.map +1 -0
- package/dist/src/core/fusion/clean-context.js +64 -0
- package/dist/src/core/fusion/clean-context.js.map +1 -0
- package/dist/src/core/fusion/config.js +349 -0
- package/dist/src/core/fusion/config.js.map +1 -0
- package/dist/src/core/fusion/context.js +184 -0
- package/dist/src/core/fusion/context.js.map +1 -0
- package/dist/src/core/fusion/evaluation.js +696 -0
- package/dist/src/core/fusion/evaluation.js.map +1 -0
- package/dist/src/core/fusion/facade-contract.js +3 -0
- package/dist/src/core/fusion/facade-contract.js.map +1 -0
- package/dist/src/core/fusion/orchestrator.js +867 -0
- package/dist/src/core/fusion/orchestrator.js.map +1 -0
- package/dist/src/core/fusion/output-contract.js +27 -0
- package/dist/src/core/fusion/output-contract.js.map +1 -0
- package/dist/src/core/fusion/pi-child.js +1741 -0
- package/dist/src/core/fusion/pi-child.js.map +1 -0
- package/dist/src/core/fusion/prompts.js +260 -0
- package/dist/src/core/fusion/prompts.js.map +1 -0
- package/dist/src/core/fusion/result-package.js +857 -0
- package/dist/src/core/fusion/result-package.js.map +1 -0
- package/dist/src/core/fusion/source-policy.js +259 -0
- package/dist/src/core/fusion/source-policy.js.map +1 -0
- package/dist/src/core/fusion/types.js +207 -0
- package/dist/src/core/fusion/types.js.map +1 -0
- package/dist/src/core/fusion/web-fetch.js +777 -0
- package/dist/src/core/fusion/web-fetch.js.map +1 -0
- package/dist/src/core/fusion/workflows.js +126 -0
- package/dist/src/core/fusion/workflows.js.map +1 -0
- package/dist/src/core/lazy-module.js +181 -0
- package/dist/src/core/lazy-module.js.map +1 -0
- package/dist/src/core/pi-launch.js +431 -0
- package/dist/src/core/pi-launch.js.map +1 -0
- package/dist/src/core/registry.js +3331 -0
- package/dist/src/core/registry.js.map +1 -0
- package/dist/src/core/reload-shell-owner.js +1396 -0
- package/dist/src/core/reload-shell-owner.js.map +1 -0
- package/dist/src/core/shell-policy.js +80 -0
- package/dist/src/core/shell-policy.js.map +1 -0
- package/dist/src/core/task-durable.js +51 -0
- package/dist/src/core/task-durable.js.map +1 -0
- package/dist/src/core/update-check.js +92 -0
- package/dist/src/core/update-check.js.map +1 -0
- package/dist/src/core/windows-taskkill.js +185 -0
- package/dist/src/core/windows-taskkill.js.map +1 -0
- package/dist/src/delegate-child-extension.js +776 -0
- package/dist/src/delegate-child-extension.js.map +1 -0
- package/dist/src/delegate-extension.js +746 -0
- package/dist/src/delegate-extension.js.map +1 -0
- package/dist/src/extension.js +953 -0
- package/dist/src/extension.js.map +1 -0
- package/dist/src/fusion-child-extension.js +760 -0
- package/dist/src/fusion-child-extension.js.map +1 -0
- package/dist/src/fusion-extension.js +1030 -0
- package/dist/src/fusion-extension.js.map +1 -0
- package/dist/src/ui/background-tasks-manager.js +689 -0
- package/dist/src/ui/background-tasks-manager.js.map +1 -0
- package/dist/src/ui/fusion-model-selector.js +277 -0
- package/dist/src/ui/fusion-model-selector.js.map +1 -0
- package/docs/INDEX.md +35 -33
- package/docs/api/eventbus-v1.md +21 -5
- package/docs/choose-a-workflow.md +2 -0
- package/docs/commands/bg-clear.md +3 -3
- package/docs/commands/bg-update.md +3 -3
- package/docs/commands/bg.md +18 -6
- package/docs/commands/claude-cache.md +4 -4
- package/docs/commands/fusion-models.md +49 -4
- package/docs/commands/fusion.md +3 -3
- package/docs/commands/jobs.md +3 -3
- package/docs/commands/kill.md +3 -3
- package/docs/commands/logs.md +3 -3
- package/docs/commands/task-manager.md +5 -5
- package/docs/concepts/completion-delivery.md +6 -2
- package/docs/getting-started.md +7 -1
- package/docs/manifest.json +228 -42
- package/docs/operations/configuration.md +58 -5
- package/docs/operations/releasing.md +4 -2
- package/docs/operations/testing.md +22 -0
- package/docs/operations/troubleshooting.md +6 -2
- package/docs/read-before-edit.md +44 -31
- package/docs/reference/runtime-contracts.md +87 -51
- package/docs/reference/shortcuts-and-dock.md +28 -14
- package/docs/subsystems/anthropic-attribution.md +23 -11
- package/docs/subsystems/attested-pi-runs.md +12 -7
- package/docs/subsystems/background-task-runtime.md +53 -11
- package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
- package/docs/subsystems/delegation.md +16 -3
- package/docs/subsystems/docs-freshness-gate.md +14 -7
- package/docs/subsystems/fusion.md +36 -11
- package/docs/subsystems/host-ui-and-telemetry.md +22 -9
- package/docs/tools/bg_delegate.md +3 -1
- package/docs/tools/bg_kill.md +3 -1
- package/docs/tools/bg_logs.md +3 -1
- package/docs/tools/bg_result.md +5 -3
- package/docs/tools/bg_run.md +31 -5
- package/docs/tools/bg_run_pi_attested.md +3 -1
- package/docs/tools/bg_status.md +3 -1
- package/docs/tools/fusion_investigate.md +3 -1
- package/docs/tools/fusion_reason.md +3 -1
- package/docs/tools/fusion_research.md +3 -1
- package/docs/tools/fusion_validate.md +3 -1
- package/extensions/anthropic-attribution-child.ts +3 -0
- package/extensions/anthropic-attribution.ts +166 -1
- package/package.json +12 -10
- package/src/core/anthropic-attribution-path.ts +5 -4
- package/src/core/anthropic-attribution.ts +293 -113
- package/src/core/attested-pi-contract.ts +4 -0
- package/src/core/attested-pi-run.ts +468 -81
- package/src/core/canonical-json.ts +21 -0
- package/src/core/common.ts +450 -40
- package/src/core/config.ts +121 -0
- package/src/core/context/visible-conversation-v2.ts +3 -6
- package/src/core/delegate/artifacts.ts +13 -9
- package/src/core/delegate/budget.ts +13 -6
- package/src/core/delegate/facade-contract.ts +6 -0
- package/src/core/delegate/result-package.ts +32 -14
- package/src/core/delegate/runner.ts +129 -58
- package/src/core/delegate/seed.ts +5 -7
- package/src/core/durable-fs.ts +178 -30
- package/src/core/extension-api.ts +36 -8
- package/src/core/fusion/artifacts.ts +17 -16
- package/src/core/fusion/clean-context.ts +8 -5
- package/src/core/fusion/config.ts +2 -1
- package/src/core/fusion/context.ts +2 -6
- package/src/core/fusion/facade-contract.ts +2 -0
- package/src/core/fusion/orchestrator.ts +3 -3
- package/src/core/fusion/prompts.ts +1 -1
- package/src/core/fusion/result-package.ts +259 -72
- package/src/core/fusion/source-policy.ts +43 -17
- package/src/core/lazy-module.ts +215 -0
- package/src/core/pi-launch.ts +390 -65
- package/src/core/registry.ts +2010 -381
- package/src/core/reload-shell-owner.ts +1662 -0
- package/src/core/shell-policy.ts +134 -0
- package/src/core/task-durable.ts +67 -0
- package/src/delegate-extension.ts +404 -144
- package/src/extension.ts +387 -114
- package/src/fusion-extension.ts +202 -96
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { lstat, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { canonicalJson, sha256Buffer } from '../
|
|
3
|
+
import { canonicalJson, sha256Buffer } from '../canonical-json.js';
|
|
4
4
|
import { parseJsonText, type JsonObject } from '../common.js';
|
|
5
5
|
import {
|
|
6
6
|
FUSION_FAILURE_SUMMARY_ATTEMPT_CAP,
|
|
@@ -461,21 +461,41 @@ export async function readFusionCommittedResult(
|
|
|
461
461
|
// Keep the verified details below 8 KiB even after that model-visible envelope.
|
|
462
462
|
const FAILURE_VIEW_MAX_BYTES = 6 * 1024;
|
|
463
463
|
const FAILURE_CODES = new Set([
|
|
464
|
-
'config_invalid',
|
|
465
|
-
'
|
|
466
|
-
'
|
|
467
|
-
'
|
|
468
|
-
'
|
|
469
|
-
'
|
|
470
|
-
'
|
|
464
|
+
'config_invalid',
|
|
465
|
+
'config_conflict',
|
|
466
|
+
'model_unavailable',
|
|
467
|
+
'context_capture_failed',
|
|
468
|
+
'context_policy_unsupported_block',
|
|
469
|
+
'prompt_budget_exceeded_forecast',
|
|
470
|
+
'prompt_budget_exceeded_measured',
|
|
471
|
+
'model_capacity_unknown',
|
|
472
|
+
'child_spawn_failed',
|
|
473
|
+
'child_stdin_failed',
|
|
474
|
+
'child_event_invalid',
|
|
475
|
+
'child_exit_failed',
|
|
476
|
+
'child_runtime_limit_exceeded',
|
|
477
|
+
'child_runtime_payload_invalid',
|
|
478
|
+
'child_cache_policy_invalid',
|
|
479
|
+
'child_timeout',
|
|
480
|
+
'child_output_cap',
|
|
481
|
+
'child_cancelled',
|
|
482
|
+
'evaluation_invalid',
|
|
483
|
+
'artifact_error',
|
|
484
|
+
'state_transition_invalid',
|
|
485
|
+
'orchestration_failed',
|
|
471
486
|
]);
|
|
472
487
|
const FAILURE_REMEDIATION_IDS = new Set([
|
|
473
|
-
'inspect_manifest_bound_evidence',
|
|
488
|
+
'inspect_manifest_bound_evidence',
|
|
489
|
+
'inspect_terminal_error',
|
|
490
|
+
'split_or_reduce_work',
|
|
474
491
|
'retry_same_route_after_operator_review',
|
|
475
492
|
]);
|
|
476
493
|
const FAILURE_CLASSIFICATIONS = new Set([
|
|
477
|
-
'complete_stage_output',
|
|
478
|
-
'
|
|
494
|
+
'complete_stage_output',
|
|
495
|
+
'partial_stage_output',
|
|
496
|
+
'oversized_original',
|
|
497
|
+
'empty_rejected_output',
|
|
498
|
+
'evidence_only',
|
|
479
499
|
]);
|
|
480
500
|
|
|
481
501
|
interface TrustedFailureManifest {
|
|
@@ -498,7 +518,8 @@ function failureUnavailable(
|
|
|
498
518
|
summary_status: status,
|
|
499
519
|
terminal_state: state,
|
|
500
520
|
answer: { present: false, reason: 'run_did_not_commit' },
|
|
501
|
-
summary_unavailable_reason:
|
|
521
|
+
summary_unavailable_reason:
|
|
522
|
+
status === 'unavailable' ? 'manifest_untrusted' : 'summary_integrity_failed',
|
|
502
523
|
};
|
|
503
524
|
}
|
|
504
525
|
|
|
@@ -531,7 +552,11 @@ function failureRef(value: unknown, label: string, artifactDir: string): FusionA
|
|
|
531
552
|
}
|
|
532
553
|
|
|
533
554
|
function sameFailureRef(left: FusionArtifactRef, right: FusionArtifactRef): boolean {
|
|
534
|
-
return
|
|
555
|
+
return (
|
|
556
|
+
left.path === right.path &&
|
|
557
|
+
left.byte_length === right.byte_length &&
|
|
558
|
+
left.sha256 === right.sha256
|
|
559
|
+
);
|
|
535
560
|
}
|
|
536
561
|
|
|
537
562
|
function trustedFailureManifest(
|
|
@@ -540,12 +565,16 @@ function trustedFailureManifest(
|
|
|
540
565
|
): TrustedFailureManifest {
|
|
541
566
|
if (!isRecord(value)) throw new Error('manifest must be an object');
|
|
542
567
|
const schemaVersion = value['schema_version'];
|
|
543
|
-
if (
|
|
568
|
+
if (
|
|
569
|
+
schemaVersion !== FUSION_MANIFEST_SCHEMA_VERSION &&
|
|
570
|
+
schemaVersion !== FUSION_LEGACY_MANIFEST_SCHEMA_VERSION
|
|
571
|
+
)
|
|
544
572
|
throw new Error('manifest schema version mismatch');
|
|
545
573
|
if (value['run_id'] !== options.runId || value['workflow'] !== options.workflow)
|
|
546
574
|
throw new Error('manifest identity mismatch');
|
|
547
575
|
const state = value['state'];
|
|
548
|
-
if (state !== 'failed' && state !== 'cancelled')
|
|
576
|
+
if (state !== 'failed' && state !== 'cancelled')
|
|
577
|
+
throw new Error('manifest is not failed or cancelled');
|
|
549
578
|
const source = value['source'];
|
|
550
579
|
if (source !== 'command' && source !== 'tool') throw new Error('manifest source is invalid');
|
|
551
580
|
const artifactsValue = value['artifacts'];
|
|
@@ -571,14 +600,18 @@ function trustedFailureManifest(
|
|
|
571
600
|
for (const attemptValue of attemptsValue) {
|
|
572
601
|
if (!isRecord(attemptValue)) throw new Error('manifest attempt is invalid');
|
|
573
602
|
const metadata = failureAttempt({
|
|
574
|
-
stage: attemptValue['stage'],
|
|
575
|
-
|
|
603
|
+
stage: attemptValue['stage'],
|
|
604
|
+
slot: attemptValue['slot'],
|
|
605
|
+
attempt: attemptValue['attempt'],
|
|
606
|
+
status: attemptValue['status'],
|
|
607
|
+
child_created: attemptValue['child_created'],
|
|
576
608
|
});
|
|
577
609
|
attempts.push(metadata);
|
|
578
610
|
const response = attemptArtifact(attemptValue['response_path'], 'manifest response_path');
|
|
579
611
|
if (response !== undefined) {
|
|
580
612
|
const responseRef = artifacts[response];
|
|
581
|
-
if (responseRef === undefined)
|
|
613
|
+
if (responseRef === undefined)
|
|
614
|
+
throw new Error('manifest response_path is not manifest-bound');
|
|
582
615
|
classifications[response] =
|
|
583
616
|
responseRef.byte_length === 0 && metadata.status !== 'completed'
|
|
584
617
|
? 'empty_rejected_output'
|
|
@@ -596,30 +629,48 @@ function trustedFailureManifest(
|
|
|
596
629
|
recovery['original_response_path'],
|
|
597
630
|
'manifest output_recovery.original_response_path',
|
|
598
631
|
);
|
|
599
|
-
if (original === undefined)
|
|
632
|
+
if (original === undefined)
|
|
633
|
+
throw new Error('manifest output recovery has no original response');
|
|
600
634
|
classifications[original] = 'oversized_original';
|
|
601
635
|
}
|
|
602
636
|
}
|
|
603
|
-
attempts.sort(
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
637
|
+
attempts.sort(
|
|
638
|
+
(left, right) =>
|
|
639
|
+
compareFailureText(left.stage, right.stage) ||
|
|
640
|
+
(left.slot ?? 0) - (right.slot ?? 0) ||
|
|
641
|
+
left.attempt - right.attempt,
|
|
607
642
|
);
|
|
608
643
|
const manifestUsage = failureUsage(value['usage'], options.artifactDir);
|
|
609
644
|
const error = value['error'];
|
|
610
|
-
if (error !== undefined && typeof error !== 'string')
|
|
611
|
-
|
|
645
|
+
if (error !== undefined && typeof error !== 'string')
|
|
646
|
+
throw new Error('manifest error is invalid');
|
|
647
|
+
return {
|
|
648
|
+
schemaVersion,
|
|
649
|
+
source,
|
|
650
|
+
state,
|
|
651
|
+
usage: manifestUsage,
|
|
652
|
+
artifacts,
|
|
653
|
+
attempts,
|
|
654
|
+
classifications,
|
|
655
|
+
...(error === undefined ? {} : { error }),
|
|
656
|
+
};
|
|
612
657
|
}
|
|
613
658
|
|
|
614
659
|
function failureMessage(value: unknown): FusionFailureSummaryV1['failure']['message'] {
|
|
615
660
|
if (!isRecord(value)) throw new Error('failure message is invalid');
|
|
616
|
-
assertOnlyKeys(
|
|
661
|
+
assertOnlyKeys(
|
|
662
|
+
value,
|
|
663
|
+
['byte_length', 'sha256', 'inline_message', 'omission_reason'],
|
|
664
|
+
'failure message',
|
|
665
|
+
'failure-summary.json',
|
|
666
|
+
);
|
|
617
667
|
const byteLength = failureInteger(value['byte_length'], 'failure message byte_length');
|
|
618
668
|
const sha256 = failureString(value['sha256'], 'failure message sha256');
|
|
619
669
|
if (!SHA256_PATTERN.test(sha256)) throw new Error('failure message sha256 is invalid');
|
|
620
670
|
const inline = value['inline_message'];
|
|
621
671
|
const omission = value['omission_reason'];
|
|
622
|
-
if ((inline === undefined) === (omission === undefined))
|
|
672
|
+
if ((inline === undefined) === (omission === undefined))
|
|
673
|
+
throw new Error('failure message must have exactly one representation');
|
|
623
674
|
if (inline !== undefined) {
|
|
624
675
|
if (
|
|
625
676
|
typeof inline !== 'string' ||
|
|
@@ -654,7 +705,12 @@ function failureList<T>(
|
|
|
654
705
|
|
|
655
706
|
function failureAttempt(value: unknown): FusionFailureAttemptMetadata {
|
|
656
707
|
if (!isRecord(value)) throw new Error('failure attempt is invalid');
|
|
657
|
-
assertOnlyKeys(
|
|
708
|
+
assertOnlyKeys(
|
|
709
|
+
value,
|
|
710
|
+
['stage', 'slot', 'attempt', 'status', 'child_created'],
|
|
711
|
+
'failure attempt',
|
|
712
|
+
'failure-summary.json',
|
|
713
|
+
);
|
|
658
714
|
const stage = failureStage(value['stage'], 'failure attempt stage');
|
|
659
715
|
const slot = value['slot'];
|
|
660
716
|
if (slot !== undefined && slot !== 1 && slot !== 2 && slot !== 3)
|
|
@@ -662,11 +718,19 @@ function failureAttempt(value: unknown): FusionFailureAttemptMetadata {
|
|
|
662
718
|
if ((stage === 'candidate') !== (slot !== undefined))
|
|
663
719
|
throw new Error('failure attempt stage/slot is inconsistent');
|
|
664
720
|
const status = value['status'];
|
|
665
|
-
if (status !== 'completed' && status !== 'failed' && status !== 'cancelled')
|
|
666
|
-
|
|
721
|
+
if (status !== 'completed' && status !== 'failed' && status !== 'cancelled')
|
|
722
|
+
throw new Error('failure attempt status is invalid');
|
|
723
|
+
if (typeof value['child_created'] !== 'boolean')
|
|
724
|
+
throw new Error('failure attempt child_created is invalid');
|
|
667
725
|
const attempt = failureInteger(value['attempt'], 'failure attempt number');
|
|
668
726
|
if (attempt === 0) throw new Error('failure attempt number must be positive');
|
|
669
|
-
return {
|
|
727
|
+
return {
|
|
728
|
+
stage,
|
|
729
|
+
...(slot === undefined ? {} : { slot }),
|
|
730
|
+
attempt,
|
|
731
|
+
status,
|
|
732
|
+
child_created: value['child_created'],
|
|
733
|
+
};
|
|
670
734
|
}
|
|
671
735
|
|
|
672
736
|
function failureEvidence(
|
|
@@ -686,35 +750,81 @@ function failureEvidence(
|
|
|
686
750
|
if (manifestRef === undefined || !sameFailureRef(ref, manifestRef))
|
|
687
751
|
throw new Error('failure evidence ref diverges from manifest');
|
|
688
752
|
const expectedClassification = manifest.classifications[name] ?? 'evidence_only';
|
|
689
|
-
if (classification !== expectedClassification)
|
|
753
|
+
if (classification !== expectedClassification)
|
|
754
|
+
throw new Error('failure evidence classification diverges from manifest');
|
|
690
755
|
return { name, classification: expectedClassification, ref };
|
|
691
756
|
}
|
|
692
757
|
|
|
693
|
-
function failureProgress(
|
|
758
|
+
function failureProgress(
|
|
759
|
+
value: unknown,
|
|
760
|
+
manifest: TrustedFailureManifest,
|
|
761
|
+
artifactDir: string,
|
|
762
|
+
): FusionRunProgress {
|
|
694
763
|
if (!isRecord(value)) throw new Error('failure progress is invalid');
|
|
695
|
-
assertOnlyKeys(
|
|
696
|
-
|
|
697
|
-
|
|
764
|
+
assertOnlyKeys(
|
|
765
|
+
value,
|
|
766
|
+
['manifest_state', 'candidates', 'evaluation', 'merge', 'usage_so_far'],
|
|
767
|
+
'failure progress',
|
|
768
|
+
artifactDir,
|
|
769
|
+
);
|
|
770
|
+
if (value['manifest_state'] !== manifest.state)
|
|
771
|
+
throw new Error('failure progress state diverges from manifest');
|
|
772
|
+
const stage = (
|
|
773
|
+
entry: unknown,
|
|
774
|
+
label: string,
|
|
775
|
+
candidates: boolean,
|
|
776
|
+
): FusionRunProgress['candidates'] => {
|
|
698
777
|
if (!isRecord(entry)) throw new Error(`${label} is invalid`);
|
|
699
|
-
assertOnlyKeys(
|
|
778
|
+
assertOnlyKeys(
|
|
779
|
+
entry,
|
|
780
|
+
[
|
|
781
|
+
'status',
|
|
782
|
+
'attempts_recorded',
|
|
783
|
+
'children_created',
|
|
784
|
+
'children_completed',
|
|
785
|
+
'children_failed',
|
|
786
|
+
'children_cancelled',
|
|
787
|
+
'not_started_slots',
|
|
788
|
+
],
|
|
789
|
+
label,
|
|
790
|
+
artifactDir,
|
|
791
|
+
);
|
|
700
792
|
const status = entry['status'];
|
|
701
|
-
if (status !== 'not_started' && status !== 'incomplete' && status !== 'completed')
|
|
793
|
+
if (status !== 'not_started' && status !== 'incomplete' && status !== 'completed')
|
|
794
|
+
throw new Error(`${label}.status is invalid`);
|
|
702
795
|
const notStarted = entry['not_started_slots'];
|
|
703
|
-
if (
|
|
796
|
+
if (
|
|
797
|
+
candidates
|
|
798
|
+
? !Number.isSafeInteger(notStarted) || Number(notStarted) < 0 || Number(notStarted) > 3
|
|
799
|
+
: notStarted !== undefined
|
|
800
|
+
)
|
|
704
801
|
throw new Error(`${label}.not_started_slots is invalid`);
|
|
705
802
|
return {
|
|
706
803
|
status,
|
|
707
804
|
attempts_recorded: failureInteger(entry['attempts_recorded'], `${label}.attempts_recorded`),
|
|
708
805
|
children_created: failureInteger(entry['children_created'], `${label}.children_created`),
|
|
709
|
-
children_completed: failureInteger(
|
|
806
|
+
children_completed: failureInteger(
|
|
807
|
+
entry['children_completed'],
|
|
808
|
+
`${label}.children_completed`,
|
|
809
|
+
),
|
|
710
810
|
children_failed: failureInteger(entry['children_failed'], `${label}.children_failed`),
|
|
711
|
-
children_cancelled: failureInteger(
|
|
811
|
+
children_cancelled: failureInteger(
|
|
812
|
+
entry['children_cancelled'],
|
|
813
|
+
`${label}.children_cancelled`,
|
|
814
|
+
),
|
|
712
815
|
...(candidates ? { not_started_slots: Number(notStarted) } : {}),
|
|
713
816
|
};
|
|
714
817
|
};
|
|
715
818
|
const usageSoFar = failureUsage(value['usage_so_far'], artifactDir);
|
|
716
|
-
if (canonicalJson(usageSoFar) !== canonicalJson(manifest.usage))
|
|
717
|
-
|
|
819
|
+
if (canonicalJson(usageSoFar) !== canonicalJson(manifest.usage))
|
|
820
|
+
throw new Error('failure progress usage diverges from manifest');
|
|
821
|
+
return {
|
|
822
|
+
manifest_state: manifest.state,
|
|
823
|
+
candidates: stage(value['candidates'], 'failure candidates', true),
|
|
824
|
+
evaluation: stage(value['evaluation'], 'failure evaluation', false),
|
|
825
|
+
merge: stage(value['merge'], 'failure merge', false),
|
|
826
|
+
usage_so_far: usageSoFar,
|
|
827
|
+
};
|
|
718
828
|
}
|
|
719
829
|
|
|
720
830
|
function parseFailureSummary(
|
|
@@ -723,22 +833,57 @@ function parseFailureSummary(
|
|
|
723
833
|
options: ReadFusionCommittedResultOptions,
|
|
724
834
|
): FusionFailureSummaryV1 {
|
|
725
835
|
if (!isRecord(value)) throw new Error('failure summary must be an object');
|
|
726
|
-
assertOnlyKeys(
|
|
727
|
-
|
|
836
|
+
assertOnlyKeys(
|
|
837
|
+
value,
|
|
838
|
+
[
|
|
839
|
+
'schema_version',
|
|
840
|
+
'run_id',
|
|
841
|
+
'workflow',
|
|
842
|
+
'source',
|
|
843
|
+
'terminal_state',
|
|
844
|
+
'created_at',
|
|
845
|
+
'answer',
|
|
846
|
+
'failure',
|
|
847
|
+
'progress',
|
|
848
|
+
'usage_so_far',
|
|
849
|
+
'attempts',
|
|
850
|
+
'evidence_artifacts',
|
|
851
|
+
'remediation_ids',
|
|
852
|
+
],
|
|
853
|
+
'failure summary',
|
|
854
|
+
options.artifactDir,
|
|
855
|
+
);
|
|
856
|
+
if (
|
|
857
|
+
value['schema_version'] !== FUSION_FAILURE_SUMMARY_SCHEMA_VERSION ||
|
|
858
|
+
value['run_id'] !== options.runId ||
|
|
859
|
+
value['workflow'] !== options.workflow ||
|
|
860
|
+
value['source'] !== manifest.source ||
|
|
861
|
+
value['terminal_state'] !== manifest.state ||
|
|
862
|
+
typeof value['created_at'] !== 'string'
|
|
863
|
+
)
|
|
728
864
|
throw new Error('failure summary identity is invalid');
|
|
729
865
|
const answer = value['answer'];
|
|
730
|
-
if (!isRecord(answer) || answer['present'] !== false || answer['reason'] !== 'run_did_not_commit')
|
|
866
|
+
if (!isRecord(answer) || answer['present'] !== false || answer['reason'] !== 'run_did_not_commit')
|
|
867
|
+
throw new Error('failure summary answer assertion is invalid');
|
|
731
868
|
const failure = value['failure'];
|
|
732
869
|
if (!isRecord(failure)) throw new Error('failure summary failure metadata is invalid');
|
|
733
|
-
assertOnlyKeys(
|
|
870
|
+
assertOnlyKeys(
|
|
871
|
+
failure,
|
|
872
|
+
['code', 'stage', 'slot', 'attempt', 'child_created', 'message'],
|
|
873
|
+
'failure metadata',
|
|
874
|
+
options.artifactDir,
|
|
875
|
+
);
|
|
734
876
|
const code = failure['code'];
|
|
735
|
-
if (code !== null && (typeof code !== 'string' || !FAILURE_CODES.has(code)))
|
|
877
|
+
if (code !== null && (typeof code !== 'string' || !FAILURE_CODES.has(code)))
|
|
878
|
+
throw new Error('failure code is invalid');
|
|
736
879
|
const stageValue = failure['stage'];
|
|
737
880
|
const stage = stageValue === undefined ? undefined : failureStage(stageValue, 'failure stage');
|
|
738
881
|
const slot = failure['slot'];
|
|
739
|
-
if (slot !== undefined && slot !== 1 && slot !== 2 && slot !== 3)
|
|
882
|
+
if (slot !== undefined && slot !== 1 && slot !== 2 && slot !== 3)
|
|
883
|
+
throw new Error('failure slot is invalid');
|
|
740
884
|
if (failure['attempt'] !== undefined) failureInteger(failure['attempt'], 'failure attempt');
|
|
741
|
-
if (typeof failure['child_created'] !== 'boolean')
|
|
885
|
+
if (typeof failure['child_created'] !== 'boolean')
|
|
886
|
+
throw new Error('failure child_created is invalid');
|
|
742
887
|
const progress = failureProgress(value['progress'], manifest, options.artifactDir);
|
|
743
888
|
const expectedProgress = buildFusionRunProgress(manifest);
|
|
744
889
|
if (canonicalJson(progress) !== canonicalJson(expectedProgress))
|
|
@@ -775,7 +920,11 @@ function parseFailureSummary(
|
|
|
775
920
|
);
|
|
776
921
|
const expectedEvidence = Object.entries(manifest.artifacts)
|
|
777
922
|
.filter(([name]) => name !== 'failure-summary.json')
|
|
778
|
-
.map(([name, ref]) => ({
|
|
923
|
+
.map(([name, ref]) => ({
|
|
924
|
+
name,
|
|
925
|
+
classification: manifest.classifications[name] ?? 'evidence_only',
|
|
926
|
+
ref,
|
|
927
|
+
}))
|
|
779
928
|
.sort((left, right) => compareFailureText(left.name, right.name));
|
|
780
929
|
const expectedEvidenceListedCount = Math.min(
|
|
781
930
|
expectedEvidence.length,
|
|
@@ -818,11 +967,25 @@ function parseFailureSummary(
|
|
|
818
967
|
if (canonicalJson(terminalMessage) !== canonicalJson(expectedMessage))
|
|
819
968
|
throw new Error('failure message diverges from manifest terminal error');
|
|
820
969
|
return {
|
|
821
|
-
schema_version: FUSION_FAILURE_SUMMARY_SCHEMA_VERSION,
|
|
822
|
-
|
|
970
|
+
schema_version: FUSION_FAILURE_SUMMARY_SCHEMA_VERSION,
|
|
971
|
+
run_id: options.runId,
|
|
972
|
+
workflow: options.workflow,
|
|
973
|
+
source: manifest.source,
|
|
974
|
+
terminal_state: manifest.state,
|
|
975
|
+
created_at: value['created_at'],
|
|
823
976
|
answer: { present: false, reason: 'run_did_not_commit' },
|
|
824
|
-
failure: {
|
|
825
|
-
|
|
977
|
+
failure: {
|
|
978
|
+
code: code as FusionFailureSummaryV1['failure']['code'],
|
|
979
|
+
...(stage === undefined ? {} : { stage }),
|
|
980
|
+
...(slot === undefined ? {} : { slot }),
|
|
981
|
+
...(failure['attempt'] === undefined ? {} : { attempt: Number(failure['attempt']) }),
|
|
982
|
+
child_created: failure['child_created'],
|
|
983
|
+
message: terminalMessage,
|
|
984
|
+
},
|
|
985
|
+
progress,
|
|
986
|
+
usage_so_far: usageSoFar,
|
|
987
|
+
attempts,
|
|
988
|
+
evidence_artifacts: evidence,
|
|
826
989
|
remediation_ids: [...remediationIds] as FusionFailureSummaryV1['remediation_ids'],
|
|
827
990
|
};
|
|
828
991
|
}
|
|
@@ -842,17 +1005,21 @@ function boundedFailureView(
|
|
|
842
1005
|
status: FusionFailureResultView['summary_status'],
|
|
843
1006
|
summaryRef?: FusionArtifactRef,
|
|
844
1007
|
): FusionFailureResultView {
|
|
845
|
-
const attempts = {
|
|
846
|
-
|
|
1008
|
+
const attempts = {
|
|
1009
|
+
listed: [...source.attempts.listed],
|
|
1010
|
+
omitted_count: source.attempts.omitted_count,
|
|
1011
|
+
};
|
|
1012
|
+
const evidence = {
|
|
1013
|
+
listed: [...source.evidence_artifacts.listed],
|
|
1014
|
+
omitted_count: source.evidence_artifacts.omitted_count,
|
|
1015
|
+
};
|
|
847
1016
|
const failure = source.failure;
|
|
848
1017
|
const view: FusionFailureResultView = {
|
|
849
1018
|
schema_version: FUSION_FAILURE_SUMMARY_SCHEMA_VERSION,
|
|
850
1019
|
summary_status: status,
|
|
851
1020
|
terminal_state: source.terminal_state,
|
|
852
1021
|
answer: { present: false, reason: 'run_did_not_commit' },
|
|
853
|
-
...(failure === undefined
|
|
854
|
-
? {}
|
|
855
|
-
: { failure: { ...failure, message: { ...failure.message } } }),
|
|
1022
|
+
...(failure === undefined ? {} : { failure: { ...failure, message: { ...failure.message } } }),
|
|
856
1023
|
progress: source.progress,
|
|
857
1024
|
usage_so_far: source.usage_so_far,
|
|
858
1025
|
attempts,
|
|
@@ -860,7 +1027,8 @@ function boundedFailureView(
|
|
|
860
1027
|
remediation_ids: source.remediation_ids,
|
|
861
1028
|
...(summaryRef === undefined ? {} : { failure_summary_ref: summaryRef }),
|
|
862
1029
|
};
|
|
863
|
-
const fits = (): boolean =>
|
|
1030
|
+
const fits = (): boolean =>
|
|
1031
|
+
Buffer.byteLength(canonicalJson(view), 'utf8') <= FAILURE_VIEW_MAX_BYTES;
|
|
864
1032
|
if (!fits() && view.failure?.message.inline_message !== undefined) {
|
|
865
1033
|
const message = view.failure.message;
|
|
866
1034
|
view.failure = {
|
|
@@ -872,15 +1040,22 @@ function boundedFailureView(
|
|
|
872
1040
|
},
|
|
873
1041
|
};
|
|
874
1042
|
}
|
|
875
|
-
while (!fits() && evidence.listed.length > 0) {
|
|
876
|
-
|
|
877
|
-
|
|
1043
|
+
while (!fits() && evidence.listed.length > 0) {
|
|
1044
|
+
evidence.listed.pop();
|
|
1045
|
+
evidence.omitted_count += 1;
|
|
1046
|
+
}
|
|
1047
|
+
while (!fits() && attempts.listed.length > 0) {
|
|
1048
|
+
attempts.listed.pop();
|
|
1049
|
+
attempts.omitted_count += 1;
|
|
1050
|
+
}
|
|
1051
|
+
if (!fits())
|
|
1052
|
+
throw new Error(
|
|
1053
|
+
'failure result view exceeds its byte budget without a safe whole-section omission',
|
|
1054
|
+
);
|
|
878
1055
|
return view;
|
|
879
1056
|
}
|
|
880
1057
|
|
|
881
|
-
function legacyFailureSource(
|
|
882
|
-
manifest: TrustedFailureManifest,
|
|
883
|
-
): FailureViewSource {
|
|
1058
|
+
function legacyFailureSource(manifest: TrustedFailureManifest): FailureViewSource {
|
|
884
1059
|
const message =
|
|
885
1060
|
manifest.error === undefined
|
|
886
1061
|
? undefined
|
|
@@ -913,11 +1088,13 @@ function legacyFailureSource(
|
|
|
913
1088
|
usage_so_far: manifest.usage,
|
|
914
1089
|
attempts: {
|
|
915
1090
|
listed: attempts.filter((_attempt, index) => index < FUSION_FAILURE_SUMMARY_ATTEMPT_CAP),
|
|
916
|
-
omitted_count:
|
|
1091
|
+
omitted_count:
|
|
1092
|
+
attempts.length - Math.min(attempts.length, FUSION_FAILURE_SUMMARY_ATTEMPT_CAP),
|
|
917
1093
|
},
|
|
918
1094
|
evidence_artifacts: {
|
|
919
1095
|
listed: evidence.filter((_evidence, index) => index < FUSION_FAILURE_SUMMARY_EVIDENCE_CAP),
|
|
920
|
-
omitted_count:
|
|
1096
|
+
omitted_count:
|
|
1097
|
+
evidence.length - Math.min(evidence.length, FUSION_FAILURE_SUMMARY_EVIDENCE_CAP),
|
|
921
1098
|
},
|
|
922
1099
|
remediation_ids: ['inspect_manifest_bound_evidence', 'inspect_terminal_error'],
|
|
923
1100
|
};
|
|
@@ -929,7 +1106,11 @@ export async function readFusionFailureResult(
|
|
|
929
1106
|
): Promise<FusionFailureResultView> {
|
|
930
1107
|
let manifest: TrustedFailureManifest;
|
|
931
1108
|
try {
|
|
932
|
-
const file = await readUtf8(
|
|
1109
|
+
const file = await readUtf8(
|
|
1110
|
+
join(options.artifactDirAbs, 'manifest.json'),
|
|
1111
|
+
'manifest.json',
|
|
1112
|
+
options.artifactDir,
|
|
1113
|
+
);
|
|
933
1114
|
manifest = trustedFailureManifest(parseJsonText(file.text), options);
|
|
934
1115
|
} catch {
|
|
935
1116
|
return failureUnavailable('failed', 'unavailable');
|
|
@@ -938,7 +1119,10 @@ export async function readFusionFailureResult(
|
|
|
938
1119
|
if (summaryRef === undefined) {
|
|
939
1120
|
return boundedFailureView(legacyFailureSource(manifest), 'legacy_manifest_only');
|
|
940
1121
|
}
|
|
941
|
-
if (
|
|
1122
|
+
if (
|
|
1123
|
+
manifest.schemaVersion !== FUSION_MANIFEST_SCHEMA_VERSION ||
|
|
1124
|
+
summaryRef.path !== 'failure-summary.json'
|
|
1125
|
+
)
|
|
942
1126
|
return failureUnavailable(manifest.state, 'integrity_failed');
|
|
943
1127
|
try {
|
|
944
1128
|
if (summaryRef.byte_length > FUSION_FAILURE_SUMMARY_MAX_BYTES)
|
|
@@ -949,7 +1133,10 @@ export async function readFusionFailureResult(
|
|
|
949
1133
|
options.artifactDir,
|
|
950
1134
|
FUSION_FAILURE_SUMMARY_MAX_BYTES,
|
|
951
1135
|
);
|
|
952
|
-
if (
|
|
1136
|
+
if (
|
|
1137
|
+
file.bytes.length !== summaryRef.byte_length ||
|
|
1138
|
+
sha256Buffer(file.bytes) !== summaryRef.sha256
|
|
1139
|
+
)
|
|
953
1140
|
throw new Error('failure summary hash/length mismatch');
|
|
954
1141
|
const summary = parseFailureSummary(parseJsonText(file.text), manifest, options);
|
|
955
1142
|
return boundedFailureView(summary, 'verified', summaryRef);
|
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
3
|
import { open } from 'node:fs/promises';
|
|
4
4
|
import { isIP } from 'node:net';
|
|
5
|
-
import { canonicalJson } from '../
|
|
5
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
6
6
|
import { isJsonObject, parseJsonText } from '../common.js';
|
|
7
7
|
import {
|
|
8
8
|
FUSION_SOURCE_POLICY_SCHEMA_VERSION,
|
|
@@ -111,13 +111,19 @@ export function canonicalizeFusionPublicUrl(value: string): string {
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
const ipKind = isIP(normalizedHost);
|
|
114
|
-
if (
|
|
114
|
+
if (
|
|
115
|
+
(ipKind === 4 && isBlockedIpv4(normalizedHost)) ||
|
|
116
|
+
(ipKind === 6 && isBlockedIpv6(normalizedHost))
|
|
117
|
+
) {
|
|
115
118
|
throw new FusionError('fusion research source URL must be public, not private/reserved', {
|
|
116
119
|
code: 'orchestration_failed',
|
|
117
120
|
childCreated: false,
|
|
118
121
|
});
|
|
119
122
|
}
|
|
120
|
-
if (
|
|
123
|
+
if (
|
|
124
|
+
(url.protocol === 'http:' && url.port === '80') ||
|
|
125
|
+
(url.protocol === 'https:' && url.port === '443')
|
|
126
|
+
) {
|
|
121
127
|
url.port = '';
|
|
122
128
|
}
|
|
123
129
|
return url.toString();
|
|
@@ -183,21 +189,28 @@ export function sourcePolicyCanonicalBytes(policy: FusionSourcePolicyV1): string
|
|
|
183
189
|
|
|
184
190
|
function requireString(record: Record<PropertyKey, unknown>, key: string, label: string): string {
|
|
185
191
|
const value = record[key];
|
|
186
|
-
if (typeof value !== 'string' || value.length === 0)
|
|
192
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
193
|
+
throw new Error(`${label}.${key} must be non-blank string`);
|
|
187
194
|
return value;
|
|
188
195
|
}
|
|
189
196
|
|
|
190
197
|
export function parseFusionSourcePolicy(value: unknown): FusionSourcePolicyV1 {
|
|
191
|
-
if (!isJsonObject(value) || Array.isArray(value))
|
|
198
|
+
if (!isJsonObject(value) || Array.isArray(value))
|
|
199
|
+
throw new Error('fusion source policy must be object');
|
|
192
200
|
const keys = Object.keys(value).sort();
|
|
193
201
|
const expected = ['cwd', 'root_sha256', 'schema_version', 'sources', 'workflow'];
|
|
194
|
-
if (keys.join('\0') !== expected.join('\0'))
|
|
195
|
-
|
|
196
|
-
if (value['
|
|
202
|
+
if (keys.join('\0') !== expected.join('\0'))
|
|
203
|
+
throw new Error('fusion source policy keys mismatch');
|
|
204
|
+
if (value['schema_version'] !== FUSION_SOURCE_POLICY_SCHEMA_VERSION)
|
|
205
|
+
throw new Error('fusion source policy schema_version mismatch');
|
|
206
|
+
if (value['workflow'] !== 'research')
|
|
207
|
+
throw new Error('fusion source policy workflow must be research');
|
|
197
208
|
const cwd = requireString(value, 'cwd', 'fusion source policy');
|
|
198
209
|
const rootSha256 = requireString(value, 'root_sha256', 'fusion source policy');
|
|
199
|
-
if (!SHA256_HEX.test(rootSha256))
|
|
200
|
-
|
|
210
|
+
if (!SHA256_HEX.test(rootSha256))
|
|
211
|
+
throw new Error('fusion source policy.root_sha256 must be sha256');
|
|
212
|
+
if (!Array.isArray(value['sources']))
|
|
213
|
+
throw new Error('fusion source policy.sources must be array');
|
|
201
214
|
const sources = value['sources'].map((item, index): FusionDeclaredSourceV1 => {
|
|
202
215
|
const label = `fusion source policy.sources[${String(index)}]`;
|
|
203
216
|
if (!isJsonObject(item) || Array.isArray(item)) throw new Error(`${label} must be object`);
|
|
@@ -209,10 +222,12 @@ export function parseFusionSourcePolicy(value: unknown): FusionSourcePolicyV1 {
|
|
|
209
222
|
const canonical_url = requireString(item, 'canonical_url', label);
|
|
210
223
|
const sha256 = requireString(item, 'sha256', label);
|
|
211
224
|
if (!SHA256_HEX.test(sha256)) throw new Error(`${label}.sha256 must be sha256`);
|
|
212
|
-
if (canonicalizeFusionPublicUrl(url) !== canonical_url)
|
|
225
|
+
if (canonicalizeFusionPublicUrl(url) !== canonical_url)
|
|
226
|
+
throw new Error(`${label}.canonical_url mismatch`);
|
|
213
227
|
if (url !== canonical_url) throw new Error(`${label}.url must equal canonical_url`);
|
|
214
228
|
if (purpose.trim() !== purpose) throw new Error(`${label}.purpose must be trimmed`);
|
|
215
|
-
if (sha256Text(`${canonical_url}\u0000${purpose}`) !== sha256)
|
|
229
|
+
if (sha256Text(`${canonical_url}\u0000${purpose}`) !== sha256)
|
|
230
|
+
throw new Error(`${label}.sha256 mismatch`);
|
|
216
231
|
return { url, canonical_url, purpose, sha256 };
|
|
217
232
|
});
|
|
218
233
|
const seen = new Set<string>();
|
|
@@ -222,8 +237,14 @@ export function parseFusionSourcePolicy(value: unknown): FusionSourcePolicyV1 {
|
|
|
222
237
|
}
|
|
223
238
|
seen.add(source.canonical_url);
|
|
224
239
|
}
|
|
225
|
-
const body = {
|
|
226
|
-
|
|
240
|
+
const body = {
|
|
241
|
+
schema_version: FUSION_SOURCE_POLICY_SCHEMA_VERSION,
|
|
242
|
+
workflow: 'research' as const,
|
|
243
|
+
cwd,
|
|
244
|
+
sources,
|
|
245
|
+
} as const;
|
|
246
|
+
if (sha256Text(canonicalJson(body)) !== rootSha256)
|
|
247
|
+
throw new Error('fusion source policy root_sha256 mismatch');
|
|
227
248
|
return { ...body, root_sha256: rootSha256 };
|
|
228
249
|
}
|
|
229
250
|
|
|
@@ -246,12 +267,17 @@ async function readRegularFileNoSymlink(path: string, label: string): Promise<Bu
|
|
|
246
267
|
}
|
|
247
268
|
}
|
|
248
269
|
|
|
249
|
-
export async function readFusionSourcePolicyFile(
|
|
250
|
-
|
|
270
|
+
export async function readFusionSourcePolicyFile(
|
|
271
|
+
path: string,
|
|
272
|
+
expectedSha256: string,
|
|
273
|
+
): Promise<FusionSourcePolicyV1> {
|
|
274
|
+
if (!SHA256_HEX.test(expectedSha256))
|
|
275
|
+
throw new Error('fusion source policy expected hash is malformed');
|
|
251
276
|
const bytes = await readRegularFileNoSymlink(path, 'fusion source policy');
|
|
252
277
|
const actual = createHash('sha256').update(bytes).digest('hex');
|
|
253
278
|
if (actual !== expectedSha256) throw new Error('fusion source policy artifact hash mismatch');
|
|
254
279
|
const text = bytes.toString('utf8');
|
|
255
|
-
if (!Buffer.from(text, 'utf8').equals(bytes))
|
|
280
|
+
if (!Buffer.from(text, 'utf8').equals(bytes))
|
|
281
|
+
throw new Error('fusion source policy is not UTF-8');
|
|
256
282
|
return parseFusionSourcePolicy(parseJsonText(text));
|
|
257
283
|
}
|