Error Codes
Every failure surfaced by Aperture is an APERTURE_* coded error. Each code carries a canonical message and, where operator action is meaningful, one or more fixup hints. This page is generated from the error Registry in errors/codes.go.
| Code | Message | Fixups |
|---|---|---|
APERTURE_ACTION_UNDECLARED | action is not declared on the object type | Add the action verb to the object type's declared action set, or grant a verb the type already declares. List the object type's actions to see the validated verb set. |
APERTURE_ATTRIBUTE_PROVIDER_FETCH | attribute provider returned an error | Inspect the wrapped cause for the underlying provider failure. Return APERTURE_NOT_FOUND from the provider for a key it does not know, so an unknown subject stays distinguishable from an unreachable directory. |
APERTURE_ATTRIBUTE_PROVIDER_INVALID | attribute provider registration or attribute key is invalid | Register a non-nil provider, and at most one per LAYER: Register fills a slot's shared layer and RegisterLocal its local one, so a slot holds two and a third is refused. A duplicate within a layer is refused rather than replaced, so one directory cannot silently shadow another. Declare a key set on a slot's SHARED layer only (Register with provider.WithDeclaredKeys): a declared key is reserved to the layer that declared it, so a local declared set would reserve keys against the deployment's own directory. A local layer takes cache options only. Declare each attribute key at most once within a provider. Fetch with a real key: a principal id for the user and machine slots, an account id for the account slot. An empty key names nobody. Resolve the account wildcard "*" to a concrete account before fetching attributes; it is never a legal attribute key. |
APERTURE_ATTRIBUTE_PROVIDER_UNREGISTERED | no attribute provider is registered for the slot | Register an AttributeProvider for the slot before fetching its attributes. Check that the principal's kind maps to the slot you wired: a machine principal reads the machine slot, not the user slot. Deployment genuinely has no subjects of this kind? Then nothing should be fetching that slot — fix the caller rather than registering an empty provider. Seeing this from a decision? You should not: a Check/Enumerate/Explain against an unwired slot evaluates the floor bag and decides. This code reaches you only from a DIRECT attribute read (Fetch or Enumerate on the registry), so the caller to fix is that reader, not the decision path. |
APERTURE_ATTRIBUTE_SLOT_UNKNOWN | not an attribute slot | Use one of the three declared slots: user, machine, or account (provider.AttributeSlotUser / AttributeSlotMachine / AttributeSlotAccount). Converting a principal kind that arrived as a bare string? Cross over with provider.ParseAttributeSlot so an unknown kind fails at the conversion instead of resolving to an empty attribute bag. The slot set is closed on purpose — it names the parties a decision has. Model a further distinction as a FIELD in the bag, not as a fourth slot. |
APERTURE_AUTHZ_DENIED | the actor lacks the admin authority tier required for this mutation | Schema mutations (permission types, roles, object-types, providers, templates, rules) require system-admin authority: an allow grant on the admin action whose object covers system:. Grant and delegation mutations require account-admin authority in the TARGET account: an allow grant on the admin action whose object covers account:<acct>/admin:. Account-admin authority is confined to its own account; obtain authority in the account the mutation targets, or hold a broader (e.g. **) grant. |
APERTURE_BOOT | aperture failed to start | Check the APERTURE_* environment variables and any --config file. Confirm the storage backend (memory or sqlite) is reachable. |
APERTURE_CONFIG_INVALID | configuration is invalid | Validate the YAML config and APERTURE_* env vars against the docs. Enumerating an attribute slot that was declared without get_all: that slot is fetch-only by design, so add a get_all statement selecting a bare id, or read the slot through a fetch alone. |
APERTURE_DELEGATION_DENIED | the delegator may not bestow this grant | Bestow only grants that are a subset of your own effective allow grants in the account (same action and scope strategy, an equal-or-more-specific object pattern). Confirm you hold a 'may delegate' right whose object pattern covers the grant's object. Bestow grants only within an account you are a member of; cross-account bestowal is rejected. |
APERTURE_DELEGATION_NOT_DELEGATABLE | the permission is not flagged delegatable | Set Delegatable on the permission definition to allow it to be bestowed. |
APERTURE_ENTITY_UNMANAGED | this deployment does not manage the entity kind the write targeted | Set the switch for the kind named in the message — APERTURE_MANAGE_ACCOUNTS, APERTURE_MANAGE_PRINCIPALS, or APERTURE_MANAGE_MEMBERSHIPS — to true (the default), then RESTART aperture: the switches are read once at startup and never re-read. The three switches are independent; turning one on does not affect the others, so enable only the kind you meant to hand back to Aperture. Leaving a kind unmanaged is usually deliberate — those records are mastered by an upstream system. Make the change there and let it flow in, rather than flipping the switch. This is not a permission problem: no grant, role, or admin tier lifts it, and it refuses a system-admin exactly as it refuses anyone else. |
APERTURE_IDENTITY_INVALID | object identity is malformed | Use type:id segments joined by '/', e.g. account:acme/project:atlas/document:42. Ensure no segment is empty and every segment carries a ':' with a non-empty type and id. Remove illegal characters; types and ids allow letters, digits, and -._~@+ only ('*' marks a wildcard in patterns). |
APERTURE_IMPERSONATION_DENIED | the operator may not impersonate this target | Impersonate only within an account both the operator and the target are members of; cross-account impersonation is refused. Confirm the operator holds an impersonation right (augment or become) whose object pattern covers the target's identity. Become mode requires the stronger become right; an augment right alone cannot become a target. |
APERTURE_IMPERSONATION_EXPIRED | the impersonation session has expired | Start a fresh impersonation session; sessions are time-boxed and expire automatically. |
APERTURE_INVALID_INPUT | input failed validation | Re-check the request shape against the command or API contract. |
APERTURE_INVALID_TOKEN | the presented bearer credential failed verification | Confirm the token is a well-formed JWT signed by the configured issuer's keys. Check the token issuer and audience match APERTURE_OIDC_ISSUER and APERTURE_OIDC_AUDIENCE, and that it has not expired. For a parsec adapter, confirm the token was minted by the broker sharing the configured keyring/secret. |
APERTURE_METADATA_INVALID | object metadata violates the metadata value model | Make each field a scalar, a []any of scalars, or a map[string]any whose values are scalars, scalar arrays, or one further object level. Replace an array of objects with a scalar array (e.g. a list of ids) — arrays of objects are rejected at any position. Flatten a value that nests past the depth cap, or raise provider.ValueLimits.MaxDepth for the loader. Shorten a value over the per-value size cap, or raise provider.ValueLimits.MaxBytes for the loader. |
APERTURE_NOT_FOUND | the referenced entity was not found | Confirm the identifier exists in the current account scope. |
APERTURE_PROVIDER_FETCH | object provider returned an error | Inspect the wrapped cause for the underlying provider failure. Return APERTURE_NOT_FOUND from the provider for an object that does not exist. |
APERTURE_PROVIDER_INVALID | object provider registration is invalid | Register a non-nil provider under a non-empty object-type key. Register each object type at most once; check for a duplicate registration. |
APERTURE_PROVIDER_REFERENCE_INVALID | a declared object reference is not usable | Register a provider for the target object-type: a references: entry may only point at a type this registry serves. Declare each field at most once per object-type; several fields may point at the same target, but one field has one target. Declare the reference on the HOLDING side — the type whose provider actually returns the field — because that is the only side with a value to resolve. Resolving a field means declaring it first: reg.DeclareReference("dataset", "current_brands", "brand"), or a references: entry in the provider's seed block. |
APERTURE_PROVIDER_REFERENCE_MISMATCH | a reference field's value does not identify an object of its declared target type | Store FULL canonical identities in a reference field ('brand:1', 'account:acme/brand:1'), not bare primary keys — compose them where the data is loaded, e.g. SELECT 'brand:' || b.id. Make the field a string or a list of strings; a number, a map, or a list holding anything but strings cannot be an identity. Check the declared target against the values the field actually carries: an identity whose terminal segment type is not the declared type is rejected rather than skipped. |
APERTURE_PROVIDER_UNREGISTERED | no object provider is registered for the object type | Register an ObjectProvider for the object type before fetching its metadata. Confirm the object identity's terminal segment type matches a registered provider key. Filtering an enumeration by metadata fields? Build the engine with engine.WithMetadata(registry) — the same provider registry the scope lister uses — or drop the field predicates. Searching object metadata by name? Search ALWAYS needs engine.WithMetadata(registry); unlike a field predicate it is not optional, and an empty result would read as "no access" rather than as the misconfiguration it is. |
APERTURE_RULE_EVAL | rule evaluation failed | Inspect the wrapped cause for the underlying evaluation failure. Ensure the rule expression yields a boolean for the supplied context. |
APERTURE_RULE_INVALID | rule AST is malformed | Give each logical node the right child count: and/or take two or more, not takes exactly one. Give every comparison a left and right operand, and every literal a scalar value. Write variable references as dotted identifier paths, e.g. object.classification. |
APERTURE_RULE_NOT_FOUND | the referenced rule was not found | Confirm the rule reference exists in the configured rule source. |
APERTURE_RULE_TYPE_ERROR | rule failed expression type checking | Compare compatible types and make the rule evaluate to a boolean. Call only functions registered with the rules engine. |
APERTURE_RULE_UNDECLARED_ATTRIBUTE | rule reads an attribute key the wiring does not declare | Add the key to declared_keys: on the attribute_providers: entry for the slot the message names, then push the wiring again. Or stop reading the key in the rule: a key no shared entry declares is served by at most one instance's local layer, so a rule over it decides differently per instance. Read individual keys rather than the whole bag — a bare principal or account reference reads every key the bag happens to carry, which no declared set can cover.A slot that declares no key set is not enforced at all; remove declared_keys: from the entry to opt that slot back out. |
APERTURE_RULE_UNKNOWN_VARIABLE | rule references an unknown variable | Reference variables under a known context root: object, principal, account, or action. Check for a typo in the variable's root segment. |
APERTURE_SCOPE_INVALID | scope strategy reference is malformed | Use 'strategy' or 'strategy;param=value' form, e.g. inclusive;ids=account:acme/document:42. Give an inclusive/exclusive strategy an 'ids' list or a 'rule' reference; implicit takes no configuration. |
APERTURE_SCOPE_LISTER_UNCONFIGURED | scope enumeration requires an object lister that is not configured | — not applicable |
APERTURE_SCOPE_RULE_UNCONFIGURED | scope rule path requires a rule evaluator that is not configured | — not applicable |
APERTURE_SCOPE_UNKNOWN_STRATEGY | scope strategy is not registered | Use a built-in strategy (literal, implicit, inclusive, exclusive) or register the custom key with the scope registry. |
APERTURE_SQL_PROVIDER_AMBIGUOUS | SQL provider's fetch statement returned more than one row for one key | Filter the fetch statement on a unique or primary key so one identity — or one subject id — selects at most one row. A join that fans out is the usual cause; aggregate or de-duplicate the fanned-out side instead of adding LIMIT 1, which would make the metadata depend on an unspecified row order. |
APERTURE_SQL_PROVIDER_CONNECTION | a declared database connection could not be resolved into a live pool | Set the environment variable named by the connection's dsn_env: to a non-empty DSN before starting the process. Check that every kind: sql provider entry's connection: matches a name declared in the top-level connections: block. Give pool settings valid values: conn_max_lifetime and query_timeout are Go durations ("30m", "5s"), and query_timeout must be positive — there is no 'no timeout' setting. Verify the DSN's host, port, database, and credentials by connecting with psql; the driver's message is redacted here because a DSN parse failure commonly echoes the password. |
APERTURE_SQL_PROVIDER_DSN_LITERAL | a declarative database connection carries a literal dsn instead of dsn_env | Replace the connection's dsn: key with dsn_env: naming the environment variable that holds the DSN. Export the DSN in the process environment (or a .env file the deployment loads) rather than writing it into the seed file. Rotate the credential if a literal DSN was ever committed — the seed file is a version-controlled artifact. |
APERTURE_SQL_PROVIDER_QUERY | SQL provider could not run its statement | Inspect the wrapped driver error for the underlying database failure. Check the statement's placeholder count: a fetch statement binds exactly one parameter — the identity's terminal segment value for an object provider, the bare subject id for an attribute provider. Use the placeholder syntax your engine speaks — Aperture passes placeholders through untouched and never rewrites $1 to ?. Confirm the database is reachable and the connection's role can read the table; raise Config.Timeout if the statement is legitimately slow. |
APERTURE_SQL_PROVIDER_ROW_IDENTITY | SQL provider could not turn a row's id column into a usable key | Object provider: compose the full identity in the get-all statement's id column — SELECT 'brand:' || b.id AS id — because a bare primary key is not an identity and Aperture supplies no template. Attribute provider: select the BARE subject id — SELECT u.id AS id — and never 'user:' || u.id, which is a legal opaque key that no principal id will ever match, so the slot enumerates and then answers nothing. Name the key column id, or set the provider's id column to the alias the statement actually uses. Make the id column textual and never NULL: cast a numeric or uuid key with ::text before concatenating it. Check that the identity's terminal segment type is the object-type this provider is registered under; a 'brand:1' row served by the 'dataset' provider is rejected rather than cached. |
APERTURE_SQL_PROVIDER_SCAN | SQL provider could not read a row into metadata | Give every selected expression a name, and alias duplicates: each result column becomes a metadata field keyed by its column name. Cast or serialise a column whose Go type the provider does not map (the driver value's type is named in the error, alongside the types that are mapped). A []byte column is decoded as JSON, never as a string: wrap an array in to_jsonb(...), and cast a numeric, uuid, or bytea to ::text, ::float8, or encode(...) in the statement. A list-valued field only arrives as a list when the statement casts it — SELECT to_jsonb(tags) AS tags, not SELECT tags, which yields the raw array literal as a string that silently matches nothing. |
APERTURE_STORAGE | the storage backend returned an error | Inspect the wrapped cause for the underlying storage failure. |
APERTURE_STORAGE_CONSTRAINT | the database refused the write because it would break referential integrity | Delete the rows that reference the record first: a principal's memberships and group memberships, a permission's role assignments and grants, a role's principal assignments, an object type's permissions, an account's memberships and grants, and the grants that name a principal, role, or group as their subject. Creating a record? Create what it references first — a permission needs its object type, a membership and a group member need their principal, a grant and a role assignment need their permission, and a membership and a grant need their account. Refused on a grant's subject? subject_kind chooses the table subject_id must exist in — principal, role, or group. A grant naming a role id under kind "group" is refused even though that id exists elsewhere. Refused on an account id? It must name a real account, or be exactly "" (the all-accounts wildcard). "" is a sentinel, not an account: it needs no account record and cannot be given one. Ids are immutable in Aperture: to change one, create the new record, move the children, then delete the old one — an UPDATE that moved a key is refused rather than silently re-parenting. Seeing this from Setup rather than a write? The connection is not enforcing foreign keys. Open the store with sqlite.Open, which forces _pragma=foreign_keys(1) into the DSN whatever the caller passed. |
APERTURE_STORAGE_SCHEMA_INCOMPATIBLE | the existing database was written by an incompatible build and cannot be upgraded | Recreate the database: move or delete the old file, start Aperture so Setup builds the schema fresh, then re-seed it. Point the store at a new, empty database if you need to keep the old file for reference. Do not expect an in-place upgrade: Aperture ships no migration tool and no schema versioning, so a schema break is a hard break by design. |
APERTURE_TEMPLATE_INVALID | the provisioning template is malformed | Give the template a non-empty name, a version of at least 1, and at least one grant. Declare every parameter a grant references; write references as ${name} with a declared parameter. Give each template grant a valid subject, a permission id, an allow/deny effect, and a non-empty object pattern. |
APERTURE_TEMPLATE_PARAM | the template apply supplied invalid parameters | Supply a value for every parameter the template declares, and no parameters it does not. A segment-typed parameter value must be a legal identity component: letters, digits, and -._~@+ only. |
APERTURE_UNAUTHENTICATED | the request could not be resolved to a known principal | Present a credential: send an Authorization: Bearer <token> header. With the dev/static authenticator the bearer IS the principal id; send a non-empty value. Confirm the verified token carries the configured principal claim (APERTURE_AUTH_PRINCIPAL_CLAIM, default 'sub'). |
APERTURE_UNIMPLEMENTED | this surface is not yet implemented | — not applicable |
APERTURE_WIRING_CONNECTION_UNDECLARED | a pushed entry names a connection the wiring's connections: manifest does not declare | Add the connection named in the message to the document's connections: block, with dsn_env: naming the environment variable that holds its DSN. Or fix the entry's connection: to match one of the declared names the message lists — one connections: entry is one pool, so a typo opens no pool rather than a second one. Only the NAME is shared: every instance resolves that connection's DSN and pool settings from its own environment, so the manifest is a list of names and nothing else. |
APERTURE_WIRING_CONNECTION_UNROUTED | the shared wiring declares a connection name this instance has no route for | Export the environment variable the message names — APERTURE_CONNECTION_<NAME>_DSN — with this instance's DSN for that connection. It is the conventional route and needs no seed file. Or declare a connections: entry under the same name in this instance's --seed file, with dsn_env: naming a variable of your choosing; a local entry is that name's route, not a competing declaration, and it also carries the pool sizes and query_timeout. Or, in a Go host, supply seed.WithConnectionOpener and build the pool for that name yourself — the one seam a host needs, and the only one that never reads a DSN from the environment. Check which names this deployment expects with aperture wiring show --store <dsn>: the shared tables carry the connection NAME and nothing else, so every instance must supply its own route for each one.Do not work around it by removing the entry that uses the connection: an object provider that cannot reach its database yields no metadata, and an attribute provider that cannot yields a nil bag — which widens an exclusive grant rather than denying. |
APERTURE_WIRING_EMPTY_PUSH | the document being pushed declares no shared wiring at all, and a push is a REPLACE | Check the --seed path first: the likeliest cause is the wrong file, and the second likeliest is a mistyped section key (provider: for providers:, field_type: for field_types:), which the seed reader does not refuse — it simply reads no section.Confirm what the document declares with aperture wiring push --seed <file> --store <dsn>'s summary on a store you do not mind replacing, or read the file's four shared sections directly: connections:, providers:, field_types:, attribute_providers:.If retiring the deployment's wiring really is the intent, pass --allow-empty. Every instance then builds its wiring from its own --seed file again, and an instance with no --seed file builds NONE — an empty object registry and an empty attribute registry, which still starts. Retiring an attribute slot is the dangerous half: an unregistered slot answers with an empty bag, and a rule that EXCLUDED on an attribute it can no longer see stops excluding, so the grant widens with nothing in a verdict, a trace or a note to say so. Prefer pushing a document that keeps the slots and changes what you meant to change. aperture wiring show --store <dsn> prints what is deployed right now, and aperture wiring pull --out <file> writes it out as a document you can edit and push back. |
APERTURE_WIRING_KIND_UNSHAREABLE | a pushed entry selects a kind that cannot be shared wiring | Replace the kind: csv entry named in the message with kind: sql reading through a connections: entry, so every instance reaches the same data without a shared filesystem. Or leave that entry out of the pushed wiring and keep it in the LOCAL seed document, where the path belongs to the instance that reads it. A csv entry's only data source is a filesystem path; the shared-wiring schema has no path column, because a relative path resolves against the seed file's own directory and an absolute one is a guess about the other host's disk. On a BOOT this refusal means the row is already deployed — written by hand, or by a build that predates the check. Re-push a wiring document without it ( aperture wiring push), or read the same data through kind: sql; there is no path column to add a path to. |
APERTURE_WIRING_LOCAL_COLLISION | the local seed file declares an object type or attribute slot the shared wiring already declares | Delete the local declaration named in the message: with wiring rows present the database is authoritative, and the local file may only ADD an object type or slot the database never declared. Or remove the shared declaration instead — push a wiring document that omits it ( aperture wiring push --store <dsn> --seed <file>) — if the local one is the wiring you actually want the fleet to use.Check which side declares what with aperture wiring show --store <dsn>, then read the same section of this instance's --seed file.A host registering its own providers in Go gets the same refusal from the registry as APERTURE_PROVIDER_INVALID: the rule is about the registry, not about which syntax declared the entry. |
APERTURE_WIRING_NOTHING_DEPLOYED | the store has no shared wiring deployed, so there is nothing to pull | Check the --store DSN first: a typo names a database that does not exist yet, Setup creates it empty, and an empty read is exactly what that looks like. If the DSN is right, nothing has been pushed to this deployment yet — run aperture wiring push --seed <file> --store <dsn>, and every instance sharing the database will read it.aperture wiring show --store <dsn> describes an empty store without refusing, which is the command to use when you only want to know whether anything is deployed.A pull is refused rather than writing an empty document because that document, pushed back, would replace the deployment's wiring with nothing. |
APERTURE_WIRING_NO_MODEL_STATE | the target store holds no model state, so there is nothing for the pushed wiring to be wiring for | Apply model state to this store first — aperture import --store <dsn>, aperture serve --store <dsn> --seed <file>, or the mutation commands — and then push the wiring.Check the --store DSN: a typo names a database that does not exist yet, Setup creates it empty, and the wiring would land somewhere no instance reads. Two instances sharing wiring must share the MODEL too; wiring says where object metadata and attribute bags are read from, not who exists. |
APERTURE_WIRING_OBJECT_TYPE_UNKNOWN | a pushed provider entry serves an object type the model does not declare | Declare the object type named in the message in the store's object_types: (apply the model state, then push the wiring again). Check the spelling against aperture list object-type --store <dsn>: the wiring's object_type: must equal the object type's name exactly.Field-type declarations are exempt — they may name a type whose objects a local seed lists inline — so only providers: entries need a row. |
APERTURE_WIRING_OUTPUT_EXISTS | the --out path already exists and would be overwritten | Write to a new path and compare the two files yourself — the existing file is usually the version-controlled document the pull is meant to be diffed against. Pass --force to replace the file deliberately. Nothing was read from the store and nothing was written: the path is checked before the wiring is fetched, so a refusal here leaves both the file and the deployment untouched. |
APERTURE_WIRING_REFRESH_FAILED | a background re-read of the shared wiring failed, so this instance is still deciding from the wiring it has | Nothing is broken about this instance's decisions: it kept the last wiring it successfully read and is still answering. What it has lost is the ability to notice a aperture wiring push, so treat it as a deployment that has drifted, not as an outage.Read how long it has been stale with the system-admin-gated WiringPosture read (the WiringPosture RPC), which reports the duration, the failure count and the code — an alarm minutes old on a restarting database is ordinary, and the same alarm hours old is a fleet enforcing policy somebody already retired.The alarm normally carries the underlying failure's OWN code and fixups; follow those first. Seeing this code itself means nothing underneath it was coded, which is worth reporting. Check the store is reachable from this host and that its wiring tables match this build ( aperture wiring show --store <dsn>): an unreachable store and a schema from an older build are the two commonest causes.If the push added a connection name, every instance needs its own route for it — export APERTURE_CONNECTION_<NAME>_DSN, or declare the name in this instance's --seed file. See APERTURE_WIRING_CONNECTION_UNROUTED. Restarting the instance is the way to force the matter, and it is safe: a boot re-reads the shared wiring from scratch and REFUSES to start on wiring it cannot build, rather than degrading. |
APERTURE_WIRING_RESTART_REQUIRED | the deployed wiring changes this instance's connection name set, which is fixed for the life of a process | Restart this instance to adopt the push. The connection NAME SET is resolved once, at boot, because a route — which server, which credential, how big a pool — is a per-instance fact the shared tables carry no column for. For each name the message says the push ADDS, supply this instance's route first: export APERTURE_CONNECTION_<NAME>_DSN, or declare a connections: entry under the same name in this instance's --seed file, or supply seed.WithConnectionOpener in a Go host. Then restart. For each name the message says the push DROPS, nothing needs supplying: the pool stays open and this instance goes on deciding through the wiring it has until it is restarted. Nothing else in the push was applied either — a push is adopted whole or not at all — so the providers, field types and attribute providers pushed alongside the connection change are still outstanding and land on the restart. Check what the deployment expects with aperture wiring show --store <dsn>; the shared tables carry the connection NAME and nothing else, so every instance supplies its own route for each one. |