Documentation
¶
Overview ¶
agent.go defines the Agent facade: the struct that binds Options to real ACP wire methods over a *protocol.Conn (see conn.go's Handle/HandleNotify). This file wires only initialize, authenticate, and logout — the methods whose behavior does not depend on a live session (session/new and everything after it are later tasks; see the phase plan in harness/docs/plans/2026-07-23-acp-bridge-implementation.md).
capabilities.go computes the AgentCapabilities advertised in the initialize response from an Agent's Options: the capability advertisement matrix described in Task 2.2 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md.
close.go implements the session/close orchestration state machine: Task 2.7 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md.
Per the design doc's "Cancellation and close" section, session/close is "an orchestrated lifecycle operation, not a direct registry delete":
- Mark the ACP session closing and reject new prompts (promptTracker. markClosing, reused from prompt.go's begin/end machinery — Task 2.4).
- Cancel in-flight work with behavior equivalent to session/cancel (LiveSession.Interrupt — the same call handleSessionCancel already makes; not reimplemented here).
- Resolve outstanding permission requests owned by the connection (gateTracker.CancelSession, Task 2.6's own integration point: it unblocks any pending client.RequestPermission call with ctx.Err(), which drainToTerminal's failGateClosed path already turns into RespondGate(Deny) automatically — no separate deny-delivery code is needed here).
- Wait for the in-flight prompt (if any) to actually finish draining — not fire-and-forget: the channel promptTracker.markClosing returns closes only once the drained handlePrompt call has returned.
- Call the optional SessionCloser.Shutdown capability, bounded by closeShutdownGrace.
- Remove the session from the live registry — only now, after every step above has completed.
Durable history is never touched here: SessionDeleter (session/delete) is a completely separate optional capability this handler never calls.
compact.go implements the `/compact` internal slash command and its available_commands_update advertisement: Task 4.2 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md.
`/compact` is the one and only internal Harness command this facade ever exposes through session/prompt (see the design doc's "Slash commands and compaction": "No other internal Harness command is automatically exposed."). It exists only when Options.Compactor is configured (host.go); handlePrompt (prompt.go) checks isCompactSlashCommand on every incoming prompt and, only when both conditions hold, routes to handleCompactPrompt below instead of the ordinary Submit path — any other slash-prefixed (or plain) text, or "/compact" itself when no Compactor is configured, falls through unchanged to blocksFromPrompt/Submit exactly as before this task.
Correlating a compaction command's outcome differs from Task 2.4's turn correlation (prompt.go's drainToTerminal). A submitted turn only learns its LoopID/TurnID from an intervening TurnStarted event, so drainToTerminal needs two phases: correlate the loop/turn first, then match every subsequent event against it. A compaction attempt's own terminal events — CompactWaiterResolved and CompactWaiterRejected (harness/pkg/event/compaction.go) — already carry Header.Cause.CommandID directly (they are Reply events: see event.Reply and event.CompactWaiterReplyID), so a single phase suffices: drainCompactionToTerminal matches the submitted command id straight off each event it observes, with no intermediate "started" event to wait for.
available_commands_update is advertised lazily: the first time handlePrompt runs for a session (see ensureAvailableCommandsAdvertised), not eagerly at session/new/session/load/session/resume. This is a deliberate choice, not an oversight: session/resume's own documented contract is to send zero session/update notifications at all (see resume.go's package doc, an already-tested invariant), so hooking advertisement into session establishment would either have to special-case resume out again or silently break that invariant. Tying it to the first prompt instead reaches every session-establishment path uniformly, without touching session.go/resume.go/replay.go at all.
Options.Compactor is a connection-level field, set once before any session exists, so it answers exactly one question: whether compaction is available at all, for the sole purpose of deciding whether to advertise and route `/compact` (above, and ensureAvailableCommandsAdvertised below). It is NEVER invoked to actually perform a compaction — a single connection-wide field cannot tell two different sessions' compactions apart, so the actual Compactor for a given `/compact` call is always resolved from that SPECIFIC session's live value instead (live.(Compactor) in handleCompactPrompt), the same per-session type-assertion pattern SessionCloser already uses (host.go's Compactor doc; close.go's live.(SessionCloser)).
config.go implements the session/set_config_option and session/set_mode handlers: Task 4.1 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md, the first task of Phase 4.
Both wire methods run through the single unexported applyConfigOption: handleSessionSetConfigOption calls it with the request's own configId/ value, and handleSessionSetMode calls it with the well-known ModeConfigOptionID (host.go) and the requested mode id reinterpreted as a SessionConfigValueID. There is no second, independent "apply a mode change" implementation anywhere in this package — this is what keeps the two wire methods convergent rather than two behaviors a future change could accidentally let drift apart.
applyConfigOption itself:
- Fetches the LATEST RuntimeConfigCatalog snapshot for this request — never one cached from session/new, an earlier request, or anywhere else. All external input (configId/value) is untrusted, so it is validated against what is true right now.
- Rejects an unknown configId or an unknown value for that configId (InvalidParams), fail closed, before the RuntimeConfigController is ever consulted.
- Short-circuits to a no-op success when the requested value already equals the option's current value: config writes are idempotent (see RuntimeConfigController's doc in host.go), and this facade enforces that itself rather than trusting every controller implementation to — the controller is not called, and no notification is sent.
- Otherwise calls RuntimeConfigController.SetRuntimeConfigOption, then sends exactly one config_option_update session/update notification carrying the complete resulting option state, before returning that same state to the caller.
initialConfigState is the second entry point into this file's translation logic: session.go's handleSessionNew, replay.go's handleSessionLoad, and resume.go's handleSessionResume all call it to populate their response's ConfigOptions/Modes fields with the newly-established session's current RuntimeConfigCatalog snapshot, reusing translateRuntimeConfigOptions (the exact same helper applyConfigOption's own response uses) rather than a second, independent translation.
delete.go implements the session/delete handler: Task 3.4 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md, the final task of Phase 3.
session/delete is the deliberate converse of close.go's own invariant. close.go documents that "durable history is never touched" by session/close -- SessionDeleter is a completely separate optional capability that handler never calls. This file is the mirror image: it is the ONLY path in this package that may ever invoke SessionDeleter, and only for a sessionId that does NOT currently name a live, registered session (a.sessions.get, registry.go). Attempting to delete a session while it is still live is rejected outright, before the Deleter is ever consulted: durable history must never be deleted out from under a live session, so a client must session/close it first (see the design doc's "Cancellation and close": "Delete remains separate and is advertised only when a host supplies explicit storage and authorization semantics").
Wire error for "session still active" ¶
The pinned schema (protocol/schema/v1/schema.json) defines DeleteSessionRequest as exactly {sessionId, _meta} and DeleteSessionResponse as exactly {_meta} -- no dedicated field or error code anywhere in the schema names "the session is still active" as a distinguished condition, unlike (for example) ErrorCodeResourceNotFound or ErrorCodeAuthenticationRequired, which the schema's ErrorCode $def does single out. Absent a schema-documented specific code, this handler reports the rejection as protocol.InvalidRequest (-32600), reusing the exact precedent prompt.go's ErrSessionClosing and ErrPromptAlreadyInFlight already set for an analogous "invalid given current session state" condition (see prompt.go's handlePrompt) -- not a guessed or novel mapping.
gates.go implements the ACP permission-gate bridge: Task 2.6 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md.
prompt.go's drainToTerminal (Task 2.4) correlates a session/prompt to its exact turn and drains that turn's event stream to its terminal, forwarding every translatable Public event along the way as a session/update notification (Task 2.5). A turn can also PARK on an open gate mid-drain: a tool call awaiting human approval (gate.KindPermission) or an explicit user question (gate.KindAskUser) blocks the loop until it is answered. This file adds the handling drainToTerminal needs for that: when it observes a correlated, Public event.GateOpened for a translatable gate, it issues a session/request_permission call on the client connection, waits for the client's chosen option, validates that option was genuinely one that was offered, and answers the durable gate via LiveSession.RespondGate with the matching gate.GateResponse — all before drainToTerminal continues draining toward the turn's eventual terminal.
Only gate.KindPermission is translated ¶
The design doc ("Permissions and host-owned gates") describes the flow as applying to "Harness permission and ask-user gates" generically. Reading the real gate contract (harness/pkg/gate) shows the two kinds are not actually symmetric for this purpose:
gate.KindPermission's answer is exactly one of the three gate.ApprovalAction strings (Approve / Approve always for this workspace / Deny — see gate.ApprovalControls, which documents these as the gate's "exact, complete control set"). That is precisely what ACP's session/request_permission was designed to carry: a fixed, closed set of named options the user picks exactly one of, tagged with a PermissionOptionKind hint whose four values (allow_once/ allow_always/reject_once/reject_always) already have obvious, non-guessed counterparts for the three approval actions.
gate.KindAskUser's answer is NOT an action pick: internal/loopruntime's translateAskUserResponse (sessionruntime/gates.go) reads the answer from response.Values["answer"] — arbitrary free text, or one value from a bounded schema.Field's Options when the question offered fixed choices (loopruntime's askUserFields). RequestPermissionResponse's Outcome, however, carries only a discriminated Cancelled-or-Selected choice with an OptionID — there is no field anywhere on the ACP outcome for arbitrary answer text. Even in the bounded-choices case, ACP's PermissionOptionKind is a closed, approve/deny-flavored enum (see types_gen.go's UnmarshalJSON, which rejects anything else): there is no non-fabricated kind that represents "the user picked one of N arbitrary named choices" without guessing an approve/deny semantic that the actual question never had (a "which color?" question tagged allow_once/reject_once would misrepresent the choice to a client rendering approval icons around it).
This is the same category of gap already documented for elicitation in Task 1.7/1.8 (protocol/acp.go and internal/mockpeer/main.go): the pinned v1.20.0 schema has no wire shape that can carry this gate kind's answer faithfully, so — per the plan's own "pinned artifact wins" precedence rule — it is intentionally left untranslated here rather than inventing one. An ask-user GateOpened observed mid-drain therefore falls through exactly like any other untranslatable progress event (see drainToTerminal): the drain continues silently, and the gate stays open until something else (a future ACP capability, or a product-level ask-user answer path outside this facade) resolves it.
Host-owned gates (form, open-url) are never exposed here either ¶
gate.KindForm and gate.KindOpenURL are host-owned (gate.ResolverSession, never gate.ResolverLoop — see sessionruntime's hostOwnedGate), answered through session.GateHost's own AwaitGateAnswer path, not through a loop's RespondGate. This file's translation is scoped to gate.KindPermission with gate.ResolverLoop specifically (permissionOptionsFromGate checks both), so a host-owned gate is never even considered for request_permission — it is structurally excluded, not filtered out by a capability check. This matters because the design doc's own aspiration here ("session/elicitation ... when the connected client advertises elicitation") is doubly unrepresentable: the pinned schema has no elicitation method at all (confirmed absent in Task 1.7/1.8: no Elicit on ClientConn, no elicit entry in methods_gen.go) AND no client capability field for it either (protocol.ClientCapabilities has exactly Fs/Session/Terminal — see types_gen.go — nothing resembling elicitation or an open-URL interaction). There is therefore no capability to negotiate and nothing to gate a matrix test on beyond what gates_test.go already asserts: host-owned gate kinds are never flattened into request_permission, full stop, regardless of what a client advertises.
Package agent is the Looprig-facing ACP agent facade: the only package in this module that may import Harness's or Core's public packages (see acp/CLAUDE.md and harness/docs/plans/2026-07-17-acp-bridge-design.md, "Agent-side host boundary").
ACP setup cannot depend directly on serve.Rig: a Harness rig's option type is opaque to ACP, workspace placement is fixed when a rig is defined, and ACP setup carries product concerns (cwd, MCP servers, replay, catalogs, runtime configuration) that Harness itself does not know about. This file therefore defines small, consumer-owned host interfaces that the product composition root implements; the facade does not touch rig.SessionOption or workspace placement directly.
list.go implements the session/list handler and the session_info_update observation callback: Task 3.3 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md, amended by a follow-up fix to how session/list reports cwd (see this file's "cwd resolution" section below).
The CurrentWorkspace/cwd discrepancy ¶
This task's original design doc ("Session listing and metadata") said SessionMeta.CurrentWorkspace maps to ACP's cwd. It does not: reading the REAL harness/pkg/sessionstore/catalog.go, SessionMeta.CurrentWorkspace is a WorkspacePointer — Ref (a content-addressed workspace-SNAPSHOT digest, "v1:sha256:<64 hex>"), EventID, Seq, and Source (checkpoint vs restore). That identifies which immutable snapshot the session's workspace was last pointed at, not a live filesystem directory path; SessionMeta carries no field anywhere that represents a session's working-directory string at all. Mapping WorkspacePointer.Ref into ACP's Cwd string would be actively wrong (a client would try to treat a content hash as a path). The design doc has since been corrected to say this plainly (see harness/docs/plans/2026-07-17-acp-bridge-design.md, "Session listing and metadata"); a durable per-session cwd field on SessionMeta itself is a legitimate future Harness-side follow-up, out of scope here (see SessionCatalogEntry's doc in host.go).
cwd resolution ¶
The first version of this handler worked around the gap above by emitting SessionInfo.Cwd = "" for every entry. That is schema-non-conformant: the pinned ACP schema requires SessionInfo.cwd to be a non-empty absolute path, so a strict client could reject the whole session/list response over one malformed row. This version instead resolves each catalog entry's cwd through resolvedCwd and, when no cwd can be determined, OMITS that session from the response entirely — an incomplete-but-schema-valid listing beats a complete-but-invalid one. resolvedCwd consults two sources, in order:
- This facade's own live session registry (sessions.cwd): if the session id is currently live/registered, its Setup.Cwd — already validated and canonicalized at session establishment (session/new, session/load, and session/resume all construct it via NewSetup) — is authoritative, unconditionally overriding whatever (if anything) the catalog itself reports for that same id. A live session is therefore always conformant in session/list, even before any product adapter learns to answer cwd for cold/non-live sessions.
- The catalog entry's own SessionCatalogEntry.Cwd (host.go), for a session that is not currently live. A host that knows a cold session's cwd may supply it there; one that does not leaves it empty.
A session for which neither source yields a cwd is omitted, never emitted with an empty or fabricated cwd.
Pagination under cwd omission ¶
Omission happens AFTER pagination, not before: the facade still paginates over the catalog's full sorted entry set (sortedSessionMetas, paginateSessionMetas) exactly as it did before this fix, bounding each page at MaxPageSize entries and advancing the cursor to the last entry's SessionID regardless of whether that entry's cwd was resolvable. Only then, for the entries within that already-computed page, does handleSessionList drop the unresolvable-cwd ones from the response's Sessions list. This means an omitted session still "consumes" a page slot — a page can legitimately come back with fewer than MaxPageSize entries (even zero) while NextCursor is still non-nil, if enough sessions in that slice have no resolvable cwd.
This is a deliberate choice over the alternative (filtering out unresolvable-cwd entries BEFORE pagination, so every non-final page is guaranteed exactly MaxPageSize returned entries): filtering first would require re-deriving what a "page" means — either paginating over a separately-materialized filtered-and-sorted slice (an extra full pass and a second sorted view to keep consistent with the unfiltered one), or scanning ahead an unbounded distance through the catalog to accumulate MaxPageSize survivors, which turns a fixed-cost page fetch into a variable, input-dependent one. Pagination position (the cursor) staying anchored to the same catalog-wide sorted-by-SessionID key regardless of cwd resolvability is also simpler to reason about and to test: the cursor always means "resume strictly after this SessionID in the full catalog," never "resume after the Nth *cwd-resolvable* entry," a definition that would shift under a client every time the underlying cwd knowledge changed. The tradeoff this accepts is exactly what the doc above says: a client may see a short (or empty) page before NextCursor goes nil; it must keep following NextCursor to reach the end, exactly as it already must for an ordinary short-vs-full page.
The facade paginates over the catalog's SessionCatalogEntry entries sorted ascending by their Meta.SessionID bytes (sortedSessionMetas) — a stable, catalog-independent ordering that does not depend on ListSessions' own return order. A cursor is an opaque, HMAC-authenticated token naming the last SessionID included in the previous page (cursorPayload); it is validated (decodeListCursor) before ever being used to compute a page, and any structural or authentication failure is a typed *InvalidCursorError, mapped to InvalidParams — never silently treated as "start over" or accepted as some other position. The HMAC key (Agent.cursorKey) is generated fresh via crypto/rand at New() time, once per Agent instance: cursors are short-lived pagination tokens a client is expected to consume within one process's lifetime (they are never persisted or expected to survive a restart), so there is no reason to derive the key from anything stable across restarts, and every reason not to use a fixed or caller-influenced key.
session_info_update ¶
host.go's SessionCatalog interface exposes only a pull method (ListSessions): there is nothing already there a product could register a push callback against, and the direction of this signal is product-into- facade (the product's own catalog-observation mechanism learns of a change and tells the facade), not facade-into-product like every other host.go seam. ObserveSessionMeta is the narrow, facade-owned callback surface that fills that gap: a product calls it once per catalog observation (a KV watch, an event-driven fold — never a poll loop this facade runs itself), and it is the facade's OWN job — not the caller's — to decide whether that observation actually changed anything worth telling the client about (see ObserveSessionMeta's doc).
prompt.go implements the session/prompt correlation engine and the session/cancel handler: Task 2.4 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md.
ACP's session/prompt is a request whose response completes only at a turn terminal, but Harness's Submit is fire-and-forget: it returns a command id and the turn's outcome arrives later, asynchronously, on the session's event stream. Bridging the two requires the two-phase correlation rule from the design doc ("Prompt correlation and event translation"):
- Subscribe before submitting (see handlePrompt: SubscribeEvents is called, and its error path returned, strictly before Submit is ever called — a TurnStarted racing in immediately after Submit returns can therefore never be missed).
- Match TurnStarted.Header.Cause.CommandID to the submitted command id.
- Capture that event's LoopID and TurnID (Header.Coordinates).
- Match every following event using both captured identifiers, ignoring everything else (interleaved activity from other loops, other turns, or other prompts' TurnStarted events) as a decoy.
- Complete the ACP response only on the correlated TurnDone, TurnFailed, or TurnInterrupted.
registry.go is the bounded, concurrency-safe registry of live ACP sessions the facade tracks between session/new and each later session-scoped method (Task 2.3 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md).
replay.go implements the replay translator for session/load: Task 3.1 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md.
Live ephemeral token and tool-progress events are not durable (see translate.go's package doc and event.go's Class doc: only Enduring events are ever journaled). session/load therefore cannot replay the live token stream at all — TokenDelta, ToolCallStarted, and ToolCallCompleted are all Ephemeral (see event.go's TokenDelta doc and event/tool.go's ToolCallStarted/ToolCallCompleted docs) and never appear in durable history. Instead this file reconstructs, from the session's Enduring event history alone:
- user messages, from TurnStarted.Message;
- assistant messages and completed tool calls, from StepDone.Messages (the step's single *content.AIMessage followed by its *content.ToolResultMessages — see event.go's StepDone doc); and
- the session's current context-window usage, from the LAST ContextMeasured seen (reusing translate.go's translateContextMeasurement directly).
This grouped, four-bucket order — every user message, then every assistant message, then every completed tool call, then one final metadata update — is the exact shape the design doc's "Load replay versus live streaming" section and this task describe, not an interleaved chronological replay: it deliberately does not attempt to reproduce per-turn interleaving.
TurnDone/TurnFailed/TurnInterrupted are turn-boundary markers only and never separately translated: TurnDone.Message is the concatenation of content already reconstructed from this turn's StepDone events, so translating it too would duplicate client-visible content (see the no-duplication property this task requires; translate.go's live translator applies the identical "drop, don't guess" rule to TurnDone for the same reason).
resume.go implements the session/resume handler: Task 3.2 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md.
Unlike session/load (replay.go), Host.ResumeSession returns a plain LiveSession, not a LoadedSession (see host.go's SessionHost doc): there is no replay anchor and therefore no durable-history reconstruction to perform. The pinned schema documents session/resume as resuming a session "without returning previous messages (unlike session/load) ... for agents that can resume sessions but don't implement full session loading." This handler accordingly never calls a.client.SessionUpdate at all — the exact property this file's test proves — making it the simplest of the three session-establishment handlers: validate Setup, call Host.ResumeSession, register the result, and respond immediately.
session.go implements ACP session identity mapping, the reusable session-scoped resolution helper, and the session/new handler: Task 2.3 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md.
Every session-scoped ACP method beyond session/new (session/prompt, the permission gates, session/cancel, session/close, and Phase 3's load/resume/list/delete) must resolve its wire sessionId through resolveSession before the id touches the host or any session state: resolveSession validates the string first (ParseSessionID) and only then consults the live-session registry, so a malformed id is rejected before a lookup — let alone a host call — is ever attempted.
translate.go implements the live event translator: Task 2.5 of harness/docs/plans/2026-07-23-acp-bridge-implementation.md.
prompt.go's drainToTerminal (Task 2.4) already correlates a session/prompt to its exact turn (LoopID/TurnID) and drains that turn's event stream to its terminal. Until now it silently discarded every non-terminal event it saw along the way ("progress event ...: not a terminal"). This file adds the translator drainToTerminal uses to turn each of those in-flight, public events into the ACP session/update notification it forwards to the client, so a prompt's progress is actually streamed live rather than only observed internally while the client waits on the terminal response.
Harness events carry two orthogonal classifications this translator must respect: Class (Ephemeral/Enduring — durability, irrelevant here) and EventVisibility (Public/Internal). Only Public events are ever translated; this is a hard security boundary, not a convenience filter, so it is checked directly by Translate rather than trusted solely to the Harness hub's own delivery filter (see event.ShouldDeliver) upstream.
Index ¶
- Constants
- Variables
- type Agent
- type Authenticator
- type Compactor
- type CursorErrorReason
- type CwdError
- type CwdErrorReason
- type EventReplayer
- type InvalidCursorError
- type LiveSession
- type LoadedSession
- type LogoutHandler
- type MCPNotAcceptedError
- type Options
- type RuntimeConfigCatalog
- type RuntimeConfigChange
- type RuntimeConfigController
- type RuntimeConfigOption
- type RuntimeConfigValue
- type SessionCatalog
- type SessionCatalogEntry
- type SessionCloser
- type SessionDeleter
- type SessionHost
- type SessionID
- type SessionIDError
- type SessionIDReason
- type SessionMetaObservationError
- type Setup
- type TooManyLiveSessionsError
- type UnofferedPermissionOptionError
- type UnsupportedContentBlockError
Constants ¶
const MaxLiveSessions = 64
MaxLiveSessions bounds how many ACP sessions this facade tracks concurrently as live, registered sessions. session/new fails closed with a *TooManyLiveSessionsError once the registry is already at this capacity, rather than growing without bound.
const MaxPageSize = 100
MaxPageSize bounds the number of SessionInfo entries session/list returns in one response. The facade owns this bound unconditionally: the pinned ListSessionsRequest schema carries no client-supplied page-size field, so every page is exactly MaxPageSize entries, or fewer only on the final page.
const ModeConfigOptionID protocol.SessionConfigID = "mode"
ModeConfigOptionID is the well-known RuntimeConfigOption.ID a RuntimeConfigCatalog/RuntimeConfigController implementation MUST use for the session-mode option (Category protocol.SessionConfigOptionCategoryMode, Values/CurrentValue sourced from loop.ModeCatalog.Modes() — see RuntimeConfigOption's doc). This is what keeps session/set_mode and session/set_config_option convergent: the pinned schema's SetSessionModeRequest carries only a bare SessionModeID, no configId, so config.go's handleSessionSetMode always targets this constant, translating the request into exactly the same call handleSessionSetConfigOption would make for configId=ModeConfigOptionID — both paths run through the single unexported applyConfigOption (config.go), never two independent implementations that could drift.
Variables ¶
var ErrAgentNotRegistered = errors.New("agent: Register has not been called yet")
ErrAgentNotRegistered reports that ObserveSessionMeta was called before Register bound the facade to a live *protocol.Conn. There is no client to notify yet, so this fails closed rather than silently dropping the observation.
var ErrAuthenticatorWithoutMethods = errors.New("agent: Authenticator supplied without any AuthMethods to advertise")
ErrAuthenticatorWithoutMethods reports that Options supplied an Authenticator but no AuthMethods to advertise it under. Advertising the authenticate capability with no selectable method id can never succeed from a client's perspective, so New fails closed at construction rather than accepting a configuration that could never work.
var ErrCompactSubscriptionClosed = errors.New("agent: event subscription closed before compaction outcome")
ErrCompactSubscriptionClosed is the local cause used when the event subscription backing a `/compact` correlation closes before the compaction's outcome (CompactWaiterResolved/CompactWaiterRejected) is observed. Mirrors prompt.go's ErrSubscriptionClosed for the turn-terminal case: subscription loss becomes a typed failure, never a silent success. It never crosses the wire itself (only Message/Code/Data do — see protocol.Fault); it exists so local callers can errors.Is/As it.
var ErrCompactorNotImplemented = errors.New("agent: session does not implement Compactor")
ErrCompactorNotImplemented is the local cause used when a session's LiveSession value does not implement Compactor, even though Options.Compactor is configured at the connection level. Options.Compactor only gates whether `/compact` is advertised/routed at all (see host.go's Compactor doc and Options.Compactor's own doc, agent.go) — it is never the thing actually invoked, so it cannot substitute for a session whose own live value does not support compaction. A correctly implemented SessionHost should never produce this (every live session it hands back should implement Compactor exactly when compaction is generally available), but handleCompactPrompt still fails closed here rather than panicking or silently no-op-succeeding if it ever does. Never crosses the wire itself; exists so local callers can errors.Is/As it.
var ErrMissingHost = errors.New("agent: Options.Host is required")
ErrMissingHost reports that Options did not supply a SessionHost, the one field every facade requires.
var ErrPromptAlreadyInFlight = errors.New("agent: a session/prompt is already in flight for this session")
ErrPromptAlreadyInFlight is the local cause behind the *protocol.Fault returned when a second session/prompt is attempted on a session that already has one in flight. Per the design doc: "At most one prompt per ACP session is in flight at a time: a second concurrent session/prompt on the same session is rejected... never queued behind or interleaved with the running one." It never crosses the wire itself (only Message/Code/Data do — see protocol.Fault); it exists so local callers can errors.Is/As it.
var ErrSessionClosing = errors.New("agent: session is closing")
ErrSessionClosing is the local cause behind the *protocol.Fault returned when a session/prompt is attempted on a session that close.go's handleSessionClose has already marked closing (see promptTracker.markClosing). It is a distinct sentinel from ErrPromptAlreadyInFlight — both reject the same way (InvalidRequest), but a caller inspecting the cause via errors.Is can tell "busy" apart from "gone" — it never crosses the wire itself (only Message/Code/Data do — see protocol.Fault).
var ErrSessionStillLive = errors.New("agent: session is still live; close it before deleting")
ErrSessionStillLive is the local cause behind the *protocol.Fault returned when session/delete is attempted on a sessionId that currently names a live, registered session (see this file's package doc). It never crosses the wire itself (only Message/Code/Data do -- see protocol.Fault); it exists so a local caller can errors.Is/As it, matching prompt.go's ErrSessionClosing/ErrPromptAlreadyInFlight sentinels.
var ErrSubscriptionClosed = errors.New("agent: event subscription closed before turn terminal")
ErrSubscriptionClosed is the local cause used when the event subscription backing a session/prompt's correlation closes before the correlated turn's terminal is observed. Per the design doc: "Subscription loss before a terminal becomes a typed prompt failure rather than a successful empty answer" — this is never reported as if the prompt quietly produced no content.
var ErrUnknownConfigOption = errors.New("agent: config option: unknown configId")
ErrUnknownConfigOption is the local cause applyConfigOption attaches to the InvalidParams fault it returns when optionID names no option in the latest RuntimeConfigCatalog snapshot. It never crosses the wire itself (only Message/Code/Data do — see protocol.Fault) but lets a caller use errors.Is to distinguish this SPECIFIC condition from any other InvalidParams fault. handleSessionSetMode uses it this way: ModeConfigOptionID (host.go) is a package constant, never client-supplied, so if applyConfigOption ever fails with ErrUnknownConfigOption on that call path, the only possible cause is a misconfigured RuntimeConfigCatalog that omits the well-known "mode" option entirely — a host bug, not a client mistake — and handleSessionSetMode reports that as a distinct, louder diagnostic instead of silently reusing the ordinary "unknown configId" a real client would get for an arbitrary bad session/set_config_option request.
Functions ¶
This section is empty.
Types ¶
type Agent ¶
type Agent struct {
// contains filtered or unexported fields
}
Agent is the ACP-facing facade: it registers ACP wire handlers on a *protocol.Conn (via Register) and consults Options to decide, per the pinned schema, which optional capabilities to advertise and allow.
Its zero value is not usable; construct with New.
func New ¶
New validates opts and constructs the facade.
authenticated state starts unlocked when no Authenticator is configured (nothing ever gates on it), and locked when one is configured, matching the pinned schema's authenticate flow: "called when the agent requires authentication before allowing session creation."
func (*Agent) AuthorizeSessionCreation ¶
AuthorizeSessionCreation reports whether session-creation methods (session/new, session/load, session/resume — wired starting Task 2.3) may proceed right now. Per the pinned schema's authenticate/logout flow — session/new's own doc says it "may return an auth_required error if the agent requires authentication," and logout's says "after a successful logout, all new sessions will require authentication" — this returns nil when no Authenticator is configured (authentication is never required) or once Authenticate has since succeeded, and a *protocol.Fault with ErrorCodeAuthenticationRequired otherwise. Callers implementing those methods must consult this before touching the host.
func (*Agent) ObserveSessionMeta ¶
func (a *Agent) ObserveSessionMeta(ctx context.Context, meta sessionstore.SessionMeta) error
ObserveSessionMeta is the narrow callback surface a SessionCatalog-owning product calls into whenever its OWN catalog-observation mechanism (a KV watch, an event-driven fold, or any other push-based signal — never a poll loop this facade runs) sees a session's catalog entry. It is the "product observation callback" this task's design calls for (see this file's package doc for why it lives here rather than as a new host.go interface).
It compares meta's Title and LastActiveAt against the last values this method sent for meta.SessionID (an implicit zero baseline — empty title, zero time — for a session never observed before) and sends exactly one session_info_update over session/update if, and only if, at least one differs; the baseline is then updated to match. A product that calls this once per catalog write — even for a session whose title/activity did not actually change, or on its very first, still-uninitialized entry — never produces needless client-visible notification traffic as a result: this is what makes emission event-driven (proportional to actual catalog change) rather than "notify on every call."
meta.SessionID must be non-zero (fails closed with a typed *SessionMetaObservationError otherwise); Register must have already run (fails closed with ErrAgentNotRegistered otherwise, since there is no client connection yet to notify).
func (*Agent) Register ¶
Register binds the facade's currently implemented handlers onto conn: initialize, session/new, session/prompt, session/cancel, session/close, and session/resume unconditionally (every product-facing agent needs Options.Host, and SessionHost.ResumeSession is a required method with no independent Options gate of its own — see host.go's SessionHost doc — so none of these have a capability gate; each of session/new, session/prompt, session/close, and session/resume consults AuthorizeSessionCreation/resolveSession internally), authenticate/logout only when their backing Options field is supplied, session/load only when Options.Replayer is supplied, session/list only when Options.Catalog is supplied, session/delete only when Options.Deleter is supplied, and session/set_config_option/session/set_mode only when BOTH Options.ConfigCatalog and Options.ConfigController are supplied (see config.go: validating a change needs a live catalog, applying one needs a controller, so either alone leaves both methods unregistered) — matching capabilities.go's LoadSession/SessionCapabilities.List/ SessionCapabilities.Delete advertisement gates: a client is never told a capability is supported yet has the method rejected, and never told it is unsupported yet has it accepted. Every other ACP method (later Phase 4 tasks) is intentionally left unregistered here — Conn's own method-not-found fallback rejects them (see conn.go's dispatchRequest) until a later task wires them up, which is exactly the fail-closed behavior an unadvertised capability must have.
type Authenticator ¶
type Authenticator interface {
Authenticate(context.Context, protocol.AuthMethodID) error
}
Authenticator is the optional capability to complete an ACP authenticate call for the method the client selected. The facade validates methodID against the advertised authMethods before calling this.
type Compactor ¶
Compactor is the optional capability to trigger the session's focused/ active-loop compaction, matching Harness's session.Session.Compact. It returns the command id used to correlate the compaction outcome exactly like Submit; the ACP prompt handler completes only once that outcome is observed (see design doc "Slash commands and compaction").
Resolution is per-session, never connection-wide: compact.go's handleCompactPrompt type-asserts the SPECIFIC session's LiveSession value against this interface (live.(Compactor)) to find the Compactor to actually call — exactly the same pattern SessionCloser uses via live.(SessionCloser) in close.go/replay.go. Options.Compactor (agent.go) is a DIFFERENT, connection-level field of this same interface type; it is set once, before any session exists, and exists solely to answer the one question that has to be decided at that point — whether `/compact` should be advertised (and routed) at all (ensureAvailableCommandsAdvertised, compact.go). Options.Compactor is never itself invoked to perform a compaction: a single connection-wide field cannot distinguish which of several concurrent sessions on the same Agent a `/compact` request belongs to, so only the per-session live.(Compactor) resolution above is ever actually called. Do not reintroduce a direct call through Options.Compactor.Compact.
type CursorErrorReason ¶
type CursorErrorReason string
CursorErrorReason classifies why a wire session/list cursor failed validation.
const ( // CursorReasonMalformed: the cursor string was not the "<payload>.<tag>" // shape at all, or either segment was not valid base64url. CursorReasonMalformed CursorErrorReason = "malformed" // CursorReasonTampered: the cursor decoded structurally, but its tag does // not authenticate against this Agent's cursor key — the payload was // altered, forged, or produced by a different Agent instance/key. CursorReasonTampered CursorErrorReason = "tampered" // CursorReasonInvalidPayload: the tag authenticated, but the payload // bytes are not a valid cursorPayload (bad JSON, unknown field, trailing // data, or an After value that does not parse as a session id). CursorReasonInvalidPayload CursorErrorReason = "invalid_payload" )
type CwdError ¶
type CwdError struct {
Cwd string
Reason CwdErrorReason
}
CwdError reports that a candidate cwd failed canonical-absolute-path validation. All external input is untrusted, so NewSetup fails closed rather than silently canonicalizing a malformed path on the caller's behalf.
type CwdErrorReason ¶
type CwdErrorReason string
CwdErrorReason classifies why a candidate cwd was rejected.
const ( // CwdReasonEmpty: cwd was the empty string. CwdReasonEmpty CwdErrorReason = "empty" // CwdReasonNotAbsolute: cwd was not an absolute path. CwdReasonNotAbsolute CwdErrorReason = "not_absolute" // CwdReasonTraversal: cwd contained a ".." path segment. CwdReasonTraversal CwdErrorReason = "traversal" // CwdReasonNotCanonical: cwd was absolute and traversal-free but not // already in filepath.Clean canonical form (e.g. a doubled separator, a // "." segment, or a trailing separator). CwdReasonNotCanonical CwdErrorReason = "not_canonical" )
type EventReplayer ¶
type EventReplayer interface {
OpenEventReplayer(SessionID) (journal.EventReplayer, error)
}
EventReplayer is the optional capability to open a public-only durable event replayer for session/load. Its natural Harness realization is sessionstore.Store.OpenEventReplayer — never the privileged OpenInternalEventReplayer/OpenInternalRecordReplayer variants, which must never be wired into the ACP path (see design doc "Load replay versus live streaming"). Task 3.1 refines the request shape if replay needs more than a session identity.
type InvalidCursorError ¶
type InvalidCursorError struct {
Reason CursorErrorReason
// contains filtered or unexported fields
}
InvalidCursorError reports that a wire session/list cursor string failed validation before ever being used to compute a page. All three CursorErrorReason cases fail exactly the same way from the caller's perspective: session/list rejects the request outright rather than falling back to "start from the beginning" (which would silently and incorrectly reorder a client's in-progress pagination) or guessing at some other position.
func (*InvalidCursorError) Error ¶
func (e *InvalidCursorError) Error() string
func (*InvalidCursorError) Unwrap ¶
func (e *InvalidCursorError) Unwrap() error
type LiveSession ¶
type LiveSession interface {
SessionID() uuid.UUID
Submit(context.Context, []content.Block) (uuid.UUID, error)
SubscribeEvents(event.EventFilter) (event.Subscription, error)
RespondGate(context.Context, gate.GateResponse) error
Interrupt(context.Context) (bool, error)
}
LiveSession is the narrow data plane the ACP prompt/gate/interrupt handlers need. A harness-backed host satisfies it with session.Session (SessionID, Submit, SubscribeEvents, RespondGate, Interrupt) — a strict subset of that interface's full method set, since session.Session also carries loop-addressed and compaction methods (ActiveLoop, Loop, SubmitToLoop, Compact, CompactToLoop) that ACP's data plane does not need directly; Compact is exposed separately as the optional Compactor capability, resolved per-session via a live.(Compactor) type-assertion on this specific value (see Compactor's own doc below) — never through Options.Compactor directly.
Wire-exposure trust boundary: same rule as SessionHost's doc comment above. An error Submit, SubscribeEvents, or RespondGate returns is folded into a *protocol.Fault's Message field essentially verbatim (see prompt.go's handlePrompt/drainToTerminal and gates.go's runPermissionGateRoundTrip/ resolveSelectedOption) and therefore reaches the ACP wire; implementations must not embed secrets, credentials, or other sensitive material in any error they return.
type LoadedSession ¶
type LoadedSession struct {
Live LiveSession
// ReplayedThrough is the highest turn index reconstructed from durable
// history for the session's replayed loop.
ReplayedThrough event.TurnIndex
}
LoadedSession is a LiveSession plus the replay anchor the session/load handler needs: the point up to which durable history was reconstructed before the live controller took over. Task 3.1 refines this anchor if replay needs more than one TurnIndex per loop.
type LogoutHandler ¶
LogoutHandler is the optional capability to clear an authenticated connection's credentials for ACP's logout method.
type MCPNotAcceptedError ¶
type MCPNotAcceptedError struct {
Count int
}
MCPNotAcceptedError reports that Setup construction was asked to carry one or more MCP server descriptors, but the host has not advertised acceptance of MCP setup. ACP setup fails closed rather than silently dropping the requested servers (see design doc "MCP and external capabilities").
func (*MCPNotAcceptedError) Error ¶
func (e *MCPNotAcceptedError) Error() string
type Options ¶
type Options struct {
// Host is the required session factory the facade calls through for
// session/new, session/load, and session/resume once those are wired
// (Task 2.3 onward). It carries no capability gating of its own: every
// product-facing agent needs it.
Host SessionHost
// Replayer, when supplied, backs the loadSession capability and
// session/load (see EventReplayer; wired starting Task 3.1).
Replayer EventReplayer
// Catalog, when supplied, backs the session/list capability (see
// SessionCatalog; wired starting Task 3.3).
Catalog SessionCatalog
// ConfigCatalog, when supplied, lets the facade enumerate a session's
// available runtime configuration options (see RuntimeConfigCatalog;
// wired starting Task 4.1). It has no initialize-level wire
// representation: config options are surfaced per-session, in the
// session/new, session/load, and session/resume responses' ConfigOptions
// and Modes fields (see config.go's initialConfigState).
ConfigCatalog RuntimeConfigCatalog
// ConfigController, when supplied, backs session/set_config_option and
// session/set_mode (see RuntimeConfigController; wired starting Task
// 4.1).
ConfigController RuntimeConfigController
// Compactor, when supplied, signals that compaction is available and
// gates whether the facade advertises/routes the `/compact` slash
// command at all (see Compactor; wired starting Task 4.2, per-session
// resolution added as a follow-up fix). It has no initialize-level wire
// representation: it is advertised as a session-level available
// command.
//
// This field is a presence signal ONLY — decided once, before any
// session exists, purely to answer "is `/compact` a thing at all on
// this connection?" It is NEVER invoked directly to perform a
// compaction: a single connection-wide field cannot tell which of
// several concurrent sessions a `/compact` request belongs to. The
// Compactor actually called for a given session is always resolved
// from that session's own LiveSession value instead, via a
// live.(Compactor) type-assertion in compact.go's handleCompactPrompt —
// see Compactor's doc (host.go) for the full explanation and the
// SessionCloser precedent this mirrors.
Compactor Compactor
// Deleter, when supplied, backs the session/delete capability (see
// SessionDeleter; wired starting Task 3.4).
Deleter SessionDeleter
// Authenticator, when supplied, backs the authenticate method. It is
// meaningless without at least one entry in AuthMethods — a client
// could never select a method id to authenticate with — so New rejects
// that combination (see ErrAuthenticatorWithoutMethods).
Authenticator Authenticator
// AuthMethods is the set of authentication methods advertised in the
// initialize response's authMethods field. It is only meaningful when
// Authenticator is supplied, and must be non-empty in that case.
AuthMethods []protocol.AuthMethod
// Logout, when supplied, backs the logout method (see LogoutHandler).
Logout LogoutHandler
}
Options configures a facade Agent. Host is the one required field; every other field is an optional capability interface from host.go, and nil means the corresponding ACP capability is unsupported (see capabilities.go for exactly how each maps onto the initialize response, and Register for which wire methods are gated on which field).
type RuntimeConfigCatalog ¶
type RuntimeConfigCatalog interface {
RuntimeConfigOptions(context.Context, SessionID) ([]RuntimeConfigOption, error)
}
RuntimeConfigCatalog is the optional capability to enumerate the runtime configuration options currently available for a session. Concretely this is backed by Harness's loop.ModeCatalog plus a product's own model/effort catalogs (Harness deliberately has no model/effort catalog itself — see design doc "Session configuration").
config.go always fetches this catalog fresh, immediately before applying a requested change: this is the latest-snapshot validation the design requires (an option id or value id valid a moment ago may no longer be — a mode removed, a model retired — so the check must run against what is true right now, never a value cached from session/new or an earlier request).
type RuntimeConfigChange ¶
type RuntimeConfigChange struct {
OptionID protocol.SessionConfigID
ValueID protocol.SessionConfigValueID
}
RuntimeConfigChange is a validated request to set one RuntimeConfigOption (identified by OptionID) to one of the values it currently offers (ValueID). config.go constructs this only after checking both ids against a RuntimeConfigCatalog snapshot fetched in the same request.
type RuntimeConfigController ¶
type RuntimeConfigController interface {
SetRuntimeConfigOption(context.Context, SessionID, RuntimeConfigChange) ([]RuntimeConfigOption, error)
}
RuntimeConfigController is the optional capability to apply a validated runtime configuration change and return the complete resulting option state so dependent choices stay coherent. Concretely this is backed by Harness's loop.Controller (SetMode, Change) plus a product's own model/effort controllers — reached the same consumer-owned-adapter way RuntimeConfigOption's doc describes, since LiveSession does not expose Controller either.
Config writes are idempotent: setting an option to its current value must succeed without side effects. config.go itself enforces this — it compares the requested ValueID against the latest catalog's CurrentValue for that option BEFORE ever calling SetRuntimeConfigOption, and short-circuits to a no-op success (no controller call, no config_option_update notification) when they already match. An implementation of this interface is therefore never asked to special-case a same-value request, and need not itself be idempotent for this contract to hold.
type RuntimeConfigOption ¶
type RuntimeConfigOption struct {
ID protocol.SessionConfigID
Category protocol.SessionConfigOptionCategory
Name string
Description string
Values []RuntimeConfigValue
CurrentValue protocol.SessionConfigValueID
}
RuntimeConfigOption is one configurable runtime option's complete current state: its identity, semantic category, human-readable label, the full set of values it currently offers, and which of those values is currently active. This is the discriminated union Task 4.1 was asked to refine RuntimeConfigOption's shape into — discriminated on Category, exactly mirroring the pinned schema's SessionConfigOptionCategory constants (mode, model, model_config, thought_level) plus any product-defined free-form category the schema reserves for values beginning with "_" (see protocol.SessionConfigOptionCategory's doc). Every RuntimeConfigOption config.go builds is projected onto the wire as a "select" variant (protocol.SessionConfigSelect): a dropdown over Values with CurrentValue marking the active one. This module has no need for the wire's "boolean" variant — every category this facade is asked to support (mode, model, thought level, and any further product-defined option) is naturally an enumerated choice, never a raw on/off toggle — so RuntimeConfigOption does not model one; a host that needs a boolean-shaped option is out of this task's scope.
Category is protocol.SessionConfigOptionCategory directly rather than a second parallel host-side enum: this package already imports acp/protocol (see SessionID/Authenticator above), the wire category is exactly the semantic this field carries, and duplicating it would only invite the two to drift.
Concretely, a RuntimeConfigCatalog implementation sources the mode category's Values/CurrentValue from Harness's loop.ModeCatalog.Modes() (translating each loop.ModeName into a RuntimeConfigValue) and every other category from the product's own model/effort/access catalogs — Harness deliberately has no model/effort/access catalog itself (see design doc "Session configuration"). acp/agent never imports pkg/loop to do this itself: LiveSession (this file) deliberately narrows session.Session down to the data-plane methods a prompt/gate/interrupt handler needs and omits ActiveLoop/Loop/SubmitToLoop, so a RuntimeConfigCatalog/ RuntimeConfigController is where a host reaches into its own loop.ModeCatalog/loop.Controller instead — a consumer-owned adapter, not a Harness-native type, exactly like SessionHost and the rest of this file.
type RuntimeConfigValue ¶
type RuntimeConfigValue struct {
ID protocol.SessionConfigValueID
Name string
Description string
}
RuntimeConfigValue is one selectable value of a RuntimeConfigOption: its stable wire identity, human-readable label, and optional description. It mirrors the pinned schema's SessionConfigSelectOption field-for-field so config.go's translation to the wire type is a straight copy, never a guess.
type SessionCatalog ¶
type SessionCatalog interface {
ListSessions(context.Context) ([]SessionCatalogEntry, error)
}
SessionCatalog is the optional capability to list known sessions for ACP session/list, matching Harness's sessionstore.Catalog.ListSessions plus this module's own cwd overlay (SessionCatalogEntry.Cwd — see its doc for the exact contract an implementation must uphold). The facade owns bounded page construction and opaque cursor validation over the returned metadata; callers cannot depend on the catalog's key layout.
type SessionCatalogEntry ¶
type SessionCatalogEntry struct {
// Meta is the underlying Harness catalog record for this session.
Meta sessionstore.SessionMeta
// Cwd is this session's absolute working directory, if this host knows
// it; empty if unknown. See this type's doc for the full contract.
Cwd string
}
SessionCatalogEntry is one entry a SessionCatalog reports for session/list: a Harness catalog record plus this host's own knowledge (if any) of that session's actual working directory.
Harness's sessionstore.SessionMeta carries no field for a session's live working-directory path at all — SessionMeta.CurrentWorkspace is a WorkspacePointer naming a content-addressed workspace-SNAPSHOT digest, not a filesystem path (see list.go's package doc for the full discrepancy). Cwd is this module's own consumer-owned overlay for that gap: it must be an absolute path when the host knows this session's actual working directory, and left empty when it does not — never a relative path, a placeholder, or a best-effort guess. handleSessionList (list.go) omits a session from the session/list response entirely rather than emit the pinned ACP schema's required-absolute-path SessionInfo.Cwd as an empty string, UNLESS the session is currently live in this facade's own bounded session registry, in which case its already-validated Setup.Cwd is used instead of Cwd here and this field is not consulted at all for that session id (see handleSessionList's overlay step).
A durable fix — persisting a real per-session cwd inside Harness's own sessionstore.SessionMeta, so a host would not need to track this mapping itself — is a legitimate future Harness-side follow-up. It is out of scope here (Harness is read-only in this plan); this field is the narrower, consumer-side workaround until that lands, if it ever does.
type SessionCloser ¶
SessionCloser is the segregated shutdown capability behind session/close. Harness puts Shutdown on SessionController, not on Session, so the host adapter exposes it to agent as a distinct closer rather than widening LiveSession (see design doc "Agent-side host boundary").
type SessionDeleter ¶
SessionDeleter is the optional capability to permanently delete a session's durable history. It is advertised only when the host supplies explicit storage and authorization semantics (see design doc "Cancellation and close").
type SessionHost ¶
type SessionHost interface {
NewSession(context.Context, Setup) (LiveSession, error)
LoadSession(context.Context, SessionID, Setup) (LoadedSession, error)
ResumeSession(context.Context, SessionID, Setup) (LiveSession, error)
}
SessionHost is the consumer-owned factory a product implements to create, restore, and resume Harness-backed sessions on ACP's behalf. It is the narrow substitute for touching rig.SessionOption or workspace placement directly: the facade only ever calls through this boundary with a validated Setup.
Wire-exposure trust boundary: an error this interface returns is folded into a *protocol.Fault's Message field essentially verbatim (see session.go's handleSessionNew, which does exactly this for NewSession) and therefore reaches the ACP wire. This is a deliberate, narrower case than Harness's own TurnFailed causes, some of which Harness itself documents as able to carry arbitrary, unsafe content (e.g. TurnPanicError.Detail) and which the facade sanitizes before they ever reach a caller (see prompt.go's sanitizedPromptFailure): a SessionHost is product-owned, not an internal Harness turn-failure cause, so it is trusted here the same way any other consumer-supplied adapter is. Implementations must still not embed secrets, credentials, or other sensitive material in any error they return.
type SessionID ¶
SessionID identifies a session. It reuses the Harness session UUID as its underlying representation rather than defining a second durable identity system: when a host creates the underlying session, that UUID is also used as the ACP session id string (see design doc "Session identity and authorization"). Load, resume, close, and list parse and validate the wire string into this identity before crossing the host boundary.
func ParseSessionID ¶
ParseSessionID validates and decodes an ACP wire sessionId into the SessionID (Harness UUID) it identifies. Every session-scoped handler must call this — directly, or through resolveSession — before the id touches any business logic or the host boundary.
Beyond uuid.Parse's purely structural 8-4-4-4-12 hex check, ParseSessionID also requires the canonical version-4/variant-RFC4122 stamp uuid.New() always produces (see SessionID's doc in host.go: an ACP sessionId is always minted as "the Harness session UUID"). A structurally valid but differently-stamped 128-bit value — the nil UUID, a v1/v3/v5 UUID, or anything else this facade's own uuid.New() would never produce — is rejected here as wrong-variant, never silently accepted as if it could name a live session.
type SessionIDError ¶
type SessionIDError struct {
Input string
Reason SessionIDReason
// contains filtered or unexported fields
}
SessionIDError reports that a wire sessionId string failed validation before ever reaching the live-session registry or the host boundary. All external input is untrusted, so every session-scoped method must reject a malformed id this way rather than let it flow deeper (see ParseSessionID).
func (*SessionIDError) Error ¶
func (e *SessionIDError) Error() string
func (*SessionIDError) Unwrap ¶
func (e *SessionIDError) Unwrap() error
type SessionIDReason ¶
type SessionIDReason string
SessionIDReason classifies why a wire sessionId string failed ParseSessionID.
const ( // SessionIDReasonEmpty: the wire sessionId was the empty string. SessionIDReasonEmpty SessionIDReason = "empty" // SessionIDReasonMalformed: the wire sessionId was not a structurally // valid 8-4-4-4-12 hyphenated UUID encoding (wrong length, a hyphen off // its fixed offset, or a non-hex digit). SessionIDReasonMalformed SessionIDReason = "malformed" // SessionIDReasonWrongVariant: the wire sessionId decoded to a // structurally valid UUID, but not the version-4/RFC-4122-variant stamp // every ACP session id carries (see ParseSessionID). SessionIDReasonWrongVariant SessionIDReason = "wrong_variant" )
type SessionMetaObservationError ¶
type SessionMetaObservationError struct {
Reason string
}
SessionMetaObservationError reports that ObserveSessionMeta was called with a SessionMeta this facade cannot act on.
func (*SessionMetaObservationError) Error ¶
func (e *SessionMetaObservationError) Error() string
type Setup ¶
type Setup struct {
// Cwd is the canonical absolute workspace root.
Cwd string
// ClientCapabilities is the negotiated client capability set, with every
// schema-declared default applied to any subfield the client did not
// advertise.
ClientCapabilities protocol.ClientCapabilities
// MCPServers is the set of MCP server descriptors requested for this
// session. It is non-empty only when the host explicitly accepted MCP
// setup (see NewSetup's acceptMCP parameter).
MCPServers []protocol.McpServer
}
Setup is the validated, ACP-facing negotiated setup data a SessionHost needs to create, load, or resume a session. It carries only negotiated setup values — never Harness rig options or other product configuration objects (see design doc "Agent-side host boundary"). Construct it with NewSetup; the zero value is not validated.
func NewSetup ¶
func NewSetup(cwd string, capabilities *protocol.ClientCapabilities, mcpServers []protocol.McpServer, acceptMCP bool) (Setup, error)
NewSetup validates cwd, defaults capabilities, and enforces MCP acceptance, returning a Setup ready to hand to a SessionHost.
cwd must canonicalize to a clean absolute path (reject empty, relative, ".."-bearing, or otherwise non-canonical values); see CwdErrorReason for the exact failure classification.
capabilities may be nil, in which case every schema-declared default applies wholesale (protocol.DefaultClientCapabilities). When non-nil, only the subfields the caller left unset (nil pointers) are filled from their own generated defaults; explicitly supplied values are preserved untouched.
mcpServers is rejected with a *MCPNotAcceptedError unless acceptMCP is true, matching the host's advertised MCP-acceptance capability. An empty mcpServers is never rejected.
type TooManyLiveSessionsError ¶
type TooManyLiveSessionsError struct {
// Max is the capacity that was reached (MaxLiveSessions in production;
// a test may construct a registry with a smaller bound).
Max int
}
TooManyLiveSessionsError reports that the live-session registry was already at capacity when a session tried to register.
func (*TooManyLiveSessionsError) Error ¶
func (e *TooManyLiveSessionsError) Error() string
type UnofferedPermissionOptionError ¶
type UnofferedPermissionOptionError struct {
GateID gate.ID
OptionID protocol.PermissionOptionID
}
UnofferedPermissionOptionError reports that a client's session/request_permission response selected (or otherwise did not validly select) a PermissionOptionID this facade never offered for the named gate.
All external input is untrusted, including — per the design doc's own framing of ACP as "a peer, not merely a client" — the client's chosen option: this error is deliberately raised BEFORE gate.GateResponse is ever built, so the gate is left open rather than answered on the strength of a selection that was never actually on offer. Task 2.7's session/close orchestration is expected to be the thing that eventually resolves a gate left open this way (deny), the same as any other outstanding permission request open when a session closes.
func (*UnofferedPermissionOptionError) Error ¶
func (e *UnofferedPermissionOptionError) Error() string
type UnsupportedContentBlockError ¶
type UnsupportedContentBlockError struct {
// Index is the position of the offending block within PromptRequest.Prompt.
Index int
}
UnsupportedContentBlockError reports that a session/prompt request contained a content block variant blocksFromPrompt cannot yet translate.
func (*UnsupportedContentBlockError) Error ¶
func (e *UnsupportedContentBlockError) Error() string