Remove design artifacts from PR

This commit is contained in:
Dotta committed 2026-05-22 12:31:24 +00:00
1 parent c67ed74400
commit e39e9d57e6
57 files changed
-3985

No files matched your search

-35
View File
@@ -1,35 +0,0 @@
import { chromium } from "/Users/dotta/paperclip/.paperclip/worktrees/PAP-3631-agent-permissions-and-controls-plan/node_modules/.pnpm/playwright@1.58.2/node_modules/playwright/index.mjs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const here = path.dirname(fileURLToPath(import.meta.url));
const fileUrl = "file://" + path.join(here, "index.html");
const captures = [
{ name: "01-access-desktop", anchor: "#s1", viewport: { width: 1440, height: 900 }, fullPage: false },
{ name: "01-access-mobile", anchor: "#s1", viewport: { width: 390, height: 844 }, fullPage: false, isMobile: true },
{ name: "03-agent-desktop", anchor: "#s3", viewport: { width: 1440, height: 900 }, fullPage: false },
{ name: "03-agent-mobile", anchor: "#s3", viewport: { width: 390, height: 844 }, fullPage: false, isMobile: true },
{ name: "02-scope-desktop", anchor: "#s2", viewport: { width: 1440, height: 900 }, fullPage: false },
{ name: "04-denied-desktop", anchor: "#s4", viewport: { width: 1440, height: 900 }, fullPage: false },
{ name: "05-privacy-desktop", anchor: "#s5", viewport: { width: 1440, height: 900 }, fullPage: false },
{ name: "overview-desktop", anchor: "#summary", viewport: { width: 1440, height: 900 }, fullPage: true },
];
const browser = await chromium.launch();
for (const cap of captures) {
const ctx = await browser.newContext({
viewport: cap.viewport,
deviceScaleFactor: 2,
isMobile: cap.isMobile ?? false,
hasTouch: cap.isMobile ?? false,
});
const page = await ctx.newPage();
await page.goto(fileUrl + cap.anchor, { waitUntil: "networkidle" });
await page.waitForTimeout(200);
const out = path.join(here, "screenshots", `${cap.name}.png`);
await page.screenshot({ path: out, fullPage: !!cap.fullPage });
console.log("wrote", out);
await ctx.close();
}
await browser.close();
-502
View File
@@ -1,502 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PAP-9447 · Permissions UX wireframes</title>
<style>
:root {
--bg: #fafaf8;
--panel: #ffffff;
--ink: #111111;
--muted: #666666;
--line: #e5e5e0;
--accent: #d33;
--maxw: 1200px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font: 15px/1.55 -apple-system, system-ui, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
a { color: inherit; }
.shell { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; }
.toc {
position: sticky; top: 0; align-self: start;
height: 100vh; overflow: auto;
padding: 24px 16px 24px 24px;
border-right: 1px solid var(--line);
background: var(--panel);
font-size: 13px;
z-index: 20;
}
.toc h1 { font-size: 14px; margin: 0 0 4px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); }
.toc h2 { font-size: 12px; margin: 24px 0 6px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); font-weight: 700; }
.toc a { display: block; text-decoration: none; padding: 6px 8px; margin-left: -8px; border-radius: 4px; color: var(--ink); }
.toc a:hover { background: var(--bg); }
.toc .num { display: inline-block; width: 22px; color: var(--muted); }
.toc .toc-summary { display: none; }
.toc .toc-body { display: block; }
main { padding: 48px 56px 96px; max-width: 1300px; min-width: 0; }
header.hero { max-width: var(--maxw); margin-bottom: 64px; }
header.hero .crumb { color: var(--muted); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; }
header.hero h1 { font-size: 36px; line-height: 1.15; margin: 8px 0 16px; }
header.hero p { color: var(--muted); max-width: 760px; font-size: 16px; }
header.hero p + p { margin-top: 8px; }
header.hero .pills { margin-top: 20px; display: flex; gap: 8px; flex-wrap: wrap; }
header.hero .pill { font-size: 12px; padding: 4px 10px; border: 1px solid var(--line); border-radius: 999px; background: var(--panel); color: var(--muted); }
section { scroll-margin-top: 24px; max-width: var(--maxw); margin-bottom: 96px; }
section .lede { color: var(--muted); margin: 0 0 12px; font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 700; }
section h2 { font-size: 28px; line-height: 1.2; margin: 0 0 8px; }
section h2 .step-num { display: inline-block; width: 40px; color: var(--muted); }
section h3 { font-size: 16px; margin: 24px 0 8px; }
section .desc { color: var(--muted); margin: 0 0 24px; max-width: 760px; }
.grid {
display: grid; gap: 24px;
grid-template-columns: minmax(0, 1.4fr) 220px minmax(0, 0.9fr);
align-items: start;
}
@media (max-width: 1180px) {
.grid { grid-template-columns: minmax(0, 1fr) 200px; }
.grid > .notes-col { grid-column: 1 / -1; }
}
@media (max-width: 980px) { .grid { grid-template-columns: 1fr; } .grid > .mobile-col { max-width: 280px; } }
@media (max-width: 900px) {
.shell { grid-template-columns: 1fr; }
.toc {
position: sticky; top: 0;
height: auto; max-height: 60vh;
padding: 12px 16px;
border-right: none;
border-bottom: 1px solid var(--line);
}
.toc .toc-summary {
display: flex; justify-content: space-between; align-items: center;
cursor: pointer; list-style: none; padding: 4px 0;
}
.toc .toc-summary::-webkit-details-marker { display: none; }
.toc .toc-summary .title { font-size: 13px; font-weight: 600; }
.toc .toc-summary .crumb { font-size: 11px; color: var(--muted); letter-spacing: 0.04em; text-transform: uppercase; }
.toc .toc-summary .chevron {
width: 10px; height: 10px;
border-right: 1.5px solid var(--ink); border-bottom: 1.5px solid var(--ink);
transform: rotate(45deg); transition: transform 120ms ease; margin-right: 4px;
}
.toc[open] .toc-summary .chevron { transform: rotate(225deg) translate(-2px, -2px); }
.toc .toc-body { display: none; padding-top: 8px; border-top: 1px solid var(--line); margin-top: 8px; }
.toc[open] .toc-body { display: block; }
main { padding: 24px 20px 64px; }
header.hero { margin-bottom: 40px; }
header.hero h1 { font-size: 28px; }
header.hero p { font-size: 15px; }
section { margin-bottom: 64px; scroll-margin-top: 80px; }
section h2 { font-size: 24px; }
section h2 .step-num { width: 32px; }
}
@media (max-width: 560px) {
main { padding: 16px 14px 56px; }
header.hero { margin-bottom: 32px; }
header.hero h1 { font-size: 24px; line-height: 1.2; }
header.hero p { font-size: 14px; }
section { margin-bottom: 48px; }
section h2 { font-size: 20px; line-height: 1.25; }
section h2 .step-num { width: 28px; }
section h3 { font-size: 15px; margin: 20px 0 6px; }
section .desc { font-size: 14px; }
.grid { gap: 16px; }
.wire .label { padding: 8px 10px; font-size: 11px; gap: 8px; }
body { font-size: 14px; }
}
.wire {
background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
overflow: hidden; cursor: zoom-in;
transition: transform 80ms ease, box-shadow 80ms ease;
}
.wire:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
.wire .label {
padding: 10px 14px; font-size: 12px; color: var(--muted);
border-bottom: 1px solid var(--line);
display: flex; justify-content: space-between; align-items: center;
}
.wire img { display: block; width: 100%; height: auto; background: #fff; }
.wire.mobile-wire img { max-width: 220px; margin: 0 auto; }
.notes ul { padding-left: 20px; margin: 8px 0; }
.notes li { margin-bottom: 6px; }
.notes .why {
background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--ink);
padding: 14px 18px; border-radius: 4px; margin-top: 12px; font-size: 14px;
}
.notes .dont {
background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--accent);
padding: 14px 18px; border-radius: 4px; margin-top: 12px; font-size: 14px;
}
.summary-box {
background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
padding: 20px 24px; margin-bottom: 48px; max-width: var(--maxw);
}
.summary-box h3 { margin-top: 0; font-size: 16px; }
.summary-box ul { padding-left: 20px; margin: 8px 0 0; }
.summary-box li { margin-bottom: 4px; }
.lightbox {
position: fixed; inset: 0;
background: rgba(0,0,0,0.86);
display: none; align-items: center; justify-content: center;
z-index: 100; cursor: zoom-out; padding: 32px;
}
.lightbox.open { display: flex; }
.lightbox img { max-width: 100%; max-height: 100%; background: #fff; box-shadow: 0 30px 80px rgba(0,0,0,0.4); border-radius: 4px; }
.lightbox .close { position: absolute; top: 16px; right: 24px; font-size: 28px; color: #fff; cursor: pointer; }
.lightbox .caption { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); color: #fff; font-size: 13px; opacity: 0.9; }
@media (max-width: 560px) {
.lightbox { padding: 12px; overflow: auto; touch-action: pinch-zoom; }
.lightbox.open { display: block; }
.lightbox img { display: block; width: 100%; max-width: none; max-height: none; height: auto; margin: 40px auto 56px; box-shadow: none; }
.lightbox .close { position: fixed; top: 8px; right: 12px; font-size: 32px; }
.lightbox .caption { position: fixed; left: 50%; transform: translateX(-50%); font-size: 12px; bottom: 12px; text-align: center; background: rgba(0,0,0,0.7); border-radius: 4px; padding: 6px 12px; }
}
.footer { color: var(--muted); font-size: 12px; max-width: var(--maxw); border-top: 1px solid var(--line); padding-top: 24px; }
.footer a { color: var(--ink); }
</style>
</head>
<body>
<div class="shell">
<details class="toc">
<summary class="toc-summary">
<span>
<span class="crumb">PAP-9447 · Permissions UX</span><br>
<span class="title">Jump to a screen</span>
</span>
<span class="chevron" aria-hidden="true"></span>
</summary>
<nav class="toc-body" aria-label="Section navigation">
<h1>PAP-9447</h1>
<div style="font-size: 13px; color: var(--muted); margin-bottom: 16px;">Permissions UX wireframes</div>
<h2>Overview</h2>
<a href="#summary"><span class="num">·</span>Design note &amp; goals</a>
<h2>Screens</h2>
<a href="#s1"><span class="num">1</span>Company access</a>
<a href="#s2"><span class="num">2</span>Scope grant editor</a>
<a href="#s3"><span class="num">3</span>Agent permissions</a>
<a href="#s4"><span class="num">4</span>Denied / read-only</a>
<a href="#s5"><span class="num">5</span>Privacy rule</a>
<h2>Revision 2</h2>
<a href="#changelog"><span class="num">✎</span>What changed</a>
</nav>
</details>
<main>
<header class="hero">
<div class="crumb">PAP-9447 · Wireframes</div>
<h1>Permissions &amp; controls — UX review</h1>
<p>
Lo-fi paired desktop + mobile wireframes for the five permission surfaces in the
approved <a href="/PAP/issues/PAP-3631">PAP-3631</a> plan: company access, scoped task
assignment, agent permission detail, denied/read-only states, and the privacy rule.
</p>
<p>
Visual style is deliberately monochrome — the goal is to argue structure, copy, and the
permission model, not pick a colour palette. The product should land inside the existing
Settings &amp; Agents surfaces with no new framed panels.
</p>
<div class="pills">
<span class="pill">Revision 2 · 2026-05-18</span>
<span class="pill">5 screens · 10 SVGs</span>
<span class="pill">Desktop 1280 + mobile 375</span>
<span class="pill">Click any wireframe to zoom</span>
</div>
</header>
<!-- ========================= Design note ========================= -->
<section id="summary" class="summary-box">
<div class="lede">Design note</div>
<h2 style="margin: 4px 0 16px;">What we are arguing</h2>
<h3>Core flows</h3>
<ul>
<li><b>Company access</b> lives where it already does (<code>/company/settings/access</code>) — but the page now has a peer <b>Agents</b> table next to <b>Humans</b>, and the grants column surfaces the new scope summary chip in line.</li>
<li><b>Scoped task assignment grants</b> are a dedicated editor surfaced from the existing member-edit dialog. Three constraint families — project, managed-subtree, target-agent allowlist — sit as their own sections with radio + chip pattern, plus a live "all-of" summary in a right rail.</li>
<li><b>Agent permission detail</b> consolidates the five SPEC 9.4 lenses into one tab on the agent page: profile visibility, config visibility/redaction, assignment/invocation, tool/secret policy, escalation. Each card is a single rule, not a settings tree.</li>
</ul>
<h3>Empty, error, and denied states</h3>
<ul>
<li><b>Out-of-scope picker rows</b> stay visible but disabled with an "out of scope" trailing label — operators see the universe but can't act on what they don't own.</li>
<li><b>Denied toast and activity log entry</b> use a single generic shape: actor · action · outcome. They never name the failed constraint, the target id, or the hidden project.</li>
<li><b>Read-only config</b> renders fields at 0.7 opacity with a single banner ("View only · Board can edit this") and a "Request edit from board" affordance. Secret refs are visibly redacted with <code>&lt;redacted&gt;</code> placeholder text.</li>
<li><b>Empty state</b> for a member with no scope: the grants cell reads "No explicit grants — implicit from role"; the editor's right-rail summary reads "Any project · Any subtree · Any agent".</li>
</ul>
<h3>Privacy rule (load-bearing)</h3>
<ul>
<li>The privacy page leads with one block: <i>"Agent privacy does not hide work."</i> Hidden vs. visible is a two-column list — agent-identity surface (hidden) on the left, work objects (visible) on the right.</li>
<li>The footer block names what is NOT covered (project/issue ACLs) so operators don't mistake the toggle for ACL-style scoping. Roadmap link points at the Pro/Enterprise tier.</li>
</ul>
</section>
<!-- ========================= Screen 1 ========================= -->
<section id="s1">
<div class="lede">Step 1 · Company &amp; access</div>
<h2><span class="step-num">1.</span>Company access — humans &amp; agents in one place</h2>
<p class="desc">
The existing <code>/company/settings/access</code> page grows a sibling <b>Agents</b> table and surfaces the new
scope grant inline. Roles cheatsheet sits up top so operators don't need to open a doc.
</p>
<div class="grid">
<div class="wire" data-zoom data-caption="01 · Company access (desktop)">
<div class="label"><span>01-company-access.svg</span><span>1280×1240 · desktop</span></div>
<img src="wireframes/01-company-access.svg" alt="Company access desktop wireframe" />
</div>
<div class="wire mobile-wire mobile-col" data-zoom data-caption="01 · Company access (mobile)">
<div class="label"><span>mobile</span><span>375×1180</span></div>
<img src="wireframes/01-company-access-mobile.svg" alt="Company access mobile wireframe" />
</div>
<div class="notes-col">
<div class="notes">
<h3>Annotations</h3>
<ul>
<li><b>1</b> — Visibility note. Product copy: "Roles and grants control who can act — not what they see." Help icon links to the full privacy page.</li>
<li><b>2</b> — Humans table. Same shape as today, but grants column now shows scope summary chip when <code>tasks:assign_scope</code> is set.</li>
<li><b>3</b> — Agents table is new. Compact columns: role, profile visibility, config visibility, who can assign-to it, who can invoke it.</li>
<li><b>4</b> — Private agent badge inline with the agent's name in the directory. Dense dropdown pickers (Screen 4) use a lock icon instead — see revision changelog.</li>
</ul>
<div class="why"><b>Why this changes:</b> today the page only shows humans. The plan adds agent-side permissions, and the access page is where operators expect to compare. Mobile reflows to cards so role/status/grants stack vertically; the scope summary becomes a full-width pill row.</div>
</div>
</div>
</div>
</section>
<!-- ========================= Screen 2 ========================= -->
<section id="s2">
<div class="lede">Step 2 · Scoped assignment</div>
<h2><span class="step-num">2.</span>Scoped task assignment grant editor</h2>
<p class="desc">
Editor surfaced from the existing member-edit dialog when <code>tasks:assign_scope</code> is enabled. Three
constraint families with radio toggles and chip multi-pickers; a right rail shows the live "all-of" result.
</p>
<div class="grid">
<div class="wire" data-zoom data-caption="02 · Scope grant editor (desktop)">
<div class="label"><span>02-scope-editor.svg</span><span>1280×900 · desktop</span></div>
<img src="wireframes/02-scope-editor.svg" alt="Scope grant editor desktop wireframe" />
</div>
<div class="wire mobile-wire mobile-col" data-zoom data-caption="02 · Scope grant editor (mobile)">
<div class="label"><span>mobile</span><span>375×1100</span></div>
<img src="wireframes/02-scope-editor-mobile.svg" alt="Scope grant editor mobile wireframe" />
</div>
<div class="notes-col">
<div class="notes">
<h3>Annotations</h3>
<ul>
<li><b>1</b> — "All chosen scopes must match" explainer. Pre-empts the most common misread (OR vs AND).</li>
<li><b>2</b> — Project scope. "Any project" is the explicit unscoped state so the radio always has a non-empty selection.</li>
<li><b>3</b> — Managed-subtree scope. Pickers select a manager root — recursive descent is spelled out in copy, not left to inference.</li>
<li><b>4</b> — Target-agent allowlist. Chip multipicker; suggested order matches reporting chain.</li>
<li><b>5</b> — Live result rail. Mirrors the back-end scope JSON so the operator sees the assertion the server will check.</li>
</ul>
<div class="why"><b>Why this shape:</b> the SPEC allows multiple constraint families ANDed. A flat form would feel like one rule; this layout teaches the AND by structure. Mobile collapses the rail into a card directly above the sticky save button so the summary remains the last thing read before commit.</div>
</div>
</div>
</div>
</section>
<!-- ========================= Screen 3 ========================= -->
<section id="s3">
<div class="lede">Step 3 · Agent detail</div>
<h2><span class="step-num">3.</span>Agent permissions — the five lenses</h2>
<p class="desc">
A new <b>Permissions</b> tab on the agent detail page. Each SPEC 9.4 lens lives as one card with one rule.
The Private toggle sits at the top with the privacy rule restated inline so the consequence is unambiguous.
</p>
<div class="grid">
<div class="wire" data-zoom data-caption="03 · Agent permissions (desktop)">
<div class="label"><span>03-agent-permissions.svg</span><span>1280×1320 · desktop</span></div>
<img src="wireframes/03-agent-permissions.svg" alt="Agent permissions desktop wireframe" />
</div>
<div class="wire mobile-wire mobile-col" data-zoom data-caption="03 · Agent permissions (mobile)">
<div class="label"><span>mobile</span><span>375×1700</span></div>
<img src="wireframes/03-agent-permissions-mobile.svg" alt="Agent permissions mobile wireframe" />
</div>
<div class="notes-col">
<div class="notes">
<h3>Annotations</h3>
<ul>
<li><b>0</b> — Private marker + privacy rule restated. Lead line is the product copy "Agent privacy does not hide work." with Learn more →. Same wording everywhere to avoid drift.</li>
<li><b>1</b> — Profile visibility. Company-visible vs. Board-only — two states.</li>
<li><b>2</b> — Config visibility &amp; redaction. Secret-ref fields are always shown as <code>&lt;redacted&gt;</code> regardless of viewer.</li>
<li><b>3</b> — Assignment / invocation. Two distinct principal lists with chip pickers.</li>
<li><b>4</b> — Tool / secret policy. Allowed tools and secret refs as labeled chips, with a redaction notice attached.</li>
<li><b>5</b> — Escalation chain — manager → grand-manager → board. No SLA copy on hops (escalation is event-driven, not time-boxed).</li>
</ul>
<div class="why"><b>Why one tab:</b> these five lenses are easy to confuse. Surfacing them as siblings with consistent card chrome and a recent-changes audit at the bottom makes drift visible.</div>
<div class="dont"><b>What we're avoiding:</b> a settings tree with nested toggles. Every nest is a place a permission gets misread. Cards stay flat.</div>
</div>
</div>
</div>
</section>
<!-- ========================= Screen 4 ========================= -->
<section id="s4">
<div class="lede">Step 4 · Failure states</div>
<h2><span class="step-num">4.</span>Denied and read-only — the no with no leakage</h2>
<p class="desc">
Four patterns that all share one rule: <b>name the unblock owner, never the missing resource</b>. SPEC 9.8 returns 403 with a
generic scope message; the UI wraps that into a single calm sentence and a "Request wider scope" affordance.
</p>
<div class="grid">
<div class="wire" data-zoom data-caption="04 · Denied / read-only (desktop)">
<div class="label"><span>04-denied-readonly.svg</span><span>1280×1320 · desktop</span></div>
<img src="wireframes/04-denied-readonly.svg" alt="Denied / read-only desktop wireframe" />
</div>
<div class="wire mobile-wire mobile-col" data-zoom data-caption="04 · Denied / read-only (mobile)">
<div class="label"><span>mobile</span><span>375×1620</span></div>
<img src="wireframes/04-denied-readonly-mobile.svg" alt="Denied / read-only mobile wireframe" />
</div>
<div class="notes-col">
<div class="notes">
<h3>Annotations</h3>
<ul>
<li><b>1</b> — Picker shows in-scope agents at normal weight and out-of-scope agents at 45% opacity with an "out of scope" trailing label. Disabled, not hidden — the operator sees the universe but can't act.</li>
<li><b>2</b> — Copy guidelines: one sentence + a named owner. Never list hidden agents or projects.</li>
<li><b>3</b> — Prohibited copy: things that leak resource shape. These are what NOT to ship.</li>
<li><b>4</b> — Read-only config: fields render at 0.7 opacity; banner is the only colour-free signal that this is a permissions state, not a loading state.</li>
<li><b>5</b> — Secret redaction explainer. Same rule applies to UI, activity log, and exports.</li>
<li><b>6 · 7</b> — Denied toast + activity entry use the same shape. Actor · action · outcome (scope). No target id.</li>
</ul>
<div class="dont"><b>Anti-patterns we're rejecting:</b> "Project X is not in your allowed projects" — leaks that Project X exists and that you have an allowed-list. The wireframes call these out inline.</div>
</div>
</div>
</div>
</section>
<!-- ========================= Screen 5 ========================= -->
<section id="s5">
<div class="lede">Step 5 · The privacy rule</div>
<h2><span class="step-num">5.</span>Privacy rule — what "private" actually means</h2>
<p class="desc">
A dedicated Privacy page on the agent detail, designed so the toggle and its meaning sit on the same screen.
The two-column "what gets hidden / what stays visible" list teaches the boundary by example.
</p>
<div class="grid">
<div class="wire" data-zoom data-caption="05 · Privacy rule (desktop)">
<div class="label"><span>05-privacy-rule.svg</span><span>1280×1080 · desktop</span></div>
<img src="wireframes/05-privacy-rule.svg" alt="Privacy rule desktop wireframe" />
</div>
<div class="wire mobile-wire mobile-col" data-zoom data-caption="05 · Privacy rule (mobile)">
<div class="label"><span>mobile</span><span>375×1500</span></div>
<img src="wireframes/05-privacy-rule-mobile.svg" alt="Privacy rule mobile wireframe" />
</div>
<div class="notes-col">
<div class="notes">
<h3>Annotations</h3>
<ul>
<li><b>1</b> — Toggle row. Sub-copy spells out the two effects (directory hiding, config readability).</li>
<li><b>2</b> — The rule, restated as a large block — same wording as the agent-permissions tab banner.</li>
<li><b>3</b> — Hidden column. Bullets are agent-identity surface only.</li>
<li><b>4</b> — Visible column. Issues, comments, projects, costs, activity. The "why this distinction" callout names the board accountability surface as the reason.</li>
<li><b>5</b> — "What this doesn't do" strip with dashed border. Names project/issue ACLs and points at the Pro tier so operators don't conflate.</li>
</ul>
<div class="why"><b>Why a dedicated page:</b> the rule is load-bearing for the plan and easy to misread from a single toggle label. The page exists to anchor that the marker is identity-only, not work-object visibility. It also keeps the agent-permissions tab uncluttered.</div>
</div>
</div>
</div>
</section>
<!-- ========================= Revision changelog ========================= -->
<section id="changelog">
<div class="lede">Revision 2 · 2026-05-18</div>
<h2>What changed in this revision</h2>
<p class="desc">
Board accepted Q1, Q2, Q4 as proposed and asked for targeted revisions on the rest. This revision is the focused pass before final approval.
</p>
<div class="notes">
<h3>Targeted fixes</h3>
<ul>
<li><b>Lock icon in dropdowns (Screen 4).</b> Dense picker rows now show a compact lock icon (with accessible <code>title</code>/aria-label) instead of a text "private" badge. The full text badge stays in directory views (Screens 1, 3) where row density allows it. Added a private agent (Vega) into the in-scope group so the pattern is visible.</li>
<li><b>"V1" copy removed from user-facing UI.</b> All in-product strings that referenced V1 are now plain product language. Section labels like "NOT IN V1" → "WHAT THIS DOESN'T DO"; section headlines like "The V1 rule" → "Agent privacy does not hide work."; body copy "In V1, scope at the role/grant layer" → "Scope at the role/grant layer". The review-site annotation copy was scrubbed at the same time for consistency.</li>
<li><b>Meta copy replaced with product copy + help affordances (Screens 1, 3, 5).</b> Headline "Visibility in V1 is company-scoped" → "Roles and grants control who can act — not what they see." Added a help (?) icon on the Company Access banner and Agent permissions private-marker card. Each surface that mentions privacy carries a Learn more → link that routes to the Privacy page.</li>
<li><b>SLA copy removed from escalation (Screen 3).</b> Manager hop now reads simply "manager"; mobile shows "CTO · Atlas · manager". Escalation is event-driven, not time-boxed in this slice.</li>
<li><b>Denied toast names the owner action (Screen 4).</b> Both desktop and mobile toasts now include an explicit "Ask Maya Chen" button next to Dismiss, mirroring the picker-side "Ask Maya Chen" affordance. The button opens a draft comment on the failing task. Toast body copy ("Ask Maya Chen if this should change.") preserves the named owner in text for screen readers and ambient context.</li>
</ul>
<h3>Preserved from revision 1 (board approved)</h3>
<ul>
<li>Q1 — Agents as a peer table on Company Settings → Access.</li>
<li>Q2 — Mobile scope summary as a card above sticky save.</li>
<li>Q4 — Out-of-scope picker rows visible as disabled / labeled, not hidden.</li>
</ul>
<h3>Open questions for this round</h3>
<ul>
<li><b>Help destination.</b> The "Learn more →" links currently point at the Privacy page (Screen 5). If we want a single product-docs URL instead, flag and we'll route there. The wireframes don't depend on the destination.</li>
<li><b>Lock icon stroke weight.</b> The dropdown lock uses 1.4pt stroke to read at 13–14px row height. If accessibility audit later asks for a higher-contrast monochrome glyph, the icon can be swapped for a filled variant without re-laying out the row.</li>
</ul>
</div>
</section>
<div class="footer">
Wireframes follow the in-tree <code>wireframe</code> skill house style — black on white, 1.5pt strokes, 8px grid, type sizes 12/14/20/28.
Red dashed numbered callouts are the annotation layer only and not part of the proposed UI. Source:
<a href="/PAP/issues/PAP-9447">PAP-9447</a> · feeds <a href="/PAP/issues/PAP-9446">PAP-9446</a> review.
</div>
</main>
</div>
<div class="lightbox" id="lb" aria-hidden="true">
<span class="close" id="lbClose">×</span>
<img id="lbImg" alt="" />
<div class="caption" id="lbCap"></div>
</div>
<script>
const lb = document.getElementById('lb');
const lbImg = document.getElementById('lbImg');
const lbCap = document.getElementById('lbCap');
const lbClose = document.getElementById('lbClose');
document.querySelectorAll('[data-zoom]').forEach((el) => {
el.addEventListener('click', () => {
const target = el.tagName === 'IMG' ? el : el.querySelector('img');
if (!target) return;
lbImg.src = target.src;
lbImg.alt = target.alt;
lbCap.textContent = el.dataset.caption || target.alt || '';
lb.classList.add('open');
});
});
function close() { lb.classList.remove('open'); }
lb.addEventListener('click', close);
lbClose.addEventListener('click', close);
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
const toc = document.querySelector('details.toc');
if (toc) {
const mq = window.matchMedia('(max-width: 900px)');
const apply = () => { toc.open = !mq.matches; };
apply();
mq.addEventListener('change', apply);
toc.querySelectorAll('.toc-body a').forEach((a) => {
a.addEventListener('click', () => { if (mq.matches) toc.open = false; });
});
}
</script>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

-30
View File
@@ -1,30 +0,0 @@
import { chromium } from "/Users/dotta/paperclip/.paperclip/worktrees/PAP-3631-agent-permissions-and-controls-plan/node_modules/.pnpm/playwright@1.58.2/node_modules/playwright/index.mjs";
import path from "node:path";
const URL = "https://lunar-tundra-9n2h.here.now/";
const here = "/Users/dotta/paperclip/.paperclip/worktrees/PAP-3631-agent-permissions-and-controls-plan/design/PAP-9447";
const browser = await chromium.launch();
const captures = [
{ name: "live-desktop", viewport: { width: 1440, height: 900 }, isMobile: false },
{ name: "live-mobile", viewport: { width: 390, height: 844 }, isMobile: true },
];
for (const cap of captures) {
const ctx = await browser.newContext({
viewport: cap.viewport,
deviceScaleFactor: 2,
isMobile: cap.isMobile,
hasTouch: cap.isMobile,
});
const page = await ctx.newPage();
const resp = await page.goto(URL, { waitUntil: "networkidle" });
console.log(cap.name, "status=", resp.status());
await page.waitForTimeout(500);
const out = path.join(here, "screenshots", `${cap.name}.png`);
await page.screenshot({ path: out, fullPage: false });
console.log("wrote", out);
await ctx.close();
}
await browser.close();
@@ -1,141 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="375" height="1180" viewBox="0 0 375 1180"
font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
<rect x="0" y="0" width="375" height="1180" />
<!-- 1: status bar + top bar -->
<g data-region="topbar">
<line x1="0" y1="44" x2="375" y2="44" />
<text x="16" y="76" font-size="14" stroke="none" fill="#000">Access</text>
<text x="16" y="94" font-size="11" stroke="none" fill="#666">Settings › Access</text>
<g transform="translate(343,52)"><rect x="-24" y="0" width="24" height="24" /><text x="-12" y="16" font-size="14" stroke="none" fill="#000" text-anchor="middle">≡</text></g>
</g>
<!-- 2: tabs -->
<g data-region="tabs" transform="translate(16,120)">
<line x1="0" y1="32" x2="343" y2="32" />
<g font-size="12" stroke="none">
<text x="0" y="20" fill="#000" font-weight="600">Members</text>
<line x1="-2" y1="32" x2="62" y2="32" stroke="#000" stroke-width="2.5" />
<text x="84" y="20" fill="#666">Roles</text>
<text x="140" y="20" fill="#666">Invites</text>
<text x="208" y="20" fill="#666">Joins · 2</text>
</g>
</g>
<!-- 3: visibility note -->
<g data-region="visibility-banner" transform="translate(16,176)">
<rect width="343" height="88" rx="4" />
<text x="16" y="24" font-size="13" stroke="none" fill="#000" font-weight="600">Roles control who can act — not what they see.</text>
<text x="16" y="44" font-size="11" stroke="none" fill="#666">Use grants to decide who can assign tasks,</text>
<text x="16" y="58" font-size="11" stroke="none" fill="#666">manage agents, or edit config. Issues and</text>
<text x="16" y="72" font-size="11" stroke="none" fill="#666">projects stay company-visible. Learn more →</text>
</g>
<!-- 4: Humans header -->
<g data-region="humans-header" transform="translate(16,288)">
<text x="0" y="20" font-size="18" stroke="none" fill="#000">Humans · 5</text>
<text x="343" y="18" font-size="12" stroke="none" fill="#000" text-anchor="end">+ Invite</text>
</g>
<!-- 5: Humans cards -->
<g data-region="humans-card-1" transform="translate(16,320)">
<rect width="343" height="116" rx="4" />
<g stroke="none">
<text x="16" y="28" font-size="14" fill="#000" font-weight="600">Maya Chen</text>
<text x="16" y="46" font-size="12" fill="#666">maya@acme.ai · you</text>
<g transform="translate(16,58)">
<rect width="60" height="22" rx="11" /><text x="30" y="15" font-size="11" fill="#000" text-anchor="middle">Owner</text>
<rect x="68" width="58" height="22" rx="11" /><text x="97" y="15" font-size="11" fill="#000" text-anchor="middle">active</text>
</g>
<text x="16" y="100" font-size="12" fill="#666">Implicit grants from role · no overrides</text>
</g>
<g transform="translate(263,80)"><rect width="64" height="24" rx="3" /><text x="32" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Edit</text></g>
</g>
<g data-region="humans-card-2" transform="translate(16,452)">
<rect width="343" height="140" rx="4" />
<g stroke="none">
<text x="16" y="28" font-size="14" fill="#000" font-weight="600">Jordan Park</text>
<text x="16" y="46" font-size="12" fill="#666">jordan@acme.ai</text>
<g transform="translate(16,58)">
<rect width="56" height="22" rx="11" /><text x="28" y="15" font-size="11" fill="#000" text-anchor="middle">Admin</text>
<rect x="64" width="58" height="22" rx="11" /><text x="93" y="15" font-size="11" fill="#000" text-anchor="middle">active</text>
</g>
<text x="16" y="100" font-size="12" fill="#000">tasks:assign_scope</text>
<g transform="translate(16,108)">
<rect width="310" height="22" rx="3" fill="#e6e6e6" stroke="#666" />
<text x="8" y="15" font-size="10" stroke="none" fill="#000">Project: Onboarding · Agents: 3 · all must match</text>
</g>
</g>
<g transform="translate(263,80)"><rect width="64" height="24" rx="3" /><text x="32" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Edit</text></g>
</g>
<g data-region="humans-card-3" transform="translate(16,608)">
<rect width="343" height="100" rx="4" />
<g stroke="none">
<text x="16" y="28" font-size="14" fill="#000" font-weight="600">Sam Rivera</text>
<text x="16" y="46" font-size="12" fill="#666">sam@acme.ai</text>
<g transform="translate(16,58)">
<rect width="68" height="22" rx="11" /><text x="34" y="15" font-size="11" fill="#000" text-anchor="middle">Operator</text>
<rect x="76" width="64" height="22" rx="11" /><text x="108" y="15" font-size="11" fill="#000" text-anchor="middle">pending</text>
</g>
</g>
<g transform="translate(263,64)"><rect width="64" height="24" rx="3" /><text x="32" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Edit</text></g>
</g>
<!-- 6: Agents header -->
<g data-region="agents-header" transform="translate(16,728)">
<text x="0" y="20" font-size="18" stroke="none" fill="#000">Agents · 7</text>
<text x="0" y="40" font-size="11" stroke="none" fill="#666">Profile, config, assign, invoke, escalation.</text>
</g>
<!-- 7: Agents cards -->
<g data-region="agents-card-1" transform="translate(16,780)">
<rect width="343" height="116" rx="4" />
<g stroke="none">
<text x="16" y="28" font-size="14" fill="#000" font-weight="600">CEO · Helios</text>
<text x="16" y="46" font-size="12" fill="#666">claude-opus · CEO · reports board</text>
<g transform="translate(16,58)" font-size="10">
<rect width="74" height="20" rx="3" /><text x="37" y="14" stroke="none" fill="#000" text-anchor="middle">profile: co.</text>
<rect x="82" width="78" height="20" rx="3" /><text x="121" y="14" stroke="none" fill="#000" text-anchor="middle">config: redact</text>
<rect x="168" width="80" height="20" rx="3" /><text x="208" y="14" stroke="none" fill="#000" text-anchor="middle">assign: board</text>
</g>
<text x="16" y="104" font-size="12" fill="#666">View details →</text>
</g>
</g>
<g data-region="agents-card-2" transform="translate(16,912)">
<rect width="343" height="124" rx="4" />
<g stroke="none">
<text x="16" y="28" font-size="14" fill="#000" font-weight="600">Researcher · Vega</text>
<g transform="translate(160,14)"><rect width="58" height="20" rx="3" fill="#e6e6e6" stroke="#666" /><text x="29" y="14" font-size="10" stroke="none" fill="#000" text-anchor="middle">private</text></g>
<text x="16" y="46" font-size="12" fill="#666">claude-haiku · Researcher · reports CTO</text>
<g transform="translate(16,58)" font-size="10">
<rect width="80" height="20" rx="3" /><text x="40" y="14" stroke="none" fill="#000" text-anchor="middle">profile: board</text>
<rect x="88" width="80" height="20" rx="3" /><text x="128" y="14" stroke="none" fill="#000" text-anchor="middle">config: board</text>
<rect x="176" width="72" height="20" rx="3" /><text x="212" y="14" stroke="none" fill="#000" text-anchor="middle">assign: CTO</text>
</g>
<text x="16" y="104" font-size="11" fill="#666">Issues + comments stay company-visible.</text>
</g>
</g>
<!-- 8: bottom CTA -->
<g data-region="view-all" transform="translate(16,1056)">
<rect width="343" height="40" rx="4" />
<text x="171" y="26" font-size="13" stroke="none" fill="#000" text-anchor="middle">View all 7 agents</text>
</g>
<!-- 9: home indicator reserve -->
<g data-region="annotations" stroke="#d33" stroke-dasharray="4 3" fill="none">
<circle cx="32" cy="200" r="10" />
<circle cx="32" cy="468" r="10" />
<circle cx="32" cy="800" r="10" />
<circle cx="200" cy="918" r="10" />
</g>
<g font-size="10" stroke="none" fill="#d33">
<text x="48" y="203">1</text>
<text x="48" y="471">2</text>
<text x="48" y="803">3</text>
<text x="216" y="921">4</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.9 KiB

@@ -1,278 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="1240" viewBox="0 0 1280 1240"
font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
<rect x="0" y="0" width="1280" height="1240" />
<!-- 1: top bar / app chrome -->
<g data-region="topbar">
<rect x="0" y="0" width="1280" height="56" />
<text x="24" y="34" font-size="14" stroke="none" fill="#000">Paperclip · Acme AI Co.</text>
<line x1="220" y1="20" x2="220" y2="36" />
<text x="240" y="34" font-size="12" stroke="none" fill="#666">Company › Settings › Access</text>
<g transform="translate(1144,12)">
<rect width="112" height="32" rx="4" />
<text x="56" y="20" font-size="12" stroke="none" fill="#000" text-anchor="middle">+ Invite</text>
</g>
</g>
<!-- 2: side nav -->
<g data-region="sidenav" transform="translate(0,56)">
<rect x="0" y="0" width="216" height="1184" />
<text x="24" y="32" font-size="12" stroke="none" fill="#666">SETTINGS</text>
<g font-size="14" stroke="none" fill="#000">
<text x="24" y="64">General</text>
<text x="24" y="96">Branding</text>
<text x="24" y="128">Environments</text>
<text x="24" y="160">Secrets</text>
<text x="24" y="192">Budgets</text>
</g>
<rect x="8" y="208" width="200" height="32" fill="#e6e6e6" stroke="none" />
<text x="24" y="228" font-size="14" stroke="none" fill="#000">Access</text>
<g font-size="14" stroke="none" fill="#000">
<text x="24" y="264">Approvals</text>
<text x="24" y="296">Activity log</text>
</g>
</g>
<!-- 3: header -->
<g data-region="header" transform="translate(248,80)">
<text x="0" y="32" font-size="28" stroke="none" fill="#000">Company access</text>
<text x="0" y="64" font-size="14" stroke="none" fill="#666">Manage humans, agents, roles, and scoped grants for Acme AI Co.</text>
</g>
<!-- 4: tabs -->
<g data-region="tabs" transform="translate(248,168)">
<line x1="0" y1="40" x2="1000" y2="40" />
<g font-size="14" stroke="none">
<text x="0" y="24" fill="#000" font-weight="600">Members</text>
<line x1="-4" y1="40" x2="76" y2="40" stroke="#000" stroke-width="2.5" />
<text x="120" y="24" fill="#666">Roles &amp; implicit grants</text>
<text x="316" y="24" fill="#666">Invites</text>
<text x="408" y="24" fill="#666">Join requests · 2</text>
</g>
</g>
<!-- 5: visibility note -->
<g data-region="visibility-banner" transform="translate(248,232)">
<rect width="1000" height="72" rx="4" />
<g transform="translate(20,28)">
<text x="0" y="0" font-size="14" stroke="none" fill="#000" font-weight="600">Roles and grants control who can act — not what they see.</text>
<circle cx="378" cy="-6" r="8" fill="#fff" stroke="#666" />
<text x="378" y="-2" font-size="11" stroke="none" fill="#666" text-anchor="middle">?</text>
</g>
<text x="20" y="48" font-size="13" stroke="none" fill="#666">Use them to decide who can assign tasks, manage agents, or edit config. Issues, comments, and projects stay</text>
<text x="20" y="64" font-size="13" stroke="none" fill="#666">visible to the whole company so the board can audit work.</text>
<text x="936" y="44" font-size="12" stroke="none" fill="#666" text-anchor="end">Learn more →</text>
</g>
<!-- 6: roles explainer (compact) -->
<g data-region="role-cheatsheet" transform="translate(248,328)">
<text x="0" y="16" font-size="12" stroke="none" fill="#666">WHAT ROLES INCLUDE BY DEFAULT</text>
<g transform="translate(0,28)">
<rect width="1000" height="88" rx="4" />
<g font-size="13" stroke="none" fill="#000">
<text x="20" y="28" font-weight="600">Owner</text>
<text x="20" y="48" fill="#666">Create agents · Invite · Manage members · Assign tasks · Approve joins</text>
<text x="20" y="68" fill="#666">Implicit. Add explicit grants only as overrides.</text>
</g>
<line x1="250" y1="12" x2="250" y2="76" />
<g font-size="13" stroke="none" fill="#000" transform="translate(266,0)">
<text x="0" y="28" font-weight="600">Admin</text>
<text x="0" y="48" fill="#666">Create agents · Invite · Assign tasks · Approve joins</text>
</g>
<line x1="500" y1="12" x2="500" y2="76" />
<g font-size="13" stroke="none" fill="#000" transform="translate(516,0)">
<text x="0" y="28" font-weight="600">Operator</text>
<text x="0" y="48" fill="#666">Assign tasks · Read company</text>
</g>
<line x1="750" y1="12" x2="750" y2="76" />
<g font-size="13" stroke="none" fill="#000" transform="translate(766,0)">
<text x="0" y="28" font-weight="600">Viewer</text>
<text x="0" y="48" fill="#666">Read company. No mutations.</text>
</g>
</g>
</g>
<!-- 7: HUMANS section header -->
<g data-region="humans-header" transform="translate(248,464)">
<text x="0" y="20" font-size="20" stroke="none" fill="#000">Humans · 5</text>
<text x="0" y="40" font-size="13" stroke="none" fill="#666">Human members with company memberships, status, and explicit grants.</text>
</g>
<!-- 8: HUMANS table -->
<g data-region="humans-table" transform="translate(248,520)">
<rect width="1000" height="248" rx="4" />
<!-- column header row -->
<g transform="translate(0,0)">
<line x1="0" y1="40" x2="1000" y2="40" />
<g font-size="11" stroke="none" fill="#666">
<text x="20" y="26">USER</text>
<text x="380" y="26">ROLE</text>
<text x="490" y="26">STATUS</text>
<text x="600" y="26">GRANTS</text>
<text x="970" y="26" text-anchor="end">ACTION</text>
</g>
</g>
<!-- row 1 -->
<g transform="translate(0,40)">
<line x1="0" y1="56" x2="1000" y2="56" />
<g stroke="none">
<text x="20" y="26" font-size="14" fill="#000">Maya Chen</text>
<text x="20" y="44" font-size="12" fill="#666">maya@acme.ai · you</text>
<text x="380" y="32" font-size="13" fill="#000">Owner</text>
<g transform="translate(490,18)"><rect width="80" height="22" rx="11" /><text x="40" y="15" font-size="11" fill="#000" text-anchor="middle">active</text></g>
<text x="600" y="32" font-size="13" fill="#666">Implicit (Owner) · no overrides</text>
</g>
<g transform="translate(848,16)">
<rect width="56" height="24" rx="3" /><text x="28" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Edit</text>
<rect x="64" width="80" height="24" rx="3" /><text x="104" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Remove</text>
</g>
</g>
<!-- row 2 -->
<g transform="translate(0,96)">
<line x1="0" y1="56" x2="1000" y2="56" />
<g stroke="none">
<text x="20" y="26" font-size="14" fill="#000">Jordan Park</text>
<text x="20" y="44" font-size="12" fill="#666">jordan@acme.ai</text>
<text x="380" y="32" font-size="13" fill="#000">Admin</text>
<g transform="translate(490,18)"><rect width="80" height="22" rx="11" /><text x="40" y="15" font-size="11" fill="#000" text-anchor="middle">active</text></g>
<text x="600" y="32" font-size="13" fill="#000">tasks:assign_scope</text>
<g transform="translate(600,40)">
<rect width="220" height="18" rx="3" fill="#e6e6e6" stroke="#666" />
<text x="8" y="13" font-size="10" stroke="none" fill="#000">Project: Onboarding · Agents: 3</text>
</g>
</g>
<g transform="translate(848,16)">
<rect width="56" height="24" rx="3" /><text x="28" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Edit</text>
<rect x="64" width="80" height="24" rx="3" /><text x="104" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Remove</text>
</g>
</g>
<!-- row 3 -->
<g transform="translate(0,152)">
<line x1="0" y1="56" x2="1000" y2="56" />
<g stroke="none">
<text x="20" y="26" font-size="14" fill="#000">Sam Rivera</text>
<text x="20" y="44" font-size="12" fill="#666">sam@acme.ai</text>
<text x="380" y="32" font-size="13" fill="#000">Operator</text>
<g transform="translate(490,18)"><rect width="80" height="22" rx="11" /><text x="40" y="15" font-size="11" fill="#000" text-anchor="middle">pending</text></g>
<text x="600" y="32" font-size="13" fill="#666">No explicit grants</text>
</g>
<g transform="translate(848,16)">
<rect width="56" height="24" rx="3" /><text x="28" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Edit</text>
<rect x="64" width="80" height="24" rx="3" /><text x="104" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Remove</text>
</g>
</g>
<!-- row 4 -->
<g transform="translate(0,208)">
<g stroke="none">
<text x="20" y="26" font-size="14" fill="#000">Priya Shah</text>
<text x="20" y="44" font-size="12" fill="#666">priya@acme.ai</text>
<text x="380" y="32" font-size="13" fill="#000">Viewer</text>
<g transform="translate(490,18)"><rect width="80" height="22" rx="11" /><text x="40" y="15" font-size="11" fill="#000" text-anchor="middle">suspended</text></g>
<text x="600" y="32" font-size="13" fill="#666">No explicit grants</text>
</g>
<g transform="translate(848,16)">
<rect width="56" height="24" rx="3" /><text x="28" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Edit</text>
<rect x="64" width="80" height="24" rx="3" /><text x="104" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Remove</text>
</g>
</g>
</g>
<!-- 9: AGENTS section header -->
<g data-region="agents-header" transform="translate(248,800)">
<text x="0" y="20" font-size="20" stroke="none" fill="#000">Agents · 7</text>
<text x="0" y="40" font-size="13" stroke="none" fill="#666">Profile visibility, config redaction, who can assign, who can invoke, and escalation chain.</text>
<text x="1000" y="32" font-size="12" stroke="none" fill="#000" text-anchor="end">Manage agent permissions →</text>
</g>
<!-- 10: AGENTS table -->
<g data-region="agents-table" transform="translate(248,856)">
<rect width="1000" height="304" rx="4" />
<g transform="translate(0,0)">
<line x1="0" y1="40" x2="1000" y2="40" />
<g font-size="11" stroke="none" fill="#666">
<text x="20" y="26">AGENT</text>
<text x="290" y="26">ROLE / REPORTS TO</text>
<text x="490" y="26">PROFILE</text>
<text x="580" y="26">CONFIG</text>
<text x="680" y="26">ASSIGN-TO</text>
<text x="800" y="26">INVOKE</text>
<text x="970" y="26" text-anchor="end">ACTION</text>
</g>
</g>
<!-- agent row 1 -->
<g transform="translate(0,40)">
<line x1="0" y1="56" x2="1000" y2="56" />
<g stroke="none">
<text x="20" y="26" font-size="14" fill="#000">CEO · Helios</text>
<text x="20" y="44" font-size="12" fill="#666">claude-opus · process adapter</text>
<text x="290" y="32" font-size="13" fill="#000">CEO · board</text>
<text x="490" y="32" font-size="13" fill="#000">Company</text>
<text x="580" y="32" font-size="13" fill="#000">Redacted</text>
<text x="680" y="32" font-size="13" fill="#000">Board, owners</text>
<text x="800" y="32" font-size="13" fill="#000">Board only</text>
</g>
<g transform="translate(900,16)"><rect width="80" height="24" rx="3" /><text x="40" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Details</text></g>
</g>
<!-- agent row 2 -->
<g transform="translate(0,96)">
<line x1="0" y1="56" x2="1000" y2="56" />
<g stroke="none">
<text x="20" y="26" font-size="14" fill="#000">CTO · Atlas</text>
<text x="20" y="44" font-size="12" fill="#666">claude-sonnet · process adapter</text>
<text x="290" y="32" font-size="13" fill="#000">CTO · reports CEO</text>
<text x="490" y="32" font-size="13" fill="#000">Company</text>
<text x="580" y="32" font-size="13" fill="#000">Redacted</text>
<text x="680" y="32" font-size="13" fill="#000">CEO, owners</text>
<text x="800" y="32" font-size="13" fill="#000">Manager+</text>
</g>
<g transform="translate(900,16)"><rect width="80" height="24" rx="3" /><text x="40" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Details</text></g>
</g>
<!-- agent row 3 - private -->
<g transform="translate(0,152)">
<line x1="0" y1="56" x2="1000" y2="56" />
<g stroke="none">
<text x="20" y="26" font-size="14" fill="#000">Researcher · Vega</text>
<g transform="translate(160,12)"><rect width="58" height="20" rx="3" fill="#e6e6e6" stroke="#666" /><text x="29" y="14" font-size="10" stroke="none" fill="#000" text-anchor="middle">private</text></g>
<text x="20" y="44" font-size="12" fill="#666">claude-haiku · http adapter</text>
<text x="290" y="32" font-size="13" fill="#000">Researcher · CTO</text>
<text x="490" y="32" font-size="13" fill="#000">Board-only</text>
<text x="580" y="32" font-size="13" fill="#000">Board-only</text>
<text x="680" y="32" font-size="13" fill="#000">CTO, owners</text>
<text x="800" y="32" font-size="13" fill="#000">CTO only</text>
</g>
<g transform="translate(900,16)"><rect width="80" height="24" rx="3" /><text x="40" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Details</text></g>
</g>
<!-- agent row 4 -->
<g transform="translate(0,208)">
<line x1="0" y1="56" x2="1000" y2="56" />
<g stroke="none">
<text x="20" y="26" font-size="14" fill="#000">Engineer · Ada</text>
<text x="20" y="44" font-size="12" fill="#666">claude-sonnet · process adapter</text>
<text x="290" y="32" font-size="13" fill="#000">Engineer · CTO</text>
<text x="490" y="32" font-size="13" fill="#000">Company</text>
<text x="580" y="32" font-size="13" fill="#000">Redacted</text>
<text x="680" y="32" font-size="13" fill="#000">CTO, scoped</text>
<text x="800" y="32" font-size="13" fill="#000">Manager+</text>
</g>
<g transform="translate(900,16)"><rect width="80" height="24" rx="3" /><text x="40" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Details</text></g>
</g>
<!-- footer empty/load more -->
<g transform="translate(0,264)">
<text x="500" y="26" font-size="13" stroke="none" fill="#666" text-anchor="middle">3 more agents — view all →</text>
</g>
</g>
<!-- annotation callouts -->
<g data-region="annotations" stroke="#d33" stroke-dasharray="4 3" fill="none">
<circle cx="270" cy="262" r="12" />
<circle cx="270" cy="568" r="12" />
<circle cx="270" cy="872" r="12" />
<circle cx="822" cy="592" r="12" />
</g>
<g font-size="11" stroke="none" fill="#d33">
<text x="288" y="266">1</text>
<text x="288" y="572">2</text>
<text x="288" y="876">3</text>
<text x="838" y="596">4</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

@@ -1,105 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="375" height="1100" viewBox="0 0 375 1100"
font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
<rect x="0" y="0" width="375" height="1100" />
<!-- 1: status + header -->
<g data-region="topbar">
<line x1="0" y1="44" x2="375" y2="44" />
<text x="16" y="76" font-size="14" stroke="none" fill="#666">← Edit member</text>
<text x="16" y="108" font-size="20" stroke="none" fill="#000">Scoped task assignment</text>
<text x="16" y="128" font-size="12" stroke="none" fill="#666">Jordan Park · tasks:assign_scope</text>
</g>
<!-- 2: All-of explainer -->
<g data-region="explainer" transform="translate(16,152)">
<rect width="343" height="80" rx="4" />
<text x="16" y="24" font-size="13" stroke="none" fill="#000" font-weight="600">All chosen scopes must match</text>
<text x="16" y="42" font-size="11" stroke="none" fill="#666">Leave any section "Any" to skip that family.</text>
<text x="16" y="58" font-size="11" stroke="none" fill="#666">At least one family must be set, or this grant</text>
<text x="16" y="74" font-size="11" stroke="none" fill="#666">becomes unscoped tasks:assign.</text>
</g>
<!-- 3: A: project scope -->
<g data-region="project-scope" transform="translate(16,260)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">1 · PROJECT SCOPE</text>
<text x="0" y="36" font-size="14" stroke="none" fill="#000">Restrict to specific projects</text>
<g transform="translate(0,52)">
<g><circle cx="8" cy="8" r="7" /><text x="22" y="12" font-size="12" stroke="none" fill="#666">Any project</text></g>
<g transform="translate(0,24)"><circle cx="8" cy="8" r="7" fill="#000" /><circle cx="8" cy="8" r="2.5" fill="#fff" stroke="none" /><text x="22" y="12" font-size="12" stroke="none" fill="#000">Selected (2)</text></g>
</g>
<g transform="translate(0,116)">
<rect width="343" height="36" rx="4" />
<rect x="8" y="6" width="100" height="24" rx="12" fill="#e6e6e6" stroke="#666" /><text x="58" y="22" font-size="11" stroke="none" fill="#000" text-anchor="middle">Onboarding ×</text>
<rect x="116" y="6" width="116" height="24" rx="12" fill="#e6e6e6" stroke="#666" /><text x="174" y="22" font-size="11" stroke="none" fill="#000" text-anchor="middle">Growth Loops ×</text>
<text x="324" y="22" font-size="11" stroke="none" fill="#666" text-anchor="end">+ Add</text>
</g>
</g>
<!-- 4: B: managed subtree scope -->
<g data-region="subtree-scope" transform="translate(16,460)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">2 · MANAGED-SUBTREE SCOPE</text>
<text x="0" y="36" font-size="14" stroke="none" fill="#000">Restrict to everyone under a manager</text>
<g transform="translate(0,52)">
<g><circle cx="8" cy="8" r="7" fill="#000" /><circle cx="8" cy="8" r="2.5" fill="#fff" stroke="none" /><text x="22" y="12" font-size="12" stroke="none" fill="#000">Any subtree</text></g>
<g transform="translate(0,24)"><circle cx="8" cy="8" r="7" /><text x="22" y="12" font-size="12" stroke="none" fill="#666">Selected subtree roots</text></g>
</g>
<text x="0" y="116" font-size="11" stroke="none" fill="#666">Selecting a manager grants assignment to that</text>
<text x="0" y="132" font-size="11" stroke="none" fill="#666">manager and all their reports, recursively.</text>
</g>
<!-- 5: C: target agent allowlist -->
<g data-region="agent-allowlist" transform="translate(16,640)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">3 · TARGET-AGENT ALLOWLIST</text>
<text x="0" y="36" font-size="14" stroke="none" fill="#000">Restrict to a specific list of agents</text>
<g transform="translate(0,52)">
<g><circle cx="8" cy="8" r="7" /><text x="22" y="12" font-size="12" stroke="none" fill="#666">Any agent</text></g>
<g transform="translate(0,24)"><circle cx="8" cy="8" r="7" fill="#000" /><circle cx="8" cy="8" r="2.5" fill="#fff" stroke="none" /><text x="22" y="12" font-size="12" stroke="none" fill="#000">Selected (3)</text></g>
</g>
<g transform="translate(0,116)">
<rect width="343" height="64" rx="4" />
<rect x="8" y="6" width="116" height="24" rx="12" fill="#e6e6e6" stroke="#666" /><text x="66" y="22" font-size="11" stroke="none" fill="#000" text-anchor="middle">Engineer · Ada ×</text>
<rect x="132" y="6" width="116" height="24" rx="12" fill="#e6e6e6" stroke="#666" /><text x="190" y="22" font-size="11" stroke="none" fill="#000" text-anchor="middle">Designer · Iris ×</text>
<rect x="8" y="34" width="108" height="24" rx="12" fill="#e6e6e6" stroke="#666" /><text x="62" y="50" font-size="11" stroke="none" fill="#000" text-anchor="middle">QA · Cypress ×</text>
<text x="324" y="22" font-size="11" stroke="none" fill="#666" text-anchor="end">+ Add</text>
</g>
</g>
<!-- 6: result summary card -->
<g data-region="summary" transform="translate(16,852)">
<rect width="343" height="172" rx="4" />
<text x="16" y="24" font-size="11" stroke="none" fill="#666">RESULT</text>
<text x="16" y="48" font-size="13" stroke="none" fill="#000" font-weight="600">Assignment must satisfy ALL:</text>
<g font-size="12" stroke="none" transform="translate(16,60)">
<text y="20" fill="#000">Project: Onboarding, Growth Loops</text>
<text y="40" fill="#666">Subtree: any</text>
<text y="60" fill="#000">Agent: Ada, Iris, Cypress</text>
</g>
<text x="16" y="156" font-size="10" stroke="none" fill="#666">Denials return 403 with a generic message — no leakage of hidden resources.</text>
</g>
<!-- 7: sticky footer -->
<g data-region="sticky-footer" transform="translate(0,1036)">
<line x1="0" y1="0" x2="375" y2="0" />
<rect x="0" y="0" width="375" height="64" />
<g transform="translate(16,12)">
<rect width="160" height="40" rx="4" /><text x="80" y="25" font-size="13" stroke="none" fill="#000" text-anchor="middle">Cancel</text>
<rect x="168" width="175" height="40" rx="4" fill="#000" /><text x="255" y="25" font-size="13" stroke="none" fill="#fff" text-anchor="middle">Save grant</text>
</g>
</g>
<!-- annotations -->
<g stroke="#d33" stroke-dasharray="4 3" fill="none">
<circle cx="32" cy="176" r="9" />
<circle cx="32" cy="284" r="9" />
<circle cx="32" cy="484" r="9" />
<circle cx="32" cy="664" r="9" />
<circle cx="32" cy="876" r="9" />
</g>
<g font-size="10" stroke="none" fill="#d33">
<text x="46" y="180">1</text>
<text x="46" y="288">2</text>
<text x="46" y="488">3</text>
<text x="46" y="668">4</text>
<text x="46" y="880">5</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.6 KiB

@@ -1,145 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="900" viewBox="0 0 1280 900"
font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
<rect x="0" y="0" width="1280" height="900" />
<!-- 1: dimmed underlay (just outline + label) -->
<g data-region="underlay" stroke="#e6e6e6">
<text x="48" y="40" font-size="12" stroke="none" fill="#666">SETTINGS › ACCESS › EDIT MEMBER: JORDAN PARK</text>
<line x1="48" y1="56" x2="1232" y2="56" />
<rect x="48" y="80" width="1184" height="800" fill="#fafafa" stroke="#e6e6e6" />
<text x="64" y="112" font-size="12" stroke="none" fill="#666">underlying access page (dimmed)</text>
</g>
<!-- 2: dialog -->
<g data-region="dialog" transform="translate(248,72)">
<rect width="784" height="752" rx="6" />
<!-- header -->
<g transform="translate(0,0)">
<line x1="0" y1="76" x2="784" y2="76" />
<text x="24" y="36" font-size="20" stroke="none" fill="#000">Edit grant · Scoped task assignment</text>
<text x="24" y="60" font-size="13" stroke="none" fill="#666">Jordan Park · tasks:assign_scope · principal_permission_grants.scope</text>
<text x="760" y="40" font-size="20" stroke="none" fill="#666" text-anchor="end">×</text>
</g>
<!-- explainer -->
<g transform="translate(24,96)">
<rect width="736" height="64" rx="4" />
<text x="16" y="24" font-size="13" stroke="none" fill="#000" font-weight="600">All chosen scopes must match for assignment to succeed.</text>
<text x="16" y="44" font-size="12" stroke="none" fill="#666">Leave a section as "Any" to skip that family. At least one constraint family must be set, otherwise this</text>
<text x="16" y="58" font-size="12" stroke="none" fill="#666">becomes an unscoped tasks:assign grant — switch the row above to remove the scope.</text>
</g>
<!-- A: project scope -->
<g data-region="project-scope" transform="translate(24,184)">
<text x="0" y="16" font-size="12" stroke="none" fill="#666">1 · PROJECT SCOPE</text>
<text x="0" y="36" font-size="14" stroke="none" fill="#000">Restrict to specific projects</text>
<g transform="translate(0,52)">
<g><circle cx="10" cy="10" r="8" /><text x="28" y="14" font-size="13" stroke="none" fill="#666">Any project (no project restriction)</text></g>
<g transform="translate(0,28)"><circle cx="10" cy="10" r="8" fill="#000" /><circle cx="10" cy="10" r="3" fill="#fff" stroke="none" /><text x="28" y="14" font-size="13" stroke="none" fill="#000">Selected projects (1 required)</text></g>
</g>
<g transform="translate(0,128)">
<rect width="736" height="40" rx="4" />
<g transform="translate(12,8)">
<rect width="120" height="24" rx="12" fill="#e6e6e6" stroke="#666" /><text x="60" y="16" font-size="12" stroke="none" fill="#000" text-anchor="middle">Onboarding ×</text>
<rect x="128" width="124" height="24" rx="12" fill="#e6e6e6" stroke="#666" /><text x="190" y="16" font-size="12" stroke="none" fill="#000" text-anchor="middle">Growth Loops ×</text>
</g>
<text x="640" y="26" font-size="12" stroke="none" fill="#666">+ Add project</text>
</g>
</g>
<!-- B: managed-subtree scope -->
<g data-region="subtree-scope" transform="translate(24,376)">
<text x="0" y="16" font-size="12" stroke="none" fill="#666">2 · MANAGED-SUBTREE SCOPE</text>
<text x="0" y="36" font-size="14" stroke="none" fill="#000">Restrict to everyone under a manager</text>
<g transform="translate(0,52)">
<g><circle cx="10" cy="10" r="8" fill="#000" /><circle cx="10" cy="10" r="3" fill="#fff" stroke="none" /><text x="28" y="14" font-size="13" stroke="none" fill="#000">Any subtree (no subtree restriction)</text></g>
<g transform="translate(0,28)"><circle cx="10" cy="10" r="8" /><text x="28" y="14" font-size="13" stroke="none" fill="#666">Selected subtree roots</text></g>
</g>
<text x="0" y="124" font-size="12" stroke="none" fill="#666">Selecting a manager grants assignment to that manager and all reports below them, recursively.</text>
</g>
<!-- C: target agent allowlist -->
<g data-region="agent-allowlist" transform="translate(24,544)">
<text x="0" y="16" font-size="12" stroke="none" fill="#666">3 · TARGET-AGENT ALLOWLIST</text>
<text x="0" y="36" font-size="14" stroke="none" fill="#000">Restrict to a specific list of agents</text>
<g transform="translate(0,52)">
<g><circle cx="10" cy="10" r="8" /><text x="28" y="14" font-size="13" stroke="none" fill="#666">Any agent (no agent restriction)</text></g>
<g transform="translate(0,28)"><circle cx="10" cy="10" r="8" fill="#000" /><circle cx="10" cy="10" r="3" fill="#fff" stroke="none" /><text x="28" y="14" font-size="13" stroke="none" fill="#000">Selected agents (3)</text></g>
</g>
<g transform="translate(0,128)">
<rect width="736" height="40" rx="4" />
<g transform="translate(12,8)">
<rect width="120" height="24" rx="12" fill="#e6e6e6" stroke="#666" /><text x="60" y="16" font-size="12" stroke="none" fill="#000" text-anchor="middle">Engineer · Ada ×</text>
<rect x="128" width="120" height="24" rx="12" fill="#e6e6e6" stroke="#666" /><text x="188" y="16" font-size="12" stroke="none" fill="#000" text-anchor="middle">Designer · Iris ×</text>
<rect x="256" width="108" height="24" rx="12" fill="#e6e6e6" stroke="#666" /><text x="310" y="16" font-size="12" stroke="none" fill="#000" text-anchor="middle">QA · Cypress ×</text>
</g>
<text x="640" y="26" font-size="12" stroke="none" fill="#666">+ Add agent</text>
</g>
</g>
</g>
<!-- 3: live summary preview (right rail) -->
<g data-region="summary-rail" transform="translate(1048,72)">
<rect width="200" height="752" rx="6" />
<g transform="translate(16,24)">
<text x="0" y="0" font-size="12" stroke="none" fill="#666">RESULT SUMMARY</text>
<text x="0" y="32" font-size="14" stroke="none" fill="#000" font-weight="600">When Jordan tries to</text>
<text x="0" y="48" font-size="14" stroke="none" fill="#000" font-weight="600">assign a task…</text>
<text x="0" y="84" font-size="12" stroke="none" fill="#000">it must satisfy ALL of:</text>
<g transform="translate(0,108)">
<text x="0" y="0" font-size="11" stroke="none" fill="#666">PROJECT</text>
<text x="0" y="20" font-size="12" stroke="none" fill="#000">Onboarding</text>
<text x="0" y="36" font-size="12" stroke="none" fill="#000">Growth Loops</text>
</g>
<g transform="translate(0,180)">
<text x="0" y="0" font-size="11" stroke="none" fill="#666">SUBTREE</text>
<text x="0" y="20" font-size="12" stroke="none" fill="#666">— any —</text>
</g>
<g transform="translate(0,232)">
<text x="0" y="0" font-size="11" stroke="none" fill="#666">AGENT</text>
<text x="0" y="20" font-size="12" stroke="none" fill="#000">Engineer · Ada</text>
<text x="0" y="36" font-size="12" stroke="none" fill="#000">Designer · Iris</text>
<text x="0" y="52" font-size="12" stroke="none" fill="#000">QA · Cypress</text>
</g>
<g transform="translate(0,324)">
<line x1="0" y1="0" x2="168" y2="0" />
<text x="0" y="24" font-size="11" stroke="none" fill="#666">DENIALS</text>
<text x="0" y="44" font-size="11" stroke="none" fill="#666">Return 403 with a generic</text>
<text x="0" y="58" font-size="11" stroke="none" fill="#666">scope message — no leak</text>
<text x="0" y="72" font-size="11" stroke="none" fill="#666">of hidden projects/agents.</text>
</g>
</g>
</g>
<!-- 4: footer / actions -->
<g data-region="footer" transform="translate(248,824)">
<rect width="784" height="56" rx="0" />
<line x1="0" y1="0" x2="784" y2="0" />
<g transform="translate(560,12)">
<rect width="100" height="32" rx="4" /><text x="50" y="20" font-size="13" stroke="none" fill="#000" text-anchor="middle">Cancel</text>
<rect x="112" width="112" height="32" rx="4" fill="#000" /><text x="168" y="20" font-size="13" stroke="none" fill="#fff" text-anchor="middle">Save grant</text>
</g>
<text x="24" y="32" font-size="12" stroke="none" fill="#666">Saved scope is shown in the member grants column.</text>
</g>
<!-- annotations -->
<g stroke="#d33" stroke-dasharray="4 3" fill="none">
<circle cx="266" cy="216" r="10" />
<circle cx="266" cy="408" r="10" />
<circle cx="266" cy="576" r="10" />
<circle cx="1062" cy="148" r="10" />
<circle cx="266" cy="184" r="10" />
</g>
<g font-size="11" stroke="none" fill="#d33">
<text x="282" y="220">2</text>
<text x="282" y="412">3</text>
<text x="282" y="580">4</text>
<text x="1078" y="152">5</text>
<text x="282" y="188">1</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.8 KiB

@@ -1,181 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="375" height="1700" viewBox="0 0 375 1700"
font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
<rect x="0" y="0" width="375" height="1700" />
<!-- 1: top bar -->
<g data-region="topbar">
<line x1="0" y1="44" x2="375" y2="44" />
<text x="16" y="76" font-size="12" stroke="none" fill="#666">← Agents</text>
</g>
<!-- 2: agent header -->
<g data-region="agent-header" transform="translate(16,96)">
<rect width="36" height="36" rx="18" fill="#e6e6e6" stroke="#666" />
<text x="52" y="20" font-size="16" stroke="none" fill="#000">Researcher · Vega</text>
<g transform="translate(228,4)"><rect width="56" height="20" rx="3" fill="#e6e6e6" stroke="#666" /><text x="28" y="14" font-size="10" stroke="none" fill="#000" text-anchor="middle">private</text></g>
<text x="52" y="36" font-size="11" stroke="none" fill="#666">claude-haiku · reports CTO Atlas</text>
</g>
<!-- 3: subnav scroll tabs -->
<g data-region="agent-tabs" transform="translate(16,152)">
<line x1="0" y1="32" x2="343" y2="32" />
<g font-size="12" stroke="none">
<text x="0" y="20" fill="#666">Overview</text>
<text x="68" y="20" fill="#666">Config</text>
<text x="120" y="20" fill="#000" font-weight="600">Permissions</text>
<line x1="116" y1="32" x2="208" y2="32" stroke="#000" stroke-width="2.5" />
<text x="220" y="20" fill="#666">Budget</text>
<text x="276" y="20" fill="#666">Runs ›</text>
</g>
</g>
<!-- 4: Private marker card -->
<g data-region="private-marker" transform="translate(16,200)">
<rect width="343" height="148" rx="4" />
<text x="16" y="24" font-size="13" stroke="none" fill="#000" font-weight="600">Private agent marker</text>
<g transform="translate(287,12)">
<rect width="56" height="28" rx="14" fill="#000" />
<circle cx="42" cy="14" r="11" fill="#fff" stroke="#000" />
<text x="12" y="18" font-size="10" fill="#fff" stroke="none">ON</text>
</g>
<text x="16" y="48" font-size="11" stroke="none" fill="#000" font-weight="600">Agent privacy does not hide work.</text>
<text x="16" y="68" font-size="11" stroke="none" fill="#666">Hides Vega's profile and config from</text>
<text x="16" y="82" font-size="11" stroke="none" fill="#666">non-board peers.</text>
<text x="16" y="106" font-size="11" stroke="none" fill="#666">Issues, comments, and work products</text>
<text x="16" y="120" font-size="11" stroke="none" fill="#666">stay company-visible.</text>
<text x="16" y="140" font-size="11" stroke="none" fill="#000">Learn more →</text>
</g>
<!-- 5: profile visibility -->
<g data-region="profile-visibility" transform="translate(16,376)">
<rect width="343" height="180" rx="4" />
<text x="16" y="22" font-size="11" stroke="none" fill="#666">1 · PROFILE VISIBILITY</text>
<text x="16" y="44" font-size="14" stroke="none" fill="#000">Who can see this agent's identity</text>
<g transform="translate(16,60)">
<g><circle cx="8" cy="8" r="7" /><text x="22" y="12" font-size="12" stroke="none" fill="#666">Company-visible</text></g>
<g transform="translate(0,24)"><circle cx="8" cy="8" r="7" fill="#000" /><circle cx="8" cy="8" r="2.5" fill="#fff" stroke="none" /><text x="22" y="12" font-size="12" stroke="none" fill="#000">Board-only</text></g>
</g>
<g transform="translate(16,128)">
<rect width="311" height="40" fill="#e6e6e6" stroke="#666" />
<text x="8" y="16" font-size="10" stroke="none" fill="#000">Shown to: Board · Maya</text>
<text x="8" y="30" font-size="10" stroke="none" fill="#000">Chen · Jordan Park · CTO Atlas</text>
</g>
</g>
<!-- 6: config visibility -->
<g data-region="config-visibility" transform="translate(16,584)">
<rect width="343" height="200" rx="4" />
<text x="16" y="22" font-size="11" stroke="none" fill="#666">2 · CONFIG VISIBILITY &amp; REDACTION</text>
<text x="16" y="44" font-size="14" stroke="none" fill="#000">Who reads the runtime config</text>
<text x="16" y="62" font-size="11" stroke="none" fill="#666">Secret fields always redacted in UI.</text>
<g transform="translate(16,76)">
<g><circle cx="8" cy="8" r="7" /><text x="22" y="12" font-size="12" stroke="none" fill="#666">Company</text></g>
<g transform="translate(0,24)"><circle cx="8" cy="8" r="7" fill="#000" /><circle cx="8" cy="8" r="2.5" fill="#fff" stroke="none" /><text x="22" y="12" font-size="12" stroke="none" fill="#000">Board-only</text></g>
</g>
<g transform="translate(16,148)">
<rect width="311" height="40" fill="#e6e6e6" stroke="#666" />
<text x="8" y="16" font-size="10" stroke="none" fill="#000">api_key=&lt;secret · anthropic_key_v3&gt;</text>
<text x="8" y="30" font-size="10" stroke="none" fill="#000">bearer_token=&lt;secret · vega_http&gt;</text>
</g>
</g>
<!-- 7: assignment/invocation -->
<g data-region="assignment" transform="translate(16,812)">
<rect width="343" height="208" rx="4" />
<text x="16" y="22" font-size="11" stroke="none" fill="#666">3 · ASSIGNMENT / INVOCATION</text>
<text x="16" y="44" font-size="14" stroke="none" fill="#000">Who can hand work to Vega</text>
<g transform="translate(16,60)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">CAN ASSIGN TASKS</text>
<g transform="translate(0,22)">
<rect width="76" height="22" rx="11" fill="#e6e6e6" stroke="#666" /><text x="38" y="15" font-size="11" stroke="none" fill="#000" text-anchor="middle">Board</text>
<rect x="84" width="116" height="22" rx="11" fill="#e6e6e6" stroke="#666" /><text x="142" y="15" font-size="11" stroke="none" fill="#000" text-anchor="middle">CTO · Atlas</text>
</g>
</g>
<g transform="translate(16,132)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">CAN INVOKE (manual heartbeat)</text>
<g transform="translate(0,22)">
<rect width="116" height="22" rx="11" fill="#e6e6e6" stroke="#666" /><text x="58" y="15" font-size="11" stroke="none" fill="#000" text-anchor="middle">CTO · Atlas</text>
</g>
</g>
</g>
<!-- 8: tool/secret policy -->
<g data-region="tools" transform="translate(16,1048)">
<rect width="343" height="240" rx="4" />
<text x="16" y="22" font-size="11" stroke="none" fill="#666">4 · TOOL / SECRET POLICY</text>
<text x="16" y="44" font-size="14" stroke="none" fill="#000">What Vega can run and read</text>
<g transform="translate(16,60)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">ALLOWED TOOLS</text>
<g transform="translate(0,22)" font-size="11">
<rect width="72" height="22" rx="3" /><text x="36" y="15" stroke="none" fill="#000" text-anchor="middle">web_fetch</text>
<rect x="80" width="80" height="22" rx="3" /><text x="120" y="15" stroke="none" fill="#000" text-anchor="middle">web_search</text>
<rect x="168" width="120" height="22" rx="3" /><text x="228" y="15" stroke="none" fill="#000" text-anchor="middle">file_read (sandbox)</text>
</g>
</g>
<g transform="translate(16,124)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">SECRET REFS</text>
<g transform="translate(0,22)" font-size="11">
<rect width="132" height="22" rx="3" /><text x="66" y="15" stroke="none" fill="#000" text-anchor="middle">anthropic_key_v3</text>
<rect x="140" width="84" height="22" rx="3" /><text x="182" y="15" stroke="none" fill="#000" text-anchor="middle">vega_http</text>
</g>
</g>
<g transform="translate(16,184)">
<rect width="311" height="40" fill="#e6e6e6" stroke="#666" />
<text x="8" y="16" font-size="10" stroke="none" fill="#000">Tools requiring missing scopes are</text>
<text x="8" y="30" font-size="10" stroke="none" fill="#000">filtered from the picker. Logs redact secrets.</text>
</g>
</g>
<!-- 9: escalation -->
<g data-region="escalation" transform="translate(16,1316)">
<rect width="343" height="260" rx="4" />
<text x="16" y="22" font-size="11" stroke="none" fill="#666">5 · ESCALATION AUTHORITY</text>
<text x="16" y="44" font-size="14" stroke="none" fill="#000">Where refusals and blockers route</text>
<g transform="translate(16,68)" font-size="12">
<g>
<rect width="312" height="32" />
<text x="156" y="20" stroke="none" fill="#000" text-anchor="middle">Researcher · Vega</text>
</g>
<g transform="translate(0,40)">
<line x1="156" y1="0" x2="156" y2="12" />
<text x="156" y="22" font-size="11" stroke="none" fill="#666" text-anchor="middle">↓ refusal / blocked</text>
</g>
<g transform="translate(0,68)">
<rect width="312" height="32" />
<text x="156" y="20" stroke="none" fill="#000" text-anchor="middle">CTO · Atlas · manager</text>
</g>
<g transform="translate(0,108)">
<line x1="156" y1="0" x2="156" y2="12" />
<text x="156" y="22" font-size="11" stroke="none" fill="#666" text-anchor="middle">↓ if unresolved</text>
</g>
<g transform="translate(0,136)">
<rect width="312" height="32" />
<text x="156" y="20" stroke="none" fill="#000" text-anchor="middle">CEO · Helios → Board</text>
</g>
<text x="0" y="188" font-size="10" stroke="none" fill="#666">Each hop writes to activity_log.</text>
</g>
</g>
<!-- 10: audit -->
<g data-region="audit" transform="translate(16,1604)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">View recent permission changes →</text>
</g>
<!-- annotations -->
<g stroke="#d33" stroke-dasharray="4 3" fill="none">
<circle cx="32" cy="224" r="9" />
<circle cx="32" cy="392" r="9" />
<circle cx="32" cy="604" r="9" />
<circle cx="32" cy="828" r="9" />
<circle cx="32" cy="1068" r="9" />
<circle cx="32" cy="1336" r="9" />
</g>
<g font-size="10" stroke="none" fill="#d33">
<text x="46" y="228">0</text>
<text x="46" y="396">1</text>
<text x="46" y="608">2</text>
<text x="46" y="832">3</text>
<text x="46" y="1072">4</text>
<text x="46" y="1340">5</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

@@ -1,215 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="1320" viewBox="0 0 1280 1320"
font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
<rect x="0" y="0" width="1280" height="1320" />
<!-- 1: top bar -->
<g data-region="topbar">
<rect x="0" y="0" width="1280" height="56" />
<text x="24" y="34" font-size="14" stroke="none" fill="#000">Paperclip · Acme AI Co.</text>
<line x1="220" y1="20" x2="220" y2="36" />
<text x="240" y="34" font-size="12" stroke="none" fill="#666">Agents › Researcher · Vega › Permissions</text>
</g>
<!-- 2: agent header -->
<g data-region="agent-header" transform="translate(48,80)">
<rect width="48" height="48" rx="24" fill="#e6e6e6" stroke="#666" />
<text x="64" y="22" font-size="20" stroke="none" fill="#000">Researcher · Vega</text>
<g transform="translate(264,4)"><rect width="60" height="22" rx="3" fill="#e6e6e6" stroke="#666" /><text x="30" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">private</text></g>
<g transform="translate(332,4)"><rect width="56" height="22" rx="3" /><text x="28" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">active</text></g>
<text x="64" y="44" font-size="13" stroke="none" fill="#666">claude-haiku · http adapter · reports CTO · Atlas</text>
</g>
<!-- 3: subnav tabs for this agent page -->
<g data-region="agent-tabs" transform="translate(48,160)">
<line x1="0" y1="40" x2="1184" y2="40" />
<g font-size="14" stroke="none">
<text x="0" y="24" fill="#666">Overview</text>
<text x="100" y="24" fill="#666">Activity</text>
<text x="184" y="24" fill="#666">Tasks</text>
<text x="248" y="24" fill="#666">Config</text>
<text x="328" y="24" fill="#000" font-weight="600">Permissions</text>
<line x1="324" y1="40" x2="436" y2="40" stroke="#000" stroke-width="2.5" />
<text x="464" y="24" fill="#666">Budget</text>
<text x="544" y="24" fill="#666">Runs</text>
</g>
<text x="1184" y="20" font-size="12" stroke="none" fill="#666" text-anchor="end">Last changed 2 days ago by Maya Chen</text>
</g>
<!-- 4: private marker + privacy rule -->
<g data-region="private-marker" transform="translate(48,224)">
<rect width="1184" height="96" rx="4" />
<g transform="translate(20,28)">
<text x="0" y="0" font-size="14" stroke="none" fill="#000" font-weight="600">Private agent marker</text>
<circle cx="170" cy="-6" r="8" fill="#fff" stroke="#666" />
<text x="170" y="-2" font-size="11" stroke="none" fill="#666" text-anchor="middle">?</text>
</g>
<text x="20" y="48" font-size="12" stroke="none" fill="#000" font-weight="600">Agent privacy does not hide work.</text>
<text x="20" y="64" font-size="12" stroke="none" fill="#666">Hides Vega's profile and config from non-board peers. Issues, comments, and work products stay company-visible. Learn more →</text>
<g transform="translate(1080,32)">
<rect width="80" height="28" rx="14" fill="#000" />
<circle cx="64" cy="14" r="11" fill="#fff" stroke="#000" />
<text x="14" y="18" font-size="11" fill="#fff" stroke="none">ON</text>
</g>
<text x="20" y="84" font-size="11" stroke="none" fill="#666">private · since 2026-04-22 · changed by Maya Chen</text>
</g>
<!-- 5: TWO-COLUMN PERMISSIONS GRID -->
<!-- card: Profile visibility -->
<g data-region="profile-visibility" transform="translate(48,344)">
<rect width="584" height="216" rx="4" />
<text x="20" y="28" font-size="11" stroke="none" fill="#666">1 · PROFILE VISIBILITY</text>
<text x="20" y="52" font-size="18" stroke="none" fill="#000">Who can see this agent's identity</text>
<text x="20" y="72" font-size="12" stroke="none" fill="#666">Name, role, capabilities, reporting line.</text>
<g transform="translate(20,96)">
<g><circle cx="8" cy="8" r="7" /><text x="22" y="12" font-size="13" stroke="none" fill="#666">Company-visible · everyone in the company</text></g>
<g transform="translate(0,28)"><circle cx="8" cy="8" r="7" fill="#000" /><circle cx="8" cy="8" r="2.5" fill="#fff" stroke="none" /><text x="22" y="12" font-size="13" stroke="none" fill="#000">Board-only · hidden from peer agents and humans</text></g>
</g>
<g transform="translate(20,184)">
<rect width="544" height="20" fill="#e6e6e6" stroke="#666" />
<text x="8" y="14" font-size="11" stroke="none" fill="#000">Profile shown to: Board · Maya Chen · Jordan Park · CTO Atlas</text>
</g>
</g>
<!-- card: Config visibility -->
<g data-region="config-visibility" transform="translate(648,344)">
<rect width="584" height="216" rx="4" />
<text x="20" y="28" font-size="11" stroke="none" fill="#666">2 · CONFIG VISIBILITY &amp; REDACTION</text>
<text x="20" y="52" font-size="18" stroke="none" fill="#000">Who can read the runtime config</text>
<text x="20" y="72" font-size="12" stroke="none" fill="#666">Adapter, loop behavior, instructions path. Secret-backed fields are always redacted in the UI.</text>
<g transform="translate(20,96)">
<g><circle cx="8" cy="8" r="7" /><text x="22" y="12" font-size="13" stroke="none" fill="#666">Company · board reads full, peers read non-secret</text></g>
<g transform="translate(0,28)"><circle cx="8" cy="8" r="7" fill="#000" /><circle cx="8" cy="8" r="2.5" fill="#fff" stroke="none" /><text x="22" y="12" font-size="13" stroke="none" fill="#000">Board-only · peers see nothing</text></g>
</g>
<g transform="translate(20,184)">
<rect width="544" height="20" fill="#e6e6e6" stroke="#666" />
<text x="8" y="14" font-size="11" stroke="none" fill="#000">api_key=&lt;secret · anthropic_key_v3&gt; · bearer_token=&lt;secret · vega_http&gt;</text>
</g>
</g>
<!-- card: Assignment / invocation -->
<g data-region="assignment-invocation" transform="translate(48,584)">
<rect width="584" height="248" rx="4" />
<text x="20" y="28" font-size="11" stroke="none" fill="#666">3 · ASSIGNMENT / INVOCATION</text>
<text x="20" y="52" font-size="18" stroke="none" fill="#000">Who can hand work to Vega</text>
<text x="20" y="72" font-size="12" stroke="none" fill="#666">Assignment grants execution authority. Invocation = manual heartbeat trigger.</text>
<g transform="translate(20,92)">
<text x="0" y="12" font-size="12" stroke="none" fill="#666">CAN ASSIGN TASKS</text>
<g transform="translate(0,20)" font-size="12">
<rect width="100" height="22" rx="11" fill="#e6e6e6" stroke="#666" /><text x="50" y="15" stroke="none" fill="#000" text-anchor="middle">Board</text>
<rect x="108" width="120" height="22" rx="11" fill="#e6e6e6" stroke="#666" /><text x="168" y="15" stroke="none" fill="#000" text-anchor="middle">CTO · Atlas</text>
<rect x="236" width="124" height="22" rx="11" /><text x="298" y="15" stroke="none" fill="#000" text-anchor="middle">+ Add principal</text>
</g>
</g>
<g transform="translate(20,164)">
<text x="0" y="12" font-size="12" stroke="none" fill="#666">CAN INVOKE (manual heartbeat)</text>
<g transform="translate(0,20)" font-size="12">
<rect width="120" height="22" rx="11" fill="#e6e6e6" stroke="#666" /><text x="60" y="15" stroke="none" fill="#000" text-anchor="middle">CTO · Atlas</text>
<rect x="128" width="124" height="22" rx="11" /><text x="190" y="15" stroke="none" fill="#000" text-anchor="middle">+ Add principal</text>
</g>
</g>
</g>
<!-- card: Tool/secret policy -->
<g data-region="tool-secret-policy" transform="translate(648,584)">
<rect width="584" height="248" rx="4" />
<text x="20" y="28" font-size="11" stroke="none" fill="#666">4 · TOOL / SECRET POLICY</text>
<text x="20" y="52" font-size="18" stroke="none" fill="#000">What Vega can run and read</text>
<text x="20" y="72" font-size="12" stroke="none" fill="#666">Secret refs are looked up at runtime. Logs and activity always redact secret values.</text>
<g transform="translate(20,92)" font-size="13">
<g>
<text x="0" y="12" stroke="none" fill="#666">ALLOWED TOOLS</text>
<g transform="translate(0,20)" font-size="12">
<rect width="78" height="22" rx="3" /><text x="39" y="15" stroke="none" fill="#000" text-anchor="middle">web_fetch</text>
<rect x="86" width="92" height="22" rx="3" /><text x="132" y="15" stroke="none" fill="#000" text-anchor="middle">web_search</text>
<rect x="186" width="120" height="22" rx="3" /><text x="246" y="15" stroke="none" fill="#000" text-anchor="middle">file_read (sandbox)</text>
<rect x="314" width="128" height="22" rx="3" /><text x="378" y="15" stroke="none" fill="#000" text-anchor="middle">+ Add tool</text>
</g>
</g>
<g transform="translate(0,68)">
<text x="0" y="12" stroke="none" fill="#666">SECRET REFS</text>
<g transform="translate(0,20)" font-size="12">
<rect width="148" height="22" rx="3" /><text x="74" y="15" stroke="none" fill="#000" text-anchor="middle">anthropic_key_v3</text>
<rect x="156" width="92" height="22" rx="3" /><text x="202" y="15" stroke="none" fill="#000" text-anchor="middle">vega_http</text>
<rect x="256" width="124" height="22" rx="3" /><text x="318" y="15" stroke="none" fill="#000" text-anchor="middle">+ Add ref</text>
</g>
</g>
<g transform="translate(0,128)" font-size="11">
<rect width="544" height="36" fill="#e6e6e6" stroke="#666" />
<text x="12" y="14" stroke="none" fill="#000">Tools requiring scopes Vega lacks are filtered from the picker — operators don't see denials they can't act on.</text>
<text x="12" y="28" stroke="none" fill="#666">Log redaction: anthropic_key_v3 → &lt;redacted&gt; · vega_http → &lt;redacted&gt;</text>
</g>
</g>
</g>
<!-- card: Escalation authority -->
<g data-region="escalation" transform="translate(48,856)">
<rect width="1184" height="184" rx="4" />
<text x="20" y="28" font-size="11" stroke="none" fill="#666">5 · ESCALATION AUTHORITY</text>
<text x="20" y="52" font-size="18" stroke="none" fill="#000">Where refusals and blockers route</text>
<text x="20" y="72" font-size="12" stroke="none" fill="#666">When Vega marks a task blocked or refuses, the assigned owner is notified. If unresolved, escalation walks the chain.</text>
<g transform="translate(20,100)" font-size="12">
<g>
<rect width="140" height="48" />
<text x="70" y="20" stroke="none" fill="#000" text-anchor="middle">Researcher · Vega</text>
<text x="70" y="36" stroke="none" fill="#666" text-anchor="middle">refusal / blocked</text>
</g>
<line x1="140" y1="24" x2="180" y2="24" />
<text x="160" y="20" font-size="11" stroke="none" fill="#666" text-anchor="middle">→</text>
<g transform="translate(180,0)">
<rect width="140" height="48" />
<text x="70" y="20" stroke="none" fill="#000" text-anchor="middle">CTO · Atlas</text>
<text x="70" y="36" stroke="none" fill="#666" text-anchor="middle">manager</text>
</g>
<line x1="320" y1="24" x2="360" y2="24" />
<text x="340" y="20" font-size="11" stroke="none" fill="#666" text-anchor="middle">→</text>
<g transform="translate(360,0)">
<rect width="140" height="48" />
<text x="70" y="20" stroke="none" fill="#000" text-anchor="middle">CEO · Helios</text>
<text x="70" y="36" stroke="none" fill="#666" text-anchor="middle">grand-manager</text>
</g>
<line x1="500" y1="24" x2="540" y2="24" />
<text x="520" y="20" font-size="11" stroke="none" fill="#666" text-anchor="middle">→</text>
<g transform="translate(540,0)">
<rect width="140" height="48" />
<text x="70" y="20" stroke="none" fill="#000" text-anchor="middle">Board</text>
<text x="70" y="36" stroke="none" fill="#666" text-anchor="middle">final · approval</text>
</g>
<text x="720" y="28" font-size="12" stroke="none" fill="#666">Audit trail: every step writes to activity_log.</text>
</g>
</g>
<!-- card: Audit log -->
<g data-region="audit" transform="translate(48,1064)">
<rect width="1184" height="216" rx="4" />
<text x="20" y="28" font-size="11" stroke="none" fill="#666">RECENT PERMISSION CHANGES</text>
<line x1="0" y1="44" x2="1184" y2="44" />
<g transform="translate(20,60)" font-size="12">
<text x="0" y="14" stroke="none" fill="#000">2026-05-13 · Maya Chen toggled Private agent marker → ON</text>
<text x="0" y="38" stroke="none" fill="#000">2026-05-12 · Jordan Park added secret ref vega_http</text>
<text x="0" y="62" stroke="none" fill="#000">2026-05-10 · CTO Atlas added tool web_search</text>
<text x="0" y="86" stroke="none" fill="#000">2026-05-04 · Maya Chen set config visibility → board-only</text>
<text x="0" y="110" stroke="none" fill="#666">View full activity →</text>
</g>
</g>
<!-- annotations -->
<g stroke="#d33" stroke-dasharray="4 3" fill="none">
<circle cx="66" cy="244" r="10" />
<circle cx="66" cy="364" r="10" />
<circle cx="666" cy="364" r="10" />
<circle cx="66" cy="604" r="10" />
<circle cx="666" cy="604" r="10" />
<circle cx="66" cy="876" r="10" />
</g>
<g font-size="11" stroke="none" fill="#d33">
<text x="82" y="248">0</text>
<text x="82" y="368">1</text>
<text x="682" y="368">2</text>
<text x="82" y="608">3</text>
<text x="682" y="608">4</text>
<text x="82" y="880">5</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

@@ -1,156 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="375" height="1660" viewBox="0 0 375 1660"
font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
<rect x="0" y="0" width="375" height="1660" />
<!-- 1: status + title -->
<g data-region="topbar">
<line x1="0" y1="44" x2="375" y2="44" />
<text x="16" y="76" font-size="20" stroke="none" fill="#000">Denied / read-only</text>
<text x="16" y="96" font-size="11" stroke="none" fill="#666">Four denial patterns at mobile width.</text>
</g>
<!-- A: denied assignment picker (open sheet) -->
<g data-region="pattern-a" transform="translate(16,128)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">A · ASSIGNEE PICKER</text>
<text x="0" y="34" font-size="14" stroke="none" fill="#000">Out-of-scope agent</text>
<g transform="translate(0,48)">
<rect width="343" height="40" rx="4" />
<text x="14" y="26" font-size="12" stroke="none" fill="#666">Search agents…</text>
<text x="324" y="26" font-size="12" stroke="none" fill="#666" text-anchor="end">▾</text>
</g>
<g transform="translate(0,96)">
<rect width="343" height="288" rx="4" />
<text x="14" y="20" font-size="10" stroke="none" fill="#666">IN YOUR SCOPE</text>
<g transform="translate(0,28)">
<rect width="343" height="40" stroke="none" fill="#fafafa" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">Engineer · Ada</text>
<text x="328" y="24" font-size="10" stroke="none" fill="#666" text-anchor="end">in scope</text>
</g>
<line x1="0" y1="68" x2="343" y2="68" />
<g transform="translate(0,68)">
<rect width="343" height="40" stroke="none" fill="#fafafa" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">Designer · Iris</text>
<text x="328" y="24" font-size="10" stroke="none" fill="#666" text-anchor="end">in scope</text>
</g>
<line x1="0" y1="108" x2="343" y2="108" />
<g transform="translate(0,108)">
<rect width="343" height="40" stroke="none" fill="#fafafa" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">Researcher · Vega</text>
<g transform="translate(132,14)" stroke="#000" fill="none" stroke-width="1.2" aria-label="private agent">
<title>Private agent</title>
<rect x="0" y="5" width="11" height="9" rx="2" />
<path d="M 2.5 5 V 3 a 3 3 0 0 1 6 0 V 5" />
</g>
<text x="328" y="24" font-size="10" stroke="none" fill="#666" text-anchor="end">in scope</text>
</g>
<line x1="0" y1="148" x2="343" y2="148" />
<text x="14" y="170" font-size="10" stroke="none" fill="#666">OUTSIDE YOUR SCOPE</text>
<g transform="translate(0,180)" opacity="0.45">
<rect width="343" height="40" stroke="none" fill="#fff" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">Engineer · Brio</text>
<text x="328" y="24" font-size="10" stroke="none" fill="#666" text-anchor="end">out of scope</text>
</g>
<line x1="0" y1="220" x2="343" y2="220" />
<g transform="translate(0,220)" opacity="0.45">
<rect width="343" height="40" stroke="none" fill="#fff" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">CMO · Sienna</text>
<text x="328" y="24" font-size="10" stroke="none" fill="#666" text-anchor="end">out of scope</text>
</g>
</g>
</g>
<!-- A copy -->
<g data-region="pattern-a-copy" transform="translate(16,516)">
<rect width="343" height="116" rx="4" />
<text x="16" y="24" font-size="11" stroke="none" fill="#666">COPY</text>
<text x="16" y="44" font-size="13" stroke="none" fill="#000" font-weight="600">"This assignment is outside your scope."</text>
<text x="16" y="64" font-size="11" stroke="none" fill="#666">Your scoped grant doesn't cover this project or</text>
<text x="16" y="78" font-size="11" stroke="none" fill="#666">this agent. Ask Maya Chen to widen your grant.</text>
<text x="16" y="100" font-size="10" stroke="none" fill="#666">No constraint name, no hidden id list.</text>
</g>
<!-- B: read-only config -->
<g data-region="pattern-b" transform="translate(16,656)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">B · READ-ONLY CONFIG</text>
<text x="0" y="34" font-size="14" stroke="none" fill="#000">Peer viewing board-only config</text>
<g transform="translate(0,48)">
<rect width="343" height="32" rx="16" fill="#e6e6e6" stroke="#666" />
<text x="171" y="20" font-size="11" stroke="none" fill="#000" text-anchor="middle">View only · Board can edit this</text>
</g>
<g transform="translate(0,96)" opacity="0.7">
<text x="0" y="12" font-size="11" stroke="none" fill="#666">ADAPTER</text>
<g transform="translate(0,18)"><rect width="343" height="36" rx="4" /><text x="14" y="24" font-size="13" stroke="none" fill="#000">http</text></g>
<text x="0" y="80" font-size="11" stroke="none" fill="#666">API KEY (secret ref)</text>
<g transform="translate(0,86)"><rect width="343" height="36" rx="4" fill="#e6e6e6" stroke="#666" /><text x="14" y="24" font-size="13" stroke="none" fill="#000">anthropic_key_v3 · &lt;redacted&gt;</text></g>
</g>
<g transform="translate(0,256)">
<rect width="343" height="116" rx="4" />
<text x="16" y="24" font-size="12" stroke="none" fill="#000" font-weight="600">Why this is locked</text>
<text x="16" y="44" font-size="11" stroke="none" fill="#666">Vega's config visibility is Board-only.</text>
<text x="16" y="58" font-size="11" stroke="none" fill="#666">You're seeing the redacted peer view.</text>
<g transform="translate(16,72)">
<rect width="180" height="32" rx="4" />
<text x="90" y="20" font-size="11" stroke="none" fill="#000" text-anchor="middle">Request edit from board</text>
</g>
</g>
</g>
<!-- C: denied toast -->
<g data-region="pattern-c" transform="translate(16,1064)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">C · DENIED TOAST (after submit)</text>
<text x="0" y="34" font-size="14" stroke="none" fill="#000">Server says no after the picker rendered</text>
<g transform="translate(0,48)">
<rect width="343" height="116" rx="4" />
<text x="16" y="28" font-size="13" stroke="none" fill="#000" font-weight="600">Assignment denied — outside scope</text>
<text x="16" y="48" font-size="11" stroke="none" fill="#666">Your scoped grant doesn't cover this assignment.</text>
<text x="16" y="62" font-size="11" stroke="none" fill="#666">Ask Maya Chen if this should change.</text>
<g transform="translate(16,76)">
<rect width="68" height="24" rx="3" />
<text x="34" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">Dismiss</text>
<rect x="76" width="148" height="24" rx="3" fill="#000" stroke="none" />
<text x="150" y="16" font-size="11" stroke="none" fill="#fff" text-anchor="middle">Ask Maya Chen</text>
</g>
</g>
</g>
<!-- D: activity log entry -->
<g data-region="pattern-d" transform="translate(16,1252)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">D · NON-LEAKING ACTIVITY ENTRY</text>
<text x="0" y="34" font-size="14" stroke="none" fill="#000">Audit row shape</text>
<g transform="translate(0,48)">
<rect width="343" height="116" rx="4" />
<g font-size="12" stroke="none">
<text x="16" y="26" fill="#000">2026-05-15 10:42 · Jordan Park</text>
<text x="16" y="44" fill="#666">attempted task assignment</text>
<text x="16" y="62" fill="#000">→ denied (scope)</text>
</g>
<g transform="translate(16,72)">
<rect width="311" height="32" fill="#e6e6e6" stroke="#666" />
<text x="8" y="20" font-size="10" stroke="none" fill="#000">No target id, project id, or constraint name leaked.</text>
</g>
</g>
</g>
<!-- footer note -->
<g data-region="footer-note" transform="translate(16,1432)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">All four patterns share one rule: name the unblock owner, not the missing resource.</text>
</g>
<!-- annotations -->
<g stroke="#d33" stroke-dasharray="4 3" fill="none">
<circle cx="32" cy="320" r="9" />
<circle cx="32" cy="540" r="9" />
<circle cx="32" cy="692" r="9" />
<circle cx="32" cy="916" r="9" />
<circle cx="32" cy="1088" r="9" />
<circle cx="32" cy="1276" r="9" />
</g>
<g font-size="10" stroke="none" fill="#d33">
<text x="46" y="324">1</text>
<text x="46" y="544">2</text>
<text x="46" y="696">3</text>
<text x="46" y="920">4</text>
<text x="46" y="1092">5</text>
<text x="46" y="1280">6</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.7 KiB

@@ -1,234 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="1320" viewBox="0 0 1280 1320"
font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
<rect x="0" y="0" width="1280" height="1320" />
<!-- 1: top bar -->
<g data-region="topbar">
<rect x="0" y="0" width="1280" height="56" />
<text x="24" y="34" font-size="14" stroke="none" fill="#000">Paperclip · Acme AI Co.</text>
</g>
<text x="48" y="100" font-size="28" stroke="none" fill="#000">Denied and read-only patterns</text>
<text x="48" y="124" font-size="13" stroke="none" fill="#666">Four moments where the product must explain a no without leaking what it knows. Each pattern keeps the schema generic, names the path forward, and stays attached to a real workflow.</text>
<!-- PATTERN A: denied assignment inline -->
<g data-region="pattern-a" transform="translate(48,160)">
<text x="0" y="16" font-size="12" stroke="none" fill="#666">A · DENIED ASSIGNMENT — INLINE PICKER</text>
<text x="0" y="40" font-size="20" stroke="none" fill="#000">Out-of-scope agent in the picker</text>
<text x="0" y="60" font-size="13" stroke="none" fill="#666">Jordan opens the assignee dropdown on a task. Agents outside his tasks:assign_scope are shown but unselectable.</text>
<!-- mock task panel -->
<g transform="translate(0,88)">
<rect width="584" height="472" rx="4" />
<line x1="0" y1="60" x2="584" y2="60" />
<text x="20" y="28" font-size="14" stroke="none" fill="#000">PAP-9447 · Wireframe permissions UX site</text>
<text x="20" y="48" font-size="12" stroke="none" fill="#666">Project: Onboarding · Reporter: Maya · Priority: medium</text>
<text x="20" y="92" font-size="12" stroke="none" fill="#666">ASSIGNEE</text>
<g transform="translate(20,104)">
<rect width="544" height="40" rx="4" />
<text x="14" y="26" font-size="13" stroke="none" fill="#666">Search agents or members…</text>
<text x="524" y="26" font-size="13" stroke="none" fill="#666" text-anchor="end">▾</text>
</g>
<!-- dropdown open below -->
<g transform="translate(20,152)">
<rect width="544" height="288" rx="4" />
<text x="14" y="22" font-size="11" stroke="none" fill="#666">AGENTS — IN YOUR SCOPE</text>
<g transform="translate(0,32)">
<rect width="544" height="40" stroke="none" fill="#fafafa" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">Engineer · Ada</text>
<text x="530" y="24" font-size="11" stroke="none" fill="#666" text-anchor="end">in scope</text>
</g>
<g transform="translate(0,72)">
<line x1="0" y1="0" x2="544" y2="0" />
<rect width="544" height="40" stroke="none" fill="#fafafa" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">Designer · Iris</text>
<text x="530" y="24" font-size="11" stroke="none" fill="#666" text-anchor="end">in scope</text>
</g>
<g transform="translate(0,112)">
<line x1="0" y1="0" x2="544" y2="0" />
<rect width="544" height="40" stroke="none" fill="#fafafa" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">Researcher · Vega</text>
<!-- private lock icon — accessible label via aria-label/title -->
<g transform="translate(140,12)" stroke="#000" fill="none" stroke-width="1.4" aria-label="private agent">
<title>Private agent</title>
<rect x="0" y="6" width="14" height="11" rx="2" />
<path d="M 3 6 V 3 a 4 4 0 0 1 8 0 V 6" />
</g>
<text x="530" y="24" font-size="11" stroke="none" fill="#666" text-anchor="end">in scope</text>
</g>
<line x1="0" y1="152" x2="544" y2="152" />
<text x="14" y="174" font-size="11" stroke="none" fill="#666">OUTSIDE YOUR SCOPE — NOT SELECTABLE</text>
<g transform="translate(0,184)" opacity="0.45">
<rect width="544" height="40" stroke="none" fill="#fff" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">Engineer · Brio</text>
<text x="530" y="24" font-size="11" stroke="none" fill="#666" text-anchor="end">out of scope</text>
</g>
<g transform="translate(0,224)" opacity="0.45">
<line x1="0" y1="0" x2="544" y2="0" />
<rect width="544" height="40" stroke="none" fill="#fff" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">CMO · Sienna</text>
<text x="530" y="24" font-size="11" stroke="none" fill="#666" text-anchor="end">out of scope</text>
</g>
</g>
</g>
<!-- copy panel -->
<g transform="translate(608,88)">
<rect width="576" height="472" rx="4" />
<text x="20" y="28" font-size="12" stroke="none" fill="#666">COPY GUIDELINES</text>
<text x="20" y="56" font-size="14" stroke="none" fill="#000">Tell the operator…</text>
<g transform="translate(20,72)">
<rect width="536" height="84" rx="4" />
<text x="16" y="24" font-size="13" stroke="none" fill="#000" font-weight="600">"This assignment is outside your scope."</text>
<text x="16" y="44" font-size="12" stroke="none" fill="#666">Your scoped grant doesn't cover this project or this agent.</text>
<text x="16" y="64" font-size="12" stroke="none" fill="#666">Ask Maya Chen to widen your grant or assign the task themselves.</text>
</g>
<text x="20" y="184" font-size="14" stroke="none" fill="#000">Don't…</text>
<g transform="translate(20,200)" font-size="12" stroke="none" fill="#666">
<text x="0" y="14">— name the constraint that failed (project, agent, subtree)</text>
<text x="0" y="34">— list hidden projects, agents, or subtrees</text>
<text x="0" y="54">— surface the raw permission key in the toast</text>
</g>
<text x="20" y="288" font-size="14" stroke="none" fill="#000">Why generic</text>
<text x="20" y="308" font-size="12" stroke="none" fill="#666">Denials surface as a single calm sentence plus a named</text>
<text x="20" y="324" font-size="12" stroke="none" fill="#666">owner who can unblock — the server returns a generic</text>
<text x="20" y="340" font-size="12" stroke="none" fill="#666">scope explanation that doesn't leak hidden resources.</text>
<g transform="translate(20,372)">
<rect width="220" height="36" rx="4" />
<text x="110" y="22" font-size="12" stroke="none" fill="#000" text-anchor="middle">Request wider scope</text>
<rect x="232" width="180" height="36" rx="4" fill="#000" stroke="none" />
<text x="322" y="22" font-size="12" stroke="none" fill="#fff" text-anchor="middle">Ask Maya Chen</text>
</g>
<text x="20" y="430" font-size="11" stroke="none" fill="#666">Both actions are real affordances — the Ask Maya button opens a draft comment on the failing task.</text>
</g>
</g>
<!-- PATTERN B: read-only agent config -->
<g data-region="pattern-b" transform="translate(48,576)">
<text x="0" y="16" font-size="12" stroke="none" fill="#666">B · READ-ONLY CONFIG PANEL</text>
<text x="0" y="40" font-size="20" stroke="none" fill="#000">Non-board peer viewing a board-only config</text>
<text x="0" y="60" font-size="13" stroke="none" fill="#666">Peer agents and operators can read non-secret config, but can't edit. Board can.</text>
<g transform="translate(0,88)">
<rect width="1184" height="328" rx="4" />
<line x1="0" y1="48" x2="1184" y2="48" />
<text x="20" y="30" font-size="14" stroke="none" fill="#000">Vega · Config</text>
<g transform="translate(960,12)">
<rect width="208" height="24" rx="12" fill="#e6e6e6" stroke="#666" />
<text x="104" y="16" font-size="11" stroke="none" fill="#000" text-anchor="middle">View only · Board can edit this</text>
</g>
<g transform="translate(20,68)" opacity="0.7">
<text x="0" y="14" font-size="12" stroke="none" fill="#666">ADAPTER</text>
<g transform="translate(0,22)">
<rect width="380" height="36" rx="4" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">http</text>
<text x="370" y="24" font-size="13" stroke="none" fill="#666" text-anchor="end">▾</text>
</g>
<text x="0" y="84" font-size="12" stroke="none" fill="#666">INSTRUCTIONS PATH</text>
<g transform="translate(0,92)">
<rect width="380" height="36" rx="4" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">/agents/researcher/AGENTS.md</text>
</g>
<text x="0" y="154" font-size="12" stroke="none" fill="#666">API KEY (secret ref)</text>
<g transform="translate(0,162)">
<rect width="380" height="36" rx="4" fill="#e6e6e6" stroke="#666" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">anthropic_key_v3 · &lt;redacted&gt;</text>
</g>
<text x="0" y="224" font-size="12" stroke="none" fill="#666">LOOP BEHAVIOR</text>
<g transform="translate(0,232)">
<rect width="380" height="36" rx="4" />
<text x="14" y="24" font-size="13" stroke="none" fill="#000">heartbeat · 10m idle</text>
</g>
</g>
<g transform="translate(420,68)">
<rect width="744" height="234" rx="4" />
<text x="16" y="26" font-size="12" stroke="none" fill="#666">WHY THIS IS LOCKED</text>
<text x="16" y="50" font-size="14" stroke="none" fill="#000">Vega's config visibility is Board-only.</text>
<text x="16" y="76" font-size="12" stroke="none" fill="#666">Maya Chen (board) can edit. You're seeing the redacted view non-board peers receive.</text>
<text x="16" y="116" font-size="12" stroke="none" fill="#666">SECRET REDACTION RULE</text>
<g transform="translate(16,124)">
<rect width="712" height="48" rx="4" fill="#e6e6e6" stroke="#666" />
<text x="12" y="20" font-size="12" stroke="none" fill="#000">Secret-ref fields are always shown as &lt;redacted&gt;.</text>
<text x="12" y="36" font-size="12" stroke="none" fill="#666">Activity log, run output, and exports apply the same rule. No raw values ever surface to peers.</text>
</g>
<g transform="translate(16,188)">
<rect width="220" height="34" rx="4" />
<text x="110" y="22" font-size="12" stroke="none" fill="#000" text-anchor="middle">Request edit from board</text>
</g>
</g>
</g>
</g>
<!-- PATTERN C + D: composite -->
<g data-region="pattern-cd" transform="translate(48,968)">
<text x="0" y="16" font-size="12" stroke="none" fill="#666">C · DENIED MUTATION — TOAST · D · NON-LEAKING ACTIVITY ENTRY</text>
<text x="0" y="40" font-size="20" stroke="none" fill="#000">After-the-fact: failed assignment in flight</text>
<text x="0" y="60" font-size="13" stroke="none" fill="#666">If the scope changes between picker render and submit, the server still says no. The toast and activity entry use the same generic shape.</text>
<g transform="translate(0,88)">
<rect width="584" height="240" rx="4" />
<line x1="0" y1="48" x2="584" y2="48" />
<text x="20" y="30" font-size="14" stroke="none" fill="#000">Toast</text>
<g transform="translate(20,68)">
<rect width="544" height="100" rx="4" />
<text x="16" y="24" font-size="13" stroke="none" fill="#000" font-weight="600">Assignment denied — outside your scope</text>
<text x="16" y="44" font-size="12" stroke="none" fill="#666">Your scoped grant doesn't cover this assignment.</text>
<text x="16" y="60" font-size="12" stroke="none" fill="#666">Ask Maya Chen if this should change.</text>
<g transform="translate(16,72)">
<rect width="68" height="22" rx="3" /><text x="34" y="15" font-size="11" stroke="none" fill="#000" text-anchor="middle">Dismiss</text>
<rect x="76" width="132" height="22" rx="3" fill="#000" stroke="none" /><text x="142" y="15" font-size="11" stroke="none" fill="#fff" text-anchor="middle">Ask Maya Chen</text>
</g>
</g>
<text x="20" y="190" font-size="11" stroke="none" fill="#666">PROHIBITED COPY</text>
<text x="20" y="210" font-size="12" stroke="none" fill="#666">— "Project X is not in your allowed projects"</text>
<text x="20" y="226" font-size="12" stroke="none" fill="#666">Leaks resource shape. Stay generic; name the unblock owner instead.</text>
</g>
<g transform="translate(608,88)">
<rect width="576" height="240" rx="4" />
<line x1="0" y1="48" x2="576" y2="48" />
<text x="20" y="30" font-size="14" stroke="none" fill="#000">Activity log entry</text>
<g transform="translate(20,68)" font-size="12" stroke="none">
<text x="0" y="14" fill="#000">2026-05-15 10:42 · Jordan Park</text>
<text x="0" y="34" fill="#666">attempted task assignment</text>
<text x="0" y="54" fill="#000">→ denied (scope)</text>
</g>
<g transform="translate(20,144)">
<rect width="536" height="76" rx="4" fill="#e6e6e6" stroke="#666" />
<text x="12" y="20" font-size="11" stroke="none" fill="#000">Log shows: actor · action · outcome.</text>
<text x="12" y="36" font-size="11" stroke="none" fill="#666">Does NOT show: target agent id, project id, or which constraint family failed.</text>
<text x="12" y="56" font-size="11" stroke="none" fill="#666">Board sees full detail in audit export.</text>
</g>
</g>
</g>
<!-- annotations -->
<g stroke="#d33" stroke-dasharray="4 3" fill="none">
<circle cx="66" cy="316" r="10" />
<circle cx="66" cy="436" r="10" />
<circle cx="624" cy="312" r="10" />
<circle cx="66" cy="606" r="10" />
<circle cx="976" cy="592" r="10" />
<circle cx="66" cy="996" r="10" />
<circle cx="624" cy="996" r="10" />
</g>
<g font-size="11" stroke="none" fill="#d33">
<text x="82" y="320">1</text>
<text x="82" y="440">2</text>
<text x="640" y="316">3</text>
<text x="82" y="610">4</text>
<text x="992" y="596">5</text>
<text x="82" y="1000">6</text>
<text x="640" y="1000">7</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

@@ -1,114 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="375" height="1500" viewBox="0 0 375 1500"
font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
<rect x="0" y="0" width="375" height="1500" />
<!-- 1: top bar -->
<g data-region="topbar">
<line x1="0" y1="44" x2="375" y2="44" />
<text x="16" y="76" font-size="12" stroke="none" fill="#666">← Vega</text>
<text x="16" y="108" font-size="20" stroke="none" fill="#000">Privacy</text>
<text x="16" y="128" font-size="11" stroke="none" fill="#666">What "private" means for Vega.</text>
<text x="343" y="128" font-size="11" stroke="none" fill="#000" text-anchor="end">Learn more →</text>
</g>
<!-- 2: private toggle row -->
<g data-region="toggle-row" transform="translate(16,152)">
<rect width="343" height="124" rx="4" />
<text x="16" y="28" font-size="14" stroke="none" fill="#000" font-weight="600">Private agent</text>
<g transform="translate(279,16)">
<rect width="64" height="32" rx="16" fill="#000" />
<circle cx="48" cy="16" r="13" fill="#fff" stroke="#000" />
<text x="14" y="20" font-size="10" fill="#fff" stroke="none">ON</text>
</g>
<text x="16" y="56" font-size="12" stroke="none" fill="#666">Hide Vega's profile and config from non-board</text>
<text x="16" y="72" font-size="12" stroke="none" fill="#666">peers in the company.</text>
<text x="16" y="100" font-size="11" stroke="none" fill="#666">Affects: directory listing, config readability.</text>
</g>
<!-- 3: the rule -->
<g data-region="big-rule" transform="translate(16,308)">
<rect width="343" height="148" rx="4" />
<text x="16" y="28" font-size="11" stroke="none" fill="#666">AGENT PRIVACY DOES NOT HIDE WORK</text>
<text x="16" y="56" font-size="16" stroke="none" fill="#000" font-weight="600">A private agent marker does NOT</text>
<text x="16" y="76" font-size="16" stroke="none" fill="#000" font-weight="600">make its work objects private.</text>
<text x="16" y="102" font-size="12" stroke="none" fill="#666">Issues, comments, projects, costs, activity —</text>
<text x="16" y="118" font-size="12" stroke="none" fill="#666">all stay visible to the whole company.</text>
</g>
<!-- 4: hidden card -->
<g data-region="hidden-card" transform="translate(16,488)">
<rect width="343" height="332" rx="4" />
<text x="16" y="24" font-size="11" stroke="none" fill="#666">WHAT THIS HIDES</text>
<text x="16" y="48" font-size="14" stroke="none" fill="#000">Agent-identity surface</text>
<line x1="16" y1="60" x2="327" y2="60" />
<g transform="translate(16,80)" font-size="13" stroke="none" fill="#000">
<text x="0" y="0">· Agent profile</text>
<text x="0" y="24">· Reporting-line shown to peers</text>
<text x="0" y="48">· Adapter and config metadata</text>
<text x="0" y="72">· Tool list and secret-ref names</text>
<text x="0" y="96">· Manual invocation buttons</text>
</g>
<g transform="translate(16,200)">
<text x="0" y="0" font-size="11" stroke="none" fill="#666">VISIBLE TO</text>
<text x="0" y="18" font-size="12" stroke="none" fill="#000">Board · principals on assign/invoke lists ·</text>
<text x="0" y="34" font-size="12" stroke="none" fill="#000">the agent itself</text>
<text x="0" y="58" font-size="10" stroke="none" fill="#666">Everyone else sees no agent in pickers.</text>
</g>
</g>
<!-- 5: visible card -->
<g data-region="visible-card" transform="translate(16,852)">
<rect width="343" height="364" rx="4" />
<text x="16" y="24" font-size="11" stroke="none" fill="#666">WHAT STAYS COMPANY-VISIBLE</text>
<text x="16" y="48" font-size="14" stroke="none" fill="#000">Work objects (unchanged)</text>
<line x1="16" y1="60" x2="327" y2="60" />
<g transform="translate(16,80)" font-size="13" stroke="none" fill="#000">
<text x="0" y="0">· Issues Vega created or is assigned</text>
<text x="0" y="24">· Comments Vega wrote</text>
<text x="0" y="48">· Projects, goals, attachments</text>
<text x="0" y="72">· Cost events and budget impact</text>
<text x="0" y="96">· Activity log (agent shown as</text>
<text x="0" y="112"> "Private agent" to non-board peers)</text>
</g>
<g transform="translate(16,224)">
<rect width="311" height="108" rx="4" fill="#e6e6e6" stroke="#666" />
<text x="14" y="22" font-size="12" stroke="none" fill="#000" font-weight="600">Why the distinction</text>
<text x="14" y="42" font-size="11" stroke="none" fill="#666">Company-visible work keeps the board's</text>
<text x="14" y="56" font-size="11" stroke="none" fill="#666">accountability surface — budgets, audits,</text>
<text x="14" y="70" font-size="11" stroke="none" fill="#666">approvals — without exposing every</text>
<text x="14" y="84" font-size="11" stroke="none" fill="#666">agent's plumbing.</text>
</g>
</g>
<!-- 6: what this doesn't do -->
<g data-region="pro" transform="translate(16,1248)">
<rect width="343" height="156" rx="4" stroke-dasharray="6 4" />
<text x="16" y="24" font-size="11" stroke="none" fill="#666">WHAT THIS DOESN'T DO</text>
<text x="16" y="48" font-size="14" stroke="none" fill="#000">Project- and issue-level ACLs</text>
<text x="16" y="64" font-size="14" stroke="none" fill="#000">are a future Pro/Enterprise tier.</text>
<text x="16" y="92" font-size="12" stroke="none" fill="#666">If you need to hide specific issues or projects</text>
<text x="16" y="108" font-size="12" stroke="none" fill="#666">from some members, that's a separate slice.</text>
<text x="16" y="136" font-size="12" stroke="none" fill="#666">For now, scope at the role/grant layer.</text>
</g>
<!-- 7: footer -->
<g data-region="footer" transform="translate(16,1432)">
<text x="0" y="14" font-size="11" stroke="none" fill="#666">Toggling private is logged in the activity log.</text>
</g>
<!-- annotations -->
<g stroke="#d33" stroke-dasharray="4 3" fill="none">
<circle cx="32" cy="172" r="9" />
<circle cx="32" cy="328" r="9" />
<circle cx="32" cy="508" r="9" />
<circle cx="32" cy="872" r="9" />
<circle cx="32" cy="1268" r="9" />
</g>
<g font-size="10" stroke="none" fill="#d33">
<text x="46" y="176">1</text>
<text x="46" y="332">2</text>
<text x="46" y="512">3</text>
<text x="46" y="876">4</text>
<text x="46" y="1272">5</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

@@ -1,116 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="1080" viewBox="0 0 1280 1080"
font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
<rect x="0" y="0" width="1280" height="1080" />
<!-- top bar -->
<g data-region="topbar">
<rect x="0" y="0" width="1280" height="56" />
<text x="24" y="34" font-size="14" stroke="none" fill="#000">Paperclip · Acme AI Co.</text>
<line x1="220" y1="20" x2="220" y2="36" />
<text x="240" y="34" font-size="12" stroke="none" fill="#666">Agents › Researcher · Vega › Privacy</text>
</g>
<!-- header -->
<g data-region="header" transform="translate(48,88)">
<text x="0" y="32" font-size="28" stroke="none" fill="#000">Privacy</text>
<text x="0" y="56" font-size="14" stroke="none" fill="#666">What "private" means for Vega. Read this before you toggle.</text>
<text x="1184" y="32" font-size="13" stroke="none" fill="#000" text-anchor="end">Learn more →</text>
</g>
<!-- private toggle row -->
<g data-region="private-toggle" transform="translate(48,184)">
<rect width="1184" height="96" rx="4" />
<text x="24" y="36" font-size="16" stroke="none" fill="#000">Private agent</text>
<text x="24" y="60" font-size="13" stroke="none" fill="#666">Hide Vega's profile and config from non-board peers in the company.</text>
<text x="24" y="80" font-size="11" stroke="none" fill="#666">Affects two things: who can see this agent in directories, and who can read its config.</text>
<g transform="translate(1080,32)">
<rect width="80" height="32" rx="16" fill="#000" />
<circle cx="64" cy="16" r="13" fill="#fff" stroke="#000" />
<text x="14" y="20" font-size="11" fill="#fff" stroke="none">ON</text>
</g>
</g>
<!-- big rule banner -->
<g data-region="big-rule" transform="translate(48,312)">
<rect width="1184" height="120" rx="4" />
<text x="24" y="40" font-size="20" stroke="none" fill="#000">Agent privacy does not hide work.</text>
<text x="24" y="72" font-size="14" stroke="none" fill="#000" font-weight="600">A private marker on an agent does NOT make its work objects private.</text>
<text x="24" y="96" font-size="13" stroke="none" fill="#666">Issues, comments, projects, work products, costs, and activity stay visible to the whole company.</text>
<text x="1160" y="40" font-size="12" stroke="none" fill="#666" text-anchor="end">Learn more →</text>
</g>
<!-- two columns: hidden vs visible -->
<g data-region="hidden-col" transform="translate(48,464)">
<rect width="584" height="368" rx="4" />
<text x="24" y="36" font-size="11" stroke="none" fill="#666">WHAT THIS HIDES</text>
<text x="24" y="62" font-size="18" stroke="none" fill="#000">Agent-identity surface</text>
<line x1="24" y1="80" x2="560" y2="80" />
<g transform="translate(24,100)" font-size="14" stroke="none" fill="#000">
<text x="0" y="0">· Agent profile (name, role, capabilities)</text>
<text x="0" y="28">· Reporting-line shown to peers</text>
<text x="0" y="56">· Adapter and config metadata</text>
<text x="0" y="84">· Tool list and secret-ref names</text>
<text x="0" y="112">· Manual invocation buttons</text>
</g>
<g transform="translate(24,236)">
<text x="0" y="0" font-size="11" stroke="none" fill="#666">VISIBLE TO</text>
<text x="0" y="20" font-size="13" stroke="none" fill="#000">Board · explicit principals on assign/invoke lists · the agent itself</text>
<text x="0" y="40" font-size="11" stroke="none" fill="#666">Everyone else sees no agent, no card, no entry in pickers.</text>
</g>
<g transform="translate(24,304)">
<text x="0" y="0" font-size="11" stroke="none" fill="#666">DOES NOT HIDE</text>
<text x="0" y="20" font-size="13" stroke="none" fill="#000">Work the agent created or commented on (see right column).</text>
</g>
</g>
<g data-region="visible-col" transform="translate(648,464)">
<rect width="584" height="368" rx="4" />
<text x="24" y="36" font-size="11" stroke="none" fill="#666">WHAT STAYS COMPANY-VISIBLE</text>
<text x="24" y="62" font-size="18" stroke="none" fill="#000">Work objects (unchanged)</text>
<line x1="24" y1="80" x2="560" y2="80" />
<g transform="translate(24,100)" font-size="14" stroke="none" fill="#000">
<text x="0" y="0">· Issues Vega created or is assigned</text>
<text x="0" y="28">· Comments Vega wrote</text>
<text x="0" y="56">· Projects, goals, attachments</text>
<text x="0" y="84">· Cost events and budget impact</text>
<text x="0" y="112">· Activity log (with agent shown as "Private agent" to non-board peers)</text>
</g>
<g transform="translate(24,260)">
<rect width="536" height="80" rx="4" fill="#e6e6e6" stroke="#666" />
<text x="16" y="24" font-size="13" stroke="none" fill="#000" font-weight="600">Why this distinction matters</text>
<text x="16" y="44" font-size="12" stroke="none" fill="#666">Company-visible work keeps the board's accountability surface intact —</text>
<text x="16" y="60" font-size="12" stroke="none" fill="#666">budgets, audits, approvals — without exposing every agent's plumbing.</text>
</g>
</g>
<!-- bottom-bar future: pro tier -->
<g data-region="pro-tier" transform="translate(48,856)">
<rect width="1184" height="120" rx="4" stroke-dasharray="6 4" />
<text x="24" y="36" font-size="11" stroke="none" fill="#666">WHAT THIS DOESN'T DO</text>
<text x="24" y="64" font-size="16" stroke="none" fill="#000">Project- and issue-level ACLs are a future Pro/Enterprise tier.</text>
<text x="24" y="88" font-size="13" stroke="none" fill="#666">If you need to hide specific issues or projects from some company members, that's coming in a separate slice. </text>
<text x="24" y="104" font-size="13" stroke="none" fill="#666">For now, scope your control at the role/grant layer — not via the private marker.</text>
<text x="1160" y="64" font-size="12" stroke="none" fill="#666" text-anchor="end">Roadmap →</text>
</g>
<!-- footer link -->
<g data-region="footer-link" transform="translate(48,1000)">
<text x="0" y="14" font-size="12" stroke="none" fill="#666">Want to broaden visibility back? Switch off the toggle. The change is logged in the activity log.</text>
</g>
<!-- annotations -->
<g stroke="#d33" stroke-dasharray="4 3" fill="none">
<circle cx="66" cy="204" r="10" />
<circle cx="66" cy="332" r="10" />
<circle cx="66" cy="484" r="10" />
<circle cx="666" cy="484" r="10" />
<circle cx="66" cy="876" r="10" />
</g>
<g font-size="11" stroke="none" fill="#d33">
<text x="82" y="208">1</text>
<text x="82" y="336">2</text>
<text x="82" y="488">3</text>
<text x="682" y="488">4</text>
<text x="82" y="880">5</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

-32
View File
@@ -1,32 +0,0 @@
# PAP-9732 — UX review evidence
Plan: [PAP-9721](/PAP/issues/PAP-9721#document-plan)
Implementation: [PAP-9728](/PAP/issues/PAP-9728), [PAP-9729](/PAP/issues/PAP-9729), [PAP-9730](/PAP/issues/PAP-9730)
Reviewer: UXDesigner
## How these screenshots were produced
1. The EE plugin UI bundle from `packages/plugins/paperclip-ee-permissions/dist/ui/index.js` was copied verbatim into `ee-plugin.mjs`.
2. `sdk-mock.mjs` provides drop-in implementations of `useHostContext`, `usePluginData`, and `usePluginAction` keyed off a per-root context, so each rendered card represents the production component reacting to one deterministic state.
3. `states.mjs` defines a state per acceptance-criteria scenario (missing company, loading, unlicensed, populated, pending join, capability-denied warning, generic backend warning, fetch error, deny preview, empty).
4. `capture.mjs` boots a tiny static server, drives Chromium via the in-tree Playwright (`node_modules/.pnpm/playwright@1.58.2`), and screenshots each section twice — 1440×900 desktop and 390×844 mobile (deviceScaleFactor 2).
5. Run with `node design/PAP-9732/capture.mjs` (sandbox must be disabled for Chromium to start).
The mocked host hooks return the exact data shapes the worker emits (`EePermissionsOverview`, `EePermissionsMemberAccessData`, `EePermissionsAdvancedPolicyData`), so the screenshots show the same DOM the production runtime would render given the same payload.
## Screenshots
Stored under `./screenshots/`, named `{state-index}-{state}-{viewport}.png`.
| State | Desktop | Mobile |
| --- | --- | --- |
| Missing company | `01-missing-company-desktop.png` | `01-missing-company-mobile.png` |
| Loading | `02-loading-desktop.png` | `02-loading-mobile.png` |
| Unlicensed / activate | `03-unlicensed-desktop.png` | `03-unlicensed-mobile.png` |
| Activated — empty | `04-empty-desktop.png` | `04-empty-mobile.png` |
| Activated — populated | `05-populated-desktop.png` | `05-populated-mobile.png` |
| Activated — pending join | `06-pending-join-desktop.png` | `06-pending-join-mobile.png` |
| Capability-denied warning | `07-denied-desktop.png` | `07-denied-mobile.png` |
| Generic backend warning (stale) | `08-stale-desktop.png` | `08-stale-mobile.png` |
| Overview fetch error | `09-error-desktop.png` | `09-error-mobile.png` |
| Policy preview deny | `10-deny-desktop.png` | `10-deny-mobile.png` |
-89
View File
@@ -1,89 +0,0 @@
import { chromium } from "/Users/dotta/paperclip/.paperclip/worktrees/PAP-3631-agent-permissions-and-controls-plan/node_modules/.pnpm/playwright@1.58.2/node_modules/playwright/index.mjs";
import { fileURLToPath } from "node:url";
import path from "node:path";
import { createServer } from "node:http";
import { readFile, stat } from "node:fs/promises";
const here = path.dirname(fileURLToPath(import.meta.url));
const MIME = {
".html": "text/html; charset=utf-8",
".mjs": "application/javascript; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".png": "image/png",
".svg": "image/svg+xml",
};
const server = createServer(async (req, res) => {
const urlPath = decodeURIComponent(req.url.split("?")[0]);
const fsPath = path.join(here, urlPath === "/" ? "/index.html" : urlPath);
try {
const info = await stat(fsPath);
if (info.isDirectory()) {
res.statusCode = 404;
res.end();
return;
}
const body = await readFile(fsPath);
const ext = path.extname(fsPath).toLowerCase();
res.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
res.setHeader("Cache-Control", "no-store");
res.end(body);
} catch {
res.statusCode = 404;
res.end("not found");
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address();
const base = `http://127.0.0.1:${port}/index.html`;
const sections = [
{ name: "01-missing-company", anchor: "#state-missing-company" },
{ name: "02-loading", anchor: "#state-loading" },
{ name: "03-unlicensed", anchor: "#state-unlicensed" },
{ name: "04-empty", anchor: "#state-empty" },
{ name: "05-populated", anchor: "#state-populated" },
{ name: "06-pending-join", anchor: "#state-pending-join" },
{ name: "07-denied", anchor: "#state-denied" },
{ name: "08-stale", anchor: "#state-stale" },
{ name: "09-error", anchor: "#state-error" },
{ name: "10-deny", anchor: "#state-decision-deny" },
];
const browser = await chromium.launch();
async function captureViewport(viewport, label, isMobile) {
const ctx = await browser.newContext({
viewport,
deviceScaleFactor: 2,
isMobile,
hasTouch: isMobile,
});
const page = await ctx.newPage();
page.on("pageerror", (err) => console.error("pageerror", err.message));
page.on("console", (msg) => {
if (msg.type() === "error") console.error("[console.error]", msg.text());
});
await page.goto(base, { waitUntil: "networkidle" });
await page.waitForFunction(() => window.__readyForCapture === true, null, { timeout: 15000 });
await page.waitForTimeout(400);
for (const section of sections) {
const sectionEl = await page.$(section.anchor);
if (!sectionEl) continue;
const out = path.join(here, "screenshots", `${section.name}-${label}.png`);
await sectionEl.screenshot({ path: out });
console.log("wrote", out);
}
await ctx.close();
}
await captureViewport({ width: 1440, height: 900 }, "desktop", false);
await captureViewport({ width: 390, height: 844 }, "mobile", true);
await browser.close();
server.close();
console.log("done");
-922
View File
@@ -1,922 +0,0 @@
// src/ui/app.tsx
import { useEffect, useMemo, useState } from "react";
import {
AssigneePicker,
DataTable,
ErrorBoundary,
JsonTree,
KeyValueList,
MetricCard,
ProjectPicker,
Spinner,
StatusBadge,
useHostContext,
usePluginAction,
usePluginData
} from "@paperclipai/plugin-sdk/ui";
import { jsx, jsxs } from "react/jsx-runtime";
var HUMAN_ROLE_LABELS = {
owner: "Owner",
admin: "Admin",
operator: "Operator",
viewer: "Viewer"
};
var MEMBER_PERMISSION_KEYS = [
"agents:create",
"users:invite",
"users:manage_permissions",
"tasks:assign",
"tasks:manage_active_checkouts",
"joins:approve",
"environments:manage"
];
var PERMISSION_LABELS = {
"agents:create": "Create agents",
"users:invite": "Invite humans and agents",
"users:manage_permissions": "Manage members and grants",
"tasks:assign": "Assign tasks",
"tasks:assign_scope": "Assign scoped tasks",
"tasks:manage_active_checkouts": "Manage active task checkouts",
"joins:approve": "Approve join requests",
"environments:manage": "Manage environments"
};
var IMPLICIT_ROLE_GRANTS = {
owner: ["agents:create", "users:invite", "users:manage_permissions", "tasks:assign", "joins:approve"],
admin: ["agents:create", "users:invite", "tasks:assign", "joins:approve"],
operator: ["tasks:assign"],
viewer: []
};
var layoutStack = {
display: "grid",
gap: "16px",
padding: "24px 0"
};
var cardStyle = {
border: "1px solid var(--border, #e2e8f0)",
borderRadius: "8px",
padding: "16px",
display: "grid",
gap: "12px",
background: "var(--card, #ffffff)"
};
var subtleCardStyle = {
border: "1px solid var(--border, #e2e8f0)",
borderRadius: "8px",
padding: "12px",
display: "grid",
gap: "8px",
background: "var(--background, transparent)"
};
var mutedTextStyle = {
color: "var(--muted-foreground, #64748b)",
fontSize: "0.9rem",
lineHeight: 1.5
};
var sectionHeadingStyle = {
fontSize: "0.75rem",
letterSpacing: "0.04em",
textTransform: "uppercase",
color: "var(--muted-foreground, #64748b)"
};
var rowStyle = {
display: "flex",
alignItems: "center",
flexWrap: "wrap",
gap: "8px"
};
var gridStyle = {
display: "grid",
gap: "12px",
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))"
};
var inputStyle = {
width: "100%",
border: "1px solid var(--border, #cbd5e1)",
borderRadius: "6px",
padding: "8px 10px",
background: "var(--background, transparent)",
color: "inherit",
fontSize: "0.85rem"
};
var buttonStyle = {
padding: "7px 12px",
borderRadius: "6px",
border: "1px solid var(--border, #cbd5e1)",
background: "var(--background, transparent)",
color: "inherit",
cursor: "pointer"
};
var primaryButtonStyle = {
...buttonStyle,
background: "var(--foreground, #0f172a)",
color: "var(--background, #ffffff)"
};
var warningStyle = {
border: "1px solid var(--warning-border, #facc15)",
background: "var(--warning-muted, #fefce8)",
borderRadius: "8px",
padding: "10px 12px",
color: "var(--warning-foreground, #713f12)"
};
var fieldStyle = {
display: "grid",
gap: "6px"
};
function getErrorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
function statusForDecision(allowed) {
return allowed ? "ok" : "error";
}
function decisionLabel(allowed) {
return allowed ? "\u2713 Allowed" : "\u2715 Denied";
}
function membershipStatusVariant(status) {
if (status === "active") return "ok";
if (status === "pending") return "pending";
if (status === "suspended" || status === "archived") return "warning";
return "info";
}
function formatPermission(permissionKey) {
return PERMISSION_LABELS[permissionKey] ?? permissionKey;
}
function formatScope(scope) {
if (!scope || Object.keys(scope).length === 0) return "Any scope";
const parts = Object.entries(scope).map(([key, value]) => `${key}: ${String(value)}`);
return parts.join(", ");
}
function formatMode(value) {
if (!value) return "Not set";
return value.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
}
function formatDate(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString();
}
function getPolicySection(policy, key) {
const value = policy?.[key];
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function getPolicyString(policy, section, key, fallback) {
const value = getPolicySection(policy, section)[key];
return typeof value === "string" ? value : fallback;
}
function getPolicyBoolean(policy, section, key, fallback) {
const value = getPolicySection(policy, section)[key];
return typeof value === "boolean" ? value : fallback;
}
function RawDisclosure({ label = "Raw response", data }) {
return /* @__PURE__ */ jsxs("details", { children: [
/* @__PURE__ */ jsx("summary", { style: { ...mutedTextStyle, cursor: "pointer" }, children: label }),
/* @__PURE__ */ jsx(JsonTree, { data, defaultExpandDepth: 1 })
] });
}
function CapabilityWarning({ warnings }) {
if (warnings.length === 0) return null;
const denied = warnings.some((warning) => warning.code === "CAPABILITY_DENIED");
return /* @__PURE__ */ jsxs("div", { style: warningStyle, children: [
/* @__PURE__ */ jsx("strong", { children: denied ? "Some advanced data is unavailable." : "Some advanced data could not be loaded." }),
/* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: denied ? "The plugin is missing one or more capability grants. Install the latest version or re-activate the plugin to restore the missing surfaces." : "Existing restrictions remain enforced by core. Retry once the underlying service is reachable." }),
/* @__PURE__ */ jsx("ul", { style: { margin: "6px 0 0", paddingLeft: "18px" }, children: warnings.map((warning, index) => /* @__PURE__ */ jsxs("li", { children: [
/* @__PURE__ */ jsx("code", { children: warning.code }),
": ",
warning.message
] }, `${warning.code}-${index}`)) })
] });
}
function LoadingState({ label }) {
return /* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
/* @__PURE__ */ jsx(Spinner, { size: "sm", label }),
/* @__PURE__ */ jsx("span", { style: mutedTextStyle, children: label })
] });
}
function MissingCompanyState() {
return /* @__PURE__ */ jsx("div", { style: layoutStack, children: /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Permissions" }),
/* @__PURE__ */ jsx("strong", { children: "No active company" }),
/* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "Switch into a company to manage advanced permissions." })
] }) });
}
function UnlicensedState({
companyId,
onActivate,
activating
}) {
return /* @__PURE__ */ jsx("div", { style: layoutStack, children: /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Paperclip EE Permissions" }),
/* @__PURE__ */ jsx("strong", { children: "Advanced permissions mode is not active" }),
/* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "Members can collaborate across this company by default. Activate Paperclip EE permissions to unlock scoped grants, protected-agent controls, assignment previews, and audit filters." }),
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("button", { type: "button", style: buttonStyle, disabled: activating, onClick: onActivate, children: activating ? /* @__PURE__ */ jsx(LoadingState, { label: "Activating" }) : "Activate for this company" }) }),
/* @__PURE__ */ jsx(KeyValueList, { pairs: [{ label: "Company", value: /* @__PURE__ */ jsx("code", { children: companyId }) }] })
] }) });
}
function profileForPrincipal(member, agents) {
if (member.principalType === "agent") {
const agent = agents.find((entry) => entry.id === member.principalId);
if (agent) {
return {
label: agent.name,
secondary: [agent.title || agent.role, agent.status, agent.id].filter(Boolean).join(" / ")
};
}
return {
label: "Agent",
secondary: member.principalId
};
}
if (member.principalId.includes("@")) {
const [localPart] = member.principalId.split("@");
return {
label: localPart.split(/[._-]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || member.principalId,
secondary: member.principalId
};
}
return {
label: "Board user",
secondary: member.principalId
};
}
function MembersPanel({ companyId }) {
const query = usePluginData("memberAccess", { companyId });
const saveMemberAccess = usePluginAction("saveMemberAccess");
const [editingMemberId, setEditingMemberId] = useState(null);
const [draftRole, setDraftRole] = useState("");
const [draftStatus, setDraftStatus] = useState("active");
const [draftGrants, setDraftGrants] = useState(/* @__PURE__ */ new Set());
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
const members = query.data?.members ?? [];
const agents = query.data?.agents ?? [];
const editingMember = useMemo(
() => members.find((member) => member.id === editingMemberId) ?? null,
[members, editingMemberId]
);
const editingProfile = editingMember ? profileForPrincipal(editingMember, agents) : null;
const implicitGrantKeys = useMemo(
() => draftRole ? IMPLICIT_ROLE_GRANTS[draftRole] : [],
[draftRole]
);
useEffect(() => {
if (!editingMember) return;
const role = editingMember.membershipRole;
setDraftRole(role && role in HUMAN_ROLE_LABELS ? role : "");
setDraftStatus(
editingMember.status === "active" || editingMember.status === "pending" || editingMember.status === "suspended" ? editingMember.status : "suspended"
);
setDraftGrants(new Set(editingMember.grants.map((grant) => grant.permissionKey)));
setError(null);
}, [editingMember]);
if (query.loading && !query.data) {
return /* @__PURE__ */ jsx(LoadingState, { label: "Loading members" });
}
if (query.error) {
return /* @__PURE__ */ jsxs("div", { style: warningStyle, children: [
/* @__PURE__ */ jsx("strong", { children: "Could not load company members." }),
/* @__PURE__ */ jsx("div", { children: query.error.message })
] });
}
const pendingHumans = members.filter((member) => member.principalType === "user" && member.status === "pending");
const activeHumans = members.filter((member) => member.principalType === "user" && member.status !== "pending");
const agentMembers = members.filter((member) => member.principalType === "agent");
const closeEditor = () => {
setEditingMemberId(null);
setError(null);
};
async function save() {
if (!editingMember) return;
setBusy(true);
setError(null);
try {
await saveMemberAccess({
companyId,
memberId: editingMember.id,
membershipRole: draftRole || null,
status: draftStatus,
grants: [...draftGrants]
});
query.refresh();
setEditingMemberId(null);
} catch (err) {
setError(getErrorMessage(err));
} finally {
setBusy(false);
}
}
return /* @__PURE__ */ jsxs("div", { style: layoutStack, children: [
/* @__PURE__ */ jsx(CapabilityWarning, { warnings: query.data?.warnings ?? [] }),
/* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsxs("div", { style: { ...rowStyle, justifyContent: "space-between" }, children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Members" }),
/* @__PURE__ */ jsx("strong", { children: "Company access" })
] }),
/* @__PURE__ */ jsx("button", { type: "button", style: buttonStyle, onClick: () => query.refresh(), children: "Refresh" })
] }),
/* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "Roles, membership status, and explicit permission grants for humans and agents in this company." }),
members.length === 0 ? /* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "No company members yet." }) : null,
/* @__PURE__ */ jsx(
MembersTable,
{
members: pendingHumans,
agents,
label: "Pending humans",
emptyLabel: "No pending join requests",
onEdit: setEditingMemberId
}
),
/* @__PURE__ */ jsx(
MembersTable,
{
members: activeHumans,
agents,
label: "Humans",
emptyLabel: "No active human members",
onEdit: setEditingMemberId
}
),
/* @__PURE__ */ jsx(
MembersTable,
{
members: agentMembers,
agents,
label: "Agents",
emptyLabel: "No agent members",
onEdit: setEditingMemberId
}
)
] }),
editingMember && editingProfile ? /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Editing member" }),
/* @__PURE__ */ jsx(StatusBadge, { label: editingMember.principalType === "agent" ? "Agent" : "Human", status: "info" })
] }),
/* @__PURE__ */ jsx("strong", { children: editingProfile.label }),
/* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: editingProfile.secondary }),
/* @__PURE__ */ jsxs("div", { style: gridStyle, children: [
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { style: sectionHeadingStyle, children: "Company role" }),
/* @__PURE__ */ jsxs(
"select",
{
style: inputStyle,
value: draftRole,
onChange: (event) => setDraftRole(event.target.value),
children: [
/* @__PURE__ */ jsx("option", { value: "", children: "Unset" }),
Object.entries(HUMAN_ROLE_LABELS).map(([value, label]) => /* @__PURE__ */ jsx("option", { value, children: label }, value))
]
}
)
] }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { style: sectionHeadingStyle, children: "Membership status" }),
/* @__PURE__ */ jsxs(
"select",
{
style: inputStyle,
value: draftStatus,
onChange: (event) => setDraftStatus(event.target.value),
children: [
/* @__PURE__ */ jsx("option", { value: "active", children: "Active" }),
/* @__PURE__ */ jsx("option", { value: "pending", children: "Pending" }),
/* @__PURE__ */ jsx("option", { value: "suspended", children: "Suspended" })
]
}
)
] })
] }),
/* @__PURE__ */ jsxs("div", { style: subtleCardStyle, children: [
/* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
/* @__PURE__ */ jsx("strong", { children: "Implicit grants from role" }),
/* @__PURE__ */ jsx(StatusBadge, { label: draftRole ? HUMAN_ROLE_LABELS[draftRole] : "No role", status: draftRole ? "info" : "pending" })
] }),
/* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: draftRole ? `${HUMAN_ROLE_LABELS[draftRole]} already includes these permissions automatically.` : "No role selected, so this member has no implicit grants right now." }),
implicitGrantKeys.length > 0 ? /* @__PURE__ */ jsx("div", { style: rowStyle, children: implicitGrantKeys.map((permissionKey) => /* @__PURE__ */ jsx(StatusBadge, { label: PERMISSION_LABELS[permissionKey], status: "info" }, permissionKey)) }) : null
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Explicit grants" }),
/* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "Explicit grants persist when the role changes. Scoped assignment grants are managed in the policy editor below." }),
/* @__PURE__ */ jsx("div", { style: { ...gridStyle, marginTop: "8px" }, children: MEMBER_PERMISSION_KEYS.map((permissionKey) => {
const isImplicit = implicitGrantKeys.includes(permissionKey);
const isChecked = draftGrants.has(permissionKey);
return /* @__PURE__ */ jsxs("div", { style: subtleCardStyle, children: [
/* @__PURE__ */ jsxs("label", { style: { ...rowStyle, gap: "10px" }, children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: isChecked,
onChange: (event) => {
setDraftGrants((current) => {
const next = new Set(current);
if (event.target.checked) next.add(permissionKey);
else next.delete(permissionKey);
return next;
});
}
}
),
/* @__PURE__ */ jsxs("span", { children: [
/* @__PURE__ */ jsx("strong", { children: PERMISSION_LABELS[permissionKey] }),
/* @__PURE__ */ jsx("div", { style: { ...mutedTextStyle, fontSize: "0.72rem" }, children: /* @__PURE__ */ jsx("code", { children: permissionKey }) })
] })
] }),
isImplicit && !isChecked ? /* @__PURE__ */ jsxs("div", { style: mutedTextStyle, children: [
"Included implicitly by the ",
draftRole ? HUMAN_ROLE_LABELS[draftRole] : "selected",
" role."
] }) : null,
isChecked ? /* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "Stored explicitly for this member." }) : null
] }, permissionKey);
}) })
] }),
error ? /* @__PURE__ */ jsxs("div", { style: warningStyle, children: [
/* @__PURE__ */ jsx("strong", { children: "Could not save:" }),
" ",
error
] }) : null,
/* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
/* @__PURE__ */ jsx("button", { type: "button", style: buttonStyle, disabled: busy, onClick: closeEditor, children: "Cancel" }),
/* @__PURE__ */ jsx("button", { type: "button", style: primaryButtonStyle, disabled: busy, onClick: () => void save(), children: busy ? /* @__PURE__ */ jsx(LoadingState, { label: "Saving access" }) : "Save access" })
] })
] }) : null
] });
}
function MembersTable({
members,
agents,
label,
emptyLabel,
onEdit
}) {
const columns = [
{ key: "principal", header: "Principal", render: (_value, row) => row.principal },
{ key: "role", header: "Role", render: (_value, row) => row.role, width: "140px" },
{ key: "status", header: "Status", render: (_value, row) => row.status, width: "120px" },
{ key: "grants", header: "Grants", render: (_value, row) => row.grants },
{ key: "action", header: "", render: (_value, row) => row.action, width: "80px" }
];
const rows = members.map((member) => {
const profile = profileForPrincipal(member, agents);
return {
id: member.id,
member,
principal: /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "2px" }, children: [
/* @__PURE__ */ jsx("strong", { children: profile.label }),
/* @__PURE__ */ jsx("span", { style: mutedTextStyle, children: profile.secondary })
] }),
role: member.membershipRole ? formatMode(member.membershipRole) : "Unset",
status: /* @__PURE__ */ jsx(StatusBadge, { label: formatMode(member.status), status: membershipStatusVariant(member.status) }),
grants: `${member.grants.length} explicit grant${member.grants.length === 1 ? "" : "s"}`,
action: /* @__PURE__ */ jsx("button", { type: "button", style: buttonStyle, onClick: () => onEdit(member.id), children: "Edit" })
};
});
return /* @__PURE__ */ jsxs("div", { style: subtleCardStyle, children: [
/* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
/* @__PURE__ */ jsx("strong", { children: label }),
/* @__PURE__ */ jsx(StatusBadge, { label: `${members.length}`, status: members.length > 0 ? "info" : "pending" })
] }),
/* @__PURE__ */ jsx(
DataTable,
{
columns,
rows,
emptyMessage: emptyLabel
}
)
] });
}
function GrantRows({ grants }) {
if (grants.length === 0) {
return /* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "No assignment grants for this actor yet." });
}
return /* @__PURE__ */ jsx("div", { style: { display: "grid", gap: "8px" }, children: grants.map((grant, index) => /* @__PURE__ */ jsxs("div", { style: { ...rowStyle, justifyContent: "space-between", borderTop: index === 0 ? void 0 : "1px solid var(--border, #e2e8f0)", paddingTop: index === 0 ? 0 : "8px" }, children: [
/* @__PURE__ */ jsx(StatusBadge, { label: formatPermission(grant.permissionKey), status: "info" }),
/* @__PURE__ */ jsx("span", { style: mutedTextStyle, children: formatScope(grant.scope) })
] }, `${grant.permissionKey}-${index}`)) });
}
function CurrentAgentPolicy({ policy }) {
if (!policy?.policy) {
return /* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "No saved policy. Saving below will create one." });
}
const visibilityMode = getPolicyString(policy.policy, "agentVisibility", "mode", "discoverable");
const assignmentMode = getPolicyString(policy.policy, "assignmentPolicy", "mode", "company_default");
const requiresApproval = getPolicyBoolean(policy.policy, "protectedAgent", "requiresApproval", false);
const approvalReason = getPolicyString(policy.policy, "protectedAgent", "approvalReason", "");
return /* @__PURE__ */ jsx(
KeyValueList,
{
pairs: [
{
label: "Visibility",
value: /* @__PURE__ */ jsx("span", { title: "Controls whether this agent appears in assignment and discovery surfaces.", children: formatMode(visibilityMode) })
},
{
label: "Assignment",
value: /* @__PURE__ */ jsx("span", { title: "Controls whether assignment follows company defaults or protected-agent rules.", children: formatMode(assignmentMode) })
},
{
label: "Protected agent",
value: requiresApproval ? `Requires approval${approvalReason ? `: "${approvalReason}"` : ""}` : "No approval required"
}
]
}
);
}
function DecisionCard({ title, decision }) {
if (!decision) return null;
return /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: title }),
/* @__PURE__ */ jsx(StatusBadge, { label: decisionLabel(decision.allowed), status: statusForDecision(decision.allowed) })
] }),
/* @__PURE__ */ jsx("div", { children: decision.explanation }),
/* @__PURE__ */ jsx(
KeyValueList,
{
pairs: [
{ label: "Reason", value: formatMode(decision.reason) },
{ label: "Action", value: formatPermission(decision.action) },
{
label: "Matching grant",
value: decision.grant ? `${formatPermission(decision.grant.permissionKey)} / ${formatScope(decision.grant.scope)}` : "No matching grant"
}
]
}
),
/* @__PURE__ */ jsx(RawDisclosure, { data: decision })
] });
}
function AuthorizationAudit({
entries,
agents
}) {
const columns = [
{ key: "time", header: "Time", render: (_value, row) => row.time, width: "170px" },
{ key: "actor", header: "Actor", render: (_value, row) => row.actor },
{ key: "action", header: "Action", render: (_value, row) => row.action },
{ key: "resource", header: "Resource", render: (_value, row) => row.resource },
{ key: "decision", header: "Decision", render: (_value, row) => row.decision, width: "120px" },
{ key: "details", header: "Details", render: (_value, row) => row.details }
];
const rows = entries.map((entry) => {
const agent = entry.actorType === "agent" ? agents.find((candidate) => candidate.id === entry.actorId) : null;
const decision = entry.details?.decision === "deny" ? false : entry.details?.decision === "allow" ? true : null;
return {
id: entry.id,
time: formatDate(entry.createdAt),
actor: /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "2px" }, children: [
/* @__PURE__ */ jsx("strong", { children: agent?.name ?? formatMode(entry.actorType) }),
/* @__PURE__ */ jsx("span", { style: mutedTextStyle, children: entry.actorId })
] }),
action: formatPermission(entry.action),
resource: `${entry.entityType} / ${entry.entityId}`,
decision: decision === null ? /* @__PURE__ */ jsx(StatusBadge, { label: "Unknown", status: "pending" }) : /* @__PURE__ */ jsx(StatusBadge, { label: decisionLabel(decision), status: statusForDecision(decision) }),
details: /* @__PURE__ */ jsx(RawDisclosure, { label: "Details", data: entry.details ?? {} })
};
});
return /* @__PURE__ */ jsx(
DataTable,
{
columns,
rows,
emptyMessage: "No authorization decisions in this filter window yet. Adjust the filters above to broaden the audit search."
}
);
}
function AdvancedPolicyEditor({ companyId }) {
const saveAgentPolicy = usePluginAction("saveAgentPolicy");
const saveAssignmentGrant = usePluginAction("saveAssignmentGrant");
const [actorAgentId, setActorAgentId] = useState("");
const [targetAgentId, setTargetAgentId] = useState("");
const [projectId, setProjectId] = useState("");
const [issueId, setIssueId] = useState("");
const [auditAction, setAuditAction] = useState("");
const [auditActorType, setAuditActorType] = useState("");
const [auditEntityType, setAuditEntityType] = useState("");
const [auditEntityId, setAuditEntityId] = useState("");
const [auditDecision, setAuditDecision] = useState("");
const [visibilityMode, setVisibilityMode] = useState("discoverable");
const [assignmentMode, setAssignmentMode] = useState("company_default");
const [requiresApproval, setRequiresApproval] = useState(false);
const [approvalReason, setApprovalReason] = useState("");
const [grantMode, setGrantMode] = useState("scoped_agent");
const [busyAction, setBusyAction] = useState(null);
const [lastResult, setLastResult] = useState(null);
const params = useMemo(() => ({
companyId,
actorAgentId,
targetAgentId,
projectId,
issueId,
auditAction,
auditActorType,
auditEntityType,
auditEntityId,
auditDecision
}), [companyId, actorAgentId, targetAgentId, projectId, issueId, auditAction, auditActorType, auditEntityType, auditEntityId, auditDecision]);
const query = usePluginData("advancedPolicy", params);
const data = query.data;
useEffect(() => {
if (!data) return;
if (!actorAgentId && data.selected.actorAgentId) setActorAgentId(data.selected.actorAgentId);
if (!targetAgentId && data.selected.targetAgentId) setTargetAgentId(data.selected.targetAgentId);
if (!projectId && data.selected.projectId) setProjectId(data.selected.projectId);
if (!issueId && data.selected.issueId) setIssueId(data.selected.issueId);
}, [actorAgentId, data, issueId, projectId, targetAgentId]);
useEffect(() => {
const policy = data?.agentPolicy?.policy;
setVisibilityMode(getPolicyString(policy, "agentVisibility", "mode", "discoverable"));
setAssignmentMode(getPolicyString(policy, "assignmentPolicy", "mode", "company_default"));
setRequiresApproval(getPolicyBoolean(policy, "protectedAgent", "requiresApproval", false));
setApprovalReason(getPolicyString(policy, "protectedAgent", "approvalReason", ""));
}, [data?.agentPolicy?.resourceId, data?.agentPolicy?.updatedAt]);
const issueOptions = useMemo(
() => (data?.issues ?? []).filter((issue) => !projectId || issue.projectId === projectId),
[data?.issues, projectId]
);
async function run(label, action) {
setBusyAction(label);
try {
const result = await action();
setLastResult(result);
query.refresh();
} catch (error) {
setLastResult({ error: getErrorMessage(error) });
} finally {
setBusyAction(null);
}
}
if (query.loading && !data) {
return /* @__PURE__ */ jsx(LoadingState, { label: "Loading advanced policy editors" });
}
if (query.error) {
return /* @__PURE__ */ jsxs("div", { style: warningStyle, children: [
/* @__PURE__ */ jsx("strong", { children: "Advanced policy APIs unavailable." }),
/* @__PURE__ */ jsx("div", { children: query.error.message })
] });
}
const hasPreviewSelection = Boolean(actorAgentId && targetAgentId);
return /* @__PURE__ */ jsxs("div", { style: layoutStack, children: [
/* @__PURE__ */ jsx(CapabilityWarning, { warnings: data?.warnings ?? [] }),
/* @__PURE__ */ jsxs("div", { style: gridStyle, children: [
/* @__PURE__ */ jsx(MetricCard, { label: "Mode", value: formatMode(data?.summary?.permissionsMode ?? "unknown") }),
/* @__PURE__ */ jsx(MetricCard, { label: "Active members", value: `${data?.summary?.activeMemberCount ?? 0} / ${data?.summary?.memberCount ?? 0}` }),
/* @__PURE__ */ jsx(MetricCard, { label: "Explicit grants", value: data?.summary?.grantCount ?? 0 })
] }),
/* @__PURE__ */ jsxs("div", { style: { borderTop: "1px solid var(--border, #e2e8f0)", paddingTop: "16px", display: "grid", gap: "12px" }, children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Policy preview" }),
/* @__PURE__ */ jsx("strong", { children: "Check assignment decisions before saving policy changes" })
] }),
/* @__PURE__ */ jsxs("div", { style: gridStyle, children: [
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { style: sectionHeadingStyle, children: "Actor agent" }),
/* @__PURE__ */ jsx(
AssigneePicker,
{
companyId,
value: actorAgentId ? `agent:${actorAgentId}` : "",
includeUsers: false,
placeholder: "Select actor agent",
noneLabel: "No actor",
onChange: (_value, selection) => setActorAgentId(selection.assigneeAgentId ?? "")
}
)
] }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { style: sectionHeadingStyle, children: "Target agent" }),
/* @__PURE__ */ jsx(
AssigneePicker,
{
companyId,
value: targetAgentId ? `agent:${targetAgentId}` : "",
includeUsers: false,
placeholder: "Select target agent",
noneLabel: "No target",
onChange: (_value, selection) => setTargetAgentId(selection.assigneeAgentId ?? "")
}
)
] }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { style: sectionHeadingStyle, children: "Project scope" }),
/* @__PURE__ */ jsx(
ProjectPicker,
{
companyId,
value: projectId,
placeholder: "Any project",
noneLabel: "Any project",
onChange: setProjectId
}
)
] }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { style: sectionHeadingStyle, children: "Issue context" }),
/* @__PURE__ */ jsxs("select", { style: inputStyle, value: issueId, onChange: (event) => setIssueId(event.target.value), children: [
/* @__PURE__ */ jsx("option", { value: "", children: "No issue" }),
issueOptions.map((issue) => /* @__PURE__ */ jsx("option", { value: issue.id, children: issue.title }, issue.id))
] })
] })
] }),
!hasPreviewSelection ? /* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "Select an actor and target agent to preview a policy decision." }) : /* @__PURE__ */ jsxs("div", { style: gridStyle, children: [
/* @__PURE__ */ jsx(DecisionCard, { title: "Preview Decision", decision: data?.preview ?? null }),
/* @__PURE__ */ jsx(DecisionCard, { title: "Permission Explanation", decision: data?.explanation ?? null })
] })
] }),
/* @__PURE__ */ jsxs("div", { style: gridStyle, children: [
/* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Agent Visibility" }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { children: "Directory mode" }),
/* @__PURE__ */ jsxs("select", { style: inputStyle, value: visibilityMode, onChange: (event) => setVisibilityMode(event.target.value), children: [
/* @__PURE__ */ jsx("option", { value: "discoverable", children: "Discoverable" }),
/* @__PURE__ */ jsx("option", { value: "private", children: "Private" })
] })
] }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { children: "Assignment mode" }),
/* @__PURE__ */ jsxs("select", { style: inputStyle, value: assignmentMode, onChange: (event) => setAssignmentMode(event.target.value), children: [
/* @__PURE__ */ jsx("option", { value: "company_default", children: "Company default" }),
/* @__PURE__ */ jsx("option", { value: "protected", children: "Protected" })
] })
] }),
/* @__PURE__ */ jsxs("label", { style: rowStyle, children: [
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: requiresApproval, onChange: (event) => setRequiresApproval(event.target.checked) }),
/* @__PURE__ */ jsx("span", { children: "Require approval for protected assignment" })
] }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { children: "Approval reason" }),
/* @__PURE__ */ jsx("input", { style: inputStyle, value: approvalReason, onChange: (event) => setApprovalReason(event.target.value) })
] }),
/* @__PURE__ */ jsx(
"button",
{
type: "button",
style: primaryButtonStyle,
disabled: !targetAgentId || busyAction !== null,
onClick: () => void run("policy", () => saveAgentPolicy({
companyId,
agentId: targetAgentId,
visibilityMode,
assignmentMode,
requiresApproval,
approvalReason
})),
children: busyAction === "policy" ? /* @__PURE__ */ jsx(LoadingState, { label: "Saving agent policy" }) : "Save agent policy"
}
)
] }),
/* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Assignment Policy" }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { children: "Grant mode" }),
/* @__PURE__ */ jsxs("select", { style: inputStyle, value: grantMode, onChange: (event) => setGrantMode(event.target.value), children: [
/* @__PURE__ */ jsx("option", { value: "scoped_agent", children: "Scoped to selected target" }),
/* @__PURE__ */ jsx("option", { value: "broad", children: "Broad assignment" }),
/* @__PURE__ */ jsx("option", { value: "clear", children: "Clear assignment grants" })
] })
] }),
/* @__PURE__ */ jsx(
"button",
{
type: "button",
style: primaryButtonStyle,
disabled: !actorAgentId || busyAction !== null,
onClick: () => void run("grants", () => saveAssignmentGrant({
companyId,
actorAgentId,
targetAgentId,
projectId,
mode: grantMode
})),
children: busyAction === "grants" ? /* @__PURE__ */ jsx(LoadingState, { label: "Saving assignment grants" }) : "Save assignment grants"
}
),
/* @__PURE__ */ jsx(GrantRows, { grants: data?.actorGrants ?? [] })
] })
] }),
/* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Current Agent Policy" }),
/* @__PURE__ */ jsx(CurrentAgentPolicy, { policy: data?.agentPolicy ?? null })
] }),
/* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsxs("div", { style: { ...rowStyle, justifyContent: "space-between" }, children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Authorization Audit" }),
/* @__PURE__ */ jsx("strong", { children: "Recent authorization decisions" })
] }),
/* @__PURE__ */ jsx("button", { type: "button", style: buttonStyle, onClick: () => query.refresh(), children: "Refresh" })
] }),
/* @__PURE__ */ jsxs("div", { style: { ...gridStyle, gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))" }, children: [
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { children: "Action" }),
/* @__PURE__ */ jsx("input", { style: inputStyle, value: auditAction, onChange: (event) => setAuditAction(event.target.value) })
] }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { children: "Actor type" }),
/* @__PURE__ */ jsxs("select", { style: inputStyle, value: auditActorType, onChange: (event) => setAuditActorType(event.target.value), children: [
/* @__PURE__ */ jsx("option", { value: "", children: "Any actor" }),
/* @__PURE__ */ jsx("option", { value: "agent", children: "Agent" }),
/* @__PURE__ */ jsx("option", { value: "user", children: "User" }),
/* @__PURE__ */ jsx("option", { value: "plugin", children: "Plugin" }),
/* @__PURE__ */ jsx("option", { value: "system", children: "System" })
] })
] }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { children: "Resource type" }),
/* @__PURE__ */ jsx("input", { style: inputStyle, value: auditEntityType, onChange: (event) => setAuditEntityType(event.target.value) })
] }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { children: "Resource id" }),
/* @__PURE__ */ jsx("input", { style: inputStyle, value: auditEntityId, onChange: (event) => setAuditEntityId(event.target.value) })
] }),
/* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
/* @__PURE__ */ jsx("span", { children: "Decision" }),
/* @__PURE__ */ jsxs("select", { style: inputStyle, value: auditDecision, onChange: (event) => setAuditDecision(event.target.value), children: [
/* @__PURE__ */ jsx("option", { value: "", children: "Any decision" }),
/* @__PURE__ */ jsx("option", { value: "allow", children: "Allow" }),
/* @__PURE__ */ jsx("option", { value: "deny", children: "Deny" })
] })
] })
] }),
/* @__PURE__ */ jsx(AuthorizationAudit, { entries: data?.auditEntries ?? [], agents: data?.agents ?? [] })
] }),
lastResult ? /* @__PURE__ */ jsx(RawDisclosure, { label: "Last saved raw response", data: lastResult }) : null
] });
}
function EePermissionsCompanySettingsPageContent(_props) {
const hostContext = useHostContext();
const companyId = hostContext.companyId;
const overview = usePluginData("overview", companyId ? { companyId } : {});
const activate = usePluginAction("activateLicense");
const deactivate = usePluginAction("deactivateLicense");
const [activationBusy, setActivationBusy] = useState(false);
if (!companyId) return /* @__PURE__ */ jsx(MissingCompanyState, {});
if (overview.loading && !overview.data) {
return /* @__PURE__ */ jsx("div", { style: layoutStack, children: /* @__PURE__ */ jsx("div", { style: cardStyle, children: /* @__PURE__ */ jsx(LoadingState, { label: "Loading permissions overview" }) }) });
}
if (overview.error) {
return /* @__PURE__ */ jsx("div", { style: layoutStack, children: /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsx("div", { style: sectionHeadingStyle, children: "Permissions" }),
/* @__PURE__ */ jsx("strong", { children: "Could not load permissions" }),
/* @__PURE__ */ jsxs("div", { style: mutedTextStyle, children: [
/* @__PURE__ */ jsx("code", { children: overview.error.code }),
": ",
overview.error.message
] })
] }) });
}
const data = overview.data;
if (!data) {
return /* @__PURE__ */ jsx("div", { style: layoutStack, children: /* @__PURE__ */ jsx("div", { style: cardStyle, children: /* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "No permissions data returned yet." }) }) });
}
if (data.license.status !== "active") {
return /* @__PURE__ */ jsx(
UnlicensedState,
{
companyId,
activating: activationBusy,
onActivate: () => {
setActivationBusy(true);
void activate({ companyId }).then(() => overview.refresh()).finally(() => setActivationBusy(false));
}
}
);
}
return /* @__PURE__ */ jsxs("div", { style: layoutStack, children: [
/* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
/* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
/* @__PURE__ */ jsx("strong", { children: "Advanced policy editing is active" }),
/* @__PURE__ */ jsx(StatusBadge, { label: "Active", status: "ok" }),
/* @__PURE__ */ jsxs("details", { children: [
/* @__PURE__ */ jsx("summary", { style: { ...mutedTextStyle, cursor: "pointer" }, children: "About enforcement" }),
/* @__PURE__ */ jsx("div", { style: mutedTextStyle, children: "Policy data stays in core. If this plugin is unavailable later, existing restrictions remain server-enforced." })
] })
] }),
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
"button",
{
type: "button",
style: buttonStyle,
disabled: activationBusy,
onClick: () => {
setActivationBusy(true);
void deactivate({ companyId }).then(() => overview.refresh()).finally(() => setActivationBusy(false));
},
children: activationBusy ? /* @__PURE__ */ jsx(LoadingState, { label: "Updating" }) : "Deactivate"
}
) })
] }),
/* @__PURE__ */ jsx(MembersPanel, { companyId }),
/* @__PURE__ */ jsx(AdvancedPolicyEditor, { companyId })
] });
}
function EePermissionsCompanySettingsPage(props) {
return /* @__PURE__ */ jsx(ErrorBoundary, { fallback: /* @__PURE__ */ jsx("div", { style: warningStyle, children: "The Paperclip EE permissions UI could not render." }), children: /* @__PURE__ */ jsx(EePermissionsCompanySettingsPageContent, { ...props }) });
}
export {
EePermissionsCompanySettingsPage
};
//# sourceMappingURL=index.js.map
-242
View File
@@ -1,242 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PAP-9732 EE Permissions UX states</title>
<style>
:root {
color-scheme: light;
}
html,
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #f8fafc;
color: #0f172a;
}
.stage {
max-width: 960px;
margin: 0 auto;
padding: 24px 32px 80px;
}
.stage h1 {
font-size: 1.4rem;
margin: 32px 0 4px;
}
.stage h2 {
font-size: 0.9rem;
margin: 24px 0 4px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #475569;
}
.stage p {
margin: 0 0 12px;
color: #475569;
}
.surface {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 12px 18px;
margin-top: 12px;
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
}
.surface header {
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #64748b;
padding: 8px 0 0 0;
}
.surface__body {
padding: 0 0 8px;
}
.sdk-metric-card,
.sdk-table-row {
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
padding: 12px;
}
.sdk-muted,
.sdk-table-header {
color: #64748b;
font-size: 0.78rem;
}
.sdk-status {
display: inline-flex;
width: fit-content;
align-items: center;
border-radius: 999px;
border: 1px solid #cbd5e1;
padding: 2px 8px;
font-size: 0.75rem;
font-weight: 600;
}
.sdk-status-ok { border-color: #16a34a; color: #166534; background: #f0fdf4; }
.sdk-status-error { border-color: #dc2626; color: #991b1b; background: #fef2f2; }
.sdk-status-warning,
.sdk-status-pending { border-color: #eab308; color: #854d0e; background: #fefce8; }
.sdk-status-info { border-color: #94a3b8; color: #334155; background: #f8fafc; }
.sdk-table {
display: grid;
gap: 8px;
}
.sdk-table-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 10px;
}
.sdk-table-cell {
min-width: 0;
display: grid;
gap: 3px;
}
.sdk-kv {
display: grid;
grid-template-columns: minmax(120px, max-content) 1fr;
gap: 6px 12px;
margin: 0;
}
.sdk-kv dt {
color: #64748b;
}
.sdk-kv dd {
margin: 0;
}
.sdk-json {
display: block;
overflow: auto;
max-width: 100%;
white-space: pre-wrap;
font-size: 0.75rem;
}
.sdk-picker {
width: 100%;
text-align: left;
border: 1px solid #cbd5e1;
border-radius: 6px;
background: #fff;
color: inherit;
padding: 8px 10px;
}
.sdk-spinner {
width: 0.8rem;
height: 0.8rem;
border: 2px solid #cbd5e1;
border-top-color: #0f172a;
border-radius: 999px;
display: inline-block;
}
@media (max-width: 600px) {
.stage {
padding: 16px 12px 40px;
max-width: none;
}
.sdk-kv {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="stage">
<h1>EE Permissions plugin — state inventory</h1>
<p>Each card below renders the production-built <code>EePermissionsCompanySettingsPage</code> bundle with mocked host context for a single state.</p>
<section id="state-missing-company">
<h2>Missing company</h2>
<div class="surface"><header>State: no active company</header><div class="surface__body"><div id="root-missing-company"></div></div></div>
</section>
<section id="state-loading">
<h2>Loading</h2>
<div class="surface"><header>State: overview.loading = true</header><div class="surface__body"><div id="root-loading"></div></div></div>
</section>
<section id="state-unlicensed">
<h2>Unlicensed / activate</h2>
<div class="surface"><header>State: license.status = inactive</header><div class="surface__body"><div id="root-unlicensed"></div></div></div>
</section>
<section id="state-empty">
<h2>Activated — empty</h2>
<div class="surface"><header>State: licensed, no members yet</header><div class="surface__body"><div id="root-empty"></div></div></div>
</section>
<section id="state-populated">
<h2>Activated — populated</h2>
<div class="surface"><header>State: licensed, members + policy + audit data</header><div class="surface__body"><div id="root-populated"></div></div></div>
</section>
<section id="state-pending-join">
<h2>Activated — pending join member</h2>
<div class="surface"><header>State: one human member status pending</header><div class="surface__body"><div id="root-pending"></div></div></div>
</section>
<section id="state-denied">
<h2>Capability denied warning</h2>
<div class="surface"><header>State: warnings[code=CAPABILITY_DENIED]</header><div class="surface__body"><div id="root-denied"></div></div></div>
</section>
<section id="state-stale">
<h2>Generic worker / stale-data warning</h2>
<div class="surface"><header>State: warnings[code=BACKEND_UNAVAILABLE]</header><div class="surface__body"><div id="root-stale"></div></div></div>
</section>
<section id="state-error">
<h2>Overview fetch error</h2>
<div class="surface"><header>State: overview.error</header><div class="surface__body"><div id="root-error"></div></div></div>
</section>
<section id="state-decision-deny">
<h2>Policy preview — deny</h2>
<div class="surface"><header>State: licensed, preview.allowed = false</header><div class="surface__body"><div id="root-deny"></div></div></div>
</section>
</div>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.3.1?dev",
"react/jsx-runtime": "https://esm.sh/react@18.3.1/jsx-runtime?dev",
"react-dom": "https://esm.sh/react-dom@18.3.1?dev",
"react-dom/client": "https://esm.sh/react-dom@18.3.1/client?dev",
"@paperclipai/plugin-sdk/ui": "./sdk-mock.mjs"
}
}
</script>
<script type="module">
import { createElement } from "react";
import { createRoot } from "react-dom/client";
import { MockProvider } from "./sdk-mock.mjs";
import { EePermissionsCompanySettingsPage } from "./ee-plugin.mjs";
import { STATES } from "./states.mjs";
function mount(rootId, value) {
const node = document.getElementById(rootId);
if (!node) return;
const root = createRoot(node);
root.render(
createElement(MockProvider, { value }, createElement(EePermissionsCompanySettingsPage, {}))
);
}
mount("root-missing-company", STATES.missingCompany);
mount("root-loading", STATES.loading);
mount("root-unlicensed", STATES.unlicensed);
mount("root-empty", STATES.empty);
mount("root-populated", STATES.populated);
mount("root-pending", STATES.pending);
mount("root-denied", STATES.denied);
mount("root-stale", STATES.stale);
mount("root-error", STATES.error);
mount("root-deny", STATES.deny);
window.__readyForCapture = true;
</script>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 831 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 717 KiB

-101
View File
@@ -1,101 +0,0 @@
import { Component, createContext, createElement, useContext } from "react";
const MockContext = createContext(null);
export function MockProvider({ value, children }) {
return createElement(MockContext.Provider, { value }, children);
}
export function useHostContext() {
const ctx = useContext(MockContext);
return ctx?.host ?? { companyId: null, companyPrefix: null, theme: "light" };
}
export function usePluginData(name, params) {
const ctx = useContext(MockContext);
const entry = ctx?.data?.[name];
if (!entry) {
return { loading: false, error: null, data: null, refresh: () => {}, params };
}
return {
loading: entry.loading ?? false,
error: entry.error ?? null,
data: entry.data ?? null,
refresh: () => {},
params,
};
}
export function usePluginAction() {
return async () => ({});
}
export function MetricCard({ label, value, unit }) {
return createElement("div", { className: "sdk-metric-card" }, [
createElement("div", { key: "label", className: "sdk-muted" }, label),
createElement("strong", { key: "value" }, `${value}${unit ?? ""}`),
]);
}
export function StatusBadge({ label, status }) {
return createElement("span", { className: `sdk-status sdk-status-${status}` }, label);
}
export function DataTable({ columns, rows, emptyMessage }) {
if (!rows?.length) {
return createElement("div", { className: "sdk-muted" }, emptyMessage ?? "No rows.");
}
return createElement("div", { className: "sdk-table" }, rows.map((row) =>
createElement("div", { key: row.id, className: "sdk-table-row" }, columns.map((column) =>
createElement("div", { key: column.key, className: "sdk-table-cell" }, [
createElement("div", { key: "h", className: "sdk-table-header" }, column.header),
createElement("div", { key: "v" }, column.render ? column.render(row[column.key], row) : row[column.key]),
]),
)),
));
}
export function KeyValueList({ pairs }) {
return createElement("dl", { className: "sdk-kv" }, pairs.flatMap((pair) => [
createElement("dt", { key: `${pair.label}-k` }, pair.label),
createElement("dd", { key: `${pair.label}-v` }, pair.value),
]));
}
export function JsonTree({ data }) {
return createElement("code", { className: "sdk-json" }, JSON.stringify(data, null, 2));
}
export function Spinner({ label }) {
return createElement("span", { className: "sdk-spinner", "aria-label": label ?? "Loading" });
}
export class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error) {
return { error };
}
render() {
if (this.state.error) return this.props.fallback ?? createElement("div", null, "Plugin error");
return this.props.children;
}
}
export function AssigneePicker({ value, onChange, placeholder, noneLabel }) {
return createElement("button", {
type: "button",
className: "sdk-picker",
onClick: () => onChange(value, { assigneeAgentId: value.startsWith("agent:") ? value.slice(6) : null, assigneeUserId: null }),
}, value ? value.replace(/^agent:/, "") : (placeholder ?? noneLabel ?? "Assignee"));
}
export function ProjectPicker({ value, placeholder, noneLabel, onChange }) {
return createElement("button", {
type: "button",
className: "sdk-picker",
onClick: () => onChange(value),
}, value || placeholder || noneLabel || "Project");
}
-347
View File
@@ -1,347 +0,0 @@
const COMPANY_ID = "comp_demo_5cbe79ee";
const baseHost = { companyId: COMPANY_ID, companyPrefix: "PAP", theme: "light" };
const overviewInactive = {
loading: false,
error: null,
data: {
companyId: COMPANY_ID,
license: { status: "inactive" },
policySummary: null,
warnings: [],
},
};
const overviewActive = {
loading: false,
error: null,
data: {
companyId: COMPANY_ID,
license: {
status: "active",
activatedAt: "2026-05-14T12:00:00.000Z",
activatedByUserId: "usr_dotta",
note: null,
},
policySummary: {
companyId: COMPANY_ID,
permissionsMode: "simple",
memberCount: 4,
activeMemberCount: 3,
grantCount: 5,
advancedPolicyAvailable: false,
},
warnings: [],
},
};
const populatedMembers = {
loading: false,
error: null,
data: {
companyId: COMPANY_ID,
warnings: [],
agents: [
{ id: "agt_uxdesigner", name: "UXDesigner", role: "designer", status: "active" },
{ id: "agt_qa_agent", name: "QAAgent", role: "qa", status: "active" },
],
members: [
{
id: "mem_h1",
companyId: COMPANY_ID,
principalType: "user",
principalId: "dotta@magicmachine.co",
status: "active",
membershipRole: "owner",
grants: [
{ permissionKey: "agents:create", scope: null },
{ permissionKey: "users:invite", scope: null },
],
},
{
id: "mem_h2",
companyId: COMPANY_ID,
principalType: "user",
principalId: "rivka@magicmachine.co",
status: "active",
membershipRole: "operator",
grants: [{ permissionKey: "tasks:assign", scope: null }],
},
{
id: "mem_h3",
companyId: COMPANY_ID,
principalType: "user",
principalId: "auditor@vendor.com",
status: "suspended",
membershipRole: "viewer",
grants: [],
},
{
id: "mem_a1",
companyId: COMPANY_ID,
principalType: "agent",
principalId: "agt_uxdesigner",
status: "active",
membershipRole: null,
grants: [{ permissionKey: "tasks:assign", scope: null }],
},
{
id: "mem_a2",
companyId: COMPANY_ID,
principalType: "agent",
principalId: "agt_qa_agent",
status: "active",
membershipRole: null,
grants: [],
},
],
},
};
const pendingMember = {
loading: false,
error: null,
data: {
companyId: COMPANY_ID,
warnings: [],
agents: populatedMembers.data.agents,
members: [
{
id: "mem_h_pending",
companyId: COMPANY_ID,
principalType: "user",
principalId: "new-hire@magicmachine.co",
status: "pending",
membershipRole: "operator",
grants: [],
},
...populatedMembers.data.members.slice(0, 2),
],
},
};
const advancedActive = {
loading: false,
error: null,
data: {
summary: overviewActive.data.policySummary,
warnings: [],
agents: [
{ id: "agt_cto", name: "CTO", role: "manager", status: "active" },
{ id: "agt_uxdesigner", name: "UXDesigner", role: "designer", status: "active" },
{ id: "agt_qa_agent", name: "QAAgent", role: "qa", status: "active" },
{ id: "agt_protected_eng", name: "PaymentsEngineer", role: "engineer", status: "active" },
],
issues: [
{ id: "iss_1", title: "Roll out billing redesign", status: "in_progress", projectId: "proj_billing" },
{ id: "iss_2", title: "Audit log retention policy", status: "todo", projectId: "proj_compliance" },
{ id: "iss_3", title: "Customer churn dashboard", status: "todo", projectId: "proj_data" },
],
selected: {
actorAgentId: "agt_uxdesigner",
targetAgentId: "agt_protected_eng",
projectId: "proj_billing",
issueId: null,
},
agentPolicy: {
resourceType: "agent",
resourceId: "agt_protected_eng",
policy: {
agentVisibility: { mode: "discoverable" },
assignmentPolicy: { mode: "protected" },
protectedAgent: {
requiresApproval: true,
approvalReason: "Payments routing changes require manager approval.",
},
},
updatedAt: "2026-05-17T14:08:11.221Z",
},
actorGrants: [
{ permissionKey: "tasks:assign", scope: null },
{ permissionKey: "tasks:assign_scope", scope: { projectId: "proj_billing" } },
],
preview: {
allowed: true,
action: "tasks:assign",
reason: "scoped_grant",
explanation: "UXDesigner can assign tasks in proj_billing because they hold a scoped tasks:assign_scope grant.",
grant: { permissionKey: "tasks:assign_scope", scope: { projectId: "proj_billing" } },
},
explanation: {
allowed: true,
action: "tasks:assign",
reason: "scoped_grant",
explanation: "Explicit scoped grant takes precedence over the protected-agent default deny.",
},
auditEntries: [
{
id: "aud_1",
actorType: "agent",
actorId: "agt_uxdesigner",
action: "tasks:assign",
entityType: "issue",
entityId: "iss_1",
details: { decision: "allow", reason: "scoped_grant" },
createdAt: "2026-05-18T16:42:01.110Z",
},
{
id: "aud_2",
actorType: "agent",
actorId: "agt_qa_agent",
action: "tasks:assign",
entityType: "issue",
entityId: "iss_2",
details: { decision: "deny", reason: "protected_agent" },
createdAt: "2026-05-18T15:14:58.001Z",
},
],
},
};
const advancedDeny = {
...advancedActive,
data: {
...advancedActive.data,
preview: {
allowed: false,
action: "tasks:assign",
reason: "protected_agent",
explanation: "PaymentsEngineer is protected. Approval from the company owner is required before assignment.",
},
explanation: {
allowed: false,
action: "tasks:assign",
reason: "protected_agent",
explanation: "Protected-agent policy denies assignment without an approval workflow.",
},
},
};
const emptyMembers = {
loading: false,
error: null,
data: { companyId: COMPANY_ID, warnings: [], members: [], agents: [] },
};
const emptyAdvanced = {
loading: false,
error: null,
data: {
summary: overviewActive.data.policySummary,
warnings: [],
agents: [],
issues: [],
selected: { actorAgentId: null, targetAgentId: null, projectId: null, issueId: null },
agentPolicy: null,
actorGrants: [],
preview: null,
explanation: null,
auditEntries: [],
},
};
const deniedWarnings = [
{
code: "CAPABILITY_DENIED",
message: "access.members.read is not granted to this plugin version.",
},
];
const staleWarnings = [
{
code: "BACKEND_UNAVAILABLE",
message: "Authorization audit search returned 503. Existing policy is still enforced by core.",
},
];
export const STATES = {
missingCompany: {
host: { ...baseHost, companyId: null },
data: { overview: { loading: false, error: null, data: null } },
},
loading: {
host: baseHost,
data: { overview: { loading: true, error: null, data: null } },
},
unlicensed: {
host: baseHost,
data: { overview: overviewInactive },
},
empty: {
host: baseHost,
data: {
overview: overviewActive,
memberAccess: emptyMembers,
advancedPolicy: emptyAdvanced,
},
},
populated: {
host: baseHost,
data: {
overview: overviewActive,
memberAccess: populatedMembers,
advancedPolicy: advancedActive,
},
},
pending: {
host: baseHost,
data: {
overview: overviewActive,
memberAccess: pendingMember,
advancedPolicy: emptyAdvanced,
},
},
denied: {
host: baseHost,
data: {
overview: {
...overviewActive,
data: { ...overviewActive.data, warnings: deniedWarnings },
},
memberAccess: {
...populatedMembers,
data: { ...populatedMembers.data, warnings: deniedWarnings },
},
advancedPolicy: {
...advancedActive,
data: { ...advancedActive.data, warnings: deniedWarnings },
},
},
},
stale: {
host: baseHost,
data: {
overview: {
...overviewActive,
data: { ...overviewActive.data, warnings: staleWarnings },
},
memberAccess: {
...populatedMembers,
data: { ...populatedMembers.data, warnings: staleWarnings },
},
advancedPolicy: {
...advancedActive,
data: { ...advancedActive.data, warnings: staleWarnings },
},
},
},
error: {
host: baseHost,
data: {
overview: {
loading: false,
error: { code: "INTERNAL_ERROR", message: "Worker crashed loading overview." },
data: null,
},
},
},
deny: {
host: baseHost,
data: {
overview: overviewActive,
memberAccess: populatedMembers,
advancedPolicy: advancedDeny,
},
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB