@agent-native/core 0.154.0 → 0.154.1
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/content/actions/resolve-content-landing.ts +162 -32
- package/corpus/templates/content/shared/content-landing.ts +5 -0
- package/corpus/templates/slides/actions/patch-deck.ts +12 -3
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +73 -1
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +6 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -0
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +15 -0
- package/corpus/templates/slides/app/context/DeckContext.tsx +19 -31
- package/corpus/templates/slides/app/hooks/use-navigation-state.ts +15 -16
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +9 -0
- package/corpus/templates/slides/changelog/2026-08-13-clicking-a-slide-thumbnail-now-focuses-it-so-the-slide-shortc.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-duplicating-or-restoring-a-slide-now-keeps-its-transition-ani.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-opening-presentation-mode-from-the-agent-now-starts-on-the-re.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-slide-transitions-can-be-set-from-a-visible-control-in-the-sl.md +6 -0
- package/dist/a2a/audience.d.ts +2 -0
- package/dist/a2a/audience.js +23 -0
- package/dist/a2a/client.d.ts +4 -0
- package/dist/a2a/client.js +32 -14
- package/dist/a2a/server.d.ts +9 -4
- package/dist/a2a/server.js +52 -25
- package/dist/a2a/types.d.ts +4 -4
- package/dist/observability/routes.d.ts +6 -6
- package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-capabilities.js +0 -0
- package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -13
- package/dist/server/agent-chat/action-filters-a2a.js +59 -23
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { defineAction } from "@agent-native/core";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
compareAndSetAppState,
|
|
6
|
+
listAppState,
|
|
7
|
+
readAppState,
|
|
8
|
+
} from "@agent-native/core/application-state";
|
|
5
9
|
import { runWithRequestContext } from "@agent-native/core/server";
|
|
6
10
|
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
7
11
|
import { z } from "zod";
|
|
8
12
|
|
|
9
13
|
import {
|
|
10
14
|
CONTENT_LAST_LOCATION_STATE_KEY,
|
|
15
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
11
16
|
type ContentLandingResolution,
|
|
12
17
|
type ContentLastLocationState,
|
|
18
|
+
type ContentWelcomePageState,
|
|
13
19
|
} from "../shared/content-landing.js";
|
|
14
20
|
import { resolveContentDocumentAccess } from "./_content-document-access.js";
|
|
15
21
|
import { normalizeContentSpaceEmail } from "./_content-space-access.js";
|
|
@@ -24,12 +30,85 @@ const WELCOME_CONTENT = `Start here with a page that is wholly yours.
|
|
|
24
30
|
- Use the sidebar to find and organize your work.
|
|
25
31
|
- Ask the agent when you want a hand.`;
|
|
26
32
|
|
|
27
|
-
function
|
|
33
|
+
function legacyWelcomeDocumentId(userEmail: string, generation: number) {
|
|
28
34
|
const identity = normalizeContentSpaceEmail(userEmail);
|
|
29
|
-
const
|
|
35
|
+
const input = generation === 0 ? identity : `${identity}:${generation}`;
|
|
36
|
+
const digest = createHash("sha256").update(input).digest("hex");
|
|
30
37
|
return `content_welcome_${digest.slice(0, 32)}`;
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
function randomWelcomeDocumentId() {
|
|
41
|
+
return `content_welcome_${randomUUID().replace(/-/g, "")}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function welcomeGeneration(state: Record<string, unknown>): number {
|
|
45
|
+
const candidate = state as ContentWelcomePageState;
|
|
46
|
+
if (
|
|
47
|
+
typeof candidate.generation !== "number" ||
|
|
48
|
+
!Number.isSafeInteger(candidate.generation) ||
|
|
49
|
+
candidate.generation < 0
|
|
50
|
+
) {
|
|
51
|
+
throw new Error("Content welcome page state has an invalid generation");
|
|
52
|
+
}
|
|
53
|
+
return candidate.generation;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isUniqueConstraintError(error: unknown): boolean {
|
|
57
|
+
let current: unknown = error;
|
|
58
|
+
const seen = new Set<unknown>();
|
|
59
|
+
while (current && typeof current === "object" && !seen.has(current)) {
|
|
60
|
+
seen.add(current);
|
|
61
|
+
const candidate = current as {
|
|
62
|
+
code?: unknown;
|
|
63
|
+
message?: unknown;
|
|
64
|
+
cause?: unknown;
|
|
65
|
+
};
|
|
66
|
+
const code = String(candidate.code ?? "");
|
|
67
|
+
const message = String(candidate.message ?? "");
|
|
68
|
+
if (
|
|
69
|
+
code === "23505" ||
|
|
70
|
+
code.includes("SQLITE_CONSTRAINT") ||
|
|
71
|
+
/unique constraint|unique violation|duplicate key/i.test(message)
|
|
72
|
+
) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
current = candidate.cause;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function resolveWelcomeState(): Promise<{
|
|
81
|
+
generation: number;
|
|
82
|
+
documentId: string | null;
|
|
83
|
+
expectedValue: Record<string, unknown>;
|
|
84
|
+
}> {
|
|
85
|
+
while (true) {
|
|
86
|
+
const entries = await listAppState(CONTENT_WELCOME_PAGE_STATE_KEY);
|
|
87
|
+
const entry = entries.find(
|
|
88
|
+
({ key }) => key === CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
89
|
+
);
|
|
90
|
+
if (entry) {
|
|
91
|
+
if (!entry.value || typeof entry.value !== "object") {
|
|
92
|
+
throw new Error("Content welcome page state must be an object");
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
generation: welcomeGeneration(entry.value),
|
|
96
|
+
documentId:
|
|
97
|
+
typeof entry.value.documentId === "string" && entry.value.documentId
|
|
98
|
+
? entry.value.documentId
|
|
99
|
+
: null,
|
|
100
|
+
expectedValue: entry.value,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const initial = { generation: 0 };
|
|
105
|
+
if (
|
|
106
|
+
await compareAndSetAppState(CONTENT_WELCOME_PAGE_STATE_KEY, null, initial)
|
|
107
|
+
) {
|
|
108
|
+
return { generation: 0, documentId: null, expectedValue: initial };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
33
112
|
function savedDocumentId(state: Record<string, unknown> | null): string | null {
|
|
34
113
|
const candidate = state as ContentLastLocationState | null;
|
|
35
114
|
return typeof candidate?.documentId === "string" && candidate.documentId
|
|
@@ -44,22 +123,28 @@ async function resolveUsableDocument(documentId: string) {
|
|
|
44
123
|
return access.resource;
|
|
45
124
|
}
|
|
46
125
|
|
|
47
|
-
async function resolveWelcomeDocument(userEmail: string) {
|
|
48
|
-
const
|
|
49
|
-
const document =
|
|
50
|
-
if (!document)
|
|
126
|
+
async function resolveWelcomeDocument(userEmail: string, documentId: string) {
|
|
127
|
+
const access = await resolveContentDocumentAccess(documentId);
|
|
128
|
+
const document = access?.resource;
|
|
129
|
+
if (!document) {
|
|
130
|
+
return (await isSoftDeletedDatabaseDocument(documentId))
|
|
131
|
+
? { status: "unavailable" as const }
|
|
132
|
+
: { status: "missing" as const };
|
|
133
|
+
}
|
|
134
|
+
if (document.trashedAt || (await isSoftDeletedDatabaseDocument(documentId))) {
|
|
135
|
+
return { status: "unavailable" as const };
|
|
136
|
+
}
|
|
51
137
|
|
|
52
138
|
const normalizedEmail = normalizeContentSpaceEmail(userEmail);
|
|
53
139
|
if (
|
|
54
140
|
normalizeContentSpaceEmail(document.ownerEmail) !== normalizedEmail ||
|
|
55
141
|
document.spaceId !== personalContentSpaceId(normalizedEmail) ||
|
|
56
142
|
document.parentId !== null ||
|
|
57
|
-
document.visibility !== "private"
|
|
58
|
-
document.title !== WELCOME_TITLE
|
|
143
|
+
document.visibility !== "private"
|
|
59
144
|
) {
|
|
60
|
-
return
|
|
145
|
+
return { status: "unavailable" as const };
|
|
61
146
|
}
|
|
62
|
-
return documentId;
|
|
147
|
+
return { status: "usable" as const, documentId };
|
|
63
148
|
}
|
|
64
149
|
|
|
65
150
|
async function resolveWelcome(userEmail: string): Promise<{
|
|
@@ -69,27 +154,72 @@ async function resolveWelcome(userEmail: string): Promise<{
|
|
|
69
154
|
"welcome-created" | "welcome-reused"
|
|
70
155
|
>;
|
|
71
156
|
}> {
|
|
72
|
-
const existing = await resolveWelcomeDocument(userEmail);
|
|
73
|
-
if (existing) return { documentId: existing, resolution: "welcome-reused" };
|
|
74
|
-
|
|
75
|
-
const documentId = welcomeDocumentId(userEmail);
|
|
76
157
|
const normalizedEmail = normalizeContentSpaceEmail(userEmail);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
158
|
+
while (true) {
|
|
159
|
+
const state = await resolveWelcomeState();
|
|
160
|
+
const documentId =
|
|
161
|
+
state.documentId ??
|
|
162
|
+
legacyWelcomeDocumentId(normalizedEmail, state.generation);
|
|
163
|
+
const existing = await resolveWelcomeDocument(normalizedEmail, documentId);
|
|
164
|
+
if (existing.status === "usable") {
|
|
165
|
+
if (!state.documentId) {
|
|
166
|
+
await compareAndSetAppState(
|
|
167
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
168
|
+
state.expectedValue,
|
|
169
|
+
{ ...state.expectedValue, documentId },
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
documentId: existing.documentId,
|
|
174
|
+
resolution: "welcome-reused",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!state.documentId || existing.status === "unavailable") {
|
|
179
|
+
await compareAndSetAppState(
|
|
180
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
181
|
+
state.expectedValue,
|
|
182
|
+
{
|
|
183
|
+
...state.expectedValue,
|
|
184
|
+
generation: state.generation + 1,
|
|
185
|
+
documentId: randomWelcomeDocumentId(),
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await runWithRequestContext({ userEmail: normalizedEmail }, () =>
|
|
193
|
+
createDocumentAction.run({
|
|
194
|
+
id: documentId,
|
|
195
|
+
title: WELCOME_TITLE,
|
|
196
|
+
content: WELCOME_CONTENT,
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
return { documentId, resolution: "welcome-created" };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const raced = await resolveWelcomeDocument(normalizedEmail, documentId);
|
|
202
|
+
if (raced.status === "usable") {
|
|
203
|
+
return {
|
|
204
|
+
documentId: raced.documentId,
|
|
205
|
+
resolution: "welcome-reused",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (raced.status === "unavailable") continue;
|
|
209
|
+
if (isUniqueConstraintError(error)) {
|
|
210
|
+
await compareAndSetAppState(
|
|
211
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
212
|
+
state.expectedValue,
|
|
213
|
+
{
|
|
214
|
+
...state.expectedValue,
|
|
215
|
+
generation: state.generation + 1,
|
|
216
|
+
documentId: randomWelcomeDocumentId(),
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
93
223
|
}
|
|
94
224
|
}
|
|
95
225
|
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export const CONTENT_LAST_LOCATION_STATE_KEY = "content-last-location-v1";
|
|
2
|
+
export const CONTENT_WELCOME_PAGE_STATE_KEY = "content-welcome-page-v1";
|
|
2
3
|
|
|
3
4
|
export type ContentLastLocationState = {
|
|
4
5
|
documentId: string;
|
|
5
6
|
};
|
|
6
7
|
|
|
8
|
+
export type ContentWelcomePageState = {
|
|
9
|
+
generation: number;
|
|
10
|
+
documentId?: string;
|
|
11
|
+
};
|
|
7
12
|
export type ContentLandingResolution =
|
|
8
13
|
| "restored"
|
|
9
14
|
| "welcome-created"
|
|
@@ -157,6 +157,14 @@ const AddSlideOp = z.object({
|
|
|
157
157
|
notes: z.string().optional(),
|
|
158
158
|
layout: z.string().optional(),
|
|
159
159
|
background: z.string().optional(),
|
|
160
|
+
imageUrl: z.string().optional(),
|
|
161
|
+
imagePrompt: z.string().optional(),
|
|
162
|
+
excalidrawData: z.string().optional(),
|
|
163
|
+
transition: z
|
|
164
|
+
.enum(["instant", "none", "fade", "slide", "zoom"])
|
|
165
|
+
.optional(),
|
|
166
|
+
animations: z.array(z.unknown()).optional(),
|
|
167
|
+
splitByParagraph: z.boolean().optional(),
|
|
160
168
|
})
|
|
161
169
|
.passthrough(),
|
|
162
170
|
});
|
|
@@ -382,7 +390,10 @@ export function applyOperation(deck: any, op: Operation): void {
|
|
|
382
390
|
// Idempotency: if the slide already exists (duplicate delivery), skip.
|
|
383
391
|
if (slides.some((s: { id: string }) => s.id === slideId)) return;
|
|
384
392
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
393
|
+
// Copy every provided field: a duplicated or undo-restored slide has to
|
|
394
|
+
// keep its transition, animations, and image data, not just its text.
|
|
385
395
|
const newSlide: any = {
|
|
396
|
+
...fields,
|
|
386
397
|
id: slideId,
|
|
387
398
|
content:
|
|
388
399
|
typeof fields.content === "string"
|
|
@@ -391,9 +402,7 @@ export function applyOperation(deck: any, op: Operation): void {
|
|
|
391
402
|
notes: fields.notes ?? "",
|
|
392
403
|
layout: fields.layout ?? "content",
|
|
393
404
|
};
|
|
394
|
-
|
|
395
|
-
newSlide.background = fields.background;
|
|
396
|
-
}
|
|
405
|
+
delete newSlide.imageLoading;
|
|
397
406
|
const insertAfterIdx = afterSlideId
|
|
398
407
|
? slides.findIndex((s: { id: string }) => s.id === afterSlideId)
|
|
399
408
|
: -1;
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
IconLoader2,
|
|
4
|
+
IconPlus,
|
|
5
|
+
IconTextSize,
|
|
6
|
+
IconTransitionRight,
|
|
7
|
+
} from "@tabler/icons-react";
|
|
3
8
|
import { useEffect, useRef, useState } from "react";
|
|
4
9
|
|
|
10
|
+
import {
|
|
11
|
+
DropdownMenu,
|
|
12
|
+
DropdownMenuContent,
|
|
13
|
+
DropdownMenuItem,
|
|
14
|
+
DropdownMenuTrigger,
|
|
15
|
+
} from "@/components/ui/dropdown-menu";
|
|
5
16
|
import {
|
|
6
17
|
Tooltip,
|
|
7
18
|
TooltipContent,
|
|
8
19
|
TooltipTrigger,
|
|
9
20
|
} from "@/components/ui/tooltip";
|
|
21
|
+
import type { Slide } from "@/context/DeckContext";
|
|
10
22
|
import { useAgentGenerating } from "@/hooks/use-agent-generating";
|
|
11
23
|
import { cn } from "@/lib/utils";
|
|
12
24
|
|
|
@@ -19,6 +31,15 @@ const IDLE_CLASS =
|
|
|
19
31
|
const ACTIVE_CLASS = "bg-accent text-foreground";
|
|
20
32
|
const DIVIDER_CLASS = "mx-1 h-4 w-px shrink-0 bg-border";
|
|
21
33
|
|
|
34
|
+
type SlideTransition = NonNullable<Slide["transition"]>;
|
|
35
|
+
|
|
36
|
+
const TRANSITIONS: { value: SlideTransition; labelKey: string }[] = [
|
|
37
|
+
{ value: "instant", labelKey: "editorToolbar.transition_instant" },
|
|
38
|
+
{ value: "fade", labelKey: "editorToolbar.transition_fade" },
|
|
39
|
+
{ value: "slide", labelKey: "editorToolbar.transition_slide" },
|
|
40
|
+
{ value: "zoom", labelKey: "editorToolbar.transition_zoom" },
|
|
41
|
+
];
|
|
42
|
+
|
|
22
43
|
/**
|
|
23
44
|
* Add slide, undo, redo, and add-text-box — the actions that stay put
|
|
24
45
|
* regardless of what is selected. Rendered at the head of the contextual
|
|
@@ -36,6 +57,8 @@ export function EditorActionCluster({
|
|
|
36
57
|
onDuplicateCurrentSlide,
|
|
37
58
|
textBoxMode,
|
|
38
59
|
onToggleTextBoxMode,
|
|
60
|
+
slideTransition,
|
|
61
|
+
onChangeSlideTransition,
|
|
39
62
|
className,
|
|
40
63
|
}: {
|
|
41
64
|
deckId: string;
|
|
@@ -49,12 +72,19 @@ export function EditorActionCluster({
|
|
|
49
72
|
onDuplicateCurrentSlide?: () => void;
|
|
50
73
|
textBoxMode?: boolean;
|
|
51
74
|
onToggleTextBoxMode?: () => void;
|
|
75
|
+
slideTransition?: Slide["transition"];
|
|
76
|
+
onChangeSlideTransition?: (transition: SlideTransition) => void;
|
|
52
77
|
className?: string;
|
|
53
78
|
}) {
|
|
54
79
|
const t = useT();
|
|
55
80
|
const { generating, submit: agentSubmit } = useAgentGenerating();
|
|
56
81
|
const [addSlideOpen, setAddSlideOpen] = useState(false);
|
|
57
82
|
const addSlideRef = useRef<HTMLButtonElement>(null);
|
|
83
|
+
// "none" is a legacy alias the presentation view already treats as instant.
|
|
84
|
+
const activeTransition: SlideTransition =
|
|
85
|
+
!slideTransition || slideTransition === "none"
|
|
86
|
+
? "instant"
|
|
87
|
+
: slideTransition;
|
|
58
88
|
|
|
59
89
|
useEffect(() => {
|
|
60
90
|
if (!generating) onAddSlideGeneratingChange?.(false);
|
|
@@ -125,6 +155,48 @@ export function EditorActionCluster({
|
|
|
125
155
|
</Tooltip>
|
|
126
156
|
</>
|
|
127
157
|
)}
|
|
158
|
+
|
|
159
|
+
{onChangeSlideTransition && currentSlideId && (
|
|
160
|
+
<>
|
|
161
|
+
<div className={DIVIDER_CLASS} />
|
|
162
|
+
<DropdownMenu>
|
|
163
|
+
<Tooltip>
|
|
164
|
+
<TooltipTrigger asChild>
|
|
165
|
+
<DropdownMenuTrigger asChild>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
aria-label={t("editorToolbar.transition")}
|
|
169
|
+
className={cn(
|
|
170
|
+
BUTTON_CLASS,
|
|
171
|
+
activeTransition === "instant"
|
|
172
|
+
? IDLE_CLASS
|
|
173
|
+
: ACTIVE_CLASS,
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
<IconTransitionRight className="size-4" />
|
|
177
|
+
</button>
|
|
178
|
+
</DropdownMenuTrigger>
|
|
179
|
+
</TooltipTrigger>
|
|
180
|
+
<TooltipContent>{t("editorToolbar.transition")}</TooltipContent>
|
|
181
|
+
</Tooltip>
|
|
182
|
+
<DropdownMenuContent align="start" className="w-40">
|
|
183
|
+
{TRANSITIONS.map((transition) => (
|
|
184
|
+
<DropdownMenuItem
|
|
185
|
+
key={transition.value}
|
|
186
|
+
onSelect={() => onChangeSlideTransition(transition.value)}
|
|
187
|
+
className={
|
|
188
|
+
activeTransition === transition.value
|
|
189
|
+
? "bg-accent text-accent-foreground"
|
|
190
|
+
: undefined
|
|
191
|
+
}
|
|
192
|
+
>
|
|
193
|
+
{t(transition.labelKey)}
|
|
194
|
+
</DropdownMenuItem>
|
|
195
|
+
))}
|
|
196
|
+
</DropdownMenuContent>
|
|
197
|
+
</DropdownMenu>
|
|
198
|
+
</>
|
|
199
|
+
)}
|
|
128
200
|
</div>
|
|
129
201
|
);
|
|
130
202
|
}
|
|
@@ -198,7 +198,12 @@ function SortableSlideThumb({
|
|
|
198
198
|
type="button"
|
|
199
199
|
{...(readOnly ? {} : attributes)}
|
|
200
200
|
{...(readOnly ? {} : listeners)}
|
|
201
|
-
onClick={
|
|
201
|
+
onClick={(event) => {
|
|
202
|
+
// Safari does not focus a button on click, and the slide copy/paste
|
|
203
|
+
// and delete shortcuts are scoped to a focused thumbnail.
|
|
204
|
+
event.currentTarget.focus();
|
|
205
|
+
onSelect();
|
|
206
|
+
}}
|
|
202
207
|
onFocus={onSelect}
|
|
203
208
|
aria-label={t("editorSidebar.selectSlide", { number: index + 1 })}
|
|
204
209
|
aria-current={isActive ? "true" : undefined}
|
|
@@ -121,6 +121,9 @@ interface EditorToolbarProps {
|
|
|
121
121
|
textBoxMode?: boolean;
|
|
122
122
|
/** Toggle the add-text-box tool */
|
|
123
123
|
onToggleTextBoxMode?: () => void;
|
|
124
|
+
onChangeSlideTransition?: (
|
|
125
|
+
transition: NonNullable<Slide["transition"]>,
|
|
126
|
+
) => void;
|
|
124
127
|
/** Duplicate the current deck */
|
|
125
128
|
onDuplicateDeck?: () => void;
|
|
126
129
|
/** Export the deck as PDF */
|
|
@@ -176,6 +179,7 @@ export default function EditorToolbar({
|
|
|
176
179
|
onTogglePinMode,
|
|
177
180
|
textBoxMode,
|
|
178
181
|
onToggleTextBoxMode,
|
|
182
|
+
onChangeSlideTransition,
|
|
179
183
|
onDuplicateDeck,
|
|
180
184
|
onExportPdf,
|
|
181
185
|
onExportPptx,
|
|
@@ -567,6 +571,8 @@ export default function EditorToolbar({
|
|
|
567
571
|
onDuplicateCurrentSlide={onDuplicateCurrentSlide}
|
|
568
572
|
textBoxMode={textBoxMode}
|
|
569
573
|
onToggleTextBoxMode={onToggleTextBoxMode}
|
|
574
|
+
slideTransition={currentSlide?.transition}
|
|
575
|
+
onChangeSlideTransition={onChangeSlideTransition}
|
|
570
576
|
/>
|
|
571
577
|
)}
|
|
572
578
|
|
|
@@ -176,6 +176,21 @@ export function NewDeckReferenceStep({
|
|
|
176
176
|
const chooseSource = (
|
|
177
177
|
kind: NonNullable<NewDeckReferenceSelection["referenceSource"]>["kind"],
|
|
178
178
|
) => {
|
|
179
|
+
const isAlreadySelected =
|
|
180
|
+
selectedSource?.kind === kind ||
|
|
181
|
+
(kind === "google-docs" && importedReference?.source === "google-slides");
|
|
182
|
+
if (isAlreadySelected) {
|
|
183
|
+
setSelectedSource(null);
|
|
184
|
+
if (importedReference) {
|
|
185
|
+
// The import also set the reference deck. Leaving that id behind would
|
|
186
|
+
// submit a deck the UI no longer shows as selected.
|
|
187
|
+
setSelectedReferenceDeckId((current) =>
|
|
188
|
+
current === importedReference.id ? null : current,
|
|
189
|
+
);
|
|
190
|
+
setImportedReference(null);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
179
194
|
setSelectedSource({ kind, value: "" });
|
|
180
195
|
if (kind === "website" || kind === "figma") {
|
|
181
196
|
setSelectedDesignSystemId(null);
|
|
@@ -40,12 +40,10 @@ type GranularOp =
|
|
|
40
40
|
op: "add-slide";
|
|
41
41
|
slideId: string;
|
|
42
42
|
afterSlideId?: string;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
background?: string;
|
|
48
|
-
};
|
|
43
|
+
/** Everything but `id` and the transient `imageLoading` flag. A slide
|
|
44
|
+
* copied or restored through this op keeps its transition, animations,
|
|
45
|
+
* and image/Excalidraw data instead of silently losing them on reload. */
|
|
46
|
+
fields: Omit<Partial<Slide>, "id" | "imageLoading"> & { content: string };
|
|
49
47
|
}
|
|
50
48
|
| {
|
|
51
49
|
op: "patch-deck-fields";
|
|
@@ -57,6 +55,16 @@ type GranularOp =
|
|
|
57
55
|
| { op: "full-replace"; deck: Deck };
|
|
58
56
|
|
|
59
57
|
export type PatchDeckOp = Exclude<GranularOp, { op: "full-replace" }>;
|
|
58
|
+
|
|
59
|
+
/** Slide payload for an `add-slide` op. `imageLoading` is transient UI state
|
|
60
|
+
* and must not persist; everything else on the slide has to survive the round
|
|
61
|
+
* trip or a duplicated/restored slide comes back missing fields. */
|
|
62
|
+
function addSlideFields(
|
|
63
|
+
slide: Slide,
|
|
64
|
+
): Extract<GranularOp, { op: "add-slide" }>["fields"] {
|
|
65
|
+
const { id: _id, imageLoading: _imageLoading, ...fields } = slide;
|
|
66
|
+
return fields;
|
|
67
|
+
}
|
|
60
68
|
export type DeckReloadStatus = "loaded" | "failed" | "stale";
|
|
61
69
|
export interface UpdateSlideOptions {
|
|
62
70
|
persistence?: "debounced" | "immediate";
|
|
@@ -683,13 +691,11 @@ export function applyOpToDeck(deck: Deck, op: PatchDeckOp): Deck {
|
|
|
683
691
|
case "add-slide": {
|
|
684
692
|
if (deck.slides.some((s) => s.id === op.slideId)) return deck; // idempotent
|
|
685
693
|
const newSlide: Slide = {
|
|
694
|
+
...op.fields,
|
|
686
695
|
id: op.slideId,
|
|
687
696
|
content: op.fields.content,
|
|
688
697
|
notes: op.fields.notes ?? "",
|
|
689
698
|
layout: (op.fields.layout as SlideLayout) ?? "content",
|
|
690
|
-
...(op.fields.background !== undefined
|
|
691
|
-
? { background: op.fields.background }
|
|
692
|
-
: {}),
|
|
693
699
|
};
|
|
694
700
|
const slides = [...deck.slides];
|
|
695
701
|
const afterIdx = op.afterSlideId
|
|
@@ -822,14 +828,7 @@ export function deriveInverseOp(
|
|
|
822
828
|
op: "add-slide",
|
|
823
829
|
slideId: prior.id,
|
|
824
830
|
afterSlideId,
|
|
825
|
-
fields:
|
|
826
|
-
content: prior.content,
|
|
827
|
-
notes: prior.notes,
|
|
828
|
-
layout: prior.layout,
|
|
829
|
-
...(prior.background !== undefined
|
|
830
|
-
? { background: prior.background }
|
|
831
|
-
: {}),
|
|
832
|
-
},
|
|
831
|
+
fields: addSlideFields(prior),
|
|
833
832
|
},
|
|
834
833
|
{
|
|
835
834
|
op: "reorder-slides",
|
|
@@ -2127,12 +2126,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
2127
2126
|
op: "add-slide",
|
|
2128
2127
|
slideId: newSlide.id,
|
|
2129
2128
|
afterSlideId,
|
|
2130
|
-
fields:
|
|
2131
|
-
content: newSlide.content,
|
|
2132
|
-
notes: newSlide.notes,
|
|
2133
|
-
layout: newSlide.layout,
|
|
2134
|
-
background: newSlide.background,
|
|
2135
|
-
},
|
|
2129
|
+
fields: addSlideFields(newSlide),
|
|
2136
2130
|
};
|
|
2137
2131
|
enqueueDeckOp(deckId, op);
|
|
2138
2132
|
if (before) recordUndo(before, op, { label: "Add slide" });
|
|
@@ -2240,17 +2234,11 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
2240
2234
|
// Granular add-slide op — inserts the copy after the original. Build it
|
|
2241
2235
|
// from the current deck before scheduling the React state update; the
|
|
2242
2236
|
// functional updater runs later and cannot be used to produce the op.
|
|
2243
|
-
const { id: newSlideId, ...rest } = copiedSlide;
|
|
2244
2237
|
const op: PatchDeckOp = {
|
|
2245
2238
|
op: "add-slide",
|
|
2246
|
-
slideId:
|
|
2239
|
+
slideId: copiedSlide.id,
|
|
2247
2240
|
afterSlideId: slideId,
|
|
2248
|
-
fields:
|
|
2249
|
-
content: rest.content,
|
|
2250
|
-
notes: rest.notes,
|
|
2251
|
-
layout: rest.layout,
|
|
2252
|
-
background: rest.background,
|
|
2253
|
-
},
|
|
2241
|
+
fields: addSlideFields(copiedSlide),
|
|
2254
2242
|
};
|
|
2255
2243
|
enqueueDeckOp(deckId, op);
|
|
2256
2244
|
recordUndo(before, op, { label: "Duplicate slide" });
|
|
@@ -156,22 +156,21 @@ export function useNavigationState() {
|
|
|
156
156
|
path = `/deck/${cmd.deckId}`;
|
|
157
157
|
if (cmd.view === "present") {
|
|
158
158
|
path += "/present";
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
159
|
+
}
|
|
160
|
+
const internalSlideIndex =
|
|
161
|
+
typeof cmd.slideNumber === "number" &&
|
|
162
|
+
Number.isFinite(cmd.slideNumber) &&
|
|
163
|
+
cmd.slideNumber >= 1
|
|
164
|
+
? cmd.slideNumber - 1
|
|
165
|
+
: cmd.slideIndex;
|
|
166
|
+
if (
|
|
167
|
+
typeof internalSlideIndex === "number" &&
|
|
168
|
+
Number.isFinite(internalSlideIndex) &&
|
|
169
|
+
internalSlideIndex >= 0
|
|
170
|
+
) {
|
|
171
|
+
// Convert the internal zero-based value back to the 1-based
|
|
172
|
+
// ?slide=N URL param both the editor and presentation view read.
|
|
173
|
+
path += `?slide=${internalSlideIndex + 1}`;
|
|
175
174
|
}
|
|
176
175
|
}
|
|
177
176
|
|
|
@@ -1194,6 +1194,11 @@ export default function DeckEditor() {
|
|
|
1194
1194
|
onDuplicateCurrentSlide={
|
|
1195
1195
|
currentSlide ? () => duplicateSlide(id, currentSlide.id) : undefined
|
|
1196
1196
|
}
|
|
1197
|
+
onChangeSlideTransition={
|
|
1198
|
+
canEdit && currentSlide
|
|
1199
|
+
? (transition) => updateSlide(id, currentSlide.id, { transition })
|
|
1200
|
+
: undefined
|
|
1201
|
+
}
|
|
1197
1202
|
/>
|
|
1198
1203
|
|
|
1199
1204
|
{/* Full-width host for the slide's contextual style toolbar: it spans the
|
|
@@ -1312,6 +1317,10 @@ export default function DeckEditor() {
|
|
|
1312
1317
|
}
|
|
1313
1318
|
textBoxMode={textBoxMode}
|
|
1314
1319
|
onToggleTextBoxMode={toggleTextBoxMode}
|
|
1320
|
+
slideTransition={currentSlide.transition}
|
|
1321
|
+
onChangeSlideTransition={(transition) =>
|
|
1322
|
+
updateSlide(id, currentSlide.id, { transition })
|
|
1323
|
+
}
|
|
1315
1324
|
/>
|
|
1316
1325
|
) : undefined
|
|
1317
1326
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function normalizePathname(pathname, stripEndpoint) {
|
|
2
|
+
const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
3
|
+
const withoutTrailingSlashes = normalized.replace(/\/+$/, "");
|
|
4
|
+
const withoutEndpoint = stripEndpoint
|
|
5
|
+
? withoutTrailingSlashes.endsWith("/_agent-native/a2a")
|
|
6
|
+
? withoutTrailingSlashes.slice(0, -"/_agent-native/a2a".length)
|
|
7
|
+
: withoutTrailingSlashes.endsWith("/a2a")
|
|
8
|
+
? withoutTrailingSlashes.slice(0, -"/a2a".length)
|
|
9
|
+
: withoutTrailingSlashes
|
|
10
|
+
: withoutTrailingSlashes;
|
|
11
|
+
return withoutEndpoint === "" ? "/" : withoutEndpoint;
|
|
12
|
+
}
|
|
13
|
+
/** Canonical receiver identifier used by both A2A token issuers and verifiers. */
|
|
14
|
+
export function canonicalA2AAudience(rawUrl, receiverBasePath) {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(rawUrl);
|
|
17
|
+
const pathname = normalizePathname(receiverBasePath ?? url.pathname, receiverBasePath === undefined);
|
|
18
|
+
return pathname === "/" ? url.origin : `${url.origin}${pathname}`;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return rawUrl.replace(/\/+$/, "");
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/a2a/client.d.ts
CHANGED
|
@@ -62,6 +62,10 @@ export declare class A2AClient {
|
|
|
62
62
|
* Agent-native apps use /_agent-native/a2a, external agents may use /a2a.
|
|
63
63
|
*/
|
|
64
64
|
resolveEndpoint(): Promise<void>;
|
|
65
|
+
/** Resolve the card-advertised endpoint without sending an RPC request. */
|
|
66
|
+
resolveEndpointUrl(timeoutMs?: number): Promise<string>;
|
|
67
|
+
/** Replace caller credentials without discarding resolved endpoint fallbacks. */
|
|
68
|
+
setAuthentication(apiKey?: string, fallbackApiKeys?: string[]): void;
|
|
65
69
|
private headers;
|
|
66
70
|
private markApiKeySucceeded;
|
|
67
71
|
private rpc;
|
package/dist/a2a/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as jose from "jose";
|
|
3
3
|
import { ssrfSafeFetch } from "../extensions/url-safety.js";
|
|
4
|
+
import { canonicalA2AAudience } from "./audience.js";
|
|
4
5
|
/**
|
|
5
6
|
* A workspace serves every app from one gateway on loopback, so sibling A2A
|
|
6
7
|
* targets are private addresses by construction and the SSRF guard cannot tell
|
|
@@ -185,6 +186,19 @@ export class A2AClient {
|
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
}
|
|
189
|
+
/** Resolve the card-advertised endpoint without sending an RPC request. */
|
|
190
|
+
async resolveEndpointUrl(timeoutMs) {
|
|
191
|
+
await this.ensureEndpointCandidates(timeoutMs);
|
|
192
|
+
const endpoint = this.endpointCandidates[0];
|
|
193
|
+
if (!endpoint)
|
|
194
|
+
throw new Error("No A2A endpoint candidates available");
|
|
195
|
+
return endpoint;
|
|
196
|
+
}
|
|
197
|
+
/** Replace caller credentials without discarding resolved endpoint fallbacks. */
|
|
198
|
+
setAuthentication(apiKey, fallbackApiKeys = []) {
|
|
199
|
+
this.apiKey = apiKey;
|
|
200
|
+
this.apiKeyAttempts = uniqueAuthTokens([apiKey, ...fallbackApiKeys]);
|
|
201
|
+
}
|
|
188
202
|
headers(apiKey = this.apiKey) {
|
|
189
203
|
const h = { "Content-Type": "application/json" };
|
|
190
204
|
if (apiKey) {
|
|
@@ -823,14 +837,27 @@ export async function callAction(url, action, input = {}, opts) {
|
|
|
823
837
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
824
838
|
throw new Error("A2A action input must be an object");
|
|
825
839
|
}
|
|
826
|
-
const
|
|
827
|
-
const
|
|
840
|
+
const discoveryAudience = normalizeA2AAudience(url);
|
|
841
|
+
const discoveryApiKeyAttempts = await buildA2AApiKeyAttempts(opts, discoveryAudience);
|
|
842
|
+
const discoveryFallbackApiKeys = discoveryApiKeyAttempts
|
|
828
843
|
.slice(1)
|
|
829
844
|
.filter((token) => token !== undefined);
|
|
830
|
-
const client = new A2AClient(url,
|
|
831
|
-
fallbackApiKeys,
|
|
845
|
+
const client = new A2AClient(url, discoveryApiKeyAttempts[0], {
|
|
846
|
+
fallbackApiKeys: discoveryFallbackApiKeys,
|
|
832
847
|
requestTimeoutMs: opts?.requestTimeoutMs,
|
|
833
848
|
});
|
|
849
|
+
const endpointUrl = await client.resolveEndpointUrl(opts?.requestTimeoutMs);
|
|
850
|
+
const invocationAudience = normalizeA2AAudience(endpointUrl);
|
|
851
|
+
if (invocationAudience !== discoveryAudience) {
|
|
852
|
+
const invocationApiKeyAttempts = await buildA2AApiKeyAttempts(opts, invocationAudience);
|
|
853
|
+
const combinedApiKeyAttempts = uniqueAuthTokens([
|
|
854
|
+
...invocationApiKeyAttempts,
|
|
855
|
+
...discoveryApiKeyAttempts,
|
|
856
|
+
]);
|
|
857
|
+
client.setAuthentication(combinedApiKeyAttempts[0], combinedApiKeyAttempts
|
|
858
|
+
.slice(1)
|
|
859
|
+
.filter((token) => token !== undefined));
|
|
860
|
+
}
|
|
834
861
|
return client.invokeAction(actionName, input, {
|
|
835
862
|
metadata: sanitizeA2ACorrelationMetadata(opts?.correlation),
|
|
836
863
|
});
|
|
@@ -876,16 +903,7 @@ async function buildA2AApiKeyAttempts(opts, audience) {
|
|
|
876
903
|
function normalizeA2AAudience(url) {
|
|
877
904
|
const explicit = splitExplicitA2AEndpoint(url.replace(/\/$/, ""));
|
|
878
905
|
const base = (explicit?.baseUrl ?? url).replace(/\/$/, "");
|
|
879
|
-
|
|
880
|
-
// app path. In a workspace every app is mounted under one gateway origin, so
|
|
881
|
-
// signing the path-qualified URL yields an audience the receiver can never
|
|
882
|
-
// match. The origin is the identifier both sides agree on.
|
|
883
|
-
try {
|
|
884
|
-
return new URL(base).origin;
|
|
885
|
-
}
|
|
886
|
-
catch {
|
|
887
|
-
return base;
|
|
888
|
-
}
|
|
906
|
+
return canonicalA2AAudience(base);
|
|
889
907
|
}
|
|
890
908
|
function isA2AAuthRejection(err) {
|
|
891
909
|
const message = err instanceof Error ? err.message : String(err ?? "");
|
package/dist/a2a/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { A2AConfig } from "./types.js";
|
|
1
|
+
import type { A2AConfig, AgentSkill } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Result of verifying an inbound A2A JWT. `email` is the caller identity from
|
|
4
4
|
* the token's `sub` claim (null when verification fails), `orgDomain` mirrors
|
|
@@ -21,9 +21,14 @@ export interface A2ATokenPayload {
|
|
|
21
21
|
* Exported so workspaces can accept A2A callers on the HTTP action route with
|
|
22
22
|
* the same routine — including org-level fallback secrets — instead of
|
|
23
23
|
* reimplementing a partial verifier. Pass the H3 `event` to enable org-domain →
|
|
24
|
-
* org-secret lookup and audience derivation; it is optional.
|
|
24
|
+
* org-secret lookup and audience derivation; it is optional. A custom mounted
|
|
25
|
+
* JSON-RPC route also passes its prefix so endpoint-bound tokens verify against
|
|
26
|
+
* the same audience the client derived from the advertised URL.
|
|
25
27
|
*/
|
|
26
|
-
export declare function verifyA2AToken(token: string, event?: any
|
|
28
|
+
export declare function verifyA2AToken(token: string, event?: any, audienceOptions?: {
|
|
29
|
+
routePrefix?: string;
|
|
30
|
+
allowBaseAudience?: boolean;
|
|
31
|
+
}): Promise<A2ATokenPayload>;
|
|
27
32
|
/**
|
|
28
33
|
* Mount A2A protocol endpoints on an H3/Nitro app.
|
|
29
34
|
*
|
|
@@ -35,4 +40,4 @@ export declare function verifyA2AToken(token: string, event?: any): Promise<A2AT
|
|
|
35
40
|
* cryptographic identity verification for cross-app A2A calls.
|
|
36
41
|
*/
|
|
37
42
|
export declare function mountA2A(nitroApp: any, config: A2AConfig, routePrefix?: string): void;
|
|
38
|
-
export declare function filterPublicAgentCardSkills(config: A2AConfig):
|
|
43
|
+
export declare function filterPublicAgentCardSkills(config: A2AConfig): AgentSkill[];
|
package/dist/a2a/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getH3App } from "../server/framework-request-handler.js";
|
|
|
6
6
|
import { readBody } from "../server/h3-helpers.js";
|
|
7
7
|
import { isSameOriginRequest } from "../server/request-origin.js";
|
|
8
8
|
import { generateAgentCard } from "./agent-card.js";
|
|
9
|
+
import { canonicalA2AAudience } from "./audience.js";
|
|
9
10
|
import { hasConfiguredA2ASecret, isA2AProductionRuntime, } from "./auth-policy.js";
|
|
10
11
|
import { handleJsonRpcH3, processA2ATaskFromQueue } from "./handlers.js";
|
|
11
12
|
import { claimA2AApproval, getA2AApprovalForOwner, settleA2AApproval, } from "./task-store.js";
|
|
@@ -39,25 +40,46 @@ function addSecretCandidate(candidates, secret) {
|
|
|
39
40
|
* service must not verify here). Only tokens without an `aud` claim (minted
|
|
40
41
|
* before the audience claim shipped) skip the audience check.
|
|
41
42
|
*/
|
|
42
|
-
function expectedJwtAudience(event) {
|
|
43
|
+
function expectedJwtAudience(event, options) {
|
|
43
44
|
const fromEnv = process.env.APP_URL ||
|
|
44
45
|
process.env.URL ||
|
|
45
46
|
process.env.DEPLOY_URL ||
|
|
46
47
|
process.env.BETTER_AUTH_URL;
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const receiverBasePath = process.env.APP_BASE_PATH;
|
|
49
|
+
if (fromEnv) {
|
|
50
|
+
return audienceForRoute(canonicalA2AAudience(String(fromEnv), receiverBasePath), options);
|
|
51
|
+
}
|
|
49
52
|
// Best-effort: derive from the inbound request host. This is forgeable
|
|
50
53
|
// (Host-header attack), but only useful as a hint when env-derived URL
|
|
51
54
|
// is unset; the rest of the JWT verification still uses the secret.
|
|
52
55
|
try {
|
|
53
56
|
const proto = getRequestHeader(event, "x-forwarded-proto") || "https";
|
|
54
57
|
const host = getRequestHeader(event, "host");
|
|
55
|
-
if (host)
|
|
56
|
-
return `${proto}://${host}
|
|
58
|
+
if (host) {
|
|
59
|
+
return audienceForRoute(canonicalA2AAudience(`${proto}://${host}`, receiverBasePath), options);
|
|
60
|
+
}
|
|
61
|
+
// coercion-ok: undefined makes audience-bearing token verification fail closed.
|
|
57
62
|
}
|
|
58
63
|
catch { }
|
|
59
64
|
return undefined;
|
|
60
65
|
}
|
|
66
|
+
function audienceForRoute(baseAudience, options) {
|
|
67
|
+
const trimmedPrefix = options?.routePrefix?.replace(/^\/+|\/+$/g, "");
|
|
68
|
+
if (!trimmedPrefix || trimmedPrefix === "_agent-native")
|
|
69
|
+
return baseAudience;
|
|
70
|
+
const normalizedPrefix = `/${trimmedPrefix}`;
|
|
71
|
+
const routeAudience = `${baseAudience.replace(/\/+$/, "")}${normalizedPrefix}`;
|
|
72
|
+
return options?.allowBaseAudience
|
|
73
|
+
? [routeAudience, baseAudience]
|
|
74
|
+
: routeAudience;
|
|
75
|
+
}
|
|
76
|
+
function tokenHasAudienceClaim(token) {
|
|
77
|
+
return typeof jose.decodeJwt(token).aud !== "undefined";
|
|
78
|
+
}
|
|
79
|
+
function isDirectReadSkill(skill) {
|
|
80
|
+
return (skill.readOnly === true ||
|
|
81
|
+
(skill.readOnly === undefined && skill.publicAgent?.readOnly === true));
|
|
82
|
+
}
|
|
61
83
|
/**
|
|
62
84
|
* Verify an inbound A2A bearer token (HS256) exactly as the
|
|
63
85
|
* `/_agent-native/a2a` endpoint does: it peeks at the unverified `org_domain`
|
|
@@ -71,9 +93,11 @@ function expectedJwtAudience(event) {
|
|
|
71
93
|
* Exported so workspaces can accept A2A callers on the HTTP action route with
|
|
72
94
|
* the same routine — including org-level fallback secrets — instead of
|
|
73
95
|
* reimplementing a partial verifier. Pass the H3 `event` to enable org-domain →
|
|
74
|
-
* org-secret lookup and audience derivation; it is optional.
|
|
96
|
+
* org-secret lookup and audience derivation; it is optional. A custom mounted
|
|
97
|
+
* JSON-RPC route also passes its prefix so endpoint-bound tokens verify against
|
|
98
|
+
* the same audience the client derived from the advertised URL.
|
|
75
99
|
*/
|
|
76
|
-
export async function verifyA2AToken(token, event) {
|
|
100
|
+
export async function verifyA2AToken(token, event, audienceOptions) {
|
|
77
101
|
// Step 1: Peek at JWT claims WITHOUT verification to get org_domain.
|
|
78
102
|
// This is safe because we only use org_domain to look up the secret,
|
|
79
103
|
// then verify the full JWT with that secret. If someone forges a JWT
|
|
@@ -129,7 +153,7 @@ export async function verifyA2AToken(token, event) {
|
|
|
129
153
|
// whose `aud` targets ANOTHER service verify against a shared secret. A
|
|
130
154
|
// token that self-declares an audience must be checked against ours, so
|
|
131
155
|
// when we have nothing to check it against we reject rather than skip.
|
|
132
|
-
const aud = expectedJwtAudience(event);
|
|
156
|
+
const aud = expectedJwtAudience(event, audienceOptions);
|
|
133
157
|
if (!aud)
|
|
134
158
|
return { email: null, orgDomain: null };
|
|
135
159
|
verifyOptions.audience = aud;
|
|
@@ -187,19 +211,26 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
|
|
|
187
211
|
const host = getRequestHeader(event, "host") ?? "localhost";
|
|
188
212
|
const baseUrl = `${protocol}://${host}`;
|
|
189
213
|
// The anonymous card may only advertise actions safe to disclose
|
|
190
|
-
// publicly (`requiresAuth !== true`)
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// back to open-ended delegation. Show a verified caller what it can
|
|
195
|
-
// actually invoke; anonymous fetches keep the public list unchanged.
|
|
214
|
+
// publicly (`requiresAuth !== true`). A verified caller instead sees the
|
|
215
|
+
// authenticated surface: schemas for direct read invocation plus concise
|
|
216
|
+
// message-only capabilities for writes owned by the receiving agent.
|
|
217
|
+
// Anonymous fetches keep the public list unchanged.
|
|
196
218
|
let skills = filterPublicAgentCardSkills(config);
|
|
197
219
|
if (config.authenticatedSkills?.length) {
|
|
198
220
|
const bearer = extractBearerToken(getRequestHeader(event, "authorization"));
|
|
199
221
|
if (bearer) {
|
|
200
|
-
const payload = await verifyA2AToken(bearer, event
|
|
201
|
-
|
|
202
|
-
|
|
222
|
+
const payload = await verifyA2AToken(bearer, event, {
|
|
223
|
+
routePrefix,
|
|
224
|
+
// Capability discovery may begin from either the app URL or an
|
|
225
|
+
// already-advertised endpoint URL. Both identify this receiver;
|
|
226
|
+
// direct POST invocation below remains endpoint-bound.
|
|
227
|
+
allowBaseAudience: true,
|
|
228
|
+
});
|
|
229
|
+
if (payload.email) {
|
|
230
|
+
skills = tokenHasAudienceClaim(bearer)
|
|
231
|
+
? config.authenticatedSkills
|
|
232
|
+
: config.authenticatedSkills.filter((skill) => !isDirectReadSkill(skill));
|
|
233
|
+
}
|
|
203
234
|
}
|
|
204
235
|
}
|
|
205
236
|
return generateAgentCard({ ...config, skills }, baseUrl, `${routePrefix}/a2a`);
|
|
@@ -372,17 +403,13 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
|
|
|
372
403
|
const hasApiKey = !!(config.apiKeyEnv && process.env[config.apiKeyEnv]);
|
|
373
404
|
// Try JWT verification first (org-level or global A2A_SECRET-based identity)
|
|
374
405
|
if (bearerToken) {
|
|
375
|
-
const tokenPayload = await verifyA2AToken(bearerToken, event
|
|
406
|
+
const tokenPayload = await verifyA2AToken(bearerToken, event, {
|
|
407
|
+
routePrefix,
|
|
408
|
+
});
|
|
376
409
|
verifiedCallerEmail = tokenPayload.email;
|
|
377
410
|
verifiedOrgDomain = tokenPayload.orgDomain;
|
|
378
411
|
if (verifiedCallerEmail) {
|
|
379
|
-
|
|
380
|
-
verifiedAudienceBound =
|
|
381
|
-
typeof jose.decodeJwt(bearerToken).aud !== "undefined";
|
|
382
|
-
}
|
|
383
|
-
catch {
|
|
384
|
-
verifiedAudienceBound = false;
|
|
385
|
-
}
|
|
412
|
+
verifiedAudienceBound = tokenHasAudienceClaim(bearerToken);
|
|
386
413
|
}
|
|
387
414
|
bearerTokenRejectedByJwt = !verifiedCallerEmail;
|
|
388
415
|
}
|
package/dist/a2a/types.d.ts
CHANGED
|
@@ -189,10 +189,10 @@ export interface A2AConfig {
|
|
|
189
189
|
version?: string;
|
|
190
190
|
skills: AgentSkill[];
|
|
191
191
|
/**
|
|
192
|
-
* Skills advertised only to a caller with a verified A2A identity
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
192
|
+
* Skills advertised only to a caller with a verified A2A identity. Read-only
|
|
193
|
+
* skills may also be available through `actions/invoke`; mutating skills are
|
|
194
|
+
* message-only capabilities for delegation. Anonymous card fetches never see
|
|
195
|
+
* either set.
|
|
196
196
|
*/
|
|
197
197
|
authenticatedSkills?: AgentSkill[];
|
|
198
198
|
/** If true, public agent-card discovery includes only explicit public-safe skills. */
|
|
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
-
ok?: undefined;
|
|
45
|
-
error?: undefined;
|
|
46
44
|
summary: import("./types.js").TraceSummary;
|
|
47
45
|
spans: import("./types.js").TraceSpan[];
|
|
48
46
|
id?: undefined;
|
|
49
|
-
} | {
|
|
50
|
-
ok?: undefined;
|
|
51
47
|
error?: undefined;
|
|
48
|
+
ok?: undefined;
|
|
49
|
+
} | {
|
|
52
50
|
summary?: undefined;
|
|
53
51
|
spans?: undefined;
|
|
54
52
|
id: string;
|
|
55
|
-
|
|
53
|
+
error?: undefined;
|
|
56
54
|
ok?: undefined;
|
|
55
|
+
} | {
|
|
57
56
|
summary?: undefined;
|
|
58
57
|
spans?: undefined;
|
|
59
58
|
id?: undefined;
|
|
60
59
|
error: any;
|
|
60
|
+
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
-
error?: undefined;
|
|
63
62
|
summary?: undefined;
|
|
64
63
|
spans?: undefined;
|
|
65
64
|
id?: undefined;
|
|
66
65
|
ok: boolean;
|
|
66
|
+
error?: undefined;
|
|
67
67
|
}>>;
|
|
@@ -75,8 +75,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
75
75
|
user: "user";
|
|
76
76
|
}>>;
|
|
77
77
|
}, z.core.$strip>>, {
|
|
78
|
-
id?: undefined;
|
|
79
78
|
deleted?: undefined;
|
|
79
|
+
id?: undefined;
|
|
80
80
|
provider?: undefined;
|
|
81
81
|
providers: {
|
|
82
82
|
id: string;
|
|
@@ -93,8 +93,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
93
93
|
label?: undefined;
|
|
94
94
|
message?: undefined;
|
|
95
95
|
} | {
|
|
96
|
-
id?: undefined;
|
|
97
96
|
deleted?: undefined;
|
|
97
|
+
id?: undefined;
|
|
98
98
|
providers?: undefined;
|
|
99
99
|
count?: undefined;
|
|
100
100
|
found: boolean;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
ok?: undefined;
|
|
52
51
|
error: string;
|
|
52
|
+
ok?: undefined;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -34,16 +34,16 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
|
|
|
34
34
|
/** POST /_agent-native/secrets/:key — write a secret. */
|
|
35
35
|
export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
36
36
|
error: string;
|
|
37
|
-
ok?: undefined;
|
|
38
37
|
status?: undefined;
|
|
38
|
+
ok?: undefined;
|
|
39
39
|
} | {
|
|
40
40
|
ok: boolean;
|
|
41
41
|
status: string;
|
|
42
42
|
error?: undefined;
|
|
43
43
|
} | {
|
|
44
|
-
ok?: undefined;
|
|
45
44
|
error: string;
|
|
46
45
|
removed?: undefined;
|
|
46
|
+
ok?: undefined;
|
|
47
47
|
} | {
|
|
48
48
|
ok: boolean;
|
|
49
49
|
removed: boolean;
|
|
@@ -54,9 +54,9 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
54
54
|
* or the current stored value without changing anything.
|
|
55
55
|
*/
|
|
56
56
|
export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
57
|
-
ok?: undefined;
|
|
58
57
|
error: string;
|
|
59
58
|
note?: undefined;
|
|
59
|
+
ok?: undefined;
|
|
60
60
|
} | {
|
|
61
61
|
ok: boolean;
|
|
62
62
|
note?: undefined;
|
|
Binary file
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type A2AArtifactResponseOptions, type A2AToolResultSummary } from "../../a2a/artifact-response.js";
|
|
2
|
+
import type { AgentSkill } from "../../a2a/types.js";
|
|
2
3
|
import type { EngineTool } from "../../agent/engine/types.js";
|
|
3
4
|
import { type ActionEntry, type AgentLoopOutcome } from "../../agent/production-agent.js";
|
|
4
5
|
import { runAgentLoopDirectWithSoftTimeout } from "../../agent/run-loop-with-resume.js";
|
|
@@ -44,6 +45,13 @@ export interface A2AExternalAgentSurface {
|
|
|
44
45
|
* query the caller wrote.
|
|
45
46
|
*/
|
|
46
47
|
export declare function filterDirectA2AActions(actions: Record<string, ActionEntry>, options: A2AExternalAgentSurface): Record<string, ActionEntry>;
|
|
48
|
+
/**
|
|
49
|
+
* Authenticated write capabilities that a sibling may delegate to this app's
|
|
50
|
+
* agent, but never invoke directly. `publicAgent` is the action-owned opt-in;
|
|
51
|
+
* keeping this projection independent of `connectorCatalog` avoids making apps
|
|
52
|
+
* maintain a second capability registry. The deny list remains a final veto.
|
|
53
|
+
*/
|
|
54
|
+
export declare function filterDelegatedA2ACapabilityActions(actions: Record<string, ActionEntry>, options: A2AExternalAgentSurface): Record<string, ActionEntry>;
|
|
47
55
|
export declare function buildPublicAgentA2ASkills(actions: Record<string, ActionEntry>): Array<{
|
|
48
56
|
id: string;
|
|
49
57
|
name: string;
|
|
@@ -52,20 +60,13 @@ export declare function buildPublicAgentA2ASkills(actions: Record<string, Action
|
|
|
52
60
|
inputSchema?: Record<string, unknown>;
|
|
53
61
|
}>;
|
|
54
62
|
/**
|
|
55
|
-
* Skills for a caller with a verified A2A identity
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* an app has nothing callable when it does.
|
|
63
|
+
* Skills for a caller with a verified A2A identity. Exact bounded reads carry
|
|
64
|
+
* schemas and may be invoked directly; explicitly exposed writes carry no
|
|
65
|
+
* schema and are discoverable only as natural-language delegation targets.
|
|
66
|
+
* Kept separate from `buildPublicAgentA2ASkills` because neither authenticated
|
|
67
|
+
* surface may be disclosed by the anonymous card.
|
|
61
68
|
*/
|
|
62
|
-
export declare function buildAuthenticatedAgentA2ASkills(actions: Record<string, ActionEntry>, options: A2AExternalAgentSurface):
|
|
63
|
-
id: string;
|
|
64
|
-
name: string;
|
|
65
|
-
description: string;
|
|
66
|
-
publicAgent: ActionEntry["publicAgent"];
|
|
67
|
-
inputSchema?: Record<string, unknown>;
|
|
68
|
-
}>;
|
|
69
|
+
export declare function buildAuthenticatedAgentA2ASkills(actions: Record<string, ActionEntry>, options: A2AExternalAgentSurface): AgentSkill[];
|
|
69
70
|
export declare function resolveArtifactBaseUrl(event: any | undefined): string | undefined;
|
|
70
71
|
export declare function assembleA2AFinalResponse(events: readonly AgentChatEvent[], toolResults: readonly A2AToolResultSummary[], options?: A2AArtifactResponseOptions & {
|
|
71
72
|
event?: any;
|
|
@@ -96,6 +96,24 @@ export function filterDirectA2AActions(actions, options) {
|
|
|
96
96
|
exposure.isConsequential !== true);
|
|
97
97
|
}));
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Authenticated write capabilities that a sibling may delegate to this app's
|
|
101
|
+
* agent, but never invoke directly. `publicAgent` is the action-owned opt-in;
|
|
102
|
+
* keeping this projection independent of `connectorCatalog` avoids making apps
|
|
103
|
+
* maintain a second capability registry. The deny list remains a final veto.
|
|
104
|
+
*/
|
|
105
|
+
export function filterDelegatedA2ACapabilityActions(actions, options) {
|
|
106
|
+
const denied = new Set(options.externalAgents?.denyActions ?? []);
|
|
107
|
+
return Object.fromEntries(Object.entries(actions).filter(([name, entry]) => {
|
|
108
|
+
const exposure = entry.publicAgent;
|
|
109
|
+
return (!denied.has(name) &&
|
|
110
|
+
entry.agentTool !== false &&
|
|
111
|
+
entry.readOnly !== true &&
|
|
112
|
+
exposure?.expose === true &&
|
|
113
|
+
exposure.readOnly === false &&
|
|
114
|
+
exposure.requiresAuth === true);
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
99
117
|
export function buildPublicAgentA2ASkills(actions) {
|
|
100
118
|
return Object.entries(filterPublicAgentActions(actions)).map(([name, entry]) => ({
|
|
101
119
|
id: name,
|
|
@@ -110,31 +128,49 @@ export function buildPublicAgentA2ASkills(actions) {
|
|
|
110
128
|
}));
|
|
111
129
|
}
|
|
112
130
|
/**
|
|
113
|
-
* Skills for a caller with a verified A2A identity
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* an app has nothing callable when it does.
|
|
131
|
+
* Skills for a caller with a verified A2A identity. Exact bounded reads carry
|
|
132
|
+
* schemas and may be invoked directly; explicitly exposed writes carry no
|
|
133
|
+
* schema and are discoverable only as natural-language delegation targets.
|
|
134
|
+
* Kept separate from `buildPublicAgentA2ASkills` because neither authenticated
|
|
135
|
+
* surface may be disclosed by the anonymous card.
|
|
119
136
|
*/
|
|
120
137
|
export function buildAuthenticatedAgentA2ASkills(actions, options) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
const directActions = filterDirectA2AActions(actions, options);
|
|
139
|
+
const delegatedActions = filterDelegatedA2ACapabilityActions(actions, options);
|
|
140
|
+
return Object.entries(actions).flatMap(([name, entry]) => {
|
|
141
|
+
const directEntry = directActions[name];
|
|
142
|
+
const delegatedEntry = delegatedActions[name];
|
|
143
|
+
if (!directEntry && !delegatedEntry)
|
|
144
|
+
return [];
|
|
145
|
+
if (directEntry) {
|
|
146
|
+
return {
|
|
147
|
+
id: name,
|
|
148
|
+
name,
|
|
149
|
+
description: entry.tool.description,
|
|
150
|
+
publicAgent: entry.publicAgent,
|
|
151
|
+
// Every action in this set is read-only by construction. Omitting the
|
|
152
|
+
// flag made discovery label all of them "(mutating)", which pushes a
|
|
153
|
+
// caller away from invoking them and back into open-ended delegation.
|
|
154
|
+
readOnly: true,
|
|
155
|
+
// Naming a directly callable action without its parameters is what
|
|
156
|
+
// makes a caller invoke it with `{}` and fail on a required field.
|
|
157
|
+
...(entry.tool.parameters
|
|
158
|
+
? {
|
|
159
|
+
inputSchema: entry.tool.parameters,
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
id: name,
|
|
166
|
+
name,
|
|
167
|
+
description: entry.tool.description,
|
|
168
|
+
publicAgent: entry.publicAgent,
|
|
169
|
+
// No input schema: the sibling sends an objective to the receiving app,
|
|
170
|
+
// whose agent chooses and validates its own local actions.
|
|
171
|
+
readOnly: false,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
138
174
|
}
|
|
139
175
|
export function resolveArtifactBaseUrl(event) {
|
|
140
176
|
const fromEnv = process.env.APP_URL ||
|
|
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
-
error?: undefined;
|
|
31
30
|
ok: boolean;
|
|
32
31
|
key: string;
|
|
33
32
|
baseUrlKey?: string;
|
|
34
33
|
scope: AgentEngineApiKeyScope;
|
|
34
|
+
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.154.
|
|
3
|
+
"version": "0.154.1",
|
|
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": {
|
|
@@ -421,8 +421,8 @@
|
|
|
421
421
|
"y-protocols": "^1.0.7",
|
|
422
422
|
"yjs": "^13.6.31",
|
|
423
423
|
"zod": "^4.3.6",
|
|
424
|
-
"@agent-native/
|
|
425
|
-
"@agent-native/
|
|
424
|
+
"@agent-native/toolkit": "^0.14.2",
|
|
425
|
+
"@agent-native/recap-cli": "0.5.4"
|
|
426
426
|
},
|
|
427
427
|
"devDependencies": {
|
|
428
428
|
"@ai-sdk/anthropic": "^3.0.71",
|