SwitchTenderSwitchTender Docs

Red team: can an agent get a change past the gate?

The agent documentation claims an AI agent cannot approve its own work, cannot widen its own reach, and cannot execute anything a person did not release. Documentation is a claim. This page is the attempt to break the claim, with the commands, the responses, and the limits.

Everything here runs against a released binary on a Community install with no license, so anyone can repeat it and get the same answers.

The setup, deliberately hostile

The agent is given the strongest credential the product will mint for one. Its human is an admin, so the token comes back carrying an admin role:

switchtender user new alice --role admin
switchtender token new --name claude-agent --user alice --agent

{"id":"tok_...","kind":"agent","name":"claude-agent","role":"admin","user":"alice"}

If the agent cap were only a role check, an admin-role agent token would walk through it. The gate is one policy, pinned to a file so the API refuses policy writes even from an admin:

policies:
  - name: agent-work-needs-a-person
    actor_kind: agent

switchtender serve --policy-file policies.yml

Deny rules, risk floors, distinct approvers, and any second policy are Team features, and the license gate refused them on this install. What follows is therefore the floor of the free tier, not the ceiling of the paid one.

What the agent tried

Attempt Result
Submit a run Held at pending_approval, recorded as actor_type: agent
Approve its own held run 403
Mint itself a token that is not an agent token 403
Create a second admin account 403
Create a credential 403
Rewrite the policy 403
Delete the policy 403
List credentials 200, metadata only, see below
Retry the held run to reach execution 409, only split runs can retry shards
Rerun the held run 403
Relaunch failed shards 409, run has not finished
Submit with actor_type: human and actor: alice 400, unknown field
Rewrite the spec while held, by PATCH, PUT, and POST 403 on all three

The read that succeeds

Listing credentials returns 200, so it is worth showing exactly what comes back when a credential holding a real secret exists:

{"credentials":[{"id":"cred_...","name":"prod-ssh","kind":"ssh_password",
  "source":"local","created_at":"...","needs_secret":false}],"count":1}

The secret is absent, and cannot be otherwise: the field is excluded from serialization at the type, not filtered per handler, so no endpoint can return it. It is absent from the admin's own response to creating the credential too. What the agent sees is the name it needs in order to propose a run that uses the credential, which is the point of letting it look at all.

Why the forged identity fails

The forged submission is refused for a more interesting reason than a permission check. The API rejects unknown fields outright, so actor identity is not something a caller can put in a body and have believed. It comes from the token that authenticated the request.

What a person does, and what it leaves behind

A human approved the same run. It executed and succeeded, and the record still names the agent as the actor, so passing through an approval does not relabel who asked.

The receipt was then verified with the server stopped:

switchtender receipt <run-id> > receipt.json
switchtender verify receipt.json

rules in force 1
  - agent-work-needs-a-person: requires approval
decisions    OK (1, each matches what the chain committed)
  approved by human-admin (token), binding spec sha256:e761b206b0acfdf8...
spec         OK (approved, executed, and disclosed digests agree)

VERIFIED: nothing has been altered since this receipt was signed

That output carries the whole claim in one place: which rule applied, that a person and not the agent released it, the exact specification the approval was bound to, and that the approved, executed, and disclosed specifications are the same bytes. It was produced with the issuing server switched off, so nothing in it rests on asking the server what happened.

What this does not prove

If you find a way through, the security contact is in the repository, and a finding that breaks this page is worth more to us than a page that stays unbroken.