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.
- veilgate/__init__.py +3 -0
- veilgate/cli.py +545 -0
- veilgate/config/constants.py +86 -0
- veilgate/config/policy_loader.py +332 -0
- veilgate/config/policy_models.py +432 -0
- veilgate/config/settings.py +90 -0
- veilgate/dashboard/api.py +216 -0
- veilgate/dashboard/app.py +46 -0
- veilgate/dashboard/models.py +65 -0
- veilgate/dashboard/static/styles.css +314 -0
- veilgate/dashboard/templates/index.html +208 -0
- veilgate/engines/operators/__init__.py +61 -0
- veilgate/engines/operators/base.py +14 -0
- veilgate/engines/operators/checksums.py +91 -0
- veilgate/engines/operators/text.py +76 -0
- veilgate/exceptions/__init__.py +41 -0
- veilgate/exceptions/types.py +267 -0
- veilgate/gateway.py +130 -0
- veilgate/integrations/__init__.py +1 -0
- veilgate/middleware/__init__.py +15 -0
- veilgate/middleware/context_middleware.py +95 -0
- veilgate/middleware/context_mw.py +26 -0
- veilgate/middleware/discovery_mw.py +35 -0
- veilgate/middleware/firewall_call_mw.py +65 -0
- veilgate/middleware/init_mw.py +11 -0
- veilgate/packs/arg-guards.yaml +31 -0
- veilgate/packs/bloat-http.yaml +33 -0
- veilgate/packs/core-verbs.yaml +35 -0
- veilgate/packs/pii-global.yaml +88 -0
- veilgate/packs/pii-india.yaml +45 -0
- veilgate/packs/sensitive-keys.yaml +64 -0
- veilgate/packs/server-profiles.yaml +24 -0
- veilgate/pipeline.py +323 -0
- veilgate/repositories/__init__.py +35 -0
- veilgate/repositories/approval_repo.py +82 -0
- veilgate/repositories/audit_repo.py +194 -0
- veilgate/repositories/catalog_repo.py +174 -0
- veilgate/repositories/db.py +55 -0
- veilgate/repositories/migrations/0001_init.sql +118 -0
- veilgate/repositories/models.py +123 -0
- veilgate/repositories/session_repo.py +82 -0
- veilgate/repositories/stash_repo.py +87 -0
- veilgate/repositories/vault_repo.py +158 -0
- veilgate/security/crypto.py +173 -0
- veilgate/services/acl_service.py +152 -0
- veilgate/services/approval_service.py +236 -0
- veilgate/services/audit_service.py +50 -0
- veilgate/services/catalog_service.py +193 -0
- veilgate/services/compaction_service.py +111 -0
- veilgate/services/diet_service.py +111 -0
- veilgate/services/explain_service.py +150 -0
- veilgate/services/injection_service.py +73 -0
- veilgate/services/minimizer_service.py +611 -0
- veilgate/services/models.py +178 -0
- veilgate/services/taint_service.py +421 -0
- veilgate/services/tokenizer_service.py +80 -0
- veilgate/services/upstream_registry.py +209 -0
- veilgate/utils/ids.py +30 -0
- veilgate/utils/json_paths.py +115 -0
- veilgate/utils/logging_utils.py +50 -0
- veilgate/utils/text_scan.py +104 -0
- veilgate/utils/token_count.py +26 -0
- veilgate-0.1.1.dist-info/METADATA +330 -0
- veilgate-0.1.1.dist-info/RECORD +67 -0
- veilgate-0.1.1.dist-info/WHEEL +4 -0
- veilgate-0.1.1.dist-info/entry_points.txt +2 -0
- veilgate-0.1.1.dist-info/licenses/LICENSE +21 -0
veilgate/__init__.py
ADDED
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"
|