@botbuddy/cli 1.40.10 → 1.40.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.40.10",
3
+ "version": "1.40.11",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,6 @@
1
1
  // BOT-1649 — diagnose and repair a worktree's short-lived agent credential
2
2
  // without ever printing the token unless the operator explicitly requests an
3
3
  // export snippet for the current shell.
4
- import { hostname } from "node:os";
5
4
  import { readAgentBinding } from "./wait-profile.mjs";
6
5
  import { resolveOwnerToken, CLIENT_KEY_SERVICE, OWNER_TOKEN_SERVICE } from "./cli-credentials.mjs";
7
6
  import { readKeychainSecret } from "./agent-credential-store.mjs";
@@ -9,6 +8,7 @@ import { getConfig } from "./config.mjs";
9
8
  import { isAgentStateFresh, readAgentState } from "./agent-state.mjs";
10
9
  import { selfHealAgentSession } from "./agent-session.mjs";
11
10
  import { machineUuid } from "./machine-id.mjs";
11
+ import { machineHost } from "./machine-host.mjs";
12
12
 
13
13
  export function parseDoctorArgs(argv = []) {
14
14
  const parsed = { fix: false, printExports: false, json: false, errors: [] };
@@ -97,6 +97,16 @@ function applyStackLeaseCapability(report, capability) {
97
97
  if (!capability || typeof capability.status !== "string") return;
98
98
  report.stack_lease.capability = capability.status;
99
99
  if (capability.reason) report.stack_lease.reason = capability.reason;
100
+ if (capability.reported_host && capability.enrolled_host) {
101
+ report.stack_lease.hostname_drift = {
102
+ reported: capability.reported_host,
103
+ enrolled: capability.enrolled_host,
104
+ remedies: [
105
+ "use the macOS LocalHostName reported by `scutil --get LocalHostName`",
106
+ "or re-enrol the Helper if this is intentionally a different machine",
107
+ ],
108
+ };
109
+ }
100
110
  if (capability.status !== "established" && !report.recovery) {
101
111
  report.recovery = `stack leases need a device-consented Helper on this machine: ${capability.reason ?? "worktree not attested"}`;
102
112
  }
@@ -126,7 +136,7 @@ export async function doctorAgentAuth({
126
136
  // inside selfHeal, which the `--fix` gate skips when it reports the key missing.
127
137
  ownerToken = readClientKeyStrict,
128
138
  selfHeal = selfHealAgentSession,
129
- hostFn = hostname, machineUuidFn = machineUuid,
139
+ hostFn = machineHost, machineUuidFn = machineUuid,
130
140
  } = {}) {
131
141
  let owner = null;
132
142
  let ownerReadFailed = false;
@@ -140,6 +150,7 @@ export async function doctorAgentAuth({
140
150
  // LaunchAgent). Env only, exactly like selfHealAgentSession; never the value.
141
151
  const mcpEnv = typeof bound?.mcpEnv === "string" && bound.mcpEnv ? bound.mcpEnv : null;
142
152
  const mcpKeyInEnv = Boolean(mcpEnv && typeof env?.[mcpEnv] === "string" && env[mcpEnv]);
153
+ const resolvedHost = await hostFn();
143
154
  const report = {
144
155
  schema_version: 1,
145
156
  worktree: cwd,
@@ -156,7 +167,7 @@ export async function doctorAgentAuth({
156
167
  // is known only after a register call, so a plain `bb doctor` reports `unverified`
157
168
  // and `--fix` re-registers to (re)establish it (`established` | `unattested`).
158
169
  stack_lease: {
159
- host: hostFn(),
170
+ host: resolvedHost,
160
171
  machine_id_readable: Boolean(machineUuidFn()),
161
172
  capability: "unverified",
162
173
  },
Binary file
@@ -0,0 +1,26 @@
1
+ import { execFile } from "node:child_process";
2
+ import { hostname, platform } from "node:os";
3
+ import { promisify } from "node:util";
4
+ import { canonicalizeHostString } from "./pw/host.mjs";
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ /**
9
+ * Return the host identity used by Helper enrolment. macOS's kernel hostname can
10
+ * follow DHCP; Bonjour LocalHostName is stable and is what the Helper records.
11
+ */
12
+ export async function machineHost({
13
+ platformFn = platform,
14
+ hostnameFn = hostname,
15
+ execFileFn = execFileAsync,
16
+ } = {}) {
17
+ if (platformFn() !== "darwin") return hostnameFn();
18
+ try {
19
+ const { stdout } = await execFileFn("scutil", ["--get", "LocalHostName"]);
20
+ const localName = canonicalizeHostString(stdout);
21
+ if (localName) return `${localName}.local`;
22
+ } catch {
23
+ // Minimal shells can lack scutil; the existing hostname behaviour is safe.
24
+ }
25
+ return hostnameFn();
26
+ }
@@ -8,6 +8,8 @@ import { clearAgentSessionIfMatches, drainPendingCleanup, recordPendingCleanup }
8
8
  import { readAgentBinding } from "./wait-profile.mjs";
9
9
  import { resolveAgentKey, resolveOwnerToken } from "./cli-credentials.mjs";
10
10
  import { getConfig } from "./config.mjs";
11
+ import { machineHost } from "./machine-host.mjs";
12
+ import { machineUuid } from "./machine-id.mjs";
11
13
 
12
14
  // BOT-1650 → BOT-1649 reconciliation: the harness session lives in the ONE
13
15
  // unified client cache (agent-state.mjs), written in main's `agent_session_token`
@@ -29,7 +31,7 @@ export async function loadHarnessSession({ cwd = process.cwd() } = {}) {
29
31
  /** Persist a bootstrap-issued session into the unified cache. The bootstrap RPC
30
32
  * does not echo the tenant, so derive it from the committed binding exactly as
31
33
  * selfHealAgentSession does; host/worktree/minted_at are derived locally. */
32
- export async function saveHarnessSession(session, { cwd = process.cwd() } = {}) {
34
+ export async function saveHarnessSession(session, { cwd = process.cwd(), machineHostFn = machineHost } = {}) {
33
35
  const root = await worktreeRoot(cwd);
34
36
  let tenant;
35
37
  try { tenant = (await readAgentBinding(root))?.tenant; } catch { tenant = undefined; }
@@ -39,7 +41,7 @@ export async function saveHarnessSession(session, { cwd = process.cwd() } = {})
39
41
  agent_session_token: session.session_token,
40
42
  session_id: session.session_id ?? null,
41
43
  ...(tenant ? { tenant } : {}),
42
- host: os.hostname(),
44
+ host: await machineHostFn(),
43
45
  worktree: root,
44
46
  minted_at: new Date().toISOString(),
45
47
  expires_at: session.expires_at,
@@ -218,6 +220,8 @@ export async function bootstrapHarnessSession({
218
220
  model = null,
219
221
  effort = null,
220
222
  harness = type,
223
+ machineHostFn = machineHost,
224
+ machineUuidFn = machineUuid,
221
225
  call = callToolJson,
222
226
  callOptions = undefined,
223
227
  resolveCleanupOptions = durableCleanupOptions,
@@ -249,11 +253,14 @@ export async function bootstrapHarnessSession({
249
253
  // bootstrap straight through so the server reuses a still-live incarnation
250
254
  // or rotates a truly expired one; never revoke an active wait from a stale
251
255
  // local timestamp.
256
+ const host = await machineHostFn();
257
+ const hardwareUuid = machineUuidFn();
252
258
  const response = await call("bootstrap_agent_session", {
253
259
  name,
254
260
  type,
255
261
  harness_session_id: harnessSessionId,
256
- host: os.hostname(),
262
+ host,
263
+ ...(hardwareUuid ? { machine_uuid: hardwareUuid } : {}),
257
264
  worktree,
258
265
  ...(branch ? { branch } : {}),
259
266
  ...(ticketId ? { ticket_id: ticketId } : {}),