From 5865c2a6bdea05bd5942718c020fce4e73d9b173 Mon Sep 17 00:00:00 2001 From: Jeongwan Jeon Date: Sat, 22 Aug 2026 05:37:01 +0900 Subject: [PATCH 1/6] fix: start without a managed Bot when the image does not have one MANAGED_AGENT_AG_UI_URL is no longer required. A URL with no token still refuses to start; a leftover token with no URL is ignored, so a one-container image can boot from a laptop .env. --- server/src/config.ts | 57 ++++++++++++++++++++++++++++++------- server/src/index.ts | 11 +++---- server/tests/config.test.ts | 27 +++++++++++++----- 3 files changed, 73 insertions(+), 22 deletions(-) diff --git a/server/src/config.ts b/server/src/config.ts index 2981b1dd..dfb68dfd 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -86,12 +86,23 @@ export function configuredAuthProviders( return providers; } +export type ManagedAgentConfig = { + endpoint: URL; + /** Secret sent only to the managed Bot endpoint. Never stored in an agent row. */ + token: string; +}; + export type DeploymentConfig = { databaseUrl: string; keyEncryptionKey: string; - managedAgentAgUiUrl: URL; - /** Secret sent only to the managed Bot endpoint. Never stored in an agent row. */ - managedAgentToken: string; + /** + * The Bot in the box, when this deployment has one. + * + * Absent is the one-container image: it carries no AG-UI process, and a required URL would + * register a coworker against a host that is not there. Set both the URL and the token together + * when a remote Bot is actually running. + */ + managedAgent?: ManagedAgentConfig; /** * What this deployment calls itself, when more than one shares an Intelligence project. * @@ -220,8 +231,14 @@ function url(environment: Environment, name: string): string | undefined { return value; } -function requiredHttpUrl(environment: Environment, name: string): URL { - const value = required(environment, name); +function optionalHttpUrl( + environment: Environment, + name: string, +): URL | undefined { + const value = optional(environment, name); + if (!value) { + return undefined; + } let parsed: URL; try { @@ -237,6 +254,29 @@ function requiredHttpUrl(environment: Environment, name: string): URL { return parsed; } +/** + * The Bot in the box, if this deployment has one. + * + * A URL with no token would send unauthenticated calls to a Bot that refuses them, so that half + * alone refuses to start. A token with no URL is the leftover `scripts/start.sh` writes into + * `.env`; it names nothing and is ignored, so a one-container image can boot from that file. + */ +function managedAgentConfig( + environment: Environment, +): ManagedAgentConfig | undefined { + const endpoint = optionalHttpUrl(environment, "MANAGED_AGENT_AG_UI_URL"); + const token = optional(environment, "MANAGED_AGENT_TOKEN"); + if (endpoint && !token) { + throw new Error( + "MANAGED_AGENT_TOKEN must be set when MANAGED_AGENT_AG_UI_URL is set", + ); + } + if (!endpoint || !token) { + return undefined; + } + return { endpoint, token }; +} + function oauthClient( environment: Environment, provider: "GOOGLE" | "MICROSOFT" | "OKTA", @@ -542,15 +582,12 @@ export function loadConfig( ): DeploymentConfig { const google = oauthClient(environment, "GOOGLE"); const auth = authConfig(environment, google); + const managedAgent = managedAgentConfig(environment); return { databaseUrl: required(environment, "DATABASE_URL"), keyEncryptionKey: keyEncryptionKey(environment), - managedAgentAgUiUrl: requiredHttpUrl( - environment, - "MANAGED_AGENT_AG_UI_URL", - ), - managedAgentToken: required(environment, "MANAGED_AGENT_TOKEN"), + ...(managedAgent ? { managedAgent } : {}), deploymentId: optional(environment, "DEPLOYMENT_ID"), tenantPackageDirectory: optional(environment, "TENANT_PACKAGE_DIR") ?? "../examples/fintech", diff --git a/server/src/index.ts b/server/src/index.ts index 4cdac0bf..f14d7278 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -125,7 +125,7 @@ const agentVault = { }; const agentProfileStore = createAgentProfileStore( database, - config.managedAgentAgUiUrl, + config.managedAgent?.endpoint, agentVault, ); // Read here rather than beside the synchronise below, because the package names the deployment and @@ -156,10 +156,11 @@ const channelActivityListener = await startChannelActivityListener( channelEvents, ); const roleRepository = createRoleRepository(database); -const loadAgentsForActor = createRuntimeAgentLoader(database, agentVault, { - endpoint: config.managedAgentAgUiUrl, - token: config.managedAgentToken, -}); +const loadAgentsForActor = createRuntimeAgentLoader( + database, + agentVault, + config.managedAgent, +); await synchronizeTenantPackage(database, tenantPackage); /* * Built before `auth`, because the deny list is consulted during sign-in and the store is what diff --git a/server/tests/config.test.ts b/server/tests/config.test.ts index 5e470996..9deec2fa 100644 --- a/server/tests/config.test.ts +++ b/server/tests/config.test.ts @@ -50,9 +50,10 @@ describe("deployment configuration", () => { licenseToken: "license-token", }, }); - expect(config.managedAgentAgUiUrl).toEqual( - new URL("http://localhost:4200/ag-ui"), - ); + expect(config.managedAgent).toEqual({ + endpoint: new URL("http://localhost:4200/ag-ui"), + token: "managed-agent-token", + }); expect(config.tenantPackageDirectory).toBe("../examples/fintech"); }); @@ -116,22 +117,34 @@ describe("deployment configuration", () => { ); }); - test("refuses to start when MANAGED_AGENT_AG_UI_URL is missing", () => { + test("starts without a managed Bot when neither half is set", () => { const environment: Record = { ...baseEnvironment, }; delete environment.MANAGED_AGENT_AG_UI_URL; + delete environment.MANAGED_AGENT_TOKEN; - expect(() => loadConfig(environment)).toThrow("MANAGED_AGENT_AG_UI_URL"); + expect(loadConfig(environment).managedAgent).toBeUndefined(); }); - test("refuses to start when MANAGED_AGENT_TOKEN is missing", () => { + test("refuses a URL with no token", () => { const environment: Record = { ...baseEnvironment, }; delete environment.MANAGED_AGENT_TOKEN; - expect(() => loadConfig(environment)).toThrow("MANAGED_AGENT_TOKEN"); + expect(() => loadConfig(environment)).toThrow( + "MANAGED_AGENT_TOKEN must be set when MANAGED_AGENT_AG_UI_URL is set", + ); + }); + + test("ignores a leftover token when no URL is set", () => { + const environment: Record = { + ...baseEnvironment, + }; + delete environment.MANAGED_AGENT_AG_UI_URL; + + expect(loadConfig(environment).managedAgent).toBeUndefined(); }); test("refuses a non-HTTP MANAGED_AGENT_AG_UI_URL", () => { From 7874723a40a000dc8ddc43adca4e78b279986a51 Mon Sep 17 00:00:00 2001 From: Jeongwan Jeon Date: Sat, 22 Aug 2026 05:37:01 +0900 Subject: [PATCH 2/6] fix: omit a package coworker whose endpoint expanded to nothing The shipped Risk Analyst points at MANAGED_AGENT_AG_UI_URL. When that is unset the endpoint is empty, the coworker is left out of the roster, and channels drop the reference rather than failing to load. --- examples/fintech/agents.yaml | 2 +- server/src/tenant-package.ts | 64 ++++++++++++++++++----------- server/tests/tenant-package.test.ts | 34 +++++++++++++++ 3 files changed, 74 insertions(+), 26 deletions(-) diff --git a/examples/fintech/agents.yaml b/examples/fintech/agents.yaml index 3c76cab8..a41507e8 100644 --- a/examples/fintech/agents.yaml +++ b/examples/fintech/agents.yaml @@ -24,4 +24,4 @@ agents: role_description: Investigate policies, transaction monitoring, and control evidence. avatar_seed: risk-analyst type: remote-ag-ui - endpoint: ${MANAGED_AGENT_AG_UI_URL:-http://localhost:4201/ag-ui} + endpoint: ${MANAGED_AGENT_AG_UI_URL:-} diff --git a/server/src/tenant-package.ts b/server/src/tenant-package.ts index 7f56e24d..1aba4baa 100644 --- a/server/src/tenant-package.ts +++ b/server/src/tenant-package.ts @@ -244,7 +244,8 @@ export function validateTenantPackage(files: PackageFiles): TenantPackage { const tenant = asRecord(brand.tenant, "brand.tenant"); const skin = brand.skin === undefined ? undefined : asRecord(brand.skin, "brand.skin"); - const agents = asList(agentsYaml.agents, "agents.yaml agents").map( + const omittedAgentIds = new Set(); + const agents = asList(agentsYaml.agents, "agents.yaml agents").flatMap( (value) => { const agent = asRecord(value, "agent"); const type: TenantAgent["type"] | undefined = @@ -256,29 +257,42 @@ export function validateTenantPackage(files: PackageFiles): TenantPackage { if (!type) { throw new Error("agent.type must be built-in or remote-ag-ui"); } - return { - id: requiredString(agent.id, "agent.id"), - name: requiredString(agent.name, "agent.name"), - title: requiredString(agent.title, "agent.title"), - roleDescription: requiredString( - agent.role_description, - "agent.role_description", - ), - avatarSeed: - agent.avatar_seed === undefined - ? undefined - : requiredString(agent.avatar_seed, "agent.avatar_seed"), - type, - configuration: - type === "built_in" - ? { - systemPrompt: requiredString( - agent.system_prompt, - "agent.system_prompt", - ), - } - : { endpoint: requiredString(agent.endpoint, "agent.endpoint") }, - }; + const id = requiredString(agent.id, "agent.id"); + if (type === "remote_ag_ui") { + const endpoint = + typeof agent.endpoint === "string" ? agent.endpoint.trim() : ""; + if (!endpoint) { + omittedAgentIds.add(id); + return []; + } + } + return [ + { + id, + name: requiredString(agent.name, "agent.name"), + title: requiredString(agent.title, "agent.title"), + roleDescription: requiredString( + agent.role_description, + "agent.role_description", + ), + avatarSeed: + agent.avatar_seed === undefined + ? undefined + : requiredString(agent.avatar_seed, "agent.avatar_seed"), + type, + configuration: + type === "built_in" + ? { + systemPrompt: requiredString( + agent.system_prompt, + "agent.system_prompt", + ), + } + : { + endpoint: requiredString(agent.endpoint, "agent.endpoint"), + }, + }, + ]; }, ); const agentIds = new Set(agents.map((agent) => agent.id)); @@ -288,7 +302,7 @@ export function validateTenantPackage(files: PackageFiles): TenantPackage { const permittedAgents = stringArray( channel.permitted_agents, "channel.permitted_agents", - ); + ).filter((agentId) => !omittedAgentIds.has(agentId)); for (const agentId of permittedAgents) { if (!agentIds.has(agentId)) { throw new Error(`channel references unknown agent "${agentId}"`); diff --git a/server/tests/tenant-package.test.ts b/server/tests/tenant-package.test.ts index 3c4e3243..5e1dc3c8 100644 --- a/server/tests/tenant-package.test.ts +++ b/server/tests/tenant-package.test.ts @@ -294,6 +294,40 @@ describe("tenant YAML validation", () => { }), ).toThrow('references unknown agent "missing"'); }); + + test("omits a remote coworker whose endpoint expanded to nothing", () => { + const tenantPackage = validateTenantPackage({ + brand: "tenant: { id: fintech, product_name: Ledgerline }", + agents: `agents: + - id: knowledge + name: Knowledge + title: Company Knowledge + role_description: Answer company questions. + type: built-in + system_prompt: Answer from knowledge. + - id: risk-analyst + name: Risk Analyst + title: Risk + role_description: Investigate policies. + type: remote-ag-ui + endpoint: ""`, + channels: `channels: + - id: risk-and-compliance + name: Risk + description: Investigate. + permitted_agents: [knowledge, risk-analyst] + allowed_groups: [all]`, + model: + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + knowledge: "sources: []", + themeCss: "", + }); + + expect(tenantPackage.agents.map((agent) => agent.id)).toEqual([ + "knowledge", + ]); + expect(tenantPackage.channels[0]?.permittedAgents).toEqual(["knowledge"]); + }); }); describe("tenant package agent profile synchronization", () => { From 7268380786026e38a84fa834e177aec119a68489 Mon Sep 17 00:00:00 2001 From: Jeongwan Jeon Date: Sat, 22 Aug 2026 05:37:01 +0900 Subject: [PATCH 3/6] fix: refuse to create a coworker with no endpoint when there is no managed Bot Product-created coworkers used to fall back to the managed URL. Without one that fallback is gone, so create and duplicate say so instead of storing an agent that cannot answer. --- server/src/agents/profile-store.ts | 30 ++++++++++++++----- server/src/agents/routes.ts | 4 +++ .../agent-profile-store.integration.test.ts | 15 ++++++++++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/server/src/agents/profile-store.ts b/server/src/agents/profile-store.ts index 98eaf565..5b055fdd 100644 --- a/server/src/agents/profile-store.ts +++ b/server/src/agents/profile-store.ts @@ -96,6 +96,15 @@ export class ProtectedAgentError extends Error { } } +export class ManagedAgentUnavailableError extends Error { + constructor() { + super( + "This deployment has no managed Bot. Give the coworker its own AG-UI endpoint.", + ); + this.name = "ManagedAgentUnavailableError"; + } +} + const joinedProjection = { id: agents.id, name: agents.name, @@ -258,16 +267,16 @@ async function findByTokenHash( export function createAgentProfileStore( database: Database, - managedAgentAgUiUrl: URL, + managedAgentAgUiUrl: URL | undefined, /** * Where a customer agent's key is kept. Optional so a deployment without a vault still runs; an * agent with a key then simply cannot be created, which is better than storing it in the clear. */ vault?: { store: CredentialStore; encryptionKey: string }, ): AgentProfileStore { - const managedConfiguration = { - endpoint: managedAgentAgUiUrl.toString(), - }; + const managedConfiguration = managedAgentAgUiUrl + ? { endpoint: managedAgentAgUiUrl.toString() } + : undefined; return { async list(actor, hidden = false) { @@ -295,6 +304,12 @@ export function createAgentProfileStore( create(actor, input) { return database.transaction(async (transaction) => { const id = newAgentId(); + const endpoint = input.endpoint + ? { endpoint: input.endpoint } + : managedConfiguration; + if (!endpoint) { + throw new ManagedAgentUnavailableError(); + } await transaction.insert(agents).values({ id, name: input.name, @@ -305,9 +320,7 @@ export function createAgentProfileStore( // The key, if there is one, goes to the vault and only its reference is stored here. See // auth-header.ts for why a bearer token must not sit next to the endpoint. configuration: { - ...(input.endpoint - ? { endpoint: input.endpoint } - : managedConfiguration), + ...endpoint, ...(input.auth && vault ? { auth: await storeAgentAuth({ @@ -420,6 +433,9 @@ export function createAgentProfileStore( const source = await findAccessibleProfile(transaction, actor, id); if (!source) throw new AgentNotFoundError(id); + if (!managedConfiguration) { + throw new ManagedAgentUnavailableError(); + } const duplicateId = newAgentId(); await transaction.insert(agents).values({ id: duplicateId, diff --git a/server/src/agents/routes.ts b/server/src/agents/routes.ts index 4beb5ee5..144a8c9b 100644 --- a/server/src/agents/routes.ts +++ b/server/src/agents/routes.ts @@ -10,6 +10,7 @@ import { AgentNotFoundError, AgentNotManageableError, type AgentProfileStore, + ManagedAgentUnavailableError, ProtectedAgentError, } from "./profile-store"; import type { @@ -476,5 +477,8 @@ function mapStoreError(context: Context, error: unknown): Response { if (error instanceof ProtectedAgentError) { return context.json({ error: "System-owned agents are protected." }, 403); } + if (error instanceof ManagedAgentUnavailableError) { + return context.json({ error: error.message }, 400); + } throw error; } diff --git a/server/tests/agent-profile-store.integration.test.ts b/server/tests/agent-profile-store.integration.test.ts index a3d0691a..92c82908 100644 --- a/server/tests/agent-profile-store.integration.test.ts +++ b/server/tests/agent-profile-store.integration.test.ts @@ -6,6 +6,7 @@ import { AgentNotManageableError, type AgentProfileStore, createAgentProfileStore, + ManagedAgentUnavailableError, ProtectedAgentError, } from "../src/agents/profile-store"; import type { @@ -220,6 +221,20 @@ async function racePackageAttachment( } describe("agent profile store integration", () => { + test("refuses to create a coworker with no endpoint when this deployment has no managed Bot", async () => { + const owner = await createUser(); + const withoutManaged = createAgentProfileStore(database, undefined); + + await expect( + withoutManaged.create(owner, { + name: "No Endpoint", + title: "Needs an address", + roleDescription: "Should not land on a missing Bot.", + visibility: "private", + }), + ).rejects.toBeInstanceOf(ManagedAgentUnavailableError); + }); + test("lets an owner and admin get and list a private profile but hides it from another user", async () => { const owner = await createUser(); const other = await createUser(); From 352093057b26dd4c3e2443f3d586d6b75867ce7c Mon Sep 17 00:00:00 2001 From: Jeongwan Jeon Date: Sat, 22 Aug 2026 05:37:01 +0900 Subject: [PATCH 4/6] fix: keep the laptop Bot URL out of .env scripts/start.sh still points at agent-langgraph for a local stack. The URL stays in the script rather than .env, so docker run --env-file .env does not inherit a host the image does not contain. --- .env.example | 14 ++++++++------ scripts/start.sh | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 016e81df..126017d3 100644 --- a/.env.example +++ b/.env.example @@ -203,15 +203,17 @@ AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true # EGRESS_PROXY_SALES_BOT=http://sales.proxy.internal:8080 -# The managed coworker AG-UI endpoint. Required: use an HTTP(S) URL. +# The managed coworker AG-UI endpoint. Optional: use an HTTP(S) URL, and set MANAGED_AGENT_TOKEN +# with it. A URL with no token refuses to start; a leftover token with no URL is ignored. # -# Defaults to agent-langgraph on 4201, which runs a real framework and its own tool loop. The -# proof-of-concept on 4200 hand-writes the protocol and leaves the loop to whatever is watching, so -# it is a reference rather than something to build a deployment on. -MANAGED_AGENT_AG_UI_URL=http://localhost:4201/ag-ui +# Unset in the one-container image, which does not carry a Bot. `scripts/start.sh` points this at +# agent-langgraph on 4201 for a laptop. The proof-of-concept on 4200 hand-writes the protocol and +# leaves the loop to whatever is watching, so it is a reference rather than something to build a +# deployment on. +# MANAGED_AGENT_AG_UI_URL=http://localhost:4201/ag-ui # Shared secret sent by the server on every call to a managed Bot, as the `x-openbot-agent-token` -# header. Both Bots refuse to start without it, and so does the server. +# header. Required together with the URL above; the server starts without either. # # `scripts/start.sh` generates one and writes it back here on first run, so leaving this empty is # fine locally. Set it yourself for a deployment: openssl rand -base64 32 diff --git a/scripts/start.sh b/scripts/start.sh index 6cf05bbd..986727bd 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -45,6 +45,12 @@ COMPUTER_TOKEN="$(setting COMPUTER_TOKEN openbot-dev-computer-token)" # # Written into .env rather than exported for this run alone, so `docker compose up` by hand later # sees the same value the script used. +# The laptop stack runs agent-langgraph on LANGGRAPH_PORT. The one-container image does not, so +# this default stays in the script rather than in .env: a `docker run --env-file .env` must not +# inherit a URL that points at a process the image does not contain. +MANAGED_AGENT_AG_UI_URL="$(setting MANAGED_AGENT_AG_UI_URL "http://localhost:${LANGGRAPH_PORT}/ag-ui")" +export MANAGED_AGENT_AG_UI_URL + MANAGED_AGENT_TOKEN="$(setting MANAGED_AGENT_TOKEN "")" if [ -z "$MANAGED_AGENT_TOKEN" ]; then MANAGED_AGENT_TOKEN="$(openssl rand -base64 32)" From ee3e7b8563ecd6a92f89a3db19aea525db00e464 Mon Sep 17 00:00:00 2001 From: Jeongwan Jeon Date: Sat, 22 Aug 2026 05:37:02 +0900 Subject: [PATCH 5/6] ci: boot the image check without a managed Bot The image does not carry agent-langgraph. Requiring a URL against 127.0.0.1:4201 was the same lie the healthcheck used to tell. --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a14feea..8bc3f8de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,10 +161,8 @@ jobs: -e EMBEDDED_POSTGRES=on \ -e KEY_ENCRYPTION_KEY="$(openssl rand -base64 32)" \ -e TRUSTED_ORIGINS=http://localhost:3001 \ - -e OPENBOT_SINGLE_USER=true \ - -e MANAGED_AGENT_AG_UI_URL=http://127.0.0.1:4201/ag-ui \ - -e MANAGED_AGENT_TOKEN=ci-not-a-real-token \ - -e INTELLIGENCE_API_URL=https://api.intelligence.copilotkit.ai \ + -e OPENBOT_SINGLE_USER=true \ + -e INTELLIGENCE_API_URL=https://api.intelligence.copilotkit.ai \ -e INTELLIGENCE_GATEWAY_WS_URL=wss://realtime.intelligence.copilotkit.ai \ -e INTELLIGENCE_API_KEY=ci-not-a-real-key \ -e COPILOTKIT_LICENSE_TOKEN=ci-not-a-real-licence \ From 03c443d7673628174e38671db2982eb54c80bbfa Mon Sep 17 00:00:00 2001 From: Jeongwan Jeon Date: Sat, 22 Aug 2026 05:37:02 +0900 Subject: [PATCH 6/6] docs: say the one-container image starts without a remote Bot The snapshot cache already lives in Postgres, so the one-replica warning is also gone. --- CHANGELOG.md | 13 +++++++++++++ README.md | 4 +--- docs/configuration.md | 9 +++++++-- docs/coworkers.md | 5 ++++- docs/deployment.md | 16 ++++++++++------ 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b92b13..d4fe6564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,15 @@ 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. +`MANAGED_AGENT_AG_UI_URL` is no longer required to start. The one-container image does not carry a +Bot, so requiring it registered the shipped Risk Analyst against a host that was not there and every +conversation with it failed. Leave it unset for that image. A laptop `scripts/start.sh` still points +it at `agent-langgraph`. A URL with no `MANAGED_AGENT_TOKEN` still refuses to start; a leftover +token with no URL is ignored. + +A `.env` copied from an older `.env.example` still has `MANAGED_AGENT_AG_UI_URL=http://localhost:4201/ag-ui`. +Unset it before `docker run --env-file .env`, or the coworker comes back. + Sessions survive and nobody signs in again. ### Added @@ -93,6 +102,10 @@ Sessions survive and nobody signs in again. is unavailable never blocks a sign-in. ### Fixed +- **The one-container image registered a coworker it could not run.** `MANAGED_AGENT_AG_UI_URL` + defaulted to `localhost:4201` and was required, so Risk Analyst appeared on the roster and every + conversation with it failed. The URL is optional; the package omits that coworker when it is + unset. `scripts/start.sh` still points it at `agent-langgraph` on a laptop. - **A boundary rule applied on one server out of N.** The policy is read from memory on every action, which is right, but memory was only ever filled at boot. An administrator's new deny rule was enforced by whichever process served the request and roughly one action in N went through it, while diff --git a/README.md b/README.md index 855a9ad1..26de4ccc 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ From `/agents`, create a coworker with: - optional AG-UI endpoint; - optional write-only authorization header. -The server validates agent endpoints with the same target checks used for browser navigation. If no custom endpoint is set, product-created coworkers use `MANAGED_AGENT_AG_UI_URL`. +The server validates agent endpoints with the same target checks used for browser navigation. If no custom endpoint is set, product-created coworkers use `MANAGED_AGENT_AG_UI_URL` when it is configured, and are refused when it is not. Tenant package agents are declared in `agents.yaml` as either: @@ -183,8 +183,6 @@ See [docs/configuration.md](docs/configuration.md) and [docs/coworkers.md](docs/ - `DATABASE_URL` - `KEY_ENCRYPTION_KEY` -- `MANAGED_AGENT_AG_UI_URL` -- `MANAGED_AGENT_TOKEN` - `INTELLIGENCE_API_URL` - `INTELLIGENCE_GATEWAY_WS_URL` - `INTELLIGENCE_API_KEY` diff --git a/docs/configuration.md b/docs/configuration.md index dee24242..7a3ea8d8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,8 +20,6 @@ bash scripts/start.sh | ----------------------------- | ----------------------------------------------------------------------------------------------------- | | `DATABASE_URL` | PostgreSQL connection string. | | `KEY_ENCRYPTION_KEY` | Base64-encoded 32-byte key for encrypted stored credentials. Generate with `openssl rand -base64 32`. | -| `MANAGED_AGENT_AG_UI_URL` | Default AG-UI endpoint for coworkers created in the product. Must be HTTP(S). | -| `MANAGED_AGENT_TOKEN` | Secret sent only to the managed AG-UI endpoint. Generate with `openssl rand -base64 32`. | | `INTELLIGENCE_API_URL` | CopilotKit Intelligence API URL. | | `INTELLIGENCE_GATEWAY_WS_URL` | CopilotKit Intelligence realtime gateway URL. | | `INTELLIGENCE_API_KEY` | Runtime key for the Intelligence project. | @@ -29,6 +27,13 @@ bash scripts/start.sh All four Intelligence values are required together. Missing any of them stops server startup. +`MANAGED_AGENT_AG_UI_URL` names the Bot in the box: the default endpoint for coworkers created in +the product. It needs `MANAGED_AGENT_TOKEN` beside it, or the server refuses to start. Unset, the +server starts without a managed Bot, the shipped Risk Analyst coworker is omitted, and creating a +coworker without its own endpoint is refused. A leftover token with no URL is ignored. The +one-container image has no Bot process, so leave the URL unset there. `scripts/start.sh` points it +at `agent-langgraph` on a laptop. + ## General variables | Variable | Default | Meaning | diff --git a/docs/coworkers.md b/docs/coworkers.md index db1a7e63..37a40e80 100644 --- a/docs/coworkers.md +++ b/docs/coworkers.md @@ -61,7 +61,10 @@ That is `agent-langgraph`, which runs a real framework and its own tool loop. Th `4200` hand-writes the protocol and leaves the loop to whatever is watching, so it is a reference rather than something to build a deployment on. -The server requires this setting at startup. Package-provided agents use their own `agents.yaml` configuration. +The URL is optional. Set it with `MANAGED_AGENT_TOKEN`, or leave it unset: product-created coworkers +then need their own endpoint, and a package agent whose endpoint expands to nothing is omitted +rather than registered against a missing host. A leftover token with no URL is ignored. +Package-provided agents otherwise use their own `agents.yaml` configuration. ## Register an external AG-UI agent diff --git a/docs/deployment.md b/docs/deployment.md index 6dfc167f..60bf2802 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -68,12 +68,17 @@ and the fix would not be available. | `KEY_ENCRYPTION_KEY` | base64 32 bytes. `openssl rand -base64 32`. The example key is refused in production | | `INTELLIGENCE_API_URL`, `INTELLIGENCE_GATEWAY_WS_URL`, `INTELLIGENCE_API_KEY` | CopilotKit Intelligence. A free plan is available and it can be self-hosted | | `COPILOTKIT_LICENSE_TOKEN` | from `npx copilotkit@latest license --write` | -| `MANAGED_AGENT_AG_UI_URL` | the AG-UI endpoint for the example remote Bot | | a model key | `OPENAI_API_KEY`, or the provider you configured | `COMPUTER_TOKEN` is generated at start if you do not set one. Both processes that need it are inside the container, so there is nothing to share it with. +`MANAGED_AGENT_AG_UI_URL` is not required here. The image does not carry `agent-langgraph` or +`agent-bot`. Leave it unset and the shipped Risk Analyst coworker is omitted rather than registered +against a host that is not there. Set it, with `MANAGED_AGENT_TOKEN`, only when a Bot is actually +reachable from this container. Unset it if your `.env` still has the laptop default +`http://localhost:4201/ag-ui`. + **Authentication is required.** With no identity provider configured the deployment refuses to start, because a public URL where every visitor is an administrator fails silently: it looks like it works. Configure Google, Microsoft or Okta, or set `OPENBOT_SINGLE_USER=true` to say you meant an open @@ -98,12 +103,11 @@ docker run --rm --env-file .env openbot \ sh -c "cd /app/server && bun x drizzle-kit migrate --config=drizzle.config.ts" ``` -## One replica, for now +## Replicas -Run one. The gateway still caches the page snapshot a Bot resolves element references against in -process memory, so a second replica answers a click with a snapshot it never took. The symptom is an -element that cannot be found, intermittently, which reads as a flaky Bot rather than as a -configuration problem. Pin the platform's maximum instance count until that moves to the database. +The page snapshot a Bot resolves element references against lives in Postgres, so a second replica +can answer a click the first one snapshotted. Run more than one if the platform wants it. The +supervisor is still not in this image, so every replica shares the one browser inside it. ## Platform notes