feat(server): receive and apply the Paperclip Cloud onboarding seed - #11098
Open
tonio-alucema wants to merge 2 commits into
Open
feat(server): receive and apply the Paperclip Cloud onboarding seed#11098tonio-alucema wants to merge 2 commits into
tonio-alucema wants to merge 2 commits into
Conversation
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>
Contributor
Author
|
@greptile-apps review |
…ision-scoped Cloud's reconcile runs off portfolio fetches, which can overlap, so two pushes of the same revision can race. The idempotency key on the first task is what makes that safe. It is deliberately company-scoped rather than revision-scoped, so that a later revision still dedupes against whatever the first push created if the recorded issue id is lost. That reasoning was not obvious from the call site. Co-authored-by: Paperclip <noreply@paperclip.ing> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
@greptile-apps review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
Linked Issues or Issue Description
No public GitHub issue covers this. The problem is described in-PR, following the feature template.
Subsystem affected
server/ — Express REST API and orchestration services. Also
packages/db(one new table) andpackages/shared(one new validator).Problem or motivation
Paperclip Cloud collects onboarding answers at signup and pushes them to the tenant stack at activation. The tenant had no route for that request. It answered 404. Cloud treats a non-2xx as "not yet applied", so it kept the answers and retried, but the stack itself stayed unseeded. A customer who had already named their mission, their first agent and their first task arrived at an empty first-run wizard that asked for all three again.
Proposed solution
Serve
POST /api/companies/:companyId/onboarding-seed. Validate the body, apply it to the company, then acknowledge it.The seed is customer free text, so it is bounded and validated in
packages/sharedand read from the JSON body only. It is never read from anx-paperclip-cloud-*header. That header set is the trusted identity envelope: every member is derived server-side from the host plus verified domain records, and that is exactly what makes it trustworthy. Mixing user content into it would remove the property. A test plants a mission on a cloud header and asserts that the body value wins.Application reuses the shapes the first-run wizard already produces, so a seeded stack and a manually onboarded one look the same afterwards:
title. The structuralrolestaysceo, which is what the org chart and the default-instructions lookup read.Cloud retries until it gets a 2xx, and it reads any 2xx as "the tenant holds this content". So the endpoint is idempotent per
revision. A newcompany_onboarding_seedstable records the applied revision together with the goal, the agent and the issue it produced. A replay of a revision that already matches is a successful no-op. A later revision — the customer edited their answers — updates those three rows in place instead of 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.Everything is applied before the 200 is sent. This is an ordering guarantee, not eventual consistency. 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.
Alternatives considered
Store the seed and let the tenant UI apply it on first load. Rejected: the dashboard redirect is gated on the acknowledgement, so a background apply would let the dashboard open before the agent and the task exist. The whole point is that it must not.
Reuse
POST /companies/:companyId/agentsandPOST /companies/:companyId/issuesover HTTP from Cloud. Rejected: it needs three round trips with no shared idempotency key, and it moves the "did all of it land?" decision to the caller.Roadmap alignment
This completes an existing Cloud-to-tenant contract. It does not add a new user-facing surface.
What Changed
POST /api/companies/:companyId/onboarding-seedinserver/src/routes/onboarding-seed.ts. It authenticates exactly asPOST /api/companies/:companyId/logodoes, throughassertCompanyAccess.server/src/services/onboarding-seed.ts. It applies the mission, the agent and the first task, and records the applied revision last.company_onboarding_seedstable: schema, migration0212, and journal entry. It holds the applied revision and the ids of the goal, agent and issue the seed produced.applyOnboardingSeedSchemainpackages/shared. It bounds mission to 2000, agent name to 80, agent role to 120, task title to 200, and task details to 2000 — the same limits Cloud enforces before it sends.server/src/app.tsand register the path in the OpenAPI document.server/src/__tests__/onboarding-seed-route.test.tswith 11 tests.claude_local. This mirrors the teams-catalog default for agents created server-side, where no human runs an environment test first.PAPERCLIP_ONBOARDING_SEED_ADAPTER_TYPEoverrides it.Verification
The suite runs against embedded Postgres with migrations applied, so migration
0212is exercised by every test.The 11 route tests cover:
x-paperclip-cloud-*header — ignored, body winsNot verified here: the full Cloud-to-tenant walk against a live stack. That needs a deployed Cloud and a provisioned tenant together, which is separate staging work.
Risks
Migration
0212creates one new table. It adds no column to an existing table, rewrites nothing, and backfills nothing, so it is safe to apply online. The migration safety check passes.The endpoint writes to a company. Access is enforced by
assertCompanyAccess, the same gate the company logo write uses, and a test covers the denial.Behavioral note for stacks that already hold data. If a company already has a non-built-in
ceoagent, a first seed updates that agent's name and title rather than creating a second lead. Likewise a seed adopts an existing company-level goal rather than adding a parallel one. This is deliberate: the seed is the customer's own stated answer from signup, and two competing missions or two leads would be worse than one updated in place. In the intended case — a stack that Cloud has just activated — none of these exist yet.The seeded agent is created on
claude_localwith an empty adapter config. It is idle and needs the usual credential setup before it runs. Seeding it does not start it.Model Used
Claude Opus 5 (
claude-opus-5), 1M context window, extended thinking, with tool use and code execution. Used for the codebase investigation, the implementation, and the tests.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template#NNN/github.com/paperclipai/paperclipURLs)docs/...,fix/...) and contains no internal Paperclip ticket id or instance-derived details