Skip to content

feat(scripts): hcg-policy-smoke.sh — §1.5 operator pre-check (Phase E)#210

Merged
hyperpolymath merged 4 commits into
mainfrom
phase-e/policy-deny-smoke
Jun 12, 2026
Merged

feat(scripts): hcg-policy-smoke.sh — §1.5 operator pre-check (Phase E)#210
hyperpolymath merged 4 commits into
mainfrom
phase-e/policy-deny-smoke

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

Lands scripts/hcg-policy-smoke.sh — a checked-in smoke that exercises
the HCG tier-2 live Verb Governance Spec
(config/gateway-policy-boj.yaml) from outside the gateway. Replaces
the manual probe sequence the rollout runbook §1.5 last open item
formerly described as "out of band — operator pre-check".

Single-lane HCG tier-2 channel (standards#91). Phase A (#96), B (#97),
C (#98), D (#99) are joint-closed; Phase E (standards#100) is the
active phase. This PR lands one tractable artefact (§1.5 operator
pre-check now checked-in and reproducible); staging soak (§2),
production traffic split (§3), and the §6.4 Trustfile flip remain
owner-driven.

What this PR lands

  • scripts/hcg-policy-smoke.sh — POSIX-conformant bash + curl, no
    jq/yq dependency.

    • Deny mode (default): sends one no-trust-header probe to every
      non-public route in the live policy (25 routes spanning the 19
      authenticated and 6 internal+stealth entries) and asserts a 4xx
      response. The 4xx assertion covers both bare 403 and stealth-profile
      codes regardless of the gateway's :stealth_profiles runtime
      config. Plus a default-deny verb canary (DELETE /cartridges, PUT
      /health, PATCH /cartridges) confirming global_verbs: [GET, POST]
      enforces the ADR-0004 verb-governance invariant for un-listed verbs.
      Gateway-internal — BoJ does not have to be reachable.
    • --with-backend mode: additionally probes the allow path with
      X-Trust-Level: authenticated (and internal for internal+stealth
      routes), asserting the response is NOT a gateway-origin 4xx (2xx /
      3xx / 5xx all pass — BoJ's own status is fine; only a gateway deny
      is a failure). Requires BoJ reachable at the gateway's BACKEND_URL
      and the script to run from a trusted-proxy IP so the trust header is
      not stripped by the gateway's strip_untrusted_headers plug.
    • Exits 0 on all-PASS, 1 on any FAIL (with per-probe summary), 64 on
      usage error.
  • Runbook §1.5 — last unchecked operator pre-check item flips from
    a free-form "stand the gateway up ... exercise one allow + one deny
    per route" sequence (which was deferred to boj-server#165's test plan
    and documented as out-of-band) to a single
    scripts/hcg-policy-smoke.sh invocation. The PASS/FAIL summary
    attaches to the cut-over ticket; a single FAIL is a stop-the-rollout
    condition with the three failure modes named (policy not enforcing,
    BoJ unreachable, non-trusted-proxy caller stripping the header).

  • Runbook header — version 0.3 → 0.4; date 2026-06-09 → 2026-06-10;
    status line acknowledges the smoke script landing alongside the
    existing live policy promotion.

  • Runbook Appendix B — new cross-reference entry for
    scripts/hcg-policy-smoke.sh.

What this PR deliberately does NOT do

  • Close standards#100. Per runbook §6.5 the joint-close happens
    after the §6.4 Trustfile flip (tier_2_gateway.status: PENDING → DEPLOYED), which itself follows the §3.3 100% production-soak
    window. Using Refs to match the Phase E PR convention established
    by feat(config): promote gateway policy example → live (Phase E §1.5) #208 / chore(deps): bump nixpkgs from 01fbdee to 6368eda #38 / docs(hcg-load-profile): Phase D D1 — load profile declaration (standards#99) #168 and documented in §6.5 ("Do not self-close
    standards#100; joint-close is owner-only per the single-lane channel
    discipline"). The owner remains the sole closer of standards#100.
  • Touch HCG. This is a BoJ-side artefact: the script lives in
    scripts/, reads config/gateway-policy-boj.yaml, and probes the
    gateway over HTTP. No companion PR on the gateway repo required.
  • Run during CI deployment. The script is checked in but only the
    operator's explicit invocation against a live gateway URL exercises
    it. CI does not stand up a gateway to run it (would require an
    external service); the script is intentionally operator-driven, with
    the PASS/FAIL summary attached to the cut-over ticket as the
    evidence-of-pre-check artefact.
  • Diverge the policy from the script's route matrix. The script's
    route matrix mirrors the 25-route live policy. When the policy file
    evolves (new BoJ surface routes wired in), the script must be updated
    in lock-step — that is a benefit not a cost (the script doubles as a
    policy-completeness checklist), but it must be observed.

Verification

  • bash -n scripts/hcg-policy-smoke.sh — syntax check passes.
  • scripts/hcg-policy-smoke.sh (no args) — exits 64 (usage error).
  • scripts/hcg-policy-smoke.sh --help — exits 64 with full help.
  • Against a synthetic always-403 mock on :18443 — PASS=28 FAIL=0,
    exits 0 (deny-only mode covers all 25 policy routes + 3 verb
    canaries).
  • Against a closed port (no gateway up) — every probe FAILs with
    got=000 expected=deny; exits 1 with the FAIL line summary.
  • SPDX header MPL-2.0 matches repo convention (scripts/, docs/).
  • Runbook cross-references resolve (§1.5, Appendix B, sibling
    docs).

Channel position

standards#91 (parent, open)
├── #96 Phase A — closed (boj-server: contract + policy-authoring + example; gateway: -)
├── #97 Phase B — closed (gateway#10: mTLS primary path)
├── #98 Phase C — closed (gateway#11: strip; boj-server#106: TrustPolicy clause)
├── #99 Phase D — closed (boj-server#168 on 2026-06-01; gateway#12/#14/#22/#26/#30)
└── #100 Phase E — IN PROGRESS
     ├── E5 runbook draft — boj-server#128 (landed; rehearsal pending)
     ├── E1 loopback prereqs — boj-server#130/#131/#132/#165/#173 (landed)
     ├── E1 deploy spec — http-capability-gateway#38 (landed)
     ├── E1 live policy promotion — boj-server#208 (landed)
     ├── §1.5 operator pre-check smoke — THIS PR (in review)
     ├── E1 .ctp signing — owner follow-up
     ├── E2 staging cut-over — owner follow-up
     ├── E3 telemetry verification — owner follow-up
     ├── E4 production rollout — owner follow-up
     └── §6.4 Trustfile flip + §6.5 joint-close — owner-only

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

🤖 Generated with Claude Code


Generated by Claude Code

Lands `scripts/hcg-policy-smoke.sh`: a checked-in smoke that exercises
the HCG tier-2 live Verb Governance Spec
(`config/gateway-policy-boj.yaml`) from outside the gateway. Replaces
the manual probe sequence the rollout runbook §1.5 last open item
formerly described as "out of band — operator pre-check".

What the script does, by default:

* Sends one no-trust-header deny probe to every non-public route in
  the live policy (25 routes spanning the 19 authenticated and 6
  internal+stealth entries — `cartridges`, `cartridge/:name(/invoke|
  /sse|/load|/unload|/reload)`, `umoja/*`, `coprocessor/*`,
  `sdp/status`, `graphql`, `sse`, `order(-ticket)`, `community/*`,
  etc.) and asserts a 4xx response, covering both bare 403 and
  stealth-profile codes regardless of the gateway's
  `:stealth_profiles` runtime config.
* Sends a default-deny verb canary (DELETE /cartridges, PUT /health,
  PATCH /cartridges) to confirm `global_verbs: [GET, POST]` enforces
  the ADR-0004 verb-governance invariant for un-listed verbs.
* Exits non-zero with a per-probe FAIL summary on any mismatch.

This entire mode is gateway-internal — BoJ does not have to be
reachable. It can run during §1.5 staging stand-up before BoJ is wired
behind the gateway, and it'll catch a policy-not-loaded or
policy-not-enforcing regression at the cheapest possible step.

With `--with-backend`, the script additionally probes the allow path
with `X-Trust-Level: authenticated` (and `internal` for
internal+stealth routes), asserting the response is NOT a
gateway-origin 4xx (2xx / 3xx / 5xx all pass — BoJ's own status is
fine; only a gateway deny is a failure). This second mode requires
BoJ reachable at the gateway's `BACKEND_URL` and the script to run
from a trusted-proxy IP so the trust header is not stripped by the
gateway's `strip_untrusted_headers` plug.

Runbook diff:

* §1.5 — last unchecked operator pre-check item flips from a free-form
  "stand the gateway up ... exercise one allow + one deny per route"
  sequence (which was deferred to boj-server#165's test plan and
  documented as out-of-band) to a single `scripts/hcg-policy-smoke.sh`
  invocation. The PASS/FAIL summary attaches to the cut-over ticket; a
  single FAIL is a stop-the-rollout condition with the three failure
  modes named (policy not enforcing, BoJ unreachable, non-trusted-proxy
  caller stripping the header).
* Header — version 0.3 → 0.4; date 2026-06-09 → 2026-06-10; status line
  acknowledges the smoke script landing alongside the existing live
  policy promotion.
* Appendix B — new cross-reference entry for `scripts/hcg-policy-smoke.sh`.

What this PR deliberately does NOT do:

* **Close `standards#100`.** Per runbook §6.5 the joint-close happens
  after the §6.4 Trustfile flip
  (`tier_2_gateway.status: PENDING → DEPLOYED`), which itself follows
  the §3.3 100% production-soak window. Using `Refs` to match the
  Phase E PR convention established by #208 / #38 / #168 and
  documented in §6.5 ("Do not self-close standards#100; joint-close is
  owner-only per the single-lane channel discipline").
* **Touch HCG.** This is a BoJ-side artefact: the script lives in
  `scripts/`, reads `config/gateway-policy-boj.yaml`, and probes the
  gateway over HTTP. No gateway-repo change required.
* **Run during deployment.** The script is checked in but only the
  operator's explicit invocation against a live gateway URL exercises
  it. CI does not stand up a gateway to run it (would require an
  external service); the script is intentionally operator-driven.

Verification:

* `bash -n scripts/hcg-policy-smoke.sh` — syntax check passes.
* `scripts/hcg-policy-smoke.sh` (no args) — exits 64 (usage error).
* `scripts/hcg-policy-smoke.sh --help` — exits 64 with full help.
* Against a synthetic always-403 mock on :18443 — `PASS=28 FAIL=0`,
  exits 0 (deny-only mode covers all 25 policy routes + 3 verb
  canaries).
* Against a closed port (no gateway up) — every probe FAILs with
  "got=000 expected=deny"; exits 1 with the FAIL line summary.

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 273 issues detected

Severity Count
🔴 Critical 15
🟠 High 137
🟡 Medium 121

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stale AI session file -- delete",
    "type": "stale",
    "file": "GEMINI.md",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "medium"
  },
  {
    "reason": "Action  if: always()\n        uses: actions/upload-artifact@ea165f8 needs attention",
    "type": "unpinned_action",
    "file": "e2e.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in abi-drift.yml",
    "type": "missing_timeout_minutes",
    "file": "abi-drift.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "missing_timeout_minutes",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in container-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "container-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath marked this pull request as ready for review June 11, 2026 08:08
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 11, 2026 08:08
@hyperpolymath hyperpolymath disabled auto-merge June 11, 2026 08:09
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 11, 2026 08:09
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 273 issues detected

Severity Count
🔴 Critical 15
🟠 High 137
🟡 Medium 121

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stale AI session file -- delete",
    "type": "stale",
    "file": "GEMINI.md",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "medium"
  },
  {
    "reason": "Action  if: always()\n        uses: actions/upload-artifact@ea165f8 needs attention",
    "type": "unpinned_action",
    "file": "e2e.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in abi-drift.yml",
    "type": "missing_timeout_minutes",
    "file": "abi-drift.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "missing_timeout_minutes",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in container-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "container-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath disabled auto-merge June 11, 2026 08:11
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 11, 2026 08:11
@hyperpolymath hyperpolymath disabled auto-merge June 11, 2026 14:26
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 11, 2026 14:27
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 273 issues detected

Severity Count
🔴 Critical 15
🟠 High 137
🟡 Medium 121

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stale AI session file -- delete",
    "type": "stale",
    "file": "GEMINI.md",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "medium"
  },
  {
    "reason": "Action  if: always()\n        uses: actions/upload-artifact@ea165f8 needs attention",
    "type": "unpinned_action",
    "file": "e2e.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in abi-drift.yml",
    "type": "missing_timeout_minutes",
    "file": "abi-drift.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "missing_timeout_minutes",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in container-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "container-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 273 issues detected

Severity Count
🔴 Critical 15
🟠 High 137
🟡 Medium 121

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stale AI session file -- delete",
    "type": "stale",
    "file": "GEMINI.md",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "medium"
  },
  {
    "reason": "Action ses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886 needs attention",
    "type": "unpinned_action",
    "file": "e2e.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in abi-drift.yml",
    "type": "missing_timeout_minutes",
    "file": "abi-drift.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "missing_timeout_minutes",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in container-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "container-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath disabled auto-merge June 12, 2026 00:31
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 12, 2026 11:20
@hyperpolymath hyperpolymath merged commit da74a5f into main Jun 12, 2026
23 checks passed
@hyperpolymath hyperpolymath deleted the phase-e/policy-deny-smoke branch June 12, 2026 11:21
hyperpolymath added a commit that referenced this pull request Jun 13, 2026
## Summary

Tightens `scripts/hcg-policy-smoke.sh` against three verb-governance
regression classes the original three canaries (DELETE/PUT/PATCH on
`/cartridges` and `/health`) don't catch. Single-lane HCG tier-2 channel
(`standards#91`); Phase E (`standards#100`) is the active phase.

## What this PR adds

Three new deny-mode verb-canary probes:

1. **`OPTIONS /cartridges`** — `global_verbs: [GET, POST]` bans OPTIONS,
but a CORS preflight auto-responder added later would silently bypass
policy. The canary fails closed against that regression class.

2. **`DELETE /cartridge/probe/invoke`** — exercises the regex route
`^/cartridge/[A-Za-z0-9_.-]+/invoke$` under a banned verb. The existing
exact-path canaries don't catch a regex-matcher regression where the
path is accepted under any verb instead of only the verb the rule lists.

3. **`GET /cartridges/ssg-mcp/webhook`** — the path is in the policy as
a documented public exception, but only for POST. The canary verifies
the `{path, verb}` pairing is enforced: GET on the same path must
default-deny because no rule covers it.

Runbook §1.5 description updated to enumerate the expanded canary set;
runbook version bump 0.4 → 0.5.

## What this PR deliberately does NOT do

- **Probe HEAD.** Curl with `-X HEAD` (vs `--head`) waits for a body the
server will not send, which interacts badly with the script's
`--max-time 10`. HEAD enforcement remains covered by the gateway's own
unit tests; the §1.5 operator pre-check focuses on probes that survive
curl's method quirks. The reasoning is captured inline in the script
comment so a future maintainer doesn't add it back as an oversight.

- **Extend the `--with-backend` allow-path matrix.** The authenticated
routes the script probes in allow mode are the ones actually wired in
`BojRest.Router`; the additional policy entries (`graphql`, `sse`,
`order`, `umoja/*`, etc.) are declared-not-yet-wired per contract §8 and
would 404 from BoJ, which the `allow_or_upstream` pattern misdiagnoses
as gateway-deny. They stay in the deny matrix until they are wired in
BoJ. The `ssg-mcp-webhook-post` route is not added to the
`--with-backend` allow probes for the same reason.

- **Auto-derive the probe matrix from the policy YAML.** The matrix
stays hand-maintained and the parity-with-policy property remains a
manual maintenance discipline. PR #210's commitment ("the script doubles
as a policy-completeness checklist") is preserved.

- **Close `standards#100`.** Per runbook §6.5 the joint-close happens
after the §6.4 Trustfile flip, which itself follows the §3.3 100%
production-soak window. Using `Refs` per the Phase E PR convention (#38,
#208, #210).

## Verification

- [x] `bash -n scripts/hcg-policy-smoke.sh` — syntax check passes.
- [x] Synthetic always-403 mock on `:18443` — `PASS=31 FAIL=0` (was 28);
the three new canaries report PASS; exit 0.
- [x] `--help` and bad-args exit codes unchanged (64).
- [x] SPDX header `MPL-2.0` unchanged.
- [x] Runbook cross-references resolve.

## Channel position

```
standards#91 (parent, open)
├── #96 Phase A — closed
├── #97 Phase B — closed
├── #98 Phase C — closed
├── #99 Phase D — closed (joint-closed via boj-server#168)
└── #100 Phase E — IN PROGRESS
     ├── E5 runbook draft — boj-server#128 (landed)
     ├── E1 loopback prereqs — boj-server#130/#131/#132/#165/#173 (landed)
     ├── E1 deploy spec — http-capability-gateway#38 (landed)
     ├── E1 live policy promotion — boj-server#208 (landed)
     ├── §1.5 operator pre-check smoke — boj-server#210 (landed)
     ├── §1.5 verb-canary expansion — THIS PR (in review)
     ├── E1 .ctp signing — owner follow-up
     ├── E2 staging cut-over — owner follow-up
     ├── E3 telemetry verification — owner follow-up
     ├── E4 production rollout — owner follow-up
     └── §6.4 Trustfile flip + §6.5 joint-close — owner-only
```

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_01NgeisSDPZ7NJSjkhU95qYS)_

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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>
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