Skip to main content

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.

FieldTypeCanonicalNotes
tegatastringYesSchema version. Const "0.1".
policyobjectYesRequired id. See below.
attributesobject mapYesAttribute type declarations. See below.
auditobjectNoHuman/audit metadata. Never compiled. See below.
rulesarrayYesminItems: 1. See The rule object.

policy

FieldTypeCanonicalNotes
policy.idstringYeskebab-case ^[a-z][a-z0-9]*(-[a-z0-9]+)*$, maxLength: 64.
policy.descriptionstringNoFree 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:

FieldTypeNotes
source_textstringThe original natural-language requirement.
normalized_textstringA normalized restatement.
ambiguitiesarrayInterpretation 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.

FieldTypeRequiredNotes
idstringYeskebab-case, maxLength: 64, unique within the file.
effectstringYes"allow" or "deny".
descriptionstringNoMetadata.
subjectobjectYesOptional roles. See below.
actionsarrayYesSee below.
resourceobjectYesRequired type. See below.
relationshipsarrayNoCross-entity comparisons. See Relationships.
conditionsarrayNoAttribute 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.

FieldRequiredCanonicalNotes
labelYesNoHuman-readable name.
subject_attributeYesYesAn identifier.
opYesYesOne of eq, neq, gt, gte, lt, lte.
resource_attributeYesYesAn 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.*.

Familyop valuesValue fieldNotes
Scalareq, neqvalue (literal)Equality / inequality.
Orderinggt, gte, lt, ltevalue (integer or string)Not boolean.
Setin, not_invalues (non-empty, unique array of literals)Membership in a set.
Array membershipcontains, not_containsvalue (literal)Membership in an array attribute.
Existenceexists, not_existsNo 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 subject for the reviewability constraints).
  • Literals are string, integer, or boolean.
  • 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.

RuleBehavior
Within a ruleAll conditions (and relationships) are AND-ed.
Across same-effect rulesOR-ed — any matching rule of that effect matches.
allow vs denydeny overrides allow (Deny-First).
No matching allowdeny — Zero-Trust, default false.
roles / actionsMatched 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 effectA compared value is absentResult
allowThe condition does not holdThe allow rule does not match
denyThe condition is treated as holdingThe 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.classificationDeny fires?
"ok"No
"banned"Yes
absentYes
"" (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 byDerived as
a value-comparison operator (eq, neq, gt, gte, lt, lte, in, not_in, contains, not_contains) or a relationshiprequired
exists / not_exists onlyoptional — a deliberate absence is a valid state
both an existence check and a value comparisonoptional

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