@hasna/skills 0.10.83 → 0.10.89
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/README.md +166 -15
- package/bin/index.js +5829 -2913
- package/bin/mcp.js +1766 -835
- package/bin/migrate.js +1 -1
- package/bin/server.js +1 -1
- package/bin/worker.js +1 -1
- package/dist/cli/commands/agent-integration.d.ts +55 -0
- package/dist/cli/commands/context.d.ts +8 -0
- package/dist/cli/commands/runtime-launcher.d.ts +4 -9
- package/dist/index.js +3039 -1531
- package/dist/lib/agent-adapters.d.ts +5 -2
- package/dist/lib/agent-discovery.d.ts +22 -4
- package/dist/lib/agent-hermes.d.ts +7 -1
- package/dist/lib/agent-integration.d.ts +31 -2
- package/dist/lib/agent-opencode.d.ts +4 -0
- package/dist/lib/claude-hook-disable.d.ts +32 -0
- package/dist/lib/claude-settings-witness.d.ts +28 -0
- package/dist/lib/codex-settings-witness.d.ts +20 -0
- package/dist/lib/guarded-agent-config.d.ts +3 -0
- package/dist/lib/hook-diagnostics.d.ts +3 -1
- package/dist/lib/narrowed-witness-ledger.d.ts +24 -0
- package/dist/lib/native-discovery-roots.d.ts +8 -0
- package/dist/lib/native-drift-receipt.d.ts +55 -0
- package/dist/lib/opencode-hook-protocol.d.ts +5 -0
- package/dist/lib/profile-client.d.ts +1 -0
- package/dist/lib/selection-cache.d.ts +47 -2
- package/dist/lib/selection-resolver.d.ts +13 -1
- package/dist/lib/session-background-renewal.d.ts +118 -0
- package/dist/lib/session-reconciliation.d.ts +82 -2
- package/dist/lib/skill-context.d.ts +50 -1
- package/dist/lib/skill-relevance.d.ts +95 -0
- package/dist/runtime-prerequisites.js +570 -165
- package/dist/sdk/index.js +2629 -1396
- package/docs/plugin-admission.md +132 -3
- package/package.json +1 -1
|
@@ -49,7 +49,7 @@ var __require = import.meta.require;
|
|
|
49
49
|
// src/runtime-prerequisites.ts
|
|
50
50
|
import { isAbsolute as isAbsolute7 } from "path";
|
|
51
51
|
// package.json
|
|
52
|
-
var version = "0.10.
|
|
52
|
+
var version = "0.10.89";
|
|
53
53
|
var package_default = {
|
|
54
54
|
name: "@hasna/skills",
|
|
55
55
|
version,
|
|
@@ -191,10 +191,218 @@ var package_default = {
|
|
|
191
191
|
};
|
|
192
192
|
|
|
193
193
|
// src/lib/agent-install-selection.ts
|
|
194
|
-
import { lstatSync as
|
|
195
|
-
import { dirname as
|
|
194
|
+
import { lstatSync as lstatSync7 } from "fs";
|
|
195
|
+
import { dirname as dirname7, join as join9, resolve as resolve8 } from "path";
|
|
196
196
|
import { homedir as homedir2 } from "os";
|
|
197
197
|
|
|
198
|
+
// src/lib/opencode-hook-protocol.ts
|
|
199
|
+
import { createHash as createHash2 } from "crypto";
|
|
200
|
+
|
|
201
|
+
// src/lib/agent-bridge.ts
|
|
202
|
+
import { createHash } from "crypto";
|
|
203
|
+
import { lstatSync, readFileSync, readdirSync } from "fs";
|
|
204
|
+
import { join } from "path";
|
|
205
|
+
var CLI_BRIDGE_NAME = "skills-cli";
|
|
206
|
+
var CLI_BRIDGE_VERSION = 1;
|
|
207
|
+
var markdown = `---
|
|
208
|
+
name: skills-cli
|
|
209
|
+
description: Discover, load, author, and run versioned skills through the Skills CLI when a task needs reusable instructions or an executable skill.
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
Use the Skills CLI as the skill authority for this station.
|
|
213
|
+
|
|
214
|
+
- Discover available skills with \`skills list --json\` or \`skills search <query> --json\`.
|
|
215
|
+
- Read selected instructions with \`skills load <slug>\`. Use \`skills sync\` to refresh the station's shared selection profile.
|
|
216
|
+
- Create local drafts with \`skills new <name> --kind instruction\` or \`--kind executable\` in the Skills-owned source directory. Validate with \`skills validate <name>\`; use \`skills push --help\` for explicit versioned publication.
|
|
217
|
+
- Run a skill only when the user's task calls for execution. Use \`skills run --help\` and select the intended local or cloud target explicitly.
|
|
218
|
+
|
|
219
|
+
This bridge contains no skill payloads. Do not copy skill content into agent-native skill directories or load other native skill copies. If a Skills command refuses authority, selection, integrity, or native drift, report that refusal and use its repair guidance; do not substitute bundled or stale local content. Explicit local draft authoring remains available.
|
|
220
|
+
`;
|
|
221
|
+
var CLI_BRIDGE_DIGEST = createHash("sha256").update(markdown).digest("hex");
|
|
222
|
+
var CLI_BRIDGE_FILES = Object.freeze({
|
|
223
|
+
"SKILL.md": markdown,
|
|
224
|
+
".hasna-skills.json": `${JSON.stringify({ managedBy: "@hasna/skills", kind: "cli-bridge", version: CLI_BRIDGE_VERSION, protocol: "selection-v1", contentSha256: CLI_BRIDGE_DIGEST }, null, 2)}
|
|
225
|
+
`
|
|
226
|
+
});
|
|
227
|
+
function isOwnedCliBridge(path, expectedPaths) {
|
|
228
|
+
if (!expectedPaths.includes(path))
|
|
229
|
+
return false;
|
|
230
|
+
try {
|
|
231
|
+
if (!lstatSync(path).isDirectory())
|
|
232
|
+
return false;
|
|
233
|
+
if (JSON.stringify(readdirSync(path).sort()) !== JSON.stringify(Object.keys(CLI_BRIDGE_FILES).sort()))
|
|
234
|
+
return false;
|
|
235
|
+
return Object.entries(CLI_BRIDGE_FILES).every(([name, content]) => {
|
|
236
|
+
const file = join(path, name), stat = lstatSync(file);
|
|
237
|
+
return stat.isFile() && !stat.isSymbolicLink() && stat.size === Buffer.byteLength(content) && readFileSync(file, "utf8") === content;
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/lib/opencode-hook-protocol.ts
|
|
245
|
+
var OPENCODE_BRIDGE_BODY_SHA256 = createHash2("sha256").update(CLI_BRIDGE_FILES["SKILL.md"].replace(/^---\n[\s\S]*?\n---\n/, "")).digest("hex");
|
|
246
|
+
var OPENCODE_BRIDGE_DESCRIPTION_SHA256 = createHash2("sha256").update(/^description: (.+)$/m.exec(CLI_BRIDGE_FILES["SKILL.md"])[1]).digest("hex");
|
|
247
|
+
|
|
248
|
+
// src/lib/agent-opencode.ts
|
|
249
|
+
function renderOpenCodePlugin(command, profile) {
|
|
250
|
+
return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
|
|
251
|
+
import { spawn } from "node:child_process";
|
|
252
|
+
import { createHash } from "node:crypto";
|
|
253
|
+
const command = ${JSON.stringify(command)};
|
|
254
|
+
const profile = ${JSON.stringify(profile)};
|
|
255
|
+
const bridgeBodySha256 = ${JSON.stringify(OPENCODE_BRIDGE_BODY_SHA256)};
|
|
256
|
+
const bridgeDescriptionSha256 = ${JSON.stringify(OPENCODE_BRIDGE_DESCRIPTION_SHA256)};
|
|
257
|
+
function digest(text) { return createHash("sha256").update(text).digest("hex"); }
|
|
258
|
+
function canonicalSkill(skill) {
|
|
259
|
+
return skill?.id === "skills-cli" && skill.name === "skills-cli" && typeof skill.path === "string"
|
|
260
|
+
&& typeof skill.content === "string" && digest(skill.content) === bridgeBodySha256
|
|
261
|
+
&& typeof skill.description === "string" && digest(skill.description) === bridgeDescriptionSha256
|
|
262
|
+
&& (skill.autoinvoke === undefined || skill.autoinvoke === false);
|
|
263
|
+
}
|
|
264
|
+
function refusal() {
|
|
265
|
+
const error = new Error("Skills verification blocked this request. Review the OpenCode Skills hook configuration, then retry.");
|
|
266
|
+
error.name = "SkillsHookRefusal";
|
|
267
|
+
error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED" };
|
|
268
|
+
return error;
|
|
269
|
+
}
|
|
270
|
+
export default {
|
|
271
|
+
id: "local.hasna-skills-cli",
|
|
272
|
+
async setup(ctx) {
|
|
273
|
+
if (!ctx?.session?.hook || !ctx?.session?.get || !ctx?.skill?.transform || !ctx?.skill?.list || !ctx?.tool?.hook) throw refusal();
|
|
274
|
+
const registrations = [], pending = new Set(), sessions = new Set(), prompts = new Map();
|
|
275
|
+
let disposed = false;
|
|
276
|
+
function context(input) {
|
|
277
|
+
if (disposed) return Promise.reject(refusal());
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
// Availability failures may omit optional model context. Native skill
|
|
280
|
+
// consumption and explicit policy refusals always remain fail-closed.
|
|
281
|
+
const optional = input.delivery_phase === "context";
|
|
282
|
+
const chunks = []; let bytes = 0, settled = false, child, timer;
|
|
283
|
+
const finish = () => { clearTimeout(timer); pending.delete(cancel); };
|
|
284
|
+
const deny = () => { if (settled) return; settled = true; finish(); reject(refusal()); };
|
|
285
|
+
const unavailable = code => {
|
|
286
|
+
if (settled) return;
|
|
287
|
+
if (!optional || disposed) return deny();
|
|
288
|
+
settled = true; finish();
|
|
289
|
+
resolve({ delivered: false, text: "Skills context is unavailable [SKILLS_HOOK_" + code + "]. No Skills instructions were delivered by this hook. Ordinary work may continue. Do not perform actions that depend on unavailable Skills instructions, including previously loaded instructions. Explicit skills load, context and run still require verification. Do not substitute native, unmanaged or stale payloads." });
|
|
290
|
+
};
|
|
291
|
+
const stop = code => { unavailable(code); child?.kill("SIGKILL"); };
|
|
292
|
+
const cancel = () => { deny(); child?.kill("SIGKILL"); };
|
|
293
|
+
try {
|
|
294
|
+
child = spawn(command, ["hook", "user-prompt", "--agent", "opencode", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
295
|
+
} catch { unavailable("SPAWN_FAILED"); return; }
|
|
296
|
+
pending.add(cancel);
|
|
297
|
+
timer = setTimeout(() => stop("TIMEOUT"), 15000);
|
|
298
|
+
child.stdout.on("data", chunk => { if (settled) return; bytes += chunk.length; if (bytes > 1048576) stop("OUTPUT_LIMIT"); else chunks.push(chunk); });
|
|
299
|
+
child.stdout.on("error", () => stop("TRANSPORT_FAILED"));
|
|
300
|
+
child.stderr.on("data", () => {});
|
|
301
|
+
child.stderr.on("error", () => stop("TRANSPORT_FAILED"));
|
|
302
|
+
child.stdin.on("error", () => stop("TRANSPORT_FAILED"));
|
|
303
|
+
child.on("error", () => unavailable("SPAWN_FAILED"));
|
|
304
|
+
child.on("close", code => {
|
|
305
|
+
if (settled) return;
|
|
306
|
+
if (disposed) return deny();
|
|
307
|
+
if (code !== 0) return unavailable("CHILD_FAILED");
|
|
308
|
+
let result;
|
|
309
|
+
try {
|
|
310
|
+
result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
311
|
+
} catch { unavailable("INVALID_RESPONSE"); return; }
|
|
312
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return unavailable("INVALID_RESPONSE");
|
|
313
|
+
if (result.decision === "block" || result.continue === false) return deny();
|
|
314
|
+
// hookContextOutput returns exactly {} for a successful empty
|
|
315
|
+
// context. It never attests native bridge verification.
|
|
316
|
+
const output = optional && Object.keys(result).length === 0
|
|
317
|
+
? { hookEventName: input.hook_event_name, additionalContext: "" }
|
|
318
|
+
: result.hookSpecificOutput;
|
|
319
|
+
if (!output || typeof output !== "object" || Array.isArray(output) || output.hookEventName !== input.hook_event_name || typeof output.additionalContext !== "string") return unavailable("INVALID_RESPONSE");
|
|
320
|
+
if (input.hook_event_name === "BeforeTool" && result.nativeBridgeVerified !== true) return deny();
|
|
321
|
+
settled = true; finish();
|
|
322
|
+
resolve({ delivered: true, text: output.additionalContext });
|
|
323
|
+
});
|
|
324
|
+
try { child.stdin.end(JSON.stringify(input)); } catch { stop("TRANSPORT_FAILED"); }
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async function input(sessionID, prompt, event) {
|
|
328
|
+
if (disposed) throw refusal();
|
|
329
|
+
let session;
|
|
330
|
+
try { session = await ctx.session.get({ sessionID }); } catch { throw refusal(); }
|
|
331
|
+
if (disposed || !session || session.id !== sessionID || typeof session.location?.directory !== "string" || (session.parentID !== undefined && typeof session.parentID !== "string") || session.parentID === sessionID) throw refusal();
|
|
332
|
+
return { hook_event_name: event ?? (!sessions.has(sessionID) ? session.parentID ? "SubagentStart" : "SessionStart" : "UserPromptSubmit"), delivery_phase: event ? "native" : "context", cwd: session.location.directory, session_id: sessionID, parent_session_id: session.parentID ?? null, prompt, restore: true };
|
|
333
|
+
}
|
|
334
|
+
async function dispose() {
|
|
335
|
+
if (disposed) return;
|
|
336
|
+
disposed = true;
|
|
337
|
+
for (const cancel of pending) cancel();
|
|
338
|
+
sessions.clear();
|
|
339
|
+
prompts.clear();
|
|
340
|
+
const results = await Promise.allSettled(registrations.reverse().map(registration => Promise.resolve().then(() => registration.dispose())));
|
|
341
|
+
if (results.some(result => result.status === "rejected")) throw refusal();
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
registrations.push(await ctx.skill.transform(editor => {
|
|
345
|
+
for (const skill of editor.list()) if (!canonicalSkill(skill)) editor.remove(skill.id);
|
|
346
|
+
}));
|
|
347
|
+
async function verifyNativeBridge(sessionID) {
|
|
348
|
+
let skills;
|
|
349
|
+
try { skills = await ctx.skill.list(); } catch { throw refusal(); }
|
|
350
|
+
if (!Array.isArray(skills)) throw refusal();
|
|
351
|
+
const matches = skills.filter(skill => skill?.id === "skills-cli");
|
|
352
|
+
if (matches.length !== 1) throw refusal();
|
|
353
|
+
const skill = matches[0];
|
|
354
|
+
if (!canonicalSkill(skill)) throw refusal();
|
|
355
|
+
const metadata = { id: skill.id, name: skill.name, path: skill.path, contentSha256: digest(skill.content), descriptionSha256: digest(skill.description) };
|
|
356
|
+
await context({ ...await input(sessionID, "", "BeforeTool"), tool_input: { id: "skills-cli" }, native_skill: metadata });
|
|
357
|
+
}
|
|
358
|
+
registrations.push(await ctx.tool.hook("execute.before", async event => {
|
|
359
|
+
if (disposed) throw refusal();
|
|
360
|
+
if (event.tool !== "skill") return;
|
|
361
|
+
if (event.input?.id !== "skills-cli") throw refusal();
|
|
362
|
+
await verifyNativeBridge(event.sessionID);
|
|
363
|
+
}));
|
|
364
|
+
registrations.push(await ctx.session.hook("prompt", async event => {
|
|
365
|
+
if (disposed || typeof event.prompt?.text !== "string" || event.prompt.text.length > 128 * 1024) throw refusal();
|
|
366
|
+
if (event.prompt.skills !== undefined && (!Array.isArray(event.prompt.skills) || event.prompt.skills.some(skill => skill?.id !== "skills-cli"))) throw refusal();
|
|
367
|
+
// Attachments materialize immediately after this hook. Check the actual
|
|
368
|
+
// current registry object and on-disk bridge before native consumption.
|
|
369
|
+
if (event.prompt.skills?.length) await verifyNativeBridge(event.sessionID);
|
|
370
|
+
if (disposed) throw refusal();
|
|
371
|
+
const queued = prompts.get(event.sessionID) ?? [];
|
|
372
|
+
if (queued.reduce((size, entry) => size + entry.text.length + 1, 0) + event.prompt.text.length > 128 * 1024) throw refusal();
|
|
373
|
+
queued.push({ text: event.prompt.text });
|
|
374
|
+
prompts.set(event.sessionID, queued);
|
|
375
|
+
}));
|
|
376
|
+
registrations.push(await ctx.session.hook("context", async event => {
|
|
377
|
+
// Only prompt-hook input has owner-prompt provenance. Keep it transient
|
|
378
|
+
// until the one delivery-bearing CLI call, including an empty prompt.
|
|
379
|
+
// Checkpoints, synthetic user-role history and resumes never match.
|
|
380
|
+
const queued = [...(prompts.get(event.sessionID) ?? [])];
|
|
381
|
+
const result = await context(await input(event.sessionID, queued.map(entry => entry.text).join("\\n")));
|
|
382
|
+
if (disposed) throw refusal();
|
|
383
|
+
if (result.text) event.system.push({ type: "text", text: result.text });
|
|
384
|
+
// Remove only the prompt events this request consumed. New admission
|
|
385
|
+
// while the CLI was running belongs to the next model context.
|
|
386
|
+
const current = prompts.get(event.sessionID);
|
|
387
|
+
if (current) {
|
|
388
|
+
const remaining = current.filter(entry => !queued.includes(entry));
|
|
389
|
+
if (remaining.length) prompts.set(event.sessionID, remaining);
|
|
390
|
+
else prompts.delete(event.sessionID);
|
|
391
|
+
}
|
|
392
|
+
// Retry SessionStart on the next context after an unavailable child,
|
|
393
|
+
// but never replay prompt text already admitted to this model turn.
|
|
394
|
+
if (result.delivered) sessions.add(event.sessionID);
|
|
395
|
+
}));
|
|
396
|
+
return dispose;
|
|
397
|
+
} catch {
|
|
398
|
+
await dispose();
|
|
399
|
+
throw refusal();
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
`;
|
|
404
|
+
}
|
|
405
|
+
|
|
198
406
|
// src/lib/agent-adapters.ts
|
|
199
407
|
var INTEGRATION_AGENTS = ["claude", "codex", "gemini", "opencode", "cursor", "hermes", "sumi"];
|
|
200
408
|
function renderAgentHookCommand(command, agent, profileId, event) {
|
|
@@ -206,12 +414,12 @@ var AGENT_ADAPTERS = {
|
|
|
206
414
|
claude: { root: ".claude/skills", config: ".claude/settings.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
|
|
207
415
|
codex: { root: ".codex/skills", config: ".codex/hooks.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
|
|
208
416
|
gemini: { root: ".gemini/skills", config: ".gemini/settings.json", events: ["BeforeAgent", "SessionStart"], promptContext: true },
|
|
209
|
-
opencode: { root: ".config/opencode/skills", config: ".config/opencode/opencode.json", events: ["
|
|
417
|
+
opencode: { root: ".config/opencode/skills", config: ".config/opencode/opencode.json", events: ["prompt", "context"], promptContext: true },
|
|
210
418
|
sumi: { root: null, config: null, events: ["prompt", "context"], promptContext: true },
|
|
211
419
|
hermes: { root: ".hermes/skills", config: ".hermes/config.yaml", events: ["pre_llm_call", "pre_tool_call"], promptContext: true, promptFailureMode: "open", toolFailureMode: "supervised-child-errors-block", requiresNativeTrust: true },
|
|
212
420
|
cursor: { root: ".cursor/skills", config: ".cursor/hooks.json", events: ["beforeSubmitPrompt", "sessionStart"], promptContext: false }
|
|
213
421
|
};
|
|
214
|
-
function
|
|
422
|
+
function renderLegacyOpenCodePlugin(command, profileId) {
|
|
215
423
|
return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
|
|
216
424
|
import { spawn } from "node:child_process";
|
|
217
425
|
import { randomUUID } from "node:crypto";
|
|
@@ -261,8 +469,12 @@ export default async ({ directory }) => {
|
|
|
261
469
|
};
|
|
262
470
|
`;
|
|
263
471
|
}
|
|
472
|
+
function isManagedOpenCodePlugin(text, command, profileId) {
|
|
473
|
+
return text === renderOpenCodePlugin(command, profileId) || text === renderLegacyOpenCodePlugin(command, profileId);
|
|
474
|
+
}
|
|
264
475
|
|
|
265
476
|
// src/lib/native-discovery-roots.ts
|
|
477
|
+
import { dirname, join as join2, resolve } from "path";
|
|
266
478
|
var NATIVE_SKILL_ROOTS = [
|
|
267
479
|
["claude", ".claude/skills"],
|
|
268
480
|
["codex", ".codex/skills"],
|
|
@@ -282,14 +494,32 @@ var NATIVE_SKILL_ROOTS = [
|
|
|
282
494
|
["roo", ".roo/skills"],
|
|
283
495
|
["copilot", ".github/skills"]
|
|
284
496
|
];
|
|
497
|
+
function nativeSumiProjectSkillRoots(home, project, resolveSumiRoots) {
|
|
498
|
+
return [...resolveSumiRoots(home, project), ...[".claude", ".agents"].flatMap((directory) => ["skill", "skills"].map((name) => join2(project, directory, name)))];
|
|
499
|
+
}
|
|
500
|
+
function nativeProjectAncestorDirectories(projects, canonical = resolve) {
|
|
501
|
+
const directories = new Set;
|
|
502
|
+
for (const project of projects) {
|
|
503
|
+
for (let path = canonical(project), depth = 0;; depth++) {
|
|
504
|
+
if (depth >= 100)
|
|
505
|
+
throw new Error("NATIVE_SKILL_DRIFT: project ancestor discovery limit exceeded");
|
|
506
|
+
directories.add(path);
|
|
507
|
+
const parent = dirname(path);
|
|
508
|
+
if (parent === path)
|
|
509
|
+
break;
|
|
510
|
+
path = parent;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return [...directories];
|
|
514
|
+
}
|
|
285
515
|
|
|
286
516
|
// src/lib/managed-policy.ts
|
|
287
|
-
import { closeSync as closeSync5, constants as constants5, fstatSync as fstatSync5, lstatSync as
|
|
288
|
-
import { join as
|
|
517
|
+
import { closeSync as closeSync5, constants as constants5, fstatSync as fstatSync5, lstatSync as lstatSync6, openSync as openSync5, readSync as readSync4 } from "fs";
|
|
518
|
+
import { join as join8 } from "path";
|
|
289
519
|
|
|
290
520
|
// src/lib/config.ts
|
|
291
|
-
import { closeSync, constants, existsSync as existsSync2, fstatSync, lstatSync, openSync, readFileSync, statSync, writeFileSync, mkdirSync } from "fs";
|
|
292
|
-
import { join as
|
|
521
|
+
import { closeSync, constants, existsSync as existsSync2, fstatSync, lstatSync as lstatSync2, openSync, readFileSync as readFileSync2, statSync, writeFileSync, mkdirSync } from "fs";
|
|
522
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
293
523
|
|
|
294
524
|
// src/lib/retired-settings.ts
|
|
295
525
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
@@ -345,7 +575,7 @@ function assertNoRetiredConfigKeys(config, source) {
|
|
|
345
575
|
// src/lib/app-home.ts
|
|
346
576
|
import { existsSync } from "fs";
|
|
347
577
|
import { homedir } from "os";
|
|
348
|
-
import { join, resolve } from "path";
|
|
578
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
349
579
|
import { homedir as pathsResolverHomedir } from "os";
|
|
350
580
|
import { join as pathsResolverJoin } from "path";
|
|
351
581
|
var PATHS_RESOLVER_DATA_ENV = "HASNA_DATA_HOME";
|
|
@@ -381,7 +611,7 @@ function effectiveHome() {
|
|
|
381
611
|
return process.env["HOME"] || process.env["USERPROFILE"] || homedir() || "/tmp";
|
|
382
612
|
}
|
|
383
613
|
function legacyDataRoot() {
|
|
384
|
-
return
|
|
614
|
+
return join3(effectiveHome(), ".hasna", "skills");
|
|
385
615
|
}
|
|
386
616
|
function resolverDataRoot(home = effectiveHome(), env) {
|
|
387
617
|
return dataDir({ app: "skills", home, env });
|
|
@@ -390,13 +620,13 @@ function adoptResolverDataRoot(resolved, env = process.env) {
|
|
|
390
620
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
391
621
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
392
622
|
return true;
|
|
393
|
-
return existsSync(
|
|
623
|
+
return existsSync(join3(resolved, DEFAULT_SQLITE_FILENAME)) || existsSync(join3(resolved, GLOBAL_CONFIG_FILENAME));
|
|
394
624
|
}
|
|
395
625
|
function exactDataRoot() {
|
|
396
626
|
for (const key of [DATA_DIR_ENV, HASNA_SKILLS_HOME_ENV, SKILLS_HOME_ENV]) {
|
|
397
627
|
const dir = process.env[key]?.trim();
|
|
398
628
|
if (dir)
|
|
399
|
-
return
|
|
629
|
+
return resolve2(dir);
|
|
400
630
|
}
|
|
401
631
|
return;
|
|
402
632
|
}
|
|
@@ -405,19 +635,19 @@ function getDataRoot() {
|
|
|
405
635
|
if (exact)
|
|
406
636
|
return exact;
|
|
407
637
|
const resolved = resolverDataRoot();
|
|
408
|
-
return adoptResolverDataRoot(resolved) ?
|
|
638
|
+
return adoptResolverDataRoot(resolved) ? resolve2(resolved) : resolve2(legacyDataRoot());
|
|
409
639
|
}
|
|
410
640
|
function skillsDataRootForHome(home) {
|
|
411
|
-
const isOwnHome =
|
|
641
|
+
const isOwnHome = resolve2(home) === resolve2(effectiveHome()) || resolve2(home) === resolve2(homedir());
|
|
412
642
|
if (isOwnHome) {
|
|
413
643
|
const exact = exactDataRoot();
|
|
414
644
|
if (exact)
|
|
415
645
|
return exact;
|
|
416
646
|
const resolved2 = resolverDataRoot(home);
|
|
417
|
-
return adoptResolverDataRoot(resolved2) ?
|
|
647
|
+
return adoptResolverDataRoot(resolved2) ? resolve2(resolved2) : resolve2(join3(home, ".hasna", "skills"));
|
|
418
648
|
}
|
|
419
649
|
const resolved = resolverDataRoot(home, {});
|
|
420
|
-
return adoptResolverDataRoot(resolved, {}) ?
|
|
650
|
+
return adoptResolverDataRoot(resolved, {}) ? resolve2(resolved) : resolve2(join3(home, ".hasna", "skills"));
|
|
421
651
|
}
|
|
422
652
|
|
|
423
653
|
// src/lib/config.ts
|
|
@@ -448,7 +678,7 @@ var INSTALLED_SKILLS_DIRNAME = "installed";
|
|
|
448
678
|
var SKILLS_CACHE_DIRNAME = "skills";
|
|
449
679
|
var LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
|
|
450
680
|
function isOwnerLayoutMigrated(appDir) {
|
|
451
|
-
return existsSync2(
|
|
681
|
+
return existsSync2(join4(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
|
|
452
682
|
}
|
|
453
683
|
function getDataDir() {
|
|
454
684
|
const root = getDataRoot();
|
|
@@ -462,16 +692,16 @@ function getDataDirReadOnly() {
|
|
|
462
692
|
}
|
|
463
693
|
function getConfigPath(scope) {
|
|
464
694
|
if (scope === "global") {
|
|
465
|
-
return
|
|
695
|
+
return join4(getDataDir(), "config.json");
|
|
466
696
|
}
|
|
467
|
-
return
|
|
697
|
+
return join4(process.cwd(), "skills.config.json");
|
|
468
698
|
}
|
|
469
699
|
function readConfigFile(path) {
|
|
470
700
|
if (!existsSync2(path))
|
|
471
701
|
return {};
|
|
472
702
|
let parsed;
|
|
473
703
|
try {
|
|
474
|
-
parsed = JSON.parse(
|
|
704
|
+
parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
475
705
|
} catch {
|
|
476
706
|
return {};
|
|
477
707
|
}
|
|
@@ -508,7 +738,7 @@ function saveConfig(key, value, scope = "project") {
|
|
|
508
738
|
let existing = {};
|
|
509
739
|
if (existsSync2(filePath)) {
|
|
510
740
|
try {
|
|
511
|
-
existing = JSON.parse(
|
|
741
|
+
existing = JSON.parse(readFileSync2(filePath, "utf-8"));
|
|
512
742
|
if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
|
|
513
743
|
existing = {};
|
|
514
744
|
}
|
|
@@ -516,7 +746,7 @@ function saveConfig(key, value, scope = "project") {
|
|
|
516
746
|
existing = {};
|
|
517
747
|
}
|
|
518
748
|
} else {
|
|
519
|
-
const dir =
|
|
749
|
+
const dir = dirname2(filePath);
|
|
520
750
|
if (!existsSync2(dir)) {
|
|
521
751
|
mkdirSync(dir, { recursive: true });
|
|
522
752
|
}
|
|
@@ -535,7 +765,7 @@ function unsetConfig(key, scope = "project") {
|
|
|
535
765
|
return false;
|
|
536
766
|
let existing;
|
|
537
767
|
try {
|
|
538
|
-
const parsed = JSON.parse(
|
|
768
|
+
const parsed = JSON.parse(readFileSync2(filePath, "utf-8"));
|
|
539
769
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
540
770
|
return false;
|
|
541
771
|
existing = parsed;
|
|
@@ -551,17 +781,17 @@ function unsetConfig(key, scope = "project") {
|
|
|
551
781
|
}
|
|
552
782
|
|
|
553
783
|
// src/lib/selection-cache.ts
|
|
554
|
-
import { createHash as
|
|
784
|
+
import { createHash as createHash4, randomUUID } from "crypto";
|
|
555
785
|
import { spawnSync } from "child_process";
|
|
556
|
-
import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, linkSync, lstatSync as
|
|
786
|
+
import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, linkSync, lstatSync as lstatSync3, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync4, readlinkSync, readSync, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
557
787
|
import { hostname } from "os";
|
|
558
|
-
import { dirname as
|
|
788
|
+
import { dirname as dirname3, isAbsolute, join as join6, parse, resolve as resolve3 } from "path";
|
|
559
789
|
import { isDeepStrictEqual } from "util";
|
|
560
790
|
|
|
561
791
|
// src/lib/skill-bundle.ts
|
|
562
|
-
import { createHash } from "crypto";
|
|
563
|
-
import { readFileSync as
|
|
564
|
-
import { join as
|
|
792
|
+
import { createHash as createHash3 } from "crypto";
|
|
793
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
794
|
+
import { join as join5, relative } from "path";
|
|
565
795
|
import { createGunzip } from "zlib";
|
|
566
796
|
|
|
567
797
|
// src/lib/skill-entry-path.ts
|
|
@@ -748,7 +978,7 @@ function ownBytes(view) {
|
|
|
748
978
|
return out;
|
|
749
979
|
}
|
|
750
980
|
function sha256Hex(bytes) {
|
|
751
|
-
return
|
|
981
|
+
return createHash3("sha256").update(bytes).digest("hex");
|
|
752
982
|
}
|
|
753
983
|
function collectSkillBundleEntries(dir) {
|
|
754
984
|
const entries = [];
|
|
@@ -756,8 +986,8 @@ function collectSkillBundleEntries(dir) {
|
|
|
756
986
|
return entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
757
987
|
}
|
|
758
988
|
function walk(root, current, out) {
|
|
759
|
-
for (const entry of
|
|
760
|
-
const absolute =
|
|
989
|
+
for (const entry of readdirSync2(current, { withFileTypes: true })) {
|
|
990
|
+
const absolute = join5(current, entry.name);
|
|
761
991
|
const rel = relative(root, absolute).split("\\").join("/");
|
|
762
992
|
const isRootLevel = !rel.includes("/");
|
|
763
993
|
if (ANY_SEGMENT_EXCLUDES.has(entry.name.toLowerCase()))
|
|
@@ -781,7 +1011,7 @@ function walk(root, current, out) {
|
|
|
781
1011
|
const stats = statSync2(absolute);
|
|
782
1012
|
out.push({
|
|
783
1013
|
path: rel,
|
|
784
|
-
bytes: ownBytes(
|
|
1014
|
+
bytes: ownBytes(readFileSync3(absolute)),
|
|
785
1015
|
mode: stats.mode & 64 ? 493 : 420
|
|
786
1016
|
});
|
|
787
1017
|
}
|
|
@@ -1001,7 +1231,7 @@ async function inspectSkillBundle(bundle, options = {}) {
|
|
|
1001
1231
|
parser.push(chunk);
|
|
1002
1232
|
bytesSinceYield += chunk.byteLength;
|
|
1003
1233
|
if (bytesSinceYield >= 256 * 1024) {
|
|
1004
|
-
await new Promise((
|
|
1234
|
+
await new Promise((resolve3) => setTimeout(resolve3, 0));
|
|
1005
1235
|
bytesSinceYield = 0;
|
|
1006
1236
|
check();
|
|
1007
1237
|
}
|
|
@@ -1239,14 +1469,14 @@ class SkillSelectionError extends Error {
|
|
|
1239
1469
|
var MAX_CACHED_PROFILE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
1240
1470
|
var SELECTION_LOCK_FILE = "selection.lock.json";
|
|
1241
1471
|
function selectionCacheRoot(options = {}) {
|
|
1242
|
-
return
|
|
1472
|
+
return resolve3(options.cacheDir ?? join6(getDataDirReadOnly(), "selection-cache"));
|
|
1243
1473
|
}
|
|
1244
1474
|
function selectionKey(selection) {
|
|
1245
1475
|
validateSelection(selection);
|
|
1246
1476
|
return hash([selection.authority, selection.workspaceId, selection.slug, selection.version, selection.bundleDigest]);
|
|
1247
1477
|
}
|
|
1248
1478
|
function hash(value) {
|
|
1249
|
-
return
|
|
1479
|
+
return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
1250
1480
|
}
|
|
1251
1481
|
function validateSelection(selection) {
|
|
1252
1482
|
if (!selection || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(selection.slug) || !isValidSkillVersion(selection.version) || !/^sha256:[a-f0-9]{64}$/.test(selection.bundleDigest) || typeof selection.workspaceId !== "string" || !selection.workspaceId.trim() || selection.authorizationEpoch !== undefined && (typeof selection.authorizationEpoch !== "string" || !/^[a-f0-9]{32}$/.test(selection.authorizationEpoch)) || typeof selection.profileRevision !== "string" || !selection.profileRevision.trim()) {
|
|
@@ -1283,25 +1513,35 @@ function validateResolvedProfile(profile, authority) {
|
|
|
1283
1513
|
}
|
|
1284
1514
|
function selectionBundlePath(selection, options = {}) {
|
|
1285
1515
|
validateSelection(selection);
|
|
1286
|
-
return
|
|
1516
|
+
return join6(selectionCacheRoot(options), "objects", hash(selection.authority), hash(selection.workspaceId), `${selection.bundleDigest.slice(7)}.tar.gz`);
|
|
1287
1517
|
}
|
|
1288
1518
|
function assertRegularPath(path, createParents = false) {
|
|
1289
|
-
const absolute =
|
|
1519
|
+
const absolute = resolve3(path);
|
|
1290
1520
|
const root = parse(absolute).root;
|
|
1291
1521
|
let cursor = root;
|
|
1292
1522
|
const segments = absolute.slice(root.length).split("/");
|
|
1293
1523
|
for (let index = 0;index < segments.length; index++) {
|
|
1294
|
-
cursor =
|
|
1524
|
+
cursor = join6(cursor, segments[index]);
|
|
1295
1525
|
try {
|
|
1296
|
-
const stat =
|
|
1526
|
+
const stat = lstatSync3(cursor);
|
|
1297
1527
|
if (stat.isSymbolicLink() || index < segments.length - 1 && !stat.isDirectory()) {
|
|
1298
1528
|
throw new SkillSelectionError("UNSAFE_CACHE_PATH", "Skills cache paths must not traverse symlinks or non-directories.");
|
|
1299
1529
|
}
|
|
1300
1530
|
} catch (error) {
|
|
1301
1531
|
if (error.code !== "ENOENT")
|
|
1302
1532
|
throw error;
|
|
1303
|
-
if (createParents && index < segments.length - 1)
|
|
1304
|
-
|
|
1533
|
+
if (createParents && index < segments.length - 1) {
|
|
1534
|
+
try {
|
|
1535
|
+
mkdirSync2(cursor, { mode: 448 });
|
|
1536
|
+
} catch (creationError) {
|
|
1537
|
+
if (creationError.code !== "EEXIST")
|
|
1538
|
+
throw creationError;
|
|
1539
|
+
}
|
|
1540
|
+
const parent = lstatSync3(cursor);
|
|
1541
|
+
if (parent.isSymbolicLink() || !parent.isDirectory()) {
|
|
1542
|
+
throw new SkillSelectionError("UNSAFE_CACHE_PATH", "Skills cache paths must not traverse symlinks or non-directories.");
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1305
1545
|
}
|
|
1306
1546
|
}
|
|
1307
1547
|
}
|
|
@@ -1343,7 +1583,7 @@ function writeSelectionJson(path, value) {
|
|
|
1343
1583
|
}
|
|
1344
1584
|
function atomicWrite(path, bytes, mode, durable = false) {
|
|
1345
1585
|
assertRegularPath(path, true);
|
|
1346
|
-
const temporary =
|
|
1586
|
+
const temporary = join6(dirname3(path), `.selection-${randomUUID()}.tmp`);
|
|
1347
1587
|
try {
|
|
1348
1588
|
const fd = openSync2(temporary, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, mode);
|
|
1349
1589
|
try {
|
|
@@ -1355,7 +1595,7 @@ function atomicWrite(path, bytes, mode, durable = false) {
|
|
|
1355
1595
|
}
|
|
1356
1596
|
renameSync(temporary, path);
|
|
1357
1597
|
if (durable)
|
|
1358
|
-
syncDirectory(
|
|
1598
|
+
syncDirectory(dirname3(path));
|
|
1359
1599
|
} finally {
|
|
1360
1600
|
try {
|
|
1361
1601
|
unlinkSync(temporary);
|
|
@@ -1365,6 +1605,16 @@ function atomicWrite(path, bytes, mode, durable = false) {
|
|
|
1365
1605
|
}
|
|
1366
1606
|
}
|
|
1367
1607
|
}
|
|
1608
|
+
function readSelectionFileSnapshot(path, maximumBytes = 4096) {
|
|
1609
|
+
const before = lstatSync3(path, { throwIfNoEntry: false });
|
|
1610
|
+
if (!before)
|
|
1611
|
+
return null;
|
|
1612
|
+
const bytes = readRegularFile(path, maximumBytes);
|
|
1613
|
+
const after = lstatSync3(path, { throwIfNoEntry: false });
|
|
1614
|
+
if (!bytes || !after?.isFile() || after.dev !== before.dev || after.ino !== before.ino)
|
|
1615
|
+
return null;
|
|
1616
|
+
return { bytes, sha256: sha256Hex(bytes), mtimeMs: after.mtimeMs, dev: after.dev, ino: after.ino };
|
|
1617
|
+
}
|
|
1368
1618
|
function readSelectionJson(path, maximumBytes = MAX_PROFILE_DOCUMENT_BYTES) {
|
|
1369
1619
|
const bytes = readRegularFile(path, Math.min(maximumBytes, MAX_PROFILE_DOCUMENT_BYTES));
|
|
1370
1620
|
if (!bytes)
|
|
@@ -1382,7 +1632,8 @@ async function verifiedEntries(selection, bytes, signal, deadline) {
|
|
|
1382
1632
|
signal?.throwIfAborted();
|
|
1383
1633
|
if (remaining !== undefined && remaining <= 0)
|
|
1384
1634
|
throw new SkillBundleInspectionError("BUNDLE_TIMEOUT", "Bundle inspection deadline exceeded");
|
|
1385
|
-
const
|
|
1635
|
+
const inspection = remaining === undefined ? {} : { limits: { timeoutMs: Math.min(remaining, SKILL_BUNDLE_INSPECTION_LIMITS.timeoutMs) } };
|
|
1636
|
+
const bundle = await inspectSkillBundle(bytes, { signal, ...inspection });
|
|
1386
1637
|
return bundle.entries;
|
|
1387
1638
|
}
|
|
1388
1639
|
async function readCachedSelection(selection, options = {}) {
|
|
@@ -1455,7 +1706,7 @@ async function cacheSelectionBundle(selection, response, options = {}) {
|
|
|
1455
1706
|
return entries;
|
|
1456
1707
|
}
|
|
1457
1708
|
function profilePath(profileId, options) {
|
|
1458
|
-
return
|
|
1709
|
+
return join6(selectionCacheRoot(options), "profiles", `${hash(profileId)}.json`);
|
|
1459
1710
|
}
|
|
1460
1711
|
function activateSelectionProfile(profile, options = {}) {
|
|
1461
1712
|
validateResolvedProfile(profile);
|
|
@@ -1487,7 +1738,7 @@ function assertFreshCachedProfile(receipt, options = {}) {
|
|
|
1487
1738
|
}
|
|
1488
1739
|
}
|
|
1489
1740
|
function projectSelectionLockPath(projectDir) {
|
|
1490
|
-
return
|
|
1741
|
+
return join6(resolve3(projectDir), ".skills", SELECTION_LOCK_FILE);
|
|
1491
1742
|
}
|
|
1492
1743
|
function readProjectSelectionSnapshot(projectDir) {
|
|
1493
1744
|
const path = projectSelectionLockPath(projectDir);
|
|
@@ -1506,7 +1757,7 @@ function readProjectSelectionSnapshot(projectDir) {
|
|
|
1506
1757
|
function sessionReceiptPath(sessionId, options = {}) {
|
|
1507
1758
|
if (!sessionId.trim() || sessionId.length > MAX_SKILL_SESSION_ID_CHARS)
|
|
1508
1759
|
throw new SkillSelectionError("INVALID_SESSION", `A Skills session id must contain 1\u2013${MAX_SKILL_SESSION_ID_CHARS} characters.`);
|
|
1509
|
-
return
|
|
1760
|
+
return join6(selectionCacheRoot(options), "sessions", `${hash(sessionId)}.json`);
|
|
1510
1761
|
}
|
|
1511
1762
|
function skillSessionGeneration(receipt) {
|
|
1512
1763
|
return receipt.generation ?? 0;
|
|
@@ -1560,7 +1811,7 @@ var SESSION_LOCK_MIN_AGE_MS = 30000;
|
|
|
1560
1811
|
function localBootId() {
|
|
1561
1812
|
try {
|
|
1562
1813
|
if (process.platform === "linux") {
|
|
1563
|
-
const value =
|
|
1814
|
+
const value = readFileSync4("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
1564
1815
|
return /^[a-f0-9-]{36}$/i.test(value) ? value : null;
|
|
1565
1816
|
}
|
|
1566
1817
|
if (process.platform === "darwin") {
|
|
@@ -1578,7 +1829,7 @@ function processStart(pid) {
|
|
|
1578
1829
|
return { state: "unknown" };
|
|
1579
1830
|
if (process.platform === "linux") {
|
|
1580
1831
|
try {
|
|
1581
|
-
const stat =
|
|
1832
|
+
const stat = readFileSync4(`/proc/${pid}/stat`, "utf8");
|
|
1582
1833
|
const close = stat.lastIndexOf(")");
|
|
1583
1834
|
const start = close < 0 ? undefined : stat.slice(close + 2).trim().split(/\s+/)[19];
|
|
1584
1835
|
return start && /^\d+$/.test(start) ? { state: "alive", start: `linux:${start}` } : { state: "unknown" };
|
|
@@ -1597,7 +1848,16 @@ function processStart(pid) {
|
|
|
1597
1848
|
}
|
|
1598
1849
|
return { state: "unknown" };
|
|
1599
1850
|
}
|
|
1851
|
+
var cachedLocalLockIdentity;
|
|
1600
1852
|
function localLockIdentity() {
|
|
1853
|
+
if (cachedLocalLockIdentity?.pid === process.pid)
|
|
1854
|
+
return { ...cachedLocalLockIdentity.identity };
|
|
1855
|
+
const identity = probeLocalLockIdentity();
|
|
1856
|
+
if (identity)
|
|
1857
|
+
cachedLocalLockIdentity = { pid: process.pid, identity: { ...identity } };
|
|
1858
|
+
return identity;
|
|
1859
|
+
}
|
|
1860
|
+
function probeLocalLockIdentity() {
|
|
1601
1861
|
const bootId = localBootId(), owner = processStart(process.pid);
|
|
1602
1862
|
if (!bootId || owner.state !== "alive")
|
|
1603
1863
|
return null;
|
|
@@ -1615,14 +1875,14 @@ function localLockIdentity() {
|
|
|
1615
1875
|
}
|
|
1616
1876
|
function readSessionLock(sessionId, options) {
|
|
1617
1877
|
const path = `${sessionReceiptPath(sessionId, options)}.write-lock`;
|
|
1618
|
-
const before =
|
|
1878
|
+
const before = lstatSync3(path, { throwIfNoEntry: false });
|
|
1619
1879
|
if (!before)
|
|
1620
1880
|
return null;
|
|
1621
1881
|
if (!before.isFile() || ![1, 2].includes(before.nlink) || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
|
|
1622
1882
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is not a private regular file; recovery requires review.");
|
|
1623
1883
|
}
|
|
1624
1884
|
const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
|
|
1625
|
-
const after =
|
|
1885
|
+
const after = lstatSync3(path, { throwIfNoEntry: false });
|
|
1626
1886
|
if (!bytes || !after?.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs || before.mode !== after.mode || before.uid !== after.uid || before.gid !== after.gid || before.nlink !== after.nlink) {
|
|
1627
1887
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock changed during inspection; recovery requires review.");
|
|
1628
1888
|
}
|
|
@@ -1639,7 +1899,7 @@ function readSessionLock(sessionId, options) {
|
|
|
1639
1899
|
if (after.nlink === 2) {
|
|
1640
1900
|
stagingPath = `${path}.${marker.operationId}.prepared`;
|
|
1641
1901
|
assertRegularPath(stagingPath);
|
|
1642
|
-
const staged =
|
|
1902
|
+
const staged = lstatSync3(stagingPath, { throwIfNoEntry: false });
|
|
1643
1903
|
if (marker.publication !== "hard-link-v1" || !staged?.isFile() || staged.dev !== after.dev || staged.ino !== after.ino || staged.nlink !== 2 || staged.mode !== after.mode || staged.uid !== after.uid || staged.gid !== after.gid) {
|
|
1644
1904
|
throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has an unbound publication link; recovery requires review.");
|
|
1645
1905
|
}
|
|
@@ -1658,20 +1918,20 @@ function publishSessionLock(path, marker) {
|
|
|
1658
1918
|
fsyncSync(fd);
|
|
1659
1919
|
linkSync(stagingPath, path);
|
|
1660
1920
|
published = true;
|
|
1661
|
-
syncDirectory(
|
|
1662
|
-
const staged =
|
|
1921
|
+
syncDirectory(dirname3(path));
|
|
1922
|
+
const staged = lstatSync3(stagingPath);
|
|
1663
1923
|
if (!staged.isFile() || staged.dev !== stat.dev || staged.ino !== stat.ino) {
|
|
1664
1924
|
throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The initialized lock staging inode changed before publication completed.");
|
|
1665
1925
|
}
|
|
1666
1926
|
unlinkSync(stagingPath);
|
|
1667
|
-
syncDirectory(
|
|
1927
|
+
syncDirectory(dirname3(path));
|
|
1668
1928
|
handedOff = true;
|
|
1669
1929
|
return { path, stagingPath, fd, dev: stat.dev, ino: stat.ino };
|
|
1670
1930
|
} finally {
|
|
1671
1931
|
if (!handedOff) {
|
|
1672
1932
|
try {
|
|
1673
1933
|
for (const target of [...published ? [path] : [], stagingPath]) {
|
|
1674
|
-
const current =
|
|
1934
|
+
const current = lstatSync3(target, { throwIfNoEntry: false });
|
|
1675
1935
|
if (current?.isFile() && current.dev === stat.dev && current.ino === stat.ino)
|
|
1676
1936
|
unlinkSync(target);
|
|
1677
1937
|
}
|
|
@@ -1709,16 +1969,16 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
1709
1969
|
writeFileSync2(guardFd, `${JSON.stringify({ schemaVersion: 1, pid: process.pid, operationId: randomUUID() })}
|
|
1710
1970
|
`);
|
|
1711
1971
|
fsyncSync(guardFd);
|
|
1712
|
-
const cacheOptions = { cacheDir:
|
|
1972
|
+
const cacheOptions = { cacheDir: dirname3(dirname3(lock.path)) };
|
|
1713
1973
|
const current = readSessionLock(lock.marker.sessionId, cacheOptions);
|
|
1714
1974
|
if (!current || current.dev !== lock.dev || current.ino !== lock.ino || current.sha256 !== lock.sha256 || current.mtimeMs !== lock.mtimeMs || !writerIsGone(current, explicitLocalHost))
|
|
1715
1975
|
return null;
|
|
1716
1976
|
const currentReceipt = readSkillSessionSnapshotIfExists(lock.marker.sessionId, cacheOptions);
|
|
1717
1977
|
if (currentReceipt?.sha256 !== receipt?.sha256 || currentReceipt?.generation !== receipt?.generation)
|
|
1718
1978
|
return null;
|
|
1719
|
-
const directory =
|
|
1720
|
-
const archivePath =
|
|
1721
|
-
const recordPath =
|
|
1979
|
+
const directory = join6(dirname3(dirname3(lock.path)), "session-lock-recoveries", randomUUID());
|
|
1980
|
+
const archivePath = join6(directory, "original.write-lock");
|
|
1981
|
+
const recordPath = join6(directory, "recovery.json");
|
|
1722
1982
|
assertRegularPath(archivePath, true);
|
|
1723
1983
|
archiveBytes(recordPath, Buffer.from(`${JSON.stringify({
|
|
1724
1984
|
schemaVersion: 1,
|
|
@@ -1733,7 +1993,7 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
1733
1993
|
})}
|
|
1734
1994
|
`));
|
|
1735
1995
|
syncDirectory(directory);
|
|
1736
|
-
syncDirectory(
|
|
1996
|
+
syncDirectory(dirname3(directory));
|
|
1737
1997
|
const finalLock = readSessionLock(lock.marker.sessionId, cacheOptions);
|
|
1738
1998
|
if (!finalLock || finalLock.dev !== lock.dev || finalLock.ino !== lock.ino || finalLock.sha256 !== lock.sha256 || readSkillSessionSnapshotIfExists(lock.marker.sessionId, cacheOptions)?.sha256 !== receipt?.sha256)
|
|
1739
1999
|
return null;
|
|
@@ -1743,16 +2003,16 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
1743
2003
|
moved = true;
|
|
1744
2004
|
const archivedPaths = [archivePath];
|
|
1745
2005
|
if (finalLock.stagingPath) {
|
|
1746
|
-
const publicationPath =
|
|
2006
|
+
const publicationPath = join6(directory, "publication.write-lock");
|
|
1747
2007
|
renameSync(finalLock.stagingPath, publicationPath);
|
|
1748
2008
|
archivedPaths.push(publicationPath);
|
|
1749
2009
|
}
|
|
1750
2010
|
syncDirectory(directory);
|
|
1751
|
-
syncDirectory(
|
|
2011
|
+
syncDirectory(dirname3(lock.path));
|
|
1752
2012
|
for (const path of archivedPaths) {
|
|
1753
|
-
const before =
|
|
2013
|
+
const before = lstatSync3(path, { throwIfNoEntry: false });
|
|
1754
2014
|
const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
|
|
1755
|
-
const after =
|
|
2015
|
+
const after = lstatSync3(path, { throwIfNoEntry: false });
|
|
1756
2016
|
if (!before?.isFile() || before.dev !== lock.dev || before.ino !== lock.ino || before.nlink !== archivedPaths.length || before.mtimeMs !== lock.mtimeMs || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid() || !bytes || sha256Hex(bytes) !== lock.sha256 || !after?.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs || before.mode !== after.mode || before.uid !== after.uid || before.gid !== after.gid || before.nlink !== after.nlink) {
|
|
1757
2017
|
throw new Error("Archived lock ownership or bytes changed");
|
|
1758
2018
|
}
|
|
@@ -1765,19 +2025,68 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
|
|
|
1765
2025
|
return archivePath;
|
|
1766
2026
|
} finally {
|
|
1767
2027
|
closeSync2(guardFd);
|
|
1768
|
-
const current =
|
|
2028
|
+
const current = lstatSync3(guardPath, { throwIfNoEntry: false });
|
|
1769
2029
|
if (current?.isFile() && current.dev === guardStat.dev && current.ino === guardStat.ino)
|
|
1770
2030
|
unlinkSync(guardPath);
|
|
1771
2031
|
}
|
|
1772
2032
|
}
|
|
1773
|
-
|
|
2033
|
+
var SESSION_WRITE_LOCK_WAIT_MS = 2000;
|
|
2034
|
+
var SESSION_WRITE_LOCK_RETRY_MIN_MS = 25;
|
|
2035
|
+
var SESSION_WRITE_LOCK_RETRY_MAX_MS = 100;
|
|
2036
|
+
function lockStatIdentity(path) {
|
|
2037
|
+
try {
|
|
2038
|
+
const stat = lstatSync3(path, { throwIfNoEntry: false });
|
|
2039
|
+
return stat ? `${stat.dev}:${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.nlink}:${stat.size}` : null;
|
|
2040
|
+
} catch {
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function sleepSync(ms) {
|
|
2045
|
+
if (ms > 0)
|
|
2046
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
2047
|
+
}
|
|
2048
|
+
var SESSION_LOCK_INSPECTIONS = 3;
|
|
2049
|
+
function inspectSessionLockSettled(sessionId, options) {
|
|
2050
|
+
const path = `${sessionReceiptPath(sessionId, options)}.write-lock`;
|
|
2051
|
+
let invalid;
|
|
2052
|
+
for (let attempt = 0;attempt < SESSION_LOCK_INSPECTIONS; attempt++) {
|
|
2053
|
+
try {
|
|
2054
|
+
return readSessionLock(sessionId, options);
|
|
2055
|
+
} catch (error) {
|
|
2056
|
+
if (!(error instanceof SkillSelectionError) || error.code !== "SESSION_WRITE_LOCKED")
|
|
2057
|
+
throw error;
|
|
2058
|
+
const seen = lockStatIdentity(path);
|
|
2059
|
+
if (seen !== null && seen === invalid)
|
|
2060
|
+
throw error;
|
|
2061
|
+
invalid = seen;
|
|
2062
|
+
sleepSync(SESSION_WRITE_LOCK_RETRY_MIN_MS);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return "changing";
|
|
2066
|
+
}
|
|
2067
|
+
function assertSessionWriteLockReadable(sessionId, options = {}) {
|
|
2068
|
+
for (let attempt = 0;; attempt++) {
|
|
2069
|
+
try {
|
|
2070
|
+
readSessionLock(sessionId, options);
|
|
2071
|
+
return;
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
if (attempt >= 2 || !(error instanceof SkillSelectionError) || error.code !== "SESSION_WRITE_LOCKED")
|
|
2074
|
+
throw error;
|
|
2075
|
+
sleepSync(SESSION_WRITE_LOCK_RETRY_MIN_MS);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
function withSessionWriteLocks(sessionIds, options, action, waitMs = 0) {
|
|
1774
2080
|
const operationId = randomUUID();
|
|
1775
2081
|
const identity = localLockIdentity();
|
|
2082
|
+
if (!Number.isFinite(waitMs) || waitMs < 0 || waitMs > SESSION_WRITE_LOCK_WAIT_MS)
|
|
2083
|
+
throw new SkillSelectionError("INVALID_CONTEXT_BUDGET", "A session write lock wait must be within its supported bound.");
|
|
2084
|
+
const waitUntil = performance.now() + waitMs;
|
|
1776
2085
|
const locks = [...new Set(sessionIds)].map((sessionId) => ({ sessionId, path: `${sessionReceiptPath(sessionId, options)}.write-lock` })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1777
2086
|
const owned = [];
|
|
1778
2087
|
const assertOwned = () => {
|
|
1779
2088
|
for (const lock of owned) {
|
|
1780
|
-
const current =
|
|
2089
|
+
const current = lstatSync3(lock.path, { throwIfNoEntry: false });
|
|
1781
2090
|
if (!current?.isFile() || current.dev !== lock.dev || current.ino !== lock.ino) {
|
|
1782
2091
|
throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "A session write lock changed during the operation; no replacement is permitted.");
|
|
1783
2092
|
}
|
|
@@ -1788,11 +2097,14 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
1788
2097
|
assertRegularPath(lock.path, true);
|
|
1789
2098
|
const marker = { schemaVersion: identity ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity };
|
|
1790
2099
|
let acquired;
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2100
|
+
while (!acquired) {
|
|
2101
|
+
try {
|
|
2102
|
+
acquired = publishSessionLock(lock.path, marker);
|
|
2103
|
+
break;
|
|
2104
|
+
} catch (error) {
|
|
2105
|
+
if (error.code !== "EEXIST")
|
|
2106
|
+
throw error;
|
|
2107
|
+
}
|
|
1796
2108
|
try {
|
|
1797
2109
|
const stale = readSessionLock(lock.sessionId, options);
|
|
1798
2110
|
const receipt = stale && readSkillSessionSnapshotIfExists(lock.sessionId, options);
|
|
@@ -1805,14 +2117,20 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
1805
2117
|
}
|
|
1806
2118
|
try {
|
|
1807
2119
|
acquired = publishSessionLock(lock.path, marker);
|
|
2120
|
+
break;
|
|
1808
2121
|
} catch (retryError) {
|
|
1809
|
-
if (retryError.code
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2122
|
+
if (retryError.code !== "EEXIST")
|
|
2123
|
+
throw retryError;
|
|
2124
|
+
}
|
|
2125
|
+
const held = inspectSessionLockSettled(lock.sessionId, options);
|
|
2126
|
+
const remaining = waitUntil - performance.now();
|
|
2127
|
+
if (remaining <= 0) {
|
|
2128
|
+
if (!held)
|
|
2129
|
+
throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The session lock changed during acquisition.");
|
|
2130
|
+
throw new SkillSelectionError("SESSION_WRITE_BUSY", "Another Skills process owns a required session write lock; retry after it completes. A surviving legacy lock requires explicit recovery review.");
|
|
1815
2131
|
}
|
|
2132
|
+
if (held)
|
|
2133
|
+
sleepSync(Math.min(remaining, SESSION_WRITE_LOCK_RETRY_MIN_MS + Math.floor(Math.random() * (SESSION_WRITE_LOCK_RETRY_MAX_MS - SESSION_WRITE_LOCK_RETRY_MIN_MS))));
|
|
1816
2134
|
}
|
|
1817
2135
|
owned.push(acquired);
|
|
1818
2136
|
}
|
|
@@ -1821,7 +2139,7 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
1821
2139
|
for (const lock of owned.reverse()) {
|
|
1822
2140
|
try {
|
|
1823
2141
|
for (const path of [lock.path, lock.stagingPath]) {
|
|
1824
|
-
const current =
|
|
2142
|
+
const current = lstatSync3(path, { throwIfNoEntry: false });
|
|
1825
2143
|
if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
|
|
1826
2144
|
unlinkSync(path);
|
|
1827
2145
|
}
|
|
@@ -1831,8 +2149,8 @@ function withSessionWriteLocks(sessionIds, options, action) {
|
|
|
1831
2149
|
}
|
|
1832
2150
|
}
|
|
1833
2151
|
}
|
|
1834
|
-
function withSessionWriteLock(sessionId, options, action) {
|
|
1835
|
-
return withSessionWriteLocks([sessionId], options, action);
|
|
2152
|
+
function withSessionWriteLock(sessionId, options, action, waitMs = 0) {
|
|
2153
|
+
return withSessionWriteLocks([sessionId], options, action, waitMs);
|
|
1836
2154
|
}
|
|
1837
2155
|
function sameSnapshot(actual, expected) {
|
|
1838
2156
|
return actual === null ? expected === null : expected !== null && actual.sessionId === expected.sessionId && actual.generation === expected.generation && actual.receiptSha256 === expected.receiptSha256;
|
|
@@ -1873,7 +2191,7 @@ function writeSkillSession(receipt, expected, options = {}) {
|
|
|
1873
2191
|
parentChanged();
|
|
1874
2192
|
}
|
|
1875
2193
|
writeSelectionJson(sessionReceiptPath(receipt.sessionId, options), next);
|
|
1876
|
-
});
|
|
2194
|
+
}, SESSION_WRITE_LOCK_WAIT_MS);
|
|
1877
2195
|
}
|
|
1878
2196
|
function syncDirectory(path) {
|
|
1879
2197
|
assertRegularPath(path);
|
|
@@ -1909,8 +2227,8 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
|
|
|
1909
2227
|
if (before.receipt.profile.authority !== replacement.profile.authority || before.receipt.profile.workspaceId !== replacement.profile.workspaceId) {
|
|
1910
2228
|
throw new SkillSelectionError("PROFILE_IDENTITY_MISMATCH", "A session reconciliation cannot change its Skills authority or workspace.");
|
|
1911
2229
|
}
|
|
1912
|
-
const directory =
|
|
1913
|
-
const archivePath =
|
|
2230
|
+
const directory = join6(selectionCacheRoot(options), "session-reconciliations", randomUUID());
|
|
2231
|
+
const archivePath = join6(directory, "original.json"), replacementPath = join6(directory, "replacement.json"), receiptPath = join6(directory, "receipt.json");
|
|
1914
2232
|
const afterSha256 = sha256Hex(bytes);
|
|
1915
2233
|
archiveBytes(archivePath, before.bytes);
|
|
1916
2234
|
archiveBytes(replacementPath, bytes);
|
|
@@ -1918,7 +2236,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
|
|
|
1918
2236
|
archiveBytes(receiptPath, Buffer.from(`${JSON.stringify(receipt)}
|
|
1919
2237
|
`));
|
|
1920
2238
|
syncDirectory(directory);
|
|
1921
|
-
syncDirectory(
|
|
2239
|
+
syncDirectory(dirname3(directory));
|
|
1922
2240
|
syncDirectory(selectionCacheRoot(options));
|
|
1923
2241
|
if (sha256Hex(readRegularFile(archivePath, MAX_PROFILE_DOCUMENT_BYTES)) !== before.sha256 || !sameSnapshot(readSkillSessionSnapshot(replacement.sessionId, options), expected)) {
|
|
1924
2242
|
throw new SkillSelectionError("SESSION_RECEIPT_CHANGED", "The session or its preservation archive changed before replacement; retained evidence must be reviewed.");
|
|
@@ -1933,11 +2251,11 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
|
|
|
1933
2251
|
throw new SkillSelectionError("SESSION_RECONCILIATION_INCOMPLETE", `Inspect sessions show and the preserved operation receipt at ${receiptPath} before any retry; replacement may already have committed.`);
|
|
1934
2252
|
}
|
|
1935
2253
|
return { archivePath, receiptPath, beforeSha256: before.sha256, afterSha256 };
|
|
1936
|
-
});
|
|
2254
|
+
}, SESSION_WRITE_LOCK_WAIT_MS);
|
|
1937
2255
|
}
|
|
1938
2256
|
|
|
1939
2257
|
// src/lib/agent-policy-limits.ts
|
|
1940
|
-
import { basename as basename4, isAbsolute as isAbsolute5, join as
|
|
2258
|
+
import { basename as basename4, isAbsolute as isAbsolute5, join as join7, resolve as resolve7, sep as sep2 } from "path";
|
|
1941
2259
|
|
|
1942
2260
|
// src/lib/codex-native-compatibility.ts
|
|
1943
2261
|
var MEASURED_CODEX_CAPABILITIES = {
|
|
@@ -1956,7 +2274,10 @@ var MEASURED_CODEX_CAPABILITIES = {
|
|
|
1956
2274
|
"codex-cli 0.161.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
|
|
1957
2275
|
"codex-cli 0.162.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
|
|
1958
2276
|
"codex-cli 0.162.1": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
|
|
1959
|
-
"codex-cli 0.163.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"]
|
|
2277
|
+
"codex-cli 0.163.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
|
|
2278
|
+
"codex-cli 0.164.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
|
|
2279
|
+
"codex-cli 0.165.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
|
|
2280
|
+
"codex-cli 0.166.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"]
|
|
1960
2281
|
};
|
|
1961
2282
|
function supportsCodexNativeCapability(version2, capability) {
|
|
1962
2283
|
return typeof version2 === "string" && Object.hasOwn(MEASURED_CODEX_CAPABILITIES, version2) && MEASURED_CODEX_CAPABILITIES[version2].includes(capability);
|
|
@@ -1964,37 +2285,41 @@ function supportsCodexNativeCapability(version2, capability) {
|
|
|
1964
2285
|
var SUPPORTED_CODEX_HOOK_VERSIONS = Object.freeze(Object.keys(MEASURED_CODEX_CAPABILITIES).filter((version2) => supportsCodexNativeCapability(version2, "hooks")));
|
|
1965
2286
|
|
|
1966
2287
|
// src/lib/claude-marketplace-entry-witness.ts
|
|
1967
|
-
import { createHash as
|
|
1968
|
-
import { basename as basename2, dirname as
|
|
2288
|
+
import { createHash as createHash6 } from "crypto";
|
|
2289
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, resolve as resolve5 } from "path";
|
|
1969
2290
|
|
|
1970
2291
|
// src/lib/claude-settings-witness.ts
|
|
1971
|
-
import { createHash as
|
|
1972
|
-
import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync3, lstatSync as
|
|
1973
|
-
import { basename, dirname as
|
|
2292
|
+
import { createHash as createHash5 } from "crypto";
|
|
2293
|
+
import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync3, lstatSync as lstatSync4, openSync as openSync3, readSync as readSync2 } from "fs";
|
|
2294
|
+
import { basename, dirname as dirname4, isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
|
|
1974
2295
|
var CLAUDE_SETTINGS_WITNESS_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
|
|
1975
2296
|
function need(value, reason) {
|
|
1976
2297
|
if (!value)
|
|
1977
2298
|
throw new Error(`Claude settings witness ${reason}`);
|
|
1978
2299
|
}
|
|
1979
2300
|
function absolutePath(value) {
|
|
1980
|
-
need(typeof value === "string" && value.length > 0 && value.length <= CLAUDE_SETTINGS_WITNESS_LIMITS.pathCharacters && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute2(value) &&
|
|
2301
|
+
need(typeof value === "string" && value.length > 0 && value.length <= CLAUDE_SETTINGS_WITNESS_LIMITS.pathCharacters && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute2(value) && resolve4(value) === value, "requires a normalized absolute path");
|
|
1981
2302
|
}
|
|
1982
2303
|
var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
|
|
1983
2304
|
function parents(path) {
|
|
1984
2305
|
const result = [];
|
|
1985
|
-
for (let at =
|
|
1986
|
-
const stat =
|
|
2306
|
+
for (let at = dirname4(path);; at = dirname4(at)) {
|
|
2307
|
+
const stat = lstatSync4(at, { bigint: true });
|
|
1987
2308
|
need(stat.isDirectory() && !stat.isSymbolicLink(), "path traverses a symlink or non-directory");
|
|
1988
2309
|
result.push([at, stat]);
|
|
1989
|
-
if (at ===
|
|
2310
|
+
if (at === dirname4(at))
|
|
1990
2311
|
return result;
|
|
1991
2312
|
}
|
|
1992
2313
|
}
|
|
1993
2314
|
function readNativeSettingsWitnessFile(path, budget, filename = "settings.json") {
|
|
1994
2315
|
absolutePath(path);
|
|
1995
2316
|
need(basename(path) === filename, `requires ${filename}`);
|
|
2317
|
+
return readNativeDiscoveryWitnessFile(path, budget);
|
|
2318
|
+
}
|
|
2319
|
+
function readNativeDiscoveryWitnessFile(path, budget) {
|
|
2320
|
+
absolutePath(path);
|
|
1996
2321
|
need(Number.isSafeInteger(budget.remaining) && budget.remaining >= 0, "has an invalid byte budget");
|
|
1997
|
-
const ancestors = parents(path), initial =
|
|
2322
|
+
const ancestors = parents(path), initial = lstatSync4(path, { bigint: true });
|
|
1998
2323
|
need(initial.isFile() && !initial.isSymbolicLink() && initial.size <= BigInt(CLAUDE_SETTINGS_WITNESS_LIMITS.bytes), "is not a bounded regular file");
|
|
1999
2324
|
need(initial.size <= BigInt(budget.remaining), "exceeds its aggregate byte limit");
|
|
2000
2325
|
const fd = openSync3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
|
|
@@ -2009,10 +2334,10 @@ function readNativeSettingsWitnessFile(path, budget, filename = "settings.json")
|
|
|
2009
2334
|
break;
|
|
2010
2335
|
length += count;
|
|
2011
2336
|
}
|
|
2012
|
-
const after = fstatSync3(fd, { bigint: true }), current =
|
|
2337
|
+
const after = fstatSync3(fd, { bigint: true }), current = lstatSync4(path, { bigint: true });
|
|
2013
2338
|
need(current.isFile() && sameFile(opened, after) && sameFile(after, current) && BigInt(length) === opened.size, "changed during read");
|
|
2014
2339
|
for (const [ancestor, before] of ancestors) {
|
|
2015
|
-
const now =
|
|
2340
|
+
const now = lstatSync4(ancestor, { bigint: true });
|
|
2016
2341
|
need(now.isDirectory() && before.dev === now.dev && before.ino === now.ino && before.mode === now.mode, "parent changed during read");
|
|
2017
2342
|
}
|
|
2018
2343
|
budget.remaining -= length;
|
|
@@ -2232,7 +2557,7 @@ function settingsDigest(text, version2 = 1) {
|
|
|
2232
2557
|
}
|
|
2233
2558
|
return true;
|
|
2234
2559
|
});
|
|
2235
|
-
return
|
|
2560
|
+
return createHash5("sha256").update(`hasna.skills.claude-settings.v${version2}\x00`).update(JSON.stringify(canonical(value))).digest("hex");
|
|
2236
2561
|
}
|
|
2237
2562
|
function hashClaudeSettingsReplacement(text, budget) {
|
|
2238
2563
|
need(typeof text === "string", "requires settings text");
|
|
@@ -2268,7 +2593,7 @@ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
|
|
|
2268
2593
|
need(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === undefined || previous.hashMode === "bytes" || previous.hashMode === "claude-settings-v1" || previous.hashMode === "claude-settings-v2") && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact legacy settings witness");
|
|
2269
2594
|
const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
|
|
2270
2595
|
const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
|
|
2271
|
-
const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ?
|
|
2596
|
+
const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash5("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : 2);
|
|
2272
2597
|
need(beforeDigest === previous.sha256, "reviewed preimage does not match the legacy witness");
|
|
2273
2598
|
const sha256 = settingsDigest(current, 3);
|
|
2274
2599
|
need(settingsDigest(before, 3) === sha256, "non-preference settings changed; explicit discovery review required");
|
|
@@ -2288,12 +2613,91 @@ function upgradeClaudeSettingsWitnessV4(previous, reviewedSettingsPath) {
|
|
|
2288
2613
|
need(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === undefined || previous.hashMode === "bytes" || previous.hashMode === "claude-settings-v1" || previous.hashMode === "claude-settings-v2" || previous.hashMode === "claude-settings-v3") && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact prior settings witness");
|
|
2289
2614
|
const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
|
|
2290
2615
|
const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
|
|
2291
|
-
const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ?
|
|
2616
|
+
const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash5("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : previous.hashMode === "claude-settings-v2" ? 2 : 3);
|
|
2292
2617
|
need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
|
|
2293
2618
|
const sha256 = settingsDigest(current, 4);
|
|
2294
2619
|
need(settingsDigest(before, 4) === sha256, "non-preference settings changed; explicit discovery review required");
|
|
2295
2620
|
return { path: previous.path, hashMode: "claude-settings-v4", sha256 };
|
|
2296
2621
|
}
|
|
2622
|
+
var V5_DISCOVERY_KEY = /skill|plugin|hook|marketplace/i;
|
|
2623
|
+
var V5_BOUND_KEYS = new Set(["apiKeyHelper", "awsAuthRefresh", "awsCredentialExport", "otelHeadersHelper", "fileSuggestion", "outputStyle", "agent"]);
|
|
2624
|
+
var V5_DISCOVERY_ENV = new Set(["CLAUDE_CONFIG_DIR"]);
|
|
2625
|
+
var V5_SKILL_RULE = /^Skill(?:\(|$)/;
|
|
2626
|
+
var V5_PERMISSION_LISTS = new Set(["allow", "deny", "ask"]);
|
|
2627
|
+
function narrowedClaudeSettings(value) {
|
|
2628
|
+
const entries = [];
|
|
2629
|
+
for (const [key, child] of value.entries) {
|
|
2630
|
+
if (V5_DISCOVERY_KEY.test(key) || V5_BOUND_KEYS.has(key)) {
|
|
2631
|
+
entries.push([key, child]);
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
if (key === "permissions") {
|
|
2635
|
+
if (child.kind !== "object") {
|
|
2636
|
+
entries.push([key, child]);
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
const kept = [];
|
|
2640
|
+
for (const [name, rules] of child.entries) {
|
|
2641
|
+
if (name === "additionalDirectories") {
|
|
2642
|
+
kept.push([name, rules]);
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
if (!V5_PERMISSION_LISTS.has(name))
|
|
2646
|
+
continue;
|
|
2647
|
+
if (rules.kind !== "array") {
|
|
2648
|
+
kept.push([name, rules]);
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
const skillRules = rules.items.filter((rule) => rule.kind !== "string" || V5_SKILL_RULE.test(rule.value));
|
|
2652
|
+
if (skillRules.length)
|
|
2653
|
+
kept.push([name, { kind: "array", items: skillRules }]);
|
|
2654
|
+
}
|
|
2655
|
+
if (kept.length)
|
|
2656
|
+
entries.push([key, { kind: "object", entries: kept }]);
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
if (key === "env") {
|
|
2660
|
+
if (child.kind !== "object") {
|
|
2661
|
+
entries.push([key, child]);
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
const kept = child.entries.filter(([name]) => V5_DISCOVERY_ENV.has(name) || V5_DISCOVERY_KEY.test(name));
|
|
2665
|
+
if (kept.length)
|
|
2666
|
+
entries.push([key, { kind: "object", entries: kept }]);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
return { kind: "object", entries };
|
|
2670
|
+
}
|
|
2671
|
+
function claudeDiscoveryControlKeys(text) {
|
|
2672
|
+
return narrowedClaudeSettings(parseNativeJsonObject(text)).entries.map(([key]) => key);
|
|
2673
|
+
}
|
|
2674
|
+
function claudeDiscoveryEnvironmentKeys(text) {
|
|
2675
|
+
const environment = narrowedClaudeSettings(parseNativeJsonObject(text)).entries.find(([key]) => key === "env")?.[1];
|
|
2676
|
+
return environment?.kind === "object" ? environment.entries.map(([key]) => key) : [];
|
|
2677
|
+
}
|
|
2678
|
+
function settingsDigestV5(text) {
|
|
2679
|
+
return createHash5("sha256").update("hasna.skills.claude-settings.v5\x00").update(JSON.stringify(canonical(narrowedClaudeSettings(parse2(text))))).digest("hex");
|
|
2680
|
+
}
|
|
2681
|
+
function captureClaudeSettingsV5(path, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
2682
|
+
return { path, hashMode: "claude-settings-v5", sha256: settingsDigestV5(readNativeSettingsWitnessFile(path, budget)) };
|
|
2683
|
+
}
|
|
2684
|
+
function hashClaudeSettingsReplacementV5(text, budget) {
|
|
2685
|
+
need(typeof text === "string", "requires settings text");
|
|
2686
|
+
const bytes = Buffer.byteLength(text);
|
|
2687
|
+
need(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
|
|
2688
|
+
budget.remaining -= bytes;
|
|
2689
|
+
return settingsDigestV5(text);
|
|
2690
|
+
}
|
|
2691
|
+
function upgradeClaudeSettingsWitnessV5(previous, reviewedSettingsPath) {
|
|
2692
|
+
need(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === undefined || ["bytes", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4"].includes(previous.hashMode)) && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact prior settings witness");
|
|
2693
|
+
const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
|
|
2694
|
+
const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
|
|
2695
|
+
const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash5("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : previous.hashMode === "claude-settings-v2" ? 2 : previous.hashMode === "claude-settings-v3" ? 3 : 4);
|
|
2696
|
+
need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
|
|
2697
|
+
const sha256 = settingsDigestV5(current);
|
|
2698
|
+
need(settingsDigestV5(before) === sha256, "discovery settings changed; explicit discovery review required");
|
|
2699
|
+
return { path: previous.path, hashMode: "claude-settings-v5", sha256 };
|
|
2700
|
+
}
|
|
2297
2701
|
function hashNativeJsonControls(text, excludedMetadata) {
|
|
2298
2702
|
need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
|
|
2299
2703
|
const value = parse2(text);
|
|
@@ -2303,7 +2707,7 @@ function hashNativeJsonControls(text, excludedMetadata) {
|
|
|
2303
2707
|
need(child.kind === "string" && child.value.length <= 128, "has invalid version metadata");
|
|
2304
2708
|
return false;
|
|
2305
2709
|
});
|
|
2306
|
-
return
|
|
2710
|
+
return createHash5("sha256").update("hasna.skills.native-json-controls.v1\x00").update(JSON.stringify(canonical(value))).digest("hex");
|
|
2307
2711
|
}
|
|
2308
2712
|
function hashSumiNativeJsonControls(text) {
|
|
2309
2713
|
need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
|
|
@@ -2324,7 +2728,7 @@ function hashSumiNativeJsonControls(text) {
|
|
|
2324
2728
|
});
|
|
2325
2729
|
return child.entries.length > 0;
|
|
2326
2730
|
});
|
|
2327
|
-
return
|
|
2731
|
+
return createHash5("sha256").update("hasna.skills.sumi-settings.v1\x00").update(JSON.stringify(value)).digest("hex");
|
|
2328
2732
|
}
|
|
2329
2733
|
|
|
2330
2734
|
// src/lib/claude-marketplace-entry-witness.ts
|
|
@@ -2392,7 +2796,7 @@ var stringArray = (value) => value?.kind === "array" && value.items.every(isStri
|
|
|
2392
2796
|
var stringObject = (value) => value?.kind === "object" && value.entries.every(([, item]) => isString(item));
|
|
2393
2797
|
function claudeMarketplaceEntrySourceValid(source) {
|
|
2394
2798
|
const path = source?.path;
|
|
2395
|
-
return typeof path === "string" && path.length > 0 && path.length <= PATH_CHARACTERS && !/[\x00-\x1f\x7f]/.test(path) && isAbsolute3(path) &&
|
|
2799
|
+
return typeof path === "string" && path.length > 0 && path.length <= PATH_CHARACTERS && !/[\x00-\x1f\x7f]/.test(path) && isAbsolute3(path) && resolve5(path) === path && basename2(path) === "marketplace.json" && basename2(dirname5(path)) === ".claude-plugin" && typeof source.marketplace === "string" && NAME.test(source.marketplace) && !source.marketplace.includes("..") && typeof source.plugin === "string" && NAME.test(source.plugin);
|
|
2396
2800
|
}
|
|
2397
2801
|
function canonical2(value) {
|
|
2398
2802
|
if (value.kind === "string")
|
|
@@ -2505,7 +2909,7 @@ function hashClaudeMarketplaceEntry(text, selector) {
|
|
|
2505
2909
|
["allowCrossMarketplaceDependenciesOn", field(root, "allowCrossMarketplaceDependenciesOn") ?? nullValue]
|
|
2506
2910
|
] };
|
|
2507
2911
|
const projection = { kind: "object", entries: [["marketplace", marketplace], ["plugin", entry]] };
|
|
2508
|
-
return
|
|
2912
|
+
return createHash6("sha256").update(DOMAIN).update(canonical2(projection)).digest("hex");
|
|
2509
2913
|
}
|
|
2510
2914
|
function captureClaudeMarketplaceEntry(path, marketplace, plugin, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
2511
2915
|
need2(claudeMarketplaceEntrySourceValid({ path, marketplace, plugin }), "requires a normalized absolute .claude-plugin/marketplace.json path and exact names");
|
|
@@ -2519,13 +2923,13 @@ function captureClaudeMarketplaceEntry(path, marketplace, plugin, budget = { rem
|
|
|
2519
2923
|
}
|
|
2520
2924
|
|
|
2521
2925
|
// src/lib/claude-plugin-cache-marker.ts
|
|
2522
|
-
import { createHash as
|
|
2523
|
-
import { closeSync as closeSync4, constants as constants4, fstatSync as fstatSync4, lstatSync as
|
|
2524
|
-
import { basename as basename3, dirname as
|
|
2926
|
+
import { createHash as createHash7 } from "crypto";
|
|
2927
|
+
import { closeSync as closeSync4, constants as constants4, fstatSync as fstatSync4, lstatSync as lstatSync5, openSync as openSync4, readSync as readSync3 } from "fs";
|
|
2928
|
+
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute4, resolve as resolve6, sep } from "path";
|
|
2525
2929
|
var CLAUDE_ORPHAN_MARKER_MODE = "claude-plugin-orphaned-at-v1";
|
|
2526
2930
|
var CACHE = `${sep}.claude${sep}plugins${sep}cache${sep}`;
|
|
2527
2931
|
function isClaudePluginCacheOrphanMarker(path) {
|
|
2528
|
-
if (typeof path !== "string" || basename3(path) !== ".orphaned_at" || !isAbsolute4(path) ||
|
|
2932
|
+
if (typeof path !== "string" || basename3(path) !== ".orphaned_at" || !isAbsolute4(path) || resolve6(path) !== path || path.includes("\x00"))
|
|
2529
2933
|
return false;
|
|
2530
2934
|
const at = path.indexOf(CACHE);
|
|
2531
2935
|
if (at < 0 || path.indexOf(CACHE, at + 1) >= 0)
|
|
@@ -2537,10 +2941,10 @@ function same(left, right) {
|
|
|
2537
2941
|
return Boolean(right && right.isFile() && left.dev === right.dev && left.ino === right.ino && left.uid === right.uid && left.mode === right.mode && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs);
|
|
2538
2942
|
}
|
|
2539
2943
|
function safe(path) {
|
|
2540
|
-
for (let cursor = path;; cursor =
|
|
2541
|
-
if (
|
|
2944
|
+
for (let cursor = path;; cursor = dirname6(cursor)) {
|
|
2945
|
+
if (lstatSync5(cursor, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
2542
2946
|
throw new Error("Refusing symlink Claude orphan marker input");
|
|
2543
|
-
if (
|
|
2947
|
+
if (dirname6(cursor) === cursor)
|
|
2544
2948
|
break;
|
|
2545
2949
|
}
|
|
2546
2950
|
}
|
|
@@ -2550,7 +2954,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
|
|
|
2550
2954
|
if (changes?.has(path))
|
|
2551
2955
|
throw new Error("Claude orphan markers cannot intersect a planned write");
|
|
2552
2956
|
safe(path);
|
|
2553
|
-
const before =
|
|
2957
|
+
const before = lstatSync5(path, { throwIfNoEntry: false });
|
|
2554
2958
|
if (!before)
|
|
2555
2959
|
return null;
|
|
2556
2960
|
const uid = process.getuid?.();
|
|
@@ -2571,7 +2975,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
|
|
|
2571
2975
|
}
|
|
2572
2976
|
if (!/^[1-9][0-9]{12}$/.test(bytes.subarray(0, length).toString("latin1")))
|
|
2573
2977
|
throw new Error("Malformed Claude orphan marker timestamp");
|
|
2574
|
-
const after = fstatSync4(fd), current =
|
|
2978
|
+
const after = fstatSync4(fd), current = lstatSync5(path, { throwIfNoEntry: false });
|
|
2575
2979
|
safe(path);
|
|
2576
2980
|
if (!same(opened, after) || !same(after, current) || length !== after.size)
|
|
2577
2981
|
throw new Error("Claude orphan marker changed during read");
|
|
@@ -2580,7 +2984,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
|
|
|
2580
2984
|
throw new Error("Raw discovery aggregate byte limit exceeded");
|
|
2581
2985
|
budget.remaining -= length;
|
|
2582
2986
|
}
|
|
2583
|
-
return
|
|
2987
|
+
return createHash7("sha256").update(`${CLAUDE_ORPHAN_MARKER_MODE}\x00${path}\x00file`).digest("hex");
|
|
2584
2988
|
} finally {
|
|
2585
2989
|
closeSync4(fd);
|
|
2586
2990
|
}
|
|
@@ -2640,7 +3044,7 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2640
3044
|
text(control.pluginId, 1024);
|
|
2641
3045
|
text(control.namespace, 64);
|
|
2642
3046
|
text(control.pluginParent);
|
|
2643
|
-
requireBound(isAbsolute5(control.pluginParent) &&
|
|
3047
|
+
requireBound(isAbsolute5(control.pluginParent) && resolve7(control.pluginParent) === control.pluginParent && /^[a-f0-9]{64}$/.test(control.manifestSha256));
|
|
2644
3048
|
if (control.remotePluginId !== undefined)
|
|
2645
3049
|
requireBound(typeof control.remotePluginId === "string" && /^[A-Za-z0-9_~-]{1,1024}$/.test(control.remotePluginId));
|
|
2646
3050
|
if (control.skillsOnly !== undefined)
|
|
@@ -2658,7 +3062,7 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2658
3062
|
text(control.pluginId, 1024);
|
|
2659
3063
|
text(control.namespace, 64);
|
|
2660
3064
|
text(control.pluginParent);
|
|
2661
|
-
requireBound(!ids.has(control.pluginId) && isAbsolute5(control.pluginParent) &&
|
|
3065
|
+
requireBound(!ids.has(control.pluginId) && isAbsolute5(control.pluginParent) && resolve7(control.pluginParent) === control.pluginParent && ["local", "git", "npm"].includes(control.sourceType) && typeof control.sourceSha256 === "string" && /^[a-f0-9]{64}$/.test(control.sourceSha256));
|
|
2662
3066
|
ids.add(control.pluginId);
|
|
2663
3067
|
}
|
|
2664
3068
|
}
|
|
@@ -2667,7 +3071,7 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2667
3071
|
requireBound(agent === "gemini" && object(executable) && Object.keys(executable).length === 3 && executable.command === "gemini");
|
|
2668
3072
|
for (const path of [executable.path, executable.target]) {
|
|
2669
3073
|
text(path);
|
|
2670
|
-
requireBound(!/[\x00-\x1f\x7f-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]/.test(path) && isAbsolute5(path) &&
|
|
3074
|
+
requireBound(!/[\x00-\x1f\x7f-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]/.test(path) && isAbsolute5(path) && resolve7(path) === path);
|
|
2671
3075
|
}
|
|
2672
3076
|
}
|
|
2673
3077
|
if (bridge.disabledBuiltins !== undefined)
|
|
@@ -2695,11 +3099,11 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2695
3099
|
requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && Object.keys(proof).every((key) => ["roots", "parents", "directories"].includes(key)) && roots.length + parents2.length > 0 && new Set(roots).size === roots.length && new Set(parents2).size === parents2.length);
|
|
2696
3100
|
for (const root of roots) {
|
|
2697
3101
|
text(root);
|
|
2698
|
-
requireBound(isAbsolute5(root) &&
|
|
3102
|
+
requireBound(isAbsolute5(root) && resolve7(root) === root);
|
|
2699
3103
|
}
|
|
2700
3104
|
for (const parent of parents2) {
|
|
2701
3105
|
text(parent);
|
|
2702
|
-
requireBound(isAbsolute5(parent) &&
|
|
3106
|
+
requireBound(isAbsolute5(parent) && resolve7(parent) === parent);
|
|
2703
3107
|
}
|
|
2704
3108
|
for (const directory of array(proof.directories, AGENT_POLICY_LIMITS.discoveryDirectories)) {
|
|
2705
3109
|
requireBound(object(directory));
|
|
@@ -2719,7 +3123,7 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2719
3123
|
text(input.namespace, 64);
|
|
2720
3124
|
text(input.pluginParent);
|
|
2721
3125
|
text(input.sourceRoot);
|
|
2722
|
-
requireBound(!ids.has(input.pluginId) && [input.pluginParent, input.sourceRoot].every((path) => isAbsolute5(path) &&
|
|
3126
|
+
requireBound(!ids.has(input.pluginId) && [input.pluginParent, input.sourceRoot].every((path) => isAbsolute5(path) && resolve7(path) === path) && typeof input.sourceSha256 === "string" && /^[a-f0-9]{64}$/.test(input.sourceSha256));
|
|
2723
3127
|
ids.add(input.pluginId);
|
|
2724
3128
|
}
|
|
2725
3129
|
if (proof.directories !== undefined)
|
|
@@ -2733,14 +3137,14 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2733
3137
|
const proof = record(value.codexSemanticCache, 9), witness = record(proof.witness, 7);
|
|
2734
3138
|
requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && [8, 9].includes(Object.keys(proof).length) && Object.keys(proof).every((key) => ["schema", "configPath", "hookRootAlias", "witness", "appOnlyParents", "previousPolicySha256", "supersededSources", "supersededDirectories", "reviewedHookParents"].includes(key)) && proof.schema === "hasna.codex-semantic-cache-discovery.v1" && typeof proof.previousPolicySha256 === "string" && /^[a-f0-9]{64}$/.test(proof.previousPolicySha256) && [6, 7].includes(Object.keys(witness).length) && Object.keys(witness).every((key) => ["schema", "cacheRoot", "controls", "appOnlyParents", "skills", "sha256", "hookParents"].includes(key)) && witness.schema === "hasna.codex-semantic-cache-witness.v1" && typeof witness.sha256 === "string" && /^[a-f0-9]{64}$/.test(witness.sha256));
|
|
2735
3139
|
text(proof.configPath);
|
|
2736
|
-
requireBound(isAbsolute5(proof.configPath) &&
|
|
3140
|
+
requireBound(isAbsolute5(proof.configPath) && resolve7(proof.configPath) === proof.configPath && basename4(proof.configPath) === "config.toml");
|
|
2737
3141
|
text(witness.cacheRoot);
|
|
2738
|
-
requireBound(isAbsolute5(witness.cacheRoot) &&
|
|
3142
|
+
requireBound(isAbsolute5(witness.cacheRoot) && resolve7(witness.cacheRoot) === witness.cacheRoot);
|
|
2739
3143
|
if (proof.hookRootAlias !== null) {
|
|
2740
3144
|
requireBound(object(proof.hookRootAlias) && Object.keys(proof.hookRootAlias).length === 2);
|
|
2741
3145
|
for (const key of ["alias", "target"]) {
|
|
2742
3146
|
text(proof.hookRootAlias[key]);
|
|
2743
|
-
requireBound(isAbsolute5(proof.hookRootAlias[key]) &&
|
|
3147
|
+
requireBound(isAbsolute5(proof.hookRootAlias[key]) && resolve7(proof.hookRootAlias[key]) === proof.hookRootAlias[key]);
|
|
2744
3148
|
}
|
|
2745
3149
|
}
|
|
2746
3150
|
const parents2 = array(proof.appOnlyParents, 4096);
|
|
@@ -2748,7 +3152,7 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2748
3152
|
requireBound(object(parent) && Object.keys(parent).length === 5 && parent.role === "app-only");
|
|
2749
3153
|
for (const key of ["pluginId", "namespace", "pluginParent", "remotePluginId"])
|
|
2750
3154
|
text(parent[key]);
|
|
2751
|
-
requireBound(isAbsolute5(parent.pluginParent) &&
|
|
3155
|
+
requireBound(isAbsolute5(parent.pluginParent) && resolve7(parent.pluginParent) === parent.pluginParent);
|
|
2752
3156
|
}
|
|
2753
3157
|
if (proof.reviewedHookParents !== undefined)
|
|
2754
3158
|
for (const parent of array(proof.reviewedHookParents, 4096)) {
|
|
@@ -2760,9 +3164,9 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2760
3164
|
text(parent.remotePluginId);
|
|
2761
3165
|
else {
|
|
2762
3166
|
text(parent.marketplaceSourcePath);
|
|
2763
|
-
requireBound(typeof parent.marketplaceSha256 === "string" && /^[a-f0-9]{64}$/.test(parent.marketplaceSha256) && isAbsolute5(parent.marketplaceSourcePath) &&
|
|
3167
|
+
requireBound(typeof parent.marketplaceSha256 === "string" && /^[a-f0-9]{64}$/.test(parent.marketplaceSha256) && isAbsolute5(parent.marketplaceSourcePath) && resolve7(parent.marketplaceSourcePath) === parent.marketplaceSourcePath && basename4(parent.marketplaceSourcePath) === "marketplace.json");
|
|
2764
3168
|
}
|
|
2765
|
-
requireBound(isAbsolute5(parent.pluginParent) &&
|
|
3169
|
+
requireBound(isAbsolute5(parent.pluginParent) && resolve7(parent.pluginParent) === parent.pluginParent);
|
|
2766
3170
|
}
|
|
2767
3171
|
for (const row of [...array(witness.controls, 4096), ...array(witness.appOnlyParents, 4096)]) {
|
|
2768
3172
|
requireBound(object(row));
|
|
@@ -2782,9 +3186,9 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2782
3186
|
text(row.remotePluginId);
|
|
2783
3187
|
else {
|
|
2784
3188
|
text(row.marketplaceSourcePath);
|
|
2785
|
-
requireBound(typeof row.marketplaceSha256 === "string" && /^[a-f0-9]{64}$/.test(row.marketplaceSha256) && isAbsolute5(row.marketplaceSourcePath) &&
|
|
3189
|
+
requireBound(typeof row.marketplaceSha256 === "string" && /^[a-f0-9]{64}$/.test(row.marketplaceSha256) && isAbsolute5(row.marketplaceSourcePath) && resolve7(row.marketplaceSourcePath) === row.marketplaceSourcePath && basename4(row.marketplaceSourcePath) === "marketplace.json");
|
|
2786
3190
|
}
|
|
2787
|
-
requireBound(isAbsolute5(row.pluginParent) &&
|
|
3191
|
+
requireBound(isAbsolute5(row.pluginParent) && resolve7(row.pluginParent) === row.pluginParent);
|
|
2788
3192
|
for (const key of ["manifestSha256", "hooksSha256"])
|
|
2789
3193
|
requireBound(typeof row[key] === "string" && /^[a-f0-9]{64}$/.test(row[key]));
|
|
2790
3194
|
if (row.installation === "remote-receipt")
|
|
@@ -2817,17 +3221,17 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2817
3221
|
text(source.path);
|
|
2818
3222
|
requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
|
|
2819
3223
|
if (source.hashMode !== undefined)
|
|
2820
|
-
requireBound(["bytes", "path-bytes", CLAUDE_ORPHAN_MARKER_MODE, "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "codex-settings-v5", "codex-settings-v6", "executable-v1", "sumi-settings-v1", "claude-marketplace-entry-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (["bytes", CLAUDE_ORPHAN_MARKER_MODE].includes(source.hashMode) || source.sha256 !== null));
|
|
3224
|
+
requireBound(["bytes", "path-bytes", CLAUDE_ORPHAN_MARKER_MODE, "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4", "claude-settings-v5", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "codex-settings-v5", "codex-settings-v6", "codex-settings-v7", "executable-v1", "sumi-settings-v1", "claude-marketplace-entry-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (["bytes", CLAUDE_ORPHAN_MARKER_MODE].includes(source.hashMode) || source.sha256 !== null));
|
|
2821
3225
|
if (source.hashMode === CLAUDE_ORPHAN_MARKER_MODE)
|
|
2822
3226
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && isClaudePluginCacheOrphanMarker(source.path) && source.managedPlugins === undefined && source.reviewArtifact === undefined);
|
|
2823
3227
|
if (source.hashMode === "executable-v1")
|
|
2824
|
-
requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && value.sources.some((row) => row.hashMode === "codex-settings-v6") && isAbsolute5(source.path) &&
|
|
3228
|
+
requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && value.sources.some((row) => row.hashMode === "codex-settings-v6") && isAbsolute5(source.path) && resolve7(source.path) === source.path);
|
|
2825
3229
|
if (source.hashMode === "codex-settings-v6") {
|
|
2826
3230
|
const seen = new Set;
|
|
2827
3231
|
for (const hook of array(source.hookSources, AGENT_POLICY_LIMITS.discoverySources)) {
|
|
2828
3232
|
requireBound(object(hook) && Object.keys(hook).length === 2 && Object.keys(hook).every((key) => ["path", "sha256"].includes(key)));
|
|
2829
3233
|
text(hook.path);
|
|
2830
|
-
requireBound(isAbsolute5(hook.path) &&
|
|
3234
|
+
requireBound(isAbsolute5(hook.path) && resolve7(hook.path) === hook.path && !seen.has(hook.path));
|
|
2831
3235
|
seen.add(hook.path);
|
|
2832
3236
|
requireBound(hook.sha256 === null || typeof hook.sha256 === "string" && /^[a-f0-9]{64}$/.test(hook.sha256));
|
|
2833
3237
|
requireBound(value.sources.some((row) => row.path === hook.path && row.sha256 === hook.sha256 && [undefined, "bytes"].includes(row.hashMode) && row.format === undefined && row.fields === undefined));
|
|
@@ -2835,15 +3239,15 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2835
3239
|
} else
|
|
2836
3240
|
requireBound(source.hookSources === undefined);
|
|
2837
3241
|
if (source.hashMode === "claude-plugin-manifest-v1")
|
|
2838
|
-
requireBound(agent === "claude" && value.agent === "claude" && ["automatic", "reviewed"].includes(value.method) && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) &&
|
|
3242
|
+
requireBound(agent === "claude" && value.agent === "claude" && ["automatic", "reviewed"].includes(value.method) && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve7(source.path) === source.path && source.path.endsWith(join7(sep2, ".claude-plugin", "plugin.json")) && source.managedPlugins === undefined);
|
|
2839
3243
|
if (source.hashMode === "sumi-settings-v1")
|
|
2840
|
-
requireBound(agent === "sumi" && value.agent === "sumi" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) &&
|
|
2841
|
-
if (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4" || source.hashMode === "codex-settings-v5" || source.hashMode === "codex-settings-v6"))
|
|
2842
|
-
requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) &&
|
|
2843
|
-
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4"))
|
|
2844
|
-
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) &&
|
|
3244
|
+
requireBound(agent === "sumi" && value.agent === "sumi" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve7(source.path) === source.path && basename4(source.path) === "sumi.json");
|
|
3245
|
+
if (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4" || source.hashMode === "codex-settings-v5" || source.hashMode === "codex-settings-v6" || source.hashMode === "codex-settings-v7"))
|
|
3246
|
+
requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve7(source.path) === source.path && basename4(source.path) === "config.toml");
|
|
3247
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4" || source.hashMode === "claude-settings-v5"))
|
|
3248
|
+
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve7(source.path) === source.path && basename4(source.path) === "settings.json");
|
|
2845
3249
|
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
|
|
2846
|
-
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) &&
|
|
3250
|
+
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve7(source.path) === source.path && basename4(source.path) === "known_marketplaces.json");
|
|
2847
3251
|
if (source.hashMode === "claude-marketplace-entry-v1")
|
|
2848
3252
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && claudeMarketplaceEntrySourceValid(source));
|
|
2849
3253
|
if (source.hashMode === "claude-plugin-registry") {
|
|
@@ -2862,9 +3266,9 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2862
3266
|
requireBound(agent === "codex" && value.method === "reviewed" && source.hashMode === "bytes" && object(artifact) && Object.keys(artifact).length === 6 && Object.keys(artifact).every((key) => ["version", "kind", "storeRoot", "originalPath", "sha256", "catalogSha256"].includes(key)) && artifact.version === 1 && artifact.kind === "codex-native-catalog" && artifact.sha256 === source.sha256 && /^[a-f0-9]{64}$/.test(artifact.catalogSha256));
|
|
2863
3267
|
for (const path of [artifact.storeRoot, artifact.originalPath]) {
|
|
2864
3268
|
text(path);
|
|
2865
|
-
requireBound(isAbsolute5(path) &&
|
|
3269
|
+
requireBound(isAbsolute5(path) && resolve7(path) === path);
|
|
2866
3270
|
}
|
|
2867
|
-
requireBound(source.path ===
|
|
3271
|
+
requireBound(source.path === join7(artifact.storeRoot, "agent-discovery", "artifacts", artifact.sha256, "catalog.json"));
|
|
2868
3272
|
}
|
|
2869
3273
|
if (source.format !== undefined)
|
|
2870
3274
|
requireBound(["json", "toml", "yaml"].includes(source.format));
|
|
@@ -2915,10 +3319,10 @@ function serializeManagedSkillPolicy(value) {
|
|
|
2915
3319
|
return text2;
|
|
2916
3320
|
}
|
|
2917
3321
|
function readManagedSkillPolicySnapshot(dataDir2 = getDataDirReadOnly()) {
|
|
2918
|
-
const path =
|
|
3322
|
+
const path = join8(dataDir2, "agent-policy.json");
|
|
2919
3323
|
let descriptor, observed = false;
|
|
2920
3324
|
try {
|
|
2921
|
-
const stat =
|
|
3325
|
+
const stat = lstatSync6(path);
|
|
2922
3326
|
observed = true;
|
|
2923
3327
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
2924
3328
|
throw refusal();
|
|
@@ -2944,7 +3348,7 @@ function readManagedSkillPolicySnapshot(dataDir2 = getDataDirReadOnly()) {
|
|
|
2944
3348
|
return { text: text2, value: parseManagedSkillPolicy(text2) };
|
|
2945
3349
|
} catch (error) {
|
|
2946
3350
|
if (error.code === "ENOENT" && !observed) {
|
|
2947
|
-
if (
|
|
3351
|
+
if (lstatSync6(path, { throwIfNoEntry: false }) === undefined)
|
|
2948
3352
|
return null;
|
|
2949
3353
|
}
|
|
2950
3354
|
if (error instanceof SkillSelectionError)
|
|
@@ -2974,7 +3378,7 @@ function detectedIntegrationAgents(options = {}) {
|
|
|
2974
3378
|
throw new Error("Invalid existing bridge agent inventory");
|
|
2975
3379
|
const present = (path) => {
|
|
2976
3380
|
try {
|
|
2977
|
-
return
|
|
3381
|
+
return lstatSync7(path, { throwIfNoEntry: false }) !== undefined;
|
|
2978
3382
|
} catch {
|
|
2979
3383
|
throw new Error("Agent presence cannot be inspected; review native configuration before hook installation");
|
|
2980
3384
|
}
|
|
@@ -2984,13 +3388,13 @@ function detectedIntegrationAgents(options = {}) {
|
|
|
2984
3388
|
return true;
|
|
2985
3389
|
if (agent === "sumi") {
|
|
2986
3390
|
const xdg = env.XDG_CONFIG_HOME;
|
|
2987
|
-
const xdgSumi = xdg?.trim() ?
|
|
2988
|
-
return ["SUMI_HOME", "SUMI_CONFIG_DIR", "SUMI_CONFIG", "SUMI_CONFIG_CONTENT"].some((key) => env[key] !== undefined) || Bun.which("sumi-paths", { PATH: env.PATH ?? "" }) !== null || NATIVE_SKILL_ROOTS.some(([provider, root]) => provider === "sumi" && present(
|
|
3391
|
+
const xdgSumi = xdg?.trim() ? resolve8(options.cwd ?? process.cwd(), xdg === "~" ? home : xdg.startsWith("~/") || xdg.startsWith("~\\") ? resolve8(home, xdg.slice(2)) : xdg, "sumi") : null;
|
|
3392
|
+
return ["SUMI_HOME", "SUMI_CONFIG_DIR", "SUMI_CONFIG", "SUMI_CONFIG_CONTENT"].some((key) => env[key] !== undefined) || Bun.which("sumi-paths", { PATH: env.PATH ?? "" }) !== null || NATIVE_SKILL_ROOTS.some(([provider, root]) => provider === "sumi" && present(join9(home, dirname7(root)))) || present(join9(home, ".config", "sumi")) || xdgSumi !== null && present(xdgSumi);
|
|
2989
3393
|
}
|
|
2990
3394
|
if (agent === "hermes" && env.HERMES_HOME !== undefined)
|
|
2991
3395
|
return true;
|
|
2992
3396
|
const adapter = AGENT_ADAPTERS[agent];
|
|
2993
|
-
return present(
|
|
3397
|
+
return present(join9(home, dirname7(adapter.config))) || NATIVE_SKILL_ROOTS.some(([provider, root]) => provider === agent && dirname7(root) === `.${agent}` && present(join9(home, dirname7(root))));
|
|
2994
3398
|
});
|
|
2995
3399
|
if (!selected.length && !options.allowEmpty)
|
|
2996
3400
|
throw new Error(`No configured or detected agents; select --agent <agent> explicitly (${INTEGRATION_AGENTS.join(", ")}).`);
|
|
@@ -2999,8 +3403,8 @@ function detectedIntegrationAgents(options = {}) {
|
|
|
2999
3403
|
|
|
3000
3404
|
// src/lib/agent-sumi.ts
|
|
3001
3405
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3002
|
-
import { accessSync, constants as constants6, existsSync as existsSync4, lstatSync as
|
|
3003
|
-
import { delimiter, isAbsolute as isAbsolute6, join as
|
|
3406
|
+
import { accessSync, constants as constants6, existsSync as existsSync4, lstatSync as lstatSync8, realpathSync, statSync as statSync3 } from "fs";
|
|
3407
|
+
import { delimiter, isAbsolute as isAbsolute6, join as join10, resolve as resolve9 } from "path";
|
|
3004
3408
|
|
|
3005
3409
|
// src/lib/hook-diagnostics.ts
|
|
3006
3410
|
var CONTEXT_ERROR_CODES = new Set([
|
|
@@ -3068,6 +3472,7 @@ var OPTIONAL_CONTEXT_FAILURES = new Set([
|
|
|
3068
3472
|
"SESSION_RECONCILIATION_REQUIRED",
|
|
3069
3473
|
"SESSION_RENEWAL_TIMEOUT",
|
|
3070
3474
|
"SESSION_WRITE_BUSY",
|
|
3475
|
+
"SESSION_WRITE_LOCKED",
|
|
3071
3476
|
"SESSION_PARENT_CHANGED",
|
|
3072
3477
|
"SESSION_CONTEXT_CONFLICT",
|
|
3073
3478
|
"SKILLS_HOOK_TIMEOUT"
|
|
@@ -3090,7 +3495,7 @@ function recordWithKeys(value, keys) {
|
|
|
3090
3495
|
return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
3091
3496
|
}
|
|
3092
3497
|
function absolutePath2(value) {
|
|
3093
|
-
return typeof value === "string" && value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value) &&
|
|
3498
|
+
return typeof value === "string" && value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value) && resolve9(value) === value;
|
|
3094
3499
|
}
|
|
3095
3500
|
function absoluteInput(value) {
|
|
3096
3501
|
return value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value);
|
|
@@ -3101,7 +3506,7 @@ function resolveSumiPathsCommand(pathValue) {
|
|
|
3101
3506
|
for (const directory of pathValue.split(delimiter)) {
|
|
3102
3507
|
if (!directory || /[\x00-\x1f\x7f]/.test(directory))
|
|
3103
3508
|
continue;
|
|
3104
|
-
const candidate =
|
|
3509
|
+
const candidate = resolve9(directory, "sumi-paths");
|
|
3105
3510
|
try {
|
|
3106
3511
|
accessSync(candidate, constants6.X_OK);
|
|
3107
3512
|
if (statSync3(candidate).isFile())
|
|
@@ -3113,7 +3518,7 @@ function resolveSumiPathsCommand(pathValue) {
|
|
|
3113
3518
|
function validPathPlan(value, home, cwd) {
|
|
3114
3519
|
if (!recordWithKeys(value, ["schemaVersion", "kind", "home", "cwd", "roots", "legacyRoots", "configFiles", "skillRoots"]))
|
|
3115
3520
|
return false;
|
|
3116
|
-
if (value.schemaVersion !== 1 || value.kind !== "sumi-paths" || value.home !==
|
|
3521
|
+
if (value.schemaVersion !== 1 || value.kind !== "sumi-paths" || value.home !== resolve9(home) || value.cwd !== resolve9(cwd))
|
|
3117
3522
|
return false;
|
|
3118
3523
|
if (!recordWithKeys(value.roots, SUMI_PATH_KEYS) || !recordWithKeys(value.legacyRoots, SUMI_PATH_KEYS))
|
|
3119
3524
|
return false;
|
|
@@ -3135,11 +3540,11 @@ function validPathPlan(value, home, cwd) {
|
|
|
3135
3540
|
const legacyConfigRoot = value.legacyRoots.config;
|
|
3136
3541
|
if (typeof configRoot !== "string" || legacyConfigRoot !== null && typeof legacyConfigRoot !== "string")
|
|
3137
3542
|
return false;
|
|
3138
|
-
if (value.configFiles.canonical !==
|
|
3543
|
+
if (value.configFiles.canonical !== join10(configRoot, "sumi.json") || value.skillRoots.canonical !== join10(configRoot, "skills"))
|
|
3139
3544
|
return false;
|
|
3140
3545
|
if (legacyConfigRoot === null)
|
|
3141
3546
|
return value.configFiles.legacy === null && value.skillRoots.legacy === null;
|
|
3142
|
-
return value.configFiles.legacy ===
|
|
3547
|
+
return value.configFiles.legacy === join10(legacyConfigRoot, "sumi.json") && value.skillRoots.legacy === join10(legacyConfigRoot, "skills");
|
|
3143
3548
|
}
|
|
3144
3549
|
function sumiPathPlan(home, cwd = process.cwd(), env = process.env) {
|
|
3145
3550
|
for (const key of ["SUMI_CONFIG", "SUMI_CONFIG_CONTENT"])
|
|
@@ -3157,8 +3562,8 @@ function sumiPathPlan(home, cwd = process.cwd(), env = process.env) {
|
|
|
3157
3562
|
childEnv[key] = env[key];
|
|
3158
3563
|
let child2;
|
|
3159
3564
|
try {
|
|
3160
|
-
child2 = spawnSync2(executable, ["--json", "--home",
|
|
3161
|
-
cwd:
|
|
3565
|
+
child2 = spawnSync2(executable, ["--json", "--home", resolve9(home), "--cwd", resolve9(cwd)], {
|
|
3566
|
+
cwd: resolve9(cwd),
|
|
3162
3567
|
env: childEnv,
|
|
3163
3568
|
encoding: "buffer",
|
|
3164
3569
|
timeout: 3000,
|
|
@@ -3190,7 +3595,7 @@ function selectedConfigDirectory(plan) {
|
|
|
3190
3595
|
const canonical3 = plan.roots.config, legacy = plan.legacyRoots.config;
|
|
3191
3596
|
if (legacy === null)
|
|
3192
3597
|
return existsSync4(canonical3) ? realpathSync(canonical3) : canonical3;
|
|
3193
|
-
const canonicalStat =
|
|
3598
|
+
const canonicalStat = lstatSync8(canonical3, { throwIfNoEntry: false }), legacyStat = lstatSync8(legacy, { throwIfNoEntry: false });
|
|
3194
3599
|
if (canonicalStat && legacyStat && realpathSync(canonical3) !== realpathSync(legacy))
|
|
3195
3600
|
throw new Error("Sumi config roots conflict; complete native home adoption before Skills integration");
|
|
3196
3601
|
const selected = canonicalStat ? canonical3 : legacyStat ? legacy : canonical3;
|
|
@@ -3206,13 +3611,13 @@ function sumiConfigDirectory(home, env = process.env, cwd = process.cwd()) {
|
|
|
3206
3611
|
}
|
|
3207
3612
|
function sumiSkillRoots(home, cwd = process.cwd(), env = process.env) {
|
|
3208
3613
|
const plan = sumiPathPlan(home, cwd, env);
|
|
3209
|
-
return [
|
|
3614
|
+
return [join10(selectedConfigDirectory(plan), "skills")];
|
|
3210
3615
|
}
|
|
3211
3616
|
function sumiBridgeRoot(home, cwd = process.cwd(), env = process.env) {
|
|
3212
3617
|
return sumiSkillRoots(home, cwd, env)[0];
|
|
3213
3618
|
}
|
|
3214
3619
|
function sumiConfigPath(home, cwd = process.cwd(), env = process.env) {
|
|
3215
|
-
return
|
|
3620
|
+
return join10(sumiConfigDirectory(home, env, cwd), "sumi.json");
|
|
3216
3621
|
}
|
|
3217
3622
|
function renderSumiPlugin(command, profile) {
|
|
3218
3623
|
return renderSumiPluginVersion(command, profile, 3);
|