footprinter-cli 1.0.0rc1__py3-none-any.whl
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/__init__.py +8 -0
- footprinter/access.py +431 -0
- footprinter/api/__init__.py +1 -0
- footprinter/api/db.py +61 -0
- footprinter/api/entities.py +250 -0
- footprinter/api/search.py +47 -0
- footprinter/api/semantic.py +33 -0
- footprinter/api/server.py +66 -0
- footprinter/api/status.py +15 -0
- footprinter/bundled/__init__.py +0 -0
- footprinter/bundled/config.example.yaml +161 -0
- footprinter/bundled/patterns/context_patterns.yaml +18 -0
- footprinter/bundled/patterns/extensions.yaml +283 -0
- footprinter/bundled/patterns/filename_patterns.yaml +61 -0
- footprinter/bundled/patterns/mime_mappings.yaml +68 -0
- footprinter/bundled/patterns/salesforce_rules.yaml +84 -0
- footprinter/bundled/patterns/security_patterns.yaml +27 -0
- footprinter/bundled/samples/hidden-client-file-sample.txt +2 -0
- footprinter/bundled/samples/opaque-project-file-sample.txt +2 -0
- footprinter/bundled/samples/visible-file-sample.txt +2 -0
- footprinter/cli/__init__.py +135 -0
- footprinter/cli/__main__.py +6 -0
- footprinter/cli/_common.py +327 -0
- footprinter/cli/_policy_helpers.py +646 -0
- footprinter/cli/_prompt.py +220 -0
- footprinter/cli/_sample_seed.py +204 -0
- footprinter/cli/api_cmd.py +32 -0
- footprinter/cli/connect.py +591 -0
- footprinter/cli/data.py +879 -0
- footprinter/cli/delete.py +128 -0
- footprinter/cli/ingest.py +543 -0
- footprinter/cli/mcp_cmd.py +750 -0
- footprinter/cli/mcp_setup.py +306 -0
- footprinter/cli/search.py +393 -0
- footprinter/cli/search_cmd.py +69 -0
- footprinter/cli/setup.py +2001 -0
- footprinter/cli/status.py +747 -0
- footprinter/cli/status_cmd.py +104 -0
- footprinter/cli/upsert.py +794 -0
- footprinter/cli/vectorize_cmd.py +215 -0
- footprinter/cli/view.py +322 -0
- footprinter/connectors/__init__.py +171 -0
- footprinter/connectors/config_utils.py +141 -0
- footprinter/db/__init__.py +37 -0
- footprinter/db/browser.py +198 -0
- footprinter/db/chats.py +602 -0
- footprinter/db/clients.py +307 -0
- footprinter/db/emails.py +279 -0
- footprinter/db/files.py +724 -0
- footprinter/db/folders.py +659 -0
- footprinter/db/messages.py +192 -0
- footprinter/db/policies.py +151 -0
- footprinter/db/projects.py +673 -0
- footprinter/db/search.py +573 -0
- footprinter/db/sql_utils.py +168 -0
- footprinter/db/status.py +320 -0
- footprinter/db/uploads.py +70 -0
- footprinter/ingest/__init__.py +0 -0
- footprinter/ingest/adapters/__init__.py +33 -0
- footprinter/ingest/adapters/browser.py +54 -0
- footprinter/ingest/adapters/chat.py +57 -0
- footprinter/ingest/adapters/ingest.py +146 -0
- footprinter/ingest/adapters/local_files.py +68 -0
- footprinter/ingest/adapters/local_folders.py +52 -0
- footprinter/ingest/adapters/protocol.py +174 -0
- footprinter/ingest/browser_indexer.py +216 -0
- footprinter/ingest/chat_dedup.py +156 -0
- footprinter/ingest/chat_indexer.py +487 -0
- footprinter/ingest/chat_parsers/__init__.py +8 -0
- footprinter/ingest/chat_parsers/chatgpt_parser.py +229 -0
- footprinter/ingest/chat_parsers/claude_parser.py +161 -0
- footprinter/ingest/cli.py +827 -0
- footprinter/ingest/content_extractors.py +117 -0
- footprinter/ingest/database.py +36 -0
- footprinter/ingest/db/__init__.py +1 -0
- footprinter/ingest/db/connector_schema.py +47 -0
- footprinter/ingest/db/migration.py +315 -0
- footprinter/ingest/db/schema.py +1043 -0
- footprinter/ingest/db/security.py +6 -0
- footprinter/ingest/file_indexer.py +223 -0
- footprinter/ingest/file_scanner.py +277 -0
- footprinter/ingest/folder_indexer.py +226 -0
- footprinter/ingest/full_content_extractor.py +321 -0
- footprinter/ingest/orchestrator.py +112 -0
- footprinter/ingest/pipe_runner.py +200 -0
- footprinter/ingest/processing.py +165 -0
- footprinter/ingest/registry.py +186 -0
- footprinter/ingest/run_record.py +91 -0
- footprinter/ingest/status.py +346 -0
- footprinter/mcp/__init__.py +0 -0
- footprinter/mcp/__main__.py +5 -0
- footprinter/mcp/db.py +67 -0
- footprinter/mcp/errors.py +105 -0
- footprinter/mcp/extraction.py +226 -0
- footprinter/mcp/server.py +39 -0
- footprinter/mcp/tools/__init__.py +0 -0
- footprinter/mcp/tools/navigation.py +70 -0
- footprinter/mcp/tools/read.py +75 -0
- footprinter/mcp/tools/search.py +158 -0
- footprinter/mcp/tools/semantic.py +79 -0
- footprinter/mcp/tools/status.py +19 -0
- footprinter/paths.py +117 -0
- footprinter/permissions.py +1152 -0
- footprinter/semantic/__init__.py +13 -0
- footprinter/semantic/chunking.py +52 -0
- footprinter/semantic/embeddings.py +23 -0
- footprinter/semantic/hybrid_search.py +273 -0
- footprinter/semantic/vector_store.py +471 -0
- footprinter/services/__init__.py +49 -0
- footprinter/services/access_service.py +342 -0
- footprinter/services/chat_service.py +85 -0
- footprinter/services/client_service.py +267 -0
- footprinter/services/content_service.py +181 -0
- footprinter/services/email_service.py +89 -0
- footprinter/services/file_service.py +83 -0
- footprinter/services/folder_service.py +122 -0
- footprinter/services/includes.py +19 -0
- footprinter/services/ingest_service.py +231 -0
- footprinter/services/project_service.py +262 -0
- footprinter/services/roles.py +25 -0
- footprinter/services/search_service.py +177 -0
- footprinter/services/semantic_service.py +360 -0
- footprinter/services/status_service.py +18 -0
- footprinter/services/visit_service.py +65 -0
- footprinter/source_registry.py +194 -0
- footprinter/utils/__init__.py +7 -0
- footprinter/utils/hash_utils.py +59 -0
- footprinter/utils/logging_config.py +68 -0
- footprinter/utils/mime.py +30 -0
- footprinter/utils/text.py +6 -0
- footprinter/utils/time.py +11 -0
- footprinter/visibility.py +1264 -0
- footprinter_cli-1.0.0rc1.dist-info/LICENSE +21 -0
- footprinter_cli-1.0.0rc1.dist-info/METADATA +223 -0
- footprinter_cli-1.0.0rc1.dist-info/RECORD +138 -0
- footprinter_cli-1.0.0rc1.dist-info/WHEEL +5 -0
- footprinter_cli-1.0.0rc1.dist-info/entry_points.txt +2 -0
- footprinter_cli-1.0.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
"""fp mcp — MCP server and access policy management.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
fp mcp Start the MCP server
|
|
5
|
+
fp mcp view {show,set,delete,check,reset} Visibility policy management
|
|
6
|
+
fp mcp read {show,set,delete,check,reset} Permission policy management
|
|
7
|
+
fp mcp check [path] Combined resolution (both layers)
|
|
8
|
+
fp mcp bulk Bulk policy changes
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from footprinter.cli._common import FORMATTER, add_json_flag, console, output_json
|
|
16
|
+
from footprinter.cli._policy_helpers import (
|
|
17
|
+
abbreviate_home,
|
|
18
|
+
bulk_apply,
|
|
19
|
+
check_client,
|
|
20
|
+
check_file_path,
|
|
21
|
+
check_folder,
|
|
22
|
+
check_project,
|
|
23
|
+
confirm_recalculation,
|
|
24
|
+
get_policy_db,
|
|
25
|
+
recalculate_with_progress,
|
|
26
|
+
simulate_path_permission,
|
|
27
|
+
simulate_path_visibility,
|
|
28
|
+
)
|
|
29
|
+
from footprinter.db.policies import (
|
|
30
|
+
PERMISSION_SETTINGS,
|
|
31
|
+
VISIBILITY_SETTINGS,
|
|
32
|
+
clear_permission_policies,
|
|
33
|
+
clear_visibility_policies,
|
|
34
|
+
delete_permission_policy,
|
|
35
|
+
delete_visibility_policy,
|
|
36
|
+
list_permission_policies,
|
|
37
|
+
list_visibility_policies,
|
|
38
|
+
seed_permission_defaults,
|
|
39
|
+
seed_visibility_defaults,
|
|
40
|
+
set_permission_policy,
|
|
41
|
+
set_visibility_policy,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _print_recalc_stats(stats: dict[str, int]) -> None:
|
|
46
|
+
"""Print a dim summary of recalculation results."""
|
|
47
|
+
if not stats:
|
|
48
|
+
return
|
|
49
|
+
parts = [f"{count} {etype}{'s' if count != 1 else ''}" for etype, count in stats.items() if count]
|
|
50
|
+
if parts:
|
|
51
|
+
console.print(f" [dim]Recalculated: {', '.join(parts)}[/dim]")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Handler: server start
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _start_server(args) -> None:
|
|
60
|
+
from footprinter.mcp.server import main
|
|
61
|
+
|
|
62
|
+
main()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# View handlers (visibility layer only)
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _view_show(args) -> None:
|
|
71
|
+
json_output = getattr(args, "json", False)
|
|
72
|
+
|
|
73
|
+
conn = get_policy_db()
|
|
74
|
+
if conn is None:
|
|
75
|
+
if json_output:
|
|
76
|
+
output_json([])
|
|
77
|
+
else:
|
|
78
|
+
console.print("[yellow]No database found.[/yellow]")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
rows = list_visibility_policies(conn)
|
|
83
|
+
|
|
84
|
+
if json_output:
|
|
85
|
+
output_json(rows)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if not rows:
|
|
89
|
+
console.print("No visibility policies configured.")
|
|
90
|
+
console.print(" [dim]Run: fp mcp view reset[/dim]")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
table = Table(title="Visibility Policies")
|
|
94
|
+
table.add_column("Scope", style="cyan")
|
|
95
|
+
table.add_column("Setting")
|
|
96
|
+
table.add_column("Updated", style="dim")
|
|
97
|
+
for row in rows:
|
|
98
|
+
table.add_row(row["scope"], row["setting"], str(row["updated_at"] or ""))
|
|
99
|
+
console.print(table)
|
|
100
|
+
|
|
101
|
+
console.print()
|
|
102
|
+
console.print("[dim]Baseline (when no policy matches): visibility=opaque[/dim]")
|
|
103
|
+
finally:
|
|
104
|
+
conn.close()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _view_set(args) -> None:
|
|
108
|
+
setting = args.level
|
|
109
|
+
if setting not in VISIBILITY_SETTINGS:
|
|
110
|
+
console.print(
|
|
111
|
+
f"[red]Invalid visibility setting:[/red] {setting}\n Valid: {', '.join(sorted(VISIBILITY_SETTINGS))}"
|
|
112
|
+
)
|
|
113
|
+
raise SystemExit(1)
|
|
114
|
+
|
|
115
|
+
conn = get_policy_db()
|
|
116
|
+
if conn is None:
|
|
117
|
+
console.print("[yellow]No database found.[/yellow]")
|
|
118
|
+
raise SystemExit(1)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
yes = getattr(args, "yes", False)
|
|
122
|
+
if not confirm_recalculation(conn, args.scope, yes=yes):
|
|
123
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
124
|
+
return
|
|
125
|
+
set_visibility_policy(conn, args.scope, setting)
|
|
126
|
+
console.print(f"Set visibility_policies: [cyan]{args.scope}[/cyan] = [bold]{setting}[/bold]")
|
|
127
|
+
stats = recalculate_with_progress(conn, args.scope)
|
|
128
|
+
_print_recalc_stats(stats)
|
|
129
|
+
finally:
|
|
130
|
+
conn.close()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _view_delete(args) -> None:
|
|
134
|
+
conn = get_policy_db()
|
|
135
|
+
if conn is None:
|
|
136
|
+
console.print("[yellow]No database found.[/yellow]")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
exists = conn.execute("SELECT 1 FROM visibility_policies WHERE scope = ?", (args.scope,)).fetchone()
|
|
141
|
+
if not exists:
|
|
142
|
+
console.print(f"No visibility policy found for scope [cyan]{args.scope}[/cyan]")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
yes = getattr(args, "yes", False)
|
|
146
|
+
if not confirm_recalculation(conn, args.scope, yes=yes):
|
|
147
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
148
|
+
return
|
|
149
|
+
deleted = delete_visibility_policy(conn, args.scope)
|
|
150
|
+
if deleted:
|
|
151
|
+
console.print(f"Deleted visibility policy for [cyan]{args.scope}[/cyan]")
|
|
152
|
+
stats = recalculate_with_progress(conn, args.scope)
|
|
153
|
+
_print_recalc_stats(stats)
|
|
154
|
+
else:
|
|
155
|
+
console.print(f"No visibility policy found for scope [cyan]{args.scope}[/cyan]")
|
|
156
|
+
finally:
|
|
157
|
+
conn.close()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _view_check(args) -> None:
|
|
161
|
+
from footprinter.visibility import BASELINE_VISIBILITY, resolve_visibility_with_source
|
|
162
|
+
|
|
163
|
+
path = getattr(args, "path", None)
|
|
164
|
+
json_output = getattr(args, "json", False)
|
|
165
|
+
|
|
166
|
+
conn = get_policy_db()
|
|
167
|
+
if conn is None:
|
|
168
|
+
vis_str = BASELINE_VISIBILITY
|
|
169
|
+
if json_output:
|
|
170
|
+
output_json(
|
|
171
|
+
{
|
|
172
|
+
"path": path or "(none)",
|
|
173
|
+
"visibility": {"resolved": vis_str, "source": "baseline"},
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
console.print("[yellow]No database found.[/yellow] Showing baseline.")
|
|
178
|
+
console.print(f" Visibility: [bold]{vis_str}[/bold] (baseline)")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
if not path:
|
|
183
|
+
# Show global resolution
|
|
184
|
+
row = conn.execute("SELECT setting FROM visibility_policies WHERE scope = 'global'").fetchone()
|
|
185
|
+
resolved = row["setting"] if row else BASELINE_VISIBILITY
|
|
186
|
+
source = "global" if row else "baseline"
|
|
187
|
+
if json_output:
|
|
188
|
+
output_json({"visibility": {"resolved": resolved, "source": source}})
|
|
189
|
+
else:
|
|
190
|
+
console.print(f" Visibility: [bold]{resolved}[/bold] (from {source})")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
expanded = os.path.expanduser(os.path.normpath(path))
|
|
194
|
+
display = abbreviate_home(expanded)
|
|
195
|
+
|
|
196
|
+
# Try to find file in DB
|
|
197
|
+
row = conn.execute(
|
|
198
|
+
"SELECT id FROM files WHERE path = ? AND status != 'removed'",
|
|
199
|
+
(expanded,),
|
|
200
|
+
).fetchone()
|
|
201
|
+
|
|
202
|
+
found_in_db = row is not None
|
|
203
|
+
if row:
|
|
204
|
+
vis_val, vis_src = resolve_visibility_with_source(conn, "file", row["id"])
|
|
205
|
+
else:
|
|
206
|
+
# Fall back to folders table
|
|
207
|
+
folder_row = conn.execute(
|
|
208
|
+
"SELECT id FROM folders WHERE path = ?",
|
|
209
|
+
(expanded,),
|
|
210
|
+
).fetchone()
|
|
211
|
+
if folder_row:
|
|
212
|
+
found_in_db = True
|
|
213
|
+
vis_val, vis_src = resolve_visibility_with_source(conn, "folder", folder_row["id"])
|
|
214
|
+
else:
|
|
215
|
+
vis_val, vis_src = simulate_path_visibility(conn, expanded)
|
|
216
|
+
|
|
217
|
+
if json_output:
|
|
218
|
+
output_json(
|
|
219
|
+
{
|
|
220
|
+
"path": display,
|
|
221
|
+
"found_in_db": found_in_db,
|
|
222
|
+
"visibility": {"resolved": vis_val, "source": vis_src},
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
console.print(f"\nVisibility Check: [bold]{display}[/bold]")
|
|
227
|
+
if not found_in_db:
|
|
228
|
+
console.print(" [dim]Not found in files or folders — resolving from policy chain[/dim]")
|
|
229
|
+
console.print(f" Visibility: [bold]{vis_val}[/bold] (from {vis_src})")
|
|
230
|
+
finally:
|
|
231
|
+
conn.close()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _view_reset(args) -> None:
|
|
235
|
+
conn = get_policy_db()
|
|
236
|
+
if conn is None:
|
|
237
|
+
console.print("[yellow]No database found.[/yellow]")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
yes = getattr(args, "yes", False)
|
|
242
|
+
if not confirm_recalculation(conn, "global", yes=yes):
|
|
243
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
244
|
+
return
|
|
245
|
+
deleted = clear_visibility_policies(conn)
|
|
246
|
+
console.print(f"Cleared {deleted} visibility policies")
|
|
247
|
+
seed_visibility_defaults(conn)
|
|
248
|
+
console.print("Re-seeded visibility defaults (global=visible)")
|
|
249
|
+
stats = recalculate_with_progress(conn, "global")
|
|
250
|
+
_print_recalc_stats(stats)
|
|
251
|
+
finally:
|
|
252
|
+
conn.close()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
# Read handlers (permission layer only)
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _read_show(args) -> None:
|
|
261
|
+
json_output = getattr(args, "json", False)
|
|
262
|
+
|
|
263
|
+
conn = get_policy_db()
|
|
264
|
+
if conn is None:
|
|
265
|
+
if json_output:
|
|
266
|
+
output_json([])
|
|
267
|
+
else:
|
|
268
|
+
console.print("[yellow]No database found.[/yellow]")
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
rows = list_permission_policies(conn)
|
|
273
|
+
|
|
274
|
+
if json_output:
|
|
275
|
+
output_json(rows)
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
if not rows:
|
|
279
|
+
console.print("No permission policies configured.")
|
|
280
|
+
console.print(" [dim]Run: fp mcp read reset[/dim]")
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
table = Table(title="Permission Policies")
|
|
284
|
+
table.add_column("Scope", style="cyan")
|
|
285
|
+
table.add_column("Setting")
|
|
286
|
+
table.add_column("Updated", style="dim")
|
|
287
|
+
for row in rows:
|
|
288
|
+
table.add_row(row["scope"], row["setting"], str(row["updated_at"] or ""))
|
|
289
|
+
console.print(table)
|
|
290
|
+
|
|
291
|
+
console.print()
|
|
292
|
+
console.print("[dim]Baseline (when no policy matches): permission=allow[/dim]")
|
|
293
|
+
finally:
|
|
294
|
+
conn.close()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _read_set(args) -> None:
|
|
298
|
+
setting = args.level
|
|
299
|
+
if setting not in PERMISSION_SETTINGS:
|
|
300
|
+
console.print(
|
|
301
|
+
f"[red]Invalid permission setting:[/red] {setting}\n Valid: {', '.join(sorted(PERMISSION_SETTINGS))}"
|
|
302
|
+
)
|
|
303
|
+
raise SystemExit(1)
|
|
304
|
+
|
|
305
|
+
conn = get_policy_db()
|
|
306
|
+
if conn is None:
|
|
307
|
+
console.print("[yellow]No database found.[/yellow]")
|
|
308
|
+
raise SystemExit(1)
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
yes = getattr(args, "yes", False)
|
|
312
|
+
if not confirm_recalculation(conn, args.scope, yes=yes):
|
|
313
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
314
|
+
return
|
|
315
|
+
set_permission_policy(conn, args.scope, setting)
|
|
316
|
+
console.print(f"Set permission_policies: [cyan]{args.scope}[/cyan] = [bold]{setting}[/bold]")
|
|
317
|
+
stats = recalculate_with_progress(conn, args.scope)
|
|
318
|
+
_print_recalc_stats(stats)
|
|
319
|
+
finally:
|
|
320
|
+
conn.close()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _read_delete(args) -> None:
|
|
324
|
+
conn = get_policy_db()
|
|
325
|
+
if conn is None:
|
|
326
|
+
console.print("[yellow]No database found.[/yellow]")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
exists = conn.execute("SELECT 1 FROM permission_policies WHERE scope = ?", (args.scope,)).fetchone()
|
|
331
|
+
if not exists:
|
|
332
|
+
console.print(f"No permission policy found for scope [cyan]{args.scope}[/cyan]")
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
yes = getattr(args, "yes", False)
|
|
336
|
+
if not confirm_recalculation(conn, args.scope, yes=yes):
|
|
337
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
338
|
+
return
|
|
339
|
+
deleted = delete_permission_policy(conn, args.scope)
|
|
340
|
+
if deleted:
|
|
341
|
+
console.print(f"Deleted permission policy for [cyan]{args.scope}[/cyan]")
|
|
342
|
+
stats = recalculate_with_progress(conn, args.scope)
|
|
343
|
+
_print_recalc_stats(stats)
|
|
344
|
+
else:
|
|
345
|
+
console.print(f"No permission policy found for scope [cyan]{args.scope}[/cyan]")
|
|
346
|
+
finally:
|
|
347
|
+
conn.close()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _read_check(args) -> None:
|
|
351
|
+
from footprinter.permissions import BASELINE_PERMISSION, resolve_permission_with_source
|
|
352
|
+
|
|
353
|
+
path = getattr(args, "path", None)
|
|
354
|
+
json_output = getattr(args, "json", False)
|
|
355
|
+
|
|
356
|
+
conn = get_policy_db()
|
|
357
|
+
if conn is None:
|
|
358
|
+
perm_str = "allow" if BASELINE_PERMISSION else "deny"
|
|
359
|
+
if json_output:
|
|
360
|
+
output_json(
|
|
361
|
+
{
|
|
362
|
+
"path": path or "(none)",
|
|
363
|
+
"permission": {"resolved": perm_str, "source": "baseline"},
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
else:
|
|
367
|
+
console.print("[yellow]No database found.[/yellow] Showing baseline.")
|
|
368
|
+
console.print(f" Permission: [bold]{perm_str}[/bold] (baseline)")
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
if not path:
|
|
373
|
+
row = conn.execute("SELECT setting FROM permission_policies WHERE scope = 'global'").fetchone()
|
|
374
|
+
resolved = row["setting"] if row else ("allow" if BASELINE_PERMISSION else "deny")
|
|
375
|
+
source = "global" if row else "baseline"
|
|
376
|
+
if json_output:
|
|
377
|
+
output_json({"permission": {"resolved": resolved, "source": source}})
|
|
378
|
+
else:
|
|
379
|
+
console.print(f" Permission: [bold]{resolved}[/bold] (from {source})")
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
expanded = os.path.expanduser(os.path.normpath(path))
|
|
383
|
+
display = abbreviate_home(expanded)
|
|
384
|
+
|
|
385
|
+
row = conn.execute(
|
|
386
|
+
"SELECT id FROM files WHERE path = ? AND status != 'removed'",
|
|
387
|
+
(expanded,),
|
|
388
|
+
).fetchone()
|
|
389
|
+
|
|
390
|
+
found_in_db = row is not None
|
|
391
|
+
if row:
|
|
392
|
+
perm_val, perm_src = resolve_permission_with_source(conn, "file", row["id"])
|
|
393
|
+
perm_str = "allow" if perm_val else "deny"
|
|
394
|
+
else:
|
|
395
|
+
# Fall back to folders table
|
|
396
|
+
folder_row = conn.execute(
|
|
397
|
+
"SELECT id FROM folders WHERE path = ?",
|
|
398
|
+
(expanded,),
|
|
399
|
+
).fetchone()
|
|
400
|
+
if folder_row:
|
|
401
|
+
found_in_db = True
|
|
402
|
+
perm_val, perm_src = resolve_permission_with_source(conn, "folder", folder_row["id"])
|
|
403
|
+
perm_str = "allow" if perm_val else "deny"
|
|
404
|
+
else:
|
|
405
|
+
perm_str, perm_src = simulate_path_permission(conn, expanded)
|
|
406
|
+
|
|
407
|
+
if json_output:
|
|
408
|
+
output_json(
|
|
409
|
+
{
|
|
410
|
+
"path": display,
|
|
411
|
+
"found_in_db": found_in_db,
|
|
412
|
+
"permission": {"resolved": perm_str, "source": perm_src},
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
else:
|
|
416
|
+
console.print(f"\nPermission Check: [bold]{display}[/bold]")
|
|
417
|
+
if not found_in_db:
|
|
418
|
+
console.print(" [dim]Not found in files or folders — resolving from policy chain[/dim]")
|
|
419
|
+
console.print(f" Permission: [bold]{perm_str}[/bold] (from {perm_src})")
|
|
420
|
+
finally:
|
|
421
|
+
conn.close()
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _read_reset(args) -> None:
|
|
425
|
+
conn = get_policy_db()
|
|
426
|
+
if conn is None:
|
|
427
|
+
console.print("[yellow]No database found.[/yellow]")
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
yes = getattr(args, "yes", False)
|
|
432
|
+
if not confirm_recalculation(conn, "global", yes=yes):
|
|
433
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
434
|
+
return
|
|
435
|
+
deleted = clear_permission_policies(conn)
|
|
436
|
+
console.print(f"Cleared {deleted} permission policies")
|
|
437
|
+
seed_permission_defaults(conn)
|
|
438
|
+
console.print("Re-seeded permission defaults (global=allow)")
|
|
439
|
+
stats = recalculate_with_progress(conn, "global")
|
|
440
|
+
_print_recalc_stats(stats)
|
|
441
|
+
finally:
|
|
442
|
+
conn.close()
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# ---------------------------------------------------------------------------
|
|
446
|
+
# Combined check handler (both layers)
|
|
447
|
+
# ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _check(args) -> None:
|
|
451
|
+
from footprinter.permissions import BASELINE_PERMISSION
|
|
452
|
+
from footprinter.visibility import BASELINE_VISIBILITY
|
|
453
|
+
|
|
454
|
+
path = getattr(args, "path", None)
|
|
455
|
+
folder = getattr(args, "folder", None)
|
|
456
|
+
project = getattr(args, "project", None)
|
|
457
|
+
client = getattr(args, "client", None)
|
|
458
|
+
json_output = getattr(args, "json", False)
|
|
459
|
+
verbose = getattr(args, "verbose", False)
|
|
460
|
+
|
|
461
|
+
targets = []
|
|
462
|
+
if path:
|
|
463
|
+
targets.append("path")
|
|
464
|
+
if folder:
|
|
465
|
+
targets.append("folder")
|
|
466
|
+
if project is not None:
|
|
467
|
+
targets.append("project")
|
|
468
|
+
if client is not None:
|
|
469
|
+
targets.append("client")
|
|
470
|
+
|
|
471
|
+
if len(targets) == 0:
|
|
472
|
+
console.print("[red]No target specified.[/red]")
|
|
473
|
+
console.print()
|
|
474
|
+
console.print("Usage: fp mcp check <path>")
|
|
475
|
+
console.print()
|
|
476
|
+
console.print("Examples:")
|
|
477
|
+
console.print(" fp mcp check ~/Work/file.py Check a file or folder")
|
|
478
|
+
console.print(" fp mcp check --folder ~/Work Check a folder (aggregate view)")
|
|
479
|
+
console.print(" fp mcp check --project 3 Check a project")
|
|
480
|
+
raise SystemExit(1)
|
|
481
|
+
if len(targets) > 1:
|
|
482
|
+
console.print("[red]Specify only one target.[/red] Got: " + ", ".join(targets))
|
|
483
|
+
raise SystemExit(1)
|
|
484
|
+
|
|
485
|
+
conn = get_policy_db()
|
|
486
|
+
if conn is None:
|
|
487
|
+
perm_str = "allow" if BASELINE_PERMISSION else "deny"
|
|
488
|
+
vis_str = BASELINE_VISIBILITY
|
|
489
|
+
if json_output:
|
|
490
|
+
output_json(
|
|
491
|
+
{
|
|
492
|
+
"path": path or folder or str(project) or str(client),
|
|
493
|
+
"found_in_db": False,
|
|
494
|
+
"permission": {"resolved": perm_str, "source": "baseline"},
|
|
495
|
+
"visibility": {"resolved": vis_str, "source": "baseline"},
|
|
496
|
+
"chain": [{"scope": "baseline", "permission": perm_str, "visibility": vis_str}],
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
console.print("[yellow]No database found.[/yellow] Showing baseline defaults.")
|
|
501
|
+
console.print(f" Permission: [bold]{perm_str}[/bold] (baseline)")
|
|
502
|
+
console.print(f" Visibility: [bold]{vis_str}[/bold] (baseline)")
|
|
503
|
+
return
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
if path:
|
|
507
|
+
check_file_path(conn, path, json_output, verbose)
|
|
508
|
+
elif folder:
|
|
509
|
+
check_folder(conn, folder, json_output, verbose)
|
|
510
|
+
elif project is not None:
|
|
511
|
+
check_project(conn, project, json_output)
|
|
512
|
+
elif client is not None:
|
|
513
|
+
check_client(conn, client, json_output)
|
|
514
|
+
finally:
|
|
515
|
+
conn.close()
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ---------------------------------------------------------------------------
|
|
519
|
+
# Bulk handler (both layers)
|
|
520
|
+
# ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _bulk(args) -> None:
|
|
524
|
+
conn = get_policy_db()
|
|
525
|
+
if conn is None:
|
|
526
|
+
console.print("[yellow]No database found.[/yellow]")
|
|
527
|
+
raise SystemExit(1)
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
rc = bulk_apply(
|
|
531
|
+
conn,
|
|
532
|
+
folder=getattr(args, "folder", None),
|
|
533
|
+
project=getattr(args, "project", None),
|
|
534
|
+
permission=getattr(args, "permission", None),
|
|
535
|
+
visibility=getattr(args, "visibility", None),
|
|
536
|
+
dry_run=getattr(args, "dry_run", False),
|
|
537
|
+
yes=getattr(args, "yes", False),
|
|
538
|
+
)
|
|
539
|
+
if rc:
|
|
540
|
+
raise SystemExit(rc)
|
|
541
|
+
finally:
|
|
542
|
+
conn.close()
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# ---------------------------------------------------------------------------
|
|
546
|
+
# Parser registration
|
|
547
|
+
# ---------------------------------------------------------------------------
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _add_set_parser(subparsers, *, dest: str, handler) -> None:
|
|
551
|
+
"""Add a ``set <scope> <level>`` parser to *subparsers*."""
|
|
552
|
+
p = subparsers.add_parser(
|
|
553
|
+
"set",
|
|
554
|
+
help="Set a policy",
|
|
555
|
+
description=("Set a policy for a scope.\n\nScopes: global, folder:~/path, project:<id>, client:<id>."),
|
|
556
|
+
epilog=(
|
|
557
|
+
"examples:\n"
|
|
558
|
+
" fp mcp view set global visible\n"
|
|
559
|
+
" fp mcp read set folder:~/Work allow\n"
|
|
560
|
+
" fp mcp read set project:3 deny"
|
|
561
|
+
),
|
|
562
|
+
formatter_class=FORMATTER,
|
|
563
|
+
)
|
|
564
|
+
p.add_argument("scope", help="Policy scope (e.g. global, folder:~/Work, project:3)")
|
|
565
|
+
p.add_argument("level", help="Policy value (e.g. allow, deny, visible, opaque, hidden)")
|
|
566
|
+
p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
|
|
567
|
+
p.set_defaults(func=handler)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _add_delete_parser(subparsers, *, handler) -> None:
|
|
571
|
+
p = subparsers.add_parser(
|
|
572
|
+
"delete",
|
|
573
|
+
help="Delete a policy for a scope",
|
|
574
|
+
description="Remove a policy entry, reverting the scope to inherited resolution.",
|
|
575
|
+
formatter_class=FORMATTER,
|
|
576
|
+
)
|
|
577
|
+
p.add_argument("scope", help="Policy scope to delete (e.g. folder:~/Work)")
|
|
578
|
+
p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
|
|
579
|
+
p.set_defaults(func=handler)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _add_check_parser(subparsers, *, handler) -> None:
|
|
583
|
+
p = subparsers.add_parser(
|
|
584
|
+
"check",
|
|
585
|
+
help="Check resolution for a path",
|
|
586
|
+
description="Show how a policy resolves for a specific file path.",
|
|
587
|
+
formatter_class=FORMATTER,
|
|
588
|
+
)
|
|
589
|
+
p.add_argument("path", nargs="?", default=None, help="File path to check")
|
|
590
|
+
add_json_flag(p)
|
|
591
|
+
p.set_defaults(func=handler)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def register(subparsers) -> None:
|
|
595
|
+
"""Register the ``mcp`` subcommand and its verbs."""
|
|
596
|
+
parser = subparsers.add_parser(
|
|
597
|
+
"mcp",
|
|
598
|
+
help="MCP server and access policies",
|
|
599
|
+
description=(
|
|
600
|
+
"Start the MCP server or manage access control policies.\n\n"
|
|
601
|
+
"With no subcommand, starts the MCP server for Claude Desktop.\n"
|
|
602
|
+
"Use view/read subcommands to manage visibility and permission\n"
|
|
603
|
+
"policies, or check/bulk for resolution and batch operations."
|
|
604
|
+
),
|
|
605
|
+
epilog=(
|
|
606
|
+
"examples:\n"
|
|
607
|
+
" fp mcp Start the MCP server\n"
|
|
608
|
+
" fp mcp view show List visibility policies\n"
|
|
609
|
+
" fp mcp read show List permission policies\n"
|
|
610
|
+
" fp mcp check ~/Work/file.py Check combined resolution\n"
|
|
611
|
+
" fp mcp bulk --folder ~/Work --permission allow\n"
|
|
612
|
+
"\n"
|
|
613
|
+
"tip: use 'fp mcp <command> --help' for details on any command."
|
|
614
|
+
),
|
|
615
|
+
formatter_class=FORMATTER,
|
|
616
|
+
)
|
|
617
|
+
parser.set_defaults(func=_start_server)
|
|
618
|
+
|
|
619
|
+
sub = parser.add_subparsers(dest="mcp_command", metavar="COMMAND", title="commands (one required)")
|
|
620
|
+
|
|
621
|
+
# -- view subgroup (visibility) --
|
|
622
|
+
view_parser = sub.add_parser(
|
|
623
|
+
"view",
|
|
624
|
+
help="Visibility policy management",
|
|
625
|
+
description=(
|
|
626
|
+
"Manage visibility policies that control what metadata\nClaude can see (visible, opaque, or hidden)."
|
|
627
|
+
),
|
|
628
|
+
epilog=(
|
|
629
|
+
"examples:\n"
|
|
630
|
+
" fp mcp view show List all visibility policies\n"
|
|
631
|
+
" fp mcp view set global visible Set global visibility\n"
|
|
632
|
+
" fp mcp view check ~/Work/file.py Check resolution for a path\n"
|
|
633
|
+
" fp mcp view reset Re-seed defaults"
|
|
634
|
+
),
|
|
635
|
+
formatter_class=FORMATTER,
|
|
636
|
+
)
|
|
637
|
+
view_parser.set_defaults(func=lambda args: view_parser.print_help())
|
|
638
|
+
view_sub = view_parser.add_subparsers(dest="view_command", metavar="COMMAND", title="commands (one required)")
|
|
639
|
+
|
|
640
|
+
show_v = view_sub.add_parser(
|
|
641
|
+
"show",
|
|
642
|
+
help="Show visibility policies",
|
|
643
|
+
description="List all configured visibility policies.",
|
|
644
|
+
formatter_class=FORMATTER,
|
|
645
|
+
)
|
|
646
|
+
add_json_flag(show_v)
|
|
647
|
+
show_v.set_defaults(func=_view_show)
|
|
648
|
+
|
|
649
|
+
_add_set_parser(view_sub, dest="view_command", handler=_view_set)
|
|
650
|
+
_add_delete_parser(view_sub, handler=_view_delete)
|
|
651
|
+
_add_check_parser(view_sub, handler=_view_check)
|
|
652
|
+
|
|
653
|
+
reset_v = view_sub.add_parser(
|
|
654
|
+
"reset",
|
|
655
|
+
help="Clear and re-seed visibility defaults",
|
|
656
|
+
description="Delete all visibility policies and re-seed with defaults (global=visible).",
|
|
657
|
+
formatter_class=FORMATTER,
|
|
658
|
+
)
|
|
659
|
+
reset_v.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
|
|
660
|
+
reset_v.set_defaults(func=_view_reset)
|
|
661
|
+
|
|
662
|
+
# -- read subgroup (permission) --
|
|
663
|
+
read_parser = sub.add_parser(
|
|
664
|
+
"read",
|
|
665
|
+
help="Permission policy management",
|
|
666
|
+
description=("Manage permission policies that control whether Claude\ncan read file content (allow or deny)."),
|
|
667
|
+
epilog=(
|
|
668
|
+
"examples:\n"
|
|
669
|
+
" fp mcp read show List all permission policies\n"
|
|
670
|
+
" fp mcp read set folder:~/Work allow Allow reading Work files\n"
|
|
671
|
+
" fp mcp read check ~/Work/file.py Check resolution for a path\n"
|
|
672
|
+
" fp mcp read reset Re-seed defaults"
|
|
673
|
+
),
|
|
674
|
+
formatter_class=FORMATTER,
|
|
675
|
+
)
|
|
676
|
+
read_parser.set_defaults(func=lambda args: read_parser.print_help())
|
|
677
|
+
read_sub = read_parser.add_subparsers(dest="read_command", metavar="COMMAND", title="commands (one required)")
|
|
678
|
+
|
|
679
|
+
show_r = read_sub.add_parser(
|
|
680
|
+
"show",
|
|
681
|
+
help="Show permission policies",
|
|
682
|
+
description="List all configured permission policies.",
|
|
683
|
+
formatter_class=FORMATTER,
|
|
684
|
+
)
|
|
685
|
+
add_json_flag(show_r)
|
|
686
|
+
show_r.set_defaults(func=_read_show)
|
|
687
|
+
|
|
688
|
+
_add_set_parser(read_sub, dest="read_command", handler=_read_set)
|
|
689
|
+
_add_delete_parser(read_sub, handler=_read_delete)
|
|
690
|
+
_add_check_parser(read_sub, handler=_read_check)
|
|
691
|
+
|
|
692
|
+
reset_r = read_sub.add_parser(
|
|
693
|
+
"reset",
|
|
694
|
+
help="Clear and re-seed permission defaults",
|
|
695
|
+
description="Delete all permission policies and re-seed with defaults (global=allow).",
|
|
696
|
+
formatter_class=FORMATTER,
|
|
697
|
+
)
|
|
698
|
+
reset_r.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
|
|
699
|
+
reset_r.set_defaults(func=_read_reset)
|
|
700
|
+
|
|
701
|
+
# -- check (combined) --
|
|
702
|
+
check_parser = sub.add_parser(
|
|
703
|
+
"check",
|
|
704
|
+
help="Check combined access resolution",
|
|
705
|
+
description=(
|
|
706
|
+
"Check both permission and visibility resolution for a target.\n\n"
|
|
707
|
+
"Specify exactly one target: a file path, --folder, --project, or --client."
|
|
708
|
+
),
|
|
709
|
+
epilog=(
|
|
710
|
+
"examples:\n"
|
|
711
|
+
" fp mcp check ~/Work/file.py Check a file path\n"
|
|
712
|
+
" fp mcp check --folder ~/Work Check a folder\n"
|
|
713
|
+
" fp mcp check --project 3 Check a project\n"
|
|
714
|
+
" fp mcp check --folder ~/Work --verbose Show per-file details"
|
|
715
|
+
),
|
|
716
|
+
formatter_class=FORMATTER,
|
|
717
|
+
)
|
|
718
|
+
check_parser.add_argument("path", nargs="?", default=None, help="File path to check")
|
|
719
|
+
check_parser.add_argument("--folder", default=None, help="Folder path to check")
|
|
720
|
+
check_parser.add_argument("--project", type=int, default=None, help="Project ID to check")
|
|
721
|
+
check_parser.add_argument("--client", type=int, default=None, help="Client ID to check")
|
|
722
|
+
check_parser.add_argument("--verbose", action="store_true", help="Show per-file details")
|
|
723
|
+
add_json_flag(check_parser)
|
|
724
|
+
check_parser.set_defaults(func=_check)
|
|
725
|
+
|
|
726
|
+
# -- bulk --
|
|
727
|
+
bulk_parser = sub.add_parser(
|
|
728
|
+
"bulk",
|
|
729
|
+
help="Bulk policy changes",
|
|
730
|
+
description=(
|
|
731
|
+
"Apply permission and/or visibility policies in bulk.\n\n"
|
|
732
|
+
"Scope by --folder or --project. Set --permission and/or --visibility.\n"
|
|
733
|
+
"Use --dry-run to preview before applying."
|
|
734
|
+
),
|
|
735
|
+
epilog=(
|
|
736
|
+
"examples:\n"
|
|
737
|
+
" fp mcp bulk --folder ~/Work --permission allow\n"
|
|
738
|
+
" fp mcp bulk --project 3 --visibility visible\n"
|
|
739
|
+
" fp mcp bulk --folder ~/Work --permission allow --visibility visible\n"
|
|
740
|
+
" fp mcp bulk --folder ~/Work --permission deny --dry-run"
|
|
741
|
+
),
|
|
742
|
+
formatter_class=FORMATTER,
|
|
743
|
+
)
|
|
744
|
+
bulk_parser.add_argument("--folder", default=None, help="Folder scope (path)")
|
|
745
|
+
bulk_parser.add_argument("--project", type=int, default=None, help="Project ID scope")
|
|
746
|
+
bulk_parser.add_argument("--permission", default=None, help="Permission setting: allow or deny")
|
|
747
|
+
bulk_parser.add_argument("--visibility", default=None, help="Visibility setting: visible, opaque, or hidden")
|
|
748
|
+
bulk_parser.add_argument("--dry-run", action="store_true", dest="dry_run", help="Preview changes without applying")
|
|
749
|
+
bulk_parser.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
|
|
750
|
+
bulk_parser.set_defaults(func=_bulk)
|