190 lines
5.9 KiB
JavaScript
190 lines
5.9 KiB
JavaScript
|
|
"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,
|
||
|
|
};
|