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