Skip to content

Permissions

How CompozyOS applies static permission modes, workspace boundaries, and interactive approvals during session execution.

For people running agent work14 pages in this section

CompozyOS applies permissions in two layers:

  1. a daemon-side static policy for built-in ACP tool surfaces
  2. an interactive approval flow for ACP session/request_permission

Both layers are scoped to the session workspace. approve-all by itself does not guarantee unrestricted provider commands: the ACP provider also chooses an operating mode. Set permissions.provider_full_access = true in the global config to request the provider's unrestricted mode for new approve-all sessions, including Loop roles. CompozyOS selects agent-full-access for Codex and bypassPermissions for Claude Code. Unsupported providers fail session resolution clearly instead of silently accepting this preference. Existing mode selection is unchanged when the setting is false. With the current Claude Code adapter, approve-all already selects bypassPermissions when that mode is advertised. An agent can explicitly choose a narrower ACP mode, and stricter effective CompozyOS permissions never permit an unrestricted mode. This setting does not bypass an outer CompozyOS operating-system permissions; unrestricted provider commands run with the daemon user's access within those boundaries.

When provider-native tool interception is active, CompozyOS may keep the upstream ACP session in an approval-mediated mode so provider-native tool calls return through the CompozyOS execution gateway before any real side effect. That does not change the configured policy outcome: approve-all still auto-allows, approve-reads still auto-allows reads only, and deny-all still auto-rejects at the execution boundary.

Permission modes

CompozyOS supports three static modes:

ModeStatic behaviorCross-workspace requests
deny-allCompozyOS auto-allows none of the built-in file or terminal operations.Denied everywhere, with no prompt.
approve-readsCompozyOS auto-allows fs/read_text_file only.Asks at the native-tool boundary; every other boundary denies.
approve-allCompozyOS auto-allows fs/read_text_file, fs/write_text_file, and terminal/create.Allowed without asking.

The same mode decides both columns. Cross-workspace access explains what that means for a session that asks for another workspace.

Defaults and overrides

The current built-in default configuration is:

[permissions]
mode = "approve-all"

That global mode can be overridden by an agent definition.

If a session somehow reaches startup with no resolved permission string at all, the session manager falls back to approve-reads instead of launching with an empty policy.

What the static policy actually covers

These are the built-in operations CompozyOS checks directly:

Operationdeny-allapprove-readsapprove-all
fs/read_text_fileblockedallowedallowed
fs/write_text_fileblockedblockedallowed
terminal/createblockedblockedallowed

Static auto-allow is only one part of the full story. CompozyOS still enforces workspace scoping and workspace restrictions after the mode check passes.

Workspace boundaries

Every permission decision is evaluated relative to the session workspace root.

CompozyOS resolves paths carefully:

  • absolute paths are checked directly
  • relative paths are resolved under the workspace root
  • existing paths are resolved through symlinks
  • missing paths are resolved through the first existing ancestor

If the final path escapes the workspace root, CompozyOS rejects it with a path-outside-workspace error.

Cross-workspace access

A session belongs to one scope: a workspace, or the User scope of its profile. When an agent asks for a different one — a native tool call that names another workspace (or @user from a project), an agent identity lookup, a task claim, a spawn, or a workspace coordination command — the daemon sends that request through a single policy. The same workspace under another profile counts as a different scope too. Unless the agent holds the unrestricted grant, the session's own permission mode decides it:

Session modeCross-workspace outcome
approve-allAllowed. The session crosses without being asked.
approve-readsAsks at the native-tool boundary when the session is running; every other boundary denies.
deny-allDenied at every boundary, including the native-tool one. CompozyOS never prompts for crossing.

Operators are not governed by this policy — it applies to agent sessions. Extensions, automation, and the daemon itself keep the behavior they have today; a permissive mode never widens them.

deny-all reads differently on each axis, and that is deliberate. For tool risk it means "auto-allow nothing, ask instead". For the workspace boundary it means "never cross, never ask". deny-all is the lockdown setting for a session that must stay inside its own workspace.

The trade-off is that one mode now governs both axes. An approve-all session is open on tools and open across workspaces; there is no separate setting for permissive tools with an isolated workspace. The built-in default is approve-all, so set [permissions] mode — or the agent's permissions field — to approve-reads or deny-all when a session should stay in its workspace.

Denials

A denied request keeps the error shape it has today and adds one hint:

cross-workspace access is denied by this session's permission mode; ask the operator to set the agent's permissions.mode to approve-all, or approve the prompt when asked

Native tool denials carry the workspace_access_denied reason code alongside that message.

The prompt in approve-reads

In approve-reads, a cross-workspace native tool call from a running session becomes a pending permission request instead of an immediate denial. CompozyOS computes the option set:

Option IDLabelEffect
allow_onceAllow onceAllows this request only. The next crossing asks again.
allow_sessionAllow for this sessionAllows this request and every later crossing by that session, at every boundary.
reject_onceReject onceDenies this request only. The next crossing asks again.
reject_sessionReject for this sessionDenies this request and every later crossing by that session, at every boundary.

Answer it like any other pending permission — the approvals bell in Web, compozy session approve, or the approve route. The allow-once, allow-always, reject-once, and reject-always decisions map onto the four options above in that order. The wait is bounded by [tools.policy] approval_timeout_seconds; a timeout, a transport failure, or an answer outside the option set denies the request and stores no session answer.

Crossings at every other boundary are denied outright in approve-reads — they never raise a prompt. Once a session has answered allow_session, those boundaries start allowing.

Session answers do not outlive the session

allow_session and reject_session are held in daemon memory, keyed by session ID. They apply to every boundary for the rest of that session and disappear when the session stops or the daemon restarts. Nothing is written to disk, and there is no list or revoke surface: stopping the session is how you clear the answer. Persisted audit events show how later policy evaluations were decided.

The unrestricted grant

An agent definition with workspace_access: unrestricted skips this table: its sessions reach every workspace, the User scope, and every profile without a prompt, and can read All projects aggregates. Only you can set the field, and it does not loosen tool approvals — the permission mode still decides risky tools. Each allowed crossing is audited with decision source agent_grant. See Give an agent access to everything.

Audit

Each policy evaluation attempts a best-effort audit append — workspace.access_granted for an allow or workspace.access_denied for a deny, including a prompt-eligible deny — scoped to the actor's own workspace. The payload carries the actor and target profiles, the target kind (workspace, user, or aggregate), the target workspace, boundary, decision source, and resolved mode. An audit write failure is logged and does not change the access decision. Read persisted events with compozy logs --type <event-type> or GET /api/logs.

See Workspace Resolver for what workspace isolation covers today and what it does not.

Interactive approval flow

Agents can also ask for permission explicitly with ACP session/request_permission.

That flow behaves like this:

  1. the agent sends a permission request with one or more offered outcomes
  2. CompozyOS checks the workspace boundary first
  3. CompozyOS either auto-selects an outcome or records a pending permission event
  4. a client approves or rejects it
  5. CompozyOS returns the selected ACP outcome to the agent

Auto decisions vs pending decisions

Session modeRequest kindImmediate result
approve-allany valid in-workspace requestallow-once
approve-readsACP read requestallow-once
approve-readsnon-read ACP requestpending approval
deny-allany ACP requestreject-once

Outside-workspace requests are rejected immediately instead of becoming pending.

Decisions clients can send

The approval request body accepts these decisions:

  • allow-once
  • allow-always
  • reject-once
  • reject-always

Example HTTP approval:

curl -X POST http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234/approve \
  -H "Content-Type: application/json" \
  -d '{
    "request_id": "req-42",
    "turn_id": "turn-9abc",
    "decision": "allow-always"
  }'

Success response:

{
  "status": "approved"
}

Approval timeout

Pending interactive permission requests wait up to 5 minutes by default. If no approval arrives, CompozyOS resolves the request as reject-once.

What allow-always and reject-always mean

For a generic ACP permission request, CompozyOS picks the closest option offered by the agent and returns it. The agent runtime owns the long-lived meaning of "always" for that request.

Native tools routed through CompozyOS's approval gateway have an additional daemon-owned contract. Selecting allow-always or reject-always stores an exact workspace + agent + tool + input-digest decision. The next matching invocation uses that decision before prompting, and the decision survives daemon restart. Operators and agents can inspect or revoke these records through Web, compozy tool approvals, HTTP/UDS, or the compozy__tool_approvals_* native tools. This does not change ACP subprocess permissions or operating-system policy. See Policy and Invocation.

Permission events

Permission requests and resolutions are visible in both prompt streams and persisted session history.

Example canonical permission payload:

{
  "schema": "compozy.session.event.v1",
  "type": "permission",
  "session_id": "acp-session-77",
  "turn_id": "turn-9abc",
  "request_id": "req-42",
  "timestamp": "2026-04-16T01:22:00Z",
  "action": "session/request_permission",
  "resource": "/absolute/path/to/repo/internal/session/manager.go",
  "decision": "allow-always",
  "title": "Write file",
  "tool_call_id": "tool-call-7",
  "raw": {
    "tool_call": {
      "kind": "edit",
      "title": "Write file"
    }
  }
}

If a request is still pending, the initial permission event is recorded with no final decision yet. The resolved decision appears when approval completes.

Current transport status

Interactive approval is exposed differently across CompozyOS transports today:

TransportStatus
HTTPimplemented at POST /api/workspaces/:workspace_id/sessions/:session_id/approve
UDSimplemented at POST /api/workspaces/:workspace_id/sessions/:session_id/approve
CLIimplemented at compozy session approve <id>

That means browser-facing clients, UDS clients, and the local CLI all drive the same approval surface today. Cross-workspace prompts arrive through that same surface. They add no route, command, or Settings section of their own, and a session-scoped answer has no management surface at all.

Clarification questions are not approvals

compozy__clarify lets an active managed session ask one question and wait for an operator (unbounded unless [tools.clarify] timeout is finite). It is a read-only, non-destructive tool with its own live broker; it does not reuse permission requests, approval decisions, or remembered native-tool grants. One session can have at most one pending clarification.

The question may offer up to four unique choices or accept free text. The HTTP/UDS wire uses a zero-based choice_index; the CLI presents one-based choice numbers:

compozy session clarify pending <session-id> -o json
compozy session clarify answer <session-id> <request-id> --choice 1 -o json
compozy session clarify answer <session-id> <request-id> --text "Use staging" -o json

HTTP and UDS use the same workspace-scoped routes:

  • GET /api/workspaces/:workspace_id/sessions/:session_id/clarifications
  • POST /api/workspaces/:workspace_id/sessions/:session_id/clarifications/:request_id/answer

The live pending list is exact daemon truth. Durable clarify events preserve the pending, resolved, timed-out, and canceled transitions for timeline and audit. Under a finite [tools.clarify] timeout, expiry returns exactly {"choice":null,"text":"","fallback":true}; stopping the session or daemon cancels the wait.

Practical configuration example

Global default in ~/.compozy/config.toml:

[permissions]
mode = "approve-reads"

Per-agent override in an AGENT.md definition:

---
name: reviewer
provider: codex
permissions: deny-all
---

Review code changes and explain the risk.

Use the global default for the team baseline, then override individual agents only when they need a different safety envelope. There is no compozy session new --permissions flag today, and the session create HTTP payload does not currently accept a permissions override either.

Next steps

  • Use Session Lifecycle to see where approvals fit into create, active, and stop behavior.
  • Use Event Streaming to follow permission events and audit what happened in one session.

On this page