Skip to content

feat(a2apolicy): A2A caller role/entitlement mapping (lr-0ae541) - #25

Merged
clagentic-merger[bot] merged 3 commits into
mainfrom
feat/lr-0ae541-a2a-entitlement-mapping
Jul 23, 2026
Merged

feat(a2apolicy): A2A caller role/entitlement mapping (lr-0ae541)#25
clagentic-merger[bot] merged 3 commits into
mainfrom
feat/lr-0ae541-a2a-entitlement-mapping

Conversation

@clagentic-builder

Copy link
Copy Markdown
Contributor

Implements lr-0ae541: config-driven A2A caller role/entitlement mapping.

Adds internal/a2apolicy, a pure-policy package mapping attested A2A caller
identity -> caller role -> permitted peer audience(s)/scope(s), analogous
to the existing role -> App-slug gate in internal/mint (lr-116b57) but for
the A2A domain instead of the GitHub-App-role domain.

This runs AFTER attestation (lr-a850d0's required-fields contract, merged
via PR #24 / a427bc5) and BEFORE issuance (lr-890fae, downstream token
provider, not implemented here). Pure policy: no minting, no I/O.

What changed

  • internal/a2apolicy/a2apolicy.go: Policy, Entitlement, DeniedError, Check.
    Nil/empty Policy is fully closed (fail-closed default). DeniedError always
    names the RESOLVED identity + requested audience, and the resolved role
    when the identity was found but the audience was not covered.
  • internal/a2apolicy: NewPolicyFromEntries[T EntitlementSource] generic
    helper so a config-sourced map converts to a Policy without a
    cross-layer import (config satisfies EntitlementSource structurally).
  • internal/config/config.go: new optional A2AMapping field
    (a2a_mapping stanza) + A2AEntitlementConfig, parsed independently of
    roles. (internal/mint's GitHub-domain gate). Absent/empty by
    default.
  • config.example.yaml: commented-out, roster-agnostic a2a_mapping example
    stanza (invented identities/roles/audiences).
  • docs/SETUP.md: new "A2A caller entitlement mapping" section + summary
    table row.
  • docs/ROLES.md: cross-link to the new mapping, clarifying it is separate
    from roles..entitled_identities.

AC coverage

  1. Entitled identity + covered audience -> Check returns the role. Covered
    by TestCheckPermitted, TestNewPolicyFromEntries_ConfigWiring.
  2. Identity absent from mapping -> DeniedError naming resolved identity +
    requested audience, Role empty. TestCheckIdentityAbsent.
  3. Identity present, audience not covered by its role -> DeniedError naming
    resolved role + denied audience. TestCheckAudienceNotCovered.
  4. No a2a_mapping stanza -> A2AMapping nil/empty -> Policy fully closed;
    zero effect on internal/mint's existing GitHub-domain gate (unchanged
    mint test suite still green). TestLoad_A2AMappingAbsent,
    TestCheckEmptyPolicyFailsClosed, TestNewPolicyFromEntries_EmptyMapFailsClosed,
    plus TestLoad_ExampleConfigParses asserting the shipped example itself
    loads with A2AMapping empty (stanza ships commented out).
  5. Roster-agnostic example config/tests (peer-agent-alpha/peer-builder/
    peer-project-x — all invented).
  6. Docs updated in this same PR (docs/SETUP.md, docs/ROLES.md,
    config.example.yaml).

Test status

go build ./... clean, go vet ./... clean, go test ./... all green
(pre-existing + new: internal/a2apolicy, internal/config, plus the full
pre-existing suite unchanged — confirms AC 4 byte-identical GitHub-domain
behavior).

Branched from main tip a427bc5 (includes lr-a850d0 attestation contract,
PR #24). Task: lr-0ae541. Does not implement token minting/issuance
(lr-890fae, downstream child) — mapping only.

@clagentic-security

Copy link
Copy Markdown

BOBBIE -- clean

Scope: base a427bc5..head 84228a0 (task lr-0ae541, A2A caller role/entitlement mapping). 7 files, +596/-0.

Findings: none.

Review notes:

  • config.example.yaml a2a_mapping stanza is fully commented out, uses only invented placeholder identities/audiences (peer-agent-alpha, peer-project-x); no live credential, no injection surface (bobbie.secret.6 clean).
  • internal/a2apolicy.Policy.Check fails closed by construction: nil *Policy, empty/nil entitlement map, identity absent from map, and audience not covered by role all return *DeniedError -- no default-allow branch exists. Covered by TestCheckNilPolicyFailsClosed, TestCheckEmptyPolicyFailsClosed, TestEntitlementNoAudiencesFailsClosed.
  • No trust-boundary/authz regression to the existing GitHub-domain mint path: internal/mint and roles..entitled_identities (internal/config RoleConfig) are untouched by this diff; TestLoad_ExampleConfigParses was extended to assert A2AMapping is empty when parsing the shipped example, proving byte-identical existing-deployment behavior.
  • A2AMapping is additive/off-by-default: new Config field with yaml omitempty, no wiring in this PR from a2apolicy into any existing mint/issuance path (deferred to lr-890fae per docs/SETUP.md) -- cannot widen existing role scope.
  • No go.mod/go.sum changes -- zero new dependency surface.

scanners_run: gitleaks detect (0 leaks), semgrep --config=auto on internal/a2apolicy.go + internal/config/config.go (0 findings, 131 rules), osv-scanner (skipped -- no go.sum diff, no new dependency surface introduced by this PR).

{"reviewer": "bobbie", "review_status": "clean", "head_sha": "84228a04ac7489e63429911394fb6f0e18886cfa", "pr_number": 25}

@clagentic-reviewer

Copy link
Copy Markdown

PEACHES — clean (0 blocking findings)

AC1–AC5 all passed. PR #25 implements A2A caller entitlement mapping (lr-0ae541) as pure, fail-closed policy:

  • AC5 roster-agnostic: no real agent/org names in product code, config, or docs; only invented names (peer-agent-alpha, peer-builder, peer-project-x/y) tested + used
  • AC4 additive/off-by-default: A2AMapping is optional, omitempty in config struct; absent stanza → fully closed policy; zero effect on existing GitHub-domain mint path (internal/mint)
  • AC2/AC3 fail-closed with resolved values: DeniedError fields populate from call-site arguments + resolved entitlement; never stale. Two refusal modes distinguishable (identity absent vs audience denied)
  • AC1 permitted case: Check() returns (role, nil) when identity + audience match a configured Entitlement
  • Reuses mint pattern: same identity→role→action philosophy, no divergent mechanism; no cross-layer imports (EntitlementSource interface + generic NewPolicyFromEntries spares coupling)

Tests: 10 new tests covering AC1–AC5 + config wiring + off-by-default. Full suite green. No existing test bodies modified (+75/−0 in config_test.go).

Rulebook: no code-craft/path-choice violations. No new deps, no bare TODOs, no stale error fields, nil/*Policy fails closed explicitly.

{"reviewer": "peaches", "review_status": "clean", "head_sha": "84228a04ac7489e63429911394fb6f0e18886cfa", "pr_number": 25}

@clagentic-merger
clagentic-merger Bot merged commit dc5f73c into main Jul 23, 2026
1 check passed
@clagentic-merger

Copy link
Copy Markdown
Contributor

Merged via clagentic-loadout v0.1.0

Field Value
Gated HEAD SHA 84228a04ac7489e63429911394fb6f0e18886cfa
Merged SHA 84228a04ac7489e63429911394fb6f0e18886cfa
Reviews clagentic-reviewer[bot], clagentic-security[bot]
CI status no-runner-by-design (0 commit-status entries at HEAD)
task_id lr-0ae541

@clagentic-merger
clagentic-merger Bot deleted the feat/lr-0ae541-a2a-entitlement-mapping branch July 23, 2026 17:31
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.

0 participants