AgentClass

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

AgentClass is the reviewable template an agent is defined by: model, system prompt, tools, skills, identity, channels, budget and authz policy. It never runs on its own -- an AgentSession is one instantiation of it.

Namespaced. Reconciled by pkg/controllers/agentclass, which validates the spec and confirms referenced objects exist, then reports Valid=True/False. The AgentSession reconciler gates on Valid=True, so an invalid class fails a session closed rather than starting it degraded.

Spec

FieldTypeDescription
spec.agentIdentitystringAgentIdentity in same namespace; default for all bundles.
spec.agentUIobjectAgentUI references the AgentUI this class serves and carries the DEPLOYMENT half of the tool grant. It lives here, not on AgentUI, because the bundle author writes the request and the deployer writes the authorization — different objects, so no two field managers contend. Absent ⇒ no UI-callable tools.
spec.agentUI.grantedTools[]stringGrantedTools authorizes these tool names for direct browser invocation. This is the ONLY place a deployment says yes. Note the asymmetry that makes it matter: a readonly tool auto-runs at click frequency with no human in the loop, while a side-effecting one already takes an approval on every call — so this grant carries the most weight for readonly tools. The item pattern is the SAME one AgentUI.spec.tools carries — see that field for why both are pinned to synthesize.NormalizeName's output alphabet. A grant and a request that cannot be spelled differently cannot silently disagree. As there: this holds for objects written AFTER this upgrade only, since a CRD pattern is not retroactive, so the runner's normalize step MUST NOT be deleted as redundant.
spec.agentUI.ref *stringRef is the AgentUI name in the same namespace.
spec.authzobjectAuthz consolidates tool-call, session-interact, and information-leakage authorization policy. Optional; absent means legacy defaults apply.
spec.authz.approvalTimeoutstringApprovalTimeout is the consolidated approval-WAIT deadline for every human-in-the-loop authz gate on this class: the tool-call approval, the information-leakage share approval, and the cold-start scope approval all source their wait from this single value. Defaults to 10 minutes — long enough for an approver to click the Slack block. This is the approval-WAIT timeout only; it does NOT cover liveness watchdogs (budget.maxDuration, per-turn, silence, maxLlmLatencyMs).
spec.authz.crossAgentThreadsobjectCrossAgentThreads lets another agent's messages reach this one inside a human's thread. Off by default.
spec.authz.crossAgentThreads.enabledbooleanEnabled admits ANOTHER agent's messages. It never admits this agent's own posts, in any configuration — that is a self-loop, and the listener decides self before it decides bot for exactly that reason.
spec.authz.crossAgentThreads.wakeBudgetintegerWakeBudget is how many agent-driven wakes this session accepts before a HUMAN must speak again. Each human turn refills it; each agent-driven wake spends one. ZERO — including unset — means agent→agent wakes are OFF, never unlimited. The unsafe reading of an unset budget here is "no ceiling", which is precisely the loop this bounds. Seeing is not bounded by it. A message from another agent ALWAYS appends to this session's inbox; the budget governs only whether it also WAKES the agent. So a session out of credit still sees the whole conversation and answers when a person next speaks. (min 0; max 32)
spec.authz.informationLeakageobjectInformationLeakage gates outbound channel messages against tainted resources accessed by the agent during a session.
spec.authz.informationLeakage.approvalTTLstringApprovalTTL is the lifetime of a SpiceDB grant written on approve.
spec.authz.informationLeakage.loggingNoticeToRequesterbooleanLoggingNoticeToRequester sends an ephemeral notice to the requester on every would-block event while Mode=logging.
spec.authz.informationLeakage.modestringMode controls how the gate reacts when a potential leak is detected. (enum: enforcing | logging | disabled; default: enforcing)
spec.authz.informationLeakage.onUnsupportedChannelstringOnUnsupportedChannel determines binding-time behavior when a Channel's kind has no AudienceResolver capability. (enum: blockBinding | logOnly | bypass; default: blockBinding)
spec.authz.informationLeakage.singleUserBypassbooleanSingleUserBypass allows SingleUser-capability channels to skip the gate (the audience is trivially the requester themselves).
spec.authz.metaagentobjectMetaagent configures WHEN the metaagent classifies a turn. Defaults to mention-only; inline is opt-in per the field comment.
spec.authz.metaagent.triggerstringTrigger is when the metaagent classifies a turn, and whether it acts. mention (default) — ambient off; explicit @metaagent only. shadow — classify every turn, apply nothing, record it. inline — classify every turn and act. (enum: mention | shadow | inline; default: mention)
spec.authz.ownerCeilingobjectOwnerCeiling optionally bounds what bound Channels may declare as the session owner. Absent (the common case) ⇒ no ceiling.
spec.authz.ownerCeiling.fixedstringFixed pins the owner to this subject/subject-set ref regardless of Channel policy. Mutually exclusive with StarterOnly.
spec.authz.ownerCeiling.starterOnlybooleanStarterOnly forbids Channels from declaring explicit/foreign-group owners — owner must resolve to the session's starting user.
spec.authz.planGateobjectPlanGate gates permissioned tool calls on membership in an approved plan's active phase. Off by default. A class may set a mode STRICTER than the tier default freely; it may only go below the tier's SettingsLimits.MinPlanGateMode if no floor is set.
spec.authz.planGate.examples[]objectExamples are worked plans this class's author supplies, rendered after the one the runtime derives from the session's own permission surface. Optional, and most classes want none: the derived example already speaks the agent's own handles and shows the shape that costs one approval. These are for a class whose GOOD plan is not obvious from its surface alone — where the order of phases matters, or where two resource types must be named together, or where the natural split is by audience rather than by read-then-write. They are TRUSTED text, at the same level as spec.systemPrompt: an operator authors them, never the agent. But unlike the system prompt they are checked — every handle an example names must exist on this class's own surface, or the class goes Valid=False naming the offender. The prompt tells agents that a handle off the declarable list is silently dropped, and an EXAMPLE carrying such a handle would teach exactly that failure to every plan the agent writes. Measured, planners transcribe these examples closely, so a wrong one is not inert.
spec.authz.planGate.examples[].phases *[]objectPhases are the plan itself. Rendered as the JSON an agent would pass to update_plan, because that is the form it copies.
spec.authz.planGate.examples[].phases[].id *stringID is the phase's identifier, as update_plan takes it.
spec.authz.planGate.examples[].phases[].labelstringLabel is the human phrase for the phase.
spec.authz.planGate.examples[].phases[].permissions[]stringPermissions are the wire-format handles this phase declares ("perm:<permission>:<resourceType>" or "tool:<name>"). Each is checked against the class's surface at admission.
spec.authz.planGate.examples[].phases[].slots[]stringSlots are the resource TYPES this phase names an instance of. Types only: an authored example must not carry a concrete instance id, because a planner that copies one would address an object belonging to whoever the example was written about.
spec.authz.planGate.examples[].task *stringTask is the one-line request this plan answers — the "when you are asked to X" half. Without it an example shows a shape with no occasion, and the agent cannot tell which of several examples applies.
spec.authz.planGate.limitsobjectLimits bound plan size. The defaults are deliberately expansive: they exist to stop a runaway or hostile plan from DoS-ing the approver and producing an unrenderable card, NOT to shape normal authoring.
spec.authz.planGate.limits.maxPermissionsPerPhaseinteger (int32)MaxPermissionsPerPhase is naturally bounded by the permission surface (a phase cannot declare a handle that does not exist), so this is a backstop rather than a real constraint. (default: 64)
spec.authz.planGate.limits.maxPhasesinteger (int32)(default: 50)
spec.authz.planGate.limits.maxSlotsPerPhaseinteger (int32)MaxSlotsPerPhase bounds the instance axis, which is genuinely unbounded (a thread can carry many URLs), hence the larger default. (default: 128)
spec.authz.planGate.modestringMode selects how much of the gate runs. disabled — off entirely; no prompt change, no plan schema surfaced. logging — the gate runs in full: enumeration, ceiling computation, approver resolution, severity, card rendering and the append-only write. What it holds back is CEILINGS and APPROVALS — the card is logged instead of published, so nothing is approved and no grant is written, and a call that overruns a declared phase is RECORDED rather than refused. A call made with NO plan at all is still refused (see RequirePlan). That is not an exception to the mode so much as its precondition: without a declared plan there are no ceilings to record and the mode measures nothing. enforcing — the card is published and the call blocks on the outcome. (enum: disabled | logging | enforcing; default: disabled)
spec.authz.planGate.renderingobjectRendering bounds what a single approval card shows or auto-approves.
spec.authz.planGate.rendering.maxAutoApproveHandlesinteger (int32)MaxAutoApproveHandles bounds the UNION of all handles auto-approved without a human across a session — the tier-0 budget. (default: 8)
spec.authz.planGate.rendering.maxNamedApproversinteger (int32)MaxNamedApprovers is the largest approver population rendered by name before falling back to a count or a channel-supplied description. (default: 5)
spec.authz.planGate.rendering.maxSingleCardHandlesinteger (int32)MaxSingleCardHandles bounds a phase ceiling shown on one card; a phase exceeding it is split rather than folded behind "+N more", so the approver sees what they approve. (default: 16)
spec.authz.planGate.requirePlanbooleanRequirePlan denies every permissioned call made before a plan is declared. Without it, an agent that simply never calls update_plan falls through the implicit-phase rule and is unconstrained — i.e. "don't plan" is a total bypass. It bites in LOGGING mode too, which is the one place this differs from everything else the gate does. "You must declare a plan" and "you must stay within its ceiling" are separable claims; only the second is what logging mode holds back. Welding them together made logging unable to produce the dataset it exists for — agents told they MUST plan simply did not, because in logging nothing depended on it, and no wording fixes a consequence the code does not implement. So: with RequirePlan, a session under logging still refuses calls made with no plan at all, and still records-but-permits calls that overrun a declared ceiling. That yields real phases, ceilings, severity and approver data without switching on the enforcement the data is meant to justify. UNSET derives from Mode: required whenever the gate runs at all. Turning the gate on IS the intent to make agents plan, and leaving this an independent opt-in defaulting false meant a cluster could run mode: logging, look gated, and collect nothing — which is what happened across five live sessions, every call landing on the implicit phase because nothing required a plan. Explicit false remains an opt-out, because gate-on-but-observe-only is a real rollout stage: it records which handles calls actually resolve to without changing what the agent may do. Explicit true under mode: disabled is still false — a gate that does not run cannot refuse anything, and honouring it would promise a denial that never arrives. Pointer rather than a kubebuilder default: a static default cannot express "depends on another field", and it would erase the difference between unset and a deliberate false. UPGRADE NOTE. This field previously carried +kubebuilder:default=false, so every AgentClass created under the older CRD has an explicit false PERSISTED — which now reads as the observe-only opt-out rather than as unset. Such a class keeps the old behaviour after an upgrade and will not require a plan. Re-applying the class (or deleting the field) is what restores derivation; there is no way to distinguish a stamped default from a deliberate choice after the fact.
spec.authz.scopeobjectScope enables dynamic per-session permission scope. Off by default. When Enabled=false, tool dispatch behavior is identical to today (no Layer 2 check, no Layer 3 SpiceDB disallow tuples).
spec.authz.scope.coldStartstringColdStart governs how a genuinely-new session's first user message is handled: extractAndApprove — extract {scope, cleanedTask}; post the approval block; agent blocks until the approver resolves. extractAndAutoApply — extract; apply scope; run the cleaned task; no approval click. off — no cold-start extraction; run the raw first turn unmodified (mid-session @metaagent still works). Ignored when Enabled=false. (enum: extractAndApprove | extractAndAutoApply | off; default: extractAndApprove)
spec.authz.scope.composerModelOverridestringComposerModelOverride and ExtractorModelOverride allow per-class LLM model selection; empty falls back to the authzd default.
spec.authz.scope.enabledbooleanEnabled turns the feature on for sessions of this class. (default: false)
spec.authz.scope.extractorModelOverridestring
spec.authz.scope.maxLlmLatencyMsinteger (int32)MaxLLMLatencyMs caps the combined extractor+composer LLM latency before authzd gives up and posts a CannotAddressMessage. (default: 5000)
spec.authz.sessionobjectSession authorizes who may interact with sessions of this class.
spec.authz.session.allowedStarters[]stringAllowedStarters lists who may start a session of this class, as SpiceDB subject refs: "user:<canonical>" or a subject-set "group:<id>#member". Written by the AgentClass controller as agentclass#starter tuples; the set on the class IS the set in SpiceDB (entries removed here are deleted there). Empty means no start gate — anyone the channel admits may start, as before. Enforced at the AgentSession reconciler for EVERY entry path (channel, browser, delegation, restart): a session whose started_by does not hold the gate permission is failed before any runner pod exists, and the person is told. Values are canonical ids, never raw emails — a bundle install question canonicalizes on the way in. A start gate makes a class HUMAN-ENTRY-ONLY: every arm of the gate resolves a person, so a session nobody started — a cron, a webhook — is refused structurally. A class that declares an allowlist therefore cannot be bound to an input Channel that carries no person; the controller refuses that combination rather than let every such session fail. Removal is PROSPECTIVE. Dropping someone from this list deletes their standing to start NEW sessions; it does not end the ones already running, which keep their verdict as a status condition. Who may keep TALKING to a live session is OnlyStartersInteract's question, not this field's.
spec.authz.session.artifactVisibilitystringArtifactVisibility declares who may VIEW the artifacts produced by this class's sessions. "session" (the default, and what an empty value means) keeps today's audience: the session's interact set plus platform admins. "organization" opts the class's sessions into the org-wide audience — any user who can authenticate against the cluster's IdP may view the artifacts (view only: never the conversation, the plan, or send standing, and a session's deny list still beats it). Level-triggered, both ways: the AgentSession reconciler re-levels the agentsession#artifact_org_viewer wildcard tuple from this value on every reconcile — including reconciles of completed sessions — so flipping to "organization" exposes existing sessions' artifacts, and flipping back revokes org-wide access everywhere. (enum: session | organization)
spec.authz.session.interactPermissionstringInteractPermission is a SpiceDB subject-set ("<type>:<id>#<relation>", e.g. "group:engineering#member") written as agentsession#participant on every new session of this class. It WIDENS interact to that population; it never restricts the starter, who always holds interact as started_by. Mutually exclusive with OnlyStartersInteract.
spec.authz.session.onlyStartersInteractbooleanOnlyStartersInteract fixes the session interact set to this class's starters: the applied interact permission becomes "agentclass:<ns>/<name>#starter", so the existing Interact gate admits exactly AllowedStarters and nobody else — no channel-derived membership, no declared widening. Requires a non-empty AllowedStarters and an empty InteractPermission. Rename-safe: the subject-set is computed from the class's own name at use time, never stored.
spec.authz.session.platformAdminsMayStartbooleanPlatformAdminsMayStart, when false, makes the gate EXPLICIT: only AllowedStarters may start, and the platform-admin arm of agentclass#start_session does not apply (agentclass#start_explicit is checked instead). Admins who want to use such an agent list themselves. Default true. Meaningful only with a non-empty AllowedStarters — the controller refuses false with an empty list, since nobody could start.
spec.authz.slots[]objectSlots declares the resource TYPES this agent operates on, each gated by a permission — the instance axis of the two-axis ceiling. At session start defaults bind; per user message the runner extracts instances of these types, Checks each against SpiceDB, and adds authorized ones to the session's bound set. Bindings auto-fill matching tool args. Renamed and relocated from spec.boundEntities so the CRD, the plan gate, the approval flow and the SpiceDB grant all call it the same thing.
spec.authz.slots[].autoFillArgs[]object
spec.authz.slots[].autoFillArgs[].argName *string
spec.authz.slots[].autoFillArgs[].toolNamePatternstring
spec.authz.slots[].autoGrantFrom[]stringAutoGrantFrom names WHOSE contributions to a thread may bind this slot without an approval. It governs channel_thread seeding only. A thread is multi-author, so "found in the thread" would otherwise mean "anyone's value" — an SSRF/exfil surface. Unset means owner: only the session owner's contributions auto-bind, and everyone else's route to approval. participants trusts any thread member and is only defensible for a tightly-controlled channel. none trusts nobody, so every value the thread offers goes in front of a human. "Trust nobody" is the explicit value none rather than an empty list because an empty list is NOT representable here: omitempty drops it on serialization, so autoGrantFrom: [] reads back as unset and would silently become owner — a policy written to be restrictive quietly widening. When values disagree, the most restrictive wins.
spec.authz.slots[].defaults[]string
spec.authz.slots[].description *string
spec.authz.slots[].extractionPromptstring
spec.authz.slots[].fillFrom[]stringFillFrom names how an instance may come to occupy this slot. Unset means today's behavior: class-pinned defaults bind at session start and the extractor may propose instances — for every source EXCEPT trigger (below), an unset list narrows nothing. - default class-pinned IDs, bound at session start - query/extract the user names an instance in a message, unprompted - ask the agent prompts and waits, and the user names it in the reply. Binds through the SAME path as query — the two differ in who started the exchange, not in how the value binds — so declaring ask needs no extra tool: the agent asks with respond_to_user and waits with await_user_message. - channel_thread the channel seeds candidates from the thread it was minted from, governed by AutoGrantFrom - metaagent ambient scope intent, human-approved. NOT yet implemented as a binding path: a slot naming only metaagent binds nothing today. - observed a tool result recorded a fact about the instance. NOT yet implemented as a binding path: a slot naming only observed binds nothing today. - trigger the pipeline binds the instances the signed webhook delivery names, at session mint. The ONE source that is NEVER implied by an unset list: naming it puts a session-mint SpiceDB grant behind no human and no second gate, so a slot must name "trigger" here explicitly, or it binds nothing from a trigger however permissive (or absent) its other fillFrom entries read.
spec.authz.slots[].membershipstringMembership decides whether a grant follows the session's member set as it grows, or is pinned to the set present when it was approved. Defaults to frozen. The appealing argument for dynamic is that the approver grants to a SESSION and trusts its governance — but that only holds while session membership is hard to obtain, and it is not: thread adoption admits recent authors, and a channel-kind link can make one tuple mean an entire channel. Following a set that grows on its own is therefore an explicit choice, not the quiet default. Set dynamic where the session really is a small owned thread and the approval card has shown the approver the concrete subject set. (enum: dynamic | frozen; default: frozen)
spec.authz.slots[].permission *string
spec.authz.slots[].permissions[]stringPermissions are the ADDITIONAL permissions a grant on this slot may confer, beyond Permission. A slot used to be one (resourceType, permission) pair fixed at declaration time. That could not represent a real type: git_repo is reached by read, write, fetch AND push, and a plan that later needs read on a slot declared for push had nowhere to bind it. The approval was recorded, no grant could be written for it, and the very next call escalated again — a human clicking Approve on the same amendment forever. Every permission named here gets its own slot_grant_&lt;permission&gt; relation in the composed SpiceDB schema, which is what makes a grant for it writable at all. Declaring a permission does NOT grant it: a grant is still written only for what an approval actually named, still scoped to the session, still expiring and revocable. This is the CEILING of what an approval on this slot may ever bind.
spec.authz.slots[].requires[]objectRequires are predicates over the facts a candidate arrived with, all of which must be Satisfied before it may occupy this slot. Any Refused or Undetermined verdict holds the slot closed.
spec.authz.slots[].requires[].approvers[]stringApprovers routes the WAIVER card for a Refused verdict, as SpiceDB subject-set expressions (e.g. "agentsession:{ns}/{name}#approve"). The card asks a RISK question ("accept running untrusted code from a fork?"), which is not the same as "who may grant read on this resource" — and the resource's owner set is often empty for a userless session, which turns an appealable gate into an unappealable crash. Unset defaults to the slot's resolved standing.
spec.authz.slots[].requires[].cel *stringCEL is a boolean predicate over facts and slot. facts is addressed as facts.<provenance>.<name>, where provenance is envelope (derived by platform code from a signed provider payload) or observed (derived from a response to a call the agent shaped). They are deliberately separate namespaces: an author gating on the first must not silently receive the second. slot carries {resourceType, resourceID} of the candidate being decided. has() is NOT available over facts. It would let an author turn "not yet known" into a decidable boolean, collapsing the tri-state.
spec.authz.slots[].requires[].refusalMessage *stringRefusalMessage explains, in plain language, what was refused and why. It cannot be derived from CEL, and it must not be confused with the agent's own justification for a call — that is text the agent authored, and this is a gate the agent is subject to.
spec.authz.slots[].requires[].undeterminedHint *stringUndeterminedHint is shown to the AGENT while some referenced fact has not been recorded. It is the only party who can fix that, by making the call that establishes it — so this should name that call. Required, and the reason is recorded on PermissionCheck.ResourceIDHint: without one the agent sees a raw denial that reads as a system fault and tells it not to bother retrying.
spec.authz.slots[].resourceType *string
spec.authz.slots[].triggerInstancestringTriggerInstance is a CEL expression over the verified webhook delivery ({event: string, payload: dyn}) yielding the resource ID this slot binds at trigger time. The resource TYPE is always this slot's resourceType — the expression yields only the id half. When set it is authoritative for this slot; when unset, a channel kind that derives instances from the delivery (TriggerSlotProvider) supplies them. Compiled and validated at class admission; at delivery time an eval error, non-string, or empty result binds nothing. Meaningful only when fillFrom includes "trigger".
spec.authz.toolCallsobjectToolCalls authorizes per-tool dispatch.
spec.authz.toolCalls.modestring(enum: permissive | enforcing | disabled; default: enforcing)
spec.authz.toolCalls.subjectstring(enum: currentRequester | startedBy | both; default: currentRequester)
spec.authz.trifectaobjectTrifecta refuses a DELEGATION whose closure combines untrusted input, sensitive access and consequential action. Off by default. A property of the closure rather than of this class: a class that reads untrusted content is fine, one that can write is fine, and the delegation joining them is what this refuses.
spec.authz.trifecta.modestringMode selects how much of the check runs. disabled — off entirely. logging — legs are derived and the verdict recorded, nothing refused. Two-leg near-misses are the dataset this mode exists for. enforcing — a three-leg delegation is refused. Its OWN mode, and deliberately not reached through toolCalls.mode: a control that disappears when an operator disables a different permission check is not a control. An operator turning off tool-call authz has not thereby decided that untrusted content may drive a write. (enum: disabled | logging | enforcing)
spec.authz.trifecta.neverConsequentialbooleanNeverConsequential declares that this class must never hold leg C — it may read, but must never be able to act. A DECLARED ceiling validated at admission against the DERIVED permission surface, mirroring how budget already works: the class declares, the controller derives, and a contradiction marks the class Valid=False rather than being silently ignored. A class that declares this and holds a readwrite or external handle is a configuration mistake its author wants to hear about at apply time, not a delegation that quietly fails later.
spec.boundEntities[]objectBoundEntities is the PRE-RENAME spelling of spec.authz.slots. Still deserialized on purpose. This is an AUTHZ field: dropping it from the schema outright would make an upgraded AgentClass silently lose its instance constraints — nothing would fail, the agent would just be less constrained than its author wrote. Instead the reconciler rejects a class that still sets it, naming the new field, so the loss is impossible to miss. Loud beats silent when what is being dropped is a constraint. Deprecated: move to spec.authz.slots.
spec.boundEntities[].autoFillArgs[]object
spec.boundEntities[].autoFillArgs[].argName *string
spec.boundEntities[].autoFillArgs[].toolNamePatternstring
spec.boundEntities[].autoGrantFrom[]stringAutoGrantFrom names WHOSE contributions to a thread may bind this slot without an approval. It governs channel_thread seeding only. A thread is multi-author, so "found in the thread" would otherwise mean "anyone's value" — an SSRF/exfil surface. Unset means owner: only the session owner's contributions auto-bind, and everyone else's route to approval. participants trusts any thread member and is only defensible for a tightly-controlled channel. none trusts nobody, so every value the thread offers goes in front of a human. "Trust nobody" is the explicit value none rather than an empty list because an empty list is NOT representable here: omitempty drops it on serialization, so autoGrantFrom: [] reads back as unset and would silently become owner — a policy written to be restrictive quietly widening. When values disagree, the most restrictive wins.
spec.boundEntities[].defaults[]string
spec.boundEntities[].description *string
spec.boundEntities[].extractionPromptstring
spec.boundEntities[].fillFrom[]stringFillFrom names how an instance may come to occupy this slot. Unset means today's behavior: class-pinned defaults bind at session start and the extractor may propose instances — for every source EXCEPT trigger (below), an unset list narrows nothing. - default class-pinned IDs, bound at session start - query/extract the user names an instance in a message, unprompted - ask the agent prompts and waits, and the user names it in the reply. Binds through the SAME path as query — the two differ in who started the exchange, not in how the value binds — so declaring ask needs no extra tool: the agent asks with respond_to_user and waits with await_user_message. - channel_thread the channel seeds candidates from the thread it was minted from, governed by AutoGrantFrom - metaagent ambient scope intent, human-approved. NOT yet implemented as a binding path: a slot naming only metaagent binds nothing today. - observed a tool result recorded a fact about the instance. NOT yet implemented as a binding path: a slot naming only observed binds nothing today. - trigger the pipeline binds the instances the signed webhook delivery names, at session mint. The ONE source that is NEVER implied by an unset list: naming it puts a session-mint SpiceDB grant behind no human and no second gate, so a slot must name "trigger" here explicitly, or it binds nothing from a trigger however permissive (or absent) its other fillFrom entries read.
spec.boundEntities[].membershipstringMembership decides whether a grant follows the session's member set as it grows, or is pinned to the set present when it was approved. Defaults to frozen. The appealing argument for dynamic is that the approver grants to a SESSION and trusts its governance — but that only holds while session membership is hard to obtain, and it is not: thread adoption admits recent authors, and a channel-kind link can make one tuple mean an entire channel. Following a set that grows on its own is therefore an explicit choice, not the quiet default. Set dynamic where the session really is a small owned thread and the approval card has shown the approver the concrete subject set. (enum: dynamic | frozen; default: frozen)
spec.boundEntities[].permission *string
spec.boundEntities[].permissions[]stringPermissions are the ADDITIONAL permissions a grant on this slot may confer, beyond Permission. A slot used to be one (resourceType, permission) pair fixed at declaration time. That could not represent a real type: git_repo is reached by read, write, fetch AND push, and a plan that later needs read on a slot declared for push had nowhere to bind it. The approval was recorded, no grant could be written for it, and the very next call escalated again — a human clicking Approve on the same amendment forever. Every permission named here gets its own slot_grant_&lt;permission&gt; relation in the composed SpiceDB schema, which is what makes a grant for it writable at all. Declaring a permission does NOT grant it: a grant is still written only for what an approval actually named, still scoped to the session, still expiring and revocable. This is the CEILING of what an approval on this slot may ever bind.
spec.boundEntities[].requires[]objectRequires are predicates over the facts a candidate arrived with, all of which must be Satisfied before it may occupy this slot. Any Refused or Undetermined verdict holds the slot closed.
spec.boundEntities[].requires[].approvers[]stringApprovers routes the WAIVER card for a Refused verdict, as SpiceDB subject-set expressions (e.g. "agentsession:{ns}/{name}#approve"). The card asks a RISK question ("accept running untrusted code from a fork?"), which is not the same as "who may grant read on this resource" — and the resource's owner set is often empty for a userless session, which turns an appealable gate into an unappealable crash. Unset defaults to the slot's resolved standing.
spec.boundEntities[].requires[].cel *stringCEL is a boolean predicate over facts and slot. facts is addressed as facts.<provenance>.<name>, where provenance is envelope (derived by platform code from a signed provider payload) or observed (derived from a response to a call the agent shaped). They are deliberately separate namespaces: an author gating on the first must not silently receive the second. slot carries {resourceType, resourceID} of the candidate being decided. has() is NOT available over facts. It would let an author turn "not yet known" into a decidable boolean, collapsing the tri-state.
spec.boundEntities[].requires[].refusalMessage *stringRefusalMessage explains, in plain language, what was refused and why. It cannot be derived from CEL, and it must not be confused with the agent's own justification for a call — that is text the agent authored, and this is a gate the agent is subject to.
spec.boundEntities[].requires[].undeterminedHint *stringUndeterminedHint is shown to the AGENT while some referenced fact has not been recorded. It is the only party who can fix that, by making the call that establishes it — so this should name that call. Required, and the reason is recorded on PermissionCheck.ResourceIDHint: without one the agent sees a raw denial that reads as a system fault and tells it not to bother retrying.
spec.boundEntities[].resourceType *string
spec.boundEntities[].triggerInstancestringTriggerInstance is a CEL expression over the verified webhook delivery ({event: string, payload: dyn}) yielding the resource ID this slot binds at trigger time. The resource TYPE is always this slot's resourceType — the expression yields only the id half. When set it is authoritative for this slot; when unset, a channel kind that derives instances from the delivery (TriggerSlotProvider) supplies them. Compiled and validated at class admission; at delivery time an eval error, non-string, or empty result binds nothing. Meaningful only when fillFrom includes "trigger".
spec.budgetobjectBudget is optional: when omitted, the class inherits the resolved tier default budget (see status.effectiveSettings.budget).
spec.budget.maxDelegatedAgentsinteger (int32)MaxDelegatedAgents bounds the TOTAL number of sessions in one delegation tree, counting the root. It is a property of the tree rather than of any one session: every other dimension here caps a single session's spend, and a tree of N sessions each individually within budget still spends N times it. Resolved from the ROOT session's effective settings and enforced by the SubagentRequest controller before a child is created. Zero means UNSET, not unlimited. A consumer must apply its own built-in bound rather than reading zero as "no cap" — that reading is right for a DURATION (as MaxDuration uses it) and wrong for a COUNT, where it would license exactly the runaway fan-out this field exists to prevent. (min 0)
spec.budget.maxDuration *stringMaxDuration is the cumulative ACTIVE RUN-TIME budget (e.g. "30m") — the wall time the agent actually spent working, excluding time parked waiting on a human (the next user message or a tool-call approval). It persists across sleep/resume via status.runDuration; it is NOT wall-clock since session start. For a wall-clock lifetime cap, use SessionExpiration. Zero = no run-time cap.
spec.budget.maxTokens *integer (int64)(min 1)
spec.budget.maxTurns *integer (int32)(min 1)
spec.budget.sessionExpirationstringSessionExpiration is a hard wall-clock lifetime cap measured from status.startedAt (e.g. "24h"), regardless of how much the agent ran. The operator fails the session once exceeded, even while it is idle or asleep. Zero = never expires (no default). Distinct from MaxDuration, which counts only active run-time.
spec.capabilitiesobject (free-form)Capabilities grants optional meta-tool capabilities to the agent. A key present grants that capability; the value is its per-capability config (may be empty {}). Default-on capabilities (planning, channel interaction, …) are active without being listed; list one with {"enabled": false} to disable it. Unknown or unavailable grants are ignored gracefully and surfaced on status (CapabilitiesValid), never blocking readiness.
spec.channelsobjectChannels governs channel-attached behavior. Optional — kubectl-driven sessions ignore this entirely.
spec.channels.archiveAfterstringArchiveAfter is how long an Idle, channel-attached session waits before the operator transitions it to phase=Succeeded. After archive, the next channel inbound for the same Channel + key spawns a fresh AgentSession that inherits the prior session's memory turn-by-turn at creation. Defaults to the operator's --default-channel-archive-after flag (operator default: 4h). Set to 0 to disable archival (Idle indefinitely; useful for tests).
spec.channels.idleTTLstringIdleTTL is how long an idle, channel-attached runner blocks in await_user_message before exiting cleanly to phase=Idle. Default 5m. Zero disables — the runner exits as soon as the agent yields, useful for one-shot cron-driven agents. (default: 5m)
spec.channels.showAssistantStreambooleanShowAssistantStream renders the agent's live LLM stream (free-form text deltas + tool-use markers) into the channel placeholder while the agent is working. Default false. With this off, the only things that reach the channel between user turns are update_status (the rolling caption / thinking-bubble) and update_plan (the structured checklist). Turn on for debugging — the stream is verbose and includes the agent's internal narrative + every tool call. (default: false)
spec.channels.sleepAfterstringSleepAfter is how long an Idle, channel-attached session keeps its pods warm before the operator reaps ALL of them (sandboxes, runner, detector) while keeping the session Idle and wakeable. On the next inbound the pods are re-created lazily, re-mounting the shared workspace PVC. Must be well under ArchiveAfter. Defaults to the operator's --default-session-sleep-after flag (operator default: 10m). Set to 0 to keep pods warm for the whole Idle window (never sleep).
spec.channels.storageRetentionstringStorageRetention is how long a TERMINAL (Succeeded/Failed) session's workspace and snapshot-store PVCs are kept past FinishedAt before the operator deletes them. The volumes are re-creatable scratch — a woken or restarted session provisions fresh, empty ones on demand — and the session record itself (transcript, memory, relationships) is untouched. Defaults to the operator's --session-storage-retention flag (operator default: 72h; flag 0 disables the sweep cluster-wide). A per-class value here overrides the flag when > 0, same precedence ArchiveAfter follows.
spec.completionRequirements[]stringCompletionRequirements are the conditions a session of this class must satisfy before its agent may declare a round of work complete. Each entry is a key registered in the completion-requirement registry (pkg/agent/completion) — "artifact-delivered", "plan-steps-complete", "trigger-status-concluded". This is the OPERATOR's guarantee that no session of this class finishes without producing what the class exists to produce, and it deliberately does not depend on the agent choosing to declare anything: an agent that forgets its own obligation is precisely the case it covers. NOT the same axis as a plan phase's requires, which orders phase ENTRY and never reads an agent's assertion that it finished something. These are read at exactly that assertion. An unmet requirement makes agent_work_complete refuse, naming what is missing. The agent may still finish by supplying a bypass_reason, which is recorded on status.completionBypasses and posted to the session's channel — a bypass with no escape would turn a degraded result into a wedged session, and one nobody sees would be an off switch. Not enum-validated: the registry is open by design, and a key this build does not know fails closed at call time (refuse, name it, stay bypassable) rather than being silently ignored at admission.
spec.configobject (free-form)Config is a map of installer-bound values exposed to this class's toolspec constraint CEL as the config root (see the toolspec validator). It is generic and toolkit-agnostic: it never mentions repos or any tool-specific shape — a constraint decides what a value means. Each value is a JSON scalar or array. Validated against ConfigSchema by the AgentClass controller; a config that violates its schema, or that a bound toolspec's CEL references a key not declared here, marks the class Valid=False rather than failing at tool-call time.
spec.configSchema[]objectConfigSchema declares each Config key's contract, so Config can be validated at reconcile and every config.X a bound toolspec constraint references is known to exist. Mirrors the oap install-question shape, so a bundle's install question maps 1:1 onto a key.
spec.configSchema[].enum[]stringEnum is the permitted set: the allowed value when Type=enum, or the allowed item vocabulary when Type=stringList. Empty ⇒ open.
spec.configSchema[].name *stringName is the config key: the map key in spec.config and the identifier a toolspec constraint reads as config.&lt;Name&gt;.
spec.configSchema[].patternstringPattern is a regexp every string value (or stringList item) must match.
spec.configSchema[].requiredbooleanRequired fails validation when the key is absent from spec.config.
spec.configSchema[].type *stringType is the value's shape. (enum: string | stringList | int | bool | enum)
spec.credentialExplanations[]objectCredentialExplanations lets the AgentClass explain, per credential, why this agent needs it. Keyed by the FINAL (post-credentialRemap) credential name. Optional; credentials without an entry fall back to the LLM explainer (if configured) or a generic sentence for the why. Title/description are always sourced from the tool/provider catalog, never here.
spec.credentialExplanations[].credential *stringCredential is the final (post-remap) credential name this entry describes, e.g. "github-token".
spec.credentialExplanations[].reasonstringReason is the agent-specific "why we need this" sentence rendered under the credential's title.
spec.credentialLinkTimeoutstringCredentialLinkTimeout caps how long a userPassthrough session may sit in AwaitingCredentials before the operator fails it. Measured from max(parkedAt, lastInteractionAt). Default 30m. (default: 30m)
spec.descriptionstringDescription is a human-readable summary surfaced in events / status.
spec.displayNamestringDisplayName is the human-friendly label surfaced to end users (channel thread messages, approver prompts, audit logs). When unset, callers fall back to the AgentClass's metadata.name. Examples: "MarketingBot", "HubSpot Companies Agent".
spec.harnessstringHarness names the agent harness that runs this class's outer loop. Absent means "ap-native", the built-in runner loop. A named harness must be registered in the operator binary; an unknown value marks the AgentClass Valid=False rather than failing at pod-create time.
spec.identityChoiceTimeoutstringIdentityChoiceTimeout caps how long an ask|dynamic session may sit in AwaitingIdentityChoice before the operator fails it. Measured from status.identityChoiceParkedAt. Default 30m. (default: 30m)
spec.identityModestringIdentityMode selects how the agent's tool credentials are sourced. - agent (default): tools use the credentials bound to spec.agentIdentity. Today's behavior. - userPassthrough: tools use the credentials of the user who started the session, drawn from that user's UserIdentity catalog. spec.agentIdentity is ignored for credentialed tools. The session parks in AwaitingCredentials until the starter has linked everything the agent needs. - ask: interactive. At session start, the initiating user is asked to choose between acting as the agent (spec.agentIdentity) or as themselves (userPassthrough). Requires spec.agentIdentity. The session parks in AwaitingIdentityChoice until the user answers. - dynamic: interactive. An isolated recommender LLM (configured via spec.identityRecommendation) proposes agent or userPassthrough for the session, but the initiating user always confirms — the recommendation is advisory only. Requires spec.agentIdentity and spec.identityRecommendation.prompt. Also parks in AwaitingIdentityChoice until confirmed. (enum: agent | userPassthrough | ask | dynamic; default: agent)
spec.identityRecommendationobjectIdentityRecommendation configures the dynamic-mode recommender LLM. Required when identityMode=dynamic; ignored otherwise.
spec.identityRecommendation.prompt *stringPrompt is short class-authored guidance handed to the recommender LLM, e.g. "Prefer the agent's own identity in busy multi-person threads; prefer acting on the user's behalf when a single user works from fresh context." The recommender's output is advisory only — the user always confirms — so this prompt never authorizes anything by itself.
spec.mcpServers[]objectMCPServers references MCPServer CRs in the same namespace whose allowlisted tools should be exposed to the LLM during a session.
spec.mcpServers[].credentialRemapmap[string]stringCredentialRemap remaps a tool-declared credential name to a differently-named credential in the agent's identity catalog — the opt-in escape hatch when two tools declare the same credential name but need different tokens. Keyed by the tool's declared name.
spec.mcpServers[].name *stringName is the LLM-prefix; pattern [a-z0-9_-]{1,32}.
spec.mcpServers[].ref *stringRef names the MCPServer CR in the same namespace.
spec.modelobjectModel is optional: when omitted, the class inherits the resolved tier default model (see status.effectiveSettings.model). At least one of the class or a settings tier must supply a model before a session runs.
spec.model.apiKeyobjectAPIKey is a bring-your-own token. Only honored when allowModelOverride is granted (else a fatal violation). Catalog references leave this empty.
spec.model.apiKey.key *stringKey is the data key within the Secret holding the value.
spec.model.apiKey.name *stringName is the Secret's name, in the referring object's own namespace.
spec.model.fromCatalogstringFromCatalog references a model catalog entry by name. Mutually exclusive with an inline Provider/Name. When set, Provider and the token come from the catalog entry.
spec.model.namestring
spec.model.providerstring(enum: anthropic | openai | openrouter | test)
spec.model.routingMetadataobjectRoutingMetadata refines dynamic model selection for this agent. Merged over the resolved catalog entry's routing with narrowing-only semantics: an AgentClass may constrain, never loosen, admin policy. Inert unless the resolved model performs dynamic selection (Provider == "openrouter").
spec.model.routingMetadata.allowFallbacksboolean
spec.model.routingMetadata.allowedModels[]stringAuto-router hints — apply when the model is "openrouter/auto".
spec.model.routingMetadata.costQualityTradeoffnumberCostQualityTradeoff is intentionally unvalidated (no Minimum/Maximum marker): OpenRouter documents no formal range for this value — its docs example value is 3 — so a range marker would reject valid input.
spec.model.routingMetadata.dataCollectionstring(enum: allow | deny)
spec.model.routingMetadata.ignore[]string
spec.model.routingMetadata.maxPriceobjectOpenRouterMaxPrice caps per-model price, in USD per MILLION tokens (OpenRouter's max_price units; e.g. Prompt: 1 means at most $1/M prompt tokens). 0 on an axis means "no cap on this axis" (see pkg/platform/settings/routingmerge.go's tightenPrice and pkg/agent/llm/openrouter/extra.go's maxPriceObject), not "cap at $0". Only consulted when the owning ModelCatalogEntry's Provider == "openrouter".
spec.model.routingMetadata.maxPrice.completionnumber(min 0)
spec.model.routingMetadata.maxPrice.promptnumber(min 0)
spec.model.routingMetadata.models[]string
spec.model.routingMetadata.only[]string
spec.model.routingMetadata.order[]string
spec.model.routingMetadata.requireParametersbooleanRequireParameters: unset defaults to TRUE when the request carries tools (the openrouter provider injects it — see pkg/agent/llm/openrouter/extra.go's providerObject); explicit values are honored verbatim. OpenRouter's own wire default is false — agents need tools, so auto/fallback routing must only pick providers that support the request's parameters.
spec.model.routingMetadata.sortstring(enum: price | throughput | latency)
spec.sidecarToolboxes[]objectSidecarToolboxes references SidecarToolbox CRs in the same namespace whose tools should be exposed to the LLM. Each ref's Name is the LLM-facing prefix; pattern [a-z0-9_-]{1,32}.
spec.sidecarToolboxes[].name *stringName is the LLM-prefix; pattern [a-z0-9_-]{1,32}.
spec.sidecarToolboxes[].ref *stringRef names the SidecarToolbox CR in the same namespace.
spec.skills[]objectSkills lists the agent skills this class opts into. Subject to the tiered AllowedSkills/DeniedSkills ceilings, which match on Ref; a disallowed skill makes the class SettingsAccepted=False.
spec.skills[].name *stringName is the local handle, unique within this class, and constrained to [a-z0-9_-]{1,32} (validateSkillsSpec) because it is also the directory name under the mount path when this skill is staged — which is why a sandbox-targeted skill's Name must equal its SKILL.md frontmatter name; Claude Code discovers a skill only when those agree.
spec.skills[].ref *stringRef is the skill's canonical name, "<repo-locator>//<subpath>@<ref>".
spec.skills[].targetstringTarget says who consumes this skill. (enum: agent | sandbox | both; default: agent)
spec.subagentModesmap[string][]stringSubagentModes raises the delegation-mode ceiling for individual roster members, keyed by the AgentClass name exactly as it appears in subagents. Values are SubagentRequest modes -- single_turn, task, chat -- documented on SubagentRequestSpec.Mode, which is where what each one exposes is described. The ceiling sits HERE, on the delegating parent, and not on the child's own class: two parents may legitimately be trusted with different modes for the same child. A delegating agent names the mode it wants in its delegate call, and the SubagentRequest controller refuses any mode this map does not permit -- it may pick NARROWER than permitted, never wider, and a refusal is never downgraded to single_turn and run anyway. single_turn is permitted for every roster member without being listed, because it provisions strictly less than the other two (no channel at all). Nothing else is implied: chat does not imply task. A key naming a class absent from subagents, or a value that is not one of the three modes, is a configuration error rather than a silent no-op -- it marks the class Valid=False with reason RosterInvalid. There is no apiserver-level Enum on the values: controller-gen cannot put an items enum inside a map's additionalProperties ("must apply kubebuilder:validation:items:Enum to an array value, found object"), so the AgentClass controller is the one authority on what a valid value is.
spec.subagents[]stringSubagents is the closed set of AgentClass names this class may delegate to, in its own namespace. Membership is checked at delegation time and the whole graph is DAG-validated at admission, so a cycle is a validation error rather than a runtime loop — the only kind of bound that holds. Absent or empty means this class delegates to nobody, which is the default: delegation is opt-in. Membership alone permits single_turn delegation ONLY: a headless child that takes spec.prompt in and hands status.result back. Widening a member to one of the conversational modes is a separate, explicit declaration in subagentModes, so a roster written before those modes existed cannot silently gain the ability to spawn a child with a two-way channel. An entry may optionally carry a digest pin: "name@sha256:<64 lowercase hex>". Pinned entries name the exact digest of the AgentClass to delegate to; the SubagentRequest controller verifies the pin matches at delegation time. Only a properly formed @sha256:<64 hex> suffix is recognized as a pin; anything else stays part of the literal name (and the pattern below refuses it).
spec.systemPrompt *objectPromptSource carries exactly one of inline or configMapRef.
spec.systemPrompt.configMapRefobjectConfigMapKeyRef points at a single key inside a ConfigMap in the same namespace as the referencing CR.
spec.systemPrompt.configMapRef.key *stringKey is the data key within the ConfigMap.
spec.systemPrompt.configMapRef.name *stringName is the ConfigMap's name.
spec.systemPrompt.inlinestring
spec.toolBundles[]objectToolBundles is optional. An empty/missing list means the agent runs with only meta tools (e.g. agent_work_complete). Plan 1 supports only the empty case; Plan 2 wires sandbox tool dispatch.
spec.toolBundles[].agentIdentitystringAgentIdentity overrides spec.agentIdentity for this bundle.
spec.toolBundles[].class *stringClass names a SpiceboxClass (cluster-scoped).
spec.toolBundles[].credentialRemapmap[string]stringCredentialRemap remaps a tool-declared credential name to a differently-named credential in the agent's identity catalog — the opt-in escape hatch when two tools declare the same credential name but need different tokens. Keyed by the tool's declared name.
spec.toolBundles[].name *stringName is the LLM-facing prefix; pattern [a-z0-9_-]{1,32}.
spec.toolBundles[].sandboxobjectSandbox overrides the sandbox backend for this bundle, taking precedence over the referenced SpiceboxClass's own setting: the class states the bundle's default, and the agent consuming it may have a reason to run it elsewhere. Unset inherits the class's setting, or the tier defaults.
spec.toolBundles[].sandbox.configobject (free-form)Config is passed through to the backend verbatim. Opaque to AP: each kind parses its own. Unset for the built-in pod backend, which takes all of its configuration from the pod-vocabulary fields above.
spec.toolBundles[].sandbox.kindstringKind names a registered sandbox kind. Defaults to "pod". An UNRECOGNIZED kind marks the class Valid=False — lookup never falls back to the built-in backend, because silently installing a different substrate than the one asked for is worse than refusing. (default: pod)
spec.toolBundles[].sandbox.warmPoolobjectWarmPool requests pre-warmed capacity for this backend. Unset or replicas: 0 means pre-warming is off — it spends real money on idle capacity, so it is opt-in. Only a backend whose Runtime implements the sandboxkinds.Prewarmer interface can honor a non-zero value; asking for it on a backend that cannot is a class validation error, not a silent no-op. PREREQUISITE: pre-warming only actually adopts a pod when the cluster's agent-sandbox install allows the "agentprimitives.authzed.com" label domain (its AllowedLabelDomains defaults to "sandbox.users.io" alone). Without that grant AP cannot label an adopted pod, and — because AP's per-session NetworkPolicy selects on that label — the backend degrades to ordinary cold sandboxes and emits a monitoring warning rather than run one unpoliced.
spec.toolBundles[].sandbox.warmPool.namespaces[]stringNamespaces lists the namespaces to keep pre-warmed capacity in; a separate pool is created in EACH. SpiceboxClass is cluster-scoped but the pool objects are namespaced, and adoption only ever looks a pool up in the ADOPTING SESSION's own namespace — so a pool in a namespace nothing runs sessions in is pure idle spend nothing can adopt. Naming them explicitly rather than discovering them is deliberate: pre-warming spends real money, so the cluster owner states exactly where, as they state exactly how many. Replicas > 0 with an empty Namespaces is a class validation error, not a silent no-op.
spec.toolBundles[].sandbox.warmPool.replicasinteger (int32)Replicas is the number of sandboxes kept ready, PER NAMESPACE listed below. 0 disables pre-warming. (min 0)
spec.toolBundles[].stageSkills[]stringStageSkills names which of this AgentClass's sandbox-targeted skills get staged to disk, by their local AgentSkill.Name. "*" means all of them. Absent means none — the default, since most agents consume a skill through load_skill and never need it on disk. Enforced CLASS-WIDE, not per-bundle: the union of every ToolBundle's StageSkills in this class is staged onto EVERY bundle's sandbox session, so a bundle whose own StageSkills names nothing still receives whatever another bundle in the class opted in. A per-bundle mount, scoped to only the bundle that named it, is a possible future direction and not current behaviour.
spec.toolBundles[].toolspecs *[]stringToolspecs lists SpiceboxToolspec names that scope this bundle.
spec.toolGuardobjectToolGuard is this class's tool circuit-breaker / rate-limit rules, consulted first in the tier walk (overrides namespace/cluster defaults and the built-in rule for matching tools).
spec.toolGuard.rules[]objectRules are evaluated in order, first match wins; empty means this tier contributes nothing and the walk falls through.
spec.toolGuard.rules[].breakerobjectBreaker configures the circuit breaker; nil means matched tools get none.
spec.toolGuard.rules[].breaker.actionstringAction when the breaker denies: halt ends the session; deny returns an IsError tool_result; warn logs/audits but allows; off disables the breaker for matched tools. (enum: halt | deny | warn | off)
spec.toolGuard.rules[].breaker.failureThresholdinteger (int32)FailureThreshold is consecutive Execute failures (per tool) that open the breaker. (min 1)
spec.toolGuard.rules[].breaker.initialCoolOffstringInitialCoolOff is the first cool-off period after the breaker opens; it doubles on each successive trip up to MaxCoolOff. Default 30s.
spec.toolGuard.rules[].breaker.maxCoolOffstringMaxCoolOff caps the exponential-backoff cool-off. Default 10m.
spec.toolGuard.rules[].breaker.originFailureThresholdinteger (int32)OriginFailureThreshold is consecutive failures across ALL tools of the tool's origin that open the origin breaker (denying every sibling). (min 1)
spec.toolGuard.rules[].dataLimitobjectDataLimit caps per-call byte volume; nil means no byte cap.
spec.toolGuard.rules[].dataLimit.actionstringAction when a byte limit is exceeded: halt ends the session; deny errors the call (egress: tool not run; ingress: result withheld); warn logs/audits but allows. (enum: halt | deny | warn)
spec.toolGuard.rules[].dataLimit.maxEgressBytesinteger (int64)MaxEgressBytes caps the serialized tool-args size sent outbound per call. Exceeding it (at PreToolCall) applies Action; on deny the tool does not run. (min 1)
spec.toolGuard.rules[].dataLimit.maxIngressBytesinteger (int64)MaxIngressBytes caps the tool-result size returned inbound per call (any result, success or error). Exceeding it (at PostToolCall) applies Action; on deny the result is withheld (replaced with an IsError) so the oversized payload never reaches the model. (min 1)
spec.toolGuard.rules[].dataLimit.maxUIIngressBytesinteger (int64)MaxUIIngressBytes caps result bytes on an agent-UI DATA BINDING, whose result is rendered by a browser and never read by the model. Unset means the platform's browser-sized default applies (toolguard's DefaultUIIngressBytes) — NOT unlimited, and NOT MaxIngressBytes. Set this to bind the UI path tighter or looser than the platform default; tightening maxIngressBytes alone does not affect it. (min 1)
spec.toolGuard.rules[].match *objectMatch selects the tools this rule governs.
spec.toolGuard.rules[].match.kindstringKind is the runtime tool kind. Sidecar-toolbox tools report "mcp" (they are synthesized through the MCP synthesizer); select them via Origin "sidecartoolbox/*". (enum: sandbox | mcp | meta)
spec.toolGuard.rules[].match.originstringOrigin is a glob over the tool's origin in "<kind>/<name>" form, e.g. "mcpserver/github" or "sidecartoolbox/". Note: a bare "" also matches origin-less tools (empty origin); use a prefixed glob like "mcpserver/*" to scope to tools that have an origin.
spec.toolGuard.rules[].match.toolstringTool is a glob over the LLM-visible tool name (e.g. "github_*").
spec.toolGuard.rules[].rateLimitobjectRateLimit caps call volume; nil means matched tools get no rate limit.
spec.toolGuard.rules[].rateLimit.actionstringAction when a rate cap is hit: halt ends the session; deny errors the call; warn logs and audits but allows it. (enum: halt | deny | warn)
spec.toolGuard.rules[].rateLimit.maxCallsinteger (int32)MaxCalls caps calls in the sliding Window; both must be set together. (min 1)
spec.toolGuard.rules[].rateLimit.maxCallsPerTurninteger (int32)MaxCallsPerTurn caps calls within a single agent turn; 0 is unlimited. (min 1)
spec.toolGuard.rules[].rateLimit.windowstringWindow is the sliding-window span for MaxCalls; both must be set together.
spec.toolSessionLogstringToolSessionLog controls whether an interactive tool's parsed stream events (e.g. Claude Code's stream-json) are persisted to the tool_session memory Kind so oap agent logs --follow-live can show them. The runner-to-channel (Slack) stream is unaffected regardless. off — do not persist. highSignal — persist tool_use/result events, skip text deltas. full — persist every event. (enum: off | highSignal | full; default: highSignal)
spec.userPreferences[]objectUserPreferences declares the per-user preferences this class offers. Empty means the feature is absent for this class: no preferences tools are offered and no preference endpoints are active.
spec.userPreferences[].defaultobject (free-form)Default is the class-authored fallback used when neither an admin global nor a user value is set; type-checked at reconcile.
spec.userPreferences[].descriptionstringDescription is user-facing copy shown by the tools and forms.
spec.userPreferences[].enum[]objectEnum is the permitted value set; only for Type=enum.
spec.userPreferences[].enum[].descriptionstringDescription is user-facing copy for this choice, surfaced by the preferences tools and any schema-driven form.
spec.userPreferences[].enum[].value *stringValue is the stored token; unique within the enum.
spec.userPreferences[].name *stringName is the preference key, unique within the list; the identifier the tools and (phase 2) toolspec CEL read as prefs.&lt;Name&gt;.
spec.userPreferences[].patternstringPattern is a regexp every string value (or stringList item) must match. Only for Type=string and Type=stringList.
spec.userPreferences[].type *stringType is the value's shape. (enum: string | stringList | int | bool | enum)
spec.userPreferences[].visibilitystringVisibility controls who may read this preference's resolved value. "self" (the default): only the user themself — resolved for the session's current turn author. "class": any session of this class may also read it for a NAMED user (get_preferences' user argument), so the agent can honor it when addressing that user (e.g. a notification opt-out). Marking a preference "class" is the class author's declaration that its value is safe to show anyone who can talk to this agent. (enum: self | class)
spec.workspaceSourceobjectWorkspaceSource optionally binds a single WorkspaceSource whose materialized base seeds each session's /workspace as a per-session copy-on-write overlay. The referenced CR must be Valid. A single source per class (it seeds the one /workspace); multi-source is a later phase.
spec.workspaceSource.ref *stringRef names the WorkspaceSource CR in the AgentClass's namespace.
* required

Status

Status is controller-owned (observed state).

FieldTypeDescription
status.agentSessionGrantsRefstringAgentSessionGrantsRef names the owned AgentSessionGrants CR in the same namespace. Empty until the AgentClass reconciler has resolved tools and created/updated the CR.
status.boundChannels[]objectBoundChannels lists Channels in the same namespace that target this AgentClass. Reconciled by the AgentClass controller.
status.boundChannels[].kind *string
status.boundChannels[].name *string
status.conditions[]object
status.conditions[].lastTransitionTime *string (date-time)lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
status.conditions[].message *stringmessage is a human readable message indicating details about the transition. This may be an empty string.
status.conditions[].observedGenerationinteger (int64)observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. (min 0)
status.conditions[].reason *stringreason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status.conditions[].status *stringstatus of the condition, one of True, False, Unknown. (enum: True | False | Unknown)
status.conditions[].type *stringtype of condition in CamelCase or in foo.example.com/CamelCase.
status.derivedSessionInteractPermissionstringDerivedSessionInteractPermission is the subject-set the reconciler chose as this class's interact policy because the class DECLARED none and its input carries no human — the membership of the single role=output Channel bound to it (slack_channel:<id>#member for a Slack destination). Present only when the derivation actually fired: the class left spec.authz.session.interactPermission empty, status.userlessInput is true, and exactly one bound role=output Channel yielded a membership subject-set. A class that DECLARED the field leaves this empty — an authored value is never mirrored here, so a non-empty value always reads as "nobody wrote this; it was derived, and here is from what". It exists because the alternative was a fresh webhook-driven install sitting at Valid=False until a human hand-patched the one value the install already knew: the Slack channel the agent posts into is per-install, so no bundle can carry it, and the people in that channel are exactly the people who may interact with the sessions it announces. Published on status rather than left implicit so an operator can see WHICH subject-set is in force, and that it was not something they wrote, without reading code.
status.effectiveSettingsobjectEffectiveSettings is the resolved 4-tier settings snapshot (cluster → namespace → class). Stamped by the AgentClass reconciler.
status.effectiveSettings.allowedMCP[]objectAllowedMCP is the effective MCPServer ceiling; nil is unconstrained.
status.effectiveSettings.allowedMCP[].name *stringName is the permitted MCPServer CR name.
status.effectiveSettings.allowedMCP[].tools[]stringTools narrows to specific tool names; nil, empty, or ["*"] allows all.
status.effectiveSettings.allowedSandboxKinds[]stringAllowedSandboxKinds is the effective ceiling across constraining tiers. Empty means unconstrained.
status.effectiveSettings.allowedSkills[]stringAllowedSkills is the union of allow-skill patterns across constraining tiers (nil = unconstrained). Informational; the gate is the resolver.
status.effectiveSettings.allowedToolkits[]stringAllowedToolkits is the effective toolkit ceiling; nil is unconstrained.
status.effectiveSettings.authzobjectAuthz is the resolved set of human-in-the-loop timeouts.
status.effectiveSettings.authz.approvalTimeoutstringApprovalTimeout is how long a decision may wait for a human before the gate's timeout policy fires.
status.effectiveSettings.authz.informationLeakageApprovalTTLstringInformationLeakageApprovalTTL is how long an approved share may be reused before the agent must ask again.
status.effectiveSettings.authz.metaagentobjectMetaagent is the resolved metaagent trigger: the class value, else the nearest tier default, else mention. Never nil after resolution — an absent setting resolves to the narrowest value rather than to a nil the reader has to interpret.
status.effectiveSettings.authz.metaagent.triggerstringTrigger is when the metaagent classifies a turn, and whether it acts. mention (default) — ambient off; explicit @metaagent only. shadow — classify every turn, apply nothing, record it. inline — classify every turn and act. (enum: mention | shadow | inline; default: mention)
status.effectiveSettings.authz.planGateobjectPlanGate is the resolved plan-gate config: the class value (or the nearest tier default) clamped up to the strictest MinPlanGateMode any tier declares. Always non-nil after resolution. This is the value the gate READS. Unlike every other authz setting — which the runtime reads straight off AgentClass.spec.authz — the plan gate must go through the resolved path, or a class could opt out past a cluster floor by being the only value anyone consults.
status.effectiveSettings.authz.planGate.examples[]objectExamples are worked plans this class's author supplies, rendered after the one the runtime derives from the session's own permission surface. Optional, and most classes want none: the derived example already speaks the agent's own handles and shows the shape that costs one approval. These are for a class whose GOOD plan is not obvious from its surface alone — where the order of phases matters, or where two resource types must be named together, or where the natural split is by audience rather than by read-then-write. They are TRUSTED text, at the same level as spec.systemPrompt: an operator authors them, never the agent. But unlike the system prompt they are checked — every handle an example names must exist on this class's own surface, or the class goes Valid=False naming the offender. The prompt tells agents that a handle off the declarable list is silently dropped, and an EXAMPLE carrying such a handle would teach exactly that failure to every plan the agent writes. Measured, planners transcribe these examples closely, so a wrong one is not inert.
status.effectiveSettings.authz.planGate.examples[].phases *[]objectPhases are the plan itself. Rendered as the JSON an agent would pass to update_plan, because that is the form it copies.
status.effectiveSettings.authz.planGate.examples[].phases[].id *stringID is the phase's identifier, as update_plan takes it.
status.effectiveSettings.authz.planGate.examples[].phases[].labelstringLabel is the human phrase for the phase.
status.effectiveSettings.authz.planGate.examples[].phases[].permissions[]stringPermissions are the wire-format handles this phase declares ("perm:<permission>:<resourceType>" or "tool:<name>"). Each is checked against the class's surface at admission.
status.effectiveSettings.authz.planGate.examples[].phases[].slots[]stringSlots are the resource TYPES this phase names an instance of. Types only: an authored example must not carry a concrete instance id, because a planner that copies one would address an object belonging to whoever the example was written about.
status.effectiveSettings.authz.planGate.examples[].task *stringTask is the one-line request this plan answers — the "when you are asked to X" half. Without it an example shows a shape with no occasion, and the agent cannot tell which of several examples applies.
status.effectiveSettings.authz.planGate.limitsobjectLimits bound plan size. The defaults are deliberately expansive: they exist to stop a runaway or hostile plan from DoS-ing the approver and producing an unrenderable card, NOT to shape normal authoring.
status.effectiveSettings.authz.planGate.limits.maxPermissionsPerPhaseinteger (int32)MaxPermissionsPerPhase is naturally bounded by the permission surface (a phase cannot declare a handle that does not exist), so this is a backstop rather than a real constraint. (default: 64)
status.effectiveSettings.authz.planGate.limits.maxPhasesinteger (int32)(default: 50)
status.effectiveSettings.authz.planGate.limits.maxSlotsPerPhaseinteger (int32)MaxSlotsPerPhase bounds the instance axis, which is genuinely unbounded (a thread can carry many URLs), hence the larger default. (default: 128)
status.effectiveSettings.authz.planGate.modestringMode selects how much of the gate runs. disabled — off entirely; no prompt change, no plan schema surfaced. logging — the gate runs in full: enumeration, ceiling computation, approver resolution, severity, card rendering and the append-only write. What it holds back is CEILINGS and APPROVALS — the card is logged instead of published, so nothing is approved and no grant is written, and a call that overruns a declared phase is RECORDED rather than refused. A call made with NO plan at all is still refused (see RequirePlan). That is not an exception to the mode so much as its precondition: without a declared plan there are no ceilings to record and the mode measures nothing. enforcing — the card is published and the call blocks on the outcome. (enum: disabled | logging | enforcing; default: disabled)
status.effectiveSettings.authz.planGate.renderingobjectRendering bounds what a single approval card shows or auto-approves.
status.effectiveSettings.authz.planGate.rendering.maxAutoApproveHandlesinteger (int32)MaxAutoApproveHandles bounds the UNION of all handles auto-approved without a human across a session — the tier-0 budget. (default: 8)
status.effectiveSettings.authz.planGate.rendering.maxNamedApproversinteger (int32)MaxNamedApprovers is the largest approver population rendered by name before falling back to a count or a channel-supplied description. (default: 5)
status.effectiveSettings.authz.planGate.rendering.maxSingleCardHandlesinteger (int32)MaxSingleCardHandles bounds a phase ceiling shown on one card; a phase exceeding it is split rather than folded behind "+N more", so the approver sees what they approve. (default: 16)
status.effectiveSettings.authz.planGate.requirePlanbooleanRequirePlan denies every permissioned call made before a plan is declared. Without it, an agent that simply never calls update_plan falls through the implicit-phase rule and is unconstrained — i.e. "don't plan" is a total bypass. It bites in LOGGING mode too, which is the one place this differs from everything else the gate does. "You must declare a plan" and "you must stay within its ceiling" are separable claims; only the second is what logging mode holds back. Welding them together made logging unable to produce the dataset it exists for — agents told they MUST plan simply did not, because in logging nothing depended on it, and no wording fixes a consequence the code does not implement. So: with RequirePlan, a session under logging still refuses calls made with no plan at all, and still records-but-permits calls that overrun a declared ceiling. That yields real phases, ceilings, severity and approver data without switching on the enforcement the data is meant to justify. UNSET derives from Mode: required whenever the gate runs at all. Turning the gate on IS the intent to make agents plan, and leaving this an independent opt-in defaulting false meant a cluster could run mode: logging, look gated, and collect nothing — which is what happened across five live sessions, every call landing on the implicit phase because nothing required a plan. Explicit false remains an opt-out, because gate-on-but-observe-only is a real rollout stage: it records which handles calls actually resolve to without changing what the agent may do. Explicit true under mode: disabled is still false — a gate that does not run cannot refuse anything, and honouring it would promise a denial that never arrives. Pointer rather than a kubebuilder default: a static default cannot express "depends on another field", and it would erase the difference between unset and a deliberate false. UPGRADE NOTE. This field previously carried +kubebuilder:default=false, so every AgentClass created under the older CRD has an explicit false PERSISTED — which now reads as the observe-only opt-out rather than as unset. Such a class keeps the old behaviour after an upgrade and will not require a plan. Re-applying the class (or deleting the field) is what restores derivation; there is no way to distinguish a stamped default from a deliberate choice after the fact.
status.effectiveSettings.authz.scopeMaxLlmLatencyMsinteger (int32)ScopeMaxLLMLatencyMs bounds the cold-start scope extractor's LLM call, in milliseconds. A latency budget, not a window in which authority is held.
status.effectiveSettings.budgetobjectBudget is the resolved per-session budget, clamped to every tier ceiling.
status.effectiveSettings.budget.maxDelegatedAgentsinteger (int32)MaxDelegatedAgents bounds the TOTAL number of sessions in one delegation tree, counting the root. It is a property of the tree rather than of any one session: every other dimension here caps a single session's spend, and a tree of N sessions each individually within budget still spends N times it. Resolved from the ROOT session's effective settings and enforced by the SubagentRequest controller before a child is created. Zero means UNSET, not unlimited. A consumer must apply its own built-in bound rather than reading zero as "no cap" — that reading is right for a DURATION (as MaxDuration uses it) and wrong for a COUNT, where it would license exactly the runaway fan-out this field exists to prevent. (min 0)
status.effectiveSettings.budget.maxDuration *stringMaxDuration is the cumulative ACTIVE RUN-TIME budget (e.g. "30m") — the wall time the agent actually spent working, excluding time parked waiting on a human (the next user message or a tool-call approval). It persists across sleep/resume via status.runDuration; it is NOT wall-clock since session start. For a wall-clock lifetime cap, use SessionExpiration. Zero = no run-time cap.
status.effectiveSettings.budget.maxTokens *integer (int64)(min 1)
status.effectiveSettings.budget.maxTurns *integer (int32)(min 1)
status.effectiveSettings.budget.sessionExpirationstringSessionExpiration is a hard wall-clock lifetime cap measured from status.startedAt (e.g. "24h"), regardless of how much the agent ran. The operator fails the session once exceeded, even while it is idle or asleep. Zero = never expires (no default). Distinct from MaxDuration, which counts only active run-time.
status.effectiveSettings.contentInspectors[]objectContentInspectors is the resolved union of content-guard inspectors across tiers (ceiling: lower tiers add, never weaken). Empty ⇒ no inspection.
status.effectiveSettings.contentInspectors[].config *object (free-form)Config is inspector-specific configuration, validated by the inspector's Configure at admission (fail-closed) and at session start.
status.effectiveSettings.contentInspectors[].id *stringID must resolve in the contentguard registry (e.g. "url-allowlist").
status.effectiveSettings.deniedSkills[]stringDeniedSkills is the union of deny-skill patterns across tiers.
status.effectiveSettings.modelobjectModel is the resolved model the session runs against.
status.effectiveSettings.model.apiKeyobjectAPIKey is a bring-your-own token. Only honored when allowModelOverride is granted (else a fatal violation). Catalog references leave this empty.
status.effectiveSettings.model.apiKey.key *stringKey is the data key within the Secret holding the value.
status.effectiveSettings.model.apiKey.name *stringName is the Secret's name, in the referring object's own namespace.
status.effectiveSettings.model.fromCatalogstringFromCatalog references a model catalog entry by name. Mutually exclusive with an inline Provider/Name. When set, Provider and the token come from the catalog entry.
status.effectiveSettings.model.namestring
status.effectiveSettings.model.providerstring(enum: anthropic | openai | openrouter | test)
status.effectiveSettings.model.routingMetadataobjectRoutingMetadata refines dynamic model selection for this agent. Merged over the resolved catalog entry's routing with narrowing-only semantics: an AgentClass may constrain, never loosen, admin policy. Inert unless the resolved model performs dynamic selection (Provider == "openrouter").
status.effectiveSettings.model.routingMetadata.allowFallbacksboolean
status.effectiveSettings.model.routingMetadata.allowedModels[]stringAuto-router hints — apply when the model is "openrouter/auto".
status.effectiveSettings.model.routingMetadata.costQualityTradeoffnumberCostQualityTradeoff is intentionally unvalidated (no Minimum/Maximum marker): OpenRouter documents no formal range for this value — its docs example value is 3 — so a range marker would reject valid input.
status.effectiveSettings.model.routingMetadata.dataCollectionstring(enum: allow | deny)
status.effectiveSettings.model.routingMetadata.ignore[]string
status.effectiveSettings.model.routingMetadata.maxPriceobjectOpenRouterMaxPrice caps per-model price, in USD per MILLION tokens (OpenRouter's max_price units; e.g. Prompt: 1 means at most $1/M prompt tokens). 0 on an axis means "no cap on this axis" (see pkg/platform/settings/routingmerge.go's tightenPrice and pkg/agent/llm/openrouter/extra.go's maxPriceObject), not "cap at $0". Only consulted when the owning ModelCatalogEntry's Provider == "openrouter".
status.effectiveSettings.model.routingMetadata.maxPrice.completionnumber(min 0)
status.effectiveSettings.model.routingMetadata.maxPrice.promptnumber(min 0)
status.effectiveSettings.model.routingMetadata.models[]string
status.effectiveSettings.model.routingMetadata.only[]string
status.effectiveSettings.model.routingMetadata.order[]string
status.effectiveSettings.model.routingMetadata.requireParametersbooleanRequireParameters: unset defaults to TRUE when the request carries tools (the openrouter provider injects it — see pkg/agent/llm/openrouter/extra.go's providerObject); explicit values are honored verbatim. OpenRouter's own wire default is false — agents need tools, so auto/fallback routing must only pick providers that support the request's parameters.
status.effectiveSettings.model.routingMetadata.sortstring(enum: price | throughput | latency)
status.effectiveSettings.modelInputPerMToknumberModelInputPerMTok/ModelOutputPerMTok are the resolved catalog price for the chosen model (USD/MTok), 0 when the catalog carries no price. The runner cost estimator prefers these over the built-in table so its figure matches the admin dashboard (which is catalog-authoritative).
status.effectiveSettings.modelOutputPerMToknumberModelOutputPerMTok is the output half of the same catalog price.
status.effectiveSettings.modelRoutingobjectModelRouting is the effective OpenRouter dynamic-routing preference: the catalog entry's, with the AgentClass's routingMetadata merged over it. Only set when Model.Provider == "openrouter".
status.effectiveSettings.modelRouting.allowFallbacksboolean
status.effectiveSettings.modelRouting.allowedModels[]stringAuto-router hints — apply when the model is "openrouter/auto".
status.effectiveSettings.modelRouting.costQualityTradeoffnumberCostQualityTradeoff is intentionally unvalidated (no Minimum/Maximum marker): OpenRouter documents no formal range for this value — its docs example value is 3 — so a range marker would reject valid input.
status.effectiveSettings.modelRouting.dataCollectionstring(enum: allow | deny)
status.effectiveSettings.modelRouting.ignore[]string
status.effectiveSettings.modelRouting.maxPriceobjectOpenRouterMaxPrice caps per-model price, in USD per MILLION tokens (OpenRouter's max_price units; e.g. Prompt: 1 means at most $1/M prompt tokens). 0 on an axis means "no cap on this axis" (see pkg/platform/settings/routingmerge.go's tightenPrice and pkg/agent/llm/openrouter/extra.go's maxPriceObject), not "cap at $0". Only consulted when the owning ModelCatalogEntry's Provider == "openrouter".
status.effectiveSettings.modelRouting.maxPrice.completionnumber(min 0)
status.effectiveSettings.modelRouting.maxPrice.promptnumber(min 0)
status.effectiveSettings.modelRouting.models[]string
status.effectiveSettings.modelRouting.only[]string
status.effectiveSettings.modelRouting.order[]string
status.effectiveSettings.modelRouting.requireParametersbooleanRequireParameters: unset defaults to TRUE when the request carries tools (the openrouter provider injects it — see pkg/agent/llm/openrouter/extra.go's providerObject); explicit values are honored verbatim. OpenRouter's own wire default is false — agents need tools, so auto/fallback routing must only pick providers that support the request's parameters.
status.effectiveSettings.modelRouting.sortstring(enum: price | throughput | latency)
status.effectiveSettings.modelTokenSourceobjectModelTokenSource is the resolved central token location (system namespace) when the effective model came from the catalog. nil for bring-your-own / legacy. The operator materializes this into the per-session Secret.
status.effectiveSettings.modelTokenSource.key *stringKey is the data key within the Secret holding the value.
status.effectiveSettings.modelTokenSource.name *stringName is the Secret's name.
status.effectiveSettings.modelTokenSource.namespace *stringNamespace is the Secret's namespace.
status.effectiveSettings.nativeFileHandling *booleanNativeFileHandling is the resolved Tier-2 provider-native file handling grant (security-sensitive; default false). See SettingsLimits.NativeFileHandling.
status.effectiveSettings.pinningobjectPinning preserves the per-tier pinning policies. Per-item evaluation happens via settings.PinRequirementFor — bypasses are tier-scoped, so the policies cannot be pre-folded.
status.effectiveSettings.pinning.clusterobjectCluster is the cluster-tier policy, kept un-folded.
status.effectiveSettings.pinning.cluster.bypass[]objectBypass exempts named dependencies from this tier's rules and lower.
status.effectiveSettings.pinning.cluster.bypass[].kind *stringKind is the pinning registry kind this exemption applies to.
status.effectiveSettings.pinning.cluster.bypass[].name *stringName identifies the exempted item: a canonical skill name, MCPServer name, SidecarToolbox name, or SpiceboxToolkit name. Trailing-wildcard patterns are allowed (same syntax as AllowedSkills).
status.effectiveSettings.pinning.cluster.bypass[].reason *stringReason is the required audit-trail justification for the exemption.
status.effectiveSettings.pinning.cluster.rules[]objectRules is at most one requirement per dependency kind.
status.effectiveSettings.pinning.cluster.rules[].kind *stringKind is the pinning registry kind name this rule governs.
status.effectiveSettings.pinning.cluster.rules[].minStrengthstringMinStrength is the minimum pin strength a declared ref must have. Empty = no floor (drift observation still applies). (enum: frozen | named)
status.effectiveSettings.pinning.cluster.rules[].modestringMode governs what a violation or drift does: block (fail closed), approve (force per-call user approval), warn (surface warnings only), off (observe only). Empty = approve. (enum: block | approve | warn | off)
status.effectiveSettings.pinning.namespaceobjectNamespace is the namespace-tier policy, kept un-folded.
status.effectiveSettings.pinning.namespace.bypass[]objectBypass exempts named dependencies from this tier's rules and lower.
status.effectiveSettings.pinning.namespace.bypass[].kind *stringKind is the pinning registry kind this exemption applies to.
status.effectiveSettings.pinning.namespace.bypass[].name *stringName identifies the exempted item: a canonical skill name, MCPServer name, SidecarToolbox name, or SpiceboxToolkit name. Trailing-wildcard patterns are allowed (same syntax as AllowedSkills).
status.effectiveSettings.pinning.namespace.bypass[].reason *stringReason is the required audit-trail justification for the exemption.
status.effectiveSettings.pinning.namespace.rules[]objectRules is at most one requirement per dependency kind.
status.effectiveSettings.pinning.namespace.rules[].kind *stringKind is the pinning registry kind name this rule governs.
status.effectiveSettings.pinning.namespace.rules[].minStrengthstringMinStrength is the minimum pin strength a declared ref must have. Empty = no floor (drift observation still applies). (enum: frozen | named)
status.effectiveSettings.pinning.namespace.rules[].modestringMode governs what a violation or drift does: block (fail closed), approve (force per-call user approval), warn (surface warnings only), off (observe only). Empty = approve. (enum: block | approve | warn | off)
status.effectiveSettings.provenancemap[string]stringProvenance maps a resolved field to the tier that supplied it (cluster|namespace|class|session|clamped).
status.effectiveSettings.reportSessionCost *booleanReportSessionCost is the resolved end-of-session cost-estimate toggle.
status.effectiveSettings.requireStandingFor[]stringRequireStandingFor is the union of every tier's RequireStandingFor, sorted and deduplicated. A resource type named here resolves to required standing regardless of what its schema fragment declares.
status.effectiveSettings.requireSubagentDigestPins *booleanRequireSubagentDigestPins is the resolved delegation requirement (default false): when true, the SubagentRequest controller refuses delegation through any unpinned roster entry. See SettingsLimits.RequireSubagentDigestPins.
status.effectiveSettings.sandboxmap[string]objectSandbox is the resolved sandbox backend per tool-bundle name. Recorded so oap can show which backend a session landed on, and Provenance says which tier chose it.
status.effectiveSettings.toolGuardobjectToolGuard preserves the per-tier tool-guard policies + folded ceiling. Per-tool rule resolution happens in the runner at session start.
status.effectiveSettings.toolGuard.ceilingobjectCeiling is the strictest-across-tiers bound, pre-folded.
status.effectiveSettings.toolGuard.ceiling.maxCallsinteger (int32)MaxCalls caps calls in the sliding Window; both must be set together. (min 1)
status.effectiveSettings.toolGuard.ceiling.maxCallsPerTurninteger (int32)MaxCallsPerTurn / MaxCalls+Window impose rate ceilings even when no lower-tier rule configures a rate limit. (min 1)
status.effectiveSettings.toolGuard.ceiling.maxEgressBytesinteger (int64)MaxEgressBytes / MaxIngressBytes impose per-call byte ceilings even when no lower-tier rule configures a data limit (an unset limit is "unlimited", so the ceiling wins). Folded strictest-across-tiers (min). (min 1)
status.effectiveSettings.toolGuard.ceiling.maxFailureThresholdinteger (int32)MaxFailureThreshold caps the effective breaker threshold (min wins). Note: the per-origin threshold (BreakerSpec.OriginFailureThreshold) deliberately has no ceiling yet. (min 1)
status.effectiveSettings.toolGuard.ceiling.maxIngressBytesinteger (int64)MaxIngressBytes is the inbound half of the same per-call byte ceiling. (min 1)
status.effectiveSettings.toolGuard.ceiling.maxUIIngressBytesinteger (int64)MaxUIIngressBytes imposes a ceiling on the UI data-binding ingress path (toolguard.DefaultUIIngressBytes when neither a rule nor this ceiling sets one — this path is never unlimited). Folded strictest-across-tiers (min), independent of MaxIngressBytes. (min 1)
status.effectiveSettings.toolGuard.ceiling.minActionstringMinAction is a severity floor (off < warn < deny < halt). Setting "deny" makes the breaker non-disableable below this tier. (enum: warn | deny | halt)
status.effectiveSettings.toolGuard.ceiling.minInitialCoolOffstringMinInitialCoolOff raises the effective initial cool-off (max wins). Note: MaxCoolOff deliberately has no floor yet.
status.effectiveSettings.toolGuard.ceiling.rateBounds[]objectRateBounds carries sliding-window bounds BESIDE MaxCalls/Window. Every bound binds: a call must fit under all of them, and a lower tier can only add bounds, never trade one away. A ceiling is a conjunction, not a choice. Two tiers naming different windows have written bounds neither of which implies the other — a cluster {5 calls, 1s} permits 432,000/day, a namespace {10 calls, 24h} permits all 10 inside one second — so collapsing them by calls/second discards a bound its author wrote, on the very surface this type calls the hard bound lower tiers cannot escape. The settings fold writes it: the strictest-by-rate pair leads in MaxCalls/Window, so a reader that knows only the pair still sees a real bound, and every other distinct window lands here, deduped strictest-per-window and window-ordered so the object is stable to re-stamp onto status. Authoring it directly is how one tier expresses burst-plus-sustained on its own. It deliberately carries no MaxItems: one schema governs both the AUTHORED surface and the FOLDED one stamped onto status, so a cap of N would also bind a fold that unions two tiers of N+1 windows and can legitimately produce 2N+1 — the status write rejected by the very bound meant to keep it small, wedging the reconcile. Enforcement stays cheap instead because the per-call sweep is one pass over call history per bound, and the size of that history is set by maxCalls over the longest window, not by how many horizons are named.
status.effectiveSettings.toolGuard.ceiling.rateBounds[].maxCalls *integer (int32)MaxCalls is the cap on calls inside Window. (min 1)
status.effectiveSettings.toolGuard.ceiling.rateBounds[].window *stringWindow is the sliding span MaxCalls is counted over. A non-positive duration enforces nothing; the resolver reports one rather than folding it in (pkg/platform/settings, ToolGuardRateBoundUnenforceable).
status.effectiveSettings.toolGuard.ceiling.windowstringWindow is the sliding-window span for MaxCalls; both must be set together.
status.effectiveSettings.toolGuard.clusterobjectCluster is the cluster-tier policy, kept un-folded for the rule walk.
status.effectiveSettings.toolGuard.cluster.rules[]objectRules are evaluated in order, first match wins; empty means this tier contributes nothing and the walk falls through.
status.effectiveSettings.toolGuard.cluster.rules[].breakerobjectBreaker configures the circuit breaker; nil means matched tools get none.
status.effectiveSettings.toolGuard.cluster.rules[].breaker.actionstringAction when the breaker denies: halt ends the session; deny returns an IsError tool_result; warn logs/audits but allows; off disables the breaker for matched tools. (enum: halt | deny | warn | off)
status.effectiveSettings.toolGuard.cluster.rules[].breaker.failureThresholdinteger (int32)FailureThreshold is consecutive Execute failures (per tool) that open the breaker. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].breaker.initialCoolOffstringInitialCoolOff is the first cool-off period after the breaker opens; it doubles on each successive trip up to MaxCoolOff. Default 30s.
status.effectiveSettings.toolGuard.cluster.rules[].breaker.maxCoolOffstringMaxCoolOff caps the exponential-backoff cool-off. Default 10m.
status.effectiveSettings.toolGuard.cluster.rules[].breaker.originFailureThresholdinteger (int32)OriginFailureThreshold is consecutive failures across ALL tools of the tool's origin that open the origin breaker (denying every sibling). (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].dataLimitobjectDataLimit caps per-call byte volume; nil means no byte cap.
status.effectiveSettings.toolGuard.cluster.rules[].dataLimit.actionstringAction when a byte limit is exceeded: halt ends the session; deny errors the call (egress: tool not run; ingress: result withheld); warn logs/audits but allows. (enum: halt | deny | warn)
status.effectiveSettings.toolGuard.cluster.rules[].dataLimit.maxEgressBytesinteger (int64)MaxEgressBytes caps the serialized tool-args size sent outbound per call. Exceeding it (at PreToolCall) applies Action; on deny the tool does not run. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].dataLimit.maxIngressBytesinteger (int64)MaxIngressBytes caps the tool-result size returned inbound per call (any result, success or error). Exceeding it (at PostToolCall) applies Action; on deny the result is withheld (replaced with an IsError) so the oversized payload never reaches the model. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].dataLimit.maxUIIngressBytesinteger (int64)MaxUIIngressBytes caps result bytes on an agent-UI DATA BINDING, whose result is rendered by a browser and never read by the model. Unset means the platform's browser-sized default applies (toolguard's DefaultUIIngressBytes) — NOT unlimited, and NOT MaxIngressBytes. Set this to bind the UI path tighter or looser than the platform default; tightening maxIngressBytes alone does not affect it. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].match *objectMatch selects the tools this rule governs.
status.effectiveSettings.toolGuard.cluster.rules[].match.kindstringKind is the runtime tool kind. Sidecar-toolbox tools report "mcp" (they are synthesized through the MCP synthesizer); select them via Origin "sidecartoolbox/*". (enum: sandbox | mcp | meta)
status.effectiveSettings.toolGuard.cluster.rules[].match.originstringOrigin is a glob over the tool's origin in "<kind>/<name>" form, e.g. "mcpserver/github" or "sidecartoolbox/". Note: a bare "" also matches origin-less tools (empty origin); use a prefixed glob like "mcpserver/*" to scope to tools that have an origin.
status.effectiveSettings.toolGuard.cluster.rules[].match.toolstringTool is a glob over the LLM-visible tool name (e.g. "github_*").
status.effectiveSettings.toolGuard.cluster.rules[].rateLimitobjectRateLimit caps call volume; nil means matched tools get no rate limit.
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.actionstringAction when a rate cap is hit: halt ends the session; deny errors the call; warn logs and audits but allows it. (enum: halt | deny | warn)
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.maxCallsinteger (int32)MaxCalls caps calls in the sliding Window; both must be set together. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.maxCallsPerTurninteger (int32)MaxCallsPerTurn caps calls within a single agent turn; 0 is unlimited. (min 1)
status.effectiveSettings.toolGuard.cluster.rules[].rateLimit.windowstringWindow is the sliding-window span for MaxCalls; both must be set together.
status.effectiveSettings.toolGuard.namespaceobjectNamespace is the namespace-tier policy, kept un-folded for the rule walk.
status.effectiveSettings.toolGuard.namespace.rules[]objectRules are evaluated in order, first match wins; empty means this tier contributes nothing and the walk falls through.
status.effectiveSettings.toolGuard.namespace.rules[].breakerobjectBreaker configures the circuit breaker; nil means matched tools get none.
status.effectiveSettings.toolGuard.namespace.rules[].breaker.actionstringAction when the breaker denies: halt ends the session; deny returns an IsError tool_result; warn logs/audits but allows; off disables the breaker for matched tools. (enum: halt | deny | warn | off)
status.effectiveSettings.toolGuard.namespace.rules[].breaker.failureThresholdinteger (int32)FailureThreshold is consecutive Execute failures (per tool) that open the breaker. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].breaker.initialCoolOffstringInitialCoolOff is the first cool-off period after the breaker opens; it doubles on each successive trip up to MaxCoolOff. Default 30s.
status.effectiveSettings.toolGuard.namespace.rules[].breaker.maxCoolOffstringMaxCoolOff caps the exponential-backoff cool-off. Default 10m.
status.effectiveSettings.toolGuard.namespace.rules[].breaker.originFailureThresholdinteger (int32)OriginFailureThreshold is consecutive failures across ALL tools of the tool's origin that open the origin breaker (denying every sibling). (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].dataLimitobjectDataLimit caps per-call byte volume; nil means no byte cap.
status.effectiveSettings.toolGuard.namespace.rules[].dataLimit.actionstringAction when a byte limit is exceeded: halt ends the session; deny errors the call (egress: tool not run; ingress: result withheld); warn logs/audits but allows. (enum: halt | deny | warn)
status.effectiveSettings.toolGuard.namespace.rules[].dataLimit.maxEgressBytesinteger (int64)MaxEgressBytes caps the serialized tool-args size sent outbound per call. Exceeding it (at PreToolCall) applies Action; on deny the tool does not run. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].dataLimit.maxIngressBytesinteger (int64)MaxIngressBytes caps the tool-result size returned inbound per call (any result, success or error). Exceeding it (at PostToolCall) applies Action; on deny the result is withheld (replaced with an IsError) so the oversized payload never reaches the model. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].dataLimit.maxUIIngressBytesinteger (int64)MaxUIIngressBytes caps result bytes on an agent-UI DATA BINDING, whose result is rendered by a browser and never read by the model. Unset means the platform's browser-sized default applies (toolguard's DefaultUIIngressBytes) — NOT unlimited, and NOT MaxIngressBytes. Set this to bind the UI path tighter or looser than the platform default; tightening maxIngressBytes alone does not affect it. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].match *objectMatch selects the tools this rule governs.
status.effectiveSettings.toolGuard.namespace.rules[].match.kindstringKind is the runtime tool kind. Sidecar-toolbox tools report "mcp" (they are synthesized through the MCP synthesizer); select them via Origin "sidecartoolbox/*". (enum: sandbox | mcp | meta)
status.effectiveSettings.toolGuard.namespace.rules[].match.originstringOrigin is a glob over the tool's origin in "<kind>/<name>" form, e.g. "mcpserver/github" or "sidecartoolbox/". Note: a bare "" also matches origin-less tools (empty origin); use a prefixed glob like "mcpserver/*" to scope to tools that have an origin.
status.effectiveSettings.toolGuard.namespace.rules[].match.toolstringTool is a glob over the LLM-visible tool name (e.g. "github_*").
status.effectiveSettings.toolGuard.namespace.rules[].rateLimitobjectRateLimit caps call volume; nil means matched tools get no rate limit.
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.actionstringAction when a rate cap is hit: halt ends the session; deny errors the call; warn logs and audits but allows it. (enum: halt | deny | warn)
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.maxCallsinteger (int32)MaxCalls caps calls in the sliding Window; both must be set together. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.maxCallsPerTurninteger (int32)MaxCallsPerTurn caps calls within a single agent turn; 0 is unlimited. (min 1)
status.effectiveSettings.toolGuard.namespace.rules[].rateLimit.windowstringWindow is the sliding-window span for MaxCalls; both must be set together.
status.factSources[]objectFactSources publishes, per fact this class's slot preconditions read, which tool can record it and what gates that tool — so an author can see that a precondition is answerable, and see it BEFORE a session sits on an undetermined verdict with nothing anywhere reporting a fault. Deliberately a warning surface rather than a gate. The impossible case admission CAN prove — every producer of a fact gated by the very slot the precondition holds shut — is refused with Valid=False. A fact NOTHING declares a producer for is permanently undetermined too, but it is indistinguishable at admission from one a channel kind or an out-of-class tool legitimately supplies, so it lands here as state: absent instead of failing the class. Read each entry's state, not its prose: an empty producedBy is both absent and envelope, and an empty gatedBy is both ungated and unresolved. An OBSERVATION: derived by the reconciler from the slots and the tools, written set-on-change. Never SSA-applied — a volatile field a client applied would churn ownership on every reconcile.
status.factSources[].detailstringDetail explains, in prose meant for a person, an entry that is not a plain gated producer: why nothing records the fact, why the producer is ungated, or which gate could not be determined. Empty for the ordinary case, where State, ProducedBy and GatedBy already say everything. Human-facing only. Branch on State.
status.factSources[].gatedBy[]stringGatedBy lists, sorted, every resource type a permission check on ProducedBy names — that is, which slots must already be bound before the call that records this fact is allowed. Empty when ProducedBy is ungated, or when its gate could not be resolved; Detail says which.
status.factSources[].name *stringName is the fact name, as facts.&lt;provenance&gt;.&lt;name&gt; addresses it.
status.factSources[].producedBystringProducedBy names the tool whose observes block records this fact. EMPTY means nothing on this class declares a producer — see Detail for which of the two reasons applies. One entry per producer: a fact several tools record yields several entries, because a reader deciding whether a precondition is answerable needs to know every call that could answer it, not just one.
status.factSources[].provenance *stringProvenance is the fact's namespace: envelope (derived by platform code from a signed provider payload) or observed (derived from a response to a call the agent shaped).
status.factSources[].state *stringState is the machine-readable answer to "what kind of entry is this", and it is the field a consumer branches on. Detail says the same thing in prose for a human; prose is not a discriminator, and a consumer substring-matching Detail would break the first time the wording is improved. It exists because two of the five states share an empty GatedBy and two share an empty ProducedBy, so neither field can separate them: gated — a producer, and every call of it is gated (GatedBy lists the types). ungated — a producer no permission check gates, so the call that records the fact is always allowed. unresolved — a real producer whose gate could NOT be determined. Not the same as ungated, and never treated as blocked. absent — nothing on this class declares a producer for an observed fact. A warning, not a refusal. envelope — an envelope fact, which no tool ever records. Normal, and deliberately not the absent warning. (enum: gated | ungated | unresolved | absent | envelope)
status.oapInstallobjectOapInstall records provenance for an AgentClass installed from a .oap bundle: the source it came from, its content digest, and when it was installed. Absent for AgentClasses created by other means (kubectl apply, wizard, etc.).
status.oapInstall.digest *stringDigest is the content digest of the installed .oap bundle (e.g. "sha256:...").
status.oapInstall.installedAtstring (date-time)InstalledAt is when this AgentClass was installed from the bundle.
status.oapInstall.sourceKind *stringSourceKind identifies the kind of SourceRef: "registry" (an OCI reference) or "file" (a local .oap file or folder-source directory).
status.oapInstall.sourceRefstringSourceRef identifies where the bundle came from (e.g. an OCI reference or a local file path), in a form meaningful to SourceKind.
status.oapInstall.versionstringVersion is the bundle's declared version, when the bundle manifest carries one.
status.observedGenerationinteger (int64)
status.resolvedPermissionTitles[]objectResolvedPermissionTitles publishes, per (resourceType, permission) pair, the human phrase declared on the SpiceDBPermission that names it — so the runner can render an approval card without re-reading every MCPServer and SpiceboxToolkit schema fragment. An OBSERVATION: the reconciler derives this from the tools, never authors it. A pair with no declared Title is OMITTED rather than published with an empty one, so the card can tell "nobody wrote a title" (detokenize the handle) from "somebody declared it blank" (which this field never represents).
status.resolvedPermissionTitles[].permission *stringPermission is the permission handle (SpiceDBPermission.Name).
status.resolvedPermissionTitles[].planningNotestringPlanningNote is the guidance the agent reads while declaring a plan, copied from the declaring SpiceDBPermission.PlanningNote. It rides this entry rather than a list of its own because the key is identical — (resourceType, permission) — and a second list would mean a second walk of every toolkit and MCPServer producing the same keys, free to drift from this one. The two strings differ in AUDIENCE, not in what identifies them: Title is read by a human deciding, PlanningNote by the agent planning.
status.resolvedPermissionTitles[].resourceType *stringResourceType is the SpiceDB resource type the permission is declared on.
status.resolvedPermissionTitles[].title *stringTitle is the phrase a human reads on an approval card, copied from the declaring SpiceDBPermission.Title.
status.resolvedResourceDisplays[]objectResolvedResourceDisplays publishes, per resource type, the declared icon/label/name presentation — the sibling of ResolvedPermissionTitles for RESOURCE INSTANCE lines instead of permission lines. An OBSERVATION: the reconciler derives this from the tools, never authors it. A type with no declared Display is OMITTED rather than published empty, so the card can tell "nobody declared a display" (fall back to the wire type name) from "somebody declared one with blank fields".
status.resolvedResourceDisplays[].iconstringIcon mirrors SpiceDBResourceDisplay.Icon.
status.resolvedResourceDisplays[].labelstringLabel mirrors SpiceDBResourceDisplay.Label.
status.resolvedResourceDisplays[].namestringName mirrors SpiceDBResourceDisplay.Name.
status.resolvedResourceDisplays[].resourceType *stringResourceType is the SpiceDB resource type the display is declared on.
status.resolvedResourceStandings[]objectResolvedResourceStandings publishes, per RESOURCE TYPE, how approval for it is governed: required plus the permission an approver must hold, or session-only when nothing local governs it. Keyed by resource type rather than by slot, unlike ResolvedSlots[].Standing. A tool's permission check can name a type the class never declared as a slot, and the approval router needs an answer for THAT type too — with no entry it must refuse rather than fall back, which is the whole point of removing the default.
status.resolvedResourceStandings[].approverPermissionstringApproverPermission is the permission an approver must hold on an instance, set only when Standing is required.
status.resolvedResourceStandings[].resourceType *stringResourceType is the SpiceDB resource type this answer is for.
status.resolvedResourceStandings[].standing *stringStanding is required or session-only. Never empty: a type whose fragments declare nothing is refused at resolve time rather than published with a blank answer a reader would have to interpret.
status.resolvedSlots[]objectResolvedSlots publishes, per declared slot, how a value becomes the SpiceDB object id — derived by the reconciler from the tools, never authored. Anything writing a slot grant ahead of the call (thread seeding, an approval, a class default) must mint the identical id, and a second authored copy of the chain would drift silently: the grant would be written, never matched, and nothing would error.
status.resolvedSlots[].approverPermissionstringApproverPermission is the permission an approver must hold on the named instance for their approval to count, published only when Standing is required. Empty for session-only, where no local permission is consulted and the session's own approvers decide. Published rather than assumed: the router used to hardcode #owner, which made a type whose owner is a real computed permission indistinguishable from one whose owner is a bare relation nothing ever writes.
status.resolvedSlots[].permission *stringPermission is the permission a grant on this slot confers.
status.resolvedSlots[].resourceType *stringResourceType is the SpiceDB resource type the slot declares.
status.resolvedSlots[].standingstringStanding is how an approval on this slot gets its authority: session-only (the approver's decision is the authority) or required (the approver must already hold the permission on the named instance). Resolved from the type's schema fragments and the admin veto — an OBSERVATION, so it lives here rather than on spec.
status.resolvedSlots[].valueTransforms[]stringValueTransforms is the transform chain a free-form value passes through to become the object id, in order, as declared by the tools that key this type through resourceIDExpr. Empty means this slot is not value-keyed: its ids are already distinct resources and are used as-is.
status.userlessInputbooleanUserlessInput reports whether a session of this class can be BORN with no human on it: at least one bound Channel with an inbound role (input or both) is of a kind whose Kind.UserAttributable() is false AND whose Kind.SpawnsSessionOnInbound() is true — github and bento today, a webhook payload and a cron tick, neither of which names a person who asked for anything. Both halves, because every rule below is about a session the inbound BROUGHT INTO EXISTENCE. A kind that spawns nothing (agent, the conversational-delegation transport the operator binds to a child's class for the life of a SubagentRequest) carries no human either, but every session on it was pre-created with its own attribution and its own standing — so it is NOT counted here, and none of the rules fire for it. One fact, several unrelated-looking rules. There is no user to attribute a session to (spec.authz.session.interactPermission becomes required, and so does the Channel's own spec.authzSubject); with none declared, the membership of the class's single role=output Channel is derived as the effective one (status.derivedSessionInteractPermission) and written to SpiceDB per session; there is no inbound message to reply to (a role=output Channel must carry its own destination, since nothing inbound supplies one); and a session with no human on any leg acts as the service subject its Channel declared. Published as an OBSERVATION so each of those consumers reads the answer rather than re-listing this class's Channels and re-deriving the predicate — a second copy of the walk is a second place for the next rule to be forgotten, and a second place for one consumer to disagree with the others about which Channels count. Derived by the AgentClass reconciler from the same Channel list that produces status.boundChannels, on every reconcile, BEFORE the Valid condition is decided. So a consumer that has observed Valid=True on this class has observed a derived value: false there means "derived false", not "not yet derived".
* required