0013 — Policy as data¶
- Status: Implemented (step 11.4c, unreleased, for 0.2.0). Shipped behaviour: SPEC §7, keys and presets: policy reference.
- Size: ~2 agent-days (model, schema and layering ~0.5; guard and secret checks on the policy ~0.5; Settings → Policy, proposals and MCP ~0.5; docs and tests ~0.5).
Goal¶
The restrictions of 0006 (levels, guard, secret checks) are not
hard-coded: a policy defines them. A cautious team tightens them; a person alone on a trusted
machine gives agents full access and keeps harmless test passwords next to the test stand.
Without a policy file relaybay behaves exactly as before (the default preset).
Layers¶
A policy is a JSON document. The effective policy of an environment is merged from:
- the built-in preset named by the global layer (
default,strict,trusted); - the legacy
guard_modeand[permission_defaults]ofconfig.toml(config), if set; - the global file,
<data dir>/policy.json(settingpolicy_file); - the project layer, stored in the database;
- the environment layer, stored in the database.
A lower layer overrides the upper one: objects merge key by key, scalars and lists replace. A
list key may instead be written name+ (add items: strings by value, patterns by id, an item
with an existing id replaces it) or name- (remove items by value or id). The environment's own
level and per-capability overrides (Stands & access) still win over every policy layer.
Every value of the effective policy knows which layer set it; the board and policy_get show it.
Schema¶
src/relaybay/policy/schema.json (JSON Schema 2020-12, published as a reference page):
preset(global layer only);levels.<kind>.<default|capability>:none/read/ask/full;guard.mode:off/log/warn/block(log: journal only, no messages to anyone);guard.disable: built-in mutating rules by stable id (ssh-remote-change,git-push, …);guard.patterns: custom mutating patterns{id, capability?, command?, change, description}(regular expressions, at most 200 characters, no back-references or nested quantifiers, timed on hostile input when loaded);secrets.disable: built-in secret detectors by id;secrets.allow: patterns of values that pass;secrets.test_secrets.{local,test}: whether access fields may hold{"value": "...", "test": true}; the schema has nolivekey, so no layer can allow it;secrets.expose_test_secrets_to_agents: whetherenvironments_getshows those values.
Validation errors name the layer, the path and the message. A broken global file stops the start
like a broken config.toml; a broken file found on a later reload, or a broken project or
environment layer, is logged and ignored (the last good global file stays) and shown on the
Policy page.
Presets¶
| Preset | Levels | Guard | Secrets |
|---|---|---|---|
default |
local and test full, live read |
warn, all rules |
all detectors, no test secrets |
strict |
local full, test ask, live none |
block, all rules |
all detectors, no test secrets |
trusted |
full everywhere |
log |
all detectors, test secrets on local and test, not shown to agents |
Who changes it¶
- A person: edits
policy.json(the server notices the change on the next read, no restart) or uses Settings → Policy (board session and CSRF only, never the Bearer token): preset picker, a JSON editor per layer with a validation preview, and a confirmation when the change widens agents' access on live (a level abovereadthat rises, or a weaker guard mode). - An agent:
policy_propose(scope, patch, reason)records a proposal (Inbox, Settings → Policy), accepted or rejected by a person like environment proposals.policy_get(project?, environment?)returns the effective policy with the source layer of every value.
UI¶
Settings → Policy: effective policy for the chosen project and environment with the source of each value, the raw JSON of every layer, errors, the preset picker, editors and pending proposals. Stands & access shows each environment's effective level and where it comes from.
Risks¶
| Risk | Mitigation |
|---|---|
| A custom regex hangs the hook | Static limits (length, no back-references, no nested or alternation quantifiers, at most two unbounded quantifiers), a probe on load, and the regex engine with a hard per-match timeout on at most 8 KB at run time (a call that times out fails closed) |
| A layer staged on a test environment skips the live confirmation | Becoming live compares the environment's policy with the project's and asks again |
trusted hides real mistakes |
log still writes the live journal; the preset is named on the Policy page and in environments_get |
| Test secrets leak to agents or live | Only on local and test (schema and service), masked in the UI, not sent to agents unless the policy says so, masked in the journal, never accepted in proposals |
| A broken file locks the owner out | Start refuses with path and message; later breakage keeps the last good policy |