Iframe Bootstrap Document
The complete HTML document that MUST be loaded into the HyperGen sandboxed iframe, including HTMX, SSE extension, postMessage bridge, and base styles.
The bootstrap document is the complete HTML page loaded into the sandboxed iframe. It provides the runtime environment for agent-generated HTML: HTMX for interactivity, the SSE extension for streaming, a postMessage bridge for host communication, and base styles with CSS variable support.
Required Elements
A conforming bootstrap document MUST include all of the following:
| Element | Purpose |
|---|---|
| HTMX script | Processes hx-* attributes for interactivity |
| HTMX SSE extension script | Enables sse-connect and sse-swap for streaming |
SSE root element (#hg-root) | The container that connects to the SSE endpoint and receives streamed fragments |
| postMessage bridge | Handles hg:theme and hg:destroy messages from the host |
| ResizeObserver | Notifies the host of content size changes via hg:resize messages |
| Connection status script | Toggles hg-connected / hg-disconnected CSS classes on <html> |
| Base styles | Resets (box-sizing, margin: 0), CSS variable defaults, loading indicator styles |
Minimal Conforming Bootstrap
The following is the minimal HTML document that satisfies all requirements. Implementations MAY add to this but MUST NOT remove any required element.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://unpkg.com/htmx.org@2"></script>
<script src="https://unpkg.com/htmx-ext-sse@2"></script>
<style>
:root {
/* Theme variables injected by host via postMessage */
}
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
line-height: 1.5;
color: var(--hg-text, #1a1a1a);
background: var(--hg-background, transparent);
}
.hg-indicator { display: none; }
.htmx-request .hg-indicator,
.htmx-request.hg-indicator { display: inline-block; }
.hg-disconnected body { opacity: 0.7; }
</style>
</head>
<body>
<div id="hg-root"
hx-ext="sse"
sse-connect="/api/agent/stream"
sse-swap="message"
hx-swap="innerHTML">
</div>
<!-- Connection status -->
<script>
(function() {
var root = document.documentElement;
function setConnected(v) {
if (v) {
root.classList.add("hg-connected");
root.classList.remove("hg-disconnected");
} else {
root.classList.remove("hg-connected");
root.classList.add("hg-disconnected");
}
}
setConnected(false);
document.body.addEventListener("htmx:sseOpen", function() {
setConnected(true);
});
document.body.addEventListener("htmx:sseError", function() {
setConnected(false);
});
document.body.addEventListener("htmx:sseClose", function() {
setConnected(false);
});
})();
</script>
<!-- postMessage bridge -->
<script>
(function() {
var hostOrigin = "*";
window.addEventListener("message", function(e) {
if (hostOrigin !== "*" && e.origin !== hostOrigin) return;
var msg = e.data;
if (!msg || typeof msg.type !== "string") return;
switch (msg.type) {
case "hg:theme":
if (msg.vars && typeof msg.vars === "object") {
Object.keys(msg.vars).forEach(function(key) {
document.documentElement.style.setProperty(key, msg.vars[key]);
});
}
break;
case "hg:destroy":
var root = document.getElementById("hg-root");
if (root) {
root.removeAttribute("sse-connect");
root.removeAttribute("hx-ext");
if (typeof htmx !== "undefined") {
htmx.trigger(root, "htmx:sseClose");
}
}
break;
}
});
// ResizeObserver
var ro = new ResizeObserver(function() {
window.parent.postMessage({
type: "hg:resize",
height: document.documentElement.scrollHeight,
width: document.documentElement.scrollWidth
}, hostOrigin);
});
ro.observe(document.body);
// Navigation bridge
document.addEventListener("click", function(e) {
var target = e.target;
while (target && target !== document.body) {
if (target.getAttribute &&
target.getAttribute("data-hg-navigate")) {
e.preventDefault();
window.parent.postMessage({
type: "hg:navigate",
url: target.getAttribute("data-hg-navigate")
}, hostOrigin);
return;
}
target = target.parentElement;
}
});
// Data bridge
document.addEventListener("hg:data", function(e) {
window.parent.postMessage({
type: "hg:data",
payload: e.detail
}, hostOrigin);
});
})();
</script>
</body>
</html>Sandbox Attribute Requirements
The iframe element MUST use the sandbox attribute with exactly these permissions:
<iframe sandbox="allow-scripts allow-same-origin allow-forms"
src="about:blank"
style="width: 100%; border: none;">
</iframe>Required permissions
| Permission | Reason |
|---|---|
allow-scripts | HTMX requires JavaScript to process hx-* attributes and manage SSE connections. |
allow-same-origin | HTMX must make HTTP requests (GET, POST) to the agent server. Without same-origin, XHR and fetch are blocked. |
allow-forms | HTMX form submissions (hx-post on <form> elements) require form submission capability. |
Explicitly denied permissions
The following permissions MUST NOT be granted unless the host application has a documented, specific need:
| Permission | Risk if granted |
|---|---|
allow-top-navigation | Allows the iframe to redirect the entire page. |
allow-popups | Allows the iframe to open new browser windows. |
allow-modals | Allows alert(), confirm(), prompt() to block the host application. |
allow-pointer-lock | Allows the iframe to capture the cursor. |
allow-downloads | Allows the iframe to trigger file downloads. |
Host applications MAY add permissions beyond the required three by appending to the sandbox attribute. Each additional permission weakens the security boundary and SHOULD be documented.
Content Security Policy
The host application SHOULD serve the bootstrap document with a Content-Security-Policy that restricts the iframe's capabilities.
Recommended CSP
default-src 'none';
script-src 'self' 'unsafe-inline' https://unpkg.com/htmx.org https://unpkg.com/htmx-ext-sse;
style-src 'self' 'unsafe-inline';
connect-src <agent-server-origin>;
img-src 'self' data: https:;
font-src 'self' data:;| Directive | Purpose |
|---|---|
default-src 'none' | Denies everything not explicitly allowed. |
script-src | Allows inline scripts (needed for the bridge) and HTMX from a trusted CDN. |
style-src | Allows inline styles (needed for theme injection and agent-generated <style> blocks). |
connect-src | Restricts HTTP/SSE connections to the known agent server origin. This is the primary defense against data exfiltration. Replace <agent-server-origin> with the actual origin. |
img-src | Allows images from same origin, data URIs, and HTTPS. |
font-src | Allows fonts from same origin and data URIs. |
'unsafe-inline' is required for both script-src and style-src because the bootstrap document uses inline scripts and agents generate inline styles. If the implementation can use nonces or hashes for the bootstrap scripts, it SHOULD prefer those over 'unsafe-inline'.
HTMX Version Requirements
- The bootstrap document MUST load HTMX version 2.x (2.0.0 or later).
- The bootstrap document MUST load the HTMX SSE extension version 2.x.
- Both scripts MAY be loaded from a CDN (unpkg, cdnjs, jsdelivr) or served locally.
- The HTMX script MUST be loaded before the SSE extension script (the extension depends on the HTMX core).
CDN URLs
The reference implementation uses unpkg with major-version pinning:
<script src="https://unpkg.com/htmx.org@2"></script>
<script src="https://unpkg.com/htmx-ext-sse@2"></script>Production deployments SHOULD pin to a specific version (e.g., htmx.org@2.0.4) and MAY use Subresource Integrity (SRI) hashes for additional security.
Connection Status CSS Classes
The bootstrap document MUST include a script that toggles CSS classes on the <html> element based on the SSE connection state:
| Class | Applied when |
|---|---|
hg-connected | The SSE connection is open (htmx:sseOpen event received). |
hg-disconnected | The SSE connection is closed or errored (htmx:sseError or htmx:sseClose received). |
The initial state MUST be hg-disconnected (applied before the SSE connection opens).
Agents MAY use these classes to style content conditionally:
.hg-disconnected .status-indicator { color: red; }
.hg-connected .status-indicator { color: green; }Loading the Bootstrap
The bootstrap document can be loaded into the iframe in two ways:
1. Via srcdoc (recommended)
The host generates the bootstrap HTML server-side and sets it as the iframe's srcdoc attribute. This avoids an extra HTTP request.
iframe.srcdoc = bootstrapHtml;2. Via src URL
The host points the iframe to a URL that serves the bootstrap document. This is useful when the bootstrap needs to be dynamically generated per-request.
iframe.src = "/api/agent/bootstrap";When using src, the server MUST respond with Content-Type: text/html and the complete bootstrap document.
Customization Points
Implementations MAY customize the bootstrap document in the following ways:
sseEndpoint— The URL insse-connect. MUST be set to the agent's SSE endpoint.sseEventName— The value ofsse-swap. Defaults to"message".swapStrategy— The value ofhx-swapon the root element. Defaults to"innerHTML".hostOrigin— The origin used for postMessage validation. Defaults to"*". Production deployments SHOULD set this to the actual host origin.themeVars— Initial CSS variable values set in the:rootstyle block.extraHead— Additional content injected into<head>(stylesheets, meta tags).- HTMX CDN URLs — Alternative CDN or self-hosted HTMX URLs.
See the reference server implementation (hypergen-server.ts, bootstrapHtml()) for a parameterized version of the bootstrap document.