Skip to content

feat: consented session pages, sandbox surfaces, bundled views and plugin themes - #476

Merged
ackness merged 33 commits into
mainfrom
feat/session-surfaces-and-themes
Nov 1, 2026
Merged

ackness merged 33 commits into
mainfrom
feat/session-surfaces-and-themes

Conversation

@ackness

@ackness ackness commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Summary / 摘要

This PR lets plugins own more of the session screen without reaching past the host. Session pages can request player actions through a consented bridge. Sandboxed pages can replace the main area or the composer, and recover on their own. DM Console, Relationship Graph and Codex move into declarative session views, and plugins can ship restricted theme packs that players opt into per session. Two kernel guards close older gaps: sub-agents read the caller's visible world, and read-derived replacements are rejected when same-key data changed since the read. Each topic arrived as its own merge commit, followed by merges of fixes from a review of the whole set. Review them one merge at a time.

Consented session bridge writes

  • Problem: session pages could only read, so a plugin page that wanted the player to act had to ship its own controls outside the host's consent and validation.
  • Fix:
    • Pages request individually consented player actions, interaction answers, their own manual runtimes, abort and own-panel navigation. Runtime writes still go through approval, proposal validation and commit.
    • Device-local consent binds every requested capability to the complete resolved content, including each published resource. Digest asset URLs verify the actual bytes before success.
    • Ports retire on navigation, content change and revocation. Mutations and approval grants recheck the lease after credential lookup, so a late approval cannot authorize a new visit.

Sandbox surface replacement

  • Problem: a plugin could add panels but not replace the story area or the input area, and a broken page had no recovery short of reloading the app.
  • Fix:
    • Plugins provide independently selected main and composer pages. Host return, Stop, permissions and interaction recovery stay outside the frames, including on narrow screens and in immersive stage.
    • Ready deadlines, fixed page-error signals (uncaught errors, unhandled rejections, failed application scripts) and visible-tab heartbeats retire only the affected region, with an explicit consent-checked Retry. Normal page unload is not an error.
    • Same-session plugin refreshes keep healthy regions, their drafts and the selected page. Disabling one provider retires only its region.

Bundled session views

  • SessionMessages and SessionComposer let declarative views show the public story, pending interactions and the shared composer, with current-turn choice numbering and keyboard selection.
  • DM Console (new, runtime-free), Relationship Graph and Codex get dedicated plugin views. DM Mode and Spectator include DM Console; Ensemble Dialogue recommends it.

Plugin theme packs

  • contributes.ui.theme publishes localized palettes, shared layout presets and CSS compiled from a closed AST allowlist. Backgrounds use content-bound public bitmap resources only.
  • Players choose one pack per session on this device. Themes style narrative text and decorative content only; host recovery and "Use default appearance" stay reachable everywhere. Pending saves survive plugin refreshes, and failures fall back safely. Scene Stage ships an optional reading pack.

Stage renderer follow-ups

  • Departing actors fade out and keep their loaded image throughout. Story templates can show and enlarge the speaker's portrait through CharacterAvatar.

Kernel guards

  • Sub-agent world views: sub-agents read the caller's visible world at admission, including pending writes. Only successful child reads join the caller's commit dependencies.
  • Read baselines: explicit reads capture content baselines, including absence. A replacement derived from them is rejected with conflict if the same key changed before commit. Payload fields cannot redirect the guard, hooks cannot erase it, and vetoing a guarded prefix rolls back the batch. Auxiliary conflicts drop only their unit; story text survives.

Smaller fixes found on the way

  • Session menus (view permissions, theme selection, toolbar) stay inside the viewport on narrow screens, so revoking a page's consent is reachable on phones.
  • On phones, a sandboxed main view keeps a usable minimum height when host notices (interrupted turn, missing narrator, reconnecting) stack above it; the center column scrolls instead of collapsing the page.
  • Approving a plugin after it reloads imports the reloaded module, not a cached earlier one.
  • The public SDK accepts aliases and removeAliases on character upserts, matching the kernel.

Type of change / 变更类型

  • New feature / 新功能 (feat)
  • Bug fix / Bug 修复 (fix)
  • Refactor / 重构 (refactor)
  • Documentation / 文档 (docs)
  • Tests / 测试 (test)
  • Tooling, CI, release / 工具链、CI、发布 (chore / ci)
  • Performance / 性能 (perf)
  • Breaking change / 破坏性变更 (BREAKING CHANGE)

Breaking changes / 破坏性变更

  • Session bridge projection v2. Session pages receive public interactions with per-frame answer handles instead of raw pending blocks. Panel KV, context, jobs and the action/command/event RPC handlers are no longer exposed to session pages. Recreate bridge drafts and update projection consumers. Existing reading grants do not grant the new capabilities.
  • Read evidence for pending work. Cancel or recreate unresolved suspensions, pending or retryable runtime jobs, and checkpoints or snapshots that contain earlier pending proposals.
  • Sub-agent attribution. Recreate unresolved development suspensions and checkpoints when testing the new sub-agent read attribution.

No database schema change or migration is needed. Committed values, characters, sessions and appearance settings stay as they are.

Verification / 验证方式

All on the final branch head, Linux x64, Node 26.12.1, pnpm 12.14.0.

  • pnpm check
  • Tests of the changed packages: every workspace package suite (shared 1129, store 1039, plugin-loader 667, runtime 3128, server 3361, web 2992, and the rest) plus every bundled plugin suite
  • pnpm test:pg against a throwaway PostgreSQL container (96)
  • pnpm e2e:smoke (8), pnpm e2e:extensions (11), pnpm e2e twice: 187/187 both runs
  • pnpm lint, pnpm schemas:generate (no diff), pnpm test:docs, pnpm test:docs:packages, docs build
  • pnpm validate:plugin for the changed bundled plugins and the third-party probes
  • pnpm e2e:verify
  • Packaged desktop runtime and lifecycle
  • macOS ZIP
  • Windows ZIP and self-extracting EXE
  • Dagger endpoint changes: n/a

The theme and sandbox browser specs also passed three repeats in a row. After the phone layout fix, the sandbox 360px spec passed 8/8 and the extension suite twice (22/22) with the browser pinned to two CPUs, the setup that reproduced the CI failure.

Not verified / 未验证:

  • The packaged Electron GUI and restart smoke. This host runs as root with no display. Electron itself built (electron-builder --dir) but refused to start without --no-sandbox.
  • pnpm e2e:verify (real-model harness) was not run. Sub-agent world views and read baselines are covered by scripted-model runtime and server tests only.

Related issue / context / 关联

Follows #475.

Docs sync / 文档同步

  • docs/reference/ updated (ui-panels, ui-components, protocol, api, plugins, plugin-extensions, extension-points, hooks, tools, settings-store, theme-packages, transactions, world-model, world-time, generated schemas)
  • Guides, builtin pages and recipes updated; Chinese pages mirrored under docs/en/
  • Changelog fragments: session-bridge-actions, sandbox-surfaces, bundled-session-views, plugin-theme-packs, stage-renderer-followups, subagent-world-view, proposal-read-baselines, session-menu-viewport, sandbox-main-viewport, plus an update to declarative-plugins
  • AGENTS.md updated: shared owns the pure theme CSS policy and its parser dependency, the loader owns files, web owns rendering

🤖 Generated with Claude Code

ackness and others added 30 commits October 31, 2026 19:11
Sub-agents started through ctx.agents.run and delegate now capture the
caller's visible World Model synchronously at admission, so a child sees
staged characters and schemas instead of only committed state. Child
property reads record producers in an isolated recorder and join the
caller's run only after the child's output passes validation and the
child is still live; failed or cancelled children contribute no edges.

Plugin KV, recursive execution, privacy projections, depth and budget
policies are unchanged.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Session pages that hold a plugin's consent can now submit player actions,
answer pending interactions, invoke their own runtime and stop the current
execution through the host, instead of reading state only.

- Consent covers each requested capability (session read, submit action,
  answer interaction, invoke own runtime, abort, open own panel), keyed by
  the canonical resolved spec including every root resource digest.
- Served plugin assets are verified against the indexed content hash before
  any bytes are emitted, so a same-size edit cannot run under an old digest.
- The bridge projects a version 2 public session with opaque interaction
  handles and minimal input context; no raw panel, runtime or concealed data
  is posted.
- Runtime writes go through the normal runtime RPC and proposal commit;
  action and command RPC are not exposed on this bridge.
- Port replay tracking, request size bounds and a per-connection request
  budget are enforced before any handler runs.

BREAKING CHANGE: session bridge projection is version 2 and replaces the raw
pending block list with public interaction DTOs and handles. Session pages
built against version 1 must be updated. Earlier page consent grants are not
carried over; pages ask again for each requested capability.
When a visible actor leaves the current stage cast, its last displayed
sprite now fades out briefly while the authoritative cast updates at
once. Explicit exit transitions keep their meaning, and reduced motion
removes the fade.

Segment templates can display and enlarge their resolved speaker's
avatar through the CharacterAvatar component, which only receives the
speaker already resolved for that segment. Scene Stage's dialogue
template adopts it.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Replacement writes derived from a read can now record the baseline the
read observed and reject the commit when the stored value has changed.
Suspended and pending runtime jobs persist the baseline evidence with
their saved state.

BREAKING CHANGE: Recreate unresolved development suspensions and pending runtime jobs captured without read-baseline evidence.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/en/internals/security.md
#	docs/internals/security.md
#	packages/runtime/src/function-runtime/world-model-view.ts
#	packages/shared/src/index.ts
Bundled graph and codex pages use the declarative plugin-view contract,
and the DM console composes public story, cast and pending-interaction
widgets with the session's ordinary composer.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Add a strict contributes.ui.theme declaration for plugin theme packs with
a shared scoped-CSS compiler, bitmap-only resource roots, and typed
resolved theme discovery. Themes are isolated from views and webviews and
never receive a bridge or script exception.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/web/src/components/session/plugin-panel.tsx
#	apps/web/src/components/session/plugin-view-host.tsx
#	docs/en/plugins/ui.md
#	docs/plugins/ui.md
#	packages/shared/tests/ui-spec.test.ts
# Conflicts:
#	docs/en/internals/security.md
#	docs/en/plugins/ui.md
#	docs/en/reference/plugins.md
#	docs/internals/security.md
#	docs/plugins/ui.md
#	docs/reference/plugins.md
#	packages/shared/src/index.ts
Replace the session main and composer regions with consented sandbox surfaces that have independent lifecycle, recovery and a protected host return control.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/web/src/components/session/game-view.tsx
#	docs/en/internals/security.md
#	docs/en/plugins/ui.md
#	docs/en/reference/plugins.md
#	docs/en/reference/protocol.md
#	docs/en/reference/ui-panels.md
#	docs/internals/security.md
#	docs/plugins/ui.md
#	docs/reference/plugins.md
#	docs/reference/protocol.md
#	docs/reference/ui-panels.md
…ection updates

# Conflicts:
#	apps/server/tests/api/declarative-plugins.test.ts
#	apps/web/src/components/session/__tests__/plugin-view-consent.test.tsx
#	apps/web/src/components/session/plugin-view-host.tsx
#	docs/builtin/index.md
#	docs/builtin/world.md
#	docs/en/builtin/index.md
#	docs/en/builtin/world.md
#	docs/en/internals/flow.md
#	docs/en/reference/ui-panels.md
#	docs/internals/flow.md
#	docs/reference/ui-panels.md
#	tests/e2e/declarative-plugins.acceptance.ts
#	tests/e2e/ensemble-orchestration.spec.ts
Plugin theme packs could be validated and published, but the web client
never applied them, so a pack had no way to reach a player's session.

Each session can now select a validated theme pack from its appearance
settings. The selection is stored through the existing SettingsStore
helpers and applied only as the compiled, protected style output, never
as raw plugin CSS. Selections are per session, follow session switches
and reloads, and fall back to the default appearance when a pack is
missing, disabled or invalid, with a host recovery control that stays
reachable at narrow widths and in immersive stage mode.

The stage view, dialogs, chat renderer and plugin panels consume the
same protected theme layer, and the bundled scene stage ships a reading
theme as the first adopter. Tests cover selection persistence, theme
fallback, atomic rejection of malicious styles, and that appearance
changes issue no action requests during a running turn.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Departing actors remounted their media when the exit began because the
actor and visual React keys changed. The loaded portrait was discarded,
so it flashed a loading placeholder and could fail to fade out before the
exit deadline. Keep the keys stable across the live-to-outgoing
transition; generation tokens still guard cleanup.

Adds regressions that render the real Media component with a deferred
media resolution to prove the loaded image stays visible during departure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Automatic read-baseline capture derived the guard target from proposal
payload fields spread after the authoritative kind and source plugin, so a
raw proposal carrying extra pluginId or kind fields could move the guard to a
different target than the write it actually performed. Target identities are
now built from explicit namespace and key fields plus the authoritative
source identity, and batch items are handled the same way.

Target extraction also assumed well-formed ops arrays, so a malformed
auxiliary proposal threw during preparation and failed the whole story
execution. Extraction now tolerates invalid shapes and leaves rejection to
ordinary proposal validation, isolating the auxiliary unit.

A PreStateCommit veto replaced a guarded proposal with an error entry and
lost the guard marker, so a vetoed guarded increment could let a derived
merge commit against an unchanged base. Guarded-batch status is now carried
from the original proposals through screening, so the veto rolls back the
whole batch with no writes or success fan-out.

Adds regressions for each case, plus agent suspension/resuspension coverage
and a source-turn scoped retry after a baseline conflict that asserts seed
operations run exactly once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Writes issued by session plugin surfaces could dispatch after consent was
revoked, because the lease check ran before the asynchronous credential
lookup in the HTTP wrapper. The lease predicate now travels with the
request and is rechecked immediately before dispatch, covering runtime
and action writes, interaction submissions and approval grants. Work that
already reached the server still settles.

The composed privacy regression now connects the mounted host adapter to
the real page script and captures every port message, so private output,
concealed descendants and failure results are verified to stay out of the
bridge while public content still arrives. The oversized-interaction test
uses a valid choice list and the real handle issuer, so it fails when the
16 KiB limit is removed.

Failed application-script loads are detected through capturing error
handling on the frame, and the fixed page-error signal recovers only the
affected region instead of leaving an inert replacement mounted.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/web/src/components/session/game-view.tsx
#	apps/web/src/components/session/stage/StageChoices.tsx
#	apps/web/src/components/session/stage/StageView.tsx
#	apps/web/src/i18n/locales/en-US.json
#	apps/web/src/i18n/locales/ru-RU.json
#	apps/web/src/i18n/locales/zh-CN.json
#	docs/en/internals/security.md
#	docs/en/plugins/ui.md
#	docs/en/reference/plugins.md
#	docs/en/reference/protocol.md
#	docs/en/reference/settings-store.md
#	docs/en/reference/ui-components.md
#	docs/internals/security.md
#	docs/plugins/ui.md
#	docs/reference/plugins.md
#	docs/reference/protocol.md
#	docs/reference/settings-store.md
#	docs/reference/ui-components.md
…efreshes

Disabling one provider or changing language reset both session regions
and the selected bundled page, because a temporary loading state was
treated as permanent removal. Numbered choice keys also stopped working
for console choices because the public message wrappers were bypassed.

Healthy, authorized regions now survive same-session discovery refreshes,
and only the affected region retires on confirmed removal. Public turn
wrappers are kept so numbering and hotkeys work for console choices.
Acceptance runs now use the production build under the strict host CSP,
switch sessions through the app for the stale-owner race, and restore
their notebook package during cleanup. Plugin reload approvals keep each
entry's reload generation so approved imports load the reloaded module.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/web/src/components/session/game-view.tsx
#	apps/web/src/components/session/stage/StageView.tsx
A plugin discovery refresh advanced the theme state generations and
cleared the saving flag, so a theme selection or Off write that was
still in flight lost its result. The shown selection could then differ
from storage, and a failed Off write lost its warning about the theme
returning after restart.

Save ownership is now independent of discovery refreshes: the confirmed
choice is reconciled after the write settles and failures are still
reported. Browser coverage combines an active theme with real sandbox
surfaces, consent and revocation at both widths, and checks that private
and concealed content never reaches the page's message port during
streaming, failure, retry and resume.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The combined theme and sandbox browser case forwarded plugin assets to the
fixture host's own port, so the frame violated the host's frame-ancestors
policy, loaded an error page and was retired by the readiness deadline.
Fetch the fixture response and fulfill it at the original app URL, keeping
its bytes and CSP, and assert that one connected frame survives consent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 360px the view permissions menu opened from the right edge of a trigger
near the left screen edge, so its revoke control rendered off-screen.
Shared session menus now cap their width and clamp horizontally to the
viewport gutters, including after resize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reopening the sandbox consent dialog briefly leaves its content without
pointer events while the open animation runs, so an immediate hit test
landed on the overlay. The helper now waits for pointer readiness and
finished dialog animations before the same center hit test, and a dialog
test covers reopening a consent dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ackness and others added 3 commits November 1, 2026 02:27
…ider

The combined theme and streaming privacy case never saw streamed story
text because its fixture plugin lacked the narrative schema its runtime
needs, so the runtime did not load, and the write-service provider shared
a package with the page so refusal and retry could not be exercised.
Declare the schema, split the provider, correct the streamed prefix
expectation and pin the fixture with a server test. Every port message is
still checked for private and concealed content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On narrow screens the interrupted-turn, narrative and reconnect notices
could take the whole center column, collapsing a sandboxed main view to
zero height. The main view now keeps a 20rem minimum, the host composer
sits outside it and the column scrolls. The acceptance test also waits
for Stop to settle before releasing the held player action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from feat/authoring-and-presentation to main November 1, 2026 03:35
@ackness
ackness merged commit a566ead into main Nov 1, 2026
11 checks passed
@ackness
ackness deleted the feat/session-surfaces-and-themes branch November 1, 2026 03:36
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