Rename all workspace packages from @paperclip/* to @paperclipai/* and the CLI binary from `paperclip` to `paperclipai` in preparation for npm publishing. Bump CLI version to 0.1.0 and add package metadata (description, keywords, license, repository, files). Update all imports, documentation, user-facing messages, and tests accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
644 lines
16 KiB
Markdown
644 lines
16 KiB
Markdown
# 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.
|