@k2b/cloud 0.41.0 → 0.42.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 +6 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +145 -89
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +7 -1
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +202 -22
- package/src/ai/chat/action-text.ts +1 -1
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +56 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/http.ts +1 -0
- package/src/ai/migrate.ts +13 -0
- package/src/ai/open-questions.ts +4 -8
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +69 -31
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +168 -33
- package/src/ai/types.ts +10 -3
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +46 -38
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +5 -3
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +148 -54
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +1 -1
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/index.ts +14 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +11 -7
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +9 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +3 -2
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +3 -2
- package/src/ssr/index.ts +1 -1
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
package/src/ai/executor.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { AssistantMessage, CompactEvent, LoopAggregate, Message, NessiLoop, OutboundEvent } from "@k2b/nessi";
|
|
2
2
|
import { compact, nessi } from "@k2b/nessi";
|
|
3
3
|
import { capabilityReviewRememberScope } from "../_internal/capability-reviews";
|
|
4
|
-
import { listCapabilities } from "../_internal/registry";
|
|
4
|
+
import { getCapability, listCapabilities } from "../_internal/registry";
|
|
5
5
|
import type { CapabilityActionReviewData } from "../contracts/capabilities";
|
|
6
|
-
import { capabilityActionPolicy } from "../contracts/capabilities";
|
|
6
|
+
import { CapabilityActionReviewDataSchema, capabilityActionPolicy } from "../contracts/capabilities";
|
|
7
7
|
import type { AccessSubject, RequestActor } from "../server";
|
|
8
8
|
import { approvalAssurance, approvalScopeFromToolName, approvals } from "../services/approvals";
|
|
9
9
|
import { createHelpReader } from "../services/help";
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
import { retryTransientProviderErrors } from "./provider-retry";
|
|
74
74
|
import { assistantQuotaProvider, inferenceProvider } from "./quota-provider";
|
|
75
75
|
import { collectConversationResourceObservations } from "./resource-refs";
|
|
76
|
+
import { compactCapabilityReview } from "./review-content";
|
|
76
77
|
import { AiRunTimeout } from "./run-timeout";
|
|
77
78
|
import { isAiVisionModelConfigured, type resolveAiModel } from "./settings";
|
|
78
79
|
import { mergeAiSkillCatalog, selectAiSkillCatalog } from "./skill-catalog";
|
|
@@ -456,7 +457,12 @@ const approvalReviewForCallId = (
|
|
|
456
457
|
* to (attempt, turn) so re-claimed attempts never collide, and (b) maintains
|
|
457
458
|
* tool blocks keyed by callId, enriched with Cloud status/approval metadata.
|
|
458
459
|
*/
|
|
459
|
-
const createEventMapper = (
|
|
460
|
+
const createEventMapper = (
|
|
461
|
+
attempt: number,
|
|
462
|
+
seedBlocks: AiTurnBlock[],
|
|
463
|
+
allowRememberedApprovals = true,
|
|
464
|
+
location?: { conversationId: string; turnId: string },
|
|
465
|
+
) => {
|
|
460
466
|
const toolBlocks = new Map<string, Extract<AiTurnBlock, { kind: "tool" }>>();
|
|
461
467
|
/** Website receipts by the call that shows them and their index; every later state of that call keeps them. */
|
|
462
468
|
const receiptsByCall = new Map<string, Map<number, { method: string; url: string }>>();
|
|
@@ -586,7 +592,8 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
586
592
|
return [setTool(event.callId, { name: event.name, args: event.args, status: "running" })];
|
|
587
593
|
case "tool_action_request":
|
|
588
594
|
const displayCallId = event.kind === "custom_approval" ? (customApprovalParentCallId(event.callId) ?? event.callId) : event.callId;
|
|
589
|
-
const
|
|
595
|
+
const rawReview = approvalReviewForCallId(approvalReviews, event.callId);
|
|
596
|
+
const review = rawReview && location ? compactCapabilityReview(rawReview, { ...location, callId: displayCallId }) : rawReview;
|
|
590
597
|
const target = event.kind === "custom_approval" ? approvalTargets.get(event.callId) : undefined;
|
|
591
598
|
const allowAlways = target
|
|
592
599
|
? target.always
|
|
@@ -919,9 +926,9 @@ export class AiTurnExecutor {
|
|
|
919
926
|
const helpEnabled = toolActor !== null;
|
|
920
927
|
const toolDiscoveryEnabled = toolActor !== null;
|
|
921
928
|
const appToolsEnabled = toolActor !== null && defaultToolSource?.appTools === true;
|
|
922
|
-
let
|
|
929
|
+
let toolAuthority: Awaited<ReturnType<typeof resolveAiCapabilityActor>> | null = null;
|
|
923
930
|
try {
|
|
924
|
-
|
|
931
|
+
toolAuthority = toolActor
|
|
925
932
|
? await resolveAiCapabilityActor({ conversationId, persistedActor: material.actor, store: aiConversations })
|
|
926
933
|
: null;
|
|
927
934
|
} catch (error) {
|
|
@@ -940,7 +947,10 @@ export class AiTurnExecutor {
|
|
|
940
947
|
);
|
|
941
948
|
return;
|
|
942
949
|
}
|
|
943
|
-
if (
|
|
950
|
+
if (toolAuthority) material.actor = toolAuthority.actor;
|
|
951
|
+
// Skills and Help need current authority even when application operations are disabled.
|
|
952
|
+
// Keep the appTools switch responsible for exposing and executing those operations.
|
|
953
|
+
const capabilityAuthority = appToolsEnabled ? toolAuthority : null;
|
|
944
954
|
|
|
945
955
|
// Personalization applies to user-backed personal conversations.
|
|
946
956
|
const user = aiActorUser(material.actor);
|
|
@@ -959,7 +969,9 @@ export class AiTurnExecutor {
|
|
|
959
969
|
// executions must not gain an implicit database dependency or extra tools.
|
|
960
970
|
const skillSubject =
|
|
961
971
|
defaultToolSource && resolved.profile.capabilities.includes("tools") ? accessSubjectForActor(material.actor) : null;
|
|
962
|
-
const availableSkills = skillSubject
|
|
972
|
+
const availableSkills = skillSubject
|
|
973
|
+
? (await aiSkills.list(skillSubject, { authority: toolAuthority })).filter((skill) => skill.enabled)
|
|
974
|
+
: [];
|
|
963
975
|
const localClient = !config.background && !config.mandate ? config.client : undefined;
|
|
964
976
|
const localContext = localClient ? config.localContext : undefined;
|
|
965
977
|
const localSkills = (localContext?.skills ?? []).map((skill) => ({
|
|
@@ -996,15 +1008,15 @@ export class AiTurnExecutor {
|
|
|
996
1008
|
: undefined;
|
|
997
1009
|
const skillFiles = skillSubject
|
|
998
1010
|
? {
|
|
999
|
-
list: () => aiSkills.listTurnFiles(turnId, skillSubject),
|
|
1000
|
-
read: (path: string) => aiSkills.readTurnFile(turnId, path, skillSubject),
|
|
1011
|
+
list: () => aiSkills.listTurnFiles(turnId, skillSubject, toolAuthority),
|
|
1012
|
+
read: (path: string) => aiSkills.readTurnFile(turnId, path, skillSubject, toolAuthority),
|
|
1001
1013
|
}
|
|
1002
1014
|
: undefined;
|
|
1003
1015
|
const runtimeTools = [
|
|
1004
1016
|
...material.tools,
|
|
1005
1017
|
...(memoryActive ? [createCloudAiMemoryTool(query)] : []),
|
|
1006
|
-
...(skillSubject && availableSkills.length ? [createCloudAiLoadSkillTool(skillSubject)] : []),
|
|
1007
|
-
...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(skillSubject, localSkills)] : []),
|
|
1018
|
+
...(skillSubject && availableSkills.length ? [createCloudAiLoadSkillTool(skillSubject, toolAuthority)] : []),
|
|
1019
|
+
...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(skillSubject, localSkills, toolAuthority)] : []),
|
|
1008
1020
|
...(config.project && resolvedProjectId && projectSubject
|
|
1009
1021
|
? [
|
|
1010
1022
|
createCloudAiSearchProjectTool(resolvedProjectId, projectSubject),
|
|
@@ -1101,8 +1113,7 @@ export class AiTurnExecutor {
|
|
|
1101
1113
|
describeReview: async (approvalCallId: string, review: CapabilityActionReviewData) => {
|
|
1102
1114
|
capabilityActionReviews.set(approvalCallId, review);
|
|
1103
1115
|
const callId = customApprovalParentCallId(approvalCallId) ?? approvalCallId;
|
|
1104
|
-
const stored = storedActionReview(review);
|
|
1105
|
-
await recordAiToolReview({ conversationId, turnId, callId, review: stored });
|
|
1116
|
+
const stored = await recordAiToolReview({ conversationId, turnId, callId, review: storedActionReview(review) });
|
|
1106
1117
|
storedReviews.set(callId, stored);
|
|
1107
1118
|
await pipeline.setToolReview(callId, stored);
|
|
1108
1119
|
},
|
|
@@ -1125,8 +1136,12 @@ export class AiTurnExecutor {
|
|
|
1125
1136
|
const scope = capabilityReviewRememberScope(review, approval);
|
|
1126
1137
|
if (scope) rememberableCapabilityApprovals.set(callId, scope);
|
|
1127
1138
|
else rememberableCapabilityApprovals.delete(callId);
|
|
1128
|
-
const stored =
|
|
1129
|
-
|
|
1139
|
+
const stored = await recordAiToolReview({
|
|
1140
|
+
conversationId,
|
|
1141
|
+
turnId,
|
|
1142
|
+
callId,
|
|
1143
|
+
review: storedActionReview(review, capabilityActionPolicy(entry.app.manifest, entry.operation.localId)),
|
|
1144
|
+
});
|
|
1130
1145
|
storedReviews.set(callId, stored);
|
|
1131
1146
|
await pipeline.setToolReview(callId, stored);
|
|
1132
1147
|
};
|
|
@@ -1253,7 +1268,8 @@ export class AiTurnExecutor {
|
|
|
1253
1268
|
const tools = toolActor
|
|
1254
1269
|
? createAiToolResolver({
|
|
1255
1270
|
conversationId,
|
|
1256
|
-
actor:
|
|
1271
|
+
actor: toolAuthority?.actor ?? toolActor,
|
|
1272
|
+
authority: toolAuthority,
|
|
1257
1273
|
staticTools: activeTools,
|
|
1258
1274
|
...(activeTools.some((tool) => tool.def.name === "load_skill") ? { skillNames: availableSkills.map((skill) => skill.name) } : {}),
|
|
1259
1275
|
allowedTools,
|
|
@@ -1472,6 +1488,7 @@ export class AiTurnExecutor {
|
|
|
1472
1488
|
turnId,
|
|
1473
1489
|
skillSubject,
|
|
1474
1490
|
activeTools.some((tool) => tool.def.name === "load_skill"),
|
|
1491
|
+
toolAuthority,
|
|
1475
1492
|
);
|
|
1476
1493
|
const chat = await aiConversations.getConversation({ conversationId });
|
|
1477
1494
|
const workingPlan = chat?.todoPlan;
|
|
@@ -1742,7 +1759,7 @@ export class AiTurnExecutor {
|
|
|
1742
1759
|
approvalContext?: AiToolApprovalContext;
|
|
1743
1760
|
allowRememberedApprovals: boolean;
|
|
1744
1761
|
rememberableCapabilityApprovals: ReadonlyMap<string, string>;
|
|
1745
|
-
capabilityActionReviews:
|
|
1762
|
+
capabilityActionReviews: Map<string, CapabilityActionReviewData>;
|
|
1746
1763
|
approvalTargets: ReadonlyMap<string, AiApprovalTarget>;
|
|
1747
1764
|
linkedApprovals: ReadonlyMap<string, string>;
|
|
1748
1765
|
freshApprovalRequired?: ReadonlySet<string>;
|
|
@@ -1927,7 +1944,7 @@ export class AiTurnExecutor {
|
|
|
1927
1944
|
approvalContext?: AiToolApprovalContext;
|
|
1928
1945
|
allowRememberedApprovals: boolean;
|
|
1929
1946
|
rememberableCapabilityApprovals: ReadonlyMap<string, string>;
|
|
1930
|
-
capabilityActionReviews:
|
|
1947
|
+
capabilityActionReviews: Map<string, CapabilityActionReviewData>;
|
|
1931
1948
|
approvalTargets: ReadonlyMap<string, AiApprovalTarget>;
|
|
1932
1949
|
linkedApprovals: ReadonlyMap<string, string>;
|
|
1933
1950
|
freshApprovalRequired?: ReadonlySet<string>;
|
|
@@ -2016,7 +2033,7 @@ export class AiTurnExecutor {
|
|
|
2016
2033
|
}
|
|
2017
2034
|
}
|
|
2018
2035
|
|
|
2019
|
-
|
|
2036
|
+
let review = event.kind === "custom_approval" ? approvalReviewForCallId(capabilityActionReviews, event.callId) : undefined;
|
|
2020
2037
|
let approvalId: string | undefined;
|
|
2021
2038
|
let presented = event;
|
|
2022
2039
|
if (event.kind !== "client_tool") {
|
|
@@ -2031,6 +2048,24 @@ export class AiTurnExecutor {
|
|
|
2031
2048
|
approvalId = linked;
|
|
2032
2049
|
// The card shows the question the effect's own record holds, never the text the relay sent.
|
|
2033
2050
|
presented = { ...event, message: record.sentence };
|
|
2051
|
+
// A later linked question without structured details replaces an earlier child's review too.
|
|
2052
|
+
review = record.details === null ? undefined : CapabilityActionReviewDataSchema.parse(record.details);
|
|
2053
|
+
if (review) capabilityActionReviews.set(event.callId, review);
|
|
2054
|
+
else capabilityActionReviews.delete(event.callId);
|
|
2055
|
+
const callId = event.kind === "custom_approval" ? (customApprovalParentCallId(event.callId) ?? event.callId) : event.callId;
|
|
2056
|
+
const capability = review && "sentence" in review && !review.ask ? await getCapability(record.appId).catch(() => null) : null;
|
|
2057
|
+
// Keep the original proposal on the existing tool-call message as well as the approval.
|
|
2058
|
+
// Its chat reference must remain readable after the approval has been consumed.
|
|
2059
|
+
const stored = await recordAiToolReview({
|
|
2060
|
+
conversationId,
|
|
2061
|
+
turnId,
|
|
2062
|
+
callId,
|
|
2063
|
+
review: storedActionReview(
|
|
2064
|
+
review ?? { message: record.sentence },
|
|
2065
|
+
capability ? capabilityActionPolicy(capability.manifest, record.action) : undefined,
|
|
2066
|
+
),
|
|
2067
|
+
});
|
|
2068
|
+
await pipeline.setToolReview(callId, stored);
|
|
2034
2069
|
} else {
|
|
2035
2070
|
const record = await approvals.request({
|
|
2036
2071
|
...approvalScopeFromToolName(toolName),
|
|
@@ -2315,7 +2350,7 @@ class StreamPipeline {
|
|
|
2315
2350
|
this.seq = input.startSeq;
|
|
2316
2351
|
this.blocks = [];
|
|
2317
2352
|
this.allowRememberedApprovals = input.allowRememberedApprovals;
|
|
2318
|
-
this.mapper = createEventMapper(input.attempt, input.seedBlocks, input.allowRememberedApprovals);
|
|
2353
|
+
this.mapper = createEventMapper(input.attempt, input.seedBlocks, input.allowRememberedApprovals, input);
|
|
2319
2354
|
}
|
|
2320
2355
|
|
|
2321
2356
|
private ordered(run: () => Promise<void>): Promise<void> {
|
|
@@ -3,6 +3,7 @@ import { type RequestOptions as HttpsRequestOptions, request as httpsRequest } f
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { DOCUMENT_EXTRACTION_MAX_INPUT_BYTES, DOCUMENT_EXTRACTION_MAX_OUTPUT_BYTES } from "../services/document-extraction";
|
|
5
5
|
import { type PublicNetworkAddress, resolvePublicNetworkAddresses } from "../services/network-security";
|
|
6
|
+
import { awaitNetworkOperation, readNetworkResponse, withNetworkDeadline } from "../services/network-transport";
|
|
6
7
|
import { AI_FILES_MAX_FILE_BYTES_DEFAULT, aiFileStore, guessAiMediaType, normalizeAiFilePath } from "./files-store";
|
|
7
8
|
import { authorizeWebRead } from "./firecrawl-tools";
|
|
8
9
|
import { htmlToReadableMarkdown } from "./readable-html";
|
|
@@ -21,10 +22,11 @@ type PublicFileResponse = {
|
|
|
21
22
|
type FetchFileDependencies = {
|
|
22
23
|
resolve?: (hostname: string) => Promise<PublicNetworkAddress[]>;
|
|
23
24
|
request?: RequestFactory;
|
|
25
|
+
timeoutMs?: number;
|
|
24
26
|
requestPublicFile?: (
|
|
25
27
|
url: URL,
|
|
26
28
|
addresses: PublicNetworkAddress[],
|
|
27
|
-
options: { maxBytes: number; signal
|
|
29
|
+
options: { maxBytes: number; signal: AbortSignal },
|
|
28
30
|
) => Promise<PublicFileResponse>;
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -72,7 +74,6 @@ export const buildPinnedFetchFileRequestOptions = (url: URL, addresses: PublicNe
|
|
|
72
74
|
},
|
|
73
75
|
servername: url.hostname,
|
|
74
76
|
rejectUnauthorized: true,
|
|
75
|
-
timeout: FETCH_FILE_REQUEST_TIMEOUT_MS,
|
|
76
77
|
}));
|
|
77
78
|
};
|
|
78
79
|
|
|
@@ -103,52 +104,40 @@ const httpFileError = (statusCode: number): Error => {
|
|
|
103
104
|
const requestOneAddress = (
|
|
104
105
|
request: RequestFactory,
|
|
105
106
|
options: HttpsRequestOptions,
|
|
106
|
-
input: { maxBytes: number; signal
|
|
107
|
-
): Promise<PublicFileResponse> =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (settled) return;
|
|
115
|
-
settled = true;
|
|
116
|
-
input.signal?.removeEventListener("abort", abort);
|
|
117
|
-
if (result.error) reject(responseReceived ? Object.assign(result.error, { responseReceived: true }) : result.error);
|
|
118
|
-
else resolve(result.value!);
|
|
119
|
-
};
|
|
120
|
-
outgoing = request(options, (incoming) => {
|
|
107
|
+
input: { maxBytes: number; signal: AbortSignal },
|
|
108
|
+
): Promise<PublicFileResponse> => {
|
|
109
|
+
let responseReceived = false;
|
|
110
|
+
return readNetworkResponse<PublicFileResponse>({
|
|
111
|
+
request,
|
|
112
|
+
options,
|
|
113
|
+
signal: input.signal,
|
|
114
|
+
read: (incoming, resolve, reject) => {
|
|
121
115
|
responseReceived = true;
|
|
122
116
|
const statusCode = incoming.statusCode ?? 0;
|
|
123
117
|
if (statusCode >= 300 && statusCode < 400) {
|
|
124
|
-
incoming.
|
|
125
|
-
finish({ value: { statusCode, headers: incoming.headers, bytes: new Uint8Array() } });
|
|
118
|
+
resolve({ statusCode, headers: incoming.headers, bytes: new Uint8Array() });
|
|
126
119
|
return;
|
|
127
120
|
}
|
|
128
121
|
if (statusCode < 200 || statusCode >= 300) {
|
|
129
|
-
|
|
130
|
-
finish({ error: httpFileError(statusCode) });
|
|
122
|
+
reject(httpFileError(statusCode));
|
|
131
123
|
return;
|
|
132
124
|
}
|
|
133
125
|
const contentEncoding = String(incoming.headers["content-encoding"] ?? "identity")
|
|
134
126
|
.trim()
|
|
135
127
|
.toLowerCase();
|
|
136
128
|
if (contentEncoding !== "" && contentEncoding !== "identity") {
|
|
137
|
-
|
|
138
|
-
finish({ error: responseError("Unsupported file response — The server compressed the download unexpectedly.") });
|
|
129
|
+
reject(responseError("Unsupported file response — The server compressed the download unexpectedly."));
|
|
139
130
|
return;
|
|
140
131
|
}
|
|
141
132
|
let declared: number | null;
|
|
142
133
|
try {
|
|
143
134
|
declared = declaredContentLength(incoming.headers);
|
|
144
135
|
} catch (error) {
|
|
145
|
-
|
|
146
|
-
finish({ error: responseError(error instanceof Error ? error.message : "File response length is invalid.") });
|
|
136
|
+
reject(responseError(error instanceof Error ? error.message : "File response length is invalid."));
|
|
147
137
|
return;
|
|
148
138
|
}
|
|
149
139
|
if (declared !== null && declared > input.maxBytes) {
|
|
150
|
-
|
|
151
|
-
finish({ error: responseError(`File is too large — The download exceeds the ${fileLimitLabel(input.maxBytes)} limit.`) });
|
|
140
|
+
reject(responseError(`File is too large — The download exceeds the ${fileLimitLabel(input.maxBytes)} limit.`));
|
|
152
141
|
return;
|
|
153
142
|
}
|
|
154
143
|
|
|
@@ -158,36 +147,29 @@ const requestOneAddress = (
|
|
|
158
147
|
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
159
148
|
size += bytes.byteLength;
|
|
160
149
|
if (size > input.maxBytes) {
|
|
161
|
-
|
|
162
|
-
finish({ error: responseError(`File is too large — The download exceeds the ${fileLimitLabel(input.maxBytes)} limit.`) });
|
|
150
|
+
reject(responseError(`File is too large — The download exceeds the ${fileLimitLabel(input.maxBytes)} limit.`));
|
|
163
151
|
return;
|
|
164
152
|
}
|
|
165
153
|
chunks.push(bytes);
|
|
166
154
|
});
|
|
167
155
|
incoming.on("end", () => {
|
|
168
156
|
if (declared !== null && size !== declared) {
|
|
169
|
-
|
|
157
|
+
reject(responseError("Incomplete file download — The received size did not match the server response."));
|
|
170
158
|
return;
|
|
171
159
|
}
|
|
172
160
|
const body = Buffer.concat(chunks, size);
|
|
173
|
-
|
|
161
|
+
resolve({ statusCode, headers: incoming.headers, bytes: new Uint8Array(body) });
|
|
174
162
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
outgoing.on("error", (error) => finish({ error }));
|
|
179
|
-
if (input.signal?.aborted) {
|
|
180
|
-
abort();
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
input.signal?.addEventListener("abort", abort, { once: true });
|
|
184
|
-
outgoing.end();
|
|
163
|
+
},
|
|
164
|
+
}).catch((error) => {
|
|
165
|
+
throw responseReceived ? Object.assign(error, { responseReceived: true }) : error;
|
|
185
166
|
});
|
|
167
|
+
};
|
|
186
168
|
|
|
187
169
|
const requestPublicFile = async (
|
|
188
170
|
url: URL,
|
|
189
171
|
addresses: PublicNetworkAddress[],
|
|
190
|
-
options: { maxBytes: number; signal
|
|
172
|
+
options: { maxBytes: number; signal: AbortSignal },
|
|
191
173
|
request: RequestFactory = httpsRequest,
|
|
192
174
|
): Promise<PublicFileResponse> => {
|
|
193
175
|
let lastError: unknown;
|
|
@@ -235,42 +217,51 @@ type WebsiteApprovedDownload = { origin: string; onRequest: (url: URL, hop: numb
|
|
|
235
217
|
export const downloadPublicFile = async (
|
|
236
218
|
rawUrl: string,
|
|
237
219
|
options: FetchFileDependencies & { maxBytes?: number; signal?: AbortSignal; approvedWebsite?: WebsiteApprovedDownload } = {},
|
|
238
|
-
): Promise<{ bytes: Uint8Array; headers: IncomingHttpHeaders; url: URL }> =>
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
options.
|
|
242
|
-
(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
220
|
+
): Promise<{ bytes: Uint8Array; headers: IncomingHttpHeaders; url: URL }> =>
|
|
221
|
+
withNetworkDeadline(
|
|
222
|
+
options.timeoutMs ?? FETCH_FILE_REQUEST_TIMEOUT_MS,
|
|
223
|
+
options.signal,
|
|
224
|
+
new Error("File server did not respond — Try again later."),
|
|
225
|
+
async (signal) => {
|
|
226
|
+
const resolve = options.resolve ?? resolvePublicNetworkAddresses;
|
|
227
|
+
const perform =
|
|
228
|
+
options.requestPublicFile ??
|
|
229
|
+
((url: URL, addresses: PublicNetworkAddress[], input: { maxBytes: number; signal: AbortSignal }) =>
|
|
230
|
+
requestPublicFile(url, addresses, input, options.request));
|
|
231
|
+
const maxBytes = options.maxBytes ?? AI_FILES_MAX_FILE_BYTES_DEFAULT;
|
|
232
|
+
let url = publicHttpsUrl(rawUrl);
|
|
246
233
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
234
|
+
for (let redirects = 0; redirects <= FETCH_FILE_MAX_REDIRECTS; redirects++) {
|
|
235
|
+
signal.throwIfAborted();
|
|
236
|
+
if (options.approvedWebsite) {
|
|
237
|
+
// A website approval covers its own origin only; an open redirect must not carry the address to another one.
|
|
238
|
+
if (url.origin !== options.approvedWebsite.origin)
|
|
239
|
+
throw new Error(
|
|
240
|
+
`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.`,
|
|
241
|
+
);
|
|
242
|
+
await awaitNetworkOperation(options.approvedWebsite.onRequest(url, redirects), signal);
|
|
243
|
+
}
|
|
244
|
+
signal.throwIfAborted();
|
|
245
|
+
const addresses = await awaitNetworkOperation(resolveFileAddresses(resolve, url.hostname), signal);
|
|
246
|
+
signal.throwIfAborted();
|
|
247
|
+
const response = await awaitNetworkOperation(perform(url, addresses, { maxBytes, signal }), signal);
|
|
248
|
+
if (response.statusCode >= 300 && response.statusCode < 400) {
|
|
249
|
+
const location = responseLocation(response.headers);
|
|
250
|
+
if (!location) throw new Error("Invalid file redirect — The server did not provide a destination.");
|
|
251
|
+
if (redirects === FETCH_FILE_MAX_REDIRECTS) throw new Error("Too many file redirects — Use the final file URL instead.");
|
|
252
|
+
try {
|
|
253
|
+
url = publicHttpsUrl(new URL(location, url).toString());
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error instanceof Error && error.message.includes(" — ")) throw error;
|
|
256
|
+
throw new Error("Invalid file redirect — The server provided an invalid destination.");
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
return { bytes: response.bytes, headers: response.headers, url };
|
|
267
261
|
}
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
throw new Error("Too many file redirects — Use the final file URL instead.");
|
|
273
|
-
};
|
|
262
|
+
throw new Error("Too many file redirects — Use the final file URL instead.");
|
|
263
|
+
},
|
|
264
|
+
);
|
|
274
265
|
|
|
275
266
|
const safeFilename = (requested: string | undefined, url: URL): string => {
|
|
276
267
|
let basename = requested;
|
package/src/ai/http.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const AiToolPreloadSchema = z.union([
|
|
|
70
70
|
]);
|
|
71
71
|
|
|
72
72
|
export const AiCreateConversationInputSchema = z.object({
|
|
73
|
+
requestId: z.uuid().optional(),
|
|
73
74
|
title: z.string().trim().min(1).max(120).optional(),
|
|
74
75
|
projectId: z.string().regex(AI_SHORT_ID_PATTERN).optional(),
|
|
75
76
|
draft: AiInitialConversationDraftInputSchema.optional(),
|
package/src/ai/migrate.ts
CHANGED
|
@@ -47,6 +47,17 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
47
47
|
)
|
|
48
48
|
`.simple();
|
|
49
49
|
|
|
50
|
+
// Keep the request identity after deletion: retrying a lost response must not resurrect a chat.
|
|
51
|
+
await sql`
|
|
52
|
+
CREATE TABLE IF NOT EXISTS ai.conversation_creations (
|
|
53
|
+
owner_user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
54
|
+
request_id UUID NOT NULL,
|
|
55
|
+
input_hash TEXT NOT NULL,
|
|
56
|
+
conversation_id UUID REFERENCES ai.conversations(id) ON DELETE SET NULL,
|
|
57
|
+
PRIMARY KEY (owner_user_id, request_id)
|
|
58
|
+
)
|
|
59
|
+
`.simple();
|
|
60
|
+
|
|
50
61
|
// AI chat ownership was unreleased and only backed local test data. Make the
|
|
51
62
|
// product-model cut once, then keep only the global conversation contract.
|
|
52
63
|
await sql`
|
|
@@ -2657,6 +2668,8 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
2657
2668
|
);
|
|
2658
2669
|
}
|
|
2659
2670
|
await backfillApprovalRequests();
|
|
2671
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_ai_messages_submitted_draft
|
|
2672
|
+
ON ai.messages (conversation_id, (meta->>'submittedDraftRevision')) WHERE role = 'user'`.simple();
|
|
2660
2673
|
console.log(" ✓ ai conversation tables");
|
|
2661
2674
|
};
|
|
2662
2675
|
|
package/src/ai/open-questions.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { sql } from "bun";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { APPROVALS_PAGE_SIZE } from "../contracts/approvals";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
CapabilityActionReviewDataSchema,
|
|
7
|
-
CapabilitySemanticLinkSchema,
|
|
8
|
-
} from "../contracts/capabilities";
|
|
9
|
-
import { openRequests } from "../services/approvals/list";
|
|
4
|
+
import { CapabilityActionReviewPresentationSchema, CapabilitySemanticLinkSchema } from "../contracts/capabilities";
|
|
5
|
+
import { openRequests } from "../services/approvals/content";
|
|
10
6
|
import { logger } from "../services/logging";
|
|
11
7
|
import { reviewShortText, toolActionCard, toolBlockReview } from "./chat/action-card";
|
|
12
8
|
import { aiToolIcon } from "./chat/message-utils";
|
|
@@ -142,11 +138,11 @@ export const openQuestionFromRow = (row: QuestionRow, input: { locale: string; t
|
|
|
142
138
|
!["running", "awaiting_approval", "awaiting_client", "completed", "failed", "rejected"].includes(block.status)
|
|
143
139
|
)
|
|
144
140
|
throw new Error("Malformed tool block");
|
|
145
|
-
if (block.review == null && block.approval?.review != null)
|
|
141
|
+
if (block.review == null && block.approval?.review != null) CapabilityActionReviewPresentationSchema.parse(block.approval.review);
|
|
146
142
|
const review = toolBlockReview(block);
|
|
147
143
|
if (review !== null) {
|
|
148
144
|
if ("legacy" in review) legacyReviewSchema.parse(review.legacy);
|
|
149
|
-
else
|
|
145
|
+
else CapabilityActionReviewPresentationSchema.parse(review);
|
|
150
146
|
}
|
|
151
147
|
const card = toolActionCard(block, {
|
|
152
148
|
locale: input.locale,
|
package/src/ai/projects.ts
CHANGED
|
@@ -295,8 +295,7 @@ const listProjectAccess = async (projectId: string, db: SQL = sql): Promise<AiPr
|
|
|
295
295
|
const rows = await db<ProjectAccessRow[]>`
|
|
296
296
|
SELECT project_access.short_id, access.user_id, access.group_id, access.service_account_id, access.authenticated_only,
|
|
297
297
|
access.permission, access.created_at,
|
|
298
|
-
COALESCE(NULLIF(users.display_name, ''), users.uid, groups.name, service_accounts.name,
|
|
299
|
-
CASE WHEN access.authenticated_only THEN 'All authenticated users' ELSE 'Public' END) AS display_name,
|
|
298
|
+
COALESCE(NULLIF(users.display_name, ''), users.uid, groups.name, service_accounts.name) AS display_name,
|
|
300
299
|
users.avatar_hash, service_accounts.kind AS service_account_kind
|
|
301
300
|
FROM ai.project_access project_access
|
|
302
301
|
JOIN auth.access access ON access.id = project_access.access_id
|
package/src/ai/protocol.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CompactResult, InboundEvent, Message } from "@k2b/nessi";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CapabilityActionReviewPresentation } from "../contracts/capabilities";
|
|
3
3
|
import type { AiActionCheck, AiStoredActionReview } from "./action-review";
|
|
4
4
|
import type { AiConversation, AiFrontendToolMode, AiStoredMessage, AiToolPresentation, AiTurnStatus } from "./types";
|
|
5
5
|
|
|
@@ -49,7 +49,7 @@ export type AiTurnBlock =
|
|
|
49
49
|
/** Present while status is awaiting_approval. */
|
|
50
50
|
approval?: {
|
|
51
51
|
message?: string;
|
|
52
|
-
review?:
|
|
52
|
+
review?: CapabilityActionReviewPresentation;
|
|
53
53
|
allowAlways: boolean;
|
|
54
54
|
/** Whether the approval can be remembered for this chat. */
|
|
55
55
|
allowChat?: boolean;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
3
|
-
import {
|
|
4
|
-
import { type
|
|
3
|
+
import { CAPABILITY_DISCOVERY_TIMEOUT_MS } from "../_internal/capability-timeout";
|
|
4
|
+
import { type listApps, listCapabilities } from "../_internal/registry";
|
|
5
|
+
import { type CapabilityDispatchParams, dispatchCapability } from "../api/capabilities";
|
|
5
6
|
import {
|
|
6
7
|
CloudResourceRefSchema,
|
|
7
8
|
capabilityResultSchema,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
} from "../contracts/capabilities";
|
|
12
13
|
import type { CapabilityRegistryEntry } from "../contracts/registry";
|
|
13
14
|
import type { RequestAuthority } from "../server";
|
|
15
|
+
import { discoverableAppEntries } from "../services/app-discovery";
|
|
14
16
|
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
15
17
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
16
18
|
import { CODE_SOURCE_TOOLS } from "./code-source-contracts";
|
|
@@ -65,6 +67,7 @@ const studioReader = async (input: {
|
|
|
65
67
|
export type AiRecentResourceDependencies = {
|
|
66
68
|
store?: Pick<AiConversationService, "listRecentSentResourceRefs">;
|
|
67
69
|
listRegistry?: () => Promise<CapabilityRegistryEntry[]>;
|
|
70
|
+
listApps?: typeof listApps;
|
|
68
71
|
dispatch?: (params: CapabilityDispatchParams) => Promise<Response>;
|
|
69
72
|
skills?: Pick<typeof aiSkills, "list">;
|
|
70
73
|
studioReader?: typeof studioReader;
|
|
@@ -108,7 +111,7 @@ export const listAiRecentResources = async (input: {
|
|
|
108
111
|
dependencies?: AiRecentResourceDependencies;
|
|
109
112
|
}): Promise<AiRecentResource[]> => {
|
|
110
113
|
const deps = input.dependencies ?? {};
|
|
111
|
-
const signal = AbortSignal.any([input.signal, AbortSignal.timeout(deps.timeoutMs ??
|
|
114
|
+
const signal = AbortSignal.any([input.signal, AbortSignal.timeout(deps.timeoutMs ?? CAPABILITY_DISCOVERY_TIMEOUT_MS)]);
|
|
112
115
|
// Unreadable history is no answer, not an empty one: the request fails, so the browser asks again at its next opening.
|
|
113
116
|
const sent = await withinBudget(signal, () =>
|
|
114
117
|
(deps.store ?? aiConversations).listRecentSentResourceRefs({ ownerUserId: input.ownerUserId }),
|
|
@@ -118,10 +121,14 @@ export const listAiRecentResources = async (input: {
|
|
|
118
121
|
const skillIds = candidates.flatMap(({ ref }) => (ref.type === "core.ai.skill" ? [ref.id] : []));
|
|
119
122
|
// Exactly the sent Skills, so a large Skill catalog never pushes one out; failing, they are left out like a reader.
|
|
120
123
|
const skills = skillIds.length
|
|
121
|
-
? withinBudget(signal, () =>
|
|
124
|
+
? withinBudget(signal, () =>
|
|
125
|
+
(deps.skills ?? aiSkills).list(input.authority.accessSubject, { shortIds: skillIds, authority: input.authority }),
|
|
126
|
+
).catch(() => [])
|
|
122
127
|
: Promise.resolve([]);
|
|
123
128
|
const registry = candidates.some(({ ref }) => ref.type !== "core.ai.skill" && ref.type !== "assistant.artifact")
|
|
124
|
-
? withinBudget(signal,
|
|
129
|
+
? withinBudget(signal, async () =>
|
|
130
|
+
discoverableAppEntries(input.authority, await (deps.listRegistry ?? listCapabilities)(), deps.listApps),
|
|
131
|
+
).catch(() => [])
|
|
125
132
|
: Promise.resolve([]);
|
|
126
133
|
const resources = await Promise.all(
|
|
127
134
|
candidates.map(async (candidate): Promise<AiRecentResource | null> => {
|