@agent-native/core 0.145.4 → 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.
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 7851
34
+ - template files: 7853
@@ -255,6 +255,11 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
255
255
  [viewportWidth, zoom],
256
256
  );
257
257
 
258
+ const clampedScrollLeft = Math.min(
259
+ scrollLeft,
260
+ Math.max(0, totalWidth - viewportWidth),
261
+ );
262
+
258
263
  const calculateAnchoredScrollLeft = useCallback(
259
264
  (
260
265
  nextZoom: number,
@@ -815,7 +820,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
815
820
  splitPoints={splitPoints}
816
821
  activityRanges={transcriptSegments}
817
822
  onSeek={seek}
818
- scrollLeft={scrollLeft}
823
+ scrollLeft={clampedScrollLeft}
819
824
  onScroll={(s) => setScrollLeft(s)}
820
825
  />
821
826
  <div
@@ -826,7 +831,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
826
831
  className="relative h-full"
827
832
  style={{
828
833
  width: totalWidth,
829
- transform: `translateX(${-scrollLeft}px)`,
834
+ transform: `translateX(${-clampedScrollLeft}px)`,
830
835
  }}
831
836
  >
832
837
  {durationMs > 0 && (
@@ -849,7 +854,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
849
854
  >
850
855
  <div
851
856
  style={{
852
- transform: `translateX(${-scrollLeft}px)`,
857
+ transform: `translateX(${-clampedScrollLeft}px)`,
853
858
  width: totalWidth,
854
859
  }}
855
860
  >
@@ -185,7 +185,8 @@ export function Waveform({
185
185
 
186
186
  const ctx = canvas.getContext("2d");
187
187
  if (!ctx) return;
188
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
188
+ const scaleX = canvas.width / totalWidth;
189
+ ctx.setTransform(scaleX, 0, 0, dpr, 0, 0);
189
190
 
190
191
  const hasPeaks = Boolean(
191
192
  peaks?.bucketCount &&
@@ -31,7 +31,7 @@ function highlight(
31
31
  parts.push(
32
32
  <mark
33
33
  key={`${idx}-${parts.length}`}
34
- className="bg-yellow-200 text-foreground rounded-sm px-0.5"
34
+ className="bg-highlight/30 text-foreground rounded-sm px-0.5"
35
35
  >
36
36
  {text.slice(idx, idx + q.length)}
37
37
  </mark>,
@@ -36,6 +36,8 @@
36
36
  --sidebar-accent-foreground: 0 0% 15%;
37
37
  --sidebar-border: 0 0% 90%;
38
38
  --sidebar-ring: 0 0% 40%;
39
+ --highlight: 197 100% 50%;
40
+ --highlight-foreground: 0 0% 10%;
39
41
  }
40
42
 
41
43
  .dark {
@@ -67,6 +69,13 @@
67
69
  --sidebar-accent-foreground: 0 0% 90%;
68
70
  --sidebar-border: 0 0% 20%;
69
71
  --sidebar-ring: 0 0% 60%;
72
+ --highlight: 197 100% 50%;
73
+ --highlight-foreground: 0 0% 10%;
74
+ }
75
+
76
+ @theme inline {
77
+ --color-highlight: hsl(var(--highlight));
78
+ --color-highlight-foreground: hsl(var(--highlight-foreground));
70
79
  }
71
80
 
72
81
  @layer base {
@@ -392,7 +392,6 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
392
392
  let mut ever_seen_front = false;
393
393
  let mut last_front_at: Option<Instant> = None;
394
394
  let mut fired = false;
395
- let mut last_front_was_generic_browser = false;
396
395
  let mut call_app_used_microphone = false;
397
396
  let mut microphone_released_at: Option<Instant> = None;
398
397
  let mut generation: Option<u64> = None;
@@ -408,7 +407,6 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
408
407
  ever_seen_front = false;
409
408
  last_front_at = None;
410
409
  fired = false;
411
- last_front_was_generic_browser = false;
412
410
  call_app_used_microphone = false;
413
411
  microphone_released_at = None;
414
412
  continue;
@@ -417,7 +415,6 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
417
415
  ever_seen_front = false;
418
416
  last_front_at = None;
419
417
  fired = false;
420
- last_front_was_generic_browser = false;
421
418
  call_app_used_microphone = false;
422
419
  microphone_released_at = None;
423
420
  generation = Some(active_generation);
@@ -450,7 +447,6 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
450
447
  if is_strong_vc || is_generic_browser {
451
448
  ever_seen_front = true;
452
449
  last_front_at = Some(Instant::now());
453
- last_front_was_generic_browser = is_generic_browser;
454
450
  }
455
451
  if fired {
456
452
  continue;
@@ -474,10 +470,18 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
474
470
  _ => {}
475
471
  }
476
472
 
473
+ // Require audio corroboration for every trigger in this
474
+ // watcher: backgrounding the call app, or its process
475
+ // dropping its mic input, does not by itself prove the call
476
+ // ended — a browser tab can report either transition while
477
+ // the meeting is still playing through system audio. Only
478
+ // quiet mic+system audio alongside the signal does.
479
+ let audio_quiet = audio_recently_silent(&state, threshold_ms);
480
+
477
481
  let microphone_released = microphone_release_stop_ready(
478
482
  call_app_used_microphone,
479
483
  microphone_released_at.map(|at| Instant::now().duration_since(at)),
480
- );
484
+ ) && audio_quiet;
481
485
 
482
486
  let frontmost_call_ended = ever_seen_front
483
487
  && last_front_at
@@ -485,12 +489,7 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
485
489
  Instant::now().duration_since(t).as_millis() as u64 >= threshold_ms
486
490
  })
487
491
  .unwrap_or(false)
488
- // Require audio corroboration for browser-hosted calls:
489
- // backgrounding Chrome/Arc alone does not prove that the
490
- // Meet tab ended. Use the last known conference app, not
491
- // the unrelated app now in front.
492
- && (!last_front_was_generic_browser
493
- || audio_recently_silent(&state, threshold_ms));
492
+ && audio_quiet;
494
493
 
495
494
  if microphone_released || frontmost_call_ended {
496
495
  let _ = app.emit("meetings:call-ended", ());
@@ -220,6 +220,7 @@ export default defineAction({
220
220
  "See what the user is currently looking at on screen. Returns the current navigation state including which design or template is open, which view they are on (list, templates, editor, design-systems, present, settings), active/focused design screen, selected element, active inspector tab (design, comments, or tweaks), active left rail panel (file, agent, assets, import, tools, tokens, or code), active code file metadata, overview canvas state, review status and feedback queue summary, plus any pending question overlay. Always call this first before taking any action.",
221
221
  schema: z.object({}),
222
222
  http: false,
223
+ readOnly: true,
223
224
  run: async (_, ctx) => {
224
225
  const [navigation, designSelection] = await Promise.all([
225
226
  readAppStateForCurrentTab("navigation"),
@@ -277,6 +277,18 @@ export function isDesignHotkeyEditableTarget(target: EventTarget | null) {
277
277
  return tagName === "input" || tagName === "textarea" || tagName === "select";
278
278
  }
279
279
 
280
+ /** Chat bodies and panel labels are selectable but not editable targets, so the
281
+ * editable guard never sees them. Canvas layers live in the preview iframe and
282
+ * produce no parent-document range, so this cannot mask a real layer copy. */
283
+ export function hasDocumentTextSelection() {
284
+ if (typeof window === "undefined" || !window.getSelection) return false;
285
+ const selection = window.getSelection();
286
+ if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
287
+ return false;
288
+ }
289
+ return selection.toString().trim().length > 0;
290
+ }
291
+
280
292
  export function isShowKeyboardShortcutsHotkey(event: KeyboardEvent) {
281
293
  if (!(event.ctrlKey || event.metaKey) || !event.shiftKey || event.altKey) {
282
294
  return false;
@@ -523,7 +535,7 @@ export function handleDesignHotkey(
523
535
  if (primary && event.shiftKey && key === "x") {
524
536
  return run(props.onToggleStrikethrough);
525
537
  }
526
- if (primary && key === "x") {
538
+ if (primary && key === "x" && !hasDocumentTextSelection()) {
527
539
  return runSharedCanvasCommand() || run(props.onCut);
528
540
  }
529
541
  // Figma's Cmd+U — toggle underline. No existing binding claims plain "u".
@@ -556,6 +568,7 @@ export function handleDesignHotkey(
556
568
  if (primary && key === "c") {
557
569
  if (event.altKey) return run(props.onCopyProps);
558
570
  if (event.shiftKey) return run(props.onCopyAsPng);
571
+ if (hasDocumentTextSelection()) return false;
559
572
  return runSharedCanvasCommand() || run(props.onCopy);
560
573
  }
561
574
  if (primary && key === "v") {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-07
4
+ ---
5
+
6
+ Cmd+C now copies selected text in the agent panel instead of being captured by the canvas copy shortcut
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-07
4
+ ---
5
+
6
+ Real-time voice can now read the current selection and edit the design, instead of failing to verify the agent thread
@@ -77,13 +77,17 @@ export function guardRepromptActionRegistry(
77
77
  ...entry,
78
78
  run: async (args, context) => {
79
79
  if (context?.caller === "tool" && context.threadId) {
80
- const thread = await getThread(context.threadId);
81
- if (!thread) {
80
+ let thread: Awaited<ReturnType<typeof getThread>>;
81
+ try {
82
+ thread = await getThread(context.threadId);
83
+ } catch {
82
84
  throw new Error(
83
85
  `Cannot verify the agent thread for mutating action ${name}.`,
84
86
  );
85
87
  }
86
- const intent = threadSelectionIntent(thread);
88
+ // Off-thread callers (realtime voice, MCP, A2A, cron) have no
89
+ // stored thread, so they cannot be inside a reprompt turn.
90
+ const intent = thread ? threadSelectionIntent(thread) : null;
87
91
  if (
88
92
  intent === "reprompt" &&
89
93
  !REPROMPT_MUTATION_ALLOWLIST.has(name)
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -137,13 +137,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
137
137
  inputSchema: {
138
138
  type: string;
139
139
  properties: {
140
+ count?: undefined;
140
141
  query?: undefined;
141
142
  minutes?: undefined;
142
143
  limit?: undefined;
143
144
  clientHint?: undefined;
144
145
  timestamp?: undefined;
145
146
  chapterId?: undefined;
146
- count?: undefined;
147
147
  startAt?: undefined;
148
148
  endAt?: undefined;
149
149
  reason?: undefined;
@@ -159,6 +159,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
159
159
  inputSchema: {
160
160
  type: string;
161
161
  properties: {
162
+ count?: undefined;
162
163
  query: {
163
164
  type: string;
164
165
  description: string;
@@ -174,7 +175,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
174
175
  clientHint?: undefined;
175
176
  timestamp?: undefined;
176
177
  chapterId?: undefined;
177
- count?: undefined;
178
178
  startAt?: undefined;
179
179
  endAt?: undefined;
180
180
  reason?: undefined;
@@ -190,6 +190,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
190
190
  inputSchema: {
191
191
  type: string;
192
192
  properties: {
193
+ count?: undefined;
193
194
  minutes: {
194
195
  type: string;
195
196
  description: string;
@@ -199,7 +200,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
199
200
  clientHint?: undefined;
200
201
  timestamp?: undefined;
201
202
  chapterId?: undefined;
202
- count?: undefined;
203
203
  startAt?: undefined;
204
204
  endAt?: undefined;
205
205
  reason?: undefined;
@@ -216,6 +216,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
216
216
  type: string;
217
217
  required: string[];
218
218
  properties: {
219
+ count?: undefined;
219
220
  query: {
220
221
  type: string;
221
222
  description: string;
@@ -234,7 +235,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
234
235
  };
235
236
  timestamp?: undefined;
236
237
  chapterId?: undefined;
237
- count?: undefined;
238
238
  startAt?: undefined;
239
239
  endAt?: undefined;
240
240
  reason?: undefined;
@@ -250,6 +250,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
250
250
  type: string;
251
251
  required: string[];
252
252
  properties: {
253
+ count?: undefined;
253
254
  query?: undefined;
254
255
  minutes?: undefined;
255
256
  limit?: undefined;
@@ -263,7 +264,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
263
264
  description: string;
264
265
  };
265
266
  chapterId?: undefined;
266
- count?: undefined;
267
267
  startAt?: undefined;
268
268
  endAt?: undefined;
269
269
  includeMicrophone?: undefined;
@@ -314,13 +314,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
314
314
  type: string;
315
315
  required: string[];
316
316
  properties: {
317
+ count?: undefined;
317
318
  query?: undefined;
318
319
  minutes?: undefined;
319
320
  limit?: undefined;
320
321
  clientHint?: undefined;
321
322
  timestamp?: undefined;
322
323
  chapterId?: undefined;
323
- count?: undefined;
324
324
  startAt: {
325
325
  type: string;
326
326
  description: string;
@@ -349,13 +349,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
349
349
  type: string;
350
350
  required: string[];
351
351
  properties: {
352
+ count?: undefined;
352
353
  query?: undefined;
353
354
  minutes?: undefined;
354
355
  limit?: undefined;
355
356
  clientHint?: undefined;
356
357
  timestamp?: undefined;
357
358
  chapterId?: undefined;
358
- count?: undefined;
359
359
  startAt?: undefined;
360
360
  endAt?: undefined;
361
361
  reason?: undefined;
@@ -75,7 +75,6 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
- message?: undefined;
79
78
  deleted?: undefined;
80
79
  found?: undefined;
81
80
  providers: {
@@ -92,8 +91,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
92
91
  registered?: undefined;
93
92
  id?: undefined;
94
93
  label?: undefined;
95
- } | {
96
94
  message?: undefined;
95
+ } | {
97
96
  deleted?: undefined;
98
97
  providers?: undefined;
99
98
  count?: undefined;
@@ -102,8 +101,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
102
101
  registered?: undefined;
103
102
  id?: undefined;
104
103
  label?: undefined;
105
- } | {
106
104
  message?: undefined;
105
+ } | {
107
106
  deleted?: undefined;
108
107
  providers?: undefined;
109
108
  count?: undefined;
@@ -112,8 +111,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
112
111
  id: string;
113
112
  registered?: undefined;
114
113
  label?: undefined;
115
- } | {
116
114
  message?: undefined;
115
+ } | {
117
116
  found?: undefined;
118
117
  providers?: undefined;
119
118
  count?: undefined;
@@ -122,6 +121,7 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
122
121
  id: string;
123
122
  registered?: undefined;
124
123
  label?: undefined;
124
+ message?: undefined;
125
125
  } | {
126
126
  deleted?: undefined;
127
127
  found?: undefined;
@@ -312,8 +312,8 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
312
312
  scope?: "org" | "user" | undefined;
313
313
  }, {
314
314
  id?: undefined;
315
- deleted?: undefined;
316
315
  provider?: undefined;
316
+ deleted?: undefined;
317
317
  found?: undefined;
318
318
  providers: {
319
319
  id: string;
@@ -339,9 +339,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
339
339
  label?: undefined;
340
340
  message?: undefined;
341
341
  } | {
342
+ provider?: undefined;
342
343
  deleted?: undefined;
343
344
  count?: undefined;
344
- provider?: undefined;
345
345
  providers?: undefined;
346
346
  found: boolean;
347
347
  id: string;
@@ -349,9 +349,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
349
349
  label?: undefined;
350
350
  message?: undefined;
351
351
  } | {
352
- count?: undefined;
353
352
  provider?: undefined;
354
353
  found?: undefined;
354
+ count?: undefined;
355
355
  providers?: undefined;
356
356
  deleted: boolean;
357
357
  id: string;
@@ -359,10 +359,10 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
359
359
  label?: undefined;
360
360
  message?: undefined;
361
361
  } | {
362
- deleted?: undefined;
363
- count?: undefined;
364
362
  provider?: undefined;
363
+ deleted?: undefined;
365
364
  found?: undefined;
365
+ count?: undefined;
366
366
  providers?: undefined;
367
367
  registered: boolean;
368
368
  id: string;
@@ -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;
@@ -34,37 +34,37 @@ 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
- error?: undefined;
41
40
  ok: boolean;
42
41
  status: string;
42
+ error?: undefined;
43
43
  } | {
44
- ok?: undefined;
45
44
  error: string;
46
45
  removed?: undefined;
46
+ ok?: undefined;
47
47
  } | {
48
- error?: undefined;
49
48
  ok: boolean;
50
49
  removed: boolean;
50
+ error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
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
- error?: undefined;
62
61
  ok: boolean;
63
62
  note?: undefined;
64
- } | {
65
63
  error?: undefined;
64
+ } | {
66
65
  ok: boolean;
67
66
  note: string;
67
+ error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
- error?: undefined;
99
98
  ok: boolean;
100
99
  key: string;
101
- } | {
102
100
  error?: undefined;
101
+ } | {
103
102
  ok: boolean;
104
103
  removed: boolean;
104
+ error?: undefined;
105
105
  }>>;
@@ -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 {};
@@ -8,8 +8,9 @@ export declare const REALTIME_VOICE_MAX_TOOL_OUTPUT_CHARS = 16000;
8
8
  export declare const REALTIME_VOICE_MAX_TOOLS = 32;
9
9
  export declare const REALTIME_VOICE_MAX_TOOL_SCHEMA_BYTES = 32000;
10
10
  export declare const REALTIME_VOICE_MAX_SESSION_BYTES = 64000;
11
+ /** Absolute, not sliding — a signed grant cannot be extended server-side, so
12
+ * this must outlast the provider's 60-minute maximum realtime session. */
11
13
  export declare const REALTIME_VOICE_TOOL_GRANT_TTL_MS: number;
12
- export declare const REALTIME_VOICE_MAX_TOOL_GRANT_SESSIONS = 256;
13
14
  export declare const REALTIME_VOICE_CAPABILITY_HEADER = "X-Agent-Native-Realtime-Capability";
14
15
  export interface RealtimeVoiceRequestContext {
15
16
  event: H3Event;
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.145.4",
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",