Skip to main content

Read-Only by Default: The 5-Rung Agent Permissioning Ladder Every Deploy Platform Should Copy

10 min readDora NodaDora Noda
Share
On this page

The most important line in Qovery's AI-agent documentation is not about what agents can do. It is about what constrains them: guardrails sit on top of the permissions granted by the token, so "a read-only token remains your strongest safeguard regardless of what the allowlist contains." Everything else on the page — the read-only MCP server, the three-tier CLI allowlist, the Rego policy tokens — is that sentence worked out into machinery.

Qovery's 2026 agentic stack draws the line the whole category is converging on. Deploying a new app from source is a generation problem, handled by an 8-skill agent pack installed with one command. Managing existing infrastructure is an operation problem, handled by a hosted MCP server at mcp.qovery.com/mcp that ships read-only by default. Different tools, different permissions, different blast radii — and the permissioning ladder between them is concrete enough to copy. Here it is, before the detail:

RungControlQovery's mechanismSelf-hosted copy
0Split generation from operationAgent Skill deploys; MCP server managesDeploy workflow and ops verbs are separate surfaces with separate credentials
1Read-only by defaultMCP refuses writes unless read_write=true and console write-access is onDefault-deny writes; enabling them takes two deliberate switches, not one flag
2Scope the credentialView-only API token as a second layer under the modeLeast-privilege token per agent; the token bounds everything above it
3Agent-side allowlist as backstopClaude Code allow/ask/deny tiers for direct CLI useCommit the permission file, but never treat it as the control
4Server-side policy for fine grainAPI Policy Token: OPA/Rego evaluated per request, fail-closedPolicy travels with the credential; the server enforces it whatever client the agent uses

The rest of this post earns each row: why the split falls where it does, what each rung's mechanism actually is, where agent-side enforcement provably breaks down, and the deploy-from-chat checklist a self-hosted platform takes away.


Generation and operation want different tools​

Start with rung 0, because it explains why there are two surfaces at all instead of one agent interface. Qovery's docs put it bluntly: "The MCP Server is for managing existing infrastructure. To deploy a new application from your codebase using an AI agent, install the Qovery Agent Skill instead."

Deploying from code is a prompting problem. The agent needs repo context, framework detection, a generated Dockerfile, environment wiring, and a first-push sequence — a guided workflow, not a tool call. The skill packages that workflow following the Agent Skills open standard, which is why one install command covers Claude Code, Cursor, Codex, Gemini CLI, OpenCode, and 30+ other tools:

bash
curl -fsSL https://skill.qovery.com/install.sh | bash

Managing running infrastructure is a governance problem instead: restart this, scale that, read these logs, change that variable. Discrete verbs over live state, each needing a permission check and an audit entry. That is what the MCP server exposes over streamable HTTP, published in the MCP registry, with OAuth or a token for auth.

The security consequence of the split is what matters for this post. A deploy workflow runs rarely, under supervision, against a known repo — its blast radius is one new service. An ops surface runs constantly, conversationally, against production state. Giving both jobs one credential with one permission set means the everyday troubleshooting credential can also provision infrastructure. Separating them means each gets the narrowest grant its job needs, and the ops side — the one that lives closest to production — starts at zero.

Read-only by default is two switches, not one​

Rung 1 is the headline, and the mechanism is more careful than "there is a read-only mode." Write access requires two independent opt-ins: the client must add read_write=true to the connection URL, and write access must be separately enabled in the Qovery Console. Either switch alone leaves the server read-only.

bash
# Read-only (default) - safe for troubleshooting
claude mcp add --transport http qovery https://mcp.qovery.com/mcp --callback-port 4242
 
# Read/write - only when you intend to make changes
claude mcp add --transport http qovery "https://mcp.qovery.com/mcp?read_write=true" --callback-port 4242

The read/write URL alone is not enough — the console toggle has to agree. That dual control is doing real work. A connection string is exactly the kind of thing that gets pasted into a chat, committed to a dotfiles repo, or copied from a tutorial; if the URL flag were the only gate, every leaked string would be a write credential. The console toggle means the organization has to have already decided this agent is allowed to mutate, in a UI an attacker with only the string cannot reach.

Rung 2 layers underneath: even in read-only mode, Qovery recommends connecting with an API token that carries view-only permissions, "so you are guaranteed no destructive action can be taken." The mode and the token are independent constraints, and the token wins downward — if write mode were somehow requested against a view-only token, the token's own permissions block the write. That is the sentence from the top of this post as architecture: each layer can only narrow, never widen, what the layers below permit.

Qovery's own automation eats this cooking. When an agent task adds a Qovery service as context, the platform auto-attaches its own MCP server organization-scoped, read-only, and backed by a Viewer API token. The default path — the one requiring zero security decisions from the user — is the maximally constrained one. Loosening it is always the deliberate act.

Agent-side allowlists are a backstop, not a control​

Not every agent goes through the MCP server. Claude Code with the Qovery skill drives the qovery CLI directly in a shell, so Qovery publishes a recommended .claude/settings.json with three tiers: allow for read-only commands that need no confirmation, ask for reads that could leak secrets or commands that mutate depending on flags, and deny for mutating commands and anything returning credentials in clear text.

The deny list is thorough — every deploy, redeploy, stop, cancel, delete, update, create, clone, and edit verb, the env-var mutation family, the Terraform state commands, plus explicit backstops for qovery api with mutating method flags. But the most instructive entry is in ask: qovery api itself. That command is a raw passthrough to the API. It defaults to GET, but it silently switches to POST the moment you pass --field or --input, accepts explicit --method DELETE, and can reach endpoints that return secrets. Because Claude Code permission patterns are globs without negation, there is no clean way to express "allow qovery api in GET only" — so the whole command lands in ask, with the mutating flags denied as a backstop.

That limitation is the rung's whole lesson. Agent-side enforcement inherits the agent's permission model, warts included: glob patterns, per-client config formats, and a user who can edit the file. It is worth having — committed to the repo, shared across the team, it stops the casual mistake — but it is enforced by the thing being constrained. Qovery says so explicitly: the MCP server and the allowlist are enforced on the agent's side, and the right tool when the constraint must hold regardless of the agent's configuration is rung 4.

The policy travels with the credential​

The API Policy Token (in beta) flips enforcement to the server. The token carries no RBAC role. It carries an Open Policy Agent policy, written in Rego, that Qovery evaluates on every API request made with it — whatever client sends it, MCP server, CLI, or raw HTTP. The example from the docs fits in six lines:

rego
default allow := false
 
allowed_environment_id := "4a9dc488-df2b-4544-9c5f-4eb0428fda49"
 
# This token can read that environment, and do nothing else anywhere.
allow if {
  input.request.method in {"GET", "HEAD"}
  input.qovery_metadata.environment_id == allowed_environment_id
}

Three properties make this rung worth studying even in beta. First, it is fail-closed: anything other than an explicit true denies the request — no matching rule, a non-boolean allow, an unresolvable path, an unreachable policy engine. The failure mode of a broken policy is "agent can't do anything," not "agent can do everything." Second, it expresses grants finer than read-only — this environment but not that one, deploys but never deletes — which neither the mode flag nor a role can say. Third, actions are attributed in the audit log as policy:<token-id>:<token-name>, so an agent's activity stays distinguishable from a human's API calls in the same trail.

The footgun is stated just as plainly: internally the token authenticates as organization-admin and the policy is the sole constraint, so a permissive policy grants everything and only an owner or admin may create one. Least privilege is not guidance here; it is the entire security model. Qovery even ships a skill for it — qovery-policy-token authors the Rego, tests it locally with OPA and live against the API, creates the token, and proves the grant does exactly what was asked. The agent that will live under the policy helps write it, but the server holds the pen.

The category is converging on the same posture​

Qovery is the most completely documented instance, but the posture is becoming a category norm, which is what makes it safe to treat as a design input rather than one vendor's taste. Openship's migration guide now carries an MCP row comparing exactly this dimension across the self-hosted field: Coolify ships read-only, Dokploy exposes the full API, Dokku leans on community work, and Openship itself requires write plus re-authentication. Its README states the same layering as architecture: only routes that opt in become MCP tools, every call re-checks permissions, and credential/token routes can never become tools at all.

Read the row as a spectrum and the direction is obvious. Nobody's defensible position in 2026 is "the agent gets your admin token and good luck." The live debate is where the default sits and what it takes to move it — read-only until re-auth, read-only until two switches, full API with per-tool scoping — and every serious answer puts the enforcement on the server side of the connection.

What deploy-from-chat copies​

For a self-hosted platform designing deploy-from-chat on its own machines, the ladder compresses to a checklist. Each item has a Qovery mechanism behind it and a plain reason in front of it:

  • Separate the deploy credential from the ops credential. Generation and operation are different jobs with different blast radii; they should never share a token.
  • Ship the ops surface read-only, and make writes a dual opt-in. One flag in a connection string is one paste away from being a write credential. Require a second switch the string alone cannot flip.
  • Scope the token under the mode. A view-only credential bounds everything above it, including a misconfigured client — the layer that cannot be talked out of its grant.
  • Publish the agent-side allowlist, but call it a backstop. Commit the allow/ask/deny tiers for the clients you support, document where the client's pattern language cannot express the grant you want (there will be somewhere), and enforce the real boundary server-side.
  • Put fine-grained grants in server-evaluated policy, fail-closed. Per-environment, per-verb, per-agent constraints belong in something the server checks on every request, where a broken rule denies rather than permits.
  • Attribute every agent action to its token in an immutable trail. "An agent did something" must always resolve to which agent, under which grant, prompt-attributed where possible.

None of this is exotic technology. Dual opt-ins, scoped tokens, committed permission files, Rego at the API edge, an audit log keyed by credential — each piece is boring on its own. The insight is the stack: every layer narrows, no layer widens, and the default path a user stumbles into is the most constrained one. Copy the stack before any agent gets a write token to a production fleet, and the write token becomes a deliberate, auditable, revocable decision instead of a hope.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Agents are first-class operators there too: star the repo on GitHub and help design the deploy-from-chat surface the checklist above describes.

Related articles

Give your agents a chain backend

Autonomous agents hit RPC endpoints very differently than people do. See what bex router handles on their behalf.

Read the agents guide