Files
odysseus/tests/test_owned_resource_identity.py
T

430 lines
24 KiB
Python

"""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()