@hasna/skills 0.10.91 → 0.10.93

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/dist/sdk/index.js CHANGED
@@ -91998,7 +91998,7 @@ var init_event_sink = __esm(() => {
91998
91998
  init_dist();
91999
91999
  });
92000
92000
  // package.json
92001
- var version = "0.10.91";
92001
+ var version = "0.10.93";
92002
92002
  var package_default = {
92003
92003
  name: "@hasna/skills",
92004
92004
  version,
@@ -102865,6 +102865,234 @@ if (failed) process.exitCode = 2;
102865
102865
  `;
102866
102866
  }
102867
102867
 
102868
+ // src/lib/agent-opencode.ts
102869
+ function renderOpenCodePlugin(command, profile) {
102870
+ return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
102871
+ import { spawn } from "node:child_process";
102872
+ import { createHash } from "node:crypto";
102873
+ const command = ${JSON.stringify(command)};
102874
+ const profile = ${JSON.stringify(profile)};
102875
+ const bridgeBodySha256 = ${JSON.stringify(OPENCODE_BRIDGE_BODY_SHA256)};
102876
+ const bridgeDescriptionSha256 = ${JSON.stringify(OPENCODE_BRIDGE_DESCRIPTION_SHA256)};
102877
+ function digest(text) { return createHash("sha256").update(text).digest("hex"); }
102878
+ function canonicalSkill(skill) {
102879
+ return skill?.id === "skills-cli" && skill.name === "skills-cli" && typeof skill.path === "string"
102880
+ && typeof skill.content === "string" && digest(skill.content) === bridgeBodySha256
102881
+ && typeof skill.description === "string" && digest(skill.description) === bridgeDescriptionSha256
102882
+ && (skill.autoinvoke === undefined || skill.autoinvoke === false);
102883
+ }
102884
+ function refusal() {
102885
+ const error = new Error("Skills verification blocked this request. Review the OpenCode Skills hook configuration, then retry.");
102886
+ error.name = "SkillsHookRefusal";
102887
+ error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED" };
102888
+ return error;
102889
+ }
102890
+ export default {
102891
+ id: "local.hasna-skills-cli",
102892
+ async setup(ctx) {
102893
+ if (!ctx?.session?.hook || !ctx?.session?.get || !ctx?.skill?.transform || !ctx?.skill?.list || !ctx?.tool?.hook) throw refusal();
102894
+ const registrations = [], pending = new Set(), sessions = new Set(), prompts = new Map();
102895
+ let disposed = false;
102896
+ function context(input) {
102897
+ if (disposed) return Promise.reject(refusal());
102898
+ return new Promise((resolve, reject) => {
102899
+ // Availability failures may omit optional model context. Native skill
102900
+ // consumption and explicit policy refusals always remain fail-closed.
102901
+ const optional = input.delivery_phase === "context";
102902
+ const chunks = []; let bytes = 0, settled = false, child, timer;
102903
+ const finish = () => { clearTimeout(timer); pending.delete(cancel); };
102904
+ const deny = () => { if (settled) return; settled = true; finish(); reject(refusal()); };
102905
+ const unavailable = code => {
102906
+ if (settled) return;
102907
+ if (!optional || disposed) return deny();
102908
+ settled = true; finish();
102909
+ 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." });
102910
+ };
102911
+ const stop = code => { unavailable(code); child?.kill("SIGKILL"); };
102912
+ const cancel = () => { deny(); child?.kill("SIGKILL"); };
102913
+ try {
102914
+ child = spawn(command, ["hook", "user-prompt", "--agent", "opencode", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
102915
+ } catch { unavailable("SPAWN_FAILED"); return; }
102916
+ pending.add(cancel);
102917
+ timer = setTimeout(() => stop("TIMEOUT"), 15000);
102918
+ child.stdout.on("data", chunk => { if (settled) return; bytes += chunk.length; if (bytes > 1048576) stop("OUTPUT_LIMIT"); else chunks.push(chunk); });
102919
+ child.stdout.on("error", () => stop("TRANSPORT_FAILED"));
102920
+ child.stderr.on("data", () => {});
102921
+ child.stderr.on("error", () => stop("TRANSPORT_FAILED"));
102922
+ child.stdin.on("error", () => stop("TRANSPORT_FAILED"));
102923
+ child.on("error", () => unavailable("SPAWN_FAILED"));
102924
+ child.on("close", code => {
102925
+ if (settled) return;
102926
+ if (disposed) return deny();
102927
+ if (code !== 0) return unavailable("CHILD_FAILED");
102928
+ let result;
102929
+ try {
102930
+ result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
102931
+ } catch { unavailable("INVALID_RESPONSE"); return; }
102932
+ if (!result || typeof result !== "object" || Array.isArray(result)) return unavailable("INVALID_RESPONSE");
102933
+ if (result.decision === "block" || result.continue === false) return deny();
102934
+ // hookContextOutput returns exactly {} for a successful empty
102935
+ // context. It never attests native bridge verification.
102936
+ const output = optional && Object.keys(result).length === 0
102937
+ ? { hookEventName: input.hook_event_name, additionalContext: "" }
102938
+ : result.hookSpecificOutput;
102939
+ if (!output || typeof output !== "object" || Array.isArray(output) || output.hookEventName !== input.hook_event_name || typeof output.additionalContext !== "string") return unavailable("INVALID_RESPONSE");
102940
+ if (input.hook_event_name === "BeforeTool" && result.nativeBridgeVerified !== true) return deny();
102941
+ settled = true; finish();
102942
+ resolve({ delivered: true, text: output.additionalContext });
102943
+ });
102944
+ try { child.stdin.end(JSON.stringify(input)); } catch { stop("TRANSPORT_FAILED"); }
102945
+ });
102946
+ }
102947
+ async function input(sessionID, prompt, event) {
102948
+ if (disposed) throw refusal();
102949
+ let session;
102950
+ try { session = await ctx.session.get({ sessionID }); } catch { throw refusal(); }
102951
+ if (disposed || !session || session.id !== sessionID || typeof session.location?.directory !== "string" || (session.parentID !== undefined && typeof session.parentID !== "string") || session.parentID === sessionID) throw refusal();
102952
+ 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 };
102953
+ }
102954
+ async function dispose() {
102955
+ if (disposed) return;
102956
+ disposed = true;
102957
+ for (const cancel of pending) cancel();
102958
+ sessions.clear();
102959
+ prompts.clear();
102960
+ const results = await Promise.allSettled(registrations.reverse().map(registration => Promise.resolve().then(() => registration.dispose())));
102961
+ if (results.some(result => result.status === "rejected")) throw refusal();
102962
+ }
102963
+ try {
102964
+ registrations.push(await ctx.skill.transform(editor => {
102965
+ for (const skill of editor.list()) if (!canonicalSkill(skill)) editor.remove(skill.id);
102966
+ }));
102967
+ async function verifyNativeBridge(sessionID) {
102968
+ let skills;
102969
+ try { skills = await ctx.skill.list(); } catch { throw refusal(); }
102970
+ if (!Array.isArray(skills)) throw refusal();
102971
+ const matches = skills.filter(skill => skill?.id === "skills-cli");
102972
+ if (matches.length !== 1) throw refusal();
102973
+ const skill = matches[0];
102974
+ if (!canonicalSkill(skill)) throw refusal();
102975
+ const metadata = { id: skill.id, name: skill.name, path: skill.path, contentSha256: digest(skill.content), descriptionSha256: digest(skill.description) };
102976
+ await context({ ...await input(sessionID, "", "BeforeTool"), tool_input: { id: "skills-cli" }, native_skill: metadata });
102977
+ }
102978
+ registrations.push(await ctx.tool.hook("execute.before", async event => {
102979
+ if (disposed) throw refusal();
102980
+ if (event.tool !== "skill") return;
102981
+ if (event.input?.id !== "skills-cli") throw refusal();
102982
+ await verifyNativeBridge(event.sessionID);
102983
+ }));
102984
+ registrations.push(await ctx.session.hook("prompt", async event => {
102985
+ if (disposed || typeof event.prompt?.text !== "string" || event.prompt.text.length > 128 * 1024) throw refusal();
102986
+ if (event.prompt.skills !== undefined && (!Array.isArray(event.prompt.skills) || event.prompt.skills.some(skill => skill?.id !== "skills-cli"))) throw refusal();
102987
+ // Attachments materialize immediately after this hook. Check the actual
102988
+ // current registry object and on-disk bridge before native consumption.
102989
+ if (event.prompt.skills?.length) await verifyNativeBridge(event.sessionID);
102990
+ if (disposed) throw refusal();
102991
+ const queued = prompts.get(event.sessionID) ?? [];
102992
+ if (queued.reduce((size, entry) => size + entry.text.length + 1, 0) + event.prompt.text.length > 128 * 1024) throw refusal();
102993
+ queued.push({ text: event.prompt.text });
102994
+ prompts.set(event.sessionID, queued);
102995
+ }));
102996
+ registrations.push(await ctx.session.hook("context", async event => {
102997
+ // Only prompt-hook input has owner-prompt provenance. Keep it transient
102998
+ // until the one delivery-bearing CLI call, including an empty prompt.
102999
+ // Checkpoints, synthetic user-role history and resumes never match.
103000
+ const queued = [...(prompts.get(event.sessionID) ?? [])];
103001
+ const result = await context(await input(event.sessionID, queued.map(entry => entry.text).join("\\n")));
103002
+ if (disposed) throw refusal();
103003
+ if (result.text) event.system.push({ type: "text", text: result.text });
103004
+ // Remove only the prompt events this request consumed. New admission
103005
+ // while the CLI was running belongs to the next model context.
103006
+ const current = prompts.get(event.sessionID);
103007
+ if (current) {
103008
+ const remaining = current.filter(entry => !queued.includes(entry));
103009
+ if (remaining.length) prompts.set(event.sessionID, remaining);
103010
+ else prompts.delete(event.sessionID);
103011
+ }
103012
+ // Retry SessionStart on the next context after an unavailable child,
103013
+ // but never replay prompt text already admitted to this model turn.
103014
+ if (result.delivered) sessions.add(event.sessionID);
103015
+ }));
103016
+ return dispose;
103017
+ } catch {
103018
+ await dispose();
103019
+ throw refusal();
103020
+ }
103021
+ },
103022
+ };
103023
+ `;
103024
+ }
103025
+
103026
+ // src/lib/agent-adapters.ts
103027
+ var INTEGRATION_AGENTS = ["claude", "codex", "gemini", "opencode", "cursor", "hermes", "sumi"];
103028
+ function renderAgentHookCommand(command, agent, profileId, event) {
103029
+ if (typeof profileId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profileId) || profileId.includes(".."))
103030
+ throw new Error("Invalid selection profile id");
103031
+ return `'${command.replace(/'/g, `'\\''`)}' hook user-prompt --agent ${agent} --selection-profile ${profileId} --event ${event}`;
103032
+ }
103033
+ var AGENT_ADAPTERS = {
103034
+ claude: { root: ".claude/skills", config: ".claude/settings.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
103035
+ codex: { root: ".codex/skills", config: ".codex/hooks.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
103036
+ gemini: { root: ".gemini/skills", config: ".gemini/settings.json", events: ["BeforeAgent", "SessionStart"], promptContext: true },
103037
+ opencode: { root: ".config/opencode/skills", config: ".config/opencode/opencode.json", events: ["prompt", "context"], promptContext: true },
103038
+ sumi: { root: null, config: null, events: ["prompt", "context"], promptContext: true },
103039
+ 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 },
103040
+ cursor: { root: ".cursor/skills", config: ".cursor/hooks.json", events: ["beforeSubmitPrompt", "sessionStart"], promptContext: false }
103041
+ };
103042
+ function renderLegacyOpenCodePlugin(command, profileId) {
103043
+ return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
103044
+ import { spawn } from "node:child_process";
103045
+ import { randomUUID } from "node:crypto";
103046
+ const command = ${JSON.stringify(command)};
103047
+ const profile = ${JSON.stringify(profileId)};
103048
+ function context(input) {
103049
+ return new Promise((resolve, reject) => {
103050
+ const child = spawn(command, ["hook", "user-prompt", "--agent", "opencode", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
103051
+ const chunks = []; let bytes = 0, failure;
103052
+ const timer = setTimeout(() => { failure = new Error("Skills prompt hook timed out"); child.kill("SIGKILL"); }, 15000);
103053
+ child.stdout.on("data", chunk => { bytes += chunk.length; if (bytes > 1048576) { failure = new Error("Skills prompt hook output exceeded its limit"); child.kill("SIGKILL"); } else chunks.push(chunk); });
103054
+ child.stderr.on("data", () => {});
103055
+ child.stdin.on("error", () => {});
103056
+ child.on("error", error => { clearTimeout(timer); reject(error); });
103057
+ child.on("close", code => {
103058
+ clearTimeout(timer);
103059
+ if (failure || code !== 0) return reject(failure ?? new Error("Skills prompt hook failed"));
103060
+ try {
103061
+ const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
103062
+ if (!result || typeof result !== "object" || Array.isArray(result)) throw new Error("Invalid Skills hook response");
103063
+ if (result.decision === "block" || result.continue === false) throw new Error(result.reason ?? result.stopReason ?? "Skills prompt hook refused");
103064
+ if (Object.keys(result).length && (!result.hookSpecificOutput || result.hookSpecificOutput.hookEventName !== input.hook_event_name)) throw new Error("Invalid Skills hook response");
103065
+ const text = result.hookSpecificOutput?.additionalContext ?? "";
103066
+ if (typeof text !== "string") throw new Error("Invalid Skills prompt context");
103067
+ resolve(text);
103068
+ } catch (error) { reject(error); }
103069
+ });
103070
+ child.stdin.end(JSON.stringify(input));
103071
+ });
103072
+ }
103073
+ export default async ({ directory }) => {
103074
+ const sessions = new Set();
103075
+ return {
103076
+ "chat.message": async (input, output) => {
103077
+ const parts = output.parts.filter(part => part.type === "text" && typeof part.text === "string");
103078
+ const first = !sessions.has(input.sessionID);
103079
+ const text = await context({ hook_event_name: first ? "SessionStart" : "UserPromptSubmit", cwd: directory, session_id: input.sessionID, prompt: parts.map(part => part.text).join("\\n") });
103080
+ if (text && parts.length) parts[0].text += "\\n\\n" + text;
103081
+ else if (text) output.parts.push({ id: "prt_" + randomUUID().replaceAll("-", ""), sessionID: input.sessionID, messageID: output.message.id, type: "text", text, synthetic: true });
103082
+ if (sessions.size >= 1000) sessions.clear();
103083
+ sessions.add(input.sessionID);
103084
+ },
103085
+ "tool.execute.before": async (input, output) => {
103086
+ if (input.tool === "skill" && output.args?.name !== "skills-cli") throw new Error("Load instructions through the Skills CLI bridge");
103087
+ },
103088
+ };
103089
+ };
103090
+ `;
103091
+ }
103092
+ function isManagedOpenCodePlugin(text2, command, profileId) {
103093
+ return text2 === renderOpenCodePlugin(command, profileId) || text2 === renderLegacyOpenCodePlugin(command, profileId);
103094
+ }
103095
+
102868
103096
  // src/lib/plugin-projection-store.ts
102869
103097
  import { createHash as createHash24, randomUUID as randomUUID3 } from "crypto";
102870
103098
  import { constants as constants15, closeSync as closeSync17, fchmodSync, fstatSync as fstatSync17, fsyncSync as fsyncSync3, linkSync as linkSync2, lstatSync as lstatSync22, mkdirSync as mkdirSync6, openSync as openSync17, opendirSync as opendirSync3, readSync as readSync11, renameSync as renameSync4, rmdirSync as rmdirSync2, rmSync as rmSync2, unlinkSync as unlinkSync5, writeSync as writeSync2 } from "fs";
@@ -105431,6 +105659,19 @@ function discoveryRootUnresolved(agent, command, detail) {
105431
105659
  function isDiscoveryRootUnresolved(error) {
105432
105660
  return error instanceof Error && error.message.startsWith(`${DISCOVERY_ROOT_UNRESOLVED}: `);
105433
105661
  }
105662
+
105663
+ class NativeDiscoveryUnresolved extends Error {
105664
+ agent;
105665
+ detail;
105666
+ source;
105667
+ reviewable = false;
105668
+ constructor(agent, detail, source) {
105669
+ super(`Native discovery is unresolved (${agent}: ${detail}); provide a reviewed --discovery-inputs file`);
105670
+ this.agent = agent;
105671
+ this.detail = detail;
105672
+ this.source = source;
105673
+ }
105674
+ }
105434
105675
  var quotedPath = (path) => JSON.stringify(path).replace(/[^\x20-\x7e]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
105435
105676
  var realpathOrNull = (path) => {
105436
105677
  try {
@@ -106400,9 +106641,96 @@ function nativeDiscoveryAliasTargets(roots, home) {
106400
106641
  }
106401
106642
  return [...targets];
106402
106643
  }
106644
+ function hooksAreManagedBridge(agent, hooks, managed) {
106645
+ const table = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
106646
+ const exactKeys4 = (value, keys3) => Object.keys(value).sort().join("\x00") === [...keys3].sort().join("\x00");
106647
+ if (!table(hooks) || typeof managed.command !== "string" || !isAbsolute26(managed.command))
106648
+ return false;
106649
+ const events3 = agent === "gemini" ? ["BeforeAgent", "SessionStart", "BeforeTool"] : ["beforeSubmitPrompt", "sessionStart"];
106650
+ for (const [event, entries] of Object.entries(hooks)) {
106651
+ if (!events3.includes(event) || !Array.isArray(entries) || entries.length !== 1)
106652
+ return false;
106653
+ let expected;
106654
+ try {
106655
+ expected = renderAgentHookCommand(managed.command, agent, managed.profile, event);
106656
+ } catch {
106657
+ return false;
106658
+ }
106659
+ const entry = entries[0];
106660
+ if (!table(entry))
106661
+ return false;
106662
+ if (agent === "cursor") {
106663
+ if (!exactKeys4(entry, ["command", "timeout"]) || entry.timeout !== 15 || entry.command !== expected)
106664
+ return false;
106665
+ continue;
106666
+ }
106667
+ if (!exactKeys4(entry, event === "BeforeTool" ? ["hooks", "matcher"] : ["hooks"]) || event === "BeforeTool" && entry.matcher !== "activate_skill" || !Array.isArray(entry.hooks) || entry.hooks.length !== 1)
106668
+ return false;
106669
+ const hook = entry.hooks[0];
106670
+ if (!table(hook) || !exactKeys4(hook, ["command", "timeout", "type"]) || hook.type !== "command" || hook.timeout !== 15000 || hook.command !== expected)
106671
+ return false;
106672
+ }
106673
+ return true;
106674
+ }
106675
+ function sumiPluginLoadsOnlyBuiltins(text2) {
106676
+ for (const match of text2.matchAll(/\b(?:import|require)\s*\(\s*(.?)/g)) {
106677
+ const rest = text2.slice(match.index + match[0].length - match[1].length);
106678
+ if (!/^(['"])node:[A-Za-z0-9_/.-]+\1/.test(rest))
106679
+ return false;
106680
+ }
106681
+ for (const match of text2.matchAll(/\b(?:from|import)\s*(['"])([^'"]*)\1/g))
106682
+ if (!match[2].startsWith("node:"))
106683
+ return false;
106684
+ return true;
106685
+ }
106686
+ var CODEX_MARKETPLACE_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
106687
+ function codexRootFreeControls(config) {
106688
+ const table = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date));
106689
+ const adapted = [];
106690
+ const servers = config.mcp_servers;
106691
+ if (table(servers) && Object.keys(servers).length <= AGENT_POLICY_LIMITS.discoveryRoots && Object.entries(servers).every(([name, row]) => name !== "codex_apps" && name !== "_codex_apps" && isCodexLocalStdioMcpServer(name, row)))
106692
+ adapted.push("mcp_servers");
106693
+ const agents = config.agents;
106694
+ if (table(agents) && Object.keys(agents).length <= AGENT_POLICY_LIMITS.fields && Object.values(agents).every((value) => typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)))
106695
+ adapted.push("agents");
106696
+ return adapted;
106697
+ }
106698
+ var CODEX_HOOK_EVENTS = new Set(["PreToolUse", "PermissionRequest", "PostToolUse", "PreCompact", "PostCompact", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart", "SubagentStop", "Stop", "Interrupt"]);
106699
+ function codexPluginHookHandlers(value) {
106700
+ const table = (item) => Boolean(item && typeof item === "object" && !Array.isArray(item));
106701
+ const identifier7 = (item) => typeof item === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(item);
106702
+ const text2 = (item) => typeof item === "string" && item.length <= AGENT_POLICY_LIMITS.pathCharacters && !item.includes("\x00");
106703
+ if (!table(value) || !Object.keys(value).every((key2) => key2 === "hooks" || key2 === "description") || value.description !== undefined && !text2(value.description) || !table(value.hooks) || Object.keys(value.hooks).length > 32)
106704
+ return null;
106705
+ const result = { commands: 0, servers: new Set };
106706
+ for (const [event, groups] of Object.entries(value.hooks)) {
106707
+ if (!CODEX_HOOK_EVENTS.has(event) || !Array.isArray(groups) || groups.length > 32)
106708
+ return null;
106709
+ for (const group of groups) {
106710
+ if (!table(group) || !Object.keys(group).every((key2) => key2 === "hooks" || key2 === "matcher") || group.matcher !== undefined && !text2(group.matcher) || !Array.isArray(group.hooks) || group.hooks.length > 32)
106711
+ return null;
106712
+ for (const handler of group.hooks) {
106713
+ if (!table(handler) || handler.timeout !== undefined && !(Number.isSafeInteger(handler.timeout) && handler.timeout >= 0) || handler.statusMessage !== undefined && !text2(handler.statusMessage))
106714
+ return null;
106715
+ if (handler.type === "command") {
106716
+ if (!text2(handler.command) || !handler.command || !Object.keys(handler).every((key2) => ["type", "command", "commandWindows", "command_windows", "timeout", "async", "statusMessage", "additionalContextLimit"].includes(key2)))
106717
+ return null;
106718
+ result.commands++;
106719
+ } else if (handler.type === "mcp_tool") {
106720
+ if (!identifier7(handler.server) || !identifier7(handler.tool) || handler.input !== undefined && !table(handler.input) || !Object.keys(handler).every((key2) => ["type", "server", "tool", "input", "timeout", "statusMessage"].includes(key2)))
106721
+ return null;
106722
+ result.servers.add(handler.server);
106723
+ } else
106724
+ return null;
106725
+ }
106726
+ }
106727
+ }
106728
+ return result;
106729
+ }
106403
106730
  function resolveDiscovery(options, exclusion) {
106404
106731
  const canonical9 = options.canonical ?? resolve27, home = resolve27(options.home), agent = options.agent;
106405
106732
  const sources = [], roots = new Set, builtinNames = [];
106733
+ const sumiLocalPluginTexts = new Map;
106406
106734
  const budget3 = discoveryByteBudget();
106407
106735
  let remainingEntries = AGENT_POLICY_LIMITS.discoveryDirectoryEntries;
106408
106736
  const entries = (path) => {
@@ -106439,8 +106767,8 @@ function resolveDiscovery(options, exclusion) {
106439
106767
  const configPath = exclusion?.nativeHome === undefined ? agentDiscoveryConfigPath(home, agent) : join34(nativeHome, agent === "codex" ? "config.toml" : "settings.json");
106440
106768
  const configText = witness(configPath, agent === "hermes" ? "yaml" : agent === "codex" ? "toml" : "json", agent === "hermes" ? ["skills", "plugins", "hooks"] : agent === "claude" ? ["enabledPlugins", "extraKnownMarketplaces"] : agent === "codex" ? [...CODEX_DISCOVERY_PROJECTION_FIELDS] : agent === "gemini" ? ["skills", "extensions", "security"] : agent === "opencode" ? ["plugin", "skills"] : agent === "sumi" ? [...SUMI_DISCOVERY_PROJECTION_FIELDS] : ["version"]);
106441
106769
  const config = configText === null ? {} : agent === "hermes" ? parseHermesConfig(configText) : parseConfig(configText, configPath, agent === "codex");
106442
- const unresolved = (detail) => {
106443
- throw new Error(`Native discovery is unresolved (${agent}: ${detail}); provide a reviewed --discovery-inputs file`);
106770
+ const unresolved = (detail, source) => {
106771
+ throw new NativeDiscoveryUnresolved(agent, detail, source);
106444
106772
  };
106445
106773
  let opaqueClosureVerified = false;
106446
106774
  const verifyOpaqueClosure = (declarations) => {
@@ -106454,6 +106782,17 @@ function resolveDiscovery(options, exclusion) {
106454
106782
  verifyDiscovery(retained, undefined, undefined, undefined, exclusion?.narrowed, canonical9(configPath));
106455
106783
  opaqueClosureVerified = true;
106456
106784
  };
106785
+ const reviewRemedy = (source, check) => {
106786
+ try {
106787
+ check();
106788
+ } catch (error) {
106789
+ if (error instanceof NativeDiscoveryUnresolved) {
106790
+ error.reviewable = true;
106791
+ error.source ??= source;
106792
+ }
106793
+ throw error;
106794
+ }
106795
+ };
106457
106796
  if (exclusion) {
106458
106797
  if (configText !== null && agent !== "codex" && agent !== "hermes")
106459
106798
  parseNativeJsonObject(configText);
@@ -106479,12 +106818,38 @@ function resolveDiscovery(options, exclusion) {
106479
106818
  const controls = agent === "claude" ? claudeDiscoveryControlKeys(configText ?? "{}") : agent === "codex" ? codexDiscoveryControlKeys(config) : Object.keys(config).filter((key2) => /skill|plugin|hook|marketplace|extension|instruction/i.test(key2));
106480
106819
  const fieldKey = (key2) => /^[A-Za-z_$][A-Za-z0-9_$.-]{0,22}$/.test(key2) && !/(?:sk|rk|pk)_(?:live|test)_|whsec_/i.test(key2) ? JSON.stringify(key2) : "[withheld field key]";
106481
106820
  const unsupportedFields = (names, label) => unresolved(`unknown discovery controls have no root-exclusion adapter for ${label}${names.slice(0, 8).map(fieldKey).join(", ")}${names.length > 8 ? ", additional keys withheld" : ""} in ${quotedPath(canonical9(configPath))}`);
106482
- const unsupportedControls = controls.filter((key2) => !supported[agent].includes(key2));
106821
+ const adapted = agent === "codex" ? codexRootFreeControls(config) : agent === "opencode" && Array.isArray(config.instructions) && config.instructions.length === 0 ? ["instructions"] : [];
106822
+ const unsupportedControls = controls.filter((key2) => !supported[agent].includes(key2) && !adapted.includes(key2));
106483
106823
  if (unsupportedControls.length)
106484
106824
  unsupportedFields(unsupportedControls, "");
106485
106825
  if (config.hooks !== undefined && !object6(config.hooks))
106486
106826
  unresolved("hooks must be an object");
106487
- const hasHooks = controls.includes("hooks") && config.hooks !== undefined && Object.keys(config.hooks).length > 0;
106827
+ const managedSupervisorOnly = () => {
106828
+ const supervisor = exclusion.managedHook?.supervisor;
106829
+ if (agent !== "hermes" || !supervisor || typeof supervisor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(supervisor.sha256) || !object6(config.hooks))
106830
+ return false;
106831
+ let expected;
106832
+ try {
106833
+ expected = hermesHookDefinitions(supervisor);
106834
+ } catch {
106835
+ return false;
106836
+ }
106837
+ for (const [event, entries2] of Object.entries(config.hooks)) {
106838
+ const want = expected[event];
106839
+ if (!want || !Array.isArray(entries2) || entries2.length !== 1 || !object6(entries2[0]))
106840
+ return false;
106841
+ const entry = entries2[0];
106842
+ if (Object.keys(entry).sort().join("\x00") !== Object.keys(want).sort().join("\x00") || Object.keys(want).some((key2) => entry[key2] !== want[key2]))
106843
+ return false;
106844
+ }
106845
+ try {
106846
+ return hashRawDiscoveryFile(canonical9(supervisor.path), budget3) === supervisor.sha256;
106847
+ } catch {
106848
+ return false;
106849
+ }
106850
+ };
106851
+ const managedBridgeOnly = (agent === "gemini" || agent === "cursor") && exclusion.managedHook !== undefined && hooksAreManagedBridge(agent, config.hooks, exclusion.managedHook) || managedSupervisorOnly();
106852
+ const hasHooks = controls.includes("hooks") && config.hooks !== undefined && Object.keys(config.hooks).length > 0 && !managedBridgeOnly;
106488
106853
  let hasPluginConfigs = false, hasFunctionHookEnvironment = false;
106489
106854
  const verifyCurrentOpaqueSettings = (label) => {
106490
106855
  const retained = exclusion.retained;
@@ -106499,7 +106864,7 @@ function resolveDiscovery(options, exclusion) {
106499
106864
  unresolved(`non-enumerable ${label} require a current settings review`);
106500
106865
  };
106501
106866
  if (hasHooks)
106502
- verifyCurrentOpaqueSettings("hooks");
106867
+ reviewRemedy(canonical9(configPath), () => verifyCurrentOpaqueSettings("hooks"));
106503
106868
  if (agent === "claude") {
106504
106869
  if (config.env !== undefined) {
106505
106870
  if (!object6(config.env) || Object.keys(config.env).length > 512)
@@ -106512,7 +106877,7 @@ function resolveDiscovery(options, exclusion) {
106512
106877
  if (hasFunctionHookEnvironment) {
106513
106878
  if (!["0", "1"].includes(config.env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS))
106514
106879
  unresolved("malformed Claude env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS flag");
106515
- verifyCurrentOpaqueSettings("function-hook environment controls");
106880
+ reviewRemedy(canonical9(configPath), () => verifyCurrentOpaqueSettings("function-hook environment controls"));
106516
106881
  }
106517
106882
  }
106518
106883
  if (config.pluginConfigs !== undefined) {
@@ -106539,7 +106904,7 @@ function resolveDiscovery(options, exclusion) {
106539
106904
  }
106540
106905
  hasPluginConfigs = Object.keys(config.pluginConfigs).length > 0;
106541
106906
  if (hasPluginConfigs)
106542
- verifyCurrentOpaqueSettings("plugin configurations");
106907
+ reviewRemedy(canonical9(configPath), () => verifyCurrentOpaqueSettings("plugin configurations"));
106543
106908
  }
106544
106909
  if (config.enabledPlugins !== undefined && !object6(config.enabledPlugins))
106545
106910
  unresolved("enabledPlugins must be an object");
@@ -106589,7 +106954,9 @@ function resolveDiscovery(options, exclusion) {
106589
106954
  unresolved("malformed Sumi plugins");
106590
106955
  }
106591
106956
  if (agent === "hermes" && config.skills !== undefined) {
106592
- keys3(config.skills, ["external_dirs"], "Hermes skills");
106957
+ keys3(config.skills, ["external_dirs", "creation_nudge_interval"], "Hermes skills");
106958
+ if (config.skills.creation_nudge_interval !== undefined && !(Number.isSafeInteger(config.skills.creation_nudge_interval) && config.skills.creation_nudge_interval >= 0))
106959
+ unresolved("malformed Hermes skills creation_nudge_interval");
106593
106960
  if (config.skills.external_dirs !== undefined)
106594
106961
  paths(typeof config.skills.external_dirs === "string" ? [config.skills.external_dirs] : config.skills.external_dirs, "Hermes external directories");
106595
106962
  }
@@ -106609,10 +106976,21 @@ function resolveDiscovery(options, exclusion) {
106609
106976
  unresolved(`malformed Codex ${key2}`);
106610
106977
  for (const value of Object.values(config.plugins ?? {}))
106611
106978
  keys3(value, ["enabled"], "Codex plugin registration");
106612
- for (const value of Object.values(config.marketplaces ?? {}))
106613
- keys3(value, ["source_type", "source"], "Codex marketplace registration");
106979
+ for (const value of Object.values(config.marketplaces ?? {})) {
106980
+ keys3(value, ["source_type", "source", "last_updated"], "Codex marketplace registration");
106981
+ if (value.last_updated !== undefined && (typeof value.last_updated !== "string" || !CODEX_MARKETPLACE_TIMESTAMP.test(value.last_updated)))
106982
+ unresolved("malformed Codex marketplace refresh time");
106983
+ }
106614
106984
  if (config.skills !== undefined)
106615
- keys3(config.skills, ["config", "bundled"], "Codex skills");
106985
+ keys3(config.skills, ["config", "bundled", "native_allowed_paths"], "Codex skills");
106986
+ if (config.skills?.native_allowed_paths !== undefined) {
106987
+ for (const value of paths(config.skills.native_allowed_paths, "Codex native allowed skill paths")) {
106988
+ if (!isAbsolute26(value))
106989
+ unresolved("relative Codex native allowed skill path");
106990
+ const path = canonical9(value);
106991
+ roots.add(path.endsWith(`${sep13}SKILL.md`) ? dirname26(path) : path);
106992
+ }
106993
+ }
106616
106994
  if (config.skills?.bundled !== undefined) {
106617
106995
  keys3(config.skills.bundled, ["enabled"], "Codex bundled skills");
106618
106996
  if (typeof config.skills.bundled.enabled !== "boolean")
@@ -106638,7 +107016,7 @@ function resolveDiscovery(options, exclusion) {
106638
107016
  if (Object.keys(config.enabledPlugins ?? config.plugins ?? config.extensions ?? {}).length > 512)
106639
107017
  unresolved("unbounded native registrations");
106640
107018
  if (hasHooks || hasPluginConfigs || hasFunctionHookEnvironment)
106641
- verifyOpaqueClosure([]);
107019
+ reviewRemedy(canonical9(configPath), () => verifyOpaqueClosure([]));
106642
107020
  }
106643
107021
  if (options.reviewed && (options.reviewed.version !== 1 || !Array.isArray(options.reviewed.agents) || options.reviewed.agents.some((item) => !item || typeof item !== "object") || new Set(options.reviewed.agents.map((item) => item.agent)).size !== options.reviewed.agents.length))
106644
107022
  throw new Error("Invalid --discovery-inputs version or agents");
@@ -106670,7 +107048,9 @@ function resolveDiscovery(options, exclusion) {
106670
107048
  const target = join34(path, name);
106671
107049
  if (!lstatSync30(target).isFile())
106672
107050
  unresolved("local plugin packages need a dedicated discovery adapter");
106673
- witness(target);
107051
+ const text2 = witness(target);
107052
+ if (exclusion && text2 !== null)
107053
+ sumiLocalPluginTexts.set(canonical9(target), text2);
106674
107054
  }
106675
107055
  }
106676
107056
  for (const root2 of roots) {
@@ -106804,38 +107184,126 @@ function resolveDiscovery(options, exclusion) {
106804
107184
  const unique = combined.filter((source, index) => combined.findIndex((candidate2) => isDeepStrictEqual8(candidate2, source)) === index);
106805
107185
  return { ...supplied, roots: [...new Set([...roots, ...supplied.roots])].sort(), sources: unique, ...agent === "gemini" ? { builtinNames } : {} };
106806
107186
  }
106807
- function plugin(root2) {
107187
+ const reviewedManifestAbsence = (root2, manifestPath) => {
107188
+ const retained = exclusion?.retained;
107189
+ if (agent !== "claude" || retained?.method !== "reviewed" || ![join34(root2, "skills"), root2].some((path) => retained.roots.includes(path)))
107190
+ return false;
107191
+ const saved = retained.sources.filter((source) => source.path === canonical9(manifestPath) && source.hashMode === "path-bytes" && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined);
107192
+ return saved.length > 0 && saved.every((source) => projected(source, undefined, budget3) === source.sha256);
107193
+ };
107194
+ const reviewedInstalledCopy = (root2, path, identity5) => {
107195
+ const retained = exclusion?.retained;
107196
+ if (agent !== "codex" || retained?.method !== "reviewed")
107197
+ return false;
107198
+ const inside = relative8(root2, canonical9(path));
107199
+ if (!inside || inside === ".." || inside.startsWith(`..${sep13}`) || isAbsolute26(inside))
107200
+ return false;
107201
+ const parent = canonical9(join34(nativeHome, "plugins/cache", identity5.marketplace, identity5.name));
107202
+ const current = hashRawDiscoveryFile(canonical9(path), budget3);
107203
+ if (current === null)
107204
+ return false;
107205
+ return retained.sources.some((source) => {
107206
+ if (source.sha256 !== current || source.format !== undefined || source.fields !== undefined || ![undefined, "bytes"].includes(source.hashMode) || source.managedPlugins !== undefined || source.reviewArtifact !== undefined || !source.path.startsWith(parent + sep13))
107207
+ return false;
107208
+ const [version2, ...rest] = source.path.slice(parent.length + 1).split(sep13);
107209
+ return Boolean(version2) && version2 !== "." && version2 !== ".." && rest.join(sep13) === inside;
107210
+ });
107211
+ };
107212
+ const verifyCodexPluginHookClosure = (root2, identity5, marketplaceSource, servers, declarations) => {
107213
+ const retained = exclusion.retained;
107214
+ if (retained.codexSemanticCache || retained.codexInstallationInputs || retained.codexRetiredMaterializations)
107215
+ return verifyOpaqueClosure(declarations);
107216
+ const within2 = (base, path) => path === base || path.startsWith(base + sep13);
107217
+ const executables = [];
107218
+ for (const server of servers) {
107219
+ const row = config.mcp_servers?.[server];
107220
+ if (server === "codex_apps" || server === "_codex_apps" || !isCodexLocalStdioMcpServer(server, row))
107221
+ unresolved("plugin hook MCP server is not a configured local stdio server");
107222
+ for (const value of [row.command, ...row.args ?? []])
107223
+ for (const candidate2 of [value, value.slice(value.indexOf("=") + 1)]) {
107224
+ if (!isAbsolute26(candidate2) || /[\0$]/.test(candidate2) || executables.length >= AGENT_POLICY_LIMITS.discoveryRoots)
107225
+ continue;
107226
+ const path = canonical9(candidate2);
107227
+ if (lstatSync30(path, { throwIfNoEntry: false })?.isFile())
107228
+ executables.push(path);
107229
+ }
107230
+ }
107231
+ const cacheRoot = canonical9(join34(nativeHome, "plugins/cache"));
107232
+ const ownCache = canonical9(join34(nativeHome, "plugins/cache", identity5.marketplace, identity5.name));
107233
+ const ownCatalog = canonical9(join34(marketplaceSource, ".agents/plugins/marketplace.json"));
107234
+ const account = canonical9(home);
107235
+ const marketplaceRoots = Object.values(config.marketplaces ?? {}).filter((market) => market?.source_type === "local" && typeof market.source === "string" && isAbsolute26(market.source)).map((market) => canonical9(market.source)).filter((path) => !within2(path, account));
107236
+ const otherPlugin = (path) => !within2(root2, path) && !within2(ownCache, path) && path !== ownCatalog && !executables.includes(path) && (within2(cacheRoot, path) || marketplaceRoots.some((base) => within2(base, path)));
107237
+ const sources2 = retained.sources.filter((source) => source.path !== canonical9(configPath) && !otherPlugin(source.path));
107238
+ if (sources2.some((source) => source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || source.reviewArtifact !== undefined || ![undefined, "bytes", "path-bytes", "executable-v1"].includes(source.hashMode)))
107239
+ return verifyOpaqueClosure(declarations);
107240
+ const declared = declarations.map((path) => relative8(root2, path));
107241
+ const declaration = (path) => declarations.includes(path) || path.startsWith(ownCache + sep13) && declared.includes(path.slice(ownCache.length + 1).split(sep13).slice(1).join(sep13));
107242
+ if (!sources2.some((source) => !declaration(source.path) && source.sha256 !== null))
107243
+ unresolved("opaque executable dependency closure is unavailable");
107244
+ for (const source of sources2)
107245
+ if (projected(source, undefined, budget3) !== source.sha256) {
107246
+ throw new NativeDiscoveryWitnessChange("input", source.path, `Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
107247
+ }
107248
+ verifyDiscoveryDirectories((retained.directories ?? []).filter((directory2) => !otherPlugin(directory2.path)));
107249
+ };
107250
+ const readCodexHookDeclaration = (text2, path) => {
107251
+ if (text2 === null)
107252
+ return null;
107253
+ try {
107254
+ return codexPluginHookHandlers(parseConfig(text2, path));
107255
+ } catch {
107256
+ return null;
107257
+ }
107258
+ };
107259
+ function plugin(root2, codex) {
106808
107260
  root2 = canonical9(root2);
106809
107261
  safe6(root2);
106810
107262
  if (exclusion)
106811
107263
  roots.add(root2);
106812
107264
  const manifestPath = join34(root2, agent === "claude" ? ".claude-plugin/plugin.json" : ".codex-plugin/plugin.json");
106813
107265
  const raw = witness(manifestPath, undefined, undefined, agent === "claude" ? "claude-plugin-manifest-v1" : undefined);
106814
- if (raw === null)
107266
+ if (raw === null && !reviewedManifestAbsence(root2, manifestPath))
106815
107267
  unresolved("plugin manifest missing");
106816
- const manifest = parseConfig(raw, manifestPath);
107268
+ const manifest = raw === null ? {} : parseConfig(raw, manifestPath);
106817
107269
  if (exclusion && Object.keys(manifest).some((key2) => /skill|plugin|hook|marketplace|extension|instruction/i.test(key2) && !["skills", "hooks"].includes(key2)))
106818
107270
  unresolved("unknown plugin discovery controls");
106819
107271
  const hooks = witness(join34(root2, "hooks/hooks.json"));
106820
- if (manifest.hooks !== undefined || hooks !== null) {
106821
- if (!exclusion || exclusion.retained?.method !== "reviewed")
106822
- unresolved("plugin hooks require a separate no-skill-injection review");
106823
- const required = [manifestPath, ...hooks !== null ? [join34(root2, "hooks/hooks.json")] : []];
106824
- if (manifest.hooks !== undefined) {
106825
- if (typeof manifest.hooks !== "string")
106826
- unresolved("unsupported plugin hooks target");
106827
- const hook = resolve27(root2, manifest.hooks);
106828
- if (!hook.startsWith(root2 + sep13))
106829
- unresolved("plugin hooks target escapes its root");
106830
- required.push(hook);
106831
- }
106832
- for (const path of required) {
106833
- const saved = exclusion.retained.sources.filter((source) => source.path === canonical9(path) && source.sha256 !== null && source.format === undefined && source.fields === undefined);
106834
- if (!saved.length || saved.some((source) => projected(source, undefined, budget3) !== source.sha256))
106835
- unresolved("plugin hooks require current independently reviewed sources");
106836
- }
106837
- verifyOpaqueClosure(required.map((path) => canonical9(path)));
106838
- }
107272
+ if (manifest.hooks !== undefined || hooks !== null)
107273
+ reviewRemedy(canonical9(hooks !== null ? join34(root2, "hooks/hooks.json") : manifestPath), () => {
107274
+ if (!exclusion || exclusion.retained?.method !== "reviewed")
107275
+ unresolved("plugin hooks require a separate no-skill-injection review");
107276
+ const required = [manifestPath, ...hooks !== null ? [join34(root2, "hooks/hooks.json")] : []];
107277
+ const scoped2 = agent === "codex" && codex !== undefined ? [] : undefined;
107278
+ if (manifest.hooks !== undefined) {
107279
+ if (scoped2 && manifest.hooks && typeof manifest.hooks === "object" && !Array.isArray(manifest.hooks)) {
107280
+ const inline = codexPluginHookHandlers(manifest.hooks);
107281
+ if (!inline)
107282
+ unresolved("unsupported inline plugin hooks");
107283
+ scoped2.push(inline);
107284
+ } else {
107285
+ if (typeof manifest.hooks !== "string")
107286
+ unresolved("unsupported plugin hooks target");
107287
+ const hook = resolve27(root2, manifest.hooks);
107288
+ if (!hook.startsWith(root2 + sep13))
107289
+ unresolved("plugin hooks target escapes its root");
107290
+ required.push(hook);
107291
+ if (scoped2)
107292
+ scoped2.push(readCodexHookDeclaration(witness(hook), hook));
107293
+ }
107294
+ }
107295
+ if (scoped2 && hooks !== null)
107296
+ scoped2.push(readCodexHookDeclaration(hooks, join34(root2, "hooks/hooks.json")));
107297
+ for (const path of required) {
107298
+ const saved = exclusion.retained.sources.filter((source) => source.path === canonical9(path) && source.sha256 !== null && source.format === undefined && source.fields === undefined);
107299
+ if (saved.length ? saved.some((source) => projected(source, undefined, budget3) !== source.sha256) : !(scoped2 && reviewedInstalledCopy(root2, path, codex)))
107300
+ unresolved("plugin hooks require current independently reviewed sources", canonical9(path));
107301
+ }
107302
+ if (scoped2 && scoped2.every((item) => item !== null && item.commands === 0)) {
107303
+ verifyCodexPluginHookClosure(root2, codex, codex.source, new Set(scoped2.flatMap((item) => [...item.servers])), required.map((path) => canonical9(path)));
107304
+ } else
107305
+ verifyOpaqueClosure(required.map((path) => canonical9(path)));
107306
+ });
106839
107307
  const commands = join34(root2, "commands");
106840
107308
  safe6(commands);
106841
107309
  if (agent === "claude" && (manifest.commands !== undefined || lstatSync30(commands, { throwIfNoEntry: false })?.isDirectory() && entries(commands).length))
@@ -106872,302 +107340,78 @@ function resolveDiscovery(options, exclusion) {
106872
107340
  unresolved("enabled plugin registration is missing or ambiguous");
106873
107341
  if (exclusion && (!isAbsolute26(matches[0].installPath) || resolve27(matches[0].installPath) !== matches[0].installPath || matches[0].installPath.length > AGENT_POLICY_LIMITS.pathCharacters))
106874
107342
  unresolved("malformed plugin installation root");
106875
- plugin(matches[0].installPath);
106876
- }
106877
- } else if (agent === "codex") {
106878
- for (const [id2, value] of Object.entries(config.plugins ?? {})) {
106879
- if (value?.enabled === false)
106880
- continue;
106881
- if (value?.enabled !== true)
106882
- unresolved("unsupported plugin enablement");
106883
- const split = id2.lastIndexOf("@"), name = id2.slice(0, split), marketplace = id2.slice(split + 1);
106884
- const market = config.marketplaces?.[marketplace] ?? (marketplace === "personal" ? { source_type: "local", source: home } : undefined);
106885
- if (split < 1 || market?.source_type !== "local" || typeof market.source !== "string" || !isAbsolute26(market.source))
106886
- unresolved("enabled plugin marketplace is not an explicit local source");
106887
- const catalogText = witness(join34(market.source, ".agents/plugins/marketplace.json"));
106888
- if (catalogText === null)
106889
- unresolved("marketplace catalog missing");
106890
- const catalog = parseConfig(catalogText, join34(market.source, ".agents/plugins/marketplace.json")), matches = Array.isArray(catalog.plugins) ? catalog.plugins.filter((item) => item.name === name) : [];
106891
- if (matches.length !== 1 || matches[0]?.source?.source !== "local" || typeof matches[0]?.source?.path !== "string")
106892
- unresolved("plugin catalog source is missing or ambiguous");
106893
- plugin(resolve27(market.source, matches[0].source.path));
106894
- }
106895
- } else if (agent === "opencode") {
106896
- if (witness(join34(home, ".config/opencode/opencode.jsonc")) !== null || witness(join34(home, ".config/opencode/config.json")) !== null)
106897
- unresolved("additional config layers require review");
106898
- if ((config.plugin?.length ?? 0) > 0 || config.skills?.urls?.length)
106899
- unresolved("external plugin or remote skill source");
106900
- for (const path of config.skills?.paths ?? []) {
106901
- if (typeof path !== "string" || !isAbsolute26(path))
106902
- unresolved("relative or malformed added skill root");
106903
- roots.add(path);
106904
- }
106905
- const localPlugins = join34(home, ".config/opencode/plugins");
106906
- safe6(localPlugins);
106907
- if (lstatSync30(localPlugins, { throwIfNoEntry: false }) && entries(localPlugins).some((name) => name !== "skills-cli.js"))
106908
- unresolved("additional local plugin hooks require review");
106909
- } else if (agent === "sumi") {
106910
- if ((config.plugin?.length ?? 0) > 0 || (config.plugins?.length ?? 0) > 0 || sources.some((source) => source.path !== canonical9(configPath) && source.path !== canonical9(join34(sumiConfigDirectory(home), "sumi.jsonc")) && source.sha256 !== null))
106911
- unresolved("external or foreign local plugins require review");
106912
- } else if (agent === "hermes") {
106913
- for (const directory2 of [join34(home, ".hermes/plugins"), join34(home, ".hermes/hermes-agent")]) {
106914
- safe6(directory2);
106915
- if (lstatSync30(directory2, { throwIfNoEntry: false }))
106916
- unresolved("installed runtime/plugin discovery requires reviewed source bindings");
106917
- }
106918
- if (Bun.which("hermes") || Object.keys(config.plugins ?? {}).length)
106919
- unresolved("installed runtime/plugin discovery requires reviewed source bindings");
106920
- } else if (agent === "gemini") {
106921
- if (Object.keys(config.extensions ?? {}).length || config.skills?.paths?.length)
106922
- unresolved("custom extension or skill paths");
106923
- const directory2 = join34(home, ".gemini/extensions");
106924
- safe6(directory2);
106925
- if (lstatSync30(directory2, { throwIfNoEntry: false }))
106926
- for (const name of entries(directory2)) {
106927
- const root2 = join34(directory2, name);
106928
- safe6(root2);
106929
- if (!lstatSync30(root2)?.isDirectory())
106930
- continue;
106931
- const raw = witness(join34(root2, "gemini-extension.json"));
106932
- if (raw === null)
106933
- unresolved("extension registration missing");
106934
- const manifest = parseConfig(raw, join34(root2, "gemini-extension.json"));
106935
- if (manifest.hooks !== undefined || witness(join34(root2, "hooks/hooks.json")) !== null || witness(join34(root2, ".gemini-extension-install.json")) !== null)
106936
- unresolved("extension hooks or linked registration require review");
106937
- roots.add(join34(root2, "skills"));
106938
- }
106939
- }
106940
- if (exclusion && roots.size > AGENT_POLICY_LIMITS.discoveryRoots)
106941
- unresolved("native discovery roots exceed their bound");
106942
- return { agent, roots: [...roots].sort(), sources, method: "automatic", ...agent === "gemini" ? { builtinNames } : {}, ...agent === "hermes" ? { directories: captureDiscoveryDirectories([join34(home, ".hermes/plugins"), join34(home, ".hermes/hermes-agent")].map((path) => canonical9(path))) } : {} };
106943
- }
106944
-
106945
- // src/lib/agent-opencode.ts
106946
- function renderOpenCodePlugin(command, profile) {
106947
- return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
106948
- import { spawn } from "node:child_process";
106949
- import { createHash } from "node:crypto";
106950
- const command = ${JSON.stringify(command)};
106951
- const profile = ${JSON.stringify(profile)};
106952
- const bridgeBodySha256 = ${JSON.stringify(OPENCODE_BRIDGE_BODY_SHA256)};
106953
- const bridgeDescriptionSha256 = ${JSON.stringify(OPENCODE_BRIDGE_DESCRIPTION_SHA256)};
106954
- function digest(text) { return createHash("sha256").update(text).digest("hex"); }
106955
- function canonicalSkill(skill) {
106956
- return skill?.id === "skills-cli" && skill.name === "skills-cli" && typeof skill.path === "string"
106957
- && typeof skill.content === "string" && digest(skill.content) === bridgeBodySha256
106958
- && typeof skill.description === "string" && digest(skill.description) === bridgeDescriptionSha256
106959
- && (skill.autoinvoke === undefined || skill.autoinvoke === false);
106960
- }
106961
- function refusal() {
106962
- const error = new Error("Skills verification blocked this request. Review the OpenCode Skills hook configuration, then retry.");
106963
- error.name = "SkillsHookRefusal";
106964
- error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED" };
106965
- return error;
106966
- }
106967
- export default {
106968
- id: "local.hasna-skills-cli",
106969
- async setup(ctx) {
106970
- if (!ctx?.session?.hook || !ctx?.session?.get || !ctx?.skill?.transform || !ctx?.skill?.list || !ctx?.tool?.hook) throw refusal();
106971
- const registrations = [], pending = new Set(), sessions = new Set(), prompts = new Map();
106972
- let disposed = false;
106973
- function context(input) {
106974
- if (disposed) return Promise.reject(refusal());
106975
- return new Promise((resolve, reject) => {
106976
- // Availability failures may omit optional model context. Native skill
106977
- // consumption and explicit policy refusals always remain fail-closed.
106978
- const optional = input.delivery_phase === "context";
106979
- const chunks = []; let bytes = 0, settled = false, child, timer;
106980
- const finish = () => { clearTimeout(timer); pending.delete(cancel); };
106981
- const deny = () => { if (settled) return; settled = true; finish(); reject(refusal()); };
106982
- const unavailable = code => {
106983
- if (settled) return;
106984
- if (!optional || disposed) return deny();
106985
- settled = true; finish();
106986
- 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." });
106987
- };
106988
- const stop = code => { unavailable(code); child?.kill("SIGKILL"); };
106989
- const cancel = () => { deny(); child?.kill("SIGKILL"); };
106990
- try {
106991
- child = spawn(command, ["hook", "user-prompt", "--agent", "opencode", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
106992
- } catch { unavailable("SPAWN_FAILED"); return; }
106993
- pending.add(cancel);
106994
- timer = setTimeout(() => stop("TIMEOUT"), 15000);
106995
- child.stdout.on("data", chunk => { if (settled) return; bytes += chunk.length; if (bytes > 1048576) stop("OUTPUT_LIMIT"); else chunks.push(chunk); });
106996
- child.stdout.on("error", () => stop("TRANSPORT_FAILED"));
106997
- child.stderr.on("data", () => {});
106998
- child.stderr.on("error", () => stop("TRANSPORT_FAILED"));
106999
- child.stdin.on("error", () => stop("TRANSPORT_FAILED"));
107000
- child.on("error", () => unavailable("SPAWN_FAILED"));
107001
- child.on("close", code => {
107002
- if (settled) return;
107003
- if (disposed) return deny();
107004
- if (code !== 0) return unavailable("CHILD_FAILED");
107005
- let result;
107006
- try {
107007
- result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
107008
- } catch { unavailable("INVALID_RESPONSE"); return; }
107009
- if (!result || typeof result !== "object" || Array.isArray(result)) return unavailable("INVALID_RESPONSE");
107010
- if (result.decision === "block" || result.continue === false) return deny();
107011
- // hookContextOutput returns exactly {} for a successful empty
107012
- // context. It never attests native bridge verification.
107013
- const output = optional && Object.keys(result).length === 0
107014
- ? { hookEventName: input.hook_event_name, additionalContext: "" }
107015
- : result.hookSpecificOutput;
107016
- if (!output || typeof output !== "object" || Array.isArray(output) || output.hookEventName !== input.hook_event_name || typeof output.additionalContext !== "string") return unavailable("INVALID_RESPONSE");
107017
- if (input.hook_event_name === "BeforeTool" && result.nativeBridgeVerified !== true) return deny();
107018
- settled = true; finish();
107019
- resolve({ delivered: true, text: output.additionalContext });
107020
- });
107021
- try { child.stdin.end(JSON.stringify(input)); } catch { stop("TRANSPORT_FAILED"); }
107022
- });
107343
+ plugin(matches[0].installPath);
107023
107344
  }
107024
- async function input(sessionID, prompt, event) {
107025
- if (disposed) throw refusal();
107026
- let session;
107027
- try { session = await ctx.session.get({ sessionID }); } catch { throw refusal(); }
107028
- if (disposed || !session || session.id !== sessionID || typeof session.location?.directory !== "string" || (session.parentID !== undefined && typeof session.parentID !== "string") || session.parentID === sessionID) throw refusal();
107029
- 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 };
107345
+ } else if (agent === "codex") {
107346
+ for (const [id2, value] of Object.entries(config.plugins ?? {})) {
107347
+ if (value?.enabled === false)
107348
+ continue;
107349
+ if (value?.enabled !== true)
107350
+ unresolved("unsupported plugin enablement");
107351
+ const split = id2.lastIndexOf("@"), name = id2.slice(0, split), marketplace = id2.slice(split + 1);
107352
+ const market = config.marketplaces?.[marketplace] ?? (marketplace === "personal" ? { source_type: "local", source: home } : undefined);
107353
+ if (split < 1 || market?.source_type !== "local" || typeof market.source !== "string" || !isAbsolute26(market.source))
107354
+ unresolved("enabled plugin marketplace is not an explicit local source");
107355
+ const catalogText = witness(join34(market.source, ".agents/plugins/marketplace.json"));
107356
+ if (catalogText === null)
107357
+ unresolved("marketplace catalog missing");
107358
+ const catalog = parseConfig(catalogText, join34(market.source, ".agents/plugins/marketplace.json")), matches = Array.isArray(catalog.plugins) ? catalog.plugins.filter((item) => item.name === name) : [];
107359
+ if (matches.length !== 1 || matches[0]?.source?.source !== "local" || typeof matches[0]?.source?.path !== "string")
107360
+ unresolved("plugin catalog source is missing or ambiguous");
107361
+ plugin(resolve27(market.source, matches[0].source.path), exclusion ? { marketplace, name, source: market.source } : undefined);
107030
107362
  }
107031
- async function dispose() {
107032
- if (disposed) return;
107033
- disposed = true;
107034
- for (const cancel of pending) cancel();
107035
- sessions.clear();
107036
- prompts.clear();
107037
- const results = await Promise.allSettled(registrations.reverse().map(registration => Promise.resolve().then(() => registration.dispose())));
107038
- if (results.some(result => result.status === "rejected")) throw refusal();
107363
+ } else if (agent === "opencode") {
107364
+ if (witness(join34(home, ".config/opencode/opencode.jsonc")) !== null || witness(join34(home, ".config/opencode/config.json")) !== null)
107365
+ unresolved("additional config layers require review");
107366
+ if ((config.plugin?.length ?? 0) > 0 || config.skills?.urls?.length)
107367
+ unresolved("external plugin or remote skill source");
107368
+ for (const path of config.skills?.paths ?? []) {
107369
+ if (typeof path !== "string" || !isAbsolute26(path))
107370
+ unresolved("relative or malformed added skill root");
107371
+ roots.add(path);
107039
107372
  }
107040
- try {
107041
- registrations.push(await ctx.skill.transform(editor => {
107042
- for (const skill of editor.list()) if (!canonicalSkill(skill)) editor.remove(skill.id);
107043
- }));
107044
- async function verifyNativeBridge(sessionID) {
107045
- let skills;
107046
- try { skills = await ctx.skill.list(); } catch { throw refusal(); }
107047
- if (!Array.isArray(skills)) throw refusal();
107048
- const matches = skills.filter(skill => skill?.id === "skills-cli");
107049
- if (matches.length !== 1) throw refusal();
107050
- const skill = matches[0];
107051
- if (!canonicalSkill(skill)) throw refusal();
107052
- const metadata = { id: skill.id, name: skill.name, path: skill.path, contentSha256: digest(skill.content), descriptionSha256: digest(skill.description) };
107053
- await context({ ...await input(sessionID, "", "BeforeTool"), tool_input: { id: "skills-cli" }, native_skill: metadata });
107054
- }
107055
- registrations.push(await ctx.tool.hook("execute.before", async event => {
107056
- if (disposed) throw refusal();
107057
- if (event.tool !== "skill") return;
107058
- if (event.input?.id !== "skills-cli") throw refusal();
107059
- await verifyNativeBridge(event.sessionID);
107060
- }));
107061
- registrations.push(await ctx.session.hook("prompt", async event => {
107062
- if (disposed || typeof event.prompt?.text !== "string" || event.prompt.text.length > 128 * 1024) throw refusal();
107063
- if (event.prompt.skills !== undefined && (!Array.isArray(event.prompt.skills) || event.prompt.skills.some(skill => skill?.id !== "skills-cli"))) throw refusal();
107064
- // Attachments materialize immediately after this hook. Check the actual
107065
- // current registry object and on-disk bridge before native consumption.
107066
- if (event.prompt.skills?.length) await verifyNativeBridge(event.sessionID);
107067
- if (disposed) throw refusal();
107068
- const queued = prompts.get(event.sessionID) ?? [];
107069
- if (queued.reduce((size, entry) => size + entry.text.length + 1, 0) + event.prompt.text.length > 128 * 1024) throw refusal();
107070
- queued.push({ text: event.prompt.text });
107071
- prompts.set(event.sessionID, queued);
107072
- }));
107073
- registrations.push(await ctx.session.hook("context", async event => {
107074
- // Only prompt-hook input has owner-prompt provenance. Keep it transient
107075
- // until the one delivery-bearing CLI call, including an empty prompt.
107076
- // Checkpoints, synthetic user-role history and resumes never match.
107077
- const queued = [...(prompts.get(event.sessionID) ?? [])];
107078
- const result = await context(await input(event.sessionID, queued.map(entry => entry.text).join("\\n")));
107079
- if (disposed) throw refusal();
107080
- if (result.text) event.system.push({ type: "text", text: result.text });
107081
- // Remove only the prompt events this request consumed. New admission
107082
- // while the CLI was running belongs to the next model context.
107083
- const current = prompts.get(event.sessionID);
107084
- if (current) {
107085
- const remaining = current.filter(entry => !queued.includes(entry));
107086
- if (remaining.length) prompts.set(event.sessionID, remaining);
107087
- else prompts.delete(event.sessionID);
107088
- }
107089
- // Retry SessionStart on the next context after an unavailable child,
107090
- // but never replay prompt text already admitted to this model turn.
107091
- if (result.delivered) sessions.add(event.sessionID);
107092
- }));
107093
- return dispose;
107094
- } catch {
107095
- await dispose();
107096
- throw refusal();
107373
+ const localPlugins = join34(home, ".config/opencode/plugins");
107374
+ safe6(localPlugins);
107375
+ if (lstatSync30(localPlugins, { throwIfNoEntry: false }) && entries(localPlugins).some((name) => name !== "skills-cli.js"))
107376
+ unresolved("additional local plugin hooks require review");
107377
+ } else if (agent === "sumi") {
107378
+ const pinnedLocalPlugin = (source) => {
107379
+ const retained = exclusion?.retained, text2 = sumiLocalPluginTexts.get(source.path);
107380
+ return retained?.method === "reviewed" && text2 !== undefined && sumiPluginLoadsOnlyBuiltins(text2) && retained.sources.some((saved) => saved.path === source.path && saved.sha256 === source.sha256 && saved.sha256 !== null && [undefined, "bytes"].includes(saved.hashMode) && saved.format === undefined && saved.fields === undefined && saved.managedPlugins === undefined && saved.reviewArtifact === undefined);
107381
+ };
107382
+ if ((config.plugin?.length ?? 0) > 0 || (config.plugins?.length ?? 0) > 0 || sources.some((source) => source.path !== canonical9(configPath) && source.path !== canonical9(join34(sumiConfigDirectory(home), "sumi.jsonc")) && source.sha256 !== null && !pinnedLocalPlugin(source)))
107383
+ unresolved("external or foreign local plugins require review");
107384
+ } else if (agent === "hermes") {
107385
+ for (const directory2 of [join34(home, ".hermes/plugins"), join34(home, ".hermes/hermes-agent")]) {
107386
+ safe6(directory2);
107387
+ if (lstatSync30(directory2, { throwIfNoEntry: false }))
107388
+ unresolved("installed runtime/plugin discovery requires reviewed source bindings");
107097
107389
  }
107098
- },
107099
- };
107100
- `;
107101
- }
107102
-
107103
- // src/lib/agent-adapters.ts
107104
- var INTEGRATION_AGENTS = ["claude", "codex", "gemini", "opencode", "cursor", "hermes", "sumi"];
107105
- function renderAgentHookCommand(command, agent, profileId, event) {
107106
- if (typeof profileId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profileId) || profileId.includes(".."))
107107
- throw new Error("Invalid selection profile id");
107108
- return `'${command.replace(/'/g, `'\\''`)}' hook user-prompt --agent ${agent} --selection-profile ${profileId} --event ${event}`;
107109
- }
107110
- var AGENT_ADAPTERS = {
107111
- claude: { root: ".claude/skills", config: ".claude/settings.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
107112
- codex: { root: ".codex/skills", config: ".codex/hooks.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
107113
- gemini: { root: ".gemini/skills", config: ".gemini/settings.json", events: ["BeforeAgent", "SessionStart"], promptContext: true },
107114
- opencode: { root: ".config/opencode/skills", config: ".config/opencode/opencode.json", events: ["prompt", "context"], promptContext: true },
107115
- sumi: { root: null, config: null, events: ["prompt", "context"], promptContext: true },
107116
- 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 },
107117
- cursor: { root: ".cursor/skills", config: ".cursor/hooks.json", events: ["beforeSubmitPrompt", "sessionStart"], promptContext: false }
107118
- };
107119
- function renderLegacyOpenCodePlugin(command, profileId) {
107120
- return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
107121
- import { spawn } from "node:child_process";
107122
- import { randomUUID } from "node:crypto";
107123
- const command = ${JSON.stringify(command)};
107124
- const profile = ${JSON.stringify(profileId)};
107125
- function context(input) {
107126
- return new Promise((resolve, reject) => {
107127
- const child = spawn(command, ["hook", "user-prompt", "--agent", "opencode", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
107128
- const chunks = []; let bytes = 0, failure;
107129
- const timer = setTimeout(() => { failure = new Error("Skills prompt hook timed out"); child.kill("SIGKILL"); }, 15000);
107130
- child.stdout.on("data", chunk => { bytes += chunk.length; if (bytes > 1048576) { failure = new Error("Skills prompt hook output exceeded its limit"); child.kill("SIGKILL"); } else chunks.push(chunk); });
107131
- child.stderr.on("data", () => {});
107132
- child.stdin.on("error", () => {});
107133
- child.on("error", error => { clearTimeout(timer); reject(error); });
107134
- child.on("close", code => {
107135
- clearTimeout(timer);
107136
- if (failure || code !== 0) return reject(failure ?? new Error("Skills prompt hook failed"));
107137
- try {
107138
- const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
107139
- if (!result || typeof result !== "object" || Array.isArray(result)) throw new Error("Invalid Skills hook response");
107140
- if (result.decision === "block" || result.continue === false) throw new Error(result.reason ?? result.stopReason ?? "Skills prompt hook refused");
107141
- if (Object.keys(result).length && (!result.hookSpecificOutput || result.hookSpecificOutput.hookEventName !== input.hook_event_name)) throw new Error("Invalid Skills hook response");
107142
- const text = result.hookSpecificOutput?.additionalContext ?? "";
107143
- if (typeof text !== "string") throw new Error("Invalid Skills prompt context");
107144
- resolve(text);
107145
- } catch (error) { reject(error); }
107146
- });
107147
- child.stdin.end(JSON.stringify(input));
107148
- });
107149
- }
107150
- export default async ({ directory }) => {
107151
- const sessions = new Set();
107152
- return {
107153
- "chat.message": async (input, output) => {
107154
- const parts = output.parts.filter(part => part.type === "text" && typeof part.text === "string");
107155
- const first = !sessions.has(input.sessionID);
107156
- const text = await context({ hook_event_name: first ? "SessionStart" : "UserPromptSubmit", cwd: directory, session_id: input.sessionID, prompt: parts.map(part => part.text).join("\\n") });
107157
- if (text && parts.length) parts[0].text += "\\n\\n" + text;
107158
- else if (text) output.parts.push({ id: "prt_" + randomUUID().replaceAll("-", ""), sessionID: input.sessionID, messageID: output.message.id, type: "text", text, synthetic: true });
107159
- if (sessions.size >= 1000) sessions.clear();
107160
- sessions.add(input.sessionID);
107161
- },
107162
- "tool.execute.before": async (input, output) => {
107163
- if (input.tool === "skill" && output.args?.name !== "skills-cli") throw new Error("Load instructions through the Skills CLI bridge");
107164
- },
107165
- };
107166
- };
107167
- `;
107168
- }
107169
- function isManagedOpenCodePlugin(text2, command, profileId) {
107170
- return text2 === renderOpenCodePlugin(command, profileId) || text2 === renderLegacyOpenCodePlugin(command, profileId);
107390
+ if (Bun.which("hermes") || Object.keys(config.plugins ?? {}).length)
107391
+ unresolved("installed runtime/plugin discovery requires reviewed source bindings");
107392
+ } else if (agent === "gemini") {
107393
+ if (Object.keys(config.extensions ?? {}).length || config.skills?.paths?.length)
107394
+ unresolved("custom extension or skill paths");
107395
+ const directory2 = join34(home, ".gemini/extensions");
107396
+ safe6(directory2);
107397
+ if (lstatSync30(directory2, { throwIfNoEntry: false }))
107398
+ for (const name of entries(directory2)) {
107399
+ const root2 = join34(directory2, name);
107400
+ safe6(root2);
107401
+ if (!lstatSync30(root2)?.isDirectory())
107402
+ continue;
107403
+ const raw = witness(join34(root2, "gemini-extension.json"));
107404
+ if (raw === null)
107405
+ unresolved("extension registration missing");
107406
+ const manifest = parseConfig(raw, join34(root2, "gemini-extension.json"));
107407
+ if (manifest.hooks !== undefined || witness(join34(root2, "hooks/hooks.json")) !== null || witness(join34(root2, ".gemini-extension-install.json")) !== null)
107408
+ unresolved("extension hooks or linked registration require review");
107409
+ roots.add(join34(root2, "skills"));
107410
+ }
107411
+ }
107412
+ if (exclusion && roots.size > AGENT_POLICY_LIMITS.discoveryRoots)
107413
+ unresolved("native discovery roots exceed their bound");
107414
+ return { agent, roots: [...roots].sort(), sources, method: "automatic", ...agent === "gemini" ? { builtinNames } : {}, ...agent === "hermes" ? { directories: captureDiscoveryDirectories([join34(home, ".hermes/plugins"), join34(home, ".hermes/hermes-agent")].map((path) => canonical9(path))) } : {} };
107171
107415
  }
107172
107416
 
107173
107417
  // src/lib/agent-codex.ts
@@ -107683,6 +107927,82 @@ function assertPinnedLauncherRuntimeSafe(runtime) {
107683
107927
  }
107684
107928
  }
107685
107929
 
107930
+ // src/lib/linux-access-acl.ts
107931
+ var LINUX_ENODATA = 61;
107932
+ var LINUX_EOPNOTSUPP = 95;
107933
+ var LINUX_ACL_QUERY_ATTEMPTS = 4;
107934
+ var LINUX_ACL_FREE_WHEN_UNSUPPORTED = new Set([
107935
+ 0xef53n,
107936
+ 0x58465342n,
107937
+ 0x9123683en,
107938
+ 0x01021994n
107939
+ ]);
107940
+ function queryAttribute(probe2, errno, fd, attribute) {
107941
+ for (let attempt = 0;attempt < LINUX_ACL_QUERY_ATTEMPTS; attempt++) {
107942
+ errno.set?.(0);
107943
+ const result = probe2.fgetxattr(fd, attribute);
107944
+ const code = errno.get();
107945
+ if (result !== -1 && result !== -1n)
107946
+ return "present";
107947
+ if (code === LINUX_ENODATA)
107948
+ return "absent";
107949
+ if (code === LINUX_EOPNOTSUPP)
107950
+ return "unsupported";
107951
+ }
107952
+ return "unverified";
107953
+ }
107954
+ function linuxAccessAclAbsent(probe2, fd, creationParent) {
107955
+ if (probe2.arch !== "x64" && probe2.arch !== "arm64")
107956
+ return false;
107957
+ const errno = probe2.errno;
107958
+ if (!errno)
107959
+ return false;
107960
+ let unsupported = false;
107961
+ for (const attribute of ["system.posix_acl_access", ...creationParent ? ["system.posix_acl_default"] : []]) {
107962
+ const state = queryAttribute(probe2, errno, fd, attribute);
107963
+ if (state === "present" || state === "unverified")
107964
+ return false;
107965
+ if (state === "unsupported")
107966
+ unsupported = true;
107967
+ }
107968
+ if (!unsupported)
107969
+ return true;
107970
+ const type = probe2.filesystemType(fd);
107971
+ return type !== null && LINUX_ACL_FREE_WHEN_UNSUPPORTED.has(type);
107972
+ }
107973
+ function withGlibcAclProbe(use) {
107974
+ try {
107975
+ const { dlopen, ptr, read: read3, FFIType } = __require("bun:ffi");
107976
+ const library = dlopen("libc.so.6", {
107977
+ fgetxattr: { args: ["i32", "ptr", "ptr", "u64"], returns: FFIType.i64_fast },
107978
+ fstatfs: { args: ["i32", "ptr"], returns: "i32" },
107979
+ __errno_location: { args: [], returns: "ptr" }
107980
+ });
107981
+ try {
107982
+ const location = library.symbols.__errno_location();
107983
+ const names = new Map;
107984
+ return use({
107985
+ arch: process.arch,
107986
+ errno: location ? { get: () => read3.i32(location) } : null,
107987
+ fgetxattr: (fd, attribute) => {
107988
+ let name = names.get(attribute);
107989
+ if (!name)
107990
+ names.set(attribute, name = Buffer.from(attribute + "\x00"));
107991
+ return library.symbols.fgetxattr(fd, ptr(name), null, 0);
107992
+ },
107993
+ filesystemType: (fd) => {
107994
+ const statfs = new BigUint64Array(32);
107995
+ return library.symbols.fstatfs(fd, ptr(statfs)) === 0 ? statfs[0] : null;
107996
+ }
107997
+ });
107998
+ } finally {
107999
+ library.close();
108000
+ }
108001
+ } catch {
108002
+ return false;
108003
+ }
108004
+ }
108005
+
107686
108006
  // src/lib/agent-integration.ts
107687
108007
  var sha5 = (data) => createHash33("sha256").update(data).digest("hex");
107688
108008
  var ROOTS = NATIVE_SKILL_ROOTS;
@@ -108516,28 +108836,7 @@ function hookProjectionRefusal(reason2) {
108516
108836
  throw new Error(`CLAUDE_MANAGED_HOOK_${reason2}`);
108517
108837
  }
108518
108838
  function hasNoLinuxAccessAcl(fd, creationParent) {
108519
- try {
108520
- const { dlopen, read: read3, ptr } = __require("bun:ffi");
108521
- const library = dlopen("libc.so.6", {
108522
- fgetxattr: { args: ["i32", "ptr", "ptr", "u64"], returns: "i64" },
108523
- __errno_location: { args: [], returns: "ptr" }
108524
- });
108525
- try {
108526
- const errno = library.symbols.__errno_location();
108527
- if (!errno)
108528
- return false;
108529
- for (const attribute of ["system.posix_acl_access", ...creationParent ? ["system.posix_acl_default"] : []]) {
108530
- const name = Buffer.from(attribute + "\x00");
108531
- if (Number(library.symbols.fgetxattr(fd, ptr(name), null, 0)) !== -1 || read3.i32(errno) !== 61)
108532
- return false;
108533
- }
108534
- return true;
108535
- } finally {
108536
- library.close();
108537
- }
108538
- } catch {
108539
- return false;
108540
- }
108839
+ return withGlibcAclProbe((probe2) => linuxAccessAclAbsent(probe2, fd, creationParent));
108541
108840
  }
108542
108841
  function assertNativePathAcl(path, observed, options = {}) {
108543
108842
  if (process.platform !== "darwin" && process.platform !== "linux")
@@ -109478,7 +109777,7 @@ function planCodexPluginNameReconcile(request2) {
109478
109777
  pathDenies: [...pathDenies].sort(),
109479
109778
  ...discovery ? { discovery } : {}
109480
109779
  };
109481
- const plan = planAgentIntegration({ home, dataDir: dataDir2, projectDir, agents: ["codex"], codexNativeCatalog: catalog, codexReconcile: input });
109780
+ const plan = planAgentIntegration({ home, dataDir: dataDir2, projectDir, agents: ["codex"], codexNativeCatalog: catalog, codexReconcile: input, allowRootAliases: aliases.length > 0 });
109482
109781
  assertCodexReconcileScope(plan, { configPath, policyPath, prior, names, remoteIdentities, discovery: discovery ?? savedDiscovery });
109483
109782
  const configChange = plan.changes.find((change) => resolve28(change.path) === configPath);
109484
109783
  const plannedRules = configChange ? Bun.TOML.parse(configChange.after).skills?.config ?? [] : [];
@@ -111549,6 +111848,11 @@ function physicalPath(path) {
111549
111848
  }
111550
111849
  return resolve29(realpathSync15(cursor), ...suffix);
111551
111850
  }
111851
+ function portableAuthoringDiscoveryRefusal(error, destination) {
111852
+ const head = `Native discovery is unresolved (${error.agent}: ${error.detail}).${error.source ? ` Checked source: ${error.source}.` : ""}`;
111853
+ const review = `skills hook install --agent ${error.agent} --discovery-inputs <reviewed-file> --apply`;
111854
+ return error.reviewable ? `${head} Portable Skills authoring stays refused: an unreviewed ${error.agent} hook or plugin option could load skills written to ${destination}. This command takes no --discovery-inputs option; review the current ${error.agent} configuration with ${review}, then retry.` : `${head} Portable Skills authoring stays refused: Skills cannot list every skill root this ${error.agent} configuration registers, so it cannot rule out ${destination}. This command takes no --discovery-inputs option; correct that ${error.agent} configuration entry, then retry. Where a reviewed discovery binding covers it, record one with ${review}.`;
111855
+ }
111552
111856
  function contains2(root2, path) {
111553
111857
  const boundary = root2.endsWith(sep16) ? root2 : root2 + sep16;
111554
111858
  return path === root2 || path.startsWith(boundary);
@@ -111594,7 +111898,10 @@ function configuredRoots(home, explicitHome) {
111594
111898
  if (recorded && (!executable2 || executable2.target !== recorded.target || onPath && onPath.target !== recorded.target) || !executable2 && (binding.builtinNames?.length || binding.agent === "gemini" && binding.sources.some((source) => basename17(source.path) === "package.json"))) {
111595
111899
  throw new Error("Native discovery runtime target is unresolved or changed");
111596
111900
  }
111597
- roots.push(...resolveAgentDiscoveryExclusionRoots({ home, agent: binding.agent, canonical: physicalPath, retained: binding, executable: executable2, narrowed }));
111901
+ const managedCommand = current.commands?.[agent], managedProfile = current.profiles?.[agent];
111902
+ const supervisor = current.supervisors?.[agent];
111903
+ const managedHook = (binding.agent === "gemini" || binding.agent === "cursor") && typeof managedCommand === "string" && typeof managedProfile === "string" ? { command: managedCommand, profile: managedProfile } : binding.agent === "hermes" && typeof managedCommand === "string" && typeof managedProfile === "string" && supervisor && typeof supervisor === "object" && typeof supervisor.path === "string" && typeof supervisor.runtime === "string" && typeof supervisor.sha256 === "string" ? { command: managedCommand, profile: managedProfile, supervisor: { path: supervisor.path, runtime: supervisor.runtime, sha256: supervisor.sha256 } } : undefined;
111904
+ roots.push(...resolveAgentDiscoveryExclusionRoots({ home, agent: binding.agent, canonical: physicalPath, retained: binding, executable: executable2, narrowed, ...managedHook ? { managedHook } : {} }));
111598
111905
  }
111599
111906
  };
111600
111907
  if (homePolicy)
@@ -111646,11 +111953,18 @@ function assertPortableAuthoringPath(path, options = {}) {
111646
111953
  if (native) {
111647
111954
  throw new Error(`Refusing portable Skills authoring below native discovery root '${native}': ${lexical}. Set HASNA_SKILLS_DIR to a private authoring corpus outside native roots.`);
111648
111955
  }
111649
- const roots = configuredRoots(home, options.homeDir !== undefined);
111650
- for (const value of selected) {
111651
- if (physicalPath(value.path) === physicalPath(join38(home, `.${value.agent}`)))
111652
- continue;
111653
- roots.push(...resolveAgentDiscoveryExclusionRoots({ home, agent: value.agent, nativeHome: value.path, canonical: physicalPath, executable: null }));
111956
+ const roots = [];
111957
+ try {
111958
+ roots.push(...configuredRoots(home, options.homeDir !== undefined));
111959
+ for (const value of selected) {
111960
+ if (physicalPath(value.path) === physicalPath(join38(home, `.${value.agent}`)))
111961
+ continue;
111962
+ roots.push(...resolveAgentDiscoveryExclusionRoots({ home, agent: value.agent, nativeHome: value.path, canonical: physicalPath, executable: null }));
111963
+ }
111964
+ } catch (error) {
111965
+ if (error instanceof NativeDiscoveryUnresolved)
111966
+ throw new Error(portableAuthoringDiscoveryRefusal(error, lexical));
111967
+ throw error;
111654
111968
  }
111655
111969
  for (const root2 of roots) {
111656
111970
  if (candidates.some((candidate2) => [resolve29(root2), physicalPath(root2)].some((nativeRoot) => contains2(nativeRoot, candidate2) || contains2(candidate2, nativeRoot)))) {