feat: enforce capability ceiling at ability-backed tool execution (Stages B+C)#414
Merged
Merged
Conversation
…tomattic#412 B+C) Resolve each ability-backed tool's required capability declaratively from the tool declaration and gate dispatch through the existing WP_Agent_WordPress_Authorization_Policy::can() before the ability runs. - WP_Agent_Tool_Declaration: add required_capability field + resolver; validate/normalize on server declarations (absent = no gate). - WP_Agent_Ability_Tool_Executor: consult the authorization policy before dispatch; deny with an auditable tool-error result when the principal's capability ceiling does not permit the capability. - WP_Agent_Conversation_Loop: thread the resolved execution principal into the run context so it reaches tool execution. Enforcement depends only on the ceiling being present on the principal, so it works with either an explicit ceiling or the future autonomous default. Part of Automattic#412 (Stages B+C).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #412 (Stages B+C).
Summary
Wires the existing enforcement engine into ability-backed tool execution so an ability whose required capability is not permitted by the execution principal's capability ceiling is denied before it runs. No new authorization machinery is introduced — the gap was purely that
WP_Agent_WordPress_Authorization_Policy::can()was wired tocan_access_agentbut not to per-ability tool execution.Stage B — Resolving the required capability (declarative)
A substrate-recognized, product-neutral convention for a tool declaration to name its required capability:
WP_Agent_Tool_Declaration::REQUIRED_CAPABILITY = 'required_capability'— a top-level declaration field.WP_Agent_Tool_Declaration::requiredCapability( array ): string— the single resolver (trimmed capability name, or''= no gate). Reads from normalized or raw declarations.normalizeForServer()now validates/normalizes the field: a non-empty string is preserved; non-string/empty values are dropped. The declaration envelope stays clean.This deliberately avoids per-consumer tool-name special-casing. The capability vocabulary is owned by WordPress core and the host; the substrate only carries the field and asks the policy. Client-executed tools keep the field via the existing extension-field preservation, but the host can only enforce it for host/ability tools (which is where it matters).
Stage C — Enforcement
The hook lives in
WP_Agent_Ability_Tool_Executor::executeWP_Agent_Tool_Call()(src/Tools/class-wp-agent-ability-tool-executor.php), right after the ability name is resolved and beforeWP_Agent_Ability_Dispatcher::dispatch():required_capabilityfrom the tool declaration.context['authorization_policy']if it implementsWP_Agent_Authorization_Policy, otherwise the substrate defaultnew WP_Agent_WordPress_Authorization_Policy().context['principal'].$policy->can( $principal, $required_capability )— true iff the ceiling allows the cap anduser_can( $acting_user, $cap ).error_type => 'capability_denied') with adenialblock mirroring the consent-decision shape (allowed,operation,reason, audit-safeprincipalmetadata) plus redacted parameters. The model is told the tool was not permitted — it never receives the ability output.required_capabilityis declared, behavior is unchanged.Threading the principal
The principal is available on
WP_Agent_Conversation_Request::principal()but was not previously passed to the executor.WP_Agent_Conversation_Loop::run()now threads the resolved principal into the run context (context['principal']), which flowsturn context → tool context → executorvia the existing context-passing path. Denials fail closed: a tool that declares arequired_capabilitybut executes without a threaded principal is denied with reasonprincipal_unavailablerather than silently bypassing the gate.Works with either ceiling source
Enforcement depends only on the ceiling being present on the principal, not on how it was derived. With an explicit ceiling it intersects token restrictions ∩ WP user caps; with no ceiling restriction it falls back to pure WordPress
user_can; with no principal at all it fails closed. This means Stage A (the autonomous-default ceiling) can land separately and this enforcement picks it up automatically.Verify
composer smoke+composer phpstan(level max) are green. Newtests/ability-tool-ceiling-smoke.php(wired into thesmokescript) covers:normalizeForServervalidation (string preserved, non-string/empty dropped).capability_denied, reasoncapability_not_permitted, ability never dispatched.principal_unavailable(fail closed).Notes
CHANGELOG.md/version edits (homeboy owns those).