AntonData Vault Docs API Repo
On this page

Architecture

Data Vault.

How Anton asks for credentials, tests them, and saves them — without ever letting the values round-trip through chat history. The flow stitches together a chat tool, a side-panel form, a server-side cowork agent, and a fresh headless instance of Anton acting purely as a connection prober. This page documents how those pieces talk.

Endpoint/v1/datavault/submissions
Storage~/.anton/vault
Form dialectdata-vault-form / -patch
Probe budget90 seconds

Overview

Anton runs locally. When the user asks to connect a service (Postgres, Gmail, PostHog, Salesforce, anything), Anton calls a tool called request_credentials. The tool emits a JSON form spec wrapped in a data-vault-form markdown block. The renderer's markdown extension peels the block out, publishes the spec into a per-conversation store, and the side panel mounts the form.

When the user submits, the panel posts to /v1/datavault/submissions — values are staged in memory (never written to disk), and the response opens an SSE stream. The cowork agent on the server is the boss of that stream: it spins up a separate, headless Anton instance whose only job is to test the connection. The probe runs in the scratchpad, reports back via four small tools, and the cowork agent translates those reports into:

Why a vault?

Three constraints drive every decision in this document:

  1. Credentials never enter chat history. Form values stay on the user's machine, in ~/.anton/vault. Anton's chat history (used as LLM context) only ever sees field names and a submission_id.
  2. Anton's chat instance never sees the probe. The probe runs in a fresh ChatSession with empty history and no persistence — its prompt, its scratchpad cells, and its verdict tools live and die entirely on the server.
  3. Any engine can be probed. There's no per-engine snippet registry. Anton picks the right client library at probe time. PostHog, custom HTTP APIs, weird internal services — all the same path.

Actors

Six pieces collaborate. None of them know about each other directly — they're glued by the form spec, the patch dialect, and the SSE stream.

request_credentials
Cowork tool · server/anton_api/cowork_tools.py

The tool Anton calls in chat to start a connection. Takes a form spec (engine, fields, optional methods). Returns a data-vault-form block to embed verbatim in the assistant message.

DataVaultFormPanel
Renderer · src/renderer/cowork/components/datavault/

Side-panel host. Subscribes to the per-conversation form store; renders the form, the live status toast, the per-field status spinners, the success and failure states. Submits via the streaming endpoint.

datavault_submissions
In-memory · server/anton_api/datavault_submissions.py

Short-TTL submission store. Holds the staged values keyed by submission_id. 24h TTL, never written to disk. Anton's fetch_submission tool reads it.

datavault_agent
Cowork agent · server/anton_api/datavault_agent.py

The boss. Runs as a single SSE stream — stages the values, runs the probe, translates probe events into form patches and chat deltas, persists the synthesized turn, and saves to vault on success.

datavault_probe
Headless agent · server/anton_api/datavault_probe.py

A fresh ChatSession with empty history, no history_store, no session_id. Has the standard scratchpad tool plus six probe-specific tools. Runs once per submission; nothing it does survives the call.

LocalDataVault
Anton core · ~/.anton/vault

The actual credential store. Per-engine, per-name files. Anton uses vault.inject_env(...) to expose values as DS_<ENGINE>_<NAME>__<FIELD> environment variables in the scratchpad.


Architecture diagram

The full lifecycle, from request_credentials to a saved vault entry. The cowork agent is the only component aware of every other piece — by design.

flowchart TB classDef ui fill:#E2F5FD,stroke:#1F9CB0,stroke-width:1.5px,color:#0E0F10; classDef chat fill:#F4F4F0,stroke:#6B6F73,stroke-width:1px,color:#0E0F10; classDef agent fill:#FBF2DF,stroke:#B07A1F,stroke-width:1.5px,color:#0E0F10; classDef probe fill:#FBE7E7,stroke:#B33A3A,stroke-width:1.5px,color:#0E0F10; classDef store fill:#FFFFFF,stroke:#D8D8D4,stroke-width:1px,color:#0E0F10; classDef vault fill:#E5F4EC,stroke:#2E8B57,stroke-width:1.5px,color:#0E0F10; User([User in chat]):::ui subgraph Anton[Conversation Anton] direction TB LLM[LLM
turn]:::chat RC["request_credentials
(cowork tool)"]:::chat end subgraph Renderer[Renderer · React side panel] direction TB MD["MarkdownContent
peels data-vault-form fence"]:::ui Store["formStore
per-conversation pub/sub"]:::ui Panel["DataVaultFormPanel
+ DataVaultForm"]:::ui end subgraph Cowork[Cowork server] direction TB Endpoint["POST /v1/datavault/
submissions (SSE)"]:::agent Subs["datavault_submissions
(24h TTL, in-memory)"]:::store Agent["datavault_agent
process_submission_stream"]:::agent EnvFile[".env temp file
tempfile.mkstemp"]:::store end subgraph Probe[Probe · headless Anton] direction TB Sess["fresh ChatSession
no history · no persistence"]:::probe Pad["scratchpad cell
parses .env, runs test"]:::probe Tools["set_status · set_field_status
request_extra_field
switch_method · remove_field
report_success · report_failure"]:::probe end Vault["LocalDataVault
~/.anton/vault"]:::vault Chat["chat history
(values NEVER stored)"]:::store User -- "asks to connect" --> LLM LLM --> RC RC -- "data-vault-form block
(no values)" --> MD MD -- "setForm(spec)" --> Store Store -- "subscribe" --> Panel Panel -- "user fills, clicks submit" --> Endpoint Endpoint -- "stage values" --> Subs Endpoint --> Agent Agent -- "writes credentials" --> EnvFile Agent -- "spawn + drive" --> Sess Sess -- "scratchpad call" --> Pad Pad -- "reads env file
tries connection" --> Tools Tools -- "set_status / patches" --> Agent Agent -- "data-vault-form-patch
SSE deltas" --> Panel Agent -- "report_success" --> Vault Agent -- "verdict line" --> Chat Agent -- "delete temp file" --> EnvFile linkStyle default stroke:#6B6F73,stroke-width:1px;
Read the diagram top down. The conversation Anton issues a tool call; the form lands in the renderer; the user submits; the cowork agent runs the probe via a fresh Anton; patches stream back into the panel; on success, the vault gets the only persistent write that ever happens.

End-to-end flow

Eight phases. Most are sub-second; the probe phase is the only one that takes user-visible time, capped at 90 seconds.

1. Anton calls the tool

In a normal chat turn, Anton sees the user wants to connect to a service and calls request_credentials with a JSON spec. The tool returns an instruction to emit the spec verbatim wrapped in a data-vault-form fence.

2. Markdown extension lifts the spec to the side panel

The renderer's markdown extension recognises data-vault-form as a custom language. MarkdownCode.jsx parses the JSON tolerantly (smart quotes, trailing commas, JS comments), publishes the spec into formStore keyed by conversation id, and renders nothing inside the chat bubble — a one-time pointer card links the user to the side panel.

3. The form panel renders

DataVaultFormPanel subscribes to formStore for the current conversation. It mounts inside a RailCard (no chevron) with a × close affordance. A subtle slide-up + fade animation runs on first appearance, never on subsequent patches.

4. User submits

The panel POSTs to /v1/datavault/submissions with form_id, the values, the list of skipped field names, and the form spec (spread with auth_method / selected_method when the user chose a method). The response is SSE.

5. Cowork agent runs

datavault_agent.process_submission_stream opens a single SSE turn. Stages the values in datavault_submissions (24h TTL, in-memory only). Validates shape if the engine is registered (skipped for custom engines). Writes credentials to a temp .env file at tempfile.mkstemp(prefix='anton-vault-', suffix='.env').

6. Probe spins up

datavault_probe.run_probe instantiates a fresh ChatSession with the conversation's llm_client + workspace but no history_store, no session_id, and an empty initial history. Anton's standard scratchpad tool is auto-registered; six probe-specific tools are added on top. The system prompt is a tight technical brief: where the .env is, what variable names are inside, the field roster (with filled / empty / skipped state per field), and instructions to call exactly one verdict tool before stopping.

7. Live updates flow back

Each scratchpad cell, each set_status / set_field_status call, each piece of prose anton emits — the cowork agent translates them into the client's existing SSE vocabulary (response.in_progress for scratchpad, data-vault-form-patch blocks for status, plain text deltas for prose). The form panel's status toast updates live; per-field spinners appear and disappear; the chat bubble fills incrementally.

8. Verdict lands

Anton calls report_success, report_failure, or request_extra_field. The cowork agent reacts:

In every branch, the temp .env file is unlinked in finally. The synthesized turn (intro line + status patches + scratchpad events + verdict text) is persisted to the conversation's _history.json + per-turn events sidecar so reload reproduces exactly what the user saw.


Form spec

The shape Anton emits via request_credentials. Every field is optional except engine and title — the absence of fields is meaningful (it implies multi-method via the methods array).

{
  "form_id": "fm_a3f9c2b41e",        // stable id; reuse across patches
  "engine": "posthog",             // any string — registry-known or custom
  "logo": "key",                   // icon name (NOT a URL)
  "logo_color": "var(--accent)",
  "title": "Connect to PostHog",
  "subtitle": "Read-only access via personal API key.",
  "form_warning": null,             // optional amber banner
  "form_error": null,               // optional red banner
  "status_text": null,              // live status — set by patches
  "_is_probing": false,             // flips while the probe runs
  "fields": [
    {
      "name": "api_key",            // env-var-like; becomes DS_API_KEY
      "label": "Personal API key",
      "type": "password",           // text|password|url|select|textarea|boolean
      "required": true,
      "placeholder": "phx_…",
      "help": "From posthog.com/settings",
      "skipable": false             // every field is skipable by default
    }
  ],
  "actions": [                    // optional; default is Submit + Cancel
    { "id": "submit", "label": "Connect", "kind": "primary" }
  ]
}

Field types

Per-field state

Three slots can be patched independently — set on a retry to call out per-field issues:


Multi-method forms

When a service supports several auth options, the form spec uses methods[] instead of (or alongside) fields[]. The user picks a method first; the form switches to that method's field list. Submission carries an auth_method tag so the probe knows which path to test.

{
  "form_id": "fm_gmail_1",
  "engine": "gmail",
  "title": "Connect Gmail",
  "selected_method": "app_password",   // optional pre-pick
  "methods": [
    {
      "id": "app_password",
      "label": "App Password",
      "description": "Easiest if you use Gmail with 2FA. 16-char key.",
      "recommended": true,
      "help_url": "https://support.google.com/accounts/answer/185833",
      "fields": [
        { "name": "email", "label": "Email", "type": "text", "required": true },
        { "name": "app_password", "label": "App Password", "type": "password", "required": true }
      ]
    },
    {
      "id": "service_account",
      "label": "Service Account",
      "description": "Workspace only. Domain-wide delegation.",
      "fields": [
        { "name": "service_account_json", "type": "textarea", "label": "JSON key" }
      ]
    }
  ]
}

UX


Patch dialect

Once a form is on screen, every subsequent update flows as a data-vault-form-patch block. Patches are never rendered in the chat bubble — the markdown extension routes them straight to the form store, and the store merges them into the existing form by id.

Three nesting levels, three independent semantics for null:

{
  "form_id": "fm_a3f9c2b41e",
  // 1) Top-level: overwrite. null clears the key.
  "subtitle": "Probe failed — fix the highlighted issue.",
  "status_text": null,             // clears the toast
  "_is_success": true,

  // 2) Inside fields[]: name-keyed; object merges, null DELETES the field.
  "fields": {
    "api_key": {                  // merges into existing field
      "error": "Token rejected",
      "status": null               // clears just the status property
    },
    "region": null                  // removes the whole field
  },

  // 3) Inside methods[]: id-keyed; object merges, null DELETES the method.
  "methods": {
    "app_password": {
      "fields": {                // nested method.fields uses the same rules
        "app_password": { "error": "Wrong format — should start with phx_" }
      }
    }
  }
}
Why three nesting levels matter. They give us three precise affordances: remove a property, remove a field, remove a method — all without re-emitting the whole spec. Re-emission would echo every field's value, which would put credentials in chat history. Patches are append-only — values stay in the renderer's local state.

Headless probe

The probe is a second Anton, instantiated specifically to test one credential set. It uses the conversation's LLM client + workspace, but starts with empty history and never persists. Its ChatSession dies when the probe ends; the only side effect that survives the call is the verdict.

Why a second Anton?

The .env handoff

The cowork agent writes the user's submitted credentials to a tempfile in standard .env format:

DS_EMAIL="alice@example.com"
DS_APP_PASSWORD="abcd efgh ijkl mnop"

The path is the only credential channel that reaches the probe. The probe parses the file inside the scratchpad (typically with dotenv_values) and uses the values to call the engine's client. The probe is explicitly told never to print credential values — they would otherwise leak into the right-rail scratchpad output. The cowork agent unlinks the tempfile in finally regardless of outcome.

The prompt

Built dynamically per probe. Includes the engine, the env path, the variable names, and a roster of the form's fields with filled / empty / skipped state. For multi-method forms it lists every method, marks the selected one, and indents each method's fields separately so anton can choose to add fields to the right method.

Probe tools

Six tools, plus the standard scratchpad tool that ChatSession auto-registers. All accept a method_id parameter when relevant — multi-method forms scope edits to a specific method's field list.

set_status(text) Form-wide live status (the bar at the top of the panel). Use for overall phase: "Loading credentials", "Probing posthog". Renders as a dismissible toast.
set_field_status(name, status, method_id?) Per-field status line under one input. status=null clears. Used for granular feedback ("Validating…" under api_key).
remove_field(name, method_id?) Delete a field from the form (or from a specific method). For when a field is obsolete (e.g. user picked OAuth so password no longer applies).
switch_method(method_id, reason) Multi-method only. Flips the form's selected_method. The reason renders as a status toast so the user sees why anton suggests the swap.
request_extra_field(fields, reason, method_id?) Verdict — needs more from the user. Form re-opens with new fields appended (scoped to a method when given). User re-submits with the merged credentials.
report_success(summary) Verdict — connection works. Triggers vault save. Summary becomes the chat one-liner.
report_failure(error, follow_up) Verdict — connection broken. Vault stays clean. error is the user-friendly diagnosis; follow_up is a one-line hint about what to fix.

Verdicts

Every probe ends with exactly one verdict. If anton stops without firing one (rare; usually a timeout), the runner forces a failure with "Probe ended without a verdict."

success save
Cowork agent saves credentials to ~/.anton/vault via LocalDataVault. Form flips to its celebratory state with two actions: Close + View connectors →.
failure no save
Vault stays clean. Form gets form_error banner + restored input grid. User edits and resubmits.
needs_input extend form
Form gets the new fields appended (scoped to the active method when multi-method). Subtitle becomes the reason. User resubmits with merged values; probe runs again.
timeout 90s
Treated as failure. error is "Probe timed out after 90s." follow_up nudges the user to retry or check service reachability.

Form panel UI

The right-rail panel uses a RailCard with noChevron — the only dismissal control is the × at the top right. Below the card header, four UI primitives compose every state the form can be in:

Header

Logo (icon name from the app palette — never a URL) + title + subtitle. Always rendered.

Status toast

Sits between the header and the form body. Shows the current status_text with a small accent-colored spinner and a × to dismiss. Dismissal only suppresses the exact text the user closed — a new status update auto-resurfaces the toast. This keeps the form layout from jumping while the probe streams progress.

Method picker / breadcrumb

Multi-method only. Picker = vertical card stack; breadcrumb = single line above fields with a "change" link. Per-(form, method) local state means the user can flip between methods without losing typed values.

Fields + actions

Every field has a label, an input, and three optional message slots (error / warning / status). Per-field skip toggle (skip / unskip) on the right of every label. Actions row at the bottom — primary button is the only filled style. While probing, inputs are disabled (busy state) but still visible — no layout jumps.

Success state

Triggered by _is_success: true. Replaces fields + actions with a green check, the title, the subtitle (anton's success summary), and two default actions:

Animation

A subtle fade + slide-up + scale (320ms) on first appearance — re-fires only when a new form_id arrives. Patches into the same form_id don't re-trigger it. The spinner uses a 720ms linear rotation. Both keyframes inject into document.head once at module load.


Endpoints

Submission stream

POST /v1/datavault/submissions
Content-Type: application/json

{
  "form_id": "fm_a3f9c2b41e",
  "conversation_id": "task-2025-Q3",
  "values": { "api_key": "phx_…" },
  "skipped": [],
  "form_spec": { /* same shape Anton emitted, with auth_method spread in if multi-method */ }
}

Returns a Server-Sent Events stream — same event vocabulary as /v1/responses (so the renderer's existing SSE adapter consumes it natively). The stream emits the cowork agent's text deltas, the probe's translated scratchpad events, and the data-vault-form-patch blocks that drive the panel.

Submission peek (debug)

GET /v1/datavault/submissions/{submission_id}

Returns metadata + field names for a staged submission. Values are intentionally redacted from this endpoint — only the agent-side fetch_submission tool reads them, via a different code path that doesn't go through HTTP.

Datasource CRUD

Direct vault access (used by the Connect Apps and Data page, not the chat workflow):

SSE events

The submission stream uses the same five top-level events as /v1/responses:

response.created
Opens the turn. Carries the synthesized response id.
response.in_progress
Probe scratchpad lifecycle (thought_role: scratchpad.start/end/result) — feeds the right rail.
response.output_text.delta
Cowork agent prose + the probe's prose + the data-vault-form-patch blocks.
response.completed
Terminal. response.status reads success, retry, needs_input, or failed.
response.failed
Terminal error frame. Never reached on a normal probe — only fires if the runner itself crashes.

Security model

Storage

Chat history

Probe isolation

Trust boundaries


File map

Where each piece of the puzzle lives. Read in this order to follow the flow.

cowork_tools.py request_credentials tool + fetch_submission + update_form. The chat-side surface; what the LLM sees.
datavault.py (route) POST /v1/datavault/submissions + GET /v1/datavault/submissions/{id}. Wraps process_submission_stream in a StreamingResponse.
datavault_submissions.py In-memory staging store. stage_submission, get_submission, consume_submission, lazy TTL purge.
datavault_agent.py The cowork agent. process_submission_stream orchestrates the probe and translates events into the SSE vocabulary the client speaks.
datavault_probe.py run_probe + tool definitions. Builds a fresh ChatSession, runs turn_stream, drives the probe-tools event loop, manages the temp .env file.
formStore.js Per-conversation pub/sub. setForm, patchForm (with the three-level merge semantics), clearForm, subscribe.
DataVaultForm.jsx The form renderer. Picker UI, breadcrumb, fields, actions, success state. Per-(form, method) local state.
DataVaultFormPanel.jsx Side-panel host. Status toast, panel × close, RailCard wrapper, appearance animation, navigate-to-connectors handler.
MarkdownCode.jsx Lifts data-vault-form + -patch blocks out of chat into the form store. Renders nothing for patches; one-time pointer card for full forms.
parseFormSpec.js Tolerant JSON parser. Handles smart quotes, trailing commas, JS comments, single vs double quotes — the LLM gets it wrong sometimes.