Imported from albertobaselga/paperclip-skills (
skills/paperclip-manage-access/SKILL.md). Install upstream withnpx skills add albertobaselga/paperclip-skills --skill paperclip-manage-access. Copyright stays with the author.
Paperclip — Manage Access
Access control covers CLI authentication, company invites, join request approvals, member permission grants, and instance-admin user administration.
Authentication Model
Token types:
- Board API keys — long-lived, prefixed
pcp_board_. Minted via CLI auth challenge flow. - Agent API keys — long-lived per-agent tokens. Minted via
POST /api/agents/:id/keysor join-request claim. - Local agent JWTs — short-lived tokens for local-mode agents.
- Session cookies — issued by web UI login (authenticated deployments).
Headers:
Authorization: Bearer <token>— required for all non-session auth.X-Paperclip-Run-Id: <run-id>— required when an agent token mutates a checked-out issue, and on checkout/release calls.
Resolution order: local trust mode → session cookie → board API key → agent API key → local JWT → unauthenticated.
Scope rules:
- Agents are bound to their company; cross-company access returns 403.
- Board actors are limited to companies they're members of.
- Instance admins can act cross-company.
local_trustedmode bypasses membership checks.
Prerequisites
- Board operator role on the target company (most operations)
- Instance admin role (for
/api/admin/...endpoints) - Instance running in
local_trustedmode (or equivalent board access) BASEdefaults tohttp://localhost:3100— override viaPAPERCLIP_API_URL
BASE="${PAPERCLIP_API_URL:-http://localhost:3100}"
CID="<your-company-id>"
CLI Authentication
Bootstrap First Admin
Generates a one-time invite URL for the first board operator (CEO role). Run this on a fresh instance before any users exist.
# Basic
pnpm paperclipai auth bootstrap-ceo
# With options
pnpm paperclipai auth bootstrap-ceo \
--expires-hours 24 \
--base-url http://localhost:3100 \
--force
--force regenerates the token even if one already exists.
Login
Authenticate the CLI against the instance. Opens a browser challenge flow.
pnpm paperclipai auth login
# As instance admin
pnpm paperclipai auth login --instance-admin
Logout
pnpm paperclipai auth logout
Check Current Identity
pnpm paperclipai auth whoami
# API equivalent
curl -s "$BASE/api/cli-auth/me" | jq
Board Claim (First Admin Flow)
After bootstrap-ceo generates a token, the recipient claims it to become the first board operator.
TOKEN="<claim-token>"
# Inspect the claim before accepting
curl -s "$BASE/api/board-claim/$TOKEN" | jq
# Claim it (provisions the board operator role)
curl -s -X POST "$BASE/api/board-claim/$TOKEN/claim" \
-H "Content-Type: application/json" \
-d '{}' | jq
CLI Auth Challenges (API)
Public endpoints. The CLI login flow uses a challenge/approval model; the auth login command drives these, but they can also be called directly.
| Method | Path | Purpose |
|---|---|---|
| POST | /api/cli-auth/challenges |
Initiate challenge |
| GET | /api/cli-auth/challenges/:id |
Poll status (returns token when approved) |
| POST | /api/cli-auth/challenges/:id/approve |
Mint board key (requires authenticated approver) |
| POST | /api/cli-auth/challenges/:id/cancel |
Reject |
| GET | /api/cli-auth/me |
Inspect current identity |
| POST | /api/cli-auth/revoke-current |
Revoke active key |
# Create a challenge
curl -s -X POST "$BASE/api/cli-auth/challenges" \
-H "Content-Type: application/json" \
-d '{
"command": "login",
"clientName": "my-terminal",
"requestedAccess": "board",
"requestedCompanyId": "'$CID'"
}' | jq
CHALLENGE_ID="<challenge-id>"
# Poll challenge status
curl -s "$BASE/api/cli-auth/challenges/$CHALLENGE_ID" | jq '{status, token}'
# Approve (from board UI or another authenticated session)
curl -s -X POST "$BASE/api/cli-auth/challenges/$CHALLENGE_ID/approve" | jq
# Cancel
curl -s -X POST "$BASE/api/cli-auth/challenges/$CHALLENGE_ID/cancel" | jq
# Revoke current CLI token
curl -s -X POST "$BASE/api/cli-auth/revoke-current" | jq
Invites
Invites allow new users or agents to join a company.
Create an Invite
POST /api/companies/$CID/invites
| Field | Description |
|---|---|
allowedJoinTypes |
Array of join types permitted (e.g. ["agent","user"]) |
defaultsPayload |
Default profile data pre-filled on join |
agentMessage |
Message shown to agents during onboarding |
curl -s -X POST "$BASE/api/companies/$CID/invites" \
-H "Content-Type: application/json" \
-d '{
"allowedJoinTypes": ["agent"],
"agentMessage": "Welcome to the engineering team. Follow the onboarding steps."
}' | jq '{id, token, url}'
Share the returned url (or construct it as $BASE/join/$TOKEN) with the invitee.
Inspect an Invite
TOKEN="<invite-token>"
curl -s "$BASE/api/invites/$TOKEN" | jq
Onboarding Info
# Structured onboarding data
curl -s "$BASE/api/invites/$TOKEN/onboarding" | jq
# Plain text (for CLI/agent consumption)
curl -s "$BASE/api/invites/$TOKEN/onboarding.txt"
Revoke an Invite
IID="<invite-id>"
curl -s -X POST "$BASE/api/invites/$IID/revoke" | jq
OpenClaw Invite Prompt
For OpenClaw agents, generate a formatted invite prompt:
curl -s -X POST "$BASE/api/companies/$CID/openclaw/invite-prompt" \
-H "Content-Type: application/json" \
-d '{"inviteToken": "'$TOKEN'"}' | jq
Join Requests
When an agent or user attempts to join via an invite, a join request is created and must be approved before access is provisioned.
List Pending Requests
curl -s "$BASE/api/companies/$CID/join-requests" | jq '[.[] | {id, status, agentName, requestedAt}]'
Approve a Request
Approval provisions the agent/user's access immediately.
RID="<request-id>"
curl -s -X POST "$BASE/api/companies/$CID/join-requests/$RID/approve" | jq
Reject a Request
curl -s -X POST "$BASE/api/companies/$CID/join-requests/$RID/reject" | jq
Claim API Key After Approval
After a join request is approved, the joining party claims their API key:
curl -s -X POST "$BASE/api/join-requests/$RID/claim-api-key" | jq '{apiKey}'
Members and Permissions
List Members
curl -s "$BASE/api/companies/$CID/members" | jq '[.[] | {id, name, role, grants}]'
Update Member Permissions
PATCH /api/companies/$CID/members/$MID/permissions
Grants are scoped permission assignments. Available permission keys:
| Key | Description |
|---|---|
agents:create |
Create agents in the company |
users:invite |
Send invites to new users |
users:manage_permissions |
Modify other members' permissions |
tasks:assign |
Assign tasks to members |
tasks:assign_scope |
Set task scope/constraints |
joins:approve |
Approve join requests |
MID="<member-id>"
curl -s -X PATCH "$BASE/api/companies/$CID/members/$MID/permissions" \
-H "Content-Type: application/json" \
-d '{
"grants": [
{"permissionKey": "agents:create", "scope": "company"},
{"permissionKey": "joins:approve", "scope": "company"}
]
}' | jq
Instance Admin Operations
These endpoints require instance admin privileges.
Promote / Demote Instance Admin
UID="<user-id>"
# Promote
curl -s -X POST "$BASE/api/admin/users/$UID/promote-instance-admin" | jq
# Demote
curl -s -X POST "$BASE/api/admin/users/$UID/demote-instance-admin" | jq
View User's Company Access
curl -s "$BASE/api/admin/users/$UID/company-access" | jq
Set User's Company Access
Explicitly assign which companies a user can access:
curl -s -X PUT "$BASE/api/admin/users/$UID/company-access" \
-H "Content-Type: application/json" \
-d '{"companyIds": ["cid-1", "cid-2"]}' | jq
Workflow 1: Bootstrap First Admin
Goal: Get the first board operator set up on a fresh instance.
Step 1 — Generate the bootstrap invite URL:
pnpm paperclipai auth bootstrap-ceo --expires-hours 48 --base-url http://localhost:3100
Copy the printed URL.
Step 2 — Inspect the claim token (optional):
curl -s "$BASE/api/board-claim/$TOKEN" | jq '{expires, claimed}'
Step 3 — Claim the board operator role:
curl -s -X POST "$BASE/api/board-claim/$TOKEN/claim" \
-H "Content-Type: application/json" \
-d '{}' | jq
Step 4 — Log the CLI in:
pnpm paperclipai auth login
Step 5 — Confirm identity:
pnpm paperclipai auth whoami
Workflow 2: Invite an Agent to Join
Goal: Onboard a new agent into the company with controlled access.
Step 1 — Create the invite:
curl -s -X POST "$BASE/api/companies/$CID/invites" \
-H "Content-Type: application/json" \
-d '{
"allowedJoinTypes": ["agent"],
"agentMessage": "You are joining as a task execution agent. Complete onboarding before starting work."
}' | jq '{id, token, url}'
Note the id (for revocation) and share the url with the agent.
Step 2 — Agent fetches onboarding instructions:
curl -s "$BASE/api/invites/$TOKEN/onboarding.txt"
Step 3 — Monitor for the join request:
curl -s "$BASE/api/companies/$CID/join-requests" \
| jq '[.[] | select(.status == "pending") | {id, agentName, requestedAt}]'
Step 4 — Approve the join request:
curl -s -X POST "$BASE/api/companies/$CID/join-requests/$RID/approve" | jq
Step 5 — Agent claims its API key:
curl -s -X POST "$BASE/api/join-requests/$RID/claim-api-key" | jq '{apiKey}'
Step 6 — Optionally grant additional permissions:
curl -s -X PATCH "$BASE/api/companies/$CID/members/$MID/permissions" \
-H "Content-Type: application/json" \
-d '{"grants": [{"permissionKey": "agents:create", "scope": "company"}]}' | jq
Step 7 — Revoke the invite so it cannot be reused:
curl -s -X POST "$BASE/api/invites/$IID/revoke" | jq