@k2b/cloud 0.40.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 +9 -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-sentences.ts +2 -1
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +158 -92
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/setup-lock.ts +103 -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 +76 -4
- 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 +214 -23
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +1 -26
- 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 +64 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/http.ts +1 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/migrate.ts +28 -0
- package/src/ai/open-questions.ts +168 -0
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +119 -35
- 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 +184 -31
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +12 -6
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +25 -4
- 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/app-approval-actions.ts +31 -7
- 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/auth/schemas.ts +3 -0
- package/src/api/auth.ts +54 -18
- 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 +73 -41
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/me.ts +79 -3
- 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 +30 -66
- package/src/approvals/approval-card.ts +68 -0
- 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-approval.ts +3 -3
- 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 +149 -55
- 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/contracts/shared.ts +3 -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 +5 -3
- 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/app.ts +23 -3
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/accounts/users.ts +12 -1
- 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/device.ts +49 -33
- package/src/services/approvals/index.ts +15 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +72 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/password-reset.ts +32 -6
- 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 +11 -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/markdown.ts +160 -2
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/session/index.ts +203 -29
- 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/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +185 -542
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- 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 +29 -0
- 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 +7 -5
- package/src/ssr/index.ts +2 -1
- package/src/ssr/islands/index.ts +3 -4
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-views.tsx +484 -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-idle.ts +9 -0
- 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/approvals.css +6 -0
- package/src/styles/effects.css +15 -0
- 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),
|
|
@@ -1055,6 +1067,8 @@ export class AiTurnExecutor {
|
|
|
1055
1067
|
const approvalTargets = new Map<string, AiApprovalTarget>();
|
|
1056
1068
|
const linkedApprovals = new Map<string, string>();
|
|
1057
1069
|
const freshApprovalRequired = new Set<string>();
|
|
1070
|
+
const capabilityActionReviews = new Map<string, CapabilityActionReviewData>();
|
|
1071
|
+
const storedReviews = new Map<string, AiStoredActionReview>();
|
|
1058
1072
|
const dynamicToolRuntimeContext = {
|
|
1059
1073
|
turnId,
|
|
1060
1074
|
reportToolProgress: (callId: string, message: string) => pipeline.reportToolProgress(callId, message),
|
|
@@ -1096,6 +1110,13 @@ export class AiTurnExecutor {
|
|
|
1096
1110
|
return "changed";
|
|
1097
1111
|
},
|
|
1098
1112
|
describeApproval: (approvalCallId: string, target: AiApprovalTarget) => approvalTargets.set(approvalCallId, target),
|
|
1113
|
+
describeReview: async (approvalCallId: string, review: CapabilityActionReviewData) => {
|
|
1114
|
+
capabilityActionReviews.set(approvalCallId, review);
|
|
1115
|
+
const callId = customApprovalParentCallId(approvalCallId) ?? approvalCallId;
|
|
1116
|
+
const stored = await recordAiToolReview({ conversationId, turnId, callId, review: storedActionReview(review) });
|
|
1117
|
+
storedReviews.set(callId, stored);
|
|
1118
|
+
await pipeline.setToolReview(callId, stored);
|
|
1119
|
+
},
|
|
1099
1120
|
};
|
|
1100
1121
|
const prepared = prepareAiTools({
|
|
1101
1122
|
tools: activeTools,
|
|
@@ -1105,8 +1126,6 @@ export class AiTurnExecutor {
|
|
|
1105
1126
|
});
|
|
1106
1127
|
let backgroundError: string | null = null;
|
|
1107
1128
|
const rememberableCapabilityApprovals = new Map<string, string>();
|
|
1108
|
-
const capabilityActionReviews = new Map<string, CapabilityActionReviewData>();
|
|
1109
|
-
const storedReviews = new Map<string, AiStoredActionReview>();
|
|
1110
1129
|
const onCapabilityReview = async (
|
|
1111
1130
|
callId: string,
|
|
1112
1131
|
review: CapabilityActionReviewData,
|
|
@@ -1117,8 +1136,12 @@ export class AiTurnExecutor {
|
|
|
1117
1136
|
const scope = capabilityReviewRememberScope(review, approval);
|
|
1118
1137
|
if (scope) rememberableCapabilityApprovals.set(callId, scope);
|
|
1119
1138
|
else rememberableCapabilityApprovals.delete(callId);
|
|
1120
|
-
const stored =
|
|
1121
|
-
|
|
1139
|
+
const stored = await recordAiToolReview({
|
|
1140
|
+
conversationId,
|
|
1141
|
+
turnId,
|
|
1142
|
+
callId,
|
|
1143
|
+
review: storedActionReview(review, capabilityActionPolicy(entry.app.manifest, entry.operation.localId)),
|
|
1144
|
+
});
|
|
1122
1145
|
storedReviews.set(callId, stored);
|
|
1123
1146
|
await pipeline.setToolReview(callId, stored);
|
|
1124
1147
|
};
|
|
@@ -1245,7 +1268,8 @@ export class AiTurnExecutor {
|
|
|
1245
1268
|
const tools = toolActor
|
|
1246
1269
|
? createAiToolResolver({
|
|
1247
1270
|
conversationId,
|
|
1248
|
-
actor:
|
|
1271
|
+
actor: toolAuthority?.actor ?? toolActor,
|
|
1272
|
+
authority: toolAuthority,
|
|
1249
1273
|
staticTools: activeTools,
|
|
1250
1274
|
...(activeTools.some((tool) => tool.def.name === "load_skill") ? { skillNames: availableSkills.map((skill) => skill.name) } : {}),
|
|
1251
1275
|
allowedTools,
|
|
@@ -1464,6 +1488,7 @@ export class AiTurnExecutor {
|
|
|
1464
1488
|
turnId,
|
|
1465
1489
|
skillSubject,
|
|
1466
1490
|
activeTools.some((tool) => tool.def.name === "load_skill"),
|
|
1491
|
+
toolAuthority,
|
|
1467
1492
|
);
|
|
1468
1493
|
const chat = await aiConversations.getConversation({ conversationId });
|
|
1469
1494
|
const workingPlan = chat?.todoPlan;
|
|
@@ -1734,7 +1759,7 @@ export class AiTurnExecutor {
|
|
|
1734
1759
|
approvalContext?: AiToolApprovalContext;
|
|
1735
1760
|
allowRememberedApprovals: boolean;
|
|
1736
1761
|
rememberableCapabilityApprovals: ReadonlyMap<string, string>;
|
|
1737
|
-
capabilityActionReviews:
|
|
1762
|
+
capabilityActionReviews: Map<string, CapabilityActionReviewData>;
|
|
1738
1763
|
approvalTargets: ReadonlyMap<string, AiApprovalTarget>;
|
|
1739
1764
|
linkedApprovals: ReadonlyMap<string, string>;
|
|
1740
1765
|
freshApprovalRequired?: ReadonlySet<string>;
|
|
@@ -1919,7 +1944,7 @@ export class AiTurnExecutor {
|
|
|
1919
1944
|
approvalContext?: AiToolApprovalContext;
|
|
1920
1945
|
allowRememberedApprovals: boolean;
|
|
1921
1946
|
rememberableCapabilityApprovals: ReadonlyMap<string, string>;
|
|
1922
|
-
capabilityActionReviews:
|
|
1947
|
+
capabilityActionReviews: Map<string, CapabilityActionReviewData>;
|
|
1923
1948
|
approvalTargets: ReadonlyMap<string, AiApprovalTarget>;
|
|
1924
1949
|
linkedApprovals: ReadonlyMap<string, string>;
|
|
1925
1950
|
freshApprovalRequired?: ReadonlySet<string>;
|
|
@@ -2008,7 +2033,7 @@ export class AiTurnExecutor {
|
|
|
2008
2033
|
}
|
|
2009
2034
|
}
|
|
2010
2035
|
|
|
2011
|
-
|
|
2036
|
+
let review = event.kind === "custom_approval" ? approvalReviewForCallId(capabilityActionReviews, event.callId) : undefined;
|
|
2012
2037
|
let approvalId: string | undefined;
|
|
2013
2038
|
let presented = event;
|
|
2014
2039
|
if (event.kind !== "client_tool") {
|
|
@@ -2023,6 +2048,24 @@ export class AiTurnExecutor {
|
|
|
2023
2048
|
approvalId = linked;
|
|
2024
2049
|
// The card shows the question the effect's own record holds, never the text the relay sent.
|
|
2025
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);
|
|
2026
2069
|
} else {
|
|
2027
2070
|
const record = await approvals.request({
|
|
2028
2071
|
...approvalScopeFromToolName(toolName),
|
|
@@ -2307,7 +2350,7 @@ class StreamPipeline {
|
|
|
2307
2350
|
this.seq = input.startSeq;
|
|
2308
2351
|
this.blocks = [];
|
|
2309
2352
|
this.allowRememberedApprovals = input.allowRememberedApprovals;
|
|
2310
|
-
this.mapper = createEventMapper(input.attempt, input.seedBlocks, input.allowRememberedApprovals);
|
|
2353
|
+
this.mapper = createEventMapper(input.attempt, input.seedBlocks, input.allowRememberedApprovals, input);
|
|
2311
2354
|
}
|
|
2312
2355
|
|
|
2313
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;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type ToolContext, truncateMiddle } from "@k2b/nessi";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import type { CapabilityActionCallReview } from "../contracts/capabilities";
|
|
3
4
|
import type { RequestActor } from "../server";
|
|
4
5
|
import { coreSettings } from "../services";
|
|
5
6
|
import { isUnsafeNetworkAddress, isUnsafeNetworkHostname, parseNetworkAddress } from "../shared/network-address";
|
|
6
7
|
import { AI_WEBSITE_APPROVAL_TOOL, aiTurnAllowsWebsiteApprovals, aiWebsiteApprovalScope, findRememberedAiToolApproval } from "./approvals";
|
|
7
8
|
import { aiConversations } from "./store";
|
|
9
|
+
import { cloudToolReview } from "./tool-sentences";
|
|
8
10
|
import { defineAiTool } from "./tools";
|
|
9
11
|
import type { AiApprovalTarget, AiWebsiteReceipt } from "./types";
|
|
10
12
|
import { hasWebAddressProvenance, recordWebAddresses, webAddressesIn } from "./web-provenance";
|
|
@@ -280,7 +282,7 @@ export type WebReadContext = Pick<ToolContext, "requestApproval"> & {
|
|
|
280
282
|
conversationId?: string;
|
|
281
283
|
turnId?: string;
|
|
282
284
|
locale?: string;
|
|
283
|
-
requestApprovalFor?: (message: string, target: AiApprovalTarget) => Promise<boolean>;
|
|
285
|
+
requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
|
|
284
286
|
reportWebsiteReceipts?: (receipts: AiWebsiteReceipt[]) => Promise<void>;
|
|
285
287
|
};
|
|
286
288
|
|
|
@@ -306,7 +308,18 @@ export const authorizeWebRead = async (kind: "page" | "file", url: string, ctx:
|
|
|
306
308
|
return true;
|
|
307
309
|
}
|
|
308
310
|
const message = webReadApprovalMessage(kind, url, ctx.locale);
|
|
309
|
-
const
|
|
311
|
+
const review = cloudToolReview(kind === "page" ? "web_extract" : "fetch_file", { url }, ctx.locale ?? "en");
|
|
312
|
+
const question: CapabilityActionCallReview | undefined = review
|
|
313
|
+
? {
|
|
314
|
+
...review,
|
|
315
|
+
ask: "sends",
|
|
316
|
+
consequences: message
|
|
317
|
+
.split("\n")
|
|
318
|
+
.slice(1)
|
|
319
|
+
.map((text) => ({ text })),
|
|
320
|
+
}
|
|
321
|
+
: undefined;
|
|
322
|
+
const approved = ctx.requestApprovalFor ? await ctx.requestApprovalFor(message, target, question) : await ctx.requestApproval(message);
|
|
310
323
|
if (!approved) throw new Error("The person did not allow reading this address. Use an address from their message or a search result.");
|
|
311
324
|
return false;
|
|
312
325
|
};
|
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/index.ts
CHANGED
|
@@ -262,6 +262,7 @@ export {
|
|
|
262
262
|
createCloudAiMemoryTool,
|
|
263
263
|
} from "./memory-tool";
|
|
264
264
|
export { migrateCloudAi } from "./migrate";
|
|
265
|
+
export { type AiOpenQuestion, type AiOpenQuestions, listAiOpenQuestions } from "./open-questions";
|
|
265
266
|
export { personalAiModelPolicy } from "./personal-agent";
|
|
266
267
|
export { type AiUserPrefs, aiActorUser, aiPrefsUserId, aiUserPrefs } from "./prefs";
|
|
267
268
|
export {
|
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`
|
|
@@ -2297,6 +2308,21 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
2297
2308
|
AFTER UPDATE OF status, error, completed_at, cancel_requested_at ON ai.turns
|
|
2298
2309
|
FOR EACH ROW EXECUTE FUNCTION ai.live_conversation_child_changed('conversation-list,conversation-detail')
|
|
2299
2310
|
`.simple();
|
|
2311
|
+
// Only questions for the person change the list of open questions; browser tool calls come and go per turn.
|
|
2312
|
+
await sql`DROP TRIGGER IF EXISTS ai_live_pending_actions_changed ON ai.pending_actions`.simple();
|
|
2313
|
+
await sql`DROP TRIGGER IF EXISTS ai_live_pending_actions_removed ON ai.pending_actions`.simple();
|
|
2314
|
+
await sql`
|
|
2315
|
+
CREATE TRIGGER ai_live_pending_actions_changed
|
|
2316
|
+
AFTER INSERT OR UPDATE OF status ON ai.pending_actions
|
|
2317
|
+
FOR EACH ROW WHEN (NEW.kind IN ('approval', 'custom_approval'))
|
|
2318
|
+
EXECUTE FUNCTION ai.live_conversation_child_changed('conversation-list')
|
|
2319
|
+
`.simple();
|
|
2320
|
+
await sql`
|
|
2321
|
+
CREATE TRIGGER ai_live_pending_actions_removed
|
|
2322
|
+
AFTER DELETE ON ai.pending_actions
|
|
2323
|
+
FOR EACH ROW WHEN (OLD.kind IN ('approval', 'custom_approval'))
|
|
2324
|
+
EXECUTE FUNCTION ai.live_conversation_child_changed('conversation-list')
|
|
2325
|
+
`.simple();
|
|
2300
2326
|
await sql`DROP TRIGGER IF EXISTS ai_live_chat_tasks_changed ON ai.chat_tasks`.simple();
|
|
2301
2327
|
await sql`
|
|
2302
2328
|
CREATE TRIGGER ai_live_chat_tasks_changed AFTER INSERT OR UPDATE OR DELETE ON ai.chat_tasks
|
|
@@ -2642,6 +2668,8 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
2642
2668
|
);
|
|
2643
2669
|
}
|
|
2644
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();
|
|
2645
2673
|
console.log(" ✓ ai conversation tables");
|
|
2646
2674
|
};
|
|
2647
2675
|
|