@snowyroad/braid 0.94.0 → 0.96.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 +29 -10
- package/dist/{chunk-267GOUH7.js → chunk-2TZT7YDG.js} +610 -109
- package/dist/cli.js +129 -9
- package/dist/{http-JA7L7WBG.js → http-5RVYBQJV.js} +1 -1
- package/package.json +2 -1
- package/service-shrinkwrap.json +155 -151
|
@@ -400,7 +400,7 @@ var InviteExpiredError = class extends Error {
|
|
|
400
400
|
}
|
|
401
401
|
};
|
|
402
402
|
async function redeemInvite(relayHttpUrl, code) {
|
|
403
|
-
const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-
|
|
403
|
+
const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-5RVYBQJV.js");
|
|
404
404
|
const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/invites/redeem`, {
|
|
405
405
|
method: "POST",
|
|
406
406
|
headers: { "content-type": "application/json" },
|
|
@@ -432,8 +432,8 @@ async function redeemInvite(relayHttpUrl, code) {
|
|
|
432
432
|
|
|
433
433
|
// src/config.ts
|
|
434
434
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "fs";
|
|
435
|
-
import { dirname as dirname4, join as
|
|
436
|
-
import { homedir as
|
|
435
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
436
|
+
import { homedir as homedir6 } from "os";
|
|
437
437
|
|
|
438
438
|
// src/keystore.ts
|
|
439
439
|
import { spawnSync as nodeSpawnSync } from "child_process";
|
|
@@ -492,7 +492,7 @@ function validateReplacementToken(newToken, currentToken, nowMs = Date.now()) {
|
|
|
492
492
|
return { ok: true };
|
|
493
493
|
}
|
|
494
494
|
async function mintAccessToken(relayHttpUrl, agentKey, fetchFn = fetch, dpopProof) {
|
|
495
|
-
const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-
|
|
495
|
+
const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-5RVYBQJV.js");
|
|
496
496
|
const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/agents/token`, {
|
|
497
497
|
method: "POST",
|
|
498
498
|
headers: {
|
|
@@ -1101,8 +1101,195 @@ async function chooseRelayWhenAmbiguous(agentName, matches, io = { input: proces
|
|
|
1101
1101
|
}
|
|
1102
1102
|
|
|
1103
1103
|
// src/toolPolicy.ts
|
|
1104
|
+
import { homedir as homedir4 } from "os";
|
|
1105
|
+
import { join as join3, resolve, sep } from "path";
|
|
1106
|
+
|
|
1107
|
+
// src/scope/protectedPaths.ts
|
|
1108
|
+
import path2 from "path";
|
|
1109
|
+
function join2(platform, ...segments) {
|
|
1110
|
+
return platform === "win32" ? path2.win32.join(...segments) : path2.posix.join(...segments);
|
|
1111
|
+
}
|
|
1112
|
+
function isAbsolute(platform, p) {
|
|
1113
|
+
return platform === "win32" ? path2.win32.isAbsolute(p) : path2.posix.isAbsolute(p);
|
|
1114
|
+
}
|
|
1115
|
+
function dedupe(paths) {
|
|
1116
|
+
return [...new Set(paths)];
|
|
1117
|
+
}
|
|
1118
|
+
function osDeniableReadPaths(home, env, platform) {
|
|
1119
|
+
const xdgConfigHome = env.XDG_CONFIG_HOME;
|
|
1120
|
+
const paths = [
|
|
1121
|
+
join2(platform, home, ".ssh"),
|
|
1122
|
+
join2(platform, home, ".aws"),
|
|
1123
|
+
join2(platform, home, ".gnupg"),
|
|
1124
|
+
join2(platform, home, ".kube/config"),
|
|
1125
|
+
join2(platform, home, ".npmrc"),
|
|
1126
|
+
join2(platform, home, ".netrc"),
|
|
1127
|
+
join2(platform, home, ".config/gh"),
|
|
1128
|
+
join2(platform, home, ".docker/config.json"),
|
|
1129
|
+
join2(platform, home, ".git-credentials"),
|
|
1130
|
+
join2(platform, home, ".config/git/credentials"),
|
|
1131
|
+
join2(platform, home, ".config/gcloud"),
|
|
1132
|
+
join2(platform, home, ".azure"),
|
|
1133
|
+
join2(platform, home, ".zsh_history"),
|
|
1134
|
+
join2(platform, home, ".bash_history"),
|
|
1135
|
+
join2(platform, home, ".local/share/fish/fish_history"),
|
|
1136
|
+
join2(platform, home, ".pypirc"),
|
|
1137
|
+
join2(platform, home, ".cargo/credentials"),
|
|
1138
|
+
join2(platform, home, ".cargo/credentials.toml"),
|
|
1139
|
+
join2(platform, home, ".gem/credentials"),
|
|
1140
|
+
join2(platform, home, ".m2/settings.xml"),
|
|
1141
|
+
join2(platform, home, ".gradle/gradle.properties"),
|
|
1142
|
+
join2(platform, home, ".composer/auth.json"),
|
|
1143
|
+
join2(platform, home, ".bundle/config"),
|
|
1144
|
+
join2(platform, home, ".terraform.d/credentials.tfrc.json"),
|
|
1145
|
+
join2(platform, home, ".terraformrc"),
|
|
1146
|
+
join2(platform, home, ".vault-token")
|
|
1147
|
+
];
|
|
1148
|
+
if (xdgConfigHome && isAbsolute(platform, xdgConfigHome)) {
|
|
1149
|
+
paths.push(join2(platform, xdgConfigHome, "gh"));
|
|
1150
|
+
paths.push(join2(platform, xdgConfigHome, "git/credentials"));
|
|
1151
|
+
}
|
|
1152
|
+
if (platform === "win32") {
|
|
1153
|
+
const appData = env.APPDATA ?? join2(platform, home, "AppData/Roaming");
|
|
1154
|
+
const userProfile = env.USERPROFILE ?? home;
|
|
1155
|
+
paths.push(join2(platform, appData, "GitHub CLI"));
|
|
1156
|
+
paths.push(join2(platform, appData, "gcloud"));
|
|
1157
|
+
paths.push(join2(platform, appData, "Microsoft/Windows/PowerShell/PSReadLine"));
|
|
1158
|
+
paths.push(join2(platform, userProfile, "_netrc"));
|
|
1159
|
+
}
|
|
1160
|
+
return dedupe(paths);
|
|
1161
|
+
}
|
|
1162
|
+
function toolOnlyReadPaths(home, _env, platform) {
|
|
1163
|
+
return dedupe([
|
|
1164
|
+
join2(platform, home, ".claude/.credentials.json"),
|
|
1165
|
+
join2(platform, home, ".codex/auth.json"),
|
|
1166
|
+
join2(platform, home, ".gemini/oauth_creds.json"),
|
|
1167
|
+
join2(platform, home, ".gemini/google_accounts.json"),
|
|
1168
|
+
join2(platform, home, ".local/share/opencode/auth.json")
|
|
1169
|
+
]);
|
|
1170
|
+
}
|
|
1171
|
+
function protectedReadPaths(home, env, platform, braidConfigDirs) {
|
|
1172
|
+
return dedupe([
|
|
1173
|
+
...osDeniableReadPaths(home, env, platform),
|
|
1174
|
+
...toolOnlyReadPaths(home, env, platform),
|
|
1175
|
+
...braidConfigDirs
|
|
1176
|
+
]);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/readScope.ts
|
|
1180
|
+
import { realpathSync } from "fs";
|
|
1104
1181
|
import { homedir as homedir3 } from "os";
|
|
1105
|
-
import
|
|
1182
|
+
import * as posixPath from "path/posix";
|
|
1183
|
+
import * as win32Path from "path/win32";
|
|
1184
|
+
function pathModuleFor(platform) {
|
|
1185
|
+
return platform === "win32" ? win32Path : posixPath;
|
|
1186
|
+
}
|
|
1187
|
+
function foldCase(p, platform) {
|
|
1188
|
+
return platform === "darwin" || platform === "win32" ? p.toLowerCase() : p;
|
|
1189
|
+
}
|
|
1190
|
+
function canonicalize(lexical, pathMod, realpath) {
|
|
1191
|
+
let current = lexical;
|
|
1192
|
+
let remainder = "";
|
|
1193
|
+
while (true) {
|
|
1194
|
+
try {
|
|
1195
|
+
const resolved = realpath(current);
|
|
1196
|
+
return pathMod.normalize(remainder ? pathMod.join(resolved, remainder) : resolved);
|
|
1197
|
+
} catch {
|
|
1198
|
+
const parent = pathMod.dirname(current);
|
|
1199
|
+
if (parent === current) {
|
|
1200
|
+
return lexical;
|
|
1201
|
+
}
|
|
1202
|
+
const base = pathMod.basename(current);
|
|
1203
|
+
remainder = remainder ? pathMod.join(base, remainder) : base;
|
|
1204
|
+
current = parent;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function expandHome(raw, home, pathMod) {
|
|
1209
|
+
if (raw === "~") {
|
|
1210
|
+
return home;
|
|
1211
|
+
}
|
|
1212
|
+
if (raw.startsWith("~/") || pathMod === win32Path && raw.startsWith("~\\")) {
|
|
1213
|
+
return home + pathMod.sep + raw.slice(2);
|
|
1214
|
+
}
|
|
1215
|
+
if (raw.startsWith("~")) {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
return raw;
|
|
1219
|
+
}
|
|
1220
|
+
function normalizeCandidate(raw, scope) {
|
|
1221
|
+
if (!raw) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
const platform = scope.platform ?? process.platform;
|
|
1225
|
+
const pathMod = pathModuleFor(platform);
|
|
1226
|
+
const home = scope.home ?? homedir3();
|
|
1227
|
+
const expanded = expandHome(raw, home, pathMod);
|
|
1228
|
+
if (expanded === null) {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
const absolute = pathMod.isAbsolute(expanded) ? expanded : platform === "win32" ? pathMod.resolve(scope.cwd, expanded) : scope.cwd + pathMod.sep + expanded;
|
|
1232
|
+
const lexical = pathMod.normalize(absolute);
|
|
1233
|
+
const realpath = scope.realpath ?? realpathSync.native;
|
|
1234
|
+
const canonical = canonicalize(absolute, pathMod, realpath);
|
|
1235
|
+
return { lexical, canonical };
|
|
1236
|
+
}
|
|
1237
|
+
function isInsideRoot(p, root, pathMod, platform) {
|
|
1238
|
+
const foldedP = foldCase(p, platform);
|
|
1239
|
+
const foldedRoot = foldCase(root, platform);
|
|
1240
|
+
return foldedP === foldedRoot || foldedP.startsWith(foldedRoot + pathMod.sep);
|
|
1241
|
+
}
|
|
1242
|
+
function isSameDrive(p, root, platform) {
|
|
1243
|
+
if (platform !== "win32") {
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1246
|
+
if (p.startsWith("\\\\") || root.startsWith("\\\\")) {
|
|
1247
|
+
return p.toLowerCase() === root.toLowerCase() || p.toLowerCase().startsWith(root.toLowerCase());
|
|
1248
|
+
}
|
|
1249
|
+
const driveOf = (s) => {
|
|
1250
|
+
const parsed = win32Path.parse(s);
|
|
1251
|
+
return parsed.root.slice(0, 2).toLowerCase();
|
|
1252
|
+
};
|
|
1253
|
+
return driveOf(p) === driveOf(root);
|
|
1254
|
+
}
|
|
1255
|
+
function formIsInsideRootSet(form, roots, pathMod, platform) {
|
|
1256
|
+
if (platform === "win32" && form.startsWith("\\\\")) {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
for (const root of roots) {
|
|
1260
|
+
if (platform === "win32" && !isSameDrive(form, root, platform)) {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (isInsideRoot(form, root, pathMod, platform)) {
|
|
1264
|
+
return true;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
function isInsideRootSet(raw, scope) {
|
|
1270
|
+
const platform = scope.platform ?? process.platform;
|
|
1271
|
+
const pathMod = pathModuleFor(platform);
|
|
1272
|
+
const normalized = normalizeCandidate(raw, scope);
|
|
1273
|
+
if (!normalized) {
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
const roots = [scope.cwd, ...scope.allowRead].map((r) => {
|
|
1277
|
+
const n = pathMod.normalize(r);
|
|
1278
|
+
return n.length > 1 && n.endsWith(pathMod.sep) && !isFilesystemRoot(n, platform) ? n.slice(0, -1) : n;
|
|
1279
|
+
});
|
|
1280
|
+
const lexicalOk = formIsInsideRootSet(normalized.lexical, roots, pathMod, platform);
|
|
1281
|
+
const canonicalOk = formIsInsideRootSet(normalized.canonical, roots, pathMod, platform);
|
|
1282
|
+
return lexicalOk && canonicalOk;
|
|
1283
|
+
}
|
|
1284
|
+
function isFilesystemRoot(p, platform) {
|
|
1285
|
+
if (!p) {
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
const pathMod = pathModuleFor(platform);
|
|
1289
|
+
return pathMod.parse(p).root === p;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// src/toolPolicy.ts
|
|
1106
1293
|
function narrowestMode(...modes) {
|
|
1107
1294
|
for (const m of modes) {
|
|
1108
1295
|
if (m === "readonly") return "readonly";
|
|
@@ -1122,10 +1309,10 @@ function toolStatusLine(mode, cause = "operator") {
|
|
|
1122
1309
|
if (cause === "channel") {
|
|
1123
1310
|
return "Tool status: your operator granted this agent full tool access, but THIS CHANNEL restricts you to read-only here. You can still read channel context, sources, tasks, artifacts, and memory, but requests to run commands, edit files, or write to the channel are denied for this channel. This is governed by the channel's own configuration (its capability bundle), NOT your agent's bridge tool mode \u2014 changing the agent's bridge settings will not lift it. A channel admin must adjust the channel's capability bundle to allow writes.";
|
|
1124
1311
|
}
|
|
1125
|
-
return "Tool status: this agent runs in read-and-reply mode; requests to run commands or edit files are denied by the operator's bridge settings (changeable with: braid tools full <agent>).";
|
|
1312
|
+
return "Tool status: this agent runs in read-and-reply mode; requests to run commands or edit files are denied by the operator's bridge settings (changeable with: braid tools full <agent>). File reads are limited to the launch directory and any --allow-read paths.";
|
|
1126
1313
|
}
|
|
1127
1314
|
var denialHintShown = false;
|
|
1128
|
-
function takeDenialHint(policy, cause = "operator") {
|
|
1315
|
+
function takeDenialHint(policy, cause = "operator", kind = "mode", cwd) {
|
|
1129
1316
|
if (denialHintShown) return null;
|
|
1130
1317
|
denialHintShown = true;
|
|
1131
1318
|
if (cause === "channel") {
|
|
@@ -1133,6 +1320,10 @@ function takeDenialHint(policy, cause = "operator") {
|
|
|
1133
1320
|
}
|
|
1134
1321
|
const raw = policy.agentName;
|
|
1135
1322
|
const name = raw && isShellSafeName(raw) ? raw : "<agent-name>";
|
|
1323
|
+
if (kind === "scope") {
|
|
1324
|
+
const where = cwd ? ` (${cwd})` : "";
|
|
1325
|
+
return `[braid] In read-and-reply mode this agent may only read files under its launch directory${where}. To widen: start it with --allow-read <dir>, or add the path to scope.allowRead in its agent config. For full tool access: braid tools full ${name}.`;
|
|
1326
|
+
}
|
|
1136
1327
|
return `[braid] To allow tools for this agent: stop the bridge, run \`braid tools full ${name}\`, then start it again (advanced override: BRAID_TOOL_MODE=full).`;
|
|
1137
1328
|
}
|
|
1138
1329
|
function parseToolMode(env) {
|
|
@@ -1166,14 +1357,17 @@ function braidMcpFloorToolName(title) {
|
|
|
1166
1357
|
return FLOOR_BRAID_MCP_TOOLS.has(short) ? short : null;
|
|
1167
1358
|
}
|
|
1168
1359
|
function expandTilde(p) {
|
|
1169
|
-
if (p === "~") return
|
|
1170
|
-
if (p.startsWith("~/") || p.startsWith(`~${sep}`)) return
|
|
1360
|
+
if (p === "~") return homedir4();
|
|
1361
|
+
if (p.startsWith("~/") || p.startsWith(`~${sep}`)) return join3(homedir4(), p.slice(2));
|
|
1171
1362
|
return p;
|
|
1172
1363
|
}
|
|
1173
|
-
function
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1364
|
+
function foldCase2(p, platform = process.platform) {
|
|
1365
|
+
return platform === "darwin" || platform === "win32" ? p.toLowerCase() : p;
|
|
1366
|
+
}
|
|
1367
|
+
function isInsidePath(protectedAbs, rawPath, cwd = process.cwd()) {
|
|
1368
|
+
const prot = foldCase2(resolve(expandTilde(protectedAbs)));
|
|
1369
|
+
const p = foldCase2(resolve(cwd, expandTilde(rawPath)));
|
|
1370
|
+
return p === prot || p.startsWith(prot + sep);
|
|
1177
1371
|
}
|
|
1178
1372
|
function pathsFromInput(input) {
|
|
1179
1373
|
if (input == null || typeof input !== "object") return [];
|
|
@@ -1185,19 +1379,189 @@ function pathsFromInput(input) {
|
|
|
1185
1379
|
}
|
|
1186
1380
|
return out;
|
|
1187
1381
|
}
|
|
1188
|
-
function
|
|
1382
|
+
function findProtectedHit(protectedAbs, paths, cwd = process.cwd()) {
|
|
1189
1383
|
for (const p of paths) {
|
|
1190
|
-
|
|
1384
|
+
for (const prot of protectedAbs) {
|
|
1385
|
+
if (isInsidePath(prot, p, cwd)) return p;
|
|
1386
|
+
}
|
|
1191
1387
|
}
|
|
1192
1388
|
return null;
|
|
1193
1389
|
}
|
|
1194
|
-
function
|
|
1390
|
+
function legacyArpDir() {
|
|
1391
|
+
return join3(homedir4(), ".arp");
|
|
1392
|
+
}
|
|
1393
|
+
function protectedInventory(configDirAbs) {
|
|
1394
|
+
const home = homedir4();
|
|
1395
|
+
return protectedReadPaths(home, process.env, process.platform, [configDirAbs, legacyArpDir()]);
|
|
1396
|
+
}
|
|
1397
|
+
function protectedHitReason(configDirAbs, hit, cwd, toolName) {
|
|
1398
|
+
const subject = toolName ? `tool ${toolName}` : "tool call";
|
|
1399
|
+
if (isInsidePath(configDirAbs, hit, cwd) || isInsidePath(legacyArpDir(), hit, cwd)) {
|
|
1400
|
+
return `${subject} touches the Braid config dir (${hit})`;
|
|
1401
|
+
}
|
|
1402
|
+
return `${subject} touches a protected credential path (${hit})`;
|
|
1403
|
+
}
|
|
1404
|
+
var ENV_BASENAME_RE = /^\.env(\..+)?$/;
|
|
1405
|
+
function basenameOf(p) {
|
|
1406
|
+
const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
1407
|
+
return idx === -1 ? p : p.slice(idx + 1);
|
|
1408
|
+
}
|
|
1409
|
+
function unmaskGlobSegment(seg2) {
|
|
1410
|
+
return seg2.replace(/\[([^\]])\]/g, "$1").replace(/\\(.)/g, "$1").trim();
|
|
1411
|
+
}
|
|
1412
|
+
function formEscapesRootSet(form, platform) {
|
|
1413
|
+
if (form.startsWith("/") || form.startsWith("~")) return true;
|
|
1414
|
+
if (/^[A-Za-z]:[\\/]/.test(form)) return true;
|
|
1415
|
+
if (form.startsWith("\\\\")) return true;
|
|
1416
|
+
const posixSegs = form.split("/");
|
|
1417
|
+
if (posixSegs.some((s) => unmaskGlobSegment(s) === "..")) return true;
|
|
1418
|
+
if (platform === "win32" && form.split(/[\\/]/).some((s) => unmaskGlobSegment(s) === "..")) return true;
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
function splitTopLevelCommas(inner) {
|
|
1422
|
+
const parts = [];
|
|
1423
|
+
let depth = 0;
|
|
1424
|
+
let start = 0;
|
|
1425
|
+
for (let i = 0; i < inner.length; i++) {
|
|
1426
|
+
const c = inner[i];
|
|
1427
|
+
if (c === "\\") {
|
|
1428
|
+
i++;
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
if (c === "{") depth++;
|
|
1432
|
+
else if (c === "}" && depth > 0) depth--;
|
|
1433
|
+
else if (c === "," && depth === 0) {
|
|
1434
|
+
parts.push(inner.slice(start, i));
|
|
1435
|
+
start = i + 1;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
parts.push(inner.slice(start));
|
|
1439
|
+
return parts;
|
|
1440
|
+
}
|
|
1441
|
+
var BRACE_FORM_BUDGET = 1e4;
|
|
1442
|
+
var MAX_PATTERN_LEN = 512;
|
|
1443
|
+
var MAX_BRACE_GROUPS = 8;
|
|
1444
|
+
function expandBraces(pattern) {
|
|
1445
|
+
let depth = 0;
|
|
1446
|
+
let open = -1;
|
|
1447
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
1448
|
+
const c = pattern[i];
|
|
1449
|
+
if (c === "\\") {
|
|
1450
|
+
i++;
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (c === "{") {
|
|
1454
|
+
if (depth === 0) open = i;
|
|
1455
|
+
depth++;
|
|
1456
|
+
} else if (c === "}" && depth > 0) {
|
|
1457
|
+
depth--;
|
|
1458
|
+
if (depth === 0) {
|
|
1459
|
+
const opts = splitTopLevelCommas(pattern.slice(open + 1, i));
|
|
1460
|
+
if (opts.length <= 1) continue;
|
|
1461
|
+
const pre = pattern.slice(0, open);
|
|
1462
|
+
const post = pattern.slice(i + 1);
|
|
1463
|
+
const out = [];
|
|
1464
|
+
for (const opt of opts) {
|
|
1465
|
+
const sub = expandBraces(pre + opt + post);
|
|
1466
|
+
if (sub === null) return null;
|
|
1467
|
+
for (const s of sub) {
|
|
1468
|
+
if (out.length >= BRACE_FORM_BUDGET) return null;
|
|
1469
|
+
out.push(s);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return out;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return [pattern];
|
|
1477
|
+
}
|
|
1478
|
+
function patternEscapesRootSet(pattern, platform) {
|
|
1479
|
+
if (pattern.length > MAX_PATTERN_LEN) return true;
|
|
1480
|
+
if ((pattern.match(/\{/g)?.length ?? 0) > MAX_BRACE_GROUPS) return true;
|
|
1481
|
+
const forms = expandBraces(pattern);
|
|
1482
|
+
if (forms === null) return true;
|
|
1483
|
+
return forms.some((f) => formEscapesRootSet(f, platform));
|
|
1484
|
+
}
|
|
1485
|
+
function patternBasenameTargetsEnv(pattern, platform) {
|
|
1486
|
+
if (pattern.length > MAX_PATTERN_LEN) return false;
|
|
1487
|
+
const forms = expandBraces(pattern);
|
|
1488
|
+
if (forms === null) return false;
|
|
1489
|
+
return forms.some((f) => {
|
|
1490
|
+
const segs = platform === "win32" ? f.split(/[\\/]/) : f.split("/");
|
|
1491
|
+
const base = unmaskGlobSegment(segs[segs.length - 1] ?? "");
|
|
1492
|
+
return ENV_BASENAME_RE.test(base.toLowerCase());
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
function checkReadonlyRootSet(kind, candidates, patternFields, hasReadIntent, unknownShape, scope) {
|
|
1496
|
+
const platform = scope.platform ?? process.platform;
|
|
1497
|
+
const foldBasename = (s) => platform === "darwin" || platform === "win32" ? s.toLowerCase() : s;
|
|
1498
|
+
if (isFilesystemRoot(scope.cwd, platform) && hasReadIntent) {
|
|
1499
|
+
return {
|
|
1500
|
+
allow: false,
|
|
1501
|
+
reason: "readonly file reads have no working-directory boundary (bridge started at the filesystem root, e.g. a service install); pass --allow-read <dir> or set a working directory (BL-290)",
|
|
1502
|
+
deniedByMode: true,
|
|
1503
|
+
deniedByScope: true
|
|
1504
|
+
// remedy is --allow-read (a working directory), not `braid tools full`
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
for (const p of candidates) {
|
|
1508
|
+
if (ENV_BASENAME_RE.test(foldBasename(basenameOf(p)))) {
|
|
1509
|
+
return {
|
|
1510
|
+
allow: false,
|
|
1511
|
+
reason: `read of ${p} touches a .env file, denied in readonly mode (D6)`,
|
|
1512
|
+
deniedByMode: true
|
|
1513
|
+
// .env IS readable in full mode, so `braid tools full` is the right hint
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
for (const pattern of patternFields) {
|
|
1518
|
+
if (patternEscapesRootSet(pattern, platform)) {
|
|
1519
|
+
return {
|
|
1520
|
+
allow: false,
|
|
1521
|
+
reason: `${kind} pattern "${pattern}" may not be absolute, ~-relative, drive/UNC-rooted, or contain ".." segments (readonly root-set escape)`,
|
|
1522
|
+
deniedByMode: true,
|
|
1523
|
+
deniedByScope: true
|
|
1524
|
+
// remedy is --allow-read / a narrower pattern
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
if (patternBasenameTargetsEnv(pattern, platform)) {
|
|
1528
|
+
return {
|
|
1529
|
+
allow: false,
|
|
1530
|
+
reason: `${kind} glob "${pattern}" targets a .env file, denied in readonly mode (D6)`,
|
|
1531
|
+
deniedByMode: true
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (unknownShape) {
|
|
1536
|
+
return {
|
|
1537
|
+
allow: false,
|
|
1538
|
+
reason: `tool kind "${kind}" rawInput has no recognizable path field (unknown read shape)`,
|
|
1539
|
+
deniedByMode: true
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
if (candidates.length === 0) {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
for (const p of candidates) {
|
|
1546
|
+
if (!isInsideRootSet(p, scope)) {
|
|
1547
|
+
const grants = scope.allowRead.length > 0 ? `, ${scope.allowRead.join(", ")}` : "";
|
|
1548
|
+
return {
|
|
1549
|
+
allow: false,
|
|
1550
|
+
reason: `read of ${p} is outside the readonly root set [${scope.cwd}${grants}]; widen with --allow-read <dir> (or scope.allowRead), or braid tools full <name>`,
|
|
1551
|
+
deniedByScope: true,
|
|
1552
|
+
deniedByMode: true
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
function evaluateAcpPermission(mode, configDirAbs, req, scope) {
|
|
1195
1559
|
const tc = req.toolCall;
|
|
1196
1560
|
const locationPaths = (tc?.locations ?? []).map((l) => l?.path).filter((p) => typeof p === "string" && p.trim() !== "");
|
|
1197
1561
|
const candidates = [...locationPaths, ...pathsFromInput(tc?.rawInput)];
|
|
1198
|
-
const hit =
|
|
1562
|
+
const hit = findProtectedHit(protectedInventory(configDirAbs), candidates, scope.cwd);
|
|
1199
1563
|
if (hit) {
|
|
1200
|
-
return { allow: false, reason:
|
|
1564
|
+
return { allow: false, reason: protectedHitReason(configDirAbs, hit, scope.cwd) };
|
|
1201
1565
|
}
|
|
1202
1566
|
const arpFloor = braidMcpFloorToolName(tc?.title);
|
|
1203
1567
|
if (arpFloor) {
|
|
@@ -1205,6 +1569,21 @@ function evaluateAcpPermission(mode, configDirAbs, req) {
|
|
|
1205
1569
|
}
|
|
1206
1570
|
if (mode === "full") return { allow: true, reason: "BRAID_TOOL_MODE=full" };
|
|
1207
1571
|
const kind = tc?.kind ?? null;
|
|
1572
|
+
if (kind === "read" || kind === "search") {
|
|
1573
|
+
const rawInput = tc?.rawInput;
|
|
1574
|
+
const rawInputIsObject = rawInput != null && typeof rawInput === "object";
|
|
1575
|
+
const rawInputPresent = rawInputIsObject && Object.keys(rawInput).length > 0 || rawInput != null && !rawInputIsObject;
|
|
1576
|
+
const patternFields = [];
|
|
1577
|
+
if (rawInputIsObject) {
|
|
1578
|
+
const o = rawInput;
|
|
1579
|
+
if (typeof o.glob === "string") patternFields.push(o.glob);
|
|
1580
|
+
}
|
|
1581
|
+
const searchPatternPresent = kind === "search" && rawInputIsObject && typeof rawInput.pattern === "string";
|
|
1582
|
+
const hasReadIntent = candidates.length > 0 || patternFields.length > 0 || rawInputPresent || locationPaths.length > 0;
|
|
1583
|
+
const unknownShape = rawInputPresent && candidates.length === 0 && patternFields.length === 0 && !searchPatternPresent && locationPaths.length === 0;
|
|
1584
|
+
const denial = checkReadonlyRootSet(kind, candidates, patternFields, hasReadIntent, unknownShape, scope);
|
|
1585
|
+
if (denial) return denial;
|
|
1586
|
+
}
|
|
1208
1587
|
if (kind != null && READONLY_ACP_KINDS.has(kind)) {
|
|
1209
1588
|
return { allow: true, reason: `read-only tool kind "${kind}"` };
|
|
1210
1589
|
}
|
|
@@ -1225,10 +1604,11 @@ var READONLY_SDK_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1225
1604
|
"TodoWrite",
|
|
1226
1605
|
"ExitPlanMode"
|
|
1227
1606
|
]);
|
|
1228
|
-
function evaluateSdkTool(mode, configDirAbs, toolName, input) {
|
|
1229
|
-
const
|
|
1607
|
+
function evaluateSdkTool(mode, configDirAbs, toolName, input, scope) {
|
|
1608
|
+
const inputPaths = pathsFromInput(input);
|
|
1609
|
+
const hit = findProtectedHit(protectedInventory(configDirAbs), inputPaths, scope.cwd);
|
|
1230
1610
|
if (hit) {
|
|
1231
|
-
return { allow: false, reason:
|
|
1611
|
+
return { allow: false, reason: protectedHitReason(configDirAbs, hit, scope.cwd, toolName) };
|
|
1232
1612
|
}
|
|
1233
1613
|
const arpFloor = braidMcpFloorToolName(toolName);
|
|
1234
1614
|
if (arpFloor) {
|
|
@@ -1236,6 +1616,24 @@ function evaluateSdkTool(mode, configDirAbs, toolName, input) {
|
|
|
1236
1616
|
}
|
|
1237
1617
|
if (mode === "full") return { allow: true, reason: "BRAID_TOOL_MODE=full" };
|
|
1238
1618
|
if (READONLY_SDK_TOOLS.has(toolName)) {
|
|
1619
|
+
if (toolName === "Read" || toolName === "Grep" || toolName === "Glob") {
|
|
1620
|
+
const candidates = inputPaths;
|
|
1621
|
+
const patternFields = [];
|
|
1622
|
+
if (toolName === "Glob" && typeof input.pattern === "string") patternFields.push(input.pattern);
|
|
1623
|
+
if (toolName === "Grep" && typeof input.glob === "string") patternFields.push(input.glob);
|
|
1624
|
+
const kind = toolName === "Read" ? "read" : "search";
|
|
1625
|
+
const unknownShape = toolName === "Read" && candidates.length === 0;
|
|
1626
|
+
const denial = checkReadonlyRootSet(
|
|
1627
|
+
kind,
|
|
1628
|
+
candidates,
|
|
1629
|
+
patternFields,
|
|
1630
|
+
/* hasReadIntent */
|
|
1631
|
+
true,
|
|
1632
|
+
unknownShape,
|
|
1633
|
+
scope
|
|
1634
|
+
);
|
|
1635
|
+
if (denial) return denial;
|
|
1636
|
+
}
|
|
1239
1637
|
return { allow: true, reason: `read-only tool ${toolName}` };
|
|
1240
1638
|
}
|
|
1241
1639
|
const arpRead = braidMcpReadToolName(toolName);
|
|
@@ -1317,12 +1715,13 @@ import { createInterface as createInterface3 } from "readline";
|
|
|
1317
1715
|
// src/adapter.ts
|
|
1318
1716
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1319
1717
|
import { accessSync, constants, existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
1320
|
-
import { delimiter, dirname as dirname3, join as
|
|
1321
|
-
import { homedir as
|
|
1718
|
+
import { delimiter, dirname as dirname3, join as join6, resolve as resolve2 } from "path";
|
|
1719
|
+
import { homedir as homedir5 } from "os";
|
|
1322
1720
|
|
|
1323
1721
|
// src/acp/client.ts
|
|
1324
1722
|
import { spawn } from "child_process";
|
|
1325
1723
|
import { Readable, Writable } from "stream";
|
|
1724
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
1326
1725
|
import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
1327
1726
|
|
|
1328
1727
|
// src/acp/filterStream.ts
|
|
@@ -1395,7 +1794,7 @@ function capLines(s, maxLines) {
|
|
|
1395
1794
|
if (lines.length <= maxLines) return { value: s, truncated: false };
|
|
1396
1795
|
return { value: lines.slice(0, maxLines).join("\n") + BRIDGE_CAPS.truncationSuffix, truncated: true };
|
|
1397
1796
|
}
|
|
1398
|
-
function buildActivityContent(raw) {
|
|
1797
|
+
function buildActivityContent(raw, kind) {
|
|
1399
1798
|
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
1400
1799
|
const out = [];
|
|
1401
1800
|
let total = 0;
|
|
@@ -1403,13 +1802,13 @@ function buildActivityContent(raw) {
|
|
|
1403
1802
|
if (typeof item !== "object" || item === null) continue;
|
|
1404
1803
|
const it = item;
|
|
1405
1804
|
let built = null;
|
|
1406
|
-
if (it.type === "diff" && typeof it.path === "string" && typeof it.newText === "string") {
|
|
1805
|
+
if (it.type === "diff" && kind === "edit" && typeof it.path === "string" && typeof it.newText === "string") {
|
|
1407
1806
|
const oldText = typeof it.oldText === "string" ? it.oldText : "";
|
|
1408
1807
|
let patch = createTwoFilesPatch(it.path, it.path, oldText, it.newText, void 0, void 0, { context: 3 });
|
|
1409
1808
|
const byLines = capLines(patch, BRIDGE_CAPS.diffPatchLines);
|
|
1410
1809
|
const byBytes = capBytes(byLines.value, BRIDGE_CAPS.diffPatchBytes);
|
|
1411
1810
|
built = { type: "diff", path: it.path, patch: byBytes.value, truncated: byLines.truncated || byBytes.truncated };
|
|
1412
|
-
} else if (it.type === "content") {
|
|
1811
|
+
} else if (it.type === "content" && kind === "execute") {
|
|
1413
1812
|
const block = it.content;
|
|
1414
1813
|
if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) {
|
|
1415
1814
|
const capped = capBytes(block.text, BRIDGE_CAPS.textBytes);
|
|
@@ -1558,6 +1957,13 @@ function pickAllowOption(req) {
|
|
|
1558
1957
|
"ACP request_permission had no allow option (allow_once/allow_always); refusing to auto-select a non-allow option"
|
|
1559
1958
|
);
|
|
1560
1959
|
}
|
|
1960
|
+
function acpSessionCwd(cwd) {
|
|
1961
|
+
try {
|
|
1962
|
+
return realpathSync2.native(cwd);
|
|
1963
|
+
} catch {
|
|
1964
|
+
return cwd;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1561
1967
|
function safeAcpToolName(tc) {
|
|
1562
1968
|
const title = tc?.title;
|
|
1563
1969
|
if (typeof title === "string") {
|
|
@@ -1573,7 +1979,7 @@ var AcpClient = class {
|
|
|
1573
1979
|
this.afterChildExitHook = afterChildExitHook;
|
|
1574
1980
|
this.telemetry = telemetry;
|
|
1575
1981
|
this.warmupCeilingMs = warmupCeilingMs;
|
|
1576
|
-
this.policy = launch.policy ?? { mode: "readonly", configDirAbs: configDir(process.env) };
|
|
1982
|
+
this.policy = launch.policy ?? { mode: "readonly", configDirAbs: configDir(process.env), allowReadAbs: [] };
|
|
1577
1983
|
}
|
|
1578
1984
|
launch;
|
|
1579
1985
|
scopeWrap;
|
|
@@ -1771,7 +2177,10 @@ var AcpClient = class {
|
|
|
1771
2177
|
},
|
|
1772
2178
|
requestPermission: async (req) => {
|
|
1773
2179
|
const mode = this.currentTurnOverrideMode ?? this.policy.mode;
|
|
1774
|
-
const verdict = evaluateAcpPermission(mode, this.policy.configDirAbs, req
|
|
2180
|
+
const verdict = evaluateAcpPermission(mode, this.policy.configDirAbs, req, {
|
|
2181
|
+
cwd: acpSessionCwd(this.launch.cwd),
|
|
2182
|
+
allowRead: this.policy.allowReadAbs ?? []
|
|
2183
|
+
});
|
|
1775
2184
|
if (verdict.allow) {
|
|
1776
2185
|
return {
|
|
1777
2186
|
outcome: { outcome: "selected", optionId: pickAllowOption(req) }
|
|
@@ -1782,11 +2191,11 @@ var AcpClient = class {
|
|
|
1782
2191
|
tool: safeAcpToolName(req.toolCall),
|
|
1783
2192
|
mode,
|
|
1784
2193
|
path: "acp",
|
|
1785
|
-
reason: verdict.deniedByMode ? "mode" : "tripwire"
|
|
2194
|
+
reason: verdict.deniedByScope ? "scope" : verdict.deniedByMode ? "mode" : "tripwire"
|
|
1786
2195
|
});
|
|
1787
2196
|
if (verdict.deniedByMode) {
|
|
1788
2197
|
const cause = this.currentTurnOverrideMode === "readonly" && this.policy.mode !== "readonly" ? "channel" : "operator";
|
|
1789
|
-
const hint = takeDenialHint(this.policy, cause);
|
|
2198
|
+
const hint = takeDenialHint(this.policy, cause, verdict.deniedByScope ? "scope" : "mode", acpSessionCwd(this.launch.cwd));
|
|
1790
2199
|
if (hint) console.warn(sanitizeForTty(hint));
|
|
1791
2200
|
}
|
|
1792
2201
|
const rejectId = pickRejectOptionId(req);
|
|
@@ -1863,6 +2272,8 @@ var AcpClient = class {
|
|
|
1863
2272
|
const sink = this.launch.onActivity;
|
|
1864
2273
|
if (!sink) return;
|
|
1865
2274
|
const locations = u.locations && u.locations.length > 0 ? u.locations.map((l) => l.line == null ? { path: l.path } : { path: l.path, line: l.line }) : null;
|
|
2275
|
+
if (u.kind) this.activeTurnBuffer?.kinds.set(u.toolCallId, u.kind);
|
|
2276
|
+
const kind = u.kind ?? this.activeTurnBuffer?.kinds.get(u.toolCallId) ?? null;
|
|
1866
2277
|
const event = {
|
|
1867
2278
|
turnId,
|
|
1868
2279
|
toolCallId: u.toolCallId,
|
|
@@ -1872,7 +2283,7 @@ var AcpClient = class {
|
|
|
1872
2283
|
status: u.status ?? null,
|
|
1873
2284
|
locations,
|
|
1874
2285
|
ts: Date.now(),
|
|
1875
|
-
content: buildActivityContent(u.content)
|
|
2286
|
+
content: buildActivityContent(u.content, kind)
|
|
1876
2287
|
};
|
|
1877
2288
|
try {
|
|
1878
2289
|
sink(event);
|
|
@@ -1932,7 +2343,8 @@ var AcpClient = class {
|
|
|
1932
2343
|
const buffer = {
|
|
1933
2344
|
text: "",
|
|
1934
2345
|
turnId,
|
|
1935
|
-
thoughts: new ThoughtBuffer(turnId)
|
|
2346
|
+
thoughts: new ThoughtBuffer(turnId),
|
|
2347
|
+
kinds: /* @__PURE__ */ new Map()
|
|
1936
2348
|
};
|
|
1937
2349
|
this.activeTurnBuffer = buffer;
|
|
1938
2350
|
this.currentTurnOverrideMode = overrideMode;
|
|
@@ -2048,7 +2460,7 @@ var AcpClient = class {
|
|
|
2048
2460
|
|
|
2049
2461
|
// src/sessionStore.ts
|
|
2050
2462
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, chmodSync as chmodSync2 } from "fs";
|
|
2051
|
-
import { join as
|
|
2463
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
2052
2464
|
function mergeSessionPointer(existing, sessionId, cwd) {
|
|
2053
2465
|
return existing ? { ...existing, sessionId, cwd } : { sessionId, cwd };
|
|
2054
2466
|
}
|
|
@@ -2056,7 +2468,7 @@ function safe(s) {
|
|
|
2056
2468
|
return s.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2057
2469
|
}
|
|
2058
2470
|
function sessionFilePath(dir, relayUrl, agentName, channelId) {
|
|
2059
|
-
return
|
|
2471
|
+
return join4(dir, "sessions", relayHostSegment(relayUrl), `${safe(agentName)}__${safe(channelId)}.json`);
|
|
2060
2472
|
}
|
|
2061
2473
|
function parseSlot(p) {
|
|
2062
2474
|
if (typeof p.sessionId === "string" && p.sessionId.trim() !== "" && typeof p.cwd === "string" && p.cwd.trim() !== "") {
|
|
@@ -2281,14 +2693,14 @@ function modelLaunchFor(agent, model) {
|
|
|
2281
2693
|
|
|
2282
2694
|
// src/scope/sandbox.ts
|
|
2283
2695
|
import { SandboxManager, SandboxRuntimeConfigSchema, installWindowsSandbox, getSrtWinPath } from "@anthropic-ai/sandbox-runtime";
|
|
2284
|
-
import
|
|
2285
|
-
import { realpathSync as
|
|
2696
|
+
import path4 from "path";
|
|
2697
|
+
import { realpathSync as realpathSync4 } from "fs";
|
|
2286
2698
|
import { spawnSync as nodeSpawnSync2 } from "child_process";
|
|
2287
2699
|
|
|
2288
2700
|
// src/scope/scopeConfig.ts
|
|
2289
|
-
import { realpathSync, readdirSync as readdirSync2, lstatSync } from "fs";
|
|
2701
|
+
import { realpathSync as realpathSync3, readdirSync as readdirSync2, lstatSync } from "fs";
|
|
2290
2702
|
import { execSync } from "child_process";
|
|
2291
|
-
import { join as
|
|
2703
|
+
import { join as join5 } from "path";
|
|
2292
2704
|
function resolveIpcProfile(scope, flagValue) {
|
|
2293
2705
|
if (flagValue !== void 0) return flagValue;
|
|
2294
2706
|
if (scope?.ipcProfile !== void 0) return scope.ipcProfile;
|
|
@@ -2379,14 +2791,14 @@ var PROVIDER_PATHS = {
|
|
|
2379
2791
|
copilot: ["~/.copilot"],
|
|
2380
2792
|
qwen: ["~/.qwen"]
|
|
2381
2793
|
};
|
|
2382
|
-
function
|
|
2794
|
+
function expandHome2(p, home) {
|
|
2383
2795
|
if (p.startsWith("~/")) return `${home}${p.slice(1)}`;
|
|
2384
2796
|
if (p === "~") return home;
|
|
2385
2797
|
return p;
|
|
2386
2798
|
}
|
|
2387
|
-
function
|
|
2799
|
+
function canonicalize2(p) {
|
|
2388
2800
|
try {
|
|
2389
|
-
return
|
|
2801
|
+
return realpathSync3(p);
|
|
2390
2802
|
} catch {
|
|
2391
2803
|
return p;
|
|
2392
2804
|
}
|
|
@@ -2410,18 +2822,18 @@ function darwinMdsCacheDir() {
|
|
|
2410
2822
|
}
|
|
2411
2823
|
}
|
|
2412
2824
|
function resolvePaths(paths, home) {
|
|
2413
|
-
return dedup(paths.map((p) =>
|
|
2825
|
+
return dedup(paths.map((p) => canonicalize2(expandHome2(p, home))));
|
|
2414
2826
|
}
|
|
2415
|
-
function
|
|
2827
|
+
function foldCase3(p, platform) {
|
|
2416
2828
|
return platform === "darwin" || platform === "win32" ? p.toLowerCase() : p;
|
|
2417
2829
|
}
|
|
2418
2830
|
function checkGrantsAgainstReservations(grants, reservations, platform) {
|
|
2419
2831
|
const sep2 = platform === "win32" ? "\\" : "/";
|
|
2420
2832
|
const out = [];
|
|
2421
2833
|
for (const g of grants) {
|
|
2422
|
-
const gf =
|
|
2834
|
+
const gf = foldCase3(g.path, platform);
|
|
2423
2835
|
for (const r of reservations) {
|
|
2424
|
-
const rf =
|
|
2836
|
+
const rf = foldCase3(r, platform);
|
|
2425
2837
|
if (gf === rf) {
|
|
2426
2838
|
out.push({ grant: g.path, source: g.source, reservation: r, relation: "equal" });
|
|
2427
2839
|
} else if (gf.startsWith(rf + sep2)) {
|
|
@@ -2436,41 +2848,36 @@ function resolveScope(cfg, provider, env, platform = process.platform) {
|
|
|
2436
2848
|
const home = env["HOME"] ?? "";
|
|
2437
2849
|
const cwd = env["PWD"] ?? process.cwd();
|
|
2438
2850
|
const tmpDir = env["TMPDIR"] ?? "/tmp";
|
|
2439
|
-
const braidConfigDir = env["BRAID_CONFIG_DIR"]?.trim() ||
|
|
2440
|
-
const legacyArpConfigDir =
|
|
2851
|
+
const braidConfigDir = env["BRAID_CONFIG_DIR"]?.trim() || expandHome2("~/.braid", home);
|
|
2852
|
+
const legacyArpConfigDir = expandHome2("~/.arp", home);
|
|
2441
2853
|
const protectedConfigDirs = braidConfigDir === legacyArpConfigDir ? [braidConfigDir] : [braidConfigDir, legacyArpConfigDir];
|
|
2442
2854
|
const flagOverride = cfg.scopeFlags?.scopeOverride;
|
|
2443
2855
|
const disabled = flagOverride !== void 0 ? flagOverride === "off" : scope.disabled === true || env["BRAID_SCOPE"] === "off";
|
|
2444
|
-
const secretDirs =
|
|
2445
|
-
"~/.ssh",
|
|
2446
|
-
"~/.aws",
|
|
2447
|
-
"~/.gnupg",
|
|
2448
|
-
"~/.kube/config"
|
|
2449
|
-
].map((p) => expandHome(p, home));
|
|
2856
|
+
const secretDirs = osDeniableReadPaths(home, env, platform);
|
|
2450
2857
|
const protectedConfigDirEntries = protectedConfigDirs.flatMap((configDirPath) => {
|
|
2451
2858
|
if (platform !== "win32") return [configDirPath];
|
|
2452
2859
|
try {
|
|
2453
2860
|
const children = readdirSync2(configDirPath, { withFileTypes: true });
|
|
2454
|
-
const denied = children.filter((entry) => entry.name !== "runtime").map((entry) =>
|
|
2861
|
+
const denied = children.filter((entry) => entry.name !== "runtime").map((entry) => canonicalize2(join5(configDirPath, entry.name)));
|
|
2455
2862
|
if (denied.length === 0) return [configDirPath];
|
|
2456
2863
|
return denied;
|
|
2457
2864
|
} catch {
|
|
2458
2865
|
return [configDirPath];
|
|
2459
2866
|
}
|
|
2460
2867
|
});
|
|
2461
|
-
const providerPaths = PROVIDER_PATHS[provider].map((p) =>
|
|
2868
|
+
const providerPaths = PROVIDER_PATHS[provider].map((p) => expandHome2(p, home));
|
|
2462
2869
|
const uid = process.getuid?.();
|
|
2463
|
-
const claudeTmpRoot = provider === "claude-code" && uid !== void 0 ? [`${
|
|
2870
|
+
const claudeTmpRoot = provider === "claude-code" && uid !== void 0 ? [`${canonicalize2("/tmp")}/claude-${uid}`, `${canonicalize2("/tmp")}/claude-*-cwd`] : [];
|
|
2464
2871
|
const mdsCacheDir = platform === "darwin" ? darwinMdsCacheDir() : [];
|
|
2465
|
-
const opAllowWrite = (scope.allowWrite ?? []).map((p) =>
|
|
2466
|
-
const envAllowWrite = parseEnvList(env["BRAID_SCOPE_ALLOW_WRITE"]).map((p) =>
|
|
2467
|
-
const flagAllowWrite = (cfg.scopeFlags?.allowWrite ?? []).map((p) =>
|
|
2872
|
+
const opAllowWrite = (scope.allowWrite ?? []).map((p) => expandHome2(p, home));
|
|
2873
|
+
const envAllowWrite = parseEnvList(env["BRAID_SCOPE_ALLOW_WRITE"]).map((p) => expandHome2(p, home));
|
|
2874
|
+
const flagAllowWrite = (cfg.scopeFlags?.allowWrite ?? []).map((p) => expandHome2(p, home));
|
|
2468
2875
|
const rawAllowWrite = [
|
|
2469
2876
|
cwd,
|
|
2470
2877
|
...providerPaths,
|
|
2471
2878
|
...claudeTmpRoot,
|
|
2472
2879
|
...mdsCacheDir,
|
|
2473
|
-
|
|
2880
|
+
expandHome2("~/.npm", home),
|
|
2474
2881
|
tmpDir,
|
|
2475
2882
|
...opAllowWrite,
|
|
2476
2883
|
...envAllowWrite,
|
|
@@ -2480,24 +2887,24 @@ function resolveScope(cfg, provider, env, platform = process.platform) {
|
|
|
2480
2887
|
const rawDenyWrite = [
|
|
2481
2888
|
...secretDirs,
|
|
2482
2889
|
...protectedConfigDirEntries,
|
|
2483
|
-
...(scope.denyWrite ?? []).map((p) =>
|
|
2484
|
-
...parseEnvList(env["BRAID_SCOPE_DENY_WRITE"]).map((p) =>
|
|
2890
|
+
...(scope.denyWrite ?? []).map((p) => expandHome2(p, home)),
|
|
2891
|
+
...parseEnvList(env["BRAID_SCOPE_DENY_WRITE"]).map((p) => expandHome2(p, home))
|
|
2485
2892
|
];
|
|
2486
2893
|
const denyWrite = resolvePaths(rawDenyWrite, home);
|
|
2487
2894
|
const ipcProfile = resolveIpcProfile(scope, cfg.scopeFlags?.ipcProfile);
|
|
2488
2895
|
const rawDenyRead = [
|
|
2489
2896
|
...secretDirs,
|
|
2490
2897
|
...protectedConfigDirEntries,
|
|
2491
|
-
...(scope.denyRead ?? []).map((p) =>
|
|
2492
|
-
...parseEnvList(env["BRAID_SCOPE_DENY_READ"]).map((p) =>
|
|
2898
|
+
...(scope.denyRead ?? []).map((p) => expandHome2(p, home)),
|
|
2899
|
+
...parseEnvList(env["BRAID_SCOPE_DENY_READ"]).map((p) => expandHome2(p, home))
|
|
2493
2900
|
];
|
|
2494
2901
|
const denyRead = dedup([
|
|
2495
2902
|
...resolvePaths(rawDenyRead, home),
|
|
2496
2903
|
...ipcProfile === "strict" && platform === "linux" ? strictIpcDenyRead(env) : []
|
|
2497
2904
|
]);
|
|
2498
|
-
const opAllowRead = (scope.allowRead ?? []).map((p) =>
|
|
2499
|
-
const envAllowRead = parseEnvList(env["BRAID_SCOPE_ALLOW_READ"]).map((p) =>
|
|
2500
|
-
const flagAllowRead = (cfg.scopeFlags?.allowRead ?? []).map((p) =>
|
|
2905
|
+
const opAllowRead = (scope.allowRead ?? []).map((p) => expandHome2(p, home));
|
|
2906
|
+
const envAllowRead = parseEnvList(env["BRAID_SCOPE_ALLOW_READ"]).map((p) => expandHome2(p, home));
|
|
2907
|
+
const flagAllowRead = (cfg.scopeFlags?.allowRead ?? []).map((p) => expandHome2(p, home));
|
|
2501
2908
|
const rawAllowRead = [
|
|
2502
2909
|
...opAllowRead,
|
|
2503
2910
|
...envAllowRead,
|
|
@@ -2528,10 +2935,10 @@ Narrow the grant to a path outside the protected set. (Ancestor grants like "~"
|
|
|
2528
2935
|
);
|
|
2529
2936
|
}
|
|
2530
2937
|
const writableDefaults = resolvePaths(
|
|
2531
|
-
[...providerPaths,
|
|
2938
|
+
[...providerPaths, expandHome2("~/.npm", home), tmpDir],
|
|
2532
2939
|
home
|
|
2533
2940
|
);
|
|
2534
|
-
const resolvedConfigDir =
|
|
2941
|
+
const resolvedConfigDir = canonicalize2(braidConfigDir);
|
|
2535
2942
|
const placement = checkGrantsAgainstReservations(
|
|
2536
2943
|
[{ path: resolvedConfigDir, source: "BRAID_CONFIG_DIR" }],
|
|
2537
2944
|
writableDefaults,
|
|
@@ -2562,10 +2969,15 @@ Narrow the grant to a path outside the protected set. (Ancestor grants like "~"
|
|
|
2562
2969
|
return lines;
|
|
2563
2970
|
}
|
|
2564
2971
|
lines.push("[scope] Agent sandbox is ACTIVE");
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2972
|
+
{
|
|
2973
|
+
const configDirsShown = resolvePaths(protectedConfigDirs, home);
|
|
2974
|
+
const credentialPaths = resolvePaths(secretDirs, home);
|
|
2975
|
+
lines.push(
|
|
2976
|
+
`[scope] protected (never readable/writable by the agent): ${configDirsShown.join(", ")}, + ${credentialPaths.length} credential paths incl. ${credentialPaths.slice(0, 2).join(", ")}, ...`
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2568
2979
|
lines.push(`[scope] cwd (writable): ${cwd}`);
|
|
2980
|
+
lines.push(`[scope] file tools may read (readonly): ${cwd}${allowRead.length ? ", " + allowRead.join(", ") : ""}`);
|
|
2569
2981
|
if (providerPaths.length > 0) {
|
|
2570
2982
|
lines.push(`[scope] provider paths (writable): ${providerPaths.join(", ")}`);
|
|
2571
2983
|
}
|
|
@@ -2602,7 +3014,7 @@ Narrow the grant to a path outside the protected set. (Ancestor grants like "~"
|
|
|
2602
3014
|
// src/mcp/broker.ts
|
|
2603
3015
|
import http from "http";
|
|
2604
3016
|
import os from "os";
|
|
2605
|
-
import
|
|
3017
|
+
import path3 from "path";
|
|
2606
3018
|
import fs from "fs";
|
|
2607
3019
|
import { randomUUID as randomUUID4, randomBytes } from "crypto";
|
|
2608
3020
|
|
|
@@ -3026,7 +3438,7 @@ function classifyCatchUp(messages, agentName, nowMs, opts) {
|
|
|
3026
3438
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
3027
3439
|
|
|
3028
3440
|
// src/cardSigning.ts
|
|
3029
|
-
import
|
|
3441
|
+
import canonicalize3 from "canonicalize";
|
|
3030
3442
|
import {
|
|
3031
3443
|
FlattenedSign,
|
|
3032
3444
|
flattenedVerify,
|
|
@@ -3083,7 +3495,7 @@ function pruneForSigning(card) {
|
|
|
3083
3495
|
return pruneNode(clone, REQUIRED_TOP);
|
|
3084
3496
|
}
|
|
3085
3497
|
function canonicalCardString(card) {
|
|
3086
|
-
return
|
|
3498
|
+
return canonicalize3(pruneForSigning(card));
|
|
3087
3499
|
}
|
|
3088
3500
|
function publicOkpJwk(jwk) {
|
|
3089
3501
|
return { kty: jwk.kty, crv: jwk.crv, x: jwk.x };
|
|
@@ -3403,8 +3815,8 @@ var RelayClient = class {
|
|
|
3403
3815
|
* boundedJsonFetch. All relay HTTP call sites route through this. Never throws (returns
|
|
3404
3816
|
* a BoundedJsonResult union). Plain-object fetch doubles ({ok,status,json}) are supported
|
|
3405
3817
|
* by the underlying helper for test compatibility. */
|
|
3406
|
-
httpJson(
|
|
3407
|
-
return boundedJsonFetch(`${this.cfg.relayHttpUrl}${
|
|
3818
|
+
httpJson(path5, opts) {
|
|
3819
|
+
return boundedJsonFetch(`${this.cfg.relayHttpUrl}${path5}`, {
|
|
3408
3820
|
...opts,
|
|
3409
3821
|
headers: {
|
|
3410
3822
|
Authorization: `Bearer ${this.cfg.token}`,
|
|
@@ -4151,7 +4563,18 @@ var RelayClient = class {
|
|
|
4151
4563
|
candidates: Array.isArray(msg.candidates) ? msg.candidates.map((c) => untrusted(String(c ?? ""))) : void 0,
|
|
4152
4564
|
history,
|
|
4153
4565
|
// BL-126: server-minted attribution key — pure pass-through, never invented by bridge.
|
|
4154
|
-
dispatchId: typeof msg.dispatchId === "string" && msg.dispatchId.length > 0 ? msg.dispatchId : void 0
|
|
4566
|
+
dispatchId: typeof msg.dispatchId === "string" && msg.dispatchId.length > 0 ? msg.dispatchId : void 0,
|
|
4567
|
+
// D14: additive Lisa Loop signal — pass-through, mostly untransformed. Absent
|
|
4568
|
+
// on every non-lisa msg (msg.lisa undefined), so non-lisa signals are unaffected.
|
|
4569
|
+
// Defensive shape-narrowing for parity with the sibling fields above: a
|
|
4570
|
+
// malformed lisa (string/null/array) is dropped rather than passed through raw.
|
|
4571
|
+
// Review Minor 1 (T11 final wave): freshSession is ROUTING-relevant (D13 — it
|
|
4572
|
+
// decides whether the turn runs on the ephemeral one-shot session at all), so
|
|
4573
|
+
// unlike the other (display-only) lisa fields it is coerced to a STRICT boolean
|
|
4574
|
+
// here rather than passed through raw. A relay drift like `freshSession:"false"`
|
|
4575
|
+
// (string) is truthy in JS and would otherwise wrongly route to the ephemeral
|
|
4576
|
+
// path; `=== true` makes anything but the literal boolean true resolve false.
|
|
4577
|
+
lisa: typeof msg.lisa === "object" && msg.lisa !== null && !Array.isArray(msg.lisa) ? { ...msg.lisa, freshSession: msg.lisa.freshSession === true } : void 0
|
|
4155
4578
|
};
|
|
4156
4579
|
this.flowCbs.forEach((cb) => cb(signal));
|
|
4157
4580
|
}
|
|
@@ -4611,9 +5034,9 @@ var RelayClient = class {
|
|
|
4611
5034
|
// ── End private memory lane ───────────────────────────────────────────────────
|
|
4612
5035
|
// ── Task board ──────────────────────────────────────────────────────────────
|
|
4613
5036
|
/** Shared helper: fetch to a task endpoint and return {ok, status, json}. Callers surface errors. */
|
|
4614
|
-
async taskFetch(method,
|
|
5037
|
+
async taskFetch(method, path5, body) {
|
|
4615
5038
|
const bodyStr = body === void 0 ? void 0 : JSON.stringify(body);
|
|
4616
|
-
const r = await this.httpJson(
|
|
5039
|
+
const r = await this.httpJson(path5, { method, body: bodyStr, requestClass: "relay.task" });
|
|
4617
5040
|
return { ok: r.ok, status: r.ok ? r.status : r.status ?? 0, json: r.ok ? r.json : r.json };
|
|
4618
5041
|
}
|
|
4619
5042
|
async createTask(channelId, title, description) {
|
|
@@ -4670,7 +5093,7 @@ var RelayClient = class {
|
|
|
4670
5093
|
* guard so malformed JSON is rejected with a clear error before any fetch call.
|
|
4671
5094
|
* Byte-parity contract: the EXACT docJson string is embedded as the "doc" value
|
|
4672
5095
|
* so what the bridge signs and what the relay stores are identical bytes. */
|
|
4673
|
-
async artifactWriteFetch(method,
|
|
5096
|
+
async artifactWriteFetch(method, path5, docJson, extraFields) {
|
|
4674
5097
|
try {
|
|
4675
5098
|
JSON.parse(docJson);
|
|
4676
5099
|
} catch (e) {
|
|
@@ -4678,7 +5101,7 @@ var RelayClient = class {
|
|
|
4678
5101
|
}
|
|
4679
5102
|
const extras = JSON.stringify(extraFields);
|
|
4680
5103
|
const body = `{"doc":${docJson},${extras.slice(1)}`;
|
|
4681
|
-
const r = await this.httpJson(
|
|
5104
|
+
const r = await this.httpJson(path5, { method, body, requestClass: "relay.artifact.write" });
|
|
4682
5105
|
return { ok: r.ok, status: r.ok ? r.status : r.status ?? 0, json: r.ok ? r.json : r.json };
|
|
4683
5106
|
}
|
|
4684
5107
|
/** Try to produce an artifact signature. Fail-open: returns undefined on any error. */
|
|
@@ -4744,9 +5167,9 @@ var RelayClient = class {
|
|
|
4744
5167
|
// attributed by the relay's own authenticated caller identity.
|
|
4745
5168
|
/** Shared helper for canvas calls: same {ok,status,json} shape as taskFetch,
|
|
4746
5169
|
* under its own requestClass so canvas HTTP metrics are distinguishable. */
|
|
4747
|
-
async canvasFetch(method,
|
|
5170
|
+
async canvasFetch(method, path5, body) {
|
|
4748
5171
|
const bodyStr = body === void 0 ? void 0 : JSON.stringify(body);
|
|
4749
|
-
const r = await this.httpJson(
|
|
5172
|
+
const r = await this.httpJson(path5, { method, body: bodyStr, requestClass: "relay.canvas" });
|
|
4750
5173
|
return { ok: r.ok, status: r.ok ? r.status : r.status ?? 0, json: r.ok ? r.json : r.json };
|
|
4751
5174
|
}
|
|
4752
5175
|
async getCanvas(channelId) {
|
|
@@ -4818,7 +5241,7 @@ var RelayClient = class {
|
|
|
4818
5241
|
/** Post a bounded-flow reply (turn or synthesis) to the flow-scoped endpoint.
|
|
4819
5242
|
* agentId MUST be the agent NAME — the relay's flow gate resolves turn ownership and
|
|
4820
5243
|
* synthesis role via resolveAgentUUID (a name lookup); a UUID resolves to uuid.Nil -> 403. */
|
|
4821
|
-
async postFlowMessage(channelId, flowId, content, usage, dispatchId) {
|
|
5244
|
+
async postFlowMessage(channelId, flowId, content, usage, dispatchId, lisaFresh) {
|
|
4822
5245
|
const ch = this.pathId(channelId, "channelId");
|
|
4823
5246
|
const fl = this.pathId(flowId, "flowId");
|
|
4824
5247
|
if (!ch || !fl) return;
|
|
@@ -4837,6 +5260,10 @@ var RelayClient = class {
|
|
|
4837
5260
|
// BL-126: echo the server-minted attribution key unchanged. Never invented here.
|
|
4838
5261
|
...dispatchId ? { dispatchId } : {},
|
|
4839
5262
|
...signature ? { signature } : {},
|
|
5263
|
+
// D13: additive marker for a lisa fresh-session (ephemeral one-shot) turn's
|
|
5264
|
+
// reply. Sent ONLY on lisa turns (true); every other flow reply omits the field
|
|
5265
|
+
// entirely, byte-identical to the pre-lisa body shape.
|
|
5266
|
+
...lisaFresh ? { lisaFresh: true } : {},
|
|
4840
5267
|
...this.usageBody(usage)
|
|
4841
5268
|
});
|
|
4842
5269
|
const r = await this.httpJson(`/channels/${ch}/flows/${fl}/messages`, {
|
|
@@ -5041,13 +5468,13 @@ var REFLECTION_REQUIRED_FIELDS = ["stage", "failure_family", "impacted_unit", "p
|
|
|
5041
5468
|
var REFLECTION_CLUSTER_FIELDS = ["stage", "failure_family", "impacted_unit"];
|
|
5042
5469
|
var VALID_NOTE_KINDS = /* @__PURE__ */ new Set(["note", ...STRUCTURED_NOTE_KINDS]);
|
|
5043
5470
|
function brokerSocketDir() {
|
|
5044
|
-
return
|
|
5471
|
+
return path3.join(os.tmpdir(), "braid-broker");
|
|
5045
5472
|
}
|
|
5046
5473
|
function brokerSocketPath(uuid, platform = process.platform) {
|
|
5047
5474
|
if (platform === "win32") {
|
|
5048
5475
|
return `\\\\.\\pipe\\braid-broker-${uuid}`;
|
|
5049
5476
|
}
|
|
5050
|
-
return
|
|
5477
|
+
return path3.join(brokerSocketDir(), `${uuid}.sock`);
|
|
5051
5478
|
}
|
|
5052
5479
|
var SourceBroker = class {
|
|
5053
5480
|
/** `platform` injected so the win32 named-pipe branch is unit-testable on a POSIX host
|
|
@@ -5092,6 +5519,16 @@ var SourceBroker = class {
|
|
|
5092
5519
|
});
|
|
5093
5520
|
return { socketPath: this.socketPath, token: this.token };
|
|
5094
5521
|
}
|
|
5522
|
+
// G2 (lisa loop, D13 review Minor 3): the ephemeral one-shot lisa session (a blind
|
|
5523
|
+
// critic or builder round) inherits this SAME per-channel broker's MCP tools via the
|
|
5524
|
+
// startOpts replay in ChannelSession.runLisaFreshTurn (session.ts). That is safe
|
|
5525
|
+
// TODAY only because NO route below reads channel/flow MESSAGES (only sources,
|
|
5526
|
+
// reactions, notes, memory, tasks, artifacts, canvas). If a future route is added
|
|
5527
|
+
// here that reads channel or flow messages, it MUST be gated off lisa turns (or
|
|
5528
|
+
// otherwise structurally blocked from the ephemeral session) — reachable from a
|
|
5529
|
+
// blind critic, it would silently leak the transcript the relay's read-filter is
|
|
5530
|
+
// deliberately withholding (D9/G2b) and defeat critic blindness. Do not add a
|
|
5531
|
+
// channel/flow message-read route here without addressing this.
|
|
5095
5532
|
async handle(req, res) {
|
|
5096
5533
|
const send = (status, body2) => {
|
|
5097
5534
|
const s = JSON.stringify(body2);
|
|
@@ -5376,7 +5813,7 @@ function grantSrtWinRunnerRX(deps) {
|
|
|
5376
5813
|
return;
|
|
5377
5814
|
}
|
|
5378
5815
|
const systemRoot = deps?.systemRoot ?? process.env["SystemRoot"] ?? "C:\\Windows";
|
|
5379
|
-
const icacls =
|
|
5816
|
+
const icacls = path4.win32.join(systemRoot, "System32", "icacls.exe");
|
|
5380
5817
|
const spawnFn = deps?.spawnSync ?? ((cmd, args) => nodeSpawnSync2(cmd, args, { shell: false }));
|
|
5381
5818
|
const result = spawnFn(icacls, [runnerPath, "/grant", "sandbox-runtime-users:(RX)"]);
|
|
5382
5819
|
if (result.status !== 0) {
|
|
@@ -5392,7 +5829,7 @@ function brokerSocketNetworkExtras() {
|
|
|
5392
5829
|
const dir = brokerSocketDir();
|
|
5393
5830
|
const subpaths = [dir];
|
|
5394
5831
|
try {
|
|
5395
|
-
const resolved =
|
|
5832
|
+
const resolved = path4.join(realpathSync4(path4.dirname(dir)), path4.basename(dir));
|
|
5396
5833
|
if (resolved !== dir) subpaths.push(resolved);
|
|
5397
5834
|
} catch {
|
|
5398
5835
|
}
|
|
@@ -5568,7 +6005,7 @@ function formatRateLimitNotice(agentName, provider, message) {
|
|
|
5568
6005
|
|
|
5569
6006
|
// src/adapter.ts
|
|
5570
6007
|
function defaultToolPolicy() {
|
|
5571
|
-
return { mode: "readonly", configDirAbs: resolve2(configDir(process.env)) };
|
|
6008
|
+
return { mode: "readonly", configDirAbs: resolve2(configDir(process.env)), allowReadAbs: [] };
|
|
5572
6009
|
}
|
|
5573
6010
|
var ADAPTER_VERSIONS = {
|
|
5574
6011
|
// Chosen 2026-06-11 (latest verified releases at pin time).
|
|
@@ -5588,13 +6025,13 @@ function pinned(pkg) {
|
|
|
5588
6025
|
function computeNpxLaunch(platform, execPath, exists) {
|
|
5589
6026
|
const nodeDir = dirname3(execPath);
|
|
5590
6027
|
if (platform === "win32") {
|
|
5591
|
-
const npxCliJs =
|
|
6028
|
+
const npxCliJs = join6(nodeDir, "node_modules", "npm", "bin", "npx-cli.js");
|
|
5592
6029
|
if (exists(npxCliJs)) {
|
|
5593
6030
|
return { command: execPath, argsPrefix: [npxCliJs] };
|
|
5594
6031
|
}
|
|
5595
6032
|
}
|
|
5596
6033
|
for (const name of ["npx", "npx.cmd"]) {
|
|
5597
|
-
const candidate =
|
|
6034
|
+
const candidate = join6(nodeDir, name);
|
|
5598
6035
|
if (exists(candidate)) {
|
|
5599
6036
|
return { command: candidate, argsPrefix: [] };
|
|
5600
6037
|
}
|
|
@@ -5617,7 +6054,7 @@ function which(cmd, pathEnv = process.env.PATH ?? "", platform = process.platfor
|
|
|
5617
6054
|
const name = platform === "win32" && !cmd.toLowerCase().endsWith(".exe") ? `${cmd}.exe` : cmd;
|
|
5618
6055
|
for (const dir of pathEnv.split(delimiter)) {
|
|
5619
6056
|
if (!dir) continue;
|
|
5620
|
-
const candidate =
|
|
6057
|
+
const candidate = join6(dir, name);
|
|
5621
6058
|
try {
|
|
5622
6059
|
accessSync(candidate, constants.X_OK);
|
|
5623
6060
|
if (statSync2(candidate).isFile()) return resolve2(candidate);
|
|
@@ -5745,11 +6182,11 @@ function launchSpecFor(agent) {
|
|
|
5745
6182
|
}
|
|
5746
6183
|
}
|
|
5747
6184
|
function handoffFallbackDirs(name, home) {
|
|
5748
|
-
const dirs = [
|
|
5749
|
-
if (name === "claude") dirs.push(
|
|
6185
|
+
const dirs = [join6(home, ".local", "bin")];
|
|
6186
|
+
if (name === "claude") dirs.push(join6(home, ".claude", "local"));
|
|
5750
6187
|
return dirs;
|
|
5751
6188
|
}
|
|
5752
|
-
function findHandoffBinary(name, pathEnv = process.env.PATH ?? "", home =
|
|
6189
|
+
function findHandoffBinary(name, pathEnv = process.env.PATH ?? "", home = homedir5(), platform = process.platform) {
|
|
5753
6190
|
const onPath = which(name, pathEnv, platform);
|
|
5754
6191
|
if (onPath) return onPath;
|
|
5755
6192
|
return which(name, handoffFallbackDirs(name, home).join(delimiter), platform);
|
|
@@ -5913,6 +6350,16 @@ var AcpAdapter = class {
|
|
|
5913
6350
|
this.noticeCbs.push(cb);
|
|
5914
6351
|
},
|
|
5915
6352
|
converseLocal: (text, overrideMode) => this.converseLocal(text, overrideMode),
|
|
6353
|
+
// Review Minor 3: converseLocalWithUsage is exposed ONLY on a NO-PERSISTENCE
|
|
6354
|
+
// (ephemeral, one-shot) construction — this.session undefined is the exact
|
|
6355
|
+
// invariant D13 already encodes (no persistence wired => AcpClient.start()'s
|
|
6356
|
+
// newSession path). A WARM adapter (session wired, e.g. createAdapter(cfg,
|
|
6357
|
+
// channelId)) never gets this method on its AgentSession at all, so a future
|
|
6358
|
+
// warm caller cannot silently advance the channel's disk-backed cumulative
|
|
6359
|
+
// cost baseline — the method simply does not exist to call.
|
|
6360
|
+
...this.session ? {} : {
|
|
6361
|
+
converseLocalWithUsage: (text, overrideMode) => this.converseLocalWithUsage(text, overrideMode)
|
|
6362
|
+
},
|
|
5916
6363
|
stop: async () => {
|
|
5917
6364
|
this.stopped = true;
|
|
5918
6365
|
await this.client?.stop();
|
|
@@ -5960,6 +6407,22 @@ var AcpAdapter = class {
|
|
|
5960
6407
|
return `[braid] local turn failed: ${err?.message ?? String(err)}`;
|
|
5961
6408
|
}
|
|
5962
6409
|
}
|
|
6410
|
+
/**
|
|
6411
|
+
* D13/D20: like converseLocal, but for a lisa fresh-session (ephemeral one-shot)
|
|
6412
|
+
* turn — ALSO returns usage, converted via this.usageSource (for the ephemeral
|
|
6413
|
+
* construction path, an isolated non-persisted source; see createEphemeralLisaAdapter).
|
|
6414
|
+
* Deliberately does NOT swallow errors into an error-string reply: a lisa turn that
|
|
6415
|
+
* fails must fail loudly (nothing posts to the flow), never post a garbled
|
|
6416
|
+
* "local turn failed" string as though it were a real builder/critic report.
|
|
6417
|
+
*/
|
|
6418
|
+
async converseLocalWithUsage(text, overrideMode) {
|
|
6419
|
+
const client = this.client;
|
|
6420
|
+
if (!client || this.stopped || this.gaveUp || this.handoffPaused) {
|
|
6421
|
+
throw new Error("[braid] agent unavailable for local conversation");
|
|
6422
|
+
}
|
|
6423
|
+
const result = await client.submit(text, overrideMode);
|
|
6424
|
+
return { text: result.text, usage: this.usageSource?.forTurn(result.usage) };
|
|
6425
|
+
}
|
|
5963
6426
|
/**
|
|
5964
6427
|
* Run one real channel turn against the warm session. On a turn that fails because
|
|
5965
6428
|
* the subprocess crashed, supervise a restart (single-flight, capped) and best-effort
|
|
@@ -6205,17 +6668,20 @@ var ClaudeAdapter = class {
|
|
|
6205
6668
|
permissionMode: "default",
|
|
6206
6669
|
canUseTool: async (toolName, toolInput) => {
|
|
6207
6670
|
const mode = overrideQueue[0] ?? policy.mode;
|
|
6208
|
-
const verdict = evaluateSdkTool(mode, policy.configDirAbs, toolName, toolInput
|
|
6671
|
+
const verdict = evaluateSdkTool(mode, policy.configDirAbs, toolName, toolInput, {
|
|
6672
|
+
cwd: process.cwd(),
|
|
6673
|
+
allowRead: policy.allowReadAbs ?? []
|
|
6674
|
+
});
|
|
6209
6675
|
if (verdict.allow) return { behavior: "allow", updatedInput: toolInput };
|
|
6210
6676
|
console.warn(`[braid] denied agent tool use: ${sanitizeForTty(verdict.reason)}`);
|
|
6211
6677
|
this.telemetry?.captureBridgeEvent("tool_call_blocked", {
|
|
6212
6678
|
tool: toolName,
|
|
6213
6679
|
mode,
|
|
6214
6680
|
path: "sdk",
|
|
6215
|
-
reason: verdict.deniedByMode ? "mode" : "tripwire"
|
|
6681
|
+
reason: verdict.deniedByScope ? "scope" : verdict.deniedByMode ? "mode" : "tripwire"
|
|
6216
6682
|
});
|
|
6217
6683
|
if (verdict.deniedByMode) {
|
|
6218
|
-
const hint = takeDenialHint(policy);
|
|
6684
|
+
const hint = takeDenialHint(policy, "operator", verdict.deniedByScope ? "scope" : "mode", process.cwd());
|
|
6219
6685
|
if (hint) console.warn(sanitizeForTty(hint));
|
|
6220
6686
|
}
|
|
6221
6687
|
return { behavior: "deny", message: verdict.reason };
|
|
@@ -6262,13 +6728,31 @@ var ClaudeAdapter = class {
|
|
|
6262
6728
|
};
|
|
6263
6729
|
}
|
|
6264
6730
|
};
|
|
6265
|
-
function
|
|
6731
|
+
function safeAllowRead(cfg) {
|
|
6732
|
+
try {
|
|
6733
|
+
return resolveScope(cfg, cfg.agent, process.env).runtimeConfig.filesystem.allowRead;
|
|
6734
|
+
} catch {
|
|
6735
|
+
return [];
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
function acpPolicyAndClientFactory(cfg, telemetry) {
|
|
6266
6739
|
const policy = {
|
|
6267
6740
|
mode: cfg.toolMode,
|
|
6268
6741
|
configDirAbs: resolve2(configDir(process.env)),
|
|
6269
|
-
agentName: cfg.agentName
|
|
6742
|
+
agentName: cfg.agentName,
|
|
6270
6743
|
// for the once-per-process "braid tools full <name>" denial hint
|
|
6744
|
+
allowReadAbs: safeAllowRead(cfg)
|
|
6271
6745
|
};
|
|
6746
|
+
const makeClient = (launch) => new AcpClient(
|
|
6747
|
+
launch,
|
|
6748
|
+
(l) => scopedLaunch(l, cfg, cfg.agent),
|
|
6749
|
+
afterChildExit,
|
|
6750
|
+
telemetry
|
|
6751
|
+
);
|
|
6752
|
+
return { policy, makeClient };
|
|
6753
|
+
}
|
|
6754
|
+
function createAdapter(cfg, channelId, telemetry) {
|
|
6755
|
+
const { policy, makeClient } = acpPolicyAndClientFactory(cfg, telemetry);
|
|
6272
6756
|
if (cfg.agentMode !== "acp") return new ClaudeAdapter(policy, telemetry);
|
|
6273
6757
|
if (channelId) {
|
|
6274
6758
|
const prev = detectProviderSwitch(configDir(process.env), cfg.relayWsUrl, cfg.agentName, channelId, cfg.agent);
|
|
@@ -6288,14 +6772,13 @@ function createAdapter(cfg, channelId, telemetry) {
|
|
|
6288
6772
|
}
|
|
6289
6773
|
const session = channelId ? makeSessionPersistence(cfg, channelId) : void 0;
|
|
6290
6774
|
const usageSource = channelId ? makeUsageSource(cfg, channelId) : void 0;
|
|
6291
|
-
const makeClient = (launch) => new AcpClient(
|
|
6292
|
-
launch,
|
|
6293
|
-
(l) => scopedLaunch(l, cfg, cfg.agent),
|
|
6294
|
-
afterChildExit,
|
|
6295
|
-
telemetry
|
|
6296
|
-
);
|
|
6297
6775
|
return new AcpAdapter(cfg.agent, makeClient, void 0, policy, session, usageSource, cfg.agentName, cfg.stripEnvKeys, cfg.fallbackModel);
|
|
6298
6776
|
}
|
|
6777
|
+
function createEphemeralLisaAdapter(cfg, telemetry) {
|
|
6778
|
+
const { policy, makeClient } = acpPolicyAndClientFactory(cfg, telemetry);
|
|
6779
|
+
const usageSource = makeEphemeralUsageSource(cfg);
|
|
6780
|
+
return new AcpAdapter(cfg.agent, makeClient, void 0, policy, void 0, usageSource, cfg.agentName, cfg.stripEnvKeys, cfg.fallbackModel);
|
|
6781
|
+
}
|
|
6299
6782
|
function makeSessionPersistence(cfg, channelId) {
|
|
6300
6783
|
const dir = configDir(process.env);
|
|
6301
6784
|
return {
|
|
@@ -6326,6 +6809,22 @@ function makeUsageSource(cfg, channelId) {
|
|
|
6326
6809
|
saveBaseline: (cumulative, currency) => updateStoredCost(dir, cfg.relayWsUrl, cfg.agentName, channelId, cumulative, currency, cfg.agent)
|
|
6327
6810
|
});
|
|
6328
6811
|
}
|
|
6812
|
+
function makeEphemeralUsageSource(cfg) {
|
|
6813
|
+
return new AcpUsageSource({
|
|
6814
|
+
model: cfg.model,
|
|
6815
|
+
provider: cfg.agent,
|
|
6816
|
+
authMode: effectiveUsageAuthMode({
|
|
6817
|
+
agent: cfg.agent,
|
|
6818
|
+
agentMode: cfg.agentMode,
|
|
6819
|
+
authMode: cfg.authMode ?? "auto",
|
|
6820
|
+
env: process.env,
|
|
6821
|
+
hasSubscriptionLogin: detectSubscriptionLogin(process.env)
|
|
6822
|
+
}),
|
|
6823
|
+
loadBaseline: () => void 0,
|
|
6824
|
+
saveBaseline: () => {
|
|
6825
|
+
}
|
|
6826
|
+
});
|
|
6827
|
+
}
|
|
6329
6828
|
|
|
6330
6829
|
// src/providerPrompt.ts
|
|
6331
6830
|
var PROVIDER_LABELS = {
|
|
@@ -6460,7 +6959,7 @@ function detectSubscriptionLogin(env) {
|
|
|
6460
6959
|
return void 0;
|
|
6461
6960
|
}
|
|
6462
6961
|
};
|
|
6463
|
-
const home = env.HOME?.trim() ||
|
|
6962
|
+
const home = env.HOME?.trim() || homedir6();
|
|
6464
6963
|
return hasSubscriptionLogin(env, readText, home, keychainProbeFor(home));
|
|
6465
6964
|
}
|
|
6466
6965
|
function isHttpsOrLocalhostUrl(s) {
|
|
@@ -6874,7 +7373,7 @@ function parseClaudeConfigModel(jsonText) {
|
|
|
6874
7373
|
function readClaudeConfigModel(agent) {
|
|
6875
7374
|
if (agent !== "claude-code") return null;
|
|
6876
7375
|
try {
|
|
6877
|
-
const text = readFileSync3(
|
|
7376
|
+
const text = readFileSync3(join7(homedir6(), ".claude.json"), "utf8");
|
|
6878
7377
|
return parseClaudeConfigModel(text);
|
|
6879
7378
|
} catch {
|
|
6880
7379
|
return null;
|
|
@@ -7164,6 +7663,7 @@ export {
|
|
|
7164
7663
|
acquireAgentLock,
|
|
7165
7664
|
pinEndpoint,
|
|
7166
7665
|
updateAgentWorkspace,
|
|
7666
|
+
isFilesystemRoot,
|
|
7167
7667
|
narrowestMode,
|
|
7168
7668
|
narrowestOverride,
|
|
7169
7669
|
toolModeLabel,
|
|
@@ -7191,6 +7691,7 @@ export {
|
|
|
7191
7691
|
interactiveHandoffSupported,
|
|
7192
7692
|
interactiveResumeSpecFor,
|
|
7193
7693
|
createAdapter,
|
|
7694
|
+
createEphemeralLisaAdapter,
|
|
7194
7695
|
handoffCwdFor,
|
|
7195
7696
|
PROVIDER_LABELS,
|
|
7196
7697
|
chooseFirstRunProvider,
|