@agent-native/core 0.101.11 → 0.101.13

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.
Files changed (72) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/create.ts +45 -8
  5. package/corpus/core/src/client/assistant-ui-recovery.tsx +9 -3
  6. package/corpus/core/src/client/chat/message-components.tsx +25 -2
  7. package/corpus/core/src/client/chat/tool-call-display.tsx +6 -9
  8. package/corpus/core/src/coding-tools/run-code.ts +65 -12
  9. package/corpus/core/src/extensions/actions.ts +141 -7
  10. package/corpus/core/src/mcp/server.ts +10 -1
  11. package/corpus/core/src/server/agent-chat/framework-prompts.ts +0 -3
  12. package/corpus/core/src/server/agent-chat-plugin.ts +2 -58
  13. package/corpus/core/src/server/prompts/framework-core-compact.ts +0 -1
  14. package/corpus/core/src/server/prompts/framework-core.ts +0 -30
  15. package/corpus/core/src/server/prompts/index.ts +1 -4
  16. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +59 -0
  17. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +5 -2
  18. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +13 -4
  19. package/corpus/templates/analytics/server/lib/real-data-actions.ts +63 -1
  20. package/corpus/templates/analytics/server/plugins/agent-chat.ts +30 -0
  21. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +0 -14
  22. package/corpus/templates/chat/changelog/2026-07-15-chat-navigation-focuses-on-chat-and-agent.md +6 -0
  23. package/dist/cli/create.d.ts.map +1 -1
  24. package/dist/cli/create.js +31 -7
  25. package/dist/cli/create.js.map +1 -1
  26. package/dist/client/assistant-ui-recovery.d.ts +2 -0
  27. package/dist/client/assistant-ui-recovery.d.ts.map +1 -1
  28. package/dist/client/assistant-ui-recovery.js +5 -2
  29. package/dist/client/assistant-ui-recovery.js.map +1 -1
  30. package/dist/client/chat/message-components.d.ts +6 -0
  31. package/dist/client/chat/message-components.d.ts.map +1 -1
  32. package/dist/client/chat/message-components.js +14 -1
  33. package/dist/client/chat/message-components.js.map +1 -1
  34. package/dist/client/chat/tool-call-display.d.ts +1 -1
  35. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  36. package/dist/client/chat/tool-call-display.js +5 -8
  37. package/dist/client/chat/tool-call-display.js.map +1 -1
  38. package/dist/coding-tools/run-code.js +64 -11
  39. package/dist/coding-tools/run-code.js.map +1 -1
  40. package/dist/collab/awareness.d.ts +2 -2
  41. package/dist/collab/awareness.d.ts.map +1 -1
  42. package/dist/collab/struct-routes.d.ts +1 -1
  43. package/dist/extensions/actions.d.ts.map +1 -1
  44. package/dist/extensions/actions.js +129 -7
  45. package/dist/extensions/actions.js.map +1 -1
  46. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  47. package/dist/mcp/server.d.ts.map +1 -1
  48. package/dist/mcp/server.js +10 -1
  49. package/dist/mcp/server.js.map +1 -1
  50. package/dist/notifications/routes.d.ts +1 -1
  51. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  52. package/dist/resources/handlers.d.ts +2 -2
  53. package/dist/server/agent-chat/framework-prompts.d.ts.map +1 -1
  54. package/dist/server/agent-chat/framework-prompts.js +0 -3
  55. package/dist/server/agent-chat/framework-prompts.js.map +1 -1
  56. package/dist/server/agent-chat-plugin.d.ts +0 -22
  57. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  58. package/dist/server/agent-chat-plugin.js +2 -51
  59. package/dist/server/agent-chat-plugin.js.map +1 -1
  60. package/dist/server/prompts/framework-core-compact.d.ts +0 -1
  61. package/dist/server/prompts/framework-core-compact.d.ts.map +1 -1
  62. package/dist/server/prompts/framework-core-compact.js +0 -1
  63. package/dist/server/prompts/framework-core-compact.js.map +1 -1
  64. package/dist/server/prompts/framework-core.d.ts +0 -9
  65. package/dist/server/prompts/framework-core.d.ts.map +1 -1
  66. package/dist/server/prompts/framework-core.js +0 -29
  67. package/dist/server/prompts/framework-core.js.map +1 -1
  68. package/dist/server/prompts/index.d.ts +1 -1
  69. package/dist/server/prompts/index.d.ts.map +1 -1
  70. package/dist/server/prompts/index.js +1 -1
  71. package/dist/server/prompts/index.js.map +1 -1
  72. package/package.json +1 -1
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: 2258
31
- - template files: 5674
31
+ - template files: 5675
@@ -1,5 +1,19 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.101.13
4
+
5
+ ### Patch Changes
6
+
7
+ - e503433: Keep CLI repository downloads asynchronous and challenge bare loopback MCP URLs for OAuth clients.
8
+ - e503433: Keep completed chat work disclosures collapsed and stable when submitting a later message.
9
+ - e503433: Remove the automatic first-session personalization questions from agent chat so new requests are handled directly.
10
+
11
+ ## 0.101.12
12
+
13
+ ### Patch Changes
14
+
15
+ - 2ff004e: Fix workspace-file resolution and run-code paging/routing edge cases. `contentFromWorkspaceFile` now resolves the same file the run-code `workspaceRead`/`workspaceWrite` bridge sees (bridge scope first, then Resources), and fails closed on a bridge read error instead of silently falling back to a possibly-different same-path Resources body. `workspaceRead` returns null instead of a silently truncated prefix when a later page fails, and the run-code bridge now returns a distinct "not registered" (404) error for unknown tools instead of a misleading read-only access error.
16
+
3
17
  ## 0.101.11
4
18
 
5
19
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.101.11",
3
+ "version": "0.101.13",
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": {
@@ -1,4 +1,4 @@
1
- import { execFileSync } from "child_process";
1
+ import { execFile, execFileSync } from "child_process";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { fileURLToPath, pathToFileURL } from "url";
@@ -1363,18 +1363,53 @@ function tarExtractArgs(
1363
1363
  return ["xzf", tarPath, "--strip-components=1", ...excludes, "-C", destDir];
1364
1364
  }
1365
1365
 
1366
- function downloadAndExtract(
1366
+ function execFileBuffer(
1367
+ command: string,
1368
+ args: string[],
1369
+ options: { maxBuffer: number },
1370
+ ): Promise<Buffer> {
1371
+ return new Promise((resolve, reject) => {
1372
+ execFile(
1373
+ command,
1374
+ args,
1375
+ { ...options, encoding: "buffer" },
1376
+ (error, stdout, stderr) => {
1377
+ if (error) {
1378
+ const detail = stderr?.toString().trim();
1379
+ if (detail) error.message = `${error.message}: ${detail}`;
1380
+ reject(error);
1381
+ return;
1382
+ }
1383
+ resolve(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
1384
+ },
1385
+ );
1386
+ });
1387
+ }
1388
+
1389
+ async function downloadAndExtract(
1367
1390
  url: string,
1368
1391
  destDir: string,
1369
1392
  options: { skipAgentSymlinks?: boolean } = {},
1370
- ): void {
1393
+ ): Promise<void> {
1371
1394
  fs.mkdirSync(destDir, { recursive: true });
1372
1395
  // --fail-with-body so curl exits non-zero on HTTP 4xx/5xx instead of writing
1373
1396
  // the error body (HTML/JSON) to disk where tar then fails with the opaque
1374
1397
  // "Unrecognized archive format" message.
1375
- const tarball = execFileSync("curl", ["--fail-with-body", "-sL", url], {
1376
- maxBuffer: 100 * 1024 * 1024,
1377
- });
1398
+ // Keep this asynchronous: a synchronous curl blocks the event loop, which
1399
+ // makes the create command's spinner look frozen during the GitHub fetch.
1400
+ const tarball = await execFileBuffer(
1401
+ "curl",
1402
+ [
1403
+ "--fail-with-body",
1404
+ "--connect-timeout",
1405
+ "10",
1406
+ "--max-time",
1407
+ "120",
1408
+ "-sL",
1409
+ url,
1410
+ ],
1411
+ { maxBuffer: 100 * 1024 * 1024 },
1412
+ );
1378
1413
  const tarPath = path.join(destDir, ".download.tar.gz");
1379
1414
  fs.writeFileSync(tarPath, tarball);
1380
1415
  try {
@@ -1407,7 +1442,9 @@ async function downloadGitHubSubdir(
1407
1442
  `.agent-native-tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1408
1443
  );
1409
1444
  try {
1410
- downloadAndExtract(tarUrl, tmpDir, { skipAgentSymlinks: repo === REPO });
1445
+ await downloadAndExtract(tarUrl, tmpDir, {
1446
+ skipAgentSymlinks: repo === REPO,
1447
+ });
1411
1448
  const srcDir = path.join(tmpDir, subdir);
1412
1449
  if (!fs.existsSync(srcDir)) {
1413
1450
  throw new Error(
@@ -1435,7 +1472,7 @@ async function downloadGitHubRepo(
1435
1472
  ): Promise<void> {
1436
1473
  validateRepoName(repo);
1437
1474
  const tarUrl = `https://api.github.com/repos/${repo}/tarball/main`;
1438
- downloadAndExtract(tarUrl, targetDir);
1475
+ await downloadAndExtract(tarUrl, targetDir);
1439
1476
  }
1440
1477
 
1441
1478
  /* ─────────────────────────────────────────────────────────────────────────
@@ -38,6 +38,8 @@ export function isAssistantUiRecoverableRenderError(error: unknown): boolean {
38
38
 
39
39
  type AssistantUiStaleIndexErrorBoundaryProps = {
40
40
  resetKey: string;
41
+ /** Remount children when the recovery scope changes. */
42
+ remountOnResetKey?: boolean;
41
43
  componentName?: string;
42
44
  children: React.ReactNode;
43
45
  };
@@ -173,10 +175,13 @@ export class AssistantUiStaleIndexErrorBoundary extends React.Component<
173
175
  return null;
174
176
  }
175
177
 
178
+ const fragmentKey =
179
+ this.props.remountOnResetKey === false
180
+ ? String(this.state.retryToken)
181
+ : `${this.props.resetKey}:${this.state.retryToken}`;
182
+
176
183
  return (
177
- <React.Fragment key={`${this.props.resetKey}:${this.state.retryToken}`}>
178
- {this.props.children}
179
- </React.Fragment>
184
+ <React.Fragment key={fragmentKey}>{this.props.children}</React.Fragment>
180
185
  );
181
186
  }
182
187
  }
@@ -191,6 +196,7 @@ export function AssistantMessageListErrorBoundary({
191
196
  return (
192
197
  <AssistantUiStaleIndexErrorBoundary
193
198
  resetKey={resetKey}
199
+ remountOnResetKey={false}
194
200
  componentName="AssistantMessageList"
195
201
  >
196
202
  {children}
@@ -848,6 +848,25 @@ export function shouldShowAssistantMessageFooter({
848
848
  return !chatRunning && statusIsTerminal;
849
849
  }
850
850
 
851
+ export function shouldShowAssistantWorkSummary({
852
+ isLast,
853
+ isComplete,
854
+ hasCollapsibleWork,
855
+ hasUnresolvedTool,
856
+ }: {
857
+ isLast: boolean;
858
+ isComplete: boolean;
859
+ hasCollapsibleWork: boolean;
860
+ hasUnresolvedTool: boolean;
861
+ }): boolean {
862
+ if (!hasCollapsibleWork || hasUnresolvedTool) return false;
863
+
864
+ // Keep completed historical work wrapped while a later turn is running.
865
+ // Removing the wrapper exposes/remounts ReasoningCell and resets its
866
+ // disclosure state to the default-open value on every new submission.
867
+ return isComplete || !isLast;
868
+ }
869
+
851
870
  function ReasoningMessagePart() {
852
871
  const part = useMessagePartReasoning();
853
872
  const partRuntime = useMessagePartRuntime();
@@ -1040,8 +1059,12 @@ export function AssistantMessage() {
1040
1059
  {({ part, children }) => {
1041
1060
  switch (part.type) {
1042
1061
  case "group-work": {
1043
- const showSummary =
1044
- isComplete && !chatRunning && hasCollapsibleWork;
1062
+ const showSummary = shouldShowAssistantWorkSummary({
1063
+ isLast,
1064
+ isComplete,
1065
+ hasCollapsibleWork,
1066
+ hasUnresolvedTool,
1067
+ });
1045
1068
  if (!showSummary) return <>{children}</>;
1046
1069
  return (
1047
1070
  <WorkedForSummary
@@ -1088,20 +1088,17 @@ export function WorkedForSummary({
1088
1088
  children,
1089
1089
  }: {
1090
1090
  durationMs?: number | null;
1091
- /** When true, start open then animate closed (post-run collapse). */
1091
+ /** When true, close the summary after a run has completed. */
1092
1092
  autoCollapse?: boolean;
1093
1093
  children: React.ReactNode;
1094
1094
  }) {
1095
- const [open, setOpen] = useState(autoCollapse);
1096
- const didAutoCollapseRef = useRef(false);
1095
+ // Start closed so a remounted completed message never flashes its work
1096
+ // details open while auto-collapse settles. If the summary was already
1097
+ // open when autoCollapse changes, AnimatedCollapse still animates it shut.
1098
+ const [open, setOpen] = useState(false);
1097
1099
 
1098
1100
  useEffect(() => {
1099
- if (!autoCollapse || didAutoCollapseRef.current) return;
1100
- didAutoCollapseRef.current = true;
1101
- const frame = requestAnimationFrame(() => {
1102
- setOpen(false);
1103
- });
1104
- return () => cancelAnimationFrame(frame);
1101
+ if (autoCollapse) setOpen(false);
1105
1102
  }, [autoCollapse]);
1106
1103
 
1107
1104
  const label =
@@ -740,8 +740,18 @@ function handleBridgeRequest(
740
740
 
741
741
  // Enforce allowlist.
742
742
  const entry = actions[toolName];
743
+ // Unknown/mistyped tool: report "not registered" (404) before the
744
+ // access-control branch below. Otherwise an undefined `entry` falls into the
745
+ // allowlist 403 and returns a misleading access error for a tool that simply
746
+ // does not exist. (Bridge-allowlisted tools always have an entry, so this
747
+ // cannot mask a legitimate allowlisted call.)
748
+ if (!entry) {
749
+ res.writeHead(404, { "Content-Type": "application/json" });
750
+ res.end(JSON.stringify({ error: `Tool "${toolName}" is not registered.` }));
751
+ return;
752
+ }
743
753
  const isReadOnlyAction =
744
- entry?.readOnly === true &&
754
+ entry.readOnly === true &&
745
755
  entry.agentTool !== false &&
746
756
  entry.toolCallable !== false;
747
757
  if (
@@ -749,21 +759,24 @@ function handleBridgeRequest(
749
759
  !extraTools.has(toolName) &&
750
760
  !isReadOnlyAction
751
761
  ) {
762
+ // A registered, agent-exposed action that isn't read-only is a mutation.
763
+ // (Unknown tools already returned 404 above, so `entry` is defined here.)
764
+ // Point the caller at the native tool path instead of leaving them to guess
765
+ // (the common trap: retrying create-extension/update-extension through
766
+ // appAction, which cannot work).
767
+ const isMutatingAction =
768
+ entry.agentTool !== false && entry.readOnly !== true;
752
769
  res.writeHead(403, { "Content-Type": "application/json" });
753
770
  res.end(
754
771
  JSON.stringify({
755
- error: `Tool "${toolName}" is not an agent-exposed read-only action or sandbox bridge allowlisted tool.`,
772
+ error: isMutatingAction
773
+ ? `Tool "${toolName}" is a mutating action and cannot be called from run-code (appAction only exposes read-only actions). Call "${toolName}" directly as a native tool. For large content bodies, stage the content and pass "contentFromAttachment" instead of an inline string rather than routing it through run-code.`
774
+ : `Tool "${toolName}" is not an agent-exposed read-only action or sandbox bridge allowlisted tool.`,
756
775
  }),
757
776
  );
758
777
  return;
759
778
  }
760
779
 
761
- if (!entry) {
762
- res.writeHead(404, { "Content-Type": "application/json" });
763
- res.end(JSON.stringify({ error: `Tool "${toolName}" is not registered.` }));
764
- return;
765
- }
766
-
767
780
  const toolArgs = parsed.args ?? {};
768
781
  usedTools.add(toolName);
769
782
  // Run the tool with the parent request context so auth/org/owner resolution
@@ -1534,12 +1547,52 @@ async function webRead(url, init = {}) {
1534
1547
  /**
1535
1548
  * Read a Resources-backed workspace file by path. Returns the file content as
1536
1549
  * a string, or null if not found.
1537
- * Supports optional offset and maxChars for paging large files.
1550
+ *
1551
+ * By default this returns the WHOLE file: the underlying store caps a single
1552
+ * read at 100k chars, so this auto-pages across chunks and concatenates them,
1553
+ * so callers never get a silently truncated body. Pass an explicit \`offset\`
1554
+ * or \`maxChars\` to take manual control of a single page instead.
1538
1555
  */
1539
1556
  async function workspaceRead(path, opts = {}) {
1540
- const parsed = await workspaceReadMeta(path, opts);
1541
- if (parsed && parsed.ok === false) return null;
1542
- return parsed && typeof parsed.content === "string" ? parsed.content : null;
1557
+ // Explicit paging requested → single read, caller owns the window.
1558
+ if (opts.offset !== undefined || opts.maxChars !== undefined) {
1559
+ const parsed = await workspaceReadMeta(path, opts);
1560
+ if (parsed && parsed.ok === false) return null;
1561
+ return parsed && typeof parsed.content === "string" ? parsed.content : null;
1562
+ }
1563
+ // Default → assemble the full file by paging until the store reports no more.
1564
+ let offset = 0;
1565
+ let out = "";
1566
+ let found = false;
1567
+ let complete = false;
1568
+ // Bounded loop (files cap at 2 MB / 100k-char reads) so a misbehaving store
1569
+ // can never spin forever.
1570
+ for (let i = 0; i < 512; i++) {
1571
+ const parsed = await workspaceReadMeta(path, { offset, maxChars: 100000 });
1572
+ // A failed/missing page aborts. If it failed before the first page we never
1573
+ // found the file (return null below); if it failed part-way through a
1574
+ // truncated read, complete stays false and we return null rather than a
1575
+ // silently truncated body.
1576
+ if (!parsed || parsed.ok === false) break;
1577
+ found = true;
1578
+ const chunk = typeof parsed.content === "string" ? parsed.content : "";
1579
+ out += chunk;
1580
+ if (!parsed.truncated) {
1581
+ complete = true;
1582
+ break;
1583
+ }
1584
+ const next =
1585
+ typeof parsed.nextOffset === "number"
1586
+ ? parsed.nextOffset
1587
+ : offset + chunk.length;
1588
+ if (next <= offset) break; // no forward progress; stop rather than loop
1589
+ offset = next;
1590
+ }
1591
+ if (!found) return null;
1592
+ // Only hand back a body we know is whole. A truncated read followed by a
1593
+ // failed/stalled page would otherwise corrupt a large clone with a partial
1594
+ // prefix and no error signal.
1595
+ return complete ? out : null;
1543
1596
  }
1544
1597
 
1545
1598
  /**
@@ -3,8 +3,17 @@ import type { ActionRunContext } from "../action.js";
3
3
  import type { ActionEntry } from "../agent/production-agent.js";
4
4
  import type { AgentChatAttachment } from "../agent/types.js";
5
5
  import { writeAppState } from "../application-state/script-helpers.js";
6
- import { getRequestRunContext } from "../server/request-context.js";
6
+ import { readResource } from "../resources/script-helpers.js";
7
+ import {
8
+ getRequestOrgId,
9
+ getRequestRunContext,
10
+ getRequestUserEmail,
11
+ } from "../server/request-context.js";
7
12
  import { resolveAccess } from "../sharing/access.js";
13
+ import {
14
+ readWorkspaceFile,
15
+ type WorkspaceFilesScope,
16
+ } from "../workspace-files/store.js";
8
17
  import type {
9
18
  ExtensionContentEdit,
10
19
  ExtensionLegacyPatch,
@@ -483,7 +492,7 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
483
492
  "create-extension": {
484
493
  tool: {
485
494
  description:
486
- 'Create a persisted sandboxed Alpine.js mini-app extension and render it inline in the chat. Use this when the user wants generated UI that should be saved, reusable, or visible in the Extensions view: extensions, widgets, dashboards, calculators, mini-apps, and reusable interactive utilities. For one-time chat-only UI, use render-inline-extension instead. The content must be a self-contained Alpine.js HTML body snippet that can use appAction(), appFetch(), dbQuery(), extensionFetch(), extensionData, agentNative.ui.output(value, opts?), and agentNative.chat.send()/sendToAgentChat(). Use appAction() for app data writes and extensionData for extension-owned persisted UI state; dbQuery() is for read-only inspection of known app SQL tables. Use agentNative.ui.output for passive current values from knobs, sliders, and selections; it writes application state at inline-ui:<extension id>:output, which the agent can read later with readAppState when the user says to use that value. Use agentNative.chat.send for visible submit/apply actions. Persist reusable user-edited state with extensionData: if the extension has checkboxes, todos, notes, filters, preferences, or any control whose value should survive reload/reopen, load that state on init and save changes with extensionData, usually at user scope, instead of keeping it only in Alpine state. IMPORTANT — hosting a pasted file: if the user pasted a large HTML/Alpine file (it appears in your context as an <attachment name="pasted-text-…"> block) and asked you to host it as-is, do NOT copy that file into `content`. Instead leave `content` empty and pass `contentFromAttachment` set to that attachment\'s name (or the literal "latest" for the most recent pasted block) — the server reads the file verbatim. Re-emitting a large pasted file as `content` regularly gets cut off mid-stream and stalls the turn. Prefer appAction(name, params) for app data and actions, including read actions mounted as GET; do not call template /api/* routes from appFetch because the extension bridge only allows framework /_agent-native/* paths. Parse JSON string action results before aggregating; use dbQuery() only for known existing SQL tables and never for writes. Keep the initial create-extension payload compact and working; for complex extensions, create a useful v1 first, then use focused update-extension edits for refinements rather than assembling one enormous initial tool input. For any non-trivial component (more than a couple of state fields, any methods, any string formatting, any branching) put the component in a <script> block via Alpine.data(\'name\', () => ({...})) and reference it with x-data="name" — do NOT cram methods, template literals, or branching logic into an inline x-data="{...}" attribute (HTML parser pitfalls cause ReferenceError failures). Define every variable referenced from x-text/x-show/x-if/x-for on the data object\'s initial state. If the extension\'s value depends on an LLM call, require a real key via \\${keys.OPENAI_API_KEY}/\\${keys.ANTHROPIC_API_KEY} (and tell the user to add it in the Dispatch Vault, or in app Settings → API Keys & Connections for standalone apps, if missing) or route the AI work to the agent chat — never ship a stubbed analysis step that renders a placeholder/boolean as the result.',
495
+ 'Create a persisted sandboxed Alpine.js mini-app extension and render it inline in the chat. Use this when the user wants generated UI that should be saved, reusable, or visible in the Extensions view: extensions, widgets, dashboards, calculators, mini-apps, and reusable interactive utilities. For one-time chat-only UI, use render-inline-extension instead. The content must be a self-contained Alpine.js HTML body snippet that can use appAction(), appFetch(), dbQuery(), extensionFetch(), extensionData, agentNative.ui.output(value, opts?), and agentNative.chat.send()/sendToAgentChat(). Use appAction() for app data writes and extensionData for extension-owned persisted UI state; dbQuery() is for read-only inspection of known app SQL tables. Use agentNative.ui.output for passive current values from knobs, sliders, and selections; it writes application state at inline-ui:<extension id>:output, which the agent can read later with readAppState when the user says to use that value. Use agentNative.chat.send for visible submit/apply actions. Persist reusable user-edited state with extensionData: if the extension has checkboxes, todos, notes, filters, preferences, or any control whose value should survive reload/reopen, load that state on init and save changes with extensionData, usually at user scope, instead of keeping it only in Alpine state. IMPORTANT — hosting a pasted file: if the user pasted a large HTML/Alpine file (it appears in your context as an <attachment name="pasted-text-…"> block) and asked you to host it as-is, do NOT copy that file into `content`. Instead leave `content` empty and pass `contentFromAttachment` set to that attachment\'s name (or the literal "latest" for the most recent pasted block) — the server reads the file verbatim. Re-emitting a large pasted file as `content` regularly gets cut off mid-stream and stalls the turn. IMPORTANT — cloning a large extension that lives as a workspace resource (not a chat attachment): leave `content` empty and pass `contentFromWorkspaceFile` set to the resource path (e.g. "intuit-analytics-extension.html"); the server reads the full file. Do NOT try to reconstruct the body with run-code or route create-extension through run-code (mutating actions are not callable there). Prefer appAction(name, params) for app data and actions, including read actions mounted as GET; do not call template /api/* routes from appFetch because the extension bridge only allows framework /_agent-native/* paths. Parse JSON string action results before aggregating; use dbQuery() only for known existing SQL tables and never for writes. Keep the initial create-extension payload compact and working; for complex extensions, create a useful v1 first, then use focused update-extension edits for refinements rather than assembling one enormous initial tool input. For any non-trivial component (more than a couple of state fields, any methods, any string formatting, any branching) put the component in a <script> block via Alpine.data(\'name\', () => ({...})) and reference it with x-data="name" — do NOT cram methods, template literals, or branching logic into an inline x-data="{...}" attribute (HTML parser pitfalls cause ReferenceError failures). Define every variable referenced from x-text/x-show/x-if/x-for on the data object\'s initial state. If the extension\'s value depends on an LLM call, require a real key via \\${keys.OPENAI_API_KEY}/\\${keys.ANTHROPIC_API_KEY} (and tell the user to add it in the Dispatch Vault, or in app Settings → API Keys & Connections for standalone apps, if missing) or route the AI work to the agent chat — never ship a stubbed analysis step that renders a placeholder/boolean as the result.',
487
496
  parameters: {
488
497
  type: "object",
489
498
  properties: {
@@ -506,6 +515,11 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
506
515
  description:
507
516
  'Host a pasted/attached file verbatim WITHOUT re-typing it. Set this to the name of an attachment on the current turn (e.g. "pasted-text-1718000000000-ab12cd.txt") or the literal "latest" for the most recent pasted block; the server resolves it into the extension content. Use this instead of `content` whenever the user pasted a large file to host — it avoids re-emitting thousands of tokens. When set, leave `content` empty.',
508
517
  },
518
+ contentFromWorkspaceFile: {
519
+ type: "string",
520
+ description:
521
+ 'Host a workspace/shared resource file verbatim WITHOUT re-typing it. Set this to the resource path (e.g. "intuit-analytics-extension.html"); the server reads the full file and uses it as the extension content. Use this — NOT run-code or contentFromAttachment — when cloning a large extension body that already exists as a workspace resource. When set, leave `content` empty.',
522
+ },
509
523
  icon: {
510
524
  type: "string",
511
525
  description: "Optional icon name or short label.",
@@ -521,7 +535,7 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
521
535
  run: async (args, ctx) => {
522
536
  const name = String(args?.name ?? "").trim();
523
537
  if (!name) return "Error: name is required.";
524
- const resolved = resolveExtensionContent(args, ctx);
538
+ const resolved = await resolveExtensionContentAsync(args, ctx);
525
539
  if ("error" in resolved) return resolved.error;
526
540
  const content = resolved.content.trim();
527
541
  if (!content) return "Error: content is required.";
@@ -553,9 +567,12 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
553
567
  } catch {
554
568
  // Non-fatal — agent can still mention the path in its reply.
555
569
  }
570
+ const hiddenIds = await getHiddenExtensionIdsForCurrentUser();
556
571
  return {
557
572
  ok: true,
558
- extension: { ...existing, path: existingPath },
573
+ // Compact summary (contentLength + contentHash, no full body). Echoing
574
+ // the whole HTML back is pure token waste — the agent just supplied it.
575
+ extension: await summarizeExtension(existing, hiddenIds, false),
559
576
  path: existingPath,
560
577
  next: `Extension was already created in this session (recovered from a connection retry). The user is being navigated to it — no further navigation tool calls needed.`,
561
578
  };
@@ -585,9 +602,12 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
585
602
  // Non-fatal — agent can still mention the path in its reply.
586
603
  }
587
604
 
605
+ const hiddenIds = await getHiddenExtensionIdsForCurrentUser();
588
606
  return {
589
607
  ok: true,
590
- extension: { ...extension, path },
608
+ // Compact summary (contentLength + contentHash, no full body). Echoing
609
+ // the whole HTML back is pure token waste — the agent just supplied it.
610
+ extension: await summarizeExtension(extension, hiddenIds, false),
591
611
  path,
592
612
  next: `Created. The user is being navigated to the new extension automatically — no further navigation tool calls needed.`,
593
613
  };
@@ -624,6 +644,11 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
624
644
  description:
625
645
  'Optional full replacement sourced from a pasted/attached file on the current turn, by attachment name (or the literal "latest" for the most recent pasted block). Use instead of `content` when replacing the whole body with a large pasted file so you do not have to re-type it. Ignored when `content` is provided.',
626
646
  },
647
+ contentFromWorkspaceFile: {
648
+ type: "string",
649
+ description:
650
+ 'Optional full replacement sourced from a workspace/shared resource file, by resource path (e.g. "intuit-analytics-extension.html"). The server reads the full file and uses it as the replacement body. Use instead of `content` when replacing the whole body with a large file that exists as a workspace resource. Ignored when `content` is provided.',
651
+ },
627
652
  patches: {
628
653
  type: "string",
629
654
  description:
@@ -670,9 +695,10 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
670
695
  : undefined;
671
696
  if (
672
697
  replacementContent === undefined &&
673
- args?.contentFromAttachment !== undefined
698
+ (args?.contentFromAttachment !== undefined ||
699
+ args?.contentFromWorkspaceFile !== undefined)
674
700
  ) {
675
- const resolved = resolveExtensionContent(args, ctx);
701
+ const resolved = await resolveExtensionContentAsync(args, ctx);
676
702
  if ("error" in resolved) return resolved.error;
677
703
  replacementContent = resolved.content;
678
704
  }
@@ -1320,6 +1346,114 @@ function resolveExtensionContent(
1320
1346
  return { content: resolved };
1321
1347
  }
1322
1348
 
1349
+ /**
1350
+ * Resolve the workspace-files bridge scope exactly the way run-code's
1351
+ * workspaceRead/workspaceWrite do: org-preferred (org → shared owner) with the
1352
+ * requesting user's email as the solo fallback. Kept in lockstep with
1353
+ * `resolveScope` in `workspace-files/tool.ts`.
1354
+ */
1355
+ function workspaceFilesBridgeScope(): WorkspaceFilesScope | null {
1356
+ const orgId = getRequestOrgId();
1357
+ if (orgId) return { scope: "org", scopeId: orgId };
1358
+ const email = getRequestUserEmail();
1359
+ if (email) return { scope: "user", scopeId: email };
1360
+ return null;
1361
+ }
1362
+
1363
+ /**
1364
+ * Read a workspace/shared/personal resource file's FULL content by path.
1365
+ *
1366
+ * Precedence (single, documented rule so this never silently resolves a
1367
+ * different file than the agent inspected):
1368
+ * 1. The run-code `workspace-files` bridge scope (org → shared owner, else the
1369
+ * user's email). This is the SAME owner/scope `workspaceRead` /
1370
+ * `workspaceWrite` use, so a body the agent staged via `workspaceWrite` is
1371
+ * resolved here verbatim — the two paths cannot diverge.
1372
+ * 2. User-managed Resources (personal override → org/shared → workspace
1373
+ * default) as a fallback, for pre-built resources that were created in the
1374
+ * Resources panel rather than staged through the bridge.
1375
+ *
1376
+ * Unlike attachments, resource content is not capped/truncated on the way in, so
1377
+ * this is the correct path for cloning a large extension body that already
1378
+ * exists as a workspace resource (e.g. a per-customer dashboard).
1379
+ */
1380
+ async function readWorkspaceFileContent(path: string): Promise<string | null> {
1381
+ const trimmed = path.trim();
1382
+ if (!trimmed) return null;
1383
+ // 1) Bridge parity — resolve exactly the file workspaceRead/workspaceWrite see.
1384
+ const bridgeScope = workspaceFilesBridgeScope();
1385
+ if (bridgeScope) {
1386
+ let bridgeFile: Awaited<ReturnType<typeof readWorkspaceFile>>;
1387
+ try {
1388
+ bridgeFile = await readWorkspaceFile(bridgeScope, trimmed);
1389
+ } catch {
1390
+ // A THROW here is a transient store error or invalid path — NOT a
1391
+ // definitive "not found". Fail closed rather than silently hosting a
1392
+ // possibly-different same-path Resources body than workspaceRead
1393
+ // inspected. A retry re-runs this read cleanly.
1394
+ return null;
1395
+ }
1396
+ // A null result means the file genuinely does not exist in the bridge scope;
1397
+ // fall through to user-managed Resources for pre-built resource-panel files.
1398
+ if (bridgeFile && typeof bridgeFile.content === "string") {
1399
+ return bridgeFile.content;
1400
+ }
1401
+ }
1402
+ // 2) Fallback — user-managed Resources by scope precedence.
1403
+ for (const scope of ["personal", "shared", "workspace"] as const) {
1404
+ try {
1405
+ const content = await readResource(trimmed, { scope });
1406
+ if (typeof content === "string") return content;
1407
+ } catch {
1408
+ // A given scope can throw (e.g. `personal` when no user identity is
1409
+ // resolvable in this context). Don't let one scope abort the lookup —
1410
+ // fall through and try the next one.
1411
+ }
1412
+ }
1413
+ return null;
1414
+ }
1415
+
1416
+ /**
1417
+ * Resolve the extension HTML body from (in priority order) inline `content`, a
1418
+ * `contentFromWorkspaceFile` resource path, or a `contentFromAttachment` handle.
1419
+ *
1420
+ * The workspace-file path exists because a large extension body frequently lives
1421
+ * as a workspace resource (not a chat attachment). Without it the model has no
1422
+ * viable route — inline is too large to shuttle reliably, contentFromAttachment
1423
+ * only sees chat attachments, and mutating actions cannot run from run-code — so
1424
+ * it loops and the run aborts with no_progress.
1425
+ */
1426
+ async function resolveExtensionContentAsync(
1427
+ args: Record<string, string> | undefined,
1428
+ ctx: ActionRunContext | undefined,
1429
+ ): Promise<{ content: string } | { error: string }> {
1430
+ const inline = args?.content !== undefined ? String(args.content) : undefined;
1431
+ if (inline !== undefined && inline.trim().length > 0) {
1432
+ return { content: inline };
1433
+ }
1434
+
1435
+ const wsRef =
1436
+ args?.contentFromWorkspaceFile !== undefined
1437
+ ? String(args.contentFromWorkspaceFile).trim()
1438
+ : "";
1439
+ if (wsRef) {
1440
+ const content = await readWorkspaceFileContent(wsRef);
1441
+ if (content === null) {
1442
+ return {
1443
+ error: `Error: contentFromWorkspaceFile="${wsRef}" did not match any readable workspace/shared/personal resource file. Check the exact path (e.g. "intuit-analytics-extension.html"), or pass the HTML inline via content.`,
1444
+ };
1445
+ }
1446
+ if (content.trim().length === 0) {
1447
+ return {
1448
+ error: `Error: workspace file "${wsRef}" is empty. Pass non-empty HTML inline via content, or point contentFromWorkspaceFile at a file with content.`,
1449
+ };
1450
+ }
1451
+ return { content };
1452
+ }
1453
+
1454
+ return resolveExtensionContent(args, ctx);
1455
+ }
1456
+
1323
1457
  function coerceBoolean(value: unknown): boolean {
1324
1458
  return value === true || value === "true";
1325
1459
  }
@@ -310,9 +310,18 @@ export async function handleMcpRequest(
310
310
  // must fail closed rather than trust a spoofable owner-email header that
311
311
  // `fullSurface` would otherwise escalate to the full mutating surface.
312
312
  const requestMeta = deriveRequestMeta(event);
313
+ const hasLocalOwnerHint = Boolean(ownerEmailHeader?.trim());
313
314
  const authResult = await verifyAuth(authHeader, ownerEmailHeader, {
315
+ // A bare localhost URL is still a protected MCP resource. This lets
316
+ // OAuth-native hosts (Kiro, Claude Code, etc.) receive the standard 401
317
+ // challenge and open browser approval instead of silently getting the
318
+ // sparse anonymous dev surface. The stdio proxy remains zero-config for
319
+ // local installs because it forwards an owner hint; an explicit opt-in is
320
+ // available for local diagnostics.
314
321
  allowDevOpen:
315
- isLoopbackRequest(event) && isLoopbackOrigin(requestMeta.origin),
322
+ isLoopbackRequest(event) &&
323
+ isLoopbackOrigin(requestMeta.origin) &&
324
+ (hasLocalOwnerHint || process.env.AGENT_NATIVE_MCP_DEV_OPEN === "1"),
316
325
  resourceUrl: getMcpOAuthAudiences(event),
317
326
  });
318
327
  if (!authResult.authed) {
@@ -45,9 +45,6 @@ export function buildFrameworkPrompts(
45
45
  PROD_FRAMEWORK_PROMPT_COMPACT: string;
46
46
  DEV_FRAMEWORK_PROMPT_COMPACT: string;
47
47
  } {
48
- // Note: FIRST_SESSION_PERSONALIZATION is NOT appended here — it is injected
49
- // at per-request prompt-assembly time only for new threads (no prior messages).
50
- // This prevents the ~1.5KB block from appearing on every request forever.
51
48
  const FRAMEWORK_CORE = buildFrameworkCore(examples, options);
52
49
  const FRAMEWORK_CORE_COMPACT = buildFrameworkCoreCompact(examples, options);
53
50
  const extensionToolsEnabled = options?.extensionTools !== false;