@agent-native/core 0.145.3 → 0.145.5

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.
Files changed (51) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/plugins/agent-chat.ts +10 -8
  3. package/corpus/templates/assets/server/plugins/agent-chat.ts +1 -1
  4. package/corpus/templates/calendar/server/plugins/agent-chat.ts +1 -1
  5. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +8 -3
  6. package/corpus/templates/clips/app/components/editor/waveform.tsx +2 -1
  7. package/corpus/templates/clips/app/components/library/search-bar.tsx +1 -1
  8. package/corpus/templates/clips/app/global.css +9 -0
  9. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +22 -1
  10. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +10 -11
  11. package/corpus/templates/design/actions/view-screen.ts +1 -0
  12. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +14 -1
  13. package/corpus/templates/design/changelog/2026-08-07-cmd-c-now-copies-selected-text-in-the-agent-panel-instead-of.md +6 -0
  14. package/corpus/templates/design/changelog/2026-08-07-real-time-voice-can-now-read-the-current-selection-and-edit-.md +6 -0
  15. package/corpus/templates/design/server/lib/reprompt-action-guard.ts +7 -3
  16. package/corpus/templates/mail/server/plugins/agent-chat.ts +1 -1
  17. package/corpus/templates/plan/server/lib/plan-framework-tools.ts +11 -0
  18. package/corpus/templates/plan/server/plugins/00-mcp.ts +25 -68
  19. package/corpus/templates/plan/server/plugins/agent-chat.ts +8 -2
  20. package/dist/collab/awareness.d.ts +2 -2
  21. package/dist/collab/routes.d.ts +1 -1
  22. package/dist/framework-tools.d.ts +33 -1
  23. package/dist/framework-tools.js +88 -4
  24. package/dist/mcp/build-server.d.ts +15 -0
  25. package/dist/mcp/build-server.js +94 -30
  26. package/dist/mcp/oauth-client-metadata.js +6 -2
  27. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  28. package/dist/notifications/routes.d.ts +2 -2
  29. package/dist/observability/routes.d.ts +3 -3
  30. package/dist/progress/routes.d.ts +1 -1
  31. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  32. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  33. package/dist/secrets/routes.d.ts +9 -9
  34. package/dist/server/action-discovery.d.ts +3 -18
  35. package/dist/server/action-discovery.js +9 -65
  36. package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -2
  37. package/dist/server/agent-chat/action-filters-a2a.js +1 -1
  38. package/dist/server/agent-chat/mcp-options.d.ts +106 -0
  39. package/dist/server/agent-chat/mcp-options.js +50 -0
  40. package/dist/server/agent-chat/plugin-options.d.ts +17 -26
  41. package/dist/server/agent-chat-plugin.js +21 -11
  42. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  43. package/dist/server/index.d.ts +3 -0
  44. package/dist/server/index.js +7 -0
  45. package/dist/server/realtime-token.d.ts +1 -1
  46. package/dist/server/realtime-voice.d.ts +2 -1
  47. package/dist/server/realtime-voice.js +38 -56
  48. package/dist/server/short-lived-token.d.ts +43 -0
  49. package/dist/server/short-lived-token.js +74 -0
  50. package/dist/server/transcribe-voice.d.ts +1 -1
  51. package/package.json +2 -2
@@ -9,6 +9,7 @@ import { getBuilderGatewayBaseUrl, resolveBuilderCredentials, resolveSecret, } f
9
9
  import { getH3App } from "./framework-request-handler.js";
10
10
  import { runWithRequestContext } from "./request-context.js";
11
11
  import { isSameOriginRequest } from "./request-origin.js";
12
+ import { signRealtimeVoiceCapability, verifyRealtimeVoiceCapability, } from "./short-lived-token.js";
12
13
  export const REALTIME_VOICE_SESSION_PATH = "/_agent-native/realtime-voice/session";
13
14
  export const REALTIME_VOICE_TOOL_PATH = "/_agent-native/realtime-voice/tool";
14
15
  export const REALTIME_VOICE_MAX_SDP_BYTES = 64 * 1024;
@@ -17,8 +18,9 @@ export const REALTIME_VOICE_MAX_TOOL_OUTPUT_CHARS = 16_000;
17
18
  export const REALTIME_VOICE_MAX_TOOLS = 32;
18
19
  export const REALTIME_VOICE_MAX_TOOL_SCHEMA_BYTES = 32_000;
19
20
  export const REALTIME_VOICE_MAX_SESSION_BYTES = 64_000;
20
- export const REALTIME_VOICE_TOOL_GRANT_TTL_MS = 10 * 60 * 1_000;
21
- export const REALTIME_VOICE_MAX_TOOL_GRANT_SESSIONS = 256;
21
+ /** Absolute, not sliding — a signed grant cannot be extended server-side, so
22
+ * this must outlast the provider's 60-minute maximum realtime session. */
23
+ export const REALTIME_VOICE_TOOL_GRANT_TTL_MS = 75 * 60 * 1_000;
22
24
  export const REALTIME_VOICE_CAPABILITY_HEADER = "X-Agent-Native-Realtime-Capability";
23
25
  const OPENAI_REALTIME_CALLS_URL = "https://api.openai.com/v1/realtime/calls";
24
26
  const DEFAULT_MODEL = "gpt-realtime-2.1";
@@ -207,56 +209,30 @@ function packRealtimeTools(session, eligibleTools) {
207
209
  }
208
210
  return packed;
209
211
  }
210
- function mintRealtimeToolCapability() {
211
- return globalThis.crypto.randomUUID().replaceAll("-", "");
212
- }
213
- function cleanRealtimeToolCapabilities(capabilities, now = Date.now()) {
214
- for (const [key, capability] of capabilities) {
215
- if (capability.expiresAt <= now)
216
- capabilities.delete(key);
217
- }
218
- while (capabilities.size > REALTIME_VOICE_MAX_TOOL_GRANT_SESSIONS) {
219
- let oldestKey;
220
- let oldestExpiry = Number.POSITIVE_INFINITY;
221
- for (const [key, capability] of capabilities) {
222
- if (capability.expiresAt < oldestExpiry) {
223
- oldestKey = key;
224
- oldestExpiry = capability.expiresAt;
225
- }
226
- }
227
- if (!oldestKey)
228
- break;
229
- capabilities.delete(oldestKey);
230
- }
212
+ function mintRealtimeToolCapability(auth, capability) {
213
+ return signRealtimeVoiceCapability({
214
+ userEmail: auth.userEmail,
215
+ ...(auth.orgId ? { orgId: auth.orgId } : {}),
216
+ ...(auth.browserTabId ? { browserTabId: auth.browserTabId } : {}),
217
+ toolNames: [...capability.initialNames],
218
+ discoveredToolNames: [...capability.names],
219
+ }, REALTIME_VOICE_TOOL_GRANT_TTL_MS / 1_000);
231
220
  }
232
- function registerRealtimeToolCapability(capabilities, auth, initialNames) {
233
- cleanRealtimeToolCapabilities(capabilities);
234
- const token = mintRealtimeToolCapability();
235
- capabilities.set(token, {
236
- userEmail: auth.userEmail.trim().toLowerCase(),
221
+ function resolveRealtimeToolCapability(token, auth) {
222
+ const verified = verifyRealtimeVoiceCapability(token, {
223
+ userEmail: auth.userEmail,
237
224
  ...(auth.orgId ? { orgId: auth.orgId } : {}),
238
225
  ...(auth.browserTabId ? { browserTabId: auth.browserTabId } : {}),
239
- expiresAt: Date.now() + REALTIME_VOICE_TOOL_GRANT_TTL_MS,
240
- initialNames: new Set(initialNames),
241
- names: new Set(),
242
226
  });
243
- cleanRealtimeToolCapabilities(capabilities);
244
- return token;
245
- }
246
- function resolveRealtimeToolCapability(capabilities, token, auth) {
247
- cleanRealtimeToolCapabilities(capabilities);
248
- if (!token)
249
- return null;
250
- const capability = capabilities.get(token);
251
- if (!capability)
252
- return null;
253
- if (capability.userEmail !== auth.userEmail.trim().toLowerCase() ||
254
- capability.orgId !== auth.orgId ||
255
- capability.browserTabId !== auth.browserTabId) {
227
+ if (!verified.ok)
256
228
  return null;
257
- }
258
- capability.expiresAt = Date.now() + REALTIME_VOICE_TOOL_GRANT_TTL_MS;
259
- return capability;
229
+ return {
230
+ userEmail: verified.userEmail,
231
+ ...(verified.orgId ? { orgId: verified.orgId } : {}),
232
+ ...(verified.browserTabId ? { browserTabId: verified.browserTabId } : {}),
233
+ initialNames: new Set(verified.toolNames),
234
+ names: new Set(verified.discoveredToolNames),
235
+ };
260
236
  }
261
237
  function parseSuccessfulToolSearchNames(output) {
262
238
  try {
@@ -302,7 +278,6 @@ function grantDiscoveredRealtimeTools(input) {
302
278
  input.capability.names.add(tool.name);
303
279
  expandedTools.push(tool);
304
280
  }
305
- input.capability.expiresAt = Date.now() + REALTIME_VOICE_TOOL_GRANT_TTL_MS;
306
281
  return expandedTools;
307
282
  }
308
283
  function declaredBodyBytes(event) {
@@ -364,7 +339,7 @@ function invalidMethod(event) {
364
339
  setResponseStatus(event, 405);
365
340
  return { error: "Method not allowed" };
366
341
  }
367
- function createSessionHandler(tools, capabilities, options) {
342
+ function createSessionHandler(tools, options) {
368
343
  return defineEventHandler(async (event) => {
369
344
  if (getMethod(event) !== "POST")
370
345
  return invalidMethod(event);
@@ -510,7 +485,10 @@ function createSessionHandler(tools, capabilities, options) {
510
485
  }
511
486
  setResponseStatus(event, upstream.status);
512
487
  setResponseHeader(event, "Content-Type", "application/sdp");
513
- setResponseHeader(event, REALTIME_VOICE_CAPABILITY_HEADER, registerRealtimeToolCapability(capabilities, auth, packedTools.map((tool) => tool.name)));
488
+ setResponseHeader(event, REALTIME_VOICE_CAPABILITY_HEADER, mintRealtimeToolCapability(auth, {
489
+ initialNames: new Set(packedTools.map((tool) => tool.name)),
490
+ names: new Set(),
491
+ }));
514
492
  return answerSdp;
515
493
  });
516
494
  });
@@ -563,7 +541,7 @@ function normalizeExecutionResult(result) {
563
541
  : {}),
564
542
  };
565
543
  }
566
- function createToolHandler(toolsByName, capabilities, options) {
544
+ function createToolHandler(toolsByName, options) {
567
545
  return defineEventHandler(async (event) => {
568
546
  if (getMethod(event) !== "POST")
569
547
  return invalidMethod(event);
@@ -577,7 +555,7 @@ function createToolHandler(toolsByName, capabilities, options) {
577
555
  setResponseStatus(event, 401);
578
556
  return { error: "Authentication required" };
579
557
  }
580
- const capability = resolveRealtimeToolCapability(capabilities, readSafeHeader(event, REALTIME_VOICE_CAPABILITY_HEADER), auth);
558
+ const capability = resolveRealtimeToolCapability(readSafeHeader(event, REALTIME_VOICE_CAPABILITY_HEADER), auth);
581
559
  if (!capability) {
582
560
  setResponseStatus(event, 403);
583
561
  return { error: "Invalid or expired realtime voice capability" };
@@ -648,7 +626,12 @@ function createToolHandler(toolsByName, capabilities, options) {
648
626
  return {
649
627
  callId: request.callId,
650
628
  ...result,
651
- ...(expandedTools.length > 0 ? { expandedTools } : {}),
629
+ ...(expandedTools.length > 0
630
+ ? {
631
+ expandedTools,
632
+ capability: mintRealtimeToolCapability(auth, capability),
633
+ }
634
+ : {}),
652
635
  };
653
636
  }
654
637
  catch (error) {
@@ -669,10 +652,9 @@ export function mountRealtimeVoiceRoutes(nitroApp, actions, options) {
669
652
  }
670
653
  const tools = buildRealtimeTools(actions);
671
654
  const toolsByName = new Map(tools.map((tool) => [tool.name, tool]));
672
- const capabilities = new Map();
673
655
  const app = getH3App(nitroApp);
674
- app.use(REALTIME_VOICE_SESSION_PATH, createSessionHandler(tools, capabilities, options));
675
- app.use(REALTIME_VOICE_TOOL_PATH, createToolHandler(toolsByName, capabilities, options));
656
+ app.use(REALTIME_VOICE_SESSION_PATH, createSessionHandler(tools, options));
657
+ app.use(REALTIME_VOICE_TOOL_PATH, createToolHandler(toolsByName, options));
676
658
  return {
677
659
  sessionPath: REALTIME_VOICE_SESSION_PATH,
678
660
  toolPath: REALTIME_VOICE_TOOL_PATH,
@@ -121,6 +121,49 @@ export declare function verifyRealtimeSubscribeToken(token: string, expected: {
121
121
  projectId: string;
122
122
  key: string;
123
123
  }): RealtimeVerifyResult;
124
+ /** Payload `typ` discriminator for realtime voice tool capabilities. */
125
+ export declare const REALTIME_VOICE_CAPABILITY_TOKEN_TYPE = "rt-voice-capability";
126
+ /** Inputs for {@link signRealtimeVoiceCapability}. */
127
+ export interface RealtimeVoiceCapabilityClaims {
128
+ /** Authenticated caller. Re-checked against the session on every tool call. */
129
+ userEmail: string;
130
+ orgId?: string;
131
+ /** Binds the capability to the tab that opened the session. */
132
+ browserTabId?: string;
133
+ /** Tool names packed into the session's opening manifest. */
134
+ toolNames: readonly string[];
135
+ /** Names added later by a successful `tool-search`, capped separately. */
136
+ discoveredToolNames?: readonly string[];
137
+ ttlSeconds?: number;
138
+ }
139
+ /** Result of {@link verifyRealtimeVoiceCapability}. */
140
+ export type RealtimeVoiceCapabilityVerifyResult = {
141
+ ok: true;
142
+ userEmail: string;
143
+ orgId?: string;
144
+ browserTabId?: string;
145
+ toolNames: string[];
146
+ discoveredToolNames: string[];
147
+ } | {
148
+ ok: false;
149
+ reason: string;
150
+ };
151
+ /**
152
+ * Mint a capability authorising `claims.toolNames` for one realtime voice
153
+ * session. The token is a bounded manifest scope layered on top of the
154
+ * caller's session cookie — never a standalone credential.
155
+ */
156
+ export declare function signRealtimeVoiceCapability(claims: RealtimeVoiceCapabilityClaims, ttlSecondsDefault: number): string;
157
+ /**
158
+ * Verify a realtime voice capability and confirm it was minted for exactly the
159
+ * identity now making the request. Identity mismatch is reported separately
160
+ * from a bad signature so callers can tell a replay from a stale tab.
161
+ */
162
+ export declare function verifyRealtimeVoiceCapability(token: string | undefined, expected: {
163
+ userEmail: string;
164
+ orgId?: string;
165
+ browserTabId?: string;
166
+ }): RealtimeVoiceCapabilityVerifyResult;
124
167
  /** Payload `typ` discriminator for gateway access-check tokens. */
125
168
  export declare const GATEWAY_ACCESS_TOKEN_TYPE = "rt-access-check";
126
169
  /** Inputs for {@link signGatewayAccessToken}. */
@@ -241,6 +241,80 @@ export function verifyRealtimeSubscribeToken(token, expected) {
241
241
  absExp: claims.absExp,
242
242
  };
243
243
  }
244
+ // ── Realtime voice tool capabilities ─────────────────────────────────────────
245
+ // Signed, not stored: minting and spending are separate requests, and under a
246
+ // serverless preset they land on different instances.
247
+ /** Payload `typ` discriminator for realtime voice tool capabilities. */
248
+ export const REALTIME_VOICE_CAPABILITY_TOKEN_TYPE = "rt-voice-capability";
249
+ /**
250
+ * Mint a capability authorising `claims.toolNames` for one realtime voice
251
+ * session. The token is a bounded manifest scope layered on top of the
252
+ * caller's session cookie — never a standalone credential.
253
+ */
254
+ export function signRealtimeVoiceCapability(claims, ttlSecondsDefault) {
255
+ const payload = {
256
+ typ: REALTIME_VOICE_CAPABILITY_TOKEN_TYPE,
257
+ userEmail: claims.userEmail.trim().toLowerCase(),
258
+ toolNames: [...claims.toolNames],
259
+ exp: Math.floor(Date.now() / 1000) + (claims.ttlSeconds ?? ttlSecondsDefault),
260
+ };
261
+ if (claims.orgId)
262
+ payload.orgId = claims.orgId;
263
+ if (claims.browserTabId)
264
+ payload.browserTabId = claims.browserTabId;
265
+ if (claims.discoveredToolNames?.length) {
266
+ payload.discovered = [...claims.discoveredToolNames];
267
+ }
268
+ const payloadStr = base64UrlEncode(JSON.stringify(payload));
269
+ return `${payloadStr}.${hmacB64(payloadStr, getSigningKey())}`;
270
+ }
271
+ /**
272
+ * Verify a realtime voice capability and confirm it was minted for exactly the
273
+ * identity now making the request. Identity mismatch is reported separately
274
+ * from a bad signature so callers can tell a replay from a stale tab.
275
+ */
276
+ export function verifyRealtimeVoiceCapability(token, expected) {
277
+ if (typeof token !== "string" || !token.includes(".")) {
278
+ return { ok: false, reason: "malformed" };
279
+ }
280
+ const [payloadStr, sig] = token.split(".", 2);
281
+ if (!payloadStr || !sig)
282
+ return { ok: false, reason: "malformed" };
283
+ if (!timingSafeEqualB64(sig, hmacB64(payloadStr, getSigningKey()))) {
284
+ return { ok: false, reason: "bad_signature" };
285
+ }
286
+ let claims;
287
+ try {
288
+ claims = JSON.parse(base64UrlDecode(payloadStr).toString("utf8"));
289
+ }
290
+ catch {
291
+ return { ok: false, reason: "bad_payload" };
292
+ }
293
+ if (claims.typ !== REALTIME_VOICE_CAPABILITY_TOKEN_TYPE) {
294
+ return { ok: false, reason: "wrong_type" };
295
+ }
296
+ if (typeof claims.exp !== "number" || !Array.isArray(claims.toolNames)) {
297
+ return { ok: false, reason: "bad_payload" };
298
+ }
299
+ if (claims.exp * 1000 < Date.now())
300
+ return { ok: false, reason: "expired" };
301
+ if (claims.userEmail !== expected.userEmail.trim().toLowerCase() ||
302
+ claims.orgId !== expected.orgId ||
303
+ claims.browserTabId !== expected.browserTabId) {
304
+ return { ok: false, reason: "identity_mismatch" };
305
+ }
306
+ const stringsOnly = (value) => Array.isArray(value)
307
+ ? value.filter((name) => typeof name === "string")
308
+ : [];
309
+ return {
310
+ ok: true,
311
+ userEmail: claims.userEmail,
312
+ orgId: claims.orgId,
313
+ browserTabId: claims.browserTabId,
314
+ toolNames: stringsOnly(claims.toolNames),
315
+ discoveredToolNames: stringsOnly(claims.discovered),
316
+ };
317
+ }
244
318
  // ── Gateway access-check tokens ──────────────────────────────────────────────
245
319
  //
246
320
  // The hosted gateway has no access to an app's shareable-resource registry, so
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- text: string;
24
23
  error?: undefined;
24
+ text: string;
25
25
  }>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.145.3",
3
+ "version": "0.145.5",
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": {
@@ -413,7 +413,7 @@
413
413
  "yjs": "^13.6.31",
414
414
  "zod": "^4.3.6",
415
415
  "@agent-native/recap-cli": "0.5.3",
416
- "@agent-native/toolkit": "^0.13.3"
416
+ "@agent-native/toolkit": "^0.13.4"
417
417
  },
418
418
  "devDependencies": {
419
419
  "@ai-sdk/anthropic": "^3.0.71",