system

package
v1.0.0-alpha.8 Latest Latest
Warning

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

Go to latest
Published: May 4, 2026 License: MIT Imports: 27 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 BuildLockedEnv

func BuildLockedEnv(agentRoot string) []string

BuildLockedEnv returns the curated environment the runtime subprocess (and every tool descendant) should see. Notably:

  • PATH points only at <agent-root>/usr/bin so tool subprocesses find the agent's pinned BIN binaries, never the host's.
  • HOME and XDG_* live inside the agent tree so tools that touch ~/.cache / ~/.config (wget's HSTS file, curl's netrc, etc.) write inside the agent root rather than polluting the operator's real home.
  • TMPDIR points inside the agent root.
  • LANG=C.UTF-8 for predictable locale-dependent output.
  • OTTERS_AGENT_ROOT lets `sh -c` invocations address the agent tree without hardcoding the path.

Notably NOT inherited: the host's PATH, HOME, SSH_AUTH_SOCK, AWS_*, GITHUB_TOKEN, anything else. Each host-process secret stays on the host.

Callers append per-provider credential entries (<PROVIDER>_API_KEY / <PROVIDER>_API_BASE) on top of this base; those values live on the resolved Runtime, not on the host environment.

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 []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 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 Mount

type Mount struct {
	Host        string
	Target      string
	Description string
}

Mount is a host-path → chroot-target binding, symlinked into the agent's workspace at materialize time and re-applied on restore. Host must be absolute; target is chroot-relative. Description is optional and surfaces to the LLM via the generated MOUNTS.md context layer.

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.

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.

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