Skip to content

fix(subagent): enforce GrantKind::Tool TTL/revocation before dispatch#6625

Merged
bug-ops merged 1 commit into
mainfrom
fix/6567-tool-grant-enforcement
Jul 21, 2026
Merged

fix(subagent): enforce GrantKind::Tool TTL/revocation before dispatch#6625
bug-ops merged 1 commit into
mainfrom
fix/6567-tool-grant-enforcement

Conversation

@bug-ops

@bug-ops bug-ops commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Closes #6567

Summary

GrantKind::Tool capability grants were TTL-tracked in PermissionGrants identically to GrantKind::Secret, but no production call site ever checked one before dispatching a tool - a latent trap where the type and doc comments implied task-scoped enforcement that did not actually exist.

  • Shared PermissionGrants via Arc<Mutex<>> between SubAgentHandle and the spawned agent-loop task (AgentLoopArgs::tool_grants) - Secret grants are delivered per-key over a channel, but Tool grants need live shared state so revoke_all() (task completion/cancellation/drop) is visible without a delivery round-trip.
  • Added PermissionGrants::check_tool_grant() (NoGrant/Active/Expired) and enforce it as the first check in handle_tool_step, before any hook fires or the executor runs, mirroring the Secret path's rejection convention.
  • A loop-local active_tool_grants_seen set distinguishes "revoked/expired" from "never granted" after sweep_expired() evicts a stale Vec entry. Adversarial review caught two distinct instances of the same "deny-once-then-permit-forever" bypass this closes: (1) the rejection path was removing the name from the seen-set, undoing its own protection; (2) a grant already expired on its very first check never entered the seen-set at all. Both fixed; regression tests dispatch a third time after expiry/revocation to prove continued denial.
  • Semantics (explicit user sign-off, since the spec flagged this as an Ask-First security-policy decision): default-permit, time-box only - not an allow-list. Absence of a Tool grant always permits; a grant for one tool never restricts any other tool; matching is exact-name only (no prefix/glob for tool families). Documented at the enforcement point so a future caller expecting least-privilege semantics isn't misled.
  • No production caller creates a GrantKind::Tool grant yet (explicitly out of scope per the spec, deferred to a future product decision) - the enforcement point itself satisfies the acceptance criteria independent of any grant-creation API.

Spec: .local/specs/083-tool-grant-enforcement-gap/spec.md

Test plan

  • cargo +nightly fmt --check
  • cargo clippy --profile ci --workspace --all-targets --features "desktop,ide,server,chat,pdf,scheduler,testing" -- -D warnings
  • cargo nextest run --config-file .github/nextest.toml --workspace --features "desktop,ide,server,chat,pdf,scheduler" --lib --bins (15033 passed)
  • cargo test --doc --workspace --features "desktop,ide,server,chat,pdf,scheduler"
  • RUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace --features "desktop,ide,server,chat,pdf,scheduler"
  • gitleaks protect --staged --no-banner --redact
  • 9 new unit/integration tests covering active/expired/revoked/absent-grant dispatch, including third-dispatch-stays-denied regression coverage for both bug instances found in review
  • Live testing playbook + coverage-status.md updated (.local/testing/playbooks/subagent-tool-grant-enforcement.md) - no live CLI/TUI/Telegram scenario is possible yet since no production caller creates a Tool grant; coverage is unit-level, documented explicitly rather than skipped

@github-actions github-actions Bot added bug Something isn't working documentation Improvements or additions to documentation rust Rust code changes size/XL Extra large PR (500+ lines) core zeph-core crate and removed bug Something isn't working labels Jul 20, 2026
@bug-ops
bug-ops force-pushed the fix/6567-tool-grant-enforcement branch from 5fd362a to a56169d Compare July 21, 2026 00:03
@bug-ops
bug-ops enabled auto-merge (squash) July 21, 2026 00:03
@github-actions github-actions Bot added the bug Something isn't working label Jul 21, 2026
GrantKind::Tool grants were TTL-tracked in PermissionGrants identically
to GrantKind::Secret, but no production call site ever checked one
before dispatching a tool - a latent trap where the type implied
enforcement that did not exist.

Share PermissionGrants via Arc<Mutex<>> between SubAgentHandle and the
spawned agent-loop task (Secret grants are delivered per-key over a
channel; Tool grants need live shared state instead, since revoke_all()
must be visible without a delivery round-trip). Add
PermissionGrants::check_tool_grant() and enforce it as the first check
in handle_tool_step, before any hook fires or the executor runs.

A loop-local active_tool_grants_seen set distinguishes "revoked" from
"never granted" after sweep_expired() evicts a stale entry - without
it, a rejected dispatch would silently permit the very next one for
the same tool.

Confirmed semantics: default-permit, time-box only, not an allow-list -
absence of a grant always permits; a grant for one tool never restricts
another; matching is exact-name only.
@bug-ops
bug-ops force-pushed the fix/6567-tool-grant-enforcement branch from a56169d to 2568418 Compare July 21, 2026 00:09
@bug-ops
bug-ops merged commit a0dea8f into main Jul 21, 2026
43 checks passed
@bug-ops
bug-ops deleted the fix/6567-tool-grant-enforcement branch July 21, 2026 00:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working core zeph-core crate documentation Improvements or additions to documentation rust Rust code changes size/XL Extra large PR (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

research(subagent): Tool-kind capability grants are TTL-modeled but never enforced before dispatch

1 participant