@botbuddy/cli 1.63.6 → 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.
- package/daemon/bin/darwin-arm64/botbuddy-daemon +0 -0
- package/daemon/bin/linux-x64/botbuddy-daemon +0 -0
- package/daemon-release/darwin-arm64.json +4 -4
- package/daemon-release/linux-x64.json +4 -4
- package/daemon-release/manifest.json +5 -5
- package/package.json +1 -1
- package/src/run-delivery.mjs +3 -2
- package/src/stack-service-discovery.mjs +114 -0
- package/src/stack-tunnel.mjs +3 -23
- package/src/stack.mjs +8 -1
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.8",
|
|
3
|
+
"source_commit": "f26ae5e2838d2cd2bb8125f238e48ea9ae87c74c",
|
|
4
|
+
"source_sha256": "4e4cf641d3c4675efbf172dfa759d31c016d30700ca195f7b3cbc399459ede98",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "cba79a67f58f0bde2d0f20b5c92c2734b468b0ca038ee83d020e325ab06c79fe"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.8",
|
|
3
|
+
"source_commit": "f26ae5e2838d2cd2bb8125f238e48ea9ae87c74c",
|
|
4
|
+
"source_sha256": "4e4cf641d3c4675efbf172dfa759d31c016d30700ca195f7b3cbc399459ede98",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "bf0458ea4d5a2aa36d5de5b374283a3f21c677f42a9afe5f3dcaf1a74807809a"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.63.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
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": "
|
|
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": "
|
|
18
|
+
"sha256": "bf0458ea4d5a2aa36d5de5b374283a3f21c677f42a9afe5f3dcaf1a74807809a",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/run-delivery.mjs
CHANGED
|
@@ -467,7 +467,7 @@ export async function runDelivery({
|
|
|
467
467
|
* small reads. Never throws into the caller's command.
|
|
468
468
|
*/
|
|
469
469
|
export async function ensureRunDelivery({
|
|
470
|
-
home = homedir(), env = process.env, spawnImpl = spawn, isAlive = pidAlive, force = false, cooldownMs = DELIVERY_RESCAN_COOLDOWN_MS,
|
|
470
|
+
home = homedir(), env = process.env, spawnImpl = spawn, isAlive = pidAlive, force = false, cooldownMs = DELIVERY_RESCAN_COOLDOWN_MS, now = Date.now, stateStat = stat,
|
|
471
471
|
} = {}) {
|
|
472
472
|
try {
|
|
473
473
|
if (env.BOTBUDDY_RUN_DELIVERY === "off") return { started: false, reason: "disabled" };
|
|
@@ -480,7 +480,8 @@ export async function ensureRunDelivery({
|
|
|
480
480
|
// starts us itself (force) when it parks, so otherwise scan at most once
|
|
481
481
|
// per cooldown. Both times are this machine's filesystem clock.
|
|
482
482
|
if (!force && state) {
|
|
483
|
-
|
|
483
|
+
// Date.now() has millisecond precision; APFS mtime may carry fractions.
|
|
484
|
+
const stateAge = now() - Math.floor((await stateStat(deliveryStatePath(home))).mtimeMs);
|
|
484
485
|
if (stateAge >= 0 && stateAge < cooldownMs) return { started: false, reason: "cooldown" };
|
|
485
486
|
}
|
|
486
487
|
const holder = await readJson(deliveryLockPath(home));
|
|
@@ -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
|
+
}
|
package/src/stack-tunnel.mjs
CHANGED
|
@@ -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.
|