javi-forge 1.28.1 → 1.30.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/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +828 -0
- package/assets/claude-hooks/manifest.json +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/lib/__fixtures__/claude-hook-ownership.d.ts +112 -0
- package/dist/lib/__fixtures__/claude-hook-ownership.js +96 -0
- package/dist/lib/__fixtures__/fake-secure-fs.d.ts +43 -0
- package/dist/lib/__fixtures__/fake-secure-fs.js +183 -0
- package/dist/lib/claude-hook-manager.d.ts +102 -0
- package/dist/lib/claude-hook-manager.js +490 -0
- package/dist/lib/claude-hook-settings.d.ts +188 -0
- package/dist/lib/claude-hook-settings.js +416 -0
- package/dist/lib/secure-fs-posix.d.ts +37 -0
- package/dist/lib/secure-fs-posix.js +302 -0
- package/dist/lib/secure-fs-transaction.d.ts +126 -0
- package/dist/lib/secure-fs-transaction.js +239 -0
- package/package.json +1 -1
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure settings-entry ownership recognition for the SkillGuard Claude
|
|
3
|
+
* PreToolUse guard (Slice 2). Every function takes already-parsed JS values and
|
|
4
|
+
* returns values — ZERO filesystem access (all bytes-on-disk access lives in
|
|
5
|
+
* `claude-hook-manager.ts`). It covers protocol-shape validation, deterministic
|
|
6
|
+
* canonical identity (Decision ②: the live asset SHA token is normalized out
|
|
7
|
+
* before hashing), exact v0 legacy recognition by deep structural equality, and
|
|
8
|
+
* removal/merge PLANNING only. Identity is always recomputed from observed
|
|
9
|
+
* structure; a marker only claims ownership, it never proves it.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { ASSET_SHA_PLACEHOLDER, LEGACY_COHORT, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, } from "./__fixtures__/claude-hook-ownership.js";
|
|
13
|
+
export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
|
|
14
|
+
// Shape helpers
|
|
15
|
+
export function isPlainObject(value) {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A valid settings container is an object; when `hooks` is present it MUST be
|
|
20
|
+
* an object; when `hooks.PreToolUse` is present it MUST be an array. Anything
|
|
21
|
+
* else is malformed.
|
|
22
|
+
*/
|
|
23
|
+
export function validateSettingsShape(parsed) {
|
|
24
|
+
if (!isPlainObject(parsed))
|
|
25
|
+
return false;
|
|
26
|
+
const { hooks } = parsed;
|
|
27
|
+
if (hooks !== undefined && !isPlainObject(hooks))
|
|
28
|
+
return false;
|
|
29
|
+
const pre = isPlainObject(hooks) ? hooks.PreToolUse : undefined;
|
|
30
|
+
if (pre !== undefined && !Array.isArray(pre))
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
// Canonical identity (Decision ②)
|
|
35
|
+
const ASSET_SHA_TOKEN = /^[0-9a-f]{64}$/;
|
|
36
|
+
const VERSION_PATTERN = /^javi-forge-global-pretooluse:v(\d+):sha256:/;
|
|
37
|
+
/**
|
|
38
|
+
* Replace the trailing 64-hex asset SHA token in a managed `statusMessage` with
|
|
39
|
+
* the fixed placeholder so settings identity is invariant under asset rotation.
|
|
40
|
+
* A non-managed or malformed token is returned unchanged so it hashes to a
|
|
41
|
+
* distinct (non-current) value.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeStatusMessage(statusMessage) {
|
|
44
|
+
if (typeof statusMessage !== "string")
|
|
45
|
+
return "";
|
|
46
|
+
if (!statusMessage.startsWith(MANAGED_STATUS_PREFIX))
|
|
47
|
+
return statusMessage;
|
|
48
|
+
const token = statusMessage.slice(MANAGED_STATUS_PREFIX.length);
|
|
49
|
+
return ASSET_SHA_TOKEN.test(token)
|
|
50
|
+
? `${MANAGED_STATUS_PREFIX}${ASSET_SHA_PLACEHOLDER}`
|
|
51
|
+
: statusMessage;
|
|
52
|
+
}
|
|
53
|
+
/** Parse the marker version (`…:v1:…` → 1) for diagnostics only. */
|
|
54
|
+
export function parseVersionFromStatus(statusMessage) {
|
|
55
|
+
if (typeof statusMessage !== "string")
|
|
56
|
+
return undefined;
|
|
57
|
+
const match = VERSION_PATTERN.exec(statusMessage);
|
|
58
|
+
return match ? Number(match[1]) : undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Canonicalize the marker-proven handler group into deterministic bytes and its
|
|
62
|
+
* SHA-256. Fixed key order (`type,command,args,timeout,statusMessage` inside a
|
|
63
|
+
* `matcher,hooks` group), the asset-SHA token normalized out, two-space indent
|
|
64
|
+
* plus a trailing newline. Only the single handler participates.
|
|
65
|
+
*/
|
|
66
|
+
export function canonicalizeSettingsEntry(group, handler) {
|
|
67
|
+
const h = isPlainObject(handler) ? handler : {};
|
|
68
|
+
const g = isPlainObject(group) ? group : {};
|
|
69
|
+
const canonicalHandler = {
|
|
70
|
+
type: h.type,
|
|
71
|
+
command: h.command,
|
|
72
|
+
args: h.args,
|
|
73
|
+
timeout: h.timeout,
|
|
74
|
+
statusMessage: normalizeStatusMessage(h.statusMessage),
|
|
75
|
+
};
|
|
76
|
+
const canonicalGroup = { matcher: g.matcher, hooks: [canonicalHandler] };
|
|
77
|
+
const serialization = `${JSON.stringify(canonicalGroup, null, 2)}\n`;
|
|
78
|
+
const canonicalSha256 = createHash("sha256")
|
|
79
|
+
.update(serialization, "utf8")
|
|
80
|
+
.digest("hex");
|
|
81
|
+
return { serialization, canonicalSha256 };
|
|
82
|
+
}
|
|
83
|
+
// Legacy recognition (deep structural equality only)
|
|
84
|
+
/**
|
|
85
|
+
* Order-sensitive for arrays, key-set-exact (order-insensitive) for objects,
|
|
86
|
+
* strict for scalars. No substring, normalization, or tolerance.
|
|
87
|
+
*/
|
|
88
|
+
export function deepStructuralEqual(a, b) {
|
|
89
|
+
if (a === b)
|
|
90
|
+
return true;
|
|
91
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
92
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return a.every((item, index) => deepStructuralEqual(item, b[index]));
|
|
96
|
+
}
|
|
97
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
98
|
+
const keysA = Object.keys(a);
|
|
99
|
+
const keysB = Object.keys(b);
|
|
100
|
+
if (keysA.length !== keysB.length)
|
|
101
|
+
return false;
|
|
102
|
+
return keysA.every((key) => Object.hasOwn(b, key) && deepStructuralEqual(a[key], b[key]));
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
function countDeepMatches(target, list) {
|
|
107
|
+
let count = 0;
|
|
108
|
+
for (const item of list)
|
|
109
|
+
if (deepStructuralEqual(target, item))
|
|
110
|
+
count++;
|
|
111
|
+
return count;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* In-object legacy recognition: exactly one deep-equal match for each of the
|
|
115
|
+
* four cohort objects (L1–L3 Pre, L4 Post) is `exact-legacy`; a partial,
|
|
116
|
+
* duplicate, or edited cohort is `foreign` (partial-legacy); unrecognized
|
|
117
|
+
* PreToolUse handler content is `foreign` per spec R2; only a container with no
|
|
118
|
+
* PreToolUse handler content is `absent` (installable). Whole-file SHA legacy is
|
|
119
|
+
* decided by the manager, which holds the raw bytes. (Spec R2 overrides
|
|
120
|
+
* design.md Algorithm C, which returns `absent` for the no-cohort fallthrough
|
|
121
|
+
* and so cannot flag a resembling unmarked handler.)
|
|
122
|
+
*/
|
|
123
|
+
export function classifyLegacy(parsed) {
|
|
124
|
+
const hooks = isPlainObject(parsed) ? parsed.hooks : undefined;
|
|
125
|
+
const pre = isPlainObject(hooks) && Array.isArray(hooks.PreToolUse)
|
|
126
|
+
? hooks.PreToolUse
|
|
127
|
+
: [];
|
|
128
|
+
const post = isPlainObject(hooks) && Array.isArray(hooks.PostToolUse)
|
|
129
|
+
? hooks.PostToolUse
|
|
130
|
+
: [];
|
|
131
|
+
const counts = [
|
|
132
|
+
countDeepMatches(LEGACY_COHORT.L1, pre),
|
|
133
|
+
countDeepMatches(LEGACY_COHORT.L2, pre),
|
|
134
|
+
countDeepMatches(LEGACY_COHORT.L3, pre),
|
|
135
|
+
countDeepMatches(LEGACY_COHORT.L4, post),
|
|
136
|
+
];
|
|
137
|
+
if (counts.every((count) => count === 1)) {
|
|
138
|
+
return { state: "exact-legacy", detail: "cohort" };
|
|
139
|
+
}
|
|
140
|
+
if (counts.some((count) => count >= 1)) {
|
|
141
|
+
return { state: "foreign", detail: "partial-legacy" };
|
|
142
|
+
}
|
|
143
|
+
const preHandlers = pre.reduce((total, group) => total +
|
|
144
|
+
(isPlainObject(group) && Array.isArray(group.hooks)
|
|
145
|
+
? group.hooks.length
|
|
146
|
+
: 0), 0);
|
|
147
|
+
if (preHandlers > 0)
|
|
148
|
+
return { state: "foreign", detail: "no-marker" };
|
|
149
|
+
return { state: "absent" };
|
|
150
|
+
}
|
|
151
|
+
function findManagedMarkers(groups) {
|
|
152
|
+
const markers = [];
|
|
153
|
+
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
154
|
+
const group = groups[groupIndex];
|
|
155
|
+
const handlers = isPlainObject(group) && Array.isArray(group.hooks) ? group.hooks : [];
|
|
156
|
+
for (let handlerIndex = 0; handlerIndex < handlers.length; handlerIndex++) {
|
|
157
|
+
const handler = handlers[handlerIndex];
|
|
158
|
+
if (isPlainObject(handler) &&
|
|
159
|
+
typeof handler.statusMessage === "string" &&
|
|
160
|
+
handler.statusMessage.startsWith(MANAGED_STATUS_PREFIX)) {
|
|
161
|
+
markers.push({ groupIndex, handlerIndex, group, handler });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return markers;
|
|
166
|
+
}
|
|
167
|
+
function isValidMatcherGroup(group) {
|
|
168
|
+
return (isPlainObject(group) &&
|
|
169
|
+
typeof group.matcher === "string" &&
|
|
170
|
+
Array.isArray(group.hooks));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Reduce a parsed settings container to one component state, from parsed
|
|
174
|
+
* structure only. Shape validation first, then marker-driven identity (always
|
|
175
|
+
* recomputed), then the legacy fallback when no marker is present.
|
|
176
|
+
*/
|
|
177
|
+
export function classifySettingsEntry(parsed, _currentAssetSha, identities) {
|
|
178
|
+
if (!validateSettingsShape(parsed))
|
|
179
|
+
return { state: "malformed" };
|
|
180
|
+
const hooks = isPlainObject(parsed) ? parsed.hooks : undefined;
|
|
181
|
+
const groups = isPlainObject(hooks) && Array.isArray(hooks.PreToolUse)
|
|
182
|
+
? hooks.PreToolUse
|
|
183
|
+
: [];
|
|
184
|
+
const markers = findManagedMarkers(groups);
|
|
185
|
+
if (markers.length > 1) {
|
|
186
|
+
return { state: "edited-managed", detail: "multiple markers" };
|
|
187
|
+
}
|
|
188
|
+
if (markers.length === 0)
|
|
189
|
+
return classifyLegacy(parsed);
|
|
190
|
+
const marker = markers[0];
|
|
191
|
+
if (!isValidMatcherGroup(marker.group)) {
|
|
192
|
+
return { state: "edited-managed", detail: "marker in invalid container" };
|
|
193
|
+
}
|
|
194
|
+
const { canonicalSha256 } = canonicalizeSettingsEntry(marker.group, marker.handler);
|
|
195
|
+
const version = parseVersionFromStatus(marker.handler.statusMessage);
|
|
196
|
+
const base = {
|
|
197
|
+
version,
|
|
198
|
+
canonicalSha256,
|
|
199
|
+
groupIndex: marker.groupIndex,
|
|
200
|
+
handlerIndex: marker.handlerIndex,
|
|
201
|
+
};
|
|
202
|
+
if (identities.current &&
|
|
203
|
+
canonicalSha256 === identities.current.canonicalSha256) {
|
|
204
|
+
return { state: "managed-current", ...base };
|
|
205
|
+
}
|
|
206
|
+
if (identities.historical.some((entry) => entry.canonicalSha256 === canonicalSha256)) {
|
|
207
|
+
return { state: "released-outdated", ...base };
|
|
208
|
+
}
|
|
209
|
+
return { state: "edited-managed", ...base };
|
|
210
|
+
}
|
|
211
|
+
function groupHandlerCount(parsed, groupIndex) {
|
|
212
|
+
const hooks = isPlainObject(parsed) ? parsed.hooks : undefined;
|
|
213
|
+
const groups = isPlainObject(hooks) && Array.isArray(hooks.PreToolUse)
|
|
214
|
+
? hooks.PreToolUse
|
|
215
|
+
: [];
|
|
216
|
+
const group = groups[groupIndex];
|
|
217
|
+
return isPlainObject(group) && Array.isArray(group.hooks)
|
|
218
|
+
? group.hooks.length
|
|
219
|
+
: 0;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Plan removal of the marker-proven managed handler. Only a recognized managed
|
|
223
|
+
* identity (`managed-current` or `released-outdated`) is eligible; foreign,
|
|
224
|
+
* partial-legacy, edited-managed, and every non-regular state refuse.
|
|
225
|
+
*/
|
|
226
|
+
export function planManagedClaudeHookRemoval(parsed, currentAssetSha, identities) {
|
|
227
|
+
const cls = classifySettingsEntry(parsed, currentAssetSha, identities);
|
|
228
|
+
if (cls.state === "managed-current" || cls.state === "released-outdated") {
|
|
229
|
+
const handlerCount = groupHandlerCount(parsed, cls.groupIndex ?? -1);
|
|
230
|
+
const preservedSiblings = Math.max(handlerCount - 1, 0);
|
|
231
|
+
return {
|
|
232
|
+
refused: false,
|
|
233
|
+
state: cls.state,
|
|
234
|
+
groupIndex: cls.groupIndex,
|
|
235
|
+
handlerIndex: cls.handlerIndex,
|
|
236
|
+
removeGroup: preservedSiblings === 0,
|
|
237
|
+
preservedSiblings,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
refused: true,
|
|
242
|
+
reason: `refuse removal for state ${cls.state}`,
|
|
243
|
+
state: cls.state,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Plan the managed merge. `absent` installs a new group, `managed-current` is a
|
|
248
|
+
* no-op, `released-outdated` replaces the marked handler in place (siblings
|
|
249
|
+
* preserved), and every other state refuses (edited/force is a Slice-3 concern).
|
|
250
|
+
*/
|
|
251
|
+
export function planManagedClaudeHookMerge(parsed, currentAssetSha, identities) {
|
|
252
|
+
const cls = classifySettingsEntry(parsed, currentAssetSha, identities);
|
|
253
|
+
if (cls.state === "absent") {
|
|
254
|
+
return { refused: false, state: cls.state, action: "install" };
|
|
255
|
+
}
|
|
256
|
+
if (cls.state === "managed-current") {
|
|
257
|
+
return {
|
|
258
|
+
refused: false,
|
|
259
|
+
state: cls.state,
|
|
260
|
+
action: "noop",
|
|
261
|
+
groupIndex: cls.groupIndex,
|
|
262
|
+
handlerIndex: cls.handlerIndex,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (cls.state === "released-outdated") {
|
|
266
|
+
const preservedSiblings = Math.max(groupHandlerCount(parsed, cls.groupIndex ?? -1) - 1, 0);
|
|
267
|
+
return {
|
|
268
|
+
refused: false,
|
|
269
|
+
state: cls.state,
|
|
270
|
+
action: "replace",
|
|
271
|
+
groupIndex: cls.groupIndex,
|
|
272
|
+
handlerIndex: cls.handlerIndex,
|
|
273
|
+
preservedSiblings,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
refused: true,
|
|
278
|
+
reason: `refuse merge for state ${cls.state}`,
|
|
279
|
+
state: cls.state,
|
|
280
|
+
action: "refuse",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
// Slice-3a write-plan helpers (Decision 8) — pure, reuse the module primitives.
|
|
284
|
+
/** The managed timeout, matching the fixture handler shape. */
|
|
285
|
+
const MANAGED_TIMEOUT = 30;
|
|
286
|
+
function preToolUseArray(parsed) {
|
|
287
|
+
const hooks = isPlainObject(parsed) ? parsed.hooks : undefined;
|
|
288
|
+
return isPlainObject(hooks) && Array.isArray(hooks.PreToolUse)
|
|
289
|
+
? hooks.PreToolUse
|
|
290
|
+
: [];
|
|
291
|
+
}
|
|
292
|
+
function postToolUseArray(parsed) {
|
|
293
|
+
const hooks = isPlainObject(parsed) ? parsed.hooks : undefined;
|
|
294
|
+
return isPlainObject(hooks) && Array.isArray(hooks.PostToolUse)
|
|
295
|
+
? hooks.PostToolUse
|
|
296
|
+
: [];
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Plan excision of the proven four-object legacy cohort from an embedded
|
|
300
|
+
* container. Reuses `LEGACY_COHORT` + `deepStructuralEqual` (the same primitives
|
|
301
|
+
* `classifyLegacy` uses) and never re-derives the classifier. Returns the exact
|
|
302
|
+
* ascending object indices to remove per event plus the append position for the
|
|
303
|
+
* freshly built managed group; every non-cohort sibling is preserved by index.
|
|
304
|
+
* Only an exact-legacy cohort is eligible — a partial/edited cohort refuses
|
|
305
|
+
* (the classifier already routes those to `foreign`).
|
|
306
|
+
*/
|
|
307
|
+
export function planLegacyCohortExcision(parsed) {
|
|
308
|
+
const cls = classifyLegacy(parsed);
|
|
309
|
+
if (cls.state !== "exact-legacy") {
|
|
310
|
+
return {
|
|
311
|
+
refused: true,
|
|
312
|
+
reason: `refuse cohort excision for state ${cls.state}`,
|
|
313
|
+
removePreIndices: [],
|
|
314
|
+
removePostIndices: [],
|
|
315
|
+
insertPreAt: 0,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const pre = preToolUseArray(parsed);
|
|
319
|
+
const post = postToolUseArray(parsed);
|
|
320
|
+
const removePreIndices = [];
|
|
321
|
+
for (const member of [LEGACY_COHORT.L1, LEGACY_COHORT.L2, LEGACY_COHORT.L3]) {
|
|
322
|
+
const index = pre.findIndex((item) => deepStructuralEqual(member, item));
|
|
323
|
+
if (index >= 0)
|
|
324
|
+
removePreIndices.push(index);
|
|
325
|
+
}
|
|
326
|
+
const removePostIndices = [];
|
|
327
|
+
const l4Index = post.findIndex((item) => deepStructuralEqual(LEGACY_COHORT.L4, item));
|
|
328
|
+
if (l4Index >= 0)
|
|
329
|
+
removePostIndices.push(l4Index);
|
|
330
|
+
removePreIndices.sort((a, b) => a - b);
|
|
331
|
+
removePostIndices.sort((a, b) => a - b);
|
|
332
|
+
return {
|
|
333
|
+
refused: false,
|
|
334
|
+
removePreIndices,
|
|
335
|
+
removePostIndices,
|
|
336
|
+
// The managed group appends after every surviving Pre sibling.
|
|
337
|
+
insertPreAt: pre.length - removePreIndices.length,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Plan an in-place force replacement of the single marker-proven managed
|
|
342
|
+
* handler for an `edited-managed` component. Eligibility (§324 / JD-A-001):
|
|
343
|
+
* - matcherExact === true → eligible regardless of siblingHandlers.
|
|
344
|
+
* - matcherExact === false && siblingHandlers === 0 → eligible.
|
|
345
|
+
* - matcherExact === false && siblingHandlers > 0 → refused even under force.
|
|
346
|
+
* A container without exactly one marker-proven handler in a valid matcher
|
|
347
|
+
* group also refuses.
|
|
348
|
+
*/
|
|
349
|
+
export function planForceReplace(parsed, currentAssetSha) {
|
|
350
|
+
// Empty identities force the marker-proven state to resolve as edited-managed
|
|
351
|
+
// while still exposing groupIndex/handlerIndex from the marker finder.
|
|
352
|
+
const cls = classifySettingsEntry(parsed, currentAssetSha, {
|
|
353
|
+
current: null,
|
|
354
|
+
historical: [],
|
|
355
|
+
});
|
|
356
|
+
if (cls.state !== "edited-managed" ||
|
|
357
|
+
cls.groupIndex === undefined ||
|
|
358
|
+
cls.handlerIndex === undefined) {
|
|
359
|
+
return {
|
|
360
|
+
refused: true,
|
|
361
|
+
reason: `refuse force replace for state ${cls.state}`,
|
|
362
|
+
state: cls.state,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const groups = preToolUseArray(parsed);
|
|
366
|
+
const group = groups[cls.groupIndex];
|
|
367
|
+
const matcherExact = isPlainObject(group) && group.matcher === MANAGED_MATCHER;
|
|
368
|
+
const handlerCount = isPlainObject(group) && Array.isArray(group.hooks) ? group.hooks.length : 0;
|
|
369
|
+
const siblingHandlers = Math.max(handlerCount - 1, 0);
|
|
370
|
+
if (!matcherExact && siblingHandlers > 0) {
|
|
371
|
+
return {
|
|
372
|
+
refused: true,
|
|
373
|
+
reason: "refuse force replace: edited matcher with unrelated sibling handlers (§324)",
|
|
374
|
+
state: cls.state,
|
|
375
|
+
groupIndex: cls.groupIndex,
|
|
376
|
+
handlerIndex: cls.handlerIndex,
|
|
377
|
+
matcherExact,
|
|
378
|
+
siblingHandlers,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
refused: false,
|
|
383
|
+
state: cls.state,
|
|
384
|
+
groupIndex: cls.groupIndex,
|
|
385
|
+
handlerIndex: cls.handlerIndex,
|
|
386
|
+
matcherExact,
|
|
387
|
+
siblingHandlers,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Synthesize a fresh managed-only container for the two terminal states with no
|
|
392
|
+
* parsed value: fresh install (`absent`) and whole-file `exact-legacy`. Takes
|
|
393
|
+
* only the current asset SHA — not the whole Manifest — so this module never
|
|
394
|
+
* imports the manager's manifest reader at runtime (JD-A-002).
|
|
395
|
+
*/
|
|
396
|
+
export function buildManagedContainer(currentAssetSha) {
|
|
397
|
+
return {
|
|
398
|
+
hooks: {
|
|
399
|
+
PreToolUse: [
|
|
400
|
+
{
|
|
401
|
+
matcher: MANAGED_MATCHER,
|
|
402
|
+
hooks: [
|
|
403
|
+
{
|
|
404
|
+
type: "command",
|
|
405
|
+
command: "node",
|
|
406
|
+
args: [MANAGED_ASSET_ARG],
|
|
407
|
+
timeout: MANAGED_TIMEOUT,
|
|
408
|
+
statusMessage: `${MANAGED_STATUS_PREFIX}${currentAssetSha}`,
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
//# sourceMappingURL=claude-hook-settings.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POSIX `PlatformSecureFs` adapters for the SkillGuard transactional installer
|
|
3
|
+
* (Slice 3a). This is the ONLY place a shell tool runs (`getfacl` on Linux,
|
|
4
|
+
* `/bin/ls -lde` on macOS) and the ONLY place `os`/`fs` ownership bits are
|
|
5
|
+
* interpreted. Everything fails closed: a missing/erroring/timed-out ACL tool,
|
|
6
|
+
* an extended/named/mask/default/inherited ACL entry, a foreign-owned or
|
|
7
|
+
* group/other-writable directory, or any inconclusive result refuses rather than
|
|
8
|
+
* degrading to a weaker mode-only write. It never strips an ACL.
|
|
9
|
+
*/
|
|
10
|
+
import type { PlatformSecureFs, SecureResult } from "./secure-fs-transaction.js";
|
|
11
|
+
/** Outcome of a bounded, locale-C spawn of an ACL inspection tool. */
|
|
12
|
+
export interface SpawnOutcome {
|
|
13
|
+
/** The executable could not be spawned (e.g. ENOENT). */
|
|
14
|
+
spawnError?: boolean;
|
|
15
|
+
/** The call exceeded the bounded timeout. */
|
|
16
|
+
timedOut?: boolean;
|
|
17
|
+
/** Exit code, or null when it never exited cleanly. */
|
|
18
|
+
code: number | null;
|
|
19
|
+
stdout: string;
|
|
20
|
+
}
|
|
21
|
+
export type SpawnFn = (cmd: string, args: string[]) => Promise<SpawnOutcome>;
|
|
22
|
+
/** The bounded ACL prover behind each platform adapter. */
|
|
23
|
+
export interface PosixAclAdapter {
|
|
24
|
+
/** Run the bounded, LC_ALL=C ACL tool and decide clean|extended|inconclusive. */
|
|
25
|
+
proveClean(target: string): Promise<SecureResult<void>>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createLinuxAclAdapter(spawn?: SpawnFn): PosixAclAdapter;
|
|
28
|
+
export declare function createMacosAclAdapter(spawn?: SpawnFn): PosixAclAdapter;
|
|
29
|
+
export declare function createPosixSecureFs(acl: PosixAclAdapter): PlatformSecureFs;
|
|
30
|
+
/**
|
|
31
|
+
* Select the POSIX secure filesystem for the host platform. Linux uses the
|
|
32
|
+
* `getfacl` adapter, macOS uses `/bin/ls -lde`; Windows and every other platform
|
|
33
|
+
* return `null` so the manager refuses with `windows-secure-object-unavailable`
|
|
34
|
+
* and mutates nothing (Slice 3b implements Windows).
|
|
35
|
+
*/
|
|
36
|
+
export declare function selectSecureFs(platform?: NodeJS.Platform): PlatformSecureFs | null;
|
|
37
|
+
//# sourceMappingURL=secure-fs-posix.d.ts.map
|