Summary
The AgentAuthorize consent screen currently offers a binary choice: Authorize (grants a token with the owner's full capability ceiling) or Deny. The underlying AgentTokens::create_token() already accepts a $capabilities argument that narrows what a token can do, but the browser consent flow hardcodes null:
https://github.com/Extra-Chill/data-machine/blob/main/inc/Core/Auth/AgentAuthorize.php#L326-L332
\$result = \$tokens_repo->create_token(
(int) \$agent['agent_id'],
\$agent['agent_slug'],
\$token_label,
null, // All capabilities.
null // No expiry.
);
This is fine for same-owner / same-site flows, but it's the wrong default for cross-site agent auth. When Franklin on site A authorizes as chubes-bot on site B, Franklin gets everything chubes-bot's owner can do on site B, forever. Least privilege is not on the table from the UI — only from direct CLI token minting.
This issue tracks surfacing scope selection in the consent screen itself.
Why it matters
- Cross-site agent auth is where the pattern becomes dangerous by default. A compromised agent runtime on one site gets full owner access on every other site it has been authorized for.
- Application Passwords in WP core have the same limitation — "all or nothing" scope. Fixing this in DM is also a chance to ship the pattern WP core should have.
- The infrastructure is already there:
datamachine_agent_tokens.capabilities is a JSON-capable TEXT column, AgentAuthMiddleware already reads and applies it via PermissionHelper::set_agent_context(). Only the UX is missing.
Proposed scope model
Rather than asking humans to tick 400+ raw WordPress capabilities, express scopes as ability-category allowlists plus optional per-ability fine-grain. This maps to how pipeline tool policies already narrow access (ToolPolicyResolver + ability categories) and matches how the platform reasons about authority:
{
\"ability_categories\": [\"datamachine-content\", \"datamachine-memory\"],
\"ability_allow\": [\"intelligence/wiki\"],
\"ability_deny\": [\"datamachine/delete-flow\"],
\"capabilities\": [\"read\", \"edit_posts\", \"publish_posts\"]
}
ability_categories — allowlist of DM ability categories the token can invoke. Empty = all categories.
ability_allow — additional individual abilities outside the allowed categories.
ability_deny — abilities explicitly forbidden even if their category is allowed.
capabilities — raw WP caps the token can exercise (floor). Without this, token falls back to owner ceiling.
Absence of the field means "owner ceiling" (current behavior) — fully backward compatible.
Proposed consent UX
Three tiers, lowest-friction first:
Tier 1: Scope presets
A dropdown or radio on the consent screen with 3–5 human-legible presets:
- Read-only —
ability_categories: [datamachine-agent, datamachine-content], caps restricted to read. Can read posts, wiki, memory. Cannot write anything.
- Content collaborator — adds
edit_posts, publish_posts, wiki write. Cannot delete or change settings.
- Publisher — above plus
edit_others_posts, edit_published_posts. Normal editor-level agent.
- Full owner ceiling — current default. Clearly labeled as the broadest option.
Presets are registered via a filter so extensions can add domain-specific ones ("Events publisher", "SEO analyst").
Tier 2: Per-category toggles
A collapsible "Advanced" section below the presets with checkboxes for every DM ability category the agent's config declares it uses. Lets humans narrow beyond a preset without dropping to raw caps.
Tier 3: Raw capability picker
For the power user. Link to a secondary page that lists all WP caps the owner has, letting the human tick individual entries. Most humans will never see this.
Per-agent declared defaults
Let an agent's agent_config declare recommended scopes per-redirect-URI so the consent screen suggests a sane default:
\$agent_config['allowed_redirect_uris'] = array(
array(
'uri' => 'https://intelligence-chubes4.test/*',
'default_scope' => 'content_collaborator',
'max_scope' => 'publisher', // Hard ceiling — UI won't offer broader
),
);
This turns a consent for "chubes-bot from franklin's site" into "grant content-collaborator access (default) — upgrade to publisher or restrict to read-only" instead of "grant everything (default) or nothing."
Backend changes required
AgentTokens::create_token() already accepts the capabilities JSON — no schema change.
AgentAuthMiddleware already loads token_capabilities and passes them to PermissionHelper::set_agent_context(). PermissionHelper needs to honor the extended structure (categories / ability allowlist / caps floor) when checking permissions.
AgentAuthorize::handle_authorize_post() needs to parse selected scope from the form and pass it to create_token().
render_consent_screen() needs the preset UI + optional advanced panel.
- A new filter
datamachine_agent_scope_presets for extensions to register additional presets.
- Migration story: existing tokens with
capabilities = null continue to mean "owner ceiling" — no breaking change.
Non-goals
- Not trying to re-invent OAuth scopes. Just wanting scope-at-consent-time, not a full OAuth scope grammar.
- Not touching
wp datamachine external add / connect CLI flow — scope selection lives in the browser consent screen where the human already is.
- Not requiring expiry (that's a separate desirable thing but orthogonal to scopes).
Related
Acceptance
- Consent screen offers at least 3 preset scopes plus "full owner ceiling".
- Presets are filterable via
datamachine_agent_scope_presets.
- Tokens minted with a preset store a structured
capabilities payload honored by the middleware on every request.
- Existing tokens and the default Authorize behavior continue to work unchanged.
wp datamachine agents token list surfaces the scope label (e.g., "publisher") for audit.
Summary
The
AgentAuthorizeconsent screen currently offers a binary choice: Authorize (grants a token with the owner's full capability ceiling) or Deny. The underlyingAgentTokens::create_token()already accepts a$capabilitiesargument that narrows what a token can do, but the browser consent flow hardcodesnull:https://github.com/Extra-Chill/data-machine/blob/main/inc/Core/Auth/AgentAuthorize.php#L326-L332
This is fine for same-owner / same-site flows, but it's the wrong default for cross-site agent auth. When Franklin on site A authorizes as chubes-bot on site B, Franklin gets everything chubes-bot's owner can do on site B, forever. Least privilege is not on the table from the UI — only from direct CLI token minting.
This issue tracks surfacing scope selection in the consent screen itself.
Why it matters
datamachine_agent_tokens.capabilitiesis a JSON-capable TEXT column,AgentAuthMiddlewarealready reads and applies it viaPermissionHelper::set_agent_context(). Only the UX is missing.Proposed scope model
Rather than asking humans to tick 400+ raw WordPress capabilities, express scopes as ability-category allowlists plus optional per-ability fine-grain. This maps to how pipeline tool policies already narrow access (
ToolPolicyResolver+ ability categories) and matches how the platform reasons about authority:{ \"ability_categories\": [\"datamachine-content\", \"datamachine-memory\"], \"ability_allow\": [\"intelligence/wiki\"], \"ability_deny\": [\"datamachine/delete-flow\"], \"capabilities\": [\"read\", \"edit_posts\", \"publish_posts\"] }ability_categories— allowlist of DM ability categories the token can invoke. Empty = all categories.ability_allow— additional individual abilities outside the allowed categories.ability_deny— abilities explicitly forbidden even if their category is allowed.capabilities— raw WP caps the token can exercise (floor). Without this, token falls back to owner ceiling.Absence of the field means "owner ceiling" (current behavior) — fully backward compatible.
Proposed consent UX
Three tiers, lowest-friction first:
Tier 1: Scope presets
A dropdown or radio on the consent screen with 3–5 human-legible presets:
ability_categories: [datamachine-agent, datamachine-content], caps restricted toread. Can read posts, wiki, memory. Cannot write anything.edit_posts,publish_posts, wiki write. Cannot delete or change settings.edit_others_posts,edit_published_posts. Normal editor-level agent.Presets are registered via a filter so extensions can add domain-specific ones ("Events publisher", "SEO analyst").
Tier 2: Per-category toggles
A collapsible "Advanced" section below the presets with checkboxes for every DM ability category the agent's config declares it uses. Lets humans narrow beyond a preset without dropping to raw caps.
Tier 3: Raw capability picker
For the power user. Link to a secondary page that lists all WP caps the owner has, letting the human tick individual entries. Most humans will never see this.
Per-agent declared defaults
Let an agent's
agent_configdeclare recommended scopes per-redirect-URI so the consent screen suggests a sane default:This turns a consent for "chubes-bot from franklin's site" into "grant content-collaborator access (default) — upgrade to publisher or restrict to read-only" instead of "grant everything (default) or nothing."
Backend changes required
AgentTokens::create_token()already accepts thecapabilitiesJSON — no schema change.AgentAuthMiddlewarealready loadstoken_capabilitiesand passes them toPermissionHelper::set_agent_context().PermissionHelperneeds to honor the extended structure (categories / ability allowlist / caps floor) when checking permissions.AgentAuthorize::handle_authorize_post()needs to parse selected scope from the form and pass it tocreate_token().render_consent_screen()needs the preset UI + optional advanced panel.datamachine_agent_scope_presetsfor extensions to register additional presets.capabilities = nullcontinue to mean "owner ceiling" — no breaking change.Non-goals
wp datamachine external add/connectCLI flow — scope selection lives in the browser consent screen where the human already is.Related
ToolPolicyResolver+ ability categories — the primitive the scope model should lean on.Acceptance
datamachine_agent_scope_presets.capabilitiespayload honored by the middleware on every request.wp datamachine agents token listsurfaces the scope label (e.g., "publisher") for audit.