mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +01:00
## Thinking Path > - Paperclip lets people delegate work to agents and approve proposed work. > - The onboarding first task asks for scope, proposes a task, and waits for a human decision. > - An answer to its opening question supplies scope. It does not approve a proposal. > - Task creation already checks the saved proposal decision. > - Native completion used the general completion policy and could mark the onboarding parent Done without that decision. > - This PR checks the trusted decision before completion feedback and again when the native status is committed. > - The first task stays in review until a saved human decision permits closure. Ordinary completion and acknowledged refusal keep their current behavior. > - The Product E2E guard also retains closed scalar evidence so future failures show which boundary condition was observed. ## Linked Issues or Issue Description Refs: #17914. Profile compatibility follow-up to #17922. **What happened?** A native onboarding run can submit a valid low-risk Done result after only the opening answer. The native finalizer then marks the first task Done even though no human has accepted or rejected a proposal. **Expected behavior** The opening answer must not permit completion. A trusted saved proposal decision must permit it. A saved rejection permits acknowledgment and closure, but does not authorize new work. A later pending proposal must keep the task in review. **Steps to reproduce** Create a server-owned onboarding first task with an answered opening question, no proposal decision, and a valid low-risk completion contract. Pass a Done result through native completion feedback, the control-plane port, and the finalizer. Before this fix, the persisted status is Done. The regression expects In review. Ordinary completion and acknowledged rejection pass as controls. ## What Changed - Share the trusted latest-proposal and human-response checks with completion admission. Keep task creation acceptance-only. - Refuse premature native completion feedback. Keep an already saved premature result in review during finalization. - Check the decision again under the existing task and run locks before committing Done. - Cover accepted decisions, saved refusals, forged context, pending or changed proposals, and a real database race. - Retain a closed scalar receipt when the existing E2E pre-acceptance guard fails. Validate and display it without task content, IDs, filenames, or raw rows. Preserve the original failure, blocked dependents, and historical reports. - Include the evidence module in the journey definition hash. Bind the receipt to its exact failure code and check its closed keys against the full type. - Repair base-branch profile drift found by the full harness check: include the active Copilot v66 fixture in image hashing and update stale Pi version/hash expectations from #17922. Keep all older image inputs and strict hash checks. - Update the onboarding feature and agent access maps and E2E evidence documentation. ## Verification - Semantic regression before implementation: one failure and two passing controls on PostgreSQL 18. The failed assertion observed Done instead of In review. - The local database uses an owned disposable container with an immutable image digest. Actual migrations, feedback, control-plane port, finalizer, and status commit run against PostgreSQL. A temporary test adapter replaces only the embedded database transport because this host has exhausted its PostgreSQL semaphores. - Native control-plane suite: 55 tests passed, including 17 new controls. Existing task-creation admission: 25 passed. Existing completion feedback: 19 passed. - Credential-free E2E support: 206 Vitest suites passed, 4,407 tests passed, and two skipped. All 137 Node support checks passed. Harness typecheck passed. - The failure-receipt regression had 11 failed assertions before capture was implemented. Two failure-code binding assertions fail without their gate. Eight placement/binding cases first validate the receipt-free control, then reject the misplaced receipt. - The untouched base reproduced the three profile compatibility failures. The narrow repair passed all 83 affected tests before the full harness run. - Private workflow preparation and credential-free onboarding preview: nine compatibility checks passed. Ownership, recovery, evidence, shared-contract and workflow compatibility: 77 passed. The actual provider preview also confirms immediate success deletion, earlier declared failure eligibility, limits, and zero retries without credential access. Map checks and git diff checks passed. - Required repository typecheck passed for the product fix. The combined branch build passed. The local full repository suite was stopped after repeated PostgreSQL startup failures; a separate initdb probe confirmed host SysV semaphore exhaustion. No shared IPC resources were changed. [Hosted CI](https://github.com/paperclipai/paperclip/actions/runs/40591020873) passed the full database suites, typecheck, build, runner checks, serialized suites, and browser shards on final head `1ac4c23605a03bfd1f1f8a017b14ce29fda85f6b` (55 success, two expected skips, one advisory neutral). - Independent source review approved the product guard, atomic race regression, evidence contract, and maps with no actionable findings. Both automated reviews are 5/5 on final head `1ac4c23605a03bfd1f1f8a017b14ce29fda85f6b`; prior findings are fixed and resolved. Final CI is green and the PR is conflict-free. - No Cloud workspace, provider call, release publication, or deployment is part of this verification. These tests do not identify a historical Cloud failure or qualify live onboarding. - Local dependency installation uses the existing CI-generated lockfile from #17811. No lockfile change is included. ## Risks - Existing onboarding runs without a trusted saved proposal decision remain in review even when the agent reports success. This is the intended approval boundary. - Decision changes can race completion. The status commit checks the decision under its existing locks and uses the existing status-race recovery path. - A saved refusal permits closure only. Creation admission still rejects it. - Evidence readers reject malformed receipts and unrelated failure codes. The version-1 key registry is checked against every receipt field at compile time. Existing attempts do not gain evidence after the fact. ## Model Used OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), ultra reasoning, through Codex. Used repository inspection, code execution, and collaborating coding agents. The runtime did not report a context-window size. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes:` / `Closes:` / `Refs:` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub references) - [x] My branch name describes the change and contains no internal ticket or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
93 lines
6.2 KiB
TypeScript
93 lines
6.2 KiB
TypeScript
import { expect } from "@playwright/test";
|
|
import { createConnectionProof } from "../connection-evidence.js";
|
|
import { onboardingConnect } from "./onboarding-connect.js";
|
|
import { onboardingFirstTask } from "./onboarding-first-task.js";
|
|
import { JourneyError, type JourneyContext, type JourneyEvidence, type TargetSession } from "./types.js";
|
|
|
|
type Row = Record<string, any>;
|
|
export interface CloudOnboardingInput {
|
|
page: JourneyContext["page"];
|
|
campaignId: string;
|
|
deadlineAt: number;
|
|
gateway: { model: string; baseUrl: string; apiKey: string };
|
|
maxRuns: number;
|
|
evidence: JourneyEvidence;
|
|
own: TargetSession["own"];
|
|
beforeWorkspace(name: string): Promise<void>;
|
|
workspaceCreated(): Promise<JourneyContext>;
|
|
/** The saved route must pass a deny-only runtime probe before Connect can spend. */
|
|
qualify(context: JourneyContext, connectionId: string): Promise<void>;
|
|
verifyExecution(context: JourneyContext, runIds: string[]): Promise<void>;
|
|
phase(phase: "setup" | "agent"): Promise<void>;
|
|
observeRun(run: Row): Promise<void>;
|
|
}
|
|
/** Measured actions use normal Cloud and tenant controls; infrastructure stays in the provider. */
|
|
export async function cloudOnboarding(input: CloudOnboardingInput): Promise<JourneyContext> {
|
|
const { page, evidence, campaignId } = input;
|
|
const name = `Journey QA ${campaignId}`;
|
|
const firstRunName = page.getByLabel("Name", { exact: true });
|
|
const organizationName = page.getByLabel("Organization name", { exact: true });
|
|
const createLink = page.getByRole("link", { name: /^(Create (?:a new )?organization|\+ New organization)$/ }).first();
|
|
// The first organization uses the signup Name/Next form. Later organizations
|
|
// use Organization name/Create organization, reached from the managed portfolio.
|
|
await expect(firstRunName.or(organizationName).or(createLink).first()).toBeVisible();
|
|
if (!await firstRunName.isVisible() && !await organizationName.isVisible()) {
|
|
await createLink.click();
|
|
await expect(organizationName).toBeVisible();
|
|
}
|
|
const firstRun = await firstRunName.isVisible();
|
|
await (firstRun ? firstRunName : organizationName).fill(name);
|
|
await input.beforeWorkspace(name);
|
|
await page.getByRole("button", { name: firstRun ? "Next" : "Create organization", exact: true }).click();
|
|
const context = await input.workspaceCreated();
|
|
const { api, company } = context;
|
|
evidence.deployment = context.identity;
|
|
evidence.checks.push({ id: "fresh-workspace", passed: true });
|
|
await input.phase("setup");
|
|
await expect(page.getByRole("heading", { name: "Create your first agent", exact: true })).toBeVisible({ timeout: 180_000 });
|
|
await input.own({ kind: "agent", name });
|
|
await page.getByLabel("Agent name", { exact: true }).fill(name);
|
|
await page.getByRole("button", { name: "Next", exact: true }).click();
|
|
await page.getByRole("button", { name: "Use API key instead", exact: true }).click();
|
|
await page.getByRole("radio", { name: /OpenAI.*API/ }).click();
|
|
await page.getByRole("button", { name: "Use a custom gateway", exact: true }).click();
|
|
const dialog = page.getByRole("dialog", { name: "Custom gateway connection", exact: true });
|
|
await dialog.getByLabel("Provider URL", { exact: true }).fill(input.gateway.baseUrl);
|
|
await dialog.getByLabel("API key", { exact: true }).fill(input.gateway.apiKey);
|
|
await dialog.getByRole("button", { name: "Change", exact: true }).click();
|
|
await dialog.getByLabel("Model IDs", { exact: true }).fill(input.gateway.model);
|
|
const connectionName = `My ${new URL(input.gateway.baseUrl).hostname}`;
|
|
await input.own({ kind: "connection", name: connectionName });
|
|
await dialog.getByRole("button", { name: "Save connection", exact: true }).click();
|
|
await expect(dialog).not.toBeVisible();
|
|
const connections = (await api.get(`/api/companies/${company.id}/ai-connections`)).connections as Row[];
|
|
const connection = connections.find(c => c.name === connectionName && c.provider === "openai" && c.method === "api_key" && c.status === "connected");
|
|
if (!connection || connections.length !== 1) throw new JourneyError("failed", "onboarding_connection_not_saved");
|
|
await input.own({ kind: "connection", name: connectionName, id: connection.id });
|
|
await page.getByRole("combobox", { name: "Model", exact: true }).selectOption(input.gateway.model);
|
|
await page.locator("summary").filter({ hasText: "Advanced" }).click();
|
|
await page.getByRole("button", { name: "Runner", exact: true }).click();
|
|
await page.getByRole("option", { name: /^Paperclip Runner/ }).click();
|
|
await input.qualify(context, connection.id);
|
|
// Connect performs the production probe and agent creation. Never press Retry.
|
|
const connect = await onboardingConnect({ page, origin: api.origin, companyId: company.id, deadlineAt: input.deadlineAt,
|
|
retain: value => { evidence.onboardingConnect = value; } });
|
|
const agents = await api.get<Row[]>(`/api/companies/${company.id}/agents`);
|
|
const agent = agents.find(a => a.name === name);
|
|
if (!agent || agents.length !== 1 || agent.adapterType !== "paperclip_runner" || agent.adapterConfig?.provider !== "codex" || agent.adapterConfig?.model !== input.gateway.model) throw new JourneyError("failed", "onboarding_agent_configuration_mismatch");
|
|
connect.checkpoint = "agent-verification";
|
|
await input.own({ kind: "agent", name, id: agent.id });
|
|
evidence.checks.push({ id: "ui-connection-and-native-agent", passed: true });
|
|
await input.own({ kind: "issue", name: "Paperclip onboarding" });
|
|
await page.getByRole("button", { name: "Get started", exact: true }).click();
|
|
await input.phase("agent");
|
|
const completed = await onboardingFirstTask({ context, agent: { id: agent.id }, connectionId: connection.id,
|
|
proof: createConnectionProof(), campaignId, maxRuns: input.maxRuns, deadlineAt: input.deadlineAt,
|
|
own: input.own, observeRun: input.observeRun, onArtifact: artifact => { evidence.artifact = artifact; },
|
|
onPreAcceptanceFailure: receipt => { evidence.onboardingPreAcceptance = receipt; } });
|
|
await input.verifyExecution(context, [completed.artifact.originatingRunId]);
|
|
evidence.checks.push({ id: "first-task-complete", passed: true },
|
|
{ id: "downloadable-artifact-verified", passed: true }, { id: "native-trace-verified", passed: true });
|
|
return { ...context, startingState: "onboarding" };
|
|
}
|