Conversation
|
This change is part of the following stack: Change managed by git-spice. |
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository: redhat-et/ripwire/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughChangesThe binary now embeds skills and hooks at configure time and provides Skills installation
Note The walkthrough was truncated because the summary exceeded the platform comment limit. Merge Risk: 🔵 Low · up to The change is mergeable with bounded follow-up, though jq lookup and settings permissions should be hardened to avoid local execution and disclosure risks. ✨ Finishing Touches🧪 Generate unit tests (beta)
|
ripwire skills install (#225)
…gling-symlink prune, Codex --hook, Claude router, hermes-native skills) PR redhat-et#293's release matrix was red across all 24 shards on nine distinct gates — not one bug, but several unfinished slices of redhat-et#225's own plan: - pruneStale only removed entries the *previous manifest* tracked, so a stray/dangling ripwire-* symlink the manifest never recorded (a skill removed from disk by hand, or planted by an older installer) lingered forever. Now scans destDir directly for ripwire-* symlinks not in the current set, same ripwire-* scope-of-authority guard as before (I7). - `--codex --hook` was a literal "not implemented yet" stub. Ported skills/install.sh's pre-redhat-et#225 install_codex_hook: PreToolUse nudge + SessionStart primer + UserPromptSubmit router merged into ${CODEX_HOME:-~/.codex}/hooks.json in one jq call, matched by exact command (Codex has one install channel, unlike Claude's isScript basename match). - Claude's `--hook` never registered the UserPromptSubmit prompt router (install_claude_route was never ported), and the substitution-meter disclosure banner (D2) was missing from the C++ path entirely. - `--hermes` only ever linked the flat ripwire-* set; skills/hermes/ ripwire-*/ (Hermes-native content, e.g. ripwire-repo-map) was never collected, so a real shipped skill silently never installed. - runJqMergeFile redirected jq's own stderr to /dev/null, so codexinstallhonestycheck's "does jq's parse-error diagnostic reach stderr" assertion could never pass. test/routehookcheck.sh and test/codexpromptroutecheck.sh asserted an installed hook's *exact* checkout path (pre-redhat-et#225: hooks lived beside a git checkout). Under redhat-et#225 the binary always registers its own extracted store copy, so updated both to match by script basename — the same "identify by script, not location" convention skills/install.sh's own jqIsScript already used. hermesinstallcheck.sh arm 7 planted a decoy skill in a *copy* of skills/ to prove the Hermes-native loop is ripwire-*-scoped; a copy is no longer a complete installer once skills are embedded at build time, so it never actually ran. Replaced with a behavioural check against the real shipped ripwire-repo-map skill plus a source-level pin on the guard, the same "grep the invariant when no live repro is constructible" idiom routehookcheck.sh's own D9 arm uses. collectCurrentSkills extracted out of installForAgent to keep the latter's complexity from crossing the quality-delta gate; readManifestV2/ ManifestV2 deleted (dead now that pruneStale scans the directory instead). .ripwire_quality_acks covers the two remaining gating short-horizon-churn rows (installForAgent, pruneStale) with one reason — this same commit's own footprint, nothing foreign. Verified: full pargates suite (655 gates) clean modulo one pre-existing, unrelated failure (legendcoveragecheck.sh, confirmed red on unmodified HEAD too); ASan+UBSan+LSan clean; deterministic; xmllint clean; --quality-delta gating=0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
918ca89 to
cf8607e
Compare
…gling-symlink prune, Codex --hook, Claude router, hermes-native skills) PR redhat-et#293's release matrix was red across all 24 shards on nine distinct gates — not one bug, but several unfinished slices of redhat-et#225's own plan: - pruneStale only removed entries the *previous manifest* tracked, so a stray/dangling ripwire-* symlink the manifest never recorded (a skill removed from disk by hand, or planted by an older installer) lingered forever. Now scans destDir directly for ripwire-* symlinks not in the current set, same ripwire-* scope-of-authority guard as before (I7). - `--codex --hook` was a literal "not implemented yet" stub. Ported skills/install.sh's pre-redhat-et#225 install_codex_hook: PreToolUse nudge + SessionStart primer + UserPromptSubmit router merged into ${CODEX_HOME:-~/.codex}/hooks.json in one jq call, matched by exact command (Codex has one install channel, unlike Claude's isScript basename match). - Claude's `--hook` never registered the UserPromptSubmit prompt router (install_claude_route was never ported), and the substitution-meter disclosure banner (D2) was missing from the C++ path entirely. - `--hermes` only ever linked the flat ripwire-* set; skills/hermes/ ripwire-*/ (Hermes-native content, e.g. ripwire-repo-map) was never collected, so a real shipped skill silently never installed. - runJqMergeFile redirected jq's own stderr to /dev/null, so codexinstallhonestycheck's "does jq's parse-error diagnostic reach stderr" assertion could never pass. test/routehookcheck.sh and test/codexpromptroutecheck.sh asserted an installed hook's *exact* checkout path (pre-redhat-et#225: hooks lived beside a git checkout). Under redhat-et#225 the binary always registers its own extracted store copy, so updated both to match by script basename — the same "identify by script, not location" convention skills/install.sh's own jqIsScript already used. hermesinstallcheck.sh arm 7 planted a decoy skill in a *copy* of skills/ to prove the Hermes-native loop is ripwire-*-scoped; a copy is no longer a complete installer once skills are embedded at build time, so it never actually ran. Replaced with a behavioural check against the real shipped ripwire-repo-map skill plus a source-level pin on the guard, the same "grep the invariant when no live repro is constructible" idiom routehookcheck.sh's own D9 arm uses. collectCurrentSkills extracted out of installForAgent to keep the latter's complexity from crossing the quality-delta gate; readManifestV2/ ManifestV2 deleted (dead now that pruneStale scans the directory instead). .ripwire_quality_acks covers the two remaining gating short-horizon-churn rows (installForAgent, pruneStale) with one reason — this same commit's own footprint, nothing foreign. Verified: full pargates suite (655 gates) clean modulo one pre-existing, unrelated failure (legendcoveragecheck.sh, confirmed red on unmodified HEAD too); ASan+UBSan+LSan clean; deterministic; xmllint clean; --quality-delta gating=0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thank you, @s0undt3ch — this has moved a lot since the last pass, and it moved in the right direction. The four What's left is smaller than it was, and mostly concentrated in Needed before merge
Worth doing
Small items (optional)
What's nextWe had to rebuild |
56e0514 to
aebda2c
Compare
…gling-symlink prune, Codex --hook, Claude router, hermes-native skills) PR redhat-et#293's release matrix was red across all 24 shards on nine distinct gates — not one bug, but several unfinished slices of redhat-et#225's own plan: - pruneStale only removed entries the *previous manifest* tracked, so a stray/dangling ripwire-* symlink the manifest never recorded (a skill removed from disk by hand, or planted by an older installer) lingered forever. Now scans destDir directly for ripwire-* symlinks not in the current set, same ripwire-* scope-of-authority guard as before (I7). - `--codex --hook` was a literal "not implemented yet" stub. Ported skills/install.sh's pre-redhat-et#225 install_codex_hook: PreToolUse nudge + SessionStart primer + UserPromptSubmit router merged into ${CODEX_HOME:-~/.codex}/hooks.json in one jq call, matched by exact command (Codex has one install channel, unlike Claude's isScript basename match). - Claude's `--hook` never registered the UserPromptSubmit prompt router (install_claude_route was never ported), and the substitution-meter disclosure banner (D2) was missing from the C++ path entirely. - `--hermes` only ever linked the flat ripwire-* set; skills/hermes/ ripwire-*/ (Hermes-native content, e.g. ripwire-repo-map) was never collected, so a real shipped skill silently never installed. - runJqMergeFile redirected jq's own stderr to /dev/null, so codexinstallhonestycheck's "does jq's parse-error diagnostic reach stderr" assertion could never pass. test/routehookcheck.sh and test/codexpromptroutecheck.sh asserted an installed hook's *exact* checkout path (pre-redhat-et#225: hooks lived beside a git checkout). Under redhat-et#225 the binary always registers its own extracted store copy, so updated both to match by script basename — the same "identify by script, not location" convention skills/install.sh's own jqIsScript already used. hermesinstallcheck.sh arm 7 planted a decoy skill in a *copy* of skills/ to prove the Hermes-native loop is ripwire-*-scoped; a copy is no longer a complete installer once skills are embedded at build time, so it never actually ran. Replaced with a behavioural check against the real shipped ripwire-repo-map skill plus a source-level pin on the guard, the same "grep the invariant when no live repro is constructible" idiom routehookcheck.sh's own D9 arm uses. collectCurrentSkills extracted out of installForAgent to keep the latter's complexity from crossing the quality-delta gate; readManifestV2/ ManifestV2 deleted (dead now that pruneStale scans the directory instead). .ripwire_quality_acks covers the two remaining gating short-horizon-churn rows (installForAgent, pruneStale) with one reason — this same commit's own footprint, nothing foreign. Verified: full pargates suite (655 gates) clean modulo one pre-existing, unrelated failure (legendcoveragecheck.sh, confirmed red on unmodified HEAD too); ASan+UBSan+LSan clean; deterministic; xmllint clean; --quality-delta gating=0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…allback, atomic writes, ownership-checked prune, self-checks Working through the maintainer's review of redhat-et#293 item by item: 1. extractGroup no longer trusts the store directory's NAME (a content-hash key) alone — storeContentsMatch() lstats it as a real directory and byte-compares every embedded file against its store copy (O_NOFOLLOW, size then bytes; the reference is already resident in the binary, so this costs a read, not a hash). A mismatch — missing, a symlink planted at that name, a partial extraction a killed run left behind, tampering — re-extracts from scratch through the existing tmp+rename path. 2. homeDir() no longer falls back to "/tmp" when HOME is unset; it's empty, and every caller refuses instead of silently writing somewhere the operator never chose. agentHomeOr() centralizes the "${ENV_VAR:-$HOME/suffix}" shape (RIPWIRE_DATA_HOME, CLAUDE_CONFIG_DIR, CODEX_HOME) and VALIDATEs each is absolute. wrap.h::resolveSkillsRoot gets the same fix at its own "~/" expansion — home + "/.claude/skills" with an empty home IS "/.claude/skills", not a caller-visible failure, which the caller here (and wrap --all's own listing) depends on. 3. settings.json and the manifest are now written via rw::pathguard::createExclTempFile + commit (temp beside the target, rename over it) instead of truncate-in-place — a crash mid-write can no longer leave either file empty, and commit()'s rename replaces whatever sits at the final name, including a symlink, without following it. 4. pruneStale reads the previous manifest again (readManifestSkillNames, VALIDATEd against the same ripwire-* shape pruneStale's own directory scan enforces) and now requires EITHER manifest tracking OR pruneTargetIsOurs (dangling, or resolves under dataHome()/"skills") before unlinking an untracked name — a user's own symlink that merely happens to be named ripwire-<something> is left alone. A manifest-tracked name stays prunable regardless of its current target, which is what test/skillinstallcheck.sh's own link-safety arm already pins. 5. codexdoctor.h::skillsCheck's complexity (3->26, the review's own --quality-delta flag) is split: skillsCheckHint now owns the 3-way hint-message branching, skillsCheck is fact-gathering only. 6. envOr, byte-identical in codexdoctor.h and skillsinstall.h, moves to the new src/infra/envutil.h. 7. A few targeted self-checks per CONTRIBUTING §3: EXPECTS non-empty bytes in writeStoreFile (an embedded file is never empty by construction), VALIDATE on the manifest's own skill= line shape (external, hand-editable input). 8. The stray tracked planning doc (.superpowers/sdd/embedded-skills-implementation-plan/fix-wave-2-report.md) is removed; the rest of that directory was never tracked. test/skillsinstallcheck.sh's arm 17 updated: a fully read-only skill directory cannot support the atomic manifest write's temp+rename either (needs directory write, same as the symlink() the arm is actually testing) — the honest outcome is a reported manifest-write failure and an unchanged manifest, not a silently-wrong count, which was only ever achievable through the non-atomic write this round replaced on purpose. test/crashsweepcheck.sh's S2 registry gains storeContentsMatch's one guarded open, matching every neighbouring entry's shape. Not done from the review (tracked for a follow-up, not blocking): worth-doing items 8-13 (--doctor mise/aqualayout mismatch, explicit 0755 on created dirs, usage-text gaps, deferred-until-verified summary line, non-recursive hooks/ cross-check) and the optional items (FdGuard/OwnedFd duplication, popen RAII, shimBinaryCheck's ok=1 on an empty selfPath, a CHANGELOG entry). Verified: full pargates suite (661 gates) clean modulo the same two pre-existing/environmental failures confirmed earlier this session (legendcoveragecheck.sh — red on unmodified main too; pargatescheck.sh — a timing probe that fails only under -j6 parallel load, passes alone); ASan+UBSan+LSan clean; deterministic; xmllint clean; --quality-delta gating=0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
aebda2c to
7f8089a
Compare
…gling-symlink prune, Codex --hook, Claude router, hermes-native skills) PR redhat-et#293's release matrix was red across all 24 shards on nine distinct gates — not one bug, but several unfinished slices of redhat-et#225's own plan: - pruneStale only removed entries the *previous manifest* tracked, so a stray/dangling ripwire-* symlink the manifest never recorded (a skill removed from disk by hand, or planted by an older installer) lingered forever. Now scans destDir directly for ripwire-* symlinks not in the current set, same ripwire-* scope-of-authority guard as before (I7). - `--codex --hook` was a literal "not implemented yet" stub. Ported skills/install.sh's pre-redhat-et#225 install_codex_hook: PreToolUse nudge + SessionStart primer + UserPromptSubmit router merged into ${CODEX_HOME:-~/.codex}/hooks.json in one jq call, matched by exact command (Codex has one install channel, unlike Claude's isScript basename match). - Claude's `--hook` never registered the UserPromptSubmit prompt router (install_claude_route was never ported), and the substitution-meter disclosure banner (D2) was missing from the C++ path entirely. - `--hermes` only ever linked the flat ripwire-* set; skills/hermes/ ripwire-*/ (Hermes-native content, e.g. ripwire-repo-map) was never collected, so a real shipped skill silently never installed. - runJqMergeFile redirected jq's own stderr to /dev/null, so codexinstallhonestycheck's "does jq's parse-error diagnostic reach stderr" assertion could never pass. test/routehookcheck.sh and test/codexpromptroutecheck.sh asserted an installed hook's *exact* checkout path (pre-redhat-et#225: hooks lived beside a git checkout). Under redhat-et#225 the binary always registers its own extracted store copy, so updated both to match by script basename — the same "identify by script, not location" convention skills/install.sh's own jqIsScript already used. hermesinstallcheck.sh arm 7 planted a decoy skill in a *copy* of skills/ to prove the Hermes-native loop is ripwire-*-scoped; a copy is no longer a complete installer once skills are embedded at build time, so it never actually ran. Replaced with a behavioural check against the real shipped ripwire-repo-map skill plus a source-level pin on the guard, the same "grep the invariant when no live repro is constructible" idiom routehookcheck.sh's own D9 arm uses. collectCurrentSkills extracted out of installForAgent to keep the latter's complexity from crossing the quality-delta gate; readManifestV2/ ManifestV2 deleted (dead now that pruneStale scans the directory instead). .ripwire_quality_acks covers the two remaining gating short-horizon-churn rows (installForAgent, pruneStale) with one reason — this same commit's own footprint, nothing foreign. Verified: full pargates suite (655 gates) clean modulo one pre-existing, unrelated failure (legendcoveragecheck.sh, confirmed red on unmodified HEAD too); ASan+UBSan+LSan clean; deterministic; xmllint clean; --quality-delta gating=0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…allback, atomic writes, ownership-checked prune, self-checks Working through the maintainer's review of redhat-et#293 item by item: 1. extractGroup no longer trusts the store directory's NAME (a content-hash key) alone — storeContentsMatch() lstats it as a real directory and byte-compares every embedded file against its store copy (O_NOFOLLOW, size then bytes; the reference is already resident in the binary, so this costs a read, not a hash). A mismatch — missing, a symlink planted at that name, a partial extraction a killed run left behind, tampering — re-extracts from scratch through the existing tmp+rename path. 2. homeDir() no longer falls back to "/tmp" when HOME is unset; it's empty, and every caller refuses instead of silently writing somewhere the operator never chose. agentHomeOr() centralizes the "${ENV_VAR:-$HOME/suffix}" shape (RIPWIRE_DATA_HOME, CLAUDE_CONFIG_DIR, CODEX_HOME) and VALIDATEs each is absolute. wrap.h::resolveSkillsRoot gets the same fix at its own "~/" expansion — home + "/.claude/skills" with an empty home IS "/.claude/skills", not a caller-visible failure, which the caller here (and wrap --all's own listing) depends on. 3. settings.json and the manifest are now written via rw::pathguard::createExclTempFile + commit (temp beside the target, rename over it) instead of truncate-in-place — a crash mid-write can no longer leave either file empty, and commit()'s rename replaces whatever sits at the final name, including a symlink, without following it. 4. pruneStale reads the previous manifest again (readManifestSkillNames, VALIDATEd against the same ripwire-* shape pruneStale's own directory scan enforces) and now requires EITHER manifest tracking OR pruneTargetIsOurs (dangling, or resolves under dataHome()/"skills") before unlinking an untracked name — a user's own symlink that merely happens to be named ripwire-<something> is left alone. A manifest-tracked name stays prunable regardless of its current target, which is what test/skillinstallcheck.sh's own link-safety arm already pins. 5. codexdoctor.h::skillsCheck's complexity (3->26, the review's own --quality-delta flag) is split: skillsCheckHint now owns the 3-way hint-message branching, skillsCheck is fact-gathering only. 6. envOr, byte-identical in codexdoctor.h and skillsinstall.h, moves to the new src/infra/envutil.h. 7. A few targeted self-checks per CONTRIBUTING §3: EXPECTS non-empty bytes in writeStoreFile (an embedded file is never empty by construction), VALIDATE on the manifest's own skill= line shape (external, hand-editable input). 8. The stray tracked planning doc (.superpowers/sdd/embedded-skills-implementation-plan/fix-wave-2-report.md) is removed; the rest of that directory was never tracked. test/skillsinstallcheck.sh's arm 17 updated: a fully read-only skill directory cannot support the atomic manifest write's temp+rename either (needs directory write, same as the symlink() the arm is actually testing) — the honest outcome is a reported manifest-write failure and an unchanged manifest, not a silently-wrong count, which was only ever achievable through the non-atomic write this round replaced on purpose. test/crashsweepcheck.sh's S2 registry gains storeContentsMatch's one guarded open, matching every neighbouring entry's shape. Not done from the review (tracked for a follow-up, not blocking): worth-doing items 8-13 (--doctor mise/aqualayout mismatch, explicit 0755 on created dirs, usage-text gaps, deferred-until-verified summary line, non-recursive hooks/ cross-check) and the optional items (FdGuard/OwnedFd duplication, popen RAII, shimBinaryCheck's ok=1 on an empty selfPath, a CHANGELOG entry). Verified: full pargates suite (661 gates) clean modulo the same two pre-existing/environmental failures confirmed earlier this session (legendcoveragecheck.sh — red on unmodified main too; pargatescheck.sh — a timing probe that fails only under -j6 parallel load, passes alone); ASan+UBSan+LSan clean; deterministic; xmllint clean; --quality-delta gating=0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ipwire skills install (redhat-et#225) The review round on redhat-et#293 correctly noted this repo keeps one CHANGELOG.md, not changelog.d/ fragments, and redhat-et#225's whole feature had no entry yet. Added one in the file's own voice, summarizing the embed step, ripwire skills install, --doctor's provenance reporting and the mise/aqua shim fix, with the new gates it names. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Status, so this is not waiting on us silently. This is not a 0.6.2 blocker — 0.6.2 is cut on the graph-completeness, byte and Windows work — and it is high priority for 0.6.3. The refreshed review above still lists six items, the load-bearing ones being: verify the store's contents rather than trusting its name, atomic writes for Two things changed underneath you while this sat, both worth a rebase rather than a surprise: the gate roster moved (several trains landed, including a new git-environment hermeticity gate and a centralised No rush from our side, and nothing here is a criticism of the direction — the design is the one agreed in #225 and the parity ports read as faithful. Ping us when you push and we will re-review promptly. |
This gate verifies that: - generated/embedded_skills.h exists after configure - all skills/ and hooks/ files appear in the header - the header is byte-identical across two configures (determinism) - CMAKE_CONFIGURE_DEPENDS triggers on skill file changes Expected to fail until the CMake embed step is implemented. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…mbed step" This reverts commit 490cc6947af46bb6e84d305a52f3c9a7cc863489.
…ls verification Adds four new arms to the existing selfcontainedcheck.sh gate, following the established pattern for embedded_queries.h: 1. Source-tree path check: verify binary contains no literal skills/ or hooks/ paths 2. Compiled probe + cross-check: C++ probe reads embedded_skills.h interfaces and validates against real source-tree files (byte-exact match, no missing/extra entries) 3. Isolated-binary test: verify 'ripwire skills install' doesn't fail with source-tree complaints (currently fails as expected for unimplemented subcommand) The gate currently fails on missing generated/embedded_skills.h (expected red state until CMake embed step is implemented in a later task). Once that task lands, this gate will automatically pass all arms. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…igure time
- adds a _ripwire_embedded_skills block after _ripwire_embedded_queries,
modeled on it, producing generated/embedded_skills.h with
rw::embedded_skills::{EmbeddedFile, kSkillFiles, kHookFiles, kVersion,
kHash8, kStoreKey}
- CONFIGURE_DEPENDS added directly to both file(GLOB_RECURSE...) and
file(GLOB...) calls so an added/removed skill or hook file triggers a
reconfigure, not just edits to already-tracked files
- diverges from the plan brief's literal snippet in two ways forced by
Task 1's selfcontainedcheck.sh gate (the actual contract):
* stored relative paths are prefixed with skills/ or hooks/ (the gate
compares against paths relative to $ROOT, not to skills/ or hooks/)
* skills/*.md at the top level (e.g. skills/CONSOLIDATION.md) is
filtered out — only files inside a skills/<name>/ subdirectory are
embedded, matching the gate's skills/*/ + rglob walk
also fixed \x1e (not a valid CMake string escape) to a record separator
built via string(ASCII 30 ...)
- registers embedded_skills.h in ripwirepubliccheck.sh's generated-header
allowlist
test/selfcontainedcheck.sh's new skills/hooks arms now pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review finding: the glob at _ripwire_skill_files only matches .md files, while test/selfcontainedcheck.sh's byte-exact cross-check walks every file under skills/*/ with no extension filter. They agree today only because every file under every skills/<name>/ subdirectory happens to be markdown. Add a comment directly on the glob line so a future contributor adding a non-.md companion file sees the coupling at the point that would break, not buried in a report file. Comment-only change; bash test/selfcontainedcheck.sh re-run unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…and link-safety Creates test/skillsinstallcheck.sh with three arms to verify the (not-yet-implemented) ripwire skills install subcommand: - Arm 1 (fresh install): verifies skills directory creation, .ripwire-manifest-v2 file with version=2 and source= fields, and symlinks to ripwire-* skills - Arm 2 (idempotency): confirms store directory is not re-extracted on a second run - Arm 3 (link-safety): ensures the command refuses to write through a pre-planted symlink at the destination Registers the gate in test/regression.sh's absorb loop (now 619 gates, up from 618). Expected state: gate fails at arm 1 with 'ripwire: unknown command skills' or similar, since the subcommand does not yet exist. Later tasks (4, 6, 8, 10, 12) will append additional arms to verify implementation details as they are built. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…killsinstallcheck - Ran docs/gatecount_build.py to update gate count from 618 to 619 in all marked sites (docs/EVALS.md, README.md, present/deck5_ripwire_build.js) - Added skillsinstallcheck.sh weight entry (2.2) to .github/pargates-shard-weights.json in correct alphabetical position Gate count now reflects the new test/skillsinstallcheck.sh gate added in previous commit. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Changed line 6 from hardcoded ./build/ripwire path to match the standard pattern:
ripwire="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}"
Added repo-relative path guard to convert relative paths to absolute by prefixing $ROOT,
following the pattern established by test/skillscanreadcheck.sh.
This ensures the gate respects RIPWIRE_BIN environment variable set by test/regression.sh
and test/pargates.py, enabling binary override for G1 ASAN verification and CI shard testing.
Verified: gate still correctly fails at arm 1 (subcommand not yet implemented).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…default dispatch Implements `ripwire skills install` per the task-4 brief, with three amendments folded in: bare "embedded_skills.h" include (the generated dir is already an include path), rw::pathguard::isSymlink/openNoFollowTruncate reused instead of ad hoc lstat/S_ISLNK checks, and every OS-differing call grouped into named helpers (symlinkOrRefuse, renameAtomic) ahead of src/infra/os.h landing. Two deviations from the brief's illustrative snippet, both required to make the already-committed gate (test/skillsinstallcheck.sh, task 3) pass: - The embedded relativePath carries CMake's "skills/" group prefix (skills/<name>/SKILL.md), so the brief's `*rel.begin()` would have collapsed every skill into one link literally named "skills". Fixed to read the second path component instead. - Arm 1 greps for a `.ripwire-manifest-v2` sidecar (version=2, source=) that the brief's runSkillsInstall snippet never writes. Added a minimal write of just those two fields, through pathguard's openNoFollowTruncate (this sidecar is rewritten on every install, unlike the O_EXCL store extraction). The richer manifest (per-agent entries, prune inventory) stays Task 8's scope. Also filters the link loop to the "ripwire-*" name scope skills/install.sh's own Claude-mode loop uses, so skills/hermes/ (Hermes-native, --hermes-only) is never linked under the bare "hermes" name into a Claude skills directory. test/skillsinstallcheck.sh: fixed a subshell-scoping bug in sandbox() — `d1="$( sandbox )"` forked a subshell, so sandbox()'s exports never reached the parent shell and every arm ran against the real $HOME/$CLAUDE_CONFIG_DIR instead of the sandbox. No arm's assertions changed, only the calling convention (`sandbox; d1="$d"`). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
skillsinstallcheck.sh's fail() prints and exits 1 directly at each arm, with no accumulator variable, so gateexitcheck.sh's sweep correctly flags it as unclassified without a FAILFAST pin. Forced a real failure (fake ripwire binary lacking the skills subcommand) and confirmed rc=1 before pinning it — per the file's own rule that every row is run, not read from the exit literal. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Four std::filesystem calls in src/skillsinstall.h used throwing overloads
instead of the error_code& overload the rest of the file already uses
(extractGroup's exists/create_directories calls), violating the plan's
"no exceptions, failures returned by value" constraint:
- writeStoreFile's create_directories( dest.parent_path() )
- linkOrRefuse's create_directories( destLink.parent_path() )
- runSkillsInstall's create_directories( skillsDest )
- the exists( symlink_status( destLink ) ) pair in runSkillsInstall's link loop
All four now take an error_code and turn a failure into an Outcome{false,...}
(the two Outcome-returning functions) or an emitted error + non-zero exit
(runSkillsInstall, which returns int).
symlink_status's error_code overload needed one more fix once switched over:
libc++ sets ec=ENOENT for a plain "nothing there" (the ordinary first-install
case), unlike the letter of the standard — confirmed empirically, not assumed
from a doc read. That specific errc is treated as "does not exist" and the
loop proceeds to link; any other ec is treated as a real failure and that
skill is skipped rather than risk writing under an unverifiable destination
state.
Verified: bash test/skillsinstallcheck.sh ./build/ripwire still passes arms
1-3, and a manual run still links exactly 17 ripwire-* skills.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ACTIVATE_OUT="$( ... )"; ACTIVATE_RC=$? is a bare assignment under set -eu: its exit status IS the substitution's, so a failing activation aborted the whole install with nothing printed — the exact silent-failure class this branch exists to close, and a regression from f56651c5's own M9 refactor (the prior code was if-guarded and never hit set -e). ACTIVATE_RC=0 plus || ACTIVATE_RC=$? keeps this in the exempt-from-errexit OR-list shape while still capturing the real code. Verified: a stub ripwire exiting 1 now degrades with the existing hint printed, install continues, rc captured. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…v1 name The binary has written .ripwire-manifest-v2 since the v2 manifest task landed earlier on this branch; these four sites (hermesinstallcheck.sh:61,185,188, skillinstallcheck.sh:49) never got updated. Two read against a v1 path that never exists, producing false reds (a wc -l of empty output read as '1 entries', masking the real 17-vs-16 Hermes-native gap behind a wrong number); two read a grep against a missing file that always vacuously passes, regardless of what the manifest actually contains. Same defect class as the sourceinstallcheck.sh/codexwrapcheck.sh quote/recipe misses fixed earlier in this fix wave — an assertion left pinned to a name the branch renamed. Verified: both gates now report real, disclosed-gap-consistent numbers, and the two previously-vacuous arms now assert against real manifest content. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…olution resolveMiseLayout/resolveAquaLayout used a range-for over directory_iterator, whose operator++ throws on a permission change or race mid-scan — exactly the shape hazardpatterncheck's rule C exists to catch, now enforced on this branch's own new code. Switched to the increment(ec) idiom liveSkills already uses elsewhere in this file, and factored the now-identical 'list one level of subdirectories, non-throwing' body the two functions shared into one listSubdirectories helper rather than duplicating the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e two S2 sites Upstream's os-header lane made every POSIX/libc call and header route through src/infra/os.h mandatory (osswitchcheck.sh rules B/F) after this branch had already been written against raw ::open/::write/::rename/::symlink/::lstat/ ::unlink/::popen/::pclose/::getpid, struct stat, mode_t and ssize_t — 18 violations, all in this one file, none of them noticed until the rebase onto current main surfaced the rule. Added os::symlink (the one wrapper os.h didn't already have; every other call site's wrapper already existed) and switched every site to it; dropped the now-redundant raw <fcntl.h>/ <sys/stat.h>/<sys/wait.h>/<unistd.h> includes (os.h already provides them transitively, same as pathguard.h's existing convention). crashsweepcheck's S2 still flags an os::-spelled popen/open site if it isn't registered — 'the seam's own spelling is not a free pass' — so registered runShellCapture's popen (no exit between popen and pclose) and writeStoreFile's open (the only early exit is the failed open; FdGuard closes it on every remaining path) in test/crashsweepcheck.sh's registry, matching every neighbouring entry's shape. .ripwire_quality_acks covers the resulting quality-delta footprint (11 findings, all this same refactor's own churn/duplication noise) with one consolidated reason. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gling-symlink prune, Codex --hook, Claude router, hermes-native skills) PR redhat-et#293's release matrix was red across all 24 shards on nine distinct gates — not one bug, but several unfinished slices of redhat-et#225's own plan: - pruneStale only removed entries the *previous manifest* tracked, so a stray/dangling ripwire-* symlink the manifest never recorded (a skill removed from disk by hand, or planted by an older installer) lingered forever. Now scans destDir directly for ripwire-* symlinks not in the current set, same ripwire-* scope-of-authority guard as before (I7). - `--codex --hook` was a literal "not implemented yet" stub. Ported skills/install.sh's pre-redhat-et#225 install_codex_hook: PreToolUse nudge + SessionStart primer + UserPromptSubmit router merged into ${CODEX_HOME:-~/.codex}/hooks.json in one jq call, matched by exact command (Codex has one install channel, unlike Claude's isScript basename match). - Claude's `--hook` never registered the UserPromptSubmit prompt router (install_claude_route was never ported), and the substitution-meter disclosure banner (D2) was missing from the C++ path entirely. - `--hermes` only ever linked the flat ripwire-* set; skills/hermes/ ripwire-*/ (Hermes-native content, e.g. ripwire-repo-map) was never collected, so a real shipped skill silently never installed. - runJqMergeFile redirected jq's own stderr to /dev/null, so codexinstallhonestycheck's "does jq's parse-error diagnostic reach stderr" assertion could never pass. test/routehookcheck.sh and test/codexpromptroutecheck.sh asserted an installed hook's *exact* checkout path (pre-redhat-et#225: hooks lived beside a git checkout). Under redhat-et#225 the binary always registers its own extracted store copy, so updated both to match by script basename — the same "identify by script, not location" convention skills/install.sh's own jqIsScript already used. hermesinstallcheck.sh arm 7 planted a decoy skill in a *copy* of skills/ to prove the Hermes-native loop is ripwire-*-scoped; a copy is no longer a complete installer once skills are embedded at build time, so it never actually ran. Replaced with a behavioural check against the real shipped ripwire-repo-map skill plus a source-level pin on the guard, the same "grep the invariant when no live repro is constructible" idiom routehookcheck.sh's own D9 arm uses. collectCurrentSkills extracted out of installForAgent to keep the latter's complexity from crossing the quality-delta gate; readManifestV2/ ManifestV2 deleted (dead now that pruneStale scans the directory instead). .ripwire_quality_acks covers the two remaining gating short-horizon-churn rows (installForAgent, pruneStale) with one reason — this same commit's own footprint, nothing foreign. Verified: full pargates suite (655 gates) clean modulo one pre-existing, unrelated failure (legendcoveragecheck.sh, confirmed red on unmodified HEAD too); ASan+UBSan+LSan clean; deterministic; xmllint clean; --quality-delta gating=0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hecked mtime on Linux `stat -f '%m' FILE 2>/dev/null || stat -c '%Y' FILE` assumes `stat -f` fails on GNU coreutils so the `-c` fallback fires. It doesn't: GNU's `-f` is a different, valid flag (filesystem stat, not BSD's format string), so it succeeds with a multi-line filesystem-info block instead of erroring. before=/after= were comparing that block's free-space/inode counters, not the store directory's mtime — stable enough to pass on an idle machine, but those counters legitimately drift under concurrent I/O from unrelated processes, so the string compare spuriously failed under real CI load (confirmed: passed 8/8 alone in a from-scratch Docker repro of the exact ubuntu-24.04/clang/ Release CI leg, then failed 10/10 under added CPU+I/O contention; the fix passes 10/10 under the identical contention). test/cachehashcheck.sh already documents this exact coreutils trap and the house fix (detect the flavour once via `stat --version`, use one form) — skillsinstallcheck.sh's arm 2 just predates that lesson. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…allback, atomic writes, ownership-checked prune, self-checks Working through the maintainer's review of redhat-et#293 item by item: 1. extractGroup no longer trusts the store directory's NAME (a content-hash key) alone — storeContentsMatch() lstats it as a real directory and byte-compares every embedded file against its store copy (O_NOFOLLOW, size then bytes; the reference is already resident in the binary, so this costs a read, not a hash). A mismatch — missing, a symlink planted at that name, a partial extraction a killed run left behind, tampering — re-extracts from scratch through the existing tmp+rename path. 2. homeDir() no longer falls back to "/tmp" when HOME is unset; it's empty, and every caller refuses instead of silently writing somewhere the operator never chose. agentHomeOr() centralizes the "${ENV_VAR:-$HOME/suffix}" shape (RIPWIRE_DATA_HOME, CLAUDE_CONFIG_DIR, CODEX_HOME) and VALIDATEs each is absolute. wrap.h::resolveSkillsRoot gets the same fix at its own "~/" expansion — home + "/.claude/skills" with an empty home IS "/.claude/skills", not a caller-visible failure, which the caller here (and wrap --all's own listing) depends on. 3. settings.json and the manifest are now written via rw::pathguard::createExclTempFile + commit (temp beside the target, rename over it) instead of truncate-in-place — a crash mid-write can no longer leave either file empty, and commit()'s rename replaces whatever sits at the final name, including a symlink, without following it. 4. pruneStale reads the previous manifest again (readManifestSkillNames, VALIDATEd against the same ripwire-* shape pruneStale's own directory scan enforces) and now requires EITHER manifest tracking OR pruneTargetIsOurs (dangling, or resolves under dataHome()/"skills") before unlinking an untracked name — a user's own symlink that merely happens to be named ripwire-<something> is left alone. A manifest-tracked name stays prunable regardless of its current target, which is what test/skillinstallcheck.sh's own link-safety arm already pins. 5. codexdoctor.h::skillsCheck's complexity (3->26, the review's own --quality-delta flag) is split: skillsCheckHint now owns the 3-way hint-message branching, skillsCheck is fact-gathering only. 6. envOr, byte-identical in codexdoctor.h and skillsinstall.h, moves to the new src/infra/envutil.h. 7. A few targeted self-checks per CONTRIBUTING §3: EXPECTS non-empty bytes in writeStoreFile (an embedded file is never empty by construction), VALIDATE on the manifest's own skill= line shape (external, hand-editable input). 8. The stray tracked planning doc (.superpowers/sdd/embedded-skills-implementation-plan/fix-wave-2-report.md) is removed; the rest of that directory was never tracked. test/skillsinstallcheck.sh's arm 17 updated: a fully read-only skill directory cannot support the atomic manifest write's temp+rename either (needs directory write, same as the symlink() the arm is actually testing) — the honest outcome is a reported manifest-write failure and an unchanged manifest, not a silently-wrong count, which was only ever achievable through the non-atomic write this round replaced on purpose. test/crashsweepcheck.sh's S2 registry gains storeContentsMatch's one guarded open, matching every neighbouring entry's shape. Not done from the review (tracked for a follow-up, not blocking): worth-doing items 8-13 (--doctor mise/aqualayout mismatch, explicit 0755 on created dirs, usage-text gaps, deferred-until-verified summary line, non-recursive hooks/ cross-check) and the optional items (FdGuard/OwnedFd duplication, popen RAII, shimBinaryCheck's ok=1 on an empty selfPath, a CHANGELOG entry). Verified: full pargates suite (661 gates) clean modulo the same two pre-existing/environmental failures confirmed earlier this session (legendcoveragecheck.sh — red on unmodified main too; pargatescheck.sh — a timing probe that fails only under -j6 parallel load, passes alone); ASan+UBSan+LSan clean; deterministic; xmllint clean; --quality-delta gating=0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CMakeLists.txt globbed hooks/*.sh with a plain GLOB — one directory level
only — while skills/*.md already used GLOB_RECURSE. A hooks/lib/x.sh would
silently never reach generated/embedded_skills.h, and
test/selfcontainedcheck.sh's own cross-check used the identically shallow
hooks_dir.glob('*.sh'), so it could never catch the gap either.
Switched the CMake glob to GLOB_RECURSE and the Python cross-check to
rglob(). New test/hooksembedrecursivecheck.sh configures a scratch git
worktree with a synthetic hooks/lib/x.sh and asserts it lands in the
generated header — exercising the real CMakeLists.txt call, not a
duplicated toy, without ever touching the real working tree.
Gate count 646->647 via docs/gatecount_build.py; pargates weight added.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lp mention (review item 11) Three related gaps, all closed the same way: bare `ripwire skills` fell through to parseArgs, which mapped the skills/ directory as a crawl root (exit 0, dumps the corpus map) since it happens to exist in this checkout; `skills install --help`/`-h` hit the unknown-flag branch and refused with exit 2; and top-level `--help` never mentioned `skills install` at all. Added printSkillsInstallHelp/printSkillsUsage (src/skillsinstall.h), wired bare `ripwire skills` in main.cpp to the latter, and added a usage-preamble line in cli.h next to the existing `ripwire wrap <agent>` one. Kept the preamble line free of bare lowercase --flag tokens (a first attempt with `--all` written out made flagsurfacecheck.sh's --help=all scrape report --all as advertised-but-unparsed — it's skills install's own flag, not a top-level one) by pointing at `skills install --help` for the flag catalog instead of enumerating it in the one-line summary. test/skillsinstallcheck.sh arms 19-20 pin both fixes; ack added for a spurious duplication finding (printSkillsInstallHelp's single emitRaw(literal) body structurally collides with printEvalRankerNote's — same idiom as the existing symlinkOrRefuse|extentsuspect.h::inSet ack). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tem 8) resolveMiseLayout hardcoded <version>/bin/ripwire — mise's real layout, confirmed against test/wrapverbscheck.sh's own fixtures, is <version>/<archive-name>/ripwire with no "bin/" at all. resolveAquaLayout's root was pkgs/redhat-et/ripwire, missing aqua's own github_release/github.com/ registry/host prefix, and only searched one level of nesting when a real aqua install nests two (<version>/<archive>.tar.gz/<archive>/ripwire). Every real install of either manager reported managed_unverified="1" — recognised as a shim, but the layout it pointed at never resolved. Replaced both hardcoded-shape functions with one resolveManagerVersionLayout that recursively searches (bounded depth 8, non-throwing) for a file named exactly "ripwire" under the single resolved version directory, requiring exactly one match — same ambiguous/no-candidate contract as before, but shape-agnostic, so it doesn't rot the next time either manager repackages its archives. New arms d/e in test/doctorstalecheck.sh pin both real layouts against a copied-binary fixture. Fixed a latent bug in the process: arms a/b/(now d/e)'s managed_unverified assertions grepped the WHOLE --doctor output, which always contains that string in the legend's own prose explaining the attribute — scoped all four to the claude-binary row specifically. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…comment (review items 9, 10) Item 9: create_directories() on POSIX creates 0777-masked-by-umask, so a permissive umask (000) left a freshly created store/skills/settings directory world-writable. createDirectoriesMode0755 now walks the path one level at a time (create_directories()'s own "created" bool only reports the LEAF of a multi-level create — extractGroup's tmp extraction directory, later renamed into place as the store root, is an INTERMEDIATE level of exactly that shape, and the first version of this fix missed it) and forces every level it actually creates to 0755. A pre-existing directory anywhere in the chain is left untouched. Item 10: fixed the stale comment claiming the hook-merge jq command redirects its own stderr to /dev/null (it doesn't — only the unrelated jqScriptAlreadyRegistered probe does; the merge's stderr is left to inherit the caller's, which codexinstallhonestycheck.sh already pins). Added resolveJqPath() (same PATH-walk shape as codexdoctor.h's resolveExecutable) and pinned both jq call sites to its resolved absolute path instead of a bare "jq" token left for /bin/sh to resolve. New arm 21 in skillsinstallcheck.sh proves 0755-under-umask-000 for both the skills destination and the store directory. New arms F/G in skillinstallcheck.sh prove --hook resolves jq via an actual PATH walk (a wrapper placed first on PATH is the one that runs) and fails cleanly, naming jq, when none is anywhere on PATH. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…computed (review item 3, optional)
An empty selfPath (this process's own executable path could not be
determined) made shimBinaryCheck report ok=1 same_file="0" — a
comparison that never ran, indistinguishable in the output from a real
comparison that found the files different-but-tolerated. Now reports
self_unverified="1" instead, with no same_file= attribute at all,
matching the "disclosed unknown, not a failure" shape the ambiguous
managed_unverified="1" branch two lines below it already uses. Legend
text in verbs_doctor.h documents the new attribute alongside it.
New test/shimselfunverifiedcheck.sh compiles codexdoctor.h standalone
(same pattern selfcontainedcheck.sh already uses for embedded_skills.h)
and calls shimBinaryCheck("") directly against a real single-candidate
mise fixture, sidestepping the OS-specific question of how
selfExecutablePath() itself ever comes back empty in a live run.
Gate count 647->648.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…guard the popen handle (review items 1, 2, optional) FdGuard was a byte-for-byte duplicate of rw::pathguard::OwnedFd (both already flagged by --quality-delta's duplication detector and acked as a coincidental token-shape collision in an earlier round). Its two call sites (writeStoreFile, storeContentsMatch) only ever use the local `fd` int directly, never the guard's own member, so swapping the type is a pure rename — no crashsweepcheck.sh S2 registry change needed, since S2's OWNERS pattern only recognises an open() call handed straight to an owner's constructor in the same expression, not this file's open-then-wrap-separately shape either way. runShellCapture's popen() was closed by a single unconditional pclose() at the end of the function with no early return between them today — safe by construction, but only for as long as nobody adds one. PopenGuard mirrors OwnedFd's shape (move-only, RAII) but closes via pclose(), never rw::OwnedFile's fclose() — using OwnedFile here would call the wrong close function for a forked child's stream. closeAndStatus() hands back pclose()'s raw status for the WIFEXITED/WEXITSTATUS decode the same way the destructor-only path could not. Left the ~15 other raw popen/pclose call sites elsewhere in the codebase (gitmine.h, quality.h, gitoracle.h, ...) untouched — introducing a new RAII convention in this one file while the dominant established pattern stays raw would be inconsistency, not a fix, and none of those sites was in scope for this review round. Verified: full pargates suite green on both build/ and a --clean-first asan/ rebuild (crashsweepcheck's S2 registry unchanged and still accounted-for), --quality-delta gating=0, deterministic, xmllint clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ipwire skills install (redhat-et#225) The review round on redhat-et#293 correctly noted this repo keeps one CHANGELOG.md, not changelog.d/ fragments, and redhat-et#225's whole feature had no entry yet. Added one in the file's own voice, summarizing the embed step, ripwire skills install, --doctor's provenance reporting and the mise/aqua shim fix, with the new gates it names. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…l final state The first pass under-covered the PR: missing --all/--force/--contributor, link-safety repair/prune, store-integrity verification, atomic writes, Hermes-native skills, the substitution-meter banner, and the usage-text fixes, and buried wrap's shell-escaping fix inside the skills-install paragraph instead of naming it as its own entry. Rewrote both to describe the shipped feature as it stands now, not the sequence of commits that got here, and split the wrap fix into its own ### Fixed entry since it hardens an existing, previously-shipped command rather than being new skills-install behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gateexitcheck/worktreeleakcheck findings Both new gates' ok() omitted the house `; return 0;` tail, so a failed printf inside it (a full disk, a closed fd) would report the ARM as a shell-level failure to a `cmd && ok ... || no ...` chain instead of recording it through the fail= accumulator — gateexitcheck.sh's (G1) check caught both. hooksembedrecursivecheck.sh also called `git worktree add --detach "$WT" HEAD` directly against the real repository — exactly the pattern test/worktreeleakcheck.sh's (A) scan exists to forbid (a killed run leaks a worktree registration into the shared .git that every other session then has to clean up by hand). Switched to ripwire_private_checkout (test/lib/headbinlib.sh), the one sanctioned way a gate checks out a commit — a private `git clone --shared --no-checkout` + `checkout --detach`, never a worktree in $ROOT/.git. Also added a real `ripwire` binary run against the checkout right after checking it out (best-effort, targeting $WT itself so it works whether the checkout is the real ripwire tree or worktreeleakcheck's own tiny fixture repo) — every other ripwire_private_checkout caller does this as its actual functional check, and worktreeleakcheck's kill-timing tests use exactly this invocation as their synchronization point for when to deliver a signal; without it, a scenario "never reaches a checkout of the commit" and proves nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…w gates hooksembedrecursivecheck.sh ran its sanity ripwire invocation against the checkout but discarded the exit status (`[ -x "$BIN" ] && "$BIN" ... >/dev/null 2>&1`), so it stayed green even with RIPWIRE_BIN pointed at a binary that fails on every call — binoverridecheck.sh's (4) exists specifically to catch this class. Now gates on the call: a missing binary exits 2 (matching the house convention), a failing one reports through no() and exits 1. shimselfunverifiedcheck.sh was flagged the same way, but its actual shape never executes $BIN at all — it only `cp`'s it as inert bytes into a fixture mise install path (the managed-candidate side of a byte comparison a different, non-empty-selfPath test would exercise, not this one). Added it to binoverridecheck.sh's EXEMPT and STATICALLY_UNREACHABLE sets with the same verified-by-reading rigor every other row there carries, rather than fabricating a dependency on the binary this gate was never meant to have. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…main's train git-spice upstack restack replayed this branch onto main after two integration trains landed (os.h seam refactor, GDScript language support, cross-TU layout-drift detection — 18 commits, 652 files). Two mechanical follow-ups, exactly as the maintainer's own review comment on skillsinstallcheck/hooksembedrecursivecheck/shimselfunverifiedcheck/ doctorstalecheck from this branch, resolved commit-by-commit during the rebase) pushed the count to 649 — regenerated via docs/gatecount_build.py; --help's byte-identity pin moved again since main's own trains changed --help text too — re-pinned with UPDATE_GOLDEN_EXPECT=help_all, confirming only that one label moved. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… with main's second train A second `git-spice repo sync` pulled 130 more commits (another integration train — clean-env.sh/statcompat.sh centralization, a Win32-logic gate, os.h hardening). Same two mechanical follow-ups as the first restack: gate-loop union pushed the count to 651, and --help's byte-identity pin moved again — re-pinned with UPDATE_GOLDEN_EXPECT= help_all, confirming only that one label moved. Verified this restack does not regress main's own env/stat-compat work: every conflict that touched clean-env.sh vs this branch's own unset-agent-env-variables.sh was resolved toward clean-env.sh (a strict superset), across 9 files. Nothing here shadows or duplicates statcompat.sh within the same file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…h's own duplicates This branch's own test/lib/unset-agent-env-variables.sh and two hand-rolled GNU/BSD stat blocks (test/skillsinstallcheck.sh) were independent, correct implementations of the exact fixes main's integration trains centralized as test/lib/clean-env.sh and test/lib/statcompat.sh. Migrated every remaining caller (agenttablecheck.sh, doctorstalecheck.sh, hermesinstallcheck.sh — which also had a genuinely redundant SECOND sourcing left over from before clean-env.sh existed on this branch's own timeline, selfcontainedcheck.sh, shimselfunverifiedcheck.sh, skillsinstallcheck.sh) to the shared helpers and deleted the now-dead unset-agent-env-variables.sh. clean-env.sh's agent-home list was missing OPENCLAW_STATE_DIR, which unset-agent-env-variables.sh carried as a "belt-and-braces" entry (openclaw honours no such var today, per src/wrap.h's own comment, but kept for when/if that changes). Rather than re-add a one-off unset in agenttablecheck.sh (the only migrated file that tests openclaw), added it to clean-env.sh itself — the right home for a var every agent-home caller should clear — and updated test/gitenvhermeticcheck.sh's own pinned variable-name list (arm C) to match, since that gate asserts clean-env.sh's unset lines name exactly that set. skillsinstallcheck.sh's two hand-rolled stat blocks are now statcompat.sh's mtime_of/mode_of, identical implementations, one sourced definition. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…x a duplicate-key ack entry
This branch's own commit that added os::symlink (routing skillsinstall.h's
raw ::symlink through the os.h seam) predates main's later Windows-hardening
train, which added test/osswitchcheck.sh's arm (E): every function os.h
declares on the POSIX side must have a matching declaration on the Windows
side. symlink never got one, so the restack surfaced it as a real (E)
failure — not present at the time that commit was written, but real now
that the check exists.
Declared symlink alongside its neighbours in os.h's Windows
declarations-only block, and gave it a body in os_win32.cpp: ENOSYS/-1,
the same shape os.h's own exepath() fallback already uses for "not on
this platform." A full CreateSymbolicLinkW implementation needs
privilege/developer-mode detection this port hasn't built, and
skillsinstall.h's own header comment already documents this exact gap
as deferred — declared and honestly unavailable, not silently wrong,
and not guessed at without a Windows toolchain to verify against.
Separately, the mid-rebase resolution of a genuine same-(kind,key) ack
collision in .ripwire_quality_acks (kept both lines) left the ledger in
a shape the tool's own canonical form doesn't produce — caught by
test/qackconcurrencycheck.sh's (6)/(7): re-running the tool merged them
into one line ("| prior: ...") on a run that should have left the
ledger untouched. Applied that exact canonical merge by hand.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7f8089a to
8a75531
Compare
|
Pushed — rebased onto current main (through train-15, kParserVer 119; picked up the MSVC cl.exe On the four load-bearing items from the last review round:
Also folded in since: usage text for bare Ready for re-review whenever you have a moment. Posted by Claude Code on behalf of @s0undt3ch |
…fore every os:: call CI's windows (cl, ninja) and windows (clang-cl, ninja) legs both failed: 6 call sites (renameAtomic, symlinkOrRefuse, writeStoreFile, storeContentsMatch x2, pruneStale) passed a std::filesystem::path's own .c_str() straight to os::rename/symlink/open/lstat/unlink, all declared const char*. On Windows, std::filesystem::path::value_type is wchar_t, so path.c_str() is const wchar_t* — a real type mismatch, not a warning, and cl/clang-cl both refused to compile it. This is the same defect for all 6, and it never surfaced before because os::symlink had no Windows declaration at all until the parity fix earlier in this branch — the file was never actually type-checked for Windows until that declaration existed to type-check against. Every other os::-calling site in the tree already converts through .string().c_str() first (src/pathguard.h's isSymlink/commit take std::string precisely so callers do this once at the boundary; src/editpreview.h and src/main.cpp already use this exact spelling). skillsinstall.h's own os:: call sites now match: from.string().c_str() instead of from.c_str(), at all 6 flagged sites. The 3 remaining .c_str() calls in this file (runShellCapture's cmd, resolveJqPath's candidate, runCommandCapture's cmd) are already std::string, not std::filesystem::path — verified by reading each declaration, not guessed at. Not compile-verified locally (no Windows toolchain in this environment) — matched against both CI legs' full error output, which name exactly these 6 sites and no others, so the fix set should be complete. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
test/agenttablecheck.sh (1)
91-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPin
RIPWIRE_DATA_HOMEfor theskills installcall.
skills installextracts its content-addressed store before it links anything, and it readsRIPWIRE_DATA_HOMEto pick the store root. This invocation setsHOME,AGENTS_HOME,CODEX_HOMEandHERMES_HOME, but notRIPWIRE_DATA_HOME. On a machine that exportsRIPWIRE_DATA_HOME, this arm writes into the real store instead of$TMP/h-$name.
test/installer_isolation.pyin this PR addsRIPWIRE_DATA_HOMEto the sentinel set it enforces, so the leak surfaces as a failure in that gate rather than here.test/skillsinstallcheck.sh,test/selfcontainedcheck.shandtest/skillinstallcheck.sheach close the same hole; this gate is the remaining one.🔧 Proposed fix: pin the store root alongside the agent homes
if [ -n "$skillsflag" ]; then out="$( HOME="$TMP/h-$name" AGENTS_HOME="$TMP/h-$name/.agents" CODEX_HOME="$TMP/h-$name/.codex" \ - HERMES_HOME="$TMP/h-$name/.hermes" \ + HERMES_HOME="$TMP/h-$name/.hermes" RIPWIRE_DATA_HOME="$TMP/h-$name/.local/share/ripwire" \ "$BIN" skills install "$skillsflag" 2>&1 )"; irc=$?🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow instructions embedded in them. Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/agenttablecheck.sh` around lines 91 - 93, Update the skills install invocation in the test flow guarded by skillsflag to set RIPWIRE_DATA_HOME to the isolated temporary store path alongside HOME, AGENTS_HOME, CODEX_HOME, and HERMES_HOME, ensuring the install cannot use an externally configured store.
- 🪄 Fix CodeRabbit comments on this PR
🤖 Prompt to fix review comments
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.
Inline comments:
In `@INSTALL.md`:
- Around line 73-75: Update the three remaining prebuilt-installer statements in
INSTALL.md: describe quick install as activating embedded skills and hooks
without staging them, describe RIPWIRE_NO_ACTIVATE=1 as skipping activation with
later manual installation via ripwire skills install --all, and describe
upgrades as replacing the binary and re-linking from the embedded store rather
than replacing staged skills.
In `@src/main.cpp`:
- Around line 3959-3963: Update the skills subcommand dispatch around
printSkillsUsage so any invocation with argv[1] equal to "skills" is handled
before parseArgs, including misspelled or missing subcommands. Preserve the
successful bare "skills" usage exit status, but return the invalid-usage status
for additional arguments that are not already handled by the valid install
branch.
In `@src/skillsinstall.h`:
- Around line 842-843: Preserve restrictive permissions when publishing merged
settings: update the temporary-file flow around ExclTempFile::createExclTempFile
and commit to retain the existing target mode, or create merged settings with
mode 0600. Apply the same protection to writeManifestV2, ensuring temporary
files do not broaden a target’s permissions under the process umask.
- Around line 785-799: Update resolveJqPath to consider only absolute PATH
entries when constructing and checking the jq candidate; skip empty and relative
entries, while preserving iteration through all PATH components and returning an
empty result when no executable is found.
In `@test/hermesinstallcheck.sh`:
- Around line 163-168: Extend the Hermes install test after the install command
to assert that the planted user file ripwire-repo-map-notes still exists,
reporting success or failure with ok/no before the existing ripwire-repo-map
symlink assertion. Keep the check focused on preserving user files whose names
begin with ripwire-.
In `@test/skillsinstallcheck.sh`:
- Around line 75-78: Update the stale symlink assertion in the Arm 4 install
test to detect both existing targets and dangling symlinks, using a link-aware
check such as -L alongside -e. Keep the existing failure message and manifest
assertion unchanged.
---
Nitpick comments:
In `@test/agenttablecheck.sh`:
- Around line 91-93: Update the skills install invocation in the test flow
guarded by skillsflag to set RIPWIRE_DATA_HOME to the isolated temporary store
path alongside HOME, AGENTS_HOME, CODEX_HOME, and HERMES_HOME, ensuring the
install cannot use an externally configured store.
After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli?utm_source=ghpr
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: redhat-et/ripwire/.coderabbit.yaml
Review profile: CHILL
Plan: Advanced
Run ID: aec10aee-8c43-4e2b-90d9-0b0b57aa45fa
⛔ Files ignored due to path filters (1)
test/printf_parity.manifestis excluded by!test/printf_parity.manifest
📒 Files selected for processing (46)
.github/pargates-shard-weights.json.ripwire_quality_acksCHANGELOG.mdCMakeLists.txtINSTALL.mdREADME.mddocs/EVALS.mdpresent/deck5_ripwire_build.jsscripts/install.shscripts/verify-agent-integration.shskills/install.shsrc/cli.hsrc/codexdoctor.hsrc/infra/envutil.hsrc/infra/os.hsrc/infra/os_win32.cppsrc/main.cppsrc/skillsinstall.hsrc/verbs_doctor.hsrc/wrap.htest/agenttablecheck.shtest/binoverridecheck.shtest/claudeconfigdircheck.shtest/codexdoctorcheck.shtest/codexpromptroutecheck.shtest/codexwrapcheck.shtest/crashsweepcheck.shtest/deckcheck_allowlist.txttest/doctorstalecheck.shtest/gateexitcheck.shtest/gitenvhermeticcheck.shtest/hermesinstallcheck.shtest/hookcheck.shtest/hooksembedrecursivecheck.shtest/installer_isolation.pytest/lib/clean-env.shtest/regression.shtest/releaseinstallcheck.shtest/ripwirepubliccheck.shtest/routehookcheck.shtest/selfcontainedcheck.shtest/shimselfunverifiedcheck.shtest/skillinstallcheck.shtest/skillsinstallcheck.shtest/sourceinstallcheck.shtest/wrapverbscheck.sh
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.
…installer Three spots in INSTALL.md still described the prebuilt/curl installer (scripts/install.sh) as staging skills/hooks on disk. Since the embedded- skills work landed, that installer activates them straight out of the binary's own embedded store — nothing staged. Only the source-build ./install.sh route (lines 70-75, left untouched) still genuinely stages skills/ and hooks/ under <prefix>/share/ripwire/. CodeRabbit review, PR redhat-et#293. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rom review - main.cpp: `ripwire skills instal` (argc==3) fell through the old argc==2-only bare-subcommand guard and reached parseArgs, which took "skills" and "instal" as two crawl roots — silently mapping skills/ as a repo instead of naming the misspelling. Widened the guard to any argv[1]=="skills" with an unrecognized argv[2]. Arm 22 in skillsinstallcheck.sh proves it red-then-green. - resolveJqPath: only an absolute PATH entry is now considered. An empty or relative entry used to resolve to the current directory (matching sh's own PATH semantics, same as os::which/resolveExecutable) — correct for "what would a shell run", wrong here, since the result is spliced into a shell command string that `--hook` can run inside an untrusted repository (CWE-426). Arm (H) in skillinstallcheck.sh plants a jq in an empty/relative PATH entry and proves it never runs. - writeManifestV2 / runJqMergeFile: a temp-then-rename over an existing manifest or hook config no longer silently loosens its mode to 0666 & ~umask. Both now `os::stat` the original first and `os::fchmod` the temp file to its exact bits before the atomic commit, same idiom as mcpedit.h::atomicWrite (CWE-732). A first-ever file keeps the umask default. Arm 23 in skillsinstallcheck.sh and arm (I) in skillinstallcheck.sh cover the manifest and the settings.json paths. - skillsinstallcheck.sh arm 4: the prune assertion checked `-e` on a symlink whose target is intentionally dangling, which always follows to false — vacuous. Fixed to `-L`. CodeRabbit review, PR redhat-et#293. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…stall The Hermes install test planted a user file named ripwire-repo-map-notes to prove the installer's prune step only removes symlinks it owns, not real files that merely share the ripwire- prefix, but never asserted the file was still there afterwards — the scenario ran and proved nothing. CodeRabbit review, PR redhat-et#293. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Implements #225: skills and hooks are now embedded into the
ripwirebinary at configure time (a generated, content-addressed header) instead of being staged as separate files a curl/source install had to copy around. Activation is now one command,ripwire skills install, which extracts the embedded set into a versioned, immutable-by-hash store and symlinks from there into each agent's discovery directory (Claude Code, Codex, Codex-legacy, Hermes, openclaw).--doctorreports install provenance (declared vs. live, manifest staleness against the binary's own embedded version) instead of ever calling a version-manager shim STALE by mtime.This collapses two previously-separate mechanisms (
skills/install.sh's checkout-symlink installer, and the curl installer's$prefix/share/ripwire/{skills,hooks}staging step) into one binary-owned path, per the design already reviewed and approved by the maintainers in the #225 discussion.What changed
CMakeLists.txtglobsskills/andhooks/at configure time intogenerated/embedded_skills.h(byte-identical across two configures,CONFIGURE_DEPENDSon both the glob and each file, gated bytest/selfcontainedcheck.sh).ripwire skills install: extracts into$RIPWIRE_DATA_HOME/{skills,hooks}/<version>-<hash8>/(O_CREAT|O_EXCL|O_NOFOLLOW, atomic rename into place, immutable by content hash — a killed run can never leave a directory that reads back as "already extracted"), then symlinks into the destination agent's skill directory. Refuses to write through or replace a planted symlink or real file/directory even under--force; a dangling or foreign-but-live symlink is repaired; a link-loop failure is now counted and reported, never silent.--hookmerge: portedskills/install.sh's pre-wrap/skills install has no discovery path for binaries installed via mise/aqua-registry (or any channel outside the curl installer's layout) #225install_codex_hook— PreToolUse nudge + SessionStart primer + UserPromptSubmit router merged into${CODEX_HOME:-~/.codex}/hooks.jsonin onejqcall (Codex has one install channel, matched by exact command, unlike Claude'sisScriptbasename match).--hook: now also registers the UserPromptSubmit prompt router (install_claude_route, previously never ported to the C++ path) and emits the substitution-meter disclosure banner.--hermes: now collectsskills/hermes/ripwire-*/(Hermes-native content, e.g.ripwire-repo-map) in addition to the flatripwire-*set, which it previously linked exclusively.pruneStale: no longer limited to entries the previous manifest tracked — it also scans the destination directory directly forripwire-*symlinks outside the current set (a stray or dangling one, left behind by a hand-removed skill or an older installer), while still refusing anything outside itsripwire-*scope of authority./tmpfallback: an unsetHOMEis now refused everywhere instead of silently redirecting writes to/tmp;settings.jsonand the manifest are written via temp-file-plus-atomic-rename instead of truncate-in-place, so a crash mid-write can no longer leave either empty.--doctor: skills rows carrynot_installed=,stale=(manifest source vs. the binary's own embedded version), and a repair hint that names what's actually blocking (an untracked entry, not a command that can't fix it). The top-level binary-path row and the agent-specific rows both recognise a mise/aqua shim by location and compare byte-for-byte instead of by mtime.wrap: the printedskills installrecipe line is shell-escaped (rw::shSingleQuote) — a resolved binary path containing$(...)or a backtick is now inert when pasted, not executable.INSTALL.md,README.md) and the release/source installers (scripts/install.sh,install.sh) updated to match; the curl installer's separate$prefix/share/ripwire/skillsstaging step is gone, replaced by the sameripwire skills install --allcall the source build uses.Known, deliberately out of scope for this PR
Testing
Two full whole-branch adversarial review rounds (building the binary, running the full
pargatessuite, and driving hand-built fixtures against--doctor/skills installunder hostile environments — foreign symlinks, corrupt JSON, symlinked settings files, nojqon PATH, ambient env-var leaks) found and closed 3 Critical + several dozen Important/Minor findings across both rounds, including: an unrepairable v1→v2 manifest upgrade path, a manifest that recorded intent instead of outcome, an unescaped shell-injection vector in the printed recipe, two gates that were writing into the operator's real Codex/Hermes config during CI-style test runs, and several assertions that had silently stopped testing anything after an earlier refactor changed the output they grep for.Rebased onto current
mainfor this PR; the rebase surfaced (and this PR fixes) 18 raw-POSIX-call sites and one throwing-iterator use that predatedmain's portability lane, caught byosswitchcheck.sh/hazardpatterncheck.shonce replayed against it.The release matrix subsequently caught nine gates red across all 24 shards, tracing to several slices of #225's own plan left unfinished at that point (Codex
--hook, Claude's route registration, Hermes-native skills, prune's dangling-symlink scan) — all fixed and covered by gates in this PR. A further maintainer review round added store-integrity verification, removed the/tmpfallback, made the manifest/settings writes atomic, tightenedpruneStaleto an ownership check, and split an over-complexcodexdoctor.hfunction; a flaky idempotent-store test arm (comparingstat -f's GNU filesystem-info output instead of falling back tostat -cas intended) was also fixed.Full gate suite (661 gates) passes except two known, pre-existing/environmental flakes unrelated to this diff (
legendcoveragecheck.sh, red on unmodifiedmaintoo;pargatescheck.sh's timing probe, which only fails under-j6parallel load and passes standalone).Disclosure
This PR was developed with substantial assistance from Claude Code (Anthropic's AI coding agent), including implementation, the adversarial review rounds described above, and this description. All work was reviewed and directed by a human throughout.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
ripwire skills installto activate embedded skills and hooks for supported agents, with per-agent, contributor, hook, and all-agent options.Bug Fixes
Documentation