@botbuddy/cli 1.63.7 → 1.63.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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.7",
3
- "source_commit": "ad1b7a0a46c938f8dfb3f03f80e97849b879e4a2",
4
- "source_sha256": "44fb5d94a9794df429bdd315b45b75306e4e25f17e382fdb1f09b40791af2411",
2
+ "release_version": "1.63.8",
3
+ "source_commit": "f26ae5e2838d2cd2bb8125f238e48ea9ae87c74c",
4
+ "source_sha256": "4e4cf641d3c4675efbf172dfa759d31c016d30700ca195f7b3cbc399459ede98",
5
5
  "target": "darwin-arm64",
6
- "sha256": "13b8ac4129fbea660ed56afad579f6634f93e5425caf8038f46c28c586c1f858"
6
+ "sha256": "cba79a67f58f0bde2d0f20b5c92c2734b468b0ca038ee83d020e325ab06c79fe"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.7",
3
- "source_commit": "ad1b7a0a46c938f8dfb3f03f80e97849b879e4a2",
4
- "source_sha256": "44fb5d94a9794df429bdd315b45b75306e4e25f17e382fdb1f09b40791af2411",
2
+ "release_version": "1.63.8",
3
+ "source_commit": "f26ae5e2838d2cd2bb8125f238e48ea9ae87c74c",
4
+ "source_sha256": "4e4cf641d3c4675efbf172dfa759d31c016d30700ca195f7b3cbc399459ede98",
5
5
  "target": "linux-x64",
6
- "sha256": "71de432f506d59123e5d46961b6e76c550236767786e1d5b8747ab2f11010d5d"
6
+ "sha256": "bf0458ea4d5a2aa36d5de5b374283a3f21c677f42a9afe5f3dcaf1a74807809a"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.63.7",
4
- "source_commit": "ad1b7a0a46c938f8dfb3f03f80e97849b879e4a2",
5
- "source_sha256": "44fb5d94a9794df429bdd315b45b75306e4e25f17e382fdb1f09b40791af2411",
3
+ "release_version": "1.63.8",
4
+ "source_commit": "f26ae5e2838d2cd2bb8125f238e48ea9ae87c74c",
5
+ "source_sha256": "4e4cf641d3c4675efbf172dfa759d31c016d30700ca195f7b3cbc399459ede98",
6
6
  "targets": {
7
7
  "darwin-arm64": {
8
8
  "goos": "darwin",
9
9
  "goarch": "arm64",
10
10
  "binary": "daemon/bin/darwin-arm64/botbuddy-daemon",
11
- "sha256": "13b8ac4129fbea660ed56afad579f6634f93e5425caf8038f46c28c586c1f858",
11
+ "sha256": "cba79a67f58f0bde2d0f20b5c92c2734b468b0ca038ee83d020e325ab06c79fe",
12
12
  "mode": "0755"
13
13
  },
14
14
  "linux-x64": {
15
15
  "goos": "linux",
16
16
  "goarch": "amd64",
17
17
  "binary": "daemon/bin/linux-x64/botbuddy-daemon",
18
- "sha256": "71de432f506d59123e5d46961b6e76c550236767786e1d5b8747ab2f11010d5d",
18
+ "sha256": "bf0458ea4d5a2aa36d5de5b374283a3f21c677f42a9afe5f3dcaf1a74807809a",
19
19
  "mode": "0755"
20
20
  }
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.63.7",
3
+ "version": "1.63.8",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,114 @@
1
+ // BOT-2776: fixed compatibility names for the verified Supabase HTTP/database
2
+ // services. This is deliberately not a suffix rule or an endpoint registry.
3
+ const known = new Map([
4
+ ["api", ["api", "api", "http"]], ["api-port", ["api", "api", "http"]],
5
+ ["db", ["db", "db", "postgres"]], ["db-port", ["db", "db", "postgres"]],
6
+ ["inbucket", ["inbucket", "mail_http", "http"]], ["inbucket-port", ["inbucket", "mail_http", "http"]],
7
+ ["mailpit", ["mailpit", "mail_http", "http"]], ["mailpit-port", ["mailpit", "mail_http", "http"]],
8
+ ["local_smtp", ["inbucket", "mail_http", "http"]], ["local_smtp-port", ["inbucket", "mail_http", "http"]],
9
+ ]);
10
+ const port = (n) => Number.isInteger(n) && n > 0 && n <= 65535;
11
+ const fail = (message, code = "STACK_SERVICE_AMBIGUOUS") => Object.assign(new Error(message), { code });
12
+
13
+ function declaredSchema(connection) {
14
+ if (!connection || typeof connection !== "object" || Array.isArray(connection)) throw fail("invalid producer connection", "REMOTE_CONNECTION_INVALID");
15
+ if (connection.services == null) return;
16
+ if (connection.services.schema_version !== 1) throw fail("unsupported stack service schema", "STACK_SERVICE_SCHEMA_UNSUPPORTED");
17
+ if (!Array.isArray(connection.services.entries) || connection.services.entries.length > 3
18
+ || connection.services.entries.some((e) => !["api", "db", "mail_http"].includes(e?.id))
19
+ || new Set(connection.services.entries.map((e) => e.id)).size !== connection.services.entries.length) {
20
+ throw fail("invalid or unknown declared stack service");
21
+ }
22
+ }
23
+
24
+ function metadata(ports, producerPorts, forwards) {
25
+ const entries = [];
26
+ for (const row of ports) {
27
+ const forward = forwards.find((f) => f.name === row.name && f.remote === row.port);
28
+ if (!forward) throw fail("service forward does not match the producer target");
29
+ const descriptor = known.get(row.name);
30
+ if (!descriptor) continue;
31
+ const [, id, protocol] = descriptor;
32
+ entries.push({ id, name: row.name, protocol, port: forward.local, remote_port: forward.remote,
33
+ producer_names: producerPorts.filter((p) => known.get(p.name)?.[1] === id).map((p) => p.name) });
34
+ }
35
+ return { schema_version: 1, producer_ports: producerPorts, entries };
36
+ }
37
+
38
+ function validateDeclaredMetadata(declared, expected) {
39
+ if (declared == null) return;
40
+ const keys = (object, fields) => Object.keys(object).length === fields.length && fields.every((k) => Object.hasOwn(object, k));
41
+ if (!keys(declared, ["schema_version", "producer_ports", "entries"])
42
+ || !Array.isArray(declared.producer_ports) || declared.producer_ports.length !== expected.producer_ports.length
43
+ || declared.entries.length !== expected.entries.length) throw fail("declared services contradict producer targets");
44
+ for (const [i, row] of declared.producer_ports.entries()) {
45
+ if (!row || !keys(row, ["name", "port"]) || row.name !== expected.producer_ports[i].name || row.port !== expected.producer_ports[i].port) throw fail("declared service provenance contradicts producer ports");
46
+ }
47
+ for (const entry of declared.entries) {
48
+ const target = expected.entries.find((row) => row.id === entry.id);
49
+ if (!target || !keys(entry, ["id", "name", "protocol", "port", "remote_port", "producer_names"])
50
+ || ["name", "protocol", "port", "remote_port"].some((k) => entry[k] !== target[k])
51
+ || !Array.isArray(entry.producer_names) || JSON.stringify(entry.producer_names) !== JSON.stringify(target.producer_names)) throw fail("declared service contradicts its canonical target");
52
+ }
53
+ }
54
+
55
+ function urlPort(raw, protocols, label) {
56
+ try {
57
+ if (typeof raw !== "string" || /[\u0000-\u0020\u007f]/.test(raw)) throw new Error();
58
+ const u = new URL(raw);
59
+ const literal = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]*@)?(?:127\.0\.0\.1|localhost|\[::1\]):(\d+)(?:[/?#]|$)/i.exec(raw);
60
+ const number = Number(literal?.[1]);
61
+ // Match the existing transport's literal rewrite contract too: URL() alone
62
+ // normalizes e.g. 127.1 and erases explicitly declared HTTP80/HTTPS443.
63
+ // A literal port is always required; normalization must agree with it.
64
+ const agrees = u.port ? Number(u.port) === number
65
+ : u.protocol === "http:" && number === 80 || u.protocol === "https:" && number === 443;
66
+ if (!protocols.includes(u.protocol) || !["127.0.0.1", "localhost", "[::1]"].includes(u.hostname) || !port(number) || !agrees) throw new Error();
67
+ return number;
68
+ } catch { throw fail(`invalid ${label} service URL`, "REMOTE_CONNECTION_INVALID"); }
69
+ }
70
+
71
+ /** Keep one row per physical port. URL anchors are not duplicate producer rows. */
72
+ export function canonicalStackPorts(connection) {
73
+ declaredSchema(connection);
74
+ const api = urlPort(connection?.api_url, ["http:", "https:"], "api_url");
75
+ let db = connection?.db_port;
76
+ const dbURL = connection?.db_url == null ? null : urlPort(connection.db_url, ["postgres:", "postgresql:"], "db_url");
77
+ if (db != null && (!port(db) || dbURL != null && db !== dbURL)) throw fail("database service port conflicts with db_url");
78
+ db ??= dbURL;
79
+ const anchors = new Map([["api", api], ...(db == null ? [] : [["db", db]])]);
80
+ const raw = connection.ports ?? [];
81
+ if (!Array.isArray(raw) || raw.length > 32) throw fail("invalid producer service port table", "REMOTE_CONNECTION_INVALID");
82
+ const names = new Set(), numbers = new Set(), ids = new Set();
83
+ const result = [...anchors].map(([name, value]) => ({ name, port: value }));
84
+ if (new Set(result.map((r) => r.port)).size !== result.length) throw fail("API and database cannot share a service port");
85
+ for (const row of raw) {
86
+ if (typeof row?.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(row.name) || !port(row.port)) {
87
+ throw fail("invalid port or producer service name", "REMOTE_CONNECTION_INVALID");
88
+ }
89
+ if (names.has(row.name) || numbers.has(row.port)) throw fail("duplicate producer service names or ports");
90
+ names.add(row.name); numbers.add(row.port);
91
+ const descriptor = known.get(row.name);
92
+ const [name, id] = descriptor ?? [row.name, null];
93
+ if (id && ids.has(id)) throw fail(`conflicting aliases for ${id}`);
94
+ if (id) ids.add(id);
95
+ if (anchors.has(id)) {
96
+ if (anchors.get(id) !== row.port) throw fail(`${id} producer port conflicts with its service URL`);
97
+ continue;
98
+ }
99
+ if (result.some((r) => r.name === name || r.port === row.port)) throw fail("ambiguous producer service target");
100
+ result.push({ name, port: row.port });
101
+ }
102
+ validateDeclaredMetadata(connection.services, metadata(result, raw.map(({ name, port }) => ({ name, port })), result.map(({ name, port }) => ({ name, local: port, remote: port }))));
103
+ return result;
104
+ }
105
+
106
+ /** Additive discovery only: authority remains the live owning tunnel's table. */
107
+ export function stackServicesForForwards(connection, forwards) {
108
+ const ports = canonicalStackPorts(connection);
109
+ if (!Array.isArray(forwards) || forwards.length !== ports.length
110
+ || forwards.some((f) => !port(f?.local) || !port(f?.remote))
111
+ || ["name", "local", "remote"].some((key) => new Set(forwards.map((f) => f[key])).size !== forwards.length)) throw fail("invalid live service forward table");
112
+ const producerPorts = (connection.ports ?? []).map(({ name, port }) => ({ name, port }));
113
+ return metadata(ports, producerPorts, forwards);
114
+ }
@@ -23,6 +23,7 @@
23
23
 
24
24
  import { spawn, spawnSync } from "node:child_process";
25
25
  import { connect, createServer, isIP } from "node:net";
26
+ import { canonicalStackPorts, stackServicesForForwards } from "./stack-service-discovery.mjs";
26
27
 
27
28
  export const SSH_FORWARD_OPTIONS = Object.freeze([
28
29
  "-N", "-T",
@@ -90,30 +91,8 @@ function validPort(value) {
90
91
  return Number.isInteger(n) && n > 0 && n <= 65535 ? n : null;
91
92
  }
92
93
 
93
- function urlPort(url) {
94
- if (typeof url !== "string") return null;
95
- const match = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]*@)?(?:\[[^\]]+\]|[^:/?#]+):(\d+)/i.exec(url.trim());
96
- return match ? validPort(match[1]) : null;
97
- }
98
-
99
94
  /** Every stack port the child may touch: api + db always, then `connection.ports[]`. */
100
- export function remotePortsFromConnection(connection) {
101
- const ports = [];
102
- const add = (name, raw) => {
103
- const port = validPort(raw);
104
- if (port == null) throw tunnelError("REMOTE_CONNECTION_INVALID", `the remote lease connection has an invalid port for ${name}: ${raw}`);
105
- if (!ports.some((p) => p.port === port)) ports.push({ name, port });
106
- };
107
- const api = urlPort(connection?.api_url);
108
- if (api == null) throw tunnelError("REMOTE_CONNECTION_INVALID", "the remote lease connection has no api_url with an explicit port");
109
- add("api", api);
110
- const db = validPort(connection?.db_port) ?? urlPort(connection?.db_url);
111
- if (db != null) add("db", db);
112
- for (const entry of Array.isArray(connection?.ports) ? connection.ports : []) {
113
- add(String(entry?.name ?? "port"), entry?.port);
114
- }
115
- return ports;
116
- }
95
+ export function remotePortsFromConnection(connection) { return canonicalStackPorts(connection); }
117
96
 
118
97
  /** True when 127.0.0.1:<port> can be bound right now. */
119
98
  export function isLocalPortFree(port) {
@@ -186,6 +165,7 @@ export function rewriteConnectionForForwards(connection, forwards) {
186
165
  const db = validPort(connection?.db_port);
187
166
  if (db != null && byRemote.has(db)) out.db_port = byRemote.get(db);
188
167
  out.ports = forwards.map((f) => ({ name: f.name, port: f.local, remote_port: f.remote }));
168
+ out.services = stackServicesForForwards(connection, forwards);
189
169
  out.transport = "ssh-forward";
190
170
  return out;
191
171
  }
package/src/stack.mjs CHANGED
@@ -50,6 +50,7 @@ import { machineUuid } from "./machine-id.mjs";
50
50
  import { DEFAULT_BUNDLE_MAX_BYTES, buildStackBundle, uploadStackBundle, validateBundleIncludePath } from "./stack-bundle.mjs";
51
51
  import { assertSshTarget, establishSshTunnel } from "./stack-tunnel.mjs";
52
52
  import { FORWARD_ATTESTATION_ENV, startForwardAttestation, readForwardAttestation } from "./stack-forward-attestation.mjs";
53
+ import { stackServicesForForwards } from "./stack-service-discovery.mjs";
53
54
  import { ENGINE_PUBLISHES_BEYOND_LOOPBACK, engineSocketIsLocal, formatPublishIp, isLoopbackIp, publishIpFix, resolvePublishIp } from "./docker-publish-ip.mjs";
54
55
  import { bold, dim, yellow } from "./utils.mjs";
55
56
  import { reportStackRunOutcome } from "./stack-run-outcome.mjs";
@@ -3004,13 +3005,18 @@ export async function cmdStatus(leaseId, opts, {
3004
3005
  : emitResult(buildReceipt({ command: "status", outcome: "error", lease_id: leaseId, error: got.error }), opts, EXIT.BACKEND);
3005
3006
  const g = got.data;
3006
3007
  if (!g.success) return emitResult(buildReceipt({ command: "status", outcome: "error", lease_id: leaseId, code: g.code, error: g.code }), opts, EXIT.BACKEND);
3007
- let forwards;
3008
+ let forwards, services;
3008
3009
  if (g.state === "active" && forwardSocket) {
3009
3010
  try {
3010
3011
  forwards = await readForwardAttestation({ socketPath: forwardSocket, leaseId, sshTarget: g.connection?.ssh_target ?? g.ssh_target });
3011
3012
  } catch {
3012
3013
  return emitResult(buildReceipt({ command: "status", outcome: "error", lease_id: leaseId, code: "FORWARD_ATTESTATION_UNAVAILABLE", error: "live stack forward attestation unavailable" }), opts, EXIT.BACKEND);
3013
3014
  }
3015
+ try {
3016
+ services = stackServicesForForwards(g.connection, forwards);
3017
+ } catch (error) {
3018
+ return emitResult(buildReceipt({ command: "status", outcome: "error", lease_id: leaseId, code: error?.code ?? "STACK_SERVICE_AMBIGUOUS", error: "live service forwards do not match the authenticated producer connection" }), opts, EXIT.BACKEND);
3019
+ }
3014
3020
  }
3015
3021
  // BOT-2019: a queued lease names its server dispatch rank (or why it has none) and the
3016
3022
  // ticket it is scored under; a null position is reported, never a crash.
@@ -3022,6 +3028,7 @@ export async function cmdStatus(leaseId, opts, {
3022
3028
  host_key: g.host_key, slot: g.slot,
3023
3029
  connection: forwards ? { ...g.connection, ssh_target: g.connection?.ssh_target ?? g.ssh_target } : g.connection,
3024
3030
  ...(forwards ? { forwards } : {}),
3031
+ ...(services ? { services } : {}),
3025
3032
  ...(queued ? queueReceiptFields(g) : { queue_position: g.queue_position }),
3026
3033
  idle_ttl_secs: g.idle_ttl_secs, queued: g.queued,
3027
3034
  // BOT-2099 (Codex #1057 R3): machine callers get the unit ledger of a queued lease too.