@agent-native/core 0.176.1 → 0.177.0-nightly-20260828143953
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/templates/analytics/actions/set-workspace-feature-flag.ts +2 -2
- package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +16 -1
- package/corpus/templates/analytics/changelog/2026-08-27-show-analytics-s-own-registered-flags-in-fleet-feature-flag-.md +6 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +204 -32
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +3 -3
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +87 -4
- package/corpus/templates/slides/changelog/2026-08-28-shared-slides-with-a-style-block-render-again.md +5 -0
- package/dist/action.d.ts +3 -1
- package/dist/client/app-providers.js +13 -1
- package/dist/client/host-bridge.d.ts +1 -0
- package/dist/client/webmcp.d.ts +4 -0
- package/dist/client/webmcp.js +51 -4
- package/dist/collab/routes.d.ts +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/action-routes.d.ts +1 -6
- package/dist/server/action-routes.js +58 -17
- package/dist/server/agent-chat-plugin.js +8 -1
- package/docs/content/webmcp.mdx +31 -12
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -30,7 +30,7 @@ const schema = z.discriminatedUnion("operation", [
|
|
|
30
30
|
]);
|
|
31
31
|
export default defineAction({
|
|
32
32
|
description:
|
|
33
|
-
"Persist one feature-flag change on a trusted organization app.
|
|
33
|
+
"Persist one feature-flag change on Analytics or a trusted organization app. Analytics uses its local flag action; peer targets are resolved through the organization directory.",
|
|
34
34
|
schema,
|
|
35
35
|
agentInputSchema: z.object({
|
|
36
36
|
appId: z.string(),
|
|
@@ -40,6 +40,6 @@ export default defineAction({
|
|
|
40
40
|
}),
|
|
41
41
|
run: async (args, ctx) => {
|
|
42
42
|
const admin = await requireAnalyticsAdminContext(ctx);
|
|
43
|
-
return setWorkspaceFeatureFlag(admin, args);
|
|
43
|
+
return setWorkspaceFeatureFlag(admin, args, ctx);
|
|
44
44
|
},
|
|
45
45
|
});
|
|
@@ -89,6 +89,10 @@ function asDirectory(value: unknown): FlagDirectory {
|
|
|
89
89
|
return value && typeof value === "object" ? (value as FlagDirectory) : {};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
export function hasFleetApps(value: unknown): boolean {
|
|
93
|
+
return (asDirectory(value).apps?.length ?? 0) > 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
92
96
|
function isReady(app: FlagApp) {
|
|
93
97
|
return app.state === "ready" || app.status === "ready";
|
|
94
98
|
}
|
|
@@ -188,7 +192,10 @@ export function FeatureFlagsFleetPanel() {
|
|
|
188
192
|
const directory = asDirectory(flags.data);
|
|
189
193
|
const apps = directory.apps ?? [];
|
|
190
194
|
if (flags.isLoading) return <PanelLoading />;
|
|
191
|
-
if (
|
|
195
|
+
if (
|
|
196
|
+
flags.error ||
|
|
197
|
+
(directory.directoryStatus === "unavailable" && !hasFleetApps(directory))
|
|
198
|
+
)
|
|
192
199
|
return (
|
|
193
200
|
<StatusState
|
|
194
201
|
title={t("agents.flagsUnavailable")}
|
|
@@ -226,6 +233,14 @@ export function FeatureFlagsFleetPanel() {
|
|
|
226
233
|
{t("agents.reloadFlags")}
|
|
227
234
|
</Button>
|
|
228
235
|
</div>
|
|
236
|
+
{directory.directoryStatus === "unavailable" ? (
|
|
237
|
+
<p
|
|
238
|
+
className="rounded-lg border px-4 py-3 text-sm text-muted-foreground"
|
|
239
|
+
role="status"
|
|
240
|
+
>
|
|
241
|
+
{t("agents.flagsUnreachable")}
|
|
242
|
+
</p>
|
|
243
|
+
) : null}
|
|
229
244
|
{apps.map((app) => (
|
|
230
245
|
<section key={appId(app)} className="overflow-hidden rounded-lg border">
|
|
231
246
|
<div className="flex items-center justify-between gap-3 border-b bg-muted/30 px-4 py-3">
|
|
@@ -4,14 +4,18 @@ import { signA2AToken } from "@agent-native/core/a2a";
|
|
|
4
4
|
import {
|
|
5
5
|
ActionContractError,
|
|
6
6
|
AgentActionStopError,
|
|
7
|
+
type ActionRunContext,
|
|
7
8
|
} from "@agent-native/core/action";
|
|
8
9
|
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
|
10
|
+
import listLocalFeatureFlagsAction from "@agent-native/core/feature-flags/actions/list-feature-flags";
|
|
11
|
+
import setLocalFeatureFlagAction from "@agent-native/core/feature-flags/actions/set-feature-flag";
|
|
9
12
|
import {
|
|
10
13
|
fetchOrgApps,
|
|
11
14
|
fetchOrgAppsResult,
|
|
12
15
|
type OrgApp,
|
|
13
16
|
} from "@agent-native/core/mcp";
|
|
14
17
|
import { getOrgDomain } from "@agent-native/core/org";
|
|
18
|
+
import { getAppConfig } from "@agent-native/core/server";
|
|
15
19
|
|
|
16
20
|
import {
|
|
17
21
|
RESILIENT_FLEET_FLAG_DIRECTORY,
|
|
@@ -23,6 +27,7 @@ const TARGET_TIMEOUT_MS = 3_000;
|
|
|
23
27
|
const DIRECTORY_ATTEMPTS = 2;
|
|
24
28
|
const VERIFICATION_ATTEMPTS = 2;
|
|
25
29
|
const CONCURRENCY = 4;
|
|
30
|
+
const ANALYTICS_APP_ID = "analytics";
|
|
26
31
|
|
|
27
32
|
export type FleetFlagState =
|
|
28
33
|
| "ready"
|
|
@@ -91,6 +96,37 @@ export function workspaceFeatureFlagTargetInput(
|
|
|
91
96
|
: rawTargetInput;
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
function mutationValidationExpectation(
|
|
100
|
+
admin: AnalyticsAdminContext,
|
|
101
|
+
input: WorkspaceFeatureFlagMutationInput,
|
|
102
|
+
orgDomain: string,
|
|
103
|
+
) {
|
|
104
|
+
return {
|
|
105
|
+
key: input.key,
|
|
106
|
+
orgDomain,
|
|
107
|
+
allowExplicitNoOrgTarget: true,
|
|
108
|
+
...(input.operation === "replace-rules" && input.rules
|
|
109
|
+
? {
|
|
110
|
+
rules: {
|
|
111
|
+
mode: input.rules.mode,
|
|
112
|
+
emails: input.rules.emails ?? [],
|
|
113
|
+
orgIds: input.rules.orgIds ?? [],
|
|
114
|
+
percentage: input.rules.percentage ?? 0,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
: input.operation === "off"
|
|
118
|
+
? {
|
|
119
|
+
rules: {
|
|
120
|
+
mode: "off",
|
|
121
|
+
emails: [],
|
|
122
|
+
orgIds: [],
|
|
123
|
+
percentage: 0,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
: { enabledForEmail: admin.userEmail }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
94
130
|
export function validateWorkspaceFeatureFlagMutation(
|
|
95
131
|
body: unknown,
|
|
96
132
|
expected: {
|
|
@@ -179,6 +215,54 @@ function targetOrigin(app: OrgApp): string {
|
|
|
179
215
|
return new URL(app.url).origin;
|
|
180
216
|
}
|
|
181
217
|
|
|
218
|
+
function localAnalyticsApp(): OrgApp {
|
|
219
|
+
const configuredUrl = getAppConfig().app.url;
|
|
220
|
+
const url = configuredUrl ?? "http://analytics.local";
|
|
221
|
+
return {
|
|
222
|
+
id: ANALYTICS_APP_ID,
|
|
223
|
+
name: "Analytics",
|
|
224
|
+
url,
|
|
225
|
+
a2aUrl: url,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function localActionContext(
|
|
230
|
+
admin: AnalyticsAdminContext,
|
|
231
|
+
actionName: "list-feature-flags" | "set-feature-flag",
|
|
232
|
+
sourceContext?: ActionRunContext,
|
|
233
|
+
) {
|
|
234
|
+
return {
|
|
235
|
+
...sourceContext,
|
|
236
|
+
caller: sourceContext?.caller ?? ("http" as const),
|
|
237
|
+
userEmail: admin.userEmail,
|
|
238
|
+
orgId: admin.orgId,
|
|
239
|
+
appId: ANALYTICS_APP_ID,
|
|
240
|
+
actionName,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function listLocalAnalyticsFeatureFlags(admin: AnalyticsAdminContext) {
|
|
245
|
+
return classifyWorkspaceFeatureFlagList(localAnalyticsApp(), {
|
|
246
|
+
status: 200,
|
|
247
|
+
body: await listLocalFeatureFlagsAction.run(
|
|
248
|
+
{},
|
|
249
|
+
localActionContext(admin, "list-feature-flags"),
|
|
250
|
+
),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function unreachableLocalAnalyticsEntry(): FleetFlagApp {
|
|
255
|
+
const app = localAnalyticsApp();
|
|
256
|
+
return {
|
|
257
|
+
appId: app.id,
|
|
258
|
+
appName: app.name,
|
|
259
|
+
appOrigin: targetOrigin(app),
|
|
260
|
+
state: "unreachable",
|
|
261
|
+
flags: [],
|
|
262
|
+
reason: "target-execution",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
182
266
|
async function delegatedToken(
|
|
183
267
|
admin: AnalyticsAdminContext,
|
|
184
268
|
origin: string,
|
|
@@ -298,6 +382,18 @@ function targetFailure(error: unknown): WorkspaceFeatureFlagFailure {
|
|
|
298
382
|
return new WorkspaceFeatureFlagFailure(reason);
|
|
299
383
|
}
|
|
300
384
|
|
|
385
|
+
function localMutationFailure(error: unknown): WorkspaceFeatureFlagFailure {
|
|
386
|
+
const statusCode =
|
|
387
|
+
error && typeof error === "object" && "statusCode" in error
|
|
388
|
+
? (error as { statusCode?: unknown }).statusCode
|
|
389
|
+
: undefined;
|
|
390
|
+
return new WorkspaceFeatureFlagFailure(
|
|
391
|
+
statusCode === 401 || statusCode === 403
|
|
392
|
+
? "authorization"
|
|
393
|
+
: "target-action",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
301
397
|
async function resolveTargetApp(
|
|
302
398
|
admin: AnalyticsAdminContext,
|
|
303
399
|
appId: string,
|
|
@@ -471,13 +567,20 @@ async function mapBounded<T, R>(
|
|
|
471
567
|
export async function listWorkspaceFeatureFlags(
|
|
472
568
|
admin: AnalyticsAdminContext,
|
|
473
569
|
): Promise<WorkspaceFeatureFlagsResult> {
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
570
|
+
const [localEntry, directoryResult] = await Promise.all([
|
|
571
|
+
listLocalAnalyticsFeatureFlags(admin).catch(() =>
|
|
572
|
+
unreachableLocalAnalyticsEntry(),
|
|
573
|
+
),
|
|
574
|
+
fetchOrgAppsResult({
|
|
575
|
+
selfId: ANALYTICS_APP_ID,
|
|
576
|
+
includeDirectoryApp: true,
|
|
577
|
+
serviceOrgId: admin.orgId,
|
|
578
|
+
}),
|
|
579
|
+
]);
|
|
580
|
+
if (directoryResult.status === "unavailable")
|
|
581
|
+
return { directoryStatus: "unavailable", apps: [localEntry] };
|
|
582
|
+
const directoryApps = directoryResult.apps;
|
|
583
|
+
const entries = await mapBounded(directoryApps, async (app) => {
|
|
481
584
|
try {
|
|
482
585
|
return classifyWorkspaceFeatureFlagList(
|
|
483
586
|
app,
|
|
@@ -494,13 +597,101 @@ export async function listWorkspaceFeatureFlags(
|
|
|
494
597
|
};
|
|
495
598
|
}
|
|
496
599
|
});
|
|
497
|
-
return { directoryStatus: "available", apps: entries };
|
|
600
|
+
return { directoryStatus: "available", apps: [localEntry, ...entries] };
|
|
498
601
|
}
|
|
499
602
|
|
|
500
603
|
export async function setWorkspaceFeatureFlag(
|
|
501
604
|
admin: AnalyticsAdminContext,
|
|
502
605
|
input: WorkspaceFeatureFlagMutationInput,
|
|
606
|
+
sourceContext?: ActionRunContext,
|
|
503
607
|
): Promise<WorkspaceFeatureFlagMutationResult> {
|
|
608
|
+
if (input.appId === ANALYTICS_APP_ID) {
|
|
609
|
+
if (input.operation === "replace-rules" && !input.rules)
|
|
610
|
+
throw new WorkspaceFeatureFlagFailure("target-action");
|
|
611
|
+
const localRules = input.rules as
|
|
612
|
+
| {
|
|
613
|
+
mode: "off" | "on" | "rules";
|
|
614
|
+
emails?: string[];
|
|
615
|
+
orgIds?: string[];
|
|
616
|
+
percentage?: number;
|
|
617
|
+
}
|
|
618
|
+
| undefined;
|
|
619
|
+
const targetInput =
|
|
620
|
+
input.operation === "replace-rules"
|
|
621
|
+
? {
|
|
622
|
+
operation: input.operation,
|
|
623
|
+
key: input.key,
|
|
624
|
+
rules: localRules!,
|
|
625
|
+
}
|
|
626
|
+
: { operation: input.operation, key: input.key };
|
|
627
|
+
let result;
|
|
628
|
+
try {
|
|
629
|
+
result = await setLocalFeatureFlagAction.run(
|
|
630
|
+
targetInput,
|
|
631
|
+
localActionContext(admin, "set-feature-flag", sourceContext),
|
|
632
|
+
);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
throw localMutationFailure(error);
|
|
635
|
+
}
|
|
636
|
+
const orgDomain = result.scope.orgDomain ?? "";
|
|
637
|
+
let mutation: TargetFeatureFlagMutationResult;
|
|
638
|
+
try {
|
|
639
|
+
mutation = validateWorkspaceFeatureFlagMutation(
|
|
640
|
+
result,
|
|
641
|
+
mutationValidationExpectation(admin, input, orgDomain),
|
|
642
|
+
);
|
|
643
|
+
} catch {
|
|
644
|
+
throw new WorkspaceFeatureFlagFailure("persistence");
|
|
645
|
+
}
|
|
646
|
+
let verifiedApp: FleetFlagApp;
|
|
647
|
+
try {
|
|
648
|
+
verifiedApp = await listLocalAnalyticsFeatureFlags(admin);
|
|
649
|
+
} catch {
|
|
650
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
651
|
+
}
|
|
652
|
+
if (verifiedApp.state === "forbidden")
|
|
653
|
+
throw new WorkspaceFeatureFlagFailure("authorization");
|
|
654
|
+
const verifiedFlag = verifiedApp.flags.find(
|
|
655
|
+
(flag) => flag.key === input.key,
|
|
656
|
+
);
|
|
657
|
+
if (
|
|
658
|
+
verifiedApp.state !== "ready" ||
|
|
659
|
+
!verifiedFlag ||
|
|
660
|
+
typeof verifiedFlag.enabledForCurrentUser !== "boolean" ||
|
|
661
|
+
(input.operation === "enable-for-current-user" &&
|
|
662
|
+
!verifiedFlag.enabledForCurrentUser) ||
|
|
663
|
+
(input.operation === "off" && verifiedFlag.enabledForCurrentUser)
|
|
664
|
+
) {
|
|
665
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
validateWorkspaceFeatureFlagMutation(
|
|
669
|
+
{
|
|
670
|
+
contractVersion: 2,
|
|
671
|
+
status: "ready",
|
|
672
|
+
key: input.key,
|
|
673
|
+
rules: verifiedFlag.rules,
|
|
674
|
+
scope: mutation.scope,
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
key: input.key,
|
|
678
|
+
orgDomain,
|
|
679
|
+
allowExplicitNoOrgTarget: true,
|
|
680
|
+
rules: mutation.rules,
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
} catch {
|
|
684
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
contractVersion: 3,
|
|
688
|
+
status: "verified",
|
|
689
|
+
key: mutation.key,
|
|
690
|
+
rules: verifiedFlag.rules as Record<string, unknown>,
|
|
691
|
+
scope: mutation.scope,
|
|
692
|
+
enabledForCurrentUser: verifiedFlag.enabledForCurrentUser,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
504
695
|
const app = await resolveTargetApp(admin, input.appId);
|
|
505
696
|
const targetInput = workspaceFeatureFlagTargetInput(input);
|
|
506
697
|
let orgDomain: string | undefined;
|
|
@@ -533,30 +724,10 @@ export async function setWorkspaceFeatureFlag(
|
|
|
533
724
|
throw new WorkspaceFeatureFlagFailure("target-action");
|
|
534
725
|
let mutation: TargetFeatureFlagMutationResult;
|
|
535
726
|
try {
|
|
536
|
-
mutation = validateWorkspaceFeatureFlagMutation(
|
|
537
|
-
|
|
538
|
-
orgDomain,
|
|
539
|
-
|
|
540
|
-
...(input.operation === "replace-rules" && input.rules
|
|
541
|
-
? {
|
|
542
|
-
rules: {
|
|
543
|
-
mode: input.rules.mode,
|
|
544
|
-
emails: input.rules.emails ?? [],
|
|
545
|
-
orgIds: input.rules.orgIds ?? [],
|
|
546
|
-
percentage: input.rules.percentage ?? 0,
|
|
547
|
-
},
|
|
548
|
-
}
|
|
549
|
-
: input.operation === "off"
|
|
550
|
-
? {
|
|
551
|
-
rules: {
|
|
552
|
-
mode: "off",
|
|
553
|
-
emails: [],
|
|
554
|
-
orgIds: [],
|
|
555
|
-
percentage: 0,
|
|
556
|
-
},
|
|
557
|
-
}
|
|
558
|
-
: { enabledForEmail: admin.userEmail }),
|
|
559
|
-
});
|
|
727
|
+
mutation = validateWorkspaceFeatureFlagMutation(
|
|
728
|
+
result.body,
|
|
729
|
+
mutationValidationExpectation(admin, input, orgDomain),
|
|
730
|
+
);
|
|
560
731
|
} catch {
|
|
561
732
|
throw new WorkspaceFeatureFlagFailure("persistence");
|
|
562
733
|
}
|
|
@@ -632,6 +803,7 @@ export async function getWorkspaceFlagTarget(
|
|
|
632
803
|
admin: AnalyticsAdminContext,
|
|
633
804
|
appId: string,
|
|
634
805
|
): Promise<FleetFlagApp> {
|
|
806
|
+
if (appId === ANALYTICS_APP_ID) return listLocalAnalyticsFeatureFlags(admin);
|
|
635
807
|
const apps = await fetchOrgApps({
|
|
636
808
|
selfId: "analytics",
|
|
637
809
|
includeDirectoryApp: true,
|
|
@@ -1524,7 +1524,7 @@ export function EventDetailPopover({
|
|
|
1524
1524
|
side={isMobile ? "bottom" : (popoverSide ?? "right")}
|
|
1525
1525
|
sideOffset={isMobile ? 6 : 8}
|
|
1526
1526
|
collisionPadding={12}
|
|
1527
|
-
className="flex max-h-[var(--radix-popover-content-available-height)] w-[min(
|
|
1527
|
+
className="flex max-h-[var(--radix-popover-content-available-height)] w-[min(420px,calc(100vw-2rem))] flex-col overflow-hidden p-0"
|
|
1528
1528
|
onClick={(e) => e.stopPropagation()}
|
|
1529
1529
|
onOpenAutoFocus={(e) => {
|
|
1530
1530
|
e.preventDefault();
|
|
@@ -1553,8 +1553,8 @@ export function EventDetailPopover({
|
|
|
1553
1553
|
>
|
|
1554
1554
|
<TooltipProvider>
|
|
1555
1555
|
{/* Header */}
|
|
1556
|
-
<div className="flex items-center justify-between border-b border-border
|
|
1557
|
-
<div className="text-
|
|
1556
|
+
<div className="flex items-center justify-between border-b border-border px-4 py-2.5">
|
|
1557
|
+
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
1558
1558
|
<span>
|
|
1559
1559
|
{isWorkingLocation
|
|
1560
1560
|
? t("eventForm.workingLocation")
|
|
@@ -311,6 +311,92 @@ function cleanNode(
|
|
|
311
311
|
return out;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Elements whose unclosed start tag swallows the rest of the document in a real
|
|
316
|
+
* parser. `embed` is deliberately absent: it is void, so it never has a closing
|
|
317
|
+
* tag and requiring one would truncate every slide that contains a valid one.
|
|
318
|
+
*/
|
|
319
|
+
const SWALLOWING_ELEMENTS = /^(script|style|textarea|iframe|object|svg|math)$/i;
|
|
320
|
+
|
|
321
|
+
/** Elements whose children the HTML parser reads as text rather than markup. */
|
|
322
|
+
const RAW_TEXT_ELEMENTS = /^(script|style|textarea|title)$/i;
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Start-tag positions in `html`, skipping comments and anything inside a quoted
|
|
326
|
+
* attribute value.
|
|
327
|
+
*
|
|
328
|
+
* Scanning the serialized string with a bare regex cannot tell a tag from text:
|
|
329
|
+
* `<p title="Use <style> here">` reads as a `<style>` start tag, and truncating
|
|
330
|
+
* there drops the rest of a perfectly valid slide.
|
|
331
|
+
*/
|
|
332
|
+
function startTagPositions(html: string): { name: string; index: number }[] {
|
|
333
|
+
const found: { name: string; index: number }[] = [];
|
|
334
|
+
for (let i = 0; i < html.length; i++) {
|
|
335
|
+
if (html[i] !== "<") continue;
|
|
336
|
+
if (html.startsWith("<!--", i)) {
|
|
337
|
+
const end = html.indexOf("-->", i + 4);
|
|
338
|
+
i = end === -1 ? html.length : end + 2;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const name = /^<([a-z][a-z0-9-]*)/i.exec(html.slice(i, i + 32))?.[1];
|
|
342
|
+
// Walk to this tag's `>`, stepping over quoted values so a `<` or `>`
|
|
343
|
+
// inside one is not read as markup.
|
|
344
|
+
let cursor = i + 1;
|
|
345
|
+
let quote = "";
|
|
346
|
+
while (cursor < html.length) {
|
|
347
|
+
const char = html[cursor];
|
|
348
|
+
if (quote) {
|
|
349
|
+
if (char === quote) quote = "";
|
|
350
|
+
} else if (char === '"' || char === "'") {
|
|
351
|
+
quote = char;
|
|
352
|
+
} else if (char === ">") {
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
cursor++;
|
|
356
|
+
}
|
|
357
|
+
if (!name) {
|
|
358
|
+
i = cursor;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const lower = name.toLowerCase();
|
|
362
|
+
found.push({ name: lower, index: i });
|
|
363
|
+
if (RAW_TEXT_ELEMENTS.test(lower)) {
|
|
364
|
+
// A raw-text element's body is text, not markup — `content: "<script>"`
|
|
365
|
+
// inside a stylesheet is a CSS string, and reading it as a start tag
|
|
366
|
+
// truncated everything after it. Skip to the close tag; no close tag is
|
|
367
|
+
// the unclosed case the caller is looking for, and everything past it is
|
|
368
|
+
// swallowed anyway, so there is nothing further to find.
|
|
369
|
+
const closing = new RegExp(`</\\s*${lower}\\s*>`, "i").exec(
|
|
370
|
+
html.slice(cursor),
|
|
371
|
+
);
|
|
372
|
+
if (!closing) return found;
|
|
373
|
+
i = cursor + closing.index + closing[0].length - 1;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
i = cursor;
|
|
377
|
+
}
|
|
378
|
+
return found;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Truncates at the first swallowing element that never closes.
|
|
383
|
+
*
|
|
384
|
+
* The regex path has to drop the remainder to agree with `cleanNode`. It must
|
|
385
|
+
* check for the closing tag to do that: the sweep used to match any of these
|
|
386
|
+
* tags and cut to the end of the string unconditionally, which ate the
|
|
387
|
+
* sanitized `<style>` block the pass above it had just emitted — and every
|
|
388
|
+
* heading and paragraph after it. A deck with one stylesheet rendered as an
|
|
389
|
+
* empty slide on the SSR'd share and present pages.
|
|
390
|
+
*/
|
|
391
|
+
function dropFromFirstUnclosedRawText(html: string): string {
|
|
392
|
+
for (const { name, index } of startTagPositions(html)) {
|
|
393
|
+
if (!SWALLOWING_ELEMENTS.test(name)) continue;
|
|
394
|
+
const closing = new RegExp(`</\\s*${name}\\s*>`, "i");
|
|
395
|
+
if (!closing.test(html.slice(index))) return html.slice(0, index);
|
|
396
|
+
}
|
|
397
|
+
return html;
|
|
398
|
+
}
|
|
399
|
+
|
|
314
400
|
function sanitizeHtmlString(
|
|
315
401
|
html: string,
|
|
316
402
|
scopeSelector?: string,
|
|
@@ -335,10 +421,7 @@ function sanitizeHtmlString(
|
|
|
335
421
|
// twin runs instead of cleanNode(). An unclosed raw-text or embedding
|
|
336
422
|
// element swallows the rest of the document in a real parser, so dropping
|
|
337
423
|
// the remainder is what keeps this path agreeing with the DOM path.
|
|
338
|
-
.replace(
|
|
339
|
-
/<(script|style|textarea|iframe|object|embed|svg|math)\b[\s\S]*$/i,
|
|
340
|
-
"",
|
|
341
|
-
)
|
|
424
|
+
.replace(/[\s\S]*/, dropFromFirstUnclosedRawText)
|
|
342
425
|
.replace(
|
|
343
426
|
/<(script|iframe|object|embed|form|input|button|select|textarea|meta|base|link|svg|math)\b[^>]*\/?>/gi,
|
|
344
427
|
"",
|
package/dist/action.d.ts
CHANGED
|
@@ -19,8 +19,10 @@ import type { ActionAuditConfig } from "./audit/types.js";
|
|
|
19
19
|
* selected actions are attributed as `"tool"`.
|
|
20
20
|
* - `"automation"` — an event-triggered automation dispatched from a stored
|
|
21
21
|
* workspace trigger with trusted trigger lineage.
|
|
22
|
+
* - `"webmcp"` — a browser agent invoking an automatically projected action
|
|
23
|
+
* through the authenticated page-local WebMCP bridge.
|
|
22
24
|
*/
|
|
23
|
-
export type ActionCaller = "tool" | "http" | "frontend" | "cli" | "mcp" | "a2a" | "automation";
|
|
25
|
+
export type ActionCaller = "tool" | "http" | "frontend" | "cli" | "mcp" | "webmcp" | "a2a" | "automation";
|
|
24
26
|
/**
|
|
25
27
|
* Trusted automation lineage added by the trigger dispatcher. Action inputs
|
|
26
28
|
* must never be used to create or override this context.
|
|
@@ -66,6 +66,7 @@ import { AgentNativeRouteWarmup } from "./route-warmup.js";
|
|
|
66
66
|
import { RouteTransitionIndicator } from "./RouteTransitionIndicator.js";
|
|
67
67
|
import { RuntimeConfigNotice } from "./RuntimeConfigNotice.js";
|
|
68
68
|
import { EMBEDDED_THEME_CHANGE_EVENT, applyEmbeddedThemeUpdate, parseEmbeddedThemeUpdate, } from "./theme.js";
|
|
69
|
+
import { createAgentNativeServerActionWebMcpRegistration } from "./webmcp.js";
|
|
69
70
|
const DEFAULT_TOASTER = (_jsx(Toaster, { richColors: true, position: "bottom-left", offset: { bottom: 44, left: 32 }, mobileOffset: { bottom: 44, left: 16 } }));
|
|
70
71
|
function RoutedAppEnhancements() {
|
|
71
72
|
const isInRouter = useInRouterContext();
|
|
@@ -73,6 +74,17 @@ function RoutedAppEnhancements() {
|
|
|
73
74
|
return null;
|
|
74
75
|
return (_jsxs(_Fragment, { children: [_jsx(AgentNativeRouteWarmup, {}), _jsx(RouteTransitionIndicator, {})] }));
|
|
75
76
|
}
|
|
77
|
+
function AutomaticWebMcpActionRegistration() {
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const registration = createAgentNativeServerActionWebMcpRegistration();
|
|
80
|
+
void registration.start().catch(() => {
|
|
81
|
+
// WebMCP is progressive enhancement. Session expiry or a transient
|
|
82
|
+
// manifest failure must not prevent the authenticated app from loading.
|
|
83
|
+
});
|
|
84
|
+
return () => registration.stop();
|
|
85
|
+
}, []);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
76
88
|
function readDocumentTitleFallback() {
|
|
77
89
|
const selectors = [
|
|
78
90
|
'meta[name="application-name"]',
|
|
@@ -153,5 +165,5 @@ export function AppProviders({ queryClient, isPublicPath = false, clientOnlyFall
|
|
|
153
165
|
if (isPublicPath) {
|
|
154
166
|
return (_jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, showProductionEnvironmentBadge: false, children: children }));
|
|
155
167
|
}
|
|
156
|
-
return (_jsx(ClientOnly, { fallback: fallback, children: _jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, showProductionEnvironmentBadge: !sessionBypass, children: _jsx(RequireSession, { bypass: sessionBypass, fallback: fallback, children: sessionBypass ? (children) : (
|
|
168
|
+
return (_jsx(ClientOnly, { fallback: fallback, children: _jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, showProductionEnvironmentBadge: !sessionBypass, children: _jsx(RequireSession, { bypass: sessionBypass, fallback: fallback, children: sessionBypass ? (children) : (_jsxs(FirstRunOnboardingStartupGate, { children: [_jsx(AutomaticWebMcpActionRegistration, {}), children] })) }) }) }));
|
|
157
169
|
}
|
package/dist/client/webmcp.d.ts
CHANGED
|
@@ -71,4 +71,8 @@ export interface AgentNativeWebMcpRegistration {
|
|
|
71
71
|
start(): Promise<void>;
|
|
72
72
|
stop(): void;
|
|
73
73
|
}
|
|
74
|
+
export declare function createAgentNativeServerActionWebMcpRegistration(options?: {
|
|
75
|
+
document?: Document;
|
|
76
|
+
fetch?: typeof fetch;
|
|
77
|
+
}): AgentNativeWebMcpRegistration;
|
|
74
78
|
export declare function createAgentNativeWebMcpRegistration(options: AgentNativeWebMcpRegistrationOptions): AgentNativeWebMcpRegistration;
|
package/dist/client/webmcp.js
CHANGED
|
@@ -234,6 +234,52 @@ export function createAgentNativeWebMcpClient(options = {}) {
|
|
|
234
234
|
};
|
|
235
235
|
return client;
|
|
236
236
|
}
|
|
237
|
+
export function createAgentNativeServerActionWebMcpRegistration(options) {
|
|
238
|
+
const fetchImpl = options?.fetch ?? ((...args) => fetch(...args));
|
|
239
|
+
return createAgentNativeWebMcpRegistration({
|
|
240
|
+
document: options?.document,
|
|
241
|
+
maxToolCount: 1_000,
|
|
242
|
+
maxDescriptionChars: 10_000,
|
|
243
|
+
actions: async () => {
|
|
244
|
+
const response = await fetchImpl("/_agent-native/webmcp/manifest", {
|
|
245
|
+
credentials: "same-origin",
|
|
246
|
+
headers: { Accept: "application/json" },
|
|
247
|
+
});
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
throw new Error(`Unable to load WebMCP actions (${response.status})`);
|
|
250
|
+
}
|
|
251
|
+
const manifest = (await response.json());
|
|
252
|
+
if (!Array.isArray(manifest)) {
|
|
253
|
+
throw new Error("WebMCP action manifest must be an array");
|
|
254
|
+
}
|
|
255
|
+
return manifest.map((action) => ({
|
|
256
|
+
name: action.name,
|
|
257
|
+
description: action.description,
|
|
258
|
+
...(action.inputSchema ? { schema: action.inputSchema } : {}),
|
|
259
|
+
...(action.readOnly ? { readOnly: true } : {}),
|
|
260
|
+
run: async (args, runtime) => {
|
|
261
|
+
const result = await fetchImpl(`/_agent-native/webmcp/actions/${encodeURIComponent(action.name)}`, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
credentials: "same-origin",
|
|
264
|
+
headers: {
|
|
265
|
+
Accept: "application/json",
|
|
266
|
+
"Content-Type": "application/json",
|
|
267
|
+
},
|
|
268
|
+
body: JSON.stringify(args),
|
|
269
|
+
...(runtime.signal ? { signal: runtime.signal } : {}),
|
|
270
|
+
});
|
|
271
|
+
const body = await result.json();
|
|
272
|
+
if (!result.ok) {
|
|
273
|
+
throw new Error(isRecord(body) && typeof body.error === "string"
|
|
274
|
+
? body.error
|
|
275
|
+
: `WebMCP action failed (${result.status})`);
|
|
276
|
+
}
|
|
277
|
+
return body;
|
|
278
|
+
},
|
|
279
|
+
}));
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}
|
|
237
283
|
function resolveActions(actions) {
|
|
238
284
|
return Promise.resolve(typeof actions === "function" ? actions() : actions).then((value) => {
|
|
239
285
|
if (!Array.isArray(value)) {
|
|
@@ -271,7 +317,7 @@ function actionManifest(action) {
|
|
|
271
317
|
function actionInputSchema(action) {
|
|
272
318
|
return action.schema ?? action.parameters ?? EMPTY_INPUT_SCHEMA;
|
|
273
319
|
}
|
|
274
|
-
function actionRuntime(options, context, session) {
|
|
320
|
+
function actionRuntime(options, context, session, signal) {
|
|
275
321
|
const origin = options.origin ?? "agent-native-webmcp";
|
|
276
322
|
const runCommand = async (command, payload) => {
|
|
277
323
|
const handler = options.commands?.[command] ??
|
|
@@ -281,6 +327,7 @@ function actionRuntime(options, context, session) {
|
|
|
281
327
|
return handler({ command, payload, origin }, undefined);
|
|
282
328
|
};
|
|
283
329
|
return {
|
|
330
|
+
...(signal ? { signal } : {}),
|
|
284
331
|
origin,
|
|
285
332
|
context,
|
|
286
333
|
session,
|
|
@@ -381,7 +428,7 @@ export function createAgentNativeWebMcpRegistration(options) {
|
|
|
381
428
|
: {}),
|
|
382
429
|
},
|
|
383
430
|
execute: async (input, executionOptions) => {
|
|
384
|
-
if (executionOptions
|
|
431
|
+
if (executionOptions?.signal.aborted) {
|
|
385
432
|
throw new Error(`WebMCP action "${action.name}" was aborted`);
|
|
386
433
|
}
|
|
387
434
|
validateBoundedJson(input, `WebMCP action "${action.name}" input`, options.maxInputChars ?? DEFAULT_INPUT_CHARS);
|
|
@@ -417,8 +464,8 @@ export function createAgentNativeWebMcpRegistration(options) {
|
|
|
417
464
|
throw new Error(`WebMCP action "${action.name}" was not approved`);
|
|
418
465
|
}
|
|
419
466
|
}
|
|
420
|
-
const result = await action.run(input, actionRuntime(options, context, session));
|
|
421
|
-
if (executionOptions
|
|
467
|
+
const result = await action.run(input, actionRuntime(options, context, session, executionOptions?.signal));
|
|
468
|
+
if (executionOptions?.signal.aborted) {
|
|
422
469
|
throw new Error(`WebMCP action "${action.name}" was aborted`);
|
|
423
470
|
}
|
|
424
471
|
return serializeWebMcpResult(result, `WebMCP action "${action.name}" result`, options.maxResultChars ?? DEFAULT_RESULT_CHARS);
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
error: string;
|
|
30
29
|
ok?: undefined;
|
|
30
|
+
error: string;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
package/dist/index.browser.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export * from "./config.js";
|
|
|
2
2
|
export { addContextToAgentChat, appendAgentChatContextToMessage, clearAgentChatContext, formatAgentChatContextItemsForPrompt, listAgentChatContext, refreshAgentChatContext, removeAgentChatContextItem, sendToAgentChat, sendToAgentChatAndConfirm, setAgentChatContextItem, setContextToAgentChat, isEmbedMcpChatBridgeActive, useAgentChatContext, useAgentChatGenerating, useDevMode, useSendToAgentChat, AGENT_NATIVE_MCP_APP_HOST_MESSAGE_TYPES, getMcpAppHostContext, openMcpAppHostLink, requestMcpAppDisplayMode, sendMcpAppHostMessage, updateMcpAppModelContext, useMcpAppHostContext, CodeRequiredDialog, AgentNativeI18nProvider, LanguagePicker, getLocaleInitScript, localeDirection, normalizeLocaleCode, normalizeLocalePreference, normalizeLocalizationPreference, resolveLocaleFromCandidates, resolveLocaleFromPreference, useFormatters, useLocale, useT, DEFAULT_LOCALE, LOCALE_HYDRATION_GLOBAL, LOCALE_METADATA, LOCALE_STORAGE_KEY, SUPPORTED_LOCALES, useDbSync, useFileWatcher, useSession, cn, ApiKeySettings, type AgentChatContextItem, type AgentChatContextMessage, type AgentChatContextMutationOptions, type AgentChatContextRemoveOptions, type AgentChatContextSetOptions, type AgentChatContextState, type AgentChatMessage, type AgentNativeMcpAppHostMessageType, type McpAppDisplayMode, type McpAppHostChatMessage, type McpAppHostCapabilities, type McpAppHostContext, type McpAppHostContextSnapshot, type McpAppModelContextContentPart, type McpAppModelContextUpdate, type CodeRequiredDialogProps, type AuthSession, type UseAgentChatContextResult, type AgentNativeI18nCatalog, type AgentNativeI18nProviderProps, type LocaleCode, type LocaleHydrationPayload, type LocaleMessages, type LocaleMetadata, type LocalePreference, type LocalizationPreference, } from "./client/index.js";
|
|
3
3
|
export { agentChat } from "./shared/index.js";
|
|
4
4
|
export { parseArgs, camelCaseArgs } from "./scripts/parse-args.js";
|
|
5
|
-
export { AgentNativeWebMcpUnsupportedError, createAgentNativeWebMcpClient, createAgentNativeWebMcpRegistration, isAgentNativeWebMcpSupported, type AgentNativeWebMcpApprovalRequest, type AgentNativeWebMcpClient, type AgentNativeWebMcpClientOptions, type AgentNativeWebMcpRegistration, type AgentNativeWebMcpRegistrationOptions, type AgentNativeWebMcpTool, type AgentNativeWebMcpToolAnnotations, type AgentNativeWebMcpToolExecutionOptions, type AgentNativeWebMcpToolResult, } from "./client/webmcp.js";
|
|
5
|
+
export { AgentNativeWebMcpUnsupportedError, createAgentNativeWebMcpClient, createAgentNativeWebMcpRegistration, createAgentNativeServerActionWebMcpRegistration, isAgentNativeWebMcpSupported, type AgentNativeWebMcpApprovalRequest, type AgentNativeWebMcpClient, type AgentNativeWebMcpClientOptions, type AgentNativeWebMcpRegistration, type AgentNativeWebMcpRegistrationOptions, type AgentNativeWebMcpTool, type AgentNativeWebMcpToolAnnotations, type AgentNativeWebMcpToolExecutionOptions, type AgentNativeWebMcpToolResult, } from "./client/webmcp.js";
|
|
6
6
|
export { defineAction, fail, type FailOptions, ActionContractError, isActionContractError, type ActionContractErrorOptions, AgentActionStopError, isAgentActionStopError, type ActionHttpConfig, type AgentActionStopOptions, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
|
|
7
7
|
export { isEmailDerivedName, normalizeUserProfileName, resolveUserProfileName, USER_PROFILE_SETTING_KEY, type UserProfile, } from "./user-profile/index.js";
|
|
8
8
|
export { ACTION_CHAT_UI_DATA_CHART_RENDERER, ACTION_CHAT_UI_DATA_INSIGHTS_RENDERER, ACTION_CHAT_UI_DATA_TABLE_RENDERER, ACTION_CHAT_UI_DATA_WIDGET_RENDERER, ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER, ACTION_CHAT_UI_WORKSPACE_FILE_RENDERER, type ActionChatUIConfig, } from "./action-ui.js";
|
package/dist/index.browser.js
CHANGED
|
@@ -8,7 +8,7 @@ export { addContextToAgentChat, appendAgentChatContextToMessage, clearAgentChatC
|
|
|
8
8
|
export { agentChat } from "./shared/index.js";
|
|
9
9
|
// Pure utilities (no Node.js deps — safe for browser and SSR)
|
|
10
10
|
export { parseArgs, camelCaseArgs } from "./scripts/parse-args.js";
|
|
11
|
-
export { AgentNativeWebMcpUnsupportedError, createAgentNativeWebMcpClient, createAgentNativeWebMcpRegistration, isAgentNativeWebMcpSupported, } from "./client/webmcp.js";
|
|
11
|
+
export { AgentNativeWebMcpUnsupportedError, createAgentNativeWebMcpClient, createAgentNativeWebMcpRegistration, createAgentNativeServerActionWebMcpRegistration, isAgentNativeWebMcpSupported, } from "./client/webmcp.js";
|
|
12
12
|
// defineAction — used by template actions, no Node.js deps
|
|
13
13
|
export { defineAction, fail, ActionContractError, isActionContractError, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
|
|
14
14
|
export { isEmailDerivedName, normalizeUserProfileName, resolveUserProfileName, USER_PROFILE_SETTING_KEY, } from "./user-profile/index.js";
|
|
@@ -51,8 +51,8 @@ export declare function handleDeleteResource(event: any): Promise<{
|
|
|
51
51
|
error: string;
|
|
52
52
|
ok?: undefined;
|
|
53
53
|
} | {
|
|
54
|
-
ok: boolean;
|
|
55
54
|
error?: undefined;
|
|
55
|
+
ok: boolean;
|
|
56
56
|
}>;
|
|
57
57
|
/** POST /_agent-native/resources/upload — upload a file as a resource */
|
|
58
58
|
export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
|
|
@@ -73,10 +73,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
|
|
|
73
73
|
runId: string | null;
|
|
74
74
|
expiresAt: number | null;
|
|
75
75
|
metadata: string | null;
|
|
76
|
+
error?: undefined;
|
|
76
77
|
url: string;
|
|
77
78
|
provider: string;
|
|
78
79
|
storageSetupRequired?: undefined;
|
|
79
|
-
error?: undefined;
|
|
80
80
|
} | {
|
|
81
81
|
error: string;
|
|
82
82
|
storageSetupRequired: boolean;
|
|
@@ -60,10 +60,5 @@ export interface MountActionRoutesOptions {
|
|
|
60
60
|
*/
|
|
61
61
|
actionRouteAuth?: ActionRouteAuthAdapter;
|
|
62
62
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Mount discovered actions as HTTP endpoints.
|
|
65
|
-
*
|
|
66
|
-
* Only actions from `autoDiscoverActions` (template actions) are mounted.
|
|
67
|
-
* Built-in actions (resource-*, chat-*, shell, etc.) are NOT passed here.
|
|
68
|
-
*/
|
|
69
63
|
export declare function mountActionRoutes(nitroApp: any, actions: Record<string, ActionEntry>, options?: MountActionRoutesOptions): void;
|
|
64
|
+
export declare function mountWebMcpActionRoutes(nitroApp: any, actions: Record<string, ActionEntry>, options?: MountActionRoutesOptions): void;
|
|
@@ -240,21 +240,23 @@ async function resolveRequestAuthCapability(event) {
|
|
|
240
240
|
* Only actions from `autoDiscoverActions` (template actions) are mounted.
|
|
241
241
|
* Built-in actions (resource-*, chat-*, shell, etc.) are NOT passed here.
|
|
242
242
|
*/
|
|
243
|
-
|
|
243
|
+
function mountActionRoutesInternal(nitroApp, actions, options) {
|
|
244
244
|
const mounted = [];
|
|
245
245
|
const app = getH3App(nitroApp);
|
|
246
246
|
for (const [name, entry] of Object.entries(actions)) {
|
|
247
247
|
// Skip agent-only actions
|
|
248
|
-
if (entry.http === false)
|
|
248
|
+
if (entry.http === false && !options?.includeAgentOnly)
|
|
249
249
|
continue;
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
const
|
|
250
|
+
const http = entry.http || undefined;
|
|
251
|
+
const method = options?.forcePost ? "POST" : (http?.method ?? "POST");
|
|
252
|
+
const path = options?.forcePost ? name : (http?.path ?? name);
|
|
253
|
+
const routePath = `${options?.routePrefix ?? ROUTE_PREFIX}/${path}`;
|
|
253
254
|
// These two actions authenticate with a scoped A2A bearer rather than a
|
|
254
255
|
// browser session. Let that verifier see the request before the cookie
|
|
255
256
|
// auth guard rejects it; the action route still fails closed on invalid
|
|
256
257
|
// or missing credentials.
|
|
257
|
-
if (
|
|
258
|
+
if (!options?.caller &&
|
|
259
|
+
(name === "list-feature-flags" || name === "set-feature-flag")) {
|
|
258
260
|
registerAuthPublicPaths([routePath], app);
|
|
259
261
|
}
|
|
260
262
|
app.use(routePath, defineEventHandler(async (event) => {
|
|
@@ -329,7 +331,7 @@ export function mountActionRoutes(nitroApp, actions, options) {
|
|
|
329
331
|
// through, so a live same-origin session cookie can't silently execute
|
|
330
332
|
// the request as the logged-in user.
|
|
331
333
|
let resolvedCaller = null;
|
|
332
|
-
{
|
|
334
|
+
if (options?.allowDelegatedCaller !== false) {
|
|
333
335
|
let caller;
|
|
334
336
|
try {
|
|
335
337
|
caller = options?.actionRouteAuth?.resolveCaller
|
|
@@ -468,11 +470,12 @@ export function mountActionRoutes(nitroApp, actions, options) {
|
|
|
468
470
|
// userEmail / orgId mirror the request context resolved above (do
|
|
469
471
|
// NOT inject a dev identity — leave undefined when unauthenticated).
|
|
470
472
|
try {
|
|
471
|
-
const caller =
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
const caller = options?.caller ??
|
|
474
|
+
(resolvedCaller
|
|
475
|
+
? "a2a"
|
|
476
|
+
: isFrontendActionRequest(event)
|
|
477
|
+
? "frontend"
|
|
478
|
+
: "http");
|
|
476
479
|
const result = await entry.run(params, {
|
|
477
480
|
userEmail,
|
|
478
481
|
orgId: orgId ?? null,
|
|
@@ -574,11 +577,12 @@ export function mountActionRoutes(nitroApp, actions, options) {
|
|
|
574
577
|
method: reqMethod,
|
|
575
578
|
tags: {
|
|
576
579
|
action: name,
|
|
577
|
-
caller:
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
580
|
+
caller: options?.caller ??
|
|
581
|
+
(resolvedCaller
|
|
582
|
+
? "a2a"
|
|
583
|
+
: isFrontendActionRequest(event)
|
|
584
|
+
? "frontend"
|
|
585
|
+
: "http"),
|
|
582
586
|
status_code: String(status),
|
|
583
587
|
},
|
|
584
588
|
...(requestId ? { extra: { request_id: requestId } } : {}),
|
|
@@ -598,3 +602,40 @@ export function mountActionRoutes(nitroApp, actions, options) {
|
|
|
598
602
|
if (mounted.length > 0 && process.env.DEBUG)
|
|
599
603
|
console.log(`[action-routes] Mounted ${mounted.length} action route(s): ${mounted.join(", ")}`);
|
|
600
604
|
}
|
|
605
|
+
export function mountActionRoutes(nitroApp, actions, options) {
|
|
606
|
+
mountActionRoutesInternal(nitroApp, actions, options);
|
|
607
|
+
}
|
|
608
|
+
export function mountWebMcpActionRoutes(nitroApp, actions, options) {
|
|
609
|
+
const eligible = Object.fromEntries(Object.entries(actions).filter(([name, entry]) => /^[A-Za-z0-9_.-]{1,128}$/.test(name) &&
|
|
610
|
+
entry.agentTool !== false &&
|
|
611
|
+
entry.needsApproval === undefined));
|
|
612
|
+
if (Object.keys(eligible).length === 0)
|
|
613
|
+
return;
|
|
614
|
+
const app = getH3App(nitroApp);
|
|
615
|
+
app.use("/_agent-native/webmcp/manifest", defineEventHandler(async (event) => {
|
|
616
|
+
if (getMethod(event) !== "GET") {
|
|
617
|
+
setResponseStatus(event, 405);
|
|
618
|
+
return { error: "Method not allowed. Use GET." };
|
|
619
|
+
}
|
|
620
|
+
if (!options?.getOwnerFromEvent) {
|
|
621
|
+
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
622
|
+
}
|
|
623
|
+
await options.getOwnerFromEvent(event);
|
|
624
|
+
setResponseHeader(event, "Cache-Control", "no-store");
|
|
625
|
+
return Object.entries(eligible).map(([name, entry]) => ({
|
|
626
|
+
name,
|
|
627
|
+
description: entry.tool.description,
|
|
628
|
+
inputSchema: entry.tool.parameters,
|
|
629
|
+
readOnly: entry.readOnly === true,
|
|
630
|
+
}));
|
|
631
|
+
}));
|
|
632
|
+
mountActionRoutesInternal(nitroApp, eligible, {
|
|
633
|
+
...options,
|
|
634
|
+
routePrefix: "/_agent-native/webmcp/actions",
|
|
635
|
+
includeAgentOnly: true,
|
|
636
|
+
forcePost: true,
|
|
637
|
+
caller: "webmcp",
|
|
638
|
+
actionRouteAuth: undefined,
|
|
639
|
+
allowDelegatedCaller: false,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
@@ -2128,7 +2128,7 @@ export function createAgentChatPlugin(options) {
|
|
|
2128
2128
|
// Ignore — templates without sharing still work.
|
|
2129
2129
|
}
|
|
2130
2130
|
if (Object.keys(httpActions).length > 0) {
|
|
2131
|
-
const { mountActionRoutes } = await import("./action-routes.js");
|
|
2131
|
+
const { mountActionRoutes, mountWebMcpActionRoutes } = await import("./action-routes.js");
|
|
2132
2132
|
if (options?.actionRoutePublicPaths?.length) {
|
|
2133
2133
|
registerAuthPublicPaths(options.actionRoutePublicPaths, getH3App(nitroApp));
|
|
2134
2134
|
}
|
|
@@ -2139,6 +2139,13 @@ export function createAgentChatPlugin(options) {
|
|
|
2139
2139
|
resolveOrgId: options?.resolveOrgId,
|
|
2140
2140
|
actionRouteAuth: options?.actionRouteAuth,
|
|
2141
2141
|
});
|
|
2142
|
+
mountWebMcpActionRoutes(nitroApp, httpActions, {
|
|
2143
|
+
getOwnerFromEvent,
|
|
2144
|
+
getUserNameFromEvent,
|
|
2145
|
+
appId: options?.appId,
|
|
2146
|
+
resolveOrgId: options?.resolveOrgId,
|
|
2147
|
+
actionRouteAuth: options?.actionRouteAuth,
|
|
2148
|
+
});
|
|
2142
2149
|
}
|
|
2143
2150
|
const preRunGitStatusByThread = new Map();
|
|
2144
2151
|
async function recordPreRunGitStatus(threadId) {
|
package/docs/content/webmcp.mdx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "WebMCP"
|
|
3
|
-
description: "
|
|
3
|
+
description: "Automatically expose Agent-Native actions to browser agents, register explicit client actions, and consume page-local WebMCP tools from Agent-Native chat."
|
|
4
4
|
search: "WebMCP browser tools modelContext registerTool getTools executeTool browser session"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -15,24 +15,42 @@ discover them, and the browser mediates execution.
|
|
|
15
15
|
|
|
16
16
|
The framework supports both directions:
|
|
17
17
|
|
|
18
|
-
<!-- i18n-copy-ignore: oxfmt-only table alignment; no translated prose changed. -->
|
|
19
|
-
|
|
20
18
|
| You want to... | Use |
|
|
21
19
|
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
22
|
-
| Make
|
|
20
|
+
| Make backend actions available to a browser agent | Automatic on authenticated pages through `AppProviders` |
|
|
21
|
+
| Make page-local client actions available to a browser agent | `createAgentNativeWebMcpRegistration` |
|
|
23
22
|
| Let Agent-Native chat discover and call WebMCP tools in the current page | `createAgentNativeWebMcpClient` plus `webmcp` on `AgentNativeEmbedded` or `AgentNativeFrame` |
|
|
24
23
|
| Connect a headless or remote agent to an app | [MCP Server](/docs/mcp-protocol), [External Agents](/docs/external-agents), or [A2A](/docs/a2a-protocol) |
|
|
25
24
|
|
|
26
25
|
WebMCP is not a network MCP server and does not make a closed app globally
|
|
27
26
|
discoverable. A consuming agent needs an open, connected browser page and the
|
|
28
|
-
page must
|
|
27
|
+
page must expose tools to it.
|
|
29
28
|
|
|
30
29
|
The browser API is experimental and may change. See the [Chrome WebMCP
|
|
31
30
|
Imperative API](https://developer.chrome.com/docs/ai/webmcp/imperative-api) and
|
|
32
31
|
the [WebMCP specification](https://webmachinelearning.github.io/webmcp/) for
|
|
33
32
|
the current browser contract.
|
|
34
33
|
|
|
35
|
-
##
|
|
34
|
+
## Automatic backend actions
|
|
35
|
+
|
|
36
|
+
On an authenticated app page, `AppProviders` automatically registers eligible
|
|
37
|
+
backend actions with `document.modelContext`. The tools are derived from the
|
|
38
|
+
same action registry used by the in-app agent and execute through the same
|
|
39
|
+
validated, authenticated server dispatcher. This includes agent-only actions
|
|
40
|
+
declared with `http: false`; WebMCP does not turn those actions into public HTTP
|
|
41
|
+
APIs.
|
|
42
|
+
|
|
43
|
+
An action is eligible when `agentTool` is not `false` and `needsApproval` is
|
|
44
|
+
not declared. The latter is a fail-closed boundary: the server action approval
|
|
45
|
+
contract belongs to the agent loop and cannot be delegated to a browser hint.
|
|
46
|
+
Public paths, signed-out pages, and `sessionBypass` surfaces do not register
|
|
47
|
+
backend action tools. Browsers without `document.modelContext` simply keep the
|
|
48
|
+
normal app behavior.
|
|
49
|
+
|
|
50
|
+
No per-app WebMCP list or new `defineAction` metadata is required. Change the
|
|
51
|
+
existing action definition when its exposure should change.
|
|
52
|
+
|
|
53
|
+
## Explicit page-local actions
|
|
36
54
|
|
|
37
55
|
Register only the actions that should be available to a page agent. The
|
|
38
56
|
framework removes the action's local `run` function from the descriptor and
|
|
@@ -181,10 +199,10 @@ remote or headless tool calls, and use A2A when another app's agent should
|
|
|
181
199
|
receive work and reason over it. WebMCP is an opt-in browser bridge, not a
|
|
182
200
|
replacement for either network protocol.
|
|
183
201
|
|
|
184
|
-
The framework
|
|
185
|
-
|
|
186
|
-
retain normal MCP or A2A exposure for
|
|
187
|
-
open browser page.
|
|
202
|
+
The framework automatically projects eligible backend actions and still lets a
|
|
203
|
+
page register explicit client-only capabilities. Keep server-side approval
|
|
204
|
+
actions on the agent loop, and retain normal MCP or A2A exposure for
|
|
205
|
+
capabilities that must work without an open browser page.
|
|
188
206
|
|
|
189
207
|
## Compatibility and safety
|
|
190
208
|
|
|
@@ -195,7 +213,8 @@ open browser page.
|
|
|
195
213
|
- Treat descriptions, schemas, annotations, and results as page-provided
|
|
196
214
|
data. The framework validates and bounds them, but agents should still treat
|
|
197
215
|
`untrustedContentHint` results as untrusted content.
|
|
198
|
-
-
|
|
199
|
-
remain
|
|
216
|
+
- Automatic registrations are aborted when the private app shell unmounts. Use
|
|
217
|
+
`stop()` for explicit page-local registrations so stale tools do not remain
|
|
218
|
+
available.
|
|
200
219
|
- Keep server MCP and A2A endpoints for remote access. WebMCP only reaches the
|
|
201
220
|
tools registered in a connected browser context.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.177.0-nightly-20260828143953",
|
|
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": {
|