@agent-native/core 0.84.65 → 0.84.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2044
31
- - template files: 5023
31
+ - template files: 5024
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.67
4
+
5
+ ### Patch Changes
6
+
7
+ - 171f6e6: Remove app document CSP headers so hosted Google Tag Manager and framework inline bootstrap scripts are not blocked or reported by a shared policy.
8
+
9
+ ## 0.84.66
10
+
11
+ ### Patch Changes
12
+
13
+ - 70a6085: Keep hosted chat runs inside the active worker when a progress-aware action-preparation checkpoint asks to continue, instead of depending on the browser to start the recovery turn.
14
+
3
15
  ## 0.84.65
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.65",
3
+ "version": "0.84.67",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -971,6 +971,12 @@ const TOOL_INPUT_ACTIVITY_INTERVAL_MS = 1500;
971
971
  const ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS = 90_000;
972
972
  const ACTION_PREPARATION_ZERO_BYTE_RESTART_LIMIT = 2;
973
973
  const MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS = 90_000;
974
+ const MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT = 6;
975
+ const RUN_BUDGET_EXHAUSTED_ERROR_CODE = "run_budget_exhausted";
976
+ const RUN_BUDGET_EXHAUSTED_MESSAGE =
977
+ "I ran out of time before finishing this step. " +
978
+ "I stopped rather than keep retrying silently. " +
979
+ "Check any completed tool cards above before retrying, ideally as one smaller follow-up.";
974
980
  const MAX_TEXT_ATTACHMENT_CHARS = 60_000;
975
981
  const MAX_SELECTION_CONTEXT_CHARS = 8_000;
976
982
  const MAX_RESOURCE_INVENTORY_ITEMS = 40;
@@ -2978,7 +2984,7 @@ export async function runAgentLoop(opts: {
2978
2984
  };
2979
2985
  }
2980
2986
  return (
2981
- zeroByteToolInputRestart.count >
2987
+ zeroByteToolInputRestart.count >=
2982
2988
  ACTION_PREPARATION_ZERO_BYTE_RESTART_LIMIT &&
2983
2989
  now - zeroByteToolInputRestart.firstStartedAt >=
2984
2990
  ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS
@@ -4430,6 +4436,82 @@ export function backgroundContinuationReasonForRun(
4430
4436
  return "run_timeout";
4431
4437
  }
4432
4438
 
4439
+ export async function runAgentLoopWithMainChatInternalContinuations(
4440
+ opts: Parameters<typeof runAgentLoop>[0],
4441
+ ): Promise<Awaited<ReturnType<typeof runAgentLoop>>> {
4442
+ const usage: Awaited<ReturnType<typeof runAgentLoop>> = {
4443
+ inputTokens: 0,
4444
+ outputTokens: 0,
4445
+ cacheReadTokens: 0,
4446
+ cacheWriteTokens: 0,
4447
+ model: opts.model,
4448
+ };
4449
+ const addUsage = (next: Awaited<ReturnType<typeof runAgentLoop>>) => {
4450
+ usage.inputTokens += next.inputTokens;
4451
+ usage.outputTokens += next.outputTokens;
4452
+ usage.cacheReadTokens += next.cacheReadTokens;
4453
+ usage.cacheWriteTokens += next.cacheWriteTokens;
4454
+ usage.model = next.model;
4455
+ };
4456
+
4457
+ const localTurnEvents: AgentChatEvent[] = [];
4458
+ let lastAttemptWasUnfinishedContinuation = false;
4459
+ for (
4460
+ let attempt = 0;
4461
+ !opts.signal.aborted && attempt < MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT;
4462
+ attempt++
4463
+ ) {
4464
+ lastAttemptWasUnfinishedContinuation = false;
4465
+ let continuationReason: AgentLoopContinuationReason | undefined;
4466
+ const attemptStartIndex = localTurnEvents.length;
4467
+ const send = (event: AgentChatEvent) => {
4468
+ localTurnEvents.push(event);
4469
+ if (
4470
+ event.type === "auto_continue" &&
4471
+ isAgentLoopContinuationReason(event.reason)
4472
+ ) {
4473
+ continuationReason = event.reason;
4474
+ return;
4475
+ }
4476
+ opts.send(event);
4477
+ };
4478
+
4479
+ const nextUsage = await runAgentLoop({ ...opts, send });
4480
+ addUsage(nextUsage);
4481
+
4482
+ if (!continuationReason || opts.signal.aborted) {
4483
+ return usage;
4484
+ }
4485
+
4486
+ lastAttemptWasUnfinishedContinuation = true;
4487
+ const attemptEvents = localTurnEvents.slice(attemptStartIndex);
4488
+ const completedSideEffect = attemptEvents.some(
4489
+ (event) =>
4490
+ event.type === "tool_done" &&
4491
+ event.completedSideEffect === true &&
4492
+ event.isError !== true,
4493
+ );
4494
+ if (!completedSideEffect) {
4495
+ opts.send({ type: "clear" });
4496
+ }
4497
+ const actionPreparationTool =
4498
+ lastUnfinishedPreparingActionToolFromEvents(localTurnEvents);
4499
+ appendAgentLoopContinuation(opts.messages, continuationReason, {
4500
+ ...(actionPreparationTool ? { actionPreparationTool } : {}),
4501
+ });
4502
+ }
4503
+
4504
+ if (!opts.signal.aborted && lastAttemptWasUnfinishedContinuation) {
4505
+ opts.send({
4506
+ type: "error",
4507
+ error: RUN_BUDGET_EXHAUSTED_MESSAGE,
4508
+ errorCode: RUN_BUDGET_EXHAUSTED_ERROR_CODE,
4509
+ recoverable: true,
4510
+ });
4511
+ }
4512
+ return usage;
4513
+ }
4514
+
4433
4515
  function endsAtContinuationBoundary(run: ActiveRun): boolean {
4434
4516
  return (
4435
4517
  endsAtInternalContinuationBoundary(run) ||
@@ -5770,6 +5852,11 @@ export function createProductionAgentHandler(
5770
5852
  continuationDispatchPath,
5771
5853
  );
5772
5854
  try {
5855
+ await recordRunDiagnostic(
5856
+ run.runId,
5857
+ RUN_DIAG_STAGE.workerSetupStep,
5858
+ `chain_dispatch_start nextRunId=${nextRunId} reason=${continuationReason} path=${continuationDispatchPath}`,
5859
+ ).catch(() => {});
5773
5860
  await fireInternalDispatch({
5774
5861
  event,
5775
5862
  // Continuation chunks use the same path resolution as the
@@ -5795,11 +5882,28 @@ export function createProductionAgentHandler(
5795
5882
  continuationExpectsNetlifyBackgroundFunction,
5796
5883
  },
5797
5884
  },
5885
+ settleMs: continuationExpectsNetlifyBackgroundFunction
5886
+ ? BACKGROUND_CLAIM_GRACE_MS
5887
+ : undefined,
5798
5888
  });
5889
+ await recordRunDiagnostic(
5890
+ run.runId,
5891
+ RUN_DIAG_STAGE.workerSetupStep,
5892
+ `chain_dispatch_sent nextRunId=${nextRunId} reason=${continuationReason}`,
5893
+ ).catch(() => {});
5799
5894
  } catch (chainErr) {
5800
5895
  // Chain dispatch failed — fail loud so the held row goes
5801
5896
  // terminal instead of spinning. The reaper would also catch
5802
5897
  // it, but this is immediate and truthful.
5898
+ await recordRunDiagnostic(
5899
+ run.runId,
5900
+ RUN_DIAG_STAGE.workerThrew,
5901
+ `chain_dispatch_failed nextRunId=${nextRunId} ${
5902
+ chainErr instanceof Error
5903
+ ? chainErr.message
5904
+ : String(chainErr)
5905
+ }`,
5906
+ ).catch(() => {});
5803
5907
  console.error(
5804
5908
  "[agent-chat] background continuation dispatch failed:",
5805
5909
  chainErr instanceof Error ? chainErr.message : chainErr,
@@ -6242,7 +6346,7 @@ export function createProductionAgentHandler(
6242
6346
  if (obsConfig.enabled) {
6243
6347
  instrumented = true;
6244
6348
  loopUsage = await instrumentAgentLoop({
6245
- runAgentLoop,
6349
+ runAgentLoop: runAgentLoopWithMainChatInternalContinuations,
6246
6350
  loopOpts: agentLoopOpts,
6247
6351
  runId,
6248
6352
  threadId: threadId ?? null,
@@ -6272,7 +6376,8 @@ export function createProductionAgentHandler(
6272
6376
  if (instrumented) throw err;
6273
6377
  }
6274
6378
  if (!instrumented) {
6275
- loopUsage = await runAgentLoop(agentLoopOpts);
6379
+ loopUsage =
6380
+ await runAgentLoopWithMainChatInternalContinuations(agentLoopOpts);
6276
6381
  }
6277
6382
 
6278
6383
  // Record token usage for cost monitoring so the Usage panel in
@@ -42,42 +42,9 @@ function getGaMeasurementId(): string | null {
42
42
  );
43
43
  }
44
44
 
45
- /**
46
- * Script hosts the injected GA loader pulls executable code from. Google Tag
47
- * Manager serves `gtag/js`, and GA4 can lazy-load additional collectors from
48
- * `www.google-analytics.com`. These must be listed in the document `script-src`
49
- * so the CSP reflects the code the framework itself injects (see
50
- * `applyDocumentCsp` in `ssr-handler.ts`).
51
- */
52
- export const GA_CSP_SCRIPT_HOSTS = [
53
- "https://www.googletagmanager.com",
54
- "https://www.google-analytics.com",
55
- ] as const;
56
-
57
- /**
58
- * Network/image hosts used by the GA4 loader when it sends page-view and event
59
- * beacons. These are separate from `script-src`: a stricter deployment CSP with
60
- * `connect-src 'self'` or `img-src 'self'` can load gtag.js but still drop all
61
- * analytics events unless these hosts are present too.
62
- */
63
- export const GA_CSP_CONNECT_HOSTS = [
64
- "https://www.google-analytics.com",
65
- "https://analytics.google.com",
66
- "https://stats.g.doubleclick.net",
67
- "https://region1.google-analytics.com",
68
- ] as const;
69
-
70
- export const GA_CSP_IMG_HOSTS = [
71
- "https://www.google-analytics.com",
72
- "https://www.googletagmanager.com",
73
- "https://stats.g.doubleclick.net",
74
- ] as const;
75
-
76
45
  /**
77
46
  * The exact JS body (no surrounding `<script>` tags) of the inline gtag config
78
- * block injected next to the gtag.js loader. Returned so the SSR handler can
79
- * hash it for the `script-src` CSP directive — the hash must be computed from
80
- * the identical string that `getGaScript()` embeds, so both call this helper.
47
+ * block injected next to the gtag.js loader.
81
48
  * Returns `null` when GA is not configured.
82
49
  */
83
50
  export function getGaInlineConfigScriptBody(): string | null {
@@ -30,18 +30,11 @@ import {
30
30
  AGENT_NATIVE_SOCIAL_IMAGE_WIDTH,
31
31
  withAgentNativeSocialImageCacheBuster,
32
32
  } from "../shared/social-meta.js";
33
- import {
34
- GA_CSP_CONNECT_HOSTS,
35
- GA_CSP_IMG_HOSTS,
36
- GA_CSP_SCRIPT_HOSTS,
37
- getGaInlineConfigScriptBody,
38
- } from "./analytics.js";
39
33
  import {
40
34
  getAppBasePathFromViteEnv,
41
35
  stripAppBasePath as canonicalStripAppBasePath,
42
36
  } from "./app-base-path.js";
43
37
  import { runWithRequestContext } from "./request-context.js";
44
- import { computeInlineScriptHash } from "./security-headers.js";
45
38
  import { getSentryClientConfigScript } from "./sentry-config.js";
46
39
 
47
40
  export {
@@ -258,376 +251,17 @@ function applyDefaultSpeculationRulesHeader(
258
251
  }
259
252
 
260
253
  /**
261
- * Extract the plain JS body from a `<script ...>body</script>` string.
262
- * Returns `null` if the input is falsy or has no recognisable `</script>` end.
263
- * Used to compute the sha256 hash of framework-injected inline scripts so the
264
- * hash can be listed in app-owned `script-src` CSP directives.
265
- */
266
- function extractScriptBody(scriptTag: string | null): string | null {
267
- if (!scriptTag) return null;
268
- const start = scriptTag.indexOf(">") + 1;
269
- const end = scriptTag.lastIndexOf("</script>");
270
- if (start <= 0 || end < start) return null;
271
- return scriptTag.slice(start, end);
272
- }
273
-
274
- type CspDirective = {
275
- name: string;
276
- tokens: string[];
277
- };
278
-
279
- const CSP_DIRECTIVES_WITH_VALUE_TOKENS = new Set([
280
- "base-uri",
281
- "block-all-mixed-content",
282
- "child-src",
283
- "connect-src",
284
- "default-src",
285
- "fenced-frame-src",
286
- "font-src",
287
- "form-action",
288
- "frame-ancestors",
289
- "frame-src",
290
- "img-src",
291
- "manifest-src",
292
- "media-src",
293
- "navigate-to",
294
- "object-src",
295
- "plugin-types",
296
- "prefetch-src",
297
- "referrer",
298
- "reflected-xss",
299
- "require-sri-for",
300
- "require-trusted-types-for",
301
- "report-to",
302
- "report-uri",
303
- "sandbox",
304
- "script-src",
305
- "script-src-attr",
306
- "script-src-elem",
307
- "style-src",
308
- "style-src-attr",
309
- "style-src-elem",
310
- "trusted-types",
311
- "upgrade-insecure-requests",
312
- "webrtc",
313
- "worker-src",
314
- ]);
315
-
316
- function hasCommaJoinedCspPolicies(policy: string): boolean {
317
- let commaIndex = policy.indexOf(",");
318
- while (commaIndex !== -1) {
319
- const afterComma = policy.slice(commaIndex + 1);
320
- const directive = /^\s+([a-z][a-z0-9-]*)(?=\s|;|$)/i.exec(afterComma)?.[1];
321
- if (
322
- directive &&
323
- CSP_DIRECTIVES_WITH_VALUE_TOKENS.has(directive.toLowerCase())
324
- ) {
325
- return true;
326
- }
327
- commaIndex = policy.indexOf(",", commaIndex + 1);
328
- }
329
- return false;
330
- }
331
-
332
- function parseCsp(policy: string): CspDirective[] {
333
- return policy
334
- .split(";")
335
- .map((part) => part.trim())
336
- .filter(Boolean)
337
- .map((part) => {
338
- const [name = "", ...tokens] = part.split(/\s+/);
339
- return { name: name.toLowerCase(), tokens };
340
- })
341
- .filter((directive) => directive.name);
342
- }
343
-
344
- function serializeCsp(directives: CspDirective[]): string {
345
- return directives
346
- .map((directive) =>
347
- [directive.name, ...directive.tokens].filter(Boolean).join(" "),
348
- )
349
- .join("; ");
350
- }
351
-
352
- function appendCspTokens(
353
- tokens: string[],
354
- additions: readonly string[],
355
- ): string[] {
356
- if (!additions.length) return tokens;
357
- const next = tokens.filter((token) => token !== "'none'");
358
- const seen = new Set(next);
359
- for (const token of additions) {
360
- if (!token || seen.has(token)) continue;
361
- next.push(token);
362
- seen.add(token);
363
- }
364
- return next;
365
- }
366
-
367
- function findCspDirective(
368
- directives: CspDirective[],
369
- name: string,
370
- ): CspDirective | undefined {
371
- return directives.find((directive) => directive.name === name);
372
- }
373
-
374
- function appendToExistingOrDefaultCspDirective(
375
- directives: CspDirective[],
376
- name: string,
377
- additions: readonly string[],
378
- ): void {
379
- if (!additions.length) return;
380
- const existing = findCspDirective(directives, name);
381
- if (existing) {
382
- existing.tokens = appendCspTokens(existing.tokens, additions);
383
- return;
384
- }
385
-
386
- const defaultSrc = findCspDirective(directives, "default-src");
387
- if (!defaultSrc) return;
388
- directives.push({
389
- name,
390
- tokens: appendCspTokens([...defaultSrc.tokens], additions),
391
- });
392
- }
393
-
394
- function appendToExistingCspDirective(
395
- directives: CspDirective[],
396
- name: string,
397
- additions: readonly string[],
398
- ): void {
399
- const existing = findCspDirective(directives, name);
400
- if (!existing) return;
401
- existing.tokens = appendCspTokens(existing.tokens, additions);
402
- }
403
-
404
- function ensureCspDirective(
405
- directives: CspDirective[],
406
- name: string,
407
- tokens: readonly string[],
408
- ): void {
409
- if (findCspDirective(directives, name)) return;
410
- directives.push({ name, tokens: [...tokens] });
411
- }
412
-
413
- function hasStrictNonceScriptPolicy(tokens: readonly string[]): boolean {
414
- return tokens.some(
415
- (token) => token === "'strict-dynamic'" || token.startsWith("'nonce-"),
416
- );
417
- }
418
-
419
- function appendToScriptCspDirective(
420
- directives: CspDirective[],
421
- name: string,
422
- additions: readonly string[],
423
- ): boolean {
424
- const existing = findCspDirective(directives, name);
425
- if (existing) {
426
- if (hasStrictNonceScriptPolicy(existing.tokens)) return false;
427
- existing.tokens = appendCspTokens(existing.tokens, additions);
428
- return true;
429
- }
430
-
431
- const defaultSrc = findCspDirective(directives, "default-src");
432
- if (!defaultSrc || hasStrictNonceScriptPolicy(defaultSrc.tokens)) {
433
- return false;
434
- }
435
- directives.push({
436
- name,
437
- tokens: appendCspTokens([...defaultSrc.tokens], additions),
438
- });
439
- return true;
440
- }
441
-
442
- function appendToEffectiveScriptElementCspDirective(
443
- directives: CspDirective[],
444
- additions: readonly string[],
445
- ): boolean {
446
- const scriptSrcElem = findCspDirective(directives, "script-src-elem");
447
- if (scriptSrcElem) {
448
- if (hasStrictNonceScriptPolicy(scriptSrcElem.tokens)) return false;
449
- scriptSrcElem.tokens = appendCspTokens(scriptSrcElem.tokens, additions);
450
- return true;
451
- }
452
-
453
- return appendToScriptCspDirective(directives, "script-src", additions);
454
- }
455
-
456
- function augmentExistingEnforcedCspForFrameworkScripts(
457
- policy: string,
458
- options: {
459
- gaScriptSrcTokens: readonly string[];
460
- gaEnabled: boolean;
461
- },
462
- ): string {
463
- // Multiple CSP headers are surfaced by Headers.get() as one comma-joined
464
- // string. CSP is not a comma-list header, so serializing a parsed combined
465
- // value would turn two policies into one invalid policy. Leave those headers
466
- // app-owned; a comma inside a source/report URL is still safe to parse.
467
- if (hasCommaJoinedCspPolicies(policy)) return policy;
468
-
469
- const directives = parseCsp(policy);
470
- if (!directives.length) return policy;
471
-
472
- if (options.gaEnabled) {
473
- const addedScriptElement = appendToEffectiveScriptElementCspDirective(
474
- directives,
475
- options.gaScriptSrcTokens,
476
- );
477
- if (addedScriptElement) {
478
- appendToExistingOrDefaultCspDirective(
479
- directives,
480
- "connect-src",
481
- GA_CSP_CONNECT_HOSTS,
482
- );
483
- appendToExistingOrDefaultCspDirective(
484
- directives,
485
- "img-src",
486
- GA_CSP_IMG_HOSTS,
487
- );
488
- }
489
- }
490
-
491
- ensureCspDirective(directives, "object-src", ["'none'"]);
492
- ensureCspDirective(directives, "base-uri", ["'self'"]);
493
-
494
- return serializeCsp(directives);
495
- }
496
-
497
- function augmentExistingReportOnlyCspForFrameworkScripts(
498
- policy: string,
499
- options: {
500
- scriptSrcTokens: readonly string[];
501
- gaEnabled: boolean;
502
- },
503
- ): string {
504
- if (hasCommaJoinedCspPolicies(policy)) return policy;
505
-
506
- const directives = parseCsp(policy);
507
- if (!directives.length) return policy;
508
-
509
- appendToExistingOrDefaultCspDirective(
510
- directives,
511
- "script-src",
512
- options.scriptSrcTokens,
513
- );
514
- appendToExistingCspDirective(
515
- directives,
516
- "script-src-elem",
517
- options.scriptSrcTokens,
518
- );
519
-
520
- if (options.gaEnabled) {
521
- appendToExistingOrDefaultCspDirective(
522
- directives,
523
- "connect-src",
524
- GA_CSP_CONNECT_HOSTS,
525
- );
526
- appendToExistingOrDefaultCspDirective(
527
- directives,
528
- "img-src",
529
- GA_CSP_IMG_HOSTS,
530
- );
531
- }
532
-
533
- return serializeCsp(directives);
534
- }
535
-
536
- /**
537
- * Apply a Content-Security-Policy header to HTML document responses.
254
+ * Strip document-level CSP from app HTML responses.
538
255
  *
539
- * Two directives are always enforced in production:
540
- *
541
- * - `object-src 'none'` — disables Flash / Java / PDF plugin execution,
542
- * which are a reliable code-execution vector even in modern browsers.
543
- * - `base-uri 'self'` — prevents a `<base href="...">` injection from
544
- * hijacking all relative URLs in the document (a common attack target when
545
- * user-controlled content reaches the HTML).
546
- *
547
- * A third directive, `script-src`, is emitted via `Content-Security-Policy-
548
- * Report-Only` rather than enforced when the app has no existing document CSP.
549
- * The framework injects inline scripts for analytics, Sentry, and template
550
- * setup, and hosted apps need Google Tag Manager to load without noisy CSP
551
- * diagnostics. The report-only policy is intentionally permissive for scripts:
552
- * it includes `'unsafe-inline'` plus the known GA/GTM loader hosts.
553
- *
554
- * If an app or host already sends an enforced CSP with `script-src`,
555
- * `script-src-elem`, `connect-src`, `img-src`, or `default-src`, we merge only
556
- * GA-specific allowances into existing host/hash policies. Strict nonce or
557
- * `strict-dynamic` script policies stay app-owned because blindly appending
558
- * hashes or hosts would widen the policy without reliably loading our injected
559
- * scripts.
560
- *
561
- * Templates additionally render a theme-init inline script whose exact content
562
- * varies by template (default theme param, custom docs variant, etc.) and which
563
- * is rendered by React Router, not this handler, so its hash is not available
564
- * here. Shipping script-src as Report-Only surfaces the remaining violations
565
- * without breaking template customisations; teams can graduate to enforcement
566
- * once their hashes are enumerated.
567
- *
568
- * Skipped in development (`NODE_ENV !== 'production'`) so HMR eval and Vite
569
- * dev-server injects are never blocked. Set `AGENT_NATIVE_DISABLE_DOC_CSP=1`
570
- * to opt out in production for a template with exotic needs.
256
+ * Hosted templates inject framework bootstrap scripts, analytics, Sentry config,
257
+ * and app-owned inline scripts whose exact bytes vary by build/template. Any
258
+ * shared CSP header, even Report-Only, can block or noisily report Google Tag
259
+ * Manager and those bootstraps. Extension iframes and webviews keep their own
260
+ * route-specific sandboxes; normal app documents deliberately do not emit CSP.
571
261
  */
572
- function applyDocumentCsp(headers: Headers, sentryScript: string | null): void {
573
- if (process.env.NODE_ENV !== "production") return;
574
- if (process.env.AGENT_NATIVE_DISABLE_DOC_CSP === "1") return;
575
-
576
- // script-src as Report-Only: keep this deliberately loose so the framework's
577
- // injected analytics and template bootstrap scripts do not look blocked in
578
- // browser diagnostics.
579
- const sentryBody = extractScriptBody(sentryScript);
580
- const sentryHash = sentryBody ? computeInlineScriptHash(sentryBody) : null;
581
- const gaInlineBody = getGaInlineConfigScriptBody();
582
- const gaHash = gaInlineBody ? computeInlineScriptHash(gaInlineBody) : null;
583
- const gaHosts = gaInlineBody ? [...GA_CSP_SCRIPT_HOSTS] : [];
584
- const gaScriptSrcTokens = [
585
- "'unsafe-inline'",
586
- ...(gaHash ? [gaHash] : []),
587
- ...gaHosts,
588
- ];
589
- const scriptSrcTokens = [
590
- "'self'",
591
- "'unsafe-inline'",
592
- ...(sentryHash ? [sentryHash] : []),
593
- ...(gaHash ? [gaHash] : []),
594
- ...gaHosts,
595
- ];
596
-
597
- const cspAugmentOptions = {
598
- scriptSrcTokens,
599
- gaScriptSrcTokens,
600
- gaEnabled: Boolean(gaInlineBody),
601
- };
602
- const existing = headers.get("content-security-policy") ?? "";
603
- if (!existing) {
604
- headers.set(
605
- "content-security-policy",
606
- "object-src 'none'; base-uri 'self'",
607
- );
608
- } else {
609
- headers.set(
610
- "content-security-policy",
611
- augmentExistingEnforcedCspForFrameworkScripts(
612
- existing,
613
- cspAugmentOptions,
614
- ),
615
- );
616
- }
617
-
618
- const scriptSrc = `script-src ${scriptSrcTokens.join(" ")}`;
619
- const existingRo = headers.get("content-security-policy-report-only") ?? "";
620
- if (!existingRo) {
621
- headers.set("content-security-policy-report-only", scriptSrc);
622
- } else {
623
- headers.set(
624
- "content-security-policy-report-only",
625
- augmentExistingReportOnlyCspForFrameworkScripts(
626
- existingRo,
627
- cspAugmentOptions,
628
- ),
629
- );
630
- }
262
+ function removeDocumentCsp(headers: Headers): void {
263
+ headers.delete("content-security-policy");
264
+ headers.delete("content-security-policy-report-only");
631
265
  }
632
266
 
633
267
  function isFrameworkOrAssetPath(pathname: string): boolean {
@@ -665,7 +299,15 @@ async function rewriteMountedResponse(
665
299
  }
666
300
 
667
301
  const contentType = headers.get("content-type") ?? "";
668
- if (!contentType.toLowerCase().includes("text/html") || !response.body) {
302
+ if (!contentType.toLowerCase().includes("text/html")) {
303
+ return new Response(response.body, {
304
+ status: response.status,
305
+ statusText: response.statusText,
306
+ headers,
307
+ });
308
+ }
309
+ removeDocumentCsp(headers);
310
+ if (!response.body) {
669
311
  return new Response(response.body, {
670
312
  status: response.status,
671
313
  statusText: response.statusText,
@@ -675,7 +317,6 @@ async function rewriteMountedResponse(
675
317
 
676
318
  const html = await response.text();
677
319
  headers.delete("content-length");
678
- applyDocumentCsp(headers, sentryClientConfigScript);
679
320
  return new Response(
680
321
  injectHeadScript(
681
322
  injectDefaultSocialImageMeta(
@@ -349,8 +349,6 @@ function loomEmbedResponse(embedUrl: string): Response {
349
349
  "Cache-Control": "private, max-age=0, no-store",
350
350
  "Referrer-Policy": "no-referrer",
351
351
  "X-Content-Type-Options": "nosniff",
352
- "Content-Security-Policy":
353
- "default-src 'none'; frame-src https://www.loom.com; style-src 'unsafe-inline'",
354
352
  },
355
353
  });
356
354
  }
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: fixed
3
+ ---
4
+
5
+ Design chat now keeps selected-variant screen edits moving when a large edit stalls during preparation, instead of leaving the chat waiting for a browser-side recovery.
@@ -279,7 +279,6 @@ export async function renderPublicForm(event: H3Event) {
279
279
 
280
280
  const headers: Record<string, string> = {
281
281
  "Content-Type": "text/html; charset=utf-8",
282
- "Content-Security-Policy": "frame-ancestors *",
283
282
  };
284
283
  if (status === 200) {
285
284
  // Public form SSR is anonymous HTML and follows the same framework-level