footprinter-cli 1.0.5__tar.gz → 1.1.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.
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/PKG-INFO +10 -2
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/README.md +9 -1
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/access_stamper.py +27 -8
- footprinter_cli-1.1.0/footprinter/api/__init__.py +19 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/bundled/config.example.yaml +27 -1
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/__init__.py +4 -1
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/_common.py +4 -7
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/_policy_helpers.py +194 -69
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/_vectorize_stage.py +41 -8
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/add.py +4 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/diagnostics.py +2 -1
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/doctor.py +251 -37
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/ingest.py +3 -3
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/mcp_setup.py +33 -9
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/permission_cmd.py +303 -80
- footprinter_cli-1.1.0/footprinter/cli/search.py +224 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/setup.py +6 -5
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/status.py +65 -399
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/update.py +10 -8
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/clients.py +33 -3
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/files.py +37 -5
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/folders.py +76 -8
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/policies.py +8 -6
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/projects.py +3 -3
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/status.py +23 -6
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db_base.py +18 -7
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/adapters/local_folders.py +10 -2
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/chat_indexer.py +34 -87
- footprinter_cli-1.1.0/footprinter/ingest/database.py +73 -0
- footprinter_cli-1.1.0/footprinter/ingest/db/ddl.py +858 -0
- footprinter_cli-1.1.0/footprinter/ingest/db/fts.py +417 -0
- footprinter_cli-1.1.0/footprinter/ingest/db/schema.py +13 -0
- footprinter_cli-1.1.0/footprinter/ingest/processing.py +526 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/vector_ops.py +119 -54
- footprinter_cli-1.1.0/footprinter/mcp/db.py +72 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/resources/context.py +13 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/server.py +2 -2
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/tools/read.py +15 -9
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/tools/search.py +67 -20
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/paths.py +0 -3
- footprinter_cli-1.1.0/footprinter/permissions.py +292 -0
- footprinter_cli-1.1.0/footprinter/policy_resolver.py +411 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/semantic/chunking.py +18 -2
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/semantic/hybrid_search.py +3 -5
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/semantic/vector_store.py +11 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/access_service.py +10 -5
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/client_service.py +42 -10
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/folder_service.py +11 -1
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/project_service.py +9 -4
- footprinter_cli-1.1.0/footprinter/services/search_service.py +445 -0
- footprinter_cli-1.1.0/footprinter/services/status_service.py +333 -0
- footprinter_cli-1.1.0/footprinter/utils/sqlite_errors.py +58 -0
- footprinter_cli-1.1.0/footprinter/visibility.py +330 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter_cli.egg-info/PKG-INFO +10 -2
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter_cli.egg-info/SOURCES.txt +10 -7
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/pyproject.toml +3 -2
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_access_control_docs.py +42 -4
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_bundled.py +19 -26
- footprinter_cli-1.1.0/tests/test_config_limits.py +186 -0
- footprinter_cli-1.1.0/tests/test_conftest_config.py +25 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_db_base.py +38 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_no_issue_ids.py +20 -12
- footprinter_cli-1.1.0/tests/test_policy_resolver.py +435 -0
- footprinter_cli-1.1.0/tests/test_qa_dispatch.py +149 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_security_layer.py +4 -1
- footprinter_cli-1.1.0/tests/test_verify_install.py +136 -0
- footprinter_cli-1.0.5/footprinter/api/__init__.py +0 -4
- footprinter_cli-1.0.5/footprinter/bundled/patterns/context_patterns.yaml +0 -18
- footprinter_cli-1.0.5/footprinter/bundled/patterns/extensions.yaml +0 -283
- footprinter_cli-1.0.5/footprinter/bundled/patterns/filename_patterns.yaml +0 -61
- footprinter_cli-1.0.5/footprinter/bundled/patterns/mime_mappings.yaml +0 -68
- footprinter_cli-1.0.5/footprinter/bundled/patterns/salesforce_rules.yaml +0 -84
- footprinter_cli-1.0.5/footprinter/bundled/patterns/security_patterns.yaml +0 -27
- footprinter_cli-1.0.5/footprinter/cli/search.py +0 -451
- footprinter_cli-1.0.5/footprinter/ingest/database.py +0 -36
- footprinter_cli-1.0.5/footprinter/ingest/db/schema.py +0 -1182
- footprinter_cli-1.0.5/footprinter/ingest/processing.py +0 -348
- footprinter_cli-1.0.5/footprinter/mcp/db.py +0 -34
- footprinter_cli-1.0.5/footprinter/permissions.py +0 -1160
- footprinter_cli-1.0.5/footprinter/services/search_service.py +0 -161
- footprinter_cli-1.0.5/footprinter/services/status_service.py +0 -18
- footprinter_cli-1.0.5/footprinter/visibility.py +0 -1310
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/LICENSE +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/api/__main__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/api/db.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/api/entities.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/api/search.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/api/semantic.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/api/server.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/api/status.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/bundled/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/__main__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/_prompt.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/connect.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/delete.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/uninstall.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/cli/view.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/connectors/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/connectors/config_utils.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/browser.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/chats.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/emails.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/messages.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/protocols.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/search.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/sql_utils.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/db/uploads.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/adapters/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/adapters/browser.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/adapters/chat.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/adapters/ingest.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/adapters/local_files.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/adapters/protocol.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/browser_indexer.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/chat_parsers/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/chat_parsers/chatgpt_parser.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/chat_parsers/claude_code_parser.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/chat_parsers/claude_parser.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/content_extractors.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/db/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/db/connector_schema.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/file_indexer.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/file_scanner.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/folder_indexer.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/full_content_extractor.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/orchestrator.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/pipe_runner.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/registry.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/run_record.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/scan_summary.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/ingest/status.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/__main__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/errors.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/resources/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/resources/discoverability.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/tools/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/tools/navigation.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/tools/semantic.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/mcp/tools/status.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/semantic/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/semantic/embeddings.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/chat_service.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/content_service.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/email_service.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/file_service.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/includes.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/ingest_service.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/roles.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/semantic_service.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/services/visit_service.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/source_registry.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/utils/__init__.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/utils/exceptions.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/utils/extraction.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/utils/hash_utils.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/utils/logging_config.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/utils/mime.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/utils/paths.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/utils/text.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter/utils/time.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter_cli.egg-info/dependency_links.txt +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter_cli.egg-info/entry_points.txt +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter_cli.egg-info/requires.txt +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/footprinter_cli.egg-info/top_level.txt +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/setup.cfg +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_access_control_bypasses.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_access_recalculate.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_access_source_provenance.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_build_status_filter.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_e2e_install.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_e2e_pipeline.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_edit_recalculate.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_examples.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_files_rename.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_files_surface.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_inherit_resolution.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_logging.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_no_project_root.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_package_init.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_paths_no_test_marker.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_pip_install_e2e.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_prompt_safety.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/tests/test_resolver.py +0 -0
- {footprinter_cli-1.0.5 → footprinter_cli-1.1.0}/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
|
|
3
|
+
Version: 1.1.0
|
|
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/
|
|
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/
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
398
|
+
if commit:
|
|
399
|
+
conn.commit()
|
|
391
400
|
return stats
|
|
392
401
|
|
|
393
402
|
|
|
394
|
-
def recalculate_access(
|
|
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
|
|
419
|
-
Designed for large scopes where a progress bar is
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -88,6 +95,10 @@ indexing:
|
|
|
88
95
|
semantic:
|
|
89
96
|
file_vectorization: false
|
|
90
97
|
chat_vectorization: false
|
|
98
|
+
# File statuses eligible for vectorization. Default: [listed].
|
|
99
|
+
# Add 'unlisted' to also embed hidden/dot-files.
|
|
100
|
+
# vectorize_statuses:
|
|
101
|
+
# - listed
|
|
91
102
|
|
|
92
103
|
# Vectorization — controls what gets embedded for semantic search
|
|
93
104
|
# Requires semantic.file_vectorization: true to take effect for files
|
|
@@ -137,6 +148,21 @@ vectorization:
|
|
|
137
148
|
- "**/.github/**" # GitHub workflow files (FTS-only)
|
|
138
149
|
- "**/.ai-dev/**" # AI dev tool scratch dirs
|
|
139
150
|
|
|
151
|
+
# Operational limits — tune for your data volume
|
|
152
|
+
# All values have sensible defaults; omit the section entirely to use them.
|
|
153
|
+
limits:
|
|
154
|
+
# Zip upload safety checks (chat imports via fp upload)
|
|
155
|
+
zip:
|
|
156
|
+
max_decompressed_size_mb: 2048 # 2 GB; raise for very large exports
|
|
157
|
+
max_entries: 10000 # Maximum files in a zip archive
|
|
158
|
+
max_compression_ratio: 100 # Ratio ceiling (100:1) for zip bomb detection
|
|
159
|
+
# HTTP API pagination cap — requires server restart to take effect
|
|
160
|
+
api_max_limit: 200
|
|
161
|
+
# MCP search result cap per source (protocol payload limit)
|
|
162
|
+
mcp_search_limit_cap: 200
|
|
163
|
+
# Embedding batch size for vector rebuild
|
|
164
|
+
vector_batch_size: 100
|
|
165
|
+
|
|
140
166
|
# Source registry seeds — loaded into the sources table on init
|
|
141
167
|
# Connector sources added by: fp connect install <name>
|
|
142
168
|
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
|
-
|
|
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.
|
|
74
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
60
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|