@k2b/cloud 0.39.0 → 0.41.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 +11 -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 +217 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +49 -18
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/setup-lock.ts +103 -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 +464 -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 +680 -0
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +456 -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 +198 -39
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/index.ts +3 -0
- package/src/ai/migrate.ts +26 -2
- package/src/ai/open-questions.ts +172 -0
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +195 -5
- 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 +245 -24
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +36 -8
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +43 -4
- 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-actions.ts +31 -7
- 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/schemas.ts +3 -0
- package/src/api/auth.ts +67 -12
- 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 +30 -3
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +159 -36
- 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 +166 -262
- package/src/approvals/approval-card.ts +68 -0
- 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 +123 -16
- 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/contracts/shared.ts +3 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +13 -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/accounts/app.ts +23 -3
- package/src/services/accounts/users.ts +12 -1
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +49 -31
- package/src/services/approvals/index.ts +7 -5
- package/src/services/approvals/list.ts +27 -6
- package/src/services/approvals/question.ts +68 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/auth-flows/password-reset.ts +32 -6
- 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 +13 -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/pdf/markdown.ts +160 -2
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +296 -41
- 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/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 +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +222 -419
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -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/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- 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 +21 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -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-start.ts +33 -7
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-idle.ts +9 -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/approvals.css +6 -0
- package/src/styles/effects.css +213 -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/routes.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { type Context, Hono } from "hono";
|
|
2
2
|
import { bodyLimit } from "hono/body-limit";
|
|
3
|
+
import { describeRoute } from "hono-openapi";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { listCapabilities } from "../_internal/registry";
|
|
5
|
+
import { getApp, listCapabilities } from "../_internal/registry";
|
|
6
|
+
import { QUERY_TIMEOUT_MS } from "../api/capabilities";
|
|
7
|
+
import { ErrorResponseSchema } from "../contracts";
|
|
5
8
|
import { APPROVAL_CLIENT_HEADER } from "../contracts/approvals";
|
|
9
|
+
import { capabilityActionPolicy } from "../contracts/capabilities";
|
|
6
10
|
import {
|
|
7
11
|
type AccessSubject,
|
|
8
12
|
type AuthContext,
|
|
@@ -11,22 +15,30 @@ import {
|
|
|
11
15
|
fail,
|
|
12
16
|
getLocale,
|
|
13
17
|
getTimeZone,
|
|
18
|
+
jsonResponse,
|
|
14
19
|
ok,
|
|
15
20
|
type RequestActor,
|
|
16
21
|
rateLimit,
|
|
22
|
+
requiresAuth,
|
|
17
23
|
respond,
|
|
18
24
|
v,
|
|
19
25
|
} from "../server";
|
|
20
26
|
import { isAppSessionActor } from "../server/actor";
|
|
21
27
|
import { isRequestCredentialCurrent } from "../server/middleware/auth";
|
|
22
|
-
import {
|
|
28
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
29
|
+
import { REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "../server/request-timeout";
|
|
30
|
+
import { approvalDecisionChannel, approvals } from "../services/approvals";
|
|
31
|
+
import { pendingApprovalReview } from "../services/approvals/question";
|
|
23
32
|
import { ImageMetadataError } from "../services/image-metadata";
|
|
24
33
|
import { logger } from "../services/logging";
|
|
25
34
|
import { coreSettings } from "../services/settings/api";
|
|
35
|
+
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
26
36
|
import { readThemeFromCookieHeader } from "../shared/theme";
|
|
37
|
+
import { type AiStoredActionReview, isLegacyActionReview, isUnknownActionOutcome, storedActionReview } from "./action-review";
|
|
27
38
|
import { assistantAiSettingsState, listAssistantAiModels, resolveAssistantAudioModel, selectAssistantAiModelId } from "./assistant-models";
|
|
28
39
|
import { AI_AUDIO_MAX_BYTES } from "./audio-format";
|
|
29
40
|
import { buildAiCapabilityCatalog } from "./capabilities";
|
|
41
|
+
import { checkAiCapability, resolveAiCapabilityActor } from "./capability-execution";
|
|
30
42
|
import { aiChatMessages } from "./chat/messages";
|
|
31
43
|
import { getAiChatQuotas } from "./chat-quotas";
|
|
32
44
|
import { aiChatTasks } from "./chat-tasks";
|
|
@@ -67,6 +79,8 @@ import {
|
|
|
67
79
|
import { aiActorUser, aiPrefsUserId, aiUserPrefs } from "./prefs";
|
|
68
80
|
import { aiProjects } from "./projects";
|
|
69
81
|
import { projectPublicAiStoredMessages, publicAiStoredMessages } from "./public-projection";
|
|
82
|
+
import { AiRecentResourceSchema, listAiRecentResources } from "./recent-resources";
|
|
83
|
+
import { RECENT_SENT_REF_LIMIT } from "./recent-sent-refs";
|
|
70
84
|
import { type AiChatRequestContext, resolveAiChatRequestContext as resolveContext } from "./request-context";
|
|
71
85
|
import { aiResourceMarker } from "./resource-markers";
|
|
72
86
|
import { isConversationResourceCursor } from "./resource-refs";
|
|
@@ -85,9 +99,10 @@ import { AI_SHORT_ID_PATTERN } from "./short-id";
|
|
|
85
99
|
import { selectAiSkillCatalog } from "./skill-catalog";
|
|
86
100
|
import { createCloudAiLoadSkillTool, createCloudAiSearchSkillsTool } from "./skill-tool";
|
|
87
101
|
import { aiSkills } from "./skills";
|
|
88
|
-
import { aiConversations } from "./store";
|
|
102
|
+
import { AI_TOOL_CHECK_STALE_MS, aiConversations, claimAiToolCheck, readAiToolCheck, settleAiToolCheck } from "./store";
|
|
89
103
|
import { createAiConversationStreamResponse, loadAiStreamState } from "./stream";
|
|
90
104
|
import { composeAiSystemPrompt } from "./system-prompt";
|
|
105
|
+
import { aiToolAudit } from "./tool-audit";
|
|
91
106
|
import { aiToolPromptHints } from "./tools";
|
|
92
107
|
import type { AiConversation, AiTurn } from "./types";
|
|
93
108
|
|
|
@@ -305,6 +320,7 @@ export const aiRoutes = (() => {
|
|
|
305
320
|
|
|
306
321
|
return (
|
|
307
322
|
new Hono<AuthContext>()
|
|
323
|
+
.use("*", noTransform())
|
|
308
324
|
.use(rateLimit())
|
|
309
325
|
.use("*", auth.requireRole("authenticated"))
|
|
310
326
|
.get("/status", async (c) => {
|
|
@@ -459,6 +475,42 @@ export const aiRoutes = (() => {
|
|
|
459
475
|
});
|
|
460
476
|
return respond(c, ok({ prompt, renderedAt: new Date().toISOString() }));
|
|
461
477
|
})
|
|
478
|
+
.get(
|
|
479
|
+
"/resources/recent",
|
|
480
|
+
describeRoute({
|
|
481
|
+
tags: ["AI"],
|
|
482
|
+
summary: "List recently sent resources",
|
|
483
|
+
description: "Up to eight recently sent refs, with current reader-authorized metadata. Unreadable resources are omitted.",
|
|
484
|
+
...requiresAuth,
|
|
485
|
+
responses: {
|
|
486
|
+
200: jsonResponse(
|
|
487
|
+
z.object({ resources: z.array(AiRecentResourceSchema).max(RECENT_SENT_REF_LIMIT) }),
|
|
488
|
+
"Current recent resources",
|
|
489
|
+
),
|
|
490
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
491
|
+
403: jsonResponse(ErrorResponseSchema, "AI context unavailable"),
|
|
492
|
+
},
|
|
493
|
+
}),
|
|
494
|
+
// Titles come from app readers, so a scoped token needs the read scope the capability routes ask for.
|
|
495
|
+
auth.requireOAuthScope("read", "admin"),
|
|
496
|
+
async (c) => {
|
|
497
|
+
c.header("Cache-Control", "no-store");
|
|
498
|
+
const ctx = await resolveContext(c);
|
|
499
|
+
if (ctx instanceof Response) return ctx;
|
|
500
|
+
setRequestTimeout(c, Math.ceil(QUERY_TIMEOUT_MS / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
|
|
501
|
+
try {
|
|
502
|
+
const resources = await listAiRecentResources({
|
|
503
|
+
ownerUserId: ctx.ownerUserId,
|
|
504
|
+
authority: auth.getAuthority(c),
|
|
505
|
+
locale: getLocale(c),
|
|
506
|
+
signal: c.req.raw.signal,
|
|
507
|
+
});
|
|
508
|
+
return respond(c, ok({ resources }));
|
|
509
|
+
} finally {
|
|
510
|
+
setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_SECONDS);
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
)
|
|
462
514
|
.get("/resources", v("query", UserResourcesQuerySchema), async (c) => {
|
|
463
515
|
const ctx = await resolveContext(c);
|
|
464
516
|
if (ctx instanceof Response) return ctx;
|
|
@@ -1158,6 +1210,101 @@ export const aiRoutes = (() => {
|
|
|
1158
1210
|
await abortAiTurn({ conversationId: conversation.id, turnId: turn.id });
|
|
1159
1211
|
return respond(c, ok({ ok: true }));
|
|
1160
1212
|
})
|
|
1213
|
+
.post("/conversations/:conversationId/turns/:turnId/calls/:callId/check", async (c) => {
|
|
1214
|
+
const ctx = await resolveContext(c);
|
|
1215
|
+
if (ctx instanceof Response) return ctx;
|
|
1216
|
+
// The check runs with the person's session authority, so a scoped token or another client cannot borrow it.
|
|
1217
|
+
if (ctx.actor.kind !== "user" || !ctx.signedInSession)
|
|
1218
|
+
return respond(c, fail(err.forbidden("Action outcome checks require a signed-in browser session.")));
|
|
1219
|
+
const conversation = await loadConversation(c, ctx);
|
|
1220
|
+
if (!conversation) return notFound(c);
|
|
1221
|
+
const turn = await loadTurn(conversation, c.req.param("turnId"));
|
|
1222
|
+
const callId = c.req.param("callId");
|
|
1223
|
+
if (!turn || !callId) return notFound(c);
|
|
1224
|
+
const target = { conversationId: conversation.id, turnId: turn.id, callId };
|
|
1225
|
+
// The app's Query may take its whole budget; the request must outlast it.
|
|
1226
|
+
setRequestTimeout(c, Math.ceil(QUERY_TIMEOUT_MS / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
|
|
1227
|
+
try {
|
|
1228
|
+
const messages = await aiConversations.listTurnMessages({
|
|
1229
|
+
conversationId: conversation.id,
|
|
1230
|
+
loopId: turn.id,
|
|
1231
|
+
includeCompacted: true,
|
|
1232
|
+
});
|
|
1233
|
+
const message = messages.find(
|
|
1234
|
+
({ message }) =>
|
|
1235
|
+
message.role === "assistant" && message.content.some((block) => block.type === "tool_call" && block.id === callId),
|
|
1236
|
+
);
|
|
1237
|
+
if (!message) return notFound(c);
|
|
1238
|
+
const review = message.meta?.toolReviews?.[callId];
|
|
1239
|
+
const previous = message.meta?.toolChecks?.[callId];
|
|
1240
|
+
if (!review || !("sentence" in review) || !review.check) return respond(c, fail(err.conflict("This call has no outcome check.")));
|
|
1241
|
+
if (!previous) {
|
|
1242
|
+
// An unknown tool result ends executor recovery for this call, even while its turn is live. Without
|
|
1243
|
+
// a result, only a terminal cut-off call may be checked. Both paths require evidence of dispatch.
|
|
1244
|
+
const terminal = turn.status === "completed" || turn.status === "failed" || turn.status === "aborted";
|
|
1245
|
+
const result = messages.find(({ message }) => message.role === "tool_result" && message.callId === callId)?.message;
|
|
1246
|
+
const eligible = result
|
|
1247
|
+
? result.role === "tool_result" && result.isError === true && isUnknownActionOutcome(result.result)
|
|
1248
|
+
: terminal;
|
|
1249
|
+
if (!eligible || !(await aiToolAudit.hasCapabilityDispatch(target)))
|
|
1250
|
+
return respond(c, fail(err.conflict("This call has no outcome check.")));
|
|
1251
|
+
}
|
|
1252
|
+
// A settled check answers again as it was stored, so a client whose last response got lost can recover
|
|
1253
|
+
// without running the Query again.
|
|
1254
|
+
if (previous?.state === "done" || previous?.state === "not_done") return respond(c, ok(previous));
|
|
1255
|
+
const block =
|
|
1256
|
+
message.message.role === "assistant"
|
|
1257
|
+
? message.message.content.find((block) => block.type === "tool_call" && block.id === callId)
|
|
1258
|
+
: undefined;
|
|
1259
|
+
const catalog = buildAiCapabilityCatalog(await listCapabilities());
|
|
1260
|
+
const presentation = message.meta?.toolPresentations?.[callId];
|
|
1261
|
+
const entry = catalog.find(
|
|
1262
|
+
(entry) =>
|
|
1263
|
+
entry.kind === "action" &&
|
|
1264
|
+
(entry.name === (block?.type === "tool_call" ? block.name : undefined) ||
|
|
1265
|
+
entry.providerName === (block?.type === "tool_call" ? block.name : undefined)) &&
|
|
1266
|
+
(!presentation || presentation.appId === entry.appId),
|
|
1267
|
+
);
|
|
1268
|
+
if (!entry) return respond(c, fail(err.conflict("The Action's app is unavailable. Try the check again when it returns.")));
|
|
1269
|
+
const authority = await resolveAiCapabilityActor({
|
|
1270
|
+
conversationId: conversation.id,
|
|
1271
|
+
persistedActor: ctx.actor,
|
|
1272
|
+
store: aiConversations,
|
|
1273
|
+
});
|
|
1274
|
+
// One check at a time per call, across Core replicas and the executor: the claim is one conditional write,
|
|
1275
|
+
// and no transaction stays open while the app's Query runs.
|
|
1276
|
+
const claimedAt = new Date().toISOString();
|
|
1277
|
+
const claimed = await claimAiToolCheck({
|
|
1278
|
+
...target,
|
|
1279
|
+
first: !previous,
|
|
1280
|
+
at: claimedAt,
|
|
1281
|
+
staleBefore: new Date(Date.now() - AI_TOOL_CHECK_STALE_MS).toISOString(),
|
|
1282
|
+
});
|
|
1283
|
+
if (!claimed) {
|
|
1284
|
+
const stored = await readAiToolCheck(target);
|
|
1285
|
+
if (stored?.state === "done" || stored?.state === "not_done") return respond(c, ok(stored));
|
|
1286
|
+
return respond(c, fail(err.conflict("An outcome check is already running for this call.")));
|
|
1287
|
+
}
|
|
1288
|
+
const answered = await checkAiCapability({
|
|
1289
|
+
...target,
|
|
1290
|
+
entry,
|
|
1291
|
+
check: review.check,
|
|
1292
|
+
catalog,
|
|
1293
|
+
authority,
|
|
1294
|
+
// The receipt joins the stored review's sentence, so it is asked for in that language.
|
|
1295
|
+
locale: review.locale,
|
|
1296
|
+
context: { callId, signal: c.req.raw.signal },
|
|
1297
|
+
});
|
|
1298
|
+
if (await settleAiToolCheck({ ...target, claimedAt, check: answered })) return respond(c, ok(answered));
|
|
1299
|
+
// The claim went stale and a newer check took over; its stored result is the answer.
|
|
1300
|
+
const stored = await readAiToolCheck(target);
|
|
1301
|
+
return stored && stored.state !== "checking"
|
|
1302
|
+
? respond(c, ok(stored))
|
|
1303
|
+
: respond(c, fail(err.conflict("An outcome check is already running for this call.")));
|
|
1304
|
+
} finally {
|
|
1305
|
+
setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_SECONDS);
|
|
1306
|
+
}
|
|
1307
|
+
})
|
|
1161
1308
|
.post(
|
|
1162
1309
|
"/conversations/:conversationId/turns/:turnId/actions/:callId",
|
|
1163
1310
|
bodyLimit({ maxSize: AI_CLIENT_TOOL_ACTION_MAX_BYTES }),
|
|
@@ -1208,8 +1355,51 @@ export const aiRoutes = (() => {
|
|
|
1208
1355
|
if (!conversation) return notFound(c);
|
|
1209
1356
|
const turn = await loadTurn(conversation, c.req.param("turnId"));
|
|
1210
1357
|
if (!turn) return notFound(c);
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1358
|
+
const scope = { conversationId: conversation.id, turnId: turn.id };
|
|
1359
|
+
const actions = await listPendingAiTurnActions(scope);
|
|
1360
|
+
if (!actions.some((action) => action.type === "approval_request"))
|
|
1361
|
+
return respond(c, ok(actions.map((action) => publicPendingAction(action, conversation.shortId, turn.shortId))));
|
|
1362
|
+
const [records, reviews, config] = await Promise.all([
|
|
1363
|
+
aiConversations.listPendingActionRecords(scope),
|
|
1364
|
+
aiConversations.listTurnToolReviews(scope),
|
|
1365
|
+
aiConversations.getTurnRunConfig(scope),
|
|
1366
|
+
]);
|
|
1367
|
+
const locale = config?.kind !== "compact" ? (config?.locale ?? getLocale(c)) : getLocale(c);
|
|
1368
|
+
let capabilities: Promise<Awaited<ReturnType<typeof listCapabilities>> | null> | undefined;
|
|
1369
|
+
const presented = await Promise.all(
|
|
1370
|
+
actions.map(async (action) => {
|
|
1371
|
+
const result = publicPendingAction(action, conversation.shortId, turn.shortId);
|
|
1372
|
+
if (action.type !== "approval_request") return result;
|
|
1373
|
+
const record = records.find((record) => record.callId === action.callId);
|
|
1374
|
+
const request = record?.approvalId ? await approvals.get(record.approvalId) : null;
|
|
1375
|
+
const reviewCallId = record?.kind === "custom_approval" ? action.callId.replace(/-approval-[0-9]+$/, "") : action.callId;
|
|
1376
|
+
const stored = reviews.get(reviewCallId);
|
|
1377
|
+
let review: AiStoredActionReview | null = stored && !isLegacyActionReview(stored) ? stored : null;
|
|
1378
|
+
if (!review && request) {
|
|
1379
|
+
review = pendingApprovalReview({ storedReview: null, details: request.details });
|
|
1380
|
+
if (review && !isLegacyActionReview(review) && !review.ask) {
|
|
1381
|
+
// As the approvals page: the declared reason when the review names none, read once per listing.
|
|
1382
|
+
capabilities ??= listCapabilities().catch(() => null);
|
|
1383
|
+
const manifest = (await capabilities)?.find((entry) => entry.appId === request.appId)?.manifest;
|
|
1384
|
+
review = storedActionReview(review, manifest ? capabilityActionPolicy(manifest, request.action) : undefined);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
const appId = request?.appId;
|
|
1388
|
+
// The name is presentation: a registry that cannot answer leaves the app ID, never the waiting approvals unread.
|
|
1389
|
+
const app = appId && appId !== "assistant" ? await getApp(appId).catch(() => null) : null;
|
|
1390
|
+
return {
|
|
1391
|
+
...result,
|
|
1392
|
+
...(review && !isLegacyActionReview(review) ? { review } : {}),
|
|
1393
|
+
app: {
|
|
1394
|
+
id: appId ?? "assistant",
|
|
1395
|
+
name: app ? resolveAppIdentityPresentation(app, locale).name : appId && appId !== "assistant" ? appId : "Cloud",
|
|
1396
|
+
},
|
|
1397
|
+
chatTitle: conversation.title,
|
|
1398
|
+
...(request ? { expiresAt: request.expiresAt.toISOString() } : {}),
|
|
1399
|
+
};
|
|
1400
|
+
}),
|
|
1401
|
+
);
|
|
1402
|
+
return respond(c, ok(presented));
|
|
1213
1403
|
})
|
|
1214
1404
|
.get("/conversations/:conversationId/stream", async (c) => {
|
|
1215
1405
|
const ctx = await resolveContext(c);
|
package/src/ai/runtime.ts
CHANGED
|
@@ -47,6 +47,7 @@ import type {
|
|
|
47
47
|
import { isAiImageMediaType } from "./types";
|
|
48
48
|
import { validateAiTurnRequest } from "./validate";
|
|
49
49
|
|
|
50
|
+
export { createAiTurnCodeAndClientTools } from "./default-tools";
|
|
50
51
|
export type { ValidateAiTurnInput } from "./validate";
|
|
51
52
|
export { isAiSettingsError, validateAiTurnRequest } from "./validate";
|
|
52
53
|
|
package/src/ai/skills-routes.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { HTTPException } from "hono/http-exception";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { AuthenticatedPrincipalSchema } from "../contracts/shared";
|
|
5
5
|
import { type AuthContext, auth, err, fail, ok, rateLimit, respond, v } from "../server";
|
|
6
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
6
7
|
import { localizedAppName } from "../ssr/runtime";
|
|
7
8
|
import { aiProjects } from "./projects";
|
|
8
9
|
import {
|
|
@@ -63,6 +64,8 @@ type AiSkillsRouteDependencies = {
|
|
|
63
64
|
|
|
64
65
|
const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
65
66
|
new Hono<AuthContext>()
|
|
67
|
+
// The list combines skills from separate grants; the writer of one can rename it next to another skill's text.
|
|
68
|
+
.use("*", noTransform())
|
|
66
69
|
.onError((error, c) => {
|
|
67
70
|
if (error instanceof AiSkillAppForbiddenError) return respond(c, fail(err.forbidden(error.message)));
|
|
68
71
|
if (error instanceof HTTPException) return error.getResponse();
|
package/src/ai/skills.ts
CHANGED
|
@@ -522,7 +522,15 @@ const skillSnapshotFiles = (skill: AiSkill): { path: string; content: string }[]
|
|
|
522
522
|
...skill.references.map((reference) => ({ path: `${skill.name}/${reference.path}`, content: reference.content })),
|
|
523
523
|
];
|
|
524
524
|
|
|
525
|
-
type SkillListOptions = {
|
|
525
|
+
type SkillListOptions = {
|
|
526
|
+
name?: string;
|
|
527
|
+
/** Only these Skills, by short ID, such as the ones a person recently sent. */
|
|
528
|
+
shortIds?: readonly string[];
|
|
529
|
+
query?: string;
|
|
530
|
+
enabledOnly?: boolean;
|
|
531
|
+
modelInvocableOnly?: boolean;
|
|
532
|
+
limit?: number;
|
|
533
|
+
};
|
|
526
534
|
async function listSkillSummaries(
|
|
527
535
|
subject: AccessSubject | null,
|
|
528
536
|
options: SkillListOptions & {
|
|
@@ -551,6 +559,7 @@ async function listSkillSummaries(
|
|
|
551
559
|
ON disabled.skill_id = skill.id AND disabled.user_id = ${userId}::uuid
|
|
552
560
|
WHERE access.permission <> 'none'
|
|
553
561
|
AND ${options.name === undefined ? sql`true` : sql`skill.name = ${options.name}`}
|
|
562
|
+
AND ${options.shortIds === undefined ? sql`true` : sql`skill.short_id = ANY(${toPgTextArray([...options.shortIds])}::text[])`}
|
|
554
563
|
AND (${!options.manageOnly} OR access.permission='admin')
|
|
555
564
|
AND ${options.project ? sql`EXISTS(SELECT 1 FROM ai.project_skills link WHERE link.skill_id=skill.id AND link.project_id=${options.project.id}::uuid) = ${options.project.linked}` : sql`true`}
|
|
556
565
|
AND (${!options.modelInvocableOnly} OR NOT manual_only)
|