Skip to main content

Bound an Agent with a Boundary Policy

SOAT forwards the caller's credential into every builtin action an agent performs, so a chain is capped by the caller's token. That cap is not least privilege: a read-only summarizer running under a caller who may write documents executes at the caller's ceiling.

boundary_policy is a policy document stored on the agent that limits which builtin actions that agent may perform, whoever calls it. Effective permission = caller policy ∩ agent boundary, as with API keys.

You give alice broad document permissions, bind an agent to a document-writing tool, watch the agent's write be refused by a read-only boundary, then perform the same write directly as alice.

Prerequisites​

export SOAT_BASE_URL=http://localhost:5047

Step 1 — Log in as admin​

Admin bypasses policy evaluation — see IAM — Authentication.

soat login-user --username admin --password Admin1234!
soat configure # paste the token when prompted

Step 2 — Create a project and an AI provider​

A project and a local Ollama AI provider. For other providers see Connect Third-Party LLMs.

PROJECT_ID=$(soat create-project --name "Boundary Project" | jq -r '.id')

PROVIDER_ID=$(soat create-ai-provider \
--project-id "$PROJECT_ID" \
--name "Ollama" \
--provider "ollama" \
--default-model "qwen2.5:0.5b" | jq -r '.id')

echo "Project: $PROJECT_ID"
echo "Provider: $PROVIDER_ID"

Step 3 — Create the document the agent will try to overwrite​

Create a document; Step 7 asserts its content is unchanged.

DOC_ID=$(soat create-document \
--project-id "$PROJECT_ID" \
--title "Quarterly Note" \
--content "ORIGINAL CONTENT" \
--path "/notes/quarterly.txt" | jq -r '.id')
echo "Document: $DOC_ID"

Step 4 — Create alice with broad document permissions​

Alice may read and write every document in the project. See Users, Policies and SRNs for the resource scoping.

ALICE_ID=$(soat create-user --username alice-boundary --password Alice1234! | jq -r '.id')

POLICY_ID=$(soat create-policy \
--name "alice-boundary-full-documents" \
--document '{
"statement": [
{
"effect": "Allow",
"action": [
"documents:*",
"agents:CreateAgentGeneration",
"agents:GetAgent",
"traces:GetTrace",
"traces:ListTraces"
],
"resource": ["srn:'"$PROJECT_ID"':*:*"]
}
]
}' | jq -r '.id')

soat attach-user-policies \
--user-id "$ALICE_ID" \
--policy-ids '["'"$POLICY_ID"'"]'

soat login-user --username alice-boundary --password Alice1234!
soat configure --profile alice

Step 5 — Bind a write tool to the agent​

A builtin tool for update-document, with document_id pinned by preset_parameters. The tool itself is unrestricted.

WRITE_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name "docs" \
--type builtin \
--actions '["update-document"]' \
--preset-parameters '{"document_id": "'"$DOC_ID"'"}' | jq -r '.id')
echo "Write tool: $WRITE_TOOL_ID"

Step 6 — Create the agent with a read-only boundary​

  • boundary_policy allows documents:GetDocument only. Boundaries are deny-by-default, so the bound update-document tool is refused.
  • step_rules forces the tool call on step 1 so the refusal does not depend on qwen2.5:0.5b volunteering it. See Step Rules.
AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$PROVIDER_ID" \
--name "Bounded Summarizer" \
--instructions "You summarize notes. Use your tools when asked." \
--tool-bindings "[{\"tool_id\":\"$WRITE_TOOL_ID\"}]" \
--max-steps 2 \
--step-rules '[{"step":1,"tool_choice":{"type":"tool","tool_name":"docs_update-document"}}]' \
--boundary-policy '{
"statement": [
{
"effect": "Allow",
"action": ["documents:GetDocument"],
"resource": ["*"]
}
]
}' | jq -r '.id')
echo "Agent: $AGENT_ID"

:::tip Write boundaries as allow-lists An allow-list stays correct as the agent gains tools: a newly bound builtin action is refused until the boundary is widened. A deny-list permits every action nobody thought to deny. :::


Step 7 — Run the agent as alice, and watch the write fail​

The boundary is evaluated before dispatch: the tool returns an error result and the generation still completes.

soat --profile alice create-agent-generation --wait true \
--agent-id "$AGENT_ID" \
--messages '[{"role":"user","content":"Replace the note content with: OVERWRITTEN BY THE AGENT."}]' \
| jq '.status'

# The boundary refused the write — content is untouched.
soat --profile alice get-document --document-id "$DOC_ID" | jq -r '.content'

# Verify it: this exits non-zero if the agent managed to change anything.
soat --profile alice get-document --document-id "$DOC_ID" \
| jq -r '.content' | grep -qx "ORIGINAL CONTENT"

The tool result the model received:

{ "error": "Forbidden: boundary policy denies update-document" }

Step 8 — Prove the ceiling was the agent's, not alice's​

Same user, token and document, written directly via Documents.

soat --profile alice update-document \
--document-id "$DOC_ID" \
--content "UPDATED BY ALICE DIRECTLY" | jq '.id'

CONTENT=$(soat --profile alice get-document --document-id "$DOC_ID" | jq -r '.content')
echo "Content after alice wrote directly: $CONTENT"

The forwarded credential caps the chain from above; the boundary caps each agent from below. A prompt injection inherits the agent's ceiling, not alice's.


Step 9 — A mistyped action is rejected at write time​

Action strings are validated on agent create and update, so a mis-named action is rejected instead of silently matching nothing.

# → expect-fail
soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$PROVIDER_ID" \
--name "Typo Agent" \
--boundary-policy '{"statement":[{"effect":"Allow","action":["documents:GetDocumnet"],"resource":["*"]}]}'

Action names: Permissions Reference.


What the boundary does and does not cover​

Governed by boundary_policy
builtin tools (platform actions)Yes — every action is checked before dispatch
The built-in write_memory toolYes — fails closed when the boundary denies the memory write actions
http, mcp, client toolsNo — these execute outside the platform

For tool types the boundary cannot reach, use Guardrails or Approvals.

A boundary applies per hop: a sub-agent called through create-agent-generation resolves its tools under its own boundary; the orchestrator's permissions do not widen it.

Next Steps​