@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.
- package/README.md +1 -0
- package/bin/bike4mind-cli.mjs +0 -5
- package/dist/AgentHistoryStore-B2NEOvSW.mjs +18014 -0
- package/dist/{ApiClient-BOWpVvTq.mjs → ApiClient-D0fQ2FT6.mjs} +2 -2
- package/dist/{BubblewrapRuntime-CkL9-gnG.mjs → BubblewrapRuntime-5bLPTwEC.mjs} +2 -2
- package/dist/{ConfigStore-DdHHCH2t.mjs → ConfigStore-qV7NrCgZ.mjs} +1154 -1365
- package/dist/{ProxyManager-C1-lgzEU.mjs → ProxyManager-B0-RuR2w.mjs} +21 -4
- package/dist/{SandboxOrchestrator-CIegJrCj.mjs → SandboxOrchestrator-BcUv9fQ3.mjs} +46 -12
- package/dist/{SandboxRuntimeAdapter-ChGlxSGQ.mjs → SandboxRuntimeAdapter-BgLUVTJL.mjs} +2 -2
- package/dist/{SandboxRuntimeAdapter-CKelGICD.mjs → SandboxRuntimeAdapter-BmHELLuM.mjs} +1 -1
- package/dist/{SeatbeltRuntime-Qqt19cAN.mjs → SeatbeltRuntime-C_Y8q8Mr.mjs} +9 -1
- package/dist/buildAgent-C-C-VGff.mjs +2001 -0
- package/dist/commands/acpCommand.mjs +8 -6
- package/dist/commands/apiCommand.mjs +1 -1
- package/dist/commands/doctorCommand.mjs +1 -1
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +26 -16
- package/dist/commands/mcpCommand.mjs +3 -3
- package/dist/commands/pluginCommand.mjs +1 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/index.mjs +444 -100
- package/dist/{package-Bqg2LSnH.mjs → package-CGZIoxcs.mjs} +1 -1
- package/dist/{serve-DO9Edl5S.mjs → serve-CPXqcEZr.mjs} +2 -2
- package/package.json +9 -10
- package/dist/AgentHistoryStore-kT9eNMRO.mjs +0 -39471
- package/dist/ProxyManager-B1jFWL7b.mjs +0 -3
- package/dist/SandboxOrchestrator-BbMDgjzr.mjs +0 -3
- 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
|
|
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 {
|
|
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-
|
|
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 {
|
|
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
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
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) =>
|
|
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)
|
|
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
|
-
|
|
7077
|
+
const announced = await this.announce({
|
|
6835
7078
|
instanceId,
|
|
6836
7079
|
source,
|
|
6837
7080
|
workspaceName,
|
|
6838
|
-
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
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
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
|
|
6940
|
-
|
|
7226
|
+
const gen = this.generation;
|
|
7227
|
+
if (this.ws || this.wsConnectingGen === gen) return;
|
|
7228
|
+
this.wsConnectingGen = gen;
|
|
6941
7229
|
try {
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
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
|
-
|
|
6957
|
-
} catch {
|
|
6958
|
-
logger.debug(
|
|
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
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
8238
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
9757
|
-
const globalCommands =
|
|
9758
|
-
const projectCommands =
|
|
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.
|
|
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.
|
|
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:
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
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
|
-
|
|
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.
|
|
10556
|
+
customCommands: state.customCommandStore.getModelReachableCommands(),
|
|
10213
10557
|
enableSkillTool: updatedConfig.preferences.enableSkillTool !== false,
|
|
10214
10558
|
enableDynamicAgentCreation: updatedConfig.preferences.enableDynamicAgentCreation === true,
|
|
10215
10559
|
additionalDirectories: state.additionalDirectories,
|