cli.sgit.ai / Articles / Policies the vault enforces: from a rule agents keep to a folder only one key can write

Policies the vault enforces: from a rule agents keep to a folder only one key can write

By Dinis Cruz · 2026-10-16 · published · site v0.2.0

Summary: On ruv.sgit the vault is the record of a policy, not its enforcer. RiskMandate writes the mandate, the grant, the gap and the barrier; ruv.sgit compiles those rows into Ruflo's policy engine, which decides and writes receipts; the Cedar policy vault keeps the rules, their history, their signed releases and the evidence. What stops an action is the harness, a hook or an engine, never the vault. I asked whether sgit itself could enforce some of it. It already refuses things, and the rules travel with the vault: a scoped clone refuses a write outside its folders, a commit refuses secret-shaped files, signatures-required makes every client refuse an unsigned commit on pull, the named branch refuses a rewind, and a read-only clone cannot push. All of that is client-side and fails closed, and every honest client enforces it. That is the hook. A policy row that says key K may write path/ is one more rule of the same kind: refused on commit by the writer's own client and on pull by every other client, so a violating push stops the branch for the whole swarm until it is reverted, while the server stays blind. Labelled with RiskMandate's barrier kinds there are four levels: a convention is an expectation, a clone scope is a setting the agent can widen, a policy row is a boundary for every honest client and evidence against a dishonest one, and a server that verifies signed writes is a boundary against everyone. My rule, that once a folder is bound to a key it cannot be rebound and you start a new folder, is the right one because it only moves one way. Levels two and three are not built.

policy enforcement riskmandate ruv-sgit agents signatures folders barriers governance

Four levels of enforcement, labelled with RiskMandate's barrier kinds. From the bottom: a convention, which is an expectation; a clone scope, a setting the agent itself can widen; a policy row binding a folder to a key, a boundary for every honest client and evidence against a dishonest one; and a server that verifies signed writes, a boundary against everyone. The first two exist, the third and fourth are proposed.
Four levels of enforcement, labelled with RiskMandate's barrier kinds. From the bottom: a convention, which is an expectation; a clone scope, a setting the agent itself can widen; a policy row binding a folder to a key, a boundary for every honest client and evidence against a dishonest one; and a server that verifies signed writes, a boundary against everyone. The first two exist, the third and fourth are proposed.

In short

Where enforcement lives today

Three published systems, each with the vault in a different role:

SystemWhat the policy isWho decidesWhat the vault does
RiskMandate (riskmandate.ai)An Agent Behaviour Policy for one agent in one deployment: the mandate (elicited), the grant (measured), the delta (derived), and for each capability a barrier of one of four kinds: none, expectation, setting, boundary. "A control bounds a grant only if it is enforced by something the grant does not include."Nobody at runtime by design: "never in the request path, read-only by design, no enforcement point"Holds the policy, its versions and the evidence (SG/Vault: "PKI, provenance, no database")
ruv.sgit (POC1)The same ABP, six roles, 36 rows (abp.json: capability, reach, mandate, why, barrier, instances)Ruflo's policy engine in enforce mode: rules compiled from the rows, priority deny 100 / approval 90 / allow 10; 11 actions, 6 stopped, two hash-chained receipt ledgers validAn ephemeral vault per run holds the memory, the receipts and the output, one branch per worker, merged by the host
The Cedar policy vault (newsroom, 14 Oct)The ABP in English, its Cedar translation, the schema and the tests, in one versioned unit; every policy carries @id back to its ABP rowA Cedar enforcement point in a pre-tool hook; deny on errorDistribution by read key, a release gate before push and after load, signed tags, an append lane for every decision

Read down the last column: the vault is where the policy is kept, released, distributed and evidenced. It never says no. The harness says no, and every hooks article on the newsroom records the vendors' own caveat about that: "hooks are not a security boundary". The memo's question is whether the vault can say no too, for the one class of action a vault can see, which is a change to its own files.

What sgit already refuses

The CLI is not a passive store. These refusals exist today, every one fails closed, and every one is honoured by every client because the rule lives in the vault or in the clone, not in a prompt:

RuleWhere it is keptWho refusesSince
A write outside a scoped clone's folders ("refused with the path named")the clone's scope (scope_paths)the writer's own client, on commit and write0.18.0
A secret-shaped file (.env, .netrc, private keys)the client's rules; --allow-secret-files to override, recordedthe writer's client, on commit0.20
An unsigned commit when the vault says signatures-requiredthe branch index (a feature, with signed-since-<hex>)every client, on pull: "refuses the first incoming commit that does not verify, by name, before merging"0.19.0
A rewind of the named branchlast_remote_head in each cloneevery client, on status and pull; --accept-rewind to take it deliberately0.19.0
A push from a read-only clonethe server (write-key hash) and the client (clone_mode)bothalways
A whole-vault command on a partial clone (fsck, publish, vault move)the clonethe client0.18.0
A branch index entry lost by a web overwritethe index as a shared documentevery client repairs it on pull0.19.0

The pattern underneath is the one to reuse: a rule in the branch index is enforced by every client that reads the vault, so a change that breaks it cannot propagate. A rogue client can still push (the server compares a shared hash), but no honest client will pull what it pushed, status names the offending commit, and the branch is stuck for the whole team until someone reverts it. That is not a boundary against the rogue; it is a boundary for everyone else and evidence against the rogue, which is exactly what signatures-required already is.

Four levels

The memo asks for "multiple levels of security" and for some files to be "stronger than others". With RiskMandate's four barrier kinds as the labels:

LevelWhat it isBarrier kindExists
0. ConventionThe ABP says "the coder writes under src/", and the agent keeps itexpectationyes: today's swarms
1. Clone scopeThe agent's clone holds only src/ (--path src), so a write elsewhere is refused by its own clientsetting: the clone can widen itself (sgit fetch <folder>), and docs/limitations says so: "a partial clone is not an access boundary"yes
2. A policy row in the vaultA committed, signed policy says which writer key may touch which paths; the writer's client refuses on commit; every other client refuses on pullboundary for every honest client; detection and evidence against a dishonest oneno
3. A folder bound to a keyLevel 2 made monotonic: a folder, once claimed by a key, can never be claimed by another; the only way to change hands is a new foldersame as 2, with no way to quietly rebindno
4. Signed writes verified by the serverRFC 0001 Design 1: the server accepts a head only if signed by a key in the writer set; with level 3's claims in the signed index, the server can refuse a head that changes a folder its signer does not holdboundary against everyone, including a rogue clientno (designed)

Level 1 is where ruv.sgit is now: a branch per worker, merged by the host. Level 2 is the one the memo is about, and it is cheap, because it is the same mechanism as signatures-required with a path in the rule.

The policy row, and who may write it

Reuse the ABP's vocabulary so the rows compile both ways (the Cedar article already carries @id edges from every Cedar policy back to an ABP row). A vault policy is a small signed file whose rows look like the ABP's, with the principal a key fingerprint and the resource a path:

policy_version: 1
owner: <fingerprint of the vault's policy key>
valid_until: 2026-11-01
rows:
  - principal: sha256:3f9a…   # the coder's branch signing key
    capability: write.path
    resource: src/**
    mandate: yes
  - principal: sha256:3f9a…
    capability: write.path
    resource: docs/**
    mandate: no
    barrier: refused on commit; refused on pull by every client
  - principal: "*"
    capability: write.path
    resource: policy/**
    mandate: no          # only the owner key may change the policy

Three rules make it hold:

  1. The policy is pinned by fingerprint and signed by the owner key. RFC 0001 says it for sealed files: "a committed policy file … pins each recipient's public key by fingerprint, so a change at a key directory cannot silently add someone". A policy change not signed by the owner is itself a violation, so the policy cannot be rewritten by the agents it governs. This is the "a rule the agent can rewrite is a setting" test from Policy as code needs a policy first, passed.
  2. Enforcement is on commit and on pull. The writer's client checks the commit's author key against the rows before writing the commit (the same place the scope check runs). Every other client checks each incoming commit's author key against the paths it changed, before merging, the way signatures-required checks the signature. The check is cheap: a commit's changed paths come from its tree diff, which history log --stat already computes.
  3. Claims are monotonic (level 3). A row that binds a folder to a key can be added, never removed or reassigned; a folder's owner changes by creating a new folder with a new row. The named branch already works this way (it only moves forward) and so does signed-since. For a swarm whose vault lives for one run this costs nothing, and it removes the question "who changed the owner of src/ on Tuesday" entirely: nobody can.

The keys already exist. Every clone has a branch signing key (ECDSA P-256), its public half stored in bare/keys/, its id on every commit it makes. A worker's "key" in the policy is the key its clone signs with, which ruv.sgit's host knows the moment it opens the worker's branch.

What it looks like in a run

Take ruv.sgit's run r11: one worker, one branch, 36 files delivered, writes 1.6 s, merge 1.5 s, eleven sgit operations in the ledger. With a level-2 policy:

  1. The host opens the run's vault, generates the workers' clones, reads each clone's key id, and commits the policy: workers/editor/** to the editor's key, brief/** to the host's, policy/** to the owner's. One commit, signed by the owner key.
  2. The editor works in its clone. A write to brief/ is refused by its own client with the row named, before any commit exists. That is level 1's refusal with a reason the ABP can quote.
  3. The host pulls the editor's branch. Each commit's author key is checked against the paths it touched; a commit that touched brief/ is refused by name, and the pull stops. The refusal is appended to the run's evidence lane with the commit id, the key and the row, which is the receipt Ruflo's engine writes for an action, written by the vault for a change.
  4. sgit check verify at close classifies every commit, and the policy check can run over the whole history the same way, so the ledger can say "every commit in this run was within its row", or name the ones that were not.

Nothing in that list needs the server to know anything.

Who enforces what, said plainly

What this does not claim

Questions for the thread (each a code change)

  1. Policy file and check. policy/vault-policy.json (or a branch-index field) with the rows above; check on commit/write (writer's key) and on pull (each incoming commit's author_key_id against its changed paths). Refusal messages name the row.
  2. Monotonic claims. A write.path row with mandate: yes can be added, never removed; a policy commit that removes one is refused like a rewind, with --accept-reclaim for the owner key only, if at all.
  3. Owner key. Which key signs the policy: the vault's own signing key from RFC 0001's first step (derived along a third path from the passphrase), or a pki key named at init.
  4. Evidence. Append each refusal (commit id, key, row, path) to a lane named in the policy, so the run's ledger can show it beside Ruflo's receipts.
  5. ABP compile. abp → vault-policy and back, with @id edges, in the same place ruv.sgit's compile.mjs maps rows to Ruflo rules.

If you write agent policies and would like one enforced by the vault the agents share, write to agent@riskmandate.ai.

Where this comes from

Draft for the newsroom, 16 Oct 2026, from a voice memo of mine on ruv.sgit. Facts from riskmandate.ai, ruv.sgit.ai (POC1 and the ledger), the newsroom's policy articles, the sgit.ai pages it links and the CLI at dev 37218af. No code is changed; the changes that would be needed are listed as questions.

Written up by the sgit CLI team's agent (a Claude Code session) as a draft for the newsroom; the editorial responsibility is mine. The draft is team/humans/dinis_cruz/claude-code-web/10/16/article-draft__policies-the-vault-enforces.md in SGit-AI__CLI at 5473571, on the branch claude/charming-babbage-5hzcxq. Published on cli.sgit.ai on 20 October 2026 in the newsroom's format: the front matter, the summary, the lead figure, the anchors, this section and the line above it were added; the text is as drafted. The research topic it belongs to: /research/topics/keys.html.

This page as markdown · its source, admin/content/articles/policies-the-vault-enforces.md