feat: proper confirmation-resume flow + per-user tool policy

Fixes the broken confirmation gate where users had no way to approve
or deny a blocked tool call in the web UI.

Changes:
- orchestrator_engine.py: add OrchestrateCheckpoint dataclass, extract
  loop into _run_from_contents(), add resume() function
- openai_orchestrator.py: same treatment — _run_from_messages(), resume()
- routers/orchestrator.py: POST /{job_id}/confirm and /deny endpoints,
  separate _checkpoints store, _resume_job() + _finalize_job() helpers,
  "awaiting_confirmation" job status with pending_confirmation payload
- auth_utils.py: get_tool_policy() and save_tool_policy() helpers reading
  home/{user}/tool_policy.json (allow/deny lists)
- routers/orchestrator.py: loads tool_policy per user and passes
  confirm_allow/confirm_deny to both engines
- app.js: poll loop handles awaiting_confirmation — shows Confirm/Deny
  buttons inline, resumes polling after user action
- settings.html + settings.py: Tool Permissions section with allow/deny
  textareas, POST /settings/tool-policy route
- style.css: .confirm-gate, .confirm-btn, .deny-btn styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-30 19:14:53 -04:00
parent bce7de647c
commit 6405dd338d
8 changed files with 733 additions and 159 deletions

View File

@@ -546,6 +546,25 @@
.message.thinking { color: var(--muted); font-style: italic; }
/* Confirmation gate */
.confirm-gate { display: flex; flex-direction: column; gap: 0.6rem; }
.confirm-gate p { margin: 0; }
.confirm-tools { font-size: 0.82rem; color: var(--muted); }
.confirm-actions { display: flex; gap: 0.5rem; margin-top: 0.25rem; }
.confirm-btn, .deny-btn {
padding: 0.35rem 0.9rem;
border-radius: 6px;
border: none;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s;
}
.confirm-btn { background: #16a34a; color: #fff; }
.confirm-btn:hover { opacity: 0.85; }
.deny-btn { background: var(--surface); border: 1px solid var(--border); color: var(--text); }
.deny-btn:hover { border-color: var(--muted); }
/* Copy button */
.message.assistant, .message.user { position: relative; }