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 · 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.svg1280×1240 · desktop
- Company access desktop wireframe -
-
-
mobile375×1180
- Company access mobile wireframe -
-
-
-

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.svg1280×900 · desktop
- Scope grant editor desktop wireframe -
-
-
mobile375×1100
- Scope grant editor mobile wireframe -
-
-
-

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.svg1280×1320 · desktop
- Agent permissions desktop wireframe -
-
-
mobile375×1700
- Agent permissions mobile wireframe -
-
-
-

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.svg1280×1320 · desktop
- Denied / read-only desktop wireframe -
-
-
mobile375×1620
- Denied / read-only mobile wireframe -
-
-
-

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.svg1280×1080 · desktop
- Privacy rule desktop wireframe -
-
-
mobile375×1500
- Privacy rule mobile wireframe -
-
-
-

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.

- -
-

Missing company

-
State: no active company
-
- -
-

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

-
State: overview.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