Files
paperclip/doc/plan/humans-and-permissions.md
Dotta f60c1001ec refactor: rename packages to @paperclipai and CLI binary to paperclipai
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>
2026-03-03 08:45:26 -06:00

18 KiB

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
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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
  1. Company admin/instance admin reviews request and approves or rejects.
  2. On approval:
  • human: activate company_membership and apply permission grants
  • agent: create agent record and enable API-key claim flow
  1. Link is one-time and cannot be reused.
  2. 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
  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
  1. Agent enrollment follows the same link -> pending approval -> approve flow.
  2. Approved agents can claim a long-lived API key exactly once, with plaintext display-once semantics.
  3. Agent API keys are indefinite by default in V1 and revocable/regenerable by admins.
  4. Public/untrusted ingress for local_trusted is not supported in V1 (loopback-only local server).
  5. One user can hold memberships in multiple companies.
  6. Instance admins can promote another user to instance admin.
  7. Instance admins can manage which companies each user can access.
  8. Permissions can be granted/revoked per member principal (human or agent) through one shared grant system.
  9. Assignment scope prevents out-of-hierarchy or protected-role assignments.
  10. Agents can assign tasks to humans only when allowed.
  11. Humans can view assigned tasks in inbox and act on them per permissions.
  12. 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.