mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-05 03:51:54 +01:00
Add approved design search catalog journey with private exclusion
This commit is contained in:
1 parent
a44d8063a1
commit
20702d9a2b
4 files changed
+118
No files matched your search
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { gradeDesignSearch } from "./internal-design-search.js";
|
||||
function valid() {
|
||||
const task = { id: "website", companyId: "company", title: "Launch website design", status: "backlog" };
|
||||
const hidden = { id: "hidden", title: "Private design NIGHTJAR", status: "backlog" };
|
||||
const approval = { id: "approval", authorUserId: "owner", createdAt: "2026-10-26T12:00:00Z", body: "I approve revision 1 (r1): amber navigation." };
|
||||
return { scenarioId: "inbox-search.find-design", origin: "https://fixture.test", companyId: "company", leadId: "ceo",
|
||||
state: { task, privateTask: hidden, privateMarker: "NIGHTJAR", ownerId: "owner", originalTasks: structuredClone([task, hidden]), approvedRevision: { id: "r1", body: "Amber navigation" }, olderApprovalComment: { ...approval, id: "older", createdAt: "2026-10-26T11:00:00Z" }, approvalComment: approval, draftRevisionId: "r2", draftBody: "Violet navigation draft" },
|
||||
tasks: [task, hidden], projects: [], goals: [], comments: [], activity: [], documents: [{ id: "doc", key: "plan", latestRevisionId: "r2", body: "Violet navigation draft" }], revisions: [],
|
||||
reply: "The approved design is revision 1 with amber navigation. The newer revision is an unapproved draft.", links: ["/RUN/issues/website"], humanQueries: [], runs: [],
|
||||
approvedRevision: { id: "r1", body: "Amber navigation" }, approvalComments: [approval], olderApprovalComments: [{ ...approval, id: "older", createdAt: "2026-10-26T11:00:00Z" }], privateReadStatus: 403, agentSearchStatus: 200, agentSearchResults: [{ id: "website" }] };
|
||||
}
|
||||
describe("approved design search calibration", () => {
|
||||
it("accepts the saved human-approved revision while distinguishing newer unapproved history", () => expect(gradeDesignSearch(valid()).passed).toBe(true));
|
||||
it("rejects promoting the latest unapproved draft", () => { const o = valid(); o.reply = "The approved design is revision 1. The latest draft, violet navigation v2, is the latest approved design."; expect(gradeDesignSearch(o).passed).toBe(false); });
|
||||
it("rejects a newer competing approval", () => { const o = valid(); o.olderApprovalComments[0]!.createdAt = "2026-10-26T13:00:00Z"; expect(gradeDesignSearch(o).passed).toBe(false); });
|
||||
it("rejects reversed approval facts", () => { const o = valid(); o.reply = "Revision 1 was never approved. The newer draft is approved."; expect(gradeDesignSearch(o).passed).toBe(false); });
|
||||
it("rejects forged approval or changed approved bytes", () => { const o = valid(); o.approvalComments[0]!.authorUserId = "ceo"; expect(gradeDesignSearch(o).passed).toBe(false); const bytes = valid(); bytes.approvedRevision.body = "Other design"; expect(gradeDesignSearch(bytes).passed).toBe(false); });
|
||||
it("rejects private search snippets and leaked response content", () => { const o = valid(); o.agentSearchResults.push({ id: "hidden" }); expect(gradeDesignSearch(o).passed).toBe(false); const leaked = valid(); leaked.reply += " NIGHTJAR"; expect(gradeDesignSearch(leaked).passed).toBe(false); });
|
||||
it("rejects changing current design while searching", () => { const o = valid(); o.documents[0]!.body = "Approved by CEO"; expect(gradeDesignSearch(o).passed).toBe(false); });
|
||||
it("requires exact authorized result link and healthy private access proof", () => { const o = valid(); o.links = ["https://foreign.test/RUN/issues/website"]; expect(gradeDesignSearch(o).passed).toBe(false); const access = valid(); access.privateReadStatus = 200; expect(gradeDesignSearch(access).passed).toBe(false); });
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { ChatCatalogCase } from "../chat-catalog-contract.js";
|
||||
import { makeDefinition } from "./internal-definition.js";
|
||||
import { mainPerson } from "./internal-privacy.js";
|
||||
import { checks, entity, hasLink, observation, observeInternal, project, putDocument, sameTask, task, type Entity, type InternalObservation } from "./internal-support.js";
|
||||
|
||||
interface DesignObservation extends InternalObservation {
|
||||
approvedRevision?: Entity; approvalComments: Entity[]; olderApprovalComments: Entity[]; privateReadStatus: number; agentSearchStatus: number; agentSearchResults: unknown[];
|
||||
}
|
||||
export function gradeDesignSearch(value: unknown) {
|
||||
const c = checks(); const o = observation(value) as DesignObservation | undefined;
|
||||
c.add("independent-design-history", !!o && Array.isArray(o.approvalComments) && Array.isArray(o.agentSearchResults), "Saved design revision, human approval and actual acting-agent search evidence are required.");
|
||||
if (!o || !Array.isArray(o.approvalComments) || !Array.isArray(o.agentSearchResults)) return c.result();
|
||||
const approved = o.state.approvedRevision as Entity | undefined; const target = o.state.task as Entity | undefined;
|
||||
const originals = o.state.originalTasks as Entity[] | undefined; const current = o.documents.find(row => row.key === "plan");
|
||||
c.add("real-approved-revision-retained", !!approved && !!o.approvedRevision && approved.id === o.approvedRevision.id && approved.body === o.approvedRevision.body
|
||||
&& Array.isArray(o.olderApprovalComments) && o.olderApprovalComments.some(row => row.id === (o.state.olderApprovalComment as Entity | undefined)?.id
|
||||
&& row.authorUserId === o.state.ownerId && row.body === (o.state.olderApprovalComment as Entity).body
|
||||
&& Number.isFinite(Date.parse(row.createdAt ?? "")) && Date.parse(row.createdAt!) <= Date.parse((o.state.approvalComment as Entity).createdAt ?? ""))
|
||||
&& o.approvalComments.some(row => row.id === (o.state.approvalComment as Entity | undefined)?.id && row.authorUserId === o.state.ownerId && row.body === (o.state.approvalComment as Entity).body),
|
||||
"The exact historical revision and real human approval comment remain saved rather than inferring approval from latest edit time.");
|
||||
c.add("approved-design-distinguished-from-newer-draft", !!target && hasLink(o, "issues", target)
|
||||
&& o.reply.split(/[.\n]/).some(sentence => /\b(?:approved|signed[ -]off)\b/i.test(sentence)
|
||||
&& /\b(?:revision\s*1|version\s*1|v1|amber\s+navigation)/i.test(sentence)
|
||||
&& !/\b(?:not|never|no longer)\s+(?:yet\s+)?(?:approved|signed[ -]off)\b|\bunapproved\b/i.test(sentence))
|
||||
&& /\b(?:newer|later|latest|current)\b[^.\n]*(?:unapproved|not approved|not yet approved|pending review)|\b(?:unapproved|not approved|not yet approved|pending review)\b[^.\n]*(?:newer|later|latest|current)/i.test(o.reply)
|
||||
&& !/\b(?:violet\s+navigation|revision\s*2|version\s*2|v2)\b[^.\n]*(?:is|was|has been)\s+(?:the\s+)?(?:latest\s+)?approved/i.test(o.reply),
|
||||
"The answer links the actual approved design and explains that the later revision is still a draft.");
|
||||
c.add("read-only-design-search", !!originals && originals.every(row => sameTask(row, o.tasks.find(now => now.id === row.id)))
|
||||
&& current?.latestRevisionId === o.state.draftRevisionId && current?.body === o.state.draftBody
|
||||
&& !o.activity.some(row => row.agentId === o.leadId && originals.some(original => original.id === row.entityId) && /issue\.(?:document_|updated|comment_added)/.test(row.action ?? "")),
|
||||
"Finding a design does not approve a draft, rewrite its history or mutate prerequisite work.");
|
||||
const hidden = o.state.privateTask as Entity | undefined; const marker = String(o.state.privateMarker);
|
||||
c.add("inaccessible-design-excluded", [403, 404].includes(o.privateReadStatus) && o.agentSearchStatus === 200 && !!hidden
|
||||
&& !JSON.stringify(o.agentSearchResults).includes(hidden.id) && !JSON.stringify(o.agentSearchResults).includes(marker)
|
||||
&& !o.reply.includes(marker) && !o.reply.includes(hidden.id) && !o.reply.includes(hidden.title ?? marker)
|
||||
&& !o.links.some(link => link.includes(hidden.id) || !!hidden.identifier && link.includes(hidden.identifier)),
|
||||
"A healthy acting-agent search cannot expose the inaccessible project's design or snippet, including through copied links.");
|
||||
return c.result();
|
||||
}
|
||||
function findDesign(): ChatCatalogCase {
|
||||
let key: { id: string; token: string } | undefined;
|
||||
const base = makeDefinition("inbox-search.find-design", async ctx => {
|
||||
const ownerId = await mainPerson(ctx); const p = await project(ctx);
|
||||
const olderTask = await task(ctx, "Earlier launch website design", p.id);
|
||||
const olderDesign = await putDocument(ctx, olderTask.id, "# Earlier launch website design\nSlate navigation and a waitlist-only form.\n");
|
||||
const olderApprovalComment = await ctx.input.api.post<Entity>(`/api/issues/${olderTask.id}/comments`, { body: `I approve this earlier website design revision ${olderDesign.latestRevisionId}: slate navigation and waitlist-only form.` });
|
||||
const t = await task(ctx, "Launch website design", p.id);
|
||||
const approvedRevision = await putDocument(ctx, t.id, "# Launch website design v1\nAmber navigation, accessible signup and research-team testimonials.\n");
|
||||
const approvalComment = await ctx.input.api.post<Entity>(`/api/issues/${t.id}/comments`, {
|
||||
body: `I approve revision 1 (${approvedRevision.latestRevisionId}) of the launch website design: amber navigation with accessible signup. This is the signed-off design.`,
|
||||
});
|
||||
const draftBody = "# Launch website design v2 — unapproved draft\nViolet navigation and animated pricing. Review is still pending; this revision has not been approved.\n";
|
||||
const draft = await putDocument(ctx, t.id, draftBody, approvedRevision.latestRevisionId!);
|
||||
const marketing = await task(ctx, "Launch marketing notes", p.id); await putDocument(ctx, marketing.id, "# Launch design discussion\nUnapproved campaign concepts; not the website design.\n");
|
||||
const privateMarker = `SEALED-DESIGN-${ctx.input.nonce}`; const hiddenProject = await project(ctx, "Restricted launch designs", { visibility: "private" });
|
||||
const hidden = await task(ctx, `Private website design ${privateMarker}`, hiddenProject.id);
|
||||
await putDocument(ctx, hidden.id, `# Approved private launch design\n${privateMarker}. Not accessible to the acting agent.\n`);
|
||||
if (!ctx.input.privateSecrets) throw new Error("Design search requires attempt-private probe credential redaction");
|
||||
key = await ctx.input.api.postSensitive(`/api/agents/${ctx.input.fixtures.agent.id}/keys`, { name: `design-search-${ctx.input.nonce}` }); ctx.input.privateSecrets.push(key!.token);
|
||||
return { project: p, task: t, ownerId, olderTask, olderApprovalComment, originalTasks: [olderTask, t, marketing, hidden], approvedRevision: { id: approvedRevision.latestRevisionId, body: approvedRevision.body },
|
||||
approvalComment, draftRevisionId: draft.latestRevisionId, draftBody, privateTask: hidden, privateMarker };
|
||||
}, gradeDesignSearch);
|
||||
return { ...base, authentication: "human-session", sourceFiles: [...base.sourceFiles, "chat-catalog-domains/internal-design-search.ts", "chat-catalog-domains/internal-privacy.ts", "chat-catalog-domains/identity-auth.ts"],
|
||||
async observe(ctx, state): Promise<DesignObservation> {
|
||||
if (!key) throw new Error("Design probe credential was not created during setup");
|
||||
const read = async (path: string) => {
|
||||
const url = new URL(path, ctx.input.api.baseURL); if (url.origin !== ctx.input.api.baseURL || !url.pathname.startsWith("/api/")) throw new Error("Design probe escaped fixture API");
|
||||
const response = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(15_000), headers: { Authorization: `Bearer ${key!.token}` } });
|
||||
const body: unknown = response.ok ? await response.json() : null; await response.body?.cancel(); return { status: response.status, body };
|
||||
};
|
||||
const o = await observeInternal(ctx, state); const hidden = entity(state, "privateTask"); const target = entity(state, "task");
|
||||
const [privateRead, search] = await Promise.all([read(`/api/issues/${hidden.id}`), read(`/api/companies/${o.companyId}/search?q=launch%20website%20design`)]);
|
||||
const result = search.body as { results?: unknown[] } | null;
|
||||
if (search.status === 200 && !Array.isArray(result?.results)) throw new Error("Design search lacks production results collection");
|
||||
return { ...o, approvedRevision: o.revisions.find(row => row.id === (state.approvedRevision as Entity).id),
|
||||
olderApprovalComments: await ctx.input.api.get<Entity[]>(`/api/issues/${entity(state, "olderTask").id}/comments?order=asc`),
|
||||
approvalComments: await ctx.input.api.get<Entity[]>(`/api/issues/${target.id}/comments?order=asc`), privateReadStatus: privateRead.status,
|
||||
agentSearchStatus: search.status, agentSearchResults: result?.results ?? [] };
|
||||
},
|
||||
async cleanup(ctx) { if (key) { await ctx.input.api.delete(`/api/agents/${ctx.input.fixtures.agent.id}/keys/${key.id}`, { allowNotFound: true }); key = undefined; } },
|
||||
};
|
||||
}
|
||||
export const designSearchCases: readonly ChatCatalogCase[] = [findDesign()];
|
||||
@@ -15,6 +15,16 @@ function event(action: string, entityId: string, details?: Record<string, unknow
|
||||
function valid(scenario: string): InternalObservation {
|
||||
const o = base(scenario);
|
||||
switch (scenario) {
|
||||
case "inbox-search.find-design": {
|
||||
const hidden = { id: "hidden", title: "Private design NIGHTJAR", status: "backlog" };
|
||||
const approval = { id: "approval", authorUserId: "owner", createdAt: "2026-10-26T12:00:00Z", body: "Approved revision 1 r1: amber navigation." };
|
||||
o.tasks.push(hidden); Object.assign(o.state, { originalTasks: structuredClone(o.tasks), privateTask: hidden, privateMarker: "NIGHTJAR", ownerId: "owner",
|
||||
approvedRevision: { id: "r1", body: "Amber navigation" }, olderApprovalComment: { ...approval, id: "older", createdAt: "2026-10-26T11:00:00Z" }, approvalComment: approval, draftRevisionId: "r2", draftBody: "Violet draft" });
|
||||
o.documents = [{ id: "doc", key: "plan", latestRevisionId: "r2", body: "Violet draft" }];
|
||||
o.reply = "The approved design is revision 1 with amber navigation. The newer revision is unapproved.";
|
||||
Object.assign(o, { approvedRevision: { id: "r1", body: "Amber navigation" }, approvalComments: [approval], olderApprovalComments: [{ ...approval, id: "older", createdAt: "2026-10-26T11:00:00Z" }], privateReadStatus: 403,
|
||||
agentSearchStatus: 200, agentSearchResults: [{ id: "task" }] }); break;
|
||||
}
|
||||
case "inbox-search.archive-done":
|
||||
case "inbox-search.restore-archive": {
|
||||
const restore = scenario === "inbox-search.restore-archive"; const count = restore ? 3 : 501;
|
||||
@@ -252,6 +262,7 @@ const corrupt: Record<string, (o: InternalObservation) => void> = {
|
||||
"documents-artifacts.write-plan": o => { o.revisions[0]!.createdByRunId = "unrelated-run"; },
|
||||
"documents-artifacts.resolve-feedback": o => { o.annotations![1]!.status = "resolved"; },
|
||||
"documents-artifacts.restore-document": o => { o.documents[0]!.body = "third"; },
|
||||
"inbox-search.find-design": o => { o.reply = "Revision 2 is the latest approved design."; },
|
||||
"activity.company-overview": o => { o.reply = o.reply.replace("1 blocked", "3 blocked"); },
|
||||
"activity.audit-change": o => { o.reply = "Morgan, the current assignee, probably changed it today."; },
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { goalsCases } from "./internal-goals.js";
|
||||
import { documents_artifactsCases } from "./internal-documents-artifacts.js";
|
||||
import { activityCases } from "./internal-activity.js";
|
||||
import { inboxSearchCases } from "./internal-inbox-search.js";
|
||||
import { designSearchCases } from "./internal-design-search.js";
|
||||
import { inboxTriageCases } from "./internal-inbox-triage.js";
|
||||
|
||||
export const internalChatCatalogCases = [
|
||||
@@ -15,6 +16,7 @@ export const internalChatCatalogCases = [
|
||||
...activityCases,
|
||||
...inboxSearchCases,
|
||||
...inboxTriageCases,
|
||||
...designSearchCases,
|
||||
];
|
||||
export function gradeInternal(value: unknown) {
|
||||
const o = observation(value);
|
||||
|
||||
Reference in new issue
Block a user