2192 lines
72 KiB
TypeScript
2192 lines
72 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { createServer } from "node:http";
|
|
import express from "express";
|
|
import request from "supertest";
|
|
import { WebSocketServer } from "ws";
|
|
import { and, eq } from "drizzle-orm";
|
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
import {
|
|
activityLog,
|
|
agentWakeupRequests,
|
|
agentRuntimeState,
|
|
agents,
|
|
approvals,
|
|
assets,
|
|
companies,
|
|
companyMemberships,
|
|
companySkills,
|
|
createDb,
|
|
documentAnnotationComments,
|
|
documentAnnotationThreads,
|
|
documentRevisions,
|
|
documents,
|
|
externalObjectMentions,
|
|
externalObjects,
|
|
heartbeatRunEvents,
|
|
heartbeatRuns,
|
|
issueAttachments,
|
|
issueApprovals,
|
|
issueComments,
|
|
issueDocuments,
|
|
issueInboxArchives,
|
|
issueRelations,
|
|
issues,
|
|
issueThreadInteractions,
|
|
issueWorkProducts,
|
|
principalPermissionGrants,
|
|
projects,
|
|
} from "@paperclipai/db";
|
|
import {
|
|
ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY,
|
|
LOW_TRUST_REVIEW_PRESET,
|
|
} from "@paperclipai/shared";
|
|
import {
|
|
getEmbeddedPostgresTestSupport,
|
|
startEmbeddedPostgresTestDatabase,
|
|
} from "./helpers/embedded-postgres.js";
|
|
import { parseWakePayloadFromMessage } from "./helpers/wake-message.js";
|
|
import { drainHeartbeatRunsToQuiescence } from "./helpers/drain-heartbeat-runs.js";
|
|
import { errorHandler } from "../middleware/index.js";
|
|
import { agentRoutes } from "../routes/agents.js";
|
|
import { issueRoutes } from "../routes/issues.js";
|
|
import { heartbeatService } from "../services/heartbeat.js";
|
|
import { LOW_TRUST_QUARANTINED_BODY } from "../services/source-trust.js";
|
|
|
|
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
|
const describeEmbeddedPostgres = embeddedPostgresSupport.supported
|
|
? describe
|
|
: describe.skip;
|
|
|
|
if (!embeddedPostgresSupport.supported) {
|
|
console.warn(
|
|
`Skipping embedded Postgres low-trust route tests on this host: ${
|
|
embeddedPostgresSupport.reason ?? "unsupported environment"
|
|
}`,
|
|
);
|
|
}
|
|
|
|
type Db = ReturnType<typeof createDb>;
|
|
type Fixture = Awaited<ReturnType<typeof seedLowTrustFixture>>;
|
|
|
|
async function waitFor(
|
|
condition: () => boolean | Promise<boolean>,
|
|
timeoutMs = 10_000,
|
|
intervalMs = 50,
|
|
) {
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
if (await condition()) return;
|
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
}
|
|
throw new Error("Timed out waiting for condition");
|
|
}
|
|
|
|
function isHeartbeatCleanupFkError(error: unknown) {
|
|
const message =
|
|
error instanceof Error
|
|
? `${error.message} ${String(error.cause ?? "")}`
|
|
: String(error);
|
|
return (
|
|
message.includes("heartbeat_run_events_run_id_heartbeat_runs_id_fk") ||
|
|
message.includes("activity_log_run_id_heartbeat_runs_id_fk") ||
|
|
message.includes(
|
|
"heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk",
|
|
)
|
|
);
|
|
}
|
|
|
|
async function deleteHeartbeatRunsAndWakeupsAfterActivityLogDrains(db: Db) {
|
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
await db.delete(heartbeatRunEvents);
|
|
await db.delete(activityLog);
|
|
try {
|
|
await db.delete(heartbeatRunEvents);
|
|
await db.delete(heartbeatRuns);
|
|
await db.delete(agentWakeupRequests);
|
|
return;
|
|
} catch (error) {
|
|
if (!isHeartbeatCleanupFkError(error) || attempt === 9) {
|
|
throw error;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function deleteCompanySkillsAfterLateHeartbeatWritesDrain(db: Db) {
|
|
let lastError: unknown = null;
|
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
await db.delete(companySkills);
|
|
try {
|
|
await db.delete(companies);
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
}
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
function expectNoCanary(value: unknown, ...markers: string[]) {
|
|
const serialized = JSON.stringify(value);
|
|
for (const marker of markers) expect(serialized).not.toContain(marker);
|
|
}
|
|
|
|
function agentActor(
|
|
fixture: Fixture,
|
|
agentId = fixture.agents.lowTrust.id,
|
|
): Express.Request["actor"] {
|
|
return {
|
|
type: "agent",
|
|
agentId,
|
|
companyId: fixture.company.id,
|
|
runId:
|
|
agentId === fixture.agents.lowTrust.id
|
|
? fixture.runs.lowTrust.id
|
|
: fixture.runs.standard.id,
|
|
source: "agent_jwt",
|
|
};
|
|
}
|
|
|
|
function standardReportActor(fixture: Fixture): Express.Request["actor"] {
|
|
return {
|
|
type: "agent",
|
|
agentId: fixture.agents.standard.id,
|
|
companyId: fixture.company.id,
|
|
runId: fixture.runs.standardReport.id,
|
|
source: "agent_jwt",
|
|
};
|
|
}
|
|
|
|
function skillTestActor(
|
|
fixture: Fixture,
|
|
issueId = fixture.issues.assignedReview.id,
|
|
): Express.Request["actor"] {
|
|
return {
|
|
type: "agent",
|
|
agentId: fixture.agents.standard.id,
|
|
companyId: fixture.company.id,
|
|
runId: fixture.runs.standard.id,
|
|
source: "agent_jwt",
|
|
keyScope: { kind: "skill_test", issueId },
|
|
};
|
|
}
|
|
|
|
function boardActor(fixture: Fixture): Express.Request["actor"] {
|
|
return {
|
|
type: "board",
|
|
userId: "board-user",
|
|
companyIds: [fixture.company.id],
|
|
memberships: [
|
|
{
|
|
companyId: fixture.company.id,
|
|
membershipRole: "operator",
|
|
status: "active",
|
|
},
|
|
],
|
|
isInstanceAdmin: true,
|
|
source: "local_implicit",
|
|
};
|
|
}
|
|
|
|
function createApp(db: Db, actor: Express.Request["actor"]) {
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use((req, _res, next) => {
|
|
req.actor = actor;
|
|
next();
|
|
});
|
|
app.use("/api", agentRoutes(db));
|
|
app.use("/api", issueRoutes(db, {} as any));
|
|
app.use(errorHandler);
|
|
return app;
|
|
}
|
|
|
|
async function createControlledGatewayServer() {
|
|
const server = createServer();
|
|
const wss = new WebSocketServer({ server });
|
|
const agentPayloads: Array<Record<string, unknown>> = [];
|
|
let firstWaitRelease: (() => void) | null = null;
|
|
let firstWaitGate = new Promise<void>((resolve) => {
|
|
firstWaitRelease = resolve;
|
|
});
|
|
let waitCount = 0;
|
|
|
|
wss.on("connection", (socket) => {
|
|
socket.send(
|
|
JSON.stringify({
|
|
type: "event",
|
|
event: "connect.challenge",
|
|
payload: { nonce: "nonce-123" },
|
|
}),
|
|
);
|
|
|
|
socket.on("message", async (raw) => {
|
|
const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
|
|
const frame = JSON.parse(text) as {
|
|
type: string;
|
|
id: string;
|
|
method: string;
|
|
params?: Record<string, unknown>;
|
|
};
|
|
|
|
if (frame.type !== "req") return;
|
|
|
|
if (frame.method === "connect") {
|
|
socket.send(
|
|
JSON.stringify({
|
|
type: "res",
|
|
id: frame.id,
|
|
ok: true,
|
|
payload: {
|
|
type: "hello-ok",
|
|
protocol: 3,
|
|
server: { version: "test", connId: "conn-1" },
|
|
features: {
|
|
methods: ["connect", "agent", "agent.wait"],
|
|
events: ["agent"],
|
|
},
|
|
snapshot: { version: 1, ts: Date.now() },
|
|
policy: {
|
|
maxPayload: 1_000_000,
|
|
maxBufferedBytes: 1_000_000,
|
|
tickIntervalMs: 30_000,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "agent") {
|
|
agentPayloads.push((frame.params ?? {}) as Record<string, unknown>);
|
|
socket.send(
|
|
JSON.stringify({
|
|
type: "res",
|
|
id: frame.id,
|
|
ok: true,
|
|
payload: {
|
|
runId:
|
|
typeof frame.params?.idempotencyKey === "string"
|
|
? frame.params.idempotencyKey
|
|
: `run-${agentPayloads.length}`,
|
|
status: "accepted",
|
|
acceptedAt: Date.now(),
|
|
},
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "agent.wait") {
|
|
waitCount += 1;
|
|
if (waitCount === 1) await firstWaitGate;
|
|
socket.send(
|
|
JSON.stringify({
|
|
type: "res",
|
|
id: frame.id,
|
|
ok: true,
|
|
payload: {
|
|
runId: frame.params?.runId,
|
|
status: "ok",
|
|
startedAt: 1,
|
|
endedAt: 2,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
server.listen(0, "127.0.0.1", () => resolve());
|
|
});
|
|
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") {
|
|
throw new Error("Failed to resolve test server address");
|
|
}
|
|
|
|
return {
|
|
url: `ws://127.0.0.1:${address.port}`,
|
|
getAgentPayloads: () => agentPayloads,
|
|
releaseFirstWait: () => {
|
|
firstWaitRelease?.();
|
|
firstWaitRelease = null;
|
|
firstWaitGate = Promise.resolve();
|
|
},
|
|
close: async () => {
|
|
await new Promise<void>((resolve) => wss.close(() => resolve()));
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
},
|
|
};
|
|
}
|
|
|
|
async function snapshot(db: Db) {
|
|
const [
|
|
issueRows,
|
|
commentRows,
|
|
documentRows,
|
|
workProductRows,
|
|
approvalRows,
|
|
relationRows,
|
|
interactionRows,
|
|
wakeRows,
|
|
runRows,
|
|
activityRows,
|
|
] = await Promise.all([
|
|
db.select().from(issues),
|
|
db.select().from(issueComments),
|
|
db.select().from(documents),
|
|
db.select().from(issueWorkProducts),
|
|
db.select().from(approvals),
|
|
db.select().from(issueRelations),
|
|
db.select().from(issueThreadInteractions),
|
|
db.select().from(agentWakeupRequests),
|
|
db.select().from(heartbeatRuns),
|
|
db.select().from(activityLog),
|
|
]);
|
|
return {
|
|
issues: issueRows,
|
|
comments: commentRows,
|
|
documents: documentRows,
|
|
workProducts: workProductRows,
|
|
approvals: approvalRows,
|
|
relations: relationRows,
|
|
interactions: interactionRows,
|
|
wakeups: wakeRows,
|
|
runs: runRows,
|
|
activity: activityRows,
|
|
};
|
|
}
|
|
|
|
async function createQuarantinedContinuationSummary(
|
|
db: Db,
|
|
fixture: Fixture,
|
|
issueId: string,
|
|
) {
|
|
const sourceTrust = {
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "quarantined" as const,
|
|
sourceIssueId: fixture.issues.assignedReview.id,
|
|
sourceRunId: fixture.runs.lowTrust.id,
|
|
sourceAgentId: fixture.agents.lowTrust.id,
|
|
};
|
|
const [document] = await db
|
|
.insert(documents)
|
|
.values({
|
|
companyId: fixture.company.id,
|
|
title: "Continuation Summary",
|
|
latestBody: `Continuation must not leak ${fixture.canaries.raw}`,
|
|
createdByAgentId: fixture.agents.lowTrust.id,
|
|
updatedByAgentId: fixture.agents.lowTrust.id,
|
|
sourceTrust,
|
|
})
|
|
.returning();
|
|
const [revision] = await db
|
|
.insert(documentRevisions)
|
|
.values({
|
|
companyId: fixture.company.id,
|
|
documentId: document!.id,
|
|
revisionNumber: 1,
|
|
title: "Continuation Summary",
|
|
body: `Continuation must not leak ${fixture.canaries.raw}`,
|
|
createdByAgentId: fixture.agents.lowTrust.id,
|
|
})
|
|
.returning();
|
|
await db
|
|
.update(documents)
|
|
.set({ latestRevisionId: revision!.id })
|
|
.where(eq(documents.id, document!.id));
|
|
await db.insert(issueDocuments).values({
|
|
companyId: fixture.company.id,
|
|
issueId,
|
|
documentId: document!.id,
|
|
key: ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY,
|
|
});
|
|
return document!;
|
|
}
|
|
|
|
async function seedLowTrustFixture(db: Db) {
|
|
const nonce = randomUUID().slice(0, 8);
|
|
const canary = (label: string) => `LT_REDTEAM_${nonce}_${label}`;
|
|
const canaries = {
|
|
issueSibling: canary("FLAG-ISSUE-SIBLING-7R4G"),
|
|
commentSibling: canary("FLAG-COMMENT-SIBLING-7R4G"),
|
|
documentSibling: canary("FLAG-DOC-SIBLING-7R4G"),
|
|
annotationSibling: canary("FLAG-ANNOTATION-SIBLING-7R4G"),
|
|
attachmentSibling: canary("FLAG-ATTACHMENT-SIBLING-7R4G"),
|
|
externalObjectSibling: canary("FLAG-EXTERNAL-OBJECT-SIBLING-7R4G"),
|
|
workProductSibling: canary("FLAG-WP-SIBLING-7R4G"),
|
|
approval: canary("FLAG-APPROVAL-7R4G"),
|
|
agentConfig: canary("FLAG-AGENTCFG-7R4G"),
|
|
raw: canary("IGNORE_ALL_PREVIOUS_INSTRUCTIONS FLAG-RAW-7R4G"),
|
|
};
|
|
|
|
const [company] = await db
|
|
.insert(companies)
|
|
.values({
|
|
name: `Low trust ${nonce}`,
|
|
issuePrefix: `LT${nonce.slice(0, 4).toUpperCase()}`,
|
|
defaultResponsibleUserId: "board-user",
|
|
})
|
|
.returning();
|
|
const [allowedProject] = await db
|
|
.insert(projects)
|
|
.values({
|
|
companyId: company!.id,
|
|
name: `Allowed ${nonce}`,
|
|
status: "in_progress",
|
|
})
|
|
.returning();
|
|
const [outOfScopeProject] = await db
|
|
.insert(projects)
|
|
.values({
|
|
companyId: company!.id,
|
|
name: `Out ${nonce}`,
|
|
description: canary("FLAG-PROJECT-7R4G"),
|
|
status: "in_progress",
|
|
})
|
|
.returning();
|
|
|
|
const [collaborator] = await db
|
|
.insert(agents)
|
|
.values({
|
|
companyId: company!.id,
|
|
name: "Same Boundary Collaborator",
|
|
role: "engineer",
|
|
adapterType: "process",
|
|
adapterConfig: {},
|
|
runtimeConfig: {},
|
|
permissions: {},
|
|
})
|
|
.returning();
|
|
const [standard] = await db
|
|
.insert(agents)
|
|
.values({
|
|
companyId: company!.id,
|
|
name: "Standard Engineer",
|
|
role: "engineer",
|
|
adapterType: "process",
|
|
adapterConfig: { token: canaries.agentConfig },
|
|
runtimeConfig: { env: { SECRET_MARKER: canaries.agentConfig } },
|
|
permissions: {},
|
|
})
|
|
.returning();
|
|
const [cto] = await db
|
|
.insert(agents)
|
|
.values({
|
|
companyId: company!.id,
|
|
name: "CTO",
|
|
role: "cto",
|
|
adapterType: "process",
|
|
adapterConfig: { token: canaries.agentConfig },
|
|
runtimeConfig: { env: { SECRET_MARKER: canaries.agentConfig } },
|
|
permissions: {},
|
|
})
|
|
.returning();
|
|
|
|
const [reviewGrandparent] = await db
|
|
.insert(issues)
|
|
.values({
|
|
companyId: company!.id,
|
|
projectId: allowedProject!.id,
|
|
title: "Review grandparent",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
assigneeAgentId: cto!.id,
|
|
responsibleUserId: "board-user",
|
|
})
|
|
.returning();
|
|
const [reviewRoot] = await db
|
|
.insert(issues)
|
|
.values({
|
|
companyId: company!.id,
|
|
projectId: allowedProject!.id,
|
|
parentId: reviewGrandparent!.id,
|
|
title: "Review root",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
assigneeAgentId: cto!.id,
|
|
responsibleUserId: "board-user",
|
|
})
|
|
.returning();
|
|
const [assignedReview] = await db
|
|
.insert(issues)
|
|
.values({
|
|
companyId: company!.id,
|
|
projectId: allowedProject!.id,
|
|
parentId: reviewRoot!.id,
|
|
title: "Assigned low-trust review",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
responsibleUserId: "board-user",
|
|
})
|
|
.returning();
|
|
const [sameBoundaryChild] = await db
|
|
.insert(issues)
|
|
.values({
|
|
companyId: company!.id,
|
|
projectId: allowedProject!.id,
|
|
parentId: reviewRoot!.id,
|
|
title: "Same boundary child",
|
|
status: "todo",
|
|
priority: "medium",
|
|
assigneeAgentId: cto!.id,
|
|
responsibleUserId: "board-user",
|
|
})
|
|
.returning();
|
|
const [standardChild] = await db
|
|
.insert(issues)
|
|
.values({
|
|
companyId: company!.id,
|
|
projectId: allowedProject!.id,
|
|
parentId: reviewRoot!.id,
|
|
title: "Assigned standard child",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
assigneeAgentId: standard!.id,
|
|
responsibleUserId: "board-user",
|
|
})
|
|
.returning();
|
|
const [siblingOutOfScope] = await db
|
|
.insert(issues)
|
|
.values({
|
|
companyId: company!.id,
|
|
projectId: outOfScopeProject!.id,
|
|
title: `Sibling ${canaries.issueSibling}`,
|
|
description: canaries.issueSibling,
|
|
status: "todo",
|
|
priority: "medium",
|
|
responsibleUserId: "board-user",
|
|
})
|
|
.returning();
|
|
|
|
const [lowTrust] = await db
|
|
.insert(agents)
|
|
.values({
|
|
companyId: company!.id,
|
|
name: "Low Trust Reviewer",
|
|
role: "engineer",
|
|
adapterType: "process",
|
|
adapterConfig: { token: canaries.agentConfig },
|
|
runtimeConfig: { env: { SECRET_MARKER: canaries.agentConfig } },
|
|
permissions: {
|
|
trustPreset: LOW_TRUST_REVIEW_PRESET,
|
|
authorizationPolicy: {
|
|
trustBoundary: {
|
|
mode: LOW_TRUST_REVIEW_PRESET,
|
|
companyId: company!.id,
|
|
projectIds: [allowedProject!.id],
|
|
rootIssueId: reviewRoot!.id,
|
|
issueIds: [
|
|
reviewRoot!.id,
|
|
assignedReview!.id,
|
|
sameBoundaryChild!.id,
|
|
],
|
|
allowedAgentIds: [collaborator!.id],
|
|
},
|
|
},
|
|
},
|
|
})
|
|
.returning();
|
|
|
|
await db
|
|
.update(issues)
|
|
.set({ assigneeAgentId: lowTrust!.id })
|
|
.where(eq(issues.id, assignedReview!.id));
|
|
assignedReview!.assigneeAgentId = lowTrust!.id;
|
|
|
|
const executionPolicy = {
|
|
authorizationPolicy: {
|
|
trustBoundary: (lowTrust!.permissions as any).authorizationPolicy
|
|
.trustBoundary,
|
|
},
|
|
};
|
|
const [lowTrustRun] = await db
|
|
.insert(heartbeatRuns)
|
|
.values({
|
|
companyId: company!.id,
|
|
agentId: lowTrust!.id,
|
|
status: "running",
|
|
contextSnapshot: {
|
|
issueId: assignedReview!.id,
|
|
executionPolicy,
|
|
},
|
|
})
|
|
.returning();
|
|
const [standardRun] = await db
|
|
.insert(heartbeatRuns)
|
|
.values({
|
|
companyId: company!.id,
|
|
agentId: standard!.id,
|
|
status: "running",
|
|
contextSnapshot: { issueId: assignedReview!.id },
|
|
})
|
|
.returning();
|
|
const [standardReportRun] = await db
|
|
.insert(heartbeatRuns)
|
|
.values({
|
|
companyId: company!.id,
|
|
agentId: standard!.id,
|
|
status: "running",
|
|
contextSnapshot: { issueId: standardChild!.id },
|
|
})
|
|
.returning();
|
|
await db
|
|
.update(issues)
|
|
.set({
|
|
checkoutRunId: lowTrustRun!.id,
|
|
executionRunId: lowTrustRun!.id,
|
|
executionPolicy,
|
|
})
|
|
.where(eq(issues.id, assignedReview!.id));
|
|
assignedReview!.checkoutRunId = lowTrustRun!.id;
|
|
assignedReview!.executionRunId = lowTrustRun!.id;
|
|
assignedReview!.executionPolicy = executionPolicy;
|
|
await db
|
|
.update(issues)
|
|
.set({
|
|
checkoutRunId: standardReportRun!.id,
|
|
executionRunId: standardReportRun!.id,
|
|
})
|
|
.where(eq(issues.id, standardChild!.id));
|
|
standardChild!.checkoutRunId = standardReportRun!.id;
|
|
standardChild!.executionRunId = standardReportRun!.id;
|
|
|
|
await db.insert(issueComments).values({
|
|
companyId: company!.id,
|
|
issueId: siblingOutOfScope!.id,
|
|
authorAgentId: standard!.id,
|
|
authorType: "agent",
|
|
body: canaries.commentSibling,
|
|
});
|
|
const [siblingDoc] = await db
|
|
.insert(documents)
|
|
.values({
|
|
companyId: company!.id,
|
|
title: "Sibling doc",
|
|
latestBody: canaries.documentSibling,
|
|
createdByAgentId: standard!.id,
|
|
updatedByAgentId: standard!.id,
|
|
})
|
|
.returning();
|
|
const [siblingRevision] = await db
|
|
.insert(documentRevisions)
|
|
.values({
|
|
companyId: company!.id,
|
|
documentId: siblingDoc!.id,
|
|
revisionNumber: 1,
|
|
title: "Sibling doc",
|
|
body: canaries.documentSibling,
|
|
createdByAgentId: standard!.id,
|
|
})
|
|
.returning();
|
|
await db
|
|
.update(documents)
|
|
.set({ latestRevisionId: siblingRevision!.id })
|
|
.where(eq(documents.id, siblingDoc!.id));
|
|
await db.insert(issueDocuments).values({
|
|
companyId: company!.id,
|
|
issueId: siblingOutOfScope!.id,
|
|
documentId: siblingDoc!.id,
|
|
key: "canary",
|
|
});
|
|
const [siblingAnnotationThread] = await db
|
|
.insert(documentAnnotationThreads)
|
|
.values({
|
|
companyId: company!.id,
|
|
issueId: siblingOutOfScope!.id,
|
|
documentId: siblingDoc!.id,
|
|
documentKey: "canary",
|
|
originalRevisionId: siblingRevision!.id,
|
|
originalRevisionNumber: 1,
|
|
currentRevisionId: siblingRevision!.id,
|
|
currentRevisionNumber: 1,
|
|
selectedText: "Sibling",
|
|
prefixText: "",
|
|
suffixText: " doc",
|
|
normalizedStart: 0,
|
|
normalizedEnd: 7,
|
|
markdownStart: 0,
|
|
markdownEnd: 7,
|
|
anchorSelector: {
|
|
quote: { exact: "Sibling", prefix: "", suffix: " doc" },
|
|
position: {
|
|
normalizedStart: 0,
|
|
normalizedEnd: 7,
|
|
markdownStart: 0,
|
|
markdownEnd: 7,
|
|
},
|
|
},
|
|
createdByAgentId: standard!.id,
|
|
})
|
|
.returning();
|
|
await db.insert(documentAnnotationComments).values({
|
|
companyId: company!.id,
|
|
threadId: siblingAnnotationThread!.id,
|
|
issueId: siblingOutOfScope!.id,
|
|
documentId: siblingDoc!.id,
|
|
body: canaries.annotationSibling,
|
|
authorType: "agent",
|
|
authorAgentId: standard!.id,
|
|
});
|
|
const [siblingAttachmentAsset] = await db
|
|
.insert(assets)
|
|
.values({
|
|
companyId: company!.id,
|
|
provider: "local_disk",
|
|
objectKey: `issues/${siblingOutOfScope!.id}/attachment-canary.txt`,
|
|
contentType: "text/plain",
|
|
byteSize: canaries.attachmentSibling.length,
|
|
sha256: `sha256-${nonce}`,
|
|
originalFilename: "attachment-canary.txt",
|
|
createdByAgentId: standard!.id,
|
|
})
|
|
.returning();
|
|
const [siblingAttachment] = await db
|
|
.insert(issueAttachments)
|
|
.values({
|
|
companyId: company!.id,
|
|
issueId: siblingOutOfScope!.id,
|
|
assetId: siblingAttachmentAsset!.id,
|
|
})
|
|
.returning();
|
|
const [siblingExternalObject] = await db
|
|
.insert(externalObjects)
|
|
.values({
|
|
companyId: company!.id,
|
|
providerKey: "url",
|
|
objectType: "link",
|
|
externalId: `external-${nonce}`,
|
|
sanitizedCanonicalUrl: "https://example.invalid/redacted",
|
|
canonicalIdentityHash: `external-hash-${nonce}`,
|
|
displayKey: "EXT-1",
|
|
displayTitle: canaries.externalObjectSibling,
|
|
data: { canary: canaries.externalObjectSibling },
|
|
})
|
|
.returning();
|
|
await db.insert(externalObjectMentions).values({
|
|
companyId: company!.id,
|
|
sourceIssueId: siblingOutOfScope!.id,
|
|
sourceKind: "description",
|
|
matchedTextRedacted: canaries.externalObjectSibling,
|
|
sanitizedDisplayUrl: "https://example.invalid/redacted",
|
|
canonicalIdentityHash: `external-hash-${nonce}`,
|
|
canonicalIdentity: { url: "https://example.invalid/redacted" },
|
|
objectId: siblingExternalObject!.id,
|
|
providerKey: "url",
|
|
detectorKey: "test",
|
|
objectType: "link",
|
|
});
|
|
await db.insert(issueWorkProducts).values({
|
|
companyId: company!.id,
|
|
projectId: outOfScopeProject!.id,
|
|
issueId: siblingOutOfScope!.id,
|
|
type: "artifact",
|
|
provider: "test",
|
|
title: "Sibling work product",
|
|
status: "active",
|
|
summary: canaries.workProductSibling,
|
|
});
|
|
const [approval] = await db
|
|
.insert(approvals)
|
|
.values({
|
|
companyId: company!.id,
|
|
type: "request_board_approval",
|
|
requestedByAgentId: standard!.id,
|
|
status: "pending",
|
|
payload: { summary: canaries.approval },
|
|
})
|
|
.returning();
|
|
await db.insert(issueApprovals).values({
|
|
companyId: company!.id,
|
|
issueId: assignedReview!.id,
|
|
approvalId: approval!.id,
|
|
linkedByAgentId: standard!.id,
|
|
});
|
|
await db.insert(issueApprovals).values({
|
|
companyId: company!.id,
|
|
issueId: siblingOutOfScope!.id,
|
|
approvalId: approval!.id,
|
|
linkedByAgentId: standard!.id,
|
|
});
|
|
|
|
return {
|
|
company: company!,
|
|
agents: {
|
|
lowTrust: lowTrust!,
|
|
standard: standard!,
|
|
collaborator: collaborator!,
|
|
cto: cto!,
|
|
},
|
|
projects: { allowed: allowedProject!, outOfScope: outOfScopeProject! },
|
|
issues: {
|
|
reviewGrandparent: reviewGrandparent!,
|
|
reviewRoot: reviewRoot!,
|
|
assignedReview: assignedReview!,
|
|
standardChild: standardChild!,
|
|
sameBoundaryChild: sameBoundaryChild!,
|
|
siblingOutOfScope: siblingOutOfScope!,
|
|
},
|
|
approvals: { issueLinkedCanary: approval! },
|
|
sensitiveRows: {
|
|
siblingAnnotationThreadId: siblingAnnotationThread!.id,
|
|
siblingAttachmentId: siblingAttachment!.id,
|
|
},
|
|
runs: {
|
|
lowTrust: lowTrustRun!,
|
|
standard: standardRun!,
|
|
standardReport: standardReportRun!,
|
|
},
|
|
canaries,
|
|
};
|
|
}
|
|
|
|
describeEmbeddedPostgres(
|
|
"low-trust red-team HTTP route regression suite",
|
|
() => {
|
|
let db!: Db;
|
|
let tempDb: Awaited<
|
|
ReturnType<typeof startEmbeddedPostgresTestDatabase>
|
|
> | null = null;
|
|
|
|
beforeAll(async () => {
|
|
tempDb = await startEmbeddedPostgresTestDatabase(
|
|
"paperclip-low-trust-red-team-routes-",
|
|
);
|
|
db = createDb(tempDb.connectionString);
|
|
}, 20_000);
|
|
|
|
afterEach(async () => {
|
|
// Await every in-flight background heartbeat run to quiescence before the
|
|
// deletes below. A route dispatches a wakeup fire-and-forget, so a run can
|
|
// still be writing issues, issue_comments, and heartbeat_runs rows when
|
|
// teardown starts and would race the deletes.
|
|
await drainHeartbeatRunsToQuiescence(db, heartbeatService(db));
|
|
await db.delete(issueThreadInteractions);
|
|
await db.delete(issueApprovals);
|
|
await db.delete(approvals);
|
|
await db.delete(issueWorkProducts);
|
|
await db.delete(issueAttachments);
|
|
await db.delete(assets);
|
|
await db.delete(externalObjectMentions);
|
|
await db.delete(externalObjects);
|
|
await db.delete(documentAnnotationComments);
|
|
await db.delete(documentAnnotationThreads);
|
|
await db.delete(issueDocuments);
|
|
await db.delete(documentRevisions);
|
|
await db.delete(documents);
|
|
await db.delete(issueComments);
|
|
await db.delete(issueRelations);
|
|
await db.delete(issueInboxArchives);
|
|
await db.delete(activityLog);
|
|
await db.delete(heartbeatRunEvents);
|
|
await deleteHeartbeatRunsAndWakeupsAfterActivityLogDrains(db);
|
|
await db.delete(issues);
|
|
await db.delete(agentRuntimeState);
|
|
await db.delete(principalPermissionGrants);
|
|
await db.delete(companyMemberships);
|
|
await db.delete(agents);
|
|
await db.delete(projects);
|
|
await deleteCompanySkillsAfterLateHeartbeatWritesDrain(db);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await tempDb?.cleanup();
|
|
});
|
|
|
|
it("allows bounded same-issue reads and writes while quarantining low-trust output", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
const app = createApp(db, agentActor(fixture));
|
|
|
|
const issueRead = await request(app).get(
|
|
`/api/issues/${fixture.issues.assignedReview.id}`,
|
|
);
|
|
expect(issueRead.status, JSON.stringify(issueRead.body)).toBe(200);
|
|
expectNoCanary(
|
|
issueRead.body,
|
|
fixture.canaries.issueSibling,
|
|
fixture.canaries.documentSibling,
|
|
);
|
|
|
|
const comment = await request(app)
|
|
.post(`/api/issues/${fixture.issues.assignedReview.id}/comments`)
|
|
.send({ body: `review note ${fixture.canaries.raw}` });
|
|
expect(comment.status, JSON.stringify(comment.body)).toBe(201);
|
|
expect(comment.body.sourceTrust).toMatchObject({
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "quarantined",
|
|
sourceIssueId: fixture.issues.assignedReview.id,
|
|
sourceRunId: fixture.runs.lowTrust.id,
|
|
sourceAgentId: fixture.agents.lowTrust.id,
|
|
});
|
|
|
|
const document = await request(app)
|
|
.put(
|
|
`/api/issues/${fixture.issues.assignedReview.id}/documents/review-notes`,
|
|
)
|
|
.send({ format: "markdown", body: `notes ${fixture.canaries.raw}` });
|
|
expect(document.status, JSON.stringify(document.body)).toBe(201);
|
|
expect(document.body.sourceTrust).toMatchObject({
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "quarantined",
|
|
});
|
|
|
|
const workProduct = await request(app)
|
|
.post(`/api/issues/${fixture.issues.assignedReview.id}/work-products`)
|
|
.send({
|
|
type: "artifact",
|
|
provider: "test",
|
|
title: "Review artifact",
|
|
status: "active",
|
|
summary: `artifact ${fixture.canaries.raw}`,
|
|
});
|
|
expect(workProduct.status, JSON.stringify(workProduct.body)).toBe(201);
|
|
expect(workProduct.body.sourceTrust).toMatchObject({
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "quarantined",
|
|
});
|
|
});
|
|
|
|
it("preserves direct-parent reporting while default-opening visible standard-trust writes", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
const standardApp = createApp(db, standardReportActor(fixture));
|
|
const lowTrustApp = createApp(db, agentActor(fixture));
|
|
|
|
const parentComment = await request(standardApp)
|
|
.post(`/api/issues/${fixture.issues.reviewRoot.id}/comments`)
|
|
.send({ body: "Direct parent report" });
|
|
expect(parentComment.status, JSON.stringify(parentComment.body)).toBe(
|
|
201,
|
|
);
|
|
|
|
const [audit] = await db
|
|
.select({ details: activityLog.details })
|
|
.from(activityLog)
|
|
.where(
|
|
and(
|
|
eq(activityLog.entityId, fixture.issues.reviewRoot.id),
|
|
eq(activityLog.action, "issue.comment_added"),
|
|
),
|
|
);
|
|
expect(audit?.details).toMatchObject({ directParentReportGrant: true });
|
|
|
|
const lowTrustParentComment = await request(lowTrustApp)
|
|
.post(`/api/issues/${fixture.issues.reviewRoot.id}/comments`)
|
|
.send({ body: "Contained report must not cross" });
|
|
expect(
|
|
lowTrustParentComment.status,
|
|
JSON.stringify(lowTrustParentComment.body),
|
|
).toBe(403);
|
|
|
|
const defaultOpenComments = [
|
|
request(standardApp)
|
|
.post(`/api/issues/${fixture.issues.reviewGrandparent.id}/comments`)
|
|
.send({ body: "Visible grandparent context" }),
|
|
request(standardApp)
|
|
.post(`/api/issues/${fixture.issues.sameBoundaryChild.id}/comments`)
|
|
.send({ body: "Visible sibling context" }),
|
|
];
|
|
for (const defaultOpenComment of defaultOpenComments) {
|
|
const response = await defaultOpenComment;
|
|
expect(response.status, JSON.stringify(response.body)).toBe(201);
|
|
}
|
|
|
|
const checkedOutPeerUpdate = await request(standardApp)
|
|
.patch(`/api/issues/${fixture.issues.reviewRoot.id}`)
|
|
.send({ status: "blocked" });
|
|
expect(
|
|
checkedOutPeerUpdate.status,
|
|
JSON.stringify(checkedOutPeerUpdate.body),
|
|
).toBe(409);
|
|
expect(checkedOutPeerUpdate.body.details.code).toBe(
|
|
"issue_write_assignee_run_lock",
|
|
);
|
|
|
|
const documentWrite = await request(standardApp)
|
|
.put(
|
|
`/api/issues/${fixture.issues.reviewRoot.id}/documents/upward-write`,
|
|
)
|
|
.send({ format: "markdown", body: "No upward document write" });
|
|
expect(documentWrite.status, JSON.stringify(documentWrite.body)).toBe(
|
|
409,
|
|
);
|
|
expect(documentWrite.body.details.code).toBe(
|
|
"issue_write_assignee_run_lock",
|
|
);
|
|
|
|
for (const closedParent of [
|
|
{ assigneeAgentId: null, intent: { reopen: true } },
|
|
{
|
|
assigneeAgentId: fixture.agents.standard.id,
|
|
intent: { resume: true },
|
|
},
|
|
]) {
|
|
await db
|
|
.update(issues)
|
|
.set({
|
|
status: "done",
|
|
assigneeAgentId: closedParent.assigneeAgentId,
|
|
})
|
|
.where(eq(issues.id, fixture.issues.reviewRoot.id));
|
|
|
|
const closedParentComment = await request(standardApp)
|
|
.post(`/api/issues/${fixture.issues.reviewRoot.id}/comments`)
|
|
.send({
|
|
body: "Comment only on closed parent",
|
|
...closedParent.intent,
|
|
});
|
|
expect(
|
|
closedParentComment.status,
|
|
JSON.stringify(closedParentComment.body),
|
|
).toBe(201);
|
|
|
|
const [persistedParent] = await db
|
|
.select({ status: issues.status })
|
|
.from(issues)
|
|
.where(eq(issues.id, fixture.issues.reviewRoot.id));
|
|
expect(persistedParent?.status).toBe("done");
|
|
}
|
|
});
|
|
|
|
it("relays blocked and cancelled stops once without laundering child prose", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
const app = createApp(db, boardActor(fixture));
|
|
const unblockDescriptor = {
|
|
owner: "board",
|
|
action: "Review the low-trust stop",
|
|
} as const;
|
|
|
|
await db
|
|
.delete(issueApprovals)
|
|
.where(eq(issueApprovals.issueId, fixture.issues.assignedReview.id));
|
|
|
|
const blocked = await request(app)
|
|
.patch(`/api/issues/${fixture.issues.assignedReview.id}`)
|
|
.send({
|
|
status: "blocked",
|
|
comment: fixture.canaries.raw,
|
|
unblockDescriptor,
|
|
});
|
|
expect(blocked.status, JSON.stringify(blocked.body)).toBe(200);
|
|
expect(blocked.body.unblockDescriptor).toEqual(unblockDescriptor);
|
|
|
|
await request(app)
|
|
.patch(`/api/issues/${fixture.issues.assignedReview.id}`)
|
|
.send({ status: "todo" })
|
|
.expect(200);
|
|
await request(app)
|
|
.patch(`/api/issues/${fixture.issues.assignedReview.id}`)
|
|
.send({ status: "blocked", unblockDescriptor })
|
|
.expect(200);
|
|
await request(app)
|
|
.patch(`/api/issues/${fixture.issues.assignedReview.id}`)
|
|
.send({ status: "todo" })
|
|
.expect(200);
|
|
await request(app)
|
|
.patch(`/api/issues/${fixture.issues.assignedReview.id}`)
|
|
.send({ status: "cancelled" })
|
|
.expect(200);
|
|
await request(app)
|
|
.patch(`/api/issues/${fixture.issues.assignedReview.id}`)
|
|
.send({ status: "todo" })
|
|
.expect(200);
|
|
await db
|
|
.update(issues)
|
|
.set({ parentId: null })
|
|
.where(eq(issues.id, fixture.issues.assignedReview.id));
|
|
await request(app)
|
|
.patch(`/api/issues/${fixture.issues.assignedReview.id}`)
|
|
.send({
|
|
parentId: fixture.issues.reviewGrandparent.id,
|
|
status: "blocked",
|
|
unblockDescriptor,
|
|
})
|
|
.expect(200);
|
|
|
|
await request(app)
|
|
.patch(`/api/issues/${fixture.issues.standardChild.id}`)
|
|
.send({ status: "blocked", unblockDescriptor })
|
|
.expect(200);
|
|
await request(app)
|
|
.patch(`/api/issues/${fixture.issues.standardChild.id}`)
|
|
.send({ status: "todo" })
|
|
.expect(200);
|
|
await request(app)
|
|
.patch(`/api/issues/${fixture.issues.standardChild.id}`)
|
|
.send({ status: "in_review" })
|
|
.expect(200);
|
|
await request(app)
|
|
.patch(`/api/issues/${fixture.issues.standardChild.id}`)
|
|
.send({ status: "done" })
|
|
.expect(200);
|
|
|
|
const relayComments = await db
|
|
.select({
|
|
body: issueComments.body,
|
|
authorType: issueComments.authorType,
|
|
})
|
|
.from(issueComments)
|
|
.where(
|
|
and(
|
|
eq(issueComments.issueId, fixture.issues.reviewRoot.id),
|
|
eq(issueComments.authorType, "system"),
|
|
),
|
|
);
|
|
expect(relayComments).toHaveLength(2);
|
|
expect(relayComments.map((comment) => comment.body)).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringContaining(`transitioned to \`blocked\``),
|
|
expect.stringContaining(`transitioned to \`cancelled\``),
|
|
]),
|
|
);
|
|
for (const relay of relayComments) {
|
|
expect(relay.authorType).toBe("system");
|
|
expect(relay.body).toContain(
|
|
fixture.issues.assignedReview.identifier ??
|
|
fixture.issues.assignedReview.id,
|
|
);
|
|
expect(relay.body).not.toContain(fixture.canaries.raw);
|
|
expect(relay.body).not.toContain("in_review");
|
|
expect(relay.body).not.toContain("done");
|
|
expect(relay.body).not.toContain(
|
|
fixture.issues.standardChild.identifier,
|
|
);
|
|
}
|
|
|
|
const reparentedRelayComments = await db
|
|
.select({
|
|
body: issueComments.body,
|
|
authorType: issueComments.authorType,
|
|
})
|
|
.from(issueComments)
|
|
.where(
|
|
and(
|
|
eq(issueComments.issueId, fixture.issues.reviewGrandparent.id),
|
|
eq(issueComments.authorType, "system"),
|
|
),
|
|
);
|
|
expect(reparentedRelayComments).toHaveLength(1);
|
|
expect(reparentedRelayComments[0]?.body).toContain(
|
|
"transitioned to `blocked`",
|
|
);
|
|
expect(reparentedRelayComments[0]?.body).not.toContain(
|
|
fixture.canaries.raw,
|
|
);
|
|
});
|
|
|
|
it("allows mentioned low-trust agents to comment on out-of-bound assigned issues", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
const [targetIssue] = await db
|
|
.insert(issues)
|
|
.values({
|
|
companyId: fixture.company.id,
|
|
projectId: fixture.projects.outOfScope.id,
|
|
title: "Coach-owned mention target",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
assigneeAgentId: fixture.agents.standard.id,
|
|
responsibleUserId: "board-user",
|
|
})
|
|
.returning();
|
|
await db.insert(issueComments).values({
|
|
companyId: fixture.company.id,
|
|
issueId: targetIssue!.id,
|
|
authorAgentId: fixture.agents.standard.id,
|
|
authorType: "agent",
|
|
body: `[@Low Trust Reviewer](agent://${fixture.agents.lowTrust.id}) please verify this issue.`,
|
|
});
|
|
|
|
const unmentioned = await db
|
|
.insert(agents)
|
|
.values({
|
|
companyId: fixture.company.id,
|
|
name: "Unmentioned Low Trust Reviewer",
|
|
role: "engineer",
|
|
adapterType: "process",
|
|
adapterConfig: {},
|
|
runtimeConfig: {},
|
|
permissions: fixture.agents.lowTrust.permissions,
|
|
})
|
|
.returning()
|
|
.then((rows) => rows[0]!);
|
|
|
|
const comment = await request(createApp(db, agentActor(fixture)))
|
|
.post(`/api/issues/${targetIssue!.id}/comments`)
|
|
.send({ body: "Mention-scoped verification complete." });
|
|
expect(comment.status, JSON.stringify(comment.body)).toBe(201);
|
|
expect(comment.body).toMatchObject({
|
|
issueId: targetIssue!.id,
|
|
authorAgentId: fixture.agents.lowTrust.id,
|
|
});
|
|
|
|
const unmentionedComment = await request(
|
|
createApp(db, agentActor(fixture, unmentioned.id)),
|
|
)
|
|
.post(`/api/issues/${targetIssue!.id}/comments`)
|
|
.send({ body: "I was not mentioned." });
|
|
expect(
|
|
unmentionedComment.status,
|
|
JSON.stringify(unmentionedComment.body),
|
|
).toBe(403);
|
|
expect(unmentionedComment.body.details.code).toBe(
|
|
"issue_write_actor_class_excluded",
|
|
);
|
|
});
|
|
|
|
it("propagates denied low-trust policy conflicts on control-plane guards", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
const conflictingExecutionPolicy = {
|
|
authorizationPolicy: {
|
|
trustBoundary: {
|
|
mode: LOW_TRUST_REVIEW_PRESET,
|
|
companyId: fixture.company.id,
|
|
rootIssueId: fixture.issues.siblingOutOfScope.id,
|
|
},
|
|
},
|
|
};
|
|
await db
|
|
.update(heartbeatRuns)
|
|
.set({
|
|
contextSnapshot: {
|
|
issueId: fixture.issues.assignedReview.id,
|
|
executionPolicy: conflictingExecutionPolicy,
|
|
},
|
|
})
|
|
.where(eq(heartbeatRuns.id, fixture.runs.lowTrust.id));
|
|
|
|
const res = await request(createApp(db, agentActor(fixture))).get(
|
|
`/api/issues/${fixture.issues.assignedReview.id}/approvals`,
|
|
);
|
|
|
|
expect(res.status, JSON.stringify(res.body)).toBe(403);
|
|
expect(res.body.error).toBe(
|
|
"Low-trust boundary root issue scopes do not overlap.",
|
|
);
|
|
});
|
|
|
|
it("restricts low-trust self inspection without changing standard-agent visibility", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
await db.insert(companyMemberships).values({
|
|
companyId: fixture.company.id,
|
|
principalType: "agent",
|
|
principalId: fixture.agents.lowTrust.id,
|
|
status: "active",
|
|
membershipRole: "member",
|
|
});
|
|
await db.insert(principalPermissionGrants).values([
|
|
{
|
|
companyId: fixture.company.id,
|
|
principalType: "agent",
|
|
principalId: fixture.agents.lowTrust.id,
|
|
permissionKey: "agents:configure",
|
|
grantedByUserId: null,
|
|
},
|
|
{
|
|
companyId: fixture.company.id,
|
|
principalType: "agent",
|
|
principalId: fixture.agents.lowTrust.id,
|
|
permissionKey: "skills:create",
|
|
grantedByUserId: null,
|
|
},
|
|
]);
|
|
|
|
const lowTrustRes = await request(createApp(db, agentActor(fixture))).get(
|
|
"/api/agents/me",
|
|
);
|
|
expect(lowTrustRes.status, JSON.stringify(lowTrustRes.body)).toBe(200);
|
|
expect(lowTrustRes.body).toMatchObject({
|
|
id: fixture.agents.lowTrust.id,
|
|
companyId: fixture.company.id,
|
|
trustPreset: LOW_TRUST_REVIEW_PRESET,
|
|
});
|
|
expect(lowTrustRes.body).not.toHaveProperty("adapterConfig");
|
|
expect(lowTrustRes.body).not.toHaveProperty("runtimeConfig");
|
|
expect(lowTrustRes.body).not.toHaveProperty("permissions");
|
|
expect(lowTrustRes.body).not.toHaveProperty("access");
|
|
expectNoCanary(lowTrustRes.body, fixture.canaries.agentConfig);
|
|
|
|
const lowTrustSelfByIdRes = await request(
|
|
createApp(db, agentActor(fixture)),
|
|
).get(`/api/agents/${fixture.agents.lowTrust.id}`);
|
|
expect(
|
|
lowTrustSelfByIdRes.status,
|
|
JSON.stringify(lowTrustSelfByIdRes.body),
|
|
).toBe(200);
|
|
expect(lowTrustSelfByIdRes.body).toMatchObject({
|
|
id: fixture.agents.lowTrust.id,
|
|
companyId: fixture.company.id,
|
|
trustPreset: LOW_TRUST_REVIEW_PRESET,
|
|
});
|
|
expect(lowTrustSelfByIdRes.body).not.toHaveProperty("adapterConfig");
|
|
expect(lowTrustSelfByIdRes.body).not.toHaveProperty("runtimeConfig");
|
|
expect(lowTrustSelfByIdRes.body).not.toHaveProperty("permissions");
|
|
expect(lowTrustSelfByIdRes.body).not.toHaveProperty("access");
|
|
expectNoCanary(lowTrustSelfByIdRes.body, fixture.canaries.agentConfig);
|
|
|
|
const lowTrustPeerConfigRes = await request(
|
|
createApp(db, agentActor(fixture)),
|
|
).get(`/api/agents/${fixture.agents.collaborator.id}/configuration`);
|
|
expect(
|
|
lowTrustPeerConfigRes.status,
|
|
JSON.stringify(lowTrustPeerConfigRes.body),
|
|
).toBe(403);
|
|
expectNoCanary(lowTrustPeerConfigRes.body, fixture.canaries.agentConfig);
|
|
|
|
const lowTrustSelfBundleRes = await request(
|
|
createApp(db, agentActor(fixture)),
|
|
).get(`/api/agents/${fixture.agents.lowTrust.id}/instructions-bundle`);
|
|
expect(
|
|
lowTrustSelfBundleRes.status,
|
|
JSON.stringify(lowTrustSelfBundleRes.body),
|
|
).toBe(403);
|
|
expectNoCanary(lowTrustSelfBundleRes.body, fixture.canaries.agentConfig);
|
|
|
|
const standardActor = agentActor(fixture, fixture.agents.standard.id);
|
|
const standardRes = await request(
|
|
createApp(db, { ...standardActor, runId: null }),
|
|
).get("/api/agents/me");
|
|
expect(standardRes.status, JSON.stringify(standardRes.body)).toBe(200);
|
|
expect(JSON.stringify(standardRes.body)).toContain(
|
|
fixture.canaries.agentConfig,
|
|
);
|
|
|
|
const issueScopedLowTrustRes = await request(
|
|
createApp(db, standardActor),
|
|
).get("/api/agents/me");
|
|
expect(
|
|
issueScopedLowTrustRes.status,
|
|
JSON.stringify(issueScopedLowTrustRes.body),
|
|
).toBe(200);
|
|
expect(issueScopedLowTrustRes.body).toMatchObject({
|
|
id: fixture.agents.standard.id,
|
|
companyId: fixture.company.id,
|
|
trustPreset: LOW_TRUST_REVIEW_PRESET,
|
|
});
|
|
expect(issueScopedLowTrustRes.body).not.toHaveProperty("adapterConfig");
|
|
expect(issueScopedLowTrustRes.body).not.toHaveProperty("runtimeConfig");
|
|
expectNoCanary(issueScopedLowTrustRes.body, fixture.canaries.agentConfig);
|
|
|
|
for (const restrictedActor of [
|
|
skillTestActor(fixture),
|
|
{
|
|
...standardActor,
|
|
source: "agent_key" as const,
|
|
keyScope: {
|
|
kind: "task_bridge" as const,
|
|
parentIssueId: fixture.issues.assignedReview.id,
|
|
},
|
|
},
|
|
]) {
|
|
const restrictedRes = await request(createApp(db, restrictedActor)).get(
|
|
"/api/agents/me",
|
|
);
|
|
expect(restrictedRes.status, JSON.stringify(restrictedRes.body)).toBe(
|
|
200,
|
|
);
|
|
expect(restrictedRes.body).toMatchObject({
|
|
id: fixture.agents.standard.id,
|
|
companyId: fixture.company.id,
|
|
keyScope: restrictedActor.keyScope,
|
|
});
|
|
expect(restrictedRes.body).not.toHaveProperty("adapterConfig");
|
|
expect(restrictedRes.body).not.toHaveProperty("runtimeConfig");
|
|
expect(restrictedRes.body).not.toHaveProperty("permissions");
|
|
expect(restrictedRes.body).not.toHaveProperty("access");
|
|
expectNoCanary(restrictedRes.body, fixture.canaries.agentConfig);
|
|
}
|
|
|
|
await db
|
|
.update(issues)
|
|
.set({ executionPolicy: null })
|
|
.where(eq(issues.id, fixture.issues.assignedReview.id));
|
|
|
|
await db
|
|
.update(projects)
|
|
.set({
|
|
executionWorkspacePolicy: {
|
|
authorizationPolicy: {
|
|
trustBoundary: {
|
|
mode: LOW_TRUST_REVIEW_PRESET,
|
|
companyId: fixture.company.id,
|
|
projectIds: [fixture.projects.allowed.id],
|
|
},
|
|
},
|
|
},
|
|
})
|
|
.where(eq(projects.id, fixture.projects.allowed.id));
|
|
|
|
const projectScopedLowTrustRes = await request(
|
|
createApp(db, agentActor(fixture, fixture.agents.standard.id)),
|
|
).get("/api/agents/me");
|
|
expect(
|
|
projectScopedLowTrustRes.status,
|
|
JSON.stringify(projectScopedLowTrustRes.body),
|
|
).toBe(200);
|
|
expect(projectScopedLowTrustRes.body).toMatchObject({
|
|
id: fixture.agents.standard.id,
|
|
companyId: fixture.company.id,
|
|
trustPreset: LOW_TRUST_REVIEW_PRESET,
|
|
});
|
|
expect(projectScopedLowTrustRes.body).not.toHaveProperty("adapterConfig");
|
|
expect(projectScopedLowTrustRes.body).not.toHaveProperty("runtimeConfig");
|
|
expectNoCanary(
|
|
projectScopedLowTrustRes.body,
|
|
fixture.canaries.agentConfig,
|
|
);
|
|
});
|
|
|
|
it("denies out-of-bound and control-plane attempts without leaking canaries or creating durable side effects", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
const app = createApp(db, agentActor(fixture));
|
|
const forbiddenMarkers = Object.values(fixture.canaries);
|
|
|
|
const attempts = [
|
|
{
|
|
id: "LT-02",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT-08",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/documents/canary`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT-08 revisions",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/documents/canary/revisions`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT-08 annotations",
|
|
req: () =>
|
|
request(app)
|
|
.get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/documents/canary/annotations`,
|
|
)
|
|
.query({ includeComments: "true" }),
|
|
},
|
|
{
|
|
id: "LT-08 annotation thread",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/documents/canary/annotations/${fixture.sensitiveRows.siblingAnnotationThreadId}`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT recovery actions",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/recovery-actions`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT external objects",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/external-objects`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT external object summary",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/external-object-summary`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT approvals",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/approvals`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT attachments",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/attachments`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT attachment content",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/attachments/${fixture.sensitiveRows.siblingAttachmentId}/content`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT-15/16",
|
|
req: () => request(app).get(`/api/agents/${fixture.agents.cto.id}`),
|
|
},
|
|
{
|
|
id: "LT-19",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.assignedReview.id}/approvals`,
|
|
),
|
|
},
|
|
{
|
|
id: "LT-26 child",
|
|
req: () =>
|
|
request(app)
|
|
.post(`/api/issues/${fixture.issues.assignedReview.id}/children`)
|
|
.send({ title: `child ${fixture.canaries.issueSibling}` }),
|
|
},
|
|
{
|
|
id: "LT-26 company issue",
|
|
req: () =>
|
|
request(app)
|
|
.post(`/api/companies/${fixture.company.id}/issues`)
|
|
.send({
|
|
title: `child ${fixture.canaries.issueSibling}`,
|
|
parentId: fixture.issues.assignedReview.id,
|
|
}),
|
|
},
|
|
{
|
|
id: "LT-26 interaction",
|
|
req: () =>
|
|
request(app)
|
|
.post(
|
|
`/api/issues/${fixture.issues.assignedReview.id}/interactions`,
|
|
)
|
|
.send({
|
|
kind: "ask_user_questions",
|
|
title: "exfil",
|
|
payload: {
|
|
version: 1,
|
|
questions: [
|
|
{
|
|
id: "q1",
|
|
prompt: fixture.canaries.approval,
|
|
selectionMode: "single",
|
|
options: [
|
|
{ id: "a", label: "A", description: "A" },
|
|
{ id: "b", label: "B", description: "B" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
id: "LT-06 resume",
|
|
req: () =>
|
|
request(app)
|
|
.post(`/api/issues/${fixture.issues.assignedReview.id}/comments`)
|
|
.send({ body: "resume please", resume: true }),
|
|
},
|
|
{
|
|
id: "LT-06 blocker mutation",
|
|
req: () =>
|
|
request(app)
|
|
.patch(`/api/issues/${fixture.issues.assignedReview.id}`)
|
|
.send({
|
|
comment: "add blocker",
|
|
blockedByIssueIds: [fixture.issues.siblingOutOfScope.id],
|
|
}),
|
|
},
|
|
];
|
|
|
|
for (const attempt of attempts) {
|
|
const before = await snapshot(db);
|
|
const res = await attempt.req();
|
|
expect(res.status, `${attempt.id}: ${JSON.stringify(res.body)}`).toBe(
|
|
403,
|
|
);
|
|
expectNoCanary(res.body, ...forbiddenMarkers);
|
|
const after = await snapshot(db);
|
|
expect(after.issues.length, attempt.id).toBe(before.issues.length);
|
|
expect(after.comments.length, attempt.id).toBe(before.comments.length);
|
|
expect(after.documents.length, attempt.id).toBe(
|
|
before.documents.length,
|
|
);
|
|
expect(after.workProducts.length, attempt.id).toBe(
|
|
before.workProducts.length,
|
|
);
|
|
expect(after.approvals.length, attempt.id).toBe(
|
|
before.approvals.length,
|
|
);
|
|
expect(after.relations.length, attempt.id).toBe(
|
|
before.relations.length,
|
|
);
|
|
expect(after.interactions.length, attempt.id).toBe(
|
|
before.interactions.length,
|
|
);
|
|
expect(after.wakeups.length, attempt.id).toBe(before.wakeups.length);
|
|
expect(after.runs.length, attempt.id).toBe(before.runs.length);
|
|
}
|
|
|
|
const beforeBulkSummary = await snapshot(db);
|
|
const bulkSummary = await request(app)
|
|
.post(
|
|
`/api/companies/${fixture.company.id}/issues/external-object-summaries`,
|
|
)
|
|
.send({ issueIds: [fixture.issues.siblingOutOfScope.id] });
|
|
expect(bulkSummary.status, JSON.stringify(bulkSummary.body)).toBe(200);
|
|
expect(bulkSummary.body.summaries).toEqual({});
|
|
expectNoCanary(bulkSummary.body, ...forbiddenMarkers);
|
|
const afterBulkSummary = await snapshot(db);
|
|
expect(afterBulkSummary.issues.length).toBe(
|
|
beforeBulkSummary.issues.length,
|
|
);
|
|
expect(afterBulkSummary.comments.length).toBe(
|
|
beforeBulkSummary.comments.length,
|
|
);
|
|
expect(afterBulkSummary.documents.length).toBe(
|
|
beforeBulkSummary.documents.length,
|
|
);
|
|
expect(afterBulkSummary.workProducts.length).toBe(
|
|
beforeBulkSummary.workProducts.length,
|
|
);
|
|
expect(afterBulkSummary.approvals.length).toBe(
|
|
beforeBulkSummary.approvals.length,
|
|
);
|
|
expect(afterBulkSummary.relations.length).toBe(
|
|
beforeBulkSummary.relations.length,
|
|
);
|
|
expect(afterBulkSummary.interactions.length).toBe(
|
|
beforeBulkSummary.interactions.length,
|
|
);
|
|
expect(afterBulkSummary.wakeups.length).toBe(
|
|
beforeBulkSummary.wakeups.length,
|
|
);
|
|
expect(afterBulkSummary.runs.length).toBe(beforeBulkSummary.runs.length);
|
|
});
|
|
|
|
it("denies skill-test scoped tokens on foreign issue-adjacent reads", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
const app = createApp(db, skillTestActor(fixture));
|
|
const forbiddenMarkers = Object.values(fixture.canaries);
|
|
|
|
const ownIssue = await request(app).get(
|
|
`/api/issues/${fixture.issues.assignedReview.id}`,
|
|
);
|
|
expect(ownIssue.status, JSON.stringify(ownIssue.body)).toBe(200);
|
|
|
|
const attempts = [
|
|
{
|
|
id: "skill-test attachments",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/attachments`,
|
|
),
|
|
},
|
|
{
|
|
id: "skill-test attachment content",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/attachments/${fixture.sensitiveRows.siblingAttachmentId}/content`,
|
|
),
|
|
},
|
|
{
|
|
id: "skill-test document revisions",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/documents/canary/revisions`,
|
|
),
|
|
},
|
|
{
|
|
id: "skill-test annotations",
|
|
req: () =>
|
|
request(app)
|
|
.get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/documents/canary/annotations`,
|
|
)
|
|
.query({ includeComments: "true" }),
|
|
},
|
|
{
|
|
id: "skill-test annotation thread",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/documents/canary/annotations/${fixture.sensitiveRows.siblingAnnotationThreadId}`,
|
|
),
|
|
},
|
|
{
|
|
id: "skill-test approvals",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/approvals`,
|
|
),
|
|
},
|
|
{
|
|
id: "skill-test recovery actions",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/recovery-actions`,
|
|
),
|
|
},
|
|
{
|
|
id: "skill-test external objects",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/external-objects`,
|
|
),
|
|
},
|
|
{
|
|
id: "skill-test external object summary",
|
|
req: () =>
|
|
request(app).get(
|
|
`/api/issues/${fixture.issues.siblingOutOfScope.id}/external-object-summary`,
|
|
),
|
|
},
|
|
];
|
|
|
|
for (const attempt of attempts) {
|
|
const res = await attempt.req();
|
|
expect(res.status, `${attempt.id}: ${JSON.stringify(res.body)}`).toBe(
|
|
403,
|
|
);
|
|
expectNoCanary(res.body, ...forbiddenMarkers);
|
|
}
|
|
|
|
const bulkSummary = await request(app)
|
|
.post(
|
|
`/api/companies/${fixture.company.id}/issues/external-object-summaries`,
|
|
)
|
|
.send({ issueIds: [fixture.issues.siblingOutOfScope.id] });
|
|
expect(bulkSummary.status, JSON.stringify(bulkSummary.body)).toBe(200);
|
|
expect(bulkSummary.body.summaries).toEqual({});
|
|
expectNoCanary(bulkSummary.body, ...forbiddenMarkers);
|
|
});
|
|
|
|
it("counts blocked inbox issues with the low-trust boundary applied in the database", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
await db.insert(issues).values([
|
|
{
|
|
companyId: fixture.company.id,
|
|
projectId: fixture.projects.allowed.id,
|
|
parentId: fixture.issues.reviewRoot.id,
|
|
title: "Visible blocked vendor wait",
|
|
status: "blocked",
|
|
priority: "medium",
|
|
description:
|
|
"external owner: Visible vendor\nexternal action: Finish visible review",
|
|
},
|
|
{
|
|
companyId: fixture.company.id,
|
|
projectId: fixture.projects.outOfScope.id,
|
|
title: "Hidden blocked vendor wait",
|
|
status: "blocked",
|
|
priority: "medium",
|
|
description:
|
|
"external owner: Hidden vendor\nexternal action: Finish hidden review",
|
|
},
|
|
]);
|
|
|
|
const boardCount = await request(createApp(db, boardActor(fixture)))
|
|
.get(`/api/companies/${fixture.company.id}/issues/count`)
|
|
.query({ attention: "blocked", q: "blocked vendor wait" });
|
|
expect(boardCount.status, JSON.stringify(boardCount.body)).toBe(200);
|
|
expect(boardCount.body.count).toBe(2);
|
|
|
|
const lowTrustCount = await request(createApp(db, agentActor(fixture)))
|
|
.get(`/api/companies/${fixture.company.id}/issues/count`)
|
|
.query({ attention: "blocked", q: "blocked vendor wait" });
|
|
expect(lowTrustCount.status, JSON.stringify(lowTrustCount.body)).toBe(
|
|
200,
|
|
);
|
|
expect(lowTrustCount.body.count).toBe(1);
|
|
});
|
|
|
|
it("redacts quarantined low-trust output from higher-trust wake and continuation contexts", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
const lowTrustApp = createApp(db, agentActor(fixture));
|
|
const standardApp = createApp(
|
|
db,
|
|
agentActor(fixture, fixture.agents.standard.id),
|
|
);
|
|
const gateway = await createControlledGatewayServer();
|
|
const heartbeat = heartbeatService(db, {
|
|
runtimeEnv: {
|
|
...process.env,
|
|
PAPERCLIP_IN_WORKTREE: "false",
|
|
PAPERCLIP_DATABASE_RESTORE_IN_PROGRESS: "false",
|
|
PAPERCLIP_RESTORE_IN_PROGRESS: "false",
|
|
},
|
|
});
|
|
|
|
try {
|
|
const comment = await request(lowTrustApp)
|
|
.post(`/api/issues/${fixture.issues.assignedReview.id}/comments`)
|
|
.send({
|
|
body: `malicious result ${fixture.canaries.raw}`,
|
|
});
|
|
expect(comment.status, JSON.stringify(comment.body)).toBe(201);
|
|
await db
|
|
.update(issueComments)
|
|
.set({
|
|
metadata: { canary: fixture.canaries.raw },
|
|
presentation: { markdown: fixture.canaries.raw },
|
|
})
|
|
.where(eq(issueComments.id, comment.body.id));
|
|
|
|
await createQuarantinedContinuationSummary(
|
|
db,
|
|
fixture,
|
|
fixture.issues.reviewRoot.id,
|
|
);
|
|
|
|
const lowTrustContext = await request(lowTrustApp)
|
|
.get(
|
|
`/api/issues/${fixture.issues.assignedReview.id}/heartbeat-context`,
|
|
)
|
|
.query({ wakeCommentId: comment.body.id });
|
|
expect(
|
|
lowTrustContext.status,
|
|
JSON.stringify(lowTrustContext.body),
|
|
).toBe(200);
|
|
expect(JSON.stringify(lowTrustContext.body.wakeComment)).toContain(
|
|
fixture.canaries.raw,
|
|
);
|
|
|
|
// Higher-trust wakes may only hydrate comments on the exact task. Seed a
|
|
// quarantined same-task comment to preserve the raw-output redaction
|
|
// assertion without relying on a cross-task comment lookup.
|
|
const scopedComment = await db
|
|
.insert(issueComments)
|
|
.values({
|
|
companyId: fixture.company.id,
|
|
issueId: fixture.issues.reviewRoot.id,
|
|
authorAgentId: fixture.agents.lowTrust.id,
|
|
createdByRunId: fixture.runs.lowTrust.id,
|
|
body: `malicious scoped result ${fixture.canaries.raw}`,
|
|
metadata: { canary: fixture.canaries.raw },
|
|
presentation: { markdown: fixture.canaries.raw },
|
|
sourceTrust: {
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "quarantined",
|
|
sourceIssueId: fixture.issues.assignedReview.id,
|
|
sourceRunId: fixture.runs.lowTrust.id,
|
|
sourceAgentId: fixture.agents.lowTrust.id,
|
|
},
|
|
})
|
|
.returning()
|
|
.then((rows) => rows[0]!);
|
|
|
|
const higherTrustContext = await request(standardApp).get(
|
|
`/api/issues/${fixture.issues.reviewRoot.id}/heartbeat-context`,
|
|
);
|
|
expect(
|
|
higherTrustContext.status,
|
|
JSON.stringify(higherTrustContext.body),
|
|
).toBe(200);
|
|
expect(higherTrustContext.body.continuationSummary).toMatchObject({
|
|
body: LOW_TRUST_QUARANTINED_BODY,
|
|
sourceTrust: {
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "quarantined",
|
|
sourceIssueId: fixture.issues.assignedReview.id,
|
|
sourceRunId: fixture.runs.lowTrust.id,
|
|
sourceAgentId: fixture.agents.lowTrust.id,
|
|
},
|
|
});
|
|
expectNoCanary(higherTrustContext.body, fixture.canaries.raw);
|
|
|
|
const bogusRunStandardApp = createApp(db, {
|
|
...agentActor(fixture, fixture.agents.standard.id),
|
|
runId: randomUUID(),
|
|
});
|
|
const bogusRunContext = await request(bogusRunStandardApp).get(
|
|
`/api/issues/${fixture.issues.reviewRoot.id}/heartbeat-context`,
|
|
);
|
|
expect(
|
|
bogusRunContext.status,
|
|
JSON.stringify(bogusRunContext.body),
|
|
).toBe(200);
|
|
expect(bogusRunContext.body.continuationSummary).toMatchObject({
|
|
body: LOW_TRUST_QUARANTINED_BODY,
|
|
sourceTrust: {
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "quarantined",
|
|
},
|
|
});
|
|
expectNoCanary(bogusRunContext.body, fixture.canaries.raw);
|
|
|
|
await db
|
|
.update(heartbeatRuns)
|
|
.set({
|
|
status: "succeeded",
|
|
finishedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(heartbeatRuns.id, fixture.runs.standard.id));
|
|
await db
|
|
.update(agents)
|
|
.set({
|
|
status: "idle",
|
|
adapterType: "openclaw_gateway",
|
|
adapterConfig: {
|
|
url: gateway.url,
|
|
headers: {
|
|
"x-openclaw-token": "gateway-token",
|
|
},
|
|
payloadTemplate: {
|
|
message: "wake now",
|
|
},
|
|
waitTimeoutMs: 2_000,
|
|
},
|
|
runtimeConfig: { heartbeat: { wakeOnDemand: true } },
|
|
})
|
|
.where(eq(agents.id, fixture.agents.standard.id));
|
|
await db
|
|
.update(heartbeatRuns)
|
|
.set({
|
|
status: "succeeded",
|
|
finishedAt: new Date("2026-05-14T12:02:00.000Z"),
|
|
})
|
|
.where(eq(heartbeatRuns.id, fixture.runs.standard.id));
|
|
|
|
const run = await heartbeat.wakeup(fixture.agents.standard.id, {
|
|
source: "automation",
|
|
triggerDetail: "system",
|
|
reason: "issue_commented",
|
|
payload: {
|
|
issueId: fixture.issues.reviewRoot.id,
|
|
commentId: scopedComment.id,
|
|
},
|
|
contextSnapshot: {
|
|
issueId: fixture.issues.reviewRoot.id,
|
|
taskId: fixture.issues.reviewRoot.id,
|
|
wakeCommentId: scopedComment.id,
|
|
wakeCommentIds: [scopedComment.id],
|
|
wakeReason: "issue_commented",
|
|
livenessContinuationAttempt: 1,
|
|
livenessContinuationMaxAttempts: 2,
|
|
livenessContinuationSourceRunId: fixture.runs.lowTrust.id,
|
|
livenessContinuationState: "quarantined_low_trust_handoff",
|
|
livenessContinuationReason:
|
|
"Low-trust review output requires sanitized follow-up.",
|
|
livenessContinuationInstruction:
|
|
"Continue from the sanitized quarantine stub only.",
|
|
},
|
|
requestedByActorType: "system",
|
|
requestedByActorId: null,
|
|
});
|
|
|
|
expect(run).not.toBeNull();
|
|
await waitFor(() => gateway.getAgentPayloads().length === 1, 30_000);
|
|
const payload = gateway.getAgentPayloads()[0] ?? {};
|
|
// The gateway rejects unknown root params, so the wake context rides in the
|
|
// generated message rather than a top-level `paperclip` field.
|
|
expect(payload.paperclip).toBeUndefined();
|
|
const wake = parseWakePayloadFromMessage(payload.message);
|
|
// Security-critical: low-trust quarantined output is redacted to the sanitized
|
|
// stub before it reaches the higher-trust wake/continuation context. The raw
|
|
// body must never appear (asserted by expectNoCanary below). The sourceTrust
|
|
// provenance is intentionally not carried in the agent-facing message form; its
|
|
// recording is covered by the route-response assertions earlier in this suite.
|
|
expect(wake).toMatchObject({
|
|
reason: "issue_commented",
|
|
issue: {
|
|
id: fixture.issues.reviewRoot.id,
|
|
title: fixture.issues.reviewRoot.title,
|
|
},
|
|
latestCommentId: scopedComment.id,
|
|
commentIds: [scopedComment.id],
|
|
comments: [
|
|
{
|
|
id: scopedComment.id,
|
|
issueId: fixture.issues.reviewRoot.id,
|
|
body: LOW_TRUST_QUARANTINED_BODY,
|
|
},
|
|
],
|
|
continuationSummary: {
|
|
body: LOW_TRUST_QUARANTINED_BODY,
|
|
},
|
|
livenessContinuation: {
|
|
attempt: 1,
|
|
maxAttempts: 2,
|
|
sourceRunId: fixture.runs.lowTrust.id,
|
|
state: "quarantined_low_trust_handoff",
|
|
reason: "Low-trust review output requires sanitized follow-up.",
|
|
instruction: "Continue from the sanitized quarantine stub only.",
|
|
},
|
|
});
|
|
expect(String(payload.message ?? "")).toContain(
|
|
"## Paperclip Wake Payload",
|
|
);
|
|
expectNoCanary(payload, fixture.canaries.raw);
|
|
gateway.releaseFirstWait();
|
|
await waitFor(async () => {
|
|
const status = await db
|
|
.select({ status: heartbeatRuns.status })
|
|
.from(heartbeatRuns)
|
|
.where(eq(heartbeatRuns.id, run!.id))
|
|
.then((rows) => rows[0]?.status ?? null);
|
|
return (
|
|
status === "succeeded" ||
|
|
status === "failed" ||
|
|
status === "cancelled"
|
|
);
|
|
}, 30_000);
|
|
} finally {
|
|
gateway.releaseFirstWait();
|
|
await gateway.close();
|
|
}
|
|
}, 120_000);
|
|
|
|
it("keeps board positive controls for issue-linked approvals and sanitized promotion", async () => {
|
|
const fixture = await seedLowTrustFixture(db);
|
|
const app = createApp(db, boardActor(fixture));
|
|
|
|
const approvalsRes = await request(app).get(
|
|
`/api/issues/${fixture.issues.assignedReview.id}/approvals`,
|
|
);
|
|
expect(approvalsRes.status, JSON.stringify(approvalsRes.body)).toBe(200);
|
|
expect(JSON.stringify(approvalsRes.body)).toContain(
|
|
fixture.canaries.approval,
|
|
);
|
|
|
|
const [rawProduct] = await db
|
|
.insert(issueWorkProducts)
|
|
.values({
|
|
companyId: fixture.company.id,
|
|
projectId: fixture.projects.allowed.id,
|
|
issueId: fixture.issues.assignedReview.id,
|
|
type: "artifact",
|
|
provider: "test",
|
|
title: "Quarantined raw artifact",
|
|
status: "active",
|
|
summary: fixture.canaries.raw,
|
|
sourceTrust: {
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "quarantined",
|
|
sourceIssueId: fixture.issues.assignedReview.id,
|
|
sourceRunId: fixture.runs.lowTrust.id,
|
|
sourceAgentId: fixture.agents.lowTrust.id,
|
|
},
|
|
})
|
|
.returning();
|
|
|
|
const [otherCompany] = await db
|
|
.insert(companies)
|
|
.values({
|
|
name: "Foreign low-trust source",
|
|
issuePrefix: `FGN${randomUUID().slice(0, 4).toUpperCase()}`,
|
|
defaultResponsibleUserId: "board-user",
|
|
})
|
|
.returning();
|
|
const [foreignIssue] = await db
|
|
.insert(issues)
|
|
.values({
|
|
companyId: otherCompany!.id,
|
|
parentId: fixture.issues.assignedReview.id,
|
|
title: "Foreign quarantined issue",
|
|
status: "done",
|
|
priority: "medium",
|
|
sourceTrust: {
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "quarantined",
|
|
sourceIssueId: fixture.issues.assignedReview.id,
|
|
sourceRunId: fixture.runs.lowTrust.id,
|
|
sourceAgentId: fixture.agents.lowTrust.id,
|
|
},
|
|
})
|
|
.returning();
|
|
|
|
const rejectedPromotion = await request(app)
|
|
.post(
|
|
`/api/issues/${fixture.issues.assignedReview.id}/low-trust/promotions`,
|
|
)
|
|
.send({
|
|
sourceArtifactKind: "issue",
|
|
sourceArtifactId: foreignIssue!.id,
|
|
title: "Rejected foreign issue",
|
|
summary: "Should not promote across company boundaries.",
|
|
});
|
|
expect(
|
|
rejectedPromotion.status,
|
|
JSON.stringify(rejectedPromotion.body),
|
|
).toBe(404);
|
|
expect(rejectedPromotion.body.error).toBe(
|
|
"Low-trust source artifact not found",
|
|
);
|
|
|
|
const promotion = await request(app)
|
|
.post(
|
|
`/api/issues/${fixture.issues.assignedReview.id}/low-trust/promotions`,
|
|
)
|
|
.send({
|
|
sourceArtifactKind: "work_product",
|
|
sourceArtifactId: rawProduct!.id,
|
|
title: "Sanitized finding",
|
|
summary: "Sanitized summary without raw instructions.",
|
|
});
|
|
expect(promotion.status, JSON.stringify(promotion.body)).toBe(201);
|
|
expect(promotion.body.sourceTrust).toMatchObject({
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "promoted",
|
|
sourceIssueId: fixture.issues.assignedReview.id,
|
|
promotedFrom: {
|
|
artifactKind: "work_product",
|
|
artifactId: rawProduct!.id,
|
|
issueId: fixture.issues.assignedReview.id,
|
|
},
|
|
promotedByActorType: "user",
|
|
promotedByActorId: "board-user",
|
|
});
|
|
expect(promotion.body).toMatchObject({
|
|
externalId: rawProduct!.id,
|
|
metadata: {
|
|
promotion: {
|
|
sourceArtifactKind: "work_product",
|
|
sourceArtifactId: rawProduct!.id,
|
|
},
|
|
},
|
|
createdByRunId: null,
|
|
});
|
|
expect(typeof promotion.body.sourceTrust.promotedAt).toBe("string");
|
|
expectNoCanary(promotion.body, fixture.canaries.raw);
|
|
|
|
const [promotedSource] = await db
|
|
.select({ sourceTrust: issueWorkProducts.sourceTrust })
|
|
.from(issueWorkProducts)
|
|
.where(eq(issueWorkProducts.id, rawProduct!.id));
|
|
expect(promotedSource?.sourceTrust).toMatchObject({
|
|
preset: LOW_TRUST_REVIEW_PRESET,
|
|
disposition: "promoted",
|
|
promotedFrom: {
|
|
artifactKind: "work_product",
|
|
artifactId: rawProduct!.id,
|
|
issueId: fixture.issues.assignedReview.id,
|
|
},
|
|
promotedByActorType: "user",
|
|
promotedByActorId: "board-user",
|
|
});
|
|
|
|
const duplicatePromotion = await request(app)
|
|
.post(
|
|
`/api/issues/${fixture.issues.assignedReview.id}/low-trust/promotions`,
|
|
)
|
|
.send({
|
|
sourceArtifactKind: "work_product",
|
|
sourceArtifactId: rawProduct!.id,
|
|
title: "Duplicate sanitized finding",
|
|
summary: "Should not create another promoted artifact.",
|
|
});
|
|
expect(
|
|
duplicatePromotion.status,
|
|
JSON.stringify(duplicatePromotion.body),
|
|
).toBe(422);
|
|
expect(duplicatePromotion.body.error).toBe(
|
|
"Source artifact is not quarantined low-trust output",
|
|
);
|
|
|
|
const productsForSource = await db
|
|
.select({ id: issueWorkProducts.id })
|
|
.from(issueWorkProducts)
|
|
.where(eq(issueWorkProducts.externalId, rawProduct!.id));
|
|
expect(productsForSource).toHaveLength(1);
|
|
});
|
|
},
|
|
);
|