refactor: migrate Tool Permissions from Settings to /settings/tools
- Remove Tool Permissions form from settings.html; replace with a "Tool Settings →" link that redirects to /settings/tools - Add Confirmation Gate section to tools_settings.html (allow/deny textareas) inside the same form as risk policy — one save covers all - tools_settings.py save handler now writes allow/deny alongside max_risk/whitelist/blacklist into tool_policy.json - Remove /settings/tool-policy POST route from settings.py (no longer needed) - Remove get_tool_policy, save_tool_policy, CONFIRM_REQUIRED imports from settings.py (now owned by tools_settings.py) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,7 @@ import jwt
|
|||||||
from fastapi import APIRouter, Form, Request
|
from fastapi import APIRouter, Form, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
from auth_utils import COOKIE_NAME, decode_token, check_credentials, set_password, _read_auth, _write_auth, get_user_channels, get_tool_policy, save_tool_policy
|
from auth_utils import COOKIE_NAME, decode_token, check_credentials, set_password, _read_auth, _write_auth, get_user_channels
|
||||||
from tools import CONFIRM_REQUIRED
|
|
||||||
from persona import list_user_personas
|
from persona import list_user_personas
|
||||||
from config import settings as app_settings
|
from config import settings as app_settings
|
||||||
|
|
||||||
@@ -119,15 +118,6 @@ def _settings_page(username: str, personas: list[str], back_persona: str = "", s
|
|||||||
http_allowlist_text = ""
|
http_allowlist_text = ""
|
||||||
html = html.replace("{{ http_allowlist }}", http_allowlist_text)
|
html = html.replace("{{ http_allowlist }}", http_allowlist_text)
|
||||||
|
|
||||||
# Tool permission policy
|
|
||||||
policy = get_tool_policy(username)
|
|
||||||
tool_allow_text = _html.escape("\n".join(policy.get("allow", [])))
|
|
||||||
tool_deny_text = _html.escape("\n".join(policy.get("deny", [])))
|
|
||||||
confirm_tools_list = _html.escape(", ".join(sorted(CONFIRM_REQUIRED)))
|
|
||||||
html = html.replace("{{ tool_allow }}", tool_allow_text)
|
|
||||||
html = html.replace("{{ tool_deny }}", tool_deny_text)
|
|
||||||
html = html.replace("{{ confirm_required_tools }}", confirm_tools_list)
|
|
||||||
|
|
||||||
persona_items = "\n".join(
|
persona_items = "\n".join(
|
||||||
f'''<li>
|
f'''<li>
|
||||||
<a href="/{username}/{p}" class="persona-link">{p}</a>
|
<a href="/{username}/{p}" class="persona-link">{p}</a>
|
||||||
@@ -381,27 +371,6 @@ async def save_notifications(
|
|||||||
return HTMLResponse(_notifications_page(username, back_persona, success="Notification settings saved."))
|
return HTMLResponse(_notifications_page(username, back_persona, success="Notification settings saved."))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/settings/tool-policy", include_in_schema=False)
|
|
||||||
async def save_tool_policy_route(
|
|
||||||
request: Request,
|
|
||||||
allow_list: str = Form(""),
|
|
||||||
deny_list: str = Form(""),
|
|
||||||
):
|
|
||||||
username = _get_session_user(request)
|
|
||||||
if not username:
|
|
||||||
return RedirectResponse("/login", status_code=302)
|
|
||||||
|
|
||||||
personas = list_user_personas(username)
|
|
||||||
back_persona = _preferred_persona(request, username)
|
|
||||||
|
|
||||||
allow_tools = [ln.strip() for ln in allow_list.splitlines() if ln.strip()]
|
|
||||||
deny_tools = [ln.strip() for ln in deny_list.splitlines() if ln.strip()]
|
|
||||||
save_tool_policy(username, {"allow": allow_tools, "deny": deny_tools})
|
|
||||||
logger.info("tool policy updated for %s (allow=%d deny=%d)", username, len(allow_tools), len(deny_tools))
|
|
||||||
return HTMLResponse(_settings_page(username, personas, back_persona,
|
|
||||||
success="Tool permission policy saved."))
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/settings/email-allowlist", include_in_schema=False)
|
@router.post("/settings/email-allowlist", include_in_schema=False)
|
||||||
async def save_email_allowlist(
|
async def save_email_allowlist(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
|||||||
|
|
||||||
from auth_utils import COOKIE_NAME, decode_token, get_tool_policy, save_tool_policy
|
from auth_utils import COOKIE_NAME, decode_token, get_tool_policy, save_tool_policy
|
||||||
from persona import list_user_personas
|
from persona import list_user_personas
|
||||||
from tools import TOOL_CATEGORIES, TOOL_RISK
|
from tools import TOOL_CATEGORIES, TOOL_RISK, CONFIRM_REQUIRED
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -124,6 +124,9 @@ def _tools_page(
|
|||||||
|
|
||||||
html = html.replace("{{ tool_table_html }}", _build_tool_table(policy))
|
html = html.replace("{{ tool_table_html }}", _build_tool_table(policy))
|
||||||
html = html.replace("{{ tool_risk_json }}", json.dumps(TOOL_RISK))
|
html = html.replace("{{ tool_risk_json }}", json.dumps(TOOL_RISK))
|
||||||
|
html = html.replace("{{ confirm_required_tools }}", _html.escape(", ".join(sorted(CONFIRM_REQUIRED))))
|
||||||
|
html = html.replace("{{ tool_allow }}", _html.escape("\n".join(policy.get("allow") or [])))
|
||||||
|
html = html.replace("{{ tool_deny }}", _html.escape("\n".join(policy.get("deny") or [])))
|
||||||
html = html.replace("{{ back_href }}", f"/{username}/{back_persona}" if back_persona else "/")
|
html = html.replace("{{ back_href }}", f"/{username}/{back_persona}" if back_persona else "/")
|
||||||
html = html.replace("{{ help_href }}", f"/help?persona={back_persona}" if back_persona else "/help")
|
html = html.replace("{{ help_href }}", f"/help?persona={back_persona}" if back_persona else "/help")
|
||||||
|
|
||||||
@@ -167,7 +170,9 @@ async def save_tools(request: Request):
|
|||||||
elif val == "blacklist":
|
elif val == "blacklist":
|
||||||
blacklist.append(tool)
|
blacklist.append(tool)
|
||||||
|
|
||||||
# Merge into existing policy (preserve allow/deny confirmation-gate fields)
|
allow_tools = [ln.strip() for ln in (form.get("allow_list") or "").splitlines() if ln.strip()]
|
||||||
|
deny_tools = [ln.strip() for ln in (form.get("deny_list") or "").splitlines() if ln.strip()]
|
||||||
|
|
||||||
policy = get_tool_policy(username)
|
policy = get_tool_policy(username)
|
||||||
if max_risk:
|
if max_risk:
|
||||||
policy["max_risk"] = max_risk
|
policy["max_risk"] = max_risk
|
||||||
@@ -176,11 +181,13 @@ async def save_tools(request: Request):
|
|||||||
|
|
||||||
policy["whitelist"] = whitelist
|
policy["whitelist"] = whitelist
|
||||||
policy["blacklist"] = blacklist
|
policy["blacklist"] = blacklist
|
||||||
|
policy["allow"] = allow_tools
|
||||||
|
policy["deny"] = deny_tools
|
||||||
|
|
||||||
save_tool_policy(username, policy)
|
save_tool_policy(username, policy)
|
||||||
logger.info(
|
logger.info(
|
||||||
"tool policy saved for %s: max_risk=%s whitelist=%d blacklist=%d",
|
"tool policy saved for %s: max_risk=%s whitelist=%d blacklist=%d allow=%d deny=%d",
|
||||||
username, max_risk or "none", len(whitelist), len(blacklist),
|
username, max_risk or "none", len(whitelist), len(blacklist), len(allow_tools), len(deny_tools),
|
||||||
)
|
)
|
||||||
return HTMLResponse(_tools_page(
|
return HTMLResponse(_tools_page(
|
||||||
username, back_persona,
|
username, back_persona,
|
||||||
|
|||||||
@@ -379,33 +379,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tool Permissions -->
|
<!-- Tool Permissions → moved to /settings/tools -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Tool Permissions</h2>
|
<h2>Tool Permissions</h2>
|
||||||
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.5rem; line-height:1.55;">
|
<p style="font-size:0.85rem; color:var(--pg-muted); margin-bottom:1rem; line-height:1.55;">
|
||||||
Override the default confirmation gate for orchestrator tools.
|
Configure tool access, risk policy, and confirmation gate overrides on the Tools page.
|
||||||
<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>
|
||||||
<p style="font-size:0.78rem; color:var(--pg-muted); margin-bottom:0.85rem;">
|
<a href="/settings/tools"
|
||||||
Tools requiring confirmation by default: <code>{{ confirm_required_tools }}</code>
|
style="display:inline-block; padding:0.45rem 1.1rem; background:var(--pg-accent,#7c3aed);
|
||||||
</p>
|
color:#fff; border-radius:0.5rem; font-size:0.875rem; font-weight:600;
|
||||||
<form method="POST" action="/settings/tool-policy">
|
text-decoration:none;">
|
||||||
<div class="form-group">
|
Tool Settings →
|
||||||
<label for="allow_list">Allow list (bypass confirmation)</label>
|
</a>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Browser cache -->
|
<!-- Browser cache -->
|
||||||
|
|||||||
@@ -191,6 +191,41 @@
|
|||||||
<!-- ── Tool table ── -->
|
<!-- ── Tool table ── -->
|
||||||
{{ tool_table_html }}
|
{{ tool_table_html }}
|
||||||
|
|
||||||
|
<!-- ── Confirmation gate ── -->
|
||||||
|
<div class="policy-card" style="margin-top:1.75rem;">
|
||||||
|
<h2>Confirmation Gate</h2>
|
||||||
|
<p class="policy-note" style="margin-bottom:0.85rem;">
|
||||||
|
Some tools require explicit confirmation before executing. Override the defaults here.<br>
|
||||||
|
Tools requiring confirmation by default: <code style="font-size:0.78rem;">{{ confirm_required_tools }}</code>
|
||||||
|
</p>
|
||||||
|
<div class="policy-row" style="align-items:flex-start; gap:1.5rem; flex-wrap:wrap;">
|
||||||
|
<div style="flex:1; min-width:200px;">
|
||||||
|
<label style="display:block; font-size:0.8rem; font-weight:600; margin-bottom:0.35rem;">
|
||||||
|
Allow list — bypass confirmation
|
||||||
|
</label>
|
||||||
|
<textarea name="allow_list" rows="4"
|
||||||
|
placeholder="reminders_clear cron_remove"
|
||||||
|
autocomplete="off" spellcheck="false"
|
||||||
|
style="width:100%; background:var(--pg-bg); border:1px solid var(--pg-border);
|
||||||
|
border-radius:0.375rem; color:var(--pg-text); padding:0.45rem 0.65rem;
|
||||||
|
font-size:0.82rem; font-family:monospace; resize:vertical;">{{ tool_allow }}</textarea>
|
||||||
|
<p class="policy-note">One tool name per line. These tools skip the confirmation prompt.</p>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; min-width:200px;">
|
||||||
|
<label style="display:block; font-size:0.8rem; font-weight:600; margin-bottom:0.35rem;">
|
||||||
|
Deny list — always block
|
||||||
|
</label>
|
||||||
|
<textarea name="deny_list" rows="4"
|
||||||
|
placeholder="shell_exec file_write"
|
||||||
|
autocomplete="off" spellcheck="false"
|
||||||
|
style="width:100%; background:var(--pg-bg); border:1px solid var(--pg-border);
|
||||||
|
border-radius:0.375rem; color:var(--pg-text); padding:0.45rem 0.65rem;
|
||||||
|
font-size:0.82rem; font-family:monospace; resize:vertical;">{{ tool_deny }}</textarea>
|
||||||
|
<p class="policy-note">These tools are always blocked regardless of risk policy.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:1.5rem;">
|
<div style="margin-top:1.5rem;">
|
||||||
<button type="submit" class="save-btn">Save tool settings</button>
|
<button type="submit" class="save-btn">Save tool settings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user