Skip to content

[Bug] Mutating tools (deploy_bicep/destroy_azure) not gated server-side by lifecycle stage — authorization gap #5

Description

@sjohnston1972

Background

Every chat turn happens in one of five lifecycle stages: build, view, push, teardown, free (see README "Lifecycle stages"). The intent is that mutating tools are only reachable in the stage that matches — you deploy in push, delete in teardown, and build/view/free are read-only.

The stage arrives on the request body as body.stage (backend/src/routes/chat.ts:33-44).

Problem / Goal

Stage restrictions are enforced only as an English instruction in the system prompt, never in code. In chat.ts the stage maps to a natural-language STAGE_GUARD block (chat.ts:46-57) that tells Claude what's allowed. But:

  1. The actual tool list handed to the model is built by getClaudeTools(cloud) in backend/src/claude/tool-bridge.ts:79, which does not take stage into account at alldeploy_bicep, destroy_azure (and the AWS equivalents, plus every mutating Azure MCP *_create/*_delete tool) are always present regardless of stage.
  2. The dispatcher callMcpTool (tool-bridge.ts:169) executes whatever tool the model names, with no stage check.

So the only thing stopping a destroy during a build/view/free turn is the model choosing to obey the prompt. A prompt-injection payload (from an MCP tool result, an Azure resource name, template text, etc.) or a plain model slip can invoke destroy_azure or deploy_bicep in a read-only stage. stage is also fully client-controlled, so there is no server-side authority binding a mutation to the right stage.

This is an authorization gap: the security boundary is advisory, not enforced.

Where to look

  • backend/src/routes/chat.ts:46-57 (STAGE_GUARD), :186-212 (tool loading + stage handling).
  • backend/src/claude/tool-bridge.ts:79-158 (getClaudeTools — no stage param), :169-207 (callMcpTool dispatcher).
  • backend/src/claude/custom-tools.ts — the mutating custom tools.

Suggested approach

Defence in depth — do both:

  1. Filter the tool list by stage so the model literally cannot see mutating tools in read-only stages (build/view/free). Thread stage into getClaudeTools and drop deploy_bicep/destroy_azure/deploy_cloudformation/destroy_aws and known mutating MCP ops when the stage is read-only; restrict push to deploy tools and teardown to destroy tools. Mind the prompt-cache note below.
  2. Guard the dispatcher so that even if a mutating tool name reaches callMcpTool in a disallowed stage, it is refused server-side with is_error: true before any cloud call.

Prompt-cache consideration: today the tool list is cached once per cloud and forms the cached prompt prefix (tool-bridge.ts:6-15). Per-stage tool lists will create a few more cache variants (one prefix per stage) — acceptable; document it. Do not try to keep a single prefix by leaving the tools in.

To reproduce: send a POST /api/chat with stage: "build" and a message that coaxes a destroy_azure call (or temporarily hardcode a destroy tool_use). Observe that today the call executes; after the fix it is unavailable/refused.

Acceptance criteria

  • In build, view, and free stages, deploy_bicep, destroy_azure, and the AWS deploy/destroy tools are absent from the tool list AND refused by the dispatcher.
  • In push, destroy tools are unavailable; in teardown, deploy tools are unavailable.
  • A mutating tool call in a disallowed stage returns a clear is_error result and performs no cloud action.
  • Legitimate deploy in push and destroy in teardown still work.

Testing

  • Unit test: getClaudeTools(cloud, stage) returns the expected tool subset per stage.
  • Unit/integration test: dispatcher refuses a destroy_azure call when stage is build.

Out of scope

  • Fixing command injection inside the tools (separate parent).
  • Requiring an explicit human confirmation token for destroy (see the destroy-safety parent).

Sub-issues: (1) stage-aware tool filtering, (2) server-side dispatcher guard.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions