Skip to main content

Principal-Type Audit (Phase 11)

Platform delegation introduces principal_type: "platform_delegated" when a plt_ key presents X-Platform-Connection for a connection with delegation_enabled=true. Human-only paths must use an allowlist (== "user") or require_human() that rejects every non-user principal. A denylist (!= "agent" or == "agent" → forbid) incorrectly admits platform_delegated into human-only surfaces.

Types (quick reference)

principal_typeHow it is setTypical credentials
userUser JWT / 1ck_ keyDashboard, human API key
agentAgent JWT (sub: agent:<uuid>)ocv_ → agent-token
platformplt_ without connection headerPlatform operator
platform_delegatedplt_ + X-Platform-Connection when enabledSame plt_, scoped
oauthOAuth access tokenSign in with 1Claw

New connections default to delegation_enabled = false (migration 151).

Classification legend

ClassPatternVerdict
Allowlistprincipal_type == "user" / != "user" → forbidCorrect for human-only; rejects platform_delegated
Denylistprincipal_type != "agent" or == "agent" → forbid onlyRISKY on human-only paths (allows platform_delegated)
Explicit platform_delegatedSets/platform_delegated flag or branches on itOK when intentional
Scope enforcementenforce_scope_access / enforce_delegation_scopeOK

Delegation-capable CRUD (vaults, agents, secrets, automations, runtimes) intentionally uses == "agent" → forbid plus enforce_delegation_scope so humans and platform_delegated can operate within scopes. That combination is not treated as RISKY.


Key findings (verified in code)

#FindingStatus
1secrets.rs enforce_scope_access uses == "user" to skip JWT path scopesFIXED (allowlist)
2bankr_keys.rs expose_api_key uses == "user"FIXED (allowlist)
3Remaining human-only denylists that admit platform_delegatedSee RISKY denylist inventory
4Memory plaintext reveal (get/list) must not be available via delegationFIXEDresolve_agent_for_memory rejects platform_delegated
5CMEK / MPC / MFA use != "user"; treasury wallets require_human allowlistCMEK/MPC/MFA OK; treasury wallets FIXED; billing has no principal gate

Summary table

LocationCheckClassNotes
api/handlers/secrets.rs enforce_scope_access== "user" skipAllowlist + ScopeAgents and platform_delegated must match JWT/path scopes
api/handlers/secrets.rs handlersenforce_delegation_scope(secrets:*)ScopeDelegated secret CRUD gated by scopes
api/handlers/bankr_keys.rs leaseexpose_api_key = == "user"AllowlistAPI key never returned to agents / delegated
api/handlers/vaults.rs CMEK enable/disable!= "user"AllowlistHuman-only
api/handlers/vaults.rs MPC enable!= "user"AllowlistHuman-only
api/handlers/cmek.rs rotate!= "user"AllowlistHuman-only
api/handlers/auth.rs MFA / email-change / set-password / export-data!= "user"AllowlistHuman-only
api/handlers/api_keys.rs!= "user"AllowlistHuman-only
api/handlers/org.rs!= "user"AllowlistHuman-only
api/handlers/admin.rs / admin_guard!= "user"AllowlistPlatform admin humans
api/handlers/risk.rs!= "user"AllowlistHuman-only
api/handlers/devices.rs / most passkeys.rs!= "user"AllowlistHuman-only
api/handlers/reauth.rs!= "user"AllowlistHuman-only
api/handlers/bankr_config.rs!= "user"AllowlistHuman-only
api/handlers/ip_rules.rs!= "user"AllowlistHuman-only
api/handlers/approvals.rs decide/list!= "user"AllowlistHuman decide path
api/handlers/oauth.rs authorize!= "user"AllowlistConsent is human
api/handlers/platform.rs update delegation!= "user"AllowlistEnd-user consent
api/handlers/treasury.rs (most)!= "user"AllowlistSafe/treasury admin
api/handlers/treasury_proposals.rs execute!= "user"AllowlistForce-execute human-only
api/handlers/signing_keys.rs export!= "user"AllowlistKey material export
api/handlers/treasury_wallets.rs require_human!= "user"AllowlistWas denylist; fixed for Phase 11
api/handlers/agent_memory.rsrejects platform_delegatedExplicitAgents (own) + users only for plaintext
api/middleware/auth.rssets platform_delegatedExplicitHeader + delegation_enabled
api/middleware/auth.rs enforce_delegation_scopescope matchScopeEmpty scopes deny
api/handlers/{vaults,agents,secrets,automations,runtimes}.rs== "agent" → forbid + enforce_delegation_scopeExplicit + ScopeDelegation-capable CRUD — OK
api/handlers/webhooks.rs== "agent" → forbidDenylistRISKY — no delegation scope
api/handlers/signing_keys.rs create/rotate/deactivate== "agent" → forbidDenylistRISKY — provisioning not user-allowlisted
api/handlers/bindings.rs create/rotate== "agent" → forbidDenylistRISKY — credential binding writes
api/handlers/spend_policies.rs== "agent" → forbidDenylistRISKY if reachable with delegated identity
api/handlers/{deposit_destinations,internal_accounts,fiat,chat,channels}.rs require_human== "agent"DenylistRISKY — still denylist
api/handlers/runtimes.rs shell require_human!= "user"AllowlistWas denylist; fixed for Phase 11
api/handlers/billing_v2.rs / llm_billing.rs(none)GapPrefer == "user" on mutating billing
api/handlers/approvals.rs request!= "agent"Allowlist-of-agentsAgent-only — OK
api/handlers/treasury.rs request_access!= "agent"Allowlist-of-agentsAgent-only — OK
api/handlers/agents.rs /me!= "agent"Allowlist-of-agentsAgent-only — OK
api/handlers/sharing.rs recipient creator!= "agent"Allowlist-of-agentsAgent-only — OK
api/handlers/auth.rs federated exchange subject!= "agent"Allowlist-of-agentsAgent-only — OK

Detail by pattern

Allowlist (== "user" / != "user")

Used for MFA, CMEK, MPC, org admin, API keys, risk, devices/passkeys, reauth, Bankr org config, IP rules, platform delegation toggle, most treasury Safe admin, signing-key export, and treasury wallets.

if caller.principal_type != "user" {
return Err(AppError::Forbidden("Only users can …".into()));
}

Scope enforcement (OK)

enforce_scope_access (secrets.rs): only principal_type == "user" skips path-scope checks; agents and platform_delegated must match a scope glob.

enforce_delegation_scope (auth.rs): for platform_delegated callers, require exact / resource:* / * match; empty scopes deny. Non-delegated callers pass.

Explicit platform_delegated (OK)

Auth middleware resolves X-Platform-Connection, requires delegation_enabled, loads delegation_scopes, sets principal_type = "platform_delegated". Wrong app → connection mismatch → 401.

RISKY denylist inventory

These human-sensitive (or privileged) paths still use agent denylist and do not call enforce_delegation_scope:

FileHelper / siteRecommendation
deposit_destinations.rsrequire_human!= "user"
internal_accounts.rsrequire_human!= "user"
fiat.rsrequire_human!= "user"
chat.rsrequire_human!= "user"
channels.rsrequire_human!= "user"
runtimes.rsshell require_human!= "user" FIXED
webhooks.rsall mutating handlers!= "user"
signing_keys.rscreate / rotate / deactivate!= "user" (export already allowlisted)
bindings.rscreate / rotate-credential!= "user"
spend_policies.rsmutating handlers!= "user"
billing_v2.rs / llm_billing.rssubscribe / portal / topup / disableadd == "user"

Shared helper preference:

fn require_human(caller: &CallerIdentity) -> Result<(), AppError> {
if caller.principal_type != "user" {
return Err(AppError::Forbidden(
"This operation is only available to human users.".into(),
));
}
Ok(())
}

Memory / CMEK / MPC / Treasury / MFA / Billing

SurfaceRejects platform_delegated?
Memory plaintext get/listYes (resolve_agent_for_memory)
CMEK enable/disable/rotateYes (!= "user")
MPC enableYes (!= "user")
MFA setup/status/disableYes (!= "user")
Treasury Safe admin (treasury.rs)Yes (!= "user")
Treasury walletsYes (require_human allowlist)
Billing mutate (billing_v2 / LLM billing)No dedicated check — follow-up

Merge gate checklist

  • enforce_scope_access remains allowlist (== "user" only skips scopes)
  • Bankr lease never exposes api_key unless principal_type == "user"
  • Memory plaintext paths reject platform_delegated → 403
  • CMEK / MPC / MFA / treasury wallets reject platform_delegated → 403
  • No new human-only handler uses == "agent" → forbid without documenting why delegated is allowed
  • Remaining RISKY rows above are fixed or explicitly accepted with ticket
  • scripts/test-platform-delegation-prod.sh ≥ 12 TOTAL assertions covering the Phase 11 plan list
  • Runtime rebuild returns stopped (no stuck building without Cloud Build)

Suggested follow-ups (non-blocking if ticketed)

  1. Convert remaining require_human denylists (deposit, fiat, internal accounts, chat, channels).
  2. Allowlist webhooks, signing-key provision, bindings create, spend policies.
  3. Add principal_type == "user" on billing mutations.