@agent-native/core 0.158.9 → 0.159.0

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 (78) hide show
  1. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +2 -2
  2. package/corpus/templates/clips/app/i18n/en-US.ts +7 -2
  3. package/corpus/templates/clips/app/routes/record.tsx +147 -5
  4. package/dist/a2a/auth-policy.d.ts +0 -6
  5. package/dist/a2a/auth-policy.js +2 -1
  6. package/dist/a2a/client.js +10 -12
  7. package/dist/agent/durable-background.js +3 -2
  8. package/dist/agent/engine/registry.js +5 -4
  9. package/dist/agent/run-manager.js +8 -20
  10. package/dist/app-config/a2a.d.ts +14 -0
  11. package/dist/app-config/a2a.js +42 -0
  12. package/dist/app-config/agent.d.ts +18 -0
  13. package/dist/app-config/agent.js +69 -0
  14. package/dist/app-config/app.d.ts +33 -0
  15. package/dist/app-config/app.js +100 -0
  16. package/dist/app-config/describe.d.ts +26 -0
  17. package/dist/app-config/describe.js +61 -0
  18. package/dist/app-config/env-layer.d.ts +37 -0
  19. package/dist/app-config/env-layer.js +136 -0
  20. package/dist/app-config/index.d.ts +2 -0
  21. package/dist/app-config/index.js +2 -0
  22. package/dist/app-config/integrations.d.ts +5 -0
  23. package/dist/app-config/integrations.js +11 -0
  24. package/dist/app-config/private-blob.d.ts +14 -0
  25. package/dist/app-config/private-blob.js +19 -0
  26. package/dist/app-config/schema.d.ts +55 -0
  27. package/dist/app-config/schema.js +31 -0
  28. package/dist/app-config/store.d.ts +42 -0
  29. package/dist/app-config/store.js +104 -0
  30. package/dist/app-config/workspace.d.ts +21 -0
  31. package/dist/app-config/workspace.js +35 -0
  32. package/dist/cli/agent.js +2 -1
  33. package/dist/cli/code-agent-executor.js +5 -4
  34. package/dist/client/analytics.d.ts +9 -0
  35. package/dist/client/frame.js +11 -1
  36. package/dist/collab/awareness.d.ts +2 -2
  37. package/dist/deploy/build.js +36 -0
  38. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  39. package/dist/integrations/a2a-continuation-processor.js +12 -9
  40. package/dist/integrations/adapters/email.js +2 -1
  41. package/dist/integrations/adapters/telegram.js +2 -1
  42. package/dist/integrations/adapters/whatsapp.js +2 -1
  43. package/dist/integrations/webhook-handler.d.ts +7 -2
  44. package/dist/integrations/webhook-handler.js +9 -31
  45. package/dist/observability/routes.d.ts +5 -5
  46. package/dist/onboarding/app-profile.js +2 -4
  47. package/dist/onboarding/default-steps.js +4 -4
  48. package/dist/private-blob/registry.d.ts +5 -0
  49. package/dist/private-blob/registry.js +19 -8
  50. package/dist/progress/routes.d.ts +1 -1
  51. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  52. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  53. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  54. package/dist/resources/handlers.d.ts +1 -1
  55. package/dist/scripts/agent-engines/list-agent-engines.js +4 -2
  56. package/dist/secrets/routes.d.ts +9 -9
  57. package/dist/server/agent-chat/action-filters-a2a.js +5 -4
  58. package/dist/server/agent-chat/run-code-tools.js +3 -4
  59. package/dist/server/agent-chat-plugin.js +4 -3
  60. package/dist/server/agent-discovery.js +7 -5
  61. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  62. package/dist/server/app-origin-config.d.ts +23 -0
  63. package/dist/server/app-origin-config.js +43 -0
  64. package/dist/server/auth.js +21 -1
  65. package/dist/server/core-routes-plugin.js +3 -3
  66. package/dist/server/credential-provider.js +7 -8
  67. package/dist/server/embed-session.js +2 -1
  68. package/dist/server/identity-sso.js +6 -2
  69. package/dist/server/index.d.ts +1 -0
  70. package/dist/server/index.js +1 -0
  71. package/dist/server/oauth-public-origin.js +10 -14
  72. package/dist/server/oauth-return-url.js +5 -9
  73. package/dist/server/self-dispatch.js +5 -2
  74. package/dist/server/ssr-handler.js +2 -0
  75. package/dist/server/workspace-provider-oauth.js +2 -4
  76. package/dist/sharing/actions/create-agent-resource-link.js +2 -2
  77. package/dist/usage/metrics-store.js +2 -1
  78. package/package.json +3 -3
@@ -3,7 +3,7 @@ import {
3
3
  IconPlayerPause,
4
4
  IconPlayerPlay,
5
5
  IconPlayerStop,
6
- IconX,
6
+ IconTrash,
7
7
  } from "@tabler/icons-react";
8
8
  import { useEffect, useRef, useState } from "react";
9
9
 
@@ -223,7 +223,7 @@ export function RecordingToolbar({
223
223
  className="flex h-9 w-9 items-center justify-center rounded-full hover:bg-white/15"
224
224
  aria-label={t("recordingToolbar.cancel")}
225
225
  >
226
- <IconX className="h-4 w-4" />
226
+ <IconTrash className="h-4 w-4" />
227
227
  </button>
228
228
  </TooltipTrigger>
229
229
  <TooltipContent>{t("recordingToolbar.cancelShortcut")}</TooltipContent>
@@ -1031,8 +1031,13 @@ All notable user-facing changes to Clips are documented here. Open it any time f
1031
1031
  pauseShortcut: "Pause (⌥⇧P)",
1032
1032
  stop: "Stop recording",
1033
1033
  elapsed: "Elapsed time",
1034
- cancel: "Cancel recording",
1035
- cancelShortcut: "Cancel (⌥⇧C)",
1034
+ cancel: "Discard recording",
1035
+ cancelShortcut: "Discard (⌥⇧C)",
1036
+ discardConfirmTitle: "Discard this recording?",
1037
+ discardConfirmDescription:
1038
+ "This can't be undone. Your recording so far will be permanently deleted.",
1039
+ resume: "Resume",
1040
+ discardRecording: "Discard recording",
1036
1041
  },
1037
1042
  countdownOverlay: {
1038
1043
  startsIn: "Recording starts in {{count}}",
@@ -119,6 +119,16 @@ import {
119
119
  } from "@/components/recorder/recorder-engine";
120
120
  import { RecordingToolbar } from "@/components/recorder/recording-toolbar";
121
121
  import { StorageSetupCard } from "@/components/recorder/storage-setup-card";
122
+ import {
123
+ AlertDialog,
124
+ AlertDialogAction,
125
+ AlertDialogCancel,
126
+ AlertDialogContent,
127
+ AlertDialogDescription,
128
+ AlertDialogFooter,
129
+ AlertDialogHeader,
130
+ AlertDialogTitle,
131
+ } from "@/components/ui/alert-dialog";
122
132
  import { Button } from "@/components/ui/button";
123
133
  import { Spinner } from "@/components/ui/spinner";
124
134
 
@@ -787,6 +797,12 @@ export default function RecordRoute() {
787
797
  const [error, setError] = useState<string | null>(null);
788
798
  const [isPaused, setIsPaused] = useState(false);
789
799
  const visibilityAutoPausedRef = useRef(false);
800
+ const [discardConfirmOpen, setDiscardConfirmOpen] = useState(false);
801
+ // Tracks whether opening the discard-confirm dialog paused the recording
802
+ // itself (vs. the user having already paused) — so "Resume" only resumes
803
+ // when we're the ones who paused it, and the dialog never gets captured in
804
+ // the recorded screen.
805
+ const discardAutoPausedRef = useRef(false);
790
806
  const [cameraStream, setCameraStream] = useState<MediaStream | null>(null);
791
807
  const [cameraSize, setCameraSize] = useState<CameraBubbleSize>(
792
808
  () => loadRecorderPreferences().cameraSize ?? "md",
@@ -940,6 +956,10 @@ export default function RecordRoute() {
940
956
  } | null>(null);
941
957
  const previewVideoRef = useRef<HTMLVideoElement>(null);
942
958
  const fileUploadAbortRef = useRef<AbortController | null>(null);
959
+ // Set to the recording row created by uploadFile() for the duration of
960
+ // that upload, so doCancel() can trash it directly — createdId otherwise
961
+ // only lives in uploadFile's own closure and never reaches pendingRef.
962
+ const fileUploadRecordingIdRef = useRef<string | null>(null);
943
963
  const browserDiagnosticsRef = useRef<BrowserDiagnosticsCapture | null>(null);
944
964
  // Bumped by doCancel() to invalidate any in-flight startFlow().
945
965
  const startSessionRef = useRef(0);
@@ -1558,6 +1578,7 @@ export default function RecordRoute() {
1558
1578
  throw new Error("create-recording did not return an id");
1559
1579
  }
1560
1580
  createdId = info.id;
1581
+ fileUploadRecordingIdRef.current = createdId;
1561
1582
  await saveBugReportContextRef.current(info.id);
1562
1583
  if (isStale()) throw makeAbortError("Upload cancelled");
1563
1584
  const uploadBase = `${appBasePath()}${info.uploadChunkUrl}`;
@@ -1817,6 +1838,9 @@ export default function RecordRoute() {
1817
1838
  if (fileUploadAbortRef.current === abort) {
1818
1839
  fileUploadAbortRef.current = null;
1819
1840
  }
1841
+ if (fileUploadRecordingIdRef.current === createdId) {
1842
+ fileUploadRecordingIdRef.current = null;
1843
+ }
1820
1844
  setCompressionProgress(null);
1821
1845
  setUploadProgress(null);
1822
1846
  }
@@ -2163,10 +2187,12 @@ export default function RecordRoute() {
2163
2187
  startSessionRef.current += 1;
2164
2188
  countdownAudioCueRef.current?.cleanup();
2165
2189
  countdownAudioCueRef.current = null;
2190
+ const uploadRecordingId = fileUploadRecordingIdRef.current;
2166
2191
  if (fileUploadAbortRef.current) {
2167
2192
  fileUploadAbortRef.current.abort(makeAbortError("Upload cancelled"));
2168
2193
  fileUploadAbortRef.current = null;
2169
2194
  }
2195
+ fileUploadRecordingIdRef.current = null;
2170
2196
  const engine = engineRef.current;
2171
2197
  const pendingId = pendingRef.current?.id;
2172
2198
  liveTranscription.stop();
@@ -2198,6 +2224,18 @@ export default function RecordRoute() {
2198
2224
  body: JSON.stringify({ id: pendingId, skipIfReady: true }),
2199
2225
  }).catch(() => {});
2200
2226
  }
2227
+ if (uploadRecordingId) {
2228
+ // A local file import (as opposed to a live recording) never
2229
+ // populates pendingRef — its row id only exists in uploadFile's own
2230
+ // closure. Without this, discarding mid-upload aborts the transfer
2231
+ // but leaves the row merely marked "failed" instead of trashed, which
2232
+ // contradicts the confirmation dialog's "permanently deleted" copy.
2233
+ fetch(agentNativePath("/_agent-native/actions/trash-recording"), {
2234
+ method: "POST",
2235
+ headers: { "Content-Type": "application/json" },
2236
+ body: JSON.stringify({ id: uploadRecordingId, skipIfReady: true }),
2237
+ }).catch(() => {});
2238
+ }
2201
2239
  setCameraStream(null);
2202
2240
  setPreviewStream(null);
2203
2241
  setIsPaused(false);
@@ -2229,6 +2267,56 @@ export default function RecordRoute() {
2229
2267
  }
2230
2268
  }, [liveTranscription]);
2231
2269
 
2270
+ // Discarding an in-progress recording is permanent (see doCancel — it
2271
+ // trashes the pending row with no recovery), so route it through a confirm
2272
+ // dialog instead of firing immediately. While live, pause capture first so
2273
+ // the confirmation itself never ends up in the recorded video.
2274
+ const requestDiscard = useCallback(() => {
2275
+ const engine = engineRef.current;
2276
+ if (uiState === "recording" && engine && engine.getState() !== "paused") {
2277
+ engine.pause();
2278
+ liveTranscription.pause();
2279
+ setIsPaused(true);
2280
+ discardAutoPausedRef.current = true;
2281
+ }
2282
+ setDiscardConfirmOpen(true);
2283
+ }, [uiState, liveTranscription]);
2284
+
2285
+ const resumeFromDiscardPrompt = useCallback(() => {
2286
+ setDiscardConfirmOpen(false);
2287
+ if (!discardAutoPausedRef.current) return;
2288
+ discardAutoPausedRef.current = false;
2289
+ const engine = engineRef.current;
2290
+ if (!engine) return;
2291
+ engine.resume();
2292
+ liveTranscription.resume();
2293
+ setIsPaused(false);
2294
+ }, [liveTranscription]);
2295
+
2296
+ const confirmDiscard = useCallback(() => {
2297
+ discardAutoPausedRef.current = false;
2298
+ setDiscardConfirmOpen(false);
2299
+ void doCancel();
2300
+ }, [doCancel]);
2301
+
2302
+ // A background upload (e.g. importing a local video file) can finish
2303
+ // independently of user interaction while this dialog is open -- it isn't
2304
+ // gated behind uiState. Once the recording leaves a discardable state,
2305
+ // Discard would be a no-op (there's nothing left for doCancel to abort or
2306
+ // trash), so close the prompt rather than leave a misleading control open.
2307
+ useEffect(() => {
2308
+ if (!discardConfirmOpen) return;
2309
+ if (
2310
+ uiState === "recording" ||
2311
+ uiState === "uploading" ||
2312
+ uiState === "compressing"
2313
+ ) {
2314
+ return;
2315
+ }
2316
+ discardAutoPausedRef.current = false;
2317
+ setDiscardConfirmOpen(false);
2318
+ }, [uiState, discardConfirmOpen]);
2319
+
2232
2320
  useEffect(() => {
2233
2321
  if (typeof navigator === "undefined") return;
2234
2322
  if (!isMobileRecorderRuntime(navigator)) return;
@@ -2299,6 +2387,7 @@ export default function RecordRoute() {
2299
2387
  // -------------------------------------------------------------------------
2300
2388
  useEffect(() => {
2301
2389
  function onKey(e: KeyboardEvent) {
2390
+ if (discardConfirmOpen) return;
2302
2391
  const alt = e.altKey;
2303
2392
  const shift = e.shiftKey;
2304
2393
  const meta = e.metaKey;
@@ -2331,8 +2420,20 @@ export default function RecordRoute() {
2331
2420
  }
2332
2421
  }
2333
2422
 
2334
- // Opt/Alt+Shift+C cancel
2423
+ // Opt/Alt+Shift+C -- cancel. Route the same states that show a
2424
+ // discard/cancel control (the recording toolbar and the
2425
+ // uploading/compressing overlay) through the confirm dialog, so the
2426
+ // shortcut can't bypass what the equivalent on-screen button requires.
2335
2427
  if (alt && shift && k === "c") {
2428
+ if (
2429
+ uiState === "recording" ||
2430
+ uiState === "uploading" ||
2431
+ uiState === "compressing"
2432
+ ) {
2433
+ e.preventDefault();
2434
+ requestDiscard();
2435
+ return;
2436
+ }
2336
2437
  if (uiState !== "idle") {
2337
2438
  e.preventDefault();
2338
2439
  void doCancel();
@@ -2360,7 +2461,16 @@ export default function RecordRoute() {
2360
2461
  }
2361
2462
  window.addEventListener("keydown", onKey);
2362
2463
  return () => window.removeEventListener("keydown", onKey);
2363
- }, [uiState, togglePause, doCancel, doStop, restart, fireConfetti]);
2464
+ }, [
2465
+ uiState,
2466
+ discardConfirmOpen,
2467
+ togglePause,
2468
+ doCancel,
2469
+ requestDiscard,
2470
+ doStop,
2471
+ restart,
2472
+ fireConfetti,
2473
+ ]);
2364
2474
 
2365
2475
  // Query params can preselect recorder controls, but browser capture must
2366
2476
  // still start from the user's Start click. Calling getDisplayMedia from an
@@ -2594,10 +2704,42 @@ export default function RecordRoute() {
2594
2704
  isPaused={isPaused}
2595
2705
  onTogglePause={togglePause}
2596
2706
  onStop={() => void doStop()}
2597
- onCancel={() => void doCancel()}
2707
+ onCancel={requestDiscard}
2598
2708
  />
2599
2709
  )}
2600
2710
 
2711
+ <AlertDialog
2712
+ open={discardConfirmOpen}
2713
+ onOpenChange={(open) => {
2714
+ if (!open) resumeFromDiscardPrompt();
2715
+ }}
2716
+ >
2717
+ <AlertDialogContent>
2718
+ <AlertDialogHeader>
2719
+ <AlertDialogTitle>
2720
+ {t("recordingToolbar.discardConfirmTitle")}
2721
+ </AlertDialogTitle>
2722
+ <AlertDialogDescription>
2723
+ {t("recordingToolbar.discardConfirmDescription")}
2724
+ </AlertDialogDescription>
2725
+ </AlertDialogHeader>
2726
+ <AlertDialogFooter>
2727
+ <AlertDialogCancel>
2728
+ {t("recordingToolbar.resume")}
2729
+ </AlertDialogCancel>
2730
+ <AlertDialogAction
2731
+ onClick={(event) => {
2732
+ event.preventDefault();
2733
+ confirmDiscard();
2734
+ }}
2735
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
2736
+ >
2737
+ {t("recordingToolbar.discardRecording")}
2738
+ </AlertDialogAction>
2739
+ </AlertDialogFooter>
2740
+ </AlertDialogContent>
2741
+ </AlertDialog>
2742
+
2601
2743
  {/* Uploading overlay (also covers the compressing pass which can run
2602
2744
  for several minutes on long recordings — without a distinct copy
2603
2745
  users wonder if the app froze). */}
@@ -2639,10 +2781,10 @@ export default function RecordRoute() {
2639
2781
  </>
2640
2782
  )}
2641
2783
  <button
2642
- onClick={doCancel}
2784
+ onClick={requestDiscard}
2643
2785
  className="mt-1 text-xs text-white/50 underline-offset-2 hover:text-white/80 hover:underline"
2644
2786
  >
2645
- Cancel
2787
+ {t("recordingToolbar.cancel")}
2646
2788
  </button>
2647
2789
  </div>
2648
2790
  )}
@@ -1,9 +1,3 @@
1
- /**
2
- * A2A auth policy helpers shared by discovery, the JSON-RPC gate, and task
3
- * handlers. Serverless providers do not always expose `NODE_ENV=production`
4
- * consistently at runtime, so production-like A2A checks also look at the
5
- * provider flags those platforms set in deployed functions.
6
- */
7
1
  export declare function isA2AProductionRuntime(): boolean;
8
2
  export declare function hasConfiguredA2ASecret(): boolean;
9
3
  export declare function shouldAdvertiseJwtA2AAuth(): boolean;
@@ -4,6 +4,7 @@
4
4
  * consistently at runtime, so production-like A2A checks also look at the
5
5
  * provider flags those platforms set in deployed functions.
6
6
  */
7
+ import { getAppConfig } from "../app-config/index.js";
7
8
  export function isA2AProductionRuntime() {
8
9
  if (process.env.NODE_ENV === "production")
9
10
  return true;
@@ -48,7 +49,7 @@ export function shouldAdvertiseJwtA2AAuth() {
48
49
  export function isTrustedLocalRuntime(opts) {
49
50
  if (isA2AProductionRuntime())
50
51
  return false;
51
- if (process.env.A2A_ALLOW_UNSIGNED_INTERNAL === "1")
52
+ if (getAppConfig().a2a.allowUnsignedInternal)
52
53
  return true;
53
54
  return opts.loopback === true;
54
55
  }
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as jose from "jose";
3
+ import { getAppConfig } from "../app-config/index.js";
3
4
  import { ssrfSafeFetch } from "../extensions/url-safety.js";
4
5
  import { canonicalA2AAudience } from "./audience.js";
5
6
  /**
@@ -9,16 +10,15 @@ import { canonicalA2AAudience } from "./audience.js";
9
10
  * itself — never a value that arrived on a request.
10
11
  */
11
12
  function workspacePrivateOrigins() {
13
+ const config = getAppConfig();
14
+ // No trimming or blank-dropping here: the config layer trims string values
15
+ // and `a2a.allowedOrigins` rejects empty entries, so the only thing left to
16
+ // drop is an unset optional.
12
17
  const origins = [
13
- process.env.WORKSPACE_GATEWAY_URL,
14
- process.env.APP_URL,
15
- process.env.BETTER_AUTH_URL,
16
- // Escape hatch for contexts that never receive the gateway manifest (the
17
- // action CLI, one-off scripts). Comma-separated origins.
18
- ...(process.env.AGENT_NATIVE_A2A_ALLOWED_ORIGINS ?? "").split(","),
19
- ]
20
- .map((value) => value?.trim())
21
- .filter((value) => !!value);
18
+ config.workspace.gatewayUrl,
19
+ config.app.url,
20
+ ...config.a2a.allowedOrigins,
21
+ ].filter((value) => value !== undefined);
22
22
  // The gateway also hands each child the sibling manifest, and siblings are
23
23
  // reached on their own loopback ports rather than through the gateway.
24
24
  const raw = process.env.AGENT_NATIVE_WORKSPACE_APPS_JSON;
@@ -118,9 +118,7 @@ export async function signA2AToken(email, orgDomain, orgSecret, options) {
118
118
  throw new Error("No A2A secret available. Set an org-level A2A secret in Team settings, " +
119
119
  "or set A2A_SECRET as an environment variable on all apps that need to verify identity.");
120
120
  }
121
- const appUrl = process.env.APP_URL ||
122
- process.env.BETTER_AUTH_URL ||
123
- "http://localhost:3000";
121
+ const appUrl = getAppConfig().app.url ?? "http://localhost:3000";
124
122
  const jwt = new jose.SignJWT({
125
123
  ...(options?.extraClaims ?? {}),
126
124
  // `sub` / `org_domain` are spread AFTER extraClaims so a caller-supplied
@@ -38,6 +38,7 @@
38
38
  * 15-min budget.
39
39
  */
40
40
  import { hasConfiguredA2ASecret, isTrustedLocalRuntime, } from "../a2a/auth-policy.js";
41
+ import { getAppConfig } from "../app-config/index.js";
41
42
  import { extractBearerToken, verifyInternalToken, } from "../integrations/internal-token.js";
42
43
  /**
43
44
  * Framework route the background function actually runs — sibling to
@@ -87,8 +88,8 @@ export const AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD = "__agentNativeProcessorRou
87
88
  * app id is configured (single-template deploy).
88
89
  */
89
90
  function resolveWorkspaceBackgroundFunctionUrlPath() {
90
- const raw = process.env.AGENT_NATIVE_WORKSPACE_APP_ID;
91
- if (typeof raw !== "string")
91
+ const raw = getAppConfig().app.workspaceId;
92
+ if (raw === undefined)
92
93
  return null;
93
94
  // Mirror the workspace app-id normalization (resources/store.ts): take the
94
95
  // first path segment and accept only the safe slug shape used for function
@@ -8,6 +8,7 @@
8
8
  * Built-in engines (anthropic, ai-sdk) are auto-registered by builtin.ts.
9
9
  */
10
10
  import { createRequire } from "node:module";
11
+ import { getAppConfig } from "../../app-config/index.js";
11
12
  import { assertCredentialStoreReadable, canUseDeployCredentialFallbackForRequest, getBuilderCredentialAuthFailure, getProviderCredentialAuthFailure, readDeployCredentialEnv, resolveBuilderCredentialsDetailed, resolveSecret, } from "../../server/credential-provider.js";
12
13
  import { getSetting } from "../../settings/store.js";
13
14
  import { getAgentAppModelDefaultForCurrentRequest } from "../app-model-defaults.js";
@@ -281,7 +282,7 @@ function assertAgentEnginePackageInstalled(entry) {
281
282
  * markers can skip rejected deploy keys.
282
283
  */
283
284
  export function detectEngineFromEnv() {
284
- const preferByo = /^(1|true)$/i.test(process.env.AGENT_ENGINE_PREFER_BYO_KEY ?? "");
285
+ const preferByo = getAppConfig().agent.preferBringYourOwnKey;
285
286
  if (preferByo) {
286
287
  for (const entry of _registry.values()) {
287
288
  if (entry.name === "builder")
@@ -359,7 +360,7 @@ async function hasUsableEnvKeys(entry) {
359
360
  * on `missing_credentials`.
360
361
  */
361
362
  export async function detectEngineFromEnvForRequest() {
362
- const preferByo = /^(1|true)$/i.test(process.env.AGENT_ENGINE_PREFER_BYO_KEY ?? "");
363
+ const preferByo = getAppConfig().agent.preferBringYourOwnKey;
363
364
  if (preferByo) {
364
365
  for (const entry of _registry.values()) {
365
366
  if (entry.name === "builder")
@@ -445,7 +446,7 @@ export async function detectEngineFromUserSecrets(identity) {
445
446
  }
446
447
  return true;
447
448
  };
448
- const preferByo = /^(1|true)$/i.test(process.env.AGENT_ENGINE_PREFER_BYO_KEY ?? "");
449
+ const preferByo = getAppConfig().agent.preferBringYourOwnKey;
449
450
  if (preferByo) {
450
451
  for (const entry of _registry.values()) {
451
452
  if (entry.name === "builder")
@@ -836,7 +837,7 @@ export async function resolveEngine(config) {
836
837
  return entry.create(await engineCreateConfigForEntry(entry, apiKey, undefined, "explicit", apiKeyEnvVar, credentialIdentity));
837
838
  }
838
839
  // 4. Env var — explicit engine name override
839
- const envEngine = process.env.AGENT_ENGINE;
840
+ const envEngine = getAppConfig().agent.engine;
840
841
  if (envEngine) {
841
842
  const entry = _registry.get(envEngine);
842
843
  if (entry) {
@@ -1,3 +1,4 @@
1
+ import { getAppConfig } from "../app-config/index.js";
1
2
  import { captureError } from "../server/capture-error.js";
2
3
  import { isLlmCredentialError, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./engine/credential-errors.js";
3
4
  import { classifyTerminalErrorCode, describeErrorWithCauses, isProviderConnectionError, } from "./engine/error-detail.js";
@@ -376,12 +377,9 @@ export function resolveRunSoftTimeoutMs(overrideMs, options) {
376
377
  if (typeof overrideMs === "number" && Number.isFinite(overrideMs)) {
377
378
  return clampHosted(Math.max(0, overrideMs));
378
379
  }
379
- const envValue = process.env.AGENT_RUN_SOFT_TIMEOUT_MS;
380
- if (envValue !== undefined) {
381
- const raw = Number(envValue);
382
- if (Number.isFinite(raw) && raw >= 0)
383
- return clampHosted(raw);
384
- }
380
+ const configured = getAppConfig().agent.runSoftTimeoutMs;
381
+ if (configured !== undefined)
382
+ return clampHosted(configured);
385
383
  // A background-function run uses the full background budget by default; the
386
384
  // foreground default (40s) is unchanged.
387
385
  if (background) {
@@ -392,22 +390,12 @@ export function resolveRunSoftTimeoutMs(overrideMs, options) {
392
390
  : 0;
393
391
  }
394
392
  export function resolveCompletedRunRetentionMs() {
395
- const envValue = process.env.AGENT_RUN_RETENTION_MS;
396
- if (envValue !== undefined) {
397
- const raw = Number(envValue);
398
- if (Number.isFinite(raw) && raw >= 0)
399
- return raw;
400
- }
401
- return DEFAULT_COMPLETED_RUN_RETENTION_MS;
393
+ return (getAppConfig().agent.completedRunRetentionMs ??
394
+ DEFAULT_COMPLETED_RUN_RETENTION_MS);
402
395
  }
403
396
  export function resolveErroredRunRetentionMs() {
404
- const envValue = process.env.AGENT_ERRORED_RUN_RETENTION_MS;
405
- if (envValue !== undefined) {
406
- const raw = Number(envValue);
407
- if (Number.isFinite(raw) && raw >= 0)
408
- return raw;
409
- }
410
- return DEFAULT_ERRORED_RUN_RETENTION_MS;
397
+ return (getAppConfig().agent.erroredRunRetentionMs ??
398
+ DEFAULT_ERRORED_RUN_RETENTION_MS);
411
399
  }
412
400
  function isTerminalRunEvent(event) {
413
401
  return (event.type === "done" ||
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * A2A transport policy.
4
+ *
5
+ * Deliberately does not carry `A2A_SECRET`. That value is a secret, and it is
6
+ * also the key-derivation root the secrets vault uses to decrypt its own rows
7
+ * (`secrets/crypto.ts`) — so it is read below this layer, before configuration
8
+ * is available. Secrets resolve through `readDeployCredentialEnv` and the
9
+ * vault; this domain holds only the policy toggles around them.
10
+ */
11
+ export declare const a2aConfig: z.ZodObject<{
12
+ allowedOrigins: z.ZodDefault<z.ZodArray<z.ZodString>>;
13
+ allowUnsignedInternal: z.ZodDefault<z.ZodBoolean>;
14
+ }, z.core.$strip>;
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * A2A transport policy.
4
+ *
5
+ * Deliberately does not carry `A2A_SECRET`. That value is a secret, and it is
6
+ * also the key-derivation root the secrets vault uses to decrypt its own rows
7
+ * (`secrets/crypto.ts`) — so it is read below this layer, before configuration
8
+ * is available. Secrets resolve through `readDeployCredentialEnv` and the
9
+ * vault; this domain holds only the policy toggles around them.
10
+ */
11
+ export const a2aConfig = z.object({
12
+ /**
13
+ * Extra origins this deployment will treat as its own private siblings.
14
+ *
15
+ * Escape hatch for contexts that never receive the gateway manifest — the
16
+ * action CLI and one-off scripts. Only origins the deployment configured for
17
+ * itself belong here; a value that arrived on a request must never reach it,
18
+ * because these bypass the SSRF guard by construction.
19
+ *
20
+ * Entries are checked for non-emptiness but not parsed as URLs here. A
21
+ * validation failure in this schema throws out of `getAppConfig()`, which
22
+ * every config read shares — so one typo in an allow-list would stop the
23
+ * process booting at all rather than making one sibling unreachable.
24
+ * `normalizeAllowedPrivateOriginKeys` parses each entry, requires http/https,
25
+ * and matches on exact `hostname:port`, so a malformed entry is skipped and
26
+ * the private-IP guard stays closed.
27
+ */
28
+ allowedOrigins: z
29
+ .array(z.string().min(1))
30
+ .default([])
31
+ .meta({
32
+ env: ["AGENT_NATIVE_A2A_ALLOWED_ORIGINS"],
33
+ doc: "Comma-separated extra origins trusted as private A2A siblings.",
34
+ }),
35
+ allowUnsignedInternal: z
36
+ .boolean()
37
+ .default(false)
38
+ .meta({
39
+ env: ["A2A_ALLOW_UNSIGNED_INTERNAL"],
40
+ doc: "Trust unsigned internal self-dispatch on an unrecognized non-production host. Never grants trust in production.",
41
+ }),
42
+ });
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Agent engine and model selection.
4
+ *
5
+ * These are the values `resolveEngine()` already resolves through a documented
6
+ * seven-step ladder. The declared field replaces step 2 of that ladder only —
7
+ * the explicit `engineOption` argument stays a function parameter and stays
8
+ * above it, because it is per-call rather than per-process.
9
+ */
10
+ export declare const agentConfig: z.ZodObject<{
11
+ engine: z.ZodOptional<z.ZodString>;
12
+ model: z.ZodOptional<z.ZodString>;
13
+ mode: z.ZodOptional<z.ZodString>;
14
+ preferBringYourOwnKey: z.ZodDefault<z.ZodBoolean>;
15
+ runSoftTimeoutMs: z.ZodOptional<z.ZodNumber>;
16
+ completedRunRetentionMs: z.ZodOptional<z.ZodNumber>;
17
+ erroredRunRetentionMs: z.ZodOptional<z.ZodNumber>;
18
+ }, z.core.$strip>;
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Agent engine and model selection.
4
+ *
5
+ * These are the values `resolveEngine()` already resolves through a documented
6
+ * seven-step ladder. The declared field replaces step 2 of that ladder only —
7
+ * the explicit `engineOption` argument stays a function parameter and stays
8
+ * above it, because it is per-call rather than per-process.
9
+ */
10
+ export const agentConfig = z.object({
11
+ engine: z
12
+ .string()
13
+ .min(1)
14
+ .optional()
15
+ .meta({
16
+ env: ["AGENT_ENGINE"],
17
+ doc: "Name of the registered agent engine to use.",
18
+ }),
19
+ model: z
20
+ .string()
21
+ .min(1)
22
+ .optional()
23
+ .meta({
24
+ env: ["AGENT_MODEL"],
25
+ doc: "Model the agent runs with, when the caller does not pass one.",
26
+ }),
27
+ mode: z
28
+ .string()
29
+ .min(1)
30
+ .optional()
31
+ .meta({
32
+ env: ["AGENT_MODE"],
33
+ doc: 'Runtime mode. "production" turns off development-only agent behavior.',
34
+ }),
35
+ preferBringYourOwnKey: z
36
+ .boolean()
37
+ .default(false)
38
+ .meta({
39
+ env: ["AGENT_ENGINE_PREFER_BYO_KEY"],
40
+ doc: "Skip the Builder-managed engine and select a directly configured provider key first.",
41
+ }),
42
+ // These three stay `.optional()` rather than carrying a default: the value
43
+ // that applies when they are unset depends on whether the run is hosted and
44
+ // whether it is a background function, so it belongs to the resolver.
45
+ runSoftTimeoutMs: z
46
+ .number()
47
+ .nonnegative()
48
+ .optional()
49
+ .meta({
50
+ env: ["AGENT_RUN_SOFT_TIMEOUT_MS"],
51
+ doc: "Soft timeout for an agent run, in milliseconds. 0 disables it.",
52
+ }),
53
+ completedRunRetentionMs: z
54
+ .number()
55
+ .nonnegative()
56
+ .optional()
57
+ .meta({
58
+ env: ["AGENT_RUN_RETENTION_MS"],
59
+ doc: "How long a completed agent run row is kept, in milliseconds.",
60
+ }),
61
+ erroredRunRetentionMs: z
62
+ .number()
63
+ .nonnegative()
64
+ .optional()
65
+ .meta({
66
+ env: ["AGENT_ERRORED_RUN_RETENTION_MS"],
67
+ doc: "How long an errored agent run row is kept, in milliseconds.",
68
+ }),
69
+ });
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * App identity.
4
+ *
5
+ * Three fields, not one, because the eight environment keys that spell "which
6
+ * app is this" were never all the same question:
7
+ *
8
+ * - `id` is this deployment's own identity — data programs, onboarding, the
9
+ * CLI, and agent model defaults scope by it.
10
+ * - `workspaceId` is the identity a workspace deploy assigns. `vault_grants`
11
+ * rows are written with it, so credential scoping must prefer it over `id`
12
+ * or an app would look up grants under a name nobody granted.
13
+ * - `name` is the user-facing display name ("Acme"), used in transactional
14
+ * emails and page titles. It is not an identifier, and only leaks into
15
+ * identity resolution as a last-resort fallback in readers that had nothing
16
+ * better.
17
+ *
18
+ * Collapsing these into one field is the tempting simplification and it is
19
+ * wrong: it would silently repoint credential grant lookups.
20
+ *
21
+ * None of them has a default. `credentialProvider` denies access when no app
22
+ * identity is configured, and a default of `"app"` would turn that denial into
23
+ * a lookup scoped to an app literally named `app`. Readers that want a
24
+ * placeholder apply it themselves, where it is visible.
25
+ */
26
+ export declare const appConfig: z.ZodObject<{
27
+ id: z.ZodOptional<z.ZodString>;
28
+ workspaceId: z.ZodOptional<z.ZodString>;
29
+ name: z.ZodOptional<z.ZodString>;
30
+ url: z.ZodOptional<z.ZodString>;
31
+ packageName: z.ZodOptional<z.ZodString>;
32
+ template: z.ZodOptional<z.ZodString>;
33
+ }, z.core.$strip>;