@withone/cli 2.4.0 → 2.5.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/README.md CHANGED
@@ -185,7 +185,7 @@ Opens your browser, you authorize, done. The CLI polls until the connection is l
185
185
 
186
186
  ### `one list`
187
187
 
188
- List your active connections with their status, connection keys, and what your access config lets you run on each.
188
+ List your active connections with their status, connection keys, and the access this key has on each.
189
189
 
190
190
  ```bash
191
191
  one list
@@ -199,15 +199,15 @@ one list
199
199
 
200
200
  You need the connection key (rightmost column) when executing actions.
201
201
 
202
- **Access reporting.** Every connection carries an `access` field describing what the current [access control](#one-config) config permits — so an agent knows its reach up front instead of discovering it as a failure mid-workflow. Three policies:
202
+ **Access reporting.** Every connection carries an `access` field: what this key may run there. The CLI reads it from One (`GET /v1/access/me`, the access chosen on the consent page or later in the dashboard) and narrows it by any limits you set with [`one config`](#one-config) — the server's answer is the ceiling, local settings can only lower it.
203
203
 
204
- | Policy | Means | Shown when |
205
- |--------|-------|-----------|
206
- | `{"policy": "full"}` | Every action on the connection | Default (admin, no action allowlist) |
207
- | `{"policy": "methods", "methods": ["GET"]}` | Only actions with those HTTP methods | Permission level is `read` or `write` |
208
- | `{"policy": "actions", "actions": [{"actionId": "...", "title": "...", "method": "..."}]}` | Only these specific actions | An action allowlist is configured |
204
+ | Policy | Means | `level` |
205
+ |--------|-------|---------|
206
+ | `{"policy": "full"}` | Every action on the connection | `full` (Full access) |
207
+ | `{"policy": "methods", "methods": ["GET"]}` | Only actions with those methods | `read-only`, `read-write` (GET, POST, PUT, PATCH), or `custom` |
208
+ | `{"policy": "actions", "actions": [{"actionId": "...", "title": "...", "method": "..."}]}` | Only these specific actions | `custom`, or `none` when empty |
209
209
 
210
- An action allowlist wins over the permission level (and is then method-filtered by it). When the policy is `actions`, those actions are exactly what may run — `actions load <actionId>` reads their docs, no find needed.
210
+ A connection the key does not reach is not listed. When the policy is `actions`, those actions are exactly what may run — `actions load <actionId>` reads their docs, no find needed.
211
211
 
212
212
  ```jsonc
213
213
  // one --agent list
@@ -219,14 +219,14 @@ An action allowlist wins over the permission level (and is then method-filtered
219
219
  "platform": "gmail",
220
220
  "state": "operational",
221
221
  "key": "live::gmail::default::abc123",
222
- "access": { "policy": "methods", "methods": ["GET"] }
222
+ "access": { "policy": "methods", "methods": ["GET"], "level": "read-only" }
223
223
  }
224
224
  ],
225
- "accessHint": "Permission level \"read\": only GET actions will execute."
225
+ "accessSource": "server"
226
226
  }
227
227
  ```
228
228
 
229
- `knowledgeOnly: true` appears when knowledge-only mode is on (execution disabled), and `unresolvedActionIds` lists any allowlisted action ids that could not be looked up. In human output, an `Access` column and a summary note appear only when access is actually scoped.
229
+ `accessSource` is `"server"` when One reported the key's access and `"local"` when it could not (an older API); then `access` shows only the CLI's own settings, the human table labels the column "Access (CLI settings only)", and `accessHint` says One may allow less. `knowledgeOnly: true` appears when knowledge-only mode is on (execution disabled), and `unresolvedActionIds` lists action ids that could not be looked up.
230
230
 
231
231
  This mirrors the `access` field on the One MCP server's `list_one_integrations` tool, so both surfaces report access the same way.
232
232
 
@@ -104,6 +104,20 @@ var OneApi = class {
104
104
  if (!text) return {};
105
105
  return JSON.parse(text);
106
106
  }
107
+ /**
108
+ * The calling key's own access, as One enforces it: `GET /v1/access/me`.
109
+ * Resolves `null` for a key with no restrictions. Throws `ApiError` when the
110
+ * API does not answer it (for example an older backend without the route).
111
+ */
112
+ async getOwnAccess() {
113
+ const body = await this.request("/access/me");
114
+ if (body === null) return null;
115
+ if (typeof body !== "object" || !("rules" in body || "methods" in body)) {
116
+ throw new ApiError(502, "Unexpected answer from /access/me");
117
+ }
118
+ const doc = body;
119
+ return { methods: doc.methods ?? null, rules: doc.rules ?? null };
120
+ }
107
121
  async whoami() {
108
122
  return this.request("/users/whoami");
109
123
  }
@@ -2285,7 +2299,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
2285
2299
  if (flowStack.includes(resolvedKey)) {
2286
2300
  throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
2287
2301
  }
2288
- const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-SFECNAO2.js");
2302
+ const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-4A5ANCTT.js");
2289
2303
  const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
2290
2304
  const subContext = await executeFlow(
2291
2305
  subFlow,
@@ -12,7 +12,7 @@ import {
12
12
  stripStepsAlias,
13
13
  summarizeFlowInputs,
14
14
  walkSteps
15
- } from "./chunk-6FA7KUNQ.js";
15
+ } from "./chunk-MRQ47KSZ.js";
16
16
  import "./chunk-44CV5IMX.js";
17
17
  import "./chunk-HTXZZQ6U.js";
18
18
  export {
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  validateActionInput,
29
29
  walkSteps,
30
30
  writeCache
31
- } from "./chunk-6FA7KUNQ.js";
31
+ } from "./chunk-MRQ47KSZ.js";
32
32
  import {
33
33
  memSqlCommand
34
34
  } from "./chunk-7FPQOAQ4.js";
@@ -2610,21 +2610,91 @@ async function resolveAllowedActions(api, actionIds) {
2610
2610
  );
2611
2611
  return resolved.filter((a) => a !== null);
2612
2612
  }
2613
- function computeConnectionAccess(platform, permissions, allowedActionIds, grantedActions) {
2614
- if (!allowedActionIds.includes("*")) {
2615
- const actions2 = grantedActions.filter((a) => a.platform === platform).map(({ actionId, title, method }) => ({ actionId, title, method }));
2616
- return { policy: "actions", actions: actions2 };
2613
+ var UNRESTRICTED = { reachable: true, methods: null, actionIds: null };
2614
+ var UNREACHABLE = { reachable: false, methods: [], actionIds: [] };
2615
+ function serverEnvelope(doc, connectionKey) {
2616
+ if (!doc) return UNRESTRICTED;
2617
+ if (doc.rules === null) return { reachable: true, methods: doc.methods, actionIds: null };
2618
+ const rule = doc.rules.find((r) => r.connectionKey === connectionKey);
2619
+ if (!rule) return UNREACHABLE;
2620
+ return {
2621
+ reachable: true,
2622
+ methods: rule.methods ?? doc.methods,
2623
+ actionIds: rule.actionIds ?? null
2624
+ };
2625
+ }
2626
+ function localEnvelope(permissions, actionIds, connectionKeys, connectionKey) {
2627
+ if (!connectionKeys.includes("*") && !connectionKeys.includes(connectionKey)) return UNREACHABLE;
2628
+ return {
2629
+ reachable: true,
2630
+ methods: PERMISSION_METHODS[permissions],
2631
+ actionIds: actionIds.includes("*") ? null : actionIds
2632
+ };
2633
+ }
2634
+ function intersectList(a, b) {
2635
+ if (a === null) return b;
2636
+ if (b === null) return a;
2637
+ const upper = new Set(b.map((x) => x.toUpperCase()));
2638
+ return a.filter((x) => upper.has(x.toUpperCase()));
2639
+ }
2640
+ function intersectEnvelopes(a, b) {
2641
+ if (!a.reachable || !b.reachable) return UNREACHABLE;
2642
+ return {
2643
+ reachable: true,
2644
+ methods: intersectList(a.methods, b.methods),
2645
+ actionIds: a.actionIds === null ? b.actionIds : b.actionIds === null ? a.actionIds : a.actionIds.filter((id) => b.actionIds.includes(id))
2646
+ };
2647
+ }
2648
+ function envelopeToAccess(envelope, resolved, localMethods = null) {
2649
+ if (!envelope.reachable || envelope.methods !== null && envelope.methods.length === 0) {
2650
+ return { access: { policy: "actions", actions: [] }, unresolved: [] };
2617
2651
  }
2618
- const methods = PERMISSION_METHODS[permissions];
2619
- if (methods !== null) {
2620
- return { policy: "methods", methods };
2652
+ if (envelope.actionIds !== null) {
2653
+ const actions2 = envelope.actionIds.flatMap((id) => {
2654
+ const hit = resolved.find((a) => a.actionId === id);
2655
+ if (!hit) return [];
2656
+ if (localMethods && !localMethods.includes(hit.method.toUpperCase())) return [];
2657
+ return [{ actionId: hit.actionId, title: hit.title, method: hit.method }];
2658
+ });
2659
+ return {
2660
+ access: { policy: "actions", actions: actions2 },
2661
+ unresolved: envelope.actionIds.filter((id) => !resolved.some((a) => a.actionId === id))
2662
+ };
2621
2663
  }
2622
- return { policy: "full" };
2664
+ if (envelope.methods !== null) {
2665
+ return { access: { policy: "methods", methods: envelope.methods.map((m) => m.toUpperCase()) }, unresolved: [] };
2666
+ }
2667
+ return { access: { policy: "full" }, unresolved: [] };
2623
2668
  }
2624
- function formatAccess(access, maxActions = 2) {
2669
+ var READ_WRITE = ["GET", "PATCH", "POST", "PUT"];
2670
+ function accessLevel(access) {
2625
2671
  switch (access.policy) {
2626
2672
  case "full":
2627
2673
  return "full";
2674
+ case "methods": {
2675
+ const set = [...new Set(access.methods.map((m) => m.toUpperCase()))].sort();
2676
+ if (set.length === 0) return "none";
2677
+ if (set.length === 1 && set[0] === "GET") return "read-only";
2678
+ if (set.join(",") === READ_WRITE.join(",")) return "read-write";
2679
+ return "custom";
2680
+ }
2681
+ case "actions":
2682
+ return access.actions.length === 0 ? "none" : "custom";
2683
+ }
2684
+ }
2685
+ var LEVEL_LABELS = {
2686
+ full: "Full access",
2687
+ "read-write": "Read & write",
2688
+ "read-only": "Read only",
2689
+ custom: "Custom",
2690
+ none: "No access"
2691
+ };
2692
+ function formatAccess(access, maxActions = 2) {
2693
+ const level = accessLevel(access);
2694
+ if (level !== "custom") return LEVEL_LABELS[level];
2695
+ switch (access.policy) {
2696
+ case "full":
2697
+ return LEVEL_LABELS.full;
2628
2698
  case "methods":
2629
2699
  return access.methods.join(", ");
2630
2700
  case "actions": {
@@ -2847,18 +2917,30 @@ async function connectionListCommand(options) {
2847
2917
  const spinner = createSpinner();
2848
2918
  spinner.start("Loading connections...");
2849
2919
  try {
2850
- const allConnections = await api.listConnections();
2920
+ const [allConnections, serverAccess] = await Promise.all([
2921
+ api.listConnections(),
2922
+ api.getOwnAccess().then(
2923
+ (doc) => ({ ok: true, doc }),
2924
+ () => ({ ok: false, doc: null })
2925
+ )
2926
+ ]);
2851
2927
  const ac = getAccessControlFromAllSources();
2852
2928
  const permissions = ac.permissions || "admin";
2853
2929
  const allowedKeys = ac.connectionKeys || ["*"];
2854
2930
  const actionIds = ac.actionIds || ["*"];
2855
2931
  const knowledgeOnly = ac.knowledgeAgent || false;
2856
- const accessFiltered = allowedKeys.includes("*") ? allConnections : allConnections.filter((conn) => allowedKeys.includes(conn.key));
2857
- const resolvedActions = await resolveAllowedActions(api, actionIds);
2858
- const unresolvedActionIds = actionIds.includes("*") ? [] : actionIds.filter((id) => !resolvedActions.some((a) => a.actionId === id));
2859
- const grantedActions = resolvedActions.filter((a) => isMethodAllowed(a.method, permissions));
2860
- const accessFor = (platform) => computeConnectionAccess(platform, permissions, actionIds, grantedActions);
2861
- const hintText = accessHint(permissions, actionIds, knowledgeOnly);
2932
+ const envelopeFor = (key) => intersectEnvelopes(serverEnvelope(serverAccess.doc, key), localEnvelope(permissions, actionIds, allowedKeys, key));
2933
+ const accessFiltered = allConnections.filter((conn) => envelopeFor(conn.key).reachable);
2934
+ const namedActionIds = new Set(actionIds.includes("*") ? [] : actionIds);
2935
+ for (const rule of serverAccess.doc?.rules ?? []) for (const id of rule.actionIds ?? []) namedActionIds.add(id);
2936
+ const resolvedActions = await resolveAllowedActions(api, [...namedActionIds]);
2937
+ const unresolved = /* @__PURE__ */ new Set();
2938
+ const accessFor = (conn) => {
2939
+ const { access, unresolved: missing } = envelopeToAccess(envelopeFor(conn.key), resolvedActions, PERMISSION_METHODS[permissions]);
2940
+ missing.forEach((id) => unresolved.add(id));
2941
+ return access;
2942
+ };
2943
+ const hintText = accessHint(permissions, actionIds, knowledgeOnly, serverAccess.ok ? serverAccess.doc : void 0);
2862
2944
  const searchQuery = options?.search?.toLowerCase();
2863
2945
  const filtered = searchQuery ? accessFiltered.filter((conn) => conn.platform.toLowerCase().includes(searchQuery)) : accessFiltered;
2864
2946
  const limitArg = options?.limit ? parseInt(options.limit, 10) : void 0;
@@ -2875,10 +2957,11 @@ async function connectionListCommand(options) {
2875
2957
  key: conn.key,
2876
2958
  ...conn.name && { name: conn.name },
2877
2959
  ...conn.tags?.length && { tags: conn.tags },
2878
- access: accessFor(conn.platform)
2960
+ access: withLevel(accessFor(conn))
2879
2961
  })),
2962
+ accessSource: serverAccess.ok ? "server" : "local",
2880
2963
  ...knowledgeOnly && { knowledgeOnly: true },
2881
- ...unresolvedActionIds.length > 0 && { unresolvedActionIds },
2964
+ ...unresolved.size > 0 && { unresolvedActionIds: [...unresolved] },
2882
2965
  ...hintText && { accessHint: hintText },
2883
2966
  ...limited.length < filtered.length && {
2884
2967
  hint: `Showing ${limited.length} of ${filtered.length} connections. Use --search <query> to filter by platform or --limit <n> to see more.`
@@ -2896,6 +2979,13 @@ async function connectionListCommand(options) {
2896
2979
  Try: ${pc6.cyan("one connection list")} to see all connections.`,
2897
2980
  "No Results"
2898
2981
  );
2982
+ } else if (serverAccess.doc?.rules && allConnections.length === 0) {
2983
+ p4.note(
2984
+ `This key reaches no connections: it can search actions and read their docs, but runs none.
2985
+
2986
+ Change it in the dashboard under API keys.`,
2987
+ "No Connections"
2988
+ );
2899
2989
  } else {
2900
2990
  p4.note(
2901
2991
  `No connections yet.
@@ -2913,10 +3003,9 @@ Add one with: ${pc6.cyan("one connection add gmail")}`,
2913
3003
  state: conn.state,
2914
3004
  key: conn.key,
2915
3005
  tags: conn.tags?.length ? conn.tags.join(", ") : "",
2916
- access: formatAccess(accessFor(conn.platform))
3006
+ access: formatAccess(accessFor(conn))
2917
3007
  }));
2918
3008
  const hasTags = rows.some((r) => r.tags);
2919
- const hasScopedAccess = rows.some((r) => r.access !== "full");
2920
3009
  printTable(
2921
3010
  [
2922
3011
  { key: "status", label: "" },
@@ -2924,21 +3013,22 @@ Add one with: ${pc6.cyan("one connection add gmail")}`,
2924
3013
  { key: "state", label: "Status" },
2925
3014
  { key: "key", label: "Connection Key", color: pc6.dim },
2926
3015
  ...hasTags ? [{ key: "tags", label: "Tags", color: pc6.dim }] : [],
2927
- ...hasScopedAccess ? [{ key: "access", label: "Access", color: pc6.yellow }] : []
3016
+ { key: "access", label: serverAccess.ok ? "Access" : "Access (CLI settings only)", color: pc6.yellow }
2928
3017
  ],
2929
3018
  rows
2930
3019
  );
2931
3020
  console.log();
2932
- if (hintText) {
2933
- const lines = [hintText];
2934
- if (unresolvedActionIds.length > 0) {
2935
- lines.push(
2936
- `Could not resolve ${unresolvedActionIds.length} allowlisted action id(s): ${unresolvedActionIds.join(", ")}`
2937
- );
3021
+ if (hintText || unresolved.size > 0) {
3022
+ const lines = hintText ? [hintText] : [];
3023
+ if (unresolved.size > 0) {
3024
+ lines.push(`Could not resolve ${unresolved.size} allowed action id(s): ${[...unresolved].join(", ")}`);
2938
3025
  }
2939
- p4.note(`${wrapText(lines.join("\n"))}
3026
+ p4.note(
3027
+ `${wrapText(lines.join("\n"))}
2940
3028
 
2941
- Change it with: ${pc6.cyan("one config")}`, "Access");
3029
+ This key's access: dashboard \u2192 API keys. This CLI's own limits: ${pc6.cyan("one config")}`,
3030
+ "Access"
3031
+ );
2942
3032
  }
2943
3033
  if (displayed.length < filtered.length) {
2944
3034
  p4.note(
@@ -3012,21 +3102,35 @@ async function connectionDeleteCommand(connectionKey, options) {
3012
3102
  error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3013
3103
  }
3014
3104
  }
3015
- function accessHint(permissions, actionIds, knowledgeOnly) {
3105
+ function accessHint(permissions, actionIds, knowledgeOnly, server) {
3016
3106
  const parts = [];
3107
+ if (server === void 0) {
3108
+ parts.push(
3109
+ "One did not report this key's access, so each `access` shows only this CLI's own settings; One may allow less. Check the key in the dashboard under API keys."
3110
+ );
3111
+ } else if (server?.rules && server.rules.length === 0) {
3112
+ parts.push("This key reaches no connections: it can read docs and search actions, but no action will execute.");
3113
+ } else if (server?.rules?.some((r) => r.actionIds)) {
3114
+ parts.push(
3115
+ "Some connections are action-scoped: their `access.actions` are the only actions you may run there \u2014 use them directly: `actions load <actionId>` reads their docs, no find needed."
3116
+ );
3117
+ }
3017
3118
  if (!actionIds.includes("*")) {
3018
3119
  parts.push(
3019
- "Action-scoped: each connection's `access.actions` are the only actions you may run \u2014 use them directly: `actions load <actionId>` reads their docs, no find needed."
3120
+ "Action-scoped (local settings): each connection's `access.actions` are the only actions you may run \u2014 use them directly: `actions load <actionId>` reads their docs, no find needed."
3020
3121
  );
3021
3122
  } else if (permissions !== "admin") {
3022
3123
  const methods = PERMISSION_METHODS[permissions]?.join(", ") ?? "";
3023
- parts.push(`Permission level "${permissions}": only ${methods} actions will execute.`);
3124
+ parts.push(`Permission level "${permissions}" (local settings): only ${methods} actions will execute.`);
3024
3125
  }
3025
3126
  if (knowledgeOnly) {
3026
3127
  parts.push("Knowledge-only mode: `actions execute` is disabled \u2014 read knowledge and write code instead.");
3027
3128
  }
3028
3129
  return parts.length > 0 ? parts.join(" ") : null;
3029
3130
  }
3131
+ function withLevel(access) {
3132
+ return { ...access, level: accessLevel(access) };
3133
+ }
3030
3134
  function wrapText(text5, width = 72) {
3031
3135
  return text5.split("\n").map((paragraph) => {
3032
3136
  const lines = [];
@@ -11411,7 +11515,7 @@ Request specific sections:
11411
11515
  - Always use the **exact action ID** from a find answer \u2014 don't guess
11412
11516
  - Always read **knowledge** before executing any action
11413
11517
  - Connection keys come from \`one connection list\` \u2014 don't hardcode them
11414
- - \`connection list\` also reports an \`access\` field per connection (\`full\` / \`methods\` / \`actions\`) \u2014 read it before planning so you don't propose an action the access config will reject
11518
+ - \`connection list\` also reports an \`access\` field per connection (\`full\` / \`methods\` / \`actions\`, plus a \`level\`: full, read-write, read-only, custom, none) \u2014 what One enforces for this key, narrowed by \`one config\`. Read it before planning so you don't propose an action that will be refused. \`accessSource: "local"\` means One did not report it, so it is unverified
11415
11519
  - Skills stay in lockstep with the CLI version automatically \u2014 every command checks a \`.one-cli-version\` marker in the canonical skill dir and refreshes the files if the CLI has been upgraded. Check manually with \`one config skills status\`; force a resync with \`one config skills sync\`
11416
11520
  - Building an app whose users connect their own tools? \`one skills add connect\` (run inside the app) installs the One Connect skill from the \`@withone/connect\` version the app uses, into \`.agents/skills/one-connect\` and \`.claude/skills/one-connect\`. \`one --agent skills list\` shows what is installed; any \`one\` command in the project refreshes it after an SDK upgrade
11417
11521
  `;
@@ -11427,7 +11531,7 @@ Always follow this sequence. Never execute without reading the action's document
11427
11531
  one --agent connection list
11428
11532
  \`\`\`
11429
11533
 
11430
- Returns platforms, status, connection keys, tags, and an \`access\` field per connection describing what the current access config lets you run there:
11534
+ Returns platforms, status, connection keys, tags, and an \`access\` field per connection: what One enforces for this key (the consent page's choices, or the dashboard's), narrowed by \`one config\`. \`access.level\` names it as the consent page does (full, read-write, read-only, custom, none):
11431
11535
 
11432
11536
  | \`access\` | Meaning |
11433
11537
  |----------|---------|
@@ -11435,9 +11539,9 @@ Returns platforms, status, connection keys, tags, and an \`access\` field per co
11435
11539
  | \`{"policy": "methods", "methods": ["GET"]}\` | Only actions with these HTTP methods will execute |
11436
11540
  | \`{"policy": "actions", "actions": [{"actionId", "title", "method"}]}\` | Only these exact actions \u2014 use them directly: \`actions load <actionId>\` reads their docs, no find needed |
11437
11541
 
11438
- Also present when relevant: \`knowledgeOnly: true\` (execution disabled \u2014 read the docs and write code instead), \`unresolvedActionIds\` (allowlisted ids that could not be looked up), and \`accessHint\` (a one-line summary of the restriction).
11542
+ \`accessSource\` is \`server\` when One reported it, \`local\` when it could not (then it is unverified). Also present when relevant: \`knowledgeOnly: true\` (execution disabled \u2014 read the docs and write code instead), \`unresolvedActionIds\` (allowlisted ids that could not be looked up), and \`accessHint\` (a one-line summary of the restriction).
11439
11543
 
11440
- Read \`access\` before planning \u2014 it prevents proposing an action the config will reject. Change it with \`one config\`.
11544
+ Read \`access\` before planning \u2014 it prevents proposing an action that will be refused. The key's access changes in the dashboard (API keys); this CLI's own limits with \`one config\`.
11441
11545
 
11442
11546
  ### 1b. Delete a Connection
11443
11547
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -47,7 +47,7 @@ Always follow this sequence when the user wants to do something on a connected p
47
47
  one --agent connection list
48
48
  ```
49
49
 
50
- Returns connected platforms with their connection keys (needed for execution), platform names in kebab-case (needed for finding actions), and an `access` field per connection telling you what you may run there.
50
+ Returns connected platforms with their connection keys (needed for execution), platform names in kebab-case (needed for finding actions), and an `access` field per connection telling you what you may run there. `access` is what One enforces for this key (the choices made on the consent page, or later in the dashboard), narrowed by any limits set with `one config`; `access.level` names it the way the consent page does: `full`, `read-write`, `read-only`, `custom` or `none`. When telling the user their access, use those words (Full access, Read & write, Read only, Custom, No access).
51
51
 
52
52
  **Read `access` before you plan a workflow** — it saves you from discovering a restriction as a 403 halfway through:
53
53
 
@@ -57,6 +57,8 @@ Returns connected platforms with their connection keys (needed for execution), p
57
57
  | `{"policy": "methods", "methods": ["GET"]}` | Only actions with these HTTP methods will execute — don't propose writes |
58
58
  | `{"policy": "actions", "actions": [...]}` | Only these exact actions may run. Each has `actionId`, `title`, `method` — **use them directly**: `actions load <actionId>` reads their docs, no find needed |
59
59
 
60
+ `accessSource` is `"server"` when One reported the key's access, or `"local"` when it could not (the `accessHint` says so) — then `access` reflects only this CLI's settings and One may allow less: tell the user it is unverified rather than calling it full access.
61
+
60
62
  Two more fields appear only when relevant:
61
63
  - `"knowledgeOnly": true` — `actions execute` is disabled. `actions find` returns each action's whole document with how to call it from code; write integration code instead of executing.
62
64
  - `"unresolvedActionIds": [...]` — allowlisted ids that couldn't be looked up; treat them as unavailable and tell the user.