@snowyroad/braid 0.95.0 → 0.96.1
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-XUQU2KWB.js → chunk-JXTULCXB.js} +527 -106
- package/dist/cli.js +14 -1
- package/dist/{http-O3LXKBAJ.js → http-MMZM3WE3.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-MMZM3WE3.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-MMZM3WE3.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
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
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
|
+
}
|
|
1191
1555
|
}
|
|
1192
1556
|
return null;
|
|
1193
1557
|
}
|
|
1194
|
-
function evaluateAcpPermission(mode, configDirAbs, req) {
|
|
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}`,
|
|
@@ -4061,12 +4473,9 @@ var RelayClient = class {
|
|
|
4061
4473
|
break;
|
|
4062
4474
|
}
|
|
4063
4475
|
case "reaction_notification": {
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
emoji: String(msg.emoji ?? "").slice(0, 16),
|
|
4068
|
-
reactorType: String(msg.reactorType ?? "").slice(0, 16)
|
|
4069
|
-
}));
|
|
4476
|
+
const emoji = sanitizeForTty(String(msg.emoji ?? "").slice(0, 16));
|
|
4477
|
+
const reactor = sanitizeForTty(String(msg.reactorType ?? "").slice(0, 16));
|
|
4478
|
+
if (emoji) console.info(`[braid] reaction ${emoji}${reactor ? ` from ${reactor}` : ""}`);
|
|
4070
4479
|
break;
|
|
4071
4480
|
}
|
|
4072
4481
|
case "task_available":
|
|
@@ -4622,9 +5031,9 @@ var RelayClient = class {
|
|
|
4622
5031
|
// ── End private memory lane ───────────────────────────────────────────────────
|
|
4623
5032
|
// ── Task board ──────────────────────────────────────────────────────────────
|
|
4624
5033
|
/** Shared helper: fetch to a task endpoint and return {ok, status, json}. Callers surface errors. */
|
|
4625
|
-
async taskFetch(method,
|
|
5034
|
+
async taskFetch(method, path5, body) {
|
|
4626
5035
|
const bodyStr = body === void 0 ? void 0 : JSON.stringify(body);
|
|
4627
|
-
const r = await this.httpJson(
|
|
5036
|
+
const r = await this.httpJson(path5, { method, body: bodyStr, requestClass: "relay.task" });
|
|
4628
5037
|
return { ok: r.ok, status: r.ok ? r.status : r.status ?? 0, json: r.ok ? r.json : r.json };
|
|
4629
5038
|
}
|
|
4630
5039
|
async createTask(channelId, title, description) {
|
|
@@ -4681,7 +5090,7 @@ var RelayClient = class {
|
|
|
4681
5090
|
* guard so malformed JSON is rejected with a clear error before any fetch call.
|
|
4682
5091
|
* Byte-parity contract: the EXACT docJson string is embedded as the "doc" value
|
|
4683
5092
|
* so what the bridge signs and what the relay stores are identical bytes. */
|
|
4684
|
-
async artifactWriteFetch(method,
|
|
5093
|
+
async artifactWriteFetch(method, path5, docJson, extraFields) {
|
|
4685
5094
|
try {
|
|
4686
5095
|
JSON.parse(docJson);
|
|
4687
5096
|
} catch (e) {
|
|
@@ -4689,7 +5098,7 @@ var RelayClient = class {
|
|
|
4689
5098
|
}
|
|
4690
5099
|
const extras = JSON.stringify(extraFields);
|
|
4691
5100
|
const body = `{"doc":${docJson},${extras.slice(1)}`;
|
|
4692
|
-
const r = await this.httpJson(
|
|
5101
|
+
const r = await this.httpJson(path5, { method, body, requestClass: "relay.artifact.write" });
|
|
4693
5102
|
return { ok: r.ok, status: r.ok ? r.status : r.status ?? 0, json: r.ok ? r.json : r.json };
|
|
4694
5103
|
}
|
|
4695
5104
|
/** Try to produce an artifact signature. Fail-open: returns undefined on any error. */
|
|
@@ -4755,9 +5164,9 @@ var RelayClient = class {
|
|
|
4755
5164
|
// attributed by the relay's own authenticated caller identity.
|
|
4756
5165
|
/** Shared helper for canvas calls: same {ok,status,json} shape as taskFetch,
|
|
4757
5166
|
* under its own requestClass so canvas HTTP metrics are distinguishable. */
|
|
4758
|
-
async canvasFetch(method,
|
|
5167
|
+
async canvasFetch(method, path5, body) {
|
|
4759
5168
|
const bodyStr = body === void 0 ? void 0 : JSON.stringify(body);
|
|
4760
|
-
const r = await this.httpJson(
|
|
5169
|
+
const r = await this.httpJson(path5, { method, body: bodyStr, requestClass: "relay.canvas" });
|
|
4761
5170
|
return { ok: r.ok, status: r.ok ? r.status : r.status ?? 0, json: r.ok ? r.json : r.json };
|
|
4762
5171
|
}
|
|
4763
5172
|
async getCanvas(channelId) {
|
|
@@ -5056,13 +5465,13 @@ var REFLECTION_REQUIRED_FIELDS = ["stage", "failure_family", "impacted_unit", "p
|
|
|
5056
5465
|
var REFLECTION_CLUSTER_FIELDS = ["stage", "failure_family", "impacted_unit"];
|
|
5057
5466
|
var VALID_NOTE_KINDS = /* @__PURE__ */ new Set(["note", ...STRUCTURED_NOTE_KINDS]);
|
|
5058
5467
|
function brokerSocketDir() {
|
|
5059
|
-
return
|
|
5468
|
+
return path3.join(os.tmpdir(), "braid-broker");
|
|
5060
5469
|
}
|
|
5061
5470
|
function brokerSocketPath(uuid, platform = process.platform) {
|
|
5062
5471
|
if (platform === "win32") {
|
|
5063
5472
|
return `\\\\.\\pipe\\braid-broker-${uuid}`;
|
|
5064
5473
|
}
|
|
5065
|
-
return
|
|
5474
|
+
return path3.join(brokerSocketDir(), `${uuid}.sock`);
|
|
5066
5475
|
}
|
|
5067
5476
|
var SourceBroker = class {
|
|
5068
5477
|
/** `platform` injected so the win32 named-pipe branch is unit-testable on a POSIX host
|
|
@@ -5401,7 +5810,7 @@ function grantSrtWinRunnerRX(deps) {
|
|
|
5401
5810
|
return;
|
|
5402
5811
|
}
|
|
5403
5812
|
const systemRoot = deps?.systemRoot ?? process.env["SystemRoot"] ?? "C:\\Windows";
|
|
5404
|
-
const icacls =
|
|
5813
|
+
const icacls = path4.win32.join(systemRoot, "System32", "icacls.exe");
|
|
5405
5814
|
const spawnFn = deps?.spawnSync ?? ((cmd, args) => nodeSpawnSync2(cmd, args, { shell: false }));
|
|
5406
5815
|
const result = spawnFn(icacls, [runnerPath, "/grant", "sandbox-runtime-users:(RX)"]);
|
|
5407
5816
|
if (result.status !== 0) {
|
|
@@ -5417,7 +5826,7 @@ function brokerSocketNetworkExtras() {
|
|
|
5417
5826
|
const dir = brokerSocketDir();
|
|
5418
5827
|
const subpaths = [dir];
|
|
5419
5828
|
try {
|
|
5420
|
-
const resolved =
|
|
5829
|
+
const resolved = path4.join(realpathSync4(path4.dirname(dir)), path4.basename(dir));
|
|
5421
5830
|
if (resolved !== dir) subpaths.push(resolved);
|
|
5422
5831
|
} catch {
|
|
5423
5832
|
}
|
|
@@ -5593,7 +6002,7 @@ function formatRateLimitNotice(agentName, provider, message) {
|
|
|
5593
6002
|
|
|
5594
6003
|
// src/adapter.ts
|
|
5595
6004
|
function defaultToolPolicy() {
|
|
5596
|
-
return { mode: "readonly", configDirAbs: resolve2(configDir(process.env)) };
|
|
6005
|
+
return { mode: "readonly", configDirAbs: resolve2(configDir(process.env)), allowReadAbs: [] };
|
|
5597
6006
|
}
|
|
5598
6007
|
var ADAPTER_VERSIONS = {
|
|
5599
6008
|
// Chosen 2026-06-11 (latest verified releases at pin time).
|
|
@@ -5613,13 +6022,13 @@ function pinned(pkg) {
|
|
|
5613
6022
|
function computeNpxLaunch(platform, execPath, exists) {
|
|
5614
6023
|
const nodeDir = dirname3(execPath);
|
|
5615
6024
|
if (platform === "win32") {
|
|
5616
|
-
const npxCliJs =
|
|
6025
|
+
const npxCliJs = join6(nodeDir, "node_modules", "npm", "bin", "npx-cli.js");
|
|
5617
6026
|
if (exists(npxCliJs)) {
|
|
5618
6027
|
return { command: execPath, argsPrefix: [npxCliJs] };
|
|
5619
6028
|
}
|
|
5620
6029
|
}
|
|
5621
6030
|
for (const name of ["npx", "npx.cmd"]) {
|
|
5622
|
-
const candidate =
|
|
6031
|
+
const candidate = join6(nodeDir, name);
|
|
5623
6032
|
if (exists(candidate)) {
|
|
5624
6033
|
return { command: candidate, argsPrefix: [] };
|
|
5625
6034
|
}
|
|
@@ -5642,7 +6051,7 @@ function which(cmd, pathEnv = process.env.PATH ?? "", platform = process.platfor
|
|
|
5642
6051
|
const name = platform === "win32" && !cmd.toLowerCase().endsWith(".exe") ? `${cmd}.exe` : cmd;
|
|
5643
6052
|
for (const dir of pathEnv.split(delimiter)) {
|
|
5644
6053
|
if (!dir) continue;
|
|
5645
|
-
const candidate =
|
|
6054
|
+
const candidate = join6(dir, name);
|
|
5646
6055
|
try {
|
|
5647
6056
|
accessSync(candidate, constants.X_OK);
|
|
5648
6057
|
if (statSync2(candidate).isFile()) return resolve2(candidate);
|
|
@@ -5770,11 +6179,11 @@ function launchSpecFor(agent) {
|
|
|
5770
6179
|
}
|
|
5771
6180
|
}
|
|
5772
6181
|
function handoffFallbackDirs(name, home) {
|
|
5773
|
-
const dirs = [
|
|
5774
|
-
if (name === "claude") dirs.push(
|
|
6182
|
+
const dirs = [join6(home, ".local", "bin")];
|
|
6183
|
+
if (name === "claude") dirs.push(join6(home, ".claude", "local"));
|
|
5775
6184
|
return dirs;
|
|
5776
6185
|
}
|
|
5777
|
-
function findHandoffBinary(name, pathEnv = process.env.PATH ?? "", home =
|
|
6186
|
+
function findHandoffBinary(name, pathEnv = process.env.PATH ?? "", home = homedir5(), platform = process.platform) {
|
|
5778
6187
|
const onPath = which(name, pathEnv, platform);
|
|
5779
6188
|
if (onPath) return onPath;
|
|
5780
6189
|
return which(name, handoffFallbackDirs(name, home).join(delimiter), platform);
|
|
@@ -6256,17 +6665,20 @@ var ClaudeAdapter = class {
|
|
|
6256
6665
|
permissionMode: "default",
|
|
6257
6666
|
canUseTool: async (toolName, toolInput) => {
|
|
6258
6667
|
const mode = overrideQueue[0] ?? policy.mode;
|
|
6259
|
-
const verdict = evaluateSdkTool(mode, policy.configDirAbs, toolName, toolInput
|
|
6668
|
+
const verdict = evaluateSdkTool(mode, policy.configDirAbs, toolName, toolInput, {
|
|
6669
|
+
cwd: process.cwd(),
|
|
6670
|
+
allowRead: policy.allowReadAbs ?? []
|
|
6671
|
+
});
|
|
6260
6672
|
if (verdict.allow) return { behavior: "allow", updatedInput: toolInput };
|
|
6261
6673
|
console.warn(`[braid] denied agent tool use: ${sanitizeForTty(verdict.reason)}`);
|
|
6262
6674
|
this.telemetry?.captureBridgeEvent("tool_call_blocked", {
|
|
6263
6675
|
tool: toolName,
|
|
6264
6676
|
mode,
|
|
6265
6677
|
path: "sdk",
|
|
6266
|
-
reason: verdict.deniedByMode ? "mode" : "tripwire"
|
|
6678
|
+
reason: verdict.deniedByScope ? "scope" : verdict.deniedByMode ? "mode" : "tripwire"
|
|
6267
6679
|
});
|
|
6268
6680
|
if (verdict.deniedByMode) {
|
|
6269
|
-
const hint = takeDenialHint(policy);
|
|
6681
|
+
const hint = takeDenialHint(policy, "operator", verdict.deniedByScope ? "scope" : "mode", process.cwd());
|
|
6270
6682
|
if (hint) console.warn(sanitizeForTty(hint));
|
|
6271
6683
|
}
|
|
6272
6684
|
return { behavior: "deny", message: verdict.reason };
|
|
@@ -6313,12 +6725,20 @@ var ClaudeAdapter = class {
|
|
|
6313
6725
|
};
|
|
6314
6726
|
}
|
|
6315
6727
|
};
|
|
6728
|
+
function safeAllowRead(cfg) {
|
|
6729
|
+
try {
|
|
6730
|
+
return resolveScope(cfg, cfg.agent, process.env).runtimeConfig.filesystem.allowRead;
|
|
6731
|
+
} catch {
|
|
6732
|
+
return [];
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6316
6735
|
function acpPolicyAndClientFactory(cfg, telemetry) {
|
|
6317
6736
|
const policy = {
|
|
6318
6737
|
mode: cfg.toolMode,
|
|
6319
6738
|
configDirAbs: resolve2(configDir(process.env)),
|
|
6320
|
-
agentName: cfg.agentName
|
|
6739
|
+
agentName: cfg.agentName,
|
|
6321
6740
|
// for the once-per-process "braid tools full <name>" denial hint
|
|
6741
|
+
allowReadAbs: safeAllowRead(cfg)
|
|
6322
6742
|
};
|
|
6323
6743
|
const makeClient = (launch) => new AcpClient(
|
|
6324
6744
|
launch,
|
|
@@ -6536,7 +6956,7 @@ function detectSubscriptionLogin(env) {
|
|
|
6536
6956
|
return void 0;
|
|
6537
6957
|
}
|
|
6538
6958
|
};
|
|
6539
|
-
const home = env.HOME?.trim() ||
|
|
6959
|
+
const home = env.HOME?.trim() || homedir6();
|
|
6540
6960
|
return hasSubscriptionLogin(env, readText, home, keychainProbeFor(home));
|
|
6541
6961
|
}
|
|
6542
6962
|
function isHttpsOrLocalhostUrl(s) {
|
|
@@ -6950,7 +7370,7 @@ function parseClaudeConfigModel(jsonText) {
|
|
|
6950
7370
|
function readClaudeConfigModel(agent) {
|
|
6951
7371
|
if (agent !== "claude-code") return null;
|
|
6952
7372
|
try {
|
|
6953
|
-
const text = readFileSync3(
|
|
7373
|
+
const text = readFileSync3(join7(homedir6(), ".claude.json"), "utf8");
|
|
6954
7374
|
return parseClaudeConfigModel(text);
|
|
6955
7375
|
} catch {
|
|
6956
7376
|
return null;
|
|
@@ -7240,6 +7660,7 @@ export {
|
|
|
7240
7660
|
acquireAgentLock,
|
|
7241
7661
|
pinEndpoint,
|
|
7242
7662
|
updateAgentWorkspace,
|
|
7663
|
+
isFilesystemRoot,
|
|
7243
7664
|
narrowestMode,
|
|
7244
7665
|
narrowestOverride,
|
|
7245
7666
|
toolModeLabel,
|