Documentation
¶
Index ¶
- Constants
- Variables
- func AllocatePort() int
- func BuildRCommand(bundleDir string, port int, bindHost string) []string
- func BundleDir(appsDir, slug, version string) string
- func CheckAppQuota(appsDir, appDataDir, slug string, quotaMB int) (int64, error)
- func CheckColocatedShared(consumerTiers []string, sources map[string][]string, ...) error
- func DetectAppType(bundleDir string) string
- func DirSize(root string) (int64, error)
- func EphemeralDataBlockedTier(usesData, ack bool, tiers []string, tierDurable func(tier string) bool) (string, bool)
- func ExtractBundle(src, destDir string) error
- func ExtractBundleWithLimits(src, destDir string, maxEntrySize, maxTotalSize int64) error
- func PruneOldVersions(appsDir, slug string, keep int, activeDir string) error
- func ResolveCPUQuotaPercent(perAppPct *int, defaultPct int) int
- func ResolveIdentityHeaders(col *bool, globalEnabled bool) bool
- func ResolveMaxSessionsPerReplica(perApp, defaultVal int) int
- func ResolveMemoryLimitMB(perAppMB *int, defaultMB int) int
- func ResolveWorkerIsolation(perApp, def string) string
- func RunPostDeployHooks(ctx context.Context, bundleDir string, hooks []Hook, extraEnv []string, ...) error
- func UsesPersistentData(command []string, appDataDir, slug string) (bool, error)
- func ValidateCPUQuotaPercent(v int) error
- func ValidateMemoryLimitMB(v int) error
- type AppAccess
- type AppSettings
- type AutoscaleSettings
- type DepPrepStep
- type Hook
- type HookTrigger
- type LaunchOptions
- type LaunchPlan
- type Manifest
- type Params
- type PoolResult
- type Result
- type ScheduleSpec
- type TracingSettings
- type WorkerManifest
Constants ¶
const ( // DefaultMaxEntrySize caps the extracted size of a single file inside the // bundle. Matches the upload size cap — a single file can never be larger // than the full archive. DefaultMaxEntrySize int64 = 128 << 20 // DefaultMaxBundleSize caps the combined extracted size of all entries. DefaultMaxBundleSize int64 = 512 << 20 )
const ( // MinMemoryLimitMB is the smallest enforceable per-app memory ceiling; below // this no R/Python runtime can start, so a value in 1..15 is rejected as a // likely typo. The API PATCH endpoint keeps its historical >=0 floor for // backward compatibility; this stricter floor applies to the manifest + UI. MinMemoryLimitMB = 16 // MaxMemoryLimitMB caps the memory ceiling at 1 TiB as a sanity bound. MaxMemoryLimitMB = 1024 * 1024 // MaxCPUQuotaPercent caps the CPU ceiling at 64 cores. The bound is // host-independent so a bundle validates identically everywhere; the host // enforces only the cores it physically has. MaxCPUQuotaPercent = 6400 )
Resource-limit bounds shared across the manifest, the CLI/API, and the UI so every surface agrees on the same contract. 0 means "explicit unlimited"; positive values map to cgroup v2 memory.max / cpu.max per replica.
const ManifestFilename = "shinyhub.toml"
ManifestFilename is the canonical bundle manifest name. It lives at the bundle root and is optional — bundles without one deploy exactly as before.
const MiB = int64(1) << 20
MiB is 1 mebibyte expressed in bytes.
Variables ¶
var ErrBundleRejected = errors.New("bundle rejected")
ErrBundleRejected is returned by ExtractBundle when a bundle entry violates the content policy (data dirs, forbidden extensions, etc.). Callers can use errors.Is to map this to a 422 Unprocessable Entity response.
var ErrBundleTooLarge = errors.New("bundle exceeds extracted size limit")
ErrBundleTooLarge is returned by ExtractBundle when a single entry, or the combined size of all entries, exceeds the configured limits. Zip-bomb protection: uncompressed sizes in the zip header are attacker-controlled, so we also enforce the caps while streaming bytes to disk.
var ErrQuotaExceeded = errors.New("app disk quota exceeded")
ErrQuotaExceeded is returned when an app's on-disk footprint exceeds its configured per-app quota after a deploy would be committed.
Functions ¶
func AllocatePort ¶
func AllocatePort() int
AllocatePort returns an unused TCP port in the 20000–60000 range.
Each candidate is verified with a short-lived bind on 127.0.0.1 so we never hand back a port already held by a survivor process from a prior shinyhub run (the counter resets to 20000 on every startup; without the probe a restart could happily re-issue an in-use port and the spawned app would bind-fail). On range exhaustion the counter wraps; if no probed port in the range is bindable within maxAllocateProbes attempts, the OS is asked for any free port via :0.
func BuildRCommand ¶
BuildRCommand returns the command to start an R Shiny app on the given port. bindHost is the address the app listens on inside its execution environment (the host for native, the container for Docker bridge mode).
func BundleDir ¶ added in v0.7.4
BundleDir is the canonical extracted-bundle directory for an app version: <appsDir>/<slug>/versions/<version>. The single source of truth for this path, used at deploy time and by the legacy bundle_dir backfill.
func CheckAppQuota ¶
CheckAppQuota returns the measured on-disk usage (in bytes) of appsDir/slug + appDataDir/slug (excluding the data dir's UploadTempDir subtree). If quotaMB > 0 and usage exceeds that limit, the returned error wraps ErrQuotaExceeded with the measured and allowed byte counts. quotaMB <= 0 disables the check and CheckAppQuota always returns a nil error. An empty appDataDir skips the data-dir contribution.
func CheckColocatedShared ¶ added in v0.6.1
func CheckColocatedShared(consumerTiers []string, sources map[string][]string, nodeForTier func(string) string) error
CheckColocatedShared verifies that every node a consumer runs on also hosts each shared-mount source. consumerTiers are the tiers the consumer is placed on. sources maps each mounted source slug to the tiers that source runs on. nodeForTier resolves a tier to the node id that backs it. Returns an error describing the first cross-node mount it finds.
func DetectAppType ¶
DetectAppType returns "python" if app.py exists, "r" if app.R exists, or "" if neither is found.
func DirSize ¶
DirSize returns the sum of sizes (in bytes) of every regular file reachable from root. Symlinks are not followed — only their own metadata is counted, which is what we want for quota accounting. A missing root returns (0, nil) so callers can use it for first-deploy paths without a pre-stat.
func EphemeralDataBlockedTier ¶ added in v0.9.5
func EphemeralDataBlockedTier(usesData, ack bool, tiers []string, tierDurable func(tier string) bool) (string, bool)
EphemeralDataBlockedTier decides whether a deploy must be blocked by the durable-data guard. usesData is the app-side signal (UsesPersistentData); ack is the operator's explicit acceptance of ephemeral storage; tierDurable reports whether a tier's app-data survives restart and is shared across replicas. It returns the first tier whose storage is not durable and true, or "" and false when the deploy is allowed. Fail-closed: any non-durable tier in a mixed placement blocks the whole deploy rather than silently dropping the replicas on the ephemeral tier.
func ExtractBundle ¶
ExtractBundle unzips src into destDir with the default size limits.
func ExtractBundleWithLimits ¶
ExtractBundleWithLimits unzips src into destDir, rejecting any entry whose resolved path would escape destDir (zip-slip protection) and enforcing both a per-entry and aggregate size cap (zip-bomb protection). A zero or negative limit means unlimited.
func PruneOldVersions ¶
PruneOldVersions removes extracted version directories and bundle ZIPs beyond the newest `keep` entries for the given app. The activeDir is never deleted, even if it falls outside the retention window.
func ResolveCPUQuotaPercent ¶
ResolveCPUQuotaPercent returns perAppPct if non-nil, otherwise defaultPct. Zero means no limit in both cases.
func ResolveIdentityHeaders ¶ added in v0.8.6
ResolveIdentityHeaders resolves an app's effective identity-forwarding flag: the global config false is a hard kill switch a manifest cannot override; otherwise the per-app column applies (nil = inherit = on).
func ResolveMaxSessionsPerReplica ¶ added in v0.2.5
ResolveMaxSessionsPerReplica returns perApp if non-zero, otherwise defaultVal. Unlike the memory/CPU helpers, perApp is a plain int because the DB column is NOT NULL DEFAULT 0 and 0 explicitly means "fall back to the runtime default".
func ResolveMemoryLimitMB ¶
ResolveMemoryLimitMB returns perAppMB if non-nil, otherwise defaultMB. Zero means no limit in both cases.
func ResolveWorkerIsolation ¶ added in v0.9.2
ResolveWorkerIsolation returns perApp if non-empty, otherwise def. An empty stored value means "inherit the fleet default".
func RunPostDeployHooks ¶ added in v0.3.3
func RunPostDeployHooks(ctx context.Context, bundleDir string, hooks []Hook, extraEnv []string, logOut io.Writer) error
RunPostDeployHooks executes each hook sequentially in bundleDir, streaming stdout/stderr to logOut. It stops on the first failure so a deploy never proceeds past a broken setup step. Hooks inherit `extraEnv` on top of the parent process env so callers can inject the same variables the app will see at start (PORT excluded — that's per-replica).
func UsesPersistentData ¶ added in v0.9.5
UsesPersistentData reports whether an app relies on its persistent data dir. It is the app-side signal for the durable-data guard: an app uses persistent data if EITHER its command template references the {data_dir} placeholder (authored to read/write its data dir) OR data has already been pushed for it (appDataDir/slug is non-empty, excluding the upload temp dir).
An empty appDataDir skips the on-disk check, mirroring CheckAppQuota.
func ValidateCPUQuotaPercent ¶ added in v0.9.0
ValidateCPUQuotaPercent enforces the shared CPU contract used by the manifest, the API PATCH endpoint, and the UI: 0 (inherit/unlimited) or 1..MaxCPUQuotaPercent.
func ValidateMemoryLimitMB ¶ added in v0.9.0
ValidateMemoryLimitMB enforces the manifest/UI memory contract: 0 (explicit unlimited) or MinMemoryLimitMB..MaxMemoryLimitMB.
Types ¶
type AppAccess ¶ added in v0.8.2
type AppAccess struct {
ViewerGroups []string `toml:"viewer_groups"`
ManagerGroups []string `toml:"manager_groups"`
}
AppAccess declares per-app group access rules in the manifest. Groups in viewer_groups get the viewer role; groups in manager_groups get manager. These reconcile into app_group_access as source='manifest' on each deploy.
type AppSettings ¶ added in v0.5.0
type AppSettings struct {
HibernateTimeoutMinutes *int `toml:"hibernate_timeout_minutes"`
Replicas *int `toml:"replicas"`
MaxSessionsPerReplica *int `toml:"max_sessions_per_replica"`
// Autoscale declares the per-app session-saturation autoscale policy. A
// non-nil pointer means the block is present and reconciles atomically into
// the four autoscale_* columns on every deploy (like Replicas); nil means
// "not declared" and leaves the stored policy - including anything set via
// `apps set --autoscale` - untouched. Declaring it in the bundle lets the
// policy travel with the app and survive rebuild-from-config hosts, instead
// of having to be re-applied imperatively after each deploy.
Autoscale *AutoscaleSettings `toml:"autoscale"`
// IdentityHeaders opts this app out of (or explicitly into) identity
// forwarding. nil = inherit the global auth.identity_headers flag.
// Reconciled into apps.identity_headers on every deploy; removing the
// key reverts to NULL (inherit). The global false kill switch always wins.
IdentityHeaders *bool `toml:"identity_headers"`
// MinWarmReplicas sets the pre-warming floor: replicas kept running
// through idle hibernation. nil = leave the stored value unchanged.
MinWarmReplicas *int `toml:"min_warm_replicas"`
// Command overrides launch-command inference. Validated at parse time
// (and again at boot, covering rollbacks); placeholders {port}, {host},
// {data_dir} are substituted per replica at boot. With a command set,
// type detection, uv-sync, and tracing auto-instrumentation are skipped.
Command []string `toml:"command"`
// StartupTimeoutSeconds lengthens (or shortens) the readiness deadline the
// deploy health check allows before declaring the app crashed. nil =
// inherit the platform default. Like Command and [tracing] auto it is read
// from the bundle at every boot (deploy, redeploy, wake, scale, rollback),
// so a slow-warming app's deadline travels with its bundle. It is never
// reconciled into the DB.
StartupTimeoutSeconds *int `toml:"startup_timeout_seconds"`
// BuildTimeoutSeconds bounds the environment build (uv sync / renv::restore)
// the host runs before the app process starts. nil = inherit the platform
// default. Read from the bundle at every boot and never reconciled into the
// DB (like StartupTimeoutSeconds). Distinct from StartupTimeoutSeconds, which
// bounds only the post-build readiness window. Inert under the Docker runtime
// (the build runs in-container).
BuildTimeoutSeconds *int `toml:"build_timeout_seconds"`
// MemoryLimitMB / CPUQuotaPercent cap each replica's resources. They
// reconcile into apps.memory_limit_mb / apps.cpu_quota_percent on every
// deploy (declared-only, like Replicas: nil leaves the stored value
// unchanged). 0 = explicit unlimited; a positive value maps to cgroup v2
// memory.max / cpu.max per replica (cpu_quota_percent 100 = 1 full core).
// There is no manifest form for NULL (inherit-global); clear via
// `apps set --memory-limit-mb -1` / `--cpu-quota-percent -1`.
MemoryLimitMB *int `toml:"memory_limit_mb"`
CPUQuotaPercent *int `toml:"cpu_quota_percent"`
// Worker declares per-app session-isolation policy. A non-nil pointer means
// the block is present and reconciles the four worker_* columns on every
// deploy (like Autoscale); nil means "not declared" and leaves any
// previously-set value unchanged.
Worker *WorkerManifest `toml:"worker"`
HibernateResetToDefault bool `toml:"-"`
}
AppSettings mirrors the [app] section. Pointer fields distinguish "absent" (nil) from "explicit value". HibernateResetToDefault is a parsed-out signal for `hibernate_timeout_minutes = -1` since TOML has no null literal: the convention mirrors the CLI's `--hibernate-timeout -1`.
func (AppSettings) IsZero ¶ added in v0.5.0
func (a AppSettings) IsZero() bool
Command and StartupTimeoutSeconds are not part of IsZero: they are read at boot, not reconciled into the DB. MemoryLimitMB / CPUQuotaPercent ARE reconciled into the DB (like Replicas), so they count.
type AutoscaleSettings ¶ added in v0.9.1
type AutoscaleSettings struct {
Enabled *bool `toml:"enabled"`
MinReplicas int `toml:"min_replicas"`
MaxReplicas int `toml:"max_replicas"`
Target float64 `toml:"target"`
}
AutoscaleSettings mirrors the [app] autoscale inline table. The block is an atomic unit: when present, all four columns are reconciled together (matching the PATCH /api/apps autoscale object and `SetAppAutoscale`). Enabled is a pointer so a declared block must state it explicitly - this rejects an incomplete block like `{ target = 0.9 }` that would otherwise persist an incoherent all-zero policy. Target is a fraction (0,1] of the per-replica session cap; 0 inherits the runtime default.
type DepPrepStep ¶ added in v0.8.25
DepPrepStep is one host-side preparation action (EnsureProject, uv sync, renv restore). The runner executes each in order before launch.
type Hook ¶ added in v0.3.3
type Hook struct {
// On is the lifecycle trigger. Required; only "post-deploy" is accepted.
On HookTrigger `toml:"on"`
// Command is the argv to exec. Required; the first element is the
// program path and is resolved against the bundle dir's PATH.
Command []string `toml:"command"`
// Timeout caps a single hook's wall-clock runtime. Defaults to
// defaultHookTimeout when zero or unset.
Timeout time.Duration `toml:"timeout"`
}
Hook is a single declarative command in shinyhub.toml.
type HookTrigger ¶ added in v0.3.3
type HookTrigger string
HookTrigger identifies when a hook should fire in the deploy lifecycle. Only "post-deploy" is recognised today; unknown values are reported as an error at parse time so a typo doesn't silently no-op the hook.
const (
HookPostDeploy HookTrigger = "post-deploy"
)
type LaunchOptions ¶ added in v0.8.25
type LaunchOptions struct {
CommandOverride []string // API/explicit command; substituted but not validated; skips detection/prep/auto-instrument
Port int
Workers int // threaded to buildCommand; currently unused there, kept for fidelity
BindHost string
PrepHostDeps bool // include dep-prep steps (pool-wide decision)
CommandHostDeps bool // per-tier project-mode flag for buildCommand
AutoInstrumentDefault bool
HonorManifestTracing bool // apply manifest [tracing] auto override? server true, run false
Reload bool
// AppEnv is the per-app env layered into dep-prep builds on top of the
// sanitized server base (the same variables the app process will see at
// start, e.g. private package-index credentials). The server deploy path
// resolves it from the app's env store; `shinyhub run` passes --env/.env.
AppEnv []string
}
LaunchOptions are the Manager-free inputs both consumers supply. See the design spec section 4.2 for the PrepHostDeps vs CommandHostDeps distinction.
type LaunchPlan ¶ added in v0.8.25
type LaunchPlan struct {
AppType string
Manifest *Manifest
Command []string
Env []string // launch-coupled only ("PORT"); platform/per-app env layered by the consumer
BindHost string
ReadyPath string
DepPrep []DepPrepStep
Timeout time.Duration
}
LaunchPlan is the canonical description of how a single app replica launches. It is the one source of truth shared by the server boot path and `shinyhub run`.
func ResolveLaunch ¶ added in v0.8.25
func ResolveLaunch(bundleDir string, opts LaunchOptions) (*LaunchPlan, error)
ResolveLaunch resolves how a bundle launches, mirroring resolveBootParams + bootReplica's command construction exactly, but without process.Manager.
type Manifest ¶ added in v0.3.3
type Manifest struct {
App AppSettings `toml:"app"`
Hooks []Hook `toml:"hook"`
Schedules []ScheduleSpec `toml:"schedule"`
Access AppAccess `toml:"access"`
Tracing TracingSettings `toml:"tracing"`
}
Manifest is the decoded shinyhub.toml.
func LoadManifest ¶ added in v0.3.3
LoadManifest reads shinyhub.toml from bundleDir. Returns (nil, nil) when no manifest is present so callers can treat the file as optional. A malformed manifest is fatal: deploys must not silently skip declared hooks because the operator mistyped a field.
func (*Manifest) PostDeploy ¶ added in v0.3.3
PostDeploy returns the subset of hooks that should fire after dependency installation but before app processes start. Order is preserved.
type Params ¶
type Params struct {
Slug string
// AppID is the owning app's numeric DB id, threaded onto each replica's
// StartParams so a runtime can namespace per-app external resources (e.g.
// Fargate secret store names and task-def families) without a slug collision
// across a delete-then-recreate. Zero is allowed for local-only deploys.
AppID int64
BundleDir string
// Command overrides auto-detection. If empty, the app type is detected from
// the bundle and the appropriate runtime command is built per replica.
Command []string
Env []string
Workers int
Replicas int // 0 → 1 (single-replica fallback); also the fallback total when Placement is empty
// Placement maps tier name → replica count. Empty means "all Replicas on the
// default tier", reproducing single-tier behavior. When set, the sum of its
// counts is the authoritative replica total and Replicas is ignored.
Placement map[string]int
// TierOrder is the config-declared tier order used to lay out placement
// counts deterministically over a single global replica index space. Empty
// is treated as just the default tier.
TierOrder []string
// DefaultTier is the tier a replica runs under when Placement is empty or a
// tier is otherwise unresolved. Empty falls back to process.DefaultTier.
DefaultTier string
Manager *process.Manager
Proxy *proxy.Proxy
HealthTimeout time.Duration // 0 means defaultHealthTimeout (or [app] startup_timeout_seconds)
MemoryLimitMB int // 0 = no limit
CPUQuotaPercent int // 0 = no limit; 100 = 1 full core
// MaxSessionsPerReplica caps the per-replica active connection count the
// proxy will route cookie-less requests to; saturated pools shed with
// 503 + Retry-After. 0 = unlimited (caller should resolve the runtime
// default before calling).
MaxSessionsPerReplica int
// IdentityHeaders is the app's resolved effective identity-forwarding
// flag (ResolveIdentityHeaders over the app column + global config).
// Run pushes it to the proxy pool alongside the session cap.
IdentityHeaders bool
// HealthCheck is called after each replica starts to verify it is ready.
// It receives the runtime-returned endpoint URL (e.g. http://127.0.0.1:PORT).
// If nil, the default HTTP health poller (waitHealthy) is used.
HealthCheck func(endpointURL string, timeout time.Duration, transport http.RoundTripper) error
// ContentDigest, DeploymentID, and AppVersion travel with the launch so a
// remote runtime can pull-by-digest and stamp recovery labels. Empty/zero is
// allowed for local-only deploys but every API launch path now populates them.
ContentDigest string
DeploymentID int64
AppVersion string
// ColocateWorkers pins every replica of this pool to one of the named worker
// node ids, overriding least-loaded placement. The control plane sets it for
// a shared-mount consumer so each replica lands on a worker that also hosts
// its source's provisioned data. Empty means unconstrained placement. Only
// worker-routing tiers honor it; a tier whose runtime ignores TargetWorker
// (the native local tier) is unaffected.
ColocateWorkers []string
// WorkerIsolation is the per-app isolation mode stored in the DB (may be
// empty, meaning "inherit fleet default"). DefaultWorkerIsolation is the
// fleet-level fallback from config. Both are set by withTierPlacement and
// drive SetPoolMode inside Run.
WorkerIsolation string
DefaultWorkerIsolation string
// WorkerGroupedSize and WorkerMaxWorkers configure the elastic pool when
// mode is grouped or per_session; ignored for multiplex.
WorkerGroupedSize int
WorkerMaxWorkers int
}
Params controls a deploy operation.
type PoolResult ¶ added in v0.2.1
type PoolResult struct {
Replicas []Result
Failed []int
// HooksSkipped counts post-deploy hooks declared in the manifest that were
// not run because the runtime prepares dependencies inside a container
// (the host has no view of the app's environment). 0 when hooks ran or none
// were declared. The API relays this so the developer learns their hooks
// did not execute instead of finding out only from the server log.
HooksSkipped int
}
PoolResult contains the full set of replicas that were successfully booted. Failed lists the indices whose boot failed in a partial-success deploy, so the caller can persist them as crashed and let the watcher reconcile the pool back up to the desired replica count.
func Run ¶
func Run(p Params) (*PoolResult, error)
Run orchestrates a parallel pool deploy: spawns N replicas concurrently, health-checks each, and registers surviving replicas with the reverse proxy. Partial failure (some replicas healthy, some not) is accepted and logged. All-fail returns an error.
type Result ¶
type Result struct {
Index int
PID int
Port int
EndpointURL string
Tier string
Provider string
WorkerID string
}
Result contains identifiers for a single successfully deployed replica.
func ResumeReplica ¶ added in v0.8.12
ResumeReplica restores a single suspended replica via the Manager's Snapshotter path, runs an abbreviated readiness probe, and registers the route. It mirrors RunReplica's post-start steps (health check + proxy register) but skips the cold boot and dependency prep. It returns a wrapped sentinel (ErrRuntimeNotSnapshotter / ErrReplicaNotSuspended / ErrReplicaNotFound) when the slot cannot be resumed, so the caller falls back to RunReplica.
type ScheduleSpec ¶ added in v0.5.0
type ScheduleSpec struct {
Name string `toml:"name"`
Cron string `toml:"cron"`
Cmd string `toml:"cmd"`
CmdJSON string `toml:"cmd_json"`
TimeoutSeconds *int `toml:"timeout_seconds"`
Overlap string `toml:"overlap"`
Missed string `toml:"missed"`
Disabled bool `toml:"disabled"`
// Timezone is an optional IANA timezone for the schedule. Empty means
// "inherit the server default". Validated against time.LoadLocation at
// manifest parse time.
Timezone string `toml:"timezone"`
// RunOnRegister, when true, fires this schedule once immediately the first
// time it is registered on an app that has never had a successful run of it
// - warming the app's cache on a fresh deploy. It is a deploy-time
// instruction, never persisted; the gate lives in the server.
RunOnRegister bool `toml:"run_on_register"`
Command []string `toml:"-"`
}
ScheduleSpec mirrors one [[schedule]] block, post-resolution: Command is set from either Cmd (shell-parsed) or CmdJSON (JSON-parsed) during LoadManifest so the application layer doesn't re-parse and never sees an unparseable manifest reach the DB.
type TracingSettings ¶ added in v0.8.5
type TracingSettings struct {
Auto *bool `toml:"auto"`
}
TracingSettings mirrors the [tracing] section. Auto overrides the fleet's tracing.auto_instrument_apps default in either direction; nil (section or key absent) means "inherit the fleet default". The override is re-read from the bundle at every boot, so it travels with each deployed version, including rollbacks.
type WorkerManifest ¶ added in v0.9.2
type WorkerManifest struct {
Isolation *string `toml:"isolation"`
GroupedSize *int `toml:"grouped_size"`
MaxWorkers *int `toml:"max_workers"`
MaxSessionLifetimeSecs *int `toml:"max_session_lifetime_secs"`
}
WorkerManifest mirrors the [app.worker] inline table. All fields are pointers so "absent" (nil) is distinct from an explicit zero value.