Skip to content

fix: unblock fresh installs, sign the trial stamp, one honest guard count (v0.4.6) - #41

Open
FvdHMBAI wants to merge 5 commits into
mainfrom
fix/installer-trial-honest-numbers
Open

fix: unblock fresh installs, sign the trial stamp, one honest guard count (v0.4.6)#41
FvdHMBAI wants to merge 5 commits into
mainfrom
fix/installer-trial-honest-numbers

Conversation

@FvdHMBAI

Copy link
Copy Markdown
Owner

Fixes the three GuardRail items from the pricing audit
(/opt/obsidian-vault/03-Produkte/2026-08-16_angebots-pricing-audit.md, section 3).

Base is main because origin/develop was deleted after PR #40 and feature
branches in this repo PR straight to main (see #33-#36).

1. Fresh installs blocked every file edit (the expensive one)

pre-edit.sh loads edit_path_guard.sh and edit_secret_guard.sh as required,
fail-closed guards. CORE_GUARDS in install.sh never copied them. After
npx guardrail-agent init, every Write/Edit was denied with
GUARDRAIL INTEGRITY ERROR: required guard edit_path_guard.sh is missing.

Reproduced before the fix, on main. It hit 100% of new installs, so the whole
top of the funnel.

Three changes so it cannot recur silently:

  • CORE_GUARDS covers all 13 guards the dispatchers source, and the count
    check derives from the list instead of a hardcoded -ne 11.
  • install.sh now probes the pre-edit dispatcher before swapping the
    installation, not only pre-bash. The Bash-only probe reported success while
    every file write was broken. Custom guards are excluded from the probe so a
    user's own guard cannot fail their upgrade.
  • installer-adversarial.sh asserts that a fresh install allows a harmless
    edit, still denies a dangerous one, and that every guard any dispatcher
    sources exists in the installed tree.

Verified: putting the old CORE_GUARDS list back makes the new test fail.

2. Trial

The /api/guards/trial route the audit reported as missing already exists on
the live server
and works. It was added on 15/16 August, after the copy the
audit was written against. Verified against
license.guardrail.promptandbuild.de: the route serves the tarball and
rate-limits one download per IP per 24h.

What was still open, and is fixed here on the client side:

.trial-started held a bare unix timestamp, so date +%s > .trial-started
renewed the 14-day trial forever. The disable token in the same product is
HMAC-signed; the trial was not. The stamp is now <ts> <hmac32>, signed against
~/.guardrail/disable.key with the same construction as the disable token.
Unsigned, forged, replayed and foreign-key stamps all resolve to "no trial"
instead of a fresh one. Same trust model as .disabled: it stops the agent and
accidental resets, not the machine owner.

_guardrail_trial_days_left also existed twice, in bin/guardrail.sh and in
lib/guardrail-license.sh. The CLI now sources the library, so the CLI and the
dispatchers cannot disagree about whether a trial is valid.

3. One honest guard count

The README advertised 172 guards, a 96% enforcement rate and 1,048 autonomous
tasks. Those are numbers from the private system the patterns came from, not
from what a buyer installs. Six different counts were in circulation: 10
(docs/guard-classification.md), 11 (install.sh), 18/19 (README), 20 (npm),
22 (package.json), 172 (README prose).

The shipped product is 13 free guards + 48 Pro guards = 61. That is now the
only number, in README, package.json, install.sh, the CLI, the landing page,
the launch copy and docs/guard-classification.md. It is also what
guardrail status prints, so a reader can check it.

Note this is 13, not the 11 the audit assumed: the installer fix moves the two
edit guards into the free set.

The README guard table also listed nine guards that no dispatcher loads
(force_push_guard, deploy_branch_guard, large_diff_guard,
credential_leak_guard, wandering_detector, self_correction_loop,
tool_call_budget_guard, context_window_guard, uncommitted_code_guard).
They ship in the npm tarball and have tests, but they never run after an
install. They are now named separately rather than counted. Decision needed:
wire them into the dispatchers, or drop them.

4. Version drift

package.json 0.4.3, bin/guardrail.sh 0.3.3, install.sh banner 0.3.1, npm
live 0.4.5. All four now say 0.4.6.

Tests

npm test went from 147 to 230 assertions, 0 failures.

tests/pre-edit.sh (29) and tests/new-guards.sh (43) existed but neither
npm test nor CI ran them. Both are wired in now, plus the exec bit
new-guards.sh was missing. New: tests/trial-stamp.sh (8) covers the tamper
paths on the trial stamp.

End-to-end, against a sandboxed instance of the patched license API:

1. fresh install             -> 13 core guards, enforcement verified
2. guardrail upgrade --trial -> 48 Pro guards installed
3. trial stamp on disk       -> 2 fields (timestamp + signature)
4. guardrail status          -> 13 core, 48 pro, trial: 14 days left
5. date +%s > .trial-started -> "trial expired", not a fresh 14 days

Not in this PR

npm publish. There is no documented or automated publish flow in the repo
(CI only runs tests). Frederik has to publish 0.4.6 manually after merge.

License API. guardrail-license-api is not under version control on either
server, so it cannot go through a PR. The container was not redeployed. Patches
are staged inert on the Forma server at
/home/developer/guardrail-license-api/.proposed/ with a CHANGES.diff, and
cover: trial marking on the response, webhook branching by Stripe
price/product with an amount_total check instead of session.mode alone, and
a campaign column missing from initDb. Cutover steps are in the report.

Frederik added 5 commits August 17, 2026 00:20
pre-edit.sh loads edit_path_guard.sh and edit_secret_guard.sh as required,
fail-closed guards, but CORE_GUARDS never copied them. After a fresh
npx guardrail-agent init every Write/Edit was denied with
"GUARDRAIL INTEGRITY ERROR: required guard edit_path_guard.sh is missing."

Three changes so this class of bug cannot ship again:
- CORE_GUARDS covers all 13 guards the dispatchers source; the count check
  derives from the list instead of a hardcoded 11.
- install.sh probes the pre-edit dispatcher before swapping the installation,
  not just pre-bash. Custom guards are excluded from that probe so a user's
  own guard cannot fail an upgrade.
- installer-adversarial.sh asserts a fresh install allows a harmless edit,
  still denies a dangerous one, and that every guard a dispatcher sources
  exists in the installed tree.

Verified: reintroducing the old CORE_GUARDS list makes the new test fail.
.trial-started held a bare unix timestamp, so `date +%s > .trial-started`
renewed the 14-day Pro trial indefinitely. The disable token in the same
product is HMAC-signed; the trial was not.

- The stamp is now "<ts> <hmac32>", signed against ~/.guardrail/disable.key
  with the same construction as the disable token. Unsigned, forged, replayed
  and foreign-key stamps all resolve to "no trial" instead of a fresh one.
- _guardrail_trial_days_left existed twice, in bin/guardrail.sh and in
  lib/guardrail-license.sh. The CLI now sources the library so the CLI and the
  dispatchers cannot disagree about whether a trial is valid.
- tests/trial-stamp.sh covers the five tamper paths.
- tests/pre-edit.sh and tests/new-guards.sh existed but neither npm test nor
  CI ran them. Wired in, plus the exec bit new-guards.sh was missing.

Same trust model as .disabled: this stops the agent and accidental resets,
not the human who owns the machine.

Suite: 230 assertions, 0 failures.
The README advertised 172 guards, a 96% enforcement rate and 1,048 autonomous
tasks. Those are numbers from the private system the patterns came from, not
from what a user installs. Five different guard counts were in circulation:
11 (install.sh), 18/19 (README), 20 (npm), 22 (package.json), 172 (README).

The shipped product is 13 free guards plus 48 Pro guards, 61 total. That is
now the only number, in README, package.json, install.sh, the CLI, the landing
page and the launch copy. It is also what `guardrail status` prints, so a
reader can check it.

The guard table listed nine guards that no dispatcher loads
(force_push_guard, deploy_branch_guard, large_diff_guard, credential_leak_guard,
wandering_detector, self_correction_loop, tool_call_budget_guard,
context_window_guard, uncommitted_code_guard). They ship and have tests but do
not run after an install, so they are named separately instead of counted.

Version drift resolved: package.json was 0.4.3, bin/guardrail.sh 0.3.3,
install.sh printed 0.3.1, npm live is 0.4.5. All four now say 0.4.6.
…dispatcher

The table stopped at 10 and never mentioned self_bypass_guard,
edit_path_guard or edit_secret_guard, and the dispatcher mapping had no
pre-edit row at all. That was the sixth guard count in circulation.
edit_path_guard used \`realpath -m -s\` to normalize . / .. / duplicate
slashes before matching. Those flags are GNU-only. On macOS the call fails,
the guard silently kept the raw path, and the self-bypass protection for the
hook registry could be stepped around with a leading "./":

  pre-edit: 27 passed, 2 failed
    FAIL: dotslash settings expected=deny actual=allow
    FAIL: doubleslash settings expected=deny actual=allow

package.json declares darwin as supported, so this was a fail-open on a
supported platform in the guard that protects .claude/settings.json.

Replaced with _guardrail_canonicalize_path in lib/guardrail-common.sh, written
for bash 3.2 (what macOS ships). Verified against realpath -m -s across 17
paths: every path-structure case is identical. The only divergence is that
realpath also resolves symlinks in the working directory, which is what -s was
meant to prevent, so the shell version is closer to the stated intent.

Surfaced by wiring tests/pre-edit.sh into npm test in the previous commit;
it had never run in CI.
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.

1 participant