"""Tests for YAML / dict loading.""" import asyncio import os import sys import tempfile import unittest from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from omnigent.inner.datamodel import ExecutorSpec, OSEnvSandboxSpec, OSEnvSpec from omnigent.inner.loader import load_agent_def from omnigent.inner.policies import FunctionPolicy, PromptPolicy from omnigent.inner.tools import ( AgentTool, CancellableFunctionTool, FunctionTool, HandoffTool, InheritedTool, MCPTool, SkillTool, ) def _run(coro): loop = asyncio.new_event_loop() try: return loop.run_until_complete(coro) finally: loop.close() # ── Local runner-protocol fixture ──────────────────────── # # The inner stack still supports ``type: cancellable_function`` # YAMLs that point at runner-protocol objects. The test file # carries its own minimal runner instance instead of depending # on ``tests.resources.examples._shared.tool_functions`` (which is now plain- # callable only after step (c) — see # designs/SERVER_HARNESS_CONTRACT.md). class _TestSleepRunner: """Runner-protocol stub for the cancellable_function loader test.""" def start(self, args: object, on_complete: object) -> None: """Stub — never called by the loader test.""" raise NotImplementedError sleep_runner = _TestSleepRunner() class TestLoadFromDict(unittest.TestCase): def test_minimal(self): a = load_agent_def({"name": "test", "prompt": "Hello."}) self.assertEqual(a.name, "test") self.assertEqual(a.prompt, "Hello.") def test_executor_string(self): self.assertEqual( load_agent_def({"name": "t", "executor": "gpt-5"}).executor.model, "gpt-5" ) def test_executor_dict(self): a = load_agent_def({"name": "t", "executor": {"model": "claude-sonnet-4"}}) self.assertEqual(a.executor.model, "claude-sonnet-4") def test_tools_function(self): a = load_agent_def( {"name": "t", "tools": {"f": {"type": "function", "catalog_path": "a.b.c"}}} ) self.assertIsInstance(a.tools["f"], FunctionTool) self.assertEqual(a.tools["f"].catalog_path, "a.b.c") def test_tools_cancellable_function(self): a = load_agent_def( { "name": "t", "tools": { "sleep": { "type": "cancellable_function", "runner": "tests.inner.test_loader.sleep_runner", } }, } ) self.assertIsInstance(a.tools["sleep"], CancellableFunctionTool) self.assertTrue(a.tools["sleep"].cancellable) self.assertIsNotNone(a.tools["sleep"].runner) def test_tools_mcp(self): a = load_agent_def({"name": "t", "tools": {"s": {"type": "mcp", "url": "http://x"}}}) self.assertIsInstance(a.tools["s"], MCPTool) self.assertEqual(a.tools["s"].url, "http://x") def test_tools_agent(self): a = load_agent_def( { "name": "t", "tools": {"h": {"type": "agent", "prompt": "Help.", "tools": {"s": "inherit"}}}, } ) self.assertIsInstance(a.tools["h"], AgentTool) self.assertIsInstance(a.tools["h"].tools["s"], InheritedTool) def test_tools_agent_with_executor(self): a = load_agent_def( { "name": "t", "tools": { "h": { "type": "agent", "prompt": "Help.", "executor": { "model": "databricks-claude-sonnet-4", "harness": "claude-sdk", "profile": "test-profile", }, } }, } ) self.assertIsInstance(a.tools["h"], AgentTool) self.assertEqual( a.tools["h"].executor, ExecutorSpec( model="databricks-claude-sonnet-4", harness="claude-sdk", profile="test-profile", ), ) def test_tools_agent_with_inherited_os_env(self): a = load_agent_def( { "name": "t", "tools": { "h": { "type": "agent", "prompt": "Help.", "os_env": "inherit", } }, } ) self.assertIsInstance(a.tools["h"], AgentTool) self.assertEqual(a.tools["h"].os_env, "inherit") def test_tools_agent_max_sessions(self): a = load_agent_def( { "name": "t", "tools": { "h": { "type": "agent", "prompt": "Help.", "max_sessions": 3, } }, } ) self.assertIsInstance(a.tools["h"], AgentTool) self.assertEqual(a.tools["h"].max_sessions, 3) def test_tools_agent_max_sessions_defaults_to_none(self): a = load_agent_def({"name": "t", "tools": {"h": {"type": "agent", "prompt": "Help."}}}) self.assertIsInstance(a.tools["h"], AgentTool) self.assertIsNone(a.tools["h"].max_sessions) def test_tools_agent_max_sessions_valid_integer(self): """Valid positive integers are accepted and stored unchanged.""" for value in (1, 5, 100): with self.subTest(value=value): a = load_agent_def( { "name": "t", "tools": {"h": {"type": "agent", "prompt": ".", "max_sessions": value}}, } ) self.assertEqual(a.tools["h"].max_sessions, value) def test_tools_agent_max_sessions_invalid_string_raises(self): """A string value for max_sessions must raise a ValueError naming the tool and field.""" with self.assertRaises(ValueError) as ctx: load_agent_def( { "name": "t", "tools": {"my_tool": {"type": "agent", "prompt": ".", "max_sessions": "3"}}, } ) msg = str(ctx.exception) self.assertIn("my_tool", msg) self.assertIn("max_sessions", msg) def test_tools_agent_max_sessions_invalid_float_raises(self): """A float value for max_sessions must raise a ValueError naming the tool and field.""" with self.assertRaises(ValueError) as ctx: load_agent_def( { "name": "t", "tools": {"my_tool": {"type": "agent", "prompt": ".", "max_sessions": 2.5}}, } ) msg = str(ctx.exception) self.assertIn("my_tool", msg) self.assertIn("max_sessions", msg) def test_tools_agent_max_sessions_zero_raises(self): """Zero must raise a ValueError naming the tool and field.""" with self.assertRaises(ValueError) as ctx: load_agent_def( { "name": "t", "tools": {"my_tool": {"type": "agent", "prompt": ".", "max_sessions": 0}}, } ) msg = str(ctx.exception) self.assertIn("my_tool", msg) self.assertIn("max_sessions", msg) def test_tools_agent_max_sessions_negative_raises(self): """Negative integers must raise a ValueError naming the tool and field.""" with self.assertRaises(ValueError) as ctx: load_agent_def( { "name": "t", "tools": {"my_tool": {"type": "agent", "prompt": ".", "max_sessions": -1}}, } ) msg = str(ctx.exception) self.assertIn("my_tool", msg) self.assertIn("max_sessions", msg) def test_tools_inherit(self): self.assertIsInstance( load_agent_def({"name": "t", "tools": {"x": "inherit"}}).tools["x"], InheritedTool ) def test_tools_skill(self): a = load_agent_def( {"name": "t", "tools": {"d": {"type": "skill", "content": "./skills/d"}}} ) self.assertIsInstance(a.tools["d"], SkillTool) self.assertEqual(a.tools["d"].path, "./skills/d") def test_tools_handoff(self): a = load_agent_def( {"name": "t", "tools": {"e": {"type": "handoff", "target_agent": "billing"}}} ) self.assertIsInstance(a.tools["e"], HandoffTool) self.assertEqual(a.tools["e"].target_agent, "billing") def test_policies_function(self): a = load_agent_def( {"name": "t", "policies": {"p": {"type": "function", "on": ["request"]}}}, ) self.assertIsInstance(a.policies["p"], FunctionPolicy) self.assertEqual(a.policies["p"].on, ["request"]) def test_policies_function_with_factory_params(self): a = load_agent_def( { "name": "t", "policies": { "c": { "type": "function", "on": ["tool_call"], "handler": ( "tests.resources.examples._shared" ".rate_limit_policy.max_tool_calls_per_turn" ), "factory_params": {"limit": 10}, } }, } ) self.assertIsInstance(a.policies["c"], FunctionPolicy) self.assertEqual(a.policies["c"].factory_params, {"limit": 10}) self.assertIsNotNone(a.policies["c"].callable) self.assertIsNotNone(a.policies["c"].factory) def test_policies_function_with_empty_factory_params(self): """Empty factory_params: {} should still trigger factory invocation (zero-arg factory).""" a = load_agent_def( { "name": "t", "policies": { "c": { "type": "function", "on": ["tool_call"], "handler": ( "tests.resources.examples._shared" ".rate_limit_policy.max_tool_calls_per_turn" ), "factory_params": {}, } }, } ) self.assertIsInstance(a.policies["c"], FunctionPolicy) self.assertIsNotNone(a.policies["c"].factory) self.assertIsNotNone(a.policies["c"].callable) # The factory should have been called with defaults (limit=10) self.assertNotEqual(a.policies["c"].factory, a.policies["c"].callable) def test_policies_prompt(self): a = load_agent_def( { "name": "t", "policies": {"s": {"type": "prompt", "on": ["response"], "prompt": "Safe?"}}, } ) self.assertIsInstance(a.policies["s"], PromptPolicy) def test_policies_prompt_executor_string(self): a = load_agent_def( { "name": "t", "policies": { "s": { "type": "prompt", "on": ["response"], "prompt": "Safe?", "executor": "gpt-5-mini", } }, } ) self.assertEqual(a.policies["s"].executor, ExecutorSpec(model="gpt-5-mini")) def test_policies_prompt_executor_dict(self): a = load_agent_def( { "name": "t", "policies": { "s": { "type": "prompt", "on": ["response"], "prompt": "Safe?", "executor": { "model": "gpt-5-mini", "harness": "open-responses", "profile": "policy", }, } }, } ) self.assertEqual( a.policies["s"].executor, ExecutorSpec( model="gpt-5-mini", harness="open-responses", profile="policy", ), ) def test_memories(self): a = load_agent_def( {"name": "t", "memories": {"p": {"scope": "per_user"}, "s": {"scope": "cross_user"}}} ) self.assertEqual(a.memories["p"].scope, "per_user") self.assertEqual(a.memories["s"].scope, "cross_user") def test_runtime_flag(self): self.assertTrue(load_agent_def({"name": "t", "runtime": True}).runtime) def test_async_flag_defaults_true(self): self.assertTrue(load_agent_def({"name": "t"}).async_enabled) def test_async_flag_from_dict(self): self.assertFalse(load_agent_def({"name": "t", "async": False}).async_enabled) def test_cancellable_flag_from_dict(self): self.assertFalse(load_agent_def({"name": "t", "cancellable": False}).cancellable) def test_async_enabled_alias_from_dict(self): self.assertFalse(load_agent_def({"name": "t", "async_enabled": False}).async_enabled) def test_os_env_string(self): a = load_agent_def({"name": "t", "os_env": "caller_process"}) self.assertEqual(a.os_env, OSEnvSpec(type="caller_process")) def test_os_env_dict(self): a = load_agent_def( { "name": "t", "os_env": { "type": "caller_process", "cwd": "/tmp/work", "sandbox": { "type": "none", }, }, } ) self.assertEqual( a.os_env, OSEnvSpec( type="caller_process", cwd="/tmp/work", sandbox=OSEnvSandboxSpec(type="none"), ), ) def test_params(self): a = load_agent_def( {"name": "t", "params": {"u": {"type": "string", "description": "User"}}} ) self.assertEqual(a.params["u"].type, "string") def test_workflow(self): self.assertEqual( load_agent_def({"name": "t", "workflow": "a -> b -> c"}).workflow, "a -> b -> c" ) class TestLoadFromYAML(unittest.TestCase): def test_yaml_on_key_is_not_parsed_as_boolean(self): yaml_content = """ name: policy_agent runtime: true policies: block_sleep: type: function on: [tool_call] """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: a = load_agent_def(f.name) self.assertEqual(a.policies["block_sleep"].on, ["tool_call"]) self.assertTrue(a.runtime) finally: os.unlink(f.name) def test_load_yaml_file(self): yaml_content = """ name: data_analyst prompt: Answer questions using data. async: false executor: model: databricks-claude-sonnet-4 tools: sql_query: type: mcp url: http://localhost:8080 table_search: type: agent prompt: Search for tables. tools: sql_query: inherit policies: cost_limit: type: function on: [tool_call] callable: tests.resources.examples._shared.rate_limit_policy.max_tool_calls_per_turn factory_params: limit: 20 memories: user_prefs: scope: per_user runtime: true """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: a = load_agent_def(f.name) self.assertEqual(a.name, "data_analyst") self.assertEqual(a.executor.model, "databricks-claude-sonnet-4") self.assertIsInstance(a.tools["sql_query"], MCPTool) self.assertIsInstance(a.tools["table_search"], AgentTool) self.assertIsInstance(a.tools["table_search"].tools["sql_query"], InheritedTool) self.assertIsInstance(a.policies["cost_limit"], FunctionPolicy) self.assertEqual(a.policies["cost_limit"].on, ["tool_call"]) self.assertEqual(a.memories["user_prefs"].scope, "per_user") self.assertFalse(a.async_enabled) self.assertTrue(a.runtime) finally: os.unlink(f.name) def test_load_yaml_file_with_command_mcp(self): yaml_content = """ name: docs_agent prompt: Answer questions using docs. tools: google: type: mcp command: python3.10 args: - /home/matei/mcp/servers/google_mcp/google_mcp_deploy.pex tools: - google_docs_search """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: a = load_agent_def(f.name) self.assertIsInstance(a.tools["google"], MCPTool) self.assertEqual(a.tools["google"].command, "python3.10") self.assertEqual( a.tools["google"].args, ["/home/matei/mcp/servers/google_mcp/google_mcp_deploy.pex"], ) self.assertEqual(a.tools["google"].tools, ["google_docs_search"]) finally: os.unlink(f.name) class TestInstructionsField(unittest.TestCase): """ ``instructions:`` field handling in omnigent-flavored YAML. Native Omnigent YAMLs have always supported ``instructions: `` (path relative to the bundle dir, falling through to inline text if not a file). Omnigent-flavored YAMLs silently dropped the field — the loader didn't read it, the translator didn't see it. Bug from kasey_uhlenhuth's report. These tests pin the cross-format parity. """ def test_instructions_resolves_relative_path_to_file_contents(self): """``instructions: foo.md`` reads foo.md sitting next to the YAML.""" yaml_content = "name: instr_agent\nprompt: dummy\ninstructions: AGENTS.md\n" with tempfile.TemporaryDirectory() as td: yaml_path = Path(td) / "agent.yaml" yaml_path.write_text(yaml_content) (Path(td) / "AGENTS.md").write_text("REPLY ONLY: SAUCE") a = load_agent_def(str(yaml_path)) self.assertEqual(a.instructions, "REPLY ONLY: SAUCE") # ``prompt`` is preserved as the user wrote it — only # the translator picks one over the other. self.assertEqual(a.prompt, "dummy") def test_instructions_inline_text_when_no_matching_file(self): """A value that doesn't match any sibling file is treated as inline. Matches the native Omnigent behavior — silent fall-through to inline avoids breaking specs whose authors typed an instruction that happens to look pathy. """ yaml_content = "name: instr_agent\nprompt: dummy\ninstructions: this is just inline text\n" with tempfile.TemporaryDirectory() as td: yaml_path = Path(td) / "agent.yaml" yaml_path.write_text(yaml_content) a = load_agent_def(str(yaml_path)) self.assertEqual(a.instructions, "this is just inline text") def test_instructions_multiline_string_treated_as_inline(self): """Multi-line values can't be paths; treated as inline.""" yaml_content = ( "name: instr_agent\nprompt: dummy\ninstructions: |\n line one\n line two\n" ) with tempfile.TemporaryDirectory() as td: yaml_path = Path(td) / "agent.yaml" yaml_path.write_text(yaml_content) a = load_agent_def(str(yaml_path)) self.assertIn("line one", a.instructions or "") self.assertIn("line two", a.instructions or "") def test_instructions_absent_yields_none(self): """No ``instructions:`` key in YAML → ``a.instructions is None``. Catches a regression where the loader mistakenly populates a default that overrides ``prompt:``. """ yaml_content = "name: simple\nprompt: hi\n" with tempfile.TemporaryDirectory() as td: yaml_path = Path(td) / "agent.yaml" yaml_path.write_text(yaml_content) a = load_agent_def(str(yaml_path)) self.assertIsNone(a.instructions) self.assertEqual(a.prompt, "hi") def test_instructions_resolves_relative_to_yaml_dir_not_cwd(self): """Path resolution anchors on the YAML's parent dir, not os.getcwd(). A user can run ``omnigent run /elsewhere/agent.yaml`` from anywhere; the ``instructions: AGENTS.md`` reference must find ``/elsewhere/AGENTS.md``, not ``./AGENTS.md`` from the user's current working directory. """ yaml_content = "name: instr_agent\nprompt: dummy\ninstructions: AGENTS.md\n" with tempfile.TemporaryDirectory() as outer: inner = Path(outer) / "agent_dir" inner.mkdir() yaml_path = inner / "agent.yaml" yaml_path.write_text(yaml_content) (inner / "AGENTS.md").write_text("FROM INNER DIR") # Decoy at the outer dir to make sure we're not just # finding the wrong AGENTS.md. (Path(outer) / "AGENTS.md").write_text("FROM OUTER DIR") original_cwd = os.getcwd() try: os.chdir(outer) # cwd is OUTER but YAML is in INNER a = load_agent_def(str(yaml_path)) finally: os.chdir(original_cwd) self.assertEqual(a.instructions, "FROM INNER DIR") def test_instructions_from_dict_input_treated_as_inline(self): """Loading from a raw dict has no path anchor → inline only. Tools that synthesize an AgentDef from a dict (rather than a file) shouldn't pretend to do path resolution; the ``instructions:`` value is whatever string the caller passed. """ a = load_agent_def({"name": "x", "prompt": "p", "instructions": "AGENTS.md"}) # No file resolution happens here; the value is the literal # string the caller provided. self.assertEqual(a.instructions, "AGENTS.md") def test_instructions_rejects_path_traversal() -> None: """An out-of-bundle ``instructions:`` reference is treated as inline text. Mirrors the parser guard (W7 spec-injection): a YAML setting ``instructions: ../secret.txt`` must not make the loader read a file outside the YAML's own directory and fold it into the agent prompt. The value falls back to literal text so the file's contents never leak into ``instructions``. A regression here would surface the secret body. """ with tempfile.TemporaryDirectory() as td: root = Path(td) (root / "secret.txt").write_text("TOP SECRET RUNNER FILE") bundle = root / "bundle" bundle.mkdir() yaml_path = bundle / "agent.yaml" yaml_path.write_text("name: evil\nprompt: dummy\ninstructions: ../secret.txt\n") a = load_agent_def(str(yaml_path)) # The out-of-root target is never read — its contents must not leak. assert "TOP SECRET" not in (a.instructions or "") # Falls back to the literal value (the existing inline-text path). assert a.instructions == "../secret.txt" class TestLoaderOsEnvValidation(unittest.TestCase): """Validate that ``inner.loader`` mirrors Omnigent parser sandbox checks. The legacy ``load_agent_def`` is what the CLI ``omnigent run`` actually invokes (via the omnigent-compat shim). If the legacy loader silently accepts a misconfigured sandbox (egress_rules on a non-enforcing backend, start_in_scratch without an active sandbox, etc.), the user gets a spec that **looks** hardened but isn't — the same class of bug as the recent terminal egress-rules drop. """ def test_load_agent_def_preserves_egress_allow_private_destinations(self): yaml_content = """ name: t prompt: hi os_env: type: caller_process sandbox: type: darwin_seatbelt egress_rules: - "* api.github.com/repos/org/**" egress_allow_private_destinations: true """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: agent = load_agent_def(f.name) finally: os.unlink(f.name) self.assertIsNotNone(agent.os_env) sb = agent.os_env.sandbox self.assertIsNotNone(sb) self.assertEqual( sb.egress_rules, ["* api.github.com/repos/org/**"], ) # Regression: loader used to silently drop this field. self.assertEqual(sb.egress_allow_private_destinations, True) def test_load_agent_def_rejects_egress_rules_on_none(self): yaml_content = """ name: t prompt: hi os_env: type: caller_process sandbox: type: none egress_rules: - "* api.github.com/**" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: with self.assertRaisesRegex(ValueError, "egress_rules requires"): load_agent_def(f.name) finally: os.unlink(f.name) def test_load_agent_def_rejects_start_in_scratch_with_sandbox_none(self): yaml_content = """ name: t prompt: hi os_env: type: caller_process start_in_scratch: true sandbox: type: none """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: with self.assertRaisesRegex(ValueError, "start_in_scratch requires"): load_agent_def(f.name) finally: os.unlink(f.name) def test_load_agent_def_rejects_start_in_scratch_with_fork(self): yaml_content = """ name: t prompt: hi os_env: type: caller_process fork: true start_in_scratch: true """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: with self.assertRaisesRegex(ValueError, "mutually exclusive"): load_agent_def(f.name) finally: os.unlink(f.name) def test_load_agent_def_rejects_allow_sandbox_override_with_egress_rules(self): """A terminal that inherits egress_rules cannot also allow the LLM to override sandbox.type. An override to ``"none"`` cannot hard-enforce egress, so an override would leave ``egress_rules`` on the policy as inert decoration while the LLM bypassed the network allow-list. """ yaml_content = """ name: t prompt: hi os_env: type: caller_process sandbox: type: darwin_seatbelt egress_rules: - "* api.github.com/**" terminals: shell: command: zsh os_env: inherit allow_sandbox_override: true """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: with self.assertRaisesRegex( ValueError, "allow_sandbox_override.*incompatible.*egress_rules" ): load_agent_def(f.name) finally: os.unlink(f.name) def test_load_agent_def_rejects_allow_sandbox_override_with_own_egress_rules(self): """Same rule applies when the terminal carries its own egress_rules, not just when it inherits the agent's sandbox. """ yaml_content = """ name: t prompt: hi terminals: shell: command: zsh allow_sandbox_override: true os_env: type: caller_process sandbox: type: darwin_seatbelt egress_rules: - "* api.github.com/**" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: with self.assertRaisesRegex( ValueError, "allow_sandbox_override.*incompatible.*egress_rules" ): load_agent_def(f.name) finally: os.unlink(f.name) def test_load_agent_def_rejects_non_bool_egress_allow_private_destinations(self): yaml_content = """ name: t prompt: hi os_env: type: caller_process sandbox: type: darwin_seatbelt egress_allow_private_destinations: "yes" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: with self.assertRaisesRegex(TypeError, "must be a boolean"): load_agent_def(f.name) finally: os.unlink(f.name) def test_load_agent_def_parses_credential_proxy(self): """Single-file omnigent YAML must parse ``credential_proxy``. Regression: this loader (the path ``omnigent run agent.yaml`` takes, distinct from the bundle ``parse(config.yaml)`` path) had no ``credential_proxy`` parsing, so the field was silently dropped and the secretless proxy never armed even though the YAML declared it. We assert the entry actually reaches the spec with the right host/scheme/injection — not merely that the sandbox is non-None — because a dropped field would leave ``credential_proxy`` as ``None`` while everything else parsed. """ yaml_content = """ name: t prompt: hi os_env: type: caller_process sandbox: type: darwin_seatbelt egress_rules: - "* corp.example.com/**" credential_proxy: - type: https_bearer target: corp.example.com/rest source: {env: CORP} env: CORP_TOKEN """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: agent = load_agent_def(f.name) finally: os.unlink(f.name) proxy = agent.os_env.sandbox.credential_proxy self.assertIsNotNone(proxy) # The YAML declares exactly one credential_proxy binding; a # different count would mean the loader dropped it (the original # bug) or duplicated it. self.assertEqual(len(proxy.entries), 1) entry = proxy.entries[0] self.assertEqual(entry.host, "corp.example.com") self.assertEqual(entry.scheme, "bearer") self.assertEqual(entry.inject_env, ["CORP_TOKEN"]) self.assertEqual(entry.source.kind, "env") self.assertEqual(entry.source.env, "CORP") def test_load_agent_def_rejects_credential_proxy_without_egress_rules(self): """``credential_proxy`` without ``egress_rules`` is rejected here too. The MITM egress proxy (driven by egress_rules) is what performs the synthetic->real swap and blocks placeholder leaks; without it the proxy injects a placeholder the agent can't use. The loader must fail loud rather than hand back an inert, half-wired policy — mirroring the bundle parser guard so the two paths can't drift. """ yaml_content = """ name: t prompt: hi os_env: type: caller_process sandbox: type: darwin_seatbelt credential_proxy: - type: git_https target: github.com source: {env: GH_PAT} """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: with self.assertRaisesRegex(ValueError, r"requires os_env\.sandbox\.egress_rules"): load_agent_def(f.name) finally: os.unlink(f.name) def test_load_agent_def_rejects_credential_proxy_on_soft_backend(self): """``credential_proxy`` requires a network-isolating backend. On a soft backend (here ``none``) the egress proxy is not the only way out, so binding credentials to it is unsafe. We omit ``egress_rules`` so the backend guard (checked first) is the one that fires, isolating the credential_proxy-specific backend requirement rather than the generic egress-rules backend guard. """ yaml_content = """ name: t prompt: hi os_env: type: caller_process sandbox: type: none credential_proxy: - type: git_https target: github.com source: {env: GH_PAT} """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: with self.assertRaisesRegex( ValueError, r"credential_proxy requires sandbox\.type" ): load_agent_def(f.name) finally: os.unlink(f.name) def test_load_agent_def_rejects_gh_basic_on_macos(self): """Single-file YAML rejects ``gh_basic`` on macOS too. ``gh_basic`` wires the GitHub CLI (a Go binary); Go on macOS ignores SSL_CERT_FILE and verifies TLS via the keychain, so it rejects the egress MITM CA and every ``gh`` call fails at runtime with ``certificate is not trusted``. The single-file loader (the ``omnigent run agent.yaml`` path) must fail loud at load time with the same explanation as the bundle parser — sharing one detection helper so the two paths can't drift. """ yaml_content = """ name: t prompt: hi os_env: type: caller_process sandbox: type: darwin_seatbelt egress_rules: - "* github.com/**" - "* api.github.com/**" credential_proxy: - type: gh_basic source: {env: GH_PAT} """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() try: with self.assertRaisesRegex(ValueError, r"gh_basic' does not work on macOS"): load_agent_def(f.name) finally: os.unlink(f.name) def test_factory_params_with_unresolvable_handler_does_not_crash() -> None: """factory_params + a handler that cannot be imported must not raise. Previously the loader passed ``factory_params`` to ``FunctionPolicy`` even when ``_resolve_callable`` returned ``None`` (import error), which tripped ``FunctionPolicy.__post_init__``'s ``"factory_params requires factory"`` invariant check. The loader's contract is to silently handle missing callables — the error only surfaces when the policy is *evaluated*, not when the YAML is *loaded*. """ agent = load_agent_def( { "name": "t", "policies": { "broken_policy": { "type": "function", "handler": "this.module.does.not.exist.fn", "factory_params": {"limit": 5}, } }, } ) # Must load without raising — the callable is None (import failed) # but the FunctionPolicy is constructed without factory_params so # __post_init__ does not raise. policy = agent.policies["broken_policy"] assert isinstance(policy, FunctionPolicy) assert policy.callable is None assert policy.factory is None # factory_params must be cleared when the factory could not be # resolved — keeping them with factory=None would re-trigger the # invariant on the next __copy__ or serialization path. assert policy.factory_params == {} def test_load_agent_def_allows_custom_handler_by_default() -> None: """Trusted loading (the default) keeps supporting custom handlers. This is the operator/local ``omnigent run`` path — the custom FunctionPolicy feature. An unregistered, non-built-in handler must load without error when ``enforce_handler_allowlist`` is not set, so the bundle guard does not regress local custom policies. """ agent = load_agent_def( { "name": "t", "policies": { "custom": { "type": "function", "handler": "my.org.custom_policy.rate_limit", "factory_params": {"limit": 5}, } }, } ) assert "custom" in agent.policies def test_load_agent_def_enforce_rejects_unregistered_handler() -> None: """``enforce_handler_allowlist=True`` rejects an unregistered handler. This is the untrusted bundle-upload path. Rejection happens before ``_parse_agent_def`` resolves or calls the factory, so an uploaded ``subprocess.Popen`` policy never executes. """ with pytest.raises(ValueError, match=r"not a registered policy handler"): load_agent_def( { "name": "t", "policies": { "rce": { "type": "function", "handler": "subprocess.Popen", "factory_params": {"args": ["id"]}, } }, }, enforce_handler_allowlist=True, ) def test_load_agent_def_enforce_allows_registered_handler() -> None: """``enforce_handler_allowlist=True`` still allows a registered handler. A built-in registry handler passes the upload allowlist, so the guard does not over-block legitimate bundles. """ agent = load_agent_def( { "name": "t", "policies": { "ask_os": { "type": "function", "handler": "omnigent.policies.builtins.safety.ask_on_os_tools", } }, }, enforce_handler_allowlist=True, ) assert "ask_os" in agent.policies if __name__ == "__main__": unittest.main()