mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-07 07:23:08 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The runner package already provides the production protocol and execution spine. > - Contributors still need stable SDK surfaces, deterministic test tools, and local inspection tools. > - Those surfaces share generated contracts and must change as one package boundary. > - This pull request adds the package-local SDK, labs, examples, and drift checks. > - The benefit is a reviewable developer platform that does not change application execution selection. ## Linked Issues or Issue Description **Subsystem affected** `packages/paperclip-runner` — runner SDK, conformance tools, and developer tooling. **Problem or motivation** The production runner spine is present, but package consumers cannot build deterministic integrations, inspect sessions, or verify provider-neutral behavior through supported surfaces. **Proposed solution** Add browser, React, standalone, live-session, scenario, conformance, and evaluation surfaces. Add generated contract inventories and package-local verification scripts. Keep production application routing unchanged. **Alternatives considered** We considered splitting each generated catalog, SDK surface, and demo into separate pull requests. Those changes share exports, fixtures, and drift gates. Splitting them would create intermediate package states that do not build. **Roadmap alignment** No overlapping item appears in `ROADMAP.md`. This work extends the runner package that is already on `master`. ## What Changed - Add browser, React, standalone, live-session, and issue-thread SDK surfaces. - Add deterministic mock control-plane, scenario, conformance, replay, and evaluation tools. - Add bounded Codex, OpenCode, and ACPX development transports and fixtures. - Keep deferred managed-provider execution fail-closed. Persisted compatibility data remains readable. - Add generated capability inventories with their source files and drift checks. - Add examples, package documentation, browser checks, and clean-consumer checks. - Preserve the reviewed protocol bounds, replay compatibility aliases, process environment isolation, and semantic redaction limits. - Update the ACPX package patch that the existing workspace patch registry already tracks. - Do not change `pnpm-lock.yaml`, repository workflows, server runtime selection, or the application UI. ## Verification GitHub Actions is the verification authority for this pull request. The repository CI, package TypeScript and Rust checks, package tests, generated-output drift checks, browser checks, security scans, and Greptile review must pass on the exact head. Local test suites were not run because this series uses parallel GitHub Actions for verification. ## Risks This is a large greenfield package change. The main risks are public export drift, generated-output drift, and optional React consumer compatibility. Package boundary checks, clean-consumer checks, and browser tests cover those risks. Production adapter selection and server execution are outside this pull request. ## Stack 1. **This PR:** runner SDK and developer tooling. 2. [Codex production server integration](https://github.com/paperclipai/paperclip/pull/12616). 3. [Provider-neutral task-thread UI](https://github.com/paperclipai/paperclip/pull/12617). ## Model Used OpenAI Codex, GPT-5, high-reasoning mode, with tool use and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have described the issue in-PR following the feature request template - [x] I have not referenced internal/instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
225 lines
12 KiB
Markdown
225 lines
12 KiB
Markdown
# Capability Paperclip-style issue-thread UI
|
||
|
||
Capability renders the mock Paperclip issue as a native issue thread. A board user
|
||
reads the thread, answers typed interactions inline, and inspects the evidence
|
||
behind every mock mutation. The implementation follows the binding
|
||
[Capability issue-thread UX contract](design/capability-issue-thread-ux-contract.md).
|
||
|
||
The same shell hosts a second primary path: the
|
||
[clean-room live chat](capability-clean-room-chat.md) at `#/chat`, which starts a
|
||
blank thread on a freshly minted mock tenant instead of a preset scenario. It
|
||
reuses every surface described below — header, thread, composer, and Evidence
|
||
panel — and differs only in what the session is seeded with and in refusing any
|
||
non-live mode.
|
||
|
||
## Authority boundary
|
||
|
||
```text
|
||
browser -> package server -> CapabilityLiveSession -> paperclip-runnerd -> codex
|
||
(projection) CapabilitySemanticDispatcher -> ControlPlanePort mock
|
||
```
|
||
|
||
The browser holds no authority. It renders one shape,
|
||
`CapabilityIssueThreadSnapshot` (`src/issue-thread/types.ts`), and computes no
|
||
claim, policy decision, state diff, or parity verdict. Two producers emit that
|
||
shape:
|
||
|
||
- `capabilityIssueThreadFixture(slug)` — deterministic `fake` snapshots used by the
|
||
screenshot matrix and the browser suite.
|
||
- `projectCapabilityIssueThread({ snapshot })` — the live projection. It runs in the
|
||
package server and rearranges durable records only: the live session's
|
||
transcript, evidence entries, semantic authorization records, and the
|
||
serialized mock state.
|
||
|
||
The one browser-initiated mock mutation is an interaction response. It posts to
|
||
`POST /api/capability/ui/interaction`; `CapabilityLiveSession.resolveInteraction` stores
|
||
the typed response in the mock control plane **before** resuming the same Codex
|
||
thread, so the card only leaves `submitting` on server acknowledgement.
|
||
|
||
No provider, runner, or control-plane credential reaches the page. Redacted
|
||
fields render as `••• redacted` with the redaction rule name, mock issues use
|
||
the reserved `MCK-` prefix, and no real Paperclip URL is ever rendered.
|
||
|
||
## What the browser is allowed to see
|
||
|
||
The projection is an internal shape. What ships is the published DTO,
|
||
`toCapabilityPublicThreadView` (`src/issue-thread/public-view.ts`), which copies the
|
||
view field by field — so a field added to the projection, or to any record it
|
||
passes through, cannot reach a browser until it is listed there. Every response
|
||
path uses it: interim stream frames, the settled payload, and reconnect or
|
||
replay replies alike.
|
||
|
||
Three narrowings stack, so no single omission opens a disclosure path:
|
||
|
||
1. **At record time.** `redactCapabilityEvidenceData`
|
||
(`src/live/evidence-redaction.ts`) is the only way an evidence entry enters
|
||
a live session. Provider notifications are reduced to a coarse category,
|
||
provider diagnostics to the fact that one occurred, tool arguments to the
|
||
catalog-declared field names they used, and tool results to the outcome,
|
||
revisions, and mock entity refs the UI resolves into cards. Provider thread
|
||
and session identity, model and token metadata, and raw tool payloads are
|
||
never retained, so no later reader, frame, or log can republish them.
|
||
2. **In the projection.** `Runner & events` details are composed from the
|
||
redacted record rather than stringified from it, and `Calls & results` names
|
||
the operation and its field count instead of echoing arguments.
|
||
3. **In the DTO.** Provider-authored turn and call identifiers are replaced by
|
||
in-view aliases (`turn-1`, `call-1`) that stay consistent across anchors,
|
||
evidence refs, and successive frames of one turn, and any value the caller
|
||
declares withheld is scrubbed from the encoded result.
|
||
|
||
A streamed turn that fails answers with a code and fixed operator copy; the
|
||
underlying message stays server side, because provider text can quote prompts
|
||
and paths.
|
||
|
||
## Session capability
|
||
|
||
Every session-scoped route is bound to a per-browser capability. The server
|
||
mints one on session creation, stores only its SHA-256 with the session record,
|
||
sets it as an `HttpOnly; SameSite=Strict` cookie, and compares it in constant
|
||
time on every read and mutation — message, reconnect, interaction, stop, reset,
|
||
and new chat. A valid session id presented without its capability is answered
|
||
`404`, exactly like an id that never existed, so an unauthorized caller cannot
|
||
tell a live session from a dead one.
|
||
|
||
Rotation belongs to the actions that start something new — `New chat`, a scenario
|
||
POST, reset. Each revokes the caller's existing bindings before issuing the
|
||
replacement cookie. Reopening a page whose stored id is simply gone mints a
|
||
session under the capability the browser already holds instead, because rotating
|
||
there would make two tabs of one surface revoke each other on every load while
|
||
protecting nothing: one browser is one principal, and cross-browser denial rests
|
||
on the binding rather than on how often the value changes.
|
||
|
||
The two surfaces use separate cookie names (`paperclip_capability_issue`,
|
||
`paperclip_capability_chat`) because they are separate pages of one origin: a single
|
||
name would make opening the explorer revoke the clean room. A script that drives
|
||
these routes has to behave like one browser — `scripts/capability-cookie-jar.mjs` is
|
||
what the smoke scripts use for that.
|
||
|
||
## Surfaces
|
||
|
||
- **Header** — three identity chips (`Real Codex` / `Fake agent` / `Replay`,
|
||
`Real runnerd` / `In-process runner`, and `Mock Paperclip` in every mode),
|
||
status, priority, run state, and the Scenario/Replay/Reset/Stop controls.
|
||
`data-session-mode` carries the mode as data, never as styling.
|
||
- **Thread** — turn groups binding the contract's T1–T11 item types: user
|
||
messages, model prose, durable progress comments marked
|
||
`Recorded to mock thread`, collapsed tool strips, interaction cards, document
|
||
revision cards, deliverables, delegation cards, terminal dispositions, typed
|
||
denials, and muted system notices.
|
||
- **Composer** — six mutually exclusive states behind `data-composer-state`:
|
||
`ready`, `sending`, `streaming` (input stays editable to steer; Stop is
|
||
primary), `waiting`, `reconnecting`, and `disabled`. Drafts survive refresh.
|
||
- **Evidence panel** — eight accordion sections in fixed order: Tools exposed,
|
||
Calls & results, Authorization, Control plane, Runner & events, State diff,
|
||
Traceability, Parity. The Tools section groups `Agent tool — always`,
|
||
`Agent tool — granted` (with its grant), and a separated
|
||
`Control plane (not exposed to the agent)` list, because what the model
|
||
*cannot* call is first-class evidence. Every strip, denial, and card deep-links
|
||
into the matching record, and each record links back to its thread anchor.
|
||
Live sessions additionally put a six-tab DevTools inspector above these
|
||
sections: revision timeline, complete browser-safe company state, structural
|
||
diff, protocol records, runtime, and authority. The inspector can pause live
|
||
following, export redacted JSON, and fork a retained revision.
|
||
|
||
The panel is collapsed by default and resizable between 320px and 640px with a
|
||
keyboard-operable splitter. Below 1100px it becomes an overlay sheet that
|
||
Escape dismisses; below 768px the page switches to a `Thread` / `Evidence`
|
||
segmented control, with Stop kept outside the `⋯` menu while a turn is active.
|
||
Closing the panel by either route hands focus back to the visible control that
|
||
owns it.
|
||
|
||
- **Replay strip** — in `mode=replay` a progress strip pins under the header
|
||
with `Step back`, `Next turn`, and `Play all`. `?at=<ordinal>` is the single
|
||
source of truth for the parked ordinal, so the three controls and the deep
|
||
link all move the same value; `Play all` advances one ordinal every 800 ms
|
||
and parks itself at the end of the recording.
|
||
|
||
## Routes
|
||
|
||
```text
|
||
#/issue/<fixtureProfile>?shot=<slug>&panel=<section>&rec=<id>&at=<ordinal>&seg=thread|evidence&mode=live
|
||
```
|
||
|
||
- `shot` seeds one of the twelve deterministic `fake` states.
|
||
- `mode=live` opts into the package session server; the default is `fake`.
|
||
- `capture=1` freezes animation, caret, and smooth scrolling for screenshots.
|
||
- The root element sets `data-thread-state="settled"` once hydration, fixture
|
||
load, and auto-scroll finish. Tooling waits for that attribute, never a
|
||
timeout.
|
||
|
||
## Commands
|
||
|
||
```sh
|
||
# Deterministic fake-mode app (no provider process)
|
||
pnpm --filter @paperclipai/paperclip-runner console:issue-thread
|
||
|
||
# Focused browser suite, including the axe gate on all 12 slugs × 2 viewports
|
||
pnpm --filter @paperclipai/paperclip-runner test:browser:scenarios
|
||
|
||
# View-model and live-projection unit tests
|
||
pnpm --filter @paperclipai/paperclip-runner exec vitest run src/issue-thread
|
||
|
||
# Screenshot matrix (12 slugs × 2 viewports) and its byte-stability check
|
||
# Recorded evidence generation is deferred from this release.
|
||
pnpm --filter @paperclipai/paperclip-runner check:capability:ui
|
||
|
||
# Real runnerd + real Codex through the same HTTP routes the browser uses
|
||
pnpm --filter @paperclipai/paperclip-runner smoke:capability:ui
|
||
# Recorded evidence generation is deferred from this release.
|
||
```
|
||
|
||
Hosts without the Playwright chromium system libraries can either run
|
||
`pnpm --filter @paperclipai/paperclip-runner verify:rootless` or set
|
||
`PAPERCLIP_RUNNER_CHROMIUM_PATH` to a preinstalled Chromium.
|
||
|
||
The committed PNGs are pinned to the Chromium build listed in
|
||
`.paperclip-local/evidence/capability/ui/index.md`, so `check:capability:ui` needs that same
|
||
browser. Point `PAPERCLIP_RUNNER_CHROMIUM_PATH` at the recorded browser before
|
||
comparing — and when that path is the agent-browser wrapper, also set
|
||
`PAPERCLIP_CHROMIUM_BIN` to the exact binary, because the wrapper otherwise
|
||
picks the newest installed Playwright Chromium. The issue-thread bundle
|
||
self-hosts its Latin Inter and DejaVu Sans Mono WOFF2 faces plus tiny status-glyph
|
||
subsets, so host fontconfig directories do not participate in capture. The recorder probes the
|
||
package-specific bundled families and refuses to record or compare when either
|
||
face is absent or fails to load; the drift report prints the Chromium version
|
||
and bundled-font probe it recorded with.
|
||
|
||
## Accessibility
|
||
|
||
The suite enforces the contract's blocking gate: axe reports zero serious or
|
||
critical WCAG 2.1 A/AA violations on every screenshot route at both viewports.
|
||
Structure is one `h1`, `header`/`main`/`complementary` landmarks, a `form`
|
||
composer, `section` interaction cards labelled by their prompt, and tool strips
|
||
as disclosure buttons with `aria-expanded`. Every state chip pairs color with a
|
||
glyph and text, all actionable controls clear 44×44 CSS px on mobile, and
|
||
`prefers-reduced-motion` disables the pulse dot, banner slide, and smooth
|
||
scrolling.
|
||
|
||
Focus management (§9.2) is covered by named regressions in the browser suite:
|
||
|
||
- Opening Evidence moves focus to its heading; closing it — with the `Close`
|
||
button or with Escape on the overlay sheet — returns focus to the toggle.
|
||
- Resolving an interaction card moves focus to the card's state chip. The
|
||
controls the user just operated unmount on resolve, so without this the
|
||
keyboard caret drops to `body` at the moment the card changes.
|
||
- The `waiting` composer's anchor moves focus to the pending card's first
|
||
control.
|
||
|
||
## Contract deviations
|
||
|
||
One deviation is recorded against the Capability contract:
|
||
|
||
- **§5 expired-family dimming.** The contract asks for a 60% opacity body on
|
||
`stale_target` and the other expired outcomes. A literal opacity drops that
|
||
card's text to ~3.2:1 and fails the blocking axe gate in §9.7. The dim is
|
||
implemented as a recessed surface plus muted text that still clears 4.5:1.
|
||
|
||
## Determinism notes
|
||
|
||
Fake-mode fixtures render from authored data with a fixed clock, so two captures
|
||
of a slug from a clean checkout are byte-identical. Live sessions are not
|
||
byte-stable — a real model writes their prose — so live evidence lives in
|
||
`.paperclip-local/evidence/capability/ui-live/` and is excluded from the determinism gate.
|
||
Durable comments in live mode carry the mock control plane's own deterministic
|
||
clock rather than wall time, because that is the timestamp on the mock record.
|