Skip to content

fix(agent-mesh): preserve full sub-resource path in capability parsing#3246

Draft
liamcrumm wants to merge 2 commits into
mainfrom
fix/capability-4segment-escalation
Draft

fix(agent-mesh): preserve full sub-resource path in capability parsing#3246
liamcrumm wants to merge 2 commits into
mainfrom
fix/capability-4segment-escalation

Conversation

@liamcrumm

Copy link
Copy Markdown
Contributor

Summary

CapabilityGrant.parse_capability truncated the qualifier to a single segment, so a leaf capability grant authorized its parent and its siblings. This preserves the full sub-resource path so a narrow grant only authorizes what it names. Closes #3180.

Problem

parse_capability did qualifier = parts[2] if len(parts) > 2 else None, discarding parts[3:]. A grant scoped to write:database:table_users:row_1 was stored and compared as write:database:table_users, so it authorized:

  • its parent write:database:table_users, and
  • its siblings write:database:table_users:row_2.

This is the 4+-segment variant and is independent of #3176 (which fixed the 3-segment matcher under-restriction). It lives in the parser, not the matcher, and reproduced on the pulled tree.

Reproduction (before -> after)

For a write:database:table_users:row_1 grant:

Check Before After
exact leaf write:database:table_users:row_1 True True
parent write:database:table_users True False
sibling write:database:table_users:row_2 True False

Security model (blast radius)

This is a capability data-model change; the chosen approach and its blast radius:

  • Approach (option a): parse_capability now preserves the full remainder via qualifier = ":".join(parts[2:]). matches() is unchanged because it already treats qualifier as an opaque exact-match token, and the broad-satisfies-narrower direction is handled by the separate requested.startswith(capability + ":") branch (which reads the untruncated capability string).
  • Consistency hardening: a Pydantic model_validator(mode="before") re-derives action/resource/qualifier from capability on every construction and revalidation (including model_validate/deserialization). Without it, a grant built as CapabilityGrant(capability="a:b:c:d", qualifier="c") would keep a stale truncated qualifier and re-open the escalation, since matches() trusts the stored qualifier. No such path exists in the tree today, but this makes the model correct-by-construction and satisfies the "no half-migrated comparison paths" requirement.
  • Grammar decisions (pinned by tests): only whole-segment * (action/resource) and a trailing :* are wildcards. A * in the middle of the remainder (e.g. write:database:*:row) is now a literal qualifier segment (stricter / fail-closed vs the old accidentally-permissive parse). Empty/trailing-segment behavior (write:database:) is pinned.
  • Rejected: option (b) fail-closed reject of >3-segment grants (functional regression: removes deep-capability granting instead of making it correct); and rewriting matches() to a variable-length component list (larger blast radius on the security-critical matcher, no behavioral gain).
  • Out of scope: integrations/mcp/__init__.py::_check_capability is a separate, stricter matcher (exact + trailing wildcard only), never calls parse_capability, and is not vulnerable.

Maintainer decision points

Changes

File What changed
agent-governance-python/agent-mesh/src/agentmesh/trust/capability.py parse_capability preserves full remainder; add model_validator re-deriving components from capability; create() no longer passes derived fields; docstrings updated
agent-governance-python/agent-mesh/tests/test_coverage_boost.py New TestCapabilityFourSegmentEscalation matrix: exact leaf / parent / siblings, deeper nesting, resource_ids, delegation (require_grantor_capability), registry check(), get_capabilities/deny, wildcard + empty-segment grammar pins, validator self-heal, retained #3176 regressions
docs/specs/AGENTMESH-TRUST-COORDINATION-1.0.md sec 8.2/8.5: qualifier is the full sub-resource path compared as one opaque token; conformance sec 8.1-8.3 unaffected

Testing

  • Repro shows parent/sibling checks flip True -> False while exact-leaf stays True.
  • pytest full agent-mesh suite: 3424 passed, 73 skipped; the only failures are 4 pre-existing ModuleNotFoundError: agentrust_trace env failures in tests/governance/test_trace_sink.py, unrelated to this change.
  • New matrix + test_trust.py + test_spec_mesh_trust_conformance.py: all green; fix(capability): reject narrow grants satisfying broad capability checks #3176 and spec S8.1-S8.3 preserved.
  • ruff check --select E,F,W --ignore E501 clean on changed lines.
  • Docs link check: 0 new broken links.

CapabilityGrant.parse_capability truncated the qualifier to a single
segment (parts[2]), silently dropping parts[3:]. A leaf grant such as
write:database:table_users:row_1 was stored/compared as
write:database:table_users, so it authorized its parent
(write:database:table_users) and its siblings
(write:database:table_users:row_2) -- a privilege escalation (#3180).

This is the 4+-segment variant and is independent of #3176 (which fixed
the 3-segment matcher under-restriction); it lives in the parser.

Fix:
- parse_capability now preserves the full remainder:
  qualifier = ":".join(parts[2:]). matches() needs no change because it
  already exact-matches qualifier and handles broad->narrow via the
  requested.startswith(capability + ":") branch.
- Add a model_validator(mode="before") that re-derives
  action/resource/qualifier from `capability` on every construction and
  revalidation, so a grant deserialized/constructed with a stale
  truncated qualifier cannot re-open the escalation. create() no longer
  passes the derived fields.
- Spec AGENTMESH-TRUST-COORDINATION-1.0 sec 8.2/8.5 updated: qualifier is
  the full sub-resource path compared as one opaque token.

Behavior (before -> after) for a write:database:table_users:row_1 grant:
- exact leaf  : True  -> True
- parent      : True  -> False
- sibling     : True  -> False

Tests: dedicated 4+-segment matrix in test_coverage_boost.py (exact leaf,
parent, siblings, deeper nesting, resource_ids, delegation, registry
check, get_capabilities/deny, wildcard + empty-segment grammar pins,
validator self-heal) plus retained #3176 regressions. Full agent-mesh
suite green (pre-existing agentrust_trace env failures unrelated).

Out of scope: integrations/mcp/_check_capability is a separate, stricter
matcher (exact + trailing wildcard) and is not vulnerable.

Closes #3180

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests agent-mesh agent-mesh package labels Jul 2, 2026
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ⚠️ Missing No current-run comment
🛡️ Security Scan ⚠️ Missing No current-run comment
🔄 Breaking Changes ⚠️ Missing No current-run comment
📝 Docs Sync ⚠️ Missing No current-run comment
🧪 Test Coverage ⚠️ Missing No current-run comment

Verdict: ⚠️ AI review incomplete; ready for human review

AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions github-actions Bot added the size/L Large PR (< 500 lines) label Jul 2, 2026
…rity audit doc

Follow-up hardening from a deep-code-review pass on the #3180 fix:

- Switch CapabilityGrant's derive validator from mode="before" to
  mode="after" and enable ConfigDict(validate_assignment=True). The
  previous mode="before"/dict-only guard left action/resource/qualifier
  stale when a grant was built from a non-dict mapping (UserDict) or
  mutated via attribute assignment, so matches() could re-authorize the
  parent of a leaf grant. action/resource now carry defaults and are
  always re-derived from `capability`. Documented residual: pydantic
  model_copy(update=) does not run validators; callers must use
  create() to re-scope.
- Add docs/security/audits/2026-07-02-capability-subresource-path-
  preservation.md, required by scripts/ci/security-audit-required.sh for
  any change under agentmesh/trust/ (matches the #3176 precedent).
- Add regression tests for the reassignment and non-dict-mapping
  re-heal paths.

Full agent-mesh suite: 3426 passed, 73 skipped (4 pre-existing
agentrust_trace env failures unrelated). ruff clean on changed source.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>
@github-actions github-actions Bot added the security Security-related issues label Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-mesh agent-mesh package documentation Improvements or additions to documentation security Security-related issues size/L Large PR (< 500 lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CapabilityGrant.parse_capability truncates 4+ segment capabilities, enabling parent/sibling privilege escalation

1 participant