Tegata Schema
Complete reference for the Tegata (手形) schema, version 0.1. A Tegata is a JSON document named
<policy-id>.tegata.json that declares one authorization policy. This page documents every field,
operator, and the fixed evaluation semantics.
For a guided introduction, see Quickstart and Author a Policy.
Canonical vs metadata
Every field is either canonical or metadata:
- Canonical fields define the authorization specification that is compiled. They are included in
tegata_hash. - Metadata fields are for humans and audit context. They are excluded from the hash and are not compiled.
A practical consequence: reordering rules or rewriting a description does not change the
tegata_hash; changing a role, action, condition, or effect does.
Top-level structure
additionalProperties: false. Required: tegata, policy, rules.
| Field | Type | Canonical | Notes |
|---|---|---|---|
tegata | string | Yes | Schema version. Const "0.1". |
policy | object | Yes | Required id. See below. |
attributes | object map | Yes | Attribute type declarations. See below. |
audit | object | No | Human/audit metadata. Never compiled. See below. |
rules | array | Yes | minItems: 1. See The rule object. |
policy
| Field | Type | Canonical | Notes |
|---|---|---|---|
policy.id | string | Yes | kebab-case ^[a-z][a-z0-9]*(-[a-z0-9]+)*$, maxLength: 64. |
policy.description | string | No | Free text. |
attributes
An object map (canonical). Keys match ^(subject|resource|environment|custom).<name...>; values are
"string" | "integer" | "boolean". You only need to declare an attribute here when its type cannot
be derived from rule literals — for example an attribute used only in a relationship.
When such a relationship-only attribute is not declared, the Tegata carries no type source for it —
no declared type, and no condition literal to infer one from. The Sekisho says exactly that (S14, a
warning) and names the declaration to add, e.g.
"attributes": { "subject.org_id": "string", "resource.org_id": "string" }. Only the author can
supply the missing type, which is why the diagnostic stops at the Tegata.
What a generated projection does with an attribute that has no type is that projection's business, and it varies by language — the compiler does not name a consumer language's types here. Expect the call site to lose static checking for that property; the projection reference for your language says how it is represented.
audit
Metadata (never compiled). Fields:
| Field | Type | Notes |
|---|---|---|
source_text | string | The original natural-language requirement. |
normalized_text | string | A normalized restatement. |
ambiguities | array | Interpretation notes. Each: code, message, optional related_rules[]. |
Interpretation notes are the review surface: everything an agent decided on its own — including
migration parity choices and accepted semantic differences — goes here, never only in
description. The Sekisho verifies related_rules[] referential integrity (S13).
ambiguities[].code is one of: assumed_interpretation, missing_input, contradiction,
unexpressible_delegated, exception_scope.
The rule object
additionalProperties: false. Required: id, effect, subject, actions, resource.
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | Yes | kebab-case, maxLength: 64, unique within the file. |
effect | string | Yes | "allow" or "deny". |
description | string | No | Metadata. |
subject | object | Yes | Optional roles. See below. |
actions | array | Yes | See below. |
resource | object | Yes | Required type. See below. |
relationships | array | No | Cross-entity comparisons. See Relationships. |
conditions | array | No | Attribute constraints. See Conditions. |
subject
subject.roles is an optional, non-empty, unique array of role values — externally owned
vocabulary kept exactly as the identity provider issues it (admin:operate, Domain Admins), not
identifiers you have to normalize. Constraints are for reviewability only: ASCII printable, 1–64
characters, no control characters, no leading or trailing whitespace (internal spaces allowed).
Non-ASCII is not supported in 0.x, deliberately: a homoglyph such as Cyrillic а in аdmin cannot
be told from admin in review. Roles are matched
any-of (the subject matches if it holds any listed role). Omit roles to match any subject —
the rule still carries "subject": {}; the subject property itself is required, and the Sekisho
rejects a rule without it.
actions
Required. Either a non-empty, unique array of snake_case identifiers (matched any-of), or the
exact wildcard ["*"].
resource
Required type: a snake_case identifier, or the wildcard "*".
Relationships
relationships is an optional array. This is the only place a cross-entity comparison (subject
vs resource) is allowed.
| Field | Required | Canonical | Notes |
|---|---|---|---|
label | Yes | No | Human-readable name. |
subject_attribute | Yes | Yes | An identifier. |
op | Yes | Yes | One of eq, neq, gt, gte, lt, lte. |
resource_attribute | Yes | Yes | An identifier. |
Required and canonical are independent. label must be present — the schema rejects a relationship
without it — but it carries no meaning: it never reaches the hash, the generated Rego, or the helper
name derived from subject_attribute, op, and resource_attribute. Rewriting a label does not
require re-approval. It is required because a rule can hold several relationships, and a reviewer
needs a name for each.
Example — "the invoice's submitter is the requesting user":
{ "label": "own_submission", "subject_attribute": "user_id", "op": "eq", "resource_attribute": "submitted_by" }
Conditions
conditions is an optional array. Each condition is a discriminated union on op. The key is a
scoped key: subject.*, resource.*, environment.*, or custom.*.
| Family | op values | Value field | Notes |
|---|---|---|---|
| Scalar | eq, neq | value (literal) | Equality / inequality. |
| Ordering | gt, gte, lt, lte | value (integer or string) | Not boolean. |
| Set | in, not_in | values (non-empty, unique array of literals) | Membership in a set. |
| Array membership | contains, not_contains | value (literal) | Membership in an array attribute. |
| Existence | exists, not_exists | — | No value. |
Example:
{ "key": "resource.amount", "op": "lte", "value": 500000 }
Identifiers and literals
- Identifiers (actions, resource types, attribute names) are snake_case — this is vocabulary your application designs and writes as literals at the call site.
- Role values are not identifiers: they carry the vocabulary their owner (your IdP) issues, and
are kept verbatim (see
subjectfor the reviewability constraints). - Literals are
string,integer, orboolean. - Integers only — no floats in v0.1. Amounts and thresholds are integers (e.g. JPY as an integer).
Evaluation semantics
These are fixed and non-configurable.
| Rule | Behavior |
|---|---|
| Within a rule | All conditions (and relationships) are AND-ed. |
| Across same-effect rules | OR-ed — any matching rule of that effect matches. |
| allow vs deny | deny overrides allow (Deny-First). |
| No matching allow | deny — Zero-Trust, default false. |
roles / actions | Matched any-of. |
| Wildcards | "*" matches everything. Sekisho reports the use as information (S8), and a policy with resource.type: "*" claims every resource type (C6). |
For all-of on roles, split the rule or use conditions on subject attributes. roles and
actions have no all-of form.
One decision covers one resource
A check() names one policy and evaluates it against one concrete resource whose attributes you
supplied. Ninka has no set-level decision.
Filtering a list by calling check() once per candidate is correct use. Combining those answers is
not: do not OR several results into a single authorization decision for the whole set. That answer
was never evaluated by any policy, and deny-overrides applies inside one decision, not across an OR
you performed afterwards.
Deny-overrides is also resolved inside the named policy. A deny rule in a different policy file does not restrict this policy's allow rules — a prohibition placed there can never take effect. Sekisho rejects that arrangement rather than letting the dead prohibition ship.
Missing input: what a rule does when a value is absent
A decision is made from the input you pass. When a value that a rule compares is absent, the rule does not simply fail to match. The behavior depends on the rule's effect, and it is the same in both directions: an absent value never widens access.
| Rule effect | A compared value is absent | Result |
|---|---|---|
allow | The condition does not hold | The allow rule does not match |
deny | The condition is treated as holding | The deny rule fires |
The deny row surprises most readers, so it is worth stating plainly. Given a deny rule for
"documents whose classification is banned":
resource.classification | Deny fires? |
|---|---|
"ok" | No |
"banned" | Yes |
| absent | Yes |
"" (empty string) | Yes |
If a missing attribute silenced a deny, anyone could bypass a prohibition by not sending the
attribute. The same rule covers values that are present but of the wrong type: a deny comparing
status to "banned" also fires when status arrives as true or as ["banned"].
Absent is wider than "the key is missing":
- an empty string counts as absent;
- an empty array counts as absent, so
subject.roles: []is the same call as one that sends no roles at all, and role-referencing deny rules fire; - an object counts as absent whether empty or not, because a Tegata value is a scalar or an array. Two equal junk objects must not satisfy a relationship.
exists and not_exists are the exception. They test presence itself, so they are read literally
for both effects — an exists condition is not "satisfied by absence".
Required and optional inputs
The compiler derives, from the rules themselves, which input keys each policy needs. You do not declare this; the generated input types come from it.
| A key is referenced by | Derived as |
|---|---|
a value-comparison operator (eq, neq, gt, gte, lt, lte, in, not_in, contains, not_contains) or a relationship | required |
exists / not_exists only | optional — a deliberate absence is a valid state |
| both an existence check and a value comparison | optional |
action is always required. resource_type is always required unless every rule uses the wildcard.
subject.roles is required as soon as any rule lists roles.
Required here means "required by the rules it came from". The runtime does not reject a call for a
missing required key: a key needed only by an approve rule must not fail a view request. Missing
input is reported through the diagnostic channel and decided by the rules above.
Complete example
The canonical ninka/invoice-access.tegata.json:
{
"tegata": "0.1",
"policy": {
"id": "invoice-access",
"description": "Viewing, deleting, and approving invoices"
},
"attributes": {
"subject.user_id": "string",
"resource.submitted_by": "string"
},
"audit": {
"source_text": "Employees can view invoices they submitted. Managers, finance, and admins can view all invoices. Managers can approve invoices up to ¥500,000. Finance can approve invoices of any amount. Admins can delete invoices.",
"ambiguities": [
{
"code": "assumed_interpretation",
"message": "Expressed \"submitted by themselves\" as a relationship (subject.user_id = resource.submitted_by). Assumes there is no proxy or joint submission.",
"related_rules": ["allow-employee-view-own-invoice"]
},
{
"code": "assumed_interpretation",
"message": "Interpreted manager / finance / admin viewing as all invoices with no department restriction. To scope by department, add a relationship.",
"related_rules": ["allow-staff-view-invoice"]
},
{
"code": "assumed_interpretation",
"message": "Interpreted \"within ¥500,000\" as resource.amount lte 500000 (integer, JPY). Exactly ¥500,000 is approvable; use lt to exclude it.",
"related_rules": ["allow-manager-approve-invoice-within-limit"]
},
{
"code": "missing_input",
"message": "Assumes resource.amount / resource.submitted_by come from the invoice record (DB) and subject.user_id from the session. Assumes a single currency (JPY).",
"related_rules": ["allow-employee-view-own-invoice", "allow-manager-approve-invoice-within-limit"]
}
]
},
"rules": [
{
"id": "allow-employee-view-own-invoice",
"effect": "allow",
"description": "Employees can view invoices they submitted",
"subject": { "roles": ["employee"] },
"actions": ["view"],
"resource": { "type": "invoice" },
"relationships": [
{ "label": "own_submission", "subject_attribute": "user_id", "op": "eq", "resource_attribute": "submitted_by" }
]
},
{
"id": "allow-staff-view-invoice",
"effect": "allow",
"description": "Managers, finance, and admins can view all invoices",
"subject": { "roles": ["manager", "finance", "admin"] },
"actions": ["view"],
"resource": { "type": "invoice" }
},
{
"id": "allow-manager-approve-invoice-within-limit",
"effect": "allow",
"description": "Managers can approve invoices up to ¥500,000",
"subject": { "roles": ["manager"] },
"actions": ["approve"],
"resource": { "type": "invoice" },
"conditions": [ { "key": "resource.amount", "op": "lte", "value": 500000 } ]
},
{
"id": "allow-finance-approve-invoice",
"effect": "allow",
"description": "Finance can approve invoices of any amount",
"subject": { "roles": ["finance"] },
"actions": ["approve"],
"resource": { "type": "invoice" }
},
{
"id": "allow-admin-delete-invoice",
"effect": "allow",
"description": "Admins can delete invoices",
"subject": { "roles": ["admin"] },
"actions": ["delete"],
"resource": { "type": "invoice" }
}
]
}
See also
- Quickstart — write and compile your first policy.
- Author a Policy — grow your authorization surface.
- Generated Files — what the compiler emits from a Tegata.