AgentSession

Group agentprimitives.authzed.com · Scope Namespaced · Short names agses

AgentSession is one running instance of an AgentClass: a conversation with its own runner Pod, budget, channel bindings and lifecycle phase. Sessions are disposable; the class they name is the durable definition.

Namespaced. Reconciled by pkg/controllers/agentsession, which gates on the class being Valid, provisions the per-session ServiceAccount, RBAC and Secrets, and owns the runner Pod. Status is written by more than one actor, so every writer goes through pkg/controllers/agentstatus rather than a bare status update.

Spec

FieldTypeDescription
spec.agentIdentitystringAgentIdentity overrides AgentClass.spec.agentIdentity; a per-bundle agentIdentity still wins over this.
spec.budgetobjectBudget optionally narrows the AgentClass budget. Each dimension must be <= the AgentClass cap.
spec.budget.maxDelegatedAgentsinteger (int32)MaxDelegatedAgents bounds the TOTAL number of sessions in one delegation tree, counting the root. It is a property of the tree rather than of any one session: every other dimension here caps a single session's spend, and a tree of N sessions each individually within budget still spends N times it. Resolved from the ROOT session's effective settings and enforced by the SubagentRequest controller before a child is created. Zero means UNSET, not unlimited. A consumer must apply its own built-in bound rather than reading zero as "no cap" — that reading is right for a DURATION (as MaxDuration uses it) and wrong for a COUNT, where it would license exactly the runaway fan-out this field exists to prevent. (min 0)
spec.budget.maxDuration *stringMaxDuration is the cumulative ACTIVE RUN-TIME budget (e.g. "30m") — the wall time the agent actually spent working, excluding time parked waiting on a human (the next user message or a tool-call approval). It persists across sleep/resume via status.runDuration; it is NOT wall-clock since session start. For a wall-clock lifetime cap, use SessionExpiration. Zero = no run-time cap.
spec.budget.maxTokens *integer (int64)(min 1)
spec.budget.maxTurns *integer (int32)(min 1)
spec.budget.sessionExpirationstringSessionExpiration is a hard wall-clock lifetime cap measured from status.startedAt (e.g. "24h"), regardless of how much the agent ran. The operator fails the session once exceeded, even while it is idle or asleep. Zero = never expires (no default). Distinct from MaxDuration, which counts only active run-time.
spec.class *stringClass names the AgentClass in the same namespace.
spec.forkedAtTurninteger (int32)ForkedAtTurn is the index of the last turn copied from ForkedFrom (inclusive). The new user text became turn ForkedAtTurn+1.
spec.forkedFromstringForkedFrom names the prior AgentSession (same namespace) this session was forked from via Restart-from-here. Immutable. Distinct from ChannelBinding.InheritFrom (set on auto-inherit after archive); ForkedFrom is set on explicit user-triggered restart.
spec.inputChannelobjectInputChannel records the channel-driven origin — the Channel whose listener (or scheduler, for bento) produced the inbound message that created this session. Set by channelsd at session creation; nil for kubectl-driven sessions.
spec.inputChannel.capabilities *[]stringCapabilities lists the format capabilities advertised by the kind (e.g., {"text","markdown"}). The runner consumes these to shape the respond_to_user tool's JSON schema.
spec.inputChannel.externalmap[string]stringExternal carries kind-specific opaque routing metadata that the kind's Sender consumes to render outbound (e.g., Slack channel_id, thread_ts). Treated as opaque by the rest of the system.
spec.inputChannel.inheritFromstringInheritFrom names the prior AgentSession (same namespace) whose memory channelsd copied into this session at creation time. Set only when this session was spawned because the prior session for the same Channel + key was archived (Succeeded) or Failed. Diagnostic / introspection only.
spec.inputChannel.key *stringKey is the channelKey computed by the kind impl (e.g., "thread:<channel_id>:<thread_ts>" or "dm:<user_id>"). Free-form; the SHA-256-hashed form is on the metadata.labels for selector lookup.
spec.inputChannel.kind *stringKind denormalizes Channel.spec.kind for fast filtering without a secondary lookup.
spec.inputChannel.name *stringName is the Channel CR name (same namespace as the AgentSession).
spec.inputChannel.natsSubjectPrefix *stringNATSSubjectPrefix is the full prefix shared by all of this session's NATS subjects (without trailing dot, e.g., "ap.session.default.foo"). Denormalized so the runner doesn't have to construct it.
spec.inputChannel.routingModestringRoutingMode controls which inbound messages reach the agent. "" (the default, and all a bot-originated thread ever uses) routes every message in the conversation. "mention_only" routes only @-mention events and is set when the bot was summoned into a pre-existing thread: the listener drops non-mention replies and the sender appends an "@-mention to reply" hint. (enum: | mention_only)
spec.outputChannelobjectOutputChannel records the Channel the session's outbound messages route through. Nil means "same as InputChannel" — the existing single-Channel-per-session case (Slack role=both).
spec.outputChannel.capabilities *[]stringCapabilities lists the format capabilities advertised by the kind (e.g., {"text","markdown"}). The runner consumes these to shape the respond_to_user tool's JSON schema.
spec.outputChannel.externalmap[string]stringExternal carries kind-specific opaque routing metadata that the kind's Sender consumes to render outbound (e.g., Slack channel_id, thread_ts). Treated as opaque by the rest of the system.
spec.outputChannel.inheritFromstringInheritFrom names the prior AgentSession (same namespace) whose memory channelsd copied into this session at creation time. Set only when this session was spawned because the prior session for the same Channel + key was archived (Succeeded) or Failed. Diagnostic / introspection only.
spec.outputChannel.key *stringKey is the channelKey computed by the kind impl (e.g., "thread:<channel_id>:<thread_ts>" or "dm:<user_id>"). Free-form; the SHA-256-hashed form is on the metadata.labels for selector lookup.
spec.outputChannel.kind *stringKind denormalizes Channel.spec.kind for fast filtering without a secondary lookup.
spec.outputChannel.name *stringName is the Channel CR name (same namespace as the AgentSession).
spec.outputChannel.natsSubjectPrefix *stringNATSSubjectPrefix is the full prefix shared by all of this session's NATS subjects (without trailing dot, e.g., "ap.session.default.foo"). Denormalized so the runner doesn't have to construct it.
spec.outputChannel.routingModestringRoutingMode controls which inbound messages reach the agent. "" (the default, and all a bot-originated thread ever uses) routes every message in the conversation. "mention_only" routes only @-mention events and is set when the bot was summoned into a pre-existing thread: the listener drops non-mention replies and the sender appends an "@-mention to reply" hint. (enum: | mention_only)
spec.parentobjectParent is the session that delegated this one. Set ONLY by the SubagentRequest controller; a child is never self-declared. Distinct from ForkedFrom: a fork REPLACES its parent and outlives it, so it gets fresh SpiceDB tuples. A delegated child is SUBORDINATE to a live parent, so its standing resolves through a live agentsession#parent arrow and dies when the parent's does. Immutable: re-parenting would silently move a running session's approval and transcript-read standing to a different set of humans.
spec.parent.name *stringName is the object's name.
spec.parent.namespace *stringNamespace is the object's namespace.
spec.prompt *objectPrompt is the initial user message. Immutable after the session transitions out of Pending.
spec.prompt.configMapRefobjectConfigMapKeyRef points at a single key inside a ConfigMap in the same namespace as the referencing CR.
spec.prompt.configMapRef.key *stringKey is the data key within the ConfigMap.
spec.prompt.configMapRef.name *stringName is the ConfigMap's name.
spec.prompt.inlinestring
* required

Status

Status is controller-owned (observed state).

FieldTypeDescription
status.activeWidgets[]objectActiveWidgets records the most recent MCP-UI interactive widgets persisted for this session, so a user opening the browser session-view page minutes after a widget was produced can still fetch and render it. Runner-owned; capped to the most recent entries to bound growth over a long session.
status.activeWidgets[].artifactID *stringArtifactID is the artifacts.Service head ID the widget's HTML was finalized under — the durable handle the browser session-view page fetches by.
status.activeWidgets[].originstringOrigin is the MCPServer CR name whose ui:// resource produced this widget ("mcpserver/<name>", the string tool.OriginTool.Origin returns). It is the widget's IDENTITY, and it is recorded here because this is where the browser can be told about it authoritatively: webd resolves a shell-supplied artifact id against this list and stamps the answer onto the app-tool call, so the runner can refuse a widget that reaches for a DIFFERENT server's tool. The app-tool registry is one flat map across every origin, so without this the tool name alone decided. Empty on a widget persisted before this field existed, which the pin reads as "unknown origin" and therefore leaves unpinned — the same posture those widgets already had.
status.activeWidgets[].rendererKindstringRendererKind is the channelassets.Renderer kind that rendered the widget — always "mcpui" today; carried explicitly so a future second widget-renderer kind doesn't need a status-shape change.
status.activeWidgets[].toolstringTool is the MCP tool name (tool.UIResourceSpec.Tool) that produced the widget.
status.agentWakeCreditintegerAgentWakeCredit is how many further AGENT-DRIVEN wakes this session will accept before a human speaks again. Refilled to the class's WakeBudget by every human turn, counted down by each agent-driven wake. CHANNELSD-OWNED, and that ownership is the control. The sessions it bounds are the ones talking to each other, so a budget either of them could write would be widened by the loop it exists to stop — the same reasoning that makes SubagentRequest's ExchangesRemaining controller-owned rather than mirrored from the child. Nil means no cross-agent wake has been considered yet. Zero means the budget is spent: further agent messages still APPEND — the session sees the whole conversation — but none of them wakes it until a person speaks.
status.appliedInteractPermissionstringAppliedInteractPermission is the SessionInteractPermission value captured from AgentClass at session creation. Frozen for the session's lifetime; admins can compare against the class's current spec to see drift.
status.appliedInteractPermissionAtstring (date-time)AppliedInteractPermissionAt is when the snapshot was captured.
status.auditChainHeadsmap[string]stringAuditChainHeads records each publisher's final chain head (publisher → "seq:lastHash"), stamped once the session reaches a terminal phase. It anchors tail-truncation detection for ended sessions; oap audit verify reads it.
status.auditKeyIDstringAuditKeyID is the key fingerprint (hex SHA-256 prefix) — matches the keyId in each signed entry's provenance.
status.auditPublicKeystringAuditPublicKey is the base64 (std) Ed25519 public key whose private half (per-session Secret key "audit-signing-key") signs this session's append-only audit entries. The K8s-witnessed trust anchor for offline verification (oap audit verify).
status.awaitingUserInputSincestring (date-time)AwaitingUserInputSince is set when the agent yields via await_user_message and cleared on resume or terminal. It is the durable floor under the at-most-once turn_activity yield event: a non-nil value counts as a visible wait, so the silence watchdog never warns while the agent is correctly waiting for a reply. A scalar rather than a condition, so it composes with the owner-partitioned condition merge in pkg/controllers/agentstatus.
status.bundleSessions[]objectBundleSessions is one entry per tool bundle the AgentSession reconciler provisioned a SpiceboxSession for. Empty when the class declares none.
status.bundleSessions[].agentIdentity *stringAgentIdentity is the identity resolved for this bundle's sandbox.
status.bundleSessions[].name *stringName is the bundle name from AgentClass.spec.toolBundles.
status.bundleSessions[].restartsinteger (int32)Restarts counts how many times the operator deleted + recreated this bundle's SpiceboxSession after it reported Failed. One retry is allowed per session; a second failure is terminal (BundleFailed).
status.bundleSessions[].retriedSessionUIDstringRetriedSessionUID is the UID of the failed SpiceboxSession instance the retry deleted. The terminal check ignores Failed observations carrying this UID: they are stale cache reads of the deleted instance, not the replacement failing.
status.bundleSessions[].spiceboxSessionName *stringSpiceboxSessionName is the sandbox session provisioned for the bundle.
status.closureDeniedbooleanClosureDenied reports that some member of this session's DELEGATION CLOSURE has had an authorization decision denied. The trifecta dispatch gate refuses in every mode when it is true — a denied closure is a structural fact ("this delegation has already been judged to have gone wrong"), not a policy judgement about the call in front of the gate. OPERATOR-DERIVED, and it has to be. Answering it means reading every session in the tree, and the runner's Role pins agentsessions to its own name with no list at all — a runner-side closure walk is Forbidden at the first hop in production while passing every test on an admin client. So the operator computes the closure fact and the runner reads it from the one object it is allowed to Get: its own. A denial ANYWHERE in the closure sets it, because that is what makes the signal useful: a parent that cannot get approval routes around the refusal by delegating to a different child, and a per-session flag would let it. Nil means not yet evaluated; false means evaluated and clean.
status.completionBypasses[]objectCompletionBypasses records every time this session declared its work complete while a requirement its AgentClass declared was unmet. An OBSERVATION the runner writes, never an applied field: it carries a wall-clock stamp and accumulates as the session runs, so it belongs in controller-owned status rather than anywhere a client server-side-applies. Append-only within a session — a later bypass never rewrites an earlier one, because "it happened twice" is the fact an operator most wants.
status.completionBypasses[].details[]stringDetails is the per-requirement account of what was missing, in the same order as Requirements.
status.completionBypasses[].reason *stringReason is the agent's stated justification. AGENT-AUTHORED and UNTRUSTED — a surface showing it to a person renders it inert.
status.completionBypasses[].requirements[]stringRequirements are the requirement keys that were unmet, so an operator can tell "the report never went out" from "the plan was left half-finished" without reading prose.
status.completionBypasses[].time *string (date-time)Time is when the bypass was recorded.
status.conditions[]objectConditions are the session's status conditions, partitioned by owner and merged through pkg/controllers/agentstatus.
status.conditions[].lastTransitionTime *string (date-time)lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
status.conditions[].message *stringmessage is a human readable message indicating details about the transition. This may be an empty string.
status.conditions[].observedGenerationinteger (int64)observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. (min 0)
status.conditions[].reason *stringreason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status.conditions[].status *stringstatus of the condition, one of True, False, Unknown. (enum: True | False | Unknown)
status.conditions[].type *stringtype of condition in CamelCase or in foo.example.com/CamelCase.
status.credentialAuthFailures[]objectCredentialAuthFailures records, per tool ORIGIN, that the platform ITSELF observed an auth-shaped failure there — the independent corroboration a CredentialUpdateRequest needs when the provider cannot be re-probed to confirm the agent's claim. Runner-owned (only the runner sees a tool call's upstream outcome); the operator reads and never writes it. An entry is REMOVED the moment a call to the same origin succeeds: a stale entry would corroborate a healthy credential forever and help talk a human into re-entering a working token.
status.credentialAuthFailures[].countinteger (int32)Count is how many consecutive auth-shaped failures the runner had seen at this origin when the observation was recorded. It is NOT kept live: later failures increment only the runner's in-memory counter, so this stays at its transition-time value until a success clears the entry and a fresh failure records a new one.
status.credentialAuthFailures[].observedAtstring (date-time)ObservedAt is when the runner recorded this observation.
status.credentialAuthFailures[].origin *stringOrigin is the failing tool's tool.OriginTool.Origin() value, e.g. "mcpserver/github" — the same vocabulary CredentialUpdateRequestSpec.Origin uses, so the reconciler can match an observation to a request without a second resolution step.
status.effectiveIdentityModestringEffectiveIdentityMode is the RESOLVED identity mode (agent|userPassthrough). For static modes it mirrors spec.identityMode. For ask|dynamic it is unset until the initiating user chooses, then set from the signed choice event. All runtime identity logic reads THIS, not spec.identityMode.
status.effectiveSettingsobjectEffectiveSettings is the resolved 4-tier settings snapshot (cluster → namespace → class → session). Stamped by the AgentSession reconciler before the runner pod is created; the runner reads it.
status.effectiveSettings.allowedMCP[]objectAllowedMCP is the effective MCPServer ceiling; nil is unconstrained.
status.effectiveSettings.allowedMCP[].name *stringName is the permitted MCPServer CR name.
status.effectiveSettings.allowedMCP[].tools[]stringTools narrows to specific tool names; nil, empty, or ["*"] allows all.
status.effectiveSettings.allowedSandboxKinds[]stringAllowedSandboxKinds is the effective ceiling across constraining tiers. Empty means unconstrained.
status.effectiveSettings.allowedSkills[]stringAllowedSkills is the union of allow-skill patterns across constraining tiers (nil = unconstrained). Informational; the gate is the resolver.
status.effectiveSettings.allowedToolkits[]stringAllowedToolkits is the effective toolkit ceiling; nil is unconstrained.
status.effectiveSettings.authzobjectAuthz is the resolved set of human-in-the-loop timeouts.
status.effectiveSettings.authz.approvalTimeoutstringApprovalTimeout is how long a decision may wait for a human before the gate's timeout policy fires.
status.effectiveSettings.authz.informationLeakageApprovalTTLstringInformationLeakageApprovalTTL is how long an approved share may be reused before the agent must ask again.
status.effectiveSettings.authz.metaagentobjectMetaagent is the resolved metaagent trigger: the class value, else the nearest tier default, else mention. Never nil after resolution — an absent setting resolves to the narrowest value rather than to a nil the reader has to interpret.
status.effectiveSettings.authz.metaagent.triggerstringTrigger is when the metaagent classifies a turn, and whether it acts. mention (default) — ambient off; explicit @metaagent only. shadow — classify every turn, apply nothing, record it. inline — classify every turn and act. (enum: mention | shadow | inline; default: mention)
status.effectiveSettings.authz.planGateobjectPlanGate is the resolved plan-gate config: the class value (or the nearest tier default) clamped up to the strictest MinPlanGateMode any tier declares. Always non-nil after resolution. This is the value the gate READS. Unlike every other authz setting — which the runtime reads straight off AgentClass.spec.authz — the plan gate must go through the resolved path, or a class could opt out past a cluster floor by being the only value anyone consults.
status.effectiveSettings.authz.planGate.examples[]objectExamples are worked plans this class's author supplies, rendered after the one the runtime derives from the session's own permission surface. Optional, and most classes want none: the derived example already speaks the agent's own handles and shows the shape that costs one approval. These are for a class whose GOOD plan is not obvious from its surface alone — where the order of phases matters, or where two resource types must be named together, or where the natural split is by audience rather than by read-then-write. They are TRUSTED text, at the same level as spec.systemPrompt: an operator authors them, never the agent. But unlike the system prompt they are checked — every handle an example names must exist on this class's own surface, or the class goes Valid=False naming the offender. The prompt tells agents that a handle off the declarable list is silently dropped, and an EXAMPLE carrying such a handle would teach exactly that failure to every plan the agent writes. Measured, planners transcribe these examples closely, so a wrong one is not inert.
status.effectiveSettings.authz.planGate.examples[].phases *[]objectPhases are the plan itself. Rendered as the JSON an agent would pass to update_plan, because that is the form it copies.
status.effectiveSettings.authz.planGate.examples[].phases[].id *stringID is the phase's identifier, as update_plan takes it.
status.effectiveSettings.authz.planGate.examples[].phases[].labelstringLabel is the human phrase for the phase.
status.effectiveSettings.authz.planGate.examples[].phases[].permissions[]stringPermissions are the wire-format handles this phase declares ("perm:<permission>:<resourceType>" or "tool:<name>"). Each is checked against the class's surface at admission.
status.effectiveSettings.authz.planGate.examples[].phases[].slots[]stringSlots are the resource TYPES this phase names an instance of. Types only: an authored example must not carry a concrete instance id, because a planner that copies one would address an object belonging to whoever the example was written about.
status.effectiveSettings.authz.planGate.examples[].task *stringTask is the one-line request this plan answers — the "when you are asked to X" half. Without it an example shows a shape with no occasion, and the agent cannot tell which of several examples applies.
status.effectiveSettings.authz.planGate.limitsobjectLimits bound plan size. The defaults are deliberately expansive: they exist to stop a runaway or hostile plan from DoS-ing the approver and producing an unrenderable card, NOT to shape normal authoring.
status.effectiveSettings.authz.planGate.limits.maxPermissionsPerPhaseinteger (int32)MaxPermissionsPerPhase is naturally bounded by the permission surface (a phase cannot declare a handle that does not exist), so this is a backstop rather than a real constraint. (default: 64)
status.effectiveSettings.authz.planGate.limits.maxPhasesinteger (int32)(default: 50)
status.effectiveSettings.authz.planGate.limits.maxSlotsPerPhaseinteger (int32)MaxSlotsPerPhase bounds the instance axis, which is genuinely unbounded (a thread can carry many URLs), hence the larger default. (default: 128)
status.effectiveSettings.authz.planGate.modestringMode selects how much of the gate runs. disabled — off entirely; no prompt change, no plan schema surfaced. logging — the gate runs in full: enumeration, ceiling computation, approver resolution, severity, card rendering and the append-only write. What it holds back is CEILINGS and APPROVALS — the card is logged instead of published, so nothing is approved and no grant is written, and a call that overruns a declared phase is RECORDED rather than refused. A call made with NO plan at all is still refused (see RequirePlan). That is not an exception to the mode so much as its precondition: without a declared plan there are no ceilings to record and the mode measures nothing. enforcing — the card is published and the call blocks on the outcome. (enum: disabled | logging | enforcing; default: disabled)
status.effectiveSettings.authz.planGate.renderingobjectRendering bounds what a single approval card shows or auto-approves.
status.effectiveSettings.authz.planGate.rendering.maxAutoApproveHandlesinteger (int32)MaxAutoApproveHandles bounds the UNION of all handles auto-approved without a human across a session — the tier-0 budget. (default: 8)
status.effectiveSettings.authz.planGate.rendering.maxNamedApproversinteger (int32)MaxNamedApprovers is the largest approver population rendered by name before falling back to a count or a channel-supplied description. (default: 5)
status.effectiveSettings.authz.planGate.rendering.maxSingleCardHandlesinteger (int32)MaxSingleCardHandles bounds a phase ceiling shown on one card; a phase exceeding it is split rather than folded behind "+N more", so the approver sees what they approve. (default: 16)
status.effectiveSettings.authz.planGate.requirePlanbooleanRequirePlan denies every permissioned call made before a plan is declared. Without it, an agent that simply never calls update_plan falls through the implicit-phase rule and is unconstrained — i.e. "don't plan" is a total bypass. It bites in LOGGING mode too, which is the one place this differs from everything else the gate does. "You must declare a plan" and "you must stay within its ceiling" are separable claims; only the second is what logging mode holds back. Welding them together made logging unable to produce the dataset it exists for — agents told they MUST plan simply did not, because in logging nothing depended on it, and no wording fixes a consequence the code does not implement. So: with RequirePlan, a session under logging still refuses calls made with no plan at all, and still records-but-permits calls that overrun a declared ceiling. That yields real phases, ceilings, severity and approver data without switching on the enforcement the data is meant to justify. UNSET derives from Mode: required whenever the gate runs at all. Turning the gate on IS the intent to make agents plan, and leaving this an independent opt-in defaulting false meant a cluster could run mode: logging, look gated, and collect nothing — which is what happened across five live sessions, every call landing on the implicit phase because nothing required a plan. Explicit false remains an opt-out, because gate-on-but-observe-only is a real rollout stage: it records which handles calls actually resolve to without changing what the agent may do. Explicit true under mode: disabled is still false — a gate that does not run cannot refuse anything, and honouring it would promise a denial that never arrives. Pointer rather than a kubebuilder default: a static default cannot express "depends on another field", and it would erase the difference between unset and a deliberate false. UPGRADE NOTE. This field previously carried +kubebuilder:default=false, so every AgentClass created under the older CRD has an explicit false PERSISTED — which now reads as the observe-only opt-out rather than as unset. Such a class keeps the old behaviour after an upgrade and will not require a plan. Re-applying the class (or deleting the field) is what restores derivation; there is no way to distinguish a stamped default from a deliberate choice after the fact.
status.effectiveSettings.authz.scopeMaxLlmLatencyMsinteger (int32)ScopeMaxLLMLatencyMs bounds the cold-start scope extractor's LLM call, in milliseconds. A latency budget, not a window in which authority is held.
status.effectiveSettings.budgetobjectBudget is the resolved per-session budget, clamped to every tier ceiling.
status.effectiveSettings.budget.maxDelegatedAgentsinteger (int32)MaxDelegatedAgents bounds the TOTAL number of sessions in one delegation tree, counting the root. It is a property of the tree rather than of any one session: every other dimension here caps a single session's spend, and a tree of N sessions each individually within budget still spends N times it. Resolved from the ROOT session's effective settings and enforced by the SubagentRequest controller before a child is created. Zero means UNSET, not unlimited. A consumer must apply its own built-in bound rather than reading zero as "no cap" — that reading is right for a DURATION (as MaxDuration uses it) and wrong for a COUNT, where it would license exactly the runaway fan-out this field exists to prevent. (min 0)
status.effectiveSettings.budget.maxDuration *stringMaxDuration is the cumulative ACTIVE RUN-TIME budget (e.g. "30m") — the wall time the agent actually spent working, excluding time parked waiting on a human (the next user message or a tool-call approval). It persists across sleep/resume via status.runDuration; it is NOT wall-clock since session start. For a wall-clock lifetime cap, use SessionExpiration. Zero = no run-time cap.
status.effectiveSettings.budget.maxTokens *integer (int64)(min 1)
status.effectiveSettings.budget.maxTurns *integer (int32)(min 1)
status.effectiveSettings.budget.sessionExpirationstringSessionExpiration is a hard wall-clock lifetime cap measured from status.startedAt (e.g. "24h"), regardless of how much the agent ran. The operator fails the session once exceeded, even while it is idle or asleep. Zero = never expires (no default). Distinct from MaxDuration, which counts only active run-time.
status.effectiveSettings.contentInspectors[]objectContentInspectors is the resolved union of content-guard inspectors across tiers (ceiling: lower tiers add, never weaken). Empty ⇒ no inspection.
status.effectiveSettings.contentInspectors[].config *object (free-form)Config is inspector-specific configuration, validated by the inspector's Configure at admission (fail-closed) and at session start.
status.effectiveSettings.contentInspectors[].id *stringID must resolve in the contentguard registry (e.g. "url-allowlist").
status.effectiveSettings.deniedSkills[]stringDeniedSkills is the union of deny-skill patterns across tiers.
status.effectiveSettings.modelobjectModel is the resolved model the session runs against.
status.effectiveSettings.model.apiKeyobjectAPIKey is a bring-your-own token. Only honored when allowModelOverride is granted (else a fatal violation). Catalog references leave this empty.
status.effectiveSettings.model.apiKey.key *stringKey is the data key within the Secret holding the value.
status.effectiveSettings.model.apiKey.name *stringName is the Secret's name, in the referring object's own namespace.
status.effectiveSettings.model.fromCatalogstringFromCatalog references a model catalog entry by name. Mutually exclusive with an inline Provider/Name. When set, Provider and the token come from the catalog entry.
status.effectiveSettings.model.namestring
status.effectiveSettings.model.providerstring(enum: anthropic | openai | openrouter | test)
status.effectiveSettings.model.routingMetadataobjectRoutingMetadata refines dynamic model selection for this agent. Merged over the resolved catalog entry's routing with narrowing-only semantics: an AgentClass may constrain, never loosen, admin policy. Inert unless the resolved model performs dynamic selection (Provider == "openrouter").
status.effectiveSettings.model.routingMetadata.allowFallbacksboolean
status.effectiveSettings.model.routingMetadata.allowedModels[]stringAuto-router hints — apply when the model is "openrouter/auto".
status.effectiveSettings.model.routingMetadata.costQualityTradeoffnumberCostQualityTradeoff is intentionally unvalidated (no Minimum/Maximum marker): OpenRouter documents no formal range for this value — its docs example value is 3 — so a range marker would reject valid input.
status.effectiveSettings.model.routingMetadata.dataCollectionstring(enum: allow | deny)
status.effectiveSettings.model.routingMetadata.ignore[]string
status.effectiveSettings.model.routingMetadata.maxPriceobjectOpenRouterMaxPrice caps per-model price, in USD per MILLION tokens (OpenRouter's max_price units; e.g. Prompt: 1 means at most $1/M prompt tokens). 0 on an axis means "no cap on this axis" (see pkg/platform/settings/routingmerge.go's tightenPrice and pkg/agent/llm/openrouter/extra.go's maxPriceObject), not "cap at $0". Only consulted when the owning ModelCatalogEntry's Provider == "openrouter".
status.effectiveSettings.model.routingMetadata.maxPrice.completionnumber(min 0)
status.effectiveSettings.model.routingMetadata.maxPrice.promptnumber(min 0)
status.effectiveSettings.model.routingMetadata.models[]string
status.effectiveSettings.model.routingMetadata.only[]string
status.effectiveSettings.model.routingMetadata.order[]string
status.effectiveSettings.model.routingMetadata.requireParametersbooleanRequireParameters: unset defaults to TRUE when the request carries tools (the openrouter provider injects it — see pkg/agent/llm/openrouter/extra.go's providerObject); explicit values are honored verbatim. OpenRouter's own wire default is false — agents need tools, so auto/fallback routing must only pick providers that support the request's parameters.
status.effectiveSettings.model.routingMetadata.sortstring(enum: price | throughput | latency)
status.effectiveSettings.modelInputPerMToknumberModelInputPerMTok/ModelOutputPerMTok are the resolved catalog price for the chosen model (USD/MTok), 0 when the catalog carries no price. The runner cost estimator prefers these over the built-in table so its figure matches the admin dashboard (which is catalog-authoritative).
status.effectiveSettings.modelOutputPerMToknumberModelOutputPerMTok is the output half of the same catalog price.
status.effectiveSettings.modelRoutingobjectModelRouting is the effective OpenRouter dynamic-routing preference: the catalog entry's, with the AgentClass's routingMetadata merged over it. Only set when Model.Provider == "openrouter".
status.effectiveSettings.modelRouting.allowFallbacksboolean
status.effectiveSettings.modelRouting.allowedModels[]stringAuto-router hints — apply when the model is "openrouter/auto".
status.effectiveSettings.modelRouting.costQualityTradeoffnumberCostQualityTradeoff is intentionally unvalidated (no Minimum/Maximum marker): OpenRouter documents no formal range for this value — its docs example value is 3 — so a range marker would reject valid input.
status.effectiveSettings.modelRouting.dataCollectionstring(enum: allow | deny)
status.effectiveSettings.modelRouting.ignore[]string
status.effectiveSettings.modelRouting.maxPriceobjectOpenRouterMaxPrice caps per-model price, in USD per MILLION tokens (OpenRouter's max_price units; e.g. Prompt: 1 means at most $1/M prompt tokens). 0 on an axis means "no cap on this axis" (see pkg/platform/settings/routingmerge.go's tightenPrice and pkg/agent/llm/openrouter/extra.go's maxPriceObject), not "cap at $0". Only consulted when the owning ModelCatalogEntry's Provider == "openrouter".
status.effectiveSettings.modelRouting.maxPrice.completionnumber(min 0)
status.effectiveSettings.modelRouting.maxPrice.promptnumber(min 0)
status.effectiveSettings.modelRouting.models[]string
status.effectiveSettings.modelRouting.only[]string
status.effectiveSettings.modelRouting.order[]string
status.effectiveSettings.modelRouting.requireParametersbooleanRequireParameters: unset defaults to TRUE when the request carries tools (the openrouter provider injects it — see pkg/agent/llm/openrouter/extra.go's providerObject); explicit values are honored verbatim. OpenRouter's own wire default is false — agents need tools, so auto/fallback routing must only pick providers that support the request's parameters.
status.effectiveSettings.modelRouting.sortstring(enum: price | throughput | latency)
status.effectiveSettings.modelTokenSourceobjectModelTokenSource is the resolved central token location (system namespace) when the effective model came from the catalog. nil for bring-your-own / legacy. The operator materializes this into the per-session Secret.
status.effectiveSettings.modelTokenSource.key *stringKey is the data key within the Secret holding the value.
status.effectiveSettings.modelTokenSource.name *stringName is the Secret's name.
status.effectiveSettings.modelTokenSource.namespace *stringNamespace is the Secret's namespace.
status.effectiveSettings.nativeFileHandling *booleanNativeFileHandling is the resolved Tier-2 provider-native file handling grant (security-sensitive; default false). See SettingsLimits.NativeFileHandling.
status.effectiveSettings.pinningobjectPinning preserves the per-tier pinning policies. Per-item evaluation happens via settings.PinRequirementFor — bypasses are tier-scoped, so the policies cannot be pre-folded.
status.effectiveSettings.pinning.clusterobjectCluster is the cluster-tier policy, kept un-folded.
status.effectiveSettings.pinning.cluster.bypass[]objectBypass exempts named dependencies from this tier's rules and lower.
status.effectiveSettings.pinning.cluster.bypass[].kind *stringKind is the pinning registry kind this exemption applies to.
status.effectiveSettings.pinning.cluster.bypass[].name *stringName identifies the exempted item: a canonical skill name, MCPServer name, SidecarToolbox name, or SpiceboxToolkit name. Trailing-wildcard patterns are allowed (same syntax as AllowedSkills).
status.effectiveSettings.pinning.cluster.bypass[].reason *stringReason is the required audit-trail justification for the exemption.
status.effectiveSettings.pinning.cluster.rules[]objectRules is at most one requirement per dependency kind.
status.effectiveSettings.pinning.cluster.rules[].kind *stringKind is the pinning registry kind name this rule governs.
status.effectiveSettings.pinning.cluster.rules[].minStrengthstringMinStrength is the minimum pin strength a declared ref must have. Empty = no floor (drift observation still applies). (enum: frozen | named)
status.effectiveSettings.pinning.cluster.rules[].modestringMode governs what a violation or drift does: block (fail closed), approve (force per-call user approval), warn (surface warnings only), off (observe only). Empty = approve. (enum: block | approve | warn | off)
status.effectiveSettings.pinning.namespaceobjectNamespace is the namespace-tier policy, kept un-folded.
status.effectiveSettings.pinning.namespace.bypass[]objectBypass exempts named dependencies from this tier's rules and lower.
status.effectiveSettings.pinning.namespace.bypass[].kind *stringKind is the pinning registry kind this exemption applies to.
status.effectiveSettings.pinning.namespace.bypass[].name *stringName identifies the exempted item: a canonical skill name, MCPServer name, SidecarToolbox name, or SpiceboxToolkit name. Trailing-wildcard patterns are allowed (same syntax as AllowedSkills).
status.effectiveSettings.pinning.namespace.bypass[].reason *stringReason is the required audit-trail justification for the exemption.
status.effectiveSettings.pinning.namespace.rules[]objectRules is at most one requirement per dependency kind.
status.effectiveSettings.pinning.namespace.rules[].kind *stringKind is the pinning registry kind name this rule governs.
status.effectiveSettings.pinning.namespace.rules[].minStrengthstringMinStrength is the minimum pin strength a declared ref must have. Empty = no floor (drift observation still applies). (enum: frozen | named)
status.effectiveSettings.pinning.namespace.rules[].modestringMode governs what a violation or drift does: block (fail closed), approve (force per-call user approval), warn (surface warnings only), off (observe only). Empty = approve. (enum: block | approve | warn | off)
status.effectiveSettings.provenancemap[string]stringProvenance maps a resolved field to the tier that supplied it (cluster|namespace|class|session|clamped).
status.effectiveSettings.reportSessionCost *booleanReportSessionCost is the resolved end-of-session cost-estimate toggle.
status.effectiveSettings.requireStandingFor[]stringRequireStandingFor is the union of every tier's RequireStandingFor, sorted and deduplicated. A resource type named here resolves to required standing regardless of what its schema fragment declares.
status.effectiveSettings.requireSubagentDigestPins *booleanRequireSubagentDigestPins is the resolved delegation requirement (default false): when true, the SubagentRequest controller refuses delegation through any unpinned roster entry. See SettingsLimits.RequireSubagentDigestPins.
status.effectiveSettings.sandboxmap[string]objectSandbox is the resolved sandbox backend per tool-bundle name. Recorded so oap can show which backend a session landed on, and Provenance says which tier chose it.
status.effectiveSettings.toolGuardobjectToolGuard preserves the per-tier tool-guard policies + folded ceiling. Per-tool rule resolution happens in the runner at session start.
status.effectiveSettings.toolGuard.ceilingobjectCeiling is the strictest-across-tiers bound, pre-folded.
status.effectiveSettings.toolGuard.ceiling.maxCallsinteger (int32)MaxCalls caps calls in the sliding Window; both must be set together. (min 1)
status.effectiveSettings.toolGuard.ceiling.maxCallsPerTurninteger (int32)MaxCallsPerTurn / MaxCalls+Window impose rate ceilings even when no lower-tier rule configures a rate limit. (min 1)
status.effectiveSettings.toolGuard.ceiling.maxEgressBytesinteger (int64)MaxEgressBytes / MaxIngressBytes impose per-call byte ceilings even when no lower-tier rule configures a data limit (an unset limit is "unlimited", so the ceiling wins). Folded strictest-across-tiers (min). (min 1)
status.effectiveSettings.toolGuard.ceiling.maxFailureThresholdinteger (int32)MaxFailureThreshold caps the effective breaker threshold (min wins). Note: the per-origin threshold (BreakerSpec.OriginFailureThreshold) deliberately has no ceiling yet. (min 1)
status.effectiveSettings.toolGuard.ceiling.maxIngressBytesinteger (int64)MaxIngressBytes is the inbound half of the same per-call byte ceiling. (min 1)
status.effectiveSettings.toolGuard.ceiling.maxUIIngressBytesinteger (int64)MaxUIIngressBytes imposes a ceiling on the UI data-binding ingress path (toolguard.DefaultUIIngressBytes when neither a rule nor this ceiling sets one — this path is never unlimited). Folded strictest-across-tiers (min), independent of MaxIngressBytes. (min 1)
status.effectiveSettings.toolGuard.ceiling.minActionstringMinAction is a severity floor (off < warn < deny < halt). Setting "deny" makes the breaker non-disableable below this tier. (enum: warn | deny | halt)
status.effectiveSettings.toolGuard.ceiling.minInitialCoolOffstringMinInitialCoolOff raises the effective initial cool-off (max wins). Note: MaxCoolOff deliberately has no floor yet.
status.effectiveSettings.toolGuard.ceiling.rateBounds[]objectRateBounds carries sliding-window bounds BESIDE MaxCalls/Window. Every bound binds: a call must fit under all of them, and a lower tier can only add bounds, never trade one away. A ceiling is a conjunction, not a choice. Two tiers naming different windows have written bounds neither of which implies the other — a cluster {5 calls, 1s} permits 432,000/day, a namespace {10 calls, 24h} permits all 10 inside one second — so collapsing them by calls/second discards a bound its author wrote, on the very surface this type calls the hard bound lower tiers cannot escape. The settings fold writes it: the strictest-by-rate pair leads in MaxCalls/Window, so a reader that knows only the pair still sees a real bound, and every other distinct window lands here, deduped strictest-per-window and window-ordered so the object is stable to re-stamp onto status. Authoring it directly is how one tier expresses burst-plus-sustained on its own. It deliberately carries no MaxItems: one schema governs both the AUTHORED surface and the FOLDED one stamped onto status, so a cap of N would also bind a fold that unions two tiers of N+1 windows and can legitimately produce 2N+1 — the status write rejected by the very bound meant to keep it small, wedging the reconcile. Enforcement stays cheap instead because the per-call sweep is one pass over call history per bound, and the size of that history is set by maxCalls over the longest window, not by how many horizons are named.
status.effectiveSettings.toolGuard.ceiling.rateBounds[].maxCalls *integer (int32)MaxCalls is the cap on calls inside Window. (min 1)
status.effectiveSettings.toolGuard.ceiling.rateBounds[].window *stringWindow is the sliding span MaxCalls is counted over. A non-positive duration enforces nothing; the resolver reports one rather than folding it in (pkg/platform/settings, ToolGuardRateBoundUnenforceable).
status.effectiveSettings.toolGuard.ceiling.windowstringWindow is the sliding-window span for MaxCalls; both must be set together.
status.effectiveSettings.toolGuard.clusterobjectCluster is the cluster-tier policy, kept un-folded for the rule walk.
status.effectiveSettings.toolGuard.cluster.rules[]objectRules are evaluated in order, first match wins; empty means this tier contributes nothing and the walk falls through.
status.effectiveSettings.toolGuard.cluster.rules[].breakerobjectBreaker configures the circuit breaker; nil means matched tools get none.
status.effectiveSettings.toolGuard.cluster.rules[].breaker.actionstringAction when the breaker denies: halt ends the session; deny returns an IsError tool_result; warn logs/audits but allows; off disables the breaker for matched tools. (enum: halt | deny | warn | off)
status.effectiveSettings.toolGuard.cluster.rules[].breaker.failureThresholdinteger (int32)FailureThreshold is consecutive Execute failures (per tool) that open the breaker. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].breaker.initialCoolOffstringInitialCoolOff is the first cool-off period after the breaker opens; it doubles on each successive trip up to MaxCoolOff. Default 30s.
status.effectiveSettings.toolGuard.cluster.rules[].breaker.maxCoolOffstringMaxCoolOff caps the exponential-backoff cool-off. Default 10m.
status.effectiveSettings.toolGuard.cluster.rules[].breaker.originFailureThresholdinteger (int32)OriginFailureThreshold is consecutive failures across ALL tools of the tool's origin that open the origin breaker (denying every sibling). (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].dataLimitobjectDataLimit caps per-call byte volume; nil means no byte cap.
status.effectiveSettings.toolGuard.cluster.rules[].dataLimit.actionstringAction when a byte limit is exceeded: halt ends the session; deny errors the call (egress: tool not run; ingress: result withheld); warn logs/audits but allows. (enum: halt | deny | warn)
status.effectiveSettings.toolGuard.cluster.rules[].dataLimit.maxEgressBytesinteger (int64)MaxEgressBytes caps the serialized tool-args size sent outbound per call. Exceeding it (at PreToolCall) applies Action; on deny the tool does not run. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].dataLimit.maxIngressBytesinteger (int64)MaxIngressBytes caps the tool-result size returned inbound per call (any result, success or error). Exceeding it (at PostToolCall) applies Action; on deny the result is withheld (replaced with an IsError) so the oversized payload never reaches the model. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].dataLimit.maxUIIngressBytesinteger (int64)MaxUIIngressBytes caps result bytes on an agent-UI DATA BINDING, whose result is rendered by a browser and never read by the model. Unset means the platform's browser-sized default applies (toolguard's DefaultUIIngressBytes) — NOT unlimited, and NOT MaxIngressBytes. Set this to bind the UI path tighter or looser than the platform default; tightening maxIngressBytes alone does not affect it. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].match *objectMatch selects the tools this rule governs.
status.effectiveSettings.toolGuard.cluster.rules[].match.kindstringKind is the runtime tool kind. Sidecar-toolbox tools report "mcp" (they are synthesized through the MCP synthesizer); select them via Origin "sidecartoolbox/*". (enum: sandbox | mcp | meta)
status.effectiveSettings.toolGuard.cluster.rules[].match.originstringOrigin is a glob over the tool's origin in "<kind>/<name>" form, e.g. "mcpserver/github" or "sidecartoolbox/". Note: a bare "" also matches origin-less tools (empty origin); use a prefixed glob like "mcpserver/*" to scope to tools that have an origin.
status.effectiveSettings.toolGuard.cluster.rules[].match.toolstringTool is a glob over the LLM-visible tool name (e.g. "github_*").
status.effectiveSettings.toolGuard.cluster.rules[].rateLimitobjectRateLimit caps call volume; nil means matched tools get no rate limit.
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.actionstringAction when a rate cap is hit: halt ends the session; deny errors the call; warn logs and audits but allows it. (enum: halt | deny | warn)
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.maxCallsinteger (int32)MaxCalls caps calls in the sliding Window; both must be set together. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.maxCallsPerTurninteger (int32)MaxCallsPerTurn caps calls within a single agent turn; 0 is unlimited. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.windowstringWindow is the sliding-window span for MaxCalls; both must be set together.
status.effectiveSettings.toolGuard.namespaceobjectNamespace is the namespace-tier policy, kept un-folded for the rule walk.
status.effectiveSettings.toolGuard.namespace.rules[]objectRules are evaluated in order, first match wins; empty means this tier contributes nothing and the walk falls through.
status.effectiveSettings.toolGuard.namespace.rules[].breakerobjectBreaker configures the circuit breaker; nil means matched tools get none.
status.effectiveSettings.toolGuard.namespace.rules[].breaker.actionstringAction when the breaker denies: halt ends the session; deny returns an IsError tool_result; warn logs/audits but allows; off disables the breaker for matched tools. (enum: halt | deny | warn | off)
status.effectiveSettings.toolGuard.namespace.rules[].breaker.failureThresholdinteger (int32)FailureThreshold is consecutive Execute failures (per tool) that open the breaker. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].breaker.initialCoolOffstringInitialCoolOff is the first cool-off period after the breaker opens; it doubles on each successive trip up to MaxCoolOff. Default 30s.
status.effectiveSettings.toolGuard.namespace.rules[].breaker.maxCoolOffstringMaxCoolOff caps the exponential-backoff cool-off. Default 10m.
status.effectiveSettings.toolGuard.namespace.rules[].breaker.originFailureThresholdinteger (int32)OriginFailureThreshold is consecutive failures across ALL tools of the tool's origin that open the origin breaker (denying every sibling). (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].dataLimitobjectDataLimit caps per-call byte volume; nil means no byte cap.
status.effectiveSettings.toolGuard.namespace.rules[].dataLimit.actionstringAction when a byte limit is exceeded: halt ends the session; deny errors the call (egress: tool not run; ingress: result withheld); warn logs/audits but allows. (enum: halt | deny | warn)
status.effectiveSettings.toolGuard.namespace.rules[].dataLimit.maxEgressBytesinteger (int64)MaxEgressBytes caps the serialized tool-args size sent outbound per call. Exceeding it (at PreToolCall) applies Action; on deny the tool does not run. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].dataLimit.maxIngressBytesinteger (int64)MaxIngressBytes caps the tool-result size returned inbound per call (any result, success or error). Exceeding it (at PostToolCall) applies Action; on deny the result is withheld (replaced with an IsError) so the oversized payload never reaches the model. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].dataLimit.maxUIIngressBytesinteger (int64)MaxUIIngressBytes caps result bytes on an agent-UI DATA BINDING, whose result is rendered by a browser and never read by the model. Unset means the platform's browser-sized default applies (toolguard's DefaultUIIngressBytes) — NOT unlimited, and NOT MaxIngressBytes. Set this to bind the UI path tighter or looser than the platform default; tightening maxIngressBytes alone does not affect it. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].match *objectMatch selects the tools this rule governs.
status.effectiveSettings.toolGuard.namespace.rules[].match.kindstringKind is the runtime tool kind. Sidecar-toolbox tools report "mcp" (they are synthesized through the MCP synthesizer); select them via Origin "sidecartoolbox/*". (enum: sandbox | mcp | meta)
status.effectiveSettings.toolGuard.namespace.rules[].match.originstringOrigin is a glob over the tool's origin in "<kind>/<name>" form, e.g. "mcpserver/github" or "sidecartoolbox/". Note: a bare "" also matches origin-less tools (empty origin); use a prefixed glob like "mcpserver/*" to scope to tools that have an origin.
status.effectiveSettings.toolGuard.namespace.rules[].match.toolstringTool is a glob over the LLM-visible tool name (e.g. "github_*").
status.effectiveSettings.toolGuard.namespace.rules[].rateLimitobjectRateLimit caps call volume; nil means matched tools get no rate limit.
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.actionstringAction when a rate cap is hit: halt ends the session; deny errors the call; warn logs and audits but allows it. (enum: halt | deny | warn)
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.maxCallsinteger (int32)MaxCalls caps calls in the sliding Window; both must be set together. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.maxCallsPerTurninteger (int32)MaxCallsPerTurn caps calls within a single agent turn; 0 is unlimited. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.windowstringWindow is the sliding-window span for MaxCalls; both must be set together.
status.estimatedCostobjectEstimatedCost is the best-effort end-of-session USD cost estimate. Runner-owned; written at SessionEnd when reportSessionCost is on.
status.estimatedCost.amountMicroUSDinteger (int64)AmountMicroUSD is the session total in micro-USD (1e-6 USD); 0 and meaningless when PricingKnown is false.
status.estimatedCost.asOfstring (date-time)AsOf is when the estimate was computed.
status.estimatedCost.byModel[]objectByModel breaks the total down per actually-served model, so a session that fell back or auto-routed across models shows where the spend went instead of one blended total under the configured model. Empty when the runner reported no per-model usage; AmountMicroUSD and PricingKnown are then the sole source of truth.
status.estimatedCost.byModel[].amountMicroUSDinteger (int64)AmountMicroUSD is this bucket's share of the session cost, in micro-USD (1e-6 USD): the provider-reported cost when available, else tokens × the resolved per-model pricing table rate.
status.estimatedCost.byModel[].inputTokensinteger (int64)InputTokens is this model's share of prompt tokens.
status.estimatedCost.byModel[].model *stringModel is the served-model display id verbatim (provider/model, or a provider-prefixed served model under routing) — never re-parsed here.
status.estimatedCost.byModel[].outputTokensinteger (int64)OutputTokens is this model's share of completion tokens.
status.estimatedCost.byModel[].pricingKnownbooleanPricingKnown is true when AmountMicroUSD is a real cost — either the provider reported it directly, or the pricing table had a rate for this model. False means AmountMicroUSD is 0 and must not be shown as real spend (same no-fabrication contract as EstimatedSessionCost).
status.estimatedCost.currencystringCurrency is the ISO code the amount is denominated in ("USD").
status.estimatedCost.modelstringModel is the configured model the session ran under.
status.estimatedCost.pricingKnownbooleanPricingKnown is false when the model had no price; the amount is then 0 and must not be shown as a real cost.
status.failureReasonstringFailureReason is the human-readable reason phase became Failed.
status.finishedAtstring (date-time)FinishedAt is when the session reached a terminal phase.
status.identityChoiceParkedAtstring (date-time)IdentityChoiceParkedAt is when the session entered AwaitingIdentityChoice. The operator measures IdentityChoiceTimeout from here.
status.inputRequestobjectInputRequest is this child's outstanding mid-flight ask for data. The SubagentRequest controller mirrors it onto the request its parent is polling, the same route ParentExchange takes.
status.inputRequest.exchange *integer (int64)Exchange counts this child's data requests, starting at 1. Monotonic for the same reason ParentExchange's is: it is how a parent tells a new request from the one it just answered, and it must survive the child's pod being reaped mid-wait.
status.inputRequest.pendingbooleanPending is true while request number Exchange has not been answered either way — filled, or refused.
status.inputRequest.slot *stringSlot is the child-side slot name it wants filled.
status.inputRequest.why *stringWhy is the child's stated reason, in its own words. It is what the parent — and any person ruling on the disclosure — actually reads.
status.interactorTuplesWritten[]stringInteractorTuplesWritten lists the subjects for which this session has already written an agentclass#interactor tuple, so a long multi-turn session does not re-issue the idempotent SpiceDB touch every reconcile. Controller-owned; set-once per subject.
status.lastIdleAtstring (date-time)LastIdleAt records when the session most recently entered phase=Idle. Stamped by the AgentSession reconciler on Idle entry; cleared on Idle → Running. Drives the operator's archive sweep deadline.
status.lastUIServeAtstring (date-time)LastUIServeAt records the most-recent ui-serve-requested-at the operator has acted on, as LastWakeAt does for a conversation wake. Deliberately a SEPARATE marker: a conversation wake resumes the agent loop while a UI-serve spawn never does, so sharing one would let a dashboard request consume a pending message wake — the user's message silently never delivered — or a page load be answered by an agent turn nobody asked for.
status.lastWakeAtstring (date-time)LastWakeAt records the most-recent wake-requested-at the operator has acted on. Compared against the metadata annotation to detect new wake-up requests.
status.observedGenerationinteger (int64)ObservedGeneration is the spec generation this status reflects.
status.observedPins[]objectObservedPins records, per dependency the session actually used, the identity observed at session start (one unified list across kinds; Pin.Kind discriminates). Drift = observed != the definition CR's baseline pin.
status.observedPins[].name *stringName identifies the dependency: MCPServer ref name, SidecarToolbox name, Toolkit name, or canonical skill name.
status.observedPins[].pin *objectPin is the identity observed at session start; drift is this against the definition CR's baseline.
status.observedPins[].pin.detailsmap[string]stringDetails carries kind-specific extras (toolCount, registry host, …).
status.observedPins[].pin.digeststringDigest is the immutable identity: git sha, sha256:… image digest, canonical tool-manifest hash, or binary hash.
status.observedPins[].pin.kind *stringKind is the pinning registry kind name (skill, image, mcp, cli, …). Validated against the registry by controllers, not by a CRD enum, so new kinds register without an API change.
status.observedPins[].pin.observedAtstring (date-time)ObservedAt is when the recording controller observed this identity.
status.observedPins[].pin.strength *stringStrength is the syntactic pin strength of the declared ref. (enum: frozen | named | unpinned)
status.observedPins[].pin.versionstringVersion is the human-readable identity: tag, serverInfo.version, or version-probe output.
status.parentExchangeobjectParentExchange is a delegated child's side of the conversation with the agent that delegated to it. Written by the child's own runner (the ask_parent meta tool sets it; the loop clears Pending on resume) and read by the SubagentRequest controller, which mirrors it onto the request the child answers so the parent's delegate call can return with the question instead of blocking to a terminal phase. Set only on a conversational (task/chat) child. A single_turn child is headless, has no parent to ask, and leaves this nil for its whole life.
status.parentExchange.exchange *integer (int64)Exchange counts the questions this child has asked, starting at 1. Monotonic: it is read back from this same field and incremented, so it survives the pod being reaped mid-wait and is never reused. A parent tells a new question from the one it just answered by this number alone.
status.parentExchange.pendingbooleanPending is true while question number Exchange is unanswered — the one fact that says the child is parked on its parent rather than working. Cleared wherever awaitingUserInputSince is cleared on a RESUME (the loop's own resume hook, and the start of every Run, which is what makes it self-healing across a reap→wake cycle), and deliberately NOT cleared on the idle path: a child that parked, timed out to Idle and had its pod reaped is still waiting, and that is precisely the state the whole design keeps resumable.
status.parentExchange.questionstringQuestion is the text of question number Exchange, in the child's own words. Bounded because status is not a transport for arbitrary payloads; the child's runner refuses a longer one rather than truncating, so what the parent reads is never a silently altered question.
status.passthroughCredHashesmap[string]stringPassthroughCredHashes maps each projected passthrough credential name to the SHA-256 hex of its value as of the last reconcile. A change on the next reconcile means the credential was re-linked or removed, and the operator emits a per-session credential invalidation on the ap.revocation bus so a running session picks it up on its next tool call. Operator-owned observation, set on change — never an SSA-applied field.
status.pendingInteractions[]objectPendingInteractions is the single list of interaction prompts awaiting a user decision — tool_approval, info_leakage and content_inspection all park here, keyed by Category. Written by channelsd's HandleInteractionRequest. Each entry is self-contained so consumers (oap session approve, status-watchdog, webd, admind) can display and drive the decision without reading the memory layer. It does NOT drive phase: phase=AwaitingDecision derives from the runner's signed lifecycle projection.
status.pendingInteractions[].agentDisplayNamestringAgentDisplayName is the operator-configured AgentClass label, forwarded from the request payload for restart-resilient display.
status.pendingInteractions[].approverSubjectstringApproverSubject is the SpiceDB subject-set ref that has standing to decide (session-set policies: "agentsession:<ns>/<name>#approve"). oap session approve uses it for the client-side pre-check; the server re-checks authoritatively. Empty for policies with no fixed subject.
status.pendingInteractions[].category *stringCategory is the registered interaction category ("content_inspection"). status-watchdog switches wait-visibility on it; oap session approve selects the decision envelope from it.
status.pendingInteractions[].requestID *stringRequestID is the interaction RequestRef — the map key + the handle echoed on the eventual interaction_applied.
status.pendingInteractions[].requestRefstringRequestRef is the channel-kind-opaque round-trip handle. For the generic model this equals RequestID; kept distinct for parity with the typed lists and future kinds.
status.pendingInteractions[].requestedAt *string (date-time)RequestedAt is the park timestamp; used by display + the timeout watcher.
status.pendingInteractions[].summarystringSummary is the publisher-authored one-line human description (the interaction_request Lead) — the self-contained display string consumers render without reading the memory layer. Category-agnostic.
status.pendingRequesters[]objectPendingRequesters tracks ad-hoc permission requests that have been delivered to the original requester but not yet decided. Channel-agnostic; populated and cleared by channelsd's pipeline.
status.pendingRequesters[].categorystringCategory is the interaction category this entry was raised under — "start_approval" for a parked guest session start; empty means the original session-join request ("permission_request"). The decision pipe's category witness reads it so a click cannot resolve a pending request under a weaker standing policy than it was raised behind.
status.pendingRequesters[].channelKeystringChannelKey is the per-channel routing key (e.g. "thread:C0:1.2") that the inbound carried. Replayed alongside MessageText so the resubmit lands on the same session/thread.
status.pendingRequesters[].emailstringEmail is the user's email when the kind has it.
status.pendingRequesters[].externalId *stringExternalID is the per-kind user id (Slack U-id, ...).
status.pendingRequesters[].kind *stringKind is the channel kind ("slack", future "discord", ...).
status.pendingRequesters[].messageTextstringMessageText is the body of the inbound that triggered the permission request. Replayed verbatim through the inbound pipeline when the original requester clicks Approve, so the user does not have to re-send their message after approval. Truncated to a reasonable bound by the pipeline before storage.
status.pendingRequesters[].requestRef *stringRequestRef is the kind-defined opaque handle the channel kind uses to find its user-facing artifacts (rejection message, DM, thread refs) when the decision arrives.
status.pendingRequesters[].requestedAt *string (date-time)RequestedAt is when the request was delivered to the original requester.
status.pendingRequesters[].teamScopestringTeamScope is the per-kind workspace/server scope (Slack team ID, ...).
status.pendingRestartobjectPendingRestart is set by channelsd when a RestartTrigger is published, and cleared by the controller once the fork is complete (SupersededBy populated). Crash-recovery marker; the controller's reconciler retries an unfinished restart on resume.
status.pendingRestart.cutTurnIndex *integer (int32)CutTurnIndex is the index of the last turn copied to the child (inclusive). The user's edited message becomes turn CutTurnIndex+1 in the child session. Ignored when Mode is "inherit" — the reconciler recomputes the cut as the parent's last turn so the whole transcript carries forward.
status.pendingRestart.inheritHistorybooleanInheritHistory selects the takeover-mode transcript seeding: true copies the full parent transcript (ordinary terminal), false seeds only the new user's message (policy/security-halt carve-out — the halted transcript is not handed to a new owner). Ignored for restart/inherit modes.
status.pendingRestart.modestringMode selects the transcript-seeding behavior: "" (restart-from-here, cut at CutTurnIndex) or "inherit" (full-transcript continuation). Both modes are gated on agentsession#fork and copy the parent's denied tuples to the child.
status.pendingRestart.newOwnerExternalIDstringNewOwnerExternalID is the channel external-id of the taking-over user (takeover mode only). Stamped onto the child's AnnotationStartedByExternalID so the child is owned by the new user, not the parent's owner. Empty for restart/inherit modes.
status.pendingRestart.newUserText *stringNewUserText is the edited message text the user submitted through the channel-kind modal. Becomes the new session's inbox turn at CutTurnIndex+1.
status.pendingRestart.requestedAt *string (date-time)RequestedAt is when channelsd published the trigger.
status.pendingRestart.signatureobjectSignature is channelsd's Ed25519 attestation over this marker, bound to the parent session's namespace, name and UID. The operator's restart reconciler verifies it before acting and refuses the restart when it is absent or does not verify. It exists because the marker is an authorization input the session's own runner can write: the runner Role grants patch on agentsessions/status, Kubernetes RBAC has no field-level granularity, and takeover mode skips the SpiceDB fork gate. Without an author binding, a compromised runner could name any victim in triggeredBy and the operator would stamp that identity onto the child session, projecting the victim's credentials into the attacker's pod. See pkg/agent/restartmarker.
status.pendingRestart.signature.keyId *stringKeyID is the content address of the signing key's public half. The registry refuses to bind a keyID to a key that does not hash to it, so a tampered registration cannot make an attacker's key resolve here.
status.pendingRestart.signature.publisher *stringPublisher is the provenance publisher identity that signed the marker.
status.pendingRestart.signature.sig *string (byte)Sig is the raw Ed25519 signature over the canonical marker digest.
status.pendingRestart.targetSessionName *stringTargetSessionName is the deterministic name of the child AgentSession the reconciler will create. Stamped here so a crash-and-retry produces the same name.
status.pendingRestart.triggeredBy *stringTriggeredBy is the canonical subject of the user who clicked Restart (e.g., "user:alice"). Used for SpiceDB interact-perm re-check by the reconciler.
status.permissionSurface[]objectPermissionSurface is what this session's tools can actually reach: every permission handle the runner enumerated from the live tool envelope, with the tools that reach it. It answers "what can this agent do?" for the CLI and admin panel from the ONE producer that can answer it exactly — the runner, which holds the real []tool.Tool. Any second derivation (walking CRs, guessing at MCP tool names) would drift from what dispatch actually checks, and the surface≡dispatch equivalence is the whole point of enumerating it. An OBSERVATION, never authority. Nothing may gate on this field: it is a snapshot of a resolution, and a resolution read back later could be stale. Durable authority is carried by handles in frozen plans, approval records and the audit log — which is what permsurface's "only Handle persists" rule is protecting. Writing it here rather than into memory keeps that line visible: status is where this repo puts things a controller observed, and status is definitionally not a grant. A pure function of the tool envelope — sorted, deduplicated, no timestamps — so re-writing it on an unchanged session is a no-op diff.
status.permissionSurface[].handle *stringHandle is the durable, opaque reference to the permission class.
status.permissionSurface[].stateImpact *stringStateImpact is the MAX severity across every tool reaching this handle. Max, not per-tool: a handle one tool reads and another writes is a write handle, and reporting the milder value would understate the reach. (enum: readonly | readwrite | external)
status.permissionSurface[].tools[]stringTools are the LLM-facing names that reach this handle, sorted and deduplicated. A tool reaching one handle through both its base permission and a variant appears once — it is one route, not two.
status.phasestringPhase is the lifecycle phase: Pending, Running, Idle, one of the Awaiting* park states, Succeeded, or Failed.
status.pinnedMessageobjectPinnedMessage is the runner's projection of the triggered session's opening message; channelsd renders it onto the anchored opening line.
status.pinnedMessage.badgestringBadge is the current status marker. The runner writes in_progress and, on conclusion, the outcome; channelsd derives the terminal unfinished/done. (enum: in_progress | clean | problems_found | could_not_finish | unfinished | done)
status.pinnedMessage.bodystringBody is the agent-authored enrichment, last-write-wins; may be empty.
status.pinnedMessage.linkstringLink is the delivered-result view link, set on conclusion; may be empty.
status.progressobjectProgress accumulates turn, token and tool counts for the CURRENT pod; unlike RunDuration it resets when the pod is replaced.
status.progress.cacheCreationTokensinteger (int64)CacheCreationTokens is cumulative cache-write tokens (priced ~1.25x input).
status.progress.cacheReadTokensinteger (int64)CacheReadTokens is cumulative cache-read tokens (priced ~0.1x input).
status.progress.inputTokensinteger (int64)InputTokens is cumulative prompt tokens sent to the provider.
status.progress.outputTokensinteger (int64)OutputTokens is cumulative completion tokens returned by the provider.
status.progress.toolCallCountinteger (int32)ToolCallCount is how many tool calls the agent has issued.
status.progress.turnCountinteger (int32)TurnCount is how many agent turns have completed.
status.resolvedContentGuardDetectors[]objectResolvedContentGuardDetectors snapshots the detector sidecars injected for content-guard inspectors (e.g. prompt-injection). Empty ⇒ none.
status.resolvedContentGuardDetectors[].healthPathstringHealthPath is the readiness path; empty means the pod has no probe path.
status.resolvedContentGuardDetectors[].image *stringImage is the detector container image.
status.resolvedContentGuardDetectors[].inspector *stringInspector is the content-guard inspector this detector serves.
status.resolvedContentGuardDetectors[].podIPstringPodIP is the detector pod's IP, reflected once Ready. Empty ⇒ not ready; the operator requeues and holds runner-pod creation.
status.resolvedContentGuardDetectors[].podNamestringPodName is the detector pod's name.
status.resolvedContentGuardDetectors[].port *integer (int32)Port is the detector's HTTP port on its pod.
status.resolvedSidecarToolboxes[]objectResolvedSidecarToolboxes snapshots each referenced SidecarToolbox at session start, frozen for the session lifetime so a mid-flight CR edit cannot change an in-flight session's runtime contract. Empty when AgentClass.spec.sidecarToolboxes is empty.
status.resolvedSidecarToolboxes[].awaitingSecretbooleanAwaitingSecret is true when a secret-gated toolbox's bound secret-output is not yet available; the operator holds pod creation until it is.
status.resolvedSidecarToolboxes[].effectiveAllowedHosts[]stringEffectiveAllowedHosts is the sorted, deduped union of the SpiceboxClass's network.allowedHosts and the SidecarToolbox's sandbox.network.allowedHosts. RECORDED, not enforced: hostnames are not expressible in stock NetworkPolicy, so this is the source of truth a DNS-aware policy controller consumes to narrow beyond the coarse L3/L4 policy.
status.resolvedSidecarToolboxes[].effectiveNetworkModestringEffectiveNetworkMode is the merged network mode the sidecar runs under: the SpiceboxClass's mode, upgraded to allowlist when the class says none but the SidecarToolbox adds allowedHosts. For separate-pod sidecars the operator stamps an L3/L4 NetworkPolicy from it (none → full egress deny; allowlist → DNS + coarse TCP 443/80). (enum: none | allowlist)
status.resolvedSidecarToolboxes[].name *stringName is the LLM-facing prefix (the name field from AgentClassSidecarToolboxRef).
status.resolvedSidecarToolboxes[].podFailureobjectPodFailure records a TERMINAL container state on a separate sidecar pod (CrashLoopBackOff, ImagePullBackOff) — one the pod will not recover from on its own. Nil while the pod is healthy or still starting; cleared once it goes Ready. OPERATOR-owned, deliberately separate from the runner-owned SidecarReachability: reachability answers "the runner probed a running server and it did/didn't answer", PodFailure answers "the pod never got far enough to be probed." A crash-looping sidecar never receives a PodIP, so the prober never runs and reachability would stay silently empty — exactly the silent degradation this field exists to end.
status.resolvedSidecarToolboxes[].podFailure.exitCodeinteger (int32)ExitCode is the container's last terminated exit code. Nil when the container never ran (e.g. the image could not be pulled).
status.resolvedSidecarToolboxes[].podFailure.messagestringMessage is the most actionable text available: the container's last terminated message when present — the crashing container's log tail, thanks to the pod's FallbackToLogsOnError policy — otherwise the kubelet's waiting message. Preferring the log tail is what makes the field worth surfacing; the waiting message alone is content-free for a crash loop ("back-off Ns restarting failed container=…").
status.resolvedSidecarToolboxes[].podFailure.reason *stringReason is the kubelet's terminal container Waiting reason, e.g. "CrashLoopBackOff" or "ImagePullBackOff".
status.resolvedSidecarToolboxes[].port *integer (int32)Port is the runner-side loopback port (operator-allocated). Reachable at 127.0.0.1:<Port>.
status.resolvedSidecarToolboxes[].ref *stringRef is the SidecarToolbox CR name (the ref field from AgentClassSidecarToolboxRef).
status.resolvedSidecarToolboxes[].runModestringRunMode is "in-pod" (default, container in the agent pod) or "separate-pod" (secret-gated: its own per-session pod).
status.resolvedSidecarToolboxes[].secretTokenHashstringSecretTokenHash is a hash of the resolved secret value; a change triggers pod replacement.
status.resolvedSidecarToolboxes[].sidecarPodIPstringSidecarPodIP is the separate sidecar pod's IP, reflected once Ready; the runner reaches it at http://<SidecarPodIP>:<port>.
status.resolvedSidecarToolboxes[].sidecarPodNamestringSidecarPodName is the separate sidecar pod's name (RunMode=="separate-pod").
status.resolvedSidecarToolboxes[].spec *objectSpec is the SidecarToolbox spec snapshot at session-start time.
status.resolvedSidecarToolboxes[].spec.configstringConfig is an opaque configuration document for the sidecar image, delivered verbatim as the AP_SIDECAR_CONFIG environment variable. The platform does not interpret it; validation belongs to whoever admits the object (the workshop webhook parses it for first-party images known to consume one, e.g. ap-api-adapter). Bounded because it rides in the pod environment. MaxLength counts Unicode code points, not bytes — up to 4 bytes each in UTF-8 — so it alone admits up to 262144 bytes. The BYTE cap (what execve and apiadapter.MaxConfigBytes both count) is enforced by apiadapter.Parse, which both the workshop webhook (at admission) and the adapter binary (at boot) run. Deliberately NOT a CEL XValidation here: this spec type is embedded by value into AgentSession's status.resolvedSidecarToolboxes snapshot, and a CEL rule that reaches a status path both trips the status-CEL guard (pkg/platform/settings' TestNoUnguardedStatusCELRules) and blows the apiserver's CRD rule-cost budget inside that unbounded status array — the agentsessions CRD is then refused at install. Residual admin-path risk (no webhook, a >131072-byte multi-byte document): execve fails with E2BIG and the kubelet reports the container-start error on the Pod.
status.resolvedSidecarToolboxes[].spec.intentstringIntent is the one-line purpose shown to authors and reviewers.
status.resolvedSidecarToolboxes[].spec.isolationstringIsolation declares that this toolbox must run in its own per-session pod even though it has no SecretInputs. A secret-gated toolbox (any SecretInputs) is ALREADY isolated for its own reason -- its gating secret only exists per-session, so it cannot be baked into the runner pod's spec -- regardless of this field; Isolation is for the toolbox that needs a separate pod's consequences WITHOUT a gating secret: - Its own NetworkPolicy, scoped to exactly this sidecar's ingress/ egress, instead of inheriting the runner pod's shared policy (see pkg/controllers/agentsession/netpol.go: BuildRunnerNetworkPolicy vs BuildSidecarNetworkPolicy). - Eligibility for BuildSidecarPod's identity branch (a projected ServiceAccount token + operator env), which is reachable only in separate-pod mode -- an in-pod sidecar can never receive it. Absent (or "auto") never changes an existing toolbox's run mode: today's in-pod behavior is preserved exactly. (enum: auto | isolated)
status.resolvedSidecarToolboxes[].spec.mcpUiAppToolsobjectMCPUIAppTools is ACCEPTED BUT NOT CONSUMED: setting enabled: true changes nothing, and no error, log, or condition says so. It is typed identically to MCPServer's stanza so a toolbox can declare the intent to be an MCP-UI app-tool origin, but nothing reads it, so no sidecar tool is browser-callable. Recorded, not enforced — the same posture as this CRD's effectiveAllowedHosts. A toolbox tool named in AgentUI.spec.tools is therefore denied by the three-way grant's origin condition: the right fail-closed answer for an unwired origin, just not the one this field's name suggests. Absent ⇒ disabled: the permanent, fail-closed default.
status.resolvedSidecarToolboxes[].spec.mcpUiAppTools.enabled *booleanEnabled turns the capability on; no app-visible tool is callable without it.
status.resolvedSidecarToolboxes[].spec.mcpUiAppTools.maxCallsPerMininteger (int32)MaxCallsPerMin optionally caps autonomous app-tool calls per minute. Enforced by a DEDICATED per-origin limiter on the app-tool call path (runner.AppToolRateLimiter, wired in internal/cmd/runner from this field) — deliberately NOT a toolguard rule, which would also gate this MCPServer's LLM-visible tools and strip its circuit breaker. Nil ⇒ unlimited. (min 1)
status.resolvedSidecarToolboxes[].spec.name *stringName is the toolbox's toolspec-level name.
status.resolvedSidecarToolboxes[].spec.sandbox *objectSandbox is the SpiceboxClass shape and egress the sidecar runs under.
status.resolvedSidecarToolboxes[].spec.sandbox.class *stringClass names a SpiceboxClass (cluster-scoped) whose resources, runtimeClassName, default network mode, pidsLimit are inherited.
status.resolvedSidecarToolboxes[].spec.sandbox.networkobjectNetwork widens the class's egress for this toolbox.
status.resolvedSidecarToolboxes[].spec.sandbox.network.allowedHosts[]stringAllowedHosts merges with the SpiceboxClass's network.allowedHosts. If the class's mode is "none" and any AllowedHosts are supplied here, the effective sidecar network upgrades to "allowlist" with exactly these hosts.
status.resolvedSidecarToolboxes[].spec.secretInputs[]objectSecretInputs binds secret-output handles (produced by an earlier tool call) to this toolbox. A toolbox with any SecretInput is "secret-gated": it is NOT injected into the agent pod; the operator runs it as a separate per-session pod once the bound secret is available.
status.resolvedSidecarToolboxes[].spec.secretInputs[].deliver *stringDeliver is how the value reaches the sidecar at startup: "env" (env var named Name) or "file:<path>" (mounted file at <path>).
status.resolvedSidecarToolboxes[].spec.secretInputs[].from *stringFrom is the secret-output logical name the producer emitted (matches the producer's secretOutput.name / the per-session Secret key).
status.resolvedSidecarToolboxes[].spec.secretInputs[].name *stringName is the env-var name (when Deliver=="env") or the logical key.
status.resolvedSidecarToolboxes[].spec.source *objectSource is where the sidecar's container image comes from.
status.resolvedSidecarToolboxes[].spec.source.imagestringImage is a prebuilt container image reference.
status.resolvedSidecarToolboxes[].spec.source.inlineobjectInline builds the sidecar from a script layered onto a base image.
status.resolvedSidecarToolboxes[].spec.source.inline.baseImage *stringBaseImage is the image the script runs on top of.
status.resolvedSidecarToolboxes[].spec.source.inline.entrypoint *[]stringEntrypoint is the argv that starts the server inside the container.
status.resolvedSidecarToolboxes[].spec.source.inline.script *objectScript is where the server's source comes from.
status.resolvedSidecarToolboxes[].spec.source.inline.script.configMapRef *objectConfigMapRef names the ConfigMap key holding the script body.
status.resolvedSidecarToolboxes[].spec.source.inline.script.configMapRef.key *stringKey is the data key within the ConfigMap.
status.resolvedSidecarToolboxes[].spec.source.inline.script.configMapRef.name *stringName is the ConfigMap's name, in the toolbox's own namespace.
status.resolvedSidecarToolboxes[].spec.spicedbSchemaobjectSpiceDBSchema declares the SpiceDB resource definitions this toolbox contributes — identical to MCPServer.spec.spicedbSchema. The guardian's schema composer concatenates fragments across every MCPServer AND SidecarToolbox in the cluster so the audiences the tags derive from resolve; identical declarations dedupe, conflicting ones fail the reconcile.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.rawZedstringRawZed is appended verbatim to the composed schema after Resources are emitted. Use this for SpiceDB schema features the structured form can't represent: subject-relations (e.g. slack_user#user) and unions (e.g. user | agent). The composer does not parse or validate RawZed beyond schema-load time; malformed text fails the SpiceDB WriteSchema call.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[]objectResources lists structured SpiceDB definitions contributed by this fragment. Each resource is emitted with its declared relations and permissions; the composer dedupes by name across fragments.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].approverPermissionstringApproverPermission names the permission an approver must hold on an instance for their approval to count. Required when Standing is required, and must be empty when it is session-only — a type nothing governs has no permission to name, and accepting one there would read as governance that is never consulted. Declared rather than assumed. This was hardcoded to "owner" at four call sites, which silently made two very different types look identical to the approval router: crm_company's owner is a real computed permission backed by seeded tuples, while git_repo's owner is a bare relation declared only so the checks are answerable and never populated by anything. The router could not tell governance from an artifact of schema shape, so a push to a git remote resolved to an empty owner-set and became unapprovable forever. Naming it also lifts the assumption that the approving permission is called "owner": a type may route approval through maintainer, admin, or any permission its own schema defines.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].displayobjectDisplay declares how an INSTANCE of this resource type presents on an approval card — an icon, and how to turn its raw value (a URL, typically) into a short label. Absent means the card falls back to the wire type name, exactly as before this field existed.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].display.iconstringIcon names a glyph from a CLOSED, code-defined registry — never a URL and never free-form. A vendor-specific type (github_repo) may name the vendor's own mark; a host-agnostic type (git_repo) must name a generic one, because claiming a vendor mark would lie the moment an instance points somewhere else (a self-hosted remote). An unrecognized name renders no icon — never a fallback image, never a guess.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].display.labelstringLabel names a deriver from a CLOSED, code-defined set that turns an instance's raw value into a short display label — "url_path", "b64url_path", "last_segment", or "none" (the set registered in pkg/authz/plangate's labelDerivers, which is the authority; this list is prose and has drifted from it once). Not a template and not a regex: nothing here can produce a label the code did not author, and the deriver never sees anything an agent did not itself write into the plan (the instance value), so a declaration can shorten a value's presentation but can never fabricate one. An unrecognized name derives nothing, and the card falls back to Name.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].display.namestringName is the human name for this resource TYPE — "Git repository", never the wire handle "git_repo". Shown when no per-instance label can be derived (Label is "none", unset, or the deriver produces nothing for this instance's value).
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].name *stringName is the SpiceDB definition name; the composer dedupes on it.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].permissions[]objectPermissions are the resource's computed permissions.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].permissions[].expr *stringExpr is the right-hand side of permission &lt;name&gt; = …, pasted verbatim.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].permissions[].name *stringName is the permission name.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].permissions[].planningNotestringPlanningNote is one line of guidance the agent reads while DECLARING a plan, rendered next to the handles it may declare. The knowledge that shapes a good plan is toolkit-specific, so it lives with the toolkit rather than in the runner's generic prompt. git_repo splits its permissions across two instances — read/write key on the checked-out copy, fetch/push on the remote URL — so a phase declaring write without read can change files it cannot open, and the first read interrupts the human with an amendment the plan should have carried from the start. It shapes what the agent DECLARES, never what anything grants: a note is prose the model reads at plan time, and no authorization decision reads it. A phase that ignores its guidance still gets the gate it earned.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].permissions[].titlestringTitle is the phrase a human reads on an approval card instead of the permission's handle. perm:push:git_repo is a WIRE FORMAT; asking somebody to decide on it makes the decision slower and worse exactly where care matters most. Declared here because this is where the permission itself is declared, so one declaration serves every channel and the CLI. Absent is fine and common: the card detokenizes the handle instead, which shows no plumbing and invents no English.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].relations[]objectRelations are the resource's relations, emitted verbatim.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].relations[].name *stringName is the relation name as it appears in the composed schema.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].relations[].subjectType *stringSubjectType must be a bare resource-type name (e.g. "user", "hubspot_owner") declared in this same SpiceDBSchema or the implicit "user" type. Wildcards ("user:*") are expressed via the separate Wildcard field. Subject-relation forms ("team#member") are NOT representable. Exactly one SubjectType per Relation — SpiceDB unions like relation viewer: user | team#member cannot be modeled.
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].relations[].wildcardbooleanWildcard, when true, makes the relation accept ANY subject of SubjectType — emitted as relation &lt;name&gt;: &lt;subjectType&gt;:* in the composed schema. Use sparingly: a wildcard relation effectively grants the underlying-resource permission to every subject of that type, so any per-call gating must come from a different layer (e.g. stateImpact: external on the tool, which routes every call through the approval flow regardless of the SpiceDB Check result).
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].standing *stringStanding declares whether SpiceDB is AUTHORITATIVE for this resource type — that is, whether an approver can be expected to already hold a permission on an instance of it. REQUIRED, with no default. - required SpiceDB governs who may approve. The approver pool is ApproverPermission on the named instance, and an EMPTY pool is a final refusal — nobody can approve what nobody governs. - session-only No local permission governs approval for this type, so the session's own approvers decide and their decision IS the authority. Everything downstream is unchanged: the grant is still written, still expiring, still session-scoped and revocable, and the tool-call Check still runs. There is deliberately NO default. A default is a hole you open by omission: defaulting to session-only silently widens who may approve a type somebody forgot to classify, and defaulting to required makes a forge-governed type permanently unbindable because nothing writes a SpiceDB tuple for every git remote. Neither failure announces itself, so the author states the answer. Same reasoning as AP_CLUSTER_KIND, which also refuses to default. Choosing is a question about the RESOURCE, not about convenience: does a permission on this instance already say who may speak for it? A CRM company with seeded owner tuples: yes, required. A git remote whose permissions live at the forge: no, session-only. A cluster or namespace admin can force any type to required with SettingsLimits.RequireStandingFor, which no fragment may widen past. (enum: session-only | required)
status.resolvedSidecarToolboxes[].spec.toolResourceMap[]objectToolResourceMap declares, per-tool, the SpiceDB resource a read accesses — identical semantics to MCPServer.spec.toolResourceMap. Consumed by the runner's information-leakage gate so a sidecar tool participates in per-datum egress (its result mints a pt-tag) instead of falling to the undeclared coarse floor.
status.resolvedSidecarToolboxes[].spec.toolResourceMap[].noTaintbooleanNoTaint marks the tool as a pure utility that reads no user data. Set true to opt out of taint capture and the requester view check.
status.resolvedSidecarToolboxes[].spec.toolResourceMap[].readsobjectReads declares the resource the tool reads. Set this OR NoTaint; unset both is treated as "unmapped" and may block tool dispatch depending on AgentClass.Authz.InformationLeakage.Mode.
status.resolvedSidecarToolboxes[].spec.toolResourceMap[].reads.bypassRequesterCheckbooleanBypassRequesterCheck, when true, skips the requester view check. The taint is still recorded; this is useful for agents whose service identity legitimately has broader access than the requester.
status.resolvedSidecarToolboxes[].spec.toolResourceMap[].reads.idArgstringIDArg names a top-level field inside the tool's args envelope whose string value is the SpiceDB resource ID. Mutually exclusive with ResultIDField.
status.resolvedSidecarToolboxes[].spec.toolResourceMap[].reads.permission *stringPermission is the SpiceDB permission checked against the requester (e.g. "view").
status.resolvedSidecarToolboxes[].spec.toolResourceMap[].reads.resourceType *stringResourceType is the SpiceDB definition name (e.g. "linear_issue").
status.resolvedSidecarToolboxes[].spec.toolResourceMap[].reads.resultIDFieldstringResultIDField names a top-level field on the tool result's content (parsed as JSON) whose string value is the SpiceDB resource ID. Set this when the agent's input identifier differs from the SpiceDB resource ID (e.g. Linear id: "L-140" returns a UUID in the response). When set, the pre-execute requester view check happens AFTER the call against the response-extracted ID. Mutually exclusive with IDArg.
status.resolvedSidecarToolboxes[].spec.toolResourceMap[].tool *stringTool is the MCP tool name this mapping applies to.
status.resolvedSidecarToolboxes[].spec.tools *[]objectTools mirrors MCPServerTool field-for-field — same allowlist, CEL, deny effects, sensitive fields. See MCPServerTool for details.
status.resolvedSidecarToolboxes[].spec.tools[].argsobjectArgs is the argument allowlist and CEL constraints for this tool.
status.resolvedSidecarToolboxes[].spec.tools[].args.allowedFields[]stringAllowedFields lists the argument keys the agent may pass to this tool. Enforcement is fail-closed: an empty/unset AllowedFields DENIES any argument the agent passes (an arg-less tool still works). To accept free-form arguments, set UnconstrainedArgs=true instead of leaving this empty.
status.resolvedSidecarToolboxes[].spec.tools[].args.constraints[]objectConstraints are CEL predicates every call's args must satisfy (AND).
status.resolvedSidecarToolboxes[].spec.tools[].args.constraints[].cel *stringCEL is a boolean expression over the call's args; false denies the call.
status.resolvedSidecarToolboxes[].spec.tools[].args.constraints[].messagestringMessage is the denial text shown when CEL evaluates false.
status.resolvedSidecarToolboxes[].spec.tools[].args.sensitiveFields[]stringSensitiveFields names arg keys to redact from logs and approval prompts.
status.resolvedSidecarToolboxes[].spec.tools[].args.unconstrainedArgsbooleanUnconstrainedArgs explicitly opts this tool out of AllowedFields enforcement, for a tool that legitimately accepts arbitrary arguments. Required to allow free-form args — an empty AllowedFields alone is fail-closed, not allow-all.
status.resolvedSidecarToolboxes[].spec.tools[].denyobjectDeny lists effect and trust capabilities that refuse the call pre-dispatch.
status.resolvedSidecarToolboxes[].spec.tools[].deny.effectsobjectEffects denies calls by declared effect (destructive, reads, writes).
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects.credsobjectCreds denies credential-touching effects.
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects.creds.writesbooleanWrites denies any tool that would write credential material.
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects.destructivebooleanDestructive denies any tool the server marks destructive.
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects.reads[]stringReads names resource kinds whose reads are denied.
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects.writes[]stringWrites names resource kinds whose writes are denied.
status.resolvedSidecarToolboxes[].spec.tools[].deny.trustobjectTrust denies calls by asserted SEP-1913 trust capability.
status.resolvedSidecarToolboxes[].spec.tools[].deny.trust.destinationPublicbooleanDestinationPublic denies the call when it would write somewhere publicly visible.
status.resolvedSidecarToolboxes[].spec.tools[].deny.trust.outcomesIrreversiblebooleanOutcomesIrreversible denies the call when the server declares its effects cannot be undone.
status.resolvedSidecarToolboxes[].spec.tools[].deny.trust.sourceUntrustedPublicbooleanSourceUntrustedPublic denies the call when it would read untrusted public content into the agent's context.
status.resolvedSidecarToolboxes[].spec.tools[].descriptionOverridestringDescriptionOverride replaces the server's own description in the prompt; empty keeps what the server sent.
status.resolvedSidecarToolboxes[].spec.tools[].effectsobjectEffects is the declared effect profile used for approval and gating.
status.resolvedSidecarToolboxes[].spec.tools[].effects.destructivebooleanDestructive means a call can remove or overwrite upstream state.
status.resolvedSidecarToolboxes[].spec.tools[].effects.idempotentbooleanIdempotent means repeating a call has the same effect as making it once.
status.resolvedSidecarToolboxes[].spec.tools[].effects.openWorldbooleanOpenWorld means the call may reach systems beyond the named server.
status.resolvedSidecarToolboxes[].spec.tools[].effects.readOnlybooleanReadOnly means a call mutates nothing upstream.
status.resolvedSidecarToolboxes[].spec.tools[].intentstringIntent is the one-line purpose shown to authors and reviewers.
status.resolvedSidecarToolboxes[].spec.tools[].labels[]objectLabels declares per-tool CEL blocks extracting (resourceType, id, name) tuples from tool responses for approval-prompt rendering. Evaluated after a SUCCESSFUL call; failures are non-fatal (logged and skipped). Labels never reach any LLM context.
status.resolvedSidecarToolboxes[].spec.tools[].labels[].forEachstringForEach is a CEL expression that must evaluate to a list. One label is emitted per element with item bound to that element. When unset, exactly one label is emitted with item=nil.
status.resolvedSidecarToolboxes[].spec.tools[].labels[].label *objectLabel is the (resourceType, id, name) CEL triple. All three fields are required CEL string expressions.
status.resolvedSidecarToolboxes[].spec.tools[].labels[].label.id *stringID is a CEL string expression yielding the resource id.
status.resolvedSidecarToolboxes[].spec.tools[].labels[].label.name *stringName is a CEL string expression yielding the friendly label shown to approvers. Capped + sanitized at extraction time.
status.resolvedSidecarToolboxes[].spec.tools[].labels[].label.resourceType *stringResourceType is a CEL string expression yielding the SpiceDB definition name (e.g. "crm_company").
status.resolvedSidecarToolboxes[].spec.tools[].labels[].whenstringWhen is a CEL boolean expression with args, result in scope; the block is skipped if it evaluates false.
status.resolvedSidecarToolboxes[].spec.tools[].name *stringName is the tool name as the upstream server reports it.
status.resolvedSidecarToolboxes[].spec.tools[].observes[]objectObserves declares facts this tool's result asserts about specific resource instances, co-derived with the subjects they are about. Evaluated after a SUCCESSFUL call.
status.resolvedSidecarToolboxes[].spec.tools[].observes[].facts *map[string]stringFacts maps a fact name to a CEL expression yielding its value, evaluated against the same item as Subjects.
status.resolvedSidecarToolboxes[].spec.tools[].observes[].forEachstringForEach is a CEL expression yielding a list. One observation is emitted per element with item bound to it. Unset emits exactly one, with item bound to nil.
status.resolvedSidecarToolboxes[].spec.tools[].observes[].subjects *[]objectSubjects are the objects this observation is about, as CEL string expression pairs. At least one is REQUIRED: a block recording facts about nothing would produce a session-scoped boolean that answers for every instance at once.
status.resolvedSidecarToolboxes[].spec.tools[].observes[].subjects[].resourceID *stringResourceID is a CEL string expression yielding the object id.
status.resolvedSidecarToolboxes[].spec.tools[].observes[].subjects[].resourceType *stringResourceType is a CEL string expression yielding a SpiceDB definition name (commonly a literal, e.g. "github_pr").
status.resolvedSidecarToolboxes[].spec.tools[].observes[].whenstringWhen is a CEL boolean over args, result; the block is skipped if it evaluates false. Unset means always taken.
status.resolvedSidecarToolboxes[].spec.tools[].permissionobjectPermission declares the per-tool authz policy. Optional in the schema only: a tool without one fails AgentClass-time validation.
status.resolvedSidecarToolboxes[].spec.tools[].permission.checkobjectPermissionCheck names the SpiceDB resource and permission to evaluate. Required when StateImpact is Readonly / Readwrite / External; forbidden when Stateless / Passthrough.
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.enforceModestringEnforceMode controls deny-finality under permissive toolAuthMode. Empty → EnforceInherit (slice-1 default).
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.extractionPromptstringExtractionPrompt, when set, is an English fragment the runner includes in the extraction LLM's system prompt for this resource type. Tool spec authors write this once per toolspec/MCPServer file. The runner aggregates ExtractionPrompts across all tools whose Check targets the same resourceType (dedup'd, concatenated). The AgentClass's BoundEntityType.ExtractionPrompt overrides entirely if set.
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.grantBindsArgs[]stringGrantBindsArgs, when set, restricts the slice-2 grant's arguments_hash caveat binding to only the listed top-level arg keys. Default (nil/empty) hashes the full arg map, meaning the grant satisfies only the exact same call. Setting it to e.g. ["repo"] lets a single approval cover every subsequent call against the same repo regardless of other args (pr number, etc.) — useful for readonly tools where the resource is the load-bearing input. The hash is an HMAC-SHA256 under the per-session args-hash key minted by the AgentSession reconciler into the per-session Secret and read by the runner at startup. Caveat contexts therefore cannot be predicted or minted outside the controller+runner trust domain — neither the agent nor a confused deputy with SpiceDB write access can forge a valid binding for arguments that were never requested. Note: hashes of previously-requested (including denied) calls remain observable in session status and channel payloads, so a SpiceDB-write attacker could still replay those; the keying narrows that surface to exactly the calls a human has already seen. See pkg/authz/guardian/grants.ArgsHashFiltered for the filtering contract. NOTE: the hash no longer BINDS the grant. A slot grant is keyed on (instance, permission), which is what makes it safe to reuse across calls: a grant shaped for one permission cannot be spent on another against the same id. The filtered hash is still computed and published on the approval request (ToolApprovalDetails.ArgsHash) so an approver — and the audit log — can see exactly which call was asked about.
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.permission *stringPermission is the SpiceDB permission (or relation) name to Check (e.g. "read", "write", "admin"). Static. Pattern-validated at admission for the same reason as ResourceType. Mirrors BoundEntityType.Permission.
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.resourceIDExprstringResourceIDExpr is a CEL string expression evaluated against args that yields the SpiceDB resource id. Exactly one of ResourceIDTemplate or ResourceIDExpr must be set: the template form for simple &#123;arg&#125; interpolation, the Expr form for nested-arg extraction.
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.resourceIDHintstringResourceIDHint is shown to the AGENT when the resource id cannot be resolved from the call's arguments. That failure is usually the caller's to fix — it named no resource, or named it in a form the check cannot read — and the agent is the only party who can retry. Without a hint the message is the raw resolution error prefixed "internal:", which reads as a system fault and tells it not to bother. Written by whoever authors the check, because only they know what the call should have looked like. Example, for a git push whose remote must be a URL so the repository can be authorized: "name the remote as a full https:// URL, not a shorthand like origin".
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.resourceIDTemplatestringResourceIDTemplate is a string with {arg} placeholders that interpolate from the tool call's args (e.g. "{owner}/{repo}"). Resolved at runtime by ResolveTemplate; validated at AgentClass-reconcile time so every {arg} matches a parameter the tool declares.
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.resourceIDTransforms[]stringResourceIDTransforms names registered transforms applied IN ORDER to the resolved template string before SpiceDB sees it: lowercase, remove_spaces, spicedb_object_id, basename, sha256. See transforms.go.
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.resourceType *stringResourceType is the SpiceDB definition the Check runs against (e.g. "github_repo", "channel"). Static — no template interpolation. Pattern-validated at admission because this value is a component of a permsurface.Handle, which appears in approved plan ceilings and audit records. Mirrors BoundEntityType.ResourceType.
status.resolvedSidecarToolboxes[].spec.tools[].permission.stateImpact *stringStateImpact describes the policy mode for a tool / subcommand.
status.resolvedSidecarToolboxes[].spec.tools[].permission.toolNamestringToolName lets Layer 2's CheckScope evaluate the tool allow/deny axis. Wire from the registered tool name. Empty string skips the Layer 2 tool check (existing behavior).
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[]objectPermissionVariants are conditional Permission blocks, each carrying a CEL When predicate over the call's args. First match wins; Permission above is the fallback when none match.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check *objectCheck is the Permission applied when When matches. Same shape as the slice-1 singular Permission's Check, plus the new EnforceMode and ResourceIDExpr fields.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.checkobjectPermissionCheck names the SpiceDB resource and permission to evaluate. Required when StateImpact is Readonly / Readwrite / External; forbidden when Stateless / Passthrough.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.enforceModestringEnforceMode controls deny-finality under permissive toolAuthMode. Empty → EnforceInherit (slice-1 default).
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.extractionPromptstringExtractionPrompt, when set, is an English fragment the runner includes in the extraction LLM's system prompt for this resource type. Tool spec authors write this once per toolspec/MCPServer file. The runner aggregates ExtractionPrompts across all tools whose Check targets the same resourceType (dedup'd, concatenated). The AgentClass's BoundEntityType.ExtractionPrompt overrides entirely if set.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.grantBindsArgs[]stringGrantBindsArgs, when set, restricts the slice-2 grant's arguments_hash caveat binding to only the listed top-level arg keys. Default (nil/empty) hashes the full arg map, meaning the grant satisfies only the exact same call. Setting it to e.g. ["repo"] lets a single approval cover every subsequent call against the same repo regardless of other args (pr number, etc.) — useful for readonly tools where the resource is the load-bearing input. The hash is an HMAC-SHA256 under the per-session args-hash key minted by the AgentSession reconciler into the per-session Secret and read by the runner at startup. Caveat contexts therefore cannot be predicted or minted outside the controller+runner trust domain — neither the agent nor a confused deputy with SpiceDB write access can forge a valid binding for arguments that were never requested. Note: hashes of previously-requested (including denied) calls remain observable in session status and channel payloads, so a SpiceDB-write attacker could still replay those; the keying narrows that surface to exactly the calls a human has already seen. See pkg/authz/guardian/grants.ArgsHashFiltered for the filtering contract. NOTE: the hash no longer BINDS the grant. A slot grant is keyed on (instance, permission), which is what makes it safe to reuse across calls: a grant shaped for one permission cannot be spent on another against the same id. The filtered hash is still computed and published on the approval request (ToolApprovalDetails.ArgsHash) so an approver — and the audit log — can see exactly which call was asked about.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.permission *stringPermission is the SpiceDB permission (or relation) name to Check (e.g. "read", "write", "admin"). Static. Pattern-validated at admission for the same reason as ResourceType. Mirrors BoundEntityType.Permission.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.resourceIDExprstringResourceIDExpr is a CEL string expression evaluated against args that yields the SpiceDB resource id. Exactly one of ResourceIDTemplate or ResourceIDExpr must be set: the template form for simple &#123;arg&#125; interpolation, the Expr form for nested-arg extraction.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.resourceIDHintstringResourceIDHint is shown to the AGENT when the resource id cannot be resolved from the call's arguments. That failure is usually the caller's to fix — it named no resource, or named it in a form the check cannot read — and the agent is the only party who can retry. Without a hint the message is the raw resolution error prefixed "internal:", which reads as a system fault and tells it not to bother. Written by whoever authors the check, because only they know what the call should have looked like. Example, for a git push whose remote must be a URL so the repository can be authorized: "name the remote as a full https:// URL, not a shorthand like origin".
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.resourceIDTemplatestringResourceIDTemplate is a string with {arg} placeholders that interpolate from the tool call's args (e.g. "{owner}/{repo}"). Resolved at runtime by ResolveTemplate; validated at AgentClass-reconcile time so every {arg} matches a parameter the tool declares.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.resourceIDTransforms[]stringResourceIDTransforms names registered transforms applied IN ORDER to the resolved template string before SpiceDB sees it: lowercase, remove_spaces, spicedb_object_id, basename, sha256. See transforms.go.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.resourceType *stringResourceType is the SpiceDB definition the Check runs against (e.g. "github_repo", "channel"). Static — no template interpolation. Pattern-validated at admission because this value is a component of a permsurface.Handle, which appears in approved plan ceilings and audit records. Mirrors BoundEntityType.ResourceType.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.stateImpact *stringStateImpact describes the policy mode for a tool / subcommand.
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.toolNamestringToolName lets Layer 2's CheckScope evaluate the tool allow/deny axis. Wire from the registered tool name. Empty string skips the Layer 2 tool check (existing behavior).
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].when *stringWhen is a CEL boolean expression with args in scope.
status.resolvedSidecarToolboxes[].spec.tools[].trustobjectTrust is the SEP-1913 trust + action-security annotation snapshot. Mirrors mcpspec.Trust.
status.resolvedSidecarToolboxes[].spec.tools[].trust.attribution[]stringAttribution names the parties the server credits for the tool.
status.resolvedSidecarToolboxes[].spec.tools[].trust.inputMetadataobject (free-form)InputMetadata is the server's per-argument security annotations, verbatim.
status.resolvedSidecarToolboxes[].spec.tools[].trust.maliciousActivityHintbooleanMaliciousActivityHint is the server's own admission that this tool can be abused; self-reported, so treat it as a signal, not a guarantee.
status.resolvedSidecarToolboxes[].spec.tools[].trust.returnMetadataobject (free-form)ReturnMetadata is the server's per-result security annotations, verbatim.
status.resolvedSidecarToolboxes[].spec.tools[].visibility[]stringVisibility mirrors mcpspec.Tool.Visibility (MCP Apps' _meta.ui.visibility): which surfaces ("model" / "app") a tool is exposed to. This repo's routing is an EXCLUSIVE split, not a fan-out (see pkg/agent/tool/mcp/synthesize.go): a tool is browser-callable if and only if this list contains "app" and does NOT contain "model", and the owning MCPServer sets mcpUiAppTools.enabled. Concretely: unset / [] -> model-visible only; never reaches the browser ["app"] -> browser-callable (with the opt-in); withheld from the model ["app"] no opt-in -> rejected by default: in NEITHER registry ["model"] -> model-visible only ["app","model"] -> model-visible only; NOT browser-callable So "unset" is not "both": it is the model surface. A tool intended for an agent UI must say ["app"] and nothing else. oap agent lint reports any other value for a tool an AgentUI binds to.
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[]objectWritesRelationships declares JIT SpiceDB relationship writes the dispatcher performs after a successful tool call. When and ForEach are CEL over (args, result); the tuple fields are CEL string expressions.
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[].exclusivebooleanExclusive makes this write atomically write-once per subject: the write FAILS (no tuple written) if the subject already holds relation on ANY resource of the tuple's resource type. Used for session-pin semantics (a session may be pinned to exactly one cluster). Implemented as a SpiceDB MUST_NOT_MATCH precondition, so it is atomic under concurrent writers. Default false preserves the plain TOUCH-upsert behavior.
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[].forEachstringForEach is a CEL expression that must evaluate to a list. One tuple is emitted per element, with item bound to that element. When unset, exactly one tuple is emitted.
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[].requireSlotBoundbooleanRequireSlotBound refuses every tuple this block emits unless the calling session holds a SLOT GRANT on the tuple's RESOURCE — the instance a human (or the pool machinery acting on one's approval) named for this session. Declare it on a block that writes an identity or an authority tuple onto an instance the tool itself names. Without it, whatever id the tool's response happens to carry becomes the resource of a real SpiceDB write, so a response naming somebody else's instance writes there too. With it, the write can only ever land on an instance the session was already bound to. The grant's PERMISSION is deliberately not consulted: a grant is a human act naming the instance, and which permission it carries is the pool machinery's concern. Any slot_grant_* on the resource binds it. Default false is byte-identical to the previous behaviour — an unmarked block consults nothing. A marked block whose dispatcher has no checker wired is REFUSED, not written: see pkg/authz/relwrites.Run.
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[].tuple *objectTuple holds the CEL expressions that compose the SpiceDB relationship tuple.
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[].tuple.relation *stringRelation is a CEL string expression that must evaluate to a relation name declared on the Resource's type.
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[].tuple.resource *stringResource is a CEL string expression that must evaluate to a SpiceDB object reference of the form "<type>:<id>".
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[].tuple.subject *stringSubject is a CEL string expression that must evaluate to a SpiceDB object reference of the form "<type>:<id>".
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[].whenstringWhen is a CEL boolean expression with args, result in scope; the block is skipped if it evaluates false. When unset, the block is always taken.
status.resolvedSidecarToolboxes[].spec.transport *objectTransport is how the runner reaches the sidecar's MCP endpoint.
status.resolvedSidecarToolboxes[].spec.transport.healthcheckobjectHealthcheck is how the operator's probe decides the sidecar is up.
status.resolvedSidecarToolboxes[].spec.transport.healthcheck.pathstringPath is the HTTP path probed for readiness. (default: /healthz)
status.resolvedSidecarToolboxes[].spec.transport.healthcheck.timeoutSecondsinteger (int32)TimeoutSeconds bounds how long the probe waits for the sidecar to answer. (default: 30; min 1)
status.resolvedSidecarToolboxes[].spec.transport.pathstringPath is the HTTP path the sidecar's MCP Streamable-HTTP endpoint listens on (e.g. "/mcp"); empty means the pod root "/". The runner appends it to the pod URL for BOTH the tools/list reachability probe and tool-call dispatch, so it must match where the sidecar actually serves MCP. A leading slash is optional — the runner normalizes it.
status.resolvedSidecarToolboxes[].spec.transport.portinteger (int32)Port is advisory — the operator allocates a free port at pod-build time and sets MCP_PORT in the sidecar's env regardless. Sidecar code is required to read MCP_PORT to bind. (default: 8080)
status.resolvedSidecarToolboxes[].spec.upstreamAuth *objectUpstreamAuth is the credential the sidecar needs for its own upstream.
status.resolvedSidecarToolboxes[].spec.upstreamAuth.envVarstringEnvVar is the environment variable name the resolved upstream credential is injected into (in the per-session sidecar Secret). Empty means the sidecar needs no upstream credential (e.g. the echo example) — the per-session Secret is written empty.
status.resolvedSidecarToolboxes[].spec.upstreamAuth.provider *stringProvider names a provider in the /providers/ library, or the UpstreamAuthProviderNone sentinel ("none") for a controller-issued-token sidecar that needs no AgentIdentity credential. Drives oap agent setup-identity and projects the resolved credential into the sidecar's env at session boot via the toolbox: authkind.
status.resolvedSidecarToolboxes[].spec.version *stringVersion is the spec author's version of this declaration.
status.resolvedSkillBundles[]objectResolvedSkillBundles lists the skills staged onto disk in the sandbox pod for this session -- every AgentSkill whose Target is sandbox/both AND that some ToolBundle's StageSkills names (directly or via "*"). A staged entry always carries a composed SKILL.md; supporting files ride along when the skill also has a bundle. A skill that is not staged (agent-targeted, or not named by any StageSkills) does not appear here at all -- it still reaches the agent via load_skill.
status.resolvedSkillBundles[].archiveDigeststringArchiveDigest is the sha256 content digest of the actual staged archive bytes (the ConfigMap's bundle.tar.gz key). The sandbox pod's mount-unpack init container verifies content against it before extracting, so a ConfigMap that drifted between staging and use fails the pod rather than silently running altered content.
status.resolvedSkillBundles[].canonicalName *stringCanonicalName is the skill's canonical name (the AgentClass opt-in key).
status.resolvedSkillBundles[].configMapName *stringConfigMapName is the per-session ConfigMap holding the bundle tarball.
status.resolvedSkillBundles[].digest *stringDigest is a cheap, no-I/O change-detection fingerprint over this skill's staging inputs (the composed SKILL.md content, plus the underlying Skill's bundle digest when one exists). It lets the operator skip re-reading the bundle store on an unchanged reconcile pass; it is NOT the hash of the staged archive's actual bytes -- see ArchiveDigest for that.
status.resolvedSkillBundles[].localName *stringLocalName is the AgentClass's AgentSkill.Name for this skill -- the name a disk-based consumer (Claude Code) actually discovers, because the sandbox pod builder mounts the staged bundle's CONTENT at /skills/<LocalName>/ (see pkg/controllers/agentsession/bundles.go's BuildBundleSession). It is deliberately NOT the same value as MountName below: LocalName only has to be unique within one AgentClass (enforced by validateSkillsSpec) and match the skill's own SKILL.md frontmatter name (enforced in resolveAndStageSkillBundles), whereas MountName has to be safe and unique as a Kubernetes object name/volume name across every repo and version a cluster ever sees -- two different scopes, two different values.
status.resolvedSkillBundles[].mountName *stringMountName is the sanitized, collision-free ConfigMap/volume name the staged bundle's ConfigMap and the pod's internal shared-unpack subPath use. It is NOT the sandbox-visible directory name a disk-based consumer discovers the skill under -- see LocalName for that.
status.resolvedWorkspaceSourceobjectResolvedWorkspaceSource snapshots the bound WorkspaceSource for a running session: the ref, the base PVC the overlay was cut from, and whether the cut completed. Frozen at session start (like ResolvedSidecarToolboxes).
status.resolvedWorkspaceSource.baseClaimName *stringBaseClaimName is the shared base PVC the session overlay was cut from.
status.resolvedWorkspaceSource.kindstringKind/Locator/Revision snapshot the bound source's driver + locator + git revision at session start, so the runner can build sync/apply commands.
status.resolvedWorkspaceSource.locatorstring
status.resolvedWorkspaceSource.overlayCutbooleanOverlayCut is true once the reflink cut of base -> the session workspace PVC has completed successfully.
status.resolvedWorkspaceSource.reconcileImagestringReconcileImage and ReconcileServiceAccount snapshot the git image and ServiceAccount the runner's sync_workspace/apply_workspace Job runs as. The operator resolves them from its own config (--materialize-image, --snapshot-service-account), so a cluster that digest-pins the base-materialize Job gets the same pinned image here without a separate flag. Empty ⇒ the runner falls back to its built-in defaults.
status.resolvedWorkspaceSource.reconcileServiceAccountstring
status.resolvedWorkspaceSource.ref *stringRef is the bound WorkspaceSource CR name.
status.resolvedWorkspaceSource.revisionstring
status.resultobjectResult is the agent's final summary and artifacts, set on success.
status.result.artifacts[]objectArtifacts are the durable outputs the agent produced.
status.result.artifacts[].description *stringDescription is the agent's one-line account of the artifact.
status.result.artifacts[].id *stringID is the handle an earlier tool call returned for the artifact, written by the terminal tool that ended the round. Which FORM of handle depends on who reads it back, and the two terminal tools say so in their own schemas. agent_work_complete's list is an audit note nothing resolves, so any handle the agent holds is legible there. return_result's is not: the SubagentRequest controller copies it to the delegating parent, which attaches it by asking the API server for the ArtifactRender it names — so only the ar-… render handle works, and an artifact-store id would fail at the moment the parent tried to deliver it.
status.result.summary *stringSummary is the agent's own account of what it did.
status.retryAttemptsinteger (int32)RetryAttempts counts the number of times the runner has entered AwaitingRetry (provider-error). Bumped by the runner's WriteAwaitingRetry; never reset. Used by channelsd's session_watcher as a dedup key for posting the Retry button (uid+RetryAttempts) so each re-entry posts exactly one button.
status.runDurationstringRunDuration is the session's cumulative ACTIVE run-time (excludes time parked waiting on a human), the accumulator behind budget.maxDuration. The runner seeds a new pod from this and flushes it back before the pod can sleep, so run-time survives the sleep/resume boundary. Unlike status.progress (which resets per pod), this is monotonic across the whole session.
status.runnerNotes[]objectRunnerNotes are timestamped diagnostics the runner appended for operators.
status.runnerNotes[].message *string
status.runnerNotes[].time *string (date-time)
status.runnerPodNamestringRunnerPodName is the runner Pod currently backing the session; empty once the pod has been reaped.
status.runnerRestartsinteger (int32)RunnerRestarts counts container restarts observed on the runner Pod.
status.satisfiedSecretOutputs[]objectSatisfiedSecretOutputs records secret-output handles the runner has captured and published to the per-session secret-output Secret (via the operator-mediated endpoint). Observability only: the value is never here.
status.satisfiedSecretOutputs[].handle *stringHandle is the secret-output handle the runner captured.
status.satisfiedSecretOutputs[].namestringName is the secret-output logical name (the toolspec's secretOutput.name). Used by the runner to enforce write-once per name without Secret-read RBAC.
status.satisfiedSecretOutputs[].secretName *stringSecretName is the per-session Secret key the value was written under.
status.satisfiedSecretOutputs[].writtenAtstring (date-time)WrittenAt is when the value landed in the Secret.
status.sidecarReachability[]objectSidecarReachability is the runner's per-session live-probe result for each sidecar it has probed (keyed by Name). Runner-owned; kept separate from ResolvedSidecarToolboxes (operator-owned, rebuilt each reconcile) so a merge write never wipes it. Empty until the runner probes a sidecar.
status.sidecarReachability[].name *stringName is the LLM-facing prefix (ResolvedSidecarToolbox.Name).
status.sidecarReachability[].observedAtstring (date-time)ObservedAt is when the runner last probed this sidecar.
status.sidecarReachability[].observedTools[]stringObservedTools is the tool names the live probe reported (sorted). The runtime source of truth, distinct from the SidecarToolbox CR's admission-time status.observedTools (which is not read at runtime).
status.sidecarReachability[].reachable *booleanReachable is true when the last live tools/list probe succeeded and the declared allowlist was satisfied.
status.sidecarReachability[].unreachablestringUnreachable is the probe/allowlist error when Reachable is false; empty when Reachable is true.
status.sleptAtstring (date-time)SleptAt is set when an Idle session's pods have been reaped (scaled to zero) and cleared when the session is woken. Distinct from LastIdleAt (which marks Idle entry): SleptAt marks that the compute footprint is gone. Diagnostic + drives the reap decision's "already slept" guard.
status.startApprovalParkedAtstring (date-time)StartApprovalParkedAt is when the session entered AwaitingStartApproval (parked on AnnotationStartApprovalRequestRef). The operator measures the start-approval deadline from here.
status.startFailureobjectStartFailure is set by channelsd when it detects a terminal pre-start failure (e.g. an authz-write failure on session creation). The operator reads it and produces the Failed state — channelsd never writes phase/failureReason/finishedAt itself.
status.startFailure.messagestringMessage is the optional human-readable description of the failure.
status.startFailure.reason *stringReason is the machine-readable failure code (mirrors the constants used for AgentSession status conditions, e.g. ReasonAgentSessionAuthzWriteFail).
status.startedAtstring (date-time)StartedAt is when the session first left Pending.
status.supersededBystringSupersededBy names the AgentSession that forked from this one via Restart-from-here. Set when phase transitions to Succeeded due to a fork (rather than natural completion). Diagnostic only.
status.toolGuardobjectToolGuard reflects live tool-guard enforcement: currently-open circuit breakers. Patched by the runner on breaker transitions.
status.toolGuard.openBreakers[]objectOpenBreakers lists the circuits currently denying calls; empty means none.
status.toolGuard.openBreakers[].key *stringKey is the breaker key: "tool/<name>" or "origin/<kind>/<name>".
status.toolGuard.openBreakers[].openedAtstring (date-time)OpenedAt is when this circuit last opened.
status.toolGuard.openBreakers[].retryAtstring (date-time)RetryAt is when the cool-off elapses and calls are admitted again.
status.toolGuard.openBreakers[].tripsinteger (int32)Trips counts consecutive opens without an intervening success.
* required