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:
643
doc/plans/2026-02-21-humans-and-permissions-implementation.md
Normal file
643
doc/plans/2026-02-21-humans-and-permissions-implementation.md
Normal 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.
|
||||
421
doc/plans/2026-02-21-humans-and-permissions.md
Normal file
421
doc/plans/2026-02-21-humans-and-permissions.md
Normal 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.
|
||||
Reference in New Issue
Block a user