evOo-local/packages/testkit/src/bot-secrets.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

398 lines
15 KiB
TypeScript

import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import type { AgentRuntimeEvent } from "@rakazo/adapter-kit";
import { ScriptedAgentRuntime } from "@rakazo/adapters";
import { answerRunInput } from "@rakazo/db";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import type { createApp } from "../../../apps/api/src/app.ts";
const hasDb = process.env.VERIFY_DATABASE === "1" && Boolean(process.env.DATABASE_URL);
const describeIntegration = hasDb ? describe : describe.skip;
const destination = {
name: "example_api",
origin: "https://api.example.test",
auth: { type: "bearer" },
};
const key = "fake-reusable-api-key";
describeIntegration("reusable credential lifecycle", () => {
let handles: Awaited<ReturnType<typeof createApp>>;
const dataDir = mkdtempSync(path.join(tmpdir(), "rakazo-secret-lifecycle-"));
const stamp = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const fetch = vi.fn<typeof globalThis.fetch>(async (_url, init) => {
const auth = new Headers(init?.headers).get("Authorization");
return Response.json({
authenticated: auth === `Bearer ${key}` || auth === `Bearer ${key}-rotated`,
echo: auth,
});
});
beforeAll(async () => {
const { createApp } = await import("../../../apps/api/src/app.ts");
handles = await createApp({
databaseUrl: process.env.DATABASE_URL!,
dataDir,
sandboxProvider: "fake",
agentRuntime: "scripted",
wakeupDriver: "memory",
defaultProvider: "scripted",
defaultModel: "scripted",
remoteConnectors: {
fetch,
resolveHostname: async () => [{ address: "203.0.113.10", family: 4 }],
},
});
});
afterAll(async () => {
await handles?.stop();
rmSync(dataDir, { recursive: true, force: true });
});
it.each([false, true])(
"saves, reuses, rotates and revokes without model-visible plaintext (approval rule: %s)",
async (requireApproval) => {
fetch.mockClear();
const seen: string[] = [];
let calls: Array<{ name: string; args: Record<string, unknown> }> = [
{
name: "request_secret",
args: { label: "API key", purpose: "api_key", credential: destination },
},
];
const runtime = vi
.spyOn(ScriptedAgentRuntime.prototype, "run")
.mockImplementation(async function* (request): AsyncIterable<AgentRuntimeEvent> {
seen.push(
JSON.stringify({
prompt: request.prompt,
history: request.history,
tools: request.tools,
}),
);
for (const [index, call] of calls.entries()) {
yield { type: "tool", ...call, executionId: `${request.runId}:${index}` };
}
yield { type: "done", text: "Done" };
});
try {
const seeded = await seedRun(`reusable-${requireApproval}`, "Save the API credential");
if (requireApproval) {
await handles.prisma.actionApprovalRule.create({
data: {
spaceId: seeded.me.spaceId,
createdByUserId: seeded.me.userId,
effect: "require_approval",
matchKind: "tool",
matchValue: "request_secret",
},
});
}
await handles.executor.continueRun(seeded.run.id, "test-worker");
const approve = async (runId: string, interruptBeforeCard = false) => {
if (!requireApproval) return;
const message = await handles.prisma.message.findFirstOrThrow({
where: { runId, role: "bot" },
orderBy: { createdAt: "desc" },
});
expect(message.blocks).toEqual(
expect.arrayContaining([
expect.objectContaining({ approvalEffectId: expect.any(String) }),
]),
);
if (interruptBeforeCard) {
// Reproduce a worker stopping after releasing the approved effect to intended,
// before the protected card transaction commits. No background job is enqueued.
expect(
await answerRunInput(handles.prisma, {
spaceId: seeded.me.spaceId,
threadId: seeded.thread.id,
runId,
messageId: message.id,
answeredByUserId: seeded.me.userId,
answer: "allow",
}),
).toBe(true);
await handles.prisma.externalEffect.updateMany({
where: { runId, kind: "request_secret", status: "approved" },
data: { status: "intended" },
});
await handles.prisma.run.update({
where: { id: runId },
data: {
status: "running",
leaseOwner: "interrupted-worker",
leaseExpiresAt: new Date(Date.now() - 60_000),
},
});
await handles.executor.continueRun(runId, "recovery-worker");
} else {
await rpc(seeded.cookie, "threads/answer", {
botId: seeded.bot.id,
runId,
messageId: message.id,
answer: "allow",
});
}
await vi.waitFor(
async () => {
const run = await handles.prisma.run.findUniqueOrThrow({ where: { id: runId } });
expect(["completed", "waiting_input"]).toContain(run.status);
},
{ timeout: 15_000 },
);
};
await approve(seeded.run.id);
const answer = async (runId: string, value: string) => {
const message = await handles.prisma.message.findFirstOrThrow({
where: { runId, role: "bot" },
orderBy: { createdAt: "desc" },
});
expect(message.blocks).toEqual(
expect.arrayContaining([
expect.objectContaining({ input: "secret", credential: destination }),
]),
);
await rpc(seeded.cookie, "threads/answer", {
botId: seeded.bot.id,
runId,
messageId: message.id,
answer: value,
});
await vi.waitFor(
async () => {
expect(
await handles.prisma.run.findUniqueOrThrow({ where: { id: runId } }),
).toMatchObject({ status: "completed", error: null });
},
{ timeout: 15_000 },
);
};
expect(
await handles.prisma.run.findUniqueOrThrow({ where: { id: seeded.run.id } }),
).toMatchObject({ status: "waiting_input", error: null });
// An invalid value rolls back the run transition and leaves the card pending.
const pending = await handles.prisma.message.findFirstOrThrow({
where: { runId: seeded.run.id, role: "bot" },
orderBy: { seq: "desc" },
});
expect(pending.blocks).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "ask",
input: "secret",
status: "pending",
credential: destination,
}),
]),
);
for (const invalid of ["x".repeat(16_385), "key\r\nX-Evil: injected", "key "]) {
await expect(
rpc(seeded.cookie, "threads/answer", {
botId: seeded.bot.id,
runId: seeded.run.id,
messageId: pending.id,
answer: invalid,
}),
).rejects.toThrow();
}
expect(
await handles.prisma.run.findUniqueOrThrow({ where: { id: seeded.run.id } }),
).toMatchObject({ status: "waiting_input" });
expect(await handles.prisma.botSecret.count({ where: { botId: seeded.bot.id } })).toBe(0);
await answer(seeded.run.id, key);
const stored = await handles.prisma.botSecret.findFirstOrThrow({
where: { botId: seeded.bot.id },
});
expect(stored).toMatchObject({
...destination,
userId: seeded.me.userId,
spaceId: seeded.me.spaceId,
});
expect(stored.ciphertext).toMatch(/^v2:/);
expect(stored.ciphertext).not.toContain(key);
expect(
await handles.prisma.secret.count({ where: { kind: `run-secret:${seeded.run.id}` } }),
).toBe(0);
const nextRun = async (nextCalls: typeof calls) => {
calls = nextCalls;
const task = await handles.prisma.task.create({
data: {
userId: seeded.me.userId,
spaceId: seeded.me.spaceId,
botId: seeded.bot.id,
threadId: seeded.thread.id,
prompt: "Use the saved credential",
status: "queued",
},
});
const run = await handles.prisma.run.create({
data: {
userId: seeded.me.userId,
spaceId: seeded.me.spaceId,
botId: seeded.bot.id,
threadId: seeded.thread.id,
taskId: task.id,
trigger: "user",
status: "queued",
},
});
await handles.executor.continueRun(run.id, "test-worker");
return run.id;
};
const request = {
name: "secret_request",
args: { name: destination.name, url: `${destination.origin}/v1/items` },
};
const useRun = await nextRun([
{ name: "list_secrets", args: {} },
{
name: "request_secret",
args: { label: "API key", purpose: "api_key", credential: destination },
},
request,
]);
await approve(useRun);
expect(await handles.prisma.run.findUniqueOrThrow({ where: { id: useRun } })).toMatchObject(
{
status: "completed",
error: null,
},
);
expect(fetch).toHaveBeenCalledTimes(1);
expect(
await handles.prisma.externalEffect.findFirstOrThrow({
where: { runId: useRun, kind: "secret_request" },
}),
).toMatchObject({
status: "completed",
result: {
status: 200,
body: { authenticated: true, echo: expect.stringContaining("[redacted]") },
},
});
const rotateRun = await nextRun([
{
name: "request_secret",
args: { label: "API key", purpose: "api_key", credential: destination, replace: true },
},
]);
expect(
await handles.prisma.run.findUniqueOrThrow({ where: { id: rotateRun } }),
).toMatchObject({ status: "waiting_input" });
await approve(rotateRun, true);
expect(
await handles.prisma.run.findUniqueOrThrow({ where: { id: rotateRun } }),
).toMatchObject({ status: "waiting_input" });
expect(
await handles.prisma.botSecret.findUniqueOrThrow({ where: { id: stored.id } }),
).toMatchObject({ ciphertext: stored.ciphertext });
await answer(rotateRun, `${key}-rotated`);
expect(
await handles.prisma.botSecret.findUniqueOrThrow({ where: { id: stored.id } }),
).not.toMatchObject({ ciphertext: stored.ciphertext });
await nextRun([request]);
expect(new Headers(fetch.mock.calls.at(-1)?.[1]?.headers).get("Authorization")).toBe(
`Bearer ${key}-rotated`,
);
const count = fetch.mock.calls.length;
await nextRun([{ name: "forget_secret", args: { name: destination.name } }, request]);
expect(await handles.prisma.botSecret.count({ where: { botId: seeded.bot.id } })).toBe(0);
expect(fetch).toHaveBeenCalledTimes(count);
const [messages, events, effects, tasks] = await Promise.all([
handles.prisma.message.findMany({ where: { threadId: seeded.thread.id } }),
handles.prisma.event.findMany({ where: { threadId: seeded.thread.id } }),
handles.prisma.externalEffect.findMany({ where: { spaceId: seeded.me.spaceId } }),
handles.prisma.task.findMany({ where: { botId: seeded.bot.id } }),
]);
expect(JSON.stringify({ seen, messages, events, effects, tasks })).not.toContain(key);
} finally {
runtime.mockRestore();
}
},
);
async function seedRun(
label: string,
prompt: string,
runState: {
status?: string;
leaseOwner?: string;
leaseFence?: number;
leaseExpiresAt?: Date;
startedAt?: Date;
completedAt?: Date;
} = {},
) {
const cookie = await signup(`executor-${label}-${stamp}@rakazo.test`, `Executor ${label}`);
const me = await rpc<{ userId: string; spaceId: string }>(cookie, "me");
const bot = await rpc<{ id: string }>(cookie, "bots/create", {
name: `Executor ${label}`,
title: "",
description: "",
instructions: "",
notifyOnFinish: false,
});
const thread = await handles.prisma.thread.findUniqueOrThrow({ where: { botId: bot.id } });
const task = await handles.prisma.task.create({
data: {
spaceId: me.spaceId,
botId: bot.id,
threadId: thread.id,
userId: me.userId,
prompt,
status: "queued",
},
});
const run = await handles.prisma.run.create({
data: {
spaceId: me.spaceId,
botId: bot.id,
threadId: thread.id,
taskId: task.id,
userId: me.userId,
status: runState.status ?? "queued",
trigger: "user",
leaseOwner: runState.leaseOwner,
leaseFence: runState.leaseFence,
leaseExpiresAt: runState.leaseExpiresAt,
startedAt: runState.startedAt,
completedAt: runState.completedAt,
},
});
return { cookie, me, bot, thread, task, run };
}
async function signup(email: string, name: string) {
const response = await handles.app.request("/api/auth/sign-up/email", {
method: "POST",
headers: { "content-type": "application/json", origin: "http://127.0.0.1:5173" },
body: JSON.stringify({ email, password: "password12", name }),
});
expect(response.status).toBeLessThan(400);
const raw = response.headers.get("set-cookie") ?? "";
const match = raw.match(/better-auth\.session_token=([^;]+)/);
expect(match?.[1]).toBeTruthy();
return `better-auth.session_token=${match![1]}`;
}
async function rpc<T>(cookie: string, procedure: string, body: unknown = {}): Promise<T> {
const response = await handles.app.request(`/rpc/${procedure}`, {
method: "POST",
headers: {
"content-type": "application/json",
origin: "http://127.0.0.1:5173",
cookie,
},
body: JSON.stringify({ json: body }),
});
const payload = (await response.json()) as { json?: T; error?: { message?: string } };
if (!response.ok || payload.error) {
throw new Error(payload.error?.message ?? `${procedure} failed (${response.status})`);
}
return payload.json as T;
}
});