diff --git a/CHANGELOG.md b/CHANGELOG.md index 387f75a9..c73817c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,11 @@ digit. The same rule container and volume names have always followed. A deployme `AUDIT_RETENTION_DAYS` is new and unset, which keeps the audit trail forever, as before. Set it to a whole number of days to have old rows removed. +The local document index and the old connector tables are dropped by migration. `documents`, +`chunks`, `document_acls` and the four connector-bookkeeping tables are removed and their rows go +with them; this cannot be rolled back. A deployment that had been syncing into the local index loses +that copy, which is the point: answering now goes through a live system's own search. + **An MCP server pointed at a credential that no longer exists loses the pointer.** `mcp_servers` now names its credential with a real foreign key, where the column was `text` against a `uuid` primary key with nothing checking it — so a deployment is allowed to be holding a pointer to a vault @@ -69,7 +74,7 @@ Sessions survive and nobody signs in again. that system's own search as the person asking, so the vendor decides what they may see and there is no second copy of anybody's documents here to keep in step, to secure, or to leave behind when somebody is removed. The local index that was being filled — `documents`, `chunks` and - `document_acls` — is read by nothing, and the connector that filled it is going away. Retrieval over + `document_acls` — and the connector that filled it have both been dropped. Retrieval over a copy of a customer's corpus is not a thing OpenBot does. ### Added @@ -184,6 +189,19 @@ Sessions survive and nobody signs in again. is unavailable never blocks a sign-in. ### Fixed +- **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 + talked into fetching one still reached cloud metadata or an internal host. The address is + canonicalised before it is checked now, the mapped form of `0.0.0.0` (which reaches every local + port) is refused, and the container credential endpoints a hosted deployment must never expose — + ECS and Fargate's `169.254.170.2`, Alibaba's `100.100.100.200` — are refused even when the + private-host opt-in is on. The same guard backs agent registration, so it is closed there too. +- **The supervisor could adopt a container it did not create.** When starting a Bot's computer hit a + name already taken, it started whatever held the name and handed it the deployment's computer + token, so on a Docker host shared with anything else it could drive a stranger's container as a + Bot's. It now refuses a container that does not carry its own namespace label, read from the + container rather than inferred, so a second deployment on the same host is never adopted. - **Removing somebody left the credentials they had granted this deployment sitting in the vault.** Removing them from the People screen ended their sessions and stopped the next sign-in, and left the refresh token behind, unrevoked. They could not use it — the account comes from a session they no diff --git a/README.md b/README.md index 9e04e7c7..c76a850f 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,7 @@ docker run -p 3001:3001 --env-file .env \ ``` Leave `EMBEDDED_POSTGRES` off and set `DATABASE_URL` to point at a database you already run. -[docs/deployment.md](docs/deployment.md) has the minimum sizes, the platform notes, and why this runs -as one replica for now. +[docs/deployment.md](docs/deployment.md) has the minimum sizes, the platform notes, and how it behaves behind more than one replica. ## Try it diff --git a/docs/architecture.md b/docs/architecture.md index 50d75228..b9aa4e8e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,7 +19,7 @@ Regenerate it with `bun run diagram` after changing anything it shows. | `agent-bot` | 4200 | Proof-of-concept AG-UI Bot. | | `agent-langgraph` | 4201 | LangGraph AG-UI Bot. | | `supervisor` | 4500 host / 4300 container | Creates, stops, resets, and lists per-Bot computer containers. | -| PostgreSQL with pgvector | 5432 | Product data, audit rows, credentials, policy, grants, channels, components, and connector state. | +| PostgreSQL with pgvector | 5432 | Product data, audit rows, credentials, policy, grants, channels, and components. | | CopilotKit Intelligence | external | Durable threads, memory, and realtime gateway. | `scripts/start.sh` starts PostgreSQL, `agent-computer`, `agent-bot`, `agent-langgraph`, and the supervisor through Docker Compose, then starts `server` and `app` on the host. diff --git a/docs/configuration.md b/docs/configuration.md index 163661d8..37265f88 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -54,6 +54,7 @@ at `agent-langgraph` on a laptop. | `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. | | `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. | **`AGENT_STALL_TIMEOUT_MS`** watches for the failure a Bot has that nothing else in the trail can show: a stream that stops producing anything. Every other audit row is something that happened, and @@ -169,6 +170,7 @@ where `` is `google`, `microsoft` or `okta`. | `AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS` | Local-only private-host browsing when `true`. Cloud metadata addresses are still refused. | | `AGENT_COMPUTER_POLICY` | JSON action policy: `{"mode":"enforce","deny":[...],"allow":[...]}`. | | `COMPUTER_RUNTIME` | Set to `runsc` to run supervised computers under gVisor. | +| `COMPUTER_SANDBOX` | Set to `on` to enable Chromium's own sandbox where the host permits user namespaces. Which way it went is printed at start-up. | `agent-computer` also reads: diff --git a/docs/deployment.md b/docs/deployment.md index 60bf2802..09e4ec35 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -111,7 +111,9 @@ supervisor is still not in this image, so every replica shares the one browser i ## Platform notes -**Google Cloud Run.** Set memory to at least 2 GB and max instances to 1. Cloud Run runs every +**Google Cloud Run.** Set memory to at least 2 GB. More than one instance is fine (see Replicas +above); each instance has its own browser, so a Bot's logins stay on whichever instance served them. +Cloud Run runs every container under gVisor, which Chromium is sensitive to; test a navigation before trusting it. `gcloud run compose up` will also deploy the whole compose file if you want a throwaway database alongside. diff --git a/server/src/agents/invocation.ts b/server/src/agents/invocation.ts deleted file mode 100644 index 592405b6..00000000 --- a/server/src/agents/invocation.ts +++ /dev/null @@ -1,20 +0,0 @@ -type Agent = { - id: string; - type: "built_in" | "remote_ag_ui"; - available: boolean; - reason?: string; -}; -type Response = { text: string; citations: unknown[] }; - -export function createAgentInvoker(ports: { - knowledge: (question: string) => Promise; - remote: (agentId: string, question: string) => Promise; -}) { - return async (agent: Agent, question: string) => { - if (!agent.available) - throw new Error(agent.reason ?? "Agent is unavailable."); - return agent.type === "built_in" - ? ports.knowledge(question) - : ports.remote(agent.id, question); - }; -} diff --git a/server/tests/agent-invocation.test.ts b/server/tests/agent-invocation.test.ts deleted file mode 100644 index b2f6a8db..00000000 --- a/server/tests/agent-invocation.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { expect, test } from "bun:test"; -import { createAgentInvoker } from "../src/agents/invocation"; - -test("invokes the selected available built-in agent", async () => { - const invoke = createAgentInvoker({ - knowledge: async () => ({ text: "Answer", citations: [] }), - remote: async () => ({ text: "unused", citations: [] }), - }); - await expect( - invoke({ id: "knowledge", type: "built_in", available: true }, "question"), - ).resolves.toEqual({ text: "Answer", citations: [] }); -}); - -test("rejects unavailable agents before invocation", async () => { - const invoke = createAgentInvoker({ - knowledge: async () => ({ text: "unused", citations: [] }), - remote: async () => ({ text: "unused", citations: [] }), - }); - await expect( - invoke( - { - id: "knowledge", - type: "built_in", - available: false, - reason: "Model credential is not configured.", - }, - "question", - ), - ).rejects.toThrow("Model credential is not configured."); -});