@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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security allows state management
|
|
3
|
+
*
|
|
4
|
+
* Stores allowed (ignored) security warnings in .omni/security.json
|
|
5
|
+
* This allows users to suppress specific findings they've reviewed and accepted.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import type { FindingType } from "../security/types.js";
|
|
11
|
+
|
|
12
|
+
const OMNI_DIR = ".omni";
|
|
13
|
+
const SECURITY_PATH = `${OMNI_DIR}/security.json`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Security allows state structure
|
|
17
|
+
*/
|
|
18
|
+
export interface SecurityAllowsState {
|
|
19
|
+
/** Schema version */
|
|
20
|
+
version: 1;
|
|
21
|
+
/** Timestamp of last modification */
|
|
22
|
+
modifiedAt: string;
|
|
23
|
+
/** Map of capability ID -> array of allowed finding types */
|
|
24
|
+
allows: Record<string, FindingType[]>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An individual security allow entry
|
|
29
|
+
*/
|
|
30
|
+
export interface SecurityAllow {
|
|
31
|
+
capabilityId: string;
|
|
32
|
+
findingType: FindingType;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_STATE: SecurityAllowsState = {
|
|
36
|
+
version: 1,
|
|
37
|
+
modifiedAt: new Date().toISOString(),
|
|
38
|
+
allows: {},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read the security allows from local state.
|
|
43
|
+
* Returns empty state if no file exists.
|
|
44
|
+
*/
|
|
45
|
+
export async function readSecurityAllows(): Promise<SecurityAllowsState> {
|
|
46
|
+
if (!existsSync(SECURITY_PATH)) {
|
|
47
|
+
return { ...DEFAULT_STATE };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const content = await readFile(SECURITY_PATH, "utf-8");
|
|
52
|
+
const state = JSON.parse(content) as SecurityAllowsState;
|
|
53
|
+
return state;
|
|
54
|
+
} catch {
|
|
55
|
+
return { ...DEFAULT_STATE };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Write security allows to local state.
|
|
61
|
+
*/
|
|
62
|
+
export async function writeSecurityAllows(state: SecurityAllowsState): Promise<void> {
|
|
63
|
+
mkdirSync(OMNI_DIR, { recursive: true });
|
|
64
|
+
state.modifiedAt = new Date().toISOString();
|
|
65
|
+
await writeFile(SECURITY_PATH, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Add an allow for a specific capability and finding type.
|
|
70
|
+
*/
|
|
71
|
+
export async function addSecurityAllow(
|
|
72
|
+
capabilityId: string,
|
|
73
|
+
findingType: FindingType,
|
|
74
|
+
): Promise<boolean> {
|
|
75
|
+
const state = await readSecurityAllows();
|
|
76
|
+
|
|
77
|
+
if (!state.allows[capabilityId]) {
|
|
78
|
+
state.allows[capabilityId] = [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if already allowed
|
|
82
|
+
if (state.allows[capabilityId].includes(findingType)) {
|
|
83
|
+
return false; // Already exists
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
state.allows[capabilityId].push(findingType);
|
|
87
|
+
await writeSecurityAllows(state);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Remove an allow for a specific capability and finding type.
|
|
93
|
+
*/
|
|
94
|
+
export async function removeSecurityAllow(
|
|
95
|
+
capabilityId: string,
|
|
96
|
+
findingType: FindingType,
|
|
97
|
+
): Promise<boolean> {
|
|
98
|
+
const state = await readSecurityAllows();
|
|
99
|
+
|
|
100
|
+
if (!state.allows[capabilityId]) {
|
|
101
|
+
return false; // Doesn't exist
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const index = state.allows[capabilityId].indexOf(findingType);
|
|
105
|
+
if (index === -1) {
|
|
106
|
+
return false; // Doesn't exist
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
state.allows[capabilityId].splice(index, 1);
|
|
110
|
+
|
|
111
|
+
// Clean up empty arrays
|
|
112
|
+
if (state.allows[capabilityId].length === 0) {
|
|
113
|
+
delete state.allows[capabilityId];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await writeSecurityAllows(state);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if a finding type is allowed for a capability.
|
|
122
|
+
*/
|
|
123
|
+
export async function isSecurityAllowed(
|
|
124
|
+
capabilityId: string,
|
|
125
|
+
findingType: FindingType,
|
|
126
|
+
): Promise<boolean> {
|
|
127
|
+
const state = await readSecurityAllows();
|
|
128
|
+
const allows = state.allows[capabilityId];
|
|
129
|
+
if (!allows) return false;
|
|
130
|
+
return allows.includes(findingType);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get all allows for a capability.
|
|
135
|
+
*/
|
|
136
|
+
export async function getCapabilityAllows(capabilityId: string): Promise<FindingType[]> {
|
|
137
|
+
const state = await readSecurityAllows();
|
|
138
|
+
return state.allows[capabilityId] ?? [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get all security allows as a flat list.
|
|
143
|
+
*/
|
|
144
|
+
export async function getAllSecurityAllows(): Promise<SecurityAllow[]> {
|
|
145
|
+
const state = await readSecurityAllows();
|
|
146
|
+
const result: SecurityAllow[] = [];
|
|
147
|
+
|
|
148
|
+
for (const [capabilityId, findingTypes] of Object.entries(state.allows)) {
|
|
149
|
+
for (const findingType of findingTypes) {
|
|
150
|
+
result.push({ capabilityId, findingType });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Clear all allows for a capability.
|
|
159
|
+
*/
|
|
160
|
+
export async function clearCapabilityAllows(capabilityId: string): Promise<boolean> {
|
|
161
|
+
const state = await readSecurityAllows();
|
|
162
|
+
|
|
163
|
+
if (!state.allows[capabilityId]) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
delete state.allows[capabilityId];
|
|
168
|
+
await writeSecurityAllows(state);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Clear all security allows.
|
|
174
|
+
*/
|
|
175
|
+
export async function clearAllSecurityAllows(): Promise<void> {
|
|
176
|
+
const state = { ...DEFAULT_STATE };
|
|
177
|
+
await writeSecurityAllows(state);
|
|
178
|
+
}
|
package/src/sync.ts
CHANGED
|
@@ -27,11 +27,11 @@ export interface SyncOptions {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Install dependencies
|
|
31
|
-
* Only
|
|
30
|
+
* Install dependencies and build TypeScript capabilities in .omni/capabilities/
|
|
31
|
+
* Only processes capabilities that have a package.json
|
|
32
32
|
*/
|
|
33
33
|
export async function installCapabilityDependencies(silent: boolean): Promise<void> {
|
|
34
|
-
const { existsSync, readdirSync } = await import("node:fs");
|
|
34
|
+
const { existsSync, readdirSync, readFileSync } = await import("node:fs");
|
|
35
35
|
const { join } = await import("node:path");
|
|
36
36
|
|
|
37
37
|
const capabilitiesDir = ".omni/capabilities";
|
|
@@ -73,11 +73,7 @@ export async function installCapabilityDependencies(silent: boolean): Promise<vo
|
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
console.log(`Installing dependencies for ${capabilityPath}...`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Prefer Bun if available, otherwise fallback to npm.
|
|
76
|
+
// Install dependencies silently (only show errors)
|
|
81
77
|
await new Promise<void>((resolve, reject) => {
|
|
82
78
|
const useNpmCi = hasNpm && existsSync(join(capabilityPath, "package-lock.json"));
|
|
83
79
|
const cmd = hasBun ? "bun" : "npm";
|
|
@@ -85,14 +81,19 @@ export async function installCapabilityDependencies(silent: boolean): Promise<vo
|
|
|
85
81
|
|
|
86
82
|
const proc = spawn(cmd, args, {
|
|
87
83
|
cwd: capabilityPath,
|
|
88
|
-
stdio:
|
|
84
|
+
stdio: "pipe",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
let stderr = "";
|
|
88
|
+
proc.stderr?.on("data", (data) => {
|
|
89
|
+
stderr += data.toString();
|
|
89
90
|
});
|
|
90
91
|
|
|
91
92
|
proc.on("close", (code) => {
|
|
92
93
|
if (code === 0) {
|
|
93
94
|
resolve();
|
|
94
95
|
} else {
|
|
95
|
-
reject(new Error(`Failed to install dependencies for ${capabilityPath}`));
|
|
96
|
+
reject(new Error(`Failed to install dependencies for ${capabilityPath}:\n${stderr}`));
|
|
96
97
|
}
|
|
97
98
|
});
|
|
98
99
|
|
|
@@ -100,6 +101,57 @@ export async function installCapabilityDependencies(silent: boolean): Promise<vo
|
|
|
100
101
|
reject(error);
|
|
101
102
|
});
|
|
102
103
|
});
|
|
104
|
+
|
|
105
|
+
// Check if capability needs building (has index.ts but no dist/index.js)
|
|
106
|
+
const hasIndexTs = existsSync(join(capabilityPath, "index.ts"));
|
|
107
|
+
const hasBuiltIndex = existsSync(join(capabilityPath, "dist", "index.js"));
|
|
108
|
+
|
|
109
|
+
if (hasIndexTs && !hasBuiltIndex) {
|
|
110
|
+
// Check if package.json has a build script
|
|
111
|
+
let hasBuildScript = false;
|
|
112
|
+
try {
|
|
113
|
+
const pkgJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
114
|
+
hasBuildScript = Boolean(pkgJson.scripts?.build);
|
|
115
|
+
} catch {
|
|
116
|
+
// Ignore parse errors
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (hasBuildScript) {
|
|
120
|
+
// Build silently (only show errors)
|
|
121
|
+
await new Promise<void>((resolve, reject) => {
|
|
122
|
+
const cmd = hasBun ? "bun" : "npm";
|
|
123
|
+
const args = ["run", "build"];
|
|
124
|
+
|
|
125
|
+
const proc = spawn(cmd, args, {
|
|
126
|
+
cwd: capabilityPath,
|
|
127
|
+
stdio: "pipe",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
let stderr = "";
|
|
131
|
+
proc.stderr?.on("data", (data) => {
|
|
132
|
+
stderr += data.toString();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
proc.on("close", (code) => {
|
|
136
|
+
if (code === 0) {
|
|
137
|
+
resolve();
|
|
138
|
+
} else {
|
|
139
|
+
reject(new Error(`Failed to build capability ${capabilityPath}:\n${stderr}`));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
proc.on("error", (error) => {
|
|
144
|
+
reject(error);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
} else if (!silent) {
|
|
148
|
+
// Warn user that capability has TypeScript but no build setup
|
|
149
|
+
console.warn(
|
|
150
|
+
`Warning: Capability at ${capabilityPath} has index.ts but no build script.\n` +
|
|
151
|
+
` Add a "build" script to package.json (e.g., "build": "tsc") to compile TypeScript.`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
103
155
|
}
|
|
104
156
|
}
|
|
105
157
|
|
|
@@ -162,10 +214,6 @@ export async function syncAgentConfiguration(options?: SyncOptions): Promise<Syn
|
|
|
162
214
|
const silent = options?.silent ?? false;
|
|
163
215
|
const adapters = options?.adapters ?? [];
|
|
164
216
|
|
|
165
|
-
if (!silent) {
|
|
166
|
-
console.log("Syncing agent configuration...");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
217
|
const { bundle } = await buildSyncBundle({ silent });
|
|
170
218
|
const capabilities = bundle.capabilities;
|
|
171
219
|
|
|
@@ -173,24 +221,7 @@ export async function syncAgentConfiguration(options?: SyncOptions): Promise<Syn
|
|
|
173
221
|
const previousManifest = await loadManifest();
|
|
174
222
|
const currentCapabilityIds = new Set(capabilities.map((c) => c.id));
|
|
175
223
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
!silent &&
|
|
180
|
-
(cleanupResult.deletedSkills.length > 0 || cleanupResult.deletedRules.length > 0)
|
|
181
|
-
) {
|
|
182
|
-
console.log("Cleaned up stale resources:");
|
|
183
|
-
if (cleanupResult.deletedSkills.length > 0) {
|
|
184
|
-
console.log(
|
|
185
|
-
` - Removed ${cleanupResult.deletedSkills.length} skill(s): ${cleanupResult.deletedSkills.join(", ")}`,
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
if (cleanupResult.deletedRules.length > 0) {
|
|
189
|
-
console.log(
|
|
190
|
-
` - Removed ${cleanupResult.deletedRules.length} rule(s): ${cleanupResult.deletedRules.join(", ")}`,
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
224
|
+
await cleanupStaleResources(previousManifest, currentCapabilityIds);
|
|
194
225
|
|
|
195
226
|
// Call sync hooks for capabilities that have them
|
|
196
227
|
for (const capability of capabilities) {
|
|
@@ -223,7 +254,7 @@ export async function syncAgentConfiguration(options?: SyncOptions): Promise<Syn
|
|
|
223
254
|
mkdirSync(".omni", { recursive: true });
|
|
224
255
|
|
|
225
256
|
// Sync .mcp.json with capability MCP servers (before saving manifest)
|
|
226
|
-
await syncMcpJson(capabilities, previousManifest
|
|
257
|
+
await syncMcpJson(capabilities, previousManifest);
|
|
227
258
|
|
|
228
259
|
// Save updated manifest for future cleanup
|
|
229
260
|
const newManifest = buildManifestFromCapabilities(capabilities);
|
|
@@ -239,24 +270,13 @@ export async function syncAgentConfiguration(options?: SyncOptions): Promise<Syn
|
|
|
239
270
|
|
|
240
271
|
for (const adapter of adapters) {
|
|
241
272
|
try {
|
|
242
|
-
|
|
243
|
-
if (!silent && result.filesWritten.length > 0) {
|
|
244
|
-
console.log(` - ${adapter.displayName}: ${result.filesWritten.length} files`);
|
|
245
|
-
}
|
|
273
|
+
await adapter.sync(bundle, ctx);
|
|
246
274
|
} catch (error) {
|
|
247
275
|
console.error(`Error running ${adapter.displayName} adapter:`, error);
|
|
248
276
|
}
|
|
249
277
|
}
|
|
250
278
|
}
|
|
251
279
|
|
|
252
|
-
if (!silent) {
|
|
253
|
-
console.log("✓ Synced:");
|
|
254
|
-
console.log(` - ${bundle.docs.length} docs, ${bundle.rules.length} rules`);
|
|
255
|
-
if (adapters.length > 0) {
|
|
256
|
-
console.log(` - Provider adapters: ${adapters.map((a) => a.displayName).join(", ")}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
280
|
return {
|
|
261
281
|
capabilities: capabilities.map((c) => c.id),
|
|
262
282
|
skillCount: bundle.skills.length,
|
package/src/types/index.ts
CHANGED
|
@@ -222,16 +222,31 @@ export function isFileSourceConfig(
|
|
|
222
222
|
return config.source.startsWith("file://");
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Source where the version was detected from.
|
|
227
|
+
* Used for debugging and auditing to understand version provenance.
|
|
228
|
+
*/
|
|
229
|
+
export type VersionSource =
|
|
230
|
+
| "capability.toml"
|
|
231
|
+
| "plugin.json"
|
|
232
|
+
| "package.json"
|
|
233
|
+
| "commit"
|
|
234
|
+
| "content_hash";
|
|
235
|
+
|
|
225
236
|
/** Lock file entry for a capability (version tracking) */
|
|
226
237
|
export interface CapabilityLockEntry {
|
|
227
238
|
/** Original source reference */
|
|
228
239
|
source: string;
|
|
229
|
-
/** Version from capability.toml
|
|
240
|
+
/** Version from capability.toml, plugin.json, package.json, or fallback */
|
|
230
241
|
version: string;
|
|
242
|
+
/** Where the version was detected from (for auditing/debugging) */
|
|
243
|
+
version_source?: VersionSource;
|
|
231
244
|
/** For git sources: exact commit hash */
|
|
232
245
|
commit?: string;
|
|
233
246
|
/** Pinned ref if specified */
|
|
234
247
|
ref?: string;
|
|
248
|
+
/** For file sources: SHA-256 hash of content for reproducibility */
|
|
249
|
+
content_hash?: string;
|
|
235
250
|
/** Last update timestamp (ISO 8601) */
|
|
236
251
|
updated_at: string;
|
|
237
252
|
}
|
|
@@ -260,6 +275,40 @@ export interface ProfileConfig {
|
|
|
260
275
|
capabilities?: string[];
|
|
261
276
|
}
|
|
262
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Security scan mode
|
|
280
|
+
* - off: No scanning (default)
|
|
281
|
+
* - warn: Report findings but continue
|
|
282
|
+
* - error: Report findings and fail sync
|
|
283
|
+
*/
|
|
284
|
+
export type SecurityMode = "off" | "warn" | "error";
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Individual scan toggles for security scanning
|
|
288
|
+
*/
|
|
289
|
+
export interface ScanSettings {
|
|
290
|
+
/** Detect suspicious Unicode characters (bidi overrides, zero-width, control chars) */
|
|
291
|
+
unicode?: boolean;
|
|
292
|
+
/** Detect symlinks inside capability directories */
|
|
293
|
+
symlinks?: boolean;
|
|
294
|
+
/** Detect suspicious patterns in scripts/hooks */
|
|
295
|
+
scripts?: boolean;
|
|
296
|
+
/** Detect binary files in content folders */
|
|
297
|
+
binaries?: boolean;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Security configuration section in omni.toml
|
|
302
|
+
*/
|
|
303
|
+
export interface SecurityConfig {
|
|
304
|
+
/** Scan mode: off, warn, or error (default: off) */
|
|
305
|
+
mode?: SecurityMode;
|
|
306
|
+
/** Trusted source patterns (host/org/repo) that skip scanning */
|
|
307
|
+
trusted_sources?: string[];
|
|
308
|
+
/** Individual scan settings */
|
|
309
|
+
scan?: ScanSettings;
|
|
310
|
+
}
|
|
311
|
+
|
|
263
312
|
export interface OmniConfig {
|
|
264
313
|
profiles?: Record<string, ProfileConfig>;
|
|
265
314
|
providers?: {
|
|
@@ -269,6 +318,8 @@ export interface OmniConfig {
|
|
|
269
318
|
capabilities?: CapabilitiesConfig;
|
|
270
319
|
/** MCP server definitions that auto-generate capabilities */
|
|
271
320
|
mcps?: Record<string, McpConfig>;
|
|
321
|
+
/** Security scanning configuration */
|
|
322
|
+
security?: SecurityConfig;
|
|
272
323
|
}
|
|
273
324
|
|
|
274
325
|
// Provider Types
|