@letrquan/book 0.3.0 → 0.4.0-beta.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.
Files changed (148) hide show
  1. package/CHANGELOG.md +1024 -0
  2. package/README.md +6 -3
  3. package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
  4. package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-32WBCUAN.js +660 -0
  7. package/dist/chunk-32WBCUAN.js.map +1 -0
  8. package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
  9. package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
  10. package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
  11. package/dist/chunk-AHC7OU75.js.map +1 -0
  12. package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
  13. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  14. package/dist/chunk-B5FC3MYZ.js +395 -0
  15. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  16. package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
  17. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  18. package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
  19. package/dist/chunk-E5WLQSKA.js.map +1 -0
  20. package/dist/chunk-ECLC4CIJ.js +1181 -0
  21. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  22. package/dist/chunk-EEX6QNXV.js +127 -0
  23. package/dist/chunk-EEX6QNXV.js.map +1 -0
  24. package/dist/chunk-EFXVSQ5J.js +931 -0
  25. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  26. package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
  27. package/dist/chunk-ENLB7NYV.js.map +1 -0
  28. package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
  29. package/dist/chunk-FPR7SKIE.js.map +1 -0
  30. package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
  31. package/dist/chunk-HFB74CLM.js.map +1 -0
  32. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  33. package/dist/chunk-HOZXKOE3.js.map +1 -0
  34. package/dist/chunk-IDYWN37T.js +36 -0
  35. package/dist/chunk-IDYWN37T.js.map +1 -0
  36. package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
  37. package/dist/chunk-JGF5HLSI.js.map +1 -0
  38. package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
  39. package/dist/chunk-KDNWMKXV.js.map +1 -0
  40. package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
  41. package/dist/chunk-MEOB2DZA.js.map +1 -0
  42. package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
  43. package/dist/chunk-N2G4Q7ME.js +2672 -0
  44. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  45. package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
  46. package/dist/chunk-OJBMDM5H.js.map +1 -0
  47. package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
  48. package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
  49. package/dist/chunk-REASC2QG.js.map +1 -0
  50. package/dist/chunk-SBTU5BEC.js +114 -0
  51. package/dist/chunk-SBTU5BEC.js.map +1 -0
  52. package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
  53. package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
  54. package/dist/chunk-UCFJ43OS.js.map +1 -0
  55. package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
  56. package/dist/chunk-W7SLYS3M.js.map +1 -0
  57. package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
  58. package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
  59. package/dist/chunk-XJRJVXJR.js.map +1 -0
  60. package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
  61. package/dist/chunk-YIL6HTJX.js.map +1 -0
  62. package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
  63. package/dist/context-C3RAP6K5.js +41 -0
  64. package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
  65. package/dist/index.js +657 -754
  66. package/dist/index.js.map +1 -1
  67. package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
  68. package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
  69. package/dist/job-runner.js +10 -3
  70. package/dist/job-runner.js.map +1 -1
  71. package/dist/job-supervisor.js +13 -4
  72. package/dist/job-supervisor.js.map +1 -1
  73. package/dist/loader-DQNPDKOX.js +27 -0
  74. package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
  75. package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
  76. package/dist/memory-store-I6Z54VUN.js +51 -0
  77. package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
  78. package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
  79. package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
  80. package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
  81. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  82. package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
  83. package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
  84. package/dist/scrollback-GD76GNM4.js.map +1 -0
  85. package/dist/sdk.d.ts +876 -64
  86. package/dist/sdk.js +57 -37
  87. package/dist/sdk.js.map +1 -1
  88. package/dist/settings-loader-KDBH45AQ.js +43 -0
  89. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  90. package/dist/shell-selection-Z5WORZU4.js +31 -0
  91. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  92. package/package.json +5 -5
  93. package/dist/app-GE4OHJOC.js.map +0 -1
  94. package/dist/chunk-224FJZLH.js.map +0 -1
  95. package/dist/chunk-5RDRAO4B.js.map +0 -1
  96. package/dist/chunk-5YHTKWJN.js.map +0 -1
  97. package/dist/chunk-62QL5QUP.js.map +0 -1
  98. package/dist/chunk-67UZEWWF.js.map +0 -1
  99. package/dist/chunk-6OQQOMZR.js +0 -785
  100. package/dist/chunk-6OQQOMZR.js.map +0 -1
  101. package/dist/chunk-AQEQX4FF.js.map +0 -1
  102. package/dist/chunk-C7ADUNPR.js +0 -273
  103. package/dist/chunk-C7ADUNPR.js.map +0 -1
  104. package/dist/chunk-EFJ2NH6E.js.map +0 -1
  105. package/dist/chunk-IME7R2UI.js.map +0 -1
  106. package/dist/chunk-J62VZYOB.js +0 -1339
  107. package/dist/chunk-J62VZYOB.js.map +0 -1
  108. package/dist/chunk-JTPOI3KU.js.map +0 -1
  109. package/dist/chunk-QYBD22H5.js.map +0 -1
  110. package/dist/chunk-RFJ6Q6LE.js.map +0 -1
  111. package/dist/chunk-S4JMID4G.js +0 -259
  112. package/dist/chunk-S4JMID4G.js.map +0 -1
  113. package/dist/chunk-TLX3H2CL.js.map +0 -1
  114. package/dist/chunk-VPNLCMVR.js.map +0 -1
  115. package/dist/chunk-W3XFHEF4.js.map +0 -1
  116. package/dist/chunk-X4NJT436.js.map +0 -1
  117. package/dist/chunk-X66I2NQO.js +0 -137
  118. package/dist/chunk-X66I2NQO.js.map +0 -1
  119. package/dist/chunk-Y2VHN7TC.js.map +0 -1
  120. package/dist/chunk-YC6W4EJH.js.map +0 -1
  121. package/dist/context-CBOBDY5Q.js +0 -38
  122. package/dist/loader-S77OVTHH.js +0 -18
  123. package/dist/memory-store-DZUT5HUH.js +0 -75
  124. package/dist/scrollback-35ME2GFX.js.map +0 -1
  125. package/dist/settings-loader-JUNPGAAM.js +0 -35
  126. package/dist/shell-selection-CDHGJ4GC.js +0 -17
  127. /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  128. /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
  129. /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
  130. /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
  131. /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
  132. /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
  133. /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
  134. /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  135. /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
  136. /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  137. /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
  138. /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  139. /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  140. /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  141. /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  142. /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  143. /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  144. /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  145. /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  146. /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
  147. /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
  148. /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sandbox.ts","../src/tools/glob-regex.ts"],"sourcesContent":["import {\n accessSync,\n closeSync,\n constants,\n existsSync,\n lstatSync,\n openSync,\n readdirSync,\n readlinkSync,\n readSync,\n realpathSync,\n statSync,\n} from 'fs';\nimport { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'path';\nimport { homedir, platform } from 'os';\nimport type { ResolvedSettings } from './settings.js';\nimport { isOutside, resolveWorkspacePath, type PathComparison } from './tools/path-utils.js';\nimport type { CommandExecution } from './types/runtime.js';\nimport { globToRegex } from './tools/glob-regex.js';\n\nexport interface Sandbox {\n /**\n * Resolve a shell command into a sandboxed argv spawn.\n *\n * `workspaceRoot` is the only directory bound writable by default, and it is\n * deliberately not the command's working directory: `workdir` is a\n * model-supplied tool argument, and binding it would let the model widen its\n * own sandbox to any path — `workdir: \"/\"` would shadow every other mount and\n * hand back the whole host filesystem, read-write. Callers must keep the\n * working directory inside the workspace; extra paths go through\n * `sandbox.filesystem.allowWrite`.\n *\n * Returns null if the sandbox is unavailable.\n */\n wrap(\n command: string,\n workspaceRoot: string,\n protectedPaths?: ProtectedWorkspacePaths,\n ): CommandExecution | null;\n /** One-line description of the policy actually enforced, for diagnostics. */\n describe(): string;\n}\n\n/**\n * Try to detect bubblewrap (bwrap) on the system PATH.\n * Returns the bwrap binary path or null if not found.\n */\nfunction isExecutableFile(path: string): boolean {\n try {\n if (!statSync(path).isFile()) return false;\n accessSync(path, constants.X_OK);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction detectBwrap(): string | null {\n const candidates = [\n ...(process.env.PATH ?? '')\n .split(delimiter)\n .filter(Boolean)\n .map((dir) => join(dir, 'bwrap')),\n '/usr/local/bin/bwrap',\n '/opt/homebrew/bin/bwrap',\n '/usr/bin/bwrap',\n ];\n return candidates.find(isExecutableFile) ?? null;\n}\n\n/**\n * Why a specific command will not run inside a real sandbox boundary.\n *\n * These are the only three ways a Bash command escapes the sandbox, and both\n * settings that depend on \"is this command actually sandboxed?\"\n * (`allowUnsandboxedCommands` and `autoAllowBashIfSandboxed`) are decided from\n * this one enumeration so they can never disagree about a given command.\n */\nexport type SandboxSkipReason = 'disabled' | 'excluded' | 'unavailable';\n\nexport interface SandboxCoverage {\n /** True only when this exact command will execute inside a bubblewrap namespace. */\n sandboxed: boolean;\n /** Set whenever `sandboxed` is false. */\n reason?: SandboxSkipReason;\n}\n\n/**\n * Does a command match one of the `sandbox.excludedCommands` escape patterns?\n *\n * The whole command string is matched, not just its first line: the permission\n * path and the execution path must agree about the same text, and\n * `getPrimaryArg` truncates a multi-line command to its first line.\n */\nexport function matchesExcludedCommand(command: string, patterns: string[] = []): boolean {\n return patterns.some((pattern) => globToRegex(pattern).test(command));\n}\n\n/**\n * Is the bubblewrap backend usable on this machine at all? Independent of\n * settings, so callers can distinguish \"turned off\" from \"cannot be turned on\".\n */\nexport function sandboxBackendAvailable(): boolean {\n return platform() !== 'win32' && detectBwrap() !== null;\n}\n\n/**\n * Decide whether one specific command genuinely executes inside the sandbox.\n *\n * Ordering is deliberate. `enabled` is checked first because \"sandboxing is\n * off\" is the actionable diagnosis even on a host with no bwrap installed, and\n * because it is false by default — the common path never touches the\n * filesystem. `excludedCommands` is checked before the backend probe for the\n * same reason: an excluded command is unsandboxed regardless of what is\n * installed, so probing PATH for it would be wasted work.\n *\n * `backendAvailable` is injectable purely so tests can exercise the\n * missing-bwrap branch on a host that has bwrap installed.\n */\nexport function sandboxCoverage(\n command: string,\n settings: ResolvedSettings['sandbox'],\n backendAvailable: () => boolean = sandboxBackendAvailable,\n): SandboxCoverage {\n if (!settings.enabled) return { sandboxed: false, reason: 'disabled' };\n if (matchesExcludedCommand(command, settings.excludedCommands))\n return { sandboxed: false, reason: 'excluded' };\n if (!backendAvailable()) return { sandboxed: false, reason: 'unavailable' };\n return { sandboxed: true };\n}\n\nconst SKIP_REASON_DETAIL: Record<SandboxSkipReason, string> = {\n disabled: 'sandboxing is turned off (sandbox.enabled is false)',\n excluded: 'the command matches a sandbox.excludedCommands pattern, which runs it unsandboxed',\n unavailable:\n 'the bubblewrap (bwrap) sandbox backend is unavailable on this platform or is not installed',\n};\n\nconst SKIP_REASON_REMEDY: Record<SandboxSkipReason, string> = {\n disabled: 'Set sandbox.enabled to true',\n excluded: 'Remove the matching pattern from sandbox.excludedCommands',\n unavailable: 'Install bubblewrap (bwrap)',\n};\n\n/**\n * Where a `sandbox.*` key has to be written to be honoured.\n *\n * Named in every refusal because the obvious fix does not work: from\n * `.book/settings.json` or `.book/settings.local.json` the loader honours these\n * keys only in the tightening direction, so a user who \"solved\" the refusal by\n * editing the project's own settings would see it reported as successful and stay\n * refused (#373).\n */\nconst SANDBOX_SETTINGS_LOCATION = 'in ~/.book/settings.json, or with a --settings file';\n\n/**\n * The refusal shown when `sandbox.allowUnsandboxedCommands` is false and a\n * command would otherwise have run outside the sandbox. It names the setting\n * that caused the refusal, the specific reason the command could not be\n * sandboxed, and what to do about it.\n *\n * The settings location is attached to the *alternative* rather than to every\n * reason, because it is only part of that alternative: `allowUnsandboxedCommands`\n * and `excludedCommands` are loader-ignored in a workspace file, so \"set it\n * there\" is the instruction for them. A missing `bwrap` is fixed by installing\n * bubblewrap, and telling a user to edit `~/.book/settings.json` about it sends\n * them to look for a package name that is not in the file.\n */\nexport function unsandboxedRefusalMessage(reason: SandboxSkipReason): string {\n const remedy =\n reason === 'unavailable'\n ? 'Install bubblewrap (bwrap) to enable sandboxing.'\n : `${SKIP_REASON_REMEDY[reason]} ${SANDBOX_SETTINGS_LOCATION}, or set ` +\n 'sandbox.allowUnsandboxedCommands to true there to allow this command to run unsandboxed.';\n return [\n 'Refused to run this command outside the sandbox:',\n `${SKIP_REASON_DETAIL[reason]}.`,\n 'sandbox.allowUnsandboxedCommands is false, so unsandboxed commands are not permitted.',\n remedy,\n ].join(' ');\n}\n\n/**\n * Everything outside `sandbox.*` that decides whether the two policy switches\n * can actually bite. Passed as one object rather than as trailing booleans so a\n * new condition cannot be added to the permission path and forgotten here —\n * which is how `book doctor` starts reporting a policy stronger than the\n * enforced one.\n */\nexport interface SandboxPolicyState {\n /** A sandbox was really created, so some command can genuinely be confined. */\n sandboxActive: boolean;\n /**\n * The user configured `permissions.deny` / `permissions.ask` rules. See\n * `sandboxAutoAllows` in permissions.ts: any such rule keeps the default ask,\n * because a shell line evades a glob far too easily for the matched rules to\n * be the whole protection.\n */\n adjudicationConfigured: boolean;\n}\n\n/**\n * Effective (not merely configured) state of the two policy switches, for\n * `book doctor`. A setting whose configured value cannot bite is reported as\n * inert rather than as enabled, so the reported policy matches the enforced one.\n */\nexport function sandboxPolicySummary(\n settings: ResolvedSettings['sandbox'],\n state: SandboxPolicyState,\n): { unsandboxedCommands: string; autoAllowBash: string; gitDirectories: string } {\n const gitDirectories = settings.filesystem.allowGitWrites\n ? 'writable; hooks, config and pointer files read-only (sandbox.filesystem.allowGitWrites=true)'\n : 'read-only (sandbox.filesystem.allowGitWrites=false)';\n return {\n unsandboxedCommands: settings.allowUnsandboxedCommands\n ? 'allowed (sandbox.allowUnsandboxedCommands=true)'\n : 'refused (sandbox.allowUnsandboxedCommands=false)',\n autoAllowBash: !settings.autoAllowBashIfSandboxed\n ? 'off — every Bash call goes through the normal permission path (sandbox.autoAllowBashIfSandboxed=false)'\n : !state.sandboxActive\n ? 'inert — no command is sandboxed here, so nothing is auto-allowed (sandbox.autoAllowBashIfSandboxed=true)'\n : state.adjudicationConfigured\n ? 'inert — permissions.deny/ask rules are configured, so every Bash call still goes through the normal permission path (sandbox.autoAllowBashIfSandboxed=true)'\n : 'on for genuinely sandboxed commands (sandbox.autoAllowBashIfSandboxed=true)',\n gitDirectories,\n };\n}\n\n/**\n * The host facts the argv builder depends on: which paths exist, whether they\n * are directories, what a small file says, and the path semantics used to\n * resolve configured entries.\n *\n * Injectable so the generated argv can be pinned by tests on any platform.\n * The unit suite runs on Windows CI, where probing the real filesystem finds\n * none of the POSIX system mounts (`resolve('/usr')` lands on `C:\\usr`), so a\n * test against the real host would be asserting on binds that were never\n * emitted. Production always uses the real host below.\n */\nexport interface SandboxHost {\n /** Path semantics for configured entries; tests inject path.win32/path.posix. */\n path: PathComparison & {\n resolve(...segments: string[]): string;\n join(...segments: string[]): string;\n dirname(path: string): string;\n basename(path: string): string;\n };\n exists(path: string): boolean;\n isDirectory(path: string): boolean;\n /**\n * File contents, or null when the file is missing or unreadable. The read is\n * capped at `maxBytes` — a workspace-controlled file must not be able to make\n * the host read without bound — and the cap is per call because the files\n * differ by orders of magnitude: a `gitdir:` pointer is one line, a repository\n * config is a page or more and a `hooksPath` can sit anywhere in it.\n */\n readFile(path: string, maxBytes?: number): string | null;\n /** The size of a regular file in bytes, or null for anything else. */\n fileSize(path: string): number | null;\n /** Entry names of a directory; empty when it is absent, unreadable, or not one. */\n readDir(path: string): string[];\n /** What a path is *without* following a final symlink, or null when nothing is there. */\n entryKind(path: string): SandboxEntryKind | null;\n /** The path a symlink names, as written; null when the path is not a symlink. */\n linkTarget(path: string): string | null;\n /** A path with its links followed, or null when it cannot be resolved. */\n realpath(path: string): string | null;\n homedir(): string;\n}\n\n/** What a path is, as `lstat` sees it: a symlink is not yet what it points at. */\nexport type SandboxEntryKind = 'file' | 'directory' | 'symlink' | 'other';\n\n/**\n * The most bytes read out of a workspace-controlled control file. These files\n * carry one line — a `gitdir:` pointer, a `commondir` — and a workspace that can\n * make the host read anything at all can make it read gigabytes, so the read is\n * bounded rather than the file trusted to be small.\n */\nconst MAX_CONTROL_FILE_BYTES = 4096;\n\n/**\n * The cap for a repository config, which is the exception: it is a document, not\n * a pointer, and a busy one is a few hundred kilobytes of remotes before the\n * `[core]` stanza that says where the hooks live. At 4 KiB a `hooksPath` declared\n * late in a real config was simply not seen, and the directory it named stayed\n * writable. A config past *this* cap is refused instead of truncated — a\n * half-read config reads as one with no `hooksPath`, which is the unsafe answer.\n */\nconst MAX_GIT_CONFIG_BYTES = 1024 * 1024;\n\n/**\n * How deep the two workspace-controlled walks below go: `modules/<a>/<b>/…` for a\n * submodule path with segments, and `include` chains. Both are bounded by the\n * filesystem rather than by anything the walk can check, and both are\n * cycle-guarded by the sets they record what they have already visited.\n */\nconst MAX_WALK_DEPTH = 8;\n\nfunction entryKind(path: string): SandboxEntryKind | null {\n try {\n const stats = lstatSync(path);\n if (stats.isSymbolicLink()) return 'symlink';\n if (stats.isDirectory()) return 'directory';\n if (stats.isFile()) return 'file';\n return 'other';\n } catch {\n return null;\n }\n}\n\n/**\n * The production host: the real filesystem and the real path module.\n *\n * Exported so the contract it has to keep — a bounded read of a regular file,\n * and no throw for a path that is not there — is tested directly rather than\n * inferred from the argv it happens to produce.\n */\nexport const realSandboxHost: SandboxHost = {\n path: { resolve, join, relative, isAbsolute, sep, dirname, basename },\n exists: existsSync,\n // A path that vanished between the probe and the spawn is absent as far as the\n // mount list is concerned. `statSync` throws for it, and a throw here would\n // abort the whole bwrap invocation over a race, not a protection failure.\n isDirectory: (path) => {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n },\n readFile: (path, maxBytes = MAX_CONTROL_FILE_BYTES) => {\n // `lstat` first, and only a regular file. An unguarded readFileSync blocks\n // for ever on a FIFO (a workspace that plants one at `.git` would hang\n // every later sandboxed command) and never ends on a character device such\n // as `/dev/zero`.\n if (entryKind(path) !== 'file') return null;\n let fd: number | undefined;\n try {\n fd = openSync(path, 'r');\n const buffer = Buffer.alloc(maxBytes);\n const bytes = readSync(fd, buffer, 0, maxBytes, 0);\n return buffer.subarray(0, bytes).toString('utf-8');\n } catch {\n return null;\n } finally {\n if (fd !== undefined) {\n try {\n closeSync(fd);\n } catch {\n // Nothing to do: the read result stands either way.\n }\n }\n }\n },\n fileSize: (path) => {\n // A size is only meaningful for a regular file. A directory, a link and a\n // path that is not there are all \"no size to compare against the cap\".\n if (entryKind(path) !== 'file') return null;\n try {\n return statSync(path).size;\n } catch {\n return null;\n }\n },\n readDir: (path) => {\n try {\n return readdirSync(path);\n } catch {\n return [];\n }\n },\n entryKind,\n linkTarget: (path) => {\n try {\n return readlinkSync(path);\n } catch {\n return null;\n }\n },\n realpath: (path) => {\n try {\n return realpathSync(path);\n } catch {\n return null;\n }\n },\n homedir,\n};\n\n/**\n * Resolve a configured sandbox path. `~` is expanded the same way the rest of\n * the codebase expands it; a relative entry stays relative to the process cwd,\n * which `unbindablePaths` reports so it does not pass silently.\n */\nfunction expandPath(path: string, host: SandboxHost): string {\n if (path === '~') return host.homedir();\n if (path.startsWith('~/')) return host.path.join(host.homedir(), path.slice(2));\n return host.path.resolve(path);\n}\n\n/** bwrap fails the whole invocation if a bind source does not exist. */\nfunction bindIfPresent(args: string[], flag: string, path: string, host: SandboxHost): void {\n const target = expandPath(path, host);\n if (host.exists(target)) args.push(flag, target, target);\n}\n\n/**\n * Configured filesystem paths that cannot be applied because nothing exists at\n * them. bwrap aborts the whole invocation on a missing bind source, so these\n * have to be skipped — but a skipped rule is unenforced policy the user\n * believes is active, so it is reported rather than dropped quietly.\n */\nexport function unbindablePaths(\n settings: ResolvedSettings['sandbox'],\n host: SandboxHost = realSandboxHost,\n): string[] {\n return [\n ...settings.filesystem.allowWrite,\n ...settings.filesystem.denyWrite,\n ...settings.filesystem.denyRead,\n ].filter((path) => !host.exists(expandPath(path, host)));\n}\n\n/**\n * True when the declared network policy asks for something finer than\n * all-or-nothing. bwrap has no DNS or domain awareness, so a per-domain policy\n * cannot be honoured as written.\n */\nexport function hasDomainPolicy(settings: ResolvedSettings['sandbox']): boolean {\n return settings.network.allowedDomains.length > 0 || settings.network.deniedDomains.length > 0;\n}\n\n/**\n * A path with its links followed, in the caller's own path semantics.\n *\n * The nearest existing ancestor is what gets resolved and the rest of the path\n * re-appended, so a path that does not exist yet — an absent `.book/`, the\n * `hooks/` of a repository that has none — still comes back through a link\n * above it. Containment is decided on the canonical pair for the same reason\n * `resolveWorkspacePath` decides it there (#264): one directory has more than\n * one spelling, and a lexical comparison reads a path inside the workspace as\n * being outside it, or the reverse.\n */\nfunction canonicalPath(path: string, host: SandboxHost): string {\n const absolute = host.path.resolve(path);\n let existing = absolute;\n for (;;) {\n const real = host.realpath(existing);\n if (real !== null) return host.path.resolve(real, host.path.relative(existing, absolute));\n const parent = host.path.dirname(existing);\n if (parent === existing) return absolute;\n existing = parent;\n }\n}\n\n/**\n * True when `candidate` is the workspace root or something below it.\n *\n * Both sides are canonicalised first, and the comparison itself is\n * {@link isOutside} — the same predicate the file tools use — so a directory\n * called `..meta` is inside the tree it is in rather than outside it on the\n * strength of its first two characters.\n *\n * A path outside the root is not bound writable — it is not bound at all — so\n * there is nothing to protect there, and mounting it would hand the command a\n * view of a repository the user never opened.\n */\nfunction insideWorkspace(candidate: string, workspaceRoot: string, host: SandboxHost): boolean {\n return !isOutside(canonicalPath(workspaceRoot, host), canonicalPath(candidate, host), host.path);\n}\n\n/**\n * Path components of `path` strictly below `workspaceRoot`, in root-to-leaf order.\n * Returns empty array when `path` is not inside `workspaceRoot` or is the root itself.\n */\nfunction workspaceComponents(path: string, workspaceRoot: string, host: SandboxHost): string[] {\n const root = host.path.resolve(workspaceRoot);\n const resolved = host.path.resolve(path);\n const rel = host.path.relative(root, resolved);\n if (\n isOutside(root, resolved, host.path) ||\n !rel ||\n rel === '..' ||\n rel.startsWith('..' + host.path.sep)\n )\n return [];\n const segments = rel.split(host.path.sep).filter(Boolean);\n const components: string[] = [];\n let current = root;\n for (const segment of segments) {\n current = host.path.join(current, segment);\n components.push(current);\n }\n return components;\n}\n\n/** A git directory git itself would act on, and the work tree it serves. */\ninterface GitDirRef {\n dir: string;\n /**\n * The work tree a relative `core.hooksPath` in this directory's config is\n * resolved against — the root git resolves it against, which is the work tree\n * root and not the git dir.\n */\n workTree: string;\n}\n\ninterface WorkspaceGitDirsResult {\n gitDirs: GitDirRef[];\n pointerPaths: string[];\n refusals: ControlPathRefusal[];\n}\n\nfunction looksLikeGitDir(dir: string, host: SandboxHost): boolean {\n if (!host.isDirectory(dir)) return false;\n const head = host.path.join(dir, 'HEAD');\n if (!host.exists(head)) return false;\n const objects = host.path.join(dir, 'objects');\n if (host.isDirectory(objects)) return true;\n const commondir = host.path.join(dir, 'commondir');\n if (host.exists(commondir)) return true;\n return false;\n}\n\n/**\n * The git directories a workspace's `.git` names, in the order they apply.\n *\n * `.git` is usually the directory itself, but a worktree or submodule carries a\n * one-line `.git` file (`gitdir: <path>`; relative to the workspace when it is\n * not absolute), and that directory can itself name a `commondir` — the shared\n * part of the repository, where `hooks/` and `config` actually live for a\n * linked worktree. Under a repository sit `<gitdir>/worktrees/<name>` for every\n * linked worktree and `<gitdir>/modules/<path>` for every submodule, and a\n * submodule of a submodule nests again, so the walk is a breadth-first one over\n * directories already accepted.\n *\n * Only directories inside the workspace are returned, and only ones that are\n * directories at all: `gitdir:` and `commondir` are workspace-controlled text\n * and can name a plain file, which `<dir>/hooks` is not a directory bubblewrap\n * can mount.\n */\nfunction workspaceGitDirs(workspaceRoot: string, host: SandboxHost): WorkspaceGitDirsResult {\n const root = host.path.resolve(workspaceRoot);\n const canonicalRoot = canonicalPath(root, host);\n const found: GitDirRef[] = [];\n const pointerPaths: string[] = [];\n const refusals: ControlPathRefusal[] = [];\n const dotGit = host.path.join(root, '.git');\n const add = (candidate: string, workTree: string): void => {\n const dir = host.path.resolve(candidate);\n if (found.some((entry) => entry.dir === dir)) return;\n if (!host.isDirectory(dir) || !insideWorkspace(dir, root, host)) return;\n if (dir !== dotGit && !looksLikeGitDir(dir, host)) return;\n if (canonicalPath(dir, host) === canonicalRoot) {\n if (!refusals.some((r) => r.path === dir)) {\n refusals.push({ path: dir, reason: 'workspace-root-is-git-dir' });\n }\n return;\n }\n found.push({ dir, workTree: host.path.resolve(workTree) });\n };\n if (!host.exists(dotGit)) return { gitDirs: found, pointerPaths, refusals };\n if (host.isDirectory(dotGit)) {\n add(dotGit, root);\n } else {\n const target = /^gitdir:\\s*(.+)$/m.exec(host.readFile(dotGit) ?? '')?.[1]?.trim();\n if (!target) return { gitDirs: found, pointerPaths, refusals };\n const resolvedTarget = host.path.isAbsolute(target) ? target : host.path.join(root, target);\n pointerPaths.push(resolvedTarget);\n add(resolvedTarget, root);\n }\n // A linked worktree's own dir holds only `config.worktree`, `HEAD` and the\n // `gitdir` naming its work tree; the hooks and the shared config live in the\n // common dir.\n for (let index = 0; index < found.length; index++) {\n const { dir, workTree } = found[index];\n const common = host.readFile(host.path.join(dir, 'commondir'))?.trim();\n if (common) {\n const resolvedCommon = host.path.isAbsolute(common) ? common : host.path.join(dir, common);\n pointerPaths.push(resolvedCommon);\n add(resolvedCommon, workTree);\n }\n for (const name of host.readDir(host.path.join(dir, 'worktrees'))) {\n const linked = host.path.join(dir, 'worktrees', name);\n // `gitdir` inside a linked worktree's dir names the work tree's own\n // `.git` file, so its directory is the work tree root — what a relative\n // `core.hooksPath` in that worktree's config is resolved against.\n const workTreeGitdir = host.readFile(host.path.join(linked, 'gitdir'))?.trim();\n if (workTreeGitdir) {\n const resolvedGitdir = host.path.isAbsolute(workTreeGitdir)\n ? workTreeGitdir\n : host.path.join(linked, workTreeGitdir);\n pointerPaths.push(resolvedGitdir);\n }\n add(linked, workTreeGitdir ? host.path.dirname(workTreeGitdir) : root);\n }\n // `<gitdir>/modules/<path>` holds the git dir of the submodule at `<path>`,\n // and `<path>` is the submodule's own path in the work tree — which can have\n // segments, so `git submodule add url libs/deep` puts the git dir at\n // `modules/libs/deep` and `modules/libs` holds no `HEAD` and no `config`. It\n // is a container, and its own entries are the candidates. `HEAD` or `config`\n // is what tells the two apart, and the walk is depth-bounded because the tree\n // is workspace-controlled.\n const addModules = (modulesRoot: string, prefix: string, depth: number): void => {\n for (const name of host.readDir(modulesRoot)) {\n const moduleDir = host.path.join(modulesRoot, name);\n const workTreePath = prefix ? `${prefix}/${name}` : name;\n const isGitDir = ['HEAD', 'config'].some((file) =>\n host.exists(host.path.join(moduleDir, file)),\n );\n if (isGitDir) add(moduleDir, host.path.join(root, workTreePath));\n else if (depth < MAX_WALK_DEPTH) addModules(moduleDir, workTreePath, depth + 1);\n }\n };\n addModules(host.path.join(dir, 'modules'), '', 0);\n }\n // Every work tree the walk found carries its own `.git` pointer file, and the\n // pointer is what the host reads to decide *which* git dir those control files\n // are: `sub/.git` says `gitdir: ../.git/modules/sub`, which resolves against\n // the work tree holding it and not against the workspace root.\n for (const { workTree } of [...found]) {\n if (!insideWorkspace(workTree, root, host)) continue;\n const pointer = host.path.join(workTree, '.git');\n if (host.isDirectory(pointer)) continue;\n const target = /^gitdir:\\s*(.+)$/m.exec(host.readFile(pointer) ?? '')?.[1]?.trim();\n if (!target) continue;\n const resolvedTarget = host.path.isAbsolute(target) ? target : host.path.join(workTree, target);\n pointerPaths.push(resolvedTarget);\n add(resolvedTarget, workTree);\n }\n return { gitDirs: found, pointerPaths, refusals };\n}\n\n/**\n * The part of a git config line after the `=`, as git reads it.\n *\n * Two rules the raw text does not show: a `;` or `#` outside quotes ends the\n * value (so `hooksPath = .husky ; note` is `.husky`, and the directory named by\n * a path that kept the comment never exists), and a quoted value carries\n * backslash escapes. Without the first, the protection lands at a path the\n * repository chose and never fires; the second is rare in a path and cheap.\n */\nfunction parseConfigValue(raw: string): string {\n const text = raw.trim();\n if (!text.startsWith('\"')) {\n let quoted = false;\n for (let index = 0; index < text.length; index += 1) {\n const character = text[index];\n if (character === '\"') quoted = !quoted;\n else if (!quoted && (character === ';' || character === '#'))\n return text.slice(0, index).trim();\n }\n return text;\n }\n let value = '';\n for (let index = 1; index < text.length; index += 1) {\n const character = text[index];\n if (character === '\"') break;\n if (character !== '\\\\') {\n value += character;\n continue;\n }\n const escaped = text[index + 1];\n index += 1;\n value +=\n escaped === 'n' ? '\\n' : escaped === 't' ? '\\t' : escaped === 'b' ? '\\b' : (escaped ?? '');\n }\n return value;\n}\n\n/**\n * The section a `[header]` line names, or undefined when the line carries a\n * subsection. `[core \"x\"]` is not `[core]`, and counting it as one protects a\n * directory git never reads hooks from.\n */\nfunction configSectionName(line: string): string | undefined {\n const header = /^\\[\\s*([A-Za-z0-9.-]+)(\\s[^\\]]*)?\\]$/.exec(line.trim());\n if (!header || header[2]) return undefined;\n return header[1].toLowerCase();\n}\n\n/**\n * Every `core.hooksPath` a git config file states, or none.\n *\n * A deliberately small `[core]` parser rather than a git-config dependency: the\n * key is the one setting that moves `hooks/` out of the git dir, and it is read\n * from a file a workspace can rewrite. Git takes the *last* value for a key, so\n * a parser that stops at the first is protecting a directory git ignores; every\n * value is returned instead, which also makes the answer independent of which\n * one git would have chosen — the safer of the two mistakes to make.\n */\nexport function readCoreHooksPaths(configText: string): string[] {\n const found: string[] = [];\n let inCore = false;\n for (const line of configText.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed.startsWith('[')) {\n inCore = configSectionName(trimmed) === 'core';\n continue;\n }\n if (!inCore) continue;\n const separator = trimmed.indexOf('=');\n if (separator === -1) continue;\n if (trimmed.slice(0, separator).trim().toLowerCase() !== 'hookspath') continue;\n const value = parseConfigValue(trimmed.slice(separator + 1));\n if (value && !found.includes(value)) found.push(value);\n }\n return found;\n}\n\n/**\n * The files a git config pulls in, from `[include]` and `[includeIf \"…\"]`.\n *\n * An included file is part of the same config as far as git is concerned: a\n * `core.hooksPath` set in one is the directory git runs hooks from, and one\n * left writable inside the workspace is a control file the host reads on its next\n * commit. An `includeIf` condition is not evaluated here — only a file that\n * exists can be included, and a file the sandbox creates later is the case this\n * cannot reach.\n */\nexport function readGitConfigIncludes(configText: string): string[] {\n const found: string[] = [];\n let include = false;\n for (const line of configText.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed.startsWith('[')) {\n // An `includeIf` header carries the condition as its subsection, so the\n // section name is matched on the text up to it rather than exactly.\n include = configSectionName(trimmed) === 'include' || /^\\[includeif\\b/i.test(trimmed);\n continue;\n }\n if (!include) continue;\n const separator = trimmed.indexOf('=');\n if (separator === -1) continue;\n if (trimmed.slice(0, separator).trim().toLowerCase() !== 'path') continue;\n const value = parseConfigValue(trimmed.slice(separator + 1));\n if (value) found.push(value);\n }\n return found;\n}\n\n/**\n * One workspace path the namespace must shield, and how.\n *\n * - `directory` — a directory the host reads after the command exits. Bound\n * read-only when it exists, masked with an empty read-only tmpfs when it does\n * not, which both hides any contents the host grows there later and makes\n * creating it fail.\n * - `file` — a file the host reads after the command exits. Bound read-only\n * when it exists: bubblewrap aborts the whole invocation on a tmpfs mounted\n * over a file path (\"Not a directory\"), so an absent file cannot be masked at\n * all.\n * - `hidden-file` — a file whose *contents* must not reach the command, bound\n * over with `/dev/null`. A read-only bind of `.book/` still serves what is in\n * it, and the namespace shares the host network, so a key the user stored\n * there would be one `cat` away from a sandboxed command.\n */\nexport type ProtectedMountKind = 'directory' | 'file' | 'hidden-file';\n\nexport interface ProtectedMount {\n /**\n * The path the namespace mounts at, in the workspace's own spelling;\n * containment is decided on canonical paths.\n */\n path: string;\n kind: ProtectedMountKind;\n /** False when nothing exists at it, which for a directory becomes a mask. */\n present: boolean;\n}\n\nexport interface ProtectedWorkspacePaths {\n /**\n * Directories the namespace must pin as mount points in their own right.\n *\n * A read-only child mount is not enough: the directory that holds it can still\n * be renamed away and replaced, and rename(2) on a path that is not a mount\n * point succeeds. `--bind <dir> <dir>` pins it, which is what makes\n * `mv .git .git.old` fail with EBUSY instead of redirecting the host's git.\n *\n * Only the workspace's own top-level `.git` is pinned, and only in the opt-out\n * mode (`allowGitWrites: true`) where git directories stay writable. Read-only\n * mode has no pin because the git directory is already a mount point through\n * its own `--ro-bind`, and the half-done `git worktree remove` it describes\n * (deleting the work tree and failing on the read-only admin directory) now\n * happens by design in read-only mode.\n */\n pinnedDirectories: string[];\n mounts: ProtectedMount[];\n /**\n * Control paths this namespace cannot protect at all. A refusal rather than a\n * mount: bwrap aborts the whole invocation on the shapes that could be\n * mistaken for one, and guessing would leave a control file writable while\n * reporting a sandbox.\n */\n refusals: ControlPathRefusal[];\n /**\n * Control mounts inside read-only git dirs (hooks, config, etc.), preserved in\n * read-only mode so an allowWrite opt-in on a git dir can re-apply them.\n */\n gitDirControlMounts: ProtectedMount[];\n}\n\n/**\n * Why a workspace cannot be sandboxed, and which path says so.\n *\n * Both are shapes a repository controls and no mount can cover. The run is\n * refused with the path named, because the fix — replace the link, trim the\n * config — is a change to the workspace, not to a Book setting.\n */\nexport type ControlPathRefusalReason =\n 'symlinked-control-path' | 'oversized-git-config' | 'workspace-root-is-git-dir';\n\nexport interface ControlPathRefusal {\n path: string;\n reason: ControlPathRefusalReason;\n}\n\n/** A refusal in the words the model is told, which name the path to change. */\nexport function describeControlPathRefusal(refusal: ControlPathRefusal): string {\n if (refusal.reason === 'oversized-git-config') {\n return (\n `Refused to run this command in the sandbox: ${refusal.path} is larger than the ` +\n `${MAX_GIT_CONFIG_BYTES / 1024 / 1024} MiB read limit for a git config file, so the ` +\n 'core.hooksPath it may declare cannot be found and the hooks directory it names cannot ' +\n 'be made read-only. Trim the config, or run this command without the sandbox.'\n );\n }\n if (refusal.reason === 'workspace-root-is-git-dir') {\n return (\n `Refused to run this command in the sandbox: ${refusal.path} is the workspace root and ` +\n 'a git directory, so the sandbox cannot make it read-only without making the whole ' +\n 'workspace read-only. Run this command without the sandbox.'\n );\n }\n return (\n `Refused to run this command in the sandbox: ${refusal.path} is a symlink, and the ` +\n 'sandbox cannot protect a symlinked control path. The link itself sits in the writable ' +\n 'workspace, so a command could remove it, put a directory or file in its place, and write ' +\n 'the settings, hooks or repository config the host then reads through it. Replace the ' +\n 'symlink with the file or directory it names, or run this command without the sandbox.'\n );\n}\n\n/**\n * The workspace control files a sandboxed command must not be able to rewrite.\n *\n * Each one is read by the *host* after the command exits, which is what makes a\n * write to it an escape rather than a scratch file: `.book/settings.local.json`\n * is resolved on the host from the next session (it can switch the sandbox off\n * or add `Bash(*)`), a `.git/hooks/*` script runs on the next host `git commit`,\n * and `core.hooksPath` / `core.fsmonitor` in `.git/config` redirect both. The\n * permission layer asks before a file tool touches `.book/`, but a prompt is one\n * approval the model can ask for — and a shell command is not a file tool (#373).\n *\n * What this cannot cover in the default mode is stated in the docs rather than\n * implied here: a sandboxed command can still *create* a pointer file outside\n * every git dir it found (`.git/commondir` in a workspace with no `.git`, an\n * `[includeIf]` target that appears later), because the mounts are built from\n * the files that are there.\n */\nexport interface ProtectedWorkspacePathsOptions {\n /**\n * `false` (the default) binds every git directory the workspace names\n * read-only, which is what closes the pointer-file redirect: a command that\n * cannot write the git dir cannot create `.git/commondir` in it either\n * (#373). Sandboxed git writes fail, and the user opts back out with\n * `sandbox.filesystem.allowGitWrites`. `true` restores the previous shape —\n * git dirs writable with their control files protected one by one and the\n * top-level `.git` pinned — and reopens that gap, which is why only a trusted\n * layer may set it.\n */\n allowGitWrites?: boolean;\n denyDirectories?: string[];\n}\n\nexport function protectedWorkspacePaths(\n workspaceRoot: string,\n host: SandboxHost = realSandboxHost,\n options: ProtectedWorkspacePathsOptions = {},\n): ProtectedWorkspacePaths {\n const root = host.path.resolve(workspaceRoot);\n const canonicalRoot = canonicalPath(root, host);\n const allowGitWrites = options.allowGitWrites ?? false;\n const result: ProtectedWorkspacePaths = {\n pinnedDirectories: [],\n mounts: [],\n refusals: [],\n gitDirControlMounts: [],\n };\n const seen = new Set<string>();\n\n const safeDenyDirs = (options.denyDirectories ?? [])\n .filter((d) => host.isDirectory(d))\n .map((d) => canonicalPath(d, host));\n\n /**\n * The mount one control path needs, or null when it needs none.\n *\n * A symlink is refused rather than resolved. Protecting the link's *target* is\n * not protection, because the link itself sits in the writable workspace:\n * `rm .book && mkdir .book && …` replaces it, and the target protection then\n * guards a file nothing reads any more. There is no mount that pins a symlink,\n * and a mount shaped for one aborts the whole bwrap invocation, so a repository\n * shipping one is a workspace Book declines to sandbox.\n *\n * A path that is neither a regular file nor a directory — a FIFO, a socket, a\n * device — is skipped instead: nothing reads it as a control file, and there is\n * no mount bwrap can make over it.\n */\n const target = (path: string, kind: ProtectedMountKind): ProtectedMount | null => {\n const resolved = host.path.resolve(path);\n if (seen.has(resolved)) return null;\n const describe = (present: boolean): ProtectedMount => {\n seen.add(resolved);\n return { path: resolved, kind, present };\n };\n const first = host.entryKind(resolved);\n if (first === 'symlink') {\n const parentCanonical = canonicalPath(host.path.dirname(resolved), host);\n const symlinkLivePath = host.path.join(parentCanonical, host.path.basename(resolved));\n const isSafe = safeDenyDirs.some(\n (dir) => !isOutside(dir, symlinkLivePath, host.path) && dir !== symlinkLivePath,\n );\n if (!isSafe && !result.refusals.some((entry) => entry.path === resolved)) {\n result.refusals.push({ path: resolved, reason: 'symlinked-control-path' });\n }\n return null;\n }\n if (first === null) {\n // Absent: only a directory has a mask, and only a directory needs one —\n // an absent file has nothing to protect and nothing to create that git or\n // the settings resolver would read as one.\n return kind === 'directory' ? describe(false) : null;\n }\n if (first === 'directory') {\n // A mask is `--ro-bind /dev/null <dir>`, which bwrap rejects as \"Is a\n // directory\" — and one aborts the whole invocation, so a repository that\n // ships `.book/settings.local.json` as a directory would otherwise break\n // every sandboxed command in the workspace, not only its own.\n return kind === 'hidden-file' ? null : describe(true);\n }\n return first === 'file' ? describe(true) : null;\n };\n\n const addDirectory = (path: string): void => {\n const mount = target(path, 'directory');\n if (mount) result.mounts.push(mount);\n };\n const addFile = (path: string): void => {\n const mount = target(path, 'file');\n if (mount) result.mounts.push(mount);\n };\n const addMaskedFile = (path: string): void => {\n const mount = target(path, 'hidden-file');\n if (mount) result.mounts.push(mount);\n };\n\n const { gitDirs, pointerPaths, refusals: gitDirRefusals } = workspaceGitDirs(root, host);\n result.refusals.push(...gitDirRefusals);\n const boundCanonical: string[] = [];\n const gitDirMounts = new Set<ProtectedMount>();\n\n // The read-only mode binds every git dir the walk found, ahead of the other\n // mounts, at the path the sandbox sees: decide containment and dedupe on\n // canonical paths, but emit each bind at\n // `root + relative(canonicalPath(root), canonicalPath(dir))`.\n // Outermost first, and a dir inside one already bound is skipped: the outer\n // bind already covers it, and a second mount inside a mount point is noise.\n // Deliberately not through `target()`: a nested git dir that is a symlink is\n // bound at the path the sandbox sees rather than refused, because the whole\n // directory is covered by the bind — there is no path left through the link for a\n // replaced one to matter. A symlinked *top-level* `.git` is still refused, by\n // the pointer check below.\n if (!allowGitWrites) {\n const canonicalDirs = [...new Set(gitDirs.map((gitDir) => canonicalPath(gitDir.dir, host)))];\n canonicalDirs.sort(\n (left, right) => left.split(host.path.sep).length - right.split(host.path.sep).length,\n );\n for (const canonicalDir of canonicalDirs) {\n if (boundCanonical.some((outer) => !isOutside(outer, canonicalDir, host.path))) continue;\n if (!host.exists(canonicalDir)) continue;\n boundCanonical.push(canonicalDir);\n const rel = host.path.relative(canonicalRoot, canonicalDir);\n const bindPath = rel ? host.path.resolve(root, rel) : root;\n const mount: ProtectedMount = { path: bindPath, kind: 'directory', present: true };\n gitDirMounts.add(mount);\n result.mounts.push(mount);\n }\n }\n\n // The settings directory, which holds both workspace layers plus the trust\n // store writes and `migrations.json`.\n addDirectory(host.path.join(root, '.book'));\n // ...and the one file in it that can hold a provider credential. Read-only is\n // not enough: the namespace shares the host network, so the value has to be\n // unreadable inside it, not merely unwritable.\n addMaskedFile(host.path.join(root, '.book', 'settings.local.json'));\n // The legacy config file the host reads on the next launch, where its\n // `baseUrl` wins over every settings layer (src/config.ts).\n addFile(host.path.join(root, '.bookrc.json'));\n\n const dotGit = host.path.join(root, '.git');\n // Only a real directory inside the workspace is pinned, and only in the\n // opt-out mode where it stays writable: in the read-only mode the git dir is\n // a mount point through its own `--ro-bind`, which fails `mv .git elsewhere`\n // the same way, so `pinnedDirectories` stays empty there.\n if (host.entryKind(dotGit) === 'directory') {\n if (allowGitWrites) {\n result.pinnedDirectories.push(dotGit);\n }\n } else {\n // A worktree's `.git` is a file naming its git dir. Read-only, so the\n // pointer itself cannot be rewritten at another git directory. In the\n // read-only mode a real `.git` directory is already covered by the\n // git-dir binds above; calling `addFile` on a directory would emit a second\n // `--ro-bind` because `target()` describes any existing path when kind is\n // `file`. A symlinked `.git` is not a directory, so it is passed to\n // `addFile` and refused by `target()` in both modes.\n addFile(dotGit);\n }\n\n // Config files already read, so an `include` chain that names one of them ends\n // at the file rather than descending for ever.\n const configsRead = new Set<string>();\n const readConfig = (file: string, workTree: string, depth: number): void => {\n const at = host.path.resolve(file);\n if (configsRead.has(at)) return;\n configsRead.add(at);\n const size = host.fileSize(at);\n if (size !== null && size > MAX_GIT_CONFIG_BYTES) {\n // Truncating a config reads as one with no `hooksPath` in it, which is the\n // answer that leaves the hooks directory writable. Refusing says so instead.\n result.refusals.push({ path: at, reason: 'oversized-git-config' });\n return;\n }\n const text = host.readFile(at, MAX_GIT_CONFIG_BYTES);\n if (text === null) return;\n addFile(at);\n for (const hooksPath of readCoreHooksPaths(text)) {\n const directory = host.path.isAbsolute(hooksPath)\n ? hooksPath\n : host.path.join(workTree, hooksPath);\n if (insideWorkspace(directory, root, host)) addDirectory(directory);\n }\n if (depth >= MAX_WALK_DEPTH) return;\n for (const included of readGitConfigIncludes(text)) {\n const target = included.startsWith('~/')\n ? host.path.join(host.homedir(), included.slice(2))\n : host.path.join(host.path.dirname(at), included);\n readConfig(target, workTree, depth + 1);\n }\n };\n\n for (const gitDir of gitDirs) {\n // The analysis runs in both modes so refusals (a symlinked `hooks/`,\n // an oversized config) and a `core.hooksPath` outside the git dirs stay\n // exact. In read-only mode, redundant mounts inside the bound git dirs are\n // filtered out below.\n addDirectory(host.path.join(gitDir.dir, 'hooks'));\n for (const name of ['config', 'config.worktree', 'commondir', 'gitdir']) {\n addFile(host.path.join(gitDir.dir, name));\n }\n // The work tree's own `.git`: a directory is the git dir itself, already\n // covered by its own bind or pin, and a file is the pointer the host reads\n // to find the git dir. It is read-only in both modes for the same reason —\n // rewriting it to `gitdir: ../evil` moves every control path above onto a\n // repository the command built.\n const workTreePointer = host.path.join(gitDir.workTree, '.git');\n if (host.entryKind(workTreePointer) === 'file') addFile(workTreePointer);\n // `core.hooksPath` moves the hook directory out of the git dir entirely —\n // husky v9 points it at `.husky/_` — so a config this namespace leaves\n // writable is a hooks directory it never looked at. An `[include]`d file is\n // part of that config, and is followed to the same depth.\n for (const name of ['config', 'config.worktree']) {\n readConfig(host.path.join(gitDir.dir, name), gitDir.workTree, 0);\n }\n }\n\n // Drop every directory or file mount whose canonical path lies strictly inside\n // a present read-only directory mount (a bound git dir, .book/, a protected\n // hooksPath directory). Hiding a file's contents (hidden-file) is not redundant\n // with a read-only parent, so keep every hidden-file mask.\n // In read-only mode, keep the per-file control mounts that lie inside a bound\n // git dir in gitDirControlMounts so an allowWrite opt-in on a git dir can\n // re-apply them.\n const presentReadOnlyDirs = result.mounts\n .filter((mount) => mount.kind === 'directory' && mount.present)\n .map((mount) => canonicalPath(mount.path, host));\n\n const filteredMounts: ProtectedMount[] = [];\n const gitDirControlMounts: ProtectedMount[] = [];\n\n for (const mount of result.mounts) {\n if (mount.kind === 'hidden-file') {\n filteredMounts.push(mount);\n continue;\n }\n const mountCanonical = canonicalPath(mount.path, host);\n const strictlyInsidePresentReadOnlyDir = presentReadOnlyDirs.some(\n (dir) => !isOutside(dir, mountCanonical, host.path) && dir !== mountCanonical,\n );\n if (strictlyInsidePresentReadOnlyDir) {\n if (!allowGitWrites) {\n const strictlyInsideBoundGitDir = boundCanonical.some(\n (dir) => !isOutside(dir, mountCanonical, host.path) && dir !== mountCanonical,\n );\n if (strictlyInsideBoundGitDir) {\n gitDirControlMounts.push(mount);\n }\n }\n } else {\n filteredMounts.push(mount);\n }\n }\n\n result.mounts = filteredMounts;\n result.gitDirControlMounts = gitDirControlMounts;\n\n // In read-only mode, pin each ancestor directory strictly between the workspace root\n // and every protected mount whose path lies strictly below the workspace root and is not\n // inside a present read-only directory mount. A writable self-bind (--bind <dir> <dir>)\n // makes renaming the ancestor fail with EBUSY instead of moving the protected mount away.\n if (!allowGitWrites) {\n for (const mount of result.mounts) {\n const mountCanonical = canonicalPath(mount.path, host);\n if (isOutside(canonicalRoot, mountCanonical, host.path) || mountCanonical === canonicalRoot) {\n continue;\n }\n const isInsidePresentReadOnly = presentReadOnlyDirs.some(\n (dir) => !isOutside(dir, mountCanonical, host.path) && dir !== mountCanonical,\n );\n if (isInsidePresentReadOnly) continue;\n\n const ancestors = workspaceComponents(mount.path, root, host).slice(0, -1);\n for (const anc of ancestors) {\n if (!host.isDirectory(anc)) continue;\n const ancCanonical = canonicalPath(anc, host);\n if (presentReadOnlyDirs.some((dir) => !isOutside(dir, ancCanonical, host.path))) continue;\n if (!result.pinnedDirectories.includes(anc)) {\n result.pinnedDirectories.push(anc);\n }\n }\n }\n }\n\n const safeDirs = [...presentReadOnlyDirs, ...safeDenyDirs];\n\n // For every protected mount (pointer files, hooksPath directories, included config files),\n // discovered git dir, and pointer target, check each component strictly below workspace root.\n // A symlink is safe only if it lives inside a directory the host cannot see changed:\n // a present read-only directory mount, or a denyWrite / denyRead directory.\n const candidatesToCheck = new Set<string>([\n ...gitDirs.map((g) => g.dir),\n ...pointerPaths,\n ...result.mounts.map((m) => m.path),\n ...result.gitDirControlMounts.map((m) => m.path),\n ]);\n const checkedComponents = new Set<string>();\n for (const cand of candidatesToCheck) {\n for (const comp of workspaceComponents(cand, root, host)) {\n if (checkedComponents.has(comp)) continue;\n checkedComponents.add(comp);\n if (host.entryKind(comp) === 'symlink') {\n const parentCanonical = canonicalPath(host.path.dirname(comp), host);\n const symlinkLivePath = host.path.join(parentCanonical, host.path.basename(comp));\n const isSafe = safeDirs.some(\n (dir) => !isOutside(dir, symlinkLivePath, host.path) && dir !== symlinkLivePath,\n );\n if (!isSafe && !result.refusals.some((r) => r.path === comp)) {\n result.refusals.push({ path: comp, reason: 'symlinked-control-path' });\n }\n }\n }\n }\n\n return result;\n}\n\nfunction mountProtected(args: string[], mount: ProtectedMount, host: SandboxHost): void {\n const target = host.path.resolve(mount.path);\n if (mount.kind === 'hidden-file') {\n if (mount.present) args.push('--ro-bind', '/dev/null', target);\n return;\n }\n if (mount.kind === 'file') {\n if (mount.present) args.push('--ro-bind', target, target);\n return;\n }\n if (mount.present) {\n args.push('--ro-bind', target, target);\n return;\n }\n // `--tmpfs` creates the mount point, and bwrap does that *through* the\n // workspace bind: a sandboxed command in a workspace with no `.book/` leaves\n // an empty `.book/` directory on the host as a side effect. It is recorded in\n // docs/guide/configuration.md; the alternative would be a writable hole.\n args.push('--tmpfs', target, '--remount-ro', target);\n}\n\n/**\n * Build the bubblewrap argument vector for one command.\n *\n * Mount order is significant: bwrap applies operations in sequence and a later\n * mount shadows an earlier one covering the same path. The workspace bind\n * therefore comes *after* the system read-only binds and the /tmp tmpfs (a\n * workspace under /usr/local or /tmp would otherwise be silently shadowed), and\n * explicit filesystem policy comes after the workspace so it can override it.\n *\n * The workspace control files are the exception to \"policy comes after the\n * workspace\": their read-only mounts come after the workspace bind, so the\n * workspace does not shadow them, and before `denyWrite`/`denyRead`, which stay\n * free to be stricter still. The one `allowWrite` entry that is applied after\n * them is one that names a protected path or something under it, which is the\n * user asking for that path to be writable again; a broader root stays before,\n * so it cannot reopen what the protections just closed.\n *\n * Exported for testing: it does not require bwrap to be installed, and with an\n * injected `host` its output is fully determined by its arguments.\n */\nexport function buildSandboxExecution(\n bwrapPath: string,\n command: string,\n workspaceRoot: string,\n settings: ResolvedSettings['sandbox'],\n host: SandboxHost = realSandboxHost,\n protectedPaths?: ProtectedWorkspacePaths,\n): CommandExecution {\n const args: string[] = [];\n\n // Fresh PID/IPC/UTS namespaces, and the sandbox dies if the spawning process\n // does.\n //\n // Deliberately NOT --new-session: it makes bwrap call setsid(), which moves\n // the sandboxed tree into its own process group. Every teardown path here\n // (KillShell, foreground timeout, Ctrl-C) signals the group Node created with\n // `detached: true` and confirms death with `kill(-pgid, 0)`, so the group\n // would read as empty while the command kept running — a kill that reports\n // success and does nothing. The TIOCSTI hardening --new-session buys is moot\n // anyway: all three spawn sites pipe stdio and never hand over a tty.\n args.push('--unshare-pid', '--unshare-ipc', '--unshare-uts', '--die-with-parent');\n args.push('--proc', '/proc');\n args.push('--dev', '/dev');\n\n // Minimal system directories, read-only.\n for (const dir of ['/usr', '/lib', '/lib64', '/bin', '/sbin', '/etc', '/opt']) {\n bindIfPresent(args, '--ro-bind', dir, host);\n }\n\n args.push('--tmpfs', '/tmp');\n\n // The workspace is writable. Bound after /tmp so a workspace inside /tmp\n // (every mkdtemp-based test run, among others) survives the tmpfs.\n bindIfPresent(args, '--bind', workspaceRoot, host);\n\n // Declared filesystem policy overrides the defaults above.\n //\n // The control files are computed first, because where an `allowWrite` entry\n // lands in the sequence depends on them. An entry *at or under* a protected\n // path is the user asking for exactly that path to be writable again — a hook\n // installer that has to write `.git/hooks/pre-commit`, a cache under\n // `.book/cache` — so it is applied after the protected mounts and wins. An\n // entry *above* them (a parent directory, `/home`, the workspace itself) stays\n // before, because a broad root that reopened `.book/` or a hooks directory on\n // its way down would not be an opt-in but the default back the protections\n // exist to remove. Only a trusted layer can hold one: a workspace layer's\n // `allowWrite` entries are dropped by the loader (#373).\n const paths =\n protectedPaths ??\n protectedWorkspacePaths(workspaceRoot, host, {\n allowGitWrites: settings.filesystem.allowGitWrites,\n denyDirectories: [...settings.filesystem.denyWrite, ...settings.filesystem.denyRead].map(\n (p) => expandPath(p, host),\n ),\n });\n const isProtectedArea = (path: string): boolean => {\n const canonical = canonicalPath(expandPath(path, host), host);\n return paths.mounts.some(\n (mount) => !isOutside(canonicalPath(mount.path, host), canonical, host.path),\n );\n };\n\n // Control files the host acts on after this command exits come after the\n // workspace bind, so the workspace does not shadow them.\n //\n // The pins come before the opt-in binds as well: a pin is a writable self-bind\n // of the git dir, so an `allowWrite` entry *under* it emitted earlier is\n // shadowed by it and silently does nothing — the entry the user wrote, dropped\n // without a word. In the read-only git-dirs mode there is no pin: the git dir\n // is a protected mount, so an `allowWrite` entry naming it reopens what it names,\n // while control paths strictly below it (hooks, config, pointer files) stay\n // read-only unless an entry names them directly.\n for (const dir of paths.pinnedDirectories) {\n args.push('--bind', host.path.resolve(dir), host.path.resolve(dir));\n }\n for (const path of settings.filesystem.allowWrite) {\n if (isProtectedArea(path)) continue;\n bindIfPresent(args, '--bind', path, host);\n }\n for (const mount of paths.mounts) mountProtected(args, mount, host);\n // ...and only now the opt-ins for the protected areas themselves.\n const deferredOptInCanonicals: string[] = [];\n for (const path of settings.filesystem.allowWrite) {\n if (!isProtectedArea(path)) continue;\n const target = expandPath(path, host);\n if (host.exists(target)) {\n args.push('--bind', target, target);\n deferredOptInCanonicals.push(canonicalPath(target, host));\n }\n }\n\n // An allowWrite opt-in on a git dir reopens what it names, but not the control\n // paths strictly below it (hooks, config, pointer files). Emit each of those\n // control mounts whose canonical path is strictly below a deferred opt-in entry,\n // unless an entry exactly at that control path reopens it.\n for (const mount of paths.gitDirControlMounts) {\n const mountCanonical = canonicalPath(mount.path, host);\n const strictlyBelowDeferredOptIn = deferredOptInCanonicals.some(\n (optIn) => !isOutside(optIn, mountCanonical, host.path) && optIn !== mountCanonical,\n );\n const explicitlyOptedIn = deferredOptInCanonicals.includes(mountCanonical);\n if (strictlyBelowDeferredOptIn && !explicitlyOptedIn) {\n mountProtected(args, mount, host);\n }\n }\n\n // A mask hides a file's *contents*, and an opt-in bind for the directory that\n // holds it undoes that: `allowWrite: [\"<workspace>/.book\"]` puts the whole\n // directory back in reach, provider credential included. The masks are\n // therefore emitted again after every opt-in, so the last mount of a masked\n // path is `/dev/null` whichever entry named its parent.\n for (const mount of paths.mounts) {\n if (mount.kind === 'hidden-file') mountProtected(args, mount, host);\n }\n\n for (const path of settings.filesystem.denyWrite) bindIfPresent(args, '--ro-bind', path, host);\n // bwrap cannot unmount a subpath, so a denied path is masked instead. The\n // mask has to match the kind: --tmpfs needs to mkdir its target, so pointing\n // it at a file aborts the entire invocation with \"Not a directory\" — and a\n // credentials *file* is the most natural thing to deny. Files are masked with\n // /dev/null instead, which makes reads fail outright.\n for (const path of settings.filesystem.denyRead) {\n const target = expandPath(path, host);\n if (!host.exists(target)) continue;\n if (host.isDirectory(target)) args.push('--tmpfs', target);\n else args.push('--ro-bind', '/dev/null', target);\n }\n\n // bwrap can only share or unshare the network wholesale. A declared\n // per-domain policy cannot be enforced as written, so fail closed rather\n // than hand out the unrestricted host network the caller did not ask for.\n args.push(hasDomainPolicy(settings) ? '--unshare-net' : '--share-net');\n\n args.push('--cap-drop', 'ALL');\n\n // The command string is a single argv element: bash inside the sandbox\n // parses it, and nothing outside the sandbox ever does.\n args.push('--', '/bin/bash', '-c', command);\n\n return { file: bwrapPath, args };\n}\n\n/**\n * The context facts the sandbox decision reads. Structural rather than\n * `ToolContext` so this stays free of the tool layer: `Bash` and the `Check`\n * tool both pass one, and neither type is imported here.\n */\nexport interface SandboxDecisionContext {\n sandbox?: ResolvedSettings['sandbox'];\n workspaceRoot: string;\n /** The session's sandbox, built once per distinct settings object. */\n runtime?: { sandbox(settings: ResolvedSettings['sandbox']): Sandbox | null };\n}\n\n/**\n * How one command is to be run, or why it may not run at all.\n *\n * `exec` is set only on the sandboxed outcome, and is what the caller spawns\n * instead of the command. `reason` is set when a sandbox was asked for and\n * skipped, which is what `allowUnsandboxedCommands` governs; it is absent when\n * no sandbox was configured at all. `error` means do not run anything.\n */\nexport interface SandboxDecision {\n sandboxed: boolean;\n exec?: CommandExecution;\n reason?: SandboxSkipReason;\n error?: string;\n gitDirReadOnly?: boolean;\n}\n\n/**\n * Decide how `command` is to be run, and refuse it when it must not run at all.\n *\n * One function, because the order of these checks is the policy:\n *\n * 1. no sandbox configured, or the command is excluded → run unsandboxed,\n * unless `allowUnsandboxedCommands` is false, which refuses it;\n * 2. a workdir outside the workspace → refused, since the sandbox binds the\n * workspace and the command would run somewhere the caller did not ask for;\n * 3. a control path the namespace cannot protect → refused, since every mount\n * that could stand in for one either aborts bwrap or leaves the control file\n * writable;\n * 4. otherwise wrap it, and report the wrapped argv.\n *\n * A path that ends with the command running outside a bubblewrap namespace all\n * funnels through here, so `allowUnsandboxedCommands: false` cannot be enforced\n * on one escape and quietly missed on another. `Bash` and the `Check` tool both\n * call this, so a check cannot run unsandboxed in a session that refuses\n * unsandboxed commands (#373).\n *\n * The refusal and failure messages are the ones `Bash` has always returned; they\n * are part of the tool contract, not an implementation detail.\n */\nexport function decideSandboxExecution(\n ctx: SandboxDecisionContext,\n command: string,\n workdir: string,\n): SandboxDecision {\n // Every path that ends with the command running outside a bubblewrap\n // namespace funnels through here.\n const unsandboxed = (reason: SandboxSkipReason): SandboxDecision =>\n ctx.sandbox && !ctx.sandbox.allowUnsandboxedCommands\n ? { sandboxed: false, reason, error: unsandboxedRefusalMessage(reason) }\n : { sandboxed: false, reason };\n\n if (!ctx.sandbox?.enabled) return unsandboxed('disabled');\n if (matchesExcludedCommand(command, ctx.sandbox.excludedCommands)) return unsandboxed('excluded');\n\n // The sandbox binds the workspace, not this workdir. A workdir outside it\n // would leave the command with no working directory inside the namespace,\n // and silently running it against the workspace root instead would execute\n // somewhere the caller did not ask for.\n if (!resolveWorkspacePath(ctx.workspaceRoot, workdir)) {\n return {\n sandboxed: false,\n error: `workdir is outside the sandboxed workspace: ${workdir}. Add it to sandbox.filesystem.allowWrite, or run without the sandbox.`,\n };\n }\n // createSandbox emits one-time diagnostics, so reuse the session's instance\n // rather than rebuilding it per command.\n //\n // Its `failIfUnavailable` refusal is a throw, and this decision is the only\n // thing between that throw and a tool exception worded in terms of `Bash`\n // whichever tool happened to be running. It is caught here and returned as the\n // decision's own error, so `Check` and `Bash` report it the same way and the\n // message is the one the settings author has to act on.\n let sandbox: Sandbox | null;\n try {\n sandbox = ctx.runtime ? ctx.runtime.sandbox(ctx.sandbox) : createSandbox(ctx.sandbox);\n } catch (error) {\n return { sandboxed: false, error: error instanceof Error ? error.message : String(error) };\n }\n // A control path the namespace cannot protect is refused, not worked around,\n // and only once a sandbox is actually in hand: with none, the command runs\n // unsandboxed under the policy the settings author chose, which is not this\n // decision to tighten.\n let protectedPaths: ProtectedWorkspacePaths | undefined;\n if (sandbox) {\n const denyDirectories = [\n ...ctx.sandbox.filesystem.denyWrite,\n ...ctx.sandbox.filesystem.denyRead,\n ].map((p) => expandPath(p, realSandboxHost));\n\n protectedPaths = protectedWorkspacePaths(ctx.workspaceRoot, realSandboxHost, {\n allowGitWrites: ctx.sandbox.filesystem.allowGitWrites,\n denyDirectories,\n });\n const refusal = protectedPaths.refusals[0];\n if (refusal) return { sandboxed: false, error: describeControlPathRefusal(refusal) };\n }\n const exec = sandbox?.wrap(command, ctx.workspaceRoot, protectedPaths);\n if (exec) {\n return {\n sandboxed: true,\n exec,\n gitDirReadOnly: !ctx.sandbox.filesystem.allowGitWrites,\n };\n }\n if (ctx.sandbox.failIfUnavailable) {\n return { sandboxed: false, error: 'Sandbox unavailable and failIfUnavailable is set' };\n }\n return unsandboxed('unavailable');\n}\n\n/**\n * The line a sandboxed git command needs when it cannot write in the git dir.\n *\n * Which file is read-only depends on the mode. With\n * `sandbox.filesystem.allowGitWrites=false` (the default) every git directory\n * of the workspace is bound read-only (issue 373), so `git commit`, `git add`,\n * `git stash`, `git checkout` and `git fetch` fail on the index lock, on\n * `FETCH_HEAD`, on the object database — errors that say nothing about a\n * read-only bind. With it true, only the git dir's `config` and the pointer\n * files are, so `git commit` still works but `git remote add` and `git config`\n * fail with \"could not write config file\" / \"could not lock config file\".\n * Either way the error names no cause, so the model retries the same command or\n * rewrites it to route around the file instead of running it outside.\n *\n * Returned as a line the caller appends to the output that carried the error,\n * and only for a sandboxed command: unsandboxed git has no read-only git dir,\n * and an unrelated file that happens to be called `config` is not this. The\n * note is appended once, however many streams the error reached the caller in.\n *\n * `command` is what rules out `git config --global`. That writes `~/.gitconfig`,\n * which the namespace binds read-only only if the user listed it in\n * `denyWrite` — a different remedy, and pointing this note at the repository's\n * git dir for it would send the model after the wrong file.\n */\nexport function withGitConfigReadOnlyNotice(\n text: string,\n output: string,\n sandboxed: boolean,\n command = '',\n gitDirReadOnly?: boolean,\n): string {\n if (!sandboxed) return text;\n\n const isConfigFileFailure = /could not (?:write|lock) config file/.test(output);\n if (isConfigFileFailure) {\n if (writesAGlobalConfig(command)) return text;\n return gitDirReadOnly ? withReadOnlyGitDirNote(text) : withLegacyGitConfigNote(text);\n }\n\n if (gitDirReadOnly) {\n const linesWithRo = output\n .split('\\n')\n .filter((line) => /\\bRead-only file system\\b/i.test(line));\n const runsGit = /(?:^|[;&|(\\s])git\\b/.test(command);\n const hasDotGitSegment = linesWithRo.some((line) =>\n /(?:^|[/\\s'\"`])\\.git(?=[/\\\\s'\":`]|[\\r\\n]|$)/.test(line),\n );\n const roFailure = linesWithRo.length > 0 && (runsGit || hasDotGitSegment);\n\n if (roFailure) {\n return withReadOnlyGitDirNote(text);\n }\n }\n\n return text;\n}\n\nfunction withLegacyGitConfigNote(text: string): string {\n return (\n `${text}\\n` +\n 'Note: .git/config is read-only inside the sandbox, so git cannot update repository ' +\n 'configuration there. Run this command outside the sandbox.'\n );\n}\n\nfunction withReadOnlyGitDirNote(text: string): string {\n return (\n `${text}\\n` +\n \"Note: the repository's git directory is read-only inside the sandbox \" +\n '(sandbox.filesystem.allowGitWrites is off), so git cannot write to it here. Do not retry the ' +\n 'command or change sandbox settings yourself; tell the user it has to run outside the sandbox: ' +\n 'they can run it, or allow it in their own ~/.book/settings.json, where ' +\n 'sandbox.excludedCommands runs matching commands unsandboxed while ' +\n 'sandbox.allowUnsandboxedCommands is true and sandbox.filesystem.allowGitWrites lets sandboxed ' +\n 'git write again; workspace settings cannot enable either.'\n );\n}\n\n/** Whether a `git config` invocation targets the user's or the system's config. */\nfunction writesAGlobalConfig(command: string): boolean {\n return /(?:^|[;&|(\\s])git\\s+[^|;&]*\\bconfig\\b[^|;&]*\\s--(?:global|system)\\b/.test(command);\n}\n\n/**\n * Create a sandbox wrapper if bwrap is available on this platform.\n * On Windows, returns null with a warning (sandbox not supported).\n *\n * @param settings - Resolved sandbox settings\n * @returns Sandbox instance or null\n */\nexport function createSandbox(settings: ResolvedSettings['sandbox']): Sandbox | null {\n if (!settings.enabled) return null;\n\n const os = platform();\n if (os === 'win32') {\n if (settings.failIfUnavailable) {\n throw new Error(\n 'Bash sandbox is not available on Windows. Disable sandbox.enabled or set failIfUnavailable to false.',\n );\n }\n console.warn('⚠ Bash sandbox is not available on Windows. Commands will run unsandboxed.');\n return null;\n }\n\n const bwrap = detectBwrap();\n if (!bwrap) {\n if (settings.failIfUnavailable) {\n throw new Error(\n 'bubblewrap (bwrap) not found. Install it to enable bash sandboxing, or set failIfUnavailable to false.',\n );\n }\n console.warn(\n '⚠ bubblewrap (bwrap) not found — install it for bash sandboxing. Commands will run unsandboxed.',\n );\n return null;\n }\n\n // Diagnostics belong to whoever builds the sandbox, and callers are expected\n // to build it once per session — `createSandbox` runs per Bash call would\n // repeat these on every command.\n if (hasDomainPolicy(settings)) {\n console.warn(\n '⚠ sandbox.network domain rules cannot be enforced by bubblewrap, which has no per-domain filtering. Network access is disabled entirely for sandboxed commands.',\n );\n }\n const unbindable = unbindablePaths(settings);\n if (unbindable.length > 0) {\n console.warn(\n `⚠ sandbox.filesystem rules skipped — nothing exists at: ${unbindable.join(', ')}. These paths are not protected.`,\n );\n }\n\n return {\n wrap(\n command: string,\n workspaceRoot: string,\n protectedPaths?: ProtectedWorkspacePaths,\n ): CommandExecution {\n return buildSandboxExecution(\n bwrap,\n command,\n workspaceRoot,\n settings,\n realSandboxHost,\n protectedPaths,\n );\n },\n describe(): string {\n return [\n `bubblewrap (${bwrap})`,\n hasDomainPolicy(settings) ? 'network disabled' : 'host network shared',\n settings.filesystem.allowGitWrites\n ? 'git directories writable (hooks, config and pointer files read-only)'\n : 'git directories read-only',\n `${settings.filesystem.allowWrite.length} extra writable path(s)`,\n `${settings.filesystem.denyWrite.length} read-only path(s)`,\n `${settings.filesystem.denyRead.length} masked path(s)`,\n unbindable.length > 0 ? `${unbindable.length} skipped (missing)` : 'all paths resolved',\n ].join('; ');\n },\n };\n}\n","import { posix, win32 } from 'node:path';\nimport picomatch from 'picomatch';\nimport { convertPathToPattern } from 'tinyglobby';\nimport { createDebugLogger } from '../debug-log.js';\n\n/**\n * Convert a rule glob to a regex, anchored at both ends (^...$). Two characters are wildcards: `*`\n * (and `**`, the same) matches any run of characters, `/` included, and `?` matches exactly one\n * character, `/` included too (#522). Every other character, regex metacharacters included,\n * matches itself. Used by every rule kind that is a glob: permission rules (all but an exact Bash\n * allow rule, which is compared as plain text), capability rules and `sandbox.excludedCommands`.\n */\nexport function globToRegex(pattern: string): RegExp {\n const source = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*\\*|\\*|\\?/g, (wildcard) => (wildcard === '?' ? '.' : '.*'));\n return new RegExp('^' + source + '$');\n}\n\nconst log = createDebugLogger('tools:glob');\n\n/**\n * The longest pattern handed to picomatch, the matcher tinyglobby walks with.\n *\n * The limit is the one `micromatch` enforced before this walk (`Input length (40000), exceeds max\n * characters (10000)`), so a pattern that was answered then is still answered. It is not what keeps\n * the process alive, though — see {@link MAX_GLOB_GROUP_DEPTH}.\n */\nexport const MAX_GLOB_PATTERN_LENGTH = 10_000;\n\n/**\n * How deeply one pattern may nest a group, and how many it may open.\n *\n * The length limit is not the guard that matters. Two patterns well inside it take the process\n * down, measured against this walk (`glob('…', { expandDirectories: false })`):\n *\n * - `'!('.repeat(2500) + ')'.repeat(2500)` — 7500 characters, refused by nothing: picomatch\n * compiles it in 201 ms and then *matching* one path against it aborts Node with\n * `FATAL ERROR: RegExpCompiler Allocation failed`, which is below the reach of any `catch`.\n * - `'+('.repeat(3300) + ')'.repeat(3300)` — 9900 characters: picomatch's own parse takes 145 s,\n * so the call blocks the event loop for minutes. It is a cubic cost in the nesting depth — 32\n * deep compiles in 5 ms, 256 in 74 ms, 2000 in 38 s — which is what makes a depth bound the\n * bound that matters.\n *\n * So the shape of a pattern is refused as well as its length: a nesting 32 deep, or 256 groups of\n * any kind. No real pattern is near either. A brace group is no help to an attacker here —\n * picomatch folds `{a,b}` into a regex alternation and compiles `{a,b}` × 200 (400 groups) in 2 ms —\n * so the count is a backstop for the shapes above, not a brace-expansion budget.\n */\nexport const MAX_GLOB_GROUP_DEPTH = 32;\nexport const MAX_GLOB_GROUPS = 256;\n\n/**\n * Why picomatch cannot be handed this pattern, in the words the tools report, or null when it can.\n *\n * Applied to a Glob pattern, to Grep's include pattern and to every entry of a walk's `ignore`\n * list alike, because a walk compiles all of them through the same matcher. A pattern that reaches\n * a walk has to be judged in the spelling the walk compiles — see {@link globWalkPlan}: `\\` is an\n * escape to picomatch and a separator to the walk, so the two spellings of `x\\{a,b\\}` open and close\n * a different number of groups.\n */\nexport function globPatternRefusal(pattern: string): string | null {\n if (pattern.length > MAX_GLOB_PATTERN_LENGTH) {\n return `Pattern is too long: a glob pattern may be at most ${MAX_GLOB_PATTERN_LENGTH} characters (this one is ${pattern.length})`;\n }\n let depth = 0;\n let deepest = 0;\n let groups = 0;\n for (let index = 0; index < pattern.length; index++) {\n const char = pattern[index];\n // An escaped character is a literal: `\\(`, `\\{` and `\\[` open nothing.\n if (char === '\\\\') {\n index++;\n continue;\n }\n if (char === '(' || char === '{' || char === '[') {\n groups++;\n depth++;\n if (depth > deepest) deepest = depth;\n if (groups > MAX_GLOB_GROUPS) {\n return `Pattern has too many groups: a glob pattern may open at most ${MAX_GLOB_GROUPS} of them (this one opens more)`;\n }\n if (depth > MAX_GLOB_GROUP_DEPTH) {\n return `Pattern nests groups too deeply: a glob pattern may nest at most ${MAX_GLOB_GROUP_DEPTH} deep (this one nests ${deepest})`;\n }\n } else if (char === ')' || char === '}' || char === ']') {\n depth = Math.max(0, depth - 1);\n }\n }\n return null;\n}\n\n/** Whether picomatch can be handed this pattern at all. */\nexport function globPatternWithinLimit(pattern: string): boolean {\n return globPatternRefusal(pattern) === null;\n}\n\n/** Told that an entry was dropped, with the entry itself and why. */\nexport type IgnoreDropReporter = (entry: string, reason: string) => void;\n\n/**\n * The default {@link IgnoreDropReporter}: one line per dropped entry through the debug logger, which\n * prints only when `BOOK_DEBUG=1`. It names the entry, because the entry is the whole of what is\n * being reported — `.gitignore` line 12, say — and the walk it no longer obeys is on every tool that\n * reads the file, so it is logged per walk and not once per entry list.\n */\nconst logIgnoreDrop: IgnoreDropReporter = (entry, reason) => {\n log.warn(\n `glob: dropping a .gitignore entry the walk cannot obey — ${reason}: ${JSON.stringify(entry)}`,\n );\n};\n\n/**\n * The ignore list a walk can be handed: entries the matcher is able to compile, spelled the way it\n * reads them.\n *\n * A walk compiles every entry of its `ignore` option exactly as it compiles the pattern, and those\n * entries are the repository's own `.gitignore` — `loadGitignore` hands them straight to the Glob\n * tool, to Grep's include walk and to the file-mention walk. One line of ten thousand nested braces\n * takes the process down there exactly as it does in a pattern, so it is dropped here, before any\n * walk is handed a list. Dropping one widens the walk by whatever it ignored, so each one is named\n * to the {@link IgnoreDropReporter}: the `.gitignore` the matcher cannot read is walked around\n * rather than obeyed, and that is worth being able to see.\n *\n * Three more entries are dropped for the same reason — not because they cannot be compiled, but\n * because the walk reads them as something other than what git means:\n *\n * - An entry whose walk form opens with a `..` hop (`../x`, `/../x`, `a/../..`) moves the crawl root\n * out of the workspace, so the walk lists that subtree on the way, and it still ignores nothing: the\n * ignore list is matched against `cwd`-relative paths, and `../x` cannot name one. `posix.normalize`\n * collapses `/..` the same way it collapses `a/..`, so a root-anchored entry is checked in the\n * spelling the respelling below would hand the walk.\n * - `!(x)` is an extglob to picomatch, so as an ignore entry it matches everything *except* `x` — the\n * reverse of the negated literal git reads it as, and it can empty a walk (measured: a two-entry\n * workspace went from two results to none).\n * - `/` and `/.` name the repository root, which is the walk's `cwd` itself; respelling them to `.`\n * would have the walk ignore its own root and answer nothing.\n *\n * A root-anchored entry (`/build`, `/build/`, `/build/**`) is also respelled, because the walk\n * reads it as an absolute path: tinyglobby normalizes `/build` against the walk's `cwd` into\n * `../../../build`, which moves the crawl root to `/` — the walk then lists every directory from\n * the filesystem root down to the workspace, and the line still ignores nothing. Git means the\n * entry relative to the repository root, which is the directory the walk starts in, so it becomes\n * `build` plus `build/**` (measured: the crawl root stays at `cwd`, and `build` is pruned with\n * everything under it). Only the walk boundary rewrites an entry; `loadGitignore` still returns the\n * file's own lines to everything else that reads them.\n *\n * A directory-only entry (`build/`, `/build/`) means what git means — that directory and everything\n * under it, but not a *file* named `build` — and the walk cannot be told that: it strips the trailing\n * `/` off an entry before matching it, so `build/` and `build` are the same entry. It is spelled\n * `build/**` plus one more globstar, which matches only what is *inside* the directory and not the\n * name itself. The walk cannot prune with that spelling: the ignored directory is read once and its\n * entries are dropped as it goes (measured on a tree of 1600 directories: three `readdir` calls\n * instead of two). The other spelling that covers the inside, `build/**`, also matches the bare name\n * — the file git keeps.\n */\nexport function globWalkIgnore(\n patterns: readonly string[],\n report: IgnoreDropReporter = logIgnoreDrop,\n): string[] {\n const walkable: string[] = [];\n for (const pattern of patterns) {\n const refusal = globPatternRefusal(pattern);\n if (refusal) {\n report(pattern, `${refusal}, so the walk covers what it ignored`);\n continue;\n }\n if (pattern.startsWith('!(')) {\n report(\n pattern,\n 'picomatch reads `!(` as an extglob, so as an ignore entry it matches everything but what it names',\n );\n continue;\n }\n const climb = climbOutOfWalk(pattern);\n if (climb) {\n report(\n pattern,\n `${climb}, so the walk would read outside the workspace and still ignore nothing`,\n );\n continue;\n }\n walkable.push(...walkIgnoreEntry(pattern));\n }\n return walkable;\n}\n\n/**\n * Why an entry would move a walk's crawl root out of the directory it starts in, or null when it\n * would not.\n *\n * Checked on the entry as the walk would read it, which is the entry with a root-anchoring `/`\n * dropped — the repository root is the directory the walk starts in, so `/../x` is `../x` — and then\n * lexically collapsed the way `posix.normalize` collapses it in `normalizePattern`. `..foo` and\n * `..c` are directories that begin with two dots, not hops, and are kept.\n */\nfunction climbOutOfWalk(entry: string): string | null {\n const spelling = entry.replace(/^!/, '').replace(/^\\/+/, '');\n const normalized = posix.normalize(spelling).split('/');\n return normalized[0] === '..' ? 'the walk reads it as a climb out of its own root' : null;\n}\n\n/**\n * One `.gitignore` entry in the spelling a walk reads, as the entries that make it work.\n *\n * A leading `!` (a negation, other than the extglob `!(`, which is dropped before this) is kept, and\n * a glob character anywhere in the entry is left for the matcher. An entry that is not anchored to\n * the walk's root is already one entry the matcher reads where it stands; a directory-only one is the\n * exception, because the walk strips the trailing `/` off it and reads it as the bare name.\n */\nfunction walkIgnoreEntry(entry: string): string[] {\n const negated = entry.startsWith('!') && !entry.startsWith('!(');\n const bang = negated ? '!' : '';\n const body = negated ? entry.slice(1) : entry;\n const relative = body.replace(/^\\/+/, '').replace(/\\/+$/, '');\n const dirOnly = body.endsWith('/') && !relative.endsWith('/**');\n if (!body.startsWith('/')) return dirOnly ? [`${bang}${relative}/**/*`] : [entry];\n // `/` and `/.` alone name the repository root, which is the walk's `cwd` itself: nothing to ignore.\n if (!relative || relative === '.') return [];\n const anchored = `${bang}${relative}`;\n if (dirOnly) return [`${anchored}/**/*`];\n return relative.endsWith('/**') ? [anchored] : [anchored, `${anchored}/**`];\n}\n\n/**\n * The pattern a walk is handed, and whether picomatch can be handed it at all.\n */\nexport interface GlobWalkPlan {\n /** The pattern in the spelling the walk reads — the string picomatch compiles. */\n pattern: string;\n /** Why the matcher cannot be handed that pattern, in the words the tools report, or null. */\n refusal: string | null;\n /**\n * Every directory the walk of {@link pattern} can return a result from — one per alternative of a\n * leading group, all of which have to be judged — or null when the pattern is refused.\n */\n scopes: string[] | null;\n}\n\n/**\n * What to hand a walk of this pattern from this directory, and whether it can be handed at all.\n *\n * The judgment and the walk are made from one pattern, so they cannot disagree: whatever the walk is\n * handed is the string that is judged, and both read it as the walk does. A refusal is reported\n * before the walk rather than by it, so the words the caller sees name the shape of the pattern\n * instead of a permission refusal that misfiled it (see `readToolTarget`).\n */\nexport function globWalkPlan(\n pattern: string,\n cwd: string,\n platform: NodeJS.Platform = process.platform,\n): GlobWalkPlan {\n const walked = globWalkPattern(pattern, cwd, platform);\n const refusal = globPatternRefusal(walked);\n return {\n pattern: walked,\n refusal,\n scopes: refusal ? null : confinedDirectoriesOfPattern(walked, cwd, platform),\n };\n}\n\n/**\n * The path module the platform being judged for reads its paths with.\n *\n * `win32` reads `D:\\ws\\src`, `D:/ws/src` and `/ws/src` (the root of the current drive) as absolute\n * paths of one filesystem; `posix` reads all three as relative segment lists. That is not a\n * spelling difference: a Windows pattern judged with the POSIX module came back as the *relative*\n * scope `../../../tmp/ws/src`, which the permission layer resolves against the workspace and so\n * judges as a place the pattern never named.\n */\ntype PathModule = Pick<\n typeof posix,\n 'isAbsolute' | 'join' | 'normalize' | 'parse' | 'relative' | 'resolve'\n>;\n\n/** {@link PathModule} for the platform a caller passed, not the one this process runs on. */\nfunction pathsOf(platform: NodeJS.Platform): PathModule {\n return platform === 'win32' ? win32 : posix;\n}\n\n/** A path as a walk spells it: resolved by the platform, and in the one separator a pattern uses. */\nfunction walkPath(value: string): string {\n return value.replaceAll('\\\\', '/');\n}\n\n/**\n * An absolute pattern as the walk reads it from `walkCwd`: the platform's relative spelling, which\n * is the one the walk is handed and the one every scope is judged against.\n *\n * The pattern is resolved against the *root* of the walk directory first, which is what fills in\n * the drive of a Windows pattern that names none: `/ws/src/*.ts` is the root of the current drive,\n * so it is `C:\\ws\\src\\*.ts` under a workspace on `C:`. Skipping that step, `win32.relative` reads\n * the two as two filesystems and hands the target back untouched — and a rooted pattern would then\n * be judged as `ws/src` *inside* the workspace instead of at the root of the drive.\n */\nfunction relativePatternOf(platformPaths: PathModule, walkCwd: string, body: string): string {\n const absolute = platformPaths.resolve(platformPaths.parse(walkCwd).root || '/', body);\n return walkPath(platformPaths.relative(walkCwd, absolute));\n}\n\n/**\n * A pattern in the form tinyglobby can read, which on Windows is not the form a caller writes.\n *\n * tinyglobby treats `\\` as an escape character, so a Windows pattern spelled with backslashes\n * (`C:\\ws\\**\\*.ts`) parses as a single escaped token: the walk matches nothing. An absolute Glob\n * therefore found no files at all, and a relative one (`src\\*.ts`) escaped the directory it named.\n * The separators are converted first, then the leading base is re-escaped by `convertPathToPattern`\n * and spliced back onto the dynamic tail.\n *\n * Only on Windows: a backslash in a POSIX pattern is a real escape, and converting it would read\n * `\\*` as a star.\n *\n * An absolute pattern inside the walk's own directory is handed to the walk as the part of it below\n * that directory. tinyglobby makes an absolute pattern relative by comparing it against an\n * *escaped* `cwd` (`escapePath`), so a workspace whose path holds a glob character — `project (2)`,\n * `repo[x]`, anything checked out under one — reads every absolute pattern as a climb into its\n * parent and then answers nothing at all: measured, a `src/a.ts` behind `project (2)` came back\n * empty for `<workspace>/src/*.ts` and one file for `src/*.ts`. Relative to the real `cwd` there is\n * nothing to climb, so this is the same walk with the metacharacters out of the comparison.\n */\nexport function globWalkPattern(\n pattern: string,\n cwd: string,\n platform: NodeJS.Platform = process.platform,\n): string {\n const platformPaths = pathsOf(platform);\n const spelled = posixPattern(pattern, platform);\n const body = spelled.endsWith('/') ? spelled.slice(0, -1) : spelled;\n const relative = platformPaths.isAbsolute(body.replace(ESCAPING_BACKSLASHES, ''))\n ? relativePatternOf(platformPaths, walkCwdOf(cwd, platformPaths), body)\n : '';\n if (relative) return relative;\n // The base is built from the pattern's own parts, which are `/`-separated on every platform.\n const base = staticBaseOfPattern(body);\n // A base that is not a prefix of the pattern cannot be spliced onto one: `**/*` starts where it\n // stands, and there is nothing in the pattern for a base to replace.\n if (!base || !body.startsWith(base)) return body;\n return `${convertPathToPattern(base)}${body.slice(base.length)}`;\n}\n\n/**\n * A pattern with the separators tinyglobby reads, which on Windows is not the form a caller writes.\n *\n * Only on Windows: a backslash in a POSIX pattern is a real escape, and converting it would read\n * `\\*` as a star. The platform is a parameter so that the Windows spelling is judged on any platform\n * (the judgement does not have a platform-dependent code path of its own — it reads what\n * `globWalkPattern` handed the walk).\n */\nfunction posixPattern(pattern: string, platform: NodeJS.Platform): string {\n return platform === 'win32' ? pattern.replaceAll('\\\\', '/') : pattern;\n}\n\n/** The `cwd` as the walk reads it: resolved by the platform, and in one separator. */\nfunction walkCwdOf(cwd: string, platformPaths: PathModule): string {\n return walkPath(platformPaths.resolve(cwd));\n}\n\n/** The leading `..` run of a normalized pattern, however many are in it. */\nconst PARENT_DIRECTORY = /^(\\/?\\.\\.)+/;\n/** The backslashes that only escape a glob character, which do not hide an absolute path. */\nconst ESCAPING_BACKSLASHES = /\\\\(?=[()[\\]{}!*+?@|])/g;\n/** A drive letter alone, which `posix.join` would read as a path segment rather than a root. */\nconst DRIVE_RELATIVE_PATH = /^[A-Za-z]:$/;\n\nfunction ensureNonDriveRelativePath(path: string): string {\n return path.replace(DRIVE_RELATIVE_PATH, (match) => `${match}/`);\n}\n\n/**\n * A scope as a path: joined and normalized by the platform, so a caller is handed `/ws/..c/b` rather\n * than the `/ws/c/../..c/b` it was built from. Normalizing cannot lose a hop — a `join` steps a `..`\n * only when a whole segment is one, which leaves `..c` (a name the walk matches) where it was. The\n * result is absolute wherever the walk directory is, which is what a caller resolving a scope\n * against its own roots needs: a relative one would be re-anchored to somewhere the walk never was.\n */\nfunction scopePath(platformPaths: PathModule, cwd: string, ...segments: readonly string[]): string {\n return ensureNonDriveRelativePath(\n walkPath(platformPaths.normalize(platformPaths.join(cwd, ...segments))),\n );\n}\n\n/**\n * One pattern split into the parts tinyglobby walks it part by part.\n *\n * Its own `splitPattern`, which is what decides where a walk stops descending: a pattern with no\n * usable part list is walked as the single part it spells, and that part contains a `/` — which is\n * what makes a leading globstar start where it stands rather than at a named directory.\n */\nfunction splitPatternParts(pattern: string): string[] {\n const parts = picomatch.scan(pattern, { parts: true }).parts;\n return parts?.length ? parts : [pattern];\n}\n\n/** Whether a segment has to be matched rather than looked for by name. */\nfunction isDynamicPart(part: string): boolean {\n const scan = picomatch.scan(part);\n return scan.isGlob || scan.negated;\n}\n\n/** The most alternatives a leading group is broken into before the run stops naming them. */\nconst MAX_LEADING_ALTERNATIVES = 64;\n\n/**\n * The named directories a pattern's parts open with: everything but its last segment, up to the\n * first segment that has to be matched.\n *\n * This is tinyglobby's own `commonPath` for a single pattern (`{ popTrailingGlobstar: true }`), and it\n * is where a walk *searches* from, which is why a brace group in the first segment (`{a,b}` then a\n * separator) walks the workspace while `src/*.ts` walks `src`: the group is a segment that has to be\n * matched, so the named run ends before it. A pattern ending in a globstar drops that globstar and\n * the segment before it, which is why `src/**` searches the workspace and not `src`.\n */\nfunction leadingPartsOfParts(parts: readonly string[], popTrailingGlobstar: boolean): string[] {\n const leading: string[] = [];\n for (let index = 0; index < parts.length; index++) {\n const part = parts[index];\n if (popTrailingGlobstar && part === '**' && !parts[index + 1]) {\n leading.pop();\n break;\n }\n if (index === parts.length - 1 || isDynamicPart(part)) break;\n leading.push(part);\n }\n return leading;\n}\n\n/**\n * The named leading directories of a pattern, as a path: the part of it the walk never has to\n * match. Empty when the walk starts where the pattern stands (a globstar, `*.ts`).\n */\nfunction staticBaseOfPattern(pattern: string): string {\n const leadingParts = leadingPartsOfParts(splitPatternParts(pattern), false);\n return leadingParts.length ? posix.join(...leadingParts) : '';\n}\n\n/** Whether a directory is inside another, or is it. */\nfunction holds(platformPaths: PathModule, root: string, path: string): boolean {\n const relativePath = walkPath(platformPaths.relative(root, path));\n return (\n relativePath === '' ||\n (!relativePath.startsWith('../') &&\n relativePath !== '..' &&\n !platformPaths.isAbsolute(relativePath))\n );\n}\n\n/**\n * Every directory a walk of this pattern can answer from, one run per alternative of the first\n * group it names, as the walk itself computes it.\n *\n * `normalizePattern` first, then the choice `buildCrawler` makes from what is left. Getting this\n * wrong is a permission bypass rather than a wrong answer, so every step here is measured against the\n * walk (see {@link globWalkScopes} and its tests):\n *\n * - The pattern is normalized first, which is a purely *lexical* collapse of `.` and `..`. It is\n * what makes a globstar followed by two parent hops a climb even though the first segment is a\n * glob: `normalize` drops the `*` and the `..` beside it, and the walk runs as a leading\n * `../`. A brace group is not touched — the segment `..}}` is not `..` — so a `..` spelled inside\n * one keeps the walk inside the directory in front of it.\n * - An absolute pattern is made relative to the `cwd` first, so one naming a directory elsewhere\n * arrives as a leading `..` run, and an absolute path the `cwd` itself ends in arrives without\n * one: `<workspace>/src/*.ts` walks `src`, not the workspace. Both readings are the platform's\n * ({@link pathsOf}), so `C:\\ws\\src\\*.ts` is made relative to `C:\\ws` on any platform the caller\n * asks this for.\n * - That run is a climb, and it cancels only against the tail of the `cwd`: `../ws/src/*.ts` from\n * `/a/ws` is the same directory as `src/*.ts`, and both walk `/a/ws/src`.\n * - What is left of the climb that did not cancel, plus the named run of the pattern's remaining\n * segments, is a directory the walk answers from: entries are matched against the `cwd`-relative\n * path they have, and no matcher can turn the named run into something else.\n *\n * The named run of a *search* drops the segment in front of a trailing globstar — that is the\n * directory the walk reads from on its way, which it may answer from anything under. The run a\n * judgment needs does not: `src/**` answers from `src` and not from the workspace (measured, with a\n * symlinked `src`, and through the permission layer's `guarded` answer).\n *\n * Every run here is inside the directory the walk searches from, and a run that is not — a group\n * spelling a `..` hop, `{..,src}/*` — is dropped, because no entry can have it: the walk formats\n * every entry as a `cwd`-relative path of a file *inside* the tree it searched.\n */\nfunction confinedDirectoriesOfPattern(\n pattern: string,\n cwd: string,\n platform: NodeJS.Platform,\n): string[] {\n const platformPaths = pathsOf(platform);\n const walkCwd = walkCwdOf(cwd, platformPaths);\n const body = pattern.endsWith('/') ? pattern.slice(0, -1) : pattern;\n const normalized = platformPaths.isAbsolute(body.replace(ESCAPING_BACKSLASHES, ''))\n ? relativePatternOf(platformPaths, walkCwd, body)\n : walkPath(platformPaths.normalize(body));\n\n const parts = splitPatternParts(normalized);\n const parentDirectory = PARENT_DIRECTORY.exec(normalized)?.[0];\n // The count of hops the walk itself works from. It is read off the leading `..` run of the\n // *pattern*, which is where the run really is: a segment that begins with two dots and then does\n // not stop (`....`, `..c`) is a directory, not one and a half hops, and counting it as one used to\n // hand `new Array(1.6666666666666667)` to a walk — a RangeError that took the run down — and to\n // answer for `../..c/b/c/**` with a directory the walk never reads.\n const climb = leadingClimbOfParts(parts);\n const cwdParts = walkCwd.split('/');\n // The count of hops the walk itself works from, read off the leading `..` run of the *pattern*,\n // which is where the run really is: a segment that begins with two dots and then does not stop\n // (`....`, `..c`, `..foo`) is a name the walk climbs one hop toward and then matches, not one and\n // a half hops to step over. It is the walk's own count, so it is fractional for those — and it is\n // this number, read as a whole, that used to hand `new Array(1.6666666666666667)` to a hop list\n // and take the run down with a RangeError.\n const walkedClimb = ((parentDirectory?.length ?? 0) + 1) / 3;\n let cancelled = 0;\n while (\n Number.isInteger(walkedClimb) &&\n cancelled < walkedClimb &&\n parts[cancelled + walkedClimb] !== undefined &&\n parts[cancelled + walkedClimb] === cwdParts[cwdParts.length + cancelled - walkedClimb]\n ) {\n cancelled++;\n }\n // Where the walk reads from, which holds every entry it can return. It is one of two directories,\n // and which one is the walk's own choice: the directory the climb lands on (which it also makes\n // its search directory when the climb leaves the `cwd`), or the named run of the pattern as the\n // caller wrote it, which is where a walk starts once it has cancelled its way back in.\n // The climb as the walk spells it *after* it has cancelled: the hops that cancelled against the\n // tail of the `cwd` are gone from it, which is why `../ws/src/*.ts` does not climb at all.\n const potentialRoot = scopePath(\n platformPaths,\n walkCwd,\n parentDirectory?.slice(cancelled * 3) ?? '',\n );\n const climbedOutOfCwd = potentialRoot[0] !== '.' && walkCwd.length > potentialRoot.length;\n const searchesFrom = climbedOutOfCwd\n ? potentialRoot\n : scopePath(platformPaths, walkCwd, ...leadingPartsOfParts(parts, true));\n\n if (!parentDirectory) {\n // No climb: the directory the walk searches from is the directory it answers from.\n return confinedRunsWithin(platformPaths, searchesFrom, leadingRunsOfParts(parts), walkCwd);\n }\n if (cancelled > 0) {\n // The walk rewrites the pattern as it cancels a hop against the tail of the `cwd`, so what it\n // filters with no longer spells what the caller wrote — `../..c/b/c/**` from a `cwd` ending in\n // `b/c` becomes `**`, matched from `<cwd>/../..c/b` — and a matcher that has lost its named run\n // can match anything under the directory the walk reads. Nothing narrower than that directory\n // can be said to hold every entry.\n return [searchesFrom];\n }\n // The climb is still in the pattern, so its hops are in front of the run and the entries sit under\n // the pattern's named segments measured from the directory it lands on.\n if (!Number.isInteger(walkedClimb)) {\n // A run that is only partly a hop names the directory after the hop (`..foo/*` reads the parent\n // and matches `..foo` in it), so nothing inside that directory is named and the directory the\n // walk reads is all there is. Counting it as a hop rather than as a name is the walk's reading,\n // and it is the reason this is not answered from the workspace: the walk lists the parent.\n return [searchesFrom];\n }\n const hops = new Array<string>(walkedClimb).fill('..');\n return confinedRunsWithin(\n platformPaths,\n searchesFrom,\n leadingRunsOfParts(parts.slice(climb)).map((run) => [...hops, ...run]),\n walkCwd,\n );\n}\n\n/** The leading `..` run of a split pattern: one hop per segment that is exactly `..`. */\nfunction leadingClimbOfParts(parts: readonly string[]): number {\n let climb = 0;\n while (parts[climb] === '..') climb++;\n return climb;\n}\n\n/**\n * The named directories a pattern's parts open with, one run per alternative of the first group it\n * names: `['src']` for a pattern under `src`, `['link']` and `['src']` for `{link,src}` followed by\n * a globstar, `[]` for a leading globstar. The last segment is never part of a run: `src/notes.txt`\n * answers from `src`.\n *\n * The alternatives are what a group *in the first segment* means to a permission judgment: the group\n * is matched, so the walk answers from wherever the alternative names, and both have to be judged.\n * They are enumerated with the walk's own brace expansion and dropped past\n * {@link MAX_LEADING_ALTERNATIVES} — an unenumerated group names nothing, so the run stops in front\n * of it, which is the wider and so the safer answer.\n */\nfunction leadingRunsOfParts(parts: readonly string[]): string[][] {\n let runs: string[][] = [[]];\n for (let index = 0; index < parts.length - 1; index++) {\n const part = parts[index];\n const alternatives = alternativesOfGroup(part);\n if (alternatives) {\n if (runs.length * alternatives.length > MAX_LEADING_ALTERNATIVES) break;\n runs = runs.flatMap((run) => alternatives.map((alternative) => [...run, alternative]));\n continue;\n }\n // A segment that has to be matched rather than looked for by name ends the run: the walk never\n // starts inside it, so nothing in it can be named as a directory the results are held in.\n if (isDynamicPart(part)) break;\n for (const run of runs) run.push(part);\n }\n return runs;\n}\n\n/**\n * The alternatives of a segment spelled as one plain brace group, or null when it is not one.\n *\n * A group that holds a range (`{1..12}`) or a nested group is not enumerated: either way the\n * alternative count is not bounded by what is written, and the caller's answer is then the run in\n * front of the group.\n */\nfunction alternativesOfGroup(part: string): string[] | null {\n if (part.length < 5 || !part.startsWith('{') || !part.endsWith('}')) return null;\n const body = part.slice(1, -1);\n if (!body.includes(',') || /\\.\\./.test(body)) return null;\n const alternatives: string[] = [];\n let nested = 0;\n let current = '';\n for (const char of body) {\n if (char === '{' || char === '(' || char === '[') nested++;\n else if (char === '}' || char === ')' || char === ']') nested--;\n if (char === ',' && nested === 0) {\n alternatives.push(current);\n current = '';\n continue;\n }\n current += char;\n }\n alternatives.push(current);\n // An alternative the walk has to match names no directory, so a group of nothing but those is a\n // group to stop in front of.\n const named = alternatives.filter(\n (alternative) => !alternative.includes('/') && !isDynamicPart(alternative),\n );\n return named.length ? named : null;\n}\n\n/** The runs as directories, keeping the ones the walk's own root holds, or that root when none is. */\nfunction confinedRunsWithin(\n platformPaths: PathModule,\n root: string,\n runs: readonly string[][],\n walkCwd: string,\n): string[] {\n const held = [\n ...new Set(\n runs\n .map((run) => scopePath(platformPaths, walkCwd, ...run))\n .filter((directory) => holds(platformPaths, root, directory)),\n ),\n ];\n return held.length ? held : [root];\n}\n\n/**\n * Every directory a walk of this pattern can answer from — or null when picomatch cannot be handed\n * the pattern at all.\n *\n * The walk's own answer to that question (see {@link confinedDirectoriesOfPattern}), so a caller\n * judging what a Glob reaches is judging the directory the walk really uses rather than how the\n * pattern reads. Every entry it returns is inside one of these, which is what a permission judgment\n * needs: it judges each, and the worst one decides. They are absolute — in the platform's own\n * reading of an absolute path, spelled the way a walk spells it — so a caller can resolve them\n * against the roots it serves rather than re-anchoring a `..` to a root the pattern never named.\n *\n * `platform` is the platform whose paths are being judged, which is not necessarily the one this\n * process runs on: it is what decides how an absolute pattern is recognized and a relative one\n * resolved (`pathsOf`), so the Windows reading of a Windows path is measurable on any host.\n *\n * Where the walk *searches* from is one of them or an ancestor of them, so a pattern that climbs\n * makes tinyglobby start from the directory the climb lands on and list that directory's subtree on\n * the way to the one it answers from; only what the pattern matches comes back (measured; the\n * `globWalkScopes` tests walk both).\n *\n * Assumes the options the Glob tool walks with: `expandDirectories: false` (a pattern naming a\n * directory is not walked as a subtree) and a single pattern. Both only ever narrow the walk to a\n * subtree of what is reported here.\n */\nexport function globWalkScopes(\n pattern: string,\n cwd: string,\n platform: NodeJS.Platform = process.platform,\n): string[] | null {\n return globWalkPlan(pattern, cwd, platform).scopes;\n}\n"],"mappings":"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","names":["relative","platform","target"]}
@@ -1,3 +1,6 @@
1
+ import {
2
+ reachesUncPath
3
+ } from "./chunk-EEX6QNXV.js";
1
4
  import {
2
5
  resolveBookHome
3
6
  } from "./chunk-ZPSZZUWF.js";
@@ -23,6 +26,22 @@ var TOOL_ALIASES = {
23
26
  function canonicalToolName(name) {
24
27
  return TOOL_ALIASES[name] ?? name;
25
28
  }
29
+ var PROVIDER_TOOL_PREFIXES = [
30
+ "parent:",
31
+ "default:",
32
+ "tool:",
33
+ "parent.",
34
+ "default.",
35
+ "tool.",
36
+ "functions."
37
+ ];
38
+ function unwrapProviderPrefix(name) {
39
+ const prefix = PROVIDER_TOOL_PREFIXES.find((candidate) => name.startsWith(candidate));
40
+ return prefix ? name.slice(prefix.length) : name;
41
+ }
42
+ function displayToolName(name) {
43
+ return canonicalToolName(unwrapProviderPrefix(name));
44
+ }
26
45
 
27
46
  // src/tools/primary-arg.ts
28
47
  function getPrimaryArg(args) {
@@ -74,7 +93,9 @@ function isFileMutatingTool(canonicalName) {
74
93
  // src/tools/result.ts
75
94
  var TOOL_RESULT_MAX_BYTES = 50 * 1024;
76
95
  var READ_EMPTY_FILE_NOTICE = "[Empty file: 0 lines.]";
77
- var TOOL_OUTPUT_DIRECTORY = join(resolveBookHome(), "tool-output");
96
+ function toolOutputDirectory() {
97
+ return join(resolveBookHome(), "tool-output");
98
+ }
78
99
  function presentationFor(content, override) {
79
100
  return {
80
101
  kind: override?.kind ?? "text",
@@ -203,7 +224,7 @@ function clippedOutputPreview(content, outputPath, maxBytes) {
203
224
  const previewBudget = Math.max(0, maxBytes - Buffer.byteLength(notice));
204
225
  return `${utf8Prefix(content, previewBudget).trimEnd()}${notice}`;
205
226
  }
206
- async function boundToolResultOutput(input, _workspaceRoot, maxBytes = TOOL_RESULT_MAX_BYTES, artifactRoot = TOOL_OUTPUT_DIRECTORY) {
227
+ async function boundToolResultOutput(input, _workspaceRoot, maxBytes = TOOL_RESULT_MAX_BYTES, artifactRoot) {
207
228
  const result = normalizeToolResult(input);
208
229
  const modelContent = rawToolResultModelContent(result);
209
230
  const modelContentBytes = Buffer.byteLength(modelContent);
@@ -213,7 +234,7 @@ async function boundToolResultOutput(input, _workspaceRoot, maxBytes = TOOL_RESU
213
234
  if (modelContentBytes <= maxBytes && detailsBytes <= maxBytes) return result;
214
235
  let outputPath = result.artifacts?.outputPath;
215
236
  if (!outputPath) {
216
- const outputFile = join(artifactRoot, `${crypto.randomUUID()}.txt`);
237
+ const outputFile = join(artifactRoot ?? toolOutputDirectory(), `${crypto.randomUUID()}.txt`);
217
238
  try {
218
239
  await mkdir(dirname(outputFile), { recursive: true });
219
240
  await writeFile(
@@ -416,9 +437,195 @@ function enrichToolResultPresentation(input, toolName, args) {
416
437
  };
417
438
  }
418
439
 
440
+ // src/tools/path-utils.ts
441
+ import { existsSync, lstatSync, realpathSync } from "fs";
442
+ import { basename, dirname as dirname2, isAbsolute, join as join2, relative, resolve, sep } from "path";
443
+ var REAL_PATH_COMPARISON = { relative, isAbsolute, sep };
444
+ function isOutside(root, candidate, path = REAL_PATH_COMPARISON) {
445
+ const rel = path.relative(root, candidate);
446
+ return rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel);
447
+ }
448
+ function nearestExistingPath(inputPath) {
449
+ let candidate = inputPath;
450
+ while (!existsSync(candidate)) {
451
+ const parent = dirname2(candidate);
452
+ if (parent === candidate) return null;
453
+ candidate = parent;
454
+ }
455
+ return candidate;
456
+ }
457
+ function resolveWorkspacePath(workspaceRoot, inputPath) {
458
+ const lexicalRoot = resolve(workspaceRoot);
459
+ const filePath = resolve(isAbsolute(inputPath) ? inputPath : resolve(lexicalRoot, inputPath));
460
+ const canonicalRoot = canonicalizePath(lexicalRoot);
461
+ const canonicalPath = canonicalizePath(filePath);
462
+ if (isOutside(canonicalRoot, canonicalPath)) return null;
463
+ const asWrittenInside = !isOutside(lexicalRoot, filePath);
464
+ const rel = asWrittenInside ? relative(lexicalRoot, filePath) : relative(canonicalRoot, canonicalPath);
465
+ return { filePath, canonicalPath, relativePath: rel.replace(/\\/g, "/") };
466
+ }
467
+ function resolveReadablePathDetail(context, inputPath) {
468
+ const wsMatch = resolveWorkspacePath(context.workspaceRoot, inputPath);
469
+ if (wsMatch) {
470
+ return { path: { ...wsMatch, root: context.workspaceRoot, inWorkspace: true } };
471
+ }
472
+ if (!isAbsolute(inputPath)) return { reason: "unreachable" };
473
+ for (const root of [...context.readOnlyRoots ?? [], ...context.additionalRoots ?? []]) {
474
+ const match = resolveWorkspacePath(root, inputPath);
475
+ if (match) return { path: { ...match, root, inWorkspace: false } };
476
+ }
477
+ const memory = context.memorySearchRoot ? resolveMemorySearchPath(context.memorySearchRoot, inputPath) : null;
478
+ return memory ? { path: memory } : { reason: "unreachable" };
479
+ }
480
+ function resolveMemorySearchPath(memoryRoot, inputPath) {
481
+ const match = resolveWorkspacePath(memoryRoot, inputPath);
482
+ if (!match) return null;
483
+ const fromRoot = relative(canonicalizePath(resolve(memoryRoot)), match.canonicalPath);
484
+ const segments = [...match.relativePath.split("/"), ...fromRoot.split(sep)];
485
+ if (segments.some((segment) => segment.startsWith(".") && segment !== "." && segment !== "..")) {
486
+ return null;
487
+ }
488
+ return { ...match, root: memoryRoot, inWorkspace: false };
489
+ }
490
+ function resolveMentionReadablePath(roots, absolutePath) {
491
+ if (reachesUncPath(absolutePath)) return { reason: "unc" };
492
+ return resolveReadablePathDetail(
493
+ { workspaceRoot: roots.workspaceRoot, additionalRoots: roots.additionalRoots },
494
+ absolutePath
495
+ );
496
+ }
497
+ function resolveReadablePath(context, inputPath) {
498
+ const detail = resolveReadablePathDetail(context, inputPath);
499
+ return "path" in detail ? detail.path : null;
500
+ }
501
+ function resolveMutationPath(context, inputPath) {
502
+ const memory = context.memoryWrite ? resolveMemoryWritePath(context.workspaceRoot, context.memoryWrite.root, inputPath) : null;
503
+ if (memory) return memory;
504
+ const inWorkspace = resolveWorkspacePath(context.workspaceRoot, inputPath);
505
+ if (inWorkspace) return { ...inWorkspace, root: context.workspaceRoot, inWorkspace: true };
506
+ if (!isAbsolute(inputPath)) return null;
507
+ for (const root of context.additionalRoots ?? []) {
508
+ const match = resolveWorkspacePath(root, inputPath);
509
+ if (!match) continue;
510
+ const rel = relative(resolve(context.workspaceRoot), match.filePath);
511
+ return { ...match, relativePath: rel.replace(/\\/g, "/"), root, inWorkspace: false };
512
+ }
513
+ return null;
514
+ }
515
+ function hasLinkBelow(root, filePath) {
516
+ let current = root;
517
+ for (const segment of relative(root, filePath).split(sep)) {
518
+ current = join2(current, segment);
519
+ try {
520
+ if (lstatSync(current).isSymbolicLink()) return true;
521
+ } catch (error) {
522
+ const code = error.code;
523
+ if (code === "ENOENT" || code === "ENOTDIR") return false;
524
+ return true;
525
+ }
526
+ }
527
+ return false;
528
+ }
529
+ function resolveMemoryWritePath(workspaceRoot, memoryRoot, inputPath) {
530
+ if (!isAbsolute(inputPath)) return null;
531
+ const filePath = resolve(inputPath);
532
+ if (!basename(filePath).toLowerCase().endsWith(".md")) return null;
533
+ const lexicalRoot = resolve(memoryRoot);
534
+ const canonicalRoot = canonicalizePath(lexicalRoot);
535
+ const canonicalPath = canonicalizePath(filePath);
536
+ if (canonicalPath === canonicalRoot || isOutside(canonicalRoot, canonicalPath)) return null;
537
+ const writtenRoot = [lexicalRoot, canonicalRoot].find((root) => !isOutside(root, filePath));
538
+ if (!writtenRoot) return null;
539
+ const below = relative(writtenRoot, filePath).split(sep);
540
+ if (below.some((segment) => segment.startsWith("."))) return null;
541
+ if (relative(canonicalRoot, canonicalPath).split(sep).some((s) => s.startsWith(".")))
542
+ return null;
543
+ if (hasLinkBelow(writtenRoot, filePath)) return null;
544
+ const rel = relative(resolve(workspaceRoot), filePath);
545
+ return {
546
+ filePath,
547
+ canonicalPath,
548
+ relativePath: rel.replace(/\\/g, "/"),
549
+ root: memoryRoot,
550
+ inWorkspace: false,
551
+ memory: true
552
+ };
553
+ }
554
+ function mutationDisplayPath(resolved) {
555
+ if (!resolved.memory) return resolved.relativePath;
556
+ return memoryDisplayName(resolved.root, resolved.canonicalPath);
557
+ }
558
+ function memoryDisplayName(memoryRoot, canonicalPath) {
559
+ const below = relative(canonicalizePath(resolve(memoryRoot)), canonicalPath);
560
+ return `memory/${below.split(sep).join("/")}`;
561
+ }
562
+ function memoryReadDisplayPath(roots, resolved) {
563
+ const memoryRoot = roots.memorySearchRoot ?? roots.memoryWrite?.root;
564
+ if (!memoryRoot) return void 0;
565
+ const canonicalRoot = canonicalizePath(resolve(memoryRoot));
566
+ if (resolved.canonicalPath === canonicalRoot || isOutside(canonicalRoot, resolved.canonicalPath)) {
567
+ return void 0;
568
+ }
569
+ return memoryDisplayName(memoryRoot, resolved.canonicalPath);
570
+ }
571
+ function messagePathFor(resolved) {
572
+ return resolved.memory ? resolved.filePath : resolved.relativePath;
573
+ }
574
+ function pathOutsideWorkspaceResult(inputPath) {
575
+ return toolFailure(`Path outside workspace and additionalDirectories: ${inputPath}`, {
576
+ code: "path_outside_workspace",
577
+ remediation: "Add its directory to additionalDirectories, or start Book in a directory that contains it. No permission rule and no permission mode can change which roots a read may cross."
578
+ });
579
+ }
580
+ function realWorkspaceRoot(root) {
581
+ try {
582
+ return realpathSync.native(root);
583
+ } catch {
584
+ return resolve(root);
585
+ }
586
+ }
587
+ function canonicalizePath(inputPath) {
588
+ const absolute = resolve(inputPath);
589
+ const existing = nearestExistingPath(absolute);
590
+ if (!existing) return absolute;
591
+ try {
592
+ return resolve(realpathSync.native(existing), relative(existing, absolute));
593
+ } catch {
594
+ return absolute;
595
+ }
596
+ }
597
+ function isUnderRoot(canonicalPath, root) {
598
+ return !isOutside(canonicalizePath(root), canonicalPath);
599
+ }
600
+ function comparablePath(value, platform = process.platform) {
601
+ if (platform !== "win32") return value;
602
+ return value.replaceAll("\\", "/").toLowerCase();
603
+ }
604
+ function hasBookLocalSettingsShape(canonicalPath, options) {
605
+ const parts = canonicalPath.toLowerCase().split(/[\\/]/);
606
+ if (parts[parts.length - 1] === ".book") return options.directory === true;
607
+ return parts[parts.length - 2] === ".book" && parts[parts.length - 1] === "settings.local.json";
608
+ }
609
+ function isBookLocalSettingsPath(path, root, options = {}) {
610
+ if (!hasBookLocalSettingsShape(path, options)) return false;
611
+ const target = canonicalizePath(path).toLowerCase();
612
+ const bookDir = join2(canonicalizePath(root), ".book").toLowerCase();
613
+ return target === join2(bookDir, "settings.local.json") || options.directory === true && target === bookDir;
614
+ }
615
+ function isBookLocalSettingsUnderRoots(path, roots, options = {}) {
616
+ if (!hasBookLocalSettingsShape(path, options)) return false;
617
+ return [
618
+ roots.workspaceRoot,
619
+ ...roots.readOnlyRoots ?? [],
620
+ ...roots.additionalRoots ?? []
621
+ ].some((root) => isBookLocalSettingsPath(path, root, options));
622
+ }
623
+
419
624
  export {
420
625
  TOOL_ALIASES,
421
626
  canonicalToolName,
627
+ unwrapProviderPrefix,
628
+ displayToolName,
422
629
  getPrimaryArg,
423
630
  isFileMutatingTool,
424
631
  TOOL_RESULT_MAX_BYTES,
@@ -435,6 +642,22 @@ export {
435
642
  readLineMetadata,
436
643
  readResultMetadata,
437
644
  grepResultMetadata,
438
- enrichToolResultPresentation
645
+ enrichToolResultPresentation,
646
+ isOutside,
647
+ resolveWorkspacePath,
648
+ resolveReadablePathDetail,
649
+ resolveMentionReadablePath,
650
+ resolveReadablePath,
651
+ resolveMutationPath,
652
+ mutationDisplayPath,
653
+ memoryReadDisplayPath,
654
+ messagePathFor,
655
+ pathOutsideWorkspaceResult,
656
+ realWorkspaceRoot,
657
+ canonicalizePath,
658
+ isUnderRoot,
659
+ comparablePath,
660
+ isBookLocalSettingsPath,
661
+ isBookLocalSettingsUnderRoots
439
662
  };
440
- //# sourceMappingURL=chunk-224FJZLH.js.map
663
+ //# sourceMappingURL=chunk-YIL6HTJX.js.map