@agent-native/core 0.126.0 → 0.127.3
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 +2 -2
- package/corpus/core/CHANGELOG.md +83 -0
- package/corpus/core/docs/content/external-agents-catalog.mdx +14 -11
- package/corpus/core/docs/content/mcp-protocol.mdx +9 -4
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/index.ts +2 -0
- package/corpus/core/src/agent/production-agent.ts +6 -1
- package/corpus/core/src/agent/run-manager.ts +37 -0
- package/corpus/core/src/agent/run-store.ts +18 -0
- package/corpus/core/src/cli/template-sync.ts +96 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +75 -43
- package/corpus/core/src/client/agent-chat.ts +19 -1
- package/corpus/core/src/client/chat/tool-call-display.tsx +25 -0
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +23 -0
- package/corpus/core/src/client/settings/AgentsSection.tsx +565 -71
- package/corpus/core/src/client/settings/SecretsSection.tsx +71 -36
- package/corpus/core/src/client/use-chat-models.ts +54 -37
- package/corpus/core/src/localization/default-messages.ts +11 -0
- package/corpus/core/src/mcp/ask-app-task-handle.ts +183 -0
- package/corpus/core/src/mcp/builtin-tools.ts +146 -34
- package/corpus/core/src/server/agent-capabilities.ts +8 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +17 -0
- package/corpus/core/src/server/agent-discovery.ts +1 -1
- package/corpus/core/src/server/agent-peer-probe.ts +133 -0
- package/corpus/core/src/server/builder-design-systems.ts +1 -1
- package/corpus/core/src/server/core-routes-plugin.ts +67 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
- package/corpus/core/src/templates/chat/_gitignore +2 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
- package/corpus/templates/chat/_gitignore +2 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -0
- package/corpus/templates/design/actions/create-file.ts +15 -0
- package/corpus/templates/design/actions/generate-design.ts +40 -0
- package/corpus/templates/design/actions/present-design-variants.ts +16 -0
- package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
- package/corpus/templates/design/app/pages/design-editor/save-failure.ts +7 -1
- package/corpus/templates/design/changelog/2026-07-28-generated-designs-with-malformed-html-are-now-rejected-at-sa.md +6 -0
- package/corpus/templates/design/changelog/2026-07-28-the-model-you-pick-when-starting-a-new-design-is-now-used-fo.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +1 -0
- package/corpus/templates/design/shared/html-integrity.ts +608 -18
- package/corpus/templates/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -6
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +5 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +5 -0
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +31 -8
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +15 -0
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +78 -8
- package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +179 -0
- package/corpus/templates/slides/app/components/presentation/present-channel.ts +15 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +10 -1
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +67 -0
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +16 -3
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -0
- package/corpus/templates/slides/app/pages/Presentation.tsx +11 -3
- package/corpus/templates/slides/changelog/2026-07-28-export-to-google-slides-now-creates-the-deck-directly-in-you.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-28-opening-a-presentation-link-directly-no-longer-bounces-back-.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-28-presenter-view-open-a-second-window-while-presenting-to-see-.md +6 -0
- package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +90 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/composer/TiptapComposer.tsx +143 -111
- package/corpus/toolkit/src/composer/runtime-adapters.tsx +2 -0
- package/dist/a2a/index.d.ts +2 -0
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -0
- package/dist/a2a/index.js.map +1 -1
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +6 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +11 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +34 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +8 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +16 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/template-sync.d.ts.map +1 -1
- package/dist/cli/template-sync.js +88 -0
- package/dist/cli/template-sync.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +60 -31
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +6 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +13 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +14 -0
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +16 -0
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/settings/AgentsSection.d.ts.map +1 -1
- package/dist/client/settings/AgentsSection.js +250 -30
- package/dist/client/settings/AgentsSection.js.map +1 -1
- package/dist/client/settings/SecretsSection.d.ts.map +1 -1
- package/dist/client/settings/SecretsSection.js +16 -2
- package/dist/client/settings/SecretsSection.js.map +1 -1
- package/dist/client/use-chat-models.d.ts.map +1 -1
- package/dist/client/use-chat-models.js +46 -27
- package/dist/client/use-chat-models.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +9 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +9 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp/ask-app-task-handle.d.ts +25 -0
- package/dist/mcp/ask-app-task-handle.d.ts.map +1 -0
- package/dist/mcp/ask-app-task-handle.js +136 -0
- package/dist/mcp/ask-app-task-handle.js.map +1 -0
- package/dist/mcp/builtin-tools.d.ts +3 -3
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +115 -38
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-capabilities.d.ts +7 -1
- package/dist/server/agent-capabilities.d.ts.map +1 -1
- package/dist/server/agent-capabilities.js +1 -0
- package/dist/server/agent-capabilities.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +7 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +16 -1
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-discovery.d.ts +1 -0
- package/dist/server/agent-discovery.d.ts.map +1 -1
- package/dist/server/agent-discovery.js +1 -1
- package/dist/server/agent-discovery.js.map +1 -1
- package/dist/server/agent-peer-probe.d.ts +37 -0
- package/dist/server/agent-peer-probe.d.ts.map +1 -0
- package/dist/server/agent-peer-probe.js +90 -0
- package/dist/server/agent-peer-probe.js.map +1 -0
- package/dist/server/builder-design-systems.js +1 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +53 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +1 -0
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +4 -0
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/templates/chat/_gitignore +2 -0
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
- package/docs/content/external-agents-catalog.mdx +14 -11
- package/docs/content/mcp-protocol.mdx +9 -4
- package/package.json +2 -2
- package/src/a2a/index.ts +2 -0
- package/src/agent/production-agent.ts +6 -1
- package/src/agent/run-manager.ts +37 -0
- package/src/agent/run-store.ts +18 -0
- package/src/cli/template-sync.ts +96 -0
- package/src/client/MultiTabAssistantChat.tsx +75 -43
- package/src/client/agent-chat.ts +19 -1
- package/src/client/chat/tool-call-display.tsx +25 -0
- package/src/client/resources/mcp-integration-catalog.ts +23 -0
- package/src/client/settings/AgentsSection.tsx +565 -71
- package/src/client/settings/SecretsSection.tsx +71 -36
- package/src/client/use-chat-models.ts +54 -37
- package/src/localization/default-messages.ts +11 -0
- package/src/mcp/ask-app-task-handle.ts +183 -0
- package/src/mcp/builtin-tools.ts +146 -34
- package/src/server/agent-capabilities.ts +8 -1
- package/src/server/agent-chat-plugin.ts +17 -0
- package/src/server/agent-discovery.ts +1 -1
- package/src/server/agent-peer-probe.ts +133 -0
- package/src/server/builder-design-systems.ts +1 -1
- package/src/server/core-routes-plugin.ts +67 -0
- package/src/shared/mcp-embed-headers.ts +4 -0
- package/src/templates/chat/_gitignore +2 -0
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
|
@@ -9,6 +9,15 @@ import {
|
|
|
9
9
|
Button as ToolkitButton,
|
|
10
10
|
ButtonBase as ToolkitButtonBase,
|
|
11
11
|
} from "@agent-native/toolkit/ui/button";
|
|
12
|
+
import {
|
|
13
|
+
Command,
|
|
14
|
+
CommandEmpty,
|
|
15
|
+
CommandGroup,
|
|
16
|
+
CommandInput,
|
|
17
|
+
CommandItem,
|
|
18
|
+
CommandList,
|
|
19
|
+
CommandSeparator,
|
|
20
|
+
} from "@agent-native/toolkit/ui/command";
|
|
12
21
|
import {
|
|
13
22
|
IconCheck,
|
|
14
23
|
IconChevronRight,
|
|
@@ -23,13 +32,10 @@ import React, { useEffect, useMemo, useState, useCallback } from "react";
|
|
|
23
32
|
|
|
24
33
|
import { agentNativePath } from "../api-path.js";
|
|
25
34
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
DropdownMenuSeparator,
|
|
31
|
-
DropdownMenuTrigger,
|
|
32
|
-
} from "../components/ui/dropdown-menu.js";
|
|
35
|
+
Popover,
|
|
36
|
+
PopoverContent,
|
|
37
|
+
PopoverTrigger,
|
|
38
|
+
} from "../components/ui/popover.js";
|
|
33
39
|
import {
|
|
34
40
|
Tooltip,
|
|
35
41
|
TooltipContent,
|
|
@@ -207,11 +213,13 @@ function KeysHeader({
|
|
|
207
213
|
onSecret?: (key: string) => void;
|
|
208
214
|
onCustomKey: () => void;
|
|
209
215
|
}) {
|
|
216
|
+
const [open, setOpen] = useState(false);
|
|
217
|
+
|
|
210
218
|
return (
|
|
211
219
|
<div className="flex items-center justify-between gap-3">
|
|
212
220
|
<p className="text-[11px] font-medium text-foreground">Keys</p>
|
|
213
|
-
<
|
|
214
|
-
<
|
|
221
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
222
|
+
<PopoverTrigger asChild>
|
|
215
223
|
<ToolkitButtonBase
|
|
216
224
|
type="button"
|
|
217
225
|
variant="outline"
|
|
@@ -220,34 +228,61 @@ function KeysHeader({
|
|
|
220
228
|
<IconPlus size={11} />
|
|
221
229
|
New
|
|
222
230
|
</ToolkitButtonBase>
|
|
223
|
-
</
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
</PopoverTrigger>
|
|
232
|
+
<PopoverContent align="end" className="w-60 p-0">
|
|
233
|
+
<Command
|
|
234
|
+
// cmdk's default scorer matches loose subsequences, so "logo"
|
|
235
|
+
// also surfaces every "G-o-o-g-l-e ... " key.
|
|
236
|
+
filter={(value, search) =>
|
|
237
|
+
value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0
|
|
238
|
+
}
|
|
239
|
+
>
|
|
240
|
+
{availableSecrets.length > 0 && (
|
|
241
|
+
<CommandInput placeholder="Search keys..." />
|
|
242
|
+
)}
|
|
243
|
+
<CommandList>
|
|
244
|
+
<CommandEmpty>No keys found.</CommandEmpty>
|
|
245
|
+
{availableSecrets.length > 0 && (
|
|
246
|
+
<>
|
|
247
|
+
<CommandGroup heading="Choose a key">
|
|
248
|
+
{availableSecrets.map((secret) => (
|
|
249
|
+
<CommandItem
|
|
250
|
+
key={secret.key}
|
|
251
|
+
value={`${secret.label} ${secret.key}`}
|
|
252
|
+
onSelect={() => {
|
|
253
|
+
setOpen(false);
|
|
254
|
+
onSecret?.(secret.key);
|
|
255
|
+
}}
|
|
256
|
+
className="flex items-center justify-between gap-3"
|
|
257
|
+
>
|
|
258
|
+
<span className="truncate">{secret.label}</span>
|
|
259
|
+
{secret.required && (
|
|
260
|
+
<span className="shrink-0 text-[9px] font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-400">
|
|
261
|
+
Required
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
</CommandItem>
|
|
265
|
+
))}
|
|
266
|
+
</CommandGroup>
|
|
267
|
+
<CommandSeparator />
|
|
268
|
+
</>
|
|
269
|
+
)}
|
|
270
|
+
<CommandGroup>
|
|
271
|
+
<CommandItem
|
|
272
|
+
value="custom key"
|
|
273
|
+
onSelect={() => {
|
|
274
|
+
setOpen(false);
|
|
275
|
+
onCustomKey();
|
|
276
|
+
}}
|
|
233
277
|
>
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<DropdownMenuSeparator />
|
|
243
|
-
</>
|
|
244
|
-
)}
|
|
245
|
-
<DropdownMenuItem onSelect={onCustomKey}>
|
|
246
|
-
<IconPlus size={14} />
|
|
247
|
-
Custom
|
|
248
|
-
</DropdownMenuItem>
|
|
249
|
-
</DropdownMenuContent>
|
|
250
|
-
</DropdownMenu>
|
|
278
|
+
<IconPlus size={14} />
|
|
279
|
+
Custom
|
|
280
|
+
</CommandItem>
|
|
281
|
+
</CommandGroup>
|
|
282
|
+
</CommandList>
|
|
283
|
+
</Command>
|
|
284
|
+
</PopoverContent>
|
|
285
|
+
</Popover>
|
|
251
286
|
</div>
|
|
252
287
|
);
|
|
253
288
|
}
|
|
@@ -200,7 +200,14 @@ export function useChatModels({
|
|
|
200
200
|
.catch(() => null),
|
|
201
201
|
])
|
|
202
202
|
.then(([enginesData, envKeys, builderStatus]) => {
|
|
203
|
-
if (!enginesData?.engines)
|
|
203
|
+
if (!enginesData?.engines) {
|
|
204
|
+
// Without a catalog the picker keeps an unvalidated DEFAULT_MODEL,
|
|
205
|
+
// which is indistinguishable from a real selection unless we say so.
|
|
206
|
+
console.warn(
|
|
207
|
+
"[agent-chat] no engine list; model picker is showing an unvalidated default",
|
|
208
|
+
);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
204
211
|
const configuredKeys = new Set(
|
|
205
212
|
(envKeys as Array<{ key: string; configured: boolean }>)
|
|
206
213
|
.filter((k) => k.configured)
|
|
@@ -223,22 +230,45 @@ export function useChatModels({
|
|
|
223
230
|
setDefaultModel(nextDefaultModel);
|
|
224
231
|
|
|
225
232
|
const selection = selectionRef.current;
|
|
233
|
+
|
|
234
|
+
// Default only to a CONFIGURED group, and to nothing when there is
|
|
235
|
+
// none. `DEFAULT_MODEL` is a builder-gateway id that no group carries
|
|
236
|
+
// unless Builder is connected, and unconfigured groups are kept in the
|
|
237
|
+
// list for their connect affordance — so both `?? DEFAULT_MODEL` and
|
|
238
|
+
// `?? groups[0]` yield a selection the app cannot route, which the
|
|
239
|
+
// server silently replaces with its own default. An empty selection
|
|
240
|
+
// hides the picker instead of showing a model that will not be used.
|
|
241
|
+
const configuredGroups = groups.filter((g) => g.configured);
|
|
242
|
+
const resolveRoutableSelection = () => {
|
|
243
|
+
const group =
|
|
244
|
+
configuredGroups.find((g) => g.models.includes(nextDefaultModel)) ??
|
|
245
|
+
configuredGroups[0];
|
|
246
|
+
if (!group) return null;
|
|
247
|
+
const model =
|
|
248
|
+
group.models.find((m) => m === nextDefaultModel) ?? group.models[0];
|
|
249
|
+
if (!model) return null;
|
|
250
|
+
return {
|
|
251
|
+
model,
|
|
252
|
+
engine: group.engine,
|
|
253
|
+
effort: resolveReasoningEffortSelection(
|
|
254
|
+
model,
|
|
255
|
+
selection.selectedEffort,
|
|
256
|
+
),
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const applyFallback = (persist: boolean) => {
|
|
261
|
+
const next = resolveRoutableSelection();
|
|
262
|
+
setSelectedModel(next?.model ?? "");
|
|
263
|
+
setSelectedEngine(next?.engine ?? "");
|
|
264
|
+
if (next) {
|
|
265
|
+
setSelectedEffort(next.effort);
|
|
266
|
+
if (persist) writePersisted(storageKey, next);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
226
270
|
if (!hasExplicitSelectionRef.current) {
|
|
227
|
-
|
|
228
|
-
groups.find((group) => group.models.includes(nextDefaultModel)) ??
|
|
229
|
-
groups[0];
|
|
230
|
-
const nextModel =
|
|
231
|
-
defaultGroup?.models.find((model) => model === nextDefaultModel) ??
|
|
232
|
-
defaultGroup?.models[0] ??
|
|
233
|
-
nextDefaultModel;
|
|
234
|
-
const nextEngine = defaultGroup?.engine ?? "";
|
|
235
|
-
const nextEffort = resolveReasoningEffortSelection(
|
|
236
|
-
nextModel,
|
|
237
|
-
selection.selectedEffort,
|
|
238
|
-
);
|
|
239
|
-
setSelectedModel(nextModel);
|
|
240
|
-
setSelectedEngine(nextEngine);
|
|
241
|
-
setSelectedEffort(nextEffort);
|
|
271
|
+
applyFallback(false);
|
|
242
272
|
return;
|
|
243
273
|
}
|
|
244
274
|
|
|
@@ -248,28 +278,15 @@ export function useChatModels({
|
|
|
248
278
|
(!selection.selectedEngine ||
|
|
249
279
|
group.engine === selection.selectedEngine),
|
|
250
280
|
);
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
nextDefaultModel;
|
|
259
|
-
const nextEngine = defaultGroup?.engine ?? "";
|
|
260
|
-
const nextEffort = resolveReasoningEffortSelection(
|
|
261
|
-
nextModel,
|
|
262
|
-
selection.selectedEffort,
|
|
263
|
-
);
|
|
264
|
-
setSelectedModel(nextModel);
|
|
265
|
-
setSelectedEngine(nextEngine);
|
|
266
|
-
setSelectedEffort(nextEffort);
|
|
267
|
-
writePersisted(storageKey, {
|
|
268
|
-
model: nextModel,
|
|
269
|
-
engine: nextEngine,
|
|
270
|
-
effort: nextEffort,
|
|
271
|
-
});
|
|
281
|
+
if (selectedGroup) {
|
|
282
|
+
// Heal a selection stored without an engine (or with a stale one) so
|
|
283
|
+
// later submits carry the pair the catalog resolved.
|
|
284
|
+
if (selection.selectedEngine !== selectedGroup.engine) {
|
|
285
|
+
setSelectedEngine(selectedGroup.engine);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
272
288
|
}
|
|
289
|
+
applyFallback(true);
|
|
273
290
|
})
|
|
274
291
|
.catch(() => {})
|
|
275
292
|
.finally(() => setIsLoading(false));
|
|
@@ -206,6 +206,7 @@ const messages = {
|
|
|
206
206
|
otherAppsDescription: "Open other apps available to you.",
|
|
207
207
|
availableCount: "{{count}} available",
|
|
208
208
|
activeCount: "{{count}} active",
|
|
209
|
+
pendingCount: "{{count}} pending",
|
|
209
210
|
hiddenCount: "{{count}} hidden",
|
|
210
211
|
createApp: "Create app",
|
|
211
212
|
templates: "Templates",
|
|
@@ -215,6 +216,16 @@ const messages = {
|
|
|
215
216
|
show: "Show",
|
|
216
217
|
hiddenApps: "Hidden apps",
|
|
217
218
|
hiddenAppCount: "{{count}} hidden apps",
|
|
219
|
+
pendingApps: "Pending apps",
|
|
220
|
+
pendingAppsDescription:
|
|
221
|
+
"{{count}} pending apps are hidden by default. Show them only when you need to inspect work in progress.",
|
|
222
|
+
showPendingApps: "Show pending apps",
|
|
223
|
+
hidePendingApps: "Hide pending apps",
|
|
224
|
+
noActiveWorkspaceApps: "No active workspace apps yet",
|
|
225
|
+
noActiveWorkspaceAppsDescription:
|
|
226
|
+
"Pending apps are hidden by default. Expand Pending apps below to inspect them.",
|
|
227
|
+
appMetadataOwner: "Owner",
|
|
228
|
+
appMetadataTeams: "Teams",
|
|
218
229
|
workspaceAppFallback: "Workspace App",
|
|
219
230
|
workspaceAppDescription:
|
|
220
231
|
"Open a deployed app or check the status of an app being created.",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { CompactEncrypt, compactDecrypt } from "jose";
|
|
4
|
+
|
|
5
|
+
import { getAuthSecret } from "../server/better-auth-instance.js";
|
|
6
|
+
|
|
7
|
+
const HANDLE_VERSION = 1;
|
|
8
|
+
const HANDLE_TTL_SECONDS = 60 * 60;
|
|
9
|
+
const INVALID_HANDLE_ERROR = "Invalid or expired ask_app task handle.";
|
|
10
|
+
|
|
11
|
+
export interface AskAppTaskHandleRoute {
|
|
12
|
+
app: string;
|
|
13
|
+
origin: string;
|
|
14
|
+
routedVia: "local" | "a2a";
|
|
15
|
+
requestOrigin?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface AskAppTaskHandleClaims {
|
|
19
|
+
version: number;
|
|
20
|
+
issuerApp: string;
|
|
21
|
+
issuerAudience: string;
|
|
22
|
+
organization: string;
|
|
23
|
+
subject: string;
|
|
24
|
+
route: AskAppTaskHandleRoute;
|
|
25
|
+
taskId: string;
|
|
26
|
+
expiresAt: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizedIdentity(value: string): string {
|
|
30
|
+
return value.trim().toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizedAudience(value: string): string | null {
|
|
34
|
+
const url = validHttpUrl(value);
|
|
35
|
+
if (!url || url.search || url.hash) return null;
|
|
36
|
+
const pathname = url.pathname.replace(/\/+$/, "");
|
|
37
|
+
return `${url.origin}${pathname}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function rootSecrets(): string[] {
|
|
41
|
+
const a2a = process.env.A2A_SECRET?.trim();
|
|
42
|
+
const auth = getAuthSecret();
|
|
43
|
+
return [...new Set([a2a, auth].filter((value): value is string => !!value))];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function handleKey(rootSecret: string): Uint8Array {
|
|
47
|
+
return createHmac("sha256", rootSecret)
|
|
48
|
+
.update("agent-native:ask-app-task-handle:v1")
|
|
49
|
+
.digest();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validHttpUrl(value: string): URL | null {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(value);
|
|
55
|
+
if (!["http:", "https:"].includes(url.protocol)) return null;
|
|
56
|
+
if (url.username || url.password) return null;
|
|
57
|
+
return url;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validRoute(value: unknown): value is AskAppTaskHandleRoute {
|
|
64
|
+
if (!value || typeof value !== "object") return false;
|
|
65
|
+
const route = value as Partial<AskAppTaskHandleRoute>;
|
|
66
|
+
if (typeof route.app !== "string" || !route.app.trim()) return false;
|
|
67
|
+
if (route.routedVia !== "local" && route.routedVia !== "a2a") return false;
|
|
68
|
+
if (typeof route.origin !== "string") return false;
|
|
69
|
+
const endpoint = validHttpUrl(route.origin);
|
|
70
|
+
if (!endpoint) return false;
|
|
71
|
+
if (route.requestOrigin == null) return true;
|
|
72
|
+
if (typeof route.requestOrigin !== "string") return false;
|
|
73
|
+
const requestOrigin = validHttpUrl(route.requestOrigin);
|
|
74
|
+
return requestOrigin?.origin === endpoint.origin;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function invalidHandle(): Error {
|
|
78
|
+
return new Error(INVALID_HANDLE_ERROR);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function signAskAppTaskHandle(params: {
|
|
82
|
+
issuerApp: string;
|
|
83
|
+
issuerAudience: string;
|
|
84
|
+
organization: string;
|
|
85
|
+
subject: string;
|
|
86
|
+
route: AskAppTaskHandleRoute;
|
|
87
|
+
taskId: string;
|
|
88
|
+
expiresInSeconds?: number;
|
|
89
|
+
}): Promise<string> {
|
|
90
|
+
const issuerApp = normalizedIdentity(params.issuerApp);
|
|
91
|
+
const issuerAudience = normalizedAudience(params.issuerAudience);
|
|
92
|
+
const organization = normalizedIdentity(params.organization);
|
|
93
|
+
const subject = normalizedIdentity(params.subject);
|
|
94
|
+
const taskId = params.taskId.trim();
|
|
95
|
+
if (
|
|
96
|
+
!issuerApp ||
|
|
97
|
+
!issuerAudience ||
|
|
98
|
+
!subject ||
|
|
99
|
+
!taskId ||
|
|
100
|
+
!validRoute(params.route)
|
|
101
|
+
) {
|
|
102
|
+
throw invalidHandle();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const claims: AskAppTaskHandleClaims = {
|
|
106
|
+
version: HANDLE_VERSION,
|
|
107
|
+
issuerApp,
|
|
108
|
+
issuerAudience,
|
|
109
|
+
organization,
|
|
110
|
+
subject,
|
|
111
|
+
route: {
|
|
112
|
+
...params.route,
|
|
113
|
+
app: normalizedIdentity(params.route.app),
|
|
114
|
+
},
|
|
115
|
+
taskId,
|
|
116
|
+
expiresAt:
|
|
117
|
+
Math.floor(Date.now() / 1000) +
|
|
118
|
+
(params.expiresInSeconds ?? HANDLE_TTL_SECONDS),
|
|
119
|
+
};
|
|
120
|
+
const payload = new TextEncoder().encode(JSON.stringify(claims));
|
|
121
|
+
return new CompactEncrypt(payload)
|
|
122
|
+
.setProtectedHeader({
|
|
123
|
+
alg: "dir",
|
|
124
|
+
enc: "A256GCM",
|
|
125
|
+
typ: "agent-native-ask-app-task-handle",
|
|
126
|
+
})
|
|
127
|
+
.encrypt(handleKey(rootSecrets()[0]));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function verifyAskAppTaskHandle(
|
|
131
|
+
handle: string,
|
|
132
|
+
expected: {
|
|
133
|
+
issuerApp: string;
|
|
134
|
+
issuerAudience: string;
|
|
135
|
+
organization: string;
|
|
136
|
+
subject: string;
|
|
137
|
+
},
|
|
138
|
+
): Promise<{ route: AskAppTaskHandleRoute; taskId: string }> {
|
|
139
|
+
const issuerApp = normalizedIdentity(expected.issuerApp);
|
|
140
|
+
const issuerAudience = normalizedAudience(expected.issuerAudience);
|
|
141
|
+
const organization = normalizedIdentity(expected.organization);
|
|
142
|
+
const subject = normalizedIdentity(expected.subject);
|
|
143
|
+
if (!handle.trim() || !issuerApp || !issuerAudience || !subject) {
|
|
144
|
+
throw invalidHandle();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const rootSecret of rootSecrets()) {
|
|
148
|
+
try {
|
|
149
|
+
const { plaintext, protectedHeader } = await compactDecrypt(
|
|
150
|
+
handle,
|
|
151
|
+
handleKey(rootSecret),
|
|
152
|
+
);
|
|
153
|
+
if (
|
|
154
|
+
protectedHeader.alg !== "dir" ||
|
|
155
|
+
protectedHeader.enc !== "A256GCM" ||
|
|
156
|
+
protectedHeader.typ !== "agent-native-ask-app-task-handle"
|
|
157
|
+
) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const claims = JSON.parse(
|
|
161
|
+
new TextDecoder().decode(plaintext),
|
|
162
|
+
) as Partial<AskAppTaskHandleClaims>;
|
|
163
|
+
if (
|
|
164
|
+
claims.version !== HANDLE_VERSION ||
|
|
165
|
+
claims.issuerApp !== issuerApp ||
|
|
166
|
+
claims.issuerAudience !== issuerAudience ||
|
|
167
|
+
claims.organization !== organization ||
|
|
168
|
+
claims.subject !== subject ||
|
|
169
|
+
typeof claims.taskId !== "string" ||
|
|
170
|
+
!claims.taskId ||
|
|
171
|
+
typeof claims.expiresAt !== "number" ||
|
|
172
|
+
claims.expiresAt <= Math.floor(Date.now() / 1000) ||
|
|
173
|
+
!validRoute(claims.route)
|
|
174
|
+
) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
return { route: claims.route, taskId: claims.taskId };
|
|
178
|
+
} catch {
|
|
179
|
+
// All handle failures are deliberately indistinguishable to callers.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
throw invalidHandle();
|
|
183
|
+
}
|