Evaluate confidential delegation across real private audiences

This commit is contained in:
Dotta committed 2026-10-26 18:08:58 -05:00
1 parent f8b2bb3b49
commit caa71d29c8
4 files changed
+118 -6

No files matched your search

@@ -12,19 +12,20 @@ interface PrivacyObservation extends InternalObservation {
observer: { projectStatus: number; taskStatus: number; projectsStatus: number; searchStatus: number; projectsBody: string; searchBody: string };
leadStatus: number; unrelatedAgentStatus: number; unrelatedPublicStatus: number;
}
async function mainPerson(ctx: ChatCatalogContext) {
export async function mainPerson(ctx: ChatCatalogContext) {
const session = await ctx.input.api.get<{ user?: { id: string } }>("/api/auth/get-session");
if (!session.user?.id) throw new Error("Privacy fixtures require the real signed-in responsible person");
return session.user.id;
}
async function observerPerson(ctx: ChatCatalogContext) {
export async function observerPerson(ctx: ChatCatalogContext) {
const browser = ctx.input.page.context().browser();
if (!browser || !ctx.input.privateSecrets) throw new Error("Independent authenticated observer requires a browser and attempt-private redaction sink");
const observer = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Riley" });
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, observer, ctx.input.privateSecrets, "viewer");
try { await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, observer, ctx.input.privateSecrets, "viewer"); }
catch (error) { await observer.dispose(); throw error; }
return observer;
}
async function personRead(person: Person, path: string) {
export async function personRead(person: Person, path: string) {
const response = await person.request(path);
const body = response.ok ? await response.text() : "";
await response.body?.cancel();
@@ -42,13 +43,13 @@ async function privacyProbeKey(ctx: ChatCatalogContext, agentId: string) {
}
return pending;
}
async function cleanupPrivacyProbes(ctx: ChatCatalogContext) {
export async function cleanupPrivacyProbes(ctx: ChatCatalogContext) {
const keys = privacyProbeKeys.get(ctx); privacyProbeKeys.delete(ctx);
for (const [agentId, pending] of keys ?? []) {
const key = await pending; await ctx.input.api.delete(`/api/agents/${agentId}/keys/${key.id}`, { allowNotFound: true });
}
}
async function agentStatus(ctx: ChatCatalogContext, agentId: string, path: string) {
export async function agentStatus(ctx: ChatCatalogContext, agentId: string, path: string) {
const key = await privacyProbeKey(ctx, agentId);
const url = new URL(path, ctx.input.api.baseURL);
if (url.origin !== ctx.input.api.baseURL || !url.pathname.startsWith("/api/")) throw new Error("Privacy probe escaped its isolated API");
@@ -0,0 +1,88 @@
import { expect } from "@playwright/test";
import type { ChatCatalogCase } from "../chat-catalog-contract.js";
import { fixtureWorker, makeDefinition } from "./internal-definition.js";
import { openStandingChat } from "./identity-support.js";
import { agentStatus, cleanupPrivacyProbes, mainPerson, observerPerson, personRead } from "./internal-privacy.js";
import { checks, entity, hasLink, observation, observeInternal, project, putDocument, type Entity, type InternalObservation } from "./internal-support.js";
interface PrivateTask extends Entity { visibility?: string; privacyRootIssueId?: string | null; conversationAgentId?: string | null }
interface PrivateDelegationObservation extends InternalObservation {
sourcePrivate: boolean; projectPrivate: boolean; leadProjectStatus: number; workerProjectStatus: number; workerTaskStatus?: number;
observerCompanyStatus: number; observerTaskStatus?: number; observerSourceStatus: number; observerProjectsStatus: number;
observerProjectsBody: string; observerSearchStatus: number; observerSearchResults: string;
}
export function gradePrivateDelegation(value: unknown) {
const c = checks(); const o = observation(value) as PrivateDelegationObservation | undefined;
c.add("actual-private-delegation-evidence", !!o && typeof o.sourcePrivate === "boolean" && typeof o.observerCompanyStatus === "number",
"Saved source/project privacy and independently authenticated reader/search evidence are required.");
if (!o || typeof o.sourcePrivate !== "boolean") return c.result();
const p = entity(o.state, "project"); const worker = entity(o.state, "worker"); const saved = o.tasks[0];
c.add("source-and-project-boundary", o.sourcePrivate && o.projectPrivate && o.leadProjectStatus === 200 && o.workerProjectStatus === 200,
"The private conversation and existing private project remain restricted while both authorized agents retain actual access.");
c.add("one-authorized-confidential-task", o.tasks.length === 1 && !!saved && saved.companyId === o.companyId && saved.projectId === p.id
&& saved.assigneeAgentId === worker.id && !(saved as PrivateTask).conversationAgentId
&& /\b(?:confidential|private)\b/i.test(`${saved.title} ${saved.description ?? ""}`)
&& /\b(?:announcement|launch)\b/i.test(`${saved.title} ${saved.description ?? ""}`)
&& o.activity.some(row => row.action === "issue.created" && row.entityId === saved.id && row.agentId === o.leadId)
&& hasLink(o, "issues", saved), "Exactly one ordinary confidential launch task belongs to the actual private project and authorized Morgan, with an inspectable source link.");
c.add("morgan-retains-effective-access", o.workerTaskStatus === 200, "The actual assignee's independent bearer can retrieve the private delegated task.");
c.add("unrelated-human-cannot-discover-or-read", o.observerCompanyStatus === 200 && o.observerProjectsStatus === 200 && o.observerSearchStatus === 200
&& [403, 404].includes(o.observerTaskStatus ?? 0) && [403, 404].includes(o.observerSourceStatus)
&& !!saved && !o.observerProjectsBody.includes(p.id) && !o.observerSearchResults.includes(saved.id)
&& !o.observerSearchResults.includes(String(o.state.confidentialMarker)),
"A healthy unrelated company member cannot discover protected project/task contents or open the copied work/source IDs.");
const workRuns = saved ? o.runs.filter(row => row.contextSnapshot?.issueId === saved.id || row.contextSnapshot?.taskId === saved.id) : [];
c.add("real-private-worker-admission", workRuns.length === 1 && workRuns[0]?.agentId === worker.id && ["queued", "running", "succeeded"].includes(workRuns[0]?.status ?? ""),
"Actual worker execution belongs to the private work task instead of the standing conversation.");
return c.result();
}
function privateDelegation(): ChatCatalogCase {
let observer: Awaited<ReturnType<typeof observerPerson>> | undefined;
const base = makeDefinition("tasks.private-task", async ctx => {
const ownerId = await mainPerson(ctx); observer = await observerPerson(ctx); const worker = await fixtureWorker(ctx);
const confidentialMarker = `Confidential launch code NIGHTJAR-${ctx.input.nonce}`;
const p = await project(ctx, "Private Launch", { visibility: "private", description: `${confidentialMarker}. Acorn Notes launches next month for small research teams; draft a short confidential announcement for internal review.` });
for (const agentId of [ctx.input.fixtures.agent.id, worker.id])
await ctx.input.api.post(`/api/projects/${p.id}/access-members`, { subjectType: "agent", subjectId: agentId });
const source = await openStandingChat(ctx);
const current = await ctx.input.api.get<PrivateTask>(`/api/issues/${source.id}`);
if (current.visibility !== "private") await ctx.input.api.patch(`/api/issues/${source.id}`, { visibility: "private" });
await putDocument(ctx, source.id, `# Confidential launch brief\n${confidentialMarker}. Keep the announcement inside Private Launch and its authorized team.\n`);
return { project: p, worker, ownerId, observerId: observer.id, sourceId: source.id, confidentialMarker };
}, gradePrivateDelegation);
return { ...base, authentication: "human-session", expectedRunCount: 3, minimumRunCount: 2,
runPolicy: { kind: "source-settled-with-active-work", maximumActiveWorkerRuns: 1 },
sourceFiles: [...base.sourceFiles, "chat-catalog-domains/internal-private-delegation.ts", "chat-catalog-domains/internal-privacy.ts", "chat-catalog-domains/identity-auth.ts", "chat-catalog-domains/identity-support.ts"],
async observe(ctx, state): Promise<PrivateDelegationObservation> {
if (!observer) throw new Error("Missing authenticated unrelated member");
const o = await observeInternal(ctx, state); const saved = o.tasks[0]; const p = entity(state, "project"); const worker = entity(state, "worker");
const [source, projectNow, leadProjectStatus, workerProjectStatus, workerTaskStatus, companyRead, taskRead, sourceRead, projectsRead, searchRead] = await Promise.all([
ctx.input.api.get<PrivateTask>(`/api/issues/${state.sourceId}`), ctx.input.api.get<PrivateTask>(`/api/projects/${p.id}`),
agentStatus(ctx, o.leadId, `/api/projects/${p.id}`), agentStatus(ctx, worker.id, `/api/projects/${p.id}`),
saved ? agentStatus(ctx, worker.id, `/api/issues/${saved.id}`) : undefined,
personRead(observer, `/api/companies/${o.companyId}`), saved ? personRead(observer, `/api/issues/${saved.id}`) : undefined,
personRead(observer, `/api/issues/${state.sourceId}`), personRead(observer, `/api/companies/${o.companyId}/projects`),
personRead(observer, `/api/companies/${o.companyId}/search?q=${encodeURIComponent(saved?.identifier ?? saved?.title ?? String(state.confidentialMarker))}`),
]);
const search = searchRead.status === 200 ? JSON.parse(searchRead.body) as { results?: unknown[] } : {};
if (searchRead.status === 200 && !Array.isArray(search.results)) throw new Error("Private-task search did not return the production result collection");
return { ...o, sourcePrivate: source.visibility === "private", projectPrivate: projectNow.visibility === "private", leadProjectStatus, workerProjectStatus, workerTaskStatus,
observerCompanyStatus: companyRead.status, observerTaskStatus: taskRead?.status, observerSourceStatus: sourceRead.status,
observerProjectsStatus: projectsRead.status, observerProjectsBody: projectsRead.body, observerSearchStatus: searchRead.status,
observerSearchResults: JSON.stringify(search.results ?? []) };
},
async verifyResult(ctx, state, value) {
await base.verifyResult(ctx, state, value);
if (!observer) throw new Error("Missing authenticated observer browser"); const o = value as PrivateDelegationObservation; const saved = o.tasks[0]!;
const page = await observer.context.newPage();
try {
await page.goto(new URL(`/${ctx.input.fixtures.company.issuePrefix}/issues/${saved.identifier ?? saved.id}`, o.origin).href, { waitUntil: "domcontentloaded" });
await expect(page.getByText(/not found|access denied|forbidden|unable to load|could not load/i).first()).toBeVisible();
await expect(page.getByRole("heading", { name: saved.title!, exact: true })).toHaveCount(0);
await ctx.input.evidence("chat-catalog-private-delegation-denied-observer.json", { taskId: saved.id, observerId: observer.id, denied: true });
} finally { await page.close(); }
},
async cleanup(ctx) { try { await cleanupPrivacyProbes(ctx); } finally { await observer?.dispose(); observer = undefined; } },
};
}
export const privateDelegationCases: readonly ChatCatalogCase[] = [privateDelegation()];
@@ -3,6 +3,7 @@ import { taskPrivacyCases } from "./internal-privacy.js";
import { taskInventoryCases } from "./internal-task-inventory.js";
import { taskAttachmentCases } from "./internal-task-attachment.js";
import { taskCompletionCases } from "./internal-task-completion.js";
import { privateDelegationCases } from "./internal-private-delegation.js";
import type { ChatCatalogCase } from "../chat-catalog-contract.js";
import { project, task, goal, putDocument, entity, observation, checks, hasLink, sameTask, type Entity } from "./internal-support.js";
import { makeDefinition, retryDefinition, fixtureWorker } from "./internal-definition.js";
@@ -64,6 +65,7 @@ export const tasksCases: readonly ChatCatalogCase[] = [
...taskInventoryCases,
...taskAttachmentCases,
...taskCompletionCases,
...privateDelegationCases,
ambiguousTask(),
define("tasks.edit-properties", async ctx => {
const p = await project(ctx); const worker = await fixtureWorker(ctx);
@@ -15,6 +15,14 @@ function event(action: string, entityId: string, details?: Record<string, unknow
function valid(scenario: string): InternalObservation {
const o = base(scenario);
switch (scenario) {
case "tasks.private-task": {
o.state.worker = { id: "morgan" }; o.state.confidentialMarker = "Confidential marker";
o.tasks[0] = { ...o.tasks[0]!, title: "Confidential launch announcement", assigneeAgentId: "morgan" };
o.activity = [event("issue.created", "task")];
o.runs.push({ id: "private-worker", companyId: "company", agentId: "morgan", status: "running", contextSnapshot: { issueId: "task" } });
Object.assign(o, { sourcePrivate: true, projectPrivate: true, leadProjectStatus: 200, workerProjectStatus: 200, workerTaskStatus: 200,
observerCompanyStatus: 200, observerTaskStatus: 404, observerSourceStatus: 404, observerProjectsStatus: 200, observerProjectsBody: "[]", observerSearchStatus: 200, observerSearchResults: "[]" }); break;
}
case "tasks.completion-with-gate": {
const policy = { stages: [{ id: "review-stage", type: "review", participants: [{ type: "user", userId: "riley" }] }] };
const t = { ...o.tasks[0]!, status: "in_review", reviewPolicy: "human_only", executionPolicy: policy,
@@ -185,6 +193,7 @@ function valid(scenario: string): InternalObservation {
return o;
}
const corrupt: Record<string, (o: InternalObservation) => void> = {
"tasks.private-task": o => { Object.assign(o, { observerTaskStatus: 200 }); },
"tasks.completion-with-gate": o => { o.tasks[0]!.status = "done"; },
"tasks.attachment-brief": o => { Object.assign(o, { workerDownloadStatus: 403 }); },
"projects.add-repository": o => { Object.assign(o, { repositoryProject: { ...o.projects[0], workspaces: [{ id: "wrong", repoUrl: "https://github.com/acorn/unapproved" }] } }); },
@@ -503,3 +512,15 @@ describe("governed completion calibration", () => {
expect(gradeInternal(o).passed).toBe(false);
});
});
describe("private delegation audience calibration", () => {
it("rejects a public source conversation even when the new project is private", () => {
const o = valid("tasks.private-task"); Object.assign(o, { sourcePrivate: false }); expect(gradeInternal(o).passed).toBe(false);
});
it("rejects a private-looking task whose actual assignee cannot read it", () => {
const o = valid("tasks.private-task"); Object.assign(o, { workerTaskStatus: 403 }); expect(gradeInternal(o).passed).toBe(false);
});
it("rejects protected task discovery in a healthy unrelated member's search", () => {
const o = valid("tasks.private-task"); Object.assign(o, { observerSearchResults: '[{"id":"task"}]' }); expect(gradeInternal(o).passed).toBe(false);
});
});