@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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { APPROVALS_PAGE_SIZE } from "../contracts/approvals";
|
|
4
|
+
import {
|
|
5
|
+
CapabilityActionCallReviewSchema,
|
|
6
|
+
CapabilityActionReviewDataSchema,
|
|
7
|
+
CapabilitySemanticLinkSchema,
|
|
8
|
+
} from "../contracts/capabilities";
|
|
9
|
+
import { openRequests } from "../services/approvals/list";
|
|
10
|
+
import { logger } from "../services/logging";
|
|
11
|
+
import { reviewShortText, toolActionCard, toolBlockReview } from "./chat/action-card";
|
|
12
|
+
import { aiToolIcon } from "./chat/message-utils";
|
|
13
|
+
import { legacyOutcomeWords } from "./chat/tool-action-words";
|
|
14
|
+
import { type AiTurnBlock, toolBlockId } from "./protocol";
|
|
15
|
+
|
|
16
|
+
export type AiOpenQuestion = {
|
|
17
|
+
/** The approval request id (approvals.requests.id). */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Short id of the chat the question waits in. */
|
|
20
|
+
conversationId: string;
|
|
21
|
+
/** The chat's current title, at most 200 code points; chat content, never Cloud's words. */
|
|
22
|
+
conversationTitle: string | null;
|
|
23
|
+
/** The question's data-chat-anchor: the id of the tool block the chat renders it in. */
|
|
24
|
+
anchor: string;
|
|
25
|
+
/** The question's short form, plain text, at most 160 code points. */
|
|
26
|
+
short: string;
|
|
27
|
+
/** The app the question is about, as the card shows it. */
|
|
28
|
+
app: { name: string; icon: string; accent?: string };
|
|
29
|
+
createdAt: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type AiOpenQuestions = {
|
|
33
|
+
items: AiOpenQuestion[];
|
|
34
|
+
/** All open questions of the person, across pages. */
|
|
35
|
+
count: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type QuestionRow = {
|
|
39
|
+
id: string;
|
|
40
|
+
conversationId: string;
|
|
41
|
+
conversationTitle: string | null;
|
|
42
|
+
displayCallId: string;
|
|
43
|
+
appId: string;
|
|
44
|
+
sentence: string;
|
|
45
|
+
toolName: string;
|
|
46
|
+
createdAt: Date;
|
|
47
|
+
block: Extract<AiTurnBlock, { kind: "tool" }> | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const log = logger("ai:open-questions");
|
|
51
|
+
const legacyReviewSchema = z.object({
|
|
52
|
+
question: z.string().min(1),
|
|
53
|
+
rows: z.array(z.object({ label: z.string(), value: z.string(), format: z.enum(["date", "date-time"]).optional() })),
|
|
54
|
+
markdown: z.string().optional(),
|
|
55
|
+
summary: z.string().optional(),
|
|
56
|
+
links: z.array(CapabilitySemanticLinkSchema).optional(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const ownedRequests = (ownerUserId: string) => sql`
|
|
60
|
+
SELECT r.* FROM (${openRequests(ownerUserId)}) r
|
|
61
|
+
WHERE EXISTS (
|
|
62
|
+
SELECT 1 FROM ai.pending_actions p JOIN ai.conversations c ON c.id=p.conversation_id
|
|
63
|
+
WHERE p.approval_id=r.id AND p.status='pending' AND p.kind IN ('approval','custom_approval')
|
|
64
|
+
AND c.created_by_user_id=${ownerUserId}::uuid
|
|
65
|
+
)`;
|
|
66
|
+
|
|
67
|
+
/** One bounded, owner-scoped snapshot, using exactly the chat's stored question card. */
|
|
68
|
+
export const listAiOpenQuestions = async (input: { ownerUserId: string; locale: string; timeZone: string }): Promise<AiOpenQuestions> => {
|
|
69
|
+
const [counts] = await sql<{ count: number }[]>`
|
|
70
|
+
SELECT count(*)::int AS count FROM (${ownedRequests(input.ownerUserId)}) r`;
|
|
71
|
+
const rows = await sql<QuestionRow[]>`
|
|
72
|
+
WITH page AS (
|
|
73
|
+
SELECT * FROM (${ownedRequests(input.ownerUserId)}) r
|
|
74
|
+
ORDER BY r.created_at DESC, r.id DESC LIMIT ${APPROVALS_PAGE_SIZE}
|
|
75
|
+
)
|
|
76
|
+
SELECT r.id, r.app_id AS "appId", r.sentence, r.created_at AS "createdAt",
|
|
77
|
+
question.short_id AS "conversationId", question.title AS "conversationTitle",
|
|
78
|
+
question.display_call_id AS "displayCallId", question.tool_name AS "toolName", question.block
|
|
79
|
+
FROM page r JOIN LATERAL (
|
|
80
|
+
SELECT c.short_id, c.title, p.tool_name,
|
|
81
|
+
CASE WHEN p.kind='custom_approval'
|
|
82
|
+
THEN COALESCE(substring(p.call_id FROM '^(.*)-approval-[0-9]+$'), p.call_id)
|
|
83
|
+
ELSE p.call_id END AS display_call_id,
|
|
84
|
+
(SELECT block FROM jsonb_array_elements(
|
|
85
|
+
CASE WHEN jsonb_typeof(t.live_blocks)='array' THEN t.live_blocks ELSE '[]'::jsonb END
|
|
86
|
+
) AS blocks(block)
|
|
87
|
+
WHERE block->>'kind'='tool' AND block->>'callId'=p.call_id LIMIT 1) AS block
|
|
88
|
+
FROM ai.pending_actions p JOIN ai.conversations c ON c.id=p.conversation_id
|
|
89
|
+
JOIN ai.turns t ON t.id=p.turn_id
|
|
90
|
+
WHERE p.approval_id=r.id AND p.status='pending' AND p.kind IN ('approval','custom_approval')
|
|
91
|
+
AND c.created_by_user_id=${input.ownerUserId}::uuid
|
|
92
|
+
ORDER BY p.created_at DESC, p.id DESC LIMIT 1
|
|
93
|
+
) question ON true
|
|
94
|
+
ORDER BY r.created_at DESC, r.id DESC`;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
count: counts?.count ?? 0,
|
|
98
|
+
items: rows.map((row) => openQuestionFromRow(row, input)),
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Stop at the payload bound without splitting a Unicode code point or copying the rest of a legacy message. */
|
|
103
|
+
const boundedText = (text: string, max: number): string => {
|
|
104
|
+
const points: string[] = [];
|
|
105
|
+
for (const point of text) {
|
|
106
|
+
if (points.length === max) return `${points.slice(0, max - 1).join("")}…`;
|
|
107
|
+
points.push(point);
|
|
108
|
+
}
|
|
109
|
+
return text;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Present one stored question without database access. */
|
|
113
|
+
export const openQuestionFromRow = (row: QuestionRow, input: { locale: string; timeZone: string }): AiOpenQuestion => {
|
|
114
|
+
const block = row.block;
|
|
115
|
+
const presentation = block?.presentation;
|
|
116
|
+
const question: AiOpenQuestion = {
|
|
117
|
+
id: row.id,
|
|
118
|
+
conversationId: row.conversationId,
|
|
119
|
+
conversationTitle: row.conversationTitle === null ? null : boundedText(row.conversationTitle, 200),
|
|
120
|
+
// The id the chat renders the card under: a custom approval's card carries its parent call, also when a stored
|
|
121
|
+
// block still has the approval's own id (see normalizeCustomApprovalBlocks in client/projection.ts).
|
|
122
|
+
anchor: toolBlockId(row.displayCallId),
|
|
123
|
+
short: boundedText(row.sentence.split(/\r?\n/, 1)[0] ?? "", 160),
|
|
124
|
+
app: {
|
|
125
|
+
name: typeof presentation?.appName === "string" ? presentation.appName : block ? "Cloud" : row.appId,
|
|
126
|
+
icon: aiToolIcon(
|
|
127
|
+
typeof block?.name === "string" ? block.name : row.toolName,
|
|
128
|
+
typeof presentation?.appIcon === "string" ? presentation.appIcon : undefined,
|
|
129
|
+
),
|
|
130
|
+
...(typeof presentation?.appAccent === "string" ? { accent: presentation.appAccent } : {}),
|
|
131
|
+
},
|
|
132
|
+
createdAt: row.createdAt.toISOString(),
|
|
133
|
+
};
|
|
134
|
+
if (!block) return question;
|
|
135
|
+
try {
|
|
136
|
+
if (
|
|
137
|
+
typeof block.id !== "string" ||
|
|
138
|
+
!block.id ||
|
|
139
|
+
typeof block.name !== "string" ||
|
|
140
|
+
!block.name ||
|
|
141
|
+
typeof block.callId !== "string" ||
|
|
142
|
+
!["running", "awaiting_approval", "awaiting_client", "completed", "failed", "rejected"].includes(block.status)
|
|
143
|
+
)
|
|
144
|
+
throw new Error("Malformed tool block");
|
|
145
|
+
if (block.review == null && block.approval?.review != null) CapabilityActionReviewDataSchema.parse(block.approval.review);
|
|
146
|
+
const review = toolBlockReview(block);
|
|
147
|
+
if (review !== null) {
|
|
148
|
+
if ("legacy" in review) legacyReviewSchema.parse(review.legacy);
|
|
149
|
+
else CapabilityActionCallReviewSchema.parse(review);
|
|
150
|
+
}
|
|
151
|
+
const card = toolActionCard(block, {
|
|
152
|
+
locale: input.locale,
|
|
153
|
+
timeZone: input.timeZone,
|
|
154
|
+
outcome: legacyOutcomeWords(block, input.locale, input.timeZone),
|
|
155
|
+
});
|
|
156
|
+
const short =
|
|
157
|
+
review && !("legacy" in review)
|
|
158
|
+
? reviewShortText(review, { locale: input.locale, timeZone: input.timeZone, app: card.app })
|
|
159
|
+
: card.questionText;
|
|
160
|
+
if (
|
|
161
|
+
typeof short !== "string" ||
|
|
162
|
+
typeof card.app.name !== "string" ||
|
|
163
|
+
typeof card.app.icon !== "string" ||
|
|
164
|
+
(card.app.accent !== undefined && typeof card.app.accent !== "string")
|
|
165
|
+
)
|
|
166
|
+
throw new Error("Malformed question card");
|
|
167
|
+
return { ...question, short: boundedText(short, 160), app: card.app };
|
|
168
|
+
} catch {
|
|
169
|
+
log.warn("Failed to present open question", { approvalId: row.id, conversationId: row.conversationId });
|
|
170
|
+
return question;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
@@ -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" } : {}),
|
|
@@ -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";
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
3
|
+
import { listCapabilities } from "../_internal/registry";
|
|
4
|
+
import { type CapabilityDispatchParams, dispatchCapability, QUERY_TIMEOUT_MS } from "../api/capabilities";
|
|
5
|
+
import {
|
|
6
|
+
CloudResourceRefSchema,
|
|
7
|
+
capabilityResultSchema,
|
|
8
|
+
cloudResourceRefAppId,
|
|
9
|
+
LinkedCloudResourceReferenceSchema,
|
|
10
|
+
resolveCapabilityResourceReader,
|
|
11
|
+
} from "../contracts/capabilities";
|
|
12
|
+
import type { CapabilityRegistryEntry } from "../contracts/registry";
|
|
13
|
+
import type { RequestAuthority } from "../server";
|
|
14
|
+
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
15
|
+
import { LOCALE_HEADER } from "../shared/locale";
|
|
16
|
+
import { CODE_SOURCE_TOOLS } from "./code-source-contracts";
|
|
17
|
+
import { createCodeSourceTool } from "./code-source-tools";
|
|
18
|
+
import { RECENT_SENT_REF_LIMIT } from "./recent-sent-refs";
|
|
19
|
+
import { addCloudResourceHrefs } from "./resource-refs";
|
|
20
|
+
import { aiSkills } from "./skills";
|
|
21
|
+
import { aiConversations } from "./store";
|
|
22
|
+
import type { AiConversationService, AiRecentResource } from "./types";
|
|
23
|
+
|
|
24
|
+
export const AiRecentResourceSchema: z.ZodType<AiRecentResource> = z
|
|
25
|
+
.object({
|
|
26
|
+
ref: CloudResourceRefSchema,
|
|
27
|
+
title: LinkedCloudResourceReferenceSchema.shape.title.unwrap(),
|
|
28
|
+
icon: LinkedCloudResourceReferenceSchema.shape.icon,
|
|
29
|
+
href: LinkedCloudResourceReferenceSchema.shape.href,
|
|
30
|
+
kind: z.string().min(1).max(120),
|
|
31
|
+
preview: z.string().max(160).optional(),
|
|
32
|
+
appName: z.string().min(1).max(120),
|
|
33
|
+
count: z.number().int().positive(),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
|
|
37
|
+
const ReaderResult = capabilityResultSchema(z.unknown(), { consumer: true });
|
|
38
|
+
const StudioData = z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
title: LinkedCloudResourceReferenceSchema.shape.title.unwrap(),
|
|
41
|
+
icon: LinkedCloudResourceReferenceSchema.shape.icon,
|
|
42
|
+
description: z.string().optional(),
|
|
43
|
+
});
|
|
44
|
+
const studioReader = async (input: {
|
|
45
|
+
id: string;
|
|
46
|
+
conversationId: string;
|
|
47
|
+
authority: RequestAuthority;
|
|
48
|
+
locale: string;
|
|
49
|
+
signal: AbortSignal;
|
|
50
|
+
}): Promise<unknown> => {
|
|
51
|
+
const tool = createCodeSourceTool("code_read");
|
|
52
|
+
if (tool.location !== "server") throw new Error("Studio reader must run on the server.");
|
|
53
|
+
return tool.run(CODE_SOURCE_TOOLS.code_read.input.parse({ id: input.id }), {
|
|
54
|
+
actor: input.authority.actor,
|
|
55
|
+
conversationId: input.conversationId,
|
|
56
|
+
locale: input.locale,
|
|
57
|
+
signal: input.signal,
|
|
58
|
+
requestApproval: async () => false,
|
|
59
|
+
requestClientTool: async () => {
|
|
60
|
+
throw new Error("Recent resources cannot call client tools.");
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type AiRecentResourceDependencies = {
|
|
66
|
+
store?: Pick<AiConversationService, "listRecentSentResourceRefs">;
|
|
67
|
+
listRegistry?: () => Promise<CapabilityRegistryEntry[]>;
|
|
68
|
+
dispatch?: (params: CapabilityDispatchParams) => Promise<Response>;
|
|
69
|
+
skills?: Pick<typeof aiSkills, "list">;
|
|
70
|
+
studioReader?: typeof studioReader;
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const german = (locale: string) => locale.toLowerCase().split("-")[0] === "de";
|
|
75
|
+
/** Skills and Studio apps belong to the Assistant, which has no capability presentation to read its name from. */
|
|
76
|
+
const assistantName = (locale: string) => (german(locale) ? "Assistent" : "Assistant");
|
|
77
|
+
|
|
78
|
+
/** First line only; reserve one character for the ellipsis and avoid splitting words. */
|
|
79
|
+
export const recentResourcePreview = (value: string | undefined): string | undefined => {
|
|
80
|
+
const line = value?.split(/\r\n?|\n/, 1)[0]?.trim();
|
|
81
|
+
if (!line) return undefined;
|
|
82
|
+
if (line.length <= 160) return line;
|
|
83
|
+
const clipped = line.slice(0, 159);
|
|
84
|
+
const boundary = clipped.search(/\s+\S*$/);
|
|
85
|
+
return `${(boundary > 0 ? clipped.slice(0, boundary) : clipped).trimEnd()}…`;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Enforce the shared budget even when a dependency ignores cancellation. */
|
|
89
|
+
const withinBudget = async <T>(signal: AbortSignal, run: () => Promise<T>): Promise<T> => {
|
|
90
|
+
signal.throwIfAborted();
|
|
91
|
+
let abort: () => void = () => {};
|
|
92
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
93
|
+
abort = () => reject(signal.reason);
|
|
94
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
return await Promise.race([run(), aborted]);
|
|
98
|
+
} finally {
|
|
99
|
+
signal.removeEventListener("abort", abort);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const listAiRecentResources = async (input: {
|
|
104
|
+
ownerUserId: string;
|
|
105
|
+
authority: RequestAuthority;
|
|
106
|
+
locale: string;
|
|
107
|
+
signal: AbortSignal;
|
|
108
|
+
dependencies?: AiRecentResourceDependencies;
|
|
109
|
+
}): Promise<AiRecentResource[]> => {
|
|
110
|
+
const deps = input.dependencies ?? {};
|
|
111
|
+
const signal = AbortSignal.any([input.signal, AbortSignal.timeout(deps.timeoutMs ?? QUERY_TIMEOUT_MS)]);
|
|
112
|
+
// Unreadable history is no answer, not an empty one: the request fails, so the browser asks again at its next opening.
|
|
113
|
+
const sent = await withinBudget(signal, () =>
|
|
114
|
+
(deps.store ?? aiConversations).listRecentSentResourceRefs({ ownerUserId: input.ownerUserId }),
|
|
115
|
+
);
|
|
116
|
+
if (!sent.length || signal.aborted) return [];
|
|
117
|
+
const candidates = sent.slice(0, RECENT_SENT_REF_LIMIT);
|
|
118
|
+
const skillIds = candidates.flatMap(({ ref }) => (ref.type === "core.ai.skill" ? [ref.id] : []));
|
|
119
|
+
// Exactly the sent Skills, so a large Skill catalog never pushes one out; failing, they are left out like a reader.
|
|
120
|
+
const skills = skillIds.length
|
|
121
|
+
? withinBudget(signal, () => (deps.skills ?? aiSkills).list(input.authority.accessSubject, { shortIds: skillIds })).catch(() => [])
|
|
122
|
+
: Promise.resolve([]);
|
|
123
|
+
const registry = candidates.some(({ ref }) => ref.type !== "core.ai.skill" && ref.type !== "assistant.artifact")
|
|
124
|
+
? withinBudget(signal, deps.listRegistry ?? listCapabilities).catch(() => [])
|
|
125
|
+
: Promise.resolve([]);
|
|
126
|
+
const resources = await Promise.all(
|
|
127
|
+
candidates.map(async (candidate): Promise<AiRecentResource | null> => {
|
|
128
|
+
try {
|
|
129
|
+
signal.throwIfAborted();
|
|
130
|
+
const { ref, count } = candidate;
|
|
131
|
+
if (ref.type === "core.ai.skill") {
|
|
132
|
+
const skill = (await skills).find((skill) => skill.shortId === ref.id && skill.enabled !== false);
|
|
133
|
+
if (!skill) return null;
|
|
134
|
+
return {
|
|
135
|
+
ref,
|
|
136
|
+
count,
|
|
137
|
+
title: skill.name.slice(0, 500),
|
|
138
|
+
icon: "ti ti-sparkles",
|
|
139
|
+
kind: "Skill",
|
|
140
|
+
preview: recentResourcePreview(skill.description),
|
|
141
|
+
appName: assistantName(input.locale),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (ref.type === "assistant.artifact") {
|
|
145
|
+
const { actor, accessSubject, credentialKind, scopes } = input.authority;
|
|
146
|
+
// code_read reconstructs a user session authority. Only use it when that is
|
|
147
|
+
// exactly this request's authority; never widen scoped/delegated credentials.
|
|
148
|
+
if (
|
|
149
|
+
credentialKind !== "session" ||
|
|
150
|
+
scopes.length ||
|
|
151
|
+
actor.kind !== "user" ||
|
|
152
|
+
actor.delegation ||
|
|
153
|
+
actor.sessionKind === "app" ||
|
|
154
|
+
accessSubject.type !== "user" ||
|
|
155
|
+
accessSubject.userId !== actor.user.id
|
|
156
|
+
)
|
|
157
|
+
return null;
|
|
158
|
+
const answer = ReaderResult.parse(
|
|
159
|
+
await withinBudget(signal, () =>
|
|
160
|
+
(deps.studioReader ?? studioReader)({
|
|
161
|
+
id: ref.id,
|
|
162
|
+
conversationId: candidate.conversationId,
|
|
163
|
+
authority: input.authority,
|
|
164
|
+
locale: input.locale,
|
|
165
|
+
signal,
|
|
166
|
+
}),
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
signal.throwIfAborted();
|
|
170
|
+
const data = StudioData.parse(answer.data);
|
|
171
|
+
if (data.id !== ref.id || !data.title.trim()) return null;
|
|
172
|
+
const matched = answer.refs?.find((entry) => entry.type === ref.type && entry.id === ref.id);
|
|
173
|
+
const ownRef = matched ? LinkedCloudResourceReferenceSchema.parse(matched) : undefined;
|
|
174
|
+
const href = ownRef?.href ?? answer.links?.find((link) => link.rel === "open")?.href;
|
|
175
|
+
return {
|
|
176
|
+
ref,
|
|
177
|
+
count,
|
|
178
|
+
title: data.title,
|
|
179
|
+
icon: ownRef?.icon ?? data.icon,
|
|
180
|
+
href,
|
|
181
|
+
preview: recentResourcePreview(ownRef?.preview ?? data.description),
|
|
182
|
+
kind: german(input.locale) ? "Studio-App" : "Studio app",
|
|
183
|
+
appName: assistantName(input.locale),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const apps = await registry;
|
|
187
|
+
signal.throwIfAborted();
|
|
188
|
+
const appId = cloudResourceRefAppId(ref);
|
|
189
|
+
const app = apps.find((app) => app.appId === appId);
|
|
190
|
+
const reader = app && resolveCapabilityResourceReader(app.manifest, ref);
|
|
191
|
+
if (!app || !reader) return null;
|
|
192
|
+
const response = await withinBudget(signal, () =>
|
|
193
|
+
(deps.dispatch ?? dispatchCapability)({
|
|
194
|
+
request: new Request("http://localhost/api/ai/resources/recent", {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: { [LOCALE_HEADER]: input.locale, "x-request-id": crypto.randomUUID() },
|
|
197
|
+
signal,
|
|
198
|
+
}),
|
|
199
|
+
kind: "queries",
|
|
200
|
+
origin: "assistant",
|
|
201
|
+
appId: app.appId,
|
|
202
|
+
capabilityId: reader.localId,
|
|
203
|
+
input: { id: ref.id },
|
|
204
|
+
authority: input.authority,
|
|
205
|
+
locale: input.locale,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
if (!response.ok) return null;
|
|
209
|
+
const answer = ReaderResult.parse(addCloudResourceHrefs(await withinBudget(signal, () => response.json()), apps));
|
|
210
|
+
const matched = answer.refs?.find((entry) => entry.type === ref.type && entry.id === ref.id);
|
|
211
|
+
const ownRef = matched ? LinkedCloudResourceReferenceSchema.parse(matched) : undefined;
|
|
212
|
+
if (!ownRef?.title?.trim()) return null;
|
|
213
|
+
signal.throwIfAborted();
|
|
214
|
+
const manifest = resolveCapabilityManifestPresentation(app.manifest, app.presentation, input.locale);
|
|
215
|
+
const type = manifest.types.find((type) => `${app.appId}.${type.localId}` === ref.type);
|
|
216
|
+
if (!type) return null;
|
|
217
|
+
const presentation = resolveAppIdentityPresentation(
|
|
218
|
+
{ name: app.appName, description: app.appDescription, presentation: app.appPresentation },
|
|
219
|
+
input.locale,
|
|
220
|
+
);
|
|
221
|
+
return {
|
|
222
|
+
ref,
|
|
223
|
+
count,
|
|
224
|
+
title: ownRef.title,
|
|
225
|
+
icon: (ownRef.icon ?? type.icon ?? app.appIcon)?.slice(0, 120),
|
|
226
|
+
href: ownRef.href,
|
|
227
|
+
kind: type.title.slice(0, 120),
|
|
228
|
+
preview: recentResourcePreview(ownRef.preview),
|
|
229
|
+
appName: presentation.name.slice(0, 120),
|
|
230
|
+
};
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
return resources.flatMap((resource) => {
|
|
237
|
+
const parsed = AiRecentResourceSchema.safeParse(resource);
|
|
238
|
+
return parsed.success ? [parsed.data] : [];
|
|
239
|
+
});
|
|
240
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parseAiResourceMarker } from "./resource-markers";
|
|
2
|
+
import type { AiRecentSentResourceRef } from "./types";
|
|
3
|
+
|
|
4
|
+
export const RECENT_SENT_REF_LIMIT = 8;
|
|
5
|
+
// Look back 25 messages/conversations per displayed ref: useful sparse history without
|
|
6
|
+
// scanning the owner's lifetime. insertMessageLocked (used by submitChatTurn and steering)
|
|
7
|
+
// bumps conversation.updated_at, so newly sent turns enter this indexed window.
|
|
8
|
+
export const RECENT_SENT_MESSAGE_SCAN_LIMIT = RECENT_SENT_REF_LIMIT * 25;
|
|
9
|
+
export const RECENT_SENT_CONVERSATION_SCAN_LIMIT = RECENT_SENT_MESSAGE_SCAN_LIMIT;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Rows arrive newest first and contain only marker text, never whole message bodies. Forking or copying a chat copies
|
|
13
|
+
* its messages with their short IDs, so a message ID counts once, however many chats hold a copy of it.
|
|
14
|
+
*/
|
|
15
|
+
export const collectRecentSentResourceRefs = (
|
|
16
|
+
messages: readonly { conversationId: string; messageId: string; lastSentAt: string; markers: readonly string[] }[],
|
|
17
|
+
): AiRecentSentResourceRef[] => {
|
|
18
|
+
const refs = new Map<string, AiRecentSentResourceRef>();
|
|
19
|
+
const counted = new Set<string>();
|
|
20
|
+
for (const message of messages) {
|
|
21
|
+
if (counted.has(message.messageId)) continue;
|
|
22
|
+
counted.add(message.messageId);
|
|
23
|
+
const seen = new Set<string>();
|
|
24
|
+
for (const text of message.markers) {
|
|
25
|
+
const marker = parseAiResourceMarker(text);
|
|
26
|
+
if (!marker) continue;
|
|
27
|
+
const key = JSON.stringify([marker.ref.type, marker.ref.id]);
|
|
28
|
+
if (seen.has(key)) continue;
|
|
29
|
+
seen.add(key);
|
|
30
|
+
const existing = refs.get(key);
|
|
31
|
+
if (existing) existing.count++;
|
|
32
|
+
else if (refs.size < RECENT_SENT_REF_LIMIT) {
|
|
33
|
+
refs.set(key, { ref: marker.ref, count: 1, lastSentAt: message.lastSentAt, conversationId: message.conversationId });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return [...refs.values()];
|
|
38
|
+
};
|