PodWarden Hub
PodWarden Hub

Warden Agent Development Mode

Unrestricted tool access — give the assistant every tool a Core announces, at every tier. Development only. What it costs, and how to turn it off.

What this is

The Warden Agent normally sees a deliberately narrow slice of what your Core can do. Unrestricted tool access removes that slice and gives it every tool the bound Core announced, at every tier — reads, mutations and destructive operations alike.

It exists for one situation: a development platform with no users, where you want to find out what the assistant is actually good for before deciding what it should be allowed to do. Start from the whole surface, watch what happens, then add policy from what you learned.

This is a development-only mode. Do not run it on a deployment that has users, customer data, or clusters you cannot afford to lose. It is off by default and nothing in the product turns it on for you.

Where the chat lives

The Warden Agent is no longer a drawer docked to the MCP page — it is its own page, /dashboard/warden (a "Warden Agent" entry appears in the dashboard nav, right after "MCP"). The page is gated the same way as the switches below: if the Warden Agent switch is off, the page answers 404 and the nav entry is not rendered, so a disabled deployment ships no hint that the feature exists.

Chats are shared within your organization

A Warden Agent chat belongs to the org, not only to whoever started it:

  • Every chat anyone in your org starts appears in the shared sidebar, tagged with its owner.
  • You can open and read any of a colleague's chats once its current turn has finished. A chat with a turn still running is visible only to its owner and an org admin — the Hub answers as if the chat didn't exist rather than confirm it does, so you cannot learn that a colleague is mid-conversation.
  • You can fork any chat you can read — your own or a colleague's — into a new chat that continues from that point without altering the original.
  • Renaming, deleting, or sending the next message in a chat you don't own is reserved for the owner and org admins; read access does not include write access.

There is no picker yet for which Core a new chat talks to: the first message binds the chat to your org's first connected Core instance, and a badge on the chat names it once bound. Choosing the instance yourself is expected in a later release, not this one.

Where the switch lives

Both Warden Agent switches are rows in the Hub database, not environment variables:

SwitchStored atWhat it does
Warden Agentsystem_settings scope warden, key agent_enabledWhether the agent exists at all on this deployment
Unrestricted tool accesssystem_settings scope warden, key unrestricted_toolsWhether the tool-tier gate is suspended

They used to be WARDEN_AGENT_ENABLED and WARDEN_AGENT_UNRESTRICTED_TOOLS in the Hub's environment, which meant a change required shell access to every Hub node plus a container restart — and the production deploy pipeline, which publishes a DNS deploy signal and carries no environment, could not set them at all.

They are deployment-wide, not per organization: one Hub, one answer, every org. They are not part of Settings › Admin › Warden Agent's per-org configuration even though they are shown on the same page.

Turning it on

In Settings › Admin › Warden Agent › Deployment switches, as a platform admin (Keycloak realm role admin or superadmin). An organization admin cannot do this — the org-admin role governs one org, and this switch governs every org on the Hub.

Turning unrestricted access on is a two-step control: type unrestricted to arm the button, then press it. Turning it off is a single click.

There is no restart. The change is visible on every Hub replica within a few seconds. Unrestricted access requires the Warden Agent switch to be on as well — it widens the agent's reach, it does not create the agent.

Every flip of either switch, in either direction, writes an admin_audit_log row with your identity and the old and new value.

The stored value is matched exactly

The stored value must be the exact string true. Everything else is off, and the discipline moved with the switch — it now guards the database value rather than the environment one:

Stored valueResult
no rowOff
"" (empty)Off
falseOff
TRUE, TrueOff — no case folding
1, yes, onOff — no truthiness
" true " (padded)Off — no trimming
trueOn

The strictness is deliberate. Every one of those near-misses is a plausible way to switch this on by accident, and none of them do. The API refuses anything but a JSON boolean, so the UI cannot store one of them either.

The environment can still force it off

WARDEN_AGENT_UNRESTRICTED_TOOLS and WARDEN_AGENT_ENABLED survive as a one-way OFF override:

Environment variableDatabase valueEffective
exactly falseanythingOff
unset, or any other valueexactly trueOn
unset, or any other valueanything elseOff

Read the table twice, because the asymmetry is the point: the environment can only force the flag off. It can no longer turn it on. A leftover WARDEN_AGENT_UNRESTRICTED_TOOLS=true on a node does nothing at all.

The brake is PER-NODE. Set it on every node.

This is the one thing on this page you can get wrong in a way that hurts you.

The switch is a database row: one value for the whole deployment. The environment brake is not — it is the environment of one container, and a Hub node cannot see any other node's environment. Set WARDEN_AGENT_UNRESTRICTED_TOOLS=false on two of three nodes and you have gated two of three nodes. The third obeys the stored switch, and requests reach it as normal.

That failure is invisible from the wrong angle. With the switch stored on and the brake set unevenly:

  • A request routed to a braked node reports the mode as held off — for that node.
  • A chat turn routed to any other node is handed every tool the bound Core announced, at every tier.

So the settings page never claims the deployment is gated. It reports the answer given by the node that served your request, and every field it reads is named for that (effective_on_this_node, env_forced_off_on_this_node) — as are the corresponding fields in the admin_audit_log payload. When the switch is stored on and your node is braked, the status chip reads Active · off on this node, not Gated.

To gate a deployment with the environment brake, set the variable on every node and restart each one. If you cannot guarantee that, switch the stored value off instead: that is the one control that is genuinely deployment-wide.

Why keep it: while the switch lived in the environment, turning unrestricted mode on required shell access to the nodes, and that inaccessibility was itself a control. Moving it into a database row hands the switch to everyone who can write that row. Setting WARDEN_AGENT_UNRESTRICTED_TOOLS=false in a node's environment bolts the door from outside the application — no UI, API or SQL statement can unbolt it. That is the brake to reach for on any Hub that must not be able to enter this mode, whatever a future admin clicks.

When the override is in force on the node serving the page, the settings page says so, and says whether the stored switch is currently being overridden there. A flip that changes nothing on that node is never silent.

How you know it is on

You cannot miss it, by design:

  • The moment it turns on, the Hub prints a banner naming the switch and what it did — on the transition, not only at startup, because the value can now change while the Hub is running. Turning it back off prints a line too.
  • On every turn, the request log carries [warden][UNRESTRICTED] lines showing how many tools were offered and which tool is being dispatched, with its real tier.
  • On the Warden Agent chat page (/dashboard/warden), a banner above each answer states the posture for that turn. It is raised per turn and cleared when the turn ends, so what you read always describes the answer you are looking at rather than a session-wide claim that may have gone stale.
  • In Settings › Admin › Warden Agent, a red notice states that the mode is active, and the Deployment switches card shows the switch as Active — or Active · off on this node when the stored switch is on but the node that answered carries the environment brake.

If you are ever unsure which mode a Hub is in, open the chat page. The answer is on screen.

Turning it off

Press Restore the tier gate in Settings › Admin › Warden Agent › Deployment switches. One click, no restart, effective on every replica within seconds.

If you cannot reach the UI — or you want it held off regardless of what anyone stores — set WARDEN_AGENT_UNRESTRICTED_TOOLS=false in the Hub node's environment and restart that node. That override cannot be lifted from inside the application. It gates only the node whose environment carries it, so set it on every node; see the brake is per-node above. Pressing Restore the tier gate remains the only single action that gates the whole deployment.

That is the entire procedure. Nothing was deleted to build this mode. The tier and may_be_read gate is intact in the code and starts enforcing again on the next request. There is no migration to reverse, no allowlist to prune, and no code to restore.

The database was never touched either: mcp_tool_metadata.may_be_read remains FALSE for every tool, exactly as it was. This is a runtime bypass, so your stored tier data keeps meaning what it says, and the G-7 state check keeps telling you the truth about it.

What it actually costs

This section is not a warning label. It is the thing you are choosing, stated plainly so it stays legible three weeks from now.

Untrusted text and dangerous tools share one context window. The assistant reads text it did not write — workload labels, container names, log lines, annotations, the descriptions of the Core's own tools. Any of that can be authored by whoever deployed the workload. In normal operation that text sits alongside read-only tools, and the worst case is a misleading answer. In this mode it sits alongside tools that can read your secrets and delete your clusters.

The assistant is instructed to treat tool output as data, to confirm destructive actions with you first, and never to retry a mutating call that errored. Those are mitigations, not boundaries. In this mode the model's judgement is the only thing between a hostile string in a pod label and a destructive call, because the gate that used to be the boundary is switched off.

That includes confirm-to-mutate. Elsewhere in the product it is described as always on and not disableable; in this mode there is no server-side confirmation step for it to enforce, so it is a prompt instruction and nothing more. Read it as "the model has been told to ask first", not "the platform will stop an unconfirmed write".

Specifically, with the flag on:

  • Tools with no tier classification at all are offered. The Hub has tier rows for roughly 71 tools; a current Core announces around 192. Normally the ~121 unclassified ones fail closed. Here they are included.
  • Credential-disclosing tools (get_secret, list_stack_secrets, and friends) are reachable, and their output lands in the chat transcript.
  • The relay's read-verb allowlist and its independent tier re-read are both skipped.

What stays on

The flag suspends tier policy. It does not turn the Hub into an open door:

  • Tenancy. The bound instance must still belong to the caller's organization, re-checked at dispatch time on every call.
  • Who can switch it. Platform admin only, and every flip is in the admin audit log. Org admins cannot reach it.
  • Attribution. Every call is still tied to the human who asked for it.
  • Rate limits and the monthly token budget. Probing should not be able to bill without limit.
  • Abort on disconnect. Leaving the chat page stops the agent from starting further work and cancels the Hub's wait on a call already in flight. It does not cancel work the Core has already begun — a destructive operation that reached the cluster will finish. Verify state afterwards rather than assuming the close undid it.
  • Tool-name well-formedness. Malformed names are refused.
  • Description sanitisation. Tool descriptions are still stripped of control characters and length-capped. You opted into unrestricted tool access, not an unbounded prompt-injection channel.

Tool list limits

The unrestricted list is capped at 256 tools, which clears a current Core's ~192 with room to spare. If a Core ever announces more, the extra tools are dropped — and you are told, in the log line, in the chat page's banner, and in the assistant's own instructions.

Tools whose names are not lower_snake_case are also dropped, and counted in the log line. The relay refuses such names in both modes, so offering them would produce tools the assistant can see and call but never successfully dispatch. A silent cap would let the UI claim "unrestricted" while the model held two thirds of the surface, which is exactly the kind of quiet wrongness this mode is meant to expose rather than create.

Choosing the assistant's model

The model is configured in Settings › Admin › LLM Providers › Model per Job Type, in the AI Assistant row, alongside every other model in the product. The setting is deployment-wide and applies from the next message.

The Warden Agent settings tab shows which model is in use but no longer sets it — there is one place to change it.

The model and the sampling are the deployment's, not the user's

The chat page has no settings panel. Model, temperature, top_p, max_tokens and thinking are all decided server-side, and nothing a chat stores can change what a turn sends:

  • the model is the deployment-wide setting above, resolved fresh on every turn. The chat list reports that model on every row, whatever a chat's own stored model says. When no model is configured, the page says so and names this screen rather than offering a picker;
  • the sampling lives in src/lib/warden/chat/generation.ts, one module with the reasoning for each value written beside it — currently temperature 0.2, top_p 0.9, max_tokens 4096 per model call, thinking off, a 120-second per-call timeout, and character budgets for tool results and history. The values are provisional and meant to be retuned from the telemetry the turn now logs.

A turn is also bounded as a whole, by 16,384 output tokens (turn_output_budget_tokens) counted across every model call it makes, reasoning included — because that is what is billed. When a turn spends its budget while it is still calling tools, the agent stops gathering evidence, answers from what it has, and says so. max_tokens per call is headroom for one answer; this is the ceiling on the whole exchange, and it is the number the chat reports back as settings.max_tokens so the per-message meter measures against something true.

It is a stopping rule, not a hard cap: the budget is checked between rounds, so the forced final answer and — if that answer comes back empty — its retry are still made after it is spent, and a turn can overshoot by roughly 9,000 output tokens in the worst case.

A turn never ends in silence

Some models — the reasoning ones especially — can spend an entire model call producing chain-of-thought that the Hub discards, and return no answer at all. Left alone that looks exactly like a finished turn from the server's side: the agent stops calling tools, and the operator is left with a transcript of tool cards and nothing else.

Reasoning is now turned off in each provider's own spelling (vLLM's chat_template_kwargs, DeepSeek's thinking object; the deepseek-reasoner models are left alone, because there the parameter may be refused). If a provider rejects it anyway, the request is retried once without it and the answer is merely slow rather than lost. If a call comes back empty, the agent retries once with a larger budget and a longer deadline to match, and if that is empty too it writes an explanation into the transcript rather than ending quietly. A turn cut short in any of these ways is reported as length, not as a clean stop — so it reads as unfinished in the UI and in the logs alike.

The chat2 API still accepts and stores a settings patch, because the package's wire contract requires the round-trip. It is simply never read on the way to a provider: what a chat reports back is what the server will actually send.

Which Core answers

A chat is bound to one Core (one connected PodWarden instance), and the badge on each row in the sidebar names it.

With one connected Core the binding is made for you on the first message. With two or more, the sidebar shows a Core picker above the New-chat button and the choice is required — creating a chat without one is refused, because the alternative is being answered about a cluster you did not ask about, with no way to tell and no way back.

Only connected Cores are offered. An existing chat keeps its badge even while that Core is down.