@maintainabilityai/research-runner 0.1.45 → 0.1.47

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.
@@ -1338,6 +1338,27 @@ const handleKnowledgeCode = async (input) => {
1338
1338
  // Workflow gate consumes this to validate cited paths.
1339
1339
  inventory_paths: inventoryPaths,
1340
1340
  };
1341
+ // Bug-R / R6 + Bug-S / S6 (Codex round-4 hardening) — persist
1342
+ // inventory to the clone cache so knowledge-code-read can strict-
1343
+ // mode validate requested paths against the same list that lands
1344
+ // in the audit chain. Round-3 caught the inventory-persist write
1345
+ // being non-fatal; round-4 flagged that as fail-open — without
1346
+ // a written inventory, knowledge-code-read's path-not-in-inventory
1347
+ // check silently falls through (the file just isn't there to
1348
+ // check against). S6 fix: persist failure is now FATAL. The
1349
+ // brownfield grounding contract requires the inventory to exist;
1350
+ // a skill_call that succeeded without writing it is a coverage lie.
1351
+ try {
1352
+ fs.writeFileSync(path.join(cloneTarget, '.knowledge-code-inventory.json'), JSON.stringify({ inventory_paths: inventoryPaths, sha, cachedAt: new Date().toISOString() }), 'utf8');
1353
+ }
1354
+ catch (err) {
1355
+ return {
1356
+ ok: false,
1357
+ reason: `inventory-persist-failed: ${err instanceof Error ? err.message : String(err)}`,
1358
+ repo: repoSlug,
1359
+ remediation: "knowledge-code MUST persist its file inventory so knowledge-code-read can strict-mode validate file reads against it. Check tmpdir permissions and disk space.",
1360
+ };
1361
+ }
1341
1362
  return {
1342
1363
  ok: true,
1343
1364
  mode: 'brownfield',
@@ -1407,9 +1428,26 @@ const handleKnowledgeCodeRead = async (input) => {
1407
1428
  if (normalized.startsWith('..') || normalized === '..' || normalized.includes(`${path.sep}..${path.sep}`)) {
1408
1429
  return { ok: false, reason: `path-rejected: path-traversal segments forbidden (${filePath} -> ${normalized})` };
1409
1430
  }
1410
- // Reuse the cached clone from knowledge-code; clone fresh if missing
1411
- // (e.g. agent called knowledge-code-read without calling knowledge-
1412
- // code first supported but slower).
1431
+ // Bug-R / R6 (Codex round-3) auth tightening. A prior knowledge-
1432
+ // code call for this (runId, owner, name) MUST have populated the
1433
+ // cache before knowledge-code-read can return content. Closes two
1434
+ // gaps Codex flagged: (1) skill could read any public GitHub repo
1435
+ // by URL alone, (2) audit chain didn't prove the standard
1436
+ // brownfield-grounding pipeline ran before the file read. Test
1437
+ // mode (KNOWLEDGE_CODE_READ_ALLOW_UNCACHED=1) bypasses this for
1438
+ // unit tests that drive the skill directly.
1439
+ const cacheDir = knowledgeCodeCacheDir(runId, gh.owner, gh.name);
1440
+ const metaPath = path.join(cacheDir, '.cache-meta.json');
1441
+ const allowUncached = process.env.KNOWLEDGE_CODE_READ_ALLOW_UNCACHED === '1';
1442
+ if (!allowUncached && !fs.existsSync(metaPath)) {
1443
+ return {
1444
+ ok: false,
1445
+ reason: `no-prior-knowledge-code: knowledge-code-read requires a prior knowledge-code call for ${gh.owner}/${gh.name} in run ${runId}. Call knowledge-code first to clone + classify the repo, then knowledge-code-read can return file contents from the cached clone.`,
1446
+ remediation: "Call `knowledge-code` with the same repoUrl + runId before invoking knowledge-code-read. The audit chain then proves the agent went through brownfield grounding before reading files.",
1447
+ };
1448
+ }
1449
+ // Reuse the cached clone from knowledge-code; clone fresh only in
1450
+ // test mode (allowUncached).
1413
1451
  const cloneResult = ensureClone(runId, repoUrl, ref ?? 'HEAD', gh.owner, gh.name);
1414
1452
  if (!cloneResult.ok) {
1415
1453
  return {
@@ -1419,6 +1457,49 @@ const handleKnowledgeCodeRead = async (input) => {
1419
1457
  remediation: `Could not access clone for ${repoUrl}. Underlying error: ${cloneResult.error ?? 'unknown'}`,
1420
1458
  };
1421
1459
  }
1460
+ // Bug-R / R6 + Bug-S / S6 (Codex round-4 hardening) — validate
1461
+ // the requested path against the inventory persisted by knowledge-
1462
+ // code. Only paths knowledge-code advertised in `inventory_paths`
1463
+ // are readable. Round-3 left missing/malformed inventory as a
1464
+ // fall-through (silent pass); round-4 caught that as fail-open.
1465
+ // S6: missing inventory file or malformed JSON is now a HARD
1466
+ // FAILURE in non-test mode. The cache exists (we passed the
1467
+ // earlier no-cache check), so the inventory file MUST exist —
1468
+ // its absence means knowledge-code didn't complete normally.
1469
+ if (!allowUncached) {
1470
+ const inventoryPath = path.join(cloneResult.path, '.knowledge-code-inventory.json');
1471
+ if (!fs.existsSync(inventoryPath)) {
1472
+ return {
1473
+ ok: false,
1474
+ reason: `inventory-missing: ${gh.owner}/${gh.name}'s clone exists but .knowledge-code-inventory.json is absent. knowledge-code did not complete normally — re-run it before invoking knowledge-code-read.`,
1475
+ };
1476
+ }
1477
+ let inv;
1478
+ try {
1479
+ inv = JSON.parse(fs.readFileSync(inventoryPath, 'utf8'));
1480
+ }
1481
+ catch (err) {
1482
+ return {
1483
+ ok: false,
1484
+ reason: `inventory-malformed: .knowledge-code-inventory.json could not be parsed: ${err instanceof Error ? err.message : String(err)}`,
1485
+ remediation: "Re-run knowledge-code to regenerate the inventory.",
1486
+ };
1487
+ }
1488
+ const allowed = new Set(inv.inventory_paths ?? []);
1489
+ if (allowed.size === 0) {
1490
+ return {
1491
+ ok: false,
1492
+ reason: `inventory-empty: .knowledge-code-inventory.json carries no inventory_paths. knowledge-code likely walked zero files (empty repo? maxFiles=0?). Cannot validate file reads.`,
1493
+ };
1494
+ }
1495
+ if (!allowed.has(normalized)) {
1496
+ return {
1497
+ ok: false,
1498
+ reason: `path-not-in-inventory: ${normalized} is not in the knowledge-code inventory_paths for ${gh.owner}/${gh.name}. The agent can only read files knowledge-code advertised in the chain.`,
1499
+ remediation: "If the file is real but missed by the bounded walk (default maxFiles=200), call knowledge-code with a higher maxFiles before retrying.",
1500
+ };
1501
+ }
1502
+ }
1422
1503
  const absPath = path.join(cloneResult.path, normalized);
1423
1504
  // Final paranoia check — resolve the real path and verify it's still
1424
1505
  // a child of the clone root. Defends against symlink-shaped escapes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainabilityai/research-runner",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "Research + PRD agent runner — orchestrates the Archeologist and PRD pipelines for the MaintainabilityAI governance mesh",
5
5
  "license": "MIT",
6
6
  "author": "MaintainabilityAI",