fix(trust): enforce §3 invariant 3 — BoJ ignores X-Trust-Level from non-loopback (standards#98)#106
Merged
Merged
Conversation
…on-loopback (standards#98)
The http-capability-gateway BoJ contract §3 invariant 3 mandates:
> Any `X-Trust-Level` arriving from any other source MUST be ignored
> and treated as `untrusted`.
`BojRest.TrustPolicy.satisfies?/3` previously accepted
`X-Trust-Level: authenticated|internal` from any caller — the third
function clause matched regardless of `is_local`. A non-loopback caller
reaching BoJ's back-side bind (a §4 violation) could therefore claim any
trust class by setting a header. This is the BoJ-side half of the §3
defence-in-depth pair; the gateway-side strip + re-emit landed as
http-capability-gateway#11.
Adds an explicit `_required, _trust, false` clause between the `:public`
and `:authenticated` arms that rejects every non-`:public` exposure when
`is_local=false`. Plus the surrounding work that the test suite needs:
* `trust_policy_test.exs` — 2 existing assertions inverted (they
codified the buggy behaviour) + 2 new tests explicitly pinning the
§3 enforcement at the function level
* `router_test.exs` — 2 existing tests inverted (asserted non-loopback
+ X-Trust-Level passed, now assert 403)
* `phase_c_seam_test.exs` — 5 `@tag skip:` annotations removed; module
moduledoc updated to reflect the defect is now fixed (was a
"documented finding" module; now a live enforcement test module)
* `docs/integration/http-capability-gateway-boj-contract.md` —
"Implementation status" subsection under §3 noting that the
gateway-side (PR #11), BoJ-side (this commit), and operational (§4)
controls are all named with their landing context
* `lib/boj_rest/trust_policy.ex` — moduledoc updated with a new
"Phase A §3 invariant 3" subsection explaining the new clause's
semantics and rationale
`mix test test/{trust_policy,router,phase_c_seam}_test.exs` — 64/64
green. Two failures elsewhere in the suite (`catalog_test.exs:85` +
`catalog_properties_test.exs:73`, both about cartridge auth.method
values) are pre-existing baseline rot unrelated to this PR; cross-
checked against main.
Refs hyperpolymath/standards#98
Refs hyperpolymath/standards#91
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🔍 Hypatia Security ScanFindings: 31 issues detected
View findings[
{
"reason": "Stale AI session file -- delete",
"type": "stale",
"file": "GEMINI.md",
"action": "delete",
"rule_module": "root_hygiene",
"severity": "medium"
},
{
"reason": "Issue in quality.yml",
"type": "missing_workflow",
"file": "quality.yml",
"action": "create",
"rule_module": "workflow_audit",
"severity": "high"
},
{
"reason": "Issue in security-policy.yml",
"type": "missing_workflow",
"file": "security-policy.yml",
"action": "create",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Action hyperpolymath/standards/.github/workflows/governance-reusable.yml@main needs attention",
"type": "unpinned_action",
"file": "governance.yml",
"action": "pin_sha",
"rule_module": "workflow_audit",
"severity": "high"
},
{
"reason": "Python file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/boj-server/boj-server/.github/scripts/validate-eclexiaiser.py",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
},
{
"reason": "TypeScript file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/boj-server/boj-server/cartridges/sanctify-mcp/adapter/mod.ts",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
},
{
"reason": "TypeScript file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/boj-server/boj-server/cartridges/academic-workflow-mcp/adapter/mod.ts",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
},
{
"reason": "TypeScript file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/boj-server/boj-server/cartridges/fireflag-mcp/adapter/mod.ts",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
},
{
"reason": "TypeScript file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/boj-server/boj-server/cartridges/ephapax-mcp/adapter/mod.ts",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
},
{
"reason": "TypeScript file detected -- banned language",
"type": "banned_language_file",
"file": "/home/runner/work/boj-server/boj-server/cartridges/bofig-mcp/adapter/mod.ts",
"action": "flag",
"rule_module": "cicd_rules",
"severity": "critical"
}
]Powered by Hypatia Neurosymbolic CI/CD Intelligence |
2 tasks
hyperpolymath
added a commit
that referenced
this pull request
Jun 8, 2026
…ards#91 / #100) Refreshes docs/integration/hcg-tier2-rollout-runbook.md from v0.1 (draft, 2026-05-20, pre Phase-D) to v0.2 reflecting the current state of the single-lane channel rooted at standards#91: - §1.1 Phase D deliverables: tick D-1..D-3 + D-4 bootstrap with http-capability-gateway PR refs (#12 / #14 / #22 / #26 / #30) and the boj-server D-1 load-profile (#168) that joint-closed standards#99 on 2026-06-01. The one remaining open item is the owner-driven perf-rebaseline workflow dispatch + `_status: scaffold-placeholder -> active` flip; called out explicitly rather than left as a stale unchecked checkbox. - §1.4 BoJ-side prereqs: tick the three loopback-bind layers (#130 / #131 / #132), the Phase C TrustPolicy clause (#106), the NetworkPolicy (#173), and the SSE-route policy coverage (#165). The Trustfile `tier_2_gateway.status: PENDING` line stays intentionally unchecked - it's the §6.4 last-action target. - §1.5 Gateway-side prereqs: tick the new `container/gateway-deploy.k9.ncl` from http-capability-gateway#38 (2026-06-03), record what stays PLACEHOLDER until cerro-torre signing runs, and expand the smoke-test entry with the concrete allow/deny sequence boj-server#165 deferred. - Header banner: replace the stale "Phase D has merged the scaffold only" Phase-D-dependency note with a current-state summary, bump version 0.1 -> 0.2, date 2026-05-20 -> 2026-06-08. - CHANGELOG.md: Documentation entry under [Unreleased] summarising the refresh. No code, infrastructure, or runtime behaviour changes. The runbook is the operator-facing source of truth for what's gating the next Phase E owner action; the drift it had was making "what's still open" harder to read at a glance. Refs hyperpolymath/standards#91 Refs hyperpolymath/standards#100 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7 tasks
hyperpolymath
added a commit
that referenced
this pull request
Jun 21, 2026
## Summary Adds `scripts/hcg-spec-coverage-check.sh`: a static, source-only audit that asserts every HTTP route declared in `docs/specification/openapi.yaml` is covered by at least one rule in the HCG live Verb Governance Spec (`config/gateway-policy-boj.yaml`). Companion / complement to PR #228's `hcg-surface-drift-check.sh`. The two scripts bracket the contract §8 declared-surface invariant from both directions: | Script | Invariant | Catches | |---|---|---| | `hcg-surface-drift-check.sh` (#228) | wired (router.ex) ⊆ policy | policy lag behind wiring | | `hcg-spec-coverage-check.sh` (this PR) | declared (openapi.yaml) ⊆ policy | policy lag behind the spec | Contract §8 (`docs/integration/http-capability-gateway-boj-contract.md`) is explicit: "the Verb Governance Spec governs the **declared** surface (openapi.yaml), not only the currently-wired subset. Declared-but-unimplemented routes are still classified in the policy so that when the gnosis handler grows them they are governed from day one rather than silently exposed." The live policy header carries the cross-check statement (*"Surface source: docs/specification/openapi.yaml, cross-checked against elixir/lib/boj_rest/router.ex"*); PR #228 made the router half machine-checkable, this PR makes the openapi half machine-checkable. Together they make the entire §1.5 re-verification stamp executable. Without this check the risk is concrete: someone adds a new path to `openapi.yaml` without a corresponding policy rule. The surface-drift check does not catch it (the route is not yet wired in `router.ex`). The day the route is wired, the surface-drift gate fires — but by then the operator has to either (a) ship the wiring with a default-deny in production for a route that should be live or (b) hold the wiring PR until the policy catches up. Catching the gap at spec-edit time avoids both, with no procedural cost above running the existing CI gate. ### What the script does 1. Extracts `(verb, path-template)` tuples from the `paths:` section of `docs/specification/openapi.yaml` — path entries at exactly 2-space indent, HTTP operations (get/post/put/delete/patch/head/options) at exactly 4-space indent under each path. Other keys at 4-space indent (parameters/summary/description/tags/...) are metadata, not operations, and are skipped. 2. Extracts `(verb, path-pattern)` tuples from `config/gateway-policy-boj.yaml` using the identical extraction block that `hcg-surface-drift-check.sh` uses, so the two scripts cannot drift in how they read the policy. 3. For each declared route, concretises `{name}`-style placeholders with a known probe segment (`probe`, shared with the smoke + surface-drift scripts so a future regex tightening fails all three in lock-step) and asserts at least one policy rule covers it: literal equality for non-regex paths; ERE `grep -E` match against the concrete URL for `^…` regex paths. The declared verb must be in the policy rule's verb list. 4. Exit `0` on no gap, `1` on gap detected, `64` on bad usage. ### What this PR does NOT do - Does **not** modify the rollout runbook §1.5 or the contract §8. Adoption as the §1.5 declared-surface check is a separate, owner-driven PR; this PR lands the artefact only so the runbook update is a one-line wiring change. Matches the §228-then-runbook split. - Does **not** wire the script into CI. Boj-server's CI discipline (`docs/wikis/CI-and-Required-Checks.adoc` / `.claude/CLAUDE.md`) requires path-filtered required checks to use the "always-trigger + changes job" pattern; a CI wiring PR should follow that pattern, matching the #228 → #229 split. Out of scope here. - Does **not** modify the openapi.yaml or the policy. On this branch the script reports OK against today's surface — every one of the 26 `(verb, path)` pairs declared in openapi.yaml has a matching rule among the 28 `(verb, path)` rules in the live policy. The 2-rule surplus is the policy's coverage of routes the openapi.yaml does not declare (notably `/.well-known/boj-node-pubkey`, which the router wires but the spec does not yet enumerate); the script intentionally does not penalise that direction — see the script's `Limitations` header. - Does **not** pre-empt the §6.4 Trustfile flip (`tier_2_gateway.status` stays `PENDING`). - Per single-lane HCG channel discipline (pattern set in `http-capability-gateway` PRs #10, #11, #12, #14, #22, #26, #30, #38 and `boj-server` PRs #78, #90, #106, #168, #173, #207, #208, #210, #215, #222, #224, #226, #228, #229): joint-close is owner-only. **This PR refs but does not close `standards#100`.** ### Channel state note This session could not read `hyperpolymath/standards#91` / `#100` (the session's repository scope is restricted to `http-capability-gateway` and `boj-server`), so the brief's instructed status comment on `standards#91` could not be posted. State was reconstructed from the canonical sources in this repo (ADR-0004, the integration plan, the audit, the rollout runbook, the live policy, the openapi spec, and the merged-PR commit history) plus the current `main` of both in-scope repos. The analysis: Phase A/B/C/D are closed (artefacts merged, runbook §1.2 and the Phase-D status note in the runbook header confirm); Phase E (`standards#100`) is the only open phase; all remaining §1 checklist items are owner-driven (`!OWNER:` placeholders, D-4 rebaseline `workflow_dispatch`, cerro-torre `.ctp` signing, the §6.4 Trustfile flip). This PR advances Phase E §1.5 ("Gateway-side prerequisites") by converting one half of the declared-surface invariant into an executable artefact, mirroring exactly the script-first split of #228. ## Test plan - [ ] Run the script on this branch's working tree: `bash scripts/hcg-spec-coverage-check.sh` — expect exit `0`, "OK: every openapi-declared route is covered by at least one policy rule." with `Declared (openapi) routes: 26` and `Policy (verb,path) rules: 28`. - [ ] Run `bash scripts/hcg-spec-coverage-check.sh -v` — expect the same exit `0` plus a `Matched:` block listing each of the 26 declared routes against its policy rule (literal `/health` → literal rule; `/cartridge/{name}/invoke` → `^/cartridge/[A-Za-z0-9_.-]+/invoke$` regex; `/grpc/{service}/{method}` → two-segment regex; `/umoja/peers` matches both `GET` and `POST` rules; etc.). - [ ] Synthetic gap test: build a temporary openapi.yaml containing a single declared path with no policy rule and run `OPENAPI_FILE=... bash scripts/hcg-spec-coverage-check.sh` — expect exit `1` with the route listed under `GAP:`. (Verified locally on this branch.) - [ ] Confirm `shellcheck scripts/hcg-spec-coverage-check.sh` produces only the same `SC1001` info note that `scripts/hcg-surface-drift-check.sh` produces today (the `\^` escape inside a `case` pattern is intentional and matches the sibling script's posture exactly). - [ ] Confirm SPDX header + Owner copyright match the canonical estate format (matches `scripts/hcg-surface-drift-check.sh`'s header shape). - [ ] Verify `scripts/check-shebang-first.sh` is still green with the new file present. - [ ] Verify no Hypatia / governance / spdx gates fire on the new script file. Refs hyperpolymath/standards#91 Refs hyperpolymath/standards#100 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_013VLPKSTEMFnPYQdx6rD91b)_ Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase A contract §3 invariant 3 mandates: "Any
X-Trust-Levelarriving from any other source MUST be ignored and treated as
untrusted."BojRest.TrustPolicy.satisfies?/3previously acceptedX-Trust-Level: authenticated|internalfrom any caller — the thirdclause matched regardless of
is_local. A non-loopback caller reachingBoJ's back-side bind (a §4 violation) could therefore claim any trust
class by setting a header. This is the BoJ-side half of the §3
defence-in-depth pair; the gateway-side strip + re-emit landed as
http-capability-gateway#11.
The fix
One new clause between the existing
:publicand:authenticatedarms:
Plus the surrounding bookkeeping:
trust_policy_test.exs— 2 existing assertions inverted (theycodified the buggy behaviour) + 2 new tests explicitly pinning §3
router_test.exs— 2 existing tests inverted (used to assert thata non-loopback caller with
X-Trust-Level: authenticated|internalpassed; now assert they get 403)
phase_c_seam_test.exs— 5@tag skip:annotations removed; modulemoduledoc updated (was a "documented finding" module; now a live
enforcement test module)
docs/integration/http-capability-gateway-boj-contract.md— new"Implementation status (Phase C)" subsection naming the three
controls (gateway-side / BoJ-side / operational §4) and where each
landed
lib/boj_rest/trust_policy.ex— moduledoc updated with a "Phase A §3invariant 3" subsection explaining the new clause
Test plan
mix test test/{trust_policy,router,phase_c_seam}_test.exs— 64/64 greenmix test— 2 failures, both pre-existing baseline rot incatalog_test.exs:85+catalog_properties_test.exs:73(cartridgeauth.methodvalues), unrelated to this PRRefs
Refs hyperpolymath/standards#98
Refs hyperpolymath/standards#91
🤖 Generated with Claude Code