@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
|
@@ -1569,6 +1569,73 @@ export function createCoreRoutesPlugin(
|
|
|
1569
1569
|
// middleware any plugin's route can possibly land behind, regardless of
|
|
1570
1570
|
// plugin init ordering.
|
|
1571
1571
|
|
|
1572
|
+
// Peer reachability + auth probe for the settings UI. Deliberately
|
|
1573
|
+
// separate from `${P}/agents` (discovery) — this route makes live
|
|
1574
|
+
// network calls to the peer, so it is session-gated and answers one
|
|
1575
|
+
// peer (`?url=`) or every registered peer (no query) via `discoverAgents`.
|
|
1576
|
+
//
|
|
1577
|
+
// MUST be mounted BEFORE `${P}/agents` below: h3's `.use()` matches by
|
|
1578
|
+
// path prefix, and that handler always returns a value (never calls
|
|
1579
|
+
// `next()`), so it would swallow `/agents/probe` requests before they
|
|
1580
|
+
// ever reached this route if registered second (same hazard as the A2A
|
|
1581
|
+
// `_process-task` route vs. its `/a2a` catch-all — see a2a/server.ts).
|
|
1582
|
+
getH3App(nitroApp).use(
|
|
1583
|
+
`${P}/agents/probe`,
|
|
1584
|
+
defineEventHandler(async (event) => {
|
|
1585
|
+
if (getMethod(event) !== "GET") {
|
|
1586
|
+
setResponseStatus(event, 405);
|
|
1587
|
+
return { error: "Method not allowed" };
|
|
1588
|
+
}
|
|
1589
|
+
const session = await getSession(event).catch(() => null);
|
|
1590
|
+
if (!session?.email) {
|
|
1591
|
+
setResponseStatus(event, 401);
|
|
1592
|
+
return { error: "Authentication required" };
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
return runWithRequestContext(
|
|
1596
|
+
{ userEmail: session.email, orgId: session.orgId ?? undefined },
|
|
1597
|
+
async () => {
|
|
1598
|
+
const { probePeerAgent, probeAllPeerAgents } =
|
|
1599
|
+
await import("./agent-peer-probe.js");
|
|
1600
|
+
const query = getRequestURL(event).searchParams;
|
|
1601
|
+
const urlParam = query.get("url");
|
|
1602
|
+
|
|
1603
|
+
if (urlParam === null) {
|
|
1604
|
+
const selfAppId = query.get("selfAppId") ?? undefined;
|
|
1605
|
+
const { discoverAgents } = await import("./agent-discovery.js");
|
|
1606
|
+
const agents = await discoverAgents(selfAppId);
|
|
1607
|
+
const results = await probeAllPeerAgents(agents);
|
|
1608
|
+
return { results };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (!urlParam.trim()) {
|
|
1612
|
+
setResponseStatus(event, 400);
|
|
1613
|
+
return { error: "url is required" };
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const result = await probePeerAgent({
|
|
1617
|
+
id: "probe",
|
|
1618
|
+
name: urlParam,
|
|
1619
|
+
description: "",
|
|
1620
|
+
url: urlParam,
|
|
1621
|
+
color: "",
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
// Reachability and auth are independent, but a malformed/SSRF-blocked
|
|
1625
|
+
// URL is a caller input error, not a peer that failed to answer — the
|
|
1626
|
+
// one case where the probe's "unreachable" result is reclassified into
|
|
1627
|
+
// a 400 instead of a 200 with `reachable: false`.
|
|
1628
|
+
if (result.error?.startsWith("SSRF blocked:")) {
|
|
1629
|
+
setResponseStatus(event, 400);
|
|
1630
|
+
return { url: urlParam, error: result.error };
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
return result;
|
|
1634
|
+
},
|
|
1635
|
+
);
|
|
1636
|
+
}),
|
|
1637
|
+
);
|
|
1638
|
+
|
|
1572
1639
|
// Agent discovery primitive — shared by headless CLI/A2A surfaces and
|
|
1573
1640
|
// UI shells that need to show connected peer apps without depending on
|
|
1574
1641
|
// the chat route namespace.
|
|
@@ -134,9 +134,13 @@ export function shouldAllowMcpEmbedCredentials(
|
|
|
134
134
|
);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// These paths are served straight off the CDN, so the security-headers h3
|
|
138
|
+
// middleware never runs for them — anything it sets that must also hold for
|
|
139
|
+
// static assets has to be repeated here.
|
|
137
140
|
export const MCP_EMBED_STATIC_ASSET_HEADERS = {
|
|
138
141
|
"Access-Control-Allow-Origin": "*",
|
|
139
142
|
"Cross-Origin-Resource-Policy": "cross-origin",
|
|
143
|
+
"X-Content-Type-Options": "nosniff",
|
|
140
144
|
} as const;
|
|
141
145
|
|
|
142
146
|
const STATIC_ASSET_PATTERNS = [
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: a2a-protocol
|
|
3
3
|
description: >-
|
|
4
|
-
How
|
|
5
|
-
Use when
|
|
6
|
-
|
|
4
|
+
How agent-native apps discover, authenticate with, and call each other over
|
|
5
|
+
A2A. Use when connecting one app to another, debugging a remote agent that
|
|
6
|
+
won't authenticate, exposing skills to peers, or calling agents from scripts.
|
|
7
7
|
scope: dev
|
|
8
8
|
metadata:
|
|
9
9
|
internal: true
|
|
@@ -13,110 +13,155 @@ metadata:
|
|
|
13
13
|
|
|
14
14
|
## Rule
|
|
15
15
|
|
|
16
|
-
Agents
|
|
16
|
+
Agents call other agents over A2A, a JSON-RPC protocol for discovery and
|
|
17
|
+
delegation. Use it when work belongs to a different agent entirely — not the
|
|
18
|
+
local agent chat.
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
Connecting app A to app B is two independent things, and both must be true:
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
1. **B is registered on A** as a `remote-agents/<id>.json` resource.
|
|
23
|
+
2. **A and B share a secret**, so A's signed JWT verifies on B.
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
Neither is a code change, and neither is symmetric. Registering B on A does not
|
|
26
|
+
let B call A.
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
## A2A is already mounted
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
import { mountA2A } from "@agent-native/core/a2a";
|
|
29
|
-
|
|
30
|
-
export default defineNitroPlugin((nitro) => {
|
|
31
|
-
const app = nitro.h3App;
|
|
32
|
-
|
|
33
|
-
mountA2A(app, {
|
|
34
|
-
name: "Analytics Agent",
|
|
35
|
-
description: "Queries analytics data across providers",
|
|
36
|
-
version: "1.0.0",
|
|
37
|
-
skills: [
|
|
38
|
-
{
|
|
39
|
-
id: "query-data",
|
|
40
|
-
name: "Query Data",
|
|
41
|
-
description: "Run analytics queries across connected data sources",
|
|
42
|
-
tags: ["analytics", "data"],
|
|
43
|
-
examples: ["What were last week's signups?", "Show conversion rates"],
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
apiKeyEnv: "A2A_API_KEY", // env var holding the bearer token
|
|
47
|
-
streaming: true, // enable message/stream method
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
This mounts the agent-native A2A endpoints:
|
|
53
|
-
|
|
54
|
-
- `GET /.well-known/agent-card.json` — public agent discovery (no auth required)
|
|
55
|
-
- `POST /_agent-native/a2a` — primary JSON-RPC endpoint (bearer token auth required)
|
|
30
|
+
`createAgentChatPlugin` calls `mountA2A` for every app, so a generated app
|
|
31
|
+
already serves:
|
|
56
32
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`/_agent-native/a2a` endpoint.
|
|
33
|
+
- `GET /.well-known/agent-card.json` — public discovery, never authenticated
|
|
34
|
+
- `POST /_agent-native/a2a` — JSON-RPC, authenticated
|
|
60
35
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
interface A2AConfig {
|
|
65
|
-
name: string; // agent display name
|
|
66
|
-
description: string; // what this agent does
|
|
67
|
-
version?: string; // semver version (default: "1.0.0")
|
|
68
|
-
skills: AgentSkill[]; // capabilities this agent exposes
|
|
69
|
-
handler?: A2AHandler; // custom message handler
|
|
70
|
-
apiKeyEnv?: string; // env var name for bearer token auth
|
|
71
|
-
streaming?: boolean; // enable streaming responses
|
|
72
|
-
}
|
|
36
|
+
Do not add a `mountA2A` server plugin to enable A2A; it is on. Hand-mounting is
|
|
37
|
+
only for a bespoke card or a custom handler (see **Custom mount** below).
|
|
73
38
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
name: string; // human-readable name
|
|
77
|
-
description: string; // what this skill does
|
|
78
|
-
tags?: string[]; // categorization tags
|
|
79
|
-
examples?: string[]; // example prompts
|
|
80
|
-
}
|
|
81
|
-
```
|
|
39
|
+
The client falls back to `POST /a2a` for external peers that only expose that
|
|
40
|
+
path. New agent-native apps should call `/_agent-native/a2a`.
|
|
82
41
|
|
|
83
|
-
##
|
|
42
|
+
## Registering a remote agent
|
|
84
43
|
|
|
85
|
-
|
|
44
|
+
A remote agent is **a row in the resources table, not a file on disk**. Path
|
|
45
|
+
`remote-agents/<id>.json`, owner `SHARED_OWNER`, content:
|
|
86
46
|
|
|
87
47
|
```json
|
|
88
48
|
{
|
|
89
|
-
"
|
|
49
|
+
"id": "analytics",
|
|
50
|
+
"name": "Analytics",
|
|
90
51
|
"description": "Queries analytics data across providers",
|
|
91
52
|
"url": "https://analytics.example.com",
|
|
92
|
-
"
|
|
93
|
-
"protocolVersion": "0.3",
|
|
94
|
-
"capabilities": { "streaming": true },
|
|
95
|
-
"skills": [
|
|
96
|
-
{
|
|
97
|
-
"id": "query-data",
|
|
98
|
-
"name": "Query Data",
|
|
99
|
-
"description": "Run analytics queries across connected data sources"
|
|
100
|
-
}
|
|
101
|
-
]
|
|
53
|
+
"color": "#6B7280"
|
|
102
54
|
}
|
|
103
55
|
```
|
|
104
56
|
|
|
105
|
-
|
|
57
|
+
`url` is the only required field. `parseRemoteAgentManifest` accepts **only**
|
|
58
|
+
these five keys — there is no `apiKey`, `env`, `skills`, or `token` field, and
|
|
59
|
+
anything else is silently dropped.
|
|
60
|
+
|
|
61
|
+
Four ways to create it, all writing the same row:
|
|
62
|
+
|
|
63
|
+
| Surface | Where |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| Settings → Manage agent → Connected Agents (A2A) | any app with the settings panel |
|
|
66
|
+
| Agent page → Connections | apps mounting `AgentTabsPage` |
|
|
67
|
+
| Dispatch → Agents → Add external agent | workspace dispatch |
|
|
68
|
+
| The agent itself | `resources` tool, `action: "write"`, `--scope shared` |
|
|
69
|
+
|
|
70
|
+
The last one matters for template apps with no Code tab: "connect me to
|
|
71
|
+
https://analytics.example.com" is a complete instruction. `remote-agents/` is
|
|
72
|
+
whitelisted as a durable control path, so the write is workspace-scoped, and it
|
|
73
|
+
takes effect on the next `discoverAgents()` call with no restart.
|
|
74
|
+
|
|
75
|
+
First-party templates are seeded automatically and overlaid on top of manifests,
|
|
76
|
+
so `Mail`, `Calendar`, and friends appear without registration. In dev they
|
|
77
|
+
resolve to `http://localhost:<devPort>`; a manifest pointing at localhost is
|
|
78
|
+
ignored in production.
|
|
79
|
+
|
|
80
|
+
`call-agent` also accepts a raw URL, which skips registration entirely — useful
|
|
81
|
+
for one-off calls, useless for @-mentions.
|
|
82
|
+
|
|
83
|
+
### Checking a peer
|
|
84
|
+
|
|
85
|
+
`GET /_agent-native/agents/probe?url=<base>` reads the peer's card and makes one
|
|
86
|
+
authenticated no-op call, returning `reachable` and `authorized` as independent
|
|
87
|
+
fields. Omit `url` to probe every registered peer. Reachable-but-unauthorized is
|
|
88
|
+
the failure worth looking for: local dev runs the receiver unauthenticated, so a
|
|
89
|
+
mismatched secret only surfaces after deploy. The Connected Agents settings
|
|
90
|
+
section calls this on add and on open.
|
|
91
|
+
|
|
92
|
+
## Authentication
|
|
93
|
+
|
|
94
|
+
A2A authenticates with a **short-lived JWT the caller signs**, not a stored
|
|
95
|
+
bearer key. `Authorization: Bearer <jwt>`; claims carry the caller's email and
|
|
96
|
+
org domain; HS256; 15-minute default TTL. There is no per-peer API key
|
|
97
|
+
anywhere in the system, which is why the manifest has no field for one.
|
|
98
|
+
|
|
99
|
+
Two secrets can sign it:
|
|
100
|
+
|
|
101
|
+
| Secret | Scope | How it is set |
|
|
102
|
+
| --- | --- | --- |
|
|
103
|
+
| `A2A_SECRET` | whole deployment | env var, **never auto-generated** |
|
|
104
|
+
| org `a2a_secret` | one organization | auto-generated on org creation; Team page UI |
|
|
105
|
+
|
|
106
|
+
The org secret is the managed path: **Team page → A2A secret** (owner only)
|
|
107
|
+
reveals, copies, regenerates, and pushes it to every discovered peer. Rotation
|
|
108
|
+
is tolerated — the caller tries both secrets in order and sticks to whichever
|
|
109
|
+
worked.
|
|
110
|
+
|
|
111
|
+
`A2A_SECRET` is the deploy-level path. `workspace-deploy` refuses a production
|
|
112
|
+
workspace deploy without it and prints the generator command. Peers that must
|
|
113
|
+
trust each other need the *same* value on both sides.
|
|
114
|
+
|
|
115
|
+
With no secret configured at all:
|
|
116
|
+
|
|
117
|
+
| Runtime | Result |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| local dev | open, one-time console warning |
|
|
120
|
+
| production | `503` — "A2A authentication not configured" |
|
|
121
|
+
| production, secret set, bad/absent token | `401` |
|
|
122
|
+
|
|
123
|
+
"Production" is detected broadly (NODE_ENV, Netlify, Lambda, Vercel, Render,
|
|
124
|
+
Fly, Cloud Run, …) and unrecognized deployed hosts fail closed unless the
|
|
125
|
+
request is genuine loopback or `A2A_ALLOW_UNSIGNED_INTERNAL=1`.
|
|
126
|
+
|
|
127
|
+
`A2AConfig.apiKeyEnv` still exists for static bearer auth against non-agent-native
|
|
128
|
+
peers, but the framework's own mount never sets it. Do not reach for it when
|
|
129
|
+
debugging a connection between two agent-native apps — the answer there is
|
|
130
|
+
always the shared secret.
|
|
131
|
+
|
|
132
|
+
Never hardcode either secret in source, docs, prompts, app state, action
|
|
133
|
+
descriptions, client bundles, or examples. Read them from runtime config; never
|
|
134
|
+
log or return them.
|
|
135
|
+
|
|
136
|
+
## Advertising what this agent can do
|
|
137
|
+
|
|
138
|
+
Card `skills` are derived from actions marked `publicAgent`. An app that marks
|
|
139
|
+
none publishes `"skills": []` and peers cannot see what it offers, even though
|
|
140
|
+
delegation still works. Mark the actions a peer should be able to see and
|
|
141
|
+
invoke directly; leave the rest internal.
|
|
142
|
+
|
|
143
|
+
## Calling another agent
|
|
106
144
|
|
|
107
145
|
### Simple: `callAgent()` (text in, text out)
|
|
108
146
|
|
|
109
147
|
```ts
|
|
110
|
-
import { callAgent } from "@agent-native/core/a2a";
|
|
148
|
+
import { callAgent, resolveA2ACallerAuth } from "@agent-native/core/a2a";
|
|
111
149
|
|
|
150
|
+
const auth = await resolveA2ACallerAuth();
|
|
112
151
|
const answer = await callAgent(
|
|
113
152
|
"https://analytics.example.com",
|
|
114
153
|
"What were last week's signups?",
|
|
115
|
-
|
|
154
|
+
auth,
|
|
116
155
|
);
|
|
117
156
|
// answer is a plain string
|
|
118
157
|
```
|
|
119
158
|
|
|
159
|
+
`callAgent` signs nothing on its own — with no `userEmail`/`orgSecret` in opts
|
|
160
|
+
it calls **unauthenticated**, which silently works in local dev and 401s in
|
|
161
|
+
production. `resolveA2ACallerAuth()` pulls the authenticated email, org domain,
|
|
162
|
+
and org secret out of request context; pass them explicitly only from CLI or
|
|
163
|
+
cron, where there is no request.
|
|
164
|
+
|
|
120
165
|
### Fast bounded read: `invokeAgentAction()`
|
|
121
166
|
|
|
122
167
|
When the caller knows the exact receiver-owned read action and arguments, skip
|
|
@@ -168,12 +213,13 @@ action arguments or results.
|
|
|
168
213
|
### Advanced: `A2AClient` (full control)
|
|
169
214
|
|
|
170
215
|
```ts
|
|
171
|
-
import { A2AClient } from "@agent-native/core/a2a";
|
|
216
|
+
import { A2AClient, resolveA2ACallerAuth } from "@agent-native/core/a2a";
|
|
172
217
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
218
|
+
// The second argument is the bearer token itself, not a signing secret.
|
|
219
|
+
const { apiKey, apiKeyFallbacks } = await resolveA2ACallerAuth();
|
|
220
|
+
const client = new A2AClient("https://analytics.example.com", apiKey, {
|
|
221
|
+
fallbackApiKeys: apiKeyFallbacks,
|
|
222
|
+
});
|
|
177
223
|
|
|
178
224
|
// Discover agent capabilities
|
|
179
225
|
const card = await client.getAgentCard();
|
|
@@ -264,19 +310,6 @@ submitted → working → completed
|
|
|
264
310
|
- **canceled** — task was canceled via `tasks/cancel`
|
|
265
311
|
- **input-required** — agent needs more information from the caller
|
|
266
312
|
|
|
267
|
-
## Security
|
|
268
|
-
|
|
269
|
-
A2A uses bearer token auth. The server reads the token from the environment variable specified by `apiKeyEnv`:
|
|
270
|
-
|
|
271
|
-
- Set `A2A_API_KEY=<A2A_API_KEY_VALUE>` in the server's deployment environment
|
|
272
|
-
- Callers pass it as `Authorization: Bearer <A2A_API_KEY_VALUE>`
|
|
273
|
-
- The agent card endpoint (`/.well-known/agent-card.json`) is public — no auth needed for discovery
|
|
274
|
-
|
|
275
|
-
Never hardcode the bearer token in source, docs, prompts, app state, action
|
|
276
|
-
descriptions, client bundles, or examples. A2A tokens are deploy-level secrets
|
|
277
|
-
unless a specific app designs a scoped credential flow; read them from secure
|
|
278
|
-
runtime configuration and never log or return them.
|
|
279
|
-
|
|
280
313
|
## Message Parts
|
|
281
314
|
|
|
282
315
|
Messages contain typed parts:
|
|
@@ -287,34 +320,38 @@ Messages contain typed parts:
|
|
|
287
320
|
| `file` | `{ type: "file", file: { ... } }` | Files (bytes or URI) |
|
|
288
321
|
| `data` | `{ type: "data", data: { ... } }` | Structured JSON data |
|
|
289
322
|
|
|
290
|
-
##
|
|
323
|
+
## Custom mount
|
|
291
324
|
|
|
292
|
-
|
|
325
|
+
Only when the default card is wrong for the app — a curated skill list, a
|
|
326
|
+
non-agent handler, or static bearer auth against an external peer:
|
|
293
327
|
|
|
294
328
|
```ts
|
|
295
|
-
//
|
|
296
|
-
import {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
mode: "compose",
|
|
329
|
+
// server/plugins/a2a.ts
|
|
330
|
+
import { mountA2A } from "@agent-native/core/a2a";
|
|
331
|
+
|
|
332
|
+
export default defineNitroPlugin((nitro) => {
|
|
333
|
+
mountA2A(nitro, {
|
|
334
|
+
appId: "analytics",
|
|
335
|
+
name: "Analytics Agent",
|
|
336
|
+
description: "Queries analytics data across providers",
|
|
337
|
+
skills: [
|
|
338
|
+
{
|
|
339
|
+
id: "query-data",
|
|
340
|
+
name: "Query Data",
|
|
341
|
+
description: "Run analytics queries across connected data sources",
|
|
342
|
+
tags: ["analytics", "data"],
|
|
343
|
+
examples: ["What were last week's signups?", "Show conversion rates"],
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
streaming: true,
|
|
314
347
|
});
|
|
315
|
-
}
|
|
348
|
+
});
|
|
316
349
|
```
|
|
317
350
|
|
|
351
|
+
Config also carries `handler`, `publicSkillsOnly`, `durableBackgroundRuns`,
|
|
352
|
+
`executeReadOnlyAction`, `executeApproval`, and `apiKeyEnv`. See `A2AConfig` in
|
|
353
|
+
`@agent-native/core/a2a` for the full shape.
|
|
354
|
+
|
|
318
355
|
## All Types
|
|
319
356
|
|
|
320
357
|
All types are exported from `@agent-native/core/a2a`:
|
|
@@ -345,5 +382,8 @@ import type {
|
|
|
345
382
|
## Related Skills
|
|
346
383
|
|
|
347
384
|
- **delegate-to-agent** — For work the local agent handles. Use A2A when the work goes to a different agent.
|
|
348
|
-
- **
|
|
349
|
-
- **
|
|
385
|
+
- **authentication** — Org creation, membership, and where the org secret lives.
|
|
386
|
+
- **actions** — A2A calls typically happen inside actions; `publicAgent` marks what peers can see.
|
|
387
|
+
- **storing-data** — Results from A2A calls are stored in SQL like any other data.
|
|
388
|
+
</content>
|
|
389
|
+
</invoke>
|
|
@@ -377,6 +377,29 @@ view only when the user asked to focus one specific screen.
|
|
|
377
377
|
|
|
378
378
|
## HTML Structure Requirements
|
|
379
379
|
|
|
380
|
+
### The save gate rejects malformed markup
|
|
381
|
+
|
|
382
|
+
Every path that persists model-authored HTML — `generate-design`, `create-file`,
|
|
383
|
+
`present-design-variants`, `edit-design`, `update-file`, canvas edits — runs an
|
|
384
|
+
integrity check, and a **new** file is held to it strictly. Import paths
|
|
385
|
+
(Figma, localhost, templates, duplication) deliberately do not fail closed:
|
|
386
|
+
that markup comes from a real app or an existing design, and rejecting it would
|
|
387
|
+
block bringing work in rather than prevent a bad generation.
|
|
388
|
+
Rejections name the file, line, column, and offending source line — fix exactly
|
|
389
|
+
what it points at instead of re-sending the payload differently. Blocked: an
|
|
390
|
+
attribute quote that is never closed; an unclosed element or a closing tag with
|
|
391
|
+
no opener; content ending mid-tag (a payload cut off in transit); a
|
|
392
|
+
`<style>`/`<script>` missing its opener or closer; duplicated editor-managed
|
|
393
|
+
style blocks; more than one `<html>`/`<body>`.
|
|
394
|
+
|
|
395
|
+
This exists because the HTML parser never throws — it recovers silently and
|
|
396
|
+
renders a page, just not the one you wrote. An unterminated quote in `<head>`
|
|
397
|
+
swallows every following tag into that attribute, so the Tailwind runtime stops
|
|
398
|
+
applying and the screen renders unstyled with no console error to show it.
|
|
399
|
+
|
|
400
|
+
Saves may also return non-blocking `warnings` (e.g. utility classes with no
|
|
401
|
+
reachable Tailwind runtime). Fix those before reporting the design as ready.
|
|
402
|
+
|
|
380
403
|
### Mandatory Elements
|
|
381
404
|
|
|
382
405
|
Every `index.html` must include:
|
|
@@ -6,6 +6,10 @@ import { nanoid } from "nanoid";
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
8
|
import { getDb, schema } from "../server/db/index.js";
|
|
9
|
+
import {
|
|
10
|
+
assertDesignHtmlCreateIntegrity,
|
|
11
|
+
describeDesignHtmlIntegrityIssue,
|
|
12
|
+
} from "../shared/html-integrity.js";
|
|
9
13
|
import { annotateScreenHtmlForPersist } from "../shared/screen-annotation.js";
|
|
10
14
|
|
|
11
15
|
export default defineAction({
|
|
@@ -63,6 +67,14 @@ export default defineAction({
|
|
|
63
67
|
// the first time someone opens it.
|
|
64
68
|
const annotatedContent = annotateScreenHtmlForPersist(content, fileType);
|
|
65
69
|
|
|
70
|
+
// Reject malformed HTML before the row exists — creation went through raw
|
|
71
|
+
// inserts, so it was the one write path with no integrity gate.
|
|
72
|
+
const advisory = assertDesignHtmlCreateIntegrity({
|
|
73
|
+
content: annotatedContent,
|
|
74
|
+
fileType: fileType ?? "html",
|
|
75
|
+
filename,
|
|
76
|
+
});
|
|
77
|
+
|
|
66
78
|
await db.insert(schema.designFiles).values({
|
|
67
79
|
id,
|
|
68
80
|
designId,
|
|
@@ -94,6 +106,9 @@ export default defineAction({
|
|
|
94
106
|
fileType: resolvedFileType,
|
|
95
107
|
renderable,
|
|
96
108
|
urlPath: renderable ? `/design/${designId}` : null,
|
|
109
|
+
...(advisory.length > 0
|
|
110
|
+
? { warnings: advisory.map(describeDesignHtmlIntegrityIssue) }
|
|
111
|
+
: {}),
|
|
97
112
|
};
|
|
98
113
|
},
|
|
99
114
|
});
|
|
@@ -45,6 +45,11 @@ import {
|
|
|
45
45
|
type DesignGenerationSession,
|
|
46
46
|
updateGenerationSessionWithSavedFiles,
|
|
47
47
|
} from "../shared/generation-session.js";
|
|
48
|
+
import {
|
|
49
|
+
assertDesignHtmlCreateIntegrity,
|
|
50
|
+
describeDesignHtmlIntegrityIssue,
|
|
51
|
+
inspectDesignHtmlDocumentIntegrity,
|
|
52
|
+
} from "../shared/html-integrity.js";
|
|
48
53
|
import { assertLockedLayersPreserved } from "../shared/locked-layers.js";
|
|
49
54
|
import { widthToPrefix } from "../shared/responsive-classes.js";
|
|
50
55
|
import { annotateScreenHtmlForPersist } from "../shared/screen-annotation.js";
|
|
@@ -769,6 +774,38 @@ const generateDesignAction = defineAction({
|
|
|
769
774
|
content: annotateScreenHtmlForPersist(file.content, file.fileType),
|
|
770
775
|
}));
|
|
771
776
|
|
|
777
|
+
// Gate every NEW file up front so a rejected file cannot orphan the ones
|
|
778
|
+
// written before it. Existing files take the edit transition inside
|
|
779
|
+
// writeInlineSourceFile, which still allows repairing a malformed screen.
|
|
780
|
+
const integrityWarnings: Array<{ filename: string; message: string }> = [];
|
|
781
|
+
for (const file of annotatedFiles) {
|
|
782
|
+
if ((file.fileType ?? "html") !== "html") continue;
|
|
783
|
+
const existing = existingByName.get(file.filename);
|
|
784
|
+
// A row changing type into HTML is new HTML, not an edit: the edit
|
|
785
|
+
// transition validates against the row's CURRENT type, so a css→html
|
|
786
|
+
// candidate would skip the HTML checks and still be stored as HTML below.
|
|
787
|
+
const becomesHtml =
|
|
788
|
+
existing !== undefined && (existing.fileType ?? "html") !== "html";
|
|
789
|
+
// Blocking applies to new files and type transitions. An existing HTML row
|
|
790
|
+
// takes the lenient edit transition instead, so a legacy-malformed screen
|
|
791
|
+
// stays repairable — but its advisories are still reported, or the same
|
|
792
|
+
// content would warn as a new file and save silently as a regeneration.
|
|
793
|
+
const advisory =
|
|
794
|
+
!existing || becomesHtml
|
|
795
|
+
? assertDesignHtmlCreateIntegrity({
|
|
796
|
+
content: file.content,
|
|
797
|
+
fileType: "html",
|
|
798
|
+
filename: file.filename,
|
|
799
|
+
})
|
|
800
|
+
: (inspectDesignHtmlDocumentIntegrity(file.content).advisory ?? []);
|
|
801
|
+
for (const entry of advisory) {
|
|
802
|
+
integrityWarnings.push({
|
|
803
|
+
filename: file.filename,
|
|
804
|
+
message: describeDesignHtmlIntegrityIssue(entry),
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
772
809
|
for (const file of annotatedFiles) {
|
|
773
810
|
const existing = existingByName.get(file.filename);
|
|
774
811
|
if (existing) {
|
|
@@ -1198,6 +1235,9 @@ const generateDesignAction = defineAction({
|
|
|
1198
1235
|
savedFiles,
|
|
1199
1236
|
placedFrames,
|
|
1200
1237
|
fileCount: savedFiles.length,
|
|
1238
|
+
// Non-blocking: a well-formed screen with no Tailwind runtime renders
|
|
1239
|
+
// unstyled, which reads as a layout bug rather than a missing runtime.
|
|
1240
|
+
...(integrityWarnings.length > 0 ? { warnings: integrityWarnings } : {}),
|
|
1201
1241
|
...creativeContextProvenance,
|
|
1202
1242
|
};
|
|
1203
1243
|
},
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type CanvasFramePlacement,
|
|
20
20
|
} from "../shared/canvas-frames.js";
|
|
21
21
|
import { isUniqueConstraintViolation } from "../shared/db-conflict.js";
|
|
22
|
+
import { assertDesignHtmlWellFormed } from "../shared/html-integrity.js";
|
|
22
23
|
import { widthToPrefix } from "../shared/responsive-classes.js";
|
|
23
24
|
import { annotateScreenHtmlForPersist } from "../shared/screen-annotation.js";
|
|
24
25
|
|
|
@@ -826,6 +827,21 @@ export default defineAction({
|
|
|
826
827
|
run: async ({ designId, prompt, variants, deleteSupersededSetIds }) => {
|
|
827
828
|
await assertAccess("design", designId, "editor");
|
|
828
829
|
|
|
830
|
+
// Before any mutation. These are model-authored screens created by raw
|
|
831
|
+
// insert, so they need the same well-formedness gate as generate-design —
|
|
832
|
+
// though not its document-shape rules, since a variant may be a sketch with
|
|
833
|
+
// `<html>`/`<body>` implied. Ordering is the load-bearing part: the
|
|
834
|
+
// supersession and deletion below are irreversible, so throwing after them
|
|
835
|
+
// would destroy existing variant sets and create nothing to replace them.
|
|
836
|
+
for (const variant of variants) {
|
|
837
|
+
const candidate = variant.content?.trim();
|
|
838
|
+
if (!candidate) continue;
|
|
839
|
+
assertDesignHtmlWellFormed({
|
|
840
|
+
content: annotateScreenHtmlForPersist(candidate, "html"),
|
|
841
|
+
filename: `variant-${slugify(variant.label.trim(), "option")}.html`,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
829
845
|
// Non-destructive bookkeeping: flag earlier still-complete variant sets
|
|
830
846
|
// as superseded. Files are NEVER deleted automatically — a user's pick
|
|
831
847
|
// exists only as a chat instruction until the agent's delete-file turn
|