From aea133ff9fe23d5b06a4d76ba0cfef8893f56927 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 14 Mar 2026 17:47:53 -0500 Subject: [PATCH 01/10] Add archive project button and filter archived projects from selectors - Add "Archive project" / "Unarchive project" button in the project configuration danger zone (ProjectProperties) - Filter archived projects from the Projects listing page - Filter archived projects from NewIssueDialog project selector - Filter archived projects from IssueProperties project picker (keeps current project visible even if archived) - Filter archived projects from CommandPalette - SidebarProjects already filters archived projects Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- ui/src/components/CommandPalette.tsx | 6 +++- ui/src/components/IssueProperties.tsx | 6 +++- ui/src/components/NewIssueDialog.tsx | 6 +++- ui/src/components/ProjectProperties.tsx | 45 +++++++++++++++++++++++-- ui/src/pages/ProjectDetail.tsx | 17 ++++++++++ ui/src/pages/Projects.tsx | 12 ++++--- 6 files changed, 83 insertions(+), 9 deletions(-) diff --git a/ui/src/components/CommandPalette.tsx b/ui/src/components/CommandPalette.tsx index 3defb0e6..9d84be52 100644 --- a/ui/src/components/CommandPalette.tsx +++ b/ui/src/components/CommandPalette.tsx @@ -75,11 +75,15 @@ export function CommandPalette() { enabled: !!selectedCompanyId && open, }); - const { data: projects = [] } = useQuery({ + const { data: allProjects = [] } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId && open, }); + const projects = useMemo( + () => allProjects.filter((p) => !p.archivedAt), + [allProjects], + ); function go(path: string) { setOpen(false); diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index cf4b6a43..4781aea5 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -131,8 +131,12 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp queryFn: () => projectsApi.list(companyId!), enabled: !!companyId, }); + const activeProjects = useMemo( + () => (projects ?? []).filter((p) => !p.archivedAt || p.id === issue.projectId), + [projects, issue.projectId], + ); const { orderedProjects } = useProjectOrder({ - projects: projects ?? [], + projects: activeProjects, companyId, userId: currentUserId, }); diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index dc2d73c7..5a9ce792 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -288,8 +288,12 @@ export function NewIssueDialog() { queryFn: () => authApi.getSession(), }); const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; + const activeProjects = useMemo( + () => (projects ?? []).filter((p) => !p.archivedAt), + [projects], + ); const { orderedProjects } = useProjectOrder({ - projects: projects ?? [], + projects: activeProjects, companyId: effectiveCompanyId, userId: currentUserId, }); diff --git a/ui/src/components/ProjectProperties.tsx b/ui/src/components/ProjectProperties.tsx index 9237f5e3..38dc1a33 100644 --- a/ui/src/components/ProjectProperties.tsx +++ b/ui/src/components/ProjectProperties.tsx @@ -13,7 +13,7 @@ import { Separator } from "@/components/ui/separator"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { AlertCircle, Check, ExternalLink, Github, Loader2, Plus, Trash2, X } from "lucide-react"; +import { AlertCircle, Archive, ArchiveRestore, Check, ExternalLink, Github, Loader2, Plus, Trash2, X } from "lucide-react"; import { ChoosePathButton } from "./PathInstructionsModal"; import { DraftInput } from "./agent-config-primitives"; import { InlineEditor } from "./InlineEditor"; @@ -34,6 +34,8 @@ interface ProjectPropertiesProps { onUpdate?: (data: Record) => void; onFieldUpdate?: (field: ProjectConfigFieldKey, data: Record) => void; getFieldSaveState?: (field: ProjectConfigFieldKey) => ProjectFieldSaveState; + onArchive?: (archived: boolean) => void; + archivePending?: boolean; } export type ProjectFieldSaveState = "idle" | "saving" | "saved" | "error"; @@ -152,7 +154,7 @@ function ProjectStatusPicker({ status, onChange }: { status: string; onChange: ( ); } -export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState }: ProjectPropertiesProps) { +export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState, onArchive, archivePending }: ProjectPropertiesProps) { const { selectedCompanyId } = useCompany(); const queryClient = useQueryClient(); const [goalOpen, setGoalOpen] = useState(false); @@ -954,6 +956,45 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa )} + + {onArchive && ( + <> + +
+
+ Danger Zone +
+
+

+ {project.archivedAt + ? "Unarchive this project to restore it in the sidebar and project selectors." + : "Archive this project to hide it from the sidebar and project selectors."} +

+ +
+
+ + )} ); } diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index 42bb5b86..5134c22b 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -274,6 +274,21 @@ export function ProjectDetail() { onSuccess: invalidateProject, }); + const archiveProject = useMutation({ + mutationFn: (archived: boolean) => + projectsApi.update( + projectLookupRef, + { archivedAt: archived ? new Date().toISOString() : null }, + resolvedCompanyId ?? lookupCompanyId, + ), + onSuccess: (_, archived) => { + invalidateProject(); + if (archived) { + navigate("/projects"); + } + }, + }); + const uploadImage = useMutation({ mutationFn: async (file: File) => { if (!resolvedCompanyId) throw new Error("No company selected"); @@ -476,6 +491,8 @@ export function ProjectDetail() { onUpdate={(data) => updateProject.mutate(data)} onFieldUpdate={updateProjectField} getFieldSaveState={(field) => fieldSaveStates[field] ?? "idle"} + onArchive={(archived) => archiveProject.mutate(archived)} + archivePending={archiveProject.isPending} /> )} diff --git a/ui/src/pages/Projects.tsx b/ui/src/pages/Projects.tsx index 6fe80ada..886a2b60 100644 --- a/ui/src/pages/Projects.tsx +++ b/ui/src/pages/Projects.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; @@ -22,11 +22,15 @@ export function Projects() { setBreadcrumbs([{ label: "Projects" }]); }, [setBreadcrumbs]); - const { data: projects, isLoading, error } = useQuery({ + const { data: allProjects, isLoading, error } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); + const projects = useMemo( + () => (allProjects ?? []).filter((p) => !p.archivedAt), + [allProjects], + ); if (!selectedCompanyId) { return ; @@ -47,7 +51,7 @@ export function Projects() { {error &&

{error.message}

} - {projects && projects.length === 0 && ( + {!isLoading && projects.length === 0 && ( )} - {projects && projects.length > 0 && ( + {projects.length > 0 && (
{projects.map((project) => ( Date: Sun, 15 Mar 2026 10:48:27 -0500 Subject: [PATCH 02/10] Restyle markdown code blocks: dark background, smaller font, compact padding - Switch code block background from transparent accent to dark (#1e1e2e) with light text (#cdd6f4) for better readability in both light and dark modes - Reduce code font size from 0.84em to 0.78em - Compact padding and margins on pre blocks - Hide MDXEditor code block toolbar by default, show on hover/focus to prevent overlap with code content on mobile - Use horizontal scroll instead of word-wrap for code blocks to preserve formatting Co-Authored-By: Paperclip --- ui/src/components/MarkdownBody.tsx | 2 +- ui/src/index.css | 46 +++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index ca9624c0..1242fa8a 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -117,7 +117,7 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) { return (
Date: Sun, 15 Mar 2026 10:49:24 -0500 Subject: [PATCH 03/10] Fix sidebar scrollbar: hide track background when not hovering The scrollbar track background was still visible as a colored "well" even when the thumb was hidden. Now both track and thumb are fully transparent by default, only appearing on container hover. Co-Authored-By: Paperclip --- ui/src/index.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/index.css b/ui/src/index.css index 3b8a5af6..a2a3b794 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -178,12 +178,13 @@ background: oklch(0.5 0 0); } -/* Auto-hide scrollbar: fully transparent by default, visible on container hover */ +/* Auto-hide scrollbar: fully invisible by default, visible on container hover */ .scrollbar-auto-hide::-webkit-scrollbar-track { background: transparent !important; } .scrollbar-auto-hide::-webkit-scrollbar-thumb { background: transparent !important; + transition: background 150ms ease; } .scrollbar-auto-hide:hover::-webkit-scrollbar-track { background: oklch(0.205 0 0) !important; From d7f45eac14845555c4c92b179ea4f3047b517c6a Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 10:55:53 -0500 Subject: [PATCH 04/10] Add doc-maintenance skill for periodic documentation accuracy audits Skill detects documentation drift by scanning git history since last review, cross-referencing shipped features against README, SPEC, and PRODUCT docs, and opening PRs with minimal fixes. Includes audit checklist and section map references. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .agents/skills/doc-maintenance/SKILL.md | 201 ++++++++++++++++++ .../references/audit-checklist.md | 85 ++++++++ .../doc-maintenance/references/section-map.md | 22 ++ 3 files changed, 308 insertions(+) create mode 100644 .agents/skills/doc-maintenance/SKILL.md create mode 100644 .agents/skills/doc-maintenance/references/audit-checklist.md create mode 100644 .agents/skills/doc-maintenance/references/section-map.md diff --git a/.agents/skills/doc-maintenance/SKILL.md b/.agents/skills/doc-maintenance/SKILL.md new file mode 100644 index 00000000..a597e90c --- /dev/null +++ b/.agents/skills/doc-maintenance/SKILL.md @@ -0,0 +1,201 @@ +--- +name: doc-maintenance +description: > + Audit top-level documentation (README, SPEC, PRODUCT) against recent git + history to find drift — shipped features missing from docs or features + listed as upcoming that already landed. Proposes minimal edits, creates + a branch, and opens a PR. Use when asked to review docs for accuracy, + after major feature merges, or on a periodic schedule. +--- + +# Doc Maintenance Skill + +Detect documentation drift and fix it via PR — no rewrites, no churn. + +## When to Use + +- Periodic doc review (e.g. weekly or after releases) +- After major feature merges +- When asked "are our docs up to date?" +- When asked to audit README / SPEC / PRODUCT accuracy + +## Target Documents + +| Document | Path | What matters | +|----------|------|-------------| +| README | `README.md` | Features table, roadmap, quickstart, "what is" accuracy, "works with" table | +| SPEC | `doc/SPEC.md` | No false "not supported" claims, major model/schema accuracy | +| PRODUCT | `doc/PRODUCT.md` | Core concepts, feature list, principles accuracy | + +Out of scope: DEVELOPING.md, DATABASE.md, CLI.md, doc/plans/, skill files, +release notes. These are dev-facing or ephemeral — lower risk of user-facing +confusion. + +## Workflow + +### Step 1 — Detect what changed + +Find the last review cursor: + +```bash +# Read the last-reviewed commit SHA +CURSOR_FILE=".doc-review-cursor" +if [ -f "$CURSOR_FILE" ]; then + LAST_SHA=$(cat "$CURSOR_FILE" | head -1) +else + # First run: look back 60 days + LAST_SHA=$(git log --format="%H" --after="60 days ago" --reverse | head -1) +fi +``` + +Then gather commits since the cursor: + +```bash +git log "$LAST_SHA"..HEAD --oneline --no-merges +``` + +### Step 2 — Classify changes + +Scan commit messages and changed files. Categorize into: + +- **Feature** — new capabilities (keywords: `feat`, `add`, `implement`, `support`) +- **Breaking** — removed/renamed things (keywords: `remove`, `breaking`, `drop`, `rename`) +- **Structural** — new directories, config changes, new adapters, new CLI commands + +**Ignore:** refactors, test-only changes, CI config, dependency bumps, doc-only +changes, style/formatting commits. These don't affect doc accuracy. + +For borderline cases, check the actual diff — a commit titled "refactor: X" +that adds a new public API is a feature. + +### Step 3 — Build a change summary + +Produce a concise list like: + +``` +Since last review (, ): +- FEATURE: Plugin system merged (runtime, SDK, CLI, slots, event bridge) +- FEATURE: Project archiving added +- BREAKING: Removed legacy webhook adapter +- STRUCTURAL: New .agents/skills/ directory convention +``` + +If there are no notable changes, skip to Step 7 (update cursor and exit). + +### Step 4 — Audit each target doc + +For each target document, read it fully and cross-reference against the change +summary. Check for: + +1. **False negatives** — major shipped features not mentioned at all +2. **False positives** — features listed as "coming soon" / "roadmap" / "planned" + / "not supported" / "TBD" that already shipped +3. **Quickstart accuracy** — install commands, prereqs, and startup instructions + still correct (README only) +4. **Feature table accuracy** — does the features section reflect current + capabilities? (README only) +5. **Works-with accuracy** — are supported adapters/integrations listed correctly? + +Use `references/audit-checklist.md` as the structured checklist. +Use `references/section-map.md` to know where to look for each feature area. + +### Step 5 — Create branch and apply minimal edits + +```bash +# Create a branch for the doc updates +BRANCH="docs/maintenance-$(date +%Y%m%d)" +git checkout -b "$BRANCH" +``` + +Apply **only** the edits needed to fix drift. Rules: + +- **Minimal patches only.** Fix inaccuracies, don't rewrite sections. +- **Preserve voice and style.** Match the existing tone of each document. +- **No cosmetic changes.** Don't fix typos, reformat tables, or reorganize + sections unless they're part of a factual fix. +- **No new sections.** If a feature needs a whole new section, note it in the + PR description as a follow-up — don't add it in a maintenance pass. +- **Roadmap items:** Move shipped features out of Roadmap. Add a brief mention + in the appropriate existing section if there isn't one already. Don't add + long descriptions. + +### Step 6 — Open a PR + +Commit the changes and open a PR: + +```bash +git add README.md doc/SPEC.md doc/PRODUCT.md .doc-review-cursor +git commit -m "docs: update documentation for accuracy + +- [list each fix briefly] + +Co-Authored-By: Paperclip " + +git push -u origin "$BRANCH" + +gh pr create \ + --title "docs: periodic documentation accuracy update" \ + --body "$(cat <<'EOF' +## Summary +Automated doc maintenance pass. Fixes documentation drift detected since +last review. + +### Changes +- [list each fix] + +### Change summary (since last review) +- [list notable code changes that triggered doc updates] + +## Review notes +- Only factual accuracy fixes — no style/cosmetic changes +- Preserves existing voice and structure +- Larger doc additions (new sections, tutorials) noted as follow-ups + +🤖 Generated by doc-maintenance skill +EOF +)" +``` + +### Step 7 — Update the cursor + +After a successful audit (whether or not edits were needed), update the cursor: + +```bash +git rev-parse HEAD > .doc-review-cursor +``` + +If edits were made, this is already committed in the PR branch. If no edits +were needed, commit the cursor update to the current branch. + +## Change Classification Rules + +| Signal | Category | Doc update needed? | +|--------|----------|-------------------| +| `feat:`, `add`, `implement`, `support` in message | Feature | Yes if user-facing | +| `remove`, `drop`, `breaking`, `!:` in message | Breaking | Yes | +| New top-level directory or config file | Structural | Maybe | +| `fix:`, `bugfix` | Fix | No (unless it changes behavior described in docs) | +| `refactor:`, `chore:`, `ci:`, `test:` | Maintenance | No | +| `docs:` | Doc change | No (already handled) | +| Dependency bumps only | Maintenance | No | + +## Patch Style Guide + +- Fix the fact, not the prose +- If removing a roadmap item, don't leave a gap — remove the bullet cleanly +- If adding a feature mention, match the format of surrounding entries + (e.g. if features are in a table, add a table row) +- Keep README changes especially minimal — it shouldn't churn often +- For SPEC/PRODUCT, prefer updating existing statements over adding new ones + (e.g. change "not supported in V1" to "supported via X" rather than adding + a new section) + +## Output + +When the skill completes, report: + +- How many commits were scanned +- How many notable changes were found +- How many doc edits were made (and to which files) +- PR link (if edits were made) +- Any follow-up items that need larger doc work diff --git a/.agents/skills/doc-maintenance/references/audit-checklist.md b/.agents/skills/doc-maintenance/references/audit-checklist.md new file mode 100644 index 00000000..9c13a437 --- /dev/null +++ b/.agents/skills/doc-maintenance/references/audit-checklist.md @@ -0,0 +1,85 @@ +# Doc Maintenance Audit Checklist + +Use this checklist when auditing each target document. For each item, compare +against the change summary from git history. + +## README.md + +### Features table +- [ ] Each feature card reflects a shipped capability +- [ ] No feature cards for things that don't exist yet +- [ ] No major shipped features missing from the table + +### Roadmap +- [ ] Nothing listed as "planned" or "coming soon" that already shipped +- [ ] No removed/cancelled items still listed +- [ ] Items reflect current priorities (cross-check with recent PRs) + +### Quickstart +- [ ] `npx paperclipai onboard` command is correct +- [ ] Manual install steps are accurate (clone URL, commands) +- [ ] Prerequisites (Node version, pnpm version) are current +- [ ] Server URL and port are correct + +### "What is Paperclip" section +- [ ] High-level description is accurate +- [ ] Step table (Define goal / Hire team / Approve and run) is correct + +### "Works with" table +- [ ] All supported adapters/runtimes are listed +- [ ] No removed adapters still listed +- [ ] Logos and labels match current adapter names + +### "Paperclip is right for you if" +- [ ] Use cases are still accurate +- [ ] No claims about capabilities that don't exist + +### "Why Paperclip is special" +- [ ] Technical claims are accurate (atomic execution, governance, etc.) +- [ ] No features listed that were removed or significantly changed + +### FAQ +- [ ] Answers are still correct +- [ ] No references to removed features or outdated behavior + +### Development section +- [ ] Commands are accurate (`pnpm dev`, `pnpm build`, etc.) +- [ ] Link to DEVELOPING.md is correct + +## doc/SPEC.md + +### Company Model +- [ ] Fields match current schema +- [ ] Governance model description is accurate + +### Agent Model +- [ ] Adapter types match what's actually supported +- [ ] Agent configuration description is accurate +- [ ] No features described as "not supported" or "not V1" that shipped + +### Task Model +- [ ] Task hierarchy description is accurate +- [ ] Status values match current implementation + +### Extensions / Plugins +- [ ] If plugins are shipped, no "not in V1" or "future" language +- [ ] Plugin model description matches implementation + +### Open Questions +- [ ] Resolved questions removed or updated +- [ ] No "TBD" items that have been decided + +## doc/PRODUCT.md + +### Core Concepts +- [ ] Company, Employees, Task Management descriptions accurate +- [ ] Agent Execution modes described correctly +- [ ] No missing major concepts + +### Principles +- [ ] Principles haven't been contradicted by shipped features +- [ ] No principles referencing removed capabilities + +### User Flow +- [ ] Dream scenario still reflects actual onboarding +- [ ] Steps are achievable with current features diff --git a/.agents/skills/doc-maintenance/references/section-map.md b/.agents/skills/doc-maintenance/references/section-map.md new file mode 100644 index 00000000..4ec64f83 --- /dev/null +++ b/.agents/skills/doc-maintenance/references/section-map.md @@ -0,0 +1,22 @@ +# Section Map + +Maps feature areas to specific document sections so the skill knows where to +look when a feature ships or changes. + +| Feature Area | README Section | SPEC Section | PRODUCT Section | +|-------------|---------------|-------------|----------------| +| Plugins / Extensions | Features table, Roadmap | Extensions, Agent Model | Core Concepts | +| Adapters (new runtimes) | "Works with" table, FAQ | Agent Model, Agent Configuration | Employees & Agents, Agent Execution | +| Governance / Approvals | Features table, "Why special" | Board Governance, Board Approval Gates | Principles | +| Budget / Cost Control | Features table, "Why special" | Budget Delegation | Company (revenue & expenses) | +| Task Management | Features table | Task Model | Task Management | +| Org Chart / Hierarchy | Features table | Agent Model (reporting) | Employees & Agents | +| Multi-Company | Features table, FAQ | Company Model | Company | +| Heartbeats | Features table, FAQ | Agent Execution | Agent Execution | +| CLI Commands | Development section | — | — | +| Onboarding / Quickstart | Quickstart, FAQ | — | User Flow | +| Skills / Skill Injection | "Why special" | — | — | +| Company Templates | "Why special", Roadmap (ClipMart) | — | — | +| Mobile / UI | Features table | — | — | +| Project Archiving | — | — | — | +| OpenClaw Integration | "Works with" table, FAQ | Agent Model | Agent Execution | From 41e03bae61d1ad5c96bf250043076b682cd25d5e Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:08:37 -0500 Subject: [PATCH 05/10] Fix org chart canvas height to fit viewport without scrolling The height calc subtracted only 4rem but the actual overhead is ~6rem (3rem breadcrumb bar + 3rem main padding). Also use dvh for better mobile support. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- ui/src/pages/OrgChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/OrgChart.tsx b/ui/src/pages/OrgChart.tsx index 981545c0..7eb0f0d9 100644 --- a/ui/src/pages/OrgChart.tsx +++ b/ui/src/pages/OrgChart.tsx @@ -269,7 +269,7 @@ export function OrgChart() { return (
Date: Sun, 15 Mar 2026 14:18:56 -0500 Subject: [PATCH 06/10] Add Docker setup for untrusted PR review in isolated containers Adds a dedicated Docker environment for reviewing untrusted pull requests with codex/claude, keeping CLI auth state in volumes and using a separate scratch workspace for PR checkouts. Co-Authored-By: Claude Opus 4.6 (1M context) --- doc/DEVELOPING.md | 4 + doc/DOCKER.md | 6 + doc/UNTRUSTED-PR-REVIEW.md | 135 ++++++++++++++++++ docker-compose.untrusted-review.yml | 33 +++++ docker/untrusted-review/Dockerfile | 44 ++++++ .../untrusted-review/bin/review-checkout-pr | 65 +++++++++ 6 files changed, 287 insertions(+) create mode 100644 doc/UNTRUSTED-PR-REVIEW.md create mode 100644 docker-compose.untrusted-review.yml create mode 100644 docker/untrusted-review/Dockerfile create mode 100644 docker/untrusted-review/bin/review-checkout-pr diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index e3668516..b39839c1 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -89,6 +89,10 @@ docker compose -f docker-compose.quickstart.yml up --build See `doc/DOCKER.md` for API key wiring (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`) and persistence details. +## Docker For Untrusted PR Review + +For a separate review-oriented container that keeps `codex`/`claude` login state in Docker volumes and checks out PRs into an isolated scratch workspace, see `doc/UNTRUSTED-PR-REVIEW.md`. + ## Database in Dev (Auto-Handled) For local development, leave `DATABASE_URL` unset. diff --git a/doc/DOCKER.md b/doc/DOCKER.md index 82559bf8..6f6ca374 100644 --- a/doc/DOCKER.md +++ b/doc/DOCKER.md @@ -93,6 +93,12 @@ Notes: - Without API keys, the app still runs normally. - Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites. +## Untrusted PR Review Container + +If you want a separate Docker environment for reviewing untrusted pull requests with `codex` or `claude`, use the dedicated review workflow in `doc/UNTRUSTED-PR-REVIEW.md`. + +That setup keeps CLI auth state in Docker volumes instead of your host home directory and uses a separate scratch workspace for PR checkouts and preview runs. + ## Onboard Smoke Test (Ubuntu + npm only) Use this when you want to mimic a fresh machine that only has Ubuntu + npm and verify: diff --git a/doc/UNTRUSTED-PR-REVIEW.md b/doc/UNTRUSTED-PR-REVIEW.md new file mode 100644 index 00000000..0061a581 --- /dev/null +++ b/doc/UNTRUSTED-PR-REVIEW.md @@ -0,0 +1,135 @@ +# Untrusted PR Review In Docker + +Use this workflow when you want Codex or Claude to inspect a pull request that you do not want touching your host machine directly. + +This is intentionally separate from the normal Paperclip dev image. + +## What this container isolates + +- `codex` auth/session state in a Docker volume, not your host `~/.codex` +- `claude` auth/session state in a Docker volume, not your host `~/.claude` +- `gh` auth state in the same container-local home volume +- review clones, worktrees, dependency installs, and local databases in a writable scratch volume under `/work` + +By default this workflow does **not** mount your host repo checkout, your host home directory, or your SSH agent. + +## Files + +- `docker/untrusted-review/Dockerfile` +- `docker-compose.untrusted-review.yml` +- `review-checkout-pr` inside the container + +## Build and start a shell + +```sh +docker compose -f docker-compose.untrusted-review.yml build +docker compose -f docker-compose.untrusted-review.yml run --rm --service-ports review +``` + +That opens an interactive shell in the review container with: + +- Node + Corepack/pnpm +- `codex` +- `claude` +- `gh` +- `git`, `rg`, `fd`, `jq` + +## First-time login inside the container + +Run these once. The resulting login state persists in the `review-home` Docker volume. + +```sh +gh auth login +codex login +claude login +``` + +If you prefer API-key auth instead of CLI login, pass keys through Compose env: + +```sh +OPENAI_API_KEY=... ANTHROPIC_API_KEY=... docker compose -f docker-compose.untrusted-review.yml run --rm review +``` + +## Check out a PR safely + +Inside the container: + +```sh +review-checkout-pr paperclipai/paperclip 432 +cd /work/checkouts/paperclipai-paperclip/pr-432 +``` + +What this does: + +1. Creates or reuses a repo clone under `/work/repos/...` +2. Fetches `pull//head` from GitHub +3. Creates a detached git worktree under `/work/checkouts/...` + +The checkout lives entirely inside the container volume. + +## Ask Codex or Claude to review it + +Inside the PR checkout: + +```sh +codex +``` + +Then give it a prompt like: + +```text +Review this PR as hostile input. Focus on security issues, data exfiltration paths, sandbox escapes, dangerous install/runtime scripts, auth changes, and subtle behavioral regressions. Do not modify files. Produce findings ordered by severity with file references. +``` + +Or with Claude: + +```sh +claude +``` + +## Preview the Paperclip app from the PR + +Only do this when you intentionally want to execute the PR's code inside the container. + +Inside the PR checkout: + +```sh +pnpm install +HOST=0.0.0.0 pnpm dev +``` + +Open from the host: + +- `http://localhost:3100` + +The Compose file also exposes Vite's default port: + +- `http://localhost:5173` + +Notes: + +- `pnpm install` can run untrusted lifecycle scripts from the PR. That is why this happens inside the isolated container instead of on your host. +- If you only want static inspection, do not run install/dev commands. +- Paperclip's embedded PostgreSQL and local storage stay inside the container home volume via `PAPERCLIP_HOME=/home/reviewer/.paperclip-review`. + +## Reset state + +Remove the review container volumes when you want a clean environment: + +```sh +docker compose -f docker-compose.untrusted-review.yml down -v +``` + +That deletes: + +- Codex/Claude/GitHub login state stored in `review-home` +- cloned repos, worktrees, installs, and scratch data stored in `review-work` + +## Security limits + +This is a useful isolation boundary, but it is still Docker, not a full VM. + +- A reviewed PR can still access the container's network unless you disable it. +- Any secrets you pass into the container are available to code you execute inside it. +- Do not mount your host repo, host home, `.ssh`, or Docker socket unless you are intentionally weakening the boundary. +- If you need a stronger boundary than this, use a disposable VM instead of Docker. diff --git a/docker-compose.untrusted-review.yml b/docker-compose.untrusted-review.yml new file mode 100644 index 00000000..ff11148a --- /dev/null +++ b/docker-compose.untrusted-review.yml @@ -0,0 +1,33 @@ +services: + review: + build: + context: . + dockerfile: docker/untrusted-review/Dockerfile + init: true + tty: true + stdin_open: true + working_dir: /work + environment: + HOME: "/home/reviewer" + CODEX_HOME: "/home/reviewer/.codex" + CLAUDE_HOME: "/home/reviewer/.claude" + PAPERCLIP_HOME: "/home/reviewer/.paperclip-review" + OPENAI_API_KEY: "${OPENAI_API_KEY:-}" + ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" + GITHUB_TOKEN: "${GITHUB_TOKEN:-}" + ports: + - "${REVIEW_PAPERCLIP_PORT:-3100}:3100" + - "${REVIEW_VITE_PORT:-5173}:5173" + volumes: + - review-home:/home/reviewer + - review-work:/work + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + tmpfs: + - /tmp:mode=1777,size=1g + +volumes: + review-home: + review-work: diff --git a/docker/untrusted-review/Dockerfile b/docker/untrusted-review/Dockerfile new file mode 100644 index 00000000..c8b1f432 --- /dev/null +++ b/docker/untrusted-review/Dockerfile @@ -0,0 +1,44 @@ +FROM node:lts-trixie-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + curl \ + fd-find \ + gh \ + git \ + jq \ + less \ + openssh-client \ + procps \ + ripgrep \ + && rm -rf /var/lib/apt/lists/* + +RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd + +RUN corepack enable \ + && npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest + +RUN useradd --create-home --shell /bin/bash reviewer + +ENV HOME=/home/reviewer \ + CODEX_HOME=/home/reviewer/.codex \ + CLAUDE_HOME=/home/reviewer/.claude \ + PAPERCLIP_HOME=/home/reviewer/.paperclip-review \ + PNPM_HOME=/home/reviewer/.local/share/pnpm \ + PATH=/home/reviewer/.local/share/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +WORKDIR /work + +COPY --chown=reviewer:reviewer docker/untrusted-review/bin/review-checkout-pr /usr/local/bin/review-checkout-pr + +RUN chmod +x /usr/local/bin/review-checkout-pr \ + && mkdir -p /work \ + && chown -R reviewer:reviewer /work + +USER reviewer + +EXPOSE 3100 5173 + +CMD ["bash", "-l"] diff --git a/docker/untrusted-review/bin/review-checkout-pr b/docker/untrusted-review/bin/review-checkout-pr new file mode 100644 index 00000000..abca98ad --- /dev/null +++ b/docker/untrusted-review/bin/review-checkout-pr @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: review-checkout-pr [checkout-dir] + +Examples: + review-checkout-pr paperclipai/paperclip 432 + review-checkout-pr https://github.com/paperclipai/paperclip.git 432 +EOF +} + +if [[ $# -lt 2 || $# -gt 3 ]]; then + usage >&2 + exit 1 +fi + +normalize_repo_slug() { + local raw="$1" + raw="${raw#git@github.com:}" + raw="${raw#ssh://git@github.com/}" + raw="${raw#https://github.com/}" + raw="${raw#http://github.com/}" + raw="${raw%.git}" + printf '%s\n' "${raw#/}" +} + +repo_slug="$(normalize_repo_slug "$1")" +pr_number="$2" + +if [[ ! "$repo_slug" =~ ^[^/]+/[^/]+$ ]]; then + echo "Expected GitHub repo slug like owner/repo or a GitHub repo URL, got: $1" >&2 + exit 1 +fi + +if [[ ! "$pr_number" =~ ^[0-9]+$ ]]; then + echo "PR number must be numeric, got: $pr_number" >&2 + exit 1 +fi + +repo_key="${repo_slug//\//-}" +mirror_dir="/work/repos/${repo_key}" +checkout_dir="${3:-/work/checkouts/${repo_key}/pr-${pr_number}}" +pr_ref="refs/remotes/origin/pr/${pr_number}" + +mkdir -p "$(dirname "$mirror_dir")" "$(dirname "$checkout_dir")" + +if [[ ! -d "$mirror_dir/.git" ]]; then + if command -v gh >/dev/null 2>&1; then + gh repo clone "$repo_slug" "$mirror_dir" -- --filter=blob:none + else + git clone --filter=blob:none "https://github.com/${repo_slug}.git" "$mirror_dir" + fi +fi + +git -C "$mirror_dir" fetch --force origin "pull/${pr_number}/head:${pr_ref}" + +if [[ -e "$checkout_dir" ]]; then + printf '%s\n' "$checkout_dir" + exit 0 +fi + +git -C "$mirror_dir" worktree add --detach "$checkout_dir" "$pr_ref" >/dev/null +printf '%s\n' "$checkout_dir" From 597c4b1d457d2207dee01863c6ade1078d7f6c83 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:27:34 -0500 Subject: [PATCH 07/10] Fix code block styles with robust prose overrides Previous attempt was being overridden by Tailwind prose/prose-invert CSS variables. This fix: - Overrides --tw-prose-pre-bg and --tw-prose-invert-pre-bg CSS variables on .paperclip-markdown to force dark background in both modes - Uses .paperclip-markdown pre with \!important for bulletproof overrides - Removes conflicting prose-pre: utility classes from MarkdownBody - Adds explicit pre code reset (inherit color/size, no background) - Verified visually with Playwright at desktop and mobile viewports Co-Authored-By: Paperclip --- ui/src/components/MarkdownBody.tsx | 2 +- ui/src/index.css | 36 +++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index 1242fa8a..683adc53 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -117,7 +117,7 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) { return (
Date: Sun, 15 Mar 2026 14:33:22 -0500 Subject: [PATCH 08/10] Remove duplicate @paperclipai/adapter-openclaw-gateway in server/package.json Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- server/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/server/package.json b/server/package.json index a2387fe9..1887d64c 100644 --- a/server/package.json +++ b/server/package.json @@ -41,7 +41,6 @@ "@paperclipai/adapter-openclaw-gateway": "workspace:*", "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", - "@paperclipai/adapter-openclaw-gateway": "workspace:*", "hermes-paperclip-adapter": "0.1.1", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", From 16ab8c830325d36cb532e3e8701d9f6c59c683a6 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:39:09 -0500 Subject: [PATCH 09/10] Dark theme for CodeMirror code blocks in MDXEditor The code blocks users see in issue documents are rendered by CodeMirror (via MDXEditor's codeMirrorPlugin), not by MarkdownBody. MDXEditor bundles cm6-theme-basic-light which gives them a white background. Added dark overrides for all CodeMirror elements: - .cm-editor: dark background (#1e1e2e), light text (#cdd6f4) - .cm-gutters: darker gutter with muted line numbers - .cm-activeLine, .cm-selectionBackground: subtle dark highlights - .cm-cursor: light cursor for visibility - Language selector dropdown: dark-themed to match - Reduced pre padding to 0 since CodeMirror handles its own spacing Uses \!important to beat CodeMirror's programmatically-injected theme styles (EditorView.theme generates high-specificity scoped selectors). Co-Authored-By: Paperclip --- ui/src/index.css | 50 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/ui/src/index.css b/ui/src/index.css index 1172f103..c9ee652f 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -417,7 +417,7 @@ .paperclip-mdxeditor-content pre { margin: 0.4rem 0; - padding: 0.5rem 0.65rem; + padding: 0; border: 1px solid color-mix(in oklab, var(--foreground) 12%, transparent); border-radius: calc(var(--radius) - 3px); background: #1e1e2e; @@ -425,7 +425,46 @@ overflow-x: auto; } -/* MDXEditor code block language selector – keep it out of the way on small screens */ +/* Dark theme for CodeMirror code blocks inside the MDXEditor. + Overrides the default cm6-theme-basic-light that MDXEditor bundles. */ +.paperclip-mdxeditor .cm-editor { + background-color: #1e1e2e !important; + color: #cdd6f4 !important; + font-size: 0.78em; +} + +.paperclip-mdxeditor .cm-gutters { + background-color: #181825 !important; + color: #585b70 !important; + border-right: 1px solid #313244 !important; +} + +.paperclip-mdxeditor .cm-activeLineGutter { + background-color: #1e1e2e !important; +} + +.paperclip-mdxeditor .cm-activeLine { + background-color: color-mix(in oklab, #cdd6f4 5%, transparent) !important; +} + +.paperclip-mdxeditor .cm-cursor, +.paperclip-mdxeditor .cm-dropCursor { + border-left-color: #cdd6f4 !important; +} + +.paperclip-mdxeditor .cm-selectionBackground { + background-color: color-mix(in oklab, #89b4fa 25%, transparent) !important; +} + +.paperclip-mdxeditor .cm-focused .cm-selectionBackground { + background-color: color-mix(in oklab, #89b4fa 30%, transparent) !important; +} + +.paperclip-mdxeditor .cm-content { + caret-color: #cdd6f4; +} + +/* MDXEditor code block language selector – show on hover only */ .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"] { position: relative; } @@ -440,6 +479,13 @@ transition: opacity 150ms ease; } +.paperclip-mdxeditor-content [class*="_codeMirrorToolbar_"] select, +.paperclip-mdxeditor-content [class*="_codeBlockToolbar_"] select { + background-color: #313244; + color: #cdd6f4; + border-color: #45475a; +} + .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:hover [class*="_codeMirrorToolbar_"], .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:hover [class*="_codeBlockToolbar_"], .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:focus-within [class*="_codeMirrorToolbar_"], From c5cc191a08fbd9da2e1b68b525ffd48c14160bc0 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:44:01 -0500 Subject: [PATCH 10/10] chore: ignore superset artifacts --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 066fcc68..06303bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ tmp/ # Playwright tests/e2e/test-results/ -tests/e2e/playwright-report/ \ No newline at end of file +tests/e2e/playwright-report/ +.superset/ \ No newline at end of file