Files
NetMesh/electron/bridges/aiBridge/sdk/netcattySkillsOpenCodePermissions.cjs

190 lines
5.9 KiB
JavaScript
Raw Normal View History

"use strict";
const fs = require("node:fs");
const path = require("node:path");
function normalizeOpenCodePath(targetPath, platform = process.platform) {
return platform === "win32"
? targetPath.replace(/\\/g, "/")
: targetPath;
}
function appendOpenCodePathPattern(baseDir, suffix) {
const trimmedSuffix = suffix.replace(/^\//, "");
return baseDir.endsWith("/")
? `${baseDir}${trimmedSuffix}`
: `${baseDir}/${trimmedSuffix}`;
}
function toOpenCodeDirectoryBase(dirPath, options = {}) {
if (!dirPath || typeof dirPath !== "string") return null;
const pathModule = options.pathModule || path;
const platform = options.platform || process.platform;
try {
const resolved = pathModule.resolve(dirPath);
let baseDir = resolved;
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
baseDir = pathModule.dirname(resolved);
}
return normalizeOpenCodePath(baseDir, platform);
} catch {
return null;
}
}
function toOpenCodeDirectoryGlob(dirPath, options = {}) {
const baseDir = toOpenCodeDirectoryBase(dirPath, options);
return baseDir ? appendOpenCodePathPattern(baseDir, "**") : null;
}
function toOpenCodeDirectoryPermissionPatterns(dirPath, options = {}) {
const baseDir = toOpenCodeDirectoryBase(dirPath, options);
return baseDir
? [
baseDir,
appendOpenCodePathPattern(baseDir, "*"),
appendOpenCodePathPattern(baseDir, "**"),
]
: [];
}
function toOpenCodeFileParentGlob(filePath, options = {}) {
if (!filePath || typeof filePath !== "string") return null;
const pathModule = options.pathModule || path;
try {
return toOpenCodeDirectoryGlob(pathModule.dirname(pathModule.resolve(filePath)), options);
} catch {
return null;
}
}
function toOpenCodeFileParentPermissionPatterns(filePath, options = {}) {
if (!filePath || typeof filePath !== "string") return [];
const pathModule = options.pathModule || path;
try {
return toOpenCodeDirectoryPermissionPatterns(pathModule.dirname(pathModule.resolve(filePath)), options);
} catch {
return [];
}
}
function dedupePatterns(patterns) {
return [...new Set(patterns.filter(Boolean))];
}
// OpenCode discovers native agent skills from these well-known directories:
// its global config dirs (~/.opencode and ~/.config/opencode, both "skill"
// and "skills" spellings), Claude/agents-compatible dirs, project-level
// .opencode/.claude/.agents dirs, and the remote-skill download cache.
// Reads inside them must stay allowed even though Netcatty otherwise locks
// external directory access down, or loading a skill's reference files fails
// with an OpenCode permission error (issue #1939).
const OPENCODE_NATIVE_SKILL_DIR_SUFFIXES = [
".opencode/skill",
".opencode/skills",
".config/opencode/skill",
".config/opencode/skills",
".claude/skills",
".agents/skills",
".cache/opencode/skills",
];
// OpenCode's `read` permission checks match worktree-relative paths (e.g.
// "../../.opencode/skills/foo/references/doc.md") while `external_directory`
// checks match absolute directory globs ("C:/Users/me/.opencode/skills/foo/*").
// Anchoring each well-known suffix behind a leading wildcard covers both
// forms on every platform (OpenCode normalizes "\\" to "/" before matching).
function buildOpenCodeNativeSkillPermissionPatterns() {
return OPENCODE_NATIVE_SKILL_DIR_SUFFIXES.flatMap((suffix) => [
`*${suffix}`,
`*${suffix}/*`,
`*${suffix}/**`,
]);
}
// OpenCode's default rules gate `.env` secret files behind approval. The
// broad skill-directory read allows above would win over those defaults
// (last matching rule wins), so re-deny dot-env files inside skill dirs
// after the allow entries to keep secret-file protection intact.
function buildOpenCodeNativeSkillEnvDenyPatterns() {
return OPENCODE_NATIVE_SKILL_DIR_SUFFIXES.flatMap((suffix) => [
`*${suffix}/**.env`,
`*${suffix}/**.env.*`,
]);
}
// Base rules shared by every tool-integration mode so OpenCode's native
// skills keep working: allow loading skills and reading their files while
// still denying all other external directory access.
function buildOpenCodeNativeSkillsPermissionRules() {
const external_directory = { "*": "deny" };
const read = {};
for (const pattern of buildOpenCodeNativeSkillPermissionPatterns()) {
external_directory[pattern] = "allow";
read[pattern] = "allow";
}
for (const pattern of buildOpenCodeNativeSkillEnvDenyPatterns()) {
read[pattern] = "deny";
}
return {
skill: "allow",
read,
external_directory,
};
}
function buildNetcattySkillsOpenCodePathAllowlist({
launcherPath,
cliScriptPath,
skillPath,
discoveryFilePath,
cliStateDir,
runtimeBinaryPath,
tempDir,
extraFilePaths,
} = {}, options = {}) {
const filePaths = [
launcherPath,
cliScriptPath,
skillPath,
discoveryFilePath,
runtimeBinaryPath,
...(Array.isArray(extraFilePaths) ? extraFilePaths : []),
];
return dedupePatterns([
...filePaths.flatMap((filePath) => toOpenCodeFileParentPermissionPatterns(filePath, options)),
...(cliStateDir ? toOpenCodeDirectoryPermissionPatterns(cliStateDir, options) : []),
...(tempDir ? toOpenCodeDirectoryPermissionPatterns(tempDir, options) : []),
]);
}
function buildOpenCodeSkillsPermissionRules(pathAllowlist = []) {
const { read, external_directory } = buildOpenCodeNativeSkillsPermissionRules();
for (const pattern of pathAllowlist) {
external_directory[pattern] = "allow";
read[pattern] = "allow";
}
return {
bash: "allow",
read,
list: "deny",
glob: "deny",
grep: "deny",
skill: "allow",
external_directory,
};
}
module.exports = {
buildNetcattySkillsOpenCodePathAllowlist,
buildOpenCodeNativeSkillEnvDenyPatterns,
buildOpenCodeNativeSkillPermissionPatterns,
buildOpenCodeNativeSkillsPermissionRules,
buildOpenCodeSkillsPermissionRules,
toOpenCodeDirectoryPermissionPatterns,
toOpenCodeDirectoryGlob,
toOpenCodeFileParentPermissionPatterns,
toOpenCodeFileParentGlob,
};