veilgate 0.1.1__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.
Files changed (67) hide show
  1. veilgate/__init__.py +3 -0
  2. veilgate/cli.py +545 -0
  3. veilgate/config/constants.py +86 -0
  4. veilgate/config/policy_loader.py +332 -0
  5. veilgate/config/policy_models.py +432 -0
  6. veilgate/config/settings.py +90 -0
  7. veilgate/dashboard/api.py +216 -0
  8. veilgate/dashboard/app.py +46 -0
  9. veilgate/dashboard/models.py +65 -0
  10. veilgate/dashboard/static/styles.css +314 -0
  11. veilgate/dashboard/templates/index.html +208 -0
  12. veilgate/engines/operators/__init__.py +61 -0
  13. veilgate/engines/operators/base.py +14 -0
  14. veilgate/engines/operators/checksums.py +91 -0
  15. veilgate/engines/operators/text.py +76 -0
  16. veilgate/exceptions/__init__.py +41 -0
  17. veilgate/exceptions/types.py +267 -0
  18. veilgate/gateway.py +130 -0
  19. veilgate/integrations/__init__.py +1 -0
  20. veilgate/middleware/__init__.py +15 -0
  21. veilgate/middleware/context_middleware.py +95 -0
  22. veilgate/middleware/context_mw.py +26 -0
  23. veilgate/middleware/discovery_mw.py +35 -0
  24. veilgate/middleware/firewall_call_mw.py +65 -0
  25. veilgate/middleware/init_mw.py +11 -0
  26. veilgate/packs/arg-guards.yaml +31 -0
  27. veilgate/packs/bloat-http.yaml +33 -0
  28. veilgate/packs/core-verbs.yaml +35 -0
  29. veilgate/packs/pii-global.yaml +88 -0
  30. veilgate/packs/pii-india.yaml +45 -0
  31. veilgate/packs/sensitive-keys.yaml +64 -0
  32. veilgate/packs/server-profiles.yaml +24 -0
  33. veilgate/pipeline.py +323 -0
  34. veilgate/repositories/__init__.py +35 -0
  35. veilgate/repositories/approval_repo.py +82 -0
  36. veilgate/repositories/audit_repo.py +194 -0
  37. veilgate/repositories/catalog_repo.py +174 -0
  38. veilgate/repositories/db.py +55 -0
  39. veilgate/repositories/migrations/0001_init.sql +118 -0
  40. veilgate/repositories/models.py +123 -0
  41. veilgate/repositories/session_repo.py +82 -0
  42. veilgate/repositories/stash_repo.py +87 -0
  43. veilgate/repositories/vault_repo.py +158 -0
  44. veilgate/security/crypto.py +173 -0
  45. veilgate/services/acl_service.py +152 -0
  46. veilgate/services/approval_service.py +236 -0
  47. veilgate/services/audit_service.py +50 -0
  48. veilgate/services/catalog_service.py +193 -0
  49. veilgate/services/compaction_service.py +111 -0
  50. veilgate/services/diet_service.py +111 -0
  51. veilgate/services/explain_service.py +150 -0
  52. veilgate/services/injection_service.py +73 -0
  53. veilgate/services/minimizer_service.py +611 -0
  54. veilgate/services/models.py +178 -0
  55. veilgate/services/taint_service.py +421 -0
  56. veilgate/services/tokenizer_service.py +80 -0
  57. veilgate/services/upstream_registry.py +209 -0
  58. veilgate/utils/ids.py +30 -0
  59. veilgate/utils/json_paths.py +115 -0
  60. veilgate/utils/logging_utils.py +50 -0
  61. veilgate/utils/text_scan.py +104 -0
  62. veilgate/utils/token_count.py +26 -0
  63. veilgate-0.1.1.dist-info/METADATA +330 -0
  64. veilgate-0.1.1.dist-info/RECORD +67 -0
  65. veilgate-0.1.1.dist-info/WHEEL +4 -0
  66. veilgate-0.1.1.dist-info/entry_points.txt +2 -0
  67. veilgate-0.1.1.dist-info/licenses/LICENSE +21 -0
veilgate/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Veilgate: Local-First Zero-Trust MCP Privacy Firewall & Context-Diet Gateway."""
2
+
3
+ __version__ = "0.1.1"
veilgate/cli.py ADDED
@@ -0,0 +1,545 @@
1
+ """Command-line interface (CLI) for Veilgate Gateway."""
2
+
3
+ import asyncio
4
+ import base64
5
+ import json
6
+ import os
7
+ import shlex
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Annotated
11
+
12
+ import numpy as np
13
+ import typer
14
+ import uvicorn
15
+ import yaml
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+ from ulid import ULID
19
+
20
+ from veilgate.config.policy_loader import PolicyLoader
21
+ from veilgate.config.policy_models import Transport, UpstreamSpec
22
+ from veilgate.config.settings import VeilgateSettings
23
+ from veilgate.dashboard.app import create_dashboard_app
24
+ from veilgate.gateway import create_gateway_pipeline, create_gateway_server
25
+ from veilgate.repositories.audit_repo import AuditRepository
26
+ from veilgate.repositories.catalog_repo import CatalogRepository
27
+ from veilgate.repositories.db import DatabaseManager
28
+ from veilgate.security.crypto import resolve_master_key
29
+ from veilgate.services.explain_service import ExplainService
30
+ from veilgate.services.models import SessionContext, ToolRef
31
+
32
+ app = typer.Typer(
33
+ name="veilgate",
34
+ help="Veilgate: Local-First Zero-Trust MCP Privacy Firewall & Context-Diet Gateway.",
35
+ no_args_is_help=True,
36
+ )
37
+
38
+ policy_app = typer.Typer(name="policy", help="Manage and promote Veilgate security policies.")
39
+ catalog_app = typer.Typer(name="catalog", help="Inspect, fingerprint, and approve tool catalogs.")
40
+ audit_app = typer.Typer(name="audit", help="Verify, export, and tail tamper-evident audit logs.")
41
+
42
+ app.add_typer(policy_app)
43
+ app.add_typer(catalog_app)
44
+ app.add_typer(audit_app)
45
+
46
+
47
+ @app.command()
48
+ def keygen() -> None:
49
+ """Generate a new 32-byte AES-GCM master key and provision to OS keyring / fallback file."""
50
+ raw_key = os.urandom(32)
51
+ b64_key = base64.b64encode(raw_key).decode("utf-8")
52
+ resolve_master_key(b64_key)
53
+ typer.echo(f"Master key generated and securely provisioned:\n{b64_key}")
54
+
55
+
56
+ @policy_app.command("validate")
57
+ @app.command("validate")
58
+ def validate(
59
+ config: Annotated[
60
+ Path,
61
+ typer.Option("--config", "-c", help="Path to policy.yaml"),
62
+ ] = Path("./policy.yaml"),
63
+ ) -> None:
64
+ """Validate and compile 3-layer policy document."""
65
+ loader = PolicyLoader(policy_path=config)
66
+ try:
67
+ policy = loader.load(force_reload=True)
68
+ typer.echo(
69
+ f"Policy '{config}' is valid!\n"
70
+ f"- Extends: {policy.extends}\n"
71
+ f"- Upstreams ({len(policy.upstreams)}): {[u.id for u in policy.upstreams]}\n"
72
+ f"- Protect keywords: {policy.protect}\n"
73
+ f"- Drop keywords: {policy.drop}\n"
74
+ f"- Tool rules: {len(policy.tools)}\n"
75
+ f"- Learned grants: {len(policy.learned_grants)}"
76
+ )
77
+ except Exception as e:
78
+ typer.secho(f"Policy validation failed: {e}", fg=typer.colors.RED, err=True)
79
+ raise typer.Exit(code=1) from e
80
+
81
+
82
+ @policy_app.command("init")
83
+ def policy_init(
84
+ output_path: Annotated[Path, typer.Option("--output", "-o", help="Target path")] = Path("./policy.yaml"),
85
+ profile: Annotated[str, typer.Option("--profile", "-p", help="Base profile: recommended or strict")] = "recommended",
86
+ ) -> None:
87
+ """Initialize a starter policy.yaml configuration file."""
88
+ starter = {
89
+ "version": "1",
90
+ "extends": profile,
91
+ "default_tool_allow": True,
92
+ "upstreams": [],
93
+ "protect": ["margin", "salary", "ssn", "secret"],
94
+ "drop": ["debug_trace", "internal_id"],
95
+ }
96
+ with open(output_path, "w", encoding="utf-8") as f:
97
+ yaml.safe_dump(starter, f, sort_keys=False)
98
+ typer.echo(f"SUCCESS | Created starter {profile} policy at {output_path}")
99
+
100
+
101
+ @policy_app.command("promote")
102
+ def policy_promote(
103
+ config: Annotated[Path, typer.Option("--config", "-c", help="Path to policy.yaml")] = Path("./policy.yaml"),
104
+ ) -> None:
105
+ """Promote interactive dev grants from policy.learned.yaml into policy.yaml."""
106
+ loader = PolicyLoader(policy_path=config)
107
+ count = loader.promote_learned_grants()
108
+ typer.echo(f"SUCCESS | Promoted {count} learned grants into {config}")
109
+
110
+
111
+ @app.command()
112
+ def explain(
113
+ target: Annotated[str, typer.Argument(help="Tool name (e.g. invoicedb.get_invoice) or JSON string")],
114
+ config: Annotated[
115
+ Path,
116
+ typer.Option("--config", "-c", help="Path to policy.yaml"),
117
+ ] = Path("./policy.yaml"),
118
+ ) -> None:
119
+ """Offline sub-50ms zero-LLM policy debugger for tools and payload shapes."""
120
+ loader = PolicyLoader(policy_path=config)
121
+ policy = loader.load()
122
+
123
+ settings = VeilgateSettings(policy_path=config)
124
+
125
+ async def _run_explain() -> None:
126
+ pipeline, _ = await create_gateway_pipeline(settings, policy=policy, policy_loader=loader)
127
+ explain_svc = ExplainService(policy, pipeline.acl, pipeline.catalog_svc)
128
+
129
+ target_str = target.strip()
130
+ if target_str.startswith("{"):
131
+ try:
132
+ payload = json.loads(target_str)
133
+ explanations = explain_svc.explain_payload(payload)
134
+ typer.echo(json.dumps([e.model_dump() for e in explanations], indent=2))
135
+ return
136
+ except json.JSONDecodeError:
137
+ pass
138
+
139
+ tool_exp = explain_svc.explain_tool(target_str)
140
+ typer.echo(json.dumps(tool_exp.model_dump(), indent=2))
141
+
142
+ asyncio.run(_run_explain())
143
+
144
+
145
+ @app.command()
146
+ def adopt(
147
+ client: Annotated[str, typer.Option("--client", help="Target client: cursor or claude")] = "cursor",
148
+ mcp_json: Annotated[Path | None, typer.Option("--mcp-json", help="Explicit path to client mcp.json")] = None,
149
+ output_policy: Annotated[Path, typer.Option("--output", "-o", help="Where to save policy.yaml")] = Path("./policy.yaml"),
150
+ ) -> None:
151
+ """Auto-import upstream MCP servers from Cursor or Claude Desktop and rewire client."""
152
+ target_path = mcp_json
153
+ if not target_path:
154
+ if client.lower() == "cursor":
155
+ target_path = Path(".cursor/mcp.json")
156
+ elif client.lower() == "claude":
157
+ appdata = os.environ.get("APPDATA", "~")
158
+ target_path = Path(appdata) / "Claude" / "claude_desktop_config.json"
159
+ else:
160
+ typer.secho("Unsupported client. Choose 'cursor' or 'claude'.", fg=typer.colors.RED)
161
+ raise typer.Exit(code=1)
162
+
163
+ if not target_path.exists():
164
+ typer.secho(f"Target config '{target_path}' not found.", fg=typer.colors.RED)
165
+ raise typer.Exit(code=1)
166
+
167
+ with open(target_path, encoding="utf-8") as f:
168
+ client_config = json.load(f)
169
+
170
+ servers = client_config.get("mcpServers", {})
171
+ if not servers:
172
+ typer.secho("No 'mcpServers' entries found to adopt.", fg=typer.colors.YELLOW)
173
+ return
174
+
175
+ upstream_specs: list[dict[str, object]] = []
176
+ for srv_id, srv_def in servers.items():
177
+ if srv_id.lower() == "veilgate":
178
+ continue
179
+ clean_id = srv_id.lower().replace("_", "")
180
+ spec: dict[str, object] = {
181
+ "id": clean_id,
182
+ "transport": "stdio",
183
+ "command": srv_def.get("command", "python"),
184
+ "args": srv_def.get("args", []),
185
+ }
186
+ if "env" in srv_def:
187
+ spec["env"] = srv_def["env"]
188
+ upstream_specs.append(spec)
189
+
190
+ minimal_policy = {
191
+ "version": "1",
192
+ "extends": "recommended",
193
+ "upstreams": upstream_specs,
194
+ "protect": ["deal_margin", "salary_band", "diagnosis"],
195
+ "drop": ["internal_row_id", "audit_metadata"],
196
+ }
197
+
198
+ with open(output_policy, "w", encoding="utf-8") as f:
199
+ yaml.safe_dump(minimal_policy, f, sort_keys=False)
200
+ typer.echo(f"SUCCESS | Adopted {len(upstream_specs)} upstreams into {output_policy}")
201
+
202
+ bak_path = target_path.with_suffix(".veilgate.bak.json")
203
+ with open(bak_path, "w", encoding="utf-8") as f:
204
+ json.dump(client_config, f, indent=2)
205
+ typer.echo(f"Backed up original client configuration to {bak_path}")
206
+
207
+ client_config["mcpServers"] = {
208
+ "veilgate": {
209
+ "command": "uvx",
210
+ "args": ["veilgate", "run", "--config", str(output_policy.resolve())],
211
+ }
212
+ }
213
+ with open(target_path, "w", encoding="utf-8") as f:
214
+ json.dump(client_config, f, indent=2)
215
+ typer.echo(f"SUCCESS | Rewired {target_path} to route through Veilgate Gateway!")
216
+
217
+
218
+ @audit_app.command("verify")
219
+ def audit_verify(
220
+ db_path: Annotated[Path, typer.Option("--db", help="Path to SQLite database")] = Path("./veilgate.db"),
221
+ ) -> None:
222
+ """Verify cryptographic audit hash chain integrity."""
223
+ settings = VeilgateSettings(db_path=db_path)
224
+
225
+ async def _verify() -> None:
226
+ mgr = DatabaseManager(settings.db_path)
227
+ await mgr.initialize()
228
+ repo = AuditRepository(mgr)
229
+ is_valid, err = await repo.verify_chain()
230
+ if is_valid:
231
+ typer.secho("Cryptographic audit hash chain is INTACT and verified! No tampering detected.", fg=typer.colors.GREEN)
232
+ else:
233
+ typer.secho(f"SECURITY ALERT: Audit chain verification failed! {err}", fg=typer.colors.RED)
234
+ raise typer.Exit(code=1)
235
+
236
+ asyncio.run(_verify())
237
+
238
+
239
+ @audit_app.command("list")
240
+ @app.command("audit")
241
+ def audit_list(
242
+ action: Annotated[str, typer.Argument(help="Action: verify or list")] = "verify",
243
+ db_path: Annotated[Path, typer.Option("--db", help="Path to SQLite database")] = Path("./veilgate.db"),
244
+ limit: Annotated[int, typer.Option("--limit", "-n", help="Number of records")] = 20,
245
+ ) -> None:
246
+ """List or verify audit events."""
247
+ settings = VeilgateSettings(db_path=db_path)
248
+
249
+ async def _audit() -> None:
250
+ mgr = DatabaseManager(settings.db_path)
251
+ await mgr.initialize()
252
+ repo = AuditRepository(mgr)
253
+
254
+ if action == "verify":
255
+ is_valid, err = await repo.verify_chain()
256
+ if is_valid:
257
+ typer.secho("Cryptographic audit hash chain is INTACT and verified! No tampering detected.", fg=typer.colors.GREEN)
258
+ else:
259
+ typer.secho(f"SECURITY ALERT: Audit chain verification failed! {err}", fg=typer.colors.RED)
260
+ raise typer.Exit(code=1)
261
+ elif action == "list":
262
+ events = await repo.list_events(limit=limit)
263
+ for ev in events:
264
+ typer.echo(f"[{ev.ts}] seq={ev.seq} kind={ev.kind} tool={ev.qualified_ref} reason={ev.reason}")
265
+
266
+ asyncio.run(_audit())
267
+
268
+
269
+ @audit_app.command("tail")
270
+ def audit_tail(
271
+ db_path: Annotated[Path, typer.Option("--db", help="Path to SQLite database")] = Path("./veilgate.db"),
272
+ interval: Annotated[float, typer.Option("--interval", help="Polling interval in seconds")] = 1.0,
273
+ ) -> None:
274
+ """Tail audit events in real time."""
275
+ settings = VeilgateSettings(db_path=db_path)
276
+
277
+ async def _tail() -> None:
278
+ mgr = DatabaseManager(settings.db_path)
279
+ await mgr.initialize()
280
+ repo = AuditRepository(mgr)
281
+ last_seq = 0
282
+ typer.echo("Tailing Veilgate audit stream (Ctrl+C to stop)...")
283
+
284
+ while True:
285
+ events = await repo.list_events(limit=20)
286
+ new_events = [e for e in events if e.seq > last_seq]
287
+ for ev in reversed(new_events):
288
+ typer.echo(f"[{ev.ts}] seq={ev.seq} kind={ev.kind} tool={ev.qualified_ref} reason={ev.reason}")
289
+ if ev.seq > last_seq:
290
+ last_seq = ev.seq
291
+ await asyncio.sleep(interval)
292
+
293
+ try:
294
+ asyncio.run(_tail())
295
+ except KeyboardInterrupt:
296
+ typer.echo("\nStopped tailing audit log.")
297
+
298
+
299
+ @audit_app.command("export")
300
+ def audit_export(
301
+ db_path: Annotated[Path, typer.Option("--db", help="Path to SQLite database")] = Path("./veilgate.db"),
302
+ output: Annotated[Path, typer.Option("--output", "-o", help="Output file path")] = Path("./audit_export.jsonl"),
303
+ format_type: Annotated[str, typer.Option("--format", "-f", help="Format: jsonl or csv")] = "jsonl",
304
+ ) -> None:
305
+ """Export tamper-evident audit log to JSONL or CSV file."""
306
+ settings = VeilgateSettings(db_path=db_path)
307
+
308
+ async def _export() -> None:
309
+ mgr = DatabaseManager(settings.db_path)
310
+ await mgr.initialize()
311
+ repo = AuditRepository(mgr)
312
+ events = await repo.list_events(limit=100_000)
313
+
314
+ with open(output, "w", encoding="utf-8") as f:
315
+ if format_type.lower() == "csv":
316
+ f.write("seq,ts,event_id,session_id,kind,qualified_ref,reason\n")
317
+ for e in events:
318
+ f.write(f'{e.seq},{e.ts},{e.event_id},{e.session_id},{e.kind},{e.qualified_ref or ""},{e.reason}\n')
319
+ else:
320
+ for e in events:
321
+ f.write(e.model_dump_json() + "\n")
322
+
323
+ typer.echo(f"SUCCESS | Exported {len(events)} audit records to {output}")
324
+
325
+ asyncio.run(_export())
326
+
327
+
328
+ @catalog_app.command("approve")
329
+ def catalog_approve(
330
+ tool: Annotated[str, typer.Argument(help="Qualified tool name to approve (e.g. invoicedb.get_invoice)")],
331
+ db_path: Annotated[Path, typer.Option("--db", help="Path to SQLite database")] = Path("./veilgate.db"),
332
+ ) -> None:
333
+ """Approve a quarantined tool, pin its current schema fingerprint, and unquarantine it."""
334
+ settings = VeilgateSettings(db_path=db_path)
335
+
336
+ async def _approve() -> None:
337
+ mgr = DatabaseManager(settings.db_path)
338
+ await mgr.initialize()
339
+ repo = CatalogRepository(mgr)
340
+ entry = await repo.get_tool(tool)
341
+ if not entry:
342
+ typer.secho(f"Tool '{tool}' not found in catalog.", fg=typer.colors.RED)
343
+ raise typer.Exit(code=1)
344
+
345
+ # Update pinned_fp to current fingerprint and unquarantine
346
+ updated = entry.model_copy(update={"pinned_fp": entry.fingerprint, "status": "active"})
347
+ await repo.upsert_tool(updated)
348
+ typer.secho(f"SUCCESS | Tool '{tool}' approved and pinned to fingerprint {entry.fingerprint[:12]}!", fg=typer.colors.GREEN)
349
+
350
+ asyncio.run(_approve())
351
+
352
+
353
+ @catalog_app.callback(invoke_without_command=True)
354
+ def catalog_callback(
355
+ ctx: typer.Context,
356
+ config: Annotated[Path, typer.Option("--config", "-c", help="Path to policy.yaml")] = Path("./policy.yaml"),
357
+ from_mcp_json: Annotated[Path | None, typer.Option("--from-mcp-json", help="Path to mcp.json directly")] = None,
358
+ shapes: Annotated[bool, typer.Option("--shapes", help="Display observed response JSON shapes and classifications")] = False,
359
+ ) -> None:
360
+ """Inspect and tabulate upstream tool capabilities, risk ratings, and trust profiles."""
361
+ if ctx.invoked_subcommand is None:
362
+ catalog_impl(config=config, from_mcp_json=from_mcp_json, shapes=shapes)
363
+
364
+
365
+ @catalog_app.command("list")
366
+ def catalog_list(
367
+ config: Annotated[Path, typer.Option("--config", "-c", help="Path to policy.yaml")] = Path("./policy.yaml"),
368
+ from_mcp_json: Annotated[Path | None, typer.Option("--from-mcp-json", help="Path to mcp.json directly")] = None,
369
+ shapes: Annotated[bool, typer.Option("--shapes", help="Display observed response JSON shapes and classifications")] = False,
370
+ ) -> None:
371
+ """List upstream tools registered in the catalog."""
372
+ catalog_impl(config=config, from_mcp_json=from_mcp_json, shapes=shapes)
373
+
374
+
375
+ def catalog_impl(
376
+ config: Path,
377
+ from_mcp_json: Path | None,
378
+ shapes: bool,
379
+ ) -> None:
380
+ """Implementation of catalog inspection and tabulation."""
381
+ settings = VeilgateSettings(policy_path=config)
382
+ loader = PolicyLoader(
383
+ policy_path=config if not from_mcp_json else None,
384
+ from_mcp_json=from_mcp_json,
385
+ )
386
+
387
+ async def _catalog() -> None:
388
+ pipeline, db_manager = await create_gateway_pipeline(settings, policy_loader=loader)
389
+ console = Console()
390
+
391
+ if shapes:
392
+ async with db_manager.connection() as conn:
393
+ cursor = await conn.execute("SELECT * FROM schema_cache ORDER BY qualified_ref;")
394
+ rows = await cursor.fetchall()
395
+
396
+ table = Table(title="Veilgate Observed Schema Shapes & Classifications")
397
+ table.add_column("Qualified Tool", style="cyan")
398
+ table.add_column("Shape Fingerprint", style="magenta")
399
+ table.add_column("Source", style="green")
400
+ table.add_column("Classifications", style="yellow")
401
+
402
+ for r in rows:
403
+ table.add_row(r["qualified_ref"], r["shape_fingerprint"][:12], r["source"], r["classifications"])
404
+ console.print(table)
405
+ return
406
+
407
+ tools = await pipeline.catalog_repo.list_tools()
408
+ table = Table(title=f"Veilgate Tool Catalog ({len(tools)} tools registered)")
409
+ table.add_column("Qualified Reference", style="cyan", no_wrap=True)
410
+ table.add_column("Server", style="green")
411
+ table.add_column("Capabilities", style="yellow")
412
+ table.add_column("Risk Level", style="magenta")
413
+ table.add_column("Trust", style="blue")
414
+ table.add_column("Status", style="bold")
415
+ table.add_column("Tokens", style="dim", justify="right")
416
+
417
+ for t in tools:
418
+ risk_style = "green" if t.risk_level == "LOW" else ("red" if t.risk_level in ("HIGH", "CRITICAL") else "yellow")
419
+ status_style = "green" if t.status == "active" else "red"
420
+ trust_label = "untrusted" if t.untrusted_tool else "trusted"
421
+ table.add_row(
422
+ t.qualified_ref,
423
+ t.server_id,
424
+ ", ".join(t.capabilities) or "read",
425
+ f"[{risk_style}]{t.risk_level}[/{risk_style}]",
426
+ trust_label,
427
+ f"[{status_style}]{t.status}[/{status_style}]",
428
+ str(t.schema_tokens),
429
+ )
430
+ console.print(table)
431
+
432
+ asyncio.run(_catalog())
433
+
434
+
435
+ @app.command()
436
+ def dashboard(
437
+ config: Annotated[Path, typer.Option("--config", "-c", help="Path to policy.yaml")] = Path("./policy.yaml"),
438
+ port: Annotated[int, typer.Option("--port", "-p", help="Dashboard HTTP port")] = 8765,
439
+ db: Annotated[Path | None, typer.Option("--db", help="Explicit path to SQLite database")] = None,
440
+ ) -> None:
441
+ """Launch the Veilgate operator and reviewer dashboard."""
442
+ settings = VeilgateSettings(policy_path=config, db_path=db) if db else VeilgateSettings(policy_path=config)
443
+ mgr = DatabaseManager(settings.db_path)
444
+ asyncio.run(mgr.initialize())
445
+ app_instance = create_dashboard_app(mgr)
446
+ typer.echo(f"Starting Veilgate Dashboard on http://localhost:{port} (Database: {settings.db_path})")
447
+ uvicorn.run(app_instance, host="127.0.0.1", port=port, log_level="info")
448
+
449
+
450
+ @app.command()
451
+ def bench(
452
+ iterations: Annotated[int, typer.Option("--iterations", "-n", help="Benchmark iterations")] = 100,
453
+ db_path: Annotated[Path, typer.Option("--db", help="Path to benchmark SQLite database")] = Path("./bench.db"),
454
+ ) -> None:
455
+ """Run catalog discovery and minimization latency benchmarks."""
456
+ settings = VeilgateSettings(db_path=db_path)
457
+ loader = PolicyLoader()
458
+ policy = loader.load()
459
+
460
+ async def _run_bench() -> None:
461
+ pipeline, mgr = await create_gateway_pipeline(settings, policy=policy)
462
+ session = SessionContext(session_id="bench_ses")
463
+ await pipeline.sessions.create_or_touch(session.session_id, transport="stdio")
464
+
465
+ raw_payload = {
466
+ "invoice_id": 9012,
467
+ "vendor": "Acme Global",
468
+ "contact_email": "finance@acme.example.com",
469
+ "bank_account": "123456789012",
470
+ "routing_number": "021000021",
471
+ "internal_margin": "32.4%",
472
+ "connection": "close",
473
+ "etag": "W/109485",
474
+ }
475
+ ref = ToolRef(server="srv1", tool="get_invoice")
476
+
477
+ # Warmup
478
+ await pipeline.minimizer.minimize(raw_payload, ref, session, str(ULID()))
479
+
480
+ timings: list[float] = []
481
+ for _ in range(iterations):
482
+ t0 = time.perf_counter()
483
+ await pipeline.minimizer.minimize(raw_payload, ref, session, str(ULID()))
484
+ timings.append((time.perf_counter() - t0) * 1000.0)
485
+
486
+ mean_ms = float(np.mean(timings))
487
+ p95_ms = float(np.percentile(timings, 95))
488
+
489
+ console = Console()
490
+ table = Table(title=f"Veilgate Latency Benchmark ({iterations} iterations)")
491
+ table.add_column("Operation", style="cyan")
492
+ table.add_column("Mean Latency", style="green")
493
+ table.add_column("p95 Latency", style="yellow")
494
+ table.add_column("SLA Target", style="magenta")
495
+ table.add_row("Minimizer Egress Pipeline", f"{mean_ms:.2f} ms", f"{p95_ms:.2f} ms", "< 20.00 ms")
496
+ console.print(table)
497
+
498
+ asyncio.run(_run_bench())
499
+
500
+
501
+ @app.command()
502
+ def run(
503
+ config: Annotated[Path, typer.Option("--config", "-c", help="Path to policy.yaml")] = Path("./policy.yaml"),
504
+ from_mcp_json: Annotated[Path | None, typer.Option("--from-mcp-json", help="Wrap existing mcp.json directly in memory")] = None,
505
+ wrap: Annotated[str | None, typer.Option("--wrap", help="Zero-config wrapper for single stdio command")] = None,
506
+ transport: Annotated[str, typer.Option("--transport", help="Transport mode: stdio or http")] = "stdio",
507
+ port: Annotated[int, typer.Option("--port", "-p", help="Port for HTTP transport")] = 8765,
508
+ ) -> None:
509
+ """Start the Veilgate Gateway server."""
510
+ settings = VeilgateSettings(policy_path=config, http_port=port)
511
+ loader = PolicyLoader(
512
+ policy_path=config if not (from_mcp_json or wrap) else None,
513
+ from_mcp_json=from_mcp_json,
514
+ )
515
+
516
+ policy = loader.load()
517
+
518
+ # Zero-config --wrap mode
519
+ if wrap:
520
+ parts = shlex.split(wrap)
521
+ cmd = parts[0]
522
+ args = parts[1:] if len(parts) > 1 else []
523
+ wrap_upstream = UpstreamSpec(
524
+ id="wrapped",
525
+ transport=Transport.STDIO,
526
+ command=cmd,
527
+ args=args,
528
+ )
529
+ policy.upstreams = [wrap_upstream]
530
+
531
+ async def _start() -> None:
532
+ pipeline, _ = await create_gateway_pipeline(settings, policy=policy, policy_loader=loader)
533
+ server = create_gateway_server(settings, pipeline)
534
+
535
+ if transport.lower() == "http":
536
+ typer.echo(f"Starting Veilgate Streamable HTTP Gateway on port {port}...")
537
+ await server.run_async(transport="streamable-http", port=port)
538
+ else:
539
+ await server.run_async(transport="stdio")
540
+
541
+ asyncio.run(_start())
542
+
543
+
544
+ if __name__ == "__main__":
545
+ app()
@@ -0,0 +1,86 @@
1
+ import re
2
+ from enum import StrEnum
3
+
4
+ # Token formatting and parsing specifications
5
+ TOKEN_PREFIX: str = "<vg:"
6
+ TOKEN_SUFFIX: str = ">"
7
+ TOKEN_REGEX_PATTERN: str = r"<vg:([A-Z_]{2,32}):([0-9A-HJKMNP-TV-Z]{6})>"
8
+ TOKEN_REGEX: re.Pattern[str] = re.compile(TOKEN_REGEX_PATTERN)
9
+
10
+ # Crockford Base32 alphabet (excludes I, L, O, U to prevent visual ambiguity)
11
+ CROCKFORD_BASE32_ALPHABET: str = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
12
+
13
+ # Format-preserving email and URL tokens (.invalid RFC 2606 guarantees non-deliverability)
14
+ EMAIL_TOKEN_REGEX_PATTERN: str = r"vg-([0-9a-hjkmnp-tv-z]{6})@vault\.veilgate\.invalid"
15
+ EMAIL_TOKEN_REGEX: re.Pattern[str] = re.compile(EMAIL_TOKEN_REGEX_PATTERN, re.IGNORECASE)
16
+ EMAIL_TOKEN_DOMAIN: str = "vault.veilgate.invalid"
17
+
18
+ URL_TOKEN_PREFIX: str = "https://vault.veilgate.invalid/t/"
19
+ URL_TOKEN_REGEX_PATTERN: str = r"https://vault\.veilgate\.invalid/t/([0-9a-hjkmnp-tv-z]{6})"
20
+ URL_TOKEN_REGEX: re.Pattern[str] = re.compile(URL_TOKEN_REGEX_PATTERN, re.IGNORECASE)
21
+
22
+ # Synthetic integer base for type-preserving integer tokenization
23
+ SYNTHETIC_INT_BASE: int = 900_000_000_000
24
+
25
+ # SQLite table names
26
+ TABLE_SESSIONS: str = "sessions"
27
+ TABLE_TOKENS: str = "tokens"
28
+ TABLE_TOOL_CATALOG: str = "tool_catalog"
29
+ TABLE_SCHEMA_CACHE: str = "schema_cache"
30
+ TABLE_AUDIT_EVENTS: str = "audit_events"
31
+ TABLE_APPROVALS: str = "approvals"
32
+ TABLE_RESULT_STASH: str = "result_stash"
33
+ TABLE_TELEMETRY: str = "telemetry"
34
+
35
+ # Default operational constants
36
+ DEFAULT_TOKEN_TTL_SECONDS: int = 86_400 # 24 hours
37
+ DEFAULT_STASH_TTL_SECONDS: int = 1_800 # 30 minutes
38
+ DEFAULT_UPSTREAM_TIMEOUT_SECONDS: float = 30.0
39
+ DEFAULT_APPROVAL_TIMEOUT_SECONDS: int = 120
40
+ DEFAULT_SEARCH_THRESHOLD: int = 20
41
+ DEFAULT_TOP_K: int = 8
42
+ DEFAULT_MAX_DESCRIPTION_CHARS: int = 300
43
+ DEFAULT_TABLE_THRESHOLD: int = 10
44
+ DEFAULT_MAX_ARRAY_ITEMS: int = 50
45
+ DEFAULT_MAX_STRING_CHARS: int = 2_000
46
+
47
+
48
+ class ReasonCode(StrEnum):
49
+ """Normalized gateway decision and diagnostic reason codes."""
50
+
51
+ OK = "OK"
52
+ OK_SINK_ALLOWED = "OK_SINK_ALLOWED"
53
+ OK_FORWARD_OPAQUE = "OK_FORWARD_OPAQUE"
54
+
55
+ # ACL & Tool Catalog
56
+ TOOL_UNKNOWN = "TOOL_UNKNOWN"
57
+ TOOL_NOT_ALLOWED = "TOOL_NOT_ALLOWED"
58
+ TOOL_QUARANTINED = "TOOL_QUARANTINED"
59
+
60
+ # Human-in-the-Loop Approvals
61
+ APPROVAL_APPROVED = "APPROVAL_APPROVED"
62
+ APPROVAL_APPROVED_AND_SAVED = "APPROVAL_APPROVED_AND_SAVED"
63
+ APPROVAL_DENIED = "APPROVAL_DENIED"
64
+ APPROVAL_TIMEOUT = "APPROVAL_TIMEOUT"
65
+ EGRESS_APPROVAL_REQUIRED = "EGRESS_APPROVAL_REQUIRED"
66
+
67
+ # Token Capability & Taint Tracking
68
+ TOKEN_UNKNOWN = "TOKEN_UNKNOWN"
69
+ TOKEN_EXPIRED = "TOKEN_EXPIRED"
70
+ TOKEN_SCOPE_MISMATCH = "TOKEN_SCOPE_MISMATCH"
71
+ TAINT_EGRESS_FORBIDDEN = "TAINT_EGRESS_FORBIDDEN"
72
+ TAINT_SINK_NOT_ALLOWED = "TAINT_SINK_NOT_ALLOWED"
73
+ TAINT_ARG_GUARD_BLOCKED = "TAINT_ARG_GUARD_BLOCKED"
74
+ TOKEN_TYPE_BLOCKED = "TOKEN_TYPE_BLOCKED"
75
+ TOKEN_ENCODED_SMUGGLING = "TOKEN_ENCODED_SMUGGLING"
76
+
77
+ # Session Integrity
78
+ LOW_INTEGRITY_APPROVAL_REQUIRED = "LOW_INTEGRITY_APPROVAL_REQUIRED"
79
+
80
+ # Upstream Fleet Communication
81
+ UPSTREAM_UNAVAILABLE = "UPSTREAM_UNAVAILABLE"
82
+ UPSTREAM_TIMEOUT = "UPSTREAM_TIMEOUT"
83
+ UPSTREAM_ERROR = "UPSTREAM_ERROR"
84
+
85
+ # Internal Guardrails
86
+ INTERNAL_FAIL_CLOSED = "INTERNAL_FAIL_CLOSED"