Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 5 additions & 42 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -123,63 +123,26 @@ AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true
# What a Bot may do on its computer, as one JSON object. Absent uses the built-in default, which
# permits the acting tools and forbids nothing, and records every action either way.
#
# `deny` is evaluated first and beats everything. An empty `allow` permits nothing, a missing policy
# `deny` is evaluated first and beats `allow`. An empty `allow` permits nothing, a missing policy
# permits nothing, and a rule that fails to parse denies rather than letting the action through. The
# server refuses to start if this is set and malformed, so an invalid restriction never falls back to
# permissive behavior.
#
# `ask` is the third list, checked after `deny` and before `allow`. A match stops the Bot, puts the
# action in front of a person in the conversation, and carries on with the same action if they allow
# it, so the turn is not thrown away. Nothing an `ask` rule matches can be reached by a `deny` rule:
# forbidden stays forbidden and is never offered as a question. It has to beat `allow`, because the
# default below permits everything, and an ask checked afterwards would never fire.
#
# An answer is bound to the exact action it was given for, so allowing one button is not permission
# to press a different one, and it can only be spent once. Nobody answering within ten minutes is the
# same as nobody being asked: the action does not happen. In `dry-run` an ask interrupts nobody and is
# only recorded, because dry-run promises to change nothing.
#
# Every list judges a Bot's calls to MCP servers as well as what it does in a browser, `ask` included,
# so a rule like `intent == "write_tool" && mcp.server == "jira"` stops the call and asks rather than
# refusing it. The questions and the answers are the same three audit rows either way.
#
# Workspace and browser profile per Bot. Each Bot's computer is its own container with its own
# volumes, so one Bot cannot read another's files or use another's logins, and every action records
# which Bot took it. A rule can still restrict a single Bot with `bot.id`.
#
# Attributes: tool.name, bot.id, actor.id, page.url, page.host, element.ref/role/name/type,
# key, submit, file.path, file.name, file.extension, mcp.server/tool/effect,
# repeat.count.
#
# repeat.count is how many times this Bot has just made this exact call, counting the one being
# decided. A stuck model retries, and each retry is a real action on somebody's live website that is
# perfectly reasonable on its own terms; only the count tells the thirtieth click apart from the
# first. `repeat.count >= 10` in `deny` stops a Bot going in circles. Two calls are the same call
# when the thing acted on is the same, whatever was typed into it, so ten searches typed into one box
# are ten repeats and a rule about repetition refuses the tenth: try one in `dry-run` first. The
# count is held in memory by the process that served the call, so a deployment running two API
# replicas splits every count and a rule about ten attempts fires at twenty or never, and calls to
# another server's tools over MCP are not counted at all.
# key, file.path, file.name, file.extension.
#
# Name every route to the same effect. A form submits from a keypress in any of its fields, so a rule
# that only blocks a Submit button does not block Enter from another field, and a Bot can ask the type
# tool to press Enter for it, which arrives as `submit` rather than as a keypress. The example below
# names all three.
# that only blocks a Submit button does not block Enter from another field. The example below refuses
# Enter outright for that reason.
# Functions: contains(haystack, needle) and matches(value, pattern), both case-insensitive.
# `enforce` blocks; `dry-run` decides and records but lets everything through, so a new rule can be
# tried against real traffic before it starts refusing anybody's work.
#
# AGENT_COMPUTER_POLICY={"mode":"enforce","deny":["(intent == \"activate\" && contains(element.name, \"submit\")) || (tool.name == \"computer_key\" && key == \"Enter\") || submit"],"ask":["intent == \"write_file\" && !matches(file.path, \"^notes/\")"],"allow":["true"]}

# How long two identical calls count as the same repetition, in ms. Three minutes unset, which
# assumes a retry loop is a model round trip apart: call the tool, read the failure, try again.
# Widen it for a deployment whose provider is slow or heavily queued, where genuine retries arrive
# minutes apart and every attempt would otherwise be counted as the first one. Widen it too far and
# honest work starts to accumulate: a Bot told to watch a dashboard all morning reloads the same page
# and is not stuck. Anything that is not a positive whole number stops the server rather than falling
# back to the default, because a rule about repetition that never fires looks exactly like a Bot
# behaving itself.
# COMPUTER_REPEAT_WINDOW_MS=180000
# AGENT_COMPUTER_POLICY={"mode":"enforce","deny":["(intent == \"activate\" && contains(element.name, \"submit\")) || (tool.name == \"computer_key\" && key == \"Enter\")"],"allow":["true"]}

# How long one action waits for its element, in ms. Read by agent-computer, not the server.
# ACTION_TIMEOUT_MS=10000
Expand Down
38 changes: 38 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## What this changes

<!-- What it does, and why it is worth doing. -->

## Where it runs

OpenBot is deployed as several server processes behind a load balancer, serving a whole company.
Consecutive requests from the same person reach different processes, and the process that answered a
WebSocket upgrade is rarely the one that answers the next call on that conversation.

State that outlives a single request therefore has to be shared, or the change works on one machine
and stops working the moment there are two, without saying so. That failure is worse than not
shipping the feature: it passes review, passes CI, passes a local demo, and only surfaces as a Bot
that forgets, a question nobody can answer, or a boundary that never fires.

Answer these even when the answer is "none":

- [ ] **New state that outlives a request?** Where does it live? A `Map` or `Set` held in a module or
a factory closure does not count as somewhere.
- [ ] **What happens on the second replica?** Name the concrete outcome, not "should be fine".
- [ ] **Anything serialised?** Say what stops two processes doing it at once. A unique index, a
conditional update, or an advisory lock are answers. A check-then-write is not.
- [ ] **Anything fanned out to a browser?** Say how it reaches a socket held by another process.
- [ ] **New listener, port, or schedule?** Say how it is reached through the same ingress as the API,
and what a hundred copies of it do.

Postgres is already there and is the default answer to all of the above: a table, a unique index, a
conditional update, `LISTEN`/`NOTIFY` for fan-out.

## Boundary and audit

- [ ] Every acting call still goes through the gateway: resolve, decide, audit, then act.
- [ ] New refusals and new failures each write a row.
- [ ] Nothing new is trusted from the client that the server can resolve itself.

## Proof

<!-- What you ran, and what you saw. Screenshots or a recording for anything with a surface. -->
99 changes: 0 additions & 99 deletions app/src/components/channels/approval-request.tsx

This file was deleted.

163 changes: 0 additions & 163 deletions app/src/lib/approvals.ts

This file was deleted.

Loading