From 4a339837dcf7cc9e70eee86777eee5d5c6a93bb3 Mon Sep 17 00:00:00 2001 From: David McKay Date: Fri, 21 Aug 2026 20:40:23 -0700 Subject: [PATCH] Catch the changelog and the two build docs up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The changelog gains what a person upgrading has to know and the six defects fixed since the last pass. `AGENT_TOOL_TOKEN` is generated on a laptop now, and `start.sh` stops skipping services that are already answering, which costs about five seconds on a run that rebuilds an image and is why a rotated secret now reaches the things that use it. The README gains one row. Its settings table already named the two secrets `start.sh` sets, and this is the third: without it no Bot may call a tool, and a person setting up would otherwise not learn it exists. Nothing else there changed — it says how to build and run this, not what happened to it. `docs/configuration.md` had `AGENT_TOOL_TOKEN` as "unset", which is no longer true of the documented path. It now says start.sh generates one, why the fail-closed default is right for a deployment and wrong for a laptop, and that it is one of a pair with MANAGED_AGENT_TOKEN pointing the other way — which is the thing that makes rotating either of them confusing. Also removes server/tests/routing.test.ts, an empty file I created by accident while looking for the real one and committed in #144. --- CHANGELOG.md | 48 ++++++++++++++++++++++++++++++++++++ README.md | 1 + docs/configuration.md | 13 +++++++++- server/tests/routing.test.ts | 0 4 files changed, 61 insertions(+), 1 deletion(-) delete mode 100644 server/tests/routing.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c73817c8..4e952b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,18 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ### Upgrading +`AGENT_TOOL_TOKEN` is generated for you on a laptop. `scripts/start.sh` mints one and writes it to +`.env`, the way it already did for `MANAGED_AGENT_TOKEN`. Without it no Bot could call a tool back +through the deployment, which is the correct default for a deployment and made every MCP tool dead +on arrival on a fresh clone. A value already set is kept, and `.env.example` still ships it empty, +so a deployment not using `start.sh` is unchanged and still fails closed. + +`start.sh` also stops skipping work for services that are already answering. A Bot container is now +handed to `docker compose` on every run and the server is restarted when this run minted a secret, +because answering says a process is alive and not that it still agrees with the deployment. The cost +is that a run which rebuilds an image recreates the Bot containers, about five seconds; `supervisor` +already behaved this way. + Two configurations now refuse to start: - A provider configured with no `INITIAL_ADMIN_EMAILS`. Set it to at least one address. @@ -189,6 +201,42 @@ Sessions survive and nobody signs in again. is unavailable never blocks a sign-in. ### Fixed +- **A Bot browsed to a vendor it already had tools for.** Granted Google Drive, asked what was in a + document, it opened `drive.google.com` in its own browser, met a sign-in page that browser can + never satisfy, and asked the person to sign in to an account they had already connected. A tool + array says a tool exists; it does not say the tool is the way to reach that system, and it was + competing with a page of prose about the browser that mentions connectors nowhere. A Bot is now + told which systems it holds tools for, generated from its grants and placed before that prose, so + enabling a connector changes what the Bot is told on its next run. +- **A question went to a coworker that had no way to answer it.** Routing read the sentence somebody + wrote about what a coworker is for, which is not the same as what it can reach, so a question about + a Drive document went to the one whose description says "company knowledge" and which held no Drive + grants. Candidates now carry the systems they hold tools for. Purpose still decides first: a + specialist with no connectors is still right for a question about its specialism. +- **A deny rule about submitting a form was walked around by typing.** `computer_type` takes a + `submit` flag that presses Enter once the text is in, and the policy never saw a key, so a rule + refused at the button and at the keypress let the third route through. Both shipped copies of that + rule name both tools now, the key reaches the policy, and the audit row carries it — without it a + row said a field was filled in rather than that a form was sent. +- **A Bot refused at the door left no trace.** A callback that could not prove which Bot it was + returned 401 and wrote nothing, so a Bot holding a token the deployment no longer accepted had + every call refused, returned nothing to its own model, and the model told the person there were no + results. A false negative delivered as an answer, with the audit trail agreeing nothing had + happened. Recorded now as `mcp.callback_refused`, naming the tool and the reason but no Bot or + actor, since both arrive in the credential that just failed to verify. +- **An unanswered request for the wheel followed a Bot around.** Control belongs to a Bot's computer + rather than to a conversation, so a request nobody took sat there indefinitely and every later + conversation with that Bot showed a live prompt for work it was not doing, captioned with a reason + written for somebody else. An unanswered ask now stops being shown after ten minutes and its reason + goes with it. A person actually holding the wheel is never timed out. +- **`/admin/computers` listed nothing, ever.** Admin addressed the fleet through a per-Bot route with + a placeholder id, which stopped working when that route began checking whether the caller may act + as the Bot in the path. The screen renders nothing while the list is null, so a deployment with two + running computers looked like one with none. The fleet has a route of its own, still + administrator-only. +- **Every shipped component was recorded twice on a first start.** Two browsers announcing at once is + ordinary and the insert was already safe for it; the answer was not, so the loser of that race + named every component anyway and the caller wrote an audit row per name. - **A Bot could reach the deployment's own network by writing the address a different way.** The guard refused `169.254.169.254` and the private ranges as usually written, but not the same addresses spelled as an IPv6-mapped or NAT64 form, an integer, or with a trailing dot, so a Bot diff --git a/README.md b/README.md index 7e63e3c7..0efbe86a 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,7 @@ Settings worth knowing: | `ANTHROPIC_BASE_URL`, `GOOGLE_GENERATIVE_AI_BASE_URL` | The same, for those two APIs. | | `COMPUTER_TOKEN` | Secret every Bot computer request must present. `start.sh` sets one. | | `SUPERVISOR_TOKEN` | Secret the supervisor requires. `start.sh` sets one. | +| `AGENT_TOOL_TOKEN` | Secret a Bot presents to call a granted tool back. `start.sh` sets one. Without it no Bot may call tools. | | `COMPUTER_SUPERVISOR_URL` | Gives each Bot a computer of its own instead of one shared computer. | | `COMPUTER_RUNTIME` | Set to `runsc` to run computers under gVisor, where the host has it. | | `COMPUTER_SANDBOX` | Set to `on` for Chromium's own sandbox, where the host permits it. | diff --git a/docs/configuration.md b/docs/configuration.md index 37265f88..333424f7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -52,7 +52,7 @@ at `agent-langgraph` on a laptop. | `BOT_MODEL` | provider default from Bot code/env | Model used by the shipped Bots. | | `BOT_RESPONSES_API` | `false` | Makes `agent-langgraph` use the OpenAI Responses API. | | `AGENT_STALL_TIMEOUT_MS` | unset (off) | How long a Bot's stream may produce nothing before the turn is ended for it. | -| `AGENT_TOOL_TOKEN` | unset | The secret a framework Bot presents when it calls a granted tool back through this server. | +| `AGENT_TOOL_TOKEN` | unset; `start.sh` generates one | The secret a framework Bot presents when it calls a granted tool back through this server. | | `APP_DIST_DIR` | unset | Where the built app is, when this process serves it. Set inside the container image; unset in development, where Vite serves the app. | | `AUDIT_RETENTION_DAYS` | unset | Whole number of days to keep audit rows; older ones are removed. Unset keeps the trail forever. | @@ -67,6 +67,17 @@ may not reach a vendor directly. It calls the deployment that granted the tool, grant, the policy and the audit row live. Absent, no Bot may call tools back, and it is told so rather than quietly allowed. +That default is right for a deployment and wrong for a laptop, where it meant every granted MCP tool +was refused before it reached the grant, the boundary or the trail — and a refusal at that point is +not visible in the transcript, so a Bot reported no results rather than an error. `scripts/start.sh` +therefore generates one and writes it to `.env`, as it already does for `MANAGED_AGENT_TOKEN`. A +value already set is kept. + +It is one of a pair, and they are not interchangeable: `MANAGED_AGENT_TOKEN` is the server proving +itself to a Bot, this is a Bot proving itself to the server. Rotating either means the process +holding the old one refuses every call, which is why `start.sh` restarts the server and recreates the +Bot containers on a run that mints one. + ## OpenAI-compatible endpoints `OPENAI_BASE_URL` decides where an OpenAI-shaped request is answered. Unset, that is OpenAI. Set, it is any endpoint speaking the same API: a gateway in front of several providers, a proxy, or a model on hardware you control. diff --git a/server/tests/routing.test.ts b/server/tests/routing.test.ts deleted file mode 100644 index e69de29b..00000000