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
| Field | Type | Description |
|---|---|---|
spec.agentIdentity | string | AgentIdentity overrides AgentClass.spec.agentIdentity; a per-bundle agentIdentity still wins over this. |
spec.budget | object | Budget optionally narrows the AgentClass budget. Each dimension must be <= the AgentClass cap. |
spec.budget.maxDelegatedAgents | integer (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 * | string | MaxDuration 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.sessionExpiration | string | SessionExpiration 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 * | string | Class names the AgentClass in the same namespace. |
spec.forkedAtTurn | integer (int32) | ForkedAtTurn is the index of the last turn copied from ForkedFrom (inclusive). The new user text became turn ForkedAtTurn+1. |
spec.forkedFrom | string | ForkedFrom 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.inputChannel | object | InputChannel 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 * | []string | Capabilities 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.external | map[string]string | External 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.inheritFrom | string | InheritFrom 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 * | string | Key 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 * | string | Kind denormalizes Channel.spec.kind for fast filtering without a secondary lookup. |
spec.inputChannel.name * | string | Name is the Channel CR name (same namespace as the AgentSession). |
spec.inputChannel.natsSubjectPrefix * | string | NATSSubjectPrefix 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.routingMode | string | RoutingMode 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.outputChannel | object | OutputChannel 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 * | []string | Capabilities 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.external | map[string]string | External 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.inheritFrom | string | InheritFrom 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 * | string | Key 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 * | string | Kind denormalizes Channel.spec.kind for fast filtering without a secondary lookup. |
spec.outputChannel.name * | string | Name is the Channel CR name (same namespace as the AgentSession). |
spec.outputChannel.natsSubjectPrefix * | string | NATSSubjectPrefix 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.routingMode | string | RoutingMode 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.parent | object | Parent 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 * | string | Name is the object's name. |
spec.parent.namespace * | string | Namespace is the object's namespace. |
spec.prompt * | object | Prompt is the initial user message. Immutable after the session transitions out of Pending. |
spec.prompt.configMapRef | object | ConfigMapKeyRef points at a single key inside a ConfigMap in the same namespace as the referencing CR. |
spec.prompt.configMapRef.key * | string | Key is the data key within the ConfigMap. |
spec.prompt.configMapRef.name * | string | Name is the ConfigMap's name. |
spec.prompt.inline | string |
Status
Status is controller-owned (observed state).
| Field | Type | Description |
|---|---|---|
status.activeWidgets | []object | ActiveWidgets 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 * | string | ArtifactID 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[].origin | string | Origin 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[].rendererKind | string | RendererKind 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[].tool | string | Tool is the MCP tool name (tool.UIResourceSpec.Tool) that produced the widget. |
status.agentWakeCredit | integer | AgentWakeCredit 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.appliedInteractPermission | string | AppliedInteractPermission 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.appliedInteractPermissionAt | string (date-time) | AppliedInteractPermissionAt is when the snapshot was captured. |
status.auditChainHeads | map[string]string | AuditChainHeads 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.auditKeyID | string | AuditKeyID is the key fingerprint (hex SHA-256 prefix) — matches the keyId in each signed entry's provenance. |
status.auditPublicKey | string | AuditPublicKey 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.awaitingUserInputSince | string (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 | []object | BundleSessions is one entry per tool bundle the AgentSession reconciler provisioned a SpiceboxSession for. Empty when the class declares none. |
status.bundleSessions[].agentIdentity * | string | AgentIdentity is the identity resolved for this bundle's sandbox. |
status.bundleSessions[].name * | string | Name is the bundle name from AgentClass.spec.toolBundles. |
status.bundleSessions[].restarts | integer (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[].retriedSessionUID | string | RetriedSessionUID 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 * | string | SpiceboxSessionName is the sandbox session provisioned for the bundle. |
status.closureDenied | boolean | ClosureDenied 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 | []object | CompletionBypasses 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 | []string | Details is the per-requirement account of what was missing, in the same order as Requirements. |
status.completionBypasses[].reason * | string | Reason is the agent's stated justification. AGENT-AUTHORED and UNTRUSTED — a surface showing it to a person renders it inert. |
status.completionBypasses[].requirements | []string | Requirements 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 | []object | Conditions 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 * | string | message is a human readable message indicating details about the transition. This may be an empty string. |
status.conditions[].observedGeneration | integer (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 * | string | reason 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 * | string | status of the condition, one of True, False, Unknown. (enum: True | False | Unknown) |
status.conditions[].type * | string | type of condition in CamelCase or in foo.example.com/CamelCase. |
status.credentialAuthFailures | []object | CredentialAuthFailures 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[].count | integer (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[].observedAt | string (date-time) | ObservedAt is when the runner recorded this observation. |
status.credentialAuthFailures[].origin * | string | Origin 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.effectiveIdentityMode | string | EffectiveIdentityMode 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.effectiveSettings | object | EffectiveSettings 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 | []object | AllowedMCP is the effective MCPServer ceiling; nil is unconstrained. |
status.effectiveSettings.allowedMCP[].name * | string | Name is the permitted MCPServer CR name. |
status.effectiveSettings.allowedMCP[].tools | []string | Tools narrows to specific tool names; nil, empty, or ["*"] allows all. |
status.effectiveSettings.allowedSandboxKinds | []string | AllowedSandboxKinds is the effective ceiling across constraining tiers. Empty means unconstrained. |
status.effectiveSettings.allowedSkills | []string | AllowedSkills is the union of allow-skill patterns across constraining tiers (nil = unconstrained). Informational; the gate is the resolver. |
status.effectiveSettings.allowedToolkits | []string | AllowedToolkits is the effective toolkit ceiling; nil is unconstrained. |
status.effectiveSettings.authz | object | Authz is the resolved set of human-in-the-loop timeouts. |
status.effectiveSettings.authz.approvalTimeout | string | ApprovalTimeout is how long a decision may wait for a human before the gate's timeout policy fires. |
status.effectiveSettings.authz.informationLeakageApprovalTTL | string | InformationLeakageApprovalTTL is how long an approved share may be reused before the agent must ask again. |
status.effectiveSettings.authz.metaagent | object | Metaagent 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.trigger | string | Trigger 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.planGate | object | PlanGate 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 | []object | Examples 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 * | []object | Phases 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 * | string | ID is the phase's identifier, as update_plan takes it. |
status.effectiveSettings.authz.planGate.examples[].phases[].label | string | Label is the human phrase for the phase. |
status.effectiveSettings.authz.planGate.examples[].phases[].permissions | []string | Permissions 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 | []string | Slots 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 * | string | Task 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.limits | object | Limits 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.maxPermissionsPerPhase | integer (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.maxPhases | integer (int32) | (default: 50) |
status.effectiveSettings.authz.planGate.limits.maxSlotsPerPhase | integer (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.mode | string | Mode 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.rendering | object | Rendering bounds what a single approval card shows or auto-approves. |
status.effectiveSettings.authz.planGate.rendering.maxAutoApproveHandles | integer (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.maxNamedApprovers | integer (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.maxSingleCardHandles | integer (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.requirePlan | boolean | RequirePlan 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.scopeMaxLlmLatencyMs | integer (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.budget | object | Budget is the resolved per-session budget, clamped to every tier ceiling. |
status.effectiveSettings.budget.maxDelegatedAgents | integer (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 * | string | MaxDuration 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.sessionExpiration | string | SessionExpiration 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 | []object | ContentInspectors 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 * | string | ID must resolve in the contentguard registry (e.g. "url-allowlist"). |
status.effectiveSettings.deniedSkills | []string | DeniedSkills is the union of deny-skill patterns across tiers. |
status.effectiveSettings.model | object | Model is the resolved model the session runs against. |
status.effectiveSettings.model.apiKey | object | APIKey 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 * | string | Key is the data key within the Secret holding the value. |
status.effectiveSettings.model.apiKey.name * | string | Name is the Secret's name, in the referring object's own namespace. |
status.effectiveSettings.model.fromCatalog | string | FromCatalog 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.name | string | |
status.effectiveSettings.model.provider | string | (enum: anthropic | openai | openrouter | test) |
status.effectiveSettings.model.routingMetadata | object | RoutingMetadata 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.allowFallbacks | boolean | |
status.effectiveSettings.model.routingMetadata.allowedModels | []string | Auto-router hints — apply when the model is "openrouter/auto". |
status.effectiveSettings.model.routingMetadata.costQualityTradeoff | number | CostQualityTradeoff 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.dataCollection | string | (enum: allow | deny) |
status.effectiveSettings.model.routingMetadata.ignore | []string | |
status.effectiveSettings.model.routingMetadata.maxPrice | object | OpenRouterMaxPrice 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.completion | number | (min 0) |
status.effectiveSettings.model.routingMetadata.maxPrice.prompt | number | (min 0) |
status.effectiveSettings.model.routingMetadata.models | []string | |
status.effectiveSettings.model.routingMetadata.only | []string | |
status.effectiveSettings.model.routingMetadata.order | []string | |
status.effectiveSettings.model.routingMetadata.requireParameters | boolean | RequireParameters: 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.sort | string | (enum: price | throughput | latency) |
status.effectiveSettings.modelInputPerMTok | number | ModelInputPerMTok/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.modelOutputPerMTok | number | ModelOutputPerMTok is the output half of the same catalog price. |
status.effectiveSettings.modelRouting | object | ModelRouting 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.allowFallbacks | boolean | |
status.effectiveSettings.modelRouting.allowedModels | []string | Auto-router hints — apply when the model is "openrouter/auto". |
status.effectiveSettings.modelRouting.costQualityTradeoff | number | CostQualityTradeoff 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.dataCollection | string | (enum: allow | deny) |
status.effectiveSettings.modelRouting.ignore | []string | |
status.effectiveSettings.modelRouting.maxPrice | object | OpenRouterMaxPrice 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.completion | number | (min 0) |
status.effectiveSettings.modelRouting.maxPrice.prompt | number | (min 0) |
status.effectiveSettings.modelRouting.models | []string | |
status.effectiveSettings.modelRouting.only | []string | |
status.effectiveSettings.modelRouting.order | []string | |
status.effectiveSettings.modelRouting.requireParameters | boolean | RequireParameters: 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.sort | string | (enum: price | throughput | latency) |
status.effectiveSettings.modelTokenSource | object | ModelTokenSource 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 * | string | Key is the data key within the Secret holding the value. |
status.effectiveSettings.modelTokenSource.name * | string | Name is the Secret's name. |
status.effectiveSettings.modelTokenSource.namespace * | string | Namespace is the Secret's namespace. |
status.effectiveSettings.nativeFileHandling * | boolean | NativeFileHandling is the resolved Tier-2 provider-native file handling grant (security-sensitive; default false). See SettingsLimits.NativeFileHandling. |
status.effectiveSettings.pinning | object | Pinning 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.cluster | object | Cluster is the cluster-tier policy, kept un-folded. |
status.effectiveSettings.pinning.cluster.bypass | []object | Bypass exempts named dependencies from this tier's rules and lower. |
status.effectiveSettings.pinning.cluster.bypass[].kind * | string | Kind is the pinning registry kind this exemption applies to. |
status.effectiveSettings.pinning.cluster.bypass[].name * | string | Name 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 * | string | Reason is the required audit-trail justification for the exemption. |
status.effectiveSettings.pinning.cluster.rules | []object | Rules is at most one requirement per dependency kind. |
status.effectiveSettings.pinning.cluster.rules[].kind * | string | Kind is the pinning registry kind name this rule governs. |
status.effectiveSettings.pinning.cluster.rules[].minStrength | string | MinStrength 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[].mode | string | Mode 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.namespace | object | Namespace is the namespace-tier policy, kept un-folded. |
status.effectiveSettings.pinning.namespace.bypass | []object | Bypass exempts named dependencies from this tier's rules and lower. |
status.effectiveSettings.pinning.namespace.bypass[].kind * | string | Kind is the pinning registry kind this exemption applies to. |
status.effectiveSettings.pinning.namespace.bypass[].name * | string | Name 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 * | string | Reason is the required audit-trail justification for the exemption. |
status.effectiveSettings.pinning.namespace.rules | []object | Rules is at most one requirement per dependency kind. |
status.effectiveSettings.pinning.namespace.rules[].kind * | string | Kind is the pinning registry kind name this rule governs. |
status.effectiveSettings.pinning.namespace.rules[].minStrength | string | MinStrength 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[].mode | string | Mode 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.provenance | map[string]string | Provenance maps a resolved field to the tier that supplied it (cluster|namespace|class|session|clamped). |
status.effectiveSettings.reportSessionCost * | boolean | ReportSessionCost is the resolved end-of-session cost-estimate toggle. |
status.effectiveSettings.requireStandingFor | []string | RequireStandingFor 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 * | boolean | RequireSubagentDigestPins is the resolved delegation requirement (default false): when true, the SubagentRequest controller refuses delegation through any unpinned roster entry. See SettingsLimits.RequireSubagentDigestPins. |
status.effectiveSettings.sandbox | map[string]object | Sandbox 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.toolGuard | object | ToolGuard preserves the per-tier tool-guard policies + folded ceiling. Per-tool rule resolution happens in the runner at session start. |
status.effectiveSettings.toolGuard.ceiling | object | Ceiling is the strictest-across-tiers bound, pre-folded. |
status.effectiveSettings.toolGuard.ceiling.maxCalls | integer (int32) | MaxCalls caps calls in the sliding Window; both must be set together. (min 1) |
status.effectiveSettings.toolGuard.ceiling.maxCallsPerTurn | integer (int32) | MaxCallsPerTurn / MaxCalls+Window impose rate ceilings even when no lower-tier rule configures a rate limit. (min 1) |
status.effectiveSettings.toolGuard.ceiling.maxEgressBytes | integer (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.maxFailureThreshold | integer (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.maxIngressBytes | integer (int64) | MaxIngressBytes is the inbound half of the same per-call byte ceiling. (min 1) |
status.effectiveSettings.toolGuard.ceiling.maxUIIngressBytes | integer (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.minAction | string | MinAction 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.minInitialCoolOff | string | MinInitialCoolOff raises the effective initial cool-off (max wins). Note: MaxCoolOff deliberately has no floor yet. |
status.effectiveSettings.toolGuard.ceiling.rateBounds | []object | RateBounds 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 * | string | Window 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.window | string | Window is the sliding-window span for MaxCalls; both must be set together. |
status.effectiveSettings.toolGuard.cluster | object | Cluster is the cluster-tier policy, kept un-folded for the rule walk. |
status.effectiveSettings.toolGuard.cluster.rules | []object | Rules are evaluated in order, first match wins; empty means this tier contributes nothing and the walk falls through. |
status.effectiveSettings.toolGuard.cluster.rules[].breaker | object | Breaker configures the circuit breaker; nil means matched tools get none. |
status.effectiveSettings.toolGuard.cluster.rules[].breaker.action | string | Action 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.failureThreshold | integer (int32) | FailureThreshold is consecutive Execute failures (per tool) that open the breaker. (min 1) |
status.effectiveSettings.toolGuard.cluster.rules[].breaker.initialCoolOff | string | InitialCoolOff 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.maxCoolOff | string | MaxCoolOff caps the exponential-backoff cool-off. Default 10m. |
status.effectiveSettings.toolGuard.cluster.rules[].breaker.originFailureThreshold | integer (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[].dataLimit | object | DataLimit caps per-call byte volume; nil means no byte cap. |
status.effectiveSettings.toolGuard.cluster.rules[].dataLimit.action | string | Action 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.maxEgressBytes | integer (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.maxIngressBytes | integer (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.maxUIIngressBytes | integer (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 * | object | Match selects the tools this rule governs. |
status.effectiveSettings.toolGuard.cluster.rules[].match.kind | string | Kind 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.origin | string | Origin 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.tool | string | Tool is a glob over the LLM-visible tool name (e.g. "github_*"). |
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit | object | RateLimit caps call volume; nil means matched tools get no rate limit. |
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.action | string | Action 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.maxCalls | integer (int32) | MaxCalls caps calls in the sliding Window; both must be set together. (min 1) |
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.maxCallsPerTurn | integer (int32) | MaxCallsPerTurn caps calls within a single agent turn; 0 is unlimited. (min 1) |
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.window | string | Window is the sliding-window span for MaxCalls; both must be set together. |
status.effectiveSettings.toolGuard.namespace | object | Namespace is the namespace-tier policy, kept un-folded for the rule walk. |
status.effectiveSettings.toolGuard.namespace.rules | []object | Rules are evaluated in order, first match wins; empty means this tier contributes nothing and the walk falls through. |
status.effectiveSettings.toolGuard.namespace.rules[].breaker | object | Breaker configures the circuit breaker; nil means matched tools get none. |
status.effectiveSettings.toolGuard.namespace.rules[].breaker.action | string | Action 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.failureThreshold | integer (int32) | FailureThreshold is consecutive Execute failures (per tool) that open the breaker. (min 1) |
status.effectiveSettings.toolGuard.namespace.rules[].breaker.initialCoolOff | string | InitialCoolOff 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.maxCoolOff | string | MaxCoolOff caps the exponential-backoff cool-off. Default 10m. |
status.effectiveSettings.toolGuard.namespace.rules[].breaker.originFailureThreshold | integer (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[].dataLimit | object | DataLimit caps per-call byte volume; nil means no byte cap. |
status.effectiveSettings.toolGuard.namespace.rules[].dataLimit.action | string | Action 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.maxEgressBytes | integer (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.maxIngressBytes | integer (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.maxUIIngressBytes | integer (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 * | object | Match selects the tools this rule governs. |
status.effectiveSettings.toolGuard.namespace.rules[].match.kind | string | Kind 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.origin | string | Origin 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.tool | string | Tool is a glob over the LLM-visible tool name (e.g. "github_*"). |
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit | object | RateLimit caps call volume; nil means matched tools get no rate limit. |
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.action | string | Action 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.maxCalls | integer (int32) | MaxCalls caps calls in the sliding Window; both must be set together. (min 1) |
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.maxCallsPerTurn | integer (int32) | MaxCallsPerTurn caps calls within a single agent turn; 0 is unlimited. (min 1) |
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.window | string | Window is the sliding-window span for MaxCalls; both must be set together. |
status.estimatedCost | object | EstimatedCost is the best-effort end-of-session USD cost estimate. Runner-owned; written at SessionEnd when reportSessionCost is on. |
status.estimatedCost.amountMicroUSD | integer (int64) | AmountMicroUSD is the session total in micro-USD (1e-6 USD); 0 and meaningless when PricingKnown is false. |
status.estimatedCost.asOf | string (date-time) | AsOf is when the estimate was computed. |
status.estimatedCost.byModel | []object | ByModel 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[].amountMicroUSD | integer (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[].inputTokens | integer (int64) | InputTokens is this model's share of prompt tokens. |
status.estimatedCost.byModel[].model * | string | Model is the served-model display id verbatim (provider/model, or a provider-prefixed served model under routing) — never re-parsed here. |
status.estimatedCost.byModel[].outputTokens | integer (int64) | OutputTokens is this model's share of completion tokens. |
status.estimatedCost.byModel[].pricingKnown | boolean | PricingKnown 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.currency | string | Currency is the ISO code the amount is denominated in ("USD"). |
status.estimatedCost.model | string | Model is the configured model the session ran under. |
status.estimatedCost.pricingKnown | boolean | PricingKnown is false when the model had no price; the amount is then 0 and must not be shown as a real cost. |
status.failureReason | string | FailureReason is the human-readable reason phase became Failed. |
status.finishedAt | string (date-time) | FinishedAt is when the session reached a terminal phase. |
status.identityChoiceParkedAt | string (date-time) | IdentityChoiceParkedAt is when the session entered AwaitingIdentityChoice. The operator measures IdentityChoiceTimeout from here. |
status.inputRequest | object | InputRequest 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.pending | boolean | Pending is true while request number Exchange has not been answered either way — filled, or refused. |
status.inputRequest.slot * | string | Slot is the child-side slot name it wants filled. |
status.inputRequest.why * | string | Why 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 | []string | InteractorTuplesWritten 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.lastIdleAt | string (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.lastUIServeAt | string (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.lastWakeAt | string (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.observedGeneration | integer (int64) | ObservedGeneration is the spec generation this status reflects. |
status.observedPins | []object | ObservedPins 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 * | string | Name identifies the dependency: MCPServer ref name, SidecarToolbox name, Toolkit name, or canonical skill name. |
status.observedPins[].pin * | object | Pin is the identity observed at session start; drift is this against the definition CR's baseline. |
status.observedPins[].pin.details | map[string]string | Details carries kind-specific extras (toolCount, registry host, …). |
status.observedPins[].pin.digest | string | Digest is the immutable identity: git sha, sha256:… image digest, canonical tool-manifest hash, or binary hash. |
status.observedPins[].pin.kind * | string | Kind 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.observedAt | string (date-time) | ObservedAt is when the recording controller observed this identity. |
status.observedPins[].pin.strength * | string | Strength is the syntactic pin strength of the declared ref. (enum: frozen | named | unpinned) |
status.observedPins[].pin.version | string | Version is the human-readable identity: tag, serverInfo.version, or version-probe output. |
status.parentExchange | object | ParentExchange 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.pending | boolean | Pending 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.question | string | Question 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.passthroughCredHashes | map[string]string | PassthroughCredHashes 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 | []object | PendingInteractions 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[].agentDisplayName | string | AgentDisplayName is the operator-configured AgentClass label, forwarded from the request payload for restart-resilient display. |
status.pendingInteractions[].approverSubject | string | ApproverSubject 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 * | string | Category 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 * | string | RequestID is the interaction RequestRef — the map key + the handle echoed on the eventual interaction_applied. |
status.pendingInteractions[].requestRef | string | RequestRef 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[].summary | string | Summary 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 | []object | PendingRequesters 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[].category | string | Category 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[].channelKey | string | ChannelKey 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[].email | string | Email is the user's email when the kind has it. |
status.pendingRequesters[].externalId * | string | ExternalID is the per-kind user id (Slack U-id, ...). |
status.pendingRequesters[].kind * | string | Kind is the channel kind ("slack", future "discord", ...). |
status.pendingRequesters[].messageText | string | MessageText 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 * | string | RequestRef 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[].teamScope | string | TeamScope is the per-kind workspace/server scope (Slack team ID, ...). |
status.pendingRestart | object | PendingRestart 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.inheritHistory | boolean | InheritHistory 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.mode | string | Mode 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.newOwnerExternalID | string | NewOwnerExternalID 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 * | string | NewUserText 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.signature | object | Signature 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 * | string | KeyID 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 * | string | Publisher 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 * | string | TargetSessionName 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 * | string | TriggeredBy 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 | []object | PermissionSurface 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 * | string | Handle is the durable, opaque reference to the permission class. |
status.permissionSurface[].stateImpact * | string | StateImpact 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 | []string | Tools 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.phase | string | Phase is the lifecycle phase: Pending, Running, Idle, one of the Awaiting* park states, Succeeded, or Failed. |
status.pinnedMessage | object | PinnedMessage is the runner's projection of the triggered session's opening message; channelsd renders it onto the anchored opening line. |
status.pinnedMessage.badge | string | Badge 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.body | string | Body is the agent-authored enrichment, last-write-wins; may be empty. |
status.pinnedMessage.link | string | Link is the delivered-result view link, set on conclusion; may be empty. |
status.progress | object | Progress accumulates turn, token and tool counts for the CURRENT pod; unlike RunDuration it resets when the pod is replaced. |
status.progress.cacheCreationTokens | integer (int64) | CacheCreationTokens is cumulative cache-write tokens (priced ~1.25x input). |
status.progress.cacheReadTokens | integer (int64) | CacheReadTokens is cumulative cache-read tokens (priced ~0.1x input). |
status.progress.inputTokens | integer (int64) | InputTokens is cumulative prompt tokens sent to the provider. |
status.progress.outputTokens | integer (int64) | OutputTokens is cumulative completion tokens returned by the provider. |
status.progress.toolCallCount | integer (int32) | ToolCallCount is how many tool calls the agent has issued. |
status.progress.turnCount | integer (int32) | TurnCount is how many agent turns have completed. |
status.resolvedContentGuardDetectors | []object | ResolvedContentGuardDetectors snapshots the detector sidecars injected for content-guard inspectors (e.g. prompt-injection). Empty ⇒ none. |
status.resolvedContentGuardDetectors[].healthPath | string | HealthPath is the readiness path; empty means the pod has no probe path. |
status.resolvedContentGuardDetectors[].image * | string | Image is the detector container image. |
status.resolvedContentGuardDetectors[].inspector * | string | Inspector is the content-guard inspector this detector serves. |
status.resolvedContentGuardDetectors[].podIP | string | PodIP is the detector pod's IP, reflected once Ready. Empty ⇒ not ready; the operator requeues and holds runner-pod creation. |
status.resolvedContentGuardDetectors[].podName | string | PodName is the detector pod's name. |
status.resolvedContentGuardDetectors[].port * | integer (int32) | Port is the detector's HTTP port on its pod. |
status.resolvedSidecarToolboxes | []object | ResolvedSidecarToolboxes 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[].awaitingSecret | boolean | AwaitingSecret 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 | []string | EffectiveAllowedHosts 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[].effectiveNetworkMode | string | EffectiveNetworkMode 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 * | string | Name is the LLM-facing prefix (the name field from AgentClassSidecarToolboxRef). |
status.resolvedSidecarToolboxes[].podFailure | object | PodFailure 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.exitCode | integer (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.message | string | Message 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 * | string | Reason 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 * | string | Ref is the SidecarToolbox CR name (the ref field from AgentClassSidecarToolboxRef). |
status.resolvedSidecarToolboxes[].runMode | string | RunMode is "in-pod" (default, container in the agent pod) or "separate-pod" (secret-gated: its own per-session pod). |
status.resolvedSidecarToolboxes[].secretTokenHash | string | SecretTokenHash is a hash of the resolved secret value; a change triggers pod replacement. |
status.resolvedSidecarToolboxes[].sidecarPodIP | string | SidecarPodIP is the separate sidecar pod's IP, reflected once Ready; the runner reaches it at http://<SidecarPodIP>:<port>. |
status.resolvedSidecarToolboxes[].sidecarPodName | string | SidecarPodName is the separate sidecar pod's name (RunMode=="separate-pod"). |
status.resolvedSidecarToolboxes[].spec * | object | Spec is the SidecarToolbox spec snapshot at session-start time. |
status.resolvedSidecarToolboxes[].spec.config | string | Config 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.intent | string | Intent is the one-line purpose shown to authors and reviewers. |
status.resolvedSidecarToolboxes[].spec.isolation | string | Isolation 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.mcpUiAppTools | object | MCPUIAppTools 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 * | boolean | Enabled turns the capability on; no app-visible tool is callable without it. |
status.resolvedSidecarToolboxes[].spec.mcpUiAppTools.maxCallsPerMin | integer (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 * | string | Name is the toolbox's toolspec-level name. |
status.resolvedSidecarToolboxes[].spec.sandbox * | object | Sandbox is the SpiceboxClass shape and egress the sidecar runs under. |
status.resolvedSidecarToolboxes[].spec.sandbox.class * | string | Class names a SpiceboxClass (cluster-scoped) whose resources, runtimeClassName, default network mode, pidsLimit are inherited. |
status.resolvedSidecarToolboxes[].spec.sandbox.network | object | Network widens the class's egress for this toolbox. |
status.resolvedSidecarToolboxes[].spec.sandbox.network.allowedHosts | []string | AllowedHosts 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 | []object | SecretInputs 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 * | string | Deliver 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 * | string | From 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 * | string | Name is the env-var name (when Deliver=="env") or the logical key. |
status.resolvedSidecarToolboxes[].spec.source * | object | Source is where the sidecar's container image comes from. |
status.resolvedSidecarToolboxes[].spec.source.image | string | Image is a prebuilt container image reference. |
status.resolvedSidecarToolboxes[].spec.source.inline | object | Inline builds the sidecar from a script layered onto a base image. |
status.resolvedSidecarToolboxes[].spec.source.inline.baseImage * | string | BaseImage is the image the script runs on top of. |
status.resolvedSidecarToolboxes[].spec.source.inline.entrypoint * | []string | Entrypoint is the argv that starts the server inside the container. |
status.resolvedSidecarToolboxes[].spec.source.inline.script * | object | Script is where the server's source comes from. |
status.resolvedSidecarToolboxes[].spec.source.inline.script.configMapRef * | object | ConfigMapRef names the ConfigMap key holding the script body. |
status.resolvedSidecarToolboxes[].spec.source.inline.script.configMapRef.key * | string | Key is the data key within the ConfigMap. |
status.resolvedSidecarToolboxes[].spec.source.inline.script.configMapRef.name * | string | Name is the ConfigMap's name, in the toolbox's own namespace. |
status.resolvedSidecarToolboxes[].spec.spicedbSchema | object | SpiceDBSchema 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.rawZed | string | RawZed 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 | []object | Resources 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[].approverPermission | string | ApproverPermission 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[].display | object | Display 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.icon | string | Icon 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.label | string | Label 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.name | string | Name 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 * | string | Name is the SpiceDB definition name; the composer dedupes on it. |
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].permissions | []object | Permissions are the resource's computed permissions. |
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].permissions[].expr * | string | Expr is the right-hand side of permission <name> = …, pasted verbatim. |
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].permissions[].name * | string | Name is the permission name. |
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].permissions[].planningNote | string | PlanningNote 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[].title | string | Title 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 | []object | Relations are the resource's relations, emitted verbatim. |
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].relations[].name * | string | Name is the relation name as it appears in the composed schema. |
status.resolvedSidecarToolboxes[].spec.spicedbSchema.resources[].relations[].subjectType * | string | SubjectType 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[].wildcard | boolean | Wildcard, when true, makes the relation accept ANY subject of SubjectType — emitted as relation <name>: <subjectType>:* 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 * | string | Standing 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 | []object | ToolResourceMap 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[].noTaint | boolean | NoTaint 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[].reads | object | Reads 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.bypassRequesterCheck | boolean | BypassRequesterCheck, 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.idArg | string | IDArg 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 * | string | Permission is the SpiceDB permission checked against the requester (e.g. "view"). |
status.resolvedSidecarToolboxes[].spec.toolResourceMap[].reads.resourceType * | string | ResourceType is the SpiceDB definition name (e.g. "linear_issue"). |
status.resolvedSidecarToolboxes[].spec.toolResourceMap[].reads.resultIDField | string | ResultIDField 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 * | string | Tool is the MCP tool name this mapping applies to. |
status.resolvedSidecarToolboxes[].spec.tools * | []object | Tools mirrors MCPServerTool field-for-field — same allowlist, CEL, deny effects, sensitive fields. See MCPServerTool for details. |
status.resolvedSidecarToolboxes[].spec.tools[].args | object | Args is the argument allowlist and CEL constraints for this tool. |
status.resolvedSidecarToolboxes[].spec.tools[].args.allowedFields | []string | AllowedFields 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 | []object | Constraints are CEL predicates every call's args must satisfy (AND). |
status.resolvedSidecarToolboxes[].spec.tools[].args.constraints[].cel * | string | CEL is a boolean expression over the call's args; false denies the call. |
status.resolvedSidecarToolboxes[].spec.tools[].args.constraints[].message | string | Message is the denial text shown when CEL evaluates false. |
status.resolvedSidecarToolboxes[].spec.tools[].args.sensitiveFields | []string | SensitiveFields names arg keys to redact from logs and approval prompts. |
status.resolvedSidecarToolboxes[].spec.tools[].args.unconstrainedArgs | boolean | UnconstrainedArgs 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[].deny | object | Deny lists effect and trust capabilities that refuse the call pre-dispatch. |
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects | object | Effects denies calls by declared effect (destructive, reads, writes). |
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects.creds | object | Creds denies credential-touching effects. |
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects.creds.writes | boolean | Writes denies any tool that would write credential material. |
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects.destructive | boolean | Destructive denies any tool the server marks destructive. |
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects.reads | []string | Reads names resource kinds whose reads are denied. |
status.resolvedSidecarToolboxes[].spec.tools[].deny.effects.writes | []string | Writes names resource kinds whose writes are denied. |
status.resolvedSidecarToolboxes[].spec.tools[].deny.trust | object | Trust denies calls by asserted SEP-1913 trust capability. |
status.resolvedSidecarToolboxes[].spec.tools[].deny.trust.destinationPublic | boolean | DestinationPublic denies the call when it would write somewhere publicly visible. |
status.resolvedSidecarToolboxes[].spec.tools[].deny.trust.outcomesIrreversible | boolean | OutcomesIrreversible denies the call when the server declares its effects cannot be undone. |
status.resolvedSidecarToolboxes[].spec.tools[].deny.trust.sourceUntrustedPublic | boolean | SourceUntrustedPublic denies the call when it would read untrusted public content into the agent's context. |
status.resolvedSidecarToolboxes[].spec.tools[].descriptionOverride | string | DescriptionOverride replaces the server's own description in the prompt; empty keeps what the server sent. |
status.resolvedSidecarToolboxes[].spec.tools[].effects | object | Effects is the declared effect profile used for approval and gating. |
status.resolvedSidecarToolboxes[].spec.tools[].effects.destructive | boolean | Destructive means a call can remove or overwrite upstream state. |
status.resolvedSidecarToolboxes[].spec.tools[].effects.idempotent | boolean | Idempotent means repeating a call has the same effect as making it once. |
status.resolvedSidecarToolboxes[].spec.tools[].effects.openWorld | boolean | OpenWorld means the call may reach systems beyond the named server. |
status.resolvedSidecarToolboxes[].spec.tools[].effects.readOnly | boolean | ReadOnly means a call mutates nothing upstream. |
status.resolvedSidecarToolboxes[].spec.tools[].intent | string | Intent is the one-line purpose shown to authors and reviewers. |
status.resolvedSidecarToolboxes[].spec.tools[].labels | []object | Labels 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[].forEach | string | ForEach 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 * | object | Label is the (resourceType, id, name) CEL triple. All three fields are required CEL string expressions. |
status.resolvedSidecarToolboxes[].spec.tools[].labels[].label.id * | string | ID is a CEL string expression yielding the resource id. |
status.resolvedSidecarToolboxes[].spec.tools[].labels[].label.name * | string | Name is a CEL string expression yielding the friendly label shown to approvers. Capped + sanitized at extraction time. |
status.resolvedSidecarToolboxes[].spec.tools[].labels[].label.resourceType * | string | ResourceType is a CEL string expression yielding the SpiceDB definition name (e.g. "crm_company"). |
status.resolvedSidecarToolboxes[].spec.tools[].labels[].when | string | When is a CEL boolean expression with args, result in scope; the block is skipped if it evaluates false. |
status.resolvedSidecarToolboxes[].spec.tools[].name * | string | Name is the tool name as the upstream server reports it. |
status.resolvedSidecarToolboxes[].spec.tools[].observes | []object | Observes 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]string | Facts maps a fact name to a CEL expression yielding its value, evaluated against the same item as Subjects. |
status.resolvedSidecarToolboxes[].spec.tools[].observes[].forEach | string | ForEach 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 * | []object | Subjects 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 * | string | ResourceID is a CEL string expression yielding the object id. |
status.resolvedSidecarToolboxes[].spec.tools[].observes[].subjects[].resourceType * | string | ResourceType is a CEL string expression yielding a SpiceDB definition name (commonly a literal, e.g. "github_pr"). |
status.resolvedSidecarToolboxes[].spec.tools[].observes[].when | string | When is a CEL boolean over args, result; the block is skipped if it evaluates false. Unset means always taken. |
status.resolvedSidecarToolboxes[].spec.tools[].permission | object | Permission declares the per-tool authz policy. Optional in the schema only: a tool without one fails AgentClass-time validation. |
status.resolvedSidecarToolboxes[].spec.tools[].permission.check | object | PermissionCheck 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.enforceMode | string | EnforceMode controls deny-finality under permissive toolAuthMode. Empty → EnforceInherit (slice-1 default). |
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.extractionPrompt | string | ExtractionPrompt, 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 | []string | GrantBindsArgs, 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 * | string | Permission 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.resourceIDExpr | string | ResourceIDExpr 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 {arg} interpolation, the Expr form for nested-arg extraction. |
status.resolvedSidecarToolboxes[].spec.tools[].permission.check.resourceIDHint | string | ResourceIDHint 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.resourceIDTemplate | string | ResourceIDTemplate 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 | []string | ResourceIDTransforms 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 * | string | ResourceType 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 * | string | StateImpact describes the policy mode for a tool / subcommand. |
status.resolvedSidecarToolboxes[].spec.tools[].permission.toolName | string | ToolName 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 | []object | PermissionVariants 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 * | object | Check 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.check | object | PermissionCheck 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.enforceMode | string | EnforceMode controls deny-finality under permissive toolAuthMode. Empty → EnforceInherit (slice-1 default). |
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.extractionPrompt | string | ExtractionPrompt, 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 | []string | GrantBindsArgs, 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 * | string | Permission 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.resourceIDExpr | string | ResourceIDExpr 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 {arg} interpolation, the Expr form for nested-arg extraction. |
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.check.resourceIDHint | string | ResourceIDHint 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.resourceIDTemplate | string | ResourceIDTemplate 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 | []string | ResourceIDTransforms 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 * | string | ResourceType 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 * | string | StateImpact describes the policy mode for a tool / subcommand. |
status.resolvedSidecarToolboxes[].spec.tools[].permissionVariants[].check.toolName | string | ToolName 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 * | string | When is a CEL boolean expression with args in scope. |
status.resolvedSidecarToolboxes[].spec.tools[].trust | object | Trust is the SEP-1913 trust + action-security annotation snapshot. Mirrors mcpspec.Trust. |
status.resolvedSidecarToolboxes[].spec.tools[].trust.attribution | []string | Attribution names the parties the server credits for the tool. |
status.resolvedSidecarToolboxes[].spec.tools[].trust.inputMetadata | object (free-form) | InputMetadata is the server's per-argument security annotations, verbatim. |
status.resolvedSidecarToolboxes[].spec.tools[].trust.maliciousActivityHint | boolean | MaliciousActivityHint 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.returnMetadata | object (free-form) | ReturnMetadata is the server's per-result security annotations, verbatim. |
status.resolvedSidecarToolboxes[].spec.tools[].visibility | []string | Visibility 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 | []object | WritesRelationships 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[].exclusive | boolean | Exclusive 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[].forEach | string | ForEach 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[].requireSlotBound | boolean | RequireSlotBound 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 * | object | Tuple holds the CEL expressions that compose the SpiceDB relationship tuple. |
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[].tuple.relation * | string | Relation 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 * | string | Resource 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 * | string | Subject is a CEL string expression that must evaluate to a SpiceDB object reference of the form "<type>:<id>". |
status.resolvedSidecarToolboxes[].spec.tools[].writesRelationships[].when | string | When 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 * | object | Transport is how the runner reaches the sidecar's MCP endpoint. |
status.resolvedSidecarToolboxes[].spec.transport.healthcheck | object | Healthcheck is how the operator's probe decides the sidecar is up. |
status.resolvedSidecarToolboxes[].spec.transport.healthcheck.path | string | Path is the HTTP path probed for readiness. (default: /healthz) |
status.resolvedSidecarToolboxes[].spec.transport.healthcheck.timeoutSeconds | integer (int32) | TimeoutSeconds bounds how long the probe waits for the sidecar to answer. (default: 30; min 1) |
status.resolvedSidecarToolboxes[].spec.transport.path | string | Path 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.port | integer (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 * | object | UpstreamAuth is the credential the sidecar needs for its own upstream. |
status.resolvedSidecarToolboxes[].spec.upstreamAuth.envVar | string | EnvVar 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 * | string | Provider 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 * | string | Version is the spec author's version of this declaration. |
status.resolvedSkillBundles | []object | ResolvedSkillBundles 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[].archiveDigest | string | ArchiveDigest 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 * | string | CanonicalName is the skill's canonical name (the AgentClass opt-in key). |
status.resolvedSkillBundles[].configMapName * | string | ConfigMapName is the per-session ConfigMap holding the bundle tarball. |
status.resolvedSkillBundles[].digest * | string | Digest 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 * | string | LocalName 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 * | string | MountName 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.resolvedWorkspaceSource | object | ResolvedWorkspaceSource 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 * | string | BaseClaimName is the shared base PVC the session overlay was cut from. |
status.resolvedWorkspaceSource.kind | string | Kind/Locator/Revision snapshot the bound source's driver + locator + git revision at session start, so the runner can build sync/apply commands. |
status.resolvedWorkspaceSource.locator | string | |
status.resolvedWorkspaceSource.overlayCut | boolean | OverlayCut is true once the reflink cut of base -> the session workspace PVC has completed successfully. |
status.resolvedWorkspaceSource.reconcileImage | string | ReconcileImage 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.reconcileServiceAccount | string | |
status.resolvedWorkspaceSource.ref * | string | Ref is the bound WorkspaceSource CR name. |
status.resolvedWorkspaceSource.revision | string | |
status.result | object | Result is the agent's final summary and artifacts, set on success. |
status.result.artifacts | []object | Artifacts are the durable outputs the agent produced. |
status.result.artifacts[].description * | string | Description is the agent's one-line account of the artifact. |
status.result.artifacts[].id * | string | ID 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 * | string | Summary is the agent's own account of what it did. |
status.retryAttempts | integer (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.runDuration | string | RunDuration 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 | []object | RunnerNotes are timestamped diagnostics the runner appended for operators. |
status.runnerNotes[].message * | string | |
status.runnerNotes[].time * | string (date-time) | |
status.runnerPodName | string | RunnerPodName is the runner Pod currently backing the session; empty once the pod has been reaped. |
status.runnerRestarts | integer (int32) | RunnerRestarts counts container restarts observed on the runner Pod. |
status.satisfiedSecretOutputs | []object | SatisfiedSecretOutputs 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 * | string | Handle is the secret-output handle the runner captured. |
status.satisfiedSecretOutputs[].name | string | Name 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 * | string | SecretName is the per-session Secret key the value was written under. |
status.satisfiedSecretOutputs[].writtenAt | string (date-time) | WrittenAt is when the value landed in the Secret. |
status.sidecarReachability | []object | SidecarReachability 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 * | string | Name is the LLM-facing prefix (ResolvedSidecarToolbox.Name). |
status.sidecarReachability[].observedAt | string (date-time) | ObservedAt is when the runner last probed this sidecar. |
status.sidecarReachability[].observedTools | []string | ObservedTools 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 * | boolean | Reachable is true when the last live tools/list probe succeeded and the declared allowlist was satisfied. |
status.sidecarReachability[].unreachable | string | Unreachable is the probe/allowlist error when Reachable is false; empty when Reachable is true. |
status.sleptAt | string (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.startApprovalParkedAt | string (date-time) | StartApprovalParkedAt is when the session entered AwaitingStartApproval (parked on AnnotationStartApprovalRequestRef). The operator measures the start-approval deadline from here. |
status.startFailure | object | StartFailure 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.message | string | Message is the optional human-readable description of the failure. |
status.startFailure.reason * | string | Reason is the machine-readable failure code (mirrors the constants used for AgentSession status conditions, e.g. ReasonAgentSessionAuthzWriteFail). |
status.startedAt | string (date-time) | StartedAt is when the session first left Pending. |
status.supersededBy | string | SupersededBy 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.toolGuard | object | ToolGuard reflects live tool-guard enforcement: currently-open circuit breakers. Patched by the runner on breaker transitions. |
status.toolGuard.openBreakers | []object | OpenBreakers lists the circuits currently denying calls; empty means none. |
status.toolGuard.openBreakers[].key * | string | Key is the breaker key: "tool/<name>" or "origin/<kind>/<name>". |
status.toolGuard.openBreakers[].openedAt | string (date-time) | OpenedAt is when this circuit last opened. |
status.toolGuard.openBreakers[].retryAt | string (date-time) | RetryAt is when the cool-off elapses and calls are admitted again. |
status.toolGuard.openBreakers[].trips | integer (int32) | Trips counts consecutive opens without an intervening success. |