Documentation
¶
Index ¶
- Constants
- Variables
- func CleanupStaleACPManagedMCPServers(ctx context.Context, db libdb.DBManager) error
- func New(deps Deps) libacp.AgentFactory
- func NewACPCommandRunner(transport func() *Transport) localtools.CommandRunner
- func NewACPCommandRunnerWithShell(transport func() *Transport, shell localtools.PlatformShell) localtools.CommandRunner
- func NewACPCwdResolver(transport func() *Transport) func(context.Context) string
- func NewACPFileIO(transport func() *Transport) localtools.FileIO
- func NewServeCwdResolver(db libdb.DBManager, defaultRoot string) func(context.Context) string
- func ReadConfigValue(ctx context.Context, db libdb.DBManager, key string) string
- type ChainRegistry
- type Deps
- type EnvSetupSpec
- type PermissionRouter
- type Transport
- func (t *Transport) AskApproval(ctx context.Context, req hitlservice.ApprovalRequest) (bool, error)
- func (t *Transport) Authenticate(ctx context.Context, req libacp.AuthenticateRequest) (libacp.AuthenticateResponse, error)
- func (t *Transport) Cancel(ctx context.Context, req libacp.CancelNotification) error
- func (t *Transport) Close(ctx context.Context) error
- func (t *Transport) CloseSession(ctx context.Context, req libacp.CloseSessionRequest) (libacp.CloseSessionResponse, error)
- func (t *Transport) DeleteSession(ctx context.Context, req libacp.DeleteSessionRequest) (libacp.DeleteSessionResponse, error)
- func (t *Transport) Initialize(ctx context.Context, req libacp.InitializeRequest) (libacp.InitializeResponse, error)
- func (t *Transport) ListSessions(ctx context.Context, req libacp.ListSessionsRequest) (libacp.ListSessionsResponse, error)
- func (t *Transport) LoadSession(ctx context.Context, req libacp.LoadSessionRequest) (libacp.LoadSessionResponse, error)
- func (t *Transport) Logout(_ context.Context, _ libacp.LogoutRequest) (libacp.LogoutResponse, error)
- func (t *Transport) NewSession(ctx context.Context, req libacp.NewSessionRequest) (libacp.NewSessionResponse, error)
- func (t *Transport) Prompt(ctx context.Context, req libacp.PromptRequest) (libacp.PromptResponse, error)
- func (t *Transport) ResumeSession(ctx context.Context, req libacp.ResumeSessionRequest) (libacp.ResumeSessionResponse, error)
- func (t *Transport) SetSessionConfigOption(ctx context.Context, req libacp.SetSessionConfigOptionRequest) (libacp.SetSessionConfigOptionResponse, error)
- func (t *Transport) SetSessionMode(_ context.Context, _ libacp.SetSessionModeRequest) (libacp.SetSessionModeResponse, error)
- func (t *Transport) SetSessionModel(_ context.Context, _ libacp.SetSessionModelRequest) (libacp.SetSessionModelResponse, error)
Constants ¶
const ( // TerminalOutputMetaKey is the `_meta` key under which contenox streams live // shell-session output over the ACP WebSocket. It rides a session/update // notification whose sessionUpdate discriminator is TerminalOutputUpdateKind; // the whole thing lives in the spec's reserved `_meta` namespace, exactly like // WorkspaceConfigOptionsMetaKey — a conformant foreign client that does not // recognize the key (or the extension update kind) ignores it and keeps // working. The payload is a terminalOutputPayload: {sessionId, offset, chunk, // reset}. See docs/development/blueprints/beam/shell-sessions.md. TerminalOutputMetaKey = "contenox.terminalOutput" // TerminalOutputUpdateKind is the extension session/update discriminator that // carries a TerminalOutputMetaKey payload. Underscore-prefixed to mark it an // extension (mirroring libacp.ExtensionMethodPrefix); unknown to conformant // clients, which skip it. TerminalOutputUpdateKind libacp.SessionUpdateKind = "_contenox.terminalOutput" )
const AgentMetaKey = "contenox.agent"
AgentMetaKey is the session/new (and session/list) `_meta` key a client uses to bind a session to a REGISTERED external ACP agent instead of the native task-chain engine: `{"contenox.agent": "<registered agent name>"}`. Absent = native chain path, byte-for-byte the historical behavior. It is a contenox extension living in the spec's reserved `_meta` namespace (the same precedent as WorkspaceConfigOptionsMetaKey); conformant clients that don't recognize it ignore `_meta` entirely.
const AgentModeConfigOptionID = "contenox.agent-mode"
AgentModeConfigOptionID is the reserved SessionConfigOption id under which an external session surfaces the DOWNSTREAM agent's session Modes (its SessionModeState) as a single synthetic "select" picker in the upstream client's toolbar. The ACP spec models session modes (session/set_mode + SessionModeState) and config options (session/set_config_option + SessionConfigOption) as two distinct surfaces; contenox does not expose a first-class mode toggle to its clients, so a downstream agent that advertises modes only (claude-code-acp: default/acceptEdits/plan/dontAsk/bypassPermissions, zero configOptions) would otherwise render an empty toolbar. Mapping the modes onto one synthetic config option — id AgentModeConfigOptionID, label "Mode", type "select", each availableMode as a value(id)→label(name), currentValue the currentModeId — lets the existing config-option picker render it; a set on this id is translated back to session/set_mode (see externalDriver.SetConfigOption), and a downstream current_mode_update is relayed as a config_option_update over this same id. It lives in contenox's reserved dotted namespace so it never collides with a downstream agent's own option ids.
const AgentModelConfigOptionID = "contenox.agent-model"
AgentModelConfigOptionID is the reserved SessionConfigOption id under which an external session surfaces the DOWNSTREAM agent's UNSTABLE model-picker state (its SessionModelState) as a single synthetic "select" picker in the upstream client's toolbar — the exact parallel of AgentModeConfigOptionID for session modes. Zed's claude-code-acp advertises a `models` state (availableModels + currentModelId) in its session/new response and switches models via the unstable `session/set_model` method (`unstable_setSessionModel`), a surface distinct from both session modes and config options; contenox does not expose a first-class model toggle to its clients, so mapping that state onto one synthetic config option — id AgentModelConfigOptionID, label "Model", type "select", each availableModel as a value(modelId)→label(name), currentValue the currentModelId — lets the existing config-option picker render it. A set on this id is translated back to session/set_model (see externalDriver.SetConfigOption). Unlike modes, the ACP session/update stream carries NO model-update kind, so there is nothing to relay after a switch: the (stateless) set_model response is the truth and the confirmed model is adopted locally. The model entries carry no effort/fast-mode facet (claude-code-acp's availableModels are modelId + name + description only), so this select has no sub-option for reasoning effort. It lives in contenox's reserved dotted namespace so it never collides with a downstream agent's own option ids, and it is placed after the synthetic mode option and before the downstream's own config options.
const ( // WorkspaceConfigOptionsMetaKey is the initialize-response `_meta` key under // which contenox advertises the workspace-level (session-less) config // options. Sessions are minted lazily (on first prompt — see AcpChatPage's // handleSubmit), so a fresh chat has no session and therefore none of the // per-session config options that normally arrive with session/new. This // extension lets a client render the model/think/HITL/token-limit controls // on the empty chat, stage the user's choices, and re-apply them via // set_config_option right after session/new — crucial when the configured // default model is broken and the user must pick a working one BEFORE the // first (failing) turn. It is a contenox extension living in the spec's // reserved `_meta` namespace: conformant clients that don't recognize the // key ignore it entirely and simply wait for the per-session options. WorkspaceConfigOptionsMetaKey = "contenox.workspaceConfigOptions" )
Variables ¶
var ErrNoBoundSession = errors.New("acpsvc: no ACP transport bound to contenox session")
ErrNoBoundSession reports that no live ACP transport owns the contenox session named in the request context. serve's shared AskApproval keys its fallback on this: when the router cannot route (a headless/API caller, or a session with no live WS connection), the engine's HITL request goes to the approval-API path instead of hanging on a permission prompt nobody answers.
Functions ¶
func CleanupStaleACPManagedMCPServers ¶ added in v0.29.0
CleanupStaleACPManagedMCPServers removes client-scoped ACP MCP registrations left behind by a previous process. Durable MCP configuration must be created through the normal `contenox mcp` commands or HTTP API; session/new and session/load MCP servers are temporary by ACP contract.
func New ¶
func New(deps Deps) libacp.AgentFactory
func NewACPCommandRunner ¶
func NewACPCommandRunner(transport func() *Transport) localtools.CommandRunner
func NewACPCommandRunnerWithShell ¶ added in v0.28.4
func NewACPCommandRunnerWithShell(transport func() *Transport, shell localtools.PlatformShell) localtools.CommandRunner
func NewACPCwdResolver ¶
func NewACPFileIO ¶
func NewACPFileIO(transport func() *Transport) localtools.FileIO
func NewServeCwdResolver ¶ added in v0.36.0
NewServeCwdResolver returns the cwd resolver for the serve path, where a single shared local_fs tool is consulted by many per-connection transports — so it cannot close over one transport the way the stdio path does. Instead it resolves the session's persisted workspace cwd from the database (keyed by the internal session id in ctx), falling back to defaultRoot when the session has none, its stored cwd is the legacy "/" sentinel, or there is no session in scope. The stored cwd is already validated against the allowlist at session/new time, so this read is trusted; defaultRoot is the Factory default.
Types ¶
type ChainRegistry ¶
type ChainRegistry struct {
// contains filtered or unexported fields
}
func LoadChainRegistry ¶
func LoadChainRegistry() (*ChainRegistry, error)
func LoadChainRegistryFrom ¶
func LoadChainRegistryFrom(filename, envVar string) (*ChainRegistry, error)
LoadChainRegistryFrom loads the ACP chain for a specific profile: filename is the ~/.contenox/ file the chain is read from, envVar overrides that path. A missing file is a hard error (fail closed) — callers must not fall back to a different chain.
func (*ChainRegistry) Default ¶
func (r *ChainRegistry) Default() *taskengine.TaskChainDefinition
func (*ChainRegistry) Source ¶
func (r *ChainRegistry) Source() string
type Deps ¶
type Deps struct {
Engine *enginesvc.Engine
DB libdb.DBManager
ChainRegistry *ChainRegistry
DefaultModel string
DefaultProvider string
DefaultAltModel string
DefaultAltProvider string
DefaultMaxTokens string
DefaultThink string
WorkspaceID string
// ContenoxDir is the active .contenox directory, used to locate auxiliary
// chains (e.g. chain-compact.json for the /compact command).
ContenoxDir string
// WorkspaceRoots is the allowlist of directories a client may choose as a
// session's workspace (its cwd). When nil, no allowlist is enforced and any
// absolute cwd is accepted — the historical behavior for the stdio ACP path,
// where the editor owns the filesystem. serve sets it so a browser client
// can only root a session inside an operator-approved directory. The sentinel
// cwd "/" (what beam sends today) and an empty cwd both resolve to the
// default root, so existing clients keep working.
WorkspaceRoots *vfs.Factory
// ShellSessions manages the per-chat-session persistent PTY shells behind the
// shell-session surface (the terminal panel + shell_session_run/read tools).
// Nil when shell tooling is disabled: the terminal extension methods report
// method-not-found and no live output is streamed — the feature is absent,
// not broken.
ShellSessions shellsession.Manager
// KnownPolicies are the HITL policy preset names shown by /policy when
// listing. Display only — empty just omits the list.
KnownPolicies []string
// HITLDefaultPolicyName is the policy the engine falls back to when no
// override is set, shown by /policy so the status is accurate. Display only.
HITLDefaultPolicyName string
// UpdateBanner is an optional one-shot message sent to the client as an
// agent_message_chunk on the first session created or loaded. Empty = no banner.
UpdateBanner string
// EnvSetup enables the env_var auth method: in setup-only mode initialize
// advertises Vars as the environment the client should collect/set, and
// authenticate with the env method calls Complete to finish setup
// non-interactively from the current environment. Nil disables the method.
EnvSetup *EnvSetupSpec
// PermissionRouter, when set, is a process-shared registry each transport
// records its live (contenox session -> this transport) bindings into, so a
// single shared engine can route a HITL approval back to the WS connection
// whose client raised it. serve sets it (it hosts many ACP WS connections
// behind one engine); the stdio ACP path leaves it nil — it has one
// transport, late-bound directly into the engine's AskApproval closure.
PermissionRouter *PermissionRouter
}
type EnvSetupSpec ¶ added in v0.36.0
type EnvSetupSpec struct {
Vars []libacp.AuthEnvVar
Complete func(ctx context.Context) error
}
EnvSetupSpec describes environment-variable-based setup (the non-interactive sibling of the terminal setup wizard).
type PermissionRouter ¶ added in v0.36.0
type PermissionRouter struct {
// contains filtered or unexported fields
}
PermissionRouter maps a contenox session id to the ACP Transport that owns it. serve runs many ACP WebSocket connections (each its own Transport) behind a SINGLE shared engine, so the engine's one AskApproval callback cannot close over a single transport the way the stdio ACP path does (acp_cmd.go late-binds its lone transport directly). Instead each Transport registers its live (contenox session -> this transport) bindings here, and serve's AskApproval consults the router to dispatch session/request_permission to the exact WS connection whose client raised the gated tool call — the one beam's PermissionGate is waiting on.
The stdio ACP path leaves Deps.PermissionRouter nil: it has exactly one transport and needs no routing.
func NewPermissionRouter ¶ added in v0.36.0
func NewPermissionRouter() *PermissionRouter
NewPermissionRouter returns an empty router ready to be shared across a serve process's ACP WebSocket transports.
func (*PermissionRouter) AskApproval ¶ added in v0.36.0
func (r *PermissionRouter) AskApproval(ctx context.Context, req hitlservice.ApprovalRequest) (bool, error)
AskApproval bridges an engine HITL request to the ACP transport that owns the contenox session named in ctx (runtimetypes.SessionIDContextKey), driving that connection's session/request_permission flow. It returns ErrNoBoundSession when no live transport owns the session so the caller can fall back to a non-ACP approval path; a genuine deny resolves as (false, nil) and a client cancellation as (false, context.Canceled) — neither is ErrNoBoundSession, so neither triggers a fallback.
type Transport ¶
type Transport struct {
// contains filtered or unexported fields
}
func (*Transport) AskApproval ¶
func (t *Transport) AskApproval(ctx context.Context, req hitlservice.ApprovalRequest) (bool, error)
func (*Transport) Authenticate ¶
func (t *Transport) Authenticate(ctx context.Context, req libacp.AuthenticateRequest) (libacp.AuthenticateResponse, error)
func (*Transport) Cancel ¶
Cancel handles session/cancel: it aborts the session's in-flight prompt turn with context.Canceled semantics. Prompt's error path keys the silent-cancel on errors.Is(err, context.Canceled) and resolves the prompt with stopReason "cancelled" (never a JSON-RPC error), per the ACP contract. A cancel for a session with no running turn is a clean no-op.
func (*Transport) CloseSession ¶ added in v0.36.0
func (t *Transport) CloseSession(ctx context.Context, req libacp.CloseSessionRequest) (libacp.CloseSessionResponse, error)
CloseSession releases the connection-local resources of a session without touching its stored history. Closing an unknown session succeeds: the desired state (not open here) already holds.
func (*Transport) DeleteSession ¶ added in v0.36.0
func (t *Transport) DeleteSession(ctx context.Context, req libacp.DeleteSessionRequest) (libacp.DeleteSessionResponse, error)
DeleteSession removes the session's stored history (and any connection-local state). Per spec, deleting a nonexistent session succeeds silently, and the session disappears from session/list.
func (*Transport) Initialize ¶
func (t *Transport) Initialize(ctx context.Context, req libacp.InitializeRequest) (libacp.InitializeResponse, error)
func (*Transport) ListSessions ¶
func (t *Transport) ListSessions(ctx context.Context, req libacp.ListSessionsRequest) (libacp.ListSessionsResponse, error)
func (*Transport) LoadSession ¶
func (t *Transport) LoadSession(ctx context.Context, req libacp.LoadSessionRequest) (libacp.LoadSessionResponse, error)
func (*Transport) Logout ¶ added in v0.36.0
func (t *Transport) Logout(_ context.Context, _ libacp.LogoutRequest) (libacp.LogoutResponse, error)
Logout is not supported: contenox's auth model (the terminal/browser/env setup wizards above) has no persisted "logged in" session to tear down, and Initialize never advertises AgentCapabilities.Auth.Logout, so a conformant client will never call this.
func (*Transport) NewSession ¶
func (t *Transport) NewSession(ctx context.Context, req libacp.NewSessionRequest) (libacp.NewSessionResponse, error)
func (*Transport) Prompt ¶
func (t *Transport) Prompt(ctx context.Context, req libacp.PromptRequest) (libacp.PromptResponse, error)
Prompt resolves the session and dispatches the turn to its driver. The driver (native chain engine vs. registered downstream ACP agent) owns everything the turn does — there is no native-vs-external branch here.
func (*Transport) ResumeSession ¶ added in v0.36.0
func (t *Transport) ResumeSession(ctx context.Context, req libacp.ResumeSessionRequest) (libacp.ResumeSessionResponse, error)
ResumeSession is session/load without the history replay: the client kept its transcript and only needs the server-side session re-bound.
func (*Transport) SetSessionConfigOption ¶ added in v0.29.0
func (t *Transport) SetSessionConfigOption(ctx context.Context, req libacp.SetSessionConfigOptionRequest) (libacp.SetSessionConfigOptionResponse, error)
func (*Transport) SetSessionMode ¶ added in v0.36.0
func (t *Transport) SetSessionMode(_ context.Context, _ libacp.SetSessionModeRequest) (libacp.SetSessionModeResponse, error)
SetSessionMode is not supported: contenox does not model a Zed-style Ask/Code session mode toggle as a first-class session/set_mode capability — the equivalent controls (model, HITL policy, think level) are exposed as session config options instead. Initialize never returns a Modes state in session/new or session/load, so a conformant client will never call this.
func (*Transport) SetSessionModel ¶ added in v0.36.0
func (t *Transport) SetSessionModel(_ context.Context, _ libacp.SetSessionModelRequest) (libacp.SetSessionModelResponse, error)
SetSessionModel is not supported on contenox's OWN upstream surface: the runtime never advertises a `models` state (SessionModelState) to its clients — for an external session the DOWNSTREAM agent's model picker is surfaced as the synthetic AgentModelConfigOptionID config option and switched via set_config_option (which the driver translates to the downstream's session/set_model), and a native session exposes no model picker of this UNSTABLE shape at all. So a conformant client never calls this; it reports MethodNotFound, mirroring SetSessionMode.