Documentation
¶
Index ¶
- Variables
- func FormatOpenCodeConfig(providers []secrets.LLMProviderData) ([]byte, error)
- func Register()
- type AgentClient
- type Client
- func (c *Client) DisposeInstance(ctx context.Context) error
- func (c *Client) GetSessionStatuses(ctx context.Context) (map[string]string, error)
- func (c *Client) InterruptV2(ctx context.Context, sessionID string) error
- func (c *Client) ListModels(ctx context.Context) ([]byte, error)
- func (c *Client) PatchConfig(ctx context.Context, config map[string]any) error
- func (c *Client) PromptV2(ctx context.Context, sessionID, text string, delivery V2Delivery) (*V2PromptResponse, error)
- func (c *Client) PushCredentials(ctx context.Context, providers []secrets.LLMProviderData) error
- func (c *Client) StageCredentials(ctx context.Context, providers []secrets.LLMProviderData) error
- type ConfigWriter
- func (w *ConfigWriter) HasRelay() bool
- func (w *ConfigWriter) Rebuild() error
- func (w *ConfigWriter) SetMCPServers(servers []MCPServerEntry)
- func (w *ConfigWriter) SetModel(model string)
- func (w *ConfigWriter) SetProviders(formattedConfig []byte) error
- func (w *ConfigWriter) SetRelay(url string, models []RelayModel)
- type ConfigWriterOption
- type Dialect
- func (d *Dialect) EventStreamPath() string
- func (d *Dialect) IsPermissionAsked(eventType string) bool
- func (d *Dialect) IsPermissionResolved(eventType string) bool
- func (d *Dialect) IsQuestionAsked(eventType string) bool
- func (d *Dialect) IsQuestionResolved(eventType string) bool
- func (d *Dialect) IsSessionBusy(eventType string, properties json.RawMessage) bool
- func (d *Dialect) IsSessionIdle(eventType string, properties json.RawMessage) bool
- func (d *Dialect) ParsePermissionRequest(eventType string, properties json.RawMessage) (*agent.PermissionRequest, error)
- func (d *Dialect) ParseQuestionRequest(eventType string, properties json.RawMessage) (*agent.QuestionRequest, error)
- func (d *Dialect) ParseSessionStatus(properties json.RawMessage) (string, string, error)
- func (d *Dialect) PermissionListPath() string
- func (d *Dialect) PermissionReplyPath(requestID string) string
- func (d *Dialect) QuestionListPath() string
- func (d *Dialect) QuestionRejectPath(requestID string) string
- func (d *Dialect) QuestionReplyPath(requestID string) string
- func (d *Dialect) SessionAbortPath(sessionID string) string
- func (d *Dialect) SessionCreatePath() string
- func (d *Dialect) SessionGetPath(sessionID string) string
- func (d *Dialect) SessionListPath() string
- func (d *Dialect) SessionMessagePath(sessionID string) string
- func (d *Dialect) SessionPromptAsyncPath(sessionID string) string
- type MCPServerEntry
- type OpenCodeAgent
- type Option
- type PasswordResolver
- type PodIPResolver
- type RelayModel
- type V2Delivery
- type V2PromptResponse
- type WorkspaceClient
- func (w *WorkspaceClient) DisposeInstance(ctx context.Context, userID, workspaceID string) error
- func (w *WorkspaceClient) GetSessionStatuses(ctx context.Context, userID, workspaceID string) (map[string]string, error)
- func (w *WorkspaceClient) ListModels(ctx context.Context, userID, workspaceID string) ([]byte, error)
- func (w *WorkspaceClient) PatchConfig(ctx context.Context, userID, workspaceID string, config map[string]any) error
- func (w *WorkspaceClient) StageCredentials(ctx context.Context, userID, workspaceID string, ...) error
- type WorkspaceClientOption
Constants ¶
This section is empty.
Variables ¶
var ( ErrV2PromptConflict = errors.New("opencode V2: prompt conflict (id collision)") ErrV2SessionNotFound = errors.New("opencode V2: session not found") )
V2 prompt/interrupt error sentinels. These are distinct from the V1 errors in agent_client.go because the V2 API has different failure modes (PromptConflictError on caller-supplied id, SessionNotFound on unknown session). Callers branch on these to decide retry vs. surface-to-user.
var ErrNoRunningPod = &pkgerrors.StatusError{ Status: http.StatusNotFound, Code: "no_running_pod", Message: "workspace pod not running", }
ErrNoRunningPod is returned when the workspace has no running pod (empty podIP). The handler maps this to 404.
Functions ¶
func FormatOpenCodeConfig ¶
func FormatOpenCodeConfig(providers []secrets.LLMProviderData) ([]byte, error)
FormatOpenCodeConfig renders a slice of validated LLMProviderData into the JSON shape opencode accepts.
**Schema** (evidence-driven; established by live cluster probe in worklog 0128. Do NOT change without re-validating against a running opencode):
{
"$schema": "https://opencode.ai/config.json",
"provider": { <-- SINGULAR (not "providers")
"<id>": {
"options": { <-- direct, NO aisdk wrapper
"apiKey": "...", <-- the credential
"baseURL": "..." <-- in options, NOT in a
}, separate `endpoint` object
"models": { "<id>": { "name": "..." } }
}
},
"model": "<id>/<modelID>"
}
What pre-fix code generated, and why opencode rejected it:
- top-level key was `providers` (plural) → ConfigInvalidError
- apiKey lived at options.aisdk.provider.apiKey → ConfigInvalidError
- baseURL lived at endpoint.url → silently ignored (chat requests went to api.openai.com instead of the operator's endpoint)
The function is pure — no side effects, no filesystem access.
Returns an error if providers is empty (callers MUST check for this — opencode treats an empty config differently and a "no-op write of an empty config" is a bug).
Types ¶
type AgentClient ¶
type AgentClient interface {
ListModels(ctx context.Context, userID, workspaceID string) ([]byte, error)
PatchConfig(ctx context.Context, userID, workspaceID string, config map[string]any) error
DisposeInstance(ctx context.Context, userID, workspaceID string) error
GetSessionStatuses(ctx context.Context, userID, workspaceID string) (map[string]string, error)
StageCredentials(ctx context.Context, userID, workspaceID string, providers []secrets.LLMProviderData) error
}
AgentClient abstracts all direct opencode HTTP communication at the workspace level (US-29.1). Each method resolves podIP and password internally from the injected resolvers, keeping callers clean of auth concerns. The interface is caller-shaped — consumers ask for what they need, not for a raw HTTP client.
userID is required for workspace ownership verification (the PodIPResolver enforces that the caller owns the workspace before returning the IP).
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client communicates with a running opencode instance's HTTP API. It implements credential injection via PUT /auth/:providerID (Control API) and instance disposal via POST /instance/dispose.
Every opencode endpoint — including /auth/* and /instance/* — is gated by HTTP Basic auth with username `agentd.AuthUsername` and the per-pod password mounted at /sandbox-cfg/password (= OPENCODE_SERVER_PASSWORD env var). Calling these endpoints without auth produces 401 + WWW-Authenticate: Basic realm="Secure Area", which is what broke the live credential flow in worklog 0125.
func NewClient ¶
NewClient creates a Client targeting the given opencode base URL.
password is the value mounted at /sandbox-cfg/password inside the sandbox pod and exported to opencode as OPENCODE_SERVER_PASSWORD. It is the SAME secret used by every other agentd → opencode call (see cmd/workspace-agentd/main.go OpenCodeClient). Passing the empty string is allowed (so unit tests that don't need auth-gated paths still work) but will fail against a real opencode server with 401.
func (*Client) DisposeInstance ¶
DisposeInstance triggers POST /instance/dispose, which invalidates all InstanceState caches for the current instance. The opencode process stays alive; the next request triggers a fresh instance load with updated auth.
In-flight LLM calls are aborted. Sessions persist in SQLite.
func (*Client) GetSessionStatuses ¶
GetSessionStatuses calls GET /session/status on opencode and returns the current status of all known sessions. The map key is the session ID; the value is the status type string: "idle", "busy", or "retry".
func (*Client) InterruptV2 ¶ added in v0.13.0
InterruptV2 sends a non-destructive interrupt to an opencode V2 session. It POSTs to /api/session/:sid/interrupt with Basic auth.
The spike confirmed: returns HTTP 204 (No Content) both when a turn is in-flight AND when the session is idle (no-op success). Interrupt is non-destructive (F8): admitted-but-unpromoted queue entries survive and run on the next execution.wake. This replaces the V1 destructive abort that cleared the Redis queue.
func (*Client) ListModels ¶
ListModels calls GET /provider on opencode and returns the raw JSON body. The caller is responsible for parsing the response shape (it varies by opencode version). The body is size-limited to providerCatalogReadLimit.
func (*Client) PatchConfig ¶
PatchConfig calls PATCH /global/config on opencode with the given config map. Used by SetModel to change the active model.
func (*Client) PromptV2 ¶ added in v0.13.0
func (c *Client) PromptV2(ctx context.Context, sessionID, text string, delivery V2Delivery) (*V2PromptResponse, error)
PromptV2 sends a prompt to an opencode V2 session endpoint with the given delivery mode. It POSTs to /api/session/:sid/prompt using the same Basic auth (opencode + password) as every other opencode call.
The response is admit-and-schedule (non-streaming): opencode queues the prompt internally and returns immediately. The body is read+discarded on non-2xx; on 2xx the data payload is decoded and returned.
F17: the caller MUST NOT supply a message id — this method omits it unconditionally. F18: the prompt body is {text:"..."} (plain string), not the parts-based contract shape; see the v2PromptRequest doc comment.
func (*Client) PushCredentials ¶
PushCredentials writes each provider's API key to opencode's auth store via PUT /auth/:providerID. This writes to auth.json but does NOT trigger provider state refresh — call DisposeInstance or (future) RefreshProviders afterward to pick up the new credentials.
Returns nil if providers is empty (no-op). Returns the first error encountered; subsequent providers are not attempted.
func (*Client) StageCredentials ¶
StageCredentials writes provider credentials to opencode's auth.json (via PUT /auth/:providerID) but does NOT trigger provider-state refresh. The credentials are "staged" — they exist on disk but opencode's in-memory provider state is unchanged until DisposeInstance is called separately by the caller (typically via POST /api/v1/workspaces/:id/agent/reload).
Returns nil if providers is empty (no-op).
type ConfigWriter ¶ added in v0.12.0
type ConfigWriter struct {
// contains filtered or unexported fields
}
ConfigWriter is the single writer of agent-config.json within the agentd process. All config changes (provider credentials, model selection, relay injection, MCP servers) go through SetProviders / SetModel / SetRelay / SetMCPServers followed by Rebuild.
Thread-safe: all mutating methods acquire mu. Rebuild serializes the read-merge-write cycle so concurrent reloads and relay injection cannot interleave.
func NewConfigWriter ¶ added in v0.12.0
func NewConfigWriter(path string, opts ...ConfigWriterOption) *ConfigWriter
NewConfigWriter creates the writer and initializes its sources from the existing agent-config.json file (written by the materialize subcommand at boot), the admin-prompt file, and the allowed-dirs file. Options set the paths; absent options mean the corresponding source is skipped. If agent-config.json is absent or corrupt, sources start empty and the first Rebuild creates a fresh file.
func (*ConfigWriter) HasRelay ¶ added in v0.12.0
func (w *ConfigWriter) HasRelay() bool
HasRelay returns true if the relay injector has successfully injected relay config. Used by the readyz handler for the RelayInjected signal.
func (*ConfigWriter) Rebuild ¶ added in v0.12.0
func (w *ConfigWriter) Rebuild() error
Rebuild merges all sources (providers, model, relay, MCP, admin prompt, allowed dirs) and writes the complete agent-config.json atomically via temp-file + os.Rename.
Merge semantics:
- $schema is always set to "https://opencode.ai/config.json"
- provider map = existing providers (from SetProviders or loadExisting)
- opencode-relay (if relay is set). No existing provider is removed.
- model = the model source (from SetModel or loadExisting)
- disabled_providers = ["opencode"] (only if relay is set)
- agent.build.prompt = admin prompt (deep-merged into existing build agent)
- mode.permissions.external_directory = allowed-dirs glob allow-rules
- mcp = staged MCP servers + pre-marshal hook additions
The temp-file + rename pattern ensures readers never see a partially written file. os.Rename is atomic on POSIX filesystems (same mount).
func (*ConfigWriter) SetMCPServers ¶ added in v0.12.0
func (w *ConfigWriter) SetMCPServers(servers []MCPServerEntry)
SetMCPServers replaces the MCP server source. Called after materialize stages MCP server entries from secrets.json. Each server renders as one entry in the opencode "mcp" top-level config section.
func (*ConfigWriter) SetModel ¶ added in v0.12.0
func (w *ConfigWriter) SetModel(model string)
SetModel updates the model source. Called by applyWorkspaceConfig at boot (via the materialize subcommand) to set the default model from workspace-config.json.
func (*ConfigWriter) SetProviders ¶ added in v0.12.0
func (w *ConfigWriter) SetProviders(formattedConfig []byte) error
SetProviders updates the provider source from a FormatOpenCodeConfig result. The formatted bytes contain the complete opencode config shape ({ $schema, provider: {...} }); this method extracts just the provider map. The model from the formatter is NOT captured — the model source is owned by SetModel (set at boot via loadExisting) and must survive credential reloads.
func (*ConfigWriter) SetRelay ¶ added in v0.12.0
func (w *ConfigWriter) SetRelay(url string, models []RelayModel)
SetRelay updates the relay source after the relay injector successfully discovers the free model list. The writer stores the URL and models; Rebuild merges them into the provider map.
type ConfigWriterOption ¶ added in v0.12.0
type ConfigWriterOption func(*ConfigWriter)
ConfigWriterOption configures a ConfigWriter at construction.
func WithAdminPromptPath ¶ added in v0.12.0
func WithAdminPromptPath(p string) ConfigWriterOption
WithAdminPromptPath sets the path to the admin-configured system prompt file (written by the agentd bootstrap subcommand). The writer reads it once at construction. Empty (the default) means no admin-prompt source.
func WithAllowedDirsPath ¶ added in v0.12.0
func WithAllowedDirsPath(p string) ConfigWriterOption
WithAllowedDirsPath sets the path to the instance's allowedExternalDirectories JSON array (written by the bootstrap subcommand). The writer reads it once at construction. Empty (the default) means no external-directory allow-rules are injected.
func WithPreMarshalHook ¶ added in v0.12.0
func WithPreMarshalHook(fn func(map[string]json.RawMessage)) ConfigWriterOption
WithPreMarshalHook registers a function invoked on the rendered config map immediately before final marshal. agentd uses this to inject its built-in admin MCP server (the "llmsafespaces" entry pointing at agentd's own admin port) without this package needing to know that port. nil (the default) means no hook.
type Dialect ¶
type Dialect struct{}
Dialect implements agent.Dialect for the opencode agent runtime.
func (*Dialect) EventStreamPath ¶
func (*Dialect) IsPermissionAsked ¶
func (*Dialect) IsPermissionResolved ¶
func (*Dialect) IsQuestionAsked ¶
func (*Dialect) IsQuestionResolved ¶
func (*Dialect) IsSessionBusy ¶
func (d *Dialect) IsSessionBusy(eventType string, properties json.RawMessage) bool
func (*Dialect) IsSessionIdle ¶
func (d *Dialect) IsSessionIdle(eventType string, properties json.RawMessage) bool
func (*Dialect) ParsePermissionRequest ¶
func (d *Dialect) ParsePermissionRequest(eventType string, properties json.RawMessage) (*agent.PermissionRequest, error)
func (*Dialect) ParseQuestionRequest ¶
func (d *Dialect) ParseQuestionRequest(eventType string, properties json.RawMessage) (*agent.QuestionRequest, error)
func (*Dialect) ParseSessionStatus ¶
func (*Dialect) PermissionListPath ¶
func (*Dialect) PermissionReplyPath ¶
func (*Dialect) QuestionListPath ¶
func (*Dialect) QuestionRejectPath ¶
func (*Dialect) QuestionReplyPath ¶
func (*Dialect) SessionAbortPath ¶
func (*Dialect) SessionCreatePath ¶
func (*Dialect) SessionGetPath ¶
func (*Dialect) SessionListPath ¶
func (*Dialect) SessionMessagePath ¶
func (*Dialect) SessionPromptAsyncPath ¶
type MCPServerEntry ¶ added in v0.12.0
type MCPServerEntry struct {
Name string
Transport string // http, sse, or stdio
URL string
Command string
Args []string
TimeoutMs int
Env map[string]string
Headers map[string]string
}
MCPServerEntry is one staged MCP server, carrying the fields needed to render its opencode config entry (local or remote shape per the contract).
type OpenCodeAgent ¶
type OpenCodeAgent struct{}
func (*OpenCodeAgent) FormatProviderConfig ¶
func (a *OpenCodeAgent) FormatProviderConfig(providers []agent.LLMProviderData) ([]byte, error)
func (*OpenCodeAgent) Type ¶
func (a *OpenCodeAgent) Type() agent.AgentType
func (*OpenCodeAgent) ValidateCredentials ¶
func (a *OpenCodeAgent) ValidateCredentials(rawConfig []byte) (*agent.CredentialCheckResult, error)
type Option ¶
type Option func(*Client)
Option configures a Client at construction.
func WithHTTPClient ¶
WithHTTPClient injects a pre-configured *http.Client (e.g. one shared across many workspaces for connection pooling — M11-a). When unset, NewClient allocates a default client with a 10s timeout.
type PasswordResolver ¶
PasswordResolver resolves the opencode Basic-auth password for a workspace. The API-side implementation reads from the K8s Secret cache (pwCache); agentd-side reads from /sandbox-cfg/password.
type PodIPResolver ¶
type PodIPResolver interface {
GetWorkspacePodIP(ctx context.Context, userID, workspaceID string) (string, error)
}
PodIPResolver resolves the pod IP for a workspace.
type RelayModel ¶ added in v0.12.0
RelayModel is one free-tier model discovered from opencode's /provider endpoint by the relay injector. The writer renders it into the opencode-relay provider block's "models" map.
type V2Delivery ¶ added in v0.13.0
type V2Delivery string
V2Delivery selects how opencode's V2 session runner admits a prompt.
"queue" — the prompt is admitted to the durable SessionInput table and promoted when the session would otherwise go idle (F2, F8). This is the default for the inboard-session-queue epic (US-63.3).
"steer" — the prompt is injected at the next safe boundary without aborting in-flight tools (F-spike). Deferred to a follow-up epic; the V2 API supports it but US-63.3 defaults to "queue".
const ( V2DeliveryQueue V2Delivery = "queue" V2DeliverySteer V2Delivery = "steer" )
type V2PromptResponse ¶ added in v0.13.0
type V2PromptResponse struct {
AdmittedSeq int `json:"admittedSeq"`
ID string `json:"id"`
SessionID string `json:"sessionID"`
TimeCreated string `json:"timeCreated,omitempty"`
}
V2PromptResponse is the data payload returned by a successful POST /api/session/:sid/prompt. The spike confirmed HTTP 200 with {data:{admittedSeq, id, sessionID, prompt, delivery, timeCreated}}.
type WorkspaceClient ¶
type WorkspaceClient struct {
// contains filtered or unexported fields
}
WorkspaceClient implements AgentClient by resolving each call to a specific pod IP + password, then delegating to the low-level Client. It is constructed once and shared across handlers; each call resolves the workspace's current podIP + password fresh, so pod migrations and password rotations are transparent to callers.
The shared httpClient enables connection pooling across all workspace calls (M11-a). The agentPort field replaces the former package-level var so tests are parallel-safe (M1-a).
func NewWorkspaceClient ¶
func NewWorkspaceClient(pw PasswordResolver, ip PodIPResolver, logger *zap.Logger, opts ...WorkspaceClientOption) *WorkspaceClient
NewWorkspaceClient creates an AgentClient that resolves workspace → podIP + password on each call. The httpClient is shared across all calls for connection pooling; agentPort defaults to agentd.AgentPort.
func (*WorkspaceClient) DisposeInstance ¶
func (w *WorkspaceClient) DisposeInstance(ctx context.Context, userID, workspaceID string) error
func (*WorkspaceClient) GetSessionStatuses ¶
func (*WorkspaceClient) ListModels ¶
func (*WorkspaceClient) PatchConfig ¶
func (*WorkspaceClient) StageCredentials ¶
func (w *WorkspaceClient) StageCredentials(ctx context.Context, userID, workspaceID string, providers []secrets.LLMProviderData) error
type WorkspaceClientOption ¶
type WorkspaceClientOption func(*WorkspaceClient)
WorkspaceClientOption configures a WorkspaceClient at construction.
func WithWorkspaceHTTPClient ¶
func WithWorkspaceHTTPClient(hc *http.Client) WorkspaceClientOption
WithWorkspaceHTTPClient injects a shared *http.Client so connections are pooled across all workspace calls (M11-a). When unset, a tuned default is used (see newTunedHTTPClient). The client must not set a per-request Timeout that would interfere with caller context deadlines.