rig

package
v0.33.0 Latest Latest
Warning

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

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

README

pkg/rig

pkg/rig is the composition root for an agent runtime. A consumer assembles a *Rig from loop definitions, hustle definitions, a session store, primers, and a workspace placement; the Rig then creates and restores live sessions.

It owns design-time topology and lifecycle policy — what loops exist, what they're called, which one starts active, where the session is persisted, where the workspace lives — while the live runtime behavior lives behind the pkg/session contracts the Rig returns. Construction and restoration of a session are owned exclusively here; nothing else in the module can mint or resume one.

What is rig?

A Rig is an immutable design-time assembly. rig.Define(opts...) validates the options, freezes the assembly, and returns a *Rig. The two lifecycle methods are the whole public surface:

  • rig.NewSession(ctx, opts...) — bring up a brand-new live session. Optionally seed its workspace from a snapshot via WithSeedSnapshot.
  • rig.RestoreSession(ctx, id) — rebuild a prior session from its durable journal by id.

Both return a session.SessionController, which embeds session.Session (the data plane) and adds the trusted policy/lifecycle methods (SetActiveLoop, LoopController, CheckpointWorkspace, RestoreWorkspace, Shutdown).

How to use

reviewPolicy, err := gate.DefaultPermissionReviewPolicy("review-policy-v1")
if err != nil { /* ... */ }

r, err := rig.Define(
    rig.WithLoops(operatorLoop, reviewerLoop),
    rig.WithPrimers("operator"),
    rig.WithSessionStore(sessionStore),
    rig.WithExclusiveWorkspace(workspaceStore, "/repo", leaser),
    rig.WithHustles(/* ...optional hustle.Definition values... */...),
    rig.WithHustleLimits(rig.HustleLimits{ /* ... */ }),
    rig.WithPermissionClassifiers(permissionClassifiers),
    rig.WithPermissionReviewPolicy(reviewPolicy),
    rig.WithPermissionReviewSecurityCeiling("consumer-access-profile/v1"),
    // Optional: bounded evidence-tool access for classifiers that gather
    // read-only evidence. Required whenever any registered classifier's
    // definition declares evidence tools — see pkg/gate/README.md#evidence-boundaries.
    rig.WithPermissionReviewEvidence(evidenceAccess, evidenceContainment, allowedEvidenceKinds),
    // Optional: TOCTOU recheck (design §13.4) for classifiers whose evidence
    // tools observe a specific target's identity/state. Only meaningful
    // paired with WithPermissionReviewEvidence; harmless to omit if no
    // registered classifier's evidence tools are target-sensitive.
    rig.WithPermissionReviewObservations(observationVerifier),
    // Optional: circuit-breaker thresholds (design §18). Defaults every one
    // of 8 turn+session counters to rig.DefaultPermissionReviewBreakerThreshold
    // (20) when omitted but classifiers are configured.
    rig.WithPermissionReviewLimits(rig.PermissionReviewLimits{ /* ... */ }),
    rig.WithForeignBuilders(/* ...foreign.Builder for codex/claude... */...),
    rig.WithGateCaps(rig.GateCaps{MaxOpen: 16, MaxTimeout: 30*time.Second}),
    rig.WithDelegationLimits(rig.DelegationLimits{Depth: 4, Quota: 32}),
    rig.WithRestoreDecider(session.DefaultPolicyDecider{}),
)
if err != nil { /* ... */ }

ctx := context.Background()
session, err := r.NewSession(ctx)
if err != nil { /* ... */ }
defer session.Shutdown(ctx)

Restore is the same shape with the session id:

session, err := r.RestoreSession(ctx, priorSessionID)

rig.WithPermissionClassifiers, WithPermissionReviewPolicy, WithPermissionReviewEvidence, WithPermissionReviewObservations, and WithPermissionReviewSecurityCeiling are all supplied at rig.Define time above, not per restore — restore itself takes no options. Omitting every one of them (the zero-classifier rig) is how you disable permission review entirely: it preserves the pre-classifier gate behavior byte-for-byte, with no separate on/off flag to flip. See pkg/gate/README.md for the full enable/disable, evidence-boundary, observation-recheck (TOCTOU), human-fallback, audit/privacy, policy-tuning, evaluation-workflow, and restore-behavior story; that package — not this one — owns the neutral review domain these options configure.

Restore and a changed permission-review configuration

Restoring a session whose rig now has permission review configured differently than when it was opened follows the ordinary configuration-drift policy above, with one extra rule specific to this feature: turning classifiers on where the session was originally opened with none configured is always a rejected DriftWarn, never a silent auto-accept, even though the rest of that restore's drift might otherwise be all-Info. A rig only resumes with review newly enabled if it explicitly opted in — with rig.WithRestoreDecider (preferred: inspect the assessment and accept this dimension specifically) or the older, blanket rig.WithAllowConfigMismatch() (accepts every Warn-level drift, not just this one). Turning classifiers off, or restoring with the same or a differently-identified classifier set that was already enabled before, is unaffected by this rule.

Sibling packages

  • pkg/looploop.Definition values you pass to rig.WithLoops.
  • pkg/session — the Session / SessionController interfaces the lifecycle returns.
  • pkg/sessionstore — the *sessionstore.Store you pass to rig.WithSessionStore.
  • pkg/workspacestore — the *workspacestore.Store you pass to a workspace placement.
  • pkg/hustlehustle.Definition values you pass to rig.WithHustles.
  • pkg/foreignforeign.Builder values you pass to rig.WithForeignBuilders for codex/claude backends.
  • pkg/gate — the gate.Evaluator you bind into each loop.Definition via loop.WithAccessGate.

How it is designed

pkg/rig is intentionally thin. It validates options, freezes the assembly, and delegates to the private internal/sessionruntime coordinator, which owns the live loops, hub, journal, and workspace lifecycle.

        rig.Define(opts...)
                │
                │ validate + freeze
                ▼
        *rig.Rig ──► internal/sessionruntime.Lifecycle
                            │
                            │ NewSession / RestoreSession
                            ▼
                     internal/sessionruntime.Session
                       │  │  │  │
                       │  │  │  └──► pkg/workspacestore (workspace snapshots)
                       │  │  └──────► pkg/sessionstore  (journal + catalog)
                       │  └─────────► pkg/hub           (event fan-in)
                       └────────────► internal/loopruntime (loop actors)
                                            │
                                            ▼
                                       pkg/session contracts returned to caller
Validation at the boundary

Define enforces the invariants of a valid rig before any session is created:

  • At least one loop and at least one primer; the active primer must be a registered loop name.
  • Loop names are unique; the active primer is the only one if exactly one loop is supplied.
  • Every delegate a loop declares is itself a registered loop.
  • A *sessionstore.Store is required; workspace placement is optional but at most one placement may be configured.
  • Hustle lane bounds are within MaxHustleQueued; gate caps are positive.
  • Permission classifiers are supplied as one validated, ordered gate.PermissionClassifierSet and are paired with a canonical local review policy revision. Supplying only one half is rejected. Their frozen definitions are automatically registered as blocking Hustles, so WithHustleLimits is required and consumers must not also pass those same definitions to WithHustles.
  • Foreign builders and restore decider are optional with fail-secure defaults.

A bad configuration fails closed at Define rather than at session construction.

Configuration fingerprint

Define computes an immutable InitialFingerprint for each loop (model, effective system, tool names) so the rig can stamp and compare compatibility before any runtime factories execute. At restore time the rig runs the configured RestoreDecider against a DriftAssessment and records the decision as a durable ConfigurationAdopted; the default DefaultPolicyDecider accepts only when every change is Info and rejects when any is Warn.

Permission review extends the topology identity with the local review-policy revision and the classifiers in registration order. Each classifier row carries only frozen, secret-free identity: classifier name and revision, its complete definition digest, structured-output digest/revision, evidence definition and produced-name digests, and every evidence-loop bound. Classifier order is significant because combination is ordered. Evidence policies accept only sealed tool.NewEvidenceDefinition definitions with frozen ToolInfo metadata. Their factories use tool.EvidenceFactoryBindings, whose complete public capability surface is the invocation session/loop identity and an optional root-only tool.ReadWorkspaceBinding; generic workspace mutation, observations, delegation, gate/grant/control state, and extra tools are not in the factory API. Canonical static descriptions and compact portable schemas contribute to the evidence catalog digest, so either can change topology identity before a session is persisted or restored. The fingerprint stores only those versioned digests: raw prompts, schemas, descriptions, model clients and credentials, workspace paths, live review subjects, and runtime-bound tool objects are never serialized into it. At concrete binding, each tool's twice-read metadata must exactly match the frozen static name, description, and schema; drift fails closed. Automatic classifier-definition registration deliberately routes this through the same Hustle binding path as every other evidence-enabled definition.

Omitting both permission-review options preserves the pre-classifier fingerprint and gate behavior byte-for-byte. There is no implicit classifier registry or default review policy.

Documentation

Overview

Package rig is the public composition root for defining an agent rig and creating or restoring its sessions. It owns design-time topology and lifecycle policy while the returned session contracts expose live runtime behavior.

Index

Constants

View Source
const DefaultPermissionReviewBreakerThreshold = 20

DefaultPermissionReviewBreakerThreshold is the default numeric circuit-breaker threshold Define() resolves for every turn-scoped and session-scoped counter (resolvePermissionReviewLimits in definition.go) when classifiers are configured but WithPermissionReviewLimits was never explicitly called.

View Source
const MaxHustleQueued = 10_000

MaxHustleQueued is the largest configured waiting capacity for either hustle lane. The execution controller may allocate no queue larger than this bound.

Variables

This section is empty.

Functions

func FingerprintFrom

func FingerprintFrom(definition loop.BoundDefinition) event.ConfigFingerprint

FingerprintFrom derives the stable, secret-free behavior fingerprint of a bound loop.

Types

type ConfigFingerprintFields

type ConfigFingerprintFields struct {
	AgentKind     string
	RuntimeSkills bool
	WorkspaceRoot string
	// AdapterID identifies a foreign-agent adapter. Empty means native.
	AdapterID string
	// Posture identifies a foreign agent's non-interactive permission posture.
	Posture string
	// NativePermissionPolicyRev is the digest of native permission configuration.
	NativePermissionPolicyRev string
	// ExternalCapabilityRev is the digest of the identity of external capabilities
	// the composition root attached to the session — tools served by processes
	// Harness does not own, such as MCP servers. Empty means none, which is what
	// keeps it additive for every rig that attaches nothing.
	//
	// The rig neither computes nor interprets it: only the composition root knows
	// what it attached. The canonical producer is github.com/looprig/mcp's
	// mcpharness.Manager.ConfigDigest, taken after the Manager has started.
	ExternalCapabilityRev string

	// WorkspaceTrust is an opaque, secret-free label for the workspace's trust
	// posture (e.g. "trusted"/"untrusted"). Empty means unspecified.
	WorkspaceTrust string
	// PermissionStrictness is the ordered native-permission posture level; higher is
	// stricter. Zero means unknown (drift assessment fails secure). It complements
	// NativePermissionPolicyRev, which is the digest-only identity.
	PermissionStrictness event.StrictnessLevel
	// ConfinementRev is a content digest of the confinement (sandbox) configuration.
	// Empty means none. Harness compares it, never parses it.
	ConfinementRev string
	// ConfinementStrictness is the ordered confinement posture level; higher is
	// stricter. Zero means unknown.
	ConfinementStrictness event.StrictnessLevel
	// AppFields are application-defined, secret-free compatibility fields the
	// composition root attaches. Canonically encoded in sorted key order by the
	// manifest. Nil means none.
	AppFields          map[string]string
	RuntimeProfile     string
	RuntimeCatalogRev  string
	RuntimeIdentityRev string
}

ConfigFingerprintFields are immutable rig-level behavior inputs that are not part of a loop.Definition. Define freezes them for both session creation and restoration.

type DefinitionError

type DefinitionError struct {
	Kind  DefinitionErrorKind
	Name  string
	Cause error
}

func (*DefinitionError) Error

func (e *DefinitionError) Error() string

func (*DefinitionError) Unwrap

func (e *DefinitionError) Unwrap() error

type DefinitionErrorKind

type DefinitionErrorKind string
const (
	DefinitionNilOption                       DefinitionErrorKind = "nil_option"
	DefinitionMissingLoop                     DefinitionErrorKind = "missing_loop"
	DefinitionInvalidLoop                     DefinitionErrorKind = "invalid_loop"
	DefinitionDuplicateLoop                   DefinitionErrorKind = "duplicate_loop"
	DefinitionMissingPrimer                   DefinitionErrorKind = "missing_primer"
	DefinitionInvalidPrimer                   DefinitionErrorKind = "invalid_primer"
	DefinitionInvalidActivePrimer             DefinitionErrorKind = "invalid_active_primer"
	DefinitionMissingSessionStore             DefinitionErrorKind = "missing_session_store"
	DefinitionInvalidSessionStore             DefinitionErrorKind = "invalid_session_store"
	DefinitionInvalidDelegationLimits         DefinitionErrorKind = "invalid_delegation_limits"
	DefinitionInvalidForeignBuilders          DefinitionErrorKind = "invalid_foreign_builders"
	DefinitionInvalidGateCaps                 DefinitionErrorKind = "invalid_gate_caps"
	DefinitionInvalidRestoreDecider           DefinitionErrorKind = "invalid_restore_decider"
	DefinitionInvalidRuntimeRestoreResolver   DefinitionErrorKind = "invalid_runtime_restore_resolver"
	DefinitionInvalidRestoreFailurePolicy     DefinitionErrorKind = "invalid_restore_failure_policy"
	DefinitionDuplicateOption                 DefinitionErrorKind = "duplicate_option"
	DefinitionInvalidHustle                   DefinitionErrorKind = "invalid_hustle"
	DefinitionDuplicateHustle                 DefinitionErrorKind = "duplicate_hustle"
	DefinitionMissingHustleLimits             DefinitionErrorKind = "missing_hustle_limits"
	DefinitionUnusedHustleLimits              DefinitionErrorKind = "unused_hustle_limits"
	DefinitionInvalidHustleLimits             DefinitionErrorKind = "invalid_hustle_limits"
	DefinitionInvalidHooks                    DefinitionErrorKind = "invalid_hooks"
	DefinitionMissingResourceStorage          DefinitionErrorKind = "missing_resource_storage"
	DefinitionInvalidResourceStorage          DefinitionErrorKind = "invalid_resource_storage"
	DefinitionMissingCompactionHustle         DefinitionErrorKind = "missing_compaction_hustle"
	DefinitionIncompatibleCompactionHustle    DefinitionErrorKind = "incompatible_compaction_hustle"
	DefinitionInvalidPermissionClassifiers    DefinitionErrorKind = "invalid_permission_classifiers"
	DefinitionInvalidPermissionReviewPolicy   DefinitionErrorKind = "invalid_permission_review_policy"
	DefinitionIncompletePermissionReview      DefinitionErrorKind = "incomplete_permission_review"
	DefinitionUnusedPermissionReviewLimits    DefinitionErrorKind = "unused_permission_review_limits"
	DefinitionInvalidPermissionReviewEvidence DefinitionErrorKind = "invalid_permission_review_evidence"
	DefinitionMissingPermissionReviewEvidence DefinitionErrorKind = "missing_permission_review_evidence"
	DefinitionUnusedPermissionReviewEvidence  DefinitionErrorKind = "unused_permission_review_evidence"

	// DefinitionInvalidPermissionReviewSecurityCeiling: WithPermissionReviewSecurityCeiling
	// was called with an empty (or all-whitespace) ceiling string. Rejected at
	// Define()-time rather than deferred to a later, harder-to-diagnose
	// review-context-capture failure (gate.ReviewContext's own non-empty
	// SecurityCeiling validation rule).
	DefinitionInvalidPermissionReviewSecurityCeiling DefinitionErrorKind = "invalid_permission_review_security_ceiling"
	// DefinitionMissingPermissionReviewSecurityCeiling: at least one permission
	// classifier is configured (WithPermissionClassifiers) but
	// WithPermissionReviewSecurityCeiling was never called. SecurityCeiling is a
	// consumer-owned value Harness cannot originate (Finding 2, Phase 6
	// spec-compliance review): a classifier-registered session with no ceiling
	// would otherwise fail every real evidence-tool containment check closed,
	// silently, at runtime.
	DefinitionMissingPermissionReviewSecurityCeiling DefinitionErrorKind = "missing_permission_review_security_ceiling"
	// DefinitionUnusedPermissionReviewSecurityCeiling: WithPermissionReviewSecurityCeiling
	// was called but no permission classifier is configured, mirroring
	// DefinitionUnusedPermissionReviewEvidence's "config X requires config Y"
	// symmetric check.
	DefinitionUnusedPermissionReviewSecurityCeiling DefinitionErrorKind = "unused_permission_review_security_ceiling"

	// DefinitionInvalidPermissionReviewObservations: WithPermissionReviewObservations
	// was called with a nil verifier.
	DefinitionInvalidPermissionReviewObservations DefinitionErrorKind = "invalid_permission_review_observations"
	// DefinitionUnusedPermissionReviewObservations: WithPermissionReviewObservations
	// was called but either no permission classifier is configured at all, or
	// no WithPermissionReviewEvidence was configured (design §13.4's
	// observation-recheck mechanism lives entirely inside the evidence
	// runtime — a verifier with no evidence runtime to ever record an
	// observation into is dead configuration). There is deliberately no
	// symmetric "missing" error the way DefinitionMissingPermissionReviewEvidence
	// pairs with WithPermissionReviewEvidence: see WithPermissionReviewObservations'
	// own doc comment for why "a classifier's evidence tools need this"
	// cannot be determined at Define()-time today, and how runtime instead
	// fails closed (internal/sessionruntime/gates.go's
	// verifyPermissionReviewObservations) if that ever turns out to matter
	// for a given session.
	DefinitionUnusedPermissionReviewObservations DefinitionErrorKind = "unused_permission_review_observations"

	// DefinitionInvalidToolResultCapture: WithToolResultCapture was called with a
	// nil object store, or with a spill base that is not an absolute path (which
	// includes empty and all-whitespace). Name is a FIELD LABEL — exactly
	// "objects" or "spill_base" — and never the rejected value, so the error can
	// be logged or returned without treating it as caller-supplied data.
	// Rejected at Define rather than at the first oversized tool result, because
	// the failure mode otherwise is a turn that ends on a retention error long
	// after the misconfiguration.
	DefinitionInvalidToolResultCapture DefinitionErrorKind = "invalid_tool_result_capture"
	// DefinitionToolResultSpillOverlapsWorkspace: the capture spill base is equal
	// to, inside, or an ancestor of the configured workspace region. A workspace
	// checkpoint archives the whole region, so a spill inside it would be
	// captured into every checkpoint — and a region inside the spill base would
	// be deleted with it at session shutdown. Name carries the canonical base.
	DefinitionToolResultSpillOverlapsWorkspace DefinitionErrorKind = "tool_result_spill_overlaps_workspace"
)

type DelegationLimits

type DelegationLimits struct {
	Depth int
	Quota int
}

type GateCaps

type GateCaps struct {
	MaxOpen    int
	MaxTimeout time.Duration
}

type HustleLimits

type HustleLimits struct {
	BlockingConcurrent   int
	BlockingQueued       int
	BackgroundConcurrent int
	BackgroundQueued     int
	AuditTimeout         time.Duration
	FinalizationTimeout  time.Duration
	WorkerDrainTimeout   time.Duration
}

HustleLimits bounds the two independent execution lanes and their audit, finalization, and worker-drain operations.

type InvalidOffloadGCIntervalError

type InvalidOffloadGCIntervalError struct {
	Interval time.Duration
}

InvalidOffloadGCIntervalError reports a non-positive OffloadGCPolicy.Interval. A GC cadence must be a positive duration, so the rig fails closed at definition time rather than wiring a runner that would never (or continuously) tick.

func (*InvalidOffloadGCIntervalError) Error

type InvalidOffloadGCTimeoutError

type InvalidOffloadGCTimeoutError struct {
	Timeout time.Duration
}

InvalidOffloadGCTimeoutError reports a non-positive OffloadGCPolicy.Timeout. A per-pass deadline must be a positive duration, so the rig fails closed at definition time.

func (*InvalidOffloadGCTimeoutError) Error

type LifecycleError

type LifecycleError struct {
	Kind  LifecycleErrorKind
	Cause error
}

func (*LifecycleError) Error

func (e *LifecycleError) Error() string

func (*LifecycleError) Unwrap

func (e *LifecycleError) Unwrap() error

type LifecycleErrorKind

type LifecycleErrorKind string
const (
	LifecycleContextDone                     LifecycleErrorKind = "context_done"
	LifecycleIDGenerationFailed              LifecycleErrorKind = "id_generation_failed"
	LifecycleLeaseFailed                     LifecycleErrorKind = "lease_failed"
	LifecycleJournalFailed                   LifecycleErrorKind = "journal_failed"
	LifecycleAppenderFailed                  LifecycleErrorKind = "appender_failed"
	LifecycleSessionFailed                   LifecycleErrorKind = "session_failed"
	LifecycleProcessNotificationsUnsupported LifecycleErrorKind = "process_notifications_unsupported"
)

type OffloadGCPolicy

type OffloadGCPolicy struct {
	Interval time.Duration
	Timeout  time.Duration
}

OffloadGCPolicy configures session offload-blob GC: how often a GC pass runs (Interval) and the per-pass deadline (Timeout). It reaps orphaned content-addressed offload blobs — a blob left durable with no in-ledger blobptr pointer (the crash gap of the writer's blob-durable-before-pointer discipline). This is SESSION OFFLOAD GC only, never workspace-snapshot GC. Both fields must be positive.

type Option

type Option func(*definitionState) error

func WithActivePrimer

func WithActivePrimer(name string) Option

func WithAllowConfigMismatch

func WithAllowConfigMismatch() Option

func WithDelegationLimits

func WithDelegationLimits(limits DelegationLimits) Option

func WithExclusiveWorkspace

func WithExclusiveWorkspace(store *workspacestore.Store, root string, leaser storage.Leaser) Option

WithExclusiveWorkspace declares one canonical fixed root fenced by a single exclusive root lease from leaser. The session lease is acquired first, then the root lease named workspace-roots/<sha256(canonical-root)>, so lexical/symlink aliases of the root contend.

func WithFingerprintFields

func WithFingerprintFields(fields ConfigFingerprintFields) Option

func WithForeignBuilders

func WithForeignBuilders(builder foreign.Builder, restored foreign.RestoredBuilder) Option

func WithForeignServicesBuilders

func WithForeignServicesBuilders(builder foreign.ServicesBuilder, restored foreign.ServicesRestoredBuilder) Option

WithForeignServicesBuilders opts a rig into the additive foreign-engine seam. Each live/restored foreign origin receives a fresh loop-scoped broker descriptor and delivery hook at session construction; no capability is part of the immutable rig configuration. The legacy WithForeignBuilders option remains supported and receives zero services.

func WithGateCaps

func WithGateCaps(caps GateCaps) Option

func WithHooks

func WithHooks(set hook.Set) Option

WithHooks installs one immutable operation-hook set. Define validates and compiles the captured set after every option has resolved.

func WithHustleLimits

func WithHustleLimits(limits HustleLimits) Option

WithHustleLimits configures the required singleton lane bounds.

func WithHustles

func WithHustles(definitions ...hustle.Definition) Option

WithHustles adds immutable hustle definitions to the rig.

func WithLoops

func WithLoops(definitions ...loop.Definition) Option

func WithOffloadGC

func WithOffloadGC(policy OffloadGCPolicy) Option

WithOffloadGC arms periodic session offload-blob GC. It validates the policy (both fields positive; typed errors on failure) and compiles to the sessionruntime lifecycle option that wires the journal-admission gate + GC runner onto both new and restored sessions. It is an at-most-once rig option.

func WithPermissionClassifiers

func WithPermissionClassifiers(classifiers gate.PermissionClassifierSet) Option

WithPermissionClassifiers installs the already-validated, ordered permission classifier registry. Registration order is behavioral and therefore remains significant in the rig fingerprint.

func WithPermissionReviewEvidence

func WithPermissionReviewEvidence(access gate.EvidenceAccessEvaluator, containment gate.EvidenceContainmentVerifier, allowedKinds []string) Option

WithPermissionReviewEvidence installs the consumer-supplied read-only evidence-tool access boundary every registered permission classifier's evidence tools run under (design §13.1). access answers the configured access state for one prepared evidence Requirement; containment independently performs the trusted-caller containment check (resolving symlinks, rejecting ambiguous scopes, enforcing the review's own security ceiling); allowedKinds is the explicit consumer-consent allowlist of Requirement.Kind values evidence tools may declare. Both access and containment are read-only, headless, trusted-caller seams — neither receives session, gate, mutation, grant, rule, or loop-control capability (design §13.1's Access/Containment split; Access alone was omitted from the design sketch of this option's signature, but the same fail-closed requirement applies to it: hustleruntime refuses to bind an evidence catalog with a nil Access evaluator exactly as it refuses a nil Containment verifier).

Required whenever any registered classifier's definition needs evidence tools; Define fails closed (DefinitionMissingPermissionReviewEvidence) if omitted in that case, and rejects it (DefinitionUnusedPermissionReviewEvidence) when supplied but no registered classifier needs it — mirroring the existing MissingHustleLimits/UnusedHustleLimits "config X requires config Y" pairing already used elsewhere in this file.

func WithPermissionReviewLimits

func WithPermissionReviewLimits(limits PermissionReviewLimits) Option

WithPermissionReviewLimits installs the circuit-breaker thresholds (design §18) applied to automatic permission review. It is only meaningful paired with WithPermissionClassifiers; Define() enforces that pairing (DefinitionUnusedPermissionReviewLimits) rather than this Option, because option application order is not guaranteed (classifiers may be registered before or after this call in the Define() argument list).

Omitting this option while classifiers ARE configured resolves a default of DefaultPermissionReviewBreakerThreshold on every one of the 8 numeric thresholds (resolvePermissionReviewLimits, definition.go); an explicit call always replaces that default wholesale — all 8 fields at once, never merged per-field.

func WithPermissionReviewObservations

func WithPermissionReviewObservations(verifier gate.EvidenceObservationVerifier) Option

WithPermissionReviewObservations installs the consumer-supplied, read-only TOCTOU-recheck seam (design §13.4) every classifier-originated auto-approval's recorded observations are verified against immediately before the gate is claimed. verifier independently re-derives each previously recorded gate.ObservationRequirement's current token and fails closed (leaving the human gate open) on any mismatch or unverifiable target — see gate.EvidenceObservationVerifier's own doc comment for the full trusted-caller contract, which deliberately reuses gate.EvidenceContainmentPolicy rather than introducing a parallel policy type (the security context — canonical read root plus the review's own non-widenable ceiling — is identical to WithPermissionReviewEvidence's own Containment collaborator).

A separate option, deliberately NOT folded into WithPermissionReviewEvidence's signature: the two are independent concerns (an evidence-tool access boundary a session needs whenever ANY registered classifier declares evidence tools, versus a TOCTOU recheck a session needs only when at least one of those evidence tools is target-sensitive). Folding Observations in as a fourth WithPermissionReviewEvidence parameter would force every existing and future caller with no target-sensitive evidence tools — the common case today, since no evidence tool in this codebase declares itself target-sensitive yet — to pass an explicit nil at that call site for a concern it will never use, which is exactly the Interface Segregation violation this package's own CLAUDE.md guidance warns against.

Required (rejected as DefinitionUnusedPermissionReviewObservations) unless BOTH at least one permission classifier is registered AND WithPermissionReviewEvidence is also configured — mirroring the "config X requires config Y" pairing precedent, but ONLY in the unused direction. There is deliberately NO symmetric "missing" pairing error the way WithPermissionReviewEvidence itself is required whenever anyClassifierNeedsEvidence(state.permissionClassifiers) reports true: that check is possible because hustle.Definition.EvidenceToolPolicy() already gives Define() a real, static, per-classifier "does this need an evidence runtime at all" signal. Nothing analogous exists for "is any of THIS classifier's evidence tools target-sensitive" — no evidence tool definition in this codebase declares that today (it is a per-concrete-tool capability, tool.EvidenceObservation, probed only at runtime after a call executes, not a static Definition-level property Define() could inspect ahead of time the way it inspects EvidenceToolPolicy). Manufacturing a parallel static declarer purely to unlock a Define()-time "missing" check, before any real tool exists that would ever set it, would be speculative generality with no consumer to validate it against. Instead, runtime fails closed: internal/sessionruntime/gates.go's verifyPermissionReviewObservations treats "an observation WAS recorded but no verifier is configured" identically to a genuine mismatch — stale, human gate stays open, never a silent pass — so a consumer who adds a target-sensitive evidence tool later without also wiring this option gets an always-safe (if initially confusing, and always correctable) runtime outcome rather than a false sense of protection. If a future evidence-tool capability gives Define() a real static signal, tightening this to a "missing" pairing error too is a natural, non-breaking follow-up — see this addendum's plan section for the explicit flag.

func WithPermissionReviewPolicy

func WithPermissionReviewPolicy(policy gate.PermissionReviewPolicy) Option

WithPermissionReviewPolicy installs the immutable local decision policy (design §20) every session forwards into the review runtime. Only the policy's Revision feeds rig identity (permissionReviewFingerprintFrom in fingerprint.go); the full value is what sessionruntime actually applies.

A policy that was never built through gate.NewPermissionReviewPolicy or gate.DefaultPermissionReviewPolicy (a hand-built literal gate.PermissionReviewPolicy{}, whose zero seal makes it fail closed at EvaluatePermissionAssessment regardless) is rejected here immediately, rather than silently accepted and left to fail closed later with a confusing runtime symptom. This is a developer-experience improvement: the security property already holds without it.

func WithPermissionReviewSecurityCeiling

func WithPermissionReviewSecurityCeiling(ceiling string) Option

WithPermissionReviewSecurityCeiling installs the consumer-supplied, effective security posture (design §13.1/§21) every registered permission classifier's ReviewContext/ReviewBasis carries as SecurityCeiling, and every evidence-tool containment check (WithPermissionReviewEvidence's Containment collaborator) is run against.

SecurityCeiling is architecturally the SAME KIND of value as the Containment/AllowedKinds collaborators WithPermissionReviewEvidence installs — a consumer-owned concept Harness structurally cannot and should not originate (this module has no first-class "effective access posture" notion; the product composition root binds its own AccessProfile name here). It is NOT like a workspace root, which Harness genuinely owns and auto-derives. A plain string, not a provider func: a consumer's ceiling is fixed for the session's lifetime by design (YAGNI — see the design consult this option was written from).

Required whenever any permission classifier is registered (WithPermissionClassifiers); Define fails closed (DefinitionMissingPermissionReviewSecurityCeiling) if omitted in that case — before this option existed, every session instead stamped a fixed Harness-side sentinel that could never equal a real consumer's own ceiling, so every real evidence-tool containment check failed closed unconditionally (Finding 2, Phase 6 spec-compliance review). It is rejected (DefinitionUnusedPermissionReviewSecurityCeiling) when supplied but no classifiers are configured, mirroring WithPermissionReviewEvidence's own "config X requires config Y" pairing. An empty (or all-whitespace) ceiling is rejected here, immediately, at Define() time — never deferred to a later, harder-to-diagnose review-context-capture failure (gate.ReviewContext's own non-empty SecurityCeiling validation rule already fails closed on an empty value, but silently and much later).

func WithPrimers

func WithPrimers(names ...string) Option

func WithRestoreDecider

func WithRestoreDecider(decider session.RestoreDecider) Option

WithRestoreDecider installs the application policy that decides whether a configuration-drifted restore proceeds. It is the successor to WithAllowConfigMismatch: rather than a blanket override, the decider inspects the typed drift assessment and accepts or rejects. Omitting it leaves restore on the fail-secure session.DefaultPolicyDecider (reject on any Warn). A nil decider is rejected at definition time so the option cannot silently disarm the default.

func WithRestoreFailurePolicy added in v0.27.0

func WithRestoreFailurePolicy(options ...RestoreFailureOption) Option

WithRestoreFailurePolicy installs one fail-closed declarative restore policy. Each Allow…Drift option exempts only its named fact; unlisted warnings remain fatal and exact runtime reconstruction remains the first restore attempt.

func WithRuntimeCatalog

func WithRuntimeCatalog(catalog loop.RuntimeCatalog) Option

WithRuntimeCatalog installs the immutable parent-scoped runtime catalog forwarded to every new and restored session.

func WithRuntimeRestoreResolver added in v0.27.0

func WithRuntimeRestoreResolver(resolver session.RuntimeRestoreResolver) Option

WithRuntimeRestoreResolver installs the composition-owned policy used only when exact durable runtime reconstruction fails. Omitting it preserves the fail-closed Harness default.

func WithSessionResourceStorage

func WithSessionResourceStorage(provider SessionResourceStorageProvider) Option

WithSessionResourceStorage installs the singleton durable-storage provider used by session-owned resources.

func WithSessionStore

func WithSessionStore(store *sessionstore.Store) Option

func WithSessionWorkspaces

func WithSessionWorkspaces(store *workspacestore.Store, baseDir string) Option

WithSessionWorkspaces declares per-session roots derived as baseDir/<sessionID>, isolated by construction (no root lease).

func WithSharedWorkspace

func WithSharedWorkspace(store *workspacestore.Store, root string) Option

WithSharedWorkspace declares one canonical fixed root shared with concurrent harness sessions, humans, and external tools — deliberately NO root lease; every checkpoint is stamped fuzzy.

func WithSnapshots

func WithSnapshots(policy SnapshotPolicy) Option

func WithToolResultCapture added in v0.31.0

func WithToolResultCapture(objects loop.ToolResultObjectStore, spillBase string) Option

WithToolResultCapture wires durable tool-result retention: the session object store each loop retains an oversized tool result into, and the ABSOLUTE base directory each session's local capture spill root is created under.

Both are required together. A store with no spill base would hold every capture's retained prefix in host memory up to the capture ceiling, which is exactly the residency a pooled host cannot budget; a spill base with no store would write local files nothing ever uploads. The base must be absolute because a relative one resolves against the process working directory, which is neither session-scoped nor stable for a pooled host, and because it must be comparable against the workspace region at Define time.

The base must ALREADY EXIST, be owner-writable only, and be a directory rather than a symlink, by the time a session is created — harness refuses to create it, because creating it would mean creating through whatever intermediate components the path has, and os.MkdirAll follows a symlinked one silently. Define checks only that the path is absolute, since the base may legitimately be created after the rig is defined; the session's own establishment is the authoritative check and a failure there ends the turn at the spill stage.

The base is a BASE, not a root: each session creates <base>/<sessionID>, owner-only, and removes it at shutdown. Define refuses a base that overlaps the configured workspace region in either direction, which is what keeps a capture spill out of every workspace checkpoint — a checkpoint archives the whole region, so the exclusion has to be a placement invariant rather than a filter some future snapshot path might not consult.

type PermissionReviewLimits

type PermissionReviewLimits struct {
	MaxConsecutiveNeedsHuman int
	MaxInvalidOrFailed       int
	MaxIdenticalSubjects     int
	MaxStaleResponses        int
	InterruptOnTrip          bool
	Session                  PermissionReviewSessionLimits
}

PermissionReviewLimits are the consumer-configurable bounded per-turn and per-session circuit-breaker thresholds (design §18) — the rig-level mirror of sessionruntime.PermissionReviewBreakerLimits. They are deliberately EXCLUDED from the rig fingerprint (fingerprint.go): these are operational tuning knobs, not behavioral identity, so two rigs that agree on classifiers and policy but differ only in these thresholds compare equal.

type PermissionReviewSessionLimits

type PermissionReviewSessionLimits struct {
	MaxConsecutiveNeedsHuman int
	MaxInvalidOrFailed       int
	MaxIdenticalSubjects     int
	MaxStaleResponses        int
}

PermissionReviewSessionLimits is PermissionReviewLimits' session-scoped counterpart (design §18: "per-turn AND per-session").

type PersistenceOverlapError

type PersistenceOverlapError struct {
	PersistencePath string
	Root            string
}

PersistenceOverlapError reports that a discoverable persistence path is equal to or beneath the managed workspace root, so appending a boundary or checkpoint event would mutate the very tree being captured. Persistence must live OUTSIDE the workspace. PersistencePath is the offending canonical path; Root is the canonical workspace root (or per-session base dir) it overlaps.

func (*PersistenceOverlapError) Error

func (e *PersistenceOverlapError) Error() string

type RestoreFailureOption added in v0.27.0

type RestoreFailureOption interface {
	// contains filtered or unexported methods
}

RestoreFailureOption is one declarative exception to Harness's fail-closed restore policy. Implementations are sealed so every option retains bounded, versioned semantics owned by Harness.

func AllowAdapterDrift added in v0.27.0

func AllowAdapterDrift() RestoreFailureOption

func AllowAgentKindDrift added in v0.27.0

func AllowAgentKindDrift() RestoreFailureOption

func AllowAgentNameDrift added in v0.27.0

func AllowAgentNameDrift() RestoreFailureOption

func AllowConfinementDrift added in v0.27.0

func AllowConfinementDrift() RestoreFailureOption

func AllowCredentialDrift added in v0.27.0

func AllowCredentialDrift() RestoreFailureOption

func AllowEffortDrift added in v0.27.0

func AllowEffortDrift() RestoreFailureOption

func AllowExternalCapabilityDrift added in v0.27.0

func AllowExternalCapabilityDrift() RestoreFailureOption

func AllowHookPolicyDrift added in v0.27.0

func AllowHookPolicyDrift() RestoreFailureOption

func AllowModelDrift added in v0.27.0

func AllowModelDrift() RestoreFailureOption

func AllowNativePermissionDrift added in v0.27.0

func AllowNativePermissionDrift() RestoreFailureOption

func AllowPermissionDrift added in v0.27.0

func AllowPermissionDrift() RestoreFailureOption

func AllowPermissionPostureDrift added in v0.27.0

func AllowPermissionPostureDrift() RestoreFailureOption

func AllowPermissionReviewDrift added in v0.27.0

func AllowPermissionReviewDrift() RestoreFailureOption

func AllowRuntimeCatalogDrift added in v0.27.0

func AllowRuntimeCatalogDrift() RestoreFailureOption

func AllowRuntimeProfileDrift added in v0.27.0

func AllowRuntimeProfileDrift() RestoreFailureOption

func AllowRuntimeSkillsDrift added in v0.27.0

func AllowRuntimeSkillsDrift() RestoreFailureOption

func AllowTrustDrift added in v0.27.0

func AllowTrustDrift() RestoreFailureOption

func AllowWorkspaceDrift added in v0.27.0

func AllowWorkspaceDrift() RestoreFailureOption

type Rig

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

Rig is an immutable design-time assembly that creates and restores sessions.

func Define

func Define(options ...Option) (*Rig, error)

func (*Rig) CaptureSafety added in v0.31.0

func (r *Rig) CaptureSafety() tool.CaptureSafetyDescriptor

CaptureSafety reports whether this rig's tools are safe to place where a tool result's residency matters: every high-output tool either streams its raw result into the capture sink, or the runtime's finite materialized maximum bounds the fallback.

It is a plain value naming only pkg/tool types, so a placement decision can be taken from it without importing anything that runs a loop — and Harness never imports the consumer that reads it. It is projected at Define time from the tool DEFINITIONS, not from bound tools, because a placement decision has to be takeable before a session exists.

func (*Rig) NewSession

func (r *Rig) NewSession(ctx context.Context, opts ...SessionOption) (session.SessionController, error)

NewSession brings up a brand-new live session. WithSeedSnapshot optionally materializes and commits a seed before any loop starts.

func (*Rig) RestoreSession

func (r *Rig) RestoreSession(ctx context.Context, id uuid.UUID) (session.SessionController, error)

type SessionOption

type SessionOption func(*sessionOptions) error

SessionOption configures a single Rig.NewSession call.

func WithSeedSnapshot

func WithSeedSnapshot(ref workspacestore.Ref) SessionOption

WithSeedSnapshot materializes ref into the new session's workspace before constructing loops and journals it as the first workspace checkpoint (design §"Seeding"). It is valid only for per-session roots and an EMPTY exclusive root, never for a shared root, and the ref must resolve in the configured workspace store — all enforced at NewSession, which fails closed on a bad seed.

func WithSessionID added in v0.33.0

func WithSessionID(id uuid.UUID) SessionOption

WithSessionID makes the new session adopt an externally-minted id instead of minting its own. NewSession then runs its whole per-session durable build under that id: the single-writer lease is acquired for it, the journal is bound to it and stamps its opening LeaseFence, and RestoreSession answers to it afterwards.

WHY A CALLER WOULD WANT THIS. An orchestrator that records the runtime session id in a durable record written BEFORE launch — a catalog binding that is immutable once created — cannot use an id the runtime mints during launch, because the record naming it is already written. The id has to be decidable in advance or the binding cannot name the session at all.

IT DOES NOT CREATE A NEW ORDERING PROBLEM. rig already mints the id first and builds the lease, journal and appenders from it before the session exists — the journal chicken-and-egg the internal sessionruntime.WithSessionID option resolves. This option only replaces that minting step with the caller's value, strictly earlier in the same order; nothing about the id is observable to the caller before NewSession returns that was not already.

THE CALLER OWNS UNIQUENESS, AND THE LEASE IS NOT A SUBSTITUTE FOR IT. Freshness is NOT verified. Passing the id of a session that already exists does not fail and does not create a second session: it re-opens THAT session's durable stream under a fresh grant and appends to it, so a stream can end up with two SessionStarted records and a restore afterwards replays both. The single-writer lease refuses only a LIVE holder, so it stops a concurrent second resident and says nothing at all about a session that has ended or been released. Mint the id with uuid.New (or another source with the same collision properties) and use it once.

A zero id is REFUSED rather than quietly replaced by a minted one, because a caller that reached here with the zero UUID believed it was naming a session and was not, and silently substituting a different id would put a name in its durable record that resolves to nothing. Supplying the option twice is refused for the same reason, including with the same id both times: two calls mean two beliefs about which id this is, and picking one of them is a guess.

It is REQUIRED for NewSession only. RestoreSession takes the id positionally and never consults this.

type SessionOptionError

type SessionOptionError struct {
	Kind SessionOptionErrorKind
}

SessionOptionError reports an invalid NewSession option.

func (*SessionOptionError) Error

func (e *SessionOptionError) Error() string

type SessionOptionErrorKind

type SessionOptionErrorKind string

SessionOptionErrorKind classifies a NewSession option failure.

const (
	// SessionOptionNil: a nil NewSession option was supplied.
	SessionOptionNil SessionOptionErrorKind = "nil_option"
	// SessionOptionDuplicateSeed: WithSeedSnapshot was supplied more than once.
	SessionOptionDuplicateSeed SessionOptionErrorKind = "duplicate_seed"
	// SessionOptionEmptySeed: WithSeedSnapshot was given an empty ref.
	SessionOptionEmptySeed SessionOptionErrorKind = "empty_seed"
	// SessionOptionDuplicateSessionID: WithSessionID was supplied more than once.
	SessionOptionDuplicateSessionID SessionOptionErrorKind = "duplicate_session_id"
	// SessionOptionZeroSessionID: WithSessionID was given the zero UUID.
	SessionOptionZeroSessionID SessionOptionErrorKind = "zero_session_id"
)

type SessionResourceStorage

type SessionResourceStorage struct {
	Path     string
	Identity string
}

SessionResourceStorage identifies the durable storage assigned to one session's shared resources. Path and Identity are opaque to Rig definition; the session lifecycle validates and consumes them when it resolves storage.

type SessionResourceStorageProvider

type SessionResourceStorageProvider interface {
	StorageForSession(context.Context, uuid.UUID) (SessionResourceStorage, error)
}

SessionResourceStorageProvider resolves durable storage for a session.

A provider is retained by the immutable Rig and may be called concurrently for multiple sessions, so implementations must be safe for concurrent use. Repeated calls for the same non-zero session ID, including after process restart, must resolve the same durable storage identity. The provider retains ownership of its own state; the harness receives only the returned value and does not mutate the provider.

type SnapshotPolicy

type SnapshotPolicy struct {
	Trigger  SnapshotTrigger
	Priority SnapshotPriority
	Timeout  time.Duration
}

type SnapshotPolicyError

type SnapshotPolicyError struct {
	Kind  SnapshotPolicyErrorKind
	Value int
}

func (*SnapshotPolicyError) Error

func (e *SnapshotPolicyError) Error() string

type SnapshotPolicyErrorKind

type SnapshotPolicyErrorKind string
const (
	SnapshotPolicyRequired         SnapshotPolicyErrorKind = "required"
	SnapshotPolicyWithoutWorkspace SnapshotPolicyErrorKind = "without_workspace"
	SnapshotPolicyInvalidTrigger   SnapshotPolicyErrorKind = "invalid_trigger"
	SnapshotPolicyInvalidPriority  SnapshotPolicyErrorKind = "invalid_priority"
	SnapshotPolicyInvalidTimeout   SnapshotPolicyErrorKind = "invalid_timeout"
	SnapshotPolicySharedRequired   SnapshotPolicyErrorKind = "shared_required"
)

type SnapshotPriority

type SnapshotPriority uint8
const (
	SnapshotBestEffort SnapshotPriority = iota
	SnapshotRequired
)

type SnapshotTrigger

type SnapshotTrigger uint8
const (
	SnapshotTriggerUnset SnapshotTrigger = iota
	SnapshotManual
	SnapshotOnIdle
	SnapshotOnTurnDone
	SnapshotOnStepDone
)

type WorkspacePlacementError

type WorkspacePlacementError struct {
	Kind  WorkspacePlacementErrorKind
	Name  string
	Cause error
}

WorkspacePlacementError reports an invalid workspace placement declaration at rig.Define.

func (*WorkspacePlacementError) Error

func (e *WorkspacePlacementError) Error() string

func (*WorkspacePlacementError) Unwrap

func (e *WorkspacePlacementError) Unwrap() error

type WorkspacePlacementErrorKind

type WorkspacePlacementErrorKind string

WorkspacePlacementErrorKind classifies a workspace placement validation failure.

const (
	// WorkspaceMultiplePlacements: more than one placement option was supplied. Exactly
	// one of WithExclusiveWorkspace / WithSessionWorkspaces / WithSharedWorkspace may be used.
	WorkspaceMultiplePlacements WorkspacePlacementErrorKind = "multiple_placements"
	// WorkspaceNilStore: a placement was given a nil workspace store.
	WorkspaceNilStore WorkspacePlacementErrorKind = "nil_store"
	// WorkspaceNilLeaser: an exclusive placement was given a nil root leaser.
	WorkspaceNilLeaser WorkspacePlacementErrorKind = "nil_leaser"
	// WorkspaceEmptyRoot: a placement root/base dir was empty or whitespace.
	WorkspaceEmptyRoot WorkspacePlacementErrorKind = "empty_root"
	// WorkspaceCanonicalizeFailed: a root/base dir could not be canonicalized.
	WorkspaceCanonicalizeFailed WorkspacePlacementErrorKind = "canonicalize_failed"
	// WorkspaceLeaseNameInvalid: the derived root lease name violated the storage grammar.
	WorkspaceLeaseNameInvalid WorkspacePlacementErrorKind = "lease_name_invalid"
	// WorkspaceToolWithoutPlacement: a workspace-requiring tool definition with no placement.
	WorkspaceToolWithoutPlacement WorkspacePlacementErrorKind = "workspace_tool_without_placement"
)

type WorkspaceRecoveryError

type WorkspaceRecoveryError = session.WorkspaceRecoveryError

type WorkspaceRootBusyError

type WorkspaceRootBusyError = session.WorkspaceRootBusyError

WorkspaceRootBusyError and WorkspaceRootLeaseLostError are re-exported at the rig composition surface while remaining canonical session lifecycle errors.

type WorkspaceRootLeaseLostError

type WorkspaceRootLeaseLostError = session.WorkspaceRootLeaseLostError

Jump to

Keyboard shortcuts

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