opencode

package
v0.5.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
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 1.15.12 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).

func Register

func Register()

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

func NewClient(baseURL, password string, logger *zap.Logger, opts ...Option) *Client

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

func (c *Client) DisposeInstance(ctx context.Context) error

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

func (c *Client) GetSessionStatuses(ctx context.Context) (map[string]string, error)

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

func (c *Client) ListModels(ctx context.Context) ([]byte, error)

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

func (c *Client) PatchConfig(ctx context.Context, config map[string]any) error

PatchConfig calls PATCH /global/config on opencode with the given config map. Used by SetModel to change the active model.

func (*Client) PushCredentials

func (c *Client) PushCredentials(ctx context.Context, providers []secrets.LLMProviderData) error

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

func (c *Client) StageCredentials(ctx context.Context, providers []secrets.LLMProviderData) error

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 (d *Dialect) EventStreamPath() string

func (*Dialect) IsPermissionAsked

func (d *Dialect) IsPermissionAsked(eventType string) bool

func (*Dialect) IsPermissionResolved

func (d *Dialect) IsPermissionResolved(eventType string) bool

func (*Dialect) IsQuestionAsked

func (d *Dialect) IsQuestionAsked(eventType string) bool

func (*Dialect) IsQuestionResolved

func (d *Dialect) IsQuestionResolved(eventType string) bool

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 (d *Dialect) ParseSessionStatus(properties json.RawMessage) (string, string, error)

func (*Dialect) PermissionListPath

func (d *Dialect) PermissionListPath() string

func (*Dialect) PermissionReplyPath

func (d *Dialect) PermissionReplyPath(requestID string) string

func (*Dialect) QuestionListPath

func (d *Dialect) QuestionListPath() string

func (*Dialect) QuestionRejectPath

func (d *Dialect) QuestionRejectPath(requestID string) string

func (*Dialect) QuestionReplyPath

func (d *Dialect) QuestionReplyPath(requestID string) string

func (*Dialect) SessionAbortPath

func (d *Dialect) SessionAbortPath(sessionID string) string

func (*Dialect) SessionCreatePath

func (d *Dialect) SessionCreatePath() string

func (*Dialect) SessionGetPath

func (d *Dialect) SessionGetPath(sessionID string) string

func (*Dialect) SessionListPath

func (d *Dialect) SessionListPath() string

func (*Dialect) SessionMessagePath

func (d *Dialect) SessionMessagePath(sessionID string) string

func (*Dialect) SessionPromptAsyncPath

func (d *Dialect) SessionPromptAsyncPath(sessionID string) string

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

func WithHTTPClient(hc *http.Client) Option

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

type PasswordResolver func(ctx context.Context, workspaceID string) (string, error)

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 (w *WorkspaceClient) GetSessionStatuses(ctx context.Context, userID, workspaceID string) (map[string]string, error)

func (*WorkspaceClient) ListModels

func (w *WorkspaceClient) ListModels(ctx context.Context, userID, workspaceID string) ([]byte, error)

func (*WorkspaceClient) PatchConfig

func (w *WorkspaceClient) PatchConfig(ctx context.Context, userID, workspaceID string, config map[string]any) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL