SOAT — open-source infrastructure for production-ready AI agBLOCK
SOAT — open-source infrastructure for production-ready AI agents.
Overview
From the repository's own README, as read at the audited commit. Badges and raw HTML are left out.
[](https://opensource.org/licenses/Apache-2.0) [](https://safeskill.dev/scan/ttoss-soat) [](https://hub.docker.com/r/ttoss/soat) [](https://hub.docker.com/r/ttoss/soat)
SOAT is open-source infrastructure for building AI applications. One self-hostable Node.js server gives you IAM, file and document storage with vector search, multimodal ingestion, conversational memory, agent orchestration, DAG-based multi-agent workflows, retrieval-augmented generation, guardrails and human approvals, usage metering and quotas, versioned agents with canary rollout, declarative stack deployment, a built-in web console, and a full Model Context Protocol server with first-party OAuth — backed by PostgreSQL.
You bring the product. SOAT handles the infrastructure layer.
Why SOAT?
Shipping AI applications means rebuilding the same infrastructure on every project: users, API keys, encrypted secrets, file storage, embeddings, conversation history, agent tool calling, traces, observability. SOAT solves all of it once and exposes it through four equivalent surfaces — REST, MCP, CLI, and TypeScript SDK — plus a built-in web console on top of them, so the same operation runs the same way whether you call it from a backend, Claude Desktop, a CI script, the bundled UI, or your own frontend.
SOAT organizes that surface around the four layers of an agent system: the harness (what an agent can reach and what it is forbidden), t
9539368356b6OBSERVED · 2026-10-12Connect
Built from this server's own package name, version and transport as found in its source — not copied from anyone's documentation, so it cannot drift against a page we do not control. Replace the environment placeholders with a token scoped to the least it needs.
claude mcp add website --env AI_PROVIDER_ALLOW_AMBIENT_CREDENTIALS=${AI_PROVIDER_ALLOW_AMBIENT_CREDENTIALS} --env CHANNEL_HANDLER_SECRET=${CHANNEL_HANDLER_SECRET} --env DATABASE_PASSWORD=${DATABASE_PASSWORD} --env EMBEDDING_API_KEY=${EMBEDDING_API_KEY} -- npx -y @soat/[email protected]{
"mcpServers": {
"website": {
"command": "npx",
"args": [
"-y",
"@soat/[email protected]"
],
"env": {
"AI_PROVIDER_ALLOW_AMBIENT_CREDENTIALS": "${AI_PROVIDER_ALLOW_AMBIENT_CREDENTIALS}",
"CHANNEL_HANDLER_SECRET": "${CHANNEL_HANDLER_SECRET}",
"DATABASE_PASSWORD": "${DATABASE_PASSWORD}",
"EMBEDDING_API_KEY": "${EMBEDDING_API_KEY}"
}
}
}
}Exposed tools (53)
39 read · 13 write · 1 destructive. Blast radius: 1 tool can delete or overwrite — an agent that can be talked into calling a tool can be talked into calling this one.
| Tool | Risk | Description |
|---|---|---|
2026-09-11-memory-tags-to-jsonb | read | Memory tags from text[] to key-value jsonb, aligning them with every other tagged resource. |
2026-09-16-memories-rename-and-provenance | write | memories -> memory_stores and memory_entries -> memories, with source_conversation_id and source_generation_id collapsed into source_id. |
2026-09-16-memory-assertions-and-shared-content | write | memories.content/embedding move to a shared memory_contents row, memory_assertions records every write, and generations.conversation_id plus the memory_stores threshold pair are added. |
2026-09-16-memory-rules-from-agent-extraction | read | memory_rules is added and owned by the memory store, memory_assertions.rule_id becomes a foreign key into it, and every agent with knowledge_config.extraction becomes one rule before the field is stripped. |
2026-09-17-memory-assertions-declared | read | memory_assertions.declared records whether a supersede was declared by the caller or chosen by the thresholds. |
2026-09-18-activity-entry-generation-id | read | activity_entries.generation_id is promoted from the detail blob to an indexed column so the feed can be filtered by generation. |
2026-09-18-orchestration-run-idempotency-key | write | orchestration_runs.idempotency_key, unique per project, so a retried run start cannot start a second run. |
2026-09-20-orchestration-node-execution-dispatches | read | orchestration_node_executions.dispatches counts the dispatches one attempt issued, so a redelivered node execution is visible on the run. |
2026-09-20-trigger-tool-context | write | triggers.tool_context stores the caller context a firing forwards, so a scheduled run can reach a tool that authorizes per call. |
2026-09-21-document-metadata-jsonb | read | documents.metadata from serialized text to jsonb, so a filter can reach the fields inside a caller bag. |
2026-09-21-document-versions | write | documents.version counts content versions, so a write can be archived and restored the way an agent config can. |
2026-09-21-durable-event-firings | read | trigger_firings gains idempotency_key, causation_chain, attempts and lease_expires_at, so an event firing survives the process that matched it. |
2026-09-21-memory-version | write | memories.version counts concurrent writes, so PUT /memories/{id} can state a write precondition. |
2026-09-22-file-metadata-jsonb | read | files.metadata from serialized text to jsonb, so the column and the wire field carry one shape. |
2026-09-22-tag-bag-gin-indexes | read | A GIN index with jsonb_path_ops over every tags column, so containment is a lookup rather than a scan. |
2026-09-24-api-key-sha256 | read | api_keys.key_hash_sha256, the SHA-256 a key is verified by, and api_keys.key_hash nullable, since a SHA-256 key carries no bcrypt hash. |
2026-09-24-foreign-key-nullability | read | agents.ai_provider_id, chats.ai_provider_id, api_keys.project_id and conversation_messages.actor_id nullable, as their models declare. |
2026-09-24-generation-idempotency-key | read | generations.idempotency_key, unique per project, and the digest of the request it names, so a retried generation cannot run the agent twice. |
2026-09-24-orchestration-output-mapping | write | orchestrations.output_mapping declares the shape of a run output, independent of terminal node ids. |
2026-09-25-usage-event-generation-public-id | read | usage_events.generation_public_id, a durable copy of the generation public id that totals.distinct.generations counts over. |
2026-09-26-usage-event-document-and-memory-store | read | usage_events.document_id and usage_events.memory_store_id, what an embedding event embedded. |
2026-09-26-usage-event-durable-public-ids | read | usage_events gains a public id beside each attribution FK, which totals.distinct counts so a deleted entity stays counted. |
2026-09-27-decider-tool-backend | read | deciders.tool_id, the tool backend, and deciders.agent_id nullable. |
2026-09-29-eval-group-by | write | evals.group_by names the item metadata key a run aggregates scores by. |
2026-09-29-eval-run-decider-versions | write | eval_runs.decider_versions pins the decider version each decider scorer grades under. |
2026-09-30-agent-records-outlive-agent | read | generations, traces and sessions keep the agent public id and survive the agent being deleted. |
2026-09-30-ingestion-rules-outlive-converter | read | ingestion rules keep their converter public id and survive the converter being deleted. |
2026-09-30-usage-event-publisher-project | read | usage_events.publisher_project_id, the project that owns a resource called through a share. |
2026-10-01-generation-input-modalities | read | generations.input_modalities, the part types a turn |
2026-10-01-price-book-resource | read | price_books.resource and quantity, a row pricing one tool by what a call consumed. |
2026-10-01-share-cap | read | shares.cap, the calls each acceptance may make per window. |
2026-10-07-decisions-openai-shape | read | Decider questions and decision answers in the OpenAI Decisions API shape; decisions.questions for inline decisions. |
Agents | read | Manage AI agents |
Files | read | Manage files |
Renamed | read | now described |
a | read | b |
channel | read | A messaging channel. |
create-share-tool | write | Reads text off an image |
delete_everything | destructive | Removes it all |
env-file | read | Path to .env file for parameter variable substitution |
fmlife-hook-v2 | read | Updated description |
fmlife-store | read | original |
get-doc-page | read | Fetches the full content of a SOAT documentation page by URL. Use get-docs first to obtain the list of valid page URLs. |
get-docs | read | Returns the SOAT documentation index in llms.txt format. The response lists all available documentation pages with their URLs. Use get-doc-page with a URL from this index to read a specific page. |
get_order_status | write | Looks up an order in the store database. |
get_weather | read | Returns the weather for a city. |
inline-weather-tool | read | Gets the weather |
kept | write | set out of band |
lookupOrder | write | Look an order up |
my-http-tool-updated | read | Updated description |
renamed-tool | read | Updated desc |
template-path | read | Path to template file (JSON or YAML). Alias: --template-file |
test_channel | read | A test channel. |
Trust audit
BLOCKgrade F · trust 48/100 Do not install this without reading the findings. The audit found something that could harm you or your machine.
| Layer | What it checks | Result |
|---|---|---|
| L0 | Provenance & inventory | PASS |
| L1 | Static analysis of the code | FAIL |
| L2 | Instruction surface (what it tells the agent) | PASS |
| L3 | Class-specific surface | WARN |
| L4 | Behavioural (sandbox) | SKIPPED |
What the source does
- Filesystem
- declared (3 observation(s))
- Network
- declared (7 observation(s))
- Shell
- declared (2 observation(s))
- Dependencies
- not all pinned
- Secrets in source
- found
Findings (25)
const doc: unknown = yaml.load(
key === null ? Math.floor(Math.random() * BUCKETS) : bucketForKey(key);
).toEqual({ apiKey: 'AIzaSyExpressModeKey' });{ secret: 'not-valid-ciphertext' },private_key: '-----BEGIN PRIVATE KEY-----\nnot-a-real-key\n-----END',
private_key: '-----BEGIN PRIVATE KEY-----\nnot-a-real-key\n-----END',
private_key: '-----BEGIN PRIVATE KEY-----\nnot-a-real-key\n-----END',
private_key: '-----BEGIN PRIVATE KEY-----\nnot-a-real-key\n-----END',
'-----BEGIN PRIVATE KEY-----\nx\n-----END PRIVATE KEY-----',
delete_everything
.commitlintrc.js
.lintstagedrc.js
.ncurc.js
.prettierignore
.prettierrc.js
const spec = yaml.load(
const spec = yaml.load(
return match ? ((yaml.load(match[1]) ?? {}) as Record<string, unknown>) : {};const noMarkdownUris = new Function(
const handler = new Function('appendIndexHtml', `${source}\nreturn handler;`)(return crypto.createHash('md5').update(value, 'utf8').digest('hex');const SPECS_DIR = path.resolve(__dirname, '../../server/src/rest/openapi/v1');
return import('../../src/index');import { resolveClient } from '../../../src/config';import { routes } from '../../../src/generated/routes';Gates applied: no_behavioural_pass.
9539368356b6full audit observations/trust-audit/mcp-server/ttoss__soat.json · Report an issue / request a re-scanAudit history
Every audit this server has had. A grade with a past is a grade somebody is still checking.
| Date | Source | Verdict | Grade | Score | Change |
|---|---|---|---|---|---|
| 2026-10-12 | 9539368356b6 | BLOCK | F | 48 | first audit |
Questions
What is the @soat/cli MCP server?
SOAT — open-source infrastructure for production-ready AI agents.
What tools does @soat/cli expose?
53 in total: 39 read-only, 13 that write, and 1 that can delete or overwrite (delete_everything). Every one is listed on this page with its risk.
Is @soat/cli safe to connect to an agent?
No — not without reading the findings first. The audit graded it F (48/100) and found 1 critical or high issue in the source. Each one is listed on this page with the file and line it is on. Separately from the audit: 1 of its tools can destroy data, so scope the token you give it to what you actually need.
What credentials does @soat/cli need?
It reads AI_PROVIDER_ALLOW_AMBIENT_CREDENTIALS, CHANNEL_HANDLER_SECRET, DATABASE_PASSWORD, EMBEDDING_API_KEY, EMBEDDING_INPUT_1M_TOKEN_PRICE_USD, FILES_S3_KEY_PREFIX, FORMATION_HANDLER_SECRET, GOOGLE_APPLICATION_CREDENTIALS, JWT_SECRET, MAX_COMPLETION_TOKENS, OPENAI_API_KEY and SECRETS_ENCRYPTION_KEY from the environment. Give it a token scoped to the least it needs — an agent that can be talked into calling a tool can be talked into calling it with your credentials.
How current is this page?
The grade is for one exact copy of the source (9539368356b6), read on 2026-10-12. The repository is watched and re-audited when it changes.