Skip to main content

Agent SOAT Tools and Preset Parameters

Give an agent access to platform documents with builtin tools, and lock a tool to one document ID with preset parameters. You create a public and a private note, a restricted user alice, three builtin tools (docs_list-documents, docs_get-document, docs_update-document with the public document's ID preset) and an agent, then verify the agent updates the right document and alice's policy blocks the private one.

Prerequisites​

  • SOAT running locally (Quick Start).
  • Ollama at http://ollama:11434 with qwen2.5:0.5b pulled (ollama pull qwen2.5:0.5b).
  • CLI, SDK, or curl; server at http://localhost:5047.
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​

Every resource lives inside a project.

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

Step 3 — Create an Ollama AI provider​

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

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

Step 4 — Create documents​

Two documents: a public note the agent updates and a private note it must not touch.

PUBLIC_DOC_ID=$(soat create-document \
--project-id "$PROJECT_ID" \
--title "Public Note" \
--content "Initial public content." \
--path "/notes/public/note.txt" | jq -r '.id')
echo "Public doc: $PUBLIC_DOC_ID"

PRIVATE_DOC_ID=$(soat create-document \
--project-id "$PROJECT_ID" \
--title "Private Note" \
--content "Confidential information." \
--path "/notes/private/note.txt" | jq -r '.id')
echo "Private doc: $PRIVATE_DOC_ID"

Step 5 — Create user alice with a restricted policy​

Alice may run agent generations and access documents under /notes/public/* only. See Users, Policies and IAM — SRNs.

ALICE_ID=$(soat create-user --username alice-agent-soat-tools --password Alice1234! | jq -r '.id')
echo "Alice: $ALICE_ID"

POLICY_ID=$(soat create-policy \
--name "alice-agent-soat-tools-notes-policy" \
--document '{
"statement": [
{
"effect": "Allow",
"action": ["agents:CreateAgentGeneration"]
},
{
"effect": "Allow",
"action": ["documents:ListDocuments"]
},
{
"effect": "Allow",
"action": ["documents:GetDocument", "documents:UpdateDocument"],
"resource": ["srn:'"$PROJECT_ID"':document:/notes/public/*"]
}
]
}' | jq -r '.id')

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

Step 6 — Create builtin tools​

Three tools. docs-write carries preset_parameters with the public document's ID, keyed by the snake_case wire name (document_id); the model never sees the field and the server injects it at call time.

# Tool 1 — list documents
LIST_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name "docs" \
--type builtin \
--actions '["list-documents"]' | jq -r '.id')

# Tool 2 — read any document (model supplies document_id)
READ_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name "docs" \
--type builtin \
--actions '["get-document"]' | jq -r '.id')

# Tool 3 — update the public document (document_id is preset)
WRITE_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name "docs" \
--type builtin \
--actions '["update-document"]' \
--preset-parameters '{"document_id": "'"$PUBLIC_DOC_ID"'"}' | jq -r '.id')

echo "List: $LIST_TOOL_ID"
echo "Read: $READ_TOOL_ID"
echo "Write: $WRITE_TOOL_ID"

Tool names the model sees:

Tool nameActiondocument_id visible to model?
docs_list-documentslist documentsN/A
docs_get-documentread a documentyes — model supplies it
docs_update-documentupdate a documentno — injected from preset_parameters

Step 7 — Create the agent​

Create the agent with all three tools attached.

AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$PROVIDER_ID" \
--name "Notes Agent" \
--instructions "You are a note-taking assistant. Use your tools to list, read, and update documents." \
--tool-bindings "[{\"tool_id\":\"$LIST_TOOL_ID\"},{\"tool_id\":\"$READ_TOOL_ID\"},{\"tool_id\":\"$WRITE_TOOL_ID\"}]" | jq -r '.id')
echo "Agent: $AGENT_ID"

Step 8 — Log in as alice and run a generation​

Alice asks the agent to update the public note via a session. The agent calls docs_update-document without the document ID; the server injects it from preset_parameters.

# Log in as alice
ALICE_TOKEN=$(soat login-user --username alice-agent-soat-tools --password Alice1234! | jq -r '.token')

# Run the generation
RESULT=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/agents/$AGENT_ID/generate?wait=true" \
-H "Authorization: Bearer $ALICE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "Please update the public note with the content: Updated by the agent."
}
]
}')

echo "$RESULT" | jq '.'

Reuse a tool result as the generation input​

A generation can start with message content of type tool_output: the server executes the referenced tool, applies output_path, and feeds the extracted value to the model as the user message.

messages carries user and assistant turns only; steer the agent with its instructions. A role: "system" entry is rejected with 400 SYSTEM_MESSAGE_NOT_ALLOWED.

TOOL_OUTPUT_RESULT=$(soat create-agent-generation --wait true \
--agent-id "$AGENT_ID" \
--messages '[
{
"role":"user",
"content": {
"type": "tool_output",
"tool_id": "'"$READ_TOOL_ID"'",
"action": "get-document",
"input": {"document_id": "'"$PUBLIC_DOC_ID"'"},
"output_path": ".content"
}
}
]')

echo "$TOOL_OUTPUT_RESULT" | jq '.'

Step 9 — Verify the update and permissions​

Confirm the public document was updated and the private one is blocked by alice's IAM policy.

Confirm the public document was updated​

soat get-document --document-id "$PUBLIC_DOC_ID" | jq '.content'
# Expected: "Updated by the agent."

Confirm alice cannot read the private document​

SOAT_TOKEN="$ALICE_TOKEN" \
soat get-document --document-id "$PRIVATE_DOC_ID" | jq '.'
# Expected: 403 Forbidden

Asking the agent to update the private note yields a 403 on docs_get-document, which the agent reports back.


Step 10 — Call a tool directly via REST​

Any non-client tool can be invoked without an agent or generation. The list-documents tool from step 6 has project_id preset, so the body needs no parameters.

soat call-tool \
--tool-id "$LIST_TOOL_ID" \
--action "list-documents"

preset_parameters are merged before dispatch and alice's IAM policy still applies. For http tools omit action and pass parameters in input; for mcp tools set action to the MCP tool name.


What happened​

  1. docs-write stored { "document_id": "<public doc id>" } as preset_parameters; the server stripped document_id from the schema shown to the model (preset keys are the tool's snake_case parameter names).
  2. The model saw docs_update-document accepting only content, title, path, metadata and tags, so it could not supply a wrong ID.
  3. On call, the server merged document_id back in before dispatching PATCH /api/v1/documents/{document_id}.
  4. The request ran under alice's JWT; her policy allows documents:UpdateDocument on /notes/public/note.txt, and paths outside /notes/public/* return 403.

Next steps​

  • Add more actions (e.g. search-knowledge) to the tools.
  • Step rules — force a specific tool call first.
  • Boundary policies — cap actions per agent, independent of the caller.
  • Agents reference — all soat actions and options.