Policy engine

The policy engine decides whether a classified request may run. It is shared by every broker, it carries no provider vocabulary of its own, and it is deliberately small. This page covers how it decides, what a rule may contain, and what makes a policy file invalid.

Minimal policy file

A policy is one top-level rules array:

{
  "rules": [
    {
      "id": "agent-a-can-run-restart",
      "effect": "allow",
      "clients": ["agent-a"],
      "operations": ["exec.command"],
      "targets": [{ "kind": "user", "name": "deploy" }],
      "attrs": {
        "command_ids": ["restart-myapp"]
      }
    }
  ]
}

Each rule says which client may perform which operation on which target with which attrs. The file is the authorization source of truth, loaded at startup. hf-broker and gh-broker call it scope.json; sudo-broker calls it policy.json.

Effects and decision order

Three effects exist, and the engine can also return a fourth outcome when nothing matches:

EffectMeaning
allowThe request may run without human approval.
requestThe request may create a pending grant. It may not run yet.
denyThe request is refused.
no_matchReturned by the engine when no rule matches. Denies.

Order of evaluation is fixed and does not depend on where rules appear in the file:

deny > active generated grant > allow > request > no_match

Four consequences follow, and they are the properties worth internalising. Deny rules override static allow rules. Deny rules also override active grants, so a human approval cannot resurrect something a deny rule forbids. Request rules never permit execution by themselves. No matching rule means deny.

Rule fields

FieldRequiredTypeMeaning
idYesstringStable human-readable rule ID, recorded in audit entries.
effectYesstringallow, request, or deny.
clientsYesstring arrayClient names or client globs.
operationsYesstring arrayRegistered operation names.
targetsYesobject arrayRegistered target matchers.
attrsNoobjectAttr constraints for request details.
grant_policyFor requestobjectGrant bounds for requestable access.
descriptionNostringOperator-facing explanation.

Unknown fields are validation errors rather than ignored keys.

Grant policy

A request rule must carry grant_policy, and an allow or deny rule must not:

{
  "mode": "window",
  "default_minutes": 5,
  "max_minutes": 10,
  "default_max_uses": 1,
  "max_uses": 1
}

Each grantable operation defaults to window and allows both window and execution. A window grant authorizes an exact classified request for a bounded time and use budget. An execution grant approves one exact provider-built action and is always single-use. A request rule may list several operations only when they resolve to one mode that every listed operation allows.

default_max_uses is always a positive finite default. max_uses is either a positive finite ceiling no greater than 1000000, or null. Providers may impose a lower operation-specific ceiling. null lets callers request unlimited uses bounded only by the required expiry. Omitting a requested max_uses selects the finite default; explicitly requesting null selects unlimited use within the window. When a window policy omits both use fields, they default to the lowest provider ceiling among its operations. Policy may select execution mode, which always requires one use.

Grant mode is authorization metadata rather than a transport selector. Agent-facing MCP and CLI calls always submit through Agent Operations V1 regardless of mode, and a matching active window grant authorizes the stored operation and reserves one use atomically during admission or execution.

The provider registry

The registry is where provider vocabulary lives, and it is the only place it may live. It defines valid operation names, valid target kinds, required target fields per kind, valid attr names, the attrs each operation supports, which operations may be granted, the grant mode for each grantable operation, and the match semantics for every field and attr.

type Registry struct {
    Operations map[string]OperationSpec
    Targets    map[string]TargetSpec
    Attrs      map[string]AttrSpec
}

The Go API lives in unyolo/policy, and brokers supply the registry values they pass into it. The evaluator hard-codes no provider vocabulary, which is what keeps the shared core honest.

Generated grants

An approved grant becomes a generated allow rule. It uses the same matching semantics as a static rule and adds runtime metadata: a grant ID, the client, an expiry time, a use budget, a status, the approved attrs, and a decision-token verifier.

Two constraints apply specifically to generated rules. They may never use wildcard clients. Raw decision tokens must never be stored in grant or status objects or serialized into grant JSON; a broker passes the raw token only to the approval notification path.

Validation

An explicitly empty rules array is valid and denies everything. Policy loading rejects:

  • missing or null rules
  • unknown fields
  • duplicate rule IDs
  • unsupported effects
  • unsupported operations
  • unsupported target kinds
  • unsupported attrs
  • attrs on a rule that are not supported by every operation the rule lists
  • invalid glob patterns
  • request rules without grant_policy
  • allow or deny rules that carry grant_policy
  • generated grants with wildcard clients
  • overlapping request rules with different normalized grant policies when the core cannot prove they are disjoint

The rule about attrs and operations is the one that surprises people. If a rule lists two operations and an attr that only one of them supports, the rule is invalid. Split it into two rules.

Engine boundaries

The policy core does not authenticate clients, parse provider requests, execute commands, call GitHub or Hugging Face, run sudo or systemd, decide whether a Git push is a fast-forward, inspect shell commands, write audit logs, or project provider-specific approval semantics into the shared presentation model. Those all belong to the broker using it.

The fast-forward example is the useful one to remember. Deciding whether a push is append-only requires reading Git objects, which is provider work. The broker does that, produces the operation name git.push.append or git.push.force, and only then asks the engine anything.

Native Git transactions

A receive-pack request is one authorization transaction. Provider adapters classify every ref before any bytes go upstream. A deny rejects the whole batch. Requestable refs produce a single approval bound to the exact body digest and ordered command set.

The original Git request waits for the durable decision, reacquires any provider lock, revalidates policy and repository state, reserves one use, and only then forwards the unchanged body. Atomic and multi-ref pushes are never split into separately authorized updates, so a partial result is not a state the design allows.

Esc

Type to search across every documentation page.