Fenzi

Use

Requests and approvals

When the agent needs a host the policy does not allow, the call is blocked and a request is filed. A person reads it and decides. The agent never approves itself.

Free: from the terminalTeam: from the console

Where a request comes from

Every blocked network call becomes a draft request, with the host, port, and the program that tried. The sandbox drafts it on its own.

The agent can also file a request itself, with a reason, such as "install the project's Python dependencies". When Fenzi starts an agent that supports it, it adds a short guide to the agent's instructions: how to see what was blocked, how to ask for the narrowest access the task needs, and how to wait for the answer. fenzi run prints a guide line when it does. Claude Code, Codex, OpenCode, and Grok get the guide today.

The reason helps the reviewer. It does not change the decision rules. A request with a convincing reason still waits for a person.

What can be requested

Network access only: a host, a port, the program allowed to use it, and, for inspected hosts, the methods and paths. That is a door, and it applies to the running sandbox.

A request for a new folder is not something the agent can file. Paths are walls. Changing them means editing the policy, and the next sandbox picks it up. See Walls and doors.

Approving on Free

List what is waiting, then decide.

fenzi requests
Example output
■ 1 request from the agent  bl-web-4f2a

    7f3a91c2  crates.io:443
              /usr/bin/curl  “User asked for the newest serde version from the crates.io API.”

    1 more was drafted by the sandbox from refused connections, not asked
    for by the agent: sentry.io:443.

    fenzi requests --all  list the drafts too

The list shows what the agent asked for, with its reason. Drafts the sandbox made from blocked calls are counted under it, with their hosts. Many come from the agent's own program working in the background, not from the task. fenzi requests --all lists them as well, under their own heading, so you can approve or deny one.

Fenzi tells the two apart from the sandbox log, which marks each request the agent filed, and from the reason. A draft's reason is generated, in the form "Allow node to connect to sentry.io:443 (HTTPS)."

The id is the start of OpenShell's longer one. Any start that matches only one request works.

fenzi approve 7f3a fenzi deny 9c01 --reason "do not send errors from local runs"

An approval becomes a new policy revision for the running sandbox. The sandbox picks it up on its next poll, about ten seconds later, and the agent can retry. A denial is kept with its reason. If the agent filed the request and is still waiting, it is told the reason and can ask for something narrower.

One approval covers calls of the same kind. Fenzi adds the request's host to the running sandbox's policy, read-only for a read and the method and path for a write, and approves the other pending requests for that host that the rule covers. The agent does not ask again for the next page of the same site. On Team the approval stays exactly as the request asked, since the team's policy comes from the console.

An approval lasts as long as the sandbox. To keep a host for the project, approve with --save, which also adds it to fenzi.yaml and runs the policy check, or add it to the file yourself.

fenzi approve 7f3a --save

A request with no method or path opens every method on that host and port for the sandbox that asked, so read it as a request for full access to the host. With --save it is saved as read-only, and the command says so, because the file has no level for a host with every method open. On Team, --save is refused: the team policy is published from the console, and the message names the request's page there.

To see the change first, add --dry-run. Nothing is approved or written: it shows the diff of fenzi.yaml and the policy check of the file as it would be. If the entry should be different, copy fenzi.yaml, write the entry you want in the copy, and approve with --policy-from. The copy is checked first. A copy that fails the check is not written, and the request keeps waiting.

fenzi approve 7f3a --save --dry-run fenzi approve 7f3a --save --policy-from edited.yaml

The Mac app does the same from a request's Approve and add to fenzi.yaml: the diff, the file to edit, the check, then Approve and save.

Approving on Team

Who approves where

WhereWho can approveWhat the record shows
FreeThe person at the machine, in the terminal or the Mac appThe request and its answer, on that machine only
Team, in the consoleAn admin or a reviewerWho approved or denied, and when
Team, on the machineWhoever runs fenzi approve there, or clicks Approve in the Mac appAnswered on the machine, with no reviewer, and a drift alert

An admin or reviewer can approve a request their own agent filed. The console records who approved every request. There is no separation-of-duties rule yet.

  1. The forwarder on the developer's machine sends the request to the control plane.
  2. A reviewer opens it in the console and approves or denies it, with a reason.
  3. The forwarder applies the decision on its next pass, every five seconds while fenzi run, the Mac app, or fenzi forward is running. The sandbox then loads the rule on its next poll, so the agent can retry about fifteen seconds after the review. The connection is always from the machine to us.
  4. The control plane records who decided and when, and matches it to the sandbox's own record of the change.

Approve, or add to the policy

The console has two ways to say yes. Approve is a one-off: it opens the host for the sandbox that asked, for as long as that sandbox runs. Approve and add to policy gives the same approval, then drafts the next policy revision with the host in it, so every sandbox of the team gets it once an admin publishes.

ApproveApprove and add to policy
Who canAn admin or a reviewerAn admin
The sandbox that askedGets the host at its machine's next forwarder passThe same
Other running sandboxes of the teamNo changeGet the host at their machine's next forwarder pass, once the draft is published
New sandboxesNo changeStart with the host, once the draft is published
Policy revisionUnchangedA new signed revision when the draft is published

The draft is the policy in force with one more hosts entry, written like the entries already there, with the comments kept. The access comes from the request's rule: a rule that only reads, with GET or HEAD, is read-only, and any other method is read-write with that method and path under allow. A rule with no method or path opens every method for the sandbox that asked, but goes into the policy as read-only, and the draft says so.

If the host is already in the policy with that access or more, the console approves and says so; there is nothing to draft. If it is there with less, the draft widens that entry instead of adding a second one. A read-only entry that needs a write becomes read-write and keeps its reads as GET, HEAD, and OPTIONS on every path.

The draft opens on the Policy page, with the change shown line by line. Check it, edit it if you need to, then publish it or discard it. Discarding keeps the approval. If someone publishes another revision first, the console says so and makes the draft again on the new one when you ask. See Publish and revisions.

When the team has a boundary, the console refuses to approve a request whose host and port have no rule in it, with a message that names the destination. The boundary is compiled for the sandbox's agent, so it also covers the agent's model API and the hosts the agent needs to start. Only the host and port are compared, not methods or paths. The machine makes the same check before it applies an approval, and fenzi approve makes it too. To approve such a request, an admin adds the host to the boundary first. A denial is never refused. The console learns which agent runs in a sandbox from the machine's last report, so a request from a sandbox the machine has not reported waits until it does.

If the control plane is unreachable, the request waits and the call stays blocked.

Answered on the machine

Someone can still answer a request on the machine, with fenzi approve or fenzi deny or in the Mac app. The console does not leave it pending. On each pass the forwarder sends every request still pending on each sandbox, an empty list too. A request the console listed that is no longer there moves to Reviewed as Answered on the machine, with the time the console learned it and no reviewer.

It reads Approved on the machine once a connection is allowed under the rule the request added, or the sandbox logs its approval. Without either, the console does not know whether it was approved or denied. A draft that left the list because the agent filed its own request for the same host and program is shown as replaced by it.

Each answer made on the machine is also a drift alert, because the team policy says a person approves in the console. See Drift.

What the reviewer sees

FieldExample
Who askedTwo lists: requests the agent filed itself, and drafts the sandbox made from a refused connection.
Machine and sandboxdana-mbp, bl-web-4f2a
Policy revisionr14, the revision the machine ran when it sent the request
Destinationraw.githubusercontent.com:443
Programcurl, with the full path /usr/bin/curl on hover
Reason from the agentfetch the schema file from the README
ProverThe runtime's own check of the proposed rule, shown as written.
Recent deniesThe last calls this sandbox tried that were blocked.

The reviewer does not see the agent's conversation, the code, or the terminal. Those stay on the developer's machine.

Never automatic

OpenShell can be set to approve requests on its own. Fenzi never turns that on. fenzi doctor fails on a gateway where automatic approval is set for every sandbox, fenzi doctor --fix turns it off, and the policy file has no field for it.

Why a person

A model can write a good reason for a bad request. The decision about what leaves the machine belongs to someone who answers for it.