Skip to content
Chock
Approvals (5 of 23)

Approvals

An action a rule answers ask is put to a person before it happens, wherever there is a moment at which a person can be asked. The broker knows eight actions: git.commit, git.push, git.branch.delete, net.fetch, nix.build, file.write, workspace.apply and model.select. It answers one more, policy.widen, which is a session asking to be let out of a promise it made to itself. The policy table in chock.zon says which of them is allowed, denied or asked.

Two of those are read before the work they govern, at a moment when nobody is waiting to answer, so ask is a refusal for nix.build and model.select. net.fetch is asked live for the host the agent named, and stays a refusal for every redirect hop after it: see actions.md. workspace.apply is asked at the end of a session, and again during one when the agent asks for it with request_action. policy.widen is asked during one.

An ordinary tool call is gated as well. gateToolCall asks the same arbiter about every one, so a project rule of ask on call.write_file, exec.*, or any other action Tool.actionInto names reaches a person mid session. A filtered tool call’s own net.connect.* question reaches the same person: a host no rule names answers ask, and that ask is a live question. web_search is the one ordinary tool call that ships as ask rather than allow, under the name web.search, and it is asked while the agent waits. search.md has the engine it puts the query to. threat-model.md describes the network descriptor every foreground tool call holds, whether or not any host has been named.

The one every project meets

The agent works in a throwaway copy of the project, so its commit has to be carried back into your repository before you can read it. That carry is workspace.apply, and it is asked at the end, when the session is over:

chock: this needs your approval before it can happen.

  action   workspace.apply
  asked by main
  summary  land 4 objects of the session in /home/you/site, set
           refs/chock/01K2... to a1b2c3d, and merge it into refs/heads/main
  reason   the session made a commit, and the workspace it is in is about to
           be removed

  what it changes

  a1b2c3 add a test for the parser

Allow this once, or for the rest of the session? [y/N/a]

y or yes, in any case, answers yes for this apply alone. a or always answers yes and remembers this exact action for the rest of the session, so a later workspace.apply in the same session does not ask again. Anything else, a bare Enter included, is a no, and a no leaves your repository exactly as it was. The work is still in the session’s workspace either way, and the line printed after a refusal says where. chock approve, answering over a socket instead of a keyboard, is never offered the session letter: only the terminal that holds the session’s own lock can keep that promise.

A question that comes from a part of Chock rather than from the agent’s own tool call carries a source line naming it: the sandbox, a Nix build, a Nix flake input, git, an MCP server or a plugin. An action name alone does not say who wanted it, and the answer can turn on that. A host a Nix build wants is not a host your agent wants.

Ctrl-C at the prompt ends the run with the question unanswered, which is a refusal, and keeps the workspace.

Where the work lands, and how to change it

By default an approved apply merges the work into the branch you have checked out, and your working tree is updated to the result. The work is parked at refs/chock/<session> first, always, so the ref is there whatever else happens and git merge refs/chock/... is always available to you.

A project that wants another shape says so in chock.zon:

.{
    .apply = .{ .mode = .rebase },
}

Three modes and one question:

mode what an approved apply does
merge the default. Parks the work, then merges it into the branch you have checked out.
rebase parks the work, then replays it on top of the branch you have checked out.
squash parks the work, then puts all of it on that branch as one commit.
ask Chock asks you which of the three, at the moment of the apply.

There is no mode that means “move nothing”. There are two ways to say that: answer n to the apply, or write .{ .action = "workspace.integrate", .decision = .deny } on the policy table.

The prompt says which one you are approving. The summary line names the landing and the branch, and the detail has a paragraph of its own about your branch: which branch, where it is, where it moves to, and what happens to your working tree. An apply that moves no branch says so in the same place, and says why. The questions do not read alike, because the same y does not mean the same thing.

Every mode parks the work at the ref first. That is why merge is safe as the default: nothing is ever lost, and the fallback is one git reset --hard away.

Chock never leaves your repository in the middle of a merge. The merge, the rebase and the squash are all built inside the session’s own object store, with no index and no working tree involved, and the only thing that ever runs in your repository is a fast forward onto a clean tree. There is nothing to abort, because nothing was started.

So Chock decides before it asks you whether the integration is possible at all. When it is not, the prompt says so and the work waits at the ref instead. It refuses for these reasons, and each one is named in the prompt and in the log:

  • your working tree holds changes that are not committed, or files that are not tracked
  • the work and your branch change the same lines, so it would stop on a conflict
  • no branch is checked out
  • a merge, a rebase, a cherry pick or a revert is unfinished there
  • your branch moved between the question and your answer

A refusal never loses the work and never refuses the apply. The objects and the ref land exactly as they always do, which is the behaviour you already have a git merge for. Chock checks the same facts again immediately before it moves your branch, so a working tree you dirtied while reading the question parks the work rather than integrating into it.

ask needs somebody at the keyboard. A subagent, a session the daemon started, a chock run behind a pipe and a session with the full screen interface up all have nobody to ask, and all of them keep the work at the ref. So does an answer that is not one of the three words.

chock run prints what happened either way, and says how to put your branch back:

chock run: 21 objects and the ref refs/chock/01M17... were applied to /home/you/site.
chock run: your branch refs/heads/main moved from a1b2c3d to 9f8e7d6 (merge), and
your working tree is there now.
chock run: put it back with `git reset --hard a1b2c3d...`.

The session log carries the same fact as a workspace.integrate record: the mode, the policy answer that permitted it, the branch, where it moved from and to, and, when no branch moved, why.

An organisation can close this road for every project at once with one policy rule.

The agent can ask for the same thing

request_action is the one act an agent may ask for by name, and it takes workspace.apply and nothing else. An agent that believes it has finished calls it, and you get the question above, with the agent’s own reason in it, without waiting for the session to end. Everything else is identical: the same table decides, the same diff is shown, and the work lands on the same refs/chock/<session> ref. An agent gains nothing by asking, and it cannot choose how the work lands. The mode comes from chock.zon and from the policy row above it, both read outside the sandbox, and the one thing an agent fills in for an apply is its reason. There is no field of the request a mode could arrive in, and the build fails if one is ever added.

Two answers never reach you at all. An agent that asks for any other action is refused by name, and an agent that asks having made no commit is told how many files it has left uncommitted, because there is nothing to put in front of you.

How a question reaches a person

The session log is the truth of a session, and the loop holds that log’s exclusive lock from the first turn to the last. So an answer cannot be appended by another process, and for a long time that meant no question could be asked while a session ran at all. The way through is the seam the broker already has: the wait for an answer happens inside the process that holds the lock, and the answer is appended through the same handle the question was written with. There is no second lock and no second open of the log.

Three things can answer, and a session may have more than one of them:

  • The terminal. An interactive chock run prompts, as above.
  • A unix socket beside the log. chock approve <session id> attaches to a running session and answers what it asks. The socket serves the user that started the session and refuses every other peer, by the credential the kernel puts on the connection.
  • The display. Bare chock brings up a full screen interface, which reads the log and writes the answer the same way the terminal path does.
chock approve                 # the newest session of this project
chock approve <session id>

A session only asks while it is running. Attaching to one that has ended says so and exits.

Nobody at all is a refusal

A session with nobody at the keyboard and nobody attached is refused instead of asked. A subagent and a session the daemon started are both spawned with no terminal, and a chock run whose standard input is a pipe has none either. With no client attached to its socket, such a session gets no waiting period at all: the question is refused at once rather than held open for five minutes. A hang would be worse than any of the available answers, because the session lock is held while it hangs.

An approval nobody answers is a refusal, everywhere.

The way to say yes without a person there is a rule in chock.zon:

.{
    .policy = .{
        .rules = .{
            .{ .action = "workspace.apply", .decision = .allow },
        },
    },
}

.decision = .agent_review has a reviewer agent decide instead, and .decision = .agent_then_human asks the reviewer first and then asks you, with the reviewer’s verdict shown above the question.

The agent cannot reach chock.zon. Chock binds it into the workspace read only, so the file that says what the agent may do is not a file the agent can edit.