worker

package
v0.22.142 Latest Latest
Warning

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

Go to latest
Published: May 2, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package worker — daemon-side client for the sandbox worker (ADR-029 phase 1).

The daemon dials the worker once at first tool call and re-uses the connection for the lifetime of the dispatch. Phase 1 keeps a single connection per Client; multiple concurrent tool calls serialise through it. Phase 2 will pool connections.

Package worker — process-wide singleton client used by tool handlers (Bash / Read / Edit / Write) to route through the sandbox worker when configured.

The lifecycle: server.go's buildMCPServer reads cfg.SandboxWorker at boot, calls SetGlobal once if Mode != "off", and tool handlers consult Global() per call. nil global = host fallback (legacy behaviour preserved).

Package worker — sandbox-worker protocol shapes (ADR-029).

The worker is the second leg of clawtool's orchestrator+worker pair. The daemon dials the worker over a single bearer-auth'd WebSocket; tool calls (Bash / Read / Edit / Write / Glob / Grep) route through Request frames. Wire format: JSON-line over WS, one request → one response, no streaming primitive in Phase 1 (large outputs cap at 4 MiB and truncate; matches BIAM runner's existing readCapped policy).

Two design choices worth reading the ADR for:

  1. Daemon dials worker, NOT the reverse. claude.ai's mimic uses the same asymmetry — the orchestrator owns the connection lifetime. The worker is a passive listener that accepts a single trusted dial.
  2. Same binary serves both roles. `clawtool serve` is the daemon; `clawtool sandbox-worker` is the worker. Shared codebase = shared semantics for tool calls.

Package worker — sandbox-worker server (ADR-029 phase 1).

Listens on a single TCP port, accepts one bearer-authenticated WebSocket dial from the daemon, dispatches Request frames to per-kind handlers, writes Response frames back. Closes the listener after the first client (single-tenant by design; future phase will pool workers per-conversation).

Index

Constants

View Source
const ProtocolVersion = "1"

ProtocolVersion bumps when wire format breaks. Phase 1 = "1".

Variables

View Source
var ErrUnconfigured = errors.New("worker: not configured (sandbox.worker.mode=off)")

ErrUnconfigured signals the daemon's tool path that no worker is wired (mode=off). Caller falls back to host execution.

Functions

func DefaultTokenPath

func DefaultTokenPath() string

DefaultTokenPath honours XDG conventions for the worker token file. Mirrors internal/cli/sandbox_worker.go's helper but duplicated here so daemon-side code doesn't import internal/cli (would create a cycle).

func EncodeRequest

func EncodeRequest(r *Request) ([]byte, error)

EncodeRequest marshals one request to a single JSON line.

func LoadToken

func LoadToken(path string) (string, error)

LoadToken reads the bearer token from path with the same trimming rules the worker server uses on its end. Empty file or missing file returns ("", error).

func MarshalBody

func MarshalBody(v any) (json.RawMessage, error)

MarshalBody is sugar for typed-payload → RawMessage.

func Run

func Run(ctx context.Context, opts ServerOptions) error

Run is the worker's main entrypoint. Blocks until ctx is cancelled or the listener errors out fatally.

func SetGlobal

func SetGlobal(c *Client)

SetGlobal registers the daemon-wide worker client. Pass nil to disable. Idempotent.

func UnmarshalBody

func UnmarshalBody(raw json.RawMessage, v any) error

UnmarshalBody is the inverse — Request.Body → typed payload.

Types

type Client

type Client struct {
	URL   string // ws://host:port/ws
	Token string
	// contains filtered or unexported fields
}

Client is the daemon's handle on a sandbox worker. Goroutine- safe: Send serialises through a mutex.

func Global

func Global() *Client

Global returns the registered client, or nil when worker mode is off / unconfigured. Tool handlers MUST handle nil by falling back to host execution — this is the contract that keeps `mode=off` backward-compatible.

func NewClient

func NewClient(url, token string) *Client

NewClient returns an unconnected client. Dial happens lazily on first Send.

func (*Client) Close

func (c *Client) Close()

Close drops the underlying WebSocket. Safe to call repeatedly.

func (*Client) Exec

func (c *Client) Exec(ctx context.Context, req ExecRequest) (*ExecResponse, error)

Exec routes a Bash tool call to the worker. Mirrors the host path's semantics so the daemon can route transparently.

func (*Client) Ping

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

Ping verifies the worker is reachable + auth is correct. Returns nil on success.

func (*Client) Read

func (c *Client) Read(ctx context.Context, req ReadRequest) (*ReadResponse, error)

Read routes a Read tool call.

func (*Client) Write

func (c *Client) Write(ctx context.Context, req WriteRequest) (*WriteResponse, error)

Write routes a Write tool call.

type ExecRequest

type ExecRequest struct {
	Command   string            `json:"command"`
	Cwd       string            `json:"cwd,omitempty"`
	Env       map[string]string `json:"env,omitempty"`
	TimeoutMs int               `json:"timeout_ms,omitempty"` // hard wall-clock cap
}

ExecRequest mirrors mcp__clawtool__Bash's input shape so the daemon can transparently route Bash tool calls here.

type ExecResponse

type ExecResponse struct {
	Stdout     string `json:"stdout"`
	Stderr     string `json:"stderr"`
	ExitCode   int    `json:"exit_code"`
	DurationMs int64  `json:"duration_ms"`
	TimedOut   bool   `json:"timed_out"`
	Cwd        string `json:"cwd"`
}

ExecResponse mirrors clawtool's structured Bash output shape.

type GlobRequest

type GlobRequest struct {
	Pattern string `json:"pattern"`
	Cwd     string `json:"cwd,omitempty"`
	Limit   int    `json:"limit,omitempty"`
}

type GlobResponse

type GlobResponse struct {
	Matches []string `json:"matches"`
	Count   int      `json:"count"`
}

type GrepHit

type GrepHit struct {
	Path string `json:"path"`
	Line int    `json:"line"`
	Text string `json:"text"`
}

type GrepRequest

type GrepRequest struct {
	Pattern string `json:"pattern"`
	Path    string `json:"path,omitempty"`
	Glob    string `json:"glob,omitempty"`
}

type GrepResponse

type GrepResponse struct {
	Matches []GrepHit `json:"matches"`
	Count   int       `json:"count"`
}

type Kind

type Kind string

Kind enumerates the request types the worker handles. Adding new kinds is a wire-format break — bump the protocol version.

const (
	KindExec  Kind = "exec"
	KindRead  Kind = "read"
	KindWrite Kind = "write"
	KindGlob  Kind = "glob"
	KindGrep  Kind = "grep"
	KindStat  Kind = "stat"
	KindPing  Kind = "ping"
)

type ReadRequest

type ReadRequest struct {
	Path      string `json:"path"`
	LineStart int    `json:"line_start,omitempty"`
	LineEnd   int    `json:"line_end,omitempty"`
}

type ReadResponse

type ReadResponse struct {
	Content    string `json:"content"`
	TotalLines int    `json:"total_lines"`
	SizeBytes  int64  `json:"size_bytes"`
	FileHash   string `json:"file_hash,omitempty"`
}

type Request

type Request struct {
	V    string          `json:"v"`              // protocol version
	ID   string          `json:"id"`             // caller-assigned request id (uuid recommended)
	Kind Kind            `json:"kind"`           // operation
	Body json.RawMessage `json:"body,omitempty"` // per-kind payload
}

Request is the inbound shape on the worker WebSocket. ID is caller-assigned; responses echo it back so a client can pipeline multiple requests onto one connection (Phase 2).

func DecodeRequest

func DecodeRequest(b []byte) (*Request, error)

DecodeRequest parses one JSON line. Caller must have already authenticated the WebSocket frame.

type Response

type Response struct {
	V      string          `json:"v"`
	ID     string          `json:"id"`
	Status int             `json:"status"`
	Body   json.RawMessage `json:"body,omitempty"`
	Error  string          `json:"error,omitempty"`
}

Response is the outbound shape. Either Body OR Error is populated, never both. Status mirrors HTTP-ish conventions: 0 = ok, 1 = caller error, 2 = worker internal error.

type ServerOptions

type ServerOptions struct {
	Listen   string // ":2024" or "127.0.0.1:0" (port 0 = pick a free port)
	Token    string // bearer token; clients must present `Authorization: Bearer <token>`
	Workdir  string // root the worker resolves relative paths against; default cwd
	MaxBytes int    // per-response cap (default 4 MiB)
}

ServerOptions configures the worker's listener.

type StatRequest

type StatRequest struct {
	Path string `json:"path"`
}

type StatResponse

type StatResponse struct {
	Exists  bool   `json:"exists"`
	IsDir   bool   `json:"is_dir"`
	Size    int64  `json:"size,omitempty"`
	ModeStr string `json:"mode,omitempty"`
}

type WriteRequest

type WriteRequest struct {
	Path    string `json:"path"`
	Content string `json:"content"`
	Mode    string `json:"mode,omitempty"` // "overwrite" | "create"
}

type WriteResponse

type WriteResponse struct {
	BytesWritten int  `json:"bytes_written"`
	Created      bool `json:"created"`
}

Jump to

Keyboard shortcuts

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