mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 22:24:16 +01:00
feat(server): receive and apply the Paperclip Cloud onboarding seed
Paperclip Cloud collects a mission, a first agent and a first task during signup and pushes them into the tenant stack at activation, as `POST /api/companies/:companyId/onboarding-seed`. Nothing served that route, so the tenant answered 404, Cloud recorded the push as unacknowledged, and a freshly activated stack opened on the empty first-run wizard with the answers the customer had already given stranded on the Cloud side. This adds the receiver. Contract. The body carries a content-addressed `revision` plus optional `mission`, `agent` (`name`, `role`) and `firstTask` (`title`, `details`). It is customer free text, so it is validated and bounded in `packages/shared` and never read from a header — the `x-paperclip-cloud-*` set is the trusted identity envelope, derived server-side from host plus verified domain records, and mixing user content into it would undermine exactly that property. A test plants a mission on a cloud header and asserts the body wins. Application. The mission becomes the company-level goal, the agent becomes the company's first hire (its free-text role lands on `title`; the structural `role` stays `ceo`, which is what the org chart and the default-instructions lookup read), and the first task becomes an issue in the Onboarding project assigned to that agent — the same shapes the first-run wizard produces, so a seeded stack and a manually onboarded one are indistinguishable afterwards. The seeded agent is created on `claude_local`, mirroring the teams-catalog default for agents created server-side without a human running an environment test first, and overridable via `PAPERCLIP_ONBOARDING_SEED_ADAPTER_TYPE`. Idempotency. Cloud retries until it gets a 2xx and treats any 2xx as "the tenant holds this content", so the new `company_onboarding_seeds` table records the applied revision along with the goal, agent and issue it produced. Replaying a revision that already matches is a successful no-op; a later revision — the customer edited their answers — updates those three rows in place rather than creating a second agent and a second task. The record is written last, after every other write has landed, so a partial application cannot present itself as acknowledged. Ordering, not eventual consistency. Everything is applied before the 200 is sent. The tests read the database immediately after the response with no waiting and no polling, so a lazy receiver fails them on a fast machine as well as a slow one. That matters because the redirect into the tenant dashboard is gated on this acknowledgement. Tested with `pnpm typecheck` (whole workspace) and `vitest run server/src/__tests__/onboarding-seed-route.test.ts server/src/__tests__/openapi-routes.test.ts` — 14 passing, including the happy path, replay, revision update, project reuse, the header-injection guard, the field bounds, and the 403 for a caller without access to the company. The suite runs against embedded Postgres, so the new migration is exercised too. Co-authored-by: Paperclip <noreply@paperclip.ing> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
276730d63e
commit
d6745ac8bd
14 files changed
+800
No files matched your search
@@ -590,6 +590,11 @@ export {
|
||||
type UpdateGoal,
|
||||
} from "./goal.js";
|
||||
|
||||
export {
|
||||
applyOnboardingSeedSchema,
|
||||
type ApplyOnboardingSeed,
|
||||
} from "./onboarding-seed.js";
|
||||
|
||||
export {
|
||||
createApprovalSchema,
|
||||
resolveApprovalSchema,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* The onboarding seed Paperclip Cloud pushes into a stack at activation
|
||||
* Every field except `revision` is customer free text collected in
|
||||
* the Cloud signup wizard, so it is untrusted input and is bounded here to the
|
||||
* same limits Cloud enforces before sending.
|
||||
*
|
||||
* The seed rides the JSON body only. The `x-paperclip-cloud-*` headers are the
|
||||
* trusted identity envelope — every member is derived server-side from host +
|
||||
* verified domain records — and must never be read for seed content.
|
||||
*/
|
||||
export const MISSION_MAX_LENGTH = 2000;
|
||||
export const AGENT_NAME_MAX_LENGTH = 80;
|
||||
export const AGENT_ROLE_MAX_LENGTH = 120;
|
||||
export const FIRST_TASK_TITLE_MAX_LENGTH = 200;
|
||||
export const FIRST_TASK_DETAILS_MAX_LENGTH = 2000;
|
||||
|
||||
export const applyOnboardingSeedSchema = z.object({
|
||||
revision: z.string().min(1).max(128),
|
||||
mission: z.string().max(MISSION_MAX_LENGTH).optional(),
|
||||
agent: z
|
||||
.object({
|
||||
name: z.string().min(1).max(AGENT_NAME_MAX_LENGTH),
|
||||
role: z.string().max(AGENT_ROLE_MAX_LENGTH).optional(),
|
||||
})
|
||||
.optional(),
|
||||
firstTask: z
|
||||
.object({
|
||||
title: z.string().min(1).max(FIRST_TASK_TITLE_MAX_LENGTH),
|
||||
details: z.string().max(FIRST_TASK_DETAILS_MAX_LENGTH).optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type ApplyOnboardingSeed = z.infer<typeof applyOnboardingSeedSchema>;
|
||||
Reference in new issue
Block a user