@mediamonks/canopy-cli 0.7.43 → 0.7.44
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/dist/{browse-dyOThTyf.mjs → browse-B8-0dweq.mjs} +1 -1
- package/dist/dev-CUJKgaqJ.mjs +2 -0
- package/dist/{dev-adLLSY1F.mjs → dev-Cq98yblV.mjs} +95 -13
- package/dist/{dev-worker-DJekKSdb.mjs → dev-worker-CcYwQjxz.mjs} +23 -10
- package/dist/docs/agent-browsing.md +2 -2
- package/dist/docs/agent-tools.md +2 -2
- package/dist/docs/ci.md +1 -1
- package/dist/docs/cli-browse.md +1 -1
- package/dist/docs/cli-dev.md +1 -0
- package/dist/docs/cli-mcp.md +1 -1
- package/dist/docs/cli-members.md +41 -0
- package/dist/docs/cli-roles.md +39 -0
- package/dist/docs/cli-token.md +1 -1
- package/dist/docs/index.json +16 -1
- package/dist/docs/overview-faq.md +6 -4
- package/dist/docs/overview-how-it-works.md +3 -2
- package/dist/docs/overview-use-cases.md +1 -1
- package/dist/docs/overview-what-is-canopy.md +1 -1
- package/dist/docs/platform-client-access.md +1 -1
- package/dist/docs/platform-members-and-access.md +66 -42
- package/dist/docs/roles.md +100 -0
- package/dist/docs/sdk-auth.md +24 -10
- package/dist/docs/sdk-realtime.md +1 -1
- package/dist/docs/static-serving.md +1 -1
- package/dist/{docs-CaAgJWK3.mjs → docs-KmN6jD1S.mjs} +1 -1
- package/dist/{feedback-CMnG2Zvs.mjs → feedback-B5-95neo.mjs} +1 -1
- package/dist/index.mjs +10 -8
- package/dist/{local-2xtxtoUd.mjs → local-CIkL7tXP.mjs} +4 -3
- package/dist/{mcp-BVIU4j9N.mjs → mcp-DZdMNTWr.mjs} +2 -2
- package/dist/{package-XOtqboS1.mjs → package-Bvly8-rB.mjs} +1 -1
- package/dist/roles-BDZaN3vi.mjs +2 -0
- package/dist/roles-UM0Iiws6.mjs +379 -0
- package/dist/skill/canopy/SKILL.md +9 -0
- package/dist/{skill-Ct-aCOzg.mjs → skill-DRLniv-I.mjs} +1 -1
- package/dist/{src-CScXotBB.mjs → src-8vxOBOxW.mjs} +38 -3
- package/dist/{upgrade-DJogeXAF.mjs → upgrade-D_yrD7zp.mjs} +1 -1
- package/package.json +2 -2
- package/dist/dev-BmI5kwoG.mjs +0 -2
|
@@ -15,7 +15,7 @@ var BrowseError = class extends Error {
|
|
|
15
15
|
/** The control plane's refusal, in words that say what to do about it. */
|
|
16
16
|
function explainBrowseRefusal(code, slug, detail) {
|
|
17
17
|
switch (code) {
|
|
18
|
-
case "members_only": return `${slug} is limited to its
|
|
18
|
+
case "members_only": return `${slug} is limited to its members — ask the owner to add you.`;
|
|
19
19
|
case "project_not_found": return `There is no project ${slug} on this site — check the slug and --profile.`;
|
|
20
20
|
case "project_paused": return `${slug} is paused.`;
|
|
21
21
|
case "project_moved": return `${slug} has moved${detail ? ` (${detail})` : ""}.`;
|
|
@@ -2,7 +2,8 @@ import { a as projectDialect, f as tryReadCanopyJson, l as resolveProject, o as
|
|
|
2
2
|
import { a as apiRequest, f as withContext, t as ApiError } from "./api-Bl6SG8Oq.mjs";
|
|
3
3
|
import { r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
|
|
4
4
|
import { r as readMigrationFiles } from "./migrations-D8NqjC5I.mjs";
|
|
5
|
-
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER } from "./src-
|
|
5
|
+
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER } from "./src-8vxOBOxW.mjs";
|
|
6
|
+
import { a as parseAppRoleFlag } from "./roles-UM0Iiws6.mjs";
|
|
6
7
|
import { n as writeDevServerRecord } from "./dev-server-file-Dk9pk4AR.mjs";
|
|
7
8
|
import { createRequire } from "node:module";
|
|
8
9
|
import { defineCommand } from "citty";
|
|
@@ -10,6 +11,71 @@ import { existsSync } from "node:fs";
|
|
|
10
11
|
import { join } from "node:path";
|
|
11
12
|
import { randomBytes } from "node:crypto";
|
|
12
13
|
import { pathToFileURL } from "node:url";
|
|
14
|
+
//#region src/dev-roles.ts
|
|
15
|
+
/** The roles a visitor holding `asRoles` carries: the default first, then theirs. */
|
|
16
|
+
function overrideRoles(defaultRole, asRoles) {
|
|
17
|
+
const mine = [...new Set(asRoles)].filter((r) => r !== defaultRole);
|
|
18
|
+
return defaultRole ? [defaultRole, ...mine] : mine;
|
|
19
|
+
}
|
|
20
|
+
/** An /auth/me body with `roles` replaced; anything that isn't an object passes through. */
|
|
21
|
+
function patchAuthMeRoles(body, roles) {
|
|
22
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
|
|
23
|
+
return {
|
|
24
|
+
...body,
|
|
25
|
+
roles
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The roles this dev session runs with, plus lines to print. `roles` is
|
|
30
|
+
* undefined when nothing could be learned and nothing was asked for — the
|
|
31
|
+
* worker then falls back to whatever /auth/me reports.
|
|
32
|
+
*/
|
|
33
|
+
async function resolveDevRoles(slug, asRoles) {
|
|
34
|
+
const notes = [];
|
|
35
|
+
const mine = await apiRequest("GET", `/projects/${slug}/me/roles`).catch(() => void 0);
|
|
36
|
+
if (!asRoles || asRoles.length === 0) {
|
|
37
|
+
if (mine && mine.roles.length > 0) notes.push(` app roles: ${mine.roles.join(", ")}`);
|
|
38
|
+
return {
|
|
39
|
+
roles: mine?.roles,
|
|
40
|
+
override: false,
|
|
41
|
+
notes
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const roles = overrideRoles(mine?.default ?? null, asRoles);
|
|
45
|
+
const known = await apiRequest("GET", `/projects/${slug}/roles`).then((r) => r.roles.map((role) => role.slug)).catch(() => void 0);
|
|
46
|
+
const unknown = known ? asRoles.filter((r) => !known.includes(r)) : [];
|
|
47
|
+
if (unknown.length > 0) notes.push(` warning: ${slug} has no app role ${unknown.join(", ")} — its roles are ${known.length > 0 ? known.join(", ") : "(none yet)"}. Using it anyway.`);
|
|
48
|
+
notes.push(` app roles: ${roles.join(", ") || "(none)"} (--as-role; the platform is unchanged)`);
|
|
49
|
+
return {
|
|
50
|
+
roles,
|
|
51
|
+
override: true,
|
|
52
|
+
notes
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Answers `/_api/data/v1/auth/me` with the platform's answer and `roles`
|
|
57
|
+
* swapped, for the page and the worker alike (the worker's CANOPY.fetch goes
|
|
58
|
+
* through the dev server's own `/_api`). Registered as a plain middleware so
|
|
59
|
+
* it runs ahead of Vite's proxy.
|
|
60
|
+
*/
|
|
61
|
+
function authMeRolesPlugin(roles, upstream) {
|
|
62
|
+
return {
|
|
63
|
+
name: "canopy-dev-as-role",
|
|
64
|
+
configureServer(server) {
|
|
65
|
+
server.middlewares.use((req, res, next) => {
|
|
66
|
+
const path = (req.url ?? "").split("?")[0];
|
|
67
|
+
if (req.method !== "GET" || path !== "/_api/data/v1/auth/me") return next();
|
|
68
|
+
(async () => {
|
|
69
|
+
const response = await upstream();
|
|
70
|
+
const body = await response.json().catch(() => null);
|
|
71
|
+
res.writeHead(response.status, { "content-type": "application/json" });
|
|
72
|
+
res.end(JSON.stringify(response.ok ? patchAuthMeRoles(body, roles) : body));
|
|
73
|
+
})().catch(next);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
13
79
|
//#region src/dev.ts
|
|
14
80
|
/**
|
|
15
81
|
* Mirror the gateway (apps/gateway/src/index.ts): strip /_api and forward.
|
|
@@ -34,21 +100,25 @@ function workerDialect(flags) {
|
|
|
34
100
|
* gateway serves it in production (dev-worker.ts). Returns the plugin plus
|
|
35
101
|
* the notice lines to print, or undefined when the project has no worker.
|
|
36
102
|
*/
|
|
37
|
-
async function workerPlugin(flags, slug, workerAttest, remoteUser) {
|
|
103
|
+
async function workerPlugin(flags, slug, workerAttest, remoteUser, roles) {
|
|
38
104
|
const entry = tryReadCanopyJson(flags.dir)?.worker;
|
|
39
105
|
if (entry === void 0) return void 0;
|
|
40
106
|
if (!existsSync(join(flags.dir, entry))) {
|
|
41
107
|
console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
|
|
42
108
|
return;
|
|
43
109
|
}
|
|
44
|
-
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-
|
|
110
|
+
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-CcYwQjxz.mjs");
|
|
45
111
|
const secrets = readDevVars(flags.dir);
|
|
46
112
|
const email = siteConfig().email;
|
|
47
|
-
const
|
|
113
|
+
const found = await remoteUser?.() ?? (email ? {
|
|
48
114
|
sub: "local-dev",
|
|
49
115
|
email,
|
|
50
116
|
name: "Local Dev"
|
|
51
117
|
} : null);
|
|
118
|
+
const user = found && roles ? {
|
|
119
|
+
...found,
|
|
120
|
+
roles
|
|
121
|
+
} : found;
|
|
52
122
|
const plugin = workerDevPlugin({
|
|
53
123
|
dir: flags.dir,
|
|
54
124
|
entry,
|
|
@@ -170,7 +240,8 @@ async function fetchDevUser(apiUrl, headers) {
|
|
|
170
240
|
return {
|
|
171
241
|
sub: me.id,
|
|
172
242
|
email: me.email ?? "",
|
|
173
|
-
name: me.name ?? ""
|
|
243
|
+
name: me.name ?? "",
|
|
244
|
+
...Array.isArray(me.roles) && { roles: me.roles }
|
|
174
245
|
};
|
|
175
246
|
} catch {
|
|
176
247
|
return;
|
|
@@ -223,14 +294,16 @@ async function bridgeToSandbox(sandbox, req, res) {
|
|
|
223
294
|
async function devRemote(flags, slug) {
|
|
224
295
|
const { token, expires_at } = await withContext(apiRequest("POST", `/projects/${slug}/dev-token`), devTokenContext(slug));
|
|
225
296
|
const { apiUrl } = siteConfig();
|
|
226
|
-
const
|
|
227
|
-
const
|
|
297
|
+
const proxyHeaders = devProxyHeaders(token, slug, flags.staging ? "staging" : "live");
|
|
298
|
+
const devRoles = await resolveDevRoles(slug, flags.asRoles);
|
|
299
|
+
const worker = await workerPlugin(flags, slug, void 0, () => fetchDevUser(apiUrl, proxyHeaders), devRoles.override ? devRoles.roles : void 0);
|
|
300
|
+
const asRolePlugin = devRoles.override && devRoles.roles ? [authMeRolesPlugin(devRoles.roles, () => fetch(`${apiUrl}/data/v1/auth/me`, { headers: proxyHeaders }))] : [];
|
|
228
301
|
const server = await startVite(flags, { proxy: { "/_api": {
|
|
229
302
|
target: apiUrl,
|
|
230
303
|
changeOrigin: true,
|
|
231
|
-
headers:
|
|
304
|
+
headers: proxyHeaders,
|
|
232
305
|
rewrite: rewriteApiPath
|
|
233
|
-
} } }, worker ? [worker.plugin] : []);
|
|
306
|
+
} } }, [...asRolePlugin, ...worker ? [worker.plugin] : []]);
|
|
234
307
|
if (flags.staging) {
|
|
235
308
|
console.log(`\n ${slug} — remote-first dev against the STAGING channel's data`);
|
|
236
309
|
console.log(" (live data is untouched; `canopy staging sync` re-clones it from live)");
|
|
@@ -244,6 +317,7 @@ async function devRemote(flags, slug) {
|
|
|
244
317
|
console.log(" while the schema is in flux");
|
|
245
318
|
}
|
|
246
319
|
for (const note of worker?.notes ?? []) console.log(note);
|
|
320
|
+
for (const note of devRoles.notes) console.log(note);
|
|
247
321
|
console.log("");
|
|
248
322
|
server.printUrls();
|
|
249
323
|
console.log(`\n Dev token expires ${formatDateTime(expires_at)} — restart canopy dev after that.`);
|
|
@@ -251,7 +325,7 @@ async function devRemote(flags, slug) {
|
|
|
251
325
|
async function devLocal(flags, slug) {
|
|
252
326
|
let dump;
|
|
253
327
|
if (flags.pull) {
|
|
254
|
-
const { fetchProjectDump } = await import("./local-
|
|
328
|
+
const { fetchProjectDump } = await import("./local-CIkL7tXP.mjs");
|
|
255
329
|
console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
|
|
256
330
|
dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
|
|
257
331
|
}
|
|
@@ -267,11 +341,13 @@ async function devLocal(flags, slug) {
|
|
|
267
341
|
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
268
342
|
}
|
|
269
343
|
const workerAttest = randomBytes(24).toString("base64url");
|
|
270
|
-
const
|
|
344
|
+
const devRoles = await resolveDevRoles(slug, flags.asRoles);
|
|
345
|
+
const { createSandbox } = await import("./local-CIkL7tXP.mjs");
|
|
271
346
|
const sandbox = await createSandbox({
|
|
272
347
|
slug,
|
|
273
348
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
274
349
|
email: siteConfig().email,
|
|
350
|
+
roles: devRoles.roles ?? [],
|
|
275
351
|
dialect: flags.dialect,
|
|
276
352
|
workerAttest
|
|
277
353
|
});
|
|
@@ -304,7 +380,7 @@ async function devLocal(flags, slug) {
|
|
|
304
380
|
headers: { authorization: `Bearer ${remote.token}` },
|
|
305
381
|
rewrite: rewriteApiPath
|
|
306
382
|
};
|
|
307
|
-
const worker = await workerPlugin(flags, slug, workerAttest);
|
|
383
|
+
const worker = await workerPlugin(flags, slug, workerAttest, void 0, devRoles.roles ?? []);
|
|
308
384
|
const server = await startVite(flags, { proxy: {
|
|
309
385
|
...remoteEntry && {
|
|
310
386
|
"/_api/data/v1/ai": remoteEntry,
|
|
@@ -328,6 +404,7 @@ async function devLocal(flags, slug) {
|
|
|
328
404
|
console.log(" blobs are not pulled — the local blob store starts empty");
|
|
329
405
|
console.log(remote ? " ai/services pass through to the platform (metered as usual)" : " ai/services answer 501 until you log in and restart");
|
|
330
406
|
for (const note of worker?.notes ?? []) console.log(note);
|
|
407
|
+
for (const note of devRoles.notes) console.log(note);
|
|
331
408
|
console.log("");
|
|
332
409
|
server.printUrls();
|
|
333
410
|
}
|
|
@@ -356,6 +433,10 @@ const devCommand = defineCommand({
|
|
|
356
433
|
type: "boolean",
|
|
357
434
|
default: false,
|
|
358
435
|
description: "Point remote dev at the staging channel's data (or pull the staging dump with --local --pull)"
|
|
436
|
+
},
|
|
437
|
+
"as-role": {
|
|
438
|
+
type: "string",
|
|
439
|
+
description: "Try the app as someone with this app role instead of your own (repeatable; the default role stays)"
|
|
359
440
|
}
|
|
360
441
|
},
|
|
361
442
|
run: reportErrors("dev", async ({ args }) => {
|
|
@@ -370,7 +451,8 @@ const devCommand = defineCommand({
|
|
|
370
451
|
local: args.local,
|
|
371
452
|
pull: args.pull,
|
|
372
453
|
staging: args.staging,
|
|
373
|
-
dialect: projectDialect(dir)
|
|
454
|
+
dialect: projectDialect(dir),
|
|
455
|
+
asRoles: parseAppRoleFlag(args["as-role"])
|
|
374
456
|
};
|
|
375
457
|
await (flags.local ? devLocal(flags, slug) : devRemote(flags, slug));
|
|
376
458
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER, r as redactCanopyCredentials, t as CALLER_HEADER } from "./src-
|
|
1
|
+
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER, r as redactCanopyCredentials, t as CALLER_HEADER } from "./src-8vxOBOxW.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
4
|
//#region src/dev-worker.ts
|
|
@@ -189,14 +189,22 @@ function createDevBlob(canopyFetch, prefix) {
|
|
|
189
189
|
}
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
|
+
/** Stamped app roles (project-roles); undefined when the request carries none. */
|
|
193
|
+
function rolesFromHeaders(headers) {
|
|
194
|
+
const raw = headers.get("x-canopy-identity-roles");
|
|
195
|
+
if (raw === null) return void 0;
|
|
196
|
+
return raw.split(",").map((r) => r.trim()).filter(Boolean);
|
|
197
|
+
}
|
|
192
198
|
/** Gateway-stamped identity, honored in dev too so a request can pose as one. */
|
|
193
199
|
function userFromHeaders(headers, fallback) {
|
|
194
200
|
const sub = (headers.get("x-canopy-user-sub") ?? "").trim();
|
|
195
201
|
if (!sub) return fallback;
|
|
202
|
+
const roles = rolesFromHeaders(headers);
|
|
196
203
|
return {
|
|
197
204
|
sub,
|
|
198
205
|
email: (headers.get("x-canopy-user-email") ?? "").trim(),
|
|
199
|
-
name: (headers.get("x-canopy-user-name") ?? "").trim()
|
|
206
|
+
name: (headers.get("x-canopy-user-name") ?? "").trim(),
|
|
207
|
+
...roles && { roles }
|
|
200
208
|
};
|
|
201
209
|
}
|
|
202
210
|
/**
|
|
@@ -208,20 +216,25 @@ function callerFromHeaders(headers, fallback) {
|
|
|
208
216
|
const kind = (headers.get("x-canopy-identity-kind") ?? "").trim();
|
|
209
217
|
const id = (headers.get("x-canopy-identity-id") ?? "").trim();
|
|
210
218
|
const agent = headers.get("x-canopy-caller") === "agent";
|
|
211
|
-
if ((kind === "monk" || kind === "external") && id)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
if ((kind === "monk" || kind === "external") && id) {
|
|
220
|
+
const roles = rolesFromHeaders(headers);
|
|
221
|
+
return {
|
|
222
|
+
kind,
|
|
223
|
+
id,
|
|
224
|
+
email: (headers.get("x-canopy-identity-email") ?? "").trim(),
|
|
225
|
+
name: (headers.get("x-canopy-identity-name") ?? "").trim(),
|
|
226
|
+
agent,
|
|
227
|
+
...roles && { roles }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
218
230
|
const user = userFromHeaders(headers, fallback);
|
|
219
231
|
return user ? {
|
|
220
232
|
kind: "monk",
|
|
221
233
|
id: user.sub,
|
|
222
234
|
email: user.email,
|
|
223
235
|
name: user.name,
|
|
224
|
-
agent
|
|
236
|
+
agent,
|
|
237
|
+
...user.roles && { roles: user.roles }
|
|
225
238
|
} : null;
|
|
226
239
|
}
|
|
227
240
|
/**
|
|
@@ -16,7 +16,7 @@ It prints a one-time link. Whatever browser opens it is signed in to **my-app**,
|
|
|
16
16
|
## What the agent gets
|
|
17
17
|
|
|
18
18
|
- **One project, one channel.** The live site, or staging with `--staging`. The browser is not signed in to your other projects, or to the dashboard.
|
|
19
|
-
- **You.** The app sees you as the visitor, exactly as when you open it yourself. Your backend's `env.USER` is you, and data you can see, the agent can see. On a
|
|
19
|
+
- **You.** The app sees you as the visitor, exactly as when you open it yourself. Your backend's `env.USER` is you, and data you can see, the agent can see. On a members-only project you have to be a member.
|
|
20
20
|
- **An hour.** After that the agent meets the sign-in page and can run `canopy browse` again.
|
|
21
21
|
- **Marked as an agent.** Your backend can tell the request came from an agent: `mcpCaller(request, env).agent` from `@mediamonks/canopy/mcp` is `true`.
|
|
22
22
|
|
|
@@ -47,7 +47,7 @@ The agent's session ends when the hour is up. It also ends early if you:
|
|
|
47
47
|
- revoke that CLI sign-in on your Account page in the dashboard, or
|
|
48
48
|
- sign out everywhere.
|
|
49
49
|
|
|
50
|
-
If you lose access to a
|
|
50
|
+
If you lose access to a members-only project, the agent loses it too.
|
|
51
51
|
|
|
52
52
|
## Good to know
|
|
53
53
|
|
package/dist/docs/agent-tools.md
CHANGED
|
@@ -136,7 +136,7 @@ schema to decide what to send. A few habits help a lot:
|
|
|
136
136
|
## Connect your agent
|
|
137
137
|
|
|
138
138
|
Anyone who can open the project in a browser can connect. For
|
|
139
|
-
|
|
139
|
+
members-only projects, that means members only. From a terminal:
|
|
140
140
|
|
|
141
141
|
```bash
|
|
142
142
|
canopy mcp add <slug>
|
|
@@ -202,7 +202,7 @@ an agent (`agent`), in any route. Two limits apply to your own server:
|
|
|
202
202
|
- They can only call your tools. They can't reach your app's pages or `/api/*`
|
|
203
203
|
routes directly, the dashboard, or other projects.
|
|
204
204
|
- Their access ends when the person signs out (`canopy logout`, or "sign out
|
|
205
|
-
everywhere"), or loses access to a
|
|
205
|
+
everywhere"), or loses access to a members-only project.
|
|
206
206
|
- Very long tool descriptions or schemas are held back, a project shows at
|
|
207
207
|
most 64 tools, results over 512 KB become an error, and a call stops after
|
|
208
208
|
60 seconds.
|
package/dist/docs/ci.md
CHANGED
|
@@ -27,7 +27,7 @@ can deploy that one project and nothing else.
|
|
|
27
27
|
|
|
28
28
|
## Create a token
|
|
29
29
|
|
|
30
|
-
Only the project owner can create a deploy token (any
|
|
30
|
+
Only the project owner can create a deploy token (any editor can list
|
|
31
31
|
and revoke them). From inside the project directory:
|
|
32
32
|
|
|
33
33
|
```bash
|
package/dist/docs/cli-browse.md
CHANGED
|
@@ -24,7 +24,7 @@ The site comes from the active profile, as for every command (`--profile staging
|
|
|
24
24
|
|
|
25
25
|
## How it works
|
|
26
26
|
|
|
27
|
-
The command uses your `canopy login` sign-in to ask Canopy for a link to the project, under the same rules as opening it yourself.
|
|
27
|
+
The command uses your `canopy login` sign-in to ask Canopy for a link to the project, under the same rules as opening it yourself. Members-only projects only give links to their members.
|
|
28
28
|
|
|
29
29
|
- The link **works once** and **expires after a minute**. Run the command again for a new one.
|
|
30
30
|
- The browser that opens it is signed in to that project and channel for an hour. After that it sees the sign-in page again.
|
package/dist/docs/cli-dev.md
CHANGED
|
@@ -21,6 +21,7 @@ canopy dev --local --pull # sandbox hydrated from a fresh prod export
|
|
|
21
21
|
- `--local` — use the local-data sandbox instead of live data. Default off.
|
|
22
22
|
- `--pull` — hydrate the sandbox from a fresh export of the project's production database first. Only valid with `--local`; the command errors otherwise (remote dev already sees live data).
|
|
23
23
|
- `--staging` — point remote dev at the [staging channel's](https://docs.canopy.website/guides/staging) data instead of live (the safe remote target while a schema is in flux), or, with `--local --pull`, hydrate the sandbox from the staging dump.
|
|
24
|
+
- `--as-role <slug>` — run as someone with that [app role](https://docs.canopy.website/guides/roles) instead of your own (repeatable). You keep the project's default role, as a real visitor would. `auth.user()` and `env.CALLER` both report the override, and nothing changes on the platform. Without it, dev runs with your real roles.
|
|
24
25
|
- `--profile <site>` / `CANOPY_PROFILE` — which site the dev token and API calls target. Projects usually pin a profile in `canopy.json`.
|
|
25
26
|
|
|
26
27
|
## How it works
|
package/dist/docs/cli-mcp.md
CHANGED
|
@@ -79,7 +79,7 @@ While developing: the tools from your working tree, served by `canopy dev`.
|
|
|
79
79
|
usual cases:
|
|
80
80
|
|
|
81
81
|
- **Not signed in** — run `canopy login`.
|
|
82
|
-
- **Limited to its
|
|
82
|
+
- **Limited to its members** — ask the owner to add you.
|
|
83
83
|
- **No backend, or serves no tools** — the project has no `worker/`, or it
|
|
84
84
|
doesn't mount any tools yet.
|
|
85
85
|
- **Redeploy required** — the backend was deployed before tools existed. Run
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: canopy members
|
|
3
|
+
description: See who is on the project, add people as editors or viewers, and give them app roles.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy members` manages the project's roster from a terminal: the same as
|
|
7
|
+
the **Members** card under Settings → Access. See
|
|
8
|
+
[Members and access](https://docs.canopy.website/platform/members-and-access) for what editors and
|
|
9
|
+
viewers can do.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
canopy members list
|
|
15
|
+
canopy members add jane@monks.com
|
|
16
|
+
canopy members add sam@monks.com --viewer --app-role approver
|
|
17
|
+
canopy members set sam@monks.com --app-role approver --app-role reviewer
|
|
18
|
+
canopy members set sam@monks.com --editor
|
|
19
|
+
canopy members set sam@monks.com --clear-app-roles
|
|
20
|
+
canopy members remove sam@monks.com
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Arguments & flags
|
|
24
|
+
|
|
25
|
+
All subcommands find the project the usual way (`canopy.json` in the target
|
|
26
|
+
directory, or `--project <slug>`) and honor the global `--profile <site>`.
|
|
27
|
+
|
|
28
|
+
- `list` prints everyone on the project and every pending invite, with their
|
|
29
|
+
Canopy role and app roles. `--json` prints the same as JSON.
|
|
30
|
+
- `add <email>` adds someone as an editor, or a viewer with `--viewer`.
|
|
31
|
+
Someone who hasn't signed in to Canopy yet gets an invite and joins at their
|
|
32
|
+
first sign-in. Owner only.
|
|
33
|
+
- `set <email>` changes someone. `--editor` or `--viewer` changes their Canopy
|
|
34
|
+
role (owner only). `--app-role <slug>` sets their app roles, replacing what
|
|
35
|
+
they had (repeat it, or separate slugs with commas), and `--clear-app-roles`
|
|
36
|
+
takes them all away. Editors can change app roles.
|
|
37
|
+
- `remove <email>` takes someone off the project, or withdraws their invite.
|
|
38
|
+
Owner only.
|
|
39
|
+
|
|
40
|
+
`--app-role` takes slugs from [`canopy roles list`](https://docs.canopy.website/cli/roles). The default
|
|
41
|
+
role needs no assigning: everyone has it.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: canopy roles
|
|
3
|
+
description: Create, rename and remove the project's app roles, and pick the default.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy roles` manages your project's [app roles](https://docs.canopy.website/guides/roles): the labels
|
|
7
|
+
your app reads as `roles` on the signed-in user. It does the same job as the
|
|
8
|
+
**App roles** card under Settings → Access. Owners and editors can use it.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
canopy roles list
|
|
14
|
+
canopy roles list --json
|
|
15
|
+
canopy roles add "Approver"
|
|
16
|
+
canopy roles add "Sign-off lead" --slug lead
|
|
17
|
+
canopy roles rename approver "Sign-off"
|
|
18
|
+
canopy roles default reader
|
|
19
|
+
canopy roles remove approver
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Arguments & flags
|
|
23
|
+
|
|
24
|
+
All subcommands find the project the usual way (`canopy.json` in the target
|
|
25
|
+
directory, or `--project <slug>`) and honor the global `--profile <site>`.
|
|
26
|
+
|
|
27
|
+
- `list` prints each role's slug and name, with the default marked. `--json`
|
|
28
|
+
prints the same as JSON, handy for an agent before it writes a role check.
|
|
29
|
+
- `add <name>` creates a role. The slug comes from the name unless you pass
|
|
30
|
+
`--slug`. Slugs are 1–32 lowercase letters, digits and dashes, starting with
|
|
31
|
+
a letter, and never change. The first role becomes the default. A project
|
|
32
|
+
has at most 20 roles.
|
|
33
|
+
- `rename <slug> <name>` changes the display name. The slug stays.
|
|
34
|
+
- `default <slug>` makes that role the default, which everyone who can open
|
|
35
|
+
the app has.
|
|
36
|
+
- `remove <slug>` deletes the role and takes it away from everyone. Make
|
|
37
|
+
another role the default before removing the current default.
|
|
38
|
+
|
|
39
|
+
To give roles to people, use [`canopy members`](https://docs.canopy.website/cli/members).
|
package/dist/docs/cli-token.md
CHANGED
|
@@ -17,7 +17,7 @@ canopy token revoke <id>
|
|
|
17
17
|
|
|
18
18
|
All subcommands resolve the project the usual way — `canopy.json` in the target directory, or `--project <slug>` to override — and honor the global `--profile <site>` / `CANOPY_PROFILE` for which site the token belongs to.
|
|
19
19
|
|
|
20
|
-
- `create` — mint a new deploy token for the project. Owner only:
|
|
20
|
+
- `create` — mint a new deploy token for the project. Owner only: editors get a clear error and should ask the owner.
|
|
21
21
|
- `--name <label>` — a label shown in the dashboard, e.g. `"GitHub Actions"`. Optional but recommended so you can tell tokens apart later.
|
|
22
22
|
- `list` — show the project's deploy tokens: id, name, creation time, who created them, and when each was last used. Token values are never shown here.
|
|
23
23
|
- `revoke <id>` — delete a token by its id (from `canopy token list`). Deploys using it stop working immediately.
|
package/dist/docs/index.json
CHANGED
|
@@ -52,6 +52,11 @@
|
|
|
52
52
|
"title": "Staging & promoting",
|
|
53
53
|
"summary": "Deploy to a safe copy of your project first — own database cloned from live — then promote the exact same version."
|
|
54
54
|
},
|
|
55
|
+
{
|
|
56
|
+
"slug": "roles",
|
|
57
|
+
"title": "Roles and permissions",
|
|
58
|
+
"summary": "Give people different powers inside your app with app roles, and check them where it counts."
|
|
59
|
+
},
|
|
55
60
|
{
|
|
56
61
|
"slug": "data",
|
|
57
62
|
"title": "Working with project data",
|
|
@@ -157,7 +162,7 @@
|
|
|
157
162
|
{
|
|
158
163
|
"slug": "platform-members-and-access",
|
|
159
164
|
"title": "Members & access",
|
|
160
|
-
"summary": "Everything on Canopy sits behind Monks sign-in.
|
|
165
|
+
"summary": "Everything on Canopy sits behind Monks sign-in. Editors manage a project, viewers open it, and your app can hand out its own roles on top."
|
|
161
166
|
},
|
|
162
167
|
{
|
|
163
168
|
"slug": "platform-feedback",
|
|
@@ -296,6 +301,16 @@
|
|
|
296
301
|
"title": "canopy secrets",
|
|
297
302
|
"summary": "Set, list, and remove the project's worker secrets from the terminal."
|
|
298
303
|
},
|
|
304
|
+
{
|
|
305
|
+
"slug": "cli-members",
|
|
306
|
+
"title": "canopy members",
|
|
307
|
+
"summary": "See who is on the project, add people as editors or viewers, and give them app roles."
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"slug": "cli-roles",
|
|
311
|
+
"title": "canopy roles",
|
|
312
|
+
"summary": "Create, rename and remove the project's app roles, and pick the default."
|
|
313
|
+
},
|
|
299
314
|
{
|
|
300
315
|
"slug": "cli-schedule",
|
|
301
316
|
"title": "canopy schedule",
|
|
@@ -9,7 +9,7 @@ start with [getting started](https://docs.canopy.website/guides/getting-started)
|
|
|
9
9
|
## Who can see my project?
|
|
10
10
|
|
|
11
11
|
By default, any Monk who signs in with OneLogin. If that's too broad, switch
|
|
12
|
-
the project to **
|
|
12
|
+
the project to **members only** in Settings — then only the people you've
|
|
13
13
|
invited can open the live app (it still appears in the company project list,
|
|
14
14
|
and platform admins can still manage it).
|
|
15
15
|
|
|
@@ -63,9 +63,11 @@ agent so it knows how the platform works.
|
|
|
63
63
|
|
|
64
64
|
## Can I work on it with teammates?
|
|
65
65
|
|
|
66
|
-
Yes.
|
|
67
|
-
|
|
68
|
-
this also controls who can open the live
|
|
66
|
+
Yes. Add people under **Settings → Access** in the dashboard: as editors,
|
|
67
|
+
who work on the project with you, or as viewers, who can only open the app.
|
|
68
|
+
Combined with members-only mode, this also controls who can open the live
|
|
69
|
+
app. Your app can give people its own roles too, see
|
|
70
|
+
[Roles and permissions](https://docs.canopy.website/guides/roles).
|
|
69
71
|
|
|
70
72
|
## Can I bring an app I already started?
|
|
71
73
|
|
|
@@ -28,7 +28,7 @@ address exists as soon as the project does.
|
|
|
28
28
|
|
|
29
29
|
Visitors to your project's URL hit OneLogin before they see anything. By
|
|
30
30
|
default any Monk can open it. From the project's Settings you can tighten that
|
|
31
|
-
to
|
|
31
|
+
to members only, or add a shared client password so people outside Monks
|
|
32
32
|
can view it. The password stays visible in Settings, so you can look it up and
|
|
33
33
|
share it again later. There is no "make it public" switch — that's by design.
|
|
34
34
|
|
|
@@ -76,7 +76,8 @@ loop gone wrong stops at the cap instead of running up a bill.
|
|
|
76
76
|
| **Snapshot** | A saved copy of a project's database you can restore later. |
|
|
77
77
|
| **Slug** | The project's short name — the first part of its URL. |
|
|
78
78
|
| **Site** (or **profile**) | An installation of the Canopy platform. Most people only ever use the main one; the CLI can point at others (like staging) and always shows which one you're on. |
|
|
79
|
-
| **
|
|
79
|
+
| **Member** | Someone on a project's roster: an editor, who works on the project, or a viewer, who can only open the app. |
|
|
80
|
+
| **App role** | A label your app gives people, like `approver`, read as `roles` on the signed-in user. |
|
|
80
81
|
|
|
81
82
|
- [Getting started](https://docs.canopy.website/guides/getting-started) — From nothing to a live project, step by step.
|
|
82
83
|
- [Local development](https://docs.canopy.website/guides/dev) — Preview changes on your machine against your real project data.
|
|
@@ -40,7 +40,7 @@ content is a normal use, not an exception.
|
|
|
40
40
|
- A strategist vibecoding a pitch prototype over a weekend, sharing it Monday
|
|
41
41
|
behind a client password.
|
|
42
42
|
- A producer building a tracker the whole team uses daily, with teammates
|
|
43
|
-
invited as
|
|
43
|
+
invited as members.
|
|
44
44
|
- A developer prototyping an AI-assisted workflow with a custom backend,
|
|
45
45
|
scheduled jobs, and secrets — still inside platform auth and budgets.
|
|
46
46
|
|
|
@@ -35,7 +35,7 @@ behind the scenes, so for you it looks like this:
|
|
|
35
35
|
don't set up servers, domains, or certificates.
|
|
36
36
|
- **Sign-in, always on.** Every project sits behind Monks OneLogin
|
|
37
37
|
automatically. Any Monk can open it by default; you can tighten access to
|
|
38
|
-
|
|
38
|
+
members only, or add a shared password so clients without a Monks
|
|
39
39
|
account can see work-in-progress. Nothing on Canopy is ever truly public.
|
|
40
40
|
- **Data and files.** Each project has its own private database and file
|
|
41
41
|
storage. No tables to design, no migrations, no connection strings — and no
|
|
@@ -57,7 +57,7 @@ route that should serve clients checks `env.CALLER`, not `env.USER`. See
|
|
|
57
57
|
## Playing well with other access controls
|
|
58
58
|
|
|
59
59
|
Client access is independent of
|
|
60
|
-
[
|
|
60
|
+
[members-only mode](https://docs.canopy.website/platform/members-and-access): you can lock
|
|
61
61
|
the app down for Monks outside the team while a client with the password
|
|
62
62
|
still gets in. That combination — "the client and the project team, nobody
|
|
63
63
|
else" — is a common setup during a review phase.
|