docs: organize plans into doc/plans with date prefixes

Move plans from doc/plan/ into doc/plans/ and add YYYY-MM-DD date
prefixes to all undated plan files based on document headers or
earliest git commit dates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dotta
2026-03-13 08:43:44 -05:00
parent dfe40ffcca
commit 9c7d9ded1e
13 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,643 @@
# Humans and Permissions Implementation (V1)
Status: Draft
Date: 2026-02-21
Owners: Server + UI + CLI + DB + Shared
Companion plan: `doc/plan/humans-and-permissions.md`
## 1. Document role
This document is the engineering implementation contract for the humans-and-permissions plan.
It translates product decisions into concrete schema, API, middleware, UI, CLI, and test work.
If this document conflicts with prior exploratory notes, this document wins for V1 execution.
## 2. Locked V1 decisions
1. Two deployment modes remain:
- `local_trusted`
- `cloud_hosted`
2. `local_trusted`:
- no login UX
- implicit local instance admin actor
- loopback-only server binding
- full admin/settings/invite/approval capabilities available locally
3. `cloud_hosted`:
- Better Auth for humans
- email/password only
- no email verification requirement in V1
4. Permissions:
- one shared authorization system for humans and agents
- normalized grants table (`principal_permission_grants`)
- no separate “agent permissions engine”
5. Invites:
- copy-link only (no outbound email sending in V1)
- unified `company_join` link that supports human or agent path
- acceptance creates `pending_approval` join request
- no access until admin approval
6. Join review metadata:
- source IP required
- no GeoIP/country lookup in V1
7. Agent API keys:
- indefinite by default
- hash at rest
- display once on claim
- revoke/regenerate supported
8. Local ingress:
- public/untrusted ingress is out of scope for V1
- no `--dangerous-agent-ingress` in V1
## 3. Current baseline and delta
Current baseline (repo as of this doc):
- server actor model defaults to `board` in `server/src/middleware/auth.ts`
- authorization is mostly `assertBoard` + company check in `server/src/routes/authz.ts`
- no human auth/session tables in local schema
- no principal membership or grants tables
- no invite or join-request lifecycle
Required delta:
- move from board-vs-agent authz to principal-based authz
- add Better Auth integration in cloud mode
- add membership/grants/invite/join-request persistence
- add approval inbox signals and actions
- preserve local no-login UX without weakening cloud security
## 4. Architecture
## 4.1 Deployment mode contract
Add explicit runtime mode:
- `deployment.mode = local_trusted | cloud_hosted`
Config behavior:
- mode stored in config file (`packages/shared/src/config-schema.ts`)
- loaded in server config (`server/src/config.ts`)
- surfaced in `/api/health`
Startup guardrails:
- `local_trusted`: fail startup if bind host is not loopback
- `cloud_hosted`: fail startup if Better Auth is not configured
## 4.2 Actor model
Replace implicit “board” semantics with explicit actors:
- `user` (session-authenticated human)
- `agent` (bearer API key)
- `local_implicit_admin` (local_trusted only)
Implementation note:
- keep `req.actor` shape backward-compatible during migration by introducing a normalizer helper
- remove hard-coded `"board"` checks route-by-route after new authz helpers are in place
## 4.3 Authorization model
Authorization input tuple:
- `(company_id, principal_type, principal_id, permission_key, scope_payload)`
Principal types:
- `user`
- `agent`
Role layers:
- `instance_admin` (instance-wide)
- company-scoped grants via `principal_permission_grants`
Evaluation order:
1. resolve principal from actor
2. resolve instance role (`instance_admin` short-circuit for admin-only actions)
3. resolve company membership (`active` required for company access)
4. resolve grant + scope for requested action
## 5. Data model
## 5.1 Better Auth tables
Managed by Better Auth adapter/migrations (expected minimum):
- `user`
- `session`
- `account`
- `verification`
Note:
- use Better Auth canonical table names/types to avoid custom forks
## 5.2 New Paperclip tables
1. `instance_user_roles`
- `id` uuid pk
- `user_id` text not null
- `role` text not null (`instance_admin`)
- `created_at`, `updated_at`
- unique index: `(user_id, role)`
2. `company_memberships`
- `id` uuid pk
- `company_id` uuid fk `companies.id` not null
- `principal_type` text not null (`user | agent`)
- `principal_id` text not null
- `status` text not null (`pending | active | suspended`)
- `membership_role` text null
- `created_at`, `updated_at`
- unique index: `(company_id, principal_type, principal_id)`
- index: `(principal_type, principal_id, status)`
3. `principal_permission_grants`
- `id` uuid pk
- `company_id` uuid fk `companies.id` not null
- `principal_type` text not null (`user | agent`)
- `principal_id` text not null
- `permission_key` text not null
- `scope` jsonb null
- `granted_by_user_id` text null
- `created_at`, `updated_at`
- unique index: `(company_id, principal_type, principal_id, permission_key)`
- index: `(company_id, permission_key)`
4. `invites`
- `id` uuid pk
- `company_id` uuid fk `companies.id` not null
- `invite_type` text not null (`company_join | bootstrap_ceo`)
- `token_hash` text not null
- `allowed_join_types` text not null (`human | agent | both`) for `company_join`
- `defaults_payload` jsonb null
- `expires_at` timestamptz not null
- `invited_by_user_id` text null
- `revoked_at` timestamptz null
- `accepted_at` timestamptz null
- `created_at` timestamptz not null default now()
- unique index: `(token_hash)`
- index: `(company_id, invite_type, revoked_at, expires_at)`
5. `join_requests`
- `id` uuid pk
- `invite_id` uuid fk `invites.id` not null
- `company_id` uuid fk `companies.id` not null
- `request_type` text not null (`human | agent`)
- `status` text not null (`pending_approval | approved | rejected`)
- `request_ip` text not null
- `requesting_user_id` text null
- `request_email_snapshot` text null
- `agent_name` text null
- `adapter_type` text null
- `capabilities` text null
- `agent_defaults_payload` jsonb null
- `created_agent_id` uuid fk `agents.id` null
- `approved_by_user_id` text null
- `approved_at` timestamptz null
- `rejected_by_user_id` text null
- `rejected_at` timestamptz null
- `created_at`, `updated_at`
- index: `(company_id, status, request_type, created_at desc)`
- unique index: `(invite_id)` to enforce one request per consumed invite
## 5.3 Existing table changes
1. `issues`
- add `assignee_user_id` text null
- enforce single-assignee invariant:
- at most one of `assignee_agent_id` and `assignee_user_id` is non-null
2. `agents`
- keep existing `permissions` JSON for transition only
- mark as deprecated in code path once principal grants are live
## 5.4 Migration strategy
Migration ordering:
1. add new tables/columns/indexes
2. backfill minimum memberships/grants for existing data:
- create local implicit admin membership context in local mode at runtime (not persisted as Better Auth user)
- for cloud, bootstrap creates first admin user role on acceptance
3. switch authz reads to new tables
4. remove legacy board-only checks
## 6. API contract (new/changed)
All under `/api`.
## 6.1 Health
`GET /api/health` response additions:
- `deploymentMode`
- `authReady`
- `bootstrapStatus` (`ready | bootstrap_pending`)
## 6.2 Invites
1. `POST /api/companies/:companyId/invites`
- create `company_join` invite
- copy-link value returned once
2. `GET /api/invites/:token`
- validate token
- return invite landing payload
- includes `allowedJoinTypes`
3. `POST /api/invites/:token/accept`
- body:
- `requestType: human | agent`
- human path: no extra payload beyond authenticated user
- agent path: `agentName`, `adapterType`, `capabilities`, optional adapter defaults
- consumes invite token
- creates `join_requests(status=pending_approval)`
4. `POST /api/invites/:inviteId/revoke`
- revokes non-consumed invite
## 6.3 Join requests
1. `GET /api/companies/:companyId/join-requests?status=pending_approval&requestType=...`
2. `POST /api/companies/:companyId/join-requests/:requestId/approve`
- human:
- create/activate `company_memberships`
- apply default grants
- agent:
- create `agents` row
- create pending claim context for API key
- create/activate agent membership
- apply default grants
3. `POST /api/companies/:companyId/join-requests/:requestId/reject`
4. `POST /api/join-requests/:requestId/claim-api-key`
- approved agent request only
- returns plaintext key once
- stores hash in `agent_api_keys`
## 6.4 Membership and grants
1. `GET /api/companies/:companyId/members`
- returns both principal types
2. `PATCH /api/companies/:companyId/members/:memberId/permissions`
- upsert/remove grants
3. `PUT /api/admin/users/:userId/company-access`
- instance admin only
4. `GET /api/admin/users/:userId/company-access`
5. `POST /api/admin/users/:userId/promote-instance-admin`
6. `POST /api/admin/users/:userId/demote-instance-admin`
## 6.5 Inbox
`GET /api/companies/:companyId/inbox` additions:
- pending join request alert items when actor can `joins:approve`
- each item includes inline action metadata:
- join request id
- request type
- source IP
- human email snapshot when applicable
## 7. Server implementation details
## 7.1 Config and startup
Files:
- `packages/shared/src/config-schema.ts`
- `server/src/config.ts`
- `server/src/index.ts`
- `server/src/startup-banner.ts`
Changes:
- add deployment mode + bind host settings
- enforce loopback-only for `local_trusted`
- enforce Better Auth readiness in `cloud_hosted`
- banner shows mode and bootstrap status
## 7.2 Better Auth integration
Files:
- `server/package.json` (dependency)
- `server/src/auth/*` (new)
- `server/src/app.ts` (mount auth handler endpoints + session middleware)
Changes:
- add Better Auth server instance
- cookie/session handling for cloud mode
- no-op session auth in local mode
## 7.3 Actor middleware
Files:
- `server/src/middleware/auth.ts`
- `server/src/routes/authz.ts`
- `server/src/middleware/board-mutation-guard.ts`
Changes:
- stop defaulting every request to board in cloud mode
- map local requests to `local_implicit_admin` actor in local mode
- map Better Auth session to `user` actor in cloud mode
- preserve agent bearer path
- replace `assertBoard` with permission-oriented helpers:
- `requireInstanceAdmin(req)`
- `requireCompanyAccess(req, companyId)`
- `requireCompanyPermission(req, companyId, permissionKey, scope?)`
## 7.4 Authorization services
Files:
- `server/src/services` (new modules)
- `memberships.ts`
- `permissions.ts`
- `invites.ts`
- `join-requests.ts`
- `instance-admin.ts`
Changes:
- centralized permission evaluation
- centralized membership resolution
- one place for principal-type branching
## 7.5 Routes
Files:
- `server/src/routes/index.ts` and new route modules:
- `auth.ts` (if needed)
- `invites.ts`
- `join-requests.ts`
- `members.ts`
- `instance-admin.ts`
- `inbox.ts` (or extension of existing inbox source)
Changes:
- add new endpoints listed above
- apply company and permission checks consistently
- log all mutations through activity log service
## 7.6 Activity log and audit
Files:
- `server/src/services/activity-log.ts`
- call sites in invite/join/member/admin routes
Required actions:
- `invite.created`
- `invite.revoked`
- `join.requested`
- `join.approved`
- `join.rejected`
- `membership.activated`
- `permission.granted`
- `permission.revoked`
- `instance_admin.promoted`
- `instance_admin.demoted`
- `agent_api_key.claimed`
- `agent_api_key.revoked`
## 7.7 Real-time and inbox propagation
Files:
- `server/src/services/live-events.ts`
- `server/src/realtime/live-events-ws.ts`
- inbox data source endpoint(s)
Changes:
- emit join-request events
- ensure inbox refresh path includes join alerts
## 8. CLI implementation
Files:
- `cli/src/index.ts`
- `cli/src/commands/onboard.ts`
- `cli/src/commands/configure.ts`
- `cli/src/prompts/server.ts`
Commands:
1. `paperclipai auth bootstrap-ceo`
- create bootstrap invite
- print one-time URL
2. `paperclipai onboard`
- in cloud mode with `bootstrap_pending`, print bootstrap URL and next steps
- in local mode, skip bootstrap requirement
Config additions:
- deployment mode
- bind host (validated against mode)
## 9. UI implementation
Files:
- routing: `ui/src/App.tsx`
- API clients: `ui/src/api/*`
- pages/components (new):
- `AuthLogin` / `AuthSignup` (cloud mode)
- `BootstrapPending` page
- `InviteLanding` page
- `InstanceSettings` page
- join approval components in `Inbox`
- member/grant management in company settings
Required UX:
1. Cloud unauthenticated user:
- redirect to login/signup
2. Cloud bootstrap pending:
- block app with setup command guidance
3. Invite landing:
- choose human vs agent path (respect `allowedJoinTypes`)
- submit join request
- show pending approval confirmation
4. Inbox:
- show join approval cards with approve/reject actions
- include source IP and human email snapshot when applicable
5. Local mode:
- no login prompts
- full settings/invite/approval UI available
## 10. Security controls
1. Token handling
- invite tokens hashed at rest
- API keys hashed at rest
- one-time plaintext key reveal only
2. Local mode isolation
- loopback bind enforcement
- startup hard-fail on non-loopback host
3. Cloud auth
- no implicit board fallback
- session auth mandatory for human mutations
4. Join workflow hardening
- one request per invite token
- pending request has no data access
- approval required before membership activation
5. Abuse controls
- rate limit invite accept and key claim endpoints
- structured logging for join and claim failures
## 11. Migration and compatibility
## 11.1 Runtime compatibility
- keep existing board-dependent routes functional while migrating authz helper usage
- phase out `assertBoard` calls only after permission helpers cover all routes
## 11.2 Data compatibility
- do not delete `agents.permissions` in V1
- stop reading it once grants are wired
- remove in post-V1 cleanup migration
## 11.3 Better Auth user ID handling
- treat `user.id` as text end-to-end
- existing `created_by_user_id` and similar text fields remain valid
## 12. Testing strategy
## 12.1 Unit tests
- permission evaluator:
- instance admin bypass
- grant checks
- scope checks
- join approval state machine
- invite token lifecycle
## 12.2 Integration tests
- cloud mode unauthenticated mutation -> `401`
- local mode implicit admin mutation -> success
- invite accept -> pending join -> no access
- join approve (human) -> membership/grants active
- join approve (agent) -> key claim once
- cross-company access denied for user and agent principals
- local mode non-loopback bind -> startup failure
## 12.3 UI tests
- login gate in cloud mode
- bootstrap pending screen
- invite landing choose-path UX
- inbox join alert approve/reject flows
## 12.4 Regression tests
- existing agent API key flows still work
- task assignment and checkout invariants unchanged
- activity logging still emitted for all mutations
## 13. Delivery plan
## Phase A: Foundations
- config mode/bind host support
- startup guardrails
- Better Auth integration skeleton
- actor type expansion
## Phase B: Schema and authz core
- add membership/grants/invite/join tables
- add permission service and helpers
- wire company/member/instance admin checks
## Phase C: Invite + join backend
- invite create/revoke
- invite accept -> pending request
- approve/reject + key claim
- activity log + live events
## Phase D: UI + CLI
- cloud login/bootstrap screens
- invite landing
- inbox join approval actions
- instance settings and member permissions
- bootstrap CLI command and onboarding updates
## Phase E: Hardening
- full integration/e2e coverage
- docs updates (`SPEC-implementation`, `DEVELOPING`, `CLI`)
- cleanup of legacy board-only codepaths
## 14. Verification gate
Before handoff:
```sh
pnpm -r typecheck
pnpm test:run
pnpm build
```
If any command is skipped, record exactly what was skipped and why.
## 15. Done criteria
1. Behavior matches locked V1 decisions in this doc and `doc/plan/humans-and-permissions.md`.
2. Cloud mode requires auth; local mode has no login UX.
3. Unified invite + pending approval flow works for both humans and agents.
4. Shared principal membership + permission system is live for users and agents.
5. Local mode remains loopback-only and fails otherwise.
6. Inbox shows actionable join approvals.
7. All new mutating paths are activity-logged.

View File

@@ -0,0 +1,421 @@
# Humans and Permissions Plan
Status: Draft
Date: 2026-02-21
Owner: Server + UI + Shared + DB
## Goal
Add first-class human users and permissions while preserving two deployment modes:
- local trusted single-user mode with no login friction
- cloud-hosted multi-user mode with mandatory authentication and authorization
## Why this plan
Current V1 assumptions are centered on one board operator. We now need:
- multi-human collaboration with per-user permissions
- safe cloud deployment defaults (no accidental loginless production)
- local mode that still feels instant (`npx paperclipai run` and go)
- agent-to-human task delegation, including a human inbox
- one user account with access to multiple companies in one deployment
- instance admins who can manage company access across the instance
- join approvals surfaced as actionable inbox alerts, not buried in admin-only pages
- a symmetric invite-and-approve onboarding path for both humans and agents
- one shared membership and permission model for both humans and agents
## Product constraints
1. Keep company scoping strict for every new table, endpoint, and permission check.
2. Preserve existing control-plane invariants:
- single-assignee task model
- approval gates
- budget hard-stop behavior
- mutation activity logging
3. Keep local mode easy and trusted, but prevent unsafe cloud posture.
## Deployment modes
## Mode A: `local_trusted`
Behavior:
- no login UI
- browser opens directly into board context
- embedded DB and local storage defaults remain
- a local implicit human actor exists for attribution
- local implicit actor has effective `instance_admin` authority for that instance
- full invite/approval/permission settings flows remain available in local mode (including agent enrollment)
Guardrails:
- server binds to loopback by default
- fail startup if mode is `local_trusted` with non-loopback bind
- UI shows a persistent "Local trusted mode" badge
## Mode B: `cloud_hosted`
Behavior:
- login required for all human endpoints
- Better Auth for human auth
- initial auth method: email + password
- email verification is not required for initial release
- hosted DB and remote deployment supported
- multi-user sessions and role/permission enforcement
Guardrails:
- fail startup if auth provider/session config is missing
- fail startup if insecure auth bypass flag is set
- health payload includes mode and auth readiness
## Authentication choice
- use Better Auth for human users
- start with email/password login only
- no email confirmation requirement in V1
- keep implementation structured so social/SSO providers can be added later without changing membership/permission semantics
## Auth and actor model
Unify request actors into a single model:
- `user` (authenticated human)
- `agent` (API key)
- `local_board_implicit` (local trusted mode only)
Rules:
- in `cloud_hosted`, only `user` and `agent` are valid actors
- in `local_trusted`, unauthenticated browser/API requests resolve to `local_board_implicit`
- `local_board_implicit` is authorized as an instance admin principal for local operations
- all mutating actions continue writing `activity_log` with actor type/id
## First admin bootstrap
Problem:
- new cloud deployments need a safe, explicit first human admin path
- app cannot assume a pre-existing admin account
- `local_trusted` does not use bootstrap flow because implicit local instance admin already exists
Bootstrap flow:
1. If no `instance_admin` user exists for the deployment, instance is in `bootstrap_pending` state.
2. CLI command `pnpm paperclipai auth bootstrap-ceo` creates a one-time CEO onboarding invite URL for that instance.
3. `pnpm paperclipai onboard` runs this bootstrap check and prints the invite URL automatically when `bootstrap_pending`.
4. Visiting the app while `bootstrap_pending` shows a blocking setup page with the exact CLI command to run (`pnpm paperclipai onboard`).
5. Accepting that CEO invite creates the first admin user and exits bootstrap mode.
Security rules:
- bootstrap invite is single-use, short-lived, and token-hash stored at rest
- only one active bootstrap invite at a time per instance (regeneration revokes prior token)
- bootstrap actions are audited in `activity_log`
## Data model additions
## New tables
1. `users`
- identity record for human users (email-based)
- optional instance-level role field (or companion table) for admin rights
2. `company_memberships`
- `company_id`, `principal_type` (`user | agent`), `principal_id`
- status (`pending | active | suspended`), role metadata
- stores effective access state for both humans and agents
- many-to-many: one principal can belong to multiple companies
3. `invites`
- `company_id`, `invite_type` (`company_join | bootstrap_ceo`), token hash, expires_at, invited_by, revoked_at, accepted_at
- one-time share link (no pre-bound invite email)
- `allowed_join_types` (`human | agent | both`) for `company_join` links
- optional defaults payload keyed by join type:
- human defaults: initial permissions/membership role
- agent defaults: proposed role/title/adapter defaults
4. `principal_permission_grants`
- `company_id`, `principal_type` (`user | agent`), `principal_id`, `permission_key`
- explicit grants such as `agents:create`
- includes scope payload for chain-of-command limits
- normalized table (not JSON blob) for auditable grant/revoke history
5. `join_requests`
- `invite_id`, `company_id`, `request_type` (`human | agent`)
- `status` (`pending_approval | approved | rejected`)
- common review metadata:
- `request_ip`
- `approved_by_user_id`, `approved_at`, `rejected_by_user_id`, `rejected_at`
- human request fields:
- `requesting_user_id`, `request_email_snapshot`
- agent request fields:
- `agent_name`, `adapter_type`, `capabilities`, `created_agent_id` nullable until approved
- each consumed invite creates exactly one join request record after join type is selected
6. `issues` extension
- add `assignee_user_id` nullable
- preserve single-assignee invariant with XOR check:
- exactly zero or one of `assignee_agent_id` / `assignee_user_id`
## Compatibility
- existing `created_by_user_id` / `author_user_id` fields remain and become fully active
- agent API keys remain auth credentials; membership + grants remain authorization source
## Permission model (initial set)
Principle:
- humans and agents use the same membership + grant evaluation engine
- permission checks resolve against `(company_id, principal_type, principal_id)` for both actor types
- this avoids separate authz codepaths and keeps behavior consistent
Role layers:
- `instance_admin`: deployment-wide admin, can access/manage all companies and user-company access mapping
- `company_member`: company-scoped permissions only
Core grants:
1. `agents:create`
2. `users:invite`
3. `users:manage_permissions`
4. `tasks:assign`
5. `tasks:assign_scope` (org-constrained delegation)
6. `joins:approve` (approve/reject human and agent join requests)
Additional behavioral rules:
- instance admins can promote/demote instance admins and manage user access across companies
- board-level users can manage company grants inside companies they control
- non-admin principals can only act within explicit grants
- assignment checks apply to both agent and human assignees
## Chain-of-command scope design
Initial approach:
- represent assignment scope as an allow rule over org hierarchy
- examples:
- `subtree:<agentId>` (can assign into that manager subtree)
- `exclude:<agentId>` (cannot assign to protected roles, e.g., CEO)
Enforcement:
- resolve target assignee org position
- evaluate allow/deny scope rules before assignment mutation
- return `403` for out-of-scope assignments
## Invite and signup flow
1. Authorized user creates one `company_join` invite share link with optional defaults + expiry.
2. System sends invite URL containing one-time token.
3. Invite landing page presents two paths: `Join as human` or `Join as agent` (subject to `allowed_join_types`).
4. Requester selects join path and submits required data.
5. Submission consumes token and creates a `pending_approval` join request (no access yet).
6. Join request captures review metadata:
- human: authenticated email
- both: source IP
- agent: proposed agent metadata
7. Company admin/instance admin reviews request and approves or rejects.
8. On approval:
- human: activate `company_membership` and apply permission grants
- agent: create agent record and enable API-key claim flow
9. Link is one-time and cannot be reused.
10. Inviter/admin can revoke invite before acceptance.
Security rules:
- store invite token hashed at rest
- one-time use token with short expiry
- all invite lifecycle events logged in `activity_log`
- pending users cannot read or mutate any company data until approved
## Join approval inbox
- join requests generate inbox alerts for eligible approvers (`joins:approve` or admin role)
- alerts appear in both:
- global/company inbox feed
- dedicated pending-approvals UI
- each alert includes approve/reject actions inline (no context switch required)
- alert payload must include:
- requester email when `request_type=human`
- source IP
- request type (`human | agent`)
## Human inbox and agent-to-human delegation
Behavior:
- agents can assign tasks to humans when policy permits
- humans see assigned tasks in inbox view (including in local trusted mode)
- comment and status transitions follow same issue lifecycle guards
## Agent join path (via unified invite link)
1. Authorized user shares one `company_join` invite link (with `allowed_join_types` including `agent`).
2. Agent operator opens link, chooses `Join as agent`, and submits join payload (name/role/adapter metadata).
3. System creates `pending_approval` agent join request and captures source IP.
4. Approver sees alert in inbox and approves or rejects.
5. On approval, server creates the agent record and mints a long-lived API key.
6. API key is shown exactly once via secure claim flow with explicit "save now" instruction.
Long-lived token policy:
- default to long-lived revocable API keys (hash stored at rest)
- show plaintext key once only
- support immediate revoke/regenerate from admin UI
- optionally add expirations/rotation policy later without changing join flow
API additions (proposed):
- `GET /companies/:companyId/inbox` (human actor scoped to self; includes task items + pending join approval alerts when authorized)
- `POST /companies/:companyId/issues/:issueId/assign-user`
- `POST /companies/:companyId/invites`
- `GET /invites/:token` (invite landing payload with `allowed_join_types`)
- `POST /invites/:token/accept` (body includes `requestType=human|agent` and request metadata)
- `POST /invites/:inviteId/revoke`
- `GET /companies/:companyId/join-requests?status=pending_approval&requestType=human|agent`
- `POST /companies/:companyId/join-requests/:requestId/approve`
- `POST /companies/:companyId/join-requests/:requestId/reject`
- `POST /join-requests/:requestId/claim-api-key` (approved agent requests only)
- `GET /companies/:companyId/members` (returns both human and agent principals)
- `PATCH /companies/:companyId/members/:memberId/permissions`
- `POST /admin/users/:userId/promote-instance-admin`
- `POST /admin/users/:userId/demote-instance-admin`
- `PUT /admin/users/:userId/company-access` (set accessible companies for a user)
- `GET /admin/users/:userId/company-access`
## Local mode UX policy
- no login prompt or account setup required
- local implicit board user is auto-provisioned for audit attribution
- local operator can still use instance settings and company settings as effective instance admin
- invite, join approval, and permission-management UI is available in local mode
- agent onboarding is expected in local mode, including creating invite links and approving join requests
- public/untrusted network ingress is out of scope for V1 local mode
## Cloud agents in this model
- cloud agents continue authenticating through `agent_api_keys`
- same-company boundary checks remain mandatory
- agent ability to assign human tasks is permission-gated, not implicit
## Instance settings surface
This plan introduces instance-level concerns (for example bootstrap state, instance admins, invite defaults, and token policy). There is no dedicated UI surface today.
V1 approach:
- add a minimal `Instance Settings` page for instance admins
- expose key instance settings in API + CLI (`paperclipai configure` / `paperclipai onboard`)
- show read-only instance status indicators in the main UI until full settings UX exists
## Implementation phases
## Phase 1: Mode and guardrails
- add explicit deployment mode config (`local_trusted | cloud_hosted`)
- enforce startup safety checks and health visibility
- implement actor resolution for local implicit board
- map local implicit board actor to instance-admin authorization context
- add bootstrap status signal in health/config surface (`ready | bootstrap_pending`)
- add minimal instance settings API/CLI surface and read-only UI indicators
## Phase 2: Human identity and memberships
- add schema + migrations for users/memberships/invites
- wire auth middleware for cloud mode
- add membership lookup and company access checks
- implement Better Auth email/password flow (no email verification)
- implement first-admin bootstrap invite command and onboard integration
- implement one-time share-link invite acceptance flow with `pending_approval` join requests
## Phase 3: Permissions and assignment scope
- add shared principal grant model and enforcement helpers
- add chain-of-command scope checks for assignment APIs
- add tests for forbidden assignment (for example, cannot assign to CEO)
- add instance-admin promotion/demotion and global company-access management APIs
- add `joins:approve` permission checks for human and agent join approvals
## Phase 4: Invite workflow
- unified `company_join` invite create/landing/accept/revoke endpoints
- join request approve/reject endpoints with review metadata (email when applicable, IP)
- one-time token security and revocation semantics
- UI for invite management, pending join approvals, and membership permissions
- inbox alert generation for pending join requests
- ensure invite and approval UX is enabled in both `cloud_hosted` and `local_trusted`
## Phase 5: Human inbox + task assignment updates
- extend issue assignee model for human users
- inbox API and UI for:
- task assignments
- pending join approval alerts with inline approve/reject actions
- agent-to-human assignment flow with policy checks
## Phase 6: Agent self-join and token claim
- add agent join path on unified invite landing page
- capture agent join requests and admin approval flow
- create one-time API-key claim flow after approval (display once)
## Acceptance criteria
1. `local_trusted` starts with no login and shows board UI immediately.
2. `local_trusted` does not expose optional human login UX in V1.
3. `local_trusted` local implicit actor can manage instance settings, invite links, join approvals, and permission grants.
4. `cloud_hosted` cannot start without auth configured.
5. No request in `cloud_hosted` can mutate data without authenticated actor.
6. If no initial admin exists, app shows bootstrap instructions with CLI command.
7. `pnpm paperclipai onboard` outputs a CEO onboarding invite URL when bootstrap is pending.
8. One `company_join` link supports both human and agent onboarding via join-type selection on the invite landing page.
9. Invite delivery in V1 is copy-link only (no built-in email delivery).
10. Share-link acceptance creates a pending join request; it does not grant immediate access.
11. Pending join requests appear as inbox alerts with inline approve/reject actions.
12. Admin review view includes join metadata before decision (human email when applicable, source IP, and agent metadata for agent requests).
13. Only approved join requests unlock access:
- human: active company membership + permission grants
- agent: agent creation + API-key claim eligibility
14. Agent enrollment follows the same link -> pending approval -> approve flow.
15. Approved agents can claim a long-lived API key exactly once, with plaintext display-once semantics.
16. Agent API keys are indefinite by default in V1 and revocable/regenerable by admins.
17. Public/untrusted ingress for `local_trusted` is not supported in V1 (loopback-only local server).
18. One user can hold memberships in multiple companies.
19. Instance admins can promote another user to instance admin.
20. Instance admins can manage which companies each user can access.
21. Permissions can be granted/revoked per member principal (human or agent) through one shared grant system.
22. Assignment scope prevents out-of-hierarchy or protected-role assignments.
23. Agents can assign tasks to humans only when allowed.
24. Humans can view assigned tasks in inbox and act on them per permissions.
25. All new mutations are company-scoped and logged in `activity_log`.
## V1 decisions (locked)
1. `local_trusted` will not support login UX in V1; implicit local board actor only.
2. Permissions use a normalized shared table: `principal_permission_grants` with scoped grants.
3. Invite delivery is copy-link only in V1 (no built-in email sending).
4. Bootstrap invite creation should require local shell access only (CLI path only, no HTTP bootstrap endpoint).
5. Approval review shows source IP only; no GeoIP/country lookup in V1.
6. Agent API-key lifetime is indefinite by default in V1, with explicit revoke/regenerate controls.
7. Local mode keeps full admin/settings/invite capabilities through the implicit local instance-admin actor.
8. Public/untrusted ingress for local mode is out of scope for V1; no `--dangerous-agent-ingress` in V1.