mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-06 14:02:36 +01:00
fix(evals): prepare prerequisite people through public membership routes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
81987af644
commit
dea44755b0
16 files changed
+177
-32
No files matched your search
@@ -563,3 +563,21 @@ Runner hook performs it. Only synthetic source bytes and proof JSON enter the
|
||||
existing private attempt evidence and registered-artifact verification pipeline.
|
||||
A failed attempt remains retained. These development checks are not staging or
|
||||
Slack qualification, and they do not publish trusted archives or credentials.
|
||||
|
||||
## Authenticated prerequisite people
|
||||
|
||||
Twenty explicitly selected OpenCode/local chat journeys use ordinary public
|
||||
signup and instance-admin company-access management to prepare a fresh second
|
||||
person. The setup verifies the administrator's real session, assigns only the
|
||||
requested company and its existing operator/viewer role grants, and independently
|
||||
reads the saved membership and grant attribution. The person's own session must
|
||||
prove company access and denial of instance administration. Setup consumes the
|
||||
original observation clock and neither dispatches native work nor performs the
|
||||
human action under evaluation. Private-resource and other-person boundaries
|
||||
remain part of each journey's independent oracle.
|
||||
|
||||
Actual invitation journeys retain the invitation/acceptance path and its logger
|
||||
qualification prerequisite. This alternative does not qualify that path, create
|
||||
an instance administrator, bypass route authorization, or change retained failed
|
||||
outcomes. Passwords and cookies remain in the existing private session registry;
|
||||
the setup receipt contains only public identity, role, grant keys and statuses.
|
||||
@@ -3,7 +3,7 @@ import { expect } from "@playwright/test";
|
||||
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import type { McpConnection } from "../../../packages/shared/src/public-mcp.js";
|
||||
import { connect, mcp, type ToolResult } from "../public-mcp-client.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
import { prepareAssistantSetup } from "./external-assistant-connections.js";
|
||||
import { definition, externalObservation, observeExternal, root, checks, verifyRenderedReply } from "./external-support.js";
|
||||
import { waitForReply } from "./identity-support.js";
|
||||
@@ -38,7 +38,7 @@ export const assistantConsentExternalCases = [definition("assistant-connections.
|
||||
const viewer = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets, name: "Viewer Casey" }); people.set(ctx, viewer);
|
||||
try {
|
||||
const caller = await ctx.input.api.get<{ user: { id: string } }>("/api/auth/get-session");
|
||||
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, viewer, secrets, "viewer");
|
||||
await enrollPrerequisitePerson(ctx, viewer, "viewer");
|
||||
const marker = `Viewer allowed fixture ${randomBytes(12).toString("hex")}`;
|
||||
const task = await ctx.input.api.post<{ id: string; title: string; description: string }>(`${root(ctx)}/issues`, { title: "Viewer consent sample task", description: marker, status: "backlog" });
|
||||
return { viewerId: viewer.id, chatCallerId: caller.user.id, phaseBoundary: "operator-chat-to-viewer-browser-consent", taskId: task.id, taskBefore: task, marker, forbiddenTitle: `Forbidden viewer task ${ctx.input.nonce}` };
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "./identity-support.js";
|
||||
import {
|
||||
createAuthenticatedPerson,
|
||||
enrollAuthenticatedPerson,
|
||||
enrollPrerequisitePerson,
|
||||
} from "./identity-auth.js";
|
||||
import { privateBrowserEntry } from "./external-private-browser.js";
|
||||
import { secretTestEndpoint } from "./external-secret-endpoint.js";
|
||||
@@ -204,12 +204,7 @@ export const personalSecretExternalCases = [
|
||||
});
|
||||
let endpoint: PrivateFixture["endpoint"] | undefined;
|
||||
try {
|
||||
await enrollAuthenticatedPerson(
|
||||
ctx.input.api,
|
||||
ctx.input.fixtures.company.id,
|
||||
other,
|
||||
secrets,
|
||||
);
|
||||
await enrollPrerequisitePerson(ctx, other);
|
||||
const definition = await ctx.input.api.post<UserSecretDefinition>(
|
||||
`${root(ctx)}/user-secret-definitions`,
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ import {expect} from "@playwright/test";
|
||||
import type {ChatCatalogContext} from "../chat-catalog-contract.js";
|
||||
import type {ConnectionIntentInteraction} from "../../../packages/shared/src/types/issue.js";
|
||||
import {pollUntil} from "../api.js";
|
||||
import {createAuthenticatedPerson,enrollAuthenticatedPerson} from "./identity-auth.js";
|
||||
import {createAuthenticatedPerson,enrollPrerequisitePerson} from "./identity-auth.js";
|
||||
import {openStandingChat,waitForReply} from "./identity-support.js";
|
||||
import {definition,externalObservation,observeExternal,checks,root,verifyRenderedReply} from "./external-support.js";
|
||||
import type {Entity} from "./internal-support.js";
|
||||
@@ -27,7 +27,7 @@ export const wrongResolverExternalCases=[definition("connection-setup.wrong-pers
|
||||
const session=await ctx.input.api.get<{user:{id:string}}>("/api/auth/get-session");if(!session.user?.id||session.user.id==="local-board")throw new Error("Actual signed-in other member is required");
|
||||
const alex=await createAuthenticatedPerson({api:ctx.input.api,browser,secrets,name:"Alex"});people.set(ctx,alex);
|
||||
try{
|
||||
await enrollAuthenticatedPerson(ctx.input.api,ctx.input.fixtures.company.id,alex,secrets);
|
||||
await enrollPrerequisitePerson(ctx, alex);
|
||||
for(const scope of [{scopeType:"company",scopeId:ctx.input.fixtures.company.id},{scopeType:"agent",scopeId:ctx.input.fixtures.agent.id}])await ctx.input.api.post(`${root(ctx)}/budgets/policies`,{...scope,amount:500,hardStopEnabled:true,isActive:true,unpricedUsagePolicy:"allow"});
|
||||
if((await ctx.input.api.get<{connections:unknown[]}>(`${root(ctx)}/tools/connections`)).connections.length)throw new Error("Wrong resolver requires no existing Notion account");
|
||||
const response=await alex.request(`${root(ctx)}/issues`,"POST",{title:"Alex's launch notes",status:"todo",assigneeAgentId:ctx.input.fixtures.agent.id,description:"Read my Notion and summarize the current launch notes. I have not connected my Notion account yet."});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import type { CompanyMemberRecord, CompanyMembersResponse, UserCompanyAccessResponse } from "../../../packages/shared/src/types/access.js";
|
||||
import { grantsForHumanRole } from "../../../server/src/services/company-member-roles.js";
|
||||
import { mkdtemp, writeFile, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -99,3 +102,52 @@ export async function enrollAuthenticatedPerson(api: RunnerApi, companyId: strin
|
||||
if (!companiesResponse.ok || !(await companiesResponse.json() as Array<{ id: string }>).some(row => row.id === companyId)) throw new Error("Approved person cannot read the intended company");
|
||||
return { userId: person.id, companyId, inviteId: invite.id, joinRequestId: pending[0]!.id };
|
||||
}
|
||||
|
||||
export const prerequisiteMembershipScenarioIds = Object.freeze([
|
||||
"assistant-connections.consent-read-only", "connection-setup.wrong-person-resolves",
|
||||
"documents-artifacts.private-download", "documents-artifacts.restore-document",
|
||||
"inbox-search.archive-done", "inbox-search.notification-audience", "inbox-search.restore-archive", "inbox-search.show-decisions",
|
||||
"navigation-preferences.broken-link", "navigation-preferences.dismiss-announcement", "navigation-preferences.edit-profile", "navigation-preferences.quick-search", "navigation-preferences.sidebar-order",
|
||||
"projects.private-project", "projects.share-project", "questions-and-approvals.wrong-reviewer", "secrets.personal-secret",
|
||||
"tasks.completion-with-gate", "tasks.private-task", "tasks.share-private-task",
|
||||
]);
|
||||
export async function enrollPrerequisitePerson(ctx: ChatCatalogContext, person: Awaited<ReturnType<typeof createAuthenticatedPerson>>, role: "operator" | "viewer" = "operator") {
|
||||
if (!prerequisiteMembershipScenarioIds.includes(ctx.scenario.id) || ctx.input.execution.id !== `chat-features.runner-opencode.local.${ctx.scenario.id}` || !["operator", "viewer"].includes(role))
|
||||
throw new Error("Public prerequisite membership is not admitted for this scenario or role");
|
||||
const deadline = ctx.observationDeadlineAt;
|
||||
const remaining = () => { const now = Date.now(); if (!Number.isFinite(deadline) || deadline! <= now) throw new Error("Original prerequisite observation budget exhausted or missing"); return Math.min(15000, deadline! - now); };
|
||||
const { api, fixtures } = ctx.input, companyId = fixtures.company.id;
|
||||
const read = async <T>(route: string) => { const value = await api.get<T>(route, { timeout: remaining() }); remaining(); return value; };
|
||||
const personal = async (route: string) => { remaining(); const response = await person.request(route); remaining(); return response; };
|
||||
const health = await read<{ deploymentMode: string }>("/api/health");
|
||||
if (health.deploymentMode !== "authenticated") throw new Error("Public membership requires real authenticated sessions");
|
||||
const session = await read<{ user: { id: string } }>("/api/auth/get-session");
|
||||
if (!session.user?.id || session.user.id === person.id || session.user.id === "local-board") throw new Error("Distinct real first-admin authority is required");
|
||||
const admin = await read<UserCompanyAccessResponse>(`/api/admin/users/${session.user.id}/company-access`);
|
||||
if (admin.user?.id !== session.user.id || admin.user.isInstanceAdmin !== true) throw new Error("Authenticated fixture administrator authority was not verified");
|
||||
const targetBefore = await read<UserCompanyAccessResponse>(`/api/admin/users/${person.id}/company-access`);
|
||||
if (targetBefore.user?.id !== person.id || targetBefore.user.isInstanceAdmin !== false || targetBefore.companyAccess.length !== 0) throw new Error("Prerequisite person must be a fresh non-admin user");
|
||||
const own = await personal("/api/auth/get-session");
|
||||
if (own.status !== 200) { await own.body?.cancel(); throw new Error("Prerequisite person's real session unavailable"); }
|
||||
if (((await own.json()) as { user?: { id?: string } }).user?.id !== person.id) throw new Error("Prerequisite session belongs to another person");
|
||||
const before = await personal(`/api/companies/${companyId}`), beforeStatus = before.status; await before.body?.cancel();
|
||||
if (![403, 404].includes(beforeStatus)) throw new Error("Fresh person already has company access or denial proof failed");
|
||||
remaining(); const assigned = await api.putSensitive<UserCompanyAccessResponse>(`/api/admin/users/${person.id}/company-access`, { companyIds: [companyId] }); remaining();
|
||||
const membership = assigned.companyAccess.find(row => row.companyId === companyId);
|
||||
if (assigned.user?.id !== person.id || assigned.user.isInstanceAdmin !== false || assigned.companyAccess.length !== 1 || membership?.principalId !== person.id || membership.principalType !== "user" || membership.status !== "active" || membership.membershipRole !== "operator") throw new Error("Public company access response did not establish the exact prerequisite member");
|
||||
const grants = grantsForHumanRole(role);
|
||||
remaining(); await api.patchSensitive<CompanyMemberRecord>(`/api/companies/${companyId}/members/${membership.id}/role-and-grants`, { membershipRole: role, status: "active", grants }); remaining();
|
||||
const members = await read<CompanyMembersResponse>(`/api/companies/${companyId}/members`), actual = members.members.find(row => row.id === membership.id);
|
||||
const keys = (rows: Array<{ permissionKey: string; scope?: Record<string, unknown> | null }>) => rows.map(row => `${row.permissionKey}:${JSON.stringify(row.scope ?? null)}`).sort().join("\n");
|
||||
if (!actual || actual.companyId !== companyId || actual.principalId !== person.id || actual.principalType !== "user" || actual.membershipRole !== role || actual.status !== "active" || keys(actual.grants) !== keys(grants) || actual.grants.some(row => row.companyId !== companyId || row.principalId !== person.id || row.principalType !== "user" || row.grantedByUserId !== session.user.id)) throw new Error("Persisted prerequisite role or grants differ from the public requested contract");
|
||||
const targetAfter = await read<UserCompanyAccessResponse>(`/api/admin/users/${person.id}/company-access`);
|
||||
if (targetAfter.user?.id !== person.id || targetAfter.user.isInstanceAdmin !== false || targetAfter.companyAccess.length !== 1 || targetAfter.companyAccess[0]?.id !== membership.id || targetAfter.companyAccess[0]?.companyId !== companyId || targetAfter.companyAccess[0]?.membershipRole !== role || targetAfter.companyAccess[0]?.status !== "active") throw new Error("Prerequisite membership or non-admin identity changed after role assignment");
|
||||
const allowed = await personal(`/api/companies/${companyId}`);
|
||||
if (allowed.status !== 200) { await allowed.body?.cancel(); throw new Error("Real prerequisite person cannot read its company"); }
|
||||
if (((await allowed.json()) as { id?: string }).id !== companyId) throw new Error("Real prerequisite person read another company");
|
||||
const denied = await personal("/api/admin/users"), deniedStatus = denied.status; await denied.body?.cancel();
|
||||
if (deniedStatus !== 403) throw new Error("Prerequisite person acquired instance authority or denial proof failed");
|
||||
const receipt = { userId: person.id, companyId, membershipId: membership.id, role, grantKeys: grants.map(row => row.permissionKey).sort(), authentication: "real-person-session", administrator: "verified-instance-admin", enrollment: "public-company-access", beforeStatus, companyReadStatus: 200, instanceAdminReadStatus: deniedStatus };
|
||||
remaining(); await ctx.input.evidence("identity-public-prerequisite-membership.json", receipt); remaining();
|
||||
return receipt;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type { ChatCatalogCase, ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { RunnerApiHttpError } from "../api.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
import { checks, entity, project, sameTask, task, type Entity } from "./internal-support.js";
|
||||
import { identityDefinition, identityObservation, observeIdentity, openStandingChat, probeAgentPermission, waitForReply } from "./identity-support.js";
|
||||
|
||||
@@ -64,7 +64,7 @@ export const navigationBoundaryIdentityCases: readonly ChatCatalogCase[] = [
|
||||
const browser = ctx.input.page.context().browser(); if (!browser || !ctx.input.privateSecrets) throw new Error("Missing independent real-user setup");
|
||||
const observer = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Riley" }); observers.set(ctx, observer);
|
||||
try {
|
||||
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, observer, ctx.input.privateSecrets, "viewer");
|
||||
await enrollPrerequisitePerson(ctx, observer, "viewer");
|
||||
const denial = await observer.request(`/api/issues/${restricted.id}`); const observerRestrictedStatus = denial.status; await denial.body?.cancel();
|
||||
return { project: p, stale, similar, restricted, restrictedMarker, stalePath: path, deletedStatus: await status(ctx, `/api/issues/${stale.id}`), observerRestrictedStatus };
|
||||
} catch (error) { await observer.dispose(); observers.delete(ctx); throw error; }
|
||||
@@ -97,7 +97,7 @@ export const navigationBoundaryIdentityCases: readonly ChatCatalogCase[] = [
|
||||
const observer = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Riley" }); observers.set(ctx, observer);
|
||||
try {
|
||||
const otherCompany = await ctx.input.api.post<Entity & { issuePrefix: string }>("/api/companies", { name: `Birch announcements ${ctx.input.nonce}` }); otherCompanies.set(ctx, otherCompany.id);
|
||||
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, observer, ctx.input.privateSecrets, "viewer");
|
||||
await enrollPrerequisitePerson(ctx, observer, "viewer");
|
||||
const before = await observer.request("/api/announcements/current"); const observerPublication = await before.json() as Publication | null;
|
||||
const invalid = await observer.request(`/api/announcements/${encodeURIComponent(publication.id)}/dismiss`, "POST", { companyId: otherCompany.id });
|
||||
const invalidAuditCompanyStatus = invalid.status; await invalid.body?.cancel();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { checks, entity, hasLink, project, task, verifyInternalResult } from "./internal-support.js";
|
||||
import { identityDefinition, identityObservation, waitForReply, probeAgentPermission } from "./identity-support.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
const searchObservers = new WeakMap<ChatCatalogContext, Awaited<ReturnType<typeof createAuthenticatedPerson>>>();
|
||||
|
||||
@@ -65,7 +65,7 @@ export const navigationIdentityCases = [
|
||||
if (!browser || !ctx.input.privateSecrets) throw new Error("Quick search audience requires actual authenticated browser identities");
|
||||
const observer = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Riley" }); searchObservers.set(ctx, observer);
|
||||
try {
|
||||
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, observer, ctx.input.privateSecrets, "viewer");
|
||||
await enrollPrerequisitePerson(ctx, observer, "viewer");
|
||||
const privateTask = await task(ctx, "Launch private acquisition", state.project.id, { visibility: "private", description: `Private launch note ${ctx.input.nonce}` });
|
||||
return { ...state, privateTask, observerId: observer.id };
|
||||
} catch (error) { await observer.dispose(); searchObservers.delete(ctx); throw error; }
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { grantsForHumanRole } from "../../../server/src/services/company-member-roles.js";
|
||||
import { enrollPrerequisitePerson, prerequisiteMembershipScenarioIds } from "./identity-auth.js";
|
||||
afterEach(() => vi.useRealTimers());
|
||||
function fixture(role: "viewer" | "operator" = "operator") {
|
||||
const membership = { id: "membership", companyId: "company", principalId: "person", principalType: "user", status: "active", membershipRole: "operator" };
|
||||
const state = { assigned: false, administrator: true, targetAdministrator: false, preexisting: false, personalId: "person", companyReadId: "company", beforeStatus: 403, adminStatus: 403, readRole: role, extraGrant: false, foreignGrant: false, grantedBy: "admin", slowPut: false };
|
||||
const writes: Array<{ route: string; body: unknown }> = [], reads: string[] = [], personalReads: string[] = [];
|
||||
const api = {
|
||||
get: vi.fn(async (route: string) => {
|
||||
reads.push(route);
|
||||
if (route === "/api/health") return { deploymentMode: "authenticated" };
|
||||
if (route === "/api/auth/get-session") return { user: { id: "admin" } };
|
||||
if (route === "/api/admin/users/admin/company-access") return { user: { id: "admin", isInstanceAdmin: state.administrator }, companyAccess: [] };
|
||||
if (route === "/api/admin/users/person/company-access") return { user: { id: "person", isInstanceAdmin: state.targetAdministrator }, companyAccess: state.assigned ? [{ ...membership, membershipRole: state.readRole }] : state.preexisting ? [membership] : [] };
|
||||
if (route === "/api/companies/company/members") return { members: [{ ...membership, membershipRole: state.readRole, grants: [...grantsForHumanRole(role), ...(state.extraGrant ? [{ permissionKey: "users:manage_permissions", scope: null }] : [])].map(grant => ({ ...grant, companyId: state.foreignGrant ? "foreign" : "company", principalId: "person", principalType: "user", grantedByUserId: state.grantedBy })) }] };
|
||||
throw new Error("Unexpected fixture read");
|
||||
}),
|
||||
putSensitive: vi.fn(async (route: string, body: unknown) => { writes.push({ route, body }); state.assigned = true; if (state.slowPut) vi.setSystemTime(100000); return { user: { id: "person", isInstanceAdmin: state.targetAdministrator }, companyAccess: [membership] }; }),
|
||||
patchSensitive: vi.fn(async (route: string, body: unknown) => { writes.push({ route, body }); return membership; }),
|
||||
};
|
||||
const person = { id: "person", request: vi.fn(async (route: string) => {
|
||||
personalReads.push(route);
|
||||
if (route === "/api/auth/get-session") return new Response(JSON.stringify({ user: { id: state.personalId } }));
|
||||
if (route === "/api/admin/users") return new Response("closed", { status: state.adminStatus });
|
||||
if (route === "/api/companies/company") return new Response(JSON.stringify({ id: state.companyReadId }), { status: state.assigned ? 200 : state.beforeStatus });
|
||||
throw new Error("Unexpected person request");
|
||||
}) } as unknown as Parameters<typeof enrollPrerequisitePerson>[1];
|
||||
const evidence = vi.fn();
|
||||
const ctx = { scenario: { id: "inbox-search.show-decisions" }, observationDeadlineAt: 100000, input: { execution: { id: "chat-features.runner-opencode.local.inbox-search.show-decisions" }, api, fixtures: { company: { id: "company" } }, evidence } } as unknown as ChatCatalogContext;
|
||||
vi.useFakeTimers(); vi.setSystemTime(0);
|
||||
return { ctx, person, api, state, writes, reads, personalReads, evidence };
|
||||
}
|
||||
it.each(["operator", "viewer"] as const)("establishes only the real public %s prerequisite and proves the person's own scope", async role => {
|
||||
const f = fixture(role); const receipt = await enrollPrerequisitePerson(f.ctx, f.person, role);
|
||||
expect(f.writes).toEqual([{ route: "/api/admin/users/person/company-access", body: { companyIds: ["company"] } }, { route: "/api/companies/company/members/membership/role-and-grants", body: { membershipRole: role, status: "active", grants: grantsForHumanRole(role) } }]);
|
||||
expect(f.personalReads).toEqual(["/api/auth/get-session", "/api/companies/company", "/api/companies/company", "/api/admin/users"]);
|
||||
expect(receipt).toMatchObject({ userId: "person", companyId: "company", role, beforeStatus: 403, companyReadStatus: 200, instanceAdminReadStatus: 403 });
|
||||
expect(f.evidence).toHaveBeenCalledOnce(); expect([...f.reads, ...f.writes.map(row => row.route)]).not.toEqual(expect.arrayContaining([expect.stringContaining("invites")]));
|
||||
});
|
||||
it.each(["access.invite-teammate", "access.manage-membership", "activity.agent-feedback", "unknown"])("does not substitute public membership for unapproved/invitation journey %s", async scenarioId => {
|
||||
const f = fixture(); Object.assign(f.ctx.scenario, { id: scenarioId }); f.ctx.input.execution.id = `chat-features.runner-opencode.local.${scenarioId}`;
|
||||
await expect(enrollPrerequisitePerson(f.ctx, f.person)).rejects.toThrow("not admitted"); expect(f.api.get).not.toHaveBeenCalled(); expect(f.writes).toHaveLength(0);
|
||||
});
|
||||
it("requires the exact OpenCode local execution rather than authorizing by scenario name alone", async () => {
|
||||
const f = fixture(); f.ctx.input.execution.id = "chat-features.runner-codex.local.inbox-search.show-decisions";
|
||||
await expect(enrollPrerequisitePerson(f.ctx, f.person)).rejects.toThrow("not admitted"); expect(f.writes).toHaveLength(0);
|
||||
});
|
||||
it.each(["administrator", "target administrator", "preexisting", "wrong person", "initial access"])("refuses %s before altering access", async invalid => {
|
||||
const f = fixture();
|
||||
if (invalid === "administrator") f.state.administrator = false;
|
||||
if (invalid === "target administrator") f.state.targetAdministrator = true;
|
||||
if (invalid === "preexisting") f.state.preexisting = true;
|
||||
if (invalid === "wrong person") f.state.personalId = "other";
|
||||
if (invalid === "initial access") f.state.beforeStatus = 200;
|
||||
await expect(enrollPrerequisitePerson(f.ctx, f.person)).rejects.toThrow(); expect(f.writes).toHaveLength(0); expect(f.evidence).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each(["wrong role", "extra grant", "foreign grant", "wrong author", "instance authority", "wrong company"])("rejects persisted %s or unauthorized own-session proof", async invalid => {
|
||||
const f = fixture();
|
||||
if (invalid === "wrong role") f.state.readRole = "viewer";
|
||||
if (invalid === "extra grant") f.state.extraGrant = true;
|
||||
if (invalid === "foreign grant") f.state.foreignGrant = true;
|
||||
if (invalid === "wrong author") f.state.grantedBy = "other";
|
||||
if (invalid === "instance authority") f.state.adminStatus = 200;
|
||||
if (invalid === "wrong company") f.state.companyReadId = "other";
|
||||
await expect(enrollPrerequisitePerson(f.ctx, f.person)).rejects.toThrow(); expect(f.evidence).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each([0, -1, Number.NaN, undefined])("will not start setup without a finite positive original clock (%s)", async deadline => {
|
||||
const f = fixture(); Object.assign(f.ctx, { observationDeadlineAt: deadline });
|
||||
await expect(enrollPrerequisitePerson(f.ctx, f.person)).rejects.toThrow("budget"); expect(f.api.get).not.toHaveBeenCalled(); expect(f.writes).toHaveLength(0);
|
||||
});
|
||||
it("a slow public access write cannot reset the original clock or start role/grant mutation after expiry", async () => {
|
||||
const f = fixture(); f.state.slowPut = true;
|
||||
await expect(enrollPrerequisitePerson(f.ctx, f.person)).rejects.toThrow("budget"); expect(f.writes).toHaveLength(1); expect(f.api.patchSensitive).not.toHaveBeenCalled(); expect(f.evidence).not.toHaveBeenCalled();
|
||||
});
|
||||
it("the closed allowlist contains no access/invitation scenarios", () => {
|
||||
expect(prerequisiteMembershipScenarioIds).toHaveLength(20);
|
||||
expect(prerequisiteMembershipScenarioIds.some(id => id.startsWith("access."))).toBe(false);
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type { ChatCatalogCase, ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { checks } from "./internal-support.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
import { identityDefinition, identityObservation, observeIdentity, probeAgentPermission, waitForReply } from "./identity-support.js";
|
||||
|
||||
interface Session { user: { id: string; name: string; image?: string | null } }
|
||||
@@ -28,7 +28,7 @@ export const profileIdentityCases: readonly ChatCatalogCase[] = [identityDefinit
|
||||
const observer = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Riley" });
|
||||
observers.set(ctx, observer);
|
||||
try {
|
||||
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, observer, ctx.input.privateSecrets);
|
||||
await enrollPrerequisitePerson(ctx, observer);
|
||||
await updateOwnProfile(ctx, "Alex Former");
|
||||
await ctx.input.page.reload({ waitUntil: "domcontentloaded" });
|
||||
const company = await ctx.input.api.get<{ name: string }>(`/api/companies/${ctx.input.fixtures.company.id}`);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { pollUntil } from "../api.js";
|
||||
import { checks, project, entity, hasLink, putDocument } from "./internal-support.js";
|
||||
import { identityDefinition, identityObservation, observeIdentity, openStandingChat, readAgentResource, waitForReply, type Interaction } from "./identity-support.js";
|
||||
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
const audiencePeople = new WeakMap<ChatCatalogContext, Awaited<ReturnType<typeof createAuthenticatedPerson>>>();
|
||||
|
||||
async function cards(ctx: ChatCatalogContext) {
|
||||
@@ -144,7 +144,7 @@ export const questionIdentityCases = [
|
||||
const renamed = await ctx.input.page.context().request.patch(`${ctx.input.api.baseURL}/api/auth/profile`, { data: { name: "Alex" }, headers: { Origin: ctx.input.api.baseURL } });
|
||||
if (!renamed.ok()) throw new Error("Synthetic Alex profile setup failed");
|
||||
const riley = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets, name: "Riley" }); audiencePeople.set(ctx, riley);
|
||||
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, riley, secrets);
|
||||
await enrollPrerequisitePerson(ctx, riley);
|
||||
const source = await openStandingChat(ctx);
|
||||
const original = await ctx.input.api.post<Interaction>(`/api/issues/${source.id}/interactions`, createIssueThreadInteractionSchema.parse({
|
||||
kind: "request_confirmation", title: "Riley's website proposal", resolverPolicy: "human_only", addresseeUserId: riley.id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type { ChatCatalogCase, ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { checks, entity, project } from "./internal-support.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
import { identityDefinition, identityObservation, probeAgentPermission, waitForReply } from "./identity-support.js";
|
||||
type Person = Awaited<ReturnType<typeof createAuthenticatedPerson>>;
|
||||
const observers = new WeakMap<ChatCatalogContext, Person>();
|
||||
@@ -40,7 +40,7 @@ export const sidebarOrderIdentityCases: readonly ChatCatalogCase[] = [identityDe
|
||||
const endpoint = `/api/companies/${ctx.input.fixtures.company.id}/sidebar-preferences/me`;
|
||||
await ctx.input.api.putSensitive(endpoint, { orderedIds: [first.id, launch.id] });
|
||||
const observer = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Riley" }); observers.set(ctx, observer);
|
||||
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, observer, ctx.input.privateSecrets);
|
||||
await enrollPrerequisitePerson(ctx, observer);
|
||||
const saved = await observer.request(endpoint, "PUT", { orderedIds: [first.id, launch.id] }); if (!saved.ok) throw new Error("Riley's public sidebar fixture failed"); await saved.body?.cancel();
|
||||
const before = await observer.request(endpoint); if (!before.ok) throw new Error("Riley sidebar read failed");
|
||||
const observerBefore = (await before.json() as Order).orderedIds;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from "@playwright/test";
|
||||
import { createIssueThreadInteractionSchema } from "../../../packages/shared/src/validators/issue.js";
|
||||
import type { ChatCatalogCase } from "../chat-catalog-contract.js";
|
||||
import { makeDefinition } from "./internal-definition.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
import { mainPerson } from "./internal-privacy.js";
|
||||
import { checks, entity, hasLink, observation, observeInternal, project, sameTask, task, type Entity, type InternalObservation } from "./internal-support.js";
|
||||
interface Request extends Entity { addresseeUserId?: string; resolvedByUserId?: string; result?: unknown; payload?: unknown; decidedByUserId?: string | null }
|
||||
@@ -53,7 +53,7 @@ function showDecisions(): ChatCatalogCase {
|
||||
if (!browser || !ctx.input.privateSecrets) throw new Error("Decision audiences require an independent signed-in human and private credential redaction");
|
||||
riley = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Riley" });
|
||||
try {
|
||||
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, riley, ctx.input.privateSecrets, "operator");
|
||||
await enrollPrerequisitePerson(ctx, riley, "operator");
|
||||
const p = await project(ctx); const t = await task(ctx, "Riley launch question", p.id);
|
||||
const history = await task(ctx, "Completed launch information", p.id);
|
||||
const question = (title: string, addresseeUserId: string) => createIssueThreadInteractionSchema.parse({ kind: "ask_user_questions", title, resolverPolicy: "human_only", addresseeUserId, continuationPolicy: "none",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createIssueThreadInteractionSchema } from "../../../packages/shared/src
|
||||
import type { ChatCatalogCase, ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { pollUntil } from "../api.js";
|
||||
import { RunnerApiHttpError } from "../api.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
import { makeDefinition } from "./internal-definition.js";
|
||||
import { checks, entity, hasLink, observation, observeInternal, project, sameTask, task, type Entity, type InternalObservation } from "./internal-support.js";
|
||||
|
||||
@@ -97,7 +97,7 @@ function triageCase(scenarioId: string): ChatCatalogCase {
|
||||
if (!browser || !ctx.input.privateSecrets) throw new Error("Personal triage requires authenticated independent people and private credential redaction");
|
||||
other = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Riley" });
|
||||
try {
|
||||
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, other, ctx.input.privateSecrets, "operator");
|
||||
await enrollPrerequisitePerson(ctx, other, "operator");
|
||||
const session = await ctx.input.api.get<{ user?: { id: string } }>("/api/auth/get-session");
|
||||
if (!session.user?.id) throw new Error("Personal inbox target is not a real authenticated human");
|
||||
const p = await project(ctx); const originalTasks: Entity[] = [];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type { ChatCatalogCase, ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
import { makeDefinition, fixtureWorker } from "./internal-definition.js";
|
||||
import { project, task, entity, observation, checks, observeInternal, hasLink, sameTask, type Entity, type InternalObservation } from "./internal-support.js";
|
||||
import { pollUntil } from "../api.js";
|
||||
@@ -21,7 +21,7 @@ export async function observerPerson(ctx: ChatCatalogContext) {
|
||||
const browser = ctx.input.page.context().browser();
|
||||
if (!browser || !ctx.input.privateSecrets) throw new Error("Independent authenticated observer requires a browser and attempt-private redaction sink");
|
||||
const observer = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Riley" });
|
||||
try { await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, observer, ctx.input.privateSecrets, "viewer"); }
|
||||
try { await enrollPrerequisitePerson(ctx, observer, "viewer"); }
|
||||
catch (error) { await observer.dispose(); throw error; }
|
||||
return observer;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from "@playwright/test";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { ChatCatalogCase, ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { pollUntil } from "../api.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
import { checks, entity, observation, observeInternal, sameTask, hasLink, type Entity, type InternalObservation } from "./internal-support.js";
|
||||
interface Revision extends Entity { createdByUserId?: string | null }
|
||||
interface ConcurrentRestore extends InternalObservation { phaseReply: string }
|
||||
@@ -64,7 +64,7 @@ export function withRestoreConcurrency(primary: ChatCatalogCase): ChatCatalogCas
|
||||
sourceFiles: [...primary.sourceFiles, "chat-catalog-domains/internal-restore-concurrency.ts", "chat-catalog-domains/identity-auth.ts"],
|
||||
async setup(ctx) { const browser = ctx.input.page.context().browser(); if (!browser || !ctx.input.privateSecrets) throw new Error("Concurrent editor context requires actual authenticated people and private credential redaction");
|
||||
editor = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Casey" });
|
||||
try { await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, editor, ctx.input.privateSecrets, "operator"); return { ...await primary.setup(ctx), editorId: editor.id }; }
|
||||
try { await enrollPrerequisitePerson(ctx, editor, "operator"); return { ...await primary.setup(ctx), editorId: editor.id }; }
|
||||
catch (error) { await editor.dispose(); editor = undefined; throw error; } },
|
||||
grade: value => gradeConcurrentRestore(value, primary.grade),
|
||||
async act(ctx, state) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
||||
import { expect } from "@playwright/test";
|
||||
import type { ChatCatalogCase } from "../chat-catalog-contract.js";
|
||||
import { pollUntil } from "../api.js";
|
||||
import { createAuthenticatedPerson, enrollAuthenticatedPerson } from "./identity-auth.js";
|
||||
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
|
||||
import { fixtureWorker, makeDefinition } from "./internal-definition.js";
|
||||
import { checks, entity, observation, observeInternal, project, sameTask, task, type Entity, type InternalObservation } from "./internal-support.js";
|
||||
|
||||
@@ -51,7 +51,7 @@ function completionGate(): ChatCatalogCase {
|
||||
const browser = ctx.input.page.context().browser();
|
||||
if (!browser || !ctx.input.privateSecrets) throw new Error("Independent reviewer fixture requires a real authenticated browser and secret redactor");
|
||||
reviewer = await createAuthenticatedPerson({ api: ctx.input.api, browser, secrets: ctx.input.privateSecrets, name: "Riley" });
|
||||
await enrollAuthenticatedPerson(ctx.input.api, ctx.input.fixtures.company.id, reviewer, ctx.input.privateSecrets, "operator");
|
||||
await enrollPrerequisitePerson(ctx, reviewer, "operator");
|
||||
const worker = await fixtureWorker(ctx); const p = await project(ctx);
|
||||
const root = `/api/companies/${ctx.input.fixtures.company.id}`;
|
||||
for (const scope of [{ scopeType: "company", scopeId: ctx.input.fixtures.company.id }, { scopeType: "agent", scopeId: worker.id }])
|
||||
|
||||
Reference in new issue
Block a user