Skip to content
Open
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ OWNER_ID=opendots-owner
# Required for an external HOST binding (24+ characters); enables local login too.
# OWNER_TOKEN=

# `copilotkit project select` rewrites this line to CPK_INTELLIGENCE_API_KEY.
# The server accepts either name and prefers a non-empty CPK_INTELLIGENCE_API_KEY.
INTELLIGENCE_API_KEY=
# INTELLIGENCE_API_URL=
# INTELLIGENCE_WS_URL=
Expand Down
1 change: 1 addition & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ services:
APP_ORIGIN: ${APP_ORIGIN:-http://localhost:4310}
OWNER_ID: ${OWNER_ID:-opendots-owner}
INTELLIGENCE_API_KEY: ${INTELLIGENCE_API_KEY:-}
CPK_INTELLIGENCE_API_KEY: ${CPK_INTELLIGENCE_API_KEY:-}
INTELLIGENCE_API_URL: ${INTELLIGENCE_API_URL:-}
INTELLIGENCE_WS_URL: ${INTELLIGENCE_WS_URL:-}
VOICE_API_KEY: ${VOICE_API_KEY:-}
Expand Down
7 changes: 5 additions & 2 deletions docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Edit `.env` on the server and restart after changes:
| `OWNER_TOKEN` | Application access token; required for external bindings |
| `APP_ORIGIN` | Exact browser origin when using a proxy or custom domain |

`npx copilotkit project select` deletes an `INTELLIGENCE_API_KEY` line and writes the same credential as `CPK_INTELLIGENCE_API_KEY`. The server reads either name. A non-empty `CPK_INTELLIGENCE_API_KEY` takes precedence over `INTELLIGENCE_API_KEY`.

The model environment variable names follow the configured provider adapter. Provider credentials belong in `.env`, not client-side variables or source code. Conversation history lives in the configured Intelligence project; copying the SQLite file alone does not back up that history.

## Pages and page conversations
Expand Down Expand Up @@ -135,8 +137,9 @@ For remote hosting, configure an HTTPS reverse proxy and the matching `APP_ORIGI
## Automatic Learning

OpenDots connects [CopilotKit Automatic Learning](https://docs.copilotkit.ai/learning)
to individual Dots. It uses the existing server-side `INTELLIGENCE_API_KEY` and
optional `INTELLIGENCE_API_URL`; no additional model key or frontend key is needed.
to individual Dots. It uses the server-side Intelligence credential
(`CPK_INTELLIGENCE_API_KEY` or `INTELLIGENCE_API_KEY`) and optional
`INTELLIGENCE_API_URL`; no additional model key or frontend key is needed.

1. Open **Learning** in the same Intelligence project and create a container for
one focused workflow, such as `research-workflow`. IDs use 1–64 lowercase
Expand Down
7 changes: 5 additions & 2 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { Runner } from './runner.js';
import { createApp } from './app.js';
import { WorkspaceStore } from './workspace.js';
import { Platform } from './platform.js';
import type { PlatformConfig } from './platform-config.js';
import {
intelligenceApiKeyFromEnv,
type PlatformConfig,
} from './platform-config.js';
const host = process.env.HOST ?? '127.0.0.1';
const port = Number(process.env.PORT ?? 4310);
const ownerToken = process.env.OWNER_TOKEN;
Expand All @@ -26,7 +29,7 @@ const workspace = new WorkspaceStore(
process.env.OWNER_ID ?? 'opendots-owner',
);
const config: PlatformConfig = {
intelligenceKey: process.env.INTELLIGENCE_API_KEY,
intelligenceKey: intelligenceApiKeyFromEnv(process.env),
intelligenceApiUrl: process.env.INTELLIGENCE_API_URL || undefined,
intelligenceWsUrl: process.env.INTELLIGENCE_WS_URL || undefined,
apiKey: process.env.OPENAI_API_KEY,
Expand Down
20 changes: 19 additions & 1 deletion src/server/platform-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import type { WebConfig } from './parallel.js';
import type { SetupStatus } from '../shared/types.js';

// `copilotkit project select` writes the CLI name and deletes the template name.
const INTELLIGENCE_API_KEY_ENV_NAMES = [
'CPK_INTELLIGENCE_API_KEY',
'INTELLIGENCE_API_KEY',
] as const;
export const INTELLIGENCE_KEY_MISSING_LABEL = `${INTELLIGENCE_API_KEY_ENV_NAMES[1]} (or ${INTELLIGENCE_API_KEY_ENV_NAMES[0]})`;

export function intelligenceApiKeyFromEnv(
env: Record<string, string | undefined>,
): string | undefined {
for (const name of INTELLIGENCE_API_KEY_ENV_NAMES) {
const value = env[name]?.trim();
if (value) return value;
}
return undefined;
}

export interface PlatformConfig extends WebConfig {
intelligenceKey?: string;
intelligenceApiUrl?: string;
Expand Down Expand Up @@ -29,7 +47,7 @@ export function setupStatus(
activationFailed = false,
): SetupStatus {
const missing = [
!config.intelligenceKey && 'INTELLIGENCE_API_KEY',
!config.intelligenceKey && INTELLIGENCE_KEY_MISSING_LABEL,
!config.apiKey && 'OPENAI_API_KEY',
!config.model && 'OPENAI_MODEL',
].filter((item): item is string => !!item);
Expand Down
8 changes: 6 additions & 2 deletions src/server/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import { Store } from './store.js';
import { WorkspaceStore } from './workspace.js';
import { DotAgent } from './dot-agent.js';
import { runThreadTurn } from './headless.js';
import { setupStatus, type PlatformConfig } from './platform-config.js';
import {
INTELLIGENCE_KEY_MISSING_LABEL,
setupStatus,
type PlatformConfig,
} from './platform-config.js';
import { validateRuntimeScope } from './runtime-scope.js';
import { learningSelector } from './learning.js';
export class Platform {
Expand Down Expand Up @@ -151,7 +155,7 @@ export class Platform {
async handle(request: Request): Promise<Response> {
if (!this.handler)
return Response.json(
{ error: 'Setup required: INTELLIGENCE_API_KEY.' },
{ error: `Setup required: ${INTELLIGENCE_KEY_MISSING_LABEL}.` },
{ status: 503 },
);
let body: unknown;
Expand Down
73 changes: 73 additions & 0 deletions tests/intelligence-key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect, it } from 'vitest';
import { Platform } from '../src/server/platform.js';
import {
INTELLIGENCE_KEY_MISSING_LABEL,
intelligenceApiKeyFromEnv,
setupStatus,
type PlatformConfig,
} from '../src/server/platform-config.js';
import { Store } from '../src/server/store.js';
import { WorkspaceStore } from '../src/server/workspace.js';

const configured: PlatformConfig = {
apiKey: 'fixture',
model: 'fixture',
baseUrl: 'https://example.com',
runtimeUrl: '',
voiceName: 'marin',
slackUsers: [],
};

it('accepts the CLI project key and still accepts the template name', () => {
expect(
intelligenceApiKeyFromEnv({ CPK_INTELLIGENCE_API_KEY: 'cpk-from-cli' }),
).toBe('cpk-from-cli');
expect(intelligenceApiKeyFromEnv({ INTELLIGENCE_API_KEY: 'legacy' })).toBe(
'legacy',
);
expect(
intelligenceApiKeyFromEnv({
CPK_INTELLIGENCE_API_KEY: 'cpk-from-cli',
INTELLIGENCE_API_KEY: 'legacy',
}),
).toBe('cpk-from-cli');
expect(
intelligenceApiKeyFromEnv({
CPK_INTELLIGENCE_API_KEY: ' ',
INTELLIGENCE_API_KEY: 'legacy',
}),
).toBe('legacy');
expect(intelligenceApiKeyFromEnv({})).toBeUndefined();
});

it('reports either accepted name from the same label used by setup status and the copilotkit error', async () => {
const key = intelligenceApiKeyFromEnv({
CPK_INTELLIGENCE_API_KEY: 'cpk-from-cli',
});
expect(setupStatus({ ...configured, intelligenceKey: key })).toMatchObject({
intelligence: true,
missing: [],
});
expect(INTELLIGENCE_KEY_MISSING_LABEL).toContain('CPK_INTELLIGENCE_API_KEY');
expect(INTELLIGENCE_KEY_MISSING_LABEL).toContain('INTELLIGENCE_API_KEY');
expect(setupStatus(configured).missing).toContain(
INTELLIGENCE_KEY_MISSING_LABEL,
);

const store = new Store(':memory:');
const workspace = new WorkspaceStore(':memory:', 'owner');
try {
const platform = new Platform(store, workspace, configured);
const response = await platform.handle(
new Request('http://127.0.0.1/api/copilotkit/info'),
);
expect(response.status).toBe(503);
expect(await response.json()).toEqual({
error: `Setup required: ${INTELLIGENCE_KEY_MISSING_LABEL}.`,
});
expect(platform.setup().missing).toContain(INTELLIGENCE_KEY_MISSING_LABEL);
} finally {
store.close();
workspace.close();
}
});
7 changes: 5 additions & 2 deletions tests/runtime-scope.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { expect, it } from 'vitest';
import { validateRuntimeScope } from '../src/server/runtime-scope.js';
import { WorkspaceStore } from '../src/server/workspace.js';
import { setupStatus } from '../src/server/platform-config.js';
import {
INTELLIGENCE_KEY_MISSING_LABEL,
setupStatus,
} from '../src/server/platform-config.js';
it('blocks unbound cross-Dot run and inspector routes before contacting Intelligence', () => {
const store = new WorkspaceStore(':memory:', 'owner');
const dot = store.dots()[0];
Expand Down Expand Up @@ -43,7 +46,7 @@ it('reports setup honestly without a standalone agent fallback', () => {
expect(status.intelligence).toBe(false);
expect(status.voice).toBe(false);
expect(status.slack).toBe('not_configured');
expect(status.missing).toContain('INTELLIGENCE_API_KEY');
expect(status.missing).toContain(INTELLIGENCE_KEY_MISSING_LABEL);
});
it('rejects stop scope bypasses and misleading prefixes while allowing canonical owned routes', () => {
const store = new WorkspaceStore(':memory:', 'owner');
Expand Down
6 changes: 5 additions & 1 deletion tests/setup.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, it } from 'vitest';
import {
INTELLIGENCE_KEY_MISSING_LABEL,
setupStatus,
type PlatformConfig,
} from '../src/server/platform-config.js';
Expand Down Expand Up @@ -37,7 +38,10 @@ it('requires Intelligence and model setup and disables voice when either is abse
voiceKey: 'fixture',
voiceModel: 'fixture',
}),
).toMatchObject({ missing: ['INTELLIGENCE_API_KEY'], voice: false });
).toMatchObject({
missing: [INTELLIGENCE_KEY_MISSING_LABEL],
voice: false,
});
});
it('reports activation failure until the SDK recovers online', () => {
const declared = {
Expand Down