SidecarToolbox

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

SidecarToolbox declares a user-supplied MCP server that runs as a sidecar container alongside the agent runner pod: its image, sandbox shape, tool allowlist, CEL constraints and egress policy.

Namespaced. Reconciled by pkg/controllers/sidecartoolbox, which validates the spec and runs a one-shot probe Pod to confirm the image is reachable and its live tool list matches the allowlist. Upstream credentials are resolved by the AgentSession reconciler at pod-create time and frozen for the life of the pod -- there is no just-in-time refresh for a sidecar.

Spec

FieldTypeDescription
spec.configstringConfig is an opaque configuration document for the sidecar image, delivered verbatim as the AP_SIDECAR_CONFIG environment variable. The platform does not interpret it; validation belongs to whoever admits the object (the workshop webhook parses it for first-party images known to consume one, e.g. ap-api-adapter). Bounded because it rides in the pod environment. MaxLength counts Unicode code points, not bytes — up to 4 bytes each in UTF-8 — so it alone admits up to 262144 bytes. The BYTE cap (what execve and apiadapter.MaxConfigBytes both count) is enforced by apiadapter.Parse, which both the workshop webhook (at admission) and the adapter binary (at boot) run. Deliberately NOT a CEL XValidation here: this spec type is embedded by value into AgentSession's status.resolvedSidecarToolboxes snapshot, and a CEL rule that reaches a status path both trips the status-CEL guard (pkg/platform/settings' TestNoUnguardedStatusCELRules) and blows the apiserver's CRD rule-cost budget inside that unbounded status array — the agentsessions CRD is then refused at install. Residual admin-path risk (no webhook, a >131072-byte multi-byte document): execve fails with E2BIG and the kubelet reports the container-start error on the Pod.
spec.intentstringIntent is the one-line purpose shown to authors and reviewers.
spec.isolationstringIsolation declares that this toolbox must run in its own per-session pod even though it has no SecretInputs. A secret-gated toolbox (any SecretInputs) is ALREADY isolated for its own reason -- its gating secret only exists per-session, so it cannot be baked into the runner pod's spec -- regardless of this field; Isolation is for the toolbox that needs a separate pod's consequences WITHOUT a gating secret: - Its own NetworkPolicy, scoped to exactly this sidecar's ingress/ egress, instead of inheriting the runner pod's shared policy (see pkg/controllers/agentsession/netpol.go: BuildRunnerNetworkPolicy vs BuildSidecarNetworkPolicy). - Eligibility for BuildSidecarPod's identity branch (a projected ServiceAccount token + operator env), which is reachable only in separate-pod mode -- an in-pod sidecar can never receive it. Absent (or "auto") never changes an existing toolbox's run mode: today's in-pod behavior is preserved exactly. (enum: auto | isolated)
spec.mcpUiAppToolsobjectMCPUIAppTools is ACCEPTED BUT NOT CONSUMED: setting enabled: true changes nothing, and no error, log, or condition says so. It is typed identically to MCPServer's stanza so a toolbox can declare the intent to be an MCP-UI app-tool origin, but nothing reads it, so no sidecar tool is browser-callable. Recorded, not enforced — the same posture as this CRD's effectiveAllowedHosts. A toolbox tool named in AgentUI.spec.tools is therefore denied by the three-way grant's origin condition: the right fail-closed answer for an unwired origin, just not the one this field's name suggests. Absent ⇒ disabled: the permanent, fail-closed default.
spec.mcpUiAppTools.enabled *booleanEnabled turns the capability on; no app-visible tool is callable without it.
spec.mcpUiAppTools.maxCallsPerMininteger (int32)MaxCallsPerMin optionally caps autonomous app-tool calls per minute. Enforced by a DEDICATED per-origin limiter on the app-tool call path (runner.AppToolRateLimiter, wired in internal/cmd/runner from this field) — deliberately NOT a toolguard rule, which would also gate this MCPServer's LLM-visible tools and strip its circuit breaker. Nil ⇒ unlimited. (min 1)
spec.name *stringName is the toolbox's toolspec-level name.
spec.sandbox *objectSandbox is the SpiceboxClass shape and egress the sidecar runs under.
spec.sandbox.class *stringClass names a SpiceboxClass (cluster-scoped) whose resources, runtimeClassName, default network mode, pidsLimit are inherited.
spec.sandbox.networkobjectNetwork widens the class's egress for this toolbox.
spec.sandbox.network.allowedHosts[]stringAllowedHosts merges with the SpiceboxClass's network.allowedHosts. If the class's mode is "none" and any AllowedHosts are supplied here, the effective sidecar network upgrades to "allowlist" with exactly these hosts.
spec.secretInputs[]objectSecretInputs binds secret-output handles (produced by an earlier tool call) to this toolbox. A toolbox with any SecretInput is "secret-gated": it is NOT injected into the agent pod; the operator runs it as a separate per-session pod once the bound secret is available.
spec.secretInputs[].deliver *stringDeliver is how the value reaches the sidecar at startup: "env" (env var named Name) or "file:<path>" (mounted file at <path>).
spec.secretInputs[].from *stringFrom is the secret-output logical name the producer emitted (matches the producer's secretOutput.name / the per-session Secret key).
spec.secretInputs[].name *stringName is the env-var name (when Deliver=="env") or the logical key.
spec.source *objectSource is where the sidecar's container image comes from.
spec.source.imagestringImage is a prebuilt container image reference.
spec.source.inlineobjectInline builds the sidecar from a script layered onto a base image.
spec.source.inline.baseImage *stringBaseImage is the image the script runs on top of.
spec.source.inline.entrypoint *[]stringEntrypoint is the argv that starts the server inside the container.
spec.source.inline.script *objectScript is where the server's source comes from.
spec.source.inline.script.configMapRef *objectConfigMapRef names the ConfigMap key holding the script body.
spec.source.inline.script.configMapRef.key *stringKey is the data key within the ConfigMap.
spec.source.inline.script.configMapRef.name *stringName is the ConfigMap's name, in the toolbox's own namespace.
spec.spicedbSchemaobjectSpiceDBSchema declares the SpiceDB resource definitions this toolbox contributes — identical to MCPServer.spec.spicedbSchema. The guardian's schema composer concatenates fragments across every MCPServer AND SidecarToolbox in the cluster so the audiences the tags derive from resolve; identical declarations dedupe, conflicting ones fail the reconcile.
spec.spicedbSchema.rawZedstringRawZed is appended verbatim to the composed schema after Resources are emitted. Use this for SpiceDB schema features the structured form can't represent: subject-relations (e.g. slack_user#user) and unions (e.g. user | agent). The composer does not parse or validate RawZed beyond schema-load time; malformed text fails the SpiceDB WriteSchema call.
spec.spicedbSchema.resources[]objectResources lists structured SpiceDB definitions contributed by this fragment. Each resource is emitted with its declared relations and permissions; the composer dedupes by name across fragments.
spec.spicedbSchema.resources[].approverPermissionstringApproverPermission names the permission an approver must hold on an instance for their approval to count. Required when Standing is required, and must be empty when it is session-only — a type nothing governs has no permission to name, and accepting one there would read as governance that is never consulted. Declared rather than assumed. This was hardcoded to "owner" at four call sites, which silently made two very different types look identical to the approval router: crm_company's owner is a real computed permission backed by seeded tuples, while git_repo's owner is a bare relation declared only so the checks are answerable and never populated by anything. The router could not tell governance from an artifact of schema shape, so a push to a git remote resolved to an empty owner-set and became unapprovable forever. Naming it also lifts the assumption that the approving permission is called "owner": a type may route approval through maintainer, admin, or any permission its own schema defines.
spec.spicedbSchema.resources[].displayobjectDisplay declares how an INSTANCE of this resource type presents on an approval card — an icon, and how to turn its raw value (a URL, typically) into a short label. Absent means the card falls back to the wire type name, exactly as before this field existed.
spec.spicedbSchema.resources[].display.iconstringIcon names a glyph from a CLOSED, code-defined registry — never a URL and never free-form. A vendor-specific type (github_repo) may name the vendor's own mark; a host-agnostic type (git_repo) must name a generic one, because claiming a vendor mark would lie the moment an instance points somewhere else (a self-hosted remote). An unrecognized name renders no icon — never a fallback image, never a guess.
spec.spicedbSchema.resources[].display.labelstringLabel names a deriver from a CLOSED, code-defined set that turns an instance's raw value into a short display label — "url_path", "b64url_path", "last_segment", or "none" (the set registered in pkg/authz/plangate's labelDerivers, which is the authority; this list is prose and has drifted from it once). Not a template and not a regex: nothing here can produce a label the code did not author, and the deriver never sees anything an agent did not itself write into the plan (the instance value), so a declaration can shorten a value's presentation but can never fabricate one. An unrecognized name derives nothing, and the card falls back to Name.
spec.spicedbSchema.resources[].display.namestringName is the human name for this resource TYPE — "Git repository", never the wire handle "git_repo". Shown when no per-instance label can be derived (Label is "none", unset, or the deriver produces nothing for this instance's value).
spec.spicedbSchema.resources[].name *stringName is the SpiceDB definition name; the composer dedupes on it.
spec.spicedbSchema.resources[].permissions[]objectPermissions are the resource's computed permissions.
spec.spicedbSchema.resources[].permissions[].expr *stringExpr is the right-hand side of permission &lt;name&gt; = …, pasted verbatim.
spec.spicedbSchema.resources[].permissions[].name *stringName is the permission name.
spec.spicedbSchema.resources[].permissions[].planningNotestringPlanningNote is one line of guidance the agent reads while DECLARING a plan, rendered next to the handles it may declare. The knowledge that shapes a good plan is toolkit-specific, so it lives with the toolkit rather than in the runner's generic prompt. git_repo splits its permissions across two instances — read/write key on the checked-out copy, fetch/push on the remote URL — so a phase declaring write without read can change files it cannot open, and the first read interrupts the human with an amendment the plan should have carried from the start. It shapes what the agent DECLARES, never what anything grants: a note is prose the model reads at plan time, and no authorization decision reads it. A phase that ignores its guidance still gets the gate it earned.
spec.spicedbSchema.resources[].permissions[].titlestringTitle is the phrase a human reads on an approval card instead of the permission's handle. perm:push:git_repo is a WIRE FORMAT; asking somebody to decide on it makes the decision slower and worse exactly where care matters most. Declared here because this is where the permission itself is declared, so one declaration serves every channel and the CLI. Absent is fine and common: the card detokenizes the handle instead, which shows no plumbing and invents no English.
spec.spicedbSchema.resources[].relations[]objectRelations are the resource's relations, emitted verbatim.
spec.spicedbSchema.resources[].relations[].name *stringName is the relation name as it appears in the composed schema.
spec.spicedbSchema.resources[].relations[].subjectType *stringSubjectType must be a bare resource-type name (e.g. "user", "hubspot_owner") declared in this same SpiceDBSchema or the implicit "user" type. Wildcards ("user:*") are expressed via the separate Wildcard field. Subject-relation forms ("team#member") are NOT representable. Exactly one SubjectType per Relation — SpiceDB unions like relation viewer: user | team#member cannot be modeled.
spec.spicedbSchema.resources[].relations[].wildcardbooleanWildcard, when true, makes the relation accept ANY subject of SubjectType — emitted as relation &lt;name&gt;: &lt;subjectType&gt;:* in the composed schema. Use sparingly: a wildcard relation effectively grants the underlying-resource permission to every subject of that type, so any per-call gating must come from a different layer (e.g. stateImpact: external on the tool, which routes every call through the approval flow regardless of the SpiceDB Check result).
spec.spicedbSchema.resources[].standing *stringStanding declares whether SpiceDB is AUTHORITATIVE for this resource type — that is, whether an approver can be expected to already hold a permission on an instance of it. REQUIRED, with no default. - required SpiceDB governs who may approve. The approver pool is ApproverPermission on the named instance, and an EMPTY pool is a final refusal — nobody can approve what nobody governs. - session-only No local permission governs approval for this type, so the session's own approvers decide and their decision IS the authority. Everything downstream is unchanged: the grant is still written, still expiring, still session-scoped and revocable, and the tool-call Check still runs. There is deliberately NO default. A default is a hole you open by omission: defaulting to session-only silently widens who may approve a type somebody forgot to classify, and defaulting to required makes a forge-governed type permanently unbindable because nothing writes a SpiceDB tuple for every git remote. Neither failure announces itself, so the author states the answer. Same reasoning as AP_CLUSTER_KIND, which also refuses to default. Choosing is a question about the RESOURCE, not about convenience: does a permission on this instance already say who may speak for it? A CRM company with seeded owner tuples: yes, required. A git remote whose permissions live at the forge: no, session-only. A cluster or namespace admin can force any type to required with SettingsLimits.RequireStandingFor, which no fragment may widen past. (enum: session-only | required)
spec.toolResourceMap[]objectToolResourceMap declares, per-tool, the SpiceDB resource a read accesses — identical semantics to MCPServer.spec.toolResourceMap. Consumed by the runner's information-leakage gate so a sidecar tool participates in per-datum egress (its result mints a pt-tag) instead of falling to the undeclared coarse floor.
spec.toolResourceMap[].noTaintbooleanNoTaint marks the tool as a pure utility that reads no user data. Set true to opt out of taint capture and the requester view check.
spec.toolResourceMap[].readsobjectReads declares the resource the tool reads. Set this OR NoTaint; unset both is treated as "unmapped" and may block tool dispatch depending on AgentClass.Authz.InformationLeakage.Mode.
spec.toolResourceMap[].reads.bypassRequesterCheckbooleanBypassRequesterCheck, when true, skips the requester view check. The taint is still recorded; this is useful for agents whose service identity legitimately has broader access than the requester.
spec.toolResourceMap[].reads.idArgstringIDArg names a top-level field inside the tool's args envelope whose string value is the SpiceDB resource ID. Mutually exclusive with ResultIDField.
spec.toolResourceMap[].reads.permission *stringPermission is the SpiceDB permission checked against the requester (e.g. "view").
spec.toolResourceMap[].reads.resourceType *stringResourceType is the SpiceDB definition name (e.g. "linear_issue").
spec.toolResourceMap[].reads.resultIDFieldstringResultIDField names a top-level field on the tool result's content (parsed as JSON) whose string value is the SpiceDB resource ID. Set this when the agent's input identifier differs from the SpiceDB resource ID (e.g. Linear id: "L-140" returns a UUID in the response). When set, the pre-execute requester view check happens AFTER the call against the response-extracted ID. Mutually exclusive with IDArg.
spec.toolResourceMap[].tool *stringTool is the MCP tool name this mapping applies to.
spec.tools *[]objectTools mirrors MCPServerTool field-for-field — same allowlist, CEL, deny effects, sensitive fields. See MCPServerTool for details.
spec.tools[].argsobjectArgs is the argument allowlist and CEL constraints for this tool.
spec.tools[].args.allowedFields[]stringAllowedFields lists the argument keys the agent may pass to this tool. Enforcement is fail-closed: an empty/unset AllowedFields DENIES any argument the agent passes (an arg-less tool still works). To accept free-form arguments, set UnconstrainedArgs=true instead of leaving this empty.
spec.tools[].args.constraints[]objectConstraints are CEL predicates every call's args must satisfy (AND).
spec.tools[].args.constraints[].cel *stringCEL is a boolean expression over the call's args; false denies the call.
spec.tools[].args.constraints[].messagestringMessage is the denial text shown when CEL evaluates false.
spec.tools[].args.sensitiveFields[]stringSensitiveFields names arg keys to redact from logs and approval prompts.
spec.tools[].args.unconstrainedArgsbooleanUnconstrainedArgs explicitly opts this tool out of AllowedFields enforcement, for a tool that legitimately accepts arbitrary arguments. Required to allow free-form args — an empty AllowedFields alone is fail-closed, not allow-all.
spec.tools[].denyobjectDeny lists effect and trust capabilities that refuse the call pre-dispatch.
spec.tools[].deny.effectsobjectEffects denies calls by declared effect (destructive, reads, writes).
spec.tools[].deny.effects.credsobjectCreds denies credential-touching effects.
spec.tools[].deny.effects.creds.writesbooleanWrites denies any tool that would write credential material.
spec.tools[].deny.effects.destructivebooleanDestructive denies any tool the server marks destructive.
spec.tools[].deny.effects.reads[]stringReads names resource kinds whose reads are denied.
spec.tools[].deny.effects.writes[]stringWrites names resource kinds whose writes are denied.
spec.tools[].deny.trustobjectTrust denies calls by asserted SEP-1913 trust capability.
spec.tools[].deny.trust.destinationPublicbooleanDestinationPublic denies the call when it would write somewhere publicly visible.
spec.tools[].deny.trust.outcomesIrreversiblebooleanOutcomesIrreversible denies the call when the server declares its effects cannot be undone.
spec.tools[].deny.trust.sourceUntrustedPublicbooleanSourceUntrustedPublic denies the call when it would read untrusted public content into the agent's context.
spec.tools[].descriptionOverridestringDescriptionOverride replaces the server's own description in the prompt; empty keeps what the server sent.
spec.tools[].effectsobjectEffects is the declared effect profile used for approval and gating.
spec.tools[].effects.destructivebooleanDestructive means a call can remove or overwrite upstream state.
spec.tools[].effects.idempotentbooleanIdempotent means repeating a call has the same effect as making it once.
spec.tools[].effects.openWorldbooleanOpenWorld means the call may reach systems beyond the named server.
spec.tools[].effects.readOnlybooleanReadOnly means a call mutates nothing upstream.
spec.tools[].intentstringIntent is the one-line purpose shown to authors and reviewers.
spec.tools[].labels[]objectLabels declares per-tool CEL blocks extracting (resourceType, id, name) tuples from tool responses for approval-prompt rendering. Evaluated after a SUCCESSFUL call; failures are non-fatal (logged and skipped). Labels never reach any LLM context.
spec.tools[].labels[].forEachstringForEach is a CEL expression that must evaluate to a list. One label is emitted per element with item bound to that element. When unset, exactly one label is emitted with item=nil.
spec.tools[].labels[].label *objectLabel is the (resourceType, id, name) CEL triple. All three fields are required CEL string expressions.
spec.tools[].labels[].label.id *stringID is a CEL string expression yielding the resource id.
spec.tools[].labels[].label.name *stringName is a CEL string expression yielding the friendly label shown to approvers. Capped + sanitized at extraction time.
spec.tools[].labels[].label.resourceType *stringResourceType is a CEL string expression yielding the SpiceDB definition name (e.g. "crm_company").
spec.tools[].labels[].whenstringWhen is a CEL boolean expression with args, result in scope; the block is skipped if it evaluates false.
spec.tools[].name *stringName is the tool name as the upstream server reports it.
spec.tools[].observes[]objectObserves declares facts this tool's result asserts about specific resource instances, co-derived with the subjects they are about. Evaluated after a SUCCESSFUL call.
spec.tools[].observes[].facts *map[string]stringFacts maps a fact name to a CEL expression yielding its value, evaluated against the same item as Subjects.
spec.tools[].observes[].forEachstringForEach is a CEL expression yielding a list. One observation is emitted per element with item bound to it. Unset emits exactly one, with item bound to nil.
spec.tools[].observes[].subjects *[]objectSubjects are the objects this observation is about, as CEL string expression pairs. At least one is REQUIRED: a block recording facts about nothing would produce a session-scoped boolean that answers for every instance at once.
spec.tools[].observes[].subjects[].resourceID *stringResourceID is a CEL string expression yielding the object id.
spec.tools[].observes[].subjects[].resourceType *stringResourceType is a CEL string expression yielding a SpiceDB definition name (commonly a literal, e.g. "github_pr").
spec.tools[].observes[].whenstringWhen is a CEL boolean over args, result; the block is skipped if it evaluates false. Unset means always taken.
spec.tools[].permissionobjectPermission declares the per-tool authz policy. Optional in the schema only: a tool without one fails AgentClass-time validation.
spec.tools[].permission.checkobjectPermissionCheck names the SpiceDB resource and permission to evaluate. Required when StateImpact is Readonly / Readwrite / External; forbidden when Stateless / Passthrough.
spec.tools[].permission.check.enforceModestringEnforceMode controls deny-finality under permissive toolAuthMode. Empty → EnforceInherit (slice-1 default).
spec.tools[].permission.check.extractionPromptstringExtractionPrompt, when set, is an English fragment the runner includes in the extraction LLM's system prompt for this resource type. Tool spec authors write this once per toolspec/MCPServer file. The runner aggregates ExtractionPrompts across all tools whose Check targets the same resourceType (dedup'd, concatenated). The AgentClass's BoundEntityType.ExtractionPrompt overrides entirely if set.
spec.tools[].permission.check.grantBindsArgs[]stringGrantBindsArgs, when set, restricts the slice-2 grant's arguments_hash caveat binding to only the listed top-level arg keys. Default (nil/empty) hashes the full arg map, meaning the grant satisfies only the exact same call. Setting it to e.g. ["repo"] lets a single approval cover every subsequent call against the same repo regardless of other args (pr number, etc.) — useful for readonly tools where the resource is the load-bearing input. The hash is an HMAC-SHA256 under the per-session args-hash key minted by the AgentSession reconciler into the per-session Secret and read by the runner at startup. Caveat contexts therefore cannot be predicted or minted outside the controller+runner trust domain — neither the agent nor a confused deputy with SpiceDB write access can forge a valid binding for arguments that were never requested. Note: hashes of previously-requested (including denied) calls remain observable in session status and channel payloads, so a SpiceDB-write attacker could still replay those; the keying narrows that surface to exactly the calls a human has already seen. See pkg/authz/guardian/grants.ArgsHashFiltered for the filtering contract. NOTE: the hash no longer BINDS the grant. A slot grant is keyed on (instance, permission), which is what makes it safe to reuse across calls: a grant shaped for one permission cannot be spent on another against the same id. The filtered hash is still computed and published on the approval request (ToolApprovalDetails.ArgsHash) so an approver — and the audit log — can see exactly which call was asked about.
spec.tools[].permission.check.permission *stringPermission is the SpiceDB permission (or relation) name to Check (e.g. "read", "write", "admin"). Static. Pattern-validated at admission for the same reason as ResourceType. Mirrors BoundEntityType.Permission.
spec.tools[].permission.check.resourceIDExprstringResourceIDExpr is a CEL string expression evaluated against args that yields the SpiceDB resource id. Exactly one of ResourceIDTemplate or ResourceIDExpr must be set: the template form for simple &#123;arg&#125; interpolation, the Expr form for nested-arg extraction.
spec.tools[].permission.check.resourceIDHintstringResourceIDHint is shown to the AGENT when the resource id cannot be resolved from the call's arguments. That failure is usually the caller's to fix — it named no resource, or named it in a form the check cannot read — and the agent is the only party who can retry. Without a hint the message is the raw resolution error prefixed "internal:", which reads as a system fault and tells it not to bother. Written by whoever authors the check, because only they know what the call should have looked like. Example, for a git push whose remote must be a URL so the repository can be authorized: "name the remote as a full https:// URL, not a shorthand like origin".
spec.tools[].permission.check.resourceIDTemplatestringResourceIDTemplate is a string with {arg} placeholders that interpolate from the tool call's args (e.g. "{owner}/{repo}"). Resolved at runtime by ResolveTemplate; validated at AgentClass-reconcile time so every {arg} matches a parameter the tool declares.
spec.tools[].permission.check.resourceIDTransforms[]stringResourceIDTransforms names registered transforms applied IN ORDER to the resolved template string before SpiceDB sees it: lowercase, remove_spaces, spicedb_object_id, basename, sha256. See transforms.go.
spec.tools[].permission.check.resourceType *stringResourceType is the SpiceDB definition the Check runs against (e.g. "github_repo", "channel"). Static — no template interpolation. Pattern-validated at admission because this value is a component of a permsurface.Handle, which appears in approved plan ceilings and audit records. Mirrors BoundEntityType.ResourceType.
spec.tools[].permission.stateImpact *stringStateImpact describes the policy mode for a tool / subcommand.
spec.tools[].permission.toolNamestringToolName lets Layer 2's CheckScope evaluate the tool allow/deny axis. Wire from the registered tool name. Empty string skips the Layer 2 tool check (existing behavior).
spec.tools[].permissionVariants[]objectPermissionVariants are conditional Permission blocks, each carrying a CEL When predicate over the call's args. First match wins; Permission above is the fallback when none match.
spec.tools[].permissionVariants[].check *objectCheck is the Permission applied when When matches. Same shape as the slice-1 singular Permission's Check, plus the new EnforceMode and ResourceIDExpr fields.
spec.tools[].permissionVariants[].check.checkobjectPermissionCheck names the SpiceDB resource and permission to evaluate. Required when StateImpact is Readonly / Readwrite / External; forbidden when Stateless / Passthrough.
spec.tools[].permissionVariants[].check.check.enforceModestringEnforceMode controls deny-finality under permissive toolAuthMode. Empty → EnforceInherit (slice-1 default).
spec.tools[].permissionVariants[].check.check.extractionPromptstringExtractionPrompt, when set, is an English fragment the runner includes in the extraction LLM's system prompt for this resource type. Tool spec authors write this once per toolspec/MCPServer file. The runner aggregates ExtractionPrompts across all tools whose Check targets the same resourceType (dedup'd, concatenated). The AgentClass's BoundEntityType.ExtractionPrompt overrides entirely if set.
spec.tools[].permissionVariants[].check.check.grantBindsArgs[]stringGrantBindsArgs, when set, restricts the slice-2 grant's arguments_hash caveat binding to only the listed top-level arg keys. Default (nil/empty) hashes the full arg map, meaning the grant satisfies only the exact same call. Setting it to e.g. ["repo"] lets a single approval cover every subsequent call against the same repo regardless of other args (pr number, etc.) — useful for readonly tools where the resource is the load-bearing input. The hash is an HMAC-SHA256 under the per-session args-hash key minted by the AgentSession reconciler into the per-session Secret and read by the runner at startup. Caveat contexts therefore cannot be predicted or minted outside the controller+runner trust domain — neither the agent nor a confused deputy with SpiceDB write access can forge a valid binding for arguments that were never requested. Note: hashes of previously-requested (including denied) calls remain observable in session status and channel payloads, so a SpiceDB-write attacker could still replay those; the keying narrows that surface to exactly the calls a human has already seen. See pkg/authz/guardian/grants.ArgsHashFiltered for the filtering contract. NOTE: the hash no longer BINDS the grant. A slot grant is keyed on (instance, permission), which is what makes it safe to reuse across calls: a grant shaped for one permission cannot be spent on another against the same id. The filtered hash is still computed and published on the approval request (ToolApprovalDetails.ArgsHash) so an approver — and the audit log — can see exactly which call was asked about.
spec.tools[].permissionVariants[].check.check.permission *stringPermission is the SpiceDB permission (or relation) name to Check (e.g. "read", "write", "admin"). Static. Pattern-validated at admission for the same reason as ResourceType. Mirrors BoundEntityType.Permission.
spec.tools[].permissionVariants[].check.check.resourceIDExprstringResourceIDExpr is a CEL string expression evaluated against args that yields the SpiceDB resource id. Exactly one of ResourceIDTemplate or ResourceIDExpr must be set: the template form for simple &#123;arg&#125; interpolation, the Expr form for nested-arg extraction.
spec.tools[].permissionVariants[].check.check.resourceIDHintstringResourceIDHint is shown to the AGENT when the resource id cannot be resolved from the call's arguments. That failure is usually the caller's to fix — it named no resource, or named it in a form the check cannot read — and the agent is the only party who can retry. Without a hint the message is the raw resolution error prefixed "internal:", which reads as a system fault and tells it not to bother. Written by whoever authors the check, because only they know what the call should have looked like. Example, for a git push whose remote must be a URL so the repository can be authorized: "name the remote as a full https:// URL, not a shorthand like origin".
spec.tools[].permissionVariants[].check.check.resourceIDTemplatestringResourceIDTemplate is a string with {arg} placeholders that interpolate from the tool call's args (e.g. "{owner}/{repo}"). Resolved at runtime by ResolveTemplate; validated at AgentClass-reconcile time so every {arg} matches a parameter the tool declares.
spec.tools[].permissionVariants[].check.check.resourceIDTransforms[]stringResourceIDTransforms names registered transforms applied IN ORDER to the resolved template string before SpiceDB sees it: lowercase, remove_spaces, spicedb_object_id, basename, sha256. See transforms.go.
spec.tools[].permissionVariants[].check.check.resourceType *stringResourceType is the SpiceDB definition the Check runs against (e.g. "github_repo", "channel"). Static — no template interpolation. Pattern-validated at admission because this value is a component of a permsurface.Handle, which appears in approved plan ceilings and audit records. Mirrors BoundEntityType.ResourceType.
spec.tools[].permissionVariants[].check.stateImpact *stringStateImpact describes the policy mode for a tool / subcommand.
spec.tools[].permissionVariants[].check.toolNamestringToolName lets Layer 2's CheckScope evaluate the tool allow/deny axis. Wire from the registered tool name. Empty string skips the Layer 2 tool check (existing behavior).
spec.tools[].permissionVariants[].when *stringWhen is a CEL boolean expression with args in scope.
spec.tools[].trustobjectTrust is the SEP-1913 trust + action-security annotation snapshot. Mirrors mcpspec.Trust.
spec.tools[].trust.attribution[]stringAttribution names the parties the server credits for the tool.
spec.tools[].trust.inputMetadataobject (free-form)InputMetadata is the server's per-argument security annotations, verbatim.
spec.tools[].trust.maliciousActivityHintbooleanMaliciousActivityHint is the server's own admission that this tool can be abused; self-reported, so treat it as a signal, not a guarantee.
spec.tools[].trust.returnMetadataobject (free-form)ReturnMetadata is the server's per-result security annotations, verbatim.
spec.tools[].visibility[]stringVisibility mirrors mcpspec.Tool.Visibility (MCP Apps' _meta.ui.visibility): which surfaces ("model" / "app") a tool is exposed to. This repo's routing is an EXCLUSIVE split, not a fan-out (see pkg/agent/tool/mcp/synthesize.go): a tool is browser-callable if and only if this list contains "app" and does NOT contain "model", and the owning MCPServer sets mcpUiAppTools.enabled. Concretely: unset / [] -> model-visible only; never reaches the browser ["app"] -> browser-callable (with the opt-in); withheld from the model ["app"] no opt-in -> rejected by default: in NEITHER registry ["model"] -> model-visible only ["app","model"] -> model-visible only; NOT browser-callable So "unset" is not "both": it is the model surface. A tool intended for an agent UI must say ["app"] and nothing else. oap agent lint reports any other value for a tool an AgentUI binds to.
spec.tools[].writesRelationships[]objectWritesRelationships declares JIT SpiceDB relationship writes the dispatcher performs after a successful tool call. When and ForEach are CEL over (args, result); the tuple fields are CEL string expressions.
spec.tools[].writesRelationships[].exclusivebooleanExclusive makes this write atomically write-once per subject: the write FAILS (no tuple written) if the subject already holds relation on ANY resource of the tuple's resource type. Used for session-pin semantics (a session may be pinned to exactly one cluster). Implemented as a SpiceDB MUST_NOT_MATCH precondition, so it is atomic under concurrent writers. Default false preserves the plain TOUCH-upsert behavior.
spec.tools[].writesRelationships[].forEachstringForEach is a CEL expression that must evaluate to a list. One tuple is emitted per element, with item bound to that element. When unset, exactly one tuple is emitted.
spec.tools[].writesRelationships[].requireSlotBoundbooleanRequireSlotBound refuses every tuple this block emits unless the calling session holds a SLOT GRANT on the tuple's RESOURCE — the instance a human (or the pool machinery acting on one's approval) named for this session. Declare it on a block that writes an identity or an authority tuple onto an instance the tool itself names. Without it, whatever id the tool's response happens to carry becomes the resource of a real SpiceDB write, so a response naming somebody else's instance writes there too. With it, the write can only ever land on an instance the session was already bound to. The grant's PERMISSION is deliberately not consulted: a grant is a human act naming the instance, and which permission it carries is the pool machinery's concern. Any slot_grant_* on the resource binds it. Default false is byte-identical to the previous behaviour — an unmarked block consults nothing. A marked block whose dispatcher has no checker wired is REFUSED, not written: see pkg/authz/relwrites.Run.
spec.tools[].writesRelationships[].tuple *objectTuple holds the CEL expressions that compose the SpiceDB relationship tuple.
spec.tools[].writesRelationships[].tuple.relation *stringRelation is a CEL string expression that must evaluate to a relation name declared on the Resource's type.
spec.tools[].writesRelationships[].tuple.resource *stringResource is a CEL string expression that must evaluate to a SpiceDB object reference of the form "<type>:<id>".
spec.tools[].writesRelationships[].tuple.subject *stringSubject is a CEL string expression that must evaluate to a SpiceDB object reference of the form "<type>:<id>".
spec.tools[].writesRelationships[].whenstringWhen is a CEL boolean expression with args, result in scope; the block is skipped if it evaluates false. When unset, the block is always taken.
spec.transport *objectTransport is how the runner reaches the sidecar's MCP endpoint.
spec.transport.healthcheckobjectHealthcheck is how the operator's probe decides the sidecar is up.
spec.transport.healthcheck.pathstringPath is the HTTP path probed for readiness. (default: /healthz)
spec.transport.healthcheck.timeoutSecondsinteger (int32)TimeoutSeconds bounds how long the probe waits for the sidecar to answer. (default: 30; min 1)
spec.transport.pathstringPath is the HTTP path the sidecar's MCP Streamable-HTTP endpoint listens on (e.g. "/mcp"); empty means the pod root "/". The runner appends it to the pod URL for BOTH the tools/list reachability probe and tool-call dispatch, so it must match where the sidecar actually serves MCP. A leading slash is optional — the runner normalizes it.
spec.transport.portinteger (int32)Port is advisory — the operator allocates a free port at pod-build time and sets MCP_PORT in the sidecar's env regardless. Sidecar code is required to read MCP_PORT to bind. (default: 8080)
spec.upstreamAuth *objectUpstreamAuth is the credential the sidecar needs for its own upstream.
spec.upstreamAuth.envVarstringEnvVar is the environment variable name the resolved upstream credential is injected into (in the per-session sidecar Secret). Empty means the sidecar needs no upstream credential (e.g. the echo example) — the per-session Secret is written empty.
spec.upstreamAuth.provider *stringProvider names a provider in the /providers/ library, or the UpstreamAuthProviderNone sentinel ("none") for a controller-issued-token sidecar that needs no AgentIdentity credential. Drives oap agent setup-identity and projects the resolved credential into the sidecar's env at session boot via the toolbox: authkind.
spec.version *stringVersion is the spec author's version of this declaration.
* required

Status

Status is controller-owned (observed state).

FieldTypeDescription
status.conditions[]objectConditions carries Valid, Reachable and PinDrift.
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.lastValidatedAtstring (date-time)LastValidatedAt is when the spec last passed validation.
status.observedGenerationinteger (int64)ObservedGeneration is the spec generation this status reflects.
status.observedTools[]stringObservedTools is the tool list the admission-time probe saw. Not read at runtime — the runner's live per-session probe is authoritative there.
status.pinobjectPin is the recorded pin baseline in the common PinRecord shape: Digest = kubelet-resolved image digest (sha256:…), Version = declared image tag (audit metadata). Strength reflects the declared ref: frozen iff the source image is by-digest, named iff by-tag, else unpinned.
status.pin.detailsmap[string]stringDetails carries kind-specific extras (toolCount, registry host, …).
status.pin.digeststringDigest is the immutable identity: git sha, sha256:… image digest, canonical tool-manifest hash, or binary hash.
status.pin.kind *stringKind is the pinning registry kind name (skill, image, mcp, cli, …). Validated against the registry by controllers, not by a CRD enum, so new kinds register without an API change.
status.pin.observedAtstring (date-time)ObservedAt is when the recording controller observed this identity.
status.pin.strength *stringStrength is the syntactic pin strength of the declared ref. (enum: frozen | named | unpinned)
status.pin.versionstringVersion is the human-readable identity: tag, serverInfo.version, or version-probe output.
* required