Vancetope — Designer App
A Designer App (
app: designer) is a collection of web designs: A design is a folder of ordinary documents —index.htmlplus any CSS/JS/Assets — and the app displays each design folder as a live preview in a sandboxed iframe. No build step, no framework: Anyone who can write documents can write a design, and an Agent can create it withdoc_writejust as a human can via the editor.The core is the Content Endpoint
GET /brain/{tenant}/addon/designer/content/{appDocId}/{previewToken}/{design}/{path…}: It delivers the files of a design to the browser — authenticated via a short-livedDESIGN_PREVIEWJWT, which is embedded as a path segment in the URL, with a per-request READ check. This allows the preview to run in an Opaque-Origin Iframe (no cookies, noAuthorizationheader, no access to the Vance API) without opening a security hole.
See also: bistromath-system (the other “behavior from documents” approach: app logic instead of static designs) doc-kind-application report-themes (the first “file as web content” path, PDF-only) permission-system (per-request check, project confinement)
1. Purpose and Delimitation
Problem. Designs — landing pages, mockups, style studies — are currently HTML/CSS/JS. In Vance, there was no place for them: a workpage renders blocks, an image is static, and “view in browser” meant exporting files.
Solution. An app type whose entire substance comes from the document tree: The app is a folder with _app.yaml (app: designer), each design a subfolder with index.html as its entry point. The app reads the tree live (no registry document), and the browser renders the preview itself.
Boundary with Bistromath. A Bistromath app is a program (views, state, vance.* host API, data access). A design is static web content — there is deliberately no host API access, no cookies, no interaction with Vance. Both are “behavior from documents,” but the trust levels are different: A Bistromath program runs in a controlled sandbox with an API bridge, a design runs in a hard sandbox without.
Not a Website Host. The content endpoint is authenticated and read-only and serves for preview; publishing is done via export or an external host.
2. File and Folder Model
designs/ ← App folder
_app.yaml ← Manifest (designer_app_create writes it)
_index.md ← generated catalog (app_rebuild)
landing/ ← Design = subfolder
index.html ← Entry — without it, the folder is not a design
style.css, script.js ← ordinary documents, relatively referenced
design.yaml ← optional: title, description (editable per design)
assets/logo.png ← Binary documents are delivered with their MIME type
- A design is the first path level under the app folder. Names with a leading
_are reserved (manifest, artifacts) and never designs. - Folders without
index.htmlare not designs — no error, they remain in the tree and do not appear in the app. design.yaml(title/description) is metadata display, leniently parsed; a broken file does not hide the design.- All files are ordinary documents:
doc_write, upload, locking, history — the Designer does not add a second file system. - Design order is in the manifest (
config.designer.order) — written by drag-and-drop from the catalog list, read by the scan: listed designs first in list order, the rest alphabetically. The scan reads it leniently; a broken manifest falls back to alphabetical order.
3. The Preview: Sandbox + Path Segment Token
3.1 Why the Token is in the Path and Not in the Query
The Preview iframe runs with sandbox="allow-scripts" — Opaque Origin. This is the security boundary: no cookie access, no Authorization header, no way back to the host page. The price: The browser no longer automatically authenticates sub-resource requests (style.css, images, fonts) — and a query parameter (?token=…) does not survive the first relative URL resolution of the design.
Therefore, the token is as a path segment before the design:
GET /brain/{tenant}/addon/designer/content/{appDocId}/{previewToken}/{design}/{inner…}
css/style.css, resolved relatively from .../{design}/, ends up again within the same token prefix. This is the only reason for the URL form — it is not a convention, but the condition for relative sub-resources to work.
3.2 The DESIGN_PREVIEW Token Type
- Minting:
POST /brain/{tenant}/addon/designer/preview-session?projectId&folder— normally authenticated (Cookie/Bearer), READ on the app manifest is enforced. Response:{token, expiresAt}. - Claims:
tid(Tenant),pid(Project),fld(App folder). TTL default 30 minutes, configurable viavance.designer.preview-token-ttl-seconds(kebab-case, see property rule). - The token type is not accepted as Bearer by any access filter. Only the Content Controller validates it. A leaked token can therefore never open another REST surface — it can only re-read files from that one app folder that its minter was already allowed to read, plus the
style.cssof the Design Skills in the project scope (Skill Style Previews, §4d — the same surface that the token’s catalog already shows), and only untilexpiresAt. - Per-Request READ Check: Each content fetch builds a
SecurityContextfrom the token claims (project-confined viarestrictedToProject— the same attenuation as for integration credentials, see permission-system) and enforces READ on the addressed document. A revocation takes effect on the next fetch, not only when the token expires.
3.3 Validation in the Content Controller (in this order)
- Token: Signature, type, tenant match,
pid/fldpresent — otherwise 404 (the route confirms nothing about existence). appDocIdfrom the URL must be the manifest document that matches thefldclaim (tenant, project, path) — otherwise 404. This ensures that a token for App A can never deliver App B.- Path resolution via
DesignerPaths(pure, tested): empty inner path →index.html;..,., backslash, empty segments,/in design name → 404. The composition from validated segments guarantees that the resulting path remains within the design folder. - Load document by path — 404 if unknown.
- Enforce READ (per-request, see above).
- Deliver with
storageId-ETag +Cache-Control: no-cache(same freshness rule as the Document Content Endpoint: edit visible on next reload, unchanged file a cheap 304) andX-Content-Type-Options: nosniff. MIME correction on delivery for the two preview-critical extensions:.html/.htmis served astext/html,.cssastext/css— regardless of the stored MIME (historicaldoc_writekind default error:index.htmlrendered astext/markdownwithnosniffas source code). All other files retain their stored MIME;designer_validatecontinues to report the discrepancy so that the data can be fixed at the source.
Filter Exception: BrainAccessFilter exempts the route from the Bearer requirement (like WebDAV/Office callbacks: controller-side auth). The exception is limited to exactly this path form.
3.4 What the Sandbox Makes Secure (and What It Deliberately Cannot Do)
- Design code runs, can style itself, animations, canvas, everything — but without
allow-same-originthere are: no cookies, nolocalStorageof the Vance origin, noparentaccess, no Vance REST calls with credentials. A malicious design written by a project member cannot act in the viewer’s session. - External requests (CDN fonts, external images) are technically possible — the endpoint cannot prevent them (the browser loads them directly). The Manual advises local assets; it’s no more than a hint, and no more is needed: Designs are project content from people with WRITE.
- No
allow-forms, noallow-popups, noallow-top-navigation— the whitelist remainsallow-scripts.
4. App Type (VanceApplication)
| SPI Method | Behavior |
|---|---|
create |
Writes _app.yaml (via designer_app_create tool or the Application Template _vance/templates/designer.yaml, app: dispatch — the same call); Config block designer.index.outputPath: _index.md. Seeds a starter design hello/ if the folder doesn’t have one yet (index.html + style.css with relative reference + design.yaml) — the acceptance of the scaffold is something visibly running, not a note about itself (Bistromath Hello World rule); the relative CSS reference simultaneously demonstrates the sub-resource contract of the preview |
refresh |
Scans and writes _index.md (catalog of Markdown links) — readable anywhere Markdown renders |
describe |
🎨 |
status |
Number of designs + first 8 designs as card items |
targets |
Designs as AppTargets (NAVIGATE; Handle = design name) |
promptInject |
Short chunk: folder, design names, authoring method (doc_write), design skill hint, Manual reference manual_read('app-designer') — plus the open design page (see below) |
The per-app prompt injection path exists and is used here as with Links/Feeds (see prompts-and-manuals, ActiveAppPromptResolver).
The open design page travels with the turn. The client reports the design preview as app selection (vance:report-app-selection, like Links/Canvasbook): as selection travels only the design name, everything else (title, file list) the server reads from its own scan — an authority for what a design is, not a second copy that is correct until someone edits the first (same rule as Links: only the URL travels). The permanent reference (SelectionReference): Label = title snapshot, vanceUri = vance:/<folder>/_app.yaml?entry=<design> — roundtrip-capable because the client consumes the same handle as ?entry= (useAppEntry): Click = push, auto-selection/restore = replace, tab switch pulls back the selection. Server-side, appendOpenDesign phrases it as “The design currently open in the preview is landing …” — deliberately not “selection” (chat model reads this as a text area; lesson from Links), lists the files, and names the doc_write path. A stale name (deleted in another tab) silently disappears.
Scan (DesignerFolderReader): lists the app folder, grouped by the first path level, design = group with index.html; design.yaml metadata lenient, order from the manifest. No intermediate state — every view/status/refresh response is a live scan.
4a. Design Mutations (REST)
Four endpoints, all Project-WRITE-enforced, all with the same response: the freshly scanned catalog (DesignerView) — the client replaces its state with a form, and a failed drag resynchronizes from the server instead of recalculating locally. Business logic resides in DesignerApplication (data sovereignty over the folder), the controller is an inbound adapter:
| Endpoint | Effect |
|---|---|
POST …/addon/designer/design |
Creates a design: Entry stub index.html (HTML-escaped title field), plus design.yaml if title/description are given. Invalid name (traversal, reserved) or existing design ⇒ 400 |
DELETE …/addon/designer/design?name= |
Moves every file of the design to the trash — the reconstructible delete, as in the document editor |
POST …/addon/designer/reorder |
Persists the complete order in config.designer.order; names that are not designs are dropped (a stale client catalog must not nail down ghost positions) |
POST …/addon/designer/design-meta |
Upsert of design.yaml: empty values remove their key, foreign keys survive |
4b. Agent Tools (LLM)
| Tool | Effect |
|---|---|
designer_app_create |
Manifest + Seed (see above) |
designer_design_delete |
Moves a design completely to the trash (same as the ✕ button). Deliberately the only design mutation tool: Creation is doc_write — an agent writes real content, a stub tool would have no added value; editing is also doc_write. Deletion, however, would be file-by-file doc_delete with fragmentation risk — that’s worth a tool |
designer_validate |
Static folder check ({ok, errors, warnings, findings[]}, pattern workbook_validate): names what the catalog silently drops — folders without index.html (classic authoring error, invisible in the app), broken design.yaml, ghost entries in designer.order, missing manifest, MIME breaches (stored MIME ≠ extension, e.g., index.html as text/markdown — with nosniff the preview shows raw source code; entry file = Error with re-save hint, other files = Warning). The reader provides scanFull for this (the full picture including dropped states) — a grouping pass feeds catalog and validator, the two views cannot diverge |
The Manual (app-designer.md) is the complete authoring guide: file layout, doc_write workflow, responsive + Dark Mode authoring (prefers-color-scheme reacts to the preview toggle), validate and delete calls. The promptInject names folders, designs, and the validate hint.
4c. Bundled Example Skill
_vance/skills/design-blueprint/ (Classpath layer in the Addon): the demonstration skill — visible, trigger-less, and agent-advertised (enabled: true, no triggers: block, activation: agent): appears in /skill list and in the “Available skills” section of the Turn context, but never activates on a keyword; the agent calls it itself via skill_activate if a brief matches, the user via /skill design-blueprint (Skills Spec §4g — this field is why the agent is allowed to see it despite missing triggers). (Deliberately not enabled: false — that would make it invisible, the loader skips disabled skills in the listing.) It shows the pattern of how Designer Skills carry sample assets:
SKILL.md— Authoring workflow for a landing page in the Designer (create app → load blueprint stylesheet as reference → write actual files → validate).style.css— a complete, usable stylesheet (token set, mobile-first grid with breakpoints,prefers-color-schemeDark Mode), directly in the skill folder next toSKILL.md(formerlyreferences/blueprint.css— the path has been fixed since the style preview convention, see below and §4d). It also remains areferenceDocwithloadMode: ON_DEMAND— reloadable viamanual_read('Designer Blueprint Stylesheet')without costing tokens upfront.
To customize: copy the folder to the Project or _tenant layer and edit (Resource layer is deliberately silent). Also fixed: SkillSteerProcessor.activate now refuses explicit activation of disabled skills with a copy hint (DisabledSkillException) — previously this was possible, contrary to the Skills Spec (“neither explicitly nor implicitly activatable”; the implicit side was already checked in SkillTriggerMatcher). The discovery test (BundledSkillResourceTest) checks frontmatter, the design label, and the asset against the actual Classpath glob pattern — the loader would only skip broken bundled skills with WARN.
Label Convention design: A skill with tag design is a design blueprint — activatable to create designs in the Designer. tags travel into the Skill Panel (SkillSummaryDto), discovery indexes Title/Description/Triggers. The Manual (app-designer.md, section “Design skills”) tells the agent: before designing from scratch, check the Available-skills section in the context (activation: agent) and activate a suitable design skill via skill_activate; the promptInject provides the hint per turn.
style.css Convention: A design skill carries its house stylesheet as style.css directly next to SKILL.md — the same filename that a design folder uses. This is the actual stylesheet file (also a referenceDoc), not a preview-only copy: The Skill Catalog (§4d) renders exactly this file as a live style preview. It is read tier-pinned via skill sibling resolution (SkillLoader.readSkillFile — the same mechanism as referenceDocs, never re-cascaded); if it’s missing, the catalog shows the skill without a preview, it doesn’t hide it.
4d. Design Skill Catalog (REST + Style Previews)
Two endpoints, two trust models as with View/Content:
| Endpoint | Effect |
|---|---|
GET …/addon/designer/design-skills?projectId&sessionId&processName |
Lists the skills visible in the project scope with tag design (cascade-deduplicated, disabled skipped), sorted by title: Name, Title, Description, Version, Source Tier, style (style.css present). sessionId+processName are optional: Only with an open chat does the server join the active status per skill — including fromRecipe (Recipe-bound activation, which ✕ in the UI deactivates; READ on the Think Process — the same resource as for process-skill-WS-LIST). Without chat, active/fromRecipe remain absent, not false — without an active chat, there is no activation status, and “inactive” would be an assertion the server cannot make. A stale Process Handle degrades to “nothing active” instead of failing the entire catalog. LIST-READ on the project covers the metadata |
Both endpoints roughly evaluate: unknown or style-less skill, broken token, wrong pin → 404. With an open chat, the Modal offers the SkillPanel pair: ▶ writes /skill <name> (trailing space for arguments) as Composer Prefill — the user completes and submits themselves, the Composer remains the only activation path — and ✕ performs the same process-skill-CLEAR round-trip (Recipe-bound skills: ✕ disabled). The Remote possesses neither Composer nor Socket for this: The Host provides both as injects (vance:compose-prompt through the expose chain EditorApp → CortexRightPanel → CortexChatPanel → ChatSidePanel → ChatComposer.setText; vance:process-skill-clear wraps the tab-singleton WS with fixed process name chat). The CLEAR response carries the post-mutation activeSkills, the modal updates its badges without relisting.
5. Client Surface
- Kind Registration:
application:designer(Federation Remote@vance-addon/designer,kinds:invance-addon.yamlas load trigger). The host resolves via explicit ID lookup;matches()is intentionallyfalse(as with all app kinds). DesignerApp.vue: Design catalog on the left (name, title, description, file count), live preview on the right. Session minting automatically (also renews 60s before expiration), reload button mints fresh and remounts the iframe. Empty state explains the folder/entry model.- Catalog Operations: “New Design” button (name/title/description, modal), ✎-edit title/description, ✕-delete with confirmation modal (trash hint), drag-and-drop sorting of the list (
vue-draggable-plus, entire card = handle) — each mutation adopts the server response, a failure resynchronizes. - Design Skill Catalog (§4d): Toolbar button “Design Skills” opens a modal: for each skill with tag
design, a small framed style preview box on the left (sandboxed iframe like the main preview, token from the same preview session) and on the right title, name, description, source badge, and — only with an open chat — the active badge plus the SkillPanel control pair: ▶ = Composer Prefill/skill <name>, ✕ =process-skill-CLEAR (disabled for Recipe binding). The chat view comes from the host:inject('vance:session-id')(null in chatless tab) + the injectsvance:compose-prompt/vance:process-skill-clear; without chat, the modal shows a hint instead, not a false “inactive”. - Preview Settings (bar above the preview):
- Color Scheme ◐/☀/☾ —
color-schemeon the iframe element propagates into the browser context of the design:@media (prefers-color-scheme: dark)in the design reacts to the toggle without the design needing to be wired for it. “◐” = like system, “☀”/”☾” enforce light/dark. - Responsive Presets 🖥/▭/▯ — Desktop (fills), Tablet (820 px), Phone (390 px); device rendering is centered with letterbox and shadow in the preview area.
- Rotate ⟳ — portrait/landscape swaps the preset width (Tablet 820/1180 px, Phone 390/844 px); only visible for device presets.
- Color Scheme ◐/☀/☾ —
DesignerAppKind.vue: WS reaction viauseDocumentPrefixReactionon the app folder prefix — remote changes (new design, edited CSS) reload the catalog.- i18n complete (en/de, own namespace
designer.*), no DaisyUI classes outside@vance/components.
6. Files
| File | Role |
|---|---|
vance-addon-brain-designer/.../DesignerPaths.java |
Pure path arithmetic + traversal rules (11 Unit Tests) |
.../DesignerFolderReader.java |
Live scan of the app folder |
.../DesignerPreviewTokenService.java |
Mint/Validate of DESIGN_PREVIEW tokens |
.../DesignerContentController.java |
view, preview-session, Content Route (per-request READ, ETag, 404 coarsening), design-skills, skill-preview |
.../DesignerSkillService.java + DesignerSkill(List).java |
Design Skill Catalog: Tag filter, style.css convention, demo body rendering; DTOs generated to client/src/generated/designer/ |
.../DesignerApplication.java |
App SPI: create/refresh/describe/status/targets/promptInject |
.../DesignerAppCreateTool.java |
designer_app_create — only writes the manifest |
.../templates/designer.yaml |
Application Template (Create Dialog): app: designer dispatch, title/description fields |
.../_vance/skills/design-blueprint/style.css |
Example asset of the style.css convention (formerly references/blueprint.css) |
vance-shared/.../TokenType.java, VanceJwtClaims.java, JwtService.java |
DESIGN_PREVIEW type, fld claim, Mint/Parse |
vance-brain/.../BrainAccessFilter.java |
Route release (controller-side auth) |
vance-brain/.../SecurityContextFactory.java |
fromDesignPreviewClaims — project-restricted context |
vance-brain/.../SkillLoader.java (readSkillFile) + SkillResolver |
Tier-pinned sibling read for skill assets — same rule as referenceDocs |
client/src/* |
Federation Remote: DesignerApp, api (URL builder with path token), Skill Catalog Modal, i18n, Tests |
7. Open / Later
The points that this step deliberately does not solve (Step 2+ of the “Designer”):
- Preview Thumbnails (as OpenDesign shows them): Catalog cards currently show metadata, not miniature renderings. An image per design would be a
design.yamlfield or a server-side rendered poster — open. - Per-Design Manuals/Prompt Fragments beyond the app content.
- Skills with Design Assets — implemented: the
style.cssconvention (§4c/§4d) turns the already existing sibling file capability (referenceDocs) into a design skill convention with live style preview. The inverse case remains open: a skill that also makes its structural examples (entire HTML sections) previewable. - Export/Publish a design as a ZIP or to an external host.