footprinter-cli 1.0.5__tar.gz → 1.1.1__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 (193) hide show
  1. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/PKG-INFO +10 -2
  2. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/README.md +9 -1
  3. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/access_stamper.py +27 -8
  4. footprinter_cli-1.1.1/footprinter/api/__init__.py +19 -0
  5. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/bundled/config.example.yaml +58 -2
  6. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/__init__.py +4 -1
  7. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/_common.py +4 -7
  8. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/_policy_helpers.py +194 -69
  9. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/_vectorize_stage.py +41 -8
  10. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/add.py +4 -0
  11. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/diagnostics.py +2 -1
  12. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/doctor.py +251 -37
  13. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/ingest.py +3 -3
  14. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/mcp_setup.py +46 -22
  15. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/permission_cmd.py +303 -80
  16. footprinter_cli-1.1.1/footprinter/cli/search.py +224 -0
  17. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/setup.py +24 -20
  18. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/status.py +65 -399
  19. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/update.py +10 -8
  20. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/clients.py +36 -5
  21. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/files.py +37 -5
  22. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/folders.py +78 -10
  23. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/policies.py +8 -6
  24. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/projects.py +4 -4
  25. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/search.py +186 -28
  26. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/status.py +23 -6
  27. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db_base.py +18 -7
  28. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/adapters/local_folders.py +10 -2
  29. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/chat_indexer.py +34 -87
  30. footprinter_cli-1.1.1/footprinter/ingest/database.py +73 -0
  31. footprinter_cli-1.1.1/footprinter/ingest/db/ddl.py +878 -0
  32. footprinter_cli-1.1.1/footprinter/ingest/db/fts.py +417 -0
  33. footprinter_cli-1.1.1/footprinter/ingest/db/schema.py +13 -0
  34. footprinter_cli-1.1.1/footprinter/ingest/processing.py +526 -0
  35. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/vector_ops.py +119 -54
  36. footprinter_cli-1.1.1/footprinter/mcp/db.py +72 -0
  37. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/resources/context.py +13 -0
  38. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/server.py +2 -2
  39. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/tools/navigation.py +15 -0
  40. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/tools/read.py +30 -11
  41. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/tools/search.py +74 -21
  42. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/tools/semantic.py +5 -3
  43. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/paths.py +0 -3
  44. footprinter_cli-1.1.1/footprinter/permissions.py +292 -0
  45. footprinter_cli-1.1.1/footprinter/policy_resolver.py +411 -0
  46. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/semantic/chunking.py +18 -2
  47. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/semantic/hybrid_search.py +11 -7
  48. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/semantic/vector_store.py +75 -4
  49. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/access_service.py +102 -11
  50. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/chat_service.py +4 -1
  51. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/client_service.py +44 -10
  52. footprinter_cli-1.1.1/footprinter/services/content_service.py +429 -0
  53. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/email_service.py +4 -1
  54. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/file_service.py +4 -1
  55. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/folder_service.py +13 -1
  56. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/project_service.py +11 -4
  57. footprinter_cli-1.1.1/footprinter/services/search_service.py +453 -0
  58. footprinter_cli-1.1.1/footprinter/services/semantic_service.py +716 -0
  59. footprinter_cli-1.1.1/footprinter/services/status_service.py +333 -0
  60. footprinter_cli-1.1.1/footprinter/utils/context_md.py +172 -0
  61. footprinter_cli-1.1.1/footprinter/utils/sqlite_errors.py +58 -0
  62. footprinter_cli-1.1.1/footprinter/utils/text.py +66 -0
  63. footprinter_cli-1.1.1/footprinter/visibility.py +330 -0
  64. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter_cli.egg-info/PKG-INFO +10 -2
  65. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter_cli.egg-info/SOURCES.txt +11 -7
  66. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/pyproject.toml +3 -2
  67. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_access_control_docs.py +42 -4
  68. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_bundled.py +19 -26
  69. footprinter_cli-1.1.1/tests/test_config_limits.py +186 -0
  70. footprinter_cli-1.1.1/tests/test_conftest_config.py +25 -0
  71. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_db_base.py +38 -0
  72. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_e2e_install.py +16 -16
  73. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_inherit_resolution.py +8 -6
  74. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_no_issue_ids.py +20 -12
  75. footprinter_cli-1.1.1/tests/test_policy_resolver.py +435 -0
  76. footprinter_cli-1.1.1/tests/test_qa_dispatch.py +149 -0
  77. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_security_layer.py +7 -3
  78. footprinter_cli-1.1.1/tests/test_verify_install.py +443 -0
  79. footprinter_cli-1.0.5/footprinter/api/__init__.py +0 -4
  80. footprinter_cli-1.0.5/footprinter/bundled/patterns/context_patterns.yaml +0 -18
  81. footprinter_cli-1.0.5/footprinter/bundled/patterns/extensions.yaml +0 -283
  82. footprinter_cli-1.0.5/footprinter/bundled/patterns/filename_patterns.yaml +0 -61
  83. footprinter_cli-1.0.5/footprinter/bundled/patterns/mime_mappings.yaml +0 -68
  84. footprinter_cli-1.0.5/footprinter/bundled/patterns/salesforce_rules.yaml +0 -84
  85. footprinter_cli-1.0.5/footprinter/bundled/patterns/security_patterns.yaml +0 -27
  86. footprinter_cli-1.0.5/footprinter/cli/search.py +0 -451
  87. footprinter_cli-1.0.5/footprinter/ingest/database.py +0 -36
  88. footprinter_cli-1.0.5/footprinter/ingest/db/schema.py +0 -1182
  89. footprinter_cli-1.0.5/footprinter/ingest/processing.py +0 -348
  90. footprinter_cli-1.0.5/footprinter/mcp/db.py +0 -34
  91. footprinter_cli-1.0.5/footprinter/permissions.py +0 -1160
  92. footprinter_cli-1.0.5/footprinter/services/content_service.py +0 -191
  93. footprinter_cli-1.0.5/footprinter/services/search_service.py +0 -161
  94. footprinter_cli-1.0.5/footprinter/services/semantic_service.py +0 -380
  95. footprinter_cli-1.0.5/footprinter/services/status_service.py +0 -18
  96. footprinter_cli-1.0.5/footprinter/utils/text.py +0 -6
  97. footprinter_cli-1.0.5/footprinter/visibility.py +0 -1310
  98. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/LICENSE +0 -0
  99. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/__init__.py +0 -0
  100. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/api/__main__.py +0 -0
  101. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/api/db.py +0 -0
  102. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/api/entities.py +0 -0
  103. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/api/search.py +0 -0
  104. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/api/semantic.py +0 -0
  105. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/api/server.py +0 -0
  106. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/api/status.py +0 -0
  107. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/bundled/__init__.py +0 -0
  108. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/__main__.py +0 -0
  109. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/_prompt.py +0 -0
  110. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/connect.py +0 -0
  111. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/delete.py +0 -0
  112. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/uninstall.py +0 -0
  113. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/cli/view.py +0 -0
  114. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/connectors/__init__.py +0 -0
  115. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/connectors/config_utils.py +0 -0
  116. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/__init__.py +0 -0
  117. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/browser.py +0 -0
  118. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/chats.py +0 -0
  119. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/emails.py +0 -0
  120. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/messages.py +0 -0
  121. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/protocols.py +0 -0
  122. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/sql_utils.py +0 -0
  123. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/db/uploads.py +0 -0
  124. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/__init__.py +0 -0
  125. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/adapters/__init__.py +0 -0
  126. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/adapters/browser.py +0 -0
  127. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/adapters/chat.py +0 -0
  128. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/adapters/ingest.py +0 -0
  129. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/adapters/local_files.py +0 -0
  130. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/adapters/protocol.py +0 -0
  131. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/browser_indexer.py +0 -0
  132. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/chat_parsers/__init__.py +0 -0
  133. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/chat_parsers/chatgpt_parser.py +0 -0
  134. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/chat_parsers/claude_code_parser.py +0 -0
  135. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/chat_parsers/claude_parser.py +0 -0
  136. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/content_extractors.py +0 -0
  137. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/db/__init__.py +0 -0
  138. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/db/connector_schema.py +0 -0
  139. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/file_indexer.py +0 -0
  140. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/file_scanner.py +0 -0
  141. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/folder_indexer.py +0 -0
  142. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/full_content_extractor.py +0 -0
  143. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/orchestrator.py +0 -0
  144. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/pipe_runner.py +0 -0
  145. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/registry.py +0 -0
  146. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/run_record.py +0 -0
  147. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/scan_summary.py +0 -0
  148. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/ingest/status.py +0 -0
  149. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/__init__.py +0 -0
  150. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/__main__.py +0 -0
  151. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/errors.py +0 -0
  152. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/resources/__init__.py +0 -0
  153. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/resources/discoverability.py +0 -0
  154. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/tools/__init__.py +0 -0
  155. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/mcp/tools/status.py +0 -0
  156. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/semantic/__init__.py +0 -0
  157. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/semantic/embeddings.py +0 -0
  158. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/__init__.py +0 -0
  159. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/includes.py +0 -0
  160. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/ingest_service.py +0 -0
  161. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/roles.py +0 -0
  162. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/services/visit_service.py +0 -0
  163. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/source_registry.py +0 -0
  164. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/utils/__init__.py +0 -0
  165. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/utils/exceptions.py +0 -0
  166. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/utils/extraction.py +0 -0
  167. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/utils/hash_utils.py +0 -0
  168. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/utils/logging_config.py +0 -0
  169. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/utils/mime.py +0 -0
  170. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/utils/paths.py +0 -0
  171. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter/utils/time.py +0 -0
  172. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter_cli.egg-info/dependency_links.txt +0 -0
  173. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter_cli.egg-info/entry_points.txt +0 -0
  174. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter_cli.egg-info/requires.txt +0 -0
  175. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/footprinter_cli.egg-info/top_level.txt +0 -0
  176. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/setup.cfg +0 -0
  177. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_access_control_bypasses.py +0 -0
  178. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_access_recalculate.py +0 -0
  179. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_access_source_provenance.py +0 -0
  180. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_build_status_filter.py +0 -0
  181. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_e2e_pipeline.py +0 -0
  182. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_edit_recalculate.py +0 -0
  183. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_examples.py +0 -0
  184. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_files_rename.py +0 -0
  185. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_files_surface.py +0 -0
  186. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_logging.py +0 -0
  187. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_no_project_root.py +0 -0
  188. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_package_init.py +0 -0
  189. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_paths_no_test_marker.py +0 -0
  190. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_pip_install_e2e.py +0 -0
  191. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_prompt_safety.py +0 -0
  192. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_resolver.py +0 -0
  193. {footprinter_cli-1.0.5 → footprinter_cli-1.1.1}/tests/test_security_permissions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: footprinter-cli
3
- Version: 1.0.5
3
+ Version: 1.1.1
4
4
  Summary: A local context layer for your files, browser history, chats, and email — searchable, user-owned, MCP-served.
5
5
  Author: SwellCity Group
6
6
  License: MIT
@@ -59,6 +59,12 @@ Requires-Dist: httpx<1.0,>=0.27.0; extra == "dev"
59
59
 
60
60
  **A local context layer for your files, browser history, chats, and email — searchable, user-owned, and served to AI agents through [MCP](https://modelcontextprotocol.io/).**
61
61
 
62
+ > ⚠️ **Install with `pipx`, not `pip`.** `pipx` puts the `fp` command on your PATH automatically; bare `pip` often doesn't — which leaves you with `fp: command not found` even though the install succeeded.
63
+
64
+ ```bash
65
+ pipx install footprinter-cli
66
+ ```
67
+
62
68
  Your work lives across filesystems, browsers, inboxes, chat histories, and other tools. Footprinter indexes those sources into a single local store, organizes them into the projects and groupings you define, and serves the result to AI agents through a governed access layer. You control what the agent can see. Everything stays on your machine.
63
69
 
64
70
  ## Install
@@ -100,6 +106,8 @@ To install with extras: use the [full install script](https://raw.githubusercont
100
106
 
101
107
  **ChromaDB telemetry:** Footprinter sets `anonymized_telemetry=False`. ChromaDB also removed product telemetry in v1.5.4. See [Chroma OSS overview](https://docs.trychroma.com/docs/overview/oss).
102
108
 
109
+ **Apple Silicon (Rosetta):** If `fp doctor` warns about x86_64 Python on arm64 hardware, recreate the venv with a native interpreter: `pipx reinstall footprinter-cli --python /opt/homebrew/bin/python3`. This avoids compatibility issues with native-extension dependencies.
110
+
103
111
  </details>
104
112
 
105
113
  ### Uninstall
@@ -182,7 +190,7 @@ Sources are scanned into SQLite with bidirectional links connecting local files
182
190
  - [Data Model](https://github.com/harringjohn/footprinter-cli/blob/main/reference/data-model.md) — database schema
183
191
  - [Pipeline](https://github.com/harringjohn/footprinter-cli/blob/main/reference/pipeline.md) — indexing stages and configuration
184
192
  - [Content Storage](https://github.com/harringjohn/footprinter-cli/blob/main/reference/content-storage.md) — metadata vs. snippet vs. full-content tiers
185
- - [Access Control](https://github.com/harringjohn/footprinter-cli/blob/main/reference/mcp-access-control.md) — MCP security model
193
+ - [Permission Policies and Access Control](https://github.com/harringjohn/footprinter-cli/blob/main/reference/permission-policies-and-access-control.md) — permission policies and access control
186
194
 
187
195
  ## Contributing
188
196
 
@@ -5,6 +5,12 @@
5
5
 
6
6
  **A local context layer for your files, browser history, chats, and email — searchable, user-owned, and served to AI agents through [MCP](https://modelcontextprotocol.io/).**
7
7
 
8
+ > ⚠️ **Install with `pipx`, not `pip`.** `pipx` puts the `fp` command on your PATH automatically; bare `pip` often doesn't — which leaves you with `fp: command not found` even though the install succeeded.
9
+
10
+ ```bash
11
+ pipx install footprinter-cli
12
+ ```
13
+
8
14
  Your work lives across filesystems, browsers, inboxes, chat histories, and other tools. Footprinter indexes those sources into a single local store, organizes them into the projects and groupings you define, and serves the result to AI agents through a governed access layer. You control what the agent can see. Everything stays on your machine.
9
15
 
10
16
  ## Install
@@ -46,6 +52,8 @@ To install with extras: use the [full install script](https://raw.githubusercont
46
52
 
47
53
  **ChromaDB telemetry:** Footprinter sets `anonymized_telemetry=False`. ChromaDB also removed product telemetry in v1.5.4. See [Chroma OSS overview](https://docs.trychroma.com/docs/overview/oss).
48
54
 
55
+ **Apple Silicon (Rosetta):** If `fp doctor` warns about x86_64 Python on arm64 hardware, recreate the venv with a native interpreter: `pipx reinstall footprinter-cli --python /opt/homebrew/bin/python3`. This avoids compatibility issues with native-extension dependencies.
56
+
49
57
  </details>
50
58
 
51
59
  ### Uninstall
@@ -128,7 +136,7 @@ Sources are scanned into SQLite with bidirectional links connecting local files
128
136
  - [Data Model](https://github.com/harringjohn/footprinter-cli/blob/main/reference/data-model.md) — database schema
129
137
  - [Pipeline](https://github.com/harringjohn/footprinter-cli/blob/main/reference/pipeline.md) — indexing stages and configuration
130
138
  - [Content Storage](https://github.com/harringjohn/footprinter-cli/blob/main/reference/content-storage.md) — metadata vs. snippet vs. full-content tiers
131
- - [Access Control](https://github.com/harringjohn/footprinter-cli/blob/main/reference/mcp-access-control.md) — MCP security model
139
+ - [Permission Policies and Access Control](https://github.com/harringjohn/footprinter-cli/blob/main/reference/permission-policies-and-access-control.md) — permission policies and access control
132
140
 
133
141
  ## Contributing
134
142
 
@@ -353,18 +353,26 @@ def count_affected_entities(conn: sqlite3.Connection, scope: str) -> dict[str, i
353
353
  return {etype: len(ids) for etype, ids in _get_ids_for_scope(conn, scope).items() if ids}
354
354
 
355
355
 
356
- def stamp_entities(conn: sqlite3.Connection, ids_by_type: dict[str, list[int]]) -> dict[str, int]:
356
+ def stamp_entities(
357
+ conn: sqlite3.Connection,
358
+ ids_by_type: dict[str, list[int]],
359
+ *,
360
+ commit: bool = True,
361
+ ) -> dict[str, int]:
357
362
  """Resolve and write visibility + permissions for the given entity IDs.
358
363
 
359
364
  Used by ``recalculate_access`` (full scope resolution) and the incremental
360
365
  pipeline path in ``processing.run_access_resolution``. The batched variant
361
366
  (``recalculate_access_batched``) uses its own loop for per-chunk commits.
362
367
 
363
- Always commits before returning, even when *ids_by_type* is empty.
368
+ Commits before returning by default, even when *ids_by_type* is empty.
369
+ When *commit* is False the caller is responsible for committing the
370
+ transaction.
364
371
 
365
372
  Args:
366
373
  conn: SQLite connection with row_factory = sqlite3.Row
367
374
  ids_by_type: Mapping of entity type to list of row IDs to stamp.
375
+ commit: If False, skip the final ``conn.commit()``.
368
376
 
369
377
  Returns:
370
378
  Dict mapping entity type to count of rows stamped.
@@ -387,22 +395,26 @@ def stamp_entities(conn: sqlite3.Connection, ids_by_type: dict[str, list[int]])
387
395
 
388
396
  stats[entity_type] = len(ids)
389
397
 
390
- conn.commit()
398
+ if commit:
399
+ conn.commit()
391
400
  return stats
392
401
 
393
402
 
394
- def recalculate_access(conn: sqlite3.Connection, scope: str) -> dict[str, int]:
403
+ def recalculate_access(
404
+ conn: sqlite3.Connection, scope: str, *, commit: bool = True,
405
+ ) -> dict[str, int]:
395
406
  """Recalculate visibility and permissions for all entities affected by *scope*.
396
407
 
397
408
  Args:
398
409
  conn: SQLite connection with row_factory = sqlite3.Row
399
410
  scope: Policy scope string (e.g. "global", "project:3", "folder:~/Work/")
411
+ commit: If False, skip committing — caller manages the transaction.
400
412
 
401
413
  Returns:
402
414
  Dict mapping entity type to count of rows updated.
403
415
  """
404
416
  ids_by_type = _get_ids_for_scope(conn, scope)
405
- return stamp_entities(conn, ids_by_type)
417
+ return stamp_entities(conn, ids_by_type, commit=commit)
406
418
 
407
419
 
408
420
  def recalculate_access_batched(
@@ -411,18 +423,24 @@ def recalculate_access_batched(
411
423
  *,
412
424
  batch_size: int = 5000,
413
425
  on_batch: Callable[[int], None] | None = None,
426
+ commit: bool = True,
414
427
  ) -> dict[str, int]:
415
428
  """Recalculate visibility and permissions in batches with progress callback.
416
429
 
417
430
  Same semantics as ``recalculate_access()`` but commits after each batch
418
- and calls *on_batch* with the count of entities processed per chunk.
419
- Designed for large scopes where a progress bar is needed.
431
+ (when *commit* is True) and calls *on_batch* with the count of entities
432
+ processed per chunk. Designed for large scopes where a progress bar is
433
+ needed.
420
434
 
421
435
  Args:
422
436
  conn: SQLite connection with row_factory = sqlite3.Row
423
437
  scope: Policy scope string (e.g. "global", "folder:~/Work/")
424
438
  batch_size: Number of entity IDs per chunk (default 5000)
425
439
  on_batch: Optional callback receiving the count processed per chunk
440
+ commit: If False, skip per-batch commits — all batches accumulate
441
+ in the caller's transaction. This trades incremental commit
442
+ boundaries for atomicity; the caller is responsible for the
443
+ final commit.
426
444
 
427
445
  Returns:
428
446
  Dict mapping entity type to total count of rows updated.
@@ -446,7 +464,8 @@ def recalculate_access_batched(
446
464
  perm_results = batch_resolve_permissions(conn, entity_type, chunk)
447
465
  _write_back_permissions(conn, entity_type, perm_results)
448
466
 
449
- conn.commit()
467
+ if commit:
468
+ conn.commit()
450
469
 
451
470
  if on_batch is not None:
452
471
  on_batch(len(chunk))
@@ -0,0 +1,19 @@
1
+ """Footprinter HTTP API — FastAPI routers calling the service layer."""
2
+
3
+ import logging
4
+
5
+ _logger = logging.getLogger(__name__)
6
+
7
+
8
+ def _get_api_max_limit() -> int:
9
+ try:
10
+ from footprinter.source_registry import get_config
11
+
12
+ return get_config().get("limits", {}).get("api_max_limit", 200)
13
+ except Exception:
14
+ _logger.debug("Config unavailable for api_max_limit, using default 200")
15
+ return 200
16
+
17
+
18
+ MAX_LIMIT = _get_api_max_limit()
19
+ """Upper bound for `limit` query params on HTTP list/search endpoints."""
@@ -33,7 +33,7 @@ exclusions:
33
33
  - ".*/\\.venv/.*" # Python virtualenvs (hidden)
34
34
  - ".*/site-packages/.*" # Python packages
35
35
  - ".*\\.pyc$" # Python compiled files
36
- - ".*/\\.vscode/extensions/.*" # VS Code extensions (reinstallable)
36
+ - ".*/\\.vscode/.*" # VS Code config and extensions
37
37
  - ".*/\\.npm/.*" # npm cache
38
38
  - ".*/\\.nvm/.*" # Node version manager
39
39
  - ".*/\\.cache/.*" # Generic caches
@@ -44,6 +44,13 @@ exclusions:
44
44
  - ".*/\\.browser_state/.*" # Playwright/Puppeteer browser state
45
45
  - ".*/\\.context/.*" # IDE/agent context directories
46
46
  - ".*/\\.ai-dev/.*" # AI dev tool scratch dirs
47
+ # Build output and tool config dot-folders (no archival value)
48
+ - ".*/\\.next/.*" # Next.js build output
49
+ - ".*/\\.husky/.*" # Git hooks manager
50
+ - ".*/\\.astro/.*" # Astro build cache
51
+ - ".*/\\.githooks/.*" # Custom git hooks
52
+ - ".*/\\.aesthetic/.*" # Aesthetic tool config
53
+ - ".*/\\.users/.*" # IDE user preferences
47
54
  # Home-level Claude dirs only (keep .claude within Work/Personal)
48
55
  - "^~/\\.claude/.*" # Home-level .claude (includes session-env snapshots)
49
56
  - "^~/\\.claude-worktrees/.*" # Home-level .claude-worktrees
@@ -79,8 +86,27 @@ exclusions:
79
86
  indexing:
80
87
  supported_extensions: [] # Empty = index ALL file types
81
88
  max_file_size_mb: 50 # MB; 0 = no size limit. 50 is generous for prose/docs.
89
+ # Cap on how many bytes the MCP read path (footprinter_read) pulls from a local
90
+ # file before decode/extraction. MB; 0 = no cap (read the whole file).
91
+ # Default 10 MB closes the old 500 KB silent-truncation gap so the read tool can
92
+ # return content the index already holds.
93
+ # The returned content is ALWAYS bounded to a payload-safe size regardless of
94
+ # this cap: a single MCP tool result has a ~1 MB protocol payload ceiling, so an
95
+ # oversized read returns a marked, in-budget slice (output_truncated) plus a
96
+ # pointer to semantic/keyword search — never a hard failure. That bound is a
97
+ # UTF-8 BYTE budget (sliced on a code-point boundary) sized under the ~1 MB JSON
98
+ # wall with headroom for JSON-escape expansion of multibyte content (CJK/emoji)
99
+ # and the result envelope — not a character count, so non-Latin scripts stay
100
+ # safe. Raise this cap to read more bytes from large files; for large documents,
101
+ # prefer search.
102
+ max_read_size_mb: 10
82
103
  lookback_days: 14 # Browser history window (days back to index)
83
- content_snippets: false # Extract file/email content previews for keyword search
104
+ # Opt-in (default off). When true, ingest reads each file/email and stores a
105
+ # short preview (~1000 chars/file) so keyword search matches file content, not
106
+ # just names. Cost: extra read work during ingest + the stored preview in SQLite.
107
+ # Enabling this after the first ingest backfills already-indexed files only on
108
+ # the next `fp ingest` run; new ingests populate previews automatically.
109
+ content_snippets: false
84
110
 
85
111
  # Semantic search — stores content as embeddings in a local ChromaDB database
86
112
  # Enables finding files and chats by meaning, not just keywords
@@ -88,6 +114,21 @@ indexing:
88
114
  semantic:
89
115
  file_vectorization: false
90
116
  chat_vectorization: false
117
+ # File statuses eligible for vectorization. Default: [listed].
118
+ # Add 'unlisted' to also embed hidden/dot-files.
119
+ # vectorize_statuses:
120
+ # - listed
121
+ # Semantic file results — how much matched content to return per result.
122
+ # A chunk is ~1000 chars ≈ 250 tokens. Payload size ≈ max_chunks_per_file ×
123
+ # max_chunk_chars, so raise these for more context at higher token cost.
124
+ # Max characters of each matched chunk to return. Default 1000. 0 = no cap
125
+ # (return the whole chunk).
126
+ max_chunk_chars: 1000
127
+ # How many matched chunks to return per file, best-relevance first. Default 3.
128
+ # Clamped to [1, 20] — values above the cap are bounded to keep payloads sane.
129
+ # The per-result `chunks` list is returned only on the vector (semantic) path;
130
+ # the FTS5 keyword fallback (used when semantic search is unavailable) omits it.
131
+ max_chunks_per_file: 3
91
132
 
92
133
  # Vectorization — controls what gets embedded for semantic search
93
134
  # Requires semantic.file_vectorization: true to take effect for files
@@ -137,6 +178,21 @@ vectorization:
137
178
  - "**/.github/**" # GitHub workflow files (FTS-only)
138
179
  - "**/.ai-dev/**" # AI dev tool scratch dirs
139
180
 
181
+ # Operational limits — tune for your data volume
182
+ # All values have sensible defaults; omit the section entirely to use them.
183
+ limits:
184
+ # Zip upload safety checks (chat imports via fp upload)
185
+ zip:
186
+ max_decompressed_size_mb: 2048 # 2 GB; raise for very large exports
187
+ max_entries: 10000 # Maximum files in a zip archive
188
+ max_compression_ratio: 100 # Ratio ceiling (100:1) for zip bomb detection
189
+ # HTTP API pagination cap — requires server restart to take effect
190
+ api_max_limit: 200
191
+ # MCP search result cap per source (protocol payload limit)
192
+ mcp_search_limit_cap: 200
193
+ # Embedding batch size for vector rebuild
194
+ vector_batch_size: 100
195
+
140
196
  # Source registry seeds — loaded into the sources table on init
141
197
  # Connector sources added by: fp connect install <name>
142
198
  source_seeds:
@@ -133,7 +133,10 @@ def main(argv=None) -> None:
133
133
  from footprinter.cli._prompt import PromptCancelled
134
134
 
135
135
  try:
136
- args.func(args)
136
+ # A handler may signal its exit status by returning an int; None means
137
+ # success. This complements the raise SystemExit(n) pattern used
138
+ # elsewhere — those bubble out before the wrapper here ever runs.
139
+ raise SystemExit(args.func(args) or 0)
137
140
  except _ConfigError as e:
138
141
  print(str(e), file=_sys.stderr)
139
142
  _sys.exit(1)
@@ -16,6 +16,7 @@ from rich.console import Console
16
16
 
17
17
  from footprinter.db.clients import VALID_STATUSES as VALID_CLIENT_STATUSES
18
18
  from footprinter.db.projects import VALID_STATUSES as VALID_PROJECT_STATUSES
19
+ from footprinter.db_base import get_connection
19
20
  from footprinter.services import access_service as _access
20
21
  from footprinter.services.access_service import (
21
22
  resolve_inherit_permission,
@@ -70,17 +71,13 @@ ALLOWED_COLUMNS = frozenset({"name"})
70
71
  def connect_db(db_path: Union[str, Path]) -> Optional[sqlite3.Connection]:
71
72
  """Open a read/write connection to the Footprinter database.
72
73
 
73
- Returns None if the database file does not exist. Sets row_factory
74
- and busy_timeout so callers don't need to repeat boilerplate.
74
+ Returns None if the database file does not exist. Delegates to
75
+ :func:`~footprinter.db_base.get_connection` for PRAGMA setup.
75
76
  """
76
77
  db_path = Path(db_path)
77
78
  if not db_path.exists():
78
79
  return None
79
- conn = sqlite3.connect(str(db_path), timeout=10)
80
- conn.row_factory = sqlite3.Row
81
- conn.execute("PRAGMA busy_timeout=5000")
82
- conn.execute("PRAGMA foreign_keys=ON")
83
- return conn
80
+ return get_connection(db_path)
84
81
 
85
82
 
86
83
  @contextmanager
@@ -52,18 +52,24 @@ def confirm_recalculation(conn: sqlite3.Connection, scope: str, *, yes: bool = F
52
52
  # ---------------------------------------------------------------------------
53
53
 
54
54
 
55
- def recalculate_with_progress(conn: sqlite3.Connection, scope: str) -> dict[str, int]:
55
+ def recalculate_with_progress(
56
+ conn: sqlite3.Connection, scope: str, *, commit: bool = True,
57
+ ) -> dict[str, int]:
56
58
  """Recalculate access with a Rich progress bar for large scopes.
57
59
 
58
60
  If total affected entities <= CONFIRM_THRESHOLD, runs the fast unbatched
59
- path and prints a one-line summary. Otherwise shows a Rich progress bar
60
- with per-batch updates.
61
+ path. Otherwise shows a Rich progress bar with per-batch progress
62
+ updates.
63
+
64
+ When *commit* is False, no commits are issued (including per-batch
65
+ commits in the batched path) — the caller manages the transaction
66
+ boundary.
61
67
  """
62
68
  counts = count_affected_entities(conn, scope)
63
69
  total = sum(counts.values())
64
70
 
65
71
  if total <= CONFIRM_THRESHOLD:
66
- return recalculate_access(conn, scope)
72
+ return recalculate_access(conn, scope, commit=commit)
67
73
 
68
74
  from rich.progress import Progress
69
75
 
@@ -73,6 +79,7 @@ def recalculate_with_progress(conn: sqlite3.Connection, scope: str) -> dict[str,
73
79
  conn,
74
80
  scope,
75
81
  on_batch=lambda n: progress.advance(task, advance=n),
82
+ commit=commit,
76
83
  )
77
84
  return stats
78
85
 
@@ -102,6 +109,39 @@ def normalize_path(path: str) -> str:
102
109
  return normalized
103
110
 
104
111
 
112
+ # ---------------------------------------------------------------------------
113
+ # Numeric ID resolution helpers
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ def resolve_file_id(conn: sqlite3.Connection, file_id: int) -> str | None:
118
+ """Resolve a numeric file ID to its path, or None on failure."""
119
+ row = conn.execute(
120
+ "SELECT path, status FROM files WHERE id = ?", (file_id,),
121
+ ).fetchone()
122
+ if not row:
123
+ console.print(f"[red]File not found:[/red] id={file_id}")
124
+ return None
125
+ if row["status"] != "listed":
126
+ console.print(
127
+ f"[red]File id={file_id} has status '{row['status']}'.[/red]\n"
128
+ " Only listed files are checked."
129
+ )
130
+ return None
131
+ return row["path"]
132
+
133
+
134
+ def resolve_folder_id(conn: sqlite3.Connection, folder_id: int) -> str | None:
135
+ """Resolve a numeric folder ID to its path, or None on failure."""
136
+ row = conn.execute(
137
+ "SELECT path FROM folders WHERE id = ?", (folder_id,),
138
+ ).fetchone()
139
+ if not row:
140
+ console.print(f"[red]Folder not found:[/red] id={folder_id}")
141
+ return None
142
+ return row["path"]
143
+
144
+
105
145
  # ---------------------------------------------------------------------------
106
146
  # Single-path check helpers
107
147
  # ---------------------------------------------------------------------------
@@ -168,7 +208,7 @@ def check_file_path(conn: sqlite3.Connection, path: str, json_output: bool, verb
168
208
  console.print(f"\nAccess Check: [bold]{display_path}[/bold]")
169
209
  if not found:
170
210
  console.print(" [dim]Not found in files or folders — resolving from policy chain[/dim]")
171
- console.print(" [dim]Tip: Use --folder for directory aggregate, --project for project ID[/dim]")
211
+ console.print(" [dim]Tip: Use folder:<path> for a directory aggregate, project:<id> for a project[/dim]")
172
212
  console.print()
173
213
  console.print(f" Permission: [bold]{perm_str}[/bold] (from {perm_src})")
174
214
  console.print(f" Visibility: [bold]{vis_val}[/bold] (from {vis_src})")
@@ -484,7 +524,150 @@ def check_client(
484
524
 
485
525
 
486
526
  # ---------------------------------------------------------------------------
487
- # Policy chain / simulation
527
+ # Single-entity check helper (email, chat, visit)
528
+ # ---------------------------------------------------------------------------
529
+
530
+ _ENTITY_QUERIES: dict[str, tuple[str, str]] = {
531
+ "email": (
532
+ "SELECT id, subject, account, project_id, client_id FROM emails WHERE id = ?",
533
+ "subject",
534
+ ),
535
+ "chat": (
536
+ "SELECT id, title, account, project_id, client_id FROM chats WHERE id = ?",
537
+ "title",
538
+ ),
539
+ "visit": (
540
+ "SELECT id, title, url FROM visits WHERE id = ?",
541
+ "title",
542
+ ),
543
+ }
544
+
545
+ _SOURCE_TYPE: dict[str, str] = {
546
+ "email": "emails",
547
+ "chat": "chats",
548
+ "visit": "browser",
549
+ }
550
+
551
+
552
+ def check_entity(
553
+ conn: sqlite3.Connection,
554
+ entity_type: str,
555
+ entity_id: int,
556
+ json_output: bool,
557
+ verbose: bool = False,
558
+ ) -> int:
559
+ """Check resolved access for a single entity (email, chat, or visit) by ID."""
560
+ from footprinter.permissions import resolve_permission_with_source
561
+ from footprinter.visibility import resolve_visibility_with_source
562
+
563
+ query, name_col = _ENTITY_QUERIES[entity_type]
564
+ row = conn.execute(query, (entity_id,)).fetchone()
565
+
566
+ if not row:
567
+ console.print(f"[red]{entity_type.title()} not found:[/red] id={entity_id}")
568
+ return 1
569
+
570
+ display_name = row[name_col] or (row["url"] if entity_type == "visit" else f"(no {name_col})")
571
+
572
+ perm_val, perm_src = resolve_permission_with_source(conn, entity_type, entity_id)
573
+ vis_val, vis_src = resolve_visibility_with_source(conn, entity_type, entity_id)
574
+ perm_str = "allow" if perm_val else "deny"
575
+
576
+ chain = build_entity_policy_chain(
577
+ conn,
578
+ entity_type,
579
+ entity_id,
580
+ project_id=row["project_id"] if "project_id" in row.keys() else None,
581
+ client_id=row["client_id"] if "client_id" in row.keys() else None,
582
+ account=row["account"] if "account" in row.keys() else None,
583
+ )
584
+
585
+ if json_output:
586
+ data = {
587
+ "entity_type": entity_type,
588
+ "entity_id": entity_id,
589
+ "display_name": display_name,
590
+ "permission": {"resolved": perm_str, "source": perm_src},
591
+ "visibility": {"resolved": vis_val, "source": vis_src},
592
+ "chain": chain,
593
+ }
594
+ if verbose:
595
+ data["verbose_note"] = "single-entity output already includes the full policy chain"
596
+ output_json(data)
597
+ else:
598
+ console.print(f"\n{entity_type.title()} Check: [bold]{display_name}[/bold] (id={entity_id})")
599
+ console.print()
600
+ console.print(f" Permission: [bold]{perm_str}[/bold] (from {perm_src})")
601
+ console.print(f" Visibility: [bold]{vis_val}[/bold] (from {vis_src})")
602
+ if chain:
603
+ console.print()
604
+ print_policy_chain(chain)
605
+ if verbose:
606
+ console.print(
607
+ " [dim](--verbose adds no additional detail — single-entity output "
608
+ "already includes the full policy chain)[/dim]"
609
+ )
610
+
611
+ return 0
612
+
613
+
614
+ def _lookup_scope_policy(conn: sqlite3.Connection, scope: str) -> dict:
615
+ """Look up permission and visibility policy for a single scope string."""
616
+ perm = conn.execute(
617
+ "SELECT setting FROM permission_policies WHERE scope = ?", (scope,),
618
+ ).fetchone()
619
+ vis = conn.execute(
620
+ "SELECT setting FROM visibility_policies WHERE scope = ?", (scope,),
621
+ ).fetchone()
622
+ return {
623
+ "scope": scope,
624
+ "permission": perm["setting"] if perm else None,
625
+ "visibility": vis["setting"] if vis else None,
626
+ }
627
+
628
+
629
+ def build_entity_policy_chain(
630
+ conn: sqlite3.Connection,
631
+ entity_type: str,
632
+ entity_id: int,
633
+ project_id: int | None,
634
+ client_id: int | None,
635
+ account: str | None,
636
+ ) -> list[dict]:
637
+ """Build diagnostic policy chain for an entity (email/chat/visit)."""
638
+ from footprinter.permissions import BASELINE_PERMISSION
639
+ from footprinter.visibility import BASELINE_VISIBILITY
640
+
641
+ chain: list[dict] = []
642
+
643
+ if entity_type != "visit":
644
+ chain.append(_lookup_scope_policy(conn, f"{entity_type}:{entity_id}"))
645
+
646
+ if project_id is not None:
647
+ chain.append(_lookup_scope_policy(conn, f"project:{project_id}"))
648
+
649
+ if client_id is not None:
650
+ chain.append(_lookup_scope_policy(conn, f"client:{client_id}"))
651
+
652
+ if account is not None:
653
+ chain.append(_lookup_scope_policy(conn, f"account:{account}"))
654
+
655
+ source_type = _SOURCE_TYPE[entity_type]
656
+ chain.append(_lookup_scope_policy(conn, f"source:{source_type}"))
657
+
658
+ chain.append(_lookup_scope_policy(conn, "global"))
659
+
660
+ chain.append({
661
+ "scope": "baseline",
662
+ "permission": "allow" if BASELINE_PERMISSION else "deny",
663
+ "visibility": BASELINE_VISIBILITY,
664
+ })
665
+
666
+ return chain
667
+
668
+
669
+ # ---------------------------------------------------------------------------
670
+ # Policy chain / simulation (file-path specific)
488
671
  # ---------------------------------------------------------------------------
489
672
 
490
673
 
@@ -503,21 +686,7 @@ def build_policy_chain(
503
686
 
504
687
  # 1. File-level
505
688
  if file_id is not None:
506
- perm = conn.execute(
507
- "SELECT setting FROM permission_policies WHERE scope = ?",
508
- (f"file:{file_id}",),
509
- ).fetchone()
510
- vis = conn.execute(
511
- "SELECT setting FROM visibility_policies WHERE scope = ?",
512
- (f"file:{file_id}",),
513
- ).fetchone()
514
- chain.append(
515
- {
516
- "scope": f"file:{file_id}",
517
- "permission": perm["setting"] if perm else None,
518
- "visibility": vis["setting"] if vis else None,
519
- }
520
- )
689
+ chain.append(_lookup_scope_policy(conn, f"file:{file_id}"))
521
690
 
522
691
  # 2. Folder prefix policies (longest first)
523
692
  if path:
@@ -550,61 +719,17 @@ def build_policy_chain(
550
719
 
551
720
  # 3. Project-level
552
721
  if project_id is not None:
553
- perm = conn.execute(
554
- "SELECT setting FROM permission_policies WHERE scope = ?",
555
- (f"project:{project_id}",),
556
- ).fetchone()
557
- vis = conn.execute(
558
- "SELECT setting FROM visibility_policies WHERE scope = ?",
559
- (f"project:{project_id}",),
560
- ).fetchone()
561
- chain.append(
562
- {
563
- "scope": f"project:{project_id}",
564
- "permission": perm["setting"] if perm else None,
565
- "visibility": vis["setting"] if vis else None,
566
- }
567
- )
722
+ chain.append(_lookup_scope_policy(conn, f"project:{project_id}"))
568
723
 
569
724
  # 4. Client-level
570
725
  if client_id is not None:
571
- perm = conn.execute(
572
- "SELECT setting FROM permission_policies WHERE scope = ?",
573
- (f"client:{client_id}",),
574
- ).fetchone()
575
- vis = conn.execute(
576
- "SELECT setting FROM visibility_policies WHERE scope = ?",
577
- (f"client:{client_id}",),
578
- ).fetchone()
579
- chain.append(
580
- {
581
- "scope": f"client:{client_id}",
582
- "permission": perm["setting"] if perm else None,
583
- "visibility": vis["setting"] if vis else None,
584
- }
585
- )
726
+ chain.append(_lookup_scope_policy(conn, f"client:{client_id}"))
586
727
 
587
728
  # 5. Source: files
588
- src_perm = conn.execute("SELECT setting FROM permission_policies WHERE scope = 'source:files'").fetchone()
589
- src_vis = conn.execute("SELECT setting FROM visibility_policies WHERE scope = 'source:files'").fetchone()
590
- chain.append(
591
- {
592
- "scope": "source:files",
593
- "permission": src_perm["setting"] if src_perm else None,
594
- "visibility": src_vis["setting"] if src_vis else None,
595
- }
596
- )
729
+ chain.append(_lookup_scope_policy(conn, "source:files"))
597
730
 
598
731
  # 6. Global
599
- global_perm = conn.execute("SELECT setting FROM permission_policies WHERE scope = 'global'").fetchone()
600
- global_vis = conn.execute("SELECT setting FROM visibility_policies WHERE scope = 'global'").fetchone()
601
- chain.append(
602
- {
603
- "scope": "global",
604
- "permission": global_perm["setting"] if global_perm else None,
605
- "visibility": global_vis["setting"] if global_vis else None,
606
- }
607
- )
732
+ chain.append(_lookup_scope_policy(conn, "global"))
608
733
 
609
734
  # 7. Baseline
610
735
  chain.append(