Agent SOAT Tools and Preset Parameters
This tutorial shows how to give an agent access to platform documents using soat tools — and how to use preset parameters to lock a tool to a specific document ID so the model never has to guess it.
You will:
- Log in as admin.
- Create a project and an Ollama AI provider.
- Create two documents: a public note and a private note.
- Create a user alice with a policy that restricts her to the public document path.
- Create three soat tools:
docs_list-documents— lists documents in the project.docs_get-document— reads any document by ID (model supplies the ID).docs_update-document— updates the public document (ID is preset; model never sees it).
- Create an agent that uses these tools and attach it to alice's project.
- Run a generation as alice and observe the agent updating the correct document without being told its ID.
- Verify that alice cannot read or update the private document (permissions enforcement).
By the end you will understand:
- How to wire agent-side document tooling.
- How
preset_parameterseliminates the probabilistic risk of the model choosing the wrong ID. - How IAM policies are enforced even when an agent calls platform APIs on behalf of a user.
Prerequisites
- SOAT running locally with Ollama. Follow the Quick Start guide.
- An Ollama instance accessible at
http://ollama:11434with modelqwen2.5:0.5bpulled (ollama pull qwen2.5:0.5b). - CLI, SDK, or curl available. The server is at
http://localhost:5047.
- CLI
- SDK
- curl
export SOAT_BASE_URL=http://localhost:5047
import { SoatClient } from '@soat/sdk';
export SOAT_BASE_URL=http://localhost:5047
Step 1 — Log in as admin
Admin is the built-in superuser role. It bypasses policy evaluation entirely. See IAM — Authentication for details on JWT tokens and the admin role.
- CLI
- SDK
- curl
soat login-user --username admin --password Admin1234!
soat configure # paste the token when prompted
const soat = new SoatClient({ baseUrl: 'http://localhost:5047' });
const { data: session } = await soat.users.loginUser({
body: { username: 'admin', password: 'Admin1234!' },
});
const adminSoat = new SoatClient({
baseUrl: 'http://localhost:5047',
token: session!.token,
});
ADMIN_TOKEN=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin1234!"}' | jq -r '.token')
Step 2 — Create a project
Every resource in SOAT lives inside a project. Create one to hold the agent, documents, and tools.
- CLI
- SDK
- curl
PROJECT_ID=$(soat create-project --name "Notes Project" | jq -r '.id')
echo "Project: $PROJECT_ID"
const { data: project } = await adminSoat.projects.createProject({
body: { name: 'Notes Project' },
});
const projectId = project!.id;
PROJECT_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/projects" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Notes Project"}' | jq -r '.id')
echo "Project: $PROJECT_ID"
Step 3 — Create an Ollama AI provider
Set up a local AI provider backed by Ollama. This tutorial uses a local Ollama provider so it can run without external credentials. To connect xAI, OpenAI, Anthropic, or Amazon Bedrock instead, see Connect Third-Party LLMs.
- CLI
- SDK
- curl
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"
const { data: provider } = await adminSoat.aiProviders.createAiProvider({
body: {
project_id: projectId,
name: 'Ollama',
provider: 'ollama',
default_model: 'qwen2.5:0.5b',
},
});
const providerId = provider!.id;
PROVIDER_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/ai-providers" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"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
Create two documents: a public note the agent will update, and a private note it must not touch.
- CLI
- SDK
- curl
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"
const { data: publicDoc } = await adminSoat.documents.createDocument({
body: {
project_id: projectId,
title: 'Public Note',
content: 'Initial public content.',
path: '/notes/public/note.txt',
},
});
const publicDocId = publicDoc!.id;
const { data: privateDoc } = await adminSoat.documents.createDocument({
body: {
project_id: projectId,
title: 'Private Note',
content: 'Confidential information.',
path: '/notes/private/note.txt',
},
});
const privateDocId = privateDoc!.id;
PUBLIC_DOC_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/documents" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"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=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/documents" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"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 is allowed to run agent generations and access documents under /notes/public/*. She cannot read or modify documents at other paths. See Users, Policies, and IAM — SRNs for the full access-control model.
- CLI
- SDK
- curl
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": ["soat:'"$PROJECT_ID"':document:/notes/public/*"]
}
]
}' | jq -r '.id')
soat attach-user-policies \
--user-id "$ALICE_ID" \
--policy-ids '["'"$POLICY_ID"'"]'
const { data: alice } = await adminSoat.users.createUser({
body: { username: 'alice-agent-soat-tools', password: 'Alice1234!' },
});
const aliceId = alice!.id;
const { data: policy } = await adminSoat.policies.createPolicy({
body: {
name: 'alice-agent-soat-tools-notes-policy',
policy: {
statement: [
{ effect: 'Allow', action: ['agents:CreateAgentGeneration'] },
{ effect: 'Allow', action: ['documents:ListDocuments'] },
{
effect: 'Allow',
action: ['documents:GetDocument', 'documents:UpdateDocument'],
resource: [`soat:${projectId}:document:/notes/public/*`],
},
],
},
},
});
await adminSoat.users.attachUserPolicies({
path: { user_id: aliceId },
body: { policy_ids: [policy!.id] },
});
ALICE_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/users" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"username":"alice-agent-soat-tools","password":"Alice1234!"}' | jq -r '.id')
echo "Alice: $ALICE_ID"
POLICY_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/policies" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"alice-agent-soat-tools-notes-policy\",
\"policy\": {
\"statement\": [
{\"effect\": \"Allow\", \"action\": [\"agents:CreateAgentGeneration\"]},
{\"effect\": \"Allow\", \"action\": [\"documents:ListDocuments\"]},
{
\"effect\": \"Allow\",
\"action\": [\"documents:GetDocument\", \"documents:UpdateDocument\"],
\"resource\": [\"soat:$PROJECT_ID:document:/notes/public/*\"]
}
]
}
}" | jq -r '.id')
curl -s -X POST "$SOAT_BASE_URL/api/v1/policies/attach-user" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"user_id\": \"$ALICE_ID\", \"policy_id\": \"$POLICY_ID\"}"
Step 6 — Create soat tools
Create three agent tools. Notice the third tool — docs-write — has preset_parameters containing the public document's ID. The key uses camelCase (documentId) because soat tool schemas use camelCase property names internally. The model will never see the documentId field; it will be injected automatically at call time.
- CLI
- SDK
- curl
# Tool 1 — list documents
LIST_TOOL_ID=$(soat create-agent-tool \
--project-id "$PROJECT_ID" \
--name "docs" \
--type soat \
--actions '["list-documents"]' | jq -r '.id')
# Tool 2 — read any document (model supplies document_id)
READ_TOOL_ID=$(soat create-agent-tool \
--project-id "$PROJECT_ID" \
--name "docs" \
--type soat \
--actions '["get-document"]' | jq -r '.id')
# Tool 3 — update the public document (document_id is preset)
WRITE_TOOL_ID=$(soat create-agent-tool \
--project-id "$PROJECT_ID" \
--name "docs" \
--type soat \
--actions '["update-document"]' \
--preset-parameters '{"documentId": "'"$PUBLIC_DOC_ID"'"}' | jq -r '.id')
echo "List: $LIST_TOOL_ID"
echo "Read: $READ_TOOL_ID"
echo "Write: $WRITE_TOOL_ID"
const { data: listTool } = await adminSoat.agents.createAgentTool({
body: {
project_id: projectId,
name: 'docs',
type: 'soat',
actions: ['list-documents'],
},
});
const { data: readTool } = await adminSoat.agents.createAgentTool({
body: {
project_id: projectId,
name: 'docs',
type: 'soat',
actions: ['get-document'],
},
});
// document_id is preset — the model never sees this parameter
const { data: writeTool } = await adminSoat.agents.createAgentTool({
body: {
project_id: projectId,
name: 'docs',
type: 'soat',
actions: ['update-document'],
preset_parameters: { document_id: publicDocId },
},
});
const listToolId = listTool!.id;
const readToolId = readTool!.id;
const writeToolId = writeTool!.id;
LIST_TOOL_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/agents/tools" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"project_id\": \"$PROJECT_ID\",
\"name\": \"docs\",
\"type\": \"soat\",
\"actions\": [\"list-documents\"]
}" | jq -r '.id')
READ_TOOL_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/agents/tools" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"project_id\": \"$PROJECT_ID\",
\"name\": \"docs\",
\"type\": \"soat\",
\"actions\": [\"get-document\"]
}" | jq -r '.id')
WRITE_TOOL_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/agents/tools" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"project_id\": \"$PROJECT_ID\",
\"name\": \"docs\",
\"type\": \"soat\",
\"actions\": [\"update-document\"],
\"preset_parameters\": {\"documentId\": \"$PUBLIC_DOC_ID\"}
}" | jq -r '.id')
echo "List: $LIST_TOOL_ID"
echo "Read: $READ_TOOL_ID"
echo "Write: $WRITE_TOOL_ID"
The three tool names the model will see at runtime are:
| Tool name | Action | document_id visible to model? |
|---|---|---|
docs_list-documents | list documents | N/A |
docs_get-document | read a document | yes — model supplies it |
docs_update-document | update a document | no — injected from preset_parameters |
Step 7 — Create the agent
Create the agent and attach all three tools. The agent's instructions guide the model to use its tools when answering requests.
- CLI
- SDK
- curl
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-ids "[\"$LIST_TOOL_ID\", \"$READ_TOOL_ID\", \"$WRITE_TOOL_ID\"]" | jq -r '.id')
echo "Agent: $AGENT_ID"
const { data: agent } = await adminSoat.agents.createAgent({
body: {
project_id: projectId,
ai_provider_id: providerId,
name: 'Notes Agent',
instructions:
'You are a note-taking assistant. Use your tools to list, read, and update documents.',
tool_ids: [listToolId, readToolId, writeToolId],
},
});
const agentId = agent!.id;
AGENT_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/agents" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"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_ids\": [\"$LIST_TOOL_ID\", \"$READ_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 will call docs_update-document without knowing the document ID — the server injects it from preset_parameters.
- CLI
- SDK
- curl
# 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" \
-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 '.'
// Log in as alice
const aliceSoat = new SoatClient({ baseUrl: 'http://localhost:5047' });
const { data: aliceSession } = await aliceSoat.users.loginUser({
body: { username: 'alice-agent-soat-tools', password: 'Alice1234!' },
});
const aliceClient = new SoatClient({
baseUrl: 'http://localhost:5047',
token: aliceSession!.token,
});
// Run the generation
const { data: generation } = await aliceClient.agents.createAgentGeneration({
path: { agent_id: agentId },
body: {
messages: [
{
role: 'user',
content:
'Please update the public note with the content: Updated by the agent.',
},
],
},
});
console.log('Status:', generation!.status);
console.log('Result:', generation!.result);
ALICE_TOKEN=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{"username":"alice-agent-soat-tools","password":"Alice1234!"}' | jq -r '.token')
curl -s -X POST "$SOAT_BASE_URL/api/v1/agents/$AGENT_ID/generate" \
-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."
}
]
}' | jq '.'
Step 9 — Verify the update and permissions
Confirm the agent updated the public document and was blocked from accessing the private one. This demonstrates how IAM policies enforce path-based access at runtime.
Confirm the public document was updated
- CLI
- SDK
- curl
soat get-document --document-id "$PUBLIC_DOC_ID" | jq '.content'
# Expected: "Updated by the agent."
const { data: updated } = await adminSoat.documents.getDocument({
path: { document_id: publicDocId },
});
console.log('Content:', updated!.content);
// Expected: "Updated by the agent."
curl -s "$SOAT_BASE_URL/api/v1/documents/$PUBLIC_DOC_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '.content'
# Expected: "Updated by the agent."
Confirm alice cannot read the private document
- CLI
- SDK
- curl
curl -s "$SOAT_BASE_URL/api/v1/documents/$PRIVATE_DOC_ID" \
-H "Authorization: Bearer $ALICE_TOKEN" | jq '.'
# Expected: 403 Forbidden
const { error } = await aliceClient.documents.getDocument({
path: { document_id: privateDocId },
});
console.log('Error:', error);
// Expected: 403 Forbidden
curl -s "$SOAT_BASE_URL/api/v1/documents/$PRIVATE_DOC_ID" \
-H "Authorization: Bearer $ALICE_TOKEN" | jq '.'
# Expected: 403 Forbidden
The private document is inaccessible. If you asked the agent to update the private note, it would receive a 403 when trying to call docs_get-document with the private document's ID, and would report back that it is not permitted.
What happened
-
Tool creation with
preset_parameters: When you createddocs-write, you stored{ "documentId": "<public doc id>" }alongside the tool. The server strippeddocumentIdfrom the schema before registering the tool with the model (preset keys must use the camelCase form of the parameter name). -
Model's view: The model saw
docs_update-documentaccepting onlycontent,title,path,metadata, andtags— nodocumentIdin sight. This eliminates the risk of the model supplying a wrong or hallucinated ID. -
Execution: When the model called
docs_update-document, the server merged the presetdocument_idback in before dispatching thePATCH /api/v1/documents/{document_id}request. -
Permission enforcement: The request ran under alice's JWT. The platform's document permission check verified that alice's policy allows
documents:UpdateDocumentfor the path/notes/public/note.txt. The private document path falls outside/notes/public/*, so any attempt there returns 403.
Next steps
- Add more actions to the tools (e.g.,
search-documents) for richer agent workflows. - Use step rules to force the agent to call a specific tool first.
- Explore boundary policies to limit which actions agents can use at the agent level, independent of caller IAM policies.
- Read the agents module reference for the full list of soat actions and configuration options.