mirror of
https://github.com/pydantic/pydantic-ai-harness.git
synced 2026-07-21 02:45:34 +00:00
* feat(shell): control subprocess environment to stop secret inheritance Shell spawned every command with the inherited parent environment, so an agent running model-written commands in a sandbox could read the host's LLM API keys and tokens. This blocked replacing Pydanty's hand-rolled runner with Shell. Add two composable controls: `env` replaces inheritance entirely (a hard boundary -- the subprocess sees only what you pass), and `denied_env_patterns` strips glob-matched names from the base environment (mirrors `denied_commands`). When both are set, patterns also filter the explicit `env`. The default stays inherit-everything. Export `LLM_API_KEY_ENV_PATTERNS` for the common provider-credential denylist; opt-in, since stripping env silently would break agents that rely on inherited credentials. Closes #281 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(shell): tighten env-control wording after adversarial review Three-lens self-review (API breadth, correctness, docs/tests) surfaced doc-precision gaps and one missing end-to-end test; no correctness defects. - denied_env_patterns matches by glob, not exact match like denied_commands: correct the "mirrors denied_commands" claim to name the real parallel (the denied_* naming convention). - LLM_API_KEY_ENV_PATTERNS targets LLM credentials only and uses coarse prefixes (GOOGLE_* also strips GOOGLE_APPLICATION_CREDENTIALS); say so, and note it does not cover other host secrets like LOGFIRE/GitHub tokens. - env is enforced at spawn; clarify that with denied_env_patterns it is the resolved (filtered) env, and warn that stripping PATH/HOME breaks command resolution. - Add an end-to-end test proving env and denied_env_patterns compose at spawn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: David SF <david.sanchez@pydantic.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
58 lines
1.4 KiB
Python
58 lines
1.4 KiB
Python
import inspect
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pydantic_ai import Agent
|
|
from pydantic_ai.models.test import TestModel
|
|
|
|
import pydantic_ai_harness
|
|
|
|
|
|
def test_import():
|
|
assert pydantic_ai_harness.__doc__ is not None
|
|
assert isinstance(pydantic_ai_harness.__all__, list)
|
|
|
|
|
|
def test_lazy_import_filesystem():
|
|
from pydantic_ai_harness import FileSystem
|
|
|
|
assert inspect.isclass(FileSystem)
|
|
assert hasattr(FileSystem, 'get_toolset')
|
|
|
|
|
|
def test_lazy_import_shell():
|
|
from pydantic_ai_harness import Shell
|
|
|
|
assert inspect.isclass(Shell)
|
|
assert hasattr(Shell, 'get_toolset')
|
|
|
|
|
|
def test_lazy_import_llm_api_key_env_patterns():
|
|
from pydantic_ai_harness import LLM_API_KEY_ENV_PATTERNS
|
|
|
|
assert isinstance(LLM_API_KEY_ENV_PATTERNS, tuple)
|
|
assert 'OPENAI_*' in LLM_API_KEY_ENV_PATTERNS
|
|
|
|
|
|
def test_lazy_import_unknown():
|
|
with pytest.raises(AttributeError, match='has no attribute'):
|
|
pydantic_ai_harness.__getattr__('Nonexistent')
|
|
|
|
|
|
def test_test_model_fixture(test_model: TestModel):
|
|
assert isinstance(test_model, TestModel)
|
|
|
|
|
|
def test_test_agent_fixture(test_agent: Agent[None, str]):
|
|
assert test_agent.name == 'test-agent'
|
|
|
|
|
|
def test_tmp_dir_fixture(tmp_dir: Path):
|
|
assert tmp_dir.is_dir()
|
|
|
|
|
|
async def test_allow_model_requests(allow_model_requests: None):
|
|
import pydantic_ai.models
|
|
|
|
assert pydantic_ai.models.ALLOW_MODEL_REQUESTS is True
|