v2.9.6 (#682)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
b23f49e5f4
commit
626f905186
59 changed files with 702 additions and 183 deletions
|
|
@ -67,7 +67,7 @@ Always go through `uv` — never invoke bare `python` or `pip`.
|
||||||
|
|
||||||
- Use `from vibe.core.logger import logger` — stdlib `logging` with `StructuredLogFormatter`, not `structlog`.
|
- Use `from vibe.core.logger import logger` — stdlib `logging` with `StructuredLogFormatter`, not `structlog`.
|
||||||
- Configure via env: `LOG_LEVEL` (default `WARNING`), `LOG_MAX_BYTES`. Logs land in `~/.vibe/logs/vibe.log`.
|
- Configure via env: `LOG_LEVEL` (default `WARNING`), `LOG_MAX_BYTES`. Logs land in `~/.vibe/logs/vibe.log`.
|
||||||
- Pass variables as keyword args, not interpolated into the message: prefer `logger.error("Failed to fetch", url=url)` over `logger.error(f"Failed to fetch {url}")`.
|
- Pass variables as `%s` positional args, not f-string interpolation: prefer `logger.error("Failed to fetch url=%s", url)` over `logger.error(f"Failed to fetch {url}")`. This defers formatting to the logging framework (only formats if the message is emitted) and keeps messages grep-friendly.
|
||||||
- Define module-local exception hierarchies. Always chain with `raise NewError(...) from e`. Rich exceptions expose a `_fmt()` helper for human-readable output.
|
- Define module-local exception hierarchies. Always chain with `raise NewError(...) from e`. Rich exceptions expose a `_fmt()` helper for human-readable output.
|
||||||
|
|
||||||
## File I/O
|
## File I/O
|
||||||
|
|
|
||||||
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.9.6] - 2026-05-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Syntax-highlighted file diffs for `write_file` and `search_replace` in the IDE agent webview
|
||||||
|
- Spinner and loader for `!` (bang) command output
|
||||||
|
- `prepare release` script can resume after resolving merge conflicts
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Strip wildcard suffix when persisting bash allowlist patterns
|
||||||
|
- Build a combined SSL context for all outbound HTTPS requests
|
||||||
|
- Hide non-interactive tools from the LLM in ACP and programmatic mode
|
||||||
|
|
||||||
|
|
||||||
## [2.9.5] - 2026-05-06
|
## [2.9.5] - 2026-05-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
id = "mistral-vibe"
|
id = "mistral-vibe"
|
||||||
name = "Mistral Vibe"
|
name = "Mistral Vibe"
|
||||||
description = "Mistral's open-source coding assistant"
|
description = "Mistral's open-source coding assistant"
|
||||||
version = "2.9.5"
|
version = "2.9.6"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Mistral AI"]
|
authors = ["Mistral AI"]
|
||||||
repository = "https://github.com/mistralai/mistral-vibe"
|
repository = "https://github.com/mistralai/mistral-vibe"
|
||||||
|
|
@ -11,21 +11,21 @@ name = "Mistral Vibe"
|
||||||
icon = "./icons/mistral_vibe.svg"
|
icon = "./icons/mistral_vibe.svg"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.5/vibe-acp-darwin-aarch64-2.9.5.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.6/vibe-acp-darwin-aarch64-2.9.6.zip"
|
||||||
cmd = "./vibe-acp"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.5/vibe-acp-darwin-x86_64-2.9.5.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.6/vibe-acp-darwin-x86_64-2.9.6.zip"
|
||||||
cmd = "./vibe-acp"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.5/vibe-acp-linux-aarch64-2.9.5.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.6/vibe-acp-linux-aarch64-2.9.6.zip"
|
||||||
cmd = "./vibe-acp"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.5/vibe-acp-linux-x86_64-2.9.5.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.6/vibe-acp-linux-x86_64-2.9.6.zip"
|
||||||
cmd = "./vibe-acp"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.5/vibe-acp-windows-x86_64-2.9.5.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.6/vibe-acp-windows-x86_64-2.9.6.zip"
|
||||||
cmd = "./vibe-acp.exe"
|
cmd = "./vibe-acp.exe"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "mistral-vibe"
|
name = "mistral-vibe"
|
||||||
version = "2.9.5"
|
version = "2.9.6"
|
||||||
description = "Minimal CLI coding agent by Mistral"
|
description = "Minimal CLI coding agent by Mistral"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
@ -30,6 +30,7 @@ dependencies = [
|
||||||
"agent-client-protocol==0.9.0",
|
"agent-client-protocol==0.9.0",
|
||||||
"anyio>=4.12.0",
|
"anyio>=4.12.0",
|
||||||
"cachetools>=5.5.0",
|
"cachetools>=5.5.0",
|
||||||
|
"certifi>=2026.4.22",
|
||||||
"charset-normalizer>=3.4.4",
|
"charset-normalizer>=3.4.4",
|
||||||
"cryptography>=44.0.0",
|
"cryptography>=44.0.0",
|
||||||
"gitpython>=3.1.46",
|
"gitpython>=3.1.46",
|
||||||
|
|
@ -173,7 +174,7 @@ order-by-type = true
|
||||||
required-imports = ["from __future__ import annotations"]
|
required-imports = ["from __future__ import annotations"]
|
||||||
|
|
||||||
[tool.ruff.lint.pylint]
|
[tool.ruff.lint.pylint]
|
||||||
max-public-methods = 25
|
max-public-methods = 26
|
||||||
max-positional-args = 10
|
max-positional-args = 10
|
||||||
max-statements = 50
|
max-statements = 50
|
||||||
max-branches = 15
|
max-branches = 15
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,26 @@ def get_latest_version() -> str:
|
||||||
return max(versions)[3]
|
return max(versions)[3]
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_version_from_current_branch() -> str:
|
||||||
|
result = run_git_command(
|
||||||
|
"describe",
|
||||||
|
"--tags",
|
||||||
|
"--match",
|
||||||
|
"v[0-9]*.[0-9]*.[0-9]*",
|
||||||
|
"--exclude",
|
||||||
|
"*-private",
|
||||||
|
"--abbrev=0",
|
||||||
|
"HEAD",
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
tag = result.stdout.strip()
|
||||||
|
match = re.match(r"^v(\d+\.\d+\.\d+)$", tag)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Invalid base version tag found: {tag}")
|
||||||
|
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
|
||||||
def parse_version(version_str: str) -> tuple[int, int, int]:
|
def parse_version(version_str: str) -> tuple[int, int, int]:
|
||||||
match = re.match(r"^(\d+)\.(\d+)\.(\d+)$", version_str.strip())
|
match = re.match(r"^(\d+)\.(\d+)\.(\d+)$", version_str.strip())
|
||||||
if not match:
|
if not match:
|
||||||
|
|
@ -130,34 +150,31 @@ def create_release_branch(version: str) -> None:
|
||||||
print(f"Created and switched to branch {branch_name}")
|
print(f"Created and switched to branch {branch_name}")
|
||||||
|
|
||||||
|
|
||||||
def cherry_pick_commits(
|
def cherry_pick_commits(previous_private_tag: str, current_private_tag: str) -> None:
|
||||||
previous_version: str, current_version: str, squash: bool
|
|
||||||
) -> None:
|
|
||||||
previous_tag = f"v{previous_version}-private"
|
|
||||||
current_tag = f"v{current_version}-private"
|
|
||||||
|
|
||||||
result = run_git_command(
|
result = run_git_command(
|
||||||
"rev-parse", "--verify", previous_tag, capture_output=True, check=False
|
"rev-parse", "--verify", previous_private_tag, capture_output=True, check=False
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise ValueError(f"Tag {previous_tag} does not exist")
|
raise ValueError(f"Tag {previous_private_tag} does not exist")
|
||||||
|
|
||||||
result = run_git_command(
|
result = run_git_command(
|
||||||
"rev-parse", "--verify", current_tag, capture_output=True, check=False
|
"rev-parse", "--verify", current_private_tag, capture_output=True, check=False
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise ValueError(f"Tag {current_tag} does not exist")
|
raise ValueError(f"Tag {current_private_tag} does not exist")
|
||||||
|
|
||||||
print(f"Cherry-picking commits from {previous_tag}..{current_tag}...")
|
print(
|
||||||
run_git_command("cherry-pick", f"{previous_tag}..{current_tag}")
|
f"Cherry-picking commits from {previous_private_tag}..{current_private_tag}..."
|
||||||
|
)
|
||||||
|
run_git_command("cherry-pick", f"{previous_private_tag}..{current_private_tag}")
|
||||||
print("Successfully cherry-picked all commits")
|
print("Successfully cherry-picked all commits")
|
||||||
|
|
||||||
if squash:
|
|
||||||
squash_commits(previous_version, current_version, previous_tag, current_tag)
|
|
||||||
|
|
||||||
|
|
||||||
def squash_commits(
|
def squash_commits(
|
||||||
previous_version: str, current_version: str, previous_tag: str, current_tag: str
|
previous_version: str,
|
||||||
|
current_version: str,
|
||||||
|
previous_private_tag: str,
|
||||||
|
current_private_tag: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
print("Squashing commits into a single release commit...")
|
print("Squashing commits into a single release commit...")
|
||||||
run_git_command("reset", "--soft", f"v{previous_version}")
|
run_git_command("reset", "--soft", f"v{previous_version}")
|
||||||
|
|
@ -165,7 +182,7 @@ def squash_commits(
|
||||||
# Get all contributors between previous and current private tags
|
# Get all contributors between previous and current private tags
|
||||||
result = run_git_command(
|
result = run_git_command(
|
||||||
"log",
|
"log",
|
||||||
f"{previous_tag}..{current_tag}",
|
f"{previous_private_tag}..{current_private_tag}",
|
||||||
"--format=%aN <%aE>",
|
"--format=%aN <%aE>",
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
)
|
)
|
||||||
|
|
@ -289,6 +306,9 @@ def main() -> None:
|
||||||
default=True,
|
default=True,
|
||||||
help="Disable squashing of commits into a single release commit",
|
help="Disable squashing of commits into a single release commit",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--resume", action="store_true", help="Resume after cherry-picking commits"
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
current_version = args.version
|
current_version = args.version
|
||||||
|
|
@ -298,13 +318,16 @@ def main() -> None:
|
||||||
# Step 1: Ensure public remote exists
|
# Step 1: Ensure public remote exists
|
||||||
ensure_public_remote()
|
ensure_public_remote()
|
||||||
|
|
||||||
# Step 2: Fetch all remotes
|
if args.resume:
|
||||||
print("Fetching all remotes...")
|
previous_version = get_base_version_from_current_branch()
|
||||||
run_git_command("fetch", "--all")
|
else:
|
||||||
print("Successfully fetched all remotes")
|
# Step 2: Fetch all remotes
|
||||||
|
print("Fetching all remotes...")
|
||||||
|
run_git_command("fetch", "--all")
|
||||||
|
print("Successfully fetched all remotes")
|
||||||
|
|
||||||
# Step 3: Find latest version
|
# Step 3: Find latest version
|
||||||
previous_version = get_latest_version()
|
previous_version = get_latest_version()
|
||||||
print(f"Previous version: {previous_version}")
|
print(f"Previous version: {previous_version}")
|
||||||
|
|
||||||
# Step 4: Verify version matches pyproject.toml
|
# Step 4: Verify version matches pyproject.toml
|
||||||
|
|
@ -316,14 +339,27 @@ def main() -> None:
|
||||||
)
|
)
|
||||||
print(f"Version verified: {current_version}")
|
print(f"Version verified: {current_version}")
|
||||||
|
|
||||||
# Step 5: Switch to previous version tag
|
previous_private_tag = f"v{previous_version}-private"
|
||||||
switch_to_tag(previous_version)
|
current_private_tag = f"v{current_version}-private"
|
||||||
|
|
||||||
# Step 6: Create release branch
|
if not args.resume:
|
||||||
create_release_branch(current_version)
|
# Step 5: Switch to previous version tag
|
||||||
|
switch_to_tag(previous_version)
|
||||||
|
|
||||||
# Step 7: Cherry-pick commits
|
# Step 6: Create release branch
|
||||||
cherry_pick_commits(previous_version, current_version, squash)
|
create_release_branch(current_version)
|
||||||
|
|
||||||
|
# Step 7: Cherry-pick commits
|
||||||
|
cherry_pick_commits(previous_private_tag, current_private_tag)
|
||||||
|
|
||||||
|
# Step 8: Squash commits
|
||||||
|
if squash:
|
||||||
|
squash_commits(
|
||||||
|
previous_version,
|
||||||
|
current_version,
|
||||||
|
previous_private_tag,
|
||||||
|
current_private_tag,
|
||||||
|
)
|
||||||
|
|
||||||
# Step 8: Get summary information
|
# Step 8: Get summary information
|
||||||
commits_summary = get_commits_summary(previous_version, current_version)
|
commits_summary = get_commits_summary(previous_version, current_version)
|
||||||
|
|
|
||||||
|
|
@ -19,16 +19,16 @@ class TestCloseSession:
|
||||||
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||||
session = acp_agent_loop.sessions[session_response.session_id]
|
session = acp_agent_loop.sessions[session_response.session_id]
|
||||||
|
|
||||||
backend_close = AsyncMock()
|
backend_aexit = AsyncMock()
|
||||||
telemetry_close = AsyncMock()
|
telemetry_close = AsyncMock()
|
||||||
cast(Any, session.agent_loop.backend).close = backend_close
|
cast(Any, session.agent_loop.backend).__aexit__ = backend_aexit
|
||||||
session.agent_loop.telemetry_client.aclose = telemetry_close
|
session.agent_loop.telemetry_client.aclose = telemetry_close
|
||||||
|
|
||||||
response = await acp_agent_loop.close_session(session_response.session_id)
|
response = await acp_agent_loop.close_session(session_response.session_id)
|
||||||
|
|
||||||
assert response is not None
|
assert response is not None
|
||||||
assert session_response.session_id not in acp_agent_loop.sessions
|
assert session_response.session_id not in acp_agent_loop.sessions
|
||||||
backend_close.assert_awaited_once()
|
backend_aexit.assert_awaited_once_with(None, None, None)
|
||||||
telemetry_close.assert_awaited_once()
|
telemetry_close.assert_awaited_once()
|
||||||
session_closed_events = [
|
session_closed_events = [
|
||||||
event
|
event
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class TestACPInitialize:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert response.agent_info == Implementation(
|
assert response.agent_info == Implementation(
|
||||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.5"
|
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.6"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.auth_methods == []
|
assert response.auth_methods == []
|
||||||
|
|
@ -62,7 +62,7 @@ class TestACPInitialize:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert response.agent_info == Implementation(
|
assert response.agent_info == Implementation(
|
||||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.5"
|
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.6"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.auth_methods is not None
|
assert response.auth_methods is not None
|
||||||
|
|
|
||||||
|
|
@ -432,12 +432,12 @@ class TestMistralRetry:
|
||||||
with patch("vibe.core.llm.backend.mistral.Mistral") as mock_mistral_class:
|
with patch("vibe.core.llm.backend.mistral.Mistral") as mock_mistral_class:
|
||||||
mock_mistral_class.return_value = MagicMock()
|
mock_mistral_class.return_value = MagicMock()
|
||||||
backend._get_client()
|
backend._get_client()
|
||||||
mock_mistral_class.assert_called_once_with(
|
call_kwargs = mock_mistral_class.call_args.kwargs
|
||||||
api_key=backend._api_key,
|
assert call_kwargs["api_key"] == backend._api_key
|
||||||
server_url=backend._server_url,
|
assert call_kwargs["server_url"] == backend._server_url
|
||||||
timeout_ms=720000,
|
assert call_kwargs["timeout_ms"] == 720000
|
||||||
retry_config=backend._retry_config,
|
assert call_kwargs["retry_config"] is backend._retry_config
|
||||||
)
|
assert "async_client" in call_kwargs
|
||||||
|
|
||||||
|
|
||||||
class TestMistralMapperPrepareMessage:
|
class TestMistralMapperPrepareMessage:
|
||||||
|
|
|
||||||
78
tests/core/test_build_ssl_context.py
Normal file
78
tests/core/test_build_ssl_context.py
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ssl
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _clear_ssl_cache():
|
||||||
|
build_ssl_context.cache_clear()
|
||||||
|
yield
|
||||||
|
build_ssl_context.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_ssl_context_returns_ssl_context():
|
||||||
|
ctx = build_ssl_context()
|
||||||
|
assert isinstance(ctx, ssl.SSLContext)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_ssl_context_loads_custom_cert_file(monkeypatch, tmp_path):
|
||||||
|
cert_file = tmp_path / "custom.pem"
|
||||||
|
cert_file.write_text("dummy")
|
||||||
|
monkeypatch.setenv("SSL_CERT_FILE", str(cert_file))
|
||||||
|
monkeypatch.delenv("SSL_CERT_DIR", raising=False)
|
||||||
|
|
||||||
|
mock_ctx = MagicMock(spec=ssl.SSLContext)
|
||||||
|
with patch(
|
||||||
|
"vibe.core.utils.http.ssl.create_default_context", return_value=mock_ctx
|
||||||
|
):
|
||||||
|
build_ssl_context()
|
||||||
|
|
||||||
|
mock_ctx.load_verify_locations.assert_called_once_with(
|
||||||
|
cafile=str(cert_file), capath=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_ssl_context_loads_custom_cert_dir(monkeypatch, tmp_path):
|
||||||
|
cert_dir = tmp_path / "certs"
|
||||||
|
cert_dir.mkdir()
|
||||||
|
monkeypatch.delenv("SSL_CERT_FILE", raising=False)
|
||||||
|
monkeypatch.setenv("SSL_CERT_DIR", str(cert_dir))
|
||||||
|
|
||||||
|
mock_ctx = MagicMock(spec=ssl.SSLContext)
|
||||||
|
with patch(
|
||||||
|
"vibe.core.utils.http.ssl.create_default_context", return_value=mock_ctx
|
||||||
|
):
|
||||||
|
build_ssl_context()
|
||||||
|
|
||||||
|
mock_ctx.load_verify_locations.assert_called_once_with(
|
||||||
|
cafile=None, capath=str(cert_dir)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_ssl_context_warns_on_invalid_cert(monkeypatch, caplog):
|
||||||
|
monkeypatch.setenv("SSL_CERT_FILE", "/nonexistent/path/cert.pem")
|
||||||
|
monkeypatch.delenv("SSL_CERT_DIR", raising=False)
|
||||||
|
|
||||||
|
ctx = build_ssl_context()
|
||||||
|
assert isinstance(ctx, ssl.SSLContext)
|
||||||
|
assert any(
|
||||||
|
"Failed to load custom SSL certificates" in r.message for r in caplog.records
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_ssl_context_no_custom_certs(monkeypatch):
|
||||||
|
monkeypatch.delenv("SSL_CERT_FILE", raising=False)
|
||||||
|
monkeypatch.delenv("SSL_CERT_DIR", raising=False)
|
||||||
|
|
||||||
|
mock_ctx = MagicMock(spec=ssl.SSLContext)
|
||||||
|
with patch(
|
||||||
|
"vibe.core.utils.http.ssl.create_default_context", return_value=mock_ctx
|
||||||
|
):
|
||||||
|
build_ssl_context()
|
||||||
|
|
||||||
|
mock_ctx.load_verify_locations.assert_not_called()
|
||||||
|
|
@ -293,6 +293,68 @@ class TestMigrateLeavesFindInBashAllowlist:
|
||||||
assert "tools" not in result
|
assert "tools" not in result
|
||||||
|
|
||||||
|
|
||||||
|
class TestMigrateStripsBashAllowlistWildcardSuffix:
|
||||||
|
def test_strips_trailing_wildcard_from_bash_allowlist_entries(
|
||||||
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
|
config_file = tmp_path / "config.toml"
|
||||||
|
data = {
|
||||||
|
"tools": {"bash": {"allowlist": ["git commit *", "npm install *", "echo"]}}
|
||||||
|
}
|
||||||
|
with config_file.open("wb") as f:
|
||||||
|
tomli_w.dump(data, f)
|
||||||
|
|
||||||
|
reset_harness_files_manager()
|
||||||
|
init_harness_files_manager("user")
|
||||||
|
VibeConfig._migrate()
|
||||||
|
|
||||||
|
with config_file.open("rb") as f:
|
||||||
|
result = tomllib.load(f)
|
||||||
|
assert result["tools"]["bash"]["allowlist"] == [
|
||||||
|
"echo",
|
||||||
|
"find",
|
||||||
|
"git commit",
|
||||||
|
"npm install",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_dedupes_when_stripping_collides_with_existing_entry(
|
||||||
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
|
config_file = tmp_path / "config.toml"
|
||||||
|
data = {
|
||||||
|
"tools": {"bash": {"allowlist": ["git commit *", "git commit", "find"]}}
|
||||||
|
}
|
||||||
|
with config_file.open("wb") as f:
|
||||||
|
tomli_w.dump(data, f)
|
||||||
|
|
||||||
|
reset_harness_files_manager()
|
||||||
|
init_harness_files_manager("user")
|
||||||
|
VibeConfig._migrate()
|
||||||
|
|
||||||
|
with config_file.open("rb") as f:
|
||||||
|
result = tomllib.load(f)
|
||||||
|
assert result["tools"]["bash"]["allowlist"] == ["find", "git commit"]
|
||||||
|
|
||||||
|
def test_noop_when_no_wildcard_suffix_present(
|
||||||
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
|
config_file = tmp_path / "config.toml"
|
||||||
|
data = {"tools": {"bash": {"allowlist": ["echo", "find", "ls"]}}}
|
||||||
|
with config_file.open("wb") as f:
|
||||||
|
tomli_w.dump(data, f)
|
||||||
|
|
||||||
|
reset_harness_files_manager()
|
||||||
|
init_harness_files_manager("user")
|
||||||
|
VibeConfig._migrate()
|
||||||
|
|
||||||
|
with config_file.open("rb") as f:
|
||||||
|
result = tomllib.load(f)
|
||||||
|
assert result["tools"]["bash"]["allowlist"] == ["echo", "find", "ls"]
|
||||||
|
|
||||||
|
|
||||||
class TestMigrateMistralVibeCliLatestDefaults:
|
class TestMigrateMistralVibeCliLatestDefaults:
|
||||||
def test_updates_alias_temperature_and_thinking_for_default_model(
|
def test_updates_alias_temperature_and_thinking_for_default_model(
|
||||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,6 @@ class FakeTranscribeClient:
|
||||||
) -> AsyncIterator[TranscribeEvent]:
|
) -> AsyncIterator[TranscribeEvent]:
|
||||||
for event in self._events:
|
for event in self._events:
|
||||||
yield event
|
yield event
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
pass
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,9 @@ class FakeVoiceManager:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
def _set_state(self, state: TranscribeState) -> None:
|
def _set_state(self, state: TranscribeState) -> None:
|
||||||
if self._transcribe_state == state:
|
if self._transcribe_state == state:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from tests.stubs.fake_backend import FakeBackend
|
||||||
from vibe.core.agents.manager import AgentManager
|
from vibe.core.agents.manager import AgentManager
|
||||||
from vibe.core.agents.models import (
|
from vibe.core.agents.models import (
|
||||||
BUILTIN_AGENTS,
|
BUILTIN_AGENTS,
|
||||||
|
CHAT,
|
||||||
AgentProfile,
|
AgentProfile,
|
||||||
AgentSafety,
|
AgentSafety,
|
||||||
AgentType,
|
AgentType,
|
||||||
|
|
@ -188,6 +189,49 @@ class TestAgentApplyToConfig:
|
||||||
"exit_plan_mode",
|
"exit_plan_mode",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_base_disabled_tools_are_filtered_from_profile_enabled_tools(self) -> None:
|
||||||
|
base = VibeConfig(
|
||||||
|
include_project_context=False,
|
||||||
|
include_prompt_detail=False,
|
||||||
|
disabled_tools=["ask_user_question"],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = CHAT.apply_to_config(base)
|
||||||
|
|
||||||
|
assert "ask_user_question" not in result.enabled_tools
|
||||||
|
assert "grep" in result.enabled_tools
|
||||||
|
assert "read_file" in result.enabled_tools
|
||||||
|
assert "task" in result.enabled_tools
|
||||||
|
|
||||||
|
def test_base_disabled_tools_filter_supports_glob_patterns(self) -> None:
|
||||||
|
base = VibeConfig(
|
||||||
|
include_project_context=False,
|
||||||
|
include_prompt_detail=False,
|
||||||
|
disabled_tools=["ask_*"],
|
||||||
|
)
|
||||||
|
agent = AgentProfile(
|
||||||
|
name="custom",
|
||||||
|
display_name="Custom",
|
||||||
|
description="",
|
||||||
|
safety=AgentSafety.NEUTRAL,
|
||||||
|
overrides={"enabled_tools": ["grep", "ask_user_question", "ask_extra"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = agent.apply_to_config(base)
|
||||||
|
|
||||||
|
assert result.enabled_tools == ["grep"]
|
||||||
|
|
||||||
|
def test_empty_base_disabled_tools_leaves_enabled_tools_untouched(self) -> None:
|
||||||
|
base = VibeConfig(
|
||||||
|
include_project_context=False,
|
||||||
|
include_prompt_detail=False,
|
||||||
|
disabled_tools=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = CHAT.apply_to_config(base)
|
||||||
|
|
||||||
|
assert "ask_user_question" in result.enabled_tools
|
||||||
|
|
||||||
def test_custom_prompt_found_in_global_when_missing_from_project(
|
def test_custom_prompt_found_in_global_when_missing_from_project(
|
||||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ class TestApproveAlwaysPermanentWithGranularPermissions:
|
||||||
agent.approve_always("bash", perms, save_permanently=True)
|
agent.approve_always("bash", perms, save_permanently=True)
|
||||||
|
|
||||||
persisted = _read_persisted_config(config_dir)
|
persisted = _read_persisted_config(config_dir)
|
||||||
assert persisted["tools"]["bash"]["allowlist"] == ["npm install *"]
|
assert persisted["tools"]["bash"]["allowlist"] == ["npm install"]
|
||||||
|
|
||||||
def test_also_adds_session_rules(self, config_dir: Path):
|
def test_also_adds_session_rules(self, config_dir: Path):
|
||||||
agent = build_test_agent_loop()
|
agent = build_test_agent_loop()
|
||||||
|
|
@ -75,9 +75,7 @@ class TestApproveAlwaysPermanentWithGranularPermissions:
|
||||||
assert "bash" not in persisted.get("tools", {})
|
assert "bash" not in persisted.get("tools", {})
|
||||||
|
|
||||||
def test_does_not_duplicate_existing_allowlist_entries(self, config_dir: Path):
|
def test_does_not_duplicate_existing_allowlist_entries(self, config_dir: Path):
|
||||||
config = build_test_vibe_config(
|
config = build_test_vibe_config(tools={"bash": {"allowlist": ["npm install"]}})
|
||||||
tools={"bash": {"allowlist": ["npm install *"]}}
|
|
||||||
)
|
|
||||||
agent = build_test_agent_loop(config=config)
|
agent = build_test_agent_loop(config=config)
|
||||||
perms = self._make_permissions()
|
perms = self._make_permissions()
|
||||||
|
|
||||||
|
|
@ -88,14 +86,14 @@ class TestApproveAlwaysPermanentWithGranularPermissions:
|
||||||
assert persisted.get("tools", {}).get("bash", {}).get("allowlist") is None
|
assert persisted.get("tools", {}).get("bash", {}).get("allowlist") is None
|
||||||
|
|
||||||
def test_appends_new_patterns_to_existing_allowlist(self, config_dir: Path):
|
def test_appends_new_patterns_to_existing_allowlist(self, config_dir: Path):
|
||||||
config = build_test_vibe_config(tools={"bash": {"allowlist": ["git *"]}})
|
config = build_test_vibe_config(tools={"bash": {"allowlist": ["git"]}})
|
||||||
agent = build_test_agent_loop(config=config)
|
agent = build_test_agent_loop(config=config)
|
||||||
perms = self._make_permissions()
|
perms = self._make_permissions()
|
||||||
|
|
||||||
agent.approve_always("bash", perms, save_permanently=True)
|
agent.approve_always("bash", perms, save_permanently=True)
|
||||||
|
|
||||||
persisted = _read_persisted_config(config_dir)
|
persisted = _read_persisted_config(config_dir)
|
||||||
assert persisted["tools"]["bash"]["allowlist"] == ["git *", "npm install *"]
|
assert persisted["tools"]["bash"]["allowlist"] == ["git", "npm install"]
|
||||||
|
|
||||||
def test_multiple_permissions_persisted(self, config_dir: Path):
|
def test_multiple_permissions_persisted(self, config_dir: Path):
|
||||||
agent = build_test_agent_loop()
|
agent = build_test_agent_loop()
|
||||||
|
|
@ -117,4 +115,4 @@ class TestApproveAlwaysPermanentWithGranularPermissions:
|
||||||
agent.approve_always("bash", perms, save_permanently=True)
|
agent.approve_always("bash", perms, save_permanently=True)
|
||||||
|
|
||||||
persisted = _read_persisted_config(config_dir)
|
persisted = _read_persisted_config(config_dir)
|
||||||
assert persisted["tools"]["bash"]["allowlist"] == ["/tmp/*", "npm install *"]
|
assert persisted["tools"]["bash"]["allowlist"] == ["/tmp/*", "npm install"]
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,35 @@ class TestReadOnlyAgentMiddleware:
|
||||||
PLAN_REMINDER_SNIPPET = "Plan mode is active"
|
PLAN_REMINDER_SNIPPET = "Plan mode is active"
|
||||||
|
|
||||||
|
|
||||||
|
class TestMakePlanAgentReminder:
|
||||||
|
def test_default_includes_both_interactive_tool_instructions(self) -> None:
|
||||||
|
reminder = make_plan_agent_reminder("/tmp/test-plan.md")
|
||||||
|
assert "ask_user_question" in reminder
|
||||||
|
assert "exit_plan_mode" in reminder
|
||||||
|
|
||||||
|
def test_omits_ask_user_question_when_tool_unavailable(self) -> None:
|
||||||
|
reminder = make_plan_agent_reminder(
|
||||||
|
"/tmp/test-plan.md", has_ask_user_question=False
|
||||||
|
)
|
||||||
|
assert "ask_user_question" not in reminder
|
||||||
|
assert "exit_plan_mode" in reminder
|
||||||
|
|
||||||
|
def test_omits_exit_plan_mode_when_tool_unavailable(self) -> None:
|
||||||
|
reminder = make_plan_agent_reminder(
|
||||||
|
"/tmp/test-plan.md", has_exit_plan_mode=False
|
||||||
|
)
|
||||||
|
assert "exit_plan_mode" not in reminder
|
||||||
|
assert "tell them to switch modes" in reminder
|
||||||
|
|
||||||
|
def test_omits_both_when_neither_available(self) -> None:
|
||||||
|
reminder = make_plan_agent_reminder(
|
||||||
|
"/tmp/test-plan.md", has_ask_user_question=False, has_exit_plan_mode=False
|
||||||
|
)
|
||||||
|
assert "ask_user_question" not in reminder
|
||||||
|
assert "exit_plan_mode" not in reminder
|
||||||
|
assert "Plan mode is active" in reminder
|
||||||
|
|
||||||
|
|
||||||
class TestMiddlewarePipelineWithReadOnlyAgent:
|
class TestMiddlewarePipelineWithReadOnlyAgent:
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pipeline_includes_injection(self, ctx: ConversationContext) -> None:
|
async def test_pipeline_includes_injection(self, ctx: ConversationContext) -> None:
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,9 @@ class TestStopRecording:
|
||||||
return
|
return
|
||||||
yield # makes this an async generator
|
yield # makes this an async generator
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
recorder = FakeAudioRecorder()
|
recorder = FakeAudioRecorder()
|
||||||
config = build_test_vibe_config(voice_mode_enabled=True)
|
config = build_test_vibe_config(voice_mode_enabled=True)
|
||||||
manager = VoiceManager(
|
manager = VoiceManager(
|
||||||
|
|
@ -218,6 +221,9 @@ class TestCancelRecording:
|
||||||
return
|
return
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
recorder = FakeAudioRecorder()
|
recorder = FakeAudioRecorder()
|
||||||
config = build_test_vibe_config(voice_mode_enabled=True)
|
config = build_test_vibe_config(voice_mode_enabled=True)
|
||||||
manager = VoiceManager(
|
manager = VoiceManager(
|
||||||
|
|
@ -388,6 +394,9 @@ class TestTranscription:
|
||||||
raise RuntimeError("network error")
|
raise RuntimeError("network error")
|
||||||
yield # makes this an async generator
|
yield # makes this an async generator
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
recorder = FakeAudioRecorder()
|
recorder = FakeAudioRecorder()
|
||||||
config = build_test_vibe_config(voice_mode_enabled=True)
|
config = build_test_vibe_config(voice_mode_enabled=True)
|
||||||
manager = VoiceManager(
|
manager = VoiceManager(
|
||||||
|
|
@ -488,6 +497,9 @@ class TestTelemetryTracking:
|
||||||
raise RuntimeError("network error")
|
raise RuntimeError("network error")
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
recorder = FakeAudioRecorder()
|
recorder = FakeAudioRecorder()
|
||||||
config = build_test_vibe_config(voice_mode_enabled=True)
|
config = build_test_vibe_config(voice_mode_enabled=True)
|
||||||
mock_telemetry = MagicMock()
|
mock_telemetry = MagicMock()
|
||||||
|
|
@ -556,6 +568,9 @@ class TestTelemetryTracking:
|
||||||
return
|
return
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
recorder = FakeAudioRecorder()
|
recorder = FakeAudioRecorder()
|
||||||
config = build_test_vibe_config(voice_mode_enabled=True)
|
config = build_test_vibe_config(voice_mode_enabled=True)
|
||||||
mock_telemetry = MagicMock()
|
mock_telemetry = MagicMock()
|
||||||
|
|
|
||||||
6
uv.lock
generated
6
uv.lock
generated
|
|
@ -3,7 +3,7 @@ revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
exclude-newer = "2026-04-29T14:28:22.645142Z"
|
exclude-newer = "2026-05-04T08:14:44.283731Z"
|
||||||
exclude-newer-span = "P7D"
|
exclude-newer-span = "P7D"
|
||||||
|
|
||||||
[options.exclude-newer-package]
|
[options.exclude-newer-package]
|
||||||
|
|
@ -820,12 +820,13 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mistral-vibe"
|
name = "mistral-vibe"
|
||||||
version = "2.9.5"
|
version = "2.9.6"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "agent-client-protocol" },
|
{ name = "agent-client-protocol" },
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
{ name = "cachetools" },
|
{ name = "cachetools" },
|
||||||
|
{ name = "certifi" },
|
||||||
{ name = "charset-normalizer" },
|
{ name = "charset-normalizer" },
|
||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
{ name = "gitpython" },
|
{ name = "gitpython" },
|
||||||
|
|
@ -888,6 +889,7 @@ requires-dist = [
|
||||||
{ name = "agent-client-protocol", specifier = "==0.9.0" },
|
{ name = "agent-client-protocol", specifier = "==0.9.0" },
|
||||||
{ name = "anyio", specifier = ">=4.12.0" },
|
{ name = "anyio", specifier = ">=4.12.0" },
|
||||||
{ name = "cachetools", specifier = ">=5.5.0" },
|
{ name = "cachetools", specifier = ">=5.5.0" },
|
||||||
|
{ name = "certifi", specifier = ">=2026.4.22" },
|
||||||
{ name = "charset-normalizer", specifier = ">=3.4.4" },
|
{ name = "charset-normalizer", specifier = ">=3.4.4" },
|
||||||
{ name = "cryptography", specifier = ">=44.0.0" },
|
{ name = "cryptography", specifier = ">=44.0.0" },
|
||||||
{ name = "gitpython", specifier = ">=3.1.46" },
|
{ name = "gitpython", specifier = ">=3.1.46" },
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
VIBE_ROOT = Path(__file__).parent
|
VIBE_ROOT = Path(__file__).parent
|
||||||
__version__ = "2.9.5"
|
__version__ = "2.9.6"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import AsyncGenerator, Callable
|
from collections.abc import AsyncGenerator, Callable
|
||||||
from contextlib import aclosing
|
from contextlib import aclosing
|
||||||
import inspect
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -86,6 +85,7 @@ from vibe.acp.exceptions import (
|
||||||
from vibe.acp.session import AcpSessionLoop
|
from vibe.acp.session import AcpSessionLoop
|
||||||
from vibe.acp.tools.base import BaseAcpTool
|
from vibe.acp.tools.base import BaseAcpTool
|
||||||
from vibe.acp.tools.session_update import (
|
from vibe.acp.tools.session_update import (
|
||||||
|
resolve_kind,
|
||||||
tool_call_session_update,
|
tool_call_session_update,
|
||||||
tool_result_session_update,
|
tool_result_session_update,
|
||||||
)
|
)
|
||||||
|
|
@ -158,6 +158,8 @@ from vibe.core.utils import (
|
||||||
|
|
||||||
logger = logging.getLogger("vibe")
|
logger = logging.getLogger("vibe")
|
||||||
|
|
||||||
|
NON_INTERACTIVE_DISABLED_TOOLS = ["ask_user_question", "exit_plan_mode"]
|
||||||
|
|
||||||
|
|
||||||
class ForkSessionParams(BaseModel):
|
class ForkSessionParams(BaseModel):
|
||||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
@ -303,7 +305,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||||
|
|
||||||
def _load_config(self) -> VibeConfig:
|
def _load_config(self) -> VibeConfig:
|
||||||
try:
|
try:
|
||||||
config = VibeConfig.load(disabled_tools=["ask_user_question"])
|
config = VibeConfig.load(disabled_tools=NON_INTERACTIVE_DISABLED_TOOLS)
|
||||||
config.tool_paths.extend(self._get_acp_tool_overrides())
|
config.tool_paths.extend(self._get_acp_tool_overrides())
|
||||||
return config
|
return config
|
||||||
except MissingAPIKeyError as e:
|
except MissingAPIKeyError as e:
|
||||||
|
|
@ -695,7 +697,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||||
async def _reload_config(self, session: AcpSessionLoop) -> None:
|
async def _reload_config(self, session: AcpSessionLoop) -> None:
|
||||||
new_config = VibeConfig.load(
|
new_config = VibeConfig.load(
|
||||||
tool_paths=session.agent_loop.config.tool_paths,
|
tool_paths=session.agent_loop.config.tool_paths,
|
||||||
disabled_tools=["ask_user_question"],
|
disabled_tools=NON_INTERACTIVE_DISABLED_TOOLS,
|
||||||
)
|
)
|
||||||
await session.agent_loop.reload_with_initial_messages(base_config=new_config)
|
await session.agent_loop.reload_with_initial_messages(base_config=new_config)
|
||||||
|
|
||||||
|
|
@ -969,6 +971,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||||
yield ToolCallProgress(
|
yield ToolCallProgress(
|
||||||
session_update="tool_call_update",
|
session_update="tool_call_update",
|
||||||
tool_call_id=event.tool_call_id,
|
tool_call_id=event.tool_call_id,
|
||||||
|
kind=resolve_kind(event.tool_name),
|
||||||
content=[
|
content=[
|
||||||
ContentToolCallContent(
|
ContentToolCallContent(
|
||||||
type="content",
|
type="content",
|
||||||
|
|
@ -977,6 +980,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
field_meta={"tool_name": event.tool_name},
|
||||||
)
|
)
|
||||||
|
|
||||||
elif isinstance(event, CompactStartEvent):
|
elif isinstance(event, CompactStartEvent):
|
||||||
|
|
@ -1016,12 +1020,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||||
if deferred_init_thread is not None and deferred_init_thread.is_alive():
|
if deferred_init_thread is not None and deferred_init_thread.is_alive():
|
||||||
await asyncio.to_thread(deferred_init_thread.join)
|
await asyncio.to_thread(deferred_init_thread.join)
|
||||||
|
|
||||||
backend_close = getattr(agent_loop.backend, "close", None)
|
await agent_loop.aclose()
|
||||||
if callable(backend_close):
|
|
||||||
close_result = backend_close()
|
|
||||||
if inspect.isawaitable(close_result):
|
|
||||||
await close_result
|
|
||||||
|
|
||||||
await agent_loop.telemetry_client.aclose()
|
await agent_loop.telemetry_client.aclose()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -1295,7 +1294,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||||
"""Reload config from disk and reinitialize the agent loop."""
|
"""Reload config from disk and reinitialize the agent loop."""
|
||||||
new_config = VibeConfig.load(
|
new_config = VibeConfig.load(
|
||||||
tool_paths=session.agent_loop.config.tool_paths,
|
tool_paths=session.agent_loop.config.tool_paths,
|
||||||
disabled_tools=["ask_user_question"],
|
disabled_tools=NON_INTERACTIVE_DISABLED_TOOLS,
|
||||||
)
|
)
|
||||||
await session.agent_loop.reload_with_initial_messages(base_config=new_config)
|
await session.agent_loop.reload_with_initial_messages(base_config=new_config)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,17 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from typing import Annotated, Protocol, cast, runtime_checkable
|
from typing import Annotated, cast
|
||||||
|
|
||||||
from acp import Client
|
from acp import Client
|
||||||
from acp.helpers import SessionUpdate, ToolCallContentVariant
|
from acp.helpers import ToolCallContentVariant
|
||||||
from acp.schema import ToolCallProgress
|
from acp.schema import ToolCallProgress
|
||||||
from pydantic import BaseModel, ConfigDict, Field, SkipValidation
|
from pydantic import BaseModel, ConfigDict, Field, SkipValidation
|
||||||
|
|
||||||
|
from vibe.acp.tools.session_update import resolve_kind
|
||||||
from vibe.core.logger import logger
|
from vibe.core.logger import logger
|
||||||
from vibe.core.tools.base import BaseTool, ToolError
|
from vibe.core.tools.base import BaseTool, ToolError
|
||||||
from vibe.core.tools.manager import ToolManager
|
from vibe.core.tools.manager import ToolManager
|
||||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
|
||||||
class ToolCallSessionUpdateProtocol(Protocol):
|
|
||||||
@classmethod
|
|
||||||
def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
|
||||||
class ToolResultSessionUpdateProtocol(Protocol):
|
|
||||||
@classmethod
|
|
||||||
def tool_result_session_update(
|
|
||||||
cls, event: ToolResultEvent
|
|
||||||
) -> SessionUpdate | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
class AcpToolState(BaseModel):
|
class AcpToolState(BaseModel):
|
||||||
|
|
@ -86,6 +72,7 @@ class BaseAcpTool[ToolState: AcpToolState](BaseTool):
|
||||||
if tool_call_id is None:
|
if tool_call_id is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
tool_name = self.get_name()
|
||||||
try:
|
try:
|
||||||
await client.session_update(
|
await client.session_update(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
|
|
@ -93,7 +80,9 @@ class BaseAcpTool[ToolState: AcpToolState](BaseTool):
|
||||||
session_update="tool_call_update",
|
session_update="tool_call_update",
|
||||||
tool_call_id=tool_call_id,
|
tool_call_id=tool_call_id,
|
||||||
status="in_progress",
|
status="in_progress",
|
||||||
|
kind=resolve_kind(tool_name),
|
||||||
content=content,
|
content=content,
|
||||||
|
field_meta={"tool_name": tool_name},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,9 @@ from acp.schema import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from vibe import VIBE_ROOT
|
from vibe import VIBE_ROOT
|
||||||
from vibe.acp.tools.base import (
|
from vibe.acp.tools.session_update import (
|
||||||
ToolCallSessionUpdateProtocol,
|
ToolCallSessionUpdateProtocol,
|
||||||
ToolResultSessionUpdateProtocol,
|
ToolResultSessionUpdateProtocol,
|
||||||
)
|
|
||||||
from vibe.acp.tools.session_update import (
|
|
||||||
failed_tool_result,
|
failed_tool_result,
|
||||||
fallback_tool_call,
|
fallback_tool_call,
|
||||||
resolve_kind,
|
resolve_kind,
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,10 @@ from acp.schema import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from vibe import VIBE_ROOT
|
from vibe import VIBE_ROOT
|
||||||
from vibe.acp.tools.base import (
|
from vibe.acp.tools.base import AcpToolState, BaseAcpTool
|
||||||
AcpToolState,
|
from vibe.acp.tools.session_update import (
|
||||||
BaseAcpTool,
|
|
||||||
ToolCallSessionUpdateProtocol,
|
ToolCallSessionUpdateProtocol,
|
||||||
ToolResultSessionUpdateProtocol,
|
ToolResultSessionUpdateProtocol,
|
||||||
)
|
|
||||||
from vibe.acp.tools.session_update import (
|
|
||||||
failed_tool_result,
|
failed_tool_result,
|
||||||
fallback_tool_call,
|
fallback_tool_call,
|
||||||
resolve_kind,
|
resolve_kind,
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,9 @@ from acp.schema import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from vibe import VIBE_ROOT
|
from vibe import VIBE_ROOT
|
||||||
from vibe.acp.tools.base import (
|
from vibe.acp.tools.session_update import (
|
||||||
ToolCallSessionUpdateProtocol,
|
ToolCallSessionUpdateProtocol,
|
||||||
ToolResultSessionUpdateProtocol,
|
ToolResultSessionUpdateProtocol,
|
||||||
)
|
|
||||||
from vibe.acp.tools.session_update import (
|
|
||||||
failed_tool_result,
|
failed_tool_result,
|
||||||
fallback_tool_call,
|
fallback_tool_call,
|
||||||
resolve_kind,
|
resolve_kind,
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,9 @@ from acp.schema import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from vibe import VIBE_ROOT
|
from vibe import VIBE_ROOT
|
||||||
from vibe.acp.tools.base import (
|
from vibe.acp.tools.session_update import (
|
||||||
ToolCallSessionUpdateProtocol,
|
ToolCallSessionUpdateProtocol,
|
||||||
ToolResultSessionUpdateProtocol,
|
ToolResultSessionUpdateProtocol,
|
||||||
)
|
|
||||||
from vibe.acp.tools.session_update import (
|
|
||||||
failed_tool_result,
|
failed_tool_result,
|
||||||
fallback_tool_call,
|
fallback_tool_call,
|
||||||
resolve_kind,
|
resolve_kind,
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,9 @@ from acp.schema import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from vibe import VIBE_ROOT
|
from vibe import VIBE_ROOT
|
||||||
from vibe.acp.tools.base import (
|
from vibe.acp.tools.session_update import (
|
||||||
ToolCallSessionUpdateProtocol,
|
ToolCallSessionUpdateProtocol,
|
||||||
ToolResultSessionUpdateProtocol,
|
ToolResultSessionUpdateProtocol,
|
||||||
)
|
|
||||||
from vibe.acp.tools.session_update import (
|
|
||||||
failed_tool_result,
|
failed_tool_result,
|
||||||
fallback_tool_call,
|
fallback_tool_call,
|
||||||
resolve_kind,
|
resolve_kind,
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,9 @@ from acp.schema import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from vibe import VIBE_ROOT
|
from vibe import VIBE_ROOT
|
||||||
from vibe.acp.tools.base import (
|
from vibe.acp.tools.session_update import (
|
||||||
ToolCallSessionUpdateProtocol,
|
ToolCallSessionUpdateProtocol,
|
||||||
ToolResultSessionUpdateProtocol,
|
ToolResultSessionUpdateProtocol,
|
||||||
)
|
|
||||||
from vibe.acp.tools.session_update import (
|
|
||||||
failed_tool_result,
|
failed_tool_result,
|
||||||
fallback_tool_call,
|
fallback_tool_call,
|
||||||
resolve_kind,
|
resolve_kind,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
from acp.helpers import SessionUpdate, ToolCallContentVariant
|
from acp.helpers import SessionUpdate, ToolCallContentVariant
|
||||||
from acp.schema import (
|
from acp.schema import (
|
||||||
ContentToolCallContent,
|
ContentToolCallContent,
|
||||||
|
|
@ -10,15 +12,25 @@ from acp.schema import (
|
||||||
)
|
)
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from vibe.acp.tools.base import (
|
|
||||||
ToolCallSessionUpdateProtocol,
|
|
||||||
ToolResultSessionUpdateProtocol,
|
|
||||||
)
|
|
||||||
from vibe.core.tools.ui import ToolUIDataAdapter
|
from vibe.core.tools.ui import ToolUIDataAdapter
|
||||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||||
from vibe.core.utils import TaggedText, is_user_cancellation_event
|
from vibe.core.utils import TaggedText, is_user_cancellation_event
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class ToolCallSessionUpdateProtocol(Protocol):
|
||||||
|
@classmethod
|
||||||
|
def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class ToolResultSessionUpdateProtocol(Protocol):
|
||||||
|
@classmethod
|
||||||
|
def tool_result_session_update(
|
||||||
|
cls, event: ToolResultEvent
|
||||||
|
) -> SessionUpdate | None: ...
|
||||||
|
|
||||||
|
|
||||||
def _cancellation_raw_output(event: ToolResultEvent) -> str | None:
|
def _cancellation_raw_output(event: ToolResultEvent) -> str | None:
|
||||||
if event.skip_reason:
|
if event.skip_reason:
|
||||||
return TaggedText.from_string(event.skip_reason).message
|
return TaggedText.from_string(event.skip_reason).message
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,11 @@ def run_cli(args: argparse.Namespace) -> None:
|
||||||
stdin_prompt = get_prompt_from_stdin()
|
stdin_prompt = get_prompt_from_stdin()
|
||||||
if args.prompt is not None:
|
if args.prompt is not None:
|
||||||
warn_if_workdir_trust_is_unset()
|
warn_if_workdir_trust_is_unset()
|
||||||
config.disabled_tools = [*config.disabled_tools, "ask_user_question"]
|
config.disabled_tools = [
|
||||||
|
*config.disabled_tools,
|
||||||
|
"ask_user_question",
|
||||||
|
"exit_plan_mode",
|
||||||
|
]
|
||||||
programmatic_prompt = args.prompt or stdin_prompt
|
programmatic_prompt = args.prompt or stdin_prompt
|
||||||
if not programmatic_prompt:
|
if not programmatic_prompt:
|
||||||
print(
|
print(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from vibe.cli.plan_offer.ports.whoami_gateway import (
|
||||||
WhoAmIGatewayUnauthorized,
|
WhoAmIGatewayUnauthorized,
|
||||||
WhoAmIResponse,
|
WhoAmIResponse,
|
||||||
)
|
)
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
BASE_URL = "https://console.mistral.ai"
|
BASE_URL = "https://console.mistral.ai"
|
||||||
WHOAMI_PATH = "/api/vibe/whoami"
|
WHOAMI_PATH = "/api/vibe/whoami"
|
||||||
|
|
@ -23,7 +24,7 @@ class HttpWhoAmIGateway:
|
||||||
url = f"{self._base_url}{WHOAMI_PATH}"
|
url = f"{self._base_url}{WHOAMI_PATH}"
|
||||||
headers = {"Authorization": f"Bearer {api_key}"}
|
headers = {"Authorization": f"Bearer {api_key}"}
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient(verify=build_ssl_context()) as client:
|
||||||
response = await client.get(url, headers=headers)
|
response = await client.get(url, headers=headers)
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
raise WhoAmIGatewayError() from exc
|
raise WhoAmIGatewayError() from exc
|
||||||
|
|
|
||||||
|
|
@ -987,6 +987,8 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
|
|
||||||
bash_msg = BashOutputMessage(command, str(Path.cwd()), pending=True)
|
bash_msg = BashOutputMessage(command, str(Path.cwd()), pending=True)
|
||||||
await self._mount_and_scroll(bash_msg)
|
await self._mount_and_scroll(bash_msg)
|
||||||
|
await self._ensure_loading_widget("Running command")
|
||||||
|
bash_loading_widget = self._loading_widget
|
||||||
|
|
||||||
proc: asyncio.subprocess.Process | None = None
|
proc: asyncio.subprocess.Process | None = None
|
||||||
stdout_parts: list[str] = []
|
stdout_parts: list[str] = []
|
||||||
|
|
@ -1071,6 +1073,9 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
status=f"failed before completion: {e}",
|
status=f"failed before completion: {e}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
finally:
|
||||||
|
if self._loading_widget is bash_loading_widget:
|
||||||
|
await self._remove_loading_widget()
|
||||||
|
|
||||||
def _get_bash_max_output_bytes(self) -> int:
|
def _get_bash_max_output_bytes(self) -> int:
|
||||||
from vibe.core.tools.builtins.bash import BashToolConfig
|
from vibe.core.tools.builtins.bash import BashToolConfig
|
||||||
|
|
@ -2132,7 +2137,9 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
self._emit_session_closed_for_active_session()
|
self._emit_session_closed_for_active_session()
|
||||||
await self._loop_runner.stop()
|
await self._loop_runner.stop()
|
||||||
self._log_reader.shutdown()
|
self._log_reader.shutdown()
|
||||||
|
await self._voice_manager.close()
|
||||||
await self._narrator_manager.close()
|
await self._narrator_manager.close()
|
||||||
|
await self.agent_loop.aclose()
|
||||||
try:
|
try:
|
||||||
await self.agent_loop.telemetry_client.aclose()
|
await self.agent_loop.telemetry_client.aclose()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
@ -2629,6 +2636,9 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
return True
|
return True
|
||||||
|
|
||||||
interrupted = False
|
interrupted = False
|
||||||
|
if self._bash_task and not self._bash_task.done():
|
||||||
|
self._bash_task.cancel()
|
||||||
|
interrupted = True
|
||||||
if self._agent_running:
|
if self._agent_running:
|
||||||
self._handle_agent_running_escape()
|
self._handle_agent_running_escape()
|
||||||
interrupted = True
|
interrupted = True
|
||||||
|
|
@ -2815,6 +2825,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
|
|
||||||
self._log_reader.shutdown()
|
self._log_reader.shutdown()
|
||||||
self._narrator_manager.cancel()
|
self._narrator_manager.cancel()
|
||||||
|
await self.agent_loop.aclose()
|
||||||
try:
|
try:
|
||||||
await self.agent_loop.telemetry_client.aclose()
|
await self.agent_loop.telemetry_client.aclose()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
|
||||||
|
|
@ -406,6 +406,10 @@ Markdown {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
|
&.bash-pending {
|
||||||
|
color: $foreground;
|
||||||
|
}
|
||||||
|
|
||||||
&.bash-success {
|
&.bash-success {
|
||||||
color: ansi_green;
|
color: ansi_green;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,9 @@ class InterruptMessage(Static):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BashOutputMessage(Static):
|
class BashOutputMessage(SpinnerMixin, Static):
|
||||||
|
SPINNER_TYPE = SpinnerType.PULSE
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
command: str,
|
command: str,
|
||||||
|
|
@ -274,6 +276,7 @@ class BashOutputMessage(Static):
|
||||||
pending: bool = False,
|
pending: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.init_spinner()
|
||||||
self.add_class("bash-output-message")
|
self.add_class("bash-output-message")
|
||||||
self._command = command
|
self._command = command
|
||||||
self._cwd = cwd
|
self._cwd = cwd
|
||||||
|
|
@ -283,17 +286,29 @@ class BashOutputMessage(Static):
|
||||||
self._output_widget: NoMarkupStatic | None = None
|
self._output_widget: NoMarkupStatic | None = None
|
||||||
self._output_container: Horizontal | None = None
|
self._output_container: Horizontal | None = None
|
||||||
self._prompt_widget: NonSelectableStatic | None = None
|
self._prompt_widget: NonSelectableStatic | None = None
|
||||||
|
self._indicator_widget: Static | None = None
|
||||||
|
|
||||||
|
def _update_spinner_frame(self) -> None:
|
||||||
|
if not self._is_spinning or not self._prompt_widget:
|
||||||
|
return
|
||||||
|
self._prompt_widget.update(f"{self._spinner.next_frame()} ")
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
if self._pending:
|
||||||
|
self.start_spinner_timer()
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
status_class = (
|
if self._pending:
|
||||||
"bash-error"
|
status_class = "bash-pending"
|
||||||
if not self._pending and self._exit_code != 0
|
elif self._exit_code != 0:
|
||||||
else "bash-success"
|
status_class = "bash-error"
|
||||||
)
|
else:
|
||||||
|
status_class = "bash-success"
|
||||||
self.add_class(status_class)
|
self.add_class(status_class)
|
||||||
|
prompt_text = f"{self._spinner.current_frame()} " if self._pending else "$ "
|
||||||
with Horizontal(classes="bash-command-line"):
|
with Horizontal(classes="bash-command-line"):
|
||||||
self._prompt_widget = NonSelectableStatic(
|
self._prompt_widget = NonSelectableStatic(
|
||||||
"$ ", classes=f"bash-prompt {status_class}"
|
prompt_text, classes=f"bash-prompt {status_class}"
|
||||||
)
|
)
|
||||||
yield self._prompt_widget
|
yield self._prompt_widget
|
||||||
yield NoMarkupStatic(self._command, classes="bash-command")
|
yield NoMarkupStatic(self._command, classes="bash-command")
|
||||||
|
|
@ -326,18 +341,20 @@ class BashOutputMessage(Static):
|
||||||
async def finish(self, exit_code: int, *, interrupted: bool = False) -> None:
|
async def finish(self, exit_code: int, *, interrupted: bool = False) -> None:
|
||||||
self._exit_code = exit_code
|
self._exit_code = exit_code
|
||||||
self._pending = False
|
self._pending = False
|
||||||
|
self.stop_spinning()
|
||||||
|
if self._prompt_widget:
|
||||||
|
self._prompt_widget.update("$ ")
|
||||||
if interrupted:
|
if interrupted:
|
||||||
self.remove_class("bash-success")
|
new_class = "bash-interrupted"
|
||||||
self.add_class("bash-interrupted")
|
|
||||||
if self._prompt_widget:
|
|
||||||
self._prompt_widget.remove_class("bash-success")
|
|
||||||
self._prompt_widget.add_class("bash-interrupted")
|
|
||||||
elif exit_code != 0:
|
elif exit_code != 0:
|
||||||
self.remove_class("bash-success")
|
new_class = "bash-error"
|
||||||
self.add_class("bash-error")
|
else:
|
||||||
if self._prompt_widget:
|
new_class = "bash-success"
|
||||||
self._prompt_widget.remove_class("bash-success")
|
self.remove_class("bash-pending")
|
||||||
self._prompt_widget.add_class("bash-error")
|
self.add_class(new_class)
|
||||||
|
if self._prompt_widget:
|
||||||
|
self._prompt_widget.remove_class("bash-pending")
|
||||||
|
self._prompt_widget.add_class(new_class)
|
||||||
if interrupted:
|
if interrupted:
|
||||||
suffix = (
|
suffix = (
|
||||||
"\n(interrupted)"
|
"\n(interrupted)"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from vibe.cli.update_notifier.ports.update_gateway import (
|
||||||
UpdateGatewayCause,
|
UpdateGatewayCause,
|
||||||
UpdateGatewayError,
|
UpdateGatewayError,
|
||||||
)
|
)
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
|
|
||||||
class GitHubUpdateGateway(UpdateGateway):
|
class GitHubUpdateGateway(UpdateGateway):
|
||||||
|
|
@ -47,7 +48,9 @@ class GitHubUpdateGateway(UpdateGateway):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
base_url=self._base_url, timeout=self._timeout
|
base_url=self._base_url,
|
||||||
|
timeout=self._timeout,
|
||||||
|
verify=build_ssl_context(),
|
||||||
) as client:
|
) as client:
|
||||||
response = await client.get(request_path, headers=headers)
|
response = await client.get(request_path, headers=headers)
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from vibe.cli.update_notifier.ports.update_gateway import (
|
||||||
UpdateGatewayCause,
|
UpdateGatewayCause,
|
||||||
UpdateGatewayError,
|
UpdateGatewayError,
|
||||||
)
|
)
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
_STATUS_CAUSES: dict[int, UpdateGatewayCause] = {
|
_STATUS_CAUSES: dict[int, UpdateGatewayCause] = {
|
||||||
httpx.codes.NOT_FOUND: UpdateGatewayCause.NOT_FOUND,
|
httpx.codes.NOT_FOUND: UpdateGatewayCause.NOT_FOUND,
|
||||||
|
|
@ -81,7 +82,9 @@ class PyPIUpdateGateway(UpdateGateway):
|
||||||
)
|
)
|
||||||
|
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
base_url=self._base_url, timeout=self._timeout
|
base_url=self._base_url,
|
||||||
|
timeout=self._timeout,
|
||||||
|
verify=build_ssl_context(),
|
||||||
) as client:
|
) as client:
|
||||||
return await client.get(request_path, headers=headers)
|
return await client.get(request_path, headers=headers)
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from asyncio import CancelledError, create_task, wait_for
|
from asyncio import CancelledError, create_task, wait_for
|
||||||
|
import contextlib
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from vibe.cli.voice_manager.telemetry import TranscriptionTrackingState
|
from vibe.cli.voice_manager.telemetry import TranscriptionTrackingState
|
||||||
|
|
@ -154,6 +155,15 @@ class VoiceManager:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
transcribe_task = self._transcribe_task
|
||||||
|
self.cancel_recording()
|
||||||
|
if transcribe_task is not None:
|
||||||
|
with contextlib.suppress(CancelledError):
|
||||||
|
await transcribe_task
|
||||||
|
if self._transcribe_client is not None:
|
||||||
|
await self._transcribe_client.close()
|
||||||
|
|
||||||
async def _run_transcription(self) -> None:
|
async def _run_transcription(self) -> None:
|
||||||
if self._transcribe_client is None:
|
if self._transcribe_client is None:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -54,3 +54,5 @@ class VoiceManagerPort(Protocol):
|
||||||
def add_listener(self, listener: VoiceManagerListener) -> None: ...
|
def add_listener(self, listener: VoiceManagerListener) -> None: ...
|
||||||
|
|
||||||
def remove_listener(self, listener: VoiceManagerListener) -> None: ...
|
def remove_listener(self, listener: VoiceManagerListener) -> None: ...
|
||||||
|
|
||||||
|
async def close(self) -> None: ...
|
||||||
|
|
|
||||||
|
|
@ -519,6 +519,10 @@ class AgentLoop:
|
||||||
def emit_session_closed_telemetry(self) -> None:
|
def emit_session_closed_telemetry(self) -> None:
|
||||||
self.telemetry_client.send_session_closed()
|
self.telemetry_client.send_session_closed()
|
||||||
|
|
||||||
|
async def aclose(self) -> None:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await self.backend.__aexit__(None, None, None)
|
||||||
|
|
||||||
def _create_connector_registry(self) -> ConnectorRegistry | None:
|
def _create_connector_registry(self) -> ConnectorRegistry | None:
|
||||||
if not connectors_enabled():
|
if not connectors_enabled():
|
||||||
return None
|
return None
|
||||||
|
|
@ -671,7 +675,13 @@ class AgentLoop:
|
||||||
ReadOnlyAgentMiddleware(
|
ReadOnlyAgentMiddleware(
|
||||||
lambda: self.agent_profile,
|
lambda: self.agent_profile,
|
||||||
BuiltinAgentName.PLAN,
|
BuiltinAgentName.PLAN,
|
||||||
lambda: make_plan_agent_reminder(self._plan_session.plan_file_path_str),
|
lambda: make_plan_agent_reminder(
|
||||||
|
self._plan_session.plan_file_path_str,
|
||||||
|
has_ask_user_question="ask_user_question"
|
||||||
|
in self.tool_manager.available_tools,
|
||||||
|
has_exit_plan_mode="exit_plan_mode"
|
||||||
|
in self.tool_manager.available_tools,
|
||||||
|
),
|
||||||
PLAN_AGENT_EXIT,
|
PLAN_AGENT_EXIT,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1661,7 +1671,12 @@ class AgentLoop:
|
||||||
self._base_config = base_config
|
self._base_config = base_config
|
||||||
self.agent_manager.invalidate_config()
|
self.agent_manager.invalidate_config()
|
||||||
|
|
||||||
self.backend = self.backend_factory()
|
old_backend = self.backend
|
||||||
|
new_backend = self.backend_factory()
|
||||||
|
self.backend = new_backend
|
||||||
|
if new_backend is not old_backend:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await old_backend.__aexit__(None, None, None)
|
||||||
|
|
||||||
if max_turns is not None:
|
if max_turns is not None:
|
||||||
self._max_turns = max_turns
|
self._max_turns = max_turns
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import tomllib
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from vibe.core.paths import PLANS_DIR
|
from vibe.core.paths import PLANS_DIR
|
||||||
|
from vibe.core.utils import name_matches
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from vibe.core.config import VibeConfig
|
from vibe.core.config import VibeConfig
|
||||||
|
|
@ -68,6 +69,15 @@ class AgentProfile:
|
||||||
*merged.get("disabled_tools", []),
|
*merged.get("disabled_tools", []),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Environment-level disables (set by ACP/programmatic mode) must take
|
||||||
|
# precedence over an agent's enabled_tools allowlist
|
||||||
|
if base.disabled_tools and merged.get("enabled_tools"):
|
||||||
|
merged["enabled_tools"] = [
|
||||||
|
t
|
||||||
|
for t in merged["enabled_tools"]
|
||||||
|
if not name_matches(t, base.disabled_tools)
|
||||||
|
]
|
||||||
|
|
||||||
return VC.model_validate(merged)
|
return VC.model_validate(merged)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import httpx
|
||||||
import keyring
|
import keyring
|
||||||
import keyring.errors
|
import keyring.errors
|
||||||
|
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
GITHUB_CLIENT_ID = "Ov23liJ7sk5kFDMEyvDT"
|
GITHUB_CLIENT_ID = "Ov23liJ7sk5kFDMEyvDT"
|
||||||
|
|
||||||
_SERVICE_NAME = "vibe"
|
_SERVICE_NAME = "vibe"
|
||||||
|
|
@ -51,7 +53,9 @@ class GitHubAuthProvider:
|
||||||
|
|
||||||
async def __aenter__(self) -> GitHubAuthProvider:
|
async def __aenter__(self) -> GitHubAuthProvider:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
|
self._client = httpx.AsyncClient(
|
||||||
|
timeout=httpx.Timeout(self._timeout), verify=build_ssl_context()
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(
|
async def __aexit__(
|
||||||
|
|
@ -66,7 +70,9 @@ class GitHubAuthProvider:
|
||||||
|
|
||||||
def _get_client(self) -> httpx.AsyncClient:
|
def _get_client(self) -> httpx.AsyncClient:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
|
self._client = httpx.AsyncClient(
|
||||||
|
timeout=httpx.Timeout(self._timeout), verify=build_ssl_context()
|
||||||
|
)
|
||||||
self._owns_client = True
|
self._owns_client = True
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,12 @@ from vibe.core.utils import get_server_url_from_api_base
|
||||||
from vibe.core.utils.io import read_safe
|
from vibe.core.utils.io import read_safe
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_bash_pattern_wildcard(pattern: str) -> str:
|
||||||
|
if pattern.endswith(" *"):
|
||||||
|
return pattern[:-2]
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
|
||||||
def deep_update(
|
def deep_update(
|
||||||
mapping: dict[str, Any], updating_mapping: dict[str, Any]
|
mapping: dict[str, Any], updating_mapping: dict[str, Any]
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
|
@ -946,6 +952,8 @@ class VibeConfig(BaseSettings):
|
||||||
type(self).save_updates({"models": models})
|
type(self).save_updates({"models": models})
|
||||||
|
|
||||||
def add_tool_allowlist_patterns(self, tool_name: str, patterns: list[str]) -> None:
|
def add_tool_allowlist_patterns(self, tool_name: str, patterns: list[str]) -> None:
|
||||||
|
if tool_name == "bash":
|
||||||
|
patterns = [_strip_bash_pattern_wildcard(p) for p in patterns]
|
||||||
current_allowlist: list[str] = list(
|
current_allowlist: list[str] = list(
|
||||||
self.tools.get(tool_name, {}).get("allowlist", [])
|
self.tools.get(tool_name, {}).get("allowlist", [])
|
||||||
)
|
)
|
||||||
|
|
@ -1005,6 +1013,12 @@ class VibeConfig(BaseSettings):
|
||||||
allowlist.sort()
|
allowlist.sort()
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
|
if allowlist is not None and any(p.endswith(" *") for p in allowlist):
|
||||||
|
stripped = [_strip_bash_pattern_wildcard(p) for p in allowlist]
|
||||||
|
deduped = sorted(set(stripped))
|
||||||
|
bash_tools["allowlist"] = deduped
|
||||||
|
changed = True
|
||||||
|
|
||||||
for model in data.get("models", []):
|
for model in data.get("models", []):
|
||||||
if (
|
if (
|
||||||
model.get("name") == "mistral-vibe-cli-latest"
|
model.get("name") == "mistral-vibe-cli-latest"
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from vibe.core.types import (
|
||||||
StrToolChoice,
|
StrToolChoice,
|
||||||
)
|
)
|
||||||
from vibe.core.utils import async_generator_retry, async_retry
|
from vibe.core.utils import async_generator_retry, async_retry
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from vibe.core.config import ModelConfig, ProviderConfig
|
from vibe.core.config import ModelConfig, ProviderConfig
|
||||||
|
|
@ -209,6 +210,7 @@ class GenericBackend:
|
||||||
self._client = httpx.AsyncClient(
|
self._client = httpx.AsyncClient(
|
||||||
timeout=httpx.Timeout(self._timeout),
|
timeout=httpx.Timeout(self._timeout),
|
||||||
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
||||||
|
verify=build_ssl_context(),
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -227,6 +229,7 @@ class GenericBackend:
|
||||||
self._client = httpx.AsyncClient(
|
self._client = httpx.AsyncClient(
|
||||||
timeout=httpx.Timeout(self._timeout),
|
timeout=httpx.Timeout(self._timeout),
|
||||||
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
||||||
|
verify=build_ssl_context(),
|
||||||
)
|
)
|
||||||
self._owns_client = True
|
self._owns_client = True
|
||||||
return self._client
|
return self._client
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ from vibe.core.types import (
|
||||||
ToolCall,
|
ToolCall,
|
||||||
)
|
)
|
||||||
from vibe.core.utils import get_server_url_from_api_base
|
from vibe.core.utils import get_server_url_from_api_base
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from vibe.core.config import ModelConfig, ProviderConfig
|
from vibe.core.config import ModelConfig, ProviderConfig
|
||||||
|
|
@ -182,6 +183,7 @@ _THINKING_TO_REASONING_EFFORT: dict[str, ReasoningEffortValue] = {
|
||||||
class MistralBackend:
|
class MistralBackend:
|
||||||
def __init__(self, provider: ProviderConfig, timeout: float = 720.0) -> None:
|
def __init__(self, provider: ProviderConfig, timeout: float = 720.0) -> None:
|
||||||
self._client: Mistral | None = None
|
self._client: Mistral | None = None
|
||||||
|
self._http_client: httpx.AsyncClient | None = None
|
||||||
self._provider = provider
|
self._provider = provider
|
||||||
self._mapper = MistralMapper()
|
self._mapper = MistralMapper()
|
||||||
self._api_key = (
|
self._api_key = (
|
||||||
|
|
@ -231,17 +233,32 @@ class MistralBackend:
|
||||||
exc_val: BaseException | None,
|
exc_val: BaseException | None,
|
||||||
exc_tb: types.TracebackType | None,
|
exc_tb: types.TracebackType | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if self._client is not None:
|
client = self._client
|
||||||
await self._client.__aexit__(
|
http_client = self._http_client
|
||||||
exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb
|
self._client = None
|
||||||
)
|
self._http_client = None
|
||||||
|
try:
|
||||||
|
if client is not None:
|
||||||
|
await client.__aexit__(
|
||||||
|
exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if http_client is not None:
|
||||||
|
await http_client.aclose()
|
||||||
|
|
||||||
|
async def aclose(self) -> None:
|
||||||
|
await self.__aexit__(None, None, None)
|
||||||
|
|
||||||
def _create_mistral_client(self) -> Mistral:
|
def _create_mistral_client(self) -> Mistral:
|
||||||
|
self._http_client = httpx.AsyncClient(
|
||||||
|
verify=build_ssl_context(), follow_redirects=True
|
||||||
|
)
|
||||||
return Mistral(
|
return Mistral(
|
||||||
api_key=self._api_key,
|
api_key=self._api_key,
|
||||||
server_url=self._server_url,
|
server_url=self._server_url,
|
||||||
timeout_ms=int(self._timeout * 1000),
|
timeout_ms=int(self._timeout * 1000),
|
||||||
retry_config=self._retry_config,
|
retry_config=self._retry_config,
|
||||||
|
async_client=self._http_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_client(self) -> Mistral:
|
def _get_client(self) -> Mistral:
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,30 @@ class ContextWarningMiddleware:
|
||||||
self.has_warned = False
|
self.has_warned = False
|
||||||
|
|
||||||
|
|
||||||
def make_plan_agent_reminder(plan_file_path: str) -> str:
|
def make_plan_agent_reminder(
|
||||||
|
plan_file_path: str,
|
||||||
|
*,
|
||||||
|
has_ask_user_question: bool = True,
|
||||||
|
has_exit_plan_mode: bool = True,
|
||||||
|
) -> str:
|
||||||
|
instructions = [
|
||||||
|
"Research the user's query using read-only tools (grep, read_file, etc.)"
|
||||||
|
]
|
||||||
|
if has_ask_user_question:
|
||||||
|
instructions.append(
|
||||||
|
"If you are unsure about requirements or approach, use the ask_user_question tool to clarify before finalizing your plan"
|
||||||
|
)
|
||||||
|
instructions.append("Write your plan to the plan file above")
|
||||||
|
if has_exit_plan_mode:
|
||||||
|
instructions.append(
|
||||||
|
"When your plan is complete, call the exit_plan_mode tool to request user approval and switch to implementation mode"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
instructions.append(
|
||||||
|
"When your plan is complete, present it to the user and tell them to switch modes if they approve the plan"
|
||||||
|
)
|
||||||
|
numbered = "\n".join(f"{i}. {step}" for i, step in enumerate(instructions, start=1))
|
||||||
|
|
||||||
return f"""<{VIBE_WARNING_TAG}>Plan mode is active. You MUST NOT make any edits (except to the plan file below, or in your scratchpad), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
|
return f"""<{VIBE_WARNING_TAG}>Plan mode is active. You MUST NOT make any edits (except to the plan file below, or in your scratchpad), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
|
||||||
|
|
||||||
## Plan File Info
|
## Plan File Info
|
||||||
|
|
@ -134,10 +157,7 @@ Build your plan incrementally by writing to or editing this file.
|
||||||
This is the only file you are allowed to edit. Make sure to create it early and edit as soon as you internally update your plan.
|
This is the only file you are allowed to edit. Make sure to create it early and edit as soon as you internally update your plan.
|
||||||
|
|
||||||
## Instructions
|
## Instructions
|
||||||
1. Research the user's query using read-only tools (grep, read_file, etc.)
|
{numbered}</{VIBE_WARNING_TAG}>"""
|
||||||
2. If you are unsure about requirements or approach, use the ask_user_question tool to clarify before finalizing your plan
|
|
||||||
3. Write your plan to the plan file above
|
|
||||||
4. When your plan is complete, call the exit_plan_mode tool to request user approval and switch to implementation mode</{VIBE_WARNING_TAG}>"""
|
|
||||||
|
|
||||||
|
|
||||||
PLAN_AGENT_EXIT = f"""<{VIBE_WARNING_TAG}>Plan mode has ended. If you have a plan ready, you can now start executing it. If not, you can now use editing tools and make changes to the system.</{VIBE_WARNING_TAG}>"""
|
PLAN_AGENT_EXIT = f"""<{VIBE_WARNING_TAG}>Plan mode has ended. If you have a plan ready, you can now start executing it. If not, you can now use editing tools and make changes to the system.</{VIBE_WARNING_TAG}>"""
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from vibe.core.nuage.workflow import (
|
||||||
WorkflowExecutionListResponse,
|
WorkflowExecutionListResponse,
|
||||||
WorkflowExecutionStatus,
|
WorkflowExecutionStatus,
|
||||||
)
|
)
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
|
|
||||||
class WorkflowsClient:
|
class WorkflowsClient:
|
||||||
|
|
@ -32,7 +33,9 @@ class WorkflowsClient:
|
||||||
headers: dict[str, str] = {}
|
headers: dict[str, str] = {}
|
||||||
if self._api_key:
|
if self._api_key:
|
||||||
headers["Authorization"] = f"Bearer {self._api_key}"
|
headers["Authorization"] = f"Bearer {self._api_key}"
|
||||||
self._client = httpx.AsyncClient(timeout=self._timeout, headers=headers)
|
self._client = httpx.AsyncClient(
|
||||||
|
timeout=self._timeout, headers=headers, verify=build_ssl_context()
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(
|
async def __aexit__(
|
||||||
|
|
@ -51,7 +54,9 @@ class WorkflowsClient:
|
||||||
headers: dict[str, str] = {}
|
headers: dict[str, str] = {}
|
||||||
if self._api_key:
|
if self._api_key:
|
||||||
headers["Authorization"] = f"Bearer {self._api_key}"
|
headers["Authorization"] = f"Bearer {self._api_key}"
|
||||||
self._client = httpx.AsyncClient(timeout=self._timeout, headers=headers)
|
self._client = httpx.AsyncClient(
|
||||||
|
timeout=self._timeout, headers=headers, verify=build_ssl_context()
|
||||||
|
)
|
||||||
self._owns_client = True
|
self._owns_client = True
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ def run_programmatic( # noqa: PLR0913, PLR0917
|
||||||
return formatter.finalize()
|
return formatter.finalize()
|
||||||
finally:
|
finally:
|
||||||
agent_loop.emit_session_closed_telemetry()
|
agent_loop.emit_session_closed_telemetry()
|
||||||
|
await agent_loop.aclose()
|
||||||
await agent_loop.telemetry_client.aclose()
|
await agent_loop.telemetry_client.aclose()
|
||||||
|
|
||||||
return asyncio.run(_async_run())
|
return asyncio.run(_async_run())
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from vibe.core.telemetry.types import (
|
||||||
TeleportFailureStage,
|
TeleportFailureStage,
|
||||||
)
|
)
|
||||||
from vibe.core.utils import get_server_url_from_api_base, get_user_agent
|
from vibe.core.utils import get_server_url_from_api_base, get_user_agent
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from vibe.core.agent_loop import ToolDecision
|
from vibe.core.agent_loop import ToolDecision
|
||||||
|
|
@ -92,6 +93,7 @@ class TelemetryClient:
|
||||||
self._client = httpx.AsyncClient(
|
self._client = httpx.AsyncClient(
|
||||||
timeout=httpx.Timeout(5.0),
|
timeout=httpx.Timeout(5.0),
|
||||||
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
||||||
|
verify=build_ssl_context(),
|
||||||
)
|
)
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import httpx
|
||||||
from pydantic import BaseModel, Field, ValidationError
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
|
||||||
from vibe.core.teleport.errors import ServiceTeleportError
|
from vibe.core.teleport.errors import ServiceTeleportError
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
|
|
||||||
class GitHubParams(BaseModel):
|
class GitHubParams(BaseModel):
|
||||||
|
|
@ -121,7 +122,9 @@ class NuageClient:
|
||||||
|
|
||||||
async def __aenter__(self) -> NuageClient:
|
async def __aenter__(self) -> NuageClient:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
|
self._client = httpx.AsyncClient(
|
||||||
|
timeout=httpx.Timeout(self._timeout), verify=build_ssl_context()
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(
|
async def __aexit__(
|
||||||
|
|
@ -137,7 +140,9 @@ class NuageClient:
|
||||||
@property
|
@property
|
||||||
def _http_client(self) -> httpx.AsyncClient:
|
def _http_client(self) -> httpx.AsyncClient:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
|
self._client = httpx.AsyncClient(
|
||||||
|
timeout=httpx.Timeout(self._timeout), verify=build_ssl_context()
|
||||||
|
)
|
||||||
self._owns_client = True
|
self._owns_client = True
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ from vibe.core.teleport.types import (
|
||||||
TeleportWaitingForGitHubEvent,
|
TeleportWaitingForGitHubEvent,
|
||||||
TeleportYieldEvent,
|
TeleportYieldEvent,
|
||||||
)
|
)
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
_DEFAULT_TELEPORT_PROMPT = "Your session has been teleported on a remote workspace. Changes of workspace has been automatically teleported. External workspace changes has NOT been teleported. Environment variables has NOT been teleported. Please continue where you left off."
|
_DEFAULT_TELEPORT_PROMPT = "Your session has been teleported on a remote workspace. Changes of workspace has been automatically teleported. External workspace changes has NOT been teleported. Environment variables has NOT been teleported. Please continue where you left off."
|
||||||
|
|
||||||
|
|
@ -73,7 +74,9 @@ class TeleportService:
|
||||||
|
|
||||||
async def __aenter__(self) -> TeleportService:
|
async def __aenter__(self) -> TeleportService:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
|
self._client = httpx.AsyncClient(
|
||||||
|
timeout=httpx.Timeout(self._timeout), verify=build_ssl_context()
|
||||||
|
)
|
||||||
self._nuage_client_instance = NuageClient(
|
self._nuage_client_instance = NuageClient(
|
||||||
self._vibe_code_base_url,
|
self._vibe_code_base_url,
|
||||||
self._vibe_code_api_key,
|
self._vibe_code_api_key,
|
||||||
|
|
@ -98,7 +101,9 @@ class TeleportService:
|
||||||
@property
|
@property
|
||||||
def _http_client(self) -> httpx.AsyncClient:
|
def _http_client(self) -> httpx.AsyncClient:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
|
self._client = httpx.AsyncClient(
|
||||||
|
timeout=httpx.Timeout(self._timeout), verify=build_ssl_context()
|
||||||
|
)
|
||||||
self._owns_client = True
|
self._owns_client = True
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from vibe.core.tools.permissions import (
|
||||||
)
|
)
|
||||||
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
|
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
|
||||||
from vibe.core.types import ToolStreamEvent
|
from vibe.core.types import ToolStreamEvent
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||||
|
|
@ -198,7 +199,9 @@ class WebFetch(
|
||||||
self, url: str, timeout: int, headers: dict[str, str]
|
self, url: str, timeout: int, headers: dict[str, str]
|
||||||
) -> httpx.Response:
|
) -> httpx.Response:
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
follow_redirects=True, timeout=httpx.Timeout(timeout)
|
follow_redirects=True,
|
||||||
|
timeout=httpx.Timeout(timeout),
|
||||||
|
verify=build_ssl_context(),
|
||||||
) as client:
|
) as client:
|
||||||
response = await client.get(url, headers=headers)
|
response = await client.get(url, headers=headers)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from collections.abc import AsyncGenerator
|
||||||
import os
|
import os
|
||||||
from typing import TYPE_CHECKING, ClassVar, final
|
from typing import TYPE_CHECKING, ClassVar, final
|
||||||
|
|
||||||
|
import httpx
|
||||||
from mistralai.client import Mistral
|
from mistralai.client import Mistral
|
||||||
from mistralai.client.errors import SDKError
|
from mistralai.client.errors import SDKError
|
||||||
from mistralai.client.models import (
|
from mistralai.client.models import (
|
||||||
|
|
@ -24,7 +25,7 @@ from vibe.core.tools.base import (
|
||||||
)
|
)
|
||||||
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
|
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
|
||||||
from vibe.core.types import Backend, ToolStreamEvent
|
from vibe.core.types import Backend, ToolStreamEvent
|
||||||
from vibe.core.utils import get_server_url_from_api_base
|
from vibe.core.utils.http import build_ssl_context, get_server_url_from_api_base
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||||
|
|
@ -73,14 +74,17 @@ class WebSearch(
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise ToolError("MISTRAL_API_KEY environment variable not set.")
|
raise ToolError("MISTRAL_API_KEY environment variable not set.")
|
||||||
|
|
||||||
client = Mistral(
|
ssl_context = build_ssl_context()
|
||||||
api_key=api_key,
|
async_http_client = httpx.AsyncClient(follow_redirects=True, verify=ssl_context)
|
||||||
server_url=self._resolve_server_url(ctx),
|
|
||||||
timeout_ms=self.config.timeout * 1000,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with client:
|
client = Mistral(
|
||||||
|
api_key=api_key,
|
||||||
|
server_url=self._resolve_server_url(ctx),
|
||||||
|
timeout_ms=self.config.timeout * 1000,
|
||||||
|
async_client=async_http_client,
|
||||||
|
)
|
||||||
|
async with async_http_client, client:
|
||||||
response = await client.beta.conversations.start_async(
|
response = await client.beta.conversations.start_async(
|
||||||
model=self.config.model,
|
model=self.config.model,
|
||||||
instructions="Always use the web_search tool to answer queries. Never answer from memory alone.",
|
instructions="Always use the web_search tool to answer queries. Never answer from memory alone.",
|
||||||
|
|
@ -93,6 +97,8 @@ class WebSearch(
|
||||||
|
|
||||||
except SDKError as exc:
|
except SDKError as exc:
|
||||||
raise ToolError(f"Mistral API error: {exc}") from exc
|
raise ToolError(f"Mistral API error: {exc}") from exc
|
||||||
|
finally:
|
||||||
|
await async_http_client.aclose()
|
||||||
|
|
||||||
def _resolve_server_url(self, ctx: InvokeContext | None) -> str | None:
|
def _resolve_server_url(self, ctx: InvokeContext | None) -> str | None:
|
||||||
if not ctx or not ctx.agent_manager:
|
if not ctx or not ctx.agent_manager:
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from vibe.core.tools.mcp.tools import (
|
||||||
from vibe.core.tools.ui import ToolResultDisplay
|
from vibe.core.tools.ui import ToolResultDisplay
|
||||||
from vibe.core.types import ToolStreamEvent
|
from vibe.core.types import ToolStreamEvent
|
||||||
from vibe.core.utils import run_sync
|
from vibe.core.utils import run_sync
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from vibe.core.types import ToolResultEvent
|
from vibe.core.types import ToolResultEvent
|
||||||
|
|
@ -238,9 +239,6 @@ class ConnectorRegistry:
|
||||||
self._alias_to_id: dict[str, str] = {}
|
self._alias_to_id: dict[str, str] = {}
|
||||||
self._discover_lock = asyncio.Lock()
|
self._discover_lock = asyncio.Lock()
|
||||||
|
|
||||||
def _get_client(self) -> Mistral:
|
|
||||||
return Mistral(api_key=self._api_key, server_url=self._server_url)
|
|
||||||
|
|
||||||
def get_tools(self) -> dict[str, type[BaseTool]]:
|
def get_tools(self) -> dict[str, type[BaseTool]]:
|
||||||
"""Return proxy tool classes for all connectors, using cache when possible."""
|
"""Return proxy tool classes for all connectors, using cache when possible."""
|
||||||
return run_sync(self.get_tools_async())
|
return run_sync(self.get_tools_async())
|
||||||
|
|
@ -259,7 +257,9 @@ class ConnectorRegistry:
|
||||||
base_url = self._server_url or _DEFAULT_BASE_URL
|
base_url = self._server_url or _DEFAULT_BASE_URL
|
||||||
url = f"{base_url}/v1/connectors/bootstrap"
|
url = f"{base_url}/v1/connectors/bootstrap"
|
||||||
headers = {"Authorization": f"Bearer {self._api_key}"}
|
headers = {"Authorization": f"Bearer {self._api_key}"}
|
||||||
async with httpx.AsyncClient(timeout=_BOOTSTRAP_TIMEOUT) as http:
|
async with httpx.AsyncClient(
|
||||||
|
timeout=_BOOTSTRAP_TIMEOUT, verify=build_ssl_context()
|
||||||
|
) as http:
|
||||||
response = await http.get(url, headers=headers)
|
response = await http.get(url, headers=headers)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
@ -427,11 +427,22 @@ class ConnectorRegistry:
|
||||||
if connector_id is None:
|
if connector_id is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
async with self._get_client() as client:
|
http_client = httpx.AsyncClient(
|
||||||
result = await client.beta.connectors.get_auth_url_async(
|
verify=build_ssl_context(), follow_redirects=True
|
||||||
connector_id_or_name=connector_id
|
)
|
||||||
|
try:
|
||||||
|
sdk_client = Mistral(
|
||||||
|
api_key=self._api_key,
|
||||||
|
server_url=self._server_url,
|
||||||
|
async_client=http_client,
|
||||||
)
|
)
|
||||||
return result.auth_url
|
async with sdk_client as client:
|
||||||
|
result = await client.beta.connectors.get_auth_url_async(
|
||||||
|
connector_id_or_name=connector_id
|
||||||
|
)
|
||||||
|
return result.auth_url
|
||||||
|
finally:
|
||||||
|
await http_client.aclose()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("Failed to get auth URL for connector %s", alias)
|
logger.debug("Failed to get auth URL for connector %s", alias)
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -254,19 +254,19 @@ class ToolManager:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_source_disabled(
|
def _is_source_disabled(
|
||||||
cls: type[BaseTool],
|
tool_cls: type[BaseTool],
|
||||||
disabled_sources: set[tuple[str, bool]],
|
disabled_sources: set[tuple[str, bool]],
|
||||||
per_source_disabled: dict[tuple[str, bool], set[str]],
|
per_source_disabled: dict[tuple[str, bool], set[str]],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not issubclass(cls, MCPTool):
|
if not issubclass(tool_cls, MCPTool):
|
||||||
return False
|
return False
|
||||||
server_name = cls.get_server_name()
|
server_name = tool_cls.get_server_name()
|
||||||
if server_name is None:
|
if server_name is None:
|
||||||
return False
|
return False
|
||||||
key = (server_name, cls.is_connector())
|
key = (server_name, tool_cls.is_connector())
|
||||||
if key in disabled_sources:
|
if key in disabled_sources:
|
||||||
return True
|
return True
|
||||||
return cls.get_remote_name() in per_source_disabled.get(key, set())
|
return tool_cls.get_remote_name() in per_source_disabled.get(key, set())
|
||||||
|
|
||||||
def integrate_mcp(self, *, raise_on_failure: bool = False) -> None:
|
def integrate_mcp(self, *, raise_on_failure: bool = False) -> None:
|
||||||
"""Discover and register MCP tools (sync wrapper).
|
"""Discover and register MCP tools (sync wrapper).
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import httpx
|
||||||
from mistralai.client import Mistral
|
from mistralai.client import Mistral
|
||||||
from mistralai.client.models import (
|
from mistralai.client.models import (
|
||||||
AudioFormat,
|
AudioFormat,
|
||||||
|
|
@ -21,6 +22,7 @@ from vibe.core.transcribe.transcribe_client_port import (
|
||||||
TranscribeSessionCreated,
|
TranscribeSessionCreated,
|
||||||
TranscribeTextDelta,
|
TranscribeTextDelta,
|
||||||
)
|
)
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
|
|
||||||
class MistralTranscribeClient:
|
class MistralTranscribeClient:
|
||||||
|
|
@ -35,10 +37,18 @@ class MistralTranscribeClient:
|
||||||
)
|
)
|
||||||
self._target_streaming_delay_ms = model.target_streaming_delay_ms
|
self._target_streaming_delay_ms = model.target_streaming_delay_ms
|
||||||
self._client: Mistral | None = None
|
self._client: Mistral | None = None
|
||||||
|
self._http_client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
def _get_client(self) -> Mistral:
|
def _get_client(self) -> Mistral:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = Mistral(api_key=self._api_key, server_url=self._server_url)
|
self._http_client = httpx.AsyncClient(
|
||||||
|
verify=build_ssl_context(), follow_redirects=True
|
||||||
|
)
|
||||||
|
self._client = Mistral(
|
||||||
|
api_key=self._api_key,
|
||||||
|
server_url=self._server_url,
|
||||||
|
async_client=self._http_client,
|
||||||
|
)
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
async def transcribe(
|
async def transcribe(
|
||||||
|
|
@ -61,3 +71,15 @@ class MistralTranscribeClient:
|
||||||
yield TranscribeError(message=str(event.error.message))
|
yield TranscribeError(message=str(event.error.message))
|
||||||
elif isinstance(event, UnknownRealtimeEvent):
|
elif isinstance(event, UnknownRealtimeEvent):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
client = self._client
|
||||||
|
http_client = self._http_client
|
||||||
|
self._client = None
|
||||||
|
self._http_client = None
|
||||||
|
try:
|
||||||
|
if client is not None:
|
||||||
|
await client.__aexit__(exc_type=None, exc_val=None, exc_tb=None)
|
||||||
|
finally:
|
||||||
|
if http_client is not None:
|
||||||
|
await http_client.aclose()
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,5 @@ class TranscribeClientPort(Protocol):
|
||||||
def transcribe(
|
def transcribe(
|
||||||
self, audio_stream: AsyncIterator[bytes]
|
self, audio_stream: AsyncIterator[bytes]
|
||||||
) -> AsyncIterator[TranscribeEvent]: ...
|
) -> AsyncIterator[TranscribeEvent]: ...
|
||||||
|
|
||||||
|
async def close(self) -> None: ...
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ from __future__ import annotations
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import httpx
|
||||||
from mistralai.client import Mistral
|
from mistralai.client import Mistral
|
||||||
from mistralai.client.models import SpeechOutputFormat
|
from mistralai.client.models import SpeechOutputFormat
|
||||||
|
|
||||||
from vibe.core.config import TTSModelConfig, TTSProviderConfig
|
from vibe.core.config import TTSModelConfig, TTSProviderConfig
|
||||||
from vibe.core.tts.tts_client_port import TTSResult
|
from vibe.core.tts.tts_client_port import TTSResult
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
|
|
||||||
class MistralTTSClient:
|
class MistralTTSClient:
|
||||||
|
|
@ -18,10 +20,18 @@ class MistralTTSClient:
|
||||||
self._voice = model.voice
|
self._voice = model.voice
|
||||||
self._response_format: SpeechOutputFormat = model.response_format
|
self._response_format: SpeechOutputFormat = model.response_format
|
||||||
self._client: Mistral | None = None
|
self._client: Mistral | None = None
|
||||||
|
self._http_client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
def _get_client(self) -> Mistral:
|
def _get_client(self) -> Mistral:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = Mistral(api_key=self._api_key, server_url=self._server_url)
|
self._http_client = httpx.AsyncClient(
|
||||||
|
verify=build_ssl_context(), follow_redirects=True
|
||||||
|
)
|
||||||
|
self._client = Mistral(
|
||||||
|
api_key=self._api_key,
|
||||||
|
server_url=self._server_url,
|
||||||
|
async_client=self._http_client,
|
||||||
|
)
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
async def speak(self, text: str) -> TTSResult:
|
async def speak(self, text: str) -> TTSResult:
|
||||||
|
|
@ -36,6 +46,13 @@ class MistralTTSClient:
|
||||||
return TTSResult(audio_data=audio_bytes)
|
return TTSResult(audio_data=audio_bytes)
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
if self._client is not None:
|
client = self._client
|
||||||
await self._client.__aexit__(exc_type=None, exc_val=None, exc_tb=None)
|
http_client = self._http_client
|
||||||
self._client = None
|
self._client = None
|
||||||
|
self._http_client = None
|
||||||
|
try:
|
||||||
|
if client is not None:
|
||||||
|
await client.__aexit__(exc_type=None, exc_val=None, exc_tb=None)
|
||||||
|
finally:
|
||||||
|
if http_client is not None:
|
||||||
|
await http_client.aclose()
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,11 @@ from vibe.core.utils.concurrency import (
|
||||||
run_sync,
|
run_sync,
|
||||||
)
|
)
|
||||||
from vibe.core.utils.display import compact_reduction_display
|
from vibe.core.utils.display import compact_reduction_display
|
||||||
from vibe.core.utils.http import get_server_url_from_api_base, get_user_agent
|
from vibe.core.utils.http import (
|
||||||
|
build_ssl_context,
|
||||||
|
get_server_url_from_api_base,
|
||||||
|
get_user_agent,
|
||||||
|
)
|
||||||
from vibe.core.utils.matching import name_matches
|
from vibe.core.utils.matching import name_matches
|
||||||
from vibe.core.utils.merge import MergeConflictError, MergeStrategy
|
from vibe.core.utils.merge import MergeConflictError, MergeStrategy
|
||||||
from vibe.core.utils.paths import is_dangerous_directory
|
from vibe.core.utils.paths import is_dangerous_directory
|
||||||
|
|
@ -46,6 +50,7 @@ __all__ = [
|
||||||
"TaggedText",
|
"TaggedText",
|
||||||
"async_generator_retry",
|
"async_generator_retry",
|
||||||
"async_retry",
|
"async_retry",
|
||||||
|
"build_ssl_context",
|
||||||
"compact_reduction_display",
|
"compact_reduction_display",
|
||||||
"get_server_url_from_api_base",
|
"get_server_url_from_api_base",
|
||||||
"get_user_agent",
|
"get_user_agent",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,37 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
import certifi
|
||||||
|
|
||||||
from vibe import __version__
|
from vibe import __version__
|
||||||
from vibe.core.types import Backend
|
from vibe.core.types import Backend
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def build_ssl_context() -> ssl.SSLContext:
|
||||||
|
ctx = ssl.create_default_context(cafile=certifi.where())
|
||||||
|
|
||||||
|
# Custom certs are additive so private-CA users don't lose public roots.
|
||||||
|
ssl_cert_file = os.getenv("SSL_CERT_FILE")
|
||||||
|
ssl_cert_dir = os.getenv("SSL_CERT_DIR")
|
||||||
|
if ssl_cert_file or ssl_cert_dir:
|
||||||
|
try:
|
||||||
|
ctx.load_verify_locations(cafile=ssl_cert_file, capath=ssl_cert_dir)
|
||||||
|
except (OSError, ssl.SSLError):
|
||||||
|
from vibe.core.logger import logger
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"Failed to load custom SSL certificates: SSL_CERT_FILE=%s SSL_CERT_DIR=%s",
|
||||||
|
ssl_cert_file,
|
||||||
|
ssl_cert_dir,
|
||||||
|
)
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
def get_user_agent(backend: Backend | None) -> str:
|
def get_user_agent(backend: Backend | None) -> str:
|
||||||
user_agent = f"Mistral-Vibe/{__version__}"
|
user_agent = f"Mistral-Vibe/{__version__}"
|
||||||
if backend == Backend.MISTRAL:
|
if backend == Backend.MISTRAL:
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from urllib.parse import SplitResult, unquote, urlsplit
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from vibe.core.logger import logger
|
from vibe.core.logger import logger
|
||||||
|
from vibe.core.utils.http import build_ssl_context
|
||||||
from vibe.setup.auth.browser_sign_in_gateway import (
|
from vibe.setup.auth.browser_sign_in_gateway import (
|
||||||
BrowserSignInError,
|
BrowserSignInError,
|
||||||
BrowserSignInErrorCode,
|
BrowserSignInErrorCode,
|
||||||
|
|
@ -203,7 +204,7 @@ class HttpBrowserSignInGateway(BrowserSignInGateway):
|
||||||
|
|
||||||
def _ensure_client(self) -> httpx.AsyncClient:
|
def _ensure_client(self) -> httpx.AsyncClient:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = httpx.AsyncClient()
|
self._client = httpx.AsyncClient(verify=build_ssl_context())
|
||||||
self._should_manage_client = True
|
self._should_manage_client = True
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# What's new in v2.9.5
|
|
||||||
|
|
||||||
- **`/loop` command**: Run a prompt or slash command on a recurring interval.
|
|
||||||
- **`default_agent` config**: Set which agent profile starts each session.
|
|
||||||
- **Parallel-safe history**: Multiple `vibe` instances no longer clobber each other's history file.
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue