venice-cli 0.26.0__tar.gz → 0.28.0__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 (85) hide show
  1. {venice_cli-0.26.0 → venice_cli-0.28.0}/PKG-INFO +32 -4
  2. {venice_cli-0.26.0 → venice_cli-0.28.0}/README.md +31 -3
  3. venice_cli-0.28.0/src/venice/__init__.py +1 -0
  4. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_agent.py +44 -3
  5. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_code.py +19 -0
  6. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_index.py +59 -6
  7. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/code.py +7 -0
  8. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/index.py +15 -0
  9. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/search.py +20 -2
  10. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/userconfig.py +3 -0
  11. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_chat.py +30 -0
  12. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_code.py +34 -0
  13. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_code_command.py +55 -1
  14. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_index.py +109 -2
  15. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_search.py +100 -1
  16. venice_cli-0.26.0/src/venice/__init__.py +0 -1
  17. {venice_cli-0.26.0 → venice_cli-0.28.0}/.gitignore +0 -0
  18. {venice_cli-0.26.0 → venice_cli-0.28.0}/CONTRIBUTING.md +0 -0
  19. {venice_cli-0.26.0 → venice_cli-0.28.0}/LICENSE +0 -0
  20. {venice_cli-0.26.0 → venice_cli-0.28.0}/Makefile +0 -0
  21. {venice_cli-0.26.0 → venice_cli-0.28.0}/SECURITY.md +0 -0
  22. {venice_cli-0.26.0 → venice_cli-0.28.0}/bin/venice +0 -0
  23. {venice_cli-0.26.0 → venice_cli-0.28.0}/install.sh +0 -0
  24. {venice_cli-0.26.0 → venice_cli-0.28.0}/pyproject.toml +0 -0
  25. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/__main__.py +0 -0
  26. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/audio_player.py +0 -0
  27. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/audio_post.py +0 -0
  28. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/auth.py +0 -0
  29. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/billing.py +0 -0
  30. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/cli.py +0 -0
  31. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/client.py +0 -0
  32. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/__init__.py +0 -0
  33. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_audio.py +0 -0
  34. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_mcp.py +0 -0
  35. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_mcp_client.py +0 -0
  36. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_models.py +0 -0
  37. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_openai.py +0 -0
  38. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_queue.py +0 -0
  39. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_repl.py +0 -0
  40. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/_shared.py +0 -0
  41. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/balance.py +0 -0
  42. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/bg_remove.py +0 -0
  43. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/chat.py +0 -0
  44. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/config.py +0 -0
  45. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/contact_sheet.py +0 -0
  46. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/embed.py +0 -0
  47. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/image.py +0 -0
  48. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/image_edit.py +0 -0
  49. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/login.py +0 -0
  50. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/master.py +0 -0
  51. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/mcp_serve.py +0 -0
  52. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/models.py +0 -0
  53. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/music.py +0 -0
  54. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/sfx.py +0 -0
  55. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/tts.py +0 -0
  56. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/upscale.py +0 -0
  57. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/commands/video.py +0 -0
  58. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/config.py +0 -0
  59. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/image_montage.py +0 -0
  60. {venice_cli-0.26.0 → venice_cli-0.28.0}/src/venice/mcp_server.py +0 -0
  61. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/__init__.py +0 -0
  62. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/_mcp_fake_server.py +0 -0
  63. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_auth.py +0 -0
  64. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_balance.py +0 -0
  65. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_bg_remove.py +0 -0
  66. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_client.py +0 -0
  67. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_config.py +0 -0
  68. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_contact_sheet.py +0 -0
  69. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_embed.py +0 -0
  70. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_image.py +0 -0
  71. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_image_edit.py +0 -0
  72. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_master.py +0 -0
  73. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_mcp_client.py +0 -0
  74. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_mcp_serve.py +0 -0
  75. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_mcp_tools.py +0 -0
  76. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_models.py +0 -0
  77. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_music.py +0 -0
  78. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_repl.py +0 -0
  79. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_sfx.py +0 -0
  80. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_shared.py +0 -0
  81. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_shared_openai.py +0 -0
  82. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_tts.py +0 -0
  83. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_upscale.py +0 -0
  84. {venice_cli-0.26.0 → venice_cli-0.28.0}/tests/test_video.py +0 -0
  85. {venice_cli-0.26.0 → venice_cli-0.28.0}/uninstall.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: venice-cli
3
- Version: 0.26.0
3
+ Version: 0.28.0
4
4
  Summary: A stdlib-only Python CLI for the Venice.ai API: audio, video, images, chat, and embeddings.
5
5
  Project-URL: Homepage, https://github.com/gobha-me/venice-cli
6
6
  Project-URL: Repository, https://github.com/gobha-me/venice-cli
@@ -803,6 +803,26 @@ venice index . --embed-base-url http://localhost:1234/v1 --embed-model bge-m3
803
803
  venice search "parse the queue response" # uses the index's own backend/model
804
804
  ```
805
805
 
806
+ **Self-signed backends.** Both commands accept the same TLS escape hatches as
807
+ `venice embed` — **applied only to a local backend, never the Venice endpoint**:
808
+
809
+ ```sh
810
+ venice index . --embed-base-url https://embed.local/v1 --embed-model bge-m3 \
811
+ --embed-ca-bundle /etc/ssl/my-ca.pem # trust a private CA (or $VENICE_EMBED_CA_BUNDLE)
812
+ venice search "parse the queue response" --embed-ca-bundle /etc/ssl/my-ca.pem
813
+ venice index . --embed-base-url https://embed.local/v1 --embed-model bge-m3 \
814
+ --embed-insecure # disable verification (warns; CLI-only)
815
+ ```
816
+
817
+ `--embed-ca-bundle` reads `$VENICE_EMBED_CA_BUNDLE` and is config-backable
818
+ (`defaults.index.embed_ca_bundle` / `defaults.search.embed_ca_bundle`);
819
+ `--embed-insecure` is CLI-only, mutually exclusive with it, and errors (exit 2) if
820
+ the flags don't apply (no `--embed-base-url` for `index`, or a Venice-built index
821
+ for `search`). For `search` the CA bundle is supplied fresh at query time — it is
822
+ never baked into the index — and the `project_search` agent tool also honours
823
+ `$VENICE_EMBED_CA_BUNDLE`, so a `venice chat`/`venice code` session can search an
824
+ index built against a self-signed embedder.
825
+
806
826
  The index is machine-generated: `venice index` drops a self-ignoring
807
827
  `.venice/.gitignore`, so it won't be committed even if your repo doesn't already
808
828
  ignore `.venice/`. Config-backable per-flag via `defaults.index.*` /
@@ -866,6 +886,7 @@ complete — a loud stderr warning is printed).
866
886
  | `write_file` | create/overwrite a file (atomic) | yes |
867
887
  | `edit_file` | replace an exact, unique string in a file | yes |
868
888
  | `run` | run a shell command (`/bin/sh -c`) at the root | yes |
889
+ | `venice_image` / `venice_image_edit` / `venice_sfx` / `venice_music` / `venice_tts` / `venice_upscale` / `venice_bg_remove` | generate/edit images & audio into the project — **opt-in with `--assets`** | yes |
869
890
 
870
891
  **Safety.** Every filesystem path is resolved and confined to the project root
871
892
  (default: cwd, or `--root` / `$VENICE_CODE_ROOT`); a path that escapes the root, names
@@ -888,10 +909,17 @@ forced cwd, timeout, and env-scrub — which is why it always confirms. git muta
888
909
  | `--root DIR` | project directory to sandbox to (default: cwd) |
889
910
  | `--max-tool-calls N` | cap tool invocations before forcing a final answer (default 25) |
890
911
  | `--exec-timeout SECS` | timeout for `run`/`git` (default 120) |
912
+ | `--assets` | also expose the in-process asset-generation tools (image / image-edit / sfx / music / tts / upscale / bg-remove) so the agent can create images & audio in the project; paid — each confirms per call unless `--auto` |
891
913
  | `-i`, `--json`, `--model`, `--system` | interactive REPL · JSON envelope · model · extra system instructions |
892
914
 
915
+ With `--assets`, generated files land in `$VENICE_MCP_OUTPUT_DIR` or, by default, under
916
+ the project root, and paid calls are capped per call by `$VENICE_MCP_MAX_SPEND` (default
917
+ **$0.10**) — **except** that `--auto` auto-approves every call and so bypasses that cap;
918
+ `--auto --assets` can incur up to `--max-tool-calls` paid generations, so use a cheap
919
+ model and a sane `--max-tool-calls` when running unattended.
920
+
893
921
  Per-flag config defaults live under `defaults.code.*` (e.g. `model`, `root`, `auto`,
894
- `max_tool_calls`).
922
+ `assets`, `max_tool_calls`).
895
923
 
896
924
  ## MCP server
897
925
 
@@ -1048,7 +1076,7 @@ The player list (`paplay` -> `aplay` -> `ffplay` -> `mpg123` -> `play`
1048
1076
  | `VENICE_BASE_URL` | override the API base URL (testing, proxy) |
1049
1077
  | `VENICE_EMBED_BASE_URL` | `embed` alternate OpenAI-compatible endpoint (local backend) |
1050
1078
  | `VENICE_EMBED_API_KEY` | key for `VENICE_EMBED_BASE_URL` (if the backend needs one) |
1051
- | `VENICE_EMBED_CA_BUNDLE` | CA bundle to trust for `VENICE_EMBED_BASE_URL` TLS (self-signed backend) |
1079
+ | `VENICE_EMBED_CA_BUNDLE` | CA bundle to trust for a self-signed embedding backend (`embed`, `index`, `search`, and the `project_search` agent tool) |
1052
1080
  | `VENICE_MCP_MAX_SPEND` | `mcp-serve` auto-approve cap in USD (default `0.10`) |
1053
1081
  | `VENICE_MCP_OUTPUT_DIR` | where `mcp-serve` tools write files (default: cwd) |
1054
1082
 
@@ -1072,7 +1100,7 @@ The player list (`paplay` -> `aplay` -> `ffplay` -> `mpg123` -> `play`
1072
1100
  | `venice chat MESSAGE [--system S] [--model M] [--web-search on] [...]` | one-shot chat completion (OpenAI SDK) |
1073
1101
  | `venice chat [-i] [--resume FILE]` | interactive multi-turn REPL (conversation state, `/`-commands, transcripts) |
1074
1102
  | `venice embed [TEXT] [--from-file PATH] [--model M] [--dimensions N] [--json] [--embed-base-url URL --embed-model M [--embed-ca-bundle PATH \| --embed-insecure]]` | text embeddings (OpenAI SDK; alt/local backend) |
1075
- | `venice index [PATH] [--model M] [...]` / `venice search QUERY [-k N] [--json]` | build / query a local semantic index of a project tree |
1103
+ | `venice index [PATH] [--model M] [--embed-base-url URL --embed-model M [--embed-ca-bundle PATH \| --embed-insecure]] [...]` / `venice search QUERY [-k N] [--json] [--embed-ca-bundle PATH \| --embed-insecure]` | build / query a local semantic index of a project tree |
1076
1104
  | `venice code [TASK] [--auto\|--manual] [--plan-only] [-i] [--root DIR] [--json] [...]` | coding agent: plan → accept → edit/run a project (needs `[openai]` + tool-calling model) |
1077
1105
  | `venice mcp-serve` | run an MCP server (stdio) exposing venice tools (needs `[mcp]`) |
1078
1106
  | `venice config add\|list\|remove\|show` | manage the MCP server registry |
@@ -765,6 +765,26 @@ venice index . --embed-base-url http://localhost:1234/v1 --embed-model bge-m3
765
765
  venice search "parse the queue response" # uses the index's own backend/model
766
766
  ```
767
767
 
768
+ **Self-signed backends.** Both commands accept the same TLS escape hatches as
769
+ `venice embed` — **applied only to a local backend, never the Venice endpoint**:
770
+
771
+ ```sh
772
+ venice index . --embed-base-url https://embed.local/v1 --embed-model bge-m3 \
773
+ --embed-ca-bundle /etc/ssl/my-ca.pem # trust a private CA (or $VENICE_EMBED_CA_BUNDLE)
774
+ venice search "parse the queue response" --embed-ca-bundle /etc/ssl/my-ca.pem
775
+ venice index . --embed-base-url https://embed.local/v1 --embed-model bge-m3 \
776
+ --embed-insecure # disable verification (warns; CLI-only)
777
+ ```
778
+
779
+ `--embed-ca-bundle` reads `$VENICE_EMBED_CA_BUNDLE` and is config-backable
780
+ (`defaults.index.embed_ca_bundle` / `defaults.search.embed_ca_bundle`);
781
+ `--embed-insecure` is CLI-only, mutually exclusive with it, and errors (exit 2) if
782
+ the flags don't apply (no `--embed-base-url` for `index`, or a Venice-built index
783
+ for `search`). For `search` the CA bundle is supplied fresh at query time — it is
784
+ never baked into the index — and the `project_search` agent tool also honours
785
+ `$VENICE_EMBED_CA_BUNDLE`, so a `venice chat`/`venice code` session can search an
786
+ index built against a self-signed embedder.
787
+
768
788
  The index is machine-generated: `venice index` drops a self-ignoring
769
789
  `.venice/.gitignore`, so it won't be committed even if your repo doesn't already
770
790
  ignore `.venice/`. Config-backable per-flag via `defaults.index.*` /
@@ -828,6 +848,7 @@ complete — a loud stderr warning is printed).
828
848
  | `write_file` | create/overwrite a file (atomic) | yes |
829
849
  | `edit_file` | replace an exact, unique string in a file | yes |
830
850
  | `run` | run a shell command (`/bin/sh -c`) at the root | yes |
851
+ | `venice_image` / `venice_image_edit` / `venice_sfx` / `venice_music` / `venice_tts` / `venice_upscale` / `venice_bg_remove` | generate/edit images & audio into the project — **opt-in with `--assets`** | yes |
831
852
 
832
853
  **Safety.** Every filesystem path is resolved and confined to the project root
833
854
  (default: cwd, or `--root` / `$VENICE_CODE_ROOT`); a path that escapes the root, names
@@ -850,10 +871,17 @@ forced cwd, timeout, and env-scrub — which is why it always confirms. git muta
850
871
  | `--root DIR` | project directory to sandbox to (default: cwd) |
851
872
  | `--max-tool-calls N` | cap tool invocations before forcing a final answer (default 25) |
852
873
  | `--exec-timeout SECS` | timeout for `run`/`git` (default 120) |
874
+ | `--assets` | also expose the in-process asset-generation tools (image / image-edit / sfx / music / tts / upscale / bg-remove) so the agent can create images & audio in the project; paid — each confirms per call unless `--auto` |
853
875
  | `-i`, `--json`, `--model`, `--system` | interactive REPL · JSON envelope · model · extra system instructions |
854
876
 
877
+ With `--assets`, generated files land in `$VENICE_MCP_OUTPUT_DIR` or, by default, under
878
+ the project root, and paid calls are capped per call by `$VENICE_MCP_MAX_SPEND` (default
879
+ **$0.10**) — **except** that `--auto` auto-approves every call and so bypasses that cap;
880
+ `--auto --assets` can incur up to `--max-tool-calls` paid generations, so use a cheap
881
+ model and a sane `--max-tool-calls` when running unattended.
882
+
855
883
  Per-flag config defaults live under `defaults.code.*` (e.g. `model`, `root`, `auto`,
856
- `max_tool_calls`).
884
+ `assets`, `max_tool_calls`).
857
885
 
858
886
  ## MCP server
859
887
 
@@ -1010,7 +1038,7 @@ The player list (`paplay` -> `aplay` -> `ffplay` -> `mpg123` -> `play`
1010
1038
  | `VENICE_BASE_URL` | override the API base URL (testing, proxy) |
1011
1039
  | `VENICE_EMBED_BASE_URL` | `embed` alternate OpenAI-compatible endpoint (local backend) |
1012
1040
  | `VENICE_EMBED_API_KEY` | key for `VENICE_EMBED_BASE_URL` (if the backend needs one) |
1013
- | `VENICE_EMBED_CA_BUNDLE` | CA bundle to trust for `VENICE_EMBED_BASE_URL` TLS (self-signed backend) |
1041
+ | `VENICE_EMBED_CA_BUNDLE` | CA bundle to trust for a self-signed embedding backend (`embed`, `index`, `search`, and the `project_search` agent tool) |
1014
1042
  | `VENICE_MCP_MAX_SPEND` | `mcp-serve` auto-approve cap in USD (default `0.10`) |
1015
1043
  | `VENICE_MCP_OUTPUT_DIR` | where `mcp-serve` tools write files (default: cwd) |
1016
1044
 
@@ -1034,7 +1062,7 @@ The player list (`paplay` -> `aplay` -> `ffplay` -> `mpg123` -> `play`
1034
1062
  | `venice chat MESSAGE [--system S] [--model M] [--web-search on] [...]` | one-shot chat completion (OpenAI SDK) |
1035
1063
  | `venice chat [-i] [--resume FILE]` | interactive multi-turn REPL (conversation state, `/`-commands, transcripts) |
1036
1064
  | `venice embed [TEXT] [--from-file PATH] [--model M] [--dimensions N] [--json] [--embed-base-url URL --embed-model M [--embed-ca-bundle PATH \| --embed-insecure]]` | text embeddings (OpenAI SDK; alt/local backend) |
1037
- | `venice index [PATH] [--model M] [...]` / `venice search QUERY [-k N] [--json]` | build / query a local semantic index of a project tree |
1065
+ | `venice index [PATH] [--model M] [--embed-base-url URL --embed-model M [--embed-ca-bundle PATH \| --embed-insecure]] [...]` / `venice search QUERY [-k N] [--json] [--embed-ca-bundle PATH \| --embed-insecure]` | build / query a local semantic index of a project tree |
1038
1066
  | `venice code [TASK] [--auto\|--manual] [--plan-only] [-i] [--root DIR] [--json] [...]` | coding agent: plan → accept → edit/run a project (needs `[openai]` + tool-calling model) |
1039
1067
  | `venice mcp-serve` | run an MCP server (stdio) exposing venice tools (needs `[mcp]`) |
1040
1068
  | `venice config add\|list\|remove\|show` | manage the MCP server registry |
@@ -0,0 +1 @@
1
+ __version__ = "0.28.0"
@@ -180,6 +180,28 @@ _SEARCH_SCHEMA = _obj(
180
180
  required=["query"],
181
181
  )
182
182
 
183
+ # Schema for a tool folded in ONLY via `only=` (e.g. `venice code --assets`), so it
184
+ # is not part of chat's default advertised set. Curated subset of
185
+ # `_mcp.image_edit_tool`; `confirm`/`max_spend`/`output_dir` omitted (loop-injected).
186
+ _IMAGE_EDIT_SCHEMA = _obj(
187
+ {
188
+ "prompt": _p("string", "Text directions for the edit, e.g. 'change the sky to a sunrise'."),
189
+ "input_path": _p("string", "Path to a local base image to edit."),
190
+ "image_url": _p("string", "URL of a base image (instead of input_path)."),
191
+ "layer_paths": {
192
+ "type": "array",
193
+ "items": {"type": "string"},
194
+ "description": "One or two local mask/overlay images (routes to /image/multi-edit).",
195
+ },
196
+ "model": _p("string", "Edit model id (default: the server picks one)."),
197
+ "aspect_ratio": _p("string", "Output aspect ratio ('auto' infers from the input)."),
198
+ "resolution": _p("string", "Output resolution tier, e.g. 1K/2K/4K."),
199
+ "output_format": _p("string", "Output image format: png, jpeg, or webp."),
200
+ "no_safe_mode": _p("boolean", "Disable safe mode (defaults to on)."),
201
+ },
202
+ required=["prompt"],
203
+ )
204
+
183
205
  # (tool name, `_mcp` impl attribute, description, schema, paid). The impl is
184
206
  # stored by NAME and resolved via getattr(_mcp, ...) at builtin_tools() time, so a
185
207
  # single source of truth wins and tests can patch `_mcp.<impl>`.
@@ -256,6 +278,22 @@ _BUILTINS = [
256
278
  ),
257
279
  ]
258
280
 
281
+ # Extra paid tools NOT advertised by chat's default set. Folded in only when a
282
+ # caller passes `only=` (e.g. `venice code --assets`), so chat's default stays 8
283
+ # while `code_tools` can still select them by name.
284
+ _CODE_ASSET_BUILTINS = [
285
+ (
286
+ "venice_image_edit",
287
+ "image_edit_tool",
288
+ "Edit/inpaint an existing image via Venice /image/edit from a text prompt "
289
+ "(base = a local input_path or an image_url; optional layer_paths route to "
290
+ "/image/multi-edit for masks). Writes the result and returns its path. "
291
+ "Dynamic pricing, so it always needs confirmation.",
292
+ _IMAGE_EDIT_SCHEMA,
293
+ True,
294
+ ),
295
+ ]
296
+
259
297
  # Loop-controlled kwargs the model must never supply (stripped defensively).
260
298
  _CONTROLLED = ("confirm", "max_spend", "output_dir")
261
299
 
@@ -273,11 +311,13 @@ def builtin_tools(
273
311
  output_dir: Optional[str] = None,
274
312
  only: Optional[set] = None,
275
313
  ) -> List[Tool]:
276
- """Build the 7 in-process venice tools, bound to `client`.
314
+ """Build the in-process venice tools, bound to `client`.
277
315
 
278
316
  `max_spend`/`output_dir` are baked into the paid tools' closures; `confirm` is
279
317
  passed per-call by the loop. `only` restricts the set to the named tools (an
280
- unknown name raises ValueError so the caller can exit 2).
318
+ unknown name raises ValueError so the caller can exit 2). With `only=None` the
319
+ set is exactly `_BUILTINS` (chat's default); passing `only=` also makes the
320
+ `_CODE_ASSET_BUILTINS` extras (e.g. `venice_image_edit`) selectable.
281
321
  """
282
322
 
283
323
  def _make_paid(impl):
@@ -298,6 +338,7 @@ def builtin_tools(
298
338
 
299
339
  return invoke
300
340
 
341
+ source = _BUILTINS if only is None else _BUILTINS + _CODE_ASSET_BUILTINS
301
342
  tools = [
302
343
  Tool(
303
344
  name=name,
@@ -310,7 +351,7 @@ def builtin_tools(
310
351
  ),
311
352
  paid=paid,
312
353
  )
313
- for (name, impl_name, desc, schema, paid) in _BUILTINS
354
+ for (name, impl_name, desc, schema, paid) in source
314
355
  ]
315
356
 
316
357
  if only is not None:
@@ -447,6 +447,8 @@ def code_tools(
447
447
  *,
448
448
  exec_timeout: int = DEFAULT_EXEC_TIMEOUT,
449
449
  include_search: bool = False,
450
+ assets: bool = False,
451
+ max_spend=None,
450
452
  ) -> List[_agent.Tool]:
451
453
  """Build the coding tools bound to a realpath-resolved project `root`.
452
454
 
@@ -454,6 +456,13 @@ def code_tools(
454
456
  (`write_file`/`edit_file`/`run`) are ``paid=True`` and route through the confirm
455
457
  gate. `project_search` (reusing the free `_mcp.search_tool`) is added only when
456
458
  `include_search` and a `client` are supplied and a `.venice` index exists.
459
+
460
+ When `assets` and a `client` are supplied, the in-process asset-generation tools
461
+ (`venice_image`/`venice_image_edit`/`venice_sfx`/`venice_music`/`venice_tts`/
462
+ `venice_upscale`/`venice_bg_remove`) are folded in via `_agent.builtin_tools`,
463
+ so the agent can create images/audio in the project. They are paid and route
464
+ through the same confirm/spend gate; generated files land in
465
+ ``$VENICE_MCP_OUTPUT_DIR`` or, by default, under `root`.
457
466
  """
458
467
  root = os.path.realpath(root)
459
468
 
@@ -517,6 +526,16 @@ def code_tools(
517
526
  "Read-only.",
518
527
  _SEARCH_SCHEMA, search_invoke, paid=False,
519
528
  ))
529
+
530
+ if assets and client is not None:
531
+ asset_dir = os.environ.get("VENICE_MCP_OUTPUT_DIR") or root
532
+ tools.extend(_agent.builtin_tools(
533
+ client, max_spend=max_spend, output_dir=asset_dir,
534
+ only={
535
+ "venice_image", "venice_image_edit", "venice_sfx",
536
+ "venice_music", "venice_tts", "venice_upscale", "venice_bg_remove",
537
+ },
538
+ ))
520
539
  return tools
521
540
 
522
541
 
@@ -407,24 +407,47 @@ def _sha256(data: bytes) -> str:
407
407
  # --------------------------------------------------------------------------- #
408
408
  # Embedding backend resolution + batched embedding
409
409
  # --------------------------------------------------------------------------- #
410
- def _resolve_backend(openai, *, embed_base_url, embed_model, model):
410
+ def _resolve_backend(openai, *, embed_base_url, embed_model, model,
411
+ ca_bundle=None, insecure=False):
411
412
  """Return (oai_client, model, backend_meta, exit_code). exit_code None on success.
412
413
 
413
414
  Mirrors ``embed._resolve_backend`` but stdout-free and primitive-arg. Local
414
415
  backend (--embed-base-url) skips Venice auth+catalog; Venice path validates
415
- --model against the free /models catalog.
416
+ --model against the free /models catalog. TLS overrides (`ca_bundle`/`insecure`,
417
+ #42) apply to the local backend only -- the Venice endpoint's TLS is not
418
+ overridable, so the flags are rejected there.
416
419
  """
417
420
  if embed_base_url:
418
421
  if not embed_model:
419
422
  print("index: --embed-model is required with --embed-base-url", file=sys.stderr)
420
423
  return None, None, None, 2
424
+ # TLS override, opt-in and non-Venice only: --embed-insecure wins (the CLI
425
+ # already blocks passing both), else a CA bundle, else the SDK default.
426
+ verify = False if insecure else (ca_bundle or None)
427
+ if verify is False:
428
+ print(
429
+ "index: WARNING: TLS verification disabled (--embed-insecure) "
430
+ f"for {embed_base_url}",
431
+ file=sys.stderr,
432
+ )
421
433
  oai = _openai.build_openai(
422
434
  openai, base_url=embed_base_url,
423
435
  api_key=os.environ.get(config.ENV_EMBED_API_KEY),
436
+ verify=verify,
424
437
  )
425
438
  meta = {"backend": "local", "model": embed_model, "base_url": embed_base_url}
426
439
  return oai, embed_model, meta, None
427
440
 
441
+ # The Venice endpoint's TLS is not overridable -- reject the flags here so they
442
+ # can't silently no-op against Venice.
443
+ if insecure or ca_bundle:
444
+ print(
445
+ "index: --embed-insecure/--embed-ca-bundle only apply with "
446
+ "--embed-base-url",
447
+ file=sys.stderr,
448
+ )
449
+ return None, None, None, 2
450
+
428
451
  try:
429
452
  client = build_client_from_auth()
430
453
  except auth.AuthError as e:
@@ -437,19 +460,47 @@ def _resolve_backend(openai, *, embed_base_url, embed_model, model):
437
460
  return _openai.build_openai(openai, client), resolved, {"backend": "venice", "model": resolved}, None
438
461
 
439
462
 
440
- def _backend_from_meta(openai, meta: dict):
463
+ def _backend_from_meta(openai, meta: dict, *, ca_bundle=None, insecure=False):
441
464
  """Rebuild the SDK client for `venice search` from stored index meta.
442
465
 
443
466
  The query MUST be embedded with the same backend/model/dims as the index, so
444
467
  everything comes from meta -- never re-resolved against a (possibly changed)
445
468
  catalog. The local-backend key is re-supplied from $VENICE_EMBED_API_KEY.
469
+
470
+ TLS overrides (#42) apply to a local-backend index only and are supplied fresh
471
+ at query time -- a CA path is machine-local, so it is never baked into the
472
+ store. `ca_bundle` falls back to $VENICE_EMBED_CA_BUNDLE so the no-CLI
473
+ `project_search` agent tool also reaches a self-signed embedder; `insecure` has
474
+ no env knob (CLI-only). On a Venice-built index the flags don't apply and are
475
+ rejected (exit 2) -- but the env fallback is consulted on the local path only,
476
+ so a globally-set $VENICE_EMBED_CA_BUNDLE never breaks searching a Venice index.
446
477
  """
447
478
  if meta.get("backend") == "local":
479
+ if ca_bundle is None:
480
+ ca_bundle = os.environ.get(config.ENV_EMBED_CA_BUNDLE)
481
+ verify = False if insecure else (ca_bundle or None)
482
+ if verify is False:
483
+ print(
484
+ "search: WARNING: TLS verification disabled (--embed-insecure) "
485
+ f"for {meta.get('base_url')}",
486
+ file=sys.stderr,
487
+ )
448
488
  oai = _openai.build_openai(
449
489
  openai, base_url=meta.get("base_url"),
450
490
  api_key=os.environ.get(config.ENV_EMBED_API_KEY),
491
+ verify=verify,
451
492
  )
452
493
  return oai, meta.get("model"), None
494
+
495
+ # Venice-built index: TLS is not overridable. Reject explicitly-passed flags
496
+ # loudly (parity with embed/index).
497
+ if insecure or ca_bundle:
498
+ print(
499
+ "search: --embed-insecure/--embed-ca-bundle only apply to a "
500
+ "local-backend index",
501
+ file=sys.stderr,
502
+ )
503
+ return None, None, 2
453
504
  try:
454
505
  client = build_client_from_auth()
455
506
  except auth.AuthError as e:
@@ -504,6 +555,7 @@ def _meta_conflict(old_meta: dict, backend_meta: dict, dimensions: Optional[int]
504
555
  def build_index(root, *, model=None, embed_base_url=None, embed_model=None,
505
556
  dimensions=None, rebuild=False, excludes=None, batch=DEFAULT_BATCH,
506
557
  chunk_lines=DEFAULT_CHUNK_LINES, chunk_overlap=DEFAULT_CHUNK_OVERLAP,
558
+ ca_bundle=None, insecure=False,
507
559
  on_progress: Optional[Callable[[str], None]] = None) -> dict:
508
560
  """Build/update the index under `root`. Returns a summary dict on success;
509
561
  raises IndexingError(exit_code) on failure. stdout-free (progress -> on_progress
@@ -520,7 +572,8 @@ def build_index(root, *, model=None, embed_base_url=None, embed_model=None,
520
572
  raise IndexingError("", 2)
521
573
 
522
574
  oai, resolved_model, backend_meta, rc = _resolve_backend(
523
- openai, embed_base_url=embed_base_url, embed_model=embed_model, model=model)
575
+ openai, embed_base_url=embed_base_url, embed_model=embed_model, model=model,
576
+ ca_bundle=ca_bundle, insecure=insecure)
524
577
  if rc is not None:
525
578
  raise IndexingError("", rc)
526
579
 
@@ -687,7 +740,7 @@ def _preview(root: Path, rel: str, start: int, end: int, stored_sha: Optional[st
687
740
 
688
741
 
689
742
  def search_index(store_dir, query: str, *, k: int = DEFAULT_TOP_K,
690
- with_preview: bool = True) -> List[dict]:
743
+ with_preview: bool = True, ca_bundle=None, insecure=False) -> List[dict]:
691
744
  """Embed `query` with the index's own backend/model and return the top-`k`
692
745
  chunks as dicts ``{path, start, end, score[, preview, changed]}``. stdout-free;
693
746
  raises IndexingError(exit_code) on failure."""
@@ -702,7 +755,7 @@ def search_index(store_dir, query: str, *, k: int = DEFAULT_TOP_K,
702
755
  openai = _openai.import_openai("search")
703
756
  if openai is None:
704
757
  raise IndexingError("", 2)
705
- oai, model, rc = _backend_from_meta(openai, meta)
758
+ oai, model, rc = _backend_from_meta(openai, meta, ca_bundle=ca_bundle, insecure=insecure)
706
759
  if rc is not None:
707
760
  raise IndexingError("", rc)
708
761
 
@@ -168,6 +168,12 @@ def register(subparsers) -> None:
168
168
  metavar="SECS",
169
169
  help=f"Timeout for run/git commands (default: {_code.DEFAULT_EXEC_TIMEOUT}).",
170
170
  )
171
+ grp.add_argument(
172
+ "--assets", action="store_true", dest="assets", default=None,
173
+ help="Also expose the in-process asset-generation tools (venice_image, "
174
+ "image_edit, sfx, music, tts, upscale, bg_remove) so the agent can create "
175
+ "images/audio in the project. Paid: each confirms per call unless --auto.",
176
+ )
171
177
 
172
178
  it = p.add_argument_group("Interactive")
173
179
  it.add_argument(
@@ -350,6 +356,7 @@ def _run(args) -> int:
350
356
  root, client,
351
357
  exec_timeout=args.exec_timeout or _code.DEFAULT_EXEC_TIMEOUT,
352
358
  include_search=True,
359
+ assets=bool(args.assets),
353
360
  )
354
361
  system = _system_prompt(args, root, tools)
355
362
  gen_kwargs = _gen_kwargs(args)
@@ -44,6 +44,17 @@ def register(subparsers) -> None:
44
44
  "--embed-model", dest="embed_model", default=None, metavar="NAME",
45
45
  help="Model id for --embed-base-url (required with it).",
46
46
  )
47
+ tls = p.add_mutually_exclusive_group()
48
+ tls.add_argument(
49
+ "--embed-ca-bundle", dest="embed_ca_bundle", default=None, metavar="PATH",
50
+ help="CA bundle (PEM) to verify a self-signed --embed-base-url; also "
51
+ "$VENICE_EMBED_CA_BUNDLE. Alternate backend only.",
52
+ )
53
+ tls.add_argument(
54
+ "--embed-insecure", dest="embed_insecure", action="store_true",
55
+ help="Disable TLS verification for --embed-base-url (alternate backend "
56
+ "only). Insecure -- prefer --embed-ca-bundle.",
57
+ )
47
58
  p.add_argument(
48
59
  "--dimensions", type=int, default=None,
49
60
  help="Truncate embedding vectors to this many dimensions (if supported).",
@@ -77,6 +88,8 @@ def _run(args) -> int:
77
88
  # (which only fills a dest still None), same as `venice embed`.
78
89
  if args.embed_base_url is None:
79
90
  args.embed_base_url = os.environ.get(config.ENV_EMBED_BASE_URL)
91
+ if args.embed_ca_bundle is None:
92
+ args.embed_ca_bundle = os.environ.get(config.ENV_EMBED_CA_BUNDLE)
80
93
  userconfig.apply_defaults(args, "index")
81
94
 
82
95
  def _progress(msg: str) -> None:
@@ -95,6 +108,8 @@ def _run(args) -> int:
95
108
  chunk_lines=args.chunk_lines or _index.DEFAULT_CHUNK_LINES,
96
109
  chunk_overlap=(args.chunk_overlap if args.chunk_overlap is not None
97
110
  else _index.DEFAULT_CHUNK_OVERLAP),
111
+ ca_bundle=args.embed_ca_bundle,
112
+ insecure=args.embed_insecure,
98
113
  on_progress=_progress,
99
114
  )
100
115
  except _index.IndexingError as e:
@@ -8,9 +8,10 @@ lives in the print-free ``commands._index``; this wraps it for the CLI.
8
8
  from __future__ import annotations
9
9
 
10
10
  import json
11
+ import os
11
12
  import sys
12
13
 
13
- from .. import userconfig
14
+ from .. import config, userconfig
14
15
  from . import _index
15
16
 
16
17
 
@@ -36,6 +37,17 @@ def register(subparsers) -> None:
36
37
  "-k", "--top-k", dest="top_k", type=int, default=None,
37
38
  help=f"Number of results to return (default: {_index.DEFAULT_TOP_K}).",
38
39
  )
40
+ tls = p.add_mutually_exclusive_group()
41
+ tls.add_argument(
42
+ "--embed-ca-bundle", dest="embed_ca_bundle", default=None, metavar="PATH",
43
+ help="CA bundle (PEM) to verify a self-signed embedding backend when the "
44
+ "index was built with --embed-base-url; also $VENICE_EMBED_CA_BUNDLE.",
45
+ )
46
+ tls.add_argument(
47
+ "--embed-insecure", dest="embed_insecure", action="store_true",
48
+ help="Disable TLS verification for a local-backend index's embedding "
49
+ "endpoint. Insecure -- prefer --embed-ca-bundle.",
50
+ )
39
51
  p.add_argument(
40
52
  "--json", action="store_true",
41
53
  help="Print results as a JSON object instead of text.",
@@ -44,6 +56,11 @@ def register(subparsers) -> None:
44
56
 
45
57
 
46
58
  def _run(args) -> int:
59
+ # Backend TLS flags follow CLI > env > config, same as `venice embed`/`index`;
60
+ # insecure is CLI-only (no env). The gate against a Venice-built index lives in
61
+ # the engine (a CA path is machine-local -- never stored in the index meta).
62
+ if args.embed_ca_bundle is None:
63
+ args.embed_ca_bundle = os.environ.get(config.ENV_EMBED_CA_BUNDLE)
47
64
  userconfig.apply_defaults(args, "search")
48
65
 
49
66
  store_dir = _index.discover_store(args.index_path)
@@ -55,7 +72,8 @@ def _run(args) -> int:
55
72
 
56
73
  try:
57
74
  results = _index.search_index(
58
- store_dir, args.query, k=args.top_k or _index.DEFAULT_TOP_K)
75
+ store_dir, args.query, k=args.top_k or _index.DEFAULT_TOP_K,
76
+ ca_bundle=args.embed_ca_bundle, insecure=args.embed_insecure)
59
77
  except _index.IndexingError as e:
60
78
  if str(e):
61
79
  print(f"search: {e}", file=sys.stderr)
@@ -224,18 +224,21 @@ _COMMAND_MAP = {
224
224
  "dimensions": ("dimensions", int),
225
225
  "embed_base_url": ("embed_base_url", str),
226
226
  "embed_model": ("embed_model", str),
227
+ "embed_ca_bundle": ("embed_ca_bundle", str),
227
228
  "batch": ("batch", int),
228
229
  "chunk_lines": ("chunk_lines", int),
229
230
  "chunk_overlap": ("chunk_overlap", int),
230
231
  },
231
232
  "search": {
232
233
  "top_k": ("top_k", int),
234
+ "embed_ca_bundle": ("embed_ca_bundle", str),
233
235
  },
234
236
  "code": {
235
237
  "model": ("model", str),
236
238
  "system": ("system", str),
237
239
  "root": ("root", str),
238
240
  "auto": ("auto", _as_bool),
241
+ "assets": ("assets", _as_bool),
239
242
  "max_tool_calls": ("max_tool_calls", int),
240
243
  "exec_timeout": ("exec_timeout", int),
241
244
  },
@@ -731,5 +731,35 @@ class TestChatMcp(unittest.TestCase):
731
731
  self.assertEqual(attach.specs, [("fs", {"command": "srv"})])
732
732
 
733
733
 
734
+ class TestBuiltinToolsRegistry(unittest.TestCase):
735
+ """`_agent.builtin_tools` source-selection (backs `venice code --assets`, #45)."""
736
+
737
+ def test_only_none_stays_eight(self):
738
+ # chat's default advertisement must not grow when code gains asset tools
739
+ from venice.commands import _agent
740
+ names = {t.name for t in _agent.builtin_tools(object())}
741
+ self.assertEqual(len(names), 8)
742
+ self.assertNotIn("venice_image_edit", names)
743
+
744
+ def test_only_can_select_code_asset_extra(self):
745
+ from venice.commands import _agent
746
+ tools = _agent.builtin_tools(object(), only={"venice_image_edit"})
747
+ self.assertEqual([t.name for t in tools], ["venice_image_edit"])
748
+ self.assertTrue(tools[0].paid)
749
+
750
+ def test_only_mixes_builtins_and_extras(self):
751
+ from venice.commands import _agent
752
+ names = {t.name for t in _agent.builtin_tools(
753
+ object(), only={"venice_image", "venice_image_edit"})}
754
+ self.assertEqual(names, {"venice_image", "venice_image_edit"})
755
+
756
+ def test_image_edit_schema_excludes_controlled(self):
757
+ from venice.commands import _agent
758
+ props = _agent._IMAGE_EDIT_SCHEMA["properties"]
759
+ for banned in ("confirm", "max_spend", "output_dir"):
760
+ self.assertNotIn(banned, props)
761
+ self.assertEqual(_agent._IMAGE_EDIT_SCHEMA.get("required"), ["prompt"])
762
+
763
+
734
764
  if __name__ == "__main__":
735
765
  unittest.main()
@@ -16,6 +16,12 @@ def _tools(root, **kw):
16
16
  return {t.name: t for t in _code.code_tools(root, **kw)}
17
17
 
18
18
 
19
+ _ASSET_NAMES = {
20
+ "venice_image", "venice_image_edit", "venice_sfx", "venice_music",
21
+ "venice_tts", "venice_upscale", "venice_bg_remove",
22
+ }
23
+
24
+
19
25
  class TestCodeTools(unittest.TestCase):
20
26
  def setUp(self):
21
27
  self.tmp = tempfile.mkdtemp()
@@ -224,6 +230,34 @@ class TestCodeFactory(unittest.TestCase):
224
230
  include_search=True)}
225
231
  self.assertNotIn("project_search", names) # no .venice index discoverable
226
232
 
233
+ def test_assets_absent_by_default(self):
234
+ names = {t.name for t in _code.code_tools("/tmp", client=object())}
235
+ self.assertEqual(names & _ASSET_NAMES, set())
236
+
237
+ def test_assets_need_a_client(self):
238
+ # the flag alone (no client) folds nothing in
239
+ names = {t.name for t in _code.code_tools("/tmp", assets=True)}
240
+ self.assertEqual(names & _ASSET_NAMES, set())
241
+
242
+ def test_assets_present_when_enabled(self):
243
+ names = {t.name for t in _code.code_tools("/tmp", client=object(),
244
+ assets=True)}
245
+ self.assertTrue(_ASSET_NAMES <= names) # all 7 folded in
246
+ self.assertNotIn("venice_chat", names) # excluded by design
247
+ self.assertNotIn("venice_video", names) # deferred
248
+
249
+ def test_asset_tools_are_paid(self):
250
+ by = {t.name: t for t in _code.code_tools("/tmp", client=object(),
251
+ assets=True)}
252
+ for n in _ASSET_NAMES:
253
+ self.assertTrue(by[n].paid, f"{n} should be paid")
254
+
255
+ def test_asset_schemas_exclude_control_kwargs(self):
256
+ for t in _code.code_tools("/tmp", client=object(), assets=True):
257
+ props = t.parameters.get("properties", {})
258
+ for banned in ("confirm", "max_spend", "output_dir"):
259
+ self.assertNotIn(banned, props, f"{t.name} leaks {banned}")
260
+
227
261
  def test_controlled_kwargs_stripped_from_model_args(self):
228
262
  # a model that smuggles confirm=True must not self-approve a paid tool
229
263
  with tempfile.TemporaryDirectory() as d:
@@ -23,7 +23,7 @@ def _code_args(**ov):
23
23
  task=None, root=None, model=None, system=None, temperature=None,
24
24
  max_tokens=None, json=False, auto=None, manual=None, yes=None,
25
25
  plan_only=False, no_plan=False, no_verify=False, max_tool_calls=None,
26
- exec_timeout=None, interactive=False, resume=None,
26
+ exec_timeout=None, interactive=False, resume=None, assets=None,
27
27
  )
28
28
  base.update(ov)
29
29
  return argparse.Namespace(**base)
@@ -92,6 +92,60 @@ class TestCodeCommand(unittest.TestCase):
92
92
  with open(os.path.join(self.root, "hello.py")) as f:
93
93
  self.assertEqual(f.read(), "def hi():\n return 1\n")
94
94
 
95
+ # --- --assets exposes the in-process asset tools ---
96
+ def _exec_tool_names(self, calls):
97
+ # the execute turn is the one advertising tools with tool_choice="auto"
98
+ execs = [c for c in calls if c.get("tool_choice") == "auto"]
99
+ self.assertTrue(execs, "no execute turn recorded")
100
+ return {t["function"]["name"] for t in execs[0]["tools"]}
101
+
102
+ def test_assets_flag_exposes_asset_tools(self):
103
+ seq = [
104
+ FakeToolCompletion("plan"),
105
+ FakeToolCompletion("nothing to do"), # execute (auto), no calls
106
+ FakeToolCompletion("ACCEPTANCE: PASS"),
107
+ ]
108
+ rc, calls = self._run(
109
+ _code_args(task="draw", root=self.root, auto=True, assets=True), seq)
110
+ self.assertEqual(rc, 0)
111
+ names = self._exec_tool_names(calls)
112
+ self.assertIn("venice_image", names)
113
+ self.assertIn("venice_image_edit", names)
114
+ self.assertNotIn("venice_chat", names) # excluded by design
115
+
116
+ def test_assets_absent_by_default(self):
117
+ seq = [
118
+ FakeToolCompletion("plan"),
119
+ FakeToolCompletion("nothing to do"),
120
+ FakeToolCompletion("ACCEPTANCE: PASS"),
121
+ ]
122
+ rc, calls = self._run(
123
+ _code_args(task="x", root=self.root, auto=True), seq)
124
+ self.assertEqual(rc, 0)
125
+ self.assertNotIn("venice_image", self._exec_tool_names(calls))
126
+
127
+ def test_assets_tool_dispatches_with_confirm_under_auto(self):
128
+ seq = [
129
+ FakeToolCompletion("plan"),
130
+ FakeToolCompletion(tool_calls=[
131
+ _FnCall("c1", "venice_image",
132
+ json.dumps({"prompt": "a hero sprite"}))]),
133
+ FakeToolCompletion("made the sprite"),
134
+ FakeToolCompletion("ACCEPTANCE: PASS"),
135
+ ]
136
+ with mock.patch(
137
+ "venice.commands._mcp.image_tool",
138
+ return_value={"status": "ok", "paths": ["/x.png"], "count": 1},
139
+ ) as stub:
140
+ rc, calls = self._run(
141
+ _code_args(task="draw", root=self.root, auto=True, assets=True), seq)
142
+ self.assertEqual(rc, 0)
143
+ self.assertEqual(stub.call_count, 1)
144
+ # --auto -> confirm=True bypasses the spend gate
145
+ self.assertTrue(stub.call_args.kwargs.get("confirm"))
146
+ # the model supplied only prompt; control kwargs are injected, not from args
147
+ self.assertEqual(stub.call_args.kwargs.get("prompt"), "a hero sprite")
148
+
95
149
  def test_acceptance_fail_returns_1(self):
96
150
  seq = [
97
151
  FakeToolCompletion("plan"),
@@ -68,7 +68,8 @@ def write(root, rel, content, *, binary=False):
68
68
 
69
69
  def _no_key_env():
70
70
  return {k: v for k, v in os.environ.items()
71
- if k not in ("VENICE_API_KEY", "VENICE_EMBED_BASE_URL", "VENICE_EMBED_API_KEY")}
71
+ if k not in ("VENICE_API_KEY", "VENICE_EMBED_BASE_URL",
72
+ "VENICE_EMBED_API_KEY", "VENICE_EMBED_CA_BUNDLE")}
72
73
 
73
74
 
74
75
  class _EngineBase(unittest.TestCase):
@@ -323,7 +324,8 @@ class TestMissingOpenai(_EngineBase):
323
324
  class TestIndexCLI(unittest.TestCase):
324
325
  def _args(self, root, **ov):
325
326
  base = dict(path=root, model=None, embed_base_url="http://local/v1",
326
- embed_model="m", dimensions=None, rebuild=False, exclude=None,
327
+ embed_model="m", embed_ca_bundle=None, embed_insecure=False,
328
+ dimensions=None, rebuild=False, exclude=None,
327
329
  batch=None, chunk_lines=None, chunk_overlap=None)
328
330
  base.update(ov)
329
331
  return argparse.Namespace(**base)
@@ -345,6 +347,111 @@ class TestIndexCLI(unittest.TestCase):
345
347
  self.assertEqual(out.getvalue().strip(),
346
348
  str(_index.store_file(_index.store_dir_for_root(tmp.name))))
347
349
 
350
+ # --- #42: TLS override reaches the SDK through CLI env/config layering ---
351
+
352
+ def _run_tls(self, *, env=None, doc=None, **arg_ov):
353
+ """Drive index._run against the local backend with httpx.Client patched to
354
+ a sentinel; returns (rc, Hx_mock) so a test can assert the verify value."""
355
+ from venice.commands import index
356
+ tmp = tempfile.TemporaryDirectory()
357
+ self.addCleanup(tmp.cleanup)
358
+ write(tmp.name, "a.py", "alpha\n")
359
+ base_env = _no_key_env()
360
+ if env:
361
+ base_env.update(env)
362
+ fake, _ = fake_openai()
363
+ with mock.patch("httpx.Client", return_value="httpx-sentinel") as Hx, \
364
+ mock.patch("openai.OpenAI", return_value=fake), \
365
+ mock.patch("venice.userconfig.load_config",
366
+ return_value=doc or _clean_doc()), \
367
+ mock.patch.dict(os.environ, base_env, clear=True), \
368
+ mock.patch.object(sys, "stdout", io.StringIO()), \
369
+ mock.patch.object(sys, "stderr", io.StringIO()):
370
+ rc = index._run(self._args(tmp.name, **arg_ov))
371
+ return rc, Hx
372
+
373
+ def test_ca_bundle_from_flag(self):
374
+ rc, Hx = self._run_tls(embed_ca_bundle="/flag-ca.pem")
375
+ self.assertEqual(rc, 0)
376
+ Hx.assert_called_once_with(verify="/flag-ca.pem")
377
+
378
+ def test_ca_bundle_from_env(self):
379
+ rc, Hx = self._run_tls(env={"VENICE_EMBED_CA_BUNDLE": "/env-ca.pem"})
380
+ self.assertEqual(rc, 0)
381
+ Hx.assert_called_once_with(verify="/env-ca.pem")
382
+
383
+ def test_ca_bundle_from_config(self):
384
+ doc = {"version": 1, "mcpServers": {}, "defaults": {
385
+ "index": {"embed_ca_bundle": "/cfg-ca.pem"}}}
386
+ rc, Hx = self._run_tls(doc=doc)
387
+ self.assertEqual(rc, 0)
388
+ Hx.assert_called_once_with(verify="/cfg-ca.pem")
389
+
390
+ def test_flag_beats_env(self):
391
+ rc, Hx = self._run_tls(embed_ca_bundle="/flag-ca.pem",
392
+ env={"VENICE_EMBED_CA_BUNDLE": "/env-ca.pem"})
393
+ self.assertEqual(rc, 0)
394
+ Hx.assert_called_once_with(verify="/flag-ca.pem")
395
+
396
+
397
+ class TestIndexTLS(_EngineBase):
398
+ """#42: TLS escape hatch on the local embedding backend (engine level)."""
399
+
400
+ def _build(self, *, stderr=None, **ov):
401
+ """build_index on self.root via the local backend, httpx.Client patched to
402
+ a sentinel. Returns (Hx_mock, OAI_mock); propagates IndexingError."""
403
+ write(self.root, "a.txt", "alpha\n")
404
+ fake, _ = fake_openai()
405
+ kw = dict(embed_base_url="http://local/v1", embed_model="m")
406
+ kw.update(ov)
407
+ with mock.patch("httpx.Client", return_value="httpx-sentinel") as Hx, \
408
+ mock.patch("openai.OpenAI", return_value=fake) as OAI, \
409
+ mock.patch.dict(os.environ, _no_key_env(), clear=True), \
410
+ mock.patch.object(sys, "stderr", stderr or io.StringIO()):
411
+ _index.build_index(self.root, **kw)
412
+ return Hx, OAI
413
+
414
+ def test_ca_bundle_reaches_sdk_on_alt_path(self):
415
+ Hx, OAI = self._build(ca_bundle="/ca.pem")
416
+ Hx.assert_called_once_with(verify="/ca.pem")
417
+ self.assertEqual(OAI.call_args.kwargs["http_client"], "httpx-sentinel")
418
+
419
+ def test_insecure_disables_verification_and_warns(self):
420
+ err = io.StringIO()
421
+ Hx, OAI = self._build(insecure=True, stderr=err)
422
+ Hx.assert_called_once_with(verify=False)
423
+ self.assertEqual(OAI.call_args.kwargs["http_client"], "httpx-sentinel")
424
+ self.assertIn("TLS verification disabled", err.getvalue())
425
+
426
+ def test_no_override_builds_no_http_client(self):
427
+ Hx, OAI = self._build()
428
+ Hx.assert_not_called()
429
+ self.assertNotIn("http_client", OAI.call_args.kwargs)
430
+
431
+ def test_insecure_without_base_url_is_rejected(self):
432
+ err = io.StringIO()
433
+ with self.assertRaises(_index.IndexingError) as cm:
434
+ self._build(embed_base_url=None, insecure=True, stderr=err)
435
+ self.assertEqual(cm.exception.exit_code, 2)
436
+ self.assertIn("only apply with --embed-base-url", err.getvalue())
437
+
438
+ def test_ca_bundle_without_base_url_is_rejected(self):
439
+ err = io.StringIO()
440
+ with self.assertRaises(_index.IndexingError) as cm:
441
+ self._build(embed_base_url=None, ca_bundle="/ca.pem", stderr=err)
442
+ self.assertEqual(cm.exception.exit_code, 2)
443
+ self.assertIn("only apply with --embed-base-url", err.getvalue())
444
+
445
+ def test_ca_bundle_and_insecure_are_mutually_exclusive(self):
446
+ from venice.commands import index
447
+ parser = argparse.ArgumentParser()
448
+ sub = parser.add_subparsers()
449
+ index.register(sub)
450
+ with mock.patch.object(sys, "stderr", io.StringIO()), \
451
+ self.assertRaises(SystemExit):
452
+ parser.parse_args(
453
+ ["index", "--embed-ca-bundle", "/ca.pem", "--embed-insecure"])
454
+
348
455
 
349
456
  if __name__ == "__main__":
350
457
  unittest.main()
@@ -108,7 +108,8 @@ class TestDiscovery(_Base):
108
108
 
109
109
  class TestSearchCLI(_Base):
110
110
  def _args(self, **ov):
111
- base = dict(query="alpha", index_path=self.root, top_k=None, json=False)
111
+ base = dict(query="alpha", index_path=self.root, top_k=None, json=False,
112
+ embed_ca_bundle=None, embed_insecure=False)
112
113
  base.update(ov)
113
114
  return argparse.Namespace(**base)
114
115
 
@@ -178,5 +179,103 @@ class TestSearchTool(_Base):
178
179
  self.assertEqual(res["status"], "error")
179
180
 
180
181
 
182
+ class TestSearchTLS(_Base):
183
+ """#42: TLS override supplied at query time reaches the SDK even though the
184
+ backend base_url comes from stored index meta (a local-backed index)."""
185
+
186
+ def _search_tls(self, *, env=None, stderr=None, **ov):
187
+ """search_index against the local-backed store with httpx.Client patched
188
+ to a sentinel; returns (Hx_mock, OAI_mock)."""
189
+ fake, _ = fake_openai()
190
+ base_env = _no_key_env()
191
+ if env:
192
+ base_env.update(env)
193
+ with mock.patch("httpx.Client", return_value="httpx-sentinel") as Hx, \
194
+ mock.patch("openai.OpenAI", return_value=fake) as OAI, \
195
+ mock.patch.dict(os.environ, base_env, clear=True), \
196
+ mock.patch.object(sys, "stderr", stderr or io.StringIO()):
197
+ _index.search_index(self.store, "alpha", **ov)
198
+ return Hx, OAI
199
+
200
+ def test_ca_bundle_reaches_sdk_at_query_time(self):
201
+ Hx, OAI = self._search_tls(ca_bundle="/ca.pem")
202
+ Hx.assert_called_once_with(verify="/ca.pem")
203
+ self.assertEqual(OAI.call_args.kwargs["http_client"], "httpx-sentinel")
204
+
205
+ def test_insecure_disables_verification_and_warns(self):
206
+ err = io.StringIO()
207
+ Hx, _ = self._search_tls(insecure=True, stderr=err)
208
+ Hx.assert_called_once_with(verify=False)
209
+ self.assertIn("TLS verification disabled", err.getvalue())
210
+
211
+ def test_no_override_builds_no_http_client(self):
212
+ Hx, OAI = self._search_tls()
213
+ Hx.assert_not_called()
214
+ self.assertNotIn("http_client", OAI.call_args.kwargs)
215
+
216
+ def test_env_ca_bundle_picked_up_when_no_arg(self):
217
+ # The no-CLI project_search path: search_index falls back to the env var so
218
+ # the agent tool also reaches a self-signed embedder.
219
+ Hx, _ = self._search_tls(env={"VENICE_EMBED_CA_BUNDLE": "/env-ca.pem"})
220
+ Hx.assert_called_once_with(verify="/env-ca.pem")
221
+
222
+ def test_explicit_ca_bundle_beats_env(self):
223
+ Hx, _ = self._search_tls(ca_bundle="/arg-ca.pem",
224
+ env={"VENICE_EMBED_CA_BUNDLE": "/env-ca.pem"})
225
+ Hx.assert_called_once_with(verify="/arg-ca.pem")
226
+
227
+ def test_project_search_tool_uses_env_ca_bundle(self):
228
+ # End-to-end for the agent path: _mcp.search_tool -> search_index picks up
229
+ # the env CA bundle. Discovery is cwd-based, so chdir into the indexed root.
230
+ cwd = os.getcwd()
231
+ self.addCleanup(os.chdir, cwd)
232
+ os.chdir(self.root)
233
+ fake, _ = fake_openai()
234
+ with mock.patch("httpx.Client", return_value="httpx-sentinel") as Hx, \
235
+ mock.patch("openai.OpenAI", return_value=fake), \
236
+ mock.patch.dict(os.environ, {**_no_key_env(),
237
+ "VENICE_EMBED_CA_BUNDLE": "/env-ca.pem"},
238
+ clear=True), \
239
+ mock.patch.object(sys, "stderr", io.StringIO()):
240
+ from venice.commands import _mcp
241
+ res = _mcp.search_tool(object(), "alpha", k=1)
242
+ self.assertEqual(res["status"], "ok")
243
+ Hx.assert_called_once_with(verify="/env-ca.pem")
244
+
245
+
246
+ class TestBackendFromMetaGate(unittest.TestCase):
247
+ """#42: on a Venice-built index the TLS flags don't apply. Explicit flags are
248
+ rejected (exit 2); a globally-set env CA bundle is ignored, never tripping the
249
+ gate -- so project_search over a Venice index keeps working."""
250
+
251
+ def test_explicit_flags_rejected_on_venice_index(self):
252
+ import openai
253
+ err = io.StringIO()
254
+ with mock.patch.object(sys, "stderr", err):
255
+ oai, model, rc = _index._backend_from_meta(
256
+ openai, {"backend": "venice", "model": "m"}, ca_bundle="/ca.pem")
257
+ self.assertEqual(rc, 2)
258
+ self.assertIsNone(oai)
259
+ self.assertIn("only apply to a local-backend index", err.getvalue())
260
+
261
+ def test_env_ca_bundle_does_not_trip_venice_gate(self):
262
+ import openai
263
+ from venice import auth
264
+ err = io.StringIO()
265
+ # env set + no explicit flag: the venice path never consults env, so the
266
+ # TLS gate is NOT tripped -- we fall through to auth (stubbed to fail).
267
+ with mock.patch.dict(os.environ, {**_no_key_env(),
268
+ "VENICE_EMBED_CA_BUNDLE": "/env-ca.pem"},
269
+ clear=True), \
270
+ mock.patch("venice.commands._index.build_client_from_auth",
271
+ side_effect=auth.AuthError("no key")), \
272
+ mock.patch.object(sys, "stderr", err):
273
+ oai, model, rc = _index._backend_from_meta(
274
+ openai, {"backend": "venice", "model": "m"})
275
+ self.assertEqual(rc, 2)
276
+ self.assertNotIn("only apply to a local-backend index", err.getvalue())
277
+ self.assertIn("no key", err.getvalue())
278
+
279
+
181
280
  if __name__ == "__main__":
182
281
  unittest.main()
@@ -1 +0,0 @@
1
- __version__ = "0.26.0"
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