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
7 changes: 6 additions & 1 deletion app/api/mcp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { resolvePandoraMcpPrincipal } from "@/lib/services/mcp-auth";
import { pandoraMcpPublicOrigin } from "@/lib/services/mcp-oauth";
import { createPandoraMcpServer } from "@/lib/services/pandora-mcp-server";
import type { MemoryBridgeDbClient } from "@/lib/services/memory-bridge-service";
import { getPandoraMcpDbKey, getPandoraSupabaseUrl } from "@/lib/services/pandora-mcp-env";

export const dynamic = "force-dynamic";
export const runtime = "nodejs";
Expand All @@ -27,7 +28,11 @@ function jsonError(failure: Exclude<ReturnType<typeof resolvePandoraMcpPrincipal
}

function createMcpClient() {
return createClient(process.env.NEXT_PUBLIC_SUPABASE_URL ?? "", process.env.PANDORA_MCP_DB_KEY ?? "", { auth: { autoRefreshToken: false, persistSession: false }, global: { headers: { "x-pandora-bridge": "phase-4b-mcp" } } }) as unknown as MemoryBridgeDbClient;
const url = getPandoraSupabaseUrl();
if (!url.ok) throw new Error(url.message);
const dbKey = getPandoraMcpDbKey();
if (!dbKey.ok) throw new Error(dbKey.message);
return createClient(url.value, dbKey.value, { auth: { autoRefreshToken: false, persistSession: false }, global: { headers: { "x-pandora-bridge": "phase-4b-mcp", "x-pandora-db-key-env": dbKey.envVar } } }) as unknown as MemoryBridgeDbClient;
}

async function handle(request: Request) {
Expand Down
22 changes: 13 additions & 9 deletions lib/services/mcp-auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { timingSafeEqual } from "node:crypto";
import { verifyPandoraMcpOAuthAccessToken } from "@/lib/services/mcp-oauth";
import { getPandoraMcpBearerSecret, getPandoraMcpDbKey } from "@/lib/services/pandora-mcp-env";

export type PandoraMcpPrincipal =
| { ok: true; authType: "mcp_bearer_token" | "mcp_oauth_access_token"; userId: string }
Expand All @@ -21,18 +22,21 @@ function safeEqual(a: string, b: string) {

export function resolvePandoraMcpPrincipal(request: Request, env: Partial<NodeJS.ProcessEnv> = process.env): PandoraMcpPrincipal {
if (env.PANDORA_ENABLE_MCP !== "true") return { ok: false, status: 403, code: "mcp_disabled", message: "Pandora MCP is disabled." };
const configuredToken = env.PANDORA_MCP_TOKEN;
const configuredToken = getPandoraMcpBearerSecret(env);
const suppliedToken = bearerToken(request);
if (!configuredToken || !suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
if (safeEqual(suppliedToken, configuredToken)) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Pandora MCP user id is not configured." };
if (!env.PANDORA_MCP_DB_KEY) return { ok: false, status: 403, code: "mcp_db_key_missing", message: "Pandora MCP database key is not configured." };
if (!configuredToken.ok) return { ok: false, status: 403, code: "mcp_token_env_missing", message: configuredToken.message };
if (!suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
if (safeEqual(suppliedToken, configuredToken.value)) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_bearer_token", userId: env.PANDORA_MCP_USER_ID };
}
const oauth = verifyPandoraMcpOAuthAccessToken(suppliedToken, env);
if (!oauth.ok) return { ok: false, status: 401, code: "mcp_token_invalid", message: "MCP bearer token is invalid." };
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Pandora MCP user id is not configured." };
if (!env.PANDORA_MCP_DB_KEY) return { ok: false, status: 403, code: "mcp_db_key_missing", message: "Pandora MCP database key is not configured." };
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_oauth_access_token", userId: oauth.payload.user_id };
Comment on lines +25 to 40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing bearer-secret env blocks OAuth authentication entirely.

resolvePandoraMcpPrincipal returns mcp_token_env_missing at Line 27 as soon as getPandoraMcpBearerSecret fails, before ever checking suppliedToken or reaching the OAuth branch (Line 35). Since OAuth (verifyPandoraMcpOAuthAccessToken) verifies against a completely independent secret, a deployment that intentionally relies solely on OAuth (no PANDORA_MCP_TOKEN/PANDORA_MCP_API_KEY/PANDORA_API_KEY/MEMORY_API_KEY set) will have every request — including ones bearing a perfectly valid OAuth access token — rejected with a "missing server env" error. OAuth-only auth becomes impossible.

Reorder so OAuth is attempted whenever the static secret doesn't match/isn't configured, and only report mcp_token_env_missing as the final fallback when both mechanisms fail.

🔧 Proposed fix
 export function resolvePandoraMcpPrincipal(request: Request, env: Partial<NodeJS.ProcessEnv> = process.env): PandoraMcpPrincipal {
   if (env.PANDORA_ENABLE_MCP !== "true") return { ok: false, status: 403, code: "mcp_disabled", message: "Pandora MCP is disabled." };
   const configuredToken = getPandoraMcpBearerSecret(env);
   const suppliedToken = bearerToken(request);
-  if (!configuredToken.ok) return { ok: false, status: 403, code: "mcp_token_env_missing", message: configuredToken.message };
   if (!suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
-  if (safeEqual(suppliedToken, configuredToken.value)) {
+  if (configuredToken.ok && safeEqual(suppliedToken, configuredToken.value)) {
     if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
     const dbKey = getPandoraMcpDbKey(env);
     if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
     return { ok: true, authType: "mcp_bearer_token", userId: env.PANDORA_MCP_USER_ID };
   }
   const oauth = verifyPandoraMcpOAuthAccessToken(suppliedToken, env);
-  if (!oauth.ok) return { ok: false, status: 401, code: "mcp_token_invalid", message: "MCP bearer token is invalid." };
-  if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
-  const dbKey = getPandoraMcpDbKey(env);
-  if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
-  return { ok: true, authType: "mcp_oauth_access_token", userId: oauth.payload.user_id };
+  if (oauth.ok) {
+    if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
+    const dbKey = getPandoraMcpDbKey(env);
+    if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
+    return { ok: true, authType: "mcp_oauth_access_token", userId: oauth.payload.user_id };
+  }
+  if (!configuredToken.ok) return { ok: false, status: 403, code: "mcp_token_env_missing", message: configuredToken.message };
+  return { ok: false, status: 401, code: "mcp_token_invalid", message: "MCP bearer token is invalid." };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const configuredToken = getPandoraMcpBearerSecret(env);
const suppliedToken = bearerToken(request);
if (!configuredToken || !suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
if (safeEqual(suppliedToken, configuredToken)) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Pandora MCP user id is not configured." };
if (!env.PANDORA_MCP_DB_KEY) return { ok: false, status: 403, code: "mcp_db_key_missing", message: "Pandora MCP database key is not configured." };
if (!configuredToken.ok) return { ok: false, status: 403, code: "mcp_token_env_missing", message: configuredToken.message };
if (!suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
if (safeEqual(suppliedToken, configuredToken.value)) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_bearer_token", userId: env.PANDORA_MCP_USER_ID };
}
const oauth = verifyPandoraMcpOAuthAccessToken(suppliedToken, env);
if (!oauth.ok) return { ok: false, status: 401, code: "mcp_token_invalid", message: "MCP bearer token is invalid." };
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Pandora MCP user id is not configured." };
if (!env.PANDORA_MCP_DB_KEY) return { ok: false, status: 403, code: "mcp_db_key_missing", message: "Pandora MCP database key is not configured." };
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_oauth_access_token", userId: oauth.payload.user_id };
const configuredToken = getPandoraMcpBearerSecret(env);
const suppliedToken = bearerToken(request);
if (!suppliedToken) return { ok: false, status: 401, code: "mcp_token_missing", message: "MCP bearer token is required." };
if (configuredToken.ok && safeEqual(suppliedToken, configuredToken.value)) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_bearer_token", userId: env.PANDORA_MCP_USER_ID };
}
const oauth = verifyPandoraMcpOAuthAccessToken(suppliedToken, env);
if (oauth.ok) {
if (!env.PANDORA_MCP_USER_ID) return { ok: false, status: 403, code: "mcp_user_id_missing", message: "Missing server env: PANDORA_MCP_USER_ID" };
const dbKey = getPandoraMcpDbKey(env);
if (!dbKey.ok) return { ok: false, status: 403, code: "mcp_db_key_missing", message: dbKey.message };
return { ok: true, authType: "mcp_oauth_access_token", userId: oauth.payload.user_id };
}
if (!configuredToken.ok) return { ok: false, status: 403, code: "mcp_token_env_missing", message: configuredToken.message };
return { ok: false, status: 401, code: "mcp_token_invalid", message: "MCP bearer token is invalid." };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/mcp-auth.ts` around lines 25 - 40, `resolvePandoraMcpPrincipal`
currently returns `mcp_token_env_missing` before it can try OAuth, which blocks
OAuth-only deployments. Update the auth flow so `getPandoraMcpBearerSecret` is
only required for the static bearer-token path in `resolvePandoraMcpPrincipal`,
then always fall through to `verifyPandoraMcpOAuthAccessToken` when the
configured token is missing or doesn’t match. Keep the existing `authType` and
`userId` checks, and make `mcp_token_env_missing` the final fallback only after
both bearer-token and OAuth validation fail.

}

Expand All @@ -43,11 +47,11 @@ export function requirePandoraMcpPrincipal(request: Request, env: Partial<NodeJS
}

export function requireMcpCaptureEnabled(env: Partial<NodeJS.ProcessEnv> = process.env) {
if (env.PANDORA_ENABLE_MCP_CAPTURE !== "true") return { ok: false as const, status: 403 as const, code: "mcp_capture_disabled", message: "Pandora MCP capture is disabled." };
if (env.PANDORA_ENABLE_MCP_CAPTURE !== "true") return { ok: false as const, status: 403 as const, code: "mcp_capture_disabled", message: "capture_disabled: PANDORA_ENABLE_MCP_CAPTURE is not true" };
return { ok: true as const };
}

export function requireMcpDistillationEnabled(env: Partial<NodeJS.ProcessEnv> = process.env) {
if (env.PANDORA_ENABLE_MCP_DISTILLATION !== "true") return { ok: false as const, status: 403 as const, code: "mcp_distillation_disabled", message: "Pandora MCP distillation is disabled." };
if (env.PANDORA_ENABLE_MCP_DISTILLATION !== "true") return { ok: false as const, status: 403 as const, code: "mcp_distillation_disabled", message: "distillation_disabled: PANDORA_ENABLE_MCP_DISTILLATION is not true" };
return { ok: true as const };
}
23 changes: 23 additions & 0 deletions lib/services/pandora-mcp-env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type PandoraMcpEnvStatus = { ok: true; value: string; envVar: string } | { ok: false; envVar: string; aliases: string[]; message: string };

function firstPresent(env: Partial<NodeJS.ProcessEnv>, names: string[]): PandoraMcpEnvStatus {
const found = names.find((name) => Boolean(env[name]));
if (found) return { ok: true, value: String(env[found]), envVar: found };
return { ok: false, envVar: names[0], aliases: names.slice(1), message: `Missing server env: ${names[0]}` };
}

export function getPandoraMcpBearerSecret(env: Partial<NodeJS.ProcessEnv> = process.env) {
return firstPresent(env, ["PANDORA_MCP_TOKEN", "PANDORA_MCP_API_KEY", "PANDORA_API_KEY", "MEMORY_API_KEY"]);
}

export function getPandoraMcpDbKey(env: Partial<NodeJS.ProcessEnv> = process.env) {
return firstPresent(env, ["PANDORA_MCP_DB_KEY", "PANDORA_MEMORY_BRIDGE_DB_KEY", "SUPABASE_SERVICE_ROLE_KEY"]);
}
Comment on lines +13 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

DB key fallback widens blast radius to full service-role privileges.

getPandoraMcpDbKey now silently falls back to SUPABASE_SERVICE_ROLE_KEY when no dedicated PANDORA_MCP_DB_KEY/PANDORA_MEMORY_BRIDGE_DB_KEY is set. Per the line-range change details, the prior code read env.PANDORA_MCP_DB_KEY directly with no such fallback — this is new exposure. The Supabase service-role key always bypasses Row Level Security, so any deployment that hasn't set a dedicated scoped key will have the public-facing MCP route (app/api/mcp/route.ts) operate with full, RLS-bypassing database privileges instead of failing closed with a clear "missing dedicated key" error.

Consider making the service-role fallback opt-in (e.g., a separate explicit flag) rather than a silent alias, so misconfigured deployments fail loudly instead of quietly running with elevated privileges.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/pandora-mcp-env.ts` around lines 13 - 15, The new fallback in
getPandoraMcpDbKey silently promotes the MCP route to use
SUPABASE_SERVICE_ROLE_KEY, which broadens access instead of failing closed.
Update getPandoraMcpDbKey so it only returns a dedicated PANDORA_MCP_DB_KEY or
PANDORA_MEMORY_BRIDGE_DB_KEY by default, and make any use of
SUPABASE_SERVICE_ROLE_KEY explicit and opt-in via a separate flag or
configuration path. Ensure the app/api/mcp/route.ts flow surfaces a clear
missing-key error when no dedicated key is configured rather than silently using
the service-role secret.


export function getPandoraSupabaseUrl(env: Partial<NodeJS.ProcessEnv> = process.env) {
return firstPresent(env, ["NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_URL"]);
}

export function presentEnvName(env: Partial<NodeJS.ProcessEnv>, names: string[]) {
return names.find((name) => Boolean(env[name])) ?? null;
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"db:push": "supabase db push --dry-run",
"db:pull": "supabase db pull --schema public",
"verify:first-reviewed-memory-fixture": "npx --yes tsx scripts/verify-first-reviewed-memory-fixture.ts",
"env:policy": "npx --yes tsx scripts/check-env-policy.ts"
"env:policy": "npx --yes tsx scripts/check-env-policy.ts",
"memory:smoke": "npx --yes tsx scripts/memory-smoke.ts",
"memory:diagnostics": "npx --yes tsx scripts/memory-smoke.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
Expand Down
37 changes: 37 additions & 0 deletions scripts/memory-smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createClient } from "@supabase/supabase-js";
import { getPandoraMcpDbKey, getPandoraSupabaseUrl, presentEnvName } from "../lib/services/pandora-mcp-env";
import { analyzeMemoryCandidatesTool, captureAdaptiveMemoryTool, captureMemoryEventTool, createSessionDigestTool, distillContextPackTool, getAdaptiveContextTool, getLatestContextPackTool, getMemoryContextTool, getOpenLoopsTool } from "../lib/services/pandora-mcp-tools";
import type { MemoryBridgeDbClient } from "../lib/services/memory-bridge-service";

const seed = "Creative workflow preference: preserve continuity and user feedback for future writing.";
const ns = (process.env.PANDORA_MEMORY_SMOKE_NAMESPACE === "au" ? "au" : "real_life") as "real_life" | "au";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Default namespace risks polluting/leaking real data.

ns defaults to "real_life" unless PANDORA_MEMORY_SMOKE_NAMESPACE is explicitly set to "au" (Line 7). Since this script performs real writes (captureAdaptiveMemoryTool with auto_capture_allowed, captureMemoryEventTool, createSessionDigestTool) and reads (getMemoryContextTool, getAdaptiveContextTool, distillContextPackTool) against whatever Supabase instance the env points to, running npm run memory:smoke/memory:diagnostics without remembering to set the namespace env var will inject synthetic seed memories into the real_life namespace, and step() logs up to 500 chars of each tool's JSON response (Line 14) — which could include genuine pre-existing personal memory content pulled from real_life — straight to stdout/CI logs.

Default to the safe test namespace and require explicit opt-in to touch real_life.

🔧 Proposed fix
-const ns = (process.env.PANDORA_MEMORY_SMOKE_NAMESPACE === "au" ? "au" : "real_life") as "real_life" | "au";
+const ns = (process.env.PANDORA_MEMORY_SMOKE_NAMESPACE === "real_life" ? "real_life" : "au") as "real_life" | "au";

Also applies to: 21-35

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/memory-smoke.ts` at line 7, The namespace selection in
memory-smoke.ts defaults to the production-like real_life namespace, which can
leak or pollute real data when the smoke/diagnostics flow runs. Update the ns
initialization logic so the safe test namespace (au) is the default, and only
switch to real_life when there is an explicit opt-in via
PANDORA_MEMORY_SMOKE_NAMESPACE; make sure the same guard applies to the tool
sequence in step() and the write/read calls like captureAdaptiveMemoryTool,
captureMemoryEventTool, createSessionDigestTool, getMemoryContextTool,
getAdaptiveContextTool, and distillContextPackTool.

const userId = process.env.PANDORA_MCP_USER_ID ?? process.env.PANDORA_MEMORY_BRIDGE_USER_ID ?? "";
const env = process.env;
const required = ["PANDORA_ENABLE_MCP", "PANDORA_ENABLE_MCP_CAPTURE", "PANDORA_ENABLE_MCP_DISTILLATION", "PANDORA_ENABLE_EMBEDDINGS", "PANDORA_ENABLE_MODEL_CALLS", "PANDORA_ENABLE_SEMANTIC_RETRIEVAL", "OPENAI_API_KEY", "EMBEDDING_MODEL", "LLM_MODEL", "PANDORA_EMBEDDING_MODEL", "PANDORA_MEMORY_EMBEDDING_MODEL"];
const aliases = { apiKey: ["PANDORA_MCP_TOKEN", "PANDORA_MCP_API_KEY", "PANDORA_API_KEY", "MEMORY_API_KEY"], dbKey: ["PANDORA_MCP_DB_KEY", "PANDORA_MEMORY_BRIDGE_DB_KEY", "SUPABASE_SERVICE_ROLE_KEY"], url: ["NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_URL"], anon: ["NEXT_PUBLIC_SUPABASE_ANON_KEY", "SUPABASE_ANON_KEY", "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY"] };
function flag(name: string) { return env[name] === "true" ? "enabled" : "disabled"; }
function out(name: string, ok: boolean, detail = "") { console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? ` - ${detail}` : ""}`); if (!ok) process.exitCode = 1; }
async function step(name: string, fn: () => Promise<unknown>, optional = false) { try { const r = await fn(); const text = JSON.stringify(r); const blocked = text.includes('"ok":false') || text.includes('Invalid API key'); out(name, optional || !blocked, text.slice(0, 500)); } catch (e) { out(name, false, e instanceof Error ? e.message : String(e)); } }
async function main() {
console.log("Pandora memory diagnostics (secret values redacted)");
for (const k of required) console.log(`ENV ${k}: ${env[k] ? "present" : "missing"}${k.startsWith("PANDORA_ENABLE") ? ` (${flag(k)})` : ""}`);
for (const [label, names] of Object.entries(aliases)) console.log(`ENV ${label}: ${presentEnvName(env, names) ?? "missing"}`);
console.log(`provider: ${env.OPENAI_API_KEY ? "openai_configured" : "deterministic/no_provider"}`);
console.log(`distillation: ${flag("PANDORA_ENABLE_MCP_DISTILLATION")}; embeddings: ${flag("PANDORA_ENABLE_EMBEDDINGS")}; model_calls: ${flag("PANDORA_ENABLE_MODEL_CALLS")}`);
const url = getPandoraSupabaseUrl(env); const db = getPandoraMcpDbKey(env);
if (!url.ok || !db.ok || !userId) { out("bootstrap", false, [!url.ok && url.message, !db.ok && db.message, !userId && "Missing server env: PANDORA_MCP_USER_ID or PANDORA_MEMORY_BRIDGE_USER_ID"].filter(Boolean).join("; ")); return; }
const client = createClient(url.value, db.value, { auth: { autoRefreshToken: false, persistSession: false } }) as unknown as MemoryBridgeDbClient;
const principal = { ok: true as const, authType: "mcp_bearer_token" as const, userId };
await step("supabase connection/service-role memory table read", async () => client.from("memory_events").select("id").eq("user_id", userId).eq("namespace", ns).limit(1));
await step("analyze_memory_candidates", async () => analyzeMemoryCandidatesTool(client, principal, { namespace: ns, text: seed, source: "memory_smoke", mode: "candidate_only" }, env));
await step("capture_adaptive_memory candidate_only", async () => captureAdaptiveMemoryTool(client, principal, { namespace: ns, text: seed, source: "memory_smoke", mode: "candidate_only" }, env));
await step("capture_adaptive_memory auto_capture_allowed", async () => captureAdaptiveMemoryTool(client, principal, { namespace: ns, text: seed, source: "memory_smoke", mode: "auto_capture_allowed" }, env));
await step("capture_memory_event", async () => captureMemoryEventTool(client, principal, { namespace: ns, raw_text: seed, source: "memory_smoke", sensitivity: "low" }, env), env.PANDORA_ENABLE_MCP_CAPTURE !== "true");
Comment on lines +26 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make memory smoke diagnostics dry-run only

When npm run memory:smoke is run against a configured environment with PANDORA_ENABLE_MCP_CAPTURE=true, these diagnostics use the service-role client and default to the real_life namespace, but the called tools are not read-only: analyzeMemoryCandidatesTool/captureAdaptiveMemoryTool insert candidate rows and captureMemoryEventTool inserts a captured memory_events row. A smoke/diagnostics command can therefore contaminate production user memory with the seed text instead of only reporting readiness; keep this path dry-run/read-only or require an explicit non-dry-run opt-in.

Useful? React with 👍 / 👎.

await step("get_memory_context", async () => getMemoryContextTool(client, principal, { namespace: ns, query: seed, max_items: 5 }));
await step("get_adaptive_context", async () => getAdaptiveContextTool(client, principal, { namespace: ns, query: seed, max_items: 5 }));
await step("create_session_digest", async () => createSessionDigestTool(client, principal, { namespace: ns, source: "memory_smoke", transcript_or_summary: seed }, env), env.PANDORA_ENABLE_MCP_CAPTURE !== "true" || env.PANDORA_ENABLE_MCP_DISTILLATION !== "true");
await step("get_latest_context_pack", async () => getLatestContextPackTool(client, principal, { namespace: ns }));
await step("distill master context pack", async () => distillContextPackTool(client, principal, { namespace: ns, pack_type: "master" }, env), env.PANDORA_ENABLE_MCP_DISTILLATION !== "true");
await step("retrieve open loops", async () => getOpenLoopsTool(client, principal, { namespace: ns }));
}
main().catch((e) => { out("memory smoke", false, e instanceof Error ? e.message : String(e)); });
24 changes: 24 additions & 0 deletions tests/unit/pandora-mcp-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { getPandoraMcpBearerSecret, getPandoraMcpDbKey, getPandoraSupabaseUrl } from "@/lib/services/pandora-mcp-env";
import { resolvePandoraMcpPrincipal } from "@/lib/services/mcp-auth";

function req(token: string) { return new Request("https://example.com/api/mcp", { headers: { authorization: `Bearer ${token}` } }); }

describe("Pandora MCP environment aliases", () => {
it("accepts deployed API key aliases and service-role db key aliases", () => {
expect(getPandoraMcpBearerSecret({ PANDORA_MCP_API_KEY: "tool-key" })).toMatchObject({ ok: true, envVar: "PANDORA_MCP_API_KEY" });
expect(getPandoraMcpDbKey({ SUPABASE_SERVICE_ROLE_KEY: "service-role" })).toMatchObject({ ok: true, envVar: "SUPABASE_SERVICE_ROLE_KEY" });
expect(getPandoraSupabaseUrl({ SUPABASE_URL: "https://example.supabase.co" })).toMatchObject({ ok: true, envVar: "SUPABASE_URL" });
});

it("reports a missing server env instead of generic invalid API key when no MCP token is configured", () => {
const result = resolvePandoraMcpPrincipal(req("tool-key"), { PANDORA_ENABLE_MCP: "true", PANDORA_MCP_USER_ID: "user-1", SUPABASE_SERVICE_ROLE_KEY: "service-role" });
expect(result).toMatchObject({ ok: false, code: "mcp_token_env_missing", message: "Missing server env: PANDORA_MCP_TOKEN" });
});

it("reports disabled capture and distillation with explicit env names", async () => {
const { requireMcpCaptureEnabled, requireMcpDistillationEnabled } = await import("@/lib/services/mcp-auth");
expect(requireMcpCaptureEnabled({ PANDORA_ENABLE_MCP_CAPTURE: "false" }).message).toBe("capture_disabled: PANDORA_ENABLE_MCP_CAPTURE is not true");
expect(requireMcpDistillationEnabled({ PANDORA_ENABLE_MCP_DISTILLATION: "false" }).message).toBe("distillation_disabled: PANDORA_ENABLE_MCP_DISTILLATION is not true");
});
});
Loading