Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ jobs:
with:
bun-version: 1.3.14
- run: bun install --frozen-lockfile
# The Bots are not root workspaces and each keeps its own lockfile. The startup tests spawn
# the real entrypoint, so its dependencies have to be installed as well.
- run: bun install --frozen-lockfile
working-directory: agent-bot
# Not the db:migrate script: that one loads ../.env, which does not exist in CI. DATABASE_URL
# comes from the job env instead, which drizzle.config.ts already reads.
- run: bunx drizzle-kit migrate --config=drizzle.config.ts
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ 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 built-in Bot refuses to start without `OPENAI_API_KEY`. It used to start, report healthy, and
then fail every conversation, so a missing key looked like a working deployment. The LangGraph Bot
already refused the same way.

Sessions survive and nobody signs in again.

### Added
Expand Down
19 changes: 18 additions & 1 deletion agent-bot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,25 @@ const MODEL = process.env.BOT_MODEL ?? "gpt-5.5";
*/
const BASE_URL = process.env.OPENAI_BASE_URL?.trim() || undefined;

/**
* The key that model is answered with, checked at startup rather than on the first conversation.
*
* Without the check the Bot starts, answers the healthcheck, and then fails every run, so the
* compose healthcheck reports a Bot that cannot answer as healthy. The LangGraph Bot already
* refuses to start without its provider's key, and this file already refuses without its token
* above; the model key was the one configuration that escaped the same posture. A missing key
* should fail in front of whoever is deploying, not in front of whoever is asking.
*/
const API_KEY = process.env.OPENAI_API_KEY?.trim();
if (!API_KEY) {
console.error(
"OPENAI_API_KEY is not set. This Bot cannot answer without a model.",
);
process.exit(1);
}

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
apiKey: API_KEY,
baseURL: BASE_URL,
});

Expand Down
57 changes: 57 additions & 0 deletions tests/agent-bot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect, test } from "bun:test";
import { join } from "node:path";

/**
* The compose healthcheck asks `/health`, and `/health` answers without consulting the model key.
* The refusal to run without one therefore has to happen before the server listens: a Bot that
* cannot answer should not be running, let alone reporting healthy. These spawn the real
* entrypoint with the environment compose would hand it.
*/

async function startBot(environment: Record<string, string>) {
const proc = Bun.spawn(
["bun", join(import.meta.dir, "..", "agent-bot", "src", "index.ts")],
{
// Every variable the repository's `.env` could inject is named explicitly: bun loads that
// file into the child, and a leaked token or key would let a configuration under test pass
// a check it is supposed to fail.
env: {
PATH: process.env.PATH ?? "",
MANAGED_AGENT_TOKEN: "",
OPENAI_API_KEY: "",
...environment,
},
stdout: "pipe",
stderr: "pipe",
},
);

// Both configurations under test exit before the server listens. If one reaches `serve` anyway,
// kill it so the test fails on the missing refusal rather than on bun's test timeout.
const killer = setTimeout(() => proc.kill(), 5_000);
const exitCode = await proc.exited;
clearTimeout(killer);
const stderr = await new Response(proc.stderr).text();
return { exitCode, stderr };
}

test("agent-bot refuses to start when the model key is empty", async () => {
// Compose passes `${OPENAI_API_KEY:-}`, so an unset key arrives as an empty
// string rather than missing altogether.
const { exitCode, stderr } = await startBot({
MANAGED_AGENT_TOKEN: "test-token",
OPENAI_API_KEY: "",
});

expect(exitCode).toBe(1);
expect(stderr).toContain("OPENAI_API_KEY is not set");
});

test("agent-bot still refuses to start without its server token", async () => {
const { exitCode, stderr } = await startBot({
OPENAI_API_KEY: "sk-test",
});

expect(exitCode).toBe(1);
expect(stderr).toContain("MANAGED_AGENT_TOKEN is not set");
});