@letrquan/book 0.2.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.
- package/CHANGELOG.md +3771 -40
- package/README.md +135 -1311
- package/dist/additional-roots-VAWRQ625.js +32 -0
- package/dist/{app-MOXXWEOY.js → app-J7BVPSUH.js} +9648 -5509
- package/dist/app-J7BVPSUH.js.map +1 -0
- package/dist/chunk-2AAOF5B5.js +28 -0
- package/dist/chunk-2AAOF5B5.js.map +1 -0
- package/dist/chunk-32WBCUAN.js +660 -0
- package/dist/chunk-32WBCUAN.js.map +1 -0
- package/dist/chunk-4DKHLZRI.js +120 -0
- package/dist/chunk-4DKHLZRI.js.map +1 -0
- package/dist/{chunk-CAGOB2N4.js → chunk-62EEAUID.js} +6 -4
- package/dist/chunk-62EEAUID.js.map +1 -0
- package/dist/chunk-AHC7OU75.js +1124 -0
- package/dist/chunk-AHC7OU75.js.map +1 -0
- package/dist/{chunk-E75IPST5.js → chunk-AJZWLKLQ.js} +73 -70
- package/dist/chunk-AJZWLKLQ.js.map +1 -0
- package/dist/chunk-B5FC3MYZ.js +395 -0
- package/dist/chunk-B5FC3MYZ.js.map +1 -0
- package/dist/{chunk-KWHQ2XXL.js → chunk-DNFQTSMJ.js} +55 -13
- package/dist/chunk-DNFQTSMJ.js.map +1 -0
- package/dist/{chunk-J7MIZ5CA.js → chunk-E5WLQSKA.js} +719 -89
- package/dist/chunk-E5WLQSKA.js.map +1 -0
- package/dist/chunk-ECLC4CIJ.js +1181 -0
- package/dist/chunk-ECLC4CIJ.js.map +1 -0
- package/dist/chunk-EEX6QNXV.js +127 -0
- package/dist/chunk-EEX6QNXV.js.map +1 -0
- package/dist/chunk-EFXVSQ5J.js +931 -0
- package/dist/chunk-EFXVSQ5J.js.map +1 -0
- package/dist/{chunk-V4BOYR52.js → chunk-ENLB7NYV.js} +1304 -326
- package/dist/chunk-ENLB7NYV.js.map +1 -0
- package/dist/chunk-FPR7SKIE.js +6926 -0
- package/dist/chunk-FPR7SKIE.js.map +1 -0
- package/dist/{chunk-DGMQEI6Y.js → chunk-HFB74CLM.js} +30 -9
- package/dist/chunk-HFB74CLM.js.map +1 -0
- package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
- package/dist/chunk-HOZXKOE3.js.map +1 -0
- package/dist/chunk-IDYWN37T.js +36 -0
- package/dist/chunk-IDYWN37T.js.map +1 -0
- package/dist/chunk-J4F2QLM2.js +273 -0
- package/dist/chunk-J4F2QLM2.js.map +1 -0
- package/dist/chunk-JGF5HLSI.js +1065 -0
- package/dist/chunk-JGF5HLSI.js.map +1 -0
- package/dist/{chunk-XIFFTATU.js → chunk-KDNWMKXV.js} +7166 -4237
- package/dist/chunk-KDNWMKXV.js.map +1 -0
- package/dist/chunk-KXAYUIKP.js +90 -0
- package/dist/chunk-KXAYUIKP.js.map +1 -0
- package/dist/{chunk-3U5IBM24.js → chunk-MEOB2DZA.js} +12 -2
- package/dist/chunk-MEOB2DZA.js.map +1 -0
- package/dist/{chunk-WKBO5R4O.js → chunk-MV5PQNIA.js} +2 -2
- package/dist/chunk-MV5PQNIA.js.map +1 -0
- package/dist/chunk-N2G4Q7ME.js +2672 -0
- package/dist/chunk-N2G4Q7ME.js.map +1 -0
- package/dist/chunk-OJBMDM5H.js +299 -0
- package/dist/chunk-OJBMDM5H.js.map +1 -0
- package/dist/{chunk-URLQAXTR.js → chunk-PTDC7ZUP.js} +2 -2
- package/dist/chunk-PTDC7ZUP.js.map +1 -0
- package/dist/{chunk-574C6MQR.js → chunk-REASC2QG.js} +173 -64
- package/dist/chunk-REASC2QG.js.map +1 -0
- package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
- package/dist/chunk-RT5SATF7.js.map +1 -0
- package/dist/chunk-SBTU5BEC.js +114 -0
- package/dist/chunk-SBTU5BEC.js.map +1 -0
- package/dist/{chunk-5KLVH3PY.js → chunk-SWPDKLNM.js} +5 -3
- package/dist/chunk-SWPDKLNM.js.map +1 -0
- package/dist/chunk-UCFJ43OS.js +520 -0
- package/dist/chunk-UCFJ43OS.js.map +1 -0
- package/dist/{chunk-VZQXX3WW.js → chunk-W7SLYS3M.js} +80 -16
- package/dist/chunk-W7SLYS3M.js.map +1 -0
- package/dist/{chunk-ANSJPYGI.js → chunk-WV3O5PGX.js} +3 -3
- package/dist/chunk-WV3O5PGX.js.map +1 -0
- package/dist/chunk-XHAO3K7I.js +18 -0
- package/dist/chunk-XHAO3K7I.js.map +1 -0
- package/dist/chunk-XJRJVXJR.js +1050 -0
- package/dist/chunk-XJRJVXJR.js.map +1 -0
- package/dist/chunk-YIL6HTJX.js +663 -0
- package/dist/chunk-YIL6HTJX.js.map +1 -0
- package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
- package/dist/chunk-ZPSZZUWF.js.map +1 -0
- package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-ZRY66LCW.js} +7 -5
- package/dist/context-C3RAP6K5.js +41 -0
- package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-DPQD5H65.js} +6 -5
- package/dist/index.js +947 -203
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-73AQO5WP.js} +18 -9
- package/dist/interactive-assets-73AQO5WP.js.map +1 -0
- package/dist/job-runner.js +166 -111
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.d.ts +2 -0
- package/dist/job-supervisor.js +121 -0
- package/dist/job-supervisor.js.map +1 -0
- package/dist/loader-DQNPDKOX.js +27 -0
- package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-27LJYADQ.js} +6 -5
- package/dist/{mcp-config-YIJIES2C.js → mcp-config-GXSODNCM.js} +4 -3
- package/dist/memory-store-I6Z54VUN.js +51 -0
- package/dist/{model-window-store-YFVNXCIP.js → model-window-store-6VKXXTPJ.js} +9 -4
- package/dist/openai-compatible-IPUNBJ53.js +16 -0
- package/dist/permission-approvals-Q5IMFEZI.js +15 -0
- package/dist/permissions-7Y35X6HZ.js +62 -0
- package/dist/provider-approvals-5AGH3RVT.js +76 -0
- package/dist/sandbox-N2RY72GN.js +42 -0
- package/dist/{scrollback-LUKLIBVI.js → scrollback-GD76GNM4.js} +44 -26
- package/dist/scrollback-GD76GNM4.js.map +1 -0
- package/dist/sdk.d.ts +2739 -2069
- package/dist/sdk.js +77 -42
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-KDBH45AQ.js +43 -0
- package/dist/settings-loader-KDBH45AQ.js.map +1 -0
- package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
- package/dist/settings-removed-3ATWJEF3.js.map +1 -0
- package/dist/shell-selection-Z5WORZU4.js +31 -0
- package/dist/shell-selection-Z5WORZU4.js.map +1 -0
- package/package.json +25 -24
- package/dist/app-MOXXWEOY.js.map +0 -1
- package/dist/chunk-24AXE6SP.js.map +0 -1
- package/dist/chunk-3U5IBM24.js.map +0 -1
- package/dist/chunk-574C6MQR.js.map +0 -1
- package/dist/chunk-5GDZ22YP.js +0 -449
- package/dist/chunk-5GDZ22YP.js.map +0 -1
- package/dist/chunk-5KLVH3PY.js.map +0 -1
- package/dist/chunk-5RDRAO4B.js.map +0 -1
- package/dist/chunk-7N4J557C.js.map +0 -1
- package/dist/chunk-A7JLBK2Y.js +0 -3506
- package/dist/chunk-A7JLBK2Y.js.map +0 -1
- package/dist/chunk-ANSJPYGI.js.map +0 -1
- package/dist/chunk-BTZZUCLI.js +0 -264
- package/dist/chunk-BTZZUCLI.js.map +0 -1
- package/dist/chunk-C7D4NJ2D.js +0 -217
- package/dist/chunk-C7D4NJ2D.js.map +0 -1
- package/dist/chunk-CAGOB2N4.js.map +0 -1
- package/dist/chunk-DGMQEI6Y.js.map +0 -1
- package/dist/chunk-E75IPST5.js.map +0 -1
- package/dist/chunk-EYGD6X2T.js +0 -180
- package/dist/chunk-EYGD6X2T.js.map +0 -1
- package/dist/chunk-GTVB4HEM.js +0 -765
- package/dist/chunk-GTVB4HEM.js.map +0 -1
- package/dist/chunk-J7MIZ5CA.js.map +0 -1
- package/dist/chunk-KWHQ2XXL.js.map +0 -1
- package/dist/chunk-L6LK2QIQ.js +0 -657
- package/dist/chunk-L6LK2QIQ.js.map +0 -1
- package/dist/chunk-MAQXCUR4.js +0 -1007
- package/dist/chunk-MAQXCUR4.js.map +0 -1
- package/dist/chunk-S4XL7HOM.js +0 -402
- package/dist/chunk-S4XL7HOM.js.map +0 -1
- package/dist/chunk-URLQAXTR.js.map +0 -1
- package/dist/chunk-V4BOYR52.js.map +0 -1
- package/dist/chunk-VZQXX3WW.js.map +0 -1
- package/dist/chunk-WKBO5R4O.js.map +0 -1
- package/dist/chunk-XIFFTATU.js.map +0 -1
- package/dist/context-ZC5IFEAX.js +0 -25
- package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
- package/dist/loader-4IDS2RIE.js +0 -15
- package/dist/openai-compatible-RHZUBHV6.js +0 -10
- package/dist/permission-approvals-76PVREFD.js +0 -14
- package/dist/permissions-CHCKTFQQ.js +0 -28
- package/dist/sandbox-CNVAQDA7.js +0 -23
- package/dist/scrollback-LUKLIBVI.js.map +0 -1
- package/dist/settings-loader-M2QP5TX2.js +0 -21
- package/dist/shell-selection-V6QAYXPX.js +0 -17
- package/patches/ink+6.8.0.patch +0 -13
- package/scripts/apply-ink-patch.mjs +0 -29
- package/scripts/ink-patch.mjs +0 -59
- package/scripts/verify-ink-patch.mjs +0 -11
- /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-VAWRQ625.js.map} +0 -0
- /package/dist/{context-ZC5IFEAX.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
- /package/dist/{hook-approvals-CTUFKEJT.js.map → context-C3RAP6K5.js.map} +0 -0
- /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
- /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-DQNPDKOX.js.map} +0 -0
- /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
- /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-GXSODNCM.js.map} +0 -0
- /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-I6Z54VUN.js.map} +0 -0
- /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
- /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
- /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
- /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
- /package/dist/{settings-removed-JWWPT4WA.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
- /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-N2RY72GN.js.map} +0 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeRemovedSettings
|
|
3
|
+
} from "./chunk-HOZXKOE3.js";
|
|
4
|
+
import {
|
|
5
|
+
collectDeclaredDirectories,
|
|
6
|
+
partitionProjectDirectories
|
|
7
|
+
} from "./chunk-4DKHLZRI.js";
|
|
8
|
+
import {
|
|
9
|
+
partitionProjectAllowRules
|
|
10
|
+
} from "./chunk-MV5PQNIA.js";
|
|
11
|
+
import {
|
|
12
|
+
createWorkspaceProviderGate,
|
|
13
|
+
droppedProviderOverrides,
|
|
14
|
+
emptyWorkspaceProviderPartition,
|
|
15
|
+
modelSelectionsOf,
|
|
16
|
+
partitionWorkspaceProviders,
|
|
17
|
+
profileOfModelKey,
|
|
18
|
+
providerIdsOf,
|
|
19
|
+
unaffectedModelSelections,
|
|
20
|
+
withheldModelSelections,
|
|
21
|
+
withheldProviderIds,
|
|
22
|
+
withoutWorkspaceProviderOverrides
|
|
23
|
+
} from "./chunk-B5FC3MYZ.js";
|
|
24
|
+
import {
|
|
25
|
+
collectDeclaredHooks,
|
|
26
|
+
partitionProjectHooks
|
|
27
|
+
} from "./chunk-WV3O5PGX.js";
|
|
28
|
+
import {
|
|
29
|
+
defaultTrustStorePath,
|
|
30
|
+
loadWorkspaceTrust
|
|
31
|
+
} from "./chunk-HFB74CLM.js";
|
|
32
|
+
import {
|
|
33
|
+
ADVISOR_OFF_VALUE,
|
|
34
|
+
DEFAULT_SETTINGS,
|
|
35
|
+
HOOK_EVENTS,
|
|
36
|
+
SettingsRepository,
|
|
37
|
+
bookSettingsSchema,
|
|
38
|
+
writeFileAtomic
|
|
39
|
+
} from "./chunk-REASC2QG.js";
|
|
40
|
+
import {
|
|
41
|
+
projectFileWriteRefusal
|
|
42
|
+
} from "./chunk-IDYWN37T.js";
|
|
43
|
+
import {
|
|
44
|
+
resolveBookHome
|
|
45
|
+
} from "./chunk-ZPSZZUWF.js";
|
|
46
|
+
|
|
47
|
+
// src/settings-loader.ts
|
|
48
|
+
import { readFileSync, existsSync, realpathSync } from "fs";
|
|
49
|
+
import { join, normalize, resolve } from "path";
|
|
50
|
+
|
|
51
|
+
// src/env-boolean.ts
|
|
52
|
+
function parseEnvBoolean(raw) {
|
|
53
|
+
switch (raw?.trim().toLowerCase()) {
|
|
54
|
+
case "0":
|
|
55
|
+
case "false":
|
|
56
|
+
case "off":
|
|
57
|
+
case "no":
|
|
58
|
+
return false;
|
|
59
|
+
case "1":
|
|
60
|
+
case "true":
|
|
61
|
+
case "on":
|
|
62
|
+
case "yes":
|
|
63
|
+
return true;
|
|
64
|
+
default:
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/settings-loader.ts
|
|
70
|
+
var LEGACY_PERMISSIONS_MIGRATION_VERSION = 1;
|
|
71
|
+
var SANDBOX_DENY_LIST_PATHS = [
|
|
72
|
+
"sandbox.filesystem.denyWrite",
|
|
73
|
+
"sandbox.filesystem.denyRead",
|
|
74
|
+
"sandbox.network.deniedDomains"
|
|
75
|
+
];
|
|
76
|
+
var CONCATENATED_ARRAY_PATHS = /* @__PURE__ */ new Set([
|
|
77
|
+
"permissions.allow",
|
|
78
|
+
"permissions.ask",
|
|
79
|
+
"permissions.deny",
|
|
80
|
+
// Straight from the list the hooks schema is built from: a hand-written copy of
|
|
81
|
+
// it went stale the moment `Notification` was added, and a later layer's
|
|
82
|
+
// notification hooks then replaced the user layer's instead of appending (#295).
|
|
83
|
+
...HOOK_EVENTS.map((event) => `hooks.${event}`),
|
|
84
|
+
// A later layer adds to the deny lists rather than replacing them, so a
|
|
85
|
+
// workspace layer shipping `denyRead: []` cannot erase the user's entries
|
|
86
|
+
// (#373) — and a later *trusted* layer still can, which is the one way to
|
|
87
|
+
// narrow a list that turned out to be too broad.
|
|
88
|
+
...SANDBOX_DENY_LIST_PATHS
|
|
89
|
+
]);
|
|
90
|
+
var SANDBOX_DENY_LIST_PATH_SET = new Set(SANDBOX_DENY_LIST_PATHS);
|
|
91
|
+
function mergeObject(base, override, accumulateDenyLists, prefix = "") {
|
|
92
|
+
const result = structuredClone(base);
|
|
93
|
+
for (const [key, value] of Object.entries(override)) {
|
|
94
|
+
if (value === void 0 || key === "__proto__") continue;
|
|
95
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
96
|
+
const existing = result[key];
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
if (path === "additionalDirectories") {
|
|
99
|
+
const combined = [...Array.isArray(existing) ? existing : [], ...value].map(
|
|
100
|
+
(entry) => normalize(String(entry))
|
|
101
|
+
);
|
|
102
|
+
result[key] = [...new Set(combined)];
|
|
103
|
+
} else if (CONCATENATED_ARRAY_PATHS.has(path) && (accumulateDenyLists || !SANDBOX_DENY_LIST_PATH_SET.has(path))) {
|
|
104
|
+
const combined = [...Array.isArray(existing) ? existing : [], ...value];
|
|
105
|
+
result[key] = SANDBOX_DENY_LIST_PATH_SET.has(path) ? [...new Set(combined)] : combined;
|
|
106
|
+
} else {
|
|
107
|
+
result[key] = structuredClone(value);
|
|
108
|
+
}
|
|
109
|
+
} else if (typeof value === "object" && value !== null && typeof existing === "object" && existing !== null && !Array.isArray(existing)) {
|
|
110
|
+
result[key] = mergeObject(
|
|
111
|
+
existing,
|
|
112
|
+
value,
|
|
113
|
+
accumulateDenyLists,
|
|
114
|
+
path
|
|
115
|
+
);
|
|
116
|
+
} else {
|
|
117
|
+
result[key] = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
var WORKSPACE_FORBIDDEN_PATHS = [
|
|
123
|
+
["mcp", "projectServers"],
|
|
124
|
+
["permissions", "projectAllowRules"],
|
|
125
|
+
["hooks", "projectEntries"],
|
|
126
|
+
["commands", "projectCommands"],
|
|
127
|
+
["", "projectDirectories"],
|
|
128
|
+
// The decisions about workspace-declared providers (#418), at the root for the same reason.
|
|
129
|
+
["", "workspaceProviders"]
|
|
130
|
+
];
|
|
131
|
+
function stripPaths(settings, paths) {
|
|
132
|
+
for (const [parent, key] of paths) {
|
|
133
|
+
if (parent === "") {
|
|
134
|
+
delete settings[key];
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const container = settings[parent];
|
|
138
|
+
if (container && typeof container === "object" && !Array.isArray(container)) {
|
|
139
|
+
delete container[key];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
var WORKSPACE_SANDBOX_ONLY_TIGHTENING = [
|
|
144
|
+
[["sandbox", "enabled"], true],
|
|
145
|
+
[["sandbox", "failIfUnavailable"], true],
|
|
146
|
+
[["sandbox", "allowUnsandboxedCommands"], false],
|
|
147
|
+
[["sandbox", "autoAllowBashIfSandboxed"], false],
|
|
148
|
+
[["sandbox", "excludedCommands"], null],
|
|
149
|
+
[["sandbox", "filesystem", "allowWrite"], null],
|
|
150
|
+
// `false` (the loader default) is the read-only git dirs; a workspace layer may
|
|
151
|
+
// tighten to it and may never opt itself back out (#373).
|
|
152
|
+
[["sandbox", "filesystem", "allowGitWrites"], false],
|
|
153
|
+
[["sandbox", "network", "allowedDomains"], null]
|
|
154
|
+
];
|
|
155
|
+
function readPath(settings, path) {
|
|
156
|
+
let value = settings;
|
|
157
|
+
for (const key of path) {
|
|
158
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
159
|
+
value = value[key];
|
|
160
|
+
if (value === void 0) return void 0;
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
function deletePath(settings, path) {
|
|
165
|
+
const parent = path.slice(0, -1);
|
|
166
|
+
const key = path[path.length - 1];
|
|
167
|
+
const container = parent.length === 0 ? settings : readObjectPath(settings, parent);
|
|
168
|
+
if (container) delete container[key];
|
|
169
|
+
}
|
|
170
|
+
function readObjectPath(settings, path) {
|
|
171
|
+
const value = readPath(settings, path);
|
|
172
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
173
|
+
}
|
|
174
|
+
function ignoredWorkspaceSandboxKeys(settings) {
|
|
175
|
+
const record = settings;
|
|
176
|
+
const ignored = [];
|
|
177
|
+
for (const [path, tighteningValue] of WORKSPACE_SANDBOX_ONLY_TIGHTENING) {
|
|
178
|
+
const value = readPath(record, path);
|
|
179
|
+
if (value === void 0) continue;
|
|
180
|
+
if (tighteningValue !== null && value === tighteningValue) continue;
|
|
181
|
+
ignored.push({ key: path.join("."), value });
|
|
182
|
+
}
|
|
183
|
+
return ignored;
|
|
184
|
+
}
|
|
185
|
+
var IGNORED_VALUE_PREVIEW_CHARS = 200;
|
|
186
|
+
function formatIgnoredWorkspaceSandboxKey(entry) {
|
|
187
|
+
const json = JSON.stringify(entry.value) ?? String(entry.value);
|
|
188
|
+
if (json.length <= IGNORED_VALUE_PREVIEW_CHARS) return `${entry.key}=${json}`;
|
|
189
|
+
return `${entry.key}=${json.slice(0, IGNORED_VALUE_PREVIEW_CHARS)}\u2026 (truncated, ${json.length - IGNORED_VALUE_PREVIEW_CHARS} more characters)`;
|
|
190
|
+
}
|
|
191
|
+
function applyIgnoredWorkspaceSandboxKeys(settings) {
|
|
192
|
+
const record = settings;
|
|
193
|
+
for (const entry of ignoredWorkspaceSandboxKeys(settings)) {
|
|
194
|
+
deletePath(record, entry.key.split("."));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function sanitizeLayer(settings, trust) {
|
|
198
|
+
if (trust === "trusted") return settings;
|
|
199
|
+
const sanitized = structuredClone(settings);
|
|
200
|
+
if (sanitized.defaultMode === "bypassPermissions") delete sanitized.defaultMode;
|
|
201
|
+
delete sanitized.shell;
|
|
202
|
+
applyIgnoredWorkspaceSandboxKeys(sanitized);
|
|
203
|
+
stripPaths(sanitized, WORKSPACE_FORBIDDEN_PATHS);
|
|
204
|
+
return sanitized;
|
|
205
|
+
}
|
|
206
|
+
function mergeLayer(resolved, layer, trust) {
|
|
207
|
+
const candidate = sanitizeLayer(layer, trust);
|
|
208
|
+
if (resolved.disableBypassPermissionsMode === true && candidate.disableBypassPermissionsMode === false) {
|
|
209
|
+
candidate.disableBypassPermissionsMode = true;
|
|
210
|
+
}
|
|
211
|
+
if (trust !== "trusted" && sandboxTurnedOnByWorkspaceLayer(resolved, candidate)) {
|
|
212
|
+
const tightened = {
|
|
213
|
+
sandbox: { ...candidate.sandbox, autoAllowBashIfSandboxed: false }
|
|
214
|
+
};
|
|
215
|
+
candidate.sandbox = tightened.sandbox;
|
|
216
|
+
}
|
|
217
|
+
return mergeSettings(resolved, candidate, trust);
|
|
218
|
+
}
|
|
219
|
+
function sandboxTurnedOnByWorkspaceLayer(resolved, candidate) {
|
|
220
|
+
return candidate.sandbox?.enabled === true && resolved.sandbox?.enabled !== true;
|
|
221
|
+
}
|
|
222
|
+
function workspaceLayerThatEnabledSandbox(layers) {
|
|
223
|
+
let enabled = DEFAULT_SETTINGS.sandbox.enabled;
|
|
224
|
+
for (const layer of layers) {
|
|
225
|
+
if (!layer.document) continue;
|
|
226
|
+
const candidate = sanitizeLayer(layer.document, layer.trust);
|
|
227
|
+
if (layer.trust !== "trusted" && candidate.sandbox?.enabled === true && enabled !== true) {
|
|
228
|
+
return layer.path;
|
|
229
|
+
}
|
|
230
|
+
if (candidate.sandbox?.enabled !== void 0) enabled = candidate.sandbox.enabled;
|
|
231
|
+
}
|
|
232
|
+
return void 0;
|
|
233
|
+
}
|
|
234
|
+
function mergeSettings(base, override, trust = "trusted") {
|
|
235
|
+
return mergeObject(
|
|
236
|
+
base,
|
|
237
|
+
override,
|
|
238
|
+
trust !== "trusted"
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
function loadSettingsFile(path) {
|
|
242
|
+
if (!existsSync(path)) return null;
|
|
243
|
+
const raw = readFileSync(path, "utf-8");
|
|
244
|
+
let parsed;
|
|
245
|
+
try {
|
|
246
|
+
parsed = JSON.parse(raw);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Invalid JSON in settings file: ${path}
|
|
250
|
+
${e instanceof Error ? e.message : String(e)}`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
parsed = normalizeRemovedSettings(parsed);
|
|
254
|
+
const result = bookSettingsSchema.safeParse(parsed);
|
|
255
|
+
if (!result.success) throw new Error(`Invalid settings in ${path}:
|
|
256
|
+
${result.error.message}`);
|
|
257
|
+
return parsed;
|
|
258
|
+
}
|
|
259
|
+
function settingsLayerPaths(workspace, overridePath, paths = {}) {
|
|
260
|
+
const userPath = userSettingsPath(paths);
|
|
261
|
+
const projectPath = paths.projectSettingsPath ?? join(workspace, ".book", "settings.json");
|
|
262
|
+
const layers = [
|
|
263
|
+
userPath,
|
|
264
|
+
...sameFile(projectPath, userPath) ? [] : [projectPath],
|
|
265
|
+
paths.localSettingsPath ?? join(workspace, ".book", "settings.local.json")
|
|
266
|
+
];
|
|
267
|
+
if (overridePath) layers.push(overridePath);
|
|
268
|
+
return layers;
|
|
269
|
+
}
|
|
270
|
+
function userSettingsPath(paths) {
|
|
271
|
+
return paths.userSettingsPath ?? (paths.home ? join(paths.home, ".book", "settings.json") : join(resolveBookHome(), "settings.json"));
|
|
272
|
+
}
|
|
273
|
+
function sameFile(left, right) {
|
|
274
|
+
if (resolve(left) === resolve(right)) return true;
|
|
275
|
+
try {
|
|
276
|
+
return realpathSync.native(left) === realpathSync.native(right);
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function isUserSettingsFile(path, paths = {}) {
|
|
282
|
+
return sameFile(path, userSettingsPath(paths));
|
|
283
|
+
}
|
|
284
|
+
function emptyWorkspaceProviderGate() {
|
|
285
|
+
return createWorkspaceProviderGate({ partition: emptyWorkspaceProviderPartition() });
|
|
286
|
+
}
|
|
287
|
+
function withoutStaleAdvisorOff(local, project) {
|
|
288
|
+
if (!local || project?.advisorModel !== void 0) return local;
|
|
289
|
+
if (local.advisorModel?.trim().toLowerCase() !== ADVISOR_OFF_VALUE) return local;
|
|
290
|
+
const rest = { ...local };
|
|
291
|
+
delete rest.advisorModel;
|
|
292
|
+
return rest;
|
|
293
|
+
}
|
|
294
|
+
function resolveSettings(workspace, overridePath, paths = {}) {
|
|
295
|
+
return resolveSettingsWithProviderGate(workspace, overridePath, paths).settings;
|
|
296
|
+
}
|
|
297
|
+
function resolveSettingsWithProviderGate(workspace, overridePath, paths = {}) {
|
|
298
|
+
let resolved = structuredClone(DEFAULT_SETTINGS);
|
|
299
|
+
const userPath = userSettingsPath(paths);
|
|
300
|
+
const user = loadSettingsFile(userPath);
|
|
301
|
+
const projectPath = paths.projectSettingsPath ?? join(workspace, ".book", "settings.json");
|
|
302
|
+
const project = sameFile(projectPath, userPath) ? null : loadSettingsFile(projectPath);
|
|
303
|
+
const localPath = paths.localSettingsPath ?? join(workspace, ".book", "settings.local.json");
|
|
304
|
+
const local = withoutStaleAdvisorOff(loadSettingsFile(localPath), project);
|
|
305
|
+
const override = overridePath ? loadSettingsFile(overridePath) : null;
|
|
306
|
+
const trustedProviderIds = providerIdsOf([
|
|
307
|
+
{ providers: user?.provider },
|
|
308
|
+
{ providers: override?.provider }
|
|
309
|
+
]);
|
|
310
|
+
const workspaceProviderLayers = [
|
|
311
|
+
{ path: projectPath, providers: project?.provider },
|
|
312
|
+
{ path: localPath, providers: local?.provider }
|
|
313
|
+
];
|
|
314
|
+
const gateProviders = (layer) => layer.provider ? {
|
|
315
|
+
...layer,
|
|
316
|
+
provider: withoutWorkspaceProviderOverrides(
|
|
317
|
+
layer.provider,
|
|
318
|
+
trustedProviderIds
|
|
319
|
+
)
|
|
320
|
+
} : layer;
|
|
321
|
+
if (user) resolved = mergeLayer(resolved, user, "trusted");
|
|
322
|
+
const declaredProjectAllow = project?.permissions?.allow ?? [];
|
|
323
|
+
const declaredProjectHooks = collectDeclaredHooks(project);
|
|
324
|
+
const declaredProjectDirectories = project?.additionalDirectories ?? [];
|
|
325
|
+
const userHookCounts = Object.fromEntries(
|
|
326
|
+
HOOK_EVENTS.map((event) => [event, resolved.hooks[event].length])
|
|
327
|
+
);
|
|
328
|
+
if (project) {
|
|
329
|
+
let withheld = project;
|
|
330
|
+
if (declaredProjectAllow.length > 0) {
|
|
331
|
+
withheld = { ...withheld, permissions: { ...withheld.permissions, allow: [] } };
|
|
332
|
+
}
|
|
333
|
+
if (declaredProjectDirectories.length > 0) {
|
|
334
|
+
withheld = { ...withheld, additionalDirectories: [] };
|
|
335
|
+
}
|
|
336
|
+
if (declaredProjectHooks.length > 0) {
|
|
337
|
+
withheld = { ...withheld, hooks: structuredClone(DEFAULT_SETTINGS.hooks) };
|
|
338
|
+
}
|
|
339
|
+
resolved = mergeLayer(resolved, gateProviders(withheld), "repository");
|
|
340
|
+
}
|
|
341
|
+
if (local) resolved = mergeLayer(resolved, gateProviders(local), "local");
|
|
342
|
+
if (override) resolved = mergeLayer(resolved, override, "trusted");
|
|
343
|
+
const trust = loadWorkspaceTrust(
|
|
344
|
+
workspace,
|
|
345
|
+
paths.trustStorePath ?? defaultTrustStorePath(paths.home)
|
|
346
|
+
);
|
|
347
|
+
resolved.permissions.projectAllowRules = {
|
|
348
|
+
...resolved.permissions.projectAllowRules,
|
|
349
|
+
...trust.permissionAllowRules
|
|
350
|
+
};
|
|
351
|
+
resolved.mcp.projectServers = { ...resolved.mcp.projectServers, ...trust.mcpServers };
|
|
352
|
+
resolved.hooks.projectEntries = { ...resolved.hooks.projectEntries, ...trust.hookEntries };
|
|
353
|
+
resolved.commands.projectCommands = {
|
|
354
|
+
...resolved.commands.projectCommands,
|
|
355
|
+
...trust.projectCommands
|
|
356
|
+
};
|
|
357
|
+
resolved.projectDirectories = {
|
|
358
|
+
...resolved.projectDirectories,
|
|
359
|
+
...trust.projectDirectories
|
|
360
|
+
};
|
|
361
|
+
resolved.workspaceProviders = {
|
|
362
|
+
...resolved.workspaceProviders,
|
|
363
|
+
...trust.workspaceProviders
|
|
364
|
+
};
|
|
365
|
+
const providerPartition = partitionWorkspaceProviders(
|
|
366
|
+
workspaceProviderLayers,
|
|
367
|
+
trustedProviderIds,
|
|
368
|
+
resolved.workspaceProviders
|
|
369
|
+
);
|
|
370
|
+
for (const { id, declaration } of providerPartition.approved) {
|
|
371
|
+
resolved.provider[id] = declaration;
|
|
372
|
+
}
|
|
373
|
+
const withheldProviders = new Set(withheldProviderIds(providerPartition));
|
|
374
|
+
const selectionLayers = [
|
|
375
|
+
{ path: userPath, trusted: true, selections: modelSelectionsOf(user ?? {}) },
|
|
376
|
+
{ path: projectPath, trusted: false, selections: modelSelectionsOf(project ?? {}) },
|
|
377
|
+
{ path: localPath, trusted: false, selections: modelSelectionsOf(local ?? {}) },
|
|
378
|
+
{ path: overridePath ?? "", trusted: true, selections: modelSelectionsOf(override ?? {}) }
|
|
379
|
+
];
|
|
380
|
+
const withheldSelections = withheldModelSelections(selectionLayers, withheldProviders);
|
|
381
|
+
for (const { key, fallback } of withheldSelections) {
|
|
382
|
+
const profile = profileOfModelKey(key);
|
|
383
|
+
if (profile === void 0) {
|
|
384
|
+
const topLevel = key;
|
|
385
|
+
if (fallback === void 0) delete resolved[topLevel];
|
|
386
|
+
else resolved[topLevel] = fallback;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const entry = resolved.agents.profiles[profile];
|
|
390
|
+
if (!entry) continue;
|
|
391
|
+
if (fallback === void 0) delete entry.model;
|
|
392
|
+
else entry.model = fallback;
|
|
393
|
+
if (Object.keys(entry).length === 0) delete resolved.agents.profiles[profile];
|
|
394
|
+
}
|
|
395
|
+
if (declaredProjectAllow.length > 0) {
|
|
396
|
+
const { approved } = partitionProjectAllowRules(
|
|
397
|
+
declaredProjectAllow,
|
|
398
|
+
resolved.permissions?.projectAllowRules
|
|
399
|
+
);
|
|
400
|
+
if (approved.length > 0) {
|
|
401
|
+
resolved.permissions.allow = [...resolved.permissions.allow ?? [], ...approved];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (declaredProjectDirectories.length > 0) {
|
|
405
|
+
const { approved } = partitionProjectDirectories(
|
|
406
|
+
collectDeclaredDirectories(workspace, declaredProjectDirectories),
|
|
407
|
+
resolved.projectDirectories
|
|
408
|
+
);
|
|
409
|
+
if (approved.length > 0) {
|
|
410
|
+
resolved.additionalDirectories = [
|
|
411
|
+
...resolved.additionalDirectories ?? [],
|
|
412
|
+
...approved.map((directory) => directory.realPath)
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (declaredProjectHooks.length > 0) {
|
|
417
|
+
const { approved } = partitionProjectHooks(
|
|
418
|
+
declaredProjectHooks,
|
|
419
|
+
resolved.hooks?.projectEntries
|
|
420
|
+
);
|
|
421
|
+
const approvedByEvent = /* @__PURE__ */ new Map();
|
|
422
|
+
for (const { event, entry } of approved) {
|
|
423
|
+
const entries = approvedByEvent.get(event) ?? [];
|
|
424
|
+
entries.push(entry);
|
|
425
|
+
approvedByEvent.set(event, entries);
|
|
426
|
+
}
|
|
427
|
+
for (const [event, entries] of approvedByEvent) {
|
|
428
|
+
resolved.hooks[event].splice(userHookCounts[event], 0, ...entries);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const settings = bookSettingsSchema.parse(resolved);
|
|
432
|
+
const providerGate = createWorkspaceProviderGate({
|
|
433
|
+
partition: providerPartition,
|
|
434
|
+
droppedOverrides: droppedProviderOverrides(workspaceProviderLayers, trustedProviderIds),
|
|
435
|
+
withheldSelections,
|
|
436
|
+
unaffectedSelections: unaffectedModelSelections(selectionLayers, withheldProviders)
|
|
437
|
+
});
|
|
438
|
+
return { settings, providerGate };
|
|
439
|
+
}
|
|
440
|
+
function applySettingsEnvOverrides(settings, env = process.env) {
|
|
441
|
+
const startupAnimation = parseEnvBoolean(env.BOOK_STARTUP_ANIMATION);
|
|
442
|
+
if (startupAnimation === void 0) return settings;
|
|
443
|
+
return { ...settings, ui: { ...settings.ui, startupAnimation } };
|
|
444
|
+
}
|
|
445
|
+
function startupAnimationEnvNote(env = process.env) {
|
|
446
|
+
const raw = env.BOOK_STARTUP_ANIMATION;
|
|
447
|
+
if (parseEnvBoolean(raw) === void 0) return void 0;
|
|
448
|
+
return `BOOK_STARTUP_ANIMATION is set to "${raw}" \u2014 it decides the splash at every launch, so ui.startupAnimation will not apply while it is set.`;
|
|
449
|
+
}
|
|
450
|
+
function migrateLegacyPermissions(workspace, home, validatedSettings) {
|
|
451
|
+
if (!validatedSettings) {
|
|
452
|
+
resolveSettings(workspace, void 0, home ? { home } : void 0);
|
|
453
|
+
}
|
|
454
|
+
const legacyPath = join(home ? join(home, ".book") : resolveBookHome(), "permissions.json");
|
|
455
|
+
if (!existsSync(legacyPath)) return false;
|
|
456
|
+
const markerPath = join(workspace, ".book", "migrations.json");
|
|
457
|
+
const localPath = join(workspace, ".book", "settings.local.json");
|
|
458
|
+
for (const path of [markerPath, localPath]) {
|
|
459
|
+
const refusal = projectFileWriteRefusal(path);
|
|
460
|
+
if (refusal) {
|
|
461
|
+
console.warn(`\u26A0 Permission rules not migrated: ${path}: ${refusal}.`);
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
const marker = JSON.parse(readFileSync(markerPath, "utf-8"));
|
|
467
|
+
if ((marker.legacyPermissions ?? 0) >= LEGACY_PERMISSIONS_MIGRATION_VERSION) return false;
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
let legacyRules = [];
|
|
471
|
+
try {
|
|
472
|
+
const raw = readFileSync(legacyPath, "utf-8");
|
|
473
|
+
const parsed = JSON.parse(raw);
|
|
474
|
+
legacyRules = parsed.rules ?? [];
|
|
475
|
+
} catch {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
if (legacyRules.length === 0) return false;
|
|
479
|
+
const result = new SettingsRepository(localPath).update((existing) => {
|
|
480
|
+
const permissions = existing.permissions ?? {};
|
|
481
|
+
for (const key of ["allow", "ask", "deny"]) {
|
|
482
|
+
if (!Array.isArray(permissions[key])) permissions[key] = [];
|
|
483
|
+
}
|
|
484
|
+
for (const rule of legacyRules) {
|
|
485
|
+
const specifier = rule.pattern ? `${rule.toolName}(${rule.pattern})` : rule.toolName;
|
|
486
|
+
const effect = rule.effect;
|
|
487
|
+
if (!permissions[effect].includes(specifier)) permissions[effect].push(specifier);
|
|
488
|
+
}
|
|
489
|
+
existing.permissions = permissions;
|
|
490
|
+
});
|
|
491
|
+
if (!result.ok) return false;
|
|
492
|
+
writeFileAtomic(
|
|
493
|
+
markerPath,
|
|
494
|
+
`${JSON.stringify({ legacyPermissions: LEGACY_PERMISSIONS_MIGRATION_VERSION }, null, 2)}
|
|
495
|
+
`
|
|
496
|
+
);
|
|
497
|
+
if (!result.changed) return false;
|
|
498
|
+
console.warn(
|
|
499
|
+
`\u26A0 Migrated ${legacyRules.length} permission rule(s) from ~/.book/permissions.json to ${localPath}. Delete the legacy file after verifying the migration.`
|
|
500
|
+
);
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export {
|
|
505
|
+
parseEnvBoolean,
|
|
506
|
+
ignoredWorkspaceSandboxKeys,
|
|
507
|
+
formatIgnoredWorkspaceSandboxKey,
|
|
508
|
+
workspaceLayerThatEnabledSandbox,
|
|
509
|
+
mergeSettings,
|
|
510
|
+
loadSettingsFile,
|
|
511
|
+
settingsLayerPaths,
|
|
512
|
+
isUserSettingsFile,
|
|
513
|
+
emptyWorkspaceProviderGate,
|
|
514
|
+
resolveSettings,
|
|
515
|
+
resolveSettingsWithProviderGate,
|
|
516
|
+
applySettingsEnvOverrides,
|
|
517
|
+
startupAnimationEnvNote,
|
|
518
|
+
migrateLegacyPermissions
|
|
519
|
+
};
|
|
520
|
+
//# sourceMappingURL=chunk-UCFJ43OS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/settings-loader.ts","../src/env-boolean.ts"],"sourcesContent":["import { readFileSync, existsSync, realpathSync } from 'fs';\nimport { join, normalize, resolve } from 'path';\nimport {\n ADVISOR_OFF_VALUE,\n bookSettingsSchema,\n DEFAULT_SETTINGS,\n HOOK_EVENTS,\n type BookSettings,\n type HookEntry,\n type HookEvent,\n type ResolvedSettings,\n} from './settings.js';\nimport { SettingsRepository, writeFileAtomic } from './settings-repository.js';\nimport { projectFileWriteRefusal } from './workspace-files.js';\nimport { resolveBookHome } from './book-home.js';\nimport { collectDeclaredDirectories, partitionProjectDirectories } from './additional-roots.js';\nimport { partitionProjectAllowRules } from './permission-approvals.js';\nimport { collectDeclaredHooks, partitionProjectHooks } from './hook-approvals.js';\nimport {\n createWorkspaceProviderGate,\n droppedProviderOverrides,\n emptyWorkspaceProviderPartition,\n modelSelectionsOf,\n partitionWorkspaceProviders,\n profileOfModelKey,\n providerIdsOf,\n unaffectedModelSelections,\n withheldModelSelections,\n withheldProviderIds,\n withoutWorkspaceProviderOverrides,\n type ModelSelectionLayer,\n type ProviderLayer,\n type WorkspaceProviderGate,\n} from './provider-approvals.js';\nimport { defaultTrustStorePath, loadWorkspaceTrust } from './workspace-trust.js';\nimport { normalizeRemovedSettings } from './settings-removed.js';\nimport { parseEnvBoolean } from './env-boolean.js';\n\nconst LEGACY_PERMISSIONS_MIGRATION_VERSION = 1;\n\n/**\n * The sandbox deny lists: paths a command may not write and paths it may not\n * read, plus the domains it may not reach.\n *\n * One list for all three, because they are one rule applied to three keys — and\n * because the rule is no longer \"these keys concatenate\" but \"these keys\n * concatenate *for a workspace layer*\", which both the concatenating set and the\n * deduping one are derived from. A second hand-written copy is how\n * `Notification` went missing from the hooks list in the first place (#295).\n */\nconst SANDBOX_DENY_LIST_PATHS = [\n 'sandbox.filesystem.denyWrite',\n 'sandbox.filesystem.denyRead',\n 'sandbox.network.deniedDomains',\n] as const;\n\n/**\n * Deep-merge two settings objects. For arrays, concatenate (used for\n * permission rules and additionalDirectories). For objects, merge recursively.\n * For scalars, the override wins.\n */\nconst CONCATENATED_ARRAY_PATHS = new Set<string>([\n 'permissions.allow',\n 'permissions.ask',\n 'permissions.deny',\n // Straight from the list the hooks schema is built from: a hand-written copy of\n // it went stale the moment `Notification` was added, and a later layer's\n // notification hooks then replaced the user layer's instead of appending (#295).\n ...HOOK_EVENTS.map((event) => `hooks.${event}`),\n // A later layer adds to the deny lists rather than replacing them, so a\n // workspace layer shipping `denyRead: []` cannot erase the user's entries\n // (#373) — and a later *trusted* layer still can, which is the one way to\n // narrow a list that turned out to be too broad.\n ...SANDBOX_DENY_LIST_PATHS,\n]);\n\n/**\n * The same three paths as a set, for the two questions a merge asks about one of\n * them: whether the layer's entries add to or replace the earlier ones, and\n * whether repeating a path across layers stores it once.\n */\nconst SANDBOX_DENY_LIST_PATH_SET: ReadonlySet<string> = new Set(SANDBOX_DENY_LIST_PATHS);\n\nfunction mergeObject(\n base: Record<string, unknown>,\n override: Record<string, unknown>,\n /** Whether a deny list from this layer adds to the earlier one or replaces it. */\n accumulateDenyLists: boolean,\n prefix = '',\n): Record<string, unknown> {\n const result = structuredClone(base);\n\n for (const [key, value] of Object.entries(override)) {\n // JSON.parse keeps `\"__proto__\"` as an ordinary own key, but `result[key] = …`\n // below would go through the prototype setter: a repository layer could then\n // supply inherited `shell` or `defaultMode` values that sanitizeLayer, which\n // deletes own keys, never sees. No setting has that name, so drop it.\n if (value === undefined || key === '__proto__') continue;\n const path = prefix ? `${prefix}.${key}` : key;\n const existing = result[key];\n\n if (Array.isArray(value)) {\n if (path === 'additionalDirectories') {\n const combined = [...(Array.isArray(existing) ? existing : []), ...value].map((entry) =>\n normalize(String(entry)),\n );\n result[key] = [...new Set(combined)];\n } else if (\n CONCATENATED_ARRAY_PATHS.has(path) &&\n (accumulateDenyLists || !SANDBOX_DENY_LIST_PATH_SET.has(path))\n ) {\n const combined = [...(Array.isArray(existing) ? existing : []), ...value];\n result[key] = SANDBOX_DENY_LIST_PATH_SET.has(path) ? [...new Set(combined)] : combined;\n } else {\n result[key] = structuredClone(value);\n }\n } else if (\n typeof value === 'object' &&\n value !== null &&\n typeof existing === 'object' &&\n existing !== null &&\n !Array.isArray(existing)\n ) {\n result[key] = mergeObject(\n existing as Record<string, unknown>,\n value as Record<string, unknown>,\n accumulateDenyLists,\n path,\n );\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n/**\n * How much authority a settings layer carries.\n *\n * - `trusted` — the user's own global file, or a path they passed on the CLI.\n * - `local` — `<workspace>/.book/settings.local.json`: gitignored and normally\n * written by the user, or by Book on their behalf.\n * - `repository` — `<workspace>/.book/settings.json`: checked in and controlled\n * by whoever wrote the repository. It may never grant itself trust.\n */\nexport type SettingsLayerTrust = 'trusted' | 'local' | 'repository';\n\n/**\n * Settings paths that a workspace file may not supply for itself. Most record\n * a decision the user made *about* repository-controlled input; honouring one\n * from the workspace would let a clone approve itself. Experimental capability\n * flags are included because merely opening a clone must not opt the user into\n * unstable runtime behavior.\n *\n * The local layer is no safer than the checked-in one here. `.gitignore` does\n * not stop a *tracked* file from reaching a clone, so a repository that\n * force-adds `.book/settings.local.json` ships its own approvals with it. Both\n * workspace layers are therefore stripped. Trust decisions come from the\n * user-global store; experimental flags come from a trusted user-global or\n * explicit settings document (or from a process environment opt-in).\n *\n * An empty `parent` names a key at the document root, which is how the\n * `additionalDirectories` decisions map is expressed: the setting it decides\n * about is itself top-level, so there is no container object to put it in.\n */\nconst WORKSPACE_FORBIDDEN_PATHS: ReadonlyArray<readonly [string, string]> = [\n ['mcp', 'projectServers'],\n ['permissions', 'projectAllowRules'],\n ['hooks', 'projectEntries'],\n ['commands', 'projectCommands'],\n ['', 'projectDirectories'],\n // The decisions about workspace-declared providers (#418), at the root for the same reason.\n ['', 'workspaceProviders'],\n];\n\nfunction stripPaths(\n settings: Partial<BookSettings>,\n paths: ReadonlyArray<readonly [string, string]>,\n): void {\n for (const [parent, key] of paths) {\n if (parent === '') {\n delete (settings as Record<string, unknown>)[key];\n continue;\n }\n const container = (settings as Record<string, unknown>)[parent];\n if (container && typeof container === 'object' && !Array.isArray(container)) {\n delete (container as Record<string, unknown>)[key];\n }\n }\n}\n\n/** One `sandbox.*` key a workspace layer supplied that the loader will not honour. */\nexport interface IgnoredWorkspaceSandboxKey {\n /** Dotted path of the key, e.g. `sandbox.filesystem.allowWrite`. */\n key: string;\n /** The value the layer wrote, so a report can quote what the file actually said. */\n value: unknown;\n}\n\n/**\n * `sandbox.*` keys a workspace layer supplies that only loosen, and therefore\n * never survive to the merge. The second element is the one value the layer may\n * set — the tightening direction — or `null` when any value is dropped.\n *\n * The sandbox is the boundary between a command and the host, so a file inside\n * the workspace must not be able to widen it: a checked-in `settings.json`\n * could switch off the sandbox the user enabled, exclude every command\n * (`excludedCommands: [\"*\"]`), or open a writable root (`allowWrite: [\"/\"]`)\n * — and because those arrays *replaced* the earlier layer's, even the user's own\n * `denyRead` list could be emptied. Turning the sandbox on, adding deny entries\n * and refusing unsandboxed commands stay available to a workspace layer; they\n * only ever narrow it (#373).\n */\nconst WORKSPACE_SANDBOX_ONLY_TIGHTENING: ReadonlyArray<readonly [readonly string[], unknown]> = [\n [['sandbox', 'enabled'], true],\n [['sandbox', 'failIfUnavailable'], true],\n [['sandbox', 'allowUnsandboxedCommands'], false],\n [['sandbox', 'autoAllowBashIfSandboxed'], false],\n [['sandbox', 'excludedCommands'], null],\n [['sandbox', 'filesystem', 'allowWrite'], null],\n // `false` (the loader default) is the read-only git dirs; a workspace layer may\n // tighten to it and may never opt itself back out (#373).\n [['sandbox', 'filesystem', 'allowGitWrites'], false],\n [['sandbox', 'network', 'allowedDomains'], null],\n];\n\n/**\n * The value at a path in a settings document, or `undefined` when a step is\n * missing or is not a plain object.\n *\n * One walker rather than two near-copies: `readContainer` used to be this with an\n * extra condition at the end, and a loop that differs only in its last line is\n * how two helpers drift apart and a delete lands on the wrong object.\n */\nfunction readPath(settings: Record<string, unknown>, path: readonly string[]): unknown {\n let value: unknown = settings;\n for (const key of path) {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;\n value = (value as Record<string, unknown>)[key];\n if (value === undefined) return undefined;\n }\n return value;\n}\n\nfunction deletePath(settings: Record<string, unknown>, path: readonly string[]): void {\n const parent = path.slice(0, -1);\n const key = path[path.length - 1];\n const container = parent.length === 0 ? settings : readObjectPath(settings, parent);\n if (container) delete container[key];\n}\n\n/** `readPath` for a path that has to end at an object, which `deletePath` needs. */\nfunction readObjectPath(\n settings: Record<string, unknown>,\n path: readonly string[],\n): Record<string, unknown> | undefined {\n const value = readPath(settings, path);\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\n/**\n * A workspace layer as the file wrote it: parsed, but not required to be\n * complete. The sandbox rule is about what a document *says*, so the reader\n * takes a partial view — a layer that omits `excludedCommands` entirely is\n * describing itself, not supplying an empty list. Every key is optional here,\n * including the nested ones, which is the whole point of the type.\n */\nexport interface WorkspaceLayerSandboxView {\n sandbox?: Omit<Partial<BookSettings['sandbox']>, 'filesystem' | 'network'> & {\n filesystem?: Partial<BookSettings['sandbox']['filesystem']>;\n network?: Partial<BookSettings['sandbox']['network']>;\n };\n}\n\n/**\n * The `sandbox.*` keys `settings` declares that a workspace layer may not\n * supply, with the values it wrote. Pure: `sanitizeLayer` deletes exactly the\n * keys this reports, and `book doctor` prints the same list, so the enforced\n * rule and the reported one cannot drift apart.\n */\nexport function ignoredWorkspaceSandboxKeys(\n settings: WorkspaceLayerSandboxView,\n): IgnoredWorkspaceSandboxKey[] {\n const record = settings as Record<string, unknown>;\n const ignored: IgnoredWorkspaceSandboxKey[] = [];\n for (const [path, tighteningValue] of WORKSPACE_SANDBOX_ONLY_TIGHTENING) {\n const value = readPath(record, path);\n if (value === undefined) continue;\n if (tighteningValue !== null && value === tighteningValue) continue;\n ignored.push({ key: path.join('.'), value });\n }\n return ignored;\n}\n\n/**\n * How much of an ignored value a report prints.\n *\n * `excludedCommands` and `allowWrite` are globs and directory lists, so a\n * repository can put as much text in one as it likes, and every host that\n * reports an ignored key would then print a screenful of it — pushing the part\n * of the diagnostic that says where the value belongs off the screen. The head\n * is kept because it identifies the value; the length is what follows it.\n */\nconst IGNORED_VALUE_PREVIEW_CHARS = 200;\n\n/** One ignored key as a report line reads it: `sandbox.enabled=false`. */\nexport function formatIgnoredWorkspaceSandboxKey(entry: IgnoredWorkspaceSandboxKey): string {\n const json = JSON.stringify(entry.value) ?? String(entry.value);\n if (json.length <= IGNORED_VALUE_PREVIEW_CHARS) return `${entry.key}=${json}`;\n return `${entry.key}=${json.slice(0, IGNORED_VALUE_PREVIEW_CHARS)}… (truncated, ${\n json.length - IGNORED_VALUE_PREVIEW_CHARS\n } more characters)`;\n}\n\n/**\n * Drop every loosening `sandbox.*` key from a workspace layer. Deleting rather\n * than overwriting is what makes the key harmless: the merge keeps whatever the\n * earlier, more trusted layer said.\n */\nfunction applyIgnoredWorkspaceSandboxKeys(settings: Partial<BookSettings>): void {\n const record = settings as Record<string, unknown>;\n for (const entry of ignoredWorkspaceSandboxKeys(settings)) {\n deletePath(record, entry.key.split('.'));\n }\n}\n\nfunction sanitizeLayer(\n settings: Partial<BookSettings>,\n trust: SettingsLayerTrust,\n): Partial<BookSettings> {\n if (trust === 'trusted') return settings;\n const sanitized = structuredClone(settings);\n // Project/local settings cannot opt a session into the most permissive mode.\n if (sanitized.defaultMode === 'bypassPermissions') delete sanitized.defaultMode;\n // `shell` names the program every Bash command is handed to. A clone that\n // could point it at a binary it ships would run that binary on the first\n // command, so the key is honoured from trusted layers only.\n delete sanitized.shell;\n // Nor may a workspace file widen the sandbox it is supposed to be confined by.\n applyIgnoredWorkspaceSandboxKeys(sanitized);\n stripPaths(sanitized, WORKSPACE_FORBIDDEN_PATHS);\n return sanitized;\n}\n\n/**\n * Merge one layer over the resolved settings.\n *\n * The layer's trust decides two things that are not visible in the layer itself:\n * whether a deny list adds to or replaces the earlier one, and whether turning\n * the sandbox on costs the session its auto-allow. Both are stated here, next to\n * the sanitizing that already reads `trust`, rather than left to each caller.\n */\nfunction mergeLayer(\n resolved: ResolvedSettings,\n layer: Partial<BookSettings>,\n trust: SettingsLayerTrust,\n): ResolvedSettings {\n const candidate = sanitizeLayer(layer, trust);\n // A trusted global safety ceiling cannot be disabled by a lower-trust layer.\n if (\n resolved.disableBypassPermissionsMode === true &&\n candidate.disableBypassPermissionsMode === false\n ) {\n candidate.disableBypassPermissionsMode = true;\n }\n if (trust !== 'trusted' && sandboxTurnedOnByWorkspaceLayer(resolved, candidate)) {\n // Written through the partial view the sanitizer uses: `BookSettings`\n // declares every sandbox key as present, and this layer supplies one.\n const tightened: WorkspaceLayerSandboxView = {\n sandbox: { ...candidate.sandbox, autoAllowBashIfSandboxed: false },\n };\n (candidate as WorkspaceLayerSandboxView).sandbox = tightened.sandbox;\n }\n return mergeSettings(resolved, candidate, trust);\n}\n\n/**\n * Whether this workspace layer is what turns the sandbox on.\n *\n * `enabled: true` is the one `sandbox` key a workspace layer may set, because it\n * only ever applies to sessions that had it off. On its own it narrows nothing a\n * user chose; with `autoAllowBashIfSandboxed` it stops the session from asking\n * before each command, so a checked-in `settings.json` would turn a workspace\n * where the user reads every command into one where nobody is asked at all.\n * Setting it here makes the pair equivalent to the auto-allow alone, which the\n * loader already drops.\n */\nfunction sandboxTurnedOnByWorkspaceLayer(\n resolved: ResolvedSettings,\n candidate: Partial<BookSettings>,\n): boolean {\n return candidate.sandbox?.enabled === true && resolved.sandbox?.enabled !== true;\n}\n\n/** One layer as a caller of {@link workspaceLayerThatEnabledSandbox} has it. */\nexport interface SettingsLayerForSandboxActivation {\n path: string;\n trust: SettingsLayerTrust;\n /** Null when the file is absent, which is not a layer that declared anything. */\n document: Partial<BookSettings> | null;\n}\n\n/**\n * The workspace layer that flipped `sandbox.enabled` from not-true to true, or\n * undefined when none did: the file whose `enabled: true` cost the session its\n * `autoAllowBashIfSandboxed`.\n *\n * Two hosts have to say so — `book doctor`, which lists what each layer changed,\n * and the print/SDK notices, which have no other channel — and both were asking\n * the raw layers \"does this one say `enabled: true`\". That is a different\n * question: once a trusted layer has enabled the sandbox, a repository repeating\n * the key changes nothing and was still reported, every session, as a decision it\n * did not make.\n *\n * The layers go in resolution order *including* the trusted ones, because the\n * trusted baseline is exactly what decides whether a workspace layer is the\n * flipper. The walk is this function's own and reads only `sandbox.enabled`, and\n * it asks the same question through the same {@link sanitizeLayer} the merge\n * applies, so it cannot disagree with\n * {@link sandboxTurnedOnByWorkspaceLayer} about which layer that is.\n */\nexport function workspaceLayerThatEnabledSandbox(\n layers: readonly SettingsLayerForSandboxActivation[],\n): string | undefined {\n let enabled = DEFAULT_SETTINGS.sandbox.enabled;\n for (const layer of layers) {\n if (!layer.document) continue;\n const candidate = sanitizeLayer(layer.document, layer.trust);\n if (layer.trust !== 'trusted' && candidate.sandbox?.enabled === true && enabled !== true) {\n return layer.path;\n }\n if (candidate.sandbox?.enabled !== undefined) enabled = candidate.sandbox.enabled;\n }\n return undefined;\n}\n\n/**\n * Merge a settings layer over resolved settings.\n *\n * @param trust - what the layer is (`trusted` by default: the user-global file\n * or a `--settings` path). Only a workspace layer accumulates the sandbox deny\n * lists; every other array behaviour is the same for all layers.\n */\nexport function mergeSettings(\n base: ResolvedSettings,\n override: Partial<BookSettings>,\n trust: SettingsLayerTrust = 'trusted',\n): ResolvedSettings {\n return mergeObject(\n base as unknown as Record<string, unknown>,\n override as Record<string, unknown>,\n trust !== 'trusted',\n ) as unknown as ResolvedSettings;\n}\n\n/**\n * Load and validate a single settings.json file. Returns null if the file\n * doesn't exist. Throws on parse/validation errors.\n */\nfunction loadSettingsFile(path: string): BookSettings | null {\n if (!existsSync(path)) return null;\n const raw = readFileSync(path, 'utf-8');\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw new Error(\n `Invalid JSON in settings file: ${path}\\n${e instanceof Error ? e.message : String(e)}`,\n );\n }\n // A setting that was removed must not brick a working install: an obsolete\n // value of a surviving key would otherwise fail the whole document and stop\n // Book from starting, with a schema dump that names no remedy. Removed keys\n // are reported by `book doctor`, which reads the file itself.\n parsed = normalizeRemovedSettings(parsed);\n const result = bookSettingsSchema.safeParse(parsed);\n if (!result.success) throw new Error(`Invalid settings in ${path}:\\n${result.error.message}`);\n // Resolution owns defaults. Returning the source document preserves the distinction\n // between an omitted field and a field explicitly set to an empty collection.\n return parsed as BookSettings;\n}\n\n/**\n * Resolve settings from all scopes: user → project → local.\n * Priority: Local > Project > User.\n *\n * @param workspace - Project root directory\n * @param overridePath - Optional path to an ad-hoc settings file (--settings flag)\n * @returns Fully resolved settings with all defaults filled\n */\nexport interface SettingsResolutionPaths {\n home?: string;\n userSettingsPath?: string;\n projectSettingsPath?: string;\n localSettingsPath?: string;\n trustStorePath?: string;\n}\n\n/**\n * Every settings file this resolution would read, in layer order.\n *\n * Exported so callers that need to inspect the files themselves — `book doctor`\n * reporting removed keys, the credential error checking for a stale `auth`\n * block — resolve the same paths the loader does rather than rebuilding them.\n */\nexport function settingsLayerPaths(\n workspace: string,\n overridePath?: string,\n paths: SettingsResolutionPaths = {},\n): string[] {\n const userPath = userSettingsPath(paths);\n const projectPath = paths.projectSettingsPath ?? join(workspace, '.book', 'settings.json');\n const layers = [\n userPath,\n ...(sameFile(projectPath, userPath) ? [] : [projectPath]),\n paths.localSettingsPath ?? join(workspace, '.book', 'settings.local.json'),\n ];\n if (overridePath) layers.push(overridePath);\n return layers;\n}\n\nfunction userSettingsPath(paths: SettingsResolutionPaths): string {\n return (\n paths.userSettingsPath ??\n (paths.home\n ? join(paths.home, '.book', 'settings.json')\n : join(resolveBookHome(), 'settings.json'))\n );\n}\n\n/**\n * Whether two paths name one file. The lexical check covers a file that does not\n * exist yet; a link to the file, or a workspace reached through a linked home\n * directory, needs the real paths.\n */\nfunction sameFile(left: string, right: string): boolean {\n if (resolve(left) === resolve(right)) return true;\n try {\n return realpathSync.native(left) === realpathSync.native(right);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether `path` is the user settings file.\n *\n * A workspace at `$HOME` (with `BOOK_HOME` unset) has `~/.book/settings.json` as\n * its project settings file too. Read a second time as the repository layer, the\n * user's own file would add every accumulating rule twice, and its hooks, allow\n * rules and directories would be withheld as if a repository had declared them,\n * then registered twice once approved (#429). Every reader of the project layer\n * skips a file this returns true for.\n */\nexport function isUserSettingsFile(path: string, paths: SettingsResolutionPaths = {}): boolean {\n return sameFile(path, userSettingsPath(paths));\n}\n\nexport type { WorkspaceProviderGate };\n\nexport function emptyWorkspaceProviderGate(): WorkspaceProviderGate {\n return createWorkspaceProviderGate({ partition: emptyWorkspaceProviderPartition() });\n}\n\n/**\n * The advisor `off` a local layer wrote, which outranks a lower layer's advisor, is honoured only\n * while a checked-in workspace layer still declares one (`/advisor off` writes it for exactly\n * that case). Once the repository stops declaring an advisor, the `off` is stale and would keep\n * outranking the user's own, so the layer is read without it.\n */\nfunction withoutStaleAdvisorOff(\n local: BookSettings | null,\n project: BookSettings | null,\n): BookSettings | null {\n if (!local || project?.advisorModel !== undefined) return local;\n if (local.advisorModel?.trim().toLowerCase() !== ADVISOR_OFF_VALUE) return local;\n const rest = { ...local };\n delete rest.advisorModel;\n return rest;\n}\n\nexport function resolveSettings(\n workspace: string,\n overridePath?: string,\n paths: SettingsResolutionPaths = {},\n): ResolvedSettings {\n return resolveSettingsWithProviderGate(workspace, overridePath, paths).settings;\n}\n\n/** {@link resolveSettings}, plus what the provider gate withheld and dropped on the way. */\nexport function resolveSettingsWithProviderGate(\n workspace: string,\n overridePath?: string,\n paths: SettingsResolutionPaths = {},\n): { settings: ResolvedSettings; providerGate: WorkspaceProviderGate } {\n let resolved = structuredClone(DEFAULT_SETTINGS);\n\n // Every layer is read before any is merged: whether a workspace layer may speak for a provider\n // depends on which ids the *trusted* layers define, and the `--settings` layer merges last.\n // The read order is unchanged (user, project, local, `--settings`) and nothing merged between\n // two reads can throw, so the first broken file still names the error, as before.\n const userPath = userSettingsPath(paths);\n const user = loadSettingsFile(userPath);\n const projectPath = paths.projectSettingsPath ?? join(workspace, '.book', 'settings.json');\n // Skipped when it is the user file itself, as it is for a workspace at `$HOME` (#429).\n const project = sameFile(projectPath, userPath) ? null : loadSettingsFile(projectPath);\n const localPath = paths.localSettingsPath ?? join(workspace, '.book', 'settings.local.json');\n const local = withoutStaleAdvisorOff(loadSettingsFile(localPath), project);\n const override = overridePath ? loadSettingsFile(overridePath) : null;\n\n const trustedProviderIds = providerIdsOf([\n { providers: user?.provider },\n { providers: override?.provider },\n ]);\n const workspaceProviderLayers: ProviderLayer[] = [\n { path: projectPath, providers: project?.provider },\n { path: localPath, providers: local?.provider },\n ];\n // A workspace layer reaches `provider` only for ids a trusted layer defines, and only with\n // `models` and `type`. The rest is held back for the partition below.\n const gateProviders = (layer: BookSettings): BookSettings =>\n layer.provider\n ? {\n ...layer,\n provider: withoutWorkspaceProviderOverrides(\n layer.provider,\n trustedProviderIds,\n ) as BookSettings['provider'],\n }\n : layer;\n\n // Layer 1: User settings (~/.book/settings.json)\n if (user) resolved = mergeLayer(resolved, user, 'trusted');\n\n // Layer 2: Project settings (<workspace>/.book/settings.json).\n // `allow` rules from the repository layer are held back rather than merged:\n // they only widen authority, and the decisions that release them live in the\n // local layer, which has not been merged yet.\n const declaredProjectAllow = project?.permissions?.allow ?? [];\n // Project-declared hook entries are held back the same way: each one is a\n // shell command the repository would otherwise get Book to run.\n const declaredProjectHooks = collectDeclaredHooks(project);\n // `additionalDirectories` widens the roots the file tools serve, so a checked-in\n // declaration is held back for the same reason a project allow rule is: it\n // grants authority, and the decisions that release it live in the local layer,\n // which has not been merged yet. Released after the trust store loads, by real path.\n const declaredProjectDirectories = project?.additionalDirectories ?? [];\n // Released declarations belong between the user and local layers, matching the\n // relative merge order ungated layers still produce.\n const userHookCounts = Object.fromEntries(\n HOOK_EVENTS.map((event) => [event, resolved.hooks[event].length]),\n ) as Record<HookEvent, number>;\n if (project) {\n let withheld: BookSettings = project;\n if (declaredProjectAllow.length > 0) {\n withheld = { ...withheld, permissions: { ...withheld.permissions, allow: [] } };\n }\n if (declaredProjectDirectories.length > 0) {\n withheld = { ...withheld, additionalDirectories: [] };\n }\n if (declaredProjectHooks.length > 0) {\n withheld = { ...withheld, hooks: structuredClone(DEFAULT_SETTINGS.hooks) };\n }\n resolved = mergeLayer(resolved, gateProviders(withheld), 'repository');\n }\n\n // Layer 3: Local settings (<workspace>/.book/settings.local.json)\n if (local) resolved = mergeLayer(resolved, gateProviders(local), 'local');\n\n // Layer 4 (optional): Ad-hoc override (--settings flag)\n if (override) resolved = mergeLayer(resolved, override, 'trusted');\n\n // Trust decisions come from outside the workspace, so a repository cannot\n // ship its own. Layers the user controls may still carry them — the global\n // file and an explicit `--settings` path are trusted for everything else —\n // but the store has the final say on any key it records.\n const trust = loadWorkspaceTrust(\n workspace,\n paths.trustStorePath ?? defaultTrustStorePath(paths.home),\n );\n resolved.permissions.projectAllowRules = {\n ...resolved.permissions.projectAllowRules,\n ...trust.permissionAllowRules,\n };\n resolved.mcp.projectServers = { ...resolved.mcp.projectServers, ...trust.mcpServers };\n resolved.hooks.projectEntries = { ...resolved.hooks.projectEntries, ...trust.hookEntries };\n resolved.commands.projectCommands = {\n ...resolved.commands.projectCommands,\n ...trust.projectCommands,\n };\n resolved.projectDirectories = {\n ...resolved.projectDirectories,\n ...trust.projectDirectories,\n };\n resolved.workspaceProviders = {\n ...resolved.workspaceProviders,\n ...trust.workspaceProviders,\n };\n\n // Providers only a workspace layer defines: approved ones join `provider`, the rest are absent.\n // Their keys are never resolved here or anywhere downstream of an unapproved entry.\n const providerPartition = partitionWorkspaceProviders(\n workspaceProviderLayers,\n trustedProviderIds,\n resolved.workspaceProviders,\n );\n for (const { id, declaration } of providerPartition.approved) {\n resolved.provider[id] = declaration as BookSettings['provider'][string];\n }\n const withheldProviders = new Set(withheldProviderIds(providerPartition));\n // A `model`, `compactModel`, `advisorModel` or agent-profile `model` a workspace layer set for a\n // withheld provider gives way to what the trusted layers alone say.\n const selectionLayers: ModelSelectionLayer[] = [\n { path: userPath, trusted: true, selections: modelSelectionsOf(user ?? {}) },\n { path: projectPath, trusted: false, selections: modelSelectionsOf(project ?? {}) },\n { path: localPath, trusted: false, selections: modelSelectionsOf(local ?? {}) },\n { path: overridePath ?? '', trusted: true, selections: modelSelectionsOf(override ?? {}) },\n ];\n const withheldSelections = withheldModelSelections(selectionLayers, withheldProviders);\n for (const { key, fallback } of withheldSelections) {\n const profile = profileOfModelKey(key);\n if (profile === undefined) {\n const topLevel = key as 'model' | 'compactModel' | 'advisorModel';\n if (fallback === undefined) delete resolved[topLevel];\n else resolved[topLevel] = fallback;\n continue;\n }\n const entry = resolved.agents.profiles[profile];\n if (!entry) continue;\n if (fallback === undefined) delete entry.model;\n else entry.model = fallback;\n if (Object.keys(entry).length === 0) delete resolved.agents.profiles[profile];\n }\n\n // Every decision source is in place now, so the withheld repository rules can\n // be released — approved ones only.\n if (declaredProjectAllow.length > 0) {\n const { approved } = partitionProjectAllowRules(\n declaredProjectAllow,\n resolved.permissions?.projectAllowRules,\n );\n if (approved.length > 0) {\n resolved.permissions.allow = [...(resolved.permissions.allow ?? []), ...approved];\n }\n }\n\n if (declaredProjectDirectories.length > 0) {\n // Released by real path, not by the text the repository wrote: see `additional-roots.ts`.\n // Approved entries join the resolved list as their declared text, which is what the merge\n // above normalizes and deduplicates; the file tools resolve it again to the same place.\n const { approved } = partitionProjectDirectories(\n collectDeclaredDirectories(workspace, declaredProjectDirectories),\n resolved.projectDirectories,\n );\n if (approved.length > 0) {\n // The approved **real path**, never the declared text. A consumer resolves this list again\n // later with no trust store in hand (see `resolveAdditionalRoots`), so a relative or\n // symlinked spelling would let a relink move a root the user approved as somewhere else.\n // The user saw the real path when the prompt named it; the repository's spelling is what\n // would be dangerous to re-resolve (PR #334 finding 7).\n resolved.additionalDirectories = [\n ...(resolved.additionalDirectories ?? []),\n ...approved.map((directory) => directory.realPath),\n ];\n }\n }\n\n if (declaredProjectHooks.length > 0) {\n const { approved } = partitionProjectHooks(\n declaredProjectHooks,\n resolved.hooks?.projectEntries,\n );\n const approvedByEvent = new Map<HookEvent, HookEntry[]>();\n for (const { event, entry } of approved) {\n const entries = approvedByEvent.get(event) ?? [];\n entries.push(entry);\n approvedByEvent.set(event, entries);\n }\n for (const [event, entries] of approvedByEvent) {\n resolved.hooks[event].splice(userHookCounts[event], 0, ...entries);\n }\n }\n\n const settings = bookSettingsSchema.parse(resolved) as ResolvedSettings;\n const providerGate = createWorkspaceProviderGate({\n partition: providerPartition,\n droppedOverrides: droppedProviderOverrides(workspaceProviderLayers, trustedProviderIds),\n withheldSelections,\n unaffectedSelections: unaffectedModelSelections(selectionLayers, withheldProviders),\n });\n return { settings, providerGate };\n}\n\n/**\n * The environment's say over the merged layers, applied to settings that are\n * about to be used rather than saved.\n *\n * `BOOK_STARTUP_ANIMATION` is here, and not in `loadConfig`, because the layers\n * are only half the answer: a reader that resolved the files and stopped would\n * report the file's value while Book acts on the variable's. So every path that\n * produces the *effective* settings calls this — the startup config, the\n * re-read after a provider is removed, `book config get`/`list`. A path that\n * computes what gets written back to a settings file must not: the variable\n * decides the next launch, and baking it into a file would make a later\n * `unset` look like it had failed.\n *\n * The returned object is a new one; the input is left as the caller resolved it.\n */\nexport function applySettingsEnvOverrides(\n settings: ResolvedSettings,\n env: NodeJS.ProcessEnv = process.env,\n): ResolvedSettings {\n const startupAnimation = parseEnvBoolean(env.BOOK_STARTUP_ANIMATION);\n if (startupAnimation === undefined) return settings;\n return { ...settings, ui: { ...settings.ui, startupAnimation } };\n}\n\n/**\n * What to tell a user whose read or save is being decided by the environment\n * rather than by their settings — the `BOOK_MODEL` warning in one sentence, or\n * nothing when the variable is unset or unreadable.\n */\nexport function startupAnimationEnvNote(env: NodeJS.ProcessEnv = process.env): string | undefined {\n const raw = env.BOOK_STARTUP_ANIMATION;\n if (parseEnvBoolean(raw) === undefined) return undefined;\n return (\n `BOOK_STARTUP_ANIMATION is set to \"${raw}\" — it decides the splash at every ` +\n 'launch, so ui.startupAnimation will not apply while it is set.'\n );\n}\n\nexport { loadSettingsFile };\n\n/**\n * Migrate rules from the legacy ~/.book/permissions.json into the local\n * settings file (<workspace>/.book/settings.local.json). Runs once on first\n * load when the legacy file exists and the local settings don't have rules yet.\n *\n * @param home - User home directory; injectable for isolated migration tests\n * @param validatedSettings - Already-resolved settings from the startup preflight\n * @returns true if migration occurred, false otherwise\n */\nexport function migrateLegacyPermissions(\n workspace: string,\n home?: string,\n validatedSettings?: ResolvedSettings,\n): boolean {\n // Direct callers must observe the same fail-before-storage boundary as the\n // normal startup path. Internal callers pass already-resolved settings to\n // avoid reading the layers twice on the common off path.\n if (!validatedSettings) {\n resolveSettings(workspace, undefined, home ? { home } : undefined);\n }\n\n const legacyPath = join(home ? join(home, '.book') : resolveBookHome(), 'permissions.json');\n if (!existsSync(legacyPath)) return false;\n const markerPath = join(workspace, '.book', 'migrations.json');\n const localPath = join(workspace, '.book', 'settings.local.json');\n // Before the marker is read: it is one of the files this writes.\n for (const path of [markerPath, localPath]) {\n const refusal = projectFileWriteRefusal(path);\n if (refusal) {\n console.warn(`⚠ Permission rules not migrated: ${path}: ${refusal}.`);\n return false;\n }\n }\n try {\n const marker = JSON.parse(readFileSync(markerPath, 'utf-8')) as {\n legacyPermissions?: number;\n };\n if ((marker.legacyPermissions ?? 0) >= LEGACY_PERMISSIONS_MIGRATION_VERSION) return false;\n } catch {\n // Missing or malformed markers are safely rebuilt after a successful migration.\n }\n\n let legacyRules: Array<{ toolName: string; pattern?: string; effect: string }> = [];\n try {\n const raw = readFileSync(legacyPath, 'utf-8');\n const parsed = JSON.parse(raw) as {\n rules: Array<{ toolName: string; pattern?: string; effect: string }>;\n };\n legacyRules = parsed.rules ?? [];\n } catch {\n return false; // corrupt — leave the legacy file alone\n }\n\n if (legacyRules.length === 0) return false;\n\n const result = new SettingsRepository(localPath).update((existing) => {\n const permissions = (existing.permissions ?? {}) as {\n allow?: string[];\n ask?: string[];\n deny?: string[];\n };\n for (const key of ['allow', 'ask', 'deny'] as const) {\n if (!Array.isArray(permissions[key])) permissions[key] = [];\n }\n\n for (const rule of legacyRules) {\n const specifier = rule.pattern ? `${rule.toolName}(${rule.pattern})` : rule.toolName;\n const effect = rule.effect as 'allow' | 'ask' | 'deny';\n if (!permissions[effect]!.includes(specifier)) permissions[effect]!.push(specifier);\n }\n existing.permissions = permissions;\n });\n if (!result.ok) return false;\n\n writeFileAtomic(\n markerPath,\n `${JSON.stringify({ legacyPermissions: LEGACY_PERMISSIONS_MIGRATION_VERSION }, null, 2)}\\n`,\n );\n if (!result.changed) return false;\n\n console.warn(\n `⚠ Migrated ${legacyRules.length} permission rule(s) from ~/.book/permissions.json to ${localPath}. ` +\n 'Delete the legacy file after verifying the migration.',\n );\n return true;\n}\n","/**\n * The one grammar for a boolean environment variable: `1`/`true`/`on`/`yes` and\n * `0`/`false`/`off`/`no`, case-insensitive and trimmed, and undefined for\n * anything else.\n *\n * Tri-state on purpose. A flag that reads \"unset\" as \"off\" cannot tell a caller\n * that wants the default from one that was never told, and every such caller\n * then hard-codes a second grammar beside it. `BOOK_STARTUP_ANIMATION` needs all\n * three: it has to outrank a settings file when set, leave that file alone when\n * not, and say nothing rather than guess when the value is a typo — a wrong guess\n * either delays the first render or hides the input bar a script is waiting for.\n */\nexport function parseEnvBoolean(raw: string | undefined): boolean | undefined {\n switch (raw?.trim().toLowerCase()) {\n case '0':\n case 'false':\n case 'off':\n case 'no':\n return false;\n case '1':\n case 'true':\n case 'on':\n case 'yes':\n return true;\n default:\n return undefined;\n }\n}\n"],"mappings":"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","names":[]}
|