Skip to content

Security & Capabilities

Talon uses a default-deny capability model. Every host-tool call is rejected unless the executing persona has an explicit grant. No capability, no execution — the tool isn’t even listed to the agent.

Capabilities are declared in the persona config under capabilities.allow and capabilities.requireApproval. Both lists use the same label syntax: domain.action:scope.

personas:
- name: assistant
capabilities:
allow:
- memory.access:thread
- net.http:egress
- channel.send:my-telegram
- schedule.manage:own
- subagent.invoke
- subagent.background
requireApproval:
- db.query:own

The engine enforces this at two points:

  1. Listing — only tools with a matching capability label are exposed to the agent in the first place
  2. Execution — the host-tools bridge re-checks the grant immediately before running the tool
LabelControls
channel.send:<channel>Send messages to a specific channel
channel.send:*Send to any channel
schedule.manage:ownCreate/modify/delete schedules owned by this persona
memory.access:threadRead/write per-thread structured memory
net.http:egressFetch external URLs
db.query:ownExecute read-only database queries
subagent.invokeInvoke sub-agents
subagent.backgroundLaunch background agent workers
fs.readRead files from the filesystem
fs.writeWrite files to the filesystem
persona.send:*Send tasks to other personas via A2A and list available personas

Wildcard scopes (channel.send:*) match any suffix. Specific scopes (channel.send:my-telegram) match that name only.

Any capability can be placed in requireApproval instead of allow. The tool is still visible to the agent, but before execution the daemon sends a confirmation prompt to the originating channel. The agent blocks until the human responds.

capabilities:
allow:
- memory.access:thread
requireApproval:
- db.query:own
- net.http:egress

Approval requests time out after a configurable duration. If the timeout is reached with no response, the tool call is rejected.

Every side-effecting operation — tool calls, channel sends, approval grants and rejections — is written to the audit_log table. Entries are append-only: there are no UPDATE or DELETE paths.

Each record contains:

FieldDescription
run_idThe agent run that triggered the action
thread_idThe conversation thread
persona_idThe executing persona
actione.g., tool.execute, channel.send, approval.grant
toolTool name (if applicable)
request_idMCP request ID for correlation
detailsJSON blob with action-specific fields
created_atUnix timestamp

Query it directly:

Terminal window
npx talonctl db-query "SELECT action, tool, created_at FROM audit_log ORDER BY created_at DESC LIMIT 20"

When an agent uses db_query, the query passes through five independent layers before execution:

  1. Regex pre-check — only SELECT is accepted; write keywords are rejected immediately
  2. Table whitelist — only memory_items, schedules, messages, and threads are queryable
  3. Thread/persona scoping — Talon auto-injects WHERE thread_id = ? and WHERE persona_id = ? clauses
  4. Row limit — results are capped at 1,000 rows regardless of the query
  5. Read-only connection — the query runs on a { readonly: true } SQLite connection

UNION, EXCEPT, INTERSECT, CTEs, and subqueries are rejected. This combination makes it practically impossible for a query to leak data from another thread or persona.

Credentials never go in config files as plaintext. Use environment variable substitution instead.

In talond.yaml:

channels:
- type: telegram
token: ${TELEGRAM_BOT_TOKEN}

At startup Talon loads .env from the working directory and substitutes all ${ENV_VAR} references. Two CLI commands help:

Terminal window
# show resolved config with secrets masked
npx talonctl config-show
# report missing variables before the daemon starts
npx talonctl env-check

config-show replaces secret values with *** in output. It never writes them to disk.