Documentation
¶
Overview ¶
Package stack is the supported single-process composition of Looprig: Factory (the public API and browser realtime link), a pooled Host running harness agents through host/harnessruntime, and one storage profile.
s, err := stack.Start(ctx, stack.Options{
Storage: store, // stack/localdisk, stack/memory, or shared
Tenants: []sessionwire.TenantID{"dev"},
Identity: dev.Identity(), // stack/devauth is DEVELOPMENT-ONLY
Agents: []stack.Agent{{
ID: "assistant", Compatibility: "my-app/assistant/v1",
Capabilities: department.Capabilities{CaptureSafety: department.CaptureSafetyStreaming},
Define: defineRig, // rig.WithSessionStore(binding.Journal), ...
}},
Origins: []string{"http://localhost:8080"},
Logger: logger, // enabled at WARN: devauth's banner
AllowDevelopmentIdentity: true, // the explicit devauth opt-in
})
defer s.Stop(shutdownCtx)
http.ListenAndServe(addr, s.Handler())
Factory still reaches the in-process Host over a loopback HostLink WebSocket, so the two talk only through Core records and SessionStore, as they would across machines. Moving the Host out is Hosts: RemoteHosts{} in the Factory process plus ServeHost in each Host process, over shared storage; Agents, Identity, UI and the rig code do not change.
What the stack fixes, so you cannot get it wrong ¶
Every one of these was a runtime failure a hand composition could make:
- The journal binding (JournalBindingID/JournalBindingVersion), the launch template per agent, the Host's settlement evidence router and Factory's journal resolver are derived from one source, so they cannot disagree ("places nothing", 404, runtime_unavailable).
- Factory's pending-command placement is always on; the Host is pooled, cross-tenant isolated, and advertises a BARE base.
- The HostLink credential is random per process under InProcess; the Host generation is a durable counter in Storage.Control, not the clock.
- Every runtime is harnessruntime.Target: the six required capabilities, AttemptCloser and PersistenceFaults (host's Durable profile), the live-options seam (so Live.IncludeReasoning/IncludeToolSteps work), principal and metadata copied onto every admitted command, a create's first message re-presented, and an unresolved payload reference refused (Host resolves referenced bodies before the attempt). Factory stamps the verified principal (factory.WithPrincipalStamping).
- Stop runs the one safe order, on its own lifecycle (the caller's context bounds only the wait): Factory Quiesce → Host Stop (drain while HostLink is still served; a runtime awaiting a gate is abandoned with its gate preserved, and a Parked one is a *ParkedSessionsError) → Factory Stop → HostLink listener → stores → Storage.Close.
Obligations that remain yours ¶
- SANDBOXED TOOLS ON LINUX: if any agent runs commands through github.com/looprig/sandbox, call sandbox.Init() as the very first statement of main. On Linux the sandbox re-executes the binary and refuses to build an executor without it; the stack cannot do this for you because it must run before anything else in the process.
- Pass Binding.Journal to rig.WithSessionStore in Agent.Define. A rig journaling elsewhere is unreadable to Host and to Factory.
- Change Agent.Compatibility whenever a new build cannot replay an old journal.
- Identity is yours. stack/devauth is for a developer's own machine only.
Stack v0.1.0 deliberately has no object policy (Factory's object route answers 503), no checkpointer (an agent that RequiresCheckpoint is refused) and no dedicated placement; Kubernetes placement is the controller's.
Index ¶
- Constants
- func ServeHost(ctx context.Context, o HostOptions) (*host.Service, http.Handler, error)
- func TokenVerifier(token string) host.AuthVerifier
- func Validate(o Options) error
- type Agent
- type Binding
- type HostLimits
- type HostMode
- type HostOptions
- type Identity
- type InProcess
- type Limits
- type NoRigError
- type OptionError
- type Options
- type ParkedSessionsError
- type RemoteHosts
- type Stack
- type Storage
- type UnknownJournalBindingError
- type UnknownTenantError
Constants ¶
const ( JournalBindingID = "stack-journal" JournalBindingVersion = "v1" )
The journal binding every session the stack creates is pinned to. Factory records it in the session's immutable catalog entry; Host's evidence router and Factory's journal resolver are keyed on it. It is fixed, not configurable: every stack names the same one, so any stack process over the same storage can read any stack session.
const MinHostLinkCredentialBytes = 32
MinHostLinkCredentialBytes is the shortest HostLinkCredential accepted.
Variables ¶
This section is empty.
Functions ¶
func ServeHost ¶
ServeHost composes and starts one Host for a RemoteHosts deployment, over the same Storage, Tenants and Agents the Factory process uses. It returns the running Service and the handler to mount on the listener o.Base names (HostLink, /readyz, /healthz, /metrics).
It does not own o.Storage. Shut down with the Service's Stop (which drains while HostLink is still served), then close the listener, then o.Storage.Close. On error nothing is left running, and o.Storage is still the caller's to close.
func TokenVerifier ¶
func TokenVerifier(token string) host.AuthVerifier
TokenVerifier is the host.AuthVerifier matching a static HostLink service token: the Host side of Options.Limits.HostLinkCredential. It compares in constant time and accepts the token for any tenant the Host serves (ServeHost refuses other tenants before asking it).
Types ¶
type Agent ¶
type Agent struct {
// ID is what a create request names. REQUIRED, distinct.
ID sessionwire.AgentID
// Compatibility names this agent BUILD. A session is restored only onto
// the same id, so change it when a new build cannot safely replay an old
// journal. REQUIRED.
Compatibility department.CompatibilityID
// Capabilities is what the agent declares about placement. The stack
// places every session pooled, so SupportsPooled is set for you;
// AdmissionWeight zero means 1; Recovery is forced on (the harness
// runtime supplies both capabilities by construction). CaptureSafety is
// REQUIRED and must permit pooling: department.CaptureSafetyStreaming
// when every tool's output is small or streamed, or
// department.CaptureSafetyBoundedMaterialized under a finite limit.
// RequiresCheckpoint is refused: stack v0.1.0 has no checkpointer.
Capabilities department.Capabilities
// Define builds the rig one launch runs on. REQUIRED. It is called for
// every create and every restore (rig.Define fixes the journal store and
// workspace, so a rig is per launch); memoise per tenant if your rig does
// not depend on the workspace. Pass Binding.Journal to
// rig.WithSessionStore, or the session journals nowhere Host can read.
Define func(context.Context, Binding) (*rig.Rig, error)
// Decode reads a create/input body. Optional; nil means Core's
// InputRequest (harnessruntime.DecodeInputBlocks), which is what Factory
// admits.
Decode harnessruntime.BlockDecoder
}
Agent is one agent a create may name, and how to build its rig.
type Binding ¶
type Binding struct {
Tenant sessionwire.TenantID
Session sessionwire.SessionID
// Journal is the tenant's harness session store; pass it to
// rig.WithSessionStore. Its ToolResultObjects() feeds
// rig.WithToolResultObjects.
Journal *harnessstore.Store
// WorkspaceRoot is this session's materialized workspace directory,
// <Storage.Workspaces>/<tenant>/<session>. It is "" unless the agent
// declares Capabilities.RequiresWorkspace: Host materializes a workspace
// only for a target that asks for one.
WorkspaceRoot string
// Restore is true when the launch restores an existing conversation.
Restore bool
}
Binding is what a rig needs that only the stack knows.
type HostLimits ¶
type HostLimits struct {
WarmTTL time.Duration // 10m: idle resident sessions are released after it
RegistryHeartbeat time.Duration // 2s
RegistryExpiry time.Duration // 10s; at least host.MinHeartbeatsBeforeExpiry heartbeats
ClaimTTL time.Duration // 5s
ApplyDeadline time.Duration // 60s; at least host.MinClaimAttemptsBeforeDeadline claims
CommandQueueSize int // 16; at most host.MaxCommandQueueSize
ReconcileInterval time.Duration // 1s
ReconcileBatch int // 32; at most host.MaxReconcileBatch
Link host.LinkOptions // zero fields: 64 bindings per link, 256 bindings, 4 links per tenant
// Drain bounds Stop's drain. Zero fields: grace 10s, idle boundary 5s,
// publish bound 2s; IdleBoundary and PublishBound must not exceed Grace.
//
// SIZE YOUR SHUTDOWN BUDGET ABOVE Grace + 2×IdleBoundary (20s at the
// defaults) — the Stop context you pass and the platform's termination
// grace (Kubernetes terminationGracePeriodSeconds, systemd
// TimeoutStopSec). A session awaiting a gate is never idle, so its
// release is refused for the whole Grace; Host then abandons the runtime
// crash-equivalently under a fresh IdleBoundary, waited one more if the
// abandon ignores cancellation. The gate is preserved and the journal
// lease released, so the next Start restores the session with the gate
// still answerable (Stack.DrainReport lists it as Abandoned). A shorter
// budget kills the process mid-abandon.
Drain host.DrainOptions
CompatibilityTimeout time.Duration // 20s
WorkPoll time.Duration // 1s
// MaxCommandBodyBytes bounds a referenced command body. Zero is Host's
// 8 MiB default.
MaxCommandBodyBytes int64
}
HostLimits tunes one Host. Zero fields take the single-process defaults named on each.
type HostMode ¶
type HostMode interface {
// contains filtered or unexported methods
}
HostMode selects where agents run: InProcess or RemoteHosts.
type HostOptions ¶
type HostOptions struct {
Storage Storage
Tenants []sessionwire.TenantID
Agents []Agent
// HostID names this Host. REQUIRED.
HostID sessionwire.HostID
// Generation is this run's incarnation. Zero means the next value of the
// counter the stack keeps in Storage.Control for HostID; an explicit value
// raises that counter to at least itself, so a later automatic start of
// the same HostID continues above it.
Generation uint64
// Base is the BARE HostLink base Factory dials (scheme and authority,
// no path); Factory derives each tenant's address from it. REQUIRED.
Base sessionwire.InternalEndpoint
// Credential verifies the token Factory presents. REQUIRED;
// TokenVerifier(Limits.HostLinkCredential) is the matching choice.
Credential host.AuthVerifier
// Capacity bounds resident sessions. Zero means 16.
Capacity uint64
Live *host.LiveTextOptions
Limits HostLimits
Logger *slog.Logger
}
HostOptions runs one Host process for a RemoteHosts deployment. Storage, Tenants and Agents are the same values the Factory process uses.
type Identity ¶
type Identity struct {
// Verifier verifies a presented credential. REQUIRED.
Verifier identity.Verifier
// Authorizer decides every public operation. REQUIRED.
// factory.TenantAuthorizer{} is the stock tenant-boundary choice.
Authorizer factory.Authorizer
// CSRF signs browser CSRF tokens. SharedKey is REQUIRED, at least
// identity.MinCSRFSharedKeyBytes, and must be the same bytes in every
// replica. TokenTTL zero means 12 hours. TrustedOrigins must be empty:
// use Options.Origins.
CSRF identity.CSRFConfig
// CookieName replaces the browser session cookie name. Optional.
CookieName string
// DevelopmentOnly marks a development identity (stack/devauth sets it).
// Validate then requires Options.AllowDevelopmentIdentity and an
// Options.Logger enabled at WARN, and Start logs a WARN banner on every
// start saying this process accepts development credentials.
DevelopmentOnly bool
}
Identity is how Factory authenticates and authorizes a request.
type InProcess ¶
type InProcess struct {
// Listen is the HostLink listen address. It must be loopback; "" means
// "127.0.0.1:0".
Listen string
// HostID names the Host. "" means "local".
HostID sessionwire.HostID
// Capacity bounds resident sessions. Zero means 16.
Capacity uint64
}
InProcess runs one pooled Host inside this process. Factory still reaches it over a loopback HostLink WebSocket, exactly as it would across machines, so moving to RemoteHosts changes nothing else.
type Limits ¶
type Limits struct {
// Host tunes the in-process Host (and ServeHost's).
Host HostLimits
// Reconcile, ClientLink, HostLink, HTTP and Routes replace Factory's
// defaults when non-nil. MaxCommandBodyBytes is Routes'.
Reconcile *factory.ReconcileLimits
ClientLink *factory.ClientLinkLimits
HostLink *factory.HostLinkLimits
HTTP *factory.HTTPLimits
Routes *factory.RouteLimits
// HostLinkCredential is the service token Factory presents to remote
// Hosts. REQUIRED under RemoteHosts (at least MinHostLinkCredentialBytes)
// and refused under InProcess, where the stack mints a random one per
// process.
HostLinkCredential string
}
Limits tunes the composition. Every zero field takes its default.
type NoRigError ¶
type NoRigError struct{ Agent sessionwire.AgentID }
NoRigError is an Agent.Define that returned neither a rig nor an error.
func (*NoRigError) Error ¶
func (e *NoRigError) Error() string
type OptionError ¶
OptionError is every refusal Validate (and so Start and ServeHost) makes before any I/O. Field is the Options path, for example "Agents[1].Define".
func (*OptionError) Error ¶
func (e *OptionError) Error() string
func (*OptionError) Unwrap ¶
func (e *OptionError) Unwrap() error
Unwrap returns the lower layer's refusal, if any.
type Options ¶
type Options struct {
// Storage is where every durable record lives. REQUIRED. Use
// stack/localdisk for one machine, stack/memory for tests, or assemble one
// over a shared backend (natsstore, pgstore+s3store) for RemoteHosts.
Storage Storage
// Tenants are the tenants this process serves, at least one, distinct.
// Each gets its own harness journal backend (Storage.Journal). With
// exactly one tenant it is also Factory's default tenant, so a credential
// that names no tenant is scoped to it.
Tenants []sessionwire.TenantID
// Identity authenticates and authorizes every public request. REQUIRED.
Identity Identity
// Agents are the agents a create may name, at least one, distinct IDs.
Agents []Agent
// Hosts selects where agents run. Nil means InProcess{}.
Hosts HostMode
// Live enables live previews (text, and optionally reasoning and tool
// steps) for every viewer. Nil keeps committed-only publication. Every
// runtime the stack launches implements the live-options seam, so
// IncludeReasoning and IncludeToolSteps need nothing else.
Live *host.LiveTextOptions
// UI is optional; Factory mounts it as the fallback after /v1, so an
// unknown API path is still a JSON 404.
UI http.Handler
// Origins are the browser origins trusted by CSRF and ClientLink. They
// are the ONLY source of trusted origins: Identity.CSRF.TrustedOrigins
// must be empty. At least one is REQUIRED (Factory's CSRF refuses none).
Origins []string
// Logger receives operator diagnostics. Optional, except that a
// development identity (Identity.DevelopmentOnly) refuses to start
// without one that is enabled at WARN, so its banner cannot be discarded.
Logger *slog.Logger
// AllowDevelopmentIdentity is the explicit opt-in a development identity
// (stack/devauth) requires. Without it Validate refuses such an identity,
// so development credentials are never enabled by a dependency alone.
AllowDevelopmentIdentity bool
// Limits tunes timings and bounds. Its zero value is the single-process
// default.
Limits Limits
}
Options is one single-process composition, described. Validate reports every refusal it can make without I/O; Start runs Validate first.
type ParkedSessionsError ¶
type ParkedSessionsError struct {
Sessions []host.DrainSession
}
ParkedSessionsError is a Stop that left runtimes running: their release was refused and they could not be abandoned, so they keep their journal leases until the PROCESS exits. A process that keeps running after Stop cannot restore these sessions and should treat this as a leak (exit, or alert). Every harnessruntime runtime can be abandoned, so under the stack this means an abandon failed or overran its bound (see the drain report's Failures and Drain.IdleBoundary).
func (*ParkedSessionsError) Error ¶
func (e *ParkedSessionsError) Error() string
type RemoteHosts ¶
type RemoteHosts struct{}
RemoteHosts runs Factory only; Hosts run in other processes via ServeHost over the same (shared) Storage, Tenants and Agents. Options.Limits.HostLinkCredential is the credential Factory presents to them.
type Stack ¶
type Stack struct {
// contains filtered or unexported fields
}
Stack is one running composition: Factory, and under InProcess one pooled Host, over one Storage.
func Start ¶
Start validates o, opens storage, composes Factory and (under InProcess) the Host, then starts them: Host first, so Factory's first placement finds it. Nothing is served until both compositions have succeeded; the only socket opened before host.Compose is the in-process Host's loopback listener, bound (not served) because the Host must advertise its address.
START OWNS o.Storage. When Start returns an error, it has already stopped whatever it started and called o.Storage.Close; otherwise Stop closes it, last.
func (*Stack) DrainReport ¶
func (s *Stack) DrainReport() (host.DrainReport, bool)
DrainReport is the in-process Host's final drain report, and whether there is one: false before Stop has finished, under RemoteHosts, or when the Host never started. Abandoned lists sessions given up crash-equivalently with their gates preserved; Parked is a leak (see ParkedSessionsError).
func (*Stack) Stop ¶
Stop shuts the composition down in the one safe order and reports every failure: Factory Quiesce (fence new commands, close browser links) → Host Stop (drain while HostLink is still served) → Factory Stop → the HostLink listener → the stores the stack opened → Storage.Close, last. A drain that completed with failures is an error.
THE TEARDOWN RUNS ON ITS OWN LIFECYCLE, NOT ON ctx. The first Stop starts it; ctx bounds only how long this call waits for it. A Stop whose ctx ends first returns an error wrapping ctx.Err() while the teardown continues — storage is never closed under a component still running, such as a command admission Factory's Quiesce is waiting for — and any later Stop waits for the same teardown and returns its one result.
type Storage ¶
type Storage struct {
// Control holds the SessionStore both halves use: catalog, command inbox,
// host registry, gates. REQUIRED. Its Blobs must implement
// storage.BlobReaderLifecycle (SessionStore refuses one that does not);
// stack/localdisk adapts fsstore with storage.WithBoundedBlobReaders.
Control *storage.Composite
// Journal opens (or returns) the harness journal backend of one tenant.
// REQUIRED. The stack calls it once per tenant at Start. Harness's
// journal layout holds one tenant per backend, so it must never return
// one backend for two tenants.
Journal func(sessionwire.TenantID) (*storage.Composite, error)
// Workspaces is the local directory a session's workspace is created
// under, as <Workspaces>/<tenant>/<session>. REQUIRED and absolute.
Workspaces string
// SingleProcess marks a backend only one OS process may open
// (stack/localdisk). Such storage is refused with RemoteHosts, because a
// Host in another process could not read the journals it writes.
SingleProcess bool
// Close releases the profile. Optional. The stack calls it LAST, after
// Factory and Host have stopped, and only once.
Close func() error
}
Storage is one storage profile: the control backend Factory and Host share, one harness journal backend per tenant, and the local directory session workspaces are materialized under.
type UnknownJournalBindingError ¶
type UnknownJournalBindingError struct {
Tenant sessionwire.TenantID
BindingID string
Version string
}
UnknownJournalBindingError is a journal or object read for a session pinned to a binding this stack does not serve.
func (*UnknownJournalBindingError) Error ¶
func (e *UnknownJournalBindingError) Error() string
type UnknownTenantError ¶
type UnknownTenantError struct{ Tenant sessionwire.TenantID }
UnknownTenantError is a launch or journal read for a tenant this stack does not serve.
func (*UnknownTenantError) Error ¶
func (e *UnknownTenantError) Error() string
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package devauth is DEVELOPMENT-ONLY identity for a stack running on a developer's own machine.
|
Package devauth is DEVELOPMENT-ONLY identity for a stack running on a developer's own machine. |
|
examples
|
|
|
browser-app
command
Command browser-app is a single-process, multi-user Looprig web app built on github.com/looprig/stack: Factory, an in-process Host running a harness agent, local durable storage and dev-only sign-in.
|
Command browser-app is a single-process, multi-user Looprig web app built on github.com/looprig/stack: Factory, an in-process Host running a harness agent, local durable storage and dev-only sign-in. |
|
Package localdisk is the stack's single-machine storage profile: fsstore roots under one data directory.
|
Package localdisk is the stack's single-machine storage profile: fsstore roots under one data directory. |
|
Package memory is the stack's in-memory storage profile, for tests and experiments.
|
Package memory is the stack's in-memory storage profile, for tests and experiments. |