← Documentation index Architecture guide › policy

Metnos

policy — capability registry and grants
A guide to the declarative authorization model

Ask Metnos with a request like this example: “Explain which controls apply when I ask to read a file and which apply when I ask to modify it.” The answer must distinguish active runtime controls from the declarative matrix documented on this page.

runtime/policy.py provides a canonical registry, a deterministic autonomy-level matrix, and persistent grants scoped to a target. These APIs are available and inspectable, but they are not yet the general gate on the ordinary execution path. Effective runtime authorization is documented by the components that enforce it.

Contents

  1. What policy is
  2. Capability Registry
  3. Autonomy × capability table
  4. Persistent target-scoped grants
  5. Combined outcome (effective_outcome)
  6. Runtime integration
  7. CLI
  8. How to verify the module
  9. Module boundaries

1. What policy is

The policy module describes an authorization outcome for a tuple made of an autonomy level, a capability, and a target. The result is allowed, denied, or approval required. It is a deterministic model available in the code; it must not be confused with a gate already applied to every executor.

requested capability from the executor autonomy level ReadOnly/Supervised/Full per_target grants persistent effective_outcome allow / ask / deny
Figure 1 — The calculation exposed by the module: capability, autonomy level, and target-scoped grant produce an outcome.

The model separates three responsibilities assigned to different components:

This separation describes the subsystem design, not the order of the current execution path. The current code uses the registry to validate capabilities declared by manifests; it does not yet call effective_outcome as a general gate before every execution.

Declarative contract. Active manifests declare capabilities from the canonical registry. Effective controls remain distributed across manifest validation, vaglio, approval dialogs, sandboxing, and dispatcher-specific rules; this page does not attribute all of them to policy.py.

2. Capability Registry

The registry is the closed dictionary of actions Metnos recognises. Canonical entries are defined in runtime/policy.py:CAPABILITY_REGISTRY. Each entry is a CapabilitySpec with four attributes:

The following table is a representative subset. The registry in the code and the output of the registry command are the complete, current sources.

namecriticaldefault_approvaltarget_kinddescription
compute:purenononenonedeterministic in-memory computation without external I/O
fs:readnoper_targetpath_globread files from the local filesystem within declared path_globs
fs:writeyesper_targetpath_globwrite/modify files within path_globs (critical)
code:execyesalwaysexactexecute a shell command from a whitelist (e.g. package manager)
network:httpnoper_targethostHTTP/HTTPS GET/POST to authorised hosts
llm:localnononenonelocal LLM call (llama-server, llama.cpp), zero cost
llm:onlinenoper_targetnoneonline LLM call (Anthropic, OpenAI,...), cost > 0
mail:readnoper_targetexactread IMAP messages from an authorised mailbox
mail:sendyesalwaysexactSMTP send to recipients (irreversible, high stakes)
channel:innononeexactreceive messages from a channel (Telegram, CLI, voice)
channel:outnoper_targetexactsend messages to a specific channel
time:readnononenoneread current time and timezones
parse:localnononenonelocal parsing of known formats (PDF, HTML, JSON, CSV)
calendar:readnoper_targetexactread events from an authorised calendar
index:readnoper_targetexactread a local index managed by Metnos
metnos:readnoper_targetexactread a local resource managed by Metnos
metnos:cachenononenonebest-effort technical cache in a Metnos-managed subtree, with no effect on user data
system:readnoper_targetexactread system diagnostics on an authorised host or device
provider:accessnoper_targetexactaccess the network and credentials of an authorised provider

The registry is closed: record_grant rejects a capability outside it. Adding one means changing the registry in code and validating the manifests — there is no dynamic registration during execution. The action vocabulary is therefore a controlled contract.

provider:access uses the skill binding as its exact target. A manifest when clause may narrow it to one backend of one invocation; policy never derives this permission from the executor name or a free plan value.

2.1 Reading the registry

The registry covers reads, writes, external-provider access, administrative operations, and human interaction. The critical flag and default_approval class contribute to the module's matrix. By themselves, they do not prove that the current execution path presents an approval prompt.

For example, compute:pure and time:read use none; fs:write uses per_target; and mail:send, mail:write, system:admin, and drive:permissions use always. Read the complete set from the registry rather than a hand-maintained list on this page.

3. Autonomy × capability table

The matrix is the product of the three autonomy levels (ReadOnly, Supervised, Full) and the registered capabilities. Each cell yields allowed, approval_required, or denied. _init_table generates it. The table below is an excerpt; the table command prints the complete matrix.

capabilityReadOnlySupervisedFull
compute:pureallowedallowedallowed
fs:readapprovalapprovalallowed
fs:writedeniedapprovalallowed
code:execdeniedapprovalapproval
network:httpdeniedapprovalallowed
llm:localallowedallowedallowed
llm:onlinedeniedapprovalallowed
mail:readapprovalapprovalallowed
mail:senddeniedapprovalapproval
channel:inallowedallowedallowed
channel:outdeniedapprovalallowed
time:readallowedallowedallowed
parse:localallowedallowedallowed
calendar:readapprovalapprovalallowed
index:readapprovalapprovalallowed
metnos:readapprovalapprovalallowed
metnos:cacheallowedallowedallowed
system:readapprovalapprovalallowed
provider:accessapprovalapprovalallowed

3.1 The three rules that generate the table

The table is not arbitrary: it derives from three rules, one per level, that _init_table applies while iterating over the registry.

ReadOnly. Non-critical capabilities in class none are allowed. An explicit set of scoped reads and accesses — including fs:read, mail:read, calendar:read, network:sites, and provider:access — yields approval_required. Other critical or approval-class capabilities are denied.

Supervised. Capabilities in class none are allowed; all others yield approval_required. This is matrix semantics, not proof that the current dispatcher presents the corresponding dialog.

Full. Capabilities in class always still yield approval_required; all others are allowed. Current always entries include code:exec, mail:send, mail:write, system:admin, and drive:permissions.

An outcome is not the same as an enforced gate. The matrix retains approval_required for class always even at Full. Until the dispatcher uses this outcome as a general constraint, treat it as a module contract, not a whole-system guarantee.

Reversibility in practice: the undo engine

Reversibility is a contract separate from the autonomy matrix. A standard-compliant executor declaring revertible = true must name a reverse_pattern. The runtime keeps the execution data needed for reversal, and undo_last_turn attempts the inverse path while reporting failures.

The closed catalog in runtime/reverse_patterns.py includes, among others:

For standard-compliant manifests, the validator checks consistency between the reversibility claim and the inverse pattern. Compatibility paths remain for some manually maintained executors, so it would be inaccurate to say that every mutating operation uses only the catalog.

An inverse pattern neither grants authority nor removes an approval requirement by itself. Reversibility, authorization, and isolation are separate controls.

4. Persistent target-scoped grants

The module also provides a SQLite store for scoped grants. Each record binds a channel, sender, capability, target, and optional expiry. The store exists and can be queried; the ordinary approval path does not yet write grants to it.

4.1 SQLite schema

The schema is defined by runtime/policy.py:SCHEMA:

CREATE TABLE IF NOT EXISTS grants (
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 channel TEXT NOT NULL,
 sender_id TEXT NOT NULL,
 capability TEXT NOT NULL,
 target TEXT NOT NULL,
 granted_at TEXT NOT NULL,
 expires_at TEXT,
 granted_by TEXT,
 revoked_at TEXT
);

A grant is identified by the tuple (channel, sender_id, capability, target). Dates granted_at, expires_at, and revoked_at are UTC. Logical user separation depends on the caller supplying the correct channel and sender.

The default file is ~/.local/state/metnos/grants.db for the installation user; METNOS_GRANTS_DB can select another path. Multiple application users served by one process therefore share the physical container while rows remain separated by the identity supplied to the API.

4.2 API

functionwhat it does
record_grant(channel, sender_id, capability, target, expires_at=None, granted_by=None) Records a concession. Raises ValueError if capability is not in the registry. Returns the Grant object with assigned id.
has_grant(channel, sender_id, capability, target) True if an active grant (not revoked, not expired) exists for the four-tuple. The query compares expires_at with the current time.
list_grants(channel=None, sender_id=None, include_revoked=False) Lists grants, filterable by channel/sender, optionally including revoked. Sorted by granted_at descending.
revoke_grant(grant_id) Sets revoked_at to the current time. Returns True if something was modified, False if the grant was already revoked or did not exist.

The functions open and close one connection per call; they keep no global SQLite session.

4.3 Integration status

The current runtime has no production callers of record_grant, and the planner does not consult effective_outcome. The table can be populated by calling the module API directly, but no active administration surface does so, and it must not be described as the memory of user confirmations.

5. Combined outcome (effective_outcome)

The effective_outcome function combines the matrix and grants into one result. It is available to module callers and the CLI; it is not currently the planner's entry point.

table saysactive grant for (channel, sender, target)?outcome
allowedindifferent, the DB is not queriedallowed
deniedindifferent, the DB is not querieddenied
approval_requiredyesallowed
approval_requiredno (or scope parameters missing)approval_required

The logic is linear: if the table already decides cleanly (allowed or denied), the grant is not even consulted; if it decides approval_required, an active grant turns it into allowed, otherwise it stays approval_required.

Function invariant: a grant never turns denied into allowed. It can turn approval_required into allowed.

Known limitation. When it finds an active grant, effective_outcome does not distinguish per_target from always. A capability classified as always would therefore be elevated to allowed if a caller recorded such a grant. Before connecting the module to the dispatcher, this case must be prevented or given an explicit, different meaning.

5.1 Verifiable example

Module evaluation, not turn execution. The command check Supervised fs:write, without identity and target, returns approval_required. The command check ReadOnly fs:write returns denied. These results show the matrix; they neither save a file nor open a chat dialog.

6. Runtime integration

The module participates in the runtime at these points:

This grant store must therefore not be interpreted as the planner's active authorization mechanism. Any caller must preserve exact actor and target identity, enforce per-user isolation, handle always classes correctly, and use a localized semantic renderer.

7. CLI

From the repository root, inspect the module with the Metnos environment. It exposes five subcommands.

commandwhat it does
PYTHONPATH=runtime ./.venv/bin/python -m policy registryprints one JSON line per capability with all attributes.
PYTHONPATH=runtime ./.venv/bin/python -m policy tableprints one JSON line per level with every capability outcome.
PYTHONPATH=runtime ./.venv/bin/python -m policy check <level> <capability> [--channel C --sender S --target T]prints effective_outcome. Without identity and target, it shows the matrix-only result.
PYTHONPATH=runtime ./.venv/bin/python -m policy grants [--channel C] [--sender S] [--all]lists active grants; --all includes revoked rows.
PYTHONPATH=runtime ./.venv/bin/python -m policy revoke <grant_id>revokes a grant by identifier.

The registry, table, and grants commands emit JSON Lines and can be filtered with jq.

8. How to verify the module

Module verification comprises:

9. Policy boundaries

Final notes

The module's current value is narrow but concrete: a closed registry, deterministic matrix, and structured store. The public guarantee remains limited to what the runtime actually wires in.

Presenting this API as a general gate before it is connected to the dispatcher would claim more than the demonstrated behavior.