@agent-native/core 0.101.2 → 0.101.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +18 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/credentials/index.ts +41 -8
- package/corpus/core/src/extensions/fetch-tool.ts +33 -3
- package/corpus/core/src/notifications/channels.ts +20 -6
- package/corpus/core/src/secrets/crypto.ts +26 -10
- package/corpus/core/src/secrets/storage.ts +16 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +60 -7
- package/corpus/core/src/server/derived-secret.ts +5 -1
- package/corpus/templates/analytics/changelog/2026-07-14-long-running-cross-app-analyses-now-continue-reliably-in-the.md +6 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +8 -4
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +44 -81
- package/corpus/templates/clips/app/i18n/ar-SA.ts +0 -8
- package/corpus/templates/clips/app/i18n/de-DE.ts +0 -9
- package/corpus/templates/clips/app/i18n/en-US.ts +0 -8
- package/corpus/templates/clips/app/i18n/es-ES.ts +0 -9
- package/corpus/templates/clips/app/i18n/fr-FR.ts +0 -9
- package/corpus/templates/clips/app/i18n/hi-IN.ts +0 -8
- package/corpus/templates/clips/app/i18n/ja-JP.ts +0 -9
- package/corpus/templates/clips/app/i18n/ko-KR.ts +0 -9
- package/corpus/templates/clips/app/i18n/pt-BR.ts +0 -9
- package/corpus/templates/clips/app/i18n/zh-CN.ts +0 -7
- package/corpus/templates/clips/app/i18n/zh-TW.ts +0 -7
- package/corpus/templates/clips/changelog/2026-07-14-share-dialogs-show-separate-links-for-people-and-agents.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-14-dispatch-now-distinguishes-connected-agents-from-locally-mou.md +6 -0
- package/dist/credentials/index.d.ts +12 -7
- package/dist/credentials/index.d.ts.map +1 -1
- package/dist/credentials/index.js +33 -8
- package/dist/credentials/index.js.map +1 -1
- package/dist/extensions/fetch-tool.d.ts +13 -1
- package/dist/extensions/fetch-tool.d.ts.map +1 -1
- package/dist/extensions/fetch-tool.js +9 -2
- package/dist/extensions/fetch-tool.js.map +1 -1
- package/dist/notifications/channels.js +20 -4
- package/dist/notifications/channels.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/crypto.d.ts +15 -7
- package/dist/secrets/crypto.d.ts.map +1 -1
- package/dist/secrets/crypto.js +23 -10
- package/dist/secrets/crypto.js.map +1 -1
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +16 -3
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +20 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +38 -4
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/derived-secret.d.ts +1 -1
- package/dist/server/derived-secret.d.ts.map +1 -1
- package/dist/server/derived-secret.js.map +1 -1
- package/package.json +2 -2
- package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +0 -68
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
} from "@agent-native/core/client";
|
|
8
8
|
import {
|
|
9
9
|
IconCode,
|
|
10
|
-
IconChevronDown,
|
|
11
10
|
IconExternalLink,
|
|
12
11
|
IconLink,
|
|
13
12
|
IconMail,
|
|
@@ -33,13 +32,7 @@ import {
|
|
|
33
32
|
type SharesResponse,
|
|
34
33
|
type Visibility,
|
|
35
34
|
} from "@/components/sharing/share-ui";
|
|
36
|
-
import { SlackShareHint } from "@/components/sharing/slack-share-hint";
|
|
37
35
|
import { Button } from "@/components/ui/button";
|
|
38
|
-
import {
|
|
39
|
-
Collapsible,
|
|
40
|
-
CollapsibleContent,
|
|
41
|
-
CollapsibleTrigger,
|
|
42
|
-
} from "@/components/ui/collapsible";
|
|
43
36
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
|
44
37
|
import { Input } from "@/components/ui/input";
|
|
45
38
|
import { Label } from "@/components/ui/label";
|
|
@@ -317,7 +310,6 @@ function LinkTab({
|
|
|
317
310
|
const agentLinkRequestIdRef = useRef(0);
|
|
318
311
|
const [agentContextUrl, setAgentContextUrl] = useState("");
|
|
319
312
|
const [agentLinkError, setAgentLinkError] = useState(false);
|
|
320
|
-
const [agentShareOpen, setAgentShareOpen] = useState(false);
|
|
321
313
|
|
|
322
314
|
useEffect(() => {
|
|
323
315
|
createAgentLinkAsyncRef.current = createAgentLink.mutateAsync;
|
|
@@ -352,26 +344,22 @@ function LinkTab({
|
|
|
352
344
|
setAgentLinkError(false);
|
|
353
345
|
if (!sharesLoaded) return;
|
|
354
346
|
|
|
355
|
-
if (!isPublic
|
|
347
|
+
if (!isPublic) {
|
|
356
348
|
void loadAgentContextUrl();
|
|
357
349
|
}
|
|
358
350
|
|
|
359
351
|
return () => {
|
|
360
352
|
agentLinkRequestIdRef.current += 1;
|
|
361
353
|
};
|
|
362
|
-
}, [
|
|
363
|
-
agentShareOpen,
|
|
364
|
-
isPublic,
|
|
365
|
-
loadAgentContextUrl,
|
|
366
|
-
recordingId,
|
|
367
|
-
sharesLoaded,
|
|
368
|
-
visibility,
|
|
369
|
-
]);
|
|
354
|
+
}, [isPublic, loadAgentContextUrl, recordingId, sharesLoaded, visibility]);
|
|
370
355
|
|
|
356
|
+
const agentLink = isPublic ? shareUrl : agentContextUrl;
|
|
371
357
|
const agentShareDisabled =
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
358
|
+
visibilityPending ||
|
|
359
|
+
!sharesLoaded ||
|
|
360
|
+
(!isPublic && (isPending || createAgentLink.isPending || !agentContextUrl));
|
|
361
|
+
const agentPrompt = agentLink
|
|
362
|
+
? t("shareDialog.agentPrompt", { agentContextUrl: agentLink })
|
|
375
363
|
: "";
|
|
376
364
|
|
|
377
365
|
return (
|
|
@@ -399,67 +387,42 @@ function LinkTab({
|
|
|
399
387
|
}
|
|
400
388
|
/>
|
|
401
389
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
<
|
|
415
|
-
<
|
|
416
|
-
{t("shareDialog.
|
|
417
|
-
</
|
|
418
|
-
<
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
<div className="flex items-center justify-between gap-2 rounded-md border border-border bg-background px-3 py-2">
|
|
439
|
-
<p className="text-xs text-muted-foreground">
|
|
440
|
-
{t("shareDialog.agentLinkUnavailable")}
|
|
441
|
-
</p>
|
|
442
|
-
<Button
|
|
443
|
-
type="button"
|
|
444
|
-
variant="ghost"
|
|
445
|
-
size="sm"
|
|
446
|
-
className="h-7"
|
|
447
|
-
onClick={() => void loadAgentContextUrl()}
|
|
448
|
-
disabled={createAgentLink.isPending}
|
|
449
|
-
>
|
|
450
|
-
{t("shareDialog.retryAgentLink")}
|
|
451
|
-
</Button>
|
|
452
|
-
</div>
|
|
453
|
-
) : null}
|
|
454
|
-
<CopyField
|
|
455
|
-
label={t("shareDialog.copyAgentPrompt")}
|
|
456
|
-
value={agentPrompt}
|
|
457
|
-
disabled={agentShareDisabled}
|
|
458
|
-
/>
|
|
459
|
-
</div>
|
|
460
|
-
</CollapsibleContent>
|
|
461
|
-
</Collapsible>
|
|
462
|
-
) : null}
|
|
390
|
+
<div className="space-y-2">
|
|
391
|
+
<CopyField
|
|
392
|
+
label={t("shareDialog.shareWithAgents")}
|
|
393
|
+
value={agentLink}
|
|
394
|
+
disabled={agentShareDisabled}
|
|
395
|
+
/>
|
|
396
|
+
{sharesLoaded && !isPublic ? (
|
|
397
|
+
<>
|
|
398
|
+
<p className="text-xs text-muted-foreground">
|
|
399
|
+
{t("shareDialog.agentTokenDescription")}
|
|
400
|
+
</p>
|
|
401
|
+
{agentLinkError ? (
|
|
402
|
+
<div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
|
|
403
|
+
<p className="text-xs text-muted-foreground">
|
|
404
|
+
{t("shareDialog.agentLinkUnavailable")}
|
|
405
|
+
</p>
|
|
406
|
+
<Button
|
|
407
|
+
type="button"
|
|
408
|
+
variant="ghost"
|
|
409
|
+
size="sm"
|
|
410
|
+
className="h-7"
|
|
411
|
+
onClick={() => void loadAgentContextUrl()}
|
|
412
|
+
disabled={createAgentLink.isPending}
|
|
413
|
+
>
|
|
414
|
+
{t("shareDialog.retryAgentLink")}
|
|
415
|
+
</Button>
|
|
416
|
+
</div>
|
|
417
|
+
) : null}
|
|
418
|
+
<CopyField
|
|
419
|
+
label={t("shareDialog.copyAgentPrompt")}
|
|
420
|
+
value={agentPrompt}
|
|
421
|
+
disabled={agentShareDisabled}
|
|
422
|
+
/>
|
|
423
|
+
</>
|
|
424
|
+
) : null}
|
|
425
|
+
</div>
|
|
463
426
|
|
|
464
427
|
{sharesLoaded && !isPublic && canManage ? (
|
|
465
428
|
<MakePublicCard
|
|
@@ -887,14 +887,6 @@ const messages = {
|
|
|
887
887
|
"سيفقد {{email}} الوصول إلى هذه المؤسسة. يمكنك دائمًا دعوته مرة أخرى.",
|
|
888
888
|
remove: "إزالة",
|
|
889
889
|
},
|
|
890
|
-
slackShareHint: {
|
|
891
|
-
playsInline: "يعمل داخل Slack",
|
|
892
|
-
connectedDescription:
|
|
893
|
-
"الصق هذا الرابط في أي مساحة عمل متصلة لتشغيله داخلها.",
|
|
894
|
-
makeInline: "اجعله يعمل داخل Slack",
|
|
895
|
-
connectDescription: "قم بتوصيل مساحة عمل حتى يظهر هذا الرابط كفيديو.",
|
|
896
|
-
connect: "اتصال",
|
|
897
|
-
},
|
|
898
890
|
commentsPanel: {
|
|
899
891
|
disabled: "تم تعطيل التعليقات لهذا التسجيل.",
|
|
900
892
|
beFirst: "كن أول من يعلق",
|
|
@@ -911,15 +911,6 @@ Alle sichtbaren Änderungen für Clips-Nutzer werden hier dokumentiert. Du kanns
|
|
|
911
911
|
"{{email}} verliert den Zugriff auf diese Organisation. Du kannst die Person jederzeit erneut einladen.",
|
|
912
912
|
remove: "Entfernen",
|
|
913
913
|
},
|
|
914
|
-
slackShareHint: {
|
|
915
|
-
playsInline: "Wird inline in Slack abgespielt",
|
|
916
|
-
connectedDescription:
|
|
917
|
-
"Füge diesen Link in einem verbundenen Workspace ein, um ihn inline abzuspielen.",
|
|
918
|
-
makeInline: "Inline-Wiedergabe in Slack aktivieren",
|
|
919
|
-
connectDescription:
|
|
920
|
-
"Verbinde einen Workspace, damit dieser Link als Video angezeigt wird.",
|
|
921
|
-
connect: "Verbinden",
|
|
922
|
-
},
|
|
923
914
|
commentsPanel: {
|
|
924
915
|
disabled: "Kommentare sind für diese Aufzeichnung deaktiviert.",
|
|
925
916
|
beFirst: "Schreibe den ersten Kommentar",
|
|
@@ -882,14 +882,6 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
882
882
|
"{{email}} will lose access to this organization. You can always invite them back.",
|
|
883
883
|
remove: "Remove",
|
|
884
884
|
},
|
|
885
|
-
slackShareHint: {
|
|
886
|
-
playsInline: "Plays inline in Slack",
|
|
887
|
-
connectedDescription:
|
|
888
|
-
"Paste this link in any connected workspace to play it inline.",
|
|
889
|
-
makeInline: "Make it play inline in Slack",
|
|
890
|
-
connectDescription: "Connect a workspace so this link unfurls as a video.",
|
|
891
|
-
connect: "Connect",
|
|
892
|
-
},
|
|
893
885
|
commentsPanel: {
|
|
894
886
|
disabled: "Comments are disabled for this recording.",
|
|
895
887
|
beFirst: "Be the first to comment",
|
|
@@ -904,15 +904,6 @@ Todos los cambios visibles para los usuarios de Clips se documentan aquí. Puede
|
|
|
904
904
|
"{{email}} perderá acceso a esta organización. Siempre puedes volver a invitarlo.",
|
|
905
905
|
remove: "Eliminar",
|
|
906
906
|
},
|
|
907
|
-
slackShareHint: {
|
|
908
|
-
playsInline: "Se reproduce inline en Slack",
|
|
909
|
-
connectedDescription:
|
|
910
|
-
"Pega este enlace en cualquier espacio conectado para reproducirlo inline.",
|
|
911
|
-
makeInline: "Haz que se reproduzca inline en Slack",
|
|
912
|
-
connectDescription:
|
|
913
|
-
"Conecta un espacio para que este enlace se despliegue como video.",
|
|
914
|
-
connect: "Conectar",
|
|
915
|
-
},
|
|
916
907
|
commentsPanel: {
|
|
917
908
|
disabled: "Los comentarios están desactivados para esta grabación.",
|
|
918
909
|
beFirst: "Sé la primera persona en comentar",
|
|
@@ -906,15 +906,6 @@ Tous les changements visibles par les utilisateurs de Clips sont documentés ici
|
|
|
906
906
|
"{{email}} perdra l’accès à cette organisation. Vous pourrez toujours l’inviter à nouveau.",
|
|
907
907
|
remove: "Retirer",
|
|
908
908
|
},
|
|
909
|
-
slackShareHint: {
|
|
910
|
-
playsInline: "Lecture inline dans Slack",
|
|
911
|
-
connectedDescription:
|
|
912
|
-
"Collez ce lien dans n’importe quel espace connecté pour le lire inline.",
|
|
913
|
-
makeInline: "Le lire inline dans Slack",
|
|
914
|
-
connectDescription:
|
|
915
|
-
"Connectez un espace pour que ce lien se déplie en vidéo.",
|
|
916
|
-
connect: "Connecter",
|
|
917
|
-
},
|
|
918
909
|
commentsPanel: {
|
|
919
910
|
disabled: "Les commentaires sont désactivés pour cet enregistrement.",
|
|
920
911
|
beFirst: "Soyez le premier à commenter",
|
|
@@ -876,14 +876,6 @@ Clips में उपयोगकर्ताओं को दिखने व
|
|
|
876
876
|
"{{email}} इस संगठन तक पहुंच खो देगा। आप उन्हें हमेशा वापस आमंत्रित कर सकते हैं।",
|
|
877
877
|
remove: "हटाएं",
|
|
878
878
|
},
|
|
879
|
-
slackShareHint: {
|
|
880
|
-
playsInline: "Slack में इनलाइन चलता है",
|
|
881
|
-
connectedDescription:
|
|
882
|
-
"इसे इनलाइन चलाने के लिए यह लिंक किसी भी जुड़े हुए वर्कस्पेस में पेस्ट करें।",
|
|
883
|
-
makeInline: "इसे Slack में इनलाइन चलाएं",
|
|
884
|
-
connectDescription: "एक वर्कस्पेस कनेक्ट करें ताकि यह लिंक वीडियो के रूप में खुले।",
|
|
885
|
-
connect: "कनेक्ट करें",
|
|
886
|
-
},
|
|
887
879
|
commentsPanel: {
|
|
888
880
|
disabled: "इस रिकॉर्डिंग के लिए टिप्पणियां बंद हैं।",
|
|
889
881
|
beFirst: "पहली टिप्पणी करें",
|
|
@@ -893,15 +893,6 @@ Clips のユーザー向けの主な変更はここに記録されます。コ
|
|
|
893
893
|
"{{email}} はこの組織へのアクセスを失います。いつでも再招待できます。",
|
|
894
894
|
remove: "削除",
|
|
895
895
|
},
|
|
896
|
-
slackShareHint: {
|
|
897
|
-
playsInline: "Slack 内でインライン再生",
|
|
898
|
-
connectedDescription:
|
|
899
|
-
"このリンクを接続済みワークスペースに貼り付けるとインライン再生されます。",
|
|
900
|
-
makeInline: "Slack でインライン再生する",
|
|
901
|
-
connectDescription:
|
|
902
|
-
"ワークスペースを接続すると、このリンクが動画として展開されます。",
|
|
903
|
-
connect: "接続",
|
|
904
|
-
},
|
|
905
896
|
commentsPanel: {
|
|
906
897
|
disabled: "この録画ではコメントが無効です。",
|
|
907
898
|
beFirst: "最初にコメントする",
|
|
@@ -883,15 +883,6 @@ Clips의 모든 사용자 대상 변경 사항은 여기에 기록됩니다. 명
|
|
|
883
883
|
"{{email}}은 이 조직에 대한 액세스 권한을 잃습니다. 언제든 다시 초대할 수 있습니다.",
|
|
884
884
|
remove: "제거",
|
|
885
885
|
},
|
|
886
|
-
slackShareHint: {
|
|
887
|
-
playsInline: "Slack에서 인라인 재생",
|
|
888
|
-
connectedDescription:
|
|
889
|
-
"이 링크를 연결된 워크스페이스에 붙여넣으면 인라인으로 재생됩니다.",
|
|
890
|
-
makeInline: "Slack에서 인라인 재생하기",
|
|
891
|
-
connectDescription:
|
|
892
|
-
"워크스페이스를 연결하면 이 링크가 동영상으로 펼쳐집니다.",
|
|
893
|
-
connect: "연결",
|
|
894
|
-
},
|
|
895
886
|
commentsPanel: {
|
|
896
887
|
disabled: "이 녹화에서는 댓글이 비활성화되어 있습니다.",
|
|
897
888
|
beFirst: "첫 댓글을 남겨보세요",
|
|
@@ -900,15 +900,6 @@ Todas as mudanças visíveis para usuários do Clips são documentadas aqui. Voc
|
|
|
900
900
|
"{{email}} perderá acesso a esta organização. Você sempre pode convidá-lo novamente.",
|
|
901
901
|
remove: "Remover",
|
|
902
902
|
},
|
|
903
|
-
slackShareHint: {
|
|
904
|
-
playsInline: "Reproduz inline no Slack",
|
|
905
|
-
connectedDescription:
|
|
906
|
-
"Cole este link em qualquer workspace conectado para reproduzi-lo inline.",
|
|
907
|
-
makeInline: "Faça reproduzir inline no Slack",
|
|
908
|
-
connectDescription:
|
|
909
|
-
"Conecte um workspace para que este link abra como vídeo.",
|
|
910
|
-
connect: "Conectar",
|
|
911
|
-
},
|
|
912
903
|
commentsPanel: {
|
|
913
904
|
disabled: "Os comentários estão desativados para esta gravação.",
|
|
914
905
|
beFirst: "Seja a primeira pessoa a comentar",
|
|
@@ -849,13 +849,6 @@ Clips 中所有面向用户的重要更改都会记录在这里。你可以随
|
|
|
849
849
|
"{{email}} 将失去对此组织的访问权限。你随时可以重新邀请他们。",
|
|
850
850
|
remove: "移除",
|
|
851
851
|
},
|
|
852
|
-
slackShareHint: {
|
|
853
|
-
playsInline: "可在 Slack 中内联播放",
|
|
854
|
-
connectedDescription: "将此链接粘贴到任何已连接工作区即可内联播放。",
|
|
855
|
-
makeInline: "让它在 Slack 中内联播放",
|
|
856
|
-
connectDescription: "连接一个工作区,让此链接展开为视频。",
|
|
857
|
-
connect: "连接",
|
|
858
|
-
},
|
|
859
852
|
commentsPanel: {
|
|
860
853
|
disabled: "此录制已禁用评论。",
|
|
861
854
|
beFirst: "成为第一个评论的人",
|
|
@@ -842,13 +842,6 @@ const messages = {
|
|
|
842
842
|
"{{email}} 將失去對此組織的存取權限。你隨時可以重新邀請他們。",
|
|
843
843
|
remove: "移除",
|
|
844
844
|
},
|
|
845
|
-
slackShareHint: {
|
|
846
|
-
playsInline: "可在 Slack 中行內播放",
|
|
847
|
-
connectedDescription: "將此連結貼上到任何已連線工作區即可行內播放。",
|
|
848
|
-
makeInline: "讓它在 Slack 中行內播放",
|
|
849
|
-
connectDescription: "連線一個工作區,讓此連結展開為影片。",
|
|
850
|
-
connect: "連線",
|
|
851
|
-
},
|
|
852
845
|
commentsPanel: {
|
|
853
846
|
disabled: "此錄製已停用評論。",
|
|
854
847
|
beFirst: "成為第一個評論的人",
|
|
@@ -14,16 +14,21 @@ export declare function resolveCredentialForScope(key: string, ctx: CredentialCo
|
|
|
14
14
|
scope: CredentialStorageScope;
|
|
15
15
|
}): Promise<string | undefined>;
|
|
16
16
|
/**
|
|
17
|
-
* Resolve a credential
|
|
18
|
-
*
|
|
17
|
+
* Resolve a credential across the encrypted app_secrets store and the legacy
|
|
18
|
+
* settings-backed credential store. User overrides win, followed by the
|
|
19
|
+
* active org/workspace shared value.
|
|
19
20
|
*
|
|
20
21
|
* SECURITY: NEVER reads from process.env. Env vars are global to the
|
|
21
|
-
* deployment and would leak across users in a multi-tenant app.
|
|
22
|
-
* sources are per-user / per-org rows in the SQL `settings` table.
|
|
22
|
+
* deployment and would leak across users in a multi-tenant app.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* 1.
|
|
26
|
-
* 2.
|
|
24
|
+
* Read order:
|
|
25
|
+
* 1. user-scoped app_secrets
|
|
26
|
+
* 2. user-scoped legacy settings credential
|
|
27
|
+
* 3. org-scoped app_secrets
|
|
28
|
+
* 4. legacy workspace-scoped app_secrets for the org
|
|
29
|
+
* 5. org-scoped legacy settings credential
|
|
30
|
+
*
|
|
31
|
+
* Without an active org, step 3 is replaced by the solo workspace scope.
|
|
27
32
|
*/
|
|
28
33
|
export declare function resolveCredential(key: string, ctx: CredentialContext): Promise<string | undefined>;
|
|
29
34
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/credentials/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/credentials/index.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,KAAK,CAAC;AA2CpD;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,iBAAiB,GAAG;IAAE,KAAK,EAAE,sBAAsB,CAAA;CAAE,GACzD,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO7B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA2B7B;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,OAAO,CAAC,CAElB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,iBAAiB,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,iBAAiB,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAcf"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { encryptSecretValue, decryptSecretValue, isEncryptedSecretValue, } from "../secrets/crypto.js";
|
|
2
|
+
import { readAppSecret } from "../secrets/storage.js";
|
|
2
3
|
import { getSetting, putSetting, deleteSetting } from "../settings/store.js";
|
|
3
4
|
const SETTING_PREFIX = "credential:";
|
|
4
5
|
function userCredentialSettingKey(email, key) {
|
|
@@ -26,6 +27,16 @@ async function readCredentialSetting(settingKey) {
|
|
|
26
27
|
return undefined;
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
async function readScopedAppSecret(key, scope, scopeId) {
|
|
31
|
+
try {
|
|
32
|
+
return (await readAppSecret({ key, scope, scopeId }))?.value;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Older databases may not have app_secrets yet. Keep the legacy
|
|
36
|
+
// credential store available while the table bootstraps.
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
29
40
|
/**
|
|
30
41
|
* Resolve a credential from one explicit legacy SQL credential scope.
|
|
31
42
|
*
|
|
@@ -44,20 +55,28 @@ export async function resolveCredentialForScope(key, ctx) {
|
|
|
44
55
|
return readCredentialSetting(userCredentialSettingKey(ctx.userEmail, key));
|
|
45
56
|
}
|
|
46
57
|
/**
|
|
47
|
-
* Resolve a credential
|
|
48
|
-
*
|
|
58
|
+
* Resolve a credential across the encrypted app_secrets store and the legacy
|
|
59
|
+
* settings-backed credential store. User overrides win, followed by the
|
|
60
|
+
* active org/workspace shared value.
|
|
49
61
|
*
|
|
50
62
|
* SECURITY: NEVER reads from process.env. Env vars are global to the
|
|
51
|
-
* deployment and would leak across users in a multi-tenant app.
|
|
52
|
-
*
|
|
63
|
+
* deployment and would leak across users in a multi-tenant app.
|
|
64
|
+
*
|
|
65
|
+
* Read order:
|
|
66
|
+
* 1. user-scoped app_secrets
|
|
67
|
+
* 2. user-scoped legacy settings credential
|
|
68
|
+
* 3. org-scoped app_secrets
|
|
69
|
+
* 4. legacy workspace-scoped app_secrets for the org
|
|
70
|
+
* 5. org-scoped legacy settings credential
|
|
53
71
|
*
|
|
54
|
-
*
|
|
55
|
-
* 1. u:<email>:credential:<KEY> — per-user override
|
|
56
|
-
* 2. o:<orgId>:credential:<KEY> — per-org shared credential (if orgId given)
|
|
72
|
+
* Without an active org, step 3 is replaced by the solo workspace scope.
|
|
57
73
|
*/
|
|
58
74
|
export async function resolveCredential(key, ctx) {
|
|
59
75
|
if (!ctx?.userEmail)
|
|
60
76
|
return undefined;
|
|
77
|
+
const userSecret = await readScopedAppSecret(key, "user", ctx.userEmail);
|
|
78
|
+
if (userSecret)
|
|
79
|
+
return userSecret;
|
|
61
80
|
const userSetting = await resolveCredentialForScope(key, {
|
|
62
81
|
...ctx,
|
|
63
82
|
scope: "user",
|
|
@@ -65,9 +84,15 @@ export async function resolveCredential(key, ctx) {
|
|
|
65
84
|
if (userSetting)
|
|
66
85
|
return userSetting;
|
|
67
86
|
if (ctx.orgId) {
|
|
87
|
+
const orgSecret = await readScopedAppSecret(key, "org", ctx.orgId);
|
|
88
|
+
if (orgSecret)
|
|
89
|
+
return orgSecret;
|
|
90
|
+
const workspaceSecret = await readScopedAppSecret(key, "workspace", ctx.orgId);
|
|
91
|
+
if (workspaceSecret)
|
|
92
|
+
return workspaceSecret;
|
|
68
93
|
return resolveCredentialForScope(key, { ...ctx, scope: "org" });
|
|
69
94
|
}
|
|
70
|
-
return
|
|
95
|
+
return readScopedAppSecret(key, "workspace", `solo:${ctx.userEmail}`);
|
|
71
96
|
}
|
|
72
97
|
/**
|
|
73
98
|
* Check if a credential is available for the given context.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/credentials/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE7E,MAAM,cAAc,GAAG,aAAa,CAAC;AASrC,SAAS,wBAAwB,CAAC,KAAa,EAAE,GAAW;IAC1D,OAAO,KAAK,KAAK,CAAC,WAAW,EAAE,IAAI,cAAc,GAAG,GAAG,EAAE,CAAC;AAC5D,CAAC;AAED,SAAS,uBAAuB,CAAC,KAAa,EAAE,GAAW;IACzD,OAAO,KAAK,KAAK,IAAI,cAAc,GAAG,GAAG,EAAE,CAAC;AAC9C,CAAC;AAED,KAAK,UAAU,qBAAqB,CAClC,UAAkB;IAElB,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,UAAU,CAAC,CAAC;IAC7C,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACpE,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAC7B,sEAAsE;IACtE,uEAAuE;IACvE,sEAAsE;IACtE,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IACnD,IAAI,CAAC;QACH,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;QACvE,iEAAiE;QACjE,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,GAAW,EACX,GAA0D;IAE1D,IAAI,CAAC,GAAG,EAAE,SAAS;QAAE,OAAO,SAAS,CAAC;IACtC,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC,GAAG,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QACjC,OAAO,qBAAqB,CAAC,uBAAuB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,qBAAqB,CAAC,wBAAwB,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/credentials/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n encryptSecretValue,\n decryptSecretValue,\n isEncryptedSecretValue,\n} from \"../secrets/crypto.js\";\nimport { readAppSecret, type SecretRef } from \"../secrets/storage.js\";\nimport { getSetting, putSetting, deleteSetting } from \"../settings/store.js\";\n\nconst SETTING_PREFIX = \"credential:\";\n\nexport interface CredentialContext {\n userEmail: string;\n orgId?: string | null;\n}\n\nexport type CredentialStorageScope = \"user\" | \"org\";\n\nfunction userCredentialSettingKey(email: string, key: string): string {\n return `u:${email.toLowerCase()}:${SETTING_PREFIX}${key}`;\n}\n\nfunction orgCredentialSettingKey(orgId: string, key: string): string {\n return `o:${orgId}:${SETTING_PREFIX}${key}`;\n}\n\nasync function readCredentialSetting(\n settingKey: string,\n): Promise<string | undefined> {\n const setting = await getSetting(settingKey);\n if (!setting || typeof setting.value !== \"string\") return undefined;\n const stored = setting.value;\n // Values written by saveCredential are AES-256-GCM encrypted at rest.\n // Rows that predate encryption are plaintext — read them transparently\n // (the migrate-encrypt-credentials script re-encrypts them in place).\n if (!isEncryptedSecretValue(stored)) return stored;\n try {\n return decryptSecretValue(stored);\n } catch {\n // Key rotated, corrupt, or tampered row — treat as not set rather than\n // surfacing ciphertext or throwing into every credential lookup.\n return undefined;\n }\n}\n\nasync function readScopedAppSecret(\n key: string,\n scope: SecretRef[\"scope\"],\n scopeId: string,\n): Promise<string | undefined> {\n try {\n return (await readAppSecret({ key, scope, scopeId }))?.value;\n } catch {\n // Older databases may not have app_secrets yet. Keep the legacy\n // credential store available while the table bootstraps.\n return undefined;\n }\n}\n\n/**\n * Resolve a credential from one explicit legacy SQL credential scope.\n *\n * Prefer `resolveCredential()` for normal app-local credential lookup. This\n * helper exists for workspace connection refs, where a ref can explicitly say\n * \"use the org-scoped key\" and must not accidentally read a user override.\n */\nexport async function resolveCredentialForScope(\n key: string,\n ctx: CredentialContext & { scope: CredentialStorageScope },\n): Promise<string | undefined> {\n if (!ctx?.userEmail) return undefined;\n if (ctx.scope === \"org\") {\n if (!ctx.orgId) return undefined;\n return readCredentialSetting(orgCredentialSettingKey(ctx.orgId, key));\n }\n return readCredentialSetting(userCredentialSettingKey(ctx.userEmail, key));\n}\n\n/**\n * Resolve a credential across the encrypted app_secrets store and the legacy\n * settings-backed credential store. User overrides win, followed by the\n * active org/workspace shared value.\n *\n * SECURITY: NEVER reads from process.env. Env vars are global to the\n * deployment and would leak across users in a multi-tenant app.\n *\n * Read order:\n * 1. user-scoped app_secrets\n * 2. user-scoped legacy settings credential\n * 3. org-scoped app_secrets\n * 4. legacy workspace-scoped app_secrets for the org\n * 5. org-scoped legacy settings credential\n *\n * Without an active org, step 3 is replaced by the solo workspace scope.\n */\nexport async function resolveCredential(\n key: string,\n ctx: CredentialContext,\n): Promise<string | undefined> {\n if (!ctx?.userEmail) return undefined;\n\n const userSecret = await readScopedAppSecret(key, \"user\", ctx.userEmail);\n if (userSecret) return userSecret;\n\n const userSetting = await resolveCredentialForScope(key, {\n ...ctx,\n scope: \"user\",\n });\n if (userSetting) return userSetting;\n\n if (ctx.orgId) {\n const orgSecret = await readScopedAppSecret(key, \"org\", ctx.orgId);\n if (orgSecret) return orgSecret;\n\n const workspaceSecret = await readScopedAppSecret(\n key,\n \"workspace\",\n ctx.orgId,\n );\n if (workspaceSecret) return workspaceSecret;\n\n return resolveCredentialForScope(key, { ...ctx, scope: \"org\" });\n }\n\n return readScopedAppSecret(key, \"workspace\", `solo:${ctx.userEmail}`);\n}\n\n/**\n * Check if a credential is available for the given context.\n */\nexport async function hasCredential(\n key: string,\n ctx: CredentialContext,\n): Promise<boolean> {\n return (await resolveCredential(key, ctx)) !== undefined;\n}\n\n/**\n * Save a credential. By default writes to the per-user store; pass\n * `scope: \"org\"` to write to the active org's shared credentials.\n */\nexport async function saveCredential(\n key: string,\n value: string,\n ctx: CredentialContext & { scope?: \"user\" | \"org\" },\n): Promise<void> {\n if (!ctx?.userEmail) {\n throw new Error(\"saveCredential requires CredentialContext with userEmail\");\n }\n // Encrypt at rest (AES-256-GCM) so a leaked DB backup / pg_dump / read\n // replica doesn't expose plaintext keys. resolveCredential decrypts\n // transparently on read.\n const encrypted = encryptSecretValue(value);\n if (ctx.scope === \"org\") {\n if (!ctx.orgId) {\n throw new Error(\"saveCredential scope='org' requires orgId\");\n }\n await putSetting(orgCredentialSettingKey(ctx.orgId, key), {\n value: encrypted,\n });\n return;\n }\n await putSetting(userCredentialSettingKey(ctx.userEmail, key), {\n value: encrypted,\n });\n}\n\n/**\n * Delete a credential from the per-user (default) or per-org store.\n */\nexport async function deleteCredential(\n key: string,\n ctx: CredentialContext & { scope?: \"user\" | \"org\" },\n): Promise<void> {\n if (!ctx?.userEmail) {\n throw new Error(\n \"deleteCredential requires CredentialContext with userEmail\",\n );\n }\n if (ctx.scope === \"org\") {\n if (!ctx.orgId) {\n throw new Error(\"deleteCredential scope='org' requires orgId\");\n }\n await deleteSetting(orgCredentialSettingKey(ctx.orgId, key));\n return;\n }\n await deleteSetting(userCredentialSettingKey(ctx.userEmail, key));\n}\n"]}
|
|
@@ -10,15 +10,27 @@
|
|
|
10
10
|
* the raw secret never enters the model's context.
|
|
11
11
|
*/
|
|
12
12
|
import type { ActionEntry } from "../agent/production-agent.js";
|
|
13
|
+
import type { ResolvedKeyReference } from "../secrets/substitution.js";
|
|
13
14
|
export interface FetchToolOptions {
|
|
14
15
|
/** Resolve ${keys.NAME} references. Injected by the plugin at setup time. */
|
|
15
16
|
resolveKeys?: (text: string) => Promise<{
|
|
16
17
|
resolved: string;
|
|
17
18
|
usedKeys: string[];
|
|
18
19
|
secretValues?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Optional: which scope (user/org/workspace) each used key actually
|
|
22
|
+
* resolved at. Populated by resolvers like
|
|
23
|
+
* `resolveKeyReferencesWithRequestScopes` so `validateUrl` can look up
|
|
24
|
+
* the allowlist at the scope the value came from rather than assuming
|
|
25
|
+
* user scope. Backwards compatible — callers that omit this (or a
|
|
26
|
+
* resolver that doesn't report it) are unaffected.
|
|
27
|
+
*/
|
|
28
|
+
resolvedKeys?: ResolvedKeyReference[];
|
|
19
29
|
}>;
|
|
20
30
|
/** Validate URL against per-key allowlists. */
|
|
21
|
-
validateUrl?: (url: string, usedKeys: string[]
|
|
31
|
+
validateUrl?: (url: string, usedKeys: string[],
|
|
32
|
+
/** Accumulated `resolvedKeys` from every `resolveKeys` call this request made, when the resolver reported them. */
|
|
33
|
+
resolvedKeys?: ResolvedKeyReference[]) => Promise<boolean>;
|
|
22
34
|
}
|
|
23
35
|
/**
|
|
24
36
|
* Create the fetch tool entry for the agent tool registry.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-tool.d.ts","sourceRoot":"","sources":["../../src/extensions/fetch-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch-tool.d.ts","sourceRoot":"","sources":["../../src/extensions/fetch-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AA6DvE,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QACtC,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QACxB;;;;;;;WAOG;QACH,YAAY,CAAC,EAAE,oBAAoB,EAAE,CAAC;KACvC,CAAC,CAAC;IACH,+CAA+C;IAC/C,WAAW,CAAC,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAAE;IAClB,mHAAmH;IACnH,YAAY,CAAC,EAAE,oBAAoB,EAAE,KAClC,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,GAAE,gBAAqB,GAC1B,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CA8V7B"}
|
|
@@ -153,21 +153,25 @@ export function createFetchToolEntry(opts = {}) {
|
|
|
153
153
|
let resolvedBody = rawBody;
|
|
154
154
|
const allUsedKeys = [];
|
|
155
155
|
const allSecretValues = [];
|
|
156
|
+
const allResolvedKeys = [];
|
|
156
157
|
if (opts.resolveKeys) {
|
|
157
158
|
try {
|
|
158
159
|
const urlResult = await opts.resolveKeys(rawUrl);
|
|
159
160
|
resolvedUrl = urlResult.resolved;
|
|
160
161
|
allUsedKeys.push(...urlResult.usedKeys);
|
|
161
162
|
allSecretValues.push(...(urlResult.secretValues ?? []));
|
|
163
|
+
allResolvedKeys.push(...(urlResult.resolvedKeys ?? []));
|
|
162
164
|
const headerResult = await opts.resolveKeys(rawHeaders);
|
|
163
165
|
resolvedHeaders = headerResult.resolved;
|
|
164
166
|
allUsedKeys.push(...headerResult.usedKeys);
|
|
165
167
|
allSecretValues.push(...(headerResult.secretValues ?? []));
|
|
168
|
+
allResolvedKeys.push(...(headerResult.resolvedKeys ?? []));
|
|
166
169
|
if (rawBody) {
|
|
167
170
|
const bodyResult = await opts.resolveKeys(rawBody);
|
|
168
171
|
resolvedBody = bodyResult.resolved;
|
|
169
172
|
allUsedKeys.push(...bodyResult.usedKeys);
|
|
170
173
|
allSecretValues.push(...(bodyResult.secretValues ?? []));
|
|
174
|
+
allResolvedKeys.push(...(bodyResult.resolvedKeys ?? []));
|
|
171
175
|
}
|
|
172
176
|
}
|
|
173
177
|
catch (err) {
|
|
@@ -175,6 +179,9 @@ export function createFetchToolEntry(opts = {}) {
|
|
|
175
179
|
}
|
|
176
180
|
}
|
|
177
181
|
const secretValues = collectSecretValues(allSecretValues);
|
|
182
|
+
const resolvedKeys = allResolvedKeys.length
|
|
183
|
+
? allResolvedKeys
|
|
184
|
+
: undefined;
|
|
178
185
|
// Block SSRF targets regardless of key usage
|
|
179
186
|
if (await isBlockedExtensionUrlWithDns(resolvedUrl)) {
|
|
180
187
|
return `Requests to private/internal addresses are not allowed: "${rawUrl}".`;
|
|
@@ -182,7 +189,7 @@ export function createFetchToolEntry(opts = {}) {
|
|
|
182
189
|
// Validate URL against per-key allowlists
|
|
183
190
|
if (opts.validateUrl && allUsedKeys.length > 0) {
|
|
184
191
|
try {
|
|
185
|
-
const allowed = await opts.validateUrl(resolvedUrl, allUsedKeys);
|
|
192
|
+
const allowed = await opts.validateUrl(resolvedUrl, allUsedKeys, resolvedKeys);
|
|
186
193
|
if (!allowed) {
|
|
187
194
|
return `URL "${rawUrl}" is not in the allowlist for the referenced keys. Check your key settings.`;
|
|
188
195
|
}
|
|
@@ -236,7 +243,7 @@ export function createFetchToolEntry(opts = {}) {
|
|
|
236
243
|
return "Redirect to private/internal address blocked.";
|
|
237
244
|
}
|
|
238
245
|
if (redirectUrl && opts.validateUrl && allUsedKeys.length > 0) {
|
|
239
|
-
const allowed = await opts.validateUrl(redirectUrl, allUsedKeys);
|
|
246
|
+
const allowed = await opts.validateUrl(redirectUrl, allUsedKeys, resolvedKeys);
|
|
240
247
|
if (!allowed) {
|
|
241
248
|
return "Redirect URL is not in the allowlist for the referenced keys.";
|
|
242
249
|
}
|