@k2b/cloud 0.39.0 → 0.40.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/README.md +8 -0
- package/package.json +8 -4
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +36 -15
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +2 -2
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/background-review.ts +72 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +10 -160
- package/src/ai/chat/view-block.tsx +15 -5
- package/src/ai/chat-task-contracts.ts +4 -1
- package/src/ai/chat-task-reasons-migrate.ts +7 -0
- package/src/ai/chat-task-reasons.ts +1 -1
- package/src/ai/chat-tasks.ts +55 -25
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +2 -0
- package/src/ai/migrate.ts +11 -2
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +145 -1
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +227 -24
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +24 -2
- package/src/ai/types.ts +28 -3
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +2 -0
- package/src/api/app-approval.ts +46 -5
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +2 -0
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +6 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/app-approval.ts +54 -5
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +45 -3
- package/src/browser/composer-insert.ts +10 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +11 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +412 -10
- package/src/browser/panel.ts +37 -0
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +120 -13
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/seen-hints.ts +35 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +7 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +11 -0
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/webauthn.ts +12 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +10 -2
- package/src/ssr/Layout.tsx +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +9 -1
- package/src/ssr/PanelRuntime.island.tsx +7 -0
- package/src/ssr/PanelSlot.tsx +53 -3
- package/src/ssr/ShellRuntime.island.tsx +11 -2
- package/src/ssr/app-color.ts +14 -9
- package/src/ssr/app-navigation.ts +16 -3
- package/src/ssr/compact-shell.browser-harness.ts +30 -22
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +2 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +53 -4
- package/src/ssr/platform-messages.ts +18 -0
- package/src/ssr/shell-data.tsx +2 -1
- package/src/styles/effects.css +198 -9
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +49 -12
- package/src/styles/panel.css +47 -4
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +17 -11
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +17 -22
package/src/ai/executor.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AssistantMessage, CompactEvent, LoopAggregate, Message, NessiLoop, OutboundEvent } from "@k2b/nessi";
|
|
2
2
|
import { compact, nessi } from "@k2b/nessi";
|
|
3
|
+
import { capabilityReviewRememberScope } from "../_internal/capability-reviews";
|
|
3
4
|
import { listCapabilities } from "../_internal/registry";
|
|
4
|
-
import type {
|
|
5
|
+
import type { CapabilityActionReviewData } from "../contracts/capabilities";
|
|
6
|
+
import { capabilityActionPolicy } from "../contracts/capabilities";
|
|
5
7
|
import type { AccessSubject, RequestActor } from "../server";
|
|
6
8
|
import { approvalAssurance, approvalScopeFromToolName, approvals } from "../services/approvals";
|
|
7
9
|
import { createHelpReader } from "../services/help";
|
|
@@ -10,6 +12,7 @@ import { getMandate } from "../services/mandates";
|
|
|
10
12
|
import type { MandatePolicyV1 } from "../services/mandates/policy";
|
|
11
13
|
import { coreSettings } from "../services/settings/api";
|
|
12
14
|
import { normalizeLocale } from "../shared/locale";
|
|
15
|
+
import { type AiActionCheck, type AiStoredActionReview, storedActionReview } from "./action-review";
|
|
13
16
|
import {
|
|
14
17
|
AI_WEBSITE_APPROVAL_TOOL,
|
|
15
18
|
type AiToolApprovalContext,
|
|
@@ -19,20 +22,23 @@ import {
|
|
|
19
22
|
hasRememberedAiToolApproval,
|
|
20
23
|
} from "./approvals";
|
|
21
24
|
import { aiChatAccessSubject, isAssistantChatTurn, resolveAssistantAudioModel } from "./assistant-models";
|
|
25
|
+
import { authorizeBackgroundActionWith } from "./background-review";
|
|
22
26
|
import { CODE_RUNTIME_TOOL_NAMES } from "./browser-code-contracts";
|
|
23
|
-
import { type AiCapabilityCatalogEntry, createAiToolResolver, createRunToolStore } from "./capabilities";
|
|
24
|
-
import {
|
|
27
|
+
import { type AiCapabilityCatalogEntry, buildAiCapabilityCatalog, createAiToolResolver, createRunToolStore } from "./capabilities";
|
|
28
|
+
import {
|
|
29
|
+
AiCapabilityExecutionError,
|
|
30
|
+
aiCapabilityCheckResult,
|
|
31
|
+
checkAiCapability,
|
|
32
|
+
executeAiCapability,
|
|
33
|
+
resolveAiCapabilityActor,
|
|
34
|
+
reviewAiCapability,
|
|
35
|
+
} from "./capability-execution";
|
|
25
36
|
import { aiChatMessages } from "./chat/messages";
|
|
26
37
|
import { isFailedTool } from "./chat/tool-groups";
|
|
27
38
|
import { aiTurnErrorText } from "./chat/turn-error";
|
|
28
39
|
import { AiChatTaskAuthorityError, aiChatTasks } from "./chat-tasks";
|
|
29
40
|
import { createCloudCompactFn } from "./compaction";
|
|
30
|
-
import {
|
|
31
|
-
createCloudAiCodeTools,
|
|
32
|
-
createCloudAiLocalBashTool,
|
|
33
|
-
createCloudAiLocalTools,
|
|
34
|
-
createConfiguredDefaultCloudAiTools,
|
|
35
|
-
} from "./default-tools";
|
|
41
|
+
import { createAiTurnCodeAndClientTools, createConfiguredDefaultCloudAiTools } from "./default-tools";
|
|
36
42
|
import { aiFileStore } from "./files-store";
|
|
37
43
|
import { AI_LOCAL_TOOL_NAMES } from "./local-tool-contracts";
|
|
38
44
|
import {
|
|
@@ -72,7 +78,15 @@ import { isAiVisionModelConfigured, type resolveAiModel } from "./settings";
|
|
|
72
78
|
import { mergeAiSkillCatalog, selectAiSkillCatalog } from "./skill-catalog";
|
|
73
79
|
import { createCloudAiLoadSkillTool, createCloudAiSearchSkillsTool, loadSelectedAiSkills } from "./skill-tool";
|
|
74
80
|
import { aiSkills } from "./skills";
|
|
75
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
AI_TOOL_CHECK_STALE_MS,
|
|
83
|
+
aiConversations,
|
|
84
|
+
claimAiToolCheck,
|
|
85
|
+
readAiToolCheck,
|
|
86
|
+
recordAiToolReview,
|
|
87
|
+
recordAiWebsiteReceipts,
|
|
88
|
+
settleAiToolCheck,
|
|
89
|
+
} from "./store";
|
|
76
90
|
import { AI_LIVE_SNAPSHOT_INTERVAL_MS, publishAiWireEvent } from "./stream";
|
|
77
91
|
import { composeAiSystemPrompt } from "./system-prompt";
|
|
78
92
|
import { aiToolAudit } from "./tool-audit";
|
|
@@ -427,9 +441,9 @@ type ToolBlockPatch = Partial<Extract<AiTurnBlock, { kind: "tool" }>> & { name?:
|
|
|
427
441
|
const customApprovalParentCallId = (callId: string): string | undefined => /^(.*)-approval-\d+$/.exec(callId)?.[1];
|
|
428
442
|
|
|
429
443
|
const approvalReviewForCallId = (
|
|
430
|
-
reviews: ReadonlyMap<string,
|
|
444
|
+
reviews: ReadonlyMap<string, CapabilityActionReviewData>,
|
|
431
445
|
callId: string,
|
|
432
|
-
):
|
|
446
|
+
): CapabilityActionReviewData | undefined => {
|
|
433
447
|
const direct = reviews.get(callId);
|
|
434
448
|
if (direct) return direct;
|
|
435
449
|
const parentCallId = customApprovalParentCallId(callId);
|
|
@@ -479,9 +493,11 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
479
493
|
const setCanonicalNames = (items: Map<string, string>) => {
|
|
480
494
|
canonicalNames = items;
|
|
481
495
|
};
|
|
482
|
-
let approvalReviews = new Map<string,
|
|
483
|
-
|
|
496
|
+
let approvalReviews = new Map<string, CapabilityActionReviewData>();
|
|
497
|
+
let capabilityScopes: ReadonlyMap<string, string> = new Map();
|
|
498
|
+
const setApprovalReviews = (items: Map<string, CapabilityActionReviewData>, scopes: ReadonlyMap<string, string> = new Map()) => {
|
|
484
499
|
approvalReviews = items;
|
|
500
|
+
capabilityScopes = scopes;
|
|
485
501
|
};
|
|
486
502
|
let approvalPolicies: PreparedAiTools["approvalPolicies"] = new Map();
|
|
487
503
|
const setApprovalPolicies = (items: PreparedAiTools["approvalPolicies"]) => {
|
|
@@ -505,7 +521,7 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
505
521
|
const streamKinds = new Map<string, "text" | "thinking">();
|
|
506
522
|
|
|
507
523
|
const setTool = (callId: string, patch: ToolBlockPatch, displayCallId = callId): BlockOp => {
|
|
508
|
-
const existing = toolBlocks.get(callId);
|
|
524
|
+
const existing = toolBlocks.get(callId) ?? toolBlocks.get(displayCallId);
|
|
509
525
|
const rawName = patch.name ?? existing?.name ?? "tool";
|
|
510
526
|
const name = canonicalNames.get(rawName) ?? rawName;
|
|
511
527
|
const approval = patch.clearApproval ? undefined : "approval" in patch ? patch.approval : existing?.approval;
|
|
@@ -520,6 +536,8 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
520
536
|
result: "result" in patch ? patch.result : existing?.result,
|
|
521
537
|
isError: "isError" in patch ? patch.isError : existing?.isError,
|
|
522
538
|
approval: approval && !allowRememberedApprovals ? { ...approval, allowAlways: false, allowChat: false } : approval,
|
|
539
|
+
review: patch.review ?? existing?.review,
|
|
540
|
+
check: patch.check ?? existing?.check,
|
|
523
541
|
frontendMode: patch.frontendMode ?? existing?.frontendMode,
|
|
524
542
|
presentation: patch.presentation ?? existing?.presentation ?? presentations.get(rawName) ?? presentations.get(name),
|
|
525
543
|
...(approvedCallIds.has(callId) || approvedCallIds.has(displayCallId) || existing?.approved ? { approved: true } : {}),
|
|
@@ -572,7 +590,8 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
572
590
|
const target = event.kind === "custom_approval" ? approvalTargets.get(event.callId) : undefined;
|
|
573
591
|
const allowAlways = target
|
|
574
592
|
? target.always
|
|
575
|
-
:
|
|
593
|
+
: capabilityScopes.has(customApprovalParentCallId(event.callId) ?? event.callId) ||
|
|
594
|
+
aiToolAllowsAlways(approvalPolicies.get(event.name));
|
|
576
595
|
return [
|
|
577
596
|
setTool(
|
|
578
597
|
event.callId,
|
|
@@ -651,6 +670,7 @@ const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowReme
|
|
|
651
670
|
|
|
652
671
|
return {
|
|
653
672
|
translate,
|
|
673
|
+
setTool,
|
|
654
674
|
storedMessage,
|
|
655
675
|
compaction,
|
|
656
676
|
setFrontendModes,
|
|
@@ -690,10 +710,7 @@ const materializeChatConfig = async (config: AiChatTurnRunConfig, signal: AbortS
|
|
|
690
710
|
accessSubject: aiChatAccessSubject(config.actor),
|
|
691
711
|
allowedDataBoundaries: config.modelPolicy?.allowedDataBoundaries,
|
|
692
712
|
})),
|
|
693
|
-
|
|
694
|
-
...[createCloudAiLocalBashTool(), ...createCloudAiLocalTools(), ...createCloudAiCodeTools()].filter(
|
|
695
|
-
(tool) => tool.location === "server" || config.clientToolIds?.some((name) => name === tool.def.name),
|
|
696
|
-
),
|
|
713
|
+
...createAiTurnCodeAndClientTools(config),
|
|
697
714
|
]
|
|
698
715
|
: [],
|
|
699
716
|
toolApprovalContext: config.toolApprovalContext,
|
|
@@ -997,11 +1014,7 @@ export class AiTurnExecutor {
|
|
|
997
1014
|
];
|
|
998
1015
|
const toolsSupported = resolved.profile.capabilities.includes("tools");
|
|
999
1016
|
const allowed = allowedTools === null ? null : new Set(allowedTools);
|
|
1000
|
-
const activeTools = toolsSupported
|
|
1001
|
-
? runtimeTools.filter(
|
|
1002
|
-
(tool) => (!allowed || allowed.has(tool.def.name)) && !(config.mandate && ["code_open", "code_secret"].includes(tool.def.name)),
|
|
1003
|
-
)
|
|
1004
|
-
: [];
|
|
1017
|
+
const activeTools = toolsSupported ? runtimeTools.filter((tool) => !allowed || allowed.has(tool.def.name)) : [];
|
|
1005
1018
|
let audioUnavailableInstruction: string | undefined;
|
|
1006
1019
|
if (
|
|
1007
1020
|
defaultToolSource &&
|
|
@@ -1092,7 +1105,23 @@ export class AiTurnExecutor {
|
|
|
1092
1105
|
});
|
|
1093
1106
|
let backgroundError: string | null = null;
|
|
1094
1107
|
const rememberableCapabilityApprovals = new Map<string, string>();
|
|
1095
|
-
const capabilityActionReviews = new Map<string,
|
|
1108
|
+
const capabilityActionReviews = new Map<string, CapabilityActionReviewData>();
|
|
1109
|
+
const storedReviews = new Map<string, AiStoredActionReview>();
|
|
1110
|
+
const onCapabilityReview = async (
|
|
1111
|
+
callId: string,
|
|
1112
|
+
review: CapabilityActionReviewData,
|
|
1113
|
+
approval: "none" | "rememberable" | undefined,
|
|
1114
|
+
entry: AiCapabilityCatalogEntry,
|
|
1115
|
+
) => {
|
|
1116
|
+
capabilityActionReviews.set(callId, review);
|
|
1117
|
+
const scope = capabilityReviewRememberScope(review, approval);
|
|
1118
|
+
if (scope) rememberableCapabilityApprovals.set(callId, scope);
|
|
1119
|
+
else rememberableCapabilityApprovals.delete(callId);
|
|
1120
|
+
const stored = storedActionReview(review, capabilityActionPolicy(entry.app.manifest, entry.operation.localId));
|
|
1121
|
+
await recordAiToolReview({ conversationId, turnId, callId, review: stored });
|
|
1122
|
+
storedReviews.set(callId, stored);
|
|
1123
|
+
await pipeline.setToolReview(callId, stored);
|
|
1124
|
+
};
|
|
1096
1125
|
pipeline.setApprovalTargets(approvalTargets);
|
|
1097
1126
|
pipeline.setFrontendModes(prepared.frontendModes);
|
|
1098
1127
|
pipeline.setCanonicalNames(prepared.canonicalNames);
|
|
@@ -1101,7 +1130,7 @@ export class AiTurnExecutor {
|
|
|
1101
1130
|
const rejectedToolCallIds = new Set<string>();
|
|
1102
1131
|
const approvedToolCallIds = new Set<string>();
|
|
1103
1132
|
pipeline.setPresentations(toolPresentations);
|
|
1104
|
-
pipeline.setApprovalReviews(capabilityActionReviews);
|
|
1133
|
+
pipeline.setApprovalReviews(capabilityActionReviews, rememberableCapabilityApprovals);
|
|
1105
1134
|
let turnInput = config.input;
|
|
1106
1135
|
try {
|
|
1107
1136
|
if (resolved.profile.capabilities.includes("vision") && config.files?.attached.some((file) => isAiImageMediaType(file.mediaType))) {
|
|
@@ -1192,7 +1221,7 @@ export class AiTurnExecutor {
|
|
|
1192
1221
|
);
|
|
1193
1222
|
return;
|
|
1194
1223
|
}
|
|
1195
|
-
const
|
|
1224
|
+
const authorizeBackgroundMandate =
|
|
1196
1225
|
config.background && config.mandate
|
|
1197
1226
|
? async (entry: AiCapabilityCatalogEntry, args: unknown) => {
|
|
1198
1227
|
try {
|
|
@@ -1202,6 +1231,7 @@ export class AiTurnExecutor {
|
|
|
1202
1231
|
capabilityId: entry.operation.localId,
|
|
1203
1232
|
kind: entry.kind,
|
|
1204
1233
|
input: args,
|
|
1234
|
+
policy: capabilityActionPolicy(entry.app.manifest, entry.operation.localId),
|
|
1205
1235
|
approval: entry.kind === "action" && "approval" in entry.operation ? entry.operation.approval : undefined,
|
|
1206
1236
|
});
|
|
1207
1237
|
} catch (error) {
|
|
@@ -1245,16 +1275,51 @@ export class AiTurnExecutor {
|
|
|
1245
1275
|
}),
|
|
1246
1276
|
}
|
|
1247
1277
|
: {}),
|
|
1248
|
-
authorizeBackground
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1278
|
+
authorizeBackground: authorizeBackgroundMandate
|
|
1279
|
+
? async (entry, args, context) => {
|
|
1280
|
+
const review = await authorizeBackgroundActionWith(
|
|
1281
|
+
{
|
|
1282
|
+
turnId,
|
|
1283
|
+
mandate: config.mandate!,
|
|
1284
|
+
appId: entry.appId,
|
|
1285
|
+
capabilityId: entry.operation.localId,
|
|
1286
|
+
kind: entry.kind,
|
|
1287
|
+
args,
|
|
1288
|
+
policy: capabilityActionPolicy(entry.app.manifest, entry.operation.localId),
|
|
1289
|
+
approval: entry.kind === "action" && "approval" in entry.operation ? entry.operation.approval : undefined,
|
|
1290
|
+
hasReview: entry.kind === "action" && "review" in entry.operation && entry.operation.review === true,
|
|
1291
|
+
action: entry.display.title,
|
|
1292
|
+
review: () =>
|
|
1293
|
+
capabilityAuthority
|
|
1294
|
+
? reviewAiCapability({
|
|
1295
|
+
conversationId,
|
|
1296
|
+
authority: capabilityAuthority,
|
|
1297
|
+
mandate: config.mandate,
|
|
1298
|
+
locale: promptLocale,
|
|
1299
|
+
entry,
|
|
1300
|
+
args,
|
|
1301
|
+
context,
|
|
1302
|
+
})
|
|
1303
|
+
: Promise.resolve(null),
|
|
1304
|
+
signal: context.signal,
|
|
1305
|
+
},
|
|
1306
|
+
() => authorizeBackgroundMandate(entry, args),
|
|
1307
|
+
);
|
|
1308
|
+
if (review && context.callId)
|
|
1309
|
+
await onCapabilityReview(
|
|
1310
|
+
context.callId,
|
|
1311
|
+
review,
|
|
1312
|
+
"approval" in entry.operation ? entry.operation.approval : undefined,
|
|
1313
|
+
entry,
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
: undefined,
|
|
1317
|
+
onReview: onCapabilityReview,
|
|
1253
1318
|
...(capabilityAuthority
|
|
1254
1319
|
? {
|
|
1255
1320
|
execute: async (entry, args, context) => {
|
|
1256
1321
|
try {
|
|
1257
|
-
await
|
|
1322
|
+
await authorizeBackgroundMandate?.(entry, args);
|
|
1258
1323
|
const result = await executeAiCapability({
|
|
1259
1324
|
conversationId,
|
|
1260
1325
|
turnId,
|
|
@@ -1268,9 +1333,78 @@ export class AiTurnExecutor {
|
|
|
1268
1333
|
entry,
|
|
1269
1334
|
args,
|
|
1270
1335
|
context,
|
|
1336
|
+
}).catch(async (error: unknown) => {
|
|
1337
|
+
const review = context.callId ? storedReviews.get(context.callId) : undefined;
|
|
1338
|
+
if (entry.kind !== "action" || !(error instanceof AiCapabilityExecutionError)) throw error;
|
|
1339
|
+
// Only an answer from the app about the request itself (a 4xx) proves that nothing happened. A 5xx,
|
|
1340
|
+
// a timeout, or a cancellation leaves the outcome open only once Cloud sent the request to the app;
|
|
1341
|
+
// before that, nothing ran and the error stays as it is. The idempotency codes speak of an earlier
|
|
1342
|
+
// attempt with the same key, so they leave it open even when this one sent nothing.
|
|
1343
|
+
const uncertain =
|
|
1344
|
+
["ACTION_OUTCOME_UNKNOWN", "IDEMPOTENCY_UNCERTAIN", "IDEMPOTENCY_IN_PROGRESS"].includes(error.code) ||
|
|
1345
|
+
(error.dispatched && (error.status >= 500 || error.status === 499));
|
|
1346
|
+
if (!uncertain) throw error;
|
|
1347
|
+
const unknown =
|
|
1348
|
+
error.code === "ACTION_OUTCOME_UNKNOWN"
|
|
1349
|
+
? error
|
|
1350
|
+
: new AiCapabilityExecutionError(
|
|
1351
|
+
"ACTION_OUTCOME_UNKNOWN",
|
|
1352
|
+
error.status,
|
|
1353
|
+
`The outcome is unknown after ${error.code}; do not retry automatically.`,
|
|
1354
|
+
error.dispatched,
|
|
1355
|
+
);
|
|
1356
|
+
if (!review || !("sentence" in review) || !review.check || !context.callId) throw unknown;
|
|
1357
|
+
let check: AiActionCheck;
|
|
1358
|
+
try {
|
|
1359
|
+
// Durable first: the claim stores `checking` before the Query runs. If this worker stops, the
|
|
1360
|
+
// claim goes stale and the person can check again; while it holds, no re-check runs beside it.
|
|
1361
|
+
const target = { conversationId, turnId, callId: context.callId };
|
|
1362
|
+
const claimedAt = new Date().toISOString();
|
|
1363
|
+
const claimed = await claimAiToolCheck({
|
|
1364
|
+
...target,
|
|
1365
|
+
at: claimedAt,
|
|
1366
|
+
staleBefore: new Date(Date.now() - AI_TOOL_CHECK_STALE_MS).toISOString(),
|
|
1367
|
+
first: true,
|
|
1368
|
+
});
|
|
1369
|
+
if (!claimed) {
|
|
1370
|
+
// A person's check holds or settled the call already; its stored result is the outcome.
|
|
1371
|
+
const stored = await readAiToolCheck(target);
|
|
1372
|
+
const known = stored && stored.state !== "checking" ? stored : { state: "pending" as const };
|
|
1373
|
+
check = known;
|
|
1374
|
+
} else {
|
|
1375
|
+
await pipeline.setToolCheck(context.callId, { state: "checking" });
|
|
1376
|
+
const answered = await checkAiCapability({
|
|
1377
|
+
conversationId,
|
|
1378
|
+
turnId,
|
|
1379
|
+
authority: capabilityAuthority!,
|
|
1380
|
+
mandate: config.mandate,
|
|
1381
|
+
// The receipt joins the review's sentence, so it is asked for in the review's language.
|
|
1382
|
+
locale: review.locale,
|
|
1383
|
+
entry,
|
|
1384
|
+
check: review.check,
|
|
1385
|
+
catalog: buildAiCapabilityCatalog([entry.app], promptLocale),
|
|
1386
|
+
context,
|
|
1387
|
+
});
|
|
1388
|
+
// A claim that went stale and was taken over loses to the newer check's stored result.
|
|
1389
|
+
check = (await settleAiToolCheck({ ...target, claimedAt, check: answered }))
|
|
1390
|
+
? answered
|
|
1391
|
+
: ((await readAiToolCheck(target)) ?? answered);
|
|
1392
|
+
}
|
|
1393
|
+
await pipeline.setToolCheck(context.callId, check);
|
|
1394
|
+
} catch (recoveryError) {
|
|
1395
|
+
log.warn("AI Action outcome recovery failed", {
|
|
1396
|
+
code: "action_outcome_recovery_failed",
|
|
1397
|
+
turnId,
|
|
1398
|
+
callId: context.callId,
|
|
1399
|
+
error: recoveryError instanceof Error ? recoveryError.message : String(recoveryError),
|
|
1400
|
+
});
|
|
1401
|
+
throw unknown;
|
|
1402
|
+
}
|
|
1403
|
+
return aiCapabilityCheckResult(check);
|
|
1271
1404
|
});
|
|
1272
1405
|
const resources = collectConversationResourceObservations(args, result);
|
|
1273
1406
|
if (resources.length) {
|
|
1407
|
+
// Bookkeeping after a call that ran never turns its outcome into a failure: the effect happened.
|
|
1274
1408
|
await Promise.all([
|
|
1275
1409
|
indexConversationResources({ conversationId, turnId, callId: context.callId, resources }),
|
|
1276
1410
|
recordMemoryWorkflowEvidence({
|
|
@@ -1280,7 +1414,14 @@ export class AiTurnExecutor {
|
|
|
1280
1414
|
capabilityId: entry.name,
|
|
1281
1415
|
resources,
|
|
1282
1416
|
}),
|
|
1283
|
-
])
|
|
1417
|
+
]).catch((error: unknown) =>
|
|
1418
|
+
log.warn("AI capability bookkeeping failed after the call ran", {
|
|
1419
|
+
code: "capability_bookkeeping_failed",
|
|
1420
|
+
turnId,
|
|
1421
|
+
callId: context.callId,
|
|
1422
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1423
|
+
}),
|
|
1424
|
+
);
|
|
1284
1425
|
}
|
|
1285
1426
|
return result;
|
|
1286
1427
|
} catch (error) {
|
|
@@ -1593,7 +1734,7 @@ export class AiTurnExecutor {
|
|
|
1593
1734
|
approvalContext?: AiToolApprovalContext;
|
|
1594
1735
|
allowRememberedApprovals: boolean;
|
|
1595
1736
|
rememberableCapabilityApprovals: ReadonlyMap<string, string>;
|
|
1596
|
-
capabilityActionReviews: ReadonlyMap<string,
|
|
1737
|
+
capabilityActionReviews: ReadonlyMap<string, CapabilityActionReviewData>;
|
|
1597
1738
|
approvalTargets: ReadonlyMap<string, AiApprovalTarget>;
|
|
1598
1739
|
linkedApprovals: ReadonlyMap<string, string>;
|
|
1599
1740
|
freshApprovalRequired?: ReadonlySet<string>;
|
|
@@ -1778,7 +1919,7 @@ export class AiTurnExecutor {
|
|
|
1778
1919
|
approvalContext?: AiToolApprovalContext;
|
|
1779
1920
|
allowRememberedApprovals: boolean;
|
|
1780
1921
|
rememberableCapabilityApprovals: ReadonlyMap<string, string>;
|
|
1781
|
-
capabilityActionReviews: ReadonlyMap<string,
|
|
1922
|
+
capabilityActionReviews: ReadonlyMap<string, CapabilityActionReviewData>;
|
|
1782
1923
|
approvalTargets: ReadonlyMap<string, AiApprovalTarget>;
|
|
1783
1924
|
linkedApprovals: ReadonlyMap<string, string>;
|
|
1784
1925
|
freshApprovalRequired?: ReadonlySet<string>;
|
|
@@ -2207,8 +2348,8 @@ class StreamPipeline {
|
|
|
2207
2348
|
this.mapper.setCanonicalNames(names);
|
|
2208
2349
|
}
|
|
2209
2350
|
|
|
2210
|
-
setApprovalReviews(reviews: Map<string,
|
|
2211
|
-
this.mapper.setApprovalReviews(reviews);
|
|
2351
|
+
setApprovalReviews(reviews: Map<string, CapabilityActionReviewData>, scopes: ReadonlyMap<string, string>): void {
|
|
2352
|
+
this.mapper.setApprovalReviews(reviews, scopes);
|
|
2212
2353
|
}
|
|
2213
2354
|
|
|
2214
2355
|
setApprovalPolicies(policies: PreparedAiTools["approvalPolicies"]): void {
|
|
@@ -2290,6 +2431,16 @@ class StreamPipeline {
|
|
|
2290
2431
|
await this.maybeSnapshot();
|
|
2291
2432
|
}
|
|
2292
2433
|
|
|
2434
|
+
async setToolReview(callId: string, review: AiStoredActionReview): Promise<void> {
|
|
2435
|
+
await this.emitOp(this.mapper.setTool(callId, { review }));
|
|
2436
|
+
await this.maybeSnapshot();
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
async setToolCheck(callId: string, check: AiActionCheck): Promise<void> {
|
|
2440
|
+
await this.emitOp(this.mapper.setTool(callId, { check, status: "running" }));
|
|
2441
|
+
await this.maybeSnapshot();
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2293
2444
|
async reportToolProgress(callId: string, progress: string): Promise<void> {
|
|
2294
2445
|
const block = this.blocks.find((block) => block.kind === "tool" && block.callId === callId);
|
|
2295
2446
|
if (!block || block.kind !== "tool" || block.progress === progress) return;
|
package/src/ai/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export {
|
|
|
22
22
|
export { assistantAiSettingsState, listAssistantAiModels, selectAssistantAiModelId } from "./assistant-models";
|
|
23
23
|
export { type AiAttachmentRef, aiAttachmentMarker, formatAiFileSize, parseAiAttachmentMarkers } from "./attachments";
|
|
24
24
|
export { CloudAiTranscribeAudioInputSchema, CloudAiTranscribeAudioOutputSchema, createCloudAiTranscribeAudioTool } from "./audio-tool";
|
|
25
|
+
export { AiBackgroundApprovalRequiredError, authorizeBackgroundAction } from "./background-review";
|
|
25
26
|
export { CODE_RUNTIME_TOOL_NAMES, CodeResourceId, CodeRuntimeInput, parseCodeToolInput } from "./browser-code-contracts";
|
|
26
27
|
export { aiCapabilityId, aiCapabilityToolName } from "./capabilities";
|
|
27
28
|
export { getAiChatQuotas } from "./chat-quotas";
|
|
@@ -419,6 +420,7 @@ export type {
|
|
|
419
420
|
AiProjectPromptSnapshot,
|
|
420
421
|
AiProviderId,
|
|
421
422
|
AiPublicModelProfile,
|
|
423
|
+
AiRecentResource,
|
|
422
424
|
AiResolvedModel,
|
|
423
425
|
AiRuntimeTool,
|
|
424
426
|
AiSettingsError,
|
package/src/ai/migrate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sql } from "bun";
|
|
2
|
-
import {
|
|
2
|
+
import { CapabilityActionReviewDataSchema } from "../contracts/capabilities";
|
|
3
3
|
import { APPROVAL_MAX_WAIT_MS, approvalAssurance, approvalScopeFromToolName, approvals, migrateApprovals } from "../services/approvals";
|
|
4
4
|
import { migrateChatTaskReasons } from "./chat-task-reasons-migrate";
|
|
5
5
|
import { migrateAiMessageQueue } from "./message-queue";
|
|
@@ -404,6 +404,15 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
404
404
|
ON ai.messages(conversation_id, loop_id, seq ASC)
|
|
405
405
|
WHERE loop_id IS NOT NULL
|
|
406
406
|
`.simple();
|
|
407
|
+
// "Recent" in the slash menu reads the newest messages a person sent in each of their latest chats. Without it the
|
|
408
|
+
// read walks every Assistant and tool row of those chats. The predicate matches the store query word for word.
|
|
409
|
+
await sql`
|
|
410
|
+
CREATE INDEX IF NOT EXISTS idx_ai_messages_sent_by_person
|
|
411
|
+
ON ai.messages(conversation_id, seq DESC) INCLUDE (created_at, id)
|
|
412
|
+
WHERE role = 'user' AND kind = 'message'
|
|
413
|
+
AND NOT (COALESCE(meta, '{}'::jsonb) ? 'scheduledTask')
|
|
414
|
+
AND NOT (COALESCE(meta, '{}'::jsonb) ? 'agentMessage')
|
|
415
|
+
`.simple();
|
|
407
416
|
|
|
408
417
|
// Optional pg_textsearch ranking. Native weighted FTS above is always the
|
|
409
418
|
// correctness path; missing extension support must never block startup.
|
|
@@ -2667,7 +2676,7 @@ const backfillApprovalRequests = async (): Promise<void> => {
|
|
|
2667
2676
|
WHERE r.requester_kind='assistant_turn' AND r.requester_ref=a.turn_id::text || '/' || a.call_id)
|
|
2668
2677
|
ORDER BY a.created_at, a.id LIMIT 200 FOR UPDATE OF a SKIP LOCKED`;
|
|
2669
2678
|
for (const row of rows) {
|
|
2670
|
-
const review =
|
|
2679
|
+
const review = CapabilityActionReviewDataSchema.safeParse(row.review == null ? null : decoded(row.review));
|
|
2671
2680
|
// request computes both canonical digests; preserve the original wait deadline, including expiry.
|
|
2672
2681
|
const record = await approvals.request(
|
|
2673
2682
|
{
|
|
@@ -4,6 +4,7 @@ import { getCapability } from "../_internal/registry";
|
|
|
4
4
|
import { CloudResourceRefSchema, cloudResourceRefAppId, resolveCapabilityResourceReader } from "../contracts/capabilities";
|
|
5
5
|
import { AuthenticatedPrincipalSchema } from "../contracts/shared";
|
|
6
6
|
import { type AuthContext, auth, err, fail, getLocale, ok, rateLimit, respond, v } from "../server";
|
|
7
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
7
8
|
import { ImageMetadataError } from "../services/image-metadata";
|
|
8
9
|
import { aiChatMessages } from "./chat/messages";
|
|
9
10
|
import { decodeAiFileContent } from "./files-store";
|
|
@@ -71,6 +72,8 @@ type AiProjectsRouteDependencies = {
|
|
|
71
72
|
|
|
72
73
|
const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =>
|
|
73
74
|
new Hono<AuthContext>()
|
|
75
|
+
// The list combines projects from separate grants; the writer of one can rename it next to another project's instructions.
|
|
76
|
+
.use("*", noTransform())
|
|
74
77
|
.use(dependencies.limit ?? rateLimit())
|
|
75
78
|
.use("*", dependencies.authenticate ?? auth.requireRole("authenticated"))
|
|
76
79
|
.get("/", async (c) => respond(c, ok({ projects: (await aiProjects.list(c.get("accessSubject") ?? null)).map(publicProject) })))
|
package/src/ai/protocol.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CompactResult, InboundEvent, Message } from "@k2b/nessi";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CapabilityActionReviewData } from "../contracts/capabilities";
|
|
3
|
+
import type { AiActionCheck, AiStoredActionReview } from "./action-review";
|
|
3
4
|
import type { AiConversation, AiFrontendToolMode, AiStoredMessage, AiToolPresentation, AiTurnStatus } from "./types";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -48,7 +49,7 @@ export type AiTurnBlock =
|
|
|
48
49
|
/** Present while status is awaiting_approval. */
|
|
49
50
|
approval?: {
|
|
50
51
|
message?: string;
|
|
51
|
-
review?:
|
|
52
|
+
review?: CapabilityActionReviewData;
|
|
52
53
|
allowAlways: boolean;
|
|
53
54
|
/** Whether the approval can be remembered for this chat. */
|
|
54
55
|
allowChat?: boolean;
|
|
@@ -66,6 +67,13 @@ export type AiTurnBlock =
|
|
|
66
67
|
presentation?: AiToolPresentation;
|
|
67
68
|
/** Requests a website approval let through without asking, in order; shown whatever the call's outcome. */
|
|
68
69
|
receipts?: { method: string; url: string }[];
|
|
70
|
+
/**
|
|
71
|
+
* The call's review as Cloud stored it when the app reviewed it. It stays after the decision, so the question,
|
|
72
|
+
* its receipt, and a reload all read the same sentence.
|
|
73
|
+
*/
|
|
74
|
+
review?: AiStoredActionReview;
|
|
75
|
+
/** The app's `check` after an outcome Cloud could not see. */
|
|
76
|
+
check?: AiActionCheck;
|
|
69
77
|
}
|
|
70
78
|
| { id: string; kind: "compaction"; status: "running" | "completed" | "skipped" | "failed"; result?: CompactResult };
|
|
71
79
|
|
|
@@ -248,6 +256,8 @@ export const buildBlocksFromMessages = (
|
|
|
248
256
|
meta?: {
|
|
249
257
|
steerId?: string;
|
|
250
258
|
toolPresentations?: Record<string, AiToolPresentation>;
|
|
259
|
+
toolReviews?: Record<string, AiStoredActionReview>;
|
|
260
|
+
toolChecks?: Record<string, AiActionCheck>;
|
|
251
261
|
toolOutcomes?: Record<string, "rejected" | "approved" | "expired">;
|
|
252
262
|
websiteReceipts?: Record<string, Record<string, { method: string; url: string }>>;
|
|
253
263
|
} | null;
|
|
@@ -275,6 +285,8 @@ export const buildBlocksFromMessages = (
|
|
|
275
285
|
args: block.args,
|
|
276
286
|
status: "running",
|
|
277
287
|
presentation: meta?.toolPresentations?.[block.id],
|
|
288
|
+
review: meta?.toolReviews?.[block.id],
|
|
289
|
+
check: meta?.toolChecks?.[block.id],
|
|
278
290
|
// A turn that ended before an approved call returned records the approval on the call's message, and an
|
|
279
291
|
// approval it left waiting as expired.
|
|
280
292
|
...(outcome === "approved" || outcome === "expired" ? { approved: outcome === "approved" } : {}),
|