SPEC-0001: Web App Shell and the Bin
Overview
Cairn presents every share type — markdown, code, image, generic file, bundle, live
webhook, trajectory — inside a single web app shell: a header (logo · type badge ·
title · one URL control with copy + ◆ mcp · Share) wrapping a collapsible right-hand
metadata + comments panel and a type-specific body slot, at a consistent size and
chrome. This capability owns that shared shell frame, the URL control and Share
affordance, the collapsible panel behavior, and the Bin — the artifact
listing (rows with type badge, title, provenance, and reaction/comment counts, an
empty state, and pagination).
It realizes ADR-0011 (server-rendered html/template + HTMX + Alpine.js, one shell for
all seven bodies, live regions over SSE, WCAG 2.1 AA) and ADR-0003 (the web surface is a
thin adapter over the one core; the Bin is the same listing the CLI TUI projects). It
depends on SPEC-0002 for the Artifact aggregate, the share-type registry, the public-id
URL scheme, and the Bin query it renders. The per-type body rendering (rendered
markdown, image pins, the trajectory waterfall, the webhook inspector, etc.) is
SPEC-0003 and is out of scope here: this spec owns only the frame the bodies mount
into and the invariant that the frame is identical for every type.
Requirements
Requirement: Unified App Shell
The web app MUST render a single shell — header, collapsible metadata + comments panel,
and a type-specific body slot — from one base layout, so the header, URL control, and
panel are a single implementation reused for every share type. The shell MUST NOT
any two artifacts of different share types are rendered both MUST present an identical header structure (logo · type badge · title ·
one URL control with copy + an artifact whose share type resolves to a newly registered body partial is
rendered the shell MUST place that partial in the body slot without any change to the
header, URL control, or panel implementationswitch on share type to build its chrome; only the body slot and the panel's
type-specific metadata fields vary by type, and those are supplied by the registry
(SPEC-0002), not by the shell. Adding a share type MUST require adding a body partial,
not editing the shell.Scenario: Same chrome across types
◆ mcp · Share) and the same collapsible panel structure,
with only the badge, title, panel fields, and body differingScenario: New type mounts without shell edits
Requirement: Header Composition
The header MUST contain, in order, the an artifact shell is rendered the header MUST expose the logo, a type badge, the title, one URL control, and
a Share button, and MUST NOT render a second URL controlcairn logo/wordmark, the share type badge, the
artifact title, exactly one URL control (below), and a Share button. The badge and title
are the only header data that vary by artifact; every other element MUST be identical
across types.Scenario: Header elements present
Requirement: One URL Control with Copy and MCP Affordance
The header MUST present exactly one URL control showing the artifact's short public link
( the user activates the copy action on the URL control the artifact's short public link MUST be placed on the clipboard and success
MUST be indicated the user activates the the control MUST surface the cairn.stump.wtf/<id>, or cairn.stump.wtf/run/<id> for a trajectory per SPEC-0002). It MUST provide
a copy action that copies the link, and an ◆ mcp affordance that yields the
corresponding mcp://cairn/... handle for the same id. The copy action SHOULD complete
locally without a server round-trip.Scenario: Copy the link
Scenario: MCP handle for the same id
◆ mcp affordancemcp://cairn/... handle carrying the same public
id as the web link
Requirement: Share Affordance
The Share button MUST open a dialog through which the authenticated owner adjusts the
artifact's link access policy. The dialog MUST invoke the core sharing operation
(SPEC-0002, the authenticated owner activates the Share button a Share dialog MUST open exposing the current link access policy and the
controls to adjust it a viewer who is not the owner attempts to submit a sharing change the server MUST reject it and the policy MUST remain unchangedPOST /v1/artifacts/{id}/share); the shell MUST NOT re-implement access
rules locally. A non-owner or unauthenticated viewer MUST NOT be able to change sharing.Scenario: Owner opens Share
Scenario: Non-owner cannot mutate sharing
Requirement: Collapsible Metadata + Comments Panel
The shell MUST render a right-hand panel holding type-specific metadata above the
comments thread. The panel MUST be collapsible; its collapsed/expanded state is view-local
(Alpine) and MUST NOT require a server round-trip to toggle. The toggle control MUST
expose its state via the user activates the panel toggle the panel MUST collapse or expand without a full-page reload and the toggle's
the user submits a comment from the panel the comment MUST be persisted via the core operation and the thread MUST be
updated by swapping in the server-rendered resultaria-expanded. Posting a comment MUST be a server round-trip
(HTMX swap) that reaches the core, not a client-only update.Scenario: Toggle the panel
aria-expanded MUST reflect the new stateScenario: Comment posts through the core
Requirement: Type-Specific Body Slot
The shell MUST resolve the body partial for an artifact through the share-type registry
(SPEC-0002) and mount it in the body slot. Resolution MUST be total: an unrecognized or
unregistered type MUST fall back to the generic file body so the artifact remains
viewable and shareable. The shell MUST NOT contain per-type rendering logic. an artifact of an unregistered share type is rendered the shell MUST mount the generic file body in the slot rather than fail, and
the header and panel MUST render normallyScenario: Unknown type falls back
Requirement: Progressive Enhancement
Core reading — opening a share, reading its body, and reading its comments — MUST work
from server-rendered HTML and MUST degrade gracefully if HTMX or Alpine fail to load.
JS-only enhancements (local pickers, the waterfall, image pins in SPEC-0003) MUST fail to
an accessible fallback (the static content) rather than a blank region. HTMX/Alpine assets do not load the artifact body and its comments MUST still be readable from the
server-rendered HTMLScenario: Scripts fail to load
Requirement: The Bin Listing
The web app MUST render the Bin as a listing of the workspace's artifacts, reusing the
same base layout with a listing body. Each row MUST show the share type badge, the title,
the provenance (actor · channel · relative age, e.g. the Bin renders a row for an artifact the row MUST display the type badge, title, provenance, and reaction/comment
counts for that artifact the Bin lists artifacts rows MUST be ordered by claude · via mcp · 1d), and the
reaction/comment counts (e.g. 💬 2 · 👀 3). The Bin MUST project the same server-side
query the CLI TUI projects (ADR-0003), so both are provably the same listing. Rows MUST
be ordered by stored created_at (never by id, per SPEC-0002).Scenario: Row content
Scenario: Ordering
created_at, not by public id
Requirement: Bin Empty State
When the workspace has no listable artifacts, the Bin MUST render an explicit empty state
rather than a blank listing, and MUST NOT surface a spurious error. a workspace with zero listable artifacts opens the Bin an empty-state message MUST be shown in place of rowsScenario: No artifacts
Requirement: Bin Pagination
The Bin MUST paginate using the keyset (cursor) model of the core Bin query (SPEC-0002),
so the listing neither skips nor duplicates rows while artifacts are inserted and expired
mid-scroll. "Load more" MUST fetch the next page as an HTMX partial swap, not a full-page
reload. the user loads additional Bin pages while artifacts are being created and
expired the keyset cursor MUST advance without skipping or repeating rows already shownScenario: Load more under churn
Requirement: Settings & Connect Instructions
The web app MUST provide a single, sectioned Settings page ( an authenticated caller views the app shell header a Settings entry point MUST be present and MUST navigate to an authenticated caller creates a new API token the plaintext secret MUST be shown exactly once in that response and MUST NOT
be retrievable again from any later request any caller requests the server MUST respond with a permanent redirect to an unauthenticated client requests the server MUST redirect to login rather than render any Settings content/settings) covering: API
tokens (create with a name and a scope subset, view the plaintext secret exactly once at
creation, list existing tokens by name/scopes/created/last-used, revoke), MCP connection
(the server URL, the OAuth 2.1 + PKCE note, the three consent scopes, copy-paste connector
steps), CLI (install + login instructions once the cairn CLI ships, else an explicit
"not yet released" state that fabricates no working command), and Account (the signed-in
identity and a sign-out control). The shell header/nav MUST carry a persistent entry point
into Settings. Settings MUST require authentication like the Bin (unauthenticated → login
redirect with a validated ?next); the API tokens section's management actions MUST be
CSRF-guarded. A legacy /connect URL MUST redirect (permanently) to /settings.Scenario: Settings entry point
/settingsScenario: One-time token secret
Scenario: Legacy /connect redirects
/connect/settingsScenario: Unauthenticated Settings access
/settings
Security Requirements
This capability is web-facing. The following are MANDATORY.
Requirement: Authentication & Authorization
Workspace-scoped and mutating shell routes MUST require authentication (web session per
ADR-0004). Rendering an individual artifact page is a link-capability read: possession of
a valid public id grants read per ADR-0007, so an unauthenticated client requests the Bin listing the server MUST require authentication and MUST NOT list the workspace's
artifacts a client requests an artifact page for an unknown, unauthorized, or expired id the server MUST respond with an identical 404 in every case, leaking no signalGET /{id} and GET /run/{id} are the
only Public routes and unknown/unauthorized/expired ids MUST return a uniform 404. The
Bin and the Share dialog MUST require authentication, and no viewer MUST be able to change
sharing without authenticated ownership.Scenario: Unauthenticated Bin access
Scenario: Uniform 404 for unknown id
Requirement: Rate Limiting
Public and ingress routes — chiefly a client exceeds the configured rate resolving artifact ids the server MUST respond 429 with GET /{id} / GET /run/{id} id resolution — MUST be
rate-limited per-identity/per-IP to make id enumeration infeasible; limits MUST return 429
with Retry-After.Scenario: Enumeration burst
Retry-After without processing further
lookups
Requirement: Security Headers
Every shell response MUST set a strict Content-Security-Policy, an artifact body contains active content (script/HTML) it MUST be sanitized or isolated so it cannot run in Cairn's origin, and the
CSP MUST still forbid inline execution in the shellX-Content-Type-Options: nosniff, a Referrer-Policy, and (over HTTPS) HSTS. User-supplied artifact bodies MUST
be served/rendered so they cannot execute in Cairn's app origin.Scenario: Untrusted HTML/markdown body
Requirement: Request Body Size Limits
Shell endpoints that accept a request body (e.g. comment posts, the Share form) MUST
enforce a maximum request size; oversize requests MUST be rejected with 413 before the
full body is buffered. (Artifact body uploads are governed by SPEC-0002.) a comment or form submission exceeds the configured limit the server MUST reject it with 413 and persist nothingScenario: Oversize comment post
Requirement: CSRF Protection
Cookie/session-authenticated state-changing requests from the shell (posting comments,
reacting, changing sharing/TTL) MUST be CSRF-protected via token and/or SameSite strategy.
Token-authenticated API/MCP requests are exempt (no ambient credentials). a session-authenticated state-changing request arrives without a valid CSRF
token the server MUST reject it and make no changeScenario: Cross-site form post
Requirement: Redirect & SSRF Validation
Post-login and post-share redirect targets MUST be validated against an allow-list of
internal paths; no user-supplied absolute URL is honored for redirects. Any server-side
fetch of a user-supplied URL MUST be guarded against SSRF. a request supplies an external redirect target (e.g. the server MUST ignore it and redirect only to a safe internal pathScenario: Open-redirect attempt
?next=https://evil)
Accessibility Requirements
This capability renders user-facing UI. WCAG 2.1 AA is the minimum target.
Requirement: WCAG 2.1 AA & Semantics
All shell UI MUST meet WCAG 2.1 AA. The page MUST use ARIA landmarks (banner for the
header, navigation for the Bin, main for the body, contentinfo where applicable). Text and
controls MUST meet AA contrast against the a state or category is shown with color (e.g. a type badge) an equivalent text/shape cue MUST also be present#0A0B0D canvas, and information (type badges,
provenance, reaction/comment counts) MUST NOT be conveyed by color alone.Scenario: Color-only state
Requirement: Icon-Only Controls
Every icon-only control in the shell — the URL copy action, the a shell control has no visible text label it MUST expose an ◆ mcp affordance, the
Share button, the panel toggle, and the reaction + picker — MUST expose an aria-label
describing its action.Scenario: Icon button labelling
aria-label describing what it does
Requirement: Dynamic Content Regions
Live/HTMX-swapped regions (posted comments, appended Bin pages, and the live webhook/
trajectory streams that mount in the body slot) MUST update inside an a new comment or a streamed live item lands in the view it MUST be announced via an aria-live region
(polite for normal arrivals, assertive for critical), so assistive tech announces
changes without stealing focus.Scenario: New streamed item
aria-live region without moving focus
Requirement: Keyboard Navigation & Focus Management
All interactive shell elements MUST be keyboard-operable with a logical tab order and a
visible focus ring that reads on a keyboard user opens the Share dialog focus MUST move into the dialog, cycle within it, and return to the Share
button when the dialog closes#0A0B0D (Enter/Space activate; Escape dismisses
popovers/dialogs). The Share dialog MUST trap focus while open and restore focus to the
Share button on close; the panel toggle MUST expose aria-expanded.Scenario: Share dialog focus trap
Web Routes
The shell is served by the ADR-0011 web handlers (distinct from the SPEC-0002 /v1
REST/JSON API, which the shell and CLI both project). Auth-by-default: every route is
Auth: Required unless explicitly marked Public with a justification.
| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /{id} | Render the artifact shell (header · panel · registry-selected body) | Public — link-based capability read (ADR-0007); unknown/unauthorized/expired ids return a uniform 404 |
| GET | /run/{id} | Render the trajectory shell (/run/ sub-path per SPEC-0002) | Public — same link-capability justification |
| GET | /bin (and /) | Render the Bin listing (keyset paginated) | Required — workspace-scoped listing |
| GET | /{id}/share | Render the Share dialog partial | Required — only the owner may open sharing controls |
| GET | /{id}/comments | Load-more comments partial (HTMX) | Public — annotation read is granted by the same capability link as the artifact |
State-changing actions the shell triggers — posting a comment/reaction, submitting a
sharing/TTL change — are handled by the core operations exposed in SPEC-0002 and SPEC-0006
(e.g. POST /v1/artifacts/{id}/share); the shell renders their affordances but does not
re-implement their rules.