Vocabulary
ninka/vocabulary.json declares project terms that Tegata policies may use, and the subject/resource attributes that may be recorded unmasked in the Decision Log.
The file is optional. Without it, role/action/resource-type governance falls back to the open-world checks Ninka can perform heuristically. When the file exists, Ninka applies the closed-world V1 check: a role, action, or resource type used by a policy must be declared in the corresponding vocabulary list. Attribute keys become closed-world when the attributes map is non-empty.
{
"roles": ["employee", "member"],
"actions": ["view", "edit"],
"resource_types": ["invoice", "document"],
"attributes": {
"subject.user_id": "string",
"resource.owner_id": "string",
"resource.amount": { "type": "integer" }
},
"decision_log": { "unmasked": ["resource.amount"] }
}
Why declare a vocabulary
Heuristics cannot reliably decide that two different strings were intended to be the same project term. A closed-world vocabulary moves that decision into an explicit file instead.
The review benefit is straightforward: introducing a role such as admin2 requires adding it to vocabulary.json. The new term therefore appears explicitly in the change being reviewed instead of becoming part of the policy set merely because an author typed a new string.
This does not make Ninka a semantic thesaurus. If the vocabulary explicitly contains both admin and Admin, they are still two different exact tokens. The closed-world contract guarantees declaration, not automatic equivalence detection.
Fields
The root accepts exactly these five keys. Any other root key is an error so that a misspelling cannot silently look like “nothing was configured.”
| Field | Type | Meaning |
|---|---|---|
roles | array of strings | Roles that policies may name. |
actions | array of strings | Actions that policies may name. |
resource_types | array of strings | Resource types that policies may name. |
attributes | object | Scoped attribute path → declared value type. |
decision_log | object | Decision Log disclosure settings. Only unmasked is defined. |
All five fields are optional.
roles, actions, and resource_types
When vocabulary.json exists, every non-wildcard role, action, and resource type used by the policy set must appear in its corresponding list. An undeclared use is a V1 compile error.
Role values are compared exactly and may follow an external IdP's spelling within the role-value constraints. Actions and resource types are Ninka-owned vocabulary and follow the Tegata naming rules. See Tegata Schema.
attributes
Keys are scoped attribute paths such as subject.user_id or resource.owner_id.
Values may use either shorthand or object form:
{
"subject.user_id": "string",
"resource.amount": { "type": "integer" }
}
The allowed value types are:
stringintegerboolean
The object form may contain only type. Unknown keys or unknown types are rejected rather than ignored.
The declared type also participates in the cross-policy C4 input-contract check, so conflicting declarations or inferred uses of one scoped key are reported.
Attribute closed-world enforcement has one deliberate exception: an empty attributes: {} does not mean “this project uses no attributes.” Attribute-key V1 checks begin only when the map is non-empty. This lets a project introduce vocabulary governance gradually without instantly rejecting every existing attribute.
decision_log.unmasked
This list explicitly declares which attributes may be written with their raw values in the Decision Log.
Attributes omitted from the list remain masked. If decision_log is absent, or if unmasked is empty, no subject/resource attribute is implicitly disclosed by Ninka.
Each entry must:
- be declared in
attributes; - appear only once;
- not be
action.nameorresource.type.
action.name and resource.type are always recorded raw by the Decision Log contract because they are policy-selection literals. Listing them in decision_log.unmasked is therefore an error.
Ninka sorts the unmasked list before carrying it into generated manifests so declaration order does not affect artifact bytes. ninka-authz verify recomputes the source-derived value and compares it with the manifest files at the artifact paths being checked. It does not inspect whether those files are tracked by Git.
The runtime applies the disclosure policy when it emits Decision Log entries. See Runtime API and .NET Runtime API.
How checks change when the file exists
No vocabulary.json | With vocabulary.json | |
|---|---|---|
| Undeclared role / action / resource type | no closed-world declaration to check | error (V1) |
| Cross-policy case-only vocabulary variants | fallback warning (C3) | C3 is not used; every used token must instead be explicitly declared |
| Undeclared attribute key | no vocabulary-based key check | error (V1) when attributes is non-empty |
Without a vocabulary file, Sekisho can still report the fallback vocabulary-drift heuristics defined for that mode. With a vocabulary file, the control changes from “guess whether two terms look suspicious” to “require every used term to have been deliberately declared.”
Missing and malformed are different
A missing vocabulary.json is legal. A file that exists but is invalid is not treated as though it were missing.
Compilation stops for errors such as:
- invalid JSON;
- an unknown root key;
- an invalid attribute declaration;
- an invalid Decision Log disclosure entry.
This distinction prevents a configuration typo from silently turning governance off.