doc/plans/2026-02-21-humans-and-permissions.md
Status: Draft Date: 2026-02-21 Owner: Server + UI + Shared + DB
Add first-class human users and permissions while preserving two deployment modes:
Current V1 assumptions are centered on one board operator. We now need:
npx paperclipai run and go)local_trustedBehavior:
instance_admin authority for that instanceGuardrails:
local_trusted with non-loopback bindcloud_hostedBehavior:
Guardrails:
Unify request actors into a single model:
user (authenticated human)agent (API key)local_board_implicit (local trusted mode only)Rules:
cloud_hosted, only user and agent are valid actorslocal_trusted, unauthenticated browser/API requests resolve to local_board_implicitlocal_board_implicit is authorized as an instance admin principal for local operationsactivity_log with actor type/idProblem:
local_trusted does not use bootstrap flow because implicit local instance admin already existsBootstrap flow:
instance_admin user exists for the deployment, instance is in bootstrap_pending state.pnpm paperclipai auth bootstrap-ceo creates a one-time CEO onboarding invite URL for that instance.pnpm paperclipai onboard runs this bootstrap check and prints the invite URL automatically when bootstrap_pending.bootstrap_pending shows a blocking setup page with the exact CLI command to run (pnpm paperclipai onboard).Security rules:
activity_loguserscompany_membershipscompany_id, principal_type (user | agent), principal_idpending | active | suspended), role metadatainvitescompany_id, invite_type (company_join | bootstrap_ceo), token hash, expires_at, invited_by, revoked_at, accepted_atallowed_join_types (human | agent | both) for company_join linksprincipal_permission_grantscompany_id, principal_type (user | agent), principal_id, permission_keyagents:createjoin_requestsinvite_id, company_id, request_type (human | agent)status (pending_approval | approved | rejected)request_ipapproved_by_user_id, approved_at, rejected_by_user_id, rejected_atrequesting_user_id, request_email_snapshotagent_name, adapter_type, capabilities, created_agent_id nullable until approvedissues extensionassignee_user_id nullableassignee_agent_id / assignee_user_idcreated_by_user_id / author_user_id fields remain and become fully activePrinciple:
(company_id, principal_type, principal_id) for both actor typesRole layers:
instance_admin: deployment-wide admin, can access/manage all companies and user-company access mappingcompany_member: company-scoped permissions onlyCore grants:
agents:createusers:inviteusers:manage_permissionstasks:assigntasks:assign_scope (org-constrained delegation)joins:approve (approve/reject human and agent join requests)Additional behavioral rules:
Initial approach:
subtree:<agentId> (can assign into that manager subtree)exclude:<agentId> (cannot assign to protected roles, e.g., CEO)Enforcement:
403 for out-of-scope assignmentscompany_join invite share link with optional defaults + expiry.Join as human or Join as agent (subject to allowed_join_types).pending_approval join request (no access yet).company_membership and apply permission grantsSecurity rules:
activity_logjoins:approve or admin role)request_type=humanhuman | agent)Behavior:
company_join invite link (with allowed_join_types including agent).Join as agent, and submits join payload (name/role/adapter metadata).pending_approval agent join request and captures source IP.Long-lived token policy:
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-userPOST /companies/:companyId/invitesGET /invites/:token (invite landing payload with allowed_join_types)POST /invites/:token/accept (body includes requestType=human|agent and request metadata)POST /invites/:inviteId/revokeGET /companies/:companyId/join-requests?status=pending_approval&requestType=human|agentPOST /companies/:companyId/join-requests/:requestId/approvePOST /companies/:companyId/join-requests/:requestId/rejectPOST /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/permissionsPOST /admin/users/:userId/promote-instance-adminPOST /admin/users/:userId/demote-instance-adminPUT /admin/users/:userId/company-access (set accessible companies for a user)GET /admin/users/:userId/company-accessagent_api_keysThis 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:
Instance Settings page for instance adminspaperclipai configure / paperclipai onboard)local_trusted | cloud_hosted)ready | bootstrap_pending)pending_approval join requestsjoins:approve permission checks for human and agent join approvalscompany_join invite create/landing/accept/revoke endpointscloud_hosted and local_trustedlocal_trusted starts with no login and shows board UI immediately.local_trusted does not expose optional human login UX in V1.local_trusted local implicit actor can manage instance settings, invite links, join approvals, and permission grants.cloud_hosted cannot start without auth configured.cloud_hosted can mutate data without authenticated actor.pnpm paperclipai onboard outputs a CEO onboarding invite URL when bootstrap is pending.company_join link supports both human and agent onboarding via join-type selection on the invite landing page.local_trusted is not supported in V1 (loopback-only local server).activity_log.local_trusted will not support login UX in V1; implicit local board actor only.principal_permission_grants with scoped grants.--dangerous-agent-ingress in V1.