Skip to content

Let article drafts carry figures and embeds, approved with the article - #311

Merged
egeozin merged 2 commits into
mainfrom
feat/page-bundles-contract
Oct 5, 2026
Merged

egeozin merged 2 commits into
mainfrom
feat/page-bundles-contract

Conversation

@egeozin

@egeozin egeozin commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

What and why

Page bundles, part 1a: the contract. Plan: https://with.md/s/6bxvmfrh

Today a Tin draft is exactly one Markdown file, and every step after it assumes that: the sandbox runner, publication, the approval token, revisions and delivery. On sheepdogs.io, the report's 14 diagrams and its interactive canvas were dropped at the draft step for that reason.

This PR lets an article draft carry its figures and embeds and have them approved with the article. No workflow turns it on yet. Part 1b enables it for content.public_article and content.generate, together with the reader, the Decisions card and the prompts.

The bundle

A draft whose definition declares output.assets may write files into an assets folder next to the article and refer to them from the Markdown:

content/articles/2026-10-05-1a2b3c4d.md
content/articles/2026-10-05-1a2b3c4d.assets/flow.svg      ![How a request flows](./2026-10-05-1a2b3c4d.assets/flow.svg)
content/articles/2026-10-05-1a2b3c4d.assets/field.html    a tin-embed block with src: ./2026-10-05-1a2b3c4d.assets/field.html
  • The article declares its assets. Tin keeps exactly the files the Markdown refers to. This also works when Tin recovers a run whose sandbox died, and files nothing refers to are never published.
  • Types and limits. SVG and self-contained HTML for now; raster images come with binary support (plan PR 4). Each definition sets its limits, under ceilings of 24 files and 4 MB. Files are capped at 512 KB each.
  • Failing well. A missing, oversized or unsafe file is left out with its reason, recorded as dropped_assets on the persist receipt. An unsafe file is an SVG with script, event handlers, foreignObject or outside references. A left-out file never fails the run.

What changed

  • page_assets.py (new): the folder rule, reference parsing, types, the SVG safety rule, the definition policy, and the approval binding helpers.
  • procedures.py: output.assets is accepted only for article drafts (the content draft and public article validators) and is pinned with the definition. The sandbox context carries the folder and limits.
  • sandbox/run_procedure.sh and procedure_app_server.py:
    • Files directly in the folder count as declared outputs. Anything else in the folder (subfolders, symlinks) is discarded with a note rather than failing the run.
    • Runaway limits and the identity/card leak scan apply to assets.
    • Assets are committed with the article, and the recovery path accepts them.
    • --check-assets lets the host refuse an old image before any model call.
    • The agent's instructions name the folder.
  • publication.py and code_storage.py:
    • The checkpoint records assets as checksummed files; validation keeps them in the article's folder, at the same revision, with matching types.
    • Publication, retries and reconciliation cover them through the existing files list.
    • A checkpoint without assets serializes exactly as before.
  • activities.py: collects the assets at persist, on both the fresh and the recovered path, and rechecks them at publication.
  • Approval:
    • The review artifact, and so the token, binds each asset's path and checksum. Drafts without assets leave the key out, so their tokens don't change.
    • approved_article and approved_document carry the verified asset list to delivery, and their guards rebuild the same record.
  • Revisions: the revision packet's source.assets lists the saved copy's files, and the revision instruction says to copy the ones it keeps into the run's own folder. compare lists each version's assets.
  • Delivery: the wording check from Put approved pages on the site in the site's own format #302 skips tin-embed, tin-video and mermaid blocks, which are figures rather than prose.
  • Docs: a contributor invariant in docs/content-review.md.

Rollout

  • This PR alone changes nothing for running workflows: no definition declares output.assets yet, and drafts keep their exact contract and tokens.
  • The sandbox runner and app server changed, so the Codex templates need a rebuild before part 1b turns the feature on. Finish runs instead of crashing on recoverable checks, and write down how to fail well #298's task-bridge fix is waiting for one too.
  • If part 1b ever ran on an old image, the run would stop at --check-assets before buying a model call.

No migration and no Temporal change.

Verification

  • tests/test_page_assets.py:
    • Reference parsing and the SVG safety rule.
    • The definition: accepted only for article drafts, limits checked, and the sandbox context carries the folder.
    • End to end on the content draft fixture:
      • The draft refers to a good SVG, an HTML embed, an unsafe SVG and a missing file, and also writes a file nothing refers to.
      • Persist keeps exactly the first two, and records the other two with their reasons.
      • Publication writes only those files.
      • The review artifact binds them, and approved-page delivery re-reads them and refuses one that changed.
    • Runtime: the host passes the folder and limits to the runner, and an image without --check-assets fails before any model call.
    • Revisions: the revision packet carries the approved assets.
    • Checkpoints: a saved checkpoint keeps its assets inside the article's folder, at the same revision, with matching types.
    • Delivery: the wording check skips embed and diagram blocks and keeps ordinary code.
  • Full suite: 5,281 passed, 22 skipped against a disposable Postgres. The 2 macOS-only localtime tests fail as on main. ruff, format, lint-imports and dump_catalog.py --check are clean.
  • Runner filter: checked by hand against real paths. The repo's runner tests check its text, not its execution.

🤖 Generated with Claude Code

Page bundles, part 1a (the contract). An article draft whose definition declares
output.assets may write SVG figures and self-contained HTML embeds into its
assets folder (<article>.assets/) and refer to them from the Markdown.

- The article declares its assets: Tin keeps exactly the files it refers to,
  records their checksums with the checkpoint and publishes them with the
  article. A missing, oversized or unsafe file (an SVG with script, event
  handlers or outside references) is left out with its reason; it never fails
  the run. Files nothing refers to are never published.
- The sandbox runner allows files directly in the folder, discards anything
  else there, applies runaway limits and the identity/card leak scan, and
  commits them; --check-assets lets the host refuse an old image before any
  model call.
- The review token binds the asset list with the article; approved-page
  consumers re-check each file; a revision copies the files it keeps into its
  own folder; compare lists each version's assets.
- The delivery wording check skips embed, video and diagram blocks.

No workflow turns this on yet: definitions without assets keep their exact
contract and tokens. The sandbox templates need a rebuild before part 1b
enables it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
)

Page bundles, part 1b (the experience), on top of #311's contract.

- Reader: render_markdown takes the article's assets folder. Figures render
  without a URL and load with the member's session (SVG as an inert data:
  image, as in Files); tin-embed blocks run in an allow-scripts-only sandboxed
  frame with an injected policy that blocks the network; tin-video blocks play
  YouTube (privacy mode), Vimeo, Loom or Mux, a video file loads only on play,
  and anything else stays a link; `> [!NOTE]`-style quotes become callouts. A
  note under the title names any file Tin left out and why.
- The run and Files document endpoints return the kept assets; the app passes
  a session-bound loader to the viewer.
- Decisions: the explanation says what the draft carries ("With 2 figures and
  1 interactive piece.").
- Drafts: content.public_article 1.9.0 and content.generate 1.10.0 (articles
  only) declare an assets folder and use a new page-figures skill: figures only
  where they show what words can't, Mermaid for diagrams, self-contained embeds
  with no network, existing videos only, and kept figures from an existing page.

Co-authored-by: egeozin <ege@emotionmachine.ai>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@egeozin
egeozin merged commit 7c44f31 into main Oct 5, 2026
1 of 3 checks passed
egeozin added a commit that referenced this pull request Oct 5, 2026
# Conflicts:
#	src/tin_lite/content_repository_delivery.py
#	tests/test_page_assets.py
egeozin added a commit that referenced this pull request Oct 5, 2026
* Let article drafts carry figures and embeds, approved with the article

Page bundles, part 1a (the contract). An article draft whose definition declares
output.assets may write SVG figures and self-contained HTML embeds into its
assets folder (<article>.assets/) and refer to them from the Markdown.

- The article declares its assets: Tin keeps exactly the files it refers to,
  records their checksums with the checkpoint and publishes them with the
  article. A missing, oversized or unsafe file (an SVG with script, event
  handlers or outside references) is left out with its reason; it never fails
  the run. Files nothing refers to are never published.
- The sandbox runner allows files directly in the folder, discards anything
  else there, applies runaway limits and the identity/card leak scan, and
  commits them; --check-assets lets the host refuse an old image before any
  model call.
- The review token binds the asset list with the article; approved-page
  consumers re-check each file; a revision copies the files it keeps into its
  own folder; compare lists each version's assets.
- The delivery wording check skips embed, video and diagram blocks.

No workflow turns this on yet: definitions without assets keep their exact
contract and tokens. The sandbox templates need a rebuild before part 1b
enables it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Show a draft's figures, embeds and videos, and let drafts use them

Page bundles, part 1b (the experience), on top of #311's contract.

- Reader: render_markdown takes the article's assets folder. Figures render
  without a URL and load with the member's session (SVG as an inert data:
  image, as in Files); tin-embed blocks run in an allow-scripts-only sandboxed
  frame with an injected policy that blocks the network; tin-video blocks play
  YouTube (privacy mode), Vimeo, Loom or Mux, a video file loads only on play,
  and anything else stays a link; `> [!NOTE]`-style quotes become callouts. A
  note under the title names any file Tin left out and why.
- The run and Files document endpoints return the kept assets; the app passes
  a session-bound loader to the viewer.
- Decisions: the explanation says what the draft carries ("With 2 figures and
  1 interactive piece.").
- Drafts: content.public_article 1.9.0 and content.generate 1.10.0 (articles
  only) declare an assets folder and use a new page-figures skill: figures only
  where they show what words can't, Mermaid for diagrams, self-contained embeds
  with no network, existing videos only, and kept figures from an existing page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Deliver a page's figures, embeds, diagrams and videos to the site

Page bundles, part 2, on top of #312.

- content.deliver 1.7.0 and website.change 1.6.0 copy each approved asset
  from the project checkout, now pinned to the approved revision when a page
  has assets, and show it the way the site shows media; Mermaid blocks become
  SVG with the sandbox's offline renderer unless the site has its own diagram
  support; videos and callouts use the site's own components.
- figure_check beside copy_check: approved files byte for byte, videos by
  address or ID, diagrams as new SVG or the site's Mermaid support. Unconfirmed
  holds only Tin's own merge; the PR still opens.
- Limits: one pull request or commit may hold 30 files and 2 MB (it held 10
  files and 512 KB, which also refused technical batches the procedure allowed
  at 20 files); a saved patch reads back up to 4 MB. Procedures that declare
  nothing keep their 512 KB contract.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Show a draft's figures, embeds and videos, and let drafts use them (#312)

Page bundles, part 1b (the experience), on top of #311's contract.

- Reader: render_markdown takes the article's assets folder. Figures render
  without a URL and load with the member's session (SVG as an inert data:
  image, as in Files); tin-embed blocks run in an allow-scripts-only sandboxed
  frame with an injected policy that blocks the network; tin-video blocks play
  YouTube (privacy mode), Vimeo, Loom or Mux, a video file loads only on play,
  and anything else stays a link; `> [!NOTE]`-style quotes become callouts. A
  note under the title names any file Tin left out and why.
- The run and Files document endpoints return the kept assets; the app passes
  a session-bound loader to the viewer.
- Decisions: the explanation says what the draft carries ("With 2 figures and
  1 interactive piece.").
- Drafts: content.public_article 1.9.0 and content.generate 1.10.0 (articles
  only) declare an assets folder and use a new page-figures skill: figures only
  where they show what words can't, Mermaid for diagrams, self-contained embeds
  with no network, existing videos only, and kept figures from an existing page.

Co-authored-by: egeozin <ege@emotionmachine.ai>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: egeozin <ege@emotionmachine.ai>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

2 participants