SpiceDBBootstrap

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

SpiceDBBootstrap declaratively seeds SpiceDB state: an optional schema fragment and/or a set of relationships TOUCHed into SpiceDB. At least one of spec.spicedbSchema.resources and spec.relationships MUST be non-empty.

Namespaced. It has no reconciler of its own -- pkg/controllers/guardian watches it from the AgentSessionGrants loop, composes its fragment into the unified schema alongside MCPServer fragments, and refcounts its relationships across every CR that claims them, so deleting one CR removes only the tuples no other CR still owns.

Spec

FieldTypeDescription
spec.reclaimPolicystringReclaimPolicy controls cleanup of tuples this CR previously wrote. Delete (default) reconciles to desired state via cluster-wide refcounting; Retain TOUCHes on each reconcile but never DELETEs and does not participate in refcount. (enum: Delete | Retain; default: Delete)
spec.relationships[]objectRelationships is an optional list of tuples to TOUCH into SpiceDB. With reclaimPolicy=Delete (default), the controller also DELETEs tuples that were previously TOUCHed but have since dropped out of the cluster-wide desired set (refcounted across all SpiceDBBootstrap CRs).
spec.relationships[].relation *stringRelation is the relation name declared on Resource's type.
spec.relationships[].resource *objectResource is the tuple's left-hand object.
spec.relationships[].resource.id *stringSpiceDB's object_id regex is ^(([a-zA-Z0-9/_|\-=+]{1,})|\*)$ — letters, digits, and _ / | - = + . The pattern below admits all of those, plus '@' and '.' so that an email can be carried verbatim when subject.canonicalize=true; the controller's L2 validation rejects an '@' in a user subject when canonicalize is false. '=' is load-bearing beyond email. It is the escape character authz.spicedb_escape uses to fit a free-form value — a repository URL — into an object id INJECTIVELY, which is what lets an approval name one repository without also covering another. A bootstrap that seeds a grant on such a value has to be able to express the id the check will compute, so a pattern narrower than SpiceDB's own rejects legal ids.
spec.relationships[].resource.type *stringType is the SpiceDB definition name.
spec.relationships[].subject *objectSubject is the tuple's right-hand subject.
spec.relationships[].subject.canonicalizebooleanCanonicalize, when true, runs ID through identity.EmailReference(id).Canonical() before writing. Only valid when Type=="user" and ID is an email. Mutually exclusive with wildcard=true.
spec.relationships[].subject.idstringID is required UNLESS Wildcard=true (which forces the wire-form SpiceDB subject id to "*", the wildcard match-any value).
spec.relationships[].subject.relationstringRelation, when set, makes this a subject-set reference like "group:eng#member". Mutually exclusive with canonicalize=true and wildcard=true.
spec.relationships[].subject.type *stringType is the SpiceDB definition name of the subject.
spec.relationships[].subject.wildcardbooleanWildcard, when true, writes the SpiceDB wildcard subject (<type>:*) — every subject of Type matches. Requires the corresponding relation on the target resource to itself be wildcard-typed in the schema (see SpiceDBRelation.Wildcard). Mutually exclusive with Relation and Canonicalize. When true, ID is ignored.
spec.spicedbSchemaobjectSpiceDBSchema is an optional schema fragment composed into the unified SpiceDB schema by the guardian reconciler. Same shape and rules as MCPServer.spec.spicedbSchema. Must not redeclare 'user'.
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 <name> = …, 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 <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 *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)
* required

Status

Status is controller-owned (observed state).

FieldTypeDescription
status.conditions[]objectConditions carries Valid, SchemaIncluded and RelationshipsApplied.
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.observedGenerationinteger (int64)ObservedGeneration is the spec generation this status reflects.
status.observedRelationshipsinteger (int32)ObservedRelationships is the count of tuples in spec.relationships that the controller successfully TOUCHed on the last reconcile.
* required