@hilbras/omninode 2.0.0-alpha.12 → 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 +9 -1
- package/dist/cli/index.js +116 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +24 -2
- package/dist/index.js +52 -1
- 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,14 @@ 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.
|
|
34
42
|
- **Phase 12 — Persistence Layer v2**: writes are now **fsynced and atomic**
|
|
35
43
|
(temp → fsync → rename → directory fsync), corrupt store files are
|
|
36
44
|
**quarantined instead of lost**, documents carry a **schema version** with
|
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 {
|
|
@@ -3384,6 +3421,7 @@ function truncate4(text, max) {
|
|
|
3384
3421
|
}
|
|
3385
3422
|
|
|
3386
3423
|
// src/reports/service.ts
|
|
3424
|
+
var MAX_REPORT_BYTES = 256 * 1024;
|
|
3387
3425
|
var ReportService = class {
|
|
3388
3426
|
constructor(store, log = logger) {
|
|
3389
3427
|
this.store = store;
|
|
@@ -3418,6 +3456,16 @@ var ReportService = class {
|
|
|
3418
3456
|
const reports = [];
|
|
3419
3457
|
const rejected = [];
|
|
3420
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
|
+
}
|
|
3421
3469
|
const result = validateReport(candidate);
|
|
3422
3470
|
if (result.valid) {
|
|
3423
3471
|
reports.push(result.report);
|
|
@@ -4955,6 +5003,72 @@ function registerMigrateCommand(program) {
|
|
|
4955
5003
|
});
|
|
4956
5004
|
}
|
|
4957
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
|
+
|
|
4958
5072
|
// src/cli/index.ts
|
|
4959
5073
|
function createProgram() {
|
|
4960
5074
|
const program = new Command();
|
|
@@ -4975,6 +5089,7 @@ function createProgram() {
|
|
|
4975
5089
|
registerStatusCommand(program);
|
|
4976
5090
|
registerAuditCommand(program);
|
|
4977
5091
|
registerMigrateCommand(program);
|
|
5092
|
+
registerSecurityCommand(program);
|
|
4978
5093
|
registerRunAlias(program);
|
|
4979
5094
|
return program;
|
|
4980
5095
|
}
|