MCPServer
Group agentprimitives.authzed.com · Scope Namespaced · Short names mcpsrv
MCPServer declares one MCP endpoint an agent may call: where it lives (spec.server), the allowlisted tools, per-tool CEL argument constraints, the credential used at runtime, and the SpiceDB schema fragment its resources need.
Namespaced. Reconciled by pkg/controllers/mcpserver, which probes the server's tools/list, compiles every constraint, and reflects the outcome on Valid and Reachable. The probe is unauthenticated -- runtime auth resolves spec.auth.credential at session startup, so Reachable=True is not evidence that the agent's credential works.
Spec
| Field | Type | Description |
|---|---|---|
spec.auth | object | Auth is how outbound calls to the server are authenticated. |
spec.auth.credential | string | Credential is the name of the credential (in the agent's identity catalog) that fills this server's auth header. When empty, resolution falls back to the MCPServer's metadata.name. |
spec.auth.description | string | Description is the user-facing "what is this token" sentence for this server's credential. Takes precedence over the provider catalog; empty falls back to the catalog or renders no description. |
spec.auth.header | string | Header is the HTTP header name to set on outbound requests (default "Authorization"). |
spec.auth.provider | string | Provider names a provider in the /providers/ library. The setup engine uses this to drive oap agent setup-identity for this server. At runtime the credential is resolved by name (spec.auth.credential) from the AgentIdentity; this Provider field is metadata for setup, not a runtime hook. |
spec.auth.resource | string | Resource is the identifier the enterprise IdP knows this server by, used as the ID-JAG audience when Type=federated. Required when Type=federated; ignored otherwise. |
spec.auth.title | string | Title is the short user-facing display name for this server's credential ("Linear"). Shown as the bold header of a credential-request row. Takes precedence over the provider catalog; empty falls back to the catalog (via Provider) or a humanized credential name. |
spec.auth.type | string | Type is the authentication mechanism the MCPServer requires. "static": the user supplies a long-lived token (PAT/API key) through the /my/accounts/<credname>/link form. "oauth": the credential comes from the OAuth Authorization Code flow via /link/oauth/<credname>. Empty means the portal falls back to the PAT form. (enum: static | oauth | federated) |
spec.auth.valuePrefix | string | ValuePrefix is prepended to the resolved access_token in the header value (default "Bearer "). |
spec.callTimeout | string | CallTimeout bounds a single tool call; zero uses the dispatcher default. |
spec.generation | object | Generation holds authoring-time test cases for the spec generator. |
spec.generation.testCases | []object | TestCases are the author-supplied allow/deny examples for the spec. |
spec.generation.testCases[].args | object (free-form) | Args is the call's argument envelope. |
spec.generation.testCases[].expectedAllow * | boolean | ExpectedAllow is whether the constraints should admit these args. |
spec.generation.testCases[].intent | string | Intent describes what this case is meant to demonstrate. |
spec.generation.testCases[].toolName * | string | ToolName is the tool the case exercises. |
spec.intent | string | Intent is the one-line purpose shown to authors and reviewers. |
spec.mcpUiAppTools | object | MCPUIAppTools opts this MCPServer into MCP-UI "app"-visible tool calls: a tool whose MCPServerTool.Visibility is EXACTLY ["app"] — present, and NOT also carrying "model"; see that field's own doc for why "including app" is not sufficient — is synthesized into the browser-callable registry instead of the model's (pkg/agent/tool/mcp/synthesize.go). Absent ⇒ disabled (the permanent default): an app-only tool is then synthesized into NEITHER registry. |
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 toolspec-level server name; tool names are prefixed with it. |
spec.pinnedManifestHash | string | PinnedManifestHash, when set, asserts the canonical tools/list manifest hash ("sha256:…", see pkg/authz/pinning/kinds/mcp) this server must serve. Drift from it flips the PinDrift condition and is enforced per the effective pinning mode. When unset, the first observed manifest is recorded as the baseline (trust-on-first-use) in status.pin. |
spec.server * | object | Server is where the endpoint lives and how to talk to it. |
spec.server.transport * | string | Transport is the MCP wire transport to speak to this endpoint. |
spec.server.url * | string | URL is the MCP server endpoint. Production servers should use https://; http:// is permitted for in-cluster servers addressed by a Kubernetes Service DNS name (*.svc / *.svc.cluster.local) and for loopback test stubs. Scheme/SSRF validation is intentionally NOT a CRD loopback/private destinations would also reject the legitimate in-cluster http:// case and the envtest e2e harness (which applies http://127.0.0.1:<port>). The authoritative runtime SSRF backstop is the guarded dialer in pkg/x/safehttp: every client that fetches this URL (the MCPServer controller's probe, the runner's session-start probe, the MCP tool-call dispatcher, and the oap CLI probe paths) resolves the host and refuses any private/loopback/link-local IP. A finer- grained controller-side scheme check (e.g. surfacing a non-https, non-in-cluster URL on the Valid condition) is RECOMMENDED as a future enhancement but is not implemented today — do not rely on the controller for SSRF protection; pkg/x/safehttp is the backstop. |
spec.siteURL | string | SiteURL is the service's user-facing homepage. identityd discovers a favicon from this URL for credential-row UI surfaces (portal, deep-link menu, Slack Home tab, credential-request DM). Optional; unset renders the deterministic generated fallback icon. Must be http or https; admission rejects other schemes. |
spec.spicedbSchema | object | SpiceDBSchema declares the SpiceDB resource definitions this MCPServer contributes. The guardian's schema composer concatenates fragments across every MCPServer an AgentClass references; identical declarations dedupe, conflicting ones fail the AgentClass 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. Consumed by the runner's information-leakage gate. |
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 is the allowlist: a tool the server offers but that is absent here is never callable. |
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.version * | string | Version is the spec author's version of this declaration, not the server's self-reported one. |
Status
Status is controller-owned (observed state).
| Field | Type | Description |
|---|---|---|
status.conditions | []object | Conditions carries Valid, Reachable, PinDrift and SpiceDBSchemaValid. |
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 the last time the server spec was successfully validated. |
status.observedGeneration | integer (int64) | ObservedGeneration is the spec generation this status reflects. |
status.observedTools | []string | ObservedTools is the snapshot of names returned by the server's most recent successful tools/list probe. Sorted, deduplicated. |
status.pin | object | Pin is the recorded manifest baseline in the common PinRecord shape: Digest = canonical manifest hash, Version = the server's self-reported serverInfo.version (audit metadata only — self-reported by the party pinning defends against). |
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. |