mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-28 16:52:22 +01:00
- Enforce vision probe SSRF safe local host validation, SafeProbeBackend TOCTOU elimination, and session owner authorization - Authorize session titling endpoints against registered owner configuration - Sanitize sensitive metadata logging from agent loop debug logs - Add async non-blocking execution for spec router tools and thinking decisions - Harden email cache invalidation fencing and thread-safe mutation - Add regression test suites for legacy browser authority, spec contracts, spec router request scoping, and task MCP delivery
597 lines
32 KiB
Python
597 lines
32 KiB
Python
"""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, tmp_path):
|
|
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", workspace=str(tmp_path), disabled_tools=disabled,
|
|
security_context=execution.NO_TOOL_SECURITY_CONTEXT,
|
|
request_authority=authority("bash", workspace=str(tmp_path)))
|
|
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"})
|
|
# Legacy authority-only snapshots have no exact job generation to restore.
|
|
with pytest.raises(ValueError):
|
|
save_background_authority("job1", grant)
|
|
restored = restore_background_authority("job1", owner="alice", session_id="s")
|
|
assert restored.grants == ()
|
|
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.grants == () # A launch double returning an ID cannot publish authority.
|
|
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, tmp_path):
|
|
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")
|
|
from src.agent_runtime.resources import ProcessLaunchScope, FilesystemRoot, NativeBackendResource
|
|
from src.containment import DEFAULT_REQUIRED
|
|
original = replace(original, launch_scopes=(ProcessLaunchScope(NativeBackendResource("bash"),
|
|
FilesystemRoot.seal(tmp_path), DEFAULT_REQUIRED),))
|
|
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, tmp_path):
|
|
from src import agent_loop
|
|
from src.task_scheduler import TaskScheduler
|
|
from tests.helpers.database import disposable_database
|
|
import core.database as database
|
|
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")
|
|
with disposable_database(tmp_path) as factory:
|
|
monkeypatch.setattr(database, "SessionLocal", factory)
|
|
with factory() as db:
|
|
db.add(database.ModelEndpoint(id="model", name="model", owner="alice",
|
|
base_url="https://inference.invalid", is_enabled=True))
|
|
db.commit()
|
|
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
|