Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.

Releases: kanywst/mandatum

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 14:23
ebb3fb8

The AuthZEN and MCP binding release. A tool's declared mapping now decides what the Policy Decision Point is asked, and the enforcement point has been run against three PDPs that somebody else wrote. Those were the last two v0.2 items in ROADMAP.md.

Wire format: accepts mdt.v 1, issues mdt.v 1. Unchanged from 0.1.0.

This is a 0.x minor release, so the Go API may change, and it does in one place: mcp.Authorized.Decision became Decisions, under Changed below.

The gate, and how it was met

  • Works against three PDPs from different vendors with no implementation-specific code paths. The PDPs are Open Policy Agent through its contrib AuthZEN proxy, Cerbos, and OpenFGA. All three are on the AuthZEN working group's interop list, none shares code with this project, and the interop workflow runs them from pinned, digest-checked releases. The client and the enforcement point have no code that knows which PDP they are talking to. The per-PDP differences are a policy file each and one client option that already existed, and test/interop/README.md says why each is there.

Not a gate for this version, but held to anyway: the new fuzz target, FuzzDeclaredMapping, accumulated twenty-five hours with no crasher before the tag, about 810 million executions, over the code this tag names. The commits between the one fuzzed, 4338a42, and the tag change only documentation. That is the same bar v0.1 set for the targets it shipped, met the same way, as five concurrent campaigns. The mapping resolver parses input written by the server being authorized, and shipping it with minutes of fuzzing would not have matched how the parsers before it were treated.

Added

  • Declared mappings for tools/call, the COAZ-MCP binding's x-authzen-mapping. authzen.ParseMapping reads one and compiles its CEL up front, so a mapping that cannot work is refused when it is loaded rather than on the first call; authzen.MappingFromInputSchema finds one in a tool's inputSchema. A catalog returns it in mcp.Facts.Mapping and the enforcer asks the PDP what the mapping constructs instead of the default mapping. Both envelopes are supported: evaluations becomes one Access Evaluation request per entry, and the call is allowed only if every one is.
  • The identities in a declared mapping's request come from the chain, not the mapping. subject.id is the sponsor, subject.properties.iss the sponsor's issuer, context.agent the acting agent, and the verified chain is in context["mandatum.delegation"]; a mapping that sets any of them to something else is refused. The binding lets a mapping override subject.id for deployments that cannot carry the user in a token. A chain always carries the user, so here the override would only ever let the server being authorized say whose call it is. The divergence is in the conformance notes.
  • A declared mapping changes the question the PDP is asked and nothing else. The chain's own grant is still checked first, against the deployment's description of the tool, so a server cannot declare its way past what the sponsor delegated; TestADeclaredMappingCannotWidenTheGrant asserts the PDP is not asked.
  • mcp.StageMapping, the stage that refuses a call whose mapping could not produce a request. It runs between the grant and the PDP, and the PDP is not asked.
  • Bounds on a mapping's expressions, which are written by the server being authorized and run on every call to its tool: a CEL cost limit, authzen.MaxExpressionCost, and a separate bound on what they produce for one call, authzen.MaxResolvedValues and authzen.MaxResolvedBytes. The second exists because the first does not cover it — CEL charges for referencing a list, not for its length, so an expression mapping one argument's list onto another's evaluated well under the cost limit while allocating tens of gigabytes. Found in review before merge; TestAnAmplifyingExpressionIsStopped is the case.
  • FuzzDeclaredMapping, which holds the anchoring above as an invariant over arbitrary mappings. The nightly campaign picks it up with the others.
  • The module's first dependency, CEL (cel.dev/cel-go). The binding has no expression-free conformance level. It is imported by pkg/authzen alone; the verification core still depends only on the standard library, and the supply-chain notes say how to check that.
  • An interoperability test against three independent PDPs: Open Policy Agent through its contrib AuthZEN proxy, Cerbos, and OpenFGA. This is the v0.2 gate. One scenario, run through the enforcement point with the stock client, covers both the default and a declared mapping. Every refusal in it has to come from the PDP reading one specific field. The PDPs run from pinned, digest-checked releases on every pull request that touches the client or the enforcement point. Per PDP, only the policy file differs. test/interop/README.md records what the run found:
    • Cerbos and OpenFGA do not echo X-Request-ID, which the Authorization API requires, so the client is configured not to send one.
    • Cerbos policies cannot read the request context, so behind Cerbos nothing can be decided by the acting agent.

Changed

  • mcp.Authorized.Decision is now Decisions, one per evaluation the mapping constructed, in order. There is still exactly one unless the tool declares an evaluations mapping, so Decisions[0] is what Decision was.
  • The conformance notes record a divergence that was there before this change and was not written down: the middleware answers every refusal with code 403, where the binding specifies -32602, -32001 and -32603 by kind.

Known limitations

What 0.1.0 listed and this release does not fix still stands: no audit log, an in-process sequence store only, nothing publishing a revocation set on a schedule, no third-party security review, and rule V7 unreachable through the public API. Two of its limitations are gone, declared mappings and an untested PDP binding. What changed or is new:

  • Only tools/call is mapped to AuthZEN. Declared mappings now apply to it. Every other MCP method is still forwarded unexamined, which diverges from the binding's deny-by-default over unmapped methods. The middleware does not learn mappings from tools/list responses, so a deployment supplies them through its catalog. See docs/spec/coaz-mcp-conformance.md.
  • Two of the three tested PDPs need request correlation switched off. Cerbos and OpenFGA do not echo X-Request-ID, and the client refuses an answer it cannot tie to its question. Against them, authzen.WithRequestID(func() string { return "" }) is required, and the PEP and PDP logs then cannot be joined on a request identifier.
  • Behind Cerbos, nothing can be decided by the acting agent. Cerbos policies cannot read the evaluation context, which is where context.agent and the verified chain go. A declared mapping can project the agent into subject or resource properties, where Cerbos can read it. The default mapping does not do that.
  • Refusals carry code 403, not the binding's codes. The binding specifies -32602, -32001 and -32603 by kind. The middleware's reason is recorded where the constant is defined.

Verifying this release

cosign verify-blob \
  --bundle checksums.txt.bundle \
  --certificate-identity-regexp 'https://github.com/kanywst/mandatum/.github/workflows/release.yml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt

sha256sum -c checksums.txt

An SPDX SBOM is attached. Build provenance is attested and can be checked with gh attestation verify.

The full changelog, including which Delegation Assertion format versions every release accepts and issues, is in CHANGELOG.md.

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 18 Sep 17:22
26227db

What's Changed

  • fix(release): write a Sigstore bundle, not a detached sig and cert by @kanywst in #5
  • release: 0.1.0-rc.2 by @kanywst in #6
  • feat(authzen): bind a verified chain to the Authorization API by @kanywst in #7
  • feat(sequence): evaluate constraints across a chain's action history by @kanywst in #8
  • docs: RFC 8693 is not silent about this, and we said it was by @kanywst in #9
  • docs: write the two documents that were referenced as if they existed by @kanywst in #10
  • ci: refuse a document that references a document nobody wrote by @kanywst in #11
  • docs: the threat model and security-insights described an older project by @kanywst in #12
  • docs: cover the OAuth Actor Profile drafts, and drop a claim that was wrong when written by @kanywst in #13
  • Enforce the capability set, and stop claiming properties nothing checks by @kanywst in #14
  • chore(ci): bump anthropics/claude-code-action from 1.0.216 to 1.0.222 in the actions group by @dependabot[bot] in #18
  • The COAZ-MCP binding does not say the things we attributed to it by @kanywst in #15
  • The documents described a pipeline that is not the one running by @kanywst in #20
  • Four external citations pointed at documents that do not contain them by @kanywst in #16
  • Claims with no source behind them, and drafts that have moved by @kanywst in #17
  • Pre-releases were published as releases, and processes described a project with more people in it by @kanywst in #21
  • A quotation missing four words, and a translation stating an obligation by @kanywst in #22
  • The sign-off check leaves every Dependabot pull request red by @kanywst in #23
  • chore(ci): bump actions/upload-artifact from 5.0.0 to 7.0.1 by @dependabot[bot] in #19
  • A chain could be re-targeted; verification read claims nobody signed; and two specified mechanisms did not exist by @kanywst in #24
  • A gate that cannot be met, a date that moved, and two releases nobody wrote down by @kanywst in #25
  • Section 6 had no rule 7, and three documents said it did by @kanywst in #26
  • docs: a log nothing writes to, and a date that stopped being true two commits ago by @kanywst in #28
  • feat(mcp): the enforcement point, in the order the specification requires by @kanywst in #29
  • fix(ci): the nightly campaign fuzzes a hand-written list, and a target is not on it by @kanywst in #27
  • fix(revoke): a published set could make every authorization cost seconds by @kanywst in #31
  • docs: two more dates that tracked edits rather than claims by @kanywst in #33
  • feat(release): the release notes are the changelog entry, not a pointer to it by @kanywst in #32
  • docs: cut the 0.1.0 changelog entry by @kanywst in #30

New Contributors

Full Changelog: v0.1.0-rc.1...v0.1.0

v0.1.0-rc.1

v0.1.0-rc.1 Pre-release
Pre-release

Choose a tag to compare

@kanywst kanywst released this 16 Sep 15:09