@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 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.12 — v2 Phase 12: Persistence Layer v2**
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.12";
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
  }