Policy, rules and kill switches

Last updated October 5, 2026

On this page

Policy is what OrchKernel checks before an action runs: the rules that allow, hold or refuse an action, the kill switches that stop agents and tools, and the budgets that cap spending. This page covers the Governance › Policy tab, keeping company policy in a Git repository ("policy as code"), and the kill switches.

How the gate weighs all of this for one call, step by step, is in How the gate decides. This page describes policy as it works today on the golive branch. What is not possible yet is listed at the end.

The steps below were run in the demo company with no model configured. The demo's stub planner then answers the walkthrough's asks (such as the Sales Assistant's "Email a follow up to my contacted lead") in place of a model, so nothing on this page needs a model key.

What policy holds#

Part What it does Who changes it
Rules Each rule matches some calls (by tool, action, collection, team, role, actor, risk, amount) and allows, denies or requires approval. The highest-priority matching rules decide; at equal priority deny beats approval beats allow. Company rules: an admin, through a proposal in the change pipeline. Kernel and module rules: their owner.
Kill switches Stop every agent, one agent, one tool, or one team at once. An admin, from the screen or the API. Through the API any signed-in person can too today (see Known problems).
Budgets Spending limits for teams and agents. The policy repository (budgets.yaml) or a module.
Model governance Which model may see which class of data. Configuration (Models).

A proposal is a change waiting in the change pipeline: it is checked, reviewed by an admin, optionally run in shadow, and only then promoted (made live). Shadow means the proposed policy decides every action a second time next to the live one, without effect, so you can see what it would change.

The Governance › Policy tab#

Open Governance, then Policy. Everyone can open it; only an admin sees the kill switch buttons and the drift line.

Card What it shows
Source of truth source: Kernel (the default) or source: Repository · acme/policy@main, then either "no bundle applied yet" or "applied commit 1a2b3c4d5e6f from acme/policy@main at" a date and time. For an admin, also "In sync with the applied bundle." or a red line "Drift: 1 object(s) differ from the applied bundle: ...".
Try an action Ask the gate what it would decide for one action by one person or agent, without running it. See How the gate decides.
Kill switches A green Agents running badge, or a red Every agent is stopped. Then "Paused agents: ... · tools: ... · teams: ...", each name followed by who holds the switch: "(an admin)", "(the policy repository)" or the module, as in "(Support operations (paused))". Switches the repository holds are listed again under "Held by the policy repository:" with their reason. Admins get Engage global (or Release global), Pause an agent, Resume an agent (greyed out until an admin has paused one) and Disable a tool.
Model governance The default model and, per model, the most sensitive data it may see. In the demo: mock-default, Restricted.
Rules (21) Every rule: its description and id, its effect (Allowed, Denied, Needs approval), what it applies to (Agents only, gmail.send, Over $100.00, Risk Medium or higher), its owner (Kernel, Company, or the module such as CRM) and its priority. Below the table, a folded defaults section holds the kernel's defaults as JSON.

The demo company starts with 21 rules: 2 owned by the kernel and 19 from its modules. It has no company rules until you add one.

Policy as code#

Company rules, the switches the repository holds, and budgets can live in a directory in a Git repository, policy/ by default. Someone changes them in a pull request, CI checks the change, and after the merge the change is filed in OrchKernel as a proposal. Nothing in Git applies itself: an admin reviews and promotes it in OrchKernel.

The directory#

policy/
  policy.yaml        which kernel this is for, and what the repository manages
  rules/<id>.yaml    company rules
  kill.yaml          switches the repository holds
  budgets.yaml       team budgets, and budgets of agents no module manages
  tests/*.yaml       expected decisions, run by every check
  managed/           read-only copies of what others own; never applied
  reference.json     ids of actors, tools and collections, for offline checks

A rule file holds a list of rules:

yaml
- id: agent-email-needs-sales-lead
  description: While the pricing change is announced, every email an agent sends waits for Sam
  effect: require_approval
  approver: { approver: actor, id: sam }
  matches: { agents_only: true, tools: [gmail.send] }
  priority: 40
Field Values
effect allow, deny, require_approval
approver With require_approval: { approver: acting_for } (the person the agent acts for), owner (of the task or thread), manager, { approver: role, role: admin }, { approver: actor, id: sam }
matches Any of actors, teams, roles, agents_only, humans_only, external_only, actions (prefixes such as brain.upsert(leads)), tools, collections, min_risk, min_sensitivity, data_classes, amount_over (cents; name tools with it). Every condition given must hold.
priority A number; higher decides first.

kill.yaml and budgets.yaml:

yaml
# kill.yaml
switches:
  - target: tool:marketing.ads.update_audience
    reason: Audience sync paused until the consent review closes (LEGAL-88)

# budgets.yaml
agents:
  architect: { alert_pct: 80, limit_cents: unlimited }
teams:
  team:sales: { limit_cents: 50000, alert_pct: 80 }

A test case is the question "what would the gate decide?" with the answer you expect:

yaml
- name: the sales assistant's email waits for sam
  actor: sales-assistant
  for: alice
  do: { op: tool, tool: gmail.send, args: { to: dana@litware.example, subject: Following up } }
  expect: require_approval      # allow, deny, require_approval or conditional
  rule: agent-email-needs-sales-lead

Files are read strictly. A misspelled key (tool: for tools:) is an error with its file and line. So are YAML anchors ("YAML anchors are not allowed in a policy file") and anything that looks like an OrchKernel token or a private key ("looks like a secret (a kernel token or a private key); remove it from the repository"). Keep the repository private: reference.json and the rules name people.

Who owns what#

Object Owner In the directory Changed by an apply
Rules the kernel installs the kernel managed/kernel-rules.yaml No
Rules a module installs the module managed/pack-rules/<module>.yaml No
Every other rule the company rules/<id>.yaml Yes
Admin and module kill switches the admin, the module managed/kill.yaml No
Switches the repository holds the repository kill.yaml Yes
Team budgets, budgets of agents no module manages the company budgets.yaml Listed entries only
Budgets of a module's agents the module managed/budgets.yaml No
Model governance, defaults configuration managed/models.yaml, managed/defaults.yaml No

A company rule cannot reuse a kernel or module rule's id: the check says "rule id crm-agent-email-needs-rep belongs to the pack crm; a company rule needs its own id". To change what a module rule does, add a company rule with a higher priority.

An apply sets and lifts only the switches the repository holds, so an admin's emergency switch survives every apply.

Walkthrough: add a rule that holds an action#

In the demo, when alice asks the Sales Assistant to email a lead, the email waits for alice herself (the CRM module's rule crm-agent-email-needs-rep). You will add a company rule so that, for a while, every agent email waits for sam, the sales lead.

You need a terminal with the ok command and the demo running (see Quickstart). ok demo serve listens on http://127.0.0.1:8080 unless you pass --bind; use the Workspace address it printed.

Every ok policy command that talks to the server reads its token from OK_TOKEN, never from the command line, and the server from OK_SERVER or --server. Use ada's token from the demo's start-up table:

sh
export OK_TOKEN=orchk_...        # ada's token
export OK_SERVER=http://127.0.0.1:8080

1. Export the policy#

sh
ok policy export policy/

You see "exported 17 files to policy/" and "keep this repository private: it names actor ids, which can be personal data". The directory holds policy.yaml, kill.yaml (switches: []), budgets.yaml, reference.json and managed/. There is no rules/ folder yet: the demo has no company rules. Run it again and it refuses: "policy/ is not empty; pass --overwrite to replace what it holds". --overwrite replaces what export writes and keeps your tests/.

2. Write the rule and a test#

Create policy/rules/agent-email-needs-sales-lead.yaml with the rule shown in The directory. Create policy/tests/email.yaml with the test case shown there, followed by a second case that says a person is not affected:

yaml
- name: the sales assistant's email waits for sam
  actor: sales-assistant
  for: alice
  do: { op: tool, tool: gmail.send, args: { to: dana@litware.example, subject: Following up } }
  expect: require_approval      # allow, deny, require_approval or conditional
  rule: agent-email-needs-sales-lead
- name: alice can still send her own email
  actor: alice
  do: { op: tool, tool: gmail.send, args: { to: dana@litware.example, subject: Hi } }
  expect: allow

3. Check it offline#

An offline check needs no server and no token. ok policy check talks to the server whenever OK_SERVER is set, so leave it out for this one command:

sh
env -u OK_SERVER ok policy check policy/

You see:

pass tests/email.yaml:1 the sales assistant's email waits for sam
pass tests/email.yaml:7 alice can still send her own email
0 errors, 0 warnings; 2 of 2 cases passed

Try a mistake to see what a failure looks like; each one exits with code 1:

You change The check says
tools: to tool: in the rule error rules/agent-email-needs-sales-lead.yaml:5 unknown_key: unknown key `tool` in matches
id: sam to id: samm "unknown_reference: rule agent-email-needs-sales-lead names approver samm, who is not an active human"
expect: allow to expect: deny in the second case "fail tests/email.yaml:7 alice can still send her own email: expected deny, got allow"

Put the files back as they were before going on.

Offline, cases run against the rule layer only (switches, rules, module rules and defaults; not delegations, budgets or model governance). Checked against the server, an admin's cases run through the full gate.

4. Check it against the server and see the difference#

sh
ok policy check policy/ --server "$OK_SERVER"
ok policy diff policy/ --server "$OK_SERVER"

The check against the server also lists what the change loosens. Here it prints "loosening: rule agent-email-needs-sales-lead requires approval: may override pack rule crm-no-mass-stage-changes-by-sandboxed (deny); may override pack rule marketing-agents-never-delete-people (deny); may override pack rule product-ops-agents-never-edit-the-roadmap (deny)". This rule only holds emails, so none of that is true (see Known problems), but it means promoting will need shadow or a confirmation (step 7). Once the kernel has changed since your export, the check also warns "reference.json differs from the kernel: run export to refresh managed/".

The diff marks each object:

Mark Meaning
in_sync The same in the directory and in the kernel.
pending Added, changed or removed in the directory, not yet applied.
changed Changed in the kernel since the last apply, with who and when (for admins).
conflict Changed in both.

Here you see pending rule agent-email-needs-sales-lead and "not in sync". With --exit-code, diff exits 1 unless everything is in sync.

5. File it as a proposal#

sh
ok policy apply policy/ --server "$OK_SERVER" \
    --repo acme/policy --branch main --commit 1a2b3c4d5e6f7a8b9c0d

You see "proposal 7e74ebd4-... proposed: policy from acme/policy@main commit 1a2b3c4d5e6f7a8b9c0d" and the loosening line again. Note the proposal id. Nothing has changed yet.

6. Review it#

Open Governance › Changes. The card policy from the repository (marked Proposed) shows "acme/policy@main commit 1a2b3c4d5e6f (policy) as reported by the Git host", "author: ada (directory: Ada Park) · approved in Git by: nobody", a table of what it changes (Rule added), what it loosens in red ("Loosens: ..."), and its evals: "Evals: 4 passed", which are bundle valid, references resolve and one per test case.

Reviewing, shadow and promoting take an admin who neither filed the proposal nor wrote it. When that is you, the card greys out Approve review and says "Review, shadow and promote are off: you proposed it."

In the demo ada is the only admin, and the server lets a company's only admin review their own proposal (it is recorded as self-reviewed). The screen does not know this and keeps the buttons off through review, shadow and promote (see Known problems). So as ada in the demo, do steps 6 and 7 from the API, with the proposal id from step 5:

sh
curl -X POST -H "Authorization: Bearer $OK_TOKEN" -H 'content-type: application/json' \
  "$OK_SERVER/api/changes/<proposal-id>/review" -d '{"approve": true}'

The card moves to Reviewed with "Ada Park approved".

When CI files the proposal and names another author (see the CI recipe), ada does all of this on the card instead: Approve review opens "Approve this policy change" with an optional Note; press Approve review in it. The card then offers Start shadow, Promote despite loosening (or Promote) and Reject.

7. Shadow, then promote#

A change that may loosen policy must run in shadow, or be promoted with the loosening confirmed. Without either, promote is refused: "loosening_unconfirmed: this change loosens policy and never ran shadow: ...; run shadow or confirm the loosening".

Start shadow (Start shadow on the card, or from the API as ada):

sh
curl -X POST -H "Authorization: Bearer $OK_TOKEN" -H 'content-type: application/json' \
  "$OK_SERVER/api/changes/<proposal-id>/canary" -d '{}'

The card moves to Shadow and shows "Shadow: 0 decisions compared, 0 differed." The live policy still decides. Sign in as alice and ask the Sales Assistant to Email a follow up to my contacted lead (as in step 8). The email still waits for alice, and the card now counts the decisions, for example "Shadow: 5 decisions compared, 1 differed."

Then promote (Promote on the card, or from the API):

sh
curl -X POST -H "Authorization: Bearer $OK_TOKEN" -H 'content-type: application/json' \
  "$OK_SERVER/api/changes/<proposal-id>/promote" -d '{}'

A change that never ran in shadow can still be promoted with the loosening confirmed: from the API with -d '{"confirm_loosening": true}'; on the card the button reads Promote despite loosening and asks "This loosens policy and never ran in shadow. Promote anyway?".

After promoting, the card is marked Promoted. The Policy tab shows Rules (22), with the new rule at the end, owner Company, priority 40. Source of truth shows "source: Kernel · applied commit 1a2b3c4d5e6f from acme/policy@main at ..." and "In sync with the applied bundle."

8. See the rule hold the action#

  1. Sign in as alice and open Threads. Under Talk to an agent, click Sales Assistant. Type Email a follow up to my contacted lead and press Ask agent.
  2. A Send email approval card appears for dana@litware.example. It shows the rule's description ("While the pricing change is announced, every email an agent sends waits for Sam") and "Waiting for Sam Ortiz." Alice has no Approve button on it; sam has the approval in his Inbox.

An approval asked before the promote is checked against the policy as it is now. Alice's older email card (from step 7) now says "Policy changed since this was asked: it now asks sam". If she presses Approve, she gets "policy denied approve(...): the policy now asks sam". Sam may approve it, but it is not in his inbox; he can only reach it through the API today (see Known problems). See Inbox and approvals.

9. Take the rule away again#

  1. Move policy/rules/agent-email-needs-sales-lead.yaml out of the directory.
  2. In policy/tests/email.yaml, change the first case to expect the CRM rule again: rule: crm-agent-email-needs-rep (and rename it "the sales assistant's email waits for alice").
  3. Run ok policy check policy/ --server "$OK_SERVER". It prints "loosening: rule agent-email-needs-sales-lead (require approval) is removed" and the reference.json warning, and both cases pass.
  4. Apply it as a new commit, for example --commit 2b3c4d5e6f7a8b9c0d1e. The card shows Rule removed and the same loosening.
  5. Review it and promote it with the loosening confirmed (step 7). Alice's assistant's emails wait for alice again: the card says "Agents never send email without the rep approving the draft" and alice has Approve.

CI recipe#

  1. An admin creates an agent with the role policy-ci and no skills, tools or delegations, and issues it a token limited to the policy scope. With the server running, from the API:

    sh
    curl -X POST -H "Authorization: Bearer $OK_TOKEN" -H 'content-type: application/json' \
      "$OK_SERVER/api/actors" -d '{"actor": {"id": "policy-ci", "kind": "agent", "name": "Policy CI", "role": "policy-ci"}}'
    curl -X POST -H "Authorization: Bearer $OK_TOKEN" -H 'content-type: application/json' \
      "$OK_SERVER/api/tokens" -d '{"actor": "policy-ci", "scopes": ["policy"]}'
    

    The second call answers with the token. With the server stopped, ok token issue --for policy-ci --scope policy does the same. That token reaches check, diff, proposals and the applied record, and nothing else: its inbox answers "this token's scopes do not reach /inbox".

  2. On every pull request, with the CI token in OK_TOKEN: the offline check (ok policy check policy/ with OK_SERVER unset), and ok policy check policy/ --server "$OK_SERVER" where CI can reach the kernel. Add --format markdown for a table to post as a comment.

  3. After a merge to the main branch:

    sh
    ok policy apply policy/ --server "$OK_SERVER" --repo acme/policy --branch main \
        --commit "$COMMIT" --author "$PR_AUTHOR" --author-email "$PR_AUTHOR_EMAIL" \
        --approved-by "$APPROVER"
    

    CI must name the author: without --author the apply is refused with "origin.author is required: CI names the pull request's author". The author is matched to a person by login or email. In the demo, with --author erin --approved-by pat, the card says "proposed by Policy CI" and "author: erin (directory: Erin Walsh) · approved in Git by: pat (directory: Pat Kim)". Neither erin nor the CI agent can review it; ada can, on the card. A new apply from the same repository and branch replaces the open one, which is marked Rejected and "superseded by" the new proposal's id.

  4. On a schedule: ok policy diff policy/ --server "$OK_SERVER" --exit-code, to catch changes made in the kernel.

ok policy refuses plain http except to a loopback address: "plain http is allowed only to a loopback address; use https so the token is not sent in clear".

Drift and the source of truth#

Policy can still change in the kernel outside Git, for example a rules proposal promoted in the change pipeline. That is drift. ok policy diff lists it as changed rule no-agent-calendar-on-weekends (by ada at ...), and the Source of truth card says "Drift: 1 object(s) differ from the applied bundle: rule no-agent-calendar-on-weekends (changed)".

To keep such a change, export with --overwrite (the rule is written to rules/, your tests/ stay), commit it, and apply it. The diff is in sync right after the export; the card's drift line clears once that apply is promoted. An apply of a directory that leaves the rule out removes it again.

To make the repository the only place company policy changes:

sh
ok policy source repository --repo acme/policy --branch main --server "$OK_SERVER"

It prints "policy source: repository" and the card shows "source: Repository · acme/policy@main". From then on a rules proposal in the kernel is refused: "managed_by_repository: rule x-test is managed by the policy repository acme/policy (main); change it there and apply". Kill switches and module pauses still work. ok policy source kernel --server "$OK_SERVER" goes back to the default.

Kill switches#

A kill switch stops agents at once. It needs no approval and no review.

Switch Stops From the screen What a stopped run or ask is told
Global Every agent in the company, and most of what people do (below) Engage global (type STOP) "global kill switch engaged"
Agent (actor:<id>) One agent Pause an agent "actor sales-assistant is paused"
Tool (tool:<id>) Every call to one tool, by anyone Disable a tool "tool gmail.send is disabled"
Team (team:<team-id>) Everyone in one team, agents and people Not on the screen; API only "team team:sales is paused"

What people can still do. The switches are checked for every action the gate governs, not only agents' actions, so they stop people too:

Switch People
Global Everyone can still open threads and decide approvals. Reading records, posting a message and creating a thread are refused: "policy denied thread.post(...): global kill switch engaged". On Brain, maya's tickets show "No records" and "Every agent is paused by an admin."
Team The team's people are refused the same way: as alice under a Sales team switch, a post answers "team team:sales is paused".
Agent People are not affected. An approval for the paused agent cannot be approved ("policy denied approve(...): actor sales-assistant is paused") until it runs again.
Tool Nobody may call the tool, people included.

The Engage global sheet says "People can still work"; as the table shows, that is not so today (see Known problems).

A team's id already starts with team:, so the team switch's target is team:team:sales. A switch on an id that does not exist (actor:churn-risk-watcher when the agent's id is churn-watcher, or team:sales) is accepted with {"ok": true} and stops nothing; check the card after engaging one.

Each switch has a holder#

Holder Set by Lifted by
An admin The Policy tab, POST /api/kill The same. Resume an agent lists only agents an admin paused.
A module Pause on the module (Modules) Resume on the module
The policy repository kill.yaml, at apply An apply whose kill.yaml leaves it out

A target stays stopped while anyone holds a switch on it. Pausing the Support operations module puts switches on its three agents and four tools, shown as "Churn-risk watcher (Support operations (paused))". Releasing actor:churn-watcher as an admin answers {"ok": true} but leaves it paused: maya's ask is still refused with "kill switch engaged: support-ops is paused" until the module resumes.

Pausing a module or a kill switch#

Pausing a module A kill switch
Covers All its agents, tools, triggers and pages One agent, one tool, one team, or everything
Its data and settings Stay Stay
Lifted by Resuming the module Whoever set it
Use it when A whole department's automation should stop One agent misbehaves, one system must not be called, or everything must stop now

Walkthrough: stop the Sales Assistant, then everything#

  1. Sign in as ada and open Governance › Policy.
  2. In the Kill switches card, press Pause an agent. The sheet says "It stops at its next step and starts nothing new until it is resumed." Choose Sales Assistant under Agent and press Pause. A note says "Sales Assistant is paused". After a few seconds the card reads "Paused agents: Sales Assistant (an admin)".
  3. Sign in as alice and open Threads. Under Talk to an agent, the Sales Assistant is greyed out and marked "paused" (its tooltip: "Paused by an admin"). Asking it through the API is refused with "policy denied run: kill switch engaged: actor sales-assistant is paused".
  4. As ada, press Resume an agent, choose Sales Assistant, press Resume. The note says "Sales Assistant may run again".
  5. Press Engage global. The sheet "Stop every agent?" explains that running work is refused at its next step and nothing new starts. Its Engage global button stays greyed out until you type STOP under "Type STOP to confirm". Press it. The note says "Every agent is stopped" and the badge turns red: Every agent is stopped.
  6. Sign in as maya and open Threads. Every agent under Talk to an agent, the Support Agent included, is greyed out and marked "paused". Asked through the API, the Support Agent is refused: "kill switch engaged: global kill switch engaged".
  7. As ada, press Release global. The note says "Agents may run again" and the badge is green again. Maya can now click Support Agent, ask What is open?, and get the table of open tickets.

Disable a tool works the same way: its sheet says "No agent may call it until it is enabled again"; choose a tool such as Send email (gmail.send) and press Disable. The note says "Send email is disabled", and the card lists "tools: Send email (an admin)". Alice's next email ask fails with "op 1: policy denied tool.call(gmail.send): tool gmail.send is disabled". The screen has no button to enable the tool again. Release it from the API:

sh
curl -X POST -H "Authorization: Bearer $OK_TOKEN" -H 'content-type: application/json' \
  "$OK_SERVER/api/kill" -d '{"scope": "tool:gmail.send", "engaged": false}'

It answers {"ok":true}.

Every switch is logged. Open Events and choose the type Kill switch: each row reads like "tool:gmail.send released" or "global engaged". Open a row to see who flipped it ("by Ada Park"). See Events and audit.

Budgets#

Budgets cap what a team or an agent may spend on model calls and tools. The gate checks them before each call (How the gate decides); alert_pct is the share spent at which an alert is raised.

  • Team budgets, and budgets of agents no module manages, come from budgets.yaml. In the demo, adding team:sales: { limit_cents: 50000 } shows on the proposal as "Budget team:team:sales none → $500.00". Once promoted, Try an action (or ok policy explain) for the Sales Assistant includes "team budget: $0.00 of $500.00 spent".
  • A module sets its own agents' budgets (managed/budgets.yaml).
  • An agent removed from budgets.yaml keeps its budget: the proposal lists "agent designer is no longer managed; its budget stays as it is".
  • A team removed after an apply listed it loses its budget, which is flagged as loosening: "budget team:team:sales is removed".

Not possible yet#

  • Changing a company rule from the screen. Rules change through the repository or a rules proposal in the change pipeline.
  • Engaging a team switch, or enabling a disabled tool again, from the screen.
  • Setting a budget from the screen.
  • Reaching a running server with ok kill, ok changes or ok token issue: they work on the state file only and have no --server option.
  • Issuing the CI agent's scoped token from the screen.

Known problems#

Found while writing this page; reported for fixing.

  • Any person can flip any kill switch. The screen shows the buttons only to admins, but POST /api/kill checks only that the caller is a person. In the demo alice, a sales rep, engaged and released the global switch, and released ada's pause on the Sales Assistant.
  • The global and team switches stop people too. Under the global switch maya cannot read tickets, post or create a thread; under a team switch the team's people cannot post. The Engage global sheet says "People can still work."
  • The only admin cannot review their own policy change on screen. The server allows it (self-reviewed); the card keeps Approve review, Start shadow and Promote greyed out with "you proposed it". Use the API, as in steps 6 and 7.
  • An approval the policy re-routes does not reach the new approver. After the promote in step 7, alice's older email asks sam, but it stays in alice's inbox and not in sam's; sam can approve it only through the API.
  • Loosening lists unrelated rules. A rule that holds gmail.send is said to "may override" deny rules on brain.upsert(leads) and similar, so a tightening change needs shadow or a confirmation.
  • ok kill against a running server's state file prints "tool:gmail.send released" but the server keeps the switch. Use the screen or POST /api/kill.
  • Misspelled switch targets are accepted and stop nothing; an unknown scope (bogus) answers 500 ("unknown kill scope bogus") instead of 422.
  • Admin release of a switch someone else holds answers {"ok": true} though nothing changed.
  • A module's paused agents say "Paused by an admin" in Talk to an agent, though a module holds the switch.
  • The Pause an agent note appears a few seconds before the card's "Paused agents" line updates.
  • The proposal card shows a rule's matches as raw JSON with every empty field, and a team budget as team:team:sales.

For developers#

API#

All under /api, with Authorization: Bearer <token>.

Method and path Who Notes
GET /policy everyone rules, module_rules, kill (global, actors, tools, teams, each target with its holders: "admin", "policy", {"module": "<pack>"}), models, defaults, rule_owners, source, applied, switch_reasons
GET /policy/export admins { files: { path: text } }, as ok policy export writes
POST /policy/check admins; the policy scope (pass or fail per case)
POST /policy/diff admins; the policy scope (without who and when)
POST /policy/proposals admins; the policy scope with origin.author
GET /policy/applied everyone
GET /policy/drift admins { applied, applied_hash, live_hash, objects, source }
PUT /policy/source admins Governed as policy.change
POST /policy/explain see How the gate decides Changes nothing
GET /changes, POST /changes see Changing a skill or playbook POST with { "kind": "rules", "rules": [...], "rationale": "..." } files a rules proposal
POST /changes/:id/review, /canary, /promote, /reject admins review takes { "approve": true, "note": "" }; promote takes { "confirm_loosening": true }
POST /kill any person (see Known problems) { scope: "global" | "actor:<id>" | "tool:<id>" | "team:<id>", engaged }
POST /actors, POST /tokens admins (as used here) The CI recipe's agent and its scoped token

Errors: 422 invalid with problems ({ file, line, message } for a bundle check), 413 too_large, 409 policy_moved, wrong_kernel, id_owned, loosening_unconfirmed, managed_by_repository, too_many_proposals, 403 same_person, denied, token_scope.

Events#

Event When
kill_switch A switch is engaged or released: scope as given, engaged, the actor who did it
policy_applied, rule_changed, budget_set A policy proposal is promoted
policy_shadow_differed In shadow, the candidate decided differently (the first 1,000)
policy_source_changed ok policy source

CLI#

sh
ok policy export DIR [--overwrite] [--retarget] [--server URL]
ok policy check DIR [--server URL] [--format text|json|markdown]   # offline only when OK_SERVER is unset
ok policy diff DIR [--server URL] [--exit-code]
ok policy apply DIR --server URL --repo R --branch B --commit C [--path policy] \
    [--author LOGIN] [--author-email E] [--approved-by LOGIN]...
ok policy source kernel|repository [--repo R --branch B] --server URL
ok policy explain [OP] [TARGET] --actor ID [--for ID] [--args JSON] --server URL
ok kill global|actor:<id>|tool:<id>|team:<id> [--off]     # state file only
ok changes review|canary|promote <id> [--confirm-loosening] # state file only

--retarget points a directory exported from one kernel at another (each kernel has an instance id, kin_ and 32 hex characters, in policy.yaml). The full design is in docs/policy.md.