@piagent/platform 1.3.1 → 1.4.1
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/CHANGELOG.md +132 -0
- package/README.md +10 -4
- package/README.vi.md +27 -0
- package/architecture/layers.json +96 -1
- package/docs/README.md +1 -0
- package/docs/architecture.md +1 -0
- package/docs/authority-manifest.md +1 -0
- package/docs/auto-delegation-policy.md +1 -0
- package/docs/capability-packs.md +1 -0
- package/docs/command-reference-vietnamese.md +5 -4
- package/docs/context-engine.md +1 -0
- package/docs/context-window-policy.md +1 -0
- package/docs/distribution-standard.md +8 -7
- package/docs/en/architecture.md +13 -0
- package/docs/herdr-workflow.md +1 -0
- package/docs/languages.json +20 -4
- package/docs/mcp-and-tools.md +125 -9
- package/docs/memory-policy.md +1 -0
- package/docs/model-options.md +1 -0
- package/docs/oauth-providers.md +1 -0
- package/docs/operator-manual-vietnamese.md +15 -12
- package/docs/package-architecture-notes.md +1 -0
- package/docs/project-adapters.md +1 -0
- package/docs/project-onboarding.md +1 -0
- package/docs/publishing-for-teams.md +37 -0
- package/docs/quality-benchmark.md +1 -0
- package/docs/quickstart-vietnamese.md +3 -2
- package/docs/release-install-policy.md +22 -8
- package/docs/runtime-harness-standard.md +2 -1
- package/docs/runtime-policy-design.md +1 -0
- package/docs/runtime-quality-baseline.md +1 -0
- package/docs/security-redaction-benchmark.md +1 -0
- package/docs/security-threat-model.md +2 -1
- package/docs/subagent-orchestration-capabilities.md +1 -0
- package/docs/subagents-and-multiagent.md +1 -0
- package/docs/task-implementation-contract.md +12 -4
- package/docs/task-lifecycle-vietnamese.md +1 -0
- package/docs/team-onboarding.md +3 -2
- package/docs/third-party-neutrality.md +26 -0
- package/docs/usage-observability.md +71 -0
- package/docs/vercel-docs-site.md +1 -0
- package/docs/vi/architecture.md +13 -0
- package/docs/workflow-recipes.md +1 -0
- package/docs-site/architecture.html +5 -1
- package/docs-site/benchmark.html +5 -1
- package/docs-site/commands.html +5 -1
- package/docs-site/content/en/index.html +49 -0
- package/docs-site/content/en/runtime.html +13 -0
- package/docs-site/content/index.html +49 -0
- package/docs-site/content/runtime.html +28 -0
- package/docs-site/context-engine.html +5 -1
- package/docs-site/en/architecture.html +5 -1
- package/docs-site/en/benchmark.html +5 -1
- package/docs-site/en/commands.html +5 -1
- package/docs-site/en/context-engine.html +5 -1
- package/docs-site/en/index.html +54 -1
- package/docs-site/en/mcp-approval.html +5 -1
- package/docs-site/en/mcp-auth.html +5 -1
- package/docs-site/en/mcp-servers.html +5 -1
- package/docs-site/en/mcp.html +5 -1
- package/docs-site/en/permissions.html +5 -1
- package/docs-site/en/profiles.html +5 -1
- package/docs-site/en/quickstart.html +5 -1
- package/docs-site/en/runtime.html +18 -1
- package/docs-site/en/scope.html +5 -1
- package/docs-site/en/security.html +5 -1
- package/docs-site/en/team.html +5 -1
- package/docs-site/en/whats-new.html +5 -1
- package/docs-site/en/workflows.html +5 -1
- package/docs-site/index.html +54 -1
- package/docs-site/mcp-approval.html +5 -1
- package/docs-site/mcp-auth.html +5 -1
- package/docs-site/mcp-servers.html +5 -1
- package/docs-site/mcp.html +5 -1
- package/docs-site/permissions.html +5 -1
- package/docs-site/profiles.html +5 -1
- package/docs-site/quickstart.html +5 -1
- package/docs-site/runtime.html +33 -1
- package/docs-site/scope.html +5 -1
- package/docs-site/security.html +5 -1
- package/docs-site/team.html +5 -1
- package/docs-site/whats-new.html +5 -1
- package/docs-site/workflows.html +5 -1
- package/evals/fixtures/mutation-provenance-record.invalid.json +17 -0
- package/evals/fixtures/mutation-provenance-record.valid.json +27 -0
- package/evals/fixtures/piagent-webui/approval-v1.invalid.json +54 -0
- package/evals/fixtures/piagent-webui/approval-v1.valid.json +58 -0
- package/evals/fixtures/piagent-webui/attachment-v1.invalid.json +25 -0
- package/evals/fixtures/piagent-webui/attachment-v1.valid.json +25 -0
- package/evals/fixtures/piagent-webui/capabilities-v1.invalid.json +51 -0
- package/evals/fixtures/piagent-webui/capabilities-v1.valid.json +122 -0
- package/evals/fixtures/piagent-webui/commit-summary-v1.invalid.json +10 -0
- package/evals/fixtures/piagent-webui/commit-summary-v1.valid.json +25 -0
- package/evals/fixtures/piagent-webui/control-command-v1.invalid.json +37 -0
- package/evals/fixtures/piagent-webui/control-command-v1.valid.json +35 -0
- package/evals/fixtures/piagent-webui/diff-v1.invalid.json +13 -0
- package/evals/fixtures/piagent-webui/diff-v1.valid.json +141 -0
- package/evals/fixtures/piagent-webui/gateway-capabilities-v1.invalid.json +35 -0
- package/evals/fixtures/piagent-webui/gateway-capabilities-v1.valid.json +35 -0
- package/evals/fixtures/piagent-webui/gateway-protocol-v1.invalid.json +14 -0
- package/evals/fixtures/piagent-webui/gateway-protocol-v1.valid.json +14 -0
- package/evals/fixtures/piagent-webui/handoff-history-v1.invalid.json +16 -0
- package/evals/fixtures/piagent-webui/handoff-history-v1.valid.json +16 -0
- package/evals/fixtures/piagent-webui/model-catalog-v1.invalid.json +31 -0
- package/evals/fixtures/piagent-webui/model-catalog-v1.valid.json +43 -0
- package/evals/fixtures/piagent-webui/queue-v1.invalid.json +29 -0
- package/evals/fixtures/piagent-webui/queue-v1.valid.json +44 -0
- package/evals/fixtures/piagent-webui/recovery-history-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/recovery-history-v1.valid.json +20 -0
- package/evals/fixtures/piagent-webui/release-monitor-v1.invalid.json +12 -0
- package/evals/fixtures/piagent-webui/release-monitor-v1.valid.json +29 -0
- package/evals/fixtures/piagent-webui/review-state-v1.invalid.json +25 -0
- package/evals/fixtures/piagent-webui/review-state-v1.valid.json +38 -0
- package/evals/fixtures/piagent-webui/runtime-event-v2.invalid.json +49 -0
- package/evals/fixtures/piagent-webui/runtime-event-v2.valid.json +49 -0
- package/evals/fixtures/piagent-webui/session-catalog-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/session-catalog-v1.valid.json +58 -0
- package/evals/fixtures/piagent-webui/session-command-v1.invalid.json +19 -0
- package/evals/fixtures/piagent-webui/session-command-v1.valid.json +19 -0
- package/evals/fixtures/piagent-webui/snapshot-v1.invalid.json +5 -0
- package/evals/fixtures/piagent-webui/snapshot-v1.valid.json +191 -0
- package/evals/fixtures/piagent-webui/source-change-v1.invalid.json +57 -0
- package/evals/fixtures/piagent-webui/source-change-v1.valid.json +98 -0
- package/evals/fixtures/piagent-webui/source-mutation-v1.invalid.json +19 -0
- package/evals/fixtures/piagent-webui/source-mutation-v1.valid.json +38 -0
- package/evals/fixtures/piagent-webui/source-revert-v1.invalid.json +20 -0
- package/evals/fixtures/piagent-webui/source-revert-v1.valid.json +62 -0
- package/evals/fixtures/piagent-webui/subagent-tree-v1.invalid.json +26 -0
- package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +39 -0
- package/evals/fixtures/piagent-webui/task-index-v1.invalid.json +15 -0
- package/evals/fixtures/piagent-webui/task-index-v1.valid.json +15 -0
- package/evals/fixtures/piagent-webui/task-timeline-v1.invalid.json +14 -0
- package/evals/fixtures/piagent-webui/task-timeline-v1.valid.json +16 -0
- package/evals/fixtures/piagent-webui/transcript-v1.invalid.json +35 -0
- package/evals/fixtures/piagent-webui/transcript-v1.valid.json +55 -0
- package/evals/fixtures/task-baseline-manifest.invalid.json +16 -0
- package/evals/fixtures/task-baseline-manifest.valid.json +41 -0
- package/evals/fixtures/verifier-file-snapshot.invalid.json +21 -0
- package/evals/fixtures/verifier-file-snapshot.valid.json +21 -0
- package/package.json +43 -4
- package/packages/piagent-core/README.md +3 -2
- package/packages/piagent-core/benchmark/benchmark-candidate.d.ts +1 -0
- package/packages/piagent-core/benchmark/benchmark-forensics.d.ts +1 -0
- package/packages/piagent-core/benchmark/benchmark-ledger.d.ts +4 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.d.ts +1 -0
- package/packages/piagent-core/benchmark/benchmark-runtime.d.ts +1 -0
- package/packages/piagent-core/capabilities/capability-core.js +2 -2
- package/packages/piagent-core/capabilities/capability-verification-cache.js +17 -7
- package/packages/piagent-core/capabilities/runtime-integrity.js +190 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +80 -272
- package/packages/piagent-core/extensions/redaction-core.d.ts +2 -0
- package/packages/piagent-core/extensions/runtime-evidence.js +12 -1
- package/packages/piagent-core/extensions/shell-reach.ts +250 -0
- package/packages/piagent-core/extensions/task-journal.js +30 -1
- package/packages/piagent-core/extensions/task-state.js +33 -36
- package/packages/piagent-core/mcp/mcp-command-actions.js +1 -1
- package/packages/piagent-core/mcp/mcp-server-catalog.js +139 -9
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -2
- package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +12 -2
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +20 -19
- package/packages/piagent-core/runtime/hooks/tool-result-metadata.ts +13 -0
- package/packages/piagent-core/runtime/input/chat-images.ts +2 -2
- package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +94 -0
- package/packages/piagent-core/runtime/inspection/approval-broker.ts +318 -0
- package/packages/piagent-core/runtime/inspection/commit-summary-projection.ts +59 -0
- package/packages/piagent-core/runtime/inspection/context-telemetry-inspection.ts +37 -0
- package/packages/piagent-core/runtime/inspection/criteria-links.ts +295 -0
- package/packages/piagent-core/runtime/inspection/diff-projection.ts +499 -0
- package/packages/piagent-core/runtime/inspection/git-filter-safety.ts +38 -0
- package/packages/piagent-core/runtime/inspection/git-numstat-adapter.ts +71 -0
- package/packages/piagent-core/runtime/inspection/git-status-adapter.ts +493 -0
- package/packages/piagent-core/runtime/inspection/git-tree-adapter.ts +108 -0
- package/packages/piagent-core/runtime/inspection/mcp-control.ts +10 -0
- package/packages/piagent-core/runtime/inspection/mutation-provenance-contract.ts +131 -0
- package/packages/piagent-core/runtime/inspection/mutation-provenance-recorder.ts +48 -0
- package/packages/piagent-core/runtime/inspection/mutation-provenance-store.ts +161 -0
- package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +36 -0
- package/packages/piagent-core/runtime/inspection/review-state-contract.ts +108 -0
- package/packages/piagent-core/runtime/inspection/review-state-projection.ts +93 -0
- package/packages/piagent-core/runtime/inspection/review-state-store.ts +112 -0
- package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +262 -0
- package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +103 -0
- package/packages/piagent-core/runtime/inspection/source-change-projection.ts +471 -0
- package/packages/piagent-core/runtime/inspection/source-evidence-contract.ts +139 -0
- package/packages/piagent-core/runtime/inspection/source-evidence-store.ts +315 -0
- package/packages/piagent-core/runtime/inspection/source-handoff-store.ts +70 -0
- package/packages/piagent-core/runtime/inspection/source-inspection-plan.ts +34 -0
- package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +164 -0
- package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +168 -0
- package/packages/piagent-core/runtime/inspection/source-open-target.ts +38 -0
- package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +147 -0
- package/packages/piagent-core/runtime/inspection/task-baseline-start-capture.ts +22 -0
- package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +160 -0
- package/packages/piagent-core/runtime/inspection/task-control-journal.ts +131 -0
- package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +95 -0
- package/packages/piagent-core/runtime/inspection/task-provenance-projection.ts +60 -0
- package/packages/piagent-core/runtime/inspection/task-recovery-timeline.ts +88 -0
- package/packages/piagent-core/runtime/inspection/task-run-index.ts +87 -0
- package/packages/piagent-core/runtime/inspection/task-source-projection.ts +401 -0
- package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +101 -0
- package/packages/piagent-core/runtime/inspection/text-diff.ts +60 -0
- package/packages/piagent-core/runtime/inspection/verifier-snapshot-contract.ts +114 -0
- package/packages/piagent-core/runtime/inspection/verifier-snapshot-store.ts +189 -0
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +320 -0
- package/packages/piagent-core/runtime/inspection/workspace-file-reader.ts +132 -0
- package/packages/piagent-core/runtime/inspection/workspace-source-projection.ts +96 -0
- package/packages/piagent-core/runtime/inspection/zero-turn-conformance.ts +175 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +93 -6
- package/packages/piagent-core/runtime/policy/block-remedy.ts +139 -0
- package/packages/piagent-core/runtime/policy/source-index-transaction.ts +173 -0
- package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +42 -0
- package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +184 -0
- package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +105 -0
- package/packages/piagent-core/runtime/product/activity-inspector-footer.ts +5 -3
- package/packages/piagent-core/runtime/product/activity-inspector.ts +85 -343
- package/packages/piagent-core/runtime/recovery/resume-state.ts +38 -1
- package/packages/piagent-core/runtime/registration/activity-inspector-command.ts +68 -14
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +22 -27
- package/packages/piagent-core/runtime/session/model-authorship-state.ts +31 -3
- package/packages/piagent-core/runtime/session/runtime-state.ts +11 -1
- package/packages/piagent-core/runtime/session/token-economics.js +34 -0
- package/packages/piagent-core/runtime/tools/phase-tool-runtime.ts +2 -1
- package/packages/piagent-core/runtime/workflows/task-intake.ts +5 -0
- package/packages/piagent-core/security/sensitive-data.js +7 -0
- package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +1 -0
- package/packages/piagent-webui/dist/client/assets/index-bu7XpD9Q.js +172 -0
- package/packages/piagent-webui/dist/client/index.html +16 -0
- package/packages/piagent-webui/extension/attachment-store.ts +323 -0
- package/packages/piagent-webui/extension/held-message-queue.ts +311 -0
- package/packages/piagent-webui/extension/lifecycle-controller.ts +423 -0
- package/packages/piagent-webui/extension/lifecycle-event-adapter.ts +23 -0
- package/packages/piagent-webui/extension/piagent-webui.ts +470 -0
- package/packages/piagent-webui/extension/review-controller.ts +160 -0
- package/packages/piagent-webui/extension/same-session-bridge.ts +500 -0
- package/packages/piagent-webui/extension/session-options-controller.ts +281 -0
- package/packages/piagent-webui/extension/session-stream-adapter.ts +249 -0
- package/packages/piagent-webui/extension/source-mutation-controller.ts +215 -0
- package/packages/piagent-webui/extension/source-open-controller.ts +136 -0
- package/packages/piagent-webui/extension/source-revert-controller.ts +189 -0
- package/packages/piagent-webui/extension/terminal-session-adapter.ts +73 -0
- package/packages/piagent-webui/extension/vscode-handoff.ts +51 -0
- package/packages/piagent-webui/gateway/control-socket.ts +92 -0
- package/packages/piagent-webui/gateway/extension-authority.ts +47 -0
- package/packages/piagent-webui/gateway/gateway-events.ts +60 -0
- package/packages/piagent-webui/gateway/gateway-protocol-service.ts +64 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +215 -0
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +110 -0
- package/packages/piagent-webui/gateway/mcp-auth-broker.ts +158 -0
- package/packages/piagent-webui/gateway/native-project-picker.ts +29 -0
- package/packages/piagent-webui/gateway/pi-host.ts +41 -0
- package/packages/piagent-webui/gateway/profile-state.ts +1 -0
- package/packages/piagent-webui/gateway/project-registry.ts +108 -0
- package/packages/piagent-webui/gateway/provider-auth-broker.ts +172 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +128 -0
- package/packages/piagent-webui/gateway/session-command-controller.ts +237 -0
- package/packages/piagent-webui/gateway/session-command-store.ts +224 -0
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +239 -0
- package/packages/piagent-webui/gateway/session-lease-store.ts +1 -0
- package/packages/piagent-webui/gateway/session-metadata-store.ts +152 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +489 -0
- package/packages/piagent-webui/ownership/gateway-runtime-context.ts +8 -0
- package/packages/piagent-webui/ownership/profile-state.ts +117 -0
- package/packages/piagent-webui/ownership/session-lease-store.ts +329 -0
- package/packages/piagent-webui/ownership/session-refs.ts +8 -0
- package/packages/piagent-webui/package.json +33 -0
- package/packages/piagent-webui/server/README.md +4 -0
- package/packages/piagent-webui/server/benchmark-release-monitor.ts +230 -0
- package/packages/piagent-webui/server/core-inspection-provider.ts +403 -0
- package/packages/piagent-webui/server/gateway-websocket.ts +143 -0
- package/packages/piagent-webui/server/http-security.ts +27 -0
- package/packages/piagent-webui/server/ipc-read-model-client.ts +89 -0
- package/packages/piagent-webui/server/loopback-server.ts +368 -0
- package/packages/piagent-webui/server/read-model-provider.ts +37 -0
- package/packages/piagent-webui/server/read-only-router.ts +105 -0
- package/packages/piagent-webui/server/session-auth.ts +88 -0
- package/packages/piagent-webui/server/sidecar-main.ts +86 -0
- package/packages/piagent-webui/server/sse-hub.ts +76 -0
- package/packages/piagent-webui/server/static-bundle.ts +40 -0
- package/packages/piagent-webui/server/transcript-projection.ts +125 -0
- package/schemas/mutation-provenance-record.schema.json +100 -0
- package/schemas/piagent-webui/approval-v1.schema.json +654 -0
- package/schemas/piagent-webui/attachment-v1.schema.json +267 -0
- package/schemas/piagent-webui/capabilities-v1.schema.json +729 -0
- package/schemas/piagent-webui/catalog-v1.json +240 -0
- package/schemas/piagent-webui/commit-summary-v1.schema.json +63 -0
- package/schemas/piagent-webui/common-v1.schema.json +479 -0
- package/schemas/piagent-webui/control-command-v1.schema.json +1780 -0
- package/schemas/piagent-webui/diff-v1.schema.json +826 -0
- package/schemas/piagent-webui/gateway-capabilities-v1.schema.json +136 -0
- package/schemas/piagent-webui/gateway-protocol-v1.schema.json +456 -0
- package/schemas/piagent-webui/handoff-history-v1.schema.json +92 -0
- package/schemas/piagent-webui/model-catalog-v1.schema.json +95 -0
- package/schemas/piagent-webui/queue-v1.schema.json +99 -0
- package/schemas/piagent-webui/recovery-history-v1.schema.json +144 -0
- package/schemas/piagent-webui/release-monitor-v1.schema.json +173 -0
- package/schemas/piagent-webui/review-state-v1.schema.json +206 -0
- package/schemas/piagent-webui/runtime-event-v2.schema.json +5206 -0
- package/schemas/piagent-webui/session-catalog-v1.schema.json +222 -0
- package/schemas/piagent-webui/session-command-v1.schema.json +390 -0
- package/schemas/piagent-webui/snapshot-v1.schema.json +2622 -0
- package/schemas/piagent-webui/source-change-v1.schema.json +763 -0
- package/schemas/piagent-webui/source-mutation-v1.schema.json +96 -0
- package/schemas/piagent-webui/source-revert-v1.schema.json +130 -0
- package/schemas/piagent-webui/subagent-tree-v1.schema.json +88 -0
- package/schemas/piagent-webui/task-index-v1.schema.json +74 -0
- package/schemas/piagent-webui/task-timeline-v1.schema.json +49 -0
- package/schemas/piagent-webui/transcript-v1.schema.json +243 -0
- package/schemas/task-baseline-manifest.schema.json +122 -0
- package/schemas/verifier-file-snapshot.schema.json +81 -0
- package/scripts/build-docs-site.mjs +21 -1
- package/scripts/check-architecture.mjs +1 -0
- package/scripts/check-doc-languages.mjs +19 -2
- package/scripts/check-third-party-neutrality.mjs +43 -0
- package/scripts/explain-command.mjs +240 -0
- package/scripts/install-global.sh +105 -4
- package/scripts/mcp-manage.mjs +17 -4
- package/scripts/pi-session-stats.sh +10 -2
- package/scripts/piagent-cli.mjs +30 -3
- package/scripts/piagent-dashboard.mjs +261 -0
- package/scripts/piagent-webui-launcher.mjs +121 -0
- package/scripts/setup.sh +69 -33
- package/scripts/traffic-snapshot.mjs +214 -0
- package/scripts/typescript-loader.mjs +42 -1
- package/scripts/uninstall-global.sh +53 -2
- package/scripts/update-global.mjs +33 -1
- package/scripts/verify-local.sh +257 -0
- package/scripts/verify-release-identity.mjs +4 -1
- package/scripts/verify-translation-pairing.mjs +121 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const MUTATION_PROVENANCE_VERSION = "piagent-mutation-provenance-v1" as const;
|
|
5
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
6
|
+
const TREE = /^wt-content-v2:[a-f0-9]{64}$/;
|
|
7
|
+
const PUBLIC_ID = /^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,159}$/;
|
|
8
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
9
|
+
|
|
10
|
+
export type MutationProvenanceChange = {
|
|
11
|
+
pathDigest: string;
|
|
12
|
+
repoPathBase64: string;
|
|
13
|
+
beforeCarrierDigest: string | null;
|
|
14
|
+
afterCarrierDigest: string | null;
|
|
15
|
+
afterContentDigest: string | null;
|
|
16
|
+
proofMode: "full-content" | "exact-replacement" | null;
|
|
17
|
+
effect: "exact-content" | "content-preserved" | "content-changed" | "unknown";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type MutationProvenanceRecord = {
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
version: typeof MUTATION_PROVENANCE_VERSION;
|
|
23
|
+
recordId: string;
|
|
24
|
+
evidenceRef: string;
|
|
25
|
+
taskId: string;
|
|
26
|
+
taskRunId: string;
|
|
27
|
+
sessionIdentityHash: string;
|
|
28
|
+
toolCallIdentityHash: string;
|
|
29
|
+
toolName: "edit" | "write" | "apply_patch" | "shell" | "opaque";
|
|
30
|
+
recordedAt: string;
|
|
31
|
+
evidenceMode: "exact-runtime" | "observed-runtime";
|
|
32
|
+
beforeTreeDigest: string;
|
|
33
|
+
afterTreeDigest: string;
|
|
34
|
+
changes: MutationProvenanceChange[];
|
|
35
|
+
integrityDigest: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function plain(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
40
|
+
const prototype = Object.getPrototypeOf(value);
|
|
41
|
+
return prototype === Object.prototype || prototype === null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonical(value: unknown): unknown {
|
|
45
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
46
|
+
if (!plain(value)) return value;
|
|
47
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hash(value: string): string {
|
|
51
|
+
return createHash("sha256").update(value).digest("hex");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function mutationProvenanceIdentity(value: Pick<MutationProvenanceRecord,
|
|
55
|
+
"taskRunId" | "toolCallIdentityHash" | "recordedAt" | "afterTreeDigest" | "changes">): string {
|
|
56
|
+
const identity = {
|
|
57
|
+
taskRunId: value.taskRunId,
|
|
58
|
+
toolCallIdentityHash: value.toolCallIdentityHash,
|
|
59
|
+
recordedAt: value.recordedAt,
|
|
60
|
+
afterTreeDigest: value.afterTreeDigest,
|
|
61
|
+
changes: value.changes
|
|
62
|
+
};
|
|
63
|
+
return hash(`mutation-provenance-identity-v1\0${JSON.stringify(canonical(identity))}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function mutationProvenanceDigest(value: Omit<MutationProvenanceRecord, "integrityDigest"> | MutationProvenanceRecord): string {
|
|
67
|
+
const { integrityDigest: _ignored, ...payload } = value as MutationProvenanceRecord;
|
|
68
|
+
return `sha256:${hash(`mutation-provenance-record-v1\0${JSON.stringify(canonical(payload))}`)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function decodeMutationRepoPath(change: MutationProvenanceChange): string | null {
|
|
72
|
+
const value = Buffer.from(change.repoPathBase64, "base64url").toString("utf8");
|
|
73
|
+
return value && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value)
|
|
74
|
+
&& !value.split("/").includes("..") && !value.includes("\0") ? value : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function exactKeys(value: Record<string, unknown>, keys: string[]): boolean {
|
|
78
|
+
const actual = Object.keys(value).sort(), expected = [...keys].sort();
|
|
79
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validChange(value: unknown): value is MutationProvenanceChange {
|
|
83
|
+
if (!plain(value) || !exactKeys(value, ["pathDigest", "repoPathBase64", "beforeCarrierDigest", "afterCarrierDigest", "afterContentDigest", "proofMode", "effect"])) return false;
|
|
84
|
+
if (!DIGEST.test(String(value.pathDigest))) return false;
|
|
85
|
+
if (typeof value.repoPathBase64 !== "string" || !value.repoPathBase64 || value.repoPathBase64.length > 4096 || !/^[A-Za-z0-9_-]+$/.test(value.repoPathBase64)) return false;
|
|
86
|
+
if (value.beforeCarrierDigest !== null && !TREE.test(String(value.beforeCarrierDigest))) return false;
|
|
87
|
+
if (value.afterCarrierDigest !== null && !TREE.test(String(value.afterCarrierDigest))) return false;
|
|
88
|
+
if (value.afterContentDigest !== null && !DIGEST.test(String(value.afterContentDigest))) return false;
|
|
89
|
+
if (value.beforeCarrierDigest === value.afterCarrierDigest) return false;
|
|
90
|
+
if (value.proofMode !== null && !["full-content", "exact-replacement"].includes(String(value.proofMode))) return false;
|
|
91
|
+
if (!["exact-content", "content-preserved", "content-changed", "unknown"].includes(String(value.effect))) return false;
|
|
92
|
+
const repoPath = decodeMutationRepoPath(value as MutationProvenanceChange);
|
|
93
|
+
return Boolean(repoPath && value.pathDigest === `sha256:${hash(repoPath as string)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function mutationProvenanceErrors(value: unknown): string[] {
|
|
97
|
+
const keys = ["schemaVersion", "version", "recordId", "evidenceRef", "taskId", "taskRunId", "sessionIdentityHash", "toolCallIdentityHash", "toolName", "recordedAt", "evidenceMode", "beforeTreeDigest", "afterTreeDigest", "changes", "integrityDigest"];
|
|
98
|
+
if (!plain(value) || !exactKeys(value, keys)) return ["record shape is invalid"];
|
|
99
|
+
const errors: string[] = [];
|
|
100
|
+
if (value.schemaVersion !== 1 || value.version !== MUTATION_PROVENANCE_VERSION) errors.push("record version is invalid");
|
|
101
|
+
if (!PUBLIC_ID.test(String(value.taskId)) || !PUBLIC_ID.test(String(value.taskRunId))) errors.push("task identity is invalid");
|
|
102
|
+
if (!DIGEST.test(String(value.sessionIdentityHash)) || !DIGEST.test(String(value.toolCallIdentityHash))) errors.push("private identity hash is invalid");
|
|
103
|
+
if (!["edit", "write", "apply_patch", "shell", "opaque"].includes(String(value.toolName))
|
|
104
|
+
|| !["exact-runtime", "observed-runtime"].includes(String(value.evidenceMode))) errors.push("tool evidence is invalid");
|
|
105
|
+
if (!TIMESTAMP.test(String(value.recordedAt)) || !Number.isFinite(Date.parse(String(value.recordedAt)))) errors.push("recorded timestamp is invalid");
|
|
106
|
+
if (!TREE.test(String(value.beforeTreeDigest)) || !TREE.test(String(value.afterTreeDigest)) || value.beforeTreeDigest === value.afterTreeDigest) errors.push("tree evidence is invalid");
|
|
107
|
+
if (!Array.isArray(value.changes) || value.changes.length < 1 || value.changes.length > 100 || !value.changes.every(validChange)) errors.push("file changes are invalid");
|
|
108
|
+
else if (new Set(value.changes.map((change) => (change as MutationProvenanceChange).pathDigest)).size !== value.changes.length) errors.push("file changes are duplicated");
|
|
109
|
+
else if (value.evidenceMode === "exact-runtime" && (
|
|
110
|
+
!["edit", "write", "apply_patch"].includes(String(value.toolName))
|
|
111
|
+
|| value.changes.some((change) => {
|
|
112
|
+
const item = change as MutationProvenanceChange;
|
|
113
|
+
return item.afterCarrierDigest === null || item.afterContentDigest === null || item.proofMode === null || item.effect !== "exact-content";
|
|
114
|
+
})
|
|
115
|
+
)) errors.push("exact mutation proof is invalid");
|
|
116
|
+
else if (value.evidenceMode === "observed-runtime" && value.changes.some((change) => {
|
|
117
|
+
const item = change as MutationProvenanceChange;
|
|
118
|
+
return item.proofMode !== null || item.effect === "exact-content"
|
|
119
|
+
|| (item.effect === "content-preserved" && item.afterContentDigest === null);
|
|
120
|
+
})) errors.push("observed mutation proof is invalid");
|
|
121
|
+
const identity = mutationProvenanceIdentity(value as MutationProvenanceRecord);
|
|
122
|
+
if (value.recordId !== `provenance.${identity}` || value.evidenceRef !== `mutation.${identity}`) errors.push("record identity is invalid");
|
|
123
|
+
if (!DIGEST.test(String(value.integrityDigest)) || value.integrityDigest !== mutationProvenanceDigest(value as MutationProvenanceRecord)) errors.push("record integrity is invalid");
|
|
124
|
+
return errors;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function validateMutationProvenanceRecord(value: unknown): MutationProvenanceRecord {
|
|
128
|
+
const errors = mutationProvenanceErrors(value);
|
|
129
|
+
if (errors.length) throw new Error(`Invalid mutation provenance record: ${errors.join("; ")}`);
|
|
130
|
+
return structuredClone(value) as MutationProvenanceRecord;
|
|
131
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { currentFileContentDigests } from "../quality/model-mutation-proof.ts";
|
|
2
|
+
import type { ModelMutationEvidenceCompletion, ModelMutationIdentity } from "../session/model-authorship-state.ts";
|
|
3
|
+
import { appendMutationProvenance, type AppendMutationProvenanceOptions } from "./mutation-provenance-store.ts";
|
|
4
|
+
|
|
5
|
+
type RecordMutationResultOptions = {
|
|
6
|
+
projectRoot: string;
|
|
7
|
+
identity: ModelMutationIdentity;
|
|
8
|
+
toolCallId: string;
|
|
9
|
+
toolName: string;
|
|
10
|
+
recordedAt: string;
|
|
11
|
+
successful: boolean;
|
|
12
|
+
currentSnapshot: Record<string, string>;
|
|
13
|
+
completion: ModelMutationEvidenceCompletion;
|
|
14
|
+
shellSnapshotBefore?: Record<string, string>;
|
|
15
|
+
shellChangedPaths: string[];
|
|
16
|
+
protectedPaths: string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function toolName(value: string): AppendMutationProvenanceOptions["toolName"] {
|
|
20
|
+
const normalized = value.toLowerCase();
|
|
21
|
+
if (normalized === "edit" || normalized === "write" || normalized === "apply_patch") return normalized;
|
|
22
|
+
if (["bash", "shell", "exec"].includes(normalized)) return "shell";
|
|
23
|
+
return "opaque";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function recordMutationResult(options: RecordMutationResultOptions) {
|
|
27
|
+
if (!options.successful) return undefined;
|
|
28
|
+
const changedPaths = [...new Set([...options.completion.changedPaths, ...options.shellChangedPaths])].sort();
|
|
29
|
+
const beforeSnapshot = options.completion.beforeSnapshot ?? options.shellSnapshotBefore;
|
|
30
|
+
if (!beforeSnapshot || changedPaths.length === 0) return undefined;
|
|
31
|
+
const contentDigests = currentFileContentDigests(options.projectRoot, changedPaths);
|
|
32
|
+
return appendMutationProvenance({
|
|
33
|
+
projectRoot: options.projectRoot,
|
|
34
|
+
taskId: options.identity.taskId,
|
|
35
|
+
taskRunId: options.identity.taskRunId,
|
|
36
|
+
sessionId: options.identity.sessionId,
|
|
37
|
+
toolCallId: options.toolCallId,
|
|
38
|
+
toolName: toolName(options.toolName),
|
|
39
|
+
recordedAt: options.recordedAt,
|
|
40
|
+
beforeSnapshot,
|
|
41
|
+
afterSnapshot: options.currentSnapshot,
|
|
42
|
+
changedPaths,
|
|
43
|
+
recordedDigests: options.completion.recordedDigests,
|
|
44
|
+
recordedContentDigests: { ...contentDigests, ...options.completion.recordedContentDigests },
|
|
45
|
+
proofModes: options.completion.proofModes,
|
|
46
|
+
protectedPaths: options.protectedPaths
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { ensurePrivateStateDirectory, resolveLocalStatePath } from "../../extensions/local-state-path.js";
|
|
6
|
+
import { workingTreeEvidenceDigest, workingTreeSnapshotUsesCurrentAlgorithm } from "../../extensions/working-tree-digest.js";
|
|
7
|
+
import { matchesProtectedPath } from "../../extensions/policy-core.js";
|
|
8
|
+
import {
|
|
9
|
+
MUTATION_PROVENANCE_VERSION,
|
|
10
|
+
mutationProvenanceDigest,
|
|
11
|
+
mutationProvenanceIdentity,
|
|
12
|
+
validateMutationProvenanceRecord,
|
|
13
|
+
type MutationProvenanceRecord
|
|
14
|
+
} from "./mutation-provenance-contract.ts";
|
|
15
|
+
import { readTaskBaselineManifest, taskBaselineRetentionState } from "./source-evidence-store.ts";
|
|
16
|
+
|
|
17
|
+
const MAX_RECORDS = 500;
|
|
18
|
+
const MAX_RECORD_BYTES = 128 * 1024;
|
|
19
|
+
|
|
20
|
+
export type AppendMutationProvenanceOptions = {
|
|
21
|
+
projectRoot: string;
|
|
22
|
+
taskId: string;
|
|
23
|
+
taskRunId: string;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
toolCallId: string;
|
|
26
|
+
toolName: "edit" | "write" | "apply_patch" | "shell" | "opaque";
|
|
27
|
+
recordedAt: string;
|
|
28
|
+
beforeSnapshot: Record<string, string>;
|
|
29
|
+
afterSnapshot: Record<string, string>;
|
|
30
|
+
recordedDigests: Record<string, string>;
|
|
31
|
+
recordedContentDigests: Record<string, string>;
|
|
32
|
+
proofModes: Record<string, "full-content" | "exact-replacement">;
|
|
33
|
+
changedPaths: string[];
|
|
34
|
+
protectedPaths?: string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function hash(value: string | Buffer): string {
|
|
38
|
+
return createHash("sha256").update(value).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function evidenceDirectory(projectRoot: string, taskRunId: string): string {
|
|
42
|
+
return path.join(projectRoot, ".pi", "piagent-state", "source-evidence", `run-${hash(taskRunId)}`, "mutations");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readPrivate(projectRoot: string, file: string): Buffer {
|
|
46
|
+
const safe = resolveLocalStatePath(projectRoot, file, { label: "Mutation provenance", kind: "file" });
|
|
47
|
+
const descriptor = fs.openSync(safe, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
48
|
+
try {
|
|
49
|
+
const stat = fs.fstatSync(descriptor);
|
|
50
|
+
if (!stat.isFile() || stat.size > MAX_RECORD_BYTES) throw new Error("Mutation provenance record is oversized or not regular");
|
|
51
|
+
return fs.readFileSync(descriptor);
|
|
52
|
+
} finally { fs.closeSync(descriptor); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function publishPrivate(projectRoot: string, target: string, content: Buffer): void {
|
|
56
|
+
const parent = ensurePrivateStateDirectory(projectRoot, path.dirname(target), "Mutation provenance directory");
|
|
57
|
+
const safeTarget = resolveLocalStatePath(projectRoot, target, { label: "Mutation provenance" });
|
|
58
|
+
const temporary = path.join(parent, `${path.basename(target)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
59
|
+
const descriptor = fs.openSync(temporary, "wx", 0o600);
|
|
60
|
+
try { fs.writeFileSync(descriptor, content); fs.fsyncSync(descriptor); fs.fchmodSync(descriptor, 0o600); }
|
|
61
|
+
finally { fs.closeSync(descriptor); }
|
|
62
|
+
try { fs.linkSync(temporary, safeTarget); }
|
|
63
|
+
catch (error) {
|
|
64
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
65
|
+
if (!readPrivate(projectRoot, safeTarget).equals(content)) throw new Error("Mutation provenance identity collision");
|
|
66
|
+
} finally { fs.rmSync(temporary, { force: true }); }
|
|
67
|
+
try { fs.chmodSync(parent, 0o700); fs.chmodSync(safeTarget, 0o600); } catch {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function safePath(value: string): boolean {
|
|
71
|
+
return Boolean(value && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value)
|
|
72
|
+
&& !value.split("/").includes("..") && !value.includes("\0") && value.length <= 4096);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function appendMutationProvenance(options: AppendMutationProvenanceOptions): MutationProvenanceRecord | undefined {
|
|
76
|
+
const manifest = readTaskBaselineManifest(options.projectRoot, options.taskRunId);
|
|
77
|
+
if (!manifest || manifest.taskId !== options.taskId || manifest.captureState !== "current"
|
|
78
|
+
|| taskBaselineRetentionState(manifest, new Date(options.recordedAt)) !== "active") return undefined;
|
|
79
|
+
if (!workingTreeSnapshotUsesCurrentAlgorithm(options.beforeSnapshot) || !workingTreeSnapshotUsesCurrentAlgorithm(options.afterSnapshot)) return undefined;
|
|
80
|
+
const paths = [...new Set(options.changedPaths)].sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
81
|
+
if (paths.length < 1 || paths.length > 100) return undefined;
|
|
82
|
+
const evidenceMode = paths.every((repoPath) => options.recordedDigests[repoPath] && options.proofModes[repoPath])
|
|
83
|
+
? "exact-runtime" as const : "observed-runtime" as const;
|
|
84
|
+
const previous = evidenceMode === "observed-runtime" ? readMutationProvenance(options.projectRoot, options.taskRunId).records : [];
|
|
85
|
+
const changes = [];
|
|
86
|
+
for (const repoPath of paths) {
|
|
87
|
+
const before = options.beforeSnapshot[repoPath] ?? null;
|
|
88
|
+
const after = options.afterSnapshot[repoPath];
|
|
89
|
+
const content = options.recordedContentDigests[repoPath] ?? null;
|
|
90
|
+
const proofMode = evidenceMode === "exact-runtime" ? options.proofModes[repoPath] : null;
|
|
91
|
+
if (!safePath(repoPath) || matchesProtectedPath(repoPath, options.protectedPaths ?? [])
|
|
92
|
+
|| before === (after ?? null) || (content !== null && !/^[a-f0-9]{64}$/.test(content))) return undefined;
|
|
93
|
+
if (evidenceMode === "exact-runtime" && (!after || after !== options.recordedDigests[repoPath] || !content || !proofMode)) return undefined;
|
|
94
|
+
const priorContent = [...previous].reverse().flatMap((record) => record.changes)
|
|
95
|
+
.find((change) => Buffer.from(change.repoPathBase64, "base64url").toString("utf8") === repoPath)?.afterContentDigest ?? null;
|
|
96
|
+
const effect = evidenceMode === "exact-runtime" ? "exact-content"
|
|
97
|
+
: content && priorContent === `sha256:${content}` ? "content-preserved"
|
|
98
|
+
: content ? "content-changed" : "unknown";
|
|
99
|
+
changes.push({
|
|
100
|
+
pathDigest: `sha256:${hash(repoPath)}`,
|
|
101
|
+
repoPathBase64: Buffer.from(repoPath, "utf8").toString("base64url"),
|
|
102
|
+
beforeCarrierDigest: before,
|
|
103
|
+
afterCarrierDigest: after ?? null,
|
|
104
|
+
afterContentDigest: content ? `sha256:${content}` : null,
|
|
105
|
+
proofMode,
|
|
106
|
+
effect
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const beforeTreeDigest = workingTreeEvidenceDigest(options.beforeSnapshot);
|
|
110
|
+
const afterTreeDigest = workingTreeEvidenceDigest(options.afterSnapshot);
|
|
111
|
+
if (beforeTreeDigest === afterTreeDigest) return undefined;
|
|
112
|
+
const core = {
|
|
113
|
+
taskRunId: options.taskRunId,
|
|
114
|
+
toolCallIdentityHash: `sha256:${hash(`tool-call\0${options.toolCallId}`)}`,
|
|
115
|
+
recordedAt: options.recordedAt,
|
|
116
|
+
afterTreeDigest,
|
|
117
|
+
changes
|
|
118
|
+
};
|
|
119
|
+
const identity = mutationProvenanceIdentity(core);
|
|
120
|
+
const payload: Omit<MutationProvenanceRecord, "integrityDigest"> = {
|
|
121
|
+
schemaVersion: 1, version: MUTATION_PROVENANCE_VERSION,
|
|
122
|
+
recordId: `provenance.${identity}`, evidenceRef: `mutation.${identity}`,
|
|
123
|
+
taskId: options.taskId, taskRunId: options.taskRunId,
|
|
124
|
+
sessionIdentityHash: `sha256:${hash(`session\0${options.sessionId}`)}`,
|
|
125
|
+
toolCallIdentityHash: core.toolCallIdentityHash,
|
|
126
|
+
toolName: options.toolName, recordedAt: options.recordedAt, evidenceMode,
|
|
127
|
+
beforeTreeDigest, afterTreeDigest, changes
|
|
128
|
+
};
|
|
129
|
+
const record = validateMutationProvenanceRecord({ ...payload, integrityDigest: mutationProvenanceDigest(payload) });
|
|
130
|
+
const directory = evidenceDirectory(options.projectRoot, options.taskRunId);
|
|
131
|
+
let existing = 0;
|
|
132
|
+
try { existing = fs.readdirSync(resolveLocalStatePath(options.projectRoot, directory, { label: "Mutation provenance", kind: "directory" })).filter((file) => file.endsWith(".json")).length; }
|
|
133
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
|
134
|
+
const target = path.join(directory, `${record.recordId}.json`);
|
|
135
|
+
if (existing >= MAX_RECORDS && !fs.existsSync(target)) throw new Error("Mutation provenance record quota is exhausted");
|
|
136
|
+
publishPrivate(options.projectRoot, target, Buffer.from(`${JSON.stringify(record, null, 2)}\n`));
|
|
137
|
+
return record;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function readMutationProvenance(
|
|
141
|
+
projectRoot: string,
|
|
142
|
+
taskRunId: string
|
|
143
|
+
): { records: MutationProvenanceRecord[]; corruptions: string[] } {
|
|
144
|
+
const directory = evidenceDirectory(projectRoot, taskRunId);
|
|
145
|
+
let files: string[];
|
|
146
|
+
try { files = fs.readdirSync(resolveLocalStatePath(projectRoot, directory, { label: "Mutation provenance", kind: "directory" })).filter((file) => file.endsWith(".json")).sort(); }
|
|
147
|
+
catch (error) {
|
|
148
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { records: [], corruptions: [] };
|
|
149
|
+
return { records: [], corruptions: ["provenance-directory-unavailable"] };
|
|
150
|
+
}
|
|
151
|
+
if (files.length > MAX_RECORDS) return { records: [], corruptions: ["provenance-record-limit"] };
|
|
152
|
+
const records: MutationProvenanceRecord[] = [], corruptions: string[] = [];
|
|
153
|
+
for (const file of files) {
|
|
154
|
+
try {
|
|
155
|
+
const record = validateMutationProvenanceRecord(JSON.parse(readPrivate(projectRoot, path.join(directory, file)).toString("utf8")));
|
|
156
|
+
if (file !== `${record.recordId}.json` || record.taskRunId !== taskRunId) throw new Error("identity mismatch");
|
|
157
|
+
records.push(record);
|
|
158
|
+
} catch { corruptions.push(`record.${hash(file)}`); }
|
|
159
|
+
}
|
|
160
|
+
return { records: corruptions.length ? [] : records.sort((left, right) => left.recordedAt.localeCompare(right.recordedAt)), corruptions };
|
|
161
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { readProjectProfileDocument } from "../../capabilities/project-profile.js";
|
|
5
|
+
import { effectiveProtectedPaths } from "../../extensions/context-index-policy.js";
|
|
6
|
+
import { collectServers, unreadableLayers } from "../../mcp/mcp-config-layers.js";
|
|
7
|
+
|
|
8
|
+
function readRequiredJsonObject(file: string): Record<string, unknown> {
|
|
9
|
+
const stat = fs.lstatSync(file);
|
|
10
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_048_576) throw new Error("inspection-policy-unavailable");
|
|
11
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("inspection-policy-unavailable");
|
|
13
|
+
return value as Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function runtimeProtectedPaths(packageRoot: string, cwd: string): string[] {
|
|
17
|
+
const policy = readRequiredJsonObject(path.join(packageRoot, "packages", "piagent-core", "policies", "base-policy.json"));
|
|
18
|
+
const profileFile = path.join(cwd, ".pi", "piagent-profile.json");
|
|
19
|
+
let profile: Record<string, unknown> = {};
|
|
20
|
+
try {
|
|
21
|
+
const stat = fs.lstatSync(profileFile);
|
|
22
|
+
if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= 1_048_576) {
|
|
23
|
+
profile = readProjectProfileDocument(packageRoot, profileFile).profile ?? {};
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// Missing or unsafe project profiles never remove the platform policy.
|
|
27
|
+
}
|
|
28
|
+
return effectiveProtectedPaths(policy, profile).readProtectedPaths;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function runtimeConnectionDefinitions(cwd: string): {
|
|
32
|
+
servers: ReturnType<typeof collectServers>;
|
|
33
|
+
unreadableLayerCount: number;
|
|
34
|
+
} {
|
|
35
|
+
return { servers: collectServers({ projectPath: cwd }), unreadableLayerCount: unreadableLayers({ projectPath: cwd }).length };
|
|
36
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const REVIEW_EVIDENCE_VERSION = "piagent-webui-review-evidence-v1";
|
|
4
|
+
|
|
5
|
+
export type ReviewView = "task" | "working-tree" | "staged";
|
|
6
|
+
export type ReviewTarget = {
|
|
7
|
+
view: ReviewView;
|
|
8
|
+
fileRef: string;
|
|
9
|
+
diffRef: string;
|
|
10
|
+
taskRevision: string;
|
|
11
|
+
workspaceRevision: string;
|
|
12
|
+
indexRevision: string | null;
|
|
13
|
+
viewRevision: string;
|
|
14
|
+
fileRevision: string;
|
|
15
|
+
baseDigest: string | null;
|
|
16
|
+
currentDigest: string | null;
|
|
17
|
+
patchPreimage: string;
|
|
18
|
+
contentDigest: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ReviewEvidenceRecord = {
|
|
22
|
+
schemaVersion: 1;
|
|
23
|
+
version: typeof REVIEW_EVIDENCE_VERSION;
|
|
24
|
+
recordId: string;
|
|
25
|
+
evidenceRef: string;
|
|
26
|
+
sequence: number;
|
|
27
|
+
taskId: string;
|
|
28
|
+
taskRunId: string;
|
|
29
|
+
projectRef: string;
|
|
30
|
+
runtimeInstanceId: string;
|
|
31
|
+
sessionRef: string;
|
|
32
|
+
commandId: string;
|
|
33
|
+
idempotencyKeyDigest: string;
|
|
34
|
+
actionDigest: string;
|
|
35
|
+
reviewState: "reviewed" | "unreviewed";
|
|
36
|
+
target: ReviewTarget;
|
|
37
|
+
requestedAt: string;
|
|
38
|
+
recordedAt: string;
|
|
39
|
+
retentionUntil: string;
|
|
40
|
+
observedRevisions: Record<string, string | null>;
|
|
41
|
+
integrityDigest: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const REF = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
|
|
45
|
+
const PUBLIC_REF = /^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,159}$/;
|
|
46
|
+
const REVISION = /^[A-Za-z0-9][A-Za-z0-9._:~-]{0,159}$/;
|
|
47
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
48
|
+
const REVISION_KEYS = ["runtimeRevision", "taskRevision", "controlRevision", "workspaceRevision", "indexRevision",
|
|
49
|
+
"approvalRevision", "sessionOptionRevision", "queueRevision"];
|
|
50
|
+
|
|
51
|
+
export function canonicalReviewValue(value: unknown): string {
|
|
52
|
+
if (Array.isArray(value)) return `[${value.map(canonicalReviewValue).join(",")}]`;
|
|
53
|
+
if (value && typeof value === "object") return `{${Object.entries(value as Record<string, unknown>)
|
|
54
|
+
.sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalReviewValue(item)}`).join(",")}}`;
|
|
55
|
+
return JSON.stringify(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function reviewDigest(value: unknown): string {
|
|
59
|
+
return `sha256:${createHash("sha256").update(canonicalReviewValue(value)).digest("hex")}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function reviewRecordDigest(record: Omit<ReviewEvidenceRecord, "integrityDigest">): string {
|
|
63
|
+
return reviewDigest(record);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function timestamp(value: unknown): value is string {
|
|
67
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return false;
|
|
68
|
+
const parsed = Date.parse(value); return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function validateReviewTarget(value: unknown): ReviewTarget {
|
|
72
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Review target is invalid");
|
|
73
|
+
const target = value as ReviewTarget;
|
|
74
|
+
const keys = ["view", "fileRef", "diffRef", "taskRevision", "workspaceRevision", "indexRevision", "viewRevision", "fileRevision",
|
|
75
|
+
"baseDigest", "currentDigest", "patchPreimage", "contentDigest"];
|
|
76
|
+
if (canonicalReviewValue(Object.keys(target).sort()) !== canonicalReviewValue(keys.sort())
|
|
77
|
+
|| !["task", "working-tree", "staged"].includes(target.view) || !REF.test(target.fileRef) || !REF.test(target.diffRef)
|
|
78
|
+
|| ![target.taskRevision, target.workspaceRevision, target.viewRevision, target.fileRevision].every((item) => REVISION.test(item))
|
|
79
|
+
|| target.indexRevision !== null && !REVISION.test(target.indexRevision)
|
|
80
|
+
|| ![target.baseDigest, target.currentDigest].every((item) => item === null || DIGEST.test(item))
|
|
81
|
+
|| !DIGEST.test(target.patchPreimage) || target.contentDigest !== target.patchPreimage) throw new Error("Review target is invalid");
|
|
82
|
+
return structuredClone(target);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function validateReviewEvidenceRecord(value: unknown): ReviewEvidenceRecord {
|
|
86
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Review evidence is invalid");
|
|
87
|
+
const record = value as ReviewEvidenceRecord;
|
|
88
|
+
const keys = ["schemaVersion", "version", "recordId", "evidenceRef", "sequence", "taskId", "taskRunId", "projectRef", "runtimeInstanceId", "sessionRef", "commandId",
|
|
89
|
+
"idempotencyKeyDigest", "actionDigest", "reviewState", "target", "requestedAt", "recordedAt", "retentionUntil", "observedRevisions", "integrityDigest"];
|
|
90
|
+
if (canonicalReviewValue(Object.keys(record).sort()) !== canonicalReviewValue(keys.sort()) || record.schemaVersion !== 1
|
|
91
|
+
|| record.version !== REVIEW_EVIDENCE_VERSION || ![record.recordId, record.evidenceRef, record.projectRef, record.runtimeInstanceId, record.sessionRef, record.commandId].every((item) => REF.test(item))
|
|
92
|
+
|| ![record.taskId, record.taskRunId].every((item) => PUBLIC_REF.test(item))
|
|
93
|
+
|| !Number.isInteger(record.sequence) || record.sequence < 1 || record.sequence > 2_000
|
|
94
|
+
|| ![record.idempotencyKeyDigest, record.actionDigest, record.integrityDigest].every((item) => DIGEST.test(item))
|
|
95
|
+
|| !["reviewed", "unreviewed"].includes(record.reviewState) || ![record.requestedAt, record.recordedAt, record.retentionUntil].every(timestamp)
|
|
96
|
+
|| Date.parse(record.requestedAt) > Date.parse(record.recordedAt) || Date.parse(record.recordedAt) >= Date.parse(record.retentionUntil)) {
|
|
97
|
+
throw new Error("Review evidence is invalid");
|
|
98
|
+
}
|
|
99
|
+
validateReviewTarget(record.target);
|
|
100
|
+
const revisions = record.observedRevisions;
|
|
101
|
+
if (!revisions || typeof revisions !== "object" || Array.isArray(revisions)
|
|
102
|
+
|| canonicalReviewValue(Object.keys(revisions).sort()) !== canonicalReviewValue([...REVISION_KEYS].sort())
|
|
103
|
+
|| !REVISION.test(String(revisions.runtimeRevision ?? ""))
|
|
104
|
+
|| REVISION_KEYS.slice(1).some((key) => revisions[key] !== null && !REVISION.test(String(revisions[key])))) throw new Error("Review revisions are invalid");
|
|
105
|
+
const { integrityDigest, ...payload } = record;
|
|
106
|
+
if (reviewRecordDigest(payload) !== integrityDigest) throw new Error("Review evidence integrity mismatch");
|
|
107
|
+
return structuredClone(record);
|
|
108
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { DiffDocument } from "./diff-projection.ts";
|
|
2
|
+
import { canonicalReviewValue, reviewDigest, type ReviewEvidenceRecord, type ReviewTarget, type ReviewView } from "./review-state-contract.ts";
|
|
3
|
+
import type { WebUiIdentity } from "./source-change-projection.ts";
|
|
4
|
+
|
|
5
|
+
export type ReviewStateProjection = {
|
|
6
|
+
schemaVersion: 1;
|
|
7
|
+
version: "piagent-webui-review-state-v1";
|
|
8
|
+
generatedAt: string;
|
|
9
|
+
identity: WebUiIdentity;
|
|
10
|
+
state: "reviewed" | "unreviewed" | "stale" | "unavailable";
|
|
11
|
+
target: ReviewTarget | null;
|
|
12
|
+
recordedState: "reviewed" | "unreviewed" | null;
|
|
13
|
+
recordedAt: string | null;
|
|
14
|
+
evidenceRef: string | null;
|
|
15
|
+
reasonCode: string | null;
|
|
16
|
+
health: { state: "ok" | "degraded" | "unavailable" | "error"; reasonCode: string | null; message: string | null };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type DeriveReviewTargetOptions = {
|
|
20
|
+
diff: DiffDocument;
|
|
21
|
+
taskId: string;
|
|
22
|
+
taskRunId: string;
|
|
23
|
+
taskRevision: string;
|
|
24
|
+
workspaceRevision: string;
|
|
25
|
+
indexRevision: string | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function targetCore(options: DeriveReviewTargetOptions): Omit<ReviewTarget, "diffRef" | "patchPreimage" | "contentDigest"> & { basisRef: string } {
|
|
29
|
+
const diff = options.diff as Record<string, any>, file = diff.file as Record<string, any>, observed = diff.observed as Record<string, any>;
|
|
30
|
+
return {
|
|
31
|
+
view: diff.view as ReviewView, fileRef: String(file.fileRef), taskRevision: options.taskRevision,
|
|
32
|
+
workspaceRevision: options.workspaceRevision, indexRevision: options.indexRevision,
|
|
33
|
+
viewRevision: String(observed.viewRevision), fileRevision: String(observed.fileRevision),
|
|
34
|
+
baseDigest: observed.baseDigest ?? null, currentDigest: observed.currentDigest ?? null,
|
|
35
|
+
basisRef: String(diff.basis?.basisRef ?? file.basisRef)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function deriveReviewTarget(options: DeriveReviewTargetOptions): ReviewTarget | null {
|
|
40
|
+
const diff = options.diff as Record<string, any>;
|
|
41
|
+
if (!options.taskId || !options.taskRunId || !options.taskRevision || !options.workspaceRevision
|
|
42
|
+
|| diff.availability?.state !== "current" || diff.fallback?.kind !== "none"
|
|
43
|
+
|| diff.truncation?.truncated !== false || diff.redaction?.applied !== false || diff.redaction?.truncated !== false
|
|
44
|
+
|| diff.file?.status === "C" || diff.file?.content?.kind !== "text" || diff.file?.content?.access !== "available") return null;
|
|
45
|
+
const core = targetCore(options), preimage = reviewDigest({ schemaVersion: 1, taskId: options.taskId, taskRunId: options.taskRunId, ...core });
|
|
46
|
+
return { view: core.view, fileRef: core.fileRef, diffRef: `diff.${preimage.slice("sha256:".length)}`, taskRevision: core.taskRevision,
|
|
47
|
+
workspaceRevision: core.workspaceRevision, indexRevision: core.indexRevision, viewRevision: core.viewRevision,
|
|
48
|
+
fileRevision: core.fileRevision, baseDigest: core.baseDigest, currentDigest: core.currentDigest,
|
|
49
|
+
patchPreimage: preimage, contentDigest: preimage };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sameTarget(left: ReviewTarget, right: ReviewTarget): boolean {
|
|
53
|
+
return canonicalReviewValue(left) === canonicalReviewValue(right);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function projectReviewState(options: {
|
|
57
|
+
identity: WebUiIdentity;
|
|
58
|
+
target: ReviewTarget | null;
|
|
59
|
+
records: ReviewEvidenceRecord[];
|
|
60
|
+
corruptions?: string[];
|
|
61
|
+
generatedAt?: string;
|
|
62
|
+
unavailableReason?: string;
|
|
63
|
+
}): ReviewStateProjection {
|
|
64
|
+
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
65
|
+
const base = { schemaVersion: 1 as const, version: "piagent-webui-review-state-v1" as const, generatedAt,
|
|
66
|
+
identity: structuredClone(options.identity) };
|
|
67
|
+
if (options.corruptions?.length) return { ...base, state: "unavailable", target: null, recordedState: null, recordedAt: null,
|
|
68
|
+
evidenceRef: null, reasonCode: "review-evidence-corrupt", health: { state: "error", reasonCode: "review-evidence-corrupt", message: "Review evidence is corrupt" } };
|
|
69
|
+
if (!options.target) {
|
|
70
|
+
const reasonCode = options.unavailableReason ?? "review-target-unavailable";
|
|
71
|
+
return { ...base, state: "unavailable", target: null, recordedState: null, recordedAt: null, evidenceRef: null, reasonCode,
|
|
72
|
+
health: { state: "unavailable", reasonCode, message: "The exact review target is unavailable" } };
|
|
73
|
+
}
|
|
74
|
+
const relevant = options.records.filter((record) => record.taskId === options.identity.taskId && record.taskRunId === options.identity.taskRunId
|
|
75
|
+
&& record.target.view === options.target!.view && record.target.fileRef === options.target!.fileRef);
|
|
76
|
+
const latest = relevant.at(-1);
|
|
77
|
+
if (!latest) return { ...base, state: "unreviewed", target: structuredClone(options.target), recordedState: null, recordedAt: null,
|
|
78
|
+
evidenceRef: null, reasonCode: null, health: { state: "ok", reasonCode: null, message: null } };
|
|
79
|
+
if (!sameTarget(latest.target, options.target)) {
|
|
80
|
+
const priorReviewed = [...relevant].reverse().find((record) => record.reviewState === "reviewed");
|
|
81
|
+
if (priorReviewed) return { ...base, state: "stale", target: structuredClone(options.target), recordedState: "reviewed",
|
|
82
|
+
recordedAt: priorReviewed.recordedAt, evidenceRef: priorReviewed.evidenceRef, reasonCode: "review-target-changed",
|
|
83
|
+
health: { state: "degraded", reasonCode: "review-target-changed", message: "The file or diff changed after it was reviewed" } };
|
|
84
|
+
return { ...base, state: "unreviewed", target: structuredClone(options.target), recordedState: null, recordedAt: null,
|
|
85
|
+
evidenceRef: null, reasonCode: null, health: { state: "ok", reasonCode: null, message: null } };
|
|
86
|
+
}
|
|
87
|
+
if (latest.reviewState === "unreviewed") return { ...base, state: "unreviewed", target: structuredClone(options.target),
|
|
88
|
+
recordedState: "unreviewed", recordedAt: latest.recordedAt, evidenceRef: latest.evidenceRef, reasonCode: null,
|
|
89
|
+
health: { state: "ok", reasonCode: null, message: null } };
|
|
90
|
+
return { ...base, state: "reviewed", target: structuredClone(options.target), recordedState: "reviewed",
|
|
91
|
+
recordedAt: latest.recordedAt, evidenceRef: latest.evidenceRef, reasonCode: null,
|
|
92
|
+
health: { state: "ok", reasonCode: null, message: null } };
|
|
93
|
+
}
|