Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions plugins/chassis/src/security-quarantine.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
export const SECURITY_QUARANTINE_REFUSAL_TEXT =
"I couldn't act because my security screen flagged part of this message or its conversation context. Please retry without the flagged context, or ask an admin to review the quarantine.";

/**
* The refusal text promises an admin review of the quarantine. The refused
* input and the screen's full request snapshot are stored on the session
* (readable via GET /v1/admin/sessions/:id/llm), so when the refusal carries a
* session id, surface it: that is the record an admin reviews (#574).
*/
export function quarantineRefusalText(sessionId?: string): string {
return sessionId
? `${SECURITY_QUARANTINE_REFUSAL_TEXT} (quarantine record: session ${sessionId})`
: SECURITY_QUARANTINE_REFUSAL_TEXT;
}
4 changes: 2 additions & 2 deletions src/delivery/run-result-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Destination, OutgoingAttachment } from "../types.ts";
import type { Run, RunStore } from "../runs/run-store.ts";
import type { DeliveryStore } from "./delivery-store.ts";
import type { Task, TaskStore } from "../tasks/task-store.ts";
import { SECURITY_QUARANTINE_REFUSAL_TEXT } from "../../plugins/chassis/src/security-quarantine.ts";
import { quarantineRefusalText } from "../../plugins/chassis/src/security-quarantine.ts";
import { resolveTurnOrigin } from "../core/turn-origin.ts";
import { errMessage } from "../util/errors.ts";

Expand Down Expand Up @@ -31,7 +31,7 @@ export function runResultDelivery(run: Run, taskList: Task[] = []): RunResultDel
run.result.refusalKind === "security_quarantine" &&
run.request.addressed
) {
return { destination, text: SECURITY_QUARANTINE_REFUSAL_TEXT, idempotencyKey };
return { destination, text: quarantineRefusalText(run.result.sessionId), idempotencyKey };
}
if (run.request.surfaceTools && run.result?.status !== "failed" && !run.result?.attachments?.length) return null;
if (run.status === "failed") {
Expand Down
11 changes: 8 additions & 3 deletions src/slack/refusals.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SECURITY_QUARANTINE_REFUSAL_TEXT } from "../../plugins/chassis/src/security-quarantine.ts";
import { quarantineRefusalText } from "../../plugins/chassis/src/security-quarantine.ts";

export function isBoundaryRefusal(reason: string | undefined): boolean {
return (reason ?? "").startsWith("internal-only");
Expand Down Expand Up @@ -27,11 +27,16 @@ export async function postThenAckRunDelivery(opts: {
}

export function refusalNote(
result: { reason?: string; adminUrl?: string; refusalKind?: "security_quarantine" },
result: {
reason?: string;
adminUrl?: string;
refusalKind?: "security_quarantine";
sessionId?: string;
},
kind: "dm" | "channel",
): string {
if (result.refusalKind === "security_quarantine") {
return SECURITY_QUARANTINE_REFUSAL_TEXT;
return quarantineRefusalText(result.sessionId);
}
const reason = result.reason ?? "refused";
if (isBoundaryRefusal(reason)) {
Expand Down
16 changes: 16 additions & 0 deletions test/run-result-delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ test("runResultDelivery recovers a security quarantine without exposing its inte
assert.doesNotMatch(d?.text ?? "", /internal screening details/);
});

test("runResultDelivery surfaces the session id holding the quarantine record (#574)", () => {
const d = runResultDelivery(
run({
request: { ...turn("hi", "C9:171.001"), addressed: true },
result: {
status: "refused",
refusalKind: "security_quarantine",
reason: "internal screening details",
sessionId: "sess-5678",
},
}),
);
assert.match(d?.text ?? "", /quarantine record: session sess-5678/);
assert.doesNotMatch(d?.text ?? "", /internal screening details/);
});

test("runResultDelivery keeps an unprompted quarantine silent — a replay has no live handler to suppress it", () => {
const spine = run({ result: { status: "refused", refusalKind: "security_quarantine" } });
spine.request = { ...spine.request, surfaceTools: true, origin: { kind: "ambient" } };
Expand Down
22 changes: 22 additions & 0 deletions test/slack-refusals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,25 @@ test("isBoundaryRefusal: only internal-only reasons are boundary refusals", () =
assert.equal(isBoundaryRefusal("An unknown error occurred"), false);
assert.equal(isBoundaryRefusal(undefined), false);
});

test("refusalNote: a quarantine refusal surfaces the session that holds the quarantine record (#574)", () => {
const note = refusalNote(
{
refusalKind: "security_quarantine",
reason: "Auto quarantined suspicious or unscreenable external input before the agent ran.",
sessionId: "sess-1234",
},
"channel",
);
assert.match(note, /quarantine record: session sess-1234/);
assert.match(note, /ask an admin to review the quarantine/);
assert.doesNotMatch(note, /Auto quarantined|unscreenable/);
});

test("refusalNote: a quarantine refusal without a session keeps the plain text", () => {
const note = refusalNote({ refusalKind: "security_quarantine" }, "channel");
assert.equal(
note,
"I couldn't act because my security screen flagged part of this message or its conversation context. Please retry without the flagged context, or ask an admin to review the quarantine.",
);
});