system

package
v1.0.0-alpha.47 Latest Latest
Warning

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

Go to latest
Published: May 19, 2026 License: MIT Imports: 34 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) DeleteNote

func (a *Agent) DeleteNote(ctx context.Context, key string) (bool, error)

func (*Agent) DeleteSession

func (a *Agent) DeleteSession(ctx context.Context, sessionID string) error

DeleteSession drops sessionID from the runtime's session store. Satisfies executor.SessionDeleter.

func (*Agent) Exec

func (a *Agent) Exec(ctx context.Context, bin string, args []string, stdin string) executor.ExecResult

Exec runs a BIN subprocess against the agent's already-materialized spawn env: same locked-down env vars, same workspace cwd, same PATH pointing at <rootDir>/usr/bin (where the agent's declared BINs are installed). Returns when the process exits or ctx is cancelled.

The child runs in its own process group (Setpgid). On ctx cancellation we send SIGKILL to the whole group so any sub-processes the BIN itself spawned (`sh -c "a | b"` style) die with it — no orphaned grandchildren.

The agent MUST be initialized (via Run / Start having reached the running state at least once) before Exec is called: the runtime descriptor populated at initialize time supplies the env. Calling Exec on an un-initialized agent returns an error in ExecResult.Err.

func (*Agent) FailureReason

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

FailureReason returns the cause when Status() == StatusFailed. Returns FailureNone for non-failed states.

func (*Agent) GetNote

func (a *Agent) GetNote(ctx context.Context, key string) (executor.Note, error)

func (*Agent) ListNotes

func (a *Agent) ListNotes(ctx context.Context, onlyInContext bool) ([]executor.Note, error)

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) ListSessions

func (a *Agent) ListSessions(ctx context.Context) ([]executor.SessionInfo, error)

ListSessions enumerates every session known to the runtime. Satisfies executor.SessionLister.

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.

Status transitions on the way through:

  • On entry: Pulling (set by Run; Prepare leaves it alone so a standalone Prepare call still surfaces the pull window).
  • On success: caller (Run) flips to Starting.
  • On error: Failed with a FailureReason derived from the error sentinel (FailurePull / FailureModel / FailureInit).

func (*Agent) Probe

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

Probe issues a single Ready() RPC against the running runtime. Returns nil when the runtime answers Ready=true; any dial failure, transport error, or Ready=false response is surfaced as an error.

The daemon supervisor calls Probe in a retry loop after Run sets StatusStarting; the first successful Probe transitions the agent to StatusReady.

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.

Lifecycle: Pulling (entry) → Starting (after Prepare returns) → (daemon supervisor flips to Ready once the readiness probe answers) → Stopped on subprocess exit (the daemon decides whether to retry or mark Failed+FailureCrashed).

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) SaveNote

func (a *Agent) SaveNote(
	ctx context.Context, key, content string, maxBytes, maxCount int32,
) (executor.SaveResult, error)

func (*Agent) SetAgentToken

func (a *Agent) SetAgentToken(token string)

SetAgentToken swaps the JWT injected into the runtime's spawn env on subsequent Run/Start calls. The currently-running process keeps its current token until restart; the openotters daemon's refresh path always pairs this with a Stop+Start so the new value reaches the runtime. Guarded by initMu — the same lock that serialises every other read of a.agentToken via buildCmdFn.

func (*Agent) SetNoteInContext

func (a *Agent) SetNoteInContext(ctx context.Context, key string, inContext bool) (executor.Note, error)

func (*Agent) Start

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

Start re-runs a stopped (or failed) agent. Blocks until the subprocess exits or ctx is cancelled (same contract as Run). Returns an error if the agent is already pulling / starting / ready / working / 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 agents (still in StatusPulling) 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) StatusTracker

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

StatusTracker exposes the underlying tracker. The daemon supervisor uses it for the Ready / Working / readiness-timeout / crashed transitions it owns; tests reach for it to assert specific sequences without coupling to the Set/SetFailure call sites.

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 WithAgentToken

func WithAgentToken(token string) AgentOption

WithAgentToken sets the JWT minted by the daemon for this agent; injected into the spawn env as OTTERS_AGENT_TOKEN. The runtime presents it as `Authorization: Bearer …` on every outbound RPC. Empty disables the env var (runtime can still spawn — outbound daemon calls would fail Unauthenticated, which is the desired behaviour when the agent isn't supposed to call back).

func WithAgentUsageFetcher

func WithAgentUsageFetcher(f agentoci.UsageFetcher) AgentOption

WithAgentUsageFetcher sets the OCI usage fetcher on the agent. Symmetric with WithAgentPuller: scoped to a single Agent created directly (rather than via Provider, which propagates the Provider-level WithUsageFetcher).

func WithCapabilities

func WithCapabilities(caps []executor.Capability) AgentOption

WithCapabilities sets the list of LLM-facing tool functions the runtime image registers (job_submit, context_show, …) and their descriptions. The daemon supplies the list at create-time; the workspace plumbs it into ResolvedConfig.Capabilities and the AGENT.md generator surfaces it in the system prompt.

func WithDaemonURL

func WithDaemonURL(url string) AgentOption

WithDaemonURL sets the openotters daemon URL the runtime should dial back to. Format is backend-conventional: for the system executor, callers typically pass `unix://<host-path>` (the chrooted runtime can dial host paths directly since the chroot is billy-rooted, not a real syscall chroot). Empty disables the env var.

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
	UsageFetcher   agentoci.UsageFetcher
	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>` so the runtime resolves the symlink
	// stamped on the agent root (which the kernel then follows to
	// the read-only image-mount, hidden under /opt/bin-images).
	ToolBinaryPath func(name string) string

	// SymlinkBinAt, when non-nil, is invoked once per declared BIN
	// with (name, target) — name is the BIN's tool name (resolves
	// to opt/bins/<name> on the agent root), target is the
	// container-local string the symlink should point at (e.g.
	// /opt/bin-images/<name>/<name>). Only the docker executor sets
	// this; system installs the binary as a regular file under
	// usr/bin and doesn't need the indirection.
	SymlinkBinAt func(name string) (target string, ok bool)

	// Capabilities is the list of LLM-facing tool functions the
	// runtime image registers, each with a description. Surfaces
	// in agent.yaml's capabilities: block. Daemon-supplied — the
	// agentfile library itself doesn't know which tools exist or
	// what they do.
	Capabilities []executor.Capability

	// View is the agent-visible filesystem layout used to render
	// WORKSPACE.md. Zero-value uses system defaults derived from
	// fs.Root() (host path, no sandbox). The docker executor sets
	// a container-rooted view so the LLM doesn't see the host
	// path of its bind mount.
	View executor.WorkspaceView

	// ViewBinDirsForTools, when non-nil, fills View.BinDirs from
	// the resolved tool name list — invoked once after the spec is
	// parsed and tools are populated, before WORKSPACE.md is
	// rendered. Lets the docker executor produce one
	// `/opt/bins/<name>` entry per BIN without parsing the spec
	// twice.
	ViewBinDirsForTools func(toolNames []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.

func WithUsageFetcher

func WithUsageFetcher(f agentoci.UsageFetcher) ProviderOption

WithUsageFetcher sets the OCI fetcher used to read each BIN's USAGE.md body at materialisation time. Nil disables doc extraction — materializeContent still stamps the conventional path on each ResolvedTool so the runtime stays consistent, but no file is written and the loader silently skips the doc.

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