mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-14 20:22:22 +02:00
Merge commit from fork
* fix(security): stop API tokens reaching privileged agent tools A bearer API token resolves to the human who minted it, and minting is admin-only, so every owner-keyed privilege check in the agent path answers "admin". A token issued for a narrow integration therefore reached bash and python with the authority of the account that created it. Three independent routes to that sink, each closed here. The token could answer its own tool-approval prompt. An approval records that a person authorized one dangerous action, and a token cannot make that statement, so /api/chat_stream now refuses an approval resume from a bearer caller. The chat-session grant was reconstructable from caller-supplied message metadata. Two routes persist a metadata blob on the caller's behalf, so the shape of a resolved approval card could be written straight into a transcript and was then read back as authority. The server now signs the grant when it resolves an approval and verifies that signature when reading it back, binding it to the chat and the approval it was issued for. Both routes also drop server-owned keys from an inbound blob. A run driven by a token inherited its owner's tool set. Such a run is now capped at the non-admin policy regardless of who minted the credential, which holds even where no approval is raised at all. The human path is unchanged: a browser session still receives the prompt, still approves, and a granted chat-session scope still carries to later turns in that chat. Scope enforcement across the wider route surface is a separate gap and is not addressed here. * fix scoped chat delegation boundaries * fix(auth): reject malformed chat approval signatures --------- Co-authored-by: RaresKeY <158580472+RaresKeY@users.noreply.github.com>
This commit is contained in:
@@ -1291,6 +1291,58 @@ def test_approval_pause_does_not_trigger_teacher_takeover(monkeypatch):
|
||||
)
|
||||
|
||||
|
||||
def test_teacher_takeover_inherits_delegated_and_tainted_run_authority(monkeypatch):
|
||||
from src.prompt_security import untrusted_context_message
|
||||
|
||||
import src.agent_loop as agent_loop
|
||||
import src.teacher_escalation as teacher_escalation
|
||||
|
||||
monkeypatch.setattr(
|
||||
agent_loop,
|
||||
"get_setting",
|
||||
lambda key, default=None: default,
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(agent_loop, "get_mcp_manager", lambda: None, raising=False)
|
||||
monkeypatch.setattr(agent_loop, "estimate_tokens", lambda *args, **kwargs: 10)
|
||||
monkeypatch.setattr(
|
||||
agent_loop,
|
||||
"blocked_tools_for_owner",
|
||||
lambda owner: set(),
|
||||
raising=False,
|
||||
)
|
||||
|
||||
async def fake_stream(*args, **kwargs):
|
||||
yield "data: " + json.dumps({"delta": "finished"}) + "\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
captured = {}
|
||||
|
||||
async def capture_teacher(*args, **kwargs):
|
||||
captured.update(kwargs)
|
||||
if False:
|
||||
yield "" # pragma: no cover
|
||||
|
||||
monkeypatch.setattr(agent_loop, "stream_llm_with_fallback", fake_stream)
|
||||
monkeypatch.setattr(teacher_escalation, "run_teacher_inline", capture_teacher)
|
||||
_collect_agent_events(
|
||||
agent_loop.stream_agent_loop(
|
||||
"http://local.test/v1",
|
||||
"qwen-local-model",
|
||||
[
|
||||
{"role": "user", "content": "finish it"},
|
||||
untrusted_context_message("stored context", "untrusted"),
|
||||
],
|
||||
session_id="session-1",
|
||||
max_rounds=1,
|
||||
delegated_credential=True,
|
||||
)
|
||||
)
|
||||
|
||||
assert captured["delegated_credential"] is True
|
||||
assert captured["external_untrusted_context_seen"] is True
|
||||
|
||||
|
||||
def test_frontend_tool_approval_uses_opaque_id_and_fixed_decisions():
|
||||
root = Path(__file__).parents[1]
|
||||
chat = (root / "static/js/chat.js").read_text()
|
||||
|
||||
Reference in New Issue
Block a user