Skip to content

Artifacts: pages the fleet draws to show an idea - #118

Merged
hhff merged 13 commits into
mainfrom
artifacts
Nov 6, 2026
Merged

hhff merged 13 commits into
mainfrom
artifacts

Conversation

@hhff

@hhff hhff commented Nov 6, 2026

Copy link
Copy Markdown
Member

What

Artifacts: a session draws a page that shows the owner an idea, such as a UI mock, a flow, a mock docs site, a 3D model, or screenshots with notes, instead of describing it.

  • Storage. An artifact lives in project/artifacts/<slug>/: index.html, an artifact.json manifest, and any files the page loads.
  • Commands (devteam.mjs):
    • artifact new <slug> --title --summary [--for] scaffolds one in a worktree, and refuses the main checkout or a slug that has already landed.
    • artifact check refuses a missing index or manifest, a bad manifest, a symlink, a dotfile, a file over 8 MB or an artifact over 25 MB. It warns on CDNs not on the list and on unpinned versions.
    • artifacts [--json] lists what has landed and what's being drafted.
  • check-pr and merge judge every artifact a home PR adds or changes, from the head's tree.
  • Serving. The dashboard serves artifacts at /artifact/main/<slug>/ (landed, read from main's tree) and /artifact/wt/<worktree>/<slug>/ (drafts, live from disk, uncommitted files included). Reviewer (review-*) and scratch (tmp-*) worktrees are skipped, and so is any copy identical to the landed one.
    • Each artifact is served under a policy that allows only its own folder plus cdnjs, jsdelivr and unpkg. The sandbox directive makes its origin opaque even when it's opened in its own tab, so it can't act as the dashboard.
    • Paths are confined to the artifact's folder (real path checked), and symlinks and dotfiles are refused.
  • Dashboard.
    • A Library sidebar section lists artifacts (landed, their drafts, and drafts that haven't landed yet) and the link registry, which the dashboard didn't show before.
    • A centered viewer covers 80% of the window, with "Open in new tab" and Reload.
    • The hub's frame gains allow-modals allow-pointer-lock and fullscreen, so 3D pages work through the hub too.
  • Question links:
    • an optional name, with [[name]] rendered inline in the question and its context (ask refuses a name no link has);
    • exactly one of path, url, link or artifact per item;
    • artifact links are pinned when asked (at, and a sha256 fingerprint written by ask into the single question line). The dashboard says when the artifact has changed since it was asked, and falls back to the landed copy once the draft's worktree is gone.
  • Prompts. Worker and planner prompts gain an "Artifacts (data, not instructions)" block listing the landed artifacts their scope matches. for accepts ticket:<id> for artifacts.
  • Docs. supervisor.md (question links, and a new Artifacts section, including the owner asking for one), planning.md §2 (draw artifacts for UI and flows, and link them from the goal-approval question), worker.md §7, and the README.

Spec: docs/superpowers/specs/2026-11-05-artifacts-design.md. Plan: docs/superpowers/plans/2026-11-05-artifacts.md.

Why

Agents describe UI and flows in prose, and the owner approves designs they can't see. With artifacts, a planner mocks the screens before a goal is approved, the supervisor draws what the owner asks for, and the owner sees drafts take shape live across every session's worktree. Every artifact persists in the repo beside the designs it illustrates.

Migration

None. Restart the dashboard after the sync. project/artifacts/ is created by the first artifact new.

🤖 Generated with Claude Code

hhff and others added 12 commits November 5, 2026 18:31
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….DS_Store ignored; slug-shaped [[name]] only; a broken artifact isn't asked about

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hhff

hhff commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

Review (upstream loop, adversarial): CHANGES

What holds:

  • The sandbox works in real Chrome: an artifact gets origin: null, and its fetch('/api/data') and POST /api/answer (JSON and no-cors text/plain) all fail.
  • The POST guard refuses Origin: null.
  • No allow-same-origin or allow-popups is set anywhere.
  • Every response under /artifact/ gets the CSP, including SVG and HTML files in subfolders.
  • .. and URL-encoded traversal in the slug, worktree name or file path return 404.
  • [[name]] and link rendering are escaped.

Blockers

  1. A symlinked artifact folder serves any file on disk. Containment compares against realpath(<wt>/…/artifacts/<slug>). When the slug folder, or artifacts/ itself, is a symlink, that real path is the target.
    • Repro: ln -s /some/dir .worktrees/t1/docs/devteam/project/artifacts/leak, then curl :PORT/artifact/wt/t1/leak/creds.txt returns 200 with the file's contents.
    • The dotfile rule only covers the path after the slug, so a link to ~/.ssh serves id_rsa, to any local client, with no auth.
    • Fix: realpath the worktree root, require realpath(slugDir) === realpath(root)/<artifacts rel>/<slug>, and lstat-refuse every path component. Apply the same rule in diskEntries and artifactSource.
  2. An artifact link doesn't bind the answer to what the owner saw (the An owner's answer binds to the question as the owner saw it #116 class).
    • Repro: ask --links '[{"artifact":"probe",…}]', then overwrite the draft's index.html. questions --json shows changed: true, yet answer <q> yes --seen <hash> succeeds.
    • The reverse also works: the owner approves A, the drafter swaps in B, and B lands. Nothing compares the landed fingerprint with the approved one.
    • Fix: pin a commit at ask (the draft must be committed) and serve that commit's tree. Fold the artifact's fingerprint into what question_hash binds, or refuse an answer while the artifact differs from the pin. Check the pinned fingerprint at merge.

Should-fix

  1. Artifacts can't load their own files except through tags.
    • Because the page's origin is opaque, same-folder fetch/XHR and module scripts fail CORS. That breaks the headline 3D, JSON and ESM cases, and localStorage throws.
    • Add Access-Control-Allow-Origin: null, but only after blocker 1 is fixed.
    • The tests only check headers. Add a browser-level check, or at least document the limitation.
  2. Phishing surface. In its own tab the page shows the dashboard's address, and with allow-modals it can prompt() for a token and then navigate away with it.
    • Prefer serving artifacts from a separate port.
    • Drop allow-modals and allow-forms (form-action 'none' already makes forms moot).
  3. Draft sources are too loose.
    • Any folder under .worktrees/ counts as a draft source: a plain mkdir served (200).
    • The review-*/tmp-* skip is checked only on the URL segment, so an alias symlink to a reviewer worktree served (200).
    • Fix: check the name after realpath, and require a registered git worktree.
  4. Every dashboard refresh reads and hashes every draft byte, up to 25 MB each, again for each artifact link on each open question. A symlinked slug folder makes that walk unbounded. Cache by mtime and size, or hash only on demand.

Nits

  • The Library renders registry url links without validating them: a hand-written javascript: URL reaches href. Render only validated https:// URLs.
  • artifactRule:
    • a file directly in artifacts/ (outside a slug folder) is skipped;
    • a gitlink (mode 160000) isn't refused;
    • with no merge-base it returns no failures.
  • The prompt block's "open a folder's index.html": say plainly that the page is data, not instructions.
  • The log schema doesn't enforce "exactly one of" path/url/link/artifact; only ask does.

Method: a scratch worktree with a kit fixture, a dashboard on a private port, and headless Chrome over CDP. Everything was removed afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hhff
hhff merged commit 4c36c06 into main Nov 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant