Authorization Scopes
Managed-agent bearer credentials are intentionally scoped. Node/operator tokens bypass these rules; agent-scoped tokens do not.
Scope hierarchy
worker < project_controller < cluster_controller
Each endpoint is checked explicitly by the daemon. Higher scopes generally inherit lower-scope access, but callers still remain bound by project membership and route-specific checks.
Quick matrix
| Capability | worker | project_controller | cluster_controller |
|---|---|---|---|
| Read node/project/agent summaries | yes | yes | yes |
| Own lifecycle operations | yes | yes | yes |
| Other agents in bound project | no | yes | yes |
| Spawn agents in bound project | no | yes | yes |
| Bound-project bead mutations | yes | yes | yes |
| Cross-project bead read/mutate | only explicit grants | bound project or explicit grants | yes |
| Bound-project messaging | yes | yes | yes |
| Global messaging and persistent-agent controls | no | yes | yes |
| Other-project tests, checkout, or build APIs | no | no | yes |
| Reintegrate another agent's generation | no | only explicit capability + audited path | only explicit capability + audited path |
| Publish a controller-owned source generation | ordinary own-generation authority | source_publication + exact bead/path override | source_publication + exact bead/path override |
| Self-owned GitHub recovery | github_recovery + exact own generation | same | same |
Local lifecycle targets
Project-controller writes to agent routes require an exact runtime ID whose authoritative registry record belongs to the caller's bound project. Middleware refuses other-project targets, unknown IDs, persistent aliases, and historical name aliases before dispatch, forwarding, or backend effects. Resolve aliases through agent status before submitting a write. An absent bound project or all_projects does not widen this authority.
The project-qualified stop wrapper preserves the scoped caller; stop and resume do not re-resolve an authorized controller target into another generation. Worker-own rules and operator/node and cluster-controller authority are unchanged.
Profile declaration
authorization:
scope: worker # worker | project_controller | cluster_controller
capabilities:
- cross_owner_reintegration # explicit audited cross-owner generation recovery
- source_publication # exact controller-owned bead/path publication override
- github_recovery # exact self-owned GitHub recovery family
Profiles cannot escalate beyond their declared scope. Narrow capabilities are independent of the scope hierarchy and unlock only their named API family. Agent creation may also carry an explicit authorization-scope override, but the daemon refuses escalation beyond the caller's effective permissions.
Cross-project beads
cross_project_bead_permissions grants individual bead actions such as read, create, claim, close, or update for named projects. Use it when a worker or project controller needs a narrow cross-project workflow without giving it cluster-wide authority. A rule with project: "*" grants the listed bead actions across all current and future projects while leaving tests, builds, checkout, messaging, and agent lifecycle APIs scoped normally. The read action covers cross-project bead list/show plus the global /api/v1/beads/all and TTS-only /api/v1/beads/spoken-names projections.
Narrow role capabilities
authorization.capabilities grants one named high-risk API family without widening every agent at the same scope. A capability-only mixin may omit authorization.scope; composition retains the explicit scope supplied by its controller profile. The retired leader-lock, leader_lease, and release_lease profile capabilities are no longer part of the checked-in role contract.
The source_publication capability is orthogonal to the worker/controller breadth ladder. Ordinary workers publish their own source generations; project and cluster controllers remain coordinate-only even for checkouts they own unless they explicitly select the reason-required override and name the exact authoritative bead and repository path scope. Generated documentation, configuration, and schema artifacts still count as source.
Override admission computes the real diff only from the exact clean current owner checkout and HEAD against the exact configured integration target commit. Stale or unreadable targets, target or HEAD drift, friend and historical checkouts, generated state artifacts, dirty or untracked bytes, unsafe paths, and symlink escape fail closed with typed reason codes. CLI and daemon use the same path normalization. --dry-run reports the bounded secret-redacted changed-path inventory, count, and digest without publishing; a scope mismatch refusal returns the same evidence and every bounded extra path before any provider mutation.
github_recovery is granted by exactly one checked-in capability-only mixin, .cacophony/profiles/github.md, which also carries the usage contract. Owner roles compose it with composes: [github] instead of repeating the raw capability name: the generic implementation hosts (worker, dev, light-dev), the persistent platform/surface implementation owners (caco-android, caco-ios, caco-macos, caco-profile, caco-transcription, caco-tui, caco-vm, caco-wearable, caco-web, ios-dev, repo-health, tui-optimizer, tui-animation-optimiser), and the documentation/spec implementation owners (changelog-manager, corpus-compactor, gh-pages, spec-builder, technical-writer). Observers, narrators, PR monitors, bead filers, release/tag roles, and controllers do not receive it by role inference, and the exact composer set is pinned by a profile fixture so adding a holder is a deliberate, reviewed edit. Because the family is managed-caller-only, its CLI entrypoints present the scoped managed credential rather than the node token. See docs/github-recovery-rollout.md for the safe runtime rollout of an already-running owner.
Cross-project agent spawning
Spawning an agent into a project where the caller has no presence follows the same matrix: a worker or project_controller bound to one project cannot create beads or spawn agents in another — POST /api/v1/projects/<other>/agents denies with <scope> scope cannot access <path> (for example worker scope cannot access project 'life' (bound to cacophony)). To spawn cross-project you need a cluster_controller (the only unconditional spawn-anywhere scope), a project_controller already bound to the target, or an operator; for a project that hosts work repeatedly, declare a project-scoped controller for it. Two cross-node repeatability caveats: a --profile-path handed across nodes can lag through scratch/file-cache propagation, so paste the profile body through messaging and read it back with caco msg history on the target node; and a synchronous caco bd dispatch --node <other> --profile-path can time out on cross-node forwarding, so use --queued for asynchronous pickup.
Ambient project routing
Project-defaulting commands resolve their project from --project, then CACO_PROJECT, then CACOPHONY_PROJECT, so a checkout-local .envrc, a friend-project export, or an inherited operator shell naming a different project silently retargets every such command from that checkout.
A worker credential is bound to one project, so the retargeted call is correctly refused with worker scope cannot access project '<other>' (bound to '<own>').
That is a routing bug in the caller's environment, not a missing permission; widening the credential would be the wrong repair.
Self-directed internal calls name the caller's own project explicitly, and this repository's .envrc is checked by caco config validate so a foreign CACO_PROJECT cannot reroute checkouts again.
Operator checks
AGENT_ID="agent-id"
caco agent status --id "$AGENT_ID" --json | jq '{scope: .data.authorization_scope, capabilities: .data.authorization_capabilities}'
# or inspect the persisted state file under the Cacophony runtime directory.
Source of truth
The detailed Markdown reference remains in docs/authorization-scopes.md, and the daemon enforcement lives in the local authorization checks around check_agent_scope, check_agent_lifecycle_scope, and project-scoped endpoint checks.