papercliped 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/access/guard.js +17 -0
- package/dist/access/policy.js +203 -0
- package/dist/execute.js +25 -1
- package/dist/manage/routes.js +46 -2
- package/dist/mcp.js +6 -5
- package/dist/oauth/pg-store.js +13 -1
- package/dist/oauth/provider.js +50 -1
- package/dist/oauth/store.js +20 -0
- package/dist/public-api/types.js +1 -1
- package/dist/server.js +2 -2
- package/dist/site/site.js +2 -0
- package/docs/CONTROL-ROOM.md +79 -0
- package/docs/SECURITY.md +1 -1
- package/docs/SITEMAP.md +3 -1
- package/docs/supabase-schema.sql +12 -1
- package/migrations/007_control_room.sql +9 -0
- package/package.json +1 -1
- package/site/docs/access-modes.md +72 -0
- package/site/docs/connect-your-paperclip.md +1 -1
- package/site/docs/control-room.md +61 -0
- package/site/docs/faq.md +6 -0
- package/site/docs/getting-started.md +1 -1
- package/site/docs/limits-and-errors.md +1 -0
- package/site/docs/manage-api.md +12 -1
- package/site/docs/paperclip-plugin.md +21 -10
- package/site/docs/permissions.md +8 -0
- package/site/docs/public-api.md +1 -1
- package/site/docs/tools.md +35 -35
- package/site/docs/troubleshooting.md +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v2.4.0 — the control room: manage every session, agent and tool inside Paperclip
|
|
6
|
+
|
|
7
|
+
- **The control room.** The Papercliped plugin now opens a control room inside Paperclip, in Paperclip's own design: a left menu that replaces the company menu (Overview, Sessions, Agents, Tools, Activity, Settings), the way Paperclip's own agent page does. See every connected AI app, what each may use, which agents it applies to, and what it has done. New guides: **The control room** and **API only, Full or Agent only**.
|
|
8
|
+
- **An access switch: API only, Full or Agent only.** One three-position switch (with a sliding knob, keyboard support and a small version for the dashboard) decides how AI apps may work with your Paperclip. **API only**: look and control Paperclip directly (pause, budgets, approvals, goals) but don't hand work to agents. **Agent only**: look and work through agents (create, assign and comment on issues, wake an agent) but no direct control. **Full** (the default, so nothing changes until you choose): both. It takes effect on the app's next request.
|
|
9
|
+
- **Set any agent apart.** On the Agents page, or on a new **Papercliped tab on every agent's page**, give an agent its own mode, or turn it **Off**: AI apps can't see it, touch it, or see what is assigned to it. Off agents are removed from lists and reports before the report is written, so they can't leak into a summary.
|
|
10
|
+
- **Sessions you can manage.** Every connected app is a session: give it a name, change its level, choose exactly which of the tools it may use (grouped as Look, Work through agents and Direct control), and choose whether it applies to everyone or only selected agents. A session limited to selected agents works only with those agents; it can't use views across every agent, and the page says so.
|
|
11
|
+
- **Blocked calls are visible.** A refused call returns a plain message naming the setting and where to change it (with a `policy` code in the API), and shows as **Blocked** in the new Activity page, which lists the newest calls, their session, tool and result, never what was sent or returned. AI apps are no longer shown tools that can never run for their session.
|
|
12
|
+
- **A dashboard widget** with the access switch in small, the number of sessions and how many calls were blocked today.
|
|
13
|
+
- **Tool reference shows each tool's kind** (a new "Works in" column), and `paperclip_api_request` is described correctly: `GET` works in every mode.
|
|
14
|
+
- **For API users:** new management endpoints `GET /sessions`, `POST|DELETE /sessions/:id`, `GET|POST|PUT /policy`, `GET /tools` and `GET /activity`; a new error class `policy_blocked` in the public API's aggregate error classes; migration `007_control_room.sql` adds two nullable columns and needs no backfill.
|
|
15
|
+
- **Update note.** Paperclip won't update a plugin to a version that asks for new permissions without an admin's approval, so to get the control room remove the Papercliped plugin in Settings → Plugins and install it again (or connect your Paperclip again with the install box ticked). Your account, sessions and settings are kept.
|
|
16
|
+
- **The Papercliped plugin now looks like part of Paperclip.** Its sidebar entry has the same paperclip icon, spacing, type, hover and highlighted-when-open look as Tasks, Artifacts and Cases, and the page uses Paperclip’s own cards, inputs, buttons, tabs and text styles, so it follows your theme (light, dark, seasonal) instead of imitating it.
|
|
17
|
+
- **Fixed: the Papercliped page inside Paperclip no longer invites your browser to fill in your Paperclip login.** The page shares an address with Paperclip’s own sign-in, so browsers offered your saved Paperclip email and password in the "Link your Papercliped account" fields, one click from sending that password to Papercliped as a "secret key". Those fields (and the confirm boxes) now tell the browser not to autofill. If you linked an account on 2.2.1, nothing needs doing; if a saved login was ever filled in, do not press Link.
|
|
18
|
+
|
|
5
19
|
## v2.3.0 — host your Paperclip anywhere, and a prompt gallery
|
|
6
20
|
|
|
7
21
|
- **Host your Paperclip: a step-by-step guide for every option.** A new docs section takes you from nothing to a running Paperclip with a public `https://` address, connected to Papercliped and answering your first prompt. It covers your own computer with a free tunnel (Cloudflare Tunnel, Tailscale Funnel, ngrok), a VPS with Docker and Caddy (Hetzner, DigitalOcean, Linode, Oracle's free tier), Railway, Render, Fly.io and Coolify, with a comparison of cost and effort. Every guide uses the official Paperclip image and the settings current Paperclip needs: authenticated mode with public exposure, your public URL, the two secrets, a persistent disk at `/paperclip`, creating the first admin with `paperclipai auth bootstrap-ceo`, and signing in your agents with API keys or a subscription. They also cover updates and backups. Hosts that sleep or have no disk are flagged as unsuitable.
|
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ There are 30 tools ([full list](https://docs.papercliped.co/tools)). **Read only
|
|
|
92
92
|
|
|
93
93
|
## Manage your connections in Paperclip
|
|
94
94
|
|
|
95
|
-
Install the **Papercliped plugin for Paperclip** (npm `papercliped-paperclip-plugin`, from **Settings → Plugins** in Paperclip). It adds a
|
|
95
|
+
Install the **Papercliped plugin for Paperclip** (npm `papercliped-paperclip-plugin`, from **Settings → Plugins** in Paperclip). It adds a **control room**: see every connected app, choose which tools each may use and which agents it applies to, set **API only / Full / Agent only** for every agent or one at a time (or turn an agent off), read what was blocked, go anonymous, and look after your account. [Plugin guide](https://docs.papercliped.co/paperclip-plugin) · [The control room](https://docs.papercliped.co/control-room) · [API only, Full or Agent only](https://docs.papercliped.co/access-modes).
|
|
96
96
|
|
|
97
97
|
## Security and privacy
|
|
98
98
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { redactAgents } from "./policy.js";
|
|
2
|
+
/**
|
|
3
|
+
* A view of the user's Paperclip in which some agents do not exist. Every response is stripped of those agents before a tool
|
|
4
|
+
* sees it, so reports, lists and syncs are all consistent: a report built from this client cannot name a hidden agent,
|
|
5
|
+
* because it never received one. Anything the server already summed (totals) is left as it is.
|
|
6
|
+
*/
|
|
7
|
+
export function guardClient(client, hidden) {
|
|
8
|
+
if (hidden.size === 0)
|
|
9
|
+
return client;
|
|
10
|
+
const g = Object.create(client);
|
|
11
|
+
const request = async (...args) => redactAgents(await client.request(...args), hidden);
|
|
12
|
+
g.request = request;
|
|
13
|
+
g.get = ((path, query) => request("GET", path, { query }));
|
|
14
|
+
g.post = ((path, body) => request("POST", path, { body: body ?? {} }));
|
|
15
|
+
g.patch = ((path, body) => request("PATCH", path, { body }));
|
|
16
|
+
return g;
|
|
17
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export const MODES = ["api", "full", "agent"];
|
|
2
|
+
export const isMode = (v) => typeof v === "string" && MODES.includes(v);
|
|
3
|
+
export const isAgentMode = (v) => v === "off" || isMode(v);
|
|
4
|
+
export const DEFAULT_ACCOUNT_POLICY = { mode: "full", agents: {} };
|
|
5
|
+
export const DEFAULT_SESSION_POLICY = { label: null, tools: null, agents: null };
|
|
6
|
+
/** Hands work to an agent. Everything else that changes something is direct control of Paperclip. */
|
|
7
|
+
const AGENT_WORK = new Set(["paperclip_create_issue", "paperclip_update_issue", "paperclip_comment_issue", "paperclip_wake_agent"]);
|
|
8
|
+
/** Which surface a call is on. The raw API tool is read for GET and direct control for anything else. */
|
|
9
|
+
export function surfaceOf(tool, input = {}) {
|
|
10
|
+
if (tool.name === "paperclip_api_request")
|
|
11
|
+
return input.method === "GET" ? "read" : "api";
|
|
12
|
+
if (tool.access === "read")
|
|
13
|
+
return "read";
|
|
14
|
+
return AGENT_WORK.has(tool.name) ? "agent" : "api";
|
|
15
|
+
}
|
|
16
|
+
/** The surface a tool is on when called in its usual way (what the control room shows). */
|
|
17
|
+
export const surfaceOfTool = (tool) => surfaceOf(tool, {});
|
|
18
|
+
export const surfaceAllowed = (mode, surface) => surface === "read" || mode === "full" || (mode === "api" && surface === "api") || (mode === "agent" && surface === "agent");
|
|
19
|
+
export const MODE_LABEL = { api: "API only", full: "Full", agent: "Agent only", off: "Off" };
|
|
20
|
+
// ───────────── validation of what the manage API receives ─────────────
|
|
21
|
+
export class PolicyInputError extends Error {
|
|
22
|
+
}
|
|
23
|
+
const ID = /^[A-Za-z0-9._:-]{1,80}$/;
|
|
24
|
+
const MAX_AGENTS = 500;
|
|
25
|
+
/** A strict, bounded copy of untrusted input. Unknown keys are dropped; anything invalid is refused. */
|
|
26
|
+
export function parseAccountPolicy(raw) {
|
|
27
|
+
const o = (raw && typeof raw === "object" ? raw : {});
|
|
28
|
+
const mode = o.mode === undefined ? DEFAULT_ACCOUNT_POLICY.mode : o.mode;
|
|
29
|
+
if (!isMode(mode))
|
|
30
|
+
throw new PolicyInputError("mode must be api, full or agent");
|
|
31
|
+
const agents = {};
|
|
32
|
+
const src = o.agents === undefined ? {} : o.agents;
|
|
33
|
+
if (!src || typeof src !== "object" || Array.isArray(src))
|
|
34
|
+
throw new PolicyInputError("agents must be an object of agent id to mode");
|
|
35
|
+
const entries = Object.entries(src);
|
|
36
|
+
if (entries.length > MAX_AGENTS)
|
|
37
|
+
throw new PolicyInputError(`at most ${MAX_AGENTS} agents`);
|
|
38
|
+
for (const [id, m] of entries) {
|
|
39
|
+
if (!ID.test(id))
|
|
40
|
+
throw new PolicyInputError("invalid agent id");
|
|
41
|
+
if (!isAgentMode(m))
|
|
42
|
+
throw new PolicyInputError("an agent's mode must be api, full, agent or off");
|
|
43
|
+
if (m !== mode)
|
|
44
|
+
agents[id] = m; // an override equal to the default is no override
|
|
45
|
+
}
|
|
46
|
+
return { mode, agents };
|
|
47
|
+
}
|
|
48
|
+
export function parseSessionPolicy(raw, knownTools) {
|
|
49
|
+
const o = (raw && typeof raw === "object" ? raw : {});
|
|
50
|
+
let label = null;
|
|
51
|
+
if (o.label !== undefined && o.label !== null) {
|
|
52
|
+
if (typeof o.label !== "string")
|
|
53
|
+
throw new PolicyInputError("label must be text");
|
|
54
|
+
label = o.label.replace(/\s+/g, " ").trim().slice(0, 60) || null;
|
|
55
|
+
}
|
|
56
|
+
const list = (v, what, ok) => {
|
|
57
|
+
if (v === undefined || v === null)
|
|
58
|
+
return null;
|
|
59
|
+
if (!Array.isArray(v))
|
|
60
|
+
throw new PolicyInputError(`${what} must be a list, or null for all`);
|
|
61
|
+
if (v.length > MAX_AGENTS)
|
|
62
|
+
throw new PolicyInputError(`too many ${what}`);
|
|
63
|
+
const out = [...new Set(v.map(String))];
|
|
64
|
+
if (!out.every(ok))
|
|
65
|
+
throw new PolicyInputError(`invalid entry in ${what}`);
|
|
66
|
+
return out;
|
|
67
|
+
};
|
|
68
|
+
const tools = list(o.tools, "tools", (s) => knownTools.has(s));
|
|
69
|
+
const agents = list(o.agents, "agents", (s) => ID.test(s));
|
|
70
|
+
if (tools && tools.length === 0)
|
|
71
|
+
throw new PolicyInputError("choose at least one tool, or allow all");
|
|
72
|
+
if (agents && agents.length === 0)
|
|
73
|
+
throw new PolicyInputError("choose at least one agent, or apply to everyone");
|
|
74
|
+
return { label, tools, agents };
|
|
75
|
+
}
|
|
76
|
+
/** Stored JSON back to a policy, tolerating older or damaged rows by falling back to the safe default. */
|
|
77
|
+
export function readAccountPolicy(stored) {
|
|
78
|
+
try {
|
|
79
|
+
return parseAccountPolicy(typeof stored === "string" ? JSON.parse(stored) : stored);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { ...DEFAULT_ACCOUNT_POLICY, agents: {} };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function readSessionPolicy(stored, knownTools) {
|
|
86
|
+
if (stored === null || stored === undefined)
|
|
87
|
+
return { ...DEFAULT_SESSION_POLICY };
|
|
88
|
+
try {
|
|
89
|
+
return parseSessionPolicy(typeof stored === "string" ? JSON.parse(stored) : stored, knownTools);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { ...DEFAULT_SESSION_POLICY };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const CONTROL_ROOM = "Change it in the Papercliped page inside Paperclip.";
|
|
96
|
+
/** Agent ids a call names, wherever the tool takes them (a target, an assignee, an owner, or a filter). */
|
|
97
|
+
export function targetsOf(input) {
|
|
98
|
+
const ids = [input.agentId, input.assigneeAgentId, input.ownerAgentId].filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
99
|
+
return [...new Set(ids.map((s) => s.trim()))];
|
|
100
|
+
}
|
|
101
|
+
/** Tools that act on an issue, whose agent is whoever it is assigned to (not named in the call). */
|
|
102
|
+
const BY_ISSUE = new Set(["paperclip_get_issue", "paperclip_update_issue", "paperclip_comment_issue"]);
|
|
103
|
+
/** Tools a session limited to selected agents may still use without naming an agent. */
|
|
104
|
+
const OPEN_TO_LIMITED = new Set(["papercliped_service_status", "paperclip_list_companies", "paperclip_list_agents"]);
|
|
105
|
+
/** Does this call need the issue's current assignee looked up before it can be judged? */
|
|
106
|
+
export function needsIssueAgent(p, tool) {
|
|
107
|
+
return BY_ISSUE.has(tool.name) && (p.session.agents !== null || Object.keys(p.account.agents).length > 0);
|
|
108
|
+
}
|
|
109
|
+
/** Agents Papercliped may not touch at all: hidden from every result, refused as a target. */
|
|
110
|
+
export const hiddenAgents = (a) => Object.entries(a.agents).filter(([, m]) => m === "off").map(([id]) => id);
|
|
111
|
+
/**
|
|
112
|
+
* Decide one call. `issueAgent` is the assignee of the issue the call names (null = unassigned, undefined = not looked up).
|
|
113
|
+
* Returns null when the call may go ahead, or why not.
|
|
114
|
+
*/
|
|
115
|
+
export function evaluate(p, tool, input, issueAgent) {
|
|
116
|
+
const { account, session } = p;
|
|
117
|
+
const label = tool.title || tool.name;
|
|
118
|
+
if (session.tools && !session.tools.includes(tool.name)) {
|
|
119
|
+
return { code: "tool_not_allowed", message: `This session isn't allowed to use "${label}". ${CONTROL_ROOM}` };
|
|
120
|
+
}
|
|
121
|
+
const surface = surfaceOf(tool, input);
|
|
122
|
+
const targets = targetsOf(input);
|
|
123
|
+
if (typeof issueAgent === "string")
|
|
124
|
+
targets.push(issueAgent);
|
|
125
|
+
for (const a of new Set(targets)) {
|
|
126
|
+
const eff = account.agents[a] ?? account.mode;
|
|
127
|
+
if (eff === "off")
|
|
128
|
+
return { code: "agent_off", message: `Papercliped isn't allowed to use that agent. ${CONTROL_ROOM}` };
|
|
129
|
+
if (!surfaceAllowed(eff, surface))
|
|
130
|
+
return { code: "mode_blocks", message: `That agent is set to "${MODE_LABEL[eff]}", which doesn't allow "${label}". ${CONTROL_ROOM}` };
|
|
131
|
+
if (session.agents && !session.agents.includes(a))
|
|
132
|
+
return { code: "agent_not_in_session", message: `This session only applies to selected agents, and that one isn't included. ${CONTROL_ROOM}` };
|
|
133
|
+
}
|
|
134
|
+
if (targets.length === 0 && !surfaceAllowed(account.mode, surface)) {
|
|
135
|
+
return { code: "mode_blocks", message: `Papercliped is set to "${MODE_LABEL[account.mode]}", which doesn't allow "${label}". ${CONTROL_ROOM}` };
|
|
136
|
+
}
|
|
137
|
+
if (session.agents && targets.length === 0 && !OPEN_TO_LIMITED.has(tool.name)) {
|
|
138
|
+
const why = BY_ISSUE.has(tool.name) || tool.name === "paperclip_create_issue" ? "needs an issue assigned to one of its agents" : "looks across every agent";
|
|
139
|
+
return { code: "needs_all_agents", message: `This session only applies to selected agents, and "${label}" ${why}. ${CONTROL_ROOM}` };
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/** For a session limited to selected agents: the agent list shows only those. */
|
|
144
|
+
export function narrowAgentList(result, allowed) {
|
|
145
|
+
const keep = (x) => !!x && typeof x === "object" && allowed.includes(String(x.id));
|
|
146
|
+
if (Array.isArray(result))
|
|
147
|
+
return result.filter(keep);
|
|
148
|
+
if (result && typeof result === "object" && Array.isArray(result.agents)) {
|
|
149
|
+
return { ...result, agents: result.agents.filter(keep) };
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
const AGENT_REFS = ["id", "agentId", "assigneeAgentId", "ownerAgentId", "executorAgentId", "createdByAgentId"];
|
|
154
|
+
/**
|
|
155
|
+
* Remove every record that belongs to one of the hidden agents, wherever it appears in a response: list items, nested
|
|
156
|
+
* rows, and keys named by agent id. Totals the server already summed are left as they are.
|
|
157
|
+
*/
|
|
158
|
+
export function redactAgents(value, hidden) {
|
|
159
|
+
if (hidden.size === 0)
|
|
160
|
+
return value;
|
|
161
|
+
const belongs = (x) => {
|
|
162
|
+
if (!x || typeof x !== "object")
|
|
163
|
+
return false;
|
|
164
|
+
const o = x;
|
|
165
|
+
return AGENT_REFS.some((k) => typeof o[k] === "string" && hidden.has(o[k]));
|
|
166
|
+
};
|
|
167
|
+
const walk = (v) => {
|
|
168
|
+
if (Array.isArray(v))
|
|
169
|
+
return v.filter((x) => !belongs(x)).map(walk);
|
|
170
|
+
if (v && typeof v === "object") {
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const [k, x] of Object.entries(v)) {
|
|
173
|
+
if (hidden.has(k) || belongs(x))
|
|
174
|
+
continue;
|
|
175
|
+
out[k] = walk(x);
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
return v;
|
|
180
|
+
};
|
|
181
|
+
return walk(value);
|
|
182
|
+
}
|
|
183
|
+
const takesAgent = (tool) => {
|
|
184
|
+
const shape = tool.schema.shape ?? {};
|
|
185
|
+
return "agentId" in shape || "assigneeAgentId" in shape || "ownerAgentId" in shape;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Could this tool ever run for this session? A tool that can never run is not advertised to the AI, so it doesn't try it.
|
|
189
|
+
* (Whether a particular call may run is `evaluate`'s answer.)
|
|
190
|
+
*/
|
|
191
|
+
export function canEverRun(p, tool) {
|
|
192
|
+
if (p.session.tools && !p.session.tools.includes(tool.name))
|
|
193
|
+
return false;
|
|
194
|
+
if (tool.name !== "paperclip_api_request") {
|
|
195
|
+
const surface = surfaceOfTool(tool);
|
|
196
|
+
const modes = [p.account.mode, ...Object.values(p.account.agents).filter((m) => m !== "off")];
|
|
197
|
+
if (!modes.some((m) => surfaceAllowed(m, surface)))
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (p.session.agents)
|
|
201
|
+
return OPEN_TO_LIMITED.has(tool.name) || BY_ISSUE.has(tool.name) || tool.name === "paperclip_create_issue" || takesAgent(tool);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
package/dist/execute.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { guardClient } from "./access/guard.js";
|
|
2
|
+
import { evaluate, hiddenAgents, narrowAgentList, needsIssueAgent } from "./access/policy.js";
|
|
1
3
|
import { PaperclipApiError } from "./client.js";
|
|
2
4
|
import { ToolInputError } from "./errors.js";
|
|
3
5
|
import { requiredScope, scopeAllows } from "./oauth/scopes.js";
|
|
@@ -53,8 +55,30 @@ export async function executeTool(client, name, rawInput, opts = {}) {
|
|
|
53
55
|
if (client.config.readOnly && mutation) {
|
|
54
56
|
return done({ ok: false, status: 403, error: "Bridge is in read-only mode (PAPERCLIP_READ_ONLY); this action was not sent to Paperclip." }, "read_only");
|
|
55
57
|
}
|
|
58
|
+
let runWith = client;
|
|
59
|
+
const policy = opts.policy;
|
|
60
|
+
if (policy) {
|
|
61
|
+
// An issue's agent is whoever it is assigned to; look it up once, only when the answer can change the decision.
|
|
62
|
+
let issueAgent;
|
|
63
|
+
if (needsIssueAgent(policy, tool) && typeof parsed.data.issueId === "string") {
|
|
64
|
+
try {
|
|
65
|
+
const issue = await client.get(`/issues/${encodeURIComponent(parsed.data.issueId)}`);
|
|
66
|
+
issueAgent = typeof issue?.assigneeAgentId === "string" ? issue.assigneeAgentId : null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
issueAgent = undefined; // can't tell: a session limited to selected agents is refused below, anything else lets the tool report the real error
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const denial = evaluate(policy, tool, parsed.data, issueAgent);
|
|
73
|
+
if (denial)
|
|
74
|
+
return done({ ok: false, status: 403, error: { message: denial.message, policy: denial.code } }, "policy_blocked");
|
|
75
|
+
runWith = guardClient(client, new Set(hiddenAgents(policy.account)));
|
|
76
|
+
}
|
|
56
77
|
try {
|
|
57
|
-
|
|
78
|
+
let result = await runTimed(timer, () => tool.run(runWith, parsed.data));
|
|
79
|
+
if (policy?.session.agents && tool.name === "paperclip_list_agents")
|
|
80
|
+
result = narrowAgentList(result, policy.session.agents);
|
|
81
|
+
return done({ ok: true, result });
|
|
58
82
|
}
|
|
59
83
|
catch (err) {
|
|
60
84
|
if (err instanceof ToolInputError)
|
package/dist/manage/routes.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PolicyInputError } from "../access/policy.js";
|
|
1
2
|
import { displayName } from "../accounts/alias.js";
|
|
2
3
|
const HEADERS = { "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "no-referrer" };
|
|
3
4
|
/** A DNS name or IPv4 literal, optionally with a port; undefined when it is anything else. */
|
|
@@ -92,8 +93,51 @@ export class ManageRoutes {
|
|
|
92
93
|
const link = await this.p.manageInstanceLabel(a.id);
|
|
93
94
|
return this.json(res, 200, { name: displayName(a), anonymous: !!a.anonymous, alias: a.alias ?? null, paperclip: link.label, connected: link.connected, createdAt: a.createdAt });
|
|
94
95
|
}
|
|
95
|
-
if (m === "GET" && route === "/connections")
|
|
96
|
-
|
|
96
|
+
if (m === "GET" && (route === "/connections" || route === "/sessions")) {
|
|
97
|
+
const list = await this.p.manageConnections(a.id);
|
|
98
|
+
return this.json(res, 200, route === "/sessions" ? { sessions: list } : { connections: list });
|
|
99
|
+
}
|
|
100
|
+
// The control room: the access switch, per-session limits, the tool list and recent calls.
|
|
101
|
+
if (route === "/policy" && (m === "GET" || m === "POST" || m === "PUT")) {
|
|
102
|
+
if (m === "GET")
|
|
103
|
+
return this.json(res, 200, await this.p.manageGetPolicy(a));
|
|
104
|
+
const b = await this.body(req);
|
|
105
|
+
if (!b)
|
|
106
|
+
return this.json(res, 400, { error: "Send JSON" });
|
|
107
|
+
try {
|
|
108
|
+
return this.json(res, 200, await this.p.manageSetPolicy(a, b));
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
return e instanceof PolicyInputError ? this.json(res, 400, { error: e.message }) : this.json(res, 500, { error: "Could not save the access settings" });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (m === "GET" && route === "/tools")
|
|
115
|
+
return this.json(res, 200, { tools: this.p.manageTools() });
|
|
116
|
+
if (m === "GET" && route === "/activity") {
|
|
117
|
+
const session = url.searchParams.get("session") ?? undefined;
|
|
118
|
+
if (session !== undefined && !/^[A-Za-z0-9_-]{1,80}$/.test(session))
|
|
119
|
+
return this.json(res, 400, { error: "Invalid session" });
|
|
120
|
+
const limit = Number.parseInt(url.searchParams.get("limit") ?? "50", 10);
|
|
121
|
+
return this.json(res, 200, { calls: await this.p.manageActivity(a, { session, limit: Number.isFinite(limit) ? limit : 50 }) });
|
|
122
|
+
}
|
|
123
|
+
const sess = /^\/sessions\/([A-Za-z0-9_-]{1,80})$/.exec(route);
|
|
124
|
+
if (sess && m === "POST") {
|
|
125
|
+
const b = await this.body(req);
|
|
126
|
+
if (!b)
|
|
127
|
+
return this.json(res, 400, { error: "Send JSON" });
|
|
128
|
+
const level = b.level === undefined ? undefined : b.level === "read" ? "paperclip:read" : b.level === "control" ? "paperclip:control" : null;
|
|
129
|
+
if (level === null)
|
|
130
|
+
return this.json(res, 400, { error: "level must be read or control" });
|
|
131
|
+
try {
|
|
132
|
+
const next = await this.p.manageSetSession(a, sess[1], { label: b.label, tools: b.tools, agents: b.agents, level });
|
|
133
|
+
return next ? this.json(res, 200, next) : this.json(res, 404, { error: "No such session" });
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
return e instanceof PolicyInputError ? this.json(res, 400, { error: e.message }) : this.json(res, 500, { error: "Could not save the session" });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (sess && m === "DELETE")
|
|
140
|
+
return (await this.p.manageRevoke(a, sess[1])) ? this.json(res, 200, { ok: true }) : this.json(res, 404, { error: "No such session" });
|
|
97
141
|
const c = /^\/connections\/([A-Za-z0-9_-]{1,80})$/.exec(route);
|
|
98
142
|
if (c && m === "POST") {
|
|
99
143
|
const b = await this.body(req);
|
package/dist/mcp.js
CHANGED
|
@@ -2,19 +2,20 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { PaperclipClient } from "./client.js";
|
|
3
3
|
import { readConfig } from "./config.js";
|
|
4
4
|
import { executeTool, renderForModel } from "./execute.js";
|
|
5
|
+
import { canEverRun } from "./access/policy.js";
|
|
5
6
|
import { requiredScope, scopeAllows } from "./oauth/scopes.js";
|
|
6
7
|
import { tools } from "./tools.js";
|
|
7
8
|
export { VERSION as SERVER_VERSION } from "./version.js";
|
|
8
9
|
import { VERSION } from "./version.js";
|
|
9
10
|
/** Tools above the caller's scope are not advertised (they would only fail). The raw API tool stays: its GETs need only read. */
|
|
10
|
-
export function visibleTools(scopes) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return
|
|
11
|
+
export function visibleTools(scopes, policy) {
|
|
12
|
+
const byScope = !scopes ? tools : tools.filter((t) => t.name === "paperclip_api_request" || scopeAllows(scopes, requiredScope(t)));
|
|
13
|
+
// Tools the session or the account switch can never allow are not advertised either.
|
|
14
|
+
return policy ? byScope.filter((t) => canEverRun(policy, t)) : byScope;
|
|
14
15
|
}
|
|
15
16
|
export function createMcpServer(config = readConfig(), client = new PaperclipClient(config), exec = {}) {
|
|
16
17
|
const server = new McpServer({ name: "papercliped", version: VERSION });
|
|
17
|
-
for (const tool of visibleTools(exec.scopes)) {
|
|
18
|
+
for (const tool of visibleTools(exec.scopes, exec.policy)) {
|
|
18
19
|
server.registerTool(tool.name, {
|
|
19
20
|
title: tool.title,
|
|
20
21
|
description: tool.description,
|
package/dist/oauth/pg-store.js
CHANGED
|
@@ -28,6 +28,7 @@ const toGrant = (r) => ({
|
|
|
28
28
|
createdAt: num(r.created_at),
|
|
29
29
|
lastUsedAt: num(r.last_used_at),
|
|
30
30
|
revoked: r.revoked,
|
|
31
|
+
policy: r.policy ?? null,
|
|
31
32
|
});
|
|
32
33
|
/**
|
|
33
34
|
* Postgres-backed Store (schema `bridge`, see migrations/001_init.sql). Safe to share between several bridge
|
|
@@ -174,7 +175,7 @@ export class PgStore {
|
|
|
174
175
|
}
|
|
175
176
|
// ───────────── accounts ─────────────
|
|
176
177
|
static toAccount(x) {
|
|
177
|
-
return { id: x.id, username: x.username, usernameKey: x.username_key, secretHash: x.secret_hash, createdAt: num(x.created_at), lastLoginAt: x.last_login_at == null ? null : num(x.last_login_at), disabled: x.disabled, anonymous: !!x.anonymous, alias: x.alias ?? null, beta: !!x.beta };
|
|
178
|
+
return { id: x.id, username: x.username, usernameKey: x.username_key, secretHash: x.secret_hash, createdAt: num(x.created_at), lastLoginAt: x.last_login_at == null ? null : num(x.last_login_at), disabled: x.disabled, anonymous: !!x.anonymous, alias: x.alias ?? null, beta: !!x.beta, policy: x.policy ?? null };
|
|
178
179
|
}
|
|
179
180
|
static toLink(x) {
|
|
180
181
|
return { accountId: x.account_id, instanceUrl: x.instance_url, paperclipUserId: x.paperclip_user_id, sealedCredential: x.sealed_credential, createdAt: num(x.created_at), connectedAt: num(x.connected_at), lastUsedAt: num(x.last_used_at), instanceLabel: x.instance_label ?? null };
|
|
@@ -199,6 +200,17 @@ export class PgStore {
|
|
|
199
200
|
async setGrantScopes(id, scopes) {
|
|
200
201
|
return ((await this.q("update bridge.grants set scopes = $2 where id = $1 and not revoked", [id, scopes])).rowCount ?? 0) === 1;
|
|
201
202
|
}
|
|
203
|
+
async setGrantPolicy(id, policy) {
|
|
204
|
+
return ((await this.q("update bridge.grants set policy = $2 where id = $1 and not revoked", [id, policy === null ? null : JSON.stringify(policy)])).rowCount ?? 0) === 1;
|
|
205
|
+
}
|
|
206
|
+
async setAccountPolicy(id, policy) {
|
|
207
|
+
return ((await this.q("update bridge.accounts set policy = $2 where id = $1", [id, JSON.stringify(policy)])).rowCount ?? 0) === 1;
|
|
208
|
+
}
|
|
209
|
+
async recentCallsForGrants(grantIds, limit) {
|
|
210
|
+
if (grantIds.length === 0)
|
|
211
|
+
return [];
|
|
212
|
+
return (await this.q("select * from bridge.audit_events where kind = 'tool' and grant_id = any($1) order by id desc limit $2", [[...grantIds], limit])).rows.map(PgStore.toRow);
|
|
213
|
+
}
|
|
202
214
|
async setAccountPrivacy(id, c) {
|
|
203
215
|
const cl = await this.pool.connect();
|
|
204
216
|
try {
|
package/dist/oauth/provider.js
CHANGED
|
@@ -8,6 +8,8 @@ import { UnsafeUrlError, createSafeFetch, parseInstanceUrl } from "../net/safe-f
|
|
|
8
8
|
import { Keyring, isValidCodeChallenge, randomToken, safeEqual, sha256Hex, verifyPkce } from "./crypto.js";
|
|
9
9
|
import { approvePage, choosePage, consentPage, errorPage, instancePage, scopePage, secretPage, usernamePage, welcomePage } from "./pages.js";
|
|
10
10
|
import { LoginError, PaperclipLogin } from "./paperclip-login.js";
|
|
11
|
+
import { parseAccountPolicy, parseSessionPolicy, readAccountPolicy, readSessionPolicy, surfaceOfTool } from "../access/policy.js";
|
|
12
|
+
import { tools as ALL_TOOLS } from "../tools.js";
|
|
11
13
|
import { VERSION } from "../version.js";
|
|
12
14
|
import { MemoryStore } from "./store.js";
|
|
13
15
|
import { SCOPES, grantableScopes, isScope, maxRank, normalizeScope, normalizeScopes, scopeAllows, scopesUpTo } from "./scopes.js";
|
|
@@ -16,6 +18,8 @@ const PENDING_TTL_MS = 10 * 60_000;
|
|
|
16
18
|
const CODE_TTL_MS = 60_000;
|
|
17
19
|
const MAX_PENDING = 5000;
|
|
18
20
|
const MAX_INSTANCE_ATTEMPTS = 5;
|
|
21
|
+
/** Every tool name, for checking a session's tool list. */
|
|
22
|
+
const KNOWN_TOOLS = new Set(ALL_TOOLS.map((t) => t.name));
|
|
19
23
|
/** The plugin offered on the instance step: one fixed package, pinned to this bridge's own version (both are released together). */
|
|
20
24
|
const PLUGIN_PACKAGE = "papercliped-paperclip-plugin";
|
|
21
25
|
/** Its id inside Paperclip (the manifest id), used to see whether it is already installed. */
|
|
@@ -305,7 +309,52 @@ export class OAuthProvider {
|
|
|
305
309
|
async manageConnections(accountId) {
|
|
306
310
|
return (await this.store.listAccountGrants(accountId))
|
|
307
311
|
.filter((g) => !g.revoked && g.clientId !== "paperclip-plugin")
|
|
308
|
-
.map((g) =>
|
|
312
|
+
.map((g) => {
|
|
313
|
+
const p = readSessionPolicy(g.policy, KNOWN_TOOLS);
|
|
314
|
+
return { id: g.id, app: g.clientName, label: p.label, level: levelOf(g.scopes), tools: p.tools, agents: p.agents, createdAt: g.createdAt, lastUsedAt: g.lastUsedAt };
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/** What the policy engine checks on every call for this grant: the account's switch plus this session's limits. */
|
|
318
|
+
async policyFor(grant) {
|
|
319
|
+
const account = grant.accountId ? await this.store.getAccount(grant.accountId) : undefined;
|
|
320
|
+
return { account: readAccountPolicy(account?.policy), session: readSessionPolicy(grant.policy, KNOWN_TOOLS) };
|
|
321
|
+
}
|
|
322
|
+
async manageGetPolicy(a) {
|
|
323
|
+
return readAccountPolicy((await this.store.getAccount(a.id))?.policy);
|
|
324
|
+
}
|
|
325
|
+
/** Replace the access switch and per-agent overrides. Throws PolicyInputError on anything invalid. Takes effect on the next call. */
|
|
326
|
+
async manageSetPolicy(a, raw) {
|
|
327
|
+
const next = parseAccountPolicy(raw);
|
|
328
|
+
if (!(await this.store.setAccountPolicy(a.id, next)))
|
|
329
|
+
throw new Error("No such account");
|
|
330
|
+
await this.event("updated", { accountId: a.id, username: displayName(a) }, `mode_${next.mode}`);
|
|
331
|
+
return next;
|
|
332
|
+
}
|
|
333
|
+
/** Change one session: its name, tools, agents and (optionally) its access level. False if it isn't this account's live session. */
|
|
334
|
+
async manageSetSession(a, grantId, change) {
|
|
335
|
+
const g = await this.store.getGrant(grantId);
|
|
336
|
+
if (!g || g.revoked || g.accountId !== a.id || g.clientId === "paperclip-plugin")
|
|
337
|
+
return null;
|
|
338
|
+
const before = readSessionPolicy(g.policy, KNOWN_TOOLS);
|
|
339
|
+
const next = parseSessionPolicy({ label: change.label === undefined ? before.label : change.label, tools: change.tools === undefined ? before.tools : change.tools, agents: change.agents === undefined ? before.agents : change.agents }, KNOWN_TOOLS);
|
|
340
|
+
if (!(await this.store.setGrantPolicy(g.id, next)))
|
|
341
|
+
return null;
|
|
342
|
+
if (change.level)
|
|
343
|
+
await this.store.setGrantScopes(g.id, scopesUpTo(change.level));
|
|
344
|
+
await this.event("updated", { accountId: a.id, username: displayName(a) }, "session_limits");
|
|
345
|
+
return next;
|
|
346
|
+
}
|
|
347
|
+
/** The tools, as the control room lists them: what each does and which surface it is on. */
|
|
348
|
+
manageTools() {
|
|
349
|
+
return ALL_TOOLS.map((t) => ({ name: t.name, title: t.title, description: t.description.slice(0, 240), access: t.access, surface: surfaceOfTool(t) }));
|
|
350
|
+
}
|
|
351
|
+
/** The newest tool calls through this account's sessions (names and outcomes only; never arguments or results). */
|
|
352
|
+
async manageActivity(a, opts) {
|
|
353
|
+
const grants = (await this.store.listAccountGrants(a.id)).filter((g) => g.clientId !== "paperclip-plugin");
|
|
354
|
+
const ids = opts.session ? grants.filter((g) => g.id === opts.session).map((g) => g.id) : grants.map((g) => g.id);
|
|
355
|
+
const byId = new Map(grants.map((g) => [g.id, readSessionPolicy(g.policy, KNOWN_TOOLS).label ?? g.clientName]));
|
|
356
|
+
const rows = await this.store.recentCallsForGrants(ids, Math.min(Math.max(1, opts.limit), 200));
|
|
357
|
+
return rows.map((r) => ({ at: r.at, tool: r.name, ok: r.ok, blocked: r.errorClass === "policy_blocked", status: r.status, error: r.errorClass, ms: r.totalMs, session: r.grantId, app: (r.grantId && byId.get(r.grantId)) || r.client }));
|
|
309
358
|
}
|
|
310
359
|
/** Change what a connected app may do: Read only or Full control. */
|
|
311
360
|
async manageSetLevel(a, grantId, level) {
|
package/dist/oauth/store.js
CHANGED
|
@@ -239,6 +239,26 @@ export class MemoryStore {
|
|
|
239
239
|
this.save();
|
|
240
240
|
return true;
|
|
241
241
|
}
|
|
242
|
+
async setGrantPolicy(id, policy) {
|
|
243
|
+
const g = this.d.grants[id];
|
|
244
|
+
if (!g || g.revoked)
|
|
245
|
+
return false;
|
|
246
|
+
g.policy = policy ? { ...policy, tools: policy.tools && [...policy.tools], agents: policy.agents && [...policy.agents] } : null;
|
|
247
|
+
this.save();
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
async setAccountPolicy(id, policy) {
|
|
251
|
+
const a = this.accts[id];
|
|
252
|
+
if (!a)
|
|
253
|
+
return false;
|
|
254
|
+
a.policy = { mode: policy.mode, agents: { ...policy.agents } };
|
|
255
|
+
this.save();
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
async recentCallsForGrants(grantIds, limit) {
|
|
259
|
+
const ids = new Set(grantIds);
|
|
260
|
+
return this.audit.filter((r) => r.kind === "tool" && r.grantId !== null && ids.has(r.grantId)).slice(-limit).reverse();
|
|
261
|
+
}
|
|
242
262
|
async setAccountPrivacy(id, c) {
|
|
243
263
|
const a = this.accts[id];
|
|
244
264
|
if (!a)
|
package/dist/public-api/types.js
CHANGED
|
@@ -11,4 +11,4 @@ export const PUBLIC_WINDOWS = ["1h", "24h", "7d"];
|
|
|
11
11
|
/** Why a sign-in or connect flow failed. Anything the bridge records outside this list is counted as `other`. */
|
|
12
12
|
export const AUTH_FAILURE_REASONS = ["bad_credentials", "rate_limited", "denied", "expired", "unreachable", "invalid_instance", "too_many_attempts", "other"];
|
|
13
13
|
/** Error classes of tool calls and protocol requests. Anything else is counted as `other`. */
|
|
14
|
-
export const ERROR_CLASSES = ["invalid_input", "insufficient_scope", "read_only", "upstream_4xx", "upstream_5xx", "upstream_unreachable", "rate_limited", "unauthorized", "internal", "other"];
|
|
14
|
+
export const ERROR_CLASSES = ["invalid_input", "insufficient_scope", "read_only", "upstream_4xx", "upstream_5xx", "upstream_unreachable", "rate_limited", "unauthorized", "policy_blocked", "internal", "other"];
|
package/dist/server.js
CHANGED
|
@@ -74,11 +74,11 @@ export function createHttpServer(config, http, opts = {}) {
|
|
|
74
74
|
catch {
|
|
75
75
|
return null; // unreadable credential, expired connection, or instance no longer allowed: an invalid token (the user reconnects)
|
|
76
76
|
}
|
|
77
|
-
return { kind: "oauth", grantId: grant.id, client: grant.clientName, userId: grant.userId, username: grant.username ?? undefined, instance, scopes: grant.scopes, paperclip };
|
|
77
|
+
return { kind: "oauth", grantId: grant.id, client: grant.clientName, userId: grant.userId, username: grant.username ?? undefined, instance, scopes: grant.scopes, paperclip, policy: await oauth.policyFor(grant) };
|
|
78
78
|
}
|
|
79
79
|
const execFor = (p) => p.kind === "static"
|
|
80
80
|
? { client: baseClient, exec: { actor: { id: "static-token" }, audit } }
|
|
81
|
-
: { client: p.paperclip, exec: { scopes: p.scopes, actor: { id: p.grantId, client: p.client, userId: p.userId, instance: p.instance, username: p.username }, audit } };
|
|
81
|
+
: { client: p.paperclip, exec: { scopes: p.scopes, policy: p.policy, actor: { id: p.grantId, client: p.client, userId: p.userId, instance: p.instance, username: p.username }, audit } };
|
|
82
82
|
const unauthorized = (res, invalid) => {
|
|
83
83
|
const challenge = oauth ? oauth.challengeHeader(invalid ? "invalid_token" : undefined) : "Bearer";
|
|
84
84
|
json(res, 401, { error: invalid ? "Invalid or expired token" : "Missing or invalid bearer token" }, { "WWW-Authenticate": challenge });
|
package/dist/site/site.js
CHANGED
|
@@ -20,6 +20,8 @@ const NAV = [
|
|
|
20
20
|
{ slug: "chatgpt", title: "ChatGPT setup" },
|
|
21
21
|
{ slug: "prompts", title: "Prompt gallery" },
|
|
22
22
|
{ slug: "permissions", title: "Permissions" },
|
|
23
|
+
{ slug: "control-room", title: "The control room" },
|
|
24
|
+
{ slug: "access-modes", title: "API only, Full or Agent only" },
|
|
23
25
|
{ slug: "manage", title: "Manage connections" },
|
|
24
26
|
{ slug: "paperclip-plugin", title: "Paperclip plugin" },
|
|
25
27
|
{ slug: "anonymous-mode", title: "Anonymous mode" },
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# The control room: audit, design and decisions
|
|
2
|
+
|
|
3
|
+
How v2.4.0 turns the Papercliped plugin into a control room inside Paperclip. Written alongside the build. The user-facing guides are `site/docs/control-room.md` and `site/docs/access-modes.md`; this page is for people changing the code.
|
|
4
|
+
|
|
5
|
+
Labels: **Observed** = read from Paperclip's installed source (2026.1001.0) or a live instance. **Decided** = a choice made for this release.
|
|
6
|
+
|
|
7
|
+
## 1. What was asked
|
|
8
|
+
- See every session (one connected AI app) and manage it from inside Paperclip: which tools it may use, and whether it applies to everyone or to selected agents.
|
|
9
|
+
- Follow Paperclip's native design, as a control room.
|
|
10
|
+
- Papercliped integrated into the app like agents: finely manageable access, **full control for every agent and the API by default**, and a main-page switch between **API only | Full | Agent only**.
|
|
11
|
+
- Audit Paperclip to see what needs to be integrated with what, and document as we build.
|
|
12
|
+
|
|
13
|
+
## 2. Audit of Paperclip (Observed)
|
|
14
|
+
|
|
15
|
+
### What a plugin can be
|
|
16
|
+
Paperclip 2026.1001.0 has a plugin system (alpha). The UI slot types are `page`, `detailTab`, `taskDetailView`, `dashboardWidget`, `sidebar`, `routeSidebar`, `sidebarPanel`, `projectSidebarItem`, `globalToolbarButton`, `appShellOverlay`, `toolbarButton`, `contextMenuItem`, `commentAnnotation`, `commentContextMenuItem`, `settingsPage`, `companySettingsPage`. Each needs a capability: `page` ← `ui.page.register`, `sidebar`/`routeSidebar` ← `ui.sidebar.register`, `detailTab` ← `ui.detailTab.register`, `dashboardWidget` ← `ui.dashboardWidget.register`.
|
|
17
|
+
|
|
18
|
+
- `routeSidebar` replaces the company menu while a route is open. This is how Paperclip's own agent page gets its grouped left menu, so it is the native way to build a control room.
|
|
19
|
+
- `detailTab` with `entityTypes: ["agent"]` adds a tab to every agent's page.
|
|
20
|
+
- A plugin worker reads agents with `ctx.agents.list` (capability `agents.read`). The host supplies the verified company with every action call (`context.companyId`); the page's own parameters are never trusted for it.
|
|
21
|
+
- A `routePath` is allowed only on `page`, `routeSidebar` and `companySettingsPage` slots. The 2.2.0 manifest put one on a `sidebar` slot, which is why Paperclip refused to install it (a bare `400`). The plugin's tests now run Paperclip's own `pluginManifestV1Schema`.
|
|
22
|
+
- Paperclip refuses to **upgrade** a plugin whose new version asks for capabilities the old one didn't have (an admin must approve). Moving from 2.2.x to 2.4 therefore means remove and reinstall. Documented in `site/docs/paperclip-plugin.md`.
|
|
23
|
+
- Install is `POST /api/plugins/install` (instance admin only), which runs `npm install <pkg>@<version> --ignore-scripts` into `~/.paperclip/plugins`, then validates the manifest (schema, API version, capabilities, route conflicts, minimum host version) and registers it.
|
|
24
|
+
|
|
25
|
+
### What Paperclip already has that overlaps
|
|
26
|
+
- **Tool access** (`/companies/:id/tools/*`, `/tool-connections`, `/tool-profiles`, grants and delegations) and a **tool gateway** (`/tool-gateway/*`, MCP gateways). These govern the tools *agents* use *outward* (GitHub, Google, remote MCP connectors). Papercliped is the opposite direction: AI apps controlling Paperclip *inward*. **Decided:** do not reuse or mirror them; they model a different thing, and a plugin can't extend them. The per-agent **Tools** tab in Paperclip's own UI (`AgentToolsTab`) is the closest native pattern, and is why Papercliped's per-agent control lives on the agent's own page too.
|
|
27
|
+
- **Agent permissions** (`PATCH /agents/:id/permissions`), **authorization grants and policies** (capabilities `authorization.*`). These are Paperclip-side permissions for principals inside Paperclip. **Decided:** not used. Papercliped's limits are enforced by the bridge, which every AI call passes through, so they hold for Claude, ChatGPT and any MCP client regardless of Paperclip's version, and need no extra capability.
|
|
28
|
+
- **Agent sessions** (`agent.sessions.*`, `task-sessions`, `runtime-state`). These are an agent's own runtime conversations. They are not Papercliped sessions. In the control room a **session** always means a connected AI app (an OAuth grant).
|
|
29
|
+
|
|
30
|
+
### Native design (Observed from `ui/` and a live page)
|
|
31
|
+
shadcn-style components on Tailwind with CSS variables (`--background`, `--card`, `--border`, `--primary`, `--sidebar-accent`, …), light and dark via a `.dark` class, Inter, Lucide icons at 16px. The plugin uses Paperclip's **own compiled classes** (every class used is checked against the live stylesheet, see `plugin/src/ui/look.ts`), the SDK's `StatusBadge`, `MetricCard`, `DataTable` and toast, and Lucide icons. On touch screens Paperclip forces buttons to at least 44px, so controls size themselves from their buttons instead of a fixed height.
|
|
32
|
+
|
|
33
|
+
## 3. Design
|
|
34
|
+
|
|
35
|
+
### Sessions
|
|
36
|
+
A **session** is one OAuth grant (one connected AI app). `Grant.policy` (new, nullable JSON) holds `{ label, tools, agents }`; `null` for `tools` or `agents` means "all". The access **level** stays in `grant.scopes`.
|
|
37
|
+
|
|
38
|
+
### The access switch
|
|
39
|
+
`Account.policy` (new, nullable JSON) holds `{ mode: "api"|"full"|"agent", agents: { [agentId]: "api"|"full"|"agent"|"off" } }`. No row means Full for every agent, so nothing changes for anyone until they choose.
|
|
40
|
+
|
|
41
|
+
### Tool surfaces (`src/access/policy.ts`)
|
|
42
|
+
Every tool is exactly one of **read** (any read, and `GET` through the raw API tool), **agent** (create/update/comment on issues, wake agent: it hands work to agents), or **api** (every other change: pause, resume, clear error, budget, terminate, goals, approvals, non-`GET` raw API). A mode allows `read` plus: `api` → api, `agent` → agent, `full` → both. A test ties `site/docs/access-modes.md` and the generated tool reference to this classification.
|
|
43
|
+
|
|
44
|
+
### Enforcement order (`evaluate`, called from `executeTool`)
|
|
45
|
+
1. Session tool list. 2. For each agent the call is about (named, assigned or owner, or the assignee of the named issue): Off → refuse; the agent's (or default) mode must allow the surface; the session's agent list must include it. 3. With no agent named, the default mode must allow the surface. 4. A session limited to selected agents may only use tools that name an agent (plus companies, agents list and service status), because every other view looks across all agents. The access level (scopes) is checked before all of this, unchanged.
|
|
46
|
+
|
|
47
|
+
Refusals are `403` with `{ message, policy: <code> }`, audited as `policy_blocked`, and never reach Paperclip. The message names the setting and where to change it.
|
|
48
|
+
|
|
49
|
+
### Hiding agents (`src/access/guard.ts`)
|
|
50
|
+
For agents set to **Off**, the Paperclip client handed to every tool is wrapped so each response has those agents' records removed (list items, rows keyed by agent id, anything referencing them), **before** the tool builds its answer. A report built from that client cannot name a hidden agent because it never received one. Server-side totals are left as the server computed them (documented). For a session limited to selected agents, `paperclip_list_agents` is narrowed to those agents.
|
|
51
|
+
|
|
52
|
+
An issue's agent is not named in the call, so for sessions with limits or accounts with per-agent settings the bridge looks the issue up once (`GET /issues/:id`) before deciding; if that fails, a limited session is refused and anyone else lets the tool report the real error.
|
|
53
|
+
|
|
54
|
+
### What the AI is shown
|
|
55
|
+
`visibleTools(scopes, policy)` drops tools that can never run for the session (not in its tool list; no mode allows the surface; tools that look across all agents for a limited session). Tools that *might* run for some agent stay.
|
|
56
|
+
|
|
57
|
+
### Management API (`src/manage/routes.ts`)
|
|
58
|
+
`GET /sessions`, `POST|DELETE /sessions/:id`, `GET|POST|PUT /policy`, `GET /tools`, `GET /activity`. Input is parsed strictly (`parseAccountPolicy`, `parseSessionPolicy`): unknown tool names, empty lists, bad ids and over-long lists are refused; damaged stored JSON falls back to the safe default instead of failing a call. Activity is read from the audit table by grant id and never contains arguments or results.
|
|
59
|
+
|
|
60
|
+
### Plugin (`plugin/`)
|
|
61
|
+
New worker actions: `sessions`, `setSession`, `policy`, `setPolicy`, `tools`, `activity`, `agents`. Five slots: `sidebar` (entry), `page` (the control room), `routeSidebar` (its menu), `detailTab` on agents, `dashboardWidget`. Navigation is `/papercliped?s=<section>&id=<thing>` so links, back and reload work. The access switch is a three-position vertical switch with a sliding knob (arrow keys, announced as a radio group, reduced-motion respected); the same control is reused compactly for agents.
|
|
62
|
+
|
|
63
|
+
### Database
|
|
64
|
+
`007_control_room.sql`: `accounts.policy jsonb`, `grants.policy jsonb`. No backfill. The panel views expose neither column. The generated `docs/supabase-schema.sql` is regenerated and a test fails if it is stale.
|
|
65
|
+
|
|
66
|
+
## 4. How it was verified
|
|
67
|
+
- **Rule engine**: unit tests for every mode, per-agent override, Off, session tool and agent lists, issue assignee, redaction, validation and damaged-row fallback (`test/policy.test.ts`).
|
|
68
|
+
- **End to end through the real server** with a fake Paperclip: switching modes changes the next call, nothing blocked reaches Paperclip, Off agents vanish from lists and reports, limited sessions, another account is never affected, activity includes blocked calls and no arguments (`test/control-room.test.ts`).
|
|
69
|
+
- **Postgres**: the whole suite (memory, Postgres, and the least-privilege role) ran against a real PostgreSQL 16, including the new store methods.
|
|
70
|
+
- **Plugin**: handlers against a fake bridge, the SDK test harness (capabilities are enforced there), and the UI (`plugin/test`).
|
|
71
|
+
- **Paperclip's own validators**: the built manifest passes `pluginManifestV1Schema` and the host's capability validator, run on the live instance's installed code.
|
|
72
|
+
- **Visual**: every page, the agent tab and the widget were rendered with Paperclip's real stylesheet in light, dark and phone width.
|
|
73
|
+
|
|
74
|
+
## 5. Known limits and next steps
|
|
75
|
+
- Limits apply to calls through Papercliped. The Paperclip key Papercliped holds still has the account's access (see `docs/SECURITY.md`); a bridge compromise bypasses them.
|
|
76
|
+
- Server-summed totals include Off agents.
|
|
77
|
+
- Activity is capped at 200 rows per request and, on the hosted service, kept 30 days (`BRIDGE_AUDIT_RETENTION_DAYS`).
|
|
78
|
+
- The plugin could not be installed into a live Paperclip with these slots before release; the manifest was validated against the live host's code instead. First live install after release should be checked: left menu replaces the company menu on `/papercliped`, the agent tab appears, the widget can be added.
|
|
79
|
+
- Paperclip's plugin API is alpha; `routeSidebar` and `detailTab` behaviour may change.
|
package/docs/SECURITY.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Read this before running the bridge for people other than yourself.
|
|
4
4
|
|
|
5
5
|
## What you are holding
|
|
6
|
-
For every connected user the bridge stores a **Paperclip board API key**. In Paperclip that key is bound to the *user*, not to a company, and carries **all** of that user's board power (verified in Paperclip's source: `board_api_keys` has no scope column, and the challenge's `requestedCompanyId` is only used to attribute activity-log entries, not to restrict the key). The bridge's two access levels
|
|
6
|
+
For every connected user the bridge stores a **Paperclip board API key**. In Paperclip that key is bound to the *user*, not to a company, and carries **all** of that user's board power (verified in Paperclip's source: `board_api_keys` has no scope column, and the challenge's `requestedCompanyId` is only used to attribute activity-log entries, not to restrict the key). The bridge's limits (the two access levels, the API only / Full / Agent only switch, per-agent settings and per-session tool and agent lists, see `docs/CONTROL-ROOM.md`) limit what *connected AI clients* may do through the bridge. They do **not** limit what the stored key can do if the bridge itself is compromised.
|
|
7
7
|
|
|
8
8
|
> **Headline risk:** a full compromise of the running bridge (code execution on the Render service *and* access to its environment) exposes every connected user's Paperclip with full board access, until those keys are revoked. Everything below reduces the likelihood or blast radius; none of it removes that risk. Until Paperclip offers scoped/short-lived keys, tell users plainly, encourage a dedicated Paperclip user for the connection, and keep the user base small enough to be comfortable with that.
|
|
9
9
|
|
package/docs/SITEMAP.md
CHANGED
|
@@ -29,6 +29,8 @@ One service, routed by host (`PUBLIC_HOSTS`, `src/web/`): `papercliped.co` the w
|
|
|
29
29
|
| `/docs/manage` | Manage connections |
|
|
30
30
|
| `/docs/paperclip-plugin` | Paperclip plugin |
|
|
31
31
|
| `/docs/permissions` | Permissions |
|
|
32
|
+
| `/docs/control-room` | The control room |
|
|
33
|
+
| `/docs/access-modes` | API only, Full or Agent only |
|
|
32
34
|
| `/docs/anonymous-mode` | Anonymous mode |
|
|
33
35
|
| `/docs/public-api` | Public API and status |
|
|
34
36
|
| `/docs/security` | Security |
|
|
@@ -54,7 +56,7 @@ One service, routed by host (`PUBLIC_HOSTS`, `src/web/`): `papercliped.co` the w
|
|
|
54
56
|
| Path | Purpose |
|
|
55
57
|
| --- | --- |
|
|
56
58
|
| `GET /manage` | Old address: redirects to `/docs/paperclip-plugin` |
|
|
57
|
-
| `/api/manage/*` | JSON API used by the Paperclip plugin (bearer token only, no cookies): `plugin-link/sign-in`, `me`, `connections`, `connections/:id`, `privacy`, `plugin-links`, `plugin-links/:id`, `secret/rotate`, `paperclip/disconnect`, `account/delete` (the last three need the secret key again) |
|
|
59
|
+
| `/api/manage/*` | JSON API used by the Paperclip plugin (bearer token only, no cookies): `plugin-link/sign-in`, `me`, `connections`, `connections/:id`, `sessions`, `sessions/:id`, `policy`, `tools`, `activity`, `privacy`, `plugin-links`, `plugin-links/:id`, `secret/rotate`, `paperclip/disconnect`, `account/delete` (the last three need the secret key again) |
|
|
58
60
|
| Domain section in the plugin **(planned v3)** | Pro members: address, tunnel status, token, port, release |
|
|
59
61
|
|
|
60
62
|
## 4. AI client endpoints
|
package/docs/supabase-schema.sql
CHANGED
|
@@ -274,6 +274,17 @@ update bridge.grants
|
|
|
274
274
|
set scopes = array(select distinct s from unnest(array_replace(scopes, 'paperclip:admin', 'paperclip:control')) as s order by s desc)
|
|
275
275
|
where 'paperclip:admin' = any(scopes);
|
|
276
276
|
|
|
277
|
+
-- ───── 007_control_room.sql ─────
|
|
278
|
+
-- Control room: who may do what through Papercliped.
|
|
279
|
+
-- accounts.policy the access switch (api | full | agent) and per-agent overrides, as JSON. Null = Full for every agent.
|
|
280
|
+
-- grants.policy one session's limits (a name, which tools, which agents), as JSON. Null = no limits beyond its access level.
|
|
281
|
+
-- The app validates both on the way in and falls back to the safe default on anything it can't read, so old rows need no backfill.
|
|
282
|
+
alter table bridge.accounts add column if not exists policy jsonb;
|
|
283
|
+
alter table bridge.grants add column if not exists policy jsonb;
|
|
284
|
+
|
|
285
|
+
-- Audit rows now also record calls the policy refused (error_class = policy_blocked).
|
|
286
|
+
comment on column bridge.audit_events.error_class is 'invalid_input | insufficient_scope | read_only | policy_blocked | upstream_4xx | upstream_5xx | upstream_unreachable | rate_limited | unauthorized | internal';
|
|
287
|
+
|
|
277
288
|
-- ───── migration bookkeeping ─────
|
|
278
289
|
create table if not exists bridge.schema_migrations (version text primary key, applied_at timestamptz not null default now());
|
|
279
290
|
alter table bridge.schema_migrations enable row level security;
|
|
@@ -283,6 +294,6 @@ do $$ declare r text; begin
|
|
|
283
294
|
if exists (select 1 from pg_roles where rolname = r) then execute format('revoke all on bridge.schema_migrations from %I', r); end if;
|
|
284
295
|
end loop;
|
|
285
296
|
end $$;
|
|
286
|
-
insert into bridge.schema_migrations (version) values ('001_init.sql'), ('002_audit.sql'), ('003_accounts.sql'), ('004_privacy_panel.sql'), ('005_beta.sql'), ('006_two_levels.sql') on conflict (version) do nothing;
|
|
297
|
+
insert into bridge.schema_migrations (version) values ('001_init.sql'), ('002_audit.sql'), ('003_accounts.sql'), ('004_privacy_panel.sql'), ('005_beta.sql'), ('006_two_levels.sql'), ('007_control_room.sql') on conflict (version) do nothing;
|
|
287
298
|
|
|
288
299
|
commit;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Control room: who may do what through Papercliped.
|
|
2
|
+
-- accounts.policy the access switch (api | full | agent) and per-agent overrides, as JSON. Null = Full for every agent.
|
|
3
|
+
-- grants.policy one session's limits (a name, which tools, which agents), as JSON. Null = no limits beyond its access level.
|
|
4
|
+
-- The app validates both on the way in and falls back to the safe default on anything it can't read, so old rows need no backfill.
|
|
5
|
+
alter table bridge.accounts add column if not exists policy jsonb;
|
|
6
|
+
alter table bridge.grants add column if not exists policy jsonb;
|
|
7
|
+
|
|
8
|
+
-- Audit rows now also record calls the policy refused (error_class = policy_blocked).
|
|
9
|
+
comment on column bridge.audit_events.error_class is 'invalid_input | insufficient_scope | read_only | policy_blocked | upstream_4xx | upstream_5xx | upstream_unreachable | rate_limited | unauthorized | internal';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "papercliped",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Papercliped — connect Claude, ChatGPT and any MCP app to your Paperclip: control agents, sync, report. MCP server + hosted multi-tenant OAuth bridge + public stats API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# API only, Full or Agent only
|
|
2
|
+
|
|
3
|
+
One switch decides how AI apps may work with your Paperclip: **API only**, **Full** or **Agent only**. **Full** is the default for every agent. You set it in the [control room](/docs/control-room), and you can set any single agent differently (or turn it **Off**).
|
|
4
|
+
|
|
5
|
+
## What each position means
|
|
6
|
+
| Mode | AI apps can | AI apps can't |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| **API only** | Look at everything, and control Paperclip directly: pause and resume agents, set budgets, decide approvals, manage goals, send API requests | Hand work to agents |
|
|
9
|
+
| **Full** (default) | Everything: look, control directly, and work through agents | Nothing is held back by the switch |
|
|
10
|
+
| **Agent only** | Look at everything, and work through agents: create, assign and comment on issues, wake an agent | Control Paperclip directly |
|
|
11
|
+
|
|
12
|
+
The AI app's access **level** (Read only or Full control) still applies on top. Read only can never change anything, whatever the switch says.
|
|
13
|
+
|
|
14
|
+
## The three kinds of tool
|
|
15
|
+
Every tool is exactly one of these:
|
|
16
|
+
|
|
17
|
+
| Kind | Tools | Allowed in |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| **Look** | Everything that only reads: lists, details, reports, sync, waiting for an agent, and raw `GET` requests | Every mode |
|
|
20
|
+
| **Work through agents** | Create issue, Update issue, Comment on issue, Wake agent now | Full, Agent only |
|
|
21
|
+
| **Direct control** | Pause agent, Resume agent, Clear agent error, Set agent budget, Terminate agent, Create goal, Update goal, Decide approval, and raw API requests that aren't `GET` | Full, API only |
|
|
22
|
+
|
|
23
|
+
The full list, with what each tool does, is the [tool reference](/docs/tools).
|
|
24
|
+
|
|
25
|
+
## One agent at a time
|
|
26
|
+
Each agent can follow the default or be set apart:
|
|
27
|
+
|
|
28
|
+
- **API only**, **Full** or **Agent only** for just that agent. For example, keep the default **Full** but set one agent to **Agent only**: AI apps can give it work but can't pause or terminate it.
|
|
29
|
+
- **Off**: Papercliped can't see or touch that agent. It disappears from lists and reports, anything assigned to it is out of reach, and calls that name it are refused.
|
|
30
|
+
|
|
31
|
+
A call is judged by the agent it is about: the agent it names, the agent it assigns work to, or the agent an issue is assigned to. A call that isn't about one agent (for example "list goals") follows the default.
|
|
32
|
+
|
|
33
|
+
## One session at a time
|
|
34
|
+
Each connected app (a **session**) can also be limited, whatever the switch says:
|
|
35
|
+
|
|
36
|
+
- **Tools:** only the tools you tick.
|
|
37
|
+
- **Agents:** only the agents you choose. The session then works with those agents only, and can't use views across every agent (reports, the org chart, goals, approvals).
|
|
38
|
+
|
|
39
|
+
## How the layers combine
|
|
40
|
+
Papercliped checks, in this order, and refuses at the first thing that doesn't allow the call:
|
|
41
|
+
|
|
42
|
+
1. The session's **tool list** (if it has one).
|
|
43
|
+
2. The agent's **setting**: Off refuses; otherwise its mode must allow this kind of tool.
|
|
44
|
+
3. The session's **agent list** (if it has one).
|
|
45
|
+
4. The access **level** (Read only, Full control).
|
|
46
|
+
|
|
47
|
+
So a call goes through only if *every* layer allows it.
|
|
48
|
+
|
|
49
|
+
## What the AI app sees
|
|
50
|
+
- Tools that can **never** run for a session are left out of what it is shown, so it doesn't try them. A session limited to `List agents` and `Pause agent` sees exactly those two.
|
|
51
|
+
- A refused call returns a message that says which setting stopped it and where to change it. In the API the response is `403` with `{ "error": { "message": "...", "policy": "<code>" } }`:
|
|
52
|
+
|
|
53
|
+
| `policy` code | Meaning |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `tool_not_allowed` | The session's tool list doesn't include this tool |
|
|
56
|
+
| `mode_blocks` | The access switch (or that agent's setting) doesn't allow this kind of tool |
|
|
57
|
+
| `agent_off` | The agent is turned off |
|
|
58
|
+
| `agent_not_in_session` | The session applies to selected agents and this isn't one of them |
|
|
59
|
+
| `needs_all_agents` | The session applies to selected agents, and this view looks across every agent (or the issue has no agent to check) |
|
|
60
|
+
|
|
61
|
+
Blocked calls appear in [Activity](/docs/control-room) as **Blocked**, and count under `policy_blocked` in the aggregate error classes of the [public API](/docs/public-api).
|
|
62
|
+
|
|
63
|
+
## Examples
|
|
64
|
+
- *"I want Claude to run my agents but never change budgets or approvals."* Give that session **Tools: Work through agents + Look** (untick the Direct control tools), or set the switch to **Agent only**.
|
|
65
|
+
- *"I want a reviewer app that only works on two agents."* Open its session, choose **selected agents**, tick the two. It can look at them, wake them and comment on their issues, and nothing else.
|
|
66
|
+
- *"Keep my finance agent away from AI apps."* Agents → set it to **Off**.
|
|
67
|
+
- *"Pause everything fast, but no new work."* Set the switch to **API only**.
|
|
68
|
+
|
|
69
|
+
## Limits worth knowing
|
|
70
|
+
- These settings apply to calls made **through Papercliped**. They don't change what your Paperclip account can do in Paperclip itself, and the Paperclip key Papercliped holds still has your account's access (see [Security](/docs/security)). They limit the AI app, not the key.
|
|
71
|
+
- Totals the Paperclip server already summed (such as total spend) still include hidden agents. Lists, per-agent breakdowns and reports built from them don't.
|
|
72
|
+
- Changing a setting takes effect on the next request. A call already running isn't interrupted.
|
|
@@ -139,7 +139,7 @@ curl https://your-address/api/health
|
|
|
139
139
|
## 6. Connect Papercliped
|
|
140
140
|
|
|
141
141
|
1. Add the connector in your AI app. In Claude: **Settings → Connectors → Add custom connector**, address `{{MCP}}/mcp`.
|
|
142
|
-
2. Choose **Connect your Paperclip**, enter your public address (just the address, no path) and approve the request in your Paperclip.
|
|
142
|
+
2. Choose **Connect your Paperclip**, enter your public address (just the address, no path) and approve the request in your Paperclip. The box **Also install the Papercliped plugin in my Paperclip** is ticked by default; it installs the [Paperclip plugin](/docs/paperclip-plugin) and its [control room](/docs/control-room) for you if you are your Paperclip's instance admin.
|
|
143
143
|
3. Pick a username, save the secret key you're shown once, and choose **Read only** or **Full control**. Every screen: [Create your account](/docs/signup).
|
|
144
144
|
4. Send your first prompt: *"Catch me up on my Paperclip."* More in the [prompt gallery](/docs/prompts).
|
|
145
145
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# The control room
|
|
2
|
+
|
|
3
|
+
The control room is Papercliped inside Paperclip: one place to see every AI app connected to your Paperclip, what each is allowed to do, and what it has done. It opens from **Papercliped** in Paperclip's sidebar and uses Paperclip's own look, so it follows your theme.
|
|
4
|
+
|
|
5
|
+
You need the [Paperclip plugin](/docs/paperclip-plugin) installed and your Papercliped account linked. Everything here takes effect on the app's **next request**: nothing needs reconnecting.
|
|
6
|
+
|
|
7
|
+
## How it fits together
|
|
8
|
+
Three things decide what an AI app can do. Papercliped checks all three on every call, and the strictest one wins.
|
|
9
|
+
|
|
10
|
+
| Layer | Where you set it | Decides |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| **Access switch** | Overview, Agents, dashboard widget, agent tab | Whether AI apps may control Paperclip directly, work through agents, or both ([API only, Full or Agent only](/docs/access-modes)), for every agent or one agent at a time |
|
|
13
|
+
| **Session** | Sessions | Which tools one connected app may use, and which agents it applies to |
|
|
14
|
+
| **Level** | Sessions | Read only or Full control, as chosen when the app connected ([Permissions](/docs/permissions)) |
|
|
15
|
+
|
|
16
|
+
## The menu
|
|
17
|
+
While the control room is open, Paperclip's left menu is replaced by Papercliped's, the same way it is on an agent's page. **Back to Paperclip** at the top returns you.
|
|
18
|
+
|
|
19
|
+
### Overview
|
|
20
|
+
The front page. The **access switch** is a three-position switch: **API only**, **Full** (the default) and **Agent only**. Click a position, or use the arrow keys when the switch has focus. It saves at once. Beside it: how many sessions you have, how many agents, the calls made today and how many were blocked, a short list of things that need a look (a session that never made a call, an agent turned off, blocked calls, a Paperclip key that is not connected), and the latest calls.
|
|
21
|
+
|
|
22
|
+
### Sessions
|
|
23
|
+
Every AI app connected through Papercliped is a **session**. Open one to change:
|
|
24
|
+
|
|
25
|
+
- **Name.** Rename a session to tell two apps or two devices apart. Up to 60 characters.
|
|
26
|
+
- **Level.** Read only or Full control.
|
|
27
|
+
- **Tools.** *All tools* means every tool its level and the switch allow. Untick it to choose exactly which tools this session may use, grouped as **Look**, **Work through agents** and **Direct control**. When a session is Read only, tools that change things are greyed out with "Needs Full control".
|
|
28
|
+
- **Agents.** *Everyone* (including agents you add later), or **selected agents**. A session limited to selected agents works only with those agents: it can look at them, pause them, wake them, give them work, and use issues assigned to them. It cannot use views across every agent, such as reports, the org chart, goals and approvals, because those would show agents it doesn't apply to. The page says so when you choose it.
|
|
29
|
+
- **Recent calls** for just this session, and **Disconnect**.
|
|
30
|
+
|
|
31
|
+
Save with **Save changes** (it stays disabled until something changed and says what's wrong if a list is empty). **Discard** puts everything back.
|
|
32
|
+
|
|
33
|
+
### Agents
|
|
34
|
+
Papercliped follows the access switch for every agent. Here you can set any agent apart: **Default**, **API only**, **Full**, **Agent only** or **Off**.
|
|
35
|
+
|
|
36
|
+
**Off** hides the agent completely. AI apps can't see it, can't touch it, and don't see anything assigned to it or its spending in lists and reports. Use it for agents you never want an AI app near.
|
|
37
|
+
|
|
38
|
+
The default for every agent can also be changed from this page. Settings for agents that no longer exist (or are in another company) are listed so you can remove them.
|
|
39
|
+
|
|
40
|
+
### Tools
|
|
41
|
+
All tools an AI app can use, grouped by what they do, with what the switch allows right now and how many of your sessions can use each. Use it to answer "can my laptop session pause an agent?" without opening each session.
|
|
42
|
+
|
|
43
|
+
### Activity
|
|
44
|
+
The newest calls through your sessions: when, which session, which tool, and the result: **OK**, **Blocked** (by your settings) or **Failed** (with the reason, such as "Paperclip unreachable"). Filter by session, or to blocked and failed only. It refreshes every 20 seconds.
|
|
45
|
+
|
|
46
|
+
Activity shows *what was used and what happened*. It never records what was sent to a tool or what came back. On the hosted service it is kept for 30 days.
|
|
47
|
+
|
|
48
|
+
### Settings
|
|
49
|
+
Privacy (appear anonymously in the service's logs), the Paperclips linked to your account, and the account itself (a new secret key, disconnect your Paperclip, delete the account). These ask for your secret key again. See [Anonymous mode](/docs/anonymous-mode).
|
|
50
|
+
|
|
51
|
+
## In the rest of Paperclip
|
|
52
|
+
- **An "Papercliped" tab on every agent's page.** Set what AI apps may do with *this* agent, and see which sessions reach it.
|
|
53
|
+
- **A dashboard widget.** The access switch in small, with the number of sessions and blocked calls.
|
|
54
|
+
- **A sidebar entry**, with the paperclip icon, under your other sections.
|
|
55
|
+
|
|
56
|
+
## Good to know
|
|
57
|
+
- These settings limit what AI apps can do **through Papercliped**. They don't change what your own Paperclip account can do in Paperclip itself.
|
|
58
|
+
- A blocked call returns a plain message to the AI app saying which setting stopped it and where to change it, so it doesn't keep retrying. See [Limits and errors](/docs/limits-and-errors).
|
|
59
|
+
- Tools that can never run for a session (switched off by the access switch or left out of its tool list) are not shown to the AI app at all.
|
|
60
|
+
- Totals the Paperclip server already added up (for example total spend) still include hidden agents. Lists, reports by agent and anything built from them don't.
|
|
61
|
+
- New permissions need approval in Paperclip. If you installed an earlier version of the plugin, remove it and install it again from **Settings → Plugins** to get the control room (see [Paperclip plugin](/docs/paperclip-plugin)).
|
package/site/docs/faq.md
CHANGED
|
@@ -27,6 +27,12 @@ Yes: run it locally with `npx papercliped@latest`. The hosted bridge needs a pub
|
|
|
27
27
|
## Why does pausing an agent fail with 403?
|
|
28
28
|
Pause, resume, wake and approve need a Paperclip **board** token, and a connection at **Full control**. Check your level in the [Paperclip plugin](/docs/paperclip-plugin).
|
|
29
29
|
|
|
30
|
+
## Why was a call blocked, or a tool missing?
|
|
31
|
+
Your own settings. Open **Papercliped** in Paperclip: the switch (API only, Full, Agent only), an agent's setting, or the session's tool and agent lists decide what an AI app may do, and **Activity** shows each blocked call and the reason. See [API only, Full or Agent only](/docs/access-modes).
|
|
32
|
+
|
|
33
|
+
## Can I keep an agent away from AI apps?
|
|
34
|
+
Yes. In the [control room](/docs/control-room), open **Agents** and set it to **Off**: AI apps can't see it, touch it, or see what's assigned to it.
|
|
35
|
+
|
|
30
36
|
## How do I stop it?
|
|
31
37
|
Disconnect the app on the manage page, revoke the key in your Paperclip, or delete your account. It stops at once.
|
|
32
38
|
|
|
@@ -30,7 +30,7 @@ This starts a local server for one Paperclip, using `PAPERCLIP_API_URL` and `PAP
|
|
|
30
30
|
Any app that supports MCP can use Papercliped. See [Any AI app (MCP)](/docs/other-ai-apps).
|
|
31
31
|
|
|
32
32
|
## Inside your Paperclip
|
|
33
|
-
The Papercliped plugin for Paperclip is where you manage everything: connected apps,
|
|
33
|
+
The Papercliped plugin for Paperclip is where you manage everything: the [control room](/docs/control-room) for connected apps, what each may do and which agents it applies to, plus privacy and your account. See [Paperclip plugin](/docs/paperclip-plugin).
|
|
34
34
|
|
|
35
35
|
## ChatGPT
|
|
36
36
|
Papercliped publishes an OpenAPI description at `{{MCP}}/openapi.json` for GPT Actions. See [ChatGPT setup](/docs/chatgpt).
|
|
@@ -21,6 +21,7 @@ Tool errors come back to your AI app with a message it can read. Over HTTP (Chat
|
|
|
21
21
|
| --- | --- | --- | --- |
|
|
22
22
|
| `400` | `invalid_input` | The request didn't match the tool's inputs | Usually the AI retries with fixed input |
|
|
23
23
|
| `403` | `insufficient_scope` | The connection's level is too low for this tool | Switch the app to Full control in the [Paperclip plugin](/docs/paperclip-plugin) |
|
|
24
|
+
| `403` | `policy_blocked` | Your settings don't allow this call: the access switch, an agent's setting, or the session's tool or agent list. The error carries a `policy` code (see [API only, Full or Agent only](/docs/access-modes)) | Change the setting in the [control room](/docs/control-room) if you want it allowed |
|
|
24
25
|
| `403` | `read_only` | The bridge runs with `PAPERCLIP_READ_ONLY` | Ask the operator |
|
|
25
26
|
| `401`, `403`, `404`, `409`… | `upstream_4xx` | Paperclip refused (e.g. your Paperclip key was revoked: reconnect) | Check the message |
|
|
26
27
|
| `5xx` | `upstream_5xx` | Paperclip had an error | Try again later |
|
package/site/docs/manage-api.md
CHANGED
|
@@ -14,7 +14,14 @@ The token has no scopes: it can't call MCP, Actions or any Paperclip tool. It la
|
|
|
14
14
|
| --- | --- |
|
|
15
15
|
| `POST /plugin-link/sign-in` | Link: username + secret key → plugin token |
|
|
16
16
|
| `GET /me` | Name, anonymity, connected Paperclip |
|
|
17
|
-
| `GET /connections` | Connected apps with level and last use |
|
|
17
|
+
| `GET /connections` | Connected apps with level and last use (also `GET /sessions`, which is the same list under the name the control room uses) |
|
|
18
|
+
| `GET /sessions` | Sessions: each with `label`, `level`, `tools` (or `null` for all) and `agents` (or `null` for everyone) |
|
|
19
|
+
| `POST /sessions/:id` `{ "label", "tools", "agents", "level" }` | Change one session. Leave a field out to keep it; `null` clears a limit. Takes effect on the next request |
|
|
20
|
+
| `DELETE /sessions/:id` | Disconnect a session |
|
|
21
|
+
| `GET /policy` | The access switch and per-agent settings: `{ "mode": "api" \| "full" \| "agent", "agents": { "<agent id>": "api" \| "full" \| "agent" \| "off" } }` |
|
|
22
|
+
| `POST /policy` (or `PUT`) | Replace them with the same shape |
|
|
23
|
+
| `GET /tools` | Every tool with its `access` and its `surface` (`read`, `agent` or `api`) |
|
|
24
|
+
| `GET /activity?session=:id&limit=:n` | The newest tool calls (names and outcomes only), up to 200 |
|
|
18
25
|
| `POST /connections/:id` `{ "level": "read" \| "control" }` | Change an app's level |
|
|
19
26
|
| `DELETE /connections/:id` | Disconnect an app |
|
|
20
27
|
| `POST /privacy` `{ "anonymous": true }` | Turn anonymity on or off |
|
|
@@ -26,6 +33,10 @@ The token has no scopes: it can't call MCP, Actions or any Paperclip tool. It la
|
|
|
26
33
|
|
|
27
34
|
The last three need your secret key in the request.
|
|
28
35
|
|
|
36
|
+
A session's `tools` must be known tool names and `agents` agent ids; an empty list is refused (use `null` for "all"). Per-agent settings equal to the default are dropped. See [API only, Full or Agent only](/docs/access-modes).
|
|
37
|
+
|
|
38
|
+
The activity list holds, for each call: `at`, `tool`, `ok`, `blocked`, `status`, `error` (a coarse class such as `policy_blocked`), `ms`, `session` and `app`. It never holds what was sent to a tool or what came back.
|
|
39
|
+
|
|
29
40
|
## Errors
|
|
30
41
|
`400` bad input, `401` wrong username or key when linking, or the token is unknown, expired or revoked, `403` wrong secret key for a sensitive action, `404` no such connection or link (also for other people's), `429` too many requests.
|
|
31
42
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# Paperclip plugin
|
|
2
2
|
|
|
3
|
-
The Papercliped plugin
|
|
3
|
+
The Papercliped plugin puts Papercliped inside Paperclip. It adds the [control room](/docs/control-room): a **Papercliped** entry in the sidebar that opens a page where you see every connected AI app, choose what each may do and which agents it applies to, set [API only, Full or Agent only](/docs/access-modes) for every agent or one at a time, read what was used and what was blocked, and look after your account. It also adds a **Papercliped** tab on every agent's page and a widget for the dashboard.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The plugin manages your Papercliped account and the limits on AI apps. It reads your agents' names, roles and status to list them, and never changes anything in Paperclip. AI apps still connect through Papercliped as before.
|
|
6
6
|
|
|
7
7
|
## Install (Paperclip admin)
|
|
8
8
|
**Automatic (recommended).** When you connect your Paperclip from an AI app, the address step has a box, ticked by default: **Also install the Papercliped plugin in my Paperclip**. After you approve the sign-in, Papercliped asks your Paperclip to install it using Paperclip's own installer and your own approved key. It installs one fixed package, `papercliped-paperclip-plugin`, at the exact version that matches the service, and only when you are the Paperclip's instance admin. If you are not an admin, if it is already installed, or if your Paperclip can't install plugins, nothing happens and your connection works as usual. Untick the box to skip it.
|
|
9
9
|
|
|
10
10
|
**By hand.** Install the npm package `papercliped-paperclip-plugin` from **Settings → Plugins** in Paperclip (instance admin only). You can remove it there at any time, whichever way it was installed.
|
|
11
11
|
|
|
12
|
-
The plugin asks for these capabilities: store its own data, make outbound web requests (to Papercliped), and add a sidebar entry and a
|
|
12
|
+
The plugin asks for these capabilities: store its own data, make outbound web requests (to Papercliped), read agents (to list them), and add a sidebar entry, a page with its own menu, an agent tab and a dashboard widget.
|
|
13
|
+
|
|
14
|
+
**Updating from an earlier version.** Paperclip won't update a plugin to a version that asks for new permissions without an admin's approval, so go to **Settings → Plugins**, remove Papercliped, and install it again (or connect your Paperclip again with the install box ticked). You may need to link your account again; your Papercliped account, sessions and settings are kept at Papercliped and are not affected.
|
|
13
15
|
|
|
14
16
|
The bridge address defaults to `https://papercliped.co`. If you host your own bridge, change **Papercliped bridge URL** in the plugin's settings. It must be `https`.
|
|
15
17
|
|
|
@@ -22,13 +24,22 @@ Papercliped checks the key once and hands the plugin a link token. The key itsel
|
|
|
22
24
|
Lost your secret key? Connect from your AI app again and choose **Connect your Paperclip**: you keep your account and get a new key.
|
|
23
25
|
|
|
24
26
|
## What you can do
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
The [control room](/docs/control-room) has six sections:
|
|
28
|
+
|
|
29
|
+
- **Overview:** the access switch (API only, Full, Agent only), your numbers, and what needs a look.
|
|
30
|
+
- **Sessions:** every connected app. Name it, set its level, choose its tools and which agents it applies to, see its recent calls, or disconnect it. It applies on the app's next request.
|
|
31
|
+
- **Agents:** what Papercliped may do with each agent, including turning one off.
|
|
32
|
+
- **Tools:** every tool, by what it does and what your settings allow.
|
|
33
|
+
- **Activity:** recent calls, including the ones that were blocked.
|
|
34
|
+
- **Settings:**
|
|
35
|
+
- **Privacy:** appear anonymously in the service's logs, or switch back.
|
|
36
|
+
- **Linked Paperclips:** see every Paperclip linked to your account, unlink this one, or remove an old one.
|
|
37
|
+
- **Account** (asks for your secret key again):
|
|
38
|
+
- **Make a new secret key.** Shown once. The old key stops working and other linked Paperclips have to link again; this one stays linked.
|
|
39
|
+
- **Disconnect my Paperclip.** Papercliped forgets your Paperclip key and asks your Paperclip to revoke it. Every app stops working and the plugin unlinks. Your account stays.
|
|
40
|
+
- **Delete my account.** Removes your username, stored key and every connection. Type your username to confirm.
|
|
41
|
+
|
|
42
|
+
Outside the control room: the **Papercliped tab** on an agent's page sets what AI apps may do with that one agent, and the **dashboard widget** shows the access switch in small.
|
|
32
43
|
|
|
33
44
|
## Good to know
|
|
34
45
|
- The plugin runs as trusted code inside Paperclip, so install it only from the official package. The automatic install is limited to that one package and to the version that matches the service, and it is always your choice on the connect screen.
|
package/site/docs/permissions.md
CHANGED
|
@@ -7,6 +7,14 @@ You decide how much the AI may do each time you connect. There are two levels.
|
|
|
7
7
|
| **Read only** (default) | View agents, tasks, goals, costs and reports. Cannot change anything. |
|
|
8
8
|
| **Full control** | Every tool: everything above, plus pause, resume, wake or terminate agents, create and update issues and goals, comment, decide approvals, change budgets and send any API request. Run your Paperclip from anywhere, without opening the dashboard. |
|
|
9
9
|
|
|
10
|
+
## Beyond the two levels
|
|
11
|
+
The level is chosen when an app connects. After that, the [control room](/docs/control-room) gives you finer control, without reconnecting anything:
|
|
12
|
+
|
|
13
|
+
- **An access switch** for every agent: [API only, Full or Agent only](/docs/access-modes), and any agent can be set differently or turned off.
|
|
14
|
+
- **Per-session limits:** name a session, choose exactly which tools it may use, and choose whether it applies to everyone or only selected agents.
|
|
15
|
+
|
|
16
|
+
These only ever narrow what a level allows; they never grant more than the level.
|
|
17
|
+
|
|
10
18
|
## Good to know
|
|
11
19
|
- Papercliped enforces these levels itself on every call, and refuses anything outside the level you chose.
|
|
12
20
|
- Your Paperclip key has the access your Paperclip account has. The levels limit what the **AI app** can do through Papercliped; they do not shrink the key itself. That is why the key is stored encrypted and why you can revoke it at any time.
|
package/site/docs/public-api.md
CHANGED
|
@@ -47,7 +47,7 @@ curl "{{URL}}/api/public/v1/stats?window=24h"
|
|
|
47
47
|
|
|
48
48
|
**Sign-in failure reasons** (a fixed list): `bad_credentials`, `rate_limited`, `denied`, `expired`, `unreachable`, `invalid_instance`, `too_many_attempts`, `other`.
|
|
49
49
|
|
|
50
|
-
**Error classes** (a fixed list): `invalid_input`, `insufficient_scope`, `read_only`, `upstream_4xx`, `upstream_5xx`, `upstream_unreachable`, `rate_limited`, `unauthorized`, `internal`, `other`. Anything the service records outside these lists is counted as `other`, never shown as text.
|
|
50
|
+
**Error classes** (a fixed list): `invalid_input`, `insufficient_scope`, `read_only`, `policy_blocked`, `upstream_4xx`, `upstream_5xx`, `upstream_unreachable`, `rate_limited`, `unauthorized`, `internal`, `other`. Anything the service records outside these lists is counted as `other`, never shown as text.
|
|
51
51
|
|
|
52
52
|
## Series
|
|
53
53
|
Each bucket has `t` (start time), `requests`, `errors`, `faults`, `p95Ms`, `authCompleted`, `authFailed` and `newUsers`. Every bucket in the window is present, with zeros when nothing happened.
|
package/site/docs/tools.md
CHANGED
|
@@ -6,42 +6,42 @@ Raw API calls (`paperclip_api_request`) need Read only for GET and Full control
|
|
|
6
6
|
|
|
7
7
|
## Read only (17)
|
|
8
8
|
|
|
9
|
-
| Tool | What it does | Changes anything? |
|
|
10
|
-
| --- | --- | --- |
|
|
11
|
-
| **List companies** `paperclip_list_companies` | List the companies the credential can access. Use this first to find a companyId. | No |
|
|
12
|
-
| **Org chart** `paperclip_org_chart` | Return the company's full agent org tree (who reports to whom). | No |
|
|
13
|
-
| **List agents** `paperclip_list_agents` | List all agents in a company with status, role and budget/spend. | No |
|
|
14
|
-
| **Get agent** `paperclip_get_agent` | Get one agent including its chain of command and current status. | No |
|
|
15
|
-
| **List goals** `paperclip_list_goals` | List the goal hierarchy for a company. | No |
|
|
16
|
-
| **List projects** `paperclip_list_projects` | List projects in a company. | No |
|
|
17
|
-
| **List issues** `paperclip_list_issues` | List issues/tasks. status may be comma-separated, e.g. 'todo,in_progress'. | No |
|
|
18
|
-
| **Get issue** `paperclip_get_issue` | Get an issue with project, goal, ancestors and plan document. | No |
|
|
19
|
-
| **List approvals** `paperclip_list_approvals` | List approval requests (hires, plans, budget overrides). Filter client-side by status if needed. | No |
|
|
20
|
-
| **Sync snapshot** `paperclip_sync_snapshot` | One-call picture of the company right now: dashboard counters, every agent's status, and live runs. Call at the start of a session to sync with the agents. | No |
|
|
21
|
-
| **Changes since cursor** `paperclip_sync_changes` | Poll for what changed. Pass the `cursor` returned by the previous call (omit the first time to get recent history). Returns new activity entries and the next cursor. | No |
|
|
22
|
-
| **Wait for agent** `paperclip_wait_for_agent` | Block (polling) until an agent reaches one of the target statuses (default: idle or error) or the timeout passes. Use after waking an agent to follow it to completion. | No |
|
|
23
|
-
| **Status report** `paperclip_report_status` | Executive status report: agents, tasks, spend, pending approvals, agents needing attention. | No |
|
|
24
|
-
| **Cost report** `paperclip_report_costs` | Spend vs budget with per-agent and per-project breakdown. Defaults to the current month. | No |
|
|
25
|
-
| **Agent performance report** `paperclip_report_agent_performance` | Per-agent throughput (done / in progress / blocked), spend and cost per completed task. | No |
|
|
26
|
-
| **Activity digest** `paperclip_report_activity` | Digest of audit-log activity, optionally since an ISO timestamp (e.g. start of day). | No |
|
|
27
|
-
| **Papercliped service status** `papercliped_service_status` | Is the hosted Papercliped service working? Returns its status (ok, degraded, down), version and aggregate 24-hour numbers (requests, success rate, latency, sign-ins). Public data only; does not touch Paperclip. | No |
|
|
9
|
+
| Tool | What it does | Changes anything? | Works in |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| **List companies** `paperclip_list_companies` | List the companies the credential can access. Use this first to find a companyId. | No | Every mode |
|
|
12
|
+
| **Org chart** `paperclip_org_chart` | Return the company's full agent org tree (who reports to whom). | No | Every mode |
|
|
13
|
+
| **List agents** `paperclip_list_agents` | List all agents in a company with status, role and budget/spend. | No | Every mode |
|
|
14
|
+
| **Get agent** `paperclip_get_agent` | Get one agent including its chain of command and current status. | No | Every mode |
|
|
15
|
+
| **List goals** `paperclip_list_goals` | List the goal hierarchy for a company. | No | Every mode |
|
|
16
|
+
| **List projects** `paperclip_list_projects` | List projects in a company. | No | Every mode |
|
|
17
|
+
| **List issues** `paperclip_list_issues` | List issues/tasks. status may be comma-separated, e.g. 'todo,in_progress'. | No | Every mode |
|
|
18
|
+
| **Get issue** `paperclip_get_issue` | Get an issue with project, goal, ancestors and plan document. | No | Every mode |
|
|
19
|
+
| **List approvals** `paperclip_list_approvals` | List approval requests (hires, plans, budget overrides). Filter client-side by status if needed. | No | Every mode |
|
|
20
|
+
| **Sync snapshot** `paperclip_sync_snapshot` | One-call picture of the company right now: dashboard counters, every agent's status, and live runs. Call at the start of a session to sync with the agents. | No | Every mode |
|
|
21
|
+
| **Changes since cursor** `paperclip_sync_changes` | Poll for what changed. Pass the `cursor` returned by the previous call (omit the first time to get recent history). Returns new activity entries and the next cursor. | No | Every mode |
|
|
22
|
+
| **Wait for agent** `paperclip_wait_for_agent` | Block (polling) until an agent reaches one of the target statuses (default: idle or error) or the timeout passes. Use after waking an agent to follow it to completion. | No | Every mode |
|
|
23
|
+
| **Status report** `paperclip_report_status` | Executive status report: agents, tasks, spend, pending approvals, agents needing attention. | No | Every mode |
|
|
24
|
+
| **Cost report** `paperclip_report_costs` | Spend vs budget with per-agent and per-project breakdown. Defaults to the current month. | No | Every mode |
|
|
25
|
+
| **Agent performance report** `paperclip_report_agent_performance` | Per-agent throughput (done / in progress / blocked), spend and cost per completed task. | No | Every mode |
|
|
26
|
+
| **Activity digest** `paperclip_report_activity` | Digest of audit-log activity, optionally since an ISO timestamp (e.g. start of day). | No | Every mode |
|
|
27
|
+
| **Papercliped service status** `papercliped_service_status` | Is the hosted Papercliped service working? Returns its status (ok, degraded, down), version and aggregate 24-hour numbers (requests, success rate, latency, sign-ins). Public data only; does not touch Paperclip. | No | Every mode |
|
|
28
28
|
|
|
29
29
|
## Full control (13)
|
|
30
30
|
|
|
31
|
-
| Tool | What it does | Changes anything? |
|
|
32
|
-
| --- | --- | --- |
|
|
33
|
-
| **Pause agent** `paperclip_pause_agent` | Pause an agent: stops its heartbeats and cancels its active run. Reversible with resume. | Yes |
|
|
34
|
-
| **Resume agent** `paperclip_resume_agent` | Resume a paused agent so heartbeats start again. | Yes |
|
|
35
|
-
| **Clear agent error** `paperclip_clear_agent_error` | Move an agent from `error` back to `idle`. Keeps run history. Only valid for agents in error. | Yes |
|
|
36
|
-
| **Wake agent now** `paperclip_wake_agent` | Manually trigger a heartbeat so the agent starts working immediately. | Yes |
|
|
37
|
-
| **Set agent budget** `paperclip_set_agent_budget` | Set an agent's monthly budget in US cents (e.g. 5000 = $50). Agents auto-pause at 100%. | Yes |
|
|
38
|
-
| **Terminate agent** `paperclip_terminate_agent` | PERMANENTLY deactivate an agent. Irreversible. Requires confirm=true; ask the human first and prefer pause. | Yes, can't be undone |
|
|
39
|
-
| **Create goal** `paperclip_create_goal` | Create a goal. level is one of company/team/agent/task (default task). | Yes |
|
|
40
|
-
| **Update goal** `paperclip_update_goal` | Update a goal's title, description or status (planned/active/achieved/cancelled). | Yes |
|
|
41
|
-
| **Create issue** `paperclip_create_issue` | Create a task. Assigning it to an agent (assigneeAgentId) with status 'todo' is how you hand work to that agent. | Yes |
|
|
42
|
-
| **Update issue** `paperclip_update_issue` | Change an issue's status, priority, assignee, title etc. Optional `comment` is added in the same call. | Yes |
|
|
43
|
-
| **Comment on issue** `paperclip_comment_issue` | Add a markdown comment to an issue. Can wake the current assignee. | Yes |
|
|
44
|
-
| **Decide approval** `paperclip_decide_approval` | Approve, reject, or request revision on an approval. Decisions bind agents, so confirm with the human. | Yes |
|
|
45
|
-
| **Raw API request** `paperclip_api_request` | Call any Paperclip REST endpoint under /api (path like '/companies/{id}/routines'). For endpoints without a dedicated tool. DELETE and any non-GET require confirm=true. | Yes |
|
|
31
|
+
| Tool | What it does | Changes anything? | Works in |
|
|
32
|
+
| --- | --- | --- | --- |
|
|
33
|
+
| **Pause agent** `paperclip_pause_agent` | Pause an agent: stops its heartbeats and cancels its active run. Reversible with resume. | Yes | Full, API only |
|
|
34
|
+
| **Resume agent** `paperclip_resume_agent` | Resume a paused agent so heartbeats start again. | Yes | Full, API only |
|
|
35
|
+
| **Clear agent error** `paperclip_clear_agent_error` | Move an agent from `error` back to `idle`. Keeps run history. Only valid for agents in error. | Yes | Full, API only |
|
|
36
|
+
| **Wake agent now** `paperclip_wake_agent` | Manually trigger a heartbeat so the agent starts working immediately. | Yes | Full, Agent only |
|
|
37
|
+
| **Set agent budget** `paperclip_set_agent_budget` | Set an agent's monthly budget in US cents (e.g. 5000 = $50). Agents auto-pause at 100%. | Yes | Full, API only |
|
|
38
|
+
| **Terminate agent** `paperclip_terminate_agent` | PERMANENTLY deactivate an agent. Irreversible. Requires confirm=true; ask the human first and prefer pause. | Yes, can't be undone | Full, API only |
|
|
39
|
+
| **Create goal** `paperclip_create_goal` | Create a goal. level is one of company/team/agent/task (default task). | Yes | Full, API only |
|
|
40
|
+
| **Update goal** `paperclip_update_goal` | Update a goal's title, description or status (planned/active/achieved/cancelled). | Yes | Full, API only |
|
|
41
|
+
| **Create issue** `paperclip_create_issue` | Create a task. Assigning it to an agent (assigneeAgentId) with status 'todo' is how you hand work to that agent. | Yes | Full, Agent only |
|
|
42
|
+
| **Update issue** `paperclip_update_issue` | Change an issue's status, priority, assignee, title etc. Optional `comment` is added in the same call. | Yes | Full, Agent only |
|
|
43
|
+
| **Comment on issue** `paperclip_comment_issue` | Add a markdown comment to an issue. Can wake the current assignee. | Yes | Full, Agent only |
|
|
44
|
+
| **Decide approval** `paperclip_decide_approval` | Approve, reject, or request revision on an approval. Decisions bind agents, so confirm with the human. | Yes | Full, API only |
|
|
45
|
+
| **Raw API request** `paperclip_api_request` | Call any Paperclip REST endpoint under /api (path like '/companies/{id}/routines'). For endpoints without a dedicated tool. DELETE and any non-GET require confirm=true. | Yes | GET: every mode; other methods: Full, API only |
|
|
46
46
|
|
|
47
|
-
See [Permissions](/docs/permissions) for how levels work and how to change them.
|
|
47
|
+
**Works in** is the [access switch](/docs/access-modes): *every mode* tools only look; *Full, Agent only* tools hand work to agents; *Full, API only* tools control Paperclip directly. See [Permissions](/docs/permissions) for how levels work and how to change them.
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
**The AI says it can't change anything.** You connected with **Read only**. Disconnect and connect again, choosing **Full control**.
|
|
18
18
|
|
|
19
|
+
**The AI says a tool isn't available, or a call was blocked.** Check the access switch, the agent's setting and the session's limits in the [control room](/docs/control-room). The message the AI gets says which one. **Activity** lists blocked calls with the reason.
|
|
20
|
+
|
|
21
|
+
**The control room shows no agents.** Open a company first: agents are listed for the company you're in. If you installed an older version of the plugin, remove it in **Settings → Plugins** and install it again so it can read agents.
|
|
22
|
+
|
|
19
23
|
**My username is rejected.** It needs 6 to 32 characters including at least one number or one of `.` `#` `_`, and only letters, numbers and those symbols. It may also be taken.
|
|
20
24
|
|
|
21
25
|
**Still stuck?** Open an issue at https://github.com/OpenSourcx/papercliped/issues. Never include your secret key or Paperclip keys.
|