@omnidev-ai/core 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +190 -7
- package/dist/index.js +773 -91
- package/package.json +1 -1
- package/src/capability/loader.ts +32 -4
- package/src/capability/sources.ts +303 -56
- package/src/index.ts +3 -0
- package/src/mcp-json/manager.ts +0 -7
- package/src/security/index.ts +11 -0
- package/src/security/scanner.ts +563 -0
- package/src/security/types.ts +108 -0
- package/src/state/index.ts +1 -0
- package/src/state/security-allows.ts +178 -0
- package/src/sync.ts +65 -45
- package/src/types/index.ts +52 -1
package/dist/index.js
CHANGED
|
@@ -959,8 +959,18 @@ async function loadCapabilityConfig(capabilityPath) {
|
|
|
959
959
|
return config;
|
|
960
960
|
}
|
|
961
961
|
async function importCapabilityExports(capabilityPath) {
|
|
962
|
-
const
|
|
963
|
-
|
|
962
|
+
const builtIndexPath = join7(capabilityPath, "dist", "index.js");
|
|
963
|
+
const jsIndexPath = join7(capabilityPath, "index.js");
|
|
964
|
+
const tsIndexPath = join7(capabilityPath, "index.ts");
|
|
965
|
+
let indexPath = null;
|
|
966
|
+
if (existsSync8(builtIndexPath)) {
|
|
967
|
+
indexPath = builtIndexPath;
|
|
968
|
+
} else if (existsSync8(jsIndexPath)) {
|
|
969
|
+
indexPath = jsIndexPath;
|
|
970
|
+
} else if (existsSync8(tsIndexPath)) {
|
|
971
|
+
indexPath = tsIndexPath;
|
|
972
|
+
}
|
|
973
|
+
if (!indexPath) {
|
|
964
974
|
return {};
|
|
965
975
|
}
|
|
966
976
|
try {
|
|
@@ -972,6 +982,11 @@ async function importCapabilityExports(capabilityPath) {
|
|
|
972
982
|
if (errorMessage.includes("Cannot find module")) {
|
|
973
983
|
const match = errorMessage.match(/Cannot find module '([^']+)'/);
|
|
974
984
|
const missingModule = match ? match[1] : "unknown";
|
|
985
|
+
if (indexPath === tsIndexPath && missingModule?.endsWith(".js")) {
|
|
986
|
+
throw new Error(`Capability at ${capabilityPath} has TypeScript files but no built output.
|
|
987
|
+
Add a "build" script to package.json (e.g., "build": "tsc") and run it to compile TypeScript.
|
|
988
|
+
The build output should be in dist/index.js.`);
|
|
989
|
+
}
|
|
975
990
|
throw new Error(`Missing dependency '${missingModule}' for capability at ${capabilityPath}.
|
|
976
991
|
If this is a project-specific capability, install dependencies or remove it from .omni/capabilities/`);
|
|
977
992
|
}
|
|
@@ -1634,6 +1649,7 @@ function getActiveProviders(config) {
|
|
|
1634
1649
|
}
|
|
1635
1650
|
|
|
1636
1651
|
// src/capability/sources.ts
|
|
1652
|
+
import { createHash } from "node:crypto";
|
|
1637
1653
|
var OMNI_LOCAL = ".omni";
|
|
1638
1654
|
var SKILL_DIRS = ["skills", "skill"];
|
|
1639
1655
|
var AGENT_DIRS = ["agents", "agent", "subagents", "subagent"];
|
|
@@ -1751,12 +1767,18 @@ function stringifyLockFile(lockFile) {
|
|
|
1751
1767
|
lines.push(`[capabilities.${id}]`);
|
|
1752
1768
|
lines.push(`source = "${entry.source}"`);
|
|
1753
1769
|
lines.push(`version = "${entry.version}"`);
|
|
1770
|
+
if (entry.version_source) {
|
|
1771
|
+
lines.push(`version_source = "${entry.version_source}"`);
|
|
1772
|
+
}
|
|
1754
1773
|
if (entry.commit) {
|
|
1755
1774
|
lines.push(`commit = "${entry.commit}"`);
|
|
1756
1775
|
}
|
|
1757
1776
|
if (entry.ref) {
|
|
1758
1777
|
lines.push(`ref = "${entry.ref}"`);
|
|
1759
1778
|
}
|
|
1779
|
+
if (entry.content_hash) {
|
|
1780
|
+
lines.push(`content_hash = "${entry.content_hash}"`);
|
|
1781
|
+
}
|
|
1760
1782
|
lines.push(`updated_at = "${entry.updated_at}"`);
|
|
1761
1783
|
lines.push("");
|
|
1762
1784
|
}
|
|
@@ -1786,6 +1808,83 @@ async function getRepoCommit(repoPath) {
|
|
|
1786
1808
|
function shortCommit(commit) {
|
|
1787
1809
|
return commit.substring(0, 7);
|
|
1788
1810
|
}
|
|
1811
|
+
function shortContentHash(hash) {
|
|
1812
|
+
return hash.substring(0, 12);
|
|
1813
|
+
}
|
|
1814
|
+
var CONTENT_HASH_EXCLUDES = [
|
|
1815
|
+
".git",
|
|
1816
|
+
"node_modules",
|
|
1817
|
+
".omni",
|
|
1818
|
+
"__pycache__",
|
|
1819
|
+
".pytest_cache",
|
|
1820
|
+
".mypy_cache",
|
|
1821
|
+
"dist",
|
|
1822
|
+
"build",
|
|
1823
|
+
".DS_Store",
|
|
1824
|
+
"Thumbs.db"
|
|
1825
|
+
];
|
|
1826
|
+
async function computeContentHash(dirPath, excludePatterns = CONTENT_HASH_EXCLUDES) {
|
|
1827
|
+
const hash = createHash("sha256");
|
|
1828
|
+
const files = [];
|
|
1829
|
+
async function collectFiles(currentPath, relativeTo) {
|
|
1830
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
1831
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1832
|
+
for (const entry of entries) {
|
|
1833
|
+
const fullPath = join8(currentPath, entry.name);
|
|
1834
|
+
const relativePath = fullPath.slice(relativeTo.length + 1);
|
|
1835
|
+
if (excludePatterns.some((pattern) => entry.name === pattern || relativePath.startsWith(`${pattern}/`))) {
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
if (entry.isDirectory()) {
|
|
1839
|
+
await collectFiles(fullPath, relativeTo);
|
|
1840
|
+
} else if (entry.isFile()) {
|
|
1841
|
+
const content = await readFile9(fullPath);
|
|
1842
|
+
files.push({ relativePath, content });
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
await collectFiles(dirPath, dirPath);
|
|
1847
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
1848
|
+
for (const file of files) {
|
|
1849
|
+
hash.update(file.relativePath);
|
|
1850
|
+
hash.update(file.content);
|
|
1851
|
+
}
|
|
1852
|
+
return hash.digest("hex");
|
|
1853
|
+
}
|
|
1854
|
+
async function detectDisplayVersion(dirPath, fallback, fallbackSource) {
|
|
1855
|
+
const capTomlPath = join8(dirPath, "capability.toml");
|
|
1856
|
+
if (existsSync11(capTomlPath)) {
|
|
1857
|
+
try {
|
|
1858
|
+
const content = await readFile9(capTomlPath, "utf-8");
|
|
1859
|
+
const parsed = parseToml2(content);
|
|
1860
|
+
const capability = parsed["capability"];
|
|
1861
|
+
if (capability?.["version"] && typeof capability["version"] === "string") {
|
|
1862
|
+
return { version: capability["version"], source: "capability.toml" };
|
|
1863
|
+
}
|
|
1864
|
+
} catch {}
|
|
1865
|
+
}
|
|
1866
|
+
const pluginJsonPath = join8(dirPath, ".claude-plugin", "plugin.json");
|
|
1867
|
+
if (existsSync11(pluginJsonPath)) {
|
|
1868
|
+
try {
|
|
1869
|
+
const content = await readFile9(pluginJsonPath, "utf-8");
|
|
1870
|
+
const parsed = JSON.parse(content);
|
|
1871
|
+
if (parsed.version && typeof parsed.version === "string") {
|
|
1872
|
+
return { version: parsed.version, source: "plugin.json" };
|
|
1873
|
+
}
|
|
1874
|
+
} catch {}
|
|
1875
|
+
}
|
|
1876
|
+
const pkgJsonPath = join8(dirPath, "package.json");
|
|
1877
|
+
if (existsSync11(pkgJsonPath)) {
|
|
1878
|
+
try {
|
|
1879
|
+
const content = await readFile9(pkgJsonPath, "utf-8");
|
|
1880
|
+
const parsed = JSON.parse(content);
|
|
1881
|
+
if (parsed.version && typeof parsed.version === "string") {
|
|
1882
|
+
return { version: parsed.version, source: "package.json" };
|
|
1883
|
+
}
|
|
1884
|
+
} catch {}
|
|
1885
|
+
}
|
|
1886
|
+
return { version: fallback, source: fallbackSource };
|
|
1887
|
+
}
|
|
1789
1888
|
async function cloneRepo(gitUrl, targetPath, ref) {
|
|
1790
1889
|
await mkdir(join8(targetPath, ".."), { recursive: true });
|
|
1791
1890
|
const args = ["clone", "--depth", "1"];
|
|
@@ -2054,15 +2153,9 @@ async function fetchGitCapabilitySource(id, config, options) {
|
|
|
2054
2153
|
if (config.path) {
|
|
2055
2154
|
const tempPath = join8(OMNI_LOCAL, "_temp", `${id}-repo`);
|
|
2056
2155
|
if (existsSync11(join8(tempPath, ".git"))) {
|
|
2057
|
-
if (!options?.silent) {
|
|
2058
|
-
console.log(` Checking ${id}...`);
|
|
2059
|
-
}
|
|
2060
2156
|
updated = await fetchRepo(tempPath, config.ref);
|
|
2061
2157
|
commit = await getRepoCommit(tempPath);
|
|
2062
2158
|
} else {
|
|
2063
|
-
if (!options?.silent) {
|
|
2064
|
-
console.log(` Cloning ${id} from ${config.source}...`);
|
|
2065
|
-
}
|
|
2066
2159
|
await mkdir(join8(tempPath, ".."), { recursive: true });
|
|
2067
2160
|
await cloneRepo(gitUrl, tempPath, config.ref);
|
|
2068
2161
|
commit = await getRepoCommit(tempPath);
|
|
@@ -2077,6 +2170,7 @@ async function fetchGitCapabilitySource(id, config, options) {
|
|
|
2077
2170
|
}
|
|
2078
2171
|
await mkdir(join8(targetPath, ".."), { recursive: true });
|
|
2079
2172
|
await cp(sourcePath, targetPath, { recursive: true });
|
|
2173
|
+
await rm(tempPath, { recursive: true });
|
|
2080
2174
|
repoPath = targetPath;
|
|
2081
2175
|
} else {
|
|
2082
2176
|
if (existsSync11(join8(targetPath, ".git"))) {
|
|
@@ -2116,20 +2210,12 @@ async function fetchGitCapabilitySource(id, config, options) {
|
|
|
2116
2210
|
}
|
|
2117
2211
|
}
|
|
2118
2212
|
}
|
|
2119
|
-
|
|
2120
|
-
const pkgJsonPath = join8(repoPath, "package.json");
|
|
2121
|
-
if (existsSync11(pkgJsonPath)) {
|
|
2122
|
-
try {
|
|
2123
|
-
const pkgJson = JSON.parse(await readFile9(pkgJsonPath, "utf-8"));
|
|
2124
|
-
if (pkgJson.version) {
|
|
2125
|
-
version = pkgJson.version;
|
|
2126
|
-
}
|
|
2127
|
-
} catch {}
|
|
2128
|
-
}
|
|
2213
|
+
const versionResult = await detectDisplayVersion(repoPath, shortCommit(commit), "commit");
|
|
2129
2214
|
return {
|
|
2130
2215
|
id,
|
|
2131
2216
|
path: targetPath,
|
|
2132
|
-
version,
|
|
2217
|
+
version: versionResult.version,
|
|
2218
|
+
versionSource: versionResult.source,
|
|
2133
2219
|
commit,
|
|
2134
2220
|
updated,
|
|
2135
2221
|
wrapped: needsWrap
|
|
@@ -2145,8 +2231,14 @@ async function fetchFileCapabilitySource(id, config, options) {
|
|
|
2145
2231
|
if (!sourceStats.isDirectory()) {
|
|
2146
2232
|
throw new Error(`File source must be a directory: ${sourcePath}`);
|
|
2147
2233
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2234
|
+
const contentHash = await computeContentHash(sourcePath);
|
|
2235
|
+
const hasCapToml = existsSync11(join8(sourcePath, "capability.toml"));
|
|
2236
|
+
let needsWrap = false;
|
|
2237
|
+
if (!hasCapToml) {
|
|
2238
|
+
needsWrap = await shouldWrapDirectory(sourcePath);
|
|
2239
|
+
if (!needsWrap) {
|
|
2240
|
+
throw new Error(`No capability.toml found in: ${sourcePath} (and no wrappable content detected)`);
|
|
2241
|
+
}
|
|
2150
2242
|
}
|
|
2151
2243
|
if (!options?.silent) {
|
|
2152
2244
|
console.log(` Copying ${id} from ${sourcePath}...`);
|
|
@@ -2156,26 +2248,86 @@ async function fetchFileCapabilitySource(id, config, options) {
|
|
|
2156
2248
|
}
|
|
2157
2249
|
await mkdir(join8(targetPath, ".."), { recursive: true });
|
|
2158
2250
|
await cp(sourcePath, targetPath, { recursive: true });
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2251
|
+
if (needsWrap) {
|
|
2252
|
+
await normalizeFolderNames(targetPath);
|
|
2253
|
+
const content = await discoverContent(targetPath);
|
|
2254
|
+
await generateFileSourceCapabilityToml(id, config.source, shortContentHash(contentHash), content, targetPath);
|
|
2255
|
+
if (!options?.silent) {
|
|
2256
|
+
const parts = [];
|
|
2257
|
+
if (content.skills.length > 0)
|
|
2258
|
+
parts.push(`${content.skills.length} skills`);
|
|
2259
|
+
if (content.agents.length > 0)
|
|
2260
|
+
parts.push(`${content.agents.length} agents`);
|
|
2261
|
+
if (content.commands.length > 0)
|
|
2262
|
+
parts.push(`${content.commands.length} commands`);
|
|
2263
|
+
if (parts.length > 0) {
|
|
2264
|
+
console.log(` Wrapped: ${parts.join(", ")}`);
|
|
2168
2265
|
}
|
|
2169
|
-
}
|
|
2266
|
+
}
|
|
2170
2267
|
}
|
|
2268
|
+
const versionResult = await detectDisplayVersion(targetPath, shortContentHash(contentHash), "content_hash");
|
|
2171
2269
|
return {
|
|
2172
2270
|
id,
|
|
2173
2271
|
path: targetPath,
|
|
2174
|
-
version,
|
|
2272
|
+
version: versionResult.version,
|
|
2273
|
+
versionSource: versionResult.source,
|
|
2274
|
+
contentHash,
|
|
2175
2275
|
updated: true,
|
|
2176
|
-
wrapped:
|
|
2276
|
+
wrapped: needsWrap
|
|
2177
2277
|
};
|
|
2178
2278
|
}
|
|
2279
|
+
async function generateFileSourceCapabilityToml(id, source, hashVersion, content, targetPath) {
|
|
2280
|
+
const pluginMeta = await parsePluginJson(targetPath);
|
|
2281
|
+
const readmeDesc = await readReadmeDescription(targetPath);
|
|
2282
|
+
let description;
|
|
2283
|
+
if (pluginMeta?.description) {
|
|
2284
|
+
description = pluginMeta.description;
|
|
2285
|
+
} else if (readmeDesc) {
|
|
2286
|
+
description = readmeDesc;
|
|
2287
|
+
} else {
|
|
2288
|
+
const parts = [];
|
|
2289
|
+
if (content.skills.length > 0) {
|
|
2290
|
+
parts.push(`${content.skills.length} skill${content.skills.length > 1 ? "s" : ""}`);
|
|
2291
|
+
}
|
|
2292
|
+
if (content.agents.length > 0) {
|
|
2293
|
+
parts.push(`${content.agents.length} agent${content.agents.length > 1 ? "s" : ""}`);
|
|
2294
|
+
}
|
|
2295
|
+
if (content.commands.length > 0) {
|
|
2296
|
+
parts.push(`${content.commands.length} command${content.commands.length > 1 ? "s" : ""}`);
|
|
2297
|
+
}
|
|
2298
|
+
description = parts.length > 0 ? `${parts.join(", ")}` : `Wrapped from ${source}`;
|
|
2299
|
+
}
|
|
2300
|
+
const name = pluginMeta?.name || `${id} (wrapped)`;
|
|
2301
|
+
const version = pluginMeta?.version || hashVersion;
|
|
2302
|
+
let tomlContent = `# Auto-generated by OmniDev - DO NOT EDIT
|
|
2303
|
+
# This capability was wrapped from a local directory
|
|
2304
|
+
|
|
2305
|
+
[capability]
|
|
2306
|
+
id = "${id}"
|
|
2307
|
+
name = "${name}"
|
|
2308
|
+
version = "${version}"
|
|
2309
|
+
description = "${description}"
|
|
2310
|
+
`;
|
|
2311
|
+
if (pluginMeta?.author?.name || pluginMeta?.author?.email) {
|
|
2312
|
+
tomlContent += `
|
|
2313
|
+
[capability.author]
|
|
2314
|
+
`;
|
|
2315
|
+
if (pluginMeta.author.name) {
|
|
2316
|
+
tomlContent += `name = "${pluginMeta.author.name}"
|
|
2317
|
+
`;
|
|
2318
|
+
}
|
|
2319
|
+
if (pluginMeta.author.email) {
|
|
2320
|
+
tomlContent += `email = "${pluginMeta.author.email}"
|
|
2321
|
+
`;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
tomlContent += `
|
|
2325
|
+
[capability.metadata]
|
|
2326
|
+
wrapped = true
|
|
2327
|
+
source = "${source}"
|
|
2328
|
+
`;
|
|
2329
|
+
await writeFile3(join8(targetPath, "capability.toml"), tomlContent, "utf-8");
|
|
2330
|
+
}
|
|
2179
2331
|
async function fetchCapabilitySource(id, sourceConfig, options) {
|
|
2180
2332
|
const config = parseSourceConfig(sourceConfig);
|
|
2181
2333
|
if (isFileSourceConfig(sourceConfig) || isFileSource(config.source)) {
|
|
@@ -2298,13 +2450,14 @@ async function generateMcpCapabilities(config) {
|
|
|
2298
2450
|
}
|
|
2299
2451
|
async function fetchAllCapabilitySources(config, options) {
|
|
2300
2452
|
await generateMcpCapabilities(config);
|
|
2453
|
+
const tempDir = join8(OMNI_LOCAL, "_temp");
|
|
2454
|
+
if (existsSync11(tempDir)) {
|
|
2455
|
+
await rm(tempDir, { recursive: true });
|
|
2456
|
+
}
|
|
2301
2457
|
const sources = config.capabilities?.sources;
|
|
2302
2458
|
if (!sources || Object.keys(sources).length === 0) {
|
|
2303
2459
|
return [];
|
|
2304
2460
|
}
|
|
2305
|
-
if (!options?.silent) {
|
|
2306
|
-
console.log("Fetching capability sources...");
|
|
2307
|
-
}
|
|
2308
2461
|
const results = [];
|
|
2309
2462
|
const lockFile = await loadLockFile();
|
|
2310
2463
|
let lockUpdated = false;
|
|
@@ -2315,11 +2468,15 @@ async function fetchAllCapabilitySources(config, options) {
|
|
|
2315
2468
|
const lockEntry = {
|
|
2316
2469
|
source: typeof source === "string" ? source : source.source,
|
|
2317
2470
|
version: result.version,
|
|
2471
|
+
version_source: result.versionSource,
|
|
2318
2472
|
updated_at: new Date().toISOString()
|
|
2319
2473
|
};
|
|
2320
2474
|
if (result.commit) {
|
|
2321
2475
|
lockEntry.commit = result.commit;
|
|
2322
2476
|
}
|
|
2477
|
+
if (result.contentHash) {
|
|
2478
|
+
lockEntry.content_hash = result.contentHash;
|
|
2479
|
+
}
|
|
2323
2480
|
if (!isFileSourceConfig(source)) {
|
|
2324
2481
|
const gitConfig = parseSourceConfig(source);
|
|
2325
2482
|
if (gitConfig.ref) {
|
|
@@ -2327,14 +2484,10 @@ async function fetchAllCapabilitySources(config, options) {
|
|
|
2327
2484
|
}
|
|
2328
2485
|
}
|
|
2329
2486
|
const existing = lockFile.capabilities[id];
|
|
2330
|
-
const hasChanged = !existing || existing.commit !== result.commit;
|
|
2487
|
+
const hasChanged = !existing || result.commit && existing.commit !== result.commit || result.contentHash && existing.content_hash !== result.contentHash;
|
|
2331
2488
|
if (hasChanged) {
|
|
2332
2489
|
lockFile.capabilities[id] = lockEntry;
|
|
2333
2490
|
lockUpdated = true;
|
|
2334
|
-
if (!options?.silent && result.updated) {
|
|
2335
|
-
const oldVersion = existing?.version || "new";
|
|
2336
|
-
console.log(` ${result.wrapped ? "+" : "~"} ${id}: ${oldVersion} -> ${result.version}`);
|
|
2337
|
-
}
|
|
2338
2491
|
}
|
|
2339
2492
|
} catch (error) {
|
|
2340
2493
|
console.error(` Failed to fetch ${id}: ${error}`);
|
|
@@ -2343,14 +2496,6 @@ async function fetchAllCapabilitySources(config, options) {
|
|
|
2343
2496
|
if (lockUpdated) {
|
|
2344
2497
|
await saveLockFile(lockFile);
|
|
2345
2498
|
}
|
|
2346
|
-
if (!options?.silent && results.length > 0) {
|
|
2347
|
-
const updated = results.filter((r) => r.updated).length;
|
|
2348
|
-
if (updated > 0) {
|
|
2349
|
-
console.log(` Updated ${updated} capability source(s)`);
|
|
2350
|
-
} else {
|
|
2351
|
-
console.log(` All ${results.length} source(s) up to date`);
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
2499
|
return results;
|
|
2355
2500
|
}
|
|
2356
2501
|
async function checkForUpdates(config) {
|
|
@@ -2728,7 +2873,7 @@ function buildMcpServerConfig(mcp) {
|
|
|
2728
2873
|
}
|
|
2729
2874
|
return config2;
|
|
2730
2875
|
}
|
|
2731
|
-
async function syncMcpJson(capabilities2, previousManifest
|
|
2876
|
+
async function syncMcpJson(capabilities2, previousManifest) {
|
|
2732
2877
|
const mcpJson = await readMcpJson();
|
|
2733
2878
|
const previouslyManagedMcps = new Set;
|
|
2734
2879
|
for (const resources of Object.values(previousManifest.capabilities)) {
|
|
@@ -2739,17 +2884,12 @@ async function syncMcpJson(capabilities2, previousManifest, options = {}) {
|
|
|
2739
2884
|
for (const serverName of previouslyManagedMcps) {
|
|
2740
2885
|
delete mcpJson.mcpServers[serverName];
|
|
2741
2886
|
}
|
|
2742
|
-
let addedCount = 0;
|
|
2743
2887
|
for (const cap of capabilities2) {
|
|
2744
2888
|
if (cap.config.mcp) {
|
|
2745
2889
|
mcpJson.mcpServers[cap.id] = buildMcpServerConfig(cap.config.mcp);
|
|
2746
|
-
addedCount++;
|
|
2747
2890
|
}
|
|
2748
2891
|
}
|
|
2749
2892
|
await writeMcpJson(mcpJson);
|
|
2750
|
-
if (!options.silent) {
|
|
2751
|
-
console.log(` - .mcp.json (${addedCount} MCP server(s))`);
|
|
2752
|
-
}
|
|
2753
2893
|
}
|
|
2754
2894
|
// src/state/manifest.ts
|
|
2755
2895
|
import { existsSync as existsSync15, mkdirSync as mkdirSync2, rmSync } from "node:fs";
|
|
@@ -2858,14 +2998,104 @@ async function isProviderEnabled(providerId) {
|
|
|
2858
2998
|
const current = await readEnabledProviders();
|
|
2859
2999
|
return current.includes(providerId);
|
|
2860
3000
|
}
|
|
3001
|
+
// src/state/security-allows.ts
|
|
3002
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync4 } from "node:fs";
|
|
3003
|
+
import { readFile as readFile15, writeFile as writeFile9 } from "node:fs/promises";
|
|
3004
|
+
var OMNI_DIR = ".omni";
|
|
3005
|
+
var SECURITY_PATH = `${OMNI_DIR}/security.json`;
|
|
3006
|
+
var DEFAULT_STATE = {
|
|
3007
|
+
version: 1,
|
|
3008
|
+
modifiedAt: new Date().toISOString(),
|
|
3009
|
+
allows: {}
|
|
3010
|
+
};
|
|
3011
|
+
async function readSecurityAllows() {
|
|
3012
|
+
if (!existsSync17(SECURITY_PATH)) {
|
|
3013
|
+
return { ...DEFAULT_STATE };
|
|
3014
|
+
}
|
|
3015
|
+
try {
|
|
3016
|
+
const content = await readFile15(SECURITY_PATH, "utf-8");
|
|
3017
|
+
const state = JSON.parse(content);
|
|
3018
|
+
return state;
|
|
3019
|
+
} catch {
|
|
3020
|
+
return { ...DEFAULT_STATE };
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
async function writeSecurityAllows(state) {
|
|
3024
|
+
mkdirSync4(OMNI_DIR, { recursive: true });
|
|
3025
|
+
state.modifiedAt = new Date().toISOString();
|
|
3026
|
+
await writeFile9(SECURITY_PATH, `${JSON.stringify(state, null, 2)}
|
|
3027
|
+
`, "utf-8");
|
|
3028
|
+
}
|
|
3029
|
+
async function addSecurityAllow(capabilityId, findingType) {
|
|
3030
|
+
const state = await readSecurityAllows();
|
|
3031
|
+
if (!state.allows[capabilityId]) {
|
|
3032
|
+
state.allows[capabilityId] = [];
|
|
3033
|
+
}
|
|
3034
|
+
if (state.allows[capabilityId].includes(findingType)) {
|
|
3035
|
+
return false;
|
|
3036
|
+
}
|
|
3037
|
+
state.allows[capabilityId].push(findingType);
|
|
3038
|
+
await writeSecurityAllows(state);
|
|
3039
|
+
return true;
|
|
3040
|
+
}
|
|
3041
|
+
async function removeSecurityAllow(capabilityId, findingType) {
|
|
3042
|
+
const state = await readSecurityAllows();
|
|
3043
|
+
if (!state.allows[capabilityId]) {
|
|
3044
|
+
return false;
|
|
3045
|
+
}
|
|
3046
|
+
const index = state.allows[capabilityId].indexOf(findingType);
|
|
3047
|
+
if (index === -1) {
|
|
3048
|
+
return false;
|
|
3049
|
+
}
|
|
3050
|
+
state.allows[capabilityId].splice(index, 1);
|
|
3051
|
+
if (state.allows[capabilityId].length === 0) {
|
|
3052
|
+
delete state.allows[capabilityId];
|
|
3053
|
+
}
|
|
3054
|
+
await writeSecurityAllows(state);
|
|
3055
|
+
return true;
|
|
3056
|
+
}
|
|
3057
|
+
async function isSecurityAllowed(capabilityId, findingType) {
|
|
3058
|
+
const state = await readSecurityAllows();
|
|
3059
|
+
const allows = state.allows[capabilityId];
|
|
3060
|
+
if (!allows)
|
|
3061
|
+
return false;
|
|
3062
|
+
return allows.includes(findingType);
|
|
3063
|
+
}
|
|
3064
|
+
async function getCapabilityAllows(capabilityId) {
|
|
3065
|
+
const state = await readSecurityAllows();
|
|
3066
|
+
return state.allows[capabilityId] ?? [];
|
|
3067
|
+
}
|
|
3068
|
+
async function getAllSecurityAllows() {
|
|
3069
|
+
const state = await readSecurityAllows();
|
|
3070
|
+
const result = [];
|
|
3071
|
+
for (const [capabilityId, findingTypes] of Object.entries(state.allows)) {
|
|
3072
|
+
for (const findingType of findingTypes) {
|
|
3073
|
+
result.push({ capabilityId, findingType });
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
return result;
|
|
3077
|
+
}
|
|
3078
|
+
async function clearCapabilityAllows(capabilityId) {
|
|
3079
|
+
const state = await readSecurityAllows();
|
|
3080
|
+
if (!state.allows[capabilityId]) {
|
|
3081
|
+
return false;
|
|
3082
|
+
}
|
|
3083
|
+
delete state.allows[capabilityId];
|
|
3084
|
+
await writeSecurityAllows(state);
|
|
3085
|
+
return true;
|
|
3086
|
+
}
|
|
3087
|
+
async function clearAllSecurityAllows() {
|
|
3088
|
+
const state = { ...DEFAULT_STATE };
|
|
3089
|
+
await writeSecurityAllows(state);
|
|
3090
|
+
}
|
|
2861
3091
|
// src/sync.ts
|
|
2862
3092
|
import { spawn as spawn2 } from "node:child_process";
|
|
2863
|
-
import { mkdirSync as
|
|
3093
|
+
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
2864
3094
|
async function installCapabilityDependencies(silent) {
|
|
2865
|
-
const { existsSync:
|
|
3095
|
+
const { existsSync: existsSync18, readdirSync: readdirSync7, readFileSync: readFileSync2 } = await import("node:fs");
|
|
2866
3096
|
const { join: join9 } = await import("node:path");
|
|
2867
3097
|
const capabilitiesDir = ".omni/capabilities";
|
|
2868
|
-
if (!
|
|
3098
|
+
if (!existsSync18(capabilitiesDir)) {
|
|
2869
3099
|
return;
|
|
2870
3100
|
}
|
|
2871
3101
|
const entries = readdirSync7(capabilitiesDir, { withFileTypes: true });
|
|
@@ -2887,31 +3117,70 @@ async function installCapabilityDependencies(silent) {
|
|
|
2887
3117
|
}
|
|
2888
3118
|
const capabilityPath = join9(capabilitiesDir, entry.name);
|
|
2889
3119
|
const packageJsonPath = join9(capabilityPath, "package.json");
|
|
2890
|
-
if (!
|
|
3120
|
+
if (!existsSync18(packageJsonPath)) {
|
|
2891
3121
|
continue;
|
|
2892
3122
|
}
|
|
2893
|
-
if (!silent) {
|
|
2894
|
-
console.log(`Installing dependencies for ${capabilityPath}...`);
|
|
2895
|
-
}
|
|
2896
3123
|
await new Promise((resolve2, reject) => {
|
|
2897
|
-
const useNpmCi = hasNpm &&
|
|
3124
|
+
const useNpmCi = hasNpm && existsSync18(join9(capabilityPath, "package-lock.json"));
|
|
2898
3125
|
const cmd = hasBun ? "bun" : "npm";
|
|
2899
3126
|
const args = hasBun ? ["install"] : useNpmCi ? ["ci"] : ["install"];
|
|
2900
3127
|
const proc = spawn2(cmd, args, {
|
|
2901
3128
|
cwd: capabilityPath,
|
|
2902
|
-
stdio:
|
|
3129
|
+
stdio: "pipe"
|
|
3130
|
+
});
|
|
3131
|
+
let stderr = "";
|
|
3132
|
+
proc.stderr?.on("data", (data) => {
|
|
3133
|
+
stderr += data.toString();
|
|
2903
3134
|
});
|
|
2904
3135
|
proc.on("close", (code) => {
|
|
2905
3136
|
if (code === 0) {
|
|
2906
3137
|
resolve2();
|
|
2907
3138
|
} else {
|
|
2908
|
-
reject(new Error(`Failed to install dependencies for ${capabilityPath}
|
|
3139
|
+
reject(new Error(`Failed to install dependencies for ${capabilityPath}:
|
|
3140
|
+
${stderr}`));
|
|
2909
3141
|
}
|
|
2910
3142
|
});
|
|
2911
3143
|
proc.on("error", (error) => {
|
|
2912
3144
|
reject(error);
|
|
2913
3145
|
});
|
|
2914
3146
|
});
|
|
3147
|
+
const hasIndexTs = existsSync18(join9(capabilityPath, "index.ts"));
|
|
3148
|
+
const hasBuiltIndex = existsSync18(join9(capabilityPath, "dist", "index.js"));
|
|
3149
|
+
if (hasIndexTs && !hasBuiltIndex) {
|
|
3150
|
+
let hasBuildScript = false;
|
|
3151
|
+
try {
|
|
3152
|
+
const pkgJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
3153
|
+
hasBuildScript = Boolean(pkgJson.scripts?.build);
|
|
3154
|
+
} catch {}
|
|
3155
|
+
if (hasBuildScript) {
|
|
3156
|
+
await new Promise((resolve2, reject) => {
|
|
3157
|
+
const cmd = hasBun ? "bun" : "npm";
|
|
3158
|
+
const args = ["run", "build"];
|
|
3159
|
+
const proc = spawn2(cmd, args, {
|
|
3160
|
+
cwd: capabilityPath,
|
|
3161
|
+
stdio: "pipe"
|
|
3162
|
+
});
|
|
3163
|
+
let stderr = "";
|
|
3164
|
+
proc.stderr?.on("data", (data) => {
|
|
3165
|
+
stderr += data.toString();
|
|
3166
|
+
});
|
|
3167
|
+
proc.on("close", (code) => {
|
|
3168
|
+
if (code === 0) {
|
|
3169
|
+
resolve2();
|
|
3170
|
+
} else {
|
|
3171
|
+
reject(new Error(`Failed to build capability ${capabilityPath}:
|
|
3172
|
+
${stderr}`));
|
|
3173
|
+
}
|
|
3174
|
+
});
|
|
3175
|
+
proc.on("error", (error) => {
|
|
3176
|
+
reject(error);
|
|
3177
|
+
});
|
|
3178
|
+
});
|
|
3179
|
+
} else if (!silent) {
|
|
3180
|
+
console.warn(`Warning: Capability at ${capabilityPath} has index.ts but no build script.
|
|
3181
|
+
Add a "build" script to package.json (e.g., "build": "tsc") to compile TypeScript.`);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
2915
3184
|
}
|
|
2916
3185
|
}
|
|
2917
3186
|
async function buildSyncBundle(options) {
|
|
@@ -2945,23 +3214,11 @@ async function buildSyncBundle(options) {
|
|
|
2945
3214
|
async function syncAgentConfiguration(options) {
|
|
2946
3215
|
const silent = options?.silent ?? false;
|
|
2947
3216
|
const adapters = options?.adapters ?? [];
|
|
2948
|
-
if (!silent) {
|
|
2949
|
-
console.log("Syncing agent configuration...");
|
|
2950
|
-
}
|
|
2951
3217
|
const { bundle } = await buildSyncBundle({ silent });
|
|
2952
3218
|
const capabilities2 = bundle.capabilities;
|
|
2953
3219
|
const previousManifest = await loadManifest();
|
|
2954
3220
|
const currentCapabilityIds = new Set(capabilities2.map((c) => c.id));
|
|
2955
|
-
|
|
2956
|
-
if (!silent && (cleanupResult.deletedSkills.length > 0 || cleanupResult.deletedRules.length > 0)) {
|
|
2957
|
-
console.log("Cleaned up stale resources:");
|
|
2958
|
-
if (cleanupResult.deletedSkills.length > 0) {
|
|
2959
|
-
console.log(` - Removed ${cleanupResult.deletedSkills.length} skill(s): ${cleanupResult.deletedSkills.join(", ")}`);
|
|
2960
|
-
}
|
|
2961
|
-
if (cleanupResult.deletedRules.length > 0) {
|
|
2962
|
-
console.log(` - Removed ${cleanupResult.deletedRules.length} rule(s): ${cleanupResult.deletedRules.join(", ")}`);
|
|
2963
|
-
}
|
|
2964
|
-
}
|
|
3221
|
+
await cleanupStaleResources(previousManifest, currentCapabilityIds);
|
|
2965
3222
|
for (const capability of capabilities2) {
|
|
2966
3223
|
const defaultExport = capability.exports.default;
|
|
2967
3224
|
if (defaultExport && typeof defaultExport.sync === "function") {
|
|
@@ -2982,8 +3239,8 @@ async function syncAgentConfiguration(options) {
|
|
|
2982
3239
|
}
|
|
2983
3240
|
}
|
|
2984
3241
|
}
|
|
2985
|
-
|
|
2986
|
-
await syncMcpJson(capabilities2, previousManifest
|
|
3242
|
+
mkdirSync5(".omni", { recursive: true });
|
|
3243
|
+
await syncMcpJson(capabilities2, previousManifest);
|
|
2987
3244
|
const newManifest = buildManifestFromCapabilities(capabilities2);
|
|
2988
3245
|
await saveManifest(newManifest);
|
|
2989
3246
|
if (adapters.length > 0) {
|
|
@@ -2994,22 +3251,12 @@ async function syncAgentConfiguration(options) {
|
|
|
2994
3251
|
};
|
|
2995
3252
|
for (const adapter of adapters) {
|
|
2996
3253
|
try {
|
|
2997
|
-
|
|
2998
|
-
if (!silent && result.filesWritten.length > 0) {
|
|
2999
|
-
console.log(` - ${adapter.displayName}: ${result.filesWritten.length} files`);
|
|
3000
|
-
}
|
|
3254
|
+
await adapter.sync(bundle, ctx);
|
|
3001
3255
|
} catch (error) {
|
|
3002
3256
|
console.error(`Error running ${adapter.displayName} adapter:`, error);
|
|
3003
3257
|
}
|
|
3004
3258
|
}
|
|
3005
3259
|
}
|
|
3006
|
-
if (!silent) {
|
|
3007
|
-
console.log("✓ Synced:");
|
|
3008
|
-
console.log(` - ${bundle.docs.length} docs, ${bundle.rules.length} rules`);
|
|
3009
|
-
if (adapters.length > 0) {
|
|
3010
|
-
console.log(` - Provider adapters: ${adapters.map((a) => a.displayName).join(", ")}`);
|
|
3011
|
-
}
|
|
3012
|
-
}
|
|
3013
3260
|
return {
|
|
3014
3261
|
capabilities: capabilities2.map((c) => c.id),
|
|
3015
3262
|
skillCount: bundle.skills.length,
|
|
@@ -3031,6 +3278,427 @@ function generateInstructionsContent(rules, _docs) {
|
|
|
3031
3278
|
}
|
|
3032
3279
|
return content.trim();
|
|
3033
3280
|
}
|
|
3281
|
+
// src/security/scanner.ts
|
|
3282
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
3283
|
+
import { lstat, readdir as readdir2, readFile as readFile16, readlink, realpath } from "node:fs/promises";
|
|
3284
|
+
import { join as join9, relative, resolve as resolve2 } from "node:path";
|
|
3285
|
+
|
|
3286
|
+
// src/security/types.ts
|
|
3287
|
+
var DEFAULT_SECURITY_CONFIG = {
|
|
3288
|
+
mode: "off",
|
|
3289
|
+
trusted_sources: [],
|
|
3290
|
+
scan: {
|
|
3291
|
+
unicode: true,
|
|
3292
|
+
symlinks: true,
|
|
3293
|
+
scripts: true,
|
|
3294
|
+
binaries: false
|
|
3295
|
+
}
|
|
3296
|
+
};
|
|
3297
|
+
var DEFAULT_SCAN_SETTINGS = {
|
|
3298
|
+
unicode: true,
|
|
3299
|
+
symlinks: true,
|
|
3300
|
+
scripts: true,
|
|
3301
|
+
binaries: false
|
|
3302
|
+
};
|
|
3303
|
+
|
|
3304
|
+
// src/security/scanner.ts
|
|
3305
|
+
var UNICODE_PATTERNS = {
|
|
3306
|
+
bidi: [
|
|
3307
|
+
8234,
|
|
3308
|
+
8235,
|
|
3309
|
+
8236,
|
|
3310
|
+
8237,
|
|
3311
|
+
8238,
|
|
3312
|
+
8294,
|
|
3313
|
+
8295,
|
|
3314
|
+
8296,
|
|
3315
|
+
8297
|
|
3316
|
+
],
|
|
3317
|
+
zeroWidth: [
|
|
3318
|
+
8203,
|
|
3319
|
+
8204,
|
|
3320
|
+
8205,
|
|
3321
|
+
8288,
|
|
3322
|
+
65279
|
|
3323
|
+
],
|
|
3324
|
+
control: [
|
|
3325
|
+
0,
|
|
3326
|
+
1,
|
|
3327
|
+
2,
|
|
3328
|
+
3,
|
|
3329
|
+
4,
|
|
3330
|
+
5,
|
|
3331
|
+
6,
|
|
3332
|
+
7,
|
|
3333
|
+
8,
|
|
3334
|
+
11,
|
|
3335
|
+
12,
|
|
3336
|
+
14,
|
|
3337
|
+
15,
|
|
3338
|
+
16,
|
|
3339
|
+
17,
|
|
3340
|
+
18,
|
|
3341
|
+
19,
|
|
3342
|
+
20,
|
|
3343
|
+
21,
|
|
3344
|
+
22,
|
|
3345
|
+
23,
|
|
3346
|
+
24,
|
|
3347
|
+
25,
|
|
3348
|
+
26,
|
|
3349
|
+
27,
|
|
3350
|
+
28,
|
|
3351
|
+
29,
|
|
3352
|
+
30,
|
|
3353
|
+
31,
|
|
3354
|
+
127
|
|
3355
|
+
]
|
|
3356
|
+
};
|
|
3357
|
+
var SUSPICIOUS_SCRIPT_PATTERNS = [
|
|
3358
|
+
{
|
|
3359
|
+
pattern: /curl\s+.*\|\s*(ba)?sh/i,
|
|
3360
|
+
message: "Piping curl to shell can execute arbitrary remote code",
|
|
3361
|
+
severity: "high"
|
|
3362
|
+
},
|
|
3363
|
+
{
|
|
3364
|
+
pattern: /wget\s+.*\|\s*(ba)?sh/i,
|
|
3365
|
+
message: "Piping wget to shell can execute arbitrary remote code",
|
|
3366
|
+
severity: "high"
|
|
3367
|
+
},
|
|
3368
|
+
{
|
|
3369
|
+
pattern: /eval\s*\(\s*\$\(/,
|
|
3370
|
+
message: "eval with command substitution can be dangerous",
|
|
3371
|
+
severity: "medium"
|
|
3372
|
+
},
|
|
3373
|
+
{
|
|
3374
|
+
pattern: /rm\s+-rf\s+\/($|\s)|rm\s+-rf\s+~($|\s)/,
|
|
3375
|
+
message: "Recursive deletion from root or home directory",
|
|
3376
|
+
severity: "critical"
|
|
3377
|
+
},
|
|
3378
|
+
{
|
|
3379
|
+
pattern: /chmod\s+777/,
|
|
3380
|
+
message: "Setting world-writable permissions",
|
|
3381
|
+
severity: "medium"
|
|
3382
|
+
},
|
|
3383
|
+
{
|
|
3384
|
+
pattern: /\bsudo\b.*>/,
|
|
3385
|
+
message: "Using sudo with output redirection",
|
|
3386
|
+
severity: "medium"
|
|
3387
|
+
},
|
|
3388
|
+
{
|
|
3389
|
+
pattern: /base64\s+-d.*\|\s*(ba)?sh/i,
|
|
3390
|
+
message: "Decoding and executing base64 content",
|
|
3391
|
+
severity: "high"
|
|
3392
|
+
}
|
|
3393
|
+
];
|
|
3394
|
+
var BINARY_EXTENSIONS = new Set([
|
|
3395
|
+
".exe",
|
|
3396
|
+
".dll",
|
|
3397
|
+
".so",
|
|
3398
|
+
".dylib",
|
|
3399
|
+
".bin",
|
|
3400
|
+
".o",
|
|
3401
|
+
".a",
|
|
3402
|
+
".lib",
|
|
3403
|
+
".pyc",
|
|
3404
|
+
".pyo",
|
|
3405
|
+
".class",
|
|
3406
|
+
".jar",
|
|
3407
|
+
".war",
|
|
3408
|
+
".ear",
|
|
3409
|
+
".wasm",
|
|
3410
|
+
".node"
|
|
3411
|
+
]);
|
|
3412
|
+
var TEXT_EXTENSIONS = new Set([
|
|
3413
|
+
".md",
|
|
3414
|
+
".txt",
|
|
3415
|
+
".toml",
|
|
3416
|
+
".yaml",
|
|
3417
|
+
".yml",
|
|
3418
|
+
".json",
|
|
3419
|
+
".js",
|
|
3420
|
+
".ts",
|
|
3421
|
+
".sh",
|
|
3422
|
+
".bash",
|
|
3423
|
+
".zsh",
|
|
3424
|
+
".fish",
|
|
3425
|
+
".py",
|
|
3426
|
+
".rb"
|
|
3427
|
+
]);
|
|
3428
|
+
async function scanFileForUnicode(filePath, relativePath) {
|
|
3429
|
+
const findings = [];
|
|
3430
|
+
try {
|
|
3431
|
+
const content = await readFile16(filePath, "utf-8");
|
|
3432
|
+
const lines = content.split(`
|
|
3433
|
+
`);
|
|
3434
|
+
for (let lineNum = 0;lineNum < lines.length; lineNum++) {
|
|
3435
|
+
const line = lines[lineNum];
|
|
3436
|
+
if (!line)
|
|
3437
|
+
continue;
|
|
3438
|
+
for (let col = 0;col < line.length; col++) {
|
|
3439
|
+
const codePoint = line.codePointAt(col);
|
|
3440
|
+
if (codePoint === undefined)
|
|
3441
|
+
continue;
|
|
3442
|
+
if (UNICODE_PATTERNS.bidi.includes(codePoint)) {
|
|
3443
|
+
findings.push({
|
|
3444
|
+
type: "unicode_bidi",
|
|
3445
|
+
severity: "high",
|
|
3446
|
+
file: relativePath,
|
|
3447
|
+
line: lineNum + 1,
|
|
3448
|
+
column: col + 1,
|
|
3449
|
+
message: "Bidirectional text override character detected",
|
|
3450
|
+
details: `Codepoint U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
if (UNICODE_PATTERNS.zeroWidth.includes(codePoint)) {
|
|
3454
|
+
if (codePoint === 65279 && lineNum === 0 && col === 0)
|
|
3455
|
+
continue;
|
|
3456
|
+
findings.push({
|
|
3457
|
+
type: "unicode_zero_width",
|
|
3458
|
+
severity: "medium",
|
|
3459
|
+
file: relativePath,
|
|
3460
|
+
line: lineNum + 1,
|
|
3461
|
+
column: col + 1,
|
|
3462
|
+
message: "Zero-width character detected",
|
|
3463
|
+
details: `Codepoint U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
if (UNICODE_PATTERNS.control.includes(codePoint)) {
|
|
3467
|
+
findings.push({
|
|
3468
|
+
type: "unicode_control",
|
|
3469
|
+
severity: "medium",
|
|
3470
|
+
file: relativePath,
|
|
3471
|
+
line: lineNum + 1,
|
|
3472
|
+
column: col + 1,
|
|
3473
|
+
message: "Suspicious control character detected",
|
|
3474
|
+
details: `Codepoint U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
} catch {}
|
|
3480
|
+
return findings;
|
|
3481
|
+
}
|
|
3482
|
+
async function scanFileForScripts(filePath, relativePath) {
|
|
3483
|
+
const findings = [];
|
|
3484
|
+
try {
|
|
3485
|
+
const content = await readFile16(filePath, "utf-8");
|
|
3486
|
+
const lines = content.split(`
|
|
3487
|
+
`);
|
|
3488
|
+
for (let lineNum = 0;lineNum < lines.length; lineNum++) {
|
|
3489
|
+
const line = lines[lineNum];
|
|
3490
|
+
if (!line)
|
|
3491
|
+
continue;
|
|
3492
|
+
for (const { pattern, message, severity } of SUSPICIOUS_SCRIPT_PATTERNS) {
|
|
3493
|
+
if (pattern.test(line)) {
|
|
3494
|
+
findings.push({
|
|
3495
|
+
type: "suspicious_script",
|
|
3496
|
+
severity,
|
|
3497
|
+
file: relativePath,
|
|
3498
|
+
line: lineNum + 1,
|
|
3499
|
+
message,
|
|
3500
|
+
details: line.trim().substring(0, 100)
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
} catch {}
|
|
3506
|
+
return findings;
|
|
3507
|
+
}
|
|
3508
|
+
async function checkSymlink(symlinkPath, relativePath, capabilityRoot) {
|
|
3509
|
+
try {
|
|
3510
|
+
const linkTarget = await readlink(symlinkPath);
|
|
3511
|
+
const resolvedTarget = resolve2(join9(symlinkPath, "..", linkTarget));
|
|
3512
|
+
const normalizedRoot = await realpath(capabilityRoot);
|
|
3513
|
+
if (linkTarget.startsWith("/")) {
|
|
3514
|
+
return {
|
|
3515
|
+
type: "symlink_absolute",
|
|
3516
|
+
severity: "high",
|
|
3517
|
+
file: relativePath,
|
|
3518
|
+
message: "Symlink points to an absolute path",
|
|
3519
|
+
details: `Target: ${linkTarget}`
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
const relativeToRoot = relative(normalizedRoot, resolvedTarget);
|
|
3523
|
+
if (relativeToRoot.startsWith("..") || relativeToRoot.startsWith("/")) {
|
|
3524
|
+
return {
|
|
3525
|
+
type: "symlink_escape",
|
|
3526
|
+
severity: "critical",
|
|
3527
|
+
file: relativePath,
|
|
3528
|
+
message: "Symlink escapes capability directory",
|
|
3529
|
+
details: `Resolves to: ${resolvedTarget}`
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
} catch {}
|
|
3533
|
+
return null;
|
|
3534
|
+
}
|
|
3535
|
+
function isBinaryFile(filePath) {
|
|
3536
|
+
const ext = filePath.toLowerCase().substring(filePath.lastIndexOf("."));
|
|
3537
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
3538
|
+
}
|
|
3539
|
+
function isTextFile(filePath) {
|
|
3540
|
+
const ext = filePath.toLowerCase().substring(filePath.lastIndexOf("."));
|
|
3541
|
+
return TEXT_EXTENSIONS.has(ext);
|
|
3542
|
+
}
|
|
3543
|
+
async function scanCapability(capabilityId, capabilityPath, settings = DEFAULT_SCAN_SETTINGS) {
|
|
3544
|
+
const startTime = Date.now();
|
|
3545
|
+
const findings = [];
|
|
3546
|
+
if (!existsSync18(capabilityPath)) {
|
|
3547
|
+
return {
|
|
3548
|
+
capabilityId,
|
|
3549
|
+
path: capabilityPath,
|
|
3550
|
+
findings: [],
|
|
3551
|
+
passed: true,
|
|
3552
|
+
duration: Date.now() - startTime
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
async function scanDirectory(dirPath) {
|
|
3556
|
+
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
3557
|
+
for (const entry of entries) {
|
|
3558
|
+
const fullPath = join9(dirPath, entry.name);
|
|
3559
|
+
const relativePath = relative(capabilityPath, fullPath);
|
|
3560
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "__pycache__") {
|
|
3561
|
+
continue;
|
|
3562
|
+
}
|
|
3563
|
+
const stats = await lstat(fullPath);
|
|
3564
|
+
if (stats.isSymbolicLink() && settings.symlinks) {
|
|
3565
|
+
const finding = await checkSymlink(fullPath, relativePath, capabilityPath);
|
|
3566
|
+
if (finding) {
|
|
3567
|
+
findings.push(finding);
|
|
3568
|
+
}
|
|
3569
|
+
continue;
|
|
3570
|
+
}
|
|
3571
|
+
if (entry.isDirectory()) {
|
|
3572
|
+
await scanDirectory(fullPath);
|
|
3573
|
+
continue;
|
|
3574
|
+
}
|
|
3575
|
+
if (entry.isFile()) {
|
|
3576
|
+
if (settings.binaries && isBinaryFile(fullPath)) {
|
|
3577
|
+
findings.push({
|
|
3578
|
+
type: "binary_file",
|
|
3579
|
+
severity: "low",
|
|
3580
|
+
file: relativePath,
|
|
3581
|
+
message: "Binary file detected in capability"
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
if (isTextFile(fullPath)) {
|
|
3585
|
+
if (settings.unicode) {
|
|
3586
|
+
const unicodeFindings = await scanFileForUnicode(fullPath, relativePath);
|
|
3587
|
+
findings.push(...unicodeFindings);
|
|
3588
|
+
}
|
|
3589
|
+
if (settings.scripts) {
|
|
3590
|
+
const ext = fullPath.toLowerCase().substring(fullPath.lastIndexOf("."));
|
|
3591
|
+
if ([".sh", ".bash", ".zsh", ".fish", ".py", ".rb", ".js", ".ts"].includes(ext)) {
|
|
3592
|
+
const scriptFindings = await scanFileForScripts(fullPath, relativePath);
|
|
3593
|
+
findings.push(...scriptFindings);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
await scanDirectory(capabilityPath);
|
|
3601
|
+
return {
|
|
3602
|
+
capabilityId,
|
|
3603
|
+
path: capabilityPath,
|
|
3604
|
+
findings,
|
|
3605
|
+
passed: findings.length === 0 || findings.every((f) => f.severity === "low"),
|
|
3606
|
+
duration: Date.now() - startTime
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
async function scanCapabilities(capabilities2, config2 = {}) {
|
|
3610
|
+
const settings = {
|
|
3611
|
+
...DEFAULT_SCAN_SETTINGS,
|
|
3612
|
+
...config2.scan
|
|
3613
|
+
};
|
|
3614
|
+
const results = [];
|
|
3615
|
+
for (const cap of capabilities2) {
|
|
3616
|
+
if (config2.trusted_sources && config2.trusted_sources.length > 0) {}
|
|
3617
|
+
const result = await scanCapability(cap.id, cap.path, settings);
|
|
3618
|
+
results.push(result);
|
|
3619
|
+
}
|
|
3620
|
+
const findingsByType = {
|
|
3621
|
+
unicode_bidi: 0,
|
|
3622
|
+
unicode_zero_width: 0,
|
|
3623
|
+
unicode_control: 0,
|
|
3624
|
+
symlink_escape: 0,
|
|
3625
|
+
symlink_absolute: 0,
|
|
3626
|
+
suspicious_script: 0,
|
|
3627
|
+
binary_file: 0
|
|
3628
|
+
};
|
|
3629
|
+
const findingsBySeverity = {
|
|
3630
|
+
low: 0,
|
|
3631
|
+
medium: 0,
|
|
3632
|
+
high: 0,
|
|
3633
|
+
critical: 0
|
|
3634
|
+
};
|
|
3635
|
+
let totalFindings = 0;
|
|
3636
|
+
let capabilitiesWithFindings = 0;
|
|
3637
|
+
for (const result of results) {
|
|
3638
|
+
if (result.findings.length > 0) {
|
|
3639
|
+
capabilitiesWithFindings++;
|
|
3640
|
+
}
|
|
3641
|
+
for (const finding of result.findings) {
|
|
3642
|
+
totalFindings++;
|
|
3643
|
+
findingsByType[finding.type]++;
|
|
3644
|
+
findingsBySeverity[finding.severity]++;
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
return {
|
|
3648
|
+
totalCapabilities: capabilities2.length,
|
|
3649
|
+
capabilitiesWithFindings,
|
|
3650
|
+
totalFindings,
|
|
3651
|
+
findingsByType,
|
|
3652
|
+
findingsBySeverity,
|
|
3653
|
+
results,
|
|
3654
|
+
allPassed: results.every((r) => r.passed)
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3657
|
+
function formatScanResults(summary, verbose = false) {
|
|
3658
|
+
const lines = [];
|
|
3659
|
+
lines.push("Security Scan Results");
|
|
3660
|
+
lines.push("=====================");
|
|
3661
|
+
lines.push("");
|
|
3662
|
+
if (summary.totalFindings === 0) {
|
|
3663
|
+
lines.push("✓ No security issues found");
|
|
3664
|
+
return lines.join(`
|
|
3665
|
+
`);
|
|
3666
|
+
}
|
|
3667
|
+
lines.push(`Found ${summary.totalFindings} issue(s) in ${summary.capabilitiesWithFindings} capability(ies)`);
|
|
3668
|
+
lines.push("");
|
|
3669
|
+
if (summary.findingsBySeverity.critical > 0) {
|
|
3670
|
+
lines.push(` ✗ Critical: ${summary.findingsBySeverity.critical}`);
|
|
3671
|
+
}
|
|
3672
|
+
if (summary.findingsBySeverity.high > 0) {
|
|
3673
|
+
lines.push(` ✗ High: ${summary.findingsBySeverity.high}`);
|
|
3674
|
+
}
|
|
3675
|
+
if (summary.findingsBySeverity.medium > 0) {
|
|
3676
|
+
lines.push(` ! Medium: ${summary.findingsBySeverity.medium}`);
|
|
3677
|
+
}
|
|
3678
|
+
if (summary.findingsBySeverity.low > 0) {
|
|
3679
|
+
lines.push(` · Low: ${summary.findingsBySeverity.low}`);
|
|
3680
|
+
}
|
|
3681
|
+
lines.push("");
|
|
3682
|
+
if (verbose) {
|
|
3683
|
+
for (const result of summary.results) {
|
|
3684
|
+
if (result.findings.length === 0)
|
|
3685
|
+
continue;
|
|
3686
|
+
lines.push(`${result.capabilityId}:`);
|
|
3687
|
+
for (const finding of result.findings) {
|
|
3688
|
+
const location = finding.line ? `:${finding.line}${finding.column ? `:${finding.column}` : ""}` : "";
|
|
3689
|
+
const severity = finding.severity.toUpperCase().padEnd(8);
|
|
3690
|
+
lines.push(` [${severity}] ${finding.file}${location}`);
|
|
3691
|
+
lines.push(` ${finding.message}`);
|
|
3692
|
+
if (finding.details) {
|
|
3693
|
+
lines.push(` ${finding.details}`);
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
lines.push("");
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
return lines.join(`
|
|
3700
|
+
`);
|
|
3701
|
+
}
|
|
3034
3702
|
// src/templates/agents.ts
|
|
3035
3703
|
function generateAgentsTemplate() {
|
|
3036
3704
|
return `# Project Instructions
|
|
@@ -3202,6 +3870,7 @@ function getVersion() {
|
|
|
3202
3870
|
return version;
|
|
3203
3871
|
}
|
|
3204
3872
|
export {
|
|
3873
|
+
writeSecurityAllows,
|
|
3205
3874
|
writeProviderConfig,
|
|
3206
3875
|
writeMcpJson,
|
|
3207
3876
|
writeEnabledProviders,
|
|
@@ -3218,9 +3887,13 @@ export {
|
|
|
3218
3887
|
sourceToGitUrl,
|
|
3219
3888
|
setProfile,
|
|
3220
3889
|
setActiveProfile,
|
|
3890
|
+
scanCapability,
|
|
3891
|
+
scanCapabilities,
|
|
3221
3892
|
saveManifest,
|
|
3222
3893
|
saveLockFile,
|
|
3223
3894
|
resolveEnabledCapabilities,
|
|
3895
|
+
removeSecurityAllow,
|
|
3896
|
+
readSecurityAllows,
|
|
3224
3897
|
readMcpJson,
|
|
3225
3898
|
readEnabledProviders,
|
|
3226
3899
|
readCapabilityIdFromPath,
|
|
@@ -3251,6 +3924,7 @@ export {
|
|
|
3251
3924
|
loadCapability,
|
|
3252
3925
|
loadBaseConfig,
|
|
3253
3926
|
isValidMatcherPattern,
|
|
3927
|
+
isSecurityAllowed,
|
|
3254
3928
|
isProviderEnabled,
|
|
3255
3929
|
isPromptHookEvent,
|
|
3256
3930
|
isMatcherEvent,
|
|
@@ -3271,6 +3945,8 @@ export {
|
|
|
3271
3945
|
getHooksConfigPath,
|
|
3272
3946
|
getEventsWithHooks,
|
|
3273
3947
|
getEnabledCapabilities,
|
|
3948
|
+
getCapabilityAllows,
|
|
3949
|
+
getAllSecurityAllows,
|
|
3274
3950
|
getActiveProviders,
|
|
3275
3951
|
getActiveProfile,
|
|
3276
3952
|
generateSkillTemplate,
|
|
@@ -3281,6 +3957,7 @@ export {
|
|
|
3281
3957
|
generateClaudeTemplate,
|
|
3282
3958
|
generateCapabilityToml2 as generateCapabilityToml,
|
|
3283
3959
|
generateAgentsTemplate,
|
|
3960
|
+
formatScanResults,
|
|
3284
3961
|
findDuplicateCommands,
|
|
3285
3962
|
fetchCapabilitySource,
|
|
3286
3963
|
fetchAllCapabilitySources,
|
|
@@ -3295,6 +3972,8 @@ export {
|
|
|
3295
3972
|
countHooks,
|
|
3296
3973
|
containsOmnidevVariables,
|
|
3297
3974
|
containsClaudeVariables,
|
|
3975
|
+
clearCapabilityAllows,
|
|
3976
|
+
clearAllSecurityAllows,
|
|
3298
3977
|
clearActiveProfileState,
|
|
3299
3978
|
cleanupStaleResources,
|
|
3300
3979
|
checkForUpdates,
|
|
@@ -3303,6 +3982,7 @@ export {
|
|
|
3303
3982
|
buildManifestFromCapabilities,
|
|
3304
3983
|
buildCommand,
|
|
3305
3984
|
buildCapabilityRegistry,
|
|
3985
|
+
addSecurityAllow,
|
|
3306
3986
|
VARIABLE_MAPPINGS,
|
|
3307
3987
|
SESSION_START_MATCHERS,
|
|
3308
3988
|
PROMPT_HOOK_EVENTS,
|
|
@@ -3313,6 +3993,8 @@ export {
|
|
|
3313
3993
|
HOOK_EVENTS,
|
|
3314
3994
|
HOOKS_DIRECTORY,
|
|
3315
3995
|
HOOKS_CONFIG_FILENAME,
|
|
3996
|
+
DEFAULT_SECURITY_CONFIG,
|
|
3997
|
+
DEFAULT_SCAN_SETTINGS,
|
|
3316
3998
|
DEFAULT_PROMPT_TIMEOUT,
|
|
3317
3999
|
DEFAULT_COMMAND_TIMEOUT,
|
|
3318
4000
|
COMMON_TOOL_MATCHERS
|