mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-12 22:56:58 +01:00
Preserve reviewed SDK and task prerequisite eval corrections
This commit is contained in:
1 parent
063925683c
commit
372b192327
11 files changed
+434
-47
No files matched your search
@@ -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
|
||||
|
||||
@@ -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<object>(), pollUntil: async (options: { load(): Promise<unknown>; 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();
|
||||
});
|
||||
+31
@@ -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();
|
||||
});
|
||||
@@ -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<PublicCustodyAgent>(`/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<PublicCustodyAgent>(`/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<PublicCustodyAgent>(`/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,
|
||||
|
||||
@@ -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<object>(),
|
||||
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();
|
||||
});
|
||||
@@ -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<string, unknown>).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);
|
||||
});
|
||||
});
|
||||
@@ -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<ChatCatalogContext, { fixture: Awaited<ReturnType<typeof createPreviewFixture>>; 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<Issue>(`/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<IssueComment[]>(`/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<RoutineDetail>(`/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<RoutineRevision[]>(`/api/routines/${routine.id}/revisions`) };
|
||||
} catch (error) { await cleanup(ctx); throw error; }
|
||||
}, observeManaged, gradeIntegratorPluginRoutine, async (ctx, state) => {
|
||||
|
||||
@@ -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<Entity>(`/api/agents/${id}`)).status !== "paused") throw new Error("Inventory owner pause did not persist");
|
||||
const before = await ctx.input.api.get<InventoryOwner>(`/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<InventoryOwner>(`/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<InventoryOwner>(`/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<Entity & { lifecycleState: string }>(`/api/agents/${ctx.input.fixtures.agent.id}`);
|
||||
const owners = await Promise.all((state.workerIds as string[]).map(id => ctx.input.api.get<Entity & { lifecycleState: string }>(`/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<InventoryOwner>(`/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();
|
||||
},
|
||||
|
||||
@@ -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<ChatCatalogContext, { id: string; token: string; workerId: string }>();
|
||||
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(/<h1\b[^>]*>([\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<Entity>(`${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<Entity>(`/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();
|
||||
|
||||
@@ -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<object>(), 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<string,string>) { 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<string,string>})=>{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<string,string>=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<string,string>;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);
|
||||
});
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user