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:
@@ -1192,6 +1192,37 @@
|
||||
: '⚡ working…';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (job.status === 'awaiting_confirmation') {
|
||||
const pc = job.pending_confirmation || {};
|
||||
const toolNames = (pc.tools || []).map(t => t.name).join(', ');
|
||||
thinkingDiv.className = 'message assistant';
|
||||
thinkingDiv.innerHTML = `<div class="confirm-gate">
|
||||
<p>${escapeHtml(pc.message || 'Confirm this action?')}</p>
|
||||
<p class="confirm-tools">Tool${(pc.tools||[]).length !== 1 ? 's' : ''}: <code>${escapeHtml(toolNames)}</code></p>
|
||||
<div class="confirm-actions">
|
||||
<button class="confirm-btn">Confirm</button>
|
||||
<button class="deny-btn">Deny</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const confirmed = await new Promise(resolve => {
|
||||
thinkingDiv.querySelector('.confirm-btn').onclick = () => resolve(true);
|
||||
thinkingDiv.querySelector('.deny-btn').onclick = () => resolve(false);
|
||||
});
|
||||
|
||||
thinkingDiv.className = 'message assistant thinking';
|
||||
thinkingDiv.textContent = confirmed ? '⚡ confirmed — continuing…' : '⚡ denied — finishing…';
|
||||
|
||||
const action = confirmed ? 'confirm' : 'deny';
|
||||
const resumeRes = await fetch(`/orchestrate/${job_id}/${action}`, {
|
||||
method: 'POST',
|
||||
signal: activeController.signal,
|
||||
});
|
||||
if (!resumeRes.ok) throw new Error(`Resume failed: HTTP ${resumeRes.status}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -393,6 +393,35 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Tool Permissions -->
|
||||
<div class="section">
|
||||
<h2>Tool Permissions</h2>
|
||||
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.5rem; line-height:1.55;">
|
||||
Override the default confirmation gate for orchestrator tools.
|
||||
<strong>Allow list</strong> — tools that run without asking for confirmation.
|
||||
<strong>Deny list</strong> — tools that are always blocked for your account.
|
||||
One tool name per line.
|
||||
</p>
|
||||
<p style="font-size:0.78rem; color:var(--pg-muted); margin-bottom:0.85rem;">
|
||||
Tools requiring confirmation by default: <code>{{ confirm_required_tools }}</code>
|
||||
</p>
|
||||
<form method="POST" action="/settings/tool-policy">
|
||||
<div class="form-group">
|
||||
<label for="allow_list">Allow list (bypass confirmation)</label>
|
||||
<textarea id="allow_list" name="allow_list" rows="3"
|
||||
placeholder="reminders_clear cron_remove"
|
||||
autocomplete="off" spellcheck="false">{{ tool_allow }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="deny_list">Deny list (always block)</label>
|
||||
<textarea id="deny_list" name="deny_list" rows="3"
|
||||
placeholder="shell_exec file_write"
|
||||
autocomplete="off" spellcheck="false">{{ tool_deny }}</textarea>
|
||||
</div>
|
||||
<button type="submit">Save tool permissions</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Browser cache -->
|
||||
<div class="section">
|
||||
<h2>Browser Cache</h2>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user