@fusengine/harness 0.1.89 → 0.1.91
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/dist/adapters/claude/index.d.mts +1 -1
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +2 -2
- package/dist/adapters/codex/index.mjs +5 -4
- package/dist/adapters/cursor/index.d.mts +15 -28
- package/dist/adapters/cursor/index.mjs +35 -46
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.d.mts +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.d.mts +1 -1
- package/dist/adapters/kimi/index.mjs +1 -1
- package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
- package/dist/cli/bin.mjs +429 -16
- package/dist/cli/index.mjs +1 -1
- package/dist/command-string-CALMTnwN.mjs +42 -0
- package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
- package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
- package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
- package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
- package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/init/index.d.mts +1 -1
- package/dist/init/index.mjs +1 -1
- package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
- package/dist/normalize-BjG6unTj.mjs +317 -0
- package/dist/permission-mode-B-SFyR0X.mjs +226 -0
- package/dist/policy/index.d.mts +1 -1
- package/dist/policy/index.mjs +2 -2
- package/dist/prompt/index.d.mts +1 -1
- package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
- package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
- package/dist/runtime/index.d.mts +27 -16
- package/dist/runtime/index.mjs +2 -2
- package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
- package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
- package/package.json +3 -2
- package/src/adapters/claude/index.ts +92 -0
- package/src/adapters/cline/index.ts +36 -0
- package/src/adapters/codex/apply-patch.ts +94 -0
- package/src/adapters/codex/index.ts +84 -0
- package/src/adapters/codex/permission-mode.ts +16 -0
- package/src/adapters/cursor/context.ts +46 -0
- package/src/adapters/cursor/events.ts +38 -0
- package/src/adapters/cursor/index.ts +43 -0
- package/src/adapters/cursor/interfaces/types.ts +88 -0
- package/src/adapters/cursor/native-response.ts +158 -0
- package/src/adapters/cursor/normalize.ts +123 -0
- package/src/adapters/cursor/respond.ts +123 -0
- package/src/adapters/gemini/index.ts +36 -0
- package/src/adapters/hermes/index.ts +55 -0
- package/src/adapters/hermes/interfaces/types.ts +26 -0
- package/src/adapters/kimi/index.ts +96 -0
- package/src/adapters/kimi/interfaces/types.ts +55 -0
- package/src/cache/compact.ts +55 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/io.ts +38 -0
- package/src/cache/mcp-response.ts +39 -0
- package/src/cache/mcp-store.ts +70 -0
- package/src/cache/store.ts +93 -0
- package/src/changelog/fetch.ts +79 -0
- package/src/cli/bin.ts +100 -0
- package/src/cli/cursor-event-scanner.ts +175 -0
- package/src/cli/cursor-stdin-reader.ts +65 -0
- package/src/cli/doctor.ts +96 -0
- package/src/cli/hook-io.ts +130 -0
- package/src/cli/hook-sound.ts +49 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/json-primitive-scanner.ts +83 -0
- package/src/cli/run.ts +36 -0
- package/src/cli/scope.ts +31 -0
- package/src/codex-rules/build.ts +63 -0
- package/src/codex-rules/index.ts +8 -0
- package/src/codex-rules/mapping.ts +42 -0
- package/src/codex-rules/rules/absolute-paths.ts +52 -0
- package/src/codex-rules/rules/ask.ts +44 -0
- package/src/codex-rules/rules/chmod-ask.ts +57 -0
- package/src/codex-rules/rules/dd.ts +23 -0
- package/src/codex-rules/rules/diskutil.ts +33 -0
- package/src/codex-rules/rules/mkfs.ts +42 -0
- package/src/codex-rules/rules/privilege.ts +49 -0
- package/src/codex-rules/rules/remote-fetch.ts +31 -0
- package/src/codex-rules/rules/rm-variants.ts +58 -0
- package/src/codex-rules/rules/single-commands.ts +57 -0
- package/src/codex-rules/skip-list.ts +25 -0
- package/src/codex-rules/starlark.ts +46 -0
- package/src/codex-rules/types.ts +36 -0
- package/src/config/dotenv.ts +55 -0
- package/src/config/env.ts +10 -0
- package/src/config/home-dir.ts +36 -0
- package/src/config/index.ts +5 -0
- package/src/config/layout.ts +52 -0
- package/src/config/limits.ts +56 -0
- package/src/config/ttl.ts +30 -0
- package/src/detect/harness.ts +73 -0
- package/src/detect/index.ts +1 -0
- package/src/detect/interfaces/types.ts +18 -0
- package/src/freshness/agent-evidence-record.ts +95 -0
- package/src/freshness/agent-evidence.ts +95 -0
- package/src/freshness/codex-spawn-evidence.ts +91 -0
- package/src/freshness/doc-helpers.ts +90 -0
- package/src/freshness/evidence-harvest-io.ts +72 -0
- package/src/freshness/evidence-harvest.ts +61 -0
- package/src/freshness/explore-tools.ts +95 -0
- package/src/freshness/index.ts +2 -0
- package/src/freshness/query-framework.ts +73 -0
- package/src/freshness/ref-evidence.ts +49 -0
- package/src/freshness/ref-journal.ts +98 -0
- package/src/freshness/trivial-edit-counter.ts +30 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +2 -0
- package/src/init/run.ts +32 -0
- package/src/init/templates.ts +75 -0
- package/src/memory/gitignore.ts +14 -0
- package/src/memory/index.ts +3 -0
- package/src/memory/registry.ts +39 -0
- package/src/memory/session-roots.ts +99 -0
- package/src/memory/state.ts +58 -0
- package/src/policy/apex-authorization.ts +100 -0
- package/src/policy/apex-gates.ts +91 -0
- package/src/policy/apex-target-steps.ts +73 -0
- package/src/policy/apex-target.ts +49 -0
- package/src/policy/apex-task-context.ts +81 -0
- package/src/policy/apex.ts +94 -0
- package/src/policy/cartographer/build-tree.ts +69 -0
- package/src/policy/cartographer/describe.ts +53 -0
- package/src/policy/cartographer/entry.ts +38 -0
- package/src/policy/cartographer/frontmatter.ts +48 -0
- package/src/policy/cartographer/indicators.ts +34 -0
- package/src/policy/claude-md-context.ts +66 -0
- package/src/policy/conventions/index.ts +11 -0
- package/src/policy/conventions/interfaces.ts +71 -0
- package/src/policy/conventions/langs.ts +53 -0
- package/src/policy/conventions/query.ts +49 -0
- package/src/policy/conventions/react-hooks.ts +18 -0
- package/src/policy/conventions/store-or-query.ts +21 -0
- package/src/policy/conventions/stores.ts +56 -0
- package/src/policy/conventions/strip.ts +87 -0
- package/src/policy/conventions/verdict.ts +38 -0
- package/src/policy/creation-intent.ts +11 -0
- package/src/policy/deny-loop.ts +88 -0
- package/src/policy/design/allowed-write.ts +28 -0
- package/src/policy/design/content-checks.ts +45 -0
- package/src/policy/design/corpus-fs.ts +21 -0
- package/src/policy/design/corpus-resolve-cache.ts +49 -0
- package/src/policy/design/corpus-resolve.ts +99 -0
- package/src/policy/design/corpus.ts +92 -0
- package/src/policy/design/design-system-rules.ts +45 -0
- package/src/policy/design/flag.ts +30 -0
- package/src/policy/design/gates-pipeline.ts +105 -0
- package/src/policy/design/gates.ts +87 -0
- package/src/policy/design/screenshot-tools.ts +24 -0
- package/src/policy/design/skill-evidence.ts +34 -0
- package/src/policy/design/skill-gate.ts +81 -0
- package/src/policy/design/skill-triggers.ts +51 -0
- package/src/policy/design/state.ts +92 -0
- package/src/policy/design/template-urls.ts +54 -0
- package/src/policy/design/transitions.ts +68 -0
- package/src/policy/detect-claude-md-project-type.ts +32 -0
- package/src/policy/detect-framework.ts +99 -0
- package/src/policy/detect-primitive-checks.ts +100 -0
- package/src/policy/detect-primitive-lib.ts +55 -0
- package/src/policy/detect-project.ts +99 -0
- package/src/policy/edit-outcome.ts +53 -0
- package/src/policy/evaluate.ts +81 -0
- package/src/policy/expert-agents.ts +60 -0
- package/src/policy/file-size-scope.ts +32 -0
- package/src/policy/file-size.ts +99 -0
- package/src/policy/framework-solid-exclude.ts +52 -0
- package/src/policy/framework-solid-extended.ts +93 -0
- package/src/policy/framework-solid-gates-systems.ts +99 -0
- package/src/policy/framework-solid-gates.ts +98 -0
- package/src/policy/framework-solid.ts +88 -0
- package/src/policy/gemini-mcp-gate.ts +83 -0
- package/src/policy/git-gates.ts +40 -0
- package/src/policy/guards/bash-command-anchor.ts +40 -0
- package/src/policy/guards/bash-write-patterns.ts +140 -0
- package/src/policy/guards/bash-write-redirects.ts +201 -0
- package/src/policy/guards/bash-write-safe-paths.ts +62 -0
- package/src/policy/guards/bash-write.ts +76 -0
- package/src/policy/guards/context.ts +12 -0
- package/src/policy/guards/index.ts +61 -0
- package/src/policy/guards/install.ts +22 -0
- package/src/policy/guards/interface-separation-ext.ts +41 -0
- package/src/policy/guards/interface-separation.ts +100 -0
- package/src/policy/guards/protected-path.ts +100 -0
- package/src/policy/guards/security.ts +97 -0
- package/src/policy/index.ts +19 -0
- package/src/policy/interfaces/types.ts +46 -0
- package/src/policy/js-gate-route.ts +51 -0
- package/src/policy/lessons/lesson-gate.ts +0 -0
- package/src/policy/lessons/trigger-index.ts +84 -0
- package/src/policy/lessons/types.ts +31 -0
- package/src/policy/module-layout.ts +31 -0
- package/src/policy/nearest-manifest.ts +75 -0
- package/src/policy/never-approval.ts +95 -0
- package/src/policy/patterns.ts +80 -0
- package/src/policy/seo/validate.ts +51 -0
- package/src/policy/shadcn-project.ts +22 -0
- package/src/policy/shadcn-skill-gate.ts +97 -0
- package/src/policy/shell-read-refs.ts +96 -0
- package/src/policy/skill-path.ts +46 -0
- package/src/policy/skill-patterns/laravel.ts +40 -0
- package/src/policy/skill-patterns/nextjs.ts +29 -0
- package/src/policy/skill-patterns/react.ts +27 -0
- package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
- package/src/policy/skill-patterns/shadcn.ts +66 -0
- package/src/policy/skill-patterns/swift.ts +30 -0
- package/src/policy/skill-patterns/tailwind.ts +40 -0
- package/src/policy/skill-source.ts +52 -0
- package/src/policy/skill-trigger-patterns.ts +33 -0
- package/src/policy/skill-triggers.ts +108 -0
- package/src/policy/tailwind-skill-gate.ts +63 -0
- package/src/policy/trivial-edits.ts +52 -0
- package/src/policy/verbosity.ts +39 -0
- package/src/prompt/index.ts +1 -0
- package/src/prompt/types.ts +36 -0
- package/src/refs/discover.ts +69 -0
- package/src/refs/frontmatter.ts +24 -0
- package/src/refs/index.ts +4 -0
- package/src/refs/loader.ts +59 -0
- package/src/refs/ref-key.ts +41 -0
- package/src/refs/router.ts +63 -0
- package/src/refs/types.ts +25 -0
- package/src/runtime/activity.ts +99 -0
- package/src/runtime/apply-patch-gate.ts +41 -0
- package/src/runtime/burst-window.ts +21 -0
- package/src/runtime/codex-shell-tool.ts +37 -0
- package/src/runtime/command-string.ts +37 -0
- package/src/runtime/confirm/codex-confirm.ts +126 -0
- package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
- package/src/runtime/confirm/confirm-code.ts +33 -0
- package/src/runtime/confirm/confirm-gate.ts +65 -0
- package/src/runtime/confirm/confirm-irreversible.ts +32 -0
- package/src/runtime/confirm/confirm-pending.ts +34 -0
- package/src/runtime/confirm/confirm-state.ts +56 -0
- package/src/runtime/confirm/confirm-subagent.ts +112 -0
- package/src/runtime/confirm/confirm-submit.ts +38 -0
- package/src/runtime/core-guards-active.ts +84 -0
- package/src/runtime/core-guards-wired.ts +47 -0
- package/src/runtime/deny-loop-store.ts +94 -0
- package/src/runtime/deny-notice.ts +52 -0
- package/src/runtime/design-cache-resolve.ts +51 -0
- package/src/runtime/design-content-gate.ts +95 -0
- package/src/runtime/design-files-gate.ts +114 -0
- package/src/runtime/design-helpers.ts +93 -0
- package/src/runtime/design-lifecycle.ts +39 -0
- package/src/runtime/design-read-credit.ts +28 -0
- package/src/runtime/design.ts +116 -0
- package/src/runtime/dev-context.ts +43 -0
- package/src/runtime/dry-patterns.ts +32 -0
- package/src/runtime/dry.ts +96 -0
- package/src/runtime/fragment-registry.ts +33 -0
- package/src/runtime/framework-skill-gate.ts +79 -0
- package/src/runtime/fs-cleanup.ts +71 -0
- package/src/runtime/gate-apex.ts +81 -0
- package/src/runtime/gate-helpers.ts +52 -0
- package/src/runtime/gate-input.ts +26 -0
- package/src/runtime/gate.ts +128 -0
- package/src/runtime/handle-post.ts +104 -0
- package/src/runtime/handle-pre.ts +137 -0
- package/src/runtime/handle-scope-async.ts +29 -0
- package/src/runtime/handle-types.ts +27 -0
- package/src/runtime/handle.ts +116 -0
- package/src/runtime/home-state.ts +55 -0
- package/src/runtime/index.ts +18 -0
- package/src/runtime/inform.ts +28 -0
- package/src/runtime/inject-budget-recap.ts +47 -0
- package/src/runtime/inject-budget.ts +81 -0
- package/src/runtime/inject-context.ts +85 -0
- package/src/runtime/inject-dedup-exclusive.ts +98 -0
- package/src/runtime/inject-dedup.ts +89 -0
- package/src/runtime/is-agent-tool.ts +19 -0
- package/src/runtime/lifecycle/agent-files.ts +49 -0
- package/src/runtime/lifecycle/agent-memory.ts +82 -0
- package/src/runtime/lifecycle/agent-transcript.ts +80 -0
- package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
- package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
- package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
- package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
- package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
- package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
- package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
- package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
- package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
- package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
- package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
- package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
- package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
- package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
- package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
- package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
- package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
- package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
- package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
- package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
- package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
- package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
- package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
- package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
- package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
- package/src/runtime/lifecycle/aipilot/types.ts +72 -0
- package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
- package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
- package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
- package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
- package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
- package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
- package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
- package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
- package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
- package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
- package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
- package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
- package/src/runtime/lifecycle/changelog-research.ts +43 -0
- package/src/runtime/lifecycle/check-file-size.ts +61 -0
- package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
- package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
- package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
- package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
- package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
- package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
- package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
- package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
- package/src/runtime/lifecycle/dispatch.ts +109 -0
- package/src/runtime/lifecycle/failure-lesson.ts +51 -0
- package/src/runtime/lifecycle/index.ts +34 -0
- package/src/runtime/lifecycle/inject-rules.ts +50 -0
- package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
- package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
- package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
- package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
- package/src/runtime/lifecycle/lessons/state.ts +23 -0
- package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
- package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
- package/src/runtime/lifecycle/memory/client.ts +65 -0
- package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
- package/src/runtime/lifecycle/memory/recall.ts +30 -0
- package/src/runtime/lifecycle/memory/salience.ts +31 -0
- package/src/runtime/lifecycle/memory/state.ts +49 -0
- package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
- package/src/runtime/lifecycle/post-compact.ts +38 -0
- package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
- package/src/runtime/lifecycle/post-tracking.ts +40 -0
- package/src/runtime/lifecycle/pre-compact.ts +38 -0
- package/src/runtime/lifecycle/rules-root.ts +83 -0
- package/src/runtime/lifecycle/security/check-skill.ts +63 -0
- package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
- package/src/runtime/lifecycle/security/scan.ts +76 -0
- package/src/runtime/lifecycle/security/skill-state.ts +47 -0
- package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
- package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
- package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
- package/src/runtime/lifecycle/session-end.ts +17 -0
- package/src/runtime/lifecycle/session-start.ts +57 -0
- package/src/runtime/lifecycle/snapshot/board.ts +27 -0
- package/src/runtime/lifecycle/snapshot/format.ts +60 -0
- package/src/runtime/lifecycle/snapshot/git.ts +71 -0
- package/src/runtime/lifecycle/snapshot/index.ts +52 -0
- package/src/runtime/lifecycle/snapshot/version.ts +34 -0
- package/src/runtime/lifecycle/solid-detect.ts +56 -0
- package/src/runtime/lifecycle/stop-core.ts +35 -0
- package/src/runtime/lifecycle/subagent-cache.ts +61 -0
- package/src/runtime/lifecycle/task-completed.ts +94 -0
- package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
- package/src/runtime/lifecycle/teammate-idle.ts +28 -0
- package/src/runtime/lifecycle/tool-failure.ts +24 -0
- package/src/runtime/lifecycle/track-changes.ts +61 -0
- package/src/runtime/lifecycle/validate-solid.ts +70 -0
- package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
- package/src/runtime/lifecycle-bridge.ts +74 -0
- package/src/runtime/mcp-key.ts +44 -0
- package/src/runtime/mcp-tool-name.ts +92 -0
- package/src/runtime/mcp.ts +91 -0
- package/src/runtime/modular.ts +77 -0
- package/src/runtime/normalize.ts +107 -0
- package/src/runtime/notices.ts +91 -0
- package/src/runtime/notification-sound.ts +49 -0
- package/src/runtime/notifications.ts +77 -0
- package/src/runtime/paths.ts +59 -0
- package/src/runtime/post-fanout.ts +41 -0
- package/src/runtime/post-outcome.ts +61 -0
- package/src/runtime/pre-allow.ts +97 -0
- package/src/runtime/precommit.ts +60 -0
- package/src/runtime/prompt-text.ts +38 -0
- package/src/runtime/receipt-capture.ts +34 -0
- package/src/runtime/record.ts +26 -0
- package/src/runtime/respond.ts +92 -0
- package/src/runtime/solid-file-size-gate.ts +58 -0
- package/src/runtime/solid-pre.ts +53 -0
- package/src/runtime/storage.ts +9 -0
- package/src/statusline/colors.ts +31 -0
- package/src/statusline/constants.ts +17 -0
- package/src/statusline/formatters.ts +54 -0
- package/src/statusline/index.ts +4 -0
- package/src/statusline/progress-bar.ts +46 -0
- package/src/tracking/codex-post-failure.ts +73 -0
- package/src/tracking/index.ts +2 -0
- package/src/tracking/integrity.ts +99 -0
- package/src/tracking/one-shot-dedup.ts +34 -0
- package/src/tracking/one-shot-failure.ts +42 -0
- package/src/tracking/one-shot-store.ts +95 -0
- package/src/tracking/one-shot.ts +93 -0
- package/src/tracking/receipts.ts +95 -0
- package/src/tracking/session-state.ts +94 -0
- package/src/tracking/store.ts +96 -0
- package/src/tracking/track-compact.ts +100 -0
- package/src/tracking/track-diff.ts +50 -0
- package/src/tracking/track-journal.ts +97 -0
- package/src/tracking/track-lock-sync.ts +83 -0
- package/src/tracking/track-lock.ts +67 -0
- package/src/util/compact-json.ts +23 -0
- package/src/util/index.ts +2 -0
- package/src/util/json-io.ts +47 -0
- package/src/util/project-root.ts +41 -0
- package/src/util/runtime-io.ts +85 -0
- package/src/util/semver.ts +30 -0
- package/dist/permission-mode-B0MUl7FT.mjs +0 -136
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module gemini-mcp-gate
|
|
3
|
+
* OPT-IN gate porting core-guards `enforce-gemini-mcp.py`: when enabled via
|
|
4
|
+
* `FUSE_ENFORCE_GEMINI_MCP`, blocks a UI write (`.tsx/.jsx/.vue/.svelte`) that
|
|
5
|
+
* hand-writes >= 3 Tailwind utility classes unless a `mcp__gemini-design__*`
|
|
6
|
+
* call was made this session. DISABLED BY DEFAULT — the flag is read fresh on
|
|
7
|
+
* every call, so nothing is gated unless the owner explicitly turns it on (a
|
|
8
|
+
* gate blocking a common dev action must never be always-on).
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import type { Prompt } from "../prompt/types";
|
|
12
|
+
import type { AuthEntry } from "../freshness/doc-helpers";
|
|
13
|
+
|
|
14
|
+
/** UI file extensions the gate polices (parity enforce-gemini-mcp.py UI_EXT). */
|
|
15
|
+
const UI_EXT_RE = /\.(tsx|jsx|vue|svelte)$/;
|
|
16
|
+
/** Paths exempt from the gate (parity EXEMPT_DIRS). */
|
|
17
|
+
const GEMINI_EXEMPT_RE = /(node_modules|dist|build|\.next|\.claude)\//;
|
|
18
|
+
/** A live `mcp__gemini-design__*` call recorded this session proves consultation. */
|
|
19
|
+
const GEMINI_MCP_SOURCE_RE = /gemini-mcp/i;
|
|
20
|
+
/** Tailwind utility patterns (parity TAILWIND_PATTERNS). */
|
|
21
|
+
const TW_PATTERNS: readonly RegExp[] = [
|
|
22
|
+
/\bflex\b/, /\bgrid\b/, /\bp-\d/, /\bpx-\d/, /\bpy-\d/,
|
|
23
|
+
/\bm-\d/, /\bmx-\d/, /\bmy-\d/, /\bmt-\d/, /\bmb-\d/,
|
|
24
|
+
/\bbg-\w+/, /\btext-\w+/, /\brounded/, /\bshadow/,
|
|
25
|
+
/\bborder\b/, /\bgap-\d/, /\bw-\w+/, /\bh-\w+/,
|
|
26
|
+
/\bjustify-\w+/, /\bitems-\w+/, /\bspace-\w+-\d/,
|
|
27
|
+
];
|
|
28
|
+
/** Min distinct Tailwind patterns before the gate fires (parity MIN_TAILWIND_CLASSES). */
|
|
29
|
+
const MIN_TW = 3;
|
|
30
|
+
/** Min newlines an Edit's new_string must have to be gated (parity MIN_LINES_FOR_EDIT). */
|
|
31
|
+
const MIN_LINES_EDIT = 2;
|
|
32
|
+
|
|
33
|
+
/** OPT-IN: off unless `FUSE_ENFORCE_GEMINI_MCP` is `1`/`true`. Read fresh each call (dynamic). */
|
|
34
|
+
export function geminiMcpEnforced(): boolean {
|
|
35
|
+
const v = process.env.FUSE_ENFORCE_GEMINI_MCP;
|
|
36
|
+
return v === "1" || v === "true";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Count distinct Tailwind utility patterns present in `content` (parity count_tailwind_classes). */
|
|
40
|
+
function countTailwindClasses(content: string): number {
|
|
41
|
+
return TW_PATTERNS.reduce((n, re) => (re.test(content) ? n + 1 : n), 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Session evidence the gate consumes. */
|
|
45
|
+
export interface GeminiEvidence {
|
|
46
|
+
authorizations?: Record<string, AuthEntry>;
|
|
47
|
+
sessionId: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True when a `mcp__gemini-design__*` call was recorded this session (parity gemini_was_called). */
|
|
51
|
+
export function geminiMcpConsulted(authorizations: Record<string, AuthEntry> | undefined, sessionId: string): boolean {
|
|
52
|
+
if (!authorizations) return false;
|
|
53
|
+
return Object.values(authorizations).some(
|
|
54
|
+
(a) => a.doc_sessions?.includes(sessionId) && (a.sources ?? (a.source ? [a.source] : [])).some((s) => GEMINI_MCP_SOURCE_RE.test(s)),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Gate a UI write when `FUSE_ENFORCE_GEMINI_MCP` is on: block hand-written
|
|
60
|
+
* Tailwind (>= 3 classes) in a `.tsx/.jsx/.vue/.svelte` file unless a Gemini
|
|
61
|
+
* Design MCP call was made this session. Returns `null` when disabled (default),
|
|
62
|
+
* out of scope, or already satisfied.
|
|
63
|
+
* @param tool - the tool name.
|
|
64
|
+
* @param filePath - the file being written.
|
|
65
|
+
* @param content - the written content (new_string on Edit).
|
|
66
|
+
* @param ev - session evidence (authorizations + sessionId).
|
|
67
|
+
*/
|
|
68
|
+
export function geminiMcpGate(tool: string, filePath: string, content: string, ev: GeminiEvidence): Prompt | null {
|
|
69
|
+
if (!geminiMcpEnforced()) return null;
|
|
70
|
+
if (tool !== "Write" && tool !== "Edit") return null;
|
|
71
|
+
if (!filePath || !UI_EXT_RE.test(filePath) || GEMINI_EXEMPT_RE.test(filePath)) return null;
|
|
72
|
+
if (!content) return null;
|
|
73
|
+
if (tool === "Edit" && (content.match(/\n/g)?.length ?? 0) < MIN_LINES_EDIT) return null;
|
|
74
|
+
if (countTailwindClasses(content) < MIN_TW) return null;
|
|
75
|
+
if (geminiMcpConsulted(ev.authorizations, ev.sessionId)) return null;
|
|
76
|
+
return {
|
|
77
|
+
kind: "block",
|
|
78
|
+
title: "Gemini Design MCP",
|
|
79
|
+
reason:
|
|
80
|
+
"BLOCKED: UI code with Tailwind detected but Gemini Design MCP not used. Use mcp__gemini-design__create_frontend, modify_frontend, or snippet_frontend BEFORE writing UI code manually.",
|
|
81
|
+
actions: ["Call mcp__gemini-design__create_frontend / modify_frontend / snippet_frontend, then retry"],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { matchPatterns, GIT_BLOCKED, GIT_ASK } from "./patterns";
|
|
2
|
+
import type { PolicyResult } from "./interfaces/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Evaluate the two git confirmation gates — destructive block ({@link
|
|
6
|
+
* GIT_BLOCKED}) and confirmation ask ({@link GIT_ASK}) — against `command`.
|
|
7
|
+
* Skipped entirely when `ralphSafe` is true: Ralph mode already silent-
|
|
8
|
+
* allowed the command upstream (parity with evaluate.ts's pre-existing
|
|
9
|
+
* `!ralphSafe &&` guards). Returns the matching gate's {@link PolicyResult},
|
|
10
|
+
* or `null` when neither gate matches.
|
|
11
|
+
*/
|
|
12
|
+
export function evaluateGitGates(command: string | undefined, ralphSafe: boolean): PolicyResult | null {
|
|
13
|
+
if (ralphSafe || !command) return null;
|
|
14
|
+
if (matchPatterns(command, GIT_BLOCKED)) {
|
|
15
|
+
const reason = `Destructive git command: ${command}`;
|
|
16
|
+
return {
|
|
17
|
+
decision: "deny",
|
|
18
|
+
message: reason,
|
|
19
|
+
prompt: {
|
|
20
|
+
kind: "block",
|
|
21
|
+
title: "Destructive git command",
|
|
22
|
+
reason,
|
|
23
|
+
actions: ["Use a non-destructive alternative (e.g. --force-with-lease; avoid --hard / -D)"],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (matchPatterns(command, GIT_ASK)) {
|
|
28
|
+
return {
|
|
29
|
+
decision: "deny",
|
|
30
|
+
message: `Git operation requires confirmation: ${command}`,
|
|
31
|
+
prompt: {
|
|
32
|
+
kind: "ask",
|
|
33
|
+
title: "Confirm git operation",
|
|
34
|
+
reason: `Authorize: ${command.trim()}`,
|
|
35
|
+
actions: ["Approve if this git operation is intended"],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-position anchoring for the Bash write guard — the regex prefix that
|
|
3
|
+
* tells a real mutator invocation apart from a quoted/argument mention of the
|
|
4
|
+
* same token. Consumed by bash-write-patterns.ts (CODE_MUTATORS, CODE_COMMAND_WRITE).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Transparent exec wrappers that run another command as their argument
|
|
9
|
+
* (`env sed -i …`, `timeout 5 patch …`, `sudo -n tee …`, `xargs sed -i`). A code
|
|
10
|
+
* mutator behind one is still a code mutator — the wrapper is not a shield.
|
|
11
|
+
*/
|
|
12
|
+
const WRAP = "(?:env|timeout|nice|nohup|sudo|command|stdbuf|time|ionice|exec|xargs)";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A wrapper's own leading arg tokens: `VAR=val` assignments, `-f`/`--flag`
|
|
16
|
+
* options, or a bare duration/number (`timeout 5`, `nice -n 10`). The flag form
|
|
17
|
+
* is `--?[^\s-]\S*` (not `-{1,2}\S+`) so a dash-run has ONE decomposition — the
|
|
18
|
+
* `-`/`--` ambiguity under the outer `*` is a ReDoS vector (OWASP ReDoS). Every
|
|
19
|
+
* alternative consumes ≥1 char and is followed by a mandatory `\s+`, so the
|
|
20
|
+
* repeated group is never zero-width (no `(X*)*` catastrophic shape).
|
|
21
|
+
*/
|
|
22
|
+
const WRAP_ARG = "(?:\\w+=\\S+|--?[^\\s-]\\S*|\\d+[smhd]?)";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Command-position anchor: line/segment start or a `;&|(` separator, then any
|
|
26
|
+
* chain of {@link WRAP} wrappers (each with its own {@link WRAP_ARG} tokens).
|
|
27
|
+
* Anchoring a write motif here makes it fire on a real invocation
|
|
28
|
+
* (`env sed -i x src/a.ts`) while ignoring a quoted/argument MENTION of the same
|
|
29
|
+
* text (`git commit -m "fix sed -i doc"`, `npm run t -- --grep "sed -i"`), which
|
|
30
|
+
* is never at a command position.
|
|
31
|
+
*
|
|
32
|
+
* Defense-in-depth first pass, NOT the sole control: regex-on-raw-string is
|
|
33
|
+
* lossy vs a real shell lexer. Known residual gaps (enumerated, not silently
|
|
34
|
+
* assumed away): command substitution `$(…)`/backticks, indirect exec via
|
|
35
|
+
* `bash -c`/`eval`/`find -exec`, process substitution `<(…)`, shell-quoting
|
|
36
|
+
* tricks (`s''ed`, `${x}ed`), and variable-indirection targets (`tee $F`). Those
|
|
37
|
+
* need a tokenizer, out of scope here; the Write/Edit tool gate stays the
|
|
38
|
+
* backstop for what slips through.
|
|
39
|
+
*/
|
|
40
|
+
export const CMD: string = `(?:^|[\\n;&|(])\\s*(?:${WRAP}\\s+(?:${WRAP_ARG}\\s+)*)*`;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { CMD } from "./bash-command-anchor";
|
|
2
|
+
|
|
3
|
+
/** Code-file extensions the write guards police (shared by CODE_REDIRECT and
|
|
4
|
+
* CODE_COMMAND_WRITE). Case-sensitive, parity with the Python guard's list. */
|
|
5
|
+
const CODE_EXT = "ts|tsx|js|jsx|py|go|rb|rs|java|kt|php|swift|vue|svelte|astro|css|c|cpp|h";
|
|
6
|
+
|
|
7
|
+
/** Redirect (`>`/`>>`) targeting a code-file extension. */
|
|
8
|
+
export const CODE_REDIRECT: RegExp = new RegExp(`(?:>>?)\\s*[^\\s|;&]*\\.(?:${CODE_EXT})\\b`);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Interpreters / tools that mutate source in place, plus heredoc-into-file —
|
|
12
|
+
* split into labeled sub-patterns (parity bash-write-guard.py `DENY_PATTERNS`,
|
|
13
|
+
* each with its own `desc`) so the deny reason names which motif matched
|
|
14
|
+
* instead of a single generic message. Every command-named motif is
|
|
15
|
+
* {@link CMD}-anchored (start/separator + optional wrapper chain); only the
|
|
16
|
+
* structural heredoc-into-file motif is position-free.
|
|
17
|
+
*/
|
|
18
|
+
export const CODE_MUTATORS: readonly { re: RegExp; desc: string }[] = [
|
|
19
|
+
{ re: new RegExp(`${CMD}python3?\\s+-\\s*<<`), desc: "Python heredoc input" },
|
|
20
|
+
{ re: new RegExp(`${CMD}sed\\b[^|]*\\s-i`), desc: "sed in-place edit" },
|
|
21
|
+
{ re: new RegExp(`${CMD}perl\\b[^|]*\\s-[pi]i?\\b`), desc: "perl in-place edit" },
|
|
22
|
+
{ re: new RegExp(`${CMD}awk\\b[^|]*-i\\s*inplace`), desc: "awk in-place edit" },
|
|
23
|
+
// `patch` as a command token (start/separator + optional wrapper chain), then a
|
|
24
|
+
// metachar/space/EOL. Never the bare word merely NAMING a path in a read-only
|
|
25
|
+
// command (`jq . apply-patch.json`, `grep patch src/`). Prefix-wrapped forms
|
|
26
|
+
// (`env patch`, `timeout 5 patch`) are now DENIED — the previously accepted gap
|
|
27
|
+
// was closed (owner decision 2026-07-06): the wrapper chain folds into CMD.
|
|
28
|
+
{ re: new RegExp(`${CMD}patch(?=\\s|<|[;&|)>]|$)`), desc: "patch file modification" },
|
|
29
|
+
{ re: /<<[-~]?\s*['"]?\w+['"]?[\s\S]*?>/, desc: "heredoc redirected into a file" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `tee`/`dd of=` whose TARGET is a code file — the command-form parallel to
|
|
34
|
+
* CODE_REDIRECT. {@link CMD}-anchored so `env`/`timeout` wrappers don't shield
|
|
35
|
+
* it; the `tee` branch scans ALL args up to the next `;&|` separator so a
|
|
36
|
+
* decoy-first-target (`tee log.txt src/x.ts`) can't hide the code write. A tee
|
|
37
|
+
* to a NON-code target (`cmd | tee results.txt`) does not match — it stays a
|
|
38
|
+
* plain ASK_WRITERS ask, never a hard block, so ordinary logging is untouched.
|
|
39
|
+
*/
|
|
40
|
+
export const CODE_COMMAND_WRITE: RegExp = new RegExp(
|
|
41
|
+
`${CMD}(?:tee\\s+[^;&|\\n]*?|dd\\b[^|]*\\bof=\\S*)\\.(?:${CODE_EXT})\\b`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
/** File-mutating one-liners via `node -e` / `ruby -e` (parity NODE_WRITES/RUBY_WRITES). */
|
|
45
|
+
export const NODE_WRITES: RegExp =
|
|
46
|
+
/writeFile|appendFile|createWriteStream|fs\.(?:write|rename|unlink|mkdir|rmdir|copyFile)|execSync|spawnSync|child_process/;
|
|
47
|
+
export const RUBY_WRITES: RegExp =
|
|
48
|
+
/File\.(?:write|open|delete|rename)|IO\.write|FileUtils|\bsystem\b|\bexec\b|`[^`]/;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Command-position anchor for `python3 -c` (same anchored-token shape as the
|
|
52
|
+
* removed unconditional CODE_MUTATORS entry) — used ONLY to gate {@link PYTHON_WRITES}
|
|
53
|
+
* below, never as a standalone block. `python3 - <<EOF` (heredoc) stays
|
|
54
|
+
* unconditionally blocked in CODE_MUTATORS — a heredoc body can't be inspected
|
|
55
|
+
* reliably with a single-line regex, so it is never widened.
|
|
56
|
+
*/
|
|
57
|
+
export const PYTHON_C_ANCHOR: RegExp = new RegExp(`${CMD}python3?\\s+-c\\b`);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* File/process-mutating constructs inside a `python3 -c` inline script (parity
|
|
61
|
+
* NODE_WRITES/RUBY_WRITES: content-gated, not name-gated). `python3 -c` is
|
|
62
|
+
* blocked ONLY when this matches — a read-only one-liner (`print(...)`,
|
|
63
|
+
* `json.load`/`json.dumps`) passes. Doc-verified against docs.python.org
|
|
64
|
+
* (functions/pathlib/shutil/subprocess/os/pickle) before writing this pattern.
|
|
65
|
+
*
|
|
66
|
+
* Families, each independently sufficient to mutate state or run arbitrary code:
|
|
67
|
+
* - `open(...)` / `Path(...).open(...)` in a write/append/exclusive/read-write
|
|
68
|
+
* mode — positional (`'w'`,`'x'`,`'a'`,`'r+'`,`'w+'`, …) or `mode=` kwarg, any
|
|
69
|
+
* `b`/`t` suffix. Plain `open(f)` / `open(f, 'r')` (read-only, the default)
|
|
70
|
+
* does NOT match.
|
|
71
|
+
* - `pathlib.Path` mutators: `write_text`, `write_bytes`, `unlink`, `rename`,
|
|
72
|
+
* `replace`, `mkdir`, `rmdir`, `touch`, `chmod`, `symlink_to`, `hardlink_to`,
|
|
73
|
+
* plus 3.14+ `move`, `move_into`, `copy`, `copy_into`.
|
|
74
|
+
* - `shutil.*` EXCLUDING the confirmed read-only members `which`,
|
|
75
|
+
* `disk_usage`, `get_terminal_size`, `get_archive_formats`,
|
|
76
|
+
* `get_unpack_formats` — every other shutil function copies/moves/deletes.
|
|
77
|
+
* - `os.*` mutators/exec: `remove`, `unlink`, `rename`, `replace`, `makedirs`,
|
|
78
|
+
* `mkdir`, `rmdir`, `removedirs`, `chmod`, `chown`, `symlink`, `link`,
|
|
79
|
+
* `truncate`, `chdir`, `fchdir`, `putenv`, `write`, `ftruncate`, `fchmod`,
|
|
80
|
+
* `fchown`, `fork`, `system`, `popen`, plus the `exec*`/`spawn*`/
|
|
81
|
+
* `posix_spawn` families.
|
|
82
|
+
* - `subprocess.run|call|check_call|check_output|Popen|getoutput|getstatusoutput`
|
|
83
|
+
* — the actual spawn surface. Non-spawning members (`PIPE`, `DEVNULL`,
|
|
84
|
+
* `STDOUT`, `CompletedProcess`, `CalledProcessError`, …) are deliberately
|
|
85
|
+
* excluded — a bare `subprocess\.\w+` would false-positive on those.
|
|
86
|
+
* - `pickle.dump` / `json.dump` (the file-writing form — `\b` after `dump`
|
|
87
|
+
* excludes `dumps`, which only serializes to a string/bytes in memory).
|
|
88
|
+
* - `csv.writer`, `tempfile.*` (temp-file/dir creation is still a write).
|
|
89
|
+
* - generic `.write(`/`.writelines(` — covers any file-like object, including
|
|
90
|
+
* one opened via a variable this static check can't trace back to `open()`.
|
|
91
|
+
* - `exec(`/`eval(` — arbitrary code execution, not merely file I/O.
|
|
92
|
+
*
|
|
93
|
+
* Deliberately NOT included: bare `import` statements, arithmetic/string
|
|
94
|
+
* formatting, `print`, `json.load`/`json.loads`, `sys.argv`/`os.environ` reads,
|
|
95
|
+
* `os.getcwd`/`os.listdir`/`os.path.*` — none of these mutate disk or spawn.
|
|
96
|
+
*/
|
|
97
|
+
export const PYTHON_WRITES: RegExp = new RegExp(
|
|
98
|
+
"\\bopen\\s*\\([^)]*(?:(['\"])(?:[wax]|r\\+|\\+r)[bt]?\\1|mode\\s*=\\s*(['\"])(?:[wax]|r\\+|\\+r)[bt]?\\2)"
|
|
99
|
+
+ "|\\.(?:write_text|write_bytes|unlink|rename|replace|mkdir|rmdir|touch|chmod|symlink_to|hardlink_to"
|
|
100
|
+
+ "|move_into|copy_into|move|copy|write|writelines)\\s*\\("
|
|
101
|
+
+ "|\\bshutil\\.(?!which\\b|disk_usage\\b|get_terminal_size\\b|get_archive_formats\\b|get_unpack_formats\\b)\\w+"
|
|
102
|
+
+ "|\\bos\\.(?:remove|unlink|rename|replace|makedirs|mkdir|rmdir|removedirs|system|popen|chmod|chown|symlink"
|
|
103
|
+
+ "|link|truncate|chdir|fchdir|putenv|write|ftruncate|fchmod|fchown|fork)\\b"
|
|
104
|
+
+ "|\\bos\\.(?:exec|spawn|posix_spawn)\\w*\\b"
|
|
105
|
+
+ "|\\bsubprocess\\.(?:run|call|check_call|check_output|Popen|getoutput|getstatusoutput)\\b"
|
|
106
|
+
+ "|\\bpickle\\.dump\\b"
|
|
107
|
+
+ "|\\bjson\\.dump\\b"
|
|
108
|
+
+ "|\\bcsv\\.writer\\b"
|
|
109
|
+
+ "|\\btempfile\\.\\w+"
|
|
110
|
+
+ "|\\b(?:exec|eval)\\s*\\(",
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/** Redirect to a non-code file. Excludes `/dev/null`, `2>`/`N>` and `>&N` fd
|
|
114
|
+
* redirects via the `(?<![0-9&])` lookbehind + `(?!…|&)` (parity has_file_redirect). */
|
|
115
|
+
export const FILE_REDIRECT: RegExp =
|
|
116
|
+
/(?<![0-9&])\s*>>?\s*(?!\/dev\/null|&)[a-zA-Z./~$]/;
|
|
117
|
+
|
|
118
|
+
/** Other ambiguous file writers (ASK): `tee <file>` (not `tee -a`/path) and `dd … of=` —
|
|
119
|
+
* labeled sub-patterns (parity bash-write-guard.py `ASK_PATTERNS`). */
|
|
120
|
+
export const ASK_WRITERS: readonly { re: RegExp; desc: string }[] = [
|
|
121
|
+
{ re: /\btee\s+[^-/\s]/, desc: "tee to file" },
|
|
122
|
+
{ re: /\bdd\b[^|]*\bof=/, desc: "dd output to file" },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
/** Commands whose first token never writes, skipped when a real redirect is
|
|
126
|
+
* present (parity bash-write-guard.py `SAFE_PREFIXES`). */
|
|
127
|
+
export const SAFE_PREFIXES: readonly string[] = [
|
|
128
|
+
"ls", "pwd", "which", "cat ", "head ", "tail ", "wc ", "file ", "stat ", "tree", "du ", "df ",
|
|
129
|
+
"find ", "grep ", "rg ", "git ", "cd ", "source ", "export ", "unset ", "env ", "printenv",
|
|
130
|
+
"bun test", "bun run", "bunx ", "npm test", "npm run", "npx ", "biome ", "eslint ",
|
|
131
|
+
"prettier ", "ruff ", "pyright ", "tsc ", "mkdir ", "mv ", "cp ",
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Session-state directory the freshness/APEX gates rely on. Any Bash command
|
|
136
|
+
* touching it is a hook-bypass vector, so it is blocked outright — a blunt
|
|
137
|
+
* substring match (read OR write), parity with bash-write-guard.py DENY_PATTERNS
|
|
138
|
+
* `fusengine-cache/sessions` (rebranded to the harness cache path).
|
|
139
|
+
*/
|
|
140
|
+
export const SESSION_STATE_FRAGMENT = ".fuse-harness/cache/sessions";
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/** One active shell output-redirection operator and its parsed target. */
|
|
2
|
+
export type ShellRedirect = Readonly<{ start: number; operator: string; target: string }>;
|
|
3
|
+
|
|
4
|
+
function closingParen(input: string, start: number): number {
|
|
5
|
+
let depth = 1;
|
|
6
|
+
let quote: "'" | '"' | null = null;
|
|
7
|
+
for (let i = start; i < input.length; i++) {
|
|
8
|
+
const ch = input[i];
|
|
9
|
+
if (ch === "\\" && quote !== "'") { i++; continue; }
|
|
10
|
+
if (quote) { if (ch === quote) quote = null; continue; }
|
|
11
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
12
|
+
if (ch === "(") { depth++; continue; }
|
|
13
|
+
if (ch === ")" && --depth === 0) return i;
|
|
14
|
+
}
|
|
15
|
+
return input.length;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function closingBacktick(input: string, start: number): number {
|
|
19
|
+
for (let i = start; i < input.length; i++) {
|
|
20
|
+
if (input[i] === "\\") { i++; continue; }
|
|
21
|
+
if (input[i] === "`") return i;
|
|
22
|
+
}
|
|
23
|
+
return input.length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function closingArithmetic(input: string, start: number): number {
|
|
27
|
+
let depth = 1;
|
|
28
|
+
let quote: "'" | '"' | null = null;
|
|
29
|
+
for (let i = start; i < input.length; i++) {
|
|
30
|
+
const ch = input[i];
|
|
31
|
+
if (ch === "\\" && quote !== "'") { i++; continue; }
|
|
32
|
+
if (quote) { if (ch === quote) quote = null; continue; }
|
|
33
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
34
|
+
if (ch === "(") { depth++; continue; }
|
|
35
|
+
if (ch !== ")") continue;
|
|
36
|
+
if (depth === 1 && input[i + 1] === ")") return i + 1;
|
|
37
|
+
depth--;
|
|
38
|
+
}
|
|
39
|
+
return input.length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function closingConditional(input: string, start: number): number {
|
|
43
|
+
let quote: "'" | '"' | null = null;
|
|
44
|
+
for (let i = start; i < input.length - 1; i++) {
|
|
45
|
+
const ch = input[i];
|
|
46
|
+
if (ch === "\\" && quote !== "'") { i++; continue; }
|
|
47
|
+
if (quote) { if (ch === quote) quote = null; continue; }
|
|
48
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
49
|
+
if (ch === "]" && input[i + 1] === "]") return i;
|
|
50
|
+
}
|
|
51
|
+
return input.length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function commentStart(input: string, index: number, start: number): boolean {
|
|
55
|
+
return input[index] === "#" && (index === start || /[\s;|&()]/.test(input[index - 1] ?? ""));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function commandPosition(input: string, index: number, start: number): boolean {
|
|
59
|
+
const prefix = input.slice(start, index);
|
|
60
|
+
const segment = prefix.slice(Math.max(prefix.lastIndexOf(";"), prefix.lastIndexOf("|"), prefix.lastIndexOf("&"), prefix.lastIndexOf("\n")) + 1).trim();
|
|
61
|
+
return segment === "" || /^(?:if|elif|while|until|then|do|!)$/.test(segment);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readTarget(input: string, start: number, end: number): string {
|
|
65
|
+
let out = "";
|
|
66
|
+
let quote: "'" | '"' | null = null;
|
|
67
|
+
for (let i = start; i < end; i++) {
|
|
68
|
+
const ch = input[i] ?? "";
|
|
69
|
+
if (ch === "\\" && quote !== "'") {
|
|
70
|
+
if (i + 1 < end) out += input[++i] ?? "";
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (quote) {
|
|
74
|
+
if (ch === quote) quote = null;
|
|
75
|
+
else out += ch;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
79
|
+
if (/\s|[;&|<>]/.test(ch)) break;
|
|
80
|
+
out += ch;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function scanDoubleQuote(input: string, start: number, end: number, out: ShellRedirect[]): number {
|
|
86
|
+
for (let i = start; i < end; i++) {
|
|
87
|
+
if (input[i] === "\\") { i++; continue; }
|
|
88
|
+
if (input[i] === '"') return i;
|
|
89
|
+
if (input[i] === "$" && input[i + 1] === "(" && input[i + 2] === "(") {
|
|
90
|
+
const close = closingArithmetic(input, i + 3);
|
|
91
|
+
scanExpansions(input, i + 3, close - 1, out);
|
|
92
|
+
i = close;
|
|
93
|
+
} else if (input[i] === "$" && input[i + 1] === "(") {
|
|
94
|
+
const close = closingParen(input, i + 2);
|
|
95
|
+
scanRange(input, i + 2, close, out);
|
|
96
|
+
i = close;
|
|
97
|
+
} else if (input[i] === "`") {
|
|
98
|
+
const close = closingBacktick(input, i + 1);
|
|
99
|
+
scanRange(input, i + 1, close, out);
|
|
100
|
+
i = close;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return end;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scanExpansions(input: string, start: number, end: number, out: ShellRedirect[]): void {
|
|
107
|
+
for (let i = start; i < end; i++) {
|
|
108
|
+
if (input[i] === "\\") { i++; continue; }
|
|
109
|
+
if (commentStart(input, i, start)) {
|
|
110
|
+
const newline = input.indexOf("\n", i + 1);
|
|
111
|
+
i = newline < 0 || newline >= end ? end : newline;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (input[i] === "'") {
|
|
115
|
+
const close = input.indexOf("'", i + 1);
|
|
116
|
+
i = close < 0 || close >= end ? end : close;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (input[i] === '"') { i = scanDoubleQuote(input, i + 1, end, out); continue; }
|
|
120
|
+
if (input[i] === "$" && input[i + 1] === "(" && input[i + 2] === "(") {
|
|
121
|
+
const close = closingArithmetic(input, i + 3);
|
|
122
|
+
scanExpansions(input, i + 3, close - 1, out);
|
|
123
|
+
i = close;
|
|
124
|
+
} else if (input[i] === "$" && input[i + 1] === "(") {
|
|
125
|
+
const close = closingParen(input, i + 2);
|
|
126
|
+
scanRange(input, i + 2, close, out);
|
|
127
|
+
i = close;
|
|
128
|
+
} else if (input[i] === "`") {
|
|
129
|
+
const close = closingBacktick(input, i + 1);
|
|
130
|
+
scanRange(input, i + 1, close, out);
|
|
131
|
+
i = close;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function scanRange(input: string, start: number, end: number, out: ShellRedirect[]): void {
|
|
137
|
+
for (let i = start; i < end; i++) {
|
|
138
|
+
const ch = input[i];
|
|
139
|
+
if (ch === "\\") { i++; continue; }
|
|
140
|
+
if (commentStart(input, i, start)) {
|
|
141
|
+
const newline = input.indexOf("\n", i + 1);
|
|
142
|
+
i = newline < 0 || newline >= end ? end : newline;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (ch === "'") {
|
|
146
|
+
const close = input.indexOf("'", i + 1);
|
|
147
|
+
i = close < 0 || close >= end ? end : close;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (ch === '"') { i = scanDoubleQuote(input, i + 1, end, out); continue; }
|
|
151
|
+
if (ch === "[" && input[i + 1] === "[" && commandPosition(input, i, start)) {
|
|
152
|
+
const close = closingConditional(input, i + 2);
|
|
153
|
+
scanExpansions(input, i + 2, close, out);
|
|
154
|
+
i = close + 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (ch === "(" && input[i + 1] === "(" && commandPosition(input, i, start)) {
|
|
158
|
+
const close = closingArithmetic(input, i + 2);
|
|
159
|
+
scanExpansions(input, i + 2, close - 1, out);
|
|
160
|
+
i = close;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch === "$" && input[i + 1] === "(" && input[i + 2] === "(") {
|
|
164
|
+
const close = closingArithmetic(input, i + 3);
|
|
165
|
+
scanExpansions(input, i + 3, close - 1, out);
|
|
166
|
+
i = close;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (ch === "$" && input[i + 1] === "(") {
|
|
170
|
+
const close = closingParen(input, i + 2);
|
|
171
|
+
scanRange(input, i + 2, close, out);
|
|
172
|
+
i = close;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (ch === "`") {
|
|
176
|
+
const close = closingBacktick(input, i + 1);
|
|
177
|
+
scanRange(input, i + 1, close, out);
|
|
178
|
+
i = close;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (ch !== ">") continue;
|
|
182
|
+
if (input[i + 1] === "(") continue;
|
|
183
|
+
if (input[i - 1] === "&") { if (input[i + 1] === ">") i++; continue; }
|
|
184
|
+
let opStart = i;
|
|
185
|
+
while (opStart > start && /\d/.test(input[opStart - 1] ?? "")) opStart--;
|
|
186
|
+
if (input[opStart - 1] === "&") opStart--;
|
|
187
|
+
let cursor = i + (input[i + 1] === ">" || input[i + 1] === "|" ? 2 : 1);
|
|
188
|
+
const operator = input.slice(opStart, cursor);
|
|
189
|
+
while (/\s/.test(input[cursor] ?? "")) cursor++;
|
|
190
|
+
if (input[cursor] === "&") continue;
|
|
191
|
+
const target = readTarget(input, cursor, end);
|
|
192
|
+
if (target) out.push({ start: opStart, operator, target });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Return active output redirects, excluding quoted/escaped `>` characters. */
|
|
197
|
+
export function shellOutputRedirects(command: string): ShellRedirect[] {
|
|
198
|
+
const out: ShellRedirect[] = [];
|
|
199
|
+
scanRange(command, 0, command.length, out);
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { join, normalize } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { claudeHome, fusengineCache } from "../../runtime/home-state";
|
|
4
|
+
import { shellOutputRedirects } from "./bash-write-redirects";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Writable paths the harness owns — writes here never need Write/Edit's APEX
|
|
8
|
+
* gates. Parity safe_paths.py `SAFE_WRITE_PATHS`; `~/.claude/fusengine-cache`
|
|
9
|
+
* is rebranded to `~/.fuse-harness/cache`, `~/.claude/logs` is unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export const SAFE_WRITE_PATHS: readonly string[] = [fusengineCache(), join(claudeHome(), "logs")];
|
|
12
|
+
|
|
13
|
+
/** Raw, un-expanded forms of {@link SAFE_WRITE_PATHS} for substring matching
|
|
14
|
+
* when a shell didn't expand `~` (parity safe_paths.py `_SAFE_RAW`). */
|
|
15
|
+
const SAFE_WRITE_RAW: readonly string[] = ["~/.fuse-harness/cache", "~/.claude/logs"];
|
|
16
|
+
|
|
17
|
+
/** Strip quotes, expand `~`/`$HOME`, normalize (parity safe_paths.resolve_path). */
|
|
18
|
+
function resolvePath(raw: string): string {
|
|
19
|
+
const stripped = raw.trim().replace(/^['"]|['"]$/g, "");
|
|
20
|
+
// POSIX tilde-prefix: only `~` alone or a `~/` prefix is `$HOME`-relative.
|
|
21
|
+
// `~user`, `~2xyz`, `~+`, `~N` are login-name/dir-stack forms — leave unexpanded.
|
|
22
|
+
const expanded = stripped === "~" || stripped.startsWith("~/") ? homedir() + stripped.slice(1) : stripped;
|
|
23
|
+
return normalize(expanded.replace(/\$HOME/g, homedir()));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when a `>`/`>>` redirect targets a harness-owned safe path (parity is_safe_write_path). */
|
|
27
|
+
export function isSafeWritePath(cmd: string): boolean {
|
|
28
|
+
const targets = shellOutputRedirects(cmd)
|
|
29
|
+
.map((redirect) => resolvePath(redirect.target))
|
|
30
|
+
.filter((target) => target !== "/dev/null");
|
|
31
|
+
return targets.length > 0 && targets.every((target) => SAFE_WRITE_PATHS.some((safe) => target === safe || target.startsWith(safe + "/")));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Extract the file argument of `tee`/`dd of=` (parity extract_command_target). */
|
|
35
|
+
function extractCommandTarget(cmd: string): string | null {
|
|
36
|
+
const tee = cmd.match(/\btee\s+(?:-[a-z]\s+)*(\S+)/);
|
|
37
|
+
if (tee?.[1]) return resolvePath(tee[1]);
|
|
38
|
+
const dd = cmd.match(/\bdd\b[^|]*\bof=(\S+)/);
|
|
39
|
+
return dd?.[1] ? resolvePath(dd[1]) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True when a `tee`/`dd` target is a harness-owned safe path (parity is_safe_command_target). */
|
|
43
|
+
export function isSafeCommandTarget(cmd: string): boolean {
|
|
44
|
+
const target = extractCommandTarget(cmd);
|
|
45
|
+
return target !== null && SAFE_WRITE_PATHS.some((safe) => target === safe || target.startsWith(safe + "/"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* True when the command quotes a safe path as a string literal (parity
|
|
50
|
+
* has_safe_write_target, hardened: the Python original — and this function
|
|
51
|
+
* before this fix — does an unanchored substring match, which fail-opens any
|
|
52
|
+
* `node -e` containing the safe path text anywhere, including inside an inert
|
|
53
|
+
* comment. Requiring it to appear as a quoted literal still covers the real
|
|
54
|
+
* use case (`fs.appendFileSync('~/.fuse-harness/cache/x.json', ...)`).
|
|
55
|
+
*/
|
|
56
|
+
export function hasSafeWriteTarget(cmd: string): boolean {
|
|
57
|
+
// Quote-anchored segment boundary: exact quoted literal OR quoted sub-path
|
|
58
|
+
// (e.g. 'fusengineCache()/x.json' — a real file under the safe dir).
|
|
59
|
+
const quoted = (p: string): boolean =>
|
|
60
|
+
cmd.includes(`'${p}'`) || cmd.includes(`"${p}"`) || cmd.includes(`'${p}/`) || cmd.includes(`"${p}/`);
|
|
61
|
+
return SAFE_WRITE_PATHS.some(quoted) || SAFE_WRITE_RAW.some(quoted);
|
|
62
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Prompt } from "../../prompt/types";
|
|
2
|
+
import type { GuardContext } from "./context";
|
|
3
|
+
import { hasSafeWriteTarget, isSafeCommandTarget, isSafeWritePath } from "./bash-write-safe-paths";
|
|
4
|
+
import { shellOutputRedirects } from "./bash-write-redirects";
|
|
5
|
+
import {
|
|
6
|
+
ASK_WRITERS, CODE_COMMAND_WRITE, CODE_MUTATORS, NODE_WRITES,
|
|
7
|
+
PYTHON_C_ANCHOR, PYTHON_WRITES, RUBY_WRITES, SAFE_PREFIXES, SESSION_STATE_FRAGMENT,
|
|
8
|
+
} from "./bash-write-patterns";
|
|
9
|
+
|
|
10
|
+
export { ASK_WRITERS, CODE_MUTATORS, CODE_REDIRECT, FILE_REDIRECT, SAFE_PREFIXES, SESSION_STATE_FRAGMENT } from "./bash-write-patterns";
|
|
11
|
+
|
|
12
|
+
function blockCodeWrite(reason: string): Prompt {
|
|
13
|
+
return { kind: "block", title: "Bash write to code file", reason, actions: ["Use the Write/Edit tool instead"] };
|
|
14
|
+
}
|
|
15
|
+
function askFileWrite(reason: string, ruleId = "bash-write:file-write"): Prompt {
|
|
16
|
+
return { kind: "ask", ruleId, title: "Bash file write", reason, actions: ["Use the Write/Edit tool instead"] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Blocks shell commands that mutate code files in place (and heredocs/redirects
|
|
21
|
+
* to source files); asks before other file-writing shell commands unless the
|
|
22
|
+
* target is a harness-owned safe path. Forces use of the Write/Edit tool so
|
|
23
|
+
* APEX/SOLID checks are not bypassed.
|
|
24
|
+
*
|
|
25
|
+
* The code-write detectors (CODE_MUTATORS, CODE_COMMAND_WRITE) run BEFORE the
|
|
26
|
+
* SAFE_PREFIXES short-circuit: they are command-position anchored
|
|
27
|
+
* (bash-command-anchor.ts), so a transparent wrapper (`env sed -i src/x.ts`,
|
|
28
|
+
* `timeout 5 patch`, `cp a b; tee src/y.ts`) can no longer smuggle a code write
|
|
29
|
+
* past its safe first token, while a quoted mention (`git commit -m "fix sed -i"`)
|
|
30
|
+
* still falls through. The short-circuit becomes the terminal allow — "first
|
|
31
|
+
* token never writes, nothing above matched".
|
|
32
|
+
*/
|
|
33
|
+
export function bashWriteGuard(ctx: GuardContext): Prompt | null {
|
|
34
|
+
if (ctx.tool !== "Bash" || !ctx.command) return null;
|
|
35
|
+
const cmd: string = ctx.command;
|
|
36
|
+
const stripped = cmd.trim();
|
|
37
|
+
const redirects = shellOutputRedirects(cmd).filter((redirect) => redirect.target !== "/dev/null");
|
|
38
|
+
|
|
39
|
+
const mutator = CODE_MUTATORS.find((m) => m.re.test(cmd));
|
|
40
|
+
if (mutator) return blockCodeWrite(`${mutator.desc} — Use Edit/Write tools instead`);
|
|
41
|
+
if (CODE_COMMAND_WRITE.test(cmd)) return blockCodeWrite("tee/dd into a code file — Use Edit/Write tools instead");
|
|
42
|
+
if (PYTHON_C_ANCHOR.test(cmd) && PYTHON_WRITES.test(cmd)) {
|
|
43
|
+
return blockCodeWrite("Python inline script mutates files/spawns a process — Use Edit/Write tools instead");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (SAFE_PREFIXES.some((p) => stripped.startsWith(p)) && redirects.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (cmd.includes(SESSION_STATE_FRAGMENT)) {
|
|
51
|
+
return {
|
|
52
|
+
kind: "block",
|
|
53
|
+
title: "Session-state tampering",
|
|
54
|
+
reason: "Bash access to the harness session-state directory is a hook-bypass vector — the freshness/APEX enforcement reads it to decide block/allow.",
|
|
55
|
+
actions: ["Never read or write session state from the shell"],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (redirects.length > 0) {
|
|
60
|
+
if (isSafeWritePath(cmd)) return null;
|
|
61
|
+
return redirects.some((redirect) => /\.(?:ts|tsx|js|jsx|py|go|rb|rs|java|kt|php|swift|vue|svelte|astro|css|c|cpp|h)\b/.test(redirect.target))
|
|
62
|
+
? blockCodeWrite("Bash redirect to code file — Use Write/Edit tools (enforces APEX + SOLID specs)")
|
|
63
|
+
: askFileWrite("Shell redirect to file detected. Authorize?", "bash-write:file-redirect");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (/\bnode\s+-e\b/.test(cmd) && NODE_WRITES.test(cmd)) {
|
|
67
|
+
return hasSafeWriteTarget(cmd) ? null : askFileWrite("Node.js write operation detected. Authorize?");
|
|
68
|
+
}
|
|
69
|
+
if (/\bruby\s+-e\b/.test(cmd) && RUBY_WRITES.test(cmd)) return askFileWrite("Ruby write operation detected. Authorize?");
|
|
70
|
+
|
|
71
|
+
const asker = ASK_WRITERS.find((a) => a.re.test(cmd));
|
|
72
|
+
if (asker) {
|
|
73
|
+
return isSafeCommandTarget(cmd) ? null : askFileWrite(`${asker.desc} detected. Authorize?`);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Prompt } from "../../prompt/types";
|
|
2
|
+
|
|
3
|
+
/** Context handed to every guard in the chain. */
|
|
4
|
+
export interface GuardContext {
|
|
5
|
+
tool: string;
|
|
6
|
+
filePath?: string;
|
|
7
|
+
content?: string;
|
|
8
|
+
command?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A single guard: returns a blocking/asking Prompt, or null to continue. */
|
|
12
|
+
export type Guard = (ctx: GuardContext) => Prompt | null;
|