Skip to content

Commit c19d065

Browse files
authored
fix(config): accept the Intelligence runtime key written by the CLI (#810)
* fix(config): accept the Intelligence runtime key written by the CLI * fix(desktop): keep selected Intelligence project authoritative * fix(setup): remove legacy key copy and clarify configuration errors
1 parent cd161d0 commit c19d065

14 files changed

Lines changed: 140 additions & 46 deletions

File tree

‎.env.example‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,13 @@ INTELLIGENCE_GATEWAY_WS_URL=wss://realtime.intelligence.copilotkit.ai
129129
#
130130
# npx --yes copilotkit@latest login # browser sign-in
131131
# npx --yes copilotkit@latest project select # writes the cpk-... key to .env as CPK_INTELLIGENCE_API_KEY
132-
# bun scripts/setup-learning.ts # copies it here, as INTELLIGENCE_API_KEY
132+
# bun scripts/setup-learning.ts # ensures the Learning container exists
133133
#
134-
# The server reads only INTELLIGENCE_API_KEY, so a key left as CPK_INTELLIGENCE_API_KEY is not seen.
134+
# OpenBot reads CPK_INTELLIGENCE_API_KEY directly. INTELLIGENCE_API_KEY is a legacy fallback.
135+
# When both are set, the non-empty CPK_INTELLIGENCE_API_KEY takes precedence.
135136
#
136137
# The runtime key is also under "API Keys" in your project at https://intelligence.copilotkit.ai
137-
INTELLIGENCE_API_KEY=
138+
CPK_INTELLIGENCE_API_KEY=
138139

139140
# Optional, and blank on purpose. Managed Intelligence derives entitlement from the project key
140141
# above and issues no licence token. A self-hosted Intelligence with its own licence sets this and

‎README.md‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,10 @@ not a production installation. Run its
138138
`npx copilotkit@latest local connect` from the OpenBot root, where `.env` is.
139139

140140
That command writes `INTELLIGENCE_API_URL`, `INTELLIGENCE_GATEWAY_WS_URL` and
141-
`CPK_INTELLIGENCE_API_KEY` to `.env`. OpenBot reads the first two as they are, but takes its project
142-
key only from `INTELLIGENCE_API_KEY` and refuses to start without it, so copy the `cpk-...` value
143-
across before `bash scripts/start.sh`:
144-
145-
```sh
146-
INTELLIGENCE_API_KEY=cpk-... # the value of CPK_INTELLIGENCE_API_KEY
147-
```
141+
`CPK_INTELLIGENCE_API_KEY` to `.env`. OpenBot reads those settings directly; no key
142+
copying or renaming is needed. `INTELLIGENCE_API_KEY` remains supported for older
143+
configurations. When both are set, the non-empty CLI key takes precedence so
144+
reconnecting uses the new project's key.
148145

149146
To give the Bots a Learning container on that stack, see
150147
[Automatic Learning](docs/automatic-learning.md).

‎desktop/src-tauri/src/env.rs‎

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -538,10 +538,11 @@ pub fn compose(
538538
"INTELLIGENCE_GATEWAY_WS_URL".into(),
539539
intelligence.gateway_ws_url.trim().to_string(),
540540
);
541-
env.insert(
542-
"INTELLIGENCE_API_KEY".into(),
543-
intelligence.api_key.trim().to_string(),
544-
);
541+
// Explicit desktop selection must override a CLI key left in the checkout's .env.
542+
// Supply both names for consumers that still read the legacy variable directly.
543+
for key in ["CPK_INTELLIGENCE_API_KEY", "INTELLIGENCE_API_KEY"] {
544+
env.insert(key.into(), intelligence.api_key.trim().to_string());
545+
}
545546

546547
/*
547548
* MINTED ONCE PER DEPLOYMENT, NOT ONCE PER START.
@@ -1344,6 +1345,42 @@ HTTPS_PROXY=http://proxy:8080
13441345
assert_eq!(env["OPENAI_API_KEY"], "sk-model");
13451346
}
13461347

1348+
#[test]
1349+
fn desktop_project_selection_replaces_and_purges_a_previous_cli_key() {
1350+
let dir = temp_root("env-cli-project-selection");
1351+
std::fs::create_dir_all(&dir).unwrap();
1352+
let path = dir.join(".env");
1353+
std::fs::write(
1354+
&path,
1355+
"CPK_INTELLIGENCE_API_KEY=cpk-old-cli-project\nINTELLIGENCE_API_KEY=cpk-old-legacy\nCUSTOM=keep-me\n",
1356+
)
1357+
.unwrap();
1358+
let selected = Intelligence {
1359+
api_url: "https://selected.example".into(),
1360+
gateway_ws_url: "wss://selected.example".into(),
1361+
api_key: " cpk-desktop-project ".into(),
1362+
};
1363+
let (settings, secrets) = crate::vault::split(compose(
1364+
&selected,
1365+
&Model::default(),
1366+
&engine_status(None),
1367+
&Ports::default(),
1368+
&pinned(),
1369+
None,
1370+
&BTreeMap::new(),
1371+
));
1372+
for key in ["CPK_INTELLIGENCE_API_KEY", "INTELLIGENCE_API_KEY"] {
1373+
assert_eq!(secrets[key], "cpk-desktop-project");
1374+
assert!(!settings.contains_key(key));
1375+
}
1376+
write(&path, &settings, &secrets).unwrap();
1377+
let saved = std::fs::read_to_string(&path).unwrap();
1378+
assert!(!saved.contains("INTELLIGENCE_API_KEY="));
1379+
assert!(saved.contains("CUSTOM=keep-me"));
1380+
assert!(saved.contains("INTELLIGENCE_API_URL=https://selected.example"));
1381+
std::fs::remove_dir_all(dir).unwrap();
1382+
}
1383+
13471384
#[test]
13481385
fn a_value_with_nothing_around_it_is_untouched() {
13491386
let env = compose(

‎desktop/src-tauri/src/vault.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub fn is_secret(key: &str) -> bool {
3131
key,
3232
// Somebody's own credentials, pasted or signed in for.
3333
"INTELLIGENCE_API_KEY"
34+
| "CPK_INTELLIGENCE_API_KEY"
3435
| "OPENAI_API_KEY"
3536
| "ANTHROPIC_API_KEY"
3637
| "CLAUDE_CODE_OAUTH_TOKEN"
@@ -1451,6 +1452,7 @@ mod tests {
14511452
fn every_credential_is_named_and_nothing_else_is() {
14521453
for key in [
14531454
"INTELLIGENCE_API_KEY",
1455+
"CPK_INTELLIGENCE_API_KEY",
14541456
"OPENAI_API_KEY",
14551457
"ANTHROPIC_API_KEY",
14561458
"CLAUDE_CODE_OAUTH_TOKEN",

‎docs/configuration.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ bash scripts/start.sh
2222
| `KEY_ENCRYPTION_KEY` | Base64-encoded 32-byte key for encrypted stored credentials. Generate with `openssl rand -base64 32`. |
2323
| `INTELLIGENCE_API_URL` | CopilotKit Intelligence API URL. |
2424
| `INTELLIGENCE_GATEWAY_WS_URL` | CopilotKit Intelligence realtime gateway URL. |
25-
| `INTELLIGENCE_API_KEY` | Runtime key for the Intelligence project. |
25+
| `CPK_INTELLIGENCE_API_KEY` | Runtime key written by the CopilotKit CLI. Preferred when non-empty. |
26+
| `INTELLIGENCE_API_KEY` | Legacy runtime key; used when `CPK_INTELLIGENCE_API_KEY` is empty or absent. |
2627

2728
All five above stop server startup if missing. The three `INTELLIGENCE_` values are additionally
2829
checked as a set, so a partial set is refused as a misconfiguration rather than treated as

‎prompt.txt‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ OpenBot root where `.env` is:
103103
npx copilotkit@latest local connect --approve-connection
104104

105105
`local connect` writes INTELLIGENCE_API_URL, INTELLIGENCE_GATEWAY_WS_URL and
106-
CPK_INTELLIGENCE_API_KEY to `.env`. OpenBot reads the first two as they are but takes its project
107-
key only from INTELLIGENCE_API_KEY, so copy the `cpk-...` value of CPK_INTELLIGENCE_API_KEY into
108-
INTELLIGENCE_API_KEY before `bash scripts/start.sh`. The local licence lasts 30 days;
106+
CPK_INTELLIGENCE_API_KEY to `.env`. OpenBot reads those values directly; do not copy or rename
107+
the key. INTELLIGENCE_API_KEY remains a legacy fallback when the CLI key is empty or absent.
108+
When both are set, the non-empty CLI key takes precedence. The local licence lasts 30 days;
109109
`npx copilotkit@latest local renew` renews it as often as needed while the account is in good
110110
standing. Skip the managed `project select` and setup-learning commands on this path.
111111

‎scripts/setup-learning.test.ts‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ test("fresh managed setup creates and verifies the selected project's container
132132
const content = await readFile(f.envPath, "utf8");
133133
expect(content).toContain("# Keep my other configuration");
134134
expect(parseEnv(content)).toMatchObject({
135-
INTELLIGENCE_API_KEY: key,
135+
CPK_INTELLIGENCE_API_KEY: key,
136+
INTELLIGENCE_API_KEY: "",
136137
CPK_INTELLIGENCE_LEARNING_CONTAINER_ID: "openbot",
137138
OPENAI_API_KEY: "synthetic-model",
138139
});
@@ -601,8 +602,27 @@ test("self-hosted setup failure or concurrent env edits do not save credentials"
601602
);
602603
});
603604

604-
test("a partial key provision cannot reuse a stale CLI key or create a container", async () => {
605-
const f = await fixture("CPK_INTELLIGENCE_API_KEY=cpk-old-project-key\n");
605+
test.each(["", "INTELLIGENCE_API_KEY=cpk-old-project-key\n"])(
606+
"a CLI key is used without reprovisioning or rewriting configuration: %s",
607+
async (legacy) => {
608+
const f = await fixture(`${legacy}CPK_INTELLIGENCE_API_KEY=cpk-cli-key\n`);
609+
const before = await readFile(f.envPath, "utf8");
610+
const view = keyView(["present"]);
611+
expect(
612+
await setupLearning({
613+
...f.options,
614+
runCli: f.runCli,
615+
keyIntelligence: view.keyIntelligence,
616+
}),
617+
).toBe("present");
618+
expect(view.keys).toEqual(["cpk-cli-key"]);
619+
expect(f.calls).toHaveLength(0);
620+
expect(await readFile(f.envPath, "utf8")).toBe(before);
621+
},
622+
);
623+
624+
test("a partial key provision does not create a container", async () => {
625+
const f = await fixture();
606626
const before = await readFile(f.envPath, "utf8");
607627
await expect(
608628
setupLearning({

‎scripts/setup-learning.ts‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFile, rename, rm, writeFile } from "node:fs/promises";
22
import { resolve } from "node:path";
33
import { createInterface } from "node:readline/promises";
44
import { parseEnv } from "node:util";
5+
import { intelligenceApiKey } from "../shared/intelligence-key";
56
import {
67
cliLoginPresent,
78
containerMissing,
@@ -136,7 +137,10 @@ export async function setupLearning(
136137
const effective = (name: string) =>
137138
(environment[name] || values[name] || "").trim();
138139
if (effective(CONTAINER)) return "preserved";
139-
const existingKey = effective("INTELLIGENCE_API_KEY");
140+
const existingKey = intelligenceApiKey({
141+
CPK_INTELLIGENCE_API_KEY: effective("CPK_INTELLIGENCE_API_KEY"),
142+
INTELLIGENCE_API_KEY: effective("INTELLIGENCE_API_KEY"),
143+
});
140144
if (existingKey) {
141145
/*
142146
* The key cannot create containers, and it is never replaced here. Creation goes through a
@@ -224,8 +228,7 @@ export async function setupLearning(
224228
"The selected project is invalid. Run npx --yes copilotkit@latest project select again.",
225229
);
226230
}
227-
// A saved record alone does not prove a stale CPK key belongs to it. Re-provision here.
228-
// CLI project select removes INTELLIGENCE_API_KEY, hence the precondition above.
231+
// No runtime key is available. Provision one for the saved project before assigning Learning.
229232
const provision = await run(
230233
["project", "select", "--project", projectId, "--json"],
231234
directory,
@@ -282,11 +285,7 @@ export async function setupLearning(
282285
"The CLI did not write a runtime key. Run project select and this helper again.",
283286
);
284287
}
285-
const content = upsert(
286-
upsert(current, "INTELLIGENCE_API_KEY", key),
287-
CONTAINER,
288-
"openbot",
289-
);
288+
const content = upsert(current, CONTAINER, "openbot");
290289
await saveEnvironment(envPath, content);
291290
return "configured";
292291
}

‎scripts/start-restart-guard.test.ts‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -351,17 +351,20 @@ describe("start.sh Learning container check", () => {
351351
* The server assigns `openbot` once it exists, but its runtime key cannot create it. start.sh asks
352352
* the setup helper, without a browser or a prompt, to make sure it exists, and relays its last line.
353353
*/
354-
test("runs the non-interactive check when a key is set and nothing is assigned", async () => {
355-
const result = await runStartWithStaleServerProbe(401, {
356-
settings: { INTELLIGENCE_API_KEY: "cpk-synthetic" },
357-
});
354+
test.each(["INTELLIGENCE_API_KEY", "CPK_INTELLIGENCE_API_KEY"])(
355+
"runs the non-interactive check when %s is set",
356+
async (keyName) => {
357+
const result = await runStartWithStaleServerProbe(401, {
358+
settings: { [keyName]: "cpk-synthetic" },
359+
});
358360

359-
expect(result.exitCode).toBe(0);
360-
expect(result.stdout).toContain(
361-
"LEARNING-CHECK scripts/setup-learning.ts --non-interactive",
362-
);
363-
expect(result.stdout).not.toContain("noise");
364-
});
361+
expect(result.exitCode).toBe(0);
362+
expect(result.stdout).toContain(
363+
"LEARNING-CHECK scripts/setup-learning.ts --non-interactive",
364+
);
365+
expect(result.stdout).not.toContain("noise");
366+
},
367+
);
365368

366369
test.each([
367370
[

‎scripts/start.sh‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ green " managed coworker endpoint: $MANAGED_AGENT_AG_UI_URL"
300300
LEARNING_CONTAINER="$(setting CPK_INTELLIGENCE_LEARNING_CONTAINER_ID "")"
301301
LEARNING_AUTO_ASSIGN="$(setting OPENBOT_LEARNING_AUTO_ASSIGN true)"
302302
if [ -z "$LEARNING_CONTAINER" ] && [ "$LEARNING_AUTO_ASSIGN" != "false" ] \
303-
&& [ "$LEARNING_AUTO_ASSIGN" != "0" ] && [ -n "$(setting INTELLIGENCE_API_KEY "")" ]; then
303+
&& [ "$LEARNING_AUTO_ASSIGN" != "0" ] && { [ -n "$(setting CPK_INTELLIGENCE_API_KEY "")" ] || [ -n "$(setting INTELLIGENCE_API_KEY "")" ]; }; then
304304
LEARNING_LINE="$(bun scripts/setup-learning.ts --non-interactive 2>>"$LOGS/learning-setup.log" | tail -1 || true)"
305305
info " ${LEARNING_LINE:-Automatic Learning: the container check did not run. Log: $LOGS/learning-setup.log}"
306306
fi
@@ -413,7 +413,7 @@ const status = info.licenseStatus;
413413
const agents = Object.keys(info.agents ?? {});
414414
if (status !== "valid") {
415415
console.log(`\x1b[31m licence is '${status}', not 'valid'.\x1b[0m`);
416-
console.log("\x1b[31m Check INTELLIGENCE_API_KEY: npx copilotkit@latest login && npx copilotkit@latest project select\x1b[0m");
416+
console.log("\x1b[31m Check CPK_INTELLIGENCE_API_KEY (or INTELLIGENCE_API_KEY): npx copilotkit@latest login && npx copilotkit@latest project select\x1b[0m");
417417
console.log("\x1b[31m See README.md for Intelligence setup.\x1b[0m");
418418
process.exit(1);
419419
}

0 commit comments

Comments
 (0)