Bridge protocol
Harn's stdio bridge uses JSON-RPC 2.0 notifications and requests for host/runtime coordination below ACP session semantics.
Tool lifecycle observation
The tool/pre_use, tool/post_use, and tool/request_approval bridge
request/response methods have been retired in favor of the canonical
ACP surface:
- Tool lifecycle is now carried on the
session/updatenotification stream astool_callandtool_call_updatevariants (see the ACP schema at https://agentclientprotocol.com/protocol/schema). Hosts observe every dispatch via the session update stream — there is no host-side approve/deny/modify hook at dispatch time. - Approvals route through canonical
session/request_permission. When harn's declarativeToolApprovalPolicyclassifies a call asRequiresHostApproval, the agent loop issues asession/request_permissionrequest to the host and fails closed if the host does not implement it (or returns an error).
Internally, the agent loop emits AgentEvent::ToolCall +
AgentEvent::ToolCallUpdate events; the packaged harn-serve ACP adapter
translates them into session/update notifications via an AgentEventSink it
registers per session.
ACP compatibility contract
Harn-owned ACP/MCP extension fields are specified in
Harn ACP/MCP extensions v1. Harn tracks the
upstream Agent Client Protocol schema and pins its
wire contract against agentclientprotocol/agent-client-protocol schema
v0.12.2. The adapter treats these session/update values as standard
ACP variants:
user_messageuser_message_chunkagent_message_chunkagent_thought_chunktool_calltool_call_updateplan
Harn emits additional host-facing lifecycle updates outside ACP. They
ride as top-level sessionUpdate discriminators for compatibility with
existing Burin Code and other host renderers, advertised during
initialize under agentCapabilities._meta.harn.sessionUpdateExtensions:
artifactavailable_commands_updatefs_watchhandoffhitl_requesthitl_resolvedlogprogressreminder_emittedskill_activatedskill_deactivatedskill_narrowskill_scope_toolstool_search_querytool_search_resulttranscript_compactedtranscript_projectedworker_update
Unknown values should be ignored per ACP forward-compatibility rules.
Hosts that render Harn extensions should key off the explicit extension
list from initialize instead of a local allow-list.
Harn keeps its tool-rendering extensions under tool_call._meta.harn and
tool_call_update._meta.harn, following ACP's extension convention while
leaving canonical ACP fields at their standard locations. These Harn
metadata keys are advertised during initialize under
agentCapabilities._meta.harn.toolLifecycleExtensionFields:
auditdurationMserrorerrorCategoryexecutionDurationMsexecutorparsingrawInputPartial
The standard ACP fields (toolCallId, title, kind, status,
content, locations, rawInput, and rawOutput) remain available in
their ACP locations. Hosts migrating from pre-#904 builds should read the
listed Harn fields from _meta.harn instead of the tool-call update root.
Harn's pinned fixtures under crates/harn-serve/tests/fixtures/acp/ pin
both the standard and extension shapes so host integrations can reference
stable examples.
Vendor-extension session-update payload namespacing (#905)
Following the
ACP extensibility convention,
every vendor field on a Harn extension session-update is emitted under
update._meta.harn — never as a bare top-level field. The sessionUpdate
discriminator stays at the canonical location so existing dispatchers
that route on it keep working unchanged. Concretely:
| Update variant | Vendor fields under _meta.harn |
|---|---|
artifact | artifactId, kind, title, mimeType, spec, fallback, sizeBytes, provenance, metadata |
progress | phase, message, progress, total, data |
log | level, message, fields |
fs_watch | subscriptionId, events |
worker_update | workerId, workerName, workerTask, workerMode, event, status, terminal, metadata, audit |
transcript_compacted | mode, reason, strategy, archivedMessages, estimatedTokensBefore, estimatedTokensAfter, snapshotAssetId, instructionMode, instructionSource, compactionPolicy |
transcript_projected | policy, reason, prefixHash, keptCount, droppedCount, providerSafetyBlocked, redactedCount, reclaimedTokens, rootsConsulted, redactionPointers |
handoff | handoffId, artifactId, handoff |
skill_activated | skillName, iteration, reason |
skill_deactivated | skillName, iteration |
skill_scope_tools | skillName, allowedTools |
tool_search_query | toolUseId, name, query, strategy, mode |
tool_search_result | toolUseId, promoted, strategy, mode |
hitl_request | requestId, kind, payload |
hitl_resolved | requestId, kind, outcome |
reminder_emitted | reminder |
Hosts migrating from pre-#905 builds must read these fields from
_meta.harn.<field> instead of the update root. The fixture
crates/harn-serve/tests/fixtures/acp/session_update_extensions.json
pins the new wire shape verbatim.
reminder_emitted is sent when a pending system reminder is rendered
into the next model request. Its payload lives at
update._meta.harn.reminder:
{
"sessionUpdate": "reminder_emitted",
"_meta": {
"harn": {
"reminder": {
"reminderId": "reminder-1",
"tags": ["token_pressure"],
"body": "Refresh the compacted context before answering.",
"roleHint": "developer",
"renderedRole": "developer",
"source": "stdlib_provider",
"ttlTurns": 2
}
}
}
}
Content extensions (visible_text and visible_delta on the
agent_message_chunk content block, advertised via
agentCapabilities._meta.harn.contentExtensionFields) follow the same
convention but ride under content._meta.harn because they extend the
canonical ACP content block, not the session-update envelope. Example:
{
"sessionUpdate": "agent_message_chunk",
"content": {
"type": "text",
"text": "hello",
"_meta": {
"harn": {
"visible_text": "hello",
"visible_delta": "hello"
}
}
}
}
Composition event migration note
Governed Code Mode composition runs are represented on the Harn
_harn/agentEvent extension channel, not as one opaque tool call and
not as new ACP session/update variants. Consumers that do not render
composition events can ignore unknown _harn/agentEvent.kind values and
continue reading ordinary tool_call / tool_call_update events.
The Harn-native MVP is surfaced by composition_binding_manifest(...),
composition_execute(...), and the std/composition
composition_mcp_tools(...) profile. Binding manifests are hashable prompt
contracts; execution reports carry the same parent/child fields described here.
TypeScript declarations generated from a manifest are editor/model affordances,
not the bridge authority.
Consumers that do render them should group by runId:
composition_startidentifies the snippet language, snippet hash, binding-manifest hash, and requested side-effect ceiling.composition_child_callrecords each child binding operation withrawInput,policyContext,requestedSideEffectLevel, and theToolAnnotationsused for policy decisions.composition_child_resultrecords the child status, executor, output/error, and timing.composition_finishandcomposition_errorare terminal parent events carrying stdout, stderr, artifacts, structured result, duration, and failure category.
Portal and transcript indexers should treat composition events as a parent/child grouping overlay. A mutating child remains a mutating child operation because its own annotations and side-effect level are present; do not infer the whole run is read-only from the parent event alone.
audit tag
Both tool_call and tool_call_update carry an optional _meta.harn.audit
field that mirrors the active mutation session for the dispatch (see
Trust boundary). Hosts use it to:
- group every tool emission belonging to the same write-capable session (so undo/redo and audit logs never cross sessions even when multiple workers run in parallel);
- correlate the canonical
tool_callstream againstsession/update.worker_update.auditand the optionalsession/request_permission.mutationpayloads — they all carry the sameMutationSessionRecord, so a host that already understands one reuses the same codepath for the others; - decide whether to surface a tool dispatch in trust-boundary UX
(e.g. badge writes that escape
mutation_scope: read_only) without guessing from the tool name.
Wire shape (snake_case fields, matching the existing worker_update.audit
contract):
{
"_meta": {
"harn": {
"audit": {
"session_id": "session_42",
"parent_session_id": "session_root",
"run_id": "run_42",
"worker_id": "worker_3",
"execution_kind": "worker",
"mutation_scope": "apply_workspace",
"approval_policy": {
"auto_approve": [],
"auto_deny": [],
"require_approval": ["edit_*"],
"write_path_allowlist": ["src/**"]
}
}
}
}
}
The _meta.harn.audit field is omitted when no mutation session is installed (read-only
harn run invocations, conformance fixtures, scripts that don't enter
a workflow). Worker lifecycle audit data is carried as
worker_update._meta.harn.audit, matching the Harn extension namespacing
contract.
executor tag
tool_call_update carries an optional _meta.harn.executor field that names the
backend that ran the tool, so clients can render "via mcp:linear" /
"via host bridge" badges, attribute latency by transport, and route
errors correctly. Variants:
"harn_builtin"— VM-stdlib (e.g.read_file,write_file,exec,http_*,mcp_*) or any Harn-side handler closure registered in the script'stoolstable."host_bridge"— capability provided by the host through the bridge (Swift IDE bridge, BurinApp, BurinCLI host shells).{"kind": "mcp_server", "serverName": "<name>"}— tool came frommcp_list_toolsagainst the named server. The agent loop detects this from the_mcp_serverannotationmcp_list_toolsinjects on every dict, so the tag survives even when the call physically proxies through a bridge."provider_native"— provider executed the tool server-side and inlined the result (currently only OpenAI Responses-APItool_searchand the equivalent Anthropic native search; the agent never dispatches these locally).
Unit variants serialize as bare strings; the mcp_server case carries
the configured server name. The field is omitted when unknown — most
commonly for the in-progress emission that fires before the dispatch
backend is picked.
session/request_permission
Request payload (harn-issued):
{
"sessionId": "session_123",
"approvalRequest": {
"id": "tool-call_123",
"action": "edit_file",
"args": {"path": "src/main.rs"},
"principal": "worker_3",
"requested_at": "2026-04-30T12:00:00Z",
"approvers_required": 1,
"evidence_refs": [
{
"kind": "workspace_path",
"ref": "src/main.rs",
"metadata": {"workspace_path": "src/main.rs"}
}
],
"undo_metadata": {
"session_id": "session_123",
"run_id": "run_123",
"worker_id": null,
"mutation_scope": "apply_workspace",
"policy_decision": {
"type": "harn.permission_policy_decision.v1",
"action": "ask",
"reason": "workspace edit",
"matched_rule": {"source": "rules", "action": "ask", "id": "edit-src", "index": 0},
"risk_labels": ["approval_required", "path_rule"]
}
},
"capabilities_requested": ["workspace.write_text"]
},
"policyDecision": {
"type": "harn.permission_policy_decision.v1",
"action": "ask",
"reason": "workspace edit",
"matched_rule": {"source": "rules", "action": "ask", "id": "edit-src", "index": 0},
"risk_labels": ["approval_required", "path_rule"],
"context": {
"tool_name": "edit_file",
"tool_kind": "edit",
"side_effect": "workspace_write",
"paths": [{"workspace_path": "src/main.rs"}]
}
},
"toolCall": {
"toolCallId": "call_123",
"toolName": "edit_file",
"rawInput": {"path": "src/main.rs"}
},
"mutation": {
"session_id": "session_123",
"run_id": "run_123",
"worker_id": null,
"mutation_scope": "apply_workspace",
"approval_policy": {"require_approval": ["edit*"]}
},
"declaredPaths": ["src/main.rs"]
}
approvalRequest is the canonical Harn ApprovalRequest payload. Hosts should
render approval UI from that object and use policyDecision as the policy
rationale: it includes the matched rule, risk labels, normalized context, and
the action (ask for host approval requests). The same receipt is copied into
approvalRequest.undo_metadata.policy_decision and into
PermissionGrant/PermissionDeny transcript-event metadata so approvals are
auditable and replayable even when the host only persists the canonical request.
Treat the surrounding toolCall, mutation, and declaredPaths fields as
compatibility/context fields. The same shape is used in stdlib HITL notifications at
harn.hitl.requested.params.payload.approval_request, so Burin Code,
harn-cloud approval inboxes, and CLI-style hosts can share one renderer.
Field meanings:
id: stable approval request id. For tool permission prompts this is the emitted tool-call id; for stdlib HITL it is the HITL request id.action: human-readable action name, usually the tool or stdlib action.args: structured action arguments safe for host rendering.principal: agent, worker, session, or other actor requesting approval.requested_at: RFC3339 UTC timestamp.deadline: optional RFC3339 UTC deadline. Omitted when there is no deadline.approvers_required: number of approving reviewers required to proceed.evidence_refs: host-renderable evidence handles, such as declared workspace paths or run artifacts.undo_metadata: mutation/audit metadata a host can use to group undo/redo.capabilities_requested: canonical capability operation names requested by the action.
Response payload (host-issued):
{ "outcome": { "outcome": "selected" } }(ACP canonical): granted{ "granted": true }(legacy shim): granted with original args{ "granted": true, "args": {...} }: granted with rewritten args{ "granted": false, "reason": "..." }: denied
Worker lifecycle notifications
Delegated workers emit session/update notifications with worker_update
content. Those payloads include lifecycle timing, child run/snapshot paths,
and audit-session metadata so hosts can render background work without
scraping plain-text logs.
Lifecycle states
The runtime emits one of seven typed lifecycle events per worker
transition. The wire status string is the same value harn writes to
the worker's persisted state, so it round-trips through the bridge
unchanged:
| Event | status | Meaning |
|---|---|---|
WorkerSpawned | running | A worker (delegated stage, workflow, or sub-agent) has begun a new cycle. |
WorkerProgressed | progressed | A retriggerable worker is resuming after worker_trigger. Followed shortly by another running from the new cycle. |
WorkerWaitingForInput | awaiting_input | A retriggerable worker has finished its current cycle and is parked waiting for the next host trigger payload. |
WorkerCompleted | completed | A non-retriggerable worker has finished successfully (terminal). |
WorkerFailed | failed | A worker terminated with an error (terminal). |
WorkerStopped | stopped | A worker stopped gracefully after emitting a typed handoff summary (terminal). |
WorkerCancelled | cancelled | A worker was cancelled via close_agent or upstream cancellation (terminal). |
The four terminal events end the worker's lifetime. Progressed and
WaitingForInput are explicitly non-terminal — observers should
expect more events on the same worker_id after they fire.
worker_update notification shape
ACP and A2A adapters subscribe to the canonical AgentEvent::WorkerUpdate
variant and translate it into their respective wire formats from one
typed source. ACP emits a session/update with sessionUpdate: "worker_update". Per the vendor-extension namespacing
rule, all
worker fields ride under update._meta.harn:
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "session_123",
"update": {
"sessionUpdate": "worker_update",
"_meta": {
"harn": {
"workerId": "worker_abc",
"workerName": "review_captain",
"workerTask": "Review PR #42",
"workerMode": "delegated_stage",
"event": "WorkerWaitingForInput",
"status": "awaiting_input",
"terminal": false,
"metadata": {
"task": "Review PR #42",
"mode": "delegated_stage",
"started_at": "0193...",
"finished_at": null,
"awaiting_started_at": "0193...",
"child_run_id": "run_xyz",
"child_run_path": ".harn-runs/run_xyz",
"snapshot_path": ".harn/workers/worker_abc.json",
"audit": { "...": "MutationSessionRecord" },
"error": null
},
"audit": { "...": "MutationSessionRecord" }
}
}
}
}
}
The event discriminator is the typed WorkerEvent variant name; the
status field is the same lower-case value the legacy bridge status
field carried. terminal is a derived hint so clients can decide whether
to retain the worker in their tracking UI without parsing the event
name.
A2A surfaces the same event as a task-stream entry of type
worker_update, scoped to the task whose dispatch spawned the worker.
A2A is a separate protocol surface and keeps the worker fields at the
task-stream entry root (it has its own envelope conventions). ACP hosts
should always read worker fields from update._meta.harn.<field>.
Structured Harn plan emissions use the same task stream. When an agent calls
emit_plan or update_plan, A2A subscribers receive a harn_plan entry with
standard ACP-compatible entries plus the normalized harn.plan.v1 artifact
under plan.
Agent progress reports use protocol-native A2A status updates. When
agent_progress emits narration or entries, task-stream subscribers receive a
non-terminal status-update event with type: "status",
status.state: "working", a populated status.message, and final: false.
Entry lists render as a deterministic markdown checklist inside the message
text. Final task completion still emits the terminal completed status
separately.
External-agent delegation
Harn core exposes an open external-agent contract for paid or autonomous delegation over interoperable transports. It does not contain proprietary adapters; hosted products, Harn Cloud, or connector packages should translate their own APIs into the capability metadata and extension methods below.
Agent Cards advertise support under one of:
capabilities.externalAgentcapabilities._meta.harn.externalAgent_meta.harn.externalAgent
The object uses schema id harn.external_agent.v1 and should include:
{
"schema": "harn.external_agent.v1",
"pre_dispatch_checkpoint": true,
"budget_cap": true,
"idempotency": true,
"reviewable_handoff": true,
"dispatch": true,
"operations": [
"_harn/externalAgent.plan",
"_harn/externalAgent.dispatch"
]
}
A2A peers expose these as JSON-RPC extension methods on the card's JSONRPC transport:
_harn/externalAgent.planreturns a checkpoint withcheckpoint_id,plan,expected_scope, optionalevidence_refs, andmetadata._harn/externalAgent.dispatchaccepts the approved checkpoint,budget,idempotency_key,task,expected_scope,context, andmetadata, then returns a result with a reviewablehandoff,artifacts,diff/patch,receipts, andbudget_used(usd,tokens,seconds,tool_calls) where applicable.
std/external_agent.external_agent_delegate(target, task, options?) enforces the
safe sequence:
- Validate that
budgethas at least one positive cap and that anidempotency_keyis present or generated by the stdlib wrapper. - Resolve the Agent Card and prove
budget_cap,idempotency,reviewable_handoff, and dispatch support before any paid work. - Fetch a pre-dispatch checkpoint and return
status: "checkpoint_required"unlesscheckpoint.approvedis already true. - Dispatch only after approval, preserving the same idempotency key. Replays of
a completed delegation return
status: "replayed"without another remote dispatch. - Normalize the result into a typed
harn.external_agent.handoff.v1envelope with a handoff artifact and a diff artifact when a patch is present.
If a peer cannot provide a remote checkpoint, normal dispatch is refused. Hosts
may set checkpoint.allow_local_fallback: true with an explicit local plan; Harn
will still require the remote peer to advertise budget, idempotency, reviewable
handoff, and dispatch support before sending the approved task. When a peer
reports budget_used above the cap, Harn returns a reviewable envelope with
status: "budget_exceeded" rather than silently treating the work as clean.
Daemon idle/resume notifications
Daemon agents stay alive after text-only turns and wait for host activity with adaptive
backoff: 100ms, 500ms, 1s, 2s, resetting to 100ms whenever activity arrives.
agent/idle
Sent as a bridge notification whenever the daemon enters or remains in the idle wait loop.
Payload:
{
"iteration": 3,
"backoff_ms": 1000
}
agent/resume
Hosts can send this notification to wake an idle daemon without injecting a user-visible message.
Payload:
{}
On the ACP adapter surface, hosts can wake the daemon by sending a pending
user-message inject. session/inject accepts {sessionId, mode, content}
where content is a string or ACP content-block array, then responds
immediately with status: "accepted" and an agent-owned messageId. Harn
later delivers the same id as session/update with sessionUpdate: "user_message". mode: "steer" maps to the next safe operation boundary, and
mode: "queue" maps to end-of-interaction delivery. Pending injects can be
revoked with session/revoke_inject or edited in place with
session/replace_inject before delivery; successful mutation responses return
status: "revoked", "already_revoked", or "replaced". Races return
structured error data with reason: "already_delivered",
"already_revoked", "unknown_message_id", or
"not_owner_or_not_authorized".
To inject ambient context without pretending it is a user message, send
session/remind:
{
"body": "The workspace changed while you were idle; re-read src/lib.rs before editing.",
"tags": ["workspace"],
"dedupe_key": "workspace-change",
"ttl_turns": 2,
"role_hint": "system",
"mode": "interrupt_immediate",
"_meta": {
"harn": {
"origin": "file-watcher"
}
}
}
mode accepts the same delivery values as queued user messages:
interrupt_immediate, finish_step, and audit_only. audit_only
reminders drain at loop_exit and land in the transcript but are never
rendered into a model prompt — see
steering seams for the full table. The reminder
payload is validated as a reminder spec; non-standard host metadata belongs under
_meta. Malformed reminder payloads are rejected with HARN-RMD-002; unknown
top-level reminder options are rejected with HARN-RMD-001.
Hosts that need an operator-facing queue can call
session/pending_injections with { "sessionId": "..." }. The response is
{pendingCount, injections} in FIFO order, with kind: "user" rows for
pending session/inject messages and kind: "reminder" rows for pending
session/remind reminders. Reminder rows include reminderId, mode, body,
tags, dedupeKey, ttlTurns, roleHint, and source.
Queued reminders can be revoked before delivery with
session/revoke_reminder and { "sessionId": "...", "reminderId": "..." }.
Successful revocation returns {reminderId, status: "revoked"}; repeated
revocation returns status: "already_revoked". Races after a checkpoint drains
the reminder return a structured already_delivered error.
Live session attach/detach
Interactive clients attach to an existing live session with a Harn-owned lifecycle contract. Hosted/cloud brokers can authenticate and route these calls, but the runtime owns the session semantics so local TUIs, portals, and remote clients do not invent incompatible takeover rules.
session/attach accepts:
{
"sessionId": "session_123",
"clientId": "portal",
"mode": "observer",
"takeover": false,
"_meta": {
"harn": {
"surface": "portal"
}
}
}
mode is observer or controller. A session may have many observers and at
most one controller. A second controller attach fails unless takeover: true is
set or the client calls session/takeover, which demotes the prior controller
to observer. session/detach removes the client and releases control when the
detaching client was the controller. session/heartbeat refreshes the client's
last-seen marker and optional metadata without changing ownership.
Responses share this shape:
{
"client": {
"client_id": "portal",
"mode": "observer",
"attached_at": "1780000000",
"last_seen_at": "1780000000",
"prompt_injection": false,
"permission_routing": false,
"metadata": {"surface": "portal"}
},
"previous_controller_id": null,
"active_controller_id": "tui",
"clients": []
}
Only the active controller may inject user prompts or route permission requests.
Controller prompt injection records the client id on the user message metadata.
Permission routing records a live_session_permission_route transcript event
containing the request id, controller client, and host metadata. Attach,
takeover, detach, and heartbeat record live_session_client transcript events
and are also emitted as session/update extensions using the same
_meta.harn namespacing rule as other Harn session-update extensions.
Unexpected client exit is represented as session/detach with
reason: "client_exit" by the host/broker that detects the disconnect. Reconnect
is a fresh session/attach using the same clientId; the runtime preserves the
transcript and recomputes ownership from the live-client registry.
Client-executed tool search
When a Harn script opts into tool_search against a provider that lacks
native defer-loading support, the runtime switches to a client-executed
fallback (see the LLM tools guide).
Built-in bm25, regex, and hybrid strategies stay in Harn/VM space. Hosts
that want embeddings, LLM reranking, project-specific indexes, or remote search
services should provide those through ordinary Harn callbacks, host-backed
tools, or MCP tools and pass the closure as tool_search.strategy. The bridge
observes only the resulting tool_search_query and tool_search_result
transcript events; there is no special tool_search/query RPC.
tool_names(required): ordered list of tool names to promote. Unknown names are ignored by the runtime — they can't be surfaced because their schemas weren't registered. Return at most ~20 names per call; the runtime caps promotions soft-per-turn regardless.diagnostic(optional): short explanation surfaced to the model in the tool result alongsidetool_names. Useful for "no hits, try broader terms"-style feedback.
An ACP-style wrapper { "result": { "tool_names": [...] } } is also
accepted for hosts that re-wrap everything in a result envelope.
Errors: a JSON-RPC error response (standard shape) is surfaced to the
model as a tool_names: [] result with a diagnostic that includes the
host error message. The loop continues — the model can retry with a
different query.
Host tool discovery
Hosts can expose their own dynamic tool surface to scripts without
pre-registering every tool in the initial prompt. Harn discovers that
surface through one bridge RPC and then invokes individual tools
through the existing builtin_call request path.
host/tools/list
VM-issued request. No parameters (or an empty object). The host responds with a list of tool descriptors. Canonical response shape:
{
"tools": [
{
"name": "Read",
"description": "Read a file from the active workspace",
"schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "File path to read"}
},
"required": ["path"]
},
"deprecated": false
},
{
"name": "open_file",
"description": "Reveal a file in the editor",
"schema": {
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
},
"deprecated": true
}
]
}
Accepted variants:
- a bare array
[{...}, {...}] - an ACP-style wrapper
{ "result": { "tools": [...] } } - compatibility field names
short_description,parameters, orinput_schema; Harn normalizes them todescriptionandschema
Each normalized descriptor surfaced to scripts has exactly these keys:
name: string, requireddescription: string, defaults to""schema: JSON Schema object ornulldeprecated: boolean, defaults tofalse
Invocation:
host_tool_list()returns the normalized list directly.host_tool_call(name, args)then dispatches that tool through the existingbuiltin_callbridge request usingnameas the builtin name andargsas the single argument payload.
Shell discovery host capability
Shell discovery is a typed process host capability so IDEs, TUIs,
headless CLI runs, and cloud workers can share one shell-selection
contract. Harn's standalone fallback exposes the same operations when no
bridge host is attached.
process.list_shells
Called through host_call("process.list_shells", {}). The response is:
{
"shells": [
{
"id": "zsh",
"label": "zsh",
"path": "/bin/zsh",
"platform": "darwin",
"available": true,
"supports_login": true,
"supports_interactive": true,
"default_args": ["-c"],
"login_args": ["-l", "-c"],
"source": "env"
}
],
"default_shell_id": "zsh"
}
Windows hosts should distinguish pwsh, powershell, and cmd.
cmd uses ["/C"]; PowerShell variants use
["-NoProfile", "-Command"].
process.get_default_shell / process.set_default_shell
process.get_default_shell returns the selected shell object for the
session. Stateful hosts may implement process.set_default_shell with
{ "shell_id": "zsh" }; hosts that keep persistence in editor settings
can omit persistence and still pass the selected shell on command
requests.
process.shell_invocation
process.shell_invocation resolves a discovered shell ID or shell object
into executable argv:
{
"shell_id": "zsh",
"command": "printf ok",
"login": false,
"interactive": false
}
Response:
{
"program": "/bin/zsh",
"args": ["-c", "printf ok"],
"command_arg_index": 1,
"shell": {"id": "zsh"}
}
Shell-mode command runners may pass a shell object or a shell ID
resolved through this capability, and otherwise use the selected default
shell. argv mode remains preferred for programmatic execution; shell
mode is for user-authored commands and interactive shell semantics. The
normative JSON Schema lives at
spec/schemas/host-shell-discovery.schema.json.
Skill registry
Hosts expose their own managed skill store to the VM through three RPCs.
Filesystem skill discovery works without the bridge (harn run walks
the seven non-host layers described in Skills); these
RPCs add a layer 8 so cloud hosts, enterprise deployments, and IDE
hosts can serve skills the filesystem can't see.
skills/list
VM-issued request. No parameters (or an empty object). The host
responds with an array of SkillManifestRef entries. Minimal shape:
[
{ "id": "deploy", "name": "deploy", "description": "Ship it", "source": "host" },
{ "id": "acme/ops/review", "name": "review", "description": "Code review", "source": "host" }
]
The VM also accepts { "skills": [ ... ] } for hosts that wrap
collections in an object.
skills/fetch
VM-issued request. Parameters: { "id": "<skill id>" }. Response is a
single skill object carrying enough metadata to populate a Skill:
{
"name": "deploy",
"description": "Ship it",
"body": "# Deploy runbook\n...",
"manifest": {
"when_to_use": "...",
"allowed_tools": ["bash", "git"],
"paths": ["infra/**"],
"model": "claude-opus-4-7"
}
}
Hosts may flatten the manifest fields into the top level instead — the CLI accepts either shape.
skills/update
Host-issued notification. No parameters. Invalidates the VM's cached
skill catalog; the CLI re-runs layered discovery (including another
skills/list call) on the next iteration boundary — for harn watch,
between file changes; for long-running agents, between turns. A VM
without an active bridge ignores the notification.
Host-delegated skill matching
Harn agents that opt into
skill_match: { strategy: "host" } (or the alias "embedding")
delegate skill ranking to the host via a single JSON-RPC request. The
host response is purely advisory — unknown skill names are ignored,
and an RPC error falls back to the in-VM metadata ranker with a
warning logged against agent.skill_match.
skill/match
Request payload (harn-issued, host response required):
{
"strategy": "host",
"prompt": "Ship the new release to production",
"working_files": ["infra/terraform/cluster.tf"],
"candidates": [
{
"name": "ship",
"description": "Ship a production release",
"when_to_use": "User says ship/release/deploy",
"paths": ["infra/**", "Dockerfile"]
},
{
"name": "review",
"description": "Review existing code for correctness",
"when_to_use": "User asks to review/audit",
"paths": []
}
]
}
Response payload (host-issued):
{
"matches": [
{"name": "ship", "score": 0.92, "reason": "matched by embedding similarity"}
]
}
matches[*].name(required): the candidate's skill name. Names absent from the originalcandidateslist are ignored.matches[*].score(optional): non-negative float; higher scores rank earlier. Defaults to1.0when omitted.matches[*].reason(optional): short diagnostic stored on theskill_matched/skill_activatedtranscript events. Defaults to"host match".
Alternative shapes accepted for host convenience:
- Top-level array:
[{"name": ..., "score": ...}, ...] {"skills": [...]}wrapping{"result": {"matches": [...]}}ACP envelope
Skill lifecycle session updates
Agents emit ACP session/update notifications for skill lifecycle
transitions so hosts can surface active-skill state in real time.
These are Harn extension variants advertised during initialize, not
upstream ACP SessionUpdate variants.
The packaged harn-serve ACP adapter translates the canonical AgentEvent
variants into:
sessionUpdate: "skill_activated"—{skillName, iteration, reason}sessionUpdate: "skill_deactivated"—{skillName, iteration}sessionUpdate: "skill_scope_tools"—{skillName, allowedTools}sessionUpdate: "skill_narrow"—{reason, removedTools, remainingTools, policy, removedToolDetails, keptToolDetails}
skill_matched stays internal to the VM transcript — the candidate
list can be large and host UIs typically only care about activation
transitions, not every ranking pass.