diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3afc6817d..01f0c653e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1082,7 +1082,7 @@ jobs:
changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
deploy_relevant=false
- if printf '%s\n' "$changed_files" | grep -E '^(\.github/workflows/ci\.yml|runtime-parent-origins\.json|vercel\.(json|examples\.json)|apps/website/.*|cockpit/.*|examples/chat/.*|libs/.*|scripts/(assemble-examples|demo-middleware|generate-runtime-parent-origins|langgraph-proxy|rate-limit)\.ts|scripts/assemble-demo\.ts)$' >/dev/null; then
+ if printf '%s\n' "$changed_files" | grep -E '^(\.github/workflows/ci\.yml|runtime-parent-origins\.json|vercel\.(json|examples\.json)|apps/website/.*|cockpit/.*|examples/chat/.*|libs/.*|scripts/(assemble-examples|prepare-example-html|demo-middleware|generate-runtime-parent-origins|langgraph-proxy|rate-limit)\.ts|scripts/assemble-demo\.ts)$' >/dev/null; then
deploy_relevant=true
fi
@@ -1101,7 +1101,7 @@ jobs:
if printf '%s\n' "$changed_files" | grep -E '^cockpit/.*/angular/' >/dev/null; then
examples_changed=true
fi
- if printf '%s\n' "$changed_files" | grep -E '^(runtime-parent-origins\.json|vercel\.examples\.json|scripts/(assemble-examples|examples-middleware|generate-runtime-parent-origins|langgraph-proxy|upstash-rate-limit)\.ts)$' >/dev/null; then
+ if printf '%s\n' "$changed_files" | grep -E '^(runtime-parent-origins\.json|vercel\.examples\.json|scripts/(assemble-examples|prepare-example-html|examples-middleware|generate-runtime-parent-origins|langgraph-proxy|upstash-rate-limit)\.ts)$' >/dev/null; then
examples_changed=true
fi
if printf '%s\n' "$changed_files" | grep -E '^(\.github/workflows/ci\.yml|vercel\.json|apps/website/.*)$' >/dev/null; then
diff --git a/apps/website/e2e/platform-production-smoke.spec.ts b/apps/website/e2e/platform-production-smoke.spec.ts
index bcffe7b89..5f259163a 100644
--- a/apps/website/e2e/platform-production-smoke.spec.ts
+++ b/apps/website/e2e/platform-production-smoke.spec.ts
@@ -200,8 +200,11 @@ test.describe('Production: unified runtime embedding policy', () => {
}
}
expect(policy).toContain(
- "connect-src 'self' https: http://localhost:* http://127.0.0.1:* http://[::1]:*"
+ "connect-src 'self' https: http://localhost:* http://127.0.0.1:*"
);
+ expect(policy).not.toContain('http://[::1]:*');
+ expect(policy).toContain("script-src 'self';");
+ expect(await response.text()).not.toContain('onload="this.media=');
expect(frameAncestors).not.toContain('*');
expect(frameAncestors).not.toContain('cockpit.threadplane.ai');
expect(headers['referrer-policy']).toBe('origin');
diff --git a/libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.spec.ts b/libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.spec.ts
index 2fbc68c83..126d8e30e 100644
--- a/libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.spec.ts
+++ b/libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.spec.ts
@@ -82,6 +82,10 @@ describe('CockpitTelemetryService', () => {
bootstrap: { distinctID: 'd1' },
autocapture: false,
capture_pageview: false,
+ advanced_disable_flags: true,
+ disable_surveys: true,
+ disable_session_recording: true,
+ disable_external_dependency_loading: true,
}),
);
});
diff --git a/libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.ts b/libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.ts
index 8d00aaeae..c9e49e90a 100644
--- a/libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.ts
+++ b/libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.ts
@@ -24,6 +24,12 @@ export class CockpitTelemetryService {
bootstrap: { distinctID: this.config.distinctId },
autocapture: false,
capture_pageview: false,
+ // This client only sends explicit lifecycle events. Optional downloaded
+ // features are unnecessary and conflict with the embedded runtime CSP.
+ advanced_disable_flags: true,
+ disable_surveys: true,
+ disable_session_recording: true,
+ disable_external_dependency_loading: true,
});
this.subscribeChat();
diff --git a/scripts/assemble-examples.ts b/scripts/assemble-examples.ts
index c37cd5e0e..bf20283d2 100644
--- a/scripts/assemble-examples.ts
+++ b/scripts/assemble-examples.ts
@@ -19,6 +19,7 @@ import {
} from 'fs';
import { resolve } from 'path';
import { capabilities as registryCapabilities } from '@threadplane/cockpit-registry';
+import { prepareExampleHtml } from './prepare-example-html';
import {
GENERATED_RUNTIME_PARENT_ORIGINS_MODULE,
generateRuntimeParentOriginPolicy,
@@ -97,10 +98,7 @@ for (const cap of capabilities) {
const indexPath = resolve(dest, 'index.html');
if (existsSync(indexPath)) {
const html = readFileSync(indexPath, 'utf-8');
- const fixed = html.replace(
- '',
- ``
- );
+ const fixed = prepareExampleHtml(html, cap.product, cap.topic);
writeFileSync(indexPath, fixed);
}
diff --git a/scripts/ci-workflow.spec.mjs b/scripts/ci-workflow.spec.mjs
index 509315d95..1f22d6a60 100644
--- a/scripts/ci-workflow.spec.mjs
+++ b/scripts/ci-workflow.spec.mjs
@@ -247,6 +247,19 @@ describe('CI workflow', () => {
);
});
+ it('redeploys examples when their HTML preparation changes', async () => {
+ const deployJob = await readDeployJob();
+ const path = 'scripts/prepare-example-html.ts';
+ const preflight = deployJob.match(/grep -E '([^']+)' >\/dev\/null/);
+ assert.match(path, new RegExp(preflight?.[1] ?? '(?!)'));
+ const examplesDetection = deployJob.slice(
+ deployJob.indexOf('Check if examples changed'),
+ deployJob.indexOf('- uses: actions/setup-node')
+ );
+ assert.ok([...examplesDetection.matchAll(/grep -E '([^']+)'/g)]
+ .some((match) => new RegExp(match[1]).test(path)));
+ });
+
it('isolates langgraph coverage before the remaining bounded library tests', async () => {
const libraryJob = await readLibraryJob();
diff --git a/scripts/generate-runtime-parent-origins.spec.ts b/scripts/generate-runtime-parent-origins.spec.ts
index 1fa6b2abd..4974f2911 100644
--- a/scripts/generate-runtime-parent-origins.spec.ts
+++ b/scripts/generate-runtime-parent-origins.spec.ts
@@ -96,8 +96,10 @@ describe('generate runtime parent origins', () => {
'Referrer-Policy': 'origin',
});
expect(policy.deploymentHeaders['Content-Security-Policy']).toContain(
- "connect-src 'self' https: http://localhost:* http://127.0.0.1:* http://[::1]:*"
+ "connect-src 'self' https: http://localhost:* http://127.0.0.1:*"
);
+ expect(policy.deploymentHeaders['Content-Security-Policy']).not.toContain('[::1]');
+ expect(policy.deploymentHeaders['Content-Security-Policy']).toContain("script-src 'self';");
expect(JSON.stringify(policy)).not.toMatch(
/apiKey|authorization|secret|token/i
);
diff --git a/scripts/generate-runtime-parent-origins.ts b/scripts/generate-runtime-parent-origins.ts
index 42f9ed19e..b1f3ae16b 100644
--- a/scripts/generate-runtime-parent-origins.ts
+++ b/scripts/generate-runtime-parent-origins.ts
@@ -8,7 +8,6 @@ const RUNTIME_CONNECT_SOURCES = [
'https:',
'http://localhost:*',
'http://127.0.0.1:*',
- 'http://[::1]:*',
] as const;
export interface RuntimeParentOriginPolicy {
diff --git a/scripts/prepare-example-html.spec.ts b/scripts/prepare-example-html.spec.ts
new file mode 100644
index 000000000..c5b71aab3
--- /dev/null
+++ b/scripts/prepare-example-html.spec.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from 'vitest';
+import { prepareExampleHtml } from './prepare-example-html';
+
+describe('assembled example HTML under script-src self', () => {
+ it('activates Angular deferred CSS without executing an inline handler', () => {
+ const html = ``;
+ const prepared = prepareExampleHtml(html, 'ag-ui', 'interrupts');
+ expect(prepared).toContain('');
+ expect(prepared).toContain(
+ ''
+ );
+ expect(prepared).not.toContain('onload=');
+ expect(prepared).toContain(
+ ''
+ );
+ expect(prepareExampleHtml(prepared, 'ag-ui', 'interrupts')).toBe(prepared);
+ });
+
+ it('preserves genuine print styles and script assets', () => {
+ const html =
+ '';
+ expect(prepareExampleHtml(html, 'chat', 'threads')).toBe(html);
+ });
+});
diff --git a/scripts/prepare-example-html.ts b/scripts/prepare-example-html.ts
new file mode 100644
index 000000000..50808663f
--- /dev/null
+++ b/scripts/prepare-example-html.ts
@@ -0,0 +1,21 @@
+/** Prepare Angular output for subpath hosting under script-src 'self'. */
+export function prepareExampleHtml(
+ html: string,
+ product: string,
+ topic: string
+): string {
+ return html
+ .replace('', ``)
+ .replace(/]*>/g, (tag) => {
+ // Angular's critical-CSS optimization defers the full stylesheet with an
+ // inline handler. Load it normally because the runtime CSP forbids handlers.
+ if (
+ !tag.includes('rel="stylesheet"') ||
+ !tag.includes('onload="this.media=\'all\'"')
+ )
+ return tag;
+ return tag
+ .replace('media="print"', 'media="all"')
+ .replace(' onload="this.media=\'all\'"', '');
+ });
+}