Skip to content

Tools & approvals

Agents act on your knowledge base through tools: typed functions with a Zod input schema. Reads and reversible edits run freely; outbound and irreversible actions pause the run for your approval; and every credential a tool needs is acquired through a broker rather than handed out ambiently. This page covers the tool surface, the approval model, and the two ways a run talks back to you mid-flight.

Every tool is a registry entry of the shape:

{
description: string,
schema: ZodType, // validated input
requires?: string[], // capability reach (see the broker below)
needsApproval: boolean | ((args) => boolean),
alwaysGate?: boolean, // trust floor: gate under every policy
execute(args, { sql, runId?, caps? }): ToolResult
}

The full, always-current catalog lives in the agent tools reference. The sections below cover the shape of it.

The kb tools let an agent search, read, and edit pages. Editing never touches the filesystem: the agent submits Markdown, and the OKF codec diffs it into node commands that land through the same command model as a human edit.

Tool What it does
kb.search {q, kinds?} Hybrid search, top 10 results.
kb.grep {pattern, regex?, limit?} Exact (ILIKE, metacharacters literal) or regex (~*) scan over every bullet. Returns page › node-id: text lines plus a total count, for exhaustive queries that ranked search cannot answer.
kb.read {page, depth? 0-2} The page as OKF markdown (node ids as <!--nid--> comments) plus labels and backlinks. depth follows [[links]] breadth-first, up to 10 pages.
kb.node.read {node, depth?} One node’s subtree as an OKF fragment. Node ids come from kb.read or kb.grep.
kb.expand {directory?} One directory level at a time, for PageIndex-style traversal.
kb.corpus {maxChars?} A bounded, title-ordered dump of every readable node, bypassing retrieval.
kb.edit {page, okf} Submit full-page OKF. The codec parses it, re-adopts current node ids by position and text, and lands one applyOkf command (a node-level diff; the page is created when missing).
kb.node.edit {node, okf} Replace one node’s subtree with an OKF fragment (exactly one root bullet), scoped through the same id-adoption diff.
kb.create {title, dir?, okf?} Create a page, ensuring its directory by slug.
kb.append / kb.remove Append or remove a bullet. kb.remove is destructive, so it gates.

Beyond the kb surface, agents reach the rest of the ops spine and the comms domains:

  • Tasks: tasks.list {section?}, tasks.create {title, section='up-next'}, tasks.done {id}, tasks.doneLog {since?}, over the task queue.
  • Memory: memory.propose {kind, text} files a proposal, and memory.consolidate {} runs the deterministic dedup pass. See memory.
  • Capture: ingest.capture {url, title, markdown, screenshotFileId?, source?} files extracted content and feeds the knowledge ingestion compiler.
  • Calendar, mail, and CRM: outbound comms tools like mail.send, im.send, and calendar writes act as the run’s user against mail, calendar, and CRM. These gate (see below).
  • Files: agents attach and read files and blobs through the same content-addressed store the rest of the product uses.
  • Heartbeat: ops.heartbeat {} runs the deterministic reminder and calendar-prep scan.

An approval gate pauses a run before an outbound or irreversible action and files a needs-confirm task card, so you decide before anything leaves the system. Gating composes two inputs:

  1. The definition’s policy

    approvalPolicy maps none / outbound / all to never / auto / always. Under auto a tool gates only if its own needsApproval says so; always gates every tool; never gates nothing.

  2. The per-tool trust floor

    A tool marked alwaysGate pauses under every policy. os.exec is the canonical example: os.exec {command, cwd?} runs /bin/sh -c with a 15s timeout and an 8k output cap, always gates, and is disabled entirely unless SUBSPACE_OS_EXEC=1 (or PROVIDERS=fake). It also takes an effect claim so a crash-replayed call refuses the silent re-run.

When a call gates, the pausing transaction writes the approval row, the task card, and a status flip to paused together. Deciding is also one commit: the decision, the card resolution, the flip back to running, and the resume notice land atomically. You can Confirm or Dismiss from the task queue with no special UI, or Approve or Deny inline on the ⚑ node in the run tree. A denied call returns the tool result denied by user; the model sees it and continues, so a denial steers the run rather than killing it.

Credentials are acquired, never ambient. The broker holds the master key, decrypts ops.secrets rows on demand, caches handles by ciphertext (so rotating a secret is a row update, picked up on the next acquire), and enforces per-run grants.

  1. Tools declare what they reach

    A tool’s requires: string[] names the capabilities it needs, for example mail.send, im.send, web.fetch, slack.send, google.oauth, llm.anthropic.

  2. Definitions grant reach

    A definition’s caps narrows what its tools may touch. Absent, the grant set is the union of the allowlisted tools’ requires, so naming caps only ever restricts reach.

  3. The runtime enforces before approval

    A missing grant is denied as the tool result (capability denied: …) before any approval row is written, so an ungranted tool never even reaches the approval prompt.

  4. Every acquire is audited

    ToolContext.caps gives connector-grade tools a grant-checked acquire(). Each acquire logs acquired <cap> into ops.step_logs, visible in the run tree’s step detail.

Recognized environment variables (ANTHROPIC_API_KEY, SLACK_BOT_TOKEN, GOOGLE_CLIENT_ID/SECRET, DEEPGRAM_API_KEY, and more) seed their secret rows once at boot, insert-only and reported by name, so a rotated row is never clobbered by a stale env var. See security and trust for the broker’s role in the trusted/worker plane split, and the environment variables reference for the full list.

human.ask {question, options?, context?, timeout?, default?} hands a decision back to you mid-run. It is ungated (asking is not an outbound action). The tool files a human-question needs-confirm card scoped to the invoking node’s page and suspends on a durable hook at zero cost.

You answer from either surface: the task card renders options as answer buttons (Dismiss answers “dismissed without an answer”), or the run tree’s question node offers the option buttons plus a free-text input and an Answer button. The answer string becomes the tool result. With a timeout, the hook races a durable sleep and expiry returns default (the card auto-resolves as expired).

A live run’s anchor row carries a Steer this run… input. An owner or admin message rides the reusable steer:<runId> hook and appends as a user message between LLM steps, including while an LLM call or an approval wait is active. Steering lets you redirect a run without killing it: correct a misread instruction, add a constraint, or point it at the right page, and the next model step sees your message in the history.