mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 03:42:20 +02:00
feat(runtime): bind remote and owned resources to authority
This commit is contained in:
@@ -14,16 +14,20 @@ def server_authorized_executor(executor):
|
||||
from src.agent_runtime.authority import OperationGrant, RequestAuthority
|
||||
from src.tool_policy import known_tool_names
|
||||
from src.turn_contract import canonical_tool
|
||||
from src.agent_runtime.remote_resources import seal_backends
|
||||
call_signature = signature(executor)
|
||||
@wraps(executor)
|
||||
async def execute(*args, **kwargs):
|
||||
bound = call_signature.bind(*args, **kwargs)
|
||||
parameters = bound.arguments
|
||||
grants = tuple(OperationGrant(name) for name in sorted(
|
||||
{canonical_tool(n) for n in known_tool_names()} | {"list_dir", "find_files"}))
|
||||
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"})),
|
||||
grants,
|
||||
backend_resources=seal_backends((g.tool for g in grants), context=parameters.get("client_runtime_context"),
|
||||
owner=str(parameters.get("owner") or "").strip().casefold()),
|
||||
))
|
||||
return await executor(*args, **kwargs)
|
||||
return execute
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
"""Server resolution pins record aliases before approval and dispatch."""
|
||||
import asyncio
|
||||
from dataclasses import replace
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from src.agent_runtime.authority import ExactOperation, OperationGrant, RequestAuthority, bind_request_authority
|
||||
from src.agent_runtime.owned_resources import (
|
||||
active_owned_operation, admit_owned_operation, bind_owned_operation,
|
||||
bound_attachment_path, resolve_owned_operation,
|
||||
observe_vault_records,
|
||||
)
|
||||
from src.agent_runtime.remote_resources import active_backend_operation
|
||||
from src.agent_runtime.resources import OwnedScope, ResourceIdentityError
|
||||
from src.tool_approvals import ToolApprovalStore
|
||||
from src.tool_capabilities import ToolRunSecurityContext, capabilities_for_action
|
||||
from src.tool_types import ToolBlock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fresh_vault_observations(monkeypatch):
|
||||
from src.agent_runtime import owned_resources
|
||||
monkeypatch.setattr(owned_resources, "_VAULT_RECORDS", {})
|
||||
|
||||
|
||||
def grant(*tools, scopes=None, owner="alice", thread="s"):
|
||||
return RequestAuthority("owned-request", owner, thread, "",
|
||||
tuple(OperationGrant(t) for t in tools), owned_scopes=scopes)
|
||||
|
||||
|
||||
async def dispatch(authority, tool, content, **kwargs):
|
||||
from src import tool_execution as execution
|
||||
return await execution.execute_tool_block(ToolBlock(tool, content), owner=authority.owner,
|
||||
session_id=authority.session_id, request_authority=authority,
|
||||
security_context=kwargs.pop("security_context", execution.NO_TOOL_SECURITY_CONTEXT), **kwargs)
|
||||
|
||||
|
||||
def approval(authority, tool, content, **kwargs):
|
||||
store = ToolApprovalStore()
|
||||
pending = store.create(owner=authority.owner, session_id=authority.session_id, origin_run_id="run",
|
||||
tool_name=tool, content=content, workspace=None, request_authority=authority,
|
||||
external_untrusted_context_seen=True, capabilities=capabilities_for_action(tool, content), **kwargs)
|
||||
return store.consume(pending.approval_id, decision="approve", owner=authority.owner, session_id=authority.session_id)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def records(monkeypatch):
|
||||
import core.database as db
|
||||
import src.database as compatibility
|
||||
from src.agent_tools import document_tools
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
db.Base.metadata.create_all(engine)
|
||||
factory = sessionmaker(bind=engine)
|
||||
monkeypatch.setattr(db, "SessionLocal", factory)
|
||||
monkeypatch.setattr(compatibility, "SessionLocal", factory)
|
||||
from src import tool_execution as execution
|
||||
monkeypatch.setattr(execution, "_owner_is_admin", lambda owner: True)
|
||||
now = datetime(2026, 1, 1)
|
||||
with factory() as connection:
|
||||
for identifier, owner in (("s", "alice"), ("other", "alice"), ("foreign", "bob")):
|
||||
connection.add(db.Session(id=identifier, owner=owner, name=identifier, endpoint_url="https://model.test", model="test"))
|
||||
for identifier, owner, thread, offset in (("d1", "alice", "s", 1), ("d2", "alice", "other", 2), ("private", "bob", "foreign", 3)):
|
||||
connection.add(db.Document(id=identifier, owner=owner, session_id=thread, title=identifier,
|
||||
current_content=identifier + " original", language="text", version_count=1,
|
||||
created_at=now, updated_at=now + timedelta(days=offset)))
|
||||
connection.add(db.Note(id="note-one", owner="alice", title="first", content="original"))
|
||||
connection.add(db.Note(id="note-two", owner="alice", title="second", content="original"))
|
||||
connection.add(db.Note(id="note-foreign", owner="bob", title="private", content="private"))
|
||||
connection.commit()
|
||||
monkeypatch.setattr(document_tools, "_active_document_id", None)
|
||||
yield factory
|
||||
engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("selector", ["active", "current", "latest"])
|
||||
async def test_document_alias_resolves_once_and_does_not_follow_new_active_or_latest(records, monkeypatch, selector):
|
||||
from src import tool_execution as execution
|
||||
authority = grant("manage_documents")
|
||||
content = json.dumps({"action": "read", "document_id": selector})
|
||||
exact = approval(authority, "manage_documents", content, document_id="d1")
|
||||
bound = exact.pending.owned_operation
|
||||
expected = "d2" if selector == "latest" else "d1"
|
||||
assert bound.document_id == expected
|
||||
import core.database as db
|
||||
with records() as connection:
|
||||
connection.add(db.Document(id="newest", owner="alice", session_id="s", title="newest",
|
||||
current_content="newest content", version_count=1, updated_at=datetime(2030, 1, 1)))
|
||||
connection.commit()
|
||||
seen = []
|
||||
async def implementation(block, **kwargs):
|
||||
seen.append((json.loads(block.content)["document_id"], kwargs["approved_document_id"], active_owned_operation()))
|
||||
return "read", {"exit_code": 0}
|
||||
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
|
||||
_, result = await dispatch(authority, "manage_documents", content, active_document_id="newest",
|
||||
exact_approval=exact, security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
assert result["exit_code"] == 0
|
||||
assert seen[0][:2] == (expected, expected)
|
||||
assert seen[0][2] is bound
|
||||
assert active_owned_operation() is None
|
||||
|
||||
|
||||
async def test_document_runtime_executes_captured_id_not_process_global_alias(records):
|
||||
from src.agent_tools import document_tools
|
||||
authority = grant("update_document")
|
||||
exact = approval(authority, "update_document", "replacement", document_id="d1")
|
||||
document_tools.set_active_document("d2")
|
||||
_, result = await dispatch(authority, "update_document", "replacement", active_document_id="d2",
|
||||
exact_approval=exact, security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
assert result.get("exit_code", 0) == 0 and not result.get("error")
|
||||
import core.database as db
|
||||
with records() as connection:
|
||||
assert connection.get(db.Document, "d1").current_content == "replacement"
|
||||
assert connection.get(db.Document, "d2").current_content == "d2 original"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["revision", "owner", "thread", "deleted", "request", "invocation_thread"])
|
||||
async def test_stale_or_rebound_document_approval_fails_before_effect(records, monkeypatch, change):
|
||||
import core.database as db
|
||||
from src import tool_execution as execution
|
||||
authority = grant("update_document")
|
||||
exact = approval(authority, "update_document", "replacement", document_id="d1")
|
||||
if change in {"request", "invocation_thread"}:
|
||||
authority = replace(authority, **({"request_id": "other"} if change == "request" else
|
||||
{"session_id": "other", "owned_scopes": (OwnedScope("documents", "alice", "other"),)}))
|
||||
else:
|
||||
with records() as connection:
|
||||
row = connection.get(db.Document, "d1")
|
||||
if change == "revision":
|
||||
row.current_content = "changed"
|
||||
row.version_count += 1
|
||||
elif change == "owner":
|
||||
row.owner = "bob"
|
||||
elif change == "thread":
|
||||
row.session_id = "other"
|
||||
else:
|
||||
connection.delete(row)
|
||||
connection.commit()
|
||||
implementation = AsyncMock()
|
||||
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
|
||||
_, result = await dispatch(authority, "update_document", "replacement", exact_approval=exact,
|
||||
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
implementation.assert_not_awaited()
|
||||
assert not exact._claimed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("owner,thread", [("", "s"), ("alice", ""), ("bob", "s")])
|
||||
async def test_owner_and_invocation_thread_are_mandatory(records, owner, thread):
|
||||
_, result = await dispatch(grant("manage_documents", owner=owner, thread=thread),
|
||||
"manage_documents", '{"action":"read","id":"d1"}')
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
|
||||
|
||||
async def test_child_record_scope_is_intersection_and_exact_approval_cannot_widen(records, monkeypatch):
|
||||
parent = grant("manage_documents", scopes=(OwnedScope("documents", "alice", "s", frozenset({"d1"})),))
|
||||
child = grant("manage_documents")
|
||||
assert parent.intersect(child).owned_scopes == parent.owned_scopes
|
||||
exact = approval(child, "manage_documents", '{"action":"read","id":"d2"}')
|
||||
from src import tool_execution as execution
|
||||
handler = AsyncMock(return_value=("read", {"exit_code": 0}))
|
||||
monkeypatch.setattr(execution, "_execute_tool_block_impl", handler)
|
||||
with bind_request_authority(parent):
|
||||
_, denied = await dispatch(child, "manage_documents", exact.pending.content, exact_approval=exact,
|
||||
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
_, allowed = await dispatch(child, "manage_documents", '{"action":"read","id":"d1"}')
|
||||
_, collection = await dispatch(child, "manage_documents", '{"action":"list"}')
|
||||
assert denied["failure_kind"] == collection["failure_kind"] == "resource_identity_denied"
|
||||
assert allowed["exit_code"] == 0 and handler.await_count == 1
|
||||
|
||||
|
||||
async def test_legacy_owned_approval_is_exact_one_use_not_reconstructed_scope(records, monkeypatch):
|
||||
snapshot = grant("manage_documents").to_dict()
|
||||
snapshot["version"] = 2
|
||||
authority = RequestAuthority.from_dict(snapshot)
|
||||
exact = approval(authority, "manage_documents", '{"action":"read","id":"d1"}')
|
||||
from src import tool_execution as execution
|
||||
handler = AsyncMock(return_value=("read", {"exit_code": 0}))
|
||||
monkeypatch.setattr(execution, "_execute_tool_block_impl", handler)
|
||||
_, denied = await dispatch(authority, "manage_documents", exact.pending.content)
|
||||
assert denied["failure_kind"] == "resource_identity_denied"
|
||||
security = ToolRunSecurityContext(external_untrusted_context_seen=True)
|
||||
_, allowed = await dispatch(authority, "manage_documents", exact.pending.content, exact_approval=exact, security_context=security)
|
||||
_, replay = await dispatch(authority, "manage_documents", exact.pending.content, exact_approval=exact, security_context=security)
|
||||
assert allowed["exit_code"] == 0 and replay["exit_code"] == 1
|
||||
assert authority.owned_scopes == () and handler.await_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool,content", [("manage_session", '{"action":"rename","session":"current","value":"new"}'),
|
||||
("manage_session", "rename\ncurrent\nnew"), ("send_to_session", "current\nhello")])
|
||||
def test_thread_current_alias_becomes_exact_owned_identity(records, tool, content):
|
||||
bound = resolve_owned_operation(ExactOperation.normalize(tool, content), owner="alice", thread_id="s")
|
||||
assert bound.resources[0].record_id == bound.resources[0].record_thread_id == "s"
|
||||
assert "current" not in bound.execution_input
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
resolve_owned_operation(ExactOperation.normalize(tool, content.replace("current", "foreign")), owner="alice", thread_id="s")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("selector", ["note-o", "first"])
|
||||
def test_note_alias_resolves_once_and_prefix_ambiguity_fails_closed(records, selector):
|
||||
content = {"action": "update", "content": "replacement", "id" if selector == "note-o" else "title": selector}
|
||||
bound = resolve_owned_operation(ExactOperation.normalize("manage_notes", json.dumps(content)), owner="alice", thread_id="s")
|
||||
assert bound.resources[0].record_id == json.loads(bound.execution_input)["id"] == "note-one"
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
resolve_owned_operation(ExactOperation.normalize("manage_notes", '{"action":"view","id":"note-"}'), owner="alice", thread_id="s")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def attachment_store(tmp_path, monkeypatch):
|
||||
from src import tool_utils
|
||||
file = tmp_path / "image.png"
|
||||
file.write_bytes(b"test image")
|
||||
row = {"id": "upload.png", "owner": "alice", "path": str(file), "hash": "observed-hash"}
|
||||
handler = SimpleNamespace(upload_dir=str(tmp_path), resolve_upload=lambda identifier, *, owner, allow_admin:
|
||||
dict(row) if identifier == row.get("id") and owner == row.get("owner") and not allow_admin else None)
|
||||
monkeypatch.setattr(tool_utils, "get_upload_handler", lambda: handler)
|
||||
return row, file
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["owner", "path", "file", "missing"])
|
||||
def test_attachment_ownership_index_and_file_identity_are_pinned(attachment_store, change):
|
||||
row, file = attachment_store
|
||||
bound = resolve_owned_operation(ExactOperation.normalize("extract_text", '{"path":"odysseus://attachment/upload.png"}'), owner="alice", thread_id="s")
|
||||
with bind_owned_operation(bound):
|
||||
assert bound_attachment_path("alice", "odysseus://attachment/upload.png") == str(file)
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
bound_attachment_path("bob", "odysseus://attachment/upload.png")
|
||||
if change == "owner":
|
||||
row["owner"] = "bob"
|
||||
elif change == "path":
|
||||
other = file.with_name("other.png")
|
||||
other.write_bytes(b"other")
|
||||
row["path"] = str(other)
|
||||
elif change == "file":
|
||||
file.rename(file.with_name("old.png"))
|
||||
file.write_bytes(b"replacement")
|
||||
else:
|
||||
row.clear()
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
bound.validate()
|
||||
|
||||
|
||||
def test_memory_prefix_is_owner_scoped_exact_and_revision_sensitive(monkeypatch):
|
||||
from src import ai_interaction
|
||||
rows = [{"id": "memory-one", "owner": "alice", "text": "secret", "timestamp": 1},
|
||||
{"id": "memory-other", "owner": "bob", "text": "private", "timestamp": 1}]
|
||||
monkeypatch.setattr(ai_interaction, "_memory_manager", SimpleNamespace(load=lambda owner: rows))
|
||||
bound = resolve_owned_operation(ExactOperation.normalize("manage_memory", "edit\nmemory-o\nreplacement"), owner="alice", thread_id="s")
|
||||
assert bound.resources[0].record_id == "memory-one"
|
||||
assert bound.execution_input == "edit\nmemory-one\nreplacement"
|
||||
assert "secret" not in json.dumps(bound.to_dict())
|
||||
rows[0]["text"] = "changed in same second"
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
bound.validate()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["owner", "endpoint", "session"])
|
||||
def test_private_vault_identity_binds_owner_endpoint_and_exact_uuid_without_credentials(monkeypatch, change):
|
||||
from src.tools import vault
|
||||
cfg = {"owner": "alice", "server_url": "https://vault.test", "session": "SECRET_SESSION", "unlocked_at": "observed"}
|
||||
monkeypatch.setattr(vault, "_load_vault_config", lambda: cfg)
|
||||
observe_vault_records("alice", cfg, [{"id": "12345678-1234-1234-1234-123456789abc", "name": "bank", "login": {"password": "PRIVATE_PASSWORD"}}])
|
||||
tool = ExactOperation.normalize("vault_get", '{"item_id":"12345678-1234-1234-1234-123456789abc","reason":"requested"}')
|
||||
bound = resolve_owned_operation(tool, owner="alice", thread_id="s")
|
||||
assert "SECRET_SESSION" not in json.dumps(bound.to_dict()) and "PRIVATE_PASSWORD" not in json.dumps(bound.to_dict())
|
||||
cfg.update({"owner": "bob"} if change == "owner" else {"server_url": "https://other.test"} if change == "endpoint" else {"session": "OTHER_SECRET"})
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
bound.validate()
|
||||
|
||||
|
||||
def test_legacy_vault_and_model_name_alias_do_not_create_private_identity(monkeypatch):
|
||||
from src.tools import vault
|
||||
monkeypatch.setattr(vault, "_load_vault_config", lambda: {"server_url": "https://vault.test", "session": "secret"})
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
resolve_owned_operation(ExactOperation.normalize("vault_search", '{"query":"bank"}'), owner="alice", thread_id="s")
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
resolve_owned_operation(ExactOperation.normalize("vault_get", '{"item_id":"latest","reason":"requested"}'), owner="alice", thread_id="s")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("error", [None, RuntimeError, asyncio.CancelledError])
|
||||
async def test_owned_and_backend_context_restore_after_success_error_cancel_and_nested_call(records, monkeypatch, error):
|
||||
from src import tool_execution as execution
|
||||
authority = grant("manage_documents")
|
||||
parent = admit_owned_operation(authority, ExactOperation.normalize("manage_documents", '{"action":"read","id":"d1"}'))
|
||||
async def implementation(block, **kwargs):
|
||||
assert active_owned_operation().resources[0].record_id == "d2"
|
||||
assert active_backend_operation() is not None
|
||||
if error:
|
||||
raise error("stop")
|
||||
return "read", {"exit_code": 0}
|
||||
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
|
||||
with bind_owned_operation(parent):
|
||||
if error:
|
||||
with pytest.raises(error):
|
||||
await dispatch(authority, "manage_documents", '{"action":"read","id":"d2"}')
|
||||
else:
|
||||
await dispatch(authority, "manage_documents", '{"action":"read","id":"d2"}')
|
||||
assert active_owned_operation() is parent
|
||||
assert active_backend_operation() is None
|
||||
assert active_owned_operation() is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["/api/document/d1", "/api/history/s", "/api/vault/config", "/api/memory", "/api/notes",
|
||||
"/api/upload/upload.png", "/api/%64ocument/d1", "/api/cookbook/../document/d1"])
|
||||
async def test_generic_internal_bridge_cannot_bypass_owned_resource_adapter(monkeypatch, path):
|
||||
from src import tool_execution as execution
|
||||
handler = AsyncMock()
|
||||
monkeypatch.setattr(execution, "_execute_tool_block_impl", handler)
|
||||
_, result = await dispatch(grant("app_api"), "app_api", json.dumps({"path": path}))
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
handler.assert_not_awaited()
|
||||
|
||||
|
||||
def test_owned_snapshot_roundtrip_and_malformed_scopes_fail_closed(records):
|
||||
authority = grant("manage_documents", scopes=(OwnedScope("documents", "alice", "s", frozenset({"d1"})),))
|
||||
snapshot = json.loads(json.dumps(authority.to_dict()))
|
||||
assert RequestAuthority.from_dict(snapshot) == authority
|
||||
for mutation in ({"owner": "bob"}, {"thread_id": "other"}, {"record_ids": ["*"]}, {"record_ids": [1]}):
|
||||
changed = json.loads(json.dumps(snapshot))
|
||||
changed["owned_scopes"][0].update(mutation)
|
||||
with pytest.raises((ValueError, TypeError)):
|
||||
RequestAuthority.from_dict(changed)
|
||||
|
||||
|
||||
def test_vault_config_owner_is_produced_by_authenticated_request_and_drops_legacy_session():
|
||||
from routes.vault.vault_routes import _bind_config_owner
|
||||
from fastapi import HTTPException
|
||||
request = SimpleNamespace(state=SimpleNamespace(current_user="alice", api_token=False))
|
||||
cfg = {"session": "legacy-secret", "unlocked_at": "legacy"}
|
||||
_bind_config_owner(cfg, request)
|
||||
assert cfg == {"owner": "alice"}
|
||||
request.state.current_user = "bob"
|
||||
with pytest.raises(HTTPException):
|
||||
_bind_config_owner(cfg, request)
|
||||
|
||||
|
||||
def test_missing_proposal_record_is_not_reconstructed_after_it_appears(records):
|
||||
authority = grant("manage_documents")
|
||||
exact = approval(authority, "manage_documents", '{"action":"read","id":"not-yet"}')
|
||||
assert exact.pending.owned_operation is None
|
||||
import core.database as db
|
||||
with records() as connection:
|
||||
connection.add(db.Document(id="not-yet", owner="alice", title="appeared", current_content="content", version_count=1))
|
||||
connection.commit()
|
||||
_, result = asyncio.run(dispatch(authority, "manage_documents", exact.pending.content, exact_approval=exact,
|
||||
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True)))
|
||||
assert result["failure_kind"] == "resource_identity_denied" and not exact._claimed
|
||||
|
||||
|
||||
def test_malformed_record_identity_and_normalized_approval_tampering_fail_closed(records):
|
||||
authority = grant("manage_documents")
|
||||
exact = approval(authority, "manage_documents", '{"action":"read","id":"d1"}')
|
||||
bound = exact.pending.owned_operation
|
||||
with pytest.raises(ValueError):
|
||||
replace(bound, resources=(replace(bound.resources[0], revision=""),))
|
||||
with pytest.raises(ValueError):
|
||||
replace(bound, document_id="d2")
|
||||
exact.pending = replace(exact.pending, owned_operation=replace(bound, execution_input='{"action":"read","document_id":"d2"}'))
|
||||
assert not exact.matches(owner="alice", session_id="s", workspace=None,
|
||||
tool_name="manage_documents", content=exact.pending.content)
|
||||
|
||||
|
||||
def test_note_prefix_wildcards_cannot_create_selector_authority(records):
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
resolve_owned_operation(ExactOperation.normalize("manage_notes", '{"action":"view","id":"note-o%"}'), owner="alice", thread_id="s")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("content", ['{"action":"read"}', '{"action":"read","id":"active"}', '{"action":"read","id":"current"}', '{"action":"read","id":42}', '{"action":"read","id":"d1","uid":"d2"}'])
|
||||
def test_missing_malformed_and_conflicting_document_selectors_fail_closed(records, content):
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
resolve_owned_operation(ExactOperation.normalize("manage_documents", content), owner="alice", thread_id="s")
|
||||
|
||||
|
||||
async def test_resumed_child_approval_cannot_restore_excluded_record(records):
|
||||
parent = grant("manage_documents", scopes=(OwnedScope("documents", "alice", "s", frozenset({"d1"})),))
|
||||
child = parent.intersect(grant("manage_documents"))
|
||||
exact = approval(child, "manage_documents", '{"action":"read","id":"d2"}')
|
||||
assert exact.pending.owned_operation is None
|
||||
_, result = await dispatch(replace(child, inherited=False), "manage_documents", exact.pending.content, exact_approval=exact,
|
||||
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
assert result["failure_kind"] == "resource_identity_denied" and not exact._claimed
|
||||
|
||||
|
||||
async def test_vault_search_producer_supplies_exact_owned_item_identity_and_alias_binding(monkeypatch):
|
||||
from src.tools import vault
|
||||
from src import tool_execution as execution
|
||||
cfg = {"owner": "alice", "server_url": "https://vault.test", "session": "SECRET_SESSION", "unlocked_at": "observed"}
|
||||
item = {"id": "12345678-1234-1234-1234-123456789abc", "name": "bank", "login": {"password": "PRIVATE_PASSWORD"}}
|
||||
monkeypatch.setattr(vault, "_load_vault_config", lambda: cfg)
|
||||
monkeypatch.setattr(execution, "_owner_is_admin", lambda owner: True)
|
||||
cli = AsyncMock(side_effect=[(json.dumps([item]), "", 0), (json.dumps(item), "", 0)])
|
||||
monkeypatch.setattr(vault, "_run_bw", cli)
|
||||
authority = grant("vault_search", "vault_get")
|
||||
_, missing = await dispatch(authority, "vault_get", json.dumps({"item_id": item["id"], "reason": "requested"}))
|
||||
assert missing["failure_kind"] == "resource_identity_denied"
|
||||
cli.assert_not_awaited()
|
||||
_, search = await dispatch(authority, "vault_search", '{"query":"bank"}')
|
||||
assert search["exit_code"] == 0 and item["id"] in search["output"]
|
||||
exact = approval(authority, "vault_get", '{"item_id":"bank","reason":"requested"}')
|
||||
assert exact.pending.owned_operation.resources[0].record_id == item["id"]
|
||||
# A later producer result with the same alias cannot change the approved ID.
|
||||
other = {"id": "87654321-1234-1234-1234-123456789abc", "name": "bank", "login": {"password": "OTHER_PASSWORD"}}
|
||||
observe_vault_records("alice", cfg, [other])
|
||||
_, result = await dispatch(authority, "vault_get", exact.pending.content, exact_approval=exact,
|
||||
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
assert result["exit_code"] == 0 and "PRIVATE_PASSWORD" in result["output"] and "OTHER_PASSWORD" not in result["output"]
|
||||
assert cli.await_args.args[0] == ["get", "item", item["id"]]
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
resolve_owned_operation(ExactOperation.normalize("vault_get", exact.pending.content), owner="alice", thread_id="s")
|
||||
|
||||
|
||||
def test_vault_producer_revision_change_invalidates_sealed_item_and_cannot_cross_owner(monkeypatch):
|
||||
from src.tools import vault
|
||||
cfg = {"owner": "alice", "server_url": "https://vault.test", "session": "SECRET"}
|
||||
item = {"id": "12345678-1234-1234-1234-123456789abc", "name": "bank", "revisionDate": "one"}
|
||||
monkeypatch.setattr(vault, "_load_vault_config", lambda: cfg)
|
||||
observe_vault_records("alice", cfg, [item])
|
||||
bound = resolve_owned_operation(ExactOperation.normalize("vault_get", '{"item_id":"12345678","reason":"requested"}'), owner="alice", thread_id="s")
|
||||
assert json.loads(bound.execution_input)["item_id"] == item["id"]
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
observe_vault_records("bob", cfg, [item])
|
||||
observe_vault_records("alice", cfg, [{**item, "revisionDate": "two"}])
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
bound.validate()
|
||||
@@ -0,0 +1,397 @@
|
||||
"""Backend selection is resolution, never an operation or resource grant."""
|
||||
import asyncio
|
||||
from dataclasses import replace
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agent_runtime.authority import ExactOperation, OperationGrant, RequestAuthority, bind_request_authority
|
||||
from src.agent_runtime.remote_resources import (
|
||||
active_backend_operation, bind_backend_operation, bind_backend_for_operation,
|
||||
configuration_incarnation, endpoint_identity, integration_resource, seal_backends,
|
||||
)
|
||||
from src.agent_runtime.resources import ExternalResource, NativeBackendResource, ResourceIdentityError
|
||||
from src.mcp_manager import McpManager
|
||||
from src.tool_approvals import ToolApprovalStore
|
||||
from src.tool_capabilities import ToolRunSecurityContext, capabilities_for_action
|
||||
from src.tool_types import ToolBlock
|
||||
|
||||
|
||||
def grant(*tools, resources=None):
|
||||
return RequestAuthority("remote-request", "alice", "s", "",
|
||||
tuple(OperationGrant(t) for t in tools), backend_resources=resources)
|
||||
|
||||
|
||||
async def dispatch(authority, tool, content="{}", **kwargs):
|
||||
from src import tool_execution as execution
|
||||
return await execution.execute_tool_block(ToolBlock(tool, content), owner=authority.owner,
|
||||
session_id=authority.session_id, request_authority=authority,
|
||||
security_context=kwargs.pop("security_context", execution.NO_TOOL_SECURITY_CONTEXT), **kwargs)
|
||||
|
||||
|
||||
def approval(authority, tool, content="{}", **kwargs):
|
||||
store = ToolApprovalStore()
|
||||
pending = store.create(owner=authority.owner, session_id=authority.session_id, origin_run_id="run",
|
||||
tool_name=tool, content=content, workspace=None, request_authority=authority,
|
||||
external_untrusted_context_seen=True, capabilities=capabilities_for_action(tool, content), **kwargs)
|
||||
return store.consume(pending.approval_id, decision="approve", owner=authority.owner, session_id=authority.session_id)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def manager(monkeypatch):
|
||||
from src import tool_execution as execution
|
||||
value = McpManager()
|
||||
monkeypatch.setattr(execution, "get_mcp_manager", lambda: value)
|
||||
monkeypatch.setattr(execution, "_owner_is_admin", lambda owner: True)
|
||||
return value
|
||||
|
||||
|
||||
def connect(manager, server="alpha", tools=("read", "write"), url="https://example.test/mcp?token=SECRET"):
|
||||
session = SimpleNamespace(call_tool=AsyncMock(return_value=SimpleNamespace(
|
||||
content=[SimpleNamespace(text="remote result")], isError=False)))
|
||||
manager._sessions[server] = session
|
||||
manager._tools[server] = [{"name": tool} for tool in tools]
|
||||
manager._resource_endpoints[server] = (endpoint_identity(url), configuration_incarnation(url))
|
||||
manager._register_resource_connection(server, session)
|
||||
return session
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["availability", "selection", "model_name", "legacy"])
|
||||
async def test_remote_availability_does_not_create_resource_authority(manager, kind):
|
||||
session = connect(manager)
|
||||
authority = grant("mcp__alpha__read", resources=()) if kind != "model_name" else grant()
|
||||
if kind == "legacy":
|
||||
snapshot = grant("mcp__alpha__read").to_dict()
|
||||
snapshot["version"] = 2
|
||||
authority = RequestAuthority.from_dict(snapshot)
|
||||
_, result = await dispatch(authority, "mcp__alpha__read")
|
||||
assert result["exit_code"] == 1
|
||||
session.call_tool.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_qualified_mcp_binds_exact_tool_and_backend(manager):
|
||||
session = connect(manager)
|
||||
authority = grant("mcp__alpha__read")
|
||||
_, allowed = await dispatch(authority, "mcp__alpha__read", '{"record":"one"}')
|
||||
assert allowed["exit_code"] == 0
|
||||
session.call_tool.assert_awaited_once_with("read", {"record": "one"})
|
||||
_, denied = await dispatch(replace(authority, grants=(OperationGrant("mcp__alpha__write"),)), "mcp__alpha__write")
|
||||
assert denied["failure_kind"] == "resource_identity_denied"
|
||||
assert active_backend_operation() is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["session", "endpoint", "path", "query", "discovery"])
|
||||
async def test_remote_identity_changes_invalidate_admission_and_exact_approval(manager, change):
|
||||
old = connect(manager)
|
||||
authority = grant("mcp__alpha__read")
|
||||
exact = approval(authority, "mcp__alpha__read", '{"resource":"one"}')
|
||||
assert exact.pending.backend_operation is not None
|
||||
if change == "session":
|
||||
new = connect(manager)
|
||||
elif change == "discovery":
|
||||
manager._tools["alpha"] = [{"name": "write"}]
|
||||
else:
|
||||
url = {"endpoint": "https://other.test/mcp", "path": "https://example.test/other",
|
||||
"query": "https://example.test/mcp?token=OTHER"}[change]
|
||||
manager._resource_endpoints["alpha"] = (endpoint_identity(url), configuration_incarnation(url))
|
||||
for extra in ({}, {"exact_approval": exact, "security_context": ToolRunSecurityContext(external_untrusted_context_seen=True)}):
|
||||
_, result = await dispatch(authority, "mcp__alpha__read", '{"resource":"one"}', **extra)
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
old.call_tool.assert_not_awaited()
|
||||
if change == "session":
|
||||
new.call_tool.assert_not_awaited()
|
||||
assert not exact._claimed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["tool", "selector", "request", "owner", "session"])
|
||||
async def test_remote_approval_is_bound_to_operation_and_request(manager, change):
|
||||
session = connect(manager)
|
||||
authority = grant("mcp__alpha__read", "mcp__alpha__write")
|
||||
exact = approval(authority, "mcp__alpha__read", '{"record":"one"}')
|
||||
tool, content = "mcp__alpha__read", '{"record":"one"}'
|
||||
if change == "tool":
|
||||
tool = "mcp__alpha__write"
|
||||
elif change == "selector":
|
||||
content = '{"record":"two"}'
|
||||
else:
|
||||
authority = replace(authority, **{"request": {"request_id": "other"}, "owner": {"owner": "bob"},
|
||||
"session": {"session_id": "other"}}[change])
|
||||
_, result = await dispatch(authority, tool, content, exact_approval=exact,
|
||||
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
assert result["exit_code"] == 1
|
||||
session.call_tool.assert_not_awaited()
|
||||
assert not exact._claimed
|
||||
|
||||
|
||||
async def test_legacy_exact_remote_approval_is_one_use_and_does_not_mint_backend_scope(manager):
|
||||
session = connect(manager)
|
||||
authority = grant(resources=())
|
||||
exact = approval(authority, "mcp__alpha__read")
|
||||
security = ToolRunSecurityContext(external_untrusted_context_seen=True)
|
||||
_, result = await dispatch(authority, "mcp__alpha__read", exact_approval=exact, security_context=security)
|
||||
assert result["exit_code"] == 0
|
||||
_, replay = await dispatch(authority, "mcp__alpha__read", exact_approval=exact, security_context=security)
|
||||
assert replay["exit_code"] == 1
|
||||
assert session.call_tool.await_count == 1 and authority.backend_resources == ()
|
||||
assert "backend_operation" not in exact.pending.public_payload()
|
||||
|
||||
|
||||
async def test_child_cannot_use_parent_ungranted_backend_or_exact_approval(manager):
|
||||
session = connect(manager)
|
||||
parent = grant("mcp__alpha__read", resources=())
|
||||
child = grant("mcp__alpha__read")
|
||||
exact = approval(child, "mcp__alpha__read")
|
||||
with bind_request_authority(parent):
|
||||
_, denied = await dispatch(child, "mcp__alpha__read", exact_approval=exact,
|
||||
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
assert denied["failure_kind"] == "resource_identity_denied"
|
||||
session.call_tool.assert_not_awaited()
|
||||
|
||||
|
||||
def test_remote_snapshots_exclude_credentials_and_cannot_claim_containment(manager):
|
||||
connect(manager, url="https://user:PASSWORD@example.test/SECRET_PATH?token=TOKEN")
|
||||
authority = grant("mcp__alpha__read")
|
||||
snapshot = json.dumps(authority.to_dict())
|
||||
assert all(secret not in snapshot for secret in ("PASSWORD", "SECRET_PATH", "TOKEN", "user:"))
|
||||
resource = authority.backend_resources[0]
|
||||
assert resource.endpoint_id == "https://example.test"
|
||||
assert resource.external is True and resource.contained is False
|
||||
assert RequestAuthority.from_dict(json.loads(snapshot)) == authority
|
||||
with pytest.raises(ValueError):
|
||||
replace(resource, contained=True)
|
||||
|
||||
|
||||
async def test_mcp_revalidates_at_transport_and_never_retries_bound_calls(manager):
|
||||
manager._resource_owners["memory"] = "alice"
|
||||
session = connect(manager, server="memory")
|
||||
authority = grant("mcp__memory__read")
|
||||
operation = ExactOperation.normalize("mcp__memory__read", "{}")
|
||||
bound = bind_backend_for_operation(authority, operation)
|
||||
reconnect = AsyncMock()
|
||||
manager._reconnect_builtin = reconnect
|
||||
session.call_tool.side_effect = RuntimeError("disconnected")
|
||||
with bind_backend_operation(bound):
|
||||
result = await manager.call_tool(operation.tool, {})
|
||||
assert result["exit_code"] == 1
|
||||
replacement = connect(manager, server="memory")
|
||||
result = await manager.call_tool(operation.tool, {})
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
replacement.call_tool.assert_not_awaited()
|
||||
reconnect.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("owner", ["", "bob"])
|
||||
async def test_builtin_memory_backend_requires_its_configured_owner(manager, owner):
|
||||
manager._resource_owners["memory"] = owner
|
||||
session = connect(manager, server="memory")
|
||||
_, result = await dispatch(grant("mcp__memory__read"), "mcp__memory__read")
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
session.call_tool.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_native_filesystem_cannot_be_redirected_through_mcp(manager, tmp_path):
|
||||
session = connect(manager, server="filesystem", tools=("read_file",))
|
||||
(tmp_path / "a").write_text("native contents")
|
||||
authority = RequestAuthority("request", "alice", "s", str(tmp_path), (OperationGrant("read_file"),))
|
||||
from src import tool_execution as execution
|
||||
_, result = await execution.execute_tool_block(ToolBlock("read_file", "a"), owner="alice", session_id="s",
|
||||
workspace=str(tmp_path), request_authority=authority, security_context=execution.NO_TOOL_SECURITY_CONTEXT)
|
||||
assert result["output"] == "native contents"
|
||||
assert isinstance(authority.backend_resources[0], NativeBackendResource)
|
||||
session.call_tool.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["alias", "endpoint", "secret_path"])
|
||||
async def test_integration_alias_and_configuration_cannot_retarget_approval(monkeypatch, change):
|
||||
from src import integrations
|
||||
rows = [{"id": "one", "name": "service", "base_url": "https://service.test/SECRET", "enabled": True}]
|
||||
monkeypatch.setattr(integrations, "load_integrations", lambda: rows)
|
||||
authority = grant("api_call", resources=(integration_resource(rows[0]),))
|
||||
exact = approval(authority, "api_call", '{"integration":"service","path":"/record/one"}')
|
||||
assert exact.pending.backend_operation.resource.server_id == "one"
|
||||
assert "SECRET" not in json.dumps(authority.to_dict())
|
||||
if change == "alias":
|
||||
rows[:] = [{**rows[0], "id": "two"}]
|
||||
else:
|
||||
rows[0]["base_url"] = "https://other.test/SECRET" if change == "endpoint" else "https://service.test/OTHER"
|
||||
from src import tool_execution as execution
|
||||
handler = AsyncMock()
|
||||
monkeypatch.setattr(execution, "_execute_tool_block_impl", handler)
|
||||
_, result = await dispatch(authority, "api_call", exact.pending.content, exact_approval=exact,
|
||||
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
handler.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("error", [None, RuntimeError, asyncio.CancelledError])
|
||||
async def test_scoped_bridge_context_restores_and_replacement_is_ungranted(monkeypatch, error):
|
||||
from src import tool_execution as execution
|
||||
seen = []
|
||||
async def route(*args):
|
||||
seen.append(active_backend_operation().resource)
|
||||
if error:
|
||||
raise error("stop")
|
||||
return "bridge", {"exit_code": 0}
|
||||
bridge = execution.AgentExecutionBridge(route, frozenset({"host_shell"}), name="test")
|
||||
monkeypatch.setattr(execution, "_owner_is_admin", lambda owner: True)
|
||||
with execution.bind_execution_bridge(bridge):
|
||||
authority = grant("host_shell")
|
||||
parent_bound = bind_backend_for_operation(authority, ExactOperation.normalize("host_shell", "parent"))
|
||||
with bind_backend_operation(parent_bound):
|
||||
if error is asyncio.CancelledError:
|
||||
with pytest.raises(error):
|
||||
await dispatch(authority, "host_shell", "pwd")
|
||||
else:
|
||||
await dispatch(authority, "host_shell", "pwd")
|
||||
assert active_backend_operation() is parent_bound
|
||||
assert active_backend_operation() is None
|
||||
assert seen[0].external and not seen[0].contained
|
||||
with execution.bind_execution_bridge(replace(bridge)):
|
||||
_, denied = await dispatch(authority, "host_shell", "pwd")
|
||||
assert denied["failure_kind"] == "resource_identity_denied"
|
||||
|
||||
|
||||
async def test_tui_endpoint_is_registered_only_at_trusted_admission(monkeypatch):
|
||||
from src import tool_execution as execution
|
||||
context = {"surface": "odysseus-tui", "host_shell_bridge": {"url": "http://127.0.0.1:17654/run", "token": "TOKEN"}}
|
||||
authority = grant("bash", resources=(NativeBackendResource("bash"),))
|
||||
_, denied = await dispatch(authority, "bash", "pwd", client_runtime_context=context)
|
||||
assert denied["failure_kind"] == "resource_identity_denied"
|
||||
authority = replace(authority, backend_resources=seal_backends(["bash"], context=context, owner="alice"))
|
||||
monkeypatch.setattr(execution, "_bridge_post", AsyncMock(return_value={"exit_code": 0, "stdout": "external", "stderr": ""}))
|
||||
monkeypatch.setattr(execution, "_owner_is_admin", lambda owner: True)
|
||||
_, allowed = await dispatch(authority, "bash", "pwd", client_runtime_context=context)
|
||||
assert allowed["exit_code"] == 0
|
||||
context["host_shell_bridge"]["url"] = "http://127.0.0.1:17655/run"
|
||||
_, denied = await dispatch(authority, "bash", "pwd", client_runtime_context=context)
|
||||
assert denied["failure_kind"] == "resource_identity_denied"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", [None, "url", "token"])
|
||||
async def test_http_bridge_factory_and_admission_share_config_identity(monkeypatch, change):
|
||||
from src import tool_execution as execution
|
||||
from routes.chat_routes import _external_execution_bridge
|
||||
context = {"external_execution_bridge": {"url": "http://127.0.0.1:17654/execute",
|
||||
"token": "SECRET_TOKEN", "supported_tools": ["host_shell"]}}
|
||||
authority = grant("host_shell", resources=seal_backends(["host_shell"], context=context, owner="alice"))
|
||||
if change:
|
||||
context["external_execution_bridge"][change] = "http://127.0.0.1:17655/execute" if change == "url" else "OTHER_TOKEN"
|
||||
bridge = _external_execution_bridge(context)
|
||||
route = AsyncMock(return_value=("bridge", {"exit_code": 0}))
|
||||
bridge = replace(bridge, route_tool=route)
|
||||
monkeypatch.setattr(execution, "_owner_is_admin", lambda owner: True)
|
||||
with execution.bind_execution_bridge(bridge):
|
||||
_, result = await dispatch(authority, "host_shell", "pwd", client_runtime_context=context)
|
||||
if change:
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
route.assert_not_awaited()
|
||||
else:
|
||||
assert result["exit_code"] == 0
|
||||
route.assert_awaited_once()
|
||||
assert "SECRET_TOKEN" not in json.dumps(authority.to_dict())
|
||||
|
||||
|
||||
async def test_backend_alias_cannot_retarget_a_legacy_tool_after_approval(manager, monkeypatch):
|
||||
from src import tool_execution as execution
|
||||
first = connect(manager, server="web_fetch", tools=("web_fetch",))
|
||||
second = connect(manager, server="other", tools=("fetch",))
|
||||
authority = grant("web_fetch")
|
||||
exact = approval(authority, "web_fetch", "https://page.test/one")
|
||||
monkeypatch.setitem(execution._MCP_TOOL_MAP, "web_fetch", ("other", "fetch"))
|
||||
_, result = await dispatch(authority, "web_fetch", exact.pending.content, exact_approval=exact,
|
||||
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
first.call_tool.assert_not_awaited()
|
||||
second.call_tool.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_native_backend_is_pinned_when_mcp_becomes_available(manager, monkeypatch):
|
||||
from src import tool_execution as execution
|
||||
authority = grant("web_fetch")
|
||||
session = connect(manager, server="web_fetch", tools=("web_fetch",))
|
||||
fallback = AsyncMock(return_value={"output": "native", "exit_code": 0})
|
||||
monkeypatch.setattr(execution, "_direct_fallback", fallback)
|
||||
_, result = await dispatch(authority, "web_fetch", "https://page.test/one")
|
||||
assert result["output"] == "native"
|
||||
session.call_tool.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_integration_inventory_does_not_supply_backend_scope(monkeypatch):
|
||||
from src import integrations
|
||||
rows = [{"id": "one", "name": "service", "base_url": "https://service.test", "enabled": True}]
|
||||
monkeypatch.setattr(integrations, "load_integrations", lambda: rows)
|
||||
authority = grant("api_call")
|
||||
assert authority.backend_resources == ()
|
||||
_, denied = await dispatch(authority, "api_call", '{"integration":"service"}')
|
||||
assert denied["failure_kind"] == "resource_identity_denied"
|
||||
|
||||
|
||||
async def test_external_bash_marker_cannot_switch_to_local_background_execution(manager, monkeypatch):
|
||||
from src import bg_jobs
|
||||
session = connect(manager, server="bash", tools=("bash",))
|
||||
launch = AsyncMock()
|
||||
monkeypatch.setattr(bg_jobs, "launch", launch)
|
||||
_, result = await dispatch(grant("bash"), "bash", "#!bg\npwd")
|
||||
assert result["exit_code"] == 0
|
||||
assert session.call_tool.await_count == 1
|
||||
launch.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("alias", ["host_shell_bridge", "hostShellBridge"])
|
||||
def test_host_shell_bridge_aliases_resolve_to_one_external_identity(alias):
|
||||
context = {"surface": "odysseus-tui", alias: {"url": "http://127.0.0.1:17654/run", "token": "TOKEN"}}
|
||||
resources = seal_backends(["host_shell"], context=context, owner="alice")
|
||||
assert len(resources) == 1 and isinstance(resources[0], ExternalResource)
|
||||
assert resources[0].external and not resources[0].contained
|
||||
|
||||
|
||||
async def test_host_shell_cannot_reconstruct_an_unsealed_external_backend(monkeypatch):
|
||||
from src import tool_execution as execution
|
||||
handler = AsyncMock()
|
||||
monkeypatch.setattr(execution, "_execute_tool_block_impl", handler)
|
||||
_, result = await dispatch(grant("host_shell"), "host_shell", "pwd")
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
handler.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_http_backend_without_bound_producer_cannot_fall_back_to_native(monkeypatch):
|
||||
from src import tool_execution as execution
|
||||
context = {"external_execution_bridge": {"url": "http://127.0.0.1:17654/execute",
|
||||
"token": "TOKEN", "supported_tools": ["bash"]}}
|
||||
authority = grant("bash", resources=seal_backends(["bash"], context=context, owner="alice"))
|
||||
fallback = AsyncMock()
|
||||
monkeypatch.setattr(execution, "_direct_fallback", fallback)
|
||||
_, denied = await dispatch(authority, "bash", "pwd", client_runtime_context=context)
|
||||
assert denied["failure_kind"] == "resource_identity_denied"
|
||||
fallback.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_resumed_child_approval_cannot_restore_excluded_backend(manager):
|
||||
session = connect(manager)
|
||||
child = grant("mcp__alpha__read", resources=()).intersect(grant("mcp__alpha__read"))
|
||||
exact = approval(child, "mcp__alpha__read")
|
||||
assert exact.pending.backend_operation is None
|
||||
_, result = await dispatch(replace(child, inherited=False), "mcp__alpha__read", exact_approval=exact,
|
||||
security_context=ToolRunSecurityContext(external_untrusted_context_seen=True))
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
session.call_tool.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_integration_executes_server_resolved_id_and_revalidates_loaded_configuration(monkeypatch):
|
||||
from src import integrations, tool_execution as execution
|
||||
row = {"id": "one", "name": "service", "base_url": "https://service.test", "enabled": True}
|
||||
monkeypatch.setattr(integrations, "load_integrations", lambda: [dict(row)])
|
||||
monkeypatch.setattr(execution, "_owner_is_admin", lambda owner: True)
|
||||
authority = grant("api_call", resources=(integration_resource(row),))
|
||||
producer = AsyncMock(return_value={"output": "remote", "exit_code": 0})
|
||||
original = integrations.execute_api_call
|
||||
monkeypatch.setattr(integrations, "execute_api_call", producer)
|
||||
_, allowed = await dispatch(authority, "api_call", '{"integration":"service","method":"GET","path":"/record/one"}')
|
||||
assert allowed["exit_code"] == 0
|
||||
assert producer.await_args.args == ("one", "GET", "/record/one")
|
||||
monkeypatch.setattr(integrations, "execute_api_call", original)
|
||||
monkeypatch.setattr(integrations, "_find_integration", lambda identifier: {**row, "base_url": "https://other.test"})
|
||||
_, denied = await dispatch(authority, "api_call", '{"integration":"service","path":"/record/one"}')
|
||||
assert denied["failure_kind"] == "resource_identity_denied"
|
||||
@@ -81,6 +81,31 @@ def test_symlink_escape_is_not_a_resource(tmp_path):
|
||||
resolve(authority(workspace, "read_file"), "read_file", "alias")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("alias", ["direct", "relative", "symlink", "hardlink"])
|
||||
@pytest.mark.parametrize("must_exist", [True, False])
|
||||
def test_media_workspace_paths_cannot_address_control_state(tmp_path, monkeypatch, alias, must_exist):
|
||||
from src import constants, tool_execution
|
||||
from src.agent_tools.media_tools import _resolve_workspace_path
|
||||
control = tmp_path / "receipts.json"
|
||||
control.write_text("private execution state")
|
||||
monkeypatch.setattr(constants, "CONTAINMENT_STATE_FILE", str(control))
|
||||
monkeypatch.setattr(tool_execution, "get_active_workspace", lambda: str(tmp_path))
|
||||
if alias == "direct":
|
||||
selector = str(control)
|
||||
elif alias == "relative":
|
||||
selector = "./receipts.json"
|
||||
else:
|
||||
target = tmp_path / "image.png"
|
||||
if alias == "symlink":
|
||||
target.symlink_to(control)
|
||||
else:
|
||||
os.link(control, target)
|
||||
selector = "/workspace/image.png"
|
||||
with pytest.raises(ValueError, match="execution-control"):
|
||||
_resolve_workspace_path(selector, must_exist=must_exist)
|
||||
assert control.read_text() == "private execution state"
|
||||
|
||||
|
||||
def test_destination_binds_absence_and_existing_ancestors(tmp_path):
|
||||
parent = tmp_path / "existing"
|
||||
parent.mkdir()
|
||||
@@ -143,6 +168,93 @@ async def test_user_filesystem_scope_cannot_write_server_execution_state(tmp_pat
|
||||
assert not (tmp_path / target).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", ["authority", "jobs", "containment", "result", "exit", "database", "vault", "uploads"])
|
||||
@pytest.mark.parametrize("alias", ["direct", "relative", "symlink", "hardlink"])
|
||||
async def test_control_files_cannot_be_read_or_written_through_aliases(tmp_path, monkeypatch, state, alias):
|
||||
import src.constants as constants
|
||||
jobs = tmp_path / "jobs"
|
||||
jobs.mkdir()
|
||||
monkeypatch.setattr(constants, "BG_JOBS_DIR", str(jobs))
|
||||
monkeypatch.setattr(constants, "DATA_DIR", str(tmp_path))
|
||||
monkeypatch.setattr(constants, "UPLOAD_DIR", str(tmp_path / "uploads"))
|
||||
for name, filename in (("BG_JOBS_FILE", "jobs.json"), ("CONTAINMENT_STATE_FILE", "receipts.json"),
|
||||
("APP_DB", "private.db"), ("VAULT_FILE", "vault.json")):
|
||||
monkeypatch.setattr(constants, name, str(tmp_path / filename))
|
||||
filename = {"authority": "jobs/job.authority.json", "jobs": "jobs.json", "containment": "receipts.json",
|
||||
"result": "jobs/job.result.json", "exit": "jobs/job.exit", "database": "private.db",
|
||||
"vault": "vault.json", "uploads": "uploads/uploads.json"}[state]
|
||||
target = tmp_path / filename
|
||||
target.parent.mkdir(exist_ok=True)
|
||||
target.write_text("control-secret")
|
||||
selector = str(target)
|
||||
if alias == "relative":
|
||||
selector = "./" + filename
|
||||
elif alias in {"symlink", "hardlink"}:
|
||||
link = tmp_path / "ordinary.txt"
|
||||
try:
|
||||
link.symlink_to(target) if alias == "symlink" else os.link(target, link)
|
||||
except OSError as error:
|
||||
pytest.skip(f"Platform cannot create {alias}: {error}")
|
||||
selector = str(link)
|
||||
grant = authority(tmp_path, "read_file", "write_file")
|
||||
for tool, content in (("read_file", selector), ("write_file", selector + "\nforged")):
|
||||
_, result = await dispatch(grant, tool, content)
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
assert target.read_text() == "control-secret"
|
||||
|
||||
|
||||
async def test_directory_grep_does_not_scan_control_state_or_hardlinks(tmp_path, monkeypatch):
|
||||
import src.constants as constants
|
||||
control = tmp_path / "jobs.json"
|
||||
control.write_text("UNIQUE_CONTROL_SECRET")
|
||||
(tmp_path / "ordinary").write_text("visible text")
|
||||
os.link(control, tmp_path / "innocent.txt")
|
||||
monkeypatch.setattr(constants, "BG_JOBS_FILE", str(control))
|
||||
_, result = await dispatch(authority(tmp_path, "grep"), "grep", '{"pattern":"UNIQUE_CONTROL_SECRET","path":"."}')
|
||||
assert result["exit_code"] == 0
|
||||
assert "No matches" in result["output"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool,content", [("glob", '{"pattern":"*.json","path":"."}'), ("ls", ".")])
|
||||
async def test_directory_enumeration_does_not_address_control_files(tmp_path, monkeypatch, tool, content):
|
||||
import src.constants as constants
|
||||
control = tmp_path / "jobs.json"
|
||||
control.write_text("control")
|
||||
monkeypatch.setattr(constants, "BG_JOBS_FILE", str(control))
|
||||
_, result = await dispatch(authority(tmp_path, tool), tool, content)
|
||||
assert result["exit_code"] == 0
|
||||
assert "jobs.json" not in result["output"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("producer", ["database", "containment", "jobs", "uploads"])
|
||||
@pytest.mark.parametrize("alias", ["direct", "hardlink"])
|
||||
async def test_configured_control_producer_paths_are_protected(tmp_path, monkeypatch, producer, alias):
|
||||
target = tmp_path / "custom" / "state"
|
||||
target.parent.mkdir()
|
||||
if producer == "database":
|
||||
import core.database as database
|
||||
monkeypatch.setattr(database, "engine", SimpleNamespace(url=SimpleNamespace(
|
||||
get_backend_name=lambda: "sqlite", database=str(target))))
|
||||
elif producer == "containment":
|
||||
from src import containment
|
||||
monkeypatch.setattr(containment, "_store_path", lambda: target)
|
||||
elif producer == "jobs":
|
||||
from src import bg_jobs
|
||||
monkeypatch.setattr(bg_jobs, "_STORE", target)
|
||||
else:
|
||||
from src import tool_utils
|
||||
target = target.parent / "uploads.json"
|
||||
monkeypatch.setattr(tool_utils, "get_upload_handler", lambda: SimpleNamespace(upload_dir=str(target.parent)))
|
||||
target.write_text("server state")
|
||||
selector = str(target)
|
||||
if alias == "hardlink":
|
||||
link = tmp_path / "ordinary"
|
||||
os.link(target, link)
|
||||
selector = str(link)
|
||||
_, result = await dispatch(authority(tmp_path, "read_file"), "read_file", selector)
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("roots", [(), None])
|
||||
async def test_nonworkspace_allowlist_and_operation_do_not_grant_resources(tmp_path, monkeypatch, roots):
|
||||
from src import tool_execution as execution
|
||||
@@ -217,6 +329,43 @@ def test_child_cannot_renew_replaced_parent_root(tmp_path):
|
||||
assert parent.intersect(child).resource_roots == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("caller", ["intersection", "context", "task"])
|
||||
@pytest.mark.parametrize("child_location", ["root", "subtree"])
|
||||
def test_replaced_parent_cannot_be_renewed_by_new_child_observation(tmp_path, caller, child_location):
|
||||
root = tmp_path / "root"
|
||||
root.mkdir()
|
||||
parent = authority(root, "read_file")
|
||||
root.rename(tmp_path / "old")
|
||||
root.mkdir()
|
||||
sub = root / "sub"
|
||||
sub.mkdir()
|
||||
(sub / "a").write_text("replacement")
|
||||
child_root = FilesystemRoot.seal(root if child_location == "root" else sub, owner="alice")
|
||||
child = authority(root, "read_file", roots=(child_root,))
|
||||
if caller == "intersection":
|
||||
effective = parent.intersect(child)
|
||||
elif caller == "context":
|
||||
with bind_request_authority(parent), bind_request_authority(child) as effective:
|
||||
assert effective.resource_roots == ()
|
||||
else:
|
||||
with bind_request_authority(parent):
|
||||
sealed = seal_task_authority("Read files in the workspace", "llm", None, owner="alice")
|
||||
effective = restore_task_authority(sealed, "Read files in the workspace", "llm", None,
|
||||
owner="alice", session_id="continuation")
|
||||
assert effective.resource_roots == ()
|
||||
with pytest.raises(ValueError):
|
||||
resolve(effective, "read_file", "sub/a")
|
||||
|
||||
|
||||
def test_equal_stale_roots_are_revalidated(tmp_path):
|
||||
root = tmp_path / "root"
|
||||
root.mkdir()
|
||||
parent = authority(root, "read_file")
|
||||
root.rename(tmp_path / "old")
|
||||
root.mkdir()
|
||||
assert parent.intersect(parent).resource_roots == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("legacy", [False, True])
|
||||
async def test_snapshot_preserves_incarnation_and_never_reconstructs_legacy(tmp_path, legacy):
|
||||
root = tmp_path / "root"
|
||||
@@ -336,7 +485,7 @@ async def test_last_dispatch_validation_refuses_replacement_and_resets_context(t
|
||||
assert execution.get_active_workspace() is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("error_type", [RuntimeError, asyncio.CancelledError])
|
||||
@pytest.mark.parametrize("error_type", [None, RuntimeError, asyncio.CancelledError])
|
||||
async def test_nested_resource_context_restores_on_failure_or_cancellation(tmp_path, monkeypatch, error_type):
|
||||
from src import tool_execution as execution
|
||||
for name in ("parent", "child"):
|
||||
@@ -345,10 +494,15 @@ async def test_nested_resource_context_restores_on_failure_or_cancellation(tmp_p
|
||||
parent = resolve(grant, "read_file", "parent")
|
||||
async def implementation(block, **kwargs):
|
||||
assert active_resource_operation().bindings[0].resource.path == str(tmp_path / "child")
|
||||
raise error_type("stop")
|
||||
if error_type:
|
||||
raise error_type("stop")
|
||||
return "read", {"exit_code": 0}
|
||||
monkeypatch.setattr(execution, "_execute_tool_block_impl", implementation)
|
||||
with bind_resource_operation(parent):
|
||||
with pytest.raises(error_type):
|
||||
if error_type:
|
||||
with pytest.raises(error_type):
|
||||
await dispatch(grant, "read_file", "child")
|
||||
else:
|
||||
await dispatch(grant, "read_file", "child")
|
||||
assert active_resource_operation() is parent
|
||||
assert active_resource_operation() is None
|
||||
@@ -480,6 +634,63 @@ async def test_exact_user_approval_binds_only_one_missing_destination(tmp_path):
|
||||
assert not (tmp_path / "other.txt").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", [1, 2])
|
||||
async def test_restored_empty_roots_approval_is_exact_and_never_restores_generic_scope(tmp_path, version):
|
||||
(tmp_path / "approved").write_text("approved content")
|
||||
(tmp_path / "sibling").write_text("private sibling")
|
||||
snapshot = authority(tmp_path, "read_file", "write_file", "ls").to_dict()
|
||||
snapshot["version"] = version
|
||||
snapshot["resource_roots"] = []
|
||||
restored = RequestAuthority.from_dict(snapshot)
|
||||
exact, security = approval(restored, "read_file", "approved")
|
||||
assert exact.pending.resource_operation is not None
|
||||
for tool, content in (("read_file", "sibling"), ("ls", "."), ("write_file", "sibling\nx")):
|
||||
_, blocked = await dispatch(restored, tool, content, exact_approval=exact, security_context=security)
|
||||
assert blocked["exit_code"] == 1
|
||||
_, unapproved = await dispatch(restored, tool, content)
|
||||
assert unapproved["failure_kind"] == "resource_identity_denied"
|
||||
_, allowed = await dispatch(restored, "read_file", "approved", exact_approval=exact, security_context=security)
|
||||
assert allowed["output"] == "approved content"
|
||||
_, replay = await dispatch(restored, "read_file", "approved", exact_approval=exact, security_context=security)
|
||||
assert replay["exit_code"] == 1
|
||||
assert restored.resource_roots == () and restored.backend_resources == ()
|
||||
assert (tmp_path / "sibling").read_text() == "private sibling"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["alias", "request", "session", "owner"])
|
||||
async def test_restored_exact_filesystem_binding_rejects_retarget_and_rebinding(tmp_path, change):
|
||||
(tmp_path / "a").write_text("a")
|
||||
(tmp_path / "b").write_text("b")
|
||||
(tmp_path / "alias").symlink_to(tmp_path / "a")
|
||||
snapshot = authority(tmp_path, "read_file").to_dict()
|
||||
snapshot["version"] = 1
|
||||
restored = RequestAuthority.from_dict(snapshot)
|
||||
exact, security = approval(restored, "read_file", "alias")
|
||||
if change == "alias":
|
||||
(tmp_path / "alias").unlink()
|
||||
(tmp_path / "alias").symlink_to(tmp_path / "b")
|
||||
else:
|
||||
restored = replace(restored, **{"request": {"request_id": "other"},
|
||||
"session": {"session_id": "other"}, "owner": {"owner": "bob"}}[change])
|
||||
_, result = await dispatch(restored, "read_file", "alias", exact_approval=exact, security_context=security)
|
||||
assert result["exit_code"] == 1
|
||||
assert exact.matches(owner="alice", session_id="s", workspace=str(tmp_path), tool_name="read_file", content="alias")
|
||||
|
||||
|
||||
async def test_resumed_child_approval_cannot_renew_replaced_parent_root(tmp_path):
|
||||
root = tmp_path / "root"
|
||||
root.mkdir()
|
||||
parent = authority(root, "read_file")
|
||||
root.rename(tmp_path / "old")
|
||||
root.mkdir()
|
||||
(root / "new").write_text("replacement")
|
||||
child = parent.intersect(authority(root, "read_file"))
|
||||
exact, security = approval(child, "read_file", "new")
|
||||
assert child.resource_roots == () and exact.pending.resource_operation is None
|
||||
_, result = await dispatch(replace(child, inherited=False), "read_file", "new", exact_approval=exact, security_context=security)
|
||||
assert result["failure_kind"] == "resource_identity_denied" and not exact._claimed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("request_text,denied", [
|
||||
("Transcribe /workspace/audio.wav", "read_file"),
|
||||
("OCR extract exact text from /workspace/image.png", "write_file"),
|
||||
|
||||
@@ -204,6 +204,16 @@ async def test_dispatcher_claims_approval_immediately_before_execution(monkeypat
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatcher_uses_sealed_document_target(monkeypatch):
|
||||
import src.tool_execution as tool_execution
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
from src.agent_runtime import owned_resources
|
||||
# This dispatcher fixture seals an observed owned row, as production does;
|
||||
# model/document text alone cannot stand in for a resource identity.
|
||||
row = SimpleNamespace(id="document-7", owner="alice", session_id="session-1",
|
||||
version_count=4, current_content="original", created_at=datetime(2026, 1, 1),
|
||||
updated_at=datetime(2026, 1, 2))
|
||||
monkeypatch.setattr(owned_resources, "_row", lambda namespace, identifier, owner: row
|
||||
if (namespace, identifier, owner) == ("documents", "document-7", "alice") else None)
|
||||
|
||||
store = ToolApprovalStore()
|
||||
content = '{"content":"replacement"}'
|
||||
|
||||
Reference in New Issue
Block a user