@hilbras/omninode 2.0.0-alpha.11 → 2.0.0-alpha.13
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/README.md +16 -2
- package/dist/cli/index.js +292 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +78 -11
- package/dist/index.js +212 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ existing intelligence; it does not try to become another model.
|
|
|
18
18
|
|
|
19
19
|
## Status
|
|
20
20
|
|
|
21
|
-
**v2.0.0-alpha.
|
|
21
|
+
**v2.0.0-alpha.13 — v2 Phase 13: Security Hardening**
|
|
22
22
|
(the reliability & interoperability line: architecture → execution →
|
|
23
23
|
protocol → adapters → pipelines → providers → OmniHilbras → memory → reports) (the v2
|
|
24
24
|
roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
|
|
@@ -31,6 +31,20 @@ for the plan and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design).
|
|
|
31
31
|
|
|
32
32
|
### What's new in v2 (so far)
|
|
33
33
|
|
|
34
|
+
- **Phase 13 — Security Hardening**: configuration containing literal
|
|
35
|
+
credentials is **rejected before parsing** (with a redacted, actionable
|
|
36
|
+
error); reports over 256 KiB are rejected rather than persisted; static
|
|
37
|
+
guards keep shell execution out of the codebase; and `omninode security
|
|
38
|
+
audit` reviews a project's posture (environment exposure, external working
|
|
39
|
+
directories, credential references) and states the agent trust model
|
|
40
|
+
plainly — agents run with your OS user's permissions, and OmniNode is not
|
|
41
|
+
a sandbox.
|
|
42
|
+
- **Phase 12 — Persistence Layer v2**: writes are now **fsynced and atomic**
|
|
43
|
+
(temp → fsync → rename → directory fsync), corrupt store files are
|
|
44
|
+
**quarantined instead of lost**, documents carry a **schema version** with
|
|
45
|
+
v1 data migrated in place (`omninode migrate [--check]`), and files from a
|
|
46
|
+
newer OmniNode are refused rather than misread. Details:
|
|
47
|
+
[PERSISTENCE.md](docs/PERSISTENCE.md).
|
|
34
48
|
- **Phase 11 — Planner v2**: the planner now receives the full §15 input —
|
|
35
49
|
task, project context, agent reports, **aggregated findings (consensus and
|
|
36
50
|
conflicts included)**, memory and **constraints** (settable per pipeline
|
|
@@ -359,7 +373,7 @@ required for the core engine.
|
|
|
359
373
|
- [Architecture](docs/ARCHITECTURE.md) — module map, workflow, local state
|
|
360
374
|
- [Security model](docs/SECURITY.md) — secrets, process execution, env isolation, audit log
|
|
361
375
|
- [API stability policy](docs/API_STABILITY.md) and [public API inventory](docs/API.md)
|
|
362
|
-
- [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Memory](docs/MEMORY.md) · [Reports](docs/REPORTS.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
|
|
376
|
+
- [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Memory](docs/MEMORY.md) · [Reports](docs/REPORTS.md) · [Persistence](docs/PERSISTENCE.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
|
|
363
377
|
- [Example project](examples/demo/) — a runnable end-to-end demo (`./demo.sh`)
|
|
364
378
|
|
|
365
379
|
Prefer a container? `docker build -t omninode .` then
|
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
9
|
+
var OMNINODE_VERSION = "2.0.0-alpha.13";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -145,6 +145,39 @@ var appConfigSchema = z.object({
|
|
|
145
145
|
logging: z.object({ level: z.enum(["debug", "info", "warn", "error"]) }).strict().optional()
|
|
146
146
|
}).strict();
|
|
147
147
|
|
|
148
|
+
// src/config/secrets.ts
|
|
149
|
+
var SAFE_KEY_SUFFIXES = ["_env", "_env_var", "_file", "_path", "_ref", "_id", "_url", "_command"];
|
|
150
|
+
var SECRET_KEY = /^\s*([A-Za-z0-9_-]*(?:api[_-]?key|token|password|passwd|secret|credential)[A-Za-z0-9_-]*)\s*:\s*(.*)$/i;
|
|
151
|
+
var SAFE_VALUE = /^\s*(?:\$\{[^}]*\}|\$[A-Z_][A-Z0-9_]*|<[^>]*>|""|''|null|~)\s*$/i;
|
|
152
|
+
function scanForInlineSecrets(source) {
|
|
153
|
+
const findings = [];
|
|
154
|
+
source.split("\n").forEach((line, index) => {
|
|
155
|
+
const trimmed = line.trim();
|
|
156
|
+
if (trimmed.startsWith("#")) return;
|
|
157
|
+
const match = SECRET_KEY.exec(line);
|
|
158
|
+
if (!match) return;
|
|
159
|
+
const key = match[1] ?? "";
|
|
160
|
+
const rawValue = (match[2] ?? "").trim().replace(/,\s*$/, "");
|
|
161
|
+
const keyIsReference = SAFE_KEY_SUFFIXES.some((suffix) => key.toLowerCase().endsWith(suffix));
|
|
162
|
+
if (keyIsReference) return;
|
|
163
|
+
const value = rawValue.replace(/^["']|["']$/g, "");
|
|
164
|
+
if (value.length === 0 || SAFE_VALUE.test(rawValue)) return;
|
|
165
|
+
if (value.length < 8) return;
|
|
166
|
+
findings.push({
|
|
167
|
+
line: index + 1,
|
|
168
|
+
key,
|
|
169
|
+
excerpt: `${key}: ${value.slice(0, 3)}\u2026 (${value.length} chars, redacted)`
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
return findings;
|
|
173
|
+
}
|
|
174
|
+
function describeSecretFindings(file, findings) {
|
|
175
|
+
const lines = findings.map((f) => ` line ${f.line}: ${f.excerpt}`).join("\n");
|
|
176
|
+
return `${file} appears to contain ${findings.length} inline secret(s):
|
|
177
|
+
${lines}
|
|
178
|
+
OmniNode never stores credentials in configuration. Replace each value with an environment-variable reference, e.g. api_key_env_var: MY_API_KEY.`;
|
|
179
|
+
}
|
|
180
|
+
|
|
148
181
|
// src/config/loader.ts
|
|
149
182
|
import { existsSync, readFileSync } from "fs";
|
|
150
183
|
import { parse as parseYaml } from "yaml";
|
|
@@ -181,6 +214,10 @@ function loadConfig(options = {}) {
|
|
|
181
214
|
);
|
|
182
215
|
}
|
|
183
216
|
const raw = readFileSync(path, "utf8");
|
|
217
|
+
const secretFindings = scanForInlineSecrets(raw);
|
|
218
|
+
if (secretFindings.length > 0) {
|
|
219
|
+
throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path, secretFindings));
|
|
220
|
+
}
|
|
184
221
|
const expanded = expandEnvRefs(raw, env, path);
|
|
185
222
|
let data;
|
|
186
223
|
try {
|
|
@@ -2108,51 +2145,119 @@ function registerRoleCommands(program) {
|
|
|
2108
2145
|
}
|
|
2109
2146
|
|
|
2110
2147
|
// src/persistence/json-file-store.ts
|
|
2111
|
-
import { mkdir, readFile, rename,
|
|
2148
|
+
import { open, mkdir, readFile, rename, stat } from "fs/promises";
|
|
2112
2149
|
import { dirname } from "path";
|
|
2150
|
+
var SCHEMA_VERSION = 2;
|
|
2151
|
+
async function writeFileAtomic(target, contents) {
|
|
2152
|
+
const tmp = `${target}.tmp`;
|
|
2153
|
+
await mkdir(dirname(target), { recursive: true });
|
|
2154
|
+
const handle = await open(tmp, "w");
|
|
2155
|
+
try {
|
|
2156
|
+
await handle.writeFile(contents, "utf8");
|
|
2157
|
+
await handle.sync();
|
|
2158
|
+
} finally {
|
|
2159
|
+
await handle.close();
|
|
2160
|
+
}
|
|
2161
|
+
await rename(tmp, target);
|
|
2162
|
+
try {
|
|
2163
|
+
const dir = await open(dirname(target), "r");
|
|
2164
|
+
try {
|
|
2165
|
+
await dir.sync();
|
|
2166
|
+
} finally {
|
|
2167
|
+
await dir.close();
|
|
2168
|
+
}
|
|
2169
|
+
} catch {
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
async function inspectDocument(path) {
|
|
2173
|
+
let raw;
|
|
2174
|
+
try {
|
|
2175
|
+
await stat(path);
|
|
2176
|
+
} catch {
|
|
2177
|
+
return { status: "missing" };
|
|
2178
|
+
}
|
|
2179
|
+
try {
|
|
2180
|
+
raw = await readFile(path, "utf8");
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
return { status: "corrupt", detail: `unreadable: ${error instanceof Error ? error.message : String(error)}` };
|
|
2183
|
+
}
|
|
2184
|
+
if (raw.trim().length === 0) return { status: "empty" };
|
|
2185
|
+
let parsed;
|
|
2186
|
+
try {
|
|
2187
|
+
parsed = JSON.parse(raw);
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
return { status: "corrupt", detail: error instanceof Error ? error.message : "invalid JSON" };
|
|
2190
|
+
}
|
|
2191
|
+
if (Array.isArray(parsed)) return { status: "v1-legacy" };
|
|
2192
|
+
if (parsed === null || typeof parsed !== "object") return { status: "corrupt", detail: "not an object" };
|
|
2193
|
+
const doc = parsed;
|
|
2194
|
+
if (!Array.isArray(doc.items)) return { status: "corrupt", detail: "missing items array" };
|
|
2195
|
+
const version = typeof doc.schemaVersion === "number" ? doc.schemaVersion : 1;
|
|
2196
|
+
if (version > SCHEMA_VERSION) return { status: "future", schemaVersion: version };
|
|
2197
|
+
if (version < SCHEMA_VERSION) return { status: "v1-legacy" };
|
|
2198
|
+
return { status: "current", schemaVersion: version, count: doc.items.length };
|
|
2199
|
+
}
|
|
2200
|
+
async function readDocument(path, onCorrupt, quarantine = true) {
|
|
2201
|
+
const state = await inspectDocument(path);
|
|
2202
|
+
if (state.status === "missing" || state.status === "empty") return [];
|
|
2203
|
+
if (state.status === "future") {
|
|
2204
|
+
const error = new Error(
|
|
2205
|
+
`${path} was written by a newer OmniNode (schema v${state.schemaVersion} > v${SCHEMA_VERSION})`
|
|
2206
|
+
);
|
|
2207
|
+
onCorrupt?.(path, error);
|
|
2208
|
+
return [];
|
|
2209
|
+
}
|
|
2210
|
+
let raw;
|
|
2211
|
+
try {
|
|
2212
|
+
raw = await readFile(path, "utf8");
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
onCorrupt?.(path, error);
|
|
2215
|
+
return [];
|
|
2216
|
+
}
|
|
2217
|
+
try {
|
|
2218
|
+
const parsed = JSON.parse(raw);
|
|
2219
|
+
if (Array.isArray(parsed)) return parsed;
|
|
2220
|
+
return parsed.items ?? [];
|
|
2221
|
+
} catch (error) {
|
|
2222
|
+
if (quarantine) {
|
|
2223
|
+
const quarantinePath = `${path}.corrupt-${Date.now()}`;
|
|
2224
|
+
try {
|
|
2225
|
+
await rename(path, quarantinePath);
|
|
2226
|
+
logger.child({ component: "persistence" }).warn(
|
|
2227
|
+
`Quarantined unreadable store file ${path} \u2192 ${quarantinePath}`
|
|
2228
|
+
);
|
|
2229
|
+
} catch {
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
onCorrupt?.(path, error);
|
|
2233
|
+
return [];
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2113
2236
|
var JsonFileStore = class {
|
|
2114
2237
|
filePath;
|
|
2115
2238
|
schemaVersion;
|
|
2116
2239
|
onCorrupt;
|
|
2240
|
+
quarantine;
|
|
2117
2241
|
log;
|
|
2118
2242
|
queue = Promise.resolve();
|
|
2119
2243
|
constructor(options) {
|
|
2120
2244
|
this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
|
|
2121
|
-
this.schemaVersion = options.schemaVersion ??
|
|
2245
|
+
this.schemaVersion = options.schemaVersion ?? SCHEMA_VERSION;
|
|
2122
2246
|
this.onCorrupt = options.onCorrupt;
|
|
2247
|
+
this.quarantine = options.quarantine ?? true;
|
|
2123
2248
|
this.log = logger.child({ component: "persistence", file: options.fileName });
|
|
2124
2249
|
}
|
|
2125
2250
|
get path() {
|
|
2126
2251
|
return this.filePath;
|
|
2127
2252
|
}
|
|
2128
|
-
/** All items, or [] when missing/corrupt. */
|
|
2253
|
+
/** All items, or [] when missing/migrated-from-v1/corrupt. */
|
|
2129
2254
|
async readAll() {
|
|
2130
|
-
|
|
2131
|
-
try {
|
|
2132
|
-
raw = await readFile(this.filePath, "utf8");
|
|
2133
|
-
} catch {
|
|
2134
|
-
return [];
|
|
2135
|
-
}
|
|
2136
|
-
try {
|
|
2137
|
-
const parsed = JSON.parse(raw);
|
|
2138
|
-
if (Array.isArray(parsed)) return parsed;
|
|
2139
|
-
if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.items)) {
|
|
2140
|
-
return parsed.items;
|
|
2141
|
-
}
|
|
2142
|
-
throw new Error("persisted file has an unrecognized shape");
|
|
2143
|
-
} catch (error) {
|
|
2144
|
-
this.log.warn(`Treating ${this.filePath} as empty \u2014 it could not be parsed.`);
|
|
2145
|
-
this.onCorrupt?.(this.filePath, error);
|
|
2146
|
-
return [];
|
|
2147
|
-
}
|
|
2255
|
+
return readDocument(this.filePath, this.onCorrupt, this.quarantine);
|
|
2148
2256
|
}
|
|
2149
2257
|
async writeAll(items) {
|
|
2150
2258
|
const document = { schemaVersion: this.schemaVersion, items };
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
await writeFile(tmp, `${JSON.stringify(document, null, 2)}
|
|
2154
|
-
`, "utf8");
|
|
2155
|
-
await rename(tmp, this.filePath);
|
|
2259
|
+
await writeFileAtomic(this.filePath, `${JSON.stringify(document, null, 2)}
|
|
2260
|
+
`);
|
|
2156
2261
|
}
|
|
2157
2262
|
/** Serializes an async operation against this file. */
|
|
2158
2263
|
synchronized(operation) {
|
|
@@ -2455,7 +2560,7 @@ function sleep(ms) {
|
|
|
2455
2560
|
}
|
|
2456
2561
|
|
|
2457
2562
|
// src/audit/index.ts
|
|
2458
|
-
import {
|
|
2563
|
+
import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
2459
2564
|
import { dirname as dirname2 } from "path";
|
|
2460
2565
|
var FileAuditLog = class {
|
|
2461
2566
|
filePath;
|
|
@@ -2467,8 +2572,15 @@ var FileAuditLog = class {
|
|
|
2467
2572
|
async record(event) {
|
|
2468
2573
|
try {
|
|
2469
2574
|
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
2470
|
-
await
|
|
2575
|
+
const { open: open2 } = await import("fs/promises");
|
|
2576
|
+
const handle = await open2(this.filePath, "a");
|
|
2577
|
+
try {
|
|
2578
|
+
await handle.appendFile(`${JSON.stringify(event)}
|
|
2471
2579
|
`, "utf8");
|
|
2580
|
+
await handle.sync();
|
|
2581
|
+
} finally {
|
|
2582
|
+
await handle.close();
|
|
2583
|
+
}
|
|
2472
2584
|
} catch (error) {
|
|
2473
2585
|
this.log.warn(
|
|
2474
2586
|
`Audit write failed: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -2477,7 +2589,6 @@ var FileAuditLog = class {
|
|
|
2477
2589
|
}
|
|
2478
2590
|
/** Recent events, oldest first; used by the CLI. */
|
|
2479
2591
|
async recent(limit = 20) {
|
|
2480
|
-
const { readFile: readFile2 } = await import("fs/promises");
|
|
2481
2592
|
let raw;
|
|
2482
2593
|
try {
|
|
2483
2594
|
raw = await readFile2(this.filePath, "utf8");
|
|
@@ -3310,6 +3421,7 @@ function truncate4(text, max) {
|
|
|
3310
3421
|
}
|
|
3311
3422
|
|
|
3312
3423
|
// src/reports/service.ts
|
|
3424
|
+
var MAX_REPORT_BYTES = 256 * 1024;
|
|
3313
3425
|
var ReportService = class {
|
|
3314
3426
|
constructor(store, log = logger) {
|
|
3315
3427
|
this.store = store;
|
|
@@ -3344,6 +3456,16 @@ var ReportService = class {
|
|
|
3344
3456
|
const reports = [];
|
|
3345
3457
|
const rejected = [];
|
|
3346
3458
|
for (const candidate of candidates) {
|
|
3459
|
+
const size = Buffer.byteLength(JSON.stringify(candidate), "utf8");
|
|
3460
|
+
if (size > MAX_REPORT_BYTES) {
|
|
3461
|
+
rejected.push({
|
|
3462
|
+
key: candidate?.id ?? "(unknown)",
|
|
3463
|
+
reason: `report exceeds the ${MAX_REPORT_BYTES} byte limit (${size} bytes)`,
|
|
3464
|
+
issues: []
|
|
3465
|
+
});
|
|
3466
|
+
this.log.warn(`Rejected oversized report (${size} bytes).`);
|
|
3467
|
+
continue;
|
|
3468
|
+
}
|
|
3347
3469
|
const result = validateReport(candidate);
|
|
3348
3470
|
if (result.valid) {
|
|
3349
3471
|
reports.push(result.report);
|
|
@@ -4809,6 +4931,144 @@ function registerAuditCommand(program) {
|
|
|
4809
4931
|
});
|
|
4810
4932
|
}
|
|
4811
4933
|
|
|
4934
|
+
// src/persistence/index.ts
|
|
4935
|
+
var STORE_FILES = [
|
|
4936
|
+
"tasks.json",
|
|
4937
|
+
"pipelines.json",
|
|
4938
|
+
"reports.json",
|
|
4939
|
+
"plans.json",
|
|
4940
|
+
"memory.json"
|
|
4941
|
+
];
|
|
4942
|
+
async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, options = {}) {
|
|
4943
|
+
const reports = [];
|
|
4944
|
+
for (const file of STORE_FILES) {
|
|
4945
|
+
const path = `${directory}/${file}`;
|
|
4946
|
+
const state = await inspectDocument(path);
|
|
4947
|
+
if (state.status === "missing") {
|
|
4948
|
+
reports.push({ file, state: state.status, migrated: false });
|
|
4949
|
+
continue;
|
|
4950
|
+
}
|
|
4951
|
+
if (state.status === "current") {
|
|
4952
|
+
reports.push({ file, state: state.status, before: state.count, after: state.count, migrated: false });
|
|
4953
|
+
continue;
|
|
4954
|
+
}
|
|
4955
|
+
if (state.status === "future") {
|
|
4956
|
+
reports.push({
|
|
4957
|
+
file,
|
|
4958
|
+
state: state.status,
|
|
4959
|
+
migrated: false,
|
|
4960
|
+
note: `written by a newer OmniNode (schema v${state.schemaVersion}); not touched`
|
|
4961
|
+
});
|
|
4962
|
+
continue;
|
|
4963
|
+
}
|
|
4964
|
+
if (state.status === "corrupt") {
|
|
4965
|
+
reports.push({ file, state: state.status, migrated: false, note: state.detail });
|
|
4966
|
+
continue;
|
|
4967
|
+
}
|
|
4968
|
+
const items = await readDocument(path, void 0, false);
|
|
4969
|
+
if (!options.checkOnly) {
|
|
4970
|
+
await writeFileAtomic(path, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
|
|
4971
|
+
`);
|
|
4972
|
+
}
|
|
4973
|
+
const beforeCount = items.length;
|
|
4974
|
+
reports.push({
|
|
4975
|
+
file,
|
|
4976
|
+
state: state.status,
|
|
4977
|
+
before: beforeCount,
|
|
4978
|
+
after: items.length,
|
|
4979
|
+
migrated: !options.checkOnly,
|
|
4980
|
+
...options.checkOnly ? { note: "would migrate" } : {}
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
return reports;
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
// src/cli/commands/migrate.ts
|
|
4987
|
+
function registerMigrateCommand(program) {
|
|
4988
|
+
program.command("migrate").description(
|
|
4989
|
+
`Inspect and upgrade persisted store files to schema v${SCHEMA_VERSION} (v1 data is migrated in place).`
|
|
4990
|
+
).option("--check", "Only report what would change; do not write.").action(async (options) => {
|
|
4991
|
+
const reports = await migrateProjectStores(`${process.cwd()}/.omninode`, {
|
|
4992
|
+
checkOnly: options.check ?? false
|
|
4993
|
+
});
|
|
4994
|
+
let changed = 0;
|
|
4995
|
+
for (const report of reports) {
|
|
4996
|
+
if (report.migrated || report.state === "v1-legacy") changed += 1;
|
|
4997
|
+
const detail = report.state === "corrupt" ? ` (corrupt: ${report.note})` : report.state === "future" ? ` (${report.note})` : report.state === "v1-legacy" ? ` ${report.migrated ? "migrated" : "would migrate"} ${report.before ?? 0} \u2192 ${report.after ?? 0} item(s)` : report.state === "current" ? ` ${report.after ?? 0} item(s)` : "";
|
|
4998
|
+
console.log(`${report.file.padEnd(16)} ${report.state}${detail}`);
|
|
4999
|
+
}
|
|
5000
|
+
console.log(
|
|
5001
|
+
changed === 0 ? `All stores are at schema v${SCHEMA_VERSION}.` : `${options.check ? "Would migrate" : "Migrated"} ${changed} store file(s) to schema v${SCHEMA_VERSION}.`
|
|
5002
|
+
);
|
|
5003
|
+
});
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
// src/cli/commands/security.ts
|
|
5007
|
+
function registerSecurityCommand(program) {
|
|
5008
|
+
program.command("security").description("Inspect a project's security posture (advisory).").argument("[action]", "Action to perform.", "audit").action(async (action) => {
|
|
5009
|
+
if (action !== "audit") {
|
|
5010
|
+
console.log(`Unknown security action "${action}". Available: audit.`);
|
|
5011
|
+
return;
|
|
5012
|
+
}
|
|
5013
|
+
const config = loadConfig();
|
|
5014
|
+
const findings = [];
|
|
5015
|
+
for (const agent of config.project.agents) {
|
|
5016
|
+
const policy = resolveEnvPolicy(agent);
|
|
5017
|
+
if (policy === "inherit") {
|
|
5018
|
+
findings.push({
|
|
5019
|
+
severity: "warn",
|
|
5020
|
+
subject: `agent "${agent.name}"`,
|
|
5021
|
+
message: "inherits the full environment \u2014 provider keys in OmniNode's environment are visible to it. Consider env_policy: allowlist|denylist|explicit."
|
|
5022
|
+
});
|
|
5023
|
+
}
|
|
5024
|
+
if (agent.allowExternalCwd) {
|
|
5025
|
+
findings.push({
|
|
5026
|
+
severity: "info",
|
|
5027
|
+
subject: `agent "${agent.name}"`,
|
|
5028
|
+
message: "may run outside the project root (allow_external_cwd)."
|
|
5029
|
+
});
|
|
5030
|
+
}
|
|
5031
|
+
if (agent.env && Object.keys(agent.env).length > 0) {
|
|
5032
|
+
findings.push({
|
|
5033
|
+
severity: "info",
|
|
5034
|
+
subject: `agent "${agent.name}"`,
|
|
5035
|
+
message: `receives ${Object.keys(agent.env).length} explicit environment variable(s).`
|
|
5036
|
+
});
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
for (const provider of config.project.providers) {
|
|
5040
|
+
if (!provider.apiKeyEnvVar) {
|
|
5041
|
+
findings.push({
|
|
5042
|
+
severity: "info",
|
|
5043
|
+
subject: `provider "${provider.name}"`,
|
|
5044
|
+
message: "no api_key_env_var \u2014 fine for keyless local endpoints, otherwise credentials cannot be resolved."
|
|
5045
|
+
});
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
if (config.project.memory?.provider === "remembera" && !config.project.memory.apiKeyEnvVar) {
|
|
5049
|
+
findings.push({
|
|
5050
|
+
severity: "warn",
|
|
5051
|
+
subject: "memory",
|
|
5052
|
+
message: "Remembera is configured without api_key_env_var; requests will be unauthenticated."
|
|
5053
|
+
});
|
|
5054
|
+
}
|
|
5055
|
+
console.log(`OmniNode security audit \u2014 project "${config.project.name}"`);
|
|
5056
|
+
if (findings.length === 0) {
|
|
5057
|
+
console.log(" no findings.");
|
|
5058
|
+
}
|
|
5059
|
+
for (const finding of findings) {
|
|
5060
|
+
console.log(` [${finding.severity.toUpperCase()}] ${finding.subject}: ${finding.message}`);
|
|
5061
|
+
}
|
|
5062
|
+
console.log(
|
|
5063
|
+
`
|
|
5064
|
+
Trust model: agents execute with the permissions of the OS user running OmniNode.`
|
|
5065
|
+
);
|
|
5066
|
+
console.log(
|
|
5067
|
+
`OmniNode does not sandbox agents \u2014 run untrusted agents in a container or as a dedicated user.`
|
|
5068
|
+
);
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
|
|
4812
5072
|
// src/cli/index.ts
|
|
4813
5073
|
function createProgram() {
|
|
4814
5074
|
const program = new Command();
|
|
@@ -4828,6 +5088,8 @@ function createProgram() {
|
|
|
4828
5088
|
registerPlanCommands(program);
|
|
4829
5089
|
registerStatusCommand(program);
|
|
4830
5090
|
registerAuditCommand(program);
|
|
5091
|
+
registerMigrateCommand(program);
|
|
5092
|
+
registerSecurityCommand(program);
|
|
4831
5093
|
registerRunAlias(program);
|
|
4832
5094
|
return program;
|
|
4833
5095
|
}
|