PR #3628
Sections
Review

fix: harden canvas review and runtime bootstrap

main ← fix/canvas-review-followups 6 files +412 −38 PR #3628 ↗

Hardens the canvas runtime bootstrap injector for template, SVG/MathML and missing-wrapper HTML and fixes the desktop release review dialog width.

Why this change

The bootstrap injector used a flat tag scan. It could inject the host script inside inert template content, mis-handle SVG and MathML foreign content, and fail on documents without head or body wrappers. The desktop release review dialog also used a fixed width that did not scale on wide screens.

What it does

Architecture, end to end

The backend injects the host bootstrap at serve time. The frontend reviews releases in a responsive dialog. E2E fixtures drive both paths.

flowchart LR
  Pkg[Plugin package] --> Store[(Artifact store)]
  Store --> Runtime[Webapp Runtime Serve]
  Runtime --> Inject[injectRuntimeBootstrap]
  Inject --> Tokenizer[html.Tokenizer + parser state]
  Tokenizer --> Insert[Insertion before first script or implied body]
  Insert --> Frame[iframe host-runtime.js]
  Frame --> Probe[fetch ./_kandev/v1/context]
  Probe --> Parent[postMessage to parent]
  Dialog[CanvasReleaseDialog] --> Hook[useCanvasReleases]
  Hook --> API[listCanvasReleases]
  API --> Review[CanvasReleaseReviewBody]
  Review --> Footer[CanvasReleaseFooter]
  Fixture[canvas-fixture.ts] --> E2E[plugin-canvas.spec.ts]

Key code changes

Drag to pan. Use the + and − buttons to zoom. Click a node to open the full code. The arrows show how the parts interact.

drag to pan · +/− to zoom · click a node for details

type runtimeBootstrapParserState struct
Click for details →

Tracks open elements, namespaces and template depth so the injector knows when a script is inert or foreign.

Parser state
type runtimeBootstrapNamespace uint8

const (
	runtimeBootstrapHTMLNamespace runtimeBootstrapNamespace = iota
	runtimeBootstrapSVGNamespace
	runtimeBootstrapMathMLNamespace
	runtimeBootstrapTemplateTag = "template"
)

var runtimeBootstrapForeignBreakoutTags = map[string]struct{}{
	"b": {}, "big": {}, "blockquote": {}, "body": {}, "br": {}, "center": {}, "code": {}, "dd": {}, "div": {}, "dl": {}, "dt": {}, "em": {}, "embed": {},
	"h1": {}, "h2": {}, "h3": {}, "h4": {}, "h5": {}, "h6": {}, "head": {}, "hr": {}, "i": {}, "img": {}, "li": {}, "listing": {}, "menu": {}, "meta": {},
	"nobr": {}, "ol": {}, "p": {}, "pre": {}, "ruby": {}, "s": {}, "small": {}, "span": {}, "strong": {}, "strike": {}, "sub": {}, "sup": {}, "table": {}, "tt": {},
	"u": {}, "ul": {}, "var": {},
}

type runtimeBootstrapOpenElement struct {
	name           string
	namespace      runtimeBootstrapNamespace
	childNamespace runtimeBootstrapNamespace
}

type runtimeBootstrapParserState struct {
	elements      []runtimeBootstrapOpenElement
	templateDepth int
}
Namespace resolution
func (state *runtimeBootstrapParserState) namespaceForTag(tagName string) runtimeBootstrapNamespace {
	if len(state.elements) == 0 {
		return runtimeBootstrapHTMLNamespace
	}
	top := state.elements[len(state.elements)-1]
	if top.namespace == runtimeBootstrapMathMLNamespace && isRuntimeBootstrapMathMLTextIntegrationPoint(top.name) && tagName != "mglyph" && tagName != "malignmark" {
		return runtimeBootstrapHTMLNamespace
	}
	return top.childNamespace
}

func (state *runtimeBootstrapParserState) elementNamespaces(tagName string, attrs []html.Attribute) (runtimeBootstrapNamespace, runtimeBootstrapNamespace) {
	parentNamespace := state.namespaceForTag(tagName)
	switch parentNamespace {
	case runtimeBootstrapHTMLNamespace:
		switch tagName {
		case "svg":
			return runtimeBootstrapSVGNamespace, runtimeBootstrapSVGNamespace
		case "math":
			return runtimeBootstrapMathMLNamespace, runtimeBootstrapMathMLNamespace
		default:
			return runtimeBootstrapHTMLNamespace, runtimeBootstrapHTMLNamespace
		}
	case runtimeBootstrapSVGNamespace:
		if isRuntimeBootstrapSVGHTMLIntegrationPoint(tagName) {
			return runtimeBootstrapSVGNamespace, runtimeBootstrapHTMLNamespace
		}
		return runtimeBootstrapSVGNamespace, runtimeBootstrapSVGNamespace
	case runtimeBootstrapMathMLNamespace:
		if tagName == "annotation-xml" && hasRuntimeBootstrapHTMLAnnotationEncoding(attrs) {
			return runtimeBootstrapMathMLNamespace, runtimeBootstrapHTMLNamespace
		}
		if isRuntimeBootstrapMathMLTextIntegrationPoint(tagName) {
			return runtimeBootstrapMathMLNamespace, runtimeBootstrapHTMLNamespace
		}
		return runtimeBootstrapMathMLNamespace, runtimeBootstrapMathMLNamespace
	default:
		return runtimeBootstrapHTMLNamespace, runtimeBootstrapHTMLNamespace
	}
}
func runtimeBootstrapInsertion(entry []byte) (int, error)
Click for details →

Finds the first executable script or falls back to the implied body end, and rejects unclosed HTML templates.

Insertion logic
func runtimeBootstrapInsertion(entry []byte) (int, error) {
	tokenizer := html.NewTokenizer(bytes.NewReader(entry))
	tokenizer.SetMaxBuf(len(entry) + 1)
	position := 0
	insertion := -1
	fallback := -1
	state := runtimeBootstrapParserState{}
	for {
		tokenType := tokenizer.Next()
		raw := tokenizer.Raw()
		if tokenType == html.ErrorToken {
			if errors.Is(tokenizer.Err(), io.EOF) {
				break
			}
			return -1, ErrRuntimeBootstrapUnavailable
		}
		if len(raw) == 0 {
			return -1, ErrRuntimeBootstrapUnavailable
		}
		insertion, fallback = updateRuntimeBootstrapPosition(tokenizer, tokenType, raw, position, insertion, fallback, &state)
		position += len(raw)
	}
	if position != len(entry) {
		return -1, ErrRuntimeBootstrapUnavailable
	}
	if insertion < 0 {
		insertion = fallback
	}
	if insertion < 0 {
		if state.templateDepth > 0 {
			return -1, ErrRuntimeBootstrapUnavailable
		}
		// HTML supplies implied head and body elements when an entry omits the
		// corresponding wrapper tags. Appending here keeps the original doctype
		// and encoding declarations intact while placing the script in the
		// implied body, outside any inert template content.
		insertion = len(entry)
	}
	return insertion, nil
}
Start tag handling
func updateRuntimeBootstrapStartTagPosition(tokenizer *html.Tokenizer, token html.Token, tokenType html.TokenType, raw []byte, tagName string, namespace runtimeBootstrapNamespace, position, insertion, fallback int, state *runtimeBootstrapParserState) (int, int) {
	elementNamespace, childNamespace := state.elementNamespaces(tagName, token.Attr)
	if elementNamespace != runtimeBootstrapHTMLNamespace {
		tokenizer.NextIsNotRawText()
	}
	if tagName == "script" && tokenType == html.StartTagToken && state.templateDepth == 0 && insertion < 0 {
		insertion = position
	}
	if elementNamespace == runtimeBootstrapHTMLNamespace && tagName == runtimeBootstrapTemplateTag {
		state.templateDepth++
	}
	if tokenType == html.StartTagToken && namespace == runtimeBootstrapHTMLNamespace && state.templateDepth == 0 {
		insertion, fallback = updateRuntimeBootstrapStartTag(tagName, raw, position, insertion, fallback)
	}
	state.openElement(tagName, elementNamespace, childNamespace, tokenType)
	return insertion, fallback
}
func TestInjectRuntimeBootstrapSkipsTemplateContent(t *testing.T)
Click for details →

Proves the injector skips inert template scripts, handles foreign templates, and supports documents without wrappers.

Template skipping
func TestInjectRuntimeBootstrapSkipsTemplateContent(t *testing.T) {
	entry := `<!doctype html><template><script>window.__template = true;</script></template><script src="./app.js"></script>`
	result, err := injectRuntimeBootstrap([]byte(entry))
	if err != nil {
		t.Fatalf("injectRuntimeBootstrap: %v", err)
	}
	body := string(result)
	bootstrap := `<script src="./_kandev/host-runtime.js"></script>`
	bootstrapIndex := strings.Index(body, bootstrap)
	templateEnd := strings.Index(body, "</template>")
	authoredScript := strings.Index(body, `<script src="./app.js">`)
	if bootstrapIndex <= templateEnd || bootstrapIndex >= authoredScript {
		t.Fatalf("host bootstrap was inserted outside executable document order: %q", body)
	}
}
Omitted wrappers
func TestRuntimeStartupBootstrapSupportsOmittedHTMLWrappers(t *testing.T) {
	entry := `<!doctype html><meta charset="utf-8"><title>Report</title><p>Hello</p>`
	archive := canvasArchive(t, map[string]string{
		"manifest.yaml": staticManifestYAML,
		"ui/index.html": entry,
	})
	pkg, err := ValidatePackage(bytes.NewReader(archive))
	if err != nil {
		t.Fatalf("ValidatePackage: %v", err)
	}
	artifacts, err := NewArtifactStore(filepath.Join(t.TempDir(), "artifacts"))
	if err != nil {
		t.Fatalf("NewArtifactStore: %v", err)
	}
	artifact, err := artifacts.Put(pkg)
	if err != nil {
		t.Fatalf("Put: %v", err)
	}
	manager := NewTokenManager(nil)
	token, err := manager.Issue(CapabilityBinding{
		UserID: "user-1", InstanceID: "instance-1", ReleaseID: "release-1", WebAppKey: "main",
		Placement: "task-canvas", Artifact: artifact, Entry: "ui/index.html",
	}, 0)
	if err != nil {
		t.Fatalf("Issue: %v", err)
	}
	runtime := NewRuntime(manager, artifacts, nil, nil)
	response := httptest.NewRecorder()
	runtime.Serve(response, httptest.NewRequest(http.MethodGet, "/", nil), token, "")
	if response.Code != http.StatusOK {
		t.Fatalf("entry status = %d, body = %s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	bootstrap := `<script src="./_kandev/host-runtime.js"></script>`
	if !strings.HasPrefix(body, `<!doctype html><meta charset="utf-8">`) {
		t.Fatalf("doctype or encoding prefix changed: %q", body)
	}
	if strings.Index(body, bootstrap) <= strings.Index(body, "<p>Hello</p>") {
		t.Fatalf("host bootstrap was not appended to the implied body: %q", body)
	}
}
Foreign template
func TestInjectRuntimeBootstrapDoesNotTreatForeignTemplateAsInert(t *testing.T) {
	entry := `<svg><template><script>window.__svg = true;</script></template></svg><script src="./app.js"></script>`
	result, err := injectRuntimeBootstrap([]byte(entry))
	if err != nil {
		t.Fatalf("injectRuntimeBootstrap: %v", err)
	}
	body := string(result)
	bootstrap := `<script src="./_kandev/host-runtime.js"></script>`
	bootstrapIndex := strings.Index(body, bootstrap)
	foreignScript := strings.Index(body, `<script>window.__svg = true;</script>`)
	if bootstrapIndex < 0 || foreignScript < 0 || bootstrapIndex >= foreignScript {
		t.Fatalf("host bootstrap was inserted after foreign executable script: %q", body)
	}
}
export function CanvasReleaseDialog({ canvas, open, onOpenChange }: { canvas: Canvas | null })
Click for details →

Makes the desktop dialog use a responsive max width so it stays wide on large screens without breaking mobile.

Width fix
    const surfaceClassName = isMobile
      ? "!left-0 !top-0 !h-dvh !max-h-dvh !w-screen !max-w-none !translate-x-0 !translate-y-0 flex flex-col gap-0 overflow-hidden rounded-none p-0 [padding-top:max(1rem,env(safe-area-inset-top))]"
    : "flex h-[min(90dvh,48rem)] max-h-[calc(100dvh-2rem)] w-full max-w-[48rem] flex-col gap-0 overflow-hidden p-0";
    : "flex h-[min(90dvh,48rem)] max-h-[calc(100dvh-2rem)] w-full sm:max-w-[48rem] flex-col gap-0 overflow-hidden p-0";
E2E fixture and desktop layout testapps/web/e2e/tests/canvas/canvas-fixture.ts ↗
function canvasCapabilities(options?: CanvasSourceOptions): string[]
Click for details →

Adds permission variants and release helpers so the E2E test can publish a minimal-permission canvas and verify the dialog layout.

Fixture helpers
export type CanvasReleaseRecord = {
  id: string;
  validation_status?: string;
  permissions?: {
    reads?: string[];
    writes?: string[];
    events?: string[];
    shared_state?: boolean;
  };
};

export async function listCanvasReleases(
  apiClient: ApiClient,
  canvasId: string,
): Promise<CanvasReleaseRecord[]> {
  const response = await apiClient.rawRequest(
    "GET",
    `/api/v1/canvases/${encodeURIComponent(canvasId)}/releases`,
  );
  if (!response.ok) {
    throw new Error(`Canvas release lookup failed (${response.status}).`);
  }
  const body = (await response.json()) as { releases?: CanvasReleaseRecord[] };
  return body.releases ?? [];
}

export type CanvasSourceOptions = {
  noPermissions?: boolean;
  minimalPermissions?: boolean;
};

function canvasCapabilities(options?: CanvasSourceOptions): string[] {
  if (options?.noPermissions) return [];
  if (options?.minimalPermissions) return ["  api_read:", "    - tasks", "    - workflows"];
  return [
    "  api_read:",
    "    - tasks",
    "    - workflows",
    "  api_write:",
    "    - tasks",
    "    - messages",
    "  events:",
    "    - task.updated",
    "  state: true",
  ];
}
Desktop layout test
test("keeps the desktop release review wide without scrolling two permissions", async ({
  testPage, apiClient, backend, seedData,
}) => {
  await testPage.setViewportSize({ width: 1280, height: 720 });
  const releaseFeature = await enableCanvasFeature(backend, apiClient, seedData.workspaceId);
  const seeded = await seedTaskCanvas(testPage, apiClient, seedData, true, { noPermissions: true });
  writeCanvasSource(workspacePath, seeded.canvas, { minimalPermissions: true });
  // publish a permission-increasing release and wait for pending_permission
  await expect.poll(async () => {
    const releases = await listCanvasReleases(apiClient, canvasId!);
    return releases.find((r) => r.validation_status === "pending_permission")?.id ?? null;
  }).not.toBeNull();
  await testPage.goto(canvasHref(canvasId));
  await testPage.getByRole("button", { name: "Releases and permissions" }).click();
  const dialog = testPage.getByTestId("canvas-releases-dialog");
  const dialogBox = await dialog.boundingBox();
  expect(dialogBox?.width).toBeGreaterThanOrEqual(720);
  expect(dialogBox?.width).toBeLessThanOrEqual(780);
  const permissions = dialog.getByTestId("canvas-permission-summary");
  await expect(permissions.locator("li")).toHaveCount(2);
  const scrollMetrics = await dialog.getByTestId("canvas-release-review-scroll").evaluate((el) => ({
    clientHeight: el.clientHeight,
    scrollHeight: el.scrollHeight,
  }));
  expect(scrollMetrics.scrollHeight).toBeLessThanOrEqual(scrollMetrics.clientHeight);
});
Read the changes as a list

Namespace-aware bootstrap parser

apps/backend/internal/plugins/webapp/runtime_bootstrap.go

Tracks open elements, namespaces and template depth so the injector knows when a script is inert or foreign.

Parser state
type runtimeBootstrapNamespace uint8

const (
	runtimeBootstrapHTMLNamespace runtimeBootstrapNamespace = iota
	runtimeBootstrapSVGNamespace
	runtimeBootstrapMathMLNamespace
	runtimeBootstrapTemplateTag = "template"
)

var runtimeBootstrapForeignBreakoutTags = map[string]struct{}{
	"b": {}, "big": {}, "blockquote": {}, "body": {}, "br": {}, "center": {}, "code": {}, "dd": {}, "div": {}, "dl": {}, "dt": {}, "em": {}, "embed": {},
	"h1": {}, "h2": {}, "h3": {}, "h4": {}, "h5": {}, "h6": {}, "head": {}, "hr": {}, "i": {}, "img": {}, "li": {}, "listing": {}, "menu": {}, "meta": {},
	"nobr": {}, "ol": {}, "p": {}, "pre": {}, "ruby": {}, "s": {}, "small": {}, "span": {}, "strong": {}, "strike": {}, "sub": {}, "sup": {}, "table": {}, "tt": {},
	"u": {}, "ul": {}, "var": {},
}

type runtimeBootstrapOpenElement struct {
	name           string
	namespace      runtimeBootstrapNamespace
	childNamespace runtimeBootstrapNamespace
}

type runtimeBootstrapParserState struct {
	elements      []runtimeBootstrapOpenElement
	templateDepth int
}
Namespace resolution
func (state *runtimeBootstrapParserState) namespaceForTag(tagName string) runtimeBootstrapNamespace {
	if len(state.elements) == 0 {
		return runtimeBootstrapHTMLNamespace
	}
	top := state.elements[len(state.elements)-1]
	if top.namespace == runtimeBootstrapMathMLNamespace && isRuntimeBootstrapMathMLTextIntegrationPoint(top.name) && tagName != "mglyph" && tagName != "malignmark" {
		return runtimeBootstrapHTMLNamespace
	}
	return top.childNamespace
}

func (state *runtimeBootstrapParserState) elementNamespaces(tagName string, attrs []html.Attribute) (runtimeBootstrapNamespace, runtimeBootstrapNamespace) {
	parentNamespace := state.namespaceForTag(tagName)
	switch parentNamespace {
	case runtimeBootstrapHTMLNamespace:
		switch tagName {
		case "svg":
			return runtimeBootstrapSVGNamespace, runtimeBootstrapSVGNamespace
		case "math":
			return runtimeBootstrapMathMLNamespace, runtimeBootstrapMathMLNamespace
		default:
			return runtimeBootstrapHTMLNamespace, runtimeBootstrapHTMLNamespace
		}
	case runtimeBootstrapSVGNamespace:
		if isRuntimeBootstrapSVGHTMLIntegrationPoint(tagName) {
			return runtimeBootstrapSVGNamespace, runtimeBootstrapHTMLNamespace
		}
		return runtimeBootstrapSVGNamespace, runtimeBootstrapSVGNamespace
	case runtimeBootstrapMathMLNamespace:
		if tagName == "annotation-xml" && hasRuntimeBootstrapHTMLAnnotationEncoding(attrs) {
			return runtimeBootstrapMathMLNamespace, runtimeBootstrapHTMLNamespace
		}
		if isRuntimeBootstrapMathMLTextIntegrationPoint(tagName) {
			return runtimeBootstrapMathMLNamespace, runtimeBootstrapHTMLNamespace
		}
		return runtimeBootstrapMathMLNamespace, runtimeBootstrapMathMLNamespace
	default:
		return runtimeBootstrapHTMLNamespace, runtimeBootstrapHTMLNamespace
	}
}

Insertion with implied body fallback

apps/backend/internal/plugins/webapp/runtime_bootstrap.go

Finds the first executable script or falls back to the implied body end, and rejects unclosed HTML templates.

Insertion logic
func runtimeBootstrapInsertion(entry []byte) (int, error) {
	tokenizer := html.NewTokenizer(bytes.NewReader(entry))
	tokenizer.SetMaxBuf(len(entry) + 1)
	position := 0
	insertion := -1
	fallback := -1
	state := runtimeBootstrapParserState{}
	for {
		tokenType := tokenizer.Next()
		raw := tokenizer.Raw()
		if tokenType == html.ErrorToken {
			if errors.Is(tokenizer.Err(), io.EOF) {
				break
			}
			return -1, ErrRuntimeBootstrapUnavailable
		}
		if len(raw) == 0 {
			return -1, ErrRuntimeBootstrapUnavailable
		}
		insertion, fallback = updateRuntimeBootstrapPosition(tokenizer, tokenType, raw, position, insertion, fallback, &state)
		position += len(raw)
	}
	if position != len(entry) {
		return -1, ErrRuntimeBootstrapUnavailable
	}
	if insertion < 0 {
		insertion = fallback
	}
	if insertion < 0 {
		if state.templateDepth > 0 {
			return -1, ErrRuntimeBootstrapUnavailable
		}
		// HTML supplies implied head and body elements when an entry omits the
		// corresponding wrapper tags. Appending here keeps the original doctype
		// and encoding declarations intact while placing the script in the
		// implied body, outside any inert template content.
		insertion = len(entry)
	}
	return insertion, nil
}
Start tag handling
func updateRuntimeBootstrapStartTagPosition(tokenizer *html.Tokenizer, token html.Token, tokenType html.TokenType, raw []byte, tagName string, namespace runtimeBootstrapNamespace, position, insertion, fallback int, state *runtimeBootstrapParserState) (int, int) {
	elementNamespace, childNamespace := state.elementNamespaces(tagName, token.Attr)
	if elementNamespace != runtimeBootstrapHTMLNamespace {
		tokenizer.NextIsNotRawText()
	}
	if tagName == "script" && tokenType == html.StartTagToken && state.templateDepth == 0 && insertion < 0 {
		insertion = position
	}
	if elementNamespace == runtimeBootstrapHTMLNamespace && tagName == runtimeBootstrapTemplateTag {
		state.templateDepth++
	}
	if tokenType == html.StartTagToken && namespace == runtimeBootstrapHTMLNamespace && state.templateDepth == 0 {
		insertion, fallback = updateRuntimeBootstrapStartTag(tagName, raw, position, insertion, fallback)
	}
	state.openElement(tagName, elementNamespace, childNamespace, tokenType)
	return insertion, fallback
}

Bootstrap edge-case tests

apps/backend/internal/plugins/webapp/runtime_test.go

Proves the injector skips inert template scripts, handles foreign templates, and supports documents without wrappers.

Template skipping
func TestInjectRuntimeBootstrapSkipsTemplateContent(t *testing.T) {
	entry := `<!doctype html><template><script>window.__template = true;</script></template><script src="./app.js"></script>`
	result, err := injectRuntimeBootstrap([]byte(entry))
	if err != nil {
		t.Fatalf("injectRuntimeBootstrap: %v", err)
	}
	body := string(result)
	bootstrap := `<script src="./_kandev/host-runtime.js"></script>`
	bootstrapIndex := strings.Index(body, bootstrap)
	templateEnd := strings.Index(body, "</template>")
	authoredScript := strings.Index(body, `<script src="./app.js">`)
	if bootstrapIndex <= templateEnd || bootstrapIndex >= authoredScript {
		t.Fatalf("host bootstrap was inserted outside executable document order: %q", body)
	}
}
Omitted wrappers
func TestRuntimeStartupBootstrapSupportsOmittedHTMLWrappers(t *testing.T) {
	entry := `<!doctype html><meta charset="utf-8"><title>Report</title><p>Hello</p>`
	archive := canvasArchive(t, map[string]string{
		"manifest.yaml": staticManifestYAML,
		"ui/index.html": entry,
	})
	pkg, err := ValidatePackage(bytes.NewReader(archive))
	if err != nil {
		t.Fatalf("ValidatePackage: %v", err)
	}
	artifacts, err := NewArtifactStore(filepath.Join(t.TempDir(), "artifacts"))
	if err != nil {
		t.Fatalf("NewArtifactStore: %v", err)
	}
	artifact, err := artifacts.Put(pkg)
	if err != nil {
		t.Fatalf("Put: %v", err)
	}
	manager := NewTokenManager(nil)
	token, err := manager.Issue(CapabilityBinding{
		UserID: "user-1", InstanceID: "instance-1", ReleaseID: "release-1", WebAppKey: "main",
		Placement: "task-canvas", Artifact: artifact, Entry: "ui/index.html",
	}, 0)
	if err != nil {
		t.Fatalf("Issue: %v", err)
	}
	runtime := NewRuntime(manager, artifacts, nil, nil)
	response := httptest.NewRecorder()
	runtime.Serve(response, httptest.NewRequest(http.MethodGet, "/", nil), token, "")
	if response.Code != http.StatusOK {
		t.Fatalf("entry status = %d, body = %s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	bootstrap := `<script src="./_kandev/host-runtime.js"></script>`
	if !strings.HasPrefix(body, `<!doctype html><meta charset="utf-8">`) {
		t.Fatalf("doctype or encoding prefix changed: %q", body)
	}
	if strings.Index(body, bootstrap) <= strings.Index(body, "<p>Hello</p>") {
		t.Fatalf("host bootstrap was not appended to the implied body: %q", body)
	}
}
Foreign template
func TestInjectRuntimeBootstrapDoesNotTreatForeignTemplateAsInert(t *testing.T) {
	entry := `<svg><template><script>window.__svg = true;</script></template></svg><script src="./app.js"></script>`
	result, err := injectRuntimeBootstrap([]byte(entry))
	if err != nil {
		t.Fatalf("injectRuntimeBootstrap: %v", err)
	}
	body := string(result)
	bootstrap := `<script src="./_kandev/host-runtime.js"></script>`
	bootstrapIndex := strings.Index(body, bootstrap)
	foreignScript := strings.Index(body, `<script>window.__svg = true;</script>`)
	if bootstrapIndex < 0 || foreignScript < 0 || bootstrapIndex >= foreignScript {
		t.Fatalf("host bootstrap was inserted after foreign executable script: %q", body)
	}
}

Responsive desktop review dialog

apps/web/components/settings/canvas-release-review.tsx

Makes the desktop dialog use a responsive max width so it stays wide on large screens without breaking mobile.

Width fix
    const surfaceClassName = isMobile
      ? "!left-0 !top-0 !h-dvh !max-h-dvh !w-screen !max-w-none !translate-x-0 !translate-y-0 flex flex-col gap-0 overflow-hidden rounded-none p-0 [padding-top:max(1rem,env(safe-area-inset-top))]"
    : "flex h-[min(90dvh,48rem)] max-h-[calc(100dvh-2rem)] w-full max-w-[48rem] flex-col gap-0 overflow-hidden p-0";
    : "flex h-[min(90dvh,48rem)] max-h-[calc(100dvh-2rem)] w-full sm:max-w-[48rem] flex-col gap-0 overflow-hidden p-0";

E2E fixture and desktop layout test

apps/web/e2e/tests/canvas/canvas-fixture.ts

Adds permission variants and release helpers so the E2E test can publish a minimal-permission canvas and verify the dialog layout.

Fixture helpers
export type CanvasReleaseRecord = {
  id: string;
  validation_status?: string;
  permissions?: {
    reads?: string[];
    writes?: string[];
    events?: string[];
    shared_state?: boolean;
  };
};

export async function listCanvasReleases(
  apiClient: ApiClient,
  canvasId: string,
): Promise<CanvasReleaseRecord[]> {
  const response = await apiClient.rawRequest(
    "GET",
    `/api/v1/canvases/${encodeURIComponent(canvasId)}/releases`,
  );
  if (!response.ok) {
    throw new Error(`Canvas release lookup failed (${response.status}).`);
  }
  const body = (await response.json()) as { releases?: CanvasReleaseRecord[] };
  return body.releases ?? [];
}

export type CanvasSourceOptions = {
  noPermissions?: boolean;
  minimalPermissions?: boolean;
};

function canvasCapabilities(options?: CanvasSourceOptions): string[] {
  if (options?.noPermissions) return [];
  if (options?.minimalPermissions) return ["  api_read:", "    - tasks", "    - workflows"];
  return [
    "  api_read:",
    "    - tasks",
    "    - workflows",
    "  api_write:",
    "    - tasks",
    "    - messages",
    "  events:",
    "    - task.updated",
    "  state: true",
  ];
}
Desktop layout test
test("keeps the desktop release review wide without scrolling two permissions", async ({
  testPage, apiClient, backend, seedData,
}) => {
  await testPage.setViewportSize({ width: 1280, height: 720 });
  const releaseFeature = await enableCanvasFeature(backend, apiClient, seedData.workspaceId);
  const seeded = await seedTaskCanvas(testPage, apiClient, seedData, true, { noPermissions: true });
  writeCanvasSource(workspacePath, seeded.canvas, { minimalPermissions: true });
  // publish a permission-increasing release and wait for pending_permission
  await expect.poll(async () => {
    const releases = await listCanvasReleases(apiClient, canvasId!);
    return releases.find((r) => r.validation_status === "pending_permission")?.id ?? null;
  }).not.toBeNull();
  await testPage.goto(canvasHref(canvasId));
  await testPage.getByRole("button", { name: "Releases and permissions" }).click();
  const dialog = testPage.getByTestId("canvas-releases-dialog");
  const dialogBox = await dialog.boundingBox();
  expect(dialogBox?.width).toBeGreaterThanOrEqual(720);
  expect(dialogBox?.width).toBeLessThanOrEqual(780);
  const permissions = dialog.getByTestId("canvas-permission-summary");
  await expect(permissions.locator("li")).toHaveCount(2);
  const scrollMetrics = await dialog.getByTestId("canvas-release-review-scroll").evaluate((el) => ({
    clientHeight: el.clientHeight,
    scrollHeight: el.scrollHeight,
  }));
  expect(scrollMetrics.scrollHeight).toBeLessThanOrEqual(scrollMetrics.clientHeight);
});

Data and storage

The PR adds no new stored fields. It hardens how the existing entry HTML is parsed and how releases are reviewed.

FieldTypeNotes
CanvasReleaseRecord.idstringrelease identifier used by the E2E helper
CanvasReleaseRecord.validation_statusstringpending_permission, valid, or other status
CanvasReleaseRecord.permissionsobjectreads, writes, events and shared_state for the review
CanvasSourceOptionsobjectnoPermissions or minimalPermissions for fixture variants

Risk

4 / 10 Medium
1 low5 medium10 high

Why this score

  • Bootstrap injection is security-sensitive, but the change is isolated to entry HTML and covered by new unit tests.
  • Dialog width change is a single responsive class with no data or API change.
  • E2E adds coverage for the desktop layout and permission flow.

Trade-offs and review notes

Where to look first

  1. Check runtime_bootstrap.go: templateDepth handling, foreign breakout, and the implied-body fallback at len(entry).
  2. Check runtime_test.go: omitted wrappers, template skipping, and foreign template cases.
  3. Check canvas-release-review.tsx: the sm:max-w change and mobile branch still uses h-dvh.
  4. Check canvas-fixture.ts and plugin-canvas.spec.ts: permission variants and the 720-780px width assertion.