Problem/Motivation
2.11.0 shipped signed portable policy bundles (#3616536): verify, activate, simulate, revoke, rollback, emergency deny, and policy_bundle_digest on every audit row. Simulation already refuses an operation when the attested bundle denies it, and a local deny cannot be widened by an upstream allow. That evaluation is not yet on the live access path. An activated bundle that denies an operation currently does not change what a governed request can do.
Proposed resolution
Call the existing simulate/deny evaluation from the live governance decision path so an attested bundle can only refuse more than the local profile, never less.
- If no bundle is attested, behaviour is unchanged.
- If a bundle is attested and denies the operation (or emergency deny is armed), the request is refused with a stable reason and the attested digest on the audit row.
- A local deny still wins when the bundle would allow.
- Missing signing key cannot invent new authority; existing local denials still apply.
Remaining tasks
- Wire the evaluation into the existing access/decision seam (do not add a second policy engine).
- Tests: no bundle = byte-identical allow/deny; bundle deny refuses; local deny still wins; emergency deny refuses; digest cited on the refusal row.
- Document the live hook in README/CHANGELOG.
User interface changes
None required. Operators already activate/revoke bundles; this issue only makes those attestations enforceable.
API changes
No new public type. Existing McpPolicyBundleRegistry::simulate() becomes the live evaluation, not only a preview.
Data model changes
None.
Comments
Comment #4
jmcerdaMerged to 1.x (0de7172).
The attested simulate() floor is now on the live access path: entity, create, config, and JSON:API filter access; the context schema document; GraphQL query and mutation gates; and governed raw SQL. A local deny cannot be widened. Emergency deny and revoke of the active digest refuse live access. An attested digest that will not verify fails closed. The client-facing reason is policy_bundle_denied; the digest stays on the audit row.
Working PR: https://github.com/Wilkes-Liberty/mcp_sentinel/pull/151
Not a tagged release. This issue stays Active until a module tag ships the live floor.
Comment #9
jmcerdaReleased in 2.12.0.
The attested policy-bundle floor is now consulted on the live access path: McpAccessChecker (entity, create, config, JSON:API filter), the context schema document, GraphQL query and mutation gates, and the governed drush SQL command all call the same evaluation simulate already used.
Tightening only, by construction: a local deny cannot be widened by an upstream allow; an attested digest whose document will not verify (expired, revoked, tampered, or missing key) fails closed rather than allowing; emergency deny and revoke of the active digest refuse live access; and cached allows do not outlive the attested document's remaining TTL, so the floor cannot expire into an allow.
Client-facing reason is the stable code policy_bundle_denied, with the attested digest kept on the audit row rather than in the response.
https://www.drupal.org/project/mcp_sentinel/releases/2.12.0