WRAP Format Specification
Version: 0.0.1 (Draft)
Status: Pre-release
Date: 2026-03-12
Abstract
WRAP defines a convention for self-contained, interactive HTML documents with a machine-readable manifest. A conforming wrap is a valid HTML file that any browser can open. The manifest declares metadata, capability requirements, security tier, and extension data — making the document self-describing for humans, tools, and AI.
1. Conventions
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
2. Design Principles
- Valid HTML first. A wrap MUST be a valid HTML document that renders in any modern browser without special tooling.
- Deny by default. Capabilities not declared in the manifest are assumed absent. Security tiers not understood by a viewer are treated as restrictive.
- Graceful degradation. Unknown manifest fields are ignored. Unknown module types are skipped. Unknown security tiers refuse to render. The wrap degrades, never crashes.
- AI-native. The format is designed to be generated, read, and modified by LLMs as easily as by humans.
2.1 Viewer Environments
WRAP defines an artifact format first. A conforming wrap is still a valid HTML document that can be opened directly in a browser.
Different viewers MAY provide different runtime behavior around the same artifact. For example, a hosted viewer MAY add compatibility layers for navigation, networking, or embedded-resource resolution that are not available when the same file is opened directly from disk.
This specification intentionally does not standardize product-specific compatibility tiers, export modes, or runtime analysis labels in v0.0.1. Viewers MAY derive such metadata under extensions, but that metadata is not part of the WRAP core contract unless standardized in a future spec revision.
3. File Identity
3.1 File Extension
The canonical file extension is .html. The extension .wrap is RESERVED for use with dedicated viewers and tooling.
A .wrap file MUST be a valid HTML document that can be renamed to .html and opened in any conforming browser.
3.2 MIME Type
The registered MIME type is application/vnd.wrap (IANA vendor tree). When served over HTTP, servers SHOULD use this MIME type. When opening local files, browsers will treat the file as text/html, which is correct — the file IS valid HTML.
4. File Structure
A conforming wrap MUST follow this structure:
<!DOCTYPE html>
<html lang="{language}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<!-- 1. Manifest (REQUIRED) -->
<script type="application/json" id="wrap-manifest">
{ ... }
</script>
<!-- 2. Content Security Policy (RECOMMENDED) -->
<meta http-equiv="Content-Security-Policy" content="...">
<!-- 3. Styles (OPTIONAL) -->
<style id="wrap-styles">...</style>
</head>
<body>
<!-- 4. Static fallback (RECOMMENDED) -->
<noscript>...</noscript>
<!-- 5. Content root (REQUIRED) -->
<div id="wrap-root">
...
</div>
<!-- 6. Embedded data (OPTIONAL, repeatable) -->
<script type="application/json" class="wrap-data" data-id="{id}">
{ ... }
</script>
<!-- 7. Bootloader (REQUIRED) -->
<script id="wrap-bootloader">
...
</script>
</body>
</html>
4.1 Manifest Element
The manifest MUST be a <script> element with:
type="application/json"id="wrap-manifest"
It MUST appear in the <head> element. It MUST contain valid JSON conforming to the manifest schema (Section 5).
The manifest MUST be parseable without executing JavaScript — tools can extract it by parsing the HTML and reading the script element's text content.
4.2 Content Security Policy
Conforming generators SHOULD include a CSP <meta> tag. The CSP SHOULD, at minimum:
- Restrict
default-srcto'none' - Restrict
script-srcto'unsafe-inline'(required for the embedded bootloader) - Restrict
style-srcto'unsafe-inline' - Restrict
img-srcto'self' data:(for embedded base64 images) - Derive
connect-srcand other domain allowlists from the wrap's actual resource usage (modulesrcURLs, API endpoints referenced in code). Generators SHOULD auto-generate these rather than requiring manual enumeration. - Include
integrityandcrossoriginattributes on all external<script>and<link>elements, using Subresource Integrity (SRI) hashes. This provides browser-native verification that CDN-hosted code has not been tampered with.
If the wrap declares permissions.network: "none", the CSP SHOULD NOT include any external domain allowlists.
Limitations: Because the bootloader is an inline script, 'unsafe-inline' is required for script-src. This means the CSP does not prevent injected inline scripts from executing — its primary value is restricting external resource loading (no connections to undeclared domains, no external script includes). For open tier wraps this is acceptable — the file is plaintext HTML with no stronger guarantee. For signed tier wraps, the signature is the real integrity mechanism, not the CSP. The CSP is a defense-in-depth layer against network exfiltration, not a substitute for signing.
4.3 Static Fallback
A conforming wrap SHOULD include a <noscript> element providing a static representation of the content. This enables:
- Preview in environments where JavaScript is disabled
- Safe mode rendering (content visible without executing code)
- Accessibility for screen readers that may not execute JavaScript
- Search engine indexing
4.4 Content Root
The primary content container MUST be a <div> with id="wrap-root". The bootloader renders interactive content into this element.
4.5 Embedded Modules
Wraps embed module content directly in the HTML file. The embedding format depends on the data type:
Text formats (JSON, CSV) use <script> elements with the appropriate MIME type:
<script type="application/json" class="wrap-data" data-id="revenue">
[{"quarter": "Q1", "amount": 2400000}]
</script>
<script type="text/csv" class="wrap-data" data-id="customers">
name,email,plan
Acme Corp,[email protected],enterprise
</script>
Binary formats (SQLite, WASM, images, fonts) use base64-encoded <script> elements with data-encoding="base64":
<script type="application/wasm" class="wrap-data" data-id="analysis-engine"
data-encoding="base64">
AGFzbQEAAAABBwFgAn9/AX8DAgEABwcBA2FkZAAA...
</script>
<script type="application/x-sqlite3" class="wrap-data" data-id="survey-db"
data-encoding="base64">
U1FMaXRlIGZvcm1hdCAzABAAAQEAQCAgAAAA...
</script>
Compression: Binary content SHOULD be compressed before base64 encoding to reduce file size. Generators MAY use data-encoding="base64+gzip" to indicate that the base64 payload is gzip-compressed. Bootloaders SHOULD decompress via DecompressionStream where available, falling back to uncompressed base64 if the API is not supported.
Media assets (images, fonts, video) MAY use either:
- Base64
<script>elements as above, referenced by module ID - Inline data URIs directly in the HTML content (e.g.
<img src="data:image/png;base64,...">)
Data URIs are simpler for small assets used directly in markup. <script> embedding is preferred for assets managed by the bootloader or referenced by multiple elements.
All embedded module elements share these attributes:
class="wrap-data"— identifies the element as wrap module contentdata-id="{identifier}"— MUST match a moduleidin the manifestdata-encoding="base64"— present for binary formats, absent for text formats
4.6 Bootloader
The bootloader MUST be a <script> element with id="wrap-bootloader". It MUST appear after the content root in document order (so the DOM is available when it executes).
The bootloader is responsible for:
- Parsing the manifest
- Validating the security tier
- Initializing declared modules (embedded and fetched via
src) - Verifying integrity hashes for fetched modules
- Resolving local module URLs for runtime resource loading
- Rendering interactive content into
wrap-root - Exposing declared tools (Section 5.11) as callable functions
- Firing lifecycle hooks
The bootloader SHOULD be as small as practical. For open tier wraps, a minimal bootloader that parses the manifest and initializes content is sufficient.
Reference bootloader: The canonical bootloader for WRAP v0.0.1 is provided at spec/v0.0.1/bootloader.js. A generated minified form may be provided at spec/v0.0.1/bootloader.min.js. Generators SHOULD embed the version-matched bootloader inside the <script id="wrap-bootloader"> element, then append application-specific code after the IIFE. The convenience path spec/bootloader.js MAY point to the latest bootloader, but versioned tooling and generated wraps MUST use the versioned copy for the declared wrap_version. The reference bootloader handles manifest parsing, version validation, security tier checking, module loading (embedded and src-fetched with integrity verification), secrets API setup, and the wrap:ready lifecycle event.
window.wrap is exposed immediately, but for wraps with src modules, application code MUST wait for module fetches to complete before using module data. Two mechanisms are provided:
wrap.ready— aPromisethat resolves when all modules (including async fetches) are loaded. Application code SHOULD usewrap.ready.then(() => { ... })orawait wrap.ready.wrap:readyevent — dispatched ondocumentwhen all modules are loaded. Equivalent to the promise for event-driven code.
For wraps with no src modules (pure offline), both resolve immediately — there is no behavioral change from the synchronous model.
The reference bootloader also intercepts local fetch() requests that target declared modules, and SHOULD expose a helper such as wrap.resolveURL(path) for non-fetch consumers that need a URL string (for example audio players or 3D model loaders).
When permissions.storage is true, the bootloader MUST also initialize wrap.store (Section 7.1) with a functional implementation. The reference bootloader uses localStorage as the backing store. Viewer implementations MAY override wrap.store with enhanced backends (shared state, cloud sync, real-time collaboration) while preserving the same API contract.
The bootloader MUST also initialize wrap.runtime (Section 7.2). If no host runtime is present, wrap.runtime MUST default to a stub that reports available: false, returns an empty capability list, and rejects capability calls with a structured RUNTIME_UNAVAILABLE error.
Features that require platform infrastructure (Ed25519 signature verification, encrypted tier decryption, user prompts for secrets) are deferred to viewer implementations.
5. Manifest Schema
The manifest is a JSON object with the following top-level fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
wrap_version | string | REQUIRED | — | Spec version (semver) |
meta | object | REQUIRED | — | Document metadata |
layout | string | OPTIONAL | "document" | Layout mode |
connectivity | string | REQUIRED | — | Network requirements |
connectivity_details | object | OPTIONAL | — | Hybrid degradation spec |
permissions | object | OPTIONAL | (all denied) | Capability declarations |
security | object | OPTIONAL | {"tier":"open"} | Security configuration |
modules | array | OPTIONAL | [] | Declared dependencies |
data | object | OPTIONAL | — | Embedded data schemas |
secrets | array | OPTIONAL | [] | Required credentials/API keys |
tools | array | OPTIONAL | [] | AI-callable operations |
runtime | object | OPTIONAL | {} | Runtime capability requirements |
annotations | array | OPTIONAL | [] | Structured notes |
extensions | object | OPTIONAL | {} | Third-party metadata |
5.1 wrap_version
REQUIRED. A semantic version string identifying which version of this spec the wrap conforms to. For this version: "0.0.1".
Viewers MUST parse this field before processing any other manifest content. If the major version is unrecognized, the viewer MUST refuse to render and display a message directing the user to update.
5.2 meta
REQUIRED. Document metadata.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | OPTIONAL | Unique identifier (UUID v4 recommended) |
title | string | REQUIRED | Document title |
description | string | OPTIONAL | Human-readable summary |
author | object | OPTIONAL | Author information |
author.name | string | OPTIONAL | Author name |
author.email | string | OPTIONAL | Author email |
author.url | string | OPTIONAL | Author URL |
created | string | OPTIONAL | ISO 8601 datetime |
modified | string | OPTIONAL | ISO 8601 datetime |
language | string | OPTIONAL | BCP 47 language tag (e.g. "en", "ja") |
license | string | OPTIONAL | SPDX license identifier |
intent | string | OPTIONAL | AI-readable description of the wrap's purpose |
tags | array | OPTIONAL | Categorization tags (array of strings) |
generator | string | OPTIONAL | Tool/platform that created this wrap (e.g. "openwrap-cli/0.1.0") |
The intent field is designed for LLM consumption. It SHOULD describe what the wrap does, who it's for, and what data it contains, in plain language. Example: "A weekly engineering metrics dashboard showing deploy frequency, incident rate, and MTTR, filterable by team."
5.3 layout
OPTIONAL. Declares the intended layout mode. Default: "document".
| Value | Description |
|---|---|
"document" | Scrollable content (like a web page or article) |
"presentation" | Fixed-aspect slides (default 16:9) |
"dashboard" | Grid-based layout with multiple panels |
"freeform" | Canvas — no layout constraints |
Viewers MAY use this to adjust chrome, scrolling behavior, and navigation controls.
5.4 connectivity
REQUIRED. Declares the wrap's network requirements. There is no default — generators MUST set this explicitly.
| Value | Description |
|---|---|
"offline" | Fully self-contained. Works with no network. |
"online" | Requires network access to function. |
"hybrid" | Works offline with degraded functionality. Enhanced when online. |
5.5 connectivity_details
OPTIONAL. Additional context for "hybrid" connectivity. Ignored for other modes.
| Field | Type | Description |
|---|---|---|
fallback_behavior | string | Human-readable description of what works offline vs. what requires network |
5.6 permissions
OPTIONAL. Declares what capabilities the wrap requires. All capabilities default to denied. A basic wrap needs no permissions.
| Field | Type | Default | Values |
|---|---|---|---|
network | string | "none" | "none", "fetch", "websocket" |
storage | boolean | false | Persistent state via Web Storage API. Backing implementation is runtime-defined. |
camera | boolean | false | |
microphone | boolean | false | |
geolocation | boolean | false | |
clipboard | string | "none" | "none", "read", "write" |
Permission values are hierarchical where applicable: "websocket" implies "fetch" implies "none". "write" implies "read" implies "none".
Viewers with enforcement capabilities SHOULD block API calls not covered by declared permissions. In standard browsers without a dedicated viewer, these declarations are advisory — the CSP <meta> tag provides the actual enforcement layer.
5.7 security
OPTIONAL. Declares the security tier and tier-specific configuration. Default: { "tier": "open" }.
| Field | Type | Tier | Description |
|---|---|---|---|
tier | string | all | "open", "signed", "encrypted", "remote" |
signature | object | signed | Digital signature data |
certificate | string | signed | Public key or certificate URL |
encryption | object | encrypted | RESERVED |
auth | object | encrypted, remote | RESERVED |
check_in | object | encrypted, remote | RESERVED |
expiry | string | any | RESERVED — ISO 8601 expiry datetime |
revocation_list | string | encrypted, remote | RESERVED |
watermark | object | encrypted, remote | RESERVED |
content_source | string | remote | RESERVED |
render_mode | string | remote | RESERVED |
session | object | remote | RESERVED |
See Section 6 for tier definitions.
Forward-compatibility rule: Viewers that encounter security fields they do not recognize MUST treat them as restrictive. Unknown fields in the security object are never ignored — they cause the viewer to refuse to render. This ensures that future security features fail closed, not open.
5.8 modules
OPTIONAL. Declares the wrap's dependencies — runtimes, data sources, and asset bundles.
Each module is an object:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | REQUIRED | Module type identifier |
id | string | REQUIRED | Unique ID within this wrap |
description | string | OPTIONAL | Human/AI readable description |
version | string | OPTIONAL | Module version (semver) |
required | boolean | OPTIONAL | Default: true. If false, the wrap functions without this module. |
src | string (URL) | OPTIONAL | URL to fetch the module from if not embedded or to use as primary source. Enables CDN-hosted runtimes and shared libraries. |
hash | string | OPTIONAL | Subresource integrity hash of the expected content at src (e.g. "sha256:e3b0c44298fc..."). Format: {algorithm}:{base64-hash}. REQUIRED if src is present. The bootloader MUST reject fetched content that does not match. |
fallback | string | OPTIONAL | What to display if the module fails to load |
Defined module types
| Type | Embedding | MIME type in <script> |
|---|---|---|
runtime/js | Inline script or <script> element | text/javascript |
runtime/python | Base64-encoded WASM binary | application/wasm |
runtime/wasm | Base64-encoded WASM binary | application/wasm |
data/sqlite | Base64-encoded database file | application/x-sqlite3 |
data/csv | Plaintext in <script> | text/csv |
data/json | JSON in <script> | application/json |
data/parquet | Base64-encoded binary | application/vnd.apache.parquet |
media/images | Base64 <script> or data URIs | image/* |
media/audio | Base64 <script> or blob/data URLs | audio/* |
media/fonts | Base64 <script> | font/* |
media/models | Base64 <script> or blob URLs | model/* or application/octet-stream |
media/video | Base64 <script> or data URIs | video/* |
ui/components | Inline HTML/JS | text/javascript |
Module resolution
When a module declares both an embedded <script class="wrap-data"> element and a src URL, the bootloader resolves the content using this priority:
- If the wrap is
offline, use the embedded content.srcis ignored at runtime. - If the wrap is
onlineorhybrid, fetch fromsrc. Verify the response againsthash— reject on mismatch. - If the fetch fails or is rejected, fall back to the embedded content.
- If neither
srcnor embedded content is available, the module fails to load.
For offline wraps, modules MUST be embedded. src MAY be present as a hint for tooling (e.g. a viewer pre-caching shared runtimes), but the bootloader MUST NOT fetch at runtime.
For online wraps, modules MAY omit the embedded content and rely on src alone. This produces smaller files but means the wrap breaks if the CDN is unreachable.
For hybrid wraps, embedding with src gives the best tradeoff — small when online (fetch from CDN), functional when offline (use embedded copy).
Local resource resolution
Bootloaders MUST treat declared embedded modules as addressable local resources at runtime. This is separate from application navigation or router state.
For local resource loading:
- Bootloaders MUST intercept
fetch()requests whose target resolves to a declared module ID. - A "local" target includes:
- relative URLs such as ./data.json
- root-relative URLs such as /assets/sounds/boost.mp3
- same-document-origin absolute URLs that point back into the wrap
- Bootloaders MUST resolve module identity from the URL pathname. By default, the pathname without its file extension maps to the module
id. - Query strings MAY be preserved for author code and helper APIs, but they SHOULD NOT be required to participate in module identity matching.
- Fragment identifiers (
#...) MUST NOT affect module identity matching. - When a local resource request matches a declared embedded module, the bootloader MUST serve the embedded content without a network request.
- For non-
fetchconsumers that need a URL string, bootloaders SHOULD expose a helper such aswrap.resolveURL(path)that returns ablob:URL for matching embedded modules.
Examples:
fetch('./checklist-items.json')resolves modulechecklist-itemsfetch('/assets/sounds/boost.mp3?v=2')resolves moduleassets/sounds/boostloader.load(wrap.resolveURL('/assets/models/f-15.glb'))resolves moduleassets/models/f-15
{
"type": "runtime/wasm",
"id": "sql-engine",
"src": "https://cdn.example.com/[email protected]/sql-wasm.wasm",
"hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"description": "SQLite WASM runtime",
"required": true
}
Generators MAY use custom module types (e.g. runtime/lua, data/arrow). Viewers that encounter unknown module types MUST skip them and render the rest of the wrap. If the module is required: true and the viewer cannot load it, the viewer SHOULD display the module's fallback message or a generic error for that section.
5.9 data
OPTIONAL. Documents the schema of embedded data sources, enabling AI and tools to understand the data without parsing the binary.
"data": {
"schemas": {
"{module_id}": {
"type": "sqlite",
"description": "Quarterly revenue data 2024-2026",
"tables": {
"revenue": {
"columns": {
"quarter": { "type": "TEXT", "description": "e.g. 2025-Q3" },
"region": { "type": "TEXT", "description": "Sales region" },
"amount": { "type": "REAL", "description": "Revenue in USD" }
}
}
}
}
}
}
The data.schemas keys MUST correspond to module id values in the modules array.
Schema detail is intentionally not enforced — a data/sqlite entry with no tables key is valid but unhelpful. Generators SHOULD include as much schema detail as practical: tables and columns for SQLite, fields for CSV/JSON. The purpose of data.schemas is AI and tooling discoverability — a schema entry without structure defeats that purpose. Omitting schema detail is permitted (the data still works), but the wrap loses the "AI-native" benefit of self-describing data.
5.10 secrets
OPTIONAL. Declares credentials the wrap requires at runtime. Secrets MUST NOT be stored in plaintext in the wrap file. The manifest declares what the wrap needs; the runtime provides the values.
"secrets": [
{
"name": "OPENAI_API_KEY",
"description": "OpenAI API key for AI-powered summaries",
"required": true
},
{
"name": "MAPBOX_TOKEN",
"description": "Mapbox token for interactive maps",
"required": false
}
]
| Field | Type | Required | Description |
|---|---|---|---|
name | string | REQUIRED | Secret identifier. Convention: UPPER_SNAKE_CASE. |
description | string | REQUIRED | Human/AI readable explanation of what this secret is for |
required | boolean | OPTIONAL | Default: true. If false, the wrap functions without this secret (with degraded features). |
scope | string | OPTIONAL | Default: "user". Who provides the secret. |
Secret scopes:
| Scope | Meaning |
|---|---|
user | Each viewer provides their own key (e.g. "bring your own OpenAI key") |
org | The organization provides it once, all members inherit (e.g. company API key managed by IT) |
author | The creator's credential, proxied through a platform (e.g. author pays for API usage) |
Scope is advisory in v0.0.1 — it tells the runtime and the user who is expected to provide the secret. A viewer or platform MAY use scope to determine where to look (e.g. org secrets from Vault, user secrets from browser storage). In a plain browser, all scopes resolve through the same priority chain.
Runtimes MAY satisfy a declared secret in two ways:
- Direct injection — the raw value is exposed to wrap code and readable via
wrap.secrets.get(name) - Proxied fulfillment — the runtime or platform uses the secret on the wrap's behalf without exposing the raw value to wrap code
In practice, user secrets are often directly injected, while org and author secrets are often better fulfilled through a proxy. These are deployment choices, not format requirements.
Runtime API: The bootloader MUST expose wrap.secrets.get(name) which returns the secret value or null for secrets the runtime chooses to expose directly. Declaring a secret in the manifest does NOT guarantee the wrap can read the raw value.
Resolution priority for directly exposed secrets: The bootloader SHOULD resolve secrets in this order:
- Viewer/platform injection — if a viewer or platform has pre-provisioned the secret (e.g. from Vault, AWS Secrets Manager, enterprise SSO), use it
- Browser storage — if the user previously entered this secret, retrieve from localStorage under the standardized namespace
wrap:secrets:{name} - User prompt — ask the user to provide the secret, then store to browser storage for next time
Relationship to other spec features:
- Wraps with secrets typically require
permissions.network: "fetch"andconnectivity: "online"or"hybrid" - Tools MAY declare which secrets they depend on via a tool-level
secretsfield - The CSP MUST allowlist any domains the secrets are used to authenticate against
- Enterprise secret management (Vault, KMS, SSO integration) is a runtime/viewer concern, not a spec concern — the resolution priority chain accommodates any source
5.11 tools
OPTIONAL. Declares operations that AI agents or tooling can perform on the wrap. Tools turn wraps from documents AI can read into interfaces AI can use.
Each tool declaration follows the same pattern as MCP tools and LLM function/tool calling (OpenAI, Anthropic, etc.):
"tools": [
{
"name": "add_entry",
"type": "runtime",
"mutating": true,
"description": "Record an inspection result for a checklist item",
"parameters": {
"type": "object",
"properties": {
"item_id": { "type": "string", "description": "Checklist item ID" },
"status": { "type": "string", "enum": ["pass", "fail", "na"] },
"notes": { "type": "string", "description": "Optional inspector notes" }
},
"required": ["item_id", "status"]
},
"returns": {
"type": "object",
"description": "The created inspection entry with timestamp"
}
},
{
"name": "spending_by_category",
"type": "sql",
"module": "db-export",
"query": "SELECT category, SUM(amount) as total FROM transactions GROUP BY category ORDER BY total DESC",
"description": "Returns spending totals grouped by category"
},
{
"name": "summarize",
"type": "sql",
"module": "db-export",
"queries": {
"summary": "SELECT COUNT(*) as count, SUM(amount) as total FROM transactions",
"byCategory": "SELECT category, SUM(amount) as total FROM transactions GROUP BY category ORDER BY total DESC",
"recent": "SELECT * FROM transactions ORDER BY date DESC LIMIT 10"
},
"description": "Returns a compact summary of this wrap's structure, data, and current state"
},
{
"name": "refresh",
"type": "prompt",
"mutating": true,
"secrets": ["GITHUB_TOKEN"],
"description": "Fetch the latest commit data from GitHub and update the wrap",
"instruction": "1. Call the GitHub REST API at GET /repos/{owner}/{repo}/commits?per_page=20 using the GITHUB_TOKEN secret.\n2. Parse the JSON response and extract: sha, commit.message, commit.author.name, commit.author.date.\n3. Replace the contents of the 'commits' data module with the new JSON array.\n4. Update the element with id 'last-updated' to show the current timestamp.",
"parameters": {
"type": "object",
"properties": {
"owner": { "type": "string", "description": "Repository owner" },
"repo": { "type": "string", "description": "Repository name" }
},
"required": ["owner", "repo"]
}
}
]
| Field | Type | Required | Description |
|---|---|---|---|
name | string | REQUIRED | Tool name. MUST be unique within this wrap. |
type | string | OPTIONAL | Execution type: "sql", "json", "document", "runtime", or "prompt". Default: "runtime". |
description | string | REQUIRED | Human/AI readable description of what the tool does |
mutating | boolean | OPTIONAL | Default: false. If true, this tool modifies data (inserts, updates, deletes). If false, the tool is read-only. Viewers SHOULD prompt the user before executing mutating tools. AI agents SHOULD treat mutating tools with higher caution and confirm intent before calling them. |
secrets | array[string] | OPTIONAL | Names of top-level secrets declarations this tool depends on. Each entry SHOULD match a manifest.secrets[].name value. |
parameters | object | OPTIONAL | JSON Schema describing the tool's input parameters |
returns | object | OPTIONAL | JSON Schema or description of the return value |
module | string | OPTIONAL | For data tools: the module ID containing the data to operate on |
query | string | OPTIONAL | For sql type: the SQL query to execute |
queries | object | OPTIONAL | For sql type: named set of SQL queries (for tools that return multiple result sets) |
instruction | string | OPTIONAL | For prompt type: natural language instructions for an AI agent to follow when executing this tool |
Tool types:
A tool's type tells consumers how to execute it:
sql— a SQL query against an embedded SQLite module. Can be executed by any tool that can parse HTML and run SQLite — no JavaScript runtime needed. Themodulefield identifies which data module contains the database, andqueryorqueriescontains the SQL.json— an operation against an embedded JSON module. Can be executed by parsing the JSON data directly.document— extracts structured content from the HTML itself (headings, tables, text). Can be executed by any HTML parser.runtime— requires JavaScript execution. Used for tools that depend on application logic, browser APIs, network calls, or complex computation that can't be expressed as a data query.prompt— natural language instructions for an AI agent. Theinstructionfield contains step-by-step directions the AI follows using its own capabilities. No runtime or data access is required by the wrap itself — the AI is the executor. Use for operations that require judgment, external context, or multi-step workflows that can't be reduced to a query or function call.
The sql, json, and document types are data tools — they can be fulfilled by reading the file directly, without executing JavaScript. The prompt type is an AI tool — it can be fulfilled by any AI agent that understands natural language. The runtime type requires a JavaScript environment (browser, viewer, or headless runtime). This enables CLI tools, MCP servers, and AI agents to interact with wraps instantly, without spinning up a browser.
Implementation: Tool declarations define operation contracts. In self-contained runtimes, application code commonly exposes declared tools as callable functions on a global wrap.tools object (or equivalent). Runtimes and platforms MAY also fulfill declared tools through equivalent host-provided implementations, as long as the declared name, parameters, and returns contract is preserved. For data tools, external tooling MAY fulfill the tool by executing the declared query directly against the embedded data module, bypassing the JavaScript implementation.
Discovery: An AI agent scanning a directory of wraps reads each manifest to discover available tools without executing the wrap. The name, type, description, and parameters fields provide everything needed to call the tool — the same contract as MCP/LLM tool definitions.
Tool naming convention:
Tool names SHOULD follow verb or verb_noun format in snake_case. The verb describes the action; the optional noun scopes it. Examples: summarize, refresh, add_entry, export_csv. Avoid names that describe the caller or the output destination — the name should describe what the tool does, not who calls it or where the result goes.
Reserved tool names:
The following tool names are RESERVED. A wrap MAY implement them, but if it does, it MUST follow the defined semantics. A wrap without these tools is still valid — the manifest itself provides baseline AI discoverability.
| Name | Description |
|---|---|
summarize | Returns a compact, LLM-optimized summary of the wrap's structure, data schemas, available tools, and current state. The recommended first call for any AI agent encountering an unknown wrap. |
refresh | Re-fetches data from external sources and updates wrap-visible state with fresh data. MUST be mutating: true. Requires appropriate permissions.network and any declared secrets. Viewers SHOULD display a staleness indicator when the wrap supports refresh and the data is older than a viewer-determined threshold. |
Relationship to other spec features:
- Tools that modify observable state SHOULD be declared
mutating: true - Mutating tools (
mutating: true) MAY updatewrap.store, mutable modules, or equivalent runtime-managed state. Hosted runtimes MAY also persist private or authoritative state outside the wrap file. - Tools that call external APIs require appropriate
permissions.network - Tools that persist wrap-visible state require appropriate
permissions.storage - Wrap-authored tools MAY call
wrap.runtimecapabilities when present - Runtimes MUST keep host-specific capabilities separate from
wrap.tools
5.12 runtime
OPTIONAL. Declares host/runtime capability requirements. Runtime capabilities are host-provided operations exposed through wrap.runtime. They are distinct from wrap-authored tools declared in tools.
"runtime": {
"required_capabilities": ["openwrap.store.private.set"],
"optional_capabilities": ["openwrap.store.private.get"]
}
| Field | Type | Required | Description |
|---|---|---|---|
required_capabilities | array[string] | OPTIONAL | Runtime capabilities the wrap requires for full functionality |
optional_capabilities | array[string] | OPTIONAL | Runtime capabilities the wrap can use when present but can function without |
Rules:
- Runtime capability names MUST be namespaced strings (for example
openwrap.project.publishoracme.inventory.update) - Runtimes SHOULD preflight
required_capabilitiesbefore interactive execution and surface missing capabilities clearly - Wraps SHOULD degrade gracefully when
optional_capabilitiesare unavailable - Runtime capability declarations are advisory. Capability behavior, authorization, and data semantics are runtime-defined
5.13 Inline Comments
Generators SHOULD annotate wrap content with prefixed HTML comments to provide provenance, rationale, and context for AI and human readers. These comments use the wrap: prefix to distinguish them from ordinary HTML comments.
Defined prefixes:
| Prefix | Purpose | Example |
|---|---|---|
wrap:note | General context, caveats, assumptions | <!-- wrap:note Dollar amounts are nominal, not inflation-adjusted --> |
wrap:source | Data provenance — where the data came from or the query that produced it | <!-- wrap:source SELECT region, SUM(amount) FROM revenue GROUP BY region --> |
wrap:decision | Design or implementation rationale | <!-- wrap:decision Bar chart instead of pie — more than 8 categories --> |
Prefixes are not enforced — a generator MAY use any wrap: prefix that is descriptive. The three above are conventions, not a closed set.
Rules:
- Inline comments MUST use the format
<!-- wrap:{prefix} {content} --> - Comments are ignored by browsers and viewers — they have no effect on rendering or behavior
- Comments SHOULD be placed directly above the content they describe
- Generators SHOULD include at least a
wrap:sourcecomment for any section backed by a data query or external data pull - A wrap without inline comments is valid — comments are RECOMMENDED, not REQUIRED
Relationship to manifest annotations: Inline comments are free-form, contextual, and live next to the code they describe. Manifest annotations (Section 5.14) are structured, targeted at specific elements via CSS selectors, and live in the manifest. Both are useful for different purposes — inline comments for provenance and rationale during generation, manifest annotations for structured metadata that tools can query programmatically.
5.14 Annotations
OPTIONAL. Structured notes attached to specific parts of the wrap.
"annotations": [
{
"target": "#revenue-chart",
"note": "Uses trailing 12-month average per CFO request"
}
]
| Field | Type | Required | Description |
|---|---|---|---|
target | string | REQUIRED | CSS selector or component ID |
note | string | REQUIRED | Human/AI readable annotation |
5.15 extensions
OPTIONAL. Namespaced third-party metadata.
"extensions": {
"com.acme.compliance": {
"classification": "internal",
"retention_days": 90
},
"com.tableau.integration": {
"datasource_id": "ds-abc123"
}
}
Rules:
- Top-level keys under
extensionsMUST be reverse-domain namespaced (e.g.com.acme.compliance). No bare keys. - Viewers MUST ignore extension namespaces they do not recognize. Never error, never warn.
- Extensions are inert data unless a viewer or plugin explicitly understands the namespace.
- The namespace
dev.wrap.*is RESERVED for first-party extensions that may be promoted to the core spec. - Extension values MUST be valid JSON. The core spec does not validate extension schemas.
6. Security Tiers
Every wrap operates at a declared security tier. The tier determines what protections are in place and what infrastructure is required.
6.1 Open Tier
The default tier. No security mechanisms. The wrap is a plain HTML file.
"security": { "tier": "open" }
- Content is plaintext in the file
- Works offline
- No authentication, no signing, no encryption
- Equivalent to any normal HTML file
6.2 Signed Tier
The wrap is signed to prove authorship and detect tampering. Content remains plaintext — signing is about trust and integrity, not access control.
"security": {
"tier": "signed",
"signature": {
"algorithm": "ed25519",
"value": "{base64-encoded signature}",
"signed_at": "{ISO 8601 datetime}"
},
"certificate": "{URL to public key or inline base64 public key}"
}
Signing scope:
The signature covers the entire document except mutable data. Specifically:
- The manifest (with
security.signature.valueset to"") - The bootloader, styles, content root, and all embedded data modules
- The
<script id="wrap-store-snapshot">element (if present) is excluded — its content is replaced with the empty string before hashing
The signature covers the author's entire document — code, data, and structure. Runtime user state lives in wrap.store, which is persisted outside the HTML by the platform. When a wrap is exported, store data is serialized into a <script id="wrap-store-snapshot"> element that is always excluded from the signature. This allows a signed wrap (e.g. an inspection form) to carry user-entered data without breaking the author's signature.
Signing process:
- Author creates the wrap with all content finalized
security.signature.valueis set to the empty string""- If a
<script id="wrap-store-snapshot">element exists, its content is replaced with the empty string - The SHA-256 hash of the resulting document bytes is computed
- The hash is signed with the author's Ed25519 private key
- The base64-encoded signature is placed in
security.signature.valuein the original document
Verification process:
- Viewer reads
security.signatureandsecurity.certificate - Viewer creates a copy of the document with:
- security.signature.value set to ""
- Content of <script id="wrap-store-snapshot"> (if present) replaced with the empty string
- Viewer computes SHA-256 of the resulting document bytes
- Viewer fetches or reads the public key from
certificate - Viewer verifies the signature against the hash
- Viewer displays verification status:
- Verified: "Signed by {author} — signature valid"
- Failed: "WARNING: This wrap has been modified since it was signed"
- Unverifiable: "Signed, but the certificate could not be verified"
Store snapshot:
When a viewer or platform exports a wrap that has wrap.store data, it serializes the store contents into a dedicated element:
<script type="application/json" id="wrap-store-snapshot">
{"responses":[{"question":"Safety check","answer":"Pass"}],"completed":true}
</script>
The bootloader reads this element on load and populates wrap.store with its contents (if the store is empty). This allows user state to travel with the file across exports. Because the store snapshot is always excluded from the signature hash, a signed wrap remains valid even after the user adds data.
Byte-level signing (no canonicalization):
The signature covers the exact byte content of the file. Any byte-level modification — including whitespace changes, re-encoding, trailing newlines, or attribute reordering — invalidates the signature. This is intentional. Signed wraps are sealed artifacts, like signed binaries or notarized documents.
This avoids the canonicalization complexity that plagued XML signatures (C14N). The tradeoff is simple: don't edit signed files in a text editor. To modify a signed wrap's content, create a new wrap and re-sign it.
Canonicalization and the reference bootloader: Signing operates on exact file bytes — no normalization or canonicalization is applied. Verification requires access to the original file bytes, not a DOM re-serialization. The reference bootloader detects the presence of a signature and sets wrap.signature.status = "present", but does not perform verification. Full Ed25519 verification requires a viewer or platform that can read the file as raw bytes, reconstruct the signing input (zeroing signature.value and store snapshot content), and verify against the public key.
Trust model:
This version does not mandate a certificate authority. Implementations MAY use:
- TOFU (trust on first use) — remember the public key for a given author
- Organizational PKI — certificates issued by an enterprise CA
- Well-known URLs — public keys hosted at a well-known path on the author's domain
6.3 Encrypted Tier
The encrypted tier protects wrap content so that the file is useless without authorization. This version specifies passphrase-based encryption — content is encrypted with a key derived from a user-provided passphrase. No server infrastructure required.
"security": {
"tier": "encrypted",
"encryption": {
"algorithm": "aes-256-gcm",
"key_derivation": "pbkdf2",
"iterations": 600000,
"salt": "{base64-encoded random salt}",
"iv": "{base64-encoded initialization vector}"
}
}
File structure for encrypted wraps:
The wrap-root contains no readable content. Instead, the encrypted content is stored in a dedicated element:
<div id="wrap-root"></div>
<script type="application/octet-stream" id="wrap-encrypted-content"
data-encoding="base64">
base64-encoded-ciphertext...
</script>
The noscript fallback SHOULD display a message like "This wrap is password-protected. Enable JavaScript to unlock."
Encryption process (author):
- Author creates the wrap with all content finalized in the
wrap-root - Author provides a passphrase
- Bootloader derives a 256-bit key using PBKDF2 (SHA-256, 600k+ iterations, random salt)
- Content is encrypted with AES-256-GCM (random IV)
- Ciphertext replaces the wrap-root content, stored in
wrap-encrypted-content - Salt and IV are stored in the manifest (they are not secret)
Decryption process (viewer):
- Bootloader reads
security.tier→"encrypted" - Checks for
security.authfield:
- If auth is absent → passphrase mode (this version)
- If auth is present → server-managed mode (RESERVED — refuse to render)
- Prompts user for passphrase
- Derives key using PBKDF2 with the manifest's salt and iteration count
- Decrypts ciphertext with AES-256-GCM using the manifest's IV
- On success: injects decrypted HTML into
wrap-root, initializes bootloader normally - On failure: displays "Incorrect passphrase" — content never renders
All cryptographic operations use the browser's native Web Crypto API.
What's RESERVED for future versions:
When the security.auth field is present, the encrypted tier operates in server-managed mode — the decryption key comes from a key server that requires authentication (JWT, SSO, etc.), not from a user-provided passphrase. The manifest fields for this mode (auth, check_in, watermark, expiry, revocation_list) are defined in the schema but their behavior is not specified in this version.
A viewer that encounters "tier": "encrypted" with a security.auth field MUST NOT attempt to render the wrap. It MUST display a message: "This wrap requires a viewer that supports authenticated encryption."
Passphrase mode vs server-managed mode:
| Passphrase (v0.0.1) | Server-managed (future) | |
|---|---|---|
| Infrastructure needed | None | Auth server + key server |
| Works offline | Yes | No |
| Key source | User enters passphrase | Server provides after auth |
| Revocable | No — passphrase is shared knowledge | Yes — server controls access |
| Use case | Share with specific people who know the password | Enterprise access control |
6.4 Remote Tier (RESERVED)
The remote tier is defined in the manifest schema but its behavior is not specified in this version. The manifest fields (content_source, render_mode, session, auth) are reserved to ensure forward compatibility.
A viewer that encounters "tier": "remote" MUST NOT attempt to render the wrap. It MUST display a message: "This wrap requires a viewer that supports remote content."
6.5 Forward Compatibility
The security tier enum is explicitly extensible. Future versions MAY add new tiers. A viewer that encounters an unknown tier value MUST refuse to render and display an upgrade message. Security tiers always fail closed.
The reserved fields in Section 5.7 define the schema shape that future tiers will use. This ensures that:
- Manifest parsers written today will not choke on future manifests
- The
securityobject structure is stable across versions - Adding encrypted/remote support is a minor version bump, not a breaking change
Note on schema validation: The JSON Schema (spec/v0.0.1/manifest.schema.json) is version-locked — it validates v0.0.1 manifests only. A manifest using fields introduced in a later spec version should be validated against that version's schema, not this one. The additionalProperties: false constraint on the security object means the schema will reject unknown fields at validation time, which is correct: a v0.0.2 manifest is not a v0.0.1 manifest. The "fail closed" rule in Section 5.7 governs viewer runtime behavior; the schema governs authoring-time validation.
6.6 Threat Model
A wrap is executable code. This section describes what a malicious or buggy wrap can and cannot do in each execution context, so that users, security reviewers, and viewer implementors can make informed trust decisions.
6.6.1 Execution Contexts
A wrap can be opened in three contexts, each with a different trust boundary:
| Context | Enforcement | Trust level |
|---|---|---|
| Standard browser (file opened directly) | Browser sandbox + CSP meta tag | Equivalent to opening any .html file |
| Web viewer (wrap loaded in a hosted viewer app) | Browser sandbox + CSP + iframe sandbox + viewer-enforced permissions | Higher — viewer mediates all capability access |
| Native viewer (dedicated desktop/mobile app) | OS-level sandbox + viewer-enforced permissions + manifest validation | Highest — full control over execution environment |
6.6.2 Standard Browser (No Viewer)
When a user opens a .html wrap directly in a browser, the security posture is:
What the browser enforces:
- Same-origin policy — the wrap cannot read files, cookies, or storage from other origins
- CSP meta tag — if present (RECOMMENDED), restricts script sources, network access, and resource loading at the browser level. CSP is not advisory; the browser enforces it.
- Permission prompts — camera, microphone, geolocation, and notifications require explicit user consent via browser-native prompts
- File system isolation — the wrap cannot read or write local files (no File System Access API without user gesture)
What is NOT enforced:
- Manifest
permissions— these are advisory. A wrap declaringnetwork: "none"but missing a CSP tag can still make fetch calls. The CSP is the real enforcement; the manifest is the declaration. - Manifest
security.tier—signedandencryptedtiers require a viewer to verify signatures or decrypt content. In a plain browser, a signed wrap opens but the signature is not checked. An encrypted wrap's bootloader handles decryption via Web Crypto, but there is no viewer to enforce access policy. - Mutating tool gating — without a viewer, there is no prompt before a mutating tool executes
- Module integrity —
src+hashvalidation depends on the bootloader, which is itself part of the untrusted document
What a malicious open-tier wrap CAN do in a plain browser (with no CSP):
- Make network requests to any reachable server (exfiltrate data, phone home)
- Read and write localStorage/IndexedDB for its origin
- Render misleading UI (phishing)
- Consume CPU/memory (crypto mining, DoS)
- Request camera/microphone/geolocation (user will see a browser prompt)
What a malicious open-tier wrap CANNOT do in a plain browser:
- Access other tabs, windows, or origins
- Read local files without a user-initiated file picker
- Install software or escape the browser sandbox
- Bypass a CSP meta tag that the wrap itself contains (CSP meta tags are enforced even on
file://in modern browsers)
Implication: For open-tier wraps, the CSP meta tag is the primary security mechanism in standard browsers. Generators MUST include a restrictive CSP (Section 4.2). A wrap with permissions.network: "none" and a CSP of default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: is genuinely locked down — the browser will block all outbound network requests regardless of what the JavaScript attempts.
6.6.3 Web Viewer
A web viewer (e.g., Wrap Collab) loads wraps inside a sandboxed iframe with viewer-controlled attributes:
Additional protections beyond standard browser:
sandboxattribute on the iframe — disables forms, popups, top-level navigation, and scripts unless explicitly re-enabled- Viewer-injected CSP — the viewer controls the CSP, not the wrap. A malicious wrap cannot weaken its own CSP.
- Permission mediation — the viewer intercepts capability requests and checks them against the manifest's
permissionsdeclarations. Undeclared capabilities are blocked. - Mutating tool prompts — the viewer SHOULD prompt the user before executing tools marked
mutating: true - Signature verification — the viewer verifies
signedtier wraps and displays trust status
What a malicious wrap CANNOT do in a web viewer (beyond standard browser restrictions):
- Weaken its own CSP (viewer controls it)
- Access capabilities not declared in the manifest
- Navigate or redirect the parent page
- Open popups or new windows (unless sandbox allows it)
6.6.4 Native Viewer
A native viewer has full control over the execution environment:
Additional protections beyond web viewer:
- OS-level sandboxing (e.g., macOS App Sandbox, Windows AppContainer)
- File system access control — the viewer can restrict what the wrap can read/write
- Network-level enforcement — the viewer can block network access at the process level, independent of CSP
- Full manifest enforcement — all
permissionsfields are enforced, not advisory - Audit logging — the viewer can log all capability usage for compliance
6.6.5 Recommendations
For users: Treat an open-tier wrap from an untrusted source the same way you'd treat any .html file — it's executable code. If the wrap includes a restrictive CSP, it is meaningfully sandboxed even in a plain browser. For higher assurance, open untrusted wraps in a viewer.
For generators: Always include a CSP meta tag scoped to the minimum capabilities the wrap needs. Never include a CSP more permissive than what permissions declares. The CSP is the enforcement; the manifest is the contract.
For viewer implementors: Enforce all manifest permissions at the viewer level. Do not rely on the wrap's embedded CSP — inject your own. Prompt users before executing mutating tools. Display signature verification status prominently for signed wraps.
7. State, Persistence & Runtime
Wrap files are read-only at the file system level — browsers cannot modify a .html file on disk. At runtime, wraps have two mechanisms for managing state:
| Mechanism | Purpose | API |
|---|---|---|
localStorage / sessionStorage | Personal preferences, UI state | Standard Web Storage API |
wrap.store | Application data — anything the wrap creates, modifies, or shares | See §7.1 |
Both require permissions.storage: true in the manifest. The backing implementation is runtime-defined — a standalone browser uses localStorage, a platform viewer may provide shared or cloud-synced storage.
Export remains the primary mechanism for state portability. On export, wrap.store data is serialized into a <script id="wrap-store-snapshot"> element in the HTML. This produces a new, self-contained wrap with the user's data included.
Export and signing:
- If the original wrap was
opentier: the export is alsoopen. No signature concerns. - If the original wrap was
signedtier: the export preserves the original signature. The store snapshot element is always excluded from the signature scope (Section 6.2), so the signature remains valid even with user data added. The viewer can verify: "the author's template is authentic, the user added their own data." - If the export modifies anything outside the store snapshot (code, styles, data modules): the signature is invalid. The export should strip it and become
opentier.
7.1 wrap.store
wrap.store is the primary data persistence API for wraps. It provides key-value storage for single values and collection operations for lists of records. Available when permissions.storage is true.
Design principles:
- All methods are async (return Promises)
- Values are JSON-serializable (objects, arrays, strings, numbers, booleans, null)
- Collection items receive an auto-generated
_idfield on append - The backing implementation is runtime-defined — the API contract is the same regardless of whether storage is localStorage, a cloud database, or a shared real-time store
- Runtimes MAY provide real-time synchronization, in which case
subscribecallbacks fire when any connected user modifies the data
Security considerations:
wrap.storeis for persistent application state, not confidential credentials- Data in
wrap.storeMUST be treated as client-visible and client-influenced wrap.storeis not an authoritative datastore for anti-cheat, access control, or hidden per-user information- Runtimes MAY layer private or authoritative storage outside the wrap file, but those guarantees are runtime-specific, not part of the
wrap.storecontract
Terminology note:
- Private answers "who can read this?" Private state is hidden from other users or viewers
- Authoritative answers "who controls truth?" Authoritative state is validated and owned by the runtime, not trusted from raw client writes
- Data can be private, authoritative, both, or neither. For example, a hidden hand of cards is private; an official leaderboard is authoritative; a private approved draft could be both
Single-value operations:
| Method | Returns | Description | |
|---|---|---|---|
get(key) | `value \ | null` | Read a value by key |
set(key, value) | void | Write or replace a value | |
delete(key) | void | Remove a key and its value |
Collection operations:
| Method | Returns | Description | |
|---|---|---|---|
append(key, item) | object | Add an item to a collection. Returns the item with _id assigned. Creates the collection if it doesn't exist. | |
update(key, match, patch) | number | Update items matching match with fields from patch. Returns count of updated items. match is a partial object — items where all specified fields are equal are matched. | |
remove(key, match) | number | Remove items matching match. Returns count of removed items. | |
pop(key) | `object \ | null` | Remove and return the last item in a collection. |
count(key) | number | Return the number of items in a collection, without loading the full data. |
Observation:
| Method | Returns | Description |
|---|---|---|
keys() | string[] | List all keys in the store |
subscribe(key, fn) | function | Register a callback that fires when the value at key changes. Returns an unsubscribe function. In single-user mode, fires on local writes. In multi-user runtimes, fires on any user's writes. |
Example — shared guestbook:
// Add a message
const entry = await wrap.store.append('messages', { name: 'Andrew', text: 'Hello from Wrap' });
// entry = { _id: 'x7f2k', name: 'Andrew', text: 'Hello from Wrap' }
// Read all messages
const messages = await wrap.store.get('messages');
// [{ _id: 'x7f2k', name: 'Andrew', text: 'Hello from Wrap' }, ...]
// Live updates from other participants
wrap.store.subscribe('messages', (messages) => {
renderGuestbook(messages);
});
// Remove a specific entry
await wrap.store.remove('messages', { _id: 'x7f2k' });
Example — todo list with status updates:
const todo = await wrap.store.append('todos', { text: 'Buy milk', done: false });
// Later: mark as done
await wrap.store.update('todos', { _id: todo._id }, { done: true });
Bootloader default implementation:
The reference bootloader provides a localStorage-backed wrap.store. Collections are stored as JSON arrays. append reads the array, pushes the item with a generated _id, writes back. This is suitable for single-user standalone wraps. Runtimes that provide multi-user or real-time storage override wrap.store with their own implementation — the API contract remains the same.
7.2 wrap.runtime
wrap.runtime is the optional host/runtime bridge. It exposes runtime-specific capabilities that are not part of the portable wrap contract. A wrap running in a host environment such as OpenWrap or another embed platform can use wrap.runtime to call host-provided operations. In a standalone browser or downloaded-file context, the bootloader provides a no-runtime stub.
Design principles:
wrap.runtimeis always present onwindow.wrap- Runtime capabilities are host-provided and runtime-specific
- Runtime capabilities are distinct from wrap-authored tools in
wrap.tools - Capability names are namespaced to avoid collisions across runtimes
- Absence of a runtime MUST degrade gracefully
Terminology note:
- Runtime capabilities that expose private state are about visibility and access control
- Runtime capabilities that expose authoritative state are about validation and server-owned truth
- A capability may be one, the other, both, or neither depending on runtime semantics
Surface:
| Field / Method | Returns | Description |
|---|---|---|
available | boolean | Whether a host runtime is present |
listCapabilities() | CapabilityDescriptor[] | Enumerate runtime capabilities currently available |
call(name, params?) | any | Invoke a runtime capability by name |
addEventListener(type, listener) | void | Subscribe to runtime lifecycle or capability events |
removeEventListener(type, listener) | void | Remove a previously registered listener |
Capability descriptor shape:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | REQUIRED | Namespaced capability name (for example openwrap.project.publish) |
description | string | OPTIONAL | Human-readable description |
parameters | object | OPTIONAL | JSON Schema describing accepted parameters |
returns | object | OPTIONAL | JSON Schema or description of the return value |
mutating | boolean | OPTIONAL | Whether calling the capability may modify observable or authoritative state |
Standard events:
| Event | Meaning |
|---|---|
capabilitieschange | Capability availability changed |
authchange | Authentication or authorization state changed |
contextchange | Runtime context changed (e.g. active project, record, or tenant) |
connect | Runtime bridge became available |
disconnect | Runtime bridge became unavailable |
Listeners receive an event-like object with { type, detail }. The shape of detail is runtime-defined.
Bootloader default stub:
The reference bootloader MUST expose a stub with this behavior when no runtime is present:
wrap.runtime = {
available: false,
listCapabilities: async () => [],
call: async () => {
throw Object.assign(new Error('No runtime available.'), {
code: 'RUNTIME_UNAVAILABLE'
});
},
addEventListener: () => {},
removeEventListener: () => {}
};
Rules:
- Runtime capability names MUST be namespaced strings (for example
openwrap.store.private.setoracme.inventory.update) wrap.runtime.call()MUST reject with a structured runtime error when a capability cannot be fulfilled- Runtime implementations MUST keep host-specific capabilities separate from
wrap.tools - Wraps SHOULD check
wrap.runtime.availableor handleRUNTIME_UNAVAILABLEgracefully before depending on host-only behavior - Wraps MAY compose higher-level wrap-authored tools from one or more runtime capability calls
Example — calling a host capability:
if (!wrap.runtime.available) {
showOfflineBanner();
} else {
await wrap.runtime.call('acme.inventory.update', {
item_id: 'sku-4021',
quantity: 12
});
}
Example — wrap-authored tool using runtime capabilities:
wrap.tools.restock_check = async ({ warehouse_id }) => {
const items = await wrap.runtime.call('acme.inventory.list', { warehouse_id });
for (const item of items) {
if (item.quantity < item.reorder_threshold) {
await wrap.runtime.call('acme.inventory.flag', {
item_id: item.id,
reason: 'low_stock'
});
}
}
return { checked: items.length };
};
7.3 wrap.resolveURL
wrap.resolveURL(path) is a bootloader helper for consumers that require a URL string instead of calling fetch() directly.
Behavior:
- If
pathresolves to an embedded local module, the bootloader SHOULD return ablob:URL backed by that embedded content - If
pathdoes not resolve to an embedded local module, the bootloader SHOULD return the original input unchanged - Query strings and fragment identifiers MAY be preserved on the returned URL, but they do not change which module is selected
- Reference bootloaders MAY also route
Worker()andSharedWorker()script URLs through the same resolution logic automatically
Examples:
const audio = new Audio(wrap.resolveURL('/assets/sounds/boost.mp3'));
loader.load(wrap.resolveURL('/assets/models/f-15.glb'));
8. Error Handling
8.1 Error Codes
| Code | Meaning |
|---|---|
MANIFEST_MISSING | No wrap-manifest script element found |
MANIFEST_INVALID | Manifest is not valid JSON or fails schema validation |
VERSION_UNSUPPORTED | wrap_version major version not recognized |
TIER_UNSUPPORTED | Security tier not supported by this viewer |
MODULE_MISSING | Declared module not found in the document |
MODULE_LOAD_FAILED | Module failed to initialize (WASM compile error, etc.) |
RUNTIME_ERROR | Unhandled exception during wrap execution |
PERMISSION_DENIED | Code attempted an undeclared capability |
SIGNATURE_INVALID | Signature verification failed |
INTEGRITY_FAILED | Fetched module content does not match declared hash |
DATA_CORRUPT | Embedded data failed integrity check |
RUNTIME_UNAVAILABLE | wrap.runtime.call() was attempted without a host runtime |
CAPABILITY_UNAVAILABLE | Named runtime capability is not available in this runtime |
AUTH_REQUIRED | Runtime capability requires authentication |
FORBIDDEN | Runtime capability denied by authorization or policy |
INVALID_PARAMS | Runtime capability received invalid parameters |
FAILED | Runtime capability failed for an implementation-specific reason |
8.2 Graceful Degradation
Modules MUST fail independently. If one module fails to load:
- The wrap MUST continue to render all other content
- The failed module's section SHOULD display its
fallbackmessage, or a generic error - The viewer SHOULD log the error with the appropriate error code
If the manifest is missing or invalid, the file is not a wrap — the browser renders it as normal HTML.
9. Versioning
9.1 Spec Versioning
The spec uses semantic versioning:
| Bump | What changed |
|---|---|
| Patch (0.0.x) | Clarifications, no behavioral change |
| Minor (0.x.0) | New optional fields, new module types, new security tier implementations. Old wraps remain valid. Old viewers degrade gracefully. |
| Major (x.0.0) | Breaking changes — required fields added, field semantics changed |
9.2 Forward Compatibility (new wrap, old viewer)
- Unknown manifest fields MUST be ignored (except in
security— see Section 5.7) - Unknown module types MUST be skipped, with fallback displayed
- Unknown security tiers MUST refuse to render
- Unknown extension namespaces MUST be ignored
9.3 Backward Compatibility (old wrap, new viewer)
- Missing optional fields use their documented defaults
- Deprecated fields are still honored
- New defaults do not retroactively apply to old wraps
9.4 Size Guidance
This version does not impose hard limits on document size, module count, or annotation count. Implementers SHOULD apply reasonable limits appropriate to their context. For reference, practical considerations include:
- Total file size (base64 encoding inflates binary assets ~33%)
- Number of modules (each adds parsing and initialization overhead)
- Number of annotations (large arrays impact manifest parse time)
- Individual embedded data size (very large base64 blobs may cause browser memory issues)
Future versions MAY introduce normative size constraints based on implementation experience.
10. Conformance
10.1 Conforming Document
A conforming wrap document:
- MUST be a valid HTML document
- MUST contain a manifest element per Section 4.1
- MUST have a valid
wrap_versionfield - MUST have a valid
meta.titlefield - MUST contain a content root element per Section 4.4
- MUST contain a bootloader element per Section 4.6
- SHOULD include a Content Security Policy per Section 4.2
- SHOULD include a static fallback per Section 4.3
10.2 Conforming Viewer
A conforming viewer:
- MUST parse the manifest before rendering interactive content
- MUST check the security tier and refuse to render unsupported tiers
- MUST ignore unknown manifest fields (except in
security) - MUST skip unknown module types gracefully
- MUST ignore unknown extension namespaces
- SHOULD enforce declared permissions when capable
- SHOULD display signature verification status for signed wraps
- If it implements runtime capabilities, MUST expose them via
wrap.runtime - If it implements runtime capabilities, MUST keep host-specific capabilities separate from
wrap.tools
10.3 Conforming Generator
A conforming generator:
- MUST produce valid HTML documents
- MUST include a valid manifest with
wrap_versionandmeta.title - MUST include a content root and bootloader
- MUST declare all capabilities the wrap uses in
permissions - MUST declare all dependencies in
modules - SHOULD include a Content Security Policy
- SHOULD include a static fallback
- SHOULD include an
intentfield for AI discoverability - SHOULD declare
runtime.required_capabilitiesandruntime.optional_capabilitieswhen the wrap depends on host/runtime capabilities
10.4 Conformance Test Suite
A suite of self-verifying test wraps is provided at spec/tests/. Each test is a standalone HTML file that embeds the reference bootloader and displays PASS/FAIL results when opened in a browser.
spec/tests/valid/— Wraps that MUST be accepted. Tests cover: minimal wraps, JSON/CSV/base64 data modules, tools, full dashboards, signed tier, optional modules, and extensions.spec/tests/invalid/— Wraps that MUST fail gracefully. Tests cover: missing manifest, invalid JSON, missingwrap_version, missingmeta.title, unknown security tier, and missing required modules.
Implementers SHOULD verify their bootloader and viewer against this suite. All valid tests should show PASS; all invalid tests should show the appropriate error code rendered in #wrap-root.
Appendix A: Minimal Example
The smallest valid wrap:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello</title>
<script type="application/json" id="wrap-manifest">
{
"wrap_version": "0.0.1",
"meta": { "title": "Hello" }
}
</script>
</head>
<body>
<div id="wrap-root">
<h1>Hello, World</h1>
</div>
<script id="wrap-bootloader">
(() => {
const m = JSON.parse(document.getElementById('wrap-manifest').textContent);
document.title = m.meta.title;
})();
</script>
</body>
</html>
Appendix B: Dashboard Example
A wrap with embedded data, modules, and permissions:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Q3 Revenue</title>
<script type="application/json" id="wrap-manifest">
{
"wrap_version": "0.0.1",
"meta": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Q3 Revenue Report",
"description": "Interactive quarterly revenue with drill-down by region",
"author": { "name": "Finance Team", "email": "[email protected]" },
"created": "2026-03-12T10:00:00Z",
"language": "en",
"intent": "A quarterly revenue dashboard. Revenue data is in a JSON dataset, broken down by region and product line. Users can filter by region and see trends over time."
},
"layout": "dashboard",
"connectivity": "offline",
"permissions": {
"network": "none",
"storage": true
},
"security": { "tier": "open" },
"modules": [
{
"type": "data/json",
"id": "revenue-data",
"description": "Quarterly revenue by region and product, 2024-2026",
"required": true
},
{
"type": "runtime/js",
"id": "chart-engine",
"description": "Lightweight charting library for revenue visualizations"
}
],
"data": {
"schemas": {
"revenue-data": {
"type": "json",
"description": "Array of revenue records",
"fields": {
"quarter": { "type": "string", "description": "e.g. 2025-Q3" },
"region": { "type": "string", "description": "Sales region" },
"product": { "type": "string", "description": "Product line" },
"amount": { "type": "number", "description": "Revenue in USD" }
}
}
}
},
"annotations": [
{
"target": "#revenue-chart",
"note": "Uses trailing 12-month average per CFO request"
}
]
}
</script>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:;">
<style id="wrap-styles">
/* Dashboard styles */
</style>
</head>
<body>
<noscript>
<h1>Q3 Revenue Report</h1>
<table><!-- static data table fallback --></table>
</noscript>
<div id="wrap-root">
<header data-wrap-role="title">
<h1>Q3 Revenue Report</h1>
</header>
<section data-wrap-role="chart" id="revenue-chart"
data-wrap-description="Revenue by quarter, filterable by region">
</section>
<section data-wrap-role="data-table" id="revenue-table"
data-wrap-description="Detailed revenue breakdown">
</section>
</div>
<script type="application/json" class="wrap-data" data-id="revenue-data">
[
{"quarter":"2026-Q1","region":"North America","product":"Enterprise","amount":2400000},
{"quarter":"2026-Q1","region":"EMEA","product":"Enterprise","amount":1800000}
]
</script>
<script id="wrap-bootloader">
(() => {
const manifest = JSON.parse(
document.getElementById('wrap-manifest').textContent
);
// Load embedded data
const dataEl = document.querySelector('.wrap-data[data-id="revenue-data"]');
const data = JSON.parse(dataEl.textContent);
// Initialize interactive dashboard
// ... chart rendering, filtering, etc.
})();
</script>
</body>
</html>
Appendix C: Stateful Form Example
A signed wrap with wrap.store — an inspection form that can be filled in without breaking the author's signature. User responses live in wrap.store, not in the data modules. On export, store data is serialized into a <script id="wrap-store-snapshot"> element that is excluded from the signature.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Site Inspection</title>
<script type="application/json" id="wrap-manifest">
{
"wrap_version": "0.0.1",
"meta": {
"title": "Site Inspection Checklist",
"author": { "name": "Safety Team", "email": "[email protected]" },
"created": "2026-03-12T10:00:00Z",
"intent": "A field inspection form. The checklist items and scoring criteria are fixed by the author. The inspection responses are filled in by the field inspector."
},
"layout": "document",
"connectivity": "offline",
"permissions": {
"storage": true
},
"security": {
"tier": "signed",
"signature": {
"algorithm": "ed25519",
"value": "{base64-encoded signature}",
"signed_at": "2026-03-12T10:00:00Z"
},
"certificate": "https://pki.acme.com/keys/safety-team"
},
"modules": [
{
"type": "data/json",
"id": "checklist-items",
"description": "The inspection checklist — questions, categories, scoring weights"
}
]
}
</script>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:;">
</head>
<body>
<noscript>
<h1>Site Inspection Checklist</h1>
<p>Enable JavaScript to fill out this form.</p>
</noscript>
<div id="wrap-root">
<!-- Interactive form rendered by app code -->
</div>
<!-- Author's checklist template (covered by signature) -->
<script type="application/json" class="wrap-data" data-id="checklist-items">
[
{"id": "fire-exits", "category": "Safety", "question": "Are fire exits clear and marked?", "weight": 3},
{"id": "ppe", "category": "Safety", "question": "Is PPE available and in good condition?", "weight": 2},
{"id": "electrical", "category": "Electrical", "question": "Are panel covers in place?", "weight": 2}
]
</script>
<!-- Store snapshot: user data from a previous export (excluded from signature) -->
<script type="application/json" id="wrap-store-snapshot">
{}
</script>
<script id="wrap-bootloader">
// ... bootloader code ...
</script>
<script>
(() => {
const wrap = window.wrap;
// Load checklist from embedded data
fetch('./checklist-items.json').then(r => r.json()).then(items => {
// Load previous responses from wrap.store (if any)
wrap.store.get('responses').then(responses => {
responses = responses || [];
// Render form UI with items and existing responses
// On submit: wrap.store.append('responses', { itemId, answer, timestamp })
// On export: store snapshot is serialized into the HTML automatically
// — signature remains valid because wrap-store-snapshot is excluded
});
});
})();
</script>
</body>
</html>
When the inspector fills in the form and exports, the new file contains their responses in the inspection-responses data block. The signature remains valid — it was computed with that block excluded. The recipient sees: "Signed by [email protected] — template verified. Inspection data has been added."
Appendix D: AI Generation Contract
To generate a conforming wrap, an LLM SHOULD:
- Start from the file structure template in Section 4
- Populate the manifest with all relevant metadata, especially
intent - Declare all capabilities the generated code will use in
permissions - Declare all dependencies in
modules - Include a CSP
<meta>tag scoped to only what the wrap needs - Include a
<noscript>fallback with a static representation of the content - Use semantic HTML with
data-wrap-roleanddata-wrap-descriptionattributes on interactive sections - Document embedded data schemas in
data.schemas - Declare tools the wrap supports in
tools, includingsummarizewhen practical — use data tool types (sql,json,document) where possible so tools can be executed without a runtime - Declare
runtime.required_capabilitiesandruntime.optional_capabilitieswhen the wrap depends on host/runtime capabilities - Implement declared tools in a way the target runtime can fulfill — commonly via the
wrap.toolsglobal object in self-contained runtimes
The manifest is the contract between the generator and the viewer. A generator that declares accurate permissions and modules enables viewers to enforce security and manage resources. A generator that under-declares creates a wrap that may malfunction in enforcing viewers. A generator that over-declares wastes user trust.