oh-my-codex 0.20.0 → 0.20.2
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/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +10 -1
- package/dist/adapt/__tests__/hermes.test.js.map +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
- package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
- package/dist/auth/__tests__/hotswap.test.js +183 -0
- package/dist/auth/__tests__/hotswap.test.js.map +1 -0
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +17 -4
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/paths.js.map +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +4 -4
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/autopilot/fsm.d.ts +1 -1
- package/dist/autopilot/fsm.d.ts.map +1 -1
- package/dist/autoresearch/goal.js.map +1 -1
- package/dist/autoresearch/runtime.js.map +1 -1
- package/dist/capabilities/lockfile.js.map +1 -1
- package/dist/catalog/reader.js.map +1 -1
- package/dist/catalog/skill-mirror.js.map +1 -1
- package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/agents.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
- package/dist/cli/__tests__/capabilities.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +27 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js +56 -4
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +735 -14
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +216 -11
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +49 -3
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +84 -12
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +128 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +8 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +187 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +129 -0
- package/dist/cli/__tests__/ralplan.test.js.map +1 -0
- package/dist/cli/__tests__/resume.test.js +6 -1
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/session-search.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +131 -5
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
- package/dist/cli/__tests__/setup-install-mode.test.js +2264 -17
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-preferences.test.js +79 -0
- package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
- package/dist/cli/__tests__/setup-refresh.test.js +168 -5
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +73 -6
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.js +37 -0
- package/dist/cli/__tests__/state.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +61 -1
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +2067 -26
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +58 -0
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/url.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/adapt.js.map +1 -1
- package/dist/cli/agents-init.js.map +1 -1
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/ask.d.ts +1 -1
- package/dist/cli/ask.d.ts.map +1 -1
- package/dist/cli/ask.js.map +1 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/autoresearch-guided.js.map +1 -1
- package/dist/cli/autoresearch-intake.js.map +1 -1
- package/dist/cli/capabilities.js.map +1 -1
- package/dist/cli/cleanup.js.map +1 -1
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +636 -161
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +11 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +108 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-serve.js.map +1 -1
- package/dist/cli/mission.js.map +1 -1
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +18 -0
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
- package/dist/cli/native-hook-claim-journal.js +239 -0
- package/dist/cli/native-hook-claim-journal.js.map +1 -0
- package/dist/cli/omx.js.map +1 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/project-runtime-codex-homes.js.map +1 -1
- package/dist/cli/question.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +3 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +14 -0
- package/dist/cli/ralplan.d.ts.map +1 -0
- package/dist/cli/ralplan.js +203 -0
- package/dist/cli/ralplan.js.map +1 -0
- package/dist/cli/setup-preferences.d.ts +11 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +32 -0
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +53 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1403 -400
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/star-prompt.js.map +1 -1
- package/dist/cli/state.d.ts.map +1 -1
- package/dist/cli/state.js +2 -1
- package/dist/cli/state.js.map +1 -1
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +5 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +34 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +8 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +947 -99
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +8 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/url.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +5 -1
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +1287 -11
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +1049 -135
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +28 -49
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/codex-feature-flags.d.ts.map +1 -1
- package/dist/config/codex-feature-flags.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +124 -1
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +2142 -265
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/commit-lore-guard.js.map +1 -1
- package/dist/config/generator.d.ts +40 -26
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +1159 -288
- package/dist/config/generator.js.map +1 -1
- package/dist/config/mcp-registry.js.map +1 -1
- package/dist/config/models.d.ts +2 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js.map +1 -1
- package/dist/config/omx-first-party-mcp.js.map +1 -1
- package/dist/config/team-mode.d.ts.map +1 -1
- package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
- package/dist/document-refresh/enforcer.js.map +1 -1
- package/dist/exec/followup.js.map +1 -1
- package/dist/goal-workflows/artifacts.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +1372 -2
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +14 -0
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +669 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +142 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +8 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
- package/dist/hooks/__tests__/session.test.js +532 -11
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +5 -5
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/code-simplifier/index.js.map +1 -1
- package/dist/hooks/explore-routing.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/events.js.map +1 -1
- package/dist/hooks/extensibility/loader.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
- package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +33 -7
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +2228 -119
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts +12 -0
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +41 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-session-provenance.d.ts +87 -0
- package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
- package/dist/hooks/prompt-session-provenance.js +180 -0
- package/dist/hooks/prompt-session-provenance.js.map +1 -0
- package/dist/hooks/session.d.ts +139 -34
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1239 -317
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/task-size-detector.js.map +1 -1
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/constants.js.map +1 -1
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +146 -2
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +26 -5
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +131 -7
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
- package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +98 -1
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts +58 -58
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/hermes-server.d.ts +70 -70
- package/dist/mcp/hermes-server.d.ts.map +1 -1
- package/dist/mcp/lifecycle-telemetry.js.map +1 -1
- package/dist/mcp/memory-server.d.ts +34 -34
- package/dist/mcp/memory-server.d.ts.map +1 -1
- package/dist/mcp/memory-server.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +14 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +96 -20
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +47 -47
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.d.ts +4 -4
- package/dist/mcp/trace-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.js.map +1 -1
- package/dist/mcp/wiki-server.d.ts +23 -23
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +53 -1
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +5 -5
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
- package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
- package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
- package/dist/notifications/http-client.js.map +1 -1
- package/dist/notifications/index.d.ts +4 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +3 -9
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/lifecycle-dedupe.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/notifications/session-registry.js.map +1 -1
- package/dist/notifications/session-status.js.map +1 -1
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/notifications/tmux-detector.js.map +1 -1
- package/dist/notifications/tmux.js.map +1 -1
- package/dist/openclaw/config.js.map +1 -1
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +8 -5
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/ralph-verify.js.map +1 -1
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.js.map +1 -1
- package/dist/pipeline/stages/ultraqa.js.map +1 -1
- package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
- package/dist/planning/artifact-names.js.map +1 -1
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/question/__tests__/state.test.js.map +1 -1
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/client.d.ts.map +1 -1
- package/dist/question/client.js.map +1 -1
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/events.js.map +1 -1
- package/dist/question/renderer.js.map +1 -1
- package/dist/question/state.d.ts.map +1 -1
- package/dist/question/state.js.map +1 -1
- package/dist/question/ui.js.map +1 -1
- package/dist/ralph/contract.d.ts +1 -1
- package/dist/ralph/contract.d.ts.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +403 -8
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +112 -14
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +3 -3
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +155 -60
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +2 -2
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +1 -2
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/process-tree.js.map +1 -1
- package/dist/runtime/run-loop.js.map +1 -1
- package/dist/runtime/run-outcome.d.ts +3 -3
- package/dist/runtime/run-outcome.d.ts.map +1 -1
- package/dist/runtime/run-outcome.js.map +1 -1
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +5414 -801
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
- package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +205 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +175 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +848 -4
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
- package/dist/scripts/build-api.js.map +1 -1
- package/dist/scripts/build-explore-harness.js.map +1 -1
- package/dist/scripts/build-sparkshell.js.map +1 -1
- package/dist/scripts/cleanup-explore-harness.js.map +1 -1
- package/dist/scripts/codex-execution-surface.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +825 -172
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/generate-release-body.js.map +1 -1
- package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
- package/dist/scripts/hook-payload-guard.js.map +1 -1
- package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
- package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
- package/dist/scripts/notify-fallback-delivery.js +419 -0
- package/dist/scripts/notify-fallback-delivery.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +136 -30
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +39 -13
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +122 -12
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
- package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts +17 -0
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +73 -11
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +4 -4
- package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +2 -2
- package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +25 -16
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.js +15 -13
- package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
- package/dist/scripts/notify-hook.d.ts +24 -1
- package/dist/scripts/notify-hook.d.ts.map +1 -1
- package/dist/scripts/notify-hook.js +266 -90
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/prompt-inventory.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +879 -2
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +1528 -7
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/test-reply-listener-live.js.map +1 -1
- package/dist/scripts/test-sparkshell.js.map +1 -1
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/session-history/friction.js.map +1 -1
- package/dist/session-history/search.js.map +1 -1
- package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
- package/dist/sidecar/collector.js.map +1 -1
- package/dist/sidecar/index.js.map +1 -1
- package/dist/sidecar/render.js.map +1 -1
- package/dist/sidecar/tmux.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +64 -15
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +19 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +61 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +49 -55
- package/dist/state/operations.js.map +1 -1
- package/dist/state/paths.d.ts +1 -1
- package/dist/state/paths.d.ts.map +1 -1
- package/dist/state/paths.js +1 -1
- package/dist/state/paths.js.map +1 -1
- package/dist/state/skill-active.d.ts +13 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +43 -4
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/terminal-normalization.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +20 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +2 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.js +988 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +1 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +273 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
- package/dist/subagents/__tests__/tracker.test.js +1550 -10
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/adapted-role-binding.d.ts +17 -0
- package/dist/subagents/adapted-role-binding.d.ts.map +1 -0
- package/dist/subagents/adapted-role-binding.js +75 -0
- package/dist/subagents/adapted-role-binding.js.map +1 -0
- package/dist/subagents/role-routing-marker.d.ts +10 -0
- package/dist/subagents/role-routing-marker.d.ts.map +1 -0
- package/dist/subagents/role-routing-marker.js +136 -0
- package/dist/subagents/role-routing-marker.js.map +1 -0
- package/dist/subagents/tracker.d.ts +172 -0
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +1077 -61
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/approved-execution.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +231 -1
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/role-router.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +401 -1
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +181 -0
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +434 -4
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/api-interop.d.ts +2 -2
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/commit-hygiene.js.map +1 -1
- package/dist/team/contracts.d.ts +5 -5
- package/dist/team/contracts.d.ts.map +1 -1
- package/dist/team/delivery-log.js.map +1 -1
- package/dist/team/followup-planner.js.map +1 -1
- package/dist/team/goal-workflow.js.map +1 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/model-contract.d.ts +20 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +267 -30
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/orchestrator.js.map +1 -1
- package/dist/team/pane-status.js.map +1 -1
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/reminder-intents.d.ts +1 -1
- package/dist/team/reminder-intents.d.ts.map +1 -1
- package/dist/team/reminder-intents.js.map +1 -1
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +83 -52
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +9 -7
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +69 -28
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch-lock.js.map +1 -1
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/events.js.map +1 -1
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-identity.d.ts.map +1 -1
- package/dist/team/team-identity.js.map +1 -1
- package/dist/team/tmux-session.d.ts +2 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +90 -42
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +4 -1
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/url-reader/index.js.map +1 -1
- package/dist/utils/__tests__/agents-md.test.js +16 -0
- package/dist/utils/__tests__/agents-md.test.js.map +1 -1
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +4 -3
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/utils/package.js.map +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/utils/repo-deps.js.map +1 -1
- package/dist/utils/version.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/verifier.js.map +1 -1
- package/dist/visual/constants.d.ts +1 -1
- package/dist/visual/constants.d.ts.map +1 -1
- package/dist/vscode/__tests__/index.test.js.map +1 -1
- package/dist/vscode/index.js.map +1 -1
- package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
- package/dist/wiki/__tests__/lint.test.js.map +1 -1
- package/dist/wiki/__tests__/query.test.js.map +1 -1
- package/dist/wiki/__tests__/storage.test.js.map +1 -1
- package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
- package/dist/wiki/lint.js.map +1 -1
- package/dist/wiki/query.js.map +1 -1
- package/dist/wiki/storage.js.map +1 -1
- package/package.json +2 -2
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +4 -2
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -2
- package/skills/omx-setup/SKILL.md +10 -5
- package/skills/ralph/SKILL.md +4 -2
- package/skills/ralplan/SKILL.md +5 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +20497 -12377
- package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
- package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +215 -0
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +193 -0
- package/src/scripts/__tests__/smoke-packed-install.test.ts +991 -4
- package/src/scripts/codex-native-hook.ts +918 -172
- package/src/scripts/notify-fallback-delivery.ts +416 -0
- package/src/scripts/notify-fallback-watcher.ts +132 -40
- package/src/scripts/notify-hook/auto-nudge.ts +43 -12
- package/src/scripts/notify-hook/managed-tmux.ts +149 -12
- package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
- package/src/scripts/notify-hook/state-io.ts +100 -13
- package/src/scripts/notify-hook/tmux-injection.ts +38 -21
- package/src/scripts/notify-hook/visual-verdict.ts +22 -12
- package/src/scripts/notify-hook.ts +335 -90
- package/src/scripts/smoke-packed-install.ts +1829 -6
|
@@ -22,11 +22,50 @@ const CODEX_HOOK_EVENT_LABELS = {
|
|
|
22
22
|
function isPlainObject(value) {
|
|
23
23
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
24
|
}
|
|
25
|
+
function setOwnRecordValue(record, key, value) {
|
|
26
|
+
Object.defineProperty(record, key, {
|
|
27
|
+
value,
|
|
28
|
+
enumerable: true,
|
|
29
|
+
writable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function isJsonArray(value) {
|
|
34
|
+
return Array.isArray(value);
|
|
35
|
+
}
|
|
25
36
|
function cloneJson(value) {
|
|
26
37
|
return structuredClone(value);
|
|
27
38
|
}
|
|
39
|
+
function jsonValueFromUnknown(value) {
|
|
40
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
41
|
+
return value;
|
|
42
|
+
if (typeof value === "number") {
|
|
43
|
+
if (!Number.isFinite(value))
|
|
44
|
+
throw new TypeError("Managed Codex hook configuration numbers must be finite JSON values.");
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return value.map((item) => jsonValueFromUnknown(item));
|
|
49
|
+
if (typeof value === "object") {
|
|
50
|
+
const object = {};
|
|
51
|
+
for (const [key, item] of Object.entries(value))
|
|
52
|
+
setOwnRecordValue(object, key, jsonValueFromUnknown(item));
|
|
53
|
+
return object;
|
|
54
|
+
}
|
|
55
|
+
throw new TypeError("Managed Codex hook configuration must be JSON-serializable.");
|
|
56
|
+
}
|
|
57
|
+
function jsonObjectFromUnknown(value) {
|
|
58
|
+
const jsonValue = jsonValueFromUnknown(value);
|
|
59
|
+
if (typeof jsonValue !== "object" || jsonValue === null || Array.isArray(jsonValue)) {
|
|
60
|
+
throw new TypeError("Managed Codex hook configuration root must be a JSON object.");
|
|
61
|
+
}
|
|
62
|
+
return jsonValue;
|
|
63
|
+
}
|
|
28
64
|
function quoteCommandPart(value) {
|
|
29
|
-
return `"${value
|
|
65
|
+
return `"${value
|
|
66
|
+
.replace(/\\/g, "\\\\")
|
|
67
|
+
.replace(/"/g, '\\"')
|
|
68
|
+
.replace(/[$`]/g, "\\$&")}"`;
|
|
30
69
|
}
|
|
31
70
|
export function escapeTomlBasicString(value) {
|
|
32
71
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
@@ -60,6 +99,15 @@ export const WINDOWS_NATIVE_HOOK_SHIM_RELATIVE_PATH = [
|
|
|
60
99
|
"hooks",
|
|
61
100
|
"omx-native-hook-windows-shim.ps1",
|
|
62
101
|
];
|
|
102
|
+
function resolveManagedCodexHookOptions(options, hooksPath) {
|
|
103
|
+
const platform = options.platform ?? process.platform;
|
|
104
|
+
return {
|
|
105
|
+
...options,
|
|
106
|
+
...(platform === "win32" && !options.codexHomeDir
|
|
107
|
+
? { codexHomeDir: dirname(hooksPath) }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
63
111
|
const DEFAULT_WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
64
112
|
/**
|
|
65
113
|
* Resolve an absolute path to Windows PowerShell. When PATH has been shortened
|
|
@@ -76,6 +124,102 @@ export function resolveWindowsPowerShellPath(env = process.env) {
|
|
|
76
124
|
export function buildManagedCodexNativeHookWindowsShimPath(codexHomeDir) {
|
|
77
125
|
return win32.join(codexHomeDir, ...WINDOWS_NATIVE_HOOK_SHIM_RELATIVE_PATH);
|
|
78
126
|
}
|
|
127
|
+
function decodePowerShellSingleQuotedLiteral(value) {
|
|
128
|
+
if (value.length < 2 || value[0] !== "'" || value[value.length - 1] !== "'")
|
|
129
|
+
return null;
|
|
130
|
+
let decoded = "";
|
|
131
|
+
for (let index = 1; index < value.length - 1; index += 1) {
|
|
132
|
+
const character = value[index];
|
|
133
|
+
if (character !== "'") {
|
|
134
|
+
decoded += character;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (index + 1 >= value.length - 1 || value[index + 1] !== "'")
|
|
138
|
+
return null;
|
|
139
|
+
decoded += "'";
|
|
140
|
+
index += 1;
|
|
141
|
+
}
|
|
142
|
+
return decoded;
|
|
143
|
+
}
|
|
144
|
+
function decodeGeneratedWindowsProcessArgument(value) {
|
|
145
|
+
if (value.length < 2 || value[0] !== '"' || value[value.length - 1] !== '"')
|
|
146
|
+
return null;
|
|
147
|
+
let decoded = "";
|
|
148
|
+
let index = 1;
|
|
149
|
+
while (index < value.length - 1) {
|
|
150
|
+
let backslashes = 0;
|
|
151
|
+
while (value[index] === "\\") {
|
|
152
|
+
backslashes += 1;
|
|
153
|
+
index += 1;
|
|
154
|
+
}
|
|
155
|
+
if (index === value.length - 1) {
|
|
156
|
+
if (backslashes % 2 !== 0)
|
|
157
|
+
return null;
|
|
158
|
+
decoded += "\\".repeat(backslashes / 2);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
const character = value[index];
|
|
162
|
+
if (character === '"') {
|
|
163
|
+
if (backslashes % 2 === 0)
|
|
164
|
+
return null;
|
|
165
|
+
decoded += "\\".repeat((backslashes - 1) / 2) + '"';
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
decoded += "\\".repeat(backslashes) + character;
|
|
169
|
+
}
|
|
170
|
+
index += 1;
|
|
171
|
+
}
|
|
172
|
+
return quoteWindowsProcessArgument(decoded) === value ? decoded : null;
|
|
173
|
+
}
|
|
174
|
+
function isProofValidNodeExecutable(nodePath) {
|
|
175
|
+
const executable = nodePath.split(/[\\/]/).pop()?.toLowerCase();
|
|
176
|
+
return isQualifiedWindowsFilesystemPath(nodePath) &&
|
|
177
|
+
(executable === "node" || executable === "node.exe");
|
|
178
|
+
}
|
|
179
|
+
function isProofValidNativeHookScriptPath(hookScriptPath) {
|
|
180
|
+
return isQualifiedWindowsFilesystemPath(hookScriptPath) &&
|
|
181
|
+
/(?:^|\\)dist\\scripts\\codex-native-hook\.js$/i.test(hookScriptPath);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Classify a Windows native-hook shim from its raw on-disk bytes. Current
|
|
185
|
+
* shims must be byte-identical to `expected`; historical shims must be the
|
|
186
|
+
* complete, BOM-prefixed generated forwarding template with independently
|
|
187
|
+
* proof-valid variable paths. Every other input is modified.
|
|
188
|
+
*/
|
|
189
|
+
export function classifyManagedCodexNativeHookWindowsShimOwnership(content, expected) {
|
|
190
|
+
if (content.equals(expected))
|
|
191
|
+
return "current";
|
|
192
|
+
if (content[0] !== 0xef || content[1] !== 0xbb || content[2] !== 0xbf)
|
|
193
|
+
return "modified";
|
|
194
|
+
const decoded = content.toString("utf-8");
|
|
195
|
+
if (!Buffer.from(decoded, "utf-8").equals(content) || !decoded.startsWith("\uFEFF")) {
|
|
196
|
+
return "modified";
|
|
197
|
+
}
|
|
198
|
+
const lines = decoded.slice(1).split("\n");
|
|
199
|
+
if (lines.length !== 21 || lines[20] !== "")
|
|
200
|
+
return "modified";
|
|
201
|
+
const nodePath = lines[2]?.startsWith("$startInfo.FileName = ")
|
|
202
|
+
? decodePowerShellSingleQuotedLiteral(lines[2].slice("$startInfo.FileName = ".length))
|
|
203
|
+
: null;
|
|
204
|
+
const encodedHookScript = lines[7]?.startsWith("$startInfo.Arguments = ")
|
|
205
|
+
? decodePowerShellSingleQuotedLiteral(lines[7].slice("$startInfo.Arguments = ".length))
|
|
206
|
+
: null;
|
|
207
|
+
const hookScriptPath = encodedHookScript === null
|
|
208
|
+
? null
|
|
209
|
+
: decodeGeneratedWindowsProcessArgument(encodedHookScript);
|
|
210
|
+
if (nodePath === null ||
|
|
211
|
+
hookScriptPath === null ||
|
|
212
|
+
!isProofValidNodeExecutable(nodePath) ||
|
|
213
|
+
!isProofValidNativeHookScriptPath(hookScriptPath)) {
|
|
214
|
+
return "modified";
|
|
215
|
+
}
|
|
216
|
+
return Buffer.from(buildManagedCodexNativeHookWindowsShimContent("", {
|
|
217
|
+
nodePath,
|
|
218
|
+
hookScriptPath,
|
|
219
|
+
}), "utf-8").equals(content)
|
|
220
|
+
? "historical"
|
|
221
|
+
: "modified";
|
|
222
|
+
}
|
|
79
223
|
export function buildManagedCodexNativeHookWindowsShimContent(pkgRoot, options = {}) {
|
|
80
224
|
const hookScript = options.hookScriptPath ??
|
|
81
225
|
win32.join(pkgRoot, "dist", "scripts", "codex-native-hook.js");
|
|
@@ -108,214 +252,2022 @@ export function buildManagedCodexNativeHookWindowsShimContent(pkgRoot, options =
|
|
|
108
252
|
"",
|
|
109
253
|
].join("\n");
|
|
110
254
|
}
|
|
111
|
-
export function buildManagedCodexNativeHookCommand(pkgRoot, optionsOrPlatform = process.platform) {
|
|
112
|
-
const options = typeof optionsOrPlatform === "string"
|
|
113
|
-
? { platform: optionsOrPlatform }
|
|
114
|
-
: optionsOrPlatform;
|
|
115
|
-
const platform = options.platform ?? process.platform;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
255
|
+
export function buildManagedCodexNativeHookCommand(pkgRoot, optionsOrPlatform = process.platform) {
|
|
256
|
+
const options = typeof optionsOrPlatform === "string"
|
|
257
|
+
? { platform: optionsOrPlatform }
|
|
258
|
+
: optionsOrPlatform;
|
|
259
|
+
const platform = options.platform ?? process.platform;
|
|
260
|
+
if (platform === "win32") {
|
|
261
|
+
const codexHomeDir = options.codexHomeDir ?? dirname(pkgRoot);
|
|
262
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
263
|
+
const powerShellPath = resolveWindowsPowerShellPath(options.env);
|
|
264
|
+
return `& ${quotePowerShellLiteral(powerShellPath)} -NoProfile -ExecutionPolicy Bypass -File ${quotePowerShellLiteral(shimPath)}`;
|
|
265
|
+
}
|
|
266
|
+
const hookScript = join(pkgRoot, "dist", "scripts", "codex-native-hook.js");
|
|
267
|
+
return `${quoteCommandPart(process.execPath)} ${quoteCommandPart(hookScript)}`;
|
|
268
|
+
}
|
|
269
|
+
function buildCommandHook(command, options = {}) {
|
|
270
|
+
const hook = {
|
|
271
|
+
type: "command",
|
|
272
|
+
command,
|
|
273
|
+
...(options.statusMessage ? { statusMessage: options.statusMessage } : {}),
|
|
274
|
+
...(typeof options.timeout === "number" ? { timeout: options.timeout } : {}),
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
...(options.matcher ? { matcher: options.matcher } : {}),
|
|
278
|
+
hooks: [hook],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export function buildManagedCodexHooksConfig(pkgRoot, options = {}) {
|
|
282
|
+
const command = buildManagedCodexNativeHookCommand(pkgRoot, options);
|
|
283
|
+
return {
|
|
284
|
+
hooks: {
|
|
285
|
+
SessionStart: [
|
|
286
|
+
buildCommandHook(command, {
|
|
287
|
+
matcher: "startup|resume|clear",
|
|
288
|
+
}),
|
|
289
|
+
],
|
|
290
|
+
PreToolUse: [
|
|
291
|
+
buildCommandHook(command),
|
|
292
|
+
],
|
|
293
|
+
PostToolUse: [
|
|
294
|
+
buildCommandHook(command),
|
|
295
|
+
],
|
|
296
|
+
UserPromptSubmit: [
|
|
297
|
+
buildCommandHook(command),
|
|
298
|
+
],
|
|
299
|
+
PreCompact: [
|
|
300
|
+
buildCommandHook(command),
|
|
301
|
+
],
|
|
302
|
+
PostCompact: [
|
|
303
|
+
buildCommandHook(command),
|
|
304
|
+
],
|
|
305
|
+
Stop: [
|
|
306
|
+
buildCommandHook(command, {
|
|
307
|
+
timeout: 30,
|
|
308
|
+
}),
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const CODEX_HOOK_EVENTS = [
|
|
314
|
+
"PreToolUse",
|
|
315
|
+
"PermissionRequest",
|
|
316
|
+
"PostToolUse",
|
|
317
|
+
"PreCompact",
|
|
318
|
+
"PostCompact",
|
|
319
|
+
"SessionStart",
|
|
320
|
+
"UserPromptSubmit",
|
|
321
|
+
"SubagentStart",
|
|
322
|
+
"SubagentStop",
|
|
323
|
+
"Stop",
|
|
324
|
+
];
|
|
325
|
+
const MATCHER_AWARE_HOOK_EVENTS = new Set([
|
|
326
|
+
"PreToolUse",
|
|
327
|
+
"PermissionRequest",
|
|
328
|
+
"PostToolUse",
|
|
329
|
+
"PreCompact",
|
|
330
|
+
"PostCompact",
|
|
331
|
+
"SessionStart",
|
|
332
|
+
"SubagentStart",
|
|
333
|
+
"SubagentStop",
|
|
334
|
+
]);
|
|
335
|
+
const KNOWN_HOOK_EVENT_FIELDS = new Set(CODEX_HOOK_EVENTS);
|
|
336
|
+
const KNOWN_GROUP_FIELDS = new Set(["matcher", "hooks"]);
|
|
337
|
+
const KNOWN_COMMAND_FIELDS = new Set([
|
|
338
|
+
"type",
|
|
339
|
+
"command",
|
|
340
|
+
"commandWindows",
|
|
341
|
+
"command_windows",
|
|
342
|
+
"timeout",
|
|
343
|
+
"async",
|
|
344
|
+
"statusMessage",
|
|
345
|
+
]);
|
|
346
|
+
const KNOWN_ROOT_FIELDS = new Set(["hooks"]);
|
|
347
|
+
const KNOWN_HANDLER_TYPE_FIELD = new Set(["type"]);
|
|
348
|
+
const KNOWN_LEGACY_TRUST_STATE_ENTRY_FIELDS = new Set(["trusted_hash", "enabled"]);
|
|
349
|
+
const KNOWN_MATCHER_FIELD = new Set(["matcher"]);
|
|
350
|
+
export class ManagedCodexHooksPlanError extends Error {
|
|
351
|
+
code;
|
|
352
|
+
details;
|
|
353
|
+
constructor(code, message, details = {}) {
|
|
354
|
+
super(message);
|
|
355
|
+
this.name = "ManagedCodexHooksPlanError";
|
|
356
|
+
this.code = code;
|
|
357
|
+
this.details = details;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function planError(code, message, details = {}) {
|
|
361
|
+
return new ManagedCodexHooksPlanError(code, message, details);
|
|
362
|
+
}
|
|
363
|
+
function planFailure(error, diagnostics = []) {
|
|
364
|
+
return { ok: false, error, diagnostics };
|
|
365
|
+
}
|
|
366
|
+
function isManagedHookEventName(value) {
|
|
367
|
+
return MANAGED_HOOK_EVENTS.includes(value);
|
|
368
|
+
}
|
|
369
|
+
function matcherIsAware(eventName) {
|
|
370
|
+
return MATCHER_AWARE_HOOK_EVENTS.has(eventName);
|
|
371
|
+
}
|
|
372
|
+
function containsRustUnsupportedJavaScriptMatcherSyntax(matcher) {
|
|
373
|
+
for (let index = 0; index < matcher.length; index += 1) {
|
|
374
|
+
if (matcher[index] === "\\") {
|
|
375
|
+
const escaped = matcher[index + 1];
|
|
376
|
+
if (!escaped || !isValidRustRegexEscape(matcher, index))
|
|
377
|
+
return true;
|
|
378
|
+
index = skipRustRegexEscape(matcher, index) - 1;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (matcher[index] !== "(" || matcher[index + 1] !== "?")
|
|
382
|
+
continue;
|
|
383
|
+
const marker = matcher[index + 2];
|
|
384
|
+
if (marker === "=" || marker === "!")
|
|
385
|
+
return true;
|
|
386
|
+
if (marker === "<" && (matcher[index + 3] === "=" || matcher[index + 3] === "!")) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
const RUST_SIMPLE_REGEX_ESCAPES = new Set([
|
|
393
|
+
"A", "B", "D", "S", "W", "a", "b", "d", "f", "n", "r", "s", "t", "v", "w", "z",
|
|
394
|
+
]);
|
|
395
|
+
const RUST_ESCAPABLE_REGEX_PUNCTUATION = new Set([
|
|
396
|
+
"\\", ".", "*", "+", "?", "(", ")", "|", "[", "]", "{", "}", "^", "$", "-", "/",
|
|
397
|
+
]);
|
|
398
|
+
function isHexadecimal(value) {
|
|
399
|
+
return /^[0-9A-Fa-f]+$/.test(value);
|
|
400
|
+
}
|
|
401
|
+
function skipRustRegexEscape(matcher, index) {
|
|
402
|
+
const kind = matcher[index + 1];
|
|
403
|
+
if ((kind === "p" || kind === "P" || kind === "u") && matcher[index + 2] === "{") {
|
|
404
|
+
const end = matcher.indexOf("}", index + 3);
|
|
405
|
+
return end >= 0 ? end + 1 : matcher.length;
|
|
406
|
+
}
|
|
407
|
+
return index + 2 + (kind === "x" ? 2 : 0);
|
|
408
|
+
}
|
|
409
|
+
function isValidRustUnicodeProperty(property) {
|
|
410
|
+
const candidates = [property];
|
|
411
|
+
const equals = property.indexOf("=");
|
|
412
|
+
if (equals >= 0) {
|
|
413
|
+
const name = property.slice(0, equals).toLowerCase();
|
|
414
|
+
const value = property.slice(equals + 1);
|
|
415
|
+
const normalizedName = name === "gc" || name === "general_category"
|
|
416
|
+
? "General_Category"
|
|
417
|
+
: name === "sc" || name === "script"
|
|
418
|
+
? "Script"
|
|
419
|
+
: name === "scx" || name === "script_extensions"
|
|
420
|
+
? "Script_Extensions"
|
|
421
|
+
: property.slice(0, equals);
|
|
422
|
+
candidates.push(`${normalizedName}=${value}`);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
candidates.push(`Script=${property}`);
|
|
426
|
+
}
|
|
427
|
+
return candidates.some((candidate) => {
|
|
428
|
+
try {
|
|
429
|
+
new RegExp(`\\p{${candidate}}`, "u");
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function isValidRustRegexEscape(matcher, index) {
|
|
438
|
+
const kind = matcher[index + 1];
|
|
439
|
+
if (!kind)
|
|
440
|
+
return false;
|
|
441
|
+
if (RUST_SIMPLE_REGEX_ESCAPES.has(kind) || RUST_ESCAPABLE_REGEX_PUNCTUATION.has(kind))
|
|
442
|
+
return true;
|
|
443
|
+
if (kind === "x")
|
|
444
|
+
return matcher[index + 3] !== undefined && isHexadecimal(matcher.slice(index + 2, index + 4));
|
|
445
|
+
if (kind === "u") {
|
|
446
|
+
if (matcher[index + 2] !== "{")
|
|
447
|
+
return false;
|
|
448
|
+
const end = matcher.indexOf("}", index + 3);
|
|
449
|
+
const codePoint = end >= 0 ? matcher.slice(index + 3, end) : "";
|
|
450
|
+
if (codePoint.length === 0 || codePoint.length > 6 || !isHexadecimal(codePoint))
|
|
451
|
+
return false;
|
|
452
|
+
const value = Number.parseInt(codePoint, 16);
|
|
453
|
+
return value <= 0x10ffff && (value < 0xd800 || value > 0xdfff);
|
|
454
|
+
}
|
|
455
|
+
if (kind === "p" || kind === "P") {
|
|
456
|
+
if (matcher[index + 2] !== "{")
|
|
457
|
+
return false;
|
|
458
|
+
const end = matcher.indexOf("}", index + 3);
|
|
459
|
+
return end > index + 3 && isValidRustUnicodeProperty(matcher.slice(index + 3, end));
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
function normalizeRustMatcherForJavaScriptValidation(matcher) {
|
|
464
|
+
return matcher
|
|
465
|
+
.replace(/\\[pP]\{[^}]+\}/g, ".")
|
|
466
|
+
.replace(/\\u\{([0-9A-Fa-f]{1,6})\}/g, (_match, codePoint) => String.fromCodePoint(Number.parseInt(codePoint, 16)))
|
|
467
|
+
.replace(/\\A/g, "^")
|
|
468
|
+
.replace(/\\z/g, "$")
|
|
469
|
+
.replace(/\\a/g, "\\x07")
|
|
470
|
+
.replace(/\(\?P<([A-Za-z_][A-Za-z0-9_]*)>/g, "(?<$1>")
|
|
471
|
+
.replace(/\(\?([imsuUxR]*)(?:-([imsuUxR]+))?([:)])/g, (_match, enabled, disabled, terminator) => enabled || disabled ? terminator === ":" ? "(?:" : "" : _match);
|
|
472
|
+
}
|
|
473
|
+
function isValidCodexMatcher(matcher) {
|
|
474
|
+
if (matcher === "*")
|
|
475
|
+
return true;
|
|
476
|
+
if (containsRustUnsupportedJavaScriptMatcherSyntax(matcher))
|
|
477
|
+
return false;
|
|
478
|
+
try {
|
|
479
|
+
new RegExp(normalizeRustMatcherForJavaScriptValidation(matcher));
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function commandIsSkipped(command) {
|
|
487
|
+
return command.trim().length === 0;
|
|
488
|
+
}
|
|
489
|
+
function containsLoneSurrogate(value) {
|
|
490
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
491
|
+
const codeUnit = value.charCodeAt(index);
|
|
492
|
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
493
|
+
if (index + 1 >= value.length)
|
|
494
|
+
return true;
|
|
495
|
+
const next = value.charCodeAt(index + 1);
|
|
496
|
+
if (next < 0xdc00 || next > 0xdfff)
|
|
497
|
+
return true;
|
|
498
|
+
index += 1;
|
|
499
|
+
}
|
|
500
|
+
else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
function parseJsonSource(content) {
|
|
507
|
+
let cursor = 0;
|
|
508
|
+
let nesting = 0;
|
|
509
|
+
const skipWhitespace = () => {
|
|
510
|
+
while ([" ", "\n", "\r", "\t"].includes(content[cursor] ?? ""))
|
|
511
|
+
cursor += 1;
|
|
512
|
+
};
|
|
513
|
+
const parseString = () => {
|
|
514
|
+
const start = cursor;
|
|
515
|
+
if (content[cursor] !== '"')
|
|
516
|
+
return null;
|
|
517
|
+
cursor += 1;
|
|
518
|
+
let escaped = false;
|
|
519
|
+
while (cursor < content.length) {
|
|
520
|
+
const char = content[cursor++];
|
|
521
|
+
if (escaped) {
|
|
522
|
+
escaped = false;
|
|
523
|
+
}
|
|
524
|
+
else if (char === "\\") {
|
|
525
|
+
escaped = true;
|
|
526
|
+
}
|
|
527
|
+
else if (char === '"') {
|
|
528
|
+
const raw = content.slice(start, cursor);
|
|
529
|
+
try {
|
|
530
|
+
const value = JSON.parse(raw);
|
|
531
|
+
if (typeof value !== "string" || containsLoneSurrogate(value))
|
|
532
|
+
return null;
|
|
533
|
+
return { kind: "string", start, end: cursor, raw, value };
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
};
|
|
542
|
+
const parseValue = () => {
|
|
543
|
+
skipWhitespace();
|
|
544
|
+
const start = cursor;
|
|
545
|
+
const current = content[cursor];
|
|
546
|
+
if (current === "{") {
|
|
547
|
+
if (nesting >= 128)
|
|
548
|
+
return null;
|
|
549
|
+
nesting += 1;
|
|
550
|
+
try {
|
|
551
|
+
cursor += 1;
|
|
552
|
+
skipWhitespace();
|
|
553
|
+
const properties = [];
|
|
554
|
+
const value = {};
|
|
555
|
+
if (content[cursor] === "}") {
|
|
556
|
+
cursor += 1;
|
|
557
|
+
return { kind: "object", start, end: cursor, value, properties };
|
|
558
|
+
}
|
|
559
|
+
while (cursor < content.length) {
|
|
560
|
+
skipWhitespace();
|
|
561
|
+
const keyNode = parseString();
|
|
562
|
+
if (!keyNode)
|
|
563
|
+
return null;
|
|
564
|
+
skipWhitespace();
|
|
565
|
+
if (content[cursor] !== ":")
|
|
566
|
+
return null;
|
|
567
|
+
cursor += 1;
|
|
568
|
+
const child = parseValue();
|
|
569
|
+
if (!child)
|
|
570
|
+
return null;
|
|
571
|
+
properties.push({ key: keyNode.value, keyNode, value: child });
|
|
572
|
+
setOwnRecordValue(value, keyNode.value, child.value);
|
|
573
|
+
skipWhitespace();
|
|
574
|
+
if (content[cursor] === "}") {
|
|
575
|
+
cursor += 1;
|
|
576
|
+
return { kind: "object", start, end: cursor, value, properties };
|
|
577
|
+
}
|
|
578
|
+
if (content[cursor] !== ",")
|
|
579
|
+
return null;
|
|
580
|
+
cursor += 1;
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
finally {
|
|
585
|
+
nesting -= 1;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (current === "[") {
|
|
589
|
+
if (nesting >= 128)
|
|
590
|
+
return null;
|
|
591
|
+
nesting += 1;
|
|
592
|
+
try {
|
|
593
|
+
cursor += 1;
|
|
594
|
+
skipWhitespace();
|
|
595
|
+
const elements = [];
|
|
596
|
+
const value = [];
|
|
597
|
+
if (content[cursor] === "]") {
|
|
598
|
+
cursor += 1;
|
|
599
|
+
return { kind: "array", start, end: cursor, value, elements };
|
|
600
|
+
}
|
|
601
|
+
while (cursor < content.length) {
|
|
602
|
+
const child = parseValue();
|
|
603
|
+
if (!child)
|
|
604
|
+
return null;
|
|
605
|
+
elements.push(child);
|
|
606
|
+
value.push(child.value);
|
|
607
|
+
skipWhitespace();
|
|
608
|
+
if (content[cursor] === "]") {
|
|
609
|
+
cursor += 1;
|
|
610
|
+
return { kind: "array", start, end: cursor, value, elements };
|
|
611
|
+
}
|
|
612
|
+
if (content[cursor] !== ",")
|
|
613
|
+
return null;
|
|
614
|
+
cursor += 1;
|
|
615
|
+
}
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
nesting -= 1;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (current === '"')
|
|
623
|
+
return parseString();
|
|
624
|
+
if (content.startsWith("true", cursor)) {
|
|
625
|
+
cursor += 4;
|
|
626
|
+
return { kind: "boolean", start, end: cursor, raw: "true", value: true };
|
|
627
|
+
}
|
|
628
|
+
if (content.startsWith("false", cursor)) {
|
|
629
|
+
cursor += 5;
|
|
630
|
+
return { kind: "boolean", start, end: cursor, raw: "false", value: false };
|
|
631
|
+
}
|
|
632
|
+
if (content.startsWith("null", cursor)) {
|
|
633
|
+
cursor += 4;
|
|
634
|
+
return { kind: "null", start, end: cursor, raw: "null", value: null };
|
|
635
|
+
}
|
|
636
|
+
const number = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(content.slice(cursor));
|
|
637
|
+
if (!number || number.index !== 0)
|
|
638
|
+
return null;
|
|
639
|
+
const value = Number(number[0]);
|
|
640
|
+
if (!Number.isFinite(value))
|
|
641
|
+
return null;
|
|
642
|
+
cursor += number[0].length;
|
|
643
|
+
return {
|
|
644
|
+
kind: "number",
|
|
645
|
+
start,
|
|
646
|
+
end: cursor,
|
|
647
|
+
raw: number[0],
|
|
648
|
+
value,
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
const root = parseValue();
|
|
652
|
+
skipWhitespace();
|
|
653
|
+
return root && cursor === content.length ? root : null;
|
|
654
|
+
}
|
|
655
|
+
function objectProperty(node, key) {
|
|
656
|
+
return node.properties.find((property) => property.key === key);
|
|
657
|
+
}
|
|
658
|
+
function duplicateKnownProperty(node, known) {
|
|
659
|
+
const seen = new Set();
|
|
660
|
+
for (const property of node.properties) {
|
|
661
|
+
if (!known.has(property.key))
|
|
662
|
+
continue;
|
|
663
|
+
if (seen.has(property.key))
|
|
664
|
+
return property.key;
|
|
665
|
+
seen.add(property.key);
|
|
666
|
+
}
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
function stringProperty(node, key) {
|
|
670
|
+
const property = objectProperty(node, key);
|
|
671
|
+
return property?.value.kind === "string" ? property.value.value : undefined;
|
|
672
|
+
}
|
|
673
|
+
function nullableStringProperty(node, key) {
|
|
674
|
+
const property = objectProperty(node, key);
|
|
675
|
+
if (!property)
|
|
676
|
+
return undefined;
|
|
677
|
+
if (property.value.kind === "null")
|
|
678
|
+
return null;
|
|
679
|
+
return property.value.kind === "string" ? property.value.value : undefined;
|
|
680
|
+
}
|
|
681
|
+
function effectiveCommandForPlatform(handler, platform) {
|
|
682
|
+
const command = stringProperty(handler, "command");
|
|
683
|
+
if (platform !== "win32")
|
|
684
|
+
return command;
|
|
685
|
+
const windows = nullableStringProperty(handler, "commandWindows") ??
|
|
686
|
+
nullableStringProperty(handler, "command_windows");
|
|
687
|
+
return windows === undefined || windows === null ? command : windows;
|
|
688
|
+
}
|
|
689
|
+
function isU64Node(node) {
|
|
690
|
+
if (node.kind !== "number" || !/^(?:0|[1-9]\d*)$/.test(node.raw))
|
|
691
|
+
return false;
|
|
692
|
+
const maximum = "18446744073709551615";
|
|
693
|
+
return node.raw.length < maximum.length ||
|
|
694
|
+
(node.raw.length === maximum.length && node.raw <= maximum);
|
|
695
|
+
}
|
|
696
|
+
function validateCommandHandler(node, eventName, groupIndex, handlerIndex) {
|
|
697
|
+
const duplicate = duplicateKnownProperty(node, KNOWN_COMMAND_FIELDS);
|
|
698
|
+
if (duplicate) {
|
|
699
|
+
return planError("invalid_document", `Duplicate command hook field ${duplicate}.`, {
|
|
700
|
+
eventName,
|
|
701
|
+
groupIndex,
|
|
702
|
+
handlerIndex,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
if (stringProperty(node, "command") === undefined) {
|
|
706
|
+
return planError("invalid_document", "Command hooks require a string command.", {
|
|
707
|
+
eventName,
|
|
708
|
+
groupIndex,
|
|
709
|
+
handlerIndex,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
const windows = objectProperty(node, "commandWindows");
|
|
713
|
+
const windowsAlias = objectProperty(node, "command_windows");
|
|
714
|
+
if (windows && windowsAlias) {
|
|
715
|
+
return planError("invalid_document", "Command hook Windows aliases cannot both be present.", {
|
|
716
|
+
eventName,
|
|
717
|
+
groupIndex,
|
|
718
|
+
handlerIndex,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
for (const property of [windows, windowsAlias]) {
|
|
722
|
+
if (property && property.value.kind !== "string" && property.value.kind !== "null") {
|
|
723
|
+
return planError("invalid_document", "Command hook Windows command must be a string or null.", {
|
|
724
|
+
eventName,
|
|
725
|
+
groupIndex,
|
|
726
|
+
handlerIndex,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const timeout = objectProperty(node, "timeout");
|
|
731
|
+
if (timeout && timeout.value.kind !== "null" && !isU64Node(timeout.value)) {
|
|
732
|
+
return planError("invalid_document", "Command hook timeout must be an unsigned 64-bit integer or null.", {
|
|
733
|
+
eventName,
|
|
734
|
+
groupIndex,
|
|
735
|
+
handlerIndex,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const asynchronous = objectProperty(node, "async");
|
|
739
|
+
if (asynchronous && asynchronous.value.kind !== "boolean") {
|
|
740
|
+
return planError("invalid_document", "Command hook async must be a boolean.", {
|
|
741
|
+
eventName,
|
|
742
|
+
groupIndex,
|
|
743
|
+
handlerIndex,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
const status = objectProperty(node, "statusMessage");
|
|
747
|
+
if (status && status.value.kind !== "string" && status.value.kind !== "null") {
|
|
748
|
+
return planError("invalid_document", "Command hook statusMessage must be a string or null.", {
|
|
749
|
+
eventName,
|
|
750
|
+
groupIndex,
|
|
751
|
+
handlerIndex,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
function validateCodexHooksDocument(content, options = {}) {
|
|
757
|
+
const root = parseJsonSource(content);
|
|
758
|
+
if (!root || root.kind !== "object") {
|
|
759
|
+
return { ok: false, error: planError("invalid_document", "hooks.json must contain a JSON object.") };
|
|
760
|
+
}
|
|
761
|
+
const duplicateRoot = duplicateKnownProperty(root, KNOWN_ROOT_FIELDS);
|
|
762
|
+
if (duplicateRoot) {
|
|
763
|
+
return { ok: false, error: planError("invalid_document", `Duplicate root field ${duplicateRoot}.`) };
|
|
764
|
+
}
|
|
765
|
+
const rootUnknown = root.properties.find((property) => !KNOWN_ROOT_FIELDS.has(property.key));
|
|
766
|
+
if (rootUnknown) {
|
|
767
|
+
return {
|
|
768
|
+
ok: false,
|
|
769
|
+
error: planError("invalid_document", `Codex does not accept unknown root field ${rootUnknown.key}.`),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
const hooksProperty = objectProperty(root, "hooks");
|
|
773
|
+
if (hooksProperty && hooksProperty.value.kind !== "object") {
|
|
774
|
+
return { ok: false, error: planError("invalid_document", "Root hooks must be an object when present.") };
|
|
775
|
+
}
|
|
776
|
+
const hooksNode = hooksProperty?.value.kind === "object" ? hooksProperty.value : undefined;
|
|
777
|
+
const duplicateEvent = hooksNode && duplicateKnownProperty(hooksNode, KNOWN_HOOK_EVENT_FIELDS);
|
|
778
|
+
if (duplicateEvent) {
|
|
779
|
+
return { ok: false, error: planError("invalid_document", `Duplicate hooks event ${duplicateEvent}.`) };
|
|
780
|
+
}
|
|
781
|
+
const diagnostics = [];
|
|
782
|
+
const groupOccurrences = [];
|
|
783
|
+
const handlerOccurrences = [];
|
|
784
|
+
const discoveredCommands = [];
|
|
785
|
+
const platform = options.platform ?? process.platform;
|
|
786
|
+
for (const eventName of CODEX_HOOK_EVENTS) {
|
|
787
|
+
const eventProperty = hooksNode ? objectProperty(hooksNode, eventName) : undefined;
|
|
788
|
+
if (!eventProperty)
|
|
789
|
+
continue;
|
|
790
|
+
if (eventProperty.value.kind !== "array") {
|
|
791
|
+
return {
|
|
792
|
+
ok: false,
|
|
793
|
+
error: planError("invalid_document", `${eventName} must be an array of matcher groups.`, { eventName }),
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
for (const [groupIndex, groupNode] of eventProperty.value.elements.entries()) {
|
|
797
|
+
if (groupNode.kind !== "object") {
|
|
798
|
+
return {
|
|
799
|
+
ok: false,
|
|
800
|
+
error: planError("invalid_document", "Hook matcher groups must be objects.", { eventName, groupIndex }),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
groupOccurrences.push({ eventName, groupIndex, span: groupNode });
|
|
804
|
+
const duplicate = duplicateKnownProperty(groupNode, KNOWN_GROUP_FIELDS);
|
|
805
|
+
if (duplicate) {
|
|
806
|
+
return {
|
|
807
|
+
ok: false,
|
|
808
|
+
error: planError("invalid_document", `Duplicate matcher group field ${duplicate}.`, { eventName, groupIndex }),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
const matcher = objectProperty(groupNode, "matcher");
|
|
812
|
+
if (matcher && matcher.value.kind !== "string" && matcher.value.kind !== "null") {
|
|
813
|
+
return {
|
|
814
|
+
ok: false,
|
|
815
|
+
error: planError("invalid_document", "Hook matcher must be a string or null.", { eventName, groupIndex }),
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const hooks = objectProperty(groupNode, "hooks");
|
|
819
|
+
if (hooks && hooks.value.kind !== "array") {
|
|
820
|
+
return {
|
|
821
|
+
ok: false,
|
|
822
|
+
error: planError("invalid_document", "Matcher group hooks must be an array when present.", { eventName, groupIndex }),
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
const invalidMatcher = matcherIsAware(eventName) && matcher?.value.kind === "string" &&
|
|
826
|
+
!isValidCodexMatcher(matcher.value.value);
|
|
827
|
+
if (invalidMatcher) {
|
|
828
|
+
diagnostics.push({
|
|
829
|
+
code: "invalid_matcher",
|
|
830
|
+
eventName,
|
|
831
|
+
groupIndex,
|
|
832
|
+
message: "Codex skips groups whose matcher is not a valid regular expression.",
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
const handlers = hooks?.value.kind === "array" ? hooks.value.elements : [];
|
|
836
|
+
for (const [handlerIndex, handlerNode] of handlers.entries()) {
|
|
837
|
+
handlerOccurrences.push({ eventName, groupIndex, handlerIndex, span: handlerNode });
|
|
838
|
+
if (handlerNode.kind !== "object") {
|
|
839
|
+
return {
|
|
840
|
+
ok: false,
|
|
841
|
+
error: planError("invalid_document", "Hook handlers must be objects.", { eventName, groupIndex, handlerIndex }),
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
const duplicateType = duplicateKnownProperty(handlerNode, KNOWN_HANDLER_TYPE_FIELD);
|
|
845
|
+
if (duplicateType) {
|
|
846
|
+
return {
|
|
847
|
+
ok: false,
|
|
848
|
+
error: planError("invalid_document", "Duplicate hook handler type field.", { eventName, groupIndex, handlerIndex }),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
const type = stringProperty(handlerNode, "type");
|
|
852
|
+
if (type !== "command" && type !== "prompt" && type !== "agent") {
|
|
853
|
+
return {
|
|
854
|
+
ok: false,
|
|
855
|
+
error: planError("invalid_document", "Hook handler type must be command, prompt, or agent.", {
|
|
856
|
+
eventName,
|
|
857
|
+
groupIndex,
|
|
858
|
+
handlerIndex,
|
|
859
|
+
}),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (type !== "command") {
|
|
863
|
+
diagnostics.push({
|
|
864
|
+
code: "unsupported_handler",
|
|
865
|
+
eventName,
|
|
866
|
+
groupIndex,
|
|
867
|
+
handlerIndex,
|
|
868
|
+
message: `Codex preserves but does not discover ${type} hook handlers.`,
|
|
869
|
+
});
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const commandError = validateCommandHandler(handlerNode, eventName, groupIndex, handlerIndex);
|
|
873
|
+
if (commandError)
|
|
874
|
+
return { ok: false, error: commandError };
|
|
875
|
+
const command = effectiveCommandForPlatform(handlerNode, platform);
|
|
876
|
+
const asyncProperty = objectProperty(handlerNode, "async");
|
|
877
|
+
const asynchronous = asyncProperty?.value.kind === "boolean" &&
|
|
878
|
+
asyncProperty.value.value === true;
|
|
879
|
+
if (asynchronous) {
|
|
880
|
+
diagnostics.push({
|
|
881
|
+
code: "async_command",
|
|
882
|
+
eventName,
|
|
883
|
+
groupIndex,
|
|
884
|
+
handlerIndex,
|
|
885
|
+
message: "Codex skips async command hook handlers.",
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
else if (commandIsSkipped(command)) {
|
|
889
|
+
diagnostics.push({
|
|
890
|
+
code: "empty_command",
|
|
891
|
+
eventName,
|
|
892
|
+
groupIndex,
|
|
893
|
+
handlerIndex,
|
|
894
|
+
message: "Codex skips empty command hook handlers.",
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
else if (!invalidMatcher) {
|
|
898
|
+
discoveredCommands.push({ eventName, groupIndex, handlerIndex, command });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
content,
|
|
905
|
+
rootNode: root,
|
|
906
|
+
hooksNode,
|
|
907
|
+
result: {
|
|
908
|
+
ok: true,
|
|
909
|
+
root: cloneJson(root.value),
|
|
910
|
+
diagnostics,
|
|
911
|
+
groupOccurrences,
|
|
912
|
+
handlerOccurrences,
|
|
913
|
+
discoveredCommands,
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
export function validateCodexHooksConfigStrict(content, options = {}) {
|
|
918
|
+
const result = validateCodexHooksDocument(content, options);
|
|
919
|
+
return "result" in result ? result.result : result;
|
|
920
|
+
}
|
|
921
|
+
/** Compatibility-only permissive JSON intake; strict plans never use this parser. */
|
|
922
|
+
export function parseCodexHooksConfig(content) {
|
|
923
|
+
try {
|
|
924
|
+
const parsed = JSON.parse(content);
|
|
925
|
+
if (!isPlainObject(parsed))
|
|
926
|
+
return null;
|
|
927
|
+
return {
|
|
928
|
+
root: cloneJson(parsed),
|
|
929
|
+
hooks: isPlainObject(parsed.hooks) ? cloneJson(parsed.hooks) : {},
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const UNQUOTED_POSIX_SHELL_ACTIVE_CHARACTERS = new Set([
|
|
937
|
+
"\r", "\n", ";", "|", "&", "<", ">", "`", "$", "(", ")", "*", "?", "[", "]", "{", "}", "~", "!", "#",
|
|
938
|
+
]);
|
|
939
|
+
const UNQUOTED_POWERSHELL_ACTIVE_CHARACTERS = new Set([
|
|
940
|
+
"\r", "\n", ";", "|", "<", ">", "`", "$", "(", ")", "*", "?", "[", "]", "{", "}", "~", "!", "#", "@", "%", "^",
|
|
941
|
+
]);
|
|
942
|
+
function isShellWhitespace(character) {
|
|
943
|
+
return character === " " || character === "\t" || character === "\r" || character === "\n";
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Decode only shell-static POSIX words. The decoder preserves enough quote and
|
|
947
|
+
* escape provenance to reject interpolation and shell-active syntax instead of
|
|
948
|
+
* merely comparing the decoded argument values.
|
|
949
|
+
*/
|
|
950
|
+
function tokenizePosixCommand(command, preserveWindowsBackslashes = false) {
|
|
951
|
+
if (containsControlCharacter(command))
|
|
952
|
+
return null;
|
|
953
|
+
const words = [];
|
|
954
|
+
let word = "";
|
|
955
|
+
let tokenStarted = false;
|
|
956
|
+
let quote;
|
|
957
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
958
|
+
const character = command[index];
|
|
959
|
+
if (quote === "'") {
|
|
960
|
+
if (character === "'")
|
|
961
|
+
quote = undefined;
|
|
962
|
+
else
|
|
963
|
+
word += character;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (quote === '"') {
|
|
967
|
+
if (character === '"') {
|
|
968
|
+
quote = undefined;
|
|
969
|
+
}
|
|
970
|
+
else if (character === "$" || character === "`") {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
else if (character === "\\" && !preserveWindowsBackslashes) {
|
|
974
|
+
const escaped = command[index + 1];
|
|
975
|
+
if (!escaped || escaped === "\r" || escaped === "\n")
|
|
976
|
+
return null;
|
|
977
|
+
if (["$", "`", '"', "\\"].includes(escaped)) {
|
|
978
|
+
word += escaped;
|
|
979
|
+
index += 1;
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
// POSIX preserves a backslash in double quotes unless it quotes a
|
|
983
|
+
// shell-active character. Keep it so suffix comparisons are exact.
|
|
984
|
+
word += "\\";
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
word += character;
|
|
989
|
+
}
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (character === "'" || character === '"') {
|
|
993
|
+
quote = character;
|
|
994
|
+
tokenStarted = true;
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (UNQUOTED_POSIX_SHELL_ACTIVE_CHARACTERS.has(character))
|
|
998
|
+
return null;
|
|
999
|
+
if (character === "\\" && !preserveWindowsBackslashes) {
|
|
1000
|
+
const escaped = command[index + 1];
|
|
1001
|
+
if (!escaped || escaped === "\r" || escaped === "\n")
|
|
1002
|
+
return null;
|
|
1003
|
+
word += escaped;
|
|
1004
|
+
tokenStarted = true;
|
|
1005
|
+
index += 1;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (isShellWhitespace(character)) {
|
|
1009
|
+
if (tokenStarted) {
|
|
1010
|
+
words.push(word);
|
|
1011
|
+
word = "";
|
|
1012
|
+
tokenStarted = false;
|
|
1013
|
+
}
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
word += character;
|
|
1017
|
+
tokenStarted = true;
|
|
1018
|
+
}
|
|
1019
|
+
if (quote)
|
|
1020
|
+
return null;
|
|
1021
|
+
if (tokenStarted)
|
|
1022
|
+
words.push(word);
|
|
1023
|
+
return words.length > 0 ? words : null;
|
|
1024
|
+
}
|
|
1025
|
+
const ALWAYS_UNSAFE_WINDOWS_DIRECT_COMMAND_CHARACTERS = new Set(["%", "!", "$", "`"]);
|
|
1026
|
+
const UNQUOTED_UNSAFE_WINDOWS_DIRECT_COMMAND_CHARACTERS = new Set([
|
|
1027
|
+
"&", "|", "<", ">", "(", ")", "^", ";", "@", "*", "?", "[", "]", "{", "}", "~",
|
|
1028
|
+
]);
|
|
1029
|
+
/**
|
|
1030
|
+
* Empty/concatenated double-quote runs have cmd/UCRT and PowerShell parsing
|
|
1031
|
+
* rules that this ownership grammar intentionally does not model. Reject them
|
|
1032
|
+
* before tokenization so they cannot disappear into an owned proof path.
|
|
1033
|
+
*/
|
|
1034
|
+
function containsAdjacentDoubleQuotes(command) {
|
|
1035
|
+
return command.includes('""');
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Decode only cmd.exe-static argv. This intentionally permits only double-quote
|
|
1039
|
+
* grouping and the CommandLineToArgvW backslash/quote convention; all cmd
|
|
1040
|
+
* expansion and unquoted operator syntax is rejected during tokenization.
|
|
1041
|
+
*/
|
|
1042
|
+
function tokenizeWindowsDirectCommand(command) {
|
|
1043
|
+
if (containsControlCharacter(command) || containsAdjacentDoubleQuotes(command))
|
|
1044
|
+
return null;
|
|
1045
|
+
const words = [];
|
|
1046
|
+
let word = "";
|
|
1047
|
+
let tokenStarted = false;
|
|
1048
|
+
let quoted = false;
|
|
1049
|
+
for (let index = 0; index < command.length;) {
|
|
1050
|
+
const character = command[index];
|
|
1051
|
+
if (ALWAYS_UNSAFE_WINDOWS_DIRECT_COMMAND_CHARACTERS.has(character) ||
|
|
1052
|
+
(!quoted && UNQUOTED_UNSAFE_WINDOWS_DIRECT_COMMAND_CHARACTERS.has(character)))
|
|
1053
|
+
return null;
|
|
1054
|
+
if (character === "\\") {
|
|
1055
|
+
const start = index;
|
|
1056
|
+
while (command[index] === "\\")
|
|
1057
|
+
index += 1;
|
|
1058
|
+
const count = index - start;
|
|
1059
|
+
if (command[index] === '"') {
|
|
1060
|
+
word += "\\".repeat(Math.floor(count / 2));
|
|
1061
|
+
if (count % 2 === 0)
|
|
1062
|
+
quoted = !quoted;
|
|
1063
|
+
else
|
|
1064
|
+
word += '"';
|
|
1065
|
+
tokenStarted = true;
|
|
1066
|
+
index += 1;
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
word += "\\".repeat(count);
|
|
1070
|
+
tokenStarted = true;
|
|
1071
|
+
}
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (character === '"') {
|
|
1075
|
+
quoted = !quoted;
|
|
1076
|
+
tokenStarted = true;
|
|
1077
|
+
index += 1;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (isShellWhitespace(character)) {
|
|
1081
|
+
if (quoted) {
|
|
1082
|
+
word += character;
|
|
1083
|
+
}
|
|
1084
|
+
else if (tokenStarted) {
|
|
1085
|
+
words.push(word);
|
|
1086
|
+
word = "";
|
|
1087
|
+
tokenStarted = false;
|
|
1088
|
+
}
|
|
1089
|
+
index += 1;
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
word += character;
|
|
1093
|
+
tokenStarted = true;
|
|
1094
|
+
index += 1;
|
|
1095
|
+
}
|
|
1096
|
+
if (quoted)
|
|
1097
|
+
return null;
|
|
1098
|
+
if (tokenStarted)
|
|
1099
|
+
words.push(word);
|
|
1100
|
+
return words.length > 0 && words.every((value) => !value.includes('"')) ? words : null;
|
|
1101
|
+
}
|
|
1102
|
+
function containsControlCharacter(value) {
|
|
1103
|
+
for (const character of value) {
|
|
1104
|
+
const codePoint = character.codePointAt(0);
|
|
1105
|
+
if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f))
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
const WINDOWS_RESERVED_DEVICE_STEM = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])$/i;
|
|
1111
|
+
function isReservedWindowsDeviceComponent(component) {
|
|
1112
|
+
const dot = component.indexOf(".");
|
|
1113
|
+
const deviceStem = (dot === -1 ? component : component.slice(0, dot)).replace(/ +$/, "");
|
|
1114
|
+
return WINDOWS_RESERVED_DEVICE_STEM.test(deviceStem);
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Managed command ownership accepts only ordinary Win32 path syntax. Namespace,
|
|
1118
|
+
* device, stream, and wildcard forms can be lexically static yet resolve outside
|
|
1119
|
+
* the filesystem path the command appears to name, so do not own them.
|
|
1120
|
+
*/
|
|
1121
|
+
function isValidWindowsFilesystemPath(path) {
|
|
1122
|
+
if (!path || containsControlCharacter(path) || /["<>|?*]/.test(path))
|
|
1123
|
+
return false;
|
|
1124
|
+
if (/^(?:[\\/]{3,}|[\\/]{1,2}[?.][\\/]|[\\/]{1,2}\?\?[\\/]|[\\/](?:device|globalroot)[\\/])/i.test(path))
|
|
1125
|
+
return false;
|
|
1126
|
+
const firstColon = path.indexOf(":");
|
|
1127
|
+
if (firstColon !== -1 &&
|
|
1128
|
+
(firstColon !== 1 || !/[A-Za-z]/.test(path[0]) || path.indexOf(":", firstColon + 1) !== -1))
|
|
1129
|
+
return false;
|
|
1130
|
+
return path.split(/[\\/]+/).every((segment) => !segment || /^[A-Za-z]:$/.test(segment) ||
|
|
1131
|
+
(!segment.endsWith(".") && !segment.endsWith(" ") && !isReservedWindowsDeviceComponent(segment)));
|
|
1132
|
+
}
|
|
1133
|
+
/** Accept only drive-absolute or complete UNC paths as ownership proof. */
|
|
1134
|
+
function isQualifiedWindowsFilesystemPath(path) {
|
|
1135
|
+
if (!isValidWindowsFilesystemPath(path))
|
|
1136
|
+
return false;
|
|
1137
|
+
if (/^[A-Za-z]:[\\/]/.test(path))
|
|
1138
|
+
return true;
|
|
1139
|
+
if (!/^[\\/]{2}/.test(path))
|
|
1140
|
+
return false;
|
|
1141
|
+
const uncSegments = path.slice(2).split(/[\\/]+/);
|
|
1142
|
+
return uncSegments.length >= 2 && uncSegments[0].length > 0 && uncSegments[1].length > 0;
|
|
1143
|
+
}
|
|
1144
|
+
function isAbsolutePosixFilesystemPath(path) {
|
|
1145
|
+
return path.startsWith("/") && !containsControlCharacter(path);
|
|
1146
|
+
}
|
|
1147
|
+
/** Decode only literal PowerShell command words; interpolation is never owned. */
|
|
1148
|
+
function tokenizePowerShellCommand(command) {
|
|
1149
|
+
const tokens = [];
|
|
1150
|
+
let word = "";
|
|
1151
|
+
let tokenStarted = false;
|
|
1152
|
+
let tokenQuoted = false;
|
|
1153
|
+
let quote;
|
|
1154
|
+
let hasCallOperator = false;
|
|
1155
|
+
if (containsControlCharacter(command) || containsAdjacentDoubleQuotes(command))
|
|
1156
|
+
return null;
|
|
1157
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
1158
|
+
const character = command[index];
|
|
1159
|
+
if (quote === "'") {
|
|
1160
|
+
if (character !== "'") {
|
|
1161
|
+
word += character;
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
if (command[index + 1] === "'") {
|
|
1165
|
+
word += "'";
|
|
1166
|
+
index += 1;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
quote = undefined;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (quote === '"') {
|
|
1173
|
+
if (character === '"') {
|
|
1174
|
+
quote = undefined;
|
|
1175
|
+
}
|
|
1176
|
+
else if (character === "$" || character === "`") {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
word += character;
|
|
1181
|
+
}
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
if (character === "'" || character === '"') {
|
|
1185
|
+
quote = character;
|
|
1186
|
+
tokenStarted = true;
|
|
1187
|
+
tokenQuoted = true;
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (character === "&") {
|
|
1191
|
+
if (hasCallOperator ||
|
|
1192
|
+
tokenStarted ||
|
|
1193
|
+
tokens.length > 0 ||
|
|
1194
|
+
!isShellWhitespace(command[index + 1] ?? ""))
|
|
1195
|
+
return null;
|
|
1196
|
+
hasCallOperator = true;
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (UNQUOTED_POWERSHELL_ACTIVE_CHARACTERS.has(character))
|
|
1200
|
+
return null;
|
|
1201
|
+
if (isShellWhitespace(character)) {
|
|
1202
|
+
if (tokenStarted) {
|
|
1203
|
+
tokens.push({ value: word, quoted: tokenQuoted });
|
|
1204
|
+
word = "";
|
|
1205
|
+
tokenStarted = false;
|
|
1206
|
+
tokenQuoted = false;
|
|
1207
|
+
}
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
word += character;
|
|
1211
|
+
tokenStarted = true;
|
|
1212
|
+
}
|
|
1213
|
+
if (quote)
|
|
1214
|
+
return null;
|
|
1215
|
+
if (tokenStarted)
|
|
1216
|
+
tokens.push({ value: word, quoted: tokenQuoted });
|
|
1217
|
+
return tokens.length > 0 ? { tokens, hasCallOperator } : null;
|
|
1218
|
+
}
|
|
1219
|
+
function commandBasename(path, platform) {
|
|
1220
|
+
const executable = platform === "win32"
|
|
1221
|
+
? path.split(/[\\/]/).pop()
|
|
1222
|
+
: path.split("/").pop();
|
|
1223
|
+
return platform === "win32" ? executable?.toLowerCase() ?? "" : executable ?? "";
|
|
1224
|
+
}
|
|
1225
|
+
function isNativeHookScriptPath(path, platform) {
|
|
1226
|
+
return platform === "win32"
|
|
1227
|
+
? isQualifiedWindowsFilesystemPath(path) &&
|
|
1228
|
+
/(?:^|[\\/])dist[\\/]scripts[\\/]codex-native-hook\.js$/i.test(path)
|
|
1229
|
+
: isAbsolutePosixFilesystemPath(path) &&
|
|
1230
|
+
/(?:^|\/)dist\/scripts\/codex-native-hook\.js$/.test(path);
|
|
1231
|
+
}
|
|
1232
|
+
function hasNativeHookShimSuffix(path) {
|
|
1233
|
+
return /(?:^|[\\/])hooks[\\/]omx-native-hook-windows-shim\.ps1$/i.test(path);
|
|
1234
|
+
}
|
|
1235
|
+
function isNativeHookShimPath(path) {
|
|
1236
|
+
return isQualifiedWindowsFilesystemPath(path) && hasNativeHookShimSuffix(path);
|
|
1237
|
+
}
|
|
1238
|
+
function isSupportedDirectNodeExecutablePath(executablePath, platform) {
|
|
1239
|
+
const bareExecutable = platform === "win32" ? executablePath.toLowerCase() : executablePath;
|
|
1240
|
+
const supportedBareExecutable = platform === "win32"
|
|
1241
|
+
? bareExecutable === "node" || bareExecutable === "node.exe"
|
|
1242
|
+
: bareExecutable === "node";
|
|
1243
|
+
return supportedBareExecutable ||
|
|
1244
|
+
(platform === "win32"
|
|
1245
|
+
? isQualifiedWindowsFilesystemPath(executablePath)
|
|
1246
|
+
: isAbsolutePosixFilesystemPath(executablePath)) &&
|
|
1247
|
+
(platform === "win32"
|
|
1248
|
+
? commandBasename(executablePath, "win32") === "node" ||
|
|
1249
|
+
commandBasename(executablePath, "win32") === "node.exe"
|
|
1250
|
+
: commandBasename(executablePath, "linux") === "node");
|
|
1251
|
+
}
|
|
1252
|
+
function isSupportedPowerShellExecutablePath(executablePath) {
|
|
1253
|
+
return executablePath.toLowerCase() === "powershell.exe" ||
|
|
1254
|
+
(isQualifiedWindowsFilesystemPath(executablePath) &&
|
|
1255
|
+
commandBasename(executablePath, "win32") === "powershell.exe");
|
|
1256
|
+
}
|
|
1257
|
+
function isValidDirectNodeCommand(command, platform) {
|
|
1258
|
+
const words = platform === "win32"
|
|
1259
|
+
? tokenizeWindowsDirectCommand(command)
|
|
1260
|
+
: tokenizePosixCommand(command);
|
|
1261
|
+
const executablePath = words?.[0];
|
|
1262
|
+
const scriptPath = words?.[1];
|
|
1263
|
+
return words?.length === 2 &&
|
|
1264
|
+
executablePath !== undefined &&
|
|
1265
|
+
scriptPath !== undefined &&
|
|
1266
|
+
isSupportedDirectNodeExecutablePath(executablePath, platform) &&
|
|
1267
|
+
isNativeHookScriptPath(scriptPath, platform);
|
|
1268
|
+
}
|
|
1269
|
+
function hasStaticWindowsShimCommandGrammar(command) {
|
|
1270
|
+
const parsed = tokenizePowerShellCommand(command);
|
|
1271
|
+
if (!parsed)
|
|
1272
|
+
return false;
|
|
1273
|
+
const { tokens, hasCallOperator } = parsed;
|
|
1274
|
+
const powerShellToken = tokens[0];
|
|
1275
|
+
const powerShellPath = powerShellToken?.value;
|
|
1276
|
+
const shimPath = tokens[5]?.value;
|
|
1277
|
+
return tokens.length === 6 &&
|
|
1278
|
+
powerShellToken !== undefined &&
|
|
1279
|
+
powerShellPath !== undefined &&
|
|
1280
|
+
shimPath !== undefined &&
|
|
1281
|
+
(!powerShellToken.quoted || hasCallOperator) &&
|
|
1282
|
+
isSupportedPowerShellExecutablePath(powerShellPath) &&
|
|
1283
|
+
isValidWindowsFilesystemPath(shimPath) &&
|
|
1284
|
+
tokens[1]?.value.toLowerCase() === "-noprofile" &&
|
|
1285
|
+
tokens[2]?.value.toLowerCase() === "-executionpolicy" &&
|
|
1286
|
+
tokens[3]?.value.toLowerCase() === "bypass" &&
|
|
1287
|
+
tokens[4]?.value.toLowerCase() === "-file" &&
|
|
1288
|
+
hasNativeHookShimSuffix(shimPath);
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Parse an OMX Windows shim invocation using the managed-command ownership
|
|
1292
|
+
* grammar. Returns the validated shim path; returns null for every other
|
|
1293
|
+
* command. Supplying current install options additionally recognizes the exact
|
|
1294
|
+
* non-Windows host-path spelling used by platform-seam validation.
|
|
1295
|
+
*/
|
|
1296
|
+
export function parseManagedCodexNativeHookWindowsShimCommand(command, options) {
|
|
1297
|
+
const parsed = tokenizePowerShellCommand(command);
|
|
1298
|
+
const shimPath = parsed?.tokens[5]?.value;
|
|
1299
|
+
return shimPath !== undefined &&
|
|
1300
|
+
hasStaticWindowsShimCommandGrammar(command) &&
|
|
1301
|
+
(isNativeHookShimPath(shimPath) ||
|
|
1302
|
+
(options !== undefined &&
|
|
1303
|
+
isExactCurrentWindowsShimCommand(command, "win32", options)))
|
|
1304
|
+
? shimPath
|
|
1305
|
+
: null;
|
|
1306
|
+
}
|
|
1307
|
+
function isValidWindowsShimCommand(command) {
|
|
1308
|
+
return parseManagedCodexNativeHookWindowsShimCommand(command) !== null;
|
|
1309
|
+
}
|
|
1310
|
+
const OMX_COMMAND_PATTERN = /(?:codex-native-hook\.js|omx-native-hook-windows-shim\.ps1)/i;
|
|
1311
|
+
/**
|
|
1312
|
+
* Lex only static POSIX word fragments for ambiguity detection. This is
|
|
1313
|
+
* deliberately more permissive than the ownership grammar: backslash-newline
|
|
1314
|
+
* continuations and escaped characters must identify OMX-looking commands as
|
|
1315
|
+
* ambiguous, never make them owned.
|
|
1316
|
+
*/
|
|
1317
|
+
function decodedPosixCommandWords(command) {
|
|
1318
|
+
const words = [];
|
|
1319
|
+
let word = "";
|
|
1320
|
+
let quote;
|
|
1321
|
+
const flushWord = () => {
|
|
1322
|
+
if (word)
|
|
1323
|
+
words.push(word);
|
|
1324
|
+
word = "";
|
|
1325
|
+
};
|
|
1326
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
1327
|
+
const character = command[index];
|
|
1328
|
+
if (quote === "'") {
|
|
1329
|
+
if (character === "'")
|
|
1330
|
+
quote = undefined;
|
|
1331
|
+
else
|
|
1332
|
+
word += character;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
if (quote === '"') {
|
|
1336
|
+
if (character === '"') {
|
|
1337
|
+
quote = undefined;
|
|
1338
|
+
}
|
|
1339
|
+
else if (character === "\\") {
|
|
1340
|
+
const escaped = command[index + 1];
|
|
1341
|
+
if (escaped === "\n") {
|
|
1342
|
+
index += 1;
|
|
1343
|
+
}
|
|
1344
|
+
else if (escaped === "\r" && command[index + 2] === "\n") {
|
|
1345
|
+
index += 2;
|
|
1346
|
+
}
|
|
1347
|
+
else if (escaped && ["$", "`", '"', "\\"].includes(escaped)) {
|
|
1348
|
+
word += escaped;
|
|
1349
|
+
index += 1;
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
word += "\\";
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
word += character;
|
|
1357
|
+
}
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
if (character === "'" || character === '"') {
|
|
1361
|
+
quote = character;
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
if (character === "\\") {
|
|
1365
|
+
const escaped = command[index + 1];
|
|
1366
|
+
if (escaped === "\n") {
|
|
1367
|
+
index += 1;
|
|
1368
|
+
}
|
|
1369
|
+
else if (escaped === "\r" && command[index + 2] === "\n") {
|
|
1370
|
+
index += 2;
|
|
1371
|
+
}
|
|
1372
|
+
else if (escaped) {
|
|
1373
|
+
word += escaped;
|
|
1374
|
+
index += 1;
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
word += character;
|
|
1378
|
+
}
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
if (isShellWhitespace(character) || UNQUOTED_POSIX_SHELL_ACTIVE_CHARACTERS.has(character)) {
|
|
1382
|
+
flushWord();
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
word += character;
|
|
1386
|
+
}
|
|
1387
|
+
flushWord();
|
|
1388
|
+
return words;
|
|
1389
|
+
}
|
|
1390
|
+
function commandMentionsDecodedPosixOmx(command) {
|
|
1391
|
+
return decodedPosixCommandWords(command).some((word) => OMX_COMMAND_PATTERN.test(word));
|
|
1392
|
+
}
|
|
1393
|
+
function commandMentionsOmx(command) {
|
|
1394
|
+
return OMX_COMMAND_PATTERN.test(command) || OMX_COMMAND_PATTERN.test(command.replace(/["']/g, ""));
|
|
1395
|
+
}
|
|
1396
|
+
function isValidManagedCommand(command, platform) {
|
|
1397
|
+
return isValidDirectNodeCommand(command, platform) ||
|
|
1398
|
+
(platform === "win32" && isValidWindowsShimCommand(command));
|
|
1399
|
+
}
|
|
1400
|
+
function buildExactCurrentWindowsHostShimCommand(options) {
|
|
1401
|
+
if (process.platform === "win32" ||
|
|
1402
|
+
(options.platform ?? process.platform) !== "win32" ||
|
|
1403
|
+
options.codexHomeDir === undefined)
|
|
1404
|
+
return null;
|
|
1405
|
+
const shimPath = join(options.codexHomeDir, ...WINDOWS_NATIVE_HOOK_SHIM_RELATIVE_PATH);
|
|
1406
|
+
const powerShellPath = resolveWindowsPowerShellPath(options.env);
|
|
1407
|
+
return `& ${quotePowerShellLiteral(powerShellPath)} -NoProfile -ExecutionPolicy Bypass -File ${quotePowerShellLiteral(shimPath)}`;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* A current Windows command may have a drive-less shim path in non-Windows
|
|
1411
|
+
* platform seams. Accept only byte-identical canonical or host-joined command
|
|
1412
|
+
* spellings produced from resolved current options; generic historical
|
|
1413
|
+
* classification remains provenance-qualified.
|
|
1414
|
+
*/
|
|
1415
|
+
function isExactCurrentWindowsShimCommand(command, platform, options) {
|
|
1416
|
+
if (platform !== "win32" ||
|
|
1417
|
+
(options.platform ?? process.platform) !== "win32" ||
|
|
1418
|
+
options.codexHomeDir === undefined ||
|
|
1419
|
+
!hasStaticWindowsShimCommandGrammar(command))
|
|
1420
|
+
return false;
|
|
1421
|
+
return command === buildManagedCodexNativeHookCommand("", options) ||
|
|
1422
|
+
command === buildExactCurrentWindowsHostShimCommand(options);
|
|
1423
|
+
}
|
|
1424
|
+
function isValidHistoricalManagedCommand(command) {
|
|
1425
|
+
return isValidDirectNodeCommand(command, "linux") ||
|
|
1426
|
+
isValidDirectNodeCommand(command, "win32") ||
|
|
1427
|
+
isValidWindowsShimCommand(command);
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Returns whether a command has the exact approved token grammar of an OMX
|
|
1431
|
+
* native hook from this or a historical installation. Ownership requires a
|
|
1432
|
+
* shell-static executable and provenance-qualified terminal script path, but
|
|
1433
|
+
* never a current package-root path. The union is deliberately narrow and
|
|
1434
|
+
* accepts only shell-static POSIX, Windows, or PowerShell command spellings.
|
|
1435
|
+
*/
|
|
1436
|
+
export function isManagedCodexHookCommand(command) {
|
|
1437
|
+
return isValidHistoricalManagedCommand(command);
|
|
1438
|
+
}
|
|
1439
|
+
function classifyManagedCommand(handler, options) {
|
|
1440
|
+
const command = stringProperty(handler, "command");
|
|
1441
|
+
const windows = nullableStringProperty(handler, "commandWindows") ??
|
|
1442
|
+
nullableStringProperty(handler, "command_windows");
|
|
1443
|
+
const platform = options.platform ?? process.platform;
|
|
1444
|
+
const commandPlatform = typeof windows === "string" ? "linux" : platform;
|
|
1445
|
+
const supplied = [
|
|
1446
|
+
{ command, platform: commandPlatform },
|
|
1447
|
+
...(typeof windows === "string" ? [{ command: windows, platform: "win32" }] : []),
|
|
1448
|
+
];
|
|
1449
|
+
let hasOwnedCommand = false;
|
|
1450
|
+
let hasForeignAlternative = false;
|
|
1451
|
+
for (const suppliedCommand of supplied) {
|
|
1452
|
+
// Examine static POSIX word decoding before exact grammar validation. A
|
|
1453
|
+
// non-exact invocation can hide an OMX filename with escapes or a line
|
|
1454
|
+
// continuation, and must fail closed rather than become a foreign hook.
|
|
1455
|
+
const mentionsDecodedPosixOmx = commandMentionsDecodedPosixOmx(suppliedCommand.command);
|
|
1456
|
+
if (isValidManagedCommand(suppliedCommand.command, suppliedCommand.platform) ||
|
|
1457
|
+
isExactCurrentWindowsShimCommand(suppliedCommand.command, suppliedCommand.platform, options)) {
|
|
1458
|
+
hasOwnedCommand = true;
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
if (mentionsDecodedPosixOmx || commandMentionsOmx(suppliedCommand.command))
|
|
1462
|
+
return "ambiguous";
|
|
1463
|
+
hasForeignAlternative = true;
|
|
1464
|
+
}
|
|
1465
|
+
return hasOwnedCommand
|
|
1466
|
+
? hasForeignAlternative ? "ambiguous" : "owned"
|
|
1467
|
+
: "foreign";
|
|
1468
|
+
}
|
|
1469
|
+
function hasOpaqueProperties(node, known) {
|
|
1470
|
+
return node.properties.some((property) => !known.has(property.key));
|
|
1471
|
+
}
|
|
1472
|
+
function groupMatcher(node) {
|
|
1473
|
+
return nullableStringProperty(node, "matcher");
|
|
1474
|
+
}
|
|
1475
|
+
function matcherOwnershipError(eventName, group) {
|
|
1476
|
+
const matcher = groupMatcher(group.node);
|
|
1477
|
+
if (matcherIsAware(eventName) && typeof matcher === "string" && !isValidCodexMatcher(matcher)) {
|
|
1478
|
+
return planError("ambiguous_managed_group", "Cannot mutate an OMX group with an invalid matcher.", {
|
|
1479
|
+
eventName,
|
|
1480
|
+
groupIndex: group.groupIndex,
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
const foreignSiblings = group.handlers.some((handler) => !handler.owner);
|
|
1484
|
+
if (eventName === "SessionStart") {
|
|
1485
|
+
const allowed = foreignSiblings
|
|
1486
|
+
? matcher === "startup|resume|clear"
|
|
1487
|
+
: matcher === undefined || matcher === null || matcher === "startup" || matcher === "startup|resume" || matcher === "startup|resume|clear";
|
|
1488
|
+
if (!allowed) {
|
|
1489
|
+
return planError("ambiguous_managed_group", "SessionStart matcher is not compatible with OMX ownership.", {
|
|
1490
|
+
eventName,
|
|
1491
|
+
groupIndex: group.groupIndex,
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
else if (matcher !== undefined && matcher !== null) {
|
|
1496
|
+
return planError("ambiguous_managed_group", "OMX managed groups must not carry a matcher for this event.", {
|
|
1497
|
+
eventName,
|
|
1498
|
+
groupIndex: group.groupIndex,
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
function inspectOwnership(document, options) {
|
|
1504
|
+
const groups = [];
|
|
1505
|
+
const owners = [];
|
|
1506
|
+
for (const eventName of CODEX_HOOK_EVENTS) {
|
|
1507
|
+
const event = document.hooksNode ? objectProperty(document.hooksNode, eventName) : undefined;
|
|
1508
|
+
if (!event || event.value.kind !== "array")
|
|
1509
|
+
continue;
|
|
1510
|
+
for (const [groupIndex, groupNode] of event.value.elements.entries()) {
|
|
1511
|
+
if (groupNode.kind !== "object")
|
|
1512
|
+
continue;
|
|
1513
|
+
const hooks = objectProperty(groupNode, "hooks");
|
|
1514
|
+
const handlerNodes = hooks?.value.kind === "array" ? hooks.value.elements : [];
|
|
1515
|
+
const handlers = [];
|
|
1516
|
+
for (const [handlerIndex, handlerNode] of handlerNodes.entries()) {
|
|
1517
|
+
if (handlerNode.kind !== "object")
|
|
1518
|
+
continue;
|
|
1519
|
+
const type = stringProperty(handlerNode, "type");
|
|
1520
|
+
let classification = "foreign";
|
|
1521
|
+
if (type === "command")
|
|
1522
|
+
classification = classifyManagedCommand(handlerNode, options);
|
|
1523
|
+
if (classification === "ambiguous") {
|
|
1524
|
+
return planError("ambiguous_managed_handler", "A command mentions OMX but does not match the managed command grammar.", {
|
|
1525
|
+
eventName,
|
|
1526
|
+
groupIndex,
|
|
1527
|
+
handlerIndex,
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
const asyncProperty = type === "command"
|
|
1531
|
+
? objectProperty(handlerNode, "async")
|
|
1532
|
+
: undefined;
|
|
1533
|
+
const asynchronous = asyncProperty?.value.kind === "boolean" &&
|
|
1534
|
+
asyncProperty.value.value === true;
|
|
1535
|
+
if (classification !== "owned" || !isManagedHookEventName(eventName)) {
|
|
1536
|
+
if (classification === "owned") {
|
|
1537
|
+
return planError("ambiguous_managed_handler", "An OMX command is attached to an unmanaged Codex event.", {
|
|
1538
|
+
eventName,
|
|
1539
|
+
groupIndex,
|
|
1540
|
+
handlerIndex,
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
const foreignCommandIsSkipped = type === "command" && (commandIsSkipped(effectiveCommandForPlatform(handlerNode, options.platform ?? process.platform)) || asynchronous);
|
|
1544
|
+
handlers.push({ node: handlerNode, foreign: true, discoverySkipped: foreignCommandIsSkipped });
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
const command = effectiveCommandForPlatform(handlerNode, options.platform ?? process.platform);
|
|
1548
|
+
if (asynchronous || commandIsSkipped(command)) {
|
|
1549
|
+
return planError("ambiguous_managed_handler", "Cannot mutate skipped OMX command handlers.", {
|
|
1550
|
+
eventName,
|
|
1551
|
+
groupIndex,
|
|
1552
|
+
handlerIndex,
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
const owner = {
|
|
1556
|
+
eventName,
|
|
1557
|
+
groupIndex,
|
|
1558
|
+
handlerIndex,
|
|
1559
|
+
groupNode,
|
|
1560
|
+
handlerNode,
|
|
1561
|
+
opaqueHandler: hasOpaqueProperties(handlerNode, KNOWN_COMMAND_FIELDS),
|
|
1562
|
+
};
|
|
1563
|
+
owners.push(owner);
|
|
1564
|
+
handlers.push({ node: handlerNode, owner, foreign: false });
|
|
1565
|
+
}
|
|
1566
|
+
const group = {
|
|
1567
|
+
eventName,
|
|
1568
|
+
groupIndex,
|
|
1569
|
+
node: groupNode,
|
|
1570
|
+
handlers,
|
|
1571
|
+
opaque: hasOpaqueProperties(groupNode, KNOWN_GROUP_FIELDS),
|
|
1572
|
+
foreign: false,
|
|
1573
|
+
};
|
|
1574
|
+
group.foreign = handlers.length === 0 || handlers.some((handler) => handler.foreign);
|
|
1575
|
+
groups.push(group);
|
|
1576
|
+
const groupOwners = handlers.flatMap((handler) => handler.owner ? [handler.owner] : []);
|
|
1577
|
+
if (groupOwners.length > 0) {
|
|
1578
|
+
if (handlers.some((handler) => handler.discoverySkipped)) {
|
|
1579
|
+
return planError("ambiguous_managed_handler", "Cannot mutate an OMX group that also contains a skipped command handler.", {
|
|
1580
|
+
eventName,
|
|
1581
|
+
groupIndex,
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
const error = matcherOwnershipError(eventName, group);
|
|
1585
|
+
if (error)
|
|
1586
|
+
return error;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return { owners, groups };
|
|
1591
|
+
}
|
|
1592
|
+
function applySourcePatches(content, patches) {
|
|
1593
|
+
const ordered = [...patches].sort((left, right) => left.start - right.start || left.end - right.end);
|
|
1594
|
+
for (let index = 1; index < ordered.length; index += 1) {
|
|
1595
|
+
if (ordered[index - 1].end > ordered[index].start) {
|
|
1596
|
+
throw new Error("Overlapping source patches are not permitted.");
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
let result = content;
|
|
1600
|
+
for (const patch of ordered.reverse()) {
|
|
1601
|
+
result = result.slice(0, patch.start) + patch.text + result.slice(patch.end);
|
|
1602
|
+
}
|
|
1603
|
+
return result;
|
|
1604
|
+
}
|
|
1605
|
+
function propertyRemovalPatch(object, property) {
|
|
1606
|
+
if (object.properties.length === 1) {
|
|
1607
|
+
return { start: object.start + 1, end: object.end - 1, text: "" };
|
|
1608
|
+
}
|
|
1609
|
+
const index = object.properties.indexOf(property);
|
|
1610
|
+
if (index < object.properties.length - 1) {
|
|
1611
|
+
return { start: property.keyNode.start, end: object.properties[index + 1].keyNode.start, text: "" };
|
|
1612
|
+
}
|
|
1613
|
+
return { start: object.properties[index - 1].value.end, end: property.value.end, text: "" };
|
|
1614
|
+
}
|
|
1615
|
+
function appendObjectPropertyPatch(object, key, value) {
|
|
1616
|
+
const prefix = object.properties.length === 0 ? "" : ",";
|
|
1617
|
+
return { start: object.end - 1, end: object.end - 1, text: `${prefix}${JSON.stringify(key)}:${value}` };
|
|
1618
|
+
}
|
|
1619
|
+
function appendObjectPropertiesPatch(object, entries) {
|
|
1620
|
+
const prefix = object.properties.length === 0 ? "" : ",";
|
|
1621
|
+
const text = entries
|
|
1622
|
+
.map(([key, value]) => `${JSON.stringify(key)}:${value}`)
|
|
1623
|
+
.join(",");
|
|
1624
|
+
return { start: object.end - 1, end: object.end - 1, text: `${prefix}${text}` };
|
|
1625
|
+
}
|
|
1626
|
+
function appendArrayElementPatch(array, value) {
|
|
1627
|
+
const prefix = array.elements.length === 0 ? "" : ",";
|
|
1628
|
+
return { start: array.end - 1, end: array.end - 1, text: `${prefix}${value}` };
|
|
1629
|
+
}
|
|
1630
|
+
function knownFieldPatches(content, node, desired, known) {
|
|
1631
|
+
const removedProperties = node.properties.filter((property) => known.has(property.key) && !Object.hasOwn(desired, property.key));
|
|
1632
|
+
if (removedProperties.length > 1) {
|
|
1633
|
+
const members = node.properties.flatMap((property) => {
|
|
1634
|
+
if (!known.has(property.key))
|
|
1635
|
+
return [content.slice(property.keyNode.start, property.value.end)];
|
|
1636
|
+
if (!Object.hasOwn(desired, property.key))
|
|
1637
|
+
return [];
|
|
1638
|
+
return [`${JSON.stringify(property.key)}:${JSON.stringify(desired[property.key])}`];
|
|
1639
|
+
});
|
|
1640
|
+
for (const [key, value] of Object.entries(desired)) {
|
|
1641
|
+
if (!objectProperty(node, key))
|
|
1642
|
+
members.push(`${JSON.stringify(key)}:${JSON.stringify(value)}`);
|
|
1643
|
+
}
|
|
1644
|
+
return [{ start: node.start, end: node.end, text: `{${members.join(",")}}` }];
|
|
124
1645
|
}
|
|
125
|
-
|
|
1646
|
+
const patches = [];
|
|
1647
|
+
for (const property of node.properties) {
|
|
1648
|
+
if (!known.has(property.key))
|
|
1649
|
+
continue;
|
|
1650
|
+
if (Object.hasOwn(desired, property.key)) {
|
|
1651
|
+
patches.push({
|
|
1652
|
+
start: property.value.start,
|
|
1653
|
+
end: property.value.end,
|
|
1654
|
+
text: JSON.stringify(desired[property.key]),
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
patches.push(propertyRemovalPatch(node, property));
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
for (const [key, value] of Object.entries(desired)) {
|
|
1662
|
+
if (!objectProperty(node, key))
|
|
1663
|
+
patches.push(appendObjectPropertyPatch(node, key, JSON.stringify(value)));
|
|
1664
|
+
}
|
|
1665
|
+
return patches;
|
|
126
1666
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
1667
|
+
function serializeCodexHooksConfig(root) {
|
|
1668
|
+
return JSON.stringify(root, null, 2) + "\n";
|
|
1669
|
+
}
|
|
1670
|
+
class CanonicalJsonNumber {
|
|
1671
|
+
raw;
|
|
1672
|
+
constructor(raw) {
|
|
1673
|
+
this.raw = raw;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function canonicalJsonString(value) {
|
|
1677
|
+
if (value instanceof CanonicalJsonNumber)
|
|
1678
|
+
return value.raw;
|
|
1679
|
+
if (Array.isArray(value))
|
|
1680
|
+
return `[${value.map((item) => canonicalJsonString(item)).join(",")}]`;
|
|
1681
|
+
if (isPlainObject(value)) {
|
|
1682
|
+
return `{${Object.keys(value)
|
|
1683
|
+
.sort()
|
|
1684
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJsonString(value[key])}`)
|
|
1685
|
+
.join(",")}}`;
|
|
1686
|
+
}
|
|
1687
|
+
const serialized = JSON.stringify(value);
|
|
1688
|
+
if (serialized === undefined)
|
|
1689
|
+
throw new TypeError("Codex hook trust identity must be JSON-serializable.");
|
|
1690
|
+
return serialized;
|
|
1691
|
+
}
|
|
1692
|
+
function versionForCodexTomlIdentity(value) {
|
|
1693
|
+
return `sha256:${createHash("sha256").update(canonicalJsonString(value)).digest("hex")}`;
|
|
1694
|
+
}
|
|
1695
|
+
function managedHookStateKey(hooksPath, eventName, groupIndex, handlerIndex) {
|
|
1696
|
+
return `${hooksPath}:${CODEX_HOOK_EVENT_LABELS[eventName]}:${groupIndex}:${handlerIndex}`;
|
|
1697
|
+
}
|
|
1698
|
+
function canonicalHookForEvent(entry) {
|
|
1699
|
+
const hook = entry.hooks[0];
|
|
134
1700
|
return {
|
|
135
|
-
|
|
136
|
-
|
|
1701
|
+
type: "command",
|
|
1702
|
+
command: hook.command,
|
|
1703
|
+
...(typeof hook.timeout === "number" ? { timeout: hook.timeout } : {}),
|
|
137
1704
|
};
|
|
138
1705
|
}
|
|
139
|
-
|
|
140
|
-
const command = buildManagedCodexNativeHookCommand(pkgRoot, options);
|
|
1706
|
+
function canonicalGroupForEvent(entry) {
|
|
141
1707
|
return {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
buildCommandHook(command, {
|
|
145
|
-
matcher: "startup|resume|clear",
|
|
146
|
-
}),
|
|
147
|
-
],
|
|
148
|
-
PreToolUse: [
|
|
149
|
-
buildCommandHook(command),
|
|
150
|
-
],
|
|
151
|
-
PostToolUse: [
|
|
152
|
-
buildCommandHook(command),
|
|
153
|
-
],
|
|
154
|
-
UserPromptSubmit: [
|
|
155
|
-
buildCommandHook(command),
|
|
156
|
-
],
|
|
157
|
-
PreCompact: [
|
|
158
|
-
buildCommandHook(command),
|
|
159
|
-
],
|
|
160
|
-
PostCompact: [
|
|
161
|
-
buildCommandHook(command),
|
|
162
|
-
],
|
|
163
|
-
Stop: [
|
|
164
|
-
buildCommandHook(command, {
|
|
165
|
-
timeout: 30,
|
|
166
|
-
}),
|
|
167
|
-
],
|
|
168
|
-
},
|
|
1708
|
+
...(entry.matcher ? { matcher: entry.matcher } : {}),
|
|
1709
|
+
hooks: [canonicalHookForEvent(entry)],
|
|
169
1710
|
};
|
|
170
1711
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
1712
|
+
function canonicalMatcherForEvent(entry) {
|
|
1713
|
+
return entry.matcher ? { matcher: entry.matcher } : {};
|
|
1714
|
+
}
|
|
1715
|
+
function removesAllGroupHandlers(group, ownersToRemove) {
|
|
1716
|
+
return group.handlers.length > 0 &&
|
|
1717
|
+
group.handlers.every((handler) => handler.owner && ownersToRemove.has(handler.owner));
|
|
1718
|
+
}
|
|
1719
|
+
function removalProof(model, ownersToRemove) {
|
|
1720
|
+
for (const group of model.groups) {
|
|
1721
|
+
if (group.opaque && removesAllGroupHandlers(group, ownersToRemove)) {
|
|
1722
|
+
return {
|
|
1723
|
+
safe: false,
|
|
1724
|
+
shifted: {
|
|
1725
|
+
kind: "group",
|
|
1726
|
+
eventName: group.eventName,
|
|
1727
|
+
oldCoordinate: [group.groupIndex],
|
|
1728
|
+
},
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
180
1731
|
}
|
|
181
|
-
|
|
182
|
-
|
|
1732
|
+
for (const owner of ownersToRemove) {
|
|
1733
|
+
if (owner.opaqueHandler) {
|
|
1734
|
+
return {
|
|
1735
|
+
safe: false,
|
|
1736
|
+
shifted: {
|
|
1737
|
+
kind: "handler",
|
|
1738
|
+
eventName: owner.eventName,
|
|
1739
|
+
oldCoordinate: [owner.groupIndex, owner.handlerIndex],
|
|
1740
|
+
},
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
const byEvent = new Map();
|
|
1745
|
+
for (const group of model.groups) {
|
|
1746
|
+
const eventGroups = byEvent.get(group.eventName) ?? [];
|
|
1747
|
+
eventGroups.push(group);
|
|
1748
|
+
byEvent.set(group.eventName, eventGroups);
|
|
1749
|
+
}
|
|
1750
|
+
for (const [eventName, groups] of byEvent) {
|
|
1751
|
+
const removedGroups = new Set();
|
|
1752
|
+
for (const group of groups) {
|
|
1753
|
+
if (!group.opaque && removesAllGroupHandlers(group, ownersToRemove)) {
|
|
1754
|
+
removedGroups.add(group);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
const survivors = groups.filter((group) => !removedGroups.has(group));
|
|
1758
|
+
for (const group of groups) {
|
|
1759
|
+
const newGroupIndex = survivors.indexOf(group);
|
|
1760
|
+
if (group.foreign && newGroupIndex !== group.groupIndex) {
|
|
1761
|
+
return {
|
|
1762
|
+
safe: false,
|
|
1763
|
+
shifted: {
|
|
1764
|
+
kind: "group",
|
|
1765
|
+
eventName,
|
|
1766
|
+
oldCoordinate: [group.groupIndex],
|
|
1767
|
+
...(newGroupIndex >= 0 ? { newCoordinate: [newGroupIndex] } : {}),
|
|
1768
|
+
},
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
if (newGroupIndex < 0)
|
|
1772
|
+
continue;
|
|
1773
|
+
const remainingHandlers = group.handlers.filter((handler) => !handler.owner || !ownersToRemove.has(handler.owner));
|
|
1774
|
+
for (const [handlerIndex, handler] of group.handlers.entries()) {
|
|
1775
|
+
if (!handler.foreign || ownersToRemove.has(handler.owner))
|
|
1776
|
+
continue;
|
|
1777
|
+
const newHandlerIndex = remainingHandlers.indexOf(handler);
|
|
1778
|
+
if (newHandlerIndex !== handlerIndex) {
|
|
1779
|
+
return {
|
|
1780
|
+
safe: false,
|
|
1781
|
+
shifted: {
|
|
1782
|
+
kind: "handler",
|
|
1783
|
+
eventName,
|
|
1784
|
+
oldCoordinate: [group.groupIndex, handlerIndex],
|
|
1785
|
+
newCoordinate: [newGroupIndex, newHandlerIndex],
|
|
1786
|
+
},
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
183
1791
|
}
|
|
1792
|
+
return { safe: true };
|
|
184
1793
|
}
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
|
|
1794
|
+
function fullRemovalGroupKeys(model, ownersToRemove) {
|
|
1795
|
+
const keys = new Map();
|
|
1796
|
+
for (const group of model.groups) {
|
|
1797
|
+
const removable = !group.opaque && removesAllGroupHandlers(group, ownersToRemove);
|
|
1798
|
+
if (!removable)
|
|
1799
|
+
continue;
|
|
1800
|
+
const eventKeys = keys.get(group.eventName) ?? new Set();
|
|
1801
|
+
eventKeys.add(group.groupIndex);
|
|
1802
|
+
keys.set(group.eventName, eventKeys);
|
|
1803
|
+
}
|
|
1804
|
+
return keys;
|
|
188
1805
|
}
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
1806
|
+
function arrayWithoutElements(content, array, removed) {
|
|
1807
|
+
const values = [];
|
|
1808
|
+
for (const element of array.elements) {
|
|
1809
|
+
if (!removed.has(element))
|
|
1810
|
+
values.push(content.slice(element.start, element.end));
|
|
192
1811
|
}
|
|
193
|
-
return
|
|
194
|
-
return isPlainObject(hook)
|
|
195
|
-
&& hook.type === "command"
|
|
196
|
-
&& typeof hook.command === "string"
|
|
197
|
-
&& isOmxManagedHookCommand(hook.command);
|
|
198
|
-
}).length;
|
|
1812
|
+
return { start: array.start, end: array.end, text: `[${values.join(",")}]` };
|
|
199
1813
|
}
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
1814
|
+
function eventArrayForGroup(document, eventName) {
|
|
1815
|
+
const event = document.hooksNode ? objectProperty(document.hooksNode, eventName) : undefined;
|
|
1816
|
+
return event?.value.kind === "array" ? event.value : undefined;
|
|
1817
|
+
}
|
|
1818
|
+
function pruneRemovedManagedEventProperties(content, deletedEvents, options) {
|
|
1819
|
+
let current = content;
|
|
1820
|
+
for (;;) {
|
|
1821
|
+
const validation = validateCodexHooksDocument(current, options);
|
|
1822
|
+
if (!("result" in validation))
|
|
1823
|
+
return validation.error;
|
|
1824
|
+
let emptyEvent;
|
|
1825
|
+
for (const eventName of deletedEvents) {
|
|
1826
|
+
const event = validation.hooksNode ? objectProperty(validation.hooksNode, eventName) : undefined;
|
|
1827
|
+
if (event?.value.kind === "array" && event.value.elements.length === 0) {
|
|
1828
|
+
emptyEvent = eventName;
|
|
1829
|
+
break;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
if (emptyEvent === undefined) {
|
|
1833
|
+
if (!validation.hooksNode || validation.hooksNode.properties.length > 0)
|
|
1834
|
+
return current;
|
|
1835
|
+
const hooks = objectProperty(validation.rootNode, "hooks");
|
|
1836
|
+
if (!hooks)
|
|
1837
|
+
return validation.rootNode.properties.length === 0 ? null : current;
|
|
1838
|
+
current = applySourcePatches(current, [propertyRemovalPatch(validation.rootNode, hooks)]);
|
|
1839
|
+
const afterRoot = validateCodexHooksDocument(current, options);
|
|
1840
|
+
if (!("result" in afterRoot))
|
|
1841
|
+
return afterRoot.error;
|
|
1842
|
+
return afterRoot.rootNode.properties.length === 0 ? null : current;
|
|
1843
|
+
}
|
|
1844
|
+
const property = objectProperty(validation.hooksNode, emptyEvent);
|
|
1845
|
+
current = applySourcePatches(current, [propertyRemovalPatch(validation.hooksNode, property)]);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
function applyCoordinateSafeRemovals(content, model, ownersToRemove, options) {
|
|
1849
|
+
const fullGroups = fullRemovalGroupKeys(model, ownersToRemove);
|
|
1850
|
+
const partialPatches = [];
|
|
1851
|
+
for (const group of model.groups) {
|
|
1852
|
+
if (fullGroups.get(group.eventName)?.has(group.groupIndex))
|
|
1853
|
+
continue;
|
|
1854
|
+
const hooks = objectProperty(group.node, "hooks")?.value;
|
|
1855
|
+
if (hooks?.kind !== "array")
|
|
1856
|
+
continue;
|
|
1857
|
+
const removedHandlers = new Set(group.handlers
|
|
1858
|
+
.filter((handler) => handler.owner && ownersToRemove.has(handler.owner))
|
|
1859
|
+
.map((handler) => handler.node));
|
|
1860
|
+
if (removedHandlers.size > 0)
|
|
1861
|
+
partialPatches.push(arrayWithoutElements(content, hooks, removedHandlers));
|
|
1862
|
+
}
|
|
1863
|
+
let current = partialPatches.length > 0 ? applySourcePatches(content, partialPatches) : content;
|
|
1864
|
+
if (fullGroups.size === 0)
|
|
1865
|
+
return current;
|
|
1866
|
+
const afterPartial = validateCodexHooksDocument(current, options);
|
|
1867
|
+
if (!("result" in afterPartial))
|
|
1868
|
+
return afterPartial.error;
|
|
1869
|
+
const afterOwnership = inspectOwnership(afterPartial, options);
|
|
1870
|
+
if (afterOwnership instanceof ManagedCodexHooksPlanError)
|
|
1871
|
+
return afterOwnership;
|
|
1872
|
+
const groupPatches = [];
|
|
1873
|
+
const deletedEvents = new Set();
|
|
1874
|
+
for (const [eventName, groupIndices] of fullGroups) {
|
|
1875
|
+
const array = eventArrayForGroup(afterPartial, eventName);
|
|
1876
|
+
if (!array)
|
|
1877
|
+
continue;
|
|
1878
|
+
const removedGroups = new Set(afterOwnership.groups
|
|
1879
|
+
.filter((group) => group.eventName === eventName && groupIndices.has(group.groupIndex))
|
|
1880
|
+
.map((group) => group.node));
|
|
1881
|
+
if (removedGroups.size === 0)
|
|
1882
|
+
continue;
|
|
1883
|
+
groupPatches.push(arrayWithoutElements(current, array, removedGroups));
|
|
1884
|
+
if (removedGroups.size === array.elements.length)
|
|
1885
|
+
deletedEvents.add(eventName);
|
|
1886
|
+
}
|
|
1887
|
+
current = groupPatches.length > 0 ? applySourcePatches(current, groupPatches) : current;
|
|
1888
|
+
return pruneRemovedManagedEventProperties(current, deletedEvents, options);
|
|
1889
|
+
}
|
|
1890
|
+
function exactLegacyState(node) {
|
|
1891
|
+
if (node.kind !== "object")
|
|
203
1892
|
return null;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
1893
|
+
const entries = {};
|
|
1894
|
+
const seenKeys = new Set();
|
|
1895
|
+
for (const property of node.properties) {
|
|
1896
|
+
if (seenKeys.has(property.key))
|
|
1897
|
+
return null;
|
|
1898
|
+
seenKeys.add(property.key);
|
|
1899
|
+
if (property.value.kind !== "object")
|
|
1900
|
+
return null;
|
|
1901
|
+
const entry = property.value;
|
|
1902
|
+
if (entry.properties.some((member) => member.key !== "trusted_hash" && member.key !== "enabled"))
|
|
1903
|
+
return null;
|
|
1904
|
+
if (duplicateKnownProperty(entry, KNOWN_LEGACY_TRUST_STATE_ENTRY_FIELDS))
|
|
1905
|
+
return null;
|
|
1906
|
+
const hash = stringProperty(entry, "trusted_hash");
|
|
1907
|
+
const enabled = objectProperty(entry, "enabled");
|
|
1908
|
+
if (!hash || (enabled && enabled.value.kind !== "boolean"))
|
|
1909
|
+
return null;
|
|
1910
|
+
setOwnRecordValue(entries, property.key, {
|
|
1911
|
+
trusted_hash: hash,
|
|
1912
|
+
...(enabled?.value.kind === "boolean" ? { enabled: enabled.value.value } : {}),
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
return entries;
|
|
210
1916
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1917
|
+
function identicalLegacyTrustStateEntry(left, right) {
|
|
1918
|
+
return left.trusted_hash === right.trusted_hash &&
|
|
1919
|
+
Object.hasOwn(left, "enabled") === Object.hasOwn(right, "enabled") &&
|
|
1920
|
+
left.enabled === right.enabled;
|
|
1921
|
+
}
|
|
1922
|
+
function mergeLegacyTrustState(target, incoming, source) {
|
|
1923
|
+
const conflictingKeys = [];
|
|
1924
|
+
for (const [key, entry] of Object.entries(incoming)) {
|
|
1925
|
+
const existing = Object.hasOwn(target, key) ? target[key] : undefined;
|
|
1926
|
+
if (existing === undefined) {
|
|
1927
|
+
setOwnRecordValue(target, key, entry);
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
if (!identicalLegacyTrustStateEntry(existing, entry))
|
|
1931
|
+
conflictingKeys.push(key);
|
|
1932
|
+
}
|
|
1933
|
+
if (conflictingKeys.length === 0)
|
|
214
1934
|
return null;
|
|
215
|
-
const
|
|
216
|
-
|
|
1935
|
+
const keys = conflictingKeys.sort((left, right) => left.localeCompare(right));
|
|
1936
|
+
return planError("managed_trust_key_conflict", `Conflicting legacy hook trust state appears in root state and ${source}.`, { keys, source });
|
|
1937
|
+
}
|
|
1938
|
+
function prepareLegacyState(content) {
|
|
1939
|
+
const root = parseJsonSource(content);
|
|
1940
|
+
if (!root || root.kind !== "object") {
|
|
1941
|
+
return { ok: false, error: planError("invalid_document", "hooks.json must contain a JSON object.") };
|
|
1942
|
+
}
|
|
1943
|
+
const legacyTrustState = {};
|
|
1944
|
+
const rootStates = root.properties.filter((property) => property.key === "state");
|
|
1945
|
+
if (rootStates.length > 1) {
|
|
1946
|
+
return { ok: false, error: planError("invalid_document", "Duplicate top-level state is not supported.") };
|
|
1947
|
+
}
|
|
1948
|
+
if (rootStates.length === 1) {
|
|
1949
|
+
const exact = exactLegacyState(rootStates[0].value);
|
|
1950
|
+
if (!exact) {
|
|
1951
|
+
return { ok: false, error: planError("invalid_document", "Top-level state is not an exact historical OMX trust map.") };
|
|
1952
|
+
}
|
|
1953
|
+
const conflict = mergeLegacyTrustState(legacyTrustState, exact, "root");
|
|
1954
|
+
if (conflict)
|
|
1955
|
+
return { ok: false, error: conflict };
|
|
1956
|
+
}
|
|
1957
|
+
const hooks = objectProperty(root, "hooks");
|
|
1958
|
+
const nestedStates = hooks?.value.kind === "object"
|
|
1959
|
+
? hooks.value.properties.filter((property) => property.key === "state")
|
|
217
1960
|
: [];
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
1961
|
+
const exactNestedStates = nestedStates.map((state) => exactLegacyState(state.value));
|
|
1962
|
+
let nestedStatesToRemove = 0;
|
|
1963
|
+
if (nestedStates.length > 1 && exactNestedStates.some((state) => state === null)) {
|
|
1964
|
+
return {
|
|
1965
|
+
ok: false,
|
|
1966
|
+
error: planError("invalid_document", "Duplicate hooks.state entries must be exact historical OMX trust maps."),
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
if (nestedStates.length === 0 || exactNestedStates[0] !== null) {
|
|
1970
|
+
for (const exact of exactNestedStates) {
|
|
1971
|
+
if (!exact)
|
|
1972
|
+
continue;
|
|
1973
|
+
const conflict = mergeLegacyTrustState(legacyTrustState, exact, "hooks.state");
|
|
1974
|
+
if (conflict)
|
|
1975
|
+
return { ok: false, error: conflict };
|
|
1976
|
+
}
|
|
1977
|
+
nestedStatesToRemove = exactNestedStates.length;
|
|
1978
|
+
}
|
|
1979
|
+
let preparedContent = content;
|
|
1980
|
+
if (rootStates.length === 1) {
|
|
1981
|
+
const currentRoot = parseJsonSource(preparedContent);
|
|
1982
|
+
preparedContent = applySourcePatches(preparedContent, [
|
|
1983
|
+
propertyRemovalPatch(currentRoot, objectProperty(currentRoot, "state")),
|
|
1984
|
+
]);
|
|
1985
|
+
}
|
|
1986
|
+
for (let index = 0; index < nestedStatesToRemove; index += 1) {
|
|
1987
|
+
const currentRoot = parseJsonSource(preparedContent);
|
|
1988
|
+
const currentHooks = objectProperty(currentRoot, "hooks")?.value;
|
|
1989
|
+
if (currentHooks?.kind !== "object") {
|
|
1990
|
+
return { ok: false, error: planError("invalid_document", "hooks.json changed while preparing legacy state.") };
|
|
1991
|
+
}
|
|
1992
|
+
const currentState = objectProperty(currentHooks, "state");
|
|
1993
|
+
if (!currentState) {
|
|
1994
|
+
return { ok: false, error: planError("invalid_document", "hooks.state changed while preparing legacy state.") };
|
|
229
1995
|
}
|
|
1996
|
+
preparedContent = applySourcePatches(preparedContent, [propertyRemovalPatch(currentHooks, currentState)]);
|
|
230
1997
|
}
|
|
231
|
-
return
|
|
1998
|
+
return { content: preparedContent, legacyTrustState };
|
|
232
1999
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
2000
|
+
function trustFromDocument(document, hooksPath, options) {
|
|
2001
|
+
const ownership = inspectOwnership(document, options);
|
|
2002
|
+
if (ownership instanceof ManagedCodexHooksPlanError)
|
|
2003
|
+
return { ok: false, error: ownership };
|
|
2004
|
+
const trustState = {};
|
|
2005
|
+
for (const owner of ownership.owners) {
|
|
2006
|
+
const group = owner.groupNode;
|
|
2007
|
+
const handler = owner.handlerNode;
|
|
2008
|
+
const command = effectiveCommandForPlatform(handler, options.platform ?? process.platform);
|
|
2009
|
+
const timeout = objectProperty(handler, "timeout");
|
|
2010
|
+
const timeoutValue = timeout?.value.kind === "number"
|
|
2011
|
+
? timeout.value.raw === "0"
|
|
2012
|
+
? 1
|
|
2013
|
+
: new CanonicalJsonNumber(timeout.value.raw)
|
|
2014
|
+
: 600;
|
|
2015
|
+
const status = nullableStringProperty(handler, "statusMessage");
|
|
2016
|
+
const matcher = matcherIsAware(owner.eventName) ? groupMatcher(group) : undefined;
|
|
2017
|
+
setOwnRecordValue(trustState, managedHookStateKey(hooksPath, owner.eventName, owner.groupIndex, owner.handlerIndex), {
|
|
2018
|
+
trusted_hash: versionForCodexTomlIdentity({
|
|
2019
|
+
event_name: CODEX_HOOK_EVENT_LABELS[owner.eventName],
|
|
2020
|
+
...(matcher ? { matcher } : {}),
|
|
2021
|
+
hooks: [{
|
|
2022
|
+
type: "command",
|
|
2023
|
+
command,
|
|
2024
|
+
timeout: timeoutValue,
|
|
2025
|
+
async: false,
|
|
2026
|
+
...(status !== undefined && status !== null ? { statusMessage: status } : {}),
|
|
2027
|
+
}],
|
|
2028
|
+
}),
|
|
2029
|
+
});
|
|
236
2030
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
2031
|
+
return {
|
|
2032
|
+
ok: true,
|
|
2033
|
+
trustState,
|
|
2034
|
+
groupOccurrences: document.result.groupOccurrences,
|
|
2035
|
+
handlerOccurrences: document.result.handlerOccurrences,
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
export function scanManagedCodexHookTrustStateFromContent(content, hooksPath, options = {}) {
|
|
2039
|
+
const validation = validateCodexHooksDocument(content, options);
|
|
2040
|
+
if (!("result" in validation))
|
|
2041
|
+
return validation;
|
|
2042
|
+
return trustFromDocument(validation, hooksPath, options);
|
|
2043
|
+
}
|
|
2044
|
+
function hasExecutableForeignHandlerInUnknownEvent(event, platform) {
|
|
2045
|
+
if (KNOWN_HOOK_EVENT_FIELDS.has(event.key) || event.value.kind !== "array")
|
|
2046
|
+
return false;
|
|
2047
|
+
for (const group of event.value.elements) {
|
|
2048
|
+
if (group.kind !== "object")
|
|
2049
|
+
continue;
|
|
2050
|
+
const hooks = objectProperty(group, "hooks")?.value;
|
|
2051
|
+
if (hooks?.kind !== "array")
|
|
2052
|
+
continue;
|
|
2053
|
+
for (const handler of hooks.elements) {
|
|
2054
|
+
if (handler.kind !== "object")
|
|
2055
|
+
continue;
|
|
2056
|
+
const type = stringProperty(handler, "type");
|
|
2057
|
+
if (type === "prompt" || type === "agent")
|
|
2058
|
+
return true;
|
|
2059
|
+
if (type !== "command")
|
|
2060
|
+
continue;
|
|
2061
|
+
const asynchronous = objectProperty(handler, "async");
|
|
2062
|
+
if (asynchronous?.value.kind === "boolean" && asynchronous.value.value)
|
|
2063
|
+
continue;
|
|
2064
|
+
const command = platform === "win32"
|
|
2065
|
+
? nullableStringProperty(handler, "commandWindows") ??
|
|
2066
|
+
nullableStringProperty(handler, "command_windows") ??
|
|
2067
|
+
stringProperty(handler, "command")
|
|
2068
|
+
: stringProperty(handler, "command");
|
|
2069
|
+
if (typeof command === "string" && !commandIsSkipped(command))
|
|
2070
|
+
return true;
|
|
2071
|
+
}
|
|
247
2072
|
}
|
|
248
|
-
|
|
249
|
-
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
function foreignHooksInDocument(document, options) {
|
|
2076
|
+
const ownership = inspectOwnership(document, options);
|
|
2077
|
+
return ownership instanceof ManagedCodexHooksPlanError ||
|
|
2078
|
+
ownership.groups.some((group) => group.foreign) ||
|
|
2079
|
+
document.hooksNode?.properties.some((event) => hasExecutableForeignHandlerInUnknownEvent(event, options.platform ?? process.platform)) === true;
|
|
2080
|
+
}
|
|
2081
|
+
function finalizePlan(originalContent, finalContent, removedCount, coordinateProof, legacyTrustState, hooksPath, options, priorTrustState) {
|
|
2082
|
+
if (finalContent === null) {
|
|
2083
|
+
return {
|
|
2084
|
+
ok: true,
|
|
2085
|
+
finalContent: null,
|
|
2086
|
+
changed: originalContent !== null && originalContent !== undefined,
|
|
2087
|
+
removedCount,
|
|
2088
|
+
finalTrustState: {},
|
|
2089
|
+
priorTrustState,
|
|
2090
|
+
hasForeignHooks: false,
|
|
2091
|
+
coordinateProof,
|
|
2092
|
+
diagnostics: [],
|
|
2093
|
+
legacyTrustState,
|
|
2094
|
+
};
|
|
250
2095
|
}
|
|
2096
|
+
const validation = validateCodexHooksDocument(finalContent, options);
|
|
2097
|
+
if (!("result" in validation))
|
|
2098
|
+
return planFailure(validation.error);
|
|
2099
|
+
const scan = trustFromDocument(validation, hooksPath, options);
|
|
2100
|
+
if (!scan.ok)
|
|
2101
|
+
return planFailure(scan.error, validation.result.diagnostics);
|
|
251
2102
|
return {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
},
|
|
2103
|
+
ok: true,
|
|
2104
|
+
finalContent,
|
|
2105
|
+
changed: finalContent !== originalContent,
|
|
256
2106
|
removedCount,
|
|
2107
|
+
finalTrustState: scan.trustState,
|
|
2108
|
+
priorTrustState,
|
|
2109
|
+
hasForeignHooks: foreignHooksInDocument(validation, options),
|
|
2110
|
+
coordinateProof,
|
|
2111
|
+
diagnostics: validation.result.diagnostics,
|
|
2112
|
+
legacyTrustState,
|
|
257
2113
|
};
|
|
258
2114
|
}
|
|
259
|
-
function
|
|
260
|
-
|
|
2115
|
+
export function planManagedCodexHooksMerge(existingContent, pkgRoot, hooksPath, options = {}) {
|
|
2116
|
+
const resolvedOptions = resolveManagedCodexHookOptions(options, hooksPath);
|
|
2117
|
+
if (typeof existingContent !== "string") {
|
|
2118
|
+
const content = serializeCodexHooksConfig(jsonObjectFromUnknown(buildManagedCodexHooksConfig(pkgRoot, resolvedOptions)));
|
|
2119
|
+
return finalizePlan(existingContent, content, 0, { safe: true }, {}, hooksPath, resolvedOptions, {});
|
|
2120
|
+
}
|
|
2121
|
+
const prepared = prepareLegacyState(existingContent);
|
|
2122
|
+
if (!("content" in prepared))
|
|
2123
|
+
return planFailure(prepared.error);
|
|
2124
|
+
const validation = validateCodexHooksDocument(prepared.content, resolvedOptions);
|
|
2125
|
+
if (!("result" in validation))
|
|
2126
|
+
return planFailure(validation.error);
|
|
2127
|
+
const priorScan = trustFromDocument(validation, hooksPath, resolvedOptions);
|
|
2128
|
+
if (!priorScan.ok)
|
|
2129
|
+
return planFailure(priorScan.error, validation.result.diagnostics);
|
|
2130
|
+
const ownership = inspectOwnership(validation, resolvedOptions);
|
|
2131
|
+
if (ownership instanceof ManagedCodexHooksPlanError)
|
|
2132
|
+
return planFailure(ownership, validation.result.diagnostics);
|
|
2133
|
+
const managed = buildManagedCodexHooksConfig(pkgRoot, resolvedOptions);
|
|
2134
|
+
const patches = [];
|
|
2135
|
+
const missingEventProperties = [];
|
|
2136
|
+
for (const eventName of MANAGED_HOOK_EVENTS) {
|
|
2137
|
+
const eventOwners = ownership.owners.filter((owner) => owner.eventName === eventName)
|
|
2138
|
+
.sort((left, right) => left.groupIndex - right.groupIndex || left.handlerIndex - right.handlerIndex);
|
|
2139
|
+
const canonicalEntry = managed.hooks[eventName][0];
|
|
2140
|
+
if (eventOwners.length === 0) {
|
|
2141
|
+
const eventArray = eventArrayForGroup(validation, eventName);
|
|
2142
|
+
if (eventArray) {
|
|
2143
|
+
patches.push(appendArrayElementPatch(eventArray, JSON.stringify(canonicalGroupForEvent(canonicalEntry))));
|
|
2144
|
+
}
|
|
2145
|
+
else if (validation.hooksNode) {
|
|
2146
|
+
missingEventProperties.push([
|
|
2147
|
+
eventName,
|
|
2148
|
+
JSON.stringify([canonicalGroupForEvent(canonicalEntry)]),
|
|
2149
|
+
]);
|
|
2150
|
+
}
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
const primary = eventOwners[0];
|
|
2154
|
+
patches.push(...knownFieldPatches(prepared.content, primary.handlerNode, canonicalHookForEvent(canonicalEntry), KNOWN_COMMAND_FIELDS));
|
|
2155
|
+
const primaryGroup = ownership.groups.find((group) => group.node === primary.groupNode);
|
|
2156
|
+
if (!primaryGroup?.foreign) {
|
|
2157
|
+
patches.push(...knownFieldPatches(prepared.content, primary.groupNode, canonicalMatcherForEvent(canonicalEntry), KNOWN_MATCHER_FIELD));
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (validation.hooksNode && missingEventProperties.length > 0) {
|
|
2161
|
+
patches.push(appendObjectPropertiesPatch(validation.hooksNode, missingEventProperties));
|
|
2162
|
+
}
|
|
2163
|
+
if (!validation.hooksNode)
|
|
2164
|
+
patches.push(appendObjectPropertyPatch(validation.rootNode, "hooks", JSON.stringify(managed.hooks)));
|
|
2165
|
+
let intermediate;
|
|
2166
|
+
try {
|
|
2167
|
+
intermediate = applySourcePatches(prepared.content, patches);
|
|
2168
|
+
}
|
|
2169
|
+
catch (error) {
|
|
2170
|
+
return planFailure(planError("invalid_document", "Overlapping source edits prevented a safe merge.", { cause: String(error) }), validation.result.diagnostics);
|
|
2171
|
+
}
|
|
2172
|
+
const afterMerge = validateCodexHooksDocument(intermediate, resolvedOptions);
|
|
2173
|
+
if (!("result" in afterMerge))
|
|
2174
|
+
return planFailure(afterMerge.error);
|
|
2175
|
+
const afterOwnership = inspectOwnership(afterMerge, resolvedOptions);
|
|
2176
|
+
if (afterOwnership instanceof ManagedCodexHooksPlanError)
|
|
2177
|
+
return planFailure(afterOwnership, afterMerge.result.diagnostics);
|
|
2178
|
+
const removeOwners = new Set();
|
|
2179
|
+
for (const eventName of MANAGED_HOOK_EVENTS) {
|
|
2180
|
+
const eventOwners = afterOwnership.owners.filter((owner) => owner.eventName === eventName)
|
|
2181
|
+
.sort((left, right) => left.groupIndex - right.groupIndex || left.handlerIndex - right.handlerIndex);
|
|
2182
|
+
for (const duplicate of eventOwners.slice(1))
|
|
2183
|
+
removeOwners.add(duplicate);
|
|
2184
|
+
}
|
|
2185
|
+
const proof = removalProof(afterOwnership, removeOwners);
|
|
2186
|
+
if (!proof.safe) {
|
|
2187
|
+
return {
|
|
2188
|
+
ok: false,
|
|
2189
|
+
error: planError("unsafe_managed_removal", "Removing duplicate OMX hooks would shift a foreign coordinate or discard opaque metadata.", proof.shifted ? { shifted: proof.shifted } : {}),
|
|
2190
|
+
diagnostics: afterMerge.result.diagnostics,
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
let finalContent;
|
|
2194
|
+
try {
|
|
2195
|
+
finalContent = applyCoordinateSafeRemovals(intermediate, afterOwnership, removeOwners, resolvedOptions);
|
|
2196
|
+
}
|
|
2197
|
+
catch (error) {
|
|
2198
|
+
return planFailure(planError("invalid_document", "Source edits prevented a safe duplicate cleanup.", { cause: String(error) }), afterMerge.result.diagnostics);
|
|
2199
|
+
}
|
|
2200
|
+
if (finalContent instanceof ManagedCodexHooksPlanError)
|
|
2201
|
+
return planFailure(finalContent, afterMerge.result.diagnostics);
|
|
2202
|
+
return finalizePlan(existingContent, finalContent, removeOwners.size, proof, prepared.legacyTrustState, hooksPath, resolvedOptions, priorScan.trustState);
|
|
261
2203
|
}
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
2204
|
+
export function planManagedCodexHooksRemoval(existingContent, hooksPath, options = {}) {
|
|
2205
|
+
const resolvedOptions = resolveManagedCodexHookOptions(options, hooksPath);
|
|
2206
|
+
const prepared = prepareLegacyState(existingContent);
|
|
2207
|
+
if (!("content" in prepared))
|
|
2208
|
+
return planFailure(prepared.error);
|
|
2209
|
+
const validation = validateCodexHooksDocument(prepared.content, resolvedOptions);
|
|
2210
|
+
if (!("result" in validation))
|
|
2211
|
+
return planFailure(validation.error);
|
|
2212
|
+
const priorScan = trustFromDocument(validation, hooksPath, resolvedOptions);
|
|
2213
|
+
if (!priorScan.ok)
|
|
2214
|
+
return planFailure(priorScan.error, validation.result.diagnostics);
|
|
2215
|
+
const ownership = inspectOwnership(validation, resolvedOptions);
|
|
2216
|
+
if (ownership instanceof ManagedCodexHooksPlanError)
|
|
2217
|
+
return planFailure(ownership, validation.result.diagnostics);
|
|
2218
|
+
const removeOwners = new Set(ownership.owners);
|
|
2219
|
+
const proof = removalProof(ownership, removeOwners);
|
|
2220
|
+
if (!proof.safe) {
|
|
2221
|
+
return {
|
|
2222
|
+
ok: false,
|
|
2223
|
+
error: planError("unsafe_managed_removal", "Removing OMX hooks would shift a foreign coordinate or discard opaque metadata.", proof.shifted ? { shifted: proof.shifted } : {}),
|
|
2224
|
+
diagnostics: validation.result.diagnostics,
|
|
2225
|
+
};
|
|
265
2226
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
.map((key) => [key, canonicalJson(value[key])]));
|
|
2227
|
+
let finalContent;
|
|
2228
|
+
try {
|
|
2229
|
+
finalContent = applyCoordinateSafeRemovals(prepared.content, ownership, removeOwners, resolvedOptions);
|
|
270
2230
|
}
|
|
271
|
-
|
|
2231
|
+
catch (error) {
|
|
2232
|
+
return planFailure(planError("invalid_document", "Source edits prevented safe removal.", { cause: String(error) }), validation.result.diagnostics);
|
|
2233
|
+
}
|
|
2234
|
+
if (finalContent instanceof ManagedCodexHooksPlanError)
|
|
2235
|
+
return planFailure(finalContent, validation.result.diagnostics);
|
|
2236
|
+
return finalizePlan(existingContent, finalContent, removeOwners.size, proof, prepared.legacyTrustState, hooksPath, resolvedOptions, priorScan.trustState);
|
|
272
2237
|
}
|
|
273
|
-
function
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
return `sha256:${createHash("sha256").update(serialized).digest("hex")}`;
|
|
2238
|
+
function codexHookEntries(hooks, eventName) {
|
|
2239
|
+
const entries = hooks[eventName];
|
|
2240
|
+
return isJsonArray(entries) ? entries : [];
|
|
277
2241
|
}
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
type: "command",
|
|
285
|
-
command: hook.command,
|
|
286
|
-
timeout: Math.max(1, hook.timeout ?? 600),
|
|
287
|
-
async: false,
|
|
288
|
-
...(hook.statusMessage ? { statusMessage: hook.statusMessage } : {}),
|
|
289
|
-
},
|
|
290
|
-
],
|
|
291
|
-
};
|
|
2242
|
+
export function getMissingManagedCodexHookEvents(content) {
|
|
2243
|
+
const parsed = parseCodexHooksConfig(content);
|
|
2244
|
+
if (!parsed)
|
|
2245
|
+
return null;
|
|
2246
|
+
return MANAGED_HOOK_EVENTS.filter((eventName) => !codexHookEntries(parsed.hooks, eventName).some((entry) => isPlainObject(entry) && isJsonArray(entry.hooks) && entry.hooks.some((hook) => isPlainObject(hook) && hook.type === "command" && typeof hook.command === "string" &&
|
|
2247
|
+
isValidManagedCommand(hook.command, process.platform))));
|
|
292
2248
|
}
|
|
293
|
-
function
|
|
294
|
-
|
|
2249
|
+
export function getManagedCodexHookCommandsForEvent(content, eventName) {
|
|
2250
|
+
const parsed = parseCodexHooksConfig(content);
|
|
2251
|
+
if (!parsed)
|
|
2252
|
+
return null;
|
|
2253
|
+
return codexHookEntries(parsed.hooks, eventName).flatMap((entry) => isPlainObject(entry) && isJsonArray(entry.hooks)
|
|
2254
|
+
? entry.hooks.flatMap((hook) => isPlainObject(hook) && hook.type === "command" && typeof hook.command === "string" &&
|
|
2255
|
+
isValidManagedCommand(hook.command, process.platform)
|
|
2256
|
+
? [hook.command]
|
|
2257
|
+
: [])
|
|
2258
|
+
: []);
|
|
295
2259
|
}
|
|
296
2260
|
export function buildManagedCodexHookTrustState(hooksPath, pkgRoot, options = {}) {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
entry.hooks.forEach((hook, handlerIndex) => {
|
|
308
|
-
if (hook.type !== "command" || !isOmxManagedHookCommand(hook.command)) {
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
const key = managedHookStateKey(hooksPath, eventName, groupIndex, handlerIndex);
|
|
312
|
-
state[key] = {
|
|
313
|
-
trusted_hash: versionForCodexTomlIdentity(normalizedCommandHookIdentity(eventName, entry, hook)),
|
|
314
|
-
};
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
return state;
|
|
2261
|
+
const resolvedOptions = resolveManagedCodexHookOptions(options, hooksPath);
|
|
2262
|
+
if (options.hooksContent === null)
|
|
2263
|
+
return {};
|
|
2264
|
+
const content = typeof options.hooksContent === "string"
|
|
2265
|
+
? options.hooksContent
|
|
2266
|
+
: serializeCodexHooksConfig(jsonObjectFromUnknown(buildManagedCodexHooksConfig(pkgRoot, resolvedOptions)));
|
|
2267
|
+
const scan = scanManagedCodexHookTrustStateFromContent(content, hooksPath, resolvedOptions);
|
|
2268
|
+
if (!scan.ok)
|
|
2269
|
+
throw scan.error;
|
|
2270
|
+
return scan.trustState;
|
|
319
2271
|
}
|
|
320
2272
|
export function buildManagedCodexHookTrustToml(hooksPath, pkgRoot, options = {}) {
|
|
321
2273
|
if (!hooksPath)
|
|
@@ -426,10 +2378,10 @@ function collectTrustStateEntries(value) {
|
|
|
426
2378
|
if (!isPlainObject(rawEntry) || typeof rawEntry.trusted_hash !== "string") {
|
|
427
2379
|
continue;
|
|
428
2380
|
}
|
|
429
|
-
entries
|
|
2381
|
+
setOwnRecordValue(entries, key, {
|
|
430
2382
|
trusted_hash: rawEntry.trusted_hash,
|
|
431
2383
|
...(typeof rawEntry.enabled === "boolean" ? { enabled: rawEntry.enabled } : {}),
|
|
432
|
-
};
|
|
2384
|
+
});
|
|
433
2385
|
}
|
|
434
2386
|
return entries;
|
|
435
2387
|
}
|
|
@@ -451,110 +2403,35 @@ export function hasCodexHooksJsonTopLevelState(content) {
|
|
|
451
2403
|
return Object.hasOwn(parsed.root, "state");
|
|
452
2404
|
}
|
|
453
2405
|
export function mergeManagedCodexHooksConfig(existingContent, pkgRoot, hooksPathOrOptions, options = {}) {
|
|
454
|
-
const hooksPath = typeof hooksPathOrOptions === "string" ? hooksPathOrOptions :
|
|
2406
|
+
const hooksPath = typeof hooksPathOrOptions === "string" ? hooksPathOrOptions : "";
|
|
455
2407
|
const providedOptions = typeof hooksPathOrOptions === "object" && hooksPathOrOptions !== null
|
|
456
2408
|
? hooksPathOrOptions
|
|
457
2409
|
: options;
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
: {}),
|
|
463
|
-
};
|
|
464
|
-
const managedConfig = buildManagedCodexHooksConfig(pkgRoot, resolvedOptions);
|
|
465
|
-
const parsed = typeof existingContent === "string"
|
|
466
|
-
? parseCodexHooksConfig(existingContent)
|
|
467
|
-
: null;
|
|
468
|
-
const nextRoot = parsed ? cloneJson(parsed.root) : {};
|
|
469
|
-
const nextHooks = parsed ? cloneJson(parsed.hooks) : {};
|
|
470
|
-
delete nextRoot.state;
|
|
471
|
-
delete nextHooks.state;
|
|
472
|
-
for (const eventName of MANAGED_HOOK_EVENTS) {
|
|
473
|
-
const existingEntries = Array.isArray(nextHooks[eventName])
|
|
474
|
-
? nextHooks[eventName]
|
|
475
|
-
: [];
|
|
476
|
-
const preservedEntries = [];
|
|
477
|
-
for (const entry of existingEntries) {
|
|
478
|
-
const stripped = stripManagedHooksFromEntry(entry);
|
|
479
|
-
if (stripped.entry !== null) {
|
|
480
|
-
preservedEntries.push(stripped.entry);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
nextHooks[eventName] = [
|
|
484
|
-
...preservedEntries,
|
|
485
|
-
...managedConfig.hooks[eventName].map((entry) => cloneJson(entry)),
|
|
486
|
-
];
|
|
487
|
-
}
|
|
488
|
-
if (Object.keys(nextHooks).length > 0) {
|
|
489
|
-
nextRoot.hooks = nextHooks;
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
delete nextRoot.hooks;
|
|
493
|
-
}
|
|
494
|
-
return serializeCodexHooksConfig(nextRoot);
|
|
2410
|
+
const plan = planManagedCodexHooksMerge(existingContent, pkgRoot, hooksPath, providedOptions);
|
|
2411
|
+
if (!plan.ok)
|
|
2412
|
+
throw plan.error;
|
|
2413
|
+
return plan.finalContent ?? "";
|
|
495
2414
|
}
|
|
496
2415
|
export function removeManagedCodexHooks(existingContent) {
|
|
497
|
-
const
|
|
498
|
-
if (!
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
const nextRoot = cloneJson(parsed.root);
|
|
502
|
-
const nextHooks = cloneJson(parsed.hooks);
|
|
503
|
-
delete nextRoot.state;
|
|
504
|
-
delete nextHooks.state;
|
|
505
|
-
let removedCount = 0;
|
|
506
|
-
for (const [eventName, rawEntries] of Object.entries(nextHooks)) {
|
|
507
|
-
if (!Array.isArray(rawEntries))
|
|
508
|
-
continue;
|
|
509
|
-
const preservedEntries = [];
|
|
510
|
-
for (const entry of rawEntries) {
|
|
511
|
-
const stripped = stripManagedHooksFromEntry(entry);
|
|
512
|
-
removedCount += stripped.removedCount;
|
|
513
|
-
if (stripped.entry !== null) {
|
|
514
|
-
preservedEntries.push(stripped.entry);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (preservedEntries.length > 0) {
|
|
518
|
-
nextHooks[eventName] = preservedEntries;
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
delete nextHooks[eventName];
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
if (removedCount === 0) {
|
|
525
|
-
return { nextContent: existingContent, removedCount: 0 };
|
|
526
|
-
}
|
|
527
|
-
const hasRemainingHookEntries = Object.keys(nextHooks).length > 0;
|
|
528
|
-
if (hasRemainingHookEntries) {
|
|
529
|
-
nextRoot.hooks = nextHooks;
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
delete nextRoot.hooks;
|
|
533
|
-
}
|
|
534
|
-
if (Object.keys(nextRoot).length === 0) {
|
|
535
|
-
return { nextContent: null, removedCount };
|
|
536
|
-
}
|
|
537
|
-
return {
|
|
538
|
-
nextContent: serializeCodexHooksConfig(nextRoot),
|
|
539
|
-
removedCount,
|
|
540
|
-
};
|
|
2416
|
+
const plan = planManagedCodexHooksRemoval(existingContent, "");
|
|
2417
|
+
if (!plan.ok)
|
|
2418
|
+
throw plan.error;
|
|
2419
|
+
return { nextContent: plan.finalContent, removedCount: plan.removedCount };
|
|
541
2420
|
}
|
|
542
2421
|
export function hasCodexHookEntries(content) {
|
|
543
2422
|
const parsed = parseCodexHooksConfig(content);
|
|
544
2423
|
if (!parsed)
|
|
545
2424
|
return false;
|
|
546
2425
|
return Object.entries(parsed.hooks).some(([eventName, rawEntries]) => {
|
|
547
|
-
if (eventName === "state" || !
|
|
2426
|
+
if (eventName === "state" || !isJsonArray(rawEntries))
|
|
548
2427
|
return false;
|
|
549
|
-
return rawEntries.some((entry) =>
|
|
550
|
-
return isPlainObject(entry) &&
|
|
551
|
-
Array.isArray(entry.hooks) &&
|
|
552
|
-
entry.hooks.length > 0;
|
|
553
|
-
});
|
|
2428
|
+
return rawEntries.some((entry) => isPlainObject(entry) && isJsonArray(entry.hooks) && entry.hooks.length > 0);
|
|
554
2429
|
});
|
|
555
2430
|
}
|
|
556
2431
|
export function hasUserCodexHooksAfterManagedRemoval(existingContent) {
|
|
557
|
-
const
|
|
558
|
-
|
|
2432
|
+
const plan = planManagedCodexHooksRemoval(existingContent, "");
|
|
2433
|
+
if (!plan.ok)
|
|
2434
|
+
throw plan.error;
|
|
2435
|
+
return plan.finalContent !== null && hasCodexHookEntries(plan.finalContent);
|
|
559
2436
|
}
|
|
560
2437
|
//# sourceMappingURL=codex-hooks.js.map
|