@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/create.ts +45 -8
- package/corpus/core/src/client/assistant-ui-recovery.tsx +9 -3
- package/corpus/core/src/client/chat/message-components.tsx +25 -2
- package/corpus/core/src/client/chat/tool-call-display.tsx +6 -9
- package/corpus/core/src/coding-tools/run-code.ts +65 -12
- package/corpus/core/src/extensions/actions.ts +141 -7
- package/corpus/core/src/mcp/server.ts +10 -1
- package/corpus/core/src/server/agent-chat/framework-prompts.ts +0 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +2 -58
- package/corpus/core/src/server/prompts/framework-core-compact.ts +0 -1
- package/corpus/core/src/server/prompts/framework-core.ts +0 -30
- package/corpus/core/src/server/prompts/index.ts +1 -4
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +59 -0
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +5 -2
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +13 -4
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +63 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +30 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +0 -14
- package/corpus/templates/chat/changelog/2026-07-15-chat-navigation-focuses-on-chat-and-agent.md +6 -0
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +31 -7
- package/dist/cli/create.js.map +1 -1
- package/dist/client/assistant-ui-recovery.d.ts +2 -0
- package/dist/client/assistant-ui-recovery.d.ts.map +1 -1
- package/dist/client/assistant-ui-recovery.js +5 -2
- package/dist/client/assistant-ui-recovery.js.map +1 -1
- package/dist/client/chat/message-components.d.ts +6 -0
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +14 -1
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +5 -8
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/coding-tools/run-code.js +64 -11
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +129 -7
- package/dist/extensions/actions.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +10 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-chat/framework-prompts.d.ts.map +1 -1
- package/dist/server/agent-chat/framework-prompts.js +0 -3
- package/dist/server/agent-chat/framework-prompts.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +0 -22
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +2 -51
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/prompts/framework-core-compact.d.ts +0 -1
- package/dist/server/prompts/framework-core-compact.d.ts.map +1 -1
- package/dist/server/prompts/framework-core-compact.js +0 -1
- package/dist/server/prompts/framework-core-compact.js.map +1 -1
- package/dist/server/prompts/framework-core.d.ts +0 -9
- package/dist/server/prompts/framework-core.d.ts.map +1 -1
- package/dist/server/prompts/framework-core.js +0 -29
- package/dist/server/prompts/framework-core.js.map +1 -1
- package/dist/server/prompts/index.d.ts +1 -1
- package/dist/server/prompts/index.d.ts.map +1 -1
- package/dist/server/prompts/index.js +1 -1
- package/dist/server/prompts/index.js.map +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -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
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.101.
|
|
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
|
|
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
|
-
|
|
1376
|
-
|
|
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, {
|
|
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={
|
|
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
|
-
|
|
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,
|
|
1091
|
+
/** When true, close the summary after a run has completed. */
|
|
1092
1092
|
autoCollapse?: boolean;
|
|
1093
1093
|
children: React.ReactNode;
|
|
1094
1094
|
}) {
|
|
1095
|
-
|
|
1096
|
-
|
|
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 (
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
|
|
1541
|
-
if (
|
|
1542
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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) &&
|
|
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;
|