Skip to content

feat(sandbox): serve the content protocol from the sandbox daemon - #7770

Closed
tlgimenes wants to merge 10 commits into
feat/blocks-v8-supportfrom
feat/sandbox-content-protocol
Closed

tlgimenes wants to merge 10 commits into
feat/blocks-v8-supportfrom
feat/sandbox-content-protocol

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What

In a sandbox session of a Blocks v8 project, the site editor now reads and saves content through the sandbox's own daemon. The daemon works on the sandbox's working tree, so content behaves the way it does on a developer's machine. It lives in .deco/blocks/*.json, is edited over the content protocol, and is committed and pushed with git like code. The sandbox never uses a CDN draft. Previews still show the sandbox's dev server.

Daemon (Go): 7d95845e4

  • POST /_sandbox/rpc serves the content protocol: JSON-RPC 2.0 with describe, schema.get, blocks.list and blocks.apply. PUT /_sandbox/assets/<name> takes uploads, capped at assets.maxBytes. Both routes require the daemon token.
  • It is a port of the TypeScript server in @decocms/blocks/protocol, with the same file layout, file-name rules, error codes and shapes, and plaintext-secret guard (including the legacy v7 secret-loader exemption). The bytes it writes match the TS server's exactly.
  • Writes take the worktree lock and then .deco/.blocks.lock, and reuse the existing write hook. That keeps the v7 /_sandbox/decofile and its change events up to date. The lock file and transaction folders are added to .git/info/exclude. /health is not touched.
  • CI: daemon-e2e runs the published conformance suite from @decocms/[email protected] (pinned) against the daemon, so any drift fails the build. Results: 79 pass, 0 fail.

Studio: 67123890d

  • API. Two new routes, POST /api/:org/sandbox/:id/:branch/rpc and PUT …/assets/:name, proxy to the daemon. They use the same claim and auth as the other sandbox routes. A sandbox-less session gets 404.
  • Editor. New content source sandbox, behind the site_editor_content_protocol org flag.
    • Once the working tree is ready, a probe asks the daemon for its schema. A schema with "blocksMajor": 8 switches the editor to the protocol. Anything else stays on the legacy path, as before: a v7 site, or an older sandbox image that lacks the route.
    • Asset uploads go to the daemon, and image thumbnails load from the sandbox's dev server.
    • Each save refreshes the git status in the header.
  • v7 sites are unchanged, including /_sandbox/decofile.

Review fixes: c707d3dc9

  • v7 unchanged in the editor. A sandbox session is legacy unless the flag is on and the daemon confirmed a v8 working tree; it is never pending, so a v7 sandbox's editor isn't unmounted while the flag or the probe loads. A daemon that answers 404 on /_sandbox/rpc (an older image) is read as v7 and not polled.
  • No late writes. Protocol commits and uploads wait at most 10 s for the working-tree lock (a long publish, rebase or autosave), then answer Unavailable with retryAfterMs: 500. That is well under Studio's 30 s proxy timeout, so a write never lands after the editor reported it failed.
  • Stays inside the tree. If .deco, .deco/blocks or public/assets resolve through symlinks outside the working tree, every method except describe answers NotFound, and uploads are refused.

Stack

#7796 (org_sites project link, base: main) → #7728 (blocks v8 support) → #7770 (this PR) → #7766 (hosted)

Based on feat/blocks-v8-support. Nothing here depends on the hosted branch: the daemon route, the proxy routes and the editor's sandbox source only need the content protocol client that #7728 already has. #7766 sits on top of this PR (it merged this branch).

Merged #7728 with #7796 in d59b05f5f. One conflict, in sandbox-proxy.ts: main renamed the suggest-commit body cap to JUDGE_REVIEW_MAX_BODY_BYTES (its /git/suggest-commit route is gone); this branch's content-protocol caps are kept. Then merged #7796's review fixes via #7728 in 300678c19 (no conflicts). Then merged #7796's PO decisions via #7728 in a39b13c80 (no conflicts), and its comment tidy-up in e1a9d31e4 (no conflicts).

OPEN

  • While a v8 sandbox is booting, the editor shows the legacy path until the working tree is ready and the probe confirms v8, then switches to the protocol.
  • Symlink containment is stricter than the TS fs storage, which follows symlinks.
  • A busy-tree upload answers 500 (Internal), like any other failed upload in the TS handler.
  • The CDN-draft bridge (fastPreviewHostedDraft, hosted/draft-git-compat.ts) lives on feat(site-editor): hosted Deco CMS v8 — publish to CDN, CDN drafts, releases, site tokens #7766 and is kept there pending a PO decision. It is only reached by sandbox-less (cms) sessions, never by a sandbox session.
  • The daemon's describe reports the server version as "unknown".
  • Two error messages differ in wording from the TS server: the message on an invalid-json diagnostic, and filesystem errors. The error codes are the same.

Tests

  • go vet, go test -race, and the daemon conformance and parity e2e: green.
  • After the review fixes: go test ./... green (new: busy tree refused with nothing landing later; symlinked storage refused); all daemon-e2e specs 303 pass, 1 skip, 0 fail (conformance + parity included); Studio bun run test 10480 pass, 0 fail; web typecheck, oxlint, biome format pass; e2e (one worker) content-protocol-deco-serve, sandbox-drawer-site-editor, site-editor-breadcrumbs: 11 passed.
  • CI: sandbox-daemon.yml only triggers on PRs into main, so it was dispatched on this branch: https://github.com/decocms/studio/actions/runs/37640092180 (success: daemon-e2e, docker-smoke).
  • api: sandbox-proxy.content.test.ts checks that the JSON body is forwarded, that the daemon's errors pass through, that asset bytes and content type are forwarded, and that a sandbox-less session gets 404.
  • web: new cases in content-backend.test.ts and use-content-backend.test.ts.
  • Typecheck, lint, format and knip pass.
  • e2e, with one worker: content-protocol-deco-serve, content-protocol-github, sandbox-drawer-site-editor, site-editor-breadcrumbs, standalone-blocks-panel, tab-error-boundary-recovery, destination-routes and compact-page-layout. All pass except compact-page-layout, which fails the same way on feat/blocks-v8-support.

🤖 Generated with Claude Code

tlgimenes and others added 2 commits October 7, 2026 11:06
…rpc)

The sandbox daemon now serves the Deco content protocol over its working
tree, the way `deco serve` does on a laptop: content lives in
<app root>/.deco/blocks/*.json and is committed and pushed like code, with
no CDN draft.

- internal/content: a Go port of @decocms/blocks/protocol (server, fs
  storage, keys, secret guard incl. the legacy v7 secret-loader exemption,
  asset uploads). It writes the same bytes as the TS server, so it has its
  own JS-compatible JSON (property order, number formatting, escaping, lone
  surrogates) and UTF-16 string ordering.
- POST /_sandbox/rpc and PUT /_sandbox/assets/<name>, both behind the
  daemon token. Commits and uploads take the worktree lock, then
  .deco/.blocks.lock; reads take no lock and nothing touches /health.
- Every write goes through the fs routes' hook, so file-changed, the
  decofile version and branch status follow. /_sandbox/decofile (v7) is
  unchanged.
- .deco/.blocks.lock and .deco/.tx-*/ go into .git/info/exclude.
- daemon-e2e runs the published conformance suite (pinned
  @decocms/blocks 8.1.0-next.6) against the daemon, with and without a
  schema, and a byte-for-byte parity run against the reference server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ndbox/rpc

A sandbox session of a Blocks v8 project now reads and saves content through
the sandbox daemon's content protocol, the same way the editor talks to a
local `deco serve`: the working tree's `.deco/blocks`, committed and pushed
with git like code. Previews keep showing the sandbox's dev server.

- api: `POST /api/:org/sandbox/:id/:branch/rpc` and
  `PUT .../assets/:name` proxy to the daemon's `/_sandbox/rpc` and
  `/_sandbox/assets/<name>` with the same claim and auth as the other sandbox
  routes; `proxyDaemon` can forward a buffered binary body.
- web: a `sandbox` content source. Behind the `site_editor_content_protocol`
  org flag, once the working tree is there, a probe of the daemon decides:
  a `"blocksMajor": 8` schema uses the protocol, anything else (v7, or an
  older image without the route) stays legacy. Asset uploads go to the
  daemon; saves refresh the header's git status.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions github-actions Bot added the claude PR authored by a coding agent label Oct 7, 2026
…e-lock wait; stay inside the tree

- site editor: a sandbox session is `legacy` unless the flag is on and the
  daemon confirmed a v8 working tree — never `pending`, so a v7 sandbox's
  editor isn't unmounted while the flag or the probe loads. A daemon that
  answers 404 on /_sandbox/rpc (an older image) is v7, not an error to poll.
- daemon: protocol commits and uploads wait at most 10 s for the working-tree
  lock (a long publish/rebase/autosave), then answer Unavailable
  (retryAfterMs 500) — well under Studio's 30 s proxy timeout, so a write
  never lands after the editor reported it failed.
- daemon: .deco, .deco/blocks and public/assets resolving through symlinks
  outside the working tree answer NotFound (OPEN: stricter than the TS fs
  storage).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@tlgimenes tlgimenes changed the title feat(sandbox): v8 content over the daemon's /_sandbox/rpc feat(sandbox): serve the content protocol from the sandbox daemon Oct 7, 2026
Brings in the org_sites project link (#7796) and main. Conflict in
sandbox-proxy.ts: main renamed the suggest-commit body cap to
JUDGE_REVIEW_MAX_BODY_BYTES; kept that plus this branch's content caps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes added a commit that referenced this pull request Oct 8, 2026
Makes the Studio stack one line: #7796 -> #7728 -> #7770 -> #7766.
Conflict in content-protocol-api.ts (applyProtocolPatch doc): kept both the
hosted CDN-draft and the sandbox working-tree notes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
@tlgimenes

tlgimenes commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Split into small PRs; same code — the top of the new stack equals this branch exactly (split/44-content-protocol-github-e2e has the same tree as feat/blocks-v8-hosted).

The new stack, all on #7796 (the org_sites ↔ project migration, reviewed and merged first):

This branch is kept for reference.

This was referenced Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude PR authored by a coding agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant