bootstrap

package
v0.1.0-develop.2026060... Latest Latest
Warning

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

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 57 Imported by: 0

Documentation

Overview

Package bootstrap orchestrates the full GoCell application lifecycle: config loading, assembly init/start, HTTP serving, event subscriptions, background workers, and graceful shutdown.

ref: uber-go/fx app.go — Run/Start/Stop lifecycle, withRollback pattern Adopted: sequential startup with transactional rollback on failure; LIFO shutdown order for safe resource cleanup. Deviated: explicit typed options instead of DI container; direct signal handling via runtime/shutdown.Manager.

Package bootstrap provides a unified application lifecycle manager for GoCell.

It orchestrates config loading, assembly init/start, HTTP serving, event subscriptions, background workers, and graceful shutdown in a single Run call.

Example — note PrimaryListener carries a real auth chain (JWT discovered from the assembly's authProvider Cell at phase4). AuthNone is reserved for loopback-isolated listeners such as HealthListener. SEC-FAIL-CLOSED: nil authChain is rejected at phase0 (`ErrListenerAuthChainMissing`); explicit no-auth must use `auth.AuthNone{}`.

clk := clock.Real()
jwtAuth, err := auth.NewAuthJWTFromAssembly(asm)
if err != nil { /* handle */ }
app := bootstrap.New(
    clk,
    bootstrap.WithAssembly(asm),
    bootstrap.WithListener(cell.PrimaryListener, ":8080",
        []auth.ListenerAuth{jwtAuth}),
    bootstrap.WithListener(cell.HealthListener, "127.0.0.1:9091",
        []auth.ListenerAuth{auth.AuthNone{}}), // loopback-isolated
    bootstrap.WithPublisher(pub),
    bootstrap.WithSubscriber(sub),
)
if err := app.Run(ctx); err != nil { ... }

Index

Constants

View Source
const (
	// DefaultStartTimeout is the default per-hook StartTimeout. Since ADR
	// 202605170000 §D-B it is NOT an enforced OnStart ctx deadline — it is the
	// slow-start warning threshold only (warn when OnStart elapsed ≥ 80% of
	// it). The whole-Start deadlock backstop is the orchestration-layer
	// supervision in Bootstrap.Run (caller ctx + WithStartupTimeout), not this.
	DefaultStartTimeout = 30 * time.Second
	// DefaultStopTimeout is the default per-hook stop deadline.
	DefaultStopTimeout = 10 * time.Second

	// DefaultStartupTimeout is the default whole-Start orchestration budget
	// (Bootstrap.Run supervises lifecycle.Start with caller ctx + this timer).
	// It is a deadlock backstop, NOT a per-hook SLA: well-behaved hooks
	// fast-probe-return far inside it. Override with WithStartupTimeout;
	// negative disables the timer (caller-ctx-only abort).
	DefaultStartupTimeout = 30 * time.Second
)
View Source
const (

	// DefaultBootstrapHTTPWriteTimeout is the http.Server WriteTimeout.
	// Exported so cross-package consumers (e.g. cmd/corebundle metrics
	// handler scrape-timeout sanity tests) can pin against the same source
	// of truth instead of duplicating the literal.
	DefaultBootstrapHTTPWriteTimeout = 30 * time.Second
)

Variables

View Source
var ErrBootstrapStartupTimeout = errcode.New(errcode.KindInternal, errcode.ErrBootstrapLifecycle, "lifecycle startup exceeded budget")

ErrBootstrapStartupTimeout is returned by Bootstrap.Run when lifecycle.Start does not complete within the orchestration-layer startup budget (WithStartupTimeout, default DefaultStartupTimeout). It is the deadlock backstop for a hook whose OnStart never returns (ADR 202605170000 §D-B amendment / review P1-1): caller-ctx cancellation is the primary abort, this sentinel covers "caller never cancels and an OnStart wedged".

Reuses ErrBootstrapLifecycle so no new errcode sentinel/Kind is introduced (contract-fanout not triggered). Callers may errors.Is against it.

View Source
var ErrDuplicateHookName = errcode.New(errcode.KindInternal, errcode.ErrBootstrapLifecycle, "duplicate lifecycle hook name")

ErrDuplicateHookName is returned by Append when a non-empty Hook.Name has already been registered. The single source of truth for duplicate-name detection lives here so that phase3b (cell.Registrar.Lifecycle snapshot drain) and WithLifecycle (explicit composition-root Append) share the same guard without having to re-synchronize per-path "seen" maps.

Empty Name bypasses the check — callers that deliberately register nameless hooks accept the risk of duplicates (diagnostic cost, not correctness).

View Source
var ErrLifecycleAlreadyStarted = errcode.New(errcode.KindInternal, errcode.ErrBootstrapLifecycle, "lifecycle already started")

ErrLifecycleAlreadyStarted is returned by Append when the lifecycle has already started, and by Start when Start is called more than once.

Callers may use errors.Is(err, ErrLifecycleAlreadyStarted) for sentinel checks; the pointer identity is stable for the lifetime of the process.

Functions

func HealthRouteGroups

func HealthRouteGroups(h *health.Handler, opts ...HealthRouteGroupOption) []cell.RouteGroup

HealthRouteGroups returns one RouteGroup per framework-owned health route (/healthz, /readyz, optional /metrics) on the HealthListener. The HealthListener is wired with an explicit no-auth chain ([]auth.ListenerAuth{auth.AuthNone{}}; phase0 rejects a nil chain), so health probes are reachable without a token; the verbose disclosure on /readyz is gated by WithReadyzVerboseToken at the handler layer.

A nil/zero metrics handler omits the /metrics route entirely.

Types

type Bootstrap

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

Bootstrap orchestrates the GoCell application lifecycle.

Fields are flat: bootstrap is the composition root, and most options influence behavior across multiple phases (e.g. WithMetricsProvider feeds both default-assembly construction and HTTP metric auto-wiring; WithRateLimiter writes both router options and the closer list). Forcing a "concern group" sub-struct layout would make those cross-cutting consumptions look like boundary violations when in fact they are the natural shape of a composition root. fx.App and kratos.App keep their state flat for the same reason; controller-runtime only sub-groups state when each group is independently start/stop-able as a batch.

File-level decomposition (phases_assembly.go / phases_http.go / phases_events.go / phases_workers.go / phases_shutdown.go etc.) is orthogonal to struct grouping and intentionally retained: it splits this file along phase ordering, not along ownership.

ref: uber-go/fx app.go — App is a flat struct. ref: go-kratos/kratos app.go — App is a flat struct. ref: sigs.k8s.io/controller-runtime pkg/manager/internal.go — controllerManager

sub-groups runnables by lifecycle batch (HTTPServers / Webhooks / Caches),
not by visual concern.

func New

func New(clk clock.Clock, opts ...Option) *Bootstrap

New creates a Bootstrap with the given options.

ShutdownCollector metrics are registered against the provider here (plan option B): instruments live as long as the Bootstrap, matching the "register at start-up" convention used by relay_collector.go and the hook dispatcher. On registration failure the error is stored and surfaced by Run() at phase0, before any side effects start.

func (*Bootstrap) Lifecycle

func (b *Bootstrap) Lifecycle() Lifecycle

Lifecycle returns the bootstrap's Lifecycle for programmatic Hook registration. Must be called after New() returns and before Run() begins; not goroutine-safe concurrent with Run(). Hooks registered here are appended to those from WithLifecycle options.

func (*Bootstrap) MetricsProvider

func (b *Bootstrap) MetricsProvider() kernelmetrics.Provider

MetricsProvider returns the configured provider-neutral metrics backend. The returned Provider is never nil; when no WithMetricsProvider option is used the NopProvider default surfaces, so callers can register metrics unconditionally.

func (*Bootstrap) Run

func (b *Bootstrap) Run(ctx context.Context) error

Run executes the full startup sequence. It blocks until ctx is canceled (or a signal is received), then performs orderly shutdown.

Health listener required (#673): framework health routes (/healthz, /readyz, /metrics) are mounted only on a dedicated cell.HealthListener. When none is declared, phase0 fails fast — there is no silent fallback onto the public PrimaryListener. Every deployment (production and tests alike) must declare WithListener(cell.HealthListener, ...); tests use an ephemeral "127.0.0.1:0" bind. This physically separates health traffic from business traffic.

The ten phases and their responsibilities:

phase0: validate all options before any side effects
phase1: load config + create watcher + register middleware closers
phase2: init publisher/subscriber (default InMemoryEventBus)
phase3: init and start assembly; register LIFO teardown
phase4: discover auth verifier; bind config-watcher OnChange; start watcher
phase5: build HTTP router + health handler; register all health checkers
phase6: register event subscriptions; start event router on runCtx
phase7: start HTTP server; wire httpErrCh + s.httpDrain (NOT a LIFO teardown)
phase7b: start gRPC servers in parallel; wire grpcErrCh + s.grpcDrain (NOT a LIFO teardown)
phase8: start worker group on runCtx; wire workerErrCh
phase9: block until external ctx cancel, HTTP/gRPC error, worker error, or router error
phase10: explicit shutdown stages — runs in this order:
         stage1: readiness flip (/readyz=503 + preShutdownDelay)
         stage2: HTTP + gRPC drain (s.httpDrain / s.grpcDrain — stop accept + drain in-flight)
         stage3: LIFO teardown (workers, event router, assembly, kernel
                                lifecycle, closers, managed resources)
         stage4: finalize      (cancel runCtx + outcome metric)

runCtx is derived from context.Background(), NOT from the caller ctx. External ctx cancellation only triggers phase9 to return; workers and the event router continue until their phase10 teardown functions run.

Exception (review P1-1): the lifecycle.Start step is additionally supervised by the caller ctx + a startup budget (WithStartupTimeout). A hook whose OnStart never returns would otherwise wedge Run() forever because ownerCtx is background-derived and OnStart carries no per-hook deadline (ADR 202605170000 §D-B). superviseLifecycleStart cancels ownerCtx and rolls back on caller-cancel / budget-exceeded so Run() always makes progress.

ref: uber-go/fx app.go (Run/Start/Stop lifecycle, withRollback pattern) ref: sigs.k8s.io/controller-runtime pkg/manager/internal.go (engageStopProcedure LIFO)

type GRPCListenerOption

type GRPCListenerOption func(*grpcListenerConfig)

GRPCListenerOption configures a single gRPC listener within WithGRPCListener.

func WithGRPCListenerNet

func WithGRPCListenerNet(ln net.Listener) GRPCListenerOption

WithGRPCListenerNet injects a pre-bound net.Listener (e.g. a bufconn listener in tests, or a TCP socket bound by the caller). When set, addr is used only for logging. nil is stored as-is; phase7b then binds addr via net.Listen.

func WithGRPCListenerShutdownGrace

func WithGRPCListenerShutdownGrace(d time.Duration) GRPCListenerOption

WithGRPCListenerShutdownGrace sets the per-listener drain budget passed to GRPCServer.Close in phase10 stage2. Zero inherits the global shutdownTimeout; a negative value is rejected at phase0 (ErrCellInvalidConfig). Named for symmetry with the HTTP WithListenerShutdownGrace.

type GRPCServer

type GRPCServer interface {
	// Serve serves RPCs on the pre-bound listener until ctx is canceled or the
	// server stops (e.g. via Close). It blocks; bootstrap calls it in a goroutine.
	Serve(ctx context.Context, lis net.Listener) error
	// Close initiates a graceful drain of in-flight RPCs bounded by ctx, hard
	// stopping if the budget is exceeded. Idempotent.
	Close(ctx context.Context) error
	// Registrar returns the cell-facing service registrar. bootstrap calls
	// Registrar().Register(spec) in phase7b for each GRPCServiceSpec drained from
	// the cell snapshots, before grpcServeAll.
	Registrar() GRPCServiceRegistrar
	// Probes returns the server's readiness probes (grpc_ready). bootstrap
	// collects them into the health aggregator at Run() start
	// (expandGRPCServerProbes), the same way WithManagedResource collects a
	// ManagedResource's probes — the gRPC server is wired via WithGRPCListener
	// (Serve/Close/Registrar), not WithManagedResource, so its Probes() are not
	// otherwise collected (#1152).
	Probes() []healthz.Probe
}

GRPCServer is the narrow lifecycle contract bootstrap needs from a gRPC server. adapters/grpc.Server satisfies it (Serve serves a pre-bound listener; Close gracefully drains in-flight RPCs bounded by the passed ctx; Registrar returns the cell-facing service registrar used by the drain in phase7b). Defining it here — rather than importing the adapter — keeps runtime/bootstrap free of any adapters/ dependency (LAYER-03 / GRPC-ADAPTER-LAYER-01).

type GRPCServiceRegistrar

type GRPCServiceRegistrar = cell.GRPCServiceRegistrar

GRPCServiceRegistrar is a local alias for cell.GRPCServiceRegistrar (the canonical definition in kernel/cell/grpc_service.go). The alias lets the bootstrap-package GRPCServer interface and bootstrap-package tests name the type without an additional import path; the canonical definition lives in kernel/cell so both adapters/grpc and bootstrap can import it without an import cycle.

type HealthRouteGroupOption

type HealthRouteGroupOption func(*healthRouteGroupCfg)

HealthRouteGroupOption customizes the route groups returned by HealthRouteGroups. Use WithMetricsHandler / WithReadyzVerboseToken / WithReadyzVerboseDisabled.

func WithMetricsHandler

func WithMetricsHandler(h http.Handler) HealthRouteGroupOption

WithMetricsHandler installs an http.Handler at /metrics on the HealthListener. nil is a no-op (the /metrics route is not registered).

func WithReadyzVerboseDisabled

func WithReadyzVerboseDisabled() HealthRouteGroupOption

WithReadyzVerboseDisabled suppresses the /readyz?verbose body entirely. The endpoint still answers, but the aggregate body is returned regardless of the ?verbose query parameter — internal topology (cell names, dependency names) is never disclosed.

Use this for ephemeral deployments (test harnesses, single-node demos) that waive the verbose debug channel. Production deployments should attach PolicyVerboseToken instead so operators retain a token-gated diagnostic path. Equivalent to PR-A35's bootstrap.WithVerboseDisabled, threaded through the WithHealthRoutes option pattern.

func WithReadyzVerboseToken

func WithReadyzVerboseToken(token string) HealthRouteGroupOption

WithReadyzVerboseToken plumbs a verbose-mode disclosure token to the health.Handler's strict-gate path. Requests with ?verbose=true must carry a matching X-Readyz-Token header; mismatches receive 401 ErrReadyzVerboseDenied from health.Handler.SetVerboseToken / sendVerboseDenied (canonical envelope via httputil.WritePublic).

Note: verbose-token is a disclosure gate, not an authentication scheme — it only controls whether the verbose body is rendered. Listener-level auth (kauth.NewAuthJWTFromAssembly, kauth.NewAuthServiceToken, etc.) is orthogonal.

Empty token leaves the gate disabled — verbose requests then render plain body unless WithReadyzVerboseDisabled is set.

type Hook

type Hook struct {
	CellID string // runtime-stamped by phase3b; "" for WithLifecycle-appended hooks
	Name   string // diagnostic name (log dimension)

	// OnStart is called with the owner ctx (long-lived, not a startup-deadline
	// ctx). Bootstrap passes the ownerCtx derived from runCtx directly, without
	// wrapping in a StartTimeout deadline. StartTimeout is retained as the hook's
	// self-declared probe-window budget (informational; runner does not enforce).
	//
	// Supersedes ADR 202605102000 §D1 ("OnStart ctx carries startup-deadline
	// semantics"). The new contract aligns with controller-runtime
	// Runnable.Start(managerCtx): the hook should spawn a long-running goroutine
	// on ownerCtx, run a fast synchronous probe, and return.
	//
	// OnStart hooks run sequentially in the Start goroutine; a slow or blocking
	// OnStart delays all subsequent hooks, and the aggregate elapsed time may
	// exceed WithStartupTimeout — later hooks then never get a chance to start.
	// Implementations MUST spawn their worker goroutine and fast-probe-return,
	// respecting ctx for cooperative cancellation.
	//
	// ref: kubernetes-sigs/controller-runtime pkg/manager/internal.go (engageStopProcedure)
	OnStart func(ctx context.Context) error // nil = no-op

	// OnStop carries a context with StopTimeout deadline applied by the runner.
	OnStop func(ctx context.Context) error // nil = no-op
	// StartTimeout is the hook's self-declared probe budget. Informational only —
	// the runner does NOT enforce it as an OnStart ctx deadline (see ADR
	// 202605102000 §D1 RETRACTED). Used only for slow-start warning threshold.
	// Previously the runner applied context.WithTimeout(StartTimeout) to OnStart;
	// that behavior was retracted when OnStart ctx was redefined as owner ctx
	// (superseded by ADR 202605170000 §D-B).
	StartTimeout time.Duration
	StopTimeout  time.Duration // 0=use default, <0=no timeout
}

Hook is a pair of lifecycle callbacks invoked in Append order on Start and reverse order on Stop. A zero-value OnStart/OnStop means no-op.

The CellID field is stamped by phase3b when a hook is drained from a cell's Registry.Lifecycle snapshot; hooks appended via bootstrap.WithLifecycle leave it empty. It is a runtime-only observability dimension (not mirrored on cell.LifecycleHook) — cells never self-declare their identity here, by analogy with uber-go/fx's unexported Hook.callerFrame which the framework fills in at Append time rather than trusting the caller to pass it.

ref: github.com/uber-go/fx internal/lifecycle/lifecycle.go (callerFrame) ref: kubernetes/kubernetes pkg/kubelet/lifecycle/handlers.go

(containerName + pod structured slog fields, not name-encoded)

type Lifecycle

type Lifecycle interface {
	Append(h Hook) error
	Start(ctx context.Context) error
	Stop(ctx context.Context) error
}

Lifecycle manages an ordered Hook sequence.

Five-state machine: stopped → starting → (incompleteStart | started) → stopping → stopped.

func NewLifecycle

func NewLifecycle(clk clock.Clock, cfg LifecycleConfig) Lifecycle

NewLifecycle creates a new Lifecycle with the given config.

type LifecycleConfig

type LifecycleConfig struct {
	DefaultStartTimeout time.Duration // 0 → DefaultStartTimeout constant
	DefaultStopTimeout  time.Duration // 0 → DefaultStopTimeout constant
	Logger              *slog.Logger  // nil → slog.Default()
}

LifecycleConfig configures a Lifecycle instance.

type ListenerOption

type ListenerOption func(*listenerConfig)

ListenerOption configures a single listener within WithListener.

func WithListenerNet

func WithListenerNet(ln net.Listener) ListenerOption

WithListenerNet injects a pre-bound net.Listener into the listener config. When set, the addr argument to WithListener is used only for logging — the socket is already bound. Passing nil stores nil; phase0 validation rejects a nil net listener only when addr is also empty.

func WithListenerShutdownGrace

func WithListenerShutdownGrace(d time.Duration) ListenerOption

WithListenerShutdownGrace sets the graceful shutdown duration for this listener. A negative value is stored as-is; phase0 rejects negative shutdown grace durations. Zero means inherit the global shutdownTimeout.

func WithListenerTLS

func WithListenerTLS(cfg *tls.Config) ListenerOption

WithListenerTLS sets the TLS configuration for the listener. Passing nil stores nil; phase0 ignores a nil TLS config (plain-text mode).

type Option

type Option func(*Bootstrap)

Option configures a Bootstrap instance.

func WithAdapterInfo

func WithAdapterInfo(info map[string]string) Option

WithAdapterInfo sets static adapter configuration metadata that is exposed in /readyz?verbose output. Helps operators verify which storage/bus backends are active without inspecting application logs.

func WithAssembly

func WithAssembly(asm *assembly.CoreAssembly) Option

WithAssembly sets a pre-built CoreAssembly.

func WithAssemblyID

func WithAssemblyID(id string) Option

WithAssemblyID sets only the `cell_id` label used by the auto-wired HTTP metrics collector (R2). It does not change assembly identity, cell metadata, routing, health, tracing, or non-HTTP metrics.

Recommended to set this matching asm.ID() when using WithAssembly(asm); omit to reuse assembly ID (auto-derived). Explicit value overrides assembly-derived.

When neither WithAssemblyID nor WithAssembly is used, Bootstrap defaults to "default" (the ID of the auto-built assembly).

func WithCircuitBreaker

func WithCircuitBreaker(cb middleware.Allower) Option

WithCircuitBreaker enables circuit breaker protection for HTTP requests. The breaker is forwarded to the router's middleware chain via router.WithCircuitBreaker. Also registers the resource for LIFO teardown via b.closers. If the breaker implements lifecycle.ContextCloser or io.Closer, Bootstrap registers it for teardown on shutdown and startup rollback. ContextCloser is preferred so the tearCtx (stage 3 budget; see WithShutdownTimeout godoc) flows through to the resource.

Both bare-nil and typed-nil (non-nil interface holding a nil pointer) are rejected at phase0 with a fatal error so operators are not silently left without circuit-breaker protection.

ref: go-zero — resilience middleware configuration at app level ref: kubernetes/kubernetes apiserver — option fail-fast at startup ref: uber-go/fx lifecycle OnStop(ctx) — ContextCloser preferred over io.Closer

func WithConfig

func WithConfig(yamlPath, envPrefix string) Option

WithConfig sets the YAML config path and environment prefix.

func WithConfigEventCollector

func WithConfigEventCollector(c obmetrics.ConfigEventCollector) Option

WithConfigEventCollector injects a ConfigEventCollector for config-event settlement observability. The collector is applied at the SubscriberHandler layer via WrapConfigEventSubscriber inside buildEventRouter, so settlement metrics are recorded after final broker disposition rather than inside the EntryHandler.

Nil inputs are silently ignored (cumulative builder noop pattern). When this option is not called, NoopConfigEventCollector is used.

func WithConsumerBase

func WithConsumerBase(cb *outbox.ConsumerBase) Option

WithConsumerBase injects a ConsumerBase into the SubscriberWithMiddleware used by the event router. The ConsumerBase is the explicit EntryHandler→SubscriberHandler conversion boundary: after the business middleware chain, ConsumerBase.Wrap adds idempotency (Claim/Commit/Release) and exponential-backoff retry.

When any cell registers subscriptions, phase6 fails fast if this option was not supplied. Composition roots that consume events must wire a ConsumerBase explicitly so idempotency and final settlement lifecycle are not optional by accident.

ref: uber-go/fx app.go Invoke — constructor-injected dependency, not middleware-position injection.

func WithConsumerMiddleware

func WithConsumerMiddleware(mw ...outbox.SubscriptionMiddleware) Option

WithConsumerMiddleware registers business subscriber-side middleware applied to every topic's EntryHandler before ConsumerBase idempotency is applied. Middleware is applied in registration order; each entry wraps the next, so the first registered middleware is outermost at invocation time.

Business middleware operates on EntryHandler (not SubscriberHandler) — it does not see Settlement. ConsumerBase is field-injected into SubscriberWithMiddleware via WithConsumerBase and applied as the EntryHandler→SubscriberHandler conversion boundary after the business middleware chain. Observability context restoration (entry.Observability → ctx) is the outermost step inside SubscriberWithMiddleware.SubscribeEntry, so middleware registered here always sees a context populated with trace_id/request_id/correlation_id.

ref: ThreeDotsLabs/watermill message/router.go — AddMiddleware wraps handlers at router level; MassTransit UseMessageRetry — pipeline middleware at receive-endpoint configuration.

func WithControlPlaneTopology

func WithControlPlaneTopology(topo Topology) Option

WithControlPlaneTopology supplies the resolved deployment Topology so phase0 can validate that the listener service-token guard's actual NonceStore is replay-safe for the topology (#1410 review F1): an in-memory store is rejected for real multi-pod deployments, and an unrecognized kind is rejected fail-closed. composition.Builder.Build injects this from its trusted SharedDeps.Topology so the store that ACTUALLY guards /internal/v1/* — not just the declared SharedDeps.NonceStore — is checked at the real usage point (mirrors fx.ValidateApp: validate the constructed graph, not a parallel field).

Topology is a sealed value type (NewTopology / TopologyFromEnv only), so a caller cannot forge a permissive value via a struct literal. Omitting this option leaves the zero Topology (RequireProductionControlPlane()==false), which skips the topology-dependent replay check — identical to prior behavior for hand-written bootstraps. The always-on nil/noop/ring service-token checks (validateAuthServiceTokenPlan) run regardless.

func WithDevtoolsCatalog

func WithDevtoolsCatalog(
	pm *metadata.ProjectMeta,
	root string,
	pkgGraph *kerneldepgraph.Graph,
	wireSummaries ...[]metadata.CellWireSummary,
) Option

WithDevtoolsCatalog enables the GET /devtools/catalog endpoint on the primary listener with admin-only gating (auth.AnyRole("admin")).

Pass nil pm to leave the endpoint disabled; this allows composition roots to attempt metadata parse and degrade gracefully (no error / no warning from bootstrap layer when parse fails).

pkgGraph is the build-time generated package dependency graph (from cmd/corebundle/catalog_gen.go generatedPackageGraph). Pass nil to omit the packageDeps block entirely. The graph is produced at build time by running `go generate ./cmd/corebundle/` and committed as catalog_gen.go.

wireSummaries supplies per-cell wire surface summaries derived from cell.go marker comments via BuildCellWireSummaries. Pass nil to omit the wireSummary field on all Cell entities (safe default when markers are absent).

func WithEventRouterReadyTimeout

func WithEventRouterReadyTimeout(d time.Duration) Option

WithEventRouterReadyTimeout overrides the EventRouter Phase-3 ready-wait budget. A non-positive value disables the bound (router waits indefinitely until ctx cancel). Default: eventrouter.DefaultReadyTimeout (30s).

On timeout, Bootstrap.Run returns an error listing not-ready "consumerGroup/topic" pairs so operators can pinpoint the stuck subscription.

func WithGRPCListener

func WithGRPCListener(ref cell.ListenerRef, server GRPCServer, addr string, opts ...GRPCListenerOption) Option

WithGRPCListener declares a gRPC listener served on addr by the composition-root-constructed server (GAP-1 PR-7 [#1150]).

ref identifies this listener so that cells can route their GRPCServiceSpecs to the correct server via GRPCServiceSpec.Listener — fully symmetric with HTTP's WithListener(ref, addr, authChain). ref must be non-zero and unique across all WithGRPCListener calls; a zero or duplicate ref is rejected at phase0 (validateGRPCListenerConfigs), mirroring the HTTP listener ref gate, before any socket binds.

The server must be non-nil — both bare-nil and typed-nil are rejected at phase0 with ErrGRPCServerMissing (mirroring WithRateLimiter / WithManagedResource). bootstrap drives Serve in phase7b (in parallel with HTTP) and Close in phase10 stage2 (before LIFO teardown, so in-flight RPCs drain while backends are alive).

Composition-root wiring (cmd/ or examples/, which may import adapters/grpc and runtime/grpc/interceptor — cells/ may not). The registrar is created FIRST and shared by the chain (reg.CellIDForMethod feeds cell attribution) and the adapter server (Config.Registrar) — Option 3, #1152:

reg := runtimegrpc.NewServiceRegistrar()
chain := interceptor.NewUnaryChain(interceptor.Deps{
    Verifier: verifier, Clock: clk, Collector: collector, Tracer: tracer,
    Registrar: reg, CellIDClosedSet: asm.CellIDs(),
}) // always wires UnaryAuth; panics on a nil verifier / registrar (fail-closed)
srv, err := adaptersgrpc.New(adaptersgrpc.Config{
    Addr: ":9000", TLS: tlsCfg,
    ServerOptions: []grpc.ServerOption{chain}, Registrar: reg,
})
bootstrap.New(clk, bootstrap.WithGRPCListener(cell.PrimaryListener, srv, ":9000"))

Cell services are drained from RegistrySnapshot.GRPCServices in phase7b (after binding, before grpcServeAll) and registered via server.Registrar().Register(spec) — the stopgap of pre-passing registered services is replaced by this drain.

func WithHealthAggregator

func WithHealthAggregator(agg healthz.Aggregator) Option

WithHealthAggregator injects a custom healthz.Aggregator into the bootstrap. The aggregator is used by the health.Handler (/readyz) and by drainProbes to register framework-level probes.

Both bare-nil and typed-nil interface values are rejected at phase0 with errcode ERR_VALIDATION_FAILED. When WithHealthAggregator is not called, bootstrap constructs a default obshealthz.NewAggregator in phase0 (with b.clock and, if set, the WithReadyzDeadline per-probe deadline) — the option exists for hosts that want to substitute (e.g. tests with a fake aggregator). A custom aggregator owns its own deadline, so it cannot combine with WithReadyzDeadline (that combination fails fast at phase0).

ref: runtime-api.md strong-dependency wiring option pattern.

func WithHealthChecker

func WithHealthChecker(name healthz.ProbeName, fn func(context.Context) error) Option

WithHealthChecker registers a named readiness checker that contributes to aggregate /readyz and appears in `/readyz?verbose` responses. Use this to wire adapter health probes (e.g., conn.Health for RabbitMQ) without bootstrap depending on adapter types.

The first parameter MUST be a declared healthz.ProbeName const (e.g. adapters/postgres.ProbeReady, adapters/rabbitmq.ProbeReady). Bare untyped string literals are accepted at compile time but are rejected by archtest PROBENAME-SEALED-FUNNEL-01/A2 (typed funnel discipline). NewProbeName(s) and MustProbeName(s) are NOT valid here: NewProbeName is for adapter packages declaring their own typed const (not for composition-root callsites), and MustProbeName is restricted to kernel/healthz internal and test-only use (A4b rejects production callers outside kernel/healthz). This is the sole sanctioned entry point for wiring adapter/framework readiness probes into bootstrap.

Accepts func(context.Context) error so callers can honor the /readyz probe deadline. Validation (empty name, nil fn) is deferred to Run() where it fires at Step 0 before any component starts, returning an error directly.

func WithHealthRoutes

func WithHealthRoutes(opts ...HealthRouteGroupOption) Option

WithHealthRoutes accumulates HealthRouteGroupOption values that customize the framework-owned /healthz, /readyz, and /metrics route groups. The canonical use cases are:

bootstrap.WithHealthRoutes(bootstrap.WithMetricsHandler(promHandler))
bootstrap.WithHealthRoutes(bootstrap.WithReadyzVerboseToken(token))
bootstrap.WithHealthRoutes(bootstrap.WithReadyzVerboseDisabled())

Multiple WithHealthRoutes calls accumulate; later options for the same concern (metrics handler, verbose-token, verbose-disabled) overwrite earlier ones in the order they were appended. Pass nil-valued options at your peril — they overwrite any previously-set value with the zero value.

PR-A35 / PR269 round-3 strict semantics: a request with ?verbose= but no matching readyz verbose-token / disabled flag yields 401 ErrReadyzVerboseDenied at the health handler layer, never a silent downgrade to plain 200.

func WithIdempotencyStore

func WithIdempotencyStore(store idemhttp.Store) Option

WithIdempotencyStore enables HTTP idempotency middleware for mutating methods (POST/PUT/PATCH/DELETE). The store is forwarded to the router's middleware chain via router.WithIdempotency so replayed responses are served directly without re-invoking business handlers.

Both bare-nil and typed-nil (non-nil interface holding a nil pointer) are rejected at phase0 with a fatal error so operators are not silently left without idempotency protection.

Concrete implementations:

  • production: adapters/redis.NewHTTPIdempotencyStore(client, ns)
  • tests: idempotency.NewMemStore(clk) from runtime/http/idempotency

ref: runtime-api.md strong-dependency wiring option pattern.

func WithLifecycle

func WithLifecycle(fn func(lc Lifecycle)) Option

WithLifecycle registers a hook-registration callback invoked during New() (after all options are applied, as part of lifecycle initialisation). Use for composition-root Hook registration without needing a Bootstrap reference. Multiple WithLifecycle options and direct b.Lifecycle().Append() calls accumulate in the order they are applied.

func WithLifecycleDefaultStartTimeout

func WithLifecycleDefaultStartTimeout(d time.Duration) Option

WithLifecycleDefaultStartTimeout overrides the per-hook default StartTimeout. Zero value retains DefaultStartTimeout (30s).

Since ADR 202605170000 §D-B, StartTimeout is NOT enforced as an OnStart ctx deadline — it is only the slow-start warning threshold (warn when an OnStart elapses ≥ 80% of it). A hook whose OnStart never returns is bounded by the orchestration-layer backstop (caller ctx + WithStartupTimeout), not by this value. Negative therefore only disables the slow-start warning; it does NOT remove a startup deadlock backstop. The actual startup deadlock backstop is WithStartupTimeout (orchestration-layer); this option only affects the slow-start warning threshold.

func WithLifecycleDefaultStopTimeout

func WithLifecycleDefaultStopTimeout(d time.Duration) Option

WithLifecycleDefaultStopTimeout mirrors WithLifecycleDefaultStartTimeout for StopTimeout. Zero value retains DefaultStopTimeout (10s). Negative disables default timeout.

func WithListener

func WithListener(ref cell.ListenerRef, addr string, authChain []auth.ListenerAuth, opts ...ListenerOption) Option

WithListener declares a physical HTTP listener and appends it to the Bootstrap's listenerConfigs map. Registering the same ref twice is a phase0 error (duplicate listener declaration).

authChain is the ordered slice of ListenerAuth plans applied uniformly to every route mounted on this listener. RouteGroups inherit this chain — there is no group-level override (PR269 round-3: cells needing a different scheme must declare their routes on a different listener). Listener auth is explicit: a nil or empty chain is a phase0 error. Pass []auth.ListenerAuth{auth.AuthNone{}} for a deliberate no-auth listener (e.g. HealthListener behind a Kubernetes probe path). Route-specific policy and exemptions still live on auth.Mount.

authChain semantics: when chain contains both AuthJWT and non-JWT plans, AuthJWT must be at position 0 (validated at phase0). Runtime execution order is non-JWT guards (mTLS / ServiceToken) first as outer layer, then JWT as the innermost auth check. Declared order is opposite to runtime execution order; this is intentional — outer transport guards run before the JWT cryptographic check.

ref: go-kratos/kratos transport/http/server.go — options applied before server start.

See docs/ops/listener-topology.md for the deployment topology, threat boundaries, and single-listener migration guide that consume these declarations.

func WithManagedCloser

func WithManagedCloser(c kernellifecycle.ContextCloser) Option

WithManagedCloser registers an adapter or resource that implements lifecycle.ContextCloser for LIFO teardown during graceful shutdown. The tearCtx budget (stage 3 — independent from drainCtx, see WithShutdownTimeout godoc) propagates directly to c.Close(ctx), so the resource participates in the same teardown deadline as all other LIFO components.

Use this instead of a bare defer c.Close() so that:

  • The resource is closed in LIFO order after HTTP and worker shutdown.
  • The tearCtx (stage 3 budget) is honored (not an arbitrary timeout).
  • Startup rollback also triggers the teardown on phase failures.

Both bare-nil and typed-nil (non-nil interface holding a nil pointer) are rejected at phase0 with a fatal error, mirroring the WithManagedResource fail-fast pattern. This prevents a silent wiring bug from panicking at Close() call time during shutdown or rollback.

ref: uber-go/fx Lifecycle.Append OnStop(ctx) — managed teardown registration. ref: kubernetes-sigs/controller-runtime pkg/manager/manager.go — strong dependency fail-fast.

func WithManagedResource

func WithManagedResource(r kernellifecycle.ManagedResource) Option

WithManagedResource registers an external resource with the bootstrap lifecycle. At Run() time, bootstrap:

  1. Registers each Probes() entry as a typed /readyz health probe.
  2. Registers the Worker() (when non-nil) with the bootstrap WorkerGroup.
  3. Appends a LIFO teardown that calls Close() during shutdown.

Multiple calls to WithManagedResource are supported; Close() order is LIFO (last registered is first closed), mirroring fx hook order.

Both bare-nil and typed-nil (non-nil interface holding a nil pointer) are rejected at phase0 with a fatal error, mirroring the WithCircuitBreaker fail-fast pattern. This prevents a silent wiring bug from panicking at Checkers()/Worker()/Close() call time.

ref: uber-go/fx app.go — Option pattern; each Option targets a single concern. ref: uber-go/fx internal/lifecycle/lifecycle.go Append — hook registration does no nil-substitution; bad inputs surface before any component starts.

func WithMetricsProvider

func WithMetricsProvider(p kernelmetrics.Provider) Option

WithMetricsProvider registers a provider-neutral metrics backend used by components that need to emit counters/histograms through a common abstraction (hook dispatcher drop counters, OTel pool-stats collector, custom caller-registered metrics). Pass nil or omit the option to use kernel/observability/metrics.NopProvider (no emission).

Callers can read b.MetricsProvider() to register additional metrics against the same backend — useful when cmd/* builds both an HTTP Collector and a relay Collector on the same Provider instance.

ref: opentelemetry-go otel.GetMeterProvider@main — single global provider entry point; GoCell exposes it per-Bootstrap instance to avoid mutable global state.

func WithPreShutdownDelay

func WithPreShutdownDelay(d time.Duration) Option

WithPreShutdownDelay sets a delay between marking /readyz as 503 and starting the HTTP server shutdown. This gives load balancers (e.g., Kubernetes kube-proxy) time to observe the unhealthy readiness probe and stop routing new traffic before the server closes connections.

Default is 0 (no delay). Typical Kubernetes deployments use 3-5 seconds. The delay is consumed INSIDE drainCtx (stage 1+2 budget), not on top of shutdownTimeout — a long preShutdownDelay narrows the remaining budget available to HTTP drain. LIFO teardown (stage 3) is unaffected because it owns an independent tearCtx.

ref: Kubernetes pod shutdown — preStop counts toward terminationGracePeriodSeconds

func WithProjectionCheckpointStore

func WithProjectionCheckpointStore(store projection.CheckpointStore) Option

WithProjectionCheckpointStore injects the projection.CheckpointStore used by every projection Coordinator to persist consumed offsets in the framework offset table. Typed-nil or bare-nil inputs are not stored (cumulative builder semantics); when a cell has declared a projection, the phase6 drain fails fast with errcode.ErrCellInvalidConfig naming this option.

For tests, projection.NewMemCheckpointStore() is sufficient. Production deployments inject the postgres-backed adapter via the composition root.

func WithProjectionCursor

func WithProjectionCursor(cursor projection.Cursor) Option

WithProjectionCursor injects the projection.Cursor used by every projection Coordinator to extract a monotonic stream position from each consumed event (for the exactly-once checkpoint compare). Typed-nil or bare-nil inputs are not stored; the phase6 drain fails fast naming this option when a projection is declared.

For tests, a one-method type returning a fixed int64 ≥ 1 is sufficient (the cursor must return ≥ 1; 0 is the cold-start sentinel in the checkpoint store).

func WithProjectionRebuildEndpoint

func WithProjectionRebuildEndpoint() Option

WithProjectionRebuildEndpoint opts the assembly into the framework projection rebuild control-plane endpoint:

POST /admin/v1/projection/{cell}/{name}/rebuild

mounted by bootstrap on the AdminListener (the framework-owned-RouteGroup pattern, like /healthz·/readyz·/metrics — no contract.yaml, no host cell). The handler dispatches by {cell}/{name} to the Coordinator wired in the phase6 drain and returns 202 (rebuild admitted, body carries the {phase, pendingEvents, replayLagSeconds} snapshot) / 409 (already running) / 404 (unknown cell/projection).

This is an operator→system control-plane action (an administrator or deployment pipeline rebuilds a projection), NOT a cell→cell call: there is no caller-cell allowlist. Authentication is the AdminListener's operator credential gate (AuthOperator). The AdminListener MUST be declared via WithListener(cell.AdminListener, addr, []kauth.ListenerAuth{operatorAuth}) — phase0 (validateProjectionRebuildEndpoint) fails fast otherwise.

Not calling this option leaves the endpoint unmounted with NO error: projection rebuilds remain triggerable only programmatically via Coordinator.Rebuild. This is a wiring option (idempotent opt-in).

func WithProjectionReplaySource

func WithProjectionReplaySource(replay projection.ReplaySource) Option

WithProjectionReplaySource injects the projection.ReplaySource used by every projection Coordinator to replay the event stream during a rebuild. Typed-nil or bare-nil inputs are not stored; the phase6 drain fails fast naming this option when a projection is declared.

For tests, projection.NewMemReplaySource() is sufficient.

func WithProjectionTxRunner

func WithProjectionTxRunner(txRunner persistence.TxRunner) Option

WithProjectionTxRunner injects the persistence.TxRunner used by every projection Coordinator to commit the Apply mutation and the checkpoint SaveOffset in one transaction (exactly-once). Typed-nil or bare-nil inputs are not stored; the phase6 drain fails fast naming this option when a projection is declared.

For tests, a pass-through TxRunner whose RunInTx calls fn(ctx) directly is sufficient (the mem checkpoint store ignores the ambient transaction).

func WithPublisher

func WithPublisher(p outbox.Publisher) Option

WithPublisher sets the outbox.Publisher used for event publishing.

ref: uber-go/fx app.go — Option pattern; each Option targets a single concern.

func WithRateLimiter

func WithRateLimiter(rl middleware.RateLimiter) Option

WithRateLimiter enables per-IP rate limiting for HTTP requests. The limiter is forwarded to the router's middleware chain via router.WithRateLimiter. Also registers the resource for LIFO teardown via b.closers. If the limiter implements lifecycle.ContextCloser or io.Closer (e.g. adapters/ratelimit.Limiter), Bootstrap registers it for teardown on shutdown and startup rollback. ContextCloser is preferred so the shared shutCtx budget flows through to the resource.

Both bare-nil and typed-nil (non-nil interface holding a nil pointer) are rejected at phase0 with a fatal error so operators are not silently left without rate-limiter protection. This mirrors the WithCircuitBreaker / WithManagedResource fail-fast pattern.

Note: the rate limiter uses the client IP from RealIP middleware as the bucket key. Ensure WithTrustedProxies is correctly configured; an overly permissive trust list allows X-Forwarded-For spoofing, which bypasses rate limiting.

ref: go-zero — rate limiting configuration at app level ref: uber-go/fx lifecycle OnStop(ctx) — ContextCloser preferred over io.Closer ref: kubernetes-sigs/controller-runtime pkg/manager/manager.go — strong dependency fail-fast.

func WithReadyzDeadline

func WithReadyzDeadline(d time.Duration) Option

WithReadyzDeadline overrides the per-probe deadline for /readyz. All registered probes must complete within this duration; probes that exceed it are reported as status="timeout". A zero or negative value uses the default aggregator's 5 s default (Kubernetes readiness probe convention).

The deadline is owned by the healthz.Aggregator: bootstrap applies it to the default aggregator it builds in phase0. It therefore CANNOT combine with WithHealthAggregator (a custom aggregator owns its own deadline) — that combination fails fast at phase0. Configure a custom aggregator's deadline via runtime/observability/healthz.WithDeadline at construction instead.

ref: k8s.io/apiserver/pkg/server/healthz — server-side readyz deadline independent of the kubelet HTTP connection deadline.

func WithRelay

func WithRelay(r *runtimeoutbox.Relay) Option

WithRelay registers the relay BOTH for outbox wiring AND for lifecycle (Start/Stop driven through the package-private relayAdapter). Calling WithRelay is the ONLY supported path to integrate a relay; passing it to WithManagedResource is a compile-time type-mismatch — *runtimeoutbox.Relay does not implement kernel/lifecycle.ManagedResource. See ADR docs/architecture/202605201400-adr-relay-managedresource-isolation.md and archtest RELAY-NOT-MANAGEDRESOURCE-01.

Calling WithRelay more than once is a programmer error and panics through the panic-taxonomy funnel (panicregister.Approved + errcode.Assertion, B class): the second call would silently overwrite b.relay while leaving the earlier relay registered in managedResources, hiding a double-managed resource that the previous runtime guard could not catch once the active b.relay pointer moved.

Nil inputs are silently ignored (cumulative builder noop pattern, runtime-api.md §Option 范式分层): the relay remains unset, and autoWireOutboxRejectCollector skips relay-specific wiring.

Must be called before Run(). Typical usage:

relay := runtimeoutbox.NewRelay(store, pub, cfg)
relay.WithPendingDepthObserver(pendingDepthCollector)
bootstrap.New(
    bootstrap.WithRelay(relay), // sole sanctioned entry; no WithManagedResource needed
    ...
)

func WithRouterOptions

func WithRouterOptions(opts ...router.Option) Option

WithRouterOptions passes options to the router builder.

func WithSecurityHeadersOptions

func WithSecurityHeadersOptions(opts ...middleware.SecurityHeadersOption) Option

WithSecurityHeadersOptions configures HSTS and other security header directives. This is a convenience wrapper around WithRouterOptions(router.WithSecurityHeadersOptions(...)).

ref: unrolled/secure — configurable HSTS directives via struct fields

func WithShutdownTimeout

func WithShutdownTimeout(d time.Duration) Option

WithShutdownTimeout overrides the default graceful shutdown timeout.

Semantics: each phase10 stage bucket — drainCtx (readiness flip + HTTP drain) and tearCtx (LIFO teardown) — is allotted this duration independently. Worst-case wall clock for the entire shutdown is therefore approximately 2 × shutdownTimeout + finalize overhead.

K8s deployments must set terminationGracePeriodSeconds >= 2 × shutdownTimeout + 10s (safety margin). See warnTerminationGracePeriodInsufficient, docs/ops/graceful-shutdown-k8s.md, and ADR docs/architecture/202605101730-adr-shutdown-budget-decouple.md.

func WithStartupTimeout

func WithStartupTimeout(d time.Duration) Option

WithStartupTimeout sets the whole-Start orchestration budget. Bootstrap.Run supervises lifecycle.Start in a goroutine and aborts if it has not completed when EITHER the caller ctx is canceled OR this budget elapses; on abort it cancels ownerCtx (unblocking a wedged OnStart) then rolls back, returning ErrBootstrapStartupTimeout (timer path) or the caller ctx error.

This is the deadlock backstop for the ADR 202605170000 §D-B contract (OnStart = owner ctx, no per-hook deadline): without it a hook whose OnStart never returns would wedge Run() forever (review P1-1).

Zero retains DefaultStartupTimeout (30s). Negative disables the timer — the caller ctx remains the abort path (matches controller-runtime mgr.Start, which is unblocked only by its caller ctx). It is NOT a per-hook SLA; the per-hook slow-start warning is WithLifecycleDefaultStartTimeout.

ref: kubernetes-sigs/controller-runtime pkg/manager/internal.go — Start returns when the caller ctx is canceled.

func WithSubscriber

func WithSubscriber(s outbox.Subscriber) Option

WithSubscriber sets the outbox.Subscriber used for event consumption.

ref: uber-go/fx app.go — Option pattern; each Option targets a single concern.

func WithSubscriptionValidator

func WithSubscriptionValidator(v ...cell.SubscriptionValidator) Option

WithSubscriptionValidator registers a registration-time subscription validator that is invoked by the EventRouter for every Cell.RegisterSubscriptions call. A non-nil error from any validator fails the subscription registration and surfaces during phase6 startup.

Composition roots use this to enforce domain-specific invariants without polluting Cell code with infrastructure concerns. Nil validators are silently ignored.

ref: Finding 2 (PR #334 L4) — fail at registration boundary, not delivery time.

func WithTerminationGracePeriod

func WithTerminationGracePeriod(d time.Duration) Option

WithTerminationGracePeriod declares the operator's expected K8s pod terminationGracePeriodSeconds for phase0 sanity-check ONLY. The value does NOT change runtime behavior — actual graceful-shutdown windows must be set in the Kubernetes pod spec.

When the declared value is positive but smaller than 2 × shutdownTimeout + 10s, phase0 emits a slog.Warn so operators can spot a misalignment between the bootstrap budget and the pod-spec grace window before SIGKILL truncates a real shutdown. The 2× multiplier reflects the budget-isolation contract: phase10 drainCtx + tearCtx each own a shutdownTimeout-sized bucket. preShutdownDelay does not appear in the formula because it is consumed inside drainCtx (see WithPreShutdownDelay godoc and ADR 202605101730-adr-shutdown-budget-decouple).

Zero value (default) skips the sanity check entirely. Callers that do not run on Kubernetes can omit this option without consequence.

ref: Kubernetes pod lifecycle — terminationGracePeriodSeconds bounds the total time between SIGTERM and SIGKILL.

func WithTracer

func WithTracer(t wrapper.Tracer) Option

WithTracer enables distributed tracing. The tracer is forwarded to router.WithTracer (the single HTTP request span owner) and stored on Bootstrap.wrapperTracer so eventrouter.NewContractTracingSubscriber can create consumer-side wrapper.WrapSubscriber spans. Without this option, HTTP tracing is disabled and WrapSubscriber falls back to wrapper.NoopTracer{}; a slog.Warn is emitted at bootstrap time so ops notice the silent degrade.

ref: go-zero — observability configuration at app level

func WithWebhookClaimer

func WithWebhookClaimer(claimer idempotency.Claimer) Option

WithWebhookClaimer injects the idempotency.Claimer used by all inbound webhook receivers to deduplicate deliveries. A nil or typed-nil value is silently ignored; the final nil check happens during phase5 when any cell has declared a webhook receiver.

For tests, idempotency.NewInMemClaimer with the bootstrap clock is sufficient. Production deployments should use a distributed claimer backed by Redis or PostgreSQL.

func WithWebhookSSRFPolicy

func WithWebhookSSRFPolicy(policy *kwh.SafePolicy) Option

WithWebhookSSRFPolicy injects the kwh.SafePolicy wired into every outbound webhook dispatcher's *http.Client. A nil value is silently ignored; when any cell registers a webhook dispatcher and no policy was set, phase6 builds the default production policy kwh.NewSafePolicy (block all private/reserved ranges, no loopback). Override only for dev / CI — e.g. kwh.NewSafePolicy(kwh.WithAllowLoopback()) to deliver to a local test server.

Cumulative builder option (see runtime-api.md §Option 范式分层): the final decision happens at phase6 drain, not at option-apply time, because a deployment with zero dispatchers needs no policy.

func WithWebhookSourceStore

func WithWebhookSourceStore(store kwh.SourceStore) Option

WithWebhookSourceStore injects the kwh.SourceStore used to look up HMAC secrets by source ID. It serves BOTH directions: inbound webhook receivers (phase5) verify incoming signatures against it, and outbound webhook dispatchers (phase6) resolve each dispatcher's signing secret from it (inbound: BuildRouteGroups → SourceStore.Lookup; outbound: BuildConsumers → SourceStore.Lookup). A nil or typed-nil value is silently ignored; the final nil check happens at the consuming phase — phase5 if any cell declared a receiver, phase6 if any cell declared a dispatcher.

Typical usage: pass a pre-populated kwh.NewSourceRegistry that has been seeded with kwh.Source values for each expected sender.

func WithWorkers

func WithWorkers(ws ...worker.Worker) Option

WithWorkers adds background workers.

type Topology

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

Topology captures the resolved runtime topology derived from environment variables. It is the single source of truth for adapter-mode / storage-backend coupling checks used by bootstrap, health probes, and test harnesses.

ref: uber-go/fx fx.Provide(NewConfig) — single-constructor singleton that validates once and is passed everywhere. ref: go-kratos/kratos config.Config — full-lifecycle configuration object passed through the entire runtime stack.

Sealed construction: all fields are unexported, so a package-external struct literal cannot populate (let alone mis-populate) a Topology. The only ways to obtain a non-zero Topology are NewTopology / TopologyFromEnv, both of which run validate(); the illegal postgres+non-real combination is therefore unconstructable outside this package. The zero value Topology{} reads as dev/memory (adapterMode "", storageBackend "" → treated as memory) and is safe. Field set frozen by TestTopologyZeroExportedFields (TOPOLOGY-SEALED-FIELD-FROZEN-01).

func NewTopology

func NewTopology(adapterMode, storageBackend string, singlePod bool) (Topology, error)

NewTopology validates an adapter-mode / storage-backend / single-pod combination and returns the sealed Topology. An empty storageBackend is normalized to "memory" (the dev default). The postgres+non-real coupling and the AdapterMode allowlist are enforced via validate(); invalid combinations return an error and a zero Topology — they cannot be constructed.

This is the single validating constructor; TopologyFromEnv delegates here so the normalization and coupling rules live in exactly one place.

func TopologyFromEnv

func TopologyFromEnv() (Topology, error)

TopologyFromEnv reads GOCELL_CELL_ADAPTER_MODE and GOCELL_ADAPTER_MODE, validates their combination, and returns a Topology.

Coupling rule: postgres storage requires GOCELL_ADAPTER_MODE=real so production key loading, token-guarded /metrics, and token-guarded /readyz?verbose are all enforced. The check is implemented in Topology.validate below and is the sole authoritative enforcement.

ref: go-zero serviceconf — single config drives all gates; misalignment is fatal.

func (Topology) AdapterInfo

func (t Topology) AdapterInfo() map[string]string

AdapterInfo returns a map of topology metadata for the /readyz?verbose response. Operators can confirm which backends are active without reading logs.

ref: go-micro service metadata — mode changes must be visible to observers.

func (Topology) AdapterMode

func (t Topology) AdapterMode() string

AdapterMode returns the resolved GOCELL_ADAPTER_MODE: "" (dev) or "real".

func (Topology) RequireProductionControlPlane

func (t Topology) RequireProductionControlPlane() bool

RequireProductionControlPlane returns true when the runtime has opted into real (production) keys and therefore requires production-grade control-plane guards: token-authenticated /metrics, token-authenticated /readyz?verbose, HMAC-guarded /internal/v1/*, and strict (fail-fast) secret loading.

The gate is AdapterMode=="real". Postgres storage implies AdapterMode=="real" via the coupling rule enforced in validate(), so postgres topologies also return true; memory+real is likewise covered — whenever an operator has asked for real keys, the control plane must be protected.

PGResource wiring is a separate concern (see StorageBackend) — the storage backend determines whether a PG pool is owned, while this predicate determines whether anonymous control-plane access is rejected.

func (Topology) RequiresDistributedReplay

func (t Topology) RequiresDistributedReplay() bool

RequiresDistributedReplay reports whether the topology demands a distributed (cross-pod) replay-defense posture for the /internal/v1/* service-token guard: real adapter mode without the single-pod acknowledgement. In this posture an in-memory (single-process) NonceStore is insufficient — only a distributed store coordinates replay defense across pods.

This is the single source for the predicate previously copied into composition.SharedDeps.requiresDistributedReplay and cmd/corebundle/redis.go; both now delegate here so the rule lives in exactly one place.

func (Topology) SinglePodReplayProtection

func (t Topology) SinglePodReplayProtection() bool

SinglePodReplayProtection reports whether the deployment opted into single-pod in-memory replay protection (GOCELL_SINGLE_POD=1).

func (Topology) StorageBackend

func (t Topology) StorageBackend() string

StorageBackend returns the resolved GOCELL_CELL_ADAPTER_MODE: "memory" or "postgres". A zero-value Topology returns "" (treated as memory by consumers).

Jump to

Keyboard shortcuts

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