@hasna/skills 0.10.82 → 0.10.88
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 +133 -15
- package/bin/index.js +5691 -2868
- package/bin/mcp.js +1689 -810
- 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 +35 -0
- package/dist/cli/commands/context.d.ts +4 -0
- package/dist/cli/commands/runtime-launcher.d.ts +4 -9
- package/dist/index.js +3022 -1530
- 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/session-background-renewal.d.ts +118 -0
- package/dist/lib/session-reconciliation.d.ts +82 -2
- package/dist/lib/skill-context.d.ts +39 -1
- package/dist/lib/skill-relevance.d.ts +95 -0
- package/dist/runtime-prerequisites.js +570 -165
- package/dist/sdk/index.js +2627 -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.88";
|
|
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,9 @@ 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"]
|
|
1960
2280
|
};
|
|
1961
2281
|
function supportsCodexNativeCapability(version2, capability) {
|
|
1962
2282
|
return typeof version2 === "string" && Object.hasOwn(MEASURED_CODEX_CAPABILITIES, version2) && MEASURED_CODEX_CAPABILITIES[version2].includes(capability);
|
|
@@ -1964,37 +2284,41 @@ function supportsCodexNativeCapability(version2, capability) {
|
|
|
1964
2284
|
var SUPPORTED_CODEX_HOOK_VERSIONS = Object.freeze(Object.keys(MEASURED_CODEX_CAPABILITIES).filter((version2) => supportsCodexNativeCapability(version2, "hooks")));
|
|
1965
2285
|
|
|
1966
2286
|
// src/lib/claude-marketplace-entry-witness.ts
|
|
1967
|
-
import { createHash as
|
|
1968
|
-
import { basename as basename2, dirname as
|
|
2287
|
+
import { createHash as createHash6 } from "crypto";
|
|
2288
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, resolve as resolve5 } from "path";
|
|
1969
2289
|
|
|
1970
2290
|
// 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
|
|
2291
|
+
import { createHash as createHash5 } from "crypto";
|
|
2292
|
+
import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync3, lstatSync as lstatSync4, openSync as openSync3, readSync as readSync2 } from "fs";
|
|
2293
|
+
import { basename, dirname as dirname4, isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
|
|
1974
2294
|
var CLAUDE_SETTINGS_WITNESS_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
|
|
1975
2295
|
function need(value, reason) {
|
|
1976
2296
|
if (!value)
|
|
1977
2297
|
throw new Error(`Claude settings witness ${reason}`);
|
|
1978
2298
|
}
|
|
1979
2299
|
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) &&
|
|
2300
|
+
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
2301
|
}
|
|
1982
2302
|
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
2303
|
function parents(path) {
|
|
1984
2304
|
const result = [];
|
|
1985
|
-
for (let at =
|
|
1986
|
-
const stat =
|
|
2305
|
+
for (let at = dirname4(path);; at = dirname4(at)) {
|
|
2306
|
+
const stat = lstatSync4(at, { bigint: true });
|
|
1987
2307
|
need(stat.isDirectory() && !stat.isSymbolicLink(), "path traverses a symlink or non-directory");
|
|
1988
2308
|
result.push([at, stat]);
|
|
1989
|
-
if (at ===
|
|
2309
|
+
if (at === dirname4(at))
|
|
1990
2310
|
return result;
|
|
1991
2311
|
}
|
|
1992
2312
|
}
|
|
1993
2313
|
function readNativeSettingsWitnessFile(path, budget, filename = "settings.json") {
|
|
1994
2314
|
absolutePath(path);
|
|
1995
2315
|
need(basename(path) === filename, `requires ${filename}`);
|
|
2316
|
+
return readNativeDiscoveryWitnessFile(path, budget);
|
|
2317
|
+
}
|
|
2318
|
+
function readNativeDiscoveryWitnessFile(path, budget) {
|
|
2319
|
+
absolutePath(path);
|
|
1996
2320
|
need(Number.isSafeInteger(budget.remaining) && budget.remaining >= 0, "has an invalid byte budget");
|
|
1997
|
-
const ancestors = parents(path), initial =
|
|
2321
|
+
const ancestors = parents(path), initial = lstatSync4(path, { bigint: true });
|
|
1998
2322
|
need(initial.isFile() && !initial.isSymbolicLink() && initial.size <= BigInt(CLAUDE_SETTINGS_WITNESS_LIMITS.bytes), "is not a bounded regular file");
|
|
1999
2323
|
need(initial.size <= BigInt(budget.remaining), "exceeds its aggregate byte limit");
|
|
2000
2324
|
const fd = openSync3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
|
|
@@ -2009,10 +2333,10 @@ function readNativeSettingsWitnessFile(path, budget, filename = "settings.json")
|
|
|
2009
2333
|
break;
|
|
2010
2334
|
length += count;
|
|
2011
2335
|
}
|
|
2012
|
-
const after = fstatSync3(fd, { bigint: true }), current =
|
|
2336
|
+
const after = fstatSync3(fd, { bigint: true }), current = lstatSync4(path, { bigint: true });
|
|
2013
2337
|
need(current.isFile() && sameFile(opened, after) && sameFile(after, current) && BigInt(length) === opened.size, "changed during read");
|
|
2014
2338
|
for (const [ancestor, before] of ancestors) {
|
|
2015
|
-
const now =
|
|
2339
|
+
const now = lstatSync4(ancestor, { bigint: true });
|
|
2016
2340
|
need(now.isDirectory() && before.dev === now.dev && before.ino === now.ino && before.mode === now.mode, "parent changed during read");
|
|
2017
2341
|
}
|
|
2018
2342
|
budget.remaining -= length;
|
|
@@ -2232,7 +2556,7 @@ function settingsDigest(text, version2 = 1) {
|
|
|
2232
2556
|
}
|
|
2233
2557
|
return true;
|
|
2234
2558
|
});
|
|
2235
|
-
return
|
|
2559
|
+
return createHash5("sha256").update(`hasna.skills.claude-settings.v${version2}\x00`).update(JSON.stringify(canonical(value))).digest("hex");
|
|
2236
2560
|
}
|
|
2237
2561
|
function hashClaudeSettingsReplacement(text, budget) {
|
|
2238
2562
|
need(typeof text === "string", "requires settings text");
|
|
@@ -2268,7 +2592,7 @@ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
|
|
|
2268
2592
|
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
2593
|
const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
|
|
2270
2594
|
const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
|
|
2271
|
-
const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ?
|
|
2595
|
+
const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash5("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : 2);
|
|
2272
2596
|
need(beforeDigest === previous.sha256, "reviewed preimage does not match the legacy witness");
|
|
2273
2597
|
const sha256 = settingsDigest(current, 3);
|
|
2274
2598
|
need(settingsDigest(before, 3) === sha256, "non-preference settings changed; explicit discovery review required");
|
|
@@ -2288,12 +2612,91 @@ function upgradeClaudeSettingsWitnessV4(previous, reviewedSettingsPath) {
|
|
|
2288
2612
|
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
2613
|
const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
|
|
2290
2614
|
const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
|
|
2291
|
-
const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ?
|
|
2615
|
+
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
2616
|
need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
|
|
2293
2617
|
const sha256 = settingsDigest(current, 4);
|
|
2294
2618
|
need(settingsDigest(before, 4) === sha256, "non-preference settings changed; explicit discovery review required");
|
|
2295
2619
|
return { path: previous.path, hashMode: "claude-settings-v4", sha256 };
|
|
2296
2620
|
}
|
|
2621
|
+
var V5_DISCOVERY_KEY = /skill|plugin|hook|marketplace/i;
|
|
2622
|
+
var V5_BOUND_KEYS = new Set(["apiKeyHelper", "awsAuthRefresh", "awsCredentialExport", "otelHeadersHelper", "fileSuggestion", "outputStyle", "agent"]);
|
|
2623
|
+
var V5_DISCOVERY_ENV = new Set(["CLAUDE_CONFIG_DIR"]);
|
|
2624
|
+
var V5_SKILL_RULE = /^Skill(?:\(|$)/;
|
|
2625
|
+
var V5_PERMISSION_LISTS = new Set(["allow", "deny", "ask"]);
|
|
2626
|
+
function narrowedClaudeSettings(value) {
|
|
2627
|
+
const entries = [];
|
|
2628
|
+
for (const [key, child] of value.entries) {
|
|
2629
|
+
if (V5_DISCOVERY_KEY.test(key) || V5_BOUND_KEYS.has(key)) {
|
|
2630
|
+
entries.push([key, child]);
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
if (key === "permissions") {
|
|
2634
|
+
if (child.kind !== "object") {
|
|
2635
|
+
entries.push([key, child]);
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
const kept = [];
|
|
2639
|
+
for (const [name, rules] of child.entries) {
|
|
2640
|
+
if (name === "additionalDirectories") {
|
|
2641
|
+
kept.push([name, rules]);
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
if (!V5_PERMISSION_LISTS.has(name))
|
|
2645
|
+
continue;
|
|
2646
|
+
if (rules.kind !== "array") {
|
|
2647
|
+
kept.push([name, rules]);
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
const skillRules = rules.items.filter((rule) => rule.kind !== "string" || V5_SKILL_RULE.test(rule.value));
|
|
2651
|
+
if (skillRules.length)
|
|
2652
|
+
kept.push([name, { kind: "array", items: skillRules }]);
|
|
2653
|
+
}
|
|
2654
|
+
if (kept.length)
|
|
2655
|
+
entries.push([key, { kind: "object", entries: kept }]);
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
if (key === "env") {
|
|
2659
|
+
if (child.kind !== "object") {
|
|
2660
|
+
entries.push([key, child]);
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
const kept = child.entries.filter(([name]) => V5_DISCOVERY_ENV.has(name) || V5_DISCOVERY_KEY.test(name));
|
|
2664
|
+
if (kept.length)
|
|
2665
|
+
entries.push([key, { kind: "object", entries: kept }]);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
return { kind: "object", entries };
|
|
2669
|
+
}
|
|
2670
|
+
function claudeDiscoveryControlKeys(text) {
|
|
2671
|
+
return narrowedClaudeSettings(parseNativeJsonObject(text)).entries.map(([key]) => key);
|
|
2672
|
+
}
|
|
2673
|
+
function claudeDiscoveryEnvironmentKeys(text) {
|
|
2674
|
+
const environment = narrowedClaudeSettings(parseNativeJsonObject(text)).entries.find(([key]) => key === "env")?.[1];
|
|
2675
|
+
return environment?.kind === "object" ? environment.entries.map(([key]) => key) : [];
|
|
2676
|
+
}
|
|
2677
|
+
function settingsDigestV5(text) {
|
|
2678
|
+
return createHash5("sha256").update("hasna.skills.claude-settings.v5\x00").update(JSON.stringify(canonical(narrowedClaudeSettings(parse2(text))))).digest("hex");
|
|
2679
|
+
}
|
|
2680
|
+
function captureClaudeSettingsV5(path, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
2681
|
+
return { path, hashMode: "claude-settings-v5", sha256: settingsDigestV5(readNativeSettingsWitnessFile(path, budget)) };
|
|
2682
|
+
}
|
|
2683
|
+
function hashClaudeSettingsReplacementV5(text, budget) {
|
|
2684
|
+
need(typeof text === "string", "requires settings text");
|
|
2685
|
+
const bytes = Buffer.byteLength(text);
|
|
2686
|
+
need(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
|
|
2687
|
+
budget.remaining -= bytes;
|
|
2688
|
+
return settingsDigestV5(text);
|
|
2689
|
+
}
|
|
2690
|
+
function upgradeClaudeSettingsWitnessV5(previous, reviewedSettingsPath) {
|
|
2691
|
+
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");
|
|
2692
|
+
const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
|
|
2693
|
+
const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
|
|
2694
|
+
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);
|
|
2695
|
+
need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
|
|
2696
|
+
const sha256 = settingsDigestV5(current);
|
|
2697
|
+
need(settingsDigestV5(before) === sha256, "discovery settings changed; explicit discovery review required");
|
|
2698
|
+
return { path: previous.path, hashMode: "claude-settings-v5", sha256 };
|
|
2699
|
+
}
|
|
2297
2700
|
function hashNativeJsonControls(text, excludedMetadata) {
|
|
2298
2701
|
need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
|
|
2299
2702
|
const value = parse2(text);
|
|
@@ -2303,7 +2706,7 @@ function hashNativeJsonControls(text, excludedMetadata) {
|
|
|
2303
2706
|
need(child.kind === "string" && child.value.length <= 128, "has invalid version metadata");
|
|
2304
2707
|
return false;
|
|
2305
2708
|
});
|
|
2306
|
-
return
|
|
2709
|
+
return createHash5("sha256").update("hasna.skills.native-json-controls.v1\x00").update(JSON.stringify(canonical(value))).digest("hex");
|
|
2307
2710
|
}
|
|
2308
2711
|
function hashSumiNativeJsonControls(text) {
|
|
2309
2712
|
need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
|
|
@@ -2324,7 +2727,7 @@ function hashSumiNativeJsonControls(text) {
|
|
|
2324
2727
|
});
|
|
2325
2728
|
return child.entries.length > 0;
|
|
2326
2729
|
});
|
|
2327
|
-
return
|
|
2730
|
+
return createHash5("sha256").update("hasna.skills.sumi-settings.v1\x00").update(JSON.stringify(value)).digest("hex");
|
|
2328
2731
|
}
|
|
2329
2732
|
|
|
2330
2733
|
// src/lib/claude-marketplace-entry-witness.ts
|
|
@@ -2392,7 +2795,7 @@ var stringArray = (value) => value?.kind === "array" && value.items.every(isStri
|
|
|
2392
2795
|
var stringObject = (value) => value?.kind === "object" && value.entries.every(([, item]) => isString(item));
|
|
2393
2796
|
function claudeMarketplaceEntrySourceValid(source) {
|
|
2394
2797
|
const path = source?.path;
|
|
2395
|
-
return typeof path === "string" && path.length > 0 && path.length <= PATH_CHARACTERS && !/[\x00-\x1f\x7f]/.test(path) && isAbsolute3(path) &&
|
|
2798
|
+
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
2799
|
}
|
|
2397
2800
|
function canonical2(value) {
|
|
2398
2801
|
if (value.kind === "string")
|
|
@@ -2505,7 +2908,7 @@ function hashClaudeMarketplaceEntry(text, selector) {
|
|
|
2505
2908
|
["allowCrossMarketplaceDependenciesOn", field(root, "allowCrossMarketplaceDependenciesOn") ?? nullValue]
|
|
2506
2909
|
] };
|
|
2507
2910
|
const projection = { kind: "object", entries: [["marketplace", marketplace], ["plugin", entry]] };
|
|
2508
|
-
return
|
|
2911
|
+
return createHash6("sha256").update(DOMAIN).update(canonical2(projection)).digest("hex");
|
|
2509
2912
|
}
|
|
2510
2913
|
function captureClaudeMarketplaceEntry(path, marketplace, plugin, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
2511
2914
|
need2(claudeMarketplaceEntrySourceValid({ path, marketplace, plugin }), "requires a normalized absolute .claude-plugin/marketplace.json path and exact names");
|
|
@@ -2519,13 +2922,13 @@ function captureClaudeMarketplaceEntry(path, marketplace, plugin, budget = { rem
|
|
|
2519
2922
|
}
|
|
2520
2923
|
|
|
2521
2924
|
// 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
|
|
2925
|
+
import { createHash as createHash7 } from "crypto";
|
|
2926
|
+
import { closeSync as closeSync4, constants as constants4, fstatSync as fstatSync4, lstatSync as lstatSync5, openSync as openSync4, readSync as readSync3 } from "fs";
|
|
2927
|
+
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute4, resolve as resolve6, sep } from "path";
|
|
2525
2928
|
var CLAUDE_ORPHAN_MARKER_MODE = "claude-plugin-orphaned-at-v1";
|
|
2526
2929
|
var CACHE = `${sep}.claude${sep}plugins${sep}cache${sep}`;
|
|
2527
2930
|
function isClaudePluginCacheOrphanMarker(path) {
|
|
2528
|
-
if (typeof path !== "string" || basename3(path) !== ".orphaned_at" || !isAbsolute4(path) ||
|
|
2931
|
+
if (typeof path !== "string" || basename3(path) !== ".orphaned_at" || !isAbsolute4(path) || resolve6(path) !== path || path.includes("\x00"))
|
|
2529
2932
|
return false;
|
|
2530
2933
|
const at = path.indexOf(CACHE);
|
|
2531
2934
|
if (at < 0 || path.indexOf(CACHE, at + 1) >= 0)
|
|
@@ -2537,10 +2940,10 @@ function same(left, right) {
|
|
|
2537
2940
|
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
2941
|
}
|
|
2539
2942
|
function safe(path) {
|
|
2540
|
-
for (let cursor = path;; cursor =
|
|
2541
|
-
if (
|
|
2943
|
+
for (let cursor = path;; cursor = dirname6(cursor)) {
|
|
2944
|
+
if (lstatSync5(cursor, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
2542
2945
|
throw new Error("Refusing symlink Claude orphan marker input");
|
|
2543
|
-
if (
|
|
2946
|
+
if (dirname6(cursor) === cursor)
|
|
2544
2947
|
break;
|
|
2545
2948
|
}
|
|
2546
2949
|
}
|
|
@@ -2550,7 +2953,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
|
|
|
2550
2953
|
if (changes?.has(path))
|
|
2551
2954
|
throw new Error("Claude orphan markers cannot intersect a planned write");
|
|
2552
2955
|
safe(path);
|
|
2553
|
-
const before =
|
|
2956
|
+
const before = lstatSync5(path, { throwIfNoEntry: false });
|
|
2554
2957
|
if (!before)
|
|
2555
2958
|
return null;
|
|
2556
2959
|
const uid = process.getuid?.();
|
|
@@ -2571,7 +2974,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
|
|
|
2571
2974
|
}
|
|
2572
2975
|
if (!/^[1-9][0-9]{12}$/.test(bytes.subarray(0, length).toString("latin1")))
|
|
2573
2976
|
throw new Error("Malformed Claude orphan marker timestamp");
|
|
2574
|
-
const after = fstatSync4(fd), current =
|
|
2977
|
+
const after = fstatSync4(fd), current = lstatSync5(path, { throwIfNoEntry: false });
|
|
2575
2978
|
safe(path);
|
|
2576
2979
|
if (!same(opened, after) || !same(after, current) || length !== after.size)
|
|
2577
2980
|
throw new Error("Claude orphan marker changed during read");
|
|
@@ -2580,7 +2983,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
|
|
|
2580
2983
|
throw new Error("Raw discovery aggregate byte limit exceeded");
|
|
2581
2984
|
budget.remaining -= length;
|
|
2582
2985
|
}
|
|
2583
|
-
return
|
|
2986
|
+
return createHash7("sha256").update(`${CLAUDE_ORPHAN_MARKER_MODE}\x00${path}\x00file`).digest("hex");
|
|
2584
2987
|
} finally {
|
|
2585
2988
|
closeSync4(fd);
|
|
2586
2989
|
}
|
|
@@ -2640,7 +3043,7 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2640
3043
|
text(control.pluginId, 1024);
|
|
2641
3044
|
text(control.namespace, 64);
|
|
2642
3045
|
text(control.pluginParent);
|
|
2643
|
-
requireBound(isAbsolute5(control.pluginParent) &&
|
|
3046
|
+
requireBound(isAbsolute5(control.pluginParent) && resolve7(control.pluginParent) === control.pluginParent && /^[a-f0-9]{64}$/.test(control.manifestSha256));
|
|
2644
3047
|
if (control.remotePluginId !== undefined)
|
|
2645
3048
|
requireBound(typeof control.remotePluginId === "string" && /^[A-Za-z0-9_~-]{1,1024}$/.test(control.remotePluginId));
|
|
2646
3049
|
if (control.skillsOnly !== undefined)
|
|
@@ -2658,7 +3061,7 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2658
3061
|
text(control.pluginId, 1024);
|
|
2659
3062
|
text(control.namespace, 64);
|
|
2660
3063
|
text(control.pluginParent);
|
|
2661
|
-
requireBound(!ids.has(control.pluginId) && isAbsolute5(control.pluginParent) &&
|
|
3064
|
+
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
3065
|
ids.add(control.pluginId);
|
|
2663
3066
|
}
|
|
2664
3067
|
}
|
|
@@ -2667,7 +3070,7 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2667
3070
|
requireBound(agent === "gemini" && object(executable) && Object.keys(executable).length === 3 && executable.command === "gemini");
|
|
2668
3071
|
for (const path of [executable.path, executable.target]) {
|
|
2669
3072
|
text(path);
|
|
2670
|
-
requireBound(!/[\x00-\x1f\x7f-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]/.test(path) && isAbsolute5(path) &&
|
|
3073
|
+
requireBound(!/[\x00-\x1f\x7f-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]/.test(path) && isAbsolute5(path) && resolve7(path) === path);
|
|
2671
3074
|
}
|
|
2672
3075
|
}
|
|
2673
3076
|
if (bridge.disabledBuiltins !== undefined)
|
|
@@ -2695,11 +3098,11 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2695
3098
|
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
3099
|
for (const root of roots) {
|
|
2697
3100
|
text(root);
|
|
2698
|
-
requireBound(isAbsolute5(root) &&
|
|
3101
|
+
requireBound(isAbsolute5(root) && resolve7(root) === root);
|
|
2699
3102
|
}
|
|
2700
3103
|
for (const parent of parents2) {
|
|
2701
3104
|
text(parent);
|
|
2702
|
-
requireBound(isAbsolute5(parent) &&
|
|
3105
|
+
requireBound(isAbsolute5(parent) && resolve7(parent) === parent);
|
|
2703
3106
|
}
|
|
2704
3107
|
for (const directory of array(proof.directories, AGENT_POLICY_LIMITS.discoveryDirectories)) {
|
|
2705
3108
|
requireBound(object(directory));
|
|
@@ -2719,7 +3122,7 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2719
3122
|
text(input.namespace, 64);
|
|
2720
3123
|
text(input.pluginParent);
|
|
2721
3124
|
text(input.sourceRoot);
|
|
2722
|
-
requireBound(!ids.has(input.pluginId) && [input.pluginParent, input.sourceRoot].every((path) => isAbsolute5(path) &&
|
|
3125
|
+
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
3126
|
ids.add(input.pluginId);
|
|
2724
3127
|
}
|
|
2725
3128
|
if (proof.directories !== undefined)
|
|
@@ -2733,14 +3136,14 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2733
3136
|
const proof = record(value.codexSemanticCache, 9), witness = record(proof.witness, 7);
|
|
2734
3137
|
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
3138
|
text(proof.configPath);
|
|
2736
|
-
requireBound(isAbsolute5(proof.configPath) &&
|
|
3139
|
+
requireBound(isAbsolute5(proof.configPath) && resolve7(proof.configPath) === proof.configPath && basename4(proof.configPath) === "config.toml");
|
|
2737
3140
|
text(witness.cacheRoot);
|
|
2738
|
-
requireBound(isAbsolute5(witness.cacheRoot) &&
|
|
3141
|
+
requireBound(isAbsolute5(witness.cacheRoot) && resolve7(witness.cacheRoot) === witness.cacheRoot);
|
|
2739
3142
|
if (proof.hookRootAlias !== null) {
|
|
2740
3143
|
requireBound(object(proof.hookRootAlias) && Object.keys(proof.hookRootAlias).length === 2);
|
|
2741
3144
|
for (const key of ["alias", "target"]) {
|
|
2742
3145
|
text(proof.hookRootAlias[key]);
|
|
2743
|
-
requireBound(isAbsolute5(proof.hookRootAlias[key]) &&
|
|
3146
|
+
requireBound(isAbsolute5(proof.hookRootAlias[key]) && resolve7(proof.hookRootAlias[key]) === proof.hookRootAlias[key]);
|
|
2744
3147
|
}
|
|
2745
3148
|
}
|
|
2746
3149
|
const parents2 = array(proof.appOnlyParents, 4096);
|
|
@@ -2748,7 +3151,7 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2748
3151
|
requireBound(object(parent) && Object.keys(parent).length === 5 && parent.role === "app-only");
|
|
2749
3152
|
for (const key of ["pluginId", "namespace", "pluginParent", "remotePluginId"])
|
|
2750
3153
|
text(parent[key]);
|
|
2751
|
-
requireBound(isAbsolute5(parent.pluginParent) &&
|
|
3154
|
+
requireBound(isAbsolute5(parent.pluginParent) && resolve7(parent.pluginParent) === parent.pluginParent);
|
|
2752
3155
|
}
|
|
2753
3156
|
if (proof.reviewedHookParents !== undefined)
|
|
2754
3157
|
for (const parent of array(proof.reviewedHookParents, 4096)) {
|
|
@@ -2760,9 +3163,9 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2760
3163
|
text(parent.remotePluginId);
|
|
2761
3164
|
else {
|
|
2762
3165
|
text(parent.marketplaceSourcePath);
|
|
2763
|
-
requireBound(typeof parent.marketplaceSha256 === "string" && /^[a-f0-9]{64}$/.test(parent.marketplaceSha256) && isAbsolute5(parent.marketplaceSourcePath) &&
|
|
3166
|
+
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
3167
|
}
|
|
2765
|
-
requireBound(isAbsolute5(parent.pluginParent) &&
|
|
3168
|
+
requireBound(isAbsolute5(parent.pluginParent) && resolve7(parent.pluginParent) === parent.pluginParent);
|
|
2766
3169
|
}
|
|
2767
3170
|
for (const row of [...array(witness.controls, 4096), ...array(witness.appOnlyParents, 4096)]) {
|
|
2768
3171
|
requireBound(object(row));
|
|
@@ -2782,9 +3185,9 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2782
3185
|
text(row.remotePluginId);
|
|
2783
3186
|
else {
|
|
2784
3187
|
text(row.marketplaceSourcePath);
|
|
2785
|
-
requireBound(typeof row.marketplaceSha256 === "string" && /^[a-f0-9]{64}$/.test(row.marketplaceSha256) && isAbsolute5(row.marketplaceSourcePath) &&
|
|
3188
|
+
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
3189
|
}
|
|
2787
|
-
requireBound(isAbsolute5(row.pluginParent) &&
|
|
3190
|
+
requireBound(isAbsolute5(row.pluginParent) && resolve7(row.pluginParent) === row.pluginParent);
|
|
2788
3191
|
for (const key of ["manifestSha256", "hooksSha256"])
|
|
2789
3192
|
requireBound(typeof row[key] === "string" && /^[a-f0-9]{64}$/.test(row[key]));
|
|
2790
3193
|
if (row.installation === "remote-receipt")
|
|
@@ -2817,17 +3220,17 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2817
3220
|
text(source.path);
|
|
2818
3221
|
requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
|
|
2819
3222
|
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));
|
|
3223
|
+
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
3224
|
if (source.hashMode === CLAUDE_ORPHAN_MARKER_MODE)
|
|
2822
3225
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && isClaudePluginCacheOrphanMarker(source.path) && source.managedPlugins === undefined && source.reviewArtifact === undefined);
|
|
2823
3226
|
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) &&
|
|
3227
|
+
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
3228
|
if (source.hashMode === "codex-settings-v6") {
|
|
2826
3229
|
const seen = new Set;
|
|
2827
3230
|
for (const hook of array(source.hookSources, AGENT_POLICY_LIMITS.discoverySources)) {
|
|
2828
3231
|
requireBound(object(hook) && Object.keys(hook).length === 2 && Object.keys(hook).every((key) => ["path", "sha256"].includes(key)));
|
|
2829
3232
|
text(hook.path);
|
|
2830
|
-
requireBound(isAbsolute5(hook.path) &&
|
|
3233
|
+
requireBound(isAbsolute5(hook.path) && resolve7(hook.path) === hook.path && !seen.has(hook.path));
|
|
2831
3234
|
seen.add(hook.path);
|
|
2832
3235
|
requireBound(hook.sha256 === null || typeof hook.sha256 === "string" && /^[a-f0-9]{64}$/.test(hook.sha256));
|
|
2833
3236
|
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 +3238,15 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2835
3238
|
} else
|
|
2836
3239
|
requireBound(source.hookSources === undefined);
|
|
2837
3240
|
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) &&
|
|
3241
|
+
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
3242
|
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) &&
|
|
3243
|
+
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");
|
|
3244
|
+
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"))
|
|
3245
|
+
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");
|
|
3246
|
+
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"))
|
|
3247
|
+
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
3248
|
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) &&
|
|
3249
|
+
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
3250
|
if (source.hashMode === "claude-marketplace-entry-v1")
|
|
2848
3251
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && claudeMarketplaceEntrySourceValid(source));
|
|
2849
3252
|
if (source.hashMode === "claude-plugin-registry") {
|
|
@@ -2862,9 +3265,9 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2862
3265
|
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
3266
|
for (const path of [artifact.storeRoot, artifact.originalPath]) {
|
|
2864
3267
|
text(path);
|
|
2865
|
-
requireBound(isAbsolute5(path) &&
|
|
3268
|
+
requireBound(isAbsolute5(path) && resolve7(path) === path);
|
|
2866
3269
|
}
|
|
2867
|
-
requireBound(source.path ===
|
|
3270
|
+
requireBound(source.path === join7(artifact.storeRoot, "agent-discovery", "artifacts", artifact.sha256, "catalog.json"));
|
|
2868
3271
|
}
|
|
2869
3272
|
if (source.format !== undefined)
|
|
2870
3273
|
requireBound(["json", "toml", "yaml"].includes(source.format));
|
|
@@ -2915,10 +3318,10 @@ function serializeManagedSkillPolicy(value) {
|
|
|
2915
3318
|
return text2;
|
|
2916
3319
|
}
|
|
2917
3320
|
function readManagedSkillPolicySnapshot(dataDir2 = getDataDirReadOnly()) {
|
|
2918
|
-
const path =
|
|
3321
|
+
const path = join8(dataDir2, "agent-policy.json");
|
|
2919
3322
|
let descriptor, observed = false;
|
|
2920
3323
|
try {
|
|
2921
|
-
const stat =
|
|
3324
|
+
const stat = lstatSync6(path);
|
|
2922
3325
|
observed = true;
|
|
2923
3326
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
2924
3327
|
throw refusal();
|
|
@@ -2944,7 +3347,7 @@ function readManagedSkillPolicySnapshot(dataDir2 = getDataDirReadOnly()) {
|
|
|
2944
3347
|
return { text: text2, value: parseManagedSkillPolicy(text2) };
|
|
2945
3348
|
} catch (error) {
|
|
2946
3349
|
if (error.code === "ENOENT" && !observed) {
|
|
2947
|
-
if (
|
|
3350
|
+
if (lstatSync6(path, { throwIfNoEntry: false }) === undefined)
|
|
2948
3351
|
return null;
|
|
2949
3352
|
}
|
|
2950
3353
|
if (error instanceof SkillSelectionError)
|
|
@@ -2974,7 +3377,7 @@ function detectedIntegrationAgents(options = {}) {
|
|
|
2974
3377
|
throw new Error("Invalid existing bridge agent inventory");
|
|
2975
3378
|
const present = (path) => {
|
|
2976
3379
|
try {
|
|
2977
|
-
return
|
|
3380
|
+
return lstatSync7(path, { throwIfNoEntry: false }) !== undefined;
|
|
2978
3381
|
} catch {
|
|
2979
3382
|
throw new Error("Agent presence cannot be inspected; review native configuration before hook installation");
|
|
2980
3383
|
}
|
|
@@ -2984,13 +3387,13 @@ function detectedIntegrationAgents(options = {}) {
|
|
|
2984
3387
|
return true;
|
|
2985
3388
|
if (agent === "sumi") {
|
|
2986
3389
|
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(
|
|
3390
|
+
const xdgSumi = xdg?.trim() ? resolve8(options.cwd ?? process.cwd(), xdg === "~" ? home : xdg.startsWith("~/") || xdg.startsWith("~\\") ? resolve8(home, xdg.slice(2)) : xdg, "sumi") : null;
|
|
3391
|
+
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
3392
|
}
|
|
2990
3393
|
if (agent === "hermes" && env.HERMES_HOME !== undefined)
|
|
2991
3394
|
return true;
|
|
2992
3395
|
const adapter = AGENT_ADAPTERS[agent];
|
|
2993
|
-
return present(
|
|
3396
|
+
return present(join9(home, dirname7(adapter.config))) || NATIVE_SKILL_ROOTS.some(([provider, root]) => provider === agent && dirname7(root) === `.${agent}` && present(join9(home, dirname7(root))));
|
|
2994
3397
|
});
|
|
2995
3398
|
if (!selected.length && !options.allowEmpty)
|
|
2996
3399
|
throw new Error(`No configured or detected agents; select --agent <agent> explicitly (${INTEGRATION_AGENTS.join(", ")}).`);
|
|
@@ -2999,8 +3402,8 @@ function detectedIntegrationAgents(options = {}) {
|
|
|
2999
3402
|
|
|
3000
3403
|
// src/lib/agent-sumi.ts
|
|
3001
3404
|
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
|
|
3405
|
+
import { accessSync, constants as constants6, existsSync as existsSync4, lstatSync as lstatSync8, realpathSync, statSync as statSync3 } from "fs";
|
|
3406
|
+
import { delimiter, isAbsolute as isAbsolute6, join as join10, resolve as resolve9 } from "path";
|
|
3004
3407
|
|
|
3005
3408
|
// src/lib/hook-diagnostics.ts
|
|
3006
3409
|
var CONTEXT_ERROR_CODES = new Set([
|
|
@@ -3064,9 +3467,11 @@ var OPTIONAL_CONTEXT_FAILURES = new Set([
|
|
|
3064
3467
|
"CACHED_PROFILE_EXPIRED",
|
|
3065
3468
|
"CACHED_PROFILE_MISSING",
|
|
3066
3469
|
"CACHED_BUNDLE_MISSING",
|
|
3470
|
+
"PROFILE_LOCK_MISMATCH",
|
|
3067
3471
|
"SESSION_RECONCILIATION_REQUIRED",
|
|
3068
3472
|
"SESSION_RENEWAL_TIMEOUT",
|
|
3069
3473
|
"SESSION_WRITE_BUSY",
|
|
3474
|
+
"SESSION_WRITE_LOCKED",
|
|
3070
3475
|
"SESSION_PARENT_CHANGED",
|
|
3071
3476
|
"SESSION_CONTEXT_CONFLICT",
|
|
3072
3477
|
"SKILLS_HOOK_TIMEOUT"
|
|
@@ -3089,7 +3494,7 @@ function recordWithKeys(value, keys) {
|
|
|
3089
3494
|
return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
3090
3495
|
}
|
|
3091
3496
|
function absolutePath2(value) {
|
|
3092
|
-
return typeof value === "string" && value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value) &&
|
|
3497
|
+
return typeof value === "string" && value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value) && resolve9(value) === value;
|
|
3093
3498
|
}
|
|
3094
3499
|
function absoluteInput(value) {
|
|
3095
3500
|
return value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value);
|
|
@@ -3100,7 +3505,7 @@ function resolveSumiPathsCommand(pathValue) {
|
|
|
3100
3505
|
for (const directory of pathValue.split(delimiter)) {
|
|
3101
3506
|
if (!directory || /[\x00-\x1f\x7f]/.test(directory))
|
|
3102
3507
|
continue;
|
|
3103
|
-
const candidate =
|
|
3508
|
+
const candidate = resolve9(directory, "sumi-paths");
|
|
3104
3509
|
try {
|
|
3105
3510
|
accessSync(candidate, constants6.X_OK);
|
|
3106
3511
|
if (statSync3(candidate).isFile())
|
|
@@ -3112,7 +3517,7 @@ function resolveSumiPathsCommand(pathValue) {
|
|
|
3112
3517
|
function validPathPlan(value, home, cwd) {
|
|
3113
3518
|
if (!recordWithKeys(value, ["schemaVersion", "kind", "home", "cwd", "roots", "legacyRoots", "configFiles", "skillRoots"]))
|
|
3114
3519
|
return false;
|
|
3115
|
-
if (value.schemaVersion !== 1 || value.kind !== "sumi-paths" || value.home !==
|
|
3520
|
+
if (value.schemaVersion !== 1 || value.kind !== "sumi-paths" || value.home !== resolve9(home) || value.cwd !== resolve9(cwd))
|
|
3116
3521
|
return false;
|
|
3117
3522
|
if (!recordWithKeys(value.roots, SUMI_PATH_KEYS) || !recordWithKeys(value.legacyRoots, SUMI_PATH_KEYS))
|
|
3118
3523
|
return false;
|
|
@@ -3134,11 +3539,11 @@ function validPathPlan(value, home, cwd) {
|
|
|
3134
3539
|
const legacyConfigRoot = value.legacyRoots.config;
|
|
3135
3540
|
if (typeof configRoot !== "string" || legacyConfigRoot !== null && typeof legacyConfigRoot !== "string")
|
|
3136
3541
|
return false;
|
|
3137
|
-
if (value.configFiles.canonical !==
|
|
3542
|
+
if (value.configFiles.canonical !== join10(configRoot, "sumi.json") || value.skillRoots.canonical !== join10(configRoot, "skills"))
|
|
3138
3543
|
return false;
|
|
3139
3544
|
if (legacyConfigRoot === null)
|
|
3140
3545
|
return value.configFiles.legacy === null && value.skillRoots.legacy === null;
|
|
3141
|
-
return value.configFiles.legacy ===
|
|
3546
|
+
return value.configFiles.legacy === join10(legacyConfigRoot, "sumi.json") && value.skillRoots.legacy === join10(legacyConfigRoot, "skills");
|
|
3142
3547
|
}
|
|
3143
3548
|
function sumiPathPlan(home, cwd = process.cwd(), env = process.env) {
|
|
3144
3549
|
for (const key of ["SUMI_CONFIG", "SUMI_CONFIG_CONTENT"])
|
|
@@ -3156,8 +3561,8 @@ function sumiPathPlan(home, cwd = process.cwd(), env = process.env) {
|
|
|
3156
3561
|
childEnv[key] = env[key];
|
|
3157
3562
|
let child2;
|
|
3158
3563
|
try {
|
|
3159
|
-
child2 = spawnSync2(executable, ["--json", "--home",
|
|
3160
|
-
cwd:
|
|
3564
|
+
child2 = spawnSync2(executable, ["--json", "--home", resolve9(home), "--cwd", resolve9(cwd)], {
|
|
3565
|
+
cwd: resolve9(cwd),
|
|
3161
3566
|
env: childEnv,
|
|
3162
3567
|
encoding: "buffer",
|
|
3163
3568
|
timeout: 3000,
|
|
@@ -3189,7 +3594,7 @@ function selectedConfigDirectory(plan) {
|
|
|
3189
3594
|
const canonical3 = plan.roots.config, legacy = plan.legacyRoots.config;
|
|
3190
3595
|
if (legacy === null)
|
|
3191
3596
|
return existsSync4(canonical3) ? realpathSync(canonical3) : canonical3;
|
|
3192
|
-
const canonicalStat =
|
|
3597
|
+
const canonicalStat = lstatSync8(canonical3, { throwIfNoEntry: false }), legacyStat = lstatSync8(legacy, { throwIfNoEntry: false });
|
|
3193
3598
|
if (canonicalStat && legacyStat && realpathSync(canonical3) !== realpathSync(legacy))
|
|
3194
3599
|
throw new Error("Sumi config roots conflict; complete native home adoption before Skills integration");
|
|
3195
3600
|
const selected = canonicalStat ? canonical3 : legacyStat ? legacy : canonical3;
|
|
@@ -3205,13 +3610,13 @@ function sumiConfigDirectory(home, env = process.env, cwd = process.cwd()) {
|
|
|
3205
3610
|
}
|
|
3206
3611
|
function sumiSkillRoots(home, cwd = process.cwd(), env = process.env) {
|
|
3207
3612
|
const plan = sumiPathPlan(home, cwd, env);
|
|
3208
|
-
return [
|
|
3613
|
+
return [join10(selectedConfigDirectory(plan), "skills")];
|
|
3209
3614
|
}
|
|
3210
3615
|
function sumiBridgeRoot(home, cwd = process.cwd(), env = process.env) {
|
|
3211
3616
|
return sumiSkillRoots(home, cwd, env)[0];
|
|
3212
3617
|
}
|
|
3213
3618
|
function sumiConfigPath(home, cwd = process.cwd(), env = process.env) {
|
|
3214
|
-
return
|
|
3619
|
+
return join10(sumiConfigDirectory(home, env, cwd), "sumi.json");
|
|
3215
3620
|
}
|
|
3216
3621
|
function renderSumiPlugin(command, profile) {
|
|
3217
3622
|
return renderSumiPluginVersion(command, profile, 3);
|