SessionHold

Group agentprimitives.authzed.com · Scope Namespaced

SessionHold parks an AgentSession for forensic review and gates its return to running on a human decision.

Shaped after CredentialUpdateRequest deliberately: spec.sessionRef is 1:1, so its mapper is a pure name projection needing no List, and the owner-ref to the AgentSession makes the hold live exactly as long as the session it holds.

This CR's controller NEVER writes AgentSession.status.phase. Phase is owned by the AgentSession reconciler alone; co-ownership by two operator reconcilers is the hazard credentialupdate.go documents.

Spec

FieldTypeDescription
spec.reason *stringReason is platform-authored trip text shown on the release card. It is never agent-authored: on this card the agent is the SUBJECT of the decision, not the requester, so it gets no voice.
spec.sessionRef *objectSessionRef is the AgentSession this hold parks. The CR also carries an owner-ref to it, so the hold is garbage-collected with the session.
spec.sessionRef.name *stringName is the object's name.
spec.sessionRef.namespace *stringNamespace is the object's namespace.
spec.source *stringSource is "manual" or "tripper/<name>". It exists for the audit trail and the card's wording; nothing branches on it.
spec.trippedBystringTrippedBy is the canonical subject of the human who tripped it. Empty for an automated trip, which is identified by Source instead.
* required

Status

Status is controller-owned (observed state).

FieldTypeDescription
status.containmentstringContainment is platform-authored text explaining what happened to the SESSION as it was parked: that it is held, and whether the workspace snapshot that preserves the evidence succeeded. WRITTEN ONLY BY THE AGENTSESSION RECONCILER. It is a separate field from Determination because the two narratives are INDEPENDENT, not alternatives. A workspace snapshot can fail while a cascade to descendants also fails, and both are true at once. Sharing one field made each writer erase the other's report — so an operator debugging a doubly-degraded hold saw only whichever controller wrote last, in exactly the state where they need both. Two writers on one free-text status field also woke each other's watches on every pass; the loop was the symptom, the shared field was the disease. Splitting is necessary but not sufficient: each writer must ALSO skip the write when the text is unchanged, or the two keep waking each other through the same object even though neither is erasing anything.
status.determinationstringDetermination is platform-authored text explaining the current phase, surfaced rather than logged so a refusal is never silent. WRITTEN ONLY BY THE SESSIONHOLD CONTROLLER, which owns the hold's DISPOSITION: what was decided about it — released, refused, cascaded to descendants, or a cascade that failed. See Containment for the other half and why they are two fields.
status.interactionRefstringInteractionRef is the requestRef of the published release card.
status.observedGenerationinteger (int64)
status.phasestringPhase is Active or Released.
status.releasedBystringReleasedBy is the canonical subject of the human who approved the release card, set once by the SessionHold controller's Decide alongside Phase=Released. The AgentSession reconciler reads it back to attribute the lifecyclecore.Released event it emits when it observes this hold released.
status.snapshotHandlestringSnapshotHandle names the workspace snapshot taken at trip time, empty until the snapshot Job completes.
status.trippedAtstring (date-time)TrippedAt is when the controller first observed this hold. An OBSERVATION, set once: it must never live in spec, or a re-apply stops being a no-op.
* required