Documentation
¶
Overview ¶
Package envelope provides authenticated trusted control envelopes so a worker can accept a legitimate coordinator control message (for example a scope correction) without treating it as prompt injection.
Provider/repository text is untrusted free-form data and can never forge an envelope. Only a MAC-valid envelope bound to the active task, lease generation, and worker session is trusted. Receivers fail closed: spoofed, replayed, cross-task, or stale-generation envelopes are rejected or force an observable BLOCKED state rather than silently continuing the old scope.
FAC-133 / live incident: a FAC-146 worker rejected a valid orchestrator scope correction as "prompt injection" because content heuristics ran before provenance. Trust is the signature and binding fields, never body text that happens to mention injection, secrets, or shell commands.
Index ¶
- Constants
- Variables
- func DefaultAllowedIssuerRoles() map[string]struct{}
- func EqualScope(a, b *Scope) bool
- func FormatSig(sum []byte) string
- func LoadDurableSession(path string, cfg SessionConfig) (*Session, *DurableSessionState, error)
- func ParseUntrusted(raw []byte) (*Envelope, TrustClass, error)
- func SaveDurableSession(path string, sess *Session) error
- func SaveDurableSessionLocked(path string, sess *Session) error
- func SessionStatePath(root, workerSession, task string) string
- func Sign(secret []byte, e *Envelope) error
- func VerifyMAC(secret []byte, e *Envelope) bool
- func WithSessionFileLock(path string, fn func() error) error
- type Decision
- type DurableIssuerStore
- type DurableSessionState
- type Envelope
- type IssueOpts
- type Issuer
- type Kind
- type Scope
- type Session
- func (s *Session) ClearBlock()
- func (s *Session) CurrentScope() *Scope
- func (s *Session) LastSequence() uint64
- func (s *Session) Rebind(leaseGeneration int64) error
- func (s *Session) Receive(e *Envelope) (*Decision, error)
- func (s *Session) ReceiveJSON(raw []byte) (*Decision, error)
- func (s *Session) State() (SessionState, string)
- type SessionConfig
- type SessionState
- type Status
- type TrustClass
Constants ¶
const ( RoleOrchestrator = "orchestrator" RoleCoordinator = "coordinator" RoleAuditor = "auditor" )
Known issuer roles that may mint control envelopes under policy.
const DefaultMaxClockSkew = 2 * time.Minute
DefaultMaxClockSkew is how far IssuedAt/ExpiresAt may drift from local now.
const DefaultPolicyAuthority = "herd.control.v1"
DefaultPolicyAuthority is the control-plane policy that may issue trusted instructions. Receivers reject any other authority.
const DefaultTTL = 15 * time.Minute
DefaultTTL is how long a freshly issued envelope remains acceptable.
Variables ¶
var ( ErrMissingSecret = errors.New("envelope: secret is required (fail-closed)") ErrMissingBinding = errors.New("envelope: session binding incomplete (fail-closed)") ErrInvalidSignature = errors.New("envelope: invalid signature") ErrUnknownKind = errors.New("envelope: unknown control kind") ErrAuthorityMismatch = errors.New("envelope: policy authority mismatch") ErrTaskMismatch = errors.New("envelope: target task mismatch") ErrWorkerMismatch = errors.New("envelope: target worker session mismatch") ErrStaleGeneration = errors.New("envelope: stale lease generation") ErrReplay = errors.New("envelope: replay or non-monotonic sequence") ErrDuplicateID = errors.New("envelope: envelope id already seen") ErrDuplicateNonce = errors.New("envelope: nonce already seen") ErrExpired = errors.New("envelope: envelope expired or outside clock skew") ErrMissingFields = errors.New("envelope: required fields missing") ErrSessionBlocked = errors.New("envelope: session is BLOCKED; rebind required") ErrConflict = errors.New("envelope: conflicting control instruction") ErrNotControl = errors.New("envelope: input is not a trusted control envelope") ErrEmptyScope = errors.New("envelope: scope correction requires scope or body") )
Sentinel errors for typed fail-closed paths.
Functions ¶
func DefaultAllowedIssuerRoles ¶
func DefaultAllowedIssuerRoles() map[string]struct{}
DefaultAllowedIssuerRoles is the policy allowlist for control issuers.
func EqualScope ¶
EqualScope reports whether a and b describe the same scope.
func LoadDurableSession ¶
func LoadDurableSession(path string, cfg SessionConfig) (*Session, *DurableSessionState, error)
LoadDurableSession loads session state. Missing file → fresh session. Corrupt JSON or identity mismatch against cfg is fail-closed.
func ParseUntrusted ¶
func ParseUntrusted(raw []byte) (*Envelope, TrustClass, error)
ParseUntrusted attempts to decode provider/repository text that claims to be a control envelope. It never returns TrustControl: even well-formed JSON is untrusted until Session.Receive verifies the MAC under the worker secret. This is the explicit API for "card text that looks like a control message".
func SaveDurableSession ¶
SaveDurableSession persists session after apply/block with flock + fsync. When the caller already holds WithSessionFileLock, use SaveDurableSessionLocked.
func SaveDurableSessionLocked ¶
SaveDurableSessionLocked writes session state; caller must hold the session lock.
func SessionStatePath ¶
SessionStatePath returns durable path for a worker control session.
func Sign ¶
Sign attaches Signature using HMAC-SHA256 over Canonical(). Fail-closed on empty secret or incomplete envelope.
func VerifyMAC ¶
VerifyMAC reports whether e.Signature is a valid HMAC-SHA256 of Canonical() under secret. Empty secret, empty/malformed signature, or MAC mismatch all return false (fail-closed). Comparison is constant-time via hmac.Equal.
func WithSessionFileLock ¶
WithSessionFileLock holds an exclusive cross-process lock on the session state file for the duration of fn (load/apply/save transaction).
Types ¶
type Decision ¶
type Decision struct {
Status Status
Reason string
Trust TrustClass
EnvelopeID string
Sequence uint64
AppliedScope *Scope
SessionState SessionState
}
Decision is the structured result of receiving a candidate control message. Callers gate on Status; Reason is diagnostic only.
type DurableIssuerStore ¶
type DurableIssuerStore struct {
// contains filtered or unexported fields
}
DurableIssuerStore persists monotonic lastSeq per (worker,task) with a cross-process file lock, unique temp, and fsync (FAC-133 re-admission).
func NewDurableIssuerStore ¶
func NewDurableIssuerStore(path string) (*DurableIssuerStore, error)
NewDurableIssuerStore opens/creates a JSON state file under path.
type DurableSessionState ¶
type DurableSessionState struct {
WorkerSession string `json:"worker_session"`
Task string `json:"task"`
LeaseGeneration int64 `json:"lease_generation"`
LastSeq uint64 `json:"last_seq"`
LastAppliedID string `json:"last_applied_id"`
State SessionState `json:"state"`
BlockReason string `json:"block_reason,omitempty"`
SeenIDs map[string]bool `json:"seen_ids,omitempty"`
SeenNonces map[string]bool `json:"seen_nonces,omitempty"`
Scope *Scope `json:"scope,omitempty"`
ExpectedIssuer string `json:"expected_issuer_session,omitempty"`
}
DurableSessionState is persisted worker control state.
type Envelope ¶
type Envelope struct {
Version string `json:"v"`
ID string `json:"id"`
Kind Kind `json:"kind"`
Sequence uint64 `json:"seq"`
Nonce string `json:"nonce"`
IssuerRole string `json:"issuer_role"`
IssuerSession string `json:"issuer_session"`
PolicyAuthority string `json:"policy_authority"`
TargetTask string `json:"target_task"`
LeaseGeneration int64 `json:"lease_generation"`
TargetWorkerSession string `json:"target_worker_session"`
IssuedAtUnix int64 `json:"issued_at"`
ExpiresAtUnix int64 `json:"expires_at"`
// Body may contain text that resembles prompt injection. Authenticity is
// the MAC + binding fields, never a content heuristic over Body.
Body string `json:"body"`
Scope *Scope `json:"scope,omitempty"`
Signature string `json:"sig"`
}
Envelope is the authenticated control message. Signature is computed over Canonical() and is never part of the signed material itself.
func (*Envelope) Canonical ¶
Canonical returns the deterministic byte sequence that is MAC'd. Signature is excluded. Field order is fixed so neither JSON key reordering nor body splicing can preserve a captured MAC.
Format (newline-separated, no trailing newline after last field):
v1 id=... kind=... seq=... nonce=... issuer_role=... issuer_session=... policy_authority=... target_task=... lease_generation=... target_worker_session=... issued_at=... expires_at=... body_sha256=... scope=...
func (*Envelope) ValidateUnsigned ¶
ValidateUnsigned checks structural completeness before signing or verifying. It does not check the MAC.
type IssueOpts ¶
type IssueOpts struct {
Kind Kind
TargetTask string
LeaseGeneration int64
TargetWorkerSession string
Body string
Scope *Scope
// Sequence, when non-zero, overrides the issuer's auto-increment (tests).
Sequence uint64
// TTL bounds ExpiresAt; zero uses DefaultTTL.
TTL time.Duration
// ID and Nonce, when empty, are random 128-bit hex strings.
ID string
Nonce string
}
IssueOpts are the per-message fields a coordinator supplies when minting a control envelope. Sequence is assigned monotonically by the Issuer per (target worker session, target task) unless Sequence is non-zero (tests).
type Issuer ¶
type Issuer struct {
// contains filtered or unexported fields
}
Issuer mints MAC-signed control envelopes. It is the coordinator-side producer; workers never issue.
type Kind ¶
type Kind string
Kind identifies the control instruction family.
const ( // KindScopeCorrection narrows or reasserts the worker's exclusive package // allowlist / scope note without elevating merge or credential authority. KindScopeCorrection Kind = "scope.correction" )
type Scope ¶
type Scope struct {
PackageAllowlist []string `json:"package_allowlist,omitempty"`
Exclusive bool `json:"exclusive"`
Note string `json:"note,omitempty"`
}
Scope is the structured payload of a scope correction. Exclusive means the worker must not expand beyond PackageAllowlist (when non-empty).
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is the production consumer of control envelopes. A worker holds one Session for its active task/lease and feeds every candidate control message through Receive / ReceiveJSON. Free-form provider text is never elevated: only MAC-valid, binding-matched envelopes change scope.
On stale generation or conflicting control, Session transitions to StateBlocked and freezes the last applied scope until Rebind advances the lease generation (or an explicit ClearBlock after operator reconcile).
func NewSession ¶
func NewSession(cfg SessionConfig) (*Session, error)
NewSession constructs a fail-closed worker control session.
func (*Session) ClearBlock ¶
func (s *Session) ClearBlock()
ClearBlock is an operator reconcile path that unblocks without advancing generation. Prefer Rebind when the lease generation actually changed.
func (*Session) CurrentScope ¶
CurrentScope returns a copy of the last applied scope (nil if none).
func (*Session) LastSequence ¶
LastSequence returns the highest applied sequence.
func (*Session) Rebind ¶
Rebind advances the active lease generation (e.g. after reclaim) and clears BLOCKED so the worker can accept control for the new generation. Generation must strictly increase.
func (*Session) Receive ¶
Receive is the production consumer entry point: verify provenance and bindings, then apply a scope correction or fail closed. Body text that resembles prompt injection does NOT cause rejection when the MAC and bindings are valid — that is the FAC-133 incident fix.
func (*Session) ReceiveJSON ¶
ReceiveJSON unmarshals raw JSON then Receive. Malformed JSON is untrusted rejection, never control.
func (*Session) State ¶
func (s *Session) State() (SessionState, string)
State returns the current session state and block reason (if any).
type SessionConfig ¶
type SessionConfig struct {
Secret string
WorkerSession string
Task string
LeaseGeneration int64
PolicyAuthority string // empty → DefaultPolicyAuthority
MaxClockSkew time.Duration
AllowedIssuerRoles map[string]struct{}
// ExpectedIssuerSession, when set, must match envelope.IssuerSession.
ExpectedIssuerSession string
// Now overrides the clock (tests).
Now func() time.Time
}
SessionConfig binds a worker process to the control plane. All identity fields are required; construction fails closed without them.
type SessionState ¶
type SessionState string
SessionState is the durable control state a worker holds.
const ( // StateActive: worker may apply new verified control. StateActive SessionState = "active" // StateBlocked: control plane failed closed; scope is frozen until rebind. StateBlocked SessionState = "blocked" )
type Status ¶
type Status string
Status is the structured outcome of Session.Receive.
const ( // StatusApplied: envelope verified and applied (scope updated). StatusApplied Status = "applied" // StatusRejected: fail-closed refuse; session stays Active (or remains Blocked). StatusRejected Status = "rejected" // StatusBlocked: stale/conflicting control forces observable BLOCKED. StatusBlocked Status = "blocked" // StatusDuplicate: exact-id redelivery of an already-applied envelope. StatusDuplicate Status = "duplicate" )
type TrustClass ¶
type TrustClass string
TrustClass is how a receiver classifies input before (or after) verification.
const ( // TrustUntrusted is provider/repo/free-form text. It is never control. TrustUntrusted TrustClass = "untrusted" // TrustControl is a MAC-valid envelope bound to the active session. TrustControl TrustClass = "control" )
func Classify ¶
func Classify(_ string) TrustClass
Classify reports the trust class of free-form input without elevating it. Any non-empty provider/repo text is TrustUntrusted. Only a Session.Receive path that verifies a MAC can produce TrustControl.