Document apps
Agent-built HTML apps that live on a Seed document, sync like any file, and read Hypermedia as the viewer. Working note, 2026-10-20.

Document apps

Working note for PR #1262 (feat/document-apps, draft), stacked on #1139 (chat widgets), which now targets main since the integrated browser (#1138) merged on 2026-10-20.

Problem

On #1139 an app the agent builds is a session-private attachment (application/vnd.seed.app+json), readable only by the session owner and served from an ephemeral loopback listener. It cannot be shared: there is no document, no CID, nothing another desktop could sync.

Decision

An app is a UnixFS folder with an index.html referenced from a document's metadata. Sharing the document shares the app: regular document sync plus IPFS file fetch bring the folder to the other desktop.

    Metadata field app on the document: ipfs://<cid> of the folder (a bare UnixFS file is accepted too and treated as index.html). Documented in hypermedia/metadata.md and hypermedia/apps/document-apps.md.

    Origin per document, seed-app://<digest of the hm id>/ on desktop, so localStorage survives app revisions but no two documents share state. The author controls both the document and its app, so that is the right trust boundary. Agreed with Eric 2026-10-20.

    Sandbox: the app is untrusted code once it can be shared. default-src 'self': it loads only its own folder; no network, no daemon, no secret, no parent access. Its one channel is the bridge.

    Bridge: /.seed/client.js, served inside the app origin, defines window.seed (promises over postMessage): context(), resource(hmUrl), directory(hmUrl, {all}). The embedding Seed window answers with the viewer's client, so an app sees exactly what the viewer can see. Reads only in this pass; writes will need a consent prompt like browser actions.

    Agent: the apps tool publishes by default. It uploads the folder and writes the document's app attribute (create or update in place, folder blocks in the same publish) at the document address given, or else at a document in the signing space named after the title. preview: true, or no Publish grant, gives the chat preview instead.

What landed (PR #1262, four commits)


    Daemon backend/hmnet/filemanager.go: GET /ipfs/{cid}/{path...} walks UnixFS directories, types entries by extension, serves a folder as its index.html; the browser gate treats these reads as public like a bare blob. Test TestGetDirectoryEntries.

    Shared and desktop: @shm/shared/document-app (scheme, protocol, client script, message guard, app CSP, platform hook); directoryToIpfsBlobs in @seed-hypermedia/client (codec-correct dag-pb CIDs, the memory blockstore returns raw ones); desktop app-document-apps.ts (privileged scheme, handler backed by the daemon, document-app-open IPC), frame policy and CSP admit seed-app: frames; DocumentAppView in @shm/ui/document-app, embedded by the resource page when not editing; web /hm/api/file/<cid>/<path> passthrough.

    Agents: collectDocumentAppFiles (folder or single HTML, dotfiles skipped, 256 files / 16 MiB, index.html required) and publishDocumentApp in api-service.ts; tool contract updated with the bridge API.

    Docs: hypermedia/apps/document-apps.md, metadata row, links from apps and desktop pages.

Verified: Go hmnet and daemon gate tests; vitest client, shared, desktop (full suite 1379); bun agents 230 incl. a service test that publishes a folder as a document, updates it in place, and refuses without the Publish grant; typechecks for desktop, ui, shared, client, web, agents. Not yet eyeballed in the running app.

Why the first live try failed (2026-10-20 afternoon)

Eric asked Clever Hare (gpt-6-astra, grants incl. apps and publish) for a full-screen 2048 app in the dev agents at :3051. The transcript shows the agent never called apps at all: it read ~/tools/write/memory and ~/tools/execute, wrote ~/memory/apps/2048.html, tested the logic with execute, and replied with [Play 2048](~/memory/apps/2048.html), a private path nobody can open. Two causes:

    The :3051 server runs ~/Code/Seed/agents (bun --hot src/main.ts), and that checkout is on feat/integrated-browser at e6afa2d1e: it has none of #1262. Even a correct apps call there makes a session preview.

    Model behaviour: the apps tool was in the grants but the model went to memory and never looked at ~/tools/apps.

Fixes in #1262 (second push): apps publishes by default when the agent may publish (document in the signing space named after the title; preview: true opts out); a memory write of an HTML file by an agent with apps now answers with the next step ("call apps with this path… never link ~/memory paths"); and the Agent Guide's memory section says the same. To try it on :3051, run the agents server from the worktree or move ~/Code/Seed to feat/document-apps (that checkout has 29 modified files, so Eric's call).

Open

    Eyeball on desktop: open a document with app set, confirm the frame loads over seed-app://, seed.context() answers, localStorage persists across an app update.

    Web embed: hyper.media needs a sandbox origin (or an opaque-origin sandbox="allow-scripts" iframe) before it can run apps; today it shows a note to open the desktop app.

    Bridge writes (comments, document edits) behind a consent prompt.

    frame-navigation.ts on #1139 is dead code since the browser's installWindowGuards replaced the inline handlers; drop it in #1139.

    Should the web pane (the browser tab UI) also accept seed-app:// URLs, so an app can open "full screen" in a tab? Today the app replaces the document body instead.

Log

    2026-10-20: branch created from the rebased #1139 head; note opened. Same day: #1138 merged, #1139 rebased onto main and retargeted; all four pieces implemented, tested, pushed as draft #1262. Afternoon: first live try showed the agent never calling apps; publishing made the default, write hint and guide line added.

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime