mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-06 05:02:24 +01:00
feat(runtime): enforce server request authority
This commit is contained in:
+38
-1
@@ -86,6 +86,7 @@ from src.model_profiles import (
|
||||
tool_schema_profile,
|
||||
)
|
||||
from src.tool_execution import AgentExecutionBridge, bind_execution_bridge
|
||||
from src.agent_runtime.authority import is_internal_tool_request, request_authority_for_http
|
||||
from src.turn_contract import (
|
||||
FAMILY_TOOLS, bind_turn_contract, preserve_bound_editor_selected_tools,
|
||||
requested_capabilities, resolve_turn_contract,
|
||||
@@ -95,6 +96,15 @@ from src.turn_contract import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _append_internal_chat_context(ctx, message):
|
||||
tagged = untrusted_context_message("internal tool request", message)
|
||||
ctx.messages.append(tagged)
|
||||
routed = getattr(ctx, "route_messages", None)
|
||||
if routed is not None and routed is not ctx.messages:
|
||||
routed.append(tagged)
|
||||
|
||||
|
||||
# Track active streams for partial-save safety net
|
||||
_active_streams: Dict[str, dict] = {}
|
||||
|
||||
@@ -2196,7 +2206,10 @@ def setup_chat_routes(
|
||||
webhook_manager=webhook_manager,
|
||||
allow_tool_preprocessing=allow_tool_preprocessing,
|
||||
defer_context_shaping=foreground_policy.enabled,
|
||||
persist_user_message=not is_internal_tool_request(request),
|
||||
)
|
||||
if is_internal_tool_request(request):
|
||||
_append_internal_chat_context(ctx, message)
|
||||
|
||||
# Research injection
|
||||
research_blocked_by_policy = (
|
||||
@@ -2648,6 +2661,8 @@ def setup_chat_routes(
|
||||
)
|
||||
owner = effective_user(request)
|
||||
if tool_approval_id:
|
||||
from src.agent_runtime.authority import require_user_approval_request
|
||||
require_user_approval_request(request)
|
||||
pending_tool_approval = tool_approval_store.peek(tool_approval_id)
|
||||
normalized_owner = str(owner or "").strip().casefold()
|
||||
if (
|
||||
@@ -2907,10 +2922,12 @@ def setup_chat_routes(
|
||||
and pending_tool_approval.continuation_query
|
||||
else None
|
||||
),
|
||||
persist_user_message=not tool_approval_continuation,
|
||||
persist_user_message=not tool_approval_continuation and not is_internal_tool_request(request),
|
||||
interaction_mode=chat_mode,
|
||||
auto_escalated=auto_escalated,
|
||||
)
|
||||
if is_internal_tool_request(request):
|
||||
_append_internal_chat_context(ctx, message)
|
||||
|
||||
_research_flags = {"do": do_research} # Mutable container for generator scope
|
||||
|
||||
@@ -3349,6 +3366,24 @@ def setup_chat_routes(
|
||||
"manage_documents", "create_document", "edit_document", "update_document",
|
||||
}.issubset(disabled_tools),
|
||||
}
|
||||
# Capture permission state before schema selection/reconciliation.
|
||||
# Only deterministic request intent supplies grants, never inventory.
|
||||
_request_authority = request_authority_for_http(
|
||||
request, message, owner=_user, session_id=session, workspace=workspace,
|
||||
history=_turn_history, policy=tool_policy,
|
||||
active_document=bool(active_doc),
|
||||
image_attachment=any(str(a.get('mime') or '').startswith('image/')
|
||||
for a in (ctx.preprocessed.attachment_meta or [])),
|
||||
capabilities=({'search_browser'} if (
|
||||
_explicit_browser_intent or _external_discovery_intent
|
||||
) else ()),
|
||||
)
|
||||
if exact_tool_approval is not None:
|
||||
from src.agent_runtime.authority import RequestAuthority
|
||||
_request_authority = (
|
||||
exact_tool_approval.pending.request_authority
|
||||
or RequestAuthority.empty(owner=_user, session_id=session, workspace=workspace)
|
||||
).restrict(tool_policy)
|
||||
_turn_contract = None
|
||||
# Image models execute directly, not through the text-agent inventory.
|
||||
# Keep the permission policy above, but do not apply routing omissions
|
||||
@@ -3477,6 +3512,7 @@ def setup_chat_routes(
|
||||
warm_tools=_warm_tools,
|
||||
message=message, history=getattr(sess, "history", []) or [],
|
||||
)
|
||||
_request_authority = _request_authority.restrict(_contract_policy)
|
||||
# Resolution already applies user, owner, and global policy. An
|
||||
# admitted tool must not later be rejected by the stale
|
||||
# pre-contract disabled snapshot during execution.
|
||||
@@ -4415,6 +4451,7 @@ def setup_chat_routes(
|
||||
cwd=_agent_turn_cwd(sess, client_runtime_context),
|
||||
forced_tools=_forced_tools,
|
||||
turn_contract=_turn_contract,
|
||||
request_authority=_request_authority,
|
||||
uploaded_files=ctx.uploaded_files,
|
||||
defer_context_shaping=_foreground_policy.enabled,
|
||||
external_untrusted_context_seen=external_untrusted_context_seen,
|
||||
|
||||
+16
-1
@@ -534,11 +534,13 @@ async def _run_skill_test_job(
|
||||
messages=None,
|
||||
transcript=None,
|
||||
exact_approval=None,
|
||||
request_authority=None,
|
||||
):
|
||||
"""Background coroutine: run the skill in an agent loop, capture a condensed
|
||||
log + transcript, then have the judge grade it. Writes into _skill_test_jobs."""
|
||||
import json as _json
|
||||
from src.agent_loop import stream_agent_loop
|
||||
from src.agent_runtime.authority import RequestAuthority
|
||||
|
||||
job = _skill_test_jobs.get(key)
|
||||
if job is None:
|
||||
@@ -559,6 +561,9 @@ async def _run_skill_test_job(
|
||||
url, model, messages, headers=headers,
|
||||
temperature=0.3, max_tokens=0, max_rounds=8, owner=owner,
|
||||
exact_approval=exact_approval,
|
||||
request_authority=(request_authority or (
|
||||
exact_approval.pending.request_authority if exact_approval is not None else None
|
||||
) or RequestAuthority.empty(owner=owner)),
|
||||
):
|
||||
if not chunk.startswith("data: ") or chunk.strip() == "data: [DONE]":
|
||||
continue
|
||||
@@ -1039,6 +1044,7 @@ async def _run_skill_audit_arm(messages: list[dict], url, model, headers, owner,
|
||||
"""Run one audit arm in the agent loop; return transcript, stats, approval."""
|
||||
import json as _json
|
||||
from src.agent_loop import stream_agent_loop
|
||||
from src.agent_runtime.authority import RequestAuthority, active_request_authority
|
||||
transcript = []
|
||||
approval_required = None
|
||||
stats = {"turns": 0, "tool_calls": 0}
|
||||
@@ -1051,6 +1057,7 @@ async def _run_skill_audit_arm(messages: list[dict], url, model, headers, owner,
|
||||
url, model, messages, headers=headers,
|
||||
temperature=0.3, max_tokens=4096, max_rounds=8,
|
||||
owner=owner, workload=workload, suppress_skills=True,
|
||||
request_authority=(active_request_authority() or RequestAuthority.empty(owner=owner)),
|
||||
):
|
||||
# Streams can include an SSE event line before the data line,
|
||||
# notably `event: error`. Do not silently discard those failures.
|
||||
@@ -2001,6 +2008,9 @@ def setup_skills_routes(skills_manager: SkillsManager) -> APIRouter:
|
||||
user = _owner(request)
|
||||
body = await request.json()
|
||||
task = (body.get("task") or "").strip()
|
||||
from src.agent_runtime.authority import RequestAuthority, request_authority_for_http
|
||||
request_authority = (request_authority_for_http(request, task, owner=user)
|
||||
if task else RequestAuthority.empty(owner=user))
|
||||
|
||||
skills = skills_manager.load(owner=user)
|
||||
match = next((s for s in skills if s.get("name") == skill_id or s.get("id") == skill_id), None)
|
||||
@@ -2064,9 +2074,12 @@ def setup_skills_routes(skills_manager: SkillsManager) -> APIRouter:
|
||||
"model": model,
|
||||
"headers": headers,
|
||||
"owner": user,
|
||||
"request_authority": request_authority,
|
||||
},
|
||||
}
|
||||
_asyncio.create_task(_run_skill_test_job(key, name, md, task, url, model, headers, user, skills_manager))
|
||||
_asyncio.create_task(_run_skill_test_job(
|
||||
key, name, md, task, url, model, headers, user, skills_manager,
|
||||
request_authority=request_authority))
|
||||
return {"ok": True, "status": "running", "skill": name, "model": model}
|
||||
|
||||
@router.post("/{skill_id}/test-approval")
|
||||
@@ -2074,6 +2087,8 @@ def setup_skills_routes(skills_manager: SkillsManager) -> APIRouter:
|
||||
"""Resume a manual skill test with one exact server-sealed action."""
|
||||
import asyncio as _asyncio
|
||||
from src.tool_approvals import tool_approval_store
|
||||
from src.agent_runtime.authority import require_user_approval_request
|
||||
require_user_approval_request(request)
|
||||
|
||||
user = _owner(request)
|
||||
skills = skills_manager.load(owner=user)
|
||||
|
||||
@@ -25,6 +25,14 @@ from routes.prefs_routes import _load_for_user, _save_for_user
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _seal_request_task_authority(request, prompt, task_type, action, owner):
|
||||
from src.agent_runtime.authority import MISSING_AUTHORITY, is_internal_tool_request, seal_task_authority
|
||||
# A tool HTTP call starts another ASGI context. Its model-produced body is
|
||||
# not a fresh user request, even though the internal token authenticates it.
|
||||
parent = None if is_internal_tool_request(request) else MISSING_AUTHORITY
|
||||
return seal_task_authority(prompt, task_type, action, owner=owner, parent_authority=parent)
|
||||
|
||||
|
||||
def _maybe_cascade_calendar_event(task) -> None:
|
||||
"""Delete the linked calendar event when a cookbook_serve task is
|
||||
removed. Two lookup strategies:
|
||||
@@ -530,6 +538,8 @@ def setup_task_routes(task_scheduler) -> APIRouter:
|
||||
owner=user,
|
||||
name=name,
|
||||
prompt=req.prompt,
|
||||
request_authority_json=_seal_request_task_authority(
|
||||
request, req.prompt, req.task_type, req.action, user),
|
||||
task_type=req.task_type,
|
||||
action=req.action,
|
||||
schedule=req.schedule,
|
||||
@@ -737,6 +747,9 @@ def setup_task_routes(task_scheduler) -> APIRouter:
|
||||
task.task_type = req.task_type
|
||||
if req.action is not None:
|
||||
task.action = req.action
|
||||
if any(value is not None for value in (req.prompt, req.task_type, req.action)):
|
||||
task.request_authority_json = _seal_request_task_authority(
|
||||
request, task.prompt, task.task_type, task.action, user)
|
||||
if req.output_target is not None:
|
||||
task.output_target = req.output_target
|
||||
if req.model is not None:
|
||||
|
||||
Reference in New Issue
Block a user