@tritard/waterbrother 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Waterbrother: Grok-powered coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1682,6 +1682,14 @@ function extractExplicitReadOnlyRoots(line, cwd) {
1682
1682
  return [...new Set(roots)];
1683
1683
  }
1684
1684
 
1685
+ function extractExplicitWriteRoots(line, cwd) {
1686
+ const text = String(line || "").trim();
1687
+ if (!/\b(create|make|build|write|save|generate|scaffold|edit|update|rewrite|delete|remove|move|rename)\b/i.test(text)) {
1688
+ return [];
1689
+ }
1690
+ return extractExplicitReadOnlyRoots(text, cwd);
1691
+ }
1692
+
1685
1693
  function maybeRewriteExplicitInspectionPrompt(promptText, readOnlyRoots = []) {
1686
1694
  const text = String(promptText || "").trim();
1687
1695
  if (readOnlyRoots.length === 0) return text;
@@ -3308,8 +3316,10 @@ async function runTextTurnInteractive({
3308
3316
  spinnerLabel = "thinking..."
3309
3317
  }) {
3310
3318
  const readOnlyRoots = extractExplicitReadOnlyRoots(promptText, context.cwd);
3319
+ const writeRoots = extractExplicitWriteRoots(promptText, context.cwd);
3311
3320
  const effectivePromptText = maybeRewriteExplicitInspectionPrompt(promptText, readOnlyRoots);
3312
3321
  agent.toolRuntime.setReadOnlyRoots(readOnlyRoots);
3322
+ agent.toolRuntime.setWriteRoots(writeRoots);
3313
3323
  await maybeAutoCompactConversation({
3314
3324
  agent,
3315
3325
  currentSession,
@@ -3459,6 +3469,7 @@ async function runTextTurnInteractive({
3459
3469
  detachInterruptListener();
3460
3470
  spinner.stop();
3461
3471
  agent.toolRuntime.setReadOnlyRoots([]);
3472
+ agent.toolRuntime.setWriteRoots([]);
3462
3473
  }
3463
3474
 
3464
3475
  context.lastUsage = response?.usage || null;
package/src/path-utils.js CHANGED
@@ -39,8 +39,8 @@ function isAllowedReadOnlyHomePath(targetPath) {
39
39
  return COMMON_READ_ONLY_HOME_ROOTS.some((rootPath) => isWithinPath(rootPath, targetPath));
40
40
  }
41
41
 
42
- function isAllowedExplicitReadOnlyPath(targetPath, allowedReadOnlyRoots = []) {
43
- return allowedReadOnlyRoots.some((rootPath) => {
42
+ function isAllowedExplicitPath(targetPath, allowedRoots = []) {
43
+ return allowedRoots.some((rootPath) => {
44
44
  if (!rootPath) return false;
45
45
  const resolvedRoot = canonicalizePath(String(rootPath));
46
46
  return isWithinPath(resolvedRoot, targetPath);
@@ -65,11 +65,14 @@ export function resolveSandboxPath(baseDir, targetPath, allowOutsideCwd = false,
65
65
  const rel = path.relative(baseReal, resolved);
66
66
  const outside = rel.startsWith("..") || path.isAbsolute(rel);
67
67
  const allowExplicitReadOnly =
68
- Array.isArray(options.allowedReadOnlyRoots) && isAllowedExplicitReadOnlyPath(resolved, options.allowedReadOnlyRoots);
68
+ Array.isArray(options.allowedReadOnlyRoots) && isAllowedExplicitPath(resolved, options.allowedReadOnlyRoots);
69
+ const allowExplicitWrite =
70
+ Array.isArray(options.allowedWriteRoots) && isAllowedExplicitPath(resolved, options.allowedWriteRoots);
69
71
  if (
70
72
  outside &&
71
73
  !(options.allowReadOnlyHome === true && isAllowedReadOnlyHomePath(resolved)) &&
72
- !allowExplicitReadOnly
74
+ !allowExplicitReadOnly &&
75
+ !allowExplicitWrite
73
76
  ) {
74
77
  throw new Error(`Path is outside cwd sandbox: ${targetPath}`);
75
78
  }
package/src/tools.js CHANGED
@@ -517,6 +517,27 @@ function getTouchedPathsForTool(toolName, args = {}) {
517
517
  return [];
518
518
  }
519
519
 
520
+ function inferContractForTool(toolName, args = {}) {
521
+ const paths = getTouchedPathsForTool(toolName, args);
522
+ if (paths.length === 0) return null;
523
+ if (toolName === "run_shell" || toolName === "restore_checkpoint") return null;
524
+ const summaries = {
525
+ write_file: `Write ${paths.join(", ")}`,
526
+ replace_in_file: `Edit ${paths.join(", ")}`,
527
+ make_directory: `Create ${paths.join(", ")}`,
528
+ delete_path: `Delete ${paths.join(", ")}`,
529
+ apply_patch: `Patch ${paths.join(", ")}`
530
+ };
531
+ return {
532
+ summary: summaries[toolName] || `Mutate ${paths.join(", ")}`,
533
+ paths,
534
+ commands: [],
535
+ verification: [],
536
+ risk: toolName === "delete_path" ? "high" : "medium",
537
+ inferred: true
538
+ };
539
+ }
540
+
520
541
  function buildDefaultTurnRecord() {
521
542
  return {
522
543
  startedAt: new Date().toISOString(),
@@ -898,6 +919,7 @@ export function createToolRuntime({
898
919
  let checkpointCreatedThisTurn = false;
899
920
  let currentTurn = buildDefaultTurnRecord();
900
921
  let currentReadOnlyRoots = [];
922
+ let currentWriteRoots = [];
901
923
  let currentExperienceMode = normalizeExperienceMode(experienceMode);
902
924
  let currentAutonomyMode = normalizeAutonomyMode(autonomyMode);
903
925
  let currentRequireTurnContracts = requireTurnContracts !== false;
@@ -1333,7 +1355,9 @@ ${clipped}`;
1333
1355
  }
1334
1356
 
1335
1357
  async function writeFile(args = {}) {
1336
- const filePath = resolveSandboxPath(cwd, args.path, allowOutsideCwd);
1358
+ const filePath = resolveSandboxPath(cwd, args.path, allowOutsideCwd, {
1359
+ allowedWriteRoots: currentWriteRoots
1360
+ });
1337
1361
  await fs.mkdir(path.dirname(filePath), { recursive: true });
1338
1362
  if (args.mode === "append") {
1339
1363
  await fs.appendFile(filePath, args.content, "utf8");
@@ -1390,7 +1414,9 @@ ${clipped}`;
1390
1414
  }
1391
1415
 
1392
1416
  async function replaceInFile(args = {}) {
1393
- const filePath = resolveSandboxPath(cwd, args.path, allowOutsideCwd);
1417
+ const filePath = resolveSandboxPath(cwd, args.path, allowOutsideCwd, {
1418
+ allowedWriteRoots: currentWriteRoots
1419
+ });
1394
1420
  const source = await fs.readFile(filePath, "utf8");
1395
1421
 
1396
1422
  let next;
@@ -1571,13 +1597,17 @@ ${clipped}`;
1571
1597
  }
1572
1598
 
1573
1599
  async function makeDirectory(args = {}) {
1574
- const target = resolveSandboxPath(cwd, args.path, allowOutsideCwd);
1600
+ const target = resolveSandboxPath(cwd, args.path, allowOutsideCwd, {
1601
+ allowedWriteRoots: currentWriteRoots
1602
+ });
1575
1603
  await fs.mkdir(target, { recursive: true });
1576
1604
  return pretty({ ok: true, path: args.path });
1577
1605
  }
1578
1606
 
1579
1607
  async function deletePath(args = {}) {
1580
- const target = resolveSandboxPath(cwd, args.path, allowOutsideCwd);
1608
+ const target = resolveSandboxPath(cwd, args.path, allowOutsideCwd, {
1609
+ allowedWriteRoots: currentWriteRoots
1610
+ });
1581
1611
  if (target === cwd) throw new Error("Refusing to delete cwd root");
1582
1612
  await fs.rm(target, { recursive: Boolean(args.recursive), force: false });
1583
1613
  return pretty({ ok: true, path: args.path, recursive: Boolean(args.recursive) });
@@ -2078,6 +2108,12 @@ ${clipped}`;
2078
2108
  }
2079
2109
 
2080
2110
  if (currentRequireTurnContracts && toolRequiresContract(name, args)) {
2111
+ if (!currentTurn.contract) {
2112
+ const inferred = inferContractForTool(name, args);
2113
+ if (inferred) {
2114
+ currentTurn.contract = inferred;
2115
+ }
2116
+ }
2081
2117
  const contractDecision = contractAllows(name, args);
2082
2118
  if (!contractDecision.ok) {
2083
2119
  return pretty({
@@ -2188,6 +2224,8 @@ ${clipped}`;
2188
2224
  getShellCwd,
2189
2225
  setReadOnlyRoots(paths = []) { currentReadOnlyRoots = normalizePathList(paths); },
2190
2226
  getReadOnlyRoots() { return [...currentReadOnlyRoots]; },
2227
+ setWriteRoots(paths = []) { currentWriteRoots = normalizePathList(paths); },
2228
+ getWriteRoots() { return [...currentWriteRoots]; },
2191
2229
  setExperienceMode(mode) { currentExperienceMode = normalizeExperienceMode(mode); },
2192
2230
  getExperienceMode() { return currentExperienceMode; },
2193
2231
  setAutonomyMode(mode) { currentAutonomyMode = normalizeAutonomyMode(mode); },