-
Notifications
You must be signed in to change notification settings - Fork 0
feat: boot standalone when the Intelligence contract is absent #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # The BitMind execution enclave's shape: no Intelligence contract at all. | ||
| # | ||
| # The server boots with OPENBOT_RUNTIME_MODE=standalone — admin surfaces working, chat, threads | ||
| # and routines unmounted — so nothing here carries an Intelligence URL, key or licence, and the | ||
| # render must produce a Secret without those keys. Routines stay off: the standalone server has | ||
| # no runtime to hand a firing to. | ||
| # | ||
| # NOTE FOR CI: this target must be added to the chart job's matrix in | ||
| # .github/workflows/ci.yml (a change that needs the workflow permission). | ||
| config: | ||
| runtimeMode: standalone | ||
| initialAdminEmails: admin@example.com | ||
| auth: | ||
| google: | ||
| clientId: example.apps.googleusercontent.com | ||
| publicUrl: https://openbot.internal | ||
| postgresql: | ||
| enabled: true | ||
| auth: | ||
| # Yours to choose, and the same value on every upgrade. Rendering example only. | ||
| password: "example-for-rendering-only" | ||
| secrets: | ||
| # Sessions are signed with this. Rendering example only; generate one with: openssl rand -base64 32 | ||
| betterAuthSecret: "example-for-rendering-only-at-least-32-chars" | ||
| # keyEncryptionKey is supplied on the command line, like every target: | ||
| # --set-string secrets.keyEncryptionKey="$(openssl rand -base64 32)" | ||
| googleClientSecret: "example-for-rendering-only" | ||
| computerToken: "example-for-rendering-only" | ||
| ingress: | ||
| enabled: true | ||
| className: nginx | ||
| hosts: | ||
| - host: openbot.internal | ||
| paths: | ||
| - path: / | ||
| pathType: Prefix | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,11 +7,24 @@ import { singleUserEnabled } from "./auth/dev-actor"; | |
| import type { ActionPolicy } from "./computer/policy"; | ||
| import { parseActionPolicy } from "./computer/policy-store"; | ||
|
|
||
| export type RuntimeCapabilities = { | ||
| mode: "intelligence"; | ||
| durableHistory: true; | ||
| intelligence: IntelligenceSettings; | ||
| }; | ||
| export type RuntimeCapabilities = | ||
| | { | ||
| mode: "intelligence"; | ||
| durableHistory: true; | ||
| intelligence: IntelligenceSettings; | ||
| } | ||
| /** | ||
| * A deployment with no Intelligence contract at all. | ||
| * | ||
| * The admin, people, computer and plugin surfaces all work; the chat runtime, threads | ||
| * and routines do not mount, so those paths 404 by design rather than refusing. This | ||
| * is the shape the BitMind execution enclave runs in: runs arrive through the BitMind | ||
| * gateway and an AG-UI agent, never through the chat surface. | ||
| */ | ||
| | { | ||
| mode: "standalone"; | ||
| durableHistory: false; | ||
| }; | ||
|
|
||
| /** The Intelligence contract. Every field is required; see runtimeCapabilities. */ | ||
| export type IntelligenceSettings = { | ||
|
|
@@ -556,13 +569,27 @@ function oktaAuth( | |
| } | ||
|
|
||
| /** | ||
| * Resolve the Intelligence contract, or refuse to start. | ||
| * Resolve the Intelligence contract, or the explicitly chosen standalone mode, or refuse. | ||
| * | ||
| * All four values are required together. A partial set is the more dangerous shape than none at all: | ||
| * it means somebody intended to configure Intelligence and got it wrong, so failing on the partial | ||
| * set alone (as this did) let a completely unconfigured deployment through as if that were a choice. | ||
| * All four Intelligence values are required together; any missing value is a refusal to boot that | ||
| * names what is absent. That includes ALL of them being absent: a Kubernetes Secret that failed to | ||
| * mount makes all four disappear at once, and a deployment that silently came up "healthy" with the | ||
| * chat runtime missing would turn a secret outage into a mystery. Standalone is therefore an | ||
| * explicit choice — `OPENBOT_RUNTIME_MODE=standalone` — the same shape as `OPENBOT_SINGLE_USER`: | ||
| * the deployment says it meant it. Chosen standalone with Intelligence values also set is refused | ||
| * as the contradiction it is. | ||
| */ | ||
| function runtimeCapabilities(environment: Environment): RuntimeCapabilities { | ||
| const chosen = optional(environment, "OPENBOT_RUNTIME_MODE"); | ||
| if ( | ||
| chosen !== undefined && | ||
| chosen !== "standalone" && | ||
| chosen !== "intelligence" | ||
| ) { | ||
| throw new Error( | ||
| `OPENBOT_RUNTIME_MODE=${chosen} is not a mode. Use standalone or intelligence, or unset it.`, | ||
| ); | ||
| } | ||
| const settings = { | ||
| apiUrl: url(environment, "INTELLIGENCE_API_URL"), | ||
| gatewayWsUrl: url(environment, "INTELLIGENCE_GATEWAY_WS_URL"), | ||
|
|
@@ -579,9 +606,17 @@ function runtimeCapabilities(environment: Environment): RuntimeCapabilities { | |
| .filter(([, value]) => !value) | ||
| .map(([name]) => name); | ||
|
|
||
| if (chosen === "standalone") { | ||
| if (missing.length < 4) { | ||
| throw new Error( | ||
| "OPENBOT_RUNTIME_MODE=standalone contradicts the Intelligence values that are set. Unset the INTELLIGENCE_* / COPILOTKIT_LICENSE_TOKEN values, or drop the mode.", | ||
| ); | ||
| } | ||
| return { mode: "standalone", durableHistory: false }; | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The server can choose this mode, but the shipped Helm chart still cannot render it: |
||
| } | ||
| if (missing.length > 0) { | ||
| throw new Error( | ||
| `CopilotKit Intelligence is required and is not configured. Missing: ${missing.join(", ")}`, | ||
| `CopilotKit Intelligence is not fully configured. Missing: ${missing.join(", ")}. Set all four, or set OPENBOT_RUNTIME_MODE=standalone to run without it.`, | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -861,6 +896,27 @@ export function loadConfig( | |
| const auth = authConfig(environment, google); | ||
| const managedAgent = managedAgentConfig(environment); | ||
| const workerSharedSecret = optional(environment, "WORKER_SHARED_SECRET"); | ||
| const runtime = runtimeCapabilities(environment); | ||
| // Zeroed HERE, where every consumer reads it, rather than warned about where only | ||
| // one does: the capability endpoint, the grant surface and the delivery loop all | ||
| // derive "may Bots hand work off" from these caps, and a standalone deployment has | ||
| // no runtime to deliver a hop through. Zeroing at the source keeps every one of | ||
| // those answers the same. Said out loud when somebody explicitly asked for it. | ||
| let handoff = handoffCaps(environment); | ||
| if ( | ||
| runtime.mode === "standalone" && | ||
| (handoff.maxDepth > 0 || handoff.maxPerRun > 0) | ||
| ) { | ||
| if ( | ||
| optional(environment, "BOT_HANDOFF_MAX_DEPTH") !== undefined || | ||
| optional(environment, "BOT_HANDOFF_MAX_PER_RUN") !== undefined | ||
| ) { | ||
| console.warn( | ||
| "BOT_HANDOFF_* is set, but a standalone deployment has no runtime to deliver a hop through; handing work between Bots is off.", | ||
| ); | ||
| } | ||
| handoff = { maxDepth: 0, maxPerRun: 0 }; | ||
| } | ||
|
|
||
| return { | ||
| databaseUrl: required(environment, "DATABASE_URL"), | ||
|
|
@@ -879,7 +935,7 @@ export function loadConfig( | |
| )?.replace(/\/+$/, ""), | ||
| tenantPackageDirectory: | ||
| optional(environment, "TENANT_PACKAGE_DIR") ?? "../examples/fintech", | ||
| runtime: runtimeCapabilities(environment), | ||
| runtime, | ||
| agentStallTimeoutMs: agentStallTimeoutMs(environment), | ||
| auditRetentionDays: auditRetentionDays(environment), | ||
| oauth: { google }, | ||
|
|
@@ -894,7 +950,7 @@ export function loadConfig( | |
| ? { appDistDir: optional(environment, "APP_DIST_DIR") as string } | ||
| : {}), | ||
| computer: computerConfig(environment), | ||
| handoff: handoffCaps(environment), | ||
| handoff, | ||
| ...(optional(environment, "AGENT_TOOL_TOKEN") | ||
| ? { agentToolToken: optional(environment, "AGENT_TOOL_TOKEN") as string } | ||
| : {}), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Standalone chart coverage looks good locally — validation refusals,
extraEnvguards, and the subprocess boot test all check out on head4bf956b. The one remaining gap called out here is still accurate: CI's chart matrix does not includestandaloneyet, so merges rely on this test (which skips when Helm is absent, as in the plain CI test job) rather than the workflow render/refusal job. Non-blocking once this lands, but worth tracking for when workflow permissions allow the matrix change.