@letrquan/book 0.2.0 → 0.3.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 (157) hide show
  1. package/CHANGELOG.md +2747 -40
  2. package/README.md +132 -1311
  3. package/dist/additional-roots-H7UNAEHS.js +31 -0
  4. package/dist/{app-MOXXWEOY.js → app-GE4OHJOC.js} +5740 -4045
  5. package/dist/app-GE4OHJOC.js.map +1 -0
  6. package/dist/{chunk-5GDZ22YP.js → chunk-224FJZLH.js} +99 -108
  7. package/dist/chunk-224FJZLH.js.map +1 -0
  8. package/dist/chunk-2AAOF5B5.js +28 -0
  9. package/dist/chunk-2AAOF5B5.js.map +1 -0
  10. package/dist/{chunk-CAGOB2N4.js → chunk-4AS7LKKQ.js} +6 -4
  11. package/dist/chunk-4AS7LKKQ.js.map +1 -0
  12. package/dist/chunk-525CAZPY.js +120 -0
  13. package/dist/chunk-525CAZPY.js.map +1 -0
  14. package/dist/{chunk-URLQAXTR.js → chunk-56SIEMDX.js} +2 -2
  15. package/dist/chunk-56SIEMDX.js.map +1 -0
  16. package/dist/{chunk-XIFFTATU.js → chunk-5YHTKWJN.js} +5420 -4031
  17. package/dist/chunk-5YHTKWJN.js.map +1 -0
  18. package/dist/chunk-62QL5QUP.js +283 -0
  19. package/dist/chunk-62QL5QUP.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-67UZEWWF.js} +3 -3
  21. package/dist/chunk-67UZEWWF.js.map +1 -0
  22. package/dist/chunk-6OQQOMZR.js +785 -0
  23. package/dist/chunk-6OQQOMZR.js.map +1 -0
  24. package/dist/{chunk-GTVB4HEM.js → chunk-AQEQX4FF.js} +344 -315
  25. package/dist/chunk-AQEQX4FF.js.map +1 -0
  26. package/dist/chunk-C7ADUNPR.js +273 -0
  27. package/dist/chunk-C7ADUNPR.js.map +1 -0
  28. package/dist/{chunk-E75IPST5.js → chunk-EFJ2NH6E.js} +12 -58
  29. package/dist/chunk-EFJ2NH6E.js.map +1 -0
  30. package/dist/{chunk-5KLVH3PY.js → chunk-HQI7NYGX.js} +5 -3
  31. package/dist/chunk-HQI7NYGX.js.map +1 -0
  32. package/dist/chunk-IME7R2UI.js +6541 -0
  33. package/dist/chunk-IME7R2UI.js.map +1 -0
  34. package/dist/chunk-J4F2QLM2.js +273 -0
  35. package/dist/chunk-J4F2QLM2.js.map +1 -0
  36. package/dist/{chunk-MAQXCUR4.js → chunk-J62VZYOB.js} +444 -112
  37. package/dist/chunk-J62VZYOB.js.map +1 -0
  38. package/dist/chunk-JTPOI3KU.js +826 -0
  39. package/dist/chunk-JTPOI3KU.js.map +1 -0
  40. package/dist/{chunk-WKBO5R4O.js → chunk-KBQS35OV.js} +2 -2
  41. package/dist/chunk-KBQS35OV.js.map +1 -0
  42. package/dist/chunk-KXAYUIKP.js +90 -0
  43. package/dist/chunk-KXAYUIKP.js.map +1 -0
  44. package/dist/{chunk-VZQXX3WW.js → chunk-QYBD22H5.js} +1 -1
  45. package/dist/chunk-QYBD22H5.js.map +1 -0
  46. package/dist/{chunk-V4BOYR52.js → chunk-RFJ6Q6LE.js} +763 -198
  47. package/dist/chunk-RFJ6Q6LE.js.map +1 -0
  48. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  49. package/dist/chunk-RT5SATF7.js.map +1 -0
  50. package/dist/{chunk-C7D4NJ2D.js → chunk-S4JMID4G.js} +68 -26
  51. package/dist/chunk-S4JMID4G.js.map +1 -0
  52. package/dist/{chunk-ANSJPYGI.js → chunk-SOW4WRQP.js} +3 -3
  53. package/dist/chunk-SOW4WRQP.js.map +1 -0
  54. package/dist/{chunk-J7MIZ5CA.js → chunk-TLX3H2CL.js} +518 -79
  55. package/dist/chunk-TLX3H2CL.js.map +1 -0
  56. package/dist/{chunk-DGMQEI6Y.js → chunk-VPNLCMVR.js} +22 -9
  57. package/dist/chunk-VPNLCMVR.js.map +1 -0
  58. package/dist/{chunk-BTZZUCLI.js → chunk-W3XFHEF4.js} +173 -29
  59. package/dist/chunk-W3XFHEF4.js.map +1 -0
  60. package/dist/{chunk-574C6MQR.js → chunk-X4NJT436.js} +130 -60
  61. package/dist/chunk-X4NJT436.js.map +1 -0
  62. package/dist/chunk-X66I2NQO.js +137 -0
  63. package/dist/chunk-X66I2NQO.js.map +1 -0
  64. package/dist/chunk-XHAO3K7I.js +18 -0
  65. package/dist/chunk-XHAO3K7I.js.map +1 -0
  66. package/dist/{chunk-3U5IBM24.js → chunk-Y2VHN7TC.js} +2 -2
  67. package/dist/chunk-Y2VHN7TC.js.map +1 -0
  68. package/dist/chunk-YC6W4EJH.js +947 -0
  69. package/dist/chunk-YC6W4EJH.js.map +1 -0
  70. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  71. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  72. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-VBIU6BLM.js} +6 -5
  73. package/dist/context-CBOBDY5Q.js +38 -0
  74. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-ZVT4V7IM.js} +5 -5
  75. package/dist/index.js +996 -155
  76. package/dist/index.js.map +1 -1
  77. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-T4I4SEHU.js} +13 -9
  78. package/dist/interactive-assets-T4I4SEHU.js.map +1 -0
  79. package/dist/job-runner.js +159 -111
  80. package/dist/job-runner.js.map +1 -1
  81. package/dist/job-supervisor.d.ts +2 -0
  82. package/dist/job-supervisor.js +112 -0
  83. package/dist/job-supervisor.js.map +1 -0
  84. package/dist/loader-S77OVTHH.js +18 -0
  85. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-HJ2VDYMC.js} +5 -5
  86. package/dist/{mcp-config-YIJIES2C.js → mcp-config-RNB5LEVE.js} +3 -3
  87. package/dist/memory-store-DZUT5HUH.js +75 -0
  88. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-MTSQ5JDJ.js} +8 -4
  89. package/dist/openai-compatible-2OB54YST.js +14 -0
  90. package/dist/{permission-approvals-76PVREFD.js → permission-approvals-YLV6UC5W.js} +5 -5
  91. package/dist/permissions-DHOKF3W2.js +45 -0
  92. package/dist/sandbox-WRYTZH3D.js +41 -0
  93. package/dist/{scrollback-LUKLIBVI.js → scrollback-35ME2GFX.js} +32 -22
  94. package/dist/scrollback-35ME2GFX.js.map +1 -0
  95. package/dist/sdk.d.ts +2219 -2361
  96. package/dist/sdk.js +53 -38
  97. package/dist/sdk.js.map +1 -1
  98. package/dist/settings-loader-JUNPGAAM.js +35 -0
  99. package/dist/settings-loader-JUNPGAAM.js.map +1 -0
  100. package/dist/{shell-selection-V6QAYXPX.js → shell-selection-CDHGJ4GC.js} +2 -2
  101. package/dist/shell-selection-CDHGJ4GC.js.map +1 -0
  102. package/package.json +23 -22
  103. package/dist/app-MOXXWEOY.js.map +0 -1
  104. package/dist/chunk-24AXE6SP.js.map +0 -1
  105. package/dist/chunk-3U5IBM24.js.map +0 -1
  106. package/dist/chunk-574C6MQR.js.map +0 -1
  107. package/dist/chunk-5GDZ22YP.js.map +0 -1
  108. package/dist/chunk-5KLVH3PY.js.map +0 -1
  109. package/dist/chunk-7N4J557C.js.map +0 -1
  110. package/dist/chunk-A7JLBK2Y.js +0 -3506
  111. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  112. package/dist/chunk-ANSJPYGI.js.map +0 -1
  113. package/dist/chunk-BTZZUCLI.js.map +0 -1
  114. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  115. package/dist/chunk-CAGOB2N4.js.map +0 -1
  116. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  117. package/dist/chunk-E75IPST5.js.map +0 -1
  118. package/dist/chunk-EYGD6X2T.js +0 -180
  119. package/dist/chunk-EYGD6X2T.js.map +0 -1
  120. package/dist/chunk-GTVB4HEM.js.map +0 -1
  121. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  122. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  123. package/dist/chunk-L6LK2QIQ.js +0 -657
  124. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  125. package/dist/chunk-MAQXCUR4.js.map +0 -1
  126. package/dist/chunk-S4XL7HOM.js +0 -402
  127. package/dist/chunk-S4XL7HOM.js.map +0 -1
  128. package/dist/chunk-URLQAXTR.js.map +0 -1
  129. package/dist/chunk-V4BOYR52.js.map +0 -1
  130. package/dist/chunk-VZQXX3WW.js.map +0 -1
  131. package/dist/chunk-WKBO5R4O.js.map +0 -1
  132. package/dist/chunk-XIFFTATU.js.map +0 -1
  133. package/dist/context-ZC5IFEAX.js +0 -25
  134. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  135. package/dist/loader-4IDS2RIE.js +0 -15
  136. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  137. package/dist/permissions-CHCKTFQQ.js +0 -28
  138. package/dist/sandbox-CNVAQDA7.js +0 -23
  139. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  140. package/dist/settings-loader-M2QP5TX2.js +0 -21
  141. package/patches/ink+6.8.0.patch +0 -13
  142. package/scripts/apply-ink-patch.mjs +0 -29
  143. package/scripts/ink-patch.mjs +0 -59
  144. package/scripts/verify-ink-patch.mjs +0 -11
  145. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-H7UNAEHS.js.map} +0 -0
  146. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-VBIU6BLM.js.map} +0 -0
  147. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-CBOBDY5Q.js.map} +0 -0
  148. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-ZVT4V7IM.js.map} +0 -0
  149. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-S77OVTHH.js.map} +0 -0
  150. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-HJ2VDYMC.js.map} +0 -0
  151. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-RNB5LEVE.js.map} +0 -0
  152. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-DZUT5HUH.js.map} +0 -0
  153. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-MTSQ5JDJ.js.map} +0 -0
  154. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-2OB54YST.js.map} +0 -0
  155. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-YLV6UC5W.js.map} +0 -0
  156. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-DHOKF3W2.js.map} +0 -0
  157. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-WRYTZH3D.js.map} +0 -0
@@ -0,0 +1,826 @@
1
+ import {
2
+ isOutside,
3
+ resolveWorkspacePath
4
+ } from "./chunk-X66I2NQO.js";
5
+ import {
6
+ createDebugLogger
7
+ } from "./chunk-62QL5QUP.js";
8
+
9
+ // src/sandbox.ts
10
+ import {
11
+ accessSync,
12
+ closeSync,
13
+ constants,
14
+ existsSync,
15
+ lstatSync,
16
+ openSync,
17
+ readdirSync,
18
+ readlinkSync,
19
+ readSync,
20
+ realpathSync,
21
+ statSync
22
+ } from "fs";
23
+ import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "path";
24
+ import { homedir, platform } from "os";
25
+
26
+ // src/tools/glob-regex.ts
27
+ import { posix, win32 } from "path";
28
+ import picomatch from "picomatch";
29
+ import { convertPathToPattern } from "tinyglobby";
30
+ function globToRegex(pattern) {
31
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*|\*/g, ".*");
32
+ return new RegExp("^" + escaped + "$");
33
+ }
34
+ var log = createDebugLogger("tools:glob");
35
+ var MAX_GLOB_PATTERN_LENGTH = 1e4;
36
+ var MAX_GLOB_GROUP_DEPTH = 32;
37
+ var MAX_GLOB_GROUPS = 256;
38
+ function globPatternRefusal(pattern) {
39
+ if (pattern.length > MAX_GLOB_PATTERN_LENGTH) {
40
+ return `Pattern is too long: a glob pattern may be at most ${MAX_GLOB_PATTERN_LENGTH} characters (this one is ${pattern.length})`;
41
+ }
42
+ let depth = 0;
43
+ let deepest = 0;
44
+ let groups = 0;
45
+ for (let index = 0; index < pattern.length; index++) {
46
+ const char = pattern[index];
47
+ if (char === "\\") {
48
+ index++;
49
+ continue;
50
+ }
51
+ if (char === "(" || char === "{" || char === "[") {
52
+ groups++;
53
+ depth++;
54
+ if (depth > deepest) deepest = depth;
55
+ if (groups > MAX_GLOB_GROUPS) {
56
+ return `Pattern has too many groups: a glob pattern may open at most ${MAX_GLOB_GROUPS} of them (this one opens more)`;
57
+ }
58
+ if (depth > MAX_GLOB_GROUP_DEPTH) {
59
+ return `Pattern nests groups too deeply: a glob pattern may nest at most ${MAX_GLOB_GROUP_DEPTH} deep (this one nests ${deepest})`;
60
+ }
61
+ } else if (char === ")" || char === "}" || char === "]") {
62
+ depth = Math.max(0, depth - 1);
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ var logIgnoreDrop = (entry, reason) => {
68
+ log.warn(
69
+ `glob: dropping a .gitignore entry the walk cannot obey \u2014 ${reason}: ${JSON.stringify(entry)}`
70
+ );
71
+ };
72
+ function globWalkIgnore(patterns, report = logIgnoreDrop) {
73
+ const walkable = [];
74
+ for (const pattern of patterns) {
75
+ const refusal = globPatternRefusal(pattern);
76
+ if (refusal) {
77
+ report(pattern, `${refusal}, so the walk covers what it ignored`);
78
+ continue;
79
+ }
80
+ if (pattern.startsWith("!(")) {
81
+ report(
82
+ pattern,
83
+ "picomatch reads `!(` as an extglob, so as an ignore entry it matches everything but what it names"
84
+ );
85
+ continue;
86
+ }
87
+ const climb = climbOutOfWalk(pattern);
88
+ if (climb) {
89
+ report(
90
+ pattern,
91
+ `${climb}, so the walk would read outside the workspace and still ignore nothing`
92
+ );
93
+ continue;
94
+ }
95
+ walkable.push(...walkIgnoreEntry(pattern));
96
+ }
97
+ return walkable;
98
+ }
99
+ function climbOutOfWalk(entry) {
100
+ const spelling = entry.replace(/^!/, "").replace(/^\/+/, "");
101
+ const normalized = posix.normalize(spelling).split("/");
102
+ return normalized[0] === ".." ? "the walk reads it as a climb out of its own root" : null;
103
+ }
104
+ function walkIgnoreEntry(entry) {
105
+ const negated = entry.startsWith("!") && !entry.startsWith("!(");
106
+ const bang = negated ? "!" : "";
107
+ const body = negated ? entry.slice(1) : entry;
108
+ const relative2 = body.replace(/^\/+/, "").replace(/\/+$/, "");
109
+ const dirOnly = body.endsWith("/") && !relative2.endsWith("/**");
110
+ if (!body.startsWith("/")) return dirOnly ? [`${bang}${relative2}/**/*`] : [entry];
111
+ if (!relative2 || relative2 === ".") return [];
112
+ const anchored = `${bang}${relative2}`;
113
+ if (dirOnly) return [`${anchored}/**/*`];
114
+ return relative2.endsWith("/**") ? [anchored] : [anchored, `${anchored}/**`];
115
+ }
116
+ function globWalkPlan(pattern, cwd, platform2 = process.platform) {
117
+ const walked = globWalkPattern(pattern, cwd, platform2);
118
+ const refusal = globPatternRefusal(walked);
119
+ return {
120
+ pattern: walked,
121
+ refusal,
122
+ scopes: refusal ? null : confinedDirectoriesOfPattern(walked, cwd, platform2)
123
+ };
124
+ }
125
+ function pathsOf(platform2) {
126
+ return platform2 === "win32" ? win32 : posix;
127
+ }
128
+ function walkPath(value) {
129
+ return value.replaceAll("\\", "/");
130
+ }
131
+ function relativePatternOf(platformPaths, walkCwd, body) {
132
+ const absolute = platformPaths.resolve(platformPaths.parse(walkCwd).root || "/", body);
133
+ return walkPath(platformPaths.relative(walkCwd, absolute));
134
+ }
135
+ function globWalkPattern(pattern, cwd, platform2 = process.platform) {
136
+ const platformPaths = pathsOf(platform2);
137
+ const spelled = posixPattern(pattern, platform2);
138
+ const body = spelled.endsWith("/") ? spelled.slice(0, -1) : spelled;
139
+ const relative2 = platformPaths.isAbsolute(body.replace(ESCAPING_BACKSLASHES, "")) ? relativePatternOf(platformPaths, walkCwdOf(cwd, platformPaths), body) : "";
140
+ if (relative2) return relative2;
141
+ const base = staticBaseOfPattern(body);
142
+ if (!base || !body.startsWith(base)) return body;
143
+ return `${convertPathToPattern(base)}${body.slice(base.length)}`;
144
+ }
145
+ function posixPattern(pattern, platform2) {
146
+ return platform2 === "win32" ? pattern.replaceAll("\\", "/") : pattern;
147
+ }
148
+ function walkCwdOf(cwd, platformPaths) {
149
+ return walkPath(platformPaths.resolve(cwd));
150
+ }
151
+ var PARENT_DIRECTORY = /^(\/?\.\.)+/;
152
+ var ESCAPING_BACKSLASHES = /\\(?=[()[\]{}!*+?@|])/g;
153
+ var DRIVE_RELATIVE_PATH = /^[A-Za-z]:$/;
154
+ function ensureNonDriveRelativePath(path) {
155
+ return path.replace(DRIVE_RELATIVE_PATH, (match) => `${match}/`);
156
+ }
157
+ function scopePath(platformPaths, cwd, ...segments) {
158
+ return ensureNonDriveRelativePath(
159
+ walkPath(platformPaths.normalize(platformPaths.join(cwd, ...segments)))
160
+ );
161
+ }
162
+ function splitPatternParts(pattern) {
163
+ const parts = picomatch.scan(pattern, { parts: true }).parts;
164
+ return parts?.length ? parts : [pattern];
165
+ }
166
+ function isDynamicPart(part) {
167
+ const scan = picomatch.scan(part);
168
+ return scan.isGlob || scan.negated;
169
+ }
170
+ var MAX_LEADING_ALTERNATIVES = 64;
171
+ function leadingPartsOfParts(parts, popTrailingGlobstar) {
172
+ const leading = [];
173
+ for (let index = 0; index < parts.length; index++) {
174
+ const part = parts[index];
175
+ if (popTrailingGlobstar && part === "**" && !parts[index + 1]) {
176
+ leading.pop();
177
+ break;
178
+ }
179
+ if (index === parts.length - 1 || isDynamicPart(part)) break;
180
+ leading.push(part);
181
+ }
182
+ return leading;
183
+ }
184
+ function staticBaseOfPattern(pattern) {
185
+ const leadingParts = leadingPartsOfParts(splitPatternParts(pattern), false);
186
+ return leadingParts.length ? posix.join(...leadingParts) : "";
187
+ }
188
+ function holds(platformPaths, root, path) {
189
+ const relativePath = walkPath(platformPaths.relative(root, path));
190
+ return relativePath === "" || !relativePath.startsWith("../") && relativePath !== ".." && !platformPaths.isAbsolute(relativePath);
191
+ }
192
+ function confinedDirectoriesOfPattern(pattern, cwd, platform2) {
193
+ const platformPaths = pathsOf(platform2);
194
+ const walkCwd = walkCwdOf(cwd, platformPaths);
195
+ const body = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
196
+ const normalized = platformPaths.isAbsolute(body.replace(ESCAPING_BACKSLASHES, "")) ? relativePatternOf(platformPaths, walkCwd, body) : walkPath(platformPaths.normalize(body));
197
+ const parts = splitPatternParts(normalized);
198
+ const parentDirectory = PARENT_DIRECTORY.exec(normalized)?.[0];
199
+ const climb = leadingClimbOfParts(parts);
200
+ const cwdParts = walkCwd.split("/");
201
+ const walkedClimb = ((parentDirectory?.length ?? 0) + 1) / 3;
202
+ let cancelled = 0;
203
+ while (Number.isInteger(walkedClimb) && cancelled < walkedClimb && parts[cancelled + walkedClimb] !== void 0 && parts[cancelled + walkedClimb] === cwdParts[cwdParts.length + cancelled - walkedClimb]) {
204
+ cancelled++;
205
+ }
206
+ const potentialRoot = scopePath(
207
+ platformPaths,
208
+ walkCwd,
209
+ parentDirectory?.slice(cancelled * 3) ?? ""
210
+ );
211
+ const climbedOutOfCwd = potentialRoot[0] !== "." && walkCwd.length > potentialRoot.length;
212
+ const searchesFrom = climbedOutOfCwd ? potentialRoot : scopePath(platformPaths, walkCwd, ...leadingPartsOfParts(parts, true));
213
+ if (!parentDirectory) {
214
+ return confinedRunsWithin(platformPaths, searchesFrom, leadingRunsOfParts(parts), walkCwd);
215
+ }
216
+ if (cancelled > 0) {
217
+ return [searchesFrom];
218
+ }
219
+ if (!Number.isInteger(walkedClimb)) {
220
+ return [searchesFrom];
221
+ }
222
+ const hops = new Array(walkedClimb).fill("..");
223
+ return confinedRunsWithin(
224
+ platformPaths,
225
+ searchesFrom,
226
+ leadingRunsOfParts(parts.slice(climb)).map((run) => [...hops, ...run]),
227
+ walkCwd
228
+ );
229
+ }
230
+ function leadingClimbOfParts(parts) {
231
+ let climb = 0;
232
+ while (parts[climb] === "..") climb++;
233
+ return climb;
234
+ }
235
+ function leadingRunsOfParts(parts) {
236
+ let runs = [[]];
237
+ for (let index = 0; index < parts.length - 1; index++) {
238
+ const part = parts[index];
239
+ const alternatives = alternativesOfGroup(part);
240
+ if (alternatives) {
241
+ if (runs.length * alternatives.length > MAX_LEADING_ALTERNATIVES) break;
242
+ runs = runs.flatMap((run) => alternatives.map((alternative) => [...run, alternative]));
243
+ continue;
244
+ }
245
+ if (isDynamicPart(part)) break;
246
+ for (const run of runs) run.push(part);
247
+ }
248
+ return runs;
249
+ }
250
+ function alternativesOfGroup(part) {
251
+ if (part.length < 5 || !part.startsWith("{") || !part.endsWith("}")) return null;
252
+ const body = part.slice(1, -1);
253
+ if (!body.includes(",") || /\.\./.test(body)) return null;
254
+ const alternatives = [];
255
+ let nested = 0;
256
+ let current = "";
257
+ for (const char of body) {
258
+ if (char === "{" || char === "(" || char === "[") nested++;
259
+ else if (char === "}" || char === ")" || char === "]") nested--;
260
+ if (char === "," && nested === 0) {
261
+ alternatives.push(current);
262
+ current = "";
263
+ continue;
264
+ }
265
+ current += char;
266
+ }
267
+ alternatives.push(current);
268
+ const named = alternatives.filter(
269
+ (alternative) => !alternative.includes("/") && !isDynamicPart(alternative)
270
+ );
271
+ return named.length ? named : null;
272
+ }
273
+ function confinedRunsWithin(platformPaths, root, runs, walkCwd) {
274
+ const held = [
275
+ ...new Set(
276
+ runs.map((run) => scopePath(platformPaths, walkCwd, ...run)).filter((directory) => holds(platformPaths, root, directory))
277
+ )
278
+ ];
279
+ return held.length ? held : [root];
280
+ }
281
+
282
+ // src/sandbox.ts
283
+ function isExecutableFile(path) {
284
+ try {
285
+ if (!statSync(path).isFile()) return false;
286
+ accessSync(path, constants.X_OK);
287
+ return true;
288
+ } catch {
289
+ return false;
290
+ }
291
+ }
292
+ function detectBwrap() {
293
+ const candidates = [
294
+ ...(process.env.PATH ?? "").split(delimiter).filter(Boolean).map((dir) => join(dir, "bwrap")),
295
+ "/usr/local/bin/bwrap",
296
+ "/opt/homebrew/bin/bwrap",
297
+ "/usr/bin/bwrap"
298
+ ];
299
+ return candidates.find(isExecutableFile) ?? null;
300
+ }
301
+ function matchesExcludedCommand(command, patterns = []) {
302
+ return patterns.some((pattern) => globToRegex(pattern).test(command));
303
+ }
304
+ function sandboxBackendAvailable() {
305
+ return platform() !== "win32" && detectBwrap() !== null;
306
+ }
307
+ function sandboxCoverage(command, settings, backendAvailable = sandboxBackendAvailable) {
308
+ if (!settings.enabled) return { sandboxed: false, reason: "disabled" };
309
+ if (matchesExcludedCommand(command, settings.excludedCommands))
310
+ return { sandboxed: false, reason: "excluded" };
311
+ if (!backendAvailable()) return { sandboxed: false, reason: "unavailable" };
312
+ return { sandboxed: true };
313
+ }
314
+ var SKIP_REASON_DETAIL = {
315
+ disabled: "sandboxing is turned off (sandbox.enabled is false)",
316
+ excluded: "the command matches a sandbox.excludedCommands pattern, which runs it unsandboxed",
317
+ unavailable: "the bubblewrap (bwrap) sandbox backend is unavailable on this platform or is not installed"
318
+ };
319
+ var SKIP_REASON_REMEDY = {
320
+ disabled: "Set sandbox.enabled to true",
321
+ excluded: "Remove the matching pattern from sandbox.excludedCommands",
322
+ unavailable: "Install bubblewrap (bwrap)"
323
+ };
324
+ var SANDBOX_SETTINGS_LOCATION = "in ~/.book/settings.json, or with a --settings file";
325
+ function unsandboxedRefusalMessage(reason) {
326
+ const remedy = reason === "unavailable" ? "Install bubblewrap (bwrap) to enable sandboxing." : `${SKIP_REASON_REMEDY[reason]} ${SANDBOX_SETTINGS_LOCATION}, or set sandbox.allowUnsandboxedCommands to true there to allow this command to run unsandboxed.`;
327
+ return [
328
+ "Refused to run this command outside the sandbox:",
329
+ `${SKIP_REASON_DETAIL[reason]}.`,
330
+ "sandbox.allowUnsandboxedCommands is false, so unsandboxed commands are not permitted.",
331
+ remedy
332
+ ].join(" ");
333
+ }
334
+ function sandboxPolicySummary(settings, state) {
335
+ return {
336
+ unsandboxedCommands: settings.allowUnsandboxedCommands ? "allowed (sandbox.allowUnsandboxedCommands=true)" : "refused (sandbox.allowUnsandboxedCommands=false)",
337
+ autoAllowBash: !settings.autoAllowBashIfSandboxed ? "off \u2014 every Bash call goes through the normal permission path (sandbox.autoAllowBashIfSandboxed=false)" : !state.sandboxActive ? "inert \u2014 no command is sandboxed here, so nothing is auto-allowed (sandbox.autoAllowBashIfSandboxed=true)" : state.adjudicationConfigured ? "inert \u2014 permissions.deny/ask rules are configured, so every Bash call still goes through the normal permission path (sandbox.autoAllowBashIfSandboxed=true)" : "on for genuinely sandboxed commands (sandbox.autoAllowBashIfSandboxed=true)"
338
+ };
339
+ }
340
+ var MAX_CONTROL_FILE_BYTES = 4096;
341
+ var MAX_GIT_CONFIG_BYTES = 1024 * 1024;
342
+ var MAX_WALK_DEPTH = 8;
343
+ function entryKind(path) {
344
+ try {
345
+ const stats = lstatSync(path);
346
+ if (stats.isSymbolicLink()) return "symlink";
347
+ if (stats.isDirectory()) return "directory";
348
+ if (stats.isFile()) return "file";
349
+ return "other";
350
+ } catch {
351
+ return null;
352
+ }
353
+ }
354
+ var realSandboxHost = {
355
+ path: { resolve, join, relative, isAbsolute, sep, dirname },
356
+ exists: existsSync,
357
+ // A path that vanished between the probe and the spawn is absent as far as the
358
+ // mount list is concerned. `statSync` throws for it, and a throw here would
359
+ // abort the whole bwrap invocation over a race, not a protection failure.
360
+ isDirectory: (path) => {
361
+ try {
362
+ return statSync(path).isDirectory();
363
+ } catch {
364
+ return false;
365
+ }
366
+ },
367
+ readFile: (path, maxBytes = MAX_CONTROL_FILE_BYTES) => {
368
+ if (entryKind(path) !== "file") return null;
369
+ let fd;
370
+ try {
371
+ fd = openSync(path, "r");
372
+ const buffer = Buffer.alloc(maxBytes);
373
+ const bytes = readSync(fd, buffer, 0, maxBytes, 0);
374
+ return buffer.subarray(0, bytes).toString("utf-8");
375
+ } catch {
376
+ return null;
377
+ } finally {
378
+ if (fd !== void 0) {
379
+ try {
380
+ closeSync(fd);
381
+ } catch {
382
+ }
383
+ }
384
+ }
385
+ },
386
+ fileSize: (path) => {
387
+ if (entryKind(path) !== "file") return null;
388
+ try {
389
+ return statSync(path).size;
390
+ } catch {
391
+ return null;
392
+ }
393
+ },
394
+ readDir: (path) => {
395
+ try {
396
+ return readdirSync(path);
397
+ } catch {
398
+ return [];
399
+ }
400
+ },
401
+ entryKind,
402
+ linkTarget: (path) => {
403
+ try {
404
+ return readlinkSync(path);
405
+ } catch {
406
+ return null;
407
+ }
408
+ },
409
+ realpath: (path) => {
410
+ try {
411
+ return realpathSync(path);
412
+ } catch {
413
+ return null;
414
+ }
415
+ },
416
+ homedir
417
+ };
418
+ function expandPath(path, host) {
419
+ if (path === "~") return host.homedir();
420
+ if (path.startsWith("~/")) return host.path.join(host.homedir(), path.slice(2));
421
+ return host.path.resolve(path);
422
+ }
423
+ function bindIfPresent(args, flag, path, host) {
424
+ const target = expandPath(path, host);
425
+ if (host.exists(target)) args.push(flag, target, target);
426
+ }
427
+ function unbindablePaths(settings, host = realSandboxHost) {
428
+ return [
429
+ ...settings.filesystem.allowWrite,
430
+ ...settings.filesystem.denyWrite,
431
+ ...settings.filesystem.denyRead
432
+ ].filter((path) => !host.exists(expandPath(path, host)));
433
+ }
434
+ function hasDomainPolicy(settings) {
435
+ return settings.network.allowedDomains.length > 0 || settings.network.deniedDomains.length > 0;
436
+ }
437
+ function canonicalPath(path, host) {
438
+ const absolute = host.path.resolve(path);
439
+ let existing = absolute;
440
+ for (; ; ) {
441
+ const real = host.realpath(existing);
442
+ if (real !== null) return host.path.resolve(real, host.path.relative(existing, absolute));
443
+ const parent = host.path.dirname(existing);
444
+ if (parent === existing) return absolute;
445
+ existing = parent;
446
+ }
447
+ }
448
+ function insideWorkspace(candidate, workspaceRoot, host) {
449
+ return !isOutside(canonicalPath(workspaceRoot, host), canonicalPath(candidate, host), host.path);
450
+ }
451
+ function workspaceGitDirs(workspaceRoot, host) {
452
+ const root = host.path.resolve(workspaceRoot);
453
+ const found = [];
454
+ const add = (candidate, workTree) => {
455
+ const dir = host.path.resolve(candidate);
456
+ if (found.some((entry) => entry.dir === dir)) return;
457
+ if (!host.isDirectory(dir) || !insideWorkspace(dir, root, host)) return;
458
+ found.push({ dir, workTree: host.path.resolve(workTree) });
459
+ };
460
+ const dotGit = host.path.join(root, ".git");
461
+ if (!host.exists(dotGit)) return found;
462
+ if (host.isDirectory(dotGit)) {
463
+ add(dotGit, root);
464
+ } else {
465
+ const target = /^gitdir:\s*(.+)$/m.exec(host.readFile(dotGit) ?? "")?.[1]?.trim();
466
+ if (!target) return found;
467
+ add(host.path.isAbsolute(target) ? target : host.path.join(root, target), root);
468
+ }
469
+ for (let index = 0; index < found.length; index++) {
470
+ const { dir, workTree } = found[index];
471
+ const common = host.readFile(host.path.join(dir, "commondir"))?.trim();
472
+ if (common) add(host.path.isAbsolute(common) ? common : host.path.join(dir, common), workTree);
473
+ for (const name of host.readDir(host.path.join(dir, "worktrees"))) {
474
+ const linked = host.path.join(dir, "worktrees", name);
475
+ const workTreeGitdir = host.readFile(host.path.join(linked, "gitdir"))?.trim();
476
+ add(linked, workTreeGitdir ? host.path.dirname(workTreeGitdir) : root);
477
+ }
478
+ const addModules = (modulesRoot, prefix, depth) => {
479
+ for (const name of host.readDir(modulesRoot)) {
480
+ const moduleDir = host.path.join(modulesRoot, name);
481
+ const workTreePath = prefix ? `${prefix}/${name}` : name;
482
+ const isGitDir = ["HEAD", "config"].some(
483
+ (file) => host.exists(host.path.join(moduleDir, file))
484
+ );
485
+ if (isGitDir) add(moduleDir, host.path.join(root, workTreePath));
486
+ else if (depth < MAX_WALK_DEPTH) addModules(moduleDir, workTreePath, depth + 1);
487
+ }
488
+ };
489
+ addModules(host.path.join(dir, "modules"), "", 0);
490
+ }
491
+ for (const { workTree } of [...found]) {
492
+ if (!insideWorkspace(workTree, root, host)) continue;
493
+ const pointer = host.path.join(workTree, ".git");
494
+ if (host.isDirectory(pointer)) continue;
495
+ const target = /^gitdir:\s*(.+)$/m.exec(host.readFile(pointer) ?? "")?.[1]?.trim();
496
+ if (!target) continue;
497
+ add(host.path.isAbsolute(target) ? target : host.path.join(workTree, target), workTree);
498
+ }
499
+ return found;
500
+ }
501
+ function parseConfigValue(raw) {
502
+ const text = raw.trim();
503
+ if (!text.startsWith('"')) {
504
+ let quoted = false;
505
+ for (let index = 0; index < text.length; index += 1) {
506
+ const character = text[index];
507
+ if (character === '"') quoted = !quoted;
508
+ else if (!quoted && (character === ";" || character === "#"))
509
+ return text.slice(0, index).trim();
510
+ }
511
+ return text;
512
+ }
513
+ let value = "";
514
+ for (let index = 1; index < text.length; index += 1) {
515
+ const character = text[index];
516
+ if (character === '"') break;
517
+ if (character !== "\\") {
518
+ value += character;
519
+ continue;
520
+ }
521
+ const escaped = text[index + 1];
522
+ index += 1;
523
+ value += escaped === "n" ? "\n" : escaped === "t" ? " " : escaped === "b" ? "\b" : escaped ?? "";
524
+ }
525
+ return value;
526
+ }
527
+ function configSectionName(line) {
528
+ const header = /^\[\s*([A-Za-z0-9.-]+)(\s[^\]]*)?\]$/.exec(line.trim());
529
+ if (!header || header[2]) return void 0;
530
+ return header[1].toLowerCase();
531
+ }
532
+ function readCoreHooksPaths(configText) {
533
+ const found = [];
534
+ let inCore = false;
535
+ for (const line of configText.split("\n")) {
536
+ const trimmed = line.trim();
537
+ if (trimmed.startsWith("[")) {
538
+ inCore = configSectionName(trimmed) === "core";
539
+ continue;
540
+ }
541
+ if (!inCore) continue;
542
+ const separator = trimmed.indexOf("=");
543
+ if (separator === -1) continue;
544
+ if (trimmed.slice(0, separator).trim().toLowerCase() !== "hookspath") continue;
545
+ const value = parseConfigValue(trimmed.slice(separator + 1));
546
+ if (value && !found.includes(value)) found.push(value);
547
+ }
548
+ return found;
549
+ }
550
+ function readGitConfigIncludes(configText) {
551
+ const found = [];
552
+ let include = false;
553
+ for (const line of configText.split("\n")) {
554
+ const trimmed = line.trim();
555
+ if (trimmed.startsWith("[")) {
556
+ include = configSectionName(trimmed) === "include" || /^\[includeif\b/i.test(trimmed);
557
+ continue;
558
+ }
559
+ if (!include) continue;
560
+ const separator = trimmed.indexOf("=");
561
+ if (separator === -1) continue;
562
+ if (trimmed.slice(0, separator).trim().toLowerCase() !== "path") continue;
563
+ const value = parseConfigValue(trimmed.slice(separator + 1));
564
+ if (value) found.push(value);
565
+ }
566
+ return found;
567
+ }
568
+ function describeControlPathRefusal(refusal) {
569
+ if (refusal.reason === "oversized-git-config") {
570
+ return `Refused to run this command in the sandbox: ${refusal.path} is larger than the ${MAX_GIT_CONFIG_BYTES / 1024 / 1024} MiB read limit for a git config file, so the core.hooksPath it may declare cannot be found and the hooks directory it names cannot be made read-only. Trim the config, or run this command without the sandbox.`;
571
+ }
572
+ return `Refused to run this command in the sandbox: ${refusal.path} is a symlink, and the sandbox cannot protect a symlinked control path. The link itself sits in the writable workspace, so a command could remove it, put a directory or file in its place, and write the settings, hooks or repository config the host then reads through it. Replace the symlink with the file or directory it names, or run this command without the sandbox.`;
573
+ }
574
+ function protectedWorkspacePaths(workspaceRoot, host = realSandboxHost) {
575
+ const root = host.path.resolve(workspaceRoot);
576
+ const result = { pinnedDirectories: [], mounts: [], refusals: [] };
577
+ const seen = /* @__PURE__ */ new Set();
578
+ const target = (path, kind) => {
579
+ const resolved = host.path.resolve(path);
580
+ if (seen.has(resolved)) return null;
581
+ const describe = (present) => {
582
+ seen.add(resolved);
583
+ return { path: resolved, kind, present };
584
+ };
585
+ const first = host.entryKind(resolved);
586
+ if (first === "symlink") {
587
+ if (!result.refusals.some((entry) => entry.path === resolved)) {
588
+ result.refusals.push({ path: resolved, reason: "symlinked-control-path" });
589
+ }
590
+ return null;
591
+ }
592
+ if (first === null) {
593
+ return kind === "directory" ? describe(false) : null;
594
+ }
595
+ if (first === "directory") {
596
+ return kind === "hidden-file" ? null : describe(true);
597
+ }
598
+ return first === "file" ? describe(true) : null;
599
+ };
600
+ const addDirectory = (path) => {
601
+ const mount = target(path, "directory");
602
+ if (mount) result.mounts.push(mount);
603
+ };
604
+ const addFile = (path) => {
605
+ const mount = target(path, "file");
606
+ if (mount) result.mounts.push(mount);
607
+ };
608
+ const addMaskedFile = (path) => {
609
+ const mount = target(path, "hidden-file");
610
+ if (mount) result.mounts.push(mount);
611
+ };
612
+ addDirectory(host.path.join(root, ".book"));
613
+ addMaskedFile(host.path.join(root, ".book", "settings.local.json"));
614
+ addFile(host.path.join(root, ".bookrc.json"));
615
+ const dotGit = host.path.join(root, ".git");
616
+ if (host.entryKind(dotGit) === "directory") {
617
+ result.pinnedDirectories.push(dotGit);
618
+ } else {
619
+ addFile(dotGit);
620
+ }
621
+ const configsRead = /* @__PURE__ */ new Set();
622
+ const readConfig = (file, workTree, depth) => {
623
+ const at = host.path.resolve(file);
624
+ if (configsRead.has(at)) return;
625
+ configsRead.add(at);
626
+ const size = host.fileSize(at);
627
+ if (size !== null && size > MAX_GIT_CONFIG_BYTES) {
628
+ result.refusals.push({ path: at, reason: "oversized-git-config" });
629
+ return;
630
+ }
631
+ const text = host.readFile(at, MAX_GIT_CONFIG_BYTES);
632
+ if (text === null) return;
633
+ addFile(at);
634
+ for (const hooksPath of readCoreHooksPaths(text)) {
635
+ const directory = host.path.isAbsolute(hooksPath) ? hooksPath : host.path.join(workTree, hooksPath);
636
+ if (insideWorkspace(directory, root, host)) addDirectory(directory);
637
+ }
638
+ if (depth >= MAX_WALK_DEPTH) return;
639
+ for (const included of readGitConfigIncludes(text)) {
640
+ const target2 = included.startsWith("~/") ? host.path.join(host.homedir(), included.slice(2)) : host.path.join(host.path.dirname(at), included);
641
+ readConfig(target2, workTree, depth + 1);
642
+ }
643
+ };
644
+ for (const gitDir of workspaceGitDirs(root, host)) {
645
+ addDirectory(host.path.join(gitDir.dir, "hooks"));
646
+ for (const name of ["config", "config.worktree", "commondir", "gitdir"]) {
647
+ addFile(host.path.join(gitDir.dir, name));
648
+ }
649
+ const workTreePointer = host.path.join(gitDir.workTree, ".git");
650
+ if (host.entryKind(workTreePointer) === "file") addFile(workTreePointer);
651
+ for (const name of ["config", "config.worktree"]) {
652
+ readConfig(host.path.join(gitDir.dir, name), gitDir.workTree, 0);
653
+ }
654
+ }
655
+ return result;
656
+ }
657
+ function mountProtected(args, mount, host) {
658
+ const target = host.path.resolve(mount.path);
659
+ if (mount.kind === "hidden-file") {
660
+ if (mount.present) args.push("--ro-bind", "/dev/null", target);
661
+ return;
662
+ }
663
+ if (mount.kind === "file") {
664
+ if (mount.present) args.push("--ro-bind", target, target);
665
+ return;
666
+ }
667
+ if (mount.present) {
668
+ args.push("--ro-bind", target, target);
669
+ return;
670
+ }
671
+ args.push("--tmpfs", target, "--remount-ro", target);
672
+ }
673
+ function buildSandboxExecution(bwrapPath, command, workspaceRoot, settings, host = realSandboxHost) {
674
+ const args = [];
675
+ args.push("--unshare-pid", "--unshare-ipc", "--unshare-uts", "--die-with-parent");
676
+ args.push("--proc", "/proc");
677
+ args.push("--dev", "/dev");
678
+ for (const dir of ["/usr", "/lib", "/lib64", "/bin", "/sbin", "/etc", "/opt"]) {
679
+ bindIfPresent(args, "--ro-bind", dir, host);
680
+ }
681
+ args.push("--tmpfs", "/tmp");
682
+ bindIfPresent(args, "--bind", workspaceRoot, host);
683
+ const protectedPaths = protectedWorkspacePaths(workspaceRoot, host);
684
+ const isProtectedArea = (path) => {
685
+ const canonical = canonicalPath(expandPath(path, host), host);
686
+ return protectedPaths.mounts.some(
687
+ (mount) => !isOutside(canonicalPath(mount.path, host), canonical, host.path)
688
+ );
689
+ };
690
+ for (const dir of protectedPaths.pinnedDirectories) {
691
+ args.push("--bind", host.path.resolve(dir), host.path.resolve(dir));
692
+ }
693
+ for (const path of settings.filesystem.allowWrite) {
694
+ if (isProtectedArea(path)) continue;
695
+ bindIfPresent(args, "--bind", path, host);
696
+ }
697
+ for (const mount of protectedPaths.mounts) mountProtected(args, mount, host);
698
+ for (const path of settings.filesystem.allowWrite) {
699
+ if (!isProtectedArea(path)) continue;
700
+ bindIfPresent(args, "--bind", path, host);
701
+ }
702
+ for (const mount of protectedPaths.mounts) {
703
+ if (mount.kind === "hidden-file") mountProtected(args, mount, host);
704
+ }
705
+ for (const path of settings.filesystem.denyWrite) bindIfPresent(args, "--ro-bind", path, host);
706
+ for (const path of settings.filesystem.denyRead) {
707
+ const target = expandPath(path, host);
708
+ if (!host.exists(target)) continue;
709
+ if (host.isDirectory(target)) args.push("--tmpfs", target);
710
+ else args.push("--ro-bind", "/dev/null", target);
711
+ }
712
+ args.push(hasDomainPolicy(settings) ? "--unshare-net" : "--share-net");
713
+ args.push("--cap-drop", "ALL");
714
+ args.push("--", "/bin/bash", "-c", command);
715
+ return { file: bwrapPath, args };
716
+ }
717
+ function decideSandboxExecution(ctx, command, workdir) {
718
+ const unsandboxed = (reason) => ctx.sandbox && !ctx.sandbox.allowUnsandboxedCommands ? { sandboxed: false, reason, error: unsandboxedRefusalMessage(reason) } : { sandboxed: false, reason };
719
+ if (!ctx.sandbox?.enabled) return unsandboxed("disabled");
720
+ if (matchesExcludedCommand(command, ctx.sandbox.excludedCommands)) return unsandboxed("excluded");
721
+ if (!resolveWorkspacePath(ctx.workspaceRoot, workdir)) {
722
+ return {
723
+ sandboxed: false,
724
+ error: `workdir is outside the sandboxed workspace: ${workdir}. Add it to sandbox.filesystem.allowWrite, or run without the sandbox.`
725
+ };
726
+ }
727
+ let sandbox;
728
+ try {
729
+ sandbox = ctx.runtime ? ctx.runtime.sandbox(ctx.sandbox) : createSandbox(ctx.sandbox);
730
+ } catch (error) {
731
+ return { sandboxed: false, error: error instanceof Error ? error.message : String(error) };
732
+ }
733
+ if (sandbox) {
734
+ const refusal = protectedWorkspacePaths(ctx.workspaceRoot).refusals[0];
735
+ if (refusal) return { sandboxed: false, error: describeControlPathRefusal(refusal) };
736
+ }
737
+ const exec = sandbox?.wrap(command, ctx.workspaceRoot);
738
+ if (exec) return { sandboxed: true, exec };
739
+ if (ctx.sandbox.failIfUnavailable) {
740
+ return { sandboxed: false, error: "Sandbox unavailable and failIfUnavailable is set" };
741
+ }
742
+ return unsandboxed("unavailable");
743
+ }
744
+ function withGitConfigReadOnlyNotice(text, output, sandboxed, command = "") {
745
+ if (!sandboxed || !/could not (?:write|lock) config file/.test(output)) return text;
746
+ if (writesAGlobalConfig(command)) return text;
747
+ return `${text}
748
+ Note: .git/config is read-only inside the sandbox, so git cannot update repository configuration there. Run this command outside the sandbox.`;
749
+ }
750
+ function writesAGlobalConfig(command) {
751
+ return /^\s*(?:\S+\s+)*git\s+config\b[^|;&]*\s--(?:global|system)\b/.test(command);
752
+ }
753
+ function createSandbox(settings) {
754
+ if (!settings.enabled) return null;
755
+ const os = platform();
756
+ if (os === "win32") {
757
+ if (settings.failIfUnavailable) {
758
+ throw new Error(
759
+ "Bash sandbox is not available on Windows. Disable sandbox.enabled or set failIfUnavailable to false."
760
+ );
761
+ }
762
+ console.warn("\u26A0 Bash sandbox is not available on Windows. Commands will run unsandboxed.");
763
+ return null;
764
+ }
765
+ const bwrap = detectBwrap();
766
+ if (!bwrap) {
767
+ if (settings.failIfUnavailable) {
768
+ throw new Error(
769
+ "bubblewrap (bwrap) not found. Install it to enable bash sandboxing, or set failIfUnavailable to false."
770
+ );
771
+ }
772
+ console.warn(
773
+ "\u26A0 bubblewrap (bwrap) not found \u2014 install it for bash sandboxing. Commands will run unsandboxed."
774
+ );
775
+ return null;
776
+ }
777
+ if (hasDomainPolicy(settings)) {
778
+ console.warn(
779
+ "\u26A0 sandbox.network domain rules cannot be enforced by bubblewrap, which has no per-domain filtering. Network access is disabled entirely for sandboxed commands."
780
+ );
781
+ }
782
+ const unbindable = unbindablePaths(settings);
783
+ if (unbindable.length > 0) {
784
+ console.warn(
785
+ `\u26A0 sandbox.filesystem rules skipped \u2014 nothing exists at: ${unbindable.join(", ")}. These paths are not protected.`
786
+ );
787
+ }
788
+ return {
789
+ wrap(command, workspaceRoot) {
790
+ return buildSandboxExecution(bwrap, command, workspaceRoot, settings);
791
+ },
792
+ describe() {
793
+ return [
794
+ `bubblewrap (${bwrap})`,
795
+ hasDomainPolicy(settings) ? "network disabled" : "host network shared",
796
+ `${settings.filesystem.allowWrite.length} extra writable path(s)`,
797
+ `${settings.filesystem.denyWrite.length} read-only path(s)`,
798
+ `${settings.filesystem.denyRead.length} masked path(s)`,
799
+ unbindable.length > 0 ? `${unbindable.length} skipped (missing)` : "all paths resolved"
800
+ ].join("; ");
801
+ }
802
+ };
803
+ }
804
+
805
+ export {
806
+ globToRegex,
807
+ globWalkIgnore,
808
+ globWalkPlan,
809
+ matchesExcludedCommand,
810
+ sandboxBackendAvailable,
811
+ sandboxCoverage,
812
+ unsandboxedRefusalMessage,
813
+ sandboxPolicySummary,
814
+ realSandboxHost,
815
+ unbindablePaths,
816
+ hasDomainPolicy,
817
+ readCoreHooksPaths,
818
+ readGitConfigIncludes,
819
+ describeControlPathRefusal,
820
+ protectedWorkspacePaths,
821
+ buildSandboxExecution,
822
+ decideSandboxExecution,
823
+ withGitConfigReadOnlyNotice,
824
+ createSandbox
825
+ };
826
+ //# sourceMappingURL=chunk-JTPOI3KU.js.map