feat: boot standalone when the Intelligence contract is absent - #2
Conversation
IshmaelRogers
left a comment
There was a problem hiding this comment.
Reviewed the stacked standalone-mode change against production wiring and deployment surfaces. The focused standalone/config tests, server typecheck, formatting, and lint pass. The inline findings identify cases where standalone is selected implicitly, still evaluates runtime code, advertises or exposes routines/handoffs that cannot execute, and cannot be rendered by the shipped Helm chart.
| .filter(([, value]) => !value) | ||
| .map(([name]) => name); | ||
|
|
||
| if (missing.length === 4) { |
There was a problem hiding this comment.
Please make standalone an explicit deployment choice rather than inferring it from complete absence. If a Kubernetes Secret or environment injection fails, all four values disappear together; this change converts the previous fail-fast crash into a healthy /health response with chat/thread/routine behavior missing. /api/capabilities exposes the mode, but existing liveness/readiness probes need not inspect it. This config already requires an explicit OPENBOT_SINGLE_USER choice when all identity-provider settings are absent; the same pattern (for example OPENBOT_RUNTIME_MODE=standalone) would let the enclave opt in without making a production secret outage look intentional.
| undefined; | ||
|
|
||
| const copilotRuntime = intelligence?.copilotRuntime; | ||
| const routineRunner = intelligence?.routineRunner; |
There was a problem hiding this comment.
This removes only the internal routine runner. createApp still receives routineStore unconditionally below, so /api/routines remains mounted in standalone and can re-enable an existing schedule. The worker reads that same database and will continue offering due runs to /internal/routines/run, which is now a 404, causing repeated dispatch failures. That also contradicts the new capability comment/tests saying routines are unmounted. Please gate the routine management surface/worker behavior for standalone (at minimum prevent enabling), and add coverage for /api/routines plus a due worker dispatch in this mode.
| .map(([name]) => name); | ||
|
|
||
| if (missing.length === 4) { | ||
| return { mode: "standalone", durableHistory: false }; |
There was a problem hiding this comment.
The server can choose this mode, but the shipped Helm chart still cannot render it: charts/openbot/templates/validation.yaml fails when the Intelligence URLs are absent, templates/secret.yaml requires both Intelligence secrets, and the values file documents them as mandatory. Please add a chart runtime mode and condition those validation/secret/env paths, with a Helm render test for standalone; otherwise this new deployment shape is available only through ad-hoc/manual startup.
| { createRuntimeAgentLoader }, | ||
| { createTurnRunner }, | ||
| ] = await Promise.all([ | ||
| import("@copilotkit/runtime/v2"), |
There was a problem hiding this comment.
This boundary does not actually keep the CopilotKit runtime out of standalone evaluation. index.ts statically imports createApp; app.ts statically imports createIntelligenceClient; and intelligence-client.ts statically imports CopilotKitIntelligence from @copilotkit/runtime/v2. The focused test also imports copilot.ts directly, so it cannot prove the stated invariant. Even if the reduced graph happens to boot today, standalone remains coupled to the dependency whose module graph this change says it must avoid. Please inject/build the thread reader in the intelligence branch (or dynamically import intelligence-client.ts) and add a subprocess boot test of src/index.ts.
| if ( | ||
| config.handoff.maxDepth > 0 && | ||
| config.handoff.maxPerRun > 0 && | ||
| !intelligence |
There was a problem hiding this comment.
A warning does not make the exposed capability consistent with runtime behavior. Handoff caps default on, and createApp still derives handoff.enabled solely from those caps and permits the related grant surface, while this branch starts no delivery loop. Standalone clients/admins are therefore told a capability is enabled even though every offered hop is undeliverable. Please derive the effective handoff capability from runtime mode plus caps and use that same value for API responses, grant mutations/tool exposure, and listener startup.
95f5d76 to
bf57efb
Compare
|
All five findings addressed in
Gate: |
IshmaelRogers
left a comment
There was a problem hiding this comment.
Re-reviewed commit bf57efb. The five original implementation findings are addressed. Server typecheck, formatting and lint pass; 81 focused tests pass. The follow-up comments cover two reproducible prerequisite-skip failures on Bun 1.3.14/1.4.0, missing enforced Helm CI coverage, runtime-mode overrides through extraEnv, and production-wiring assertions absent from the subprocess test.
| } | ||
|
|
||
| test("the standalone chart target renders without Intelligence and refuses contradictions", { | ||
| skip: helm ? false : "helm is not installed", |
There was a problem hiding this comment.
This does not skip when Helm is absent: Bun's skip option is boolean, so the non-empty reason string is not treated as true. I reproduced the test invoking Bun.spawn("helm", ...) and failing with ENOENT on both the pinned Bun 1.3.14 and Bun 1.4.0. Please use skip: !helm (put the reason in the test name/comment, or use a conditional test.skip). Also, once this is fixed, the normal test CI job has no Helm and will skip this test while the chart matrix still omits standalone, leaving the new render untested in CI; please add standalone to that matrix.
| const databaseUrl = process.env.DATABASE_URL; | ||
|
|
||
| test("src/index.ts boots standalone as a real process", { | ||
| skip: databaseUrl ? false : "DATABASE_URL is not set", |
There was a problem hiding this comment.
The same string-valued skip issue makes this run without a database instead of skipping. On both Bun 1.3.14 and 1.4.0, with no DATABASE_URL, I reproduced the child starting with an empty value and the test failing at DATABASE_URL must be configured. Please make this boolean (skip: !databaseUrl) or conditionally register test.skip.
There was a problem hiding this comment.
config.extraEnv is appended after the generated runtime variables (around line 319), and this chart intentionally lets the last duplicate name win. In standalone, an extra OPENBOT_RUNTIME_MODE, INTELLIGENCE_*, or COPILOTKIT_LICENSE_TOKEN entry bypasses template validation and can override/contradict the selected mode; the pod then crash-loops or boots in a different mode than the chart validated. Please reject these reserved names in standalone validation (and test the refusal), or render the mode-defining variables after the operator escape hatch.
| expect(capabilities.mode).toBe("standalone"); | ||
| expect(capabilities.durableHistory).toBe(false); | ||
|
|
||
| const chat = await fetch( |
There was a problem hiding this comment.
This subprocess test is the only one exercising production index.ts wiring, but it asserts only the chat route. The focused route tests manufacture 404s by calling createApp(config) without threadReader or routineStore, so they would still pass if index.ts accidentally supplied either collaborator again. Please probe /api/threads/... and /api/routines on this spawned server as well, so the two wiring fixes are actually regression-tested.
bf57efb to
4bf956b
Compare
|
All four follow-ups addressed in 1+2. The skips are real now — you were right on both: Bun's Still outstanding and still out of my reach: adding Gate: |
IshmaelRogers
left a comment
There was a problem hiding this comment.
Round 3 re-review (4bf956b)
All substantive review findings from the prior rounds appear resolved on this head:
- Explicit opt-in:
OPENBOT_RUNTIME_MODE=standaloneis required; partial Intelligence config still fails fast. - Surface gating:
routineStoreandthreadReaderare withheld in standalone; subprocess boot test now probes/api/threadsand/api/routines(404) in addition to chat. - Helm: standalone values target, validation refusals (mode contradictions, routines CronJob, reserved
extraEnvnames), and conditional Intelligence secrets. - Import graph: Intelligence wiring behind dynamic imports; standalone avoids the MCP/eventsource crash.
- Handoff: caps zeroed in standalone so the capability is not advertised undeliverably.
- Test hygiene:
test.skipIfreplaces stringskipvalues.
Local checks: standalone-runtime.test.ts + gateway tests pass; helm-standalone.test.ts and standalone-boot.integration.test.ts use correct skip guards (Helm/Postgres absent in this environment).
Non-blocking follow-up: CI chart matrix still omits standalone (noted in standalone-values.yaml); local render test covers it until workflow permissions allow the matrix update.
| # 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 |
There was a problem hiding this comment.
Standalone chart coverage looks good locally — validation refusals, extraEnv guards, and the subprocess boot test all check out on head 4bf956b. The one remaining gap called out here is still accurate: CI's chart matrix does not include standalone yet, 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.
4bf956b to
8c940e1
Compare
|
Nothing new to fix here — round 3 raised no implementation findings on this branch. Rebased onto #1's fix ( Gate at One thing worth naming rather than quietly re-running: the first full-suite run after the rebase had a single failure, in Still outstanding and still out of my reach: |
RuntimeCapabilities grows the second mode its own comment predicted. All four INTELLIGENCE_*/COPILOTKIT_* values absent now means standalone - a working deployment with the admin, people, computer and plugin surfaces, and no chat runtime - rather than a refusal to boot. A partial set is still refused, naming what is missing and both valid shapes. - The chat runtime, threads and routines are unmounted in standalone, so those paths 404 by design instead of mounting doors that refuse; the handoff delivery loop says out loud that it stays off when configured on. createApp already treated all three as optional, so this is wiring, not route surgery. - mountCopilotRuntime guards and narrows: mounting it without the contract fails in front of the deployer, per the old single-mode comment's promise. - The Intelligence-bound wiring in index.ts (agent loader, buildAgentFor, the headless-turn pair, the runtime mount) moved behind dynamic imports inside the intelligence branch. Load-bearing, not tidy: that import graph reaches @modelcontextprotocol/sdk's CommonJS SSE client, whose require() of the ESM-only eventsource package crashed Bun at import time - in BOTH modes, on 1.3.14 and 1.4.0, before this change. Standalone now never evaluates the graph; intelligence mode loads it dynamically, which Bun handles, so both modes boot where neither did.
- Standalone is now an explicit choice: OPENBOT_RUNTIME_MODE=standalone, the same shape as OPENBOT_SINGLE_USER. Total absence of the Intelligence values without it stays a refusal to boot - a Kubernetes Secret that fails to mount must crash in front of somebody, not come up 'healthy' with the chat runtime quietly missing. Chosen standalone with Intelligence values set is refused as the contradiction it is. - The routine surface is withheld in standalone: createApp gets no routineStore, so /api/routines is unmounted and no schedule can be enabled for a worker to dispatch into a 404. Routine tools follow. - Handoff caps are zeroed at the source in standalone (warned when env explicitly set them), so the capability endpoint, the grant surface and the delivery loop all give the same answer instead of a warning only one of them reflected. - app.ts no longer imports the intelligence client: the thread reader is built inside index.ts's intelligence branch (dynamic imports) and injected as createApp's trailing optional parameter; the standalone suite's runtime-mount test now imports copilot.ts dynamically too, and a subprocess boot test drives the real src/index.ts module graph. - The Helm chart can render the mode: config.runtimeMode conditions the validation, the Secret's intelligence keys, and the env injection (OPENBOT_RUNTIME_MODE replaces the four INTELLIGENCE_* vars); a standalone ci values target plus a render test cover it, including the contradiction and routines refusals. Adding the target to the CI matrix needs the workflow permission and is left to a maintainer.
- Both prerequisite skips used Bun's skip option with a reason string, which is not treated as true - the tests ran and failed on ENOENT / an empty DATABASE_URL exactly as the review reproduced. Now test.skipIf, and both verified to genuinely skip without their prerequisite. - config.extraEnv renders after the generated variables and the last name wins - deliberately, except for the variables that define which runtime the pod IS. Standalone validation now refuses extraEnv entries naming OPENBOT_RUNTIME_MODE or the four Intelligence variables, so the escape hatch cannot boot a pod in a mode the chart never validated. Harmless extraEnv entries still render; both directions tested. - The subprocess boot test now probes /api/threads and /api/routines on the spawned server: the focused route tests build createApp without those collaborators themselves, so only this proves index.ts's real wiring withholds them.
8c940e1 to
27f1edc
Compare
Second slice of the bit-mind CopilotKit#20 enclave work, stacked on #1. The server now boots without the Intelligence contract — the M0 milestone bit-mind's service specification asks of this fork ("boots with Intelligence deleted, chat routes removed, admin working; threads/chat are 404 by design"), done additively so the fork stays rebasable on upstream.
What changes
RuntimeCapabilitiesbecomes the union its own comment predicted. All fourINTELLIGENCE_*/COPILOTKIT_LICENSE_TOKENvalues absent now means{ mode: "standalone", durableHistory: false }— a working deployment with the admin, people, computer and plugin surfaces and no chat runtime — instead of a refusal to boot. A partial set is still refused, naming what is missing and both valid shapes. This deliberately reverses the old "refuses to start when Intelligence is absent entirely, rather than degrading" test; the replacement test says why, and nothing degrades silently — the mode is stated on/api/capabilitiesand the absent surfaces are unmounted rather than mounted-and-refusing./api/threads, and the routine runner all stay off in standalone (createAppalready treated each as optional — this is wiring, not route surgery). The handoff delivery loop logs that it stays off if a deployment configures it on without a runtime.mountCopilotRuntimenow guards and narrows, so mounting it without the contract fails in front of the deployer — the exact guard the old single-mode comment inindex.tspromised.buildAgentFor, the headless-turn pair, and the runtime mount, verbatim. This is load-bearing rather than tidy — see the find below.A pre-existing boot crash, found and fixed in passing
While verifying,
bun src/index.tson the unmodified base branch failed to boot at import time in every mode, on both the pinned Bun 1.3.14 and 1.4.0:The runtime's import graph reaches the MCP SDK's CommonJS SSE client, which
require()s the ESM-onlyeventsourcepackage; whether Bun tolerates that turns out to depend on module-graph shape (each edge imports cleanly in isolation — the fullindex.tsgraph does not). Moving the cluster to dynamicimport()sidesteps it: standalone never evaluates the graph at all, and intelligence mode loads it through the path Bun handles. Both modes now boot where neither did. CI never caught this because it never boots the server — worth a follow-up smoke job once Actions are available.Verified how
Workspace gate:
format=ok lint=ok typecheck=ok test=ok— 1663 pass / 0 fail (five new standalone tests; the two config tests that pinned the old single-mode behaviour updated with the reasoning in-line).Live boot, standalone (pgvector Postgres, no Intelligence vars, single-user):
Live boot, intelligence mode (all four values set): boots and reports
{"mode":"intelligence","durableHistory":true,...}— previously an import-time crash on this box.Not in this slice