Skip to content

feat(server): receive and apply the Paperclip Cloud onboarding seed - #11098

Open
tonio-alucema wants to merge 2 commits into
masterfrom
feat/onboarding-seed-receiver
Open

feat(server): receive and apply the Paperclip Cloud onboarding seed#11098
tonio-alucema wants to merge 2 commits into
masterfrom
feat/onboarding-seed-receiver

Conversation

@tonio-alucema

Copy link
Copy Markdown
Contributor

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work.
  • Paperclip Cloud provisions a dedicated tenant stack for each customer. During signup it asks for a mission, a name and role for the first agent, and a first task.
  • Cloud pushes those answers into the new stack at activation, as POST /api/companies/:companyId/onboarding-seed.
  • No route served that path. The tenant answered 404, so Cloud recorded the push as unacknowledged and retried on every portfolio fetch.
  • The failure was soft. The answers stayed durable in Cloud and the stack still activated. But the stack opened on the empty first-run wizard, and it asked the customer again for what they had already given.
  • This pull request adds the receiving endpoint. It validates the seed, applies it, and acknowledges it.
  • The benefit is that a seeded stack opens with the mission, the agent and the first task already in place.

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) and packages/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/shared and read from the JSON body only. It is never read from an x-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:

  • The mission becomes the company-level goal. A multi-line mission splits into a title and a description, as the wizard does.
  • The agent becomes the company's first hire. Its free-text role ("Chief of Staff") lands on title. The structural role stays ceo, which is what the org chart and the default-instructions lookup read.
  • The first task becomes an issue in the Onboarding project, assigned to that agent.

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 new company_onboarding_seeds table 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/agents and POST /companies/:companyId/issues over 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

  • Add POST /api/companies/:companyId/onboarding-seed in server/src/routes/onboarding-seed.ts. It authenticates exactly as POST /api/companies/:companyId/logo does, through assertCompanyAccess.
  • Add server/src/services/onboarding-seed.ts. It applies the mission, the agent and the first task, and records the applied revision last.
  • Add the company_onboarding_seeds table: schema, migration 0212, and journal entry. It holds the applied revision and the ids of the goal, agent and issue the seed produced.
  • Add applyOnboardingSeedSchema in packages/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.
  • Mount the router in server/src/app.ts and register the path in the OpenAPI document.
  • Add server/src/__tests__/onboarding-seed-route.test.ts with 11 tests.
  • The seeded agent is created on 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_TYPE overrides it.

Verification

pnpm typecheck                      # whole workspace, passes
npx vitest run \
  server/src/__tests__/onboarding-seed-route.test.ts \
  server/src/__tests__/openapi-routes.test.ts        # 14 passed

The suite runs against embedded Postgres with migrations applied, so migration 0212 is exercised by every test.

The 11 route tests cover:

  • the happy path — mission, agent and task all applied, read immediately after the 200
  • replay of the same revision — no second agent, no second task, no second goal, no second project
  • a later revision — the goal, agent and task are updated in place
  • a multi-line mission splitting into a goal title and description
  • a revision-only seed
  • the activity log entry written once, and not again on a replay
  • a caller without access to the company — 403, and nothing written
  • a body with no revision — 400
  • each field bound past its limit — 400
  • a mission planted on an x-paperclip-cloud-* header — ignored, body wins
  • an existing Onboarding project — reused, not duplicated

Not 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 0212 creates 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 ceo agent, 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_local with 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

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have not referenced internal/instance-local Paperclip issues or links (only public GitHub #NNN / github.com/paperclipai/paperclip URLs)
  • My branch name describes the change (e.g. docs/..., fix/...) and contains no internal Paperclip ticket id or instance-derived details
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

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>
@tonio-alucema

Copy link
Copy Markdown
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>
@tonio-alucema

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant