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.
Files changed (51) hide show
  1. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/PKG-INFO +6 -4
  2. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/README.md +5 -3
  3. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/pyproject.toml +1 -1
  4. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/anthropic.py +52 -0
  5. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/cli.py +12 -1
  6. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/engine.py +14 -1
  7. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/guard.py +2 -0
  8. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/hooks.py +4 -1
  9. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/mcpdb.py +8 -1
  10. veil_proxy-0.3.2/veil/netenv.py +62 -0
  11. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/proxy.py +2 -0
  12. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/reseed.py +2 -1
  13. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/sql.py +2 -1
  14. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/LICENSE +0 -0
  15. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/__init__.py +0 -0
  16. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/adopt.py +0 -0
  17. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/cloud.py +0 -0
  18. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/crypto.py +0 -0
  19. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/data/README.md +0 -0
  20. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/data/pg_anonymizer_identifier.csv +0 -0
  21. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/data/words.txt.gz +0 -0
  22. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/decode.py +0 -0
  23. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/detect.py +0 -0
  24. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/display.py +0 -0
  25. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/filesafe.py +0 -0
  26. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/hookclient.py +0 -0
  27. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/keys.py +0 -0
  28. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/match.py +0 -0
  29. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/ner.py +0 -0
  30. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/openai.py +0 -0
  31. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/quasi.py +0 -0
  32. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/responses.py +0 -0
  33. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/rotate.py +0 -0
  34. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/secretref.py +0 -0
  35. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/shell.py +0 -0
  36. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/__init__.py +0 -0
  37. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/core.py +0 -0
  38. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/dynamo.py +0 -0
  39. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/elastic.py +0 -0
  40. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/files.py +0 -0
  41. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/git.py +0 -0
  42. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/kv.py +0 -0
  43. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/mongo.py +0 -0
  44. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/mssql.py +0 -0
  45. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/schema.py +0 -0
  46. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/sources/warehouse.py +0 -0
  47. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/state.py +0 -0
  48. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/stream.py +0 -0
  49. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/team.py +0 -0
  50. {veil_proxy-0.3.0 → veil_proxy-0.3.2}/veil/tokens.py +0 -0
  51. {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.0
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 55,000 fuzzed nested commands executed with 0 code executions.
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 55,000 fuzzed nested commands executed with 0 code executions.
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,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "veil-proxy"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Privacy middleware for AI coding agents: the model sees consistent pseudonyms, your machine keeps the real values"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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
- server = serve(veil, Policy(display_real=not args.show_tokens), args.port, args.upstream, args.capture,
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
- updated = rehydrate_tool_input(veil, Policy(), event.get("tool_name", ""), original)
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