novahiz 0.3.4 → 0.3.5

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.
@@ -409,9 +409,13 @@ const GATE_TOOLS = new Set(
409
409
  type RunResult = { status: number; stdout: string; stderr: string; spawnError?: string };
410
410
 
411
411
  // C1: timeout prevents a hung CLI from freezing the whole OpenCode process.
412
+ // Raised 10 s -> 30 s (audit 2026-09-25, MEDIUM): gate/CLI runs legitimately
413
+ // exceeded 10 s on 23-24/09 and the cap turned them into 14 spurious
414
+ // fail-closed refusals. Still bounded, so a truly hung CLI cannot freeze
415
+ // OpenCode for more than 30 s.
412
416
  // C2: maxBuffer caps output; oversized output is treated as a gate failure,
413
417
  // never as truncated-then-allowed.
414
- const RUN_TIMEOUT_MS = 10_000;
418
+ const RUN_TIMEOUT_MS = 30_000;
415
419
  const RUN_MAX_BUFFER = 1_048_576;
416
420
 
417
421
  function run(args: string[], input?: string): RunResult {
package/dist/memory.js CHANGED
@@ -390,6 +390,7 @@ export function listSlots(root) {
390
390
  return ensureMemoryRoot(dir);
391
391
  }
392
392
  export function getSlot(id, root) {
393
+ assertSlotId(id);
393
394
  const dir = root && root.length > 0 ? root : memoryRoot();
394
395
  const index = ensureMemoryRoot(dir);
395
396
  const meta = index.slots.find((slot) => slot.id === id);
@@ -423,6 +424,21 @@ export function rebuildIndex(root) {
423
424
  slots.sort((a, b) => a.id.localeCompare(b.id));
424
425
  return writeIndex(root, { version: 1, updated: nowIso(), slots });
425
426
  }
427
+ const SLOT_ID_MAX_LEN = 128;
428
+ // Audit 2026-09-25, LOW: slot ids are index keys, not paths — they must never
429
+ // carry path segments. getSlot/parseSlotInput already refuse unknown ids via
430
+ // E_SLOT; this adds an explicit format check so a crafted id fails on its own
431
+ // terms (defense in depth, and a clearer error than "slot inconnu").
432
+ export function assertSlotId(id) {
433
+ const hasPathChar = [...id].some((ch) => {
434
+ const c = ch.charCodeAt(0);
435
+ return c === 47 || c === 92 || c === 0;
436
+ });
437
+ if (id.length === 0 || id.length > SLOT_ID_MAX_LEN || id.includes("..") || hasPathChar) {
438
+ throw memError("E_SLOT_ID", `Invalid params: slot id illégal: ${JSON.stringify(id)}`);
439
+ }
440
+ return id;
441
+ }
426
442
  export function parseSlotInput(args) {
427
443
  const title = typeof args?.title === "string" ? args.title : "";
428
444
  const content = typeof args?.content === "string" ? args.content : "";
@@ -435,7 +451,7 @@ export function parseSlotInput(args) {
435
451
  content,
436
452
  description: typeof args?.description === "string" ? args.description : undefined,
437
453
  tags: Array.isArray(args?.tags) ? args.tags.map(String) : undefined,
438
- slotId: typeof args?.slotId === "string" ? args.slotId : undefined,
454
+ slotId: typeof args?.slotId === "string" ? assertSlotId(args.slotId) : undefined,
439
455
  root: typeof args?.root === "string" ? args.root : undefined
440
456
  };
441
457
  }
@@ -87,7 +87,7 @@ function generateOpenCodeJson(configDir, NovahizHome) {
87
87
  mcp: {
88
88
  context7: {
89
89
  type: "local",
90
- command: ["context7-mcp", "--transport", "stdio"],
90
+ command: ["npx", "-y", "@upstash/context7-mcp@4.1.1", "--transport", "stdio"],
91
91
  enabled: true,
92
92
  },
93
93
  narsil: {
@@ -100,7 +100,7 @@ function generateOpenCodeJson(configDir, NovahizHome) {
100
100
  // (scheduler-mcp local venv); no npm install, no template entry.
101
101
  playwright: {
102
102
  type: "local",
103
- command: ["npx", "@playwright/mcp@latest", "--browser=msedge"],
103
+ command: ["npx", "-y", "@playwright/mcp@0.0.82", "--browser=msedge"],
104
104
  enabled: true,
105
105
  },
106
106
  dart: {
@@ -189,10 +189,11 @@ async function main() {
189
189
  log("");
190
190
  log("Installing MCP servers...");
191
191
  const mcpServers = [
192
- { pkg: "@upstash/context7-mcp", bin: "context7-mcp" },
193
192
  { pkg: "narsil-mcp", bin: "narsil-mcp" },
194
193
  { pkg: "security-mcp", bin: "security-mcp" },
195
194
  ];
195
+ // context7 is invoked via `npx -y @upstash/context7-mcp@4.1.1` (pinned in
196
+ // the generated config), so no global shim is installed for it.
196
197
  // `cron` has no npm package: the plugin registers it from catalog/providers.json
197
198
  // (scheduler-mcp local venv, see docs/PROVIDERS.md). Disabled by default in
198
199
  // fresh configs — users opt in explicitly.
@@ -296,10 +296,11 @@ async function main() {
296
296
 
297
297
  // Install MCP servers globally
298
298
  const mcpServers = [
299
- { pkg: "@upstash/context7-mcp", bin: "context7-mcp", name: "context7" },
300
299
  { pkg: "narsil-mcp", bin: "narsil-mcp", name: "narsil" },
301
300
  { pkg: "security-mcp", bin: "security-mcp", name: "security" },
302
301
  ];
302
+ // context7 is invoked via `npx -y @upstash/context7-mcp@4.1.1` (pinned in
303
+ // the generated config), so no global shim is installed for it.
303
304
  // `cron` has no npm package and ships disabled (local scheduler clone only);
304
305
  // enable it after the local setup documented in docs/PROVIDERS.md.
305
306
 
@@ -393,7 +394,7 @@ async function main() {
393
394
  "mcp": {
394
395
  "context7": {
395
396
  "type": "local",
396
- "command": ["context7-mcp", "--transport", "stdio"],
397
+ "command": ["npx", "-y", "@upstash/context7-mcp@4.1.1", "--transport", "stdio"],
397
398
  "enabled": true
398
399
  },
399
400
  "narsil": {
@@ -406,7 +407,7 @@ async function main() {
406
407
  // after the local scheduler clone setup (docs/PROVIDERS.md).
407
408
  "playwright": {
408
409
  "type": "local",
409
- "command": ["npx", "@playwright/mcp@latest", "--browser=msedge"],
410
+ "command": ["npx", "-y", "@playwright/mcp@0.0.82", "--browser=msedge"],
410
411
  "enabled": true
411
412
  },
412
413
  "dart": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novahiz",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Enforce AI agent skills before every file edit: prompt classifier, skill catalog, and pre-edit gate.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/memory.ts CHANGED
@@ -465,6 +465,7 @@ export function listSlots(root?: string): MemoryIndex {
465
465
  }
466
466
 
467
467
  export function getSlot(id: string, root?: string): SlotFile {
468
+ assertSlotId(id);
468
469
  const dir = root && root.length > 0 ? root : memoryRoot();
469
470
  const index = ensureMemoryRoot(dir);
470
471
  const meta = index.slots.find((slot) => slot.id === id);
@@ -499,6 +500,23 @@ export function rebuildIndex(root: string): MemoryIndex {
499
500
  return writeIndex(root, { version: 1, updated: nowIso(), slots });
500
501
  }
501
502
 
503
+ const SLOT_ID_MAX_LEN = 128;
504
+
505
+ // Audit 2026-09-25, LOW: slot ids are index keys, not paths — they must never
506
+ // carry path segments. getSlot/parseSlotInput already refuse unknown ids via
507
+ // E_SLOT; this adds an explicit format check so a crafted id fails on its own
508
+ // terms (defense in depth, and a clearer error than "slot inconnu").
509
+ export function assertSlotId(id: string): string {
510
+ const hasPathChar = [...id].some((ch) => {
511
+ const c = ch.charCodeAt(0);
512
+ return c === 47 || c === 92 || c === 0;
513
+ });
514
+ if (id.length === 0 || id.length > SLOT_ID_MAX_LEN || id.includes("..") || hasPathChar) {
515
+ throw memError("E_SLOT_ID", `Invalid params: slot id illégal: ${JSON.stringify(id)}`);
516
+ }
517
+ return id;
518
+ }
519
+
502
520
  export function parseSlotInput(args: Record<string, unknown>): WriteEntryInput {
503
521
  const title = typeof args?.title === "string" ? args.title : "";
504
522
  const content = typeof args?.content === "string" ? args.content : "";
@@ -509,7 +527,7 @@ export function parseSlotInput(args: Record<string, unknown>): WriteEntryInput {
509
527
  content,
510
528
  description: typeof args?.description === "string" ? args.description : undefined,
511
529
  tags: Array.isArray(args?.tags) ? args.tags.map(String) : undefined,
512
- slotId: typeof args?.slotId === "string" ? args.slotId : undefined,
530
+ slotId: typeof args?.slotId === "string" ? assertSlotId(args.slotId) : undefined,
513
531
  root: typeof args?.root === "string" ? args.root : undefined
514
532
  };
515
533
  }