feat(runtime): enforce server request authority

This commit is contained in:
Alexandre Teixeira
2026-10-01 18:35:41 +01:00
parent 05c6cfcfa1
commit 9d0257134f
37 changed files with 1683 additions and 39 deletions
+27
View File
@@ -2,6 +2,33 @@
from src.agent_runtime.journal import mark_dispatch, record_action
def server_authorized_executor(executor):
"""Give standalone dispatcher fixtures their explicit server grants.
These existing suites exercise handlers, policy, confinement and approvals.
Their fixture grants cover the declared native tool registry, independently
of the proposed block. Request-authority denial tests use the raw dispatcher.
"""
from functools import wraps
from inspect import signature
from src.agent_runtime.authority import OperationGrant, RequestAuthority
from src.tool_policy import known_tool_names
from src.turn_contract import canonical_tool
call_signature = signature(executor)
@wraps(executor)
async def execute(*args, **kwargs):
bound = call_signature.bind(*args, **kwargs)
parameters = bound.arguments
kwargs.setdefault("request_authority", RequestAuthority(
"standalone-test-request", str(parameters.get("owner") or "").strip().casefold(),
str(parameters.get("session_id") or ""), str(parameters.get("workspace") or ""),
tuple(OperationGrant(name) for name in sorted(
{canonical_tool(n) for n in known_tool_names()} | {"list_dir", "find_files"})),
))
return await executor(*args, **kwargs)
return execute
def authoritative_executor(function):
@record_action
async def execute(block, *args, **kwargs):
@@ -333,6 +333,7 @@ def test_external_tool_images_are_threaded_as_multimodal_evidence():
def test_declared_external_call_reaches_scoped_bridge(monkeypatch):
from src.agent_runtime.authority import OperationGrant, RequestAuthority
bridge_calls = []
round_no = 0
monkeypatch.setattr(agent_loop, "get_setting", lambda key, default=None: default)
@@ -367,6 +368,8 @@ def test_declared_external_call_reaches_scoped_bridge(monkeypatch):
[{"role": "user", "content": "Perform the declared operation."}],
max_rounds=2,
owner="pewds",
request_authority=RequestAuthority("declared-fixture", "pewds", "", "",
(OperationGrant("inspect_state"),)),
relevant_tools={"inspect_state"},
forced_tools={"inspect_state"},
fallbacks=[],
@@ -389,6 +392,7 @@ def test_declared_external_call_reaches_scoped_bridge(monkeypatch):
def test_known_native_tool_reaches_scoped_bridge_without_redeclared_schema(monkeypatch):
from src.agent_runtime.authority import create_request_authority
bridge_calls = []
round_no = 0
monkeypatch.setattr(agent_loop, "get_setting", lambda key, default=None: default)
@@ -435,6 +439,7 @@ def test_known_native_tool_reaches_scoped_bridge_without_redeclared_schema(monke
[{"role": "user", "content": "Search email for Project Alpha."}],
max_rounds=2,
owner="public-user",
request_authority=create_request_authority("Search email for Project Alpha.", owner="public-user"),
relevant_tools={"search_emails"},
forced_tools={"search_emails"},
fallbacks=[],
+3
View File
@@ -11,6 +11,9 @@ from src.agent_tools import ToolBlock, TOOL_TAGS # noqa: E402 (import first to
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT, execute_tool_block
from src.tool_index import ALWAYS_AVAILABLE, BUILTIN_TOOL_DESCRIPTIONS
from src.tool_security import is_public_blocked_tool
from tests.runtime_evidence_helpers import server_authorized_executor
execute_tool_block = server_authorized_executor(execute_tool_block)
def _run(content):
+2 -1
View File
@@ -14,6 +14,7 @@ from routes.chat_routes import _agent_turn_cwd
import routes.chat_routes as chat_routes
from src import tool_execution as _te
from src.agent_loop import _is_explicit_local_network_request
from tests.runtime_evidence_helpers import server_authorized_executor
# Hold module-object references (not just from-imported names): other test
# modules re-import src.tool_execution via sys.modules pops, so string-target
@@ -28,7 +29,7 @@ _ROUTED_BRIDGE_TOOLS = _te._ROUTED_BRIDGE_TOOLS
async def _execute_tool_block_for_unit_tests(*args, **kwargs):
"""Use the explicit non-security-context test mode for dispatch tests."""
kwargs.setdefault("security_context", _te.NO_TOOL_SECURITY_CONTEXT)
return await _te.execute_tool_block(*args, **kwargs)
return await server_authorized_executor(_te.execute_tool_block)(*args, **kwargs)
execute_tool_block = _execute_tool_block_for_unit_tests
+8
View File
@@ -4,6 +4,14 @@ import os
import tempfile
import pytest
from tests.runtime_evidence_helpers import server_authorized_executor
@pytest.fixture(autouse=True)
def standalone_dispatch_authority(monkeypatch):
from src import tool_execution
monkeypatch.setattr(tool_execution, "execute_tool_block",
server_authorized_executor(tool_execution.execute_tool_block))
from src import tool_security
from src.tool_security import (
+4
View File
@@ -1,6 +1,7 @@
import asyncio
import logging
import src.tool_execution as tool_execution
from tests.runtime_evidence_helpers import server_authorized_executor
from src.tool_execution import (
AgentExecutionBridge,
@@ -11,6 +12,9 @@ from src.tool_execution import (
)
execute_tool_block = server_authorized_executor(execute_tool_block)
class Block:
tool_type = "host_shell"
+8
View File
@@ -7,6 +7,14 @@ from collections import namedtuple
from pathlib import Path
import pytest
from tests.runtime_evidence_helpers import server_authorized_executor
@pytest.fixture(autouse=True)
def standalone_dispatch_authority(monkeypatch):
from src import tool_execution
monkeypatch.setattr(tool_execution, "execute_tool_block",
server_authorized_executor(tool_execution.execute_tool_block))
from tests.helpers.document_source import document_source
from tests.helpers.js_modules import email_library_paths
+4 -2
View File
@@ -125,6 +125,8 @@ async def test_generation_dispatch_uses_owner_aware_backend(monkeypatch, exit_co
from types import SimpleNamespace
from src import ai_interaction, tool_execution
from src.agent_runtime.journal import ActionJournal, bind_journal
from tests.runtime_evidence_helpers import server_authorized_executor
execute = server_authorized_executor(tool_execution.execute_tool_block)
calls = []
async def generate(content, **kwargs):
calls.append((content, kwargs))
@@ -140,9 +142,9 @@ async def test_generation_dispatch_uses_owner_aware_backend(monkeypatch, exit_co
block = SimpleNamespace(tool_type='generate_image', content='{"prompt":"A city"}')
journal = ActionJournal()
with bind_journal(journal):
_, denied = await tool_execution.execute_tool_block(block, owner='pewds', session_id='fixture',
_, denied = await execute(block, owner='pewds', session_id='fixture',
disabled_tools={'generate_image'}, security_context=tool_execution.NO_TOOL_SECURITY_CONTEXT)
_, result = await tool_execution.execute_tool_block(block, owner='pewds', session_id='fixture',
_, result = await execute(block, owner='pewds', session_id='fixture',
security_context=tool_execution.NO_TOOL_SECURITY_CONTEXT)
assert denied['exit_code'] != 0
assert journal.actions[0].execution_id is None
+584
View File
@@ -0,0 +1,584 @@
"""Request grants are independent of tool offerings and model proposals."""
import asyncio
from contextlib import nullcontext
from dataclasses import replace
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from src.agent_runtime.authority import (
MISSING_AUTHORITY, ExactOperation, OperationGrant, RequestAuthority,
active_request_authority, bind_request_authority, create_request_authority,
restore_background_authority, restore_task_authority, save_background_authority,
seal_task_authority, task_operation, with_request_authority,
is_internal_tool_request, require_user_approval_request,
request_authority_for_http,
)
from src.tool_policy import ToolPolicy
from src.tool_types import ToolBlock
def authority(*tools, owner="alice", session_id="s", workspace=""):
return RequestAuthority("request-test", owner, session_id, workspace,
tuple(OperationGrant(tool) for tool in tools))
@pytest.mark.asyncio
@pytest.mark.parametrize("offering", ["schema", "bridge", "dynamic"])
async def test_availability_and_model_selection_do_not_grant_execution(monkeypatch, offering):
from src import tool_execution as execution
from src.turn_contract import bind_turn_contract, resolve_turn_contract
implementation = AsyncMock(return_value=("bash", {"exit_code": 0}))
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
offered_handler = AsyncMock()
if offering == "dynamic":
import src.agent_tools
monkeypatch.setitem(src.agent_tools.TOOL_HANDLERS, "bash", offered_handler)
bridge_context = execution.bind_execution_bridge(execution.AgentExecutionBridge(
route_tool=offered_handler, supported_tools=frozenset({"bash"}))) if offering == "bridge" else nullcontext()
contract = resolve_turn_contract(capabilities={"shell_files"}, policy=ToolPolicy(),
schemas=[{"function": {"name": "bash"}}])
with bind_turn_contract(contract), bridge_context:
description, result = await execution.execute_tool_block(
ToolBlock("bash", "echo 'authorized by model'"), owner="alice", session_id="s",
security_context=execution.NO_TOOL_SECURITY_CONTEXT,
request_authority=authority("transcribe_media"))
assert "BLOCKED" in description
assert result["failure_kind"] == "request_authority_denied"
implementation.assert_not_awaited()
offered_handler.assert_not_awaited()
@pytest.mark.parametrize("user_text,allowed,denied", [
("Transcribe /workspace/input/audio.wav", "transcribe_media", "bash"),
("OCR extract exact text from /workspace/input/image.png", "extract_text", "python"),
("List my tasks", "manage_tasks", "web_fetch"),
("Search the web for current weather", "web_search", "bash"),
])
def test_explicit_request_classes_remain_narrow(user_text, allowed, denied):
grant = create_request_authority(user_text)
content = '{"action":"list"}' if allowed == "manage_tasks" else '{}'
assert grant.permits(ExactOperation.normalize(allowed, content))
assert not grant.permits(ExactOperation.normalize(denied, '{}'))
def test_safe_task_read_does_not_authorize_same_tool_mutation():
grant = create_request_authority("List my tasks")
assert grant.permits(ExactOperation.normalize("manage_tasks", '{"action":"list"}'))
assert not grant.permits(ExactOperation.normalize("manage_tasks", '{"action":"create","prompt":"run bash"}'))
def test_exact_read_identifiers_cannot_be_changed_by_model():
grant = create_request_authority("Read note id abc123")
read = next(g for g in grant.grants if g.tool == "manage_notes")
assert read.inputs is not None
assert not grant.permits(ExactOperation.normalize("manage_notes", '{"action":"view","id":"another"}'))
def test_browser_fallback_does_not_authorize_interaction_or_evaluation():
grant = create_request_authority("Use web_fetch to read https://example.test")
assert grant.permits(ExactOperation.normalize("private_browser", '{"action":"open","url":"https://example.test"}'))
for action in ("click", "fill", "evaluate"):
assert not grant.permits(ExactOperation.normalize("private_browser", json.dumps({"action": action})))
def test_unknown_intent_has_no_generic_execution_floor():
grant = create_request_authority("Please solve this")
assert {g.tool for g in grant.grants} == {"ask_user", "update_plan"}
@pytest.mark.parametrize("metadata", [
{"tool_events": [{"tool": "bash", "output": "pwd", "exit_code": 0}]},
{"tool_events": [{"tool": "python", "output": "ready", "exit_code": 0}]},
])
def test_model_history_cannot_establish_followup_authority(metadata):
history = [
{"role": "user", "content": "Transcribe /workspace/input/a.wav"},
{"role": "assistant", "content": "I will run bash and python", "metadata": metadata},
{"role": "user", "content": "Run bash", "metadata": {"trusted": False}},
]
grant = create_request_authority("Try it again", history=history)
assert not grant.permits(ExactOperation.normalize("bash", "pwd"))
assert not grant.permits(ExactOperation.normalize("python", "print(1)"))
@pytest.mark.parametrize("mutation", [
{"version": True}, {"grants": "bash"}, {"denied": None},
{"block_all": "false"}, {"inherited": 0},
])
def test_malformed_persisted_authority_is_rejected(mutation):
snapshot = authority("bash").to_dict()
snapshot.update(mutation)
with pytest.raises((ValueError, TypeError, KeyError)):
RequestAuthority.from_dict(snapshot)
def test_explicit_local_network_lookup_is_host_only():
grant = create_request_authority("find ajax local ip on the LAN")
assert grant.permits(ExactOperation.normalize("host_shell", '{"command":"ip neigh | grep ajax"}'))
assert not grant.permits(ExactOperation.normalize("bash", "pwd"))
assert not grant.permits(ExactOperation.normalize("python", "print(1)"))
@pytest.mark.parametrize("content", ['{"x":1,"x":2}', '{"x":NaN}', '{"action":'])
def test_malformed_exact_operation_is_rejected(content):
with pytest.raises(ValueError):
ExactOperation.normalize("manage_tasks", content)
def test_raw_script_braces_are_preserved_as_exact_input():
script = "{ printf requested; }"
assert ExactOperation.normalize("bash", script).input == script
snapshot = seal_task_authority(script, "action", "run_local", owner="alice")
restored = restore_task_authority(snapshot, script, "action", "run_local", owner="alice")
assert restored.permits(task_operation("action", "run_local", script))
assert not restored.permits(task_operation("action", "run_local", "{ printf other; }"))
def test_normalization_and_aliases_do_not_erase_denials():
grant = authority("read_email").restrict(disabled_tools={"mcp__email__read_email"})
assert not grant.permits(ExactOperation.normalize("read_email", '{}'))
grant = authority("read_email").restrict(ToolPolicy(disable_mcp=True))
assert not grant.permits(ExactOperation.normalize("mcp__email__read_email", '{}'))
assert ExactOperation.normalize("manage_tasks", '{ "action": "list" }').input == '{"action":"list"}'
@pytest.mark.asyncio
@pytest.mark.parametrize("state", [MISSING_AUTHORITY, None, {}, "authorized"])
async def test_missing_and_malformed_dispatch_authority_fail_closed(monkeypatch, state):
from src import tool_execution as execution
implementation = AsyncMock()
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
_, result = await execution.execute_tool_block(ToolBlock("bash", "pwd"),
security_context=execution.NO_TOOL_SECURITY_CONTEXT, request_authority=state)
assert result["blocked"] is True
implementation.assert_not_awaited()
@pytest.mark.asyncio
async def test_dispatch_checks_grants_and_current_disabled_policy(monkeypatch):
from src import tool_execution as execution
implementation = AsyncMock(return_value=("bash", {"exit_code": 0}))
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
for disabled in (set(), {"bash"}):
_, result = await execution.execute_tool_block(ToolBlock("bash", "pwd"),
owner="alice", session_id="s", disabled_tools=disabled,
security_context=execution.NO_TOOL_SECURITY_CONTEXT,
request_authority=authority("bash"))
assert result["exit_code"] == (1 if disabled else 0)
assert implementation.await_count == 1
@pytest.mark.asyncio
async def test_nested_stream_intersects_and_restores_parent_on_close():
seen = []
@with_request_authority
async def child(messages, request_authority=MISSING_AUTHORITY, owner="alice", session_id="s"):
seen.append(active_request_authority())
yield "child"
parent = authority("transcribe_media")
with bind_request_authority(parent):
stream = child([{"role": "user", "content": "Run bash"}],
request_authority=authority("bash", "transcribe_media"))
assert await anext(stream) == "child"
assert not seen[0].permits(ExactOperation.normalize("bash", "pwd"))
assert seen[0].permits(ExactOperation.normalize("transcribe_media", '{}'))
await stream.aclose()
assert active_request_authority() is parent
assert active_request_authority() is None
@pytest.mark.asyncio
async def test_retries_and_provider_changes_do_not_recreate_authority():
seen = []
@with_request_authority
async def run(messages, request_authority=MISSING_AUTHORITY, owner="alice", session_id="s"):
for proposed in ("transcribe_media", "bash", "python"):
seen.append(active_request_authority())
yield active_request_authority().permits(ExactOperation.normalize(proposed, '{}'))
assert [x async for x in run([{"role": "user", "content": "Transcribe /workspace/input/a.wav"}])] == [True, False, False]
assert all(value is seen[0] for value in seen)
def test_task_snapshot_caps_model_payload_and_rejects_changed_or_missing_state():
parent = authority("manage_tasks")
with bind_request_authority(parent):
snapshot = seal_task_authority("Run bash in the workspace", "llm", None, owner="alice")
restored = restore_task_authority(snapshot, "Run bash in the workspace", "llm", None, owner="alice")
assert not restored.permits(ExactOperation.normalize("bash", "pwd"))
for state in (None, "{}", snapshot):
changed = restore_task_authority(state, "a different prompt", "llm", None, owner="alice")
assert changed.grants == ()
def test_direct_task_ingress_seals_only_exact_builtin_action():
snapshot = seal_task_authority("printf requested", "action", "run_local", owner="alice")
restored = restore_task_authority(snapshot, "printf requested", "action", "run_local", owner="alice")
assert restored.permits(task_operation("action", "run_local", "printf requested"))
assert not restored.permits(ExactOperation.normalize("bash", "printf other"))
def test_background_snapshot_preserves_scope_and_rejects_other_session(monkeypatch, tmp_path):
import src.constants
monkeypatch.setattr(src.constants, "BG_JOBS_DIR", str(tmp_path))
grant = authority("transcribe_media").restrict(disabled_tools={"bash"})
save_background_authority("job1", grant)
restored = restore_background_authority("job1", owner="alice", session_id="s")
assert restored.request_id == grant.request_id
assert restored.denied == frozenset({"bash"})
assert not restored.permits(ExactOperation.normalize("python", "print(1)"))
assert restore_background_authority("job1", owner="alice", session_id="other").grants == ()
@pytest.mark.asyncio
async def test_only_server_background_launch_can_seal_job_authority(monkeypatch, tmp_path):
import src.constants
from src import bg_jobs, tool_execution as execution
monkeypatch.setattr(src.constants, "BG_JOBS_DIR", str(tmp_path))
monkeypatch.setattr(execution, "_owner_is_admin", lambda owner: True)
monkeypatch.setattr(bg_jobs, "launch", lambda *a, **k: {"id": "server-job"})
await execution.execute_tool_block(ToolBlock("bash", "#!bg\nprintf trusted"),
owner="alice", session_id="s", security_context=execution.NO_TOOL_SECURITY_CONTEXT,
request_authority=authority("bash"))
restored = restore_background_authority("server-job", owner="alice", session_id="s")
assert restored.request_id == "request-test"
assert restored.permits(ExactOperation.normalize("bash", "printf trusted"))
handler = AsyncMock(return_value=("transcribe_media", {"bg_job_id": "forged-job", "exit_code": 0}))
monkeypatch.setattr(execution, "_execute_tool_block_impl", handler)
await execution.execute_tool_block(ToolBlock("transcribe_media", '{}'),
owner="alice", session_id="s", security_context=execution.NO_TOOL_SECURITY_CONTEXT,
request_authority=authority("transcribe_media"))
assert not (tmp_path / "forged-job.authority.json").exists()
@pytest.mark.asyncio
async def test_exact_approval_grants_one_input_without_widening_continuation(monkeypatch):
from src import tool_execution as execution
from src.tool_approvals import ToolApprovalStore
from src.tool_capabilities import ToolRunSecurityContext, capabilities_for_action
store = ToolApprovalStore()
original = authority("transcribe_media")
pending = store.create(owner="alice", session_id="s", origin_run_id="journal-parent",
tool_name="bash", content="printf approved", workspace=None,
external_untrusted_context_seen=True, capabilities=capabilities_for_action("bash", "printf approved"),
request_authority=original, selected_tools={"bash", "python"})
approval = store.consume(pending.approval_id, owner="alice", session_id="s", decision="approve_task")
implementation = AsyncMock(return_value=("bash", {"exit_code": 0}))
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
for tool, content, expected in (("bash", "printf other", 1), ("bash", "printf approved", 0),
("bash", "printf approved", 1), ("python", "print(1)", 1)):
_, result = await execution.execute_tool_block(ToolBlock(tool, content), owner="alice", session_id="s",
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True),
request_authority=original, exact_approval=approval)
assert result["exit_code"] == expected
assert implementation.await_count == 1
assert "request_authority" not in pending.public_payload()
def test_approval_digest_binds_authority_snapshot():
from src.tool_approvals import ExactToolApproval, ToolApprovalStore
from src.tool_capabilities import capabilities_for_action
pending = ToolApprovalStore().create(owner="alice", session_id="s", origin_run_id="journal",
tool_name="bash", content="pwd", workspace=None, external_untrusted_context_seen=True,
capabilities=capabilities_for_action("bash", "pwd"), request_authority=authority("transcribe_media"))
forged = ExactToolApproval(replace(pending, request_authority=authority("bash", "python")))
assert not forged.matches(owner="alice", session_id="s", workspace=None, tool_name="bash", content="pwd")
@pytest.mark.asyncio
async def test_nested_approval_cannot_cross_parent_class_ceiling(monkeypatch):
from src import tool_execution as execution
from src.tool_approvals import ToolApprovalStore
from src.tool_capabilities import ToolRunSecurityContext, capabilities_for_action
store = ToolApprovalStore()
pending = store.create(owner="alice", session_id="s", origin_run_id="parent",
tool_name="bash", content="pwd", workspace=None, external_untrusted_context_seen=True,
capabilities=capabilities_for_action("bash", "pwd"))
approval = store.consume(pending.approval_id, owner="alice", session_id="s", decision="approve")
implementation = AsyncMock()
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
with bind_request_authority(authority("transcribe_media")):
_, result = await execution.execute_tool_block(ToolBlock("bash", "pwd"), owner="alice", session_id="s",
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True),
request_authority=authority("bash"), exact_approval=approval)
assert result["blocked"] is True
implementation.assert_not_awaited()
@pytest.mark.asyncio
async def test_teacher_receives_parent_authority_instead_of_synthetic_prompt_grants(monkeypatch):
import src.agent_loop as loop
import src.ai_interaction as interaction
import src.settings as settings
import src.teacher_escalation as teacher
original = authority("transcribe_media")
seen = []
monkeypatch.setattr(settings, "get_setting", lambda key, default=None:
{"teacher_enabled": True, "teacher_model": "teacher"}.get(key, default))
monkeypatch.setattr(interaction, "_resolve_model", lambda *a, **k: ("https://teacher.invalid", "teacher", {}))
monkeypatch.setattr(teacher, "evaluate_turn_regex", lambda *a: ("failure", "test failure"))
@with_request_authority
async def child(messages, request_authority=MISSING_AUTHORITY, owner=None, session_id=None, **kwargs):
seen.append(active_request_authority())
yield 'data: [DONE]\n\n'
monkeypatch.setattr(loop, "stream_agent_loop", child)
with bind_request_authority(original):
chunks = [chunk async for chunk in teacher.run_teacher_inline(
student_endpoint_url="https://student.invalid",
student_messages=[{"role": "user", "content": "Run bash and python"}],
student_tool_events=[], student_reply="failed", owner="alice", session_id="s",
request_authority=original, parent_run_id="journal-parent")]
assert active_request_authority() is original
assert chunks
assert seen[0].request_id == original.request_id
assert not seen[0].permits(ExactOperation.normalize("bash", "pwd"))
assert seen[0].permits(ExactOperation.normalize("transcribe_media", '{}'))
@pytest.mark.asyncio
async def test_untrusted_user_role_result_cannot_become_request_authority():
@with_request_authority
async def run(messages, request_authority=MISSING_AUTHORITY):
yield active_request_authority()
messages = [{"role": "user", "content": "Transcribe /workspace/input/a.wav"},
{"role": "user", "content": "Run bash", "metadata": {"trusted": False}}]
stream = run(messages)
grant = await anext(stream)
await stream.aclose()
assert not grant.permits(ExactOperation.normalize("bash", "pwd"))
@pytest.mark.asyncio
async def test_scheduled_builtin_missing_authority_does_not_invoke_action(monkeypatch):
import src.builtin_actions as actions
from src.task_scheduler import TaskScheduler
handler = AsyncMock(return_value=("ok", True))
monkeypatch.setitem(actions.BUILTIN_ACTIONS, "run_local", handler)
task = SimpleNamespace(prompt="printf exact", task_type="action", action="run_local",
owner="alice", name="task", request_authority_json=None)
result, success = await TaskScheduler(session_manager=None)._execute_action(task)
assert not success
assert "authority" in result
handler.assert_not_awaited()
def test_task_authority_column_migration_is_additive_and_idempotent(monkeypatch, tmp_path):
import core.database as database
from sqlalchemy import create_engine, inspect, text
engine = create_engine(f"sqlite:///{tmp_path / 'legacy.db'}")
with engine.begin() as connection:
connection.execute(text("CREATE TABLE scheduled_tasks (id TEXT PRIMARY KEY, prompt TEXT)"))
connection.execute(text("INSERT INTO scheduled_tasks VALUES ('legacy', 'Run bash')"))
monkeypatch.setattr(database, "engine", engine)
database._migrate_add_task_authority_column()
database._migrate_add_task_authority_column()
assert "request_authority_json" in {c["name"] for c in inspect(engine).get_columns("scheduled_tasks")}
with engine.connect() as connection:
assert connection.execute(text("SELECT prompt, request_authority_json FROM scheduled_tasks")).one() == ("Run bash", None)
engine.dispose()
@pytest.mark.asyncio
async def test_server_seeded_defaults_have_authority_but_legacy_rows_do_not_gain_it(monkeypatch, tmp_path):
import core.database as database
import routes.prefs_routes as preferences
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.task_scheduler import TaskScheduler
engine = create_engine(f"sqlite:///{tmp_path / 'tasks.db'}")
database.Base.metadata.create_all(engine)
sessions = sessionmaker(bind=engine)
monkeypatch.setattr(database, "SessionLocal", sessions)
monkeypatch.setattr(preferences, "_load_for_user", lambda owner: {})
scheduler = TaskScheduler(session_manager=None)
monkeypatch.setattr(scheduler, "ensure_assistant_defaults", AsyncMock())
with sessions() as db:
db.add(database.ScheduledTask(id="legacy", owner="alice", name="Legacy housekeeping",
task_type="action", action="tidy_sessions"))
db.commit()
await scheduler.ensure_defaults("alice")
with sessions() as db:
tasks = db.query(database.ScheduledTask).all()
assert len(tasks) > 1
for task in tasks:
grant = restore_task_authority(task.request_authority_json, task.prompt,
task.task_type, task.action, owner=task.owner)
assert grant.permits(task_operation(task.task_type, task.action, task.prompt)) == (task.id != "legacy")
engine.dispose()
@pytest.mark.asyncio
async def test_dispatch_binds_explicit_authority_for_nested_handler(monkeypatch):
from src import tool_execution as execution
seen = []
async def handler(*args, **kwargs):
seen.append(active_request_authority())
with bind_request_authority(authority("bash", "transcribe_media")) as child:
assert not child.permits(ExactOperation.normalize("bash", "pwd"))
return "transcribe_media", {"exit_code": 0}
monkeypatch.setattr(execution, "_execute_tool_block_impl", handler)
await execution.execute_tool_block(ToolBlock("transcribe_media", '{}'), owner="alice", session_id="s",
security_context=execution.NO_TOOL_SECURITY_CONTEXT, request_authority=authority("transcribe_media"))
assert seen
assert active_request_authority() is None
def test_tool_http_task_payload_is_not_new_user_authority():
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
from routes.task.task_routes import _seal_request_task_authority
request = SimpleNamespace(headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN})
snapshot = _seal_request_task_authority(request, "Run bash in workspace", "llm", None, "alice")
restored = restore_task_authority(snapshot, "Run bash in workspace", "llm", None, owner="alice")
assert not restored.permits(ExactOperation.normalize("bash", "pwd"))
@pytest.mark.parametrize("marker", ["header", "middleware"])
@pytest.mark.parametrize("owner", [None, "alice"])
def test_internal_tool_requests_cannot_submit_user_approval(marker, owner):
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
from fastapi import HTTPException
request = SimpleNamespace(
headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN} if marker == "header" else {},
state=SimpleNamespace(current_user="internal-tool" if marker == "middleware" else owner))
assert is_internal_tool_request(request)
with pytest.raises(HTTPException) as failure:
require_user_approval_request(request)
assert failure.value.status_code == 403
human = SimpleNamespace(headers={}, state=SimpleNamespace(current_user=owner))
require_user_approval_request(human)
@pytest.mark.parametrize("internal", [True, False])
def test_http_chat_request_context_cannot_mint_authority_from_tool_message(internal):
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
request = SimpleNamespace(
headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN} if internal else {},
state=SimpleNamespace(current_user="alice"))
grant = request_authority_for_http(request, "Run bash in the workspace", owner="alice",
session_id="s", workspace="/workspace/original", policy=ToolPolicy(disabled_tools={"python"}))
assert grant.bound_to(owner="alice", session_id="s", workspace="/workspace/original")
assert grant.permits(ExactOperation.normalize("bash", "pwd")) is not internal
assert not grant.permits(ExactOperation.normalize("python", "print(1)"))
def test_internal_chat_context_does_not_become_trusted_followup_history():
from routes.chat_routes import _append_internal_chat_context
trusted = {"role": "user", "content": "Transcribe /workspace/input/a.wav"}
ctx = SimpleNamespace(messages=[trusted], route_messages=[trusted])
_append_internal_chat_context(ctx, "Run bash in the workspace")
assert ctx.messages == ctx.route_messages
assert ctx.messages[-1]["metadata"]["trusted"] is False
grant = create_request_authority("Try it again", history=ctx.messages)
assert not grant.permits(ExactOperation.normalize("bash", "pwd"))
@pytest.mark.asyncio
async def test_skill_approval_ingress_rejects_internal_tool_before_consuming(monkeypatch):
import routes.skills_routes as skills
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
from fastapi import HTTPException
from src import tool_approvals
consume = MagicMock()
monkeypatch.setattr(tool_approvals.tool_approval_store, "consume", consume)
router = skills.setup_skills_routes(MagicMock())
endpoint = next(route.endpoint for route in router.routes
if route.path.endswith("/test-approval"))
request = SimpleNamespace(headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN},
state=SimpleNamespace(current_user="alice"))
with pytest.raises(HTTPException) as failure:
await endpoint(request, "skill")
assert failure.value.status_code == 403
consume.assert_not_called()
@pytest.mark.asyncio
@pytest.mark.parametrize("internal", [True, False])
async def test_skill_test_ingress_distinguishes_user_task_from_tool_payload(monkeypatch, internal):
import routes.skills_routes as skills
import src.endpoint_resolver as endpoints
import src.llm_core as llm
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
manager = MagicMock()
manager.load.return_value = [{"name": "skill", "owner": "alice"}]
manager.read_skill_md.return_value = "# Skill"
monkeypatch.setattr(skills, "get_current_user", lambda request: "alice")
monkeypatch.setattr(skills, "_skill_test_jobs", {})
monkeypatch.setattr(endpoints, "resolve_endpoint", lambda *a, **k: ("https://inference.invalid", "model", {}))
monkeypatch.setattr(llm, "list_model_ids", lambda *a, **k: [])
seen = []
async def run(*args, **kwargs):
seen.append(kwargs["request_authority"])
monkeypatch.setattr(skills, "_run_skill_test_job", run)
router = skills.setup_skills_routes(manager)
endpoint = next(route.endpoint for route in router.routes if route.path.endswith("/{skill_id}/test"))
request = SimpleNamespace(
headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN} if internal else {},
state=SimpleNamespace(current_user="alice"),
json=AsyncMock(return_value={"task": "Run bash in the workspace"}))
result = await endpoint(request, "skill")
await asyncio.sleep(0)
assert result["status"] == "running"
assert len(seen) == 1
assert seen[0].permits(ExactOperation.normalize("bash", "pwd")) is not internal
@pytest.mark.asyncio
async def test_scheduled_override_cannot_replace_stored_authority(monkeypatch):
from src import agent_loop
from src.task_scheduler import TaskScheduler
seen = []
async def fake_stream(*args, **kwargs):
seen.append(kwargs)
yield 'data: {"delta":"done"}\n\n'
yield 'data: [DONE]\n\n'
monkeypatch.setattr(agent_loop, "stream_agent_loop", fake_stream)
task = SimpleNamespace(prompt="Transcribe /workspace/input/a.wav", task_type="llm", action=None,
owner="alice", name="test", max_steps=1)
with bind_request_authority(authority("transcribe_media", workspace="/workspace/original")):
task.request_authority_json = seal_task_authority(task.prompt, "llm", None, owner="alice")
await TaskScheduler(session_manager=None)._run_agent_loop("https://inference.invalid", "test", task, "s",
override_user_message="Run bash and python")
grant = seen[0]["request_authority"]
assert grant.permits(ExactOperation.normalize("transcribe_media", '{}'))
assert not grant.permits(ExactOperation.normalize("bash", "pwd"))
assert grant.workspace == "/workspace/original"
assert seen[0]["workspace"] == grant.workspace
@pytest.mark.asyncio
async def test_loop_retains_denial_before_offered_inventory_reconciliation(monkeypatch):
from src import agent_loop as loop, tool_execution as execution
from src.turn_contract import resolve_turn_contract
implementation = AsyncMock(return_value=("bash", {"exit_code": 0, "output": "unexpected"}))
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
monkeypatch.setattr(loop, "get_setting", lambda key, default=None: default)
monkeypatch.setattr(loop, "get_mcp_manager", lambda: None)
monkeypatch.setattr(loop, "estimate_tokens", lambda *a, **k: 10)
async def fake_stream(*args, **kwargs):
yield 'data: ' + json.dumps({"delta": '```bash\npwd\n```'}) + '\n\n'
monkeypatch.setattr(loop, "stream_llm_with_fallback", fake_stream)
contract = resolve_turn_contract(capabilities={"shell_files"}, selected_tools={"bash"},
schemas=[{"function": {"name": "bash"}}], policy=ToolPolicy())
chunks = [chunk async for chunk in loop.stream_agent_loop(
"https://inference.invalid", "test", [{"role": "user", "content": "Run bash pwd"}],
owner="alice", session_id="s", max_rounds=1, turn_contract=contract, disabled_tools={"bash"})]
implementation.assert_not_awaited()
assert chunks[-1] == 'data: [DONE]\n\n'
assert active_request_authority() is None
@pytest.mark.asyncio
async def test_authority_denial_cannot_create_completion_receipt(monkeypatch):
from src import tool_execution as execution
from src.agent_runtime.journal import ActionJournal, bind_journal
journal = ActionJournal()
with bind_journal(journal):
await execution.execute_tool_block(ToolBlock("bash", "pwd"), owner="alice", session_id="s",
security_context=execution.NO_TOOL_SECURITY_CONTEXT,
request_authority=authority("transcribe_media"))
assert len(journal.actions) == 1
assert journal.actions[0].execution_id is None
assert journal.actions[0].outcome["authoritative"] is False
assert journal.actions[0].outcome["blocked"] is True
+2 -1
View File
@@ -14,9 +14,10 @@ from src.preset_manager import PresetManager
async def _execute_without_run_context(execute_tool_block, *args, **kwargs):
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT
from tests.runtime_evidence_helpers import server_authorized_executor
kwargs.setdefault("security_context", NO_TOOL_SECURITY_CONTEXT)
return await execute_tool_block(*args, **kwargs)
return await server_authorized_executor(execute_tool_block)(*args, **kwargs)
class _FakeColumn:
+8
View File
@@ -6,6 +6,14 @@ import json
import os
import pytest
from tests.runtime_evidence_helpers import server_authorized_executor
@pytest.fixture(autouse=True)
def standalone_dispatch_authority(monkeypatch):
from src import tool_execution
monkeypatch.setattr(tool_execution, "execute_tool_block",
server_authorized_executor(tool_execution.execute_tool_block))
from src.agent_evidence import CompletionRequirements, EvidenceLedger, EvidenceKind
from src.agent_runtime.completion import completion_answer, with_completion_gate
+1 -1
View File
@@ -88,7 +88,7 @@ def builtin_action_info(monkeypatch):
def _req(user):
return SimpleNamespace(state=SimpleNamespace(current_user=user))
return SimpleNamespace(state=SimpleNamespace(current_user=user), headers={})
def _endpoint(method, path):
+4 -1
View File
@@ -25,6 +25,7 @@ def _setup_db(tmp_path, monkeypatch):
next_run = Column(DateTime)
last_run = Column(DateTime)
prompt = Column(Text, default='')
request_authority_json = Column(Text)
trigger_type = Column(String, default='schedule')
schedule = Column(String, default='daily')
scheduled_time = Column(String, default='08:00')
@@ -187,9 +188,11 @@ def test_running_task_cancel_keeps_event_loop_responsive_during_database_lock(tm
from src.task_scheduler import TaskScheduler
from src.builtin_actions import BUILTIN_ACTIONS
monkeypatch.setenv('BACKGROUND_TASK_FOREGROUND_GATE', 'false')
from src.agent_runtime.authority import seal_task_authority
with session_local() as db:
db.add(ScheduledTask(id='running-task', owner='alice', name='Fixture action',
task_type='action', action='fixture_wait', status='active'))
task_type='action', action='fixture_wait', status='active',
request_authority_json=seal_task_authority('', 'action', 'fixture_wait', owner='alice')))
db.commit()
started = asyncio.Event()
async def external_action(**kwargs):
+8
View File
@@ -4,6 +4,14 @@ import time
from collections import namedtuple
import pytest
from tests.runtime_evidence_helpers import server_authorized_executor
@pytest.fixture(autouse=True)
def standalone_dispatch_authority(monkeypatch):
from src import tool_execution
monkeypatch.setattr(tool_execution, "execute_tool_block",
server_authorized_executor(tool_execution.execute_tool_block))
from src.tool_approvals import ToolApprovalStore, document_content_digest
from src.tool_capabilities import ToolRunSecurityContext, capabilities_for_action
+8
View File
@@ -18,6 +18,14 @@ from types import SimpleNamespace
from unittest.mock import patch
import pytest
from tests.runtime_evidence_helpers import server_authorized_executor
@pytest.fixture(autouse=True)
def standalone_dispatch_authority(monkeypatch):
from src import tool_execution
monkeypatch.setattr(tool_execution, "execute_tool_block",
server_authorized_executor(tool_execution.execute_tool_block))
def _make_block(tool_type, content):
+3
View File
@@ -17,6 +17,9 @@ from src.tool_policy import (
web_search_enabled_for_turn,
)
from src.turn_contract import requested_capabilities
from tests.runtime_evidence_helpers import server_authorized_executor
execute_tool_block = server_authorized_executor(execute_tool_block)
def _collect(gen):
+2
View File
@@ -1806,6 +1806,8 @@ def test_candidate_schema_changes_cannot_mutate_contract():
async def test_dispatcher_enforces_bound_contract_without_external_mutations(monkeypatch):
from src import tool_execution, tool_implementations
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT, execute_tool_block
from tests.runtime_evidence_helpers import server_authorized_executor
execute_tool_block = server_authorized_executor(execute_tool_block)
handler = AsyncMock(return_value={"events": [], "exit_code": 0})
monkeypatch.setattr(tool_implementations, "do_manage_calendar", handler)
+8
View File
@@ -4,6 +4,14 @@ import json
from unittest.mock import AsyncMock, Mock
import pytest
from tests.runtime_evidence_helpers import server_authorized_executor
@pytest.fixture(autouse=True)
def standalone_dispatch_authority(monkeypatch):
from src import tool_execution
monkeypatch.setattr(tool_execution, "execute_tool_block",
server_authorized_executor(tool_execution.execute_tool_block))
from src.tool_policy import ToolPolicy
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
+3
View File
@@ -11,6 +11,9 @@ from src.agent_tools import ToolBlock, TOOL_TAGS # import first to avoid circul
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT, execute_tool_block
from src.tool_index import ALWAYS_AVAILABLE, BUILTIN_TOOL_DESCRIPTIONS
from src.tool_security import is_public_blocked_tool
from tests.runtime_evidence_helpers import server_authorized_executor
execute_tool_block = server_authorized_executor(execute_tool_block)
def _run(content):
+3
View File
@@ -10,6 +10,9 @@ from src.tool_policy import WEB_TOOL_NAMES, ToolPolicy
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
from src.tool_types import ToolBlock
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT, execute_tool_block
from tests.runtime_evidence_helpers import server_authorized_executor
execute_tool_block = server_authorized_executor(execute_tool_block)
from src.turn_contract import FAMILY_TOOLS
+4
View File
@@ -17,6 +17,7 @@ import tempfile
from types import SimpleNamespace
import pytest
from tests.runtime_evidence_helpers import server_authorized_executor
from src.tool_execution import (
NO_TOOL_SECURITY_CONTEXT,
@@ -31,6 +32,9 @@ from src.tool_execution import (
)
_execute_tool_block = server_authorized_executor(_execute_tool_block)
async def execute_tool_block(*args, **kwargs):
kwargs.setdefault("security_context", NO_TOOL_SECURITY_CONTEXT)
return await _execute_tool_block(*args, **kwargs)