From a04775ad0df0f972c1e132b45ca5a0b05e71f8dc Mon Sep 17 00:00:00 2001 From: Hermes Dev Date: Thu, 29 Oct 2026 00:32:51 +0200 Subject: [PATCH] Claude connector panel: split site approvals from add-on-panel approvals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Site approvals and panel approvals share one allowedOrigins list on disk because the policy is one SitePolicy either way, but on screen they are two different things: a URL you signed in on versus an Aegis sidebar panel you unlocked. The panel showed them in one mono list under "Sites Claude may control", so an Aegis approval read as "theseus-panel://aegis" with no label and no clue it was a panel at all. publicState() now splits allowedOrigins into allowedSites (web origins) and allowedPanels (theseus-panel://, enriched with the add-on's own title via api.tabControl.list()). The panel.html renders them in their own sections: "Sites Claude may control" and "Add-on panels Claude may control", the latter hidden when nothing is approved. Each panel row shows the add-on title in bold with the mono origin as a subtitle. The ✕ button goes through the existing remove-origin IPC — removing either kind takes one call. The renderer falls back to deriving the split from allowedOrigins so a UI loaded against an older host still renders both sections. --- PENDING.md | 5 ++++- bundled-addons/claude-connector/index.js | 22 ++++++++++++++++++++++ bundled-addons/claude-connector/panel.html | 21 +++++++++++++++++++-- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/PENDING.md b/PENDING.md index 522e114b..cf2aebc1 100644 --- a/PENDING.md +++ b/PENDING.md @@ -50,7 +50,10 @@ code change. closed panels too so a client can see what exists; `webContents(-N)` throws " is not open; ask the user to open the panel from the sidebar first" until the user docks it. Password-field refusal and the - action log still apply. + action log still apply. The connector panel.html now splits site + approvals from panel approvals into their own sections, with the add-on's + own title ("Aegis panel") above the mono origin string and a ✕ Remove + button that goes through the existing `remove-origin` IPC. ## session: `parallel:collision-policy` diff --git a/bundled-addons/claude-connector/index.js b/bundled-addons/claude-connector/index.js index 85f33cfe..bf59fbb6 100644 --- a/bundled-addons/claude-connector/index.js +++ b/bundled-addons/claude-connector/index.js @@ -116,6 +116,26 @@ module.exports = { function publicState() { const st = server ? server.status() : { listening: false, port: null, connected: false, clients: [], sessions: [] }; const url = server && server.url; + // Panel approvals (theseus-panel://) and site approvals share + // the allowedOrigins list on disk. Split them for the UI so each gets + // its own section, and enrich panel rows with the add-on's own title. + const PANEL_PREFIX = "theseus-panel://"; + const panelTitles = new Map(); + try { + for (const t of api.tabControl.list()) { + if (t && t.kind === "panel" && t.addon) panelTitles.set(String(t.addon).toLowerCase(), t.title || t.addon); + } + } catch {} + const allowedPanels = []; + const allowedSites = []; + for (const o of S.allowedOrigins) { + if (typeof o === "string" && o.startsWith(PANEL_PREFIX)) { + const addon = o.slice(PANEL_PREFIX.length); + allowedPanels.push({ origin: o, addon, title: panelTitles.get(addon.toLowerCase()) || addon.replace(/^\w/, (c) => c.toUpperCase()) }); + } else { + allowedSites.push(o); + } + } return { enabled: true, accept: S.accept, @@ -128,6 +148,8 @@ module.exports = { controlledTabs: controller.controlledTabIds(), allowEvaluate: S.allowEvaluate, allowedOrigins: S.allowedOrigins, + allowedSites, + allowedPanels, uploadDirs: S.uploadDirs, sessionLimit: S.sessionLimit, token: S.token, diff --git a/bundled-addons/claude-connector/panel.html b/bundled-addons/claude-connector/panel.html index a92d082a..344bbffd 100644 --- a/bundled-addons/claude-connector/panel.html +++ b/bundled-addons/claude-connector/panel.html @@ -116,6 +116,12 @@
    + +

    Upload folders

    Claude can attach files only from these folders.

    @@ -156,9 +162,20 @@ $("setup").hidden = !st.commands; renderApps(st.apps); if (st.commands) { $("cmdCode").textContent = st.commands.claudeCode; $("cmdDesk").textContent = st.commands.claudeDesktop; } - $("origins").innerHTML = st.allowedOrigins.length - ? st.allowedOrigins.map((o) => `
  • ${esc(o)}
  • `).join("") + // Site approvals (http(s)) and panel approvals (theseus-panel://) are + // kept apart on screen even though they share one list on disk, so the + // user sees them for what they are. publicState() does the split and + // enriches each panel with the add-on's own title; the fallback keeps + // older hosts rendering the mixed list the way they used to. + const sites = Array.isArray(st.allowedSites) ? st.allowedSites : (st.allowedOrigins || []).filter((o) => !String(o).startsWith("theseus-panel://")); + const panels = Array.isArray(st.allowedPanels) ? st.allowedPanels : (st.allowedOrigins || []).filter((o) => String(o).startsWith("theseus-panel://")).map((o) => ({ origin: o, addon: o.slice("theseus-panel://".length), title: o.slice("theseus-panel://".length).replace(/^\w/, (c) => c.toUpperCase()) })); + $("origins").innerHTML = sites.length + ? sites.map((o) => `
  • ${esc(o)}
  • `).join("") : '
  • None yet.
  • '; + $("panelsSection").hidden = panels.length === 0; + $("panels").innerHTML = panels.length + ? panels.map((p) => `
  • ${esc(p.title)} panel
    ${esc(p.origin)}
  • `).join("") + : ""; $("dirs").innerHTML = st.uploadDirs.length ? st.uploadDirs.map((d) => `
  • ${esc(d)}
  • `).join("") : '
  • None. Uploads are refused.
  • ';