@ai-sdk/harness-claude-code 1.0.174 → 1.0.176

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @ai-sdk/harness-claude-code
2
2
 
3
+ ## 1.0.176
4
+
5
+ ### Patch Changes
6
+
7
+ - e3f24b6: fix(harness-claude-code): require approval when accessing files outside of workDir with permission modes `allow-reads` and `allow-edits`
8
+ - Updated dependencies [51ca5b7]
9
+ - Updated dependencies [8a20b57]
10
+ - Updated dependencies [dc6f1c3]
11
+ - Updated dependencies [4ae6986]
12
+ - @ai-sdk/provider-utils@5.0.73
13
+ - @ai-sdk/harness@1.0.172
14
+
15
+ ## 1.0.175
16
+
17
+ ### Patch Changes
18
+
19
+ - @ai-sdk/harness@1.0.171
20
+
3
21
  ## 1.0.174
4
22
 
5
23
  ### Patch Changes
@@ -1,4 +1,4 @@
1
- import { appendFile, mkdir, writeFile } from "node:fs/promises";
1
+ import { appendFile, lstat, mkdir, realpath, writeFile } from "node:fs/promises";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { argv, env, pid, stdout } from "node:process";
@@ -7,6 +7,7 @@ import * as claudeAgentSdk from "@anthropic-ai/claude-agent-sdk";
7
7
  import * as mcpServerModule from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { isDeepStrictEqual } from "node:util";
9
9
  import { z } from "zod/v4";
10
+ import path from "node:path";
10
11
  //#region ../harness/dist/bridge/index.js
11
12
  const name = "AI_HarnessBridgeCapabilityUnsupportedError";
12
13
  /**
@@ -1315,6 +1316,53 @@ function isJsonLiteral(value) {
1315
1316
  return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
1316
1317
  }
1317
1318
  //#endregion
1319
+ //#region src/bridge/permission-paths.ts
1320
+ async function areFileToolPathsTrusted(options) {
1321
+ if (options.paths.length === 0) return false;
1322
+ try {
1323
+ const roots = [await realpath(options.workDir)];
1324
+ if (options.access === "read") for (const root of options.readableSkillRoots) try {
1325
+ roots.push(await realpath(root));
1326
+ } catch {
1327
+ continue;
1328
+ }
1329
+ for (const filePath of options.paths) {
1330
+ if (typeof filePath !== "string" || filePath.length === 0 || filePath !== filePath.trim() || filePath === "~" || filePath.startsWith("~/")) return false;
1331
+ if (!path.posix.isAbsolute(filePath) && !path.posix.isAbsolute(options.cwd)) return false;
1332
+ const absolutePath = path.posix.isAbsolute(filePath) ? filePath : `${options.cwd}/${filePath}`;
1333
+ for (const candidatePath of [absolutePath, path.posix.normalize(absolutePath)]) {
1334
+ const candidate = await resolvePermissionPath({ filePath: candidatePath });
1335
+ if (!roots.some((root) => {
1336
+ const relative = path.posix.relative(root, candidate);
1337
+ return relative === "" || relative !== ".." && !relative.startsWith("../") && !path.posix.isAbsolute(relative);
1338
+ })) return false;
1339
+ }
1340
+ }
1341
+ return true;
1342
+ } catch {
1343
+ return false;
1344
+ }
1345
+ }
1346
+ async function resolvePermissionPath(options) {
1347
+ let existingPath = options.filePath;
1348
+ const missing = [];
1349
+ for (;;) {
1350
+ let exists = true;
1351
+ try {
1352
+ await lstat(existingPath);
1353
+ } catch (error) {
1354
+ if (error == null || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") throw error;
1355
+ exists = false;
1356
+ }
1357
+ if (exists) return path.posix.join(await realpath(existingPath), ...missing);
1358
+ const parent = path.posix.dirname(existingPath);
1359
+ const segment = path.posix.basename(existingPath);
1360
+ if (parent === existingPath || segment === "." || segment === "..") throw new Error("Unable to resolve permission path.");
1361
+ missing.unshift(segment);
1362
+ existingPath = parent;
1363
+ }
1364
+ }
1365
+ //#endregion
1318
1366
  //#region src/bridge/tool-filtering.ts
1319
1367
  const PUBLIC_TO_NATIVE = {
1320
1368
  read: "Read",
@@ -1503,10 +1551,14 @@ function createPermissionOptions(input) {
1503
1551
  return {
1504
1552
  ...baseOptions,
1505
1553
  canUseTool: async (toolName, toolInput, options) => {
1506
- if (!needsHostApproval({
1554
+ if (!await needsHostApproval({
1507
1555
  toolName,
1556
+ toolInput,
1508
1557
  permissionMode,
1509
- inactiveNativeTools
1558
+ inactiveNativeTools,
1559
+ cwd: input.toolCwds.get(options.toolUseID) ?? "",
1560
+ readableSkillRoots: input.start.readableSkillRoots ?? [],
1561
+ blockedPath: options.blockedPath
1510
1562
  })) return {
1511
1563
  behavior: "allow",
1512
1564
  updatedInput: toolInput
@@ -1615,17 +1667,56 @@ function createGatewayModelOverrides(model) {
1615
1667
  const id = model.slice(10).replace(/\[[^\]]*\]$/, "");
1616
1668
  return id.includes(".") ? { [id.replace(/\./g, "-")]: model } : void 0;
1617
1669
  }
1618
- function needsHostApproval(input) {
1670
+ async function needsHostApproval(input) {
1619
1671
  if (input.toolName.startsWith("mcp__harness-tools__")) return false;
1620
- return input.inactiveNativeTools.has(input.toolName) || nativeToolRequiresApproval({
1672
+ if (input.inactiveNativeTools.has(input.toolName) || nativeToolRequiresApproval({
1621
1673
  nativeName: input.toolName,
1622
1674
  permissionMode: input.permissionMode
1675
+ })) return true;
1676
+ const args = input.toolInput != null && typeof input.toolInput === "object" ? input.toolInput : {};
1677
+ let paths;
1678
+ let access;
1679
+ if (input.toolName === "Glob" && (typeof args.pattern !== "string" || /\.\.|[{}()[\]]/.test(args.pattern) || args.pattern.includes("\\") || args.pattern.startsWith("/"))) return true;
1680
+ switch (input.toolName) {
1681
+ case "Read":
1682
+ paths = [args.file_path];
1683
+ access = "read";
1684
+ break;
1685
+ case "Write":
1686
+ case "Edit":
1687
+ paths = [args.file_path];
1688
+ access = "write";
1689
+ break;
1690
+ case "NotebookEdit":
1691
+ paths = [args.notebook_path];
1692
+ access = "write";
1693
+ break;
1694
+ case "Glob":
1695
+ case "Grep":
1696
+ paths = [args.path ?? input.cwd];
1697
+ access = "read";
1698
+ break;
1699
+ default:
1700
+ if (input.blockedPath == null) return false;
1701
+ paths = [];
1702
+ access = "read";
1703
+ }
1704
+ if (input.blockedPath != null) paths.push(input.blockedPath);
1705
+ if (!paths.every((value) => typeof value === "string")) return true;
1706
+ return !await areFileToolPathsTrusted({
1707
+ workDir: workdir,
1708
+ cwd: input.cwd,
1709
+ paths,
1710
+ access,
1711
+ readableSkillRoots: input.readableSkillRoots
1623
1712
  });
1624
1713
  }
1625
1714
  function createApprovalPreToolUseHook(input) {
1626
1715
  return async (hookInput, toolUseID) => {
1716
+ if (hookInput.hook_event_name !== "PreToolUse") return {};
1627
1717
  const toolName = hookInput.tool_name;
1628
1718
  if (toolUseID == null || typeof toolName !== "string" || toolName === "AskUserQuestion") return {};
1719
+ input.toolCwds.set(toolUseID, hookInput.cwd);
1629
1720
  const decision = input.decisions.get(toolUseID);
1630
1721
  if (decision != null) {
1631
1722
  input.nativeToolCallNames.set(toolUseID, toolName);
@@ -1638,8 +1729,11 @@ function createApprovalPreToolUseHook(input) {
1638
1729
  }
1639
1730
  } };
1640
1731
  }
1641
- if (!needsHostApproval({
1732
+ if (!await needsHostApproval({
1642
1733
  toolName,
1734
+ toolInput: hookInput.tool_input,
1735
+ cwd: hookInput.cwd,
1736
+ readableSkillRoots: input.readableSkillRoots,
1643
1737
  permissionMode: input.permissionMode,
1644
1738
  inactiveNativeTools: input.inactiveNativeTools
1645
1739
  })) return {};
@@ -1652,6 +1746,8 @@ function createApprovalPreToolUseHook(input) {
1652
1746
  }
1653
1747
  function nativeToolRequiresApproval(input) {
1654
1748
  if (input.permissionMode === "allow-all") return false;
1749
+ if (input.nativeName === "EnterWorktree" || input.nativeName === "ExitWorktree") return true;
1750
+ if (!(input.nativeName in NATIVE_TOOL_KINDS)) return true;
1655
1751
  const kind = NATIVE_TOOL_KINDS[input.nativeName] ?? "edit";
1656
1752
  if (input.permissionMode === "allow-edits") return kind === "bash";
1657
1753
  return kind === "edit" || kind === "bash";
@@ -1768,7 +1864,9 @@ async function runTurn(start, turn) {
1768
1864
  const inactiveNativeTools = resolveInactiveNativeTools(start.builtinToolFiltering);
1769
1865
  const permissionMode = start.permissionMode ?? "allow-all";
1770
1866
  const approvalDecisions = /* @__PURE__ */ new Map();
1867
+ const toolCwds = /* @__PURE__ */ new Map();
1771
1868
  const permissionOptions = createPermissionOptions({
1869
+ toolCwds,
1772
1870
  approvalDecisions,
1773
1871
  start,
1774
1872
  inactiveNativeTools,
@@ -1790,6 +1888,8 @@ async function runTurn(start, turn) {
1790
1888
  const modelOverrides = createGatewayModelOverrides(start.model);
1791
1889
  const deferredToolUseIds = /* @__PURE__ */ new Set();
1792
1890
  const approvalPreToolUseHook = createApprovalPreToolUseHook({
1891
+ toolCwds,
1892
+ readableSkillRoots: start.readableSkillRoots ?? [],
1793
1893
  permissionMode,
1794
1894
  inactiveNativeTools: new Set(inactiveNativeTools),
1795
1895
  decisions: approvalDecisions,