@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.
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +2 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +7 -2
- package/corpus/templates/clips/app/routes/record.tsx +147 -5
- package/dist/a2a/auth-policy.d.ts +0 -6
- package/dist/a2a/auth-policy.js +2 -1
- package/dist/a2a/client.js +10 -12
- package/dist/agent/durable-background.js +3 -2
- package/dist/agent/engine/registry.js +5 -4
- package/dist/agent/run-manager.js +8 -20
- package/dist/app-config/a2a.d.ts +14 -0
- package/dist/app-config/a2a.js +42 -0
- package/dist/app-config/agent.d.ts +18 -0
- package/dist/app-config/agent.js +69 -0
- package/dist/app-config/app.d.ts +33 -0
- package/dist/app-config/app.js +100 -0
- package/dist/app-config/describe.d.ts +26 -0
- package/dist/app-config/describe.js +61 -0
- package/dist/app-config/env-layer.d.ts +37 -0
- package/dist/app-config/env-layer.js +136 -0
- package/dist/app-config/index.d.ts +2 -0
- package/dist/app-config/index.js +2 -0
- package/dist/app-config/integrations.d.ts +5 -0
- package/dist/app-config/integrations.js +11 -0
- package/dist/app-config/private-blob.d.ts +14 -0
- package/dist/app-config/private-blob.js +19 -0
- package/dist/app-config/schema.d.ts +55 -0
- package/dist/app-config/schema.js +31 -0
- package/dist/app-config/store.d.ts +42 -0
- package/dist/app-config/store.js +104 -0
- package/dist/app-config/workspace.d.ts +21 -0
- package/dist/app-config/workspace.js +35 -0
- package/dist/cli/agent.js +2 -1
- package/dist/cli/code-agent-executor.js +5 -4
- package/dist/client/analytics.d.ts +9 -0
- package/dist/client/frame.js +11 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/deploy/build.js +36 -0
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/a2a-continuation-processor.js +12 -9
- package/dist/integrations/adapters/email.js +2 -1
- package/dist/integrations/adapters/telegram.js +2 -1
- package/dist/integrations/adapters/whatsapp.js +2 -1
- package/dist/integrations/webhook-handler.d.ts +7 -2
- package/dist/integrations/webhook-handler.js +9 -31
- package/dist/observability/routes.d.ts +5 -5
- package/dist/onboarding/app-profile.js +2 -4
- package/dist/onboarding/default-steps.js +4 -4
- package/dist/private-blob/registry.d.ts +5 -0
- package/dist/private-blob/registry.js +19 -8
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.js +4 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/action-filters-a2a.js +5 -4
- package/dist/server/agent-chat/run-code-tools.js +3 -4
- package/dist/server/agent-chat-plugin.js +4 -3
- package/dist/server/agent-discovery.js +7 -5
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/app-origin-config.d.ts +23 -0
- package/dist/server/app-origin-config.js +43 -0
- package/dist/server/auth.js +21 -1
- package/dist/server/core-routes-plugin.js +3 -3
- package/dist/server/credential-provider.js +7 -8
- package/dist/server/embed-session.js +2 -1
- package/dist/server/identity-sso.js +6 -2
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/oauth-public-origin.js +10 -14
- package/dist/server/oauth-return-url.js +5 -9
- package/dist/server/self-dispatch.js +5 -2
- package/dist/server/ssr-handler.js +2 -0
- package/dist/server/workspace-provider-oauth.js +2 -4
- package/dist/sharing/actions/create-agent-resource-link.js +2 -2
- package/dist/usage/metrics-store.js +2 -1
- package/package.json +3 -3
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
IconPlayerPause,
|
|
4
4
|
IconPlayerPlay,
|
|
5
5
|
IconPlayerStop,
|
|
6
|
-
|
|
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
|
-
<
|
|
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: "
|
|
1035
|
-
cancelShortcut: "
|
|
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
|
|
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
|
-
}, [
|
|
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={
|
|
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={
|
|
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
|
-
|
|
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;
|
package/dist/a2a/auth-policy.js
CHANGED
|
@@ -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 (
|
|
52
|
+
if (getAppConfig().a2a.allowUnsignedInternal)
|
|
52
53
|
return true;
|
|
53
54
|
return opts.loopback === true;
|
|
54
55
|
}
|
package/dist/a2a/client.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 =
|
|
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 =
|
|
91
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
380
|
-
if (
|
|
381
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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>;
|