Skip to content

feat(boatstack): denials carry their computed solution set - #227

Merged
bigboateng merged 2 commits into
mainfrom
feat/denial-solution-set
Jul 27, 2026
Merged

feat(boatstack): denials carry their computed solution set#227
bigboateng merged 2 commits into
mainfrom
feat/denial-solution-set

Conversation

@bigboateng

Copy link
Copy Markdown
Contributor

What

Builds on #226. A denial that only states the rule leaves a weaker model retrying the same blocked call. Every guard denial now carries the law's computed solution set: a capped You can: list of exact runnable commands legal from the position the finding describes (owed human inputs marked <REQUIRED>), plus the full set on the opt-in structured payload (boatstackDenial.options, additive, schema_version stays 1). Protected-path denials additionally name the path's declared owner verbs, derived at runtime from the state-ownership map.

How

  • denial_solutions.go: enumerateDenialSolutions(repo, host, finding) by category family — phase findings re-enter the planning enumeration at the finding's own stage; operation findings get observation-only picks (observed-effect discipline); filesystem-destruction names the sanctioned actuator (workspace-reap, confirmation owed); tamper findings get observers + diagnose-hook --host <host> and tamperOwnerVerbs from StateRegistry() (a verb whose full arguments we cannot derive is named, never fabricated into a command). Reads only finding fields + declared tables: the deny path stays fast and cannot fail on unreadable state.
  • denial.go: Denial.Options/OptionsTruncated/OwnerVerbs; You can: section in all three render modes between reassurance and hint; Structured() additive options/owner_verbs keys; denialWithOptions composes the pure denialFor with the enumeration.
  • safety.go: host deny contracts take the resolved repo; tamper findings carry the matched managed-path fragment as AttemptedPath (bounded, secret-free — same convention as phase-bypass).

Boundary conformance

  • Boundary: every host deny surface (claude/codex/cursor/gemini hook decisions) → the guard's declarations (planning tables, delivery registry, state-ownership map).
  • Control laws: solution-set-derives-from-guard-declarations, guard-never-prescribes-what-it-would-deny; ownership derivation under every-managed-path-has-a-declared-owner.
  • Required evidence: denial_solutions_conformance_test.go — category totality sweep (every denial category enumerates or is a documented exception with reason), text-guard closure over every pick, phase-bypass picks re-checked against the interlock at the finding's stage, per-subtree ownership derivation pinned, rendering in all 3 modes + structured shape. Dual-reward corpus gains routine picks from real enumerations.
  • Failure behavior: exceptions (malformed-tool-input, unsupported-host, unresolved-repository) render exactly as before; empty repo ⇒ repo-independent picks only.
  • Classification: optimization — no admissibility change anywhere; the constitutional corpus floor is untouched by construction (verified). During authoring the corpus caught one stage-inconsistent pick (record-approval offered at NOT_STARTED) — exactly the defect class this law exists to stop.

Verification

  • go build ./... && go vet ./... clean; full go test ./... green across all three packages.
  • render-denial --demo --mode plain|ansi|markdown shows the new sections.
  • Release note: 2026-07-27-denials-name-their-solutions.md; preflight passes.

A denial that only states the rule leaves a weaker model retrying the same
blocked call. Every guard denial now carries the law's computed solution
set: a capped "You can:" list of exact runnable commands legal from the
position the finding describes, with owed human inputs marked, plus the
full set on the opt-in structured payload (additive keys, schema_version
stays 1). A protected-path denial additionally names the verbs that own
the attempted path, derived at runtime from the state-ownership map — a
verb whose full arguments cannot be derived is named, never fabricated
into a command.

Enumeration reads only the finding's own fields and the declared tables
(the planning enumeration for phase findings, the registry's observe rows,
the ownership map for protected paths), so the deny path stays fast and
cannot itself fail on unreadable state. Tamper findings now carry the
matched managed-path fragment as AttemptedPath (bounded, secret-free) to
key the ownership lookup; the host deny contracts take the resolved repo.

Conformance: a denial-category totality sweep (every category enumerates
picks or sits on a documented exception list), text-guard closure over
every pick, phase-bypass picks re-checked against the interlock at the
finding's own stage, ownership derivation pinned per subtree, rendering
checks in all three modes, and dual-reward corpus routine additions from
real enumerations — the constitutional floor is untouched by construction.
The corpus immediately caught one stage-inconsistent pick during authoring
(record-approval offered at NOT_STARTED), which is exactly the class this
law exists to stop.

control-law: solution-set-derives-from-guard-declarations
control-law: guard-never-prescribes-what-it-would-deny

Disclosure-Reviewed: reviewed — public-safe only, private facet kept out of this commit
The projected distribution runs the Go tests outside any Git repository;
owner-verb derivation resolves per-worktree sample paths, which needs a
real Git directory. The tamper rendering check now uses the git-backed
fixture like its sibling ownership test, instead of the package directory.

Disclosure-Reviewed: reviewed — public-safe only, private facet kept out of this commit
@bigboateng
bigboateng merged commit 5e9cd97 into main Jul 27, 2026
12 checks passed
@bigboateng
bigboateng deleted the feat/denial-solution-set branch July 27, 2026 21:48
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