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
- Variables
- func UIRoutePrincipal(r *http.Request) (identity.Principal, bool)
- type AuditAuthorizer
- type Authorizer
- type Catalog
- type ClientLinkLimits
- type Clock
- type Commands
- type Directory
- type DirectoryLimits
- type DirectoryStore
- type Gates
- type HTTPLimits
- type HostLinkCredential
- type HostLinkLimits
- type HostTargets
- type JournalReader
- type JournalResolverdeprecated
- type LaunchTemplate
- type MissingSeamsError
- type ObjectLimits
- type ObjectPolicy
- type ObjectReader
- type ObjectStoreResolverdeprecated
- type Option
- func WithAuthorizer(a Authorizer) Option
- func WithCSRF(cfg identity.CSRFConfig) Option
- func WithCatalog(c Catalog) Option
- func WithClientLinkLimits(l ClientLinkLimits) Option
- func WithClock(clock Clock) Option
- func WithCommands(cmds Commands) Option
- func WithCredentialVerifier(v identity.Verifier) Option
- func WithDefaultTenant(tenant sessionwire.TenantID) Option
- func WithDepartment(templates ...LaunchTemplate) Option
- func WithDirectory(d Directory) Option
- func WithGates(g Gates) Option
- func WithHTTPLimits(l HTTPLimits) Option
- func WithHostLinkCredential(c HostLinkCredential) Option
- func WithHostLinkLimits(l HostLinkLimits) Option
- func WithHostTargets(t HostTargets) Option
- func WithJournalResolver(r JournalResolver) Optiondeprecated
- func WithLogger(l *slog.Logger) Option
- func WithObjectLimits(l ObjectLimits) Option
- func WithObjectPolicy(p ObjectPolicy) Option
- func WithObjectStoreResolver(r ObjectStoreResolver) Optiondeprecated
- func WithPendingCommands(p PendingCommands) Option
- func WithPlacementController(p PlacementController) Optiondeprecated
- func WithPrincipalStamping() Option
- func WithPublicCreates(p PublicCreates) Option
- func WithReconcileLimits(l ReconcileLimits) Option
- func WithReplicaID(id string) Option
- func WithRouteLimits(l RouteLimits) Option
- func WithServiceIdentity(p identity.Principal) Option
- func WithSessionBinding(storageBindingID, bindingVersion string) Option
- func WithSessionCookieName(name string) Option
- func WithSessionJournalResolver(r SessionJournalResolver) Option
- func WithSessionObjectStoreResolver(r SessionObjectStoreResolver) Option
- func WithSessionReader(r SessionReader) Option
- func WithUIFS(dir fs.FS) Option
- func WithUIHandler(h http.Handler) Option
- func WithUIRoutes(h http.Handler, authorize UIRouteAuthorizer) Option
- func WithUUIDSource(u UUIDSource) Option
- func WithVersion(v string) Option
- func WithWorkloadController(w WorkloadController) Option
- type OptionError
- type PendingCommands
- type PlacementControllerdeprecated
- type PublicCreates
- type ReconcileLimits
- type RouteLimits
- type Server
- func (s *Server) CSRF() identity.CSRFConfig
- func (s *Server) ClientLinkLimits() ClientLinkLimits
- func (s *Server) HTTPLimits() HTTPLimits
- func (s *Server) Handler() http.Handler
- func (s *Server) HostLinkLimits() HostLinkLimits
- func (s *Server) Quiesce(ctx context.Context) error
- func (s *Server) ReconcileLimits() ReconcileLimits
- func (s *Server) RouteLimits() RouteLimits
- func (s *Server) Serve(ln net.Listener) error
- func (s *Server) Start(ctx context.Context) error
- func (s *Server) Stop(ctx context.Context) error
- func (s *Server) UI() (http.Handler, bool)
- type SessionBindingTemplate
- type SessionJournalResolver
- type SessionObjectStoreResolver
- type SessionReader
- type Target
- type TenantAuthorizer
- func (TenantAuthorizer) AuthorizeAuditRead(ctx context.Context, principal identity.Principal, ...) error
- func (TenantAuthorizer) AuthorizeControl(ctx context.Context, principal identity.Principal, ...) error
- func (TenantAuthorizer) AuthorizeObjectRead(ctx context.Context, principal identity.Principal, ...) error
- func (TenantAuthorizer) AuthorizeServiceSweep(ctx context.Context, principal identity.Principal) error
- func (TenantAuthorizer) AuthorizeSessionList(ctx context.Context, principal identity.Principal) error
- func (TenantAuthorizer) AuthorizeSessionRead(ctx context.Context, principal identity.Principal, ...) error
- func (TenantAuthorizer) AuthorizeSubscribe(ctx context.Context, principal identity.Principal, channel string) error
- type UIRouteAuthorizer
- type UUIDSource
- type WorkloadController
- type WorkloadEndpointDiscovery
Constants ¶
const DefaultVersion = "factory"
DefaultVersion is the build identity a composition naming none reports.
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.39.3/client.go:3384 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 ¶
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 // object resolver behind it -- neither WithSessionObjectStoreResolver nor // the deprecated WithObjectStoreResolver. See WithObjectPolicy. ErrObjectPolicyWithoutResolver = errors.New("factory: WithObjectPolicy requires WithSessionObjectStoreResolver") // ErrSessionBindingWithoutResolver reports a SessionBinding composed with // no object resolver behind it. See WithSessionBinding. ErrSessionBindingWithoutResolver = errors.New("factory: WithSessionBinding requires WithSessionObjectStoreResolver") // 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.
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") // ErrServerQuiesced reports a Start or Serve after public admission closed. ErrServerQuiesced = errors.New("factory: server is quiesced") )
Lifecycle errors. Serve reports these; Stop reports only what shutting a component down reported.
var ErrConflictingJournalResolvers = errors.New("factory: WithSessionJournalResolver and WithJournalResolver are alternatives; supply one")
ErrConflictingJournalResolvers reports a composition that supplies both WithSessionJournalResolver and the deprecated WithJournalResolver. They are alternatives for the same read plane, and neither silently shadows the other: preferring the deprecated one would read a Host's journal without the public session id its projection needs, and preferring the other would ignore a value the deployer supplied. It is reported as an *OptionError naming WithSessionJournalResolver.
var ErrConflictingObjectResolvers = errors.New("factory: WithSessionObjectStoreResolver and WithObjectStoreResolver are alternatives; supply one")
ErrConflictingObjectResolvers reports a composition that supplies both WithSessionObjectStoreResolver and the deprecated WithObjectStoreResolver. They are alternatives for the same route and neither silently shadows the other. It is reported as an *OptionError naming WithSessionObjectStoreResolver.
var ErrHostSessionsWithoutJournalResolver = errors.New("factory: WithPublicCreates and WithPendingCommands require WithSessionJournalResolver")
ErrHostSessionsWithoutJournalResolver reports a composition that creates or places Host-owned sessions (WithPublicCreates or WithPendingCommands) with no journal resolver -- neither WithSessionJournalResolver nor the deprecated WithJournalResolver -- to read their journals. It is reported as an *OptionError naming WithSessionJournalResolver. See WithSessionJournalResolver.
var ErrInvalidDirectoryLimits = routing.ErrInvalidConfig
ErrInvalidDirectoryLimits is the class of every NewStoreDirectory rejection.
var ErrNoEntropy = errors.New("factory: random source failed")
ErrNoEntropy reports that the random source could not supply the bytes an identifier is made of.
var ErrNoHintPublisher = errors.New("factory: this replica composes no journal-tip hint publisher")
ErrNoHintPublisher was what a journal-tip hint reported while this module composed no hint publisher. Since v0.4.0 the hint is published to the session's ClientLink channel, the same one the live tail uses, and nothing returns this any more.
Deprecated: nothing returns it; it is kept so a caller comparing against it still compiles.
Functions ¶
func UIRoutePrincipal ¶ added in v0.6.0
UIRoutePrincipal returns the verified principal Factory placed on a request before invoking a protected /ui/ route. The principal is a read-only value; it does not expose the credential source or a reusable authorization grant. A request outside Factory's authenticated handler has no such principal.
Types ¶
type AuditAuthorizer ¶ added in v0.12.0
type AuditAuthorizer interface {
AuthorizeAuditRead(context.Context, identity.Principal, sessionwire.SessionID) error
}
AuditAuthorizer optionally grants a command's principal and metadata on GET /v1/sessions/{sid}/commands/{cid}. Without it, public status remains readable but the audit members are omitted.
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 ¶
type Catalog interface {
GetCatalogEntry(ctx context.Context, req sessionstore.GetCatalogEntryRequest) (sessionstore.CatalogEntry, error)
CreateCatalogEntry(ctx context.Context, req sessionstore.CreateCatalogEntryRequest) (sessionstore.CatalogEntry, bool, error)
UpdateCatalogDesiredState(ctx context.Context, req sessionstore.UpdateCatalogDesiredStateRequest) (sessionstore.CatalogEntry, error)
}
Catalog is the durable session record: the read every decision is made against, and the desired state placement authors.
CreateCatalogEntry is still required but no Factory path calls it: legacy create is refused before any durable write, and a V1 create is authored through PublicCreates. Narrowing the interface is booked for a minor release, because removing a method from an exported interface breaks callers holding a Catalog.
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 {
// MaxMessageBytes bounds the data in one inbound command RPC. At the 64 KiB
// default, the WebSocket frame cap is exactly 64 KiB. When raised, the frame
// cap is MaxMessageBytes plus 16 KiB for the RPC envelope. A payload over
// MaxMessageBytes within that cap receives a typed invalid_request refusal;
// a frame over the cap closes the transport.
MaxMessageBytes int
// 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.39.3/node.go:146-147 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.39.3'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
// OwnershipPollInterval is the gap between registry polls for unbound watched sessions.
// Zero retains the five-second default for older keyed literals.
OwnershipPollInterval 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 ¶
Clock is the time seam.
func SystemClock ¶
func SystemClock() Clock
SystemClock is the default Clock: the standard library's.
type Commands ¶
type Commands interface {
ControlShards() int
AdmitDispositionCommand(ctx context.Context, req sessionstore.AdmitDispositionCommandRequest) (sessionstore.DispositionInboxEntry, bool, error)
GetDispositionCommand(ctx context.Context, req sessionstore.GetDispositionCommandRequest) (sessionstore.DispositionInboxEntry, error)
PutCommandPayload(ctx context.Context, req sessionstore.PutCommandPayloadRequest) (sessionwire.ObjectMetadata, error)
RejectDispositionCommand(ctx context.Context, req sessionstore.RejectDispositionCommandRequest) (sessionstore.DispositionInboxEntry, bool, error)
ListDueDispositionCommands(ctx context.Context, req sessionstore.ListDueDispositionCommandsRequest) (sessionstore.DispositionDueCommandPage, error)
RejectCommand(ctx context.Context, req sessionstore.RejectCommandRequest) (sessionstore.InboxEntry, error)
ListDueCommands(ctx context.Context, req sessionstore.ListDueCommandsRequest) (sessionstore.DueCommandPage, error)
AcquireReconciliationClaim(ctx context.Context, req sessionstore.AcquireReconciliationClaimRequest) (sessionstore.ReconciliationClaimEntry, error)
ReleaseReconciliationClaim(ctx context.Context, req sessionstore.ReleaseReconciliationClaimRequest) (sessionstore.ReconciliationClaimEntry, error)
}
Commands is the durable command plane. A *sessionstore.Store satisfies it.
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.
Every command is admitted into the DISPOSITION family -- the only one a Host can take residency on -- through AdmitDispositionCommand, with GetDispositionCommand as the retry read and PutCommandPayload for a payload too large to store inline. RejectDispositionCommand and ListDueDispositionCommands are the disposition deadline sweep, which rejects a command no Host applied before its apply deadline. RejectCommand and ListDueCommands remain for the LEGACY deadline sweep only, which settles legacy rows a store may already hold; nothing admits into that family.
type Directory ¶
type Directory interface {
Owner(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID) (sessionwire.HostLinkRegistryObservation, bool, error)
Candidates(ctx context.Context, req sessionstore.ListCompatibleHostsRequest) (sessionstore.HostTargetPage, error)
}
Directory is the observed target directory.
func NewStoreDirectory ¶ added in v0.2.0
func NewStoreDirectory(store DirectoryStore, limits DirectoryLimits) (Directory, error)
NewStoreDirectory returns the Directory this module reads a SessionStore through: session ownership from the epoch-fenced registry, and placement candidates from the ranked target index in the store's own capacity order.
It holds no routing state. Absence, expiry and a released tombstone are all "no owner" rather than errors, a lapsed candidate row spends a bounded slice of the reconcile budget and the original page is retried, and the continuation handed back is always the honest continuation of the page the caller received. Limits are validated before any store call; a zero DirectoryLimits is refused rather than defaulted, because a directory that silently took a page size a caller did not name is one whose cost the caller cannot state. Use DefaultDirectoryLimits.
type DirectoryLimits ¶ added in v0.2.0
DirectoryLimits bounds the store directory's foreground capacity query and the due cleanup it may provoke. It is an alias for the reason ObjectLimits is: one declaration, read by the package that enforces it.
func DefaultDirectoryLimits ¶ added in v0.2.0
func DefaultDirectoryLimits() DirectoryLimits
DefaultDirectoryLimits is what a composition naming no directory limits receives: a 32-row candidate page, 32-row reconcile pages, SessionStore's default reconcile page budget, and two cleanup attempts per query.
type DirectoryStore ¶ added in v0.2.0
type DirectoryStore interface {
GetHostRegistration(context.Context, sessionstore.GetHostRegistrationRequest) (sessionstore.HostRegistrationEntry, error)
ListCompatibleHosts(context.Context, sessionstore.ListCompatibleHostsRequest) (sessionstore.HostTargetPage, error)
ReconcileHostTargets(context.Context, sessionstore.ReconcileHostTargetsRequest) (sessionstore.HostTargetReconcileResult, error)
}
DirectoryStore is the SessionStore surface NewStoreDirectory reads: the per-session registry, the ranked target listing, and the due-row reconcile that a lapsed advertisement provokes. A *sessionstore.Store satisfies it.
The registry read and the capacity listing are separate methods here for the reason internal/routing keeps them separate: it makes turning an advertisement into an ownership claim impossible inside the directory, because nothing on this seam can answer "who owns the session" from a capacity row.
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 ¶
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. Since v0.5.0 a Host serves each
// tenant over its own HostLink, so one link multiplexes every session
// binding of ONE tenant to one Host, and this bounds (Host, tenant) pairs
// -- Hosts times the tenants this replica serves on each -- not sessions.
// A deployment that sized it by Hosts alone should re-size it.
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 JournalReader ¶ added in v0.9.0
type JournalReader interface {
ReadPublicJournal(ctx context.Context, req sessionstore.ReadPublicJournalRequest) (sessionwire.JournalPage, error)
}
JournalReader is the read of one session's RUNTIME journal: the journal a Host-owned session's runtime writes. A *sessionstore.Store opened over the runtime's own backend satisfies it -- for a Harness runtime that is sessionstore.Open(ctx, harnessBackend, sessionstore.WithLegacySingleTenant(tenant)), the layout Harness writes its journal in.
It is addressed by the binding's RuntimeSessionID, never by the public session id: Factory rewrites the request before it is called.
type JournalResolver
deprecated
added in
v0.9.0
type JournalResolver func(ctx context.Context, tenant sessionwire.TenantID, binding sessionstore.SessionBinding) (JournalReader, error)
JournalResolver answers the runtime journal a Host-owned session's binding names, for that session's tenant, and refuses any binding this deployment does not know.
The refusal is the point, for ObjectStoreResolver's reason: a resolver that answered a default for an unknown StorageBindingID or BindingVersion would serve one deployment's journal under another's configuration. Return the store's own typed errors (or wrap them with %w): the /journal route classifies a *sessionstore.JournalError and the catalog's absence errors, and anything else is answered 500.
Deprecated: use SessionJournalResolver, which is also handed the public session id -- the only id a Host's public-journal projection can scope a session's bodies to.
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, and since v0.11.0 it is answered 404 with the same body as an absent object (v0.10.0 answered 403), so a caller cannot tell a reference it may not read from one that does not exist. Any other error is a policy dependency fault (500). The kind returned must be sessionstore.ObjectKindToolResult; the route serves no other.
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 object resolver is REFUSED -- see WithObjectPolicy.
type ObjectReader ¶
type ObjectReader = httpapi.ObjectReader
ObjectReader is the neutral read capability a frozen binding resolves to.
type ObjectStoreResolver
deprecated
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.
The reader it returns is asked by the principal's tenant and the PUBLIC session id.
Deprecated: use SessionObjectStoreResolver. This resolver is handed neither the tenant nor the public session, and its reader is addressed by the public session id -- but a Host-owned session's objects (a Harness runtime's tool-result captures) live under the binding's RuntimeSessionID in the runtime's own store, so through this resolver they are never found.
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 ¶
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 WithCommands ¶
WithCommands supplies the durable command plane.
func WithCredentialVerifier ¶
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 ¶
WithDirectory supplies the observed target directory.
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 WithJournalResolver
deprecated
added in
v0.9.0
func WithJournalResolver(r JournalResolver) Option
WithJournalResolver supplies where a Host-owned session's journal is read.
A Host-owned session is DISPOSITION-bound, and its journal is not in the SessionStore WithSessionReader reads: the Host's runtime keeps it on its own backend, under the binding's RuntimeSessionID, and SessionStore refuses to write a public journal for a disposition session at all. Read through WithSessionReader alone, every Host session's journal is empty at tip 0 -- so /journal shows no history, the journal_tip hint says 0, and every live-tail repair after a delivered record cannot build a session.reset (Core refuses last_contiguous above journal_tip) and UNSUBSCRIBES the session's viewers instead of resetting them.
With it, every journal read Factory makes -- the /journal route (and so its journal_tip), the journal_tip hint an unbound watched session publishes, and the tip every live-tail repair resets to -- reads the catalog binding for the PUBLIC session id and, for a disposition-bound session, reads the resolved runtime journal under the binding's RuntimeSessionID. Cursors are wrapped so a page's continuation is bound to the public session and its binding. A session that is not disposition-bound (legacy) is read from WithSessionReader, exactly as before.
It is REQUIRED with WithPublicCreates or WithPendingCommands: a replica that creates or places Host sessions and cannot read their journals is refused by New with ErrHostSessionsWithoutJournalResolver.
Deprecated: use WithSessionJournalResolver. This resolver is not handed the public session id, so it cannot return a reader that projects a Host's runtime journal to public identities, and /journal then serves the runtime session and command ids a Host's bodies carry. It keeps its v0.9.0 behaviour, and cannot be combined with WithSessionJournalResolver.
func WithLogger ¶ added in v0.3.0
WithLogger supplies the logger for this module's operational diagnostics: today, a pooled candidate excluded because it does not advertise hostlink.attach, and a session with open commands that could not be placed. Optional; slog.Default() is used when it is not supplied.
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 an object resolver (WithSessionObjectStoreResolver, or the deprecated 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
deprecated
func WithObjectStoreResolver(r ObjectStoreResolver) Option
WithObjectStoreResolver supplies the binding-to-store resolution an object read uses. See WithObjectPolicy for why the two travel together.
Deprecated: use WithSessionObjectStoreResolver. This resolver's reader is addressed by the public session id, so a Host-owned session's objects, stored under its RuntimeSessionID, are never found through it. It keeps its v0.10.0 behaviour and cannot be combined with WithSessionObjectStoreResolver.
func WithPendingCommands ¶ added in v0.3.0
func WithPendingCommands(p PendingCommands) Option
WithPendingCommands supplies the durable query pooled placement is triggered from. Optional.
With it, a "placement" sweep runs on the reconcile interval: one control shard per pass, every session with a command still open and no live owner is attached to the first ranked admissible Host that advertises hostlink.attach and accepts, bound with the epoch that Host answered, and woken with its pending commands. The trigger is the durable inbox rather than admission so that a replica which did not admit a command -- because the one that did has died -- still places the session.
Without it, this replica places nothing: a session is made resident only by another replica composed with it, or by a Host that attaches itself.
func WithPlacementController
deprecated
func WithPlacementController(p PlacementController) Option
WithPlacementController supplies a PlacementController. Optional since v0.3.0, and nothing reads the value; see PlacementController.
Deprecated: nothing reads it. See PlacementController.
func WithPrincipalStamping ¶ added in v0.12.0
func WithPrincipalStamping() Option
WithPrincipalStamping stamps the verified sender on every admitted command. It is off by default and requires the already-mandatory credential verifier. Upgrade Hosts to ones advertising hostlink.attribution.principal before enabling it. A retry admitted before enablement changes identity, and attributed rows require every reader to stay on sessionstore >= v0.14.0.
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 ¶
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 WithRouteLimits ¶ added in v0.14.0
func WithRouteLimits(l RouteLimits) Option
WithRouteLimits replaces the REST request and command body limits.
func WithServiceIdentity ¶
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 ¶
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 an object resolver (WithSessionObjectStoreResolver, or the deprecated 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 ¶
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 WithSessionJournalResolver ¶ added in v0.10.0
func WithSessionJournalResolver(r SessionJournalResolver) Option
WithSessionJournalResolver supplies where a Host-owned session's journal is read, exactly as WithJournalResolver does, and hands the resolver the public session id each read is for, so a composition can return a reader that projects the runtime journal to public identities:
journals := host.NewPublicJournals(0)
factory.WithSessionJournalResolver(func(ctx context.Context, tenant sessionwire.TenantID,
session sessionwire.SessionID, binding sessionstore.SessionBinding) (factory.JournalReader, error) {
store, err := runtimeStore(ctx, tenant, binding) // refuses an unknown binding
if err != nil {
return nil, err
}
return journals.Reader(store, tenant, session, binding)
})
It is the replacement for WithJournalResolver, and satisfies the same requirement (WithPublicCreates and WithPendingCommands need one of them). Supplying both is refused by New with ErrConflictingJournalResolvers.
func WithSessionObjectStoreResolver ¶ added in v0.11.0
func WithSessionObjectStoreResolver(r SessionObjectStoreResolver) Option
WithSessionObjectStoreResolver supplies where a bound session's objects are read: GET|HEAD /v1/sessions/{sid}/objects/{oid} and its /metadata.
factory.WithSessionObjectStoreResolver(func(ctx context.Context, tenant sessionwire.TenantID,
session sessionwire.SessionID, binding sessionstore.SessionBinding) (factory.ObjectReader, error) {
return runtimeStore(ctx, tenant, binding) // refuses an unknown binding
})
What the route guarantees around it:
- It serves only with an ObjectPolicy (WithObjectPolicy). With none, every object read answers 503 "unavailable" before the catalog summary, the policy or this resolver -- the same answer v0.10.0 gave, kept so a composition that serves no objects is not moved.
- The policy is asked FIRST, with the catalog entry; this resolver is reached only for a reference it authorized. A denial (identity.ErrUnauthorized) answers the same 404 body as an object the store does not hold, so a caller cannot tell "not yours" from "not there". Evidence belongs to the policy, not to the store: SessionStore's metadata index is not authorization.
- Only ObjectKindToolResult is served. A policy answering any other kind is a policy fault (500), refused before this resolver is asked.
- Size is bounded by ObjectLimits: at most 1 MiB per page and 64 MiB of whole-object verification. A larger object answers 413 however it is paged, so a runtime's capture ceiling must stay within MaxVerificationBytes (I2.2 D7) -- that is the composition's to assert; Factory cannot see the runtime's ceiling.
- Tenant and session come from the principal and the catalog, never from the client; the object reference is resolved only within the addressed (tenant, runtime session) scope, so another session's reference is absent.
It is the replacement for WithObjectStoreResolver and satisfies the same requirements (WithObjectPolicy and WithSessionBinding need one of them). Supplying both is refused by New with ErrConflictingObjectResolvers.
func WithSessionReader ¶
func WithSessionReader(r SessionReader) Option
WithSessionReader supplies the durable read plane.
func WithUIHandler ¶
WithUIHandler mounts a caller-supplied user interface handler.
func WithUIRoutes ¶ added in v0.6.0
func WithUIRoutes(h http.Handler, authorize UIRouteAuthorizer) Option
WithUIRoutes mounts application-owned /ui/ routes behind Factory's authentication, origin/CSRF guard, and a required per-request authorizer. The handler can read the verified principal with UIRoutePrincipal. The ordinary UI handler remains a public asset and SPA fallback.
func WithUUIDSource ¶
func WithUUIDSource(u UUIDSource) Option
WithUUIDSource replaces the random identifier source.
func WithVersion ¶
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.
A Factory process composes none by default: H5 puts the platform adapter in the separate looprig/controller repository, 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 PendingCommands ¶ added in v0.3.0
type PendingCommands interface {
ControlShards() int
ListDueDispositionCommands(ctx context.Context, req sessionstore.ListDueDispositionCommandsRequest) (sessionstore.DispositionDueCommandPage, error)
}
PendingCommands is the disposition inbox's service-plane due query: the durable record of every session with open work. A *sessionstore.Store satisfies it.
It enables immediate admission-triggered placement and the durable recovery sweep. Each sweep interval pages one control shard for commands still open; see WithPendingCommands.
type PlacementController
deprecated
type PlacementController interface {
EnsurePlacement(ctx context.Context, desired sessionstore.DesiredWorkload) error
ReleasePlacement(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID) error
}
PlacementController is the placement seam this module shipped before it had a placement caller.
It is NO LONGER REQUIRED, and nothing reads it. EnsurePlacement takes a DesiredWorkload, which names no tenant, session or generation, so no implementation could identify what it was asked to place; H5 put the platform adapter behind WorkloadController instead, and pooled placement is driven by this module's own HostLink attach (see WithPendingCommands). The option is still ACCEPTED, so a composition that supplies one keeps composing; removing the type or the option would break that caller for no gain.
Deprecated: nothing reads it. Pooled placement attaches through the HostLink and dedicated placement goes through WorkloadController.
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 admission workers. A placement sweep can run alongside them.
MaxConcurrent int
// PassTimeout bounds one sweep or admission-triggered placement pass.
// Zero uses the default for older keyed limits literals.
PassTimeout time.Duration
}
ReconcileLimits bounds Factory's periodic reconciliation and immediate placement of admitted commands whose sessions have 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 RouteLimits ¶ added in v0.14.0
type RouteLimits = httpapi.RouteLimits
RouteLimits bounds REST JSON bodies and request work.
func DefaultRouteLimits ¶ added in v0.14.0
func DefaultRouteLimits() RouteLimits
DefaultRouteLimits returns the REST defaults.
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 ¶
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 ¶
Handler is Factory's public HTTP surface: the API under /v1, protected application routes under /ui/ when supplied, and the injected user interface on other paths when 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) Quiesce ¶ added in v0.7.0
Quiesce permanently closes public admission and waits for every command admission already inside the shared service to return. Sweeps and HostLinks continue running until Stop. Canceling ctx only ends this caller's wait.
func (*Server) ReconcileLimits ¶
func (s *Server) ReconcileLimits() ReconcileLimits
ReconcileLimits returns the composed reconciliation limits.
func (*Server) RouteLimits ¶ added in v0.14.0
func (s *Server) RouteLimits() RouteLimits
RouteLimits returns the configured REST request and command limits.
func (*Server) Serve ¶
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 ¶
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:
- 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.
- 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 ¶
Stop shuts the Server down in a fixed order and does not return until each component it stopped has stopped.
Quiesce first fences durable command admission and closes ClientLinks while leaving sweeps and HostLinks live. Stop then closes the listener and the remaining background components.
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 ¶
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: Factory ships no UI and no binary, so a product mounts its own bundle or handler through WithUIHandler/WithUIFS, and an embedder that mounts 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 SessionJournalResolver ¶ added in v0.10.0
type SessionJournalResolver func(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, binding sessionstore.SessionBinding) (JournalReader, error)
SessionJournalResolver is JournalResolver handed, as well, the PUBLIC session id the read is for.
A Host's runtime journal carries runtime identities (the binding's RuntimeSessionID, runtime command ids) that must never reach a client, and a Host projects them to public ones (host.NewPublicJournals). That projection needs the public session id, and nothing else a resolver is handed carries it: the binding's RuntimeSessionID is a one-way derivation of the public id, and the request the returned JournalReader is asked is already addressed by the RuntimeSessionID.
session is the id Factory routed the read for: the authorized /journal route's session, or the watched session a tip hint, live-tail repair or gap probe is for. It is the id the catalog entry -- and so binding -- was read by, never a value taken from the runtime journal or a cursor. The same refusal rules as JournalResolver apply: refuse any binding this deployment does not know, and return SessionStore's typed errors (or wrap them).
type SessionObjectStoreResolver ¶ added in v0.11.0
type SessionObjectStoreResolver func(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, binding sessionstore.SessionBinding) (ObjectReader, error)
SessionObjectStoreResolver answers the store a bound session's objects live in, and is the object route's counterpart of SessionJournalResolver.
It is handed the principal's tenant, the PUBLIC session id the route was authorized for (the id the catalog entry -- and so binding -- was read by, never a value taken from the request body or the store), and the catalog's full immutable binding. It must refuse any binding this deployment does not know, for ObjectStoreResolver's reason.
The reader it returns is asked with the principal's tenant and the binding's RUNTIME session id: Factory rewrites every GetObjectMetadata and GetObject request before it is called, exactly as it rewrites a journal read. For a Harness runtime that reader is the per-tenant runtime store -- sessionstore.Open(ctx, harnessBackend, sessionstore.WithLegacySingleTenant(tenant)), the same store WithSessionJournalResolver answers -- where Harness writes a tool-result capture under the runtime session. The runtime session id never reaches a client: object metadata names no session.
It is consulted only for a session with a non-zero binding; an unbound (legacy) session's objects are read from WithSessionReader under the public id, unchanged. And it is consulted only AFTER the ObjectPolicy has authorized the reference -- see WithSessionObjectStoreResolver.
type SessionReader ¶
type SessionReader interface {
ListSessions(ctx context.Context, req sessionstore.ListSessionsRequest) (sessionstore.SessionPage, error)
GetCatalogEntry(ctx context.Context, req sessionstore.GetCatalogEntryRequest) (sessionstore.CatalogEntry, error)
ReadPublicJournal(ctx context.Context, req sessionstore.ReadPublicJournalRequest) (sessionwire.JournalPage, error)
ReadGates(ctx context.Context, req sessionstore.ReadGatesRequest) (sessionwire.GatePage, error)
GetObject(ctx context.Context, req sessionstore.GetObjectRequest) (io.ReadCloser, error)
GetObjectMetadata(ctx context.Context, req sessionstore.GetObjectMetadataRequest) (sessionwire.ObjectMetadata, error)
}
SessionReader is the durable read plane.
Its ReadPublicJournal is keyed by the PUBLIC session id, so it answers the journal of a legacy-bound session only. A Host-owned (disposition) session's journal lives on the runtime's backend under its binding's RuntimeSessionID and is read through WithSessionJournalResolver; without one it reads empty at tip 0.
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 TenantAuthorizer ¶ added in v0.2.0
type TenantAuthorizer struct{}
TenantAuthorizer is the tenant-boundary Authorizer this module composes for its own tests and the one a deployment with no finer policy uses.
Its zero value is ready to use and it holds no state. Every tenant-scoped decision is "the principal holds a tenant", every channel decision is "the channel's tenant is the principal's", and the one cross-tenant decision -- the service sweep -- requires a service principal. It reads none of the resource identifiers it is handed, because a tenant-local identifier is not authority and consulting it before authorizing would disclose existence; the tenant boundary is then applied by every SessionStore request being built with the authenticated principal's tenant. A denial is identity.ErrUnauthorized and carries no identifier.
It forwards to internal/identity.Authorizer method for method rather than embedding it, so the exported surface names no internal type; TestTenantAuthorizerIsTheInternalAuthorizer holds the two method sets equal.
func (TenantAuthorizer) AuthorizeAuditRead ¶ added in v0.12.0
func (TenantAuthorizer) AuthorizeAuditRead(ctx context.Context, principal identity.Principal, session sessionwire.SessionID) error
func (TenantAuthorizer) AuthorizeControl ¶ added in v0.2.0
func (TenantAuthorizer) AuthorizeControl(ctx context.Context, principal identity.Principal, session sessionwire.SessionID, kind sessionstore.CommandKind) error
func (TenantAuthorizer) AuthorizeObjectRead ¶ added in v0.2.0
func (TenantAuthorizer) AuthorizeObjectRead(ctx context.Context, principal identity.Principal, session sessionwire.SessionID, object sessionwire.ObjectReference) error
func (TenantAuthorizer) AuthorizeServiceSweep ¶ added in v0.2.0
func (TenantAuthorizer) AuthorizeSessionList ¶ added in v0.2.0
func (TenantAuthorizer) AuthorizeSessionRead ¶ added in v0.2.0
func (TenantAuthorizer) AuthorizeSessionRead(ctx context.Context, principal identity.Principal, session sessionwire.SessionID) error
func (TenantAuthorizer) AuthorizeSubscribe ¶ added in v0.2.0
type UIRouteAuthorizer ¶ added in v0.6.0
UIRouteAuthorizer decides access to one request under /ui/. A nil error grants access; identity.ErrUnauthorized denies it, and other errors are faults. Factory authenticates and applies its origin/CSRF guard before calling it.
type UUIDSource ¶
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
ObserveWorkload(ctx context.Context, intent sessionstore.PlacementIntent) (sessionwire.HostLinkRegistryObservation, bool, error)
RequestDrain(ctx context.Context, intent sessionstore.PlacementIntent) (sessionwire.HostLinkDrainObservation, error)
DeleteWorkload(ctx context.Context, intent sessionstore.PlacementIntent) error
}
WorkloadController owns the lifecycle of a dedicated session's workload.
It is OPTIONAL and only the controller binary supplies it. H5 (answered 2026-09-04) puts the platform adapter in a separate controller, so a Factory process 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. The lifecycle observations are Core records, so a Kubernetes PodSpec, a Nomad job and a future platform's manifest remain behind the adapter boundary.
type WorkloadEndpointDiscovery ¶ added in v0.6.0
type WorkloadEndpointDiscovery interface {
WorkloadEndpoint(ctx context.Context, intent sessionstore.PlacementIntent) (sessionwire.HostID, uint64, sessionwire.InternalEndpoint, bool, error)
}
WorkloadEndpointDiscovery is the optional pre-attach half of a dedicated workload controller. It returns the Host ID, desired generation and bare internal endpoint only once the intended workload is ready. The endpoint is a dial target, not a registry observation or proof of residency. Factory checks the generation and Core's bare-base rules; the controller must verify that the Host ID and endpoint belong to the exact intent and that its workload is ready before returning true. HostLink Attach then checks agent, runtime compatibility, session and the Host fence under the Host lease. Factory refuses a dedicated attach when a controller lacks this method; older controllers remain usable for lifecycle operations but cannot place a new dedicated session.
Source Files
¶
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. |
|
modfiles/cmd/modfiles
command
|
|
|
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/livetail
Package livetail carries a watched session's live output from the Host that owns it to the ClientLink viewers watching it on this replica.
|
Package livetail carries a watched session's live output from the Host that owns it to the ClientLink viewers watching it on this replica. |
|
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. |