talos-code 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +111 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +97 -0
- package/dist/automations/history-store.js +34 -0
- package/dist/automations/policy-store.js +43 -0
- package/dist/automations/runner.js +107 -0
- package/dist/automations/schedule.js +85 -0
- package/dist/automations/windows-task.js +53 -0
- package/dist/commands/advanced-cli.js +233 -0
- package/dist/commands/checkpoint-cli.js +45 -0
- package/dist/commands/config-cli.js +107 -0
- package/dist/commands/context.js +17 -0
- package/dist/commands/extensions-cli.js +207 -0
- package/dist/commands/project-cli.js +74 -0
- package/dist/commands/project-commands.js +143 -0
- package/dist/commands/provider-cli.js +185 -0
- package/dist/commands/services-cli.js +248 -0
- package/dist/commands/session-cli.js +171 -0
- package/dist/commands/system-cli.js +405 -0
- package/dist/config/commands.js +60 -0
- package/dist/config/load.js +382 -0
- package/dist/config/migrations.js +48 -0
- package/dist/config/types.js +11 -0
- package/dist/diagnostics/development-log.js +154 -0
- package/dist/diagnostics/doctor.js +104 -0
- package/dist/diagnostics/redact.js +80 -0
- package/dist/diagnostics/zip.js +52 -0
- package/dist/errors.js +156 -0
- package/dist/events/bridge.js +130 -0
- package/dist/extensions/installer.js +178 -0
- package/dist/extensions/package-schema.js +24 -0
- package/dist/headless/result.js +84 -0
- package/dist/headless/run.js +230 -0
- package/dist/i18n/en/approval.js +43 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +82 -0
- package/dist/i18n/en/errors.js +246 -0
- package/dist/i18n/en/firstrun.js +89 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +736 -0
- package/dist/i18n/en/tools.js +127 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/error-view.js +307 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/kernel-map.js +139 -0
- package/dist/io.js +45 -0
- package/dist/main.js +330 -0
- package/dist/paths.js +41 -0
- package/dist/protocol/v2/codec.js +9 -0
- package/dist/protocol/v2/events.js +555 -0
- package/dist/protocol/v2/index.js +4 -0
- package/dist/protocol/v2/replay.js +28 -0
- package/dist/protocol/v2/types.js +1 -0
- package/dist/provider/control-plane.js +135 -0
- package/dist/provider/environment-keys.js +275 -0
- package/dist/provider/health.js +110 -0
- package/dist/provider/missing-key.js +48 -0
- package/dist/provider/model-catalog.js +168 -0
- package/dist/provider/provider-text.js +13 -0
- package/dist/provider/store.js +95 -0
- package/dist/provider/system-keyring.js +214 -0
- package/dist/runtime/active-run.js +63 -0
- package/dist/runtime/agent-tree.js +74 -0
- package/dist/runtime/attachments.js +84 -0
- package/dist/runtime/brokered-executor.js +395 -0
- package/dist/runtime/context-status.js +76 -0
- package/dist/runtime/create-runtime.js +111 -0
- package/dist/runtime/model-profile.js +361 -0
- package/dist/runtime/output-store.js +137 -0
- package/dist/runtime/provider-attempts.js +185 -0
- package/dist/runtime/replay-buffer.js +153 -0
- package/dist/runtime/repo.js +95 -0
- package/dist/runtime/session-facade.js +135 -0
- package/dist/runtime/session-summary.js +125 -0
- package/dist/runtime/supervisor.js +262 -0
- package/dist/runtime/talos-composition.js +1006 -0
- package/dist/runtime/types.js +5 -0
- package/dist/runtime/usage-snapshot.js +82 -0
- package/dist/security/auto-classifier.js +38 -0
- package/dist/security/credential-free-environment.js +54 -0
- package/dist/security/evaluate.js +243 -0
- package/dist/security/execution-backends.js +236 -0
- package/dist/security/execution-broker.js +102 -0
- package/dist/security/forge-scan.js +74 -0
- package/dist/security/from-approval.js +39 -0
- package/dist/security/mxc-execution-backend.js +281 -0
- package/dist/security/permission-engine.js +138 -0
- package/dist/security/permission-explanation.js +54 -0
- package/dist/security/persist.js +179 -0
- package/dist/security/plugin-guard.js +230 -0
- package/dist/security/process-tree-evidence-store.js +74 -0
- package/dist/security/process-tree-probe.js +554 -0
- package/dist/security/project-resource-inventory.js +186 -0
- package/dist/security/project-trust-gate.js +38 -0
- package/dist/security/project-trust.js +367 -0
- package/dist/security/rule-parser.js +201 -0
- package/dist/security/shell-segmentation.js +68 -0
- package/dist/security/trust-authority.js +324 -0
- package/dist/security/types.js +1 -0
- package/dist/security/workspace-identity.js +102 -0
- package/dist/services/automation-facade.js +59 -0
- package/dist/services/forge-facade.js +77 -0
- package/dist/services/hook-facade.js +240 -0
- package/dist/services/index.js +16 -0
- package/dist/services/library-facade.js +174 -0
- package/dist/services/mcp-facade.js +348 -0
- package/dist/services/memory-facade.js +126 -0
- package/dist/services/notes-facade.js +157 -0
- package/dist/services/plugin-facade.js +308 -0
- package/dist/services/research-facade.js +110 -0
- package/dist/services/task-board-facade.js +236 -0
- package/dist/services/workflow-catalog.js +246 -0
- package/dist/sessions/export-format.js +98 -0
- package/dist/sessions/metadata-store.js +160 -0
- package/dist/sessions/share.js +122 -0
- package/dist/sessions/transfer.js +16 -0
- package/dist/subcommands.js +62 -0
- package/dist/tui/agent-roster.js +36 -0
- package/dist/tui/app.js +3761 -0
- package/dist/tui/approval.js +36 -0
- package/dist/tui/boot/boot-sequence.js +76 -0
- package/dist/tui/boot/cinematic.js +243 -0
- package/dist/tui/boot/desktop-logo.js +82 -0
- package/dist/tui/boot.js +3 -0
- package/dist/tui/busy-input.js +58 -0
- package/dist/tui/catalog-service.js +559 -0
- package/dist/tui/components/assistant-stream.js +1 -0
- package/dist/tui/components/command-menu.js +68 -0
- package/dist/tui/components/composer.js +219 -0
- package/dist/tui/components/diff.js +35 -0
- package/dist/tui/components/footer.js +48 -0
- package/dist/tui/components/header.js +6 -0
- package/dist/tui/components/markdown.js +305 -0
- package/dist/tui/components/status-indicator.js +32 -0
- package/dist/tui/components/terminal-shell.js +205 -0
- package/dist/tui/components/thinking-row.js +19 -0
- package/dist/tui/components/tool-row.js +97 -0
- package/dist/tui/components/transcript-virtualizer.js +165 -0
- package/dist/tui/components/transcript.js +43 -0
- package/dist/tui/diff-model.js +77 -0
- package/dist/tui/editor-history.js +21 -0
- package/dist/tui/editor.js +210 -0
- package/dist/tui/event-adapter.js +436 -0
- package/dist/tui/exit-output.js +58 -0
- package/dist/tui/external-editor.js +53 -0
- package/dist/tui/file-completion.js +89 -0
- package/dist/tui/focus-manager.js +5 -0
- package/dist/tui/highlight.js +30 -0
- package/dist/tui/input-router.js +18 -0
- package/dist/tui/interrupt.js +99 -0
- package/dist/tui/keybindings.js +194 -0
- package/dist/tui/keymap-resolver.js +91 -0
- package/dist/tui/launch-state.js +139 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +45 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +39 -0
- package/dist/tui/overlays/approval-dialog.js +640 -0
- package/dist/tui/overlays/automation-center.js +51 -0
- package/dist/tui/overlays/checkpoint-picker.js +47 -0
- package/dist/tui/overlays/context-inspector.js +36 -0
- package/dist/tui/overlays/effort-line.js +110 -0
- package/dist/tui/overlays/forge-center.js +46 -0
- package/dist/tui/overlays/help-dialog.js +16 -0
- package/dist/tui/overlays/history-picker.js +37 -0
- package/dist/tui/overlays/hook-center.js +70 -0
- package/dist/tui/overlays/library-center.js +55 -0
- package/dist/tui/overlays/mcp-center.js +60 -0
- package/dist/tui/overlays/memory-center.js +63 -0
- package/dist/tui/overlays/model-picker.js +72 -0
- package/dist/tui/overlays/notes-center.js +53 -0
- package/dist/tui/overlays/overlay-host.js +8 -0
- package/dist/tui/overlays/plugin-center.js +76 -0
- package/dist/tui/overlays/provider-picker.js +145 -0
- package/dist/tui/overlays/queue-editor.js +32 -0
- package/dist/tui/overlays/research-center.js +59 -0
- package/dist/tui/overlays/scroll-window.js +234 -0
- package/dist/tui/overlays/session-picker.js +115 -0
- package/dist/tui/overlays/tasks-center.js +83 -0
- package/dist/tui/overlays/theme-picker.js +21 -0
- package/dist/tui/overlays/transcript-search.js +58 -0
- package/dist/tui/overlays/trust-center.js +29 -0
- package/dist/tui/overlays/workflow-center.js +46 -0
- package/dist/tui/project-file-index.js +214 -0
- package/dist/tui/project-references.js +85 -0
- package/dist/tui/project-trust-prompt.js +287 -0
- package/dist/tui/prompt-history-store.js +116 -0
- package/dist/tui/queue-store.js +110 -0
- package/dist/tui/regions/budget.js +59 -0
- package/dist/tui/regions/views.js +96 -0
- package/dist/tui/render-coordinator.js +148 -0
- package/dist/tui/render-scheduler.js +4 -0
- package/dist/tui/run.js +69 -0
- package/dist/tui/selection-list.js +29 -0
- package/dist/tui/session-controller.js +778 -0
- package/dist/tui/session-export.js +54 -0
- package/dist/tui/shell-input.js +35 -0
- package/dist/tui/shell-layout.js +48 -0
- package/dist/tui/shell-model.js +98 -0
- package/dist/tui/slash-commands.js +80 -0
- package/dist/tui/state.js +151 -0
- package/dist/tui/status-bar.js +125 -0
- package/dist/tui/status-view.js +40 -0
- package/dist/tui/terminal-capabilities.js +9 -0
- package/dist/tui/terminal-session.js +11 -0
- package/dist/tui/text-width.js +66 -0
- package/dist/tui/theme-catalog.js +25 -0
- package/dist/tui/theme-store.js +23 -0
- package/dist/tui/theme.js +23 -0
- package/dist/tui/tool-display.js +135 -0
- package/dist/tui/tool-facts.js +1 -0
- package/dist/tui/tools/bash-renderer.js +32 -0
- package/dist/tui/tools/change.js +61 -0
- package/dist/tui/tools/edit-renderer.js +17 -0
- package/dist/tui/tools/generic-renderer.js +13 -0
- package/dist/tui/tools/list-renderer.js +14 -0
- package/dist/tui/tools/read-renderer.js +13 -0
- package/dist/tui/tools/registry.js +20 -0
- package/dist/tui/tools/row-format.js +187 -0
- package/dist/tui/tools/search-renderer.js +38 -0
- package/dist/tui/tools/shared.js +97 -0
- package/dist/tui/tools/write-renderer.js +15 -0
- package/dist/tui/transcript-model.js +441 -0
- package/dist/tui/ui-preferences.js +79 -0
- package/dist/tui/usage-view.js +77 -0
- package/dist/tui/vim-mode.js +99 -0
- package/dist/update/check.js +15 -0
- package/dist/update/npm.js +51 -0
- package/dist/update/run.js +129 -0
- package/dist/version.js +2 -0
- package/dist/workspace/checkpoint-store.js +210 -0
- package/dist/workspace/checkpoint.js +413 -0
- package/dist/workspace/restore.js +232 -0
- package/package.json +63 -5
- package/vendor/context-engine/package.json +11 -0
- package/vendor/context-engine/src/compaction-planner.mjs +57 -0
- package/vendor/context-engine/src/contracts.mjs +48 -0
- package/vendor/context-engine/src/engine.mjs +445 -0
- package/vendor/context-engine/src/node/context-export.mjs +164 -0
- package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
- package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
- package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
- package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
- package/vendor/context-engine/src/profiles.mjs +10 -0
- package/vendor/context-engine/src/retrieval.mjs +104 -0
- package/vendor/context-engine/src/summary.mjs +97 -0
- package/vendor/context-engine/src/usage.mjs +34 -0
- package/vendor/harness-ui/package.json +38 -0
- package/vendor/harness-ui/src/acp-agent.mjs +350 -0
- package/vendor/harness-ui/src/agent-service.mjs +2188 -0
- package/vendor/harness-ui/src/agui-events.mjs +452 -0
- package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
- package/vendor/harness-ui/src/artifact-store.mjs +39 -0
- package/vendor/harness-ui/src/assistenza.mjs +123 -0
- package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
- package/vendor/harness-ui/src/automation-store.mjs +145 -0
- package/vendor/harness-ui/src/browser-annota.mjs +639 -0
- package/vendor/harness-ui/src/browser-frame.mjs +211 -0
- package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
- package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
- package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
- package/vendor/harness-ui/src/browser-stream.mjs +445 -0
- package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
- package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
- package/vendor/harness-ui/src/config.mjs +697 -0
- package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
- package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
- package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
- package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
- package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
- package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
- package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
- package/vendor/harness-ui/src/context-runtime.mjs +118 -0
- package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
- package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
- package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
- package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
- package/vendor/harness-ui/src/custom-task.mjs +171 -0
- package/vendor/harness-ui/src/doctor.mjs +142 -0
- package/vendor/harness-ui/src/document-filename.mjs +97 -0
- package/vendor/harness-ui/src/document-generator.mjs +493 -0
- package/vendor/harness-ui/src/document-report.mjs +331 -0
- package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
- package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
- package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
- package/vendor/harness-ui/src/forge-contract.mjs +221 -0
- package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
- package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
- package/vendor/harness-ui/src/generation-idle.mjs +332 -0
- package/vendor/harness-ui/src/gguf-header.mjs +207 -0
- package/vendor/harness-ui/src/git-service.mjs +626 -0
- package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
- package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
- package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
- package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
- package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
- package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
- package/vendor/harness-ui/src/hook-registry.mjs +186 -0
- package/vendor/harness-ui/src/http-app.mjs +6259 -0
- package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
- package/vendor/harness-ui/src/id-archivio.mjs +27 -0
- package/vendor/harness-ui/src/image-generator.mjs +143 -0
- package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
- package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
- package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
- package/vendor/harness-ui/src/library-store.mjs +652 -0
- package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
- package/vendor/harness-ui/src/local-model-store.mjs +339 -0
- package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
- package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
- package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
- package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
- package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
- package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
- package/vendor/harness-ui/src/mcp-client.mjs +98 -0
- package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
- package/vendor/harness-ui/src/mcp-session.mjs +177 -0
- package/vendor/harness-ui/src/memory-store.mjs +227 -0
- package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
- package/vendor/harness-ui/src/model-catalog.mjs +129 -0
- package/vendor/harness-ui/src/model-destination.mjs +189 -0
- package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
- package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
- package/vendor/harness-ui/src/notes-store.mjs +250 -0
- package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
- package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
- package/vendor/harness-ui/src/path-policy.mjs +442 -0
- package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
- package/vendor/harness-ui/src/plugin-session.mjs +180 -0
- package/vendor/harness-ui/src/process-policy.mjs +345 -0
- package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
- package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
- package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
- package/vendor/harness-ui/src/provider-probe.mjs +582 -0
- package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
- package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
- package/vendor/harness-ui/src/public-problem.mjs +109 -0
- package/vendor/harness-ui/src/research/card.mjs +235 -0
- package/vendor/harness-ui/src/research/citations.mjs +142 -0
- package/vendor/harness-ui/src/research/collector.mjs +275 -0
- package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
- package/vendor/harness-ui/src/research/dossier.mjs +114 -0
- package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
- package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
- package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
- package/vendor/harness-ui/src/research/independence.mjs +159 -0
- package/vendor/harness-ui/src/research/ledger.mjs +166 -0
- package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
- package/vendor/harness-ui/src/research/narration.mjs +181 -0
- package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
- package/vendor/harness-ui/src/research/opposing.mjs +305 -0
- package/vendor/harness-ui/src/research/outline.mjs +111 -0
- package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
- package/vendor/harness-ui/src/research/pdf.mjs +291 -0
- package/vendor/harness-ui/src/research/plan.mjs +301 -0
- package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
- package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
- package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
- package/vendor/harness-ui/src/research/recheck.mjs +194 -0
- package/vendor/harness-ui/src/research/report.mjs +203 -0
- package/vendor/harness-ui/src/research/run.mjs +527 -0
- package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
- package/vendor/harness-ui/src/research/verification.mjs +572 -0
- package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
- package/vendor/harness-ui/src/research-store.mjs +1133 -0
- package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
- package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
- package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
- package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
- package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
- package/vendor/harness-ui/src/search-source-store.mjs +172 -0
- package/vendor/harness-ui/src/session-registry.mjs +6095 -0
- package/vendor/harness-ui/src/session-store.mjs +220 -0
- package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
- package/vendor/harness-ui/src/setup-stato.mjs +31 -0
- package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
- package/vendor/harness-ui/src/skill-registry.mjs +120 -0
- package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
- package/vendor/harness-ui/src/static-files.mjs +96 -0
- package/vendor/harness-ui/src/stream-partition.mjs +123 -0
- package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
- package/vendor/harness-ui/src/task-catalog.mjs +65 -0
- package/vendor/harness-ui/src/tasks-store.mjs +220 -0
- package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
- package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
- package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
- package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
- package/vendor/harness-ui/src/usage-cache.mjs +315 -0
- package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
- package/vendor/harness-ui/src/workspace-context.mjs +124 -0
- package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
- package/vendor/harness-ui/src/workspace-files.mjs +589 -0
- package/vendor/harness-ui/src/workspace-info.mjs +189 -0
- package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
- package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
- package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
- package/vendor/manifest.json +170 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fingerprintExecutableResource, fingerprintExecutableResourceContent } from "./project-trust.js";
|
|
4
|
+
import { isPathInsideWorkspace } from "./workspace-identity.js";
|
|
5
|
+
// A plugin package contributes EVERY file it carries, not just its manifest: a manifest only
|
|
6
|
+
// inventory lets the code an approval covered be replaced underneath that approval. Deciding which
|
|
7
|
+
// extensions are "really" executable would be a judgement about safety, which this inventory does
|
|
8
|
+
// not make - it only decides what trust must be able to NOTICE.
|
|
9
|
+
// The walk is bounded so a package carrying node_modules cannot turn a trust check into a stall,
|
|
10
|
+
// and the bound fails CLOSED: truncating the walk would reopen the same hole somewhere deeper.
|
|
11
|
+
// Measured on Windows 11 / Node 24.18, warm cache: 10 000 files = ~98 ms walk + ~370 ms fingerprint
|
|
12
|
+
// at concurrency 32. Fingerprinting the same set with an unbounded Promise.all dies with EMFILE.
|
|
13
|
+
export const PLUGIN_PACKAGE_MAX_FILES = 10_000;
|
|
14
|
+
const FINGERPRINT_CONCURRENCY = 32;
|
|
15
|
+
async function entryKind(pathname) { try {
|
|
16
|
+
const value = await lstat(pathname);
|
|
17
|
+
if (value.isSymbolicLink())
|
|
18
|
+
return 'symlink';
|
|
19
|
+
if (value.isFile())
|
|
20
|
+
return 'file';
|
|
21
|
+
if (value.isDirectory())
|
|
22
|
+
return 'directory';
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === 'ENOENT')
|
|
27
|
+
return null;
|
|
28
|
+
throw error;
|
|
29
|
+
} }
|
|
30
|
+
function fail(code) { throw Object.assign(new Error(code), { code }); }
|
|
31
|
+
function relativeId(root, pathname) { return path.relative(root, pathname).split(path.sep).join('/'); }
|
|
32
|
+
async function walkFiles(root, accept) {
|
|
33
|
+
const rootKind = await entryKind(root);
|
|
34
|
+
if (rootKind === null)
|
|
35
|
+
return [];
|
|
36
|
+
if (rootKind === 'symlink')
|
|
37
|
+
fail('RESOURCE_DIRECTORY_SYMLINK_UNSUPPORTED');
|
|
38
|
+
if (rootKind !== 'directory')
|
|
39
|
+
return [];
|
|
40
|
+
const files = [];
|
|
41
|
+
async function walk(directory) { for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
42
|
+
const pathname = path.join(directory, entry.name);
|
|
43
|
+
if (entry.isSymbolicLink()) {
|
|
44
|
+
if (accept(pathname))
|
|
45
|
+
files.push(pathname);
|
|
46
|
+
else
|
|
47
|
+
fail('RESOURCE_DIRECTORY_SYMLINK_UNSUPPORTED');
|
|
48
|
+
}
|
|
49
|
+
else if (entry.isDirectory())
|
|
50
|
+
await walk(pathname);
|
|
51
|
+
else if (entry.isFile() && accept(pathname))
|
|
52
|
+
files.push(pathname);
|
|
53
|
+
} }
|
|
54
|
+
await walk(root);
|
|
55
|
+
return files.sort();
|
|
56
|
+
}
|
|
57
|
+
// Every link is refused rather than followed or skipped: following one lets the walk leave the
|
|
58
|
+
// package, skipping one lets a file the package can load stay outside the fingerprint. Both
|
|
59
|
+
// directions are the same hole, so neither is tolerated.
|
|
60
|
+
async function walkPackageFiles(packageRoot, limit) {
|
|
61
|
+
const files = [];
|
|
62
|
+
async function walk(directory) { for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
63
|
+
const pathname = path.join(directory, entry.name);
|
|
64
|
+
if (entry.isSymbolicLink())
|
|
65
|
+
fail('RESOURCE_DIRECTORY_SYMLINK_UNSUPPORTED');
|
|
66
|
+
else if (entry.isDirectory())
|
|
67
|
+
await walk(pathname);
|
|
68
|
+
else if (entry.isFile()) {
|
|
69
|
+
if (files.length >= limit)
|
|
70
|
+
fail('RESOURCE_PACKAGE_TOO_LARGE');
|
|
71
|
+
files.push(pathname);
|
|
72
|
+
}
|
|
73
|
+
} }
|
|
74
|
+
await walk(packageRoot);
|
|
75
|
+
return files.sort();
|
|
76
|
+
}
|
|
77
|
+
async function mapBounded(items, limit, handler) {
|
|
78
|
+
const results = new Array(items.length);
|
|
79
|
+
let cursor = 0;
|
|
80
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => { for (;;) {
|
|
81
|
+
const index = cursor++;
|
|
82
|
+
if (index >= items.length)
|
|
83
|
+
return;
|
|
84
|
+
results[index] = await handler(items[index]);
|
|
85
|
+
} }));
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
async function fixedFile(root, relative, kind, candidates) { const pathname = path.join(root, relative); const value = await entryKind(pathname); if (value === 'file' || value === 'symlink')
|
|
89
|
+
candidates.push({ kind, id: relative, path: pathname }); }
|
|
90
|
+
async function safeConfigText(workspace, pathname) {
|
|
91
|
+
const canonical = await realpath(pathname);
|
|
92
|
+
if (!isPathInsideWorkspace(workspace, canonical))
|
|
93
|
+
fail('RESOURCE_OUTSIDE_WORKSPACE');
|
|
94
|
+
return readFile(canonical, 'utf8');
|
|
95
|
+
}
|
|
96
|
+
function uniqueId(value, seen) { if (typeof value !== 'string' || value.length === 0)
|
|
97
|
+
fail('RESOURCE_CONFIG_MALFORMED'); if (seen.has(value))
|
|
98
|
+
fail('RESOURCE_ID_DUPLICATE'); seen.add(value); return value; }
|
|
99
|
+
async function hookCandidates(workspace, candidates) {
|
|
100
|
+
const pathname = path.join(workspace.canonicalRoot, '.harness-ui-hooks.json');
|
|
101
|
+
const kind = await entryKind(pathname);
|
|
102
|
+
if (kind === null)
|
|
103
|
+
return;
|
|
104
|
+
if (kind !== 'file' && kind !== 'symlink')
|
|
105
|
+
fail('RESOURCE_CONFIG_MALFORMED');
|
|
106
|
+
let data;
|
|
107
|
+
try {
|
|
108
|
+
data = JSON.parse(await safeConfigText(workspace, pathname));
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error?.code === 'RESOURCE_OUTSIDE_WORKSPACE')
|
|
112
|
+
throw error;
|
|
113
|
+
fail('RESOURCE_CONFIG_MALFORMED');
|
|
114
|
+
}
|
|
115
|
+
if (!data || !Array.isArray(data.hooks))
|
|
116
|
+
fail('RESOURCE_CONFIG_MALFORMED');
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
const legacyFingerprint = (await fingerprintExecutableResource({ workspace, kind: 'hook', id: '.harness-ui-hooks.json', path: pathname })).fingerprint;
|
|
119
|
+
for (const row of data.hooks) {
|
|
120
|
+
const id = uniqueId(row?.id, seen);
|
|
121
|
+
if (typeof row?.comando !== 'string' || !row.comando || !Array.isArray(row?.eventi) || !row.eventi.length || !row.eventi.every((event) => typeof event === 'string'))
|
|
122
|
+
fail('RESOURCE_CONFIG_MALFORMED');
|
|
123
|
+
candidates.push({ kind: 'hook', id, path: pathname, semanticContent: JSON.stringify({ eventi: row.eventi, comando: row.comando }), legacyFingerprint });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function mcpCandidates(workspace, candidates) {
|
|
127
|
+
const pathname = path.join(workspace.canonicalRoot, '.harness-ui-mcp.json');
|
|
128
|
+
const kind = await entryKind(pathname);
|
|
129
|
+
if (kind === null)
|
|
130
|
+
return;
|
|
131
|
+
if (kind !== 'file' && kind !== 'symlink')
|
|
132
|
+
fail('RESOURCE_CONFIG_MALFORMED');
|
|
133
|
+
let data;
|
|
134
|
+
try {
|
|
135
|
+
data = JSON.parse(await safeConfigText(workspace, pathname));
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error?.code === 'RESOURCE_OUTSIDE_WORKSPACE')
|
|
139
|
+
throw error;
|
|
140
|
+
fail('RESOURCE_CONFIG_MALFORMED');
|
|
141
|
+
}
|
|
142
|
+
if (!data || !Array.isArray(data.server))
|
|
143
|
+
fail('RESOURCE_CONFIG_MALFORMED');
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
const legacyFingerprint = (await fingerprintExecutableResource({ workspace, kind: 'mcp', id: '.harness-ui-mcp.json', path: pathname })).fingerprint;
|
|
146
|
+
for (const row of data.server) {
|
|
147
|
+
const id = uniqueId(row?.id, seen);
|
|
148
|
+
const args = row?.argomenti ?? [];
|
|
149
|
+
if (typeof row?.comando !== 'string' || !row.comando || !Array.isArray(args) || !args.every((arg) => typeof arg === 'string') || !Array.isArray(row?.allowlist) || !row.allowlist.length || !row.allowlist.every((tool) => typeof tool === 'string' && tool.length > 0))
|
|
150
|
+
fail('RESOURCE_CONFIG_MALFORMED');
|
|
151
|
+
candidates.push({ kind: 'mcp', id, path: pathname, semanticContent: JSON.stringify({ comando: row.comando, argomenti: args, allowlist: row.allowlist }), legacyFingerprint });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export async function inventoryProjectResources(workspace, options) {
|
|
155
|
+
const packageLimit = Math.max(1, Math.min(options?.maxPackageFiles ?? PLUGIN_PACKAGE_MAX_FILES, PLUGIN_PACKAGE_MAX_FILES));
|
|
156
|
+
const root = workspace.canonicalRoot;
|
|
157
|
+
const candidates = [];
|
|
158
|
+
for (const relative of ['.talos-cli/config.json', 'AGENTS.md', 'CLAUDE.md', 'TALOS.md'])
|
|
159
|
+
await fixedFile(root, relative, 'context', candidates);
|
|
160
|
+
await hookCandidates(workspace, candidates);
|
|
161
|
+
await mcpCandidates(workspace, candidates);
|
|
162
|
+
const talosPath = path.join(root, '.talos');
|
|
163
|
+
const talosKind = await entryKind(talosPath);
|
|
164
|
+
if (talosKind === 'file' || talosKind === 'symlink')
|
|
165
|
+
candidates.push({ kind: 'context', id: '.talos', path: talosPath });
|
|
166
|
+
else if (talosKind === 'directory')
|
|
167
|
+
for (const pathname of await walkFiles(talosPath, () => true))
|
|
168
|
+
candidates.push({ kind: 'context', id: relativeId(root, pathname), path: pathname });
|
|
169
|
+
for (const pathname of await walkFiles(path.join(root, '.talos-cli', 'commands'), value => value.toLowerCase().endsWith('.md')))
|
|
170
|
+
candidates.push({ kind: 'command', id: relativeId(root, pathname), path: pathname });
|
|
171
|
+
const pluginRoot = path.join(root, '.harness-ui-plugins');
|
|
172
|
+
const pluginKind = await entryKind(pluginRoot);
|
|
173
|
+
if (pluginKind === 'symlink')
|
|
174
|
+
fail('RESOURCE_DIRECTORY_SYMLINK_UNSUPPORTED');
|
|
175
|
+
if (pluginKind === 'directory')
|
|
176
|
+
for (const entry of await readdir(pluginRoot, { withFileTypes: true })) {
|
|
177
|
+
if (entry.isSymbolicLink())
|
|
178
|
+
fail('RESOURCE_DIRECTORY_SYMLINK_UNSUPPORTED');
|
|
179
|
+
if (!entry.isDirectory())
|
|
180
|
+
continue;
|
|
181
|
+
for (const pathname of await walkPackageFiles(path.join(pluginRoot, entry.name), packageLimit))
|
|
182
|
+
candidates.push({ kind: 'plugin', id: relativeId(root, pathname), path: pathname });
|
|
183
|
+
}
|
|
184
|
+
const resources = await mapBounded(candidates, FINGERPRINT_CONCURRENCY, candidate => candidate.semanticContent !== undefined ? fingerprintExecutableResourceContent({ workspace, kind: candidate.kind, id: candidate.id, path: candidate.path, content: candidate.semanticContent, ...(candidate.legacyFingerprint ? { legacyFingerprint: candidate.legacyFingerprint } : {}) }) : fingerprintExecutableResource({ workspace, kind: candidate.kind, id: candidate.id, path: candidate.path }));
|
|
185
|
+
return resources.sort((left, right) => left.relativePath.localeCompare(right.relativePath) || left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id));
|
|
186
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createTrustAuthority } from "./trust-authority.js";
|
|
2
|
+
import { findNestedRepositories } from "./workspace-identity.js";
|
|
3
|
+
const reviewInstruction = 'Review with `talos project status`; if the change is expected, run `talos project trust` (or `talos project trust --include-nested` when status lists nested repositories).';
|
|
4
|
+
function assessmentOf(snapshot, folder) {
|
|
5
|
+
return { trusted: snapshot.trusted, reason: snapshot.reason, workspace: snapshot.workspace, resources: snapshot.resources.flatMap(row => row.current ? [row.current] : []), storeSchema: snapshot.storeSchema, migrationRequired: snapshot.migrationRequired, folder };
|
|
6
|
+
}
|
|
7
|
+
export async function assessProjectTrust({ projectRoot, trustRoot, homeDir }) {
|
|
8
|
+
const authority = createTrustAuthority({ projectRoot, trustRoot, ...(homeDir ? { homeDir } : {}) });
|
|
9
|
+
const snapshot = await authority.inspect();
|
|
10
|
+
return assessmentOf(snapshot, await authority.folderDecision(snapshot));
|
|
11
|
+
}
|
|
12
|
+
export async function nestedRepositoriesForProjectTrust(assessment) { return findNestedRepositories(assessment.workspace, [...new Set(assessment.resources.map(resource => resource.relativePath))]); }
|
|
13
|
+
/** R4 Q-R4-1: an interactive start asks when a project file is not trusted OR the folder has no saved decision. */
|
|
14
|
+
export function needsTrustQuestion(assessment) { return !assessment.trusted || (assessment.folder !== undefined && !assessment.folder.decided); }
|
|
15
|
+
export function projectTrustFailure(assessment) {
|
|
16
|
+
/* A folder that was never decided (no project files, or the home folder) is "not trusted", never "changed". */
|
|
17
|
+
const neverDecided = assessment.reason === 'PROJECT_UNTRUSTED' || assessment.reason === 'NO_PROJECT_RESOURCES' || assessment.trusted;
|
|
18
|
+
const code = neverDecided ? 'PROJECT_TRUST_REQUIRED' : 'PROJECT_TRUST_INVALIDATED';
|
|
19
|
+
const message = code === 'PROJECT_TRUST_REQUIRED'
|
|
20
|
+
? (assessment.resources.length
|
|
21
|
+
? `${code}: TALOS did not load project instructions or executable resources because this folder is not trusted. ${reviewInstruction}`
|
|
22
|
+
: `${code}: TALOS did not start because this folder is not trusted.`)
|
|
23
|
+
: `${code}: the trusted project snapshot changed (${assessment.reason}). TALOS did not load the changed snapshot. ${reviewInstruction}`;
|
|
24
|
+
return Object.assign(new Error(message), { code, reason: assessment.reason, projectIdentity: assessment.workspace.identityHash });
|
|
25
|
+
}
|
|
26
|
+
export async function assertProjectTrusted(input) {
|
|
27
|
+
const assessment = await assessProjectTrust(input);
|
|
28
|
+
if (assessment.trusted)
|
|
29
|
+
return assessment;
|
|
30
|
+
throw projectTrustFailure(assessment);
|
|
31
|
+
}
|
|
32
|
+
/** `folder` (R4 Q-R4-2): which folder decision the grant saves; omitted, this folder (nothing for the home folder or a drive root).
|
|
33
|
+
* `sessionOnly` (R4 Q-R4-17): "Trust this folder for this session" — the grant lives in memory for this process, nothing is written. */
|
|
34
|
+
export async function trustProjectAssessment({ assessment, trustRoot, nestedRepositories = [], folder, homeDir, sessionOnly = false }) {
|
|
35
|
+
const authority = createTrustAuthority({ projectRoot: assessment.workspace.canonicalRoot, trustRoot, ...(homeDir ? { homeDir } : {}) });
|
|
36
|
+
const trusted = await authority.trustProject({ expected: { workspaceIdentityHash: assessment.workspace.identityHash, resources: assessment.resources, nestedRepositories }, includeNested: nestedRepositories.length > 0, ...(folder ? { folder } : {}), ...(sessionOnly ? { session: true } : {}) });
|
|
37
|
+
return assessmentOf(trusted, await authority.folderDecision(trusted));
|
|
38
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isPathInsideWorkspace } from "./workspace-identity.js";
|
|
5
|
+
function fail(code) { throw Object.assign(new Error(code), { code }); }
|
|
6
|
+
export function projectResourceKey(resource) { return `${resource.kind}:${resource.id}`; }
|
|
7
|
+
function recordPath(trustRoot, workspace) { return path.join(trustRoot, `${workspace.identityHash}.json`); }
|
|
8
|
+
function backupPath(trustRoot, workspace) { return `${recordPath(trustRoot, workspace)}.v1.bak`; }
|
|
9
|
+
function parseResourceKey(key) {
|
|
10
|
+
const colon = key.indexOf(':');
|
|
11
|
+
const kind = key.slice(0, colon);
|
|
12
|
+
const id = key.slice(colon + 1);
|
|
13
|
+
if (colon <= 0 || !['command', 'hook', 'plugin', 'mcp', 'context'].includes(kind) || !id)
|
|
14
|
+
fail('PROJECT_TRUST_STORE_INVALID');
|
|
15
|
+
return { kind, id };
|
|
16
|
+
}
|
|
17
|
+
function trustedEntry(record, key) { return record.resources[key]; }
|
|
18
|
+
function normalizeNestedRepositories(values = []) {
|
|
19
|
+
const out = [...new Set(values)].sort();
|
|
20
|
+
for (const value of out) {
|
|
21
|
+
if (typeof value !== 'string' || !value || value.includes('\\') || path.posix.isAbsolute(value) || value.split('/').some(part => part === '' || part === '.' || part === '..'))
|
|
22
|
+
fail('PROJECT_TRUST_STORE_INVALID');
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function sameNestedRepositories(left, right) { const a = normalizeNestedRepositories(left), b = normalizeNestedRepositories(right); return a.length === b.length && a.every((value, index) => value === b[index]); }
|
|
27
|
+
function validBase(value, workspace) {
|
|
28
|
+
return value && value.identityHash === workspace.identityHash && value.canonicalRoot === workspace.canonicalRoot && value.resources && typeof value.resources === 'object';
|
|
29
|
+
}
|
|
30
|
+
function validateRecord(value, workspace) {
|
|
31
|
+
if (!validBase(value, workspace))
|
|
32
|
+
fail('PROJECT_TRUST_STORE_INVALID');
|
|
33
|
+
if (value.schema === 'talos.cli.project-trust.v1') {
|
|
34
|
+
for (const [key, row] of Object.entries(value.resources)) {
|
|
35
|
+
parseResourceKey(key);
|
|
36
|
+
if (typeof row?.fingerprint !== 'string' || typeof row?.relativePath !== 'string')
|
|
37
|
+
fail('PROJECT_TRUST_STORE_INVALID');
|
|
38
|
+
}
|
|
39
|
+
if (typeof value.trustedAt !== 'string')
|
|
40
|
+
fail('PROJECT_TRUST_STORE_INVALID');
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
if (value.schema === 'talos.cli.project-trust.v2') {
|
|
44
|
+
for (const [key, row] of Object.entries(value.resources)) {
|
|
45
|
+
const parsed = parseResourceKey(key);
|
|
46
|
+
if (row?.kind !== parsed.kind || row?.id !== parsed.id || typeof row?.fingerprint !== 'string' || typeof row?.relativePath !== 'string' || typeof row?.trustedAt !== 'string')
|
|
47
|
+
fail('PROJECT_TRUST_STORE_INVALID');
|
|
48
|
+
}
|
|
49
|
+
if (typeof value.trustedAt !== 'string' || typeof value.updatedAt !== 'string' || (value.rollbackValid !== undefined && typeof value.rollbackValid !== 'boolean'))
|
|
50
|
+
fail('PROJECT_TRUST_STORE_INVALID');
|
|
51
|
+
if (value.nestedRepositories !== undefined) {
|
|
52
|
+
if (!Array.isArray(value.nestedRepositories) || value.nestedRepositories.some((item) => typeof item !== 'string'))
|
|
53
|
+
fail('PROJECT_TRUST_STORE_INVALID');
|
|
54
|
+
normalizeNestedRepositories(value.nestedRepositories);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
fail('PROJECT_TRUST_STORE_INVALID');
|
|
59
|
+
}
|
|
60
|
+
/*
|
|
61
|
+
* R4 (owner decision Q-R4-17, 2026-09-24): the home folder and a drive root are trusted for THIS SESSION ONLY. Their grant is a
|
|
62
|
+
* v2 record kept in this map, keyed by trust root and workspace identity, and never written: every service this process builds
|
|
63
|
+
* (the runtime composition builds its own) reads it BEFORE the file, so hooks, MCP and plugins work for the session, the
|
|
64
|
+
* snapshot rule applies to it as to a saved record, changes made during the session stay here, and a new process has none.
|
|
65
|
+
*/
|
|
66
|
+
const SESSION_RECORDS = new Map();
|
|
67
|
+
function sessionKey(trustRoot, workspace) { return `${path.resolve(trustRoot)}\0${workspace.identityHash}`; }
|
|
68
|
+
async function readRecordWithText(trustRoot, workspace) {
|
|
69
|
+
const session = SESSION_RECORDS.get(sessionKey(trustRoot, workspace));
|
|
70
|
+
if (session)
|
|
71
|
+
return { record: structuredClone(session), text: '', session: true };
|
|
72
|
+
try {
|
|
73
|
+
const text = await readFile(recordPath(trustRoot, workspace), 'utf8');
|
|
74
|
+
return { record: validateRecord(JSON.parse(text), workspace), text, session: false };
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error.code === 'ENOENT')
|
|
78
|
+
return null;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function readRecord(trustRoot, workspace) { return (await readRecordWithText(trustRoot, workspace))?.record ?? null; }
|
|
83
|
+
async function atomicWriteText(file, text) {
|
|
84
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
85
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
86
|
+
try {
|
|
87
|
+
await writeFile(temporary, text, { encoding: 'utf8', mode: 0o600 });
|
|
88
|
+
const handle = await open(temporary, 'r+');
|
|
89
|
+
try {
|
|
90
|
+
await handle.sync();
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await handle.close();
|
|
94
|
+
}
|
|
95
|
+
await rename(temporary, file);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
await rm(temporary, { force: true });
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function atomicWrite(file, value) { return atomicWriteText(file, `${JSON.stringify(value, null, 2)}\n`); }
|
|
103
|
+
function v2Resources(resources, trustedAt) {
|
|
104
|
+
const entries = resources.map(resource => [projectResourceKey(resource), { kind: resource.kind, id: resource.id, fingerprint: resource.fingerprint, relativePath: resource.relativePath, trustedAt }]).sort(([a], [b]) => a.localeCompare(b));
|
|
105
|
+
return Object.fromEntries(entries);
|
|
106
|
+
}
|
|
107
|
+
function convertV1(record, updatedAt, nestedRepositories = []) {
|
|
108
|
+
const resources = {};
|
|
109
|
+
for (const key of Object.keys(record.resources).sort()) {
|
|
110
|
+
const parsed = parseResourceKey(key);
|
|
111
|
+
const row = record.resources[key];
|
|
112
|
+
resources[key] = { ...parsed, fingerprint: row.fingerprint, relativePath: row.relativePath, trustedAt: record.trustedAt };
|
|
113
|
+
}
|
|
114
|
+
return { schema: 'talos.cli.project-trust.v2', identityHash: record.identityHash, canonicalRoot: record.canonicalRoot, trustedAt: record.trustedAt, updatedAt, rollbackValid: true, nestedRepositories: normalizeNestedRepositories(nestedRepositories), resources };
|
|
115
|
+
}
|
|
116
|
+
async function preserveLegacyBackup(trustRoot, workspace, text) {
|
|
117
|
+
const backup = backupPath(trustRoot, workspace);
|
|
118
|
+
try {
|
|
119
|
+
await writeFile(backup, text, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code !== 'EEXIST')
|
|
123
|
+
throw error;
|
|
124
|
+
const existing = await readFile(backup, 'utf8');
|
|
125
|
+
if (existing !== text)
|
|
126
|
+
fail('TRUST_MIGRATION_BACKUP_CONFLICT');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function trustedForResource(record, resource, consumedLegacy) {
|
|
130
|
+
const direct = trustedEntry(record, projectResourceKey(resource));
|
|
131
|
+
if (direct)
|
|
132
|
+
return direct;
|
|
133
|
+
if (record.schema === 'talos.cli.project-trust.v1' && resource.legacyFingerprint) {
|
|
134
|
+
const legacyKey = `${resource.kind}:${resource.relativePath}`;
|
|
135
|
+
const legacy = record.resources[legacyKey];
|
|
136
|
+
if (legacy && legacy.fingerprint === resource.legacyFingerprint && legacy.relativePath === resource.relativePath) {
|
|
137
|
+
consumedLegacy?.add(legacyKey);
|
|
138
|
+
return legacy;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
function inspectRecord(record, resources, nestedRepositories = []) {
|
|
144
|
+
const current = new Map(resources.map(resource => [projectResourceKey(resource), resource]));
|
|
145
|
+
const consumedLegacy = new Set();
|
|
146
|
+
const rows = [];
|
|
147
|
+
for (const resource of resources) {
|
|
148
|
+
const key = projectResourceKey(resource);
|
|
149
|
+
const trusted = record ? trustedForResource(record, resource, consumedLegacy) : undefined;
|
|
150
|
+
const direct = record ? trustedEntry(record, key) : undefined;
|
|
151
|
+
const state = !trusted ? 'added' : direct ? (direct.fingerprint === resource.fingerprint && direct.relativePath === resource.relativePath ? 'trusted' : 'changed') : 'trusted';
|
|
152
|
+
rows.push({ key, kind: resource.kind, id: resource.id, relativePath: resource.relativePath, state, current: resource, currentFingerprint: resource.fingerprint, ...(trusted ? { trustedFingerprint: trusted.fingerprint } : {}) });
|
|
153
|
+
}
|
|
154
|
+
if (record)
|
|
155
|
+
for (const key of Object.keys(record.resources).sort())
|
|
156
|
+
if (!current.has(key) && !consumedLegacy.has(key)) {
|
|
157
|
+
const parsed = parseResourceKey(key);
|
|
158
|
+
const trusted = record.resources[key];
|
|
159
|
+
rows.push({ key, ...parsed, relativePath: trusted.relativePath, state: 'removed', trustedFingerprint: trusted.fingerprint });
|
|
160
|
+
}
|
|
161
|
+
rows.sort((a, b) => a.relativePath.localeCompare(b.relativePath) || a.key.localeCompare(b.key));
|
|
162
|
+
let state;
|
|
163
|
+
if (!record)
|
|
164
|
+
state = { trusted: false, reason: 'PROJECT_UNTRUSTED' };
|
|
165
|
+
else if (record.schema === 'talos.cli.project-trust.v1' && normalizeNestedRepositories(nestedRepositories).length > 0)
|
|
166
|
+
state = { trusted: false, reason: 'NESTED_REPOSITORY_BOUNDARY_CHANGED' };
|
|
167
|
+
else if (record.schema === 'talos.cli.project-trust.v2' && !sameNestedRepositories(record.nestedRepositories ?? [], nestedRepositories))
|
|
168
|
+
state = { trusted: false, reason: 'NESTED_REPOSITORY_BOUNDARY_CHANGED' };
|
|
169
|
+
else if (rows.some(row => row.state === 'added' || row.state === 'removed'))
|
|
170
|
+
state = { trusted: false, reason: 'RESOURCE_SET_CHANGED' };
|
|
171
|
+
else if (rows.some(row => row.state === 'changed'))
|
|
172
|
+
state = { trusted: false, reason: 'RESOURCE_FINGERPRINT_CHANGED' };
|
|
173
|
+
else
|
|
174
|
+
state = { trusted: true, reason: 'PROJECT_TRUSTED' };
|
|
175
|
+
return { state, storeSchema: record?.schema === 'talos.cli.project-trust.v1' ? 'v1' : record ? 'v2' : null, migrationRequired: record?.schema === 'talos.cli.project-trust.v1', resources: rows };
|
|
176
|
+
}
|
|
177
|
+
async function resourceLocation(workspace, resourcePath) {
|
|
178
|
+
const canonicalPath = await realpath(path.resolve(resourcePath));
|
|
179
|
+
if (!isPathInsideWorkspace(workspace, canonicalPath))
|
|
180
|
+
fail('RESOURCE_OUTSIDE_WORKSPACE');
|
|
181
|
+
return { canonicalPath, relativePath: path.relative(workspace.canonicalRoot, canonicalPath).split(path.sep).join('/') };
|
|
182
|
+
}
|
|
183
|
+
function fingerprintResource(workspace, kind, id, relativePath, content, domain) {
|
|
184
|
+
const fingerprint = createHash('sha256').update(domain).update('\0').update(kind).update('\0').update(id).update('\0').update(relativePath).update('\0').update(content).digest('hex');
|
|
185
|
+
return { workspaceIdentityHash: workspace.identityHash, kind, id, relativePath, fingerprint };
|
|
186
|
+
}
|
|
187
|
+
export async function fingerprintExecutableResource({ workspace, kind, id, path: resourcePath }) {
|
|
188
|
+
const { canonicalPath, relativePath } = await resourceLocation(workspace, resourcePath);
|
|
189
|
+
return fingerprintResource(workspace, kind, id, relativePath, await readFile(canonicalPath), 'talos-executable-resource-v1');
|
|
190
|
+
}
|
|
191
|
+
export async function fingerprintExecutableResourceContent({ workspace, kind, id, path: resourcePath, content, legacyFingerprint }) {
|
|
192
|
+
const { relativePath } = await resourceLocation(workspace, resourcePath);
|
|
193
|
+
return { ...fingerprintResource(workspace, kind, id, relativePath, content, 'talos-executable-resource-semantic-v2'), ...(legacyFingerprint ? { legacyFingerprint } : {}) };
|
|
194
|
+
}
|
|
195
|
+
export function createProjectTrustService({ trustRoot, clock = () => new Date().toISOString() }) {
|
|
196
|
+
/* A change to a session grant stays in memory; any other record is written as before. */
|
|
197
|
+
async function save(workspace, record, session) {
|
|
198
|
+
if (session) {
|
|
199
|
+
SESSION_RECORDS.set(sessionKey(trustRoot, workspace), structuredClone(record));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
await atomicWrite(recordPath(trustRoot, workspace), record);
|
|
203
|
+
}
|
|
204
|
+
async function isSession(workspace) { return SESSION_RECORDS.has(sessionKey(trustRoot, workspace)); }
|
|
205
|
+
async function ensureV2(workspace, resources, nestedRepositories = []) {
|
|
206
|
+
const current = await readRecordWithText(trustRoot, workspace);
|
|
207
|
+
if (!current)
|
|
208
|
+
fail('PROJECT_TRUST_REQUIRED');
|
|
209
|
+
if (current.record.schema === 'talos.cli.project-trust.v2')
|
|
210
|
+
return current.record;
|
|
211
|
+
await preserveLegacyBackup(trustRoot, workspace, current.text);
|
|
212
|
+
const now = clock();
|
|
213
|
+
const inspection = inspectRecord(current.record, resources);
|
|
214
|
+
const trustedCurrent = inspection.resources.filter(row => row.state === 'trusted' && row.current).map(row => row.current);
|
|
215
|
+
return { schema: 'talos.cli.project-trust.v2', identityHash: workspace.identityHash, canonicalRoot: workspace.canonicalRoot, trustedAt: current.record.trustedAt, updatedAt: now, rollbackValid: true, nestedRepositories: normalizeNestedRepositories(nestedRepositories), resources: v2Resources(trustedCurrent, current.record.trustedAt) };
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
async status(workspace) { return await readRecord(trustRoot, workspace) ? { trusted: true, reason: 'PROJECT_TRUSTED' } : { trusted: false, reason: 'PROJECT_UNTRUSTED' }; },
|
|
219
|
+
async trust(workspace, resources, nestedRepositories = [], options = {}) {
|
|
220
|
+
for (const resource of resources)
|
|
221
|
+
if (resource.workspaceIdentityHash !== workspace.identityHash)
|
|
222
|
+
fail('RESOURCE_IDENTITY_MISMATCH');
|
|
223
|
+
if (options.session) {
|
|
224
|
+
const now = clock();
|
|
225
|
+
SESSION_RECORDS.set(sessionKey(trustRoot, workspace), { schema: 'talos.cli.project-trust.v2', identityHash: workspace.identityHash, canonicalRoot: workspace.canonicalRoot, trustedAt: now, updatedAt: now, rollbackValid: false, nestedRepositories: normalizeNestedRepositories(nestedRepositories), resources: v2Resources(resources, now) });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
SESSION_RECORDS.delete(sessionKey(trustRoot, workspace));
|
|
229
|
+
const existing = await readRecordWithText(trustRoot, workspace);
|
|
230
|
+
const fromV1 = existing?.record.schema === 'talos.cli.project-trust.v1';
|
|
231
|
+
if (fromV1)
|
|
232
|
+
await preserveLegacyBackup(trustRoot, workspace, existing.text);
|
|
233
|
+
const now = clock();
|
|
234
|
+
const record = { schema: 'talos.cli.project-trust.v2', identityHash: workspace.identityHash, canonicalRoot: workspace.canonicalRoot, trustedAt: existing?.record.trustedAt ?? now, updatedAt: now, rollbackValid: fromV1, nestedRepositories: normalizeNestedRepositories(nestedRepositories), resources: v2Resources(resources, now) };
|
|
235
|
+
await atomicWrite(recordPath(trustRoot, workspace), record);
|
|
236
|
+
},
|
|
237
|
+
async revoke(workspace) { SESSION_RECORDS.delete(sessionKey(trustRoot, workspace)); await Promise.all([rm(recordPath(trustRoot, workspace), { force: true }), rm(backupPath(trustRoot, workspace), { force: true })]); },
|
|
238
|
+
forgetSession(workspace) { SESSION_RECORDS.delete(sessionKey(trustRoot, workspace)); },
|
|
239
|
+
hasSession(workspace) { return SESSION_RECORDS.has(sessionKey(trustRoot, workspace)); },
|
|
240
|
+
async verifyResource(workspace, resource) {
|
|
241
|
+
const record = await readRecord(trustRoot, workspace);
|
|
242
|
+
if (!record)
|
|
243
|
+
return { trusted: false, reason: 'PROJECT_UNTRUSTED' };
|
|
244
|
+
if (resource.workspaceIdentityHash !== workspace.identityHash)
|
|
245
|
+
return { trusted: false, reason: 'RESOURCE_IDENTITY_MISMATCH' };
|
|
246
|
+
const direct = trustedEntry(record, projectResourceKey(resource));
|
|
247
|
+
const trusted = trustedForResource(record, resource);
|
|
248
|
+
if (!trusted)
|
|
249
|
+
return { trusted: false, reason: 'RESOURCE_NOT_TRUSTED' };
|
|
250
|
+
if (direct && (direct.fingerprint !== resource.fingerprint || direct.relativePath !== resource.relativePath))
|
|
251
|
+
return { trusted: false, reason: 'RESOURCE_FINGERPRINT_CHANGED' };
|
|
252
|
+
return { trusted: true, reason: 'RESOURCE_TRUSTED' };
|
|
253
|
+
},
|
|
254
|
+
async verifySnapshot(workspace, resources, nestedRepositories = []) { return (await this.inspectSnapshot(workspace, resources, nestedRepositories)).state; },
|
|
255
|
+
async inspectSnapshot(workspace, resources, nestedRepositories = []) { for (const resource of resources)
|
|
256
|
+
if (resource.workspaceIdentityHash !== workspace.identityHash)
|
|
257
|
+
return { state: { trusted: false, reason: 'RESOURCE_IDENTITY_MISMATCH' }, storeSchema: null, migrationRequired: false, resources: [] }; return inspectRecord(await readRecord(trustRoot, workspace), resources, nestedRepositories); },
|
|
258
|
+
async reconcile(workspace, resources, keys, nestedRepositories = []) {
|
|
259
|
+
const requested = [...new Set(keys)].sort();
|
|
260
|
+
if (!requested.length)
|
|
261
|
+
return false;
|
|
262
|
+
for (const resource of resources)
|
|
263
|
+
if (resource.workspaceIdentityHash !== workspace.identityHash)
|
|
264
|
+
fail('RESOURCE_IDENTITY_MISMATCH');
|
|
265
|
+
const record = await ensureV2(workspace, resources, nestedRepositories);
|
|
266
|
+
const current = new Map(resources.map(resource => [projectResourceKey(resource), resource]));
|
|
267
|
+
const now = clock();
|
|
268
|
+
let changed = false;
|
|
269
|
+
for (const key of requested) {
|
|
270
|
+
const resource = current.get(key);
|
|
271
|
+
if (resource) {
|
|
272
|
+
const previous = record.resources[key];
|
|
273
|
+
const next = { kind: resource.kind, id: resource.id, fingerprint: resource.fingerprint, relativePath: resource.relativePath, trustedAt: now };
|
|
274
|
+
if (!previous || previous.fingerprint !== next.fingerprint || previous.relativePath !== next.relativePath) {
|
|
275
|
+
record.resources[key] = next;
|
|
276
|
+
changed = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else if (record.resources[key]) {
|
|
280
|
+
delete record.resources[key];
|
|
281
|
+
changed = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (changed) {
|
|
285
|
+
record.updatedAt = now;
|
|
286
|
+
record.rollbackValid = false;
|
|
287
|
+
await save(workspace, record, await isSession(workspace));
|
|
288
|
+
}
|
|
289
|
+
return changed;
|
|
290
|
+
},
|
|
291
|
+
async untrustResources(workspace, resources, keys, nestedRepositories = []) {
|
|
292
|
+
const requested = [...new Set(keys)].sort();
|
|
293
|
+
if (!requested.length)
|
|
294
|
+
return false;
|
|
295
|
+
const existing = await readRecord(trustRoot, workspace);
|
|
296
|
+
if (!existing)
|
|
297
|
+
return false;
|
|
298
|
+
const record = await ensureV2(workspace, resources, nestedRepositories);
|
|
299
|
+
let changed = false;
|
|
300
|
+
for (const key of requested)
|
|
301
|
+
if (record.resources[key]) {
|
|
302
|
+
delete record.resources[key];
|
|
303
|
+
changed = true;
|
|
304
|
+
}
|
|
305
|
+
if (changed) {
|
|
306
|
+
record.updatedAt = clock();
|
|
307
|
+
record.rollbackValid = false;
|
|
308
|
+
await save(workspace, record, await isSession(workspace));
|
|
309
|
+
}
|
|
310
|
+
return changed;
|
|
311
|
+
},
|
|
312
|
+
async migrate(workspace, resources, nestedRepositories = []) {
|
|
313
|
+
const existing = await readRecordWithText(trustRoot, workspace);
|
|
314
|
+
if (!existing)
|
|
315
|
+
return false;
|
|
316
|
+
if (existing.record.schema === 'talos.cli.project-trust.v2')
|
|
317
|
+
return false;
|
|
318
|
+
const inspection = inspectRecord(existing.record, resources, []);
|
|
319
|
+
if (!inspection.state.trusted)
|
|
320
|
+
fail('TRUST_MIGRATION_SNAPSHOT_CHANGED');
|
|
321
|
+
await preserveLegacyBackup(trustRoot, workspace, existing.text);
|
|
322
|
+
const now = clock();
|
|
323
|
+
const record = { schema: 'talos.cli.project-trust.v2', identityHash: workspace.identityHash, canonicalRoot: workspace.canonicalRoot, trustedAt: existing.record.trustedAt, updatedAt: now, rollbackValid: true, nestedRepositories: normalizeNestedRepositories(nestedRepositories), resources: v2Resources(resources, existing.record.trustedAt) };
|
|
324
|
+
await atomicWrite(recordPath(trustRoot, workspace), record);
|
|
325
|
+
return true;
|
|
326
|
+
},
|
|
327
|
+
async rollbackMigration(workspace) {
|
|
328
|
+
const backup = backupPath(trustRoot, workspace);
|
|
329
|
+
let text;
|
|
330
|
+
try {
|
|
331
|
+
text = await readFile(backup, 'utf8');
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (error.code === 'ENOENT')
|
|
335
|
+
fail('TRUST_MIGRATION_ROLLBACK_UNAVAILABLE');
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
const current = await readRecord(trustRoot, workspace);
|
|
339
|
+
if (!current || current.schema !== 'talos.cli.project-trust.v2')
|
|
340
|
+
fail('TRUST_MIGRATION_ROLLBACK_INVALID');
|
|
341
|
+
if (current.rollbackValid !== true)
|
|
342
|
+
fail('TRUST_MIGRATION_ROLLBACK_STALE');
|
|
343
|
+
const parsed = validateRecord(JSON.parse(text), workspace);
|
|
344
|
+
if (parsed.schema !== 'talos.cli.project-trust.v1')
|
|
345
|
+
fail('TRUST_MIGRATION_BACKUP_INVALID');
|
|
346
|
+
await atomicWriteText(recordPath(trustRoot, workspace), text);
|
|
347
|
+
await rm(backup, { force: true });
|
|
348
|
+
return true;
|
|
349
|
+
},
|
|
350
|
+
async rollbackAvailable(workspace) {
|
|
351
|
+
const current = await readRecord(trustRoot, workspace);
|
|
352
|
+
if (!current || current.schema !== 'talos.cli.project-trust.v2' || current.rollbackValid !== true)
|
|
353
|
+
return false;
|
|
354
|
+
try {
|
|
355
|
+
const text = await readFile(backupPath(trustRoot, workspace), 'utf8');
|
|
356
|
+
const parsed = validateRecord(JSON.parse(text), workspace);
|
|
357
|
+
return parsed.schema === 'talos.cli.project-trust.v1';
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (error.code === 'ENOENT')
|
|
361
|
+
return false;
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
migrationBackupPath(workspace) { return backupPath(trustRoot, workspace); }
|
|
366
|
+
};
|
|
367
|
+
}
|