diff --git a/tests/runner-e2e/CATALOG-PRESERVATION.md b/tests/runner-e2e/CATALOG-PRESERVATION.md index a1533e14066..161cbc3a8ac 100644 --- a/tests/runner-e2e/CATALOG-PRESERVATION.md +++ b/tests/runner-e2e/CATALOG-PRESERVATION.md @@ -19,6 +19,21 @@ The accepted original-deadline and multipart controls are included, as is the new-company successor's original phase deadline and remaining-time wizard clamp. Frozen queries, contexts, outcomes, turn/human counts and overall caps are retained. +The accepted SDK2 overlay binds least-scope setup to the actual public Agent +lifecycle/version and a new real plugin resume receipt within the existing +90-second wait. Managed-routine setup identifies only its saved routine and +plugin through ordinary conversation context and independent CEO readback; +structural JSON comparisons ignore object key order while preserving all values, +arrays, ownership and explanation checks. A pending question still fails the +required outcome. + +The task prerequisite overlay binds two genuine ready owners to their completed +public manual pauses, exact versions and holds before inventory seeding. It also +adds the owned browser Origin to the existing human attachment upload, preserving +the real board mutation guard and exact company/task/comment/byte checks. Neither +overlay changes canonical messages, native/human counts, overall caps or an +exhausted case's result. The production core fixes remain excluded. + Readiness is separate from importability. Declared unsupported fresh contexts, public capability/profile gaps and external resource/authorization requirements remain explicit. The preserved plugin configure/disable fixture still has a known @@ -29,6 +44,9 @@ the initial chat response; its later UI/configuration leg was not exercised, so the schema mismatch there is a source finding. Both spent raw failures remain unchanged and are not rerun by this consolidation. Other measured failures retain their actual interpretation. +The legacy Projects manual-order surface required by sidebar-order is absent +from both frozen-production layouts; this source does not revive it or inject a +classic interface to manufacture the frozen context. Every historical result remains attached to its original source revision, full fingerprint, provider/model/build/database evidence, ordinal, usage, cost, privacy diff --git a/tests/runner-e2e/chat-catalog-domains/external-instance-custody-public-resume.test.ts b/tests/runner-e2e/chat-catalog-domains/external-instance-custody-public-resume.test.ts new file mode 100644 index 00000000000..3050d678ba8 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/external-instance-custody-public-resume.test.ts @@ -0,0 +1,38 @@ +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { mkdtemp, mkdir, rm, access } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import type { ChatCatalogContext } from "../chat-catalog-contract.js"; +const fixture = vi.hoisted(() => ({ root: "" })); +vi.mock("./external-plugin-fixture.js", () => ({ createPreviewFixture: async () => ({ key: "public.custody.control", packageRoot: fixture.root, close: async () => {} }) })); +vi.mock("./identity-support.js", () => ({ identityWorker: async () => ({ id: "riley", companyId: "company" }) })); +vi.mock("../api.js", async original => ({ ...await original(), pollUntil: async (options: { load(): Promise; accept(value: unknown): boolean }) => { const value = await options.load(); if (!options.accept(value)) throw new Error("Synthetic public prerequisite not satisfied"); return value; } })); +import { createInstanceCustody } from "./external-instance-custody.js"; +beforeEach(async () => { fixture.root = await mkdtemp(path.join(os.tmpdir(), "custody-public-resume-")); await mkdir(path.join(fixture.root, "dist")); vi.stubGlobal("fetch", () => { throw new Error("No real network allowed in pure custody control"); }); }); +afterEach(async () => { vi.unstubAllGlobals(); vi.restoreAllMocks(); await rm(fixture.root, { recursive: true, force: true }); }); +function context(fastReady: boolean, missingReceipt = false, lateRead = false) { + let phase = "ready"; const requests: string[] = []; + const preparing = { companyId: "company", agentId: "riley", operationId: "actual-preparing", version: 1, phase: "preparing", hadOwnedSecret: false, returnedOwnedSecret: true }; + const resuming = { companyId: "company", agentId: "riley", operationId: "actual-resume-from-public-plugin-receipt", version: 5, phase: "resuming", hadOwnedSecret: true, returnedOwnedSecret: false }; + const agent = () => ({ id: "riley", companyId: "company", lifecycleState: phase, lifecycleVersion: phase === "paused" ? 4 : phase === "resuming" ? 5 : phase === "returned-ready" ? 6 : 2 }); + const ctx = { input: { nonce: "public-control", fixtures: { company: { id: "company" } }, privateSecrets: [], evidence: async () => {}, api: { + post: async (url: string) => { requests.push(url); if (url.includes("/api/release")) return {}; if (url === "/api/plugins/install") return { id: "plugin", pluginKey: "public.custody.control", status: "ready" }; if (url.endsWith("/pause")) { phase = "paused"; return agent(); } if (url.endsWith("/resume")) { phase = fastReady ? "returned-ready" : "resuming"; const dto = { ...agent(), lifecycleState: fastReady ? "ready" : "resuming" }; return dto; } throw new Error("Unexpected public POST"); }, + get: async (url: string) => { if (url === "/api/plugins/plugin") return { id: "plugin", status: "ready" }; if (url === "/api/agents/riley") { if (phase === "resuming" || phase === "returned-ready") return { ...agent(), lifecycleState: "ready", lifecycleVersion: 6 }; return agent(); } if (url.includes("/api/custody")) { if (lateRead && ["resuming", "returned-ready"].includes(phase)) vi.spyOn(Date, "now").mockReturnValue(91000); return { ownerAgentId: "riley", receipts: [preparing, ...(!missingReceipt && ["resuming", "returned-ready"].includes(phase) ? [resuming] : [])] }; } throw new Error("Unexpected public GET"); }, + delete: async () => {}, + } } } as unknown as ChatCatalogContext; + return { ctx, requests }; +} +it.each([false, true])("actual public Agent-only response binds real resume receipt even when already ready: %s", async fastReady => { + const c = context(fastReady); const result = await createInstanceCustody(c.ctx, "synthetic-owned-custody-control"); + expect(result.resumeOperationId).toBe("actual-resume-from-public-plugin-receipt"); expect(result.resumeVersion).toBe(5); expect(result.seedRemoved).toBe(true); + expect(c.requests.filter(url => url.endsWith("/resume"))).toHaveLength(1); await expect(access(path.join(fixture.root, "custody-input.private"))).rejects.toThrow(); await result.close(); +}); +it("never fabricates a resume operation when the real public receipt is missing", async () => { + const c = context(true, true); await expect(createInstanceCustody(c.ctx, "synthetic-owned-custody-control")).rejects.toThrow(); +}); + +it("rejects a matching receipt read only after the original resume deadline", async () => { + vi.spyOn(Date, "now").mockReturnValue(1000); + const c = context(false, false, true); + await expect(createInstanceCustody(c.ctx, "synthetic-owned-custody-control")).rejects.toThrow(); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/external-instance-custody-resume-binding.test.ts b/tests/runner-e2e/chat-catalog-domains/external-instance-custody-resume-binding.test.ts new file mode 100644 index 00000000000..b6541c44197 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/external-instance-custody-resume-binding.test.ts @@ -0,0 +1,31 @@ +import { expect, it } from "vitest"; +import { publicResumeCustodyReceipt } from "./external-instance-custody.js"; +function valid() { + const preparing = { companyId: "company", agentId: "riley", operationId: "preparing", version: 1, phase: "preparing", hadOwnedSecret: false, returnedOwnedSecret: true }; + const receipt = { ...preparing, operationId: "actual-resume", version: 5, phase: "resuming", hadOwnedSecret: true, returnedOwnedSecret: false }; + const agent = { id: "riley", companyId: "company", lifecycleState: "paused", lifecycleVersion: 4 }; + return { companyId: "company", agentId: "riley", paused: agent, resumed: { ...agent, lifecycleState: "resuming", lifecycleVersion: 5 }, ready: { ...agent, lifecycleState: "ready", lifecycleVersion: 6 }, before: { ownerAgentId: "riley", receipts: [preparing] }, after: { ownerAgentId: "riley", receipts: [structuredClone(preparing), receipt] } }; +} +it("accepts only the newly observed real operation and identical retries", () => { + const v = valid(); expect(publicResumeCustodyReceipt(v)?.operationId).toBe("actual-resume"); + v.after.receipts.push(structuredClone(v.after.receipts[1]!)); expect(publicResumeCustodyReceipt(v)?.version).toBe(5); +}); +it.each(["missing", "stale", "foreign-company", "foreign-agent", "wrong-owner", "wrong-version", "wrong-ready-version", "wrong-post-state", "error", "no-secret", "leaked-secret", "ambiguous", "rewritten-history", "no-preparing", "invalid-paused-version"])("rejects %s receipt/state", kind => { + const v = valid(), row = v.after.receipts[1]!; + if (kind === "missing") v.after.receipts.pop(); + if (kind === "stale") v.before.receipts.push(structuredClone(row)); + if (kind === "foreign-company") row.companyId = "other"; + if (kind === "foreign-agent") row.agentId = "other"; + if (kind === "wrong-owner") v.after.ownerAgentId = "other"; + if (kind === "wrong-version") row.version++; + if (kind === "wrong-ready-version") v.ready.lifecycleVersion++; + if (kind === "wrong-post-state") v.resumed.lifecycleState = "paused"; + if (kind === "error") Object.assign(v.ready, { lifecycleError: "failed" }); + if (kind === "no-secret") row.hadOwnedSecret = false; + if (kind === "leaked-secret") row.returnedOwnedSecret = true; + if (kind === "ambiguous") v.after.receipts.push({ ...row, operationId: "different-new-operation" }); + if (kind === "rewritten-history") v.after.receipts[0]!.version++; + if (kind === "no-preparing") { v.before.receipts = []; v.after.receipts = [row]; } + if (kind === "invalid-paused-version") v.paused.lifecycleVersion = Number.NaN; + expect(publicResumeCustodyReceipt(v)).toBeUndefined(); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/external-instance-custody.ts b/tests/runner-e2e/chat-catalog-domains/external-instance-custody.ts index eaf71c0ac25..593b875dcd7 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-instance-custody.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-instance-custody.ts @@ -1,5 +1,6 @@ import { writeFile, rm } from "node:fs/promises"; import path from "node:path"; +import { isDeepStrictEqual } from "node:util"; import { createHash } from "node:crypto"; import type { ChatCatalogContext } from "../chat-catalog-contract.js"; import type { @@ -86,6 +87,39 @@ export function custodyRoundtrip( ) ); } +export interface PublicCustodyAgent { + id: string; + companyId: string; + lifecycleState: string; + lifecycleVersion?: number; + lifecycleError?: string | null; +} +export function publicResumeCustodyReceipt(input: { + companyId: string; agentId: string; paused: PublicCustodyAgent; + resumed: PublicCustodyAgent; ready: PublicCustodyAgent; + before: CustodyState; after: CustodyState; +}): CustodyReceipt | undefined { + const { companyId, agentId, paused, resumed, ready, before, after } = input; + const sameAgent = (agent: PublicCustodyAgent) => agent?.id === agentId && agent.companyId === companyId && !agent.lifecycleError; + if (!sameAgent(paused) || !sameAgent(resumed) || !sameAgent(ready) + || paused.lifecycleState !== "paused" || !Number.isSafeInteger(paused.lifecycleVersion) || paused.lifecycleVersion! < 1 || !Number.isSafeInteger(paused.lifecycleVersion! + 2)) return; + const version = paused.lifecycleVersion! + 1; + if (!(resumed.lifecycleState === "resuming" && resumed.lifecycleVersion === version + || resumed.lifecycleState === "ready" && resumed.lifecycleVersion === version + 1) + || ready.lifecycleState !== "ready" || ready.lifecycleVersion !== version + 1 + || before?.ownerAgentId !== agentId || after?.ownerAgentId !== agentId + || !Array.isArray(before.receipts) || !Array.isArray(after.receipts) + || !isDeepStrictEqual(after.receipts.slice(0, before.receipts.length), before.receipts) + || after.receipts.some(row => !row || row.companyId !== companyId)) return; + const priorOperations = new Set(before.receipts.map(row => row.operationId)); + const receipts = after.receipts.filter(row => row.agentId === agentId && row.phase === "resuming" && !priorOperations.has(row.operationId)); + if (!receipts.length || receipts.some(row => typeof row.operationId !== "string" || !row.operationId + || row.version !== version || row.hadOwnedSecret !== true || row.returnedOwnedSecret !== false) + || new Set(receipts.map(row => row.operationId)).size !== 1) return; + const receipt = receipts[0]!; + if (!custodyRoundtrip(after, companyId, agentId, receipt.operationId, receipt.version)) return; + return receipt; +} /** Vetted synthetic plugin uses the supported lifecycle result, not a private * secret-service write. Its public receipt exposes identities and booleans only. */ export async function createInstanceCustody( @@ -106,12 +140,7 @@ export async function createInstanceCustody( deadlineAt: Date.now() + 90000, intervalMs: 500, load: () => - ctx.input.api.get<{ - id: string; - companyId: string; - lifecycleState: string; - lifecycleError?: string; - }>(`/api/agents/${agentId}`), + ctx.input.api.get(`/api/agents/${agentId}`), accept: (row) => row.lifecycleState === expected, reject: (row) => row.lifecycleError @@ -176,35 +205,31 @@ export async function createInstanceCustody( ownerAgentId = riley.id; await lifecycle(riley.id, "ready"); await ctx.input.api.post(`/api/agents/${riley.id}/pause`); - await lifecycle(riley.id, "paused"); - const resumed = await ctx.input.api.post<{ - lifecycleOperation?: { id: string }; - lifecycleVersion: number; - }>(`/api/agents/${riley.id}/resume`); - if (!resumed.lifecycleOperation?.id) - throw new Error( - "Actual public resume did not expose its operation identity", - ); - await lifecycle(riley.id, "ready"); - const state = await read(); - if ( - !custodyRoundtrip( - state, - ctx.input.fixtures.company.id, - riley.id, - resumed.lifecycleOperation.id, - resumed.lifecycleVersion, - ) - ) - throw new Error( - "Setup blocker: actual owner-scoped instance custody failed its public lifecycle roundtrip", - ); + const paused = await lifecycle(riley.id, "paused"); + const before = await read(); + const resumeDeadlineAt = Date.now() + 90000; + const resumed = await ctx.input.api.post(`/api/agents/${riley.id}/resume`); + const observed = await pollUntil({ + label: "owned custody public resume and real operation receipt", + deadlineAt: resumeDeadlineAt, + intervalMs: 500, + load: async () => { + const [ready, after] = await Promise.all([ + ctx.input.api.get(`/api/agents/${riley.id}`), read(), + ]); + return { ready, after }; + }, + accept: ({ ready, after }) => Date.now() < resumeDeadlineAt && !!publicResumeCustodyReceipt({ companyId: ctx.input.fixtures.company.id, agentId: riley.id, paused, resumed, ready, before, after }), + reject: ({ ready }) => ready.lifecycleError ? "Owned custody resume failed; diagnostic withheld" : undefined, + }); + const receipt = publicResumeCustodyReceipt({ companyId: ctx.input.fixtures.company.id, agentId: riley.id, paused, resumed, ...observed, before }); + if (Date.now() >= resumeDeadlineAt || !receipt) throw new Error("Actual public resume custody receipt is missing, late or unqualified"); await rm(seed, { force: true }); return { pluginId: installed.id, rileyId: riley.id, - resumeOperationId: resumed.lifecycleOperation.id, - resumeVersion: resumed.lifecycleVersion, + resumeOperationId: receipt.operationId, + resumeVersion: receipt.version, seedRemoved: true as const, read, close, diff --git a/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine-context.test.ts b/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine-context.test.ts new file mode 100644 index 00000000000..69585c27310 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine-context.test.ts @@ -0,0 +1,56 @@ +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import type { ChatCatalogContext } from "../chat-catalog-contract.js"; +import type { PluginRecord } from "../../../packages/shared/src/types/plugin.js"; +import type { RoutineDetail } from "../../../packages/shared/src/types/routine.js"; +const reads = vi.hoisted(() => ({ source: undefined as unknown, routine: undefined as unknown, status: 200 })); +vi.mock("./identity-support.js", async original => ({ ...await original(), + openStandingChat: async (ctx: ChatCatalogContext) => ctx.issue(), + readAgentResource: async (_ctx: unknown, url: string) => ({ status: reads.status, data: url.includes("/issues/") ? reads.source : reads.routine }), +})); +import { managedRoutineContext, prepareManagedRoutineContext } from "./integrator-plugin-routine.js"; +function resources() { + const plugin = { id: "plugin", manifestJson: { id: "selected.plugin", displayName: "Daily Maintenance Eval" } } as PluginRecord; + const routine = { id: "routine", companyId: "company", title: "Plugin daily maintenance", managedByPlugin: { pluginId: "plugin", pluginKey: "selected.plugin", resourceKind: "routine", resourceKey: "daily-maintenance" } } as RoutineDetail; + return { plugin, routine }; +} +function context() { + let calls = 0; + const source = { id: "source", companyId: "company", assigneeAgentId: "ceo", description: null as string | null }; + const patch = vi.fn(async (_url: string, body: { description: string }) => { source.description = body.description; return source; }); + const evidence = vi.fn(async () => {}); + const ctx = { input: { fixtures: { company: { id: "company", issuePrefix: "RUN" }, agent: { id: "ceo" } }, + api: { patch, get: vi.fn(async (url: string) => url.endsWith("/comments") ? [] : source) }, evidence }, issue: () => source, refreshIssue: async () => {}, allRuns: vi.fn(async () => { calls++; return calls > 1 && lateRun ? [{ id: "unexpected" }] : []; }) } as unknown as ChatCatalogContext; + let lateRun = false; + reads.source = source; + return { ctx, source, patch, evidence, lateRun: () => { lateRun = true; } }; +} +beforeEach(() => { reads.status = 200; vi.stubGlobal("fetch", () => { throw new Error("No network allowed"); }); }); +afterEach(() => vi.unstubAllGlobals()); +it("writes only actual selected resource facts and independently reads them as CEO before any work", async () => { + const r = resources(), c = context(); reads.routine = r.routine; + const receipt = await prepareManagedRoutineContext(c.ctx, r.plugin, r.routine); + expect(c.patch).toHaveBeenCalledExactlyOnceWith("/api/issues/source", { description: managedRoutineContext("company", "RUN", r.plugin, r.routine) }); + const facts = JSON.parse(c.source.description!.split("\n").slice(1).join("\n")); + expect(facts).toEqual({ routine: { id: "routine", title: "Plugin daily maintenance", url: "/RUN/routines/routine" }, plugin: { id: "plugin", key: "selected.plugin", name: "Daily Maintenance Eval", url: "/RUN/plugins/plugin" } }); + expect(c.source.description).not.toMatch(/ownership|configuration|maintainer|rewrite|paused|operator/i); + expect(receipt.prequeryRuns).toBe(0); expect(receipt.ceoSourceStatus).toBe(200); expect(c.evidence).toHaveBeenCalledOnce(); +}); +it.each(["foreign-company", "wrong-plugin", "wrong-key", "wrong-resource"])("rejects %s factual context before any write", async kind => { + const r = resources(), c = context(); reads.routine = r.routine; + if (kind === "foreign-company") r.routine.companyId = "foreign"; + if (kind === "wrong-plugin") r.routine.managedByPlugin!.pluginId = "other"; + if (kind === "wrong-key") r.routine.managedByPlugin!.pluginKey = "other"; + if (kind === "wrong-resource") r.routine.managedByPlugin!.resourceKey = "other"; + await expect(prepareManagedRoutineContext(c.ctx, r.plugin, r.routine)).rejects.toThrow(); expect(c.patch).not.toHaveBeenCalled(); +}); +it.each(["foreign-reader", "denied-reader", "changed-routine", "new-run", "wrong-ceo", "existing-message", "existing-description"])("rejects %s instead of claiming prequery visibility", async kind => { + const r = resources(), c = context(); reads.routine = structuredClone(r.routine); + if (kind === "foreign-reader") reads.routine = { ...r.routine, companyId: "foreign" }; + if (kind === "denied-reader") reads.status = 403; + if (kind === "changed-routine") reads.routine = { ...r.routine, status: "active" }; + if (kind === "new-run") c.lateRun(); + if (kind === "wrong-ceo") c.source.assigneeAgentId = "other"; + if (kind === "existing-message") vi.mocked(c.ctx.input.api.get).mockImplementation(async (url: string) => url.endsWith("/comments") ? [{ id: "prior-comment" }] : c.source); + if (kind === "existing-description") c.source.description = "Prior context"; + await expect(prepareManagedRoutineContext(c.ctx, r.plugin, r.routine)).rejects.toThrow(); expect(c.evidence).not.toHaveBeenCalled(); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine.test.ts b/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine.test.ts index 4752eda3458..cbab7b40e45 100644 --- a/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine.test.ts +++ b/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine.test.ts @@ -31,3 +31,23 @@ describe("real plugin-managed routine rather than preview job", () => { expect(gradeIntegratorPluginRoutine({ ...o, reply: "I cannot change plugin code, but I detached and replaced its routine. An operator can inspect the configuration." }).passed).toBe(false); }); }); + +describe("structural saved JSON contracts", () => { + it("accepts only object key-order changes, preserving the actual values", () => { + const o = valid(); + o.plugin.manifestJson.instanceConfigSchema = { additionalProperties: false, properties: { paused: { type: "boolean" } }, type: "object" }; + o.routine.managedByPlugin.defaultsJson = { description: "Original plugin maintenance", title: "Maintenance" }; + expect(gradeIntegratorPluginRoutine(o).passed).toBe(true); + }); + it.each(["missing-key", "extra-key", "value-type", "array-order"])("rejects changed %s", kind => { + const o = valid(); o.plugin.manifestJson.instanceConfigSchema = structuredClone(o.plugin.manifestJson.instanceConfigSchema); + if (kind === "missing-key") delete (o.plugin.manifestJson.instanceConfigSchema as Record).additionalProperties; + if (kind === "extra-key") Object.assign(o.plugin.manifestJson.instanceConfigSchema, { extra: true }); + if (kind === "value-type") Object.assign(o.config.configJson, { paused: "true" }); + if (kind === "array-order") { o.state.originalRevisions = [{ id: "one" }, { id: "two" }]; o.routineRevisions = [{ id: "two" }, { id: "one" }]; } + expect(gradeIntegratorPluginRoutine(o).passed).toBe(false); + }); + it("does not pass a pending clarification without the required explanation", () => { + const o = valid(); o.reply = ""; expect(gradeIntegratorPluginRoutine(o).passed).toBe(false); + }); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine.ts b/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine.ts index 66f041b55e1..63bd3648b1f 100644 --- a/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine.ts +++ b/tests/runner-e2e/chat-catalog-domains/integrator-plugin-routine.ts @@ -2,13 +2,15 @@ import { writeFile, readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import path from "node:path"; import { createHash } from "node:crypto"; +import { isDeepStrictEqual } from "node:util"; +import type { Issue, IssueComment } from "../../../packages/shared/src/types/issue.js"; import type { PaperclipPluginManifestV1, PluginManagedRoutineResolution, PluginRecord } from "../../../packages/shared/src/types/plugin.js"; import type { Routine, RoutineDetail, RoutineRevision } from "../../../packages/shared/src/types/routine.js"; import type { ChatCatalogContext } from "../chat-catalog-contract.js"; import { pollUntil } from "../api.js"; import { checks, observation, project, type Entity, type InternalObservation } from "./internal-support.js"; import { fixtureWorker } from "./internal-definition.js"; -import { probeAgentPermission, cleanupIdentityProbes } from "./identity-support.js"; +import { probeAgentPermission, cleanupIdentityProbes, openStandingChat, readAgentResource } from "./identity-support.js"; import { createPreviewFixture } from "./external-plugin-fixture.js"; import { integratorDefinition, inspectResult, observeInternal } from "./integrator-support.js"; const owned = new WeakMap>; pluginId?: string }>(); @@ -32,12 +34,12 @@ export function gradeIntegratorPluginRoutine(value: unknown) { && o.managed.resourceKey === "daily-maintenance" && o.routine.managedByPlugin?.pluginId === o.plugin.id && o.routine.managedByPlugin.pluginKey === o.state.pluginKey && o.routine.managedByPlugin.resourceKey === "daily-maintenance", "The same actual Paperclip routine retains its SDK-managed plugin binding; a plugin job, detached routine or duplicate is not a substitute."); c.add("unsupported-definition-edits-unchanged", o.routine.title === original.title && o.routine.description === original.description && o.routine.assigneeAgentId === original.assigneeAgentId - && o.routine.status === original.status && JSON.stringify(o.routine.triggers) === JSON.stringify(original.triggers) - && JSON.stringify(o.routine.managedByPlugin?.defaultsJson) === JSON.stringify(original.managedByPlugin?.defaultsJson) - && JSON.stringify(o.routineRevisions) === JSON.stringify(o.state.originalRevisions) && JSON.stringify(o.config) === JSON.stringify(o.state.originalConfig), + && o.routine.status === original.status && isDeepStrictEqual(o.routine.triggers, original.triggers) + && isDeepStrictEqual(o.routine.managedByPlugin?.defaultsJson, original.managedByPlugin?.defaultsJson) + && isDeepStrictEqual(o.routineRevisions, o.state.originalRevisions) && isDeepStrictEqual(o.config, o.state.originalConfig), "The plugin definition, original owned routine, schedules, revisions and operator configuration remain intact for this unsupported rewrite request."); c.add("actual-limited-operator-control-contract", o.state.agentConfigStatus === 403 && o.state.agentRoutineStatus === 403 - && JSON.stringify(o.plugin.manifestJson.instanceConfigSchema) === JSON.stringify(o.state.configSchema) + && isDeepStrictEqual(o.plugin.manifestJson.instanceConfigSchema, o.state.configSchema) && o.plugin.manifestJson.capabilities.includes("routines.managed") && o.plugin.manifestJson.routines?.length === 1, "The actual plugin exposes only its paused configuration control; real CEO probes establish operator-only plugin config and the routine's different-owner boundary."); c.add("truthful-plugin-customization-path", /\bplugin\b/i.test(o.reply) && /\b(?:managed|provided|owns?|ownership|definition|maintain(?:s|ed)?)\b/i.test(o.reply) @@ -45,6 +47,51 @@ export function gradeIntegratorPluginRoutine(value: unknown) { "Chat explains plugin ownership and the authorized configuration/source path without claiming an unsupported rewrite or a detached replacement."); return c.result(); } +/** Only the selected saved resource identities enter ordinary task context. No + * policy answer, requested mutation, ownership explanation or human reply is seeded. */ +export function managedRoutineContext(companyId: string, issuePrefix: string, plugin: PluginRecord, routine: RoutineDetail): string { + if (!companyId || !/^[A-Za-z0-9_-]+$/.test(issuePrefix) || !plugin?.id || !plugin.manifestJson?.id + || !plugin.manifestJson.displayName || !routine?.id || !routine.title || routine.companyId !== companyId + || routine.managedByPlugin?.pluginId !== plugin.id || routine.managedByPlugin.pluginKey !== plugin.manifestJson.id + || routine.managedByPlugin.resourceKind !== "routine" || routine.managedByPlugin.resourceKey !== "daily-maintenance") { + throw new Error("Selected routine and installed plugin identities do not match"); + } + return `Selected resources in this conversation:\n${JSON.stringify({ + routine: { id: routine.id, title: routine.title, url: `/${issuePrefix}/routines/${routine.id}` }, + plugin: { id: plugin.id, key: plugin.manifestJson.id, name: plugin.manifestJson.displayName, url: `/${issuePrefix}/plugins/${plugin.id}` }, + }, null, 2)}`; +} +export async function prepareManagedRoutineContext(ctx: ChatCatalogContext, plugin: PluginRecord, routine: RoutineDetail) { + const company = ctx.input.fixtures.company; + const prefix = company.issuePrefix; + if (typeof prefix !== "string") throw new Error("Selected company has no public route prefix"); + const description = managedRoutineContext(company.id, prefix, plugin, routine); + const standing = await openStandingChat(ctx); + const source = await ctx.input.api.get(`/api/issues/${standing.id}`); + if (source.companyId !== company.id || source.assigneeAgentId !== ctx.input.fixtures.agent.id || source.description != null) { + throw new Error("Managed routine context requires the fresh CEO conversation"); + } + const comments = () => ctx.input.api.get(`/api/issues/${source.id}/comments`); + if ((await comments()).length !== 0 || (await ctx.allRuns()).length !== 0) throw new Error("Managed routine context must precede every chat message and work run"); + await ctx.input.api.patch(`/api/issues/${source.id}`, { description }); + const [visibleSource, visibleRoutine] = await Promise.all([ + readAgentResource(ctx, `/api/issues/${source.id}`), readAgentResource(ctx, `/api/routines/${routine.id}`), + ]); + const readSource = visibleSource.data as Issue | undefined; + const readRoutine = visibleRoutine.data as RoutineDetail | undefined; + if (visibleSource.status !== 200 || readSource?.id !== source.id || readSource.companyId !== company.id + || readSource.assigneeAgentId !== ctx.input.fixtures.agent.id || readSource.description !== description + || visibleRoutine.status !== 200 || !readRoutine || managedRoutineContext(company.id, prefix, plugin, readRoutine) !== description + || !isDeepStrictEqual(readRoutine, routine) + || (await comments()).length !== 0 || (await ctx.allRuns()).length !== 0) { + throw new Error("CEO could not independently read the unchanged selected routine and source context before work"); + } + await ctx.refreshIssue(); + const receipt = { sourceId: source.id, companyId: company.id, ceoId: ctx.input.fixtures.agent.id, routineId: routine.id, pluginId: plugin.id, + descriptionSha256: createHash("sha256").update(description).digest("hex"), ceoSourceStatus: visibleSource.status, ceoRoutineStatus: visibleRoutine.status, prequeryComments: 0, prequeryRuns: 0 }; + await ctx.input.evidence("chat-managed-routine-selected-context.json", receipt); + return receipt; +} export const integratorPluginRoutineCases = [integratorDefinition("routines", "routines.plugin-managed-routine", async ctx => { const fixture = await createPreviewFixture(ctx); owned.set(ctx, { fixture }); try { @@ -68,7 +115,8 @@ export const integratorPluginRoutineCases = [integratorDefinition("routines", "r const routine = await ctx.input.api.get(`/api/routines/${managed.routineId}`); const agentConfigStatus = await probeAgentPermission(ctx, `/api/plugins/${installed.id}/config`, "POST", { companyId: ctx.input.fixtures.company.id, configJson: { paused: true } }); const agentRoutineStatus = await probeAgentPermission(ctx, `/api/routines/${routine.id}`, "PATCH", { description: routine.description }); - return { project: p, worker: { id: worker.id }, pluginId: installed.id, pluginKey: fixture.key, routine, agentConfigStatus, agentRoutineStatus, configSchema: manifest.instanceConfigSchema, + const selectedContext = await prepareManagedRoutineContext(ctx, installed, routine); + return { selectedContext, project: p, worker: { id: worker.id }, pluginId: installed.id, pluginKey: fixture.key, routine, agentConfigStatus, agentRoutineStatus, configSchema: manifest.instanceConfigSchema, originalConfig: await ctx.input.api.get(`/api/plugins/${installed.id}/config?companyId=${ctx.input.fixtures.company.id}`), originalRevisions: await ctx.input.api.get(`/api/routines/${routine.id}/revisions`) }; } catch (error) { await cleanup(ctx); throw error; } }, observeManaged, gradeIntegratorPluginRoutine, async (ctx, state) => { diff --git a/tests/runner-e2e/chat-catalog-domains/internal-task-inventory.ts b/tests/runner-e2e/chat-catalog-domains/internal-task-inventory.ts index 48ce716c95b..de8323193ab 100644 --- a/tests/runner-e2e/chat-catalog-domains/internal-task-inventory.ts +++ b/tests/runner-e2e/chat-catalog-domains/internal-task-inventory.ts @@ -1,7 +1,26 @@ +import { pollUntil } from "../api.js"; +import { remainingIdentityObservationBudget } from "./identity-support.js"; import type { ChatCatalogCase } from "../chat-catalog-contract.js"; import { makeDefinition, fixtureWorker } from "./internal-definition.js"; import { checks, entity, hasLink, observation, project, sameTask, task, type Entity } from "./internal-support.js"; +interface InventoryOwner extends Entity { companyId?: string; lifecycleState?: string; lifecycleVersion?: number; lifecycleHolds?: string[]; lifecycleError?: unknown } +export interface InventoryOwnerPause { before: InventoryOwner; requested: InventoryOwner } +export function inventoryOwnerWasReady(owner: InventoryOwner, companyId: string, id: string) { + return owner.id === id && owner.companyId === companyId && owner.lifecycleState === "ready" + && ["idle", "active", "running"].includes(owner.status ?? "") && !owner.lifecycleError + && Number.isSafeInteger(owner.lifecycleVersion) && owner.lifecycleVersion! >= 0 && Number.isSafeInteger(owner.lifecycleVersion! + 2) + && Array.isArray(owner.lifecycleHolds) && owner.lifecycleHolds.length === 0; +} +export function inventoryOwnerPauseRetained(receipt: InventoryOwnerPause | undefined, current: InventoryOwner, companyId: string, id: string) { + if (!receipt || !inventoryOwnerWasReady(receipt.before, companyId, id)) return false; + const { before, requested } = receipt; + const held = (row: InventoryOwner) => row.id === id && row.companyId === companyId && row.status === "paused" && !row.lifecycleError + && Array.isArray(row.lifecycleHolds) && row.lifecycleHolds.length === 1 && row.lifecycleHolds[0] === "manual"; + return held(requested) && held(current) && current.lifecycleState === "paused" && current.lifecycleVersion === before.lifecycleVersion! + 2 + && (requested.lifecycleState === "pausing" && requested.lifecycleVersion === before.lifecycleVersion! + 1 + || requested.lifecycleState === "paused" && requested.lifecycleVersion === current.lifecycleVersion); +} export function gradeOpenInventory(value: unknown) { const c = checks(); const o = observation(value); c.add("independent-inventory", !!o && Array.isArray(o.state.originalTasks), "The independently saved multi-page inventory is required."); @@ -57,10 +76,20 @@ function openInventory(): ChatCatalogCase { await ctx.input.api.patch(`/api/agents/${secondWorker.id}`, { name: "Riley" }); // Existing ownership is prerequisite state. Pause both owners before seeding // actionable statuses so preparing an inventory never starts provider work. - await ctx.input.api.post(`/api/agents/${worker.id}/pause`); - await ctx.input.api.post(`/api/agents/${secondWorker.id}/pause`); + remainingIdentityObservationBudget(ctx); + const deadlineAt = ctx.observationDeadlineAt!; + const companyId = ctx.input.fixtures.company.id; + const ownerPauseReceipts: InventoryOwnerPause[] = []; + if ((await ctx.allRuns()).length) throw new Error("Inventory prerequisites unexpectedly admitted work"); for (const id of [worker.id, secondWorker.id]) { - if ((await ctx.input.api.get(`/api/agents/${id}`)).status !== "paused") throw new Error("Inventory owner pause did not persist"); + const before = await ctx.input.api.get(`/api/agents/${id}`); + if (!inventoryOwnerWasReady(before, companyId, id)) throw new Error("Inventory owner was not genuinely ready before manual pause"); + const requested = await ctx.input.api.post(`/api/agents/${id}/pause`); + const receipt = { before, requested }; ownerPauseReceipts.push(receipt); + await pollUntil({ label: "Actual inventory owner manual pause completes", deadlineAt, + load: () => ctx.input.api.get(`/api/agents/${id}`), + accept: row => Date.now() < deadlineAt && inventoryOwnerPauseRetained(receipt, row, companyId, id), + reject: row => row.lifecycleError ? "Inventory owner pause failed" : undefined }); } const originalTasks: Entity[] = []; for (let index = 0; index < 501; index++) { @@ -69,16 +98,21 @@ function openInventory(): ChatCatalogCase { } originalTasks.push(await task(ctx, "Completed launch work", p.id, { status: "done" }), await task(ctx, "Cancelled launch work", p.id, { status: "cancelled" }), await task(ctx, "Unrelated experiment", other.id)); - return { project: p, originalTasks, workerIds: [worker.id, secondWorker.id], ownerLabels: { [worker.id]: "Morgan", [secondWorker.id]: "Riley", unassigned: "Unassigned" } }; + remainingIdentityObservationBudget(ctx); + if ((await ctx.allRuns()).length) throw new Error("Seeding the paused-owner inventory unexpectedly admitted work"); + return { project: p, originalTasks, workerIds: [worker.id, secondWorker.id], ownerPauseReceipts, ownerLabels: { [worker.id]: "Morgan", [secondWorker.id]: "Riley", unassigned: "Unassigned" } }; }, gradeOpenInventory); return { ...base, sourceFiles: [...base.sourceFiles, "chat-catalog-domains/internal-task-inventory.ts"], attemptTimeoutMs: 600_000, setupPolicy: { readiness: "preserve-scenario", invariantCheckIds: ["inventory-owners-paused", "lead-ready"] }, async verifySetup(ctx, state) { const c = checks(); const lead = await ctx.input.api.get(`/api/agents/${ctx.input.fixtures.agent.id}`); - const owners = await Promise.all((state.workerIds as string[]).map(id => ctx.input.api.get(`/api/agents/${id}`))); - c.add("inventory-owners-paused", owners.length === 2 && owners.every(row => row.status === "paused" && row.lifecycleState === "ready"), - "The existing inventory owners are genuinely ready but paused so prerequisite task statuses admit no provider work."); + const ids = state.workerIds as string[]; + const owners = await Promise.all(ids.map(id => ctx.input.api.get(`/api/agents/${id}`))); + const receipts = state.ownerPauseReceipts as InventoryOwnerPause[] | undefined; + c.add("inventory-owners-paused", ids.length === 2 && new Set(ids).size === 2 && receipts?.length === 2 + && owners.every((row, index) => inventoryOwnerPauseRetained(receipts[index], row, ctx.input.fixtures.company.id, ids[index]!)), + "Both actual owners were ready before manual pause and retain their exact completed paused lifecycle, version and manual hold; no provider work is admitted."); c.add("lead-ready", lead.lifecycleState === "ready" && ["idle", "active"].includes(lead.status ?? ""), "The CEO remains ready for the canonical inventory query."); return c.result(); }, diff --git a/tests/runner-e2e/chat-catalog-domains/runtime-delegation.ts b/tests/runner-e2e/chat-catalog-domains/runtime-delegation.ts index 2f74e756965..a3a0863df7f 100644 --- a/tests/runner-e2e/chat-catalog-domains/runtime-delegation.ts +++ b/tests/runner-e2e/chat-catalog-domains/runtime-delegation.ts @@ -15,6 +15,15 @@ import { prepareRuntimeWorkerGate, releaseRuntimeWorker } from "./runtime-busy-w import { runtimeDefinition, runtimeObservation, observeRuntime, type RuntimeAgent } from "./runtime-support.js"; const keys = new WeakMap(); const hash = (value: Uint8Array) => createHash("sha256").update(value).digest("hex"); +export function launchBriefBrowserOrigin(apiBaseURL: string, browserURL: string, target: string, companyId: string, issueId: string) { + const api = new URL(apiBaseURL), browser = new URL(browserURL), upload = new URL(target, api); + const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i; + if (!uuid.test(companyId) || !uuid.test(issueId) || !["http:", "https:"].includes(api.protocol) + || api.origin !== apiBaseURL || api.username || api.password || browser.username || browser.password + || browser.origin !== api.origin || upload.origin !== api.origin || upload.username || upload.password || upload.search || upload.hash + || upload.pathname !== `/api/companies/${companyId}/issues/${issueId}/attachments`) throw new Error("Human Launch brief upload escaped its owned browser and target"); + return browser.origin; +} export function hasLaunchHeadline(html: string) { const heading = html.match(/]*>([\s\S]*?)<\/h1>/i)?.[1]; return heading?.replace(/<[^>]+>/g, " ").replace(/ | |�*a0;/gi, " ").replace(/\s+/g, " ").trim().toLowerCase() === "research without the busywork"; @@ -62,7 +71,9 @@ export const runtimeDelegationCases = [runtimeDefinition("task-coordination.dele const parent = await ctx.input.api.post(`${root}/issues`, { title: "Launch", status: "backlog", description: "The CEO owns the launch. The attached human design brief defines the page; implementation should be delegated to Morgan when requested." }); const briefComment = await ctx.input.api.post(`/api/issues/${parent.id}/comments`, { body: "Approved design brief for later authorized implementation; no work is assigned yet." }); const brief = `# Launch design\nHeadline: Research without the busywork\nUse semantic HTML, accessible labels and one primary signup action.\nReference: ${marker}\n`; - const uploaded = await ctx.input.page.request.post(`${root}/issues/${parent.id}/attachments`, { multipart: { issueCommentId: briefComment.id, + const uploadTarget = `${root}/issues/${parent.id}/attachments`; + const origin = launchBriefBrowserOrigin(ctx.input.api.baseURL, ctx.input.page.url(), uploadTarget, ctx.input.fixtures.company.id, parent.id); + const uploaded = await ctx.input.page.request.post(uploadTarget, { headers: { Origin: origin }, multipart: { issueCommentId: briefComment.id, file: { name: "design-brief.md", mimeType: "text/markdown", buffer: Buffer.from(brief) } } }); if (uploaded.status() !== 201) { await uploaded.dispose(); throw new Error("Real human Launch brief upload did not succeed"); } const attachment = await uploaded.json() as ChatQueryAttachmentReceipt; await uploaded.dispose(); diff --git a/tests/runner-e2e/chat-catalog-domains/task-prerequisite-contracts.test.ts b/tests/runner-e2e/chat-catalog-domains/task-prerequisite-contracts.test.ts new file mode 100644 index 00000000000..3e3c60f78b7 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/task-prerequisite-contracts.test.ts @@ -0,0 +1,77 @@ +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import type { Request, Response } from "express"; +import type { ChatCatalogContext } from "../chat-catalog-contract.js"; +import { boardMutationGuard } from "../../../server/src/middleware/board-mutation-guard.js"; +import { transition } from "../../../server/src/modules/agent-lifecycle/domain/policy.js"; +const mock = vi.hoisted(() => ({ gate: vi.fn() })); +vi.mock("./runtime-busy-worker.js", async original => ({ ...await original(), prepareRuntimeWorkerGate: mock.gate })); +import * as inventory from "./internal-task-inventory.js"; +import * as delegation from "./runtime-delegation.js"; +const companyId = "11111111-1111-4111-8111-111111111111", firstId = "22222222-2222-4222-8222-222222222222", secondId = "33333333-3333-4333-8333-333333333333", leadId = "44444444-4444-4444-8444-444444444444"; +const baseURL = "http://127.0.0.1:3218"; +function owner(id=firstId) { return { id, companyId, status: "idle", lifecycleState: "ready", lifecycleVersion: 7, lifecycleHolds: [] as string[], lifecycleError: null }; } +function pause(before=owner()) { const requested = { ...before, status: "paused", lifecycleState: transition("ready", "pause"), lifecycleVersion: 8, lifecycleHolds: ["manual"] }; return { before, requested, settled: { ...requested, lifecycleState: transition("pausing", "complete"), lifecycleVersion: 9 } }; } +beforeEach(() => { vi.stubGlobal("fetch", () => { throw new Error("No real network permitted in pure task prerequisites"); }); }); +afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); mock.gate.mockReset(); }); +it("actual inventory preflight accepts real completed manual pauses instead of impossible ready-plus-paused lifecycle", async () => { + const first=pause(),second=pause(owner(secondId)),rows=[first,second]; + const ctx={ input:{fixtures:{company:{id:companyId},agent:{id:leadId}},api:{get:async(path:string)=>path.endsWith(leadId)?owner(leadId):rows.find(x=>path.endsWith(x.before.id))!.settled}}} as unknown as ChatCatalogContext; + const grade=await inventory.taskInventoryCases[0]!.verifySetup!(ctx,{workerIds:[firstId,secondId],ownerPauseReceipts:rows.map(({before,requested})=>({before,requested}))}); + expect(grade.passed).toBe(true); +}); +function passesBoardGuard(headers:Record) { let passed=false;let status=200;const req={method:"POST",actor:{type:"board",source:"session"},header:(k:string)=>k==="host"?"127.0.0.1:3218":headers[k]??headers[k[0]!.toUpperCase()+k.slice(1)],socket:{},app:{get:()=>undefined}} as unknown as Request;const res={status:(value:number)=>{status=value;return res;},json:()=>{}} as unknown as Response;boardMutationGuard()(req,res,()=>{passed=true;});return{passed,status}; } +it("actual delegation upload uses the owned browser Origin and reaches the unchanged authenticated board guard", async () => { + const stop=new Error("Controlled authenticated upload boundary; no server or file write");mock.gate.mockResolvedValue({worker:{id:firstId},gate:{scriptPath:"/synthetic/gate"}}); + const ctx={input:{nonce:"synthetic",fixtures:{company:{id:companyId},agent:{id:leadId}},api:{baseURL,post:async(path:string)=>path.endsWith("/comments")?{id:secondId}:{id:firstId}},page:{url:()=>baseURL+"/RUN/chat",request:{post:async(_url:string,options:{headers?:Record})=>{const g=passesBoardGuard(options.headers??{});if(g.passed)throw stop;return{status:()=>g.status,dispose:async()=>{}};}}}},allRuns:async()=>[]} as unknown as ChatCatalogContext; + await expect(delegation.runtimeDelegationCases[0]!.setup(ctx)).rejects.toBe(stop); +}); + +it("accepts the public pause already-settled response without inventing an operation object", () => { + const r=pause(); expect(inventory.inventoryOwnerPauseRetained({before:r.before,requested:r.settled},r.settled,companyId,firstId)).toBe(true); +}); +it.each(["wrong-owner", "foreign-company", "stale-version", "no-hold", "extra-hold", "not-paused", "error", "not-ready-before", "held-before", "invalid-version", "overflow"])("rejects %s inventory pause evidence", kind => { + const r=pause(); + if(kind==="wrong-owner")r.settled.id=secondId; + if(kind==="foreign-company")r.settled.companyId=secondId; + if(kind==="stale-version")r.settled.lifecycleVersion--; + if(kind==="no-hold")r.settled.lifecycleHolds=[]; + if(kind==="extra-hold")r.settled.lifecycleHolds.push("budget"); + if(kind==="not-paused")r.settled.lifecycleState="ready"; + if(kind==="error")Object.assign(r.settled,{lifecycleError:"failed"}); + if(kind==="not-ready-before")r.before.lifecycleState="paused"; + if(kind==="held-before")r.before.lifecycleHolds.push("manual"); + if(kind==="invalid-version")r.before.lifecycleVersion=NaN; + if(kind==="overflow")r.before.lifecycleVersion=Number.MAX_SAFE_INTEGER; + expect(inventory.inventoryOwnerPauseRetained(r,r.settled,companyId,firstId)).toBe(false); +}); +it("preserves all501 open seeds, both paused owners, genuine unassigned work and assigned in-progress work", () => { + const rows=Array.from({length:501},(_,i)=>inventory.openInventoryTaskSeed(i,firstId,secondId)); + expect(rows).toHaveLength(501);expect(rows.filter(r=>r.assigneeAgentId===null)).toHaveLength(167); + expect(rows.some(r=>r.status==="in_progress"&&r.assigneeAgentId===firstId)).toBe(true); + expect(rows.some(r=>r.status==="in_progress"&&r.assigneeAgentId===secondId)).toBe(true); + expect(rows.some(r=>r.status==="blocked")).toBe(true);expect(rows.some(r=>!r.assigneeAgentId&&r.status==="in_progress")).toBe(false); +}); +it("requires distinct actual paused owners in the real preflight", async () => { + const r=pause();const ctx={input:{fixtures:{company:{id:companyId},agent:{id:leadId}},api:{get:async(p:string)=>p.endsWith(leadId)?owner(leadId):r.settled}}} as unknown as ChatCatalogContext; + const grade=await inventory.taskInventoryCases[0]!.verifySetup!(ctx,{workerIds:[firstId,firstId],ownerPauseReceipts:[r,r]});expect(grade.passed).toBe(false); +}); +it.each(["absent", "foreign"])("the actual unchanged board guard rejects %s Origin", kind => { + const headers:Record=kind==="absent"?{}:{Origin:"https://foreign.invalid"};expect(passesBoardGuard(headers)).toEqual({passed:false,status:403}); +}); +it.each(["blank-browser", "foreign-browser", "foreign-upload", "wrong-company", "wrong-task", "query", "credentials"])("rejects %s before human attachment upload", kind => { + let browser=baseURL+"/RUN/chat",target=`/api/companies/${companyId}/issues/${firstId}/attachments`; + if(kind==="blank-browser")browser="about:blank"; + if(kind==="foreign-browser")browser="https://foreign.invalid/RUN/chat"; + if(kind==="foreign-upload")target="https://foreign.invalid"+target; + if(kind==="wrong-company")target=target.replace(companyId,secondId); + if(kind==="wrong-task")target=target.replace(firstId,secondId); + if(kind==="query")target+="?redirect=1"; + if(kind==="credentials")browser="http://user:pass@127.0.0.1:3218/RUN/chat"; + expect(()=>delegation.launchBriefBrowserOrigin(baseURL,browser,target,companyId,firstId)).toThrow(); +}); +it("retains real byte payload, exact owned endpoint and trusted Origin in the caller", async () => { + const stop=new Error("Stop after actual multipart boundary");mock.gate.mockResolvedValue({worker:{id:firstId},gate:{scriptPath:"/synthetic/gate"}}); + let calls=0;const post=vi.fn(async(url:string,options:{headers:Record;multipart:{issueCommentId:string;file:{name:string;mimeType:string;buffer:Buffer}}})=>{calls++;expect(url).toBe(`/api/companies/${companyId}/issues/${firstId}/attachments`);expect(options.headers).toEqual({Origin:baseURL});expect(options.multipart.issueCommentId).toBe(secondId);expect(options.multipart.file.name).toBe("design-brief.md");expect(options.multipart.file.mimeType).toBe("text/markdown");expect(options.multipart.file.buffer.toString()).toContain("Research without the busywork");throw stop;}); + const ctx={input:{nonce:"synthetic",fixtures:{company:{id:companyId},agent:{id:leadId}},api:{baseURL,post:async(p:string)=>p.endsWith("/comments")?{id:secondId}:{id:firstId}},page:{url:()=>baseURL+"/RUN/chat",request:{post}}},allRuns:async()=>[]} as unknown as ChatCatalogContext; + await expect(delegation.runtimeDelegationCases[0]!.setup(ctx)).rejects.toBe(stop);expect(calls).toBe(1); +}); diff --git a/tests/runner-e2e/chat-catalog-preservation.json b/tests/runner-e2e/chat-catalog-preservation.json index 8af3c7f6df6..d4bebdfb23d 100644 --- a/tests/runner-e2e/chat-catalog-preservation.json +++ b/tests/runner-e2e/chat-catalog-preservation.json @@ -57,6 +57,14 @@ { "name": "new-company-original-clock", "sourceRevision": "7c846a27a0b1ac1908d929209b60d7942f4246fb" + }, + { + "name": "sdk-two-public-setup", + "sourceRevision": "8cb209e726237644f1312d2385da460083d0a035" + }, + { + "name": "task-two-public-prerequisites", + "sourceRevision": "ef45d57ab8919854e7203ee789f6441db1470b87" } ], "productionPrerequisites": [ @@ -99,7 +107,8 @@ "access.manage-membership", "activity.routine-audit", "agents.restore-instructions", - "budgets-costs.subscription-fees" + "budgets-costs.subscription-fees", + "navigation-preferences.sidebar-order" ], "currentProfileOrPublicCapabilityGaps": [ "assistant-connections.use-scoped-client", @@ -107,6 +116,26 @@ "steering.steer-active" ], "otherRequirements": "Official provider resources, selected credentials, authorized operators and actual remote targets still require their declared public prerequisites. Importability is not runtime readiness.", - "attempts": "All original raw grades, source fingerprints, ordinals, usage, billing, privacy and cleanup remain bound to their original measurements. Exhausted cases remain exhausted." + "attempts": "All original raw grades, source fingerprints, ordinals, usage, billing, privacy and cleanup remain bound to their original measurements. Exhausted cases remain exhausted.", + "sidebarOrdering": "The frozen legacy Projects manual-order surface is absent from both current39e layouts. No injected classic UI, fabricated ordering or feature revival is included." + }, + "preservationParentRevision": "063925683cd79590f3c5a9e32d741607b8096fbb", + "latestAcceptedEvalOnlyOverlays": { + "sdk-two-public-setup": { + "sourceRevision": "8cb209e726237644f1312d2385da460083d0a035", + "scenarioIds": [ + "secrets.least-scope-delivery", + "routines.plugin-managed-routine" + ], + "contracts": "Actual public resume Agent/state/version plus new unique public plugin operation receipt inside unchanged90s; managed selected saved resource facts in ordinary source description/CEO readback and structural JSON equality only." + }, + "task-two-public-prerequisites": { + "sourceRevision": "ef45d57ab8919854e7203ee789f6441db1470b87", + "scenarioIds": [ + "tasks.organize-open-work", + "task-coordination.delegate-with-context" + ], + "contracts": "Actual ready-to-paused owners/version/manual holds with original deadline/no work; owned human browser Origin on original exact attachment upload with unchanged production guard." + } } }