|
@@ -191,6 +191,21 @@ _running_runners: Dict[str, Any] = {}
|
|
|
_approval_locks: Dict[str, asyncio.Lock] = {}
|
|
_approval_locks: Dict[str, asyncio.Lock] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _require_mutable_trace(trace) -> None:
|
|
|
|
|
+ """将核心协议的只读门禁统一映射为 HTTP 409。"""
|
|
|
|
|
+ from cyber_agent.core.agent_mode import (
|
|
|
|
|
+ RECURSIVE_REVISION_READ_ONLY_ERROR,
|
|
|
|
|
+ require_mutable_trace_policy,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ require_mutable_trace_policy(trace.context)
|
|
|
|
|
+ except ValueError as exc:
|
|
|
|
|
+ if str(exc) == RECURSIVE_REVISION_READ_ONLY_ERROR:
|
|
|
|
|
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
async def _cancel_and_wait_for_running_task(trace_id: str) -> None:
|
|
async def _cancel_and_wait_for_running_task(trace_id: str) -> None:
|
|
|
"""等待旧后台运行完成 finally 清理,再允许同一 Trace 续跑。"""
|
|
"""等待旧后台运行完成 finally 清理,再允许同一 Trace 续跑。"""
|
|
|
old_task = _running_tasks.get(trace_id)
|
|
old_task = _running_tasks.get(trace_id)
|
|
@@ -215,7 +230,12 @@ async def _restore_runner_and_config(
|
|
|
):
|
|
):
|
|
|
"""Restore the persisted run contract and its trusted project environment."""
|
|
"""Restore the persisted run contract and its trusted project environment."""
|
|
|
import importlib
|
|
import importlib
|
|
|
- from cyber_agent.core.agent_mode import AgentMode, policy_from_context
|
|
|
|
|
|
|
+ from cyber_agent.core.agent_mode import (
|
|
|
|
|
+ AgentMode,
|
|
|
|
|
+ RECURSIVE_REVISION_READ_ONLY_ERROR,
|
|
|
|
|
+ policy_from_context,
|
|
|
|
|
+ require_mutable_trace_policy,
|
|
|
|
|
+ )
|
|
|
from cyber_agent.core.run_snapshot import (
|
|
from cyber_agent.core.run_snapshot import (
|
|
|
RUN_CONFIG_SNAPSHOT_CONTEXT_KEY,
|
|
RUN_CONFIG_SNAPSHOT_CONTEXT_KEY,
|
|
|
RunConfigSnapshotV1,
|
|
RunConfigSnapshotV1,
|
|
@@ -225,6 +245,13 @@ async def _restore_runner_and_config(
|
|
|
)
|
|
)
|
|
|
from cyber_agent.core.runner import RunConfig
|
|
from cyber_agent.core.runner import RunConfig
|
|
|
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ require_mutable_trace_policy(trace.context)
|
|
|
|
|
+ except ValueError as exc:
|
|
|
|
|
+ if str(exc) == RECURSIVE_REVISION_READ_ONLY_ERROR:
|
|
|
|
|
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
config = RunConfig(
|
|
config = RunConfig(
|
|
|
trace_id=trace.trace_id,
|
|
trace_id=trace.trace_id,
|
|
|
after_sequence=after_sequence,
|
|
after_sequence=after_sequence,
|
|
@@ -795,6 +822,8 @@ async def stop_trace(trace_id: str):
|
|
|
|
|
|
|
|
if runner.trace_store:
|
|
if runner.trace_store:
|
|
|
trace = await runner.trace_store.get_trace(trace_id)
|
|
trace = await runner.trace_store.get_trace(trace_id)
|
|
|
|
|
+ if trace:
|
|
|
|
|
+ _require_mutable_trace(trace)
|
|
|
if trace and trace.status == "waiting_confirmation":
|
|
if trace and trace.status == "waiting_confirmation":
|
|
|
batch = (
|
|
batch = (
|
|
|
await runner.trace_store.get_tool_approval_batch(trace_id)
|
|
await runner.trace_store.get_tool_approval_batch(trace_id)
|
|
@@ -912,6 +941,10 @@ async def review_extraction(trace_id: str, extraction_id: str, req: ReviewReques
|
|
|
runner = _get_runner()
|
|
runner = _get_runner()
|
|
|
if not runner.trace_store:
|
|
if not runner.trace_store:
|
|
|
raise HTTPException(status_code=503, detail="TraceStore not configured")
|
|
raise HTTPException(status_code=503, detail="TraceStore not configured")
|
|
|
|
|
+ trace = await runner.trace_store.get_trace(trace_id)
|
|
|
|
|
+ if not trace:
|
|
|
|
|
+ raise HTTPException(status_code=404, detail=f"Trace not found: {trace_id}")
|
|
|
|
|
+ _require_mutable_trace(trace)
|
|
|
|
|
|
|
|
if req.decision not in ("approve", "edit", "discard"):
|
|
if req.decision not in ("approve", "edit", "discard"):
|
|
|
raise HTTPException(
|
|
raise HTTPException(
|
|
@@ -942,6 +975,10 @@ async def commit_extractions(trace_id: str):
|
|
|
runner = _get_runner()
|
|
runner = _get_runner()
|
|
|
if not runner.trace_store:
|
|
if not runner.trace_store:
|
|
|
raise HTTPException(status_code=503, detail="TraceStore not configured")
|
|
raise HTTPException(status_code=503, detail="TraceStore not configured")
|
|
|
|
|
+ trace = await runner.trace_store.get_trace(trace_id)
|
|
|
|
|
+ if not trace:
|
|
|
|
|
+ raise HTTPException(status_code=404, detail=f"Trace not found: {trace_id}")
|
|
|
|
|
+ _require_mutable_trace(trace)
|
|
|
|
|
|
|
|
from cyber_agent.trace.extraction_review import commit_approved
|
|
from cyber_agent.trace.extraction_review import commit_approved
|
|
|
report = await commit_approved(runner.trace_store, trace_id)
|
|
report = await commit_approved(runner.trace_store, trace_id)
|
|
@@ -1052,6 +1089,11 @@ async def reconcile_traces():
|
|
|
tid = trace.trace_id
|
|
tid = trace.trace_id
|
|
|
if tid in _running_tasks and not _running_tasks[tid].done():
|
|
if tid in _running_tasks and not _running_tasks[tid].done():
|
|
|
continue
|
|
continue
|
|
|
|
|
+ # Recursive revision 2 是历史只读记录,启动对账不得修改
|
|
|
|
|
+ # 其审批文件或 Trace.status。
|
|
|
|
|
+ from cyber_agent.core.agent_mode import policy_from_context
|
|
|
|
|
+ if policy_from_context(trace.context).is_read_only:
|
|
|
|
|
+ continue
|
|
|
batch = (
|
|
batch = (
|
|
|
await runner.trace_store.get_tool_approval_batch(tid)
|
|
await runner.trace_store.get_tool_approval_batch(tid)
|
|
|
if hasattr(runner.trace_store, "get_tool_approval_batch")
|
|
if hasattr(runner.trace_store, "get_tool_approval_batch")
|