@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,126 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { LOCK_FAILED, withTrackLockSync } from "../../tracking/track-lock-sync";
|
|
5
|
+
import { atomicWrite } from "../../util/json-io";
|
|
6
|
+
import { sanitizeSessionId, sessionsDir } from "../home-state";
|
|
7
|
+
import { commandToString } from "../command-string";
|
|
8
|
+
import { displayCodeForAction, hashForAction } from "./confirm-code";
|
|
9
|
+
import { isSubagentActive } from "./confirm-subagent";
|
|
10
|
+
import type { CodexPromptOrigin } from "./codex-prompt-origin";
|
|
11
|
+
|
|
12
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
13
|
+
const STATE_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
type RejectReason = "no-token" | "mismatch" | "expired" | "already-consumed" | "missing-tool-use-id" | "state-io";
|
|
16
|
+
type Action = Readonly<{ hash: string; code: string; command: string; ts: number }>;
|
|
17
|
+
type Receipt = Action & Readonly<{ toolUseId: string }>;
|
|
18
|
+
type CodexState = Readonly<{ codexConfirmPending?: Action; codexConfirmToken?: Action; codexConfirmReceipt?: Receipt }>;
|
|
19
|
+
|
|
20
|
+
function statePath(sid: string, home: string): string {
|
|
21
|
+
return join(sessionsDir(home), `codex-confirm-${sid}.json`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function lockDir(sid: string, home: string): string {
|
|
25
|
+
return join(sessionsDir(home), ".confirm-locks", sid);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadState(sid: string, home: string): CodexState {
|
|
29
|
+
const path = statePath(sid, home);
|
|
30
|
+
if (!existsSync(path)) return {};
|
|
31
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as CodexState : {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function saveState(sid: string, state: CodexState, home: string): void {
|
|
36
|
+
mkdirSync(sessionsDir(home), { recursive: true, mode: 0o700 });
|
|
37
|
+
atomicWrite(statePath(sid, home), JSON.stringify(state, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Canonical identity of one Codex shell action and its display token. */
|
|
41
|
+
export function codexAction(tool: string, cwd: string, command: unknown, now: number): Action | null {
|
|
42
|
+
const canonicalCommand = commandToString(command);
|
|
43
|
+
if (!canonicalCommand) return null;
|
|
44
|
+
const identity = JSON.stringify({ version: STATE_VERSION, harness: "codex", tool, cwd: resolve(cwd), command: canonicalCommand });
|
|
45
|
+
return { hash: hashForAction(identity), code: displayCodeForAction(identity), command: canonicalCommand, ts: now };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Atomically authorize or reject a Codex action, recording the current denial when needed. */
|
|
49
|
+
export function authorizeCodexAction(
|
|
50
|
+
sessionIdRaw: unknown,
|
|
51
|
+
action: Action,
|
|
52
|
+
toolUseId: string | undefined,
|
|
53
|
+
now: number,
|
|
54
|
+
home: string = homedir(),
|
|
55
|
+
): { allow: true } | { allow: false; reason: RejectReason } {
|
|
56
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
57
|
+
if (!sid) return { allow: false, reason: "state-io" };
|
|
58
|
+
try {
|
|
59
|
+
const result = withTrackLockSync(lockDir(sid, home), () => {
|
|
60
|
+
const state = loadState(sid, home);
|
|
61
|
+
const token = state.codexConfirmToken;
|
|
62
|
+
const receipt = state.codexConfirmReceipt;
|
|
63
|
+
const receiptExpired = receipt !== undefined && now - receipt.ts > TTL_MS;
|
|
64
|
+
if (!receiptExpired && receipt?.hash === action.hash && receipt.toolUseId === toolUseId && toolUseId) return { allow: true } as const;
|
|
65
|
+
|
|
66
|
+
let reason: RejectReason = "no-token";
|
|
67
|
+
if (token) {
|
|
68
|
+
if (token.hash !== action.hash) reason = "mismatch";
|
|
69
|
+
else if (now - token.ts > TTL_MS) reason = "expired";
|
|
70
|
+
else if (!toolUseId) reason = "missing-tool-use-id";
|
|
71
|
+
else {
|
|
72
|
+
const { codexConfirmToken: _token, codexConfirmPending: _pending, ...rest } = state;
|
|
73
|
+
saveState(sid, { ...rest, codexConfirmReceipt: { ...action, toolUseId } satisfies Receipt }, home);
|
|
74
|
+
return { allow: true } as const;
|
|
75
|
+
}
|
|
76
|
+
} else if (receipt?.hash === action.hash) reason = receiptExpired ? "expired" : "already-consumed";
|
|
77
|
+
|
|
78
|
+
if (reason === "missing-tool-use-id") return { allow: false, reason } as const;
|
|
79
|
+
const pending = state.codexConfirmPending;
|
|
80
|
+
const stablePending = pending?.hash === action.hash ? pending : action;
|
|
81
|
+
const { codexConfirmToken: _token, codexConfirmReceipt: _receipt, ...withoutReceipt } = state;
|
|
82
|
+
const rest = receiptExpired ? withoutReceipt : { ...withoutReceipt, codexConfirmReceipt: receipt };
|
|
83
|
+
saveState(sid, { ...rest, codexConfirmPending: stablePending }, home);
|
|
84
|
+
return { allow: false, reason } as const;
|
|
85
|
+
});
|
|
86
|
+
return result === LOCK_FAILED ? { allow: false, reason: "state-io" } : result;
|
|
87
|
+
} catch {
|
|
88
|
+
return { allow: false, reason: "state-io" };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Atomically arm the last Codex denial, consuming its pending record exactly once.
|
|
94
|
+
* A classified root prompt may bypass G0; classified subagent/unknown prompts fail closed.
|
|
95
|
+
*/
|
|
96
|
+
export function submitCodexConfirmation(
|
|
97
|
+
sessionIdRaw: unknown,
|
|
98
|
+
text: string,
|
|
99
|
+
now: number,
|
|
100
|
+
home: string = homedir(),
|
|
101
|
+
origin?: CodexPromptOrigin,
|
|
102
|
+
): void {
|
|
103
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
104
|
+
if (!sid) return;
|
|
105
|
+
const refusal = /\b(non|no|stop|annule|cancel|abort|nope|laisse tomber|pas maintenant)\b/i.test(text);
|
|
106
|
+
const typedCode = text.trim().match(/^confirm[ _-]*([0-9a-f]{4})$/i)?.[1];
|
|
107
|
+
try {
|
|
108
|
+
withTrackLockSync(lockDir(sid, home), () => {
|
|
109
|
+
const state = loadState(sid, home);
|
|
110
|
+
if (origin !== undefined && origin !== "root") return;
|
|
111
|
+
if (refusal) {
|
|
112
|
+
const { codexConfirmPending: _pending, codexConfirmToken: _token, ...rest } = state;
|
|
113
|
+
saveState(sid, rest, home);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!typedCode) return;
|
|
117
|
+
if (origin === undefined && isSubagentActive(sid, now, home)) return;
|
|
118
|
+
const pending = state.codexConfirmPending;
|
|
119
|
+
if (!pending || pending.code.toLowerCase() !== typedCode.toLowerCase()) return;
|
|
120
|
+
const { codexConfirmPending: _pending, ...rest } = state;
|
|
121
|
+
saveState(sid, { ...rest, codexConfirmToken: { ...pending, ts: now } }, home);
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
// Submission is advisory state wiring; hook execution must remain available.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Provenance classification for a Codex `UserPromptSubmit` payload. */
|
|
2
|
+
export type CodexPromptOrigin = "root" | "subagent" | "unknown";
|
|
3
|
+
|
|
4
|
+
const AGENT_FIELDS = ["agent_id", "agent_type"] as const;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Classify Codex prompt provenance from its optional agent metadata.
|
|
8
|
+
* Missing metadata is the observed root shape; malformed metadata fails closed.
|
|
9
|
+
*/
|
|
10
|
+
export function codexPromptOrigin(payload: Record<string, unknown>): CodexPromptOrigin {
|
|
11
|
+
const present = AGENT_FIELDS.filter((field) => Object.hasOwn(payload, field));
|
|
12
|
+
if (present.length === 0) return "root";
|
|
13
|
+
return present.every((field) => typeof payload[field] === "string" && payload[field].trim().length > 0)
|
|
14
|
+
? "subagent"
|
|
15
|
+
: "unknown";
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Full SHA-256 hex digest (64 chars) of an action string (the exact command
|
|
5
|
+
* a deny prompt showed). This is the ONLY value ever compared to authorize a
|
|
6
|
+
* confirmation (G3, confirm-state.ts) — collision-resistant, unlike the
|
|
7
|
+
* 4-char {@link displayCodeForAction} below.
|
|
8
|
+
* @param action - The command/content string a deny prompt showed.
|
|
9
|
+
*/
|
|
10
|
+
export function hashForAction(action: string): string {
|
|
11
|
+
return createHash("sha256").update(action).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 4-hex-char PREFIX of {@link hashForAction}, for DISPLAY ONLY — never a
|
|
16
|
+
* security boundary. It exists purely so a human has something short to
|
|
17
|
+
* retype ("CONFIRM 4f2a"); at 16 bits it collides constantly across
|
|
18
|
+
* unrelated actions (birthday bound ~a few hundred actions/session), and the
|
|
19
|
+
* agent ALWAYS sees it too — `systemMessage` never reaches the user or model
|
|
20
|
+
* under Codex (measured 2026-08-03: 0 occurrence in the rollout, nothing on
|
|
21
|
+
* screen), so `permissionDecisionReason` is the only channel, and the model
|
|
22
|
+
* reads its own code back in the very tool result that got blocked.
|
|
23
|
+
*
|
|
24
|
+
* What actually scopes a confirmation to ONE specific action is the full
|
|
25
|
+
* hash recorded at deny time and re-checked at consume time (see
|
|
26
|
+
* confirm-pending.ts / confirm-state.ts). What prevents an agent from typing
|
|
27
|
+
* its own code back to self-approve is G0 (fail-closed while a sub-agent is
|
|
28
|
+
* active) — never the code's secrecy, because it has none.
|
|
29
|
+
* @param action - The command/content string to derive the display code from.
|
|
30
|
+
*/
|
|
31
|
+
export function displayCodeForAction(action: string): string {
|
|
32
|
+
return hashForAction(action).slice(0, 4);
|
|
33
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Prompt } from "../../prompt/types";
|
|
2
|
+
import { displayCodeForAction, hashForAction } from "./confirm-code";
|
|
3
|
+
import { isIrreversible } from "./confirm-irreversible";
|
|
4
|
+
import { recordPendingDeny } from "./confirm-pending";
|
|
5
|
+
import { consumeConfirmToken } from "./confirm-state";
|
|
6
|
+
import { authorizeCodexAction, codexAction } from "./codex-confirm";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Harnesses where `respond.ts` silently downgrades `kind: "ask"` to a hard
|
|
10
|
+
* deny (Codex: `case "codex"`; Kimi: `toKimiResponse` maps `ask` to the same
|
|
11
|
+
* `permissionDecision:"deny"` envelope as `block`). Claude Code keeps native
|
|
12
|
+
* interactive `ask` — this mechanism NEVER applies there.
|
|
13
|
+
*/
|
|
14
|
+
const DEGRADES_ASK_TO_DENY: ReadonlySet<string> = new Set(["codex", "kimi"]);
|
|
15
|
+
|
|
16
|
+
export type ConfirmVerdict = { allow: true } | { allow: false; prompt: Prompt };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether/how a CONFIRM token changes an `ask` prompt about to be downgraded
|
|
20
|
+
* to a deny. Returns `null` when this mechanism doesn't apply AT ALL — any
|
|
21
|
+
* harness other than codex/kimi, any prompt kind other than `ask`, no
|
|
22
|
+
* command to key a hash off, or an irreversible command (G4) — in which case
|
|
23
|
+
* the caller's ORIGINAL prompt/response path runs completely unchanged. That
|
|
24
|
+
* `null` fast-path, hit on every claude-code call and every non-`ask` prompt,
|
|
25
|
+
* IS the non-regression property.
|
|
26
|
+
* @param id - Harness target id.
|
|
27
|
+
* @param prompt - The prompt `gate()` returned.
|
|
28
|
+
* @param command - `event.command` for the tool-use under judgment.
|
|
29
|
+
* @param sessionId - `event.sessionId`.
|
|
30
|
+
* @param now - Epoch ms.
|
|
31
|
+
* @param home - Test-only OS home override.
|
|
32
|
+
*/
|
|
33
|
+
export function confirmGate(
|
|
34
|
+
id: string,
|
|
35
|
+
prompt: Prompt,
|
|
36
|
+
command: string | undefined,
|
|
37
|
+
sessionId: string,
|
|
38
|
+
now: number,
|
|
39
|
+
home?: string,
|
|
40
|
+
codex?: Readonly<{ tool: string; cwd: string; toolUseId?: string }>,
|
|
41
|
+
): ConfirmVerdict | null {
|
|
42
|
+
if (prompt.kind !== "ask" || !DEGRADES_ASK_TO_DENY.has(id) || !command || isIrreversible(command)) return null;
|
|
43
|
+
try {
|
|
44
|
+
if (id === "codex" && codex) {
|
|
45
|
+
const action = codexAction(codex.tool, codex.cwd, command, now);
|
|
46
|
+
if (!action) return null;
|
|
47
|
+
const verdict = authorizeCodexAction(sessionId, action, codex.toolUseId, now, home);
|
|
48
|
+
if (verdict.allow) return verdict;
|
|
49
|
+
const ruleId = prompt.ruleId ?? "policy:ask";
|
|
50
|
+
const diagnostic = `rule ID: ${ruleId}\ncanonical command: ${action.command}\nexpected token: CONFIRM ${action.code}\nrejection: ${verdict.reason}`;
|
|
51
|
+
return { allow: false, prompt: { ...prompt, reason: `${prompt.reason}\nPour autoriser, réponds : CONFIRM ${action.code}\n${diagnostic}` } };
|
|
52
|
+
}
|
|
53
|
+
const hash = hashForAction(command);
|
|
54
|
+
if (consumeConfirmToken(sessionId, hash, now, home)) return { allow: true };
|
|
55
|
+
const code = displayCodeForAction(command);
|
|
56
|
+
recordPendingDeny(sessionId, hash, code, now, home);
|
|
57
|
+
return { allow: false, prompt: { ...prompt, reason: `${prompt.reason}\nPour autoriser, réponds : CONFIRM ${code}` } };
|
|
58
|
+
} catch {
|
|
59
|
+
// A state-io failure (full disk, unwritable home) must fall back to the
|
|
60
|
+
// plain deny, never crash the hook — same invariant as confirm-submit.ts.
|
|
61
|
+
// The caller's `confirm ? confirm.prompt : prompt` treats `null` exactly
|
|
62
|
+
// like "mechanism doesn't apply", i.e. the pre-CONFIRM deny unchanged.
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { GIT_BLOCKED, matchPatterns } from "../../policy/patterns";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A single flag token carrying BOTH `r` and `f` in either order (`-rf`,
|
|
5
|
+
* `-fr`, `-rfv`, …) — same lookahead technique as patterns.ts's
|
|
6
|
+
* `CLEAN_FD_FLAG`, reused here rather than re-derived (DRY).
|
|
7
|
+
*/
|
|
8
|
+
const RM_COMBINED_FLAG = /\s-(?=[a-zA-Z]*r)(?=[a-zA-Z]*f)[a-zA-Z]+(?:\s|=|$)/;
|
|
9
|
+
/** Split flags `-r ... -f` (either order) within the SAME command segment (no `;&|` between). */
|
|
10
|
+
const RM_SPLIT_FLAG = /\brm\b[^;&|\n]*\s-r\b[^;&|\n]*\s-f\b|\brm\b[^;&|\n]*\s-f\b[^;&|\n]*\s-r\b/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generic `rm -rf`/`-fr` (ANY target, not just `/`/`~`) — deliberately
|
|
14
|
+
* broader than `src/codex-rules/rules/rm-variants.ts`'s root/system-path-only
|
|
15
|
+
* forbidden list: G4 must treat every recursive force-delete as
|
|
16
|
+
* never-confirmable, not only the ones aimed at a system path.
|
|
17
|
+
*/
|
|
18
|
+
function isRmRf(cmd: string): boolean {
|
|
19
|
+
return /\brm\b/.test(cmd) && (RM_COMBINED_FLAG.test(cmd) || RM_SPLIT_FLAG.test(cmd));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* G4: commands NEVER unlockable by a CONFIRM token, regardless of a valid,
|
|
24
|
+
* fresh, correctly-scoped one. Reuses {@link GIT_BLOCKED} (push --force,
|
|
25
|
+
* reset --hard, clean -fd, branch -D, rebase --force — DRY, same list the
|
|
26
|
+
* rest of the policy already enforces) plus a generic `rm -rf` check.
|
|
27
|
+
* @param cmd - The command a prompt is about (empty/undefined = not irreversible).
|
|
28
|
+
*/
|
|
29
|
+
export function isIrreversible(cmd: string | undefined): boolean {
|
|
30
|
+
if (!cmd) return false;
|
|
31
|
+
return matchPatterns(cmd, GIT_BLOCKED) || isRmRf(cmd);
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { loadSessionState, saveSessionState, sanitizeSessionId } from "../home-state";
|
|
3
|
+
|
|
4
|
+
/** The last `ask`-turned-deny for a session: its full hash plus the short code shown to the human. */
|
|
5
|
+
export interface PendingDeny {
|
|
6
|
+
hash: string;
|
|
7
|
+
code: string;
|
|
8
|
+
ts: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Record the action a deny prompt just showed (overwrites any previous
|
|
13
|
+
* pending deny for this session — only the LAST blocked action can be
|
|
14
|
+
* confirmed). Pure bookkeeping, no gate: every ask-turned-deny records one,
|
|
15
|
+
* whether or not the user ever confirms it.
|
|
16
|
+
* @param sessionIdRaw - Raw session id from the payload.
|
|
17
|
+
* @param hash - Full {@link import("./confirm-code").hashForAction} of the command.
|
|
18
|
+
* @param code - The short display code shown alongside it.
|
|
19
|
+
* @param now - Epoch ms.
|
|
20
|
+
* @param home - Test-only OS home override.
|
|
21
|
+
*/
|
|
22
|
+
export function recordPendingDeny(sessionIdRaw: unknown, hash: string, code: string, now: number, home: string = homedir()): void {
|
|
23
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
24
|
+
if (!sid) return;
|
|
25
|
+
const state = loadSessionState(sid, home);
|
|
26
|
+
saveSessionState(sid, { ...state, pendingDeny: { hash, code, ts: now } satisfies PendingDeny }, home);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Read back the pending deny for a session (`undefined` when none/invalid session id). */
|
|
30
|
+
export function getPendingDeny(sessionIdRaw: unknown, home: string = homedir()): PendingDeny | undefined {
|
|
31
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
32
|
+
if (!sid) return undefined;
|
|
33
|
+
return loadSessionState(sid, home).pendingDeny as PendingDeny | undefined;
|
|
34
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { loadSessionState, saveSessionState, sanitizeSessionId } from "../home-state";
|
|
3
|
+
import { isSubagentActive } from "./confirm-subagent";
|
|
4
|
+
|
|
5
|
+
/** A posed confirmation token: the FULL action hash (G3) plus its mint timestamp (G2). */
|
|
6
|
+
interface ConfirmToken {
|
|
7
|
+
hash: string;
|
|
8
|
+
ts: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Confirmation freshness window (G2) — 5 minutes, matches the validated prototype. */
|
|
12
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pose a confirmation token for this session's pending action. No-op while
|
|
16
|
+
* {@link isSubagentActive} (G0, confirm-subagent.ts) — the sole gate that
|
|
17
|
+
* stops an agent, which always sees the display code in its own blocked tool
|
|
18
|
+
* result, from typing it back to self-approve.
|
|
19
|
+
*/
|
|
20
|
+
export function placeConfirmToken(sessionIdRaw: unknown, hash: string, now: number, home: string = homedir(), env: Record<string, string | undefined> = process.env): void {
|
|
21
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
22
|
+
if (!sid || isSubagentActive(sid, now, home, env)) return;
|
|
23
|
+
const state = loadSessionState(sid, home);
|
|
24
|
+
saveSessionState(sid, { ...state, confirmToken: { hash, ts: now } satisfies ConfirmToken }, home);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Invalidate any pending token for this session (G5: an explicit refusal). */
|
|
28
|
+
export function dropConfirmToken(sessionIdRaw: unknown, home: string = homedir()): void {
|
|
29
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
30
|
+
if (!sid) return;
|
|
31
|
+
const state = loadSessionState(sid, home);
|
|
32
|
+
if (state.confirmToken === undefined) return;
|
|
33
|
+
const { confirmToken: _drop, ...rest } = state;
|
|
34
|
+
saveSessionState(sid, rest, home);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Consume a token that matches `hash` exactly (G3), whether or not it's
|
|
39
|
+
* fresh — a mismatched hash leaves the token untouched (it may still be
|
|
40
|
+
* valid for the action it actually confirms). G1 (one-shot) + G2 (5-min TTL)
|
|
41
|
+
* both apply only once the hash matches.
|
|
42
|
+
* @returns true = allow (token consumed); false = deny (nothing changed, or
|
|
43
|
+
* the matching token had expired and was dropped).
|
|
44
|
+
*/
|
|
45
|
+
export function consumeConfirmToken(sessionIdRaw: unknown, hash: string, now: number, home: string = homedir()): boolean {
|
|
46
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
47
|
+
if (!sid) return false;
|
|
48
|
+
const tok = loadSessionState(sid, home).confirmToken as ConfirmToken | undefined;
|
|
49
|
+
if (!tok || tok.hash !== hash) return false;
|
|
50
|
+
if (now - tok.ts > TTL_MS) {
|
|
51
|
+
dropConfirmToken(sid, home);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
dropConfirmToken(sid, home);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { parseEnvInt } from "../../config/env";
|
|
3
|
+
import { loadSessionState, saveSessionState, sanitizeSessionId } from "../home-state";
|
|
4
|
+
|
|
5
|
+
/** Env var overriding the G0 cool-down window below (seconds). Unset = the 300s/5min default. */
|
|
6
|
+
const CONFIRM_WINDOW_ENV_KEY = "FUSE_CONFIRM_SUBAGENT_WINDOW_SEC";
|
|
7
|
+
/** Default G0 cool-down (seconds) when {@link CONFIRM_WINDOW_ENV_KEY} is unset/invalid — 300s (5min). */
|
|
8
|
+
const DEFAULT_CONFIRM_WINDOW_SEC = 300;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* G0 cool-down after the last SubagentStart/SubagentStop seen for this
|
|
12
|
+
* session — {@link DEFAULT_CONFIRM_WINDOW_SEC} by default, overridable via
|
|
13
|
+
* `FUSE_CONFIRM_SUBAGENT_WINDOW_SEC` (seconds) without a rebuild. Read this
|
|
14
|
+
* doc before ever touching this value: it is NOT G0's primary protection,
|
|
15
|
+
* and it is not sized to "safely cover a sub-agent's whole runtime".
|
|
16
|
+
*
|
|
17
|
+
* G0's PRIMARY protection is P0, measured live under Codex in a prior
|
|
18
|
+
* session: a sub-agent never receives its own `UserPromptSubmit` — only a
|
|
19
|
+
* human typing into the top-level session does — so a sub-agent is
|
|
20
|
+
* STRUCTURALLY unable to place a CONFIRM token at all, regardless of this
|
|
21
|
+
* window's value. This timestamp is a SECOND belt, covering only the
|
|
22
|
+
* residual case where that structural property does not hold on some future
|
|
23
|
+
* harness (Kimi: not yet measured either way).
|
|
24
|
+
*
|
|
25
|
+
* The default is a deliberate, owner-decided trade-off between over-refusal
|
|
26
|
+
* and usability, NOT a safety-maximizing constant: sub-agents in this
|
|
27
|
+
* ecosystem share the lead session's `session_id`, and the owner runs
|
|
28
|
+
* sub-agents continuously — an earlier 30-minute window froze CONFIRM for
|
|
29
|
+
* the entire session on every single sub-agent call, which neutralizes the
|
|
30
|
+
* feature it protects (a guard that disables what it guards is not a good
|
|
31
|
+
* guard). 5 minutes comfortably covers the case that actually matters — a
|
|
32
|
+
* sub-agent genuinely in flight at the exact moment a human types a CONFIRM
|
|
33
|
+
* code — without pinning a busy session shut. Do not read this value as "the
|
|
34
|
+
* time G0 needs to be safe" — that safety comes from P0.
|
|
35
|
+
*
|
|
36
|
+
* This deliberately reuses {@link parseEnvInt} (`src/config/env.ts`) rather
|
|
37
|
+
* than `resolveTtlSec`/`FUSE_ENFORCE_TTL_SEC` (`src/config/ttl.ts`): that TTL
|
|
38
|
+
* governs APEX evidence freshness (120s default) — an unrelated concern
|
|
39
|
+
* accidentally coupling the two would make lengthening the research-evidence
|
|
40
|
+
* window also lengthen the confirmation cool-down. `resolveTtlSec` also
|
|
41
|
+
* hardcodes its own fallback (`DEFAULT_TTL_SEC` = 120) regardless of which
|
|
42
|
+
* env key is passed, so it cannot express a 300s default either — this key
|
|
43
|
+
* gets its OWN env var and its OWN default via the same underlying
|
|
44
|
+
* `parseEnvInt` primitive, with no new parsing logic.
|
|
45
|
+
*
|
|
46
|
+
* Mechanically this is still a single monotone "last seen" timestamp, NOT a
|
|
47
|
+
* start/stop pair — a symmetric increment/decrement depth counter was tried
|
|
48
|
+
* and rejected: the harness dispatches ONE real SubagentStart/SubagentStop
|
|
49
|
+
* through MULTIPLE concurrent sibling plugin processes (the repo's own
|
|
50
|
+
* "~11-process multi-plugin fan-out"), all doing UNLOCKED read-modify-write
|
|
51
|
+
* on the same session-state file — a duplicated decrement can walk the
|
|
52
|
+
* counter to 0 while a sub-agent is still running, and G0 would then wrongly
|
|
53
|
+
* OPEN. Two ever-growing `starts`/`stops` counters were also rejected: if the
|
|
54
|
+
* fan-out dispatches SubagentStart via N processes and SubagentStop via
|
|
55
|
+
* M ≠ N, `starts > stops` stays true FOREVER and the session is frozen for
|
|
56
|
+
* good — worse than any window.
|
|
57
|
+
*
|
|
58
|
+
* `Math.max(prevSeenAt, now)` avoids both failure modes: an LWW
|
|
59
|
+
* (last-writer-wins) register merged with `max`, which is idempotent,
|
|
60
|
+
* commutative, and associative — under ANY interleaving of concurrent
|
|
61
|
+
* writers (lost updates included), the stored value converges to the
|
|
62
|
+
* largest `now` any writer supplied, and every writer's `now` is a real
|
|
63
|
+
* wall-clock read taken within milliseconds of the true event, so it can
|
|
64
|
+
* never regress below a timestamp it already held. BOTH SubagentStart and
|
|
65
|
+
* SubagentStop bump the SAME field this way — there is no decrement
|
|
66
|
+
* anywhere in this file, so there is nothing for a duplicated event to
|
|
67
|
+
* desynchronize; the flag falls only once this window elapses with no
|
|
68
|
+
* further sighting.
|
|
69
|
+
* @param env - Env map to resolve the override from (defaults to `process.env`; tests inject a plain object).
|
|
70
|
+
*/
|
|
71
|
+
function subagentWindowMs(env: Record<string, string | undefined> = process.env): number {
|
|
72
|
+
return parseEnvInt(env[CONFIRM_WINDOW_ENV_KEY], DEFAULT_CONFIRM_WINDOW_SEC) * 1000;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* G0: true within {@link subagentWindowMs} of the last SubagentStart/
|
|
77
|
+
* SubagentStop seen for this session. An invalid/missing session id is
|
|
78
|
+
* treated as active too — fail-closed, never pose a token when in doubt.
|
|
79
|
+
*/
|
|
80
|
+
export function isSubagentActive(sessionIdRaw: unknown, now: number, home: string = homedir(), env: Record<string, string | undefined> = process.env): boolean {
|
|
81
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
82
|
+
if (!sid) return true;
|
|
83
|
+
const seenAt = loadSessionState(sid, home).subagentSeenAt;
|
|
84
|
+
return typeof seenAt === "number" && now - seenAt < subagentWindowMs(env);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Record a SubagentStart OR SubagentStop sighting for this session as a
|
|
89
|
+
* monotone max-write (see {@link subagentWindowMs}'s doc for why this can
|
|
90
|
+
* never desync unsafely under concurrent racy writers). Both events call
|
|
91
|
+
* this the SAME way — there is no separate "clear" path.
|
|
92
|
+
*
|
|
93
|
+
* Called unconditionally, BEFORE scope branching, on every SubagentStart/Stop
|
|
94
|
+
* across every plugin (dispatch.ts) — never allowed to throw: a disk fault
|
|
95
|
+
* here (full disk, unwritable home) must degrade to "sighting not recorded",
|
|
96
|
+
* never crash sub-agent lifecycle dispatch for scopes that have nothing to do
|
|
97
|
+
* with CONFIRM. Safe to swallow: G0's PRIMARY protection is P0 (a sub-agent
|
|
98
|
+
* structurally never receives its own UserPromptSubmit, confirm-gate.ts's
|
|
99
|
+
* doc) — this window is only a second belt, so losing one sighting degrades
|
|
100
|
+
* that belt, it does not open the gate.
|
|
101
|
+
*/
|
|
102
|
+
export function markSubagentSeen(sessionIdRaw: unknown, now: number, home: string = homedir()): void {
|
|
103
|
+
try {
|
|
104
|
+
const sid = sanitizeSessionId(sessionIdRaw);
|
|
105
|
+
if (!sid) return;
|
|
106
|
+
const state = loadSessionState(sid, home);
|
|
107
|
+
const prev = typeof state.subagentSeenAt === "number" ? state.subagentSeenAt : 0;
|
|
108
|
+
saveSessionState(sid, { ...state, subagentSeenAt: Math.max(prev, now) }, home);
|
|
109
|
+
} catch {
|
|
110
|
+
// A state-io failure must never break the hook (same invariant as confirm-submit.ts).
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getPendingDeny } from "./confirm-pending";
|
|
2
|
+
import { dropConfirmToken, placeConfirmToken } from "./confirm-state";
|
|
3
|
+
|
|
4
|
+
/** Common explicit-refusal words (fr/en), any of which invalidates a pending token (G5). */
|
|
5
|
+
const REFUSAL_RE = /\b(non|no|stop|annule|cancel|abort|nope|laisse tomber|pas maintenant)\b/i;
|
|
6
|
+
/** `CONFIRM <4-hex-chars>`, case-insensitive, tolerant of `confirm4f2a` / `Confirm-4f2a` / `confirm_4f2a`. */
|
|
7
|
+
const CONFIRM_RE = /confirm[\s_-]*([0-9a-f]{4})\b/i;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse a submitted user prompt for `CONFIRM <code>` or an explicit refusal.
|
|
11
|
+
* A refusal always wins (checked first) and drops any pending token (G5),
|
|
12
|
+
* even if the same text also happens to contain a code. A confirm only
|
|
13
|
+
* places a token when its 4-char code matches this session's LAST pending
|
|
14
|
+
* deny (see confirm-pending.ts) — the code is looked up back to the full
|
|
15
|
+
* hash that deny recorded, never compared as a code-to-code match (that
|
|
16
|
+
* would reopen the collision {@link import("./confirm-code").displayCodeForAction}
|
|
17
|
+
* warns about). Never throws — this runs inside a hook.
|
|
18
|
+
* @param sessionId - The normalized event's session id.
|
|
19
|
+
* @param text - The prompt text ({@link import("../prompt-text").promptText} output).
|
|
20
|
+
* @param now - Epoch ms.
|
|
21
|
+
* @param home - Test-only OS home override.
|
|
22
|
+
*/
|
|
23
|
+
export function handleConfirmSubmit(sessionId: string, text: string, now: number, home?: string): void {
|
|
24
|
+
try {
|
|
25
|
+
if (REFUSAL_RE.test(text)) {
|
|
26
|
+
dropConfirmToken(sessionId, home);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const m = text.match(CONFIRM_RE);
|
|
30
|
+
const typedCode = m?.[1];
|
|
31
|
+
if (!typedCode) return;
|
|
32
|
+
const pending = getPendingDeny(sessionId, home);
|
|
33
|
+
if (!pending || pending.code.toLowerCase() !== typedCode.toLowerCase()) return;
|
|
34
|
+
placeConfirmToken(sessionId, pending.hash, now, home); // G0 enforced inside
|
|
35
|
+
} catch {
|
|
36
|
+
// A state-io failure must never break the hook.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-harness ACTIVATION probing for the core-guards plugin, behind
|
|
3
|
+
* {@link coreGuardsWired}. The owner decision: the solid scope abstains from
|
|
4
|
+
* its file-size deny ONLY when core-guards is installed AND active — an
|
|
5
|
+
* installed-but-disabled plugin must leave the solid deny firing. Safe
|
|
6
|
+
* default, per harness: anything undetectable (unreadable/missing state)
|
|
7
|
+
* reads as NOT active, so the worst case is a duplicate deny — never zero.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
/** File content, or null when unreadable (undetectable → caller's safe default). */
|
|
13
|
+
function readText(path: string): string | null {
|
|
14
|
+
try { return readFileSync(path, "utf8"); } catch { return null; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** First `core-guards@…` value in a name→bool map; undefined when absent. */
|
|
18
|
+
function enabledEntry(map: unknown): boolean | undefined {
|
|
19
|
+
if (typeof map !== "object" || map === null || Array.isArray(map)) return undefined;
|
|
20
|
+
for (const [k, v] of Object.entries(map as Record<string, unknown>)) {
|
|
21
|
+
if (k.startsWith("core-guards@")) return v !== false;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Claude Code: `~/.claude/settings.json` → `enabledPlugins`. core-guards is a
|
|
28
|
+
* REQUIRED marketplace plugin: no entry means active whenever the marketplace
|
|
29
|
+
* copy exists (`installed`); an explicit `false` means disabled. Unreadable
|
|
30
|
+
* settings → false (undetectable).
|
|
31
|
+
* @param home - OS home.
|
|
32
|
+
* @param installed - Whether the marketplace copy of core-guards exists.
|
|
33
|
+
*/
|
|
34
|
+
export function claudeCoreGuardsActive(home: string, installed: boolean): boolean {
|
|
35
|
+
const text = readText(join(home, ".claude", "settings.json"));
|
|
36
|
+
if (text === null) return false;
|
|
37
|
+
try {
|
|
38
|
+
const entry = enabledEntry((JSON.parse(text) as { enabledPlugins?: unknown }).enabledPlugins);
|
|
39
|
+
return entry ?? installed;
|
|
40
|
+
} catch { return false; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Codex: `[plugins."core-guards@<mkt>"] enabled = …` in `~/.codex/config.toml`
|
|
45
|
+
* (minimal section-scoped extraction — no TOML dependency). Missing section,
|
|
46
|
+
* `enabled = false`, or unreadable file → false.
|
|
47
|
+
* @param home - OS home.
|
|
48
|
+
*/
|
|
49
|
+
export function codexCoreGuardsActive(home: string): boolean {
|
|
50
|
+
const text = readText(join(home, ".codex", "config.toml"));
|
|
51
|
+
if (text === null) return false;
|
|
52
|
+
// `^` + `m`: the header must start a real line — an inert one-line comment
|
|
53
|
+
// like `# [plugins."core-guards@mkt"] enabled = true` must never match
|
|
54
|
+
// (verified: without the anchor it does, flipping this to a false "active"
|
|
55
|
+
// and silencing the solid file-size deny — the owner's "never zero deny").
|
|
56
|
+
const m = /^\[plugins\."core-guards@[^"]+"\]\s*enabled\s*=\s*(true|false)/m.exec(text);
|
|
57
|
+
return m?.[1] === "true";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Kimi: `<KIMI_CODE_HOME>/plugins/installed.json` — map form (`id →
|
|
62
|
+
* {enabled?}`) or array form (`[{id, enabled?}]`); an entry counts active
|
|
63
|
+
* unless `enabled === false`. File absent here today → format read
|
|
64
|
+
* tolerantly; missing/unreadable → false (undetectable).
|
|
65
|
+
* @param root - Kimi data root (`KIMI_CODE_HOME` ?? `~/.kimi-code`).
|
|
66
|
+
*/
|
|
67
|
+
export function kimiCoreGuardsActive(root: string): boolean {
|
|
68
|
+
const text = readText(join(root, "plugins", "installed.json"));
|
|
69
|
+
if (text === null) return false;
|
|
70
|
+
try {
|
|
71
|
+
const data = (JSON.parse(text) as { plugins?: unknown }).plugins;
|
|
72
|
+
if (Array.isArray(data)) {
|
|
73
|
+
const hit = data.find((p) => typeof p === "object" && p !== null && (p as { id?: unknown }).id === "core-guards");
|
|
74
|
+
return hit !== undefined && (hit as { enabled?: unknown }).enabled !== false;
|
|
75
|
+
}
|
|
76
|
+
if (typeof data !== "object" || data === null) return false;
|
|
77
|
+
for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
|
|
78
|
+
if (k === "core-guards" || k.startsWith("core-guards@")) {
|
|
79
|
+
return typeof v === "object" && v !== null ? (v as { enabled?: unknown }).enabled !== false : v !== false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
} catch { return false; }
|
|
84
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect whether the core-guards plugin is installed AND ACTIVE for a harness.
|
|
3
|
+
* When it is, its `core` scope already owns the PreToolUse file-size deny
|
|
4
|
+
* (`policy/evaluate.ts`), so the `solid` scope MUST abstain on the same
|
|
5
|
+
* tool-call — otherwise a user with BOTH plugins wired on PreToolUse
|
|
6
|
+
* Write|Edit receives the identical deny twice. Activation probing is
|
|
7
|
+
* per-harness (`core-guards-active.ts`, owner decision):
|
|
8
|
+
* - claude-code: marketplace copy present + `settings.json` `enabledPlugins`
|
|
9
|
+
* (required core: no entry = active; explicit `false` = disabled);
|
|
10
|
+
* - codex: `[plugins."core-guards@…"] enabled = true` in `~/.codex/config.toml`;
|
|
11
|
+
* - kimi: `<KIMI_CODE_HOME>/plugins/installed.json` entry not disabled.
|
|
12
|
+
* SAFE DEFAULT: anything undetectable (unreadable/missing state) reads as
|
|
13
|
+
* NOT active — the solid scope then fires its own deny, so the worst case is
|
|
14
|
+
* a duplicate deny, NEVER zero deny.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { claudeCoreGuardsActive, codexCoreGuardsActive, kimiCoreGuardsActive } from "./core-guards-active";
|
|
20
|
+
|
|
21
|
+
/** Immediate child dir names of `dir`, or [] when unreadable. */
|
|
22
|
+
function children(dir: string): string[] {
|
|
23
|
+
try { return readdirSync(dir); } catch { return []; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Claude: marketplace copy `~/.claude/plugins/marketplaces/<m>/plugins/core-guards`. */
|
|
27
|
+
function claudeInstalled(home: string): boolean {
|
|
28
|
+
const markets = join(home, ".claude", "plugins", "marketplaces");
|
|
29
|
+
return children(markets).some((m) => existsSync(join(markets, m, "plugins", "core-guards")));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* True when core-guards is installed AND active for the harness.
|
|
34
|
+
* @param id - Harness id.
|
|
35
|
+
* @param env - Environment (defaults to `process.env`).
|
|
36
|
+
* @param home - OS home (defaults to `env.HOME` / `os.homedir()`; injectable).
|
|
37
|
+
*/
|
|
38
|
+
export function coreGuardsWired(
|
|
39
|
+
id: string,
|
|
40
|
+
env: Record<string, string | undefined> = process.env,
|
|
41
|
+
home: string = env.HOME ?? homedir(),
|
|
42
|
+
): boolean {
|
|
43
|
+
if (id === "claude-code") return claudeCoreGuardsActive(home, claudeInstalled(home));
|
|
44
|
+
if (id === "codex") return codexCoreGuardsActive(home);
|
|
45
|
+
if (id === "kimi") return kimiCoreGuardsActive(env.KIMI_CODE_HOME ?? join(home, ".kimi-code"));
|
|
46
|
+
return false;
|
|
47
|
+
}
|