@fusengine/harness 0.1.90 → 0.1.92
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 +3 -2
- 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-CBaOcFZq.mjs → handle-BF1dZFjY.mjs} +1194 -144
- 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-Dy8g9Ybl.mjs +397 -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 +37 -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-budget.ts +144 -0
- package/src/adapters/cursor/context-limit.ts +115 -0
- package/src/adapters/cursor/context.ts +65 -0
- package/src/adapters/cursor/events.ts +38 -0
- package/src/adapters/cursor/index.ts +43 -0
- package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
- package/src/adapters/cursor/interfaces/types.ts +88 -0
- package/src/adapters/cursor/native-response.ts +39 -0
- package/src/adapters/cursor/native-schemas.ts +161 -0
- package/src/adapters/cursor/normalize.ts +188 -0
- package/src/adapters/cursor/plugin-root.ts +103 -0
- package/src/adapters/cursor/respond.ts +170 -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 +197 -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 +106 -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 +55 -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 +99 -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
|
@@ -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;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Prompt } from "../../prompt/types";
|
|
2
|
+
import type { Guard, GuardContext } from "./context";
|
|
3
|
+
import { securityGuard } from "./security";
|
|
4
|
+
import { protectedPathGuard } from "./protected-path";
|
|
5
|
+
import { bashWriteGuard } from "./bash-write";
|
|
6
|
+
import { interfaceSeparationGuard } from "./interface-separation";
|
|
7
|
+
import { installGuard } from "./install";
|
|
8
|
+
|
|
9
|
+
export * from "./context";
|
|
10
|
+
export * from "./security";
|
|
11
|
+
export * from "./protected-path";
|
|
12
|
+
export * from "./bash-write";
|
|
13
|
+
export * from "./interface-separation";
|
|
14
|
+
export * from "./install";
|
|
15
|
+
|
|
16
|
+
/** Ordered guard chain: critical/security + protected first, then writes/installs. */
|
|
17
|
+
export const GUARDS: ReadonlyArray<Guard> = [
|
|
18
|
+
securityGuard,
|
|
19
|
+
protectedPathGuard,
|
|
20
|
+
bashWriteGuard,
|
|
21
|
+
interfaceSeparationGuard,
|
|
22
|
+
installGuard,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** Block prompt returned when a guard or gate throws (fail-closed). */
|
|
26
|
+
export const FAIL_CLOSED: Prompt = {
|
|
27
|
+
kind: "block",
|
|
28
|
+
title: "Policy error",
|
|
29
|
+
reason: "A policy check errored — blocked for safety (fail-closed).",
|
|
30
|
+
actions: ["Fix the failing guard/gate, then retry"],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const USER_GUARDS: Guard[] = [];
|
|
34
|
+
|
|
35
|
+
/** Register a user guard — runs AFTER the privileged core chain (two-tier). */
|
|
36
|
+
export function registerGuard(guard: Guard): void {
|
|
37
|
+
USER_GUARDS.push(guard);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Remove all registered user guards (mainly for tests). */
|
|
41
|
+
export function clearUserGuards(): void {
|
|
42
|
+
USER_GUARDS.length = 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run the guard chain — privileged core guards first, then user guards — and
|
|
47
|
+
* return the first firing Prompt, else null. Fail-closed: a guard that throws
|
|
48
|
+
* blocks (never silently passes).
|
|
49
|
+
*/
|
|
50
|
+
export function runGuards(ctx: GuardContext): Prompt | null {
|
|
51
|
+
for (const guard of [...GUARDS, ...USER_GUARDS]) {
|
|
52
|
+
let hit: Prompt | null;
|
|
53
|
+
try {
|
|
54
|
+
hit = guard(ctx);
|
|
55
|
+
} catch {
|
|
56
|
+
return FAIL_CLOSED;
|
|
57
|
+
}
|
|
58
|
+
if (hit) return hit;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Prompt } from "../../prompt/types";
|
|
2
|
+
import type { GuardContext } from "./context";
|
|
3
|
+
import { matchPatterns, PROJECT_INSTALL, SYSTEM_INSTALL, isRalphMode } from "../patterns";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Asks for confirmation before a dependency or system package install. In Ralph
|
|
7
|
+
* mode (opt-in `RALPH_MODE`) a PROJECT install is auto-approved for autonomous
|
|
8
|
+
* runs (parity install-guard.py:52), but a SYSTEM install always asks.
|
|
9
|
+
*/
|
|
10
|
+
export function installGuard(ctx: GuardContext): Prompt | null {
|
|
11
|
+
if (ctx.tool !== "Bash" || !ctx.command) return null;
|
|
12
|
+
const isSystem = matchPatterns(ctx.command, SYSTEM_INSTALL);
|
|
13
|
+
const isProject = matchPatterns(ctx.command, PROJECT_INSTALL);
|
|
14
|
+
if (!isSystem && !isProject) return null;
|
|
15
|
+
if (!isSystem && isProject && isRalphMode()) return null;
|
|
16
|
+
return {
|
|
17
|
+
kind: "ask",
|
|
18
|
+
title: "Dependency install",
|
|
19
|
+
reason: `This command installs packages: ${ctx.command.trim()}`,
|
|
20
|
+
actions: ["Confirm this install is intended"],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended interface-syntax violations (owner E3, advisory-first): the
|
|
3
|
+
* syntaxes the guard's legacy regexes miss — Swift `public protocol`,
|
|
4
|
+
* Java/Kotlin `sealed`/`fun interface`, Go unexported top-level interface
|
|
5
|
+
* (`type foo interface {` at column 0 — indented locals stay excluded, D0.2
|
|
6
|
+
* invariant). Detection rides the conventions module on MASKED content; the
|
|
7
|
+
* caller wraps the verdict with `rolloutVerdict`, so these surface as
|
|
8
|
+
* `inform` until `FUSE_CONVENTIONS_MODE=deny`. Legacy cases never reach this
|
|
9
|
+
* file (the guard's hard-deny branches return first).
|
|
10
|
+
*/
|
|
11
|
+
import { declaresInterface } from "../conventions/interfaces";
|
|
12
|
+
import { maskCommentsAndStrings } from "../conventions/strip";
|
|
13
|
+
import { langOfPath, lexProfileOf } from "../conventions/langs";
|
|
14
|
+
|
|
15
|
+
/** Legacy anchors (kept in sync with interface-separation.ts by test). */
|
|
16
|
+
const SWIFT_LEGACY_RE = /^\s*protocol\s+[A-Z]/m;
|
|
17
|
+
const JAVA_LEGACY_RE = /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:interface|record)\s+[A-Z]/m;
|
|
18
|
+
const GO_LEGACY_RE = /^\s*type\s+[A-Z]\w*\s+interface\b/m;
|
|
19
|
+
|
|
20
|
+
const inAny = (path: string, ...frags: string[]): boolean => frags.some((f) => path.includes(f));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The extended-syntax violation for a file, or null when clean/legacy.
|
|
24
|
+
* @param path - The written/edited file's path.
|
|
25
|
+
* @param content - Its raw content (masked internally).
|
|
26
|
+
* @returns Message + action for the advisory prompt.
|
|
27
|
+
*/
|
|
28
|
+
export function extendedInterfaceViolation(path: string, content: string): { msg: string; action: string } | null {
|
|
29
|
+
if (!declaresInterface(path, content)) return null;
|
|
30
|
+
const masked = maskCommentsAndStrings(content, lexProfileOf(langOfPath(path) ?? "ts"));
|
|
31
|
+
if (/\.swift$/.test(path) && inAny(path, "View/", "Views/", "Component/", "Components/") && !SWIFT_LEGACY_RE.test(masked)) {
|
|
32
|
+
return { msg: "Protocol in view file. Move to Sources/Interfaces/", action: "Move the protocol to Sources/Interfaces/" };
|
|
33
|
+
}
|
|
34
|
+
if (/\.(java|kt)$/.test(path) && inAny(path, "controller/", "controllers/", "handler/", "handlers/") && !JAVA_LEGACY_RE.test(masked)) {
|
|
35
|
+
return { msg: "Interface in controller file. Move to interfaces/ package", action: "Move the interface to the interfaces/ package" };
|
|
36
|
+
}
|
|
37
|
+
if (/\.go$/.test(path) && inAny(path, "handler/", "handlers/", "controller/", "controllers/") && !GO_LEGACY_RE.test(masked)) {
|
|
38
|
+
return { msg: "Interface in handler file. Move to internal/interfaces/", action: "Move the interface to internal/interfaces/" };
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|