Skip to content

Enforce capability ceiling at ability/tool execution — the Core-worthy boundary for autonomous writes (beneath #408/#411) #412

Description

@chubes4

Summary

The write-tool leakage that motivated #408/#411 is a symptom of a deeper architectural gap: agents-api has a complete capability-ceiling authorization primitive, but it is not enforced at ability/tool execution. For a Core-candidate substrate, capability-ceiling enforcement — not tool-list curation — is the correct security boundary for autonomous writes. This issue specs that enforcement.

What already exists (and works)

The substrate already ships the right primitives:

  • WP_Agent_Execution_Principal (src/Runtime/) — models who is acting: auth_source (user/agent_token/system/runtime/…), acting_user_id (0 = system/no-human), and carries a capability_ceiling.
  • WP_Agent_Capability_Ceiling (src/Auth/) — "the intersection of the acting user's WordPress capabilities and optional token/client capability restrictions." Has allows_capability(string $cap): bool and with_allowed_capabilities() to narrow.
  • WP_Agent_WordPress_Authorization_Policy::can($principal, $capability) (src/Auth/) — the correct enforcement function: returns true only if the ceiling allows the capability AND user_can($user_id, $capability). Textbook ceiling ∩ actual-WP-caps.

WordPress Core's Abilities API (WP_Ability::execute() in wp-includes/abilities-api/class-wp-ability.php) also already enforces each ability's own permission_callback before running it.

The gap

WP_Agent_WordPress_Authorization_Policy::can() is only wired to agent-level access (can_access_agent). It is not consulted at per-ability/tool execution. So:

  1. The tool executor runs an ability-backed tool without checking the principal's capability ceiling against the ability's required capability.
  2. Ability permission_callbacks, where they exist, check current_user_can(...) — which reflects the acting user's full caps, not a reduced autonomous ceiling. A pipeline running as an admin user therefore passes every write check.

This is exactly why the production incident happened: the pipeline principal was a privileged user, so nothing — not tool-listing, not permission_callback — distinguished "this admin is interactively editing" from "this admin's token is driving an unattended loop that shouldn't publish."

Proposed design (Core-worthy boundary)

1. Autonomous principals carry a reduced ceiling by default.
When a run is autonomous (auth_source ∈ {system, runtime, agent_token} and/or acting_user_id represents automation), the substrate should default its capability_ceiling to one that excludes content-mutating capabilities unless the host explicitly grants them via with_allowed_capabilities(). The ceiling primitive already supports this; what's missing is a safe default derivation for autonomous auth sources.

2. Enforce the ceiling at ability-backed tool execution.
Before executing an ability-backed tool, the tool executor consults WP_Agent_WordPress_Authorization_Policy::can($principal, $required_capability). This requires a way to know the ability's required capability — options:

  • Read it from the ability's declared metadata/category (e.g. abilities in a *-publishing/write class declare a required cap), or
  • Derive from the ability's permission_callback contract, or
  • A substrate convention: write-capable ability declarations name their required capability.
    On failure, the tool call is denied with an auditable reason (mirroring WP_Agent_Consent_Decision shape) — the model is told "not permitted," not silently handed the tool.

3. permission_callback honesty remains the consumer's job.
Abilities must declare truthful capability requirements (not permissive stubs). The substrate can't fix a lying permission_callback, but with (1)+(2) an honest one becomes sufficient and a reduced ceiling makes even a privileged acting-user safe in autonomous mode.

Relationship to #408 / #411

  • feat: safe-by-default tool curation — write-capable tools opt-in for autonomous principals #411 (tool-list curation) stays as defense-in-depth: don't even offer write tools to an autonomous agent. It reduces prompt surface and stops the model trying. But it must be documented as curation, not the security boundary, and it should key off the principal's autonomy (acting_user_id/auth_source), NOT a mode/interactive string — the substrate already models autonomy precisely; a mode string is a weaker proxy.
  • This issue is the actual enforcement boundary beneath it.

Why this matters for Core

Shipping a tool-list curation gate as if it were the security boundary would give Core sites a false sense of safety: any consumer that offers a write tool by omission, or runs as a privileged user, is exposed. The capability-ceiling model is WordPress-native (it composes with user_can/map_meta_cap), already half-built in the substrate, and is the honest boundary. Getting this right before Core alignment is the point.

Scope note

This is a design/architecture issue. Implementation should be staged: (a) safe default ceiling derivation for autonomous auth sources, (b) ability→required-capability resolution, (c) enforcement hook at tool execution, (d) audit/telemetry. Each is separately reviewable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions