@bike4mind/cli 1.1.0 → 1.2.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.
Files changed (28) hide show
  1. package/README.md +1 -0
  2. package/bin/bike4mind-cli.mjs +0 -5
  3. package/dist/AgentHistoryStore-B2NEOvSW.mjs +18014 -0
  4. package/dist/{ApiClient-BOWpVvTq.mjs → ApiClient-D0fQ2FT6.mjs} +2 -2
  5. package/dist/{BubblewrapRuntime-CkL9-gnG.mjs → BubblewrapRuntime-5bLPTwEC.mjs} +2 -2
  6. package/dist/{ConfigStore-DdHHCH2t.mjs → ConfigStore-qV7NrCgZ.mjs} +1154 -1365
  7. package/dist/{ProxyManager-C1-lgzEU.mjs → ProxyManager-B0-RuR2w.mjs} +21 -4
  8. package/dist/{SandboxOrchestrator-CIegJrCj.mjs → SandboxOrchestrator-BcUv9fQ3.mjs} +46 -12
  9. package/dist/{SandboxRuntimeAdapter-ChGlxSGQ.mjs → SandboxRuntimeAdapter-BgLUVTJL.mjs} +2 -2
  10. package/dist/{SandboxRuntimeAdapter-CKelGICD.mjs → SandboxRuntimeAdapter-BmHELLuM.mjs} +1 -1
  11. package/dist/{SeatbeltRuntime-Qqt19cAN.mjs → SeatbeltRuntime-C_Y8q8Mr.mjs} +9 -1
  12. package/dist/buildAgent-C-C-VGff.mjs +2001 -0
  13. package/dist/commands/acpCommand.mjs +8 -6
  14. package/dist/commands/apiCommand.mjs +1 -1
  15. package/dist/commands/doctorCommand.mjs +1 -1
  16. package/dist/commands/envCommand.mjs +1 -1
  17. package/dist/commands/headlessCommand.mjs +26 -16
  18. package/dist/commands/mcpCommand.mjs +3 -3
  19. package/dist/commands/pluginCommand.mjs +1 -1
  20. package/dist/commands/updateCommand.mjs +1 -1
  21. package/dist/index.mjs +444 -100
  22. package/dist/{package-Bqg2LSnH.mjs → package-CGZIoxcs.mjs} +1 -1
  23. package/dist/{serve-DO9Edl5S.mjs → serve-CPXqcEZr.mjs} +2 -2
  24. package/package.json +9 -10
  25. package/dist/AgentHistoryStore-kT9eNMRO.mjs +0 -39471
  26. package/dist/ProxyManager-B1jFWL7b.mjs +0 -3
  27. package/dist/SandboxOrchestrator-BbMDgjzr.mjs +0 -3
  28. package/dist/buildAgent-B_kArQ_Y.mjs +0 -833
package/dist/index.mjs CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { $ as CustomCommandStore, A as createAgentDelegateTool, B as ALWAYS_DENIED_FOR_AGENTS, C as createTodoStore, D as createCoordinateTaskTool, E as parseAgentConfig, F as PermissionManager, H as DEFAULT_MAX_ITERATIONS, J as setWebSocketToolExecutor, K as clearFeatureModuleTools, L as wrapTools, M as substituteArguments, O as createResumeAgentTool, P as findIterationBoundary, Q as RemoteSkillSource, R as getProcessHooks, S as createFindDefinitionTool, T as createSkillTool, U as DEFAULT_RETRY_CONFIG, V as DEFAULT_AGENT_MODEL, W as DEFAULT_THOROUGHNESS, X as buildSystemPrompt, Y as getPlanModeFilePath, Z as buildSkillsPromptSection, _ as createDecisionLogTool, at as mergeCommands, b as createGetFileStructureTool, ct as warmFileCache, d as createReviewGateStore, dt as MAX_PASTE_SIZE, et as CheckpointStore, f as createReviewGateTool, g as formatBlockersOutput, h as createBlockerTools, it as searchCommands, j as FallbackLlmBackend, k as createBackgroundAgentTools, l as isTransientNetworkError, lt as COMPACTION_SUMMARY_MARKER, m as createBlockerStore, nt as hasFileReferences, ot as formatFileSize, p as formatReviewGatesOutput, q as registerFeatureModuleTools, rt as processFileReferences, st as searchFiles, tt as SessionStore, v as createDecisionStore, w as createWriteTodosTool, x as createWorkItemTools, y as formatDecisionsOutput } from "./AgentHistoryStore-kT9eNMRO.mjs";
2
+ import { $ as CustomCommandStore, A as createAgentDelegateTool, B as ALWAYS_DENIED_FOR_AGENTS, C as createTodoStore, D as createCoordinateTaskTool, E as parseAgentConfig, F as PermissionManager, H as DEFAULT_MAX_ITERATIONS, J as setWebSocketToolExecutor, K as clearFeatureModuleTools, L as wrapTools, M as substituteArguments, O as createResumeAgentTool, P as findIterationBoundary, Q as RemoteSkillSource, R as getProcessHooks, S as createFindDefinitionTool, T as createSkillTool, U as DEFAULT_RETRY_CONFIG, V as DEFAULT_AGENT_MODEL, W as DEFAULT_THOROUGHNESS, X as buildSystemPrompt, Y as getPlanModeFilePath, Z as buildSkillsPromptSection, _ as createDecisionLogTool, at as searchCommands, b as createGetFileStructureTool, ct as formatFileSize, d as createReviewGateStore, dt as COMPACTION_SUMMARY_MARKER, et as CheckpointStore, f as createReviewGateTool, g as formatBlockersOutput, h as createBlockerTools, j as FallbackLlmBackend, k as createBackgroundAgentTools, lt as searchFiles, m as createBlockerStore, nt as hasFileReferences, ot as mergeCommands, p as formatReviewGatesOutput, pt as MAX_PASTE_SIZE, q as registerFeatureModuleTools, rt as processFileReferences, s as buildSandbox, st as rewireReservedNames, tt as SessionStore, u as isTransientNetworkError, ut as warmFileCache, v as createDecisionStore, w as createWriteTodosTool, x as createWorkItemTools, y as formatDecisionsOutput } from "./AgentHistoryStore-B2NEOvSW.mjs";
3
3
  import { a as selectLiveSubagentTokens, i as selectLiveSubagentCredits, n as selectActiveBackgroundShells, o as useCliStore, r as selectCompletedBackgroundShells, t as selectActiveBackgroundAgents } from "./store-CvjTpQPs.mjs";
4
4
  import { r as isFeatureEnabled, t as PluginStore } from "./PluginStore-DwvOJ-G3.mjs";
5
- import { D as ChatModels, E as CREDIT_DEDUCT_TRANSACTION_TYPES, Ot as isEarlyStop, c as ApiEndpointUnconfiguredError, cn as actorColorIndex, d as getEnvironmentName, dn as selfClaimedActorKindSchema, et as REVIEW_GATE_STATUS_VALUES, f as parseApiUrl, l as LOCAL_DEV_URL, ln as actorKindMarker, m as resolveApiEndpoint, n as logger, p as requireApiUrl, r as extractCompactInstructions, s as isReadOnlyTool, sn as ACTOR_COLOR_SLOTS, t as ConfigStore, tt as SUBQUEST_STATUS_VALUES, u as getCreditsUrl, un as actorKindSchema } from "./ConfigStore-DdHHCH2t.mjs";
5
+ import { C as CREDIT_DEDUCT_TRANSACTION_TYPES, F as REVIEW_GATE_STATUS_VALUES, K as isEarlyStop, L as SUBQUEST_STATUS_VALUES, c as SESSION_ID_PATTERN, d as LOCAL_DEV_URL, et as ACTOR_COLOR_SLOTS, f as getCreditsUrl, g as resolveApiEndpoint, h as requireApiUrl, it as selfClaimedActorKindSchema, l as isValidSessionId, m as parseApiUrl, n as logger, nt as actorKindMarker, p as getEnvironmentName, r as extractCompactInstructions, rt as actorKindSchema, s as isReadOnlyTool, t as ConfigStore, tt as actorColorIndex, u as ApiEndpointUnconfiguredError, w as ChatModels } from "./ConfigStore-qV7NrCgZ.mjs";
6
+ import { a as deferredToolRegistry, i as createToolSearchTool, n as buildSupportingStores, o as NotifyingLlmBackend, r as buildLlmBackend, t as buildAgent } from "./buildAgent-C-C-VGff.mjs";
6
7
  import { t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
7
- import { n as OAuthClient, t as ApiClient } from "./ApiClient-BOWpVvTq.mjs";
8
+ import { n as OAuthClient, t as ApiClient } from "./ApiClient-D0fQ2FT6.mjs";
8
9
  import { i as getTokenCounter, n as ConversationContext, r as reconstructTurnBlocks, t as WorkItemsClient } from "./WorkItemsClient-Cow6nXx7.mjs";
9
- import { a as createToolSearchTool, i as buildLlmBackend, n as buildSupportingStores, o as deferredToolRegistry, r as buildSandbox, s as NotifyingLlmBackend, t as buildAgent } from "./buildAgent-B_kArQ_Y.mjs";
10
- import { t as version } from "./package-Bqg2LSnH.mjs";
10
+ import { t as version } from "./package-CGZIoxcs.mjs";
11
11
  import { r as checkForUpdate } from "./updateChecker-CQW8bxo6.mjs";
12
12
  import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
13
13
  import { Box, Static, Text, render, useApp, useInput, usePaste, useStdout } from "ink";
14
- import { execSync, spawn } from "child_process";
14
+ import { execFile, execSync, spawn } from "child_process";
15
15
  import { randomBytes } from "crypto";
16
- import { existsSync, promises, readFileSync, statSync } from "fs";
16
+ import { constants, existsSync, promises, readFileSync, statSync } from "fs";
17
17
  import { homedir } from "os";
18
18
  import path, { basename, extname, join } from "path";
19
19
  import { v4 } from "uuid";
@@ -25,6 +25,7 @@ import { z as z$1 } from "zod";
25
25
  import axios, { isAxiosError } from "axios";
26
26
  import { marked } from "marked";
27
27
  import { highlight } from "cli-highlight";
28
+ import { promisify } from "util";
28
29
  import SelectInput from "ink-select-input";
29
30
  import jwt from "jsonwebtoken";
30
31
  import open from "open";
@@ -1084,14 +1085,23 @@ function CompletedGroupNotification() {
1084
1085
  //#endregion
1085
1086
  //#region src/components/PermissionPrompt.tsx
1086
1087
  /**
1087
- * Escape terminal control characters (carriage return, ESC/ANSI, other C0
1088
- * controls, DEL) into a visible `\xHH` form. The preview is model-authored;
1089
- * rendering it raw would let a `\r` or ESC sequence rewrite what the user sees,
1090
- * so the approved text could differ from what actually runs. Tab and newline are
1091
- * left intact (newline is already the line delimiter).
1088
+ * Escape characters that let model-authored text render differently from what
1089
+ * actually runs, into a visible escape form. The preview and arguments are
1090
+ * model-authored, so the approved text could otherwise differ from what runs.
1091
+ * Two ranges are covered, deliberately and no wider:
1092
+ * - C0 controls (minus tab/newline) and DEL -> `\xHH`: a raw `\r` or ESC
1093
+ * sequence could rewrite the line the user is approving.
1094
+ * - Unicode bidi overrides U+202A..U+202E and isolates U+2066..U+2069
1095
+ * -> `\uXXXX`: these reorder displayed glyphs without changing the
1096
+ * underlying code points, spoofing the same way.
1097
+ * Tab and newline are left intact (newline is already the line delimiter).
1098
+ * Widen the scope here, so both call sites gain it at once.
1092
1099
  */
1093
1100
  function escapeTerminalControlChars(text) {
1094
- return text.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, (ch) => `\\x${ch.charCodeAt(0).toString(16).padStart(2, "0")}`);
1101
+ return text.replace(/[\x00-\x08\x0b-\x1f\x7f\u202a-\u202e\u2066-\u2069]/g, (ch) => {
1102
+ const code = ch.charCodeAt(0);
1103
+ return code > 255 ? `\\u${code.toString(16).padStart(4, "0")}` : `\\x${code.toString(16).padStart(2, "0")}`;
1104
+ });
1095
1105
  }
1096
1106
  /** Render a diff/preview string, color-coding each line by its prefix (see per-branch comments below). */
1097
1107
  function renderDiffPreview(preview) {
@@ -1228,7 +1238,7 @@ function PermissionPrompt({ toolName, toolDescription, args, preview, canBeTrust
1228
1238
  }, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Arguments:"), /* @__PURE__ */ React.createElement(Box, {
1229
1239
  paddingLeft: 2,
1230
1240
  flexDirection: "column"
1231
- }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, argsString))), preview && /* @__PURE__ */ React.createElement(Box, {
1241
+ }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, escapeTerminalControlChars(argsString)))), preview && /* @__PURE__ */ React.createElement(Box, {
1232
1242
  marginTop: 1,
1233
1243
  flexDirection: "column"
1234
1244
  }, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Preview:"), /* @__PURE__ */ React.createElement(Box, {
@@ -3566,7 +3576,7 @@ function toStringArray(value) {
3566
3576
  function extractJsonObject(text) {
3567
3577
  const trimmed = text.trim();
3568
3578
  if (!trimmed) return null;
3569
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
3579
+ const fenced = trimmed.match(/```(?:json)?([\s\S]*?)```/i);
3570
3580
  const candidate = fenced ? fenced[1].trim() : trimmed;
3571
3581
  const start = candidate.indexOf("{");
3572
3582
  if (start === -1) return null;
@@ -6758,6 +6768,168 @@ function createBuiltinModules(config, apiClient) {
6758
6768
  return modules;
6759
6769
  }
6760
6770
  //#endregion
6771
+ //#region src/features/bridgePresence/peerOwner.ts
6772
+ const execFileAsync = promisify(execFile);
6773
+ const LSOF_TIMEOUT_MS = 2e3;
6774
+ const LSOF_CANDIDATES = ["/usr/sbin/lsof", "/usr/bin/lsof"];
6775
+ /**
6776
+ * Resolve the OS owner of the process holding the LISTEN socket on
6777
+ * `127.0.0.1:<port>`. This is the only pre-transmission trust signal the CLI
6778
+ * has over loopback TCP: the peer is whoever holds the port, so we confirm it
6779
+ * is the same UID as this process BEFORE handing it the bridge secret.
6780
+ *
6781
+ * Pure platform probe: the per-platform helpers classify every EXPECTED failure
6782
+ * into `no-listener`/`unknown` themselves. An UNEXPECTED throw (a bug) is left to
6783
+ * propagate so the CLI-layer caller can log it and fail closed - this module
6784
+ * deliberately does not depend on the CLI logger.
6785
+ */
6786
+ async function resolveLoopbackListenerOwner(port) {
6787
+ if (process.platform === "linux") return resolveViaProcNet(port);
6788
+ if (process.platform === "darwin") return resolveViaLsof(port);
6789
+ return { kind: "unknown" };
6790
+ }
6791
+ /**
6792
+ * Whether this platform + process can resolve loopback listener ownership at all:
6793
+ * there must be a `getuid` to compare against (Windows has none) AND an owner probe
6794
+ * for the platform. When false, the caller latches presence off rather than spinning
6795
+ * a retry loop forever on a check that can never pass (a `getuid`-bearing platform
6796
+ * with no probe - BSD/SunOS - would otherwise retry indefinitely).
6797
+ * MUST stay in sync with `resolveLoopbackListenerOwner`'s platform dispatch above:
6798
+ * a platform gains a probe there and a `true` here in the same change.
6799
+ */
6800
+ function canResolveLoopbackOwner() {
6801
+ if (typeof process.getuid !== "function") return false;
6802
+ return process.platform === "linux" || process.platform === "darwin";
6803
+ }
6804
+ async function resolveViaProcNet(port) {
6805
+ const uids = /* @__PURE__ */ new Set();
6806
+ let readAny = false;
6807
+ for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
6808
+ let text;
6809
+ try {
6810
+ text = await promises.readFile(file, "utf8");
6811
+ } catch {
6812
+ continue;
6813
+ }
6814
+ readAny = true;
6815
+ for (const uid of parseProcNetForUid(text, port)) uids.add(uid);
6816
+ }
6817
+ if (!readAny) return { kind: "unknown" };
6818
+ if (uids.size === 0) return { kind: "no-listener" };
6819
+ return uids.size === 1 ? {
6820
+ kind: "owner",
6821
+ uid: [...uids][0]
6822
+ } : { kind: "unknown" };
6823
+ }
6824
+ /**
6825
+ * `local_address` hex (upper-case) of a socket that receives connections to
6826
+ * `127.0.0.1`: loopback itself, the v4/v6 wildcard binds (a same-UID process
6827
+ * may legitimately hold `0.0.0.0`/`::`), and the v4-mapped-in-v6 forms. A row
6828
+ * on any other IP is a different socket that happens to share the port number
6829
+ * and must NOT be attributed to the loopback peer.
6830
+ *
6831
+ * `/proc/net/tcp6` prints each 32-bit word of the address little-endian (no
6832
+ * `ntohl`), which is why `127.0.0.1` reads `0100007F` and the mapped third word
6833
+ * `00 00 ff ff` reads `FFFF0000`.
6834
+ */
6835
+ const LOOPBACK_OR_WILDCARD_HEX = /* @__PURE__ */ new Set([
6836
+ "00000000",
6837
+ "0100007F",
6838
+ "00000000000000000000000000000000",
6839
+ "00000000000000000000000001000000",
6840
+ "0000000000000000FFFF000000000000",
6841
+ "0000000000000000FFFF00000100007F"
6842
+ ]);
6843
+ /**
6844
+ * Parse a `/proc/net/tcp`(6) dump for the UIDs of LISTEN sockets on the
6845
+ * loopback `port`. Row columns are whitespace-separated: sl(0)
6846
+ * local_address(1) rem_address(2) st(3) tx:rx(4) tr:when(5) retrnsmt(6)
6847
+ * uid(7). `local_address` is `HEXIP:HEXPORT`; `st` `0A` is TCP_LISTEN. The IP
6848
+ * must be loopback or wildcard, else a same-port listener on another interface
6849
+ * would be mis-attributed to the loopback peer. Returns every match so the
6850
+ * caller can reject an ambiguous set (two distinct owners) rather than
6851
+ * silently taking the first.
6852
+ */
6853
+ function parseProcNetForUid(text, port) {
6854
+ const uids = [];
6855
+ for (const line of text.split("\n")) {
6856
+ const cols = line.trim().split(/\s+/);
6857
+ if (cols.length < 8) continue;
6858
+ const local = cols[1];
6859
+ const colon = local.lastIndexOf(":");
6860
+ if (colon < 0) continue;
6861
+ if (parseInt(local.slice(colon + 1), 16) !== port) continue;
6862
+ if (cols[3] !== "0A") continue;
6863
+ if (!LOOPBACK_OR_WILDCARD_HEX.has(local.slice(0, colon).toUpperCase())) continue;
6864
+ const uid = Number(cols[7]);
6865
+ if (Number.isInteger(uid)) uids.push(uid);
6866
+ }
6867
+ return uids;
6868
+ }
6869
+ async function resolveViaLsof(port) {
6870
+ const lsof = await resolveLsofPath();
6871
+ if (!lsof) return { kind: "unknown" };
6872
+ let stdout;
6873
+ try {
6874
+ ({stdout} = await execFileAsync(lsof, [
6875
+ "-nP",
6876
+ `-iTCP:${port}`,
6877
+ "-sTCP:LISTEN",
6878
+ "-Fpun"
6879
+ ], { timeout: LSOF_TIMEOUT_MS }));
6880
+ } catch (err) {
6881
+ const e = err;
6882
+ if (!e.killed && e.code === 1 && !e.stdout) return { kind: "no-listener" };
6883
+ if (e.killed || typeof e.stdout !== "string") return { kind: "unknown" };
6884
+ stdout = e.stdout;
6885
+ }
6886
+ const uids = new Set(parseLsofForUid(stdout));
6887
+ if (uids.size === 0) return { kind: "no-listener" };
6888
+ return uids.size === 1 ? {
6889
+ kind: "owner",
6890
+ uid: [...uids][0]
6891
+ } : { kind: "unknown" };
6892
+ }
6893
+ let lsofPathCache;
6894
+ async function resolveLsofPath() {
6895
+ if (lsofPathCache !== void 0) return lsofPathCache;
6896
+ for (const candidate of LSOF_CANDIDATES) try {
6897
+ await promises.access(candidate, constants.X_OK);
6898
+ lsofPathCache = candidate;
6899
+ return candidate;
6900
+ } catch {}
6901
+ lsofPathCache = null;
6902
+ return null;
6903
+ }
6904
+ /** Does an `lsof -nP` numeric socket name (`n` field, host:port) receive
6905
+ * connections to `127.0.0.1`? Loopback, wildcard, or v4-mapped loopback -
6906
+ * exact, to stay symmetric with the linux matcher (the CLI only dials
6907
+ * `127.0.0.1`). */
6908
+ function isLoopbackLsofName(name) {
6909
+ return name.startsWith("127.0.0.1:") || name.startsWith("*:") || name.startsWith("[::1]:") || name.startsWith("[::]:") || name.startsWith("[::ffff:127.0.0.1]:");
6910
+ }
6911
+ /**
6912
+ * Parse `lsof -Fpun` output for the UIDs of loopback listeners. `-F` prints
6913
+ * one field per line prefixed by its type char; `u<uid>` is a process-level
6914
+ * field, `n<host:port>` a file-level one. `uid` resets on each `p<pid>` block
6915
+ * so a block without a `u` field can't inherit the prior block's owner. We
6916
+ * collect the UID for every loopback/wildcard name so the caller can reject an
6917
+ * ambiguous set rather than taking the first.
6918
+ */
6919
+ function parseLsofForUid(text) {
6920
+ const uids = [];
6921
+ let uid = null;
6922
+ for (const line of text.split("\n")) {
6923
+ const tag = line[0];
6924
+ if (tag === "p") uid = null;
6925
+ else if (tag === "u") {
6926
+ const parsed = Number(line.slice(1));
6927
+ uid = Number.isInteger(parsed) ? parsed : null;
6928
+ } else if (tag === "n" && uid !== null && isLoopbackLsofName(line.slice(1))) uids.push(uid);
6929
+ }
6930
+ return uids;
6931
+ }
6932
+ //#endregion
6761
6933
  //#region src/features/bridgePresence/BridgePresence.ts
6762
6934
  const DEFAULT_PORT = Number(process.env.CC_BRIDGE_PORT ?? 48732);
6763
6935
  const CONFIG_PATH = join(homedir(), ".b4m", "cc-bridge.json");
@@ -6783,11 +6955,15 @@ var BridgePresence = class {
6783
6955
  this.callbacks = {};
6784
6956
  this.started = false;
6785
6957
  this.stopped = false;
6958
+ this.generation = 0;
6959
+ this.trusted = false;
6960
+ this.wsConnectingGen = null;
6786
6961
  this.reconnectTimer = null;
6787
6962
  this.reconnectAttempts = 0;
6788
6963
  this.announceRetryTimer = null;
6789
6964
  this.announceAttempts = 0;
6790
6965
  this.startOpts = null;
6966
+ this.peerWarned = false;
6791
6967
  this.pendingWorkspaceName = null;
6792
6968
  this.pendingCapabilities = null;
6793
6969
  this.pendingSource = null;
@@ -6796,6 +6972,19 @@ var BridgePresence = class {
6796
6972
  setCallbacks(cbs) {
6797
6973
  this.callbacks = cbs;
6798
6974
  }
6975
+ /** True iff `gen` is still the live connection generation - the single spelling
6976
+ * of the ABA guard applied after every await (see `generation`). */
6977
+ isCurrent(gen) {
6978
+ return gen === this.generation;
6979
+ }
6980
+ /** Test-only: the generation currently holding the connectCommandWs in-flight
6981
+ * latch, or null when free. Exposed to pin the `finally` identity guard - a
6982
+ * stale generation's finally must not clear a live generation's latch (which
6983
+ * would let a second overlapping connect build an untracked socket). Not part
6984
+ * of the runtime contract. */
6985
+ get __wsConnectingGenForTests() {
6986
+ return this.wsConnectingGen;
6987
+ }
6799
6988
  /**
6800
6989
  * Probe the local bridge and, if present, announce this CLI session.
6801
6990
  * Returns true iff the announce succeeded (tavern presence is active).
@@ -6809,7 +6998,10 @@ var BridgePresence = class {
6809
6998
  if (this.started) return this.instanceId !== null;
6810
6999
  this.stopped = false;
6811
7000
  this.started = true;
7001
+ this.generation += 1;
7002
+ const gen = this.generation;
6812
7003
  const config = await readBridgeConfig();
7004
+ if (!this.isCurrent(gen)) return false;
6813
7005
  if (!config) {
6814
7006
  logger.debug("[tavern] cc-bridge not configured; CLI runs without tavern presence");
6815
7007
  return false;
@@ -6821,34 +7013,119 @@ var BridgePresence = class {
6821
7013
  this.pendingSource = opts.source ?? "b4m-cli";
6822
7014
  return this.attemptAnnounce();
6823
7015
  }
7016
+ /**
7017
+ * Classify the process holding the bridge port relative to this CLI, BEFORE
7018
+ * any secret leaves the process. Over loopback TCP the peer is whoever holds
7019
+ * the port; the secret file is plaintext and per-user, so a same-UID peer is
7020
+ * already trusted (it could read the file directly), and a different-UID (or
7021
+ * sandboxed) peer is the adversary this gate stops.
7022
+ *
7023
+ * Four outcomes, so the caller warns only on a real trust failure (a foreign
7024
+ * owner) and stays quiet on the benign ones:
7025
+ * - `trusted` - same-UID owner; disclosure is safe.
7026
+ * - `absent` - no loopback listener yet (cc-bridge simply not
7027
+ * started); fail-closed but quiet - just retry.
7028
+ * - `foreign` - a different-UID owner holds the port; fail-closed AND
7029
+ * log the security-worded warning (the real adversary).
7030
+ * - `undeterminable` - ownership can't be resolved: lookup tool missing/hung,
7031
+ * a read/spawn error, or an ambiguous owner set.
7032
+ * Fail-closed, but a host that can't run the probe is not
7033
+ * an attacker - log at debug, not as the security warning.
7034
+ *
7035
+ * A platform that can't resolve ownership (see `canResolveLoopbackOwner`) is
7036
+ * latched off earlier, in `attemptAnnounce`, so this method is only reached
7037
+ * where `getuid` exists and the platform has an owner probe.
7038
+ */
7039
+ async checkPeerTrust() {
7040
+ const port = this.config?.port ?? DEFAULT_PORT;
7041
+ let owner;
7042
+ try {
7043
+ owner = await resolveLoopbackListenerOwner(port);
7044
+ } catch (err) {
7045
+ logger.debug(`[tavern] loopback owner lookup threw: ${err.message}`);
7046
+ return "undeterminable";
7047
+ }
7048
+ if (owner.kind === "no-listener") return "absent";
7049
+ if (owner.kind === "unknown") return "undeterminable";
7050
+ if (owner.uid !== process.getuid()) return "foreign";
7051
+ this.peerWarned = false;
7052
+ return "trusted";
7053
+ }
7054
+ /** Log the foreign-owner security warning once per untrusted state. */
7055
+ warnUntrustedPeerOnce() {
7056
+ if (this.peerWarned) return;
7057
+ this.peerWarned = true;
7058
+ logger.warn("[tavern] bridge port is held by a different user; not disclosing secret or handling commands");
7059
+ }
6824
7060
  /** One announce attempt. Schedules a retry on failure; wires up the
6825
7061
  * command WS + initial status on success. Idempotent: re-entering after
6826
7062
  * a successful announce short-circuits at the instanceId guard. */
6827
7063
  async attemptAnnounce() {
6828
7064
  if (this.stopped || !this.config || !this.startOpts) return false;
6829
7065
  if (this.instanceId) return true;
7066
+ if (!canResolveLoopbackOwner()) {
7067
+ logger.debug("[tavern] ownership unresolvable on this platform; tavern presence disabled");
7068
+ return false;
7069
+ }
7070
+ const gen = this.generation;
7071
+ const startOpts = this.startOpts;
7072
+ if (!await this.gateEgress(gen, "announce")) return false;
6830
7073
  const instanceId = v4();
6831
7074
  const workspaceName = this.pendingWorkspaceName;
6832
7075
  const capabilities = this.pendingCapabilities;
6833
7076
  const source = this.pendingSource;
6834
- if (!await this.announce({
7077
+ const announced = await this.announce({
6835
7078
  instanceId,
6836
7079
  source,
6837
7080
  workspaceName,
6838
- workspacePath: this.startOpts.workspacePath,
7081
+ workspacePath: startOpts.workspacePath,
6839
7082
  capabilities
6840
- })) {
7083
+ });
7084
+ if (!this.isCurrent(gen) || this.stopped || !this.config) return false;
7085
+ if (!announced) {
7086
+ this.trusted = false;
6841
7087
  this.scheduleAnnounceRetry();
6842
7088
  return false;
6843
7089
  }
6844
7090
  this.instanceId = instanceId;
6845
7091
  this.announceAttempts = 0;
6846
7092
  logger.info(`[tavern] announced ${workspaceName} to cc-bridge on 127.0.0.1:${this.config.port ?? DEFAULT_PORT}`);
6847
- this.connectCommandWs();
7093
+ this.connectCommandWs().catch((err) => logger.debug(`[tavern] connectCommandWs threw: ${err.message}`));
6848
7094
  this.emitEvent({
6849
7095
  type: "status",
6850
7096
  status: "idle"
6851
- });
7097
+ }).catch(() => {});
7098
+ return true;
7099
+ }
7100
+ /** Signal the untrusted condition at the right volume: the security-worded
7101
+ * warning only for a real foreign owner, and a quiet debug line for the
7102
+ * benign cases (bridge not started yet, or ownership undeterminable) so we
7103
+ * don't cry wolf on every backed-off retry. */
7104
+ signalUntrusted(trust, phase) {
7105
+ if (trust === "foreign") this.warnUntrustedPeerOnce();
7106
+ else if (trust === "undeterminable") logger.debug(`[tavern] bridge port owner undeterminable; retrying ${phase}`);
7107
+ else logger.debug(`[tavern] cc-bridge not reachable yet; retrying ${phase}`);
7108
+ }
7109
+ /**
7110
+ * Run the pre-disclosure ownership gate for one egress phase and open or keep
7111
+ * the trust latch closed. The caller passes the connection `generation` it
7112
+ * captured BEFORE this await, so a stop()+start() cycle (or a bare stop())
7113
+ * landing during the probe is detected and this stale continuation bails.
7114
+ * Returns true iff the gate is open (secret disclosure is now permitted for
7115
+ * this generation); false means the caller must return - either the owner is
7116
+ * untrusted (a retry has been scheduled) or the generation moved.
7117
+ */
7118
+ async gateEgress(gen, phase) {
7119
+ const trust = await this.checkPeerTrust();
7120
+ if (!this.isCurrent(gen) || this.stopped || !this.config) return false;
7121
+ if (trust !== "trusted") {
7122
+ this.trusted = false;
7123
+ this.signalUntrusted(trust, phase);
7124
+ if (phase === "announce") this.scheduleAnnounceRetry();
7125
+ else this.scheduleReconnect();
7126
+ return false;
7127
+ }
7128
+ this.trusted = true;
6852
7129
  return true;
6853
7130
  }
6854
7131
  scheduleAnnounceRetry() {
@@ -6857,17 +7134,22 @@ var BridgePresence = class {
6857
7134
  const delay = Math.min(1e3 * 2 ** (this.announceAttempts - 1), 3e4);
6858
7135
  this.announceRetryTimer = setTimeout(() => {
6859
7136
  this.announceRetryTimer = null;
6860
- this.attemptAnnounce();
7137
+ this.attemptAnnounce().catch((err) => logger.debug(`[tavern] announce retry threw: ${err.message}`));
6861
7138
  }, delay);
6862
7139
  }
6863
7140
  /** Emit an event for this session. No-op if the bridge isn't up. Events
6864
7141
  * leave in strict order - see `emitQueue` comment. */
6865
7142
  async emitEvent(event) {
6866
7143
  if (!this.config || !this.instanceId) return;
6867
- const task = () => this.post("/event", {
6868
- instanceId: this.instanceId,
6869
- event
6870
- }).catch((err) => logger.info(`[tavern] emitEvent ${event.type} failed: ${err.message}`));
7144
+ const instanceId = this.instanceId;
7145
+ const gen = this.generation;
7146
+ const task = () => {
7147
+ if (!this.isCurrent(gen)) return;
7148
+ return this.post("/event", {
7149
+ instanceId,
7150
+ event
7151
+ }).catch((err) => logger.info(`[tavern] emitEvent ${event.type} failed: ${err.message}`));
7152
+ };
6871
7153
  this.emitQueue = this.emitQueue.then(task, task);
6872
7154
  return this.emitQueue;
6873
7155
  }
@@ -6885,6 +7167,7 @@ var BridgePresence = class {
6885
7167
  async stop(reason = "cli_exit") {
6886
7168
  if (this.stopped || !this.started) return;
6887
7169
  this.stopped = true;
7170
+ this.generation += 1;
6888
7171
  if (this.reconnectTimer) {
6889
7172
  clearTimeout(this.reconnectTimer);
6890
7173
  this.reconnectTimer = null;
@@ -6893,13 +7176,14 @@ var BridgePresence = class {
6893
7176
  clearTimeout(this.announceRetryTimer);
6894
7177
  this.announceRetryTimer = null;
6895
7178
  }
7179
+ const wsWasLive = this.ws !== null;
6896
7180
  if (this.ws) {
6897
7181
  try {
6898
7182
  this.ws.close();
6899
7183
  } catch {}
6900
7184
  this.ws = null;
6901
7185
  }
6902
- if (this.config && this.instanceId) await this.post("/disconnect", {
7186
+ if (this.config && this.instanceId && wsWasLive) await this.post("/disconnect", {
6903
7187
  instanceId: this.instanceId,
6904
7188
  reason
6905
7189
  }).catch(() => {});
@@ -6912,6 +7196,8 @@ var BridgePresence = class {
6912
7196
  this.pendingSource = null;
6913
7197
  this.announceAttempts = 0;
6914
7198
  this.reconnectAttempts = 0;
7199
+ this.peerWarned = false;
7200
+ this.trusted = false;
6915
7201
  this.emitQueue = Promise.resolve();
6916
7202
  }
6917
7203
  async announce(body) {
@@ -6925,6 +7211,7 @@ var BridgePresence = class {
6925
7211
  }
6926
7212
  async post(path, body) {
6927
7213
  if (!this.config) throw new Error("bridge config not loaded");
7214
+ if (!this.trusted) throw new Error("bridge peer not trusted; refusing to disclose secret");
6928
7215
  const url = `http://127.0.0.1:${this.config.port ?? DEFAULT_PORT}${path}?secret=${encodeURIComponent(this.config.hookSecret)}`;
6929
7216
  const res = await fetch(url, {
6930
7217
  method: "POST",
@@ -6934,42 +7221,58 @@ var BridgePresence = class {
6934
7221
  });
6935
7222
  if (!res.ok) throw new Error(`bridge ${path} -> ${res.status}`);
6936
7223
  }
6937
- connectCommandWs() {
7224
+ async connectCommandWs() {
6938
7225
  if (this.stopped || !this.config || !this.instanceId) return;
6939
- const url = `ws://127.0.0.1:${this.config.port ?? DEFAULT_PORT}/commands?instanceId=${encodeURIComponent(this.instanceId)}&secret=${encodeURIComponent(this.config.hookSecret)}`;
6940
- let ws;
7226
+ const gen = this.generation;
7227
+ if (this.ws || this.wsConnectingGen === gen) return;
7228
+ this.wsConnectingGen = gen;
6941
7229
  try {
6942
- ws = new WebSocket(url);
6943
- } catch (err) {
6944
- logger.debug(`[tavern] command WS construct failed: ${err.message}`);
6945
- this.scheduleReconnect();
6946
- return;
6947
- }
6948
- this.ws = ws;
6949
- ws.on("open", () => {
6950
- this.reconnectAttempts = 0;
6951
- logger.debug("[tavern] command WS open");
6952
- });
6953
- ws.on("message", (raw) => {
6954
- let frame = null;
7230
+ if (!await this.gateEgress(gen, "command WS")) return;
7231
+ const config = this.config;
7232
+ const instanceId = this.instanceId;
7233
+ if (!config || !instanceId) return;
7234
+ const url = `ws://127.0.0.1:${config.port ?? DEFAULT_PORT}/commands?instanceId=${encodeURIComponent(instanceId)}&secret=${encodeURIComponent(config.hookSecret)}`;
7235
+ let ws;
6955
7236
  try {
6956
- frame = JSON.parse(raw.toString());
6957
- } catch {
6958
- logger.debug("[tavern] malformed command frame; ignored");
7237
+ ws = new WebSocket(url);
7238
+ } catch (err) {
7239
+ logger.debug(`[tavern] command WS construct failed: ${err.message}`);
7240
+ this.trusted = false;
7241
+ this.scheduleReconnect();
6959
7242
  return;
6960
7243
  }
6961
- if (!frame?.command) return;
6962
- this.dispatchCommand(frame.command).catch((err) => logger.warn(`[tavern] command dispatch threw: ${err.message}`));
6963
- });
6964
- ws.on("close", () => {
6965
- this.ws = null;
6966
- if (this.stopped) return;
6967
- logger.debug("[tavern] command WS closed; reconnecting");
6968
- this.scheduleReconnect();
6969
- });
6970
- ws.on("error", (err) => {
6971
- logger.debug(`[tavern] command WS error: ${err.message}`);
6972
- });
7244
+ this.ws = ws;
7245
+ ws.on("open", () => {
7246
+ if (this.ws !== ws) return;
7247
+ this.reconnectAttempts = 0;
7248
+ logger.debug("[tavern] command WS open");
7249
+ });
7250
+ ws.on("message", (raw) => {
7251
+ if (this.ws !== ws) return;
7252
+ let frame = null;
7253
+ try {
7254
+ frame = JSON.parse(raw.toString());
7255
+ } catch {
7256
+ logger.debug("[tavern] malformed command frame; ignored");
7257
+ return;
7258
+ }
7259
+ if (!frame?.command) return;
7260
+ this.dispatchCommand(frame.command).catch((err) => logger.warn(`[tavern] command dispatch threw: ${err.message}`));
7261
+ });
7262
+ ws.on("close", () => {
7263
+ if (this.ws !== ws) return;
7264
+ this.ws = null;
7265
+ this.trusted = false;
7266
+ if (this.stopped) return;
7267
+ logger.debug("[tavern] command WS closed; reconnecting");
7268
+ this.scheduleReconnect();
7269
+ });
7270
+ ws.on("error", (err) => {
7271
+ logger.debug(`[tavern] command WS error: ${err.message}`);
7272
+ });
7273
+ } finally {
7274
+ if (this.wsConnectingGen === gen) this.wsConnectingGen = null;
7275
+ }
6973
7276
  }
6974
7277
  scheduleReconnect() {
6975
7278
  if (this.stopped || this.reconnectTimer) return;
@@ -6977,10 +7280,11 @@ var BridgePresence = class {
6977
7280
  const delay = Math.min(500 * 2 ** (this.reconnectAttempts - 1), 1e4);
6978
7281
  this.reconnectTimer = setTimeout(() => {
6979
7282
  this.reconnectTimer = null;
6980
- this.connectCommandWs();
7283
+ this.connectCommandWs().catch((err) => logger.debug(`[tavern] command WS reconnect threw: ${err.message}`));
6981
7284
  }, delay);
6982
7285
  }
6983
7286
  async dispatchCommand(command) {
7287
+ if (this.stopped) return;
6984
7288
  switch (command.type) {
6985
7289
  case "send_prompt":
6986
7290
  if (this.callbacks.onSendPrompt) await this.callbacks.onSendPrompt(command.text);
@@ -7130,7 +7434,7 @@ const infoCommands = [
7130
7434
  {
7131
7435
  name: "help",
7132
7436
  run: (_args, ctx) => {
7133
- const customCommands = ctx.customCommandStore.getAllCommands();
7437
+ const customCommands = ctx.customCommandStore.getModelReachableCommands();
7134
7438
  const hasCustomCommands = customCommands.length > 0;
7135
7439
  console.log(`
7136
7440
  Available commands:
@@ -7321,6 +7625,48 @@ async function dispatch(name, args, ctx) {
7321
7625
  return true;
7322
7626
  }
7323
7627
  //#endregion
7628
+ //#region src/commands/sandboxNetworkCommand.ts
7629
+ async function handleSandboxNetworkCommand(deps, arg) {
7630
+ const { orchestrator, configStore, permissionManager } = deps;
7631
+ if (!orchestrator) return "Sandbox not initialized";
7632
+ if (arg !== "on" && arg !== "off") return "Usage: /sandbox:network <on|off>";
7633
+ const enable = arg === "on";
7634
+ if (enable && !orchestrator.isAvailable()) return "Sandbox runtime not available on this platform";
7635
+ const active = await orchestrator.setNetworkEnabled(enable);
7636
+ if (enable && !active) {
7637
+ permissionManager?.setSandboxState(orchestrator.getMode(), orchestrator.isActive());
7638
+ await configStore.saveSandboxConfig(orchestrator.getConfig());
7639
+ return "Failed to start the network proxy; network stays disabled (fail-closed).";
7640
+ }
7641
+ permissionManager?.setSandboxState(orchestrator.getMode(), orchestrator.isActive());
7642
+ await configStore.saveSandboxConfig(orchestrator.getConfig());
7643
+ if (!enable) return "Sandbox network disabled (fail-closed)";
7644
+ const port = orchestrator.getProxyManager()?.getPort();
7645
+ return `Sandbox network egress enabled${port ? ` (proxy-aware clients filtered on port ${port})` : ""}`;
7646
+ }
7647
+ //#endregion
7648
+ //#region src/commands/sandboxTrustDomainCommand.ts
7649
+ async function handleSandboxTrustDomainCommand(deps, domains) {
7650
+ const { orchestrator, configStore } = deps;
7651
+ if (!orchestrator) return "Sandbox not initialized";
7652
+ const clean = domains.map((d) => d.trim()).filter(Boolean);
7653
+ if (clean.length === 0) return "Usage: /sandbox:trust-domain <domain> [...]";
7654
+ if (!orchestrator.getProxyManager()) return "Network proxy not initialized";
7655
+ const lines = [];
7656
+ for (const domain of clean) {
7657
+ orchestrator.addAllowedDomain(domain);
7658
+ lines.push(` Added: ${domain}`);
7659
+ }
7660
+ try {
7661
+ await configStore.saveSandboxConfig(orchestrator.getConfig());
7662
+ } catch (err) {
7663
+ const msg = err instanceof Error ? err.message : String(err);
7664
+ return `${lines.join("\n")}\nWarning: failed to persist the trusted domains (${msg}); they apply for this session only.`;
7665
+ }
7666
+ lines.push(`Trusted ${clean.length} domain(s)`);
7667
+ return lines.join("\n");
7668
+ }
7669
+ //#endregion
7324
7670
  //#region src/session/turnController.ts
7325
7671
  /**
7326
7672
  * Turn lifecycle (issue #228, phase 2).
@@ -7365,7 +7711,7 @@ async function runTurn(message, ctx) {
7365
7711
  const systemPrompt = buildSystemPrompt(config?.preferences.promptVariant ?? "current", {
7366
7712
  contextContent,
7367
7713
  agentStore: agentStore || void 0,
7368
- customCommands: customCommandStore.getAllCommands(),
7714
+ customCommands: customCommandStore.getModelReachableCommands(),
7369
7715
  enableSkillTool: config?.preferences.enableSkillTool !== false,
7370
7716
  additionalDirectories,
7371
7717
  featureModulePrompts: featureRegistry?.getSystemPromptSections() || void 0,
@@ -8050,6 +8396,14 @@ function CliApp() {
8050
8396
  });
8051
8397
  const pinnedSessionId = process.env.B4M_SESSION_ID;
8052
8398
  const resumeSessionId = process.env.B4M_RESUME_ID;
8399
+ if (pinnedSessionId && !isValidSessionId(pinnedSessionId)) {
8400
+ console.error(`Invalid session id (--session-id / B4M_SESSION_ID): must match ${SESSION_ID_PATTERN.source}`);
8401
+ process.exit(1);
8402
+ }
8403
+ if (resumeSessionId && !isValidSessionId(resumeSessionId)) {
8404
+ console.error(`Invalid session id (--resume / B4M_RESUME_ID): must match ${SESSION_ID_PATTERN.source}`);
8405
+ process.exit(1);
8406
+ }
8053
8407
  let newSession;
8054
8408
  if (resumeSessionId) {
8055
8409
  const resumed = await state.sessionStore.load(resumeSessionId);
@@ -8234,8 +8588,10 @@ function CliApp() {
8234
8588
  customCommandStore: state.customCommandStore,
8235
8589
  subagentOrchestrator: orchestrator,
8236
8590
  sessionId: newSession.id,
8237
- permissionManager,
8238
- promptFn,
8591
+ permission: {
8592
+ permissionManager,
8593
+ promptFn
8594
+ },
8239
8595
  allowedDirectories: additionalDirectories
8240
8596
  })], cliWrapDeps)[0] : null;
8241
8597
  const findDefinitionTool = wrapTools([createFindDefinitionTool(additionalDirectories)], cliWrapDeps)[0];
@@ -8267,6 +8623,7 @@ function CliApp() {
8267
8623
  logger,
8268
8624
  reservedToolNames: [...loadedB4mTools, ...cliTools].map((t) => t.toolSchema.name).concat("tool_search")
8269
8625
  });
8626
+ rewireReservedNames(state, featureRegistry);
8270
8627
  const featureModuleToolNames = featureRegistry.getAllToolNames();
8271
8628
  if (featureModuleToolNames.length > 0) registerFeatureModuleTools(featureModuleToolNames);
8272
8629
  if (wsManager && featureRegistry.hasModules) featureRegistry.registerAllWsHandlers(wsManager);
@@ -9109,7 +9466,7 @@ function CliApp() {
9109
9466
  console.log(fellBack ? "✅ Session saved with local fallback handoff" : "✅ Session saved with refreshed handoff");
9110
9467
  };
9111
9468
  const handleCommand = async (command, args) => {
9112
- const customCommand = state.customCommandStore.getCommand(command);
9469
+ const customCommand = state.customCommandStore.getModelReachableCommand(command);
9113
9470
  if (customCommand) try {
9114
9471
  const sourceIcon = customCommand.source === "global" ? "🏠" : "📁";
9115
9472
  console.log(`${sourceIcon} Executing custom command: /${command}`);
@@ -9661,7 +10018,7 @@ function CliApp() {
9661
10018
  const variantForCount = state.config?.preferences.promptVariant ?? "current";
9662
10019
  const corePromptTokens = tokenCounter.countTokens(buildSystemPrompt(variantForCount));
9663
10020
  const projectContextTokens = state.contextContent ? tokenCounter.countTokens(state.contextContent) : 0;
9664
- const commands = state.customCommandStore.getAllCommands();
10021
+ const commands = state.customCommandStore.getModelReachableCommands();
9665
10022
  const skillsSection = buildSkillsPromptSection(commands);
9666
10023
  const skillsTokens = skillsSection ? tokenCounter.countTokens(skillsSection) : 0;
9667
10024
  const agentDirectoryTokens = state.agentStore ? tokenCounter.countTokens(state.agentStore.getDirectoryContext()) : 0;
@@ -9753,9 +10110,9 @@ function CliApp() {
9753
10110
  break;
9754
10111
  }
9755
10112
  case "commands": {
9756
- const customCommands = state.customCommandStore.getAllCommands();
9757
- const globalCommands = state.customCommandStore.getCommandsBySource("global");
9758
- const projectCommands = state.customCommandStore.getCommandsBySource("project");
10113
+ const customCommands = state.customCommandStore.getModelReachableCommands();
10114
+ const globalCommands = customCommands.filter((cmd) => cmd.source === "global");
10115
+ const projectCommands = customCommands.filter((cmd) => cmd.source === "project");
9759
10116
  console.log("\n📝 Custom Commands:\n");
9760
10117
  if (customCommands.length === 0) {
9761
10118
  console.log("No custom commands found.");
@@ -9937,9 +10294,10 @@ function CliApp() {
9937
10294
  state.sandboxOrchestrator.setMode("auto-allow");
9938
10295
  state.permissionManager?.setSandboxState("auto-allow", state.sandboxOrchestrator.isActive());
9939
10296
  if (state.sandboxOrchestrator.getConfig().network.enabled) {
9940
- await state.sandboxOrchestrator.startProxy();
10297
+ const on = await state.sandboxOrchestrator.setNetworkEnabled(true);
9941
10298
  const pm = state.sandboxOrchestrator.getProxyManager();
9942
- if (pm?.isRunning()) console.log(`🌐 Network proxy started on port ${pm.getPort()}`);
10299
+ if (on && pm?.isRunning()) console.log(`🌐 Network proxy started on port ${pm.getPort()}`);
10300
+ else console.log("⚠️ Network filtering requested but the proxy failed to start; egress disabled");
9943
10301
  }
9944
10302
  await state.configStore.saveSandboxConfig(state.sandboxOrchestrator.getConfig());
9945
10303
  console.log("Sandbox enabled (auto-allow mode)");
@@ -9949,7 +10307,7 @@ function CliApp() {
9949
10307
  console.log("Sandbox not initialized");
9950
10308
  break;
9951
10309
  }
9952
- await state.sandboxOrchestrator.stopProxy();
10310
+ await state.sandboxOrchestrator.setNetworkEnabled(false);
9953
10311
  state.sandboxOrchestrator.setMode("disabled");
9954
10312
  state.permissionManager?.setSandboxState("disabled", false);
9955
10313
  await state.configStore.saveSandboxConfig(state.sandboxOrchestrator.getConfig());
@@ -9975,35 +10333,19 @@ function CliApp() {
9975
10333
  console.log(`Sandbox mode set to: ${modeArg}`);
9976
10334
  break;
9977
10335
  }
9978
- case "sandbox:trust-domain": {
9979
- if (!state.sandboxOrchestrator) {
9980
- console.log("Sandbox not initialized");
9981
- break;
9982
- }
9983
- if (args.length === 0) {
9984
- console.log("Usage: /sandbox:trust-domain <domain> [...]");
9985
- break;
9986
- }
9987
- const proxyMgr = state.sandboxOrchestrator.getProxyManager();
9988
- if (!proxyMgr) {
9989
- console.log("Network proxy not initialized");
9990
- break;
9991
- }
9992
- for (const domain of args) {
9993
- proxyMgr.addAllowedDomain(domain);
9994
- console.log(` Added: ${domain}`);
9995
- }
9996
- const currentSandboxConfig = state.sandboxOrchestrator.getConfig();
9997
- await state.configStore.saveSandboxConfig({
9998
- ...currentSandboxConfig,
9999
- network: {
10000
- ...currentSandboxConfig.network,
10001
- allowedDomains: proxyMgr.getAllowedDomains()
10002
- }
10003
- });
10004
- console.log(`Trusted ${args.length} domain(s)`);
10336
+ case "sandbox:network":
10337
+ console.log(await handleSandboxNetworkCommand({
10338
+ orchestrator: state.sandboxOrchestrator,
10339
+ configStore: state.configStore,
10340
+ permissionManager: state.permissionManager
10341
+ }, args[0]));
10342
+ break;
10343
+ case "sandbox:trust-domain":
10344
+ console.log(await handleSandboxTrustDomainCommand({
10345
+ orchestrator: state.sandboxOrchestrator,
10346
+ configStore: state.configStore
10347
+ }, args));
10005
10348
  break;
10006
- }
10007
10349
  case "sandbox:domains": {
10008
10350
  if (!state.sandboxOrchestrator) {
10009
10351
  console.log("Sandbox not initialized");
@@ -10195,7 +10537,9 @@ function CliApp() {
10195
10537
  logger,
10196
10538
  reservedToolNames: baseTools.map((t) => t.toolSchema.name)
10197
10539
  });
10198
- newFeatureRegistry = rebuilt.registry;
10540
+ const rebuiltRegistry = rebuilt.registry;
10541
+ newFeatureRegistry = rebuiltRegistry;
10542
+ rewireReservedNames(state, rebuiltRegistry);
10199
10543
  for (const skippedPlugin of rebuilt.skipped) console.error(`\n\x1b[33m⚠️ Plugin ${skippedPlugin.name} skipped: ${skippedPlugin.reason}\x1b[0m`);
10200
10544
  const newToolNames = newFeatureRegistry.getAllToolNames();
10201
10545
  if (newToolNames.length > 0) registerFeatureModuleTools(newToolNames);
@@ -10209,7 +10553,7 @@ function CliApp() {
10209
10553
  state.agent.setSystemPrompt(buildSystemPrompt(updatedConfig.preferences.promptVariant ?? "current", {
10210
10554
  contextContent: state.contextContent,
10211
10555
  agentStore: state.agentStore || void 0,
10212
- customCommands: state.customCommandStore.getAllCommands(),
10556
+ customCommands: state.customCommandStore.getModelReachableCommands(),
10213
10557
  enableSkillTool: updatedConfig.preferences.enableSkillTool !== false,
10214
10558
  enableDynamicAgentCreation: updatedConfig.preferences.enableDynamicAgentCreation === true,
10215
10559
  additionalDirectories: state.additionalDirectories,