diff --git a/design/PAP-9447/capture.mjs b/design/PAP-9447/capture.mjs
deleted file mode 100644
index bc00db7f6a8..00000000000
--- a/design/PAP-9447/capture.mjs
+++ /dev/null
@@ -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();
diff --git a/design/PAP-9447/index.html b/design/PAP-9447/index.html
deleted file mode 100644
index f0d606f45b7..00000000000
--- a/design/PAP-9447/index.html
+++ /dev/null
@@ -1,502 +0,0 @@
-
-
-
-
-
-PAP-9447 · Permissions UX wireframes
-
-
-
-
-
-
-
- PAP-9447 · Permissions UX
- Jump to a screen
-
-
-
-
- PAP-9447
- Permissions UX wireframes
-
- Overview
- · Design note & goals
-
- Screens
- 1 Company access
- 2 Scope grant editor
- 3 Agent permissions
- 4 Denied / read-only
- 5 Privacy rule
-
- Revision 2
- ✎ What changed
-
-
-
-
-
- PAP-9447 · Wireframes
- Permissions & controls — UX review
-
- Lo-fi paired desktop + mobile wireframes for the five permission surfaces in the
- approved PAP-3631 plan: company access, scoped task
- assignment, agent permission detail, denied/read-only states, and the privacy rule.
-
-
- 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.
-
-
- Revision 2 · 2026-05-18
- 5 screens · 10 SVGs
- Desktop 1280 + mobile 375
- Click any wireframe to zoom
-
-
-
-
-
- Design note
- What we are arguing
-
- Core flows
-
- Company access lives where it already does (/company/settings/access) — but the page now has a peer Agents table next to Humans , and the grants column surfaces the new scope summary chip in line.
- Scoped task assignment grants 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.
- Agent permission detail 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.
-
-
- Empty, error, and denied states
-
- Out-of-scope picker rows 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.
- Denied toast and activity log entry use a single generic shape: actor · action · outcome. They never name the failed constraint, the target id, or the hidden project.
- Read-only config 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 <redacted> placeholder text.
- Empty state 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".
-
-
- Privacy rule (load-bearing)
-
- The privacy page leads with one block: "Agent privacy does not hide work." Hidden vs. visible is a two-column list — agent-identity surface (hidden) on the left, work objects (visible) on the right.
- 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.
-
-
-
-
-
- Step 1 · Company & access
- 1. Company access — humans & agents in one place
-
- The existing /company/settings/access page grows a sibling Agents table and surfaces the new
- scope grant inline. Roles cheatsheet sits up top so operators don't need to open a doc.
-
-
-
-
01-company-access.svg 1280×1240 · desktop
-
-
-
-
mobile 375×1180
-
-
-
-
-
Annotations
-
- 1 — Visibility note. Product copy: "Roles and grants control who can act — not what they see." Help icon links to the full privacy page.
- 2 — Humans table. Same shape as today, but grants column now shows scope summary chip when tasks:assign_scope is set.
- 3 — Agents table is new. Compact columns: role, profile visibility, config visibility, who can assign-to it, who can invoke it.
- 4 — 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.
-
-
Why this changes: 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.
-
-
-
-
-
-
-
- Step 2 · Scoped assignment
- 2. Scoped task assignment grant editor
-
- Editor surfaced from the existing member-edit dialog when tasks:assign_scope is enabled. Three
- constraint families with radio toggles and chip multi-pickers; a right rail shows the live "all-of" result.
-
-
-
-
02-scope-editor.svg 1280×900 · desktop
-
-
-
-
mobile 375×1100
-
-
-
-
-
Annotations
-
- 1 — "All chosen scopes must match" explainer. Pre-empts the most common misread (OR vs AND).
- 2 — Project scope. "Any project" is the explicit unscoped state so the radio always has a non-empty selection.
- 3 — Managed-subtree scope. Pickers select a manager root — recursive descent is spelled out in copy, not left to inference.
- 4 — Target-agent allowlist. Chip multipicker; suggested order matches reporting chain.
- 5 — Live result rail. Mirrors the back-end scope JSON so the operator sees the assertion the server will check.
-
-
Why this shape: 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.
-
-
-
-
-
-
-
- Step 3 · Agent detail
- 3. Agent permissions — the five lenses
-
- A new Permissions 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.
-
-
-
-
03-agent-permissions.svg 1280×1320 · desktop
-
-
-
-
mobile 375×1700
-
-
-
-
-
Annotations
-
- 0 — 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.
- 1 — Profile visibility. Company-visible vs. Board-only — two states.
- 2 — Config visibility & redaction. Secret-ref fields are always shown as <redacted> regardless of viewer.
- 3 — Assignment / invocation. Two distinct principal lists with chip pickers.
- 4 — Tool / secret policy. Allowed tools and secret refs as labeled chips, with a redaction notice attached.
- 5 — Escalation chain — manager → grand-manager → board. No SLA copy on hops (escalation is event-driven, not time-boxed).
-
-
Why one tab: 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.
-
What we're avoiding: a settings tree with nested toggles. Every nest is a place a permission gets misread. Cards stay flat.
-
-
-
-
-
-
-
- Step 4 · Failure states
- 4. Denied and read-only — the no with no leakage
-
- Four patterns that all share one rule: name the unblock owner, never the missing resource . 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.
-
-
-
-
04-denied-readonly.svg 1280×1320 · desktop
-
-
-
-
mobile 375×1620
-
-
-
-
-
Annotations
-
- 1 — 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.
- 2 — Copy guidelines: one sentence + a named owner. Never list hidden agents or projects.
- 3 — Prohibited copy: things that leak resource shape. These are what NOT to ship.
- 4 — 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.
- 5 — Secret redaction explainer. Same rule applies to UI, activity log, and exports.
- 6 · 7 — Denied toast + activity entry use the same shape. Actor · action · outcome (scope). No target id.
-
-
Anti-patterns we're rejecting: "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.
-
-
-
-
-
-
-
- Step 5 · The privacy rule
- 5. Privacy rule — what "private" actually means
-
- 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.
-
-
-
-
05-privacy-rule.svg 1280×1080 · desktop
-
-
-
-
mobile 375×1500
-
-
-
-
-
Annotations
-
- 1 — Toggle row. Sub-copy spells out the two effects (directory hiding, config readability).
- 2 — The rule, restated as a large block — same wording as the agent-permissions tab banner.
- 3 — Hidden column. Bullets are agent-identity surface only.
- 4 — Visible column. Issues, comments, projects, costs, activity. The "why this distinction" callout names the board accountability surface as the reason.
- 5 — "What this doesn't do" strip with dashed border. Names project/issue ACLs and points at the Pro tier so operators don't conflate.
-
-
Why a dedicated page: 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.
-
-
-
-
-
-
-
- Revision 2 · 2026-05-18
- What changed in this revision
-
- Board accepted Q1, Q2, Q4 as proposed and asked for targeted revisions on the rest. This revision is the focused pass before final approval.
-
-
-
Targeted fixes
-
- Lock icon in dropdowns (Screen 4). Dense picker rows now show a compact lock icon (with accessible title/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.
- "V1" copy removed from user-facing UI. 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.
- Meta copy replaced with product copy + help affordances (Screens 1, 3, 5). 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.
- SLA copy removed from escalation (Screen 3). Manager hop now reads simply "manager"; mobile shows "CTO · Atlas · manager". Escalation is event-driven, not time-boxed in this slice.
- Denied toast names the owner action (Screen 4). 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.
-
-
-
Preserved from revision 1 (board approved)
-
- Q1 — Agents as a peer table on Company Settings → Access.
- Q2 — Mobile scope summary as a card above sticky save.
- Q4 — Out-of-scope picker rows visible as disabled / labeled, not hidden.
-
-
-
Open questions for this round
-
- Help destination. 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.
- Lock icon stroke weight. 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.
-
-
-
-
-
-
-
-
-
-
×
-
-
-
-
-
-
-
diff --git a/design/PAP-9447/screenshots/01-access-desktop.png b/design/PAP-9447/screenshots/01-access-desktop.png
deleted file mode 100644
index 50da58a7b51..00000000000
Binary files a/design/PAP-9447/screenshots/01-access-desktop.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/01-access-mobile.png b/design/PAP-9447/screenshots/01-access-mobile.png
deleted file mode 100644
index 71e0950e807..00000000000
Binary files a/design/PAP-9447/screenshots/01-access-mobile.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/01-company-access-mobile-isolated.png b/design/PAP-9447/screenshots/01-company-access-mobile-isolated.png
deleted file mode 100644
index b2bdc4159d0..00000000000
Binary files a/design/PAP-9447/screenshots/01-company-access-mobile-isolated.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/02-scope-desktop.png b/design/PAP-9447/screenshots/02-scope-desktop.png
deleted file mode 100644
index 28e37575b61..00000000000
Binary files a/design/PAP-9447/screenshots/02-scope-desktop.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/03-agent-desktop.png b/design/PAP-9447/screenshots/03-agent-desktop.png
deleted file mode 100644
index 671424ae22d..00000000000
Binary files a/design/PAP-9447/screenshots/03-agent-desktop.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/03-agent-mobile.png b/design/PAP-9447/screenshots/03-agent-mobile.png
deleted file mode 100644
index fac8f77e41d..00000000000
Binary files a/design/PAP-9447/screenshots/03-agent-mobile.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/03-agent-permissions-mobile-isolated.png b/design/PAP-9447/screenshots/03-agent-permissions-mobile-isolated.png
deleted file mode 100644
index 9c11172fd19..00000000000
Binary files a/design/PAP-9447/screenshots/03-agent-permissions-mobile-isolated.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/04-denied-desktop.png b/design/PAP-9447/screenshots/04-denied-desktop.png
deleted file mode 100644
index 64d794ddb8f..00000000000
Binary files a/design/PAP-9447/screenshots/04-denied-desktop.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/04-denied-readonly-mobile-isolated.png b/design/PAP-9447/screenshots/04-denied-readonly-mobile-isolated.png
deleted file mode 100644
index 162c50232b9..00000000000
Binary files a/design/PAP-9447/screenshots/04-denied-readonly-mobile-isolated.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/05-privacy-desktop.png b/design/PAP-9447/screenshots/05-privacy-desktop.png
deleted file mode 100644
index 38b4ecb6a0c..00000000000
Binary files a/design/PAP-9447/screenshots/05-privacy-desktop.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/05-privacy-rule-mobile-isolated.png b/design/PAP-9447/screenshots/05-privacy-rule-mobile-isolated.png
deleted file mode 100644
index ba91f2b787f..00000000000
Binary files a/design/PAP-9447/screenshots/05-privacy-rule-mobile-isolated.png and /dev/null differ
diff --git a/design/PAP-9447/screenshots/overview-desktop.png b/design/PAP-9447/screenshots/overview-desktop.png
deleted file mode 100644
index c910397c0e1..00000000000
Binary files a/design/PAP-9447/screenshots/overview-desktop.png and /dev/null differ
diff --git a/design/PAP-9447/verify-live.mjs b/design/PAP-9447/verify-live.mjs
deleted file mode 100644
index 63ece4db50f..00000000000
--- a/design/PAP-9447/verify-live.mjs
+++ /dev/null
@@ -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();
diff --git a/design/PAP-9447/wireframes/01-company-access-mobile.svg b/design/PAP-9447/wireframes/01-company-access-mobile.svg
deleted file mode 100644
index 1fd74afe745..00000000000
--- a/design/PAP-9447/wireframes/01-company-access-mobile.svg
+++ /dev/null
@@ -1,141 +0,0 @@
-
-
-
-
-
-
- Access
- Settings › Access
- ≡
-
-
-
-
-
-
- Members
-
- Roles
- Invites
- Joins · 2
-
-
-
-
-
-
- Roles control who can act — not what they see.
- Use grants to decide who can assign tasks,
- manage agents, or edit config. Issues and
- projects stay company-visible. Learn more →
-
-
-
-
- Humans · 5
- + Invite
-
-
-
-
-
-
- Maya Chen
- maya@acme.ai · you
-
- Owner
- active
-
- Implicit grants from role · no overrides
-
- Edit
-
-
-
-
-
- Jordan Park
- jordan@acme.ai
-
- Admin
- active
-
- tasks:assign_scope
-
-
- Project: Onboarding · Agents: 3 · all must match
-
-
- Edit
-
-
-
-
-
- Sam Rivera
- sam@acme.ai
-
- Operator
- pending
-
-
- Edit
-
-
-
-
- Agents · 7
- Profile, config, assign, invoke, escalation.
-
-
-
-
-
-
- CEO · Helios
- claude-opus · CEO · reports board
-
- profile: co.
- config: redact
- assign: board
-
- View details →
-
-
-
-
-
-
- Researcher · Vega
- private
- claude-haiku · Researcher · reports CTO
-
- profile: board
- config: board
- assign: CTO
-
- Issues + comments stay company-visible.
-
-
-
-
-
-
- View all 7 agents
-
-
-
-
-
-
-
-
-
-
- 1
- 2
- 3
- 4
-
-
diff --git a/design/PAP-9447/wireframes/01-company-access.svg b/design/PAP-9447/wireframes/01-company-access.svg
deleted file mode 100644
index 437acc82e13..00000000000
--- a/design/PAP-9447/wireframes/01-company-access.svg
+++ /dev/null
@@ -1,278 +0,0 @@
-
-
-
-
-
-
- Paperclip · Acme AI Co.
-
- Company › Settings › Access
-
-
- + Invite
-
-
-
-
-
-
- SETTINGS
-
- General
- Branding
- Environments
- Secrets
- Budgets
-
-
- Access
-
- Approvals
- Activity log
-
-
-
-
-
- Company access
- Manage humans, agents, roles, and scoped grants for Acme AI Co.
-
-
-
-
-
-
- Members
-
- Roles & implicit grants
- Invites
- Join requests · 2
-
-
-
-
-
-
-
- Roles and grants control who can act — not what they see.
-
- ?
-
- Use them to decide who can assign tasks, manage agents, or edit config. Issues, comments, and projects stay
- visible to the whole company so the board can audit work.
- Learn more →
-
-
-
-
- WHAT ROLES INCLUDE BY DEFAULT
-
-
-
- Owner
- Create agents · Invite · Manage members · Assign tasks · Approve joins
- Implicit. Add explicit grants only as overrides.
-
-
-
- Admin
- Create agents · Invite · Assign tasks · Approve joins
-
-
-
- Operator
- Assign tasks · Read company
-
-
-
- Viewer
- Read company. No mutations.
-
-
-
-
-
-
- Humans · 5
- Human members with company memberships, status, and explicit grants.
-
-
-
-
-
-
-
-
-
- USER
- ROLE
- STATUS
- GRANTS
- ACTION
-
-
-
-
-
-
- Maya Chen
- maya@acme.ai · you
- Owner
- active
- Implicit (Owner) · no overrides
-
-
- Edit
- Remove
-
-
-
-
-
-
- Jordan Park
- jordan@acme.ai
- Admin
- active
- tasks:assign_scope
-
-
- Project: Onboarding · Agents: 3
-
-
-
- Edit
- Remove
-
-
-
-
-
-
- Sam Rivera
- sam@acme.ai
- Operator
- pending
- No explicit grants
-
-
- Edit
- Remove
-
-
-
-
-
- Priya Shah
- priya@acme.ai
- Viewer
- suspended
- No explicit grants
-
-
- Edit
- Remove
-
-
-
-
-
-
- Agents · 7
- Profile visibility, config redaction, who can assign, who can invoke, and escalation chain.
- Manage agent permissions →
-
-
-
-
-
-
-
-
- AGENT
- ROLE / REPORTS TO
- PROFILE
- CONFIG
- ASSIGN-TO
- INVOKE
- ACTION
-
-
-
-
-
-
- CEO · Helios
- claude-opus · process adapter
- CEO · board
- Company
- Redacted
- Board, owners
- Board only
-
- Details
-
-
-
-
-
- CTO · Atlas
- claude-sonnet · process adapter
- CTO · reports CEO
- Company
- Redacted
- CEO, owners
- Manager+
-
- Details
-
-
-
-
-
- Researcher · Vega
- private
- claude-haiku · http adapter
- Researcher · CTO
- Board-only
- Board-only
- CTO, owners
- CTO only
-
- Details
-
-
-
-
-
- Engineer · Ada
- claude-sonnet · process adapter
- Engineer · CTO
- Company
- Redacted
- CTO, scoped
- Manager+
-
- Details
-
-
-
- 3 more agents — view all →
-
-
-
-
-
-
-
-
-
-
-
- 1
- 2
- 3
- 4
-
-
diff --git a/design/PAP-9447/wireframes/02-scope-editor-mobile.svg b/design/PAP-9447/wireframes/02-scope-editor-mobile.svg
deleted file mode 100644
index 356c3e2df81..00000000000
--- a/design/PAP-9447/wireframes/02-scope-editor-mobile.svg
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-
-
-
-
- ← Edit member
- Scoped task assignment
- Jordan Park · tasks:assign_scope
-
-
-
-
-
- All chosen scopes must match
- Leave any section "Any" to skip that family.
- At least one family must be set, or this grant
- becomes unscoped tasks:assign.
-
-
-
-
- 1 · PROJECT SCOPE
- Restrict to specific projects
-
- Any project
- Selected (2)
-
-
-
- Onboarding ×
- Growth Loops ×
- + Add
-
-
-
-
-
- 2 · MANAGED-SUBTREE SCOPE
- Restrict to everyone under a manager
-
- Any subtree
- Selected subtree roots
-
- Selecting a manager grants assignment to that
- manager and all their reports, recursively.
-
-
-
-
- 3 · TARGET-AGENT ALLOWLIST
- Restrict to a specific list of agents
-
- Any agent
- Selected (3)
-
-
-
- Engineer · Ada ×
- Designer · Iris ×
- QA · Cypress ×
- + Add
-
-
-
-
-
-
- RESULT
- Assignment must satisfy ALL:
-
- Project: Onboarding, Growth Loops
- Subtree: any
- Agent: Ada, Iris, Cypress
-
- Denials return 403 with a generic message — no leakage of hidden resources.
-
-
-
-
-
-
-
- Cancel
- Save grant
-
-
-
-
-
-
-
-
-
-
-
-
- 1
- 2
- 3
- 4
- 5
-
-
diff --git a/design/PAP-9447/wireframes/02-scope-editor.svg b/design/PAP-9447/wireframes/02-scope-editor.svg
deleted file mode 100644
index 91f400aa29f..00000000000
--- a/design/PAP-9447/wireframes/02-scope-editor.svg
+++ /dev/null
@@ -1,145 +0,0 @@
-
-
-
-
-
- SETTINGS › ACCESS › EDIT MEMBER: JORDAN PARK
-
-
- underlying access page (dimmed)
-
-
-
-
-
-
-
-
-
- Edit grant · Scoped task assignment
- Jordan Park · tasks:assign_scope · principal_permission_grants.scope
- ×
-
-
-
-
-
- All chosen scopes must match for assignment to succeed.
- Leave a section as "Any" to skip that family. At least one constraint family must be set, otherwise this
- becomes an unscoped tasks:assign grant — switch the row above to remove the scope.
-
-
-
-
- 1 · PROJECT SCOPE
- Restrict to specific projects
-
- Any project (no project restriction)
- Selected projects (1 required)
-
-
-
-
- Onboarding ×
- Growth Loops ×
-
- + Add project
-
-
-
-
-
- 2 · MANAGED-SUBTREE SCOPE
- Restrict to everyone under a manager
-
- Any subtree (no subtree restriction)
- Selected subtree roots
-
- Selecting a manager grants assignment to that manager and all reports below them, recursively.
-
-
-
-
- 3 · TARGET-AGENT ALLOWLIST
- Restrict to a specific list of agents
-
- Any agent (no agent restriction)
- Selected agents (3)
-
-
-
-
- Engineer · Ada ×
- Designer · Iris ×
- QA · Cypress ×
-
- + Add agent
-
-
-
-
-
-
-
-
- RESULT SUMMARY
- When Jordan tries to
- assign a task…
- it must satisfy ALL of:
-
-
- PROJECT
- Onboarding
- Growth Loops
-
-
-
- SUBTREE
- — any —
-
-
-
- AGENT
- Engineer · Ada
- Designer · Iris
- QA · Cypress
-
-
-
-
- DENIALS
- Return 403 with a generic
- scope message — no leak
- of hidden projects/agents.
-
-
-
-
-
-
-
-
-
- Cancel
- Save grant
-
- Saved scope is shown in the member grants column.
-
-
-
-
-
-
-
-
-
-
-
- 2
- 3
- 4
- 5
- 1
-
-
diff --git a/design/PAP-9447/wireframes/03-agent-permissions-mobile.svg b/design/PAP-9447/wireframes/03-agent-permissions-mobile.svg
deleted file mode 100644
index 1923065ba6e..00000000000
--- a/design/PAP-9447/wireframes/03-agent-permissions-mobile.svg
+++ /dev/null
@@ -1,181 +0,0 @@
-
-
-
-
-
-
- ← Agents
-
-
-
-
-
- Researcher · Vega
- private
- claude-haiku · reports CTO Atlas
-
-
-
-
-
-
- Overview
- Config
- Permissions
-
- Budget
- Runs ›
-
-
-
-
-
-
- Private agent marker
-
-
-
- ON
-
- Agent privacy does not hide work.
- Hides Vega's profile and config from
- non-board peers.
- Issues, comments, and work products
- stay company-visible.
- Learn more →
-
-
-
-
-
- 1 · PROFILE VISIBILITY
- Who can see this agent's identity
-
- Company-visible
- Board-only
-
-
-
- Shown to: Board · Maya
- Chen · Jordan Park · CTO Atlas
-
-
-
-
-
-
- 2 · CONFIG VISIBILITY & REDACTION
- Who reads the runtime config
- Secret fields always redacted in UI.
-
- Company
- Board-only
-
-
-
- api_key=<secret · anthropic_key_v3>
- bearer_token=<secret · vega_http>
-
-
-
-
-
-
- 3 · ASSIGNMENT / INVOCATION
- Who can hand work to Vega
-
- CAN ASSIGN TASKS
-
- Board
- CTO · Atlas
-
-
-
- CAN INVOKE (manual heartbeat)
-
- CTO · Atlas
-
-
-
-
-
-
-
- 4 · TOOL / SECRET POLICY
- What Vega can run and read
-
- ALLOWED TOOLS
-
- web_fetch
- web_search
- file_read (sandbox)
-
-
-
- SECRET REFS
-
- anthropic_key_v3
- vega_http
-
-
-
-
- Tools requiring missing scopes are
- filtered from the picker. Logs redact secrets.
-
-
-
-
-
-
- 5 · ESCALATION AUTHORITY
- Where refusals and blockers route
-
-
-
- Researcher · Vega
-
-
-
- ↓ refusal / blocked
-
-
-
- CTO · Atlas · manager
-
-
-
- ↓ if unresolved
-
-
-
- CEO · Helios → Board
-
- Each hop writes to activity_log.
-
-
-
-
-
- View recent permission changes →
-
-
-
-
-
-
-
-
-
-
-
-
- 0
- 1
- 2
- 3
- 4
- 5
-
-
diff --git a/design/PAP-9447/wireframes/03-agent-permissions.svg b/design/PAP-9447/wireframes/03-agent-permissions.svg
deleted file mode 100644
index 51220dd14db..00000000000
--- a/design/PAP-9447/wireframes/03-agent-permissions.svg
+++ /dev/null
@@ -1,215 +0,0 @@
-
-
-
-
-
-
- Paperclip · Acme AI Co.
-
- Agents › Researcher · Vega › Permissions
-
-
-
-
-
- Researcher · Vega
- private
- active
- claude-haiku · http adapter · reports CTO · Atlas
-
-
-
-
-
-
- Overview
- Activity
- Tasks
- Config
- Permissions
-
- Budget
- Runs
-
- Last changed 2 days ago by Maya Chen
-
-
-
-
-
-
- Private agent marker
-
- ?
-
- Agent privacy does not hide work.
- Hides Vega's profile and config from non-board peers. Issues, comments, and work products stay company-visible. Learn more →
-
-
-
- ON
-
- private · since 2026-04-22 · changed by Maya Chen
-
-
-
-
-
-
- 1 · PROFILE VISIBILITY
- Who can see this agent's identity
- Name, role, capabilities, reporting line.
-
- Company-visible · everyone in the company
- Board-only · hidden from peer agents and humans
-
-
-
- Profile shown to: Board · Maya Chen · Jordan Park · CTO Atlas
-
-
-
-
-
-
- 2 · CONFIG VISIBILITY & REDACTION
- Who can read the runtime config
- Adapter, loop behavior, instructions path. Secret-backed fields are always redacted in the UI.
-
- Company · board reads full, peers read non-secret
- Board-only · peers see nothing
-
-
-
- api_key=<secret · anthropic_key_v3> · bearer_token=<secret · vega_http>
-
-
-
-
-
-
- 3 · ASSIGNMENT / INVOCATION
- Who can hand work to Vega
- Assignment grants execution authority. Invocation = manual heartbeat trigger.
-
-
- CAN ASSIGN TASKS
-
- Board
- CTO · Atlas
- + Add principal
-
-
-
- CAN INVOKE (manual heartbeat)
-
- CTO · Atlas
- + Add principal
-
-
-
-
-
-
-
- 4 · TOOL / SECRET POLICY
- What Vega can run and read
- Secret refs are looked up at runtime. Logs and activity always redact secret values.
-
-
-
- ALLOWED TOOLS
-
- web_fetch
- web_search
- file_read (sandbox)
- + Add tool
-
-
-
- SECRET REFS
-
- anthropic_key_v3
- vega_http
- + Add ref
-
-
-
-
- Tools requiring scopes Vega lacks are filtered from the picker — operators don't see denials they can't act on.
- Log redaction: anthropic_key_v3 → <redacted> · vega_http → <redacted>
-
-
-
-
-
-
-
- 5 · ESCALATION AUTHORITY
- Where refusals and blockers route
- When Vega marks a task blocked or refuses, the assigned owner is notified. If unresolved, escalation walks the chain.
-
-
-
-
- Researcher · Vega
- refusal / blocked
-
-
- →
-
-
- CTO · Atlas
- manager
-
-
- →
-
-
- CEO · Helios
- grand-manager
-
-
- →
-
-
- Board
- final · approval
-
- Audit trail: every step writes to activity_log.
-
-
-
-
-
-
- RECENT PERMISSION CHANGES
-
-
- 2026-05-13 · Maya Chen toggled Private agent marker → ON
- 2026-05-12 · Jordan Park added secret ref vega_http
- 2026-05-10 · CTO Atlas added tool web_search
- 2026-05-04 · Maya Chen set config visibility → board-only
- View full activity →
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
- 1
- 2
- 3
- 4
- 5
-
-
diff --git a/design/PAP-9447/wireframes/04-denied-readonly-mobile.svg b/design/PAP-9447/wireframes/04-denied-readonly-mobile.svg
deleted file mode 100644
index d5a48ea2169..00000000000
--- a/design/PAP-9447/wireframes/04-denied-readonly-mobile.svg
+++ /dev/null
@@ -1,156 +0,0 @@
-
-
-
-
-
-
- Denied / read-only
- Four denial patterns at mobile width.
-
-
-
-
- A · ASSIGNEE PICKER
- Out-of-scope agent
-
-
- Search agents…
- ▾
-
-
-
- IN YOUR SCOPE
-
-
- Engineer · Ada
- in scope
-
-
-
-
- Designer · Iris
- in scope
-
-
-
-
- Researcher · Vega
-
- Private agent
-
-
-
- in scope
-
-
- OUTSIDE YOUR SCOPE
-
-
- Engineer · Brio
- out of scope
-
-
-
-
- CMO · Sienna
- out of scope
-
-
-
-
-
-
-
- COPY
- "This assignment is outside your scope."
- Your scoped grant doesn't cover this project or
- this agent. Ask Maya Chen to widen your grant.
- No constraint name, no hidden id list.
-
-
-
-
- B · READ-ONLY CONFIG
- Peer viewing board-only config
-
-
- View only · Board can edit this
-
-
- ADAPTER
- http
- API KEY (secret ref)
- anthropic_key_v3 · <redacted>
-
-
-
- Why this is locked
- Vega's config visibility is Board-only.
- You're seeing the redacted peer view.
-
-
- Request edit from board
-
-
-
-
-
-
- C · DENIED TOAST (after submit)
- Server says no after the picker rendered
-
-
- Assignment denied — outside scope
- Your scoped grant doesn't cover this assignment.
- Ask Maya Chen if this should change.
-
-
- Dismiss
-
- Ask Maya Chen
-
-
-
-
-
-
- D · NON-LEAKING ACTIVITY ENTRY
- Audit row shape
-
-
-
- 2026-05-15 10:42 · Jordan Park
- attempted task assignment
- → denied (scope)
-
-
-
- No target id, project id, or constraint name leaked.
-
-
-
-
-
-
- All four patterns share one rule: name the unblock owner, not the missing resource.
-
-
-
-
-
-
-
-
-
-
-
-
- 1
- 2
- 3
- 4
- 5
- 6
-
-
diff --git a/design/PAP-9447/wireframes/04-denied-readonly.svg b/design/PAP-9447/wireframes/04-denied-readonly.svg
deleted file mode 100644
index 85636788316..00000000000
--- a/design/PAP-9447/wireframes/04-denied-readonly.svg
+++ /dev/null
@@ -1,234 +0,0 @@
-
-
-
-
-
-
- Paperclip · Acme AI Co.
-
-
- Denied and read-only patterns
- 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.
-
-
-
- A · DENIED ASSIGNMENT — INLINE PICKER
- Out-of-scope agent in the picker
- Jordan opens the assignee dropdown on a task. Agents outside his tasks:assign_scope are shown but unselectable.
-
-
-
-
-
- PAP-9447 · Wireframe permissions UX site
- Project: Onboarding · Reporter: Maya · Priority: medium
-
- ASSIGNEE
-
-
- Search agents or members…
- ▾
-
-
-
-
-
- AGENTS — IN YOUR SCOPE
-
-
- Engineer · Ada
- in scope
-
-
-
-
- Designer · Iris
- in scope
-
-
-
-
- Researcher · Vega
-
-
- Private agent
-
-
-
- in scope
-
-
- OUTSIDE YOUR SCOPE — NOT SELECTABLE
-
-
- Engineer · Brio
- out of scope
-
-
-
-
- CMO · Sienna
- out of scope
-
-
-
-
-
-
-
- COPY GUIDELINES
- Tell the operator…
-
-
-
- "This assignment is outside your scope."
- Your scoped grant doesn't cover this project or this agent.
- Ask Maya Chen to widen your grant or assign the task themselves.
-
-
- Don't…
-
- — name the constraint that failed (project, agent, subtree)
- — list hidden projects, agents, or subtrees
- — surface the raw permission key in the toast
-
-
- Why generic
- Denials surface as a single calm sentence plus a named
- owner who can unblock — the server returns a generic
- scope explanation that doesn't leak hidden resources.
-
-
-
- Request wider scope
-
- Ask Maya Chen
-
- Both actions are real affordances — the Ask Maya button opens a draft comment on the failing task.
-
-
-
-
-
- B · READ-ONLY CONFIG PANEL
- Non-board peer viewing a board-only config
- Peer agents and operators can read non-secret config, but can't edit. Board can.
-
-
-
-
- Vega · Config
-
-
- View only · Board can edit this
-
-
-
- ADAPTER
-
-
- http
- ▾
-
- INSTRUCTIONS PATH
-
-
- /agents/researcher/AGENTS.md
-
- API KEY (secret ref)
-
-
- anthropic_key_v3 · <redacted>
-
- LOOP BEHAVIOR
-
-
- heartbeat · 10m idle
-
-
-
-
-
- WHY THIS IS LOCKED
- Vega's config visibility is Board-only.
- Maya Chen (board) can edit. You're seeing the redacted view non-board peers receive.
-
- SECRET REDACTION RULE
-
-
- Secret-ref fields are always shown as <redacted>.
- Activity log, run output, and exports apply the same rule. No raw values ever surface to peers.
-
-
-
-
- Request edit from board
-
-
-
-
-
-
-
- C · DENIED MUTATION — TOAST · D · NON-LEAKING ACTIVITY ENTRY
- After-the-fact: failed assignment in flight
- If the scope changes between picker render and submit, the server still says no. The toast and activity entry use the same generic shape.
-
-
-
-
- Toast
-
-
- Assignment denied — outside your scope
- Your scoped grant doesn't cover this assignment.
- Ask Maya Chen if this should change.
-
- Dismiss
- Ask Maya Chen
-
-
- PROHIBITED COPY
- — "Project X is not in your allowed projects"
- Leaks resource shape. Stay generic; name the unblock owner instead.
-
-
-
-
-
- Activity log entry
-
- 2026-05-15 10:42 · Jordan Park
- attempted task assignment
- → denied (scope)
-
-
-
- Log shows: actor · action · outcome.
- Does NOT show: target agent id, project id, or which constraint family failed.
- Board sees full detail in audit export.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1
- 2
- 3
- 4
- 5
- 6
- 7
-
-
diff --git a/design/PAP-9447/wireframes/05-privacy-rule-mobile.svg b/design/PAP-9447/wireframes/05-privacy-rule-mobile.svg
deleted file mode 100644
index ccea6575ab3..00000000000
--- a/design/PAP-9447/wireframes/05-privacy-rule-mobile.svg
+++ /dev/null
@@ -1,114 +0,0 @@
-
-
-
-
-
-
- ← Vega
- Privacy
- What "private" means for Vega.
- Learn more →
-
-
-
-
-
- Private agent
-
-
-
- ON
-
- Hide Vega's profile and config from non-board
- peers in the company.
- Affects: directory listing, config readability.
-
-
-
-
-
- AGENT PRIVACY DOES NOT HIDE WORK
- A private agent marker does NOT
- make its work objects private.
- Issues, comments, projects, costs, activity —
- all stay visible to the whole company.
-
-
-
-
-
- WHAT THIS HIDES
- Agent-identity surface
-
-
- · Agent profile
- · Reporting-line shown to peers
- · Adapter and config metadata
- · Tool list and secret-ref names
- · Manual invocation buttons
-
-
- VISIBLE TO
- Board · principals on assign/invoke lists ·
- the agent itself
- Everyone else sees no agent in pickers.
-
-
-
-
-
-
- WHAT STAYS COMPANY-VISIBLE
- Work objects (unchanged)
-
-
- · Issues Vega created or is assigned
- · Comments Vega wrote
- · Projects, goals, attachments
- · Cost events and budget impact
- · Activity log (agent shown as
- "Private agent" to non-board peers)
-
-
-
- Why the distinction
- Company-visible work keeps the board's
- accountability surface — budgets, audits,
- approvals — without exposing every
- agent's plumbing.
-
-
-
-
-
-
- WHAT THIS DOESN'T DO
- Project- and issue-level ACLs
- are a future Pro/Enterprise tier.
- If you need to hide specific issues or projects
- from some members, that's a separate slice.
- For now, scope at the role/grant layer.
-
-
-
-
- Toggling private is logged in the activity log.
-
-
-
-
-
-
-
-
-
-
-
- 1
- 2
- 3
- 4
- 5
-
-
diff --git a/design/PAP-9447/wireframes/05-privacy-rule.svg b/design/PAP-9447/wireframes/05-privacy-rule.svg
deleted file mode 100644
index 89f5eb8e9d3..00000000000
--- a/design/PAP-9447/wireframes/05-privacy-rule.svg
+++ /dev/null
@@ -1,116 +0,0 @@
-
-
-
-
-
-
- Paperclip · Acme AI Co.
-
- Agents › Researcher · Vega › Privacy
-
-
-
-
- Privacy
- What "private" means for Vega. Read this before you toggle.
- Learn more →
-
-
-
-
-
- Private agent
- Hide Vega's profile and config from non-board peers in the company.
- Affects two things: who can see this agent in directories, and who can read its config.
-
-
-
- ON
-
-
-
-
-
-
- Agent privacy does not hide work.
- A private marker on an agent does NOT make its work objects private.
- Issues, comments, projects, work products, costs, and activity stay visible to the whole company.
- Learn more →
-
-
-
-
-
- WHAT THIS HIDES
- Agent-identity surface
-
-
- · Agent profile (name, role, capabilities)
- · Reporting-line shown to peers
- · Adapter and config metadata
- · Tool list and secret-ref names
- · Manual invocation buttons
-
-
- VISIBLE TO
- Board · explicit principals on assign/invoke lists · the agent itself
- Everyone else sees no agent, no card, no entry in pickers.
-
-
- DOES NOT HIDE
- Work the agent created or commented on (see right column).
-
-
-
-
-
- WHAT STAYS COMPANY-VISIBLE
- Work objects (unchanged)
-
-
- · Issues Vega created or is assigned
- · Comments Vega wrote
- · Projects, goals, attachments
- · Cost events and budget impact
- · Activity log (with agent shown as "Private agent" to non-board peers)
-
-
-
- Why this distinction matters
- Company-visible work keeps the board's accountability surface intact —
- budgets, audits, approvals — without exposing every agent's plumbing.
-
-
-
-
-
-
- WHAT THIS DOESN'T DO
- Project- and issue-level ACLs are a future Pro/Enterprise tier.
- If you need to hide specific issues or projects from some company members, that's coming in a separate slice.
- For now, scope your control at the role/grant layer — not via the private marker.
- Roadmap →
-
-
-
-
- Want to broaden visibility back? Switch off the toggle. The change is logged in the activity log.
-
-
-
-
-
-
-
-
-
-
-
- 1
- 2
- 3
- 4
- 5
-
-
diff --git a/design/PAP-9732/README.md b/design/PAP-9732/README.md
deleted file mode 100644
index 2715367420c..00000000000
--- a/design/PAP-9732/README.md
+++ /dev/null
@@ -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` |
diff --git a/design/PAP-9732/capture.mjs b/design/PAP-9732/capture.mjs
deleted file mode 100644
index d5d3c97dc0a..00000000000
--- a/design/PAP-9732/capture.mjs
+++ /dev/null
@@ -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");
diff --git a/design/PAP-9732/ee-plugin.mjs b/design/PAP-9732/ee-plugin.mjs
deleted file mode 100644
index 0a9216a3623..00000000000
--- a/design/PAP-9732/ee-plugin.mjs
+++ /dev/null
@@ -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
diff --git a/design/PAP-9732/index.html b/design/PAP-9732/index.html
deleted file mode 100644
index 63748a8bf86..00000000000
--- a/design/PAP-9732/index.html
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
-
- PAP-9732 EE Permissions UX states
-
-
-
-
-
EE Permissions plugin — state inventory
-
Each card below renders the production-built EePermissionsCompanySettingsPage bundle with mocked host context for a single state.
-
-
-
-
- Loading
- State: overview.loading = true
-
-
-
- Unlicensed / activate
- State: license.status = inactive
-
-
-
- Activated — empty
- State: licensed, no members yet
-
-
-
- Activated — populated
- State: licensed, members + policy + audit data
-
-
-
- Activated — pending join member
- State: one human member status pending
-
-
-
- Capability denied warning
- State: warnings[code=CAPABILITY_DENIED]
-
-
-
- Generic worker / stale-data warning
- State: warnings[code=BACKEND_UNAVAILABLE]
-
-
-
- Overview fetch error
-
-
-
-
- Policy preview — deny
- State: licensed, preview.allowed = false
-
-
-
-
-
-
-
-
diff --git a/design/PAP-9732/screenshots/01-missing-company-desktop.png b/design/PAP-9732/screenshots/01-missing-company-desktop.png
deleted file mode 100644
index d27cac962d8..00000000000
Binary files a/design/PAP-9732/screenshots/01-missing-company-desktop.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/01-missing-company-mobile.png b/design/PAP-9732/screenshots/01-missing-company-mobile.png
deleted file mode 100644
index 84ce4d5d9a5..00000000000
Binary files a/design/PAP-9732/screenshots/01-missing-company-mobile.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/02-loading-desktop.png b/design/PAP-9732/screenshots/02-loading-desktop.png
deleted file mode 100644
index 36a625b4dd7..00000000000
Binary files a/design/PAP-9732/screenshots/02-loading-desktop.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/02-loading-mobile.png b/design/PAP-9732/screenshots/02-loading-mobile.png
deleted file mode 100644
index 9aa57f20396..00000000000
Binary files a/design/PAP-9732/screenshots/02-loading-mobile.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/03-unlicensed-desktop.png b/design/PAP-9732/screenshots/03-unlicensed-desktop.png
deleted file mode 100644
index 1f308ae10bd..00000000000
Binary files a/design/PAP-9732/screenshots/03-unlicensed-desktop.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/03-unlicensed-mobile.png b/design/PAP-9732/screenshots/03-unlicensed-mobile.png
deleted file mode 100644
index 899ac724c0f..00000000000
Binary files a/design/PAP-9732/screenshots/03-unlicensed-mobile.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/04-empty-desktop.png b/design/PAP-9732/screenshots/04-empty-desktop.png
deleted file mode 100644
index 7d9267376ad..00000000000
Binary files a/design/PAP-9732/screenshots/04-empty-desktop.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/04-empty-mobile.png b/design/PAP-9732/screenshots/04-empty-mobile.png
deleted file mode 100644
index e0bab5f029e..00000000000
Binary files a/design/PAP-9732/screenshots/04-empty-mobile.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/05-populated-desktop.png b/design/PAP-9732/screenshots/05-populated-desktop.png
deleted file mode 100644
index 78ddea81ec6..00000000000
Binary files a/design/PAP-9732/screenshots/05-populated-desktop.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/05-populated-mobile.png b/design/PAP-9732/screenshots/05-populated-mobile.png
deleted file mode 100644
index 0ea91fa408a..00000000000
Binary files a/design/PAP-9732/screenshots/05-populated-mobile.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/06-pending-join-desktop.png b/design/PAP-9732/screenshots/06-pending-join-desktop.png
deleted file mode 100644
index 59bd2585194..00000000000
Binary files a/design/PAP-9732/screenshots/06-pending-join-desktop.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/06-pending-join-mobile.png b/design/PAP-9732/screenshots/06-pending-join-mobile.png
deleted file mode 100644
index 6822da72a61..00000000000
Binary files a/design/PAP-9732/screenshots/06-pending-join-mobile.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/07-denied-desktop.png b/design/PAP-9732/screenshots/07-denied-desktop.png
deleted file mode 100644
index cec09f22ad9..00000000000
Binary files a/design/PAP-9732/screenshots/07-denied-desktop.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/07-denied-mobile.png b/design/PAP-9732/screenshots/07-denied-mobile.png
deleted file mode 100644
index 6cea8e5947f..00000000000
Binary files a/design/PAP-9732/screenshots/07-denied-mobile.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/08-stale-desktop.png b/design/PAP-9732/screenshots/08-stale-desktop.png
deleted file mode 100644
index efe9ce91a9a..00000000000
Binary files a/design/PAP-9732/screenshots/08-stale-desktop.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/08-stale-mobile.png b/design/PAP-9732/screenshots/08-stale-mobile.png
deleted file mode 100644
index 38c672cf742..00000000000
Binary files a/design/PAP-9732/screenshots/08-stale-mobile.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/09-error-desktop.png b/design/PAP-9732/screenshots/09-error-desktop.png
deleted file mode 100644
index 883900633c7..00000000000
Binary files a/design/PAP-9732/screenshots/09-error-desktop.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/09-error-mobile.png b/design/PAP-9732/screenshots/09-error-mobile.png
deleted file mode 100644
index 9df1a999c76..00000000000
Binary files a/design/PAP-9732/screenshots/09-error-mobile.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/10-deny-desktop.png b/design/PAP-9732/screenshots/10-deny-desktop.png
deleted file mode 100644
index 078d5bc46ed..00000000000
Binary files a/design/PAP-9732/screenshots/10-deny-desktop.png and /dev/null differ
diff --git a/design/PAP-9732/screenshots/10-deny-mobile.png b/design/PAP-9732/screenshots/10-deny-mobile.png
deleted file mode 100644
index c8ce9c4ecb4..00000000000
Binary files a/design/PAP-9732/screenshots/10-deny-mobile.png and /dev/null differ
diff --git a/design/PAP-9732/sdk-mock.mjs b/design/PAP-9732/sdk-mock.mjs
deleted file mode 100644
index 2d120efafdd..00000000000
--- a/design/PAP-9732/sdk-mock.mjs
+++ /dev/null
@@ -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");
-}
diff --git a/design/PAP-9732/states.mjs b/design/PAP-9732/states.mjs
deleted file mode 100644
index 2cda8961763..00000000000
--- a/design/PAP-9732/states.mjs
+++ /dev/null
@@ -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,
- },
- },
-};
diff --git a/design/PAP-9733/screenshots/01-core-members-page.png b/design/PAP-9733/screenshots/01-core-members-page.png
deleted file mode 100644
index d0277982ab4..00000000000
Binary files a/design/PAP-9733/screenshots/01-core-members-page.png and /dev/null differ
diff --git a/design/PAP-9733/screenshots/02-core-legacy-access-fallback.png b/design/PAP-9733/screenshots/02-core-legacy-access-fallback.png
deleted file mode 100644
index ef5c36be1ed..00000000000
Binary files a/design/PAP-9733/screenshots/02-core-legacy-access-fallback.png and /dev/null differ
diff --git a/design/PAP-9733/screenshots/03-core-invites-page.png b/design/PAP-9733/screenshots/03-core-invites-page.png
deleted file mode 100644
index f94fc4bca8c..00000000000
Binary files a/design/PAP-9733/screenshots/03-core-invites-page.png and /dev/null differ
diff --git a/design/PAP-9733/screenshots/04-ee-permissions-capability-denied.png b/design/PAP-9733/screenshots/04-ee-permissions-capability-denied.png
deleted file mode 100644
index a7dac01c791..00000000000
Binary files a/design/PAP-9733/screenshots/04-ee-permissions-capability-denied.png and /dev/null differ
diff --git a/design/PAP-9733/screenshots/04-ee-permissions-page.png b/design/PAP-9733/screenshots/04-ee-permissions-page.png
deleted file mode 100644
index 7354f727e6a..00000000000
Binary files a/design/PAP-9733/screenshots/04-ee-permissions-page.png and /dev/null differ
diff --git a/design/PAP-9733/screenshots/05-ee-disabled-fallback.png b/design/PAP-9733/screenshots/05-ee-disabled-fallback.png
deleted file mode 100644
index ef5c36be1ed..00000000000
Binary files a/design/PAP-9733/screenshots/05-ee-disabled-fallback.png and /dev/null differ