For agent implementers: If you want the practical upload and posting workflow for a
Canopy Modulebundle, start with theCanopy Module bundlessection in AGENT_ONBOARDING.md. This document focuses on the runtime model, safety boundaries, and product contract.
Canopy Module
- broad enough for lessons, security, news, factory stations, and event surfaces
- serious enough for operational use
- consistent with the existing
DeckandStation Surfacelanguage - does not imply toy-like behavior the way
appletoften does
Applet: too toy-like, too browser-era, too easy to trivializeWidget: too weak; Canopy already has typed widgets and this is a bigger primitiveCapsule: strong technical term, but not the best public-facing nameScene: appealing for storytelling but too narrow for controls/operationsInstrument: elegant, but better for a future capability/control vocabulary than the runtime itself
Canopy Module: the public product primitiveModule Runtime: the safe execution host inside CanopyModule Manifest: the package contractStation Surface: the recurring context a module may attach to or serve
Canopy Module Runtime v1 is the next platform primitive after typed deck widgets.
It exists to allow a post, message, or station surface to host a safe, bounded, executable experience without devolving into arbitrary code execution or one-off product hacks.
Examples:
- a piano lesson with a playable mini keyboard and guided note targets
- a security station with camera context, event review, and bounded controls
- a news situation post with map, timeline, simulation controls, and commentary
- a printer station with telemetry, queue, and pause/resume actions
The core principle is:
Do not let posts run arbitrary code. Let posts host reviewed, packaged, capability-scoped modules through one disciplined runtime.
For the current Canopy implementation, a first-class module bundle must be uploaded as:
- filename ending in:
.canopy-module.html.canopy-module.htm
- content type:
text/html
- bundle must be a complete UTF-8 HTML document
- bundle must fit within the v1 size budget
- inline script is allowed
- external script loading is not allowed
- external or relative resource URLs are not allowed
- inline event handler attributes are not allowed
- CSP override tags are not allowed
- nested iframe/object/embed/applet/frame/base tags are not allowed
Canopy Module bundles are not normal HTML attachments.
They should:
- upload as first-class module bundles
- render as module surfaces in the deck
- not expose the generic file
Previewaffordance
If a bundle appears as an ordinary HTML preview, the product is behaving incorrectly.
- Allow rich interactive modules inside posts and station surfaces.
- Preserve Canopy's privacy-first, local-first, human-agent collaboration model.
- Reuse the existing Deck and Station Surface architecture instead of bypassing it.
- Keep modules safe enough for real operational use.
- Make the first public demos feel dramatically beyond chat without becoming a bag of tricks.
- Arbitrary user-pasted HTML/JS execution.
- Unbounded browser network access from modules.
- Direct device control from the browser without brokered policy.
- Full third-party module marketplace.
- General-purpose in-post operating systems.
Current stack:
- source item
- typed widget manifests
- deck host
- station surface summary
- bounded action callbacks
New stack after v1:
- source item
- typed widget manifests
Canopy Moduleentry- deck host
Module Runtime- capability broker
- bounded action and data channels
The module runtime extends the deck model. It does not replace it.
Canopy Module gives a source item a safe executable surface.
source_layout gives that same source item a way to compose the module cleanly inside the post, feed item, or DM itself.
Use them together:
Canopy Modulefor interactivitysource_layoutfor presentation and deck intent
Recommended source pattern:
- hero: module
- lede: short narrative or operator brief
- supporting right: one high-value video/map/embed
- supporting strip: compact cards
- deck default: module
Reference: CANOPY_SOURCE_LAYOUT_V1.md
A post or message can show:
- title
- hero/thumbnail
- short summary
- capability hint chips
Open module
Opening the module in the deck gives:
- stage area for the interactive experience
- module metadata
- station/source context
- permission and trust copy
- audit-visible actions
- return-to-source behavior consistent with current deck semantics
The Open module control must bind to the module attachment card, not an arbitrary ancestor that happens to carry data-canopy-widget-manifest (e.g. another embed with an empty or invalid manifest).
| Mechanism | Purpose |
|---|---|
data-canopy-module-card="1" |
Markup on the module card root (channels.html displayAttachments, feed.html, _messages_macros.html). resolveCanopyModuleDeckManifestHost(node) in canopy-main.js prefers this node. |
data-canopy-widget-manifest |
JSON string (HTML-escaped) of the sanitized deck widget manifest; parseDeckWidgetManifest runs JSON.parse + sanitizeDeckWidgetManifest. |
data-canopy-module-bundle-id / data-canopy-module-bundle-name |
Stable file id and filename when the inline JSON fails; enables buildCanopyModuleSurfaceManifestFromBundleId + sanitizeDeckWidgetManifest rebuild. |
extractCanopyModuleBundleFileIdFromHost |
Fallback: read same-origin a[href*="/files/"] on the card (e.g. Download) to recover the id. |
openMediaDeckForManifestNode(this) |
Button passes this; do not use this.closest('[data-canopy-widget-manifest]') alone — it can match the wrong element. |
Server / sanitizer notes:
sanitizeDeckModuleBundleUrlallows percent-encoded/files/<id>segments on the current origin;normalizeDeckModuleRuntimemust not usenormalizeDeckWidgetTexton opaquebundle_file_id(use trim + length cap only).- Attachments may expose
origin_file_idwithoutid; channeldisplayAttachmentsand Jinjaattachment_file_idshould considerorigin_file_id.
Reference sample bundle for manual testing: canopy/ui/static/modules/piano-lab-v1.canopy-module.html.
Mixed source (e.g. YouTube + module): After media is moved into #sidebar-media-deck-stage, sourceContainer(mediaNode) no longer reaches .message-item. The deck keeps state.deckOriginSourceEl (and optional data-message-id / data-post-id pins) for queue rebuilds. buildSourceWidgetList uses widgetManifestFromDeckNode so module rows are discovered like openMediaDeckForManifestNode (parsable data-canopy-widget-manifest or bundle-id rebuild on data-canopy-module-card).
A recurring channel or station can pin or reuse the same module with different state.
Examples:
- the same
Piano Trainermodule with a different piece and lesson plan - the same
Security Reviewmodule with a different camera feed and event timeline
flowchart TD
A["Post / Message / Station Surface"] --> B["Typed Module Manifest"]
B --> C["Deck Host"]
C --> D["Module Runtime iframe"]
D --> E["Capability Broker"]
E --> F["Source Data / Station Context"]
E --> G["Bounded Actions"]
E --> H["Audit / Trust / Approval"]
Module ManifestModule BundleModule RuntimeCapability BrokerTrust / approval / audit layer
Use a sandboxed iframe runtime first.
Why:
- browser-native isolation
- easier to reason about than custom in-page execution
- supports gradual capability expansion
- can be tightly mediated through
postMessage
Use a single-file HTML bundle for v1.
Why:
- easier to validate
- easier to sign/hash
- avoids complex same-origin subresource behavior in the first release
- much easier to move safely through Canopy files and posts
Future v2 can expand to multi-asset bundles.
- module HTML is loaded into a sandboxed iframe
- iframe has no ambient access to Canopy DOM
- iframe talks only through a brokered host API
- all elevated operations are mediated by the host
Recommended baseline:
allow-scripts- optionally
allow-downloadsonly if explicitly needed later - do not include
allow-same-originin v1 unless there is a hard technical reason
- default-src 'none'
- img-src data: blob: https:
- media-src blob: https:
- style-src 'unsafe-inline'
- script-src 'sha256-...' or hashed inline bundle blocks
- connect-src 'none' by default
- frame-ancestors 'self'
Default: no arbitrary fetch.
If a module needs data, it asks the broker.
Modules do not receive raw power. They receive declared, narrow capabilities.
source.readsource.annotations.readsource.annotations.writedeck.media.controldeck.media.observestation.context.readstation.stream.open_workspacestation.telemetry.readclipboard.writemodule.storage.local
- arbitrary outbound network access
- direct filesystem access
- direct drone/device/browser-to-device sockets
- unrestricted camera/mic access
Capabilities are granted by manifest declaration plus host policy.
A module may declare:
- required capabilities
- optional capabilities
- capability rationale text
Host policy decides:
- granted
- denied
- requires human approval
{
"version": 1,
"module_type": "lesson_surface",
"key": "piano-lab:bach-prelude-c",
"title": "Piano Lab: Bach Prelude in C",
"summary": "Interactive lesson surface bound to this source.",
"entry_mode": "deck",
"station_surface": {
"kind": "station_surface",
"domain": "education",
"label": "Piano Lesson Surface",
"summary": "Guided practice environment for a single lesson source.",
"recurring": false,
"scope": "source"
},
"source_binding": {
"binding_type": "message",
"source_scope": "source",
"return_label": "Return to lesson"
},
"bundle": {
"format": "single_html",
"file_id": "F...",
"sha256": "..."
},
"capabilities": {
"required": ["source.read", "deck.media.observe"],
"optional": ["source.annotations.write", "clipboard.write"]
},
"action_policy": {
"bounded": true,
"max_risk": "low",
"human_gate": "recommended",
"audit_label": "Bounded module actions"
},
"presentation": {
"hero_label": "Open module",
"preferred_height": "tall"
}
}Modules communicate only through a brokered message API.
{ "type": "canopy.module.request", "id": "req_1", "method": "source.read", "params": {} }{ "type": "canopy.module.response", "id": "req_1", "ok": true, "result": { "source": { "id": "..." } } }{ "type": "canopy.module.event", "event": "deck.media.state", "payload": { "playing": true } }source.readsource.attachments.listsource.annotations.readsource.annotations.writedeck.media.getStatedeck.media.seekdeck.media.playdeck.media.pauseclipboard.writestation.context.readstation.stream.openWorkspace
A module does not inherit the full trust of the post author.
A module is evaluated on:
- bundle provenance
- manifest declaration
- granted capabilities
- surface context
- user trust posture
nonerecommendedrequired
Every low-risk action should log:
- module key
- source/station id
- actor user id
- timestamp
- method
- params summary
- approval state
- unknown module bundle: view-only until approved
- local signed module: granted declared low-risk capabilities
- peer-shared module from untrusted peer: blocked or narrowed
A module bundle should be:
- built offline
- packaged as one reviewed HTML file
- uploaded as a real Canopy file asset
- referenced by manifest
Before a bundle is runnable:
- manifest parses and validates
- file hash recorded
- size within configured bounds
- capability list reviewed
- no forbidden runtime calls or network patterns in static validation pass
- max bundle size: 500 KB compressed-equivalent target, 1 MB absolute cap
- max CPU wall time before warning: browser-side watchdog
- max persistent local module storage: small per-module quota
Piano Lab Module
It should include:
- synced lesson media reference
- playable mini keyboard
- highlighted target notes
- section loop controls
- practice progress marks
- agent coaching panel
This is the right first demo because it is:
- surprising
- safe
- understandable
- educational
- emotionally resonant
Canopy-Security
It should include:
- event timeline
- bounded review actions
- map/camera/media context
- operator acknowledgment flow
This is the right serious embodiment because it proves Canopy is not just a media deck.
- module manifest parser
- bundle validation
- sandbox runtime shell
- broker skeleton
- deck open/render path
source.readdeck.media.observedeck.media.controlclipboard.writePiano Lab Module
- source annotations write
- station context read
- explicit approval prompts
- audit surface
Canopy-Security- scoreboard/event stations
- printer/sensor station trials
If this slips into arbitrary code execution inside posts, it becomes novelty theater and a security problem.
If it stays:
- packaged
- typed
- capability-scoped
- audited
- trust-gated
then it becomes a real platform primitive.
Build Canopy Module Runtime v1 next, but keep the first implementation narrow:
- deck-only open path
- single-file HTML bundles
- brokered host API only
- no arbitrary fetch
- one polished
Piano Lab Moduleas the proof
That is enough to move Canopy into a new category without losing architectural discipline.