@hasna/skills 0.10.22 → 0.10.24

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 CHANGED
@@ -431,6 +431,12 @@ directory within your home, add `--allow-root-aliases` to hook installation and
431
431
  native migration. The plan records and rechecks the exact link and target;
432
432
  links inside skill contents or configuration files remain refused.
433
433
 
434
+ On macOS, the OS-owned `/var`, `/tmp` and `/etc` aliases into `/private` are
435
+ accepted only while their exact targets and protected parent directories verify.
436
+ The protection check refuses extended ACLs and unavailable native ACL inspection.
437
+ Inventory reports canonical paths and deduplicates their alias spellings. This
438
+ does not permit other root-owned links or user-controlled ancestor links.
439
+
434
440
  Cooperating installers can use the root package's
435
441
  `planClaudeStopHookUpdate({ expectedSettingsSha256, replacement })`, then
436
442
  `applyAgentIntegration(plan)`, to write an explicitly authorized Claude settings
@@ -519,8 +525,12 @@ Only manifest version metadata may vary. Reviewed app declarations use the impli
519
525
  `id`, optional `category`, and optional boolean `required`. The entire canonical app
520
526
  file and its presence stay bound; it preserves existing connector availability and
521
527
  native capability hints. Changed or unknown app fields, custom references, native
522
- hooks, commands and MCP controls refuse. Name controls suppress Skills, not every
523
- plugin capability. No prompt hook starts an app server to obtain this receipt:
528
+ hooks, commands and unsupported MCP controls refuse. Referenced own-root
529
+ `./.mcp.json` declarations may preserve local stdio servers with Codex's typed
530
+ tool, approval, timeout and environment metadata. Their complete fingerprints
531
+ and presence remain bound; review never executes commands or resolves environment
532
+ values. Name controls suppress Skills, not every plugin capability.
533
+ No prompt hook starts an app server to obtain this receipt:
524
534
  native startup can maintain caches. The explicit capture starts and closes only its
525
535
  own bounded native client; it does not reload an existing agent session.
526
536
  Neither a receipt nor a config write proves an existing session adopted new rules;
package/bin/index.js CHANGED
@@ -36868,7 +36868,7 @@ var package_default;
36868
36868
  var init_package = __esm(() => {
36869
36869
  package_default = {
36870
36870
  name: "@hasna/skills",
36871
- version: "0.10.22",
36871
+ version: "0.10.24",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -39931,7 +39931,7 @@ function assertAgentPolicyCollections(policy) {
39931
39931
  }
39932
39932
  if (bridge.codexPluginSkills !== undefined)
39933
39933
  for (const control of array(bridge.codexPluginSkills, 4096)) {
39934
- requireBound(object(control) && Object.keys(control).every((key) => ["name", "pluginId", "namespace", "pluginParent", "manifestSha256", "appSha256", "remotePluginId"].includes(key)));
39934
+ requireBound(object(control) && Object.keys(control).every((key) => ["name", "pluginId", "namespace", "pluginParent", "manifestSha256", "appSha256", "mcpSha256", "remotePluginId"].includes(key)));
39935
39935
  text(control.name, 129);
39936
39936
  text(control.pluginId, 1024);
39937
39937
  text(control.namespace, 64);
@@ -39939,6 +39939,8 @@ function assertAgentPolicyCollections(policy) {
39939
39939
  requireBound(isAbsolute2(control.pluginParent) && resolve3(control.pluginParent) === control.pluginParent && /^[a-f0-9]{64}$/.test(control.manifestSha256));
39940
39940
  if (control.remotePluginId !== undefined)
39941
39941
  requireBound(typeof control.remotePluginId === "string" && /^[A-Za-z0-9_~-]{1,1024}$/.test(control.remotePluginId));
39942
+ if (control.mcpSha256 !== undefined)
39943
+ requireBound(typeof control.mcpSha256 === "string" && /^[a-f0-9]{64}$/.test(control.mcpSha256));
39942
39944
  if (control.appSha256 !== undefined)
39943
39945
  requireBound(typeof control.appSha256 === "string" && /^[a-f0-9]{64}$/.test(control.appSha256));
39944
39946
  }
@@ -51226,6 +51228,77 @@ function appControls(path, read2) {
51226
51228
  }
51227
51229
  return sha2562;
51228
51230
  }
51231
+ function mcpControls(path, read2) {
51232
+ const text2 = read2(path);
51233
+ let value, sha2562;
51234
+ try {
51235
+ sha2562 = hashNativeJsonControls(text2);
51236
+ value = JSON.parse(text2);
51237
+ } catch {
51238
+ refuse2();
51239
+ }
51240
+ const object5 = (v) => Boolean(v && typeof v === "object" && !Array.isArray(v));
51241
+ const textField = (v, empty = false) => typeof v === "string" && (empty || Boolean(v.trim())) && Buffer.byteLength(v) <= 16384 && !/[\x00-\x1f\x7f]/.test(v);
51242
+ const envName = (v) => typeof v === "string" && /^[A-Za-z_][A-Za-z0-9_]{0,255}$/.test(v);
51243
+ const optional = (v, valid) => v === undefined || v === null || valid(v);
51244
+ const stringList = (v) => Array.isArray(v) && v.length <= 256 && v.every((item) => textField(item));
51245
+ const approval = (v) => ["auto", "prompt", "writes", "approve"].includes(v);
51246
+ const duration = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
51247
+ const positiveInteger = (v) => Number.isSafeInteger(v) && v > 0;
51248
+ const fields = [
51249
+ "command",
51250
+ "args",
51251
+ "cwd",
51252
+ "env",
51253
+ "env_vars",
51254
+ "enabled",
51255
+ "required",
51256
+ "environment_id",
51257
+ "startup_readiness",
51258
+ "supports_parallel_tool_calls",
51259
+ "tool_input_schema_max_bytes",
51260
+ "startup_timeout_sec",
51261
+ "startup_timeout_ms",
51262
+ "tool_timeout_sec",
51263
+ "default_tools_approval_mode",
51264
+ "enabled_tools",
51265
+ "disabled_tools",
51266
+ "omit_tools_from",
51267
+ "tools",
51268
+ "name"
51269
+ ];
51270
+ if (!object5(value) || Object.keys(value).length !== 1 || !object5(value.mcpServers))
51271
+ refuse2();
51272
+ const servers = Object.entries(value.mcpServers);
51273
+ if (!servers.length || servers.length > 256)
51274
+ refuse2();
51275
+ for (const [name, server] of servers) {
51276
+ if (!identifier2(name) || !object5(server) || !Object.keys(server).every((key) => fields.includes(key)) || !textField(server.command) || !optional(server.args, (v) => Array.isArray(v) && v.length <= 256 && v.every((arg) => textField(arg, true))) || !optional(server.cwd, textField) || !optional(server.env, (v) => object5(v) && Object.keys(v).length <= 256 && Object.entries(v).every(([key, value2]) => envName(key) && textField(value2, true))) || !["enabled", "required", "supports_parallel_tool_calls"].every((key) => optional(server[key], (v) => typeof v === "boolean")) || !optional(server.environment_id, (v) => v === "local") || !optional(server.startup_readiness, (v) => ["connection", "catalog"].includes(v)) || !optional(server.tool_input_schema_max_bytes, positiveInteger) || !["startup_timeout_sec", "tool_timeout_sec"].every((key) => optional(server[key], duration)) || !optional(server.startup_timeout_ms, (v) => Number.isSafeInteger(v) && v >= 0) || !optional(server.default_tools_approval_mode, approval) || !["enabled_tools", "disabled_tools"].every((key) => optional(server[key], stringList)) || !optional(server.omit_tools_from, (v) => Array.isArray(v) && v.length <= 256 && v.every((item) => ["code_mode", "deferred", "direct"].includes(item))) || !optional(server.name, textField))
51277
+ refuse2();
51278
+ if (server.env_vars !== undefined && server.env_vars !== null) {
51279
+ if (!Array.isArray(server.env_vars) || server.env_vars.length > 256)
51280
+ refuse2();
51281
+ const names = server.env_vars.map((entry) => {
51282
+ if (envName(entry))
51283
+ return entry;
51284
+ if (!object5(entry) || !Object.keys(entry).every((key) => ["name", "source"].includes(key)) || !envName(entry.name) || !optional(entry.source, (v) => v === "local"))
51285
+ refuse2();
51286
+ return entry.name;
51287
+ });
51288
+ if (new Set(names).size !== names.length)
51289
+ refuse2();
51290
+ }
51291
+ if (server.tools !== undefined && server.tools !== null) {
51292
+ if (!object5(server.tools) || Object.keys(server.tools).length > 256)
51293
+ refuse2();
51294
+ for (const [tool, config] of Object.entries(server.tools)) {
51295
+ if (!textField(tool) || !object5(config) || !Object.keys(config).every((key) => ["approval_mode", "output_token_limit"].includes(key)) || !optional(config.approval_mode, approval) || !optional(config.output_token_limit, positiveInteger))
51296
+ refuse2();
51297
+ }
51298
+ }
51299
+ }
51300
+ return sha2562;
51301
+ }
51229
51302
  function rootControls(root, read2) {
51230
51303
  const manifestPath = join18(root, ".codex-plugin/plugin.json"), manifestFile = lstatSync12(manifestPath, { throwIfNoEntry: false });
51231
51304
  if (!lstatSync12(join18(root, ".codex-plugin"), { throwIfNoEntry: false })?.isDirectory() || !manifestFile?.isFile() || manifestFile.size > 1024 * 1024)
@@ -51243,15 +51316,21 @@ function rootControls(root, read2) {
51243
51316
  const cacheVersion = basename5(root), declaredVersion = manifest.version === undefined ? "" : typeof manifest.version === "string" ? manifest.version.trim() : null;
51244
51317
  if (declaredVersion === null || (declaredVersion ? declaredVersion !== cacheVersion : cacheVersion !== CODEX_DEFAULT_AGENT_PLUGIN_VERSION))
51245
51318
  refuse2();
51246
- if (manifest.hooks !== undefined || manifest.mcpServers !== undefined || manifest.commands !== undefined || manifest.apps !== undefined && manifest.apps !== "./.app.json" || ["hooks", ".mcp.json"].some((path) => lstatSync12(join18(root, path), { throwIfNoEntry: false })))
51319
+ if (manifest.hooks !== undefined || manifest.commands !== undefined || manifest.mcpServers !== undefined && manifest.mcpServers !== "./.mcp.json" || manifest.apps !== undefined && manifest.apps !== "./.app.json" || lstatSync12(join18(root, "hooks"), { throwIfNoEntry: false }))
51320
+ refuse2();
51321
+ const mcp = join18(root, ".mcp.json"), mcpPresent = lstatSync12(mcp, { throwIfNoEntry: false });
51322
+ if (mcpPresent && (!mcpPresent.isFile() || mcpPresent.isSymbolicLink() || mcpPresent.size > 1024 * 1024 || realpathSync5(mcp) !== mcp))
51323
+ refuse2();
51324
+ if (Boolean(mcpPresent) !== (manifest.mcpServers !== undefined))
51247
51325
  refuse2();
51326
+ const mcpSha256 = mcpPresent ? mcpControls(mcp, read2) : undefined;
51248
51327
  const app = join18(root, ".app.json"), present = lstatSync12(app, { throwIfNoEntry: false });
51249
51328
  if (present && (!present.isFile() || present.isSymbolicLink() || present.size > 1024 * 1024))
51250
51329
  refuse2();
51251
51330
  if (manifest.apps !== undefined && !present)
51252
51331
  refuse2();
51253
51332
  const appSha256 = present ? appControls(app, read2) : undefined;
51254
- return { namespace: manifest.name, manifestSha256, ...appSha256 ? { appSha256 } : {} };
51333
+ return { namespace: manifest.name, manifestSha256, ...appSha256 ? { appSha256 } : {}, ...mcpSha256 ? { mcpSha256 } : {} };
51255
51334
  }
51256
51335
  function identity4(document3, cache3, read2) {
51257
51336
  if (!document3.startsWith(cache3 + sep4) || !document3.endsWith(sep4 + "SKILL.md"))
@@ -51314,7 +51393,7 @@ function reviewedCodexPluginCapabilitiesUnchanged(cache3, controls, read2) {
51314
51393
  if (!stat.isDirectory() || ![".codex-plugin/plugin.json", ".app.json", "hooks", ".mcp.json"].some((path) => lstatSync12(join18(root, path), { throwIfNoEntry: false })))
51315
51394
  continue;
51316
51395
  const parsed = rootControls(root, read2);
51317
- if (!controls.some((control) => control.pluginParent === parent && control.namespace === parsed.namespace && control.manifestSha256 === parsed.manifestSha256 && control.appSha256 === parsed.appSha256))
51396
+ if (!controls.some((control) => control.pluginParent === parent && control.namespace === parsed.namespace && control.manifestSha256 === parsed.manifestSha256 && control.appSha256 === parsed.appSha256 && control.mcpSha256 === parsed.mcpSha256))
51318
51397
  return false;
51319
51398
  }
51320
51399
  }
@@ -51374,7 +51453,7 @@ function reviewCodexPluginSkillControls(catalog, documents, cache3, cwd2, read2,
51374
51453
  }
51375
51454
  result2.push({ ...parsed, pluginId: expectedPluginId, ...remotePluginId ? { remotePluginId } : {} });
51376
51455
  }
51377
- if (!result2.length || new Set(result2.map((item) => item.name)).size !== result2.length)
51456
+ if (!result2.length && documents.some((document3) => document3.startsWith(cache3 + sep4)) || new Set(result2.map((item) => item.name)).size !== result2.length)
51378
51457
  refuse2();
51379
51458
  return result2;
51380
51459
  }
@@ -51387,7 +51466,7 @@ function isReviewedCodexPluginSkillDisabled(document3, cache3, controls, rules,
51387
51466
  } catch {
51388
51467
  return false;
51389
51468
  }
51390
- const reviewed = controls.find((item) => item.name === parsed.name && item.namespace === parsed.namespace && item.pluginParent === parsed.pluginParent && item.manifestSha256 === parsed.manifestSha256 && item.appSha256 === parsed.appSha256);
51469
+ const reviewed = controls.find((item) => item.name === parsed.name && item.namespace === parsed.namespace && item.pluginParent === parsed.pluginParent && item.manifestSha256 === parsed.manifestSha256 && item.appSha256 === parsed.appSha256 && item.mcpSha256 === parsed.mcpSha256);
51391
51470
  if (!reviewed || reviewed.remotePluginId !== undefined && !remoteInstallationMatches(reviewed.pluginParent, reviewed.remotePluginId, read2))
51392
51471
  return false;
51393
51472
  return isCodexNativeSkillDisabled({ name: parsed.name, path: document3, pluginId: reviewed.pluginId, enabled: true }, rules);
@@ -52077,10 +52156,75 @@ import { isDeepStrictEqual as isDeepStrictEqual4 } from "util";
52077
52156
  import { existsSync as existsSync12, lstatSync as lstatSync15, statSync as statSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync13, readdirSync as readdirSync8, opendirSync as opendirSync3, readlinkSync as readlinkSync4, realpathSync as realpathSync6, renameSync as renameSync2, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, chmodSync, openSync as openSync8, closeSync as closeSync8, fsyncSync as fsyncSync2, fstatSync as fstatSync8, readSync as readSync6, constants as constants8, linkSync } from "fs";
52078
52157
  import { dirname as dirname11, join as join21, resolve as resolve11, relative as relative5, isAbsolute as isAbsolute11, sep as sep5 } from "path";
52079
52158
  import { homedir as homedir3 } from "os";
52159
+ function hasNoDarwinAcl(fd) {
52160
+ try {
52161
+ const { dlopen, read: read2 } = __require("bun:ffi");
52162
+ const library = dlopen("/usr/lib/libSystem.B.dylib", {
52163
+ acl_get_fd: { args: ["i32"], returns: "ptr" },
52164
+ acl_free: { args: ["ptr"], returns: "i32" },
52165
+ __error: { args: [], returns: "ptr" }
52166
+ });
52167
+ try {
52168
+ const errno = library.symbols.__error();
52169
+ if (!errno)
52170
+ return false;
52171
+ const acl = library.symbols.acl_get_fd(fd);
52172
+ if (acl) {
52173
+ library.symbols.acl_free(acl);
52174
+ return false;
52175
+ }
52176
+ return (acl === null || acl === 0) && read2.i32(errno) === 2;
52177
+ } finally {
52178
+ library.close();
52179
+ }
52180
+ } catch {
52181
+ return false;
52182
+ }
52183
+ }
52184
+ function isSystemRootAlias(path) {
52185
+ if (process.platform !== "darwin" || !["/var", "/tmp", "/etc"].includes(path))
52186
+ return false;
52187
+ const target = `/private${path}`, link2 = `private${path}`;
52188
+ const identity5 = (stat) => `${stat.dev}:${stat.ino}:${stat.ctimeNs}:${stat.uid}:${stat.mode}`;
52189
+ try {
52190
+ const before2 = lstatSync15(path, { bigint: true });
52191
+ if (!before2.isSymbolicLink() || before2.uid !== 0n || readlinkSync4(path) !== link2)
52192
+ return false;
52193
+ const directories = ["/", "/private", target].map((directory) => {
52194
+ const stat = lstatSync15(directory, { bigint: true });
52195
+ if (!stat.isDirectory() || stat.uid !== 0n)
52196
+ throw new Error("Untrusted system alias directory");
52197
+ if (directory !== "/private/tmp") {
52198
+ if ((stat.mode & 0o022n) !== 0n)
52199
+ throw new Error("Writable system alias directory");
52200
+ } else if ((stat.mode & 0o1000n) === 0n)
52201
+ throw new Error("Unprotected system temporary directory");
52202
+ const fd = openSync8(directory, constants8.O_RDONLY | constants8.O_DIRECTORY | constants8.O_NOFOLLOW);
52203
+ try {
52204
+ if (identity5(fstatSync8(fd, { bigint: true })) !== identity5(stat) || !hasNoDarwinAcl(fd) || identity5(fstatSync8(fd, { bigint: true })) !== identity5(stat))
52205
+ throw new Error("Unverified system alias directory ACL");
52206
+ } finally {
52207
+ closeSync8(fd);
52208
+ }
52209
+ return { directory, identity: identity5(stat) };
52210
+ });
52211
+ return realpathSync6(path) === target && readlinkSync4(path) === link2 && identity5(lstatSync15(path, { bigint: true })) === identity5(before2) && directories.every((item) => identity5(lstatSync15(item.directory, { bigint: true })) === item.identity);
52212
+ } catch {
52213
+ return false;
52214
+ }
52215
+ }
52216
+ function canonicalSystemPath(path) {
52217
+ const absolute = resolve11(path), root = `/${absolute.split(sep5)[1]}`;
52218
+ if (process.platform !== "darwin" || !["/var", "/tmp", "/etc"].includes(root) || !lstatSync15(root, { throwIfNoEntry: false })?.isSymbolicLink())
52219
+ return absolute;
52220
+ if (!isSystemRootAlias(root))
52221
+ throw new Error(`Refusing symlink path: ${root}`);
52222
+ return `/private${absolute}`;
52223
+ }
52080
52224
  function assertSafePath(path) {
52081
52225
  let cursor = resolve11(path);
52082
52226
  while (true) {
52083
- if (lstatSync15(cursor, { throwIfNoEntry: false })?.isSymbolicLink())
52227
+ if (lstatSync15(cursor, { throwIfNoEntry: false })?.isSymbolicLink() && !isSystemRootAlias(cursor))
52084
52228
  throw new Error(`Refusing symlink path: ${cursor}`);
52085
52229
  const parent = dirname11(cursor);
52086
52230
  if (parent === cursor)
@@ -52116,7 +52260,7 @@ function rootAliases(home, allowed = false) {
52116
52260
  return result2;
52117
52261
  }
52118
52262
  function canonicalAgentPath(path, aliases) {
52119
- const absolute = resolve11(path), binding = aliases.find((item) => absolute === item.alias || absolute.startsWith(item.alias + sep5));
52263
+ const absolute = canonicalSystemPath(path), binding = aliases.find((item) => absolute === item.alias || absolute.startsWith(item.alias + sep5));
52120
52264
  return binding ? join21(binding.target, relative5(binding.alias, absolute)) : absolute;
52121
52265
  }
52122
52266
  function disabledCodexSkillPaths(config, aliases) {
@@ -52233,8 +52377,10 @@ function parseNativeMigrationTargetManifest(input) {
52233
52377
  throw new Error("Invalid native migration target fields");
52234
52378
  if (!ROOTS.some(([agent]) => agent === item.agent))
52235
52379
  throw new Error(`Unsupported native migration target agent: ${item.agent}`);
52236
- const projectRoot = resolve11(item.projectRoot);
52237
- if (!isAbsolute11(item.projectRoot) || projectRoot !== item.projectRoot || !existsSync12(projectRoot) || !lstatSync15(projectRoot).isDirectory())
52380
+ if (!isAbsolute11(item.projectRoot) || resolve11(item.projectRoot) !== item.projectRoot)
52381
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${item.projectRoot}`);
52382
+ const projectRoot = canonicalSystemPath(item.projectRoot);
52383
+ if (!existsSync12(projectRoot) || !lstatSync15(projectRoot).isDirectory())
52238
52384
  throw new Error(`Native migration target project root must be an existing absolute directory: ${item.projectRoot}`);
52239
52385
  assertSafePath(projectRoot);
52240
52386
  if (isAbsolute11(item.path) || item.path.includes("\\") || item.path.length === 0 || item.path.includes("\x00"))
@@ -52244,7 +52390,7 @@ function parseNativeMigrationTargetManifest(input) {
52244
52390
  throw new Error(`Native migration target path escapes or is not normalized: ${item.path}`);
52245
52391
  if (!/^[0-9a-f]{64}$/.test(item.treeSha256))
52246
52392
  throw new Error(`Invalid native migration tree digest for ${item.path}`);
52247
- const identity5 = `${item.agent}\x00${projectRoot}\x00${normalized}`;
52393
+ const identity5 = `${item.agent}\x00${canonicalSystemPath(projectRoot)}\x00${normalized}`;
52248
52394
  if (identities.has(identity5))
52249
52395
  throw new Error(`Duplicate native migration target: ${item.path}`);
52250
52396
  identities.add(identity5);
@@ -52261,17 +52407,19 @@ function selectNativeMigrationTargets(inventory, manifest) {
52261
52407
  const identities = new Set;
52262
52408
  const selected = [];
52263
52409
  for (const target of manifest.targets) {
52264
- const projectRoot = resolve11(target.projectRoot), normalized = relative5(projectRoot, resolve11(projectRoot, target.path));
52265
- if (!isAbsolute11(target.projectRoot) || projectRoot !== target.projectRoot || !existsSync12(projectRoot) || !lstatSync15(projectRoot).isDirectory())
52410
+ if (!isAbsolute11(target.projectRoot) || resolve11(target.projectRoot) !== target.projectRoot)
52411
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
52412
+ const projectRoot = canonicalSystemPath(target.projectRoot), normalized = relative5(projectRoot, resolve11(projectRoot, target.path));
52413
+ if (!existsSync12(projectRoot) || !lstatSync15(projectRoot).isDirectory())
52266
52414
  throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
52267
52415
  assertSafePath(projectRoot);
52268
52416
  if (isAbsolute11(target.path) || target.path.includes("\\") || target.path.includes("\x00") || !normalized || normalized === ".." || normalized.startsWith(`..${sep5}`) || normalized !== target.path || !/^[0-9a-f]{64}$/.test(target.treeSha256))
52269
52417
  throw new Error(`Invalid native migration target: ${target.path}`);
52270
- const identity5 = `${target.agent}\x00${projectRoot}\x00${normalized}`;
52418
+ const identity5 = `${target.agent}\x00${canonicalSystemPath(projectRoot)}\x00${normalized}`;
52271
52419
  if (identities.has(identity5))
52272
52420
  throw new Error(`Duplicate native migration target: ${target.path}`);
52273
52421
  identities.add(identity5);
52274
- const expected = resolve11(target.projectRoot, target.path);
52422
+ const expected = canonicalSystemPath(resolve11(target.projectRoot, target.path));
52275
52423
  const matches2 = inventory.filter((entry2) => entry2.agent === target.agent && resolve11(entry2.path) === expected);
52276
52424
  if (matches2.length !== 1)
52277
52425
  throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
@@ -52287,7 +52435,7 @@ function selectNativeMigrationTargets(inventory, manifest) {
52287
52435
  function projectAncestorDirectories(projects) {
52288
52436
  const directories = new Set;
52289
52437
  for (const project of projects) {
52290
- for (let path = resolve11(project), depth = 0;; depth++) {
52438
+ for (let path = canonicalSystemPath(project), depth = 0;; depth++) {
52291
52439
  if (depth >= 100)
52292
52440
  throw new Error("NATIVE_SKILL_DRIFT: project ancestor discovery limit exceeded");
52293
52441
  directories.add(path);
@@ -52300,11 +52448,13 @@ function projectAncestorDirectories(projects) {
52300
52448
  return [...directories];
52301
52449
  }
52302
52450
  function inventoryNativeSkills(home = homedir3(), options = {}) {
52451
+ home = canonicalSystemPath(home);
52303
52452
  if (options.reviewedCacheAlias !== undefined && (!options.includeVendor || !isAbsolute11(options.reviewedCacheAlias) || resolve11(options.reviewedCacheAlias) !== options.reviewedCacheAlias))
52304
52453
  throw new Error("A reviewed cache alias requires vendor inventory and an exact absolute path");
52305
52454
  if (options.disabledVendorPaths?.some((path) => !isAbsolute11(path) || resolve11(path) !== path))
52306
52455
  throw new Error("Disabled vendor paths must be exact absolute paths");
52307
- const disabledVendorPaths = new Set(options.disabledVendorPaths ?? []);
52456
+ const reviewedCacheAlias = options.reviewedCacheAlias === undefined ? undefined : canonicalSystemPath(options.reviewedCacheAlias);
52457
+ const disabledVendorPaths = new Set((options.disabledVendorPaths ?? []).map(canonicalSystemPath));
52308
52458
  const aliases = rootAliases(home, options.allowRootAliases);
52309
52459
  const selectedAgents = options.agents ? new Set(options.agents) : undefined;
52310
52460
  const includesAgent = (agent) => !selectedAgents || selectedAgents.has(agent);
@@ -52327,8 +52477,11 @@ function inventoryNativeSkills(home = homedir3(), options = {}) {
52327
52477
  }
52328
52478
  let reviewedCacheAliasFound = false;
52329
52479
  function verifiedSiblingCacheAlias(agent, path, parent) {
52330
- const link2 = readlinkSync4(path), target = resolve11(parent, link2);
52331
- if (dirname11(target) !== parent || (isAbsolute11(link2) ? link2 !== target : dirname11(link2) !== "."))
52480
+ const link2 = readlinkSync4(path), lexicalTarget = resolve11(parent, link2);
52481
+ if (isAbsolute11(link2) ? link2 !== lexicalTarget : dirname11(link2) !== ".")
52482
+ return false;
52483
+ const target = canonicalSystemPath(lexicalTarget);
52484
+ if (dirname11(target) !== parent)
52332
52485
  return false;
52333
52486
  if (!lstatSync15(target, { throwIfNoEntry: false })?.isDirectory())
52334
52487
  return false;
@@ -52337,11 +52490,11 @@ function inventoryNativeSkills(home = homedir3(), options = {}) {
52337
52490
  return false;
52338
52491
  if (scan.hasSkills) {
52339
52492
  const targetSkills = entries.filter((entry) => entry.agent === agent && entry.vendor && (entry.path === target || entry.path.startsWith(target + sep5)));
52340
- if (path === options.reviewedCacheAlias && targetSkills.length > 0)
52493
+ if (path === reviewedCacheAlias && targetSkills.length > 0)
52341
52494
  reviewedCacheAliasFound = true;
52342
52495
  if (agent === "codex" && targetSkills.length > 0 && targetSkills.every((entry) => disabledVendorPaths.has(entry.path) || options.disabledVendorSkill?.(entry) === true))
52343
52496
  return true;
52344
- if (path !== options.reviewedCacheAlias)
52497
+ if (path !== reviewedCacheAlias)
52345
52498
  return false;
52346
52499
  }
52347
52500
  return true;
package/bin/mcp.js CHANGED
@@ -20019,7 +20019,7 @@ var package_default;
20019
20019
  var init_package = __esm(() => {
20020
20020
  package_default = {
20021
20021
  name: "@hasna/skills",
20022
- version: "0.10.22",
20022
+ version: "0.10.24",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
@@ -38253,7 +38253,7 @@ function assertAgentPolicyCollections(policy) {
38253
38253
  }
38254
38254
  if (bridge.codexPluginSkills !== undefined)
38255
38255
  for (const control of array3(bridge.codexPluginSkills, 4096)) {
38256
- requireBound(object4(control) && Object.keys(control).every((key) => ["name", "pluginId", "namespace", "pluginParent", "manifestSha256", "appSha256", "remotePluginId"].includes(key)));
38256
+ requireBound(object4(control) && Object.keys(control).every((key) => ["name", "pluginId", "namespace", "pluginParent", "manifestSha256", "appSha256", "mcpSha256", "remotePluginId"].includes(key)));
38257
38257
  text(control.name, 129);
38258
38258
  text(control.pluginId, 1024);
38259
38259
  text(control.namespace, 64);
@@ -38261,6 +38261,8 @@ function assertAgentPolicyCollections(policy) {
38261
38261
  requireBound(isAbsolute3(control.pluginParent) && resolve2(control.pluginParent) === control.pluginParent && /^[a-f0-9]{64}$/.test(control.manifestSha256));
38262
38262
  if (control.remotePluginId !== undefined)
38263
38263
  requireBound(typeof control.remotePluginId === "string" && /^[A-Za-z0-9_~-]{1,1024}$/.test(control.remotePluginId));
38264
+ if (control.mcpSha256 !== undefined)
38265
+ requireBound(typeof control.mcpSha256 === "string" && /^[a-f0-9]{64}$/.test(control.mcpSha256));
38264
38266
  if (control.appSha256 !== undefined)
38265
38267
  requireBound(typeof control.appSha256 === "string" && /^[a-f0-9]{64}$/.test(control.appSha256));
38266
38268
  }
package/bin/migrate.js CHANGED
@@ -7,7 +7,7 @@ import { join as join5 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@hasna/skills",
10
- version: "0.10.22",
10
+ version: "0.10.24",
11
11
  description: "Skills library for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
package/bin/server.js CHANGED
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
24122
24122
  // package.json
24123
24123
  var package_default = {
24124
24124
  name: "@hasna/skills",
24125
- version: "0.10.22",
24125
+ version: "0.10.24",
24126
24126
  description: "Skills library for AI coding agents",
24127
24127
  type: "module",
24128
24128
  bin: {
package/bin/worker.js CHANGED
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24125
24125
  // package.json
24126
24126
  var package_default = {
24127
24127
  name: "@hasna/skills",
24128
- version: "0.10.22",
24128
+ version: "0.10.24",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
package/dist/index.js CHANGED
@@ -9038,7 +9038,7 @@ function assertAgentPolicyCollections(policy) {
9038
9038
  }
9039
9039
  if (bridge.codexPluginSkills !== undefined)
9040
9040
  for (const control of array(bridge.codexPluginSkills, 4096)) {
9041
- requireBound(object(control) && Object.keys(control).every((key) => ["name", "pluginId", "namespace", "pluginParent", "manifestSha256", "appSha256", "remotePluginId"].includes(key)));
9041
+ requireBound(object(control) && Object.keys(control).every((key) => ["name", "pluginId", "namespace", "pluginParent", "manifestSha256", "appSha256", "mcpSha256", "remotePluginId"].includes(key)));
9042
9042
  text(control.name, 129);
9043
9043
  text(control.pluginId, 1024);
9044
9044
  text(control.namespace, 64);
@@ -9046,6 +9046,8 @@ function assertAgentPolicyCollections(policy) {
9046
9046
  requireBound(isAbsolute2(control.pluginParent) && resolve2(control.pluginParent) === control.pluginParent && /^[a-f0-9]{64}$/.test(control.manifestSha256));
9047
9047
  if (control.remotePluginId !== undefined)
9048
9048
  requireBound(typeof control.remotePluginId === "string" && /^[A-Za-z0-9_~-]{1,1024}$/.test(control.remotePluginId));
9049
+ if (control.mcpSha256 !== undefined)
9050
+ requireBound(typeof control.mcpSha256 === "string" && /^[a-f0-9]{64}$/.test(control.mcpSha256));
9049
9051
  if (control.appSha256 !== undefined)
9050
9052
  requireBound(typeof control.appSha256 === "string" && /^[a-f0-9]{64}$/.test(control.appSha256));
9051
9053
  }
@@ -10879,7 +10881,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
10879
10881
  // package.json
10880
10882
  var package_default = {
10881
10883
  name: "@hasna/skills",
10882
- version: "0.10.22",
10884
+ version: "0.10.24",
10883
10885
  description: "Skills library for AI coding agents",
10884
10886
  type: "module",
10885
10887
  bin: {
@@ -13367,6 +13369,77 @@ function appControls(path, read2) {
13367
13369
  }
13368
13370
  return sha2562;
13369
13371
  }
13372
+ function mcpControls(path, read2) {
13373
+ const text2 = read2(path);
13374
+ let value, sha2562;
13375
+ try {
13376
+ sha2562 = hashNativeJsonControls(text2);
13377
+ value = JSON.parse(text2);
13378
+ } catch {
13379
+ refuse2();
13380
+ }
13381
+ const object5 = (v) => Boolean(v && typeof v === "object" && !Array.isArray(v));
13382
+ const textField = (v, empty = false) => typeof v === "string" && (empty || Boolean(v.trim())) && Buffer.byteLength(v) <= 16384 && !/[\x00-\x1f\x7f]/.test(v);
13383
+ const envName = (v) => typeof v === "string" && /^[A-Za-z_][A-Za-z0-9_]{0,255}$/.test(v);
13384
+ const optional = (v, valid) => v === undefined || v === null || valid(v);
13385
+ const stringList = (v) => Array.isArray(v) && v.length <= 256 && v.every((item) => textField(item));
13386
+ const approval = (v) => ["auto", "prompt", "writes", "approve"].includes(v);
13387
+ const duration = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
13388
+ const positiveInteger = (v) => Number.isSafeInteger(v) && v > 0;
13389
+ const fields = [
13390
+ "command",
13391
+ "args",
13392
+ "cwd",
13393
+ "env",
13394
+ "env_vars",
13395
+ "enabled",
13396
+ "required",
13397
+ "environment_id",
13398
+ "startup_readiness",
13399
+ "supports_parallel_tool_calls",
13400
+ "tool_input_schema_max_bytes",
13401
+ "startup_timeout_sec",
13402
+ "startup_timeout_ms",
13403
+ "tool_timeout_sec",
13404
+ "default_tools_approval_mode",
13405
+ "enabled_tools",
13406
+ "disabled_tools",
13407
+ "omit_tools_from",
13408
+ "tools",
13409
+ "name"
13410
+ ];
13411
+ if (!object5(value) || Object.keys(value).length !== 1 || !object5(value.mcpServers))
13412
+ refuse2();
13413
+ const servers = Object.entries(value.mcpServers);
13414
+ if (!servers.length || servers.length > 256)
13415
+ refuse2();
13416
+ for (const [name, server] of servers) {
13417
+ if (!identifier2(name) || !object5(server) || !Object.keys(server).every((key) => fields.includes(key)) || !textField(server.command) || !optional(server.args, (v) => Array.isArray(v) && v.length <= 256 && v.every((arg) => textField(arg, true))) || !optional(server.cwd, textField) || !optional(server.env, (v) => object5(v) && Object.keys(v).length <= 256 && Object.entries(v).every(([key, value2]) => envName(key) && textField(value2, true))) || !["enabled", "required", "supports_parallel_tool_calls"].every((key) => optional(server[key], (v) => typeof v === "boolean")) || !optional(server.environment_id, (v) => v === "local") || !optional(server.startup_readiness, (v) => ["connection", "catalog"].includes(v)) || !optional(server.tool_input_schema_max_bytes, positiveInteger) || !["startup_timeout_sec", "tool_timeout_sec"].every((key) => optional(server[key], duration)) || !optional(server.startup_timeout_ms, (v) => Number.isSafeInteger(v) && v >= 0) || !optional(server.default_tools_approval_mode, approval) || !["enabled_tools", "disabled_tools"].every((key) => optional(server[key], stringList)) || !optional(server.omit_tools_from, (v) => Array.isArray(v) && v.length <= 256 && v.every((item) => ["code_mode", "deferred", "direct"].includes(item))) || !optional(server.name, textField))
13418
+ refuse2();
13419
+ if (server.env_vars !== undefined && server.env_vars !== null) {
13420
+ if (!Array.isArray(server.env_vars) || server.env_vars.length > 256)
13421
+ refuse2();
13422
+ const names = server.env_vars.map((entry) => {
13423
+ if (envName(entry))
13424
+ return entry;
13425
+ if (!object5(entry) || !Object.keys(entry).every((key) => ["name", "source"].includes(key)) || !envName(entry.name) || !optional(entry.source, (v) => v === "local"))
13426
+ refuse2();
13427
+ return entry.name;
13428
+ });
13429
+ if (new Set(names).size !== names.length)
13430
+ refuse2();
13431
+ }
13432
+ if (server.tools !== undefined && server.tools !== null) {
13433
+ if (!object5(server.tools) || Object.keys(server.tools).length > 256)
13434
+ refuse2();
13435
+ for (const [tool, config] of Object.entries(server.tools)) {
13436
+ if (!textField(tool) || !object5(config) || !Object.keys(config).every((key) => ["approval_mode", "output_token_limit"].includes(key)) || !optional(config.approval_mode, approval) || !optional(config.output_token_limit, positiveInteger))
13437
+ refuse2();
13438
+ }
13439
+ }
13440
+ }
13441
+ return sha2562;
13442
+ }
13370
13443
  function rootControls(root, read2) {
13371
13444
  const manifestPath = join14(root, ".codex-plugin/plugin.json"), manifestFile = lstatSync12(manifestPath, { throwIfNoEntry: false });
13372
13445
  if (!lstatSync12(join14(root, ".codex-plugin"), { throwIfNoEntry: false })?.isDirectory() || !manifestFile?.isFile() || manifestFile.size > 1024 * 1024)
@@ -13384,15 +13457,21 @@ function rootControls(root, read2) {
13384
13457
  const cacheVersion = basename5(root), declaredVersion = manifest.version === undefined ? "" : typeof manifest.version === "string" ? manifest.version.trim() : null;
13385
13458
  if (declaredVersion === null || (declaredVersion ? declaredVersion !== cacheVersion : cacheVersion !== CODEX_DEFAULT_AGENT_PLUGIN_VERSION))
13386
13459
  refuse2();
13387
- if (manifest.hooks !== undefined || manifest.mcpServers !== undefined || manifest.commands !== undefined || manifest.apps !== undefined && manifest.apps !== "./.app.json" || ["hooks", ".mcp.json"].some((path) => lstatSync12(join14(root, path), { throwIfNoEntry: false })))
13460
+ if (manifest.hooks !== undefined || manifest.commands !== undefined || manifest.mcpServers !== undefined && manifest.mcpServers !== "./.mcp.json" || manifest.apps !== undefined && manifest.apps !== "./.app.json" || lstatSync12(join14(root, "hooks"), { throwIfNoEntry: false }))
13461
+ refuse2();
13462
+ const mcp = join14(root, ".mcp.json"), mcpPresent = lstatSync12(mcp, { throwIfNoEntry: false });
13463
+ if (mcpPresent && (!mcpPresent.isFile() || mcpPresent.isSymbolicLink() || mcpPresent.size > 1024 * 1024 || realpathSync5(mcp) !== mcp))
13464
+ refuse2();
13465
+ if (Boolean(mcpPresent) !== (manifest.mcpServers !== undefined))
13388
13466
  refuse2();
13467
+ const mcpSha256 = mcpPresent ? mcpControls(mcp, read2) : undefined;
13389
13468
  const app = join14(root, ".app.json"), present = lstatSync12(app, { throwIfNoEntry: false });
13390
13469
  if (present && (!present.isFile() || present.isSymbolicLink() || present.size > 1024 * 1024))
13391
13470
  refuse2();
13392
13471
  if (manifest.apps !== undefined && !present)
13393
13472
  refuse2();
13394
13473
  const appSha256 = present ? appControls(app, read2) : undefined;
13395
- return { namespace: manifest.name, manifestSha256, ...appSha256 ? { appSha256 } : {} };
13474
+ return { namespace: manifest.name, manifestSha256, ...appSha256 ? { appSha256 } : {}, ...mcpSha256 ? { mcpSha256 } : {} };
13396
13475
  }
13397
13476
  function identity3(document2, cache, read2) {
13398
13477
  if (!document2.startsWith(cache + sep4) || !document2.endsWith(sep4 + "SKILL.md"))
@@ -13455,7 +13534,7 @@ function reviewedCodexPluginCapabilitiesUnchanged(cache, controls, read2) {
13455
13534
  if (!stat.isDirectory() || ![".codex-plugin/plugin.json", ".app.json", "hooks", ".mcp.json"].some((path) => lstatSync12(join14(root, path), { throwIfNoEntry: false })))
13456
13535
  continue;
13457
13536
  const parsed = rootControls(root, read2);
13458
- if (!controls.some((control) => control.pluginParent === parent && control.namespace === parsed.namespace && control.manifestSha256 === parsed.manifestSha256 && control.appSha256 === parsed.appSha256))
13537
+ if (!controls.some((control) => control.pluginParent === parent && control.namespace === parsed.namespace && control.manifestSha256 === parsed.manifestSha256 && control.appSha256 === parsed.appSha256 && control.mcpSha256 === parsed.mcpSha256))
13459
13538
  return false;
13460
13539
  }
13461
13540
  }
@@ -13515,7 +13594,7 @@ function reviewCodexPluginSkillControls(catalog, documents, cache, cwd, read2, r
13515
13594
  }
13516
13595
  result.push({ ...parsed, pluginId: expectedPluginId, ...remotePluginId ? { remotePluginId } : {} });
13517
13596
  }
13518
- if (!result.length || new Set(result.map((item) => item.name)).size !== result.length)
13597
+ if (!result.length && documents.some((document2) => document2.startsWith(cache + sep4)) || new Set(result.map((item) => item.name)).size !== result.length)
13519
13598
  refuse2();
13520
13599
  return result;
13521
13600
  }
@@ -13528,7 +13607,7 @@ function isReviewedCodexPluginSkillDisabled(document2, cache, controls, rules, r
13528
13607
  } catch {
13529
13608
  return false;
13530
13609
  }
13531
- const reviewed = controls.find((item) => item.name === parsed.name && item.namespace === parsed.namespace && item.pluginParent === parsed.pluginParent && item.manifestSha256 === parsed.manifestSha256 && item.appSha256 === parsed.appSha256);
13610
+ const reviewed = controls.find((item) => item.name === parsed.name && item.namespace === parsed.namespace && item.pluginParent === parsed.pluginParent && item.manifestSha256 === parsed.manifestSha256 && item.appSha256 === parsed.appSha256 && item.mcpSha256 === parsed.mcpSha256);
13532
13611
  if (!reviewed || reviewed.remotePluginId !== undefined && !remoteInstallationMatches(reviewed.pluginParent, reviewed.remotePluginId, read2))
13533
13612
  return false;
13534
13613
  return isCodexNativeSkillDisabled({ name: parsed.name, path: document2, pluginId: reviewed.pluginId, enabled: true }, rules);
@@ -14201,10 +14280,75 @@ function disableCodexBundledSkills(text2) {
14201
14280
  // src/lib/agent-integration.ts
14202
14281
  var sha3 = (data) => createHash12("sha256").update(data).digest("hex");
14203
14282
  var ROOTS = NATIVE_SKILL_ROOTS;
14283
+ function hasNoDarwinAcl(fd) {
14284
+ try {
14285
+ const { dlopen, read: read2 } = __require("bun:ffi");
14286
+ const library = dlopen("/usr/lib/libSystem.B.dylib", {
14287
+ acl_get_fd: { args: ["i32"], returns: "ptr" },
14288
+ acl_free: { args: ["ptr"], returns: "i32" },
14289
+ __error: { args: [], returns: "ptr" }
14290
+ });
14291
+ try {
14292
+ const errno = library.symbols.__error();
14293
+ if (!errno)
14294
+ return false;
14295
+ const acl = library.symbols.acl_get_fd(fd);
14296
+ if (acl) {
14297
+ library.symbols.acl_free(acl);
14298
+ return false;
14299
+ }
14300
+ return (acl === null || acl === 0) && read2.i32(errno) === 2;
14301
+ } finally {
14302
+ library.close();
14303
+ }
14304
+ } catch {
14305
+ return false;
14306
+ }
14307
+ }
14308
+ function isSystemRootAlias(path) {
14309
+ if (process.platform !== "darwin" || !["/var", "/tmp", "/etc"].includes(path))
14310
+ return false;
14311
+ const target = `/private${path}`, link = `private${path}`;
14312
+ const identity4 = (stat) => `${stat.dev}:${stat.ino}:${stat.ctimeNs}:${stat.uid}:${stat.mode}`;
14313
+ try {
14314
+ const before = lstatSync15(path, { bigint: true });
14315
+ if (!before.isSymbolicLink() || before.uid !== 0n || readlinkSync4(path) !== link)
14316
+ return false;
14317
+ const directories = ["/", "/private", target].map((directory) => {
14318
+ const stat = lstatSync15(directory, { bigint: true });
14319
+ if (!stat.isDirectory() || stat.uid !== 0n)
14320
+ throw new Error("Untrusted system alias directory");
14321
+ if (directory !== "/private/tmp") {
14322
+ if ((stat.mode & 0o022n) !== 0n)
14323
+ throw new Error("Writable system alias directory");
14324
+ } else if ((stat.mode & 0o1000n) === 0n)
14325
+ throw new Error("Unprotected system temporary directory");
14326
+ const fd = openSync8(directory, constants7.O_RDONLY | constants7.O_DIRECTORY | constants7.O_NOFOLLOW);
14327
+ try {
14328
+ if (identity4(fstatSync8(fd, { bigint: true })) !== identity4(stat) || !hasNoDarwinAcl(fd) || identity4(fstatSync8(fd, { bigint: true })) !== identity4(stat))
14329
+ throw new Error("Unverified system alias directory ACL");
14330
+ } finally {
14331
+ closeSync8(fd);
14332
+ }
14333
+ return { directory, identity: identity4(stat) };
14334
+ });
14335
+ return realpathSync6(path) === target && readlinkSync4(path) === link && identity4(lstatSync15(path, { bigint: true })) === identity4(before) && directories.every((item) => identity4(lstatSync15(item.directory, { bigint: true })) === item.identity);
14336
+ } catch {
14337
+ return false;
14338
+ }
14339
+ }
14340
+ function canonicalSystemPath(path) {
14341
+ const absolute = resolve10(path), root = `/${absolute.split(sep5)[1]}`;
14342
+ if (process.platform !== "darwin" || !["/var", "/tmp", "/etc"].includes(root) || !lstatSync15(root, { throwIfNoEntry: false })?.isSymbolicLink())
14343
+ return absolute;
14344
+ if (!isSystemRootAlias(root))
14345
+ throw new Error(`Refusing symlink path: ${root}`);
14346
+ return `/private${absolute}`;
14347
+ }
14204
14348
  function assertSafePath(path) {
14205
14349
  let cursor = resolve10(path);
14206
14350
  while (true) {
14207
- if (lstatSync15(cursor, { throwIfNoEntry: false })?.isSymbolicLink())
14351
+ if (lstatSync15(cursor, { throwIfNoEntry: false })?.isSymbolicLink() && !isSystemRootAlias(cursor))
14208
14352
  throw new Error(`Refusing symlink path: ${cursor}`);
14209
14353
  const parent = dirname11(cursor);
14210
14354
  if (parent === cursor)
@@ -14240,7 +14384,7 @@ function rootAliases(home, allowed = false) {
14240
14384
  return result;
14241
14385
  }
14242
14386
  function canonicalAgentPath(path, aliases) {
14243
- const absolute = resolve10(path), binding = aliases.find((item) => absolute === item.alias || absolute.startsWith(item.alias + sep5));
14387
+ const absolute = canonicalSystemPath(path), binding = aliases.find((item) => absolute === item.alias || absolute.startsWith(item.alias + sep5));
14244
14388
  return binding ? join17(binding.target, relative5(binding.alias, absolute)) : absolute;
14245
14389
  }
14246
14390
  function disabledCodexSkillPaths(config, aliases) {
@@ -14338,17 +14482,19 @@ function selectNativeMigrationTargets(inventory, manifest) {
14338
14482
  const identities = new Set;
14339
14483
  const selected = [];
14340
14484
  for (const target of manifest.targets) {
14341
- const projectRoot = resolve10(target.projectRoot), normalized = relative5(projectRoot, resolve10(projectRoot, target.path));
14342
- if (!isAbsolute11(target.projectRoot) || projectRoot !== target.projectRoot || !existsSync8(projectRoot) || !lstatSync15(projectRoot).isDirectory())
14485
+ if (!isAbsolute11(target.projectRoot) || resolve10(target.projectRoot) !== target.projectRoot)
14486
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
14487
+ const projectRoot = canonicalSystemPath(target.projectRoot), normalized = relative5(projectRoot, resolve10(projectRoot, target.path));
14488
+ if (!existsSync8(projectRoot) || !lstatSync15(projectRoot).isDirectory())
14343
14489
  throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
14344
14490
  assertSafePath(projectRoot);
14345
14491
  if (isAbsolute11(target.path) || target.path.includes("\\") || target.path.includes("\x00") || !normalized || normalized === ".." || normalized.startsWith(`..${sep5}`) || normalized !== target.path || !/^[0-9a-f]{64}$/.test(target.treeSha256))
14346
14492
  throw new Error(`Invalid native migration target: ${target.path}`);
14347
- const identity4 = `${target.agent}\x00${projectRoot}\x00${normalized}`;
14493
+ const identity4 = `${target.agent}\x00${canonicalSystemPath(projectRoot)}\x00${normalized}`;
14348
14494
  if (identities.has(identity4))
14349
14495
  throw new Error(`Duplicate native migration target: ${target.path}`);
14350
14496
  identities.add(identity4);
14351
- const expected = resolve10(target.projectRoot, target.path);
14497
+ const expected = canonicalSystemPath(resolve10(target.projectRoot, target.path));
14352
14498
  const matches = inventory.filter((entry2) => entry2.agent === target.agent && resolve10(entry2.path) === expected);
14353
14499
  if (matches.length !== 1)
14354
14500
  throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
@@ -14364,7 +14510,7 @@ function selectNativeMigrationTargets(inventory, manifest) {
14364
14510
  function projectAncestorDirectories(projects) {
14365
14511
  const directories = new Set;
14366
14512
  for (const project of projects) {
14367
- for (let path = resolve10(project), depth = 0;; depth++) {
14513
+ for (let path = canonicalSystemPath(project), depth = 0;; depth++) {
14368
14514
  if (depth >= 100)
14369
14515
  throw new Error("NATIVE_SKILL_DRIFT: project ancestor discovery limit exceeded");
14370
14516
  directories.add(path);
@@ -14377,11 +14523,13 @@ function projectAncestorDirectories(projects) {
14377
14523
  return [...directories];
14378
14524
  }
14379
14525
  function inventoryNativeSkills(home = homedir2(), options = {}) {
14526
+ home = canonicalSystemPath(home);
14380
14527
  if (options.reviewedCacheAlias !== undefined && (!options.includeVendor || !isAbsolute11(options.reviewedCacheAlias) || resolve10(options.reviewedCacheAlias) !== options.reviewedCacheAlias))
14381
14528
  throw new Error("A reviewed cache alias requires vendor inventory and an exact absolute path");
14382
14529
  if (options.disabledVendorPaths?.some((path) => !isAbsolute11(path) || resolve10(path) !== path))
14383
14530
  throw new Error("Disabled vendor paths must be exact absolute paths");
14384
- const disabledVendorPaths = new Set(options.disabledVendorPaths ?? []);
14531
+ const reviewedCacheAlias = options.reviewedCacheAlias === undefined ? undefined : canonicalSystemPath(options.reviewedCacheAlias);
14532
+ const disabledVendorPaths = new Set((options.disabledVendorPaths ?? []).map(canonicalSystemPath));
14385
14533
  const aliases = rootAliases(home, options.allowRootAliases);
14386
14534
  const selectedAgents = options.agents ? new Set(options.agents) : undefined;
14387
14535
  const includesAgent = (agent) => !selectedAgents || selectedAgents.has(agent);
@@ -14404,8 +14552,11 @@ function inventoryNativeSkills(home = homedir2(), options = {}) {
14404
14552
  }
14405
14553
  let reviewedCacheAliasFound = false;
14406
14554
  function verifiedSiblingCacheAlias(agent, path, parent) {
14407
- const link = readlinkSync4(path), target = resolve10(parent, link);
14408
- if (dirname11(target) !== parent || (isAbsolute11(link) ? link !== target : dirname11(link) !== "."))
14555
+ const link = readlinkSync4(path), lexicalTarget = resolve10(parent, link);
14556
+ if (isAbsolute11(link) ? link !== lexicalTarget : dirname11(link) !== ".")
14557
+ return false;
14558
+ const target = canonicalSystemPath(lexicalTarget);
14559
+ if (dirname11(target) !== parent)
14409
14560
  return false;
14410
14561
  if (!lstatSync15(target, { throwIfNoEntry: false })?.isDirectory())
14411
14562
  return false;
@@ -14414,11 +14565,11 @@ function inventoryNativeSkills(home = homedir2(), options = {}) {
14414
14565
  return false;
14415
14566
  if (scan.hasSkills) {
14416
14567
  const targetSkills = entries.filter((entry) => entry.agent === agent && entry.vendor && (entry.path === target || entry.path.startsWith(target + sep5)));
14417
- if (path === options.reviewedCacheAlias && targetSkills.length > 0)
14568
+ if (path === reviewedCacheAlias && targetSkills.length > 0)
14418
14569
  reviewedCacheAliasFound = true;
14419
14570
  if (agent === "codex" && targetSkills.length > 0 && targetSkills.every((entry) => disabledVendorPaths.has(entry.path) || options.disabledVendorSkill?.(entry) === true))
14420
14571
  return true;
14421
- if (path !== options.reviewedCacheAlias)
14572
+ if (path !== reviewedCacheAlias)
14422
14573
  return false;
14423
14574
  }
14424
14575
  return true;
@@ -6,6 +6,7 @@ export interface CodexPluginSkillControl {
6
6
  pluginParent: string;
7
7
  manifestSha256: string;
8
8
  appSha256?: string;
9
+ mcpSha256?: string;
9
10
  remotePluginId?: string;
10
11
  }
11
12
  type Read = (path: string) => string;
package/dist/sdk/index.js CHANGED
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
32373
32373
  // package.json
32374
32374
  var package_default = {
32375
32375
  name: "@hasna/skills",
32376
- version: "0.10.22",
32376
+ version: "0.10.24",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -38347,6 +38347,77 @@ function appControls(path, read) {
38347
38347
  }
38348
38348
  return sha2562;
38349
38349
  }
38350
+ function mcpControls(path, read) {
38351
+ const text = read(path);
38352
+ let value, sha2562;
38353
+ try {
38354
+ sha2562 = hashNativeJsonControls(text);
38355
+ value = JSON.parse(text);
38356
+ } catch {
38357
+ refuse3();
38358
+ }
38359
+ const object3 = (v) => Boolean(v && typeof v === "object" && !Array.isArray(v));
38360
+ const textField = (v, empty = false) => typeof v === "string" && (empty || Boolean(v.trim())) && Buffer.byteLength(v) <= 16384 && !/[\x00-\x1f\x7f]/.test(v);
38361
+ const envName = (v) => typeof v === "string" && /^[A-Za-z_][A-Za-z0-9_]{0,255}$/.test(v);
38362
+ const optional = (v, valid) => v === undefined || v === null || valid(v);
38363
+ const stringList = (v) => Array.isArray(v) && v.length <= 256 && v.every((item) => textField(item));
38364
+ const approval = (v) => ["auto", "prompt", "writes", "approve"].includes(v);
38365
+ const duration = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
38366
+ const positiveInteger = (v) => Number.isSafeInteger(v) && v > 0;
38367
+ const fields = [
38368
+ "command",
38369
+ "args",
38370
+ "cwd",
38371
+ "env",
38372
+ "env_vars",
38373
+ "enabled",
38374
+ "required",
38375
+ "environment_id",
38376
+ "startup_readiness",
38377
+ "supports_parallel_tool_calls",
38378
+ "tool_input_schema_max_bytes",
38379
+ "startup_timeout_sec",
38380
+ "startup_timeout_ms",
38381
+ "tool_timeout_sec",
38382
+ "default_tools_approval_mode",
38383
+ "enabled_tools",
38384
+ "disabled_tools",
38385
+ "omit_tools_from",
38386
+ "tools",
38387
+ "name"
38388
+ ];
38389
+ if (!object3(value) || Object.keys(value).length !== 1 || !object3(value.mcpServers))
38390
+ refuse3();
38391
+ const servers = Object.entries(value.mcpServers);
38392
+ if (!servers.length || servers.length > 256)
38393
+ refuse3();
38394
+ for (const [name, server] of servers) {
38395
+ if (!identifier3(name) || !object3(server) || !Object.keys(server).every((key) => fields.includes(key)) || !textField(server.command) || !optional(server.args, (v) => Array.isArray(v) && v.length <= 256 && v.every((arg) => textField(arg, true))) || !optional(server.cwd, textField) || !optional(server.env, (v) => object3(v) && Object.keys(v).length <= 256 && Object.entries(v).every(([key, value2]) => envName(key) && textField(value2, true))) || !["enabled", "required", "supports_parallel_tool_calls"].every((key) => optional(server[key], (v) => typeof v === "boolean")) || !optional(server.environment_id, (v) => v === "local") || !optional(server.startup_readiness, (v) => ["connection", "catalog"].includes(v)) || !optional(server.tool_input_schema_max_bytes, positiveInteger) || !["startup_timeout_sec", "tool_timeout_sec"].every((key) => optional(server[key], duration)) || !optional(server.startup_timeout_ms, (v) => Number.isSafeInteger(v) && v >= 0) || !optional(server.default_tools_approval_mode, approval) || !["enabled_tools", "disabled_tools"].every((key) => optional(server[key], stringList)) || !optional(server.omit_tools_from, (v) => Array.isArray(v) && v.length <= 256 && v.every((item) => ["code_mode", "deferred", "direct"].includes(item))) || !optional(server.name, textField))
38396
+ refuse3();
38397
+ if (server.env_vars !== undefined && server.env_vars !== null) {
38398
+ if (!Array.isArray(server.env_vars) || server.env_vars.length > 256)
38399
+ refuse3();
38400
+ const names = server.env_vars.map((entry) => {
38401
+ if (envName(entry))
38402
+ return entry;
38403
+ if (!object3(entry) || !Object.keys(entry).every((key) => ["name", "source"].includes(key)) || !envName(entry.name) || !optional(entry.source, (v) => v === "local"))
38404
+ refuse3();
38405
+ return entry.name;
38406
+ });
38407
+ if (new Set(names).size !== names.length)
38408
+ refuse3();
38409
+ }
38410
+ if (server.tools !== undefined && server.tools !== null) {
38411
+ if (!object3(server.tools) || Object.keys(server.tools).length > 256)
38412
+ refuse3();
38413
+ for (const [tool, config] of Object.entries(server.tools)) {
38414
+ if (!textField(tool) || !object3(config) || !Object.keys(config).every((key) => ["approval_mode", "output_token_limit"].includes(key)) || !optional(config.approval_mode, approval) || !optional(config.output_token_limit, positiveInteger))
38415
+ refuse3();
38416
+ }
38417
+ }
38418
+ }
38419
+ return sha2562;
38420
+ }
38350
38421
  function rootControls(root, read) {
38351
38422
  const manifestPath = join10(root, ".codex-plugin/plugin.json"), manifestFile = lstatSync6(manifestPath, { throwIfNoEntry: false });
38352
38423
  if (!lstatSync6(join10(root, ".codex-plugin"), { throwIfNoEntry: false })?.isDirectory() || !manifestFile?.isFile() || manifestFile.size > 1024 * 1024)
@@ -38364,15 +38435,21 @@ function rootControls(root, read) {
38364
38435
  const cacheVersion = basename3(root), declaredVersion = manifest.version === undefined ? "" : typeof manifest.version === "string" ? manifest.version.trim() : null;
38365
38436
  if (declaredVersion === null || (declaredVersion ? declaredVersion !== cacheVersion : cacheVersion !== CODEX_DEFAULT_AGENT_PLUGIN_VERSION))
38366
38437
  refuse3();
38367
- if (manifest.hooks !== undefined || manifest.mcpServers !== undefined || manifest.commands !== undefined || manifest.apps !== undefined && manifest.apps !== "./.app.json" || ["hooks", ".mcp.json"].some((path) => lstatSync6(join10(root, path), { throwIfNoEntry: false })))
38438
+ if (manifest.hooks !== undefined || manifest.commands !== undefined || manifest.mcpServers !== undefined && manifest.mcpServers !== "./.mcp.json" || manifest.apps !== undefined && manifest.apps !== "./.app.json" || lstatSync6(join10(root, "hooks"), { throwIfNoEntry: false }))
38439
+ refuse3();
38440
+ const mcp = join10(root, ".mcp.json"), mcpPresent = lstatSync6(mcp, { throwIfNoEntry: false });
38441
+ if (mcpPresent && (!mcpPresent.isFile() || mcpPresent.isSymbolicLink() || mcpPresent.size > 1024 * 1024 || realpathSync5(mcp) !== mcp))
38442
+ refuse3();
38443
+ if (Boolean(mcpPresent) !== (manifest.mcpServers !== undefined))
38368
38444
  refuse3();
38445
+ const mcpSha256 = mcpPresent ? mcpControls(mcp, read) : undefined;
38369
38446
  const app = join10(root, ".app.json"), present = lstatSync6(app, { throwIfNoEntry: false });
38370
38447
  if (present && (!present.isFile() || present.isSymbolicLink() || present.size > 1024 * 1024))
38371
38448
  refuse3();
38372
38449
  if (manifest.apps !== undefined && !present)
38373
38450
  refuse3();
38374
38451
  const appSha256 = present ? appControls(app, read) : undefined;
38375
- return { namespace: manifest.name, manifestSha256, ...appSha256 ? { appSha256 } : {} };
38452
+ return { namespace: manifest.name, manifestSha256, ...appSha256 ? { appSha256 } : {}, ...mcpSha256 ? { mcpSha256 } : {} };
38376
38453
  }
38377
38454
  function identity(document, cache, read) {
38378
38455
  if (!document.startsWith(cache + sep2) || !document.endsWith(sep2 + "SKILL.md"))
@@ -38435,7 +38512,7 @@ function reviewedCodexPluginCapabilitiesUnchanged(cache, controls, read) {
38435
38512
  if (!stat.isDirectory() || ![".codex-plugin/plugin.json", ".app.json", "hooks", ".mcp.json"].some((path) => lstatSync6(join10(root, path), { throwIfNoEntry: false })))
38436
38513
  continue;
38437
38514
  const parsed = rootControls(root, read);
38438
- if (!controls.some((control) => control.pluginParent === parent && control.namespace === parsed.namespace && control.manifestSha256 === parsed.manifestSha256 && control.appSha256 === parsed.appSha256))
38515
+ if (!controls.some((control) => control.pluginParent === parent && control.namespace === parsed.namespace && control.manifestSha256 === parsed.manifestSha256 && control.appSha256 === parsed.appSha256 && control.mcpSha256 === parsed.mcpSha256))
38439
38516
  return false;
38440
38517
  }
38441
38518
  }
@@ -38495,7 +38572,7 @@ function reviewCodexPluginSkillControls(catalog, documents, cache, cwd, read, ru
38495
38572
  }
38496
38573
  result.push({ ...parsed, pluginId: expectedPluginId, ...remotePluginId ? { remotePluginId } : {} });
38497
38574
  }
38498
- if (!result.length || new Set(result.map((item) => item.name)).size !== result.length)
38575
+ if (!result.length && documents.some((document) => document.startsWith(cache + sep2)) || new Set(result.map((item) => item.name)).size !== result.length)
38499
38576
  refuse3();
38500
38577
  return result;
38501
38578
  }
@@ -38508,7 +38585,7 @@ function isReviewedCodexPluginSkillDisabled(document, cache, controls, rules, re
38508
38585
  } catch {
38509
38586
  return false;
38510
38587
  }
38511
- const reviewed = controls.find((item) => item.name === parsed.name && item.namespace === parsed.namespace && item.pluginParent === parsed.pluginParent && item.manifestSha256 === parsed.manifestSha256 && item.appSha256 === parsed.appSha256);
38588
+ const reviewed = controls.find((item) => item.name === parsed.name && item.namespace === parsed.namespace && item.pluginParent === parsed.pluginParent && item.manifestSha256 === parsed.manifestSha256 && item.appSha256 === parsed.appSha256 && item.mcpSha256 === parsed.mcpSha256);
38512
38589
  if (!reviewed || reviewed.remotePluginId !== undefined && !remoteInstallationMatches(reviewed.pluginParent, reviewed.remotePluginId, read))
38513
38590
  return false;
38514
38591
  return isCodexNativeSkillDisabled({ name: parsed.name, path: document, pluginId: reviewed.pluginId, enabled: true }, rules);
@@ -38860,7 +38937,7 @@ function assertAgentPolicyCollections(policy) {
38860
38937
  }
38861
38938
  if (bridge.codexPluginSkills !== undefined)
38862
38939
  for (const control of array(bridge.codexPluginSkills, 4096)) {
38863
- requireBound(object3(control) && Object.keys(control).every((key) => ["name", "pluginId", "namespace", "pluginParent", "manifestSha256", "appSha256", "remotePluginId"].includes(key)));
38940
+ requireBound(object3(control) && Object.keys(control).every((key) => ["name", "pluginId", "namespace", "pluginParent", "manifestSha256", "appSha256", "mcpSha256", "remotePluginId"].includes(key)));
38864
38941
  text(control.name, 129);
38865
38942
  text(control.pluginId, 1024);
38866
38943
  text(control.namespace, 64);
@@ -38868,6 +38945,8 @@ function assertAgentPolicyCollections(policy) {
38868
38945
  requireBound(isAbsolute6(control.pluginParent) && resolve5(control.pluginParent) === control.pluginParent && /^[a-f0-9]{64}$/.test(control.manifestSha256));
38869
38946
  if (control.remotePluginId !== undefined)
38870
38947
  requireBound(typeof control.remotePluginId === "string" && /^[A-Za-z0-9_~-]{1,1024}$/.test(control.remotePluginId));
38948
+ if (control.mcpSha256 !== undefined)
38949
+ requireBound(typeof control.mcpSha256 === "string" && /^[a-f0-9]{64}$/.test(control.mcpSha256));
38871
38950
  if (control.appSha256 !== undefined)
38872
38951
  requireBound(typeof control.appSha256 === "string" && /^[a-f0-9]{64}$/.test(control.appSha256));
38873
38952
  }
@@ -42096,10 +42175,75 @@ function disableCodexBundledSkills(text2) {
42096
42175
  // src/lib/agent-integration.ts
42097
42176
  var sha4 = (data) => createHash18("sha256").update(data).digest("hex");
42098
42177
  var ROOTS = NATIVE_SKILL_ROOTS;
42178
+ function hasNoDarwinAcl(fd) {
42179
+ try {
42180
+ const { dlopen, read: read3 } = __require("bun:ffi");
42181
+ const library = dlopen("/usr/lib/libSystem.B.dylib", {
42182
+ acl_get_fd: { args: ["i32"], returns: "ptr" },
42183
+ acl_free: { args: ["ptr"], returns: "i32" },
42184
+ __error: { args: [], returns: "ptr" }
42185
+ });
42186
+ try {
42187
+ const errno = library.symbols.__error();
42188
+ if (!errno)
42189
+ return false;
42190
+ const acl = library.symbols.acl_get_fd(fd);
42191
+ if (acl) {
42192
+ library.symbols.acl_free(acl);
42193
+ return false;
42194
+ }
42195
+ return (acl === null || acl === 0) && read3.i32(errno) === 2;
42196
+ } finally {
42197
+ library.close();
42198
+ }
42199
+ } catch {
42200
+ return false;
42201
+ }
42202
+ }
42203
+ function isSystemRootAlias(path) {
42204
+ if (process.platform !== "darwin" || !["/var", "/tmp", "/etc"].includes(path))
42205
+ return false;
42206
+ const target = `/private${path}`, link = `private${path}`;
42207
+ const identity4 = (stat2) => `${stat2.dev}:${stat2.ino}:${stat2.ctimeNs}:${stat2.uid}:${stat2.mode}`;
42208
+ try {
42209
+ const before = lstatSync19(path, { bigint: true });
42210
+ if (!before.isSymbolicLink() || before.uid !== 0n || readlinkSync4(path) !== link)
42211
+ return false;
42212
+ const directories = ["/", "/private", target].map((directory) => {
42213
+ const stat2 = lstatSync19(directory, { bigint: true });
42214
+ if (!stat2.isDirectory() || stat2.uid !== 0n)
42215
+ throw new Error("Untrusted system alias directory");
42216
+ if (directory !== "/private/tmp") {
42217
+ if ((stat2.mode & 0o022n) !== 0n)
42218
+ throw new Error("Writable system alias directory");
42219
+ } else if ((stat2.mode & 0o1000n) === 0n)
42220
+ throw new Error("Unprotected system temporary directory");
42221
+ const fd = openSync16(directory, constants12.O_RDONLY | constants12.O_DIRECTORY | constants12.O_NOFOLLOW);
42222
+ try {
42223
+ if (identity4(fstatSync16(fd, { bigint: true })) !== identity4(stat2) || !hasNoDarwinAcl(fd) || identity4(fstatSync16(fd, { bigint: true })) !== identity4(stat2))
42224
+ throw new Error("Unverified system alias directory ACL");
42225
+ } finally {
42226
+ closeSync16(fd);
42227
+ }
42228
+ return { directory, identity: identity4(stat2) };
42229
+ });
42230
+ return realpathSync7(path) === target && readlinkSync4(path) === link && identity4(lstatSync19(path, { bigint: true })) === identity4(before) && directories.every((item) => identity4(lstatSync19(item.directory, { bigint: true })) === item.identity);
42231
+ } catch {
42232
+ return false;
42233
+ }
42234
+ }
42235
+ function canonicalSystemPath(path) {
42236
+ const absolute = resolve15(path), root2 = `/${absolute.split(sep5)[1]}`;
42237
+ if (process.platform !== "darwin" || !["/var", "/tmp", "/etc"].includes(root2) || !lstatSync19(root2, { throwIfNoEntry: false })?.isSymbolicLink())
42238
+ return absolute;
42239
+ if (!isSystemRootAlias(root2))
42240
+ throw new Error(`Refusing symlink path: ${root2}`);
42241
+ return `/private${absolute}`;
42242
+ }
42099
42243
  function assertSafePath(path) {
42100
42244
  let cursor = resolve15(path);
42101
42245
  while (true) {
42102
- if (lstatSync19(cursor, { throwIfNoEntry: false })?.isSymbolicLink())
42246
+ if (lstatSync19(cursor, { throwIfNoEntry: false })?.isSymbolicLink() && !isSystemRootAlias(cursor))
42103
42247
  throw new Error(`Refusing symlink path: ${cursor}`);
42104
42248
  const parent = dirname15(cursor);
42105
42249
  if (parent === cursor)
@@ -42135,7 +42279,7 @@ function rootAliases(home, allowed = false) {
42135
42279
  return result;
42136
42280
  }
42137
42281
  function canonicalAgentPath(path, aliases) {
42138
- const absolute = resolve15(path), binding = aliases.find((item) => absolute === item.alias || absolute.startsWith(item.alias + sep5));
42282
+ const absolute = canonicalSystemPath(path), binding = aliases.find((item) => absolute === item.alias || absolute.startsWith(item.alias + sep5));
42139
42283
  return binding ? join22(binding.target, relative4(binding.alias, absolute)) : absolute;
42140
42284
  }
42141
42285
  function disabledCodexSkillPaths(config, aliases) {
@@ -42233,17 +42377,19 @@ function selectNativeMigrationTargets(inventory, manifest) {
42233
42377
  const identities = new Set;
42234
42378
  const selected = [];
42235
42379
  for (const target of manifest.targets) {
42236
- const projectRoot = resolve15(target.projectRoot), normalized = relative4(projectRoot, resolve15(projectRoot, target.path));
42237
- if (!isAbsolute17(target.projectRoot) || projectRoot !== target.projectRoot || !existsSync6(projectRoot) || !lstatSync19(projectRoot).isDirectory())
42380
+ if (!isAbsolute17(target.projectRoot) || resolve15(target.projectRoot) !== target.projectRoot)
42381
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
42382
+ const projectRoot = canonicalSystemPath(target.projectRoot), normalized = relative4(projectRoot, resolve15(projectRoot, target.path));
42383
+ if (!existsSync6(projectRoot) || !lstatSync19(projectRoot).isDirectory())
42238
42384
  throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
42239
42385
  assertSafePath(projectRoot);
42240
42386
  if (isAbsolute17(target.path) || target.path.includes("\\") || target.path.includes("\x00") || !normalized || normalized === ".." || normalized.startsWith(`..${sep5}`) || normalized !== target.path || !/^[0-9a-f]{64}$/.test(target.treeSha256))
42241
42387
  throw new Error(`Invalid native migration target: ${target.path}`);
42242
- const identity4 = `${target.agent}\x00${projectRoot}\x00${normalized}`;
42388
+ const identity4 = `${target.agent}\x00${canonicalSystemPath(projectRoot)}\x00${normalized}`;
42243
42389
  if (identities.has(identity4))
42244
42390
  throw new Error(`Duplicate native migration target: ${target.path}`);
42245
42391
  identities.add(identity4);
42246
- const expected = resolve15(target.projectRoot, target.path);
42392
+ const expected = canonicalSystemPath(resolve15(target.projectRoot, target.path));
42247
42393
  const matches = inventory.filter((entry2) => entry2.agent === target.agent && resolve15(entry2.path) === expected);
42248
42394
  if (matches.length !== 1)
42249
42395
  throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
@@ -42259,7 +42405,7 @@ function selectNativeMigrationTargets(inventory, manifest) {
42259
42405
  function projectAncestorDirectories(projects) {
42260
42406
  const directories = new Set;
42261
42407
  for (const project of projects) {
42262
- for (let path = resolve15(project), depth = 0;; depth++) {
42408
+ for (let path = canonicalSystemPath(project), depth = 0;; depth++) {
42263
42409
  if (depth >= 100)
42264
42410
  throw new Error("NATIVE_SKILL_DRIFT: project ancestor discovery limit exceeded");
42265
42411
  directories.add(path);
@@ -42272,11 +42418,13 @@ function projectAncestorDirectories(projects) {
42272
42418
  return [...directories];
42273
42419
  }
42274
42420
  function inventoryNativeSkills(home = homedir3(), options = {}) {
42421
+ home = canonicalSystemPath(home);
42275
42422
  if (options.reviewedCacheAlias !== undefined && (!options.includeVendor || !isAbsolute17(options.reviewedCacheAlias) || resolve15(options.reviewedCacheAlias) !== options.reviewedCacheAlias))
42276
42423
  throw new Error("A reviewed cache alias requires vendor inventory and an exact absolute path");
42277
42424
  if (options.disabledVendorPaths?.some((path) => !isAbsolute17(path) || resolve15(path) !== path))
42278
42425
  throw new Error("Disabled vendor paths must be exact absolute paths");
42279
- const disabledVendorPaths = new Set(options.disabledVendorPaths ?? []);
42426
+ const reviewedCacheAlias = options.reviewedCacheAlias === undefined ? undefined : canonicalSystemPath(options.reviewedCacheAlias);
42427
+ const disabledVendorPaths = new Set((options.disabledVendorPaths ?? []).map(canonicalSystemPath));
42280
42428
  const aliases = rootAliases(home, options.allowRootAliases);
42281
42429
  const selectedAgents = options.agents ? new Set(options.agents) : undefined;
42282
42430
  const includesAgent = (agent) => !selectedAgents || selectedAgents.has(agent);
@@ -42299,8 +42447,11 @@ function inventoryNativeSkills(home = homedir3(), options = {}) {
42299
42447
  }
42300
42448
  let reviewedCacheAliasFound = false;
42301
42449
  function verifiedSiblingCacheAlias(agent, path, parent) {
42302
- const link = readlinkSync4(path), target = resolve15(parent, link);
42303
- if (dirname15(target) !== parent || (isAbsolute17(link) ? link !== target : dirname15(link) !== "."))
42450
+ const link = readlinkSync4(path), lexicalTarget = resolve15(parent, link);
42451
+ if (isAbsolute17(link) ? link !== lexicalTarget : dirname15(link) !== ".")
42452
+ return false;
42453
+ const target = canonicalSystemPath(lexicalTarget);
42454
+ if (dirname15(target) !== parent)
42304
42455
  return false;
42305
42456
  if (!lstatSync19(target, { throwIfNoEntry: false })?.isDirectory())
42306
42457
  return false;
@@ -42309,11 +42460,11 @@ function inventoryNativeSkills(home = homedir3(), options = {}) {
42309
42460
  return false;
42310
42461
  if (scan.hasSkills) {
42311
42462
  const targetSkills = entries.filter((entry) => entry.agent === agent && entry.vendor && (entry.path === target || entry.path.startsWith(target + sep5)));
42312
- if (path === options.reviewedCacheAlias && targetSkills.length > 0)
42463
+ if (path === reviewedCacheAlias && targetSkills.length > 0)
42313
42464
  reviewedCacheAliasFound = true;
42314
42465
  if (agent === "codex" && targetSkills.length > 0 && targetSkills.every((entry) => disabledVendorPaths.has(entry.path) || options.disabledVendorSkill?.(entry) === true))
42315
42466
  return true;
42316
- if (path !== options.reviewedCacheAlias)
42467
+ if (path !== reviewedCacheAlias)
42317
42468
  return false;
42318
42469
  }
42319
42470
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.22",
3
+ "version": "0.10.24",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {