Skip to content

feat(authz): wrap Permit behind FuzeFront Security API for chat-service (#254) - #836

Merged
claude[bot] merged 4 commits into
masterfrom
claude/issue-254-authz-api-wrapper
Aug 27, 2026
Merged

feat(authz): wrap Permit behind FuzeFront Security API for chat-service (#254)#836
claude[bot] merged 4 commits into
masterfrom
claude/issue-254-authz-api-wrapper

Conversation

@claude

@claude claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the FuzeFront-repo side of #254 for one real, live consumer: chat-service no longer needs to call the Permit PDP directly to get the same authorization answer.

Key finding before writing any code: most of what #254 asks for — a FuzeFront-owned authorization API fronting Permit, a generated client, fail-closed semantics — already exists, delivered as part of the unified Security API (packages/security/openapi.yaml, backend/security/src/routes/authz.ts, published as @fuzefront/security-client v0.7.0, and consumed via @fuzefront/auth's createAuthzClient). config-service's src/middleware/authz.ts is already running this in production instead of an embedded Permit SDK. The contract's own "Delivery phasing" note says it plainly: "AuthN is delivered first…AuthZ is designed here in full and its consumer rollout is sequenced after AuthN." Rollout — not the API — was the open item.

So this PR is that rollout, for one concrete, currently-in-repo direct-Permit consumer, using the proven pattern rather than inventing a new one.

What changed

services/chat-service's agent-tool permission check (agent/executor.ts) called POST {PERMIT_PDP_URL}/allowed directly (agent/permit.ts) — exactly the "consumer product calls the PDP directly" leak #254 names. Added a flag-gated wrapped path:

  • agent/securityApiPermitAdapter.ts — adapts @fuzefront/auth's createAuthzClient (AuthzClient) to the same check() shape PermitClient exposes. Fail-closed: no bearer token → deny (never guesses an identity); any AuthzError/transport failure → deny.
  • agent/authzGateway.ts — the new dependency executor.ts takes instead of PermitClient directly. Selects direct (unchanged PDP call) vs wrapped (Security API) per the flag; fails closed to direct even if the flag lookup itself throws.
  • agent/authzFlag.tsfuzefront.authz.chat-agent-security-api, release flag, default OFF, registered in packages/feature-flags/flag-registry.yaml. Lazy-required @fuzefront/feature-flags, mirrors backend/security/src/utils/rootMembershipFlag.ts exactly (degrades to OFF if the package/provider is absent — never throws).
  • agent/permit.tsPermitCheck gains an optional token field (threads the caller's bearer token to whichever implementation needs it). PermitClient itself is byte-for-byte unchanged and stays live as the flag-OFF default — per the issue's explicit "keep the direct-PDP path working until the wrapped API is proven, so nothing regresses."
  • executor.ts — forwards PendingExecution.token through the check; widened ToolExecutorDeps.permit to a small AuthzCheckable interface (structurally identical to the old Pick<PermitClient,'check'>, so the existing mock-based tests are untouched and still pass).
  • config.ts + Helm (chat-service.yaml, values.yaml, values-prod.yaml) — SECURITY_SERVICE_URL (defaults to in-cluster fuzefront-security:3002, same convention config-service/provisioning-service already use) + the Unleash featureFlags env block, mirroring security.yaml's wiring exactly (renders nothing when unwired → safe in-code default).

Both flag states are tested (authzGateway.test.ts), including the "flag lookup itself throws" case falling back to OFF. The wrapped adapter is tested against both an injected AuthzClient (unit) and a real createAuthzClient + mocked fetch (proves the actual request shape: URL, Authorization: Bearer <token>, body). permit.test.ts and executor.test.ts are unchanged and still green.

Deferred, and why (not folded into this PR)

  • services/billing-service's permit.service.ts calls the Permit cloud API (api.users.update/api.tenants.update) to sync ABAC attributes (plan_tier, plan_status) for policy evaluation. There is no equivalent operation in the frozen Security API contract (AuthzClient covers check/bulkCheck/grant/revoke/listGrants — role/permission assignment, not arbitrary subject-attribute writes). Wrapping this needs a new contract surface, which per the baseline's contract-first rule is contract-designer territory, not something to freehand here. Open question for the owner: should attribute-sync get a new PUT /authz/subjects/{id}/attributes-shaped endpoint on the Security API, or is ABAC attribute sync intentionally out of the Security API's scope (a different kind of "Permit fact" than an authorization decision or grant)?
  • backend/src/config/permit.ts, backend/src/permit/*, backend/applications/src/app-registry/permit.ts — the pre-extraction monolith's own direct Permit usage. backend/security is clearly the intended replacement (it's live, deployed, and other services already consume it), but cutting the main backend's own request path over to calling its own extracted service is the actual backendbackend/security extraction — a large, separate, already-implied initiative, not a same-PR change I should make unreviewed.
  • The companion MendysRobotics datasets-service migration is explicitly out of this repo per the issue itself (tracked in a companion MendysRobotics issue).

Verification

  • npx tsc --noEmit (chat-service) — clean.
  • npx jest (chat-service, full suite) — 27/27 suites, 153 passed / 2 pre-existing skipped, including the 4 new/changed suites (authzGateway, authzFlag, securityApiPermitAdapter, executor, permit).
  • npm run build -w services/chat-service (real tsc build, not just --noEmit) — clean.
  • helm lint deploy/helm/fuzefront -f values.yaml — clean.
  • helm template … --output-dir for all three overlays (values.yaml, values-local.yaml, values-prod.yaml) with every optional workload enabled (mirrors helm-validate.yml's exact CI invocation) → piped through kubeconform -strict -kubernetes-version 1.29.0 -skip CustomResourceDefinition,Middleware,SealedSecret0 Invalid, 0 Errors on all three.
  • Confirmed by rendered output: SECURITY_SERVICE_URL resolves to http://fuzefront-security:3002 in both base and prod values; the Unleash featureFlags block renders in prod (where securityService.enabled: true, so the wrapped path is reachable in-cluster once the flag flips) and correctly does not render on base values.yaml (empty unleashUrl default).
  • packages/auth and packages/feature-flags built cleanly via tsup (scoped install, not a full monorepo install, per the disk-constraint instruction).

Gates

  • gate-authz (semgrep .semgrep/fuze-authz.yml + OWASP/secrets packs): no new code matches any of the four seeded rules (no findById(req.params.x), no Object.assign(obj, req.body), no jwt.sign, no bcrypt.*).
  • No OpenAPI contract touched — @fuzefront/auth's frozen authzTypes.ts contract is consumed as-is, not modified.
  • services/chat-service's package.json bumped 1.1.01.2.0 (source changed) per gate-version's SemVer-bump check.

Open questions for the owner

  1. Billing-service's Permit-cloud ABAC-attribute-sync gap above — new contract surface or intentionally out of scope?
  2. Is chat-service's agent/executor.ts confirm/execute path meant to be wired into a live route soon? It currently isn't reachable from any route (index.ts never constructs a ToolExecutor) — pre-existing and unrelated to feat(authz): wrap Permit behind a FuzeFront authorization API (consumer products must not call the PDP/Permit cloud directly) #254, but it means this migration, while real and tested, has no live traffic yet to prove itself against in production before the flag flips. Worth knowing before treating "flip the flag" as low-risk in practice.
  3. Should backend/src's (main backend, not backend/security) own direct Permit config be tracked as an explicit follow-up issue for the backendbackend/security extraction, separate from feat(authz): wrap Permit behind a FuzeFront authorization API (consumer products must not call the PDP/Permit cloud directly) #254?

🤖 Generated with Claude Code

https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc


Generated by Claude Code

chat-service's agent-tool permission check (agent/executor.ts) called the
Permit.io PDP directly over REST (agent/permit.ts) — the exact "consumer
product calls Permit/PDP directly" leak #254 targets. Add a flag-gated
wrapped path that asks the same question of FuzeFront's own Security API
instead, via the already-published, already-production-proven
@fuzefront/auth createAuthzClient (the same client config-service's
middleware/authz.ts uses for its own Permit-free authorization).

- agent/securityApiPermitAdapter.ts: adapts createAuthzClient's AuthzClient
  to the PermitClient-compatible check() shape. Fail-closed: no token ->
  deny, AuthzError/any throw -> deny.
- agent/authzGateway.ts: the flag-gated selector executor.ts now depends on
  in place of PermitClient directly — OFF routes to the unchanged direct-PDP
  PermitClient, ON routes to the wrapped adapter. Fails closed to OFF even if
  the injected flag lookup itself throws.
- agent/authzFlag.ts: fuzefront.authz.chat-agent-security-api, release flag,
  default OFF (registered in packages/feature-flags/flag-registry.yaml).
  Lazy-required @fuzefront/feature-flags, mirrors
  backend/security/utils/rootMembershipFlag.ts.
- agent/permit.ts: PermitCheck gains an optional `token` field (carries the
  caller's bearer token to whichever implementation needs it); PermitClient
  itself is UNCHANGED and stays live as the flag-OFF default, per the issue's
  "keep the direct-PDP path working until the wrapped API is proven" ask.
- executor.ts: forwards PendingExecution.token through the check; widens
  ToolExecutorDeps.permit to the new AuthzCheckable interface (structurally
  compatible with the existing PermitClient/mock-based tests, which are
  unchanged and still pass).
- config.ts / Helm (chat-service.yaml, values.yaml, values-prod.yaml):
  SECURITY_SERVICE_URL (defaults to the in-cluster fuzefront-security:3002,
  same convention as config-service/provisioning-service) + the Unleash
  featureFlags block (mirrors security.yaml's wiring; degrades to in-code
  default when unwired, same as every other service's flag block).

Both flag states are unit-tested (authzGateway.test.ts), including the
flag-lookup-throws-safely-to-OFF case; the wrapped adapter is tested against
both an injected AuthzClient and a real createAuthzClient + mocked fetch
(request shape, allow/deny, fail-closed on non-2xx). Existing permit.test.ts
and executor.test.ts are unchanged and still pass.

Scope: this PR migrates chat-service only — see the PR description for the
full audit of remaining direct-Permit call sites in this repo (billing-service
ABAC attribute sync, backend/src's pre-extraction Permit config) and why each
is deliberately deferred rather than folded in here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc
@claude
claude Bot requested a review from izzywdev as a code owner August 27, 2026 08:14
@claude claude Bot added the auto-merge Enable squash auto-merge once CI passes label Aug 27, 2026
…#836)

PR #836's own defect, in three places, all required:

1. package-lock.json — the ROOT lockfile's services/chat-service entry
   never gained @fuzefront/auth / @fuzefront/feature-flags as dependency
   edges when they were added to chat-service's package.json (I edited
   package.json after the last `npm install`, never reran it). `npm ci`
   is lockfile-only and silently installs nothing for a workspace whose
   lockfile entry is stale, unlike `npm install`'s live re-resolution —
   which is exactly why my local `tsc`/jest passed (ran via `npm install`)
   while CI's `npm ci` did not. Regenerated via `npm install -w
   services/chat-service`; diff is 3 lines.

2. .github/workflows/ci.yml (`chat-service-tests` job) — @fuzefront/auth
   ships its types from dist/ (tsup, not committed), and
   securityApiPermitAdapter.ts imports createAuthzClient/AuthzClient
   statically, so both the type-check step and the jest run (ts-jest
   resolves the same way) failed TS2307 "Cannot find module
   '@fuzefront/auth'" until it's built first. Added the same prebuild
   pattern the file already uses for @fuzefront/custom-hostname-client /
   @fuzefront/security-client. @fuzefront/feature-flags needs no such
   step: agent/authzFlag.ts only ever `require()`s it at runtime inside a
   try/catch, no static import, so tsc never needs its types — confirmed
   by grep (no `import` of it anywhere) and by design (the "package
   absent" branch is exactly what authzFlag.test.ts's fail-safe-OFF case
   exercises).

3. services/chat-service/Dockerfile — the build stage never copied or
   built packages/auth or packages/feature-flags source, so the image's
   own `npm run build` hit the identical TS2307. Added COPY + `npm
   install --ignore-scripts && npm run build` for both, mirroring line
   100's existing treatment of packages/identity. The production stage
   copies each package's package.json + dist (populating the `file:`-
   linked symlink target, same as config-service's and
   selection-list-service's Dockerfiles) plus, for @fuzefront/auth,
   packages/auth/node_modules from the base (--omit=dev) stage — its
   runtime dep `jose@^5.10.0` is NOT hoisted to /app/node_modules (root
   pins jose to 4.15.9 for other consumers), so it only exists at
   packages/auth/node_modules/jose; omitting it is the exact "builds
   green, pod dies at module load" bug already fixed for
   selection-list-service in #817. packages/feature-flags/node_modules
   is copied too, even though this repo's dependency graph currently
   hoists its runtime deps (@openfeature/*, unleash-client) to root
   regardless — scripts/check-dockerfile-lockfile.mjs (FFRNT-254) flags
   any dist-only copy of a file:-linked package with runtime deps
   unconditionally, on the (correct) theory that hoisting outcomes are
   exactly the kind of thing this repo has been bitten by before and
   should never be trusted implicitly per Dockerfile.

Verified without a Docker daemon by replicating every stage outside
Docker (per #817's own verification approach): staged the Dockerfile's
manifest COPY list into a scratch dir, ran the identical `npm ci
--workspace=services/chat-service` (both --omit=dev and full) against
the real committed lockfile, ran the identical per-package build
commands, assembled a "production" directory from exactly the COPY
list's source/destination pairs, then had Node itself `require.resolve`
and actually `require()` the built adapter/flag modules AND
`@fuzefront/feature-flags` from that assembled tree — proving real
module load, not just path resolution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc
@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a fix for the CI/Docker build defect flagged on this PR (own-PR bug, not the unrelated config-service contract-conformance failure).

Root cause (three places, all required — none alone was sufficient):

  1. package-lock.json — the root lockfile's services/chat-service entry never picked up @fuzefront/auth/@fuzefront/feature-flags as dependency edges after they were added to package.json (edited after the last npm install). npm ci is lockfile-only and silently skips a workspace whose lockfile entry is stale — which is exactly why local tsc/jest passed (ran via npm install, which re-resolves live) while CI's npm ci did not. Fixed by npm install -w services/chat-service; diff is 3 lines.
  2. .github/workflows/ci.yml (chat-service-tests job) — @fuzefront/auth ships types from an uncommitted dist/ (tsup); added the same prebuild step this file already uses for @fuzefront/custom-hostname-client/@fuzefront/security-client. @fuzefront/feature-flags needs no such step — agent/authzFlag.ts only require()s it at runtime inside a try/catch, never a static import, so tsc never needs its types (confirmed by grep + by the fail-safe-OFF test case that exercises exactly the "package absent" branch).
  3. services/chat-service/Dockerfile — build stage now copies + builds packages/auth and packages/feature-flags before chat-service (mirrors the existing packages/identity treatment). Production stage copies each package's package.json + dist, plus packages/auth/node_modules from the --omit=dev base stage (its runtime dep jose@^5.10.0 is NOT hoisted to /app/node_modules — root pins jose to 4.15.9 for other consumers — so it only exists nested; omitting it is the exact "builds green, pod dies at module load" bug already fixed for selection-list-service in fix(selection-list-service): ship packages/auth's jose into the prod image #817). Also copies packages/feature-flags/node_modules — even though this repo's current graph happens to hoist its runtime deps to root regardless — because scripts/check-dockerfile-lockfile.mjs (FFRNT-254) flags any dist-only copy of a file:-linked package with runtime deps unconditionally, on the correct theory that hoisting outcomes are exactly what this repo has been bitten by before.

Verified (no Docker daemon available), per #817's own approach:

  • Reproduced the exact failure: removed packages/auth/dist, ran npm run -w @fuzefront/chat-service build → same TS2307.
  • Ran the fix sequence: npm run -w @fuzefront/auth build then npm run -w @fuzefront/chat-service build → clean, error gone.
  • Full npx jest for chat-service: 27/27 suites, 153 passed / 2 pre-existing skipped.
  • Replicated the Dockerfile's manifest COPY list into a scratch dir; ran the identical npm ci --workspace=services/chat-service (both --omit=dev and full) against the real committed lockfile, ran the identical per-package build commands, assembled a "production" directory from exactly the COPY list's source/destination pairs, then had Node itself require.resolve and actually require() the built adapter/flag modules — proving real module load, not just path resolution. Confirmed jose resolves from packages/auth/node_modules.
  • node scripts/check-dockerfile-lockfile.mjsOK (15 Dockerfile(s)).

Remote HEAD 976ac747 matches local. NOT independently verified: an actual docker build (no Docker daemon in this sandbox) and the live CI run itself — the above is the closest reproduction achievable outside Docker/CI, per the same constraint and method #817 used.


Generated by Claude Code

@github-actions
github-actions Bot enabled auto-merge (squash) August 27, 2026 08:54
The production stage failed the build outright:

  > [production 15/19] COPY --from=base /app/packages/feature-flags/node_modules
  ERROR: failed to compute cache key: "/app/packages/feature-flags/node_modules": not found

npm creates packages/<x>/node_modules only when something could NOT be hoisted.
packages/auth gets one because jose@5 conflicts with the root's jose@4, which is
why the sibling COPY a few lines up works. packages/feature-flags' runtime deps
(@openfeature/server-sdk, @openfeature/web-sdk, unleash-client) have no such
conflict, so `npm ci --omit=dev` hoists all of them into /app/node_modules and
creates no per-package directory at all — and COPY on a missing source is a hard
build failure, not a no-op.

The COPY line itself is correct and must stay: Docker has no conditional COPY,
and scripts/check-dockerfile-lockfile.mjs requires it precisely so the image does
not silently depend on what one particular resolution happened to hoist. So make
the SOURCE exist instead — `mkdir -p` in the base stage after npm ci. An empty
directory is the right outcome when everything hoisted, and if a future
resolution stops hoisting, the real contents land there and get copied without
anyone needing to remember why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc
@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

shipped code matches the frozen contract is failing, and it is not this PR's.

::error title=config-service MISSING::GET /v1/config/history is declared in
  services/config-service/openapi.yaml but no route implements it.
::error title=config-service MISSING::POST /v1/config/secrets/reveal is declared in
  services/config-service/openapi.yaml but no route implements it.
config-service: spec=9 code=7 -> 2 finding(s)

Verified rather than assumed: python3 scripts/gate_openapi_conformance.py --repo . on a
pristine master checkout — none of this PR's diff present — reproduces both findings
identically. config-service ships an openapi.yaml declaring two endpoints it does not
implement, so any client generated from that contract 404s. Pre-existing on the base branch;
this PR does not touch services/config-service/.

The fix exists: #840 implements both endpoints (config-read.routes.ts gains
GET /v1/config/history behind a new history.repository.ts + migration 004, and
secrets.write.ts adds POST /v1/config/secrets/reveal). I have read it and it clears
exactly these two findings.

Not ported into this PR, deliberately. The protocol says to port an existing fix rather
than wait — but #840 is a 25-file, ~2,000-line feature implementation including a new SQL
migration. Carrying it here would not "no-op once the base catches up"; it would put two
copies of a migration and a repository layer into conflict and widen this PR into something
nobody reviewed it as. This is the narrow case where waiting on the base is correct: #840 is
at the front of the merge queue for precisely this reason, and this PR goes green on its
first resync afterwards with no change of its own.

No re-run spent — a re-run cannot fix a deterministic contract gap.


Generated by Claude Code

claude Bot added a commit that referenced this pull request Aug 27, 2026
…te-identifier are not among them (#843)

* docs(governance): master requires 11 of 45 checks; gate-authz and gate-identifier are not among them

CLAUDE.md states the gate set IS the production guard, now that CI may approve
its own PRs: "gate-ds-conformance, gate-frames-first, gate-authz, gate-identifier,
and the full CI matrix ... A bot-authored, bot-approved PR that clears every gate
ships to prod."

The ruleset does not enforce that. Measured against the live ruleset: 11 of 45
checks are required. gate-authz and gate-identifier — both named above — are not
required. Neither is any backend, integration or e2e suite, nor CodeQL, Snyk,
Container Security Scan, gate-toolchain, gate-version, gate-pagination or
gate-vacuous-check.

auto-merge.yml arms `gh pr merge --auto --squash`, and GitHub's auto-merge waits
on REQUIRED contexts only. Master is deploy-on-push. So a PR merges and deploys
the moment those 11 pass, while the rest are still running or already red. On
2026-08-27 the bot armed auto-merge on #836 while `Build chat-service` was failing
on an unbuildable production image; that check is not required, so nothing would
have stopped the merge.

This commit is the applicable change, not a report of one: the rulesets API is
write-blocked through the agent proxy (403), so it needs repo-admin rights. The
desired set is data in governance/required-status-checks.json and the runbook
carries the exact PUT, the verification, and the rollback.

Audited every workflow trigger before choosing the set, because a required check
that does not run on every PR is never created and pins the PR at "Expected"
forever — this repo hit that on 2026-08-23 across four sampled PRs. Only
unfiltered checks are included (39 contexts). `Generate SBOM` is excluded as it
always concludes skipped.

Two findings worth more than the count:

- The most valuable gates CANNOT be required as they stand. `shipped code matches
  the frozen contract` — which would have caught config-service shipping two
  declared-but-unimplemented endpoints — is path-filtered, along with
  gate-frames-first, gate-route-ownership, image-reproducibility and helm-validate.
  Step 4 is to unfilter the trigger and filter inside the job, the correction
  workspace-deps-check.yml and gate-sealed-keys.yml already carry.

- Merge queue must NOT be enabled yet, though it is the right fix for the
  release-commit treadmill. Zero of 58 PR-triggered jobs have a `merge_group`
  trigger, so queue entries would get no checks at all and time out; and gates
  that ratchet on the PR base would compute over an empty diff under
  `merge_group`, reporting green vacuously. Trigger support and base-ref handling
  land first, proven by a gate still FAILING under a merge_group event.

Health-checked before proposing: 29 candidate contexts across all 20 open PRs
produced 0 failures, so widening stalls nothing in flight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc

* docs(governance): drop gate-code-review from the required set — it can hang, not fail

Correcting my own recommendation in the previous commit. gate-code-review is
`runs-on: fuzefront` — a SELF-HOSTED runner — with no `timeout-minutes`. Requiring
it would let one offline runner block every merge in the repo, permanently, with
no red anywhere to look at.

Not hypothetical. Measured 2026-08-27 while draining the PR queue: the job
completes in 12-14 seconds when the runner picks it up (five PRs did), but
NOTHING has completed since 09:08Z and 17 PRs sit queued for up to an hour —
including #840, the PR at the front of the merge queue, queued since 08:52Z. The
runner is not slow; it stopped taking work. Meanwhile in-progress GitHub-hosted
runs fell from 14 to 5, so this is not shared capacity.

The health check in the previous commit read gate-code-review as 5 success /
0 fail / 15 pending and I counted it safe. That was the wrong reading: those
pendings were not "slow", they were never going to complete. 0 failures is not
evidence a check can fail.

Hence the general rule now stated in the runbook, which is what I actually got
wrong: A REQUIRED CHECK MUST BE ABLE TO FAIL. A check that can only pass or hang
is worse than no check, because the deadlock it produces is indistinguishable
from CI still running, so nobody knows to go look. gate-code-review and the
path-filtered gates both fail that test, in different ways — one hangs waiting
for a runner, the others are never created at all.

Requireable again once it has a GitHub-hosted fallback, or a timeout that fails
rather than hangs. Required contexts 39 -> 38; the verification count in step 3
is updated to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc

* docs(governance): widening the required set leaves #828's guard covering 2 of 38

#828 (issue #286) adds governance/required-check-triggers.json — a gate that
fails if a required context's workflow ever gains a `paths:` filter on
`pull_request:`. It is what stops the deadlock in §2 from being reintroduced, and
its own header states the limitation:

  Adding a NEW required context to the ruleset? Add its workflow here in the same
  PR — the gate is only as complete as this list.

It ships listing 2 workflows. Step 3 of this runbook takes the required set to 38.
Applied without extending that list, the guard silently covers 2 of 38 and the
other 36 can be re-filtered by anyone with nothing to catch it — a green gate
measuring almost nothing, which is the shape of failure gate-vacuous-check exists
to prevent.

Adds step 3a: the full context -> producing-workflow mapping (seven workflows
cover all 38), plus a verification snippet that prints required / guarded /
UNGUARDED counts so the two files cannot drift silently.

Mapping verified against the workflow files rather than written from memory. Two
contexts are matrix expansions and needed checking by hand: `CodeQL Analysis
(javascript)` comes from security.yml's `codeql-analysis` job (matrix: language)
and `Lint & Test (24.x)` from ci.yml's `lint-and-test` (matrix: node-version).

Also notes that gate-openapi-conformance.yml and gate-route-ownership.yml join
the list once #844 lands, since unfiltering them is precisely the property this
guard protects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc

---------

Co-authored-by: Claude <noreply@anthropic.com>
@claude
claude Bot merged commit 8e48897 into master Aug 27, 2026
69 checks passed
@claude
claude Bot deleted the claude/issue-254-authz-api-wrapper branch August 27, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge Enable squash auto-merge once CI passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant