Policy, rules and kill switches
Last updated October 5, 2026
On this page
- What policy holds
- The Governance › Policy tab
- Policy as code
- The directory
- Who owns what
- Walkthrough: add a rule that holds an action
- 1. Export the policy
- 2. Write the rule and a test
- 3. Check it offline
- 4. Check it against the server and see the difference
- 5. File it as a proposal
- 6. Review it
- 7. Shadow, then promote
- 8. See the rule hold the action
- 9. Take the rule away again
- CI recipe
- Drift and the source of truth
- Kill switches
- Each switch has a holder
- Pausing a module or a kill switch
- Walkthrough: stop the Sales Assistant, then everything
- Budgets
- Not possible yet
- Known problems
- For developers
- API
- Events
- CLI
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:
- 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:
# 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:
- 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:
export OK_TOKEN=orchk_... # ada's token
export OK_SERVER=http://127.0.0.1:8080
1. Export the policy#
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:
- 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:
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#
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#
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:
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):
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):
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#
- Sign in as alice and open Threads. Under Talk to an agent, click
Sales Assistant. Type
Email a follow up to my contacted leadand press Ask agent. - 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#
- Move
policy/rules/agent-email-needs-sales-lead.yamlout of the directory. - 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"). - 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. - Apply it as a new commit, for example
--commit 2b3c4d5e6f7a8b9c0d1e. The card shows Rule removed and the same loosening. - 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#
-
An admin creates an agent with the role
policy-ciand no skills, tools or delegations, and issues it a token limited to thepolicyscope. With the server running, from the API: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 policydoes 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". -
On every pull request, with the CI token in
OK_TOKEN: the offline check (ok policy check policy/withOK_SERVERunset), andok policy check policy/ --server "$OK_SERVER"where CI can reach the kernel. Add--format markdownfor a table to post as a comment. -
After a merge to the main branch:
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
--authorthe 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. -
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:
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#
- Sign in as ada and open Governance › Policy.
- 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)".
- 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".
- As ada, press Resume an agent, choose Sales Assistant, press Resume. The note says "Sales Assistant may run again".
- 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
STOPunder "Type STOP to confirm". Press it. The note says "Every agent is stopped" and the badge turns red: Every agent is stopped. - 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".
- 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:
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, addingteam:sales: { limit_cents: 50000 }shows on the proposal as "Budget team:team:sales none → $500.00". Once promoted, Try an action (orok 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.yamlkeeps 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 changesorok token issue: they work on the state file only and have no--serveroption. - 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/killchecks 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.sendis said to "may override" deny rules onbrain.upsert(leads)and similar, so a tightening change needs shadow or a confirmation. ok killagainst a running server's state file prints "tool:gmail.send released" but the server keeps the switch. Use the screen orPOST /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#
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.