@k2b/cloud 0.30.0 → 0.31.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/package.json +3 -2
- package/src/_internal/app-presentation.ts +22 -1
- package/src/_internal/capabilities.ts +2 -2
- package/src/_internal/define-app.ts +14 -4
- package/src/_internal/registry.ts +50 -6
- package/src/_internal/widget-declarations.ts +53 -0
- package/src/ai/approval-routes.ts +40 -4
- package/src/ai/approvals.ts +68 -10
- package/src/ai/chat/blocks.tsx +53 -32
- package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
- package/src/ai/chat/message-actions.tsx +3 -1
- package/src/ai/chat/messages.ts +18 -0
- package/src/ai/chat/tool-groups.ts +66 -12
- package/src/ai/chat/tool-names.ts +4 -2
- package/src/ai/chat/turn-layout.ts +8 -0
- package/src/ai/chat/turn-view.tsx +94 -25
- package/src/ai/chat/web-tools.tsx +95 -1
- package/src/ai/client/controller.ts +6 -3
- package/src/ai/code-runtime-tools.ts +50 -17
- package/src/ai/code-source-contracts.ts +25 -21
- package/src/ai/code-source-tools.ts +3 -1
- package/src/ai/executor.ts +170 -14
- package/src/ai/fetch-file-tool.ts +41 -4
- package/src/ai/firecrawl-tools.ts +87 -4
- package/src/ai/index.ts +8 -0
- package/src/ai/migrate.ts +113 -2
- package/src/ai/protocol.ts +22 -1
- package/src/ai/routes.ts +11 -0
- package/src/ai/runtime.ts +46 -7
- package/src/ai/store.ts +66 -1
- package/src/ai/tool-audit.ts +11 -1
- package/src/ai/tools.ts +91 -3
- package/src/ai/types.ts +39 -0
- package/src/ai/web-provenance.ts +103 -0
- package/src/api/capabilities.ts +3 -2
- package/src/api/widgets.ts +36 -5
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/command-shortcuts.ts +21 -6
- package/src/browser/spotlight-position.ts +4 -1
- package/src/browser/widgets.ts +7 -5
- package/src/capabilities/executions.ts +3 -0
- package/src/cli/commands.ts +20 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/plugin.ts +30 -0
- package/src/contracts/app.ts +34 -2
- package/src/contracts/registry.ts +9 -1
- package/src/contracts/widgets.ts +47 -2
- package/src/desktop/solid.tsx +6 -7
- package/src/server/index.ts +1 -0
- package/src/server/widget-request.ts +27 -0
- package/src/services/approvals/index.ts +225 -0
- package/src/services/approvals/migrate.ts +33 -0
- package/src/shared/app-presentation.ts +11 -0
- package/src/shared/email-html.ts +43 -8
- package/src/ssr/AdminLayout.tsx +4 -7
- package/src/ssr/AppLaunchpad.island.tsx +1 -1
- package/src/ssr/Layout.tsx +38 -14
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +17 -20
- package/src/ssr/LayoutHelp.tsx +1 -1
- package/src/ssr/LayoutRail.tsx +1 -2
- package/src/ssr/PageTrail.tsx +23 -0
- package/src/ssr/PageTrailRow.island.tsx +20 -0
- package/src/ssr/PageTrailRuntime.island.tsx +26 -0
- package/src/ssr/ProfilePreferences.island.tsx +7 -3
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/layout-runtime.ts +60 -0
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +313 -0
- package/src/ssr/page-trail.browser-harness.tsx +216 -0
- package/src/ssr/page-trail.ts +59 -0
- package/src/ssr/platform-messages.ts +4 -0
- package/src/styles/effects.css +21 -4
- package/src/styles/global.css +35 -2
- package/src/styles/utilities-layout.css +24 -1
- package/src/styles/utilities-navigation.css +6 -18
package/src/ai/executor.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { compact, nessi } from "@k2b/nessi";
|
|
|
3
3
|
import { listCapabilities } from "../_internal/registry";
|
|
4
4
|
import type { CapabilityActionReview } from "../contracts/capabilities";
|
|
5
5
|
import type { AccessSubject, RequestActor } from "../server";
|
|
6
|
+
import { approvalAssurance, approvalScopeFromToolName, approvals } from "../services/approvals";
|
|
6
7
|
import { createHelpReader } from "../services/help";
|
|
7
8
|
import { logger } from "../services/logging";
|
|
8
9
|
import { getMandate } from "../services/mandates";
|
|
@@ -10,6 +11,7 @@ import type { MandatePolicyV1 } from "../services/mandates/policy";
|
|
|
10
11
|
import { coreSettings } from "../services/settings/api";
|
|
11
12
|
import { normalizeLocale } from "../shared/locale";
|
|
12
13
|
import {
|
|
14
|
+
AI_WEBSITE_APPROVAL_TOOL,
|
|
13
15
|
type AiToolApprovalContext,
|
|
14
16
|
aiToolAllowsAlways,
|
|
15
17
|
aiToolApprovalScope,
|
|
@@ -55,13 +57,13 @@ import { isAiVisionModelConfigured, type resolveAiModel } from "./settings";
|
|
|
55
57
|
import { selectAiSkillCatalog } from "./skill-catalog";
|
|
56
58
|
import { createCloudAiLoadSkillTool, createCloudAiSearchSkillsTool, loadSelectedAiSkills } from "./skill-tool";
|
|
57
59
|
import { aiSkills } from "./skills";
|
|
58
|
-
import { aiConversations } from "./store";
|
|
60
|
+
import { aiConversations, recordAiWebsiteReceipts } from "./store";
|
|
59
61
|
import { AI_LIVE_SNAPSHOT_INTERVAL_MS, publishAiWireEvent } from "./stream";
|
|
60
62
|
import { composeAiSystemPrompt } from "./system-prompt";
|
|
61
63
|
import { aiToolAudit } from "./tool-audit";
|
|
62
64
|
import { acceptCanonicalToolNames } from "./tool-call-names";
|
|
63
65
|
import { resolveAiToolResultMaxChars } from "./tool-result-budget";
|
|
64
|
-
import { aiToolPromptHints, type PreparedAiTools, prepareAiTools } from "./tools";
|
|
66
|
+
import { type AiApprovalUse, aiApprovalAskedLater, aiToolPromptHints, type PreparedAiTools, prepareAiTools } from "./tools";
|
|
65
67
|
import { AiTranscriptionError } from "./transcription";
|
|
66
68
|
import {
|
|
67
69
|
AiTurnFailure,
|
|
@@ -75,6 +77,7 @@ import { type AiTurnGuidance, loadAiTurnGuidance } from "./turn-guidance";
|
|
|
75
77
|
import { type AiTurnPolicyToolCall, applyAiTurnPolicy } from "./turn-policy";
|
|
76
78
|
import { createTurnTimingRecorder, withDurableTurnTiming } from "./turn-timing";
|
|
77
79
|
import type {
|
|
80
|
+
AiApprovalTarget,
|
|
78
81
|
AiChatTurnRunConfig,
|
|
79
82
|
AiFrontendToolMode,
|
|
80
83
|
AiPendingTurnActionRecord,
|
|
@@ -85,6 +88,7 @@ import type {
|
|
|
85
88
|
AiTurnFinalizedEvent,
|
|
86
89
|
AiTurnRunConfig,
|
|
87
90
|
AiTurnSteer,
|
|
91
|
+
AiWebsiteReceipt,
|
|
88
92
|
} from "./types";
|
|
89
93
|
import { isAiImageMediaType } from "./types";
|
|
90
94
|
import { validateAiTurnRequest } from "./validate";
|
|
@@ -296,7 +300,15 @@ const rebuildBlocksFromMessages = (
|
|
|
296
300
|
callId: action.callId,
|
|
297
301
|
status: action.kind === "client_tool" ? "awaiting_client" : "awaiting_approval",
|
|
298
302
|
approval:
|
|
299
|
-
action.kind === "client_tool"
|
|
303
|
+
action.kind === "client_tool"
|
|
304
|
+
? undefined
|
|
305
|
+
: {
|
|
306
|
+
message: action.message,
|
|
307
|
+
review: action.review,
|
|
308
|
+
allowAlways: action.allowAlways,
|
|
309
|
+
allowChat: action.allowChat ?? action.allowAlways,
|
|
310
|
+
...(action.rememberToolName === AI_WEBSITE_APPROVAL_TOOL ? { website: action.approvalScope } : {}),
|
|
311
|
+
},
|
|
300
312
|
frontendMode: action.frontendMode,
|
|
301
313
|
};
|
|
302
314
|
}
|
|
@@ -381,9 +393,25 @@ const approvalReviewForCallId = (
|
|
|
381
393
|
*/
|
|
382
394
|
const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowRememberedApprovals = true) => {
|
|
383
395
|
const toolBlocks = new Map<string, Extract<AiTurnBlock, { kind: "tool" }>>();
|
|
396
|
+
/** Website receipts by the call that shows them and their index; every later state of that call keeps them. */
|
|
397
|
+
const receiptsByCall = new Map<string, Map<number, { method: string; url: string }>>();
|
|
384
398
|
for (const block of seedBlocks) {
|
|
385
|
-
if (block.kind
|
|
399
|
+
if (block.kind !== "tool") continue;
|
|
400
|
+
toolBlocks.set(block.callId, block);
|
|
401
|
+
// A pending nested approval shows on its parent call's block; receipts belong to that block.
|
|
402
|
+
const shownCallId = block.id === toolBlockId(block.callId) ? block.callId : (customApprovalParentCallId(block.callId) ?? block.callId);
|
|
403
|
+
if (block.receipts?.length) receiptsByCall.set(shownCallId, new Map(block.receipts.map((receipt, index) => [index, receipt])));
|
|
386
404
|
}
|
|
405
|
+
const receiptsOf = (callId: string) => {
|
|
406
|
+
const receipts = receiptsByCall.get(callId);
|
|
407
|
+
return receipts ? [...receipts].sort(([a], [b]) => a - b).map(([, receipt]) => receipt) : undefined;
|
|
408
|
+
};
|
|
409
|
+
const addReceipts = (callId: string, receipts: AiWebsiteReceipt[]) => {
|
|
410
|
+
const known = receiptsByCall.get(callId) ?? new Map<number, { method: string; url: string }>();
|
|
411
|
+
for (const { index, method, url } of receipts) known.set(index, { method, url });
|
|
412
|
+
receiptsByCall.set(callId, known);
|
|
413
|
+
return receiptsOf(callId) ?? [];
|
|
414
|
+
};
|
|
387
415
|
/** Real frontend mode per tool name — set once the turn's tools are prepared.
|
|
388
416
|
* Getting this wrong is not cosmetic: the client auto-resolves plain "client"
|
|
389
417
|
* blocks, so a mislabeled client_interaction tool (survey) would be answered
|
|
@@ -408,6 +436,10 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
408
436
|
const setApprovalPolicies = (items: PreparedAiTools["approvalPolicies"]) => {
|
|
409
437
|
approvalPolicies = items;
|
|
410
438
|
};
|
|
439
|
+
let approvalTargets: ReadonlyMap<string, AiApprovalTarget> = new Map();
|
|
440
|
+
const setApprovalTargets = (items: ReadonlyMap<string, AiApprovalTarget>) => {
|
|
441
|
+
approvalTargets = items;
|
|
442
|
+
};
|
|
411
443
|
let rejectedCallIds = new Set<string>();
|
|
412
444
|
const setRejectedCallIds = (items: Set<string>) => {
|
|
413
445
|
rejectedCallIds = items;
|
|
@@ -435,10 +467,11 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
435
467
|
status: patch.status ?? existing?.status ?? "running",
|
|
436
468
|
result: "result" in patch ? patch.result : existing?.result,
|
|
437
469
|
isError: "isError" in patch ? patch.isError : existing?.isError,
|
|
438
|
-
approval: approval && !allowRememberedApprovals ? { ...approval, allowAlways: false } : approval,
|
|
470
|
+
approval: approval && !allowRememberedApprovals ? { ...approval, allowAlways: false, allowChat: false } : approval,
|
|
439
471
|
frontendMode: patch.frontendMode ?? existing?.frontendMode,
|
|
440
472
|
presentation: patch.presentation ?? existing?.presentation ?? presentations.get(rawName) ?? presentations.get(name),
|
|
441
473
|
...(approvedCallIds.has(callId) || approvedCallIds.has(displayCallId) || existing?.approved ? { approved: true } : {}),
|
|
474
|
+
...(receiptsOf(displayCallId) ? { receipts: receiptsOf(displayCallId) } : {}),
|
|
442
475
|
};
|
|
443
476
|
toolBlocks.set(callId, block);
|
|
444
477
|
return { type: "block_set", block };
|
|
@@ -484,6 +517,10 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
484
517
|
case "tool_action_request":
|
|
485
518
|
const displayCallId = event.kind === "custom_approval" ? (customApprovalParentCallId(event.callId) ?? event.callId) : event.callId;
|
|
486
519
|
const review = approvalReviewForCallId(approvalReviews, event.callId);
|
|
520
|
+
const target = event.kind === "custom_approval" ? approvalTargets.get(event.callId) : undefined;
|
|
521
|
+
const allowAlways = target
|
|
522
|
+
? target.always
|
|
523
|
+
: review?.approvalScope !== undefined || aiToolAllowsAlways(approvalPolicies.get(event.name));
|
|
487
524
|
return [
|
|
488
525
|
setTool(
|
|
489
526
|
event.callId,
|
|
@@ -497,7 +534,9 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
497
534
|
: {
|
|
498
535
|
message: event.message,
|
|
499
536
|
review,
|
|
500
|
-
allowAlways
|
|
537
|
+
allowAlways,
|
|
538
|
+
allowChat: target !== undefined || allowAlways,
|
|
539
|
+
...(target?.toolName === AI_WEBSITE_APPROVAL_TOOL ? { website: target.approvalScope } : {}),
|
|
501
540
|
},
|
|
502
541
|
frontendMode: event.kind === "client_tool" ? (frontendModes.get(event.name) ?? "client") : undefined,
|
|
503
542
|
},
|
|
@@ -542,8 +581,10 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
542
581
|
setCanonicalNames,
|
|
543
582
|
setApprovalPolicies,
|
|
544
583
|
setApprovalReviews,
|
|
584
|
+
setApprovalTargets,
|
|
545
585
|
setRejectedCallIds,
|
|
546
586
|
setApprovedCallIds,
|
|
587
|
+
addReceipts,
|
|
547
588
|
};
|
|
548
589
|
};
|
|
549
590
|
|
|
@@ -900,9 +941,13 @@ export class AiTurnExecutor {
|
|
|
900
941
|
});
|
|
901
942
|
}
|
|
902
943
|
|
|
944
|
+
const approvalTargets = new Map<string, AiApprovalTarget>();
|
|
945
|
+
const linkedApprovals = new Map<string, string>();
|
|
946
|
+
const freshApprovalRequired = new Set<string>();
|
|
903
947
|
const dynamicToolRuntimeContext = {
|
|
904
948
|
turnId,
|
|
905
949
|
reportToolProgress: (callId: string, message: string) => pipeline.reportToolProgress(callId, message),
|
|
950
|
+
reportWebsiteReceipts: (callId: string, receipts: AiWebsiteReceipt[]) => pipeline.reportWebsiteReceipts(callId, receipts),
|
|
906
951
|
attachedFilePaths: new Set(config.files?.attached.map((file) => file.path) ?? []),
|
|
907
952
|
allowedDataBoundaries: material.modelPolicy?.allowedDataBoundaries,
|
|
908
953
|
projectFiles,
|
|
@@ -910,6 +955,35 @@ export class AiTurnExecutor {
|
|
|
910
955
|
selectedModel: resolved,
|
|
911
956
|
locale: promptLocale,
|
|
912
957
|
timeZone,
|
|
958
|
+
linkApproval: (approvalCallId: string, approvalId: string) => {
|
|
959
|
+
linkedApprovals.set(approvalCallId, approvalId);
|
|
960
|
+
},
|
|
961
|
+
consumeApproval: async (approvalCallId: string, input: unknown): Promise<AiApprovalUse> => {
|
|
962
|
+
const pending = await aiConversations.getPendingTurnAction({ conversationId, turnId, callId: approvalCallId });
|
|
963
|
+
if (!pending?.approvalId) return "approved";
|
|
964
|
+
const result = await approvals.consume(pending.approvalId, {
|
|
965
|
+
input,
|
|
966
|
+
review: approvalReviewForCallId(capabilityActionReviews, approvalCallId) ?? null,
|
|
967
|
+
requester: { kind: "assistant_turn", ref: `${turnId}/${approvalCallId}` },
|
|
968
|
+
});
|
|
969
|
+
if (result.ok) return "approved";
|
|
970
|
+
if (result.code === "ALREADY_CONSUMED") {
|
|
971
|
+
// A later question of the same call proves this run resumes after it, not after a crash during the effect.
|
|
972
|
+
const [open, resolved] = await Promise.all([
|
|
973
|
+
aiConversations.listPendingActionRecords({ conversationId, turnId }),
|
|
974
|
+
aiConversations.listResolvedPendingActions({ conversationId, turnId }),
|
|
975
|
+
]);
|
|
976
|
+
return aiApprovalAskedLater(
|
|
977
|
+
approvalCallId,
|
|
978
|
+
[...open, ...resolved].map((action) => action.callId),
|
|
979
|
+
)
|
|
980
|
+
? "approved"
|
|
981
|
+
: "used";
|
|
982
|
+
}
|
|
983
|
+
freshApprovalRequired.add(customApprovalParentCallId(approvalCallId) ?? approvalCallId);
|
|
984
|
+
return "changed";
|
|
985
|
+
},
|
|
986
|
+
describeApproval: (approvalCallId: string, target: AiApprovalTarget) => approvalTargets.set(approvalCallId, target),
|
|
913
987
|
};
|
|
914
988
|
const prepared = prepareAiTools({
|
|
915
989
|
tools: activeTools,
|
|
@@ -920,6 +994,7 @@ export class AiTurnExecutor {
|
|
|
920
994
|
let backgroundError: string | null = null;
|
|
921
995
|
const rememberableCapabilityApprovals = new Map<string, string>();
|
|
922
996
|
const capabilityActionReviews = new Map<string, CapabilityActionReview>();
|
|
997
|
+
pipeline.setApprovalTargets(approvalTargets);
|
|
923
998
|
pipeline.setFrontendModes(prepared.frontendModes);
|
|
924
999
|
pipeline.setCanonicalNames(prepared.canonicalNames);
|
|
925
1000
|
pipeline.setApprovalPolicies(prepared.approvalPolicies);
|
|
@@ -1332,6 +1407,9 @@ export class AiTurnExecutor {
|
|
|
1332
1407
|
allowRememberedApprovals: aiTurnAllowsRememberedApprovals(config),
|
|
1333
1408
|
rememberableCapabilityApprovals,
|
|
1334
1409
|
capabilityActionReviews,
|
|
1410
|
+
approvalTargets,
|
|
1411
|
+
linkedApprovals,
|
|
1412
|
+
freshApprovalRequired,
|
|
1335
1413
|
appliedSteers,
|
|
1336
1414
|
noteToolRound: turnPolicy.noteToolRound,
|
|
1337
1415
|
noteToolCall: turnPolicy.noteToolCall,
|
|
@@ -1401,6 +1479,9 @@ export class AiTurnExecutor {
|
|
|
1401
1479
|
allowRememberedApprovals: boolean;
|
|
1402
1480
|
rememberableCapabilityApprovals: ReadonlyMap<string, string>;
|
|
1403
1481
|
capabilityActionReviews: ReadonlyMap<string, CapabilityActionReview>;
|
|
1482
|
+
approvalTargets: ReadonlyMap<string, AiApprovalTarget>;
|
|
1483
|
+
linkedApprovals: ReadonlyMap<string, string>;
|
|
1484
|
+
freshApprovalRequired?: ReadonlySet<string>;
|
|
1404
1485
|
appliedSteers: AiTurnSteer[];
|
|
1405
1486
|
noteToolRound: () => void;
|
|
1406
1487
|
noteToolCall: (call: AiTurnPolicyToolCall) => void;
|
|
@@ -1423,6 +1504,7 @@ export class AiTurnExecutor {
|
|
|
1423
1504
|
allowRememberedApprovals,
|
|
1424
1505
|
rememberableCapabilityApprovals,
|
|
1425
1506
|
capabilityActionReviews,
|
|
1507
|
+
approvalTargets,
|
|
1426
1508
|
appliedSteers,
|
|
1427
1509
|
noteToolRound,
|
|
1428
1510
|
noteToolCall,
|
|
@@ -1446,6 +1528,9 @@ export class AiTurnExecutor {
|
|
|
1446
1528
|
allowRememberedApprovals,
|
|
1447
1529
|
rememberableCapabilityApprovals,
|
|
1448
1530
|
capabilityActionReviews,
|
|
1531
|
+
approvalTargets,
|
|
1532
|
+
linkedApprovals: input.linkedApprovals,
|
|
1533
|
+
freshApprovalRequired: input.freshApprovalRequired,
|
|
1449
1534
|
onBackgroundBlocked: input.onBackgroundBlocked,
|
|
1450
1535
|
});
|
|
1451
1536
|
if (suspended) {
|
|
@@ -1549,6 +1634,9 @@ export class AiTurnExecutor {
|
|
|
1549
1634
|
allowRememberedApprovals: boolean;
|
|
1550
1635
|
rememberableCapabilityApprovals: ReadonlyMap<string, string>;
|
|
1551
1636
|
capabilityActionReviews: ReadonlyMap<string, CapabilityActionReview>;
|
|
1637
|
+
approvalTargets: ReadonlyMap<string, AiApprovalTarget>;
|
|
1638
|
+
linkedApprovals: ReadonlyMap<string, string>;
|
|
1639
|
+
freshApprovalRequired?: ReadonlySet<string>;
|
|
1552
1640
|
onBackgroundBlocked?: (message: string) => void;
|
|
1553
1641
|
}): Promise<boolean> {
|
|
1554
1642
|
const {
|
|
@@ -1562,6 +1650,7 @@ export class AiTurnExecutor {
|
|
|
1562
1650
|
allowRememberedApprovals,
|
|
1563
1651
|
rememberableCapabilityApprovals,
|
|
1564
1652
|
capabilityActionReviews,
|
|
1653
|
+
approvalTargets,
|
|
1565
1654
|
} = input;
|
|
1566
1655
|
const approvalPolicy = prepared.approvalPolicies.get(event.name);
|
|
1567
1656
|
const toolName = prepared.canonicalNames.get(event.name) ?? event.name;
|
|
@@ -1571,8 +1660,13 @@ export class AiTurnExecutor {
|
|
|
1571
1660
|
event.kind === "custom_approval"
|
|
1572
1661
|
? rememberableCapabilityApprovals.get(customApprovalParentCallId(event.callId) ?? event.callId)
|
|
1573
1662
|
: undefined;
|
|
1574
|
-
|
|
1575
|
-
|
|
1663
|
+
// A tool that names what it asks for (a code run's HTTP request or Action, a web page to read) already looked up
|
|
1664
|
+
// its own remembered approvals under its own rules; the approval is remembered for that target, not the tool.
|
|
1665
|
+
const target = event.kind === "custom_approval" ? approvalTargets.get(event.callId) : undefined;
|
|
1666
|
+
const approvalScope = target?.approvalScope ?? capabilityApprovalScope ?? aiToolApprovalScope(toolName, approvalPolicy);
|
|
1667
|
+
const allowAlways =
|
|
1668
|
+
allowRememberedApprovals && (target ? target.always : capabilityApprovalScope !== undefined || aiToolAllowsAlways(approvalPolicy));
|
|
1669
|
+
const allowChat = allowRememberedApprovals && (target !== undefined || allowAlways);
|
|
1576
1670
|
|
|
1577
1671
|
// Display-only client_view tools (cards, charts) need neither a browser nor input: Cloud answers them itself, in
|
|
1578
1672
|
// background runs too, whose transcript shows them later. The block goes from running straight to completed:
|
|
@@ -1609,8 +1703,15 @@ export class AiTurnExecutor {
|
|
|
1609
1703
|
}
|
|
1610
1704
|
|
|
1611
1705
|
// Remembered approvals resolve inline too.
|
|
1612
|
-
if (
|
|
1613
|
-
|
|
1706
|
+
if (
|
|
1707
|
+
event.kind !== "client_tool" &&
|
|
1708
|
+
!target &&
|
|
1709
|
+
allowAlways &&
|
|
1710
|
+
approvalContext &&
|
|
1711
|
+
// A stale decided snapshot needs another human decision, even if the first response also remembered it.
|
|
1712
|
+
!input.freshApprovalRequired?.has(customApprovalParentCallId(event.callId) ?? event.callId)
|
|
1713
|
+
) {
|
|
1714
|
+
const remembered = await hasRememberedAiToolApproval(approvalContext, { toolName, approvalScope, conversationId }).catch(() => false);
|
|
1614
1715
|
if (remembered) {
|
|
1615
1716
|
await aiToolAudit
|
|
1616
1717
|
.noteApprovalResolved({ turnId, callId: event.callId, approvalState: "approved_by_preference" })
|
|
@@ -1620,7 +1721,40 @@ export class AiTurnExecutor {
|
|
|
1620
1721
|
}
|
|
1621
1722
|
}
|
|
1622
1723
|
|
|
1724
|
+
const review = event.kind === "custom_approval" ? approvalReviewForCallId(capabilityActionReviews, event.callId) : undefined;
|
|
1725
|
+
let approvalId: string | undefined;
|
|
1726
|
+
let presented = event;
|
|
1727
|
+
if (event.kind !== "client_tool") {
|
|
1728
|
+
const conversation = await aiConversations.getConversation({ conversationId });
|
|
1729
|
+
const owner = conversation?.createdByUserId;
|
|
1730
|
+
if (!owner) throw new Error("Approval requires a conversation owner");
|
|
1731
|
+
const linked = input.linkedApprovals.get(event.callId);
|
|
1732
|
+
if (linked) {
|
|
1733
|
+
const record = await approvals.get(linked);
|
|
1734
|
+
if (!record || record.status !== "pending" || record.requesterKind !== "studio_app" || record.approverUserId !== owner)
|
|
1735
|
+
throw new Error("Approval does not belong to an open Studio request of the conversation owner");
|
|
1736
|
+
approvalId = linked;
|
|
1737
|
+
// The card shows the question the effect's own record holds, never the text the relay sent.
|
|
1738
|
+
presented = { ...event, message: record.sentence };
|
|
1739
|
+
} else {
|
|
1740
|
+
const record = await approvals.request({
|
|
1741
|
+
...approvalScopeFromToolName(toolName),
|
|
1742
|
+
approvalScope,
|
|
1743
|
+
requester: { kind: "assistant_turn", ref: `${turnId}/${event.callId}`, appId: "assistant", title: conversation.title },
|
|
1744
|
+
subjectId: owner,
|
|
1745
|
+
approverUserId: owner,
|
|
1746
|
+
assurance: approvalAssurance(target !== undefined || capabilityApprovalScope !== undefined || aiToolAllowsAlways(approvalPolicy)),
|
|
1747
|
+
sentence: event.message ?? toolName,
|
|
1748
|
+
review,
|
|
1749
|
+
input: event.args,
|
|
1750
|
+
expiresInMs: AI_ACTION_BUDGET_MS,
|
|
1751
|
+
});
|
|
1752
|
+
if (!record.ok) throw new Error("This approval was already decided or has expired.");
|
|
1753
|
+
approvalId = record.id;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1623
1756
|
await aiConversations.savePendingTurnAction({
|
|
1757
|
+
approvalId,
|
|
1624
1758
|
turnId,
|
|
1625
1759
|
conversationId,
|
|
1626
1760
|
callId: event.callId,
|
|
@@ -1628,10 +1762,12 @@ export class AiTurnExecutor {
|
|
|
1628
1762
|
status: "pending",
|
|
1629
1763
|
name: toolName,
|
|
1630
1764
|
args: event.args,
|
|
1631
|
-
message:
|
|
1632
|
-
review
|
|
1765
|
+
message: presented.message,
|
|
1766
|
+
review,
|
|
1633
1767
|
approvalScope,
|
|
1634
1768
|
allowAlways,
|
|
1769
|
+
allowChat,
|
|
1770
|
+
...(target ? { rememberToolName: target.toolName } : {}),
|
|
1635
1771
|
frontendMode,
|
|
1636
1772
|
resolvedEvent: null,
|
|
1637
1773
|
});
|
|
@@ -1653,7 +1789,7 @@ export class AiTurnExecutor {
|
|
|
1653
1789
|
.catch(() => undefined);
|
|
1654
1790
|
}
|
|
1655
1791
|
|
|
1656
|
-
await pipeline.apply(
|
|
1792
|
+
await pipeline.apply(presented);
|
|
1657
1793
|
const suspended = await aiConversations.suspendTurn({
|
|
1658
1794
|
conversationId,
|
|
1659
1795
|
turnId,
|
|
@@ -1871,7 +2007,9 @@ class StreamPipeline {
|
|
|
1871
2007
|
this.blocks = this.allowRememberedApprovals
|
|
1872
2008
|
? blocks
|
|
1873
2009
|
: blocks.map((block) =>
|
|
1874
|
-
block.kind === "tool" && block.approval
|
|
2010
|
+
block.kind === "tool" && block.approval
|
|
2011
|
+
? { ...block, approval: { ...block.approval, allowAlways: false, allowChat: false } }
|
|
2012
|
+
: block,
|
|
1875
2013
|
);
|
|
1876
2014
|
}
|
|
1877
2015
|
|
|
@@ -1895,6 +2033,10 @@ class StreamPipeline {
|
|
|
1895
2033
|
this.mapper.setApprovalPolicies(policies);
|
|
1896
2034
|
}
|
|
1897
2035
|
|
|
2036
|
+
setApprovalTargets(targets: ReadonlyMap<string, AiApprovalTarget>): void {
|
|
2037
|
+
this.mapper.setApprovalTargets(targets);
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1898
2040
|
setRejectedCallIds(callIds: Set<string>): void {
|
|
1899
2041
|
this.mapper.setRejectedCallIds(callIds);
|
|
1900
2042
|
}
|
|
@@ -1950,6 +2092,20 @@ class StreamPipeline {
|
|
|
1950
2092
|
await this.maybeSnapshot();
|
|
1951
2093
|
}
|
|
1952
2094
|
|
|
2095
|
+
/**
|
|
2096
|
+
* Shows requests a website approval let through on the call's block, and records them on its message before they go
|
|
2097
|
+
* out, so the chat keeps each receipt and its revoke action whether the call succeeds, fails, or the turn stops.
|
|
2098
|
+
*/
|
|
2099
|
+
async reportWebsiteReceipts(callId: string, receipts: AiWebsiteReceipt[]): Promise<void> {
|
|
2100
|
+
if (!receipts.length) return;
|
|
2101
|
+
await recordAiWebsiteReceipts({ conversationId: this.conversationId, turnId: this.turnId, callId, receipts });
|
|
2102
|
+
const merged = this.mapper.addReceipts(callId, receipts);
|
|
2103
|
+
const block = this.blocks.find((block) => block.kind === "tool" && block.id === toolBlockId(callId));
|
|
2104
|
+
if (block?.kind !== "tool") return;
|
|
2105
|
+
await this.emitOp({ type: "block_set", block: { ...block, receipts: merged } });
|
|
2106
|
+
await this.maybeSnapshot();
|
|
2107
|
+
}
|
|
2108
|
+
|
|
1953
2109
|
async reportToolProgress(callId: string, progress: string): Promise<void> {
|
|
1954
2110
|
const block = this.blocks.find((block) => block.kind === "tool" && block.callId === callId);
|
|
1955
2111
|
if (!block || block.kind !== "tool" || block.progress === progress) return;
|
|
@@ -3,6 +3,7 @@ import { type RequestOptions as HttpsRequestOptions, request as httpsRequest } f
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { type PublicNetworkAddress, resolvePublicNetworkAddresses } from "../services/network-security";
|
|
5
5
|
import { AI_FILES_MAX_FILE_BYTES_DEFAULT, aiFileStore, guessAiMediaType, normalizeAiFilePath } from "./files-store";
|
|
6
|
+
import { authorizeWebRead } from "./firecrawl-tools";
|
|
6
7
|
import { defineAiTool } from "./tools";
|
|
7
8
|
|
|
8
9
|
const FETCH_FILE_MAX_REDIRECTS = 5;
|
|
@@ -225,9 +226,12 @@ const responseLocation = (headers: IncomingHttpHeaders): string | null => {
|
|
|
225
226
|
return typeof value === "string" && value.trim() ? value : null;
|
|
226
227
|
};
|
|
227
228
|
|
|
229
|
+
/** How a download that a website approval let through may proceed: only on that origin, each request shown first. */
|
|
230
|
+
type WebsiteApprovedDownload = { origin: string; onRequest: (url: URL, hop: number) => Promise<void> };
|
|
231
|
+
|
|
228
232
|
export const downloadPublicFile = async (
|
|
229
233
|
rawUrl: string,
|
|
230
|
-
options: FetchFileDependencies & { maxBytes?: number; signal?: AbortSignal } = {},
|
|
234
|
+
options: FetchFileDependencies & { maxBytes?: number; signal?: AbortSignal; approvedWebsite?: WebsiteApprovedDownload } = {},
|
|
231
235
|
): Promise<{ bytes: Uint8Array; headers: IncomingHttpHeaders; url: URL }> => {
|
|
232
236
|
const resolve = options.resolve ?? resolvePublicNetworkAddresses;
|
|
233
237
|
const perform =
|
|
@@ -238,6 +242,14 @@ export const downloadPublicFile = async (
|
|
|
238
242
|
let url = publicHttpsUrl(rawUrl);
|
|
239
243
|
|
|
240
244
|
for (let redirects = 0; redirects <= FETCH_FILE_MAX_REDIRECTS; redirects++) {
|
|
245
|
+
if (options.approvedWebsite) {
|
|
246
|
+
// A website approval covers its own origin only; an open redirect must not carry the address to another one.
|
|
247
|
+
if (url.origin !== options.approvedWebsite.origin)
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Redirect to another website — The file moved to ${url.toString()}, which this chat has not allowed. Download that address directly so the person can decide.`,
|
|
250
|
+
);
|
|
251
|
+
await options.approvedWebsite.onRequest(url, redirects);
|
|
252
|
+
}
|
|
241
253
|
const addresses = await resolveFileAddresses(resolve, url.hostname);
|
|
242
254
|
const response = await perform(url, addresses, { maxBytes, signal: options.signal });
|
|
243
255
|
if (response.statusCode >= 300 && response.statusCode < 400) {
|
|
@@ -280,11 +292,15 @@ const responseMediaType = (headers: IncomingHttpHeaders, path: string): string =
|
|
|
280
292
|
|
|
281
293
|
export const runCloudAiFetchFile = async (
|
|
282
294
|
input: z.infer<typeof CloudAiFetchFileInputSchema>,
|
|
283
|
-
context: { conversationId?: string; signal?: AbortSignal },
|
|
295
|
+
context: { conversationId?: string; signal?: AbortSignal; approvedWebsite?: WebsiteApprovedDownload },
|
|
284
296
|
dependencies: FetchFileDependencies = {},
|
|
285
297
|
): Promise<z.infer<typeof CloudAiFetchFileOutputSchema>> => {
|
|
286
298
|
if (!context.conversationId) throw new Error("The fetch_file tool needs a conversation context.");
|
|
287
|
-
const downloaded = await downloadPublicFile(input.url, {
|
|
299
|
+
const downloaded = await downloadPublicFile(input.url, {
|
|
300
|
+
...dependencies,
|
|
301
|
+
signal: context.signal,
|
|
302
|
+
approvedWebsite: context.approvedWebsite,
|
|
303
|
+
});
|
|
288
304
|
const path = normalizeAiFilePath(`/imports/${safeFilename(input.filename, downloaded.url)}`);
|
|
289
305
|
if (!path) throw new Error("Could not derive a valid conversation file path.");
|
|
290
306
|
const stat = await aiFileStore.createAssistantFile({
|
|
@@ -307,7 +323,28 @@ export const createCloudAiFetchFileTool = (dependencies: FetchFileDependencies =
|
|
|
307
323
|
timeoutMs: 90_000,
|
|
308
324
|
promptHint:
|
|
309
325
|
"fetch an exact public HTTPS source file into the chat when the user provides a file link; inspect it with read_file or view_image and present it when useful.",
|
|
310
|
-
}).server((input, context) =>
|
|
326
|
+
}).server(async (input, context) => {
|
|
327
|
+
const url = publicHttpsUrl(input.url);
|
|
328
|
+
const allowedForChat = await authorizeWebRead("file", url.toString(), context);
|
|
329
|
+
return runCloudAiFetchFile(
|
|
330
|
+
input,
|
|
331
|
+
{
|
|
332
|
+
...context,
|
|
333
|
+
...(allowedForChat
|
|
334
|
+
? {
|
|
335
|
+
approvedWebsite: {
|
|
336
|
+
origin: url.origin,
|
|
337
|
+
// The first request's receipt is already shown; each redirect on the same website adds one.
|
|
338
|
+
onRequest: async (next: URL, hop: number) => {
|
|
339
|
+
if (hop > 0) await context.reportWebsiteReceipts?.([{ index: hop, method: "GET", url: next.toString() }]);
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
: {}),
|
|
344
|
+
},
|
|
345
|
+
dependencies,
|
|
346
|
+
);
|
|
347
|
+
});
|
|
311
348
|
|
|
312
349
|
export type CloudAiFetchFileInput = z.infer<typeof CloudAiFetchFileInputSchema>;
|
|
313
350
|
export type CloudAiFetchFileOutput = z.infer<typeof CloudAiFetchFileOutputSchema>;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { isIP } from "node:net";
|
|
2
|
-
import { truncateMiddle } from "@k2b/nessi";
|
|
2
|
+
import { type ToolContext, truncateMiddle } from "@k2b/nessi";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import type { RequestActor } from "../server";
|
|
4
5
|
import { coreSettings } from "../services";
|
|
6
|
+
import { AI_WEBSITE_APPROVAL_TOOL, aiTurnAllowsWebsiteApprovals, aiWebsiteApprovalScope, findRememberedAiToolApproval } from "./approvals";
|
|
7
|
+
import { aiConversations } from "./store";
|
|
5
8
|
import { defineAiTool } from "./tools";
|
|
9
|
+
import type { AiApprovalTarget, AiWebsiteReceipt } from "./types";
|
|
10
|
+
import { hasWebAddressProvenance, recordWebAddresses, webAddressesIn } from "./web-provenance";
|
|
6
11
|
|
|
7
12
|
export const AI_FIRECRAWL_API_KEY_SETTING_KEY = "ai.firecrawl_api_key";
|
|
8
13
|
|
|
@@ -274,13 +279,70 @@ export const createCloudAiWebSearchTool = (config: FirecrawlToolConfig = {}) =>
|
|
|
274
279
|
...result,
|
|
275
280
|
snippet: truncateSnippet(result.snippet),
|
|
276
281
|
})),
|
|
277
|
-
}).server(async (input, ctx) =>
|
|
282
|
+
}).server(async (input, ctx) => {
|
|
283
|
+
const results = await runCloudAiWebSearch(input, { ...config, signal: ctx.signal });
|
|
284
|
+
if (ctx.conversationId)
|
|
285
|
+
await recordWebAddresses(
|
|
286
|
+
ctx.conversationId,
|
|
287
|
+
results.flatMap((result) => [...webAddressesIn(result.url)]),
|
|
288
|
+
);
|
|
289
|
+
return results;
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const webReadApprovalMessage = (kind: "page" | "file", url: string, locale: string | undefined) =>
|
|
293
|
+
locale?.startsWith("de")
|
|
294
|
+
? [
|
|
295
|
+
`${kind === "page" ? "Webseite lesen" : "Datei laden"}: ${url}`,
|
|
296
|
+
"Diese Adresse steht nicht in deiner Nachricht, in einem Suchergebnis oder als Link einer gelesenen Seite.",
|
|
297
|
+
"Die vollständige Adresse geht an die Website.",
|
|
298
|
+
].join("\n")
|
|
299
|
+
: [
|
|
300
|
+
`${kind === "page" ? "Read web page" : "Download file"}: ${url}`,
|
|
301
|
+
"This address is not in your message, in a search result, or linked from a page read earlier.",
|
|
302
|
+
"The full address goes to the website.",
|
|
303
|
+
].join("\n");
|
|
304
|
+
|
|
305
|
+
export type WebReadContext = Pick<ToolContext, "requestApproval"> & {
|
|
306
|
+
actor: RequestActor;
|
|
307
|
+
conversationId?: string;
|
|
308
|
+
turnId?: string;
|
|
309
|
+
locale?: string;
|
|
310
|
+
requestApprovalFor?: (message: string, target: AiApprovalTarget) => Promise<boolean>;
|
|
311
|
+
reportWebsiteReceipts?: (receipts: AiWebsiteReceipt[]) => Promise<void>;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Lets a public web read through when the chat supplied its address. A read sends its full address to the website,
|
|
316
|
+
* and an address the chat did not supply may carry private data the model put into it, so any other address asks
|
|
317
|
+
* like an HTTP request from code: with the full URL, and with the website approval of a signed-in chat. Returns
|
|
318
|
+
* whether such an approval let it through without asking; the chat then shows the receipt before the read goes out.
|
|
319
|
+
* Throws when the person declines.
|
|
320
|
+
*/
|
|
321
|
+
export const authorizeWebRead = async (kind: "page" | "file", url: string, ctx: WebReadContext): Promise<boolean> => {
|
|
322
|
+
if (ctx.conversationId && (await hasWebAddressProvenance(ctx.conversationId, url))) return false;
|
|
323
|
+
const target = { toolName: AI_WEBSITE_APPROVAL_TOOL, approvalScope: aiWebsiteApprovalScope(new URL(url).origin), always: false };
|
|
324
|
+
const runConfig = ctx.conversationId
|
|
325
|
+
? await aiConversations.getTurnRunConfig({ conversationId: ctx.conversationId, turnId: ctx.turnId ?? "" }).catch(() => null)
|
|
326
|
+
: null;
|
|
327
|
+
const actorUserId = ctx.actor.kind === "user" ? ctx.actor.user.id : (ctx.actor.delegatedUser?.id ?? "");
|
|
328
|
+
if (
|
|
329
|
+
aiTurnAllowsWebsiteApprovals(runConfig) &&
|
|
330
|
+
(await findRememberedAiToolApproval({ actorUserId }, { ...target, conversationId: ctx.conversationId, chatOnly: true })) !== null
|
|
331
|
+
) {
|
|
332
|
+
await ctx.reportWebsiteReceipts?.([{ index: 0, method: "GET", url }]);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
const message = webReadApprovalMessage(kind, url, ctx.locale);
|
|
336
|
+
const approved = ctx.requestApprovalFor ? await ctx.requestApprovalFor(message, target) : await ctx.requestApproval(message);
|
|
337
|
+
if (!approved) throw new Error("The person did not allow reading this address. Use an address from their message or a search result.");
|
|
338
|
+
return false;
|
|
339
|
+
};
|
|
278
340
|
|
|
279
341
|
export const createCloudAiWebExtractTool = (config: FirecrawlToolConfig = {}) =>
|
|
280
342
|
defineAiTool({
|
|
281
343
|
name: "web_extract",
|
|
282
344
|
description:
|
|
283
|
-
"Read one web page by URL and return clean Markdown. Inspect the relevant pages needed to support the answer; for research or comparison, prefer primary sources and read more than one useful source when warranted.",
|
|
345
|
+
"Read one web page by URL and return clean Markdown. Inspect the relevant pages needed to support the answer; for research or comparison, prefer primary sources and read more than one useful source when warranted. Use addresses exactly as the user wrote them, as web_search returned them, or as a page you already read links them; any other address asks the user first.",
|
|
284
346
|
inputSchema: CloudAiWebExtractInputSchema,
|
|
285
347
|
outputSchema: CloudAiWebExtractOutputSchema,
|
|
286
348
|
approval: "never",
|
|
@@ -297,7 +359,28 @@ export const createCloudAiWebExtractTool = (config: FirecrawlToolConfig = {}) =>
|
|
|
297
359
|
truncated: output.truncated || content !== output.content,
|
|
298
360
|
};
|
|
299
361
|
},
|
|
300
|
-
}).server(async (input, ctx) =>
|
|
362
|
+
}).server(async (input, ctx) => {
|
|
363
|
+
const url = assertPublicHttpUrl(input.url);
|
|
364
|
+
const allowedForChat = await authorizeWebRead("page", url, ctx);
|
|
365
|
+
const output = await runCloudAiWebExtract({ url }, { ...config, signal: ctx.signal });
|
|
366
|
+
const final = URL.canParse(output.url) ? new URL(output.url) : null;
|
|
367
|
+
if (allowedForChat && final && final.href !== url) {
|
|
368
|
+
// Firecrawl follows redirects on its side. The chat shows where the read ended, and a website approval covers
|
|
369
|
+
// only content from its own origin.
|
|
370
|
+
await ctx.reportWebsiteReceipts?.([{ index: 1, method: "GET", url: final.href }]);
|
|
371
|
+
if (final.origin !== new URL(url).origin)
|
|
372
|
+
throw new Error(
|
|
373
|
+
`The page redirected to ${final.href}, which this chat has not allowed. Read that address directly so the person can decide.`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
// The page and every address it links to may be read again without asking.
|
|
377
|
+
if (ctx.conversationId)
|
|
378
|
+
await recordWebAddresses(
|
|
379
|
+
ctx.conversationId,
|
|
380
|
+
new Set([...webAddressesIn(url), ...webAddressesIn(output.url), ...webAddressesIn(output.content)]),
|
|
381
|
+
);
|
|
382
|
+
return output;
|
|
383
|
+
});
|
|
301
384
|
|
|
302
385
|
export type CloudAiWebSearchInput = z.infer<typeof CloudAiWebSearchInputSchema>;
|
|
303
386
|
export type CloudAiWebSearchOutput = z.infer<typeof CloudAiWebSearchOutputSchema>;
|
package/src/ai/index.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
export { type AiAppSkillIssue, aiAppSkills, appSkillsRegistryState, reconcileAppSkills } from "./app-skill-store";
|
|
2
2
|
export { type AiApprovalPreferenceRoutes, type AiApprovalPreferenceView, createAiApprovalPreferenceRoutes } from "./approval-routes";
|
|
3
3
|
export {
|
|
4
|
+
AI_WEBSITE_APPROVAL_TOOL,
|
|
4
5
|
type AiToolApprovalContext,
|
|
5
6
|
type AiToolApprovalPreference,
|
|
7
|
+
type AiToolApprovalRemember,
|
|
6
8
|
aiToolAllowsAlways,
|
|
7
9
|
aiToolApprovalScope,
|
|
8
10
|
aiToolNeedsApproval,
|
|
11
|
+
aiTurnAllowsWebsiteApprovals,
|
|
12
|
+
aiWebsiteApprovalScope,
|
|
13
|
+
findRememberedAiToolApproval,
|
|
9
14
|
forgetAiToolApproval,
|
|
10
15
|
hasRememberedAiToolApproval,
|
|
11
16
|
listAiToolApprovalPreferences,
|
|
17
|
+
parseAiWebsiteApprovalScope,
|
|
12
18
|
rememberAiToolApproval,
|
|
13
19
|
revokeAiToolApprovalPreference,
|
|
14
20
|
} from "./approvals";
|
|
@@ -309,6 +315,7 @@ export {
|
|
|
309
315
|
} from "./transcription";
|
|
310
316
|
export type {
|
|
311
317
|
AiAccessResult,
|
|
318
|
+
AiApprovalTarget,
|
|
312
319
|
AiCapabilityToolPresentation,
|
|
313
320
|
AiClientToolId,
|
|
314
321
|
AiConversation,
|
|
@@ -360,6 +367,7 @@ export type {
|
|
|
360
367
|
AiTurnStatus,
|
|
361
368
|
AiTurnToolSource,
|
|
362
369
|
AiUserContentPart,
|
|
370
|
+
AiWebsiteReceipt,
|
|
363
371
|
} from "./types";
|
|
364
372
|
export { AI_MESSAGE_FEEDBACK_REASONS, isAiImageMediaType } from "./types";
|
|
365
373
|
export { isAiSettingsError } from "./validate";
|