Guides / Policy reference
Policy reference
kosa8 reads two documents an organisation signs. The organisation policy sets rules for the machine: which registries, which network, which models, who may approve. The MCP policy sets rules for AI tools: which tool servers, which tools, with which arguments. This page lists every field of both, where each one is enforced, and what you see when it refuses.
If you have not installed a policy before, start with Company rules, which walks through it. This page is the reference you return to.
| Organisation policy | MCP policy | |
|---|---|---|
| Commands | kosa8 policy | kosa8 mcp-policy |
| Installed at | ~/.kosa8/policy.json | ~/.kosa8/mcp-policy.json |
| Signed with | The same organisation key. Its public half is pinned at ~/.kosa8/policy-trust.pub. | |
| Decides | Images, network defaults, approvals, audit, models, snapshots, agent identity | Tool servers, tools, arguments, tool definitions |
Part A. The organisation policy
A JSON object. Every field except name is optional, and a field left out restricts
nothing. "Enforced" names the place in kosa8 that can refuse.
| Field | Type | What it does | Where it is enforced | What you see |
|---|---|---|---|---|
name | string | The policy's name. It appears in every refusal and in the audit log. | Nowhere on its own. | — |
allowed_registries | list of strings | The only registry hosts images may come from. An image with no registry in its name counts as docker.io. Empty means any. | Wherever the service fetches an image: kosa8 pull, a sandbox pull, the pull a run does for a missing image, and the Docker-compatible API. | policy "example-corp" forbids registry "quay.io" (allowed: docker.io, ghcr.io) |
default_egress | string | The network rule for a sandbox created without one: deny or allow:a.com,b.com. Empty means open. | When a sandbox is created. It is a default, not a ceiling: a request that names its own rule keeps it. | Nothing is refused. The sandbox's audit entry carries egress=deny (org default). |
require_sandbox | true or false | Containers must run in a sandbox of their own, not on the shared engine. | kosa8 run, interactive runs, and container create on the Docker-compatible API. | policy "example-corp" requires containers to run in a sandbox, and this would run on the shared engine; use `kosa8 sandbox create` and `kosa8 sandbox run` |
allowed_mcp | list of strings | The only images a third-party tool server may run from, matched as whole names. Empty means any. | kosa8 mcp add, and again when the gateway starts the server. | policy "example-corp" forbids MCP image "IMAGE" (allowed: …) |
require_fips | true or false | kosa8 must be a build that uses a FIPS 140-3 validated cryptographic module. | Once, when the service starts. It refuses to start otherwise. | kosa8d: policy "example-corp" requires a FIPS 140-3 validated cryptographic module, and this binary was not built with one (rebuild with GOFIPS140=certified, or install a FIPS release) |
approvers | list of approvers | The people whose signed approvals count. Listing anyone turns signed approvals on. | In front of every route of the service, so the CLI, AI tools and the Docker-compatible API are covered alike. | A refusal that names the action, says it needs signed approval before it can run, lists who can sign, and gives the commands to request it. |
approval_rules | list of rules | Which actions need signed approval, and from how many people. With approvers and no rules, the rule is one approver for escalating actions. | Same place. Every rule that matches an action must be met. | As above. |
allow_unlisted_requesters | true or false | Lets someone who is not an approver ask for an approval. Off by default, so a listed approver cannot request under a throwaway key and approve under their own. | When an approval request is recorded. | The request is refused. |
require_audit_forwarding | true or false | Audit entries must leave the machine: at least one sink must be configured in ~/.kosa8/audit-config.json. | Once, when the service starts. | kosa8d: policy "example-corp" requires audit records to be forwarded off this machine, and no sink is configured: add one to ~/.kosa8/audit-config.json … |
min_audit_retention_days | whole number | The shortest time entries stay in the live log. Local settings can keep them longer, never shorter. | When retention runs, hourly in the service and in kosa8 audit prune. | Younger entries are kept. The prune report says org policy "example-corp" keeps entries for at least 400 days. |
labels | object | The classification levels a sandbox or snapshot may carry and what each permits: destinations, shared folders, widest network. Left out, the levels are UNCLASSIFIED and CUI. | When a sandbox is created, started, relabelled, or a snapshot leaves the machine. | The request is refused with the label rule it broke. kosa8 label ls shows what is allowed. |
allowed_models | list of models | The only model files the local model runtime may load, each named by the SHA-256 of its file. Empty means any. | kosa8 model serve. The built-in assistant also refuses a hosted provider, whose model cannot be pinned. | policy "example-corp" does not approve model NAME: its weights hash to sha256:…, which is not on the approved model list … |
model_recording | string | What the model gateway records about each request: off (the default), digests, or content (digests plus the text, encrypted). | The model gateway, on every request it serves. | Nothing is refused. Entries appear in the audit log. |
require_snapshot_encryption | true or false | Every snapshot is encrypted as soon as it is taken, and may leave or enter the machine only encrypted. | The service at start (it needs a usable key), on every snapshot, and on push and pull. | policy "example-corp" requires snapshots to be encrypted, and this transfer would send it in plaintext; push it with --encrypt … |
snapshot_key_scope | string | The key scope incremental transfers must use: snapshot (one key per snapshot) or lineage (one per sandbox, which also permits snapshot). Empty means snapshot when encryption is required. | Incremental snapshot transfers. | policy "example-corp" requires each snapshot under its own key, so that deleting it crypto-shreds every copy; use --key-scope snapshot |
require_agent_identity | true or false | A request that says it comes from an agent must carry a token made with kosa8 agent token create. | The service, on every request marked as an agent's; and kosa8 mcp serve, which refuses to start without a token. | the org policy requires agents to identify themselves, and this request carries no agent token |
An approver
| Field | Type | Meaning |
|---|---|---|
name | string | The person's name. Required. |
email | string | What makes two keys the same person. Someone listed twice with one email counts once. |
algorithm | string | ed25519 (the default), ecdsa-p256-sha256 or ecdsa-p384-sha384. |
public_key | string | Their public key, from their kosa8 identity show. A key may be listed only once. |
roles | list of strings | Free-form roles a rule can require, such as security. |
An approval rule
| Field | Type | Meaning |
|---|---|---|
match | string | A class (escalating, destructive, contained, read-only), one action kind such as network-create, or *. |
required | whole number | How many different people must approve. Zero reads as one. The requester never counts. |
roles | list of strings | An approver must hold at least one of these to count. Empty means any approver. |
A policy is checked before it is signed. A rule nobody listed could satisfy is refused then,
with the count: approval rule for "network-create" needs 2 distinct approvers with role
security, but the profile lists only 1 who qualify — add approvers or lower required.
An approved model
| Field | Type | Meaning |
|---|---|---|
name | string | For people reading the policy. Never matched: a file renamed to an approved name is still the wrong file. |
digest | string | sha256: and 64 lower-case hex digits of the model file. Required. |
signer | string | When set, the model must also carry a signature by this public key, made with kosa8 model sign. |
A complete organisation policy
{
"name": "example-corp",
"allowed_registries": ["docker.io", "ghcr.io"],
"default_egress": "deny",
"require_sandbox": true,
"allowed_mcp": ["ghcr.io/example-corp/github-mcp:1.4.2"],
"require_fips": false,
"approvers": [
{"name": "Asha Rao", "email": "[email protected]",
"public_key": "AoNDqYXXYG2QK5C0P+qk+xFn0a6fxXQlp7dairJCqzg=", "roles": ["security"]},
{"name": "Ben Ortiz", "email": "[email protected]",
"public_key": "PjsMrSBl7Xa0U/NmpPSbq/7sZEz777lQUbrjEzCHenE=", "roles": ["security", "release-manager"]}
],
"approval_rules": [
{"match": "escalating", "required": 1},
{"match": "network-create", "required": 2, "roles": ["security"]}
],
"allow_unlisted_requesters": false,
"require_audit_forwarding": true,
"min_audit_retention_days": 400,
"labels": {
"levels": [
{"name": "UNCLASSIFIED"},
{"name": "CUI", "max_egress": "deny",
"mount_paths": ["/Users/Shared/cui"],
"destinations": ["remote:r2.example.com/cui-snapshots"]}
],
"downgrade_approvals": 2
},
"allowed_models": [
{"name": "qwen2.5-coder-7b-q4",
"digest": "sha256:9f2c1e0b7a6d5c4f3e2d1c0b9a8f7e6d5c4b3a2918070605f4e3d2c1b0a99887"}
],
"model_recording": "digests",
"require_snapshot_encryption": true,
"snapshot_key_scope": "snapshot",
"require_agent_identity": true
}
kosa8 policy init prints a much shorter starting point:
$ kosa8 policy init
{
"name": "example-corp",
"allowed_registries": [
"docker.io",
"ghcr.io"
],
"default_egress": "deny",
"require_sandbox": true
}
Part B. The MCP policy
MCP (Model Context Protocol) is how an AI client calls tools. A tool server is a program that offers tools; kosa8's gateway runs each third-party server in a sandbox and passes calls through. The MCP policy says which of those calls may happen.
How a call is decided
- The scope is the one in the agent's verified identity, or the policy's
default_scope. The caller cannot pick it. - The server must be listed in that scope by its exact name. A server nobody listed is denied.
- A tool matching
deny_toolsis denied, even if an allow pattern also matches. - With
allow_toolsset, a tool matching none of them is denied. With it empty, every remaining tool is allowed. - Argument rules then look at what the call asks for. Among the rules that match, deny beats ask beats allow.
The policy
| Field | Type | Meaning |
|---|---|---|
name | string | Required. Audit entries say which policy decided. |
default_scope | string | The scope used when the caller has none. It must be one of the scopes below. It lives in the signed document so that nobody governed by the policy can choose a wider scope for themselves. |
scopes | list of scopes | The named sets of rules: a team, a job, a risk tier. |
rules_mode | string | enforce (the default) or log. In log mode an argument rule that would deny or ask is recorded as such and the call goes ahead, so a rule set can be tried on real traffic. It softens rules only: tool lists and pins still deny. |
classifier | object | An optional local scorer for ask decisions. Left out, every ask goes to a person. |
A scope
| Field | Type | Meaning |
|---|---|---|
name | string | Required, and unique in the policy. |
description | string | Why the scope exists. Shown by kosa8 mcp-policy scopes. |
servers | list of server rules | The approved servers. A scope with none denies every server. |
A server rule
| Field | Type | Meaning |
|---|---|---|
server | string | The server's name as registered with kosa8 mcp add, matched exactly. * and ? are refused when the policy is signed. |
allow_tools | list of strings | Tool-name patterns that may be called. * stands for any run of characters. Empty means every tool the server offers. |
deny_tools | list of strings | Tool-name patterns that may never be called. Deny beats allow. |
rules | list of argument rules | Decide a call by its arguments, once the tool lists have admitted the tool. |
pins | list of pins | The exact tool definitions the organisation reviewed. When any are listed for a server, a tool that is not listed, or whose definition has changed, is withheld, and nothing can be approved locally. |
read_* approves a family of tools without re-signing the policy each time a server
adds one. A wildcard on the server name would let one *, or a plausible typo, approve
servers nobody reviewed. Tool patterns are case-sensitive: read_file does not approve
Read_File.
An argument rule
| Field | Type | Meaning |
|---|---|---|
id | string | Names the rule in refusals and audit entries. Optional, but unique when set. |
tools | list of strings | Tool-name patterns the rule covers. Required; use * for every tool. |
when | list of conditions | All must hold. Empty means every call of the matched tools. At most 16. |
effect | string | allow, deny or ask. Ask holds the call until a person approves that exact call. |
reason | string | Shown to whoever the rule stopped. Write it as the next step. |
A condition
| Field | Type | Meaning |
|---|---|---|
arg | string | Which argument, as a JSON Pointer: /repo, /files/0/path. Empty is the whole arguments object. |
op | string | The test. One of exists, absent, eq, ne, in, not_in, prefix, suffix, contains, glob, matches, their not_ forms (not_prefix, not_suffix, not_contains, not_glob, not_matches), lt, le, gt, ge, len_le, len_ge, type. |
value | depends on op | What to compare with. Comparison is typed: the string "1" never equals the number 1. |
An argument of a type the test cannot handle counts as matching in a deny or ask rule and as not matching in an allow rule, so odd input can only make the outcome stricter. Each operator, with worked examples, is in Rules for tool calls.
A pin
| Field | Type | Meaning |
|---|---|---|
tool | string | One exact tool name. No wildcards. Each tool may be pinned once. |
digest | string | sha256: and 64 hex digits, copied from kosa8 mcp pins show SERVER. It covers the tool's name, description and schemas. |
The classifier
| Field | Type | Meaning |
|---|---|---|
scorer | string | local-model, the only kind. A policy naming any other is refused instead of running without one. |
endpoint | string | The address of a model served on this machine. It must be http:// to localhost or a loopback address: tool arguments do not leave the machine. |
model | string | Which model to ask, by the name the local model server knows it. Required: a classifier without one is refused when the policy is signed. |
threshold | number | How confident an "allow" answer must be to act on: above 0.5 and below 1. There is no default. |
timeout_ms | whole number | The longest one question may take, up to 30000. Zero means the scorer's default of two seconds. |
The scorer is consulted only for a call a rule answered ask. It can spare a person
the question; it has no answer that means deny, and anything short of a confident allow leaves the
question with the person.
kosa8's own tools: the server named kosa8
The name kosa8 is reserved. In a policy it stands for kosa8's own tools, such as
sandbox_create and container_run. A scope that lists it governs those
tools with the same lists and rules as any other server. A scope that does not list it leaves them
as they were, because a policy written before this existed said nothing about them.
kosa8 mcp add refuses to register a third-party server under that name.
A complete MCP policy
{
"name": "example-corp",
"default_scope": "engineering",
"scopes": [
{
"name": "engineering",
"description": "application engineers",
"servers": [
{
"server": "github",
"allow_tools": ["list_*", "get_*", "search_*", "create_issue", "merge_*"],
"deny_tools": ["*_delete", "delete_*"],
"rules": [
{"id": "only-acme", "tools": ["create_issue"], "effect": "deny",
"when": [{"arg": "/repo", "op": "not_glob", "value": "acme/*"}],
"reason": "issues go in acme repositories"},
{"id": "merge-needs-a-person", "tools": ["merge_*"], "effect": "ask",
"reason": "merging changes what ships"}
]
},
{
"server": "kosa8",
"deny_tools": ["sandbox_delete", "snapshot_rm"],
"rules": [
{"id": "open-network-needs-a-person", "tools": ["sandbox_create"], "effect": "ask",
"when": [{"arg": "/egress", "op": "eq", "value": "any"}],
"reason": "an open network is reviewed"}
]
}
]
},
{
"name": "contractors",
"description": "read-only access to the docs server",
"servers": [
{
"server": "docs",
"allow_tools": ["search", "read_page"],
"pins": [
{"tool": "search",
"digest": "sha256:3b1f0c9a7e5d2b4861f0a9c8e7d6b5a4938271605f4e3d2c1b0a9f8e7d6c5b4a"},
{"tool": "read_page",
"digest": "sha256:7b1f0c9a7e5d2b4861f0a9c8e7d6b5a4938271605f4e3d2c1b0a9f8e7d6c5b4a"}
]
}
]
}
],
"rules_mode": "enforce",
"classifier": {
"scorer": "local-model",
"endpoint": "http://127.0.0.1:7434",
"model": "qwen",
"threshold": 0.95,
"timeout_ms": 2000
}
}
What that policy answers, for an agent in the default scope:
| Call | Answer | Why |
|---|---|---|
github / list_issues | allow | allow_tools "list_*" |
github / create_issue with repo: acme/api | allow | No rule matched. |
github / create_issue with repo: other/api, or no repo | deny | Rule only-acme: issues go in acme repositories |
github / merge_pull_request | ask | Rule merge-needs-a-person |
github / delete_repo | deny | deny_tools "delete_*" |
github / fork_repo | deny | tool "fork_repo" is not in the allowlist for server "github" in scope "engineering" … |
slack / anything | deny | MCP server "slack" is not approved for scope "engineering"; approved servers: github, kosa8 — ask your admin to add it to the org policy |
kosa8 / sandbox_delete | deny | deny_tools "sandbox_delete" |
kosa8 / sandbox_create with egress: any | ask | Rule open-network-needs-a-person |
An agent in contractors may call the two pinned docs tools and no other
server. That scope does not list kosa8, so kosa8's own tools are not governed for it.
Part C. Signing, pinning, installing
- The organisation makes one signing key and keeps the private half.
- It signs each policy with it. The signed file carries the rules, the public key and the signature.
- On each machine the public half is pinned, once.
- kosa8 verifies the installed policy against the pinned key before it uses it: the organisation policy each time a rule is consulted, the MCP policy each time a gateway starts.
- A policy that verifies is enforced.
- One that was edited, removed or signed by another key is not ignored: kosa8 refuses the action and says why.
Make the key
$ kosa8 policy keygen -o org.key
public: kQ+JPoaWWs9JC1Nic/sRli1nJOAgQFAmmm+xtD0oEeo=
private key written to org.key (keep it secret)
-o writes the private key to that file and the public key to
org.key.pub. Without it both are printed.
Write and sign both documents
$ kosa8 policy init > policy.json # a template to edit $ kosa8 policy sign org.key policy.json > policy.signed.json $ kosa8 mcp-policy init > mcp.json $ kosa8 mcp-policy sign org.key mcp.json > mcp.signed.json
Both sign commands check the document first and refuse one that could never
work, with refusing to sign: and the reason. kosa8 policy init also
takes --require-fips and --require-snapshot-encryption.
Pin the key, then install
$ kosa8 policy trust org.key.pub trusted policy signer set $ kosa8 policy install policy.signed.json installed and activated policy "example-corp" $ kosa8 mcp-policy install mcp.signed.json installed MCP policy "example-corp" (2 scope(s)) servers not listed in a scope are now denied
trust takes the key itself or a file containing it. Pin before you install.
kosa8 mcp-policy install refuses otherwise:
no trusted policy signer is pinned, so any key would be accepted here —
pin your org's public key first with `kosa8 policy trust <PUBLIC_KEY>`
Check what is in force
$ kosa8 policy show # the organisation policy and its signer $ kosa8 mcp-policy show # scopes, servers, allow and deny lists $ kosa8 mcp-policy scopes # the scopes, with the default marked $ kosa8 mcp-policy check github list_issues $ kosa8 mcp-policy check --scope contractors github list_issues $ kosa8 mcp-policy check --args '{"repo":"other/api"}' github create_issue $ kosa8 mcp-policy events --denied # the decisions, for a SIEM
| Command | What it does |
|---|---|
kosa8 policy keygen | Makes a signing key pair. -o FILE writes it to files. |
kosa8 policy init | Prints a template organisation policy. |
kosa8 policy sign KEYFILE PROFILE.json | Prints the signed policy. |
kosa8 policy trust PUBKEY_OR_FILE | Pins the signer on this machine, for both documents and for kits. |
kosa8 policy install SIGNED.json | Verifies against the pinned key and makes the policy active. |
kosa8 policy show | Shows the active organisation policy. With none: no policy installed (everything allowed). |
kosa8 mcp-policy init | Prints a template MCP policy with two scopes. |
kosa8 mcp-policy sign KEYFILE POLICY.json | Prints the signed MCP policy. Use the same key. |
kosa8 mcp-policy install SIGNED.json | Verifies and activates it. Refuses when no key is pinned. |
kosa8 mcp-policy show | Shows the active MCP policy, or why it cannot be used. |
kosa8 mcp-policy scopes | Lists the scopes the policy defines. |
kosa8 mcp-policy check SERVER TOOL | Asks whether a scope may call a tool, and records the decision. --scope tries another scope; --actor says who is asking. Without --args only the tool lists are consulted; with --args JSON the server's argument rules are applied to those arguments as the gateway would. Exits non-zero on a denial. |
kosa8 mcp-policy events | Exports the decisions. --format jsonl, cef or table; --denied; -n for the most recent few. |
Making a key, signing, pinning and installing need the Governance subscription or a trial. Enforcing an installed policy does not.
Part D. When a policy is missing or wrong
Both documents fail closed, with one deliberate difference in the second row.
| Situation | Organisation policy | MCP policy |
|---|---|---|
| No key pinned, no policy | nothing restricted The machine is unconfigured. | nothing restricted Every registered server is reachable. |
| Key pinned, policy file missing | refuses a trusted policy signer is pinned but no policy is installed (was it deleted?); re-install with `kosa8 policy install` | denies every third-party server a trusted policy signer is pinned but no MCP policy is installed (was it deleted?); re-install it with `kosa8 mcp-policy install`kosa8's own tools still work |
| Policy file edited | refuses installed policy failed verification: signature does not match profile (tampered or wrong key) | denies everything, kosa8's own tools included. the installed MCP policy failed verification: signature does not match MCP policy (tampered or wrong key); re-install a policy signed by your org with `kosa8 mcp-policy install` |
| Policy signed by another key | refuses installed policy failed verification: profile is signed by an untrusted key | denies everything … failed verification: MCP policy is signed by an untrusted key … |
The organisation policy is read from disk each time a rule is consulted, so a change takes
effect at once. A tool gateway (kosa8 mcp serve) reads the MCP policy once, when it
starts, so that the rules do not change under a running agent: a newly installed, edited or deleted
MCP policy applies from the next time the gateway starts.
"Refuses" for the organisation policy means each place that consults it stops, with its own words in front of the reason above:
| Where | What it says |
|---|---|
| The service, at start | kosa8d: org policy present but failed verification, refusing to start: … |
| Fetching an image | org policy present but failed verification, refusing to pull: … |
| Running on the shared engine | org policy present but failed verification, refusing to run: … |
| Creating a sandbox | org policy present but failed verification, refusing to proceed: … |
| Starting tool servers | org policy present but failed verification, refusing to start MCP servers: … |
| An agent with no token | org policy present but failed verification, so an unidentified agent cannot be served: … |
An organisation can pin a key for its organisation policy and never write an MCP policy.
Third-party servers are denied there. Treating that absence as a denial of
sandbox_create and the rest would take kosa8 away from every such organisation. The
cost is stated plainly: deleting mcp-policy.json lifts the rules it held for kosa8's
own tools. A policy that is present and does not verify denies both. For actions that must not be
self-authorised, use signed approvals (approvers above), which do not live in that
file.
Part E. Checking a machine
kosa8 doctor --compliance checks this machine against the hardening baseline:
whether a policy is installed and what it requires, among other controls. It reports; the fields
above are what enforce. See the doctor reference page.
What this does not do
- Enforcement is on the machine being governed. kosa8 checks the policy with code and
files that belong to the user it constrains. Someone who can delete both the policy and the pinned
key,
~/.kosa8/policy-trust.pub, returns the machine to "no rules". Provision that file out of band and write-protect it, for example owned by an administrator account. - With no key pinned, a signature proves little.
kosa8 policy installthen accepts a policy signed by any key, including one a developer made. Pin first. - No central distribution. There is no admin console, and no SSO, SAML or SCIM. A policy is a signed file you deliver to each machine yourself, and a new version reaches a machine when someone installs it there.
default_egressis a default. A sandbox created with an explicit network rule keeps that rule. To require review of a wider one, add an approval rule for escalating actions.- Tool rules see names and arguments, not meaning. A deny rule on
/pathdoes nothing about a server that also accepts/file, and nothing here reads the text a tool returns. - Deleting the MCP policy lifts its rules on kosa8's own tools, as described above.