@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,250 @@
|
|
|
1
|
+
// What a shell command can reach, and how to say so.
|
|
2
|
+
//
|
|
3
|
+
// These checks used to live inside the guard entrypoint, which imports the Pi
|
|
4
|
+
// host runtime and therefore cannot be loaded anywhere else. `piagent explain`
|
|
5
|
+
// needs the same answers the guard gives, and a second implementation of "does
|
|
6
|
+
// this command reach a protected path" is exactly the defect this project keeps
|
|
7
|
+
// closing: one segment, many readers, each with its own view. So the readers
|
|
8
|
+
// live here and both the guard and the CLI take this one.
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
extractShellGlobCandidates,
|
|
16
|
+
extractShellPathCandidates,
|
|
17
|
+
matchesProtectedPath,
|
|
18
|
+
normalizePathCandidate
|
|
19
|
+
} from "./policy-core.js";
|
|
20
|
+
|
|
21
|
+
export function normalizeRelative(cwd: string, candidate: unknown): string | undefined {
|
|
22
|
+
if (typeof candidate !== "string" || candidate.trim().length === 0) return undefined;
|
|
23
|
+
let raw = candidate.trim();
|
|
24
|
+
try {
|
|
25
|
+
raw = decodeURIComponent(raw);
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(raw)) {
|
|
30
|
+
if (!raw.toLowerCase().startsWith("file://")) return undefined;
|
|
31
|
+
try {
|
|
32
|
+
raw = fileURLToPath(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
38
|
+
return path.relative(cwd, absolute).split(path.sep).join("/");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveRepositoryPathCandidate(cwd: string, candidate: string): string | undefined {
|
|
42
|
+
const normalized = normalizePathCandidate(candidate);
|
|
43
|
+
if (normalized === ".." || normalized.startsWith("../")) return undefined;
|
|
44
|
+
|
|
45
|
+
const relative = path.posix.isAbsolute(normalized)
|
|
46
|
+
? path.relative(cwd, normalized).split(path.sep).join("/")
|
|
47
|
+
: normalized;
|
|
48
|
+
if (relative === ".." || relative.startsWith("../")) return undefined;
|
|
49
|
+
|
|
50
|
+
const pending = relative.split("/").filter((item) => item && item !== ".");
|
|
51
|
+
let current = cwd;
|
|
52
|
+
let resolvedDepth = 0;
|
|
53
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
54
|
+
const next = path.join(current, pending[index]);
|
|
55
|
+
try {
|
|
56
|
+
fs.lstatSync(next);
|
|
57
|
+
current = next;
|
|
58
|
+
resolvedDepth = index + 1;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const code = error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
61
|
+
if (code !== "ENOENT") return undefined;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let canonicalBase: string;
|
|
67
|
+
try {
|
|
68
|
+
canonicalBase = fs.realpathSync.native(current);
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const canonical = path.resolve(canonicalBase, ...pending.slice(resolvedDepth));
|
|
73
|
+
const canonicalRoot = fs.realpathSync.native(cwd);
|
|
74
|
+
const canonicalRelative = path.relative(canonicalRoot, canonical).split(path.sep).join("/");
|
|
75
|
+
if (canonicalRelative === ".." || canonicalRelative.startsWith("../") || path.isAbsolute(canonicalRelative)) return undefined;
|
|
76
|
+
return canonicalRelative || ".";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function findResolvedProtectedPathInCommand(
|
|
80
|
+
cwd: string,
|
|
81
|
+
command: string,
|
|
82
|
+
protectedPatterns: string[]
|
|
83
|
+
): { candidate: string; resolved: string; pattern: string } | undefined {
|
|
84
|
+
for (const candidate of extractShellPathCandidates(command)) {
|
|
85
|
+
const relative = normalizeRelative(cwd, candidate);
|
|
86
|
+
if (!relative) continue;
|
|
87
|
+
const resolved = resolveRepositoryPathCandidate(cwd, relative);
|
|
88
|
+
if (!resolved || resolved === relative) continue;
|
|
89
|
+
const pattern = matchesProtectedPath(resolved, protectedPatterns);
|
|
90
|
+
if (pattern) return { candidate, resolved, pattern };
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function expandSimpleGlobAlternatives(pattern: string, max = 24): { values: string[]; complete: boolean } {
|
|
96
|
+
let results = [pattern];
|
|
97
|
+
let changed = true;
|
|
98
|
+
let complete = true;
|
|
99
|
+
|
|
100
|
+
while (changed) {
|
|
101
|
+
changed = false;
|
|
102
|
+
const expanded: string[] = [];
|
|
103
|
+
for (const item of results) {
|
|
104
|
+
const match = item.match(/\{([^{}]+)\}/);
|
|
105
|
+
if (!match) {
|
|
106
|
+
expanded.push(item);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
changed = true;
|
|
110
|
+
const options = match[1].split(",").map((option) => option.trim());
|
|
111
|
+
for (const option of options) {
|
|
112
|
+
expanded.push(`${item.slice(0, match.index)}${option}${item.slice((match.index ?? 0) + match[0].length)}`);
|
|
113
|
+
if (expanded.length >= max) {
|
|
114
|
+
complete = false;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (expanded.length >= max) break;
|
|
119
|
+
}
|
|
120
|
+
results = expanded.slice(0, max);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { values: results, complete };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function shellGlobSegmentMatches(patternSegment: string, candidateSegment: string): boolean {
|
|
127
|
+
if (candidateSegment.startsWith(".") && !patternSegment.startsWith(".")) return false;
|
|
128
|
+
let source = "";
|
|
129
|
+
for (let index = 0; index < patternSegment.length; index += 1) {
|
|
130
|
+
const char = patternSegment[index];
|
|
131
|
+
if (char === "*") {
|
|
132
|
+
source += "[^/]*";
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === "?") {
|
|
136
|
+
source += "[^/]";
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (char === "[") {
|
|
140
|
+
const end = patternSegment.indexOf("]", index + 1);
|
|
141
|
+
const body = end > index + 1 ? patternSegment.slice(index + 1, end) : "";
|
|
142
|
+
if (body && /^[!^A-Za-z0-9_-]+$/.test(body)) {
|
|
143
|
+
const negated = body.startsWith("!") ? `^${body.slice(1)}` : body;
|
|
144
|
+
source += `[${negated}]`;
|
|
145
|
+
index = end;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
source += char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
150
|
+
}
|
|
151
|
+
return new RegExp(`^${source}$`, "i").test(candidateSegment);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function shellGlobMatchesPath(pattern: string, candidate: string): boolean {
|
|
155
|
+
const patternSegments = normalizePathCandidate(pattern).split("/").filter(Boolean);
|
|
156
|
+
const candidateSegments = normalizePathCandidate(candidate).split("/").filter(Boolean);
|
|
157
|
+
|
|
158
|
+
function match(patternIndex: number, candidateIndex: number): boolean {
|
|
159
|
+
if (patternIndex === patternSegments.length) return candidateIndex === candidateSegments.length;
|
|
160
|
+
const patternSegment = patternSegments[patternIndex];
|
|
161
|
+
if (patternSegment === "**") {
|
|
162
|
+
if (match(patternIndex + 1, candidateIndex)) return true;
|
|
163
|
+
for (let next = candidateIndex; next < candidateSegments.length; next += 1) {
|
|
164
|
+
if (match(patternIndex + 1, next + 1)) return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (candidateIndex >= candidateSegments.length) return false;
|
|
169
|
+
return shellGlobSegmentMatches(patternSegment, candidateSegments[candidateIndex])
|
|
170
|
+
&& match(patternIndex + 1, candidateIndex + 1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return patternSegments.length > 0 && candidateSegments.length > 0 && match(0, 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function protectedPatternExamples(pattern: string): string[] {
|
|
177
|
+
const normalized = normalizePathCandidate(pattern);
|
|
178
|
+
if (!normalized) return [];
|
|
179
|
+
|
|
180
|
+
const examples = new Set<string>();
|
|
181
|
+
const add = (value: string | undefined) => {
|
|
182
|
+
const normalizedValue = normalizePathCandidate(value ?? "");
|
|
183
|
+
if (normalizedValue) examples.add(normalizedValue);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
add(normalized);
|
|
187
|
+
|
|
188
|
+
if (normalized.endsWith("/**")) {
|
|
189
|
+
const base = normalized.slice(0, -3);
|
|
190
|
+
add(base);
|
|
191
|
+
add(`${base}/probe`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (normalized.startsWith("**/")) {
|
|
195
|
+
const tail = normalized.slice(3);
|
|
196
|
+
const concreteTail = tail
|
|
197
|
+
.replace(/\*\*/g, "nested")
|
|
198
|
+
.replace(/\*/g, tail.includes(".env.") ? "local" : "probe");
|
|
199
|
+
add(tail);
|
|
200
|
+
add(concreteTail);
|
|
201
|
+
add(`nested/${concreteTail}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const concrete = normalized
|
|
205
|
+
.replace(/^\*\*\//, "")
|
|
206
|
+
.replace(/\/\*\*$/, "/probe")
|
|
207
|
+
.replace(/\*\*/g, "nested")
|
|
208
|
+
.replace(/\*/g, normalized.includes(".env.") ? "local" : "probe");
|
|
209
|
+
add(concrete);
|
|
210
|
+
|
|
211
|
+
for (const example of [...examples]) {
|
|
212
|
+
const base = path.posix.basename(example);
|
|
213
|
+
if (base && base !== "probe") examples.add(base);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return [...examples];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function shellGlobTargetsProtectedPath(
|
|
220
|
+
command: string,
|
|
221
|
+
protectedPatterns: string[]
|
|
222
|
+
): { glob: string; pattern: string; example: string } | undefined {
|
|
223
|
+
for (const candidate of extractShellGlobCandidates(command)) {
|
|
224
|
+
if (!/[*?{\[]/.test(candidate)) continue;
|
|
225
|
+
const expanded = expandSimpleGlobAlternatives(candidate);
|
|
226
|
+
if (!expanded.complete) return { glob: candidate, pattern: "bounded glob expansion", example: "a protected path" };
|
|
227
|
+
for (const candidateGlob of expanded.values) {
|
|
228
|
+
for (const pattern of protectedPatterns) {
|
|
229
|
+
for (const example of protectedPatternExamples(pattern)) {
|
|
230
|
+
if (/[*?{\[\]]/.test(example)) continue;
|
|
231
|
+
if (
|
|
232
|
+
shellGlobMatchesPath(candidateGlob, example)
|
|
233
|
+
|| shellGlobMatchesPath(`**/${candidateGlob}`, example)
|
|
234
|
+
|| shellGlobMatchesPath(candidateGlob, path.posix.basename(example))
|
|
235
|
+
) {
|
|
236
|
+
return { glob: candidate, pattern, example };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function unresolvedExpansionReason(subject: string, words: string[]): string {
|
|
246
|
+
const listed = words.map((word) => `\`${word}\``).join(", ");
|
|
247
|
+
return `${subject} builds a filename this guard cannot resolve: ${listed}. `
|
|
248
|
+
+ "The literal text around the expansion makes it a path, but its value is only known at run time, "
|
|
249
|
+
+ "so it cannot be checked against the protected paths. Write the path out, or put the expansion in its own argument.";
|
|
250
|
+
}
|
|
@@ -282,6 +282,23 @@ export function appendTaskJournalEventAtMost(cwd, event, options = {}) {
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
export function transactTaskJournal(cwd, transaction) {
|
|
286
|
+
if (typeof transaction !== "function") throw new TypeError("Task journal transaction must be a function");
|
|
287
|
+
const safePaths = writableJournalPaths(cwd);
|
|
288
|
+
const release = acquireLock(safePaths);
|
|
289
|
+
try {
|
|
290
|
+
const journal = recoverIncompleteJournalTail(cwd, safePaths, readTaskJournal(cwd));
|
|
291
|
+
if (journal.corruptions.length > 0) throw new Error(`Task journal chain is corrupt: ${journal.corruptions[0]}`);
|
|
292
|
+
const decision = transaction({ events: [...journal.events], head: journal.head ? { ...journal.head } : undefined });
|
|
293
|
+
if (!decision || typeof decision !== "object" || Array.isArray(decision)) throw new Error("Task journal transaction returned an invalid decision");
|
|
294
|
+
if (!decision.event) return { appended: false, record: undefined, result: decision.result };
|
|
295
|
+
const record = appendJournalRecord(safePaths, journal, decision.event, decision.recordedAt);
|
|
296
|
+
return { appended: true, record, result: decision.result };
|
|
297
|
+
} finally {
|
|
298
|
+
release();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
285
302
|
export function readTaskJournal(cwd, options = {}) {
|
|
286
303
|
const paths = taskJournalPaths(cwd);
|
|
287
304
|
let safePath;
|
|
@@ -290,7 +307,17 @@ export function readTaskJournal(cwd, options = {}) {
|
|
|
290
307
|
} catch {
|
|
291
308
|
return { events: [], corruptions: ["journal path is unsafe"], head: undefined };
|
|
292
309
|
}
|
|
293
|
-
if (!fs.existsSync(safePath)) return { events: [], corruptions: [], head: undefined };
|
|
310
|
+
if (!fs.existsSync(safePath)) return { events: [], corruptions: [], head: undefined, inputTruncated: false, omittedEvents: 0 };
|
|
311
|
+
if (Number.isInteger(options.maximumBytes) && options.maximumBytes > 0) {
|
|
312
|
+
try {
|
|
313
|
+
const stat = fs.lstatSync(safePath);
|
|
314
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > options.maximumBytes) {
|
|
315
|
+
return { events: [], corruptions: ["journal exceeds bounded inspection size"], head: undefined, inputTruncated: true, omittedEvents: 0 };
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
return { events: [], corruptions: ["journal size could not be inspected"], head: undefined, inputTruncated: false, omittedEvents: 0 };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
294
321
|
const content = fs.readFileSync(safePath);
|
|
295
322
|
const finalNewline = content.length === 0 || content.at(-1) === 0x0a;
|
|
296
323
|
const lastNewline = finalNewline ? content.length - 1 : content.lastIndexOf(0x0a);
|
|
@@ -329,6 +356,8 @@ export function readTaskJournal(cwd, options = {}) {
|
|
|
329
356
|
return {
|
|
330
357
|
events: visibleEvents,
|
|
331
358
|
corruptions,
|
|
359
|
+
inputTruncated: visibleEvents.length < events.length,
|
|
360
|
+
omittedEvents: Math.max(0, events.length - visibleEvents.length),
|
|
332
361
|
recoverableTailBytes,
|
|
333
362
|
validBytes,
|
|
334
363
|
head: events.at(-1)
|
|
@@ -66,17 +66,9 @@ export function safeTaskId(value) {
|
|
|
66
66
|
return normalized || "task";
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function stateRoot(cwd) {
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function tasksRoot(cwd) {
|
|
74
|
-
return path.join(stateRoot(cwd), "tasks");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function sessionsRoot(cwd) {
|
|
78
|
-
return path.join(stateRoot(cwd), "session-tasks");
|
|
79
|
-
}
|
|
69
|
+
function stateRoot(cwd) { return path.join(cwd, ".pi", "piagent-state"); }
|
|
70
|
+
function tasksRoot(cwd) { return path.join(stateRoot(cwd), "tasks"); }
|
|
71
|
+
function sessionsRoot(cwd) { return path.join(stateRoot(cwd), "session-tasks"); }
|
|
80
72
|
|
|
81
73
|
function taskRunPath(cwd, taskRunId) {
|
|
82
74
|
return path.join(tasksRoot(cwd), `${safeTaskId(taskRunId)}.json`);
|
|
@@ -98,7 +90,12 @@ function readJson(cwd, filePath) {
|
|
|
98
90
|
const safePath = resolveLocalStatePath(cwd, filePath, { label: "Task state", kind: "file" });
|
|
99
91
|
const descriptor = fs.openSync(safePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
100
92
|
try {
|
|
101
|
-
|
|
93
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
94
|
+
if (!before.isFile() || before.size > BigInt(8 * 1024 * 1024)) return undefined;
|
|
95
|
+
const value = JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
96
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
97
|
+
const fields = ["dev", "ino", "mode", "size", "mtimeNs", "ctimeNs"];
|
|
98
|
+
return fields.every((field) => before[field] === after[field]) ? value : undefined;
|
|
102
99
|
} finally {
|
|
103
100
|
fs.closeSync(descriptor);
|
|
104
101
|
}
|
|
@@ -586,7 +583,6 @@ function gitOutput(cwd, args, options = {}) {
|
|
|
586
583
|
stdio: ["ignore", "pipe", "ignore"]
|
|
587
584
|
});
|
|
588
585
|
}
|
|
589
|
-
|
|
590
586
|
function gitHasHead(cwd) {
|
|
591
587
|
try {
|
|
592
588
|
gitOutput(cwd, ["rev-parse", "--verify", "HEAD"]);
|
|
@@ -595,7 +591,6 @@ function gitHasHead(cwd) {
|
|
|
595
591
|
return false;
|
|
596
592
|
}
|
|
597
593
|
}
|
|
598
|
-
|
|
599
594
|
export function isGitWorkingTree(cwd) {
|
|
600
595
|
try {
|
|
601
596
|
return gitOutput(cwd, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
|
|
@@ -603,20 +598,18 @@ export function isGitWorkingTree(cwd) {
|
|
|
603
598
|
return false;
|
|
604
599
|
}
|
|
605
600
|
}
|
|
606
|
-
|
|
607
601
|
function normalizedGitPath(value) {
|
|
608
602
|
return String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "");
|
|
609
603
|
}
|
|
610
|
-
|
|
611
604
|
function prefixedGitPath(prefix, file) {
|
|
612
605
|
const normalized = normalizedGitPath(file);
|
|
613
606
|
return prefix ? `${prefix}/${normalized}` : normalized;
|
|
614
607
|
}
|
|
615
608
|
|
|
616
|
-
function workingTreeFilesForGitRoot(cwd) {
|
|
609
|
+
function workingTreeFilesForGitRoot(cwd, observeRename) {
|
|
617
610
|
if (!isGitWorkingTree(cwd)) return [];
|
|
618
611
|
const changed = gitHasHead(cwd)
|
|
619
|
-
? changedPathsFromNameStatus(gitOutput(cwd, ["diff", "--name-status", "-z", "--find-renames", "--diff-filter=ACMRD", "HEAD", "--"]))
|
|
612
|
+
? changedPathsFromNameStatus(gitOutput(cwd, ["diff", "--name-status", "-z", "--find-renames", "--diff-filter=ACMRD", "HEAD", "--"]), observeRename)
|
|
620
613
|
: gitOutput(cwd, ["ls-files", "-z"]).split("\0").filter(Boolean);
|
|
621
614
|
const untracked = gitOutput(cwd, ["ls-files", "--others", "--exclude-standard", "-z"]).split("\0").filter(Boolean);
|
|
622
615
|
return [...new Set([...changed, ...untracked].map(normalizedGitPath).filter((item) => item && !item.startsWith(".pi/piagent-state/")))].sort();
|
|
@@ -694,9 +687,7 @@ function nonGitWorkspaceFiles(cwd, gitRootPrefixes) {
|
|
|
694
687
|
return files.sort();
|
|
695
688
|
}
|
|
696
689
|
|
|
697
|
-
function nonGitWorkspaceFileDigest(cwd, file) {
|
|
698
|
-
return streamedWorkingTreeFileDigest(cwd, file, false);
|
|
699
|
-
}
|
|
690
|
+
function nonGitWorkspaceFileDigest(cwd, file, protectedProjectPath = null) { return streamedWorkingTreeFileDigest(cwd, file, false, protectedProjectPath); }
|
|
700
691
|
|
|
701
692
|
function pathWithinNonGitWorkspaceEvidenceRoot(candidate, gitRootPrefixes) {
|
|
702
693
|
const topSegment = candidate.split("/")[0];
|
|
@@ -777,7 +768,7 @@ export function repositoryFileManifest(cwd, maxFiles = 10_000) {
|
|
|
777
768
|
return [...new Set(files)].sort();
|
|
778
769
|
}
|
|
779
770
|
|
|
780
|
-
function changedPathsFromNameStatus(output) {
|
|
771
|
+
function changedPathsFromNameStatus(output, observeRename) {
|
|
781
772
|
const fields = String(output ?? "").split("\0").filter(Boolean);
|
|
782
773
|
const files = [];
|
|
783
774
|
for (let index = 0; index < fields.length;) {
|
|
@@ -787,7 +778,7 @@ function changedPathsFromNameStatus(output) {
|
|
|
787
778
|
files.push(source);
|
|
788
779
|
if (/^[RC]/.test(status)) {
|
|
789
780
|
const destination = fields[index++];
|
|
790
|
-
if (destination) files.push(destination);
|
|
781
|
+
if (destination) { files.push(destination); observeRename?.(source, destination); }
|
|
791
782
|
}
|
|
792
783
|
}
|
|
793
784
|
return files;
|
|
@@ -804,8 +795,12 @@ export function workingTreeSnapshotHasUnavailableEvidence(snapshot) {
|
|
|
804
795
|
return Object.values(snapshot ?? {}).some((digest) => isUnavailableWorkingTreeDigest(digest));
|
|
805
796
|
}
|
|
806
797
|
|
|
807
|
-
function streamedWorkingTreeFileDigest(root, file, hasHead) {
|
|
798
|
+
function streamedWorkingTreeFileDigest(root, file, hasHead, protectedProjectPath = null) {
|
|
808
799
|
const absolute = path.resolve(root, file);
|
|
800
|
+
if (protectedProjectPath) {
|
|
801
|
+
try { return unavailableWorkingTreeDigest(protectedProjectPath, "protected-path", fs.lstatSync(absolute)); }
|
|
802
|
+
catch { return unavailableWorkingTreeDigest(protectedProjectPath, "protected-path"); }
|
|
803
|
+
}
|
|
809
804
|
const relative = path.relative(root, absolute);
|
|
810
805
|
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
811
806
|
return unavailableWorkingTreeDigest(file, "outside-root");
|
|
@@ -866,22 +861,24 @@ function streamedWorkingTreeFileDigest(root, file, hasHead) {
|
|
|
866
861
|
}
|
|
867
862
|
}
|
|
868
863
|
|
|
869
|
-
export function workingTreeSnapshot(cwd) {
|
|
864
|
+
export function workingTreeSnapshot(cwd, options = {}) {
|
|
870
865
|
const snapshot = {};
|
|
866
|
+
const isProtectedProjectPath = typeof options.isProtectedProjectPath === "function" ? options.isProtectedProjectPath : () => false;
|
|
871
867
|
const roots = gitEvidenceRoots(cwd);
|
|
872
868
|
for (const root of roots) {
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
const
|
|
876
|
-
|
|
869
|
+
const protectedAliases = new Set();
|
|
870
|
+
const observeRename = (source, destination) => {
|
|
871
|
+
const paths = [source, destination];
|
|
872
|
+
if (paths.some((file) => file === ".pi/piagent-state" || file.startsWith(".pi/piagent-state/") || isProtectedProjectPath(prefixedGitPath(root.prefix, file))))
|
|
873
|
+
paths.forEach((file) => protectedAliases.add(file));
|
|
874
|
+
};
|
|
875
|
+
const hasHead = gitHasHead(root.cwd); for (const file of workingTreeFilesForGitRoot(root.cwd, observeRename)) {
|
|
876
|
+
const projectPath = prefixedGitPath(root.prefix, file);
|
|
877
|
+
snapshot[projectPath] = streamedWorkingTreeFileDigest(root.cwd, file, hasHead, isProtectedProjectPath(projectPath) || protectedAliases.has(file) ? projectPath : null);
|
|
877
878
|
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
snapshot[file] = nonGitWorkspaceFileDigest(cwd, file);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
return snapshot;
|
|
879
|
+
} if (!isGitWorkingTree(cwd) && roots.length > 0) for (const file of nonGitWorkspaceFiles(cwd, roots.map((root) => root.prefix))) {
|
|
880
|
+
snapshot[file] = nonGitWorkspaceFileDigest(cwd, file, isProtectedProjectPath(file) ? file : null);
|
|
881
|
+
} return snapshot;
|
|
885
882
|
}
|
|
886
883
|
|
|
887
884
|
export function taskStateMigrationStatus(cwd) {
|
|
@@ -37,7 +37,7 @@ export const SERVER_ACTIONS = new Set(["get", "approve", "reject", "reset", "ena
|
|
|
37
37
|
export const TERMINAL_ONLY = new Map([
|
|
38
38
|
["add", "piagent-mcp add <name> [--scope <scope>] --url <url> | -- <command> [args...]"],
|
|
39
39
|
["remove", "piagent-mcp remove <name> --scope <scope>"],
|
|
40
|
-
["preset", "piagent-mcp --preset <core|popular|design> --scope <scope>"]
|
|
40
|
+
["preset", "piagent-mcp --preset <core|popular|documents|google|design> --scope <scope>"]
|
|
41
41
|
]);
|
|
42
42
|
|
|
43
43
|
export const HELP_LINES = [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Nothing here is a secret. Every credential is referenced as an environment
|
|
7
7
|
// variable, so the config can be read, diffed and committed without leaking one.
|
|
8
8
|
|
|
9
|
-
/** @typedef {{description: string, config: Record<string, unknown>}} CatalogServer */
|
|
9
|
+
/** @typedef {{description: string, maturity?: "stable"|"experimental", config: Record<string, unknown>}} CatalogServer */
|
|
10
10
|
|
|
11
11
|
/** @type {Record<string, CatalogServer>} */
|
|
12
12
|
export const CATALOG_SERVERS = {
|
|
@@ -68,7 +68,7 @@ export const CATALOG_SERVERS = {
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
figma: {
|
|
71
|
-
description: "Figma remote MCP server.
|
|
71
|
+
description: "Figma remote MCP server. OAuth works only for clients approved in the Figma MCP Catalog.",
|
|
72
72
|
config: {
|
|
73
73
|
url: "https://mcp.figma.com/mcp",
|
|
74
74
|
auth: "oauth",
|
|
@@ -83,6 +83,85 @@ export const CATALOG_SERVERS = {
|
|
|
83
83
|
lifecycle: "lazy",
|
|
84
84
|
directTools: false
|
|
85
85
|
}
|
|
86
|
+
},
|
|
87
|
+
// Google's own remote servers, one per product, rather than a community server
|
|
88
|
+
// holding every scope at once: a session that only needs to read a document
|
|
89
|
+
// should not be one OAuth grant away from the mailbox.
|
|
90
|
+
//
|
|
91
|
+
// These carry no version pin because there is nothing local to pin -- the
|
|
92
|
+
// endpoint is operated by Google and changes under the same URL. That is a
|
|
93
|
+
// real difference from every command-based server above, where the pin is what
|
|
94
|
+
// stops a floating tag becoming a supply-chain decision each session.
|
|
95
|
+
// Scopes are declared read-only. Write access is one word longer to ask for
|
|
96
|
+
// and cannot be taken back once an agent has used it, so the default grant is
|
|
97
|
+
// the one that cannot lose anything. `clientId` is deliberately absent: every
|
|
98
|
+
// operator registers their own OAuth client, and a client id baked in here
|
|
99
|
+
// would be someone else's project answering for their consent screen.
|
|
100
|
+
"google-drive": {
|
|
101
|
+
description: "EXPERIMENTAL Developer Preview: Google Drive remote MCP server. Read-only by default.",
|
|
102
|
+
maturity: "experimental",
|
|
103
|
+
config: {
|
|
104
|
+
url: "https://drivemcp.googleapis.com/mcp/v1",
|
|
105
|
+
auth: "oauth",
|
|
106
|
+
oauth: { scopes: ["https://www.googleapis.com/auth/drive.readonly"] },
|
|
107
|
+
lifecycle: "lazy",
|
|
108
|
+
directTools: false
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
gmail: {
|
|
112
|
+
description: "EXPERIMENTAL Developer Preview: Gmail remote MCP server. Read-only by default.",
|
|
113
|
+
maturity: "experimental",
|
|
114
|
+
config: {
|
|
115
|
+
url: "https://gmailmcp.googleapis.com/mcp/v1",
|
|
116
|
+
auth: "oauth",
|
|
117
|
+
oauth: { scopes: ["https://www.googleapis.com/auth/gmail.readonly"] },
|
|
118
|
+
lifecycle: "lazy",
|
|
119
|
+
directTools: false
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"google-docs": {
|
|
123
|
+
description: "EXPERIMENTAL Developer Preview: Google Docs remote MCP server. Read-only by default.",
|
|
124
|
+
maturity: "experimental",
|
|
125
|
+
config: {
|
|
126
|
+
url: "https://docsmcp.googleapis.com/mcp/v1",
|
|
127
|
+
auth: "oauth",
|
|
128
|
+
oauth: {
|
|
129
|
+
scopes: [
|
|
130
|
+
"https://www.googleapis.com/auth/documents.readonly",
|
|
131
|
+
"https://www.googleapis.com/auth/drive.readonly"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
lifecycle: "lazy",
|
|
135
|
+
directTools: false
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"google-sheets": {
|
|
139
|
+
description: "EXPERIMENTAL Developer Preview: Google Sheets remote MCP server. Read-only by default.",
|
|
140
|
+
maturity: "experimental",
|
|
141
|
+
config: {
|
|
142
|
+
url: "https://sheetsmcp.googleapis.com/mcp/v1",
|
|
143
|
+
auth: "oauth",
|
|
144
|
+
oauth: {
|
|
145
|
+
scopes: [
|
|
146
|
+
"https://www.googleapis.com/auth/spreadsheets.readonly",
|
|
147
|
+
"https://www.googleapis.com/auth/drive.readonly"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
lifecycle: "lazy",
|
|
151
|
+
directTools: false
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
// Converts .docx, .xlsx, .pptx and .pdf to Markdown locally. Local matters:
|
|
155
|
+
// the alternative to this is uploading a document somewhere to read it, and a
|
|
156
|
+
// document worth converting is usually one worth not uploading.
|
|
157
|
+
markitdown: {
|
|
158
|
+
description: "MarkItDown MCP. Converts local docx, xlsx, pptx, pdf and html files to Markdown. Runs offline for file: URIs.",
|
|
159
|
+
config: {
|
|
160
|
+
command: "uvx",
|
|
161
|
+
args: ["markitdown-mcp@0.0.1a4"],
|
|
162
|
+
lifecycle: "lazy",
|
|
163
|
+
directTools: false
|
|
164
|
+
}
|
|
86
165
|
}
|
|
87
166
|
};
|
|
88
167
|
|
|
@@ -99,8 +178,34 @@ export const CATALOG_PREREQUISITES = {
|
|
|
99
178
|
"export GITHUB_PERSONAL_ACCESS_TOKEN with the scopes you intend to use"
|
|
100
179
|
],
|
|
101
180
|
playwright: ["browsers installed on first use (npx playwright install)"],
|
|
102
|
-
figma: ["
|
|
103
|
-
"figma-desktop": ["Figma desktop running with Dev Mode MCP enabled"]
|
|
181
|
+
figma: ["Piagent approved in the Figma MCP Catalog before remote OAuth can register"],
|
|
182
|
+
"figma-desktop": ["Figma desktop running with Dev Mode MCP enabled"],
|
|
183
|
+
"google-drive": [
|
|
184
|
+
"a Google Cloud project with drive.googleapis.com and drivemcp.googleapis.com enabled",
|
|
185
|
+
"Developer Preview: no Pi OAuth end-to-end release proof exists yet",
|
|
186
|
+
"Google's supported client guides use an OAuth client id and secret; Pi's public-client/PKCE compatibility is unverified",
|
|
187
|
+
"piagent-mcp add google-drive --url https://drivemcp.googleapis.com/mcp/v1 --oauth-client-id <id> --scope user"
|
|
188
|
+
],
|
|
189
|
+
gmail: [
|
|
190
|
+
"a Google Cloud project with gmail.googleapis.com and gmailmcp.googleapis.com enabled",
|
|
191
|
+
"Developer Preview: no Pi OAuth end-to-end release proof exists yet",
|
|
192
|
+
"Google's supported client guides use an OAuth client id and secret; Pi's public-client/PKCE compatibility is unverified",
|
|
193
|
+
"read the warning in docs/mcp-and-tools.md first: mail is untrusted input reaching an agent that can act"
|
|
194
|
+
],
|
|
195
|
+
"google-docs": [
|
|
196
|
+
"a Google Cloud project with docs.googleapis.com and docsmcp.googleapis.com enabled",
|
|
197
|
+
"Developer Preview: no Pi OAuth end-to-end release proof exists yet",
|
|
198
|
+
"Google's supported client guides use an OAuth client id and secret; Pi's public-client/PKCE compatibility is unverified"
|
|
199
|
+
],
|
|
200
|
+
"google-sheets": [
|
|
201
|
+
"a Google Cloud project with sheets.googleapis.com and sheetsmcp.googleapis.com enabled",
|
|
202
|
+
"Developer Preview: no Pi OAuth end-to-end release proof exists yet",
|
|
203
|
+
"Google's supported client guides use an OAuth client id and secret; Pi's public-client/PKCE compatibility is unverified"
|
|
204
|
+
],
|
|
205
|
+
markitdown: [
|
|
206
|
+
"uv on PATH (https://docs.astral.sh/uv); the first run downloads a Python runtime and roughly 120 MB of dependencies",
|
|
207
|
+
"note: markitdown-mcp is published as an alpha, so pin changes deliberately"
|
|
208
|
+
]
|
|
104
209
|
};
|
|
105
210
|
|
|
106
211
|
// Variables a pinned server references but works without. Context7 answers
|
|
@@ -113,18 +218,38 @@ export const CATALOG_OPTIONAL_ENV = {
|
|
|
113
218
|
context7: ["CONTEXT7_API_KEY"]
|
|
114
219
|
};
|
|
115
220
|
|
|
221
|
+
// `core` is what an install writes when nobody names a preset, and `popular` is
|
|
222
|
+
// what somebody picks without reading the list. Neither may reach a mailbox or a
|
|
223
|
+
// Drive: an install that quietly grants those is a decision the operator never
|
|
224
|
+
// made. The Google servers are therefore only in presets somebody has to type,
|
|
225
|
+
// and in `all`, which means what it says.
|
|
116
226
|
/** @type {Record<string, string[]>} */
|
|
117
227
|
export const CATALOG_PRESETS = {
|
|
118
228
|
minimal: [],
|
|
119
229
|
docs: ["context7"],
|
|
120
230
|
browser: ["chrome-devtools", "playwright"],
|
|
121
231
|
github: ["github"],
|
|
122
|
-
design: ["figma"],
|
|
232
|
+
design: ["figma-desktop"],
|
|
123
233
|
"design-local": ["figma-desktop"],
|
|
124
234
|
web: ["context7", "chrome-devtools", "playwright"],
|
|
235
|
+
// The generic documents preset stays local. Google Workspace is Developer
|
|
236
|
+
// Preview and must be selected through an explicitly named Google preset.
|
|
237
|
+
documents: ["markitdown"],
|
|
238
|
+
// One preset per product, because that is why Google ships four servers
|
|
239
|
+
// instead of one. Bundling them back together would mean reading a
|
|
240
|
+
// spreadsheet costs a grant over Drive and every document in it.
|
|
241
|
+
"google-drive": ["google-drive"],
|
|
242
|
+
"google-docs": ["google-docs"],
|
|
243
|
+
"google-sheets": ["google-sheets"],
|
|
244
|
+
"google-mail": ["gmail"],
|
|
245
|
+
google: ["google-drive", "google-docs", "google-sheets"],
|
|
246
|
+
"google-all": ["google-drive", "gmail", "google-docs", "google-sheets"],
|
|
125
247
|
core: ["context7", "chrome-devtools", "github"],
|
|
126
|
-
popular: ["context7", "chrome-devtools", "playwright", "github", "figma"],
|
|
127
|
-
all: [
|
|
248
|
+
popular: ["context7", "chrome-devtools", "playwright", "github", "figma-desktop"],
|
|
249
|
+
all: [
|
|
250
|
+
"context7", "chrome-devtools", "playwright", "github", "figma-desktop", "figma",
|
|
251
|
+
"markitdown", "google-drive", "gmail", "google-docs", "google-sheets"
|
|
252
|
+
]
|
|
128
253
|
};
|
|
129
254
|
|
|
130
255
|
/** @param {string} name @returns {boolean} */
|
|
@@ -136,7 +261,7 @@ export function isCatalogPreset(name) {
|
|
|
136
261
|
* The servers a preset installs, with their definitions resolved. Throws on an
|
|
137
262
|
* unknown preset so a typo is refused before anything is written.
|
|
138
263
|
* @param {string} name
|
|
139
|
-
* @returns {{name: string, config: Record<string, unknown>, requires: string[]}[]}
|
|
264
|
+
* @returns {{name: string, maturity: "stable"|"experimental", config: Record<string, unknown>, requires: string[]}[]}
|
|
140
265
|
*/
|
|
141
266
|
export function resolvePreset(name) {
|
|
142
267
|
if (!isCatalogPreset(name)) {
|
|
@@ -145,6 +270,11 @@ export function resolvePreset(name) {
|
|
|
145
270
|
return CATALOG_PRESETS[name].map((serverName) => {
|
|
146
271
|
const server = CATALOG_SERVERS[serverName];
|
|
147
272
|
if (!server) throw new Error(`preset ${name} references missing server ${serverName}`);
|
|
148
|
-
return {
|
|
273
|
+
return {
|
|
274
|
+
name: serverName,
|
|
275
|
+
maturity: server.maturity ?? "stable",
|
|
276
|
+
config: server.config,
|
|
277
|
+
requires: CATALOG_PREREQUISITES[serverName] ?? []
|
|
278
|
+
};
|
|
149
279
|
});
|
|
150
280
|
}
|