@cassiomc1/forgeloop 1.13.0 → 2.0.0-rc.2
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/AGENT_COMPATIBILITY.md +8 -0
- package/CONTRIBUTING.md +11 -7
- package/DOCS_INDEX.md +37 -4
- package/ENG/c-development-eng.md +2 -2
- package/ENG/cpp-development-eng.md +2 -2
- package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
- package/ENG/flutter-development-eng.md +4 -2
- package/ENG/go-development-eng.md +2 -2
- package/ENG/java-development-eng.md +3 -3
- package/ENG/nodejs-backend-development-eng.md +10 -6
- package/ENG/php-development-eng.md +3 -3
- package/ENG/python-development-eng.md +205 -0
- package/ENG/rust-development-eng.md +42 -1
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +4 -4
- package/ENG/swift-development-eng.md +5 -4
- package/ENG/test-code-eng.md +1 -1
- package/ENG/typescript-development-eng.md +4 -4
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +6 -0
- package/LICENSE +133 -21
- package/LOOP_ENGINEERING.md +28 -2
- package/LOOP_SYSTEM_DESIGN.md +10 -10
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/README.md +63 -59
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +29 -3
- package/completions/forgeloop.bash +55 -3
- package/completions/forgeloop.fish +183 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +74 -39
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +171 -35
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +512 -8
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -11
- package/docs/GETTING_STARTED.md +3 -3
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +46 -18
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +9 -0
- package/docs/RELEASE_CHECKLIST.md +116 -12
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SELF_HOSTED_TESTS.md +121 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +198 -1
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +774 -16
- package/docs/protocol-requirements.json +24 -0
- package/package.json +43 -11
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/task-bundle.schema.json +7 -2
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +6 -6
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +6 -3
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/scripts/run-mcp-clean-tests.mjs +30 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +316 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +289 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/temporary-paths.js +18 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +218 -174
- package/src/commands/action-show.js +5 -2
- package/src/commands/advance.js +5 -1
- package/src/commands/baseline.js +13 -11
- package/src/commands/checkpoint-revalidate.js +192 -0
- package/src/commands/complete.js +19 -3
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +266 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +51 -13
- package/src/commands/gate-record.js +194 -0
- package/src/commands/gate-revalidate.js +135 -0
- package/src/commands/handoff-list.js +6 -1
- package/src/commands/handoff-show.js +6 -1
- package/src/commands/migrate-protocol.js +2 -2
- package/src/commands/model-route.js +40 -0
- package/src/commands/next.js +7 -1
- package/src/commands/policy-diff.js +25 -4
- package/src/commands/progress.js +17 -11
- package/src/commands/record-diagnosis.js +3 -0
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +41 -0
- package/src/commands/status.js +6 -1
- package/src/commands/storage-backup.js +15 -0
- package/src/commands/storage-migrate.js +5 -0
- package/src/commands/storage-migration-resume.js +5 -0
- package/src/commands/storage-migration-status.js +44 -0
- package/src/commands/storage-restore-resume.js +7 -0
- package/src/commands/storage-restore.js +8 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-list.js +8 -43
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-migrate.js +3 -3
- package/src/commands/task-recover.js +10 -2
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/task-scope.js +18 -2
- package/src/commands/task-show.js +16 -2
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +39 -15
- package/src/commands/validate-receipt.js +8 -1
- package/src/config/guides.json +4 -0
- package/src/core/action-ledger-projection.js +10 -3
- package/src/core/actions.js +38 -75
- package/src/core/activation.js +11 -3
- package/src/core/approvals.js +17 -51
- package/src/core/artifact-registry.js +83 -2
- package/src/core/artifacts.js +81 -27
- package/src/core/attachment-paths.js +45 -0
- package/src/core/attestation-coverage.js +6 -1
- package/src/core/attestation-verifier.js +21 -2
- package/src/core/attestation.js +17 -6
- package/src/core/audit-ux.js +514 -0
- package/src/core/audit.js +6 -1
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/bundles.js +160 -66
- package/src/core/checkpoint-revalidation.js +321 -0
- package/src/core/cli-command-definitions.js +350 -7
- package/src/core/code-manifest.js +9 -3
- package/src/core/command-executors.js +274 -108
- package/src/core/command-input.js +136 -44
- package/src/core/completion-artifacts.js +85 -34
- package/src/core/completion-ownership.js +19 -8
- package/src/core/completion-recovery-rebind.js +11 -1
- package/src/core/completion.js +64 -15
- package/src/core/config.js +3 -0
- package/src/core/conformance.js +2 -1
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/continuity-lint.js +13 -2
- package/src/core/continuity-reconciliation.js +16 -3
- package/src/core/continuity.js +16 -8
- package/src/core/contract-bootstrap-recovery.js +656 -0
- package/src/core/contract-revision.js +209 -0
- package/src/core/decision/artifact.js +70 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +104 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +138 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +181 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +35 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/diagnosis-model.js +11 -0
- package/src/core/diagnosis.js +10 -1
- package/src/core/diagnostic-projection.js +29 -12
- package/src/core/diagnostic-record.js +28 -3
- package/src/core/efficiency.js +6 -1
- package/src/core/error-codes.js +278 -0
- package/src/core/events.js +535 -147
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +36 -13
- package/src/core/execution-profile.js +63 -38
- package/src/core/execution.js +26 -40
- package/src/core/filesystem.js +11 -0
- package/src/core/gate-artifact.js +105 -19
- package/src/core/gate-provenance.js +125 -0
- package/src/core/handoff.js +26 -14
- package/src/core/history.js +39 -35
- package/src/core/hypothesis-projection.js +32 -0
- package/src/core/information-gain-projection.js +104 -100
- package/src/core/inspect.js +12 -3
- package/src/core/integration-invocation-policy.js +37 -4
- package/src/core/integration-resources.js +119 -87
- package/src/core/ledger-event-collection.js +189 -0
- package/src/core/ledger-relations.js +23 -0
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/native-storage.js +12 -0
- package/src/core/next-action-continuity.js +31 -9
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +131 -43
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +9 -4
- package/src/core/next-action-verification-phase.js +2 -1
- package/src/core/next-action.js +56 -9
- package/src/core/phase.js +266 -103
- package/src/core/policy-engine.js +12 -13
- package/src/core/preflight-consistency.js +89 -62
- package/src/core/preflight-loaders.js +48 -9
- package/src/core/preflight-model.js +6 -1
- package/src/core/preflight.js +29 -12
- package/src/core/progress.js +21 -10
- package/src/core/protocol-info.js +68 -0
- package/src/core/protocol-migration.js +7 -6
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +281 -67
- package/src/core/recovery-history.js +67 -47
- package/src/core/reflection.js +72 -77
- package/src/core/responsibility.js +25 -13
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +28 -4
- package/src/core/router.js +67 -1
- package/src/core/runtime-context.js +123 -61
- package/src/core/schema-validation.js +11 -3
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/settlement.js +12 -1
- package/src/core/signing/bundle-file.js +53 -0
- package/src/core/signing/bundle-reference.js +15 -0
- package/src/core/signing/sigstore.js +60 -33
- package/src/core/signing/statement-file.js +27 -0
- package/src/core/sources.js +37 -0
- package/src/core/storage-runtime-registry.js +41 -0
- package/src/core/structural-quality/artifacts.js +20 -14
- package/src/core/structural-quality/service.js +81 -56
- package/src/core/target-layout.js +1 -1
- package/src/core/task-claim-state.js +249 -15
- package/src/core/task-command.js +21 -1
- package/src/core/task-conflict-inspection.js +133 -102
- package/src/core/task-descriptor.js +29 -22
- package/src/core/task-discovery.js +288 -90
- package/src/core/task-lock.js +66 -306
- package/src/core/task-migration-validation.js +5 -5
- package/src/core/task-migration.js +17 -287
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/task-snapshot.js +51 -8
- package/src/core/task-storage-migration.js +18 -0
- package/src/core/templates.js +23 -8
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +94 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +164 -105
- package/src/core/trajectory-evaluation.js +15 -3
- package/src/core/trajectory-metrics.js +47 -32
- package/src/core/transaction.js +16 -287
- package/src/core/verification-scope.js +20 -12
- package/src/core/work-state.js +100 -59
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
- package/src/repository-index/binary-manager.js +7 -5
- package/src/storage/artifact-bytes.js +22 -0
- package/src/storage/attachment-binding.js +11 -0
- package/src/storage/attachment-files.js +82 -0
- package/src/storage/attachment-references.js +36 -0
- package/src/storage/backup.js +53 -0
- package/src/storage/capabilities.js +31 -0
- package/src/storage/connection-owner.js +119 -0
- package/src/storage/connection.js +376 -0
- package/src/storage/existing-project-scope.js +45 -0
- package/src/storage/export-destination.js +44 -0
- package/src/storage/exporter.js +284 -0
- package/src/storage/file-durability.js +46 -0
- package/src/storage/importer.js +461 -0
- package/src/storage/index.js +74 -0
- package/src/storage/leases.js +63 -0
- package/src/storage/ledger-event-snapshot.js +150 -0
- package/src/storage/ledger-relations.js +220 -0
- package/src/storage/legacy-maintenance-boundary.js +18 -0
- package/src/storage/legacy-signature.js +115 -0
- package/src/storage/maintenance-handoff.js +129 -0
- package/src/storage/maintenance-owner.js +42 -0
- package/src/storage/maintenance.js +310 -0
- package/src/storage/metadata-json.js +35 -0
- package/src/storage/migration-activate.js +59 -0
- package/src/storage/migration-archive.js +57 -0
- package/src/storage/migration-attachment-root.js +61 -0
- package/src/storage/migration-candidate.js +398 -0
- package/src/storage/migration-publication.js +143 -0
- package/src/storage/migration-rollback-admission.js +54 -0
- package/src/storage/migration-rollback-preparation.js +132 -0
- package/src/storage/migration-rollback-publication.js +183 -0
- package/src/storage/migration-rollback-release.js +66 -0
- package/src/storage/migration-rollback-source-stage.js +103 -0
- package/src/storage/migration-rollback.js +43 -0
- package/src/storage/migration-source-partition.js +56 -0
- package/src/storage/migration-source.js +255 -0
- package/src/storage/migration-terminal.js +39 -0
- package/src/storage/migration-validation.js +75 -0
- package/src/storage/migration.js +43 -0
- package/src/storage/operational-attachments.js +49 -0
- package/src/storage/operational-context.js +50 -0
- package/src/storage/project-backup.js +98 -0
- package/src/storage/project-bootstrap.js +24 -0
- package/src/storage/project-boundary.js +79 -0
- package/src/storage/project-read-snapshot.js +18 -0
- package/src/storage/project-replacement.js +137 -0
- package/src/storage/project-restore.js +203 -0
- package/src/storage/record-definitions.js +45 -0
- package/src/storage/repository.js +507 -0
- package/src/storage/restore-journal.js +62 -0
- package/src/storage/restore-layout.js +30 -0
- package/src/storage/restore-preparation.js +84 -0
- package/src/storage/restore-replacement-archive.js +162 -0
- package/src/storage/restore-replacement-owner.js +31 -0
- package/src/storage/restore-replacement-preparation.js +128 -0
- package/src/storage/restore-replacement.js +142 -0
- package/src/storage/restore-terminal.js +32 -0
- package/src/storage/runtime.js +31 -0
- package/src/storage/schema.js +214 -0
- package/src/storage/singleton-import.js +84 -0
- package/src/storage/snapshot-continuity.js +16 -0
- package/src/storage/snapshot.js +36 -0
- package/src/storage/storage-marker.js +79 -0
- package/src/storage/task-guards.js +107 -0
- package/src/storage/topology.js +42 -0
- package/src/storage/transaction.js +179 -0
- package/src/storage/unit-of-work.js +663 -0
- package/src/storage/windows-process-incarnation.js +206 -0
- package/.forgeloop/forgeloop.gitignore +0 -4
- package/LICENSE-DOCS.md +0 -13
- package/src/core/transaction-maintenance.js +0 -70
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const PROVIDER_KINDS = Object.freeze([
|
|
2
|
+
"ADVISORY_CONTEXT",
|
|
3
|
+
"VERIFICATION_EXECUTION",
|
|
4
|
+
"BROWSER_VERIFICATION",
|
|
5
|
+
"SECURITY_REVIEW",
|
|
6
|
+
"PRESENTATION",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const PROVIDER_CAPABILITIES = Object.freeze({
|
|
10
|
+
ADVISORY_CONTEXT: Object.freeze({
|
|
11
|
+
lifecycleAuthority: false,
|
|
12
|
+
completionAuthority: false,
|
|
13
|
+
executableAuthority: false,
|
|
14
|
+
evidenceAuthority: false,
|
|
15
|
+
}),
|
|
16
|
+
VERIFICATION_EXECUTION: Object.freeze({
|
|
17
|
+
lifecycleAuthority: false,
|
|
18
|
+
completionAuthority: false,
|
|
19
|
+
canProduceExecutionObservation: true,
|
|
20
|
+
evidenceAuthority: false,
|
|
21
|
+
}),
|
|
22
|
+
BROWSER_VERIFICATION: Object.freeze({
|
|
23
|
+
lifecycleAuthority: false,
|
|
24
|
+
completionAuthority: false,
|
|
25
|
+
evidenceRequiresForgeLoopValidation: true,
|
|
26
|
+
evidenceAuthority: false,
|
|
27
|
+
}),
|
|
28
|
+
SECURITY_REVIEW: Object.freeze({
|
|
29
|
+
lifecycleAuthority: false,
|
|
30
|
+
completionAuthority: false,
|
|
31
|
+
evidenceRequiresForgeLoopValidation: true,
|
|
32
|
+
evidenceAuthority: false,
|
|
33
|
+
}),
|
|
34
|
+
PRESENTATION: Object.freeze({
|
|
35
|
+
lifecycleAuthority: false,
|
|
36
|
+
completionAuthority: false,
|
|
37
|
+
executableAuthority: false,
|
|
38
|
+
evidenceAuthority: false,
|
|
39
|
+
mayMutateProtocolState: false,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export function capabilityFor(kind) {
|
|
44
|
+
if (!PROVIDER_KINDS.includes(kind)) return null;
|
|
45
|
+
return PROVIDER_CAPABILITIES[kind] ?? null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const E_PROVIDER_INVALID = "E_PROVIDER_INVALID";
|
|
2
|
+
export const E_PROVIDER_UNAVAILABLE = "E_PROVIDER_UNAVAILABLE";
|
|
3
|
+
export const E_PROVIDER_TIMEOUT = "E_PROVIDER_TIMEOUT";
|
|
4
|
+
export const E_PROVIDER_OUTPUT_INVALID = "E_PROVIDER_OUTPUT_INVALID";
|
|
5
|
+
export const E_PROVIDER_OUTPUT_LIMIT = "E_PROVIDER_OUTPUT_LIMIT";
|
|
6
|
+
export const E_PROVIDER_CAPABILITY_UNSUPPORTED = "E_PROVIDER_CAPABILITY_UNSUPPORTED";
|
|
7
|
+
export const E_PROVIDER_AUTHORITY_ESCALATION = "E_PROVIDER_AUTHORITY_ESCALATION";
|
|
8
|
+
export const E_PROVIDER_EXECUTION_FAILED = "E_PROVIDER_EXECUTION_FAILED";
|
|
9
|
+
|
|
10
|
+
export function providerError(code, message) {
|
|
11
|
+
const error = new Error(message);
|
|
12
|
+
error.name = "ProviderError";
|
|
13
|
+
error.code = code;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PROVIDER_KINDS, PROVIDER_CAPABILITIES, capabilityFor } from "./capabilities.js";
|
|
2
|
+
import {
|
|
3
|
+
E_PROVIDER_INVALID,
|
|
4
|
+
E_PROVIDER_UNAVAILABLE,
|
|
5
|
+
E_PROVIDER_TIMEOUT,
|
|
6
|
+
E_PROVIDER_OUTPUT_INVALID,
|
|
7
|
+
E_PROVIDER_OUTPUT_LIMIT,
|
|
8
|
+
E_PROVIDER_CAPABILITY_UNSUPPORTED,
|
|
9
|
+
E_PROVIDER_AUTHORITY_ESCALATION,
|
|
10
|
+
E_PROVIDER_EXECUTION_FAILED,
|
|
11
|
+
providerError,
|
|
12
|
+
} from "./errors.js";
|
|
13
|
+
import { createProviderRegistry } from "./registry.js";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
PROVIDER_KINDS,
|
|
17
|
+
PROVIDER_CAPABILITIES,
|
|
18
|
+
capabilityFor,
|
|
19
|
+
E_PROVIDER_INVALID,
|
|
20
|
+
E_PROVIDER_UNAVAILABLE,
|
|
21
|
+
E_PROVIDER_TIMEOUT,
|
|
22
|
+
E_PROVIDER_OUTPUT_INVALID,
|
|
23
|
+
E_PROVIDER_OUTPUT_LIMIT,
|
|
24
|
+
E_PROVIDER_CAPABILITY_UNSUPPORTED,
|
|
25
|
+
E_PROVIDER_AUTHORITY_ESCALATION,
|
|
26
|
+
E_PROVIDER_EXECUTION_FAILED,
|
|
27
|
+
providerError,
|
|
28
|
+
createProviderRegistry,
|
|
29
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { types } from "node:util";
|
|
2
|
+
import { deepFreeze } from "../core/portable-context.js";
|
|
3
|
+
import {
|
|
4
|
+
E_PROVIDER_AUTHORITY_ESCALATION,
|
|
5
|
+
E_PROVIDER_INVALID,
|
|
6
|
+
E_PROVIDER_OUTPUT_INVALID,
|
|
7
|
+
E_PROVIDER_OUTPUT_LIMIT,
|
|
8
|
+
providerError,
|
|
9
|
+
} from "./errors.js";
|
|
10
|
+
|
|
11
|
+
const AUTHORITY_KEYS = new Set([
|
|
12
|
+
"lifecycleAuthority", "completionAuthority", "evidenceAuthority",
|
|
13
|
+
"executableAuthority", "installAuthority",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function isPlainObject(value) {
|
|
17
|
+
if (value === null || typeof value !== "object" || types.isProxy(value) || Array.isArray(value)) return false;
|
|
18
|
+
const prototype = Object.getPrototypeOf(value);
|
|
19
|
+
return prototype === Object.prototype || prototype === null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function serializeSnapshot(value, maxBytes, invalidCode) {
|
|
23
|
+
const ancestors = new WeakSet();
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
let nodes = 0;
|
|
26
|
+
const invalid = () => { throw providerError(invalidCode, "Provider payload must contain only plain JSON data without accessors"); };
|
|
27
|
+
const limit = () => { throw providerError(E_PROVIDER_OUTPUT_LIMIT, "Provider payload exceeds its byte or structural limit"); };
|
|
28
|
+
function charge(text) {
|
|
29
|
+
bytes += Buffer.byteLength(text, "utf8");
|
|
30
|
+
if (bytes > maxBytes) limit();
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
function string(value) {
|
|
34
|
+
if (value.length > maxBytes - bytes) limit();
|
|
35
|
+
return charge(JSON.stringify(value));
|
|
36
|
+
}
|
|
37
|
+
function visit(value, depth) {
|
|
38
|
+
nodes += 1;
|
|
39
|
+
if (depth > 64 || nodes > 100_000) limit();
|
|
40
|
+
if (value === null) return charge("null");
|
|
41
|
+
if (typeof value === "string") return string(value);
|
|
42
|
+
if (typeof value === "boolean") return charge(String(value));
|
|
43
|
+
if (typeof value === "number" && Number.isFinite(value)) return charge(JSON.stringify(value));
|
|
44
|
+
if (typeof value !== "object" || types.isProxy(value)) invalid();
|
|
45
|
+
return visitContainer(value, depth);
|
|
46
|
+
}
|
|
47
|
+
function assertContainer(value, array) {
|
|
48
|
+
const valid = array ? Object.getPrototypeOf(value) === Array.prototype : isPlainObject(value);
|
|
49
|
+
if (!valid) invalid();
|
|
50
|
+
}
|
|
51
|
+
function visitContainer(value, depth) {
|
|
52
|
+
const array = Array.isArray(value);
|
|
53
|
+
assertContainer(value, array);
|
|
54
|
+
if (ancestors.has(value)) invalid();
|
|
55
|
+
ancestors.add(value);
|
|
56
|
+
const keys = Reflect.ownKeys(value);
|
|
57
|
+
if (keys.length > 100_000 || keys.length > maxBytes) limit();
|
|
58
|
+
const parts = [];
|
|
59
|
+
charge(array ? "[]" : "{}");
|
|
60
|
+
const length = array ? Object.getOwnPropertyDescriptor(value, "length").value : 0;
|
|
61
|
+
if (array && keys.length !== length + 1) invalid();
|
|
62
|
+
let index = 0;
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
if (array && key === "length") continue;
|
|
65
|
+
if (typeof key !== "string") invalid();
|
|
66
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
67
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) invalid();
|
|
68
|
+
if (array && key !== String(index)) invalid();
|
|
69
|
+
if (index > 0) charge(",");
|
|
70
|
+
const prefix = array ? "" : string(key) + charge(":");
|
|
71
|
+
parts.push(prefix + visit(descriptor.value, depth + 1));
|
|
72
|
+
index += 1;
|
|
73
|
+
}
|
|
74
|
+
if (array && index !== length) invalid();
|
|
75
|
+
ancestors.delete(value);
|
|
76
|
+
return array ? `[${parts.join(",")}]` : `{${parts.join(",")}}`;
|
|
77
|
+
}
|
|
78
|
+
return visit(value, 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertAuthorityFree(value) {
|
|
82
|
+
if (value === null || typeof value !== "object") return;
|
|
83
|
+
for (const [key, child] of Object.entries(value)) {
|
|
84
|
+
if (AUTHORITY_KEYS.has(key) && child === true) {
|
|
85
|
+
throw providerError(E_PROVIDER_AUTHORITY_ESCALATION, "Provider result declares forbidden authority");
|
|
86
|
+
}
|
|
87
|
+
assertAuthorityFree(child);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function normalizeProviderInput(value, maxBytes) {
|
|
92
|
+
return deepFreeze(JSON.parse(serializeSnapshot(value, maxBytes, E_PROVIDER_INVALID)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function normalizeProviderResult(value, maxBytes) {
|
|
96
|
+
if (!isPlainObject(value)) {
|
|
97
|
+
throw providerError(E_PROVIDER_OUTPUT_INVALID, "Provider result must be a plain JSON object");
|
|
98
|
+
}
|
|
99
|
+
const normalized = JSON.parse(serializeSnapshot(value, maxBytes, E_PROVIDER_OUTPUT_INVALID));
|
|
100
|
+
assertAuthorityFree(normalized);
|
|
101
|
+
if (normalized.status === "COMPLETE") {
|
|
102
|
+
throw providerError(E_PROVIDER_AUTHORITY_ESCALATION, "Provider result cannot assign lifecycle completion");
|
|
103
|
+
}
|
|
104
|
+
return deepFreeze(normalized);
|
|
105
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { types } from "node:util";
|
|
3
|
+
import { capabilityFor } from "./capabilities.js";
|
|
4
|
+
import { isPlainObject, normalizeProviderInput, normalizeProviderResult } from "./json-snapshot.js";
|
|
5
|
+
import {
|
|
6
|
+
E_PROVIDER_EXECUTION_FAILED,
|
|
7
|
+
E_PROVIDER_INVALID,
|
|
8
|
+
E_PROVIDER_TIMEOUT,
|
|
9
|
+
E_PROVIDER_UNAVAILABLE,
|
|
10
|
+
providerError,
|
|
11
|
+
} from "./errors.js";
|
|
12
|
+
|
|
13
|
+
const PROVIDER_ID_REGEX = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
14
|
+
const REGISTRY_MAX_PROVIDERS = 256;
|
|
15
|
+
|
|
16
|
+
function assertEntry(entry, key) {
|
|
17
|
+
if (!isPlainObject(entry)) {
|
|
18
|
+
throw providerError(E_PROVIDER_INVALID, "Provider must be a plain object");
|
|
19
|
+
}
|
|
20
|
+
for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(entry))) {
|
|
21
|
+
if (!("value" in descriptor)) throw providerError(E_PROVIDER_INVALID, "Provider accessors are not supported");
|
|
22
|
+
}
|
|
23
|
+
if (typeof entry.id !== "string" || !PROVIDER_ID_REGEX.test(entry.id) || entry.id !== key) {
|
|
24
|
+
throw providerError(E_PROVIDER_INVALID, "Provider identity does not match the registry key");
|
|
25
|
+
}
|
|
26
|
+
if (entry.version !== undefined && (typeof entry.version !== "string" || entry.version.trim() === "" || entry.version.length > 64)) {
|
|
27
|
+
throw providerError(E_PROVIDER_INVALID, "Provider version must be a non-empty string of at most 64 characters");
|
|
28
|
+
}
|
|
29
|
+
if (!capabilityFor(entry.kind)) {
|
|
30
|
+
throw providerError(E_PROVIDER_INVALID, "Provider declares an unsupported kind");
|
|
31
|
+
}
|
|
32
|
+
if (entry.capability !== undefined && entry.capability !== capabilityFor(entry.kind)) {
|
|
33
|
+
throw providerError(E_PROVIDER_INVALID, "Provider capability metadata contradicts its kind");
|
|
34
|
+
}
|
|
35
|
+
if (typeof entry.operation !== "function") {
|
|
36
|
+
throw providerError(E_PROVIDER_INVALID, "Provider must implement operation(input, context)");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function boundedInteger(value, fallback, maximum) {
|
|
41
|
+
const resolved = value === undefined ? fallback : value;
|
|
42
|
+
if (!Number.isInteger(resolved) || resolved <= 0 || resolved > maximum) {
|
|
43
|
+
throw providerError(E_PROVIDER_INVALID, "Provider limits must be positive bounded integers");
|
|
44
|
+
}
|
|
45
|
+
return resolved;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeOptions(options) {
|
|
49
|
+
if (!isPlainObject(options)) throw providerError(E_PROVIDER_INVALID, "Provider options must be a plain object");
|
|
50
|
+
const descriptors = Object.getOwnPropertyDescriptors(options);
|
|
51
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
52
|
+
if (!["timeoutMs", "maxInputBytes", "maxResultBytes"].includes(key) || !("value" in descriptor)) {
|
|
53
|
+
throw providerError(E_PROVIDER_INVALID, "Unsupported provider option");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
57
|
+
if (typeof key !== "string") throw providerError(E_PROVIDER_INVALID, "Unsupported provider option");
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
timeoutMs: boundedInteger(descriptors.timeoutMs?.value, 10_000, 300_000),
|
|
61
|
+
maxInputBytes: boundedInteger(descriptors.maxInputBytes?.value, 262_144, 4_194_304),
|
|
62
|
+
maxResultBytes: boundedInteger(descriptors.maxResultBytes?.value, 262_144, 4_194_304),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function callProvider(operation, normalize) {
|
|
67
|
+
let returned;
|
|
68
|
+
try {
|
|
69
|
+
returned = operation();
|
|
70
|
+
} catch {
|
|
71
|
+
throw providerError(E_PROVIDER_EXECUTION_FAILED, "Provider execution failed");
|
|
72
|
+
}
|
|
73
|
+
if (types.isPromise(returned) && !types.isProxy(returned)) {
|
|
74
|
+
return Promise.prototype.then.call(returned, normalize, () => {
|
|
75
|
+
throw providerError(E_PROVIDER_EXECUTION_FAILED, "Provider execution failed");
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return normalize(returned);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function withProviderDeadline(providerId, timeoutMs, operation) {
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const context = Object.freeze({ signal: controller.signal, timeoutMs, providerId });
|
|
84
|
+
const expires = performance.now() + timeoutMs;
|
|
85
|
+
const timeoutError = providerError(E_PROVIDER_TIMEOUT, "Provider invocation exceeded its deadline");
|
|
86
|
+
const checkpoint = () => {
|
|
87
|
+
if (controller.signal.aborted || performance.now() >= expires) {
|
|
88
|
+
controller.abort();
|
|
89
|
+
throw timeoutError;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
let timer;
|
|
93
|
+
const timeout = new Promise((_, reject) => {
|
|
94
|
+
timer = setTimeout(() => {
|
|
95
|
+
reject(timeoutError);
|
|
96
|
+
controller.abort();
|
|
97
|
+
}, timeoutMs);
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
return await Promise.race([
|
|
101
|
+
timeout,
|
|
102
|
+
Promise.resolve().then(() => operation(context, checkpoint)),
|
|
103
|
+
]);
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createProviderRegistry({ providers = {} } = {}) {
|
|
110
|
+
if (!isPlainObject(providers)) throw providerError(E_PROVIDER_INVALID, "Provider registry requires an object map");
|
|
111
|
+
const map = new Map();
|
|
112
|
+
for (const key of Reflect.ownKeys(providers)) {
|
|
113
|
+
if (typeof key !== "string" || !PROVIDER_ID_REGEX.test(key)) {
|
|
114
|
+
throw providerError(E_PROVIDER_INVALID, "Invalid provider registry key");
|
|
115
|
+
}
|
|
116
|
+
if (map.size >= REGISTRY_MAX_PROVIDERS) throw providerError(E_PROVIDER_INVALID, "Too many providers");
|
|
117
|
+
const descriptor = Object.getOwnPropertyDescriptor(providers, key);
|
|
118
|
+
if (!("value" in descriptor) || !descriptor.enumerable) throw providerError(E_PROVIDER_INVALID, "Invalid provider registry entry");
|
|
119
|
+
const entry = descriptor.value;
|
|
120
|
+
if (typeof entry !== "function") assertEntry(entry, key);
|
|
121
|
+
map.set(key, entry);
|
|
122
|
+
}
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
get(id) { return map.get(id) ?? null; },
|
|
125
|
+
has(id) { return map.has(id); },
|
|
126
|
+
list() { return [...map.keys()].sort(); },
|
|
127
|
+
async invoke(id, input = {}, options = {}) {
|
|
128
|
+
const { timeoutMs, maxInputBytes, maxResultBytes } = normalizeOptions(options);
|
|
129
|
+
if (!map.has(id)) throw providerError(E_PROVIDER_UNAVAILABLE, "Provider is not registered");
|
|
130
|
+
const payload = normalizeProviderInput(input, maxInputBytes);
|
|
131
|
+
const entry = map.get(id);
|
|
132
|
+
return withProviderDeadline(id, timeoutMs, async (context, checkpoint) => {
|
|
133
|
+
checkpoint();
|
|
134
|
+
const validate = provider => {
|
|
135
|
+
checkpoint();
|
|
136
|
+
assertEntry(provider, id);
|
|
137
|
+
return Object.freeze({ provider });
|
|
138
|
+
};
|
|
139
|
+
const { provider } = typeof entry === "function"
|
|
140
|
+
? await callProvider(() => entry(context), validate)
|
|
141
|
+
: validate(entry);
|
|
142
|
+
checkpoint();
|
|
143
|
+
return callProvider(() => provider.operation(payload, context), result => {
|
|
144
|
+
checkpoint();
|
|
145
|
+
const normalized = normalizeProviderResult(result, maxResultBytes);
|
|
146
|
+
checkpoint();
|
|
147
|
+
return normalized;
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -183,9 +183,10 @@ export function validateArchiveEntry(entry) {
|
|
|
183
183
|
|
|
184
184
|
async function listArchiveEntries(archivePath, { archive = "tar.gz", execFileImpl = nodeExecFile } = {}) {
|
|
185
185
|
const listCommand = archive === "zip" && process.platform !== "win32" ? "unzip" : "tar";
|
|
186
|
-
const
|
|
186
|
+
const archiveName = path.basename(archivePath);
|
|
187
|
+
const listArgs = listCommand === "unzip" ? ["-Z1", archiveName] : ["-tf", archiveName];
|
|
187
188
|
try {
|
|
188
|
-
const listed = await execFileAsync(execFileImpl, listCommand, listArgs, { maxBuffer: 2 * 1024 * 1024 });
|
|
189
|
+
const listed = await execFileAsync(execFileImpl, listCommand, listArgs, { cwd: path.dirname(archivePath), maxBuffer: 2 * 1024 * 1024 });
|
|
189
190
|
return listed.stdout.split(/\r?\n/).filter(Boolean).map(validateArchiveEntry);
|
|
190
191
|
} catch (cause) {
|
|
191
192
|
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.ENGINE_EXTRACTION_FAILED, `Unable to inspect tgrep archive: ${cause.message}`, { cause });
|
|
@@ -195,11 +196,12 @@ async function listArchiveEntries(archivePath, { archive = "tar.gz", execFileImp
|
|
|
195
196
|
async function extractArchive(archivePath, extractionDirectory, { archive = "tar.gz", execFileImpl = nodeExecFile } = {}) {
|
|
196
197
|
await listArchiveEntries(archivePath, { archive, execFileImpl });
|
|
197
198
|
const extractCommand = archive === "zip" && process.platform !== "win32" ? "unzip" : "tar";
|
|
199
|
+
const destination = path.relative(path.dirname(archivePath), extractionDirectory).replaceAll("\\", "/") || ".";
|
|
198
200
|
const extractArgs = extractCommand === "unzip"
|
|
199
|
-
? ["-q", archivePath, "-d",
|
|
200
|
-
: ["-xf", archivePath, "-C",
|
|
201
|
+
? ["-q", path.basename(archivePath), "-d", destination]
|
|
202
|
+
: ["-xf", path.basename(archivePath), "-C", destination];
|
|
201
203
|
try {
|
|
202
|
-
await execFileAsync(execFileImpl, extractCommand, extractArgs, { maxBuffer: 2 * 1024 * 1024 });
|
|
204
|
+
await execFileAsync(execFileImpl, extractCommand, extractArgs, { cwd: path.dirname(archivePath), maxBuffer: 2 * 1024 * 1024 });
|
|
203
205
|
} catch (cause) {
|
|
204
206
|
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.ENGINE_EXTRACTION_FAILED, `Unable to extract tgrep archive: ${cause.message}`, { cause });
|
|
205
207
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalFingerprint } from "../core/artifacts.js";
|
|
3
|
+
|
|
4
|
+
/** Validate byte evidence separately from the domain payload schema. */
|
|
5
|
+
export function artifactByteDigest(record) {
|
|
6
|
+
if (typeof record?.sourceText !== "string" || typeof record?.byteDigest !== "string") {
|
|
7
|
+
const error = new Error("Original artifact byte evidence is unavailable");
|
|
8
|
+
error.code = "E_STORAGE_ARTIFACT_BYTES_UNAVAILABLE";
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const digest = createHash("sha256").update(record.sourceText).digest("hex");
|
|
13
|
+
if (digest !== record.byteDigest || canonicalFingerprint(JSON.parse(record.sourceText)) !== canonicalFingerprint(record.payload)) {
|
|
14
|
+
throw new Error("Artifact source bytes, digest and canonical payload disagree");
|
|
15
|
+
}
|
|
16
|
+
return digest;
|
|
17
|
+
} catch (cause) {
|
|
18
|
+
const error = new Error("Stored artifact byte evidence is invalid", { cause });
|
|
19
|
+
error.code = "E_STORAGE_ARTIFACT_INVALID";
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Pure canonical binding validation, independent of files and SQLite. */
|
|
2
|
+
export function validateAttachmentReference(reference) {
|
|
3
|
+
if (!reference || typeof reference.taskId !== "string" || !reference.taskId
|
|
4
|
+
|| typeof reference.referenceId !== "string" || !reference.referenceId || reference.referenceId.length > 256
|
|
5
|
+
|| !Number.isSafeInteger(reference.size) || reference.size < 0
|
|
6
|
+
|| !/^[a-f0-9]{64}$/.test(reference.sha256 ?? "")
|
|
7
|
+
|| reference.path !== `.forgeloop/attachments/objects/${reference.sha256}`) {
|
|
8
|
+
throw Object.assign(new Error("Invalid canonical attachment reference"), { code: "E_STORAGE_ATTACHMENT_INVALID" });
|
|
9
|
+
}
|
|
10
|
+
return reference;
|
|
11
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants, createWriteStream } from "node:fs";
|
|
3
|
+
import { link, lstat, mkdir, mkdtemp, open, rm } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { Transform } from "node:stream";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
import { digestFile, syncDirectory } from "./file-durability.js";
|
|
9
|
+
import { assertSafePath } from "../core/filesystem.js";
|
|
10
|
+
import { assertWritableContext } from "./transaction.js";
|
|
11
|
+
|
|
12
|
+
function invalid(message) { return Object.assign(new Error(message), { code: "E_STORAGE_ATTACHMENT_INVALID" }); }
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/** Verify referenced immutable bytes without loading the attachment into memory. */
|
|
16
|
+
async function attachmentFilename(target, reference) {
|
|
17
|
+
if (!reference || !Number.isSafeInteger(reference.size) || reference.size < 0
|
|
18
|
+
|| !/^[a-f0-9]{64}$/.test(reference.sha256 ?? "")
|
|
19
|
+
|| reference.path !== `.forgeloop/attachments/objects/${reference.sha256}`) throw invalid("Invalid attachment reference");
|
|
20
|
+
const filename = await assertSafePath(target, reference.path);
|
|
21
|
+
if (!(await lstat(filename)).isFile()) throw invalid("Attachment reference must name a regular file");
|
|
22
|
+
return filename;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function verifyAttachmentFile(target, reference) {
|
|
26
|
+
const filename = await attachmentFilename(target, reference);
|
|
27
|
+
const actual = await digestFile(filename, { includeSize: true });
|
|
28
|
+
if (actual.size !== reference.size || actual.sha256 !== reference.sha256) throw invalid("Attachment bytes disagree with their reference");
|
|
29
|
+
return reference;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Give consumers private verified bytes, closing the source check/use window. */
|
|
33
|
+
export async function withVerifiedAttachmentFile(target, reference, callback) {
|
|
34
|
+
const source = await attachmentFilename(target, reference);
|
|
35
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "forgeloop-attachment-read-"));
|
|
36
|
+
try {
|
|
37
|
+
const filename = path.join(directory, "bytes");
|
|
38
|
+
const handle = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
39
|
+
try {
|
|
40
|
+
if (!(await handle.stat()).isFile()) throw invalid("Attachment must remain a regular file");
|
|
41
|
+
const hash = createHash("sha256");
|
|
42
|
+
let size = 0;
|
|
43
|
+
const check = new Transform({ transform(bytes, _encoding, done) {
|
|
44
|
+
size += bytes.length;
|
|
45
|
+
if (size > reference.size) { done(invalid("Attachment exceeds its reference size")); return; }
|
|
46
|
+
hash.update(bytes);
|
|
47
|
+
done(null, bytes);
|
|
48
|
+
} });
|
|
49
|
+
await pipeline(handle.createReadStream({ autoClose: false }), check, createWriteStream(filename, { flags: "wx", mode: 0o600 }));
|
|
50
|
+
if (size !== reference.size || hash.digest("hex") !== reference.sha256) throw invalid("Attachment bytes disagree with their reference");
|
|
51
|
+
} finally { await handle.close(); }
|
|
52
|
+
return await callback(filename);
|
|
53
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Publish immutable bytes before a caller commits their database reference. */
|
|
57
|
+
export async function publishAttachmentFile(target, readable, { db, temporaryRoot } = {}) {
|
|
58
|
+
if (db?.isTransaction) throw invalid("Attachment publication must precede the database write transaction");
|
|
59
|
+
if (db) assertWritableContext(db);
|
|
60
|
+
const root = await assertSafePath(target, ".forgeloop/attachments/objects");
|
|
61
|
+
await mkdir(root, { recursive: true });
|
|
62
|
+
await assertSafePath(target, ".forgeloop/attachments/objects");
|
|
63
|
+
const temporaryParent = temporaryRoot ? await assertSafePath(temporaryRoot, ".") : root;
|
|
64
|
+
const temporary = await mkdtemp(path.join(temporaryParent, ".publishing-"));
|
|
65
|
+
const filename = path.join(temporary, "bytes");
|
|
66
|
+
try {
|
|
67
|
+
await pipeline(readable, createWriteStream(filename, { flags: "wx", mode: 0o600 }));
|
|
68
|
+
const binding = await digestFile(filename, { includeSize: true });
|
|
69
|
+
const file = await open(filename, "r+");
|
|
70
|
+
try { await file.sync(); } finally { await file.close(); }
|
|
71
|
+
const reference = { path: `.forgeloop/attachments/objects/${binding.sha256}`, ...binding };
|
|
72
|
+
const destination = await assertSafePath(target, reference.path);
|
|
73
|
+
try { await link(filename, destination); }
|
|
74
|
+
catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
75
|
+
await verifyAttachmentFile(target, reference);
|
|
76
|
+
await syncDirectory(root);
|
|
77
|
+
await syncDirectory(path.dirname(root));
|
|
78
|
+
await syncDirectory(path.dirname(path.dirname(root)));
|
|
79
|
+
await syncDirectory(target);
|
|
80
|
+
return reference;
|
|
81
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
82
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { publishAttachmentFile, verifyAttachmentFile } from "./attachment-files.js";
|
|
2
|
+
import { assertWritableContext, runInTransaction } from "./transaction.js";
|
|
3
|
+
import { validateAttachmentReference } from "./attachment-binding.js";
|
|
4
|
+
export { validateAttachmentReference } from "./attachment-binding.js";
|
|
5
|
+
|
|
6
|
+
function invalid(message) { return Object.assign(new Error(message), { code: "E_STORAGE_ATTACHMENT_INVALID" }); }
|
|
7
|
+
|
|
8
|
+
/** Synchronous insertion is used only after the referenced bytes were verified. */
|
|
9
|
+
export function insertVerifiedAttachmentReference(db, reference) {
|
|
10
|
+
assertWritableContext(db);
|
|
11
|
+
validateAttachmentReference(reference);
|
|
12
|
+
const prior = db.prepare("SELECT path, size, sha256 FROM attachment_references WHERE task_id = ? AND reference_id = ?").get(reference.taskId, reference.referenceId);
|
|
13
|
+
if (prior) {
|
|
14
|
+
if (prior.path !== reference.path || prior.size !== reference.size || prior.sha256 !== reference.sha256) throw invalid("An immutable attachment identity already names different bytes");
|
|
15
|
+
return reference;
|
|
16
|
+
}
|
|
17
|
+
db.prepare("INSERT INTO attachment_references(task_id, reference_id, path, size, sha256) VALUES (?, ?, ?, ?, ?)")
|
|
18
|
+
.run(reference.taskId, reference.referenceId, reference.path, reference.size, reference.sha256);
|
|
19
|
+
return reference;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Publish first, then commit the immutable binding in a short writer transaction. */
|
|
23
|
+
export async function registerAttachment(db, target, { taskId, referenceId, readable }) {
|
|
24
|
+
if (db.isTransaction) throw invalid("Attachment registration cannot span an active write transaction");
|
|
25
|
+
assertWritableContext(db);
|
|
26
|
+
const binding = await publishAttachmentFile(target, readable, { db });
|
|
27
|
+
const reference = validateAttachmentReference({ taskId, referenceId, ...binding });
|
|
28
|
+
await verifyAttachmentFile(target, reference);
|
|
29
|
+
return runInTransaction(db, () => insertVerifiedAttachmentReference(db, reference));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function* iterateAttachmentReferences(db) {
|
|
33
|
+
for (const row of db.prepare("SELECT * FROM attachment_references ORDER BY task_id, reference_id").iterate()) {
|
|
34
|
+
yield validateAttachmentReference({ taskId: row.task_id, referenceId: row.reference_id, path: row.path, size: row.size, sha256: row.sha256 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { loadStorageDriver } from "./runtime.js";
|
|
2
|
+
import { mkdtemp, rm, link, open } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { assertSafePath } from "../core/filesystem.js";
|
|
5
|
+
import { checkStorageIntegrity, openStorageDatabase } from "./connection.js";
|
|
6
|
+
|
|
7
|
+
/** Database-only backup; attachment restore requires its own verified inventory. */
|
|
8
|
+
export async function backupStorageDatabase(db, destination) {
|
|
9
|
+
const { backup, DatabaseSync } = loadStorageDriver();
|
|
10
|
+
if (db.isTransaction) throw Object.assign(new Error("Backup requires committed storage state"), { code: "E_STORAGE_SNAPSHOT_TRANSACTION" });
|
|
11
|
+
const absolute = path.resolve(destination);
|
|
12
|
+
const parent = path.dirname(absolute);
|
|
13
|
+
await assertSafePath(parent, path.basename(absolute));
|
|
14
|
+
const temporary = await mkdtemp(path.join(parent, ".forgeloop-backup-"));
|
|
15
|
+
const filename = path.join(temporary, "state.sqlite");
|
|
16
|
+
try {
|
|
17
|
+
await backup(db, filename);
|
|
18
|
+
const snapshot = new DatabaseSync(filename, { readOnly: true, enableForeignKeyConstraints: true });
|
|
19
|
+
try {
|
|
20
|
+
const integrity = checkStorageIntegrity(snapshot);
|
|
21
|
+
if (!integrity.ok) throw Object.assign(new Error("Backup snapshot failed storage integrity validation"), { code: "E_STORAGE_BACKUP_INVALID", integrity });
|
|
22
|
+
} finally { snapshot.close(); }
|
|
23
|
+
// FlushFileBuffers on Windows requires write access to this owned snapshot.
|
|
24
|
+
const file = await open(filename, "r+");
|
|
25
|
+
try { await file.sync(); } finally { await file.close(); }
|
|
26
|
+
// Same-directory hard-link publication is atomic and cannot overwrite a
|
|
27
|
+
// retained backup. The private temporary name is removed afterward.
|
|
28
|
+
await link(filename, absolute);
|
|
29
|
+
try {
|
|
30
|
+
const directory = await open(parent, "r");
|
|
31
|
+
try { await directory.sync(); } finally { await directory.close(); }
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Match the existing portable atomic-file boundary on platforms that
|
|
34
|
+
// cannot open/sync directories. Other I/O failures remain visible.
|
|
35
|
+
if (!["EINVAL", "EPERM", "EISDIR", "ENOTSUP", "UNKNOWN"].includes(error.code)) throw error;
|
|
36
|
+
}
|
|
37
|
+
return { path: absolute, kind: "DATABASE_ONLY", attachmentsIncluded: false };
|
|
38
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Restore into a fresh database pathname; never replace an active project. */
|
|
42
|
+
export async function restoreStorageDatabase(source, destination) {
|
|
43
|
+
const absolute = path.resolve(source);
|
|
44
|
+
const parent = path.dirname(absolute);
|
|
45
|
+
for (const suffix of ["", "-wal", "-shm"]) await assertSafePath(parent, `${path.basename(absolute)}${suffix}`);
|
|
46
|
+
// Read-only open rejects unsupported schema/format metadata rather than
|
|
47
|
+
// silently upgrading a retained backup during a restore drill.
|
|
48
|
+
const db = openStorageDatabase(absolute, { readOnly: true });
|
|
49
|
+
try {
|
|
50
|
+
const restored = await backupStorageDatabase(db, destination);
|
|
51
|
+
return { ...restored, source: absolute, restored: true };
|
|
52
|
+
} finally { db.close(); }
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { STORAGE_FORMAT, STORAGE_FORMAT_VERSION, STORAGE_RELATIVE_PATH, STORAGE_SCHEMA_VERSION } from "./schema.js";
|
|
2
|
+
import { STORAGE_MINIMUM_NODE } from "./runtime.js";
|
|
3
|
+
|
|
4
|
+
/** Package capabilities only; no project selection, database open or runtime probe. */
|
|
5
|
+
export function getStorageCapabilities() {
|
|
6
|
+
return {
|
|
7
|
+
version: 1,
|
|
8
|
+
defaultBackend: "sqlite",
|
|
9
|
+
soleSQLiteWriter: false,
|
|
10
|
+
sqlite: {
|
|
11
|
+
format: STORAGE_FORMAT,
|
|
12
|
+
formatVersion: STORAGE_FORMAT_VERSION,
|
|
13
|
+
schemaVersion: STORAGE_SCHEMA_VERSION,
|
|
14
|
+
path: STORAGE_RELATIVE_PATH,
|
|
15
|
+
driver: "node:sqlite",
|
|
16
|
+
minimumNode: STORAGE_MINIMUM_NODE,
|
|
17
|
+
selection: "FRESH_WRITABLE_BOOTSTRAP_OR_EXISTING_DATABASE",
|
|
18
|
+
},
|
|
19
|
+
portableFormats: ["JSON", "NDJSON"],
|
|
20
|
+
migration: {
|
|
21
|
+
explicit: true,
|
|
22
|
+
requiresWritersQuiesced: true,
|
|
23
|
+
resumeRequiresRecordedInventory: true,
|
|
24
|
+
automaticDowngrade: false,
|
|
25
|
+
commands: ["storage-migrate", "storage-migration-resume", "storage-migration-status", "storage-rollback", "storage-rollback-resume"],
|
|
26
|
+
rollbackRequiresNativeWritesExcluded: true,
|
|
27
|
+
rollbackRequiresPinnedLegacyTarget: true,
|
|
28
|
+
},
|
|
29
|
+
backup: { database: true, referencedAttachments: true, command: "storage-backup" },
|
|
30
|
+
};
|
|
31
|
+
}
|