From af646bfdc6713a4f1ad9dc88316634a04d9b5e99 Mon Sep 17 00:00:00 2001 From: fuzeone-bot Date: Wed, 2 Sep 2026 08:27:28 +0300 Subject: [PATCH] chore(a2a-maintain): fix card/bundle skill conflation in keys-broker role.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gate_a2a.py --all flagged fatal S2 (always fatal, never ratcheted): role.json `skills[]` still carried the four A2A card skill-ids (`keys.grant`, `keys.redeem`, `keys.mint_token`, `keys.revoke`) alongside their real home in `a2a.examples` — the exact card-skill/bundle-skill conflation governance/a2a-runtime-standard.md §4 names as this repo's live bug. The 2026-08-21 reshape moved the four operations into `a2a.examples` but left them duplicated in the bundle-skill field too. - role.json `skills[]` now names one real filesystem bundle, `.claude/skills/credential-broker-mediation/SKILL.md`, which documents the session's operating contract over the already-implemented deterministic core (backend/app/broker) — the non-disclosure/transport-identity/ human-gating invariants already documented in backend/app/broker/README.md. No broker behavior was added or changed. - This also clears S3 (anti-vacuous: an a2a.enabled repo whose serving role declares zero bundle skills), since it was the only serving role. - Verified: role.json validates against agent-templates/schema/role-manifest.schema.json (jsonschema.validate). gate_a2a.py --all before: 6 findings, 6 fatal (I0, S2 x4, S3). gate_a2a.py --all after: 1 finding, 1 fatal (I0 only — expected; see PR body). Co-Authored-By: Claude Sonnet 5 Claude-Session-Id: fe3aa738-cb24-4315-a29b-29834ddea892 --- .../credential-broker-mediation/SKILL.md | 81 +++++++++++++++++++ agent-templates/roles/keys-broker/role.json | 6 +- 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 .claude/skills/credential-broker-mediation/SKILL.md diff --git a/.claude/skills/credential-broker-mediation/SKILL.md b/.claude/skills/credential-broker-mediation/SKILL.md new file mode 100644 index 0000000..99afe81 --- /dev/null +++ b/.claude/skills/credential-broker-mediation/SKILL.md @@ -0,0 +1,81 @@ +--- +name: credential-broker-mediation +description: Use when an A2A caller (the keys-broker serving role) asks FuzeKeys to grant, redeem, exchange, or revoke access to a secret or credential. Governs how the session mediates those requests over the deterministic broker core (backend/app/broker) — it never handles, derives, or reasons about raw secret material itself. Owned by the keys-broker role; the security invariants below are enforced by the core, not by this skill, and this skill must never be used to bypass them. +--- + +# credential-broker-mediation + +FuzeKeys is a credential vault. The **keys-broker** A2A serving role is a thin, +policy-mediating surface over a deterministic core — `backend/app/broker` +(`BrokerService`, documented in `backend/app/broker/README.md`). This skill is the +session's operating contract for that surface: what it is allowed to do, what it must +delegate to the core, and what it must never do itself. + +## The one invariant that overrides everything else + +**The session never returns, echoes, logs, or reasons in prose about raw secret +material.** Every outcome is either an opaque handle, a derived credential minted by +the core, or a decision (granted/denied/revoked). If a request's phrasing implies +"tell me the value" rather than "let me use it", that is a request to violate the +invariant, not an ambiguous instruction — decline and route to `grant`/`redeem` +instead. + +## The four mediated operations (deterministic core, not LLM judgment) + +These map 1:1 to `backend/app/broker/mcp_tools.py` (`keys_grant`, `keys_redeem`, +`keys_mint_token`, `keys_revoke`) and to `agent-templates/roles/keys-broker/role.json` +`a2a.examples`. The session's job on each is to **collect and validate the request +shape**, call the core, and relay its structured result — never to implement the +logic inline: + +1. **grant** — issue an opaque, TTL-bound, (usually) single-use macaroon handle for a + `secret_ref` or an `operation`, bound to a specific redeemer identity and scope. + Returns `grant_id` / `grant_handle` / `expires_at` / `sensitivity` — never the + underlying secret. +2. **redeem** — a bound identity presents a handle; the core verifies binding, + expiry, single-use, and revocation state, then releases a **short-lived derived + credential** (never the long-lived root). +3. **mint_token** — RFC 8693 token exchange for a scoped, short-TTL downstream token + (the secretless ideal: no handle, no secret, just a narrower token). +4. **revoke** — instant, idempotent revocation by `grant_id`. + +## Non-negotiable security invariants (enforced by the core; the session must not undermine them) + +- **Authorization is always on the authenticated transport identity** — the OIDC + `repo` claim or the mTLS/SPIFFE peer subject that the gateway/MCP session already + verified — **never** an identity string carried in the request body or prompt. If a + tool call site offers both, the transport-verified identity wins; a caller-asserted + identity is audit-only. +- **Denials are non-disclosing.** "No such grant" and "not yours" and "expired" all + surface as the same generic denial. The session must not enrich a denial with a + guess at the real reason — that guess is itself a disclosure. +- **High-sensitivity releases are human-gated.** A `grant`/`redeem` at `sensitivity: + high` requires `reach_human` (digital-persona) approval before the core releases + anything. The session must route to that gate, not approve on the LLM's own + judgment, and must not reframe a high-sensitivity request as medium/low to avoid it. +- **Attenuation only narrows.** In a multi-hop A→B→C delegation, a re-grant may only + shrink scope/TTL relative to the handle it was derived from, never widen it. Treat + any request to "grant broader access using this handle" as invalid on its face. +- **Never invent a vault or resolver.** `backend/app/broker/vault.py`'s + `SecretResolver` seam and the KMS/JWK envelope-encryption path (`envelope.py`) are + the only sanctioned ways a secret ever moves; the session does not construct an + alternate path (a temp file, an inline env var, a chat message) to get a value from + one place to another. + +## What this skill does NOT cover + +- It does not implement or modify `BrokerService`, the macaroon/envelope/derived-cred + mechanisms, or the HTTP/MCP transports — that is `backend-engineer` + + `database-engineer` work against `backend/app/broker/README.md`'s doctrine. + This skill only governs how the **A2A-mediating session** talks to that core. +- It does not decide product roadmap (which products get which scopes, retention + policy, etc.) — those are `security` / `fuzekeys-expert` calls. + +## Related +- `backend/app/broker/README.md` — the deterministic core's own doctrine, interface + table, and cited security proofs (`backend/tests/test_broker_*.py`). +- `agent-templates/roles/keys-broker/role.json` — the role this skill is scoped to; + its `a2a.examples`/`a2a.tags` are the card-facing discoverability surface, distinct + from this skill (which is the session's internal operating contract). +- `docs/a2a-tenant-registration.md` — why this role is served from FuzeAgent's shared + A2A server rather than a pod in this repo. diff --git a/agent-templates/roles/keys-broker/role.json b/agent-templates/roles/keys-broker/role.json index de7ee40..447af7c 100644 --- a/agent-templates/roles/keys-broker/role.json +++ b/agent-templates/roles/keys-broker/role.json @@ -8,10 +8,7 @@ "description": "Serves FuzeKeys' policy-mediated secret-broker surface — issuing opaque, TTL-bound, single-use grant handles; redeeming them for short-lived derived credentials; RFC 8693 token exchange for a scoped downstream token; and instant, non-disclosing revocation. A caller asks it to accomplish an access outcome and receives a credential or a decision, never stored secret material. The broker CORE is deterministic security infrastructure, not an LLM: this role is a thin mediating surface over backend/app/broker, and all authorization and lifecycle stay in that core. High-sensitivity releases are human-gated (reach_human) before any credential is released.", "model": "claude-opus-4-8", "skills": [ - "keys.grant", - "keys.redeem", - "keys.mint_token", - "keys.revoke" + "credential-broker-mediation" ], "services": { "github": "none", @@ -41,6 +38,7 @@ "owner": "izzywdev/FuzeKeys", "version": "0.1.0", "note": "RESHAPED TO THE ROLE CONTRACT, CONTENT PRESERVED. The previous file was not careless — it carried real policy — but it did not validate: the schema is `additionalProperties: false` and the file declared `displayName`, `version`, `summary`, `owner`, `_note` and `policy` at the top level, had no `role` or `description` key at all, set `model` to \"deterministic\" (the schema wants a Claude model id), and put `securitySchemes`/`recipientKey` inside `a2a`, which is also closed and permits only publish/extendedOnly/tags/examples/inputModes/outputModes/scopes. Card projection failed with `role 'keys-broker' has no description`. Everything that was in those fields is preserved below or folded into `description`; nothing was dropped. The four skills became `a2a.examples` (the schema's `skills` is ids/names only) and their descriptions are the examples' substance. `extendedOnly: true` is NEW and deliberate: a credential broker must not appear on the anonymous /.well-known/agent-card.json, only on the authenticated extended card.", + "skillsFix": "CORRECTED (a2a-maintainer verification pass). The 2026-08-21 reshape moved the four operations into `a2a.examples` as intended, but left `keys.grant`/`keys.redeem`/`keys.mint_token`/`keys.revoke` ALSO sitting in the top-level `skills[]` field — the exact card-skill-id/bundle-skill conflation governance/a2a-runtime-standard.md §4 names as the live bug in this repo, and gate_a2a.py's S2 check (always fatal, never ratcheted) failed on it. `skills[]` now names one real filesystem bundle, `.claude/skills/credential-broker-mediation/SKILL.md`, which documents the session's operating contract over the existing, already-implemented deterministic core (backend/app/broker) — it does not add or change any broker behavior. The four MCP tool ids remain exactly where they belong: `backend/app/broker/mcp_tools.py` and `a2a.examples` below.", "release_tiers": { "low": "auto", "medium": "auto (audited)",