veil-proxy 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/PKG-INFO +6 -4
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/README.md +5 -3
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/pyproject.toml +1 -1
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/anthropic.py +52 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/cli.py +12 -1
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/engine.py +14 -1
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/guard.py +2 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/hooks.py +4 -1
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/mcpdb.py +8 -1
- veil_proxy-0.3.2/veil/netenv.py +62 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/proxy.py +2 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/reseed.py +2 -1
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/sql.py +2 -1
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/LICENSE +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/__init__.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/adopt.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/cloud.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/crypto.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/data/README.md +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/data/pg_anonymizer_identifier.csv +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/data/words.txt.gz +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/decode.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/detect.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/display.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/filesafe.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/hookclient.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/keys.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/match.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/ner.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/openai.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/quasi.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/responses.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/rotate.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/secretref.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/shell.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/__init__.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/core.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/dynamo.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/elastic.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/files.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/git.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/kv.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/mongo.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/mssql.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/schema.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/warehouse.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/state.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/stream.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/team.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/tokens.py +0 -0
- {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/vault.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: veil-proxy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Privacy middleware for AI coding agents: the model sees consistent pseudonyms, your machine keeps the real values
|
|
5
5
|
Keywords: privacy,pii,llm,proxy,pseudonymization,claude-code,coding-agent
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -47,10 +47,10 @@ Privacy middleware for AI coding agents. The model sees consistent pseudonyms
|
|
|
47
47
|
and commands the agent runs locally get the real values back (shell- and SQL-safe).
|
|
48
48
|
|
|
49
49
|
```sh
|
|
50
|
-
pip install "veil-proxy[postgres]" # extras: postgres, mysql, mongo, redis, bedrock, ner, mssql, all
|
|
50
|
+
pip install "veil-proxy[postgres]" # extras: postgres, mysql, mongo, redis, bedrock, ner, mssql, snowflake, all
|
|
51
51
|
# (installs the `veil` and `veil-hook` commands; import name `veil`)
|
|
52
52
|
veil init ~/code/myapp # seed from .env DB URLs, SQLite, fixtures; wire Claude Code
|
|
53
|
-
veil service install # proxy as a LaunchAgent (macOS) / systemd user unit (Linux)
|
|
53
|
+
veil service install # proxy as a LaunchAgent (macOS) / systemd user unit (Linux) / Startup item (Windows)
|
|
54
54
|
veil status ~/code/myapp
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -63,7 +63,7 @@ https://github.com/lemonadesaltbagel/veil):
|
|
|
63
63
|
pattern matching (1,000 labelled records).
|
|
64
64
|
- **21/21** one-command bypass transforms are contained. Restored values are escaped for every
|
|
65
65
|
language layer they pass through (shell, heredocs, `python -c`, `node -e`, SQL, `bash -c`...):
|
|
66
|
-
35/35 injection tests, and
|
|
66
|
+
35/35 injection tests, and 68,000 fuzzed nested commands executed with 0 code executions.
|
|
67
67
|
- Prompt caching keeps working (74–89% cache hits); about 19 ms overhead per request at 40k seeded
|
|
68
68
|
values, and 37 + 68 ms per 234 KB request at 1.2 million seeded values (1.1 GB).
|
|
69
69
|
|
|
@@ -71,6 +71,8 @@ One local proxy (default `127.0.0.1:8787`) serves every client, routed by path:
|
|
|
71
71
|
Anthropic Messages (Claude Code, Agent SDK), Amazon Bedrock InvokeModel (binary eventstream),
|
|
72
72
|
Google Vertex rawPredict, Microsoft Foundry (`/foundry`), Claude Platform on AWS (`/anthropic-aws`),
|
|
73
73
|
OpenAI Chat Completions (Aider, Continue, OpenAI SDK) and OpenAI Responses (Codex CLI).
|
|
74
|
+
DeepSeek Harness uses the [dsh-veil](https://github.com/lemonadesaltbagel/dsh-veil) plugin. Other
|
|
75
|
+
agents' tools can get veil's restore rules with `veil proxy --tool-alias NAME=ROLE`.
|
|
74
76
|
|
|
75
77
|
Seeds from Postgres, MySQL/MariaDB, SQLite, SQL Server (native sensitivity labels), MongoDB, Redis,
|
|
76
78
|
Elasticsearch/OpenSearch and fixture files, plus personal fields declared in code (Prisma, Django,
|
|
@@ -5,10 +5,10 @@ Privacy middleware for AI coding agents. The model sees consistent pseudonyms
|
|
|
5
5
|
and commands the agent runs locally get the real values back (shell- and SQL-safe).
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pip install "veil-proxy[postgres]" # extras: postgres, mysql, mongo, redis, bedrock, ner, mssql, all
|
|
8
|
+
pip install "veil-proxy[postgres]" # extras: postgres, mysql, mongo, redis, bedrock, ner, mssql, snowflake, all
|
|
9
9
|
# (installs the `veil` and `veil-hook` commands; import name `veil`)
|
|
10
10
|
veil init ~/code/myapp # seed from .env DB URLs, SQLite, fixtures; wire Claude Code
|
|
11
|
-
veil service install # proxy as a LaunchAgent (macOS) / systemd user unit (Linux)
|
|
11
|
+
veil service install # proxy as a LaunchAgent (macOS) / systemd user unit (Linux) / Startup item (Windows)
|
|
12
12
|
veil status ~/code/myapp
|
|
13
13
|
```
|
|
14
14
|
|
|
@@ -21,7 +21,7 @@ https://github.com/lemonadesaltbagel/veil):
|
|
|
21
21
|
pattern matching (1,000 labelled records).
|
|
22
22
|
- **21/21** one-command bypass transforms are contained. Restored values are escaped for every
|
|
23
23
|
language layer they pass through (shell, heredocs, `python -c`, `node -e`, SQL, `bash -c`...):
|
|
24
|
-
35/35 injection tests, and
|
|
24
|
+
35/35 injection tests, and 68,000 fuzzed nested commands executed with 0 code executions.
|
|
25
25
|
- Prompt caching keeps working (74–89% cache hits); about 19 ms overhead per request at 40k seeded
|
|
26
26
|
values, and 37 + 68 ms per 234 KB request at 1.2 million seeded values (1.1 GB).
|
|
27
27
|
|
|
@@ -29,6 +29,8 @@ One local proxy (default `127.0.0.1:8787`) serves every client, routed by path:
|
|
|
29
29
|
Anthropic Messages (Claude Code, Agent SDK), Amazon Bedrock InvokeModel (binary eventstream),
|
|
30
30
|
Google Vertex rawPredict, Microsoft Foundry (`/foundry`), Claude Platform on AWS (`/anthropic-aws`),
|
|
31
31
|
OpenAI Chat Completions (Aider, Continue, OpenAI SDK) and OpenAI Responses (Codex CLI).
|
|
32
|
+
DeepSeek Harness uses the [dsh-veil](https://github.com/lemonadesaltbagel/dsh-veil) plugin. Other
|
|
33
|
+
agents' tools can get veil's restore rules with `veil proxy --tool-alias NAME=ROLE`.
|
|
32
34
|
|
|
33
35
|
Seeds from Postgres, MySQL/MariaDB, SQLite, SQL Server (native sensitivity labels), MongoDB, Redis,
|
|
34
36
|
Elasticsearch/OpenSearch and fixture files, plus personal fields declared in code (Prisma, Django,
|
|
@@ -34,6 +34,34 @@ EGRESS = re.compile(
|
|
|
34
34
|
|
|
35
35
|
CONTENT_FIELDS = {"new_string", "content", "new_source"}
|
|
36
36
|
|
|
37
|
+
# Roles a tool can be aliased to: another agent's tool that takes the same arguments as one
|
|
38
|
+
# of these gets the same policy (`tool_aliases`, set by config, env or `--tool-alias`).
|
|
39
|
+
ALIAS_ROLES = {"Bash", "Read", "Grep", "Glob", "LS", "Edit", "MultiEdit", "Write", "NotebookEdit"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_aliases(spec) -> dict[str, str]:
|
|
43
|
+
"""{"bash": "Bash"} or "bash=Bash,read=Read" -> validated aliases. Unknown roles are refused:
|
|
44
|
+
an alias may only grant a tool the policy of one of veil's own roles."""
|
|
45
|
+
if isinstance(spec, str):
|
|
46
|
+
spec = dict(p.split("=", 1) for p in spec.replace(";", ",").split(",") if "=" in p)
|
|
47
|
+
out = {}
|
|
48
|
+
for name, role in (spec or {}).items():
|
|
49
|
+
name, role = str(name).strip(), str(role).strip()
|
|
50
|
+
if role not in ALIAS_ROLES:
|
|
51
|
+
raise ValueError(f"tool alias {name}={role}: role must be one of {sorted(ALIAS_ROLES)}")
|
|
52
|
+
if name:
|
|
53
|
+
out[name] = role
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def configured_aliases(cfg: dict | None = None, extra=None) -> dict[str, str]:
|
|
58
|
+
"""config.json "tool_aliases", then VEIL_TOOL_ALIASES, then explicit `extra` (later wins)."""
|
|
59
|
+
import os
|
|
60
|
+
aliases = parse_aliases((cfg or {}).get("tool_aliases"))
|
|
61
|
+
aliases.update(parse_aliases(os.environ.get("VEIL_TOOL_ALIASES", "")))
|
|
62
|
+
aliases.update(parse_aliases(extra))
|
|
63
|
+
return aliases
|
|
64
|
+
|
|
37
65
|
|
|
38
66
|
PSEUDONYM_NOTICE = (
|
|
39
67
|
"Privacy note: personal data in this session (names, emails, usernames, phones, addresses, "
|
|
@@ -53,6 +81,7 @@ class Policy:
|
|
|
53
81
|
full_tools: set[str] = field(default_factory=lambda: {"Bash", "Read", "Grep", "Glob", "LS"})
|
|
54
82
|
preserve_tools: set[str] = field(default_factory=lambda: {"Edit", "MultiEdit", "Write", "NotebookEdit"})
|
|
55
83
|
full_tool_prefixes: tuple[str, ...] = () # e.g. ("mcp__veil_db__",)
|
|
84
|
+
tool_aliases: dict[str, str] = field(default_factory=dict) # other agents' names -> veil roles
|
|
56
85
|
|
|
57
86
|
def mode(self, tool: str, tool_input: dict) -> str:
|
|
58
87
|
if tool == "Bash" and EGRESS.search(str(tool_input.get("command", ""))):
|
|
@@ -101,7 +130,30 @@ def _texts(obj) -> str:
|
|
|
101
130
|
return "\0".join(out)
|
|
102
131
|
|
|
103
132
|
|
|
133
|
+
def _str_replace_editor(veil: Veil, policy: Policy, tool_input: dict) -> dict:
|
|
134
|
+
"""Anthropic's text-editor tool (`str_replace_editor`): view | create | str_replace |
|
|
135
|
+
insert, mapped onto Read / Write / Edit so the preserve and escaping rules apply."""
|
|
136
|
+
cmd = tool_input.get("command")
|
|
137
|
+
if cmd == "create":
|
|
138
|
+
out = rehydrate_tool_input(veil, policy, "Write", {"file_path": tool_input.get("path", ""),
|
|
139
|
+
"content": tool_input.get("file_text", "")})
|
|
140
|
+
return {**tool_input, "path": out["file_path"], "file_text": out["content"]}
|
|
141
|
+
if cmd in ("str_replace", "insert"):
|
|
142
|
+
out = rehydrate_tool_input(veil, policy, "Edit", {"file_path": tool_input.get("path", ""),
|
|
143
|
+
"old_string": tool_input.get("old_str") or "",
|
|
144
|
+
"new_string": tool_input.get("new_str") or ""})
|
|
145
|
+
res = {**tool_input, "path": out["file_path"], "new_str": out["new_string"]}
|
|
146
|
+
if tool_input.get("old_str") is not None:
|
|
147
|
+
res["old_str"] = out["old_string"]
|
|
148
|
+
return res
|
|
149
|
+
return {**tool_input, "path": veil.rehydrate(tool_input.get("path") or "")} # view, undo_edit
|
|
150
|
+
|
|
151
|
+
|
|
104
152
|
def rehydrate_tool_input(veil: Veil, policy: Policy, tool: str, tool_input: dict) -> dict:
|
|
153
|
+
if tool in policy.tool_aliases:
|
|
154
|
+
return rehydrate_tool_input(veil, policy, policy.tool_aliases[tool], tool_input)
|
|
155
|
+
if tool == "str_replace_editor" and isinstance(tool_input, dict):
|
|
156
|
+
return _str_replace_editor(veil, policy, tool_input)
|
|
105
157
|
mode = policy.mode(tool, tool_input)
|
|
106
158
|
if mode == "full":
|
|
107
159
|
frags = veil.fragments(_texts(tool_input))
|
|
@@ -256,6 +256,8 @@ def _service(args) -> int:
|
|
|
256
256
|
|
|
257
257
|
def main(argv: list[str] | None = None) -> int:
|
|
258
258
|
argv = sys.argv[1:] if argv is None else argv
|
|
259
|
+
from .netenv import normalize_no_proxy
|
|
260
|
+
normalize_no_proxy() # before any httpx client: one unparseable NO_PROXY entry makes them all raise
|
|
259
261
|
if argv[:1] == ["audit"]:
|
|
260
262
|
try: # dev harness: only in a source checkout (prototype/veil_dev), never in the package
|
|
261
263
|
import os
|
|
@@ -304,6 +306,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
304
306
|
help="OpenAI-format upstream for /chat/completions (e.g. https://api.deepseek.com)")
|
|
305
307
|
s.add_argument("--bedrock-upstream", metavar="URL", help="e.g. https://bedrock-runtime.us-east-1.amazonaws.com")
|
|
306
308
|
s.add_argument("--pid-file", metavar="PATH", help="write the process id here (removed on exit)")
|
|
309
|
+
s.add_argument("--tool-alias", action="append", default=[], metavar="NAME=ROLE",
|
|
310
|
+
help="give another agent's tool the policy of a veil role (e.g. bash=Bash; repeatable)")
|
|
307
311
|
s.add_argument("--reseed-interval", type=float, default=30,
|
|
308
312
|
help="seconds between change checks of remembered sources (0 = off)")
|
|
309
313
|
s.add_argument("--vertex-upstream", metavar="URL", help="e.g. https://us-east5-aiplatform.googleapis.com")
|
|
@@ -428,7 +432,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
428
432
|
from .ner import GlinerDetector
|
|
429
433
|
veil.ner = GlinerDetector()
|
|
430
434
|
warm(veil.known)
|
|
431
|
-
|
|
435
|
+
from .anthropic import configured_aliases
|
|
436
|
+
try:
|
|
437
|
+
aliases = configured_aliases(state.config(), ",".join(args.tool_alias))
|
|
438
|
+
except ValueError as e:
|
|
439
|
+
raise SystemExit(f"veil: {e}")
|
|
440
|
+
if aliases:
|
|
441
|
+
print(f"[veil] tool aliases: {', '.join(f'{k}={v}' for k, v in sorted(aliases.items()))}", file=sys.stderr)
|
|
442
|
+
server = serve(veil, Policy(display_real=not args.show_tokens, tool_aliases=aliases), args.port, args.upstream, args.capture,
|
|
432
443
|
args.openai_upstream, args.bedrock_upstream, args.vertex_upstream,
|
|
433
444
|
args.foundry_upstream, args.anthropic_aws_upstream)
|
|
434
445
|
sources = state.load_sources(veil.vault.cipher)
|
|
@@ -14,6 +14,7 @@ from .vault import Vault
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
# The person code inside each token shape: user_mz5z, user_mz5z+2@…, User Mz5z, First_Mz5z, Mz5z Example Street
|
|
17
|
+
PHONE_TOKEN = re.compile(r"\+1-\d{3}-555-01\d\d") # tokens.render(kind="phone")
|
|
17
18
|
TOKEN_CODE = re.compile(r"(?:user_|User |First_|Last_|card_|secret_)([A-Za-z2-9]{4,6})|^([A-Za-z2-9]{4,6}) Example Street")
|
|
18
19
|
|
|
19
20
|
|
|
@@ -71,6 +72,7 @@ class Veil:
|
|
|
71
72
|
self.ner = ner # optional free-text detector (veil.ner.GlinerDetector)
|
|
72
73
|
self.quasi_mode = "warn" # "redact": rare quasi-identifiers are pseudonymised too (veil/quasi.py)
|
|
73
74
|
self._tokens = TokenPattern()
|
|
75
|
+
self._phone_pieces: tuple[int, set[str]] = (-1, set())
|
|
74
76
|
self._tokens_lock = threading.Lock()
|
|
75
77
|
self._codes: dict[str, list[str]] = {}
|
|
76
78
|
self._codes_version = -1
|
|
@@ -92,7 +94,7 @@ class Veil:
|
|
|
92
94
|
# job title of one person); no person group: shared values must not link people
|
|
93
95
|
extra = (extra or []) + [dataclasses.replace(s, group=None) for s in self.known.find_quasi(text)] \
|
|
94
96
|
+ [s for combo, _ in self.known.quasi_combos(text) for s in combo]
|
|
95
|
-
spans = detect(text, self.known, self.vault.is_token, extra)
|
|
97
|
+
spans = [s for s in detect(text, self.known, self.vault.is_token, extra) if not self.pseudonym_piece(s)]
|
|
96
98
|
if not spans:
|
|
97
99
|
return text
|
|
98
100
|
out, pos = [], 0
|
|
@@ -103,6 +105,17 @@ class Veil:
|
|
|
103
105
|
out.append(text[pos:])
|
|
104
106
|
return "".join(out)
|
|
105
107
|
|
|
108
|
+
def pseudonym_piece(self, span) -> bool:
|
|
109
|
+
"""A phone-pattern hit that is (part of) one of our own phone pseudonyms: a model
|
|
110
|
+
searching LIKE '%408-555-0133%' for +1-408-555-0133. Re-redacting it would mint a
|
|
111
|
+
second pseudonym for the first one's digits. Seeded values are never skipped."""
|
|
112
|
+
if span.kind != "phone" or span.priority >= 10:
|
|
113
|
+
return False
|
|
114
|
+
if self._phone_pieces[0] != self.vault.version:
|
|
115
|
+
self._phone_pieces = (self.vault.version, {re.sub(r"\D", "", t)[-10:] for t in self.vault._by_token
|
|
116
|
+
if PHONE_TOKEN.fullmatch(t)})
|
|
117
|
+
return re.sub(r"\D", "", span.value)[-10:] in self._phone_pieces[1]
|
|
118
|
+
|
|
106
119
|
# -- token -> real ---------------------------------------------------
|
|
107
120
|
def token_pattern(self) -> TokenPattern | None:
|
|
108
121
|
if hasattr(self.vault, "refresh"):
|
|
@@ -270,6 +270,8 @@ class Guard:
|
|
|
270
270
|
hits.append((a, b, PARTIAL, ix.kind_of(value), "partial"))
|
|
271
271
|
if not frozen:
|
|
272
272
|
for span in detect(s, None, self.veil.vault.is_token):
|
|
273
|
+
if self.veil.pseudonym_piece(span):
|
|
274
|
+
continue
|
|
273
275
|
if not any(a < span.end and span.start < b for a, b, *_ in hits):
|
|
274
276
|
tok = self.veil.vault.token_for(span.kind, span.value)
|
|
275
277
|
hits.append((span.start, span.end, tok, span.kind, "pattern"))
|
|
@@ -16,7 +16,10 @@ def hook_response(veil, kind: str, event: dict) -> dict | None:
|
|
|
16
16
|
return {"hookSpecificOutput": {"hookEventName": "PostToolUse", "updatedToolOutput": redacted}}
|
|
17
17
|
else:
|
|
18
18
|
original = event.get("tool_input", {})
|
|
19
|
-
|
|
19
|
+
from .anthropic import configured_aliases
|
|
20
|
+
from .state import config
|
|
21
|
+
policy = Policy(tool_aliases=configured_aliases(config()))
|
|
22
|
+
updated = rehydrate_tool_input(veil, policy, event.get("tool_name", ""), original)
|
|
20
23
|
if updated != original:
|
|
21
24
|
return {"hookSpecificOutput": {"hookEventName": "PreToolUse", "updatedInput": updated}}
|
|
22
25
|
return None
|
|
@@ -154,9 +154,16 @@ class SQLSource:
|
|
|
154
154
|
self.conn = pymysql.connect(host=u.hostname or "127.0.0.1", port=u.port or 3306,
|
|
155
155
|
user=unquote(u.username or "root"), password=unquote(u.password or ""),
|
|
156
156
|
database=u.path.lstrip("/"), autocommit=True, charset="utf8mb4",
|
|
157
|
-
connect_timeout=10)
|
|
157
|
+
connect_timeout=10, read_timeout=60, write_timeout=60)
|
|
158
158
|
with self.conn.cursor() as c:
|
|
159
159
|
c.execute("SET SESSION TRANSACTION READ ONLY")
|
|
160
|
+
for limit in ("SET SESSION MAX_EXECUTION_TIME = 30000", # MySQL (ms)
|
|
161
|
+
"SET SESSION max_statement_time = 30"): # MariaDB (s)
|
|
162
|
+
try:
|
|
163
|
+
c.execute(limit)
|
|
164
|
+
break
|
|
165
|
+
except Exception:
|
|
166
|
+
continue
|
|
160
167
|
self.kind, self.paramstyle = "mysql", "format"
|
|
161
168
|
elif u.scheme in ("mssql", "sqlserver"):
|
|
162
169
|
from .sources.mssql import _connect
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""NO_PROXY entries that httpx can't parse.
|
|
2
|
+
|
|
3
|
+
httpx turns each NO_PROXY entry into a URL pattern when a client is created, and one entry it
|
|
4
|
+
can't parse makes every `httpx.Client()` raise, so veil would exit at startup. DeepSeek
|
|
5
|
+
Harness, for one, adds `[::1]` (an IPv6 address in brackets, which httpx doesn't recognise as an
|
|
6
|
+
address) for its child processes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ipaddress
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from typing import MutableMapping
|
|
16
|
+
|
|
17
|
+
KEYS = ("NO_PROXY", "no_proxy")
|
|
18
|
+
_BRACKETED = re.compile(r"\[([0-9A-Fa-f:.%]+)\](?::\d+)?")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _pattern(host: str) -> str:
|
|
22
|
+
"""The URL pattern httpx builds for one NO_PROXY entry (httpx._utils.get_environment_proxies)."""
|
|
23
|
+
if "://" in host:
|
|
24
|
+
return host
|
|
25
|
+
for cls, fmt in ((ipaddress.IPv4Address, "all://{}"), (ipaddress.IPv6Address, "all://[{}]")):
|
|
26
|
+
try:
|
|
27
|
+
cls(host.split("/")[0])
|
|
28
|
+
return fmt.format(host)
|
|
29
|
+
except ValueError:
|
|
30
|
+
pass
|
|
31
|
+
return f"all://{host}" if host.lower() == "localhost" else f"all://*{host}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parses(host: str) -> bool:
|
|
35
|
+
import httpx
|
|
36
|
+
try:
|
|
37
|
+
httpx.URL(_pattern(host))
|
|
38
|
+
return True
|
|
39
|
+
except Exception:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_no_proxy(environ: MutableMapping[str, str] = os.environ, log=sys.stderr) -> None:
|
|
44
|
+
"""Rewrite `[::1]` and `[::1]:port` to `::1`, and drop entries httpx would still reject."""
|
|
45
|
+
for key in KEYS:
|
|
46
|
+
value = environ.get(key)
|
|
47
|
+
if not value:
|
|
48
|
+
continue
|
|
49
|
+
kept, dropped = [], []
|
|
50
|
+
for host in (h.strip() for h in value.split(",")):
|
|
51
|
+
if not host:
|
|
52
|
+
continue
|
|
53
|
+
m = _BRACKETED.fullmatch(host)
|
|
54
|
+
host = m.group(1) if m else host
|
|
55
|
+
if host == "*" or _parses(host):
|
|
56
|
+
if host not in kept:
|
|
57
|
+
kept.append(host)
|
|
58
|
+
else:
|
|
59
|
+
dropped.append(host)
|
|
60
|
+
if dropped and log:
|
|
61
|
+
print(f"[veil] ignoring {key} entries that can't be parsed: {', '.join(dropped)}", file=log)
|
|
62
|
+
environ[key] = ",".join(kept)
|
|
@@ -414,6 +414,8 @@ def serve(veil: Veil, policy: Policy, port: int = 8787, upstream: str = "https:/
|
|
|
414
414
|
capture: str | None = None, openai_upstream: str | None = None, bedrock_upstream: str | None = None,
|
|
415
415
|
vertex_upstream: str | None = None, foundry_upstream: str | None = None,
|
|
416
416
|
anthropic_aws_upstream: str | None = None):
|
|
417
|
+
from .netenv import normalize_no_proxy
|
|
418
|
+
normalize_no_proxy()
|
|
417
419
|
client = httpx.Client(timeout=httpx.Timeout(600, connect=10))
|
|
418
420
|
observer = capture_observer(capture) if capture else None
|
|
419
421
|
server = LocalHTTPServer(("127.0.0.1", port), make_handler(veil, policy, upstream, client,
|
|
@@ -63,7 +63,8 @@ def fingerprint(source: str) -> str:
|
|
|
63
63
|
if u.scheme in ("mysql", "mariadb"):
|
|
64
64
|
import pymysql
|
|
65
65
|
c = pymysql.connect(host=u.hostname or "127.0.0.1", port=u.port or 3306, user=unquote(u.username or "root"),
|
|
66
|
-
password=unquote(u.password or ""), database=u.path.lstrip("/"), connect_timeout=5
|
|
66
|
+
password=unquote(u.password or ""), database=u.path.lstrip("/"), connect_timeout=5,
|
|
67
|
+
read_timeout=5, write_timeout=5) # connect_timeout doesn't cover the handshake
|
|
67
68
|
try:
|
|
68
69
|
with c.cursor() as cur:
|
|
69
70
|
try:
|
|
@@ -207,7 +207,8 @@ def seed_mysql(known: KnownValues, host: str, user: str, password: str, database
|
|
|
207
207
|
import pymysql
|
|
208
208
|
|
|
209
209
|
conn = pymysql.connect(host=host, port=port, user=user, password=password, database=database,
|
|
210
|
-
cursorclass=pymysql.cursors.DictCursor, charset="utf8mb4", connect_timeout=10
|
|
210
|
+
cursorclass=pymysql.cursors.DictCursor, charset="utf8mb4", connect_timeout=10,
|
|
211
|
+
read_timeout=300, write_timeout=300)
|
|
211
212
|
with conn.cursor() as cur:
|
|
212
213
|
cur.execute("SET SESSION TRANSACTION READ ONLY")
|
|
213
214
|
cur.execute("""SELECT TABLE_NAME t, COLUMN_NAME c, DATA_TYPE d, COLUMN_KEY k, COLUMN_COMMENT m
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|