@agent-native/core 0.206.0 → 0.206.1-nightly-20261009213000
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/dist/action.d.ts +2 -0
- package/dist/agent/default-agent-engine.js +25 -0
- package/dist/agent/durable-background.d.ts +2 -0
- package/dist/agent/durable-background.js +2 -0
- package/dist/agent/engine/error-detail.d.ts +1 -0
- package/dist/agent/engine/error-detail.js +6 -1
- package/dist/agent/engine/tool-call-journal-seed.d.ts +1 -1
- package/dist/agent/engine/tool-call-journal-seed.js +47 -11
- package/dist/agent/engine/translate-anthropic.d.ts +9 -0
- package/dist/agent/engine/translate-anthropic.js +18 -3
- package/dist/agent/production-agent.d.ts +6 -4
- package/dist/agent/production-agent.js +126 -85
- package/dist/agent/run-store.js +15 -3
- package/dist/agent/thread-data-builder.js +1 -2
- package/dist/agent/types.d.ts +1 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/chat-threads/store.js +6 -2
- package/dist/cli/design-connect.d.ts +1 -0
- package/dist/cli/design-connect.js +72 -19
- package/dist/client/analytics.js +48 -58
- package/dist/client/chat/agentkit-agent-native.js +11 -10
- package/dist/client/chat/agentkit-protocol.js +5 -1
- package/dist/client/chat/runtime.d.ts +4 -1
- package/dist/client/chat/runtime.js +135 -43
- package/dist/client/session-replay-endpoint.d.ts +1 -0
- package/dist/client/session-replay-endpoint.js +35 -0
- package/dist/client/session-replay.js +2 -24
- package/dist/client/sharing/share-controller-helpers.d.ts +1 -1
- package/dist/client/sharing/share-controller-helpers.js +2 -1
- package/dist/client/sharing/useShareButtonController.js +2 -9
- package/dist/client/use-action.d.ts +1 -0
- package/dist/client/use-action.js +2 -1
- package/dist/deploy/build.js +1 -1
- package/dist/deploy/workspace-deploy.js +65 -6
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/file-upload/registry.d.ts +7 -1
- package/dist/file-upload/registry.js +25 -7
- package/dist/mcp/build-server.d.ts +6 -0
- package/dist/mcp/build-server.js +97 -28
- package/dist/mcp/embed-app.js +9 -8
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/metrics.d.ts +5 -0
- package/dist/observability/metrics.js +62 -7
- package/dist/observability/routes.d.ts +4 -4
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/index.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +5 -5
- package/dist/scripts/docs/search.d.ts +1 -0
- package/dist/scripts/docs/search.js +17 -4
- package/dist/secrets/index.d.ts +1 -1
- package/dist/secrets/index.js +1 -1
- package/dist/secrets/storage.d.ts +1 -0
- package/dist/secrets/storage.js +8 -0
- package/dist/server/action-routes.js +62 -29
- package/dist/server/agent-chat/mcp-options.d.ts +2 -0
- package/dist/server/agent-chat/prompt-resources.js +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +2 -1
- package/dist/server/agent-engine-api-key-route.js +66 -2
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/agent-run-context.js +21 -1
- package/dist/server/agent-teams-run-queue.js +2 -1
- package/dist/server/agents-bundle.js +1 -1
- package/dist/server/auth.js +1 -0
- package/dist/server/core-routes-plugin.js +12 -0
- package/dist/server/embed-session.js +15 -1
- package/dist/server/http-response-telemetry.js +37 -8
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/embed-auth.d.ts +4 -0
- package/dist/shared/embed-auth.js +156 -23
- package/dist/shared/inline-bytes.d.ts +29 -0
- package/dist/shared/inline-bytes.js +170 -0
- package/dist/shared/mcp-embed-headers.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/sharing/actions/list-resource-shares.js +10 -4
- package/dist/sharing/actions/set-resource-visibility.js +3 -1
- package/dist/sharing/actions/share-resource.js +4 -2
- package/dist/sharing/actions/unshare-resource.js +3 -1
- package/dist/sharing/widget-grant.d.ts +19 -0
- package/dist/sharing/widget-grant.js +55 -0
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +20 -21
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/dist/tracking/registry.d.ts +3 -2
- package/dist/tracking/registry.js +12 -3
- package/dist/vite/client.js +6 -1
- package/docs/content/observability.mdx +15 -10
- package/docs/content/tracking.mdx +1 -1
- package/package.json +4 -4
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +20 -21
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const REPLAY_ENDPOINT_PATH = "/api/analytics/replay";
|
|
2
|
+
const URL_BASE = "https://analytics-endpoint.invalid";
|
|
3
|
+
const TRACKING_ENDPOINT_SUFFIXES = [
|
|
4
|
+
"/api/analytics/track",
|
|
5
|
+
"/ssr-track",
|
|
6
|
+
"/build-track",
|
|
7
|
+
"/config-track",
|
|
8
|
+
"/track",
|
|
9
|
+
];
|
|
10
|
+
export function replayEndpointFromAnalyticsEndpoint(endpoint, baseUrl = URL_BASE) {
|
|
11
|
+
const hasScheme = /^[a-z][a-z\d+.-]*:/i.test(endpoint);
|
|
12
|
+
const isProtocolRelative = endpoint.startsWith("//");
|
|
13
|
+
let url;
|
|
14
|
+
try {
|
|
15
|
+
url = new URL(endpoint, baseUrl);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof TypeError)
|
|
19
|
+
return null;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
23
|
+
return null;
|
|
24
|
+
const trackingSuffix = TRACKING_ENDPOINT_SUFFIXES.find((suffix) => url.pathname.endsWith(suffix));
|
|
25
|
+
url.pathname = trackingSuffix
|
|
26
|
+
? `${url.pathname.slice(0, -trackingSuffix.length)}${REPLAY_ENDPOINT_PATH}`
|
|
27
|
+
: REPLAY_ENDPOINT_PATH;
|
|
28
|
+
url.hash = "";
|
|
29
|
+
if (hasScheme)
|
|
30
|
+
return url.toString();
|
|
31
|
+
if (isProtocolRelative) {
|
|
32
|
+
return `//${url.host}${url.pathname}${url.search}`;
|
|
33
|
+
}
|
|
34
|
+
return `${url.pathname}${url.search}`;
|
|
35
|
+
}
|
|
@@ -3,6 +3,7 @@ import { resolveLaneEndpoint } from "../shared/environment-lanes.js";
|
|
|
3
3
|
import { loadOptionalPeer, OptionalPeerDependencyError, } from "../shared/optional-peer.js";
|
|
4
4
|
import { isSyntheticTrafficValue } from "../shared/test-traffic.js";
|
|
5
5
|
import { getOrCreateAnalyticsAnonymousId, getOrCreateAnalyticsSessionId, } from "./analytics-session.js";
|
|
6
|
+
import { replayEndpointFromAnalyticsEndpoint } from "./session-replay-endpoint.js";
|
|
6
7
|
import { SESSION_REPLAY_BLOCK_ATTRIBUTE, SESSION_REPLAY_MASK_ATTRIBUTE, } from "./session-replay-privacy.js";
|
|
7
8
|
import { decideReplayQuotaResponse, parseRetryAfterSeconds, } from "./session-replay-quota.js";
|
|
8
9
|
import { scrubUrl } from "./url-scrub.js";
|
|
@@ -499,33 +500,10 @@ function readFirstEnvNumber(keys) {
|
|
|
499
500
|
}
|
|
500
501
|
return undefined;
|
|
501
502
|
}
|
|
502
|
-
function replayEndpointFromAnalyticsEndpoint(value) {
|
|
503
|
-
try {
|
|
504
|
-
const url = new URL(value);
|
|
505
|
-
if (url.pathname.endsWith("/api/analytics/track")) {
|
|
506
|
-
url.pathname = url.pathname.replace(/\/api\/analytics\/track$/, "/api/analytics/replay");
|
|
507
|
-
return url.toString();
|
|
508
|
-
}
|
|
509
|
-
if (url.pathname.endsWith("/track")) {
|
|
510
|
-
url.pathname = url.pathname.replace(/\/track$/, "/api/analytics/replay");
|
|
511
|
-
return url.toString();
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
catch {
|
|
515
|
-
// Fall through to the relative path cases below.
|
|
516
|
-
}
|
|
517
|
-
if (value.endsWith("/api/analytics/track")) {
|
|
518
|
-
return value.replace(/\/api\/analytics\/track$/, "/api/analytics/replay");
|
|
519
|
-
}
|
|
520
|
-
if (value.endsWith("/track")) {
|
|
521
|
-
return value.replace(/\/track$/, "/api/analytics/replay");
|
|
522
|
-
}
|
|
523
|
-
return null;
|
|
524
|
-
}
|
|
525
503
|
function defaultReplayEndpoint() {
|
|
526
504
|
const analyticsEndpoint = readEnvString("VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT");
|
|
527
505
|
const derived = analyticsEndpoint
|
|
528
|
-
? replayEndpointFromAnalyticsEndpoint(analyticsEndpoint)
|
|
506
|
+
? replayEndpointFromAnalyticsEndpoint(analyticsEndpoint, typeof document === "undefined" ? undefined : document.baseURI)
|
|
529
507
|
: null;
|
|
530
508
|
if (derived)
|
|
531
509
|
return derived;
|
|
@@ -33,7 +33,7 @@ export declare const DEFAULT_MEMBER_SUGGESTION_LIMIT = 25;
|
|
|
33
33
|
export declare const DEFAULT_MEMBER_SEARCH_DEBOUNCE_MS = 140;
|
|
34
34
|
export declare function createShareQueryParams(resourceType: string, resourceId: string): ShareQueryParams;
|
|
35
35
|
export declare function createShareQueryKey(params: ShareQueryParams): ShareQueryKey;
|
|
36
|
-
export declare function useShareQuery<TResponse>(resourceType: string, resourceId: string, enabled?: boolean): {
|
|
36
|
+
export declare function useShareQuery<TResponse>(resourceType: string, resourceId: string, enabled?: boolean, staleTime?: number): {
|
|
37
37
|
params: ShareQueryParams;
|
|
38
38
|
queryKey: ShareQueryKey;
|
|
39
39
|
query: ReturnType<typeof useActionQuery<TResponse>>;
|
|
@@ -10,12 +10,13 @@ export function createShareQueryParams(resourceType, resourceId) {
|
|
|
10
10
|
export function createShareQueryKey(params) {
|
|
11
11
|
return ["action", "list-resource-shares", params];
|
|
12
12
|
}
|
|
13
|
-
export function useShareQuery(resourceType, resourceId, enabled = true) {
|
|
13
|
+
export function useShareQuery(resourceType, resourceId, enabled = true, staleTime) {
|
|
14
14
|
const queryClient = useQueryClient();
|
|
15
15
|
const params = useMemo(() => createShareQueryParams(resourceType, resourceId), [resourceId, resourceType]);
|
|
16
16
|
const queryKey = useMemo(() => createShareQueryKey(params), [params]);
|
|
17
17
|
const query = useActionQuery("list-resource-shares", params, {
|
|
18
18
|
enabled,
|
|
19
|
+
...(staleTime === undefined ? {} : { staleTime }),
|
|
19
20
|
});
|
|
20
21
|
return { params, queryKey, query, queryClient };
|
|
21
22
|
}
|
|
@@ -10,7 +10,7 @@ export function useShareButtonController(options) {
|
|
|
10
10
|
const [visibilityOverride, setVisibilityOverride] = useState(null);
|
|
11
11
|
const visibilitySequenceStartRef = useRef(null);
|
|
12
12
|
const appliedDefaultOpenRef = useRef(false);
|
|
13
|
-
const { queryKey: shareQueryKey, query: sharesQuery, queryClient, } = useShareQuery(options.resourceType, options.resourceId);
|
|
13
|
+
const { queryKey: shareQueryKey, query: sharesQuery, queryClient, } = useShareQuery(options.resourceType, options.resourceId, open, 0);
|
|
14
14
|
const { setVisibility, share, unshare } = useShareMutations();
|
|
15
15
|
const visibilityGuard = useShareMutationGuard();
|
|
16
16
|
const data = sharesQuery.data;
|
|
@@ -24,10 +24,8 @@ export function useShareButtonController(options) {
|
|
|
24
24
|
if (nextOpen) {
|
|
25
25
|
setActiveShareTab(shareTabDefaultValue);
|
|
26
26
|
options.shareTabs?.onValueChange?.(shareTabDefaultValue);
|
|
27
|
-
if (visibilityOverride === null)
|
|
28
|
-
void sharesQuery.refetch();
|
|
29
27
|
}
|
|
30
|
-
}, [options, shareTabDefaultValue
|
|
28
|
+
}, [options, shareTabDefaultValue]);
|
|
31
29
|
useEffect(() => {
|
|
32
30
|
setInviteEmail("");
|
|
33
31
|
setSelectedGroup(null);
|
|
@@ -147,11 +145,6 @@ export function useShareButtonController(options) {
|
|
|
147
145
|
return next;
|
|
148
146
|
});
|
|
149
147
|
}, []);
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
void sharesQuery.refetch();
|
|
152
|
-
// The resource identity is intentionally stable for this controller.
|
|
153
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
154
|
-
}, []);
|
|
155
148
|
const memberSearch = useShareOrgMemberSearch(inviteEmail, canManage && suggestionsOpen, {
|
|
156
149
|
limit: DEFAULT_MEMBER_SUGGESTION_LIMIT,
|
|
157
150
|
debounceMs: DEFAULT_MEMBER_SEARCH_DEBOUNCE_MS,
|
|
@@ -110,5 +110,6 @@ export declare function useActionMutation<TData = undefined, TVariables = undefi
|
|
|
110
110
|
method?: "POST" | "PUT" | "DELETE";
|
|
111
111
|
skipActionQueryInvalidation?: boolean;
|
|
112
112
|
timeoutMs?: number;
|
|
113
|
+
headers?: Record<string, string> | ((variables: TVariables extends undefined ? ActionParams<TName> : TVariables) => Record<string, string> | undefined);
|
|
113
114
|
}): import("@tanstack/react-query").UseMutationResult<TData extends undefined ? ActionResult<TName> : TData, Error, TVariables extends undefined ? ActionParams<TName> : TVariables, unknown>;
|
|
114
115
|
export {};
|
|
@@ -782,12 +782,13 @@ export function useActionQuery(actionName, params, options) {
|
|
|
782
782
|
}
|
|
783
783
|
export function useActionMutation(actionName, options) {
|
|
784
784
|
const queryClient = useQueryClient();
|
|
785
|
-
const { method: methodOpt, onSuccess, skipActionQueryInvalidation = false, timeoutMs, ...restOptions } = options ?? {};
|
|
785
|
+
const { method: methodOpt, onSuccess, skipActionQueryInvalidation = false, timeoutMs, headers, ...restOptions } = options ?? {};
|
|
786
786
|
const method = methodOpt ?? "POST";
|
|
787
787
|
return useMutation({
|
|
788
788
|
...restOptions,
|
|
789
789
|
mutationFn: (params) => actionFetch(actionName, method, params, {
|
|
790
790
|
timeoutMs,
|
|
791
|
+
headers: typeof headers === "function" ? headers(params) : headers,
|
|
791
792
|
}),
|
|
792
793
|
onSuccess: (...args) => {
|
|
793
794
|
// A write that succeeded may have fixed whatever was failing reads.
|
package/dist/deploy/build.js
CHANGED
|
@@ -1910,7 +1910,7 @@ async function getHandler() {
|
|
|
1910
1910
|
headers: {
|
|
1911
1911
|
"Access-Control-Allow-Origin": "*",
|
|
1912
1912
|
"Access-Control-Allow-Methods": "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS",
|
|
1913
|
-
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Client-Platform,X-Agent-Native-Desktop-Verifier,X-Agent-Native-Test-Traffic,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,X-Agent-Native-Embed-Target",
|
|
1913
|
+
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Content-Save-Origin,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Client-Platform,X-Agent-Native-Desktop-Verifier,X-Agent-Native-Test-Traffic,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,X-Agent-Native-Embed-Target",
|
|
1914
1914
|
},
|
|
1915
1915
|
});
|
|
1916
1916
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
2
|
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
3
4
|
import path from "path";
|
|
4
5
|
import { AGENT_BACKGROUND_PROCESSOR_A2A, AGENT_BACKGROUND_PROCESSOR_AGENT_TEAM, AGENT_BACKGROUND_PROCESSOR_FIELD, AGENT_BACKGROUND_PROCESSOR_INTEGRATION, AGENT_BACKGROUND_PROCESSOR_ROUTE, AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, AGENT_TEAM_PROCESS_RUN_PATH, isDurableBackgroundFlagExplicitlyDisabled, } from "../agent/durable-background.js";
|
|
5
6
|
import { getAppConfig } from "../app-config/index.js";
|
|
@@ -210,10 +211,12 @@ export async function runWorkspaceDeploy(opts = {}) {
|
|
|
210
211
|
fs.mkdirSync(functionsDir, { recursive: true });
|
|
211
212
|
}
|
|
212
213
|
console.log(`[workspace-deploy] Building ${apps.length} app(s) for preset=${preset}`);
|
|
213
|
-
const
|
|
214
|
+
const concurrency = workspaceBuildConcurrency();
|
|
215
|
+
const runBuild = opts.execFile ?? spawnBuild;
|
|
216
|
+
await runWithConcurrency(apps, concurrency, (app) => buildOneApp(workspaceRoot, appsDir, app, preset, runBuild, workspaceApps, workspaceAuthMode, concurrency > 1 ? "pipe" : "inherit"));
|
|
217
|
+
// Copy in app order after all builds so shared outputs stay deterministic.
|
|
214
218
|
const sweepCrons = [];
|
|
215
219
|
for (const app of apps) {
|
|
216
|
-
buildOneApp(workspaceRoot, appsDir, app, preset, execFile, workspaceApps, workspaceAuthMode);
|
|
217
220
|
moveAppBuildIntoWorkspaceOutput(workspaceRoot, appsDir, app, preset, distDir, vercelOutputDir, workspaceApps, workspaceAuthMode);
|
|
218
221
|
if (preset === "vercel") {
|
|
219
222
|
sweepCrons.push(readVercelSweepCron(path.join(appsDir, app, VERCEL_OUTPUT_DIR), `/${app}`));
|
|
@@ -245,7 +248,7 @@ export async function runWorkspaceDeploy(opts = {}) {
|
|
|
245
248
|
}
|
|
246
249
|
console.log(`All apps live at https://<origin>/<app-name>/*. Log in once on any app\nand the session is shared across the workspace.`);
|
|
247
250
|
}
|
|
248
|
-
function buildOneApp(workspaceRoot, appsDir, app, preset,
|
|
251
|
+
async function buildOneApp(workspaceRoot, appsDir, app, preset, runBuild, workspaceApps, workspaceAuthMode, stdio) {
|
|
249
252
|
const appDir = path.join(appsDir, app);
|
|
250
253
|
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
251
254
|
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(workspaceApps, app);
|
|
@@ -306,10 +309,66 @@ function buildOneApp(workspaceRoot, appsDir, app, preset, execFile, workspaceApp
|
|
|
306
309
|
}
|
|
307
310
|
console.log(`[workspace-deploy] Building ${app} (base=/${app}, preset=${preset})`);
|
|
308
311
|
cleanAppBuildOutputs(appDir);
|
|
309
|
-
|
|
312
|
+
const startedAt = Date.now();
|
|
313
|
+
await runBuild("pnpm", ["--filter", app, "build"], {
|
|
310
314
|
cwd: workspaceRoot,
|
|
311
315
|
env,
|
|
312
|
-
stdio
|
|
316
|
+
stdio,
|
|
317
|
+
});
|
|
318
|
+
console.log(`[workspace-deploy] Built ${app} in ${((Date.now() - startedAt) / 1000).toFixed(1)}s`);
|
|
319
|
+
}
|
|
320
|
+
function workspaceBuildConcurrency() {
|
|
321
|
+
const raw = process.env.AGENT_NATIVE_DEPLOY_CONCURRENCY?.trim();
|
|
322
|
+
if (raw) {
|
|
323
|
+
const parsed = Number(raw);
|
|
324
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
325
|
+
throw new Error(`AGENT_NATIVE_DEPLOY_CONCURRENCY must be a positive integer, got "${raw}"`);
|
|
326
|
+
}
|
|
327
|
+
return parsed;
|
|
328
|
+
}
|
|
329
|
+
// Each app build can use 1-2 GB, so stay well under typical CI memory limits.
|
|
330
|
+
return Math.min(3, os.availableParallelism());
|
|
331
|
+
}
|
|
332
|
+
async function runWithConcurrency(items, limit, fn) {
|
|
333
|
+
let next = 0;
|
|
334
|
+
const errors = [];
|
|
335
|
+
const worker = async () => {
|
|
336
|
+
while (errors.length === 0 && next < items.length) {
|
|
337
|
+
try {
|
|
338
|
+
await fn(items[next++]);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
errors.push(error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
// Let in-flight builds finish so none are orphaned when the CLI exits and
|
|
346
|
+
// every failure gets reported, not just the first.
|
|
347
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
348
|
+
if (errors.length === 1)
|
|
349
|
+
throw errors[0];
|
|
350
|
+
if (errors.length > 1) {
|
|
351
|
+
throw new Error(errors
|
|
352
|
+
.map((error) => error instanceof Error ? error.message : String(error))
|
|
353
|
+
.join("\n"));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function spawnBuild(cmd, args, options) {
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
const child = spawn(cmd, args, options);
|
|
359
|
+
// Buffered so parallel builds print as whole blocks instead of interleaving.
|
|
360
|
+
const chunks = [];
|
|
361
|
+
child.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
362
|
+
child.stderr?.on("data", (chunk) => chunks.push(chunk));
|
|
363
|
+
child.on("error", reject);
|
|
364
|
+
child.on("close", (code, signal) => {
|
|
365
|
+
if (chunks.length)
|
|
366
|
+
process.stdout.write(Buffer.concat(chunks));
|
|
367
|
+
if (code === 0)
|
|
368
|
+
resolve();
|
|
369
|
+
else
|
|
370
|
+
reject(new Error(`Command failed: ${cmd} ${args.join(" ")} (${signal ?? `exit code ${code}`})`));
|
|
371
|
+
});
|
|
313
372
|
});
|
|
314
373
|
}
|
|
315
374
|
function moveAppBuildIntoWorkspaceOutput(workspaceRoot, appsDir, app, preset, distDir, vercelOutputDir, workspaceApps, workspaceAuthMode) {
|
|
@@ -33,9 +33,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
33
33
|
idempotencyKey: string;
|
|
34
34
|
deleted: boolean;
|
|
35
35
|
} | {
|
|
36
|
-
id?: undefined;
|
|
37
36
|
error: string;
|
|
38
37
|
url?: undefined;
|
|
38
|
+
id?: undefined;
|
|
39
39
|
provider?: undefined;
|
|
40
40
|
configured?: undefined;
|
|
41
41
|
} | {
|
|
@@ -45,8 +45,8 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
45
45
|
configured?: undefined;
|
|
46
46
|
error?: undefined;
|
|
47
47
|
} | {
|
|
48
|
-
id?: undefined;
|
|
49
48
|
url?: undefined;
|
|
49
|
+
id?: undefined;
|
|
50
50
|
provider?: undefined;
|
|
51
51
|
error: string;
|
|
52
52
|
configured: boolean;
|
|
@@ -2,7 +2,13 @@ import type { FileUploadDeleteInput, FileUploadInput, FileUploadProvider, FileUp
|
|
|
2
2
|
export declare function registerFileUploadProvider(provider: FileUploadProvider): void;
|
|
3
3
|
export declare function unregisterFileUploadProvider(id: string): void;
|
|
4
4
|
export declare function listFileUploadProviders(): FileUploadProvider[];
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a public upload URL only through a provider that claims ownership.
|
|
7
|
+
* Each provider is checked independently: one provider whose check throws
|
|
8
|
+
* (S3 reads its config from the DB) must not hide another's positive claim, so
|
|
9
|
+
* this throws only when no provider claims the URL and at least one could not
|
|
10
|
+
* answer — "unverifiable", never reported as "not owned".
|
|
11
|
+
*/
|
|
6
12
|
export declare function findFileUploadProviderOwningUrl(url: string): Promise<FileUploadProvider | null>;
|
|
7
13
|
export declare function getActiveFileUploadProvider(): FileUploadProvider | null;
|
|
8
14
|
/**
|
|
@@ -11,17 +11,35 @@ export function unregisterFileUploadProvider(id) {
|
|
|
11
11
|
export function listFileUploadProviders() {
|
|
12
12
|
return [...providers.values()];
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a public upload URL only through a provider that claims ownership.
|
|
16
|
+
* Each provider is checked independently: one provider whose check throws
|
|
17
|
+
* (S3 reads its config from the DB) must not hide another's positive claim, so
|
|
18
|
+
* this throws only when no provider claims the URL and at least one could not
|
|
19
|
+
* answer — "unverifiable", never reported as "not owned".
|
|
20
|
+
*/
|
|
15
21
|
export async function findFileUploadProviderOwningUrl(url) {
|
|
16
|
-
const candidates = [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const candidates = [
|
|
23
|
+
builderFileUploadProvider,
|
|
24
|
+
...[...providers.values()].filter((p) => p !== builderFileUploadProvider),
|
|
25
|
+
];
|
|
26
|
+
const failures = [];
|
|
27
|
+
const unverifiable = [];
|
|
20
28
|
for (const provider of candidates) {
|
|
21
|
-
if (provider.isOwnedUrl
|
|
22
|
-
|
|
29
|
+
if (!provider.isOwnedUrl)
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
if (await provider.isOwnedUrl(url))
|
|
33
|
+
return provider;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
failures.push(error);
|
|
37
|
+
unverifiable.push(provider.id);
|
|
23
38
|
}
|
|
24
39
|
}
|
|
40
|
+
if (failures.length > 0) {
|
|
41
|
+
throw new AggregateError(failures, `Upload URL ownership is unverifiable: ${unverifiable.join(", ")} failed its check`);
|
|
42
|
+
}
|
|
25
43
|
return null;
|
|
26
44
|
}
|
|
27
45
|
export function getActiveFileUploadProvider() {
|
|
@@ -74,6 +74,12 @@ export interface MCPConfig {
|
|
|
74
74
|
widgetReadPrivateActions?: readonly string[];
|
|
75
75
|
/** Authenticated reads surfaced on other agent profiles, but only scoped here. */
|
|
76
76
|
widgetReadAuthenticatedActions?: readonly string[];
|
|
77
|
+
/**
|
|
78
|
+
* Scoped reads minted only into a write capability, and only for a target
|
|
79
|
+
* that lists the mapped write action: `{ "list-resource-shares":
|
|
80
|
+
* "share-resource" }` keeps collaborator lists out of read-only tickets.
|
|
81
|
+
*/
|
|
82
|
+
widgetReadActionWriteGates?: Record<string, string>;
|
|
77
83
|
/** Omit the one shared resource title when tools have distinct invocation labels. */
|
|
78
84
|
widgetResourceTitle?: string | false;
|
|
79
85
|
keyToolNames?: readonly string[];
|
package/dist/mcp/build-server.js
CHANGED
|
@@ -356,6 +356,12 @@ export function validateMcpDirectoryProfile(config, sourceActions = config.produ
|
|
|
356
356
|
throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget read route "${name}" must be an explicitly scoped read-only GET action with valid resource arguments in the connector allowlist or hidden-read profiles.`);
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
|
+
for (const [readAction, writeAction] of Object.entries(profile?.widgetReadActionWriteGates ?? {})) {
|
|
360
|
+
if (!Object.hasOwn(profile?.widgetReadActionArguments ?? {}, readAction) ||
|
|
361
|
+
!Object.hasOwn(profile?.widgetWriteActionArguments ?? {}, writeAction)) {
|
|
362
|
+
throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget read "${readAction}" is gated on "${writeAction}", which must both be declared widget actions.`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
359
365
|
for (const [name, argumentMap] of Object.entries(profile?.widgetWriteActionArguments ?? {})) {
|
|
360
366
|
const entry = actions[name] ?? config.widgetWriteActions?.[name];
|
|
361
367
|
if (!entry ||
|
|
@@ -894,16 +900,6 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
|
|
|
894
900
|
(ctx?.orgId ?? undefined))) {
|
|
895
901
|
throw new Error("The widget write grant must match the authenticated MCP user and organization.");
|
|
896
902
|
}
|
|
897
|
-
const scope = restrictDirectoryWidgetCapability
|
|
898
|
-
? directoryWidget.capability.mode === "write"
|
|
899
|
-
? createMcpDirectoryWidgetWriteCapability(directoryWidget.capability.value)
|
|
900
|
-
: createMcpDirectoryWidgetReadCapability(directoryWidget.capability.value)
|
|
901
|
-
: typeof out.chrome === "string"
|
|
902
|
-
? out.chrome
|
|
903
|
-
: null;
|
|
904
|
-
if (restrictDirectoryWidgetCapability && !scope) {
|
|
905
|
-
throw new Error("Could not create a valid scoped capability for this MCP directory widget.");
|
|
906
|
-
}
|
|
907
903
|
const revocationAnchorCandidate = ctx?.mcpCredentialIssuedAtMs;
|
|
908
904
|
const revocationAnchorCreatedAtMs = typeof revocationAnchorCandidate === "number" &&
|
|
909
905
|
Number.isSafeInteger(revocationAnchorCandidate)
|
|
@@ -913,18 +909,60 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
|
|
|
913
909
|
revocationAnchorCreatedAtMs === undefined) {
|
|
914
910
|
throw new Error("Directory widget sessions require a trusted MCP credential issue time.");
|
|
915
911
|
}
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
912
|
+
const mintTicket = async (capability) => {
|
|
913
|
+
const scope = capability
|
|
914
|
+
? capability.mode === "write"
|
|
915
|
+
? createMcpDirectoryWidgetWriteCapability(capability.value)
|
|
916
|
+
: createMcpDirectoryWidgetReadCapability(capability.value)
|
|
917
|
+
: typeof out.chrome === "string"
|
|
918
|
+
? out.chrome
|
|
919
|
+
: null;
|
|
920
|
+
// An oversize or invalid scope has no ticket; the caller degrades or
|
|
921
|
+
// returns the result without one. Throwing here would fail a tool call
|
|
922
|
+
// whose action already ran.
|
|
923
|
+
if (capability && !scope)
|
|
924
|
+
return null;
|
|
925
|
+
return createEmbedSessionTicket({
|
|
926
|
+
ownerEmail,
|
|
927
|
+
orgId: ctx?.orgId,
|
|
928
|
+
targetPath,
|
|
929
|
+
scope,
|
|
930
|
+
...(capability?.mode === "write" ? { ttlSeconds: 5 * 60 } : {}),
|
|
931
|
+
...(capability ? { revocationAnchorCreatedAtMs } : {}),
|
|
932
|
+
});
|
|
933
|
+
};
|
|
934
|
+
const writeCapability = directoryWidget?.capability.mode === "write"
|
|
935
|
+
? directoryWidget.capability
|
|
936
|
+
: undefined;
|
|
937
|
+
let ticket = null;
|
|
938
|
+
let writeMintError = new Error("Could not create a valid scoped capability for this MCP directory widget.");
|
|
939
|
+
try {
|
|
940
|
+
ticket = await mintTicket(directoryWidget?.capability);
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
if (!writeCapability)
|
|
944
|
+
throw error;
|
|
945
|
+
writeMintError = error;
|
|
946
|
+
}
|
|
947
|
+
if (!ticket && writeCapability) {
|
|
948
|
+
// The write grant is an upgrade over the read grant. Losing it must not
|
|
949
|
+
// also lose the widget's session ticket, which the shell cannot start without.
|
|
950
|
+
console.error("[mcp:directory] Could not mint the widget write grant; issuing a read-only widget session instead.", writeMintError);
|
|
951
|
+
const { appId, resourceUri, resourceIds, readActionArguments } = writeCapability.value;
|
|
952
|
+
ticket = await mintTicket({
|
|
953
|
+
mode: "read",
|
|
954
|
+
value: {
|
|
955
|
+
appId,
|
|
956
|
+
resourceUri,
|
|
957
|
+
resourceIds,
|
|
958
|
+
actionArguments: readActionArguments,
|
|
959
|
+
},
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
if (!ticket) {
|
|
963
|
+
console.error("[mcp:directory] Could not build a widget capability within the scope size limits; returning the result without a session ticket.", { targetPath });
|
|
964
|
+
return resultWithoutExistingEmbedTicket;
|
|
965
|
+
}
|
|
928
966
|
const startPath = buildEmbedStartPath(ticket.ticket);
|
|
929
967
|
const embedStartUrl = meta?.origin
|
|
930
968
|
? new URL(startPath, meta.origin).toString()
|
|
@@ -957,6 +995,7 @@ async function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, a
|
|
|
957
995
|
if (!target)
|
|
958
996
|
return undefined;
|
|
959
997
|
const actionArguments = {};
|
|
998
|
+
const writeGatedReadArguments = {};
|
|
960
999
|
for (const [actionName, argumentMap] of Object.entries(profile.widgetReadActionArguments ?? {})) {
|
|
961
1000
|
const scopedPublicRead = profile.widgetReadPublicActions?.includes(actionName);
|
|
962
1001
|
const scopedPrivateRead = profile.widgetReadPrivateActions?.includes(actionName);
|
|
@@ -992,7 +1031,13 @@ async function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, a
|
|
|
992
1031
|
}
|
|
993
1032
|
}
|
|
994
1033
|
if (Object.keys(scopedArguments).length === Object.keys(argumentMap).length) {
|
|
995
|
-
|
|
1034
|
+
const writeGate = profile.widgetReadActionWriteGates?.[actionName];
|
|
1035
|
+
if (writeGate === undefined) {
|
|
1036
|
+
actionArguments[actionName] = scopedArguments;
|
|
1037
|
+
}
|
|
1038
|
+
else if (target.writeActions?.includes(writeGate)) {
|
|
1039
|
+
writeGatedReadArguments[actionName] = { writeGate, scopedArguments };
|
|
1040
|
+
}
|
|
996
1041
|
}
|
|
997
1042
|
}
|
|
998
1043
|
const writeActionArguments = {};
|
|
@@ -1071,7 +1116,15 @@ async function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, a
|
|
|
1071
1116
|
userEmail: identity.userEmail,
|
|
1072
1117
|
...(typeof requestOrgId === "string" ? { orgId: requestOrgId } : {}),
|
|
1073
1118
|
expiresAtMs: Date.now() + MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_AGE_MS,
|
|
1074
|
-
readActionArguments:
|
|
1119
|
+
readActionArguments: {
|
|
1120
|
+
...actionArguments,
|
|
1121
|
+
...Object.fromEntries(Object.entries(writeGatedReadArguments)
|
|
1122
|
+
.filter(([, { writeGate }]) => Object.hasOwn(writeActionArguments, writeGate))
|
|
1123
|
+
.map(([actionName, { scopedArguments }]) => [
|
|
1124
|
+
actionName,
|
|
1125
|
+
scopedArguments,
|
|
1126
|
+
])),
|
|
1127
|
+
},
|
|
1075
1128
|
writeActionArguments,
|
|
1076
1129
|
};
|
|
1077
1130
|
return {
|
|
@@ -2441,11 +2494,24 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2441
2494
|
mcpAppResourceCandidate !== null &&
|
|
2442
2495
|
directoryWidget === undefined;
|
|
2443
2496
|
const suppressDirectoryWidget = directoryCatalog && !hasVerifiedMcpUserIdentity(effectiveIdentity);
|
|
2497
|
+
// The host mounts a widget for this tool from its descriptor, with or
|
|
2498
|
+
// without a ticket in the result, so a withheld ticket must be visible.
|
|
2499
|
+
if (missingDirectoryWidgetCapability &&
|
|
2500
|
+
!suppressDirectoryWidget &&
|
|
2501
|
+
!mcpResultIsError) {
|
|
2502
|
+
console.warn(`[mcp:directory] ${name} returned no widget session ticket: ${typeof trustedCredentialIssuedAtMs === "number"
|
|
2503
|
+
? "the result has no widget target or scoped actions"
|
|
2504
|
+
: "the credential carries no issue time"}.`);
|
|
2505
|
+
}
|
|
2444
2506
|
const rawResultForClient = missingDirectoryWidgetCapability || suppressDirectoryWidget
|
|
2445
2507
|
? withoutMcpAppEmbedTicket(projectedRawResult)
|
|
2446
2508
|
: mcpAppResourceCandidate
|
|
2447
2509
|
? await withServerMintedMcpAppEmbedStart(projectedRawResult, requestMeta, directoryLinkUrl, directoryWidget)
|
|
2448
2510
|
: projectedRawResult;
|
|
2511
|
+
if (directoryWidget &&
|
|
2512
|
+
typeof metadataObject(rawResultForClient).embedStartUrl !== "string") {
|
|
2513
|
+
console.error(`[mcp:directory] ${name} built a widget capability but issued no session ticket.`);
|
|
2514
|
+
}
|
|
2449
2515
|
const { value: actionResultForClient, images: resultImages, notes: resultImageNotes, } = extractAgentImagesFromActionResult(rawResultForClient);
|
|
2450
2516
|
const textResultForClient = mcpResult
|
|
2451
2517
|
? projectedResultForClient
|
|
@@ -2996,12 +3062,15 @@ async function admitIssuedMcpCredential(credential, requestOrigin) {
|
|
|
2996
3062
|
stored = lookup;
|
|
2997
3063
|
}
|
|
2998
3064
|
const orgId = credential.orgId !== undefined ? credential.orgId : stored?.orgId;
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3065
|
+
// Access tokens signed before grant times existed carry no claim, and the
|
|
3066
|
+
// directory widget refuses to mint a session without an issue time. Their
|
|
3067
|
+
// signed `iat` is the only anchor they have. `iat` moves on every refresh, so
|
|
3068
|
+
// it must stay the fallback and never outrank a signed grant time.
|
|
3069
|
+
const credentialIssuedAtMs = credential.grantCreatedAtMs ??
|
|
3070
|
+
(typeof credential.issuedAt === "number" &&
|
|
3002
3071
|
Number.isSafeInteger(credential.issuedAt)
|
|
3003
3072
|
? credential.issuedAt * 1000
|
|
3004
|
-
: undefined;
|
|
3073
|
+
: undefined);
|
|
3005
3074
|
const mcpCredentialIssuedAtMs = credentialIssuedAtMs !== undefined &&
|
|
3006
3075
|
Number.isSafeInteger(credentialIssuedAtMs)
|
|
3007
3076
|
? credentialIssuedAtMs
|
package/dist/mcp/embed-app.js
CHANGED
|
@@ -128,6 +128,9 @@ export function embedApp(options = {}) {
|
|
|
128
128
|
let toolResponseMetadata = {};
|
|
129
129
|
let openUrl = "";
|
|
130
130
|
let openStartUrl = "";
|
|
131
|
+
// Start URLs are single use. The host replays a saved one on every re-sync,
|
|
132
|
+
// so remember which one the server has already refused.
|
|
133
|
+
let spentStartUrl = "";
|
|
131
134
|
let activeEmbedSessionTicket = "";
|
|
132
135
|
let startedFor = "";
|
|
133
136
|
let appFrame = null;
|
|
@@ -1503,7 +1506,7 @@ export function embedApp(options = {}) {
|
|
|
1503
1506
|
return;
|
|
1504
1507
|
}
|
|
1505
1508
|
embedSessionRefreshAttempts += 1;
|
|
1506
|
-
|
|
1509
|
+
spentStartUrl = openStartUrl;
|
|
1507
1510
|
startedFor = "";
|
|
1508
1511
|
lastFrameSrc = "";
|
|
1509
1512
|
setMessage("Refreshing app session");
|
|
@@ -2001,17 +2004,15 @@ export function embedApp(options = {}) {
|
|
|
2001
2004
|
return;
|
|
2002
2005
|
}
|
|
2003
2006
|
if (message.type === "agentNative.embedSessionExpired") {
|
|
2004
|
-
if (expiredSessionMessage) {
|
|
2005
|
-
refreshExpiredEmbedSession();
|
|
2006
|
-
} else if (typeof data.requestId === "string") {
|
|
2007
|
+
if (!expiredSessionMessage && typeof data.requestId === "string") {
|
|
2007
2008
|
void renewExpiredEmbedSession(
|
|
2008
2009
|
data.requestId,
|
|
2009
2010
|
appFrame,
|
|
2010
2011
|
appFrameDocumentState.loadGeneration,
|
|
2011
2012
|
);
|
|
2012
|
-
} else
|
|
2013
|
-
//
|
|
2014
|
-
//
|
|
2013
|
+
} else {
|
|
2014
|
+
// The server's expiry page names no request: it answered in place of
|
|
2015
|
+
// the app, so there is no mounted session to renew in place.
|
|
2015
2016
|
refreshExpiredEmbedSession();
|
|
2016
2017
|
}
|
|
2017
2018
|
return;
|
|
@@ -2063,7 +2064,7 @@ export function embedApp(options = {}) {
|
|
|
2063
2064
|
notifyHostHeight();
|
|
2064
2065
|
return;
|
|
2065
2066
|
}
|
|
2066
|
-
let launchUrl = openStartUrl || openUrl;
|
|
2067
|
+
let launchUrl = (openStartUrl !== spentStartUrl && openStartUrl) || openUrl;
|
|
2067
2068
|
if (!launchUrl) {
|
|
2068
2069
|
renderAppLaunchError("Open link was not available.");
|
|
2069
2070
|
return;
|