gate: PATH-executable wrappers via centralized safe gate — gating in every shell - #30
Open
superbiche wants to merge 7 commits into
Open
gate: PATH-executable wrappers via centralized safe gate — gating in every shell#30superbiche wants to merge 7 commits into
superbiche wants to merge 7 commits into
Conversation
superbiche
force-pushed
the
feat/gate-path-wrappers
branch
3 times, most recently
from
July 31, 2026 15:32
5e11e6d to
a1edd2a
Compare
…n every shell Install gating was zsh-function-only: bash -c, Makefiles, CI steps, and child processes resolved straight to the version-manager shims, unaudited. - lib/gate-lib.sh (new): faithful bash port of the reviewed zsh routing — leading-global-flag resolution, per-tool flag tables, exec gating with node_modules/.bin parent walk, project-scan preflight, audit gate exit mapping, host-allow/install-known fallbacks. 88/88 differential parity against the old zsh implementation (exit codes + exact audit/real logs). - bin/safe: `safe gate <tool> -- <args…>` entrypoint; wrapper-probing `safe status`; SAFE_AUDIT_PATH honors the environment. - install.sh writes marker-tagged 3-line wrappers to ~/.local/bin for npm pnpm pnpx yarn bun pip pip3 uv cargo go composer (refuses to overwrite unmarked files); uninstall.sh removes marker-matched wrappers only. - lib/install-wrappers.zsh thins to a stub (no functions) that warns once interactively when gating is absent. Degraded-mode guards are obsolete: an executable cannot be half-loaded. - No env re-entry guard: recursion cannot loop (delegate resolution skips wrapper-marked files), and child processes that install SHOULD be gated — an env switch would be a forgeable bypass and a coverage hole. - Volta retirement: dead shim-backup resolution deleted from bin/safe-run; `safe run link` clears stale Volta runner paths; host-allow exec falls back to `mise which` at exec time (never goes stale in config); volta() wrapper gone; docs updated. Closes inbox 2026-07-13 volta note. - Deliberate behavior deltas (documented): --version/--help switches pass through in all wrapped tools (previously exit 100 — fatal as a PATH wrapper); the missing-safe-audit warning is per command, not per shell. - tests/install/run.sh repointed to gate mode: 69 cases green (degraded zsh-snapshot cases retired, wrapper install/uninstall/pnpx/perf cases added); passthrough wall time 16ms with zero audit spawns asserted. Co-implemented with a subagent; differential parity harness in tmp/. Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
The bash port predated the review fix round on the verdict-core branch; bring gate-lib to parity with the fixed zsh logic it replaced: - safe_gate_scan_target_flags (per tool family) threads --tag/--registry/ --prefix/--cwd, python index flags, cargo --registry/--index, and composer --repository/--working-dir into the audit as --dist-tag/--registry/ --project-dir (finding 3); yarn --cwd now reaches resolution. - cargo install --version/--vers threads the pin into the audited spec (finding 7). - The install-known offline fallback only accepts verdict-GO evidence (finding 5). Suites: install 69/69, audit 50/50, dispatcher 15/15, safe-run 31/31. Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
…l-set probes, constrained mise, exact markers Review verdict on 944045f: DO-NOT-SHIP 4 (+1 major, +1 minor). All closed: - F1 uv index selectors reach the audit: safe_gate_uv scans target flags (like pip_like already did), and -i/--index/--default-index are value flags in the python extractor — dropping the value made the URL look like a package spec and FAIL-OPENED the extractor into an unaudited delegate. Sources accumulate (delta-2 parity): one untrusted --find-links floors the verdict even beside a trusted index; the stale reader is source-scoped and canonicalizes ecosystem keys. - F2 one upgrade unit: install.sh refreshes ~/.config/safe/gate-lib.sh in EVERY mode that refreshes the safe binary whenever an installed copy or marked wrappers exist — library first, so a new dispatcher is never paired with an old library. Regression: stale-sentinel library + `install.sh --run` must end byte-identical to the repo library. - F3 full-set health probes: safe status and safe doctor --json report per-tool wrapper state (installed/shadowed/missing/foreign) over the whole tool list; the aggregate is healthy only when every tool resolves to its owned wrapper; the zsh stub warns on the full set too. - F4 constrained mise fallback: host_runner_binary resolves via `mise which <bin> --tool=<expected>` (npx→node, bunx→bun, uvx→uv) and validates the resolved path lives under installs/<expected>/ — a backend whose bin set ships a colliding npx can never be host-executed. safe run status shows the exec-time fallback instead of "not linked". - F5 exact ownership: install/uninstall/status/stub accept only a REGULAR file whose second line is exactly `# safe-gate-wrapper v1 tool=<tool>`; marked symlinks and marker-substring foreign files are never overwritten, deleted, or counted as ours. (Delegate resolution keeps the broad marker skip: recursion safety must win there.) - F6 docs sweep: index/architecture/command-reference/installation/ integration-flows and safe-run help no longer describe the retired zsh-function topology; --no-wrappers semantics documented. Suite: 73/73 (new: selective-install refresh, loose-marker ownership, full-set status/doctor probes, uv index selectors incl. =-forms). Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
…t refresh, unresolved wrappers Rebased onto the PR#29 round-5 head (7410348), which centralizes source identity behind the `safe audit effective-sources` plumbing command, then closed the four open delta findings. F1 (blocker): the gate's offline stale-evidence reader derived its own `${SAFE_GATE_REGISTRY:-default}` identity and matched receipts on `(.source // "default")`, while safe-audit stamps every receipt `implicit-default` or `explicit:<set>`. Every receipt therefore read as unscoped-default and a custom-index receipt could vouch for a default-index install on timeout. safe_gate_known_matches now asks safe-audit for the identity — `effective-sources <name> --ecosystem <eco> [--registry <set>] [--project-dir <d>]`, resolved through the same audit-binary gate the checks use so SAFE_AUDIT_PATH is honored — instead of mirroring a derivation that has to stay byte-identical across two files. No identity (missing binary, non-zero exit, empty output) fails closed: stale evidence never applies without a trustworthy match. Receipts compare on `(.source // "implicit-default")`. safe_gate_add_source picks up the round-5 accumulator semantics: strip trailing slashes, and move a REPEATED value to the END rather than dropping it, because npm is last-wins — `--registry A --registry B --registry A` installs from A and the resolver's last-word read must agree. F2 (major): install.sh refreshed the installed gate library only when an npm wrapper was present, so a damaged installation keeping an owned pnpm wrapper got a new dispatcher and no library. A gate_wrappers_exist helper scans the whole GATE_TOOLS set instead. F3 (blocker): gate_wrapper_state printed `installed` when `command -v <tool>` resolved to nothing, so a machine whose PATH omits $SAFE_BIN_DIR (cron, systemd) reported healthy while gating nothing. New `not-on-path` state, counted unhealthy by both `safe status` and the doctor JSON aggregate. F6 (minor): docs/integration-flows.md and docs/index.md still presented sourcing the retired zsh stub as the activation step. Activation is PATH placement of the generated wrappers, verified with `safe status`; the sourced file is a compatibility/health-warning stub. Added pnpx to the documented wrapper family in docs/command-reference.md. Tests: the writer/reader loop in check_version_aware.sh drives the REAL producer and the REAL consumer over the receipt it wrote (same-source reuse, both cross-source directions, env- and .npmrc-derived identity, explicit npmjs.org normalization, and the literal `--registry default` selector never colliding with the implicit-default sentinel). The gate-mode suite inherits round 5's repeated-registry case, which now exercises gate-lib's accumulator and fails without the last-wins fix. Plus selective-install refresh from a non-npm wrapper across --run/--audit/--no-wrappers, and a not-on-path case. Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
…p probes (R1) Two follow-ups on the round-2 delta fixes, both operator-visibility rather than gate behavior. effective-sources failure now says so. The stale-evidence reader fails closed when it cannot obtain a source identity, but doing that silently is the invisible-breakage class the operator ruled must surface: reuse simply never happens and the operator sees only occasional timeout refusals. Both failure modes (no resolvable safe-audit, and a non-zero or empty effective-sources) now emit `safe gate: effective-sources unavailable — stale-evidence reuse disabled, run safe doctor` on stderr. The fail-closed outcome is unchanged — the reader still returns 1 and the caller still refuses. bin/safe's safe_install_known_matches deliberately stays byte-identical to the PR#29 reference; the note lives in gate-lib only, which is the daily surface. R1: the ownership probe no longer narrates about binary neighbours. All four call sites (bin/safe, install.sh, uninstall.sh, and the zsh stub) captured the second line of an arbitrary file through command substitution, so a foreign ELF at a wrapped-tool path made bash print "ignored null byte in input" from status, doctor, and install. The exact whole-line comparison now runs as a byte-stream pipeline (LC_ALL=C sed -n 2p | grep -qxF) with no capture: same verdict on every input, no warning on any of them. Tests: the loose-marker ownership case grows a NUL-carrying binary at a wrapped-tool path and asserts it reads as `foreign` in both `safe status` and the doctor JSON, that it survives install and uninstall untouched, and that install, status, and doctor all stay quiet about it. The writer/reader loop gains a trailing-slash selector proving writer and reader canonicalize a source element identically (round 5 strips trailing slashes per element). Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
Rebased onto the shipped PR#29 head (a253c52) and carried the scanner changes from rounds 6-10 into the bash gate. Reference for every item is `a253c52:lib/install-wrappers.zsh` (the zsh scanner gate-lib was ported from) and `a253c52:bin/safe`. - Scoped selectors thread as KEYED tokens: `--@scope:registry URL` and `--@scope:registry=URL` accumulate `@scope:registry=URL`, not the bare URL. Flattening lost the key and let the resolver pick a different scope's registry (delta-6 3.2b). `--@*:registry` also joins the npm package-parser value table so the URL is never read as a package. - `--userconfig`/`--globalconfig` thread as PATHS (SAFE_GATE_NPM_USERCONFIG/GLOBALCONFIG -> `--npm-userconfig` / `--npm-globalconfig`) on BOTH the check invocation and the effective-sources reader call. The gate never opens a config file: extracting values lost key and precedence semantics and copied credentials into argv (delta-7 3.2b/N2). - `--find-links`/`-f` records its actual endpoint instead of the `local:find-links` sentinel, so an operator can trust one location rather than every local source (delta-5 3.2, operator-ruled). `--no-index` keeps its sentinel: a boolean switch has no endpoint to record. - `safe_gate_add_source` stays operational — no redaction. Credentials are needed for authenticated packument fetches; redaction is central in safe-audit at every identity/receipt/display sink (delta-6 N2 reverted the wrapper-side strip). - The R1 ownership probes anchor their path argument (`sed -n '2p' --`), the same dash-leading-filename class round 9 fixed audit-side. Tests: the merged suite's scoped/userconfig, credentials-preserved, find-links-endpoint and repeated-registry cases now exercise gate-lib and pass unchanged. `case_wrapper_state_cleared_after_gate` asserted that a long-lived shell clears the scanned set — structurally unfalsifiable in gate mode, where the scan lives in a process that exits, so it was passing vacuously after the merge. Replaced with the equivalent live risk on this branch: the credential-bearing set must not reach the DELEGATED tool's environment, detected by a new ENVLEAK line in the tool stub (verified to fire when the variable is present). My audit-suite sections re-splice as 62-64 behind the new case 61. Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
superbiche
force-pushed
the
feat/gate-path-wrappers
branch
from
July 31, 2026 17:14
a1edd2a to
efa4093
Compare
…ne, anchored readlink - F6: docs/install-wrappers.md lists the five actual per-tool states (installed/shadowed/not-on-path/missing/foreign) instead of the retired three-token vocabulary. - P2: safe_gate_exec_real de-exports all five SAFE_GATE_* scanner globals before the delegate exec — bash assignment preserves a caller's export attribute, which would have carried the credential-bearing operational set into the delegated tool's environment. The ENVLEAK stub detector now sweeps all five names and the case pre-exports sentinels on both routes, asserting the audit sees scanned values, the tool none, and the calling shell keeps its original sentinel. - N2: gate_wrapper_state anchors both readlink operands (--) and requires BOTH canonicalizations to succeed before comparing — a dash-leading relative SAFE_BIN_DIR no longer yields empty==empty falsely-installed with readlink noise; an unresolvable wrapper path reads not-on-path. New host-independent case probes both cwd shapes with clean stderr. Tests: install suite 81/81, check_version_aware 159/159, others exit 0. Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #29 (version-aware verdict core). Part 2 of the safe overhaul.
Problem
Install gating was zsh-function-only:
bash -c 'npm i evil', Makefile recipes, CI steps, child processes, andmise execall resolved straight to the mise shims with no audit. The only non-interactive coverage was the npx/uvx/bunx symlinks.Change
lib/gate-lib.sh(new, ~1650 lines): faithful bash port of the adversarially-reviewed zsh routing — leading-global-flag resolution, per-tool flag tables, exec gating (npm exec/x, pnpm dlx, yarn dlx, bun x, uv run --with, uv tool run, go run mod@ver) with the node_modules/.bin parent walk, project-scan preflight, gate exit mapping, host-allow/install-known fallbacks, target-selector threading from the audit: version-aware verdicts + install gate (fixes the CVE-remediation catch-22) #29 fix round.safe gate <tool> -- <args…>entrypoint inbin/safe; marker-tagged 3-line wrappers in~/.local/binfor npm pnpm pnpx yarn bun pip pip3 uv cargo go composer (written by install.sh, which refuses to overwrite unmarked files; uninstall.sh removes marked ones only). The delegate is the first non-wrapper executable on PATH, so mise per-project versions keep working. No env re-entry guard: recursion cannot loop by construction, and child processes that install should be gated — an env switch would be both a forgeable bypass and a coverage hole.safe statusprobes the wrappers (installed/shadowed/not installed) from any shell.bin/safe-run;safe run linkclears stale Volta runner paths; host-allow exec resolves the runner viamise whichat exec time (never goes stale in config). Closes inbox2026-07-13-codex-acp-retire-volta-safe-integration.md.--version/--helpswitches pass through (previously exit 100 — fatal as a PATH wrapper); missing-safe-audit warning is per command, not per shell.Verification
--package, uv--withboundaries, go run value flags, alias/colon specs.npm --versionthrough wrapper → gate → exec, zero audit spawns (asserted by a perf case).Residual (accepted, doctor-visible): absolute-path invocation of shims and PATH manipulation still bypass — the gate is a seatbelt for cooperative-but-unaware processes, not a jail.
mise install/usegating lands in part 3.@superbiche · maintainer · drafted with Claude Fable 5.
https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6