@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.
@@ -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 for capabilities in .omni/capabilities/
31
- * Only installs for capabilities that have a package.json
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
- if (!silent) {
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: silent ? "ignore" : "inherit",
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
- const cleanupResult = await cleanupStaleResources(previousManifest, currentCapabilityIds);
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, { silent });
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
- const result = await adapter.sync(bundle, ctx);
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,
@@ -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 or package.json */
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