@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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { APPROVALS_PAGE_SIZE } from "../contracts/approvals";
|
|
4
|
+
import { CapabilityActionReviewPresentationSchema, CapabilitySemanticLinkSchema } from "../contracts/capabilities";
|
|
5
|
+
import { openRequests } from "../services/approvals/content";
|
|
6
|
+
import { logger } from "../services/logging";
|
|
7
|
+
import { reviewShortText, toolActionCard, toolBlockReview } from "./chat/action-card";
|
|
8
|
+
import { aiToolIcon } from "./chat/message-utils";
|
|
9
|
+
import { legacyOutcomeWords } from "./chat/tool-action-words";
|
|
10
|
+
import { type AiTurnBlock, toolBlockId } from "./protocol";
|
|
11
|
+
|
|
12
|
+
export type AiOpenQuestion = {
|
|
13
|
+
/** The approval request id (approvals.requests.id). */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Short id of the chat the question waits in. */
|
|
16
|
+
conversationId: string;
|
|
17
|
+
/** The chat's current title, at most 200 code points; chat content, never Cloud's words. */
|
|
18
|
+
conversationTitle: string | null;
|
|
19
|
+
/** The question's data-chat-anchor: the id of the tool block the chat renders it in. */
|
|
20
|
+
anchor: string;
|
|
21
|
+
/** The question's short form, plain text, at most 160 code points. */
|
|
22
|
+
short: string;
|
|
23
|
+
/** The app the question is about, as the card shows it. */
|
|
24
|
+
app: { name: string; icon: string; accent?: string };
|
|
25
|
+
createdAt: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type AiOpenQuestions = {
|
|
29
|
+
items: AiOpenQuestion[];
|
|
30
|
+
/** All open questions of the person, across pages. */
|
|
31
|
+
count: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type QuestionRow = {
|
|
35
|
+
id: string;
|
|
36
|
+
conversationId: string;
|
|
37
|
+
conversationTitle: string | null;
|
|
38
|
+
displayCallId: string;
|
|
39
|
+
appId: string;
|
|
40
|
+
sentence: string;
|
|
41
|
+
toolName: string;
|
|
42
|
+
createdAt: Date;
|
|
43
|
+
block: Extract<AiTurnBlock, { kind: "tool" }> | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const log = logger("ai:open-questions");
|
|
47
|
+
const legacyReviewSchema = z.object({
|
|
48
|
+
question: z.string().min(1),
|
|
49
|
+
rows: z.array(z.object({ label: z.string(), value: z.string(), format: z.enum(["date", "date-time"]).optional() })),
|
|
50
|
+
markdown: z.string().optional(),
|
|
51
|
+
summary: z.string().optional(),
|
|
52
|
+
links: z.array(CapabilitySemanticLinkSchema).optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const ownedRequests = (ownerUserId: string) => sql`
|
|
56
|
+
SELECT r.* FROM (${openRequests(ownerUserId)}) r
|
|
57
|
+
WHERE EXISTS (
|
|
58
|
+
SELECT 1 FROM ai.pending_actions p JOIN ai.conversations c ON c.id=p.conversation_id
|
|
59
|
+
WHERE p.approval_id=r.id AND p.status='pending' AND p.kind IN ('approval','custom_approval')
|
|
60
|
+
AND c.created_by_user_id=${ownerUserId}::uuid
|
|
61
|
+
)`;
|
|
62
|
+
|
|
63
|
+
/** One bounded, owner-scoped snapshot, using exactly the chat's stored question card. */
|
|
64
|
+
export const listAiOpenQuestions = async (input: { ownerUserId: string; locale: string; timeZone: string }): Promise<AiOpenQuestions> => {
|
|
65
|
+
const [counts] = await sql<{ count: number }[]>`
|
|
66
|
+
SELECT count(*)::int AS count FROM (${ownedRequests(input.ownerUserId)}) r`;
|
|
67
|
+
const rows = await sql<QuestionRow[]>`
|
|
68
|
+
WITH page AS (
|
|
69
|
+
SELECT * FROM (${ownedRequests(input.ownerUserId)}) r
|
|
70
|
+
ORDER BY r.created_at DESC, r.id DESC LIMIT ${APPROVALS_PAGE_SIZE}
|
|
71
|
+
)
|
|
72
|
+
SELECT r.id, r.app_id AS "appId", r.sentence, r.created_at AS "createdAt",
|
|
73
|
+
question.short_id AS "conversationId", question.title AS "conversationTitle",
|
|
74
|
+
question.display_call_id AS "displayCallId", question.tool_name AS "toolName", question.block
|
|
75
|
+
FROM page r JOIN LATERAL (
|
|
76
|
+
SELECT c.short_id, c.title, p.tool_name,
|
|
77
|
+
CASE WHEN p.kind='custom_approval'
|
|
78
|
+
THEN COALESCE(substring(p.call_id FROM '^(.*)-approval-[0-9]+$'), p.call_id)
|
|
79
|
+
ELSE p.call_id END AS display_call_id,
|
|
80
|
+
(SELECT block FROM jsonb_array_elements(
|
|
81
|
+
CASE WHEN jsonb_typeof(t.live_blocks)='array' THEN t.live_blocks ELSE '[]'::jsonb END
|
|
82
|
+
) AS blocks(block)
|
|
83
|
+
WHERE block->>'kind'='tool' AND block->>'callId'=p.call_id LIMIT 1) AS block
|
|
84
|
+
FROM ai.pending_actions p JOIN ai.conversations c ON c.id=p.conversation_id
|
|
85
|
+
JOIN ai.turns t ON t.id=p.turn_id
|
|
86
|
+
WHERE p.approval_id=r.id AND p.status='pending' AND p.kind IN ('approval','custom_approval')
|
|
87
|
+
AND c.created_by_user_id=${input.ownerUserId}::uuid
|
|
88
|
+
ORDER BY p.created_at DESC, p.id DESC LIMIT 1
|
|
89
|
+
) question ON true
|
|
90
|
+
ORDER BY r.created_at DESC, r.id DESC`;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
count: counts?.count ?? 0,
|
|
94
|
+
items: rows.map((row) => openQuestionFromRow(row, input)),
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Stop at the payload bound without splitting a Unicode code point or copying the rest of a legacy message. */
|
|
99
|
+
const boundedText = (text: string, max: number): string => {
|
|
100
|
+
const points: string[] = [];
|
|
101
|
+
for (const point of text) {
|
|
102
|
+
if (points.length === max) return `${points.slice(0, max - 1).join("")}…`;
|
|
103
|
+
points.push(point);
|
|
104
|
+
}
|
|
105
|
+
return text;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Present one stored question without database access. */
|
|
109
|
+
export const openQuestionFromRow = (row: QuestionRow, input: { locale: string; timeZone: string }): AiOpenQuestion => {
|
|
110
|
+
const block = row.block;
|
|
111
|
+
const presentation = block?.presentation;
|
|
112
|
+
const question: AiOpenQuestion = {
|
|
113
|
+
id: row.id,
|
|
114
|
+
conversationId: row.conversationId,
|
|
115
|
+
conversationTitle: row.conversationTitle === null ? null : boundedText(row.conversationTitle, 200),
|
|
116
|
+
// The id the chat renders the card under: a custom approval's card carries its parent call, also when a stored
|
|
117
|
+
// block still has the approval's own id (see normalizeCustomApprovalBlocks in client/projection.ts).
|
|
118
|
+
anchor: toolBlockId(row.displayCallId),
|
|
119
|
+
short: boundedText(row.sentence.split(/\r?\n/, 1)[0] ?? "", 160),
|
|
120
|
+
app: {
|
|
121
|
+
name: typeof presentation?.appName === "string" ? presentation.appName : block ? "Cloud" : row.appId,
|
|
122
|
+
icon: aiToolIcon(
|
|
123
|
+
typeof block?.name === "string" ? block.name : row.toolName,
|
|
124
|
+
typeof presentation?.appIcon === "string" ? presentation.appIcon : undefined,
|
|
125
|
+
),
|
|
126
|
+
...(typeof presentation?.appAccent === "string" ? { accent: presentation.appAccent } : {}),
|
|
127
|
+
},
|
|
128
|
+
createdAt: row.createdAt.toISOString(),
|
|
129
|
+
};
|
|
130
|
+
if (!block) return question;
|
|
131
|
+
try {
|
|
132
|
+
if (
|
|
133
|
+
typeof block.id !== "string" ||
|
|
134
|
+
!block.id ||
|
|
135
|
+
typeof block.name !== "string" ||
|
|
136
|
+
!block.name ||
|
|
137
|
+
typeof block.callId !== "string" ||
|
|
138
|
+
!["running", "awaiting_approval", "awaiting_client", "completed", "failed", "rejected"].includes(block.status)
|
|
139
|
+
)
|
|
140
|
+
throw new Error("Malformed tool block");
|
|
141
|
+
if (block.review == null && block.approval?.review != null) CapabilityActionReviewPresentationSchema.parse(block.approval.review);
|
|
142
|
+
const review = toolBlockReview(block);
|
|
143
|
+
if (review !== null) {
|
|
144
|
+
if ("legacy" in review) legacyReviewSchema.parse(review.legacy);
|
|
145
|
+
else CapabilityActionReviewPresentationSchema.parse(review);
|
|
146
|
+
}
|
|
147
|
+
const card = toolActionCard(block, {
|
|
148
|
+
locale: input.locale,
|
|
149
|
+
timeZone: input.timeZone,
|
|
150
|
+
outcome: legacyOutcomeWords(block, input.locale, input.timeZone),
|
|
151
|
+
});
|
|
152
|
+
const short =
|
|
153
|
+
review && !("legacy" in review)
|
|
154
|
+
? reviewShortText(review, { locale: input.locale, timeZone: input.timeZone, app: card.app })
|
|
155
|
+
: card.questionText;
|
|
156
|
+
if (
|
|
157
|
+
typeof short !== "string" ||
|
|
158
|
+
typeof card.app.name !== "string" ||
|
|
159
|
+
typeof card.app.icon !== "string" ||
|
|
160
|
+
(card.app.accent !== undefined && typeof card.app.accent !== "string")
|
|
161
|
+
)
|
|
162
|
+
throw new Error("Malformed question card");
|
|
163
|
+
return { ...question, short: boundedText(short, 160), app: card.app };
|
|
164
|
+
} catch {
|
|
165
|
+
log.warn("Failed to present open question", { approvalId: row.id, conversationId: row.conversationId });
|
|
166
|
+
return question;
|
|
167
|
+
}
|
|
168
|
+
};
|
package/src/ai/projects.ts
CHANGED
|
@@ -295,8 +295,7 @@ const listProjectAccess = async (projectId: string, db: SQL = sql): Promise<AiPr
|
|
|
295
295
|
const rows = await db<ProjectAccessRow[]>`
|
|
296
296
|
SELECT project_access.short_id, access.user_id, access.group_id, access.service_account_id, access.authenticated_only,
|
|
297
297
|
access.permission, access.created_at,
|
|
298
|
-
COALESCE(NULLIF(users.display_name, ''), users.uid, groups.name, service_accounts.name,
|
|
299
|
-
CASE WHEN access.authenticated_only THEN 'All authenticated users' ELSE 'Public' END) AS display_name,
|
|
298
|
+
COALESCE(NULLIF(users.display_name, ''), users.uid, groups.name, service_accounts.name) AS display_name,
|
|
300
299
|
users.avatar_hash, service_accounts.kind AS service_account_kind
|
|
301
300
|
FROM ai.project_access project_access
|
|
302
301
|
JOIN auth.access access ON access.id = project_access.access_id
|
package/src/ai/protocol.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CompactResult, InboundEvent, Message } from "@k2b/nessi";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CapabilityActionReviewPresentation } from "../contracts/capabilities";
|
|
3
3
|
import type { AiActionCheck, AiStoredActionReview } from "./action-review";
|
|
4
4
|
import type { AiConversation, AiFrontendToolMode, AiStoredMessage, AiToolPresentation, AiTurnStatus } from "./types";
|
|
5
5
|
|
|
@@ -49,7 +49,7 @@ export type AiTurnBlock =
|
|
|
49
49
|
/** Present while status is awaiting_approval. */
|
|
50
50
|
approval?: {
|
|
51
51
|
message?: string;
|
|
52
|
-
review?:
|
|
52
|
+
review?: CapabilityActionReviewPresentation;
|
|
53
53
|
allowAlways: boolean;
|
|
54
54
|
/** Whether the approval can be remembered for this chat. */
|
|
55
55
|
allowChat?: boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plain-text form of an Assistant question, the same one the chat card shows: for `cld`, notifications, and Cloud
|
|
3
|
+
* Login. Pure and browser-safe, in its own entry so neither the CLI bundle nor browser bundles that use
|
|
4
|
+
* `@k2b/cloud/ai/browser` pull it in by accident.
|
|
5
|
+
*/
|
|
6
|
+
export { type ApprovalCardInput, approvalCard } from "../approvals/approval-card";
|
|
7
|
+
export type { ActionCard } from "./chat/action-card";
|
|
8
|
+
export { type ActionQuestionText, type ActionTextRow, actionCardText, actionQuestionLines } from "./chat/action-text";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
3
|
-
import {
|
|
4
|
-
import { type
|
|
3
|
+
import { CAPABILITY_DISCOVERY_TIMEOUT_MS } from "../_internal/capability-timeout";
|
|
4
|
+
import { type listApps, listCapabilities } from "../_internal/registry";
|
|
5
|
+
import { type CapabilityDispatchParams, dispatchCapability } from "../api/capabilities";
|
|
5
6
|
import {
|
|
6
7
|
CloudResourceRefSchema,
|
|
7
8
|
capabilityResultSchema,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
} from "../contracts/capabilities";
|
|
12
13
|
import type { CapabilityRegistryEntry } from "../contracts/registry";
|
|
13
14
|
import type { RequestAuthority } from "../server";
|
|
15
|
+
import { discoverableAppEntries } from "../services/app-discovery";
|
|
14
16
|
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
15
17
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
16
18
|
import { CODE_SOURCE_TOOLS } from "./code-source-contracts";
|
|
@@ -65,6 +67,7 @@ const studioReader = async (input: {
|
|
|
65
67
|
export type AiRecentResourceDependencies = {
|
|
66
68
|
store?: Pick<AiConversationService, "listRecentSentResourceRefs">;
|
|
67
69
|
listRegistry?: () => Promise<CapabilityRegistryEntry[]>;
|
|
70
|
+
listApps?: typeof listApps;
|
|
68
71
|
dispatch?: (params: CapabilityDispatchParams) => Promise<Response>;
|
|
69
72
|
skills?: Pick<typeof aiSkills, "list">;
|
|
70
73
|
studioReader?: typeof studioReader;
|
|
@@ -108,7 +111,7 @@ export const listAiRecentResources = async (input: {
|
|
|
108
111
|
dependencies?: AiRecentResourceDependencies;
|
|
109
112
|
}): Promise<AiRecentResource[]> => {
|
|
110
113
|
const deps = input.dependencies ?? {};
|
|
111
|
-
const signal = AbortSignal.any([input.signal, AbortSignal.timeout(deps.timeoutMs ??
|
|
114
|
+
const signal = AbortSignal.any([input.signal, AbortSignal.timeout(deps.timeoutMs ?? CAPABILITY_DISCOVERY_TIMEOUT_MS)]);
|
|
112
115
|
// Unreadable history is no answer, not an empty one: the request fails, so the browser asks again at its next opening.
|
|
113
116
|
const sent = await withinBudget(signal, () =>
|
|
114
117
|
(deps.store ?? aiConversations).listRecentSentResourceRefs({ ownerUserId: input.ownerUserId }),
|
|
@@ -118,10 +121,14 @@ export const listAiRecentResources = async (input: {
|
|
|
118
121
|
const skillIds = candidates.flatMap(({ ref }) => (ref.type === "core.ai.skill" ? [ref.id] : []));
|
|
119
122
|
// Exactly the sent Skills, so a large Skill catalog never pushes one out; failing, they are left out like a reader.
|
|
120
123
|
const skills = skillIds.length
|
|
121
|
-
? withinBudget(signal, () =>
|
|
124
|
+
? withinBudget(signal, () =>
|
|
125
|
+
(deps.skills ?? aiSkills).list(input.authority.accessSubject, { shortIds: skillIds, authority: input.authority }),
|
|
126
|
+
).catch(() => [])
|
|
122
127
|
: Promise.resolve([]);
|
|
123
128
|
const registry = candidates.some(({ ref }) => ref.type !== "core.ai.skill" && ref.type !== "assistant.artifact")
|
|
124
|
-
? withinBudget(signal,
|
|
129
|
+
? withinBudget(signal, async () =>
|
|
130
|
+
discoverableAppEntries(input.authority, await (deps.listRegistry ?? listCapabilities)(), deps.listApps),
|
|
131
|
+
).catch(() => [])
|
|
125
132
|
: Promise.resolve([]);
|
|
126
133
|
const resources = await Promise.all(
|
|
127
134
|
candidates.map(async (candidate): Promise<AiRecentResource | null> => {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type SQL, sql } from "bun";
|
|
2
|
+
import { capabilityRequestHash } from "../capabilities/claims";
|
|
3
|
+
import { parseStoredReviewContent } from "../capabilities/review-storage";
|
|
4
|
+
import { CAPABILITY_MAX_REVIEW_CONTENT_BYTES, type CapabilityActionReviewPresentation } from "../contracts/capabilities";
|
|
5
|
+
import type { AiStoredActionReview } from "./action-review";
|
|
6
|
+
|
|
7
|
+
export type ReviewContentLocation = { conversationId: string; turnId: string; callId: string };
|
|
8
|
+
|
|
9
|
+
export const reviewContentHref = ({ conversationId, turnId, callId }: ReviewContentLocation): string =>
|
|
10
|
+
`/api/approvals/conversations/${encodeURIComponent(conversationId)}/turns/${encodeURIComponent(turnId)}/reviews/${encodeURIComponent(callId)}`;
|
|
11
|
+
|
|
12
|
+
export const compactCapabilityReview = (
|
|
13
|
+
review: CapabilityActionReviewPresentation,
|
|
14
|
+
location: ReviewContentLocation,
|
|
15
|
+
): CapabilityActionReviewPresentation => {
|
|
16
|
+
if (!("sentence" in review)) return review;
|
|
17
|
+
const { content, ...compact } = review;
|
|
18
|
+
const digest = content ? capabilityRequestHash(content) : compact.contentRef?.digest;
|
|
19
|
+
return digest ? { ...compact, contentRef: { href: reviewContentHref(location), digest } } : compact;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** The existing message snapshot owns both the content and its integrity reference. */
|
|
23
|
+
export const storedReviewWithContent = (review: AiStoredActionReview, location: ReviewContentLocation): AiStoredActionReview =>
|
|
24
|
+
"content" in review && review.content
|
|
25
|
+
? { ...review, contentRef: { href: reviewContentHref(location), digest: capabilityRequestHash(review.content) } }
|
|
26
|
+
: review;
|
|
27
|
+
|
|
28
|
+
/** Rebind the locator on a fork; never make the new conversation depend on access to its source. */
|
|
29
|
+
export const compactStoredReview = (review: AiStoredActionReview, location?: ReviewContentLocation): AiStoredActionReview => {
|
|
30
|
+
if ("legacy" in review) return review;
|
|
31
|
+
const { content: _content, ...compact } = review;
|
|
32
|
+
return compact.contentRef && location
|
|
33
|
+
? { ...compact, contentRef: { ...compact.contentRef, href: reviewContentHref(location) } }
|
|
34
|
+
: compact;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Project before transferring JSONB out of Postgres, not after loading every full review into Core. */
|
|
38
|
+
export const compactReviewMapSql = (reviews: SQL.Query<unknown>) => sql`(
|
|
39
|
+
SELECT COALESCE(jsonb_object_agg(entry.key, entry.value - 'content'), '{}'::jsonb)
|
|
40
|
+
FROM jsonb_each(CASE WHEN jsonb_typeof(${reviews}) = 'object' THEN ${reviews} ELSE '{}'::jsonb END) entry
|
|
41
|
+
)`;
|
|
42
|
+
|
|
43
|
+
export const compactReviewMetaSql = (meta: SQL.Query<unknown>) => sql`CASE
|
|
44
|
+
WHEN ${meta} ? 'toolReviews' THEN jsonb_set(${meta}, '{toolReviews}', ${compactReviewMapSql(sql`${meta}->'toolReviews'`)})
|
|
45
|
+
ELSE ${meta} END`;
|
|
46
|
+
|
|
47
|
+
/** Caller has already authorized this conversation. Read one exact persisted call from either existing message store. */
|
|
48
|
+
export const readReviewContent = async (location: ReviewContentLocation, digest: string, ownerUserId: string, signal?: AbortSignal) => {
|
|
49
|
+
signal?.throwIfAborted();
|
|
50
|
+
const query = sql<{ content: unknown }[]>`
|
|
51
|
+
SELECT meta->'toolReviews'->${location.callId}->'content' AS content
|
|
52
|
+
FROM (
|
|
53
|
+
SELECT meta, conversation_id, loop_id, role, seq FROM ai.messages
|
|
54
|
+
UNION ALL
|
|
55
|
+
SELECT meta, conversation_id, loop_id, role, seq FROM ai.task_messages
|
|
56
|
+
) messages
|
|
57
|
+
WHERE conversation_id=${location.conversationId}::uuid AND loop_id=${location.turnId} AND role='assistant'
|
|
58
|
+
AND EXISTS (SELECT 1 FROM ai.conversations c WHERE c.id=messages.conversation_id
|
|
59
|
+
AND c.created_by_user_id=${ownerUserId}::uuid AND c.archived_at IS NULL)
|
|
60
|
+
AND meta->'toolReviews'->${location.callId}->'contentRef'->>'digest'=${digest}
|
|
61
|
+
AND octet_length((meta->'toolReviews'->${location.callId}->'content')::text) <= ${2 * CAPABILITY_MAX_REVIEW_CONTENT_BYTES + 2}
|
|
62
|
+
ORDER BY seq DESC LIMIT 1`;
|
|
63
|
+
// JSONB adds spaces after punctuation; at most twice the compact JSON budget. The exact limit is checked below.
|
|
64
|
+
const abort = () => query.cancel();
|
|
65
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
66
|
+
if (signal?.aborted) abort();
|
|
67
|
+
let rows: { content: unknown }[];
|
|
68
|
+
try {
|
|
69
|
+
rows = await query;
|
|
70
|
+
} finally {
|
|
71
|
+
signal?.removeEventListener("abort", abort);
|
|
72
|
+
}
|
|
73
|
+
signal?.throwIfAborted();
|
|
74
|
+
const row = rows[0];
|
|
75
|
+
const parsed = parseStoredReviewContent(row?.content);
|
|
76
|
+
return parsed.success && capabilityRequestHash(parsed.data) === digest ? parsed.data : null;
|
|
77
|
+
};
|
package/src/ai/routes.ts
CHANGED
|
@@ -2,10 +2,11 @@ import { type Context, Hono } from "hono";
|
|
|
2
2
|
import { bodyLimit } from "hono/body-limit";
|
|
3
3
|
import { describeRoute } from "hono-openapi";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { CAPABILITY_DISCOVERY_TIMEOUT_MS, capabilityTimeoutMs } from "../_internal/capability-timeout";
|
|
6
|
+
import { getApp, listCapabilities } from "../_internal/registry";
|
|
7
7
|
import { ErrorResponseSchema } from "../contracts";
|
|
8
8
|
import { APPROVAL_CLIENT_HEADER } from "../contracts/approvals";
|
|
9
|
+
import { capabilityActionPolicy } from "../contracts/capabilities";
|
|
9
10
|
import {
|
|
10
11
|
type AccessSubject,
|
|
11
12
|
type AuthContext,
|
|
@@ -26,12 +27,16 @@ import { isAppSessionActor } from "../server/actor";
|
|
|
26
27
|
import { isRequestCredentialCurrent } from "../server/middleware/auth";
|
|
27
28
|
import { noTransform } from "../server/middleware/no-transform";
|
|
28
29
|
import { REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "../server/request-timeout";
|
|
29
|
-
import {
|
|
30
|
+
import { appAccessAuthority } from "../services/app-access";
|
|
31
|
+
import { discoverableAppEntries } from "../services/app-discovery";
|
|
32
|
+
import { approvalDecisionChannel, approvals } from "../services/approvals";
|
|
33
|
+
import { pendingApprovalReview } from "../services/approvals/question";
|
|
30
34
|
import { ImageMetadataError } from "../services/image-metadata";
|
|
31
35
|
import { logger } from "../services/logging";
|
|
32
36
|
import { coreSettings } from "../services/settings/api";
|
|
37
|
+
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
33
38
|
import { readThemeFromCookieHeader } from "../shared/theme";
|
|
34
|
-
import { isUnknownActionOutcome } from "./action-review";
|
|
39
|
+
import { type AiStoredActionReview, isLegacyActionReview, isUnknownActionOutcome, storedActionReview } from "./action-review";
|
|
35
40
|
import { assistantAiSettingsState, listAssistantAiModels, resolveAssistantAudioModel, selectAssistantAiModelId } from "./assistant-models";
|
|
36
41
|
import { AI_AUDIO_MAX_BYTES } from "./audio-format";
|
|
37
42
|
import { buildAiCapabilityCatalog } from "./capabilities";
|
|
@@ -96,7 +101,14 @@ import { AI_SHORT_ID_PATTERN } from "./short-id";
|
|
|
96
101
|
import { selectAiSkillCatalog } from "./skill-catalog";
|
|
97
102
|
import { createCloudAiLoadSkillTool, createCloudAiSearchSkillsTool } from "./skill-tool";
|
|
98
103
|
import { aiSkills } from "./skills";
|
|
99
|
-
import {
|
|
104
|
+
import {
|
|
105
|
+
AI_TOOL_CHECK_STALE_MS,
|
|
106
|
+
AiConversationCreationError,
|
|
107
|
+
aiConversations,
|
|
108
|
+
claimAiToolCheck,
|
|
109
|
+
readAiToolCheck,
|
|
110
|
+
settleAiToolCheck,
|
|
111
|
+
} from "./store";
|
|
100
112
|
import { createAiConversationStreamResponse, loadAiStreamState } from "./stream";
|
|
101
113
|
import { composeAiSystemPrompt } from "./system-prompt";
|
|
102
114
|
import { aiToolAudit } from "./tool-audit";
|
|
@@ -283,13 +295,17 @@ const prepareConversationForMessageRetry = async (conversationId: string): Promi
|
|
|
283
295
|
return (await aiConversations.getActiveTurn({ conversationId })) ? "busy" : "ready";
|
|
284
296
|
};
|
|
285
297
|
|
|
286
|
-
const prepareLaunchDraft = async (
|
|
298
|
+
const prepareLaunchDraft = async (
|
|
299
|
+
body: z.infer<typeof AiCreateConversationInputSchema>,
|
|
300
|
+
subject: AccessSubject | null,
|
|
301
|
+
authority: import("../server").RequestAuthority | null = null,
|
|
302
|
+
) => {
|
|
287
303
|
const content = [...(body.draft?.content ?? [])];
|
|
288
304
|
if (body.skills?.length && body.allowedTools && !body.allowedTools.includes("load_skill")) {
|
|
289
305
|
return fail(err.badInput("Linked Skills require load_skill in allowedTools."));
|
|
290
306
|
}
|
|
291
307
|
for (const name of new Set(body.skills ?? [])) {
|
|
292
|
-
const skill = await aiSkills.getByName(name, subject);
|
|
308
|
+
const skill = await aiSkills.getByName(name, subject, "read", authority);
|
|
293
309
|
if (!skill?.enabled) return fail(err.badInput(`Skill is unavailable: ${name}`));
|
|
294
310
|
if (!content.some((part) => part.type === "resource" && part.ref.type === "core.ai.skill" && part.ref.id === skill.shortId)) {
|
|
295
311
|
content.push({ type: "resource", ref: { type: "core.ai.skill", id: skill.shortId }, title: skill.name, icon: "ti ti-sparkles" });
|
|
@@ -430,7 +446,9 @@ export const aiRoutes = (() => {
|
|
|
430
446
|
if (preferredId) previewProfile = selectAiModelProfile(state, ctx.modelPolicy, preferredId);
|
|
431
447
|
}
|
|
432
448
|
const toolsSupported = Boolean(previewProfile?.capabilities.includes("tools"));
|
|
433
|
-
const availableSkills = toolsSupported
|
|
449
|
+
const availableSkills = toolsSupported
|
|
450
|
+
? (await aiSkills.list(c.get("accessSubject"), { authority: appAccessAuthority(c) })).filter((skill) => skill.enabled)
|
|
451
|
+
: [];
|
|
434
452
|
const skillCatalog = selectAiSkillCatalog(
|
|
435
453
|
availableSkills.filter((skill) => !skill.manualOnly).map(({ name, description }) => ({ name, description })),
|
|
436
454
|
previewProfile?.contextWindow ?? 0,
|
|
@@ -444,8 +462,8 @@ export const aiRoutes = (() => {
|
|
|
444
462
|
})),
|
|
445
463
|
...(client?.localTools === "ask" ? [createCloudAiLocalBashTool(), ...createCloudAiLocalTools()] : []),
|
|
446
464
|
...(memoryEnabled ? [createCloudAiMemoryTool()] : []),
|
|
447
|
-
...(availableSkills.length ? [createCloudAiLoadSkillTool(c.get("accessSubject"))] : []),
|
|
448
|
-
...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(c.get("accessSubject"))] : []),
|
|
465
|
+
...(availableSkills.length ? [createCloudAiLoadSkillTool(c.get("accessSubject"), appAccessAuthority(c))] : []),
|
|
466
|
+
...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(c.get("accessSubject"), [], appAccessAuthority(c))] : []),
|
|
449
467
|
]
|
|
450
468
|
: [];
|
|
451
469
|
const memoryToolEnabled = tools.some((tool) => tool.def.name === "memory");
|
|
@@ -494,7 +512,7 @@ export const aiRoutes = (() => {
|
|
|
494
512
|
c.header("Cache-Control", "no-store");
|
|
495
513
|
const ctx = await resolveContext(c);
|
|
496
514
|
if (ctx instanceof Response) return ctx;
|
|
497
|
-
setRequestTimeout(c, Math.ceil(
|
|
515
|
+
setRequestTimeout(c, Math.ceil(CAPABILITY_DISCOVERY_TIMEOUT_MS / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
|
|
498
516
|
try {
|
|
499
517
|
const resources = await listAiRecentResources({
|
|
500
518
|
ownerUserId: ctx.ownerUserId,
|
|
@@ -574,7 +592,7 @@ export const aiRoutes = (() => {
|
|
|
574
592
|
const body = c.req.valid("json");
|
|
575
593
|
const project = body.projectId ? await aiProjects.getByShortId(body.projectId, c.get("accessSubject"), "read") : null;
|
|
576
594
|
if (body.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
577
|
-
const draft = await prepareLaunchDraft(body, c.get("accessSubject"));
|
|
595
|
+
const draft = await prepareLaunchDraft(body, c.get("accessSubject"), appAccessAuthority(c));
|
|
578
596
|
if (!draft.ok) return respond(c, draft);
|
|
579
597
|
const requestedCapabilityTools = (body.preloadTools ?? []).filter(
|
|
580
598
|
(requested): requested is { appId: string; kind: "query" | "action"; id: string } => "appId" in requested,
|
|
@@ -582,7 +600,7 @@ export const aiRoutes = (() => {
|
|
|
582
600
|
let catalog: ReturnType<typeof buildAiCapabilityCatalog> = [];
|
|
583
601
|
if (requestedCapabilityTools.length) {
|
|
584
602
|
try {
|
|
585
|
-
catalog = buildAiCapabilityCatalog(await listCapabilities());
|
|
603
|
+
catalog = buildAiCapabilityCatalog(await discoverableAppEntries(appAccessAuthority(c), await listCapabilities()));
|
|
586
604
|
} catch {
|
|
587
605
|
return respond(c, fail(err.internal("The live capability catalog is unavailable.")));
|
|
588
606
|
}
|
|
@@ -613,24 +631,29 @@ export const aiRoutes = (() => {
|
|
|
613
631
|
if (body.allowedTools && preloadTools.some((name) => !body.allowedTools!.includes(name!))) {
|
|
614
632
|
return respond(c, fail(err.badInput("Preloaded tools must be included in allowedTools.")));
|
|
615
633
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
),
|
|
632
|
-
201
|
|
633
|
-
)
|
|
634
|
+
try {
|
|
635
|
+
const conversation = await aiConversations.createConversation({
|
|
636
|
+
requestId: body.requestId,
|
|
637
|
+
ownerUserId: ctx.ownerUserId,
|
|
638
|
+
title: body.title,
|
|
639
|
+
projectId: project?.id,
|
|
640
|
+
draft: draft.data.content,
|
|
641
|
+
preloadTools: preloadTools.map((name) => name!),
|
|
642
|
+
allowedTools: body.allowedTools,
|
|
643
|
+
launchedByAppId: body.launchedByAppId,
|
|
644
|
+
});
|
|
645
|
+
// A replay may return a conversation assigned to a Project since creation.
|
|
646
|
+
const currentProject = conversation.projectId
|
|
647
|
+
? await aiProjects.get(conversation.projectId, c.get("accessSubject"), "read")
|
|
648
|
+
: null;
|
|
649
|
+
if (conversation.projectId && !currentProject) return respond(c, fail(err.notFound("Project")));
|
|
650
|
+
return respond(c, ok(publicConversation(conversation, currentProject?.shortId ?? null)), 201);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
if (error instanceof AiConversationCreationError) {
|
|
653
|
+
return respond(c, fail(error.reason === "conflict" ? err.conflict(error.message) : err.notFound("Conversation")));
|
|
654
|
+
}
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
634
657
|
})
|
|
635
658
|
.get("/conversations/:conversationId", async (c) => {
|
|
636
659
|
const ctx = await resolveContext(c);
|
|
@@ -639,6 +662,24 @@ export const aiRoutes = (() => {
|
|
|
639
662
|
if (!conversation) return notFound(c);
|
|
640
663
|
return respond(c, ok(await conversationDetail(conversation, c.get("accessSubject"))));
|
|
641
664
|
})
|
|
665
|
+
.get(
|
|
666
|
+
"/conversations/:conversationId/submission/:draftRevision",
|
|
667
|
+
v("param", z.object({ conversationId: z.string().regex(AI_SHORT_ID_PATTERN), draftRevision: z.coerce.number().int().min(0) })),
|
|
668
|
+
async (c) => {
|
|
669
|
+
const ctx = await resolveContext(c);
|
|
670
|
+
if (ctx instanceof Response) return ctx;
|
|
671
|
+
const conversation = await loadConversation(c, ctx);
|
|
672
|
+
if (!conversation) return notFound(c);
|
|
673
|
+
if (conversation.projectId && !(await aiProjects.get(conversation.projectId, c.get("accessSubject"), "read"))) return notFound(c);
|
|
674
|
+
const submission = await aiConversations.getDraftSubmission({
|
|
675
|
+
conversationId: conversation.id,
|
|
676
|
+
ownerUserId: ctx.ownerUserId,
|
|
677
|
+
revision: c.req.valid("param").draftRevision,
|
|
678
|
+
});
|
|
679
|
+
// Null is deliberately not a claim that the request was rejected.
|
|
680
|
+
return respond(c, ok({ submission }));
|
|
681
|
+
},
|
|
682
|
+
)
|
|
642
683
|
.get("/conversations/:conversationId/messages", v("query", MessagesPageQuerySchema), async (c) => {
|
|
643
684
|
const ctx = await resolveContext(c);
|
|
644
685
|
if (ctx instanceof Response) return ctx;
|
|
@@ -1220,7 +1261,7 @@ export const aiRoutes = (() => {
|
|
|
1220
1261
|
if (!turn || !callId) return notFound(c);
|
|
1221
1262
|
const target = { conversationId: conversation.id, turnId: turn.id, callId };
|
|
1222
1263
|
// The app's Query may take its whole budget; the request must outlast it.
|
|
1223
|
-
setRequestTimeout(c, Math.ceil(
|
|
1264
|
+
setRequestTimeout(c, Math.ceil(capabilityTimeoutMs("query") / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
|
|
1224
1265
|
try {
|
|
1225
1266
|
const messages = await aiConversations.listTurnMessages({
|
|
1226
1267
|
conversationId: conversation.id,
|
|
@@ -1253,7 +1294,7 @@ export const aiRoutes = (() => {
|
|
|
1253
1294
|
message.message.role === "assistant"
|
|
1254
1295
|
? message.message.content.find((block) => block.type === "tool_call" && block.id === callId)
|
|
1255
1296
|
: undefined;
|
|
1256
|
-
const catalog = buildAiCapabilityCatalog(await listCapabilities());
|
|
1297
|
+
const catalog = buildAiCapabilityCatalog(await discoverableAppEntries(appAccessAuthority(c), await listCapabilities()));
|
|
1257
1298
|
const presentation = message.meta?.toolPresentations?.[callId];
|
|
1258
1299
|
const entry = catalog.find(
|
|
1259
1300
|
(entry) =>
|
|
@@ -1352,8 +1393,51 @@ export const aiRoutes = (() => {
|
|
|
1352
1393
|
if (!conversation) return notFound(c);
|
|
1353
1394
|
const turn = await loadTurn(conversation, c.req.param("turnId"));
|
|
1354
1395
|
if (!turn) return notFound(c);
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1396
|
+
const scope = { conversationId: conversation.id, turnId: turn.id };
|
|
1397
|
+
const actions = await listPendingAiTurnActions(scope);
|
|
1398
|
+
if (!actions.some((action) => action.type === "approval_request"))
|
|
1399
|
+
return respond(c, ok(actions.map((action) => publicPendingAction(action, conversation.shortId, turn.shortId))));
|
|
1400
|
+
const [records, reviews, config] = await Promise.all([
|
|
1401
|
+
aiConversations.listPendingActionRecords(scope),
|
|
1402
|
+
aiConversations.listTurnToolReviews(scope),
|
|
1403
|
+
aiConversations.getTurnRunConfig(scope),
|
|
1404
|
+
]);
|
|
1405
|
+
const locale = config?.kind !== "compact" ? (config?.locale ?? getLocale(c)) : getLocale(c);
|
|
1406
|
+
let capabilities: Promise<Awaited<ReturnType<typeof listCapabilities>> | null> | undefined;
|
|
1407
|
+
const presented = await Promise.all(
|
|
1408
|
+
actions.map(async (action) => {
|
|
1409
|
+
const result = publicPendingAction(action, conversation.shortId, turn.shortId);
|
|
1410
|
+
if (action.type !== "approval_request") return result;
|
|
1411
|
+
const record = records.find((record) => record.callId === action.callId);
|
|
1412
|
+
const request = record?.approvalId ? await approvals.get(record.approvalId, undefined, true) : null;
|
|
1413
|
+
const reviewCallId = record?.kind === "custom_approval" ? action.callId.replace(/-approval-[0-9]+$/, "") : action.callId;
|
|
1414
|
+
const stored = reviews.get(reviewCallId);
|
|
1415
|
+
let review: AiStoredActionReview | null = stored && !isLegacyActionReview(stored) ? stored : null;
|
|
1416
|
+
if (!review && request) {
|
|
1417
|
+
review = pendingApprovalReview({ storedReview: null, details: request.details });
|
|
1418
|
+
if (review && !isLegacyActionReview(review) && !review.ask) {
|
|
1419
|
+
// As the approvals page: the declared reason when the review names none, read once per listing.
|
|
1420
|
+
capabilities ??= listCapabilities().catch(() => null);
|
|
1421
|
+
const manifest = (await capabilities)?.find((entry) => entry.appId === request.appId)?.manifest;
|
|
1422
|
+
review = storedActionReview(review, manifest ? capabilityActionPolicy(manifest, request.action) : undefined);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
const appId = request?.appId;
|
|
1426
|
+
// The name is presentation: a registry that cannot answer leaves the app ID, never the waiting approvals unread.
|
|
1427
|
+
const app = appId && appId !== "assistant" ? await getApp(appId).catch(() => null) : null;
|
|
1428
|
+
return {
|
|
1429
|
+
...result,
|
|
1430
|
+
...(review && !isLegacyActionReview(review) ? { review } : {}),
|
|
1431
|
+
app: {
|
|
1432
|
+
id: appId ?? "assistant",
|
|
1433
|
+
name: app ? resolveAppIdentityPresentation(app, locale).name : appId && appId !== "assistant" ? appId : "Cloud",
|
|
1434
|
+
},
|
|
1435
|
+
chatTitle: conversation.title,
|
|
1436
|
+
...(request ? { expiresAt: request.expiresAt.toISOString() } : {}),
|
|
1437
|
+
};
|
|
1438
|
+
}),
|
|
1439
|
+
);
|
|
1440
|
+
return respond(c, ok(presented));
|
|
1357
1441
|
})
|
|
1358
1442
|
.get("/conversations/:conversationId/stream", async (c) => {
|
|
1359
1443
|
const ctx = await resolveContext(c);
|
package/src/ai/runtime.ts
CHANGED
|
@@ -55,6 +55,7 @@ const log = logger("ai:runtime");
|
|
|
55
55
|
|
|
56
56
|
const AI_WORKER_ID = `worker-${crypto.randomUUID()}`;
|
|
57
57
|
const AI_TURN_HEARTBEAT_MS = 3_000;
|
|
58
|
+
// Local turn orchestration (tools and approval waits included), not a model-provider quota.
|
|
58
59
|
const AI_TURN_WORKER_CONCURRENCY = 8;
|
|
59
60
|
const AI_TURN_MAX_ATTEMPTS = 5;
|
|
60
61
|
|