feat(curator): authority routing — mutating tools queue for review (C3/5)
The interceptor that closes the loop on the curator review queue.
With this commit, the curator can call update_note / update_milestone
/ update_project / update_profile / delete_note — those calls are
caught by execute_tool's authority='curator' path, snapshotted, and
written to pending_curator_actions for the user to approve or reject
later. Additive tools still run immediately.
services/tools/_registry.py:
- New _CURATOR_MUTATING_TOOLS frozenset: {update_note, update_milestone,
update_project, update_profile, delete_note}. update_event /
delete_event intentionally excluded — calendar events should always
be explicit user intent.
- execute_tool gains a keyword-only parameter, defaulting
to 'user'. Default behaviour is unchanged; existing callers keep
working without changes.
- When authority='curator' AND tool is in _CURATOR_MUTATING_TOOLS,
_queue_for_review captures a snapshot of the target via a per-tool
helper and writes a pending action. Returns {success:true,
pending:true, action_id:N, message:...} so the curator sees the
call as 'completed' for its bookkeeping.
- Per-tool snapshot helpers: _snapshot_note (covers update_note +
delete_note — uses the same fuzzy match update_note_tool uses, so
the snapshot reflects what'd actually be mutated), _snapshot_milestone,
_snapshot_project, _snapshot_profile. Snapshot capture is best-effort
— failure logs but still queues with empty snapshot so a curator
proposal never silently drops.
services/curator.py:
- Allowlist now includes the five mutating tools. They're safe to expose
because execute_tool intercepts them; the curator can propose without
being able to actually mutate.
- The execute_tool call now passes authority='curator'.
- System prompt explicitly authorizes the proposal pattern:
'update_note', 'update_milestone', 'update_project', 'update_profile',
'delete_note' are described as proposing tools that wait for user
approval. 'Don't try to update or delete anything' line removed.
services/pending_actions.py:
- approve() now passes authority='user' on the replay so the curator
interceptor doesn't re-route the replay back into pending and create
an infinite loop.
What's left in the queue:
- C4: API routes (list/approve/reject endpoints).
- C5: Frontend Needs Review panel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -131,17 +131,15 @@ async def approve(action_id: int, user_id: int) -> dict[str, Any]:
|
||||
|
||||
from fabledassistant.services.tools import execute_tool
|
||||
try:
|
||||
# NOTE (C2 → C3 hand-off): execute_tool gains an `authority` kwarg
|
||||
# in commit C3 that the curator interceptor checks. Approval replay
|
||||
# MUST pass authority="user" once that lands, otherwise the
|
||||
# interceptor re-routes the replay back into pending and loops.
|
||||
# Until C3 lands, execute_tool is identity (no interceptor exists
|
||||
# yet), so this call runs cleanly.
|
||||
# authority="user" bypasses the curator interceptor — required so
|
||||
# the replay actually executes the mutating tool instead of
|
||||
# creating another pending row (which would infinite-loop).
|
||||
result = await execute_tool(
|
||||
user_id,
|
||||
row.action_type,
|
||||
row.payload,
|
||||
conv_id=row.conv_id,
|
||||
authority="user",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
|
||||
Reference in New Issue
Block a user