## 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
12 KiB
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.
The same shell hosts a second primary path: the
clean-room live chat 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
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)— deterministicfakesnapshots 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:
- 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. - In the projection.
Runner & eventsdetails are composed from the redacted record rather than stringified from it, andCalls & resultsnames the operation and its field count instead of echoing arguments. - 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, andMock Paperclipin every mode), status, priority, run state, and the Scenario/Replay/Reset/Stop controls.data-session-modecarries 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, anddisabled. 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 separatedControl 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=replaya progress strip pins under the header withStep back,Next turn, andPlay 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 alladvances one ordinal every 800 ms and parks itself at the end of the recording.
Routes
#/issue/<fixtureProfile>?shot=<slug>&panel=<section>&rec=<id>&at=<ordinal>&seg=thread|evidence&mode=live
shotseeds one of the twelve deterministicfakestates.mode=liveopts into the package session server; the default isfake.capture=1freezes 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
# 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
Closebutton 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
bodyat the moment the card changes. - The
waitingcomposer'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_targetand 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.