Operator API
Web search provider configuration, instance credentials, and agent overrides are documented in Web capabilities (repository). Everything the dashboard can do, a program can do. The dashboard is a client of/api; this document is the contract that client and every other one rely
on, and the runbook for the two callers that motivated it: a migration moving
an agent in from another harness, and a control plane provisioning managed
agents.
Authentication
Two credentials open the same doors. A session cookie is what the dashboard holds afterPOST /api/login. An API token is what a program
holds:
POST /api/settings/api-tokens. The secret is returned exactly once, in the
create response, and never again; Mesh stores only its SHA-256.
A token acts as the administrator who created it. Every attributed write
(who enabled a tool, who saved a persona revision, who created a memory)
records that person, exactly as a personal access token does on GitHub.
Deleting the user deletes their tokens.
Scopes. admin is the dashboard’s full authority. read may only GET
and HEAD; a write under a read token is a 403 insufficient_scope. The bootstrapped management scope permits instance model settings, external-user
provisioning, agent operations, and model-provider catalogs. It cannot mint tokens
or access unrelated install settings. Its writes are attributed to a service
identity rather than a human operator.
CSRF. Mutating requests with a cookie must carry a same-host Origin or
none. Bearer requests are exempt: a header the caller set is not a credential a
browser attached behind their back.
Tell them apart. GET /api/me returns {"email","via":"session"|"token", "token_name","scope"}, so a script can confirm which credential it is holding
before it does anything.
Managing tokens
Revocation is immediate: the next request with that secret is a
401.
Conventions
- JSON in, JSON out. Unknown fields in a request body are a
400 invalid_json, so a typo cannot silently become a no-op. - Errors are
{"error": {"code", "message"}}. Codes are stable identifiers (slug_taken,agent_not_found,insufficient_scope); messages are for humans and may change. - Identifiers are UUIDs; agents are addressed by
slugin paths. - Every route below is behind the gate above.
GET /api/setup/status,POST /api/setup/admin,POST /api/login, and the external-auth routes are the only public ones. - There is no
/api/v1yet. The surface is stable in the sense that the dashboard depends on it; a versioned prefix arrives with the read-contract redesign the roadmap tracks as G2.
Schedules
Persistent reminders and read-only briefings are created through the agent’screate_schedule tool in the delivery conversation. The operator surface manages
existing schedules; it cannot invent a requester or delivery audience. Agent
access is required for each route, and writes record the operator’s user ID.
Actions are
pause, resume, cancel, run_now, and update. Supply the
schedule’s current version; stale or unavailable records return 409.
run_now requires a stable UUID request_id reused on retries. update requires
request: {instruction, cadence}. A cadence has kind and an IANA timezone,
plus time (HH:MM) for daily/weekly, weekdays (0–6) for weekly,
every_minutes for interval, or at (RFC3339) for once.
Reads are no-store. Invalid requests return 400; unavailable scheduling
returns 503. Pause/cancel remain available while the run worker is off.
See schedule operations and timing semantics (repository) for bounds,
DST handling, missed firings, rollout requirements, and the watchdog script.
Provisioning an agent, end to end
This is the sequence the dashboard’s wizard performs, as API calls. Each step is idempotent enough to re-run: creates return409 on an existing slug, and
every other call is an upsert.
runtime/tools.md
§Authority is the agent’s account: the key and the tokens you paste are the
agent’s own accounts, provisioned for it like a new coworker’s. Nothing in this
API takes a human’s credential and lets the agent borrow it.
Bulk memory import
POST /api/agents/{slug}/memory/import lands many memories in one transaction
and records where each came from.
audienceis required and names who may see these memories at runtime. Since the audience model landed, a memory with no conversation source starts withunknownevidence and is not eligible for any turn until an operator releases it; an import that stored hundreds of memories the agent could never recall would look like success and be a silent failure. Every imported revision is released to this audience in the same transaction, by the importing user, exactly as a console release is.{"kind":"public"}means every conversation this agent joins;{"kind":"workspace","namespace":"…"}and{"kind":"members",…}narrow it. There is no default, on purpose.kindandbodyare validated exactly as the dashboard’s create and the agent’s ownrememberare (lowercase kind, non-empty bounded body).visibilitymay be omitted orperspective.conversationis refused: it restricts a memory to the conversation it was learned in, and an import has no conversation.sourceis optional.systemis required inside it, lowercased, up to 64 characters;ref(up to 512) is the source’s own identifier;created_atis when the source first recorded the memory.memory_entries.created_atstays the moment Mesh learned it.- At most 500 entries per call (the body limit is sized for 500 full-size bodies); split larger migrations.
- All or nothing, and serialized per agent. A bad entry refuses the whole
batch with
400 invalid_entrynaming its index. A storage failure rolls back with500. Two overlapping imports for one agent run one after the other, so a retry cannot double what the first landed.
skip_duplicates on (the default), an entry is skipped
when its (system, ref) was ever imported for this agent
(skipped_source_seen) — including into a memory an operator has since
deleted, because that deletion was a decision and a re-run must not reverse it
— or when a live memory with the same kind and body exists
(skipped_duplicate). Provenance wins over body: a source row re-sent with an
edited body is still the same source memory. Re-running an import is therefore
a no-op. Set skip_duplicates: false to import everything anyway.
Dry run. Send the same body with "dry_run": true and the call reports
what it would do without doing it: identical validation, the same duplicate
check against a consistent snapshot, the same per-entry verdicts with
would_import in place of imported, no ids, no batch_id, and
"dry_run": true at the top level so a plan cannot be mistaken for a result.
Nothing is written and no lock is taken. A migration driven by an agent shows
the operator the dry run, then sends the identical body without the flag.
Imported memory carries source_type: "import" and its provenance is visible
in the export below and in the memory list. batch_id groups everything one
call created.
Whole-agent export
GET /api/agents/{slug}/export returns one document:
413 memory_too_large when the agent holds more than
10,000 live memories; page them through GET /api/agents/{slug}/memory instead.
The same ceiling applies to an import’s duplicate check (skip_duplicates: false bypasses it). No real agent is near it; it exists so one request cannot
materialize an unbounded table.
No credential appears in an export, redacted or otherwise: not the model
key, not a connector token, not a tool secret. An export is a document that
gets copied around; credentials live in the encrypted secrets table and are
reached only through the endpoints that own them. Re-provisioning from an
export always ends with capturing credentials again, on purpose. format
changes when a field changes meaning, not when one is added.
Migrating an agent from another harness
The shape is the same whatever the source: read the source’s files, map them onto the calls above, keep the source’s identifiers assource.ref so the
migration can be re-run.
Two things do not move. Credentials: the source held the operator’s
logins; Mesh wants the agent’s own, so provision accounts for the agent and
paste those. Conversation history: Mesh’s graph is what its own connectors
observed; there is no import for another system’s transcripts, by design.
Verify with
GET /api/agents/{slug}/export and compare counts against the source.
The migration is judgement work, not a converter. Every source instance
grew a different set of files, so Mesh ships no conversion tool. It ships a
skill instead: skills/migrate-agent-to-mesh/SKILL.md (repository)
tells an agent holding an API token how to survey an OpenClaw or Hermes
workspace, decide the mapping out loud, dry-run each batch, land it, and verify
against the export. Hand the agent the token, the skill, and the answer to
“who may see these memories”, and let it do the reading.
Re-running the sequence is safe from the persona step on: persona, model,
connector and tool writes replace what is there, and the import skips what it
has already landed. POST /api/agents is the exception: a slug that exists
answers 409 slug_taken, so a rerun should GET /api/agents/{slug} first and
create only on 404.
Roles
Two, on the user (0050_operator_roles.sql):
The role is read from the user’s row on every request, never from the
cookie or the token, so a change an owner makes lands on the operator’s next
call. An API token acts as its creator with its creator’s live role.
How refusals read:
403 owner_requiredon an install-level route from an agent_operator. They are signed in; the route is not theirs. Clients must not treat this as a sign-out.404 agent_not_foundon/api/agents/{slug}/*for an agent the caller holds no grant for — the same answer an unknown slug gets, on purpose, so the set of agents an operator was not given is not enumerable from status codes.GET /api/agentslists only granted agents for an agent_operator.- API tokens: an agent_operator lists and revokes only their own; a token
they did not create is
404 token_not_foundto them. Owners see all.
0050_operator_roles.sql ran, because every one of them was one. Control-plane operators (external
auth mode) arrive as owners until the control plane sends a role.
Sessions and sign-in hygiene
A cookie session (0006, 0050) ends at the first of two clocks: seven
days after sign-in, or 24 hours without a request. Each request advances
the idle clock (stamped at most once a minute). External auth mode keeps its
15-minute sessions. API tokens have neither clock; revoke them.
POST /api/login, GET /api/invites/{token}
and POST /api/invites/{token}/accept answer 429 too_many_attempts with a
Retry-After header, before any bcrypt work. A successful sign-in clears the
account’s window. The lock is the window and nothing more: no permanent lock,
no unlock endpoint, nothing that survives a restart. The source is the
connection’s address, or — when the connection comes from a proxy named in
MESH_TRUSTED_PROXIES — the nearest untrusted hop of X-Forwarded-For.
Behind a proxy that is not named the source window is shared by everyone
(thirty failures from anyone lock sign-in for the window), which is the
reason to name it; the account window holds either way.
The off switch
Two levels, both rows every replica reads (0058; internal/admission), so a
stop pulled anywhere holds everywhere and survives a restart. The agent’s own
switch is POST /api/agents/{slug}/disable (below): nothing is admitted for it,
and anything it is doing is cancelled within seconds — the governor polls the
gate beside every in-flight turn and cancels the blocked call. The install’s
switch is here. A cancelled turn ends with ❌ and no message; the run row
(cancelled) and the activity trail say why. Deliveries that arrive while
stopped are still captured into the graph and their work is cancelled at
execution (install_stopped), so nothing is lost from history but the reply.
stop from any participant cancels the run it is
addressed to (see docs/runtime/live-steering.md); switching an agent off for
good stays an operator action.
About this install and Security
Two read-only owner endpoints back the Settings panes of the same names: the facts a support conversation starts with, and the sign-in policy the install runs under. Nothing secret appears in either — the master key by fingerprint only, the database by its migration level, never its URL.Activity
The audit trail (0054): one row per state-changing request through this
API — anything but GET/HEAD, whether from the dashboard or a token, with
one exception: POST /api/client-errors, the browser’s own error reports,
which are telemetry rather than an action — written by the session gate after
the handler answers, so every route is covered by construction. A request the
gate itself refuses after resolving who is asking (a read-only token’s write,
an agent operator at an owner route) is a row too, as a refusal. Plus every sign-in attempt that reached a password
check (auth.sign_in, auth.setup_admin, auth.accept_invite), by the
auth handlers themselves. Reads are not rows. Throttled attempts (429) are
not rows either: they are refused before any bcrypt or database work, and a
write per attempt would hand a guessing burst the cost the throttle denies.
Recording never changes an answer — a failed insert is logged and the
request’s real status stands.
actor.via is session or token (with token_name) for route rows, and
the sign-in method — password, external, invite — for auth rows. An
anonymous attempt (wrong email, bad invite link) has an actor with a null id
and the attempted email in details. Nothing in a row is a secret: request
bodies are not stored, and nothing is decrypted to write one. There is no
delete or edit route; retention is the operator’s call for now.
Operators
The people who sign in. Each has a role (above); an agent_operator also has a list of agents. The surface is the way in, the way out, and what each person is. Local auth mode only: in external mode the control plane owns the roster and provisions each operator on first launch, and the invite, disable and enable writes answer409 auth_mode.
role is required for a new operator (400 role_required) and, for
an existing one, may only restate their current role (409 operator_exists
otherwise — change it with PATCH); grants are for agent_operators
(400 agents_for_owner); every slug must exist (400 unknown_agent); you
cannot change your own role (400 cannot_change_own_role); you cannot disable
yourself (400 cannot_disable_self); the last enabled owner cannot be
demoted or disabled (409 last_owner, checked under a roster-wide advisory
lock so two owners demoting or disabling each other at once cannot both
succeed); a disabled operator cannot be sent a link until enabled
(409 operator_disabled); an operator provisioned by the control plane cannot
be sent one at all (409 operator_external), since accepting a link would give
a control-plane account a local password. Last sign-in is a column on the user
stamped when a session starts, not derived from session rows, so a re-enabled
operator is active, not invited. A disabled
operator who signs in with the right password is told 403 account_disabled;
with the wrong one they get the same 401 as anyone, so the account’s state
is not an enumeration oracle.
The link’s holder has no session, so the two routes they use are public:
GET /api/invites/{token} answers {"email","expires_at"} or
404 invite_invalid (unknown, expired, used, and disabled-user links are
indistinguishable, on purpose), and POST /api/invites/{token}/accept with
{"password"} sets the password, ends every older session for that user,
consumes the link, and starts a session. Links work once and expire after
seven days.
The rest of the surface
Every dashboard route, for completeness. Everything under/api/agents/{slug}
is agent-scoped (owners and granted operators); everything under
/api/settings except API tokens, plus POST /api/agents, the commitments
health read and the install model catalog, is owner-only; the rest takes any
signed-in operator.
Agents. GET /api/agents · POST /api/agents · GET|PATCH /api/agents/{slug} ·
POST /api/agents/{slug}/enable|disable · GET /api/agents/{slug}/export
Persona. GET|POST /api/agents/{slug}/persona · POST /api/agents/{slug}/persona/activate
Model. POST /api/agents/{slug}/model-provider · POST /api/agents/{slug}/model-provider/activate ·
GET /api/agents/{slug}/model-providers/{provider}/models · GET /api/model-providers/{provider}/models
Connectors. GET /api/connector-catalog (what each connector does and how to set it up) ·
GET /api/agents/{slug}/connectors/slack/manifest · POST /api/agents/{slug}/connectors/slack|telegram ·
GET /api/agents/{slug}/connectors/{connectorID} · GET …/connectors/{connectorID}/messages/metrics
Phone numbers. POST /api/agents/{slug}/connectors/phone/numbers (verify own-account credentials, list numbers) ·
POST /api/agents/{slug}/connectors/phone (connect with the agent’s own account) ·
GET /api/connectors/phone/shared-account · GET /api/agents/{slug}/connectors/phone/shared/numbers|available ·
POST /api/agents/{slug}/connectors/phone/shared (connect or buy on the instance account) ·
PUT /api/agents/{slug}/connectors/{connectorID}/phone-settings (who may text it) ·
owner: GET|PUT|DELETE /api/settings/phone (the instance phone account)
Memory. GET|POST /api/agents/{slug}/memory · POST /api/agents/{slug}/memory/import ·
PATCH|DELETE /api/agents/{slug}/memory/{memoryID} · GET …/memory/{memoryID}/revisions ·
GET|POST …/memory/{memoryID}/grants · DELETE …/memory/{memoryID}/grants/{grantID}
Dreaming. All routes below require the same authenticated operator session
or API token as memory management. Read-only tokens may inspect state; writes
require write permission. See Dreaming for privacy and
cadence semantics. Scheduled dreaming starts disabled.
Paths are prefixed with
/api/agents/{slug}. Default configuration:
GET /api/agents/{slug}/tools · POST /api/agents/{slug}/tools/{tool}/enable|disable ·
GET|POST /api/agents/{slug}/tool-secrets/{kind} · PUT /api/agents/{slug}/tool-config/{package}
Integration catalog (read). GET /api/agents/{slug}/integrations returns
agent_id, integrations, connections, and operations for the caller’s
permitted agent. It projects enabled native tools whose required credentials are
present, approved MCP bindings, and available web capabilities. Disabled or
credential-blocked capabilities are omitted. Responses are no-store and contain
setup field descriptors, never captured values or decrypted credentials.
Operations carry a stable catalog id, their unchanged model/ledger alias,
integration_id, capability_id, connection_id, adapter_id, execution kind,
effect class, replay policy, resource needs, and definition revision. Native and
web entries include their existing input schemas. MCP entries preserve reviewed
fingerprints but set discovery_required: true and omit the schema: reading this
endpoint performs no network discovery or OAuth refresh. A catalog entry is
metadata, not an execution grant; existing call-time validation still applies.
Native operation revisions also include the manifest’s package_version, when
available, so declared package upgrades invalidate the revision even if the tool
schema is unchanged. Unversioned definitions only fingerprint their metadata.
Legacy connection references include the agent identity. Their version is the
source connection’s configuration version. Web connections also include
binding_version for the agent’s own settings: with inheritance, instance key or
configuration changes advance version, while agent settings changes advance
binding_version. Neither is a complete authorization revision or a promise that
credentials remain valid. This endpoint adds no connection
writes, database migration, UI flow, or changes to model tool offerings.
Inspection (read). GET /api/agents/{slug}/conversations · GET …/conversations/{conversationID} ·
GET …/conversations/{conversationID}/messages/{messageID}/trace · GET /api/agents/{slug}/actors ·
GET …/actors/{actorID} · GET /api/agents/{slug}/prompt-snapshots/{snapshotID}
Actor list/detail responses include nullable preferred_name, the participant’s
explicit choice. display_name uses that choice once saved and otherwise follows
connector name resolution. The actor can change it conversationally; a Slack
profile refresh cannot replace it. Broader actor profiles use attributed memory
with its normal audience permissions. See actor profiles (repository).
Conversation list/detail responses include display_name, a connector-resolved
channel name or DM counterpart name (empty when unavailable). The dashboard
prefers it over stored title, then channel_external_id/conversation_key.
The name is presentation metadata: channel filters and routing continue using
external IDs. Historical conversations resolve on read through the same path;
lookups are cached and best-effort. See Slack conversation names
for existing-install permission requirements.
Install settings. GET|PATCH /api/settings · GET|PUT /api/settings/memory ·
POST /api/settings/memory/test|reindex · POST /api/settings/error-tracking/test ·
POST /api/settings/agent-runtimes/{backend}/test · GET|POST /api/settings/api-tokens ·
DELETE /api/settings/api-tokens/{id}
Activity (owner). GET /api/settings/activity
Operators (owner). GET /api/settings/operators · POST /api/settings/operators/invites ·
PATCH /api/settings/operators/{id} · POST /api/settings/operators/{id}/invites|disable|enable ·
public: GET /api/invites/{token} · POST /api/invites/{token}/accept
Session. GET /api/me · GET /api/me/sessions · DELETE /api/me/sessions/{id} ·
POST /api/me/sessions/revoke-others · POST /api/settings/operators/{id}/sessions/revoke (owner) ·
POST /api/login · POST /api/logout · POST /api/client-errors
Install (owner). POST /api/settings/stop · DELETE /api/settings/stop ·
GET /api/settings/install · POST /api/settings/install/verify-secrets ·
GET /api/settings/security · POST /api/settings/security/sign-everyone-out
Durable commitments
Commitments are opt-in per agent. New and upgraded agents start disabled. These endpoints accept dashboard sessions or Mesh API tokens; read tokens cannot change settings or commitments. All responses useCache-Control: no-store.
Action requests require the current
version, an action, and a nonempty
reason of at most 2,000 bytes. Example:
Actions return the updated record. Stale revisions, terminal records, active
attempts that cannot be retried, or exhausted budgets return 409
commitment_changed. Unknown external writes block retry/authorization with the
distinct 409 commitment_ambiguous_effect. Invalid requests return 400
invalid_commitment; unavailable storage returns 503 commitments_unavailable;
enabling without a run worker returns 503 commitment_worker_disabled. Internal
failures use a redacted 500 commitment_failed response. An acknowledgement is
human evidence, never an agent’s self-assessment. The original run, requester,
conversation, audience, objective, and evidence contract remain immutable.
Existing evidence contracts support {"kind":"operator_ack"} and
tool_result with an exact enabled query tool, validated JSON-object arguments,
an RFC 6901 pointer, and a JSON equals value. For JSON encoded inside an MCP
text envelope, use json_source_pointer to select the string to decode first:
open, in_progress,
waiting, stalled, escalated, completed, closed_incomplete, failed, or cancelled.
The health endpoint uses database time and fails when an enabled reconciler has
no heartbeat within two minutes or a work/notice dispatch is overdue by five
minutes. Configure an independent alert using scripts/check-commitments.sh;
see the runtime runbook (repository).
Incoming accepted work uses a criteria contract. Its required outcomes may use
exact recorded query predicates, independent semantic verification of an actual
reply/artifact, a richer verifier for complex artifacts, or specifically requested
human attestation. An empty rubric is unconfigured, never satisfied. Every
criterion and its evidence revision must be current before the task completes.
waiting retains responsibility with a typed cause and a resolution condition;
questions, delivery state, reminder timing, and remaining allowance appear in
completion. Generic assent to start work does not attest completion.
Work sources
These routes use the same agent-scoped session/token authorization. Assignment plans use approved read-only MCP tools and an explicit operator assignment policy. They verify the agent’s own account, prove pagination coverage, and preserve canonical resource identities across connection changes. A connection alone does not authorize accepting assignments. Observation plans can also be derived from an exact query criterion already authorized by a task.
See the implementation and rollout runbook (repository)
for configuration, retention, and operational boundaries.
Agent progress updates
Progress updates run automatically for every active agent, including existing agents with no settings record. There is no enablement switch or configuration step. The working model is reminded to report meaningful progress every three minutes. The supervisor checks queued checkpoints every five seconds and enforces 60 seconds between delivery attempts. Empty ticks do not create progress events. Progress is delivered in the original conversation. It is never a final reply or evidence of task completion. The per-message/trace response includes
run.progress, with text, source snapshot/provenance, status, model usage,
timestamps, and connector receipt. Statuses are candidate, reserved, sent,
unknown, and suppressed. A checkpoint keeps the same event ID through delivery;
it is not also recorded as a suppressed/coalesced copy. Distinct updates queue in
creation order; exact copies of the last delivered text are marked duplicate.
reserved means delivery is pending or unconfirmed; the connector may already
have accepted it. On execution takeover, unfinished reservations become unknown.
Neither state is replayed. JSON-shaped drafts are rewritten by the fast model
before sending, and the raw draft remains in the operator-only source snapshot.
The trace distinguishes disclosure refusals by source category from operational
check failures and rewrite failures. These records are operator-only; their
content may be sensitive.
A search_history tool call in the /trace response carries search_audit: the
conversation search it recorded (docs/runtime/conversation-search.md). It has
query_digest (the query is never stored as text; this is a keyed HMAC under a
subkey of MESH_AGENT_MASTER_KEY, so it cannot be reversed by hashing guessed
words, and it is empty when no master key is configured; the call’s arguments
carry the text while bodies are retained), filters, the destination audience the search was
authorized against, selections (each returned message id, its conversation, its
score components, and the neighbor ids shown with it), and exclusions — how
many text matches each rule removed: already_in_context, no_evidence,
audience, lineage, and below_cutoff, beside matched (counted up to 1,000,
with capped; past the ceiling below_cutoff is a lower bound, while lineage
stays exact) and returned. It answers “why did the agent not find that?”
without re-running the search. Counts only, never withheld text; the model never
sees them. Other tool calls omit the key.
Instance model defaults
Owners configure defaults in Settings → Models (/settings/models).
Both instance-default routes require an owner; agent-scoped inheritance requires
access to that agent. Management scope refusals are included in the activity log.
GET /api/settings/models returns the default provider and primary/fast/heartbeat
model IDs, a providers map of key presence, and optional management provenance.
It never returns plaintext credentials. PUT /api/settings/models replaces the
non-secret defaults and optionally sets or removes the selected provider’s key:
api_key to retain a key; use clear_key: true to remove it. Providers are
openrouter and router (Ramp Router). Keys are encrypted with the existing
MESH_AGENT_MASTER_KEY. Successful writes return status: active or
saved_restart_required; the latter means persistence succeeded but the runtime
could not reload. GET exposes no secret material, including to read-only tokens.
New agents inherit the instance credential. Existing agents with a provider choice
or stored credential retain their original payer. Saving/activating an agent key
sets model_credential_source: agent; a missing or unreadable agent key never falls
through to the instance key. To opt back into inheritance:
{"status": "active" | "saved_restart_required", "provider": "<effective provider>"}: provider names what this write committed (the pinned
provider, else the instance default at the moment of the write), so a client
never has to trust an earlier read of the instance defaults. Agent model-role
choices remain explicit overrides. Clear those fields to
inherit matching-provider instance models. The process-level OpenRouter key remains
a legacy fallback only before any instance-default document has been saved.
Hosted management may supply managed_by: control_plane plus
managed_account_id with a key. Managed keys occupy separate encrypted records,
so switching back to BYOK preserves the user’s original key. A disconnect should
supply expected_managed_account_id and clear_key: true with no management
provenance; a mismatched current owner returns status: not_managed without mutation.
These provenance fields describe ownership; they are not authorization credentials.
Bootstrapped management identity
A hosting operator can setMESH_MANAGEMENT_TOKEN_SHA256 to the 64-character hex
SHA-256 of a mesh_ bearer secret retained by its control plane. Mesh stores a
management-scoped token under a service principal with no usable password. This
principal is excluded from the human operator roster, owner counts, and
first-human administrator setup. Existing revocation
survives process restart: a hash that was revoked from the dashboard is logged at
boot and never reissued, and any other active management token is left in place
rather than retired in favour of a dead hash. Supplying a different hash retires
earlier management tokens for this identity. Bootstrap takes an install-wide
advisory lock, so replicas booting with different hashes serialize instead of
each leaving the other’s token active; keep all replicas on the same hash during
normal operation. Two configurations stop boot with an actionable error rather
than silently disabling management access: a human user already owning the
reserved service email, and a hash that an active operator (admin or read)
token already carries — the unique-hash constraint would otherwise let that
broader token stand in as the control plane’s credential.
The bearer may access model defaults, agent API routes, model catalogs, and
POST /api/management/users. It cannot mint API tokens or edit unrelated install
settings. A human administrator can inspect/revoke it through the token settings UI.
In external-auth mode, user provisioning accepts { "subject": "issuer-subject", "email": "user@example.com" } and returns { "id": "...", "email": "..." }.
It always uses the deployment’s trusted issuer, is idempotent for an existing issuer
and subject, and does not create a password/session or link an existing account by
email alone. Local-auth instances reject the operation.
Conversation labels are shared across the agent overview, conversation roster,
channel filter, conversation detail, and actor timeline. Actor message responses
include conversation_display_name, resolved through the same cached,
agent/install-scoped lookup as conversation display_name. All dashboard labels
use conversationLabel to prefer the resolved name, then stored title, channel
ID, and finally conversation key. Adding another conversation link should use
that helper rather than recreate a fallback chain.
Decision model settings
Human owner/admin credentials can configure the independent installation decision role. Management-scoped provisioning tokens cannot access these endpoints.GET /api/settings/decisionsreturnsconfig: {provider, model, body_retention_days, last_source?}, available nativeproviders: [{name, label, default_model}], booleankeyspresence, andagents: the route each agent’s classifiers take under the saved configuration,[{slug, display_name, text_provider, disabled, route, provider?, billing?, model?}].routeisnative(a decision model fromprovider;billingisdecision_keyfor the installation decision account, or, when automatic mode reuses the text-model credential,instance_model_keyfor an agent billed by the installation’s model key,agent_model_keyfor the agent’s own, ordeployment_model_keywhen serving substitutes the process fallback key: a legacy agent with no provider and no key of its own, or an installation-billed agent on an install that never saved model settings and holds no installation key for the default provider;no_model_keywhenever the credential serving would use does not exist — no fallback key in those cases, or no stored key for an agent’s named provider or an installation’s saved provider — so the call cannot succeed;no_decision_keywhen an explicit source’s installation key is not stored),text(a text model on the agent’s own provider;modelis the classification override, absent means the agent’s fast model) orunavailable(a stored provider this build cannot serve; classifiers fail closed). It is computed by the same resolver serving uses, after the same provider inheritance (an installation-billed agent with no provider of its own uses the installation model provider; a provider this build does not implement reports as the catalog default). It never returns credentials.PUT /api/settings/decisionsaccepts{config: {provider, model, body_retention_days, last_source?}, api_key?}.last_sourceis kept only withprovider: "fast": the source (autoor a native provider) to restore when decision models are turned back on, returned byGETso a reload does not silently switch provider and payer. An unsupported value is refused withinvalid_last_source; with any other provider it is dropped. Providers areauto,fast,typesafe, andopenrouter.fastmeans decision models are off: every classifier runs on a text model through the agent’s own provider.autois the default and turns them on through each agent’s text provider: native OpenRouter decisions reuse each agent’s own text-provider billing key when available, while other text providers retain their fast classifier. Explicit native providers use a separate encrypted installation key and work independently of text-model provider selection. Omitapi_keyto preserve the selected provider’s existing key. Empty keys are rejected. For a native provider model may be blank to select the provider’s versioned default; forfasta model names the text model classifiers use instead of each agent’s fast model (it must exist on every agent’s provider);autorequires a blank model.body_retention_daysis 0 (default, retain indefinitely) or 1–3650. Pruning removes request/response bodies while retaining outcomes, provenance and usage. Response status isactiveorsaved_restart_requiredafter registry reload.POST /api/settings/decisions/testchecks an explicitly configured native provider using synthetic text and returns{ok: true, model}. It does not test automatic mode or send conversation history.
GET /api/agents/{slug}/decision-calls?before=<id>returns{calls, next_before}with up to 50 metadata summaries, newest first, includingbodies_available.GET /api/agents/{slug}/decision-calls/{id}includes exact request/response bodies when retained. Foreign-agent and nonexistent IDs return 404.
Workspace network policy
Owner-only. Agent workspace networking is unrestricted by default. Optional allowlist mode restricts destinations on capable backends; this is independent of credential permissions and the document parser’s network isolation.GET /api/settings/workspace-egressreturns{config: {mode, allowlist, uncontrolled}, built_in, max_allowlist}.modeis"open"(default for an unsaved install) or"allowlist".allowlistcontains additions tobuilt_in(Mesh’s source-host and package registry list); the runtime also permits the Mesh broker host in restricted mode.uncontrolledis"allow"or"refuse": in allowlist mode, accept open egress on a backend without enforcement or refuse to create a workspace there. It has no effect in open mode.max_allowlistis 200.PUT /api/settings/workspace-egressaccepts{config: {mode, allowlist, uncontrolled}}and returns the same view. All three fields are required. Invalid modes, missing/null fields, invalid domains, or storage failures leave the existing policy unchanged. Domains are hostnames or single leading wildcards (*.example.com), not URLs, IPs, ports, paths, or bare*; entries are lowercased, deduplicated and sorted even in open mode.- Saved legacy documents without
moderetain allowlist behavior. GET returns the explicit effective mode so owners can switch to"open"; omitted mode in new writes is rejected rather than silently changing a restriction. - Changes apply to newly created workspaces. Running and held/paused sandboxes keep their original policy until replaced. No live settings are changed by a software upgrade alone except the default used when no policy was ever saved.
mode to "open"; retained allowlist and
fallback settings are dormant. Open egress permits sending data and granted
credentials to arbitrary reachable destinations; it does not grant credentials.
Run ledger retention
Owner-only. How longmodel_calls keeps the exact request and response bodies
it records per model call — the largest and most conversation-laden bytes in the
install (risks.md#r8 (repository)). Everything else on the row (status,
requested and serving model, prompt snapshot reference, reply text, finish reason,
token usage, latency, timestamps) is never pruned, so run history, cost rollups and
traces stay readable after the bodies are gone.
GET /api/settings/retentionreturns{config: {model_call_body_days}, max_days}.model_call_body_daysis 0 (default: keep indefinitely) or 1–3650;max_daysis the ceiling the server enforces.PUT /api/settings/retentionaccepts{config: {model_call_body_days}}and returns the same view. Out-of-range values are refused withinvalid_retentionand leave the saved window unchanged. There is nothing to reload: the maintenance pass reads the window on its next tick (every minute) and removes bodies of terminal calls older than it, oldest first, in batches of 500 — up to 20 batches per tick per replica, and replicas take disjoint batches — so a backlog drains within minutes and sustained pruning keeps pace with ingestion. A call still running is never touched, and a window the pass cannot read or that is out of range prunes nothing. The window must be supplied explicitly:{}or{"config":{}}is refused rather than read as zero.
bodies_pruned: true on the trace (request_body is ""
and response_body is null), so “removed under policy” is distinguishable
from “this call never produced a response”.
Coding-provider credentials and sandbox delivery
Native GitHub and GitLab connections exposeworkspace_delivery with kind,
enabled, revision, and source. Store an agent’s own PAT through its
credential connection action (github_pat or gitlab_pat), then separately
use workspace_delivery with that same target and input
{"enabled":true,"revision":<current revision>}. A stale revision returns 409.
The connection must belong to the addressed agent; credential kinds cannot be
swapped across provider connections. Saving a PAT alone never enables delivery.
See GitLab setup and supported scope (repository) and
sandbox credential delivery (repository).