| 486 | } |
| 487 | |
| 488 | bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir, |
| 489 | const char *cache_dir, const char *configured_root, char *err, |
| 490 | size_t err_sz) { |
| 491 | if (err && err_sz) { |
| 492 | err[0] = '\0'; |
| 493 | } |
| 494 | if (!canonical_path || !canonical_path[0]) { |
| 495 | if (err) { |
| 496 | snprintf(err, err_sz, "no repository path given"); |
| 497 | } |
| 498 | return false; |
| 499 | } |
| 500 | |
| 501 | ws_match_t match = {canonical_path, false, false}; |
| 502 | int grants = ws_grant_walk(cache_dir, ws_match_visit, &match); |
| 503 | |
| 504 | /* A configured root behaves as an additional grant so existing |
| 505 | * CBM_ALLOWED_ROOT deployments keep working unchanged. */ |
| 506 | bool configured = configured_root && configured_root[0]; |
| 507 | bool configured_contains = configured && cbm_path_within_root(configured_root, canonical_path); |
| 508 | |
| 509 | /* Containment first when a boundary has actually been declared. A path |
| 510 | * outside a configured root is best explained as exactly that, and the |
| 511 | * wording is what callers and the shell contracts already match on. Breadth |
| 512 | * then applies to paths that ARE inside the declared root but are still too |
| 513 | * broad to index as one unit. */ |
| 514 | bool boundary_declared = grants > 0 || configured; |
| 515 | /* No manifest consultation here, deliberately. |
| 516 | * |
| 517 | * A project's manifest authorizes outside roots FOR THAT PROJECT, so the |
| 518 | * question it answers is "may project P pull in tree T", not "may T be indexed |
| 519 | * standalone". This function only ever sees one path, so it has no project |
| 520 | * context to ask that question with — an earlier draft passed the candidate as |
| 521 | * its own project root, which read a manifest that by definition was not the |
| 522 | * one that requested it and so never authorized anything. |
| 523 | * |
| 524 | * The consuming half belongs where the project context exists: discovery |
| 525 | * walking a project's approved extra roots. cbm_workspace_manifest_allows is |
| 526 | * the query that half will use. */ |
| 527 | if (boundary_declared && !match.contained && !configured_contains) { |
| 528 | if (err) { |
| 529 | /* Keep the "outside the allowed root" wording: changing it broke an |
| 530 | * assertion whose early return then leaked CBM_ALLOWED_ROOT into |
| 531 | * every later test in that suite. Guidance is appended, not |
| 532 | * substituted. */ |
| 533 | snprintf(err, err_sz, |
| 534 | "%s is outside the allowed root. To allow it, run: " |
| 535 | "codebase-memory-mcp allow-root %s", |
| 536 | canonical_path, canonical_path); |
| 537 | } |
| 538 | return false; |
| 539 | } |
| 540 | |
| 541 | cbm_ws_verdict_t verdict = cbm_workspace_classify_root(canonical_path, home_dir, cache_dir); |
| 542 | if (verdict == CBM_WS_ALLOW) { |
| 543 | return true; |
| 544 | } |
| 545 | /* An explicit human approval recorded for exactly this path is the only thing |
no test coverage detected