Help us shape the future of Go! Take the 2026 Go Developer Survey

factory

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 15, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

README

factory

factory is Looprig's public-facing orchestration service. It owns the HTTP and WebSocket surface, authentication and authorization, durable reads, command admission, target selection, Host links, multi-replica reconciliation, local realtime fan-out, and optional UI composition.

Factory imports neither Host nor Harness. Everything the two services exchange is a record from github.com/looprig/core/sessionwire/v1 or github.com/looprig/sessionstore, and that is the whole of the contract between them.

Factory is deliberately in the data path for active clients at the expected 1,000–5,000 connection scale, and its required state is disposable: reconnects and reads recover from SessionStore.

The import boundary is a test

import_boundary_test.go parses every Go file's import block — production and test — and applies five rules:

Dependency Permitted in

| github.com/looprig/host | nowhere | | github.com/looprig/harness | nowhere | | github.com/centrifugal/... | internal/realtime/... | | github.com/looprig/wui | cmd/factory/... | | k8s.io/..., sigs.k8s.io/... | internal/placement/kubernetes/... |

The rules are functions over PARSED import paths; nothing matches against source text, and containment is compared by whole path segments, so internal/realtimefanout is not inside internal/realtime and a second binary beside cmd/factory does not inherit its exemption. The scan fails as vacuous if it finds no files, and TestScanReachesEveryRuleScope builds a fixture module containing each scope so an exemption whose directory does not exist yet is still proven reachable and still proven to reject the location just outside it.

Two companion guards keep that scan honest. TestModuleHasNoUndeclaredNestedBoundary fails on any nested go.mod, .git or vendor below the root — including the .git file that a submodule or worktree writes — because such a directory stops the module-owned walk and makes all five rules silently stop applying to its whole subtree; a declared boundary is scanned in its own right rather than exempted. And module_pin_test.go parses go.mod's grammar to assert there is no replace directive in either spelling — and with tokens unquoted, since go.mod permits require "github.com/looprig/host" v1.0.0 and a split-only parser would skip it silently — and that every Looprig requirement names its exact released version — go mod verify checks content hashes, not version shape, so nothing else in make check would notice. Modules Factory may never import are rejected there before the released-version map is consulted, so the ban does not expire the day one of them ships.

go.mod names exact released versions and contains no replace directive; nothing here is vendored, so GOWORK=off go test ./... verifies the module against the versions it actually pins.

Composition

factory.New takes functional options and returns a validated *Server. An option is applied at most once -- a repeat is an error, not a last-wins -- and an explicit nil dependency is an error rather than a request for the default, because the seams that have defaults are defaulted by their ABSENCE.

The seams a deployer supplies are identity.Verifier, Authorizer, SessionReader, Commands, Directory, PlacementController, the CSRF configuration, and optionally a Clock, a UUIDSource, a session cookie name, a default tenant, limits and a UI. Authentication is supplied as a VERIFIER and not as an authenticator: Factory composes exactly one authenticator, because which credential authenticated a request must have one answer and a second one is an origin guard that skips its CSRF rules silently. The UI is optional in both shapes: WithUIHandler for a handler and WithUIFS for a static bundle, mutually exclusive, and a composition with neither is valid and is exercised as such -- the default binary mounts the Vite bundle, and an embedder that mounts its own or none composes the same Server.

Serving

Server.Handler is Factory's public HTTP surface: the API under /v1, and the injected user interface everywhere else. The injected UI is handed to the router as its fallback rather than mounted above or beside it, which is what makes API routes take precedence over the SPA: the router splits by path before authentication, so /v1/unknown is a JSON route_not_found, never index.html.

Serve and Stop are the OPTIONAL half. An embedder that owns its own http.Server uses Handler and never calls them. A deployment that wants Factory to own the server passes a listener it opened itself -- which address, which network, whether a supervisor passed the descriptor in, whether TLS is terminated here -- and gets the wire-level bounds HTTPLimits carries: ReadHeaderTimeout, IdleTimeout and MaxHeaderBytes. There is deliberately no ReadTimeout and no WriteTimeout; both are absolute per-connection deadlines, and this surface streams object bodies and will carry a WebSocket. Stop is idempotent, does not return until what it stopped has stopped, and stops public admission first so nothing new is admitted while the background components -- when they exist -- are torn down. It never touches a Host runtime: a session outlives every Factory replica.

What New composes today is that surface and nothing else -- the authenticator built from the deployer's verifier, the origin and CSRF guard, the router and the optional UI. Command admission, placement, the ClientLink and HostLink engines and the multi-replica reconcilers are separate tasks; the seams they will use are validated at composition and then held unread. The router is built with an empty launch Department, a nil object policy and no object-store resolver, each of which fails closed.

Each public seam is the union of the narrow interfaces the packages that CALL it declare for themselves, in internal/httpapi, internal/admission, internal/identity and internal/realtime/clientlink. There is no shared dependency package. The two sets are held together by tests rather than by discipline, and the two directions fail differently: widening a CONSUMER's interface still compiles and fails TestPublicSeamsSatisfyTheirConsumers, while widening a PUBLIC seam past its consumers fails the build outright, because the test fake that implements every seam no longer does.

The public seams name only standard-library, Core, SessionStore and factory/identity types, since a deployer outside this module cannot import github.com/looprig/factory/internal/... to name anything else -- which is why Principal and the CSRF configuration are in the public identity package in the first place. internal/realtime/hostlink's dialer is deliberately NOT on that surface: the HostLink protocol is internal, and what composition configures is the pool's limits.

Identity

internal/identity derives a principal from an inbound credential: an Authorization: Bearer header or the session cookie for REST, a connect token for a ClientLink. Two properties are the reason it is a package rather than a handler:

  • A tenant comes only from verified claims or from configuration. Nothing in the derivation reads the URL, the path, a path value, the body, a form or the Host header, so a conflicting tenant in a request is not weighed and discarded -- it is never read. TestTheDerivationNamesNoTenantCarrier holds that over the PARSED file, because a behavioural sweep can only cover the carriers somebody thought of.
  • An Authenticator holds no per-process state. Every decision is a function of the credential, the injected Verifier and the injected Clock, so a browser whose WebSocket reconnects to another Factory replica is authenticated there with no handoff. That is section 10 of the spec, and it is the same requirement that makes WithCSRF mandatory.

An OperationContext carries exactly a Principal, which credential authenticated the operation, and a W3C trace identifier; the field set is enumerated by test, so a header, a cookie or a raw token cannot be added to it quietly. The credential source is there because CSRF applies to an ambient credential and not to a bearer one, and Authenticator.CredentialSource is the single reader of that precedence rule -- a CSRF guard that re-derived it could disagree, and would do so by skipping CSRF rather than by failing.

For the same reason the operation context is built by a method, Authenticator.NewOperationContext, which derives the source rather than accepting it. Taking it as a parameter moved "which credential authenticated this" back into the edge's hands one call further out, and a caller that wrote SourceBearer for a cookie-authenticated operation would make the CSRF guard skip. NewOperationContextWithSource remains for a caller that authenticated by a route this package does not model, and is documented as the exception rather than the default.

A Credential redacts under %v, %s, %q, %#v, log/slog and encoding/json, because the likeliest way a token reaches a log is that somebody formatted the value they were handed. Those are not independent mechanisms, and the difference was measured: deleting GoString or MarshalJSON changes what %#v and encoding/json produce, while deleting LogValue changes nothing a slog.TextHandler prints, because its KindAny path falls back to fmt and reaches String. LogValue is kept for the handlers that reflect instead, and is held by a runtime slog.LogValuer assertion rather than by a rendering.

Scrubbing a verifier's error text is byte-for-byte replacement, so it removes a verbatim credential and nothing else. A verifier that embeds a prefix, a digest or a re-encoding leaks that derivative, and TestScrubbingCoversOnlyAVerbatimCredential pins the boundary from both sides rather than leaving it to a caveat.

Realtime transport

The realtime transport is pinned exactly. The pins below, the constants in internal/realtime/transport/doc.go and go.mod are held equal to one another by tests in that package -- including this table, which is read from this file, so it cannot drift into a fourth unguarded copy of the version triple:

Role Module / package Version
Embedded server, for ClientLink github.com/centrifugal/centrifuge v0.38.0
Go client, for HostLink github.com/centrifugal/centrifuge-go v0.12.0
Wire library both depend on github.com/centrifugal/protocol v0.19.2
Browser client npm centrifuge 5.7.2

The Go client is deliberately not the latest, and cannot be. Factory embeds the server for ClientLink and dials with the client for HostLink in one binary, so both resolve ONE centrifugal/protocol version. The compatibility test is executable, and every version below was built and run rather than inferred from a go.mod require line -- a require is a floor, not a statement about which symbols a package exports:

centrifuge-go resolved protocol go build ./...
v0.10.12 v0.17.0 exit 0
v0.11.0 v0.19.2 exit 0
v0.12.0 v0.19.2 exit 0 (pinned)
v0.12.1 v0.21.0 exit 1 -- undefined: protocol.GetStreamCommandDecoder

protocol v0.19.2 still exports GetStreamCommandDecoder alongside its replacement; only v0.21.0 drops it. So the break is at centrifuge-go v0.12.1, and v0.12.0 is the newest client that coexists with the pinned server. Reaching v0.12.1 means moving the embedded server to a release built against protocol v0.21.0+, and it is that server move -- not the client move -- that invalidates the compatibility measurement taken against the pinned browser client and requires the spike be redone.

Everything the transport is trusted to do is measured against a real embedded node over a real loopback WebSocket in internal/realtime/transport, including the answers that are inconvenient: a stalled consumer loses its whole connection, not one subscription, by either the connection queue bound or the connection write deadline; and the connect reply carries the ping interval in whole SECONDS, so a sub-second ping interval is silently not negotiated and the server then closes healthy connections for a missing pong. The 5,000-connection transport-only case is behind the transportscale build tag and makes no durability or correctness claim.

History, recovery, presence, join/leave, client publication and WebSocket compression are each asserted off rather than merely left unset. Compression is asserted by speaking the RFC 6455 opening handshake directly from the standard library and reading Sec-WebSocket-Extensions off the 101 response, because the Go client surfaces neither the response nor the negotiated extension; this deliberately adds no direct gorilla/websocket dependency. The Redis and NATS brokers and standalone Centrifugo are absent rather than asserted-refused -- the node keeps its in-process memory broker and no external process is started. The suite exercises the JSON protocol only.

internal/realtime/hostlink is Factory's client side of the Factory-Host connection, split like the ClientLink: a Pool that decides and a CentrifugeDialer that carries. The invariant is one sentence — a session binding never costs a connection, and a connection is never shared between Hosts — and the route table is keyed by tenant AND session, because a session id is unique only within its tenant.

A failed command RPC is reported as ErrCommandUndelivered, which leaves the already committed inbox record pending. A Host's own answer arrives as *HostRefusal carrying Core's typed HostLinkError and is deliberately not in that class, so a caller tells "never seen" from "answered" without reading message text. The package imports no store and a parsed-import test holds that, so it could not record anything about the record either way.

Several Factory replicas may hold their own connection to one Host. There is no broker and no leader: closing one replica's pool leaves the others' connections and routes untouched, measured over fakes and over real sockets.

Core defines the record bodies and no transport framing for them. The method names and the {type, data} push envelope here are Factory's half of a protocol whose Host half does not exist in this repository, and the tests run against a stand-in node implementing exactly that proposal. It is a declared gap, not an agreement. The strings are pinned as absolute literals, because they are what Host must mirror. Where they should finally live is unresolved: internal/ is a place Host cannot import, and Core's sessionwire/v1 is transport-neutral and deliberately not the answer — a method name is transport-shaped, and putting one in a tier-0 module would make a future transport change a tier-0 breaking release.

The pool carries the control plane only. A7.3 moved the per-binding queues and the backpressure repair above it, into internal/realtime/delivery and routing.Relay; the live tail itself is still absent, because Core defines the session-channel record bodies but no HostLink framing to carry them. Choosing which Host a session belongs to is A7.2. factory.New composes neither the pool nor the reaper's cadence; A9.1 owns that.

Placement

internal/placement decides where a session runs. Decide is pure — a catalog record, an optional registry observation and one capacity page in, one of five outcomes out — and the reconciler around it takes the session's short-lived reconciliation claim, stores desired state idempotently and hands a dedicated session's intent to a WorkloadController.

A live owner is preferred before any claim is taken, and a replica that loses the claim re-reads the registry rather than answering "busy": the winner may have finished placing in between. ReusableOwner is the module's one statement of "is this the session's own live owner"; internal/admission calls it.

Desired state is idempotent twice over. A content comparison keeps a replica from rewriting an intent already stored, which is what holds the desired generation still, and a key derived from the intent keeps a second replica from applying it again — two Factories deriving one intent derive one key, and SessionStore checks the key before the revision.

WorkloadController names no platform type and has one method. H5 puts the Kubernetes adapter inside this module and links it into cmd/controller alone, so a nil controller is a supported configuration and a dedicated session reaching the tenant-facing replica is refused by name.

Two gaps are declared rather than worked around. The pinned core v0.7.0 has no request that can ask a Host to take an unowned session — HostLinkBindRequest refuses a zero lease epoch — so a pooled decision names a Host and stops there. And a tenant_exclusive pooled advertisement is never selected, because Factory cannot see which tenants a Host serves and refusing is the only enforcement of specification section 12 available to it.

Status

Seams and identity derivation. contract.go states the cross-service contract, server.go / options.go state the composition, and internal/identity derives principals; the HTTP API, admission, routing and placement are the subject of later tasks in runbook 05. The realtime transport is pinned and measured (A5.1), the ClientLink engine is built (A6.1) and the HostLink pool and dialer are built (A7.1); factory.New composes none of the three. Target discovery (A4.1), the placement policy and reconciler (A4.2) and the local HostBindings table (A4.3) are built and likewise uncomposed.

Documentation

Overview

Package factory is the public-facing orchestration service: HTTP and WebSocket surface, authentication and authorization, durable reads, command admission, target selection and Host links.

Factory imports neither Host nor Harness. It talks to Host exclusively through Core and SessionStore records, which are the cross-service contract; import_boundary_test.go enforces the prohibition and contract.go states the positive half.

Index

Constants

View Source
const DefaultVersion = "factory"

DefaultVersion is the build identity a composition naming none reports.

View Source
const MinClientLinkPingInterval = time.Second

MinClientLinkPingInterval is the shortest ping cadence the ClientLink wire can carry.

It is one second because the connect reply expresses the cadence as a WHOLE NUMBER OF SECONDS -- centrifuge@v0.38.0/client.go:2466 computes res.Ping = uint32(c.pingInterval.Seconds()) -- so every sub-second value arrives as 0. That is not merely imprecise. The Go client assigns c.sendPong = res.Pong INSIDE `if res.Ping > 0` (centrifuge-go@v0.12.0/client.go:1467-1474), so a client told 0 is never asked to pong at all, and the server then closes a perfectly healthy connection with DisconnectNoPong (3012) every pong timeout. The deployment sees a reconnect loop and reads it as a network fault.

Truncation ABOVE the floor is harmless in the other direction and is therefore allowed: an interval of 1500ms is advertised as 1s, which only makes the client's own liveness deadline stricter than the server's cadence, and the client adds MaxServerPingDelay (10s by default, centrifuge-go@v0.12.0/client.go:165-166, 791) on top of it.

Variables

View Source
var (
	// ErrNilOption reports a nil entry in the option slice.
	ErrNilOption = errors.New("factory: nil option")
	// ErrDuplicateOption reports an option supplied more than once.
	ErrDuplicateOption = errors.New("factory: option supplied more than once")
	// ErrNilDependency reports an option carrying a nil interface value.
	ErrNilDependency = errors.New("factory: nil dependency")
	// ErrMissingDependency reports a required seam nothing supplied.
	ErrMissingDependency = errors.New("factory: required seam is missing")
	// ErrConflictingUI reports both UI options supplied at once.
	ErrConflictingUI = errors.New("factory: WithUIHandler and WithUIFS are mutually exclusive")
	// ErrInvalidLimits reports a limit value or relationship this composition
	// refuses.
	ErrInvalidLimits = errors.New("factory: invalid limits")
	// ErrNotAServiceIdentity reports a WithServiceIdentity principal that is
	// not a service identity. See that option for why it is refused here.
	ErrNotAServiceIdentity = errors.New("factory: the sweep identity is not a service principal")
	// ErrEmptyReplicaID reports an empty WithReplicaID.
	ErrEmptyReplicaID = errors.New("factory: the replica identifier is empty")
	// ErrEmptyVersion reports an empty WithVersion.
	ErrEmptyVersion = errors.New("factory: the build version is empty")
	// ErrObjectPolicyWithoutResolver reports an ObjectPolicy composed with no
	// ObjectStoreResolver behind it. See WithObjectPolicy.
	ErrObjectPolicyWithoutResolver = errors.New("factory: WithObjectPolicy requires WithObjectStoreResolver")
	// ErrSessionBindingWithoutResolver reports a SessionBinding composed with
	// no ObjectStoreResolver behind it. See WithSessionBinding.
	ErrSessionBindingWithoutResolver = errors.New("factory: WithSessionBinding requires WithObjectStoreResolver")
	// ErrIncompleteSessionBinding reports a WithSessionBinding missing a
	// member. A binding is immutable after create, so a partial one cannot be
	// completed later.
	ErrIncompleteSessionBinding = errors.New("factory: the session binding names no storage binding or no version")
	// ErrCreatePlaneIncomplete reports exactly one half of the create
	// composition. See WithSessionBinding.
	ErrCreatePlaneIncomplete = errors.New("factory: serving V1 creates requires both WithSessionBinding and WithPublicCreates")
	// ErrInvalidLaunchTemplate reports a configured launch target this
	// composition refuses.
	ErrInvalidLaunchTemplate = errors.New("factory: invalid launch template")
)

Composition errors. Every failure New reports wraps one of these sentinels, so errors.Is classifies any of them.

The CONCRETE type varies and a caller that switches on it must handle three shapes: an *OptionError naming one option, a *MissingSeamsError naming every seam that was not supplied -- the most common failure, and NOT an *OptionError -- and ErrConflictingUI returned bare, since it belongs to two options rather than one.

View Source
var (
	// ErrServerStopped reports a Serve on a Server that has been stopped. A
	// stopped Server is not restartable: its composition is still valid, but
	// the components Stop shut down are not restarted, so a caller that wants
	// to serve again composes a new Server.
	ErrServerStopped = errors.New("factory: server is stopped")

	// ErrAlreadyServing reports a second concurrent Serve. One Server owns at
	// most one listener, because Stop states an ORDER over the components it
	// shuts down and two listeners would leave that order unstated for one of
	// them.
	ErrAlreadyServing = errors.New("factory: server is already serving")

	// ErrAlreadyStarted reports a second Start. Background components are
	// started once, because starting them twice would run two sweep loops per
	// pass and two ClientLink nodes on one composition.
	ErrAlreadyStarted = errors.New("factory: server is already started")
)

Lifecycle errors. Serve reports these; Stop reports only what shutting a component down reported.

View Source
var ErrNoEntropy = errors.New("factory: random source failed")

ErrNoEntropy reports that the random source could not supply the bytes an identifier is made of.

View Source
var ErrNoHintPublisher = errors.New("factory: this replica composes no journal-tip hint publisher")

ErrNoHintPublisher is what a hint publish reports. It is returned rather than nil so that a future composition which forgets to replace this cannot look like one that succeeded.

Functions

This section is empty.

Types

type Authorizer

type Authorizer interface {
	AuthorizeSessionList(ctx context.Context, principal identity.Principal) error
	AuthorizeSessionRead(ctx context.Context, principal identity.Principal, session sessionwire.SessionID) error
	AuthorizeObjectRead(ctx context.Context, principal identity.Principal, session sessionwire.SessionID, object sessionwire.ObjectReference) error
	AuthorizeControl(ctx context.Context, principal identity.Principal, session sessionwire.SessionID, kind sessionstore.CommandKind) error
	AuthorizeSubscribe(ctx context.Context, principal identity.Principal, channel string) error
	AuthorizeServiceSweep(ctx context.Context, principal identity.Principal) error
}

Authorizer decides every public operation and the one service operation. A denial must wrap identity.ErrUnauthorized. Any other error is a fault in the authorization dependency rather than a permissions decision.

type Catalog

Catalog is the durable session record: the read every decision is made against, the create a legacy admission authors, and the desired state placement authors.

GetCatalogEntry also appears on SessionReader. That is not a second authority: it is one method on one deployer-supplied object reached through two seams, because internal/admission and internal/placement each declare the read beside a write that SessionReader must not carry. Widening SessionReader instead would put CreateCatalogEntry within reach of every read handler.

type ClientLinkLimits

type ClientLinkLimits struct {
	// MaxConnections bounds concurrent ClientLinks on this replica.
	MaxConnections int

	// MaxChannelsPerConnection bounds the session channels one link may hold.
	//
	// It is configuration rather than an omission because the transport
	// defaults it SILENTLY: centrifuge@v0.38.0/node.go:135-136 sets
	// ClientChannelLimit to 128 when it is zero. One browser link multiplexes
	// every session its user is watching, so a ceiling nobody chose is a
	// ceiling that is discovered by a user hitting it.
	MaxChannelsPerConnection int

	// PerConnectionQueueBytes bounds one connection's outbound queue in BYTES.
	//
	// The unit is not a preference. The only thing that enforces it is
	// centrifuge's ClientQueueMaxSize, which config.go:58-61 defines as "a
	// maximum size of client's message queue in bytes", and A5.1 measured the
	// resulting close as DisconnectSlow (3008). This field previously said
	// "in messages" with a default of 256, which would have configured a
	// 256-BYTE budget and closed essentially every connection on its first
	// event. The name now carries the unit so the two cannot drift again.
	PerConnectionQueueBytes int
	// WriteTimeout bounds one outbound write.
	WriteTimeout time.Duration
	// PingInterval is how often the server pings an idle connection.
	PingInterval time.Duration
	// PongTimeout is how long a ping may go unanswered.
	PongTimeout time.Duration

	// CommandTimeout bounds ONE command RPC's durable admission.
	//
	// It is the only bound such an admission has, and it is configuration
	// rather than a constant because the deployment owns how long its durable
	// plane may take. The transport gives the handler no per-RPC context --
	// centrifuge@v0.38.0's RPCEvent carries a method and a payload and nothing
	// else -- and it dispatches the RPC synchronously on the connection's read
	// loop, so an unbounded admission does not merely hang one command: it
	// holds every other frame on that link and it prevents the replica from
	// draining. See clientlink.Limits.CommandTimeout for the measurement and
	// for the limit of what a deadline can promise.
	CommandTimeout time.Duration

	// DemandReleaseDebounce is how long a session's last local subscription
	// must have been gone before this replica gives its delivery demand back.
	//
	// It is configuration rather than a constant because what it trades is a
	// deployment's: a longer window keeps a HostLink open for a session nobody
	// is watching, and a shorter one makes an ordinary browser reconnect cost a
	// registry read and a rebind. Neither is a correctness failure -- demand is
	// local routing state and never authority. Zero is permitted and means
	// "release as soon as the last subscription is gone".
	DemandReleaseDebounce time.Duration

	// DemandTimeout bounds ONE call into the demand plane, in both directions.
	//
	// The acquire is bounded for the reason CommandTimeout is and against the
	// same mechanism: a subscribe is dispatched on the connection's read loop
	// and the connection context is cancelled when that loop returns, so an
	// in-flight acquire is the very thing keeping it alive. The RELEASE is
	// bounded because it has nothing else: it runs on a timer, after the
	// connection that caused it is typically gone, so there is no request
	// context in existence to inherit a deadline from.
	DemandTimeout time.Duration
}

ClientLinkLimits bounds this replica's local connections and fan-out queues.

func DefaultClientLinkLimits

func DefaultClientLinkLimits() ClientLinkLimits

DefaultClientLinkLimits is the configuration a composition gets if it names none. MaxConnections is sized for the 1,000-5,000 connection scale a Factory replica is expected to hold.

func (ClientLinkLimits) Validate

func (l ClientLinkLimits) Validate() error

Validate reports why these limits may not be used.

type Clock

type Clock interface {
	Now() time.Time
	AfterFunc(d time.Duration, f func()) (stop func() bool)
}

Clock is the time seam.

func SystemClock

func SystemClock() Clock

SystemClock is the default Clock: the standard library's.

type Commands

Commands is the durable command plane.

ControlShards is on it because the periodic sweeps ask the store how many service-control shards it persisted, on every pass rather than once: the count is a decision of the backend and not a setting of this replica, and a replica caching it would keep sweeping a shard space that had changed.

type Directory

Directory is the observed target directory.

type Gates

type Gates interface {
	ControlShards() int
	ListDueGates(ctx context.Context, req sessionstore.ListDueGatesRequest) (sessionstore.DueGatePage, error)
	RetireGateDeadlineIntent(ctx context.Context, req sessionstore.RetireGateDeadlineIntentRequest) error
}

Gates is the service-control gate-deadline plane the gate sweeper reads and retires.

It carries RetireGateDeadlineIntent and NOTHING that could answer a gate. internal/reconcile's own two seams are narrow for that reason -- OpenGate would resurrect a deadline and ResolveGate is a synthesized resolution the runbook forbids -- and this seam is their exact union, so the composition cannot hand the sweeper a capability its own contract refuses.

type HTTPLimits

type HTTPLimits struct {
	// ReadHeaderTimeout bounds the time a peer may take to send a complete
	// header block. It is the slow-header bound, and it is the one field here
	// with no alternative: nothing above the socket can observe a request that
	// has not finished arriving.
	ReadHeaderTimeout time.Duration

	// IdleTimeout is how long a kept-alive connection may sit between
	// requests. It is set explicitly rather than left to default to
	// ReadTimeout, which is zero here, because that default is "forever".
	IdleTimeout time.Duration

	// MaxHeaderBytes bounds a request's headers. internal/identity bounds the
	// one header it parses on its own, deliberately, "because Factory does not
	// own the http.Server"; under Serve it does, and this is that bound.
	MaxHeaderBytes int
}

HTTPLimits bounds the connections Serve accepts.

These are the http.Server bounds internal/httpapi's RouteLimits documentation defers to the task that builds the server: RouteLimits bounds the WORK a request may do, as a deadline on its context, and nothing there can bound a peer that never finishes sending a header or never reads a response.

They apply to Serve only. A library embedding uses Handler and supplies its own http.Server, and these values say nothing about it.

func DefaultHTTPLimits

func DefaultHTTPLimits() HTTPLimits

DefaultHTTPLimits is what a composition naming no HTTP limits receives.

There is deliberately NO ReadTimeout and NO WriteTimeout, and their absence is a decision rather than an omission. Both are absolute per-connection deadlines that net/http applies without regard to what a handler is doing, and this surface has two shapes they would break: an authorized object body is streamed, and /v1/realtime is a WebSocket upgrade a later task fills in, which by construction outlives any write deadline. What bounds a handler's work instead is httpapi.RouteLimits.RequestTimeout, a deadline on the request context that every dependency call takes.

The residue is stated rather than hidden: a peer that completes its headers and then sends its BODY slowly is bounded by the handler's context deadline and by MaxRequestBytes, not by a socket deadline, so it holds a connection for as long as that deadline allows.

func (HTTPLimits) Validate

func (l HTTPLimits) Validate() error

Validate reports why these limits may not be used.

type HostLinkCredential

type HostLinkCredential interface {
	ServiceToken(ctx context.Context) (string, error)
}

HostLinkCredential supplies the SERVICE identity this replica presents to a Host on every dial and every reconnect.

It is deliberately not identity.Principal: a HostLink carries no end user. The Host authorizes a bind against the session's durable lease, not against a user this connection claims to act for.

type HostLinkLimits

type HostLinkLimits struct {
	// MaxLinks bounds concurrent HostLinks. One link multiplexes every session
	// binding to one Host, so this bounds Hosts, not sessions.
	MaxLinks int
	// DialTimeout bounds one dial.
	DialTimeout time.Duration
	// IdleTimeout is how long a link with no local demand is kept.
	IdleTimeout time.Duration
	// ReconnectMin and ReconnectMax bound the reconnect backoff.
	ReconnectMin time.Duration
	ReconnectMax time.Duration
}

HostLinkLimits bounds this replica's demand-driven HostLink pool.

func DefaultHostLinkLimits

func DefaultHostLinkLimits() HostLinkLimits

DefaultHostLinkLimits is the configuration a composition gets if it names none.

func (HostLinkLimits) Validate

func (l HostLinkLimits) Validate() error

Validate reports why these limits may not be used.

type HostTargets

type HostTargets interface {
	ReconcileHostTargets(ctx context.Context, req sessionstore.ReconcileHostTargetsRequest) (sessionstore.HostTargetReconcileResult, error)
}

HostTargets is the Host advertisement index the record sweeper reconciles.

One method. A due advertisement is a hint about which rows to LOOK at and is never evidence that a Host is gone, so the seam offers no way to read one and act on it directly.

type LaunchTemplate

type LaunchTemplate struct {
	// Key is the launch target's identity: the triple SessionStore files an
	// advertisement under, so a configured template and an advertised one are
	// the same value rather than two spellings of one.
	Key sessionstore.HostTargetKey

	// Capabilities are the presentation strings this deployment publishes.
	Capabilities []string

	// Workload is the platform payload a DEDICATED session's workload is
	// created from. It is required for a dedicated target and must be absent
	// for a pooled one: a pooled session runs on a Host that already exists,
	// so a workload configured for it would be a value nothing could ever
	// create.
	Workload sessionstore.DesiredWorkload
}

LaunchTemplate is one launch target this deployment is configured to offer.

It is a COMPOSITION type and not an alias of the one internal/httpapi publishes, and the extra member is exactly why. httpapi.LaunchTemplate carries identity and nothing else, deliberately: /v1/agents publishes it, and a published launch target that restated a runtime definition would be the competing static agent catalogue the specification forbids Factory to keep. Workload is a DEPLOYMENT's opaque platform payload -- a pod spec, a Nomad job -- which a tenant must never be shown. So the composition holds both halves and PROJECTS the published half; see Server.composeDepartment.

Splitting them this way is what makes the projection the only path: there is no field on the published type for a workload to leak through.

func (LaunchTemplate) Validate

func (t LaunchTemplate) Validate() error

Validate reports why this template may not be used.

It defers the identity half to the published type rather than restating it, so the two cannot drift, and adds only the rule that belongs to the member the published type does not have.

type MissingSeamsError

type MissingSeamsError struct {
	// Options are the option constructors that had to be supplied and were not.
	Options []string
}

MissingSeamsError names every seam a composition failed to supply. It is not an *OptionError, because it belongs to a set of options rather than to one.

All of them are reported at once, and SORTED, for two reasons. A caller with three missing seams is not sent round the build-and-fail loop three times; and the order the seams are checked in acquires no reader, so it stays what it is -- an ordering chosen for readability -- rather than becoming a contract nobody meant to make. The sort is held by a case whose check order and sorted order DIFFER; without one, deleting the sort would change nothing observable and that second claim would quietly stop being true.

func (*MissingSeamsError) Error

func (e *MissingSeamsError) Error() string

func (*MissingSeamsError) Unwrap

func (e *MissingSeamsError) Unwrap() error

type ObjectLimits

type ObjectLimits = httpapi.ObjectLimits

ObjectLimits bounds retained response memory and whole-object verification.

func DefaultObjectLimits

func DefaultObjectLimits() ObjectLimits

DefaultObjectLimits retains at most 1 MiB per request and verifies at most 64 MiB.

type ObjectPolicy

type ObjectPolicy = httpapi.ObjectPolicy

ObjectPolicy authorizes one object reference using trusted committed session evidence, and it is an alias for LaunchTemplate's reason. A denial must wrap identity.ErrUnauthorized; any other error is a policy dependency fault.

A nil policy fails closed at the router: an object read is answered "unavailable" before the catalog summary, before any authorization call and before any reader is reached. That is why supplying one is OPTIONAL here and why supplying one without an ObjectStoreResolver is REFUSED -- see WithObjectPolicy.

type ObjectReader

type ObjectReader = httpapi.ObjectReader

ObjectReader is the neutral read capability a frozen binding resolves to.

type ObjectStoreResolver

type ObjectStoreResolver func(ctx context.Context, binding sessionstore.SessionBinding) (ObjectReader, error)

ObjectStoreResolver consumes a session's full immutable binding and answers the store its objects live in, refusing any configuration this deployment does not know.

The refusal is the point. A binding names a StorageBindingID and a BindingVersion chosen at create and immutable afterwards; a resolver that answered a default for an unknown one would serve one deployment's bytes under another's configuration.

type Option

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

Option configures a Server. An option is applied at most once; supplying one twice is an error rather than a silent last-wins.

func WithAuthorizer

func WithAuthorizer(a Authorizer) Option

WithAuthorizer supplies the authorizer for every public operation.

func WithCSRF

func WithCSRF(cfg identity.CSRFConfig) Option

WithCSRF supplies the origin and CSRF configuration. It has no default.

func WithCatalog

func WithCatalog(c Catalog) Option

WithCatalog supplies the durable session record plane. Required.

func WithClientLinkLimits

func WithClientLinkLimits(l ClientLinkLimits) Option

WithClientLinkLimits replaces the local ClientLink and fan-out queue limits.

func WithClock

func WithClock(clock Clock) Option

WithClock replaces the system clock.

func WithCommands

func WithCommands(cmds Commands) Option

WithCommands supplies the durable command plane.

func WithCredentialVerifier

func WithCredentialVerifier(v identity.Verifier) Option

WithCredentialVerifier supplies the credential verifier.

This is the authentication seam, and it is deliberately NOT an authenticator. Which carrier a credential is read from, which one wins when a request presents two, and what the operation context records about the one that authenticated are decided in exactly one place, because a second answer to "which credential authenticated this request" is an origin guard that skips its CSRF rules silently -- internal/httpapi.GuardConfig.Credentials states that at length. What a deployment genuinely owns is what makes a presented credential valid and what it asserts, which is this interface.

func WithDefaultTenant

func WithDefaultTenant(tenant sessionwire.TenantID) Option

WithDefaultTenant scopes an actor credential that names no tenant.

Leaving it unset requires every credential to name its own tenant, which is the cloud deployment's shape; a single-tenant local composition sets it. It applies to an ACTOR only -- a service credential that named no tenant would otherwise acquire one by configuration.

func WithDepartment

func WithDepartment(templates ...LaunchTemplate) Option

WithDepartment supplies the launch targets this deployment offers.

An EMPTY department is a supported composition and is the default: a Factory serving an existing tenant's durable history needs no launchable agent, and answers /v1/agents with an empty list, which is the truthful answer. Every entry is validated here, so a malformed template is a composition failure an operator sees rather than a request-time decision.

func WithDirectory

func WithDirectory(d Directory) Option

WithDirectory supplies the observed target directory.

func WithGates

func WithGates(g Gates) Option

WithGates supplies the service-control gate-deadline plane. Required.

func WithHTTPLimits

func WithHTTPLimits(l HTTPLimits) Option

WithHTTPLimits replaces the connection limits Serve applies. They bound the socket, so they say nothing about a library embedding that uses Handler and supplies its own http.Server.

func WithHostLinkCredential

func WithHostLinkCredential(c HostLinkCredential) Option

WithHostLinkCredential supplies the service token this replica presents to a Host. Required.

func WithHostLinkLimits

func WithHostLinkLimits(l HostLinkLimits) Option

WithHostLinkLimits replaces the HostLink pool limits.

func WithHostTargets

func WithHostTargets(t HostTargets) Option

WithHostTargets supplies the Host advertisement index. Required.

func WithObjectLimits

func WithObjectLimits(l ObjectLimits) Option

WithObjectLimits bounds retained response memory and verification I/O.

func WithObjectPolicy

func WithObjectPolicy(p ObjectPolicy) Option

WithObjectPolicy supplies the committed-evidence policy that authorizes an object reference. Optional; without one every object read answers "unavailable" before any reader is reached.

Supplying it WITHOUT WithObjectStoreResolver is refused by New, and that refusal is the whole reason these are two options rather than one value. The router resolves a store only for a NON-ZERO binding and falls back to the read plane for a zero (legacy) one; a nil policy refuses first and unconditionally, so today the fallback is unreachable. A composition that supplied a policy alone would make it reachable in the same change, with no resolver behind it.

func WithObjectStoreResolver

func WithObjectStoreResolver(r ObjectStoreResolver) Option

WithObjectStoreResolver supplies the binding-to-store resolution an object read uses. See WithObjectPolicy for why the two travel together.

func WithPlacementController

func WithPlacementController(p PlacementController) Option

WithPlacementController supplies the placement controller.

func WithPublicCreates

func WithPublicCreates(p PublicCreates) Option

WithPublicCreates supplies the durable plane a V1 create admits into. Optional, and it travels with WithSessionBinding: both halves are required before a create is served, and neither implies the other -- a plane with no configured binding has nothing to pin, and a binding with no plane has nowhere to put it.

func WithReconcileLimits

func WithReconcileLimits(l ReconcileLimits) Option

WithReconcileLimits replaces the reconciliation limits.

func WithReplicaID

func WithReplicaID(id string) Option

WithReplicaID names this replica in every claim it takes. Required.

It is not authority -- a claim licenses nothing -- but it must be STABLE for a process: extending one's own claim and taking over a crashed replica's lapsed one are the same store write, and the holder is what tells them apart. It is also what a record sweep releases by, so a replica that reported two holders would leave its own claims behind.

func WithServiceIdentity

func WithServiceIdentity(p identity.Principal) Option

WithServiceIdentity names the principal the cross-tenant sweeps run as.

It is REQUIRED and it is checked for Kind service here, at composition, rather than trusted: every sweep this Server drives calls AuthorizeServiceSweep with this value, and a tenant principal reaching that call is a cross-tenant read authorized as a tenant's.

func WithSessionBinding

func WithSessionBinding(storageBindingID, bindingVersion string) Option

WithSessionBinding supplies the storage configuration every created session is permanently pinned to. Without it a V1 create is refused runtime_unavailable and every other operation is unaffected.

It is REFUSED without WithObjectStoreResolver, for a sharper reason than WithObjectPolicy's. A SessionBinding is IMMUTABLE AFTER CREATE: a session pinned to a StorageBindingID this deployment's resolver cannot resolve has permanently unreadable objects, and there is no repair short of an offline migration. Requiring the resolver does not prove the resolver knows THIS binding -- nothing at composition time can -- but it does refuse the one composition that is certainly wrong, which is pinning sessions to a storage configuration in a deployment that resolves no storage at all.

func WithSessionCookieName

func WithSessionCookieName(name string) Option

WithSessionCookieName replaces the browser session cookie a request is read from when it carries no Authorization header.

A name that is not a valid cookie name is refused by New rather than at the first request: net/http DROPS such a cookie when it writes one, so the composition would look configured and never authenticate anybody.

func WithSessionReader

func WithSessionReader(r SessionReader) Option

WithSessionReader supplies the durable read plane.

func WithUIFS

func WithUIFS(dir fs.FS) Option

WithUIFS mounts a static user interface bundle.

func WithUIHandler

func WithUIHandler(h http.Handler) Option

WithUIHandler mounts a caller-supplied user interface handler.

func WithUUIDSource

func WithUUIDSource(u UUIDSource) Option

WithUUIDSource replaces the random identifier source.

func WithVersion

func WithVersion(v string) Option

WithVersion names the Factory build identity reported to a connecting client and to a Host. Optional; DefaultVersion is used when it is not supplied.

func WithWorkloadController

func WithWorkloadController(w WorkloadController) Option

WithWorkloadController supplies the platform adapter a DEDICATED session's workload is created through. Optional, and deliberately so.

cmd/factory composes none: H5 puts the platform adapter in a separate controller binary, whose ServiceAccount holds the workload RBAC that the Factory's does not. A placement reconciler composed without one refuses a dedicated session with a named error instead of reporting success for work nothing did, which is what keeps the split from being a convention.

type OptionError

type OptionError struct {
	// Option is the constructor's name, or the name of the option that had to
	// be supplied and was not.
	Option string
	Err    error
}

OptionError names the option a composition failure belongs to.

func (*OptionError) Error

func (e *OptionError) Error() string

func (*OptionError) Unwrap

func (e *OptionError) Unwrap() error

type PlacementController

type PlacementController interface {
	EnsurePlacement(ctx context.Context, desired sessionstore.DesiredWorkload) error
	ReleasePlacement(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID) error
}

PlacementController drives desired placement.

type PublicCreates

type PublicCreates = admission.PublicCreateStore

PublicCreates is the durable public-create plane a V1 create admits into. It is the DISPOSITION family, which a *sessionstore.Store satisfies.

type ReconcileLimits

type ReconcileLimits struct {
	// Interval is the sweep cadence.
	Interval time.Duration
	// ClaimTTL is how long a reconciliation claim suppresses duplicate work.
	ClaimTTL time.Duration
	// ApplyDeadline is how long an accepted command may stay unapplied before
	// it becomes rejected/runtime_unavailable.
	ApplyDeadline time.Duration
	// MaxDuePerSweep bounds the due records one sweep withdraws.
	MaxDuePerSweep int
	// MaxConcurrent bounds concurrent reconciliations in this replica.
	MaxConcurrent int
}

ReconcileLimits bounds Factory's periodic reconciliation of pending commands with no live owner.

func DefaultReconcileLimits

func DefaultReconcileLimits() ReconcileLimits

DefaultReconcileLimits is the configuration a composition gets if it names none.

func (ReconcileLimits) Validate

func (l ReconcileLimits) Validate() error

Validate reports why these limits may not be used.

type Server

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

Server is the composed Factory.

What it composes TODAY is the durable public HTTP surface and nothing else: the credential authenticator built from the deployer's verifier, the origin and CSRF guard, internal/httpapi's router -- routed reads, the authenticated bootstrap and the JSON error envelope -- and the optional user interface, mounted as that router's fallback outside the API version segment.

What it does NOT compose is stated here rather than left to be discovered. Command admission, target placement, the ClientLink and HostLink engines and the multi-replica reconcilers are separate runbook tasks and are not built yet, so the seams they will use are validated at composition and then held unread. The router is composed with an EMPTY launch Department, a nil object policy and no object-store resolver, each of which fails closed: /v1/agents answers an empty list and an object read answers "unavailable" rather than serving bytes no policy authorized.

func New

func New(opts ...Option) (*Server, error)

New validates a composition and returns it.

The checks run in a fixed order -- shape of the option list, then what each option carries, then what is missing, then what the values mean together -- so a caller with more than one defect is told about the outermost one, and every later check is reached only when the earlier ones hold.

func (*Server) CSRF

func (s *Server) CSRF() identity.CSRFConfig

CSRF returns the composed origin and CSRF configuration. The result shares no memory with the Server's copy, so a caller that inspects and then reuses the returned key buffer cannot change what a running Factory signs with.

func (*Server) ClientLinkLimits

func (s *Server) ClientLinkLimits() ClientLinkLimits

ClientLinkLimits returns the composed ClientLink limits.

func (*Server) HTTPLimits

func (s *Server) HTTPLimits() HTTPLimits

HTTPLimits returns the composed HTTP connection limits.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler is Factory's public HTTP surface: the API under /v1, and the injected user interface everywhere else when one was supplied.

It is what a LIBRARY embedding uses. Factory does not own the socket in that shape -- the embedder supplies the http.Server, and MaxHeaderBytes with it -- so nothing here reads a listener. A deployment that wants Factory to own the server calls Serve instead.

The returned handler is the same one on every call and is safe for concurrent use.

func (*Server) HostLinkLimits

func (s *Server) HostLinkLimits() HostLinkLimits

HostLinkLimits returns the composed HostLink pool limits.

func (*Server) ReconcileLimits

func (s *Server) ReconcileLimits() ReconcileLimits

ReconcileLimits returns the composed reconciliation limits.

func (*Server) Serve

func (s *Server) Serve(ln net.Listener) error

Serve accepts connections on ln until Stop, and owns the http.Server it builds -- but not the listener, which the caller opens and which Stop closes by shutting that server down.

The split is deliberate. Opening the socket is where a deployment's real decisions live -- which address, which network, whether systemd or a supervisor passed the descriptor in, whether TLS is terminated here or ahead of here -- and none of them belong to this module. What DOES belong here is the set of bounds a wire-level peer is held to, which is HTTPLimits.

A deliberate Stop is reported as success rather than as http.ErrServerClosed: this method's contract is "serve until stopped", and a caller should not have to classify the ordinary ending. Any other failure is returned as it was.

Serve is optional. An embedder that owns its own http.Server uses Handler and never calls this. # Why the state claim is the FIRST thing this method does

It has to be, and the cost of getting it wrong is a leaked socket rather than a confusing error. Everything Stop can close about the public surface it reaches through s.http, so a Stop that lands while s.http is still nil skips Shutdown entirely. If this method can then return WITHOUT having handed the listener to net/http, nobody closes it: the caller's socket stays open and unserved, which is exactly the failure "Shutdown stops public admission/listener" exists to prevent.

An earlier draft of this stage called Start BEFORE the claim, on the reading that background planes should exist before the network is admitted -- which is right, and the PLACEMENT was wrong. A node boot and three goroutine launches sat between entering Serve and claiming the state, and a Stop in that window made Serve return ErrServerStopped with the listener orphaned. It reproduced roughly one run in three under load.

The general shape, worth carrying: MOVING WORK AHEAD OF A LOCK OR STATE CLAIM WIDENS A WINDOW THAT USED TO BE ZERO. The claim is cheap and it is what makes every later step safe to interleave, so it goes first and the work goes after it.

func (*Server) Start

func (s *Server) Start(ctx context.Context) error

Start runs this replica's background components.

It exists as a separate method from Serve because a LIBRARY embedding owns its own http.Server and reaches the surface through Handler; without Start such a deployment would compose every reconciler and run none of them, and the symptom -- commands that are accepted and never settled -- would appear nowhere near the composition that caused it. Serve calls it, so a deployment that owns the socket through this module does not have to.

The order inside it is stated rather than incidental:

  1. The ClientLink node, because clientlink.NewHandler RUNS it and a connection arriving the instant the listener opens must find a node, not a half-built one. Until this succeeds /v1/realtime answers 503.
  2. The periodic sweeps, each on its own goroutine and its own timer, so one slow pass delays only its own sweep.

The HostLink pool and the routing table are NOT started: both are demand driven, hold no goroutine until a session is bound, and are stopped by Stop whether or not they ever were.

Start is not restartable. A stopped Server refuses it, for Serve's reason: the components Stop shut down are not restarted, so a caller that wants to run again composes a new Server.

func (*Server) Stop

func (s *Server) Stop(ctx context.Context) error

Stop shuts the Server down in a fixed order and does not return until each component it stopped has stopped.

The order is the one the runbook states: PUBLIC ADMISSION first -- the listener, the in-flight requests it accepted, and the ClientLink node -- and then the background components, so nothing new is admitted while they are being torn down.

There are THREE phases and four components, and each phase is labelled at the code below rather than only here. (This comment said "today there is exactly one component" until A9.1 stage 2 composed the rest; it is stated in full now because a shutdown order that is documented wrongly is worse than one that is not documented at all.)

Stop never touches a Host runtime. Factory is not their supervisor: a session outlives every Factory replica, and a Stop that reached into placement would end sessions because a deployment restarted a front end.

It is idempotent, because a signal handler and a deferred stop reach it together, and it is safe to call on a Server that never served.

func (*Server) UI

func (s *Server) UI() (http.Handler, bool)

UI reports the optional user interface handler.

The second result is false for a library composition that serves no UI, which is a supported configuration rather than a degraded one: the default binary mounts the Vite bundle, and an embedder that mounts its own or none at all composes the same Server.

type SessionBindingTemplate

type SessionBindingTemplate = admission.SessionBindingTemplate

SessionBindingTemplate is the deployment-configuration half of the immutable SessionBinding every session this Factory creates is pinned to.

It names the two members ObjectStoreResolver keys on, and that is not a coincidence -- it is the argument for supplying it here at all. The other two members of a store SessionBinding are not a deployment's to choose: RuntimeSessionID is derived per create from the create's own identity, and ProtocolMode is fixed to disposition because a legacy-bound session is one no Host can take residency on.

type Target

type Target struct {
	Tenant  sessionwire.TenantID
	Session sessionwire.SessionID
	Agent   sessionwire.AgentID
	Host    sessionwire.HostID
}

Target names the Host that a Factory has selected for one tenant's session.

It exists in this scaffold to state the positive half of the import boundary in code rather than in prose: the cross-service contract between Factory and Host is Core's wire vocabulary and SessionStore's durable records, and nothing else. Every member below is a Core identifier, and the only way to build one is from a SessionStore record. There is deliberately no field here that could hold a Host or Harness value, because this module cannot name one.

The composition seams themselves — authenticator, authorizer, session store interface, target directory, HostLink dialer — are task A0.2's.

func TargetFor

func TargetFor(tenant sessionwire.TenantID, session sessionwire.SessionID, target sessionstore.HostTarget) Target

TargetFor projects a SessionStore capacity row into the identifiers a placement decision is recorded with.

The tenant and session are parameters rather than fields read off the row on purpose: a HostTarget is capacity, not authority, and it names no tenant and no session. Ownership is established against SessionStore's registration record and its lease epoch, never inferred from an advertisement.

type UUIDSource

type UUIDSource interface {
	NewUUID() (string, error)
}

UUIDSource is the random-identifier seam.

func CryptoUUIDSource

func CryptoUUIDSource() UUIDSource

CryptoUUIDSource is the default UUIDSource: version 4 UUIDs from crypto/rand.

type WorkloadController

type WorkloadController interface {
	EnsureWorkload(ctx context.Context, intent sessionstore.PlacementIntent) error
}

WorkloadController creates and updates the platform workload a dedicated session needs.

It is OPTIONAL and only the controller binary supplies it. H5 (answered 2026-09-04) puts the platform adapter in a separate controller, so cmd/factory composes none and a dedicated placement there fails with a named refusal rather than silently doing nothing.

The intent is the whole currency: Factory-authored desire, carrying its own generation and an opaque workload payload this module never parses. A Kubernetes PodSpec, a Nomad job and a future platform's manifest are the same value to it, which is what keeps the platform out of this module's import graph.

Directories

Path Synopsis
Package identity holds the tenancy vocabulary that Factory's composition seams exchange.
Package identity holds the tenancy vocabulary that Factory's composition seams exchange.
internal
admission
Package admission accepts commands durably and reconciles the ones with no live owner.
Package admission accepts commands durably and reconciles the ones with no live owner.
command
Package command holds the durable command vocabulary Factory admits.
Package command holds the durable command vocabulary Factory admits.
httpapi
Package httpapi serves Factory's public REST plane.
Package httpapi serves Factory's public REST plane.
identity
Package identity derives Factory's principals from inbound credentials.
Package identity derives Factory's principals from inbound credentials.
modfiles
Package modfiles enumerates Go source files owned by this module while excluding structural directories and nested repository or module boundaries.
Package modfiles enumerates Go source files owned by this module while excluding structural directories and nested repository or module boundaries.
placement
Package placement decides where a session should run, and reconciles the durable desired state that decision implies.
Package placement decides where a session should run, and reconciles the durable desired state that decision implies.
realtime/clientlink
Package clientlink holds Factory's browser-facing duplex connection.
Package clientlink holds Factory's browser-facing duplex connection.
realtime/delivery
Package delivery holds Factory's bounded record queues, ABOVE the transport.
Package delivery holds Factory's bounded record queues, ABOVE the transport.
realtime/hostlink
Package hostlink holds Factory's client side of the Factory-Host connection.
Package hostlink holds Factory's client side of the Factory-Host connection.
realtime/transport
Package transport is where Factory's realtime transport dependency is pinned and where its behaviour is MEASURED rather than assumed.
Package transport is where Factory's realtime transport dependency is pinned and where its behaviour is MEASURED rather than assumed.
reconcile
Package reconcile retires the durable remnants a crashed control-plane operation leaves behind.
Package reconcile retires the durable remnants a crashed control-plane operation leaves behind.
routing
Package routing reads the durable routing and capacity records that Factory uses to find Hosts.
Package routing reads the durable routing and capacity records that Factory uses to find Hosts.

Jump to

Keyboard shortcuts

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