talos-code 0.0.1 → 0.2.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/CHANGELOG.md +87 -0
- package/LICENSE +661 -0
- package/README.md +116 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +100 -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 +244 -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 +46 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +92 -0
- package/dist/i18n/en/errors.js +267 -0
- package/dist/i18n/en/firstrun.js +101 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +774 -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 +331 -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/openrouter-login.js +172 -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-archive.js +69 -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 +265 -0
- package/dist/runtime/talos-composition.js +1011 -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/agent-view.js +51 -0
- package/dist/tui/app.js +4098 -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 +560 -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 +38 -0
- package/dist/tui/components/terminal-shell.js +214 -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/descendant-approvals.js +104 -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 +148 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +56 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +43 -0
- package/dist/tui/overlays/approval-dialog.js +642 -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 +77 -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 +171 -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 +289 -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 +791 -0
- package/dist/tui/session-export.js +54 -0
- package/dist/tui/setup-wizard.js +46 -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 +82 -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 +27 -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 +64 -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,281 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ⭐⭐⭐ R0 lane S — THE CONTAINER'S ENVIRONMENT IS ONE ALLOWLIST, FOR THE PROBE AND FOR EVERY COMMAND.
|
|
3
|
+
*
|
|
4
|
+
* Measured 2026-09-23 on this host (Windows 11 26200.9457, Node 24.18.0, SDK 0.8.0, the policy below):
|
|
5
|
+
* · SDK 0.8.0 uses a supplied environment VERBATIM (microsoft/mxc docs/schema.md, 2026-09-21) and serialises it into
|
|
6
|
+
* `--config-base64` on the `wxc-exec.exe` command line, which process auditing records (B1 slice 19). So it is an
|
|
7
|
+
* allowlist of system paths: never `process.env`, never a denylist of it.
|
|
8
|
+
* · Without LOCALAPPDATA every sandboxed process dies before it starts: `CreateProcessW(PROC_THREAD_ATTRIBUTE_SECURITY_
|
|
9
|
+
* ENVIRONMENT) failed: WIN32_ERROR(203)` (microsoft/mxc#1130, closed 2026-09-14, "SystemRoot + LOCALAPPDATA").
|
|
10
|
+
* Without SystemRoot node, npm and powershell fail; without PATH npm, git, powershell and pwsh are not found
|
|
11
|
+
* (microsoft/mxc#1102: no PATH floor). Without APPDATA npm's prefix becomes `<cwd>\${APPDATA}\npm`; without
|
|
12
|
+
* USERPROFILE git finds no global configuration. windir, SystemDrive, TEMP, TMP, PATHEXT and ComSpec are not needed
|
|
13
|
+
* by those six workloads but are the gate E1 floor (Gemini CLI's win32 list; the container overrides TEMP with its
|
|
14
|
+
* own AppContainer folder). NUMBER_OF_PROCESSORS and PROCESSOR_ARCHITECTURE stay from B1 slice 19.
|
|
15
|
+
* · A command given NO environment gets the backend default, which is the user's profile block: 56 variables here,
|
|
16
|
+
* three of them secret-shaped names from the persistent user environment. So a command always gets this list.
|
|
17
|
+
* ⛔ A name is added only when a measurement in the real container shows a workload needs it, and only if neither its
|
|
18
|
+
* name nor its value can hold a credential.
|
|
19
|
+
*/
|
|
20
|
+
export const MXC_CHILD_ENVIRONMENT_NAMES = Object.freeze(['SystemRoot', 'windir', 'SystemDrive', 'LOCALAPPDATA', 'APPDATA', 'USERPROFILE', 'TEMP', 'TMP', 'PATH', 'PATHEXT', 'ComSpec', 'NUMBER_OF_PROCESSORS', 'PROCESSOR_ARCHITECTURE']);
|
|
21
|
+
function lookup(source, name) {
|
|
22
|
+
if (source[name] !== undefined)
|
|
23
|
+
return source[name];
|
|
24
|
+
const upper = name.toUpperCase();
|
|
25
|
+
for (const [key, value] of Object.entries(source))
|
|
26
|
+
if (key.toUpperCase() === upper && value !== undefined)
|
|
27
|
+
return value;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
function systemRootOf(source) { return lookup(source, 'SystemRoot') ?? lookup(source, 'windir') ?? 'C:\\Windows'; }
|
|
31
|
+
/**
|
|
32
|
+
* The host PATH with the Windows system folders first. ⛔ A relative entry (`.`, `bin`) resolves against whatever the
|
|
33
|
+
* working directory is — a planting vector — and is dropped, as are empty entries and duplicates (case-insensitive,
|
|
34
|
+
* trailing separator ignored). Git's `usr/bin` before System32 is what made the SDK's `whoami` print GNU usage.
|
|
35
|
+
*/
|
|
36
|
+
export function systemFirstPath(source = process.env) {
|
|
37
|
+
const root = systemRootOf(source).replace(/[\\/]+$/u, '');
|
|
38
|
+
const front = [`${root}\\System32`, root, `${root}\\System32\\Wbem`, `${root}\\System32\\WindowsPowerShell\\v1.0\\`];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
return [...front, ...String(lookup(source, 'PATH') ?? '').split(';')].filter(entry => {
|
|
41
|
+
if (!entry || !/^(?:[A-Za-z]:[\\/]|\\\\)/u.test(entry))
|
|
42
|
+
return false;
|
|
43
|
+
const key = entry.replace(/[\\/]+$/u, '').toLowerCase();
|
|
44
|
+
if (seen.has(key))
|
|
45
|
+
return false;
|
|
46
|
+
seen.add(key);
|
|
47
|
+
return true;
|
|
48
|
+
}).join(';');
|
|
49
|
+
}
|
|
50
|
+
/** Only the allowlisted names this host defines, PATH always (System32 at least). A fresh object, never `process.env`. */
|
|
51
|
+
export function mxcChildEnvironment(source = process.env) {
|
|
52
|
+
const environment = {};
|
|
53
|
+
for (const name of MXC_CHILD_ENVIRONMENT_NAMES) {
|
|
54
|
+
if (name === 'PATH') {
|
|
55
|
+
environment.PATH = systemFirstPath(source);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const value = lookup(source, name);
|
|
59
|
+
if (value !== undefined)
|
|
60
|
+
environment[name] = value;
|
|
61
|
+
}
|
|
62
|
+
return environment;
|
|
63
|
+
}
|
|
64
|
+
/*
|
|
65
|
+
* R0 lane S, decision 4 (owner, 2026-09-23; gate E2): Win32k is allowed. `allowWindows:false` becomes
|
|
66
|
+
* `DisallowWin32kSystemCalls`, and measured here node.exe and powershell.exe then exit 0xC0000142
|
|
67
|
+
* (STATUS_DLL_INIT_FAILED) before their first instruction; Codex keeps `ui.disable:false` because "PowerShell needs
|
|
68
|
+
* Win32k and desktop handles during DLL startup" (codex-rs/mxc-sandbox policy.rs, 2026-09-22). Clipboard and input
|
|
69
|
+
* injection stay blocked; filesystem and network grants are unchanged.
|
|
70
|
+
*/
|
|
71
|
+
export const MXC_UI_POLICY = Object.freeze({ allowWindows: true, clipboard: 'none', allowInputInjection: false });
|
|
72
|
+
/*
|
|
73
|
+
* ⭐⭐ R0 lane S — S4, the SDK's import-time `whoami` (review E-08, gate E5).
|
|
74
|
+
* `@microsoft/mxc-sdk@0.8.0` `dist/diagnostic.js` runs `execSync('whoami /user /fo csv /nh')` WHEN THE MODULE LOADS,
|
|
75
|
+
* through cmd, the inherited PATH and the current directory, with stderr inherited. Measured: with Git's `usr/bin`
|
|
76
|
+
* before System32 every load printed `whoami: extra operand '/user'` on TALOS's stderr; with a `whoami.bat` in the
|
|
77
|
+
* working directory and NoDefaultCurrentDirectoryInExePath unset (the Windows default) that batch file RAN on the
|
|
78
|
+
* host. So the SDK is loaded once per process, only when a sandbox is asked for, with PATH System32-first and the
|
|
79
|
+
* current-directory search off for the length of the import; both are put back exactly afterwards, and only if nobody
|
|
80
|
+
* else changed them meanwhile.
|
|
81
|
+
*/
|
|
82
|
+
let sdkModule;
|
|
83
|
+
async function importSdkWithSystemSearch() {
|
|
84
|
+
if (process.platform !== 'win32')
|
|
85
|
+
return import('@microsoft/mxc-sdk');
|
|
86
|
+
const FLAG = 'NoDefaultCurrentDirectoryInExePath';
|
|
87
|
+
const previousPath = process.env.PATH;
|
|
88
|
+
const previousFlag = process.env[FLAG];
|
|
89
|
+
const guardedPath = systemFirstPath(process.env);
|
|
90
|
+
process.env.PATH = guardedPath;
|
|
91
|
+
process.env[FLAG] = '1';
|
|
92
|
+
try {
|
|
93
|
+
return await import('@microsoft/mxc-sdk');
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
if (process.env.PATH === guardedPath) {
|
|
97
|
+
if (previousPath === undefined)
|
|
98
|
+
delete process.env.PATH;
|
|
99
|
+
else
|
|
100
|
+
process.env.PATH = previousPath;
|
|
101
|
+
}
|
|
102
|
+
if (process.env[FLAG] === '1') {
|
|
103
|
+
if (previousFlag === undefined)
|
|
104
|
+
delete process.env[FLAG];
|
|
105
|
+
else
|
|
106
|
+
process.env[FLAG] = previousFlag;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function loadMxcSdk() {
|
|
111
|
+
const sdk = await (sdkModule ??= importSdkWithSystemSearch().catch(error => { sdkModule = undefined; throw error; }));
|
|
112
|
+
return {
|
|
113
|
+
getPlatformSupport: () => sdk.getPlatformSupport(),
|
|
114
|
+
createConfigFromPolicy: (policy, containment, name) => sdk.createConfigFromPolicy(policy, containment, name),
|
|
115
|
+
spawnSandboxFromConfig: (config, options, workingDirectory, environment) => sdk.spawnSandboxFromConfig(config, options, workingDirectory, environment)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function quoteWindowsArgument(value) {
|
|
119
|
+
if (value.length > 0 && !/[\s"]/u.test(value))
|
|
120
|
+
return value;
|
|
121
|
+
let quoted = '"';
|
|
122
|
+
let slashes = 0;
|
|
123
|
+
for (const character of value) {
|
|
124
|
+
if (character === '\\') {
|
|
125
|
+
slashes++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (character === '"') {
|
|
129
|
+
quoted += '\\'.repeat(slashes * 2 + 1) + '"';
|
|
130
|
+
slashes = 0;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
quoted += '\\'.repeat(slashes) + character;
|
|
134
|
+
slashes = 0;
|
|
135
|
+
}
|
|
136
|
+
return quoted + '\\'.repeat(slashes * 2) + '"';
|
|
137
|
+
}
|
|
138
|
+
/** Shared so the probe's sandboxed executor is quoted by the same rule as a brokered command. */
|
|
139
|
+
export function windowsCommandLine(parts) { return parts.map(quoteWindowsArgument).join(' '); }
|
|
140
|
+
/*
|
|
141
|
+
* ⛔⛔ R0 lane S — cmd.exe does NOT parse MSVCRT quoting. Measured 2026-09-23 on the host, no sandbox involved:
|
|
142
|
+
* `windowsCommandLine(['/d','/s','/c','node -e "console.log(1)"'])` gives `/d /s /c "node -e \"console.log(1)\""`,
|
|
143
|
+
* cmd strips the outer quotes and hands node `\"console.log(1)\"` — node evaluates a string literal, prints nothing,
|
|
144
|
+
* exits 0. Every brokered command with a double quote in it (a `git commit -m "…"`) ran CHANGED. Node's own
|
|
145
|
+
* `spawn(cmd,{shell:true})` on Windows builds `<ComSpec> /d /s /c "<command>"` verbatim (lib/child_process.js),
|
|
146
|
+
* and with `/s` cmd removes exactly the first and the last quote (`cmd /?`). That is the form used here for the
|
|
147
|
+
* brokered shell invocation, and only for it: the request keeps its argv shape.
|
|
148
|
+
*/
|
|
149
|
+
function isCmdInvocation(input) {
|
|
150
|
+
const { executable, args } = input.request;
|
|
151
|
+
return /(?:^|[\\/])cmd(?:\.exe)?$/iu.test(executable) && args.length === 4 && args[0] === '/d' && args[1] === '/s' && args[2] === '/c';
|
|
152
|
+
}
|
|
153
|
+
function commandLine(input) {
|
|
154
|
+
if (isCmdInvocation(input))
|
|
155
|
+
return `${windowsCommandLine([input.request.executable])} /d /s /c "${input.request.args[3]}"`;
|
|
156
|
+
return windowsCommandLine([input.request.executable, ...input.request.args]);
|
|
157
|
+
}
|
|
158
|
+
function policyFor(input) {
|
|
159
|
+
const policy = input.windowsSandboxPolicy;
|
|
160
|
+
if (!policy)
|
|
161
|
+
throw new Error('MXC_WINDOWS_SANDBOX_POLICY_REQUIRED');
|
|
162
|
+
const networkDefault = input.request.network === 'internet-client' ? 'allow' : 'deny';
|
|
163
|
+
return {
|
|
164
|
+
version: '0.8.0-alpha',
|
|
165
|
+
filesystem: { readwritePaths: policy.fsReadWrite, readonlyPaths: policy.fsReadOnly, clearPolicyOnExit: true },
|
|
166
|
+
network: { egress: { default: networkDefault }, ingress: { default: 'deny', hostLoopback: 'deny' } },
|
|
167
|
+
ui: { ...MXC_UI_POLICY },
|
|
168
|
+
...(input.request.timeoutMs === undefined ? {} : { timeoutMs: input.request.timeoutMs })
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function collectChild(child, input, maxOutputBytes, applied) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
let stdout = Buffer.alloc(0);
|
|
174
|
+
let stderr = Buffer.alloc(0);
|
|
175
|
+
let outputBytes = 0;
|
|
176
|
+
let settled = false;
|
|
177
|
+
const signal = input.request.signal;
|
|
178
|
+
const cleanup = () => {
|
|
179
|
+
if (timer)
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
signal?.removeEventListener('abort', onAbort);
|
|
182
|
+
child.stdout?.off('data', onStdout);
|
|
183
|
+
child.stderr?.off('data', onStderr);
|
|
184
|
+
child.off('error', onError);
|
|
185
|
+
child.off('close', onClose);
|
|
186
|
+
};
|
|
187
|
+
const finishReject = (error) => { if (settled)
|
|
188
|
+
return; settled = true; cleanup(); try {
|
|
189
|
+
child.kill();
|
|
190
|
+
}
|
|
191
|
+
catch { } reject(error); };
|
|
192
|
+
const append = (target, chunk) => {
|
|
193
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
194
|
+
if (outputBytes + buffer.length > maxOutputBytes) {
|
|
195
|
+
finishReject(new Error('MXC_OUTPUT_LIMIT_EXCEEDED'));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
outputBytes += buffer.length;
|
|
199
|
+
if (target === 'stdout')
|
|
200
|
+
stdout = Buffer.concat([stdout, buffer]);
|
|
201
|
+
else
|
|
202
|
+
stderr = Buffer.concat([stderr, buffer]);
|
|
203
|
+
};
|
|
204
|
+
const onStdout = (chunk) => append('stdout', chunk);
|
|
205
|
+
const onStderr = (chunk) => append('stderr', chunk);
|
|
206
|
+
const onError = (cause) => finishReject(cause);
|
|
207
|
+
const onClose = (code) => { if (settled)
|
|
208
|
+
return; settled = true; cleanup(); resolve({ exitCode: code ?? -1, stdout: stdout.toString('utf8'), stderr: stderr.toString('utf8'), applied }); };
|
|
209
|
+
const onAbort = () => finishReject(new Error('MXC_EXECUTION_CANCELLED'));
|
|
210
|
+
const timer = input.request.timeoutMs === undefined ? undefined : setTimeout(() => finishReject(new Error('MXC_EXECUTION_TIMEOUT')), input.request.timeoutMs);
|
|
211
|
+
timer?.unref();
|
|
212
|
+
child.stdout?.on('data', onStdout);
|
|
213
|
+
child.stderr?.on('data', onStderr);
|
|
214
|
+
child.once('error', onError);
|
|
215
|
+
child.once('close', onClose);
|
|
216
|
+
if (signal?.aborted)
|
|
217
|
+
onAbort();
|
|
218
|
+
else
|
|
219
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
export function createMxcExecutionBackend(options = {}) {
|
|
223
|
+
const platform = options.platform ?? process.platform;
|
|
224
|
+
const maxOutputBytes = options.maxOutputBytes ?? 8 * 1024 * 1024;
|
|
225
|
+
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1)
|
|
226
|
+
throw new Error('MXC_OUTPUT_LIMIT_INVALID');
|
|
227
|
+
let sdkPromise;
|
|
228
|
+
// inspect() runs for both available() and execute(); without this memo every command would spawn a probe tree.
|
|
229
|
+
let probePromise;
|
|
230
|
+
const getSdk = () => sdkPromise ??= options.sdk ? Promise.resolve(options.sdk) : (options.sdkLoader ?? loadMxcSdk)();
|
|
231
|
+
const inspect = async () => {
|
|
232
|
+
if (platform !== 'win32')
|
|
233
|
+
return { available: false, reason: 'MXC BaseContainer enforcement requires Windows' };
|
|
234
|
+
let sdk;
|
|
235
|
+
try {
|
|
236
|
+
sdk = await getSdk();
|
|
237
|
+
}
|
|
238
|
+
catch (cause) {
|
|
239
|
+
return { available: false, reason: `MXC SDK failed to load: ${cause instanceof Error ? cause.message : String(cause)}` };
|
|
240
|
+
}
|
|
241
|
+
const evidence = sdk.getPlatformSupport();
|
|
242
|
+
if (!evidence.isSupported)
|
|
243
|
+
return { available: false, reason: evidence.reason ?? 'MXC is unsupported on this host' };
|
|
244
|
+
if (!evidence.availableMethods.includes('processcontainer'))
|
|
245
|
+
return { available: false, reason: 'MXC ProcessContainer is unavailable' };
|
|
246
|
+
if (evidence.isolationTier !== 'base-container')
|
|
247
|
+
return { available: false, reason: `MXC isolation tier ${evidence.isolationTier ?? 'unknown'} does not prove Experimental_CreateProcessInSandbox` };
|
|
248
|
+
if (!options.verifyProcessTreeKillOnClose)
|
|
249
|
+
return { available: false, reason: 'MXC process-tree kill-on-close has not been verified on this host' };
|
|
250
|
+
const verify = options.verifyProcessTreeKillOnClose;
|
|
251
|
+
let probe;
|
|
252
|
+
try {
|
|
253
|
+
probe = await (probePromise ??= Promise.resolve(verify(evidence)));
|
|
254
|
+
}
|
|
255
|
+
catch (cause) {
|
|
256
|
+
return { available: false, reason: `MXC process-tree probe failed: ${cause instanceof Error ? cause.message : String(cause)}` };
|
|
257
|
+
}
|
|
258
|
+
if (typeof probe !== 'object' || probe === null || typeof probe.verified !== 'boolean' || typeof probe.evidence !== 'object' || probe.evidence === null)
|
|
259
|
+
return { available: false, reason: 'MXC process-tree probe returned an unrecognized result: the seam is a measurement, not a boolean' };
|
|
260
|
+
if (!probe.verified)
|
|
261
|
+
return { available: false, reason: probe.reason ?? 'MXC process-tree kill-on-close probe failed' };
|
|
262
|
+
return { available: true, probe };
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
name: 'microsoft-mxc-0.8.0', level: 'windows-sandbox', available: inspect,
|
|
266
|
+
async execute(input) {
|
|
267
|
+
const availability = await inspect();
|
|
268
|
+
if (!availability.available)
|
|
269
|
+
throw Object.assign(new Error(availability.reason), { code: 'MXC_BACKEND_UNAVAILABLE' }); /* R2 (gate E6): code on error.code, printed once */
|
|
270
|
+
const sdk = await getSdk();
|
|
271
|
+
const sandboxPolicy = policyFor(input);
|
|
272
|
+
const identity = input.windowsSandboxPolicy?.identity;
|
|
273
|
+
const config = sdk.createConfigFromPolicy(sandboxPolicy, 'process', identity);
|
|
274
|
+
config.process = { ...(config.process ?? { commandLine: '' }), commandLine: commandLine(input), cwd: input.request.cwd };
|
|
275
|
+
/* ⛔ R0 lane S: never the backend default (the user profile block, secret-shaped names included, measured): the allowlist. */
|
|
276
|
+
const child = sdk.spawnSandboxFromConfig(config, { usePty: false, experimental: true }, input.request.cwd, input.request.environment ?? mxcChildEnvironment());
|
|
277
|
+
const applied = { enforcement: 'windows-sandbox', appContainer: true, filesystem: 'bfs', network: input.request.network, processTree: 'job-object-kill-on-close', processTreeEvidence: availability.probe.evidence, sandboxSpecVersion: '0.1.0' };
|
|
278
|
+
return collectChild(child, input, maxOutputBytes, applied);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { compileRuleSet, evaluateRules } from "./evaluate.js";
|
|
4
|
+
import { ruleToPersist, unreadableRulesError } from "./persist.js";
|
|
5
|
+
import { isReadOnlyShellCommand, parseRule, tokenizeCommand } from "./rule-parser.js";
|
|
6
|
+
const FS_EDIT_OPTIONS = {
|
|
7
|
+
mkdir: new Set(['-p', '-v']), touch: new Set(['-a', '-m', '-c']), rm: new Set(['-r', '-R', '-f', '-rf', '-fr', '-d']), rmdir: new Set(['-p']), cp: new Set(['-r', '-R', '-f', '-n', '-p']), mv: new Set(['-f', '-n']),
|
|
8
|
+
};
|
|
9
|
+
function insideProject(root, candidate) { const rel = path.relative(path.resolve(root), path.resolve(candidate)); return rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel)); }
|
|
10
|
+
export function acceptEditsFilesystemCommand(command, projectRoot) {
|
|
11
|
+
const tokens = tokenizeCommand(command);
|
|
12
|
+
if (tokens.length < 2)
|
|
13
|
+
return false;
|
|
14
|
+
const head = tokens[0];
|
|
15
|
+
const allowed = FS_EDIT_OPTIONS[head];
|
|
16
|
+
if (!allowed)
|
|
17
|
+
return false;
|
|
18
|
+
const operands = [];
|
|
19
|
+
let options = true;
|
|
20
|
+
for (const token of tokens.slice(1)) {
|
|
21
|
+
if (options && token === '--') {
|
|
22
|
+
options = false;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (options && token.startsWith('-')) {
|
|
26
|
+
if (!allowed.has(token))
|
|
27
|
+
return false;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
options = false;
|
|
31
|
+
operands.push(token);
|
|
32
|
+
}
|
|
33
|
+
if ((head === 'cp' || head === 'mv') ? operands.length < 2 : operands.length < 1)
|
|
34
|
+
return false;
|
|
35
|
+
for (const operand of operands) {
|
|
36
|
+
const abs = path.isAbsolute(operand) ? path.resolve(operand) : path.resolve(projectRoot, operand);
|
|
37
|
+
if (!insideProject(projectRoot, abs) || abs === path.resolve(projectRoot))
|
|
38
|
+
return false;
|
|
39
|
+
const rel = path.relative(projectRoot, abs).split(path.sep);
|
|
40
|
+
if (rel[0] === '.git' || rel[0] === '.talos-cli')
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const PROTECTED_PROJECT_ROOTS = new Set(['.git', '.talos-cli', '.github', '.vscode', '.idea', '.husky']);
|
|
46
|
+
function protectedProjectMutation(action, root) {
|
|
47
|
+
if (!['Write', 'Edit'].includes(action.tool) || !action.resource)
|
|
48
|
+
return false;
|
|
49
|
+
const abs = path.isAbsolute(action.resource) ? path.resolve(action.resource) : path.resolve(root, action.resource);
|
|
50
|
+
if (!insideProject(root, abs))
|
|
51
|
+
return false;
|
|
52
|
+
const first = path.relative(path.resolve(root), abs).split(path.sep)[0] ?? '';
|
|
53
|
+
return PROTECTED_PROJECT_ROOTS.has(first);
|
|
54
|
+
}
|
|
55
|
+
function outsideWrite(action, root) { if (!['Write', 'Edit'].includes(action.tool) || !action.resource)
|
|
56
|
+
return false; const rel = path.relative(path.resolve(root), path.resolve(root, action.resource)); return rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel); }
|
|
57
|
+
export function createPermissionEngine({ projectRoot, rules, mode, classifier, persistRule }) {
|
|
58
|
+
if (mode === 'auto' && !classifier)
|
|
59
|
+
throw new Error('PERMISSION_AUTO_UNAVAILABLE');
|
|
60
|
+
// B1 slice 21. ⛔ Fail closed and by name: one unreadable entry, in any list, and there is no engine.
|
|
61
|
+
const { rules: compiled, invalid } = compileRuleSet(rules);
|
|
62
|
+
if (invalid.length)
|
|
63
|
+
throw unreadableRulesError(projectRoot, invalid);
|
|
64
|
+
const pending = new Map();
|
|
65
|
+
const grants = [];
|
|
66
|
+
return { async decide(action, context) {
|
|
67
|
+
if (outsideWrite(action, projectRoot))
|
|
68
|
+
return { kind: 'deny', code: 'PATH_NOT_ALLOWED', message: 'Operation targets a path outside the project' };
|
|
69
|
+
let base = evaluateRules(compiled, action, { projectRoot });
|
|
70
|
+
if (base.effect === 'deny')
|
|
71
|
+
return { kind: 'deny', code: 'PERMISSION_DENIED', message: base.reason };
|
|
72
|
+
const m = context.mode;
|
|
73
|
+
if (m === 'plan' && (['Write', 'Edit', 'Hook', 'Plugin'].includes(action.tool) || (action.tool === 'Bash' && !isReadOnlyShellCommand(action.command ?? ''))))
|
|
74
|
+
return { kind: 'deny', code: 'PLAN_MODE_READ_ONLY', message: 'Plan mode permits only read-only operations' };
|
|
75
|
+
if (grants.some(g => JSON.stringify(g) === JSON.stringify(action)))
|
|
76
|
+
return { kind: 'allow', source: 'session' };
|
|
77
|
+
if (base.effect === 'allow')
|
|
78
|
+
return { kind: 'allow', source: base.rule ?? 'rule' };
|
|
79
|
+
if (m === 'acceptEdits' && base.effect === 'ask' && !base.rule && !protectedProjectMutation(action, projectRoot) && (['Write', 'Edit'].includes(action.tool) || (action.tool === 'Bash' && acceptEditsFilesystemCommand(action.command ?? '', projectRoot))))
|
|
80
|
+
return { kind: 'allow', source: 'acceptEdits' };
|
|
81
|
+
if (m === 'bypassPermissions' && base.effect === 'ask' && !base.rule)
|
|
82
|
+
return { kind: 'allow', source: 'bypassPermissions' };
|
|
83
|
+
if (m === 'auto' && base.effect === 'ask' && !base.rule && !protectedProjectMutation(action, projectRoot)) {
|
|
84
|
+
if (!classifier)
|
|
85
|
+
throw new Error('PERMISSION_AUTO_UNAVAILABLE');
|
|
86
|
+
const c = await classifier({ action, projectRoot });
|
|
87
|
+
if (c === 'allow')
|
|
88
|
+
return { kind: 'allow', source: 'auto' };
|
|
89
|
+
if (c === 'deny')
|
|
90
|
+
return { kind: 'deny', code: 'PERMISSION_AUTO_DENIED', message: 'Automatic classifier denied operation' };
|
|
91
|
+
base = { effect: 'ask', reason: 'automatic classifier requested confirmation' };
|
|
92
|
+
}
|
|
93
|
+
if (m === 'dontAsk' || !context.interactive)
|
|
94
|
+
return { kind: 'deny', code: 'PERMISSION_REQUIRED', message: 'Operation requires approval in non-interactive mode' };
|
|
95
|
+
const requestId = randomUUID();
|
|
96
|
+
pending.set(requestId, action);
|
|
97
|
+
return { kind: 'ask', requestId, action, reason: base.reason, ...(base.rule ? { rule: base.rule } : {}) };
|
|
98
|
+
}, async resolve(requestId, decision) {
|
|
99
|
+
const action = pending.get(requestId);
|
|
100
|
+
if (!action)
|
|
101
|
+
throw new Error('APPROVAL_NOT_PENDING');
|
|
102
|
+
pending.delete(requestId);
|
|
103
|
+
if (decision === 'allow-session')
|
|
104
|
+
grants.push(action);
|
|
105
|
+
if ((decision === 'allow-always' || decision === 'deny-always') && persistRule) {
|
|
106
|
+
const effect = decision === 'allow-always' ? 'allow' : 'deny';
|
|
107
|
+
// B1 slice 21. A rule the writing path refuses — a compound command, a line break, a rule that would
|
|
108
|
+
// not match its own request, an allow that narrows nothing — is known before the key is pressed (the
|
|
109
|
+
// full review previews the same refusal). Nothing is written, and the approval is still answered:
|
|
110
|
+
// measured at 805c89a0, the refusal threw out of resolve and the runtime's request was never answered.
|
|
111
|
+
// "Allow always" then answers for this session only, for the identical request, which is what Hermes
|
|
112
|
+
// does when "always" cannot be honoured; "deny always" denies this request and asks again next time.
|
|
113
|
+
let rule;
|
|
114
|
+
try {
|
|
115
|
+
rule = parseRule(ruleToPersist(effect, action, projectRoot), effect);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
if (effect === 'allow')
|
|
119
|
+
grants.push(action);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await persistRule(effect, action);
|
|
123
|
+
// ⛔ The rule is obeyed from now on, not from the next start: before this, the engine compiled its rules
|
|
124
|
+
// once and "allow always" asked again for the identical command in the same session.
|
|
125
|
+
compiled[effect].push(rule);
|
|
126
|
+
// ⛔ And the session's rule set shows it. The approval dialog explains each request from the rule set
|
|
127
|
+
// this engine was built from (`rules`, the same object); without the rule there, measured: after
|
|
128
|
+
// "allow always" on `npm test`, `npm test && npm publish` asked for segment 2 while the account
|
|
129
|
+
// said segment 1, and the dialog withheld every row as a "possible race". A frozen set is left alone.
|
|
130
|
+
try {
|
|
131
|
+
const list = rules[effect];
|
|
132
|
+
if (Array.isArray(list) && !list.includes(rule.raw))
|
|
133
|
+
list.push(rule.raw);
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
}
|
|
137
|
+
} };
|
|
138
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { compileRules, evaluateRules } from "./evaluate.js";
|
|
2
|
+
import { segmentShellCommand } from "./shell-segmentation.js";
|
|
3
|
+
function explanationForDecision(decision, segments, decisiveSegment) {
|
|
4
|
+
return { effect: decision.effect, reason: decision.reason, ...(decision.rule ? { rule: decision.rule } : {}), precedence: 'deny > ask > allow', decisiveSegment, segments };
|
|
5
|
+
}
|
|
6
|
+
export function explainPermission(rules, action, { projectRoot }) {
|
|
7
|
+
if (action.tool !== 'Bash')
|
|
8
|
+
return explanationForDecision(evaluateRules(rules, action, { projectRoot }), [], null);
|
|
9
|
+
let commandSegments;
|
|
10
|
+
try {
|
|
11
|
+
commandSegments = segmentShellCommand(action.command ?? '');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return explanationForDecision({ effect: 'deny', reason: 'unsupported shell composition' }, [], null);
|
|
15
|
+
}
|
|
16
|
+
const segments = commandSegments.map(segment => {
|
|
17
|
+
const decision = evaluateRules(rules, { ...action, command: segment.command }, { projectRoot });
|
|
18
|
+
return { command: segment.command, operatorBefore: segment.operatorBefore, effect: decision.effect, reason: decision.reason, ...(decision.rule ? { rule: decision.rule } : {}) };
|
|
19
|
+
});
|
|
20
|
+
for (const effect of ['deny', 'ask', 'allow']) {
|
|
21
|
+
const decisiveSegment = segments.findIndex(segment => segment.effect === effect);
|
|
22
|
+
if (decisiveSegment >= 0) {
|
|
23
|
+
const decisive = segments[decisiveSegment];
|
|
24
|
+
return explanationForDecision({ effect: decisive.effect, reason: decisive.reason, ...(decisive.rule ? { rule: decisive.rule } : {}) }, segments, decisiveSegment);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return explanationForDecision({ effect: 'deny', reason: 'unsupported shell composition' }, segments, null);
|
|
28
|
+
}
|
|
29
|
+
export function simulatePermission({ rules, action, projectRoot }) {
|
|
30
|
+
const explanation = explainPermission(compileRules(rules), action, { projectRoot });
|
|
31
|
+
return { effect: explanation.effect, requiresApproval: explanation.effect === 'ask', explanation };
|
|
32
|
+
}
|
|
33
|
+
export function formatPermissionSimulation(simulation) {
|
|
34
|
+
const explanation = simulation.explanation;
|
|
35
|
+
const lines = [
|
|
36
|
+
`Permission dry-run: ${simulation.effect}`,
|
|
37
|
+
`Precedence: ${explanation.precedence}`,
|
|
38
|
+
];
|
|
39
|
+
if (explanation.decisiveSegment !== null) {
|
|
40
|
+
const segment = explanation.segments[explanation.decisiveSegment];
|
|
41
|
+
lines.push(`Decisive: segment ${explanation.decisiveSegment + 1}${segment ? ` · ${segment.command}` : ''} · ${explanation.reason}`);
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
lines.push(`Decisive: ${explanation.reason}`);
|
|
45
|
+
if (explanation.rule)
|
|
46
|
+
lines.push(`Rule: ${explanation.rule}`);
|
|
47
|
+
if (explanation.segments.length) {
|
|
48
|
+
lines.push('Segments:');
|
|
49
|
+
explanation.segments.forEach((segment, index) => {
|
|
50
|
+
lines.push(` ${index + 1}. ${segment.effect} · ${segment.command}${segment.rule ? ` · ${segment.rule}` : ''} · ${segment.reason}`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n') + '\n';
|
|
54
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { projectId, resolveCliPaths } from "../paths.js";
|
|
5
|
+
import { readConfigScope, writeConfigScope } from "../config/load.js";
|
|
6
|
+
import { patternNamesOnlyAVerb, ruleMatchesAction } from "./evaluate.js";
|
|
7
|
+
import { parseRule, ruleProblem } from "./rule-parser.js";
|
|
8
|
+
import { segmentShellCommand } from "./shell-segmentation.js";
|
|
9
|
+
function normalizeResource(resource, root) { const abs = isAbsolute(resource) ? resolve(resource) : resolve(root, resource); const rel = relative(resolve(root), abs); if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
10
|
+
throw new Error('PATH_NOT_ALLOWED'); return rel.split(sep).join('/'); }
|
|
11
|
+
function spelledRule(a, root) { if (a.tool === 'Bash')
|
|
12
|
+
return `Bash(${a.command ?? ''})`; if (['Read', 'Write', 'Edit'].includes(a.tool))
|
|
13
|
+
return `${a.tool}(${normalizeResource(a.resource ?? '', root)})`; if (a.tool === 'Mcp')
|
|
14
|
+
return `Mcp(${a.serverId ?? ''}:${a.operation ?? '*'})`; if (a.tool === 'TalosService')
|
|
15
|
+
return `TalosService(${a.resource ?? ''}:${a.operation ?? '*'})`; return `${a.tool}(${a.operation ?? a.resource ?? '*'})`; }
|
|
16
|
+
/*
|
|
17
|
+
* B1 slice 21 — A RULE IS WRITTEN ONLY IF IT CAN BE READ BACK AND DECIDES THE REQUEST IT WAS WRITTEN FOR.
|
|
18
|
+
*
|
|
19
|
+
* Recorded at e5d197e7 and re-measured at 805c89a0: "allow always" on `ls<LF>rm -rf /` wrote `Bash(ls<LF>rm -rf /)`, which the
|
|
20
|
+
* parser cannot read, and the next start threw; on `git status && npm test; npm publish` it wrote the
|
|
21
|
+
* whole command, which matches none of the segments the engine judges one at a time.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ A compound command is not remembered at all. Measured in the two competitors before deciding:
|
|
24
|
+
* - Claude Code 2.1.274 (claude.exe) offers one rule per subcommand that still asks, capped at five, each
|
|
25
|
+
* the exact subcommand or a prefix ending in ` *` (`git status *`), and labels the row "don't ask again
|
|
26
|
+
* for A and B commands" from exactly the rules it writes: shown, but a prefix is broader than the ask.
|
|
27
|
+
* - Hermes Agent (0.20.0 and main f5d19261) persists the dangerous-pattern CATEGORY ("recursive
|
|
28
|
+
* delete"), and its command-text allowlist refuses any command with a shell operator; when "always"
|
|
29
|
+
* cannot be honoured it answers "session" instead.
|
|
30
|
+
* A rule per segment here would allow `rm -rf build` inside any other command, and the approval dialog
|
|
31
|
+
* previews exactly one rule, so it could not show what was written. Declining is the only form that
|
|
32
|
+
* neither widens nor hides, and the engine then answers for this session only, as Hermes does.
|
|
33
|
+
*/
|
|
34
|
+
const ALWAYS_ANSWERS = '"allow always" answers for this session only';
|
|
35
|
+
/* R2 (gate E6): the code travels on `error.code`; the message is the English sentence alone, so the failure line prints the
|
|
36
|
+
code once (review D-06). `codeForError` read the code from a `CODE: ` prefix before; the property keeps it identical. */
|
|
37
|
+
function ruleError(code, message) { return Object.assign(new Error(message), { code }); }
|
|
38
|
+
export function permissionActionToRule(a, root) {
|
|
39
|
+
if (a.tool === 'Bash') {
|
|
40
|
+
let segments;
|
|
41
|
+
try {
|
|
42
|
+
segments = segmentShellCommand(a.command ?? '');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw ruleError('RULE_COMMAND_UNSUPPORTED', `this command cannot be split into the segments the engine judges, so no rule is written for it; ${ALWAYS_ANSWERS}`);
|
|
46
|
+
}
|
|
47
|
+
if (segments.length > 1)
|
|
48
|
+
throw ruleError('RULE_COMPOUND_COMMAND', `a command of ${segments.length} segments is not remembered, because a rule for the whole command matches none of its segments and a rule for each segment would allow it inside any other command; ${ALWAYS_ANSWERS}`);
|
|
49
|
+
}
|
|
50
|
+
const rule = spelledRule(a, root);
|
|
51
|
+
let compiled;
|
|
52
|
+
try {
|
|
53
|
+
compiled = parseRule(rule, 'allow');
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw ruleError('RULE_UNREADABLE', `the rule for this request ${ruleProblem(rule) ?? 'cannot be parsed'}, so it could not be read back and is not written; ${ALWAYS_ANSWERS}`);
|
|
57
|
+
}
|
|
58
|
+
// A verb-only pattern (`cat:*`, `*`) is slice 11's case and keeps slice 11's answer: an allow is refused as
|
|
59
|
+
// RULE_NOT_NARROWED by the writer, and a deny that covers the verb is the safe direction and is written.
|
|
60
|
+
if (!(a.tool === 'Bash' && patternNamesOnlyAVerb(compiled.pattern)) && !ruleMatchesAction(compiled, a, root))
|
|
61
|
+
throw ruleError('RULE_MATCHES_NOTHING', `the rule for this request would not match the request itself, so it is not written; ${ALWAYS_ANSWERS}`);
|
|
62
|
+
return rule;
|
|
63
|
+
}
|
|
64
|
+
// B1 slice 11. Gemini CLI refuses to *construct* a persistent allow rule for its shell, read, write
|
|
65
|
+
// and search tools unless the rule carries an argument-narrowing pattern — a property of the
|
|
66
|
+
// writing interface, so it binds a hand-written administrator rule too and not only the
|
|
67
|
+
// "don't ask again" button. Measured on this host before the change: this path could produce
|
|
68
|
+
// `Bash(*)`, `Bash(**)` and `Bash(cat:*)`, because the rule is the command text and a command whose
|
|
69
|
+
// own text is `*` or ends in `:*` becomes exactly the blanket form. A deny that covers everything
|
|
70
|
+
// is the safe direction and is still written; only an `allow` that narrows nothing is refused.
|
|
71
|
+
function bashRuleNarrowsNothing(rule) { const m = /^Bash\((.*)\)$/u.exec(rule); if (!m)
|
|
72
|
+
return false; const pattern = m[1]; return pattern === '' || pattern === '*' || pattern === '**' || /:\*$/u.test(pattern); }
|
|
73
|
+
// B1 slice 21. The rule "always" would write, or the refusal that writes nothing. One function, so the engine answers
|
|
74
|
+
// exactly the refusals the writer makes: before this slice an allow that narrows nothing threw out of the
|
|
75
|
+
// engine's resolve, and the runtime's approval was never answered.
|
|
76
|
+
export function ruleToPersist(effect, action, root) {
|
|
77
|
+
const rule = permissionActionToRule(action, root);
|
|
78
|
+
if (effect === 'allow' && bashRuleNarrowsNothing(rule))
|
|
79
|
+
throw new Error(`RULE_NOT_NARROWED:${rule}`);
|
|
80
|
+
return rule;
|
|
81
|
+
}
|
|
82
|
+
// ⛔ B1 slice 21. Only the list being changed is written. Measured at 805c89a0: this function wrote all
|
|
83
|
+
// three lists into the project-user file, and a list in a later configuration file REPLACES the list of
|
|
84
|
+
// an earlier one, so one "allow always" on `npm test` wrote `deny: []` and switched off the deny rules of
|
|
85
|
+
// the project's own `.talos-cli/config.json` (`curl evil.example` went from deny to ask).
|
|
86
|
+
export async function persistPermissionRule({ paths, projectRoot, effect, action }) {
|
|
87
|
+
const rule = ruleToPersist(effect, action, projectRoot);
|
|
88
|
+
const current = await readConfigScope({ paths, projectRoot, scope: 'project-user' });
|
|
89
|
+
const list = current.permissions?.[effect] ?? [];
|
|
90
|
+
if (!Array.isArray(list))
|
|
91
|
+
throw ruleError('RULE_STORE_INVALID', `"permissions.${effect}" in the project-user configuration is not a list`);
|
|
92
|
+
// The file keeps whatever other lists it already had; a partial `permissions` object is what the loader
|
|
93
|
+
// merges over the lower scopes, so the type's three required lists do not describe a patch.
|
|
94
|
+
await writeConfigScope({ paths, projectRoot, scope: 'project-user', patch: { permissions: { [effect]: [...new Set([...list, rule])] } } });
|
|
95
|
+
}
|
|
96
|
+
/*
|
|
97
|
+
* B1 slice 21 — A RULE THAT CANNOT BE READ STOPS THE START, BY NAME.
|
|
98
|
+
*
|
|
99
|
+
* Measured at 805c89a0, starting the interactive CLI with that rule on disk: Ink printed
|
|
100
|
+
* `ERROR PERMISSION_RULE_INVALID:Bash(ls` with the rule's own line break splitting the line, then a
|
|
101
|
+
* stack through parseRule, compileRules, createPermissionEngine and the React reconciler — and nothing
|
|
102
|
+
* said which file held the rule or what to do about it.
|
|
103
|
+
* ⛔ Skipping the rule is not the cure: a skipped deny rule silently allows what it denied (Claude Code
|
|
104
|
+
* 2.1.274 does skip an invalid rule with a warning, deny rules included). The engine refuses to exist.
|
|
105
|
+
*/
|
|
106
|
+
export class PermissionRulesUnreadableError extends Error {
|
|
107
|
+
code = 'PERMISSION_RULE_INVALID';
|
|
108
|
+
constructor(message) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = 'PermissionRulesUnreadableError';
|
|
111
|
+
// A configuration error is described completely by its message. Its frames point at this constructor,
|
|
112
|
+
// not at anything a person can fix, and the interactive screen prints every frame it is given.
|
|
113
|
+
this.stack = `${this.name}: ${message}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Highest precedence first: the loader overlays user, then project, then project-user, and a list in a
|
|
117
|
+
// later file replaces the list of an earlier one. So the first file that defines the list is the file the
|
|
118
|
+
// engine's list came from. The layout mirrors `files()` in config/load.ts; a test writes through the real
|
|
119
|
+
// writer into each scope and asserts this names that exact file, so a drift turns red.
|
|
120
|
+
function permissionConfigFiles(projectRoot) {
|
|
121
|
+
let paths = null;
|
|
122
|
+
try {
|
|
123
|
+
paths = resolveCliPaths(process.env, process.platform, homedir());
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
paths = null;
|
|
127
|
+
}
|
|
128
|
+
return [...(paths ? [join(paths.projectOverridesRoot, projectId(projectRoot), 'config.json')] : []), join(projectRoot, '.talos-cli', 'config.json'), ...(paths ? [join(paths.configRoot, 'config.json')] : [])];
|
|
129
|
+
}
|
|
130
|
+
// `undefined` when the file is absent, unreadable or has no `permissions`; `null` when `permissions` is there
|
|
131
|
+
// but is not an object of lists, which replaces every lower file's lists at once.
|
|
132
|
+
function storedPermissions(file) {
|
|
133
|
+
let value;
|
|
134
|
+
try {
|
|
135
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
if (!value || typeof value !== 'object' || !Object.hasOwn(value, 'permissions'))
|
|
141
|
+
return undefined;
|
|
142
|
+
const permissions = value.permissions;
|
|
143
|
+
return permissions && typeof permissions === 'object' && !Array.isArray(permissions) ? permissions : null;
|
|
144
|
+
}
|
|
145
|
+
function locate(projectRoot, invalid) {
|
|
146
|
+
const files = permissionConfigFiles(projectRoot);
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
const permissions = storedPermissions(file);
|
|
149
|
+
if (permissions === null)
|
|
150
|
+
return invalid.index === null ? { file, whole: true } : { searched: files };
|
|
151
|
+
if (!permissions || !Object.hasOwn(permissions, invalid.effect))
|
|
152
|
+
continue;
|
|
153
|
+
const list = permissions[invalid.effect];
|
|
154
|
+
const found = invalid.index === null ? JSON.stringify(list) === JSON.stringify(invalid.value) : Array.isArray(list) && JSON.stringify(list[invalid.index]) === JSON.stringify(invalid.value);
|
|
155
|
+
return found ? { file } : { searched: files };
|
|
156
|
+
}
|
|
157
|
+
return { searched: files };
|
|
158
|
+
}
|
|
159
|
+
// JSON spells the rule the way it appears in the file (a line break as the two characters `\n`), and
|
|
160
|
+
// what JSON leaves raw — DEL, C1, line and paragraph separators, bidi controls, invisible characters —
|
|
161
|
+
// becomes a visible `\uXXXX`, so nothing in the message can move a terminal's cursor or hide text.
|
|
162
|
+
const BACKSLASH = String.fromCharCode(92);
|
|
163
|
+
const TERMINAL_UNSAFE = /[\x00-\x1f\x7f-\x9f\p{Zl}\p{Zp}\p{Bidi_Control}\p{Default_Ignorable_Code_Point}]/gu;
|
|
164
|
+
function inert(text) { return text.replace(TERMINAL_UNSAFE, character => { const code = character.codePointAt(0); return code > 0xffff ? `${BACKSLASH}u{${code.toString(16)}}` : `${BACKSLASH}u${code.toString(16).padStart(4, '0')}`; }); }
|
|
165
|
+
function spelled(value) { return JSON.stringify(value) ?? String(value); }
|
|
166
|
+
export function unreadableRulesError(projectRoot, invalid) {
|
|
167
|
+
const first = invalid[0];
|
|
168
|
+
const where = locate(projectRoot, first);
|
|
169
|
+
const found = 'file' in where;
|
|
170
|
+
const whole = found && where.whole === true;
|
|
171
|
+
const list = whole ? '"permissions"' : `"permissions.${first.effect}"`;
|
|
172
|
+
const problem = whole ? 'is not an object holding the allow, ask and deny lists' : first.problem;
|
|
173
|
+
const subject = first.index === null ? list : `the ${first.effect} rule ${spelled(first.value)} in ${list}`;
|
|
174
|
+
const place = found ? ` of ${where.file}` : '';
|
|
175
|
+
const origin = found ? '' : ` It is not in ${where.searched.join(', ')}: these rules were handed to the engine directly.`;
|
|
176
|
+
const fix = !found ? 'Remove it where these rules come from, then start TALOS again.' : whole ? `Make ${list} in that file an object such as {"allow":[],"ask":[],"deny":[]}, then start TALOS again.` : first.index === null ? `Make ${list} in that file a list of rules, for example [], then start TALOS again.` : `Delete that entry from ${list} in that file, then start TALOS again.`;
|
|
177
|
+
const more = invalid.length > 1 ? ` ${invalid.length - 1} more permission ${invalid.length === 2 ? 'entry is' : 'entries are'} unreadable too.` : '';
|
|
178
|
+
return new PermissionRulesUnreadableError(inert(`Permission rules cannot be loaded: ${subject}${place} ${problem}.${origin} No rule is skipped, because a skipped deny rule would allow what it denies. ${fix}${more}`));
|
|
179
|
+
}
|