@2kw/ai 6.3.0-dev.3 → 6.3.0-dev.8

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.
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * Manage the connector settings an organization decides centrally (#805).
4
+ *
5
+ * Only the approved public hosts live here: which MCP servers this organization's agents
6
+ * may reach directly. The connectors themselves are declared per agent, in the agent
7
+ * version's `tools`.
8
+ */
9
+ export declare function makeConnectorsCommand(): Command;
10
+ //# sourceMappingURL=connectors.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { Command } from "commander";
2
+ import { getClient, runAction } from "../lib/client.js";
3
+ import { CliUsageError } from "../lib/errors.js";
4
+ import { formatDetail, formatList, formatSuccess } from "../lib/output.js";
5
+ /**
6
+ * These three endpoints take a member sign-in only (plan ruling R10): approving a
7
+ * destination is the human gate against exfiltration, so an API key — however admin it is —
8
+ * is refused. Said in every description, because `--help` is where a reader looks before
9
+ * the 403.
10
+ */
11
+ const SESSION_ONLY = "Signed-in admins only: run 2kw auth login and sign in through the browser; an API key is refused";
12
+ /** The backend's limit, so a too-long host fails here instead of after a round trip. */
13
+ const MAX_HOST_LENGTH = 253;
14
+ const MIN_PORT = 1;
15
+ const MAX_PORT = 65535;
16
+ /**
17
+ * Parse `--port` strictly, and only when one was given.
18
+ *
19
+ * Commander's house pattern is a bare `parseInt`, which turns `--port abc` into `NaN`,
20
+ * `JSON.stringify` turns that into `null`, and the backend reads a null port as "not
21
+ * given" and approves 443. Silently approving a port nobody asked for is not a wart worth
22
+ * inheriting on a trust decision.
23
+ *
24
+ * Validated inside the action rather than as commander's option parser, so the refusal
25
+ * travels through `runAction` and exits 2 like every other usage error.
26
+ */
27
+ function parsePort(value) {
28
+ if (value === undefined) {
29
+ return undefined;
30
+ }
31
+ const port = Number(value);
32
+ if (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT) {
33
+ throw new CliUsageError(`--port must be a whole number between ${MIN_PORT} and ${MAX_PORT}, not '${value}'`);
34
+ }
35
+ return port;
36
+ }
37
+ function requireHost(host) {
38
+ if (host.length > MAX_HOST_LENGTH) {
39
+ throw new CliUsageError(`A host name is at most ${MAX_HOST_LENGTH} characters`);
40
+ }
41
+ return host;
42
+ }
43
+ /**
44
+ * Manage the connector settings an organization decides centrally (#805).
45
+ *
46
+ * Only the approved public hosts live here: which MCP servers this organization's agents
47
+ * may reach directly. The connectors themselves are declared per agent, in the agent
48
+ * version's `tools`.
49
+ */
50
+ export function makeConnectorsCommand() {
51
+ const cmd = new Command("connectors").description("Manage MCP connector settings");
52
+ const publicHosts = new Command("public-hosts").description(`Approve the public MCP hosts this organization's agents may reach. ${SESSION_ONLY}`);
53
+ publicHosts
54
+ .command("list")
55
+ .description(`List the approved public MCP hosts. ${SESSION_ONLY}`)
56
+ .action(async (_opts, command) => {
57
+ await runAction(command, async () => {
58
+ const client = getClient(command);
59
+ const { data } = await client.GET("/v1/connectors/public-hosts");
60
+ formatList(data, command, [
61
+ "id",
62
+ "host",
63
+ "port",
64
+ "createdBy",
65
+ "createdAt",
66
+ ]);
67
+ });
68
+ });
69
+ publicHosts
70
+ .command("add")
71
+ .description(`Approve a host — every path and every account on it becomes reachable. ${SESSION_ONLY}`)
72
+ .argument("<host>", "Plain host name: no scheme, path, port, wildcard or IP literal")
73
+ .option("--port <n>", "Port to approve; 443 when omitted")
74
+ .action(async (host, opts, command) => {
75
+ await runAction(command, async () => {
76
+ const body = { host: requireHost(host), port: parsePort(opts.port) };
77
+ const client = getClient(command);
78
+ const { data } = await client.POST("/v1/connectors/public-hosts", { body });
79
+ formatDetail(data, command);
80
+ });
81
+ });
82
+ publicHosts
83
+ .command("remove")
84
+ .description(`Withdraw an approved host; it takes effect on the next request. ${SESSION_ONLY}`)
85
+ .argument("<id>", "Approved host ID, from `connectors public-hosts list`")
86
+ .action(async (id, _opts, command) => {
87
+ await runAction(command, async () => {
88
+ const client = getClient(command);
89
+ await client.DELETE("/v1/connectors/public-hosts/{id}", { params: { path: { id } } });
90
+ formatSuccess(`Host ${id} withdrawn.`, command);
91
+ });
92
+ });
93
+ cmd.addCommand(publicHosts);
94
+ return cmd;
95
+ }
96
+ //# sourceMappingURL=connectors.js.map
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ import { makeConversationsCommand } from "./commands/conversations.js";
29
29
  import { makeKnowledgeCommand } from "./commands/knowledge.js";
30
30
  import { makeFilesCommand } from "./commands/files.js";
31
31
  import { makeInstallationsCommand } from "./commands/installations.js";
32
+ import { makeConnectorsCommand } from "./commands/connectors.js";
32
33
  import { makeRelaysCommand } from "./commands/relays.js";
33
34
  import { checkForUpdates } from "./lib/update-notifier.js";
34
35
  const updater = checkForUpdates(pkg.version);
@@ -66,6 +67,7 @@ program.addCommand(makeConversationsCommand());
66
67
  program.addCommand(makeKnowledgeCommand());
67
68
  program.addCommand(makeFilesCommand());
68
69
  program.addCommand(makeInstallationsCommand());
70
+ program.addCommand(makeConnectorsCommand());
69
71
  program.addCommand(makeRelaysCommand());
70
72
  program.addCommand(makeDocsCommand());
71
73
  program.parseAsync().then(() => updater.notify());
@@ -106,6 +106,12 @@ export function hintFor(err) {
106
106
  }
107
107
  if (err.code && CODE_HINTS[err.code])
108
108
  return CODE_HINTS[err.code];
109
+ // The 403 whose cause is the credential, not the role (#805). Without this the
110
+ // generic 403 hint tells an organization admin to check a role that is already
111
+ // right, and no role would ever have fixed it.
112
+ if (err.status === 403 && /^Approved connector hosts are managed by a signed-in/.test(err.message)) {
113
+ return "Approved hosts need a browser sign-in: run 2kw auth login. An API key cannot manage them, whatever its role.";
114
+ }
109
115
  // Anchored on the agent service's wording: prompts and schemas send the same
110
116
  // "Label '<x>' not found" prefix, and must keep the generic 404 hint.
111
117
  if (err.status === 404 && /^Label 'latest' not found (on agent|for this agent)/.test(err.message)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "6.3.0-dev.3",
3
+ "version": "6.3.0-dev.8",
4
4
  "description": "CLI for 2kw.ai — schema-driven document extraction, an OpenAI-compatible EU LLM gateway, transcription, prompts, datasets, and experiments from your terminal or agentic workflows. Ships as 2kw, backbone, and bb.",
5
5
  "keywords": [
6
6
  "cli",