Documentation
¶
Overview ¶
Package system implements executor.Provider using local OS processes and a chrooted billy filesystem per agent.
Index ¶
- Variables
- func BuildLockedEnv(agentRoot string) []string
- type Agent
- func (a *Agent) Addr() string
- func (a *Agent) ListSessionMessages(ctx context.Context, sessionID string, limit int) ([]executor.SessionMessage, error)
- func (a *Agent) Prepare(ctx context.Context) error
- func (a *Agent) Prompt(ctx context.Context, req executor.PromptRequest, w io.Writer) error
- func (a *Agent) PromptObject(ctx context.Context, req executor.ObjectPromptRequest) ([]byte, error)
- func (a *Agent) PromptStream(ctx context.Context, req executor.PromptRequest, cb func(executor.PromptEvent)) error
- func (a *Agent) ReapplyMounts() error
- func (a *Agent) Remove(ctx context.Context) error
- func (a *Agent) Run(ctx context.Context) error
- func (a *Agent) Runtime() *executor.Runtime
- func (a *Agent) Start(ctx context.Context) error
- func (a *Agent) Status() executor.Status
- func (a *Agent) Stop(ctx context.Context) error
- func (a *Agent) SubscribeStatus() (<-chan executor.Status, func())
- func (a *Agent) UUID() uuid.UUID
- type AgentOption
- func WithAddr(addr string) AgentOption
- func WithAgentLocalRuntime(path string) AgentOption
- func WithAgentPuller(p agentoci.Puller) AgentOption
- func WithDialer(d Dialer) AgentOption
- func WithDigestResolver(r DigestResolver) AgentOption
- func WithImageRef(ref string) AgentOption
- func WithModelResolver(r model.Resolver) AgentOption
- func WithMounts(m []Mount) AgentOption
- func WithOverrides(overrides ...spec.Override) AgentOption
- func WithReference(ref spec.Reference) AgentOption
- func WithSpawner(s Spawner) AgentOption
- func WithStaticModelResolver(apiURL, apiKey string) AgentOption
- func WithStderr(w io.Writer) AgentOption
- func WithStdout(w io.Writer) AgentOption
- func WithStore(s oras.ReadOnlyTarget) AgentOption
- type Cmd
- type Dialer
- type DialerFunc
- type DigestResolver
- type LoopbackAllocator
- type Mount
- type Provider
- func (a *Provider) Create(ctx context.Context, id uuid.UUID, ref spec.Reference, opts ...spec.Override) (executor.Agent, error)
- func (a *Provider) CreateWithOptions(_ context.Context, id uuid.UUID, ref spec.Reference, extra []AgentOption, ...) (executor.Agent, error)
- func (a *Provider) Destroy(_ context.Context) error
- func (a *Provider) Load(_ context.Context) ([]executor.Agent, error)
- type ProviderOption
- func WithAgentDefaults(opts ...AgentOption) ProviderOption
- func WithHostFS(fs billy.Filesystem) ProviderOption
- func WithLocalRuntime(path string) ProviderOption
- func WithLogDir(dir string) ProviderOption
- func WithLoopbackAllocator(a LoopbackAllocator) ProviderOption
- func WithPuller(p agentoci.Puller) ProviderOption
- type Spawner
- type StoreFor
Constants ¶
This section is empty.
Variables ¶
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.
var ErrPull = fmt.Errorf("agent pull error")
ErrPull indicates the agent image could not be loaded from the OCI store.
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 ¶
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 ¶
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 ¶
Prepare materializes the workspace synchronously. Idempotent and safe to call concurrently; repeat callers observe the same error.
func (*Agent) Prompt ¶
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 ¶
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 ¶
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 ¶
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 ¶
Run materializes the workspace (if needed), starts the serve process, and blocks.
func (*Agent) Runtime ¶
Runtime returns the resolved runtime descriptor populated at Prepare/materialize time. Nil before Prepare has succeeded.
func (*Agent) Start ¶
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 ¶
Status returns the current lifecycle state. Safe for concurrent access with Start/Stop/Remove.
func (*Agent) Stop ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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.