system

package
v1.0.0-alpha.9 Latest Latest
Warning

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

Go to latest
Published: May 8, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Overview

Package system implements executor.Provider using local OS processes and a chrooted billy filesystem per agent.

Index

Constants

This section is empty.

Variables

View Source
var ErrModel = fmt.Errorf("model resolve error")

ErrModel indicates the model resolver could not provide credentials for the agentfile's declared model — typically a missing provider entry in ~/.otters/providers.yaml or a model not in the provider's allowlist.

View Source
var ErrPull = fmt.Errorf("agent pull error")

ErrPull indicates the agent image could not be loaded from the OCI store.

View Source
var (
	RuntimeBin = filepath.Join(runtimeBinDir, "runtime")
)

Path constants for the agent workspace filesystem layout. They use filepath.Join (cross-platform separators), so they can't be declared as `const` — Go const requires compile-time literals.

Functions

func MaterializeContent

func MaterializeContent(
	ctx context.Context,
	fs billy.Filesystem,
	id uuid.UUID,
	addr string,
	opts MaterializeOptions,
) (*executor.Runtime, error)

MaterializeContent runs the system executor's content-only materialisation pipeline against fs: pulls the agent image, extracts CONTEXT + ADD layers, generates AGENT.md / WORKSPACE.md / agent.yaml, resolves model credentials, applies user mount symlinks. Does NOT install runtime / BIN binaries to disk — that step is system-only.

Exposed for the Docker executor to reuse the same FHS layout + metadata generation without copying binaries the container will pick up via image mounts instead.

Errors join ErrPull / ErrModel where appropriate so callers can route them to the right Status (StatusPullError / StatusModelError).

func NewRegistry

func NewRegistry(target oras.Target, addr string, createdAt CreatedAtFunc) executor.Registry

NewRegistry returns an executor.Registry backed by the embedded HTTP registry. Exported so other executors (notably the docker executor) can compose a fallback for agent OCI artifacts — custom mediatypes that Docker's image store can't represent.

target is the oras.Target backing Fetch / Tag / BuildTarget; addr is the embedded registry's HTTP "host:port" used for catalog / manifest endpoints; createdAt is optional and may be nil.

Types

type Agent

type Agent struct {
	// contains filtered or unexported fields
}

Agent implements executor.Agent using local OS processes.

func NewAgent

func NewAgent(id uuid.UUID, fs billy.Filesystem, opts ...AgentOption) *Agent

NewAgent creates a system agent.

func (*Agent) Addr

func (a *Agent) Addr() string

Addr returns the loopback host:port the runtime subprocess will bind (reserved by the Provider's LoopbackAllocator at Create). Empty on agents constructed without WithAddr and before Prepare.

func (*Agent) ListSessionMessages

func (a *Agent) ListSessionMessages(
	ctx context.Context, sessionID string, limit int,
) ([]executor.SessionMessage, error)

ListSessionMessages fetches the recent messages for sessionID from the running runtime's memory store via its gRPC API. Satisfies executor.SessionReader. Requires WithAddr + a running runtime subprocess, same preconditions as Prompt.

func (*Agent) Prepare

func (a *Agent) Prepare(ctx context.Context) error

Prepare materializes the workspace synchronously. Idempotent and safe to call concurrently; repeat callers observe the same error.

func (*Agent) Prompt

func (a *Agent) Prompt(ctx context.Context, req executor.PromptRequest, w io.Writer) error

Prompt opens a ChatStream and writes the final assistant response into w, discarding intermediate tool/step/delta events. Prefer PromptStream when you need per-event progress.

func (*Agent) PromptObject

func (a *Agent) PromptObject(ctx context.Context, req executor.ObjectPromptRequest) ([]byte, error)

PromptObject runs a one-shot structured-output query against the runtime's LanguageModel. Stateless: no session memory, no tool loop. The runtime's PromptObject RPC handles parsing the JSON schema and marshalling the resulting object.

func (*Agent) PromptStream

func (a *Agent) PromptStream(ctx context.Context, req executor.PromptRequest, cb func(executor.PromptEvent)) error

PromptStream opens a ChatStream against the runtime's gRPC server and invokes cb synchronously for every event received. Returns when the stream closes or ctx is cancelled. Requires the agent to have been configured with WithAddr and the runtime to be running.

func (*Agent) ReapplyMounts

func (a *Agent) ReapplyMounts() error

ReapplyMounts re-runs the chroot symlink step + MOUNTS.md context write against the agent's existing filesystem. Used by Daemon.Restore for agents loaded from disk (which skip materialize), so mounts survive a daemon restart without requiring a full rebuild.

func (*Agent) Remove

func (a *Agent) Remove(ctx context.Context) error

Remove deletes the agent's workspace directory. Callers must Stop first; Remove does not stop a running agent. Returns any filesystem error.

func (*Agent) Run

func (a *Agent) Run(ctx context.Context) error

Run materializes the workspace (if needed), starts the serve process, and blocks.

func (*Agent) Runtime

func (a *Agent) Runtime() *executor.Runtime

Runtime returns the resolved runtime descriptor populated at Prepare/materialize time. Nil before Prepare has succeeded.

func (*Agent) Start

func (a *Agent) Start(ctx context.Context) error

Start re-runs a stopped agent on the already-materialized workspace. Blocks until the subprocess exits or ctx is cancelled (same contract as Run). Returns an error if the agent is already running or has been removed. The loopback address from the original Create is reused.

Before re-running, Start re-invokes the model resolver against the agent's resolved model, so providers.yaml edits made between Stop and Start (key rotation, api-base change) take effect on the next subprocess. Fresh-create agents (StatusCreated) bypass this branch — Run → Prepare → materialize will resolve on its own.

func (*Agent) Status

func (a *Agent) Status() executor.Status

Status returns the current lifecycle state. Safe for concurrent access with Start/Stop/Remove.

func (*Agent) Stop

func (a *Agent) Stop(ctx context.Context) error

Stop signals the running agent to exit and blocks until Run has returned or ctx is cancelled. Returns ctx.Err() if ctx is cancelled before Run finishes. A no-op if the agent is not running.

func (*Agent) SubscribeStatus

func (a *Agent) SubscribeStatus() (<-chan executor.Status, func())

SubscribeStatus returns a channel of status transitions and a cancel function. Sends are non-blocking; slow subscribers may miss intermediate transitions — call Status() to resync. Always call cancel to avoid leaking the subscription.

func (*Agent) UUID

func (a *Agent) UUID() uuid.UUID

UUID returns the agent's stable identifier, unchanged across Start/Stop/Restore cycles.

type AgentOption

type AgentOption func(*Agent)

AgentOption configures an individual agent.

func WithAddr

func WithAddr(addr string) AgentOption

WithAddr sets the gRPC listen address for the agent.

func WithAgentLocalRuntime

func WithAgentLocalRuntime(path string) AgentOption

WithAgentLocalRuntime sets a local runtime binary path on the agent.

func WithAgentPuller

func WithAgentPuller(p agentoci.Puller) AgentOption

WithAgentPuller sets the OCI puller on the agent.

func WithDialer

func WithDialer(d Dialer) AgentOption

WithDialer overrides the gRPC dialer used to reach the runtime subprocess. Production uses defaultDialer (grpc.NewClient + WaitForStateChange until Ready); tests inject a bufconn-backed dialer so Prompt / PromptStream / PromptObject / ListSessionMessages can be exercised without a real subprocess.

func WithDigestResolver

func WithDigestResolver(r DigestResolver) AgentOption

WithDigestResolver wires a digest resolver into the workspace. The resolver is consulted for the agent's own image, the RUNTIME image, and every BIN tool, with the results written into the resolved agent.yaml's provenance block + per-tool ref/digest fields.

func WithImageRef

func WithImageRef(ref string) AgentOption

WithImageRef tells the workspace which image ref produced this agent. Without it, agent.yaml's provenance.image_digest stays empty because the workspace materialiser otherwise has no canonical "this is the agent image" handle — the daemon does.

func WithModelResolver

func WithModelResolver(r model.Resolver) AgentOption

WithModelResolver sets the model resolver for API credential resolution.

func WithMounts

func WithMounts(m []executor.Mount) AgentOption

WithMounts attaches bind-mount specs to the agent. The symlinks are created by workspace.applyMounts at the end of materialize (or via Agent.ReapplyMounts on restore).

func WithOverrides

func WithOverrides(overrides ...spec.Override) AgentOption

WithOverrides sets spec-level overrides (model, runtime, etc.).

func WithReference

func WithReference(ref spec.Reference) AgentOption

WithReference sets the OCI reference for the agent image.

func WithSpawner

func WithSpawner(s Spawner) AgentOption

WithSpawner overrides the process spawner used to launch the runtime binary. Production uses defaultSpawner (real os/exec); tests inject a mock so process.serve can be exercised end-to-end without spawning anything real.

func WithStaticModelResolver

func WithStaticModelResolver(apiURL, apiKey string) AgentOption

WithStaticModelResolver sets a static API URL and key for model resolution.

func WithStderr

func WithStderr(w io.Writer) AgentOption

WithStderr sets the writer for agent stderr.

func WithStdout

func WithStdout(w io.Writer) AgentOption

WithStdout sets the writer for agent stdout.

func WithStore

func WithStore(s oras.ReadOnlyTarget) AgentOption

WithStore sets the OCI store for loading the agentfile.

type Cmd

type Cmd interface {
	Start() error
	Wait() error
	Signal(sig os.Signal) error
	SetStdout(w io.Writer)
	SetStderr(w io.Writer)
	SetEnv(env []string)
	// SetDir sets the working directory for the spawned process.
	// Empty string keeps the parent's CWD. Used by process.serve to
	// chdir into <agent-root>/workspace before spawn so tools that
	// take relative paths (`cat ./foo`, `find . -type f`) resolve
	// inside the agent root regardless of where ottersd was started.
	SetDir(dir string)
}

Cmd is the narrow slice of *os/exec.Cmd that process.serve actually needs: start, wait, deliver a signal, and wire stdout/stderr. The default implementation adapts *os/exec.Cmd.

Signal implementations must be safe to call before Start and after Wait; in both cases they should return an error rather than panic, so process.signal's best-effort behaviour (signal then Kill on error) remains deterministic.

type CreatedAtFunc

type CreatedAtFunc func(repo, tag string) int64

CreatedAtFunc returns the unix-seconds when the manifest at (repo, tag) was first written to the embedded registry. The daemon implements this against its on-disk blob mtime; tests may pass nil and accept CreatedUnix=0 in returned ImageInfo.

type Dialer

type Dialer interface {
	Dial(ctx context.Context, addr string) (*grpc.ClientConn, error)
}

Dialer opens a gRPC connection to the agent runtime subprocess. Abstracted so tests can substitute a bufconn-backed dialer and exercise Prompt / PromptObject / ListSessionMessages without spawning a real subprocess. The default is defaultDialer, which matches the pre-interface behaviour (insecure.Credentials + WaitForStateChange within dialReadyTimeout).

type DialerFunc

type DialerFunc func(ctx context.Context, addr string) (*grpc.ClientConn, error)

DialerFunc lets a plain function satisfy Dialer — typical for bufconn-backed test dialers where the addr is ignored.

func (DialerFunc) Dial

func (d DialerFunc) Dial(ctx context.Context, addr string) (*grpc.ClientConn, error)

Dial calls d(ctx, addr).

type DigestResolver

type DigestResolver func(ref string) string

DigestResolver returns the OCI digest of an image reference (or the empty string if the resolver can't answer). Used by workspace materialisation to record provenance into agent.yaml.

type LoopbackAllocator

type LoopbackAllocator interface {
	Reserve() (string, error)
}

LoopbackAllocator reserves a host:port the runtime subprocess will bind for its gRPC server. Abstracted behind an interface so tests don't have to bind real TCP ports (which causes port conflicts and CI flake).

Implementations return absolute addresses like "127.0.0.1:51234". The returned port is expected to be free at the moment Reserve returns, but the window between Reserve and the runtime's bind is inherently racy — good enough for dev; production deployments should inherit a listener fd instead.

type MaterializeOptions

type MaterializeOptions struct {
	Store          oras.ReadOnlyTarget
	Ref            spec.Reference
	Overrides      []spec.Override
	OCIPuller      agentoci.Puller
	ModelResolver  model.Resolver
	DigestResolver DigestResolver
	ImageRef       string
	LocalRuntime   string
	Mounts         []executor.Mount
	HostFS         billy.Filesystem
	// ToolBinaryPath optionally returns an absolute in-container
	// path for tool `name`. The docker executor returns
	// `/opt/bins/<name>/<name>` so the runtime resolves the binary
	// against the read-only image-mount instead of the bind-mounted
	// agent root (which doesn't carry the binary on disk).
	ToolBinaryPath func(name string) string
}

MaterializeOptions packages the inputs MaterializeContent needs. Fields mirror the system workspace's internal struct so the executor that lives elsewhere (today: the Docker executor) can drive the same materialise pipeline without re-implementing it.

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

Provider implements executor.Provider using the local filesystem. It prepares the infrastructure (chroot dirs) but does not materialize agents.

func NewProvider

func NewProvider(root billy.Filesystem, storeFor StoreFor, opts ...ProviderOption) *Provider

NewProvider creates a system Provider. storeFor is called once per Create to produce the oras.ReadOnlyTarget backing that agent's image.

func (*Provider) Create

func (a *Provider) Create(
	ctx context.Context, id uuid.UUID, ref spec.Reference, opts ...spec.Override,
) (executor.Agent, error)

Create prepares a chroot directory, reserves a loopback port for the runtime's gRPC server, and returns an Agent bound to both. The agent materializes itself on first Run.

func (*Provider) CreateWithOptions

func (a *Provider) CreateWithOptions(
	_ context.Context, id uuid.UUID, ref spec.Reference,
	extra []AgentOption, overrides ...spec.Override,
) (executor.Agent, error)

CreateWithOptions is Create plus a slice of per-instance AgentOption values (mounts, custom stdout, …). Kept as an extra method so the executor.Provider interface stays narrow — only the system provider understands these options; other providers keep working unchanged.

func (*Provider) Destroy

func (a *Provider) Destroy(_ context.Context) error

Destroy removes all agent chroot directories.

func (*Provider) Load

func (a *Provider) Load(_ context.Context) ([]executor.Agent, error)

Load recovers previously created agents from existing chroot directories.

func (*Provider) Registry

func (a *Provider) Registry() executor.Registry

Registry returns the executor.Registry façade for this Provider. Lazily constructed on first call so callers that never need it pay no cost.

type ProviderOption

type ProviderOption func(*Provider)

ProviderOption configures the system Provider.

func WithAgentDefaults

func WithAgentDefaults(opts ...AgentOption) ProviderOption

WithAgentDefaults sets default AgentOptions applied to every created/loaded agent.

func WithHostFS

func WithHostFS(fs billy.Filesystem) ProviderOption

WithHostFS overrides the non-chrooted billy filesystem the Provider (and the agents it creates) use for real host paths — mount symlink targets, local-runtime source files, and the log directory. Default is osfs.New("/"); tests pass memfs.New() to keep everything in memory. Applied at Provider level so newly-created agents inherit the same hostFS automatically.

func WithLocalRuntime

func WithLocalRuntime(path string) ProviderOption

WithLocalRuntime overrides the runtime binary with a local path (skips OCI pull).

func WithLogDir

func WithLogDir(dir string) ProviderOption

WithLogDir redirects each agent's runtime stdout/stderr to <dir>/<agent-id>.log (append mode). Set by consumers (like the openotters daemon) that want the subprocess output captured on disk rather than bleeding into their own stdout.

func WithLoopbackAllocator

func WithLoopbackAllocator(a LoopbackAllocator) ProviderOption

WithLoopbackAllocator overrides the default net.Listen-based allocator. Tests use it to hand out deterministic addresses without binding real ports. The default is defaultLoopbackAllocator, which binds 127.0.0.1:0 and closes the listener immediately.

func WithPuller

func WithPuller(p agentoci.Puller) ProviderOption

WithPuller sets the OCI puller for pulling runtime and tool binaries.

func WithRegistryAddr

func WithRegistryAddr(addr string) ProviderOption

WithRegistryAddr supplies the embedded registry's HTTP address ("host:port") so the system Registry can do List / Inspect / Remove via the OCI distribution spec endpoints. Empty string means those methods return ErrNotImplemented.

func WithRegistryCreatedAt

func WithRegistryCreatedAt(fn CreatedAtFunc) ProviderOption

WithRegistryCreatedAt supplies a callback returning the unix-seconds when a manifest was first written to the embedded registry. The daemon's EmbeddedRegistry has this info via on-disk mtime. Optional — without it, ImageInfo.CreatedUnix is 0.

func WithRegistryTarget

func WithRegistryTarget(target oras.Target) ProviderOption

WithRegistryTarget supplies the oras.Target the system Registry façade reads from / writes to. The daemon plumbs in its embedded registry's target here. Tests can omit it; the Registry methods then return ErrNotImplemented.

type Spawner

type Spawner interface {
	Command(name string, args ...string) Cmd
}

Spawner produces a Cmd for a given binary + args. Abstracted so tests can substitute a scripted stub instead of spawning real subprocesses. Default implementation is defaultSpawner, which wraps os/exec.Command.

This is the *system* executor's process-spawn seam — narrow on purpose. The pluggable executor boundary (system / docker / …) lives one level up at agent.Provider; nothing outside the system package should implement Spawner.

type StoreFor

type StoreFor func(ref spec.Reference) oras.ReadOnlyTarget

StoreFor returns an OCI target backing a specific agent's ref. The Provider invokes it once per Create, so each agent gets a target scoped to its own image — typically a remote.Repository bound to the agent's repo in the caller's local registry.

Jump to

Keyboard shortcuts

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