evOo-local/packages/adapters/src/secrets-model-visibility-conformance.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

457 lines
15 KiB
TypeScript

import type { AdapterContext } from "@rakazo/adapter-kit";
import type { BotSecretDestination } from "@rakazo/contracts";
import type { PrismaClient } from "@rakazo/db";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildApprovalAskBlock } from "./approval-ask.js";
import { redactToolArgsForReview } from "./auto-review.js";
import { requestWithBotSecret } from "./bot-secrets.js";
import { sanitizeComposioError } from "./composio-connector.js";
import { redactConnectorPayload, sanitizeConnectorError } from "./connector-safety.js";
import { McpConnector } from "./mcp-connector.js";
import type { OAuthMaterial } from "./mcp-oauth.js";
import { oauthMaterialSecrets } from "./mcp-oauth.js";
import { PipedreamConnector } from "./pipedream-connector.js";
import * as remoteMcp from "./remote-mcp.js";
import { EncryptedSecretStore } from "./secrets.js";
/**
* Offline conformance: bot secrets and connector OAuth material must never
* appear in model-visible tool args/results (or review/approval payloads that
* feed the model). Prefer existing redaction/injection boundaries.
*
* Intentionally does not cover email OTP / magic-link / reset URL redaction.
*/
const OAUTH_ACCESS = "oauth-access-token-conformance-value";
const OAUTH_REFRESH = "oauth-refresh-token-conformance-value";
const OAUTH_CLIENT_SECRET = "oauth-client-secret-conformance-value";
const BOT_SECRET = "bot-secret-conformance-value+/=";
function assertNoLeak(visible: unknown, secrets: string[]) {
const serialized = JSON.stringify(visible);
for (const secret of secrets) {
expect(serialized, `leaked ${secret}`).not.toContain(secret);
}
}
describe("secrets model-visibility conformance", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe("shared connector redaction boundary", () => {
it("strips OAuth access, refresh, and client secrets from tool payloads", () => {
const secrets = [OAUTH_ACCESS, OAUTH_REFRESH, OAUTH_CLIENT_SECRET];
const payload = {
access_token: OAUTH_ACCESS,
nested: {
refresh_token: OAUTH_REFRESH,
authorization: `Bearer ${OAUTH_ACCESS}`,
},
client_secret: OAUTH_CLIENT_SECRET,
ok: true,
};
const redacted = redactConnectorPayload(payload, secrets);
assertNoLeak(redacted, secrets);
expect(redacted).toMatchObject({ ok: true });
});
it("strips registered secrets and Bearer tokens from connector errors", () => {
const message = sanitizeConnectorError(
new Error(`upstream rejected Bearer ${OAUTH_ACCESS} client_secret=${OAUTH_CLIENT_SECRET}`),
[OAUTH_ACCESS, OAUTH_CLIENT_SECRET],
);
assertNoLeak(message, [OAUTH_ACCESS, OAUTH_CLIENT_SECRET]);
expect(message).toContain("Bearer [redacted]");
});
});
describe("oauth material collection", () => {
it("redacts Cookie/X-Session and short auth material without corrupting config enums", () => {
const material: OAuthMaterial = {
secret: "Bearer static-mcp-token-value",
env: {
API_TOKEN: "env-mcp-token-value",
API_SECRET: "production",
NODE_ENV: "production",
AUTH_MODE: "oauth",
SESSION_TIMEOUT: "3600",
COOKIE_DOMAIN: "example.test",
SHORT_API_KEY: "ab12",
ACCESS_TOKEN: "123456",
},
headers: {
"X-Api-Key": "header-mcp-token-value",
Cookie: "sid=x",
"X-Session": "s1",
"X-Env": "info",
},
oauth: {
tokens: {
access_token: OAUTH_ACCESS,
refresh_token: OAUTH_REFRESH,
token_type: "bearer",
},
clientInformation: {
client_id: "client-id",
client_secret: OAUTH_CLIENT_SECRET,
},
},
};
const secrets = oauthMaterialSecrets(material);
expect(secrets).toEqual(
expect.arrayContaining([
"Bearer static-mcp-token-value",
"static-mcp-token-value",
"env-mcp-token-value",
"header-mcp-token-value",
"sid=x",
"s1",
"ab12",
"123456",
OAUTH_ACCESS,
OAUTH_REFRESH,
OAUTH_CLIENT_SECRET,
]),
);
// Ordinary config must not enter global substring redaction.
expect(secrets).not.toContain("production");
expect(secrets).not.toContain("info");
expect(secrets).not.toContain("oauth");
expect(secrets).not.toContain("3600");
expect(secrets).not.toContain("example.test");
const payload = {
ok: true,
note: "deployed to production with oauth mode",
cookie: "sid=x",
session: "s1",
shortKey: "ab12",
numericToken: "123456",
};
const redacted = redactConnectorPayload(payload, secrets);
expect(redacted).toMatchObject({
ok: true,
note: "deployed to production with oauth mode",
});
expect(JSON.stringify(redacted)).not.toContain("sid=x");
expect(JSON.stringify(redacted)).not.toContain('"s1"');
expect(JSON.stringify(redacted)).not.toContain("ab12");
expect(JSON.stringify(redacted)).not.toContain("123456");
expect(JSON.stringify(redacted)).toContain("production");
expect(JSON.stringify(redacted)).toContain("oauth");
});
it("picks up rotated access tokens from the live material object", () => {
const material: OAuthMaterial = {
oauth: {
tokens: {
access_token: "oauth-access-token-before-rotation",
refresh_token: OAUTH_REFRESH,
token_type: "bearer",
},
},
};
expect(oauthMaterialSecrets(material)).toContain("oauth-access-token-before-rotation");
material.oauth!.tokens!.access_token = "oauth-access-token-after-rotation";
const secrets = oauthMaterialSecrets(material);
expect(secrets).toContain("oauth-access-token-after-rotation");
expect(secrets).not.toContain("oauth-access-token-before-rotation");
});
it("registers numeric-only values under explicit credential keys", () => {
const secrets = oauthMaterialSecrets({
env: {
ACCESS_TOKEN: "123456",
API_SECRET: "production",
REFRESH_TOKEN_TIMEOUT: "3600",
},
});
expect(secrets).toContain("123456");
expect(secrets).not.toContain("production");
expect(secrets).not.toContain("3600");
});
});
describe("bot secret_request boundary", () => {
it("never returns bot-secret plaintext in the tool result", async () => {
const scope = { userId: "user-1", spaceId: "space-1", botId: "bot-1" };
const destination: BotSecretDestination = {
name: "example_api",
origin: "https://api.example.test",
auth: { type: "bearer" },
};
const secretStore = new EncryptedSecretStore("test-only-encryption-key");
const encrypted = await secretStore.put(
BOT_SECRET,
{
...scope,
operationId: "test",
traceId: "test",
signal: new AbortController().signal,
},
"secret-1",
);
const row = { ...scope, ...destination, ...encrypted };
const prisma = {
botSecret: {
findFirst: vi.fn(async ({ where }) =>
Object.entries(where).every(([key, value]) => row[key as keyof typeof row] === value)
? row
: null,
),
},
} as unknown as PrismaClient;
const fetch = vi.fn<typeof globalThis.fetch>(async (_url, init) => {
expect(new Headers(init?.headers).get("Authorization")).toBe(`Bearer ${BOT_SECRET}`);
return Response.json({
echo: BOT_SECRET,
authorization: `Bearer ${BOT_SECRET}`,
items: [1],
});
});
const registerRedactions = vi.fn();
const result = await requestWithBotSecret({
prisma,
secretStore: secretStore,
scope,
request: { name: destination.name, url: `${destination.origin}/v1/items` },
signal: new AbortController().signal,
remote: {
fetch,
resolveHostname: async () => [{ address: "203.0.113.10", family: 4 as const }],
},
registerRedactions,
});
assertNoLeak(result, [BOT_SECRET, `Bearer ${BOT_SECRET}`]);
expect(registerRedactions).toHaveBeenCalledWith(expect.arrayContaining([BOT_SECRET]));
});
});
describe("Pipedream connector OAuth boundary", () => {
it("redacts access token and client secret from echoed tool results", async () => {
const accessToken = "pd-access-token-conformance";
const clientSecret = "pd-client-secret-conformance";
vi.spyOn(remoteMcp, "callRemoteMcpTool").mockResolvedValue({
content: [
{
type: "text",
text: JSON.stringify({
access_token: accessToken,
client_secret: clientSecret,
ok: true,
}),
},
],
isError: false,
});
vi.stubGlobal(
"fetch",
vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.includes("/v1/oauth/token")) {
return Response.json({ access_token: accessToken, expires_in: 3_600 });
}
throw new Error(`Unexpected fetch ${url}`);
}),
);
const connector = new PipedreamConnector({
clientId: "pd-client-id",
clientSecret,
projectId: "pd-project",
environment: "development",
identitySecret: "pd-identity-secret",
});
const context = {
operationId: "conformance",
traceId: "conformance",
spaceId: "space-1",
userId: "user-1",
signal: new AbortController().signal,
connectedConnections: [
{
id: "c1",
connectorId: "pipedream",
externalId: "gmail",
displayName: "Gmail",
},
],
} as AdapterContext;
const events = [];
for await (const event of connector.execute(
{
tool: "gmail.list",
args: {},
executionId: "exec-1",
route: { connectorId: "pipedream", resourceId: "gmail", toolName: "gmail.list" },
},
context,
)) {
events.push(event);
}
expect(events).toHaveLength(1);
expect(events[0]?.type).toBe("result");
assertNoLeak(events, [accessToken, clientSecret]);
});
});
describe("MCP connector OAuth boundary", () => {
it("redacts OAuth tokens and client secrets echoed by tool results", async () => {
const material: OAuthMaterial = {
secret: "mcp-static-token",
oauth: {
tokens: {
access_token: OAUTH_ACCESS,
refresh_token: OAUTH_REFRESH,
token_type: "bearer",
},
clientInformation: {
client_id: "mcp-client",
client_secret: OAUTH_CLIENT_SECRET,
},
},
};
const server = {
id: "server-1",
slug: "demo",
transport: "streamable_http",
endpoint: "https://mcp.example.test/mcp",
secretId: "secret-1",
args: [],
revision: 1,
};
const assignment = {
botId: "bot-1",
serverId: "server-1",
spaceId: "w1",
userId: "u1",
allowAllTools: true,
allowedTools: [],
server,
};
vi.stubGlobal(
"fetch",
vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(input, init);
const message = JSON.parse(await request.text()) as {
id?: number;
method?: string;
};
if (message.method === "initialize") {
return Response.json({
jsonrpc: "2.0",
id: message.id,
result: {
protocolVersion: "2025-11-25",
capabilities: { tools: {} },
serverInfo: { name: "test", version: "1" },
},
});
}
if (message.method === "tools/list") {
return Response.json({
jsonrpc: "2.0",
id: message.id,
result: { tools: [{ name: "echo", inputSchema: { type: "object" } }] },
});
}
if (message.method === "tools/call") {
return Response.json({
jsonrpc: "2.0",
id: message.id,
result: {
content: [
{
type: "text",
text: JSON.stringify({
access_token: OAUTH_ACCESS,
refresh_token: OAUTH_REFRESH,
client_secret: OAUTH_CLIENT_SECRET,
static: material.secret,
}),
},
],
},
});
}
return new Response(null, { status: 202 });
}),
);
const prisma = {
botMcpServer: {
findMany: vi.fn().mockResolvedValue([assignment]),
findFirst: vi.fn().mockResolvedValue(assignment),
},
secret: {
findFirst: vi.fn().mockResolvedValue({ id: "secret-1", ciphertext: "encrypted" }),
},
};
const secrets = {
load: vi.fn().mockReturnValue(JSON.stringify(material)),
};
const connector = new McpConnector(prisma as never, secrets as never, {
network: {
fetch: (input: string | URL | Request, init?: RequestInit) =>
globalThis.fetch(input, init),
resolveHostname: async () => [{ address: "203.0.113.10", family: 4 }],
},
});
const context = {
spaceId: "w1",
userId: "u1",
botId: "bot-1",
signal: new AbortController().signal,
} as never;
const events = [];
for await (const event of connector.execute(
{
tool: "mcp__demo__echo",
args: {},
route: { connectorId: "mcp", resourceId: "server-1", toolName: "echo" },
} as never,
context,
)) {
events.push(event);
}
expect(events[0]).toMatchObject({ type: "result" });
assertNoLeak(events, [OAUTH_ACCESS, OAUTH_REFRESH, OAUTH_CLIENT_SECRET, "mcp-static-token"]);
await connector.close();
});
});
describe("Composio pattern redaction", () => {
it("redacts API keys and Bearer tokens from connector errors", () => {
const message = sanitizeComposioError(
"denied ak_secretvaluehere COMPOSIO_API_KEY=ak_shouldnotleak Bearer tok_abc",
);
expect(message).not.toContain("ak_secretvaluehere");
expect(message).not.toContain("ak_shouldnotleak");
expect(message).not.toContain("tok_abc");
expect(message).toContain("[redacted]");
});
});
describe("model-visible review and approval payloads", () => {
it("redacts secrets from approval ask blocks and auto-review tool args", () => {
const secrets = [OAUTH_ACCESS, OAUTH_CLIENT_SECRET, BOT_SECRET];
const args = {
to: "person@example.test",
body: `use ${OAUTH_ACCESS} and ${BOT_SECRET}`,
client_secret: OAUTH_CLIENT_SECRET,
};
const ask = buildApprovalAskBlock("effect-1", "gmail_send_email", args, secrets);
assertNoLeak(ask, secrets);
const reviewArgs = redactToolArgsForReview(args, secrets);
assertNoLeak(reviewArgs, secrets);
});
});
});