Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion apps/website/e2e/platform-production-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
);
});
Expand Down
6 changes: 6 additions & 0 deletions libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 2 additions & 4 deletions scripts/assemble-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
'<base href="/">',
`<base href="/${cap.product}/${cap.topic}/">`
);
const fixed = prepareExampleHtml(html, cap.product, cap.topic);
writeFileSync(indexPath, fixed);
}

Expand Down
13 changes: 13 additions & 0 deletions scripts/ci-workflow.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
4 changes: 3 additions & 1 deletion scripts/generate-runtime-parent-origins.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
1 change: 0 additions & 1 deletion scripts/generate-runtime-parent-origins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const RUNTIME_CONNECT_SOURCES = [
'https:',
'http://localhost:*',
'http://127.0.0.1:*',
'http://[::1]:*',
] as const;

export interface RuntimeParentOriginPolicy {
Expand Down
24 changes: 24 additions & 0 deletions scripts/prepare-example-html.spec.ts
Original file line number Diff line number Diff line change
@@ -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 = `<base href="/"><link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.css"></noscript>`;
const prepared = prepareExampleHtml(html, 'ag-ui', 'interrupts');
expect(prepared).toContain('<base href="/ag-ui/interrupts/">');
expect(prepared).toContain(
'<link rel="stylesheet" href="styles.css" media="all">'
);
expect(prepared).not.toContain('onload=');
expect(prepared).toContain(
'<noscript><link rel="stylesheet" href="styles.css"></noscript>'
);
expect(prepareExampleHtml(prepared, 'ag-ui', 'interrupts')).toBe(prepared);
});

it('preserves genuine print styles and script assets', () => {
const html =
'<link rel="stylesheet" href="print.css" media="print"><script src="main.js" type="module"></script>';
expect(prepareExampleHtml(html, 'chat', 'threads')).toBe(html);
});
});
21 changes: 21 additions & 0 deletions scripts/prepare-example-html.ts
Original file line number Diff line number Diff line change
@@ -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('<base href="/">', `<base href="/${product}/${topic}/">`)
.replace(/<link\b[^>]*>/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\'"', '');
});
}
Loading