security-knowledge-os 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (243) hide show
  1. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/PKG-INFO +1 -1
  2. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/PUBLICATION_MANIFEST.md +21 -6
  3. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/__init__.py +1 -1
  4. security_knowledge_os-0.3.0/app/adapters/capgraph.py +219 -0
  5. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/adapters/mcp_config.py +100 -23
  6. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/cli.py +110 -24
  7. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/pack-schema.md +28 -2
  8. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/pyproject.toml +1 -1
  9. security_knowledge_os-0.3.0/tests/unit/test_capgraph.py +380 -0
  10. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_mcp_scan.py +5 -2
  11. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/.env.example +0 -0
  12. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/.github/workflows/ci.yml +0 -0
  13. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/.github/workflows/publish.yml +0 -0
  14. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/.gitignore +0 -0
  15. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/LICENSE +0 -0
  16. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/NOTICE +0 -0
  17. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/README.md +0 -0
  18. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/REVIEW_CHECKLIST.md +0 -0
  19. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/REVIEW_PACKAGE.md +0 -0
  20. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/adapters/__init__.py +0 -0
  21. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/config.py +0 -0
  22. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/eval/__init__.py +0 -0
  23. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/eval/metrics.py +0 -0
  24. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/__init__.py +0 -0
  25. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/chunker.py +0 -0
  26. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/loader.py +0 -0
  27. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/parser.py +0 -0
  28. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/snapshot.py +0 -0
  29. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/validator.py +0 -0
  30. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/llm/__init__.py +0 -0
  31. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/llm/anthropic_client.py +0 -0
  32. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/llm/base.py +0 -0
  33. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/llm/factory.py +0 -0
  34. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/llm/mock.py +0 -0
  35. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/main.py +0 -0
  36. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/__init__.py +0 -0
  37. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/_credential_shapes.py +0 -0
  38. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/answer.py +0 -0
  39. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/assessment.py +0 -0
  40. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/context.py +0 -0
  41. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/knowledge.py +0 -0
  42. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/llm_io.py +0 -0
  43. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/pack.py +0 -0
  44. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/policy_outcome.py +0 -0
  45. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/report.py +0 -0
  46. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/retrieval.py +0 -0
  47. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/reviewer_output.py +0 -0
  48. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/risk.py +0 -0
  49. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/rule_clause.py +0 -0
  50. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/pack_cli.py +0 -0
  51. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/__init__.py +0 -0
  52. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/archive.py +0 -0
  53. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/build.py +0 -0
  54. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/diff.py +0 -0
  55. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/license.py +0 -0
  56. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/loader.py +0 -0
  57. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/manifest.py +0 -0
  58. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/signing.py +0 -0
  59. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/store.py +0 -0
  60. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/trusted_keys.py +0 -0
  61. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/verify.py +0 -0
  62. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/policy/__init__.py +0 -0
  63. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/policy/classification.py +0 -0
  64. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/policy/human_gate.py +0 -0
  65. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/policy/knowledge_guard.py +0 -0
  66. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/policy/safe_test.py +0 -0
  67. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/retrieval/__init__.py +0 -0
  68. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/retrieval/base.py +0 -0
  69. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/retrieval/bm25.py +0 -0
  70. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/retrieval/hybrid.py +0 -0
  71. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/retrieval/index.py +0 -0
  72. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/__init__.py +0 -0
  73. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/answers.py +0 -0
  74. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/assess.py +0 -0
  75. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/attack_surface.py +0 -0
  76. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/clause_eval.py +0 -0
  77. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/evidence.py +0 -0
  78. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/extensions.py +0 -0
  79. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/facts.py +0 -0
  80. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/llm_review.py +0 -0
  81. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/normalize.py +0 -0
  82. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/questions.py +0 -0
  83. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/report.py +0 -0
  84. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/rollup.py +0 -0
  85. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/rule_engine.py +0 -0
  86. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/rule_loader.py +0 -0
  87. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/vocabulary.py +0 -0
  88. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/safe_errors.py +0 -0
  89. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/storage/__init__.py +0 -0
  90. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/storage/db.py +0 -0
  91. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/storage/integrity.py +0 -0
  92. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/storage/repository.py +0 -0
  93. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/constraints.txt +0 -0
  94. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/acceptance-criteria.md +0 -0
  95. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/architecture.md +0 -0
  96. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/attribution.md +0 -0
  97. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/history-rewrite-map.md +0 -0
  98. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/knowledge-corpus.md +0 -0
  99. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/knowledge-schema.md +0 -0
  100. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/rule-schema.md +0 -0
  101. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/safe-test-schema.md +0 -0
  102. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/safety-boundaries.md +0 -0
  103. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/threat-model.md +0 -0
  104. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/private/README.md +0 -0
  105. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/private/confidential/.gitkeep +0 -0
  106. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/private/internal/.gitkeep +0 -0
  107. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/agent-security/.gitkeep +0 -0
  108. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/agent-security/KU-0004-excessive-agency-least-privilege.md +0 -0
  109. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/agent-security/KU-0005-human-approval-gates.md +0 -0
  110. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/agent-security/KU-0006-outbound-channels-allowlist.md +0 -0
  111. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/credential-security/.gitkeep +0 -0
  112. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/credential-security/KU-0008-secrets-not-reachable-by-model.md +0 -0
  113. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/credential-security/KU-0009-credential-broker-pattern.md +0 -0
  114. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/governance/KU-0012-human-oversight.md +0 -0
  115. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/incidents/.gitkeep +0 -0
  116. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/incidents/KU-0014-llm-payload-identifier-leak.md +0 -0
  117. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/memory-security/.gitkeep +0 -0
  118. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/memory-security/KU-0007-persistent-memory-poisoning.md +0 -0
  119. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/methodology/.gitkeep +0 -0
  120. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/methodology/KU-0011-improper-output-handling.md +0 -0
  121. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/methodology/KU-0013-assume-compromise-ja.md +0 -0
  122. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/prompt-security/.gitkeep +0 -0
  123. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/prompt-security/KU-0001-direct-prompt-injection.md +0 -0
  124. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/prompt-security/KU-0010-system-prompt-leakage.md +0 -0
  125. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/rag-security/.gitkeep +0 -0
  126. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/rag-security/KU-0002-indirect-prompt-injection.md +0 -0
  127. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/rag-security/KU-0003-untrusted-content-isolation.md +0 -0
  128. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/agent/.gitkeep +0 -0
  129. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/agent/OUT-001.yaml +0 -0
  130. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/agent/TOOL-000.yaml +0 -0
  131. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/agent/TOOL-001.yaml +0 -0
  132. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/credential/.gitkeep +0 -0
  133. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/credential/CRED-001.yaml +0 -0
  134. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/governance/.gitkeep +0 -0
  135. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/governance/GOV-001.yaml +0 -0
  136. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/memory/.gitkeep +0 -0
  137. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/memory/MEM-001.yaml +0 -0
  138. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/prompt/.gitkeep +0 -0
  139. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/rag/.gitkeep +0 -0
  140. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/rag/PI-003.yaml +0 -0
  141. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/safe_tests/ST-CRED-001.yaml +0 -0
  142. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/safe_tests/ST-IPI-001.yaml +0 -0
  143. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/safe_tests/ST-MEM-001.yaml +0 -0
  144. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/safe_tests/ST-TOOL-001.yaml +0 -0
  145. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/sbom.json +0 -0
  146. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/_scheduled_cross_review.sh +0 -0
  147. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/assess.py +0 -0
  148. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/build_index.py +0 -0
  149. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/evaluate.py +0 -0
  150. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/generate_sbom.py +0 -0
  151. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/ingest.py +0 -0
  152. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/issue_license.py +0 -0
  153. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/preflight.py +0 -0
  154. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/run_cross_review.sh +0 -0
  155. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/secret_scan.py +0 -0
  156. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/validate_knowledge.py +0 -0
  157. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/validate_rules.py +0 -0
  158. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/validate_safe_tests.py +0 -0
  159. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/conftest.py +0 -0
  160. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/safe/S-001-prompt-only.yaml +0 -0
  161. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/safe/S-002-rag-trusted-no-actions.yaml +0 -0
  162. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/safe/S-003-readonly-tool-with-approval.yaml +0 -0
  163. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/safe/S-004-credential-proxy.yaml +0 -0
  164. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/unknown/U-001-tool-permissions-missing.yaml +0 -0
  165. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/unknown/U-002-memory-persistence-unspecified.yaml +0 -0
  166. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/unknown/U-003-outbound-destination-unspecified.yaml +0 -0
  167. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/unknown/U-004-credential-handling-unspecified.yaml +0 -0
  168. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/unknown/U-005-tool-permission-unrecognized.yaml +0 -0
  169. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/vulnerable/V-001-indirect-injection-auto-email.yaml +0 -0
  170. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/vulnerable/V-002-rag-delete-tool-no-approval.yaml +0 -0
  171. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/vulnerable/V-003-persistent-memory-untrusted.yaml +0 -0
  172. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/vulnerable/V-004-env-secret-readable.yaml +0 -0
  173. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/vulnerable/V-005-unlisted-rag-source-untrusted-memory.yaml +0 -0
  174. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/private/confidential/KU-1020-confidential-tool-matrix.md +0 -0
  175. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/private/internal/KU-1010-internal-memory-heuristic.md +0 -0
  176. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/public/credential-security/KU-1003-env-secret-exposure.md +0 -0
  177. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/public/prompt-security/KU-1001-instruction-data-boundary.md +0 -0
  178. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/public/rag-security/KU-1002-indirect-injection-external-document.md +0 -0
  179. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/secret/KU-1099-secret-must-not-index.md +0 -0
  180. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus_alt/public/methodology/KU-2001-alt-corpus-marker.md +0 -0
  181. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/invalid/KU-0010-mislabeled.md +0 -0
  182. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/invalid/bad-classification.md +0 -0
  183. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/invalid/bad-id.md +0 -0
  184. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/invalid/missing-fields.md +0 -0
  185. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/invalid/no-front-matter.md +0 -0
  186. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/private/internal/KU-0002-internal-note.md +0 -0
  187. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/public/methodology/KU-0003-sparse.md +0 -0
  188. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/public/prompt-security/KU-0001-instruction-hierarchy.md +0 -0
  189. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/secret/KU-0009-secret.md +0 -0
  190. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/integration/.gitkeep +0 -0
  191. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_answers.py +0 -0
  192. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_anthropic_client.py +0 -0
  193. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_api.py +0 -0
  194. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_assess.py +0 -0
  195. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_assess_m4.py +0 -0
  196. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_assess_m5.py +0 -0
  197. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_assess_script.py +0 -0
  198. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_assessment_input.py +0 -0
  199. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_build_index_script.py +0 -0
  200. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_classification_gate.py +0 -0
  201. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_classification_leakage.py +0 -0
  202. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_clause_eval.py +0 -0
  203. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_cli.py +0 -0
  204. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_config.py +0 -0
  205. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_cross_review_script.py +0 -0
  206. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_db.py +0 -0
  207. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_eval.py +0 -0
  208. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_evaluate_script.py +0 -0
  209. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_evidence.py +0 -0
  210. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_fail_closed.py +0 -0
  211. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_finding_boundary.py +0 -0
  212. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_generate_sbom.py +0 -0
  213. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_human_gate.py +0 -0
  214. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_ingest_script.py +0 -0
  215. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_ja_retrieval.py +0 -0
  216. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_knowledge_corpus.py +0 -0
  217. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_knowledge_guard.py +0 -0
  218. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_knowledge_root_config.py +0 -0
  219. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_knowledge_schema.py +0 -0
  220. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_llm_adapter.py +0 -0
  221. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_llm_boundary.py +0 -0
  222. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_llm_output_schema.py +0 -0
  223. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_llm_review.py +0 -0
  224. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_loader.py +0 -0
  225. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_no_code_execution.py +0 -0
  226. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_no_secret_echo.py +0 -0
  227. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_normalize.py +0 -0
  228. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_packs.py +0 -0
  229. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_packs_cli.py +0 -0
  230. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_parser.py +0 -0
  231. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_policy_outcome.py +0 -0
  232. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_preflight_manifest.py +0 -0
  233. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_questions.py +0 -0
  234. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_reindex.py +0 -0
  235. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_report.py +0 -0
  236. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_repository.py +0 -0
  237. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_retrieval.py +0 -0
  238. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_rollup.py +0 -0
  239. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_rule_engine.py +0 -0
  240. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_rule_loader.py +0 -0
  241. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_safe_test.py +0 -0
  242. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_secret_scan.py +0 -0
  243. {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: security-knowledge-os
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Deterministic security assessment engine + knowledge retrieval + optional LLM assistance
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,16 +1,16 @@
1
- # Publication Manifest — Security Knowledge OS v0.2.0
1
+ # Publication Manifest — Security Knowledge OS v0.3.0
2
2
 
3
3
  Snapshot of what the first public push contains. Generated for the pre-publication
4
4
  review; regenerate with `git ls-files` after any change.
5
5
 
6
- - Commit: `316e8c6` (v0.2.0: Update Packs + `skos scan mcp`; PyPI 0.1.0 was `a7f463d`)
6
+ - Commit: `2627b44` (v0.3.0: whole-agent capability labels for the `capgraph` pack; 0.2.0 was `316e8c6`, 0.1.0 `a7f463d`)
7
7
  - History: rewritten once on 2026-09-20, before first publication, to normalise author/committer identity to the
8
8
  GitHub no-reply address (one commit message also scrubbed of a local account name). Every commit's tree is
9
9
  identical before and after, so the reviewed content is the published content; commit hashes cited in the
10
10
  review records below are the **pre-rewrite** ones - see `docs/history-rewrite-map.md` for old -> new.
11
11
  - Licence: **Apache-2.0** (`LICENSE`, `NOTICE`, `pyproject.toml`)
12
- - Tracked files: **242**
13
- - Tests: **943** items (942 pass, 1 skip = JA-R02)
12
+ - Tracked files: **244**
13
+ - Tests: **978** items (977 pass, 1 skip = JA-R02)
14
14
  - `scripts/preflight.py`: **PASS** (now also verifies the documented
15
15
  quickstart `pip install -e` command installs every declared extra,
16
16
  pins `constraints.txt`, AND passes `--build-constraint constraints.txt`
@@ -798,6 +798,21 @@ review; regenerate with `git ls-files` after any change.
798
798
  empty value in a URL, or a flag with no value, is reported as a literal
799
799
  secret - a false positive).
800
800
 
801
+ **v0.3.0 capability-label review** (2026-09-29, scoped to `master..2627b44`:
802
+ `app/adapters/capgraph.py`, the `skos scan mcp --client` / `_agent.yaml` /
803
+ `capability-labels.json` changes in `app/adapters/mcp_config.py` and
804
+ `app/cli.py`, plus the separate data-only `capgraph` pack source). Codex,
805
+ three rounds, each CHANGES-REQUIRED and fixed: round 1 (`5b6a115`) 10
806
+ findings (4 HIGH: package impersonation via launcher path or environment,
807
+ server identity bound to file names, browser and memory profiles claiming
808
+ absent capabilities); round 2 (`75f146f`) residuals plus 2 new (HIGH: a pack
809
+ failing verification treated as not installed); round 3 (`2cc494b`) one
810
+ residual HIGH (`pnpm exec`/`yarn exec` run a local command) and one LOW
811
+ (npx server arguments). Fixed in `970a1b1`, reviewed by Antigravity:
812
+ **PASS-with-nits**; the test nit was added in `2627b44`. The medium-rule
813
+ suppression Codex flagged as G10 is documented in the pack README (engine
814
+ policy), not changed.
815
+
801
816
  ## Tracked files by area
802
817
 
803
818
  Per-area counts below were last verified at round 2 (commit `bcd9519`) and
@@ -807,8 +822,8 @@ check enforces (total tracked files, total tests) are kept current above.
807
822
 
808
823
  | area | files | notes |
809
824
  | --- | --- | --- |
810
- | `app/` | 76 (.py) | engine, models, policy, retrieval, reviewer, llm, storage, eval, cli, main, packs (Update Pack manager), adapters (MCP config scan) |
811
- | `tests/` | 86 | 55 test modules + fixtures |
825
+ | `app/` | 77 (.py) | engine, models, policy, retrieval, reviewer, llm, storage, eval, cli, main, packs (Update Pack manager), adapters (MCP config scan, capability labels) |
826
+ | `tests/` | 87 | 56 test modules + fixtures |
812
827
  | `knowledge/` | 24 | 14 public KUs + `private/` skeleton (README + 2 `.gitkeep`) + category `.gitkeep`s |
813
828
  | `rules/` | 13 | 7 rule YAMLs + category `.gitkeep`s |
814
829
  | `safe_tests/` | 4 | 4 vetted templates |
@@ -1,3 +1,3 @@
1
1
  """Security Knowledge OS - deterministic security assessment engine."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -0,0 +1,219 @@
1
+ """Capability labels for one agent, derived from its MCP client config.
2
+
3
+ Each server gets six labels - ``untrusted_input``, ``sensitive_read``,
4
+ ``egress``, ``exec``, ``write``, ``persistence`` - valued ``True`` /
5
+ ``False`` / ``None`` (unknown). A label is ``True`` only when the config
6
+ (package identity, launch options) establishes it, and ``False`` only when
7
+ the absence is established the same way; a server the scanner does not know
8
+ keeps every label ``None``. Tool descriptions are never used: they can lie.
9
+
10
+ The agent is the union of its servers and the client's own built-in tools,
11
+ combined with a three-valued OR (any ``True`` -> ``True``; all ``False`` ->
12
+ ``False``; otherwise ``None``). When the client is not declared, nothing is
13
+ known about its built-in tools, so an aggregate ``False`` becomes ``None``.
14
+
15
+ The result feeds the ``capgraph`` Update Pack (``extensions.capgraph``) and
16
+ is also written as ``capability-labels.json`` for runtime enforcement (for
17
+ example a gateway that decides trust from the same labels). Only server
18
+ names from the config appear in either output (project paths are replaced by
19
+ opaque ids), never configuration values.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ from dataclasses import dataclass, field
26
+
27
+ from app.adapters.mcp_config import ServerScan
28
+
29
+ LABELS = ("untrusted_input", "sensitive_read", "egress", "exec", "write", "persistence")
30
+ # Written to extensions.capgraph; every key becomes a ``capgraph_<key>`` fact.
31
+ FACT_KEYS = (*LABELS, "flow_gated", "egress_gated")
32
+ SCHEMA = "capgraph/v0"
33
+ AGENT_FILE_STEM = "_agent" # reserved: no server may be written under this name
34
+ CLIENTS = ("claude-code", "none")
35
+
36
+ Label = bool | None
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class _Caps:
41
+ """What a known package can do beyond the MCP scanner's own facts."""
42
+
43
+ sensitive_read: Label = None
44
+ persistence: Label = None
45
+ write_scope: str | None = None # "fs" = the scanner's fs_scope
46
+ # the scanner's "no shell" does not mean "no code": a browser runs
47
+ # JavaScript on the pages it drives
48
+ exec_unknown: bool = False
49
+
50
+
51
+ # By exact (ecosystem, package) identity, like the scanner's own profiles.
52
+ _KNOWN: dict[tuple[str, str], _Caps] = {
53
+ # scope-dependent: see _labels()
54
+ ("npm", "@modelcontextprotocol/server-filesystem"): _Caps(None, True, "fs"),
55
+ # private repositories/messages behind the token; pushes can change CI
56
+ ("npm", "@modelcontextprotocol/server-github"): _Caps(True, True, "remote"),
57
+ ("npm", "@modelcontextprotocol/server-gitlab"): _Caps(True, True, "remote"),
58
+ ("npm", "@modelcontextprotocol/server-slack"): _Caps(True, None, "remote"),
59
+ # a browser reaches intranet pages and the host's services, submits forms
60
+ # and can store content that is loaded again later (Codex capgraph G03)
61
+ ("npm", "@modelcontextprotocol/server-puppeteer"): _Caps(None, None, "remote", True),
62
+ ("npm", "@playwright/mcp"): _Caps(None, None, "remote", True),
63
+ ("npm", "@modelcontextprotocol/server-brave-search"): _Caps(False, False, "none"),
64
+ # what it stores is loaded into later sessions; what it holds - and where
65
+ # the file is - is not visible (Codex capgraph G04)
66
+ ("npm", "@modelcontextprotocol/server-memory"): _Caps(None, True, None),
67
+ ("npm", "@wonderwhy-er/desktop-commander"): _Caps(True, True, "root"),
68
+ # fetch can reach localhost and intranet URLs
69
+ ("pypi", "mcp-server-fetch"): _Caps(None, False, "none"),
70
+ ("pypi", "mcp-server-git"): _Caps(None, None, "project"),
71
+ ("pypi", "mcp-server-time"): _Caps(False, False, "none"),
72
+ }
73
+
74
+ # Built-in tools of an agent client, by --client name. "none" declares a
75
+ # client without built-in tools (e.g. a custom agent that only uses MCP).
76
+ _CLIENTS: dict[str, dict[str, Label]] = {
77
+ # Bash, Read/Edit/Write, WebFetch/WebSearch
78
+ "claude-code": {
79
+ "untrusted_input": True, "sensitive_read": True, "egress": True,
80
+ "exec": True, "write": True, "persistence": True,
81
+ },
82
+ }
83
+
84
+
85
+ @dataclass
86
+ class ServerLabels:
87
+ name: str # the scan's file stem (used in notes and file names)
88
+ server: str # exact server name in the config
89
+ scope: str # "global" or "project-<id>"
90
+ labels: dict[str, Label]
91
+ write_scope: str | None
92
+
93
+
94
+ @dataclass
95
+ class AgentLabels:
96
+ client: str | None
97
+ servers: list[ServerLabels]
98
+ labels: dict[str, Label] = field(default_factory=dict)
99
+ contributors: dict[str, list[str]] = field(default_factory=dict)
100
+
101
+ def facts(self) -> dict[str, Label]:
102
+ """The ``extensions.capgraph`` block. The gates (``flow_gated``,
103
+ ``egress_gated``) are not visible in a config: they are left for the
104
+ operator and asked when a combination they guard applies."""
105
+ return {**self.labels, "flow_gated": None, "egress_gated": None}
106
+
107
+ def notes(self) -> list[str]:
108
+ out = []
109
+ if self.client is None:
110
+ out.append(
111
+ "client not declared (--client): its built-in tools are unknown, so no label "
112
+ "is claimed absent"
113
+ )
114
+ for label in LABELS:
115
+ if self.contributors.get(label):
116
+ out.append(f"{label} <- {', '.join(self.contributors[label])}")
117
+ return out
118
+
119
+
120
+ def _labels(scan: ServerScan) -> ServerLabels:
121
+ f = scan.facts
122
+ labels: dict[str, Label] = {
123
+ "untrusted_input": f.get("ingests_untrusted_content"),
124
+ "sensitive_read": None,
125
+ "egress": f.get("can_send"),
126
+ "exec": f.get("shell_exec"),
127
+ "write": f.get("can_write"),
128
+ "persistence": None,
129
+ }
130
+ write_scope: str | None = None
131
+ caps = None
132
+ if scan.package is not None and scan.ecosystem is not None:
133
+ caps = _KNOWN.get((scan.ecosystem, scan.package))
134
+ if caps is not None:
135
+ labels["sensitive_read"] = caps.sensitive_read
136
+ labels["persistence"] = caps.persistence
137
+ write_scope = caps.write_scope
138
+ if caps.exec_unknown:
139
+ labels["exec"] = None
140
+ if caps.write_scope == "fs":
141
+ scope = f.get("fs_scope")
142
+ write_scope = scope if scope in ("project", "home", "root") else None
143
+ # the whole home directory or / holds credentials and private files;
144
+ # a project directory may (.env) but that is not visible here
145
+ labels["sensitive_read"] = True if scope in ("home", "root") else None
146
+ if labels["exec"] is True and f.get("sandboxed") is not True:
147
+ # an unsandboxed shell reads and writes anything the user can
148
+ labels["sensitive_read"] = labels["write"] = labels["persistence"] = True
149
+ write_scope = write_scope or "root"
150
+ if labels["write"] is False and labels["exec"] is False and labels["persistence"] is None:
151
+ labels["persistence"] = False
152
+ return ServerLabels(scan.name, scan.server, scan.scope, labels, write_scope)
153
+
154
+
155
+ def _or(values: list[Label]) -> Label:
156
+ if any(v is True for v in values):
157
+ return True
158
+ if values and all(v is False for v in values):
159
+ return False
160
+ return None
161
+
162
+
163
+ def agent_labels(scans: list[ServerScan], client: str | None) -> AgentLabels:
164
+ if client is not None and client not in CLIENTS:
165
+ raise ValueError(f"unknown client {client!r} (known: {', '.join(CLIENTS)})")
166
+ servers = [_labels(s) for s in scans]
167
+ builtin = _CLIENTS.get(client) if client else None
168
+ agent = AgentLabels(client, servers)
169
+ for label in LABELS:
170
+ values = [s.labels[label] for s in servers]
171
+ sources = [s.name for s in servers if s.labels[label] is True]
172
+ if builtin is not None:
173
+ values.append(builtin[label])
174
+ if builtin[label] is True:
175
+ sources.insert(0, f"client:{client}")
176
+ value = _or(values)
177
+ if value is False and client is None:
178
+ value = None
179
+ agent.labels[label] = value
180
+ agent.contributors[label] = sources
181
+ return agent
182
+
183
+
184
+ def labels_json(agent: AgentLabels, source_name: str) -> str:
185
+ doc = {
186
+ "schema": SCHEMA,
187
+ "source": source_name,
188
+ "client": agent.client,
189
+ "agent": agent.labels,
190
+ "contributors": agent.contributors,
191
+ # identify a server by (scope, exact name) - the file stem is only a
192
+ # file name and may be renamed to stay unique (Codex capgraph G02)
193
+ "servers": [
194
+ {
195
+ "server": s.server, "scope": s.scope, "file": s.name,
196
+ "labels": s.labels, "write_scope": s.write_scope,
197
+ }
198
+ for s in agent.servers
199
+ ],
200
+ }
201
+ return json.dumps(doc, indent=2, ensure_ascii=False) + "\n"
202
+
203
+
204
+ def agent_yaml(agent: AgentLabels, source_name: str) -> str:
205
+ import yaml
206
+
207
+ body = yaml.safe_dump(
208
+ {"name": f"agent:{source_name}", "extensions": {"capgraph": agent.facts()}},
209
+ sort_keys=False,
210
+ allow_unicode=True,
211
+ )
212
+ header = [
213
+ f"# generated by `skos scan mcp` from {source_name} (the whole agent: every server"
214
+ + (f" + client {agent.client}" if agent.client else "") + ")",
215
+ "# null = not established by the config. Answer what you can, then `skos assess` this"
216
+ " file.",
217
+ ]
218
+ header += [f"# note: {n}" for n in agent.notes()]
219
+ return "\n".join(header) + "\n" + body
@@ -19,6 +19,7 @@ names - is ever written, printed, or placed in an error message.
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
+ import hashlib
22
23
  import ipaddress
23
24
  import json
24
25
  import posixpath
@@ -86,10 +87,12 @@ _KNOWN: dict[tuple[str, str], _Profile] = {
86
87
  ("npm", "@modelcontextprotocol/server-github"): _Profile(False, True, True, True),
87
88
  ("npm", "@modelcontextprotocol/server-gitlab"): _Profile(False, True, True, True),
88
89
  ("npm", "@modelcontextprotocol/server-slack"): _Profile(False, True, True, True),
89
- ("npm", "@modelcontextprotocol/server-puppeteer"): _Profile(False, False, True, True),
90
+ # a browser can submit forms and change state on the sites it visits
91
+ ("npm", "@modelcontextprotocol/server-puppeteer"): _Profile(False, True, True, True),
90
92
  ("npm", "@modelcontextprotocol/server-brave-search"): _Profile(False, False, True, True),
91
- ("npm", "@modelcontextprotocol/server-memory"): _Profile(False, True, False, False),
92
- ("npm", "@playwright/mcp"): _Profile(False, False, True, True),
93
+ # what is stored may itself have come from outside content
94
+ ("npm", "@modelcontextprotocol/server-memory"): _Profile(False, True, False, None),
95
+ ("npm", "@playwright/mcp"): _Profile(False, True, True, True),
93
96
  ("npm", "@wonderwhy-er/desktop-commander"): _Profile(True, True, True, None, True),
94
97
  ("pypi", "mcp-server-fetch"): _Profile(False, False, True, True),
95
98
  ("pypi", "mcp-server-git"): _Profile(False, True, False, None, True),
@@ -129,10 +132,13 @@ class ConfigError(ValueError):
129
132
 
130
133
  @dataclass
131
134
  class ServerScan:
132
- name: str
135
+ name: str # file-safe, unique stem for this scan's outputs
133
136
  package: str | None
134
137
  facts: dict[str, Any] = field(default_factory=dict)
135
138
  notes: list[str] = field(default_factory=list)
139
+ ecosystem: str | None = None # "npm" / "pypi" when the package is identified
140
+ server: str = "" # the server's exact name in the config
141
+ scope: str = "global" # "global" or "project-<id>" (an opaque id, never the path)
136
142
 
137
143
  def unknown(self) -> list[str]:
138
144
  return [k for k in FACT_KEYS if self.facts.get(k) is None]
@@ -226,7 +232,18 @@ def _launcher(command: str, args: list[str]) -> tuple[str, str | None, list[str]
226
232
  ecosystem: "npm" / "pypi" / "container" / "" (a plain command)."""
227
233
  base = PurePosixPath(command).name
228
234
  rest = list(args)
229
- if base in ("pnpm", "yarn", "npm") and rest[:1] in (["dlx"], ["exec"]):
235
+ # `pnpm exec` / `yarn exec` run a LOCAL command: the name says nothing
236
+ # about a registry package (Codex capgraph review G01). `dlx` fetches one.
237
+ if base in ("pnpm", "yarn") and rest[:1] == ["dlx"]:
238
+ base, rest = "npx", rest[1:]
239
+ elif base in ("pnpm", "yarn") and rest[:1] == ["exec"]:
240
+ return "npm", None, []
241
+ # npm itself keeps parsing its options after the package unless they
242
+ # follow `--` (a trailing --registry=... replaces what runs); npx inserts
243
+ # that `--` before the first positional argument, so for npx the
244
+ # arguments after the package belong to the server (Codex G01/G13).
245
+ npm_exec = base == "npm" and rest[:1] == ["exec"]
246
+ if npm_exec:
230
247
  base, rest = "npx", rest[1:]
231
248
  # Only the launcher options below are understood. Anything else - a
232
249
  # registry/index override, `-p/--from` naming a different package than
@@ -238,7 +255,13 @@ def _launcher(command: str, args: list[str]) -> tuple[str, str | None, list[str]
238
255
  continue
239
256
  if arg.startswith("-"):
240
257
  return "npm", None, []
241
- return "npm", arg, rest[i + 1 :]
258
+ after = rest[i + 1 :]
259
+ if not npm_exec:
260
+ return "npm", arg, after
261
+ sep = after.index("--") if "--" in after else len(after)
262
+ if any(a.startswith("-") for a in after[:sep]):
263
+ return "npm", None, []
264
+ return "npm", arg, after[:sep] + after[sep + 1 :]
242
265
  return "npm", None, []
243
266
  if base in ("uvx", "pipx"):
244
267
  if base == "pipx":
@@ -384,13 +407,33 @@ def _fs_scope(paths: list[str]) -> str | None:
384
407
  return None # pragma: no cover
385
408
 
386
409
 
387
- def scan_server(label: str, cfg: object, taken: set[str]) -> ServerScan:
410
+ def _launch_uncertain(command: str, env: dict[str, Any]) -> str | None:
411
+ """Why the configured launcher may not run the package it names, or None.
412
+
413
+ A launcher given by path is whatever that file is, and environment
414
+ variables other than credentials (NODE_OPTIONS, npm_config_registry,
415
+ PYTHONPATH, LD_PRELOAD, ...) can change what actually runs, so the
416
+ package is then not identified and no capability is inferred from it
417
+ (Codex capgraph review G01)."""
418
+ if command != PurePosixPath(command).name or "\\" in command:
419
+ return "the launcher is given by path; the package is not identified"
420
+ if any(re.search(_SECRET_NAME, k) is None for k in env):
421
+ return (
422
+ "environment variables other than credentials can change what runs; "
423
+ "the package is not identified"
424
+ )
425
+ return None
426
+
427
+
428
+ def scan_server(label: str, cfg: object, taken: set[str], *, server: str | None = None,
429
+ scope: str = "global") -> ServerScan:
388
430
  name = _safe_name(label, taken)
389
431
  if not isinstance(cfg, dict):
390
432
  raise ConfigError(f"server {name!r}: entry must be an object")
391
433
  facts: dict[str, Any] = {k: None for k in FACT_KEYS}
392
434
  notes: list[str] = []
393
435
  package: str | None = None
436
+ identified: str | None = None
394
437
  secret_seen = False
395
438
  token_seen = False
396
439
  url = cfg.get("url")
@@ -408,11 +451,11 @@ def scan_server(label: str, cfg: object, taken: set[str]) -> ServerScan:
408
451
  except ValueError:
409
452
  # The exception text can quote the URL (and credentials in it).
410
453
  raise ConfigError(f"server {name!r}: 'url' is not a valid URL") from None
411
- scope = _host_scope(host)
412
- if scope == "loopback":
454
+ reach = _host_scope(host)
455
+ if reach == "loopback":
413
456
  notes.append("reached via loopback; the server's own listen address is not visible")
414
457
  else:
415
- facts["bind_scope"] = scope # reachable at least from there
458
+ facts["bind_scope"] = reach # reachable at least from there
416
459
  if has_userinfo:
417
460
  secret_seen = token_seen = True
418
461
  if any(re.search(_SECRET_NAME, k) for k in query_keys):
@@ -427,18 +470,26 @@ def scan_server(label: str, cfg: object, taken: set[str]) -> ServerScan:
427
470
  notes.append(
428
471
  "the client sends credentials; whether the server REQUIRES them is not visible"
429
472
  )
430
- if url.startswith("http://") and scope not in (None, "loopback"):
473
+ if url.startswith("http://") and reach not in (None, "loopback"):
431
474
  notes.append("remote server over plain http (no TLS)")
432
475
  elif isinstance(cfg.get("command"), str):
433
476
  facts["transport"] = "stdio"
434
477
  args = _str_list(cfg.get("args"), f"server {name!r}: 'args'")
435
- ecosystem, spec, rest = _launcher(cfg["command"], args)
478
+ uncertain = _launch_uncertain(
479
+ cfg["command"], _str_dict(cfg.get("env"), f"server {name!r}: 'env'")
480
+ )
481
+ if uncertain is None:
482
+ ecosystem, spec, rest = _launcher(cfg["command"], args)
483
+ else:
484
+ notes.append(uncertain)
485
+ ecosystem, spec, rest = "", None, args
436
486
  if ecosystem == "container":
437
487
  facts["sandboxed"], facts["version_pinned"], extra = _container(rest)
438
488
  notes += extra
439
489
  elif spec is not None:
440
490
  package, pinned = _identity(ecosystem, spec)
441
491
  if package is not None:
492
+ identified = ecosystem
442
493
  facts["version_pinned"] = pinned
443
494
  facts["third_party"] = not (
444
495
  (ecosystem == "npm" and package.startswith(_FIRST_PARTY_NPM_SCOPE))
@@ -471,13 +522,25 @@ def scan_server(label: str, cfg: object, taken: set[str]) -> ServerScan:
471
522
  facts["token_scope"] = None
472
523
  if not token_seen:
473
524
  notes.append("no credential recognised in the config (this does not prove there is none)")
474
- return ServerScan(name, package, facts, notes)
525
+ return ServerScan(
526
+ name, package, facts, notes, identified, label if server is None else server, scope
527
+ )
528
+
529
+
530
+ def project_id(project: str) -> str:
531
+ """An opaque, stable id for a ``projects.<path>`` entry: outputs must not
532
+ carry the project path (Codex capgraph review G05). Whoever knows the path
533
+ can recompute it."""
534
+ # 128 bits: two project paths must never share an id (Codex capgraph G02)
535
+ return "project-" + hashlib.sha256(project.encode("utf-8")).hexdigest()[:32]
475
536
 
476
537
 
477
- def load_config(path: Path) -> list[tuple[str, object]]:
478
- """``[(label, server config)]`` from the top-level ``mcpServers`` and, for
479
- ``~/.claude.json``, every ``projects.<path>.mcpServers``. Labels may
480
- repeat; scan_config() makes them unique, so no server is ever dropped."""
538
+ def load_config(path: Path) -> list[tuple[str, str, object]]:
539
+ """``[(scope, server name, server config)]`` from the top-level
540
+ ``mcpServers`` (scope ``global``) and, for ``~/.claude.json``, every
541
+ ``projects.<path>.mcpServers`` (scope ``project-<id>``). Names may repeat
542
+ across scopes; scan_config() gives each a unique file stem, so no server
543
+ is ever dropped."""
481
544
  try:
482
545
  with path.open("rb") as fh:
483
546
  raw = fh.read(MAX_CONFIG_BYTES + 1)
@@ -491,25 +554,39 @@ def load_config(path: Path) -> list[tuple[str, object]]:
491
554
  raise ConfigError("config is not valid UTF-8 JSON") from None
492
555
  if not isinstance(data, dict):
493
556
  raise ConfigError("config must be a JSON object")
494
- servers: list[tuple[str, object]] = []
557
+ servers: list[tuple[str, str, object]] = []
495
558
  top = data.get("mcpServers")
496
559
  if isinstance(top, dict):
497
- servers += [(str(k), v) for k, v in top.items()]
560
+ servers += [("global", str(k), v) for k, v in top.items()]
498
561
  projects = data.get("projects")
499
562
  if isinstance(projects, dict):
500
563
  for project, pdata in projects.items():
501
564
  inner = pdata.get("mcpServers") if isinstance(pdata, dict) else None
502
565
  if isinstance(inner, dict):
503
- label = PurePosixPath(str(project)).name or "project"
504
- servers += [(f"{label}.{k}", v) for k, v in inner.items()]
566
+ pid = project_id(str(project))
567
+ servers += [(pid, str(k), v) for k, v in inner.items()]
505
568
  if not servers:
506
569
  raise ConfigError("no MCP servers found (expected 'mcpServers')")
570
+ identities = [(scope, name) for scope, name, _ in servers]
571
+ if len(set(identities)) != len(identities):
572
+ # (scope, name) is how outputs identify a server; never let two share it
573
+ raise ConfigError("two servers share the same scope and name")
507
574
  return servers
508
575
 
509
576
 
577
+ # File stems the CLI writes next to the per-server inputs.
578
+ RESERVED_NAMES = frozenset({"_agent"})
579
+
580
+
510
581
  def scan_config(path: Path) -> list[ServerScan]:
511
- taken: set[str] = set()
512
- return [scan_server(label, cfg, taken) for label, cfg in load_config(path)]
582
+ taken: set[str] = set(RESERVED_NAMES)
583
+ return [
584
+ scan_server(
585
+ name if scope == "global" else f"{scope}.{name}", cfg, taken,
586
+ server=name, scope=scope,
587
+ )
588
+ for scope, name, cfg in load_config(path)
589
+ ]
513
590
 
514
591
 
515
592
  def assessment_yaml(scan: ServerScan, source_name: str) -> str: