Remove design artifacts from PR
No files matched your search
@@ -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();
|
||||
@@ -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 & 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 & 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 & 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><redacted></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 & access</div>
|
||||
<h2><span class="step-num">1.</span>Company access — humans & 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 & redaction. Secret-ref fields are always shown as <code><redacted></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>
|
||||
|
Before Width: | Height: | Size: 520 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 501 KiB |
|
Before Width: | Height: | Size: 556 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 586 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 520 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 3.6 MiB |
@@ -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 & 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 & 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=<secret · anthropic_key_v3></text>
|
||||
<text x="8" y="30" font-size="10" stroke="none" fill="#000">bearer_token=<secret · vega_http></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 & 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=<secret · anthropic_key_v3> · bearer_token=<secret · vega_http></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 → <redacted> · vega_http → <redacted></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 · <redacted></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 · <redacted></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 <redacted>.</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 |
@@ -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` |
|
||||
@@ -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");
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 355 KiB |
|
Before Width: | Height: | Size: 687 KiB |
|
Before Width: | Height: | Size: 724 KiB |
|
Before Width: | Height: | Size: 407 KiB |
|
Before Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 777 KiB |
|
Before Width: | Height: | Size: 831 KiB |
|
Before Width: | Height: | Size: 788 KiB |
|
Before Width: | Height: | Size: 839 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 647 KiB |
|
Before Width: | Height: | Size: 717 KiB |
@@ -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");
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 65 KiB |