Agent Control¶
Editions: OSS. Contributor documentation for this repository.
Agent Control is the audited operator channel to managed agents such as OpenClaw and Hermes. This chapter covers the control WebSocket, CLI/desktop enrollment, and mobile/watch voice contact.
The control WebSocket runs authentication, heartbeat, command persistence and disconnect cleanup in short worker-owned database sessions. Only frozen scalar identity and copied command envelopes leave a worker; no ORM object crosses the thread boundary. Idle sockets do not reserve database connections. Each socket's DB phases are serialized, and cancellation drains its active worker before another phase starts. Socket and NATS I/O happen after the session closes.
A nullable managed_agent.control_connection_id UUID records the current socket
generation independently of its durable runtime identity session. Every inbound
message and every delivery read/mark locks the managed agent, then checks the
current generation, active credential, active account/owner and agent lifecycle before
runtime or command writes. Suspending or decommissioning invalidates the
generation. Disconnect clears heartbeat, mode and the matching runtime binding
atomically only if it still owns that generation, including across API replicas.
Revoked sockets may clean up their own presence but cannot retire a replacement.
Runtime-session churn does not revoke the durable agent credential. The account
active check is a scoped read rather than a row lock, avoiding a new lock order
with account lifecycle operations; a suspension racing that read is rejected by
the next control unit.
PostgreSQL control transactions set local lock and statement timeouts (1.5s and 5s); pooled connections retain their normal settings afterwards. A failed inbound transaction closes with code 1013 so the runtime can reconnect; replaced or revoked connections close with code 4000 to avoid an eviction reconnect loop. The engine's pool checkout timeout remains separately configured. Database capacity errors during authentication remain operational failures.
Local delivery, reconnect replay and NATS callbacks use persisted account/agent command envelopes, revalidating ownership before delivery and again before its mark. Producers snapshot delivery identity and release their post-commit read transaction before awaiting the guarded sender, so concurrent command requests and in-session question notices leave pool capacity for delivery. Release refuses pending caller writes instead of committing or discarding them. Replacement can occur between a completed read and socket I/O; that old socket cannot subsequently acknowledge or mark the new connection's work. Delivery remains at least once: adapters must deduplicate by stable command ID before irreversible effects. No database lock is held across the network to claim exactly-once execution. Unknown/peer command results are rejected and repeated final results do not duplicate activity history. The server owns result identity, source and role metadata even when the runtime supplies those keys.
Apply migration 20260912_control_connection before deploying this code. Old
replicas do not enforce generation checks, so complete the application rollout
and reconnect old control sockets before relying on the new fencing guarantee.
Local registration serializes the database claim and registry handoff; eviction
close attempts have a one-second deadline so an unresponsive old socket cannot
hold unrelated handshakes indefinitely.
The disposable PostgreSQL acceptance suite is
backend/tests/api/test_control_postgres_acceptance.py. It uses real sockets and
a pool of two, an independent lock-holding session, separate managers, and local
synthetic credentials. Set PRELOOP_DISABLE_TELEMETRY=true and DATABASE_URL to a
migrated disposable PostgreSQL database (UTC), then run that file with pytest.
Agent Control¶
- Purpose: Agent Control gives autonomous agents such as OpenClaw and Hermes a single, audited channel for online presence, operator messages, status updates, interruption, and future voice-originated contact.
- Implemented Today: Backend Agent Control exposes
WS /api/v1/agents/control/wsfor runtime-credential agent connections andPOST /api/v1/agents/{agent_id}/control/commandsfor authenticated operator text commands. It authenticates the runtime principal, binds presence to the managed agent and runtime session, publishes command envelopes through NATS when available, falls back to local delivery, emits account-scoped realtime events, and accepts heartbeat/status/presence/event envelopes from agents. Operator commands are now persisted BEFORE delivery in theagent_control_commandtable (state machine: pending → delivered → acked, with failed/expired side states, TTL viaagent_control_command_ttl_seconds); reconnecting agents receive undelivered commands in order with their originalcommand_ids (runtime plugins should dedupe onmessage_id), and inboundcommand_ack/command_result/command_errorenvelopes mark acknowledgement. - Related Implemented Surfaces: Browser, mobile, and console clients can use account-scoped realtime topics over WebSocket. Runtime sessions, managed-agent records, model-gateway usage, approval events, and operator lifecycle actions already share account-scoped event routing.
- Scaffolded Today:
account_realtimedefines normalized topics such asruntime_sessions,managed_agents,gateway_activity,budget_health, andaudit; the WebSocket manager can filter broadcasts by account and topic; frontend runtime-session and managed-agent views subscribe to those topics. Mobile/watch clients have native voice UI scaffolds that can create operator text turns, but the end-to-end user experience still depends on runtime adapters and production hardening. - Runtime Plugins (shipping): Standalone open-source runtime plugins live in
runtime-plugins/(@preloop-ai/openclaw-pluginon npm, TypeScript, andpreloop-hermes-pluginon PyPI, Python) and implement thepreloop.agent_control.v1protocol: they readpreloop.control.control_ws_url, connect with the durable runtime bearer token, own reconnect/backoff behavior, keep the WebSocket open, send heartbeat/status events, advertise capabilities, receivesend_messagecommand envelopes, acknowledge delivery, map operator messages into their own interactive runtime, and gate native tool calls through Preloop approvals (fail-closed by default).preloop agents install-plugin <agent>delegates installation to the runtime marketplace;PUBLISHING.mdcovers lockstep versioning. Existing enrollment can rewrite MCP and model traffic even when the runtime plugin is absent, but Agent Control is not enabled until that plugin is running inside the agent process. - Desktop capability: Hermes and OpenClaw read
$PRELOOP_DESKTOP_FILEor~/.preloop/desktop.jsonand add two keys to the presencecapabilitiesobject. A parsed file whosevnc.hostis exactly127.0.0.1sendsdesktop: "vnc"anddesktop_display(the manifestdisplay, typically:99); every other case, including a missing file or a missing key from another plugin, sendsdesktop: "none". The VNC password file and the rest of the manifest (host, port, auth, browser) stay on the machine. The backend stores that envelope with the agent's presence and exposesdesktop("vnc","rdp", or"none") anddesktop_displayon the managed-agent API. Brokered viewing and RDP detection are not part of this signal. - Claude Code Sidecar (prototype): Claude Code has no in-process plugin API for message injection, so
@preloop-ai/claude-plugin(runtime-plugins/claude-preloop) runs as a standalone sidecar daemon implementing the samepreloop.agent_control.v1protocol. It reads~/.claude/preloop-control.json(its own file;settings.jsonstays reserved for Claude Code), drives sidecar-owned sessions through the Claude Agent SDK (streaming input forsend_message,resumefor persisted sessions,interrupt()), and reports presence/telemetry for interactive terminal sessions by tailing~/.claude/projects/**/*.jsonl(summaries only, no transcript upload). Interactive TUI sessions are observe-and-approve, not steerable mid-turn; targeting one resumes it headlessly. Tool approvals remain on the PreToolUse permission hook installed bypreloop agents onboard --approvals; owned sessions load filesystem setting sources so the same hook fires there, and stopping the sidecar never ungoverns anything. - Codex CLI Sidecar (prototype): Codex CLI likewise has no in-process plugin API for message injection, so
@preloop-ai/codex-plugin(runtime-plugins/codex-preloop, binpreloop-codex-plugin) runs as a standalone sidecar implementingpreloop.agent_control.v1withruntime: "codex". It reads~/.codex/preloop-control.json(its own file;config.tomlandauth.jsonstay reserved for Codex). Owned threads are driven through@openai/codex-sdk:start_new_sessioncallsstartThreadin<workspace_root>/<repository_slug>whenmetadata.workspace.modeispersistent_checkout, otherwise incwdorworkspace_root, a follow-up turn on an owned thread callsthread.run, an unknowntarget_session_idcallsresumeThread, andinterruptaborts the in-flight run with anAbortControllerand answerscommand_resultwith a stopped marker (status: "stopped"). Presence for interactive sessions comes from tailing~/.codex/sessions/**/*.jsonl(summaries only). Advertised capabilities match the Claude sidecar exceptworktree. Tool approvals stay on the hook installed at~/.codex/hooks.jsonbypreloop agents onboard "Codex CLI" --approvals. The sidecar does not setapprovalPolicytonever, does not rewriteconfig.tomlorauth.json, and stopping it never ungoverns anything (fail-closed).codex_sandbox_modedefaults toworkspace-write;danger-full-accessis refused unlesscodex_allow_full_accessis also set.
Desktop install (desktop.install)¶
POST /api/v1/agents/{agent_id}/desktop/install (manage_agents) persists and
delivers a desktop.install command whose message_id is the returned
request_id, with payload {request_id, managed_agent_id}. An agent with no
Agent Control config, or no delivery channel, gets 409
desktop_install_unavailable with the one-line command and nothing is sent.
The plugin answers with the usual command_result, carrying
desktop_install: {ok, error?} where error is desktop_unsupported_os,
desktop_unsupported_distro, desktop_privilege_required or
desktop_failed. GET .../desktop/install/{request_id} maps the stored row to
pending, done, failed (with error) or unsupported: a plugin that
predates the command answers command_error "unsupported agent-control
command" (Python kit) or a command_result without desktop_install (older
OpenClaw), and both read as unsupported so the console falls back to the
command. The Python kit (HookedDesktopInstaller, run_desktop_enable, on by
default for Hermes) and the OpenClaw plugin run preloop desktop enable --agent
<id> locally. Runners do not implement it yet; the runner page has no
Enable desktop action.
Runtime support¶
| Kind | Channel | New session | Notes |
|---|---|---|---|
hermes |
in-process plugin | yes | PyPI preloop-hermes-plugin |
openclaw |
in-process plugin | yes | npm @preloop-ai/openclaw-plugin |
claude_code |
sidecar | yes | @preloop-ai/claude-plugin, ~/.claude/preloop-control.json |
opencode |
in-process plugin | yes | @preloop-ai/opencode-plugin |
pi |
harness plugin | no | text on an already-open session only |
deepseek |
harness plugin | no | text on an already-open session only |
codex |
sidecar | yes | @preloop-ai/codex-plugin, ~/.codex/preloop-control.json |
- Target Protocol: A managed agent opens a durable WebSocket using its runtime credential, sends runtime principal/session metadata, subscribes to account and agent-specific command topics, publishes heartbeat/status updates, and acknowledges command delivery. Server-side commands should be persisted and audited before delivery so reconnecting agents can recover missed instructions.
- Session Prompt Semantics: Operator text sent through Agent Control is an auditable user/operator turn for the selected runtime session. It is not a hidden system prompt, policy override, or privileged tool instruction. Runtime adapters should inject it as the next user-facing instruction in the agent's normal conversation model, preserve the current session context when possible, record the originating surface in metadata, and continue routing any resulting tool calls or model calls through the MCP firewall, model gateway, and approval policies.
- In-Session Question Delivery: When the
ask_userorrequest_approvalbuiltin raises a pending approval and the asking session's runtime has a live Agent Control connection,preloop.services.ask_user_inbandalso delivers the question into that session as an auditedsend_messageturn (persisted inagent_control_command, logged asagent_control_messageactivity withkind=preloop_question_notice). The in-session turn is a notice with token-free deep links (/console/approval/<id>web,preloop://approve/<id>mobile), never an answer channel: anything returned over the control WebSocket is agent output and is not accepted as the human's answer. Answers enter only through the governed approval endpoints, so the single approval record and its quorum/first-answer-wins semantics are preserved across surfaces. - Security Boundary: The channel uses the same runtime principal, subject-scoped governance, and API-key revocation model as MCP and gateway traffic. Commands that trigger tool use, model calls, or local side effects still flow through the MCP firewall, model gateway, or explicit approval policy rather than bypassing enforcement.
sequenceDiagram
participant Agent as OpenClaw/Hermes Adapter
participant WS as Preloop Agent WebSocket
participant Runtime as RuntimeSession
participant Operator as Console/Mobile/Watch
participant Policy as MCP/Gateway/Policy
Agent->>WS: Connect with managed runtime credential
WS->>Runtime: Bind or refresh runtime session
Operator->>WS: Send Agent Control command or voice-originated message
WS->>Runtime: Persist audited command event
WS-->>Agent: Deliver command
Agent->>Policy: Execute governed tool/model path
Policy-->>WS: Emit account realtime update
WS-->>Operator: Stream status/result
Flow execution on a persistent agent¶
A flow can target a live managed agent instead of provisioning an ephemeral
container. The flow form stores agent_config.execution_path = "persistent"
and agent_config.target_agent_id. create_executor_for_execution selects
AgentControlExecutor for that config before private-runner pool
resolution. Missing target_agent_id fails at start with AgentStartError;
there is no fallback onto a hosted container.
Envelope¶
The orchestrator already renders the flow prompt. The executor sends that
string as one audited send_message with start_new_session=true,
input_mode=text, and metadata:
| Key | Source |
|---|---|
source |
always flow_execution |
flow_id, flow_execution_id, flow_name |
execution context |
trigger_event_source |
trigger payload |
repository, ref |
trigger payload when present |
timeout_seconds |
the flow's timeout budget |
run_limits |
flow limits, including max_duration_seconds, when the flow sets limits |
model |
the flow's Claude model (gateway alias or identifier), only for Claude models |
workspace |
checkout contract below, or an authorized host directory. No credentials. |
workspace.mode is also a prompt placeholder ({{workspace.mode}}).
Ephemeral container runs render ephemeral. Persistent runs render
persistent_checkout or clone_less.
Workspace¶
When git_clone_config.enabled is true and the trigger or clone config
names a repository, workspace is:
| Key | Meaning |
|---|---|
mode |
persistent_checkout |
repository_url |
credential-free clone URL |
repository_slug |
owner/repo path under the sidecar workspace_root |
default_branch |
repository default branch |
ref |
branch the sidecar falls back to |
fetch_ref |
pull or merge request ref to fetch first, when present |
sha |
commit to check out detached, when the trigger has one |
pr_number |
pull request or merge request number, when present |
Clone depth and submodules are not part of this contract. The container
clone path does not take them from GitCloneConfig either. A config with
more than one repository stays clone_less: one workspace object cannot
name every checkout the container would make.
repository_url never contains a password. An ssh://git@host/... URL
keeps the git user. Other schemes are dropped and the run is
clone_less when no safe URL remains. A repository whose name cannot be
a safe checkout directory name (unsafe or missing repository_slug) also
stays clone_less: a persistent checkout is a directory named by the slug.
When clone is disabled, or no repository can be resolved, workspace is
{mode: "clone_less"}. The review reads the diff from the tracker.
When the flow sets agent_config.workspace, workspace is
{mode: "authorized_directory", kind, id | path, allow_dirty} and takes
precedence over the checkout. The Claude sidecar resolves it against the
host's ~/.preloop/runner.json authorized_directories (written by
preloop runner dirs add) and refuses unlisted paths, read_only entries,
entries limited to other harnesses, the filesystem root, the home directory
or a directory containing it, credential directories, and a dirty tree unless
allow_dirty is true. The sidecar no longer reads cwd from the payload or
metadata. It passes metadata.model to the Agent SDK as options.model and
uses run_limits.max_duration_seconds (else timeout_seconds) as the turn
timeout, bounded above by turn_timeout_ms when that is set. Only
metadata.model is read for the override (not gateway.model), so a
non-Claude flow model cannot slip through on a shared Claude Code target.
A successful command_result may include a usage object (input_tokens,
output_tokens, total_tokens, total_cost_usd) from the Agent SDK turn.
The orchestrator folds that into the execution's token and cost totals when
the run has no gateway-attributed usage rows.
A flow's agent_config.approval_policy (mode: auto_allow, tools,
paths, and commands prefixes for Bash) auto-approves covered native tool calls for one execution. The
sidecar exports PRELOOP_FLOW_EXECUTION_ID to the session it starts for a
flow message; the permission hook sends it as flow_execution_id. The
permission-check endpoint resolves the grant only for the managed agent
bound to that active execution, binds it to the first native session id
that presents it (stored in the execution's _agent_control binding), and
records covered calls as approval requests with
auto_approved_reason = "flow_approval_policy". Rules still decide first.
The Claude sidecar (runtime-plugins/claude-preloop) and the Codex
sidecar (runtime-plugins/codex-preloop) both handle persistent_checkout
as follows.
- Ensure
<workspace_root>/<repository_slug>exists. Clone once, then fetch on that same run and on later runs. Fetch triesfetch_ref, thensha, thenref. The commit is checked out detached. The Codex sidecar runs the thread withworkingDirectoryset to that checkout.clone_lessand a missingworkspacekeep the previous directory (cwd, elseworkspace_root). spawn_worktreeon the Claude sidecar creates a worktree after that checkout and runs the turn there. The Codex sidecar does not create git worktrees. Aspawn_worktreerequest fails withcommand_errorbefore git runs.- A second
persistent_checkoutfor the same repository while a turn is still running can check out another commit in that directory. Eviction skips the in-use directory, but checkout does not. That matches the Claude sidecar, which can isolate the turn withspawn_worktree. The Codex sidecar cannot, so wait for the turn to finish. - Git operations on one repository directory are serialized in-process. Eviction takes the same lock and skips a directory whose turn is still running.
- A dirty tree fails the command with
command_error. The sidecar does notreset --hardorcleanit. A persistent turn that leaves uncommitted edits in the checkout fails the next run on that repository. The Claude sidecar can isolate that turn withspawn_worktree. The Codex sidecar does not, so commit or clean before the next turn. Ownership ispreloop.managedcheckoutin the repository's git config, so a restart still recognises a tree the sidecar checked out. ssh://git@host/...is a valid clone URL. A password in the URL is refused.protocol.ext.allowandprotocol.file.allowarenever.- The resolved path is
metadata.workspace_pathoncommand_result(the ack handler does not store a payload) and on anevent/session_activity. workspace_repositories_max(default 20) evicts the least recently used clean checkout. Dirty directories are kept.workspace_fetch_timeout_ms(default 120000) bounds fetch and clone.
The envelope shape is the same one runtime plugins already accept (text,
metadata, input_mode, session_mode, start_new_session). The Claude
sidecar ignores a raw cwd; the directory comes from workspace. Persist-before-deliver is unchanged: the command row is written, then
local WebSocket or NATS delivery runs.
Binding¶
The session reference is control:{managed_agent_id}:{command_id}. The
command id is stored on the execution under
trigger_event_details._agent_control (with the live and history session
ids) so stop and status can recover both ids without a schema migration.
Trigger bodies cannot set _agent_control; it is a reserved key.
Status mapping¶
| Agent Control command | Executor AgentStatus |
Flow execution |
|---|---|---|
pending or delivered |
RUNNING |
stays running |
acked without a result |
RUNNING |
stays running |
acked plus command_result (success) |
SUCCEEDED |
SUCCEEDED |
command_error, failed, or expired |
FAILED |
FAILED |
operator/timeout stop |
FAILED (error=stopped) |
STOPPED or timeout failure |
A repeated final command_result / command_error does not flip a terminal
command or duplicate session activity.
The container confirmation contract (PRELOOP_AGENT_EXEC_START plus success
sentinel / result.json) does not apply: this path never emits the start
marker, so a successful command_result is sufficient.
Timeout and stop¶
The orchestrator TimeoutBudget still bounds the poll loop. When the budget
expires it calls AgentControlExecutor.stop, which sends send_message with
interrupt: true and session_mode: current (no tracking-session UUID).
That interrupts the agent's current session: the most recently owned
plugin session, which is usually this flow but can be a later operator
turn if one started. Native session ids from ack/presence are not persisted
on the binding yet. The bound command is marked stopped only after that
interrupt is delivered; a failed interrupt leaves the command non-terminal
so the operator can see the remote session is still live. The execution
itself still fails with the timeout message.
Start refuses (classified runner_error) when the target is missing,
inactive, not an allow-listed Agent Control kind, has no verified control
config, or has a stale heartbeat (the same three-heartbeat window the
console uses for plugin_connected). Allow-listed kinds that only accept
text on an already-open session (currently Pi and DeepSeek) also fail at
start: persistent dispatch always sends start_new_session=true. The
operator endpoint uses the same
CONTROL_NEW_SESSION_UNSUPPORTED_KINDS set.
Managed CLI/Desktop Agent Enrollment¶
- Discovery Entry Point:
preloop agents discovercan stay read-only (--json,--no-onboard-prompt) or hand off interactively into managed enrollment, with--yesavailable for auto-onboarding. - Shared Enrollment Engine:
preloop agents enroll <agent>and discovery-triggered onboarding both create or reuse a managed runtime identity, import representable MCP servers, mint a durable credential, back up the local config, and rewrite supported local endpoints to Preloop-managed MCP and gateway URLs. For Agent Control, the CLI writes thepreloop.controlcontract and can delegate installation to runtime-native plugin managers, but it does not itself own the long-lived Agent Control WebSocket or execute operator commands. - OpenClaw Coverage: The current OpenClaw adapter supports legacy and newer config locations, JSON5 parsing, gateway-backed model rewrites, and conservative import of command-backed MCP entries such as
mcporterwhen an upstream URL can be inferred safely. - Hermes Coverage: The Hermes adapter discovers
~/.hermes/config.yaml/.ymlor installed-but-unconfigured Hermes markers, preserves existingmcp_servers, adds a managedpreloopHTTP MCP server, rewrites supported model configuration to Preloop's/openai/v1gateway, and can import provider-specific environment keys or ChatGPT/Codex OAuth material when present. Onboarding, offboard, restore, andinstall-pluginrestart the Hermes gateway; on Linux they also print systemd user units namedhermes-*. Operational recovery (systemd-first stop, config-path diagnosis) is in Hermes: onboarding, rollback, and systemd recovery. - Credential Boundary: OpenClaw model credentials may be declared inline under
models.providersor indirectly throughauth.profiles; the enrollment path imports model metadata either way, but profile-backed provider secrets may still require manual configuration inside Preloop. - Durable Identity:
ManagedAgent.agent_kindis now stored alongsidesession_source_typeso operator UX and reporting do not depend on an active runtime session to recover the agent family. - Explicit Model Association: Onboarding now persists direct managed-agent to AI-model bindings instead of inferring one configured model indirectly from
AIModel.meta_data.
Mobile and Watch Voice Contact¶
- Implemented Today: iOS, watchOS, and Android clients are documented and implemented around approval review, push notifications, QR pairing, and WebSocket-driven approval updates.
- Implemented Web Voice: The web console Agent Control composer prefers browser-native
SpeechRecognitionandspeechSynthesis, then falls back to server STT/TTS endpoints backed by speech-capableAIModelrows when browser audio APIs are unavailable. - Scaffolded Native Voice: iOS/watchOS and Android contain native STT/TTS or dictation scaffolds that can capture a user turn and call the Agent Control command surface, but production behavior depends on backend availability, managed-agent lookup, and OpenClaw/Hermes runtime adapters being online.
- Planned Voice Path: Mobile/watch voice should start as a native app feature using vendor STT/TTS APIs, then post normalized operator messages into the same runtime session and Agent Control channel used by the console. The server remains the source of truth for transcript, command intent, approval requirements, and delivery state.
- Siri Constraints: Siri Shortcuts and App Intents can launch a predefined Preloop action, capture structured parameters, and hand the user into the app. They should not be treated as a general always-listening background transport for arbitrary agent conversations.
- Google Assistant Constraints: Google Assistant/App Actions can deep link into Android flows and pass structured intent data where supported, but arbitrary background agent chat or cross-app streaming is not a dependable control surface. Android should hand off to the Preloop app before sending audited commands.
Persistent flow execution diagnostics¶
Flow execution detail exposes runtime_session_id, preferring the command's
tracking history session over a shared live session. The console's Open session
action uses that identity. For control: execution references, the execution
logs endpoint returns source: agent_control and reads the command lifecycle
and bounded, credential-scrubbed session activities through the bound Agent
Control executor. It does not attempt to look up a container for these runs.
The Claude sidecar forwards up to 200 assistant/tool progress messages per turn, plus a final duration/usage summary. The server checks the authenticated agent's command ownership and attaches progress to the command's tracking session. Tool starts are normalized as tool-call activities, with replay deduplication by command and call ID; arguments and tool results are excluded.
Session start consent¶
Managed agents default others_may_start_sessions to ask_me. Agent Control checks new-session intents before dispatch, while commands for existing sessions keep their existing behavior. An approved request is consumed once, scoped to its requester and exact intent. Owner consent uses the existing notification preferences and mobile push, and does not inherit tool approval bypasses or administrator decision exemptions. Notification delivery uses a short delivery lease to prevent concurrent workers from sending the same pending request; abandoned leases expire, and failed delivery releases the claim. Persistent flows also queue owner consent before starting a new agent session; automated triggers do not infer a human owner exemption. An executor recovered without its temporary launch credentials fails closed and asks for a flow restart rather than persisting credentials. Authorized nonowners starting a fresh runner-hosted harness session need the same single-use owner approval before checkout credentials are minted.
Operator-note receipts¶
Operator-note receipts distinguish a deferred claim from delivery at a supported
model or hook boundary. Native session-end signals settle pending and claimed
notes targeted at that session. The minute runner sweeper also reconciles notes
against authoritatively ended RuntimeSession rows in bounded batches, with
guarded transitions and once-only audit/webhook emission. It does not infer
termination from inactivity alone, and standing agent-targeted notes remain
available for a later session. The operator-note guide
lists harness capabilities; the verification guide
provides isolated receipt and lifecycle checks.
Agent note governance¶
send_note resolves its target and validates the hook's caller session against
its credential principal before evaluating standard tool access rules. Seeded,
editable rules allow self and descendants, request human approval for other runs
the sender's owner can control, and deny the rest. Scope facts are recorded with
the approval; async polling executes the approved call against its resolved
target. Request tokens authorize decisions by the bound human, while automatic
approval bypasses leave these requests pending for that human. Pending note
approvals have a separate per-sender/target cap, checked under the account's
approval admission lock. Startup seeds unconfigured accounts independently,
preserving progress and logging each failure for a later retry.
preloop spawn passes the registered conductor session and worker alias to
child processes and drops the conductor's harness identity variables. See
Spawning workers.
Remote agent deployment¶
The optional agent-deployment API authorizes account owners and administrators, then runs CLI installation and live onboarding through a host-key-pinned SSH connection. The GCP adapter creates a uniquely named VM without cloud identity, reads its host key through the authenticated Compute API, and removes resources on failure. Before returning success, the API checks the account's registered agent and selected model binding through CRUD. Credentials stay in request memory; audit events contain deployment identifiers and outcomes. See operator configuration.
Cost digest consumers use full-window CRUD aggregates for model and agent request rankings. SQL window totals preserve unknown activity and remaining known groups while returning at most three named entries plus a bucket row. Agent attribution prefers the account-owned direct managed principal, then an account-owned session's managed agent, then a named principal. Account constraints and scalar session association prevent cross-account labels and join fan-out. Console exact-period links preserve UTC microseconds and use normal authenticated account scoping; URL account context grants no access.
Console agent spend (Agents list, Agent detail, the Overview Inventory and the
user page, the last two via the opt-in breakdown=agents section of
GET /api/v1/account/gateway-usage/summary, which returns usage_by_agent
for the whole account and is empty when runtime_principal_id is set) reads
one CRUD aggregate. A
gateway row belongs to an agent when its runtime principal is the agent's base
source id, or when it ran in a session the agent owns: the bound session, a
session whose runtime principal is managed_agent:<id>, or a per-run session
whose source or principal id is <base>:<run> or <base>-<run> (longest base
wins). A row whose principal names another agent's base id stays with that
agent, so no row counts twice. Only the window differs between surfaces (all
time, the detail range, the Overview range). Subscription-routed rows
(cost_source='subscription') count in an agent's requests and tokens and add
$0 to its spend; each surface also reports the agent's subscription block
(the Cost page's SubscriptionUsageSummary: requests, tokens, the summed
meta_data.api_equivalent_cost estimate and its coverage) and shows "without
subscriptions" as spend plus that estimate, a lower bound when coverage is
partial and no figure when nothing is covered.
The Agents list reads all-time figures from agent_usage_total, grouped by
account, source principal and runtime session. PostgreSQL triggers apply
transactional deltas on usage insert, update and delete, so corrections and
rollbacks affect requests, tokens, metered cost and subscription pricing
together. Statement triggers group transition rows by scope before updating
counters, so bulk imports, repricing and deletes do not rewrite a shared counter
once per request. Monetary counters use decimal arithmetic. Agent ownership is resolved
at read time from these source dimensions, so reassigning a session does not
require rewriting historical usage or counters. Date-window detail and Overview
readers retain their existing source aggregates.
The migration installs capture before an online backfill. agent_usage_counted
records which source IDs contributed; bounded batches lock source rows and claim
IDs once, making retries and concurrent writes safe. agent_usage_backfill
persists progress and enables stored reads only after completion. During an
incomplete backfill the list uses its metered-cost fallback and reports
subscription_usage_computed=false. Once ready, the without-subscriptions
estimate appears in a separate column immediately after metered estimated spend.
Managed tracker OAuth persistence uses the provider-neutral storage contract,
with tenant-bound connection transactions and serialized token-pair rotation.
The console creates a managed tracker at consent completion, before scope is
saved. Subsequent Bitbucket repository and Jira project discovery carries that
tracker's id so the backend can recover its server-held grant even when the
browser has no token. Bitbucket repository choices load for the selected
workspace; changing workspaces clears stale choices and failures require an
explicit retry. Managed detail headers use the grant state rather than the
tracker's historical validation flag.
Consumption is the resolver contract in preloop.services.managed_credentials:
a tracker with auth_type == "managed_oauth" stores no token, and every
caller (tracker factory and scanner clients, api.common.get_tracker_client
behind MCP and REST, connection/scope testing, feedback reads, flow clone
credentials, and the execution-bound publication credential endpoint) asks the
plugin service managed_oauth_resolver:<provider> for a fresh credential
right before network I/O, after the usual account/project authorization. The
Bitbucket Cloud client pins the API origin, follows at most three validated GET
redirects within the HTTPS API namespace (including canonical diff/diffstat
commit-range URLs), refuses write or unsafe redirects, and forces exactly
one refresh on a 401 and has no Basic or stale-key fallback; typed
unavailable/reconnect-required/permission failures propagate instead of an
anonymous clone or a GitHub fallback. Late publication reuses the GitHub App
refresh wrapper: the runner's execution-bound capability is exchanged for the
current access token plus its git username (x-token-auth) only while the
execution is running and the destination lies inside the tracker's workspace
binding; refresh tokens and consumer secrets never enter containers.
Managed Jira connections sharing one Atlassian user may share its required callback URL. Registration retries that URL only after validating the configured HTTPS origin and persisted ownership under the same Jira cloud and OAuth user. Signed deliveries route by exact stored callback URL and returned webhook IDs, with active grants, current project scope, and subscriptions checked for each receiving tracker. Verified cloud/user namespace metadata is retained on webhook rows and backfilled on a successful refresh, so disconnecting the original connection does not revoke another active connection's registered delivery. Legacy rows require a live anchor grant until that reconciliation. They never fan out by site alone. Registration failures count as scanner errors even when issue synchronization succeeds. Managed repository URL lookup permits an empty stored key; credentials still arrive through the orchestrator's managed resolver.
Nanobot managed runtime¶
The optional runtime-plugins/nanobot-preloop process embeds a pinned Nanobot
Python SDK and reuses Agent Control, runtime enrollment, the model gateway and
MCP firewall. It owns persisted session references and bounds turns, duration and
token/context consumption. Native and MCP tool execution require explicit
Preloop permission decisions; background subagents and outbound channel tools
are disabled. See its README for supported limits and installation.