@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/THIRD_PARTY_NOTICES +1 -1
- package/bin/index.js +1051 -725
- package/bin/mcp.js +333 -46
- package/bin/migrate.js +1 -1
- package/bin/server.js +1 -1
- package/bin/worker.js +1 -1
- package/dist/cli/commands/portable-skills.d.ts +1 -0
- package/dist/index.js +381 -67
- package/dist/lib/agent-discovery.d.ts +27 -1
- package/dist/lib/authoring-path.d.ts +5 -0
- package/dist/lib/linux-access-acl.d.ts +55 -0
- package/dist/runtime-prerequisites.js +1 -1
- package/dist/sdk/index.js +671 -357
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
106822
|
-
|
|
106823
|
-
|
|
106824
|
-
|
|
106825
|
-
|
|
106826
|
-
|
|
106827
|
-
|
|
106828
|
-
|
|
106829
|
-
|
|
106830
|
-
|
|
106831
|
-
|
|
106832
|
-
|
|
106833
|
-
|
|
106834
|
-
|
|
106835
|
-
|
|
106836
|
-
|
|
106837
|
-
|
|
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
|
-
|
|
107025
|
-
|
|
107026
|
-
|
|
107027
|
-
|
|
107028
|
-
if (
|
|
107029
|
-
|
|
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
|
-
|
|
107032
|
-
|
|
107033
|
-
|
|
107034
|
-
|
|
107035
|
-
|
|
107036
|
-
|
|
107037
|
-
|
|
107038
|
-
|
|
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
|
-
|
|
107041
|
-
|
|
107042
|
-
|
|
107043
|
-
|
|
107044
|
-
|
|
107045
|
-
|
|
107046
|
-
|
|
107047
|
-
|
|
107048
|
-
|
|
107049
|
-
|
|
107050
|
-
|
|
107051
|
-
|
|
107052
|
-
|
|
107053
|
-
|
|
107054
|
-
}
|
|
107055
|
-
|
|
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
|
-
|
|
107104
|
-
|
|
107105
|
-
|
|
107106
|
-
|
|
107107
|
-
|
|
107108
|
-
|
|
107109
|
-
|
|
107110
|
-
|
|
107111
|
-
|
|
107112
|
-
|
|
107113
|
-
|
|
107114
|
-
|
|
107115
|
-
|
|
107116
|
-
|
|
107117
|
-
|
|
107118
|
-
}
|
|
107119
|
-
|
|
107120
|
-
|
|
107121
|
-
|
|
107122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
111650
|
-
|
|
111651
|
-
|
|
111652
|
-
|
|
111653
|
-
|
|
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)))) {
|