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) ListModels(ctx context.Context) ([]byte, error)
- func (c *Client) PatchConfig(ctx context.Context, config map[string]any) error
- func (c *Client) PushCredentials(ctx context.Context, providers []secrets.LLMProviderData) error
- func (c *Client) StageCredentials(ctx context.Context, providers []secrets.LLMProviderData) error
- 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 OpenCodeAgent
- type Option
- type PasswordResolver
- type PodIPResolver
- 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 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) 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) 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 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 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 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.