@k2b/cloud 0.29.1 → 0.31.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 +4 -2
- package/src/_internal/app-presentation.ts +22 -1
- package/src/_internal/app-roles.ts +14 -0
- package/src/_internal/capabilities.ts +113 -5
- package/src/_internal/capability-sentences.ts +413 -0
- package/src/_internal/define-app.ts +24 -5
- package/src/_internal/registry-validation.ts +5 -0
- package/src/_internal/registry.ts +50 -6
- package/src/_internal/widget-declarations.ts +53 -0
- package/src/access/PermissionEditor.tsx +43 -24
- package/src/ai/app-skill-store.ts +298 -0
- package/src/ai/app-skills.ts +105 -0
- package/src/ai/approval-routes.ts +40 -4
- package/src/ai/approvals.ts +68 -10
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +68 -9
- package/src/ai/chart-block.ts +380 -0
- package/src/ai/chat/action-sentences.ts +49 -0
- package/src/ai/chat/blocks.tsx +97 -40
- package/src/ai/chat/chart-block.tsx +79 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
- package/src/ai/chat/message-actions.tsx +3 -1
- package/src/ai/chat/message-utils.ts +3 -0
- package/src/ai/chat/messages.ts +26 -2
- package/src/ai/chat/tool-groups.ts +66 -12
- package/src/ai/chat/tool-names.ts +6 -2
- package/src/ai/chat/turn-layout.ts +10 -2
- package/src/ai/chat/turn-view.tsx +123 -36
- package/src/ai/chat/web-tools.tsx +95 -1
- package/src/ai/client/controller.ts +6 -3
- package/src/ai/code-runtime-tools.ts +109 -61
- package/src/ai/code-source-contracts.ts +25 -21
- package/src/ai/code-source-tools.ts +3 -1
- package/src/ai/default-tool-contracts.ts +198 -0
- package/src/ai/default-tools.ts +25 -1
- package/src/ai/executor.ts +189 -35
- package/src/ai/fetch-file-tool.ts +41 -4
- package/src/ai/firecrawl-tools.ts +87 -4
- package/src/ai/index.ts +17 -1
- package/src/ai/migrate.ts +172 -14
- package/src/ai/protocol.ts +22 -1
- package/src/ai/routes.ts +11 -0
- package/src/ai/runtime-tools.ts +1 -0
- package/src/ai/runtime.ts +46 -7
- package/src/ai/skill-content.ts +54 -0
- package/src/ai/skills-routes.ts +22 -13
- package/src/ai/skills.ts +177 -231
- package/src/ai/store.ts +66 -1
- package/src/ai/tool-audit.ts +11 -1
- package/src/ai/tool-sentences.ts +38 -0
- package/src/ai/tools.ts +91 -3
- package/src/ai/types.ts +47 -1
- package/src/ai/web-provenance.ts +103 -0
- package/src/api/admin-ai-skills.ts +35 -16
- package/src/api/capabilities.ts +3 -2
- package/src/api/fanout.ts +128 -0
- package/src/api/help.ts +8 -3
- package/src/api/mcp.ts +4 -1
- package/src/api/search.ts +31 -106
- package/src/api/widgets.ts +291 -101
- package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/command-shortcuts.ts +21 -6
- package/src/browser/ndjson.ts +34 -0
- package/src/browser/reload.ts +46 -0
- package/src/browser/search-stream.ts +4 -26
- package/src/browser/spotlight-position.ts +4 -1
- package/src/browser/widgets.ts +30 -0
- package/src/capabilities/executions.ts +3 -0
- package/src/capabilities/server.ts +20 -2
- package/src/cli/access.ts +48 -14
- package/src/cli/admin/ai-skills.ts +59 -39
- package/src/cli/commands.ts +20 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/plugin.ts +30 -0
- package/src/contracts/announcement-cookie.ts +62 -0
- package/src/contracts/announcements.ts +14 -52
- package/src/contracts/app.ts +34 -2
- package/src/contracts/capabilities.ts +24 -0
- package/src/contracts/pwa-paths.ts +7 -2
- package/src/contracts/pwa.ts +2 -7
- package/src/contracts/registry.ts +10 -1
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +67 -2
- package/src/desktop/solid.tsx +6 -7
- package/src/index.ts +1 -0
- package/src/server/help.ts +8 -1
- package/src/server/index.ts +1 -0
- package/src/server/widget-request.ts +27 -0
- package/src/services/accounts/users.ts +15 -0
- package/src/services/approvals/index.ts +225 -0
- package/src/services/approvals/migrate.ts +33 -0
- package/src/services/help/index.ts +18 -5
- package/src/services/help/types.ts +3 -2
- package/src/services/identity/constants.ts +2 -0
- package/src/services/identity/key-ring.ts +123 -42
- package/src/services/identity/runtime-config.ts +13 -2
- package/src/shared/app-presentation.ts +11 -0
- package/src/shared/email-html.ts +43 -8
- package/src/ssr/AdminLayout.tsx +4 -7
- package/src/ssr/AppLaunchpad.island.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +12 -3
- package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
- package/src/ssr/GlobalSearchDialog.tsx +1 -43
- package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
- package/src/ssr/Layout.tsx +38 -14
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +17 -20
- package/src/ssr/LayoutHelp.tsx +1 -1
- package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
- package/src/ssr/LayoutRail.tsx +1 -2
- package/src/ssr/PageTrail.tsx +23 -0
- package/src/ssr/PageTrailRow.island.tsx +20 -0
- package/src/ssr/PageTrailRuntime.island.tsx +26 -0
- package/src/ssr/ProfilePreferences.island.tsx +7 -3
- package/src/ssr/PwaRuntime.island.tsx +1 -1
- package/src/ssr/app-navigation.ts +5 -7
- package/src/ssr/global-search-host.tsx +55 -0
- package/src/ssr/help.ts +3 -3
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/layout-runtime.ts +60 -0
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +313 -0
- package/src/ssr/page-trail.browser-harness.tsx +216 -0
- package/src/ssr/page-trail.ts +59 -0
- package/src/ssr/platform-messages.ts +4 -0
- package/src/ssr/rail-context.ts +4 -3
- package/src/ssr/runtime.ts +14 -1
- package/src/styles/effects.css +21 -4
- package/src/styles/global.css +35 -2
- package/src/styles/utilities-layout.css +24 -1
- package/src/styles/utilities-navigation.css +6 -18
- package/src/ai/code-mode-skill.ts +0 -114
- package/src/ai/data-analysis-skill.ts +0 -10
- package/src/ai/grids-skill.ts +0 -158
- package/src/ai/skill-seeds.ts +0 -560
|
@@ -5,8 +5,15 @@ import { listCapabilities } from "../_internal/registry";
|
|
|
5
5
|
import { ErrorResponseSchema } from "../contracts";
|
|
6
6
|
import type { CapabilityRegistryEntry } from "../contracts/registry";
|
|
7
7
|
import { type AuthContext, auth, expectUserBackedActor, getLocale, jsonResponse, rateLimit, requiresAuth, v } from "../server";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
AI_WEBSITE_APPROVAL_TOOL,
|
|
10
|
+
type AiToolApprovalPreference,
|
|
11
|
+
listAiToolApprovalPreferences,
|
|
12
|
+
parseAiWebsiteApprovalScope,
|
|
13
|
+
revokeAiToolApprovalPreference,
|
|
14
|
+
} from "./approvals";
|
|
9
15
|
import { buildAiCapabilityCatalog } from "./capabilities";
|
|
16
|
+
import { aiConversations } from "./store";
|
|
10
17
|
|
|
11
18
|
export type AiApprovalPreferenceView = AiToolApprovalPreference & {
|
|
12
19
|
title: string;
|
|
@@ -16,6 +23,8 @@ export type AiApprovalPreferenceView = AiToolApprovalPreference & {
|
|
|
16
23
|
icon: string;
|
|
17
24
|
accent?: string;
|
|
18
25
|
} | null;
|
|
26
|
+
/** For a website approval: the exact origin it lets code read, and the Studio app it is limited to, if any. */
|
|
27
|
+
website: { origin: string; resourceId: string | null } | null;
|
|
19
28
|
};
|
|
20
29
|
|
|
21
30
|
const AiApprovalPreferenceSchema = z.object({
|
|
@@ -24,6 +33,7 @@ const AiApprovalPreferenceSchema = z.object({
|
|
|
24
33
|
approvalScope: z.string(),
|
|
25
34
|
createdAt: z.iso.datetime(),
|
|
26
35
|
lastUsedAt: z.iso.datetime().nullable(),
|
|
36
|
+
conversationId: z.uuid().nullable(),
|
|
27
37
|
expiresAt: z.iso.datetime().nullable(),
|
|
28
38
|
title: z.string(),
|
|
29
39
|
app: z
|
|
@@ -34,20 +44,29 @@ const AiApprovalPreferenceSchema = z.object({
|
|
|
34
44
|
accent: z.string().optional(),
|
|
35
45
|
})
|
|
36
46
|
.nullable(),
|
|
47
|
+
website: z.object({ origin: z.string(), resourceId: z.string().nullable() }).nullable(),
|
|
37
48
|
});
|
|
38
49
|
|
|
39
50
|
const AiApprovalPreferenceListSchema = z.object({ approvals: z.array(AiApprovalPreferenceSchema) });
|
|
40
51
|
const AiApprovalPreferenceParamSchema = z.object({ preferenceId: z.uuid() });
|
|
52
|
+
const AiApprovalPreferenceQuerySchema = z.object({ conversation: z.string().min(1).max(80).optional() });
|
|
41
53
|
const AiApprovalPreferenceDeleteSchema = z.object({ deleted: z.literal(true) });
|
|
42
54
|
|
|
43
55
|
type AiApprovalRouteDependencies = {
|
|
44
56
|
limit?: MiddlewareHandler<AuthContext>;
|
|
45
57
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
46
|
-
listPreferences?: (actorUserId: string) => Promise<AiToolApprovalPreference[]>;
|
|
58
|
+
listPreferences?: (actorUserId: string, options: { conversationId?: string }) => Promise<AiToolApprovalPreference[]>;
|
|
47
59
|
revokePreference?: (actorUserId: string, preferenceId: string) => Promise<boolean>;
|
|
48
60
|
listCapabilities?: () => Promise<CapabilityRegistryEntry[]>;
|
|
61
|
+
/** Resolves a chat the user owns by its ID or short ID. */
|
|
62
|
+
findConversation?: (ownerUserId: string, conversation: string) => Promise<{ id: string } | null>;
|
|
49
63
|
};
|
|
50
64
|
|
|
65
|
+
const findOwnConversation = async (ownerUserId: string, conversation: string) =>
|
|
66
|
+
z.uuid().safeParse(conversation).success
|
|
67
|
+
? aiConversations.getConversation({ conversationId: conversation, ownerUserId })
|
|
68
|
+
: aiConversations.getConversationByShortId({ shortId: conversation, ownerUserId });
|
|
69
|
+
|
|
51
70
|
const fallbackTitle = (toolName: string): string => {
|
|
52
71
|
const label = toolName
|
|
53
72
|
.replaceAll("__", " ")
|
|
@@ -61,6 +80,7 @@ export const createAiApprovalPreferenceRoutes = (dependencies: AiApprovalRouteDe
|
|
|
61
80
|
const listPreferences = dependencies.listPreferences ?? listAiToolApprovalPreferences;
|
|
62
81
|
const revokePreference = dependencies.revokePreference ?? revokeAiToolApprovalPreference;
|
|
63
82
|
const registry = dependencies.listCapabilities ?? listCapabilities;
|
|
83
|
+
const findConversation = dependencies.findConversation ?? findOwnConversation;
|
|
64
84
|
|
|
65
85
|
return new Hono<AuthContext>()
|
|
66
86
|
.use(dependencies.limit ?? rateLimit())
|
|
@@ -70,25 +90,41 @@ export const createAiApprovalPreferenceRoutes = (dependencies: AiApprovalRouteDe
|
|
|
70
90
|
describeRoute({
|
|
71
91
|
tags: ["AI"],
|
|
72
92
|
summary: "List remembered AI approvals",
|
|
73
|
-
description:
|
|
93
|
+
description:
|
|
94
|
+
"Lists AI tool approvals remembered for the current user, with titles in the request locale. Without `conversation`, it lists the approvals that apply everywhere; with it, those limited to that chat.",
|
|
74
95
|
...requiresAuth,
|
|
75
96
|
responses: {
|
|
76
97
|
200: jsonResponse(AiApprovalPreferenceListSchema, "Remembered approvals"),
|
|
77
98
|
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
78
99
|
403: jsonResponse(ErrorResponseSchema, "User-backed actor required"),
|
|
100
|
+
404: jsonResponse(ErrorResponseSchema, "Chat not found"),
|
|
79
101
|
},
|
|
80
102
|
}),
|
|
103
|
+
v("query", AiApprovalPreferenceQuerySchema),
|
|
81
104
|
async (c) => {
|
|
82
105
|
const user = expectUserBackedActor(c);
|
|
106
|
+
const requested = c.req.valid("query").conversation;
|
|
107
|
+
const conversation = requested ? await findConversation(user.id, requested) : null;
|
|
108
|
+
if (requested && !conversation) return c.json({ message: "Chat not found", code: "NOT_FOUND" }, 404);
|
|
83
109
|
const [preferences, capabilities] = await Promise.all([
|
|
84
|
-
listPreferences(user.id),
|
|
110
|
+
listPreferences(user.id, conversation ? { conversationId: conversation.id } : {}),
|
|
85
111
|
registry().catch(() => [] as CapabilityRegistryEntry[]),
|
|
86
112
|
]);
|
|
87
113
|
const catalogByName = new Map(buildAiCapabilityCatalog(capabilities, getLocale(c)).map((entry) => [entry.name, entry]));
|
|
88
114
|
const approvals: AiApprovalPreferenceView[] = preferences.map((preference) => {
|
|
115
|
+
if (preference.toolName === AI_WEBSITE_APPROVAL_TOOL) {
|
|
116
|
+
const website = parseAiWebsiteApprovalScope(preference.approvalScope);
|
|
117
|
+
return {
|
|
118
|
+
...preference,
|
|
119
|
+
title: URL.canParse(website.origin) ? new URL(website.origin).host : website.origin,
|
|
120
|
+
app: null,
|
|
121
|
+
website,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
89
124
|
const capability = catalogByName.get(preference.toolName);
|
|
90
125
|
return {
|
|
91
126
|
...preference,
|
|
127
|
+
website: null,
|
|
92
128
|
title: capability?.display.title ?? fallbackTitle(preference.toolName),
|
|
93
129
|
app: capability
|
|
94
130
|
? {
|
package/src/ai/approvals.ts
CHANGED
|
@@ -5,14 +5,41 @@ import type { AiToolApprovalPolicy, AiTurnRunConfig } from "./types";
|
|
|
5
5
|
export const aiTurnAllowsRememberedApprovals = (config: AiTurnRunConfig | null | undefined): boolean =>
|
|
6
6
|
config == null || config.kind === "compact" || config.mandate === undefined;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Reserved remembered-approval name for reading one website in one chat or one managed app. No capability or tool can
|
|
10
|
+
* collide with it: tool names never contain a colon.
|
|
11
|
+
*/
|
|
12
|
+
export const AI_WEBSITE_APPROVAL_TOOL = "website:read";
|
|
13
|
+
|
|
14
|
+
/** The remembered scope of a website approval: one exact origin, in a chat or for one managed Studio app. */
|
|
15
|
+
export const aiWebsiteApprovalScope = (origin: string, resourceId?: string): string =>
|
|
16
|
+
resourceId ? `${origin} resource:${resourceId}` : origin;
|
|
17
|
+
|
|
18
|
+
export const parseAiWebsiteApprovalScope = (scope: string): { origin: string; resourceId: string | null } => {
|
|
19
|
+
const [origin = scope, resource] = scope.split(" ");
|
|
20
|
+
return { origin, resourceId: resource?.startsWith("resource:") ? resource.slice("resource:".length) : null };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Website approvals apply only to a turn a person started in a signed-in browser session: never to a scheduled task,
|
|
25
|
+
* a mandate, `cld`, an API key, or another delegated credential.
|
|
26
|
+
*/
|
|
27
|
+
export const aiTurnAllowsWebsiteApprovals = (config: AiTurnRunConfig | null | undefined): boolean =>
|
|
28
|
+
config != null && config.kind !== "compact" && config.signedInSession === true && aiTurnAllowsRememberedApprovals(config);
|
|
29
|
+
|
|
8
30
|
export type AiToolApprovalContext = {
|
|
9
31
|
actorUserId: string;
|
|
10
32
|
};
|
|
11
33
|
|
|
34
|
+
/** Where a remembered approval applies: in one chat until it is deleted, or always. */
|
|
35
|
+
export type AiToolApprovalRemember = "chat" | "always";
|
|
36
|
+
|
|
12
37
|
export type AiToolApprovalPreference = {
|
|
13
38
|
id: string;
|
|
14
39
|
toolName: string;
|
|
15
40
|
approvalScope: string;
|
|
41
|
+
/** The chat this approval is limited to; null for an approval that applies everywhere. */
|
|
42
|
+
conversationId: string | null;
|
|
16
43
|
createdAt: string;
|
|
17
44
|
lastUsedAt: string | null;
|
|
18
45
|
expiresAt: string | null;
|
|
@@ -22,6 +49,7 @@ type AiToolApprovalPreferenceRow = {
|
|
|
22
49
|
id: string;
|
|
23
50
|
tool_name: string;
|
|
24
51
|
approval_scope: string;
|
|
52
|
+
conversation_id: string | null;
|
|
25
53
|
created_at: Date | string;
|
|
26
54
|
last_used_at: Date | string | null;
|
|
27
55
|
expires_at: Date | string | null;
|
|
@@ -37,36 +65,52 @@ export const aiToolNeedsApproval = (policy: AiToolApprovalPolicy | undefined): b
|
|
|
37
65
|
export const aiToolAllowsAlways = (policy: AiToolApprovalPolicy | undefined): boolean =>
|
|
38
66
|
policy === "always" || (typeof policy === "object" && policy.kind === "user-configurable");
|
|
39
67
|
|
|
40
|
-
|
|
68
|
+
/**
|
|
69
|
+
* The remembered approval that covers this tool and scope: one that applies everywhere, or one limited to
|
|
70
|
+
* `conversationId`; with `chatOnly`, only the latter. Returns its ID and marks it used.
|
|
71
|
+
*/
|
|
72
|
+
export const findRememberedAiToolApproval = async (
|
|
41
73
|
context: AiToolApprovalContext,
|
|
42
|
-
input: { toolName: string; approvalScope: string },
|
|
43
|
-
): Promise<
|
|
74
|
+
input: { toolName: string; approvalScope: string; conversationId?: string | null; chatOnly?: boolean },
|
|
75
|
+
): Promise<string | null> => {
|
|
76
|
+
const conversationId = input.conversationId ?? null;
|
|
77
|
+
if (input.chatOnly && !conversationId) return null;
|
|
44
78
|
const rows = await sql<{ id: string }[]>`
|
|
45
79
|
SELECT id
|
|
46
80
|
FROM ai.tool_approval_preferences
|
|
47
81
|
WHERE actor_user_id = ${context.actorUserId}
|
|
48
82
|
AND tool_name = ${input.toolName}
|
|
49
83
|
AND approval_scope = ${input.approvalScope}
|
|
84
|
+
AND (conversation_id = ${conversationId}::uuid OR (conversation_id IS NULL AND NOT ${input.chatOnly === true}))
|
|
50
85
|
AND (expires_at IS NULL OR expires_at > now())
|
|
86
|
+
ORDER BY conversation_id NULLS LAST
|
|
51
87
|
LIMIT 1
|
|
52
88
|
`;
|
|
53
89
|
|
|
54
90
|
const id = rows[0]?.id;
|
|
55
|
-
if (!id) return
|
|
91
|
+
if (!id) return null;
|
|
56
92
|
await sql`UPDATE ai.tool_approval_preferences SET last_used_at = now() WHERE id = ${id}`;
|
|
57
|
-
return
|
|
93
|
+
return id;
|
|
58
94
|
};
|
|
59
95
|
|
|
96
|
+
export const hasRememberedAiToolApproval = async (
|
|
97
|
+
context: AiToolApprovalContext,
|
|
98
|
+
input: { toolName: string; approvalScope: string; conversationId?: string | null },
|
|
99
|
+
): Promise<boolean> => (await findRememberedAiToolApproval(context, input)) !== null;
|
|
100
|
+
|
|
101
|
+
/** Remembers an approval everywhere, or in one chat when `conversationId` is set; a chat approval ends with its chat. */
|
|
60
102
|
export const rememberAiToolApproval = async (
|
|
61
103
|
context: AiToolApprovalContext,
|
|
62
|
-
input: { toolName: string; approvalScope: string; expiresAt?: Date | null },
|
|
104
|
+
input: { toolName: string; approvalScope: string; conversationId?: string | null; expiresAt?: Date | null },
|
|
63
105
|
): Promise<void> => {
|
|
106
|
+
const conversationId = input.conversationId ?? null;
|
|
64
107
|
await sql.begin(async () => {
|
|
65
108
|
await sql`
|
|
66
109
|
DELETE FROM ai.tool_approval_preferences
|
|
67
110
|
WHERE actor_user_id = ${context.actorUserId}
|
|
68
111
|
AND tool_name = ${input.toolName}
|
|
69
112
|
AND approval_scope = ${input.approvalScope}
|
|
113
|
+
AND conversation_id IS NOT DISTINCT FROM ${conversationId}::uuid
|
|
70
114
|
`;
|
|
71
115
|
|
|
72
116
|
await sql`
|
|
@@ -74,6 +118,7 @@ export const rememberAiToolApproval = async (
|
|
|
74
118
|
actor_user_id,
|
|
75
119
|
tool_name,
|
|
76
120
|
approval_scope,
|
|
121
|
+
conversation_id,
|
|
77
122
|
last_used_at,
|
|
78
123
|
expires_at
|
|
79
124
|
)
|
|
@@ -81,6 +126,7 @@ export const rememberAiToolApproval = async (
|
|
|
81
126
|
${context.actorUserId},
|
|
82
127
|
${input.toolName},
|
|
83
128
|
${input.approvalScope},
|
|
129
|
+
${conversationId}::uuid,
|
|
84
130
|
now(),
|
|
85
131
|
${input.expiresAt ?? null}
|
|
86
132
|
)
|
|
@@ -88,16 +134,20 @@ export const rememberAiToolApproval = async (
|
|
|
88
134
|
});
|
|
89
135
|
};
|
|
90
136
|
|
|
137
|
+
/** Forgets the approval with exactly this reach: everywhere, or in the one chat `conversationId` names. */
|
|
91
138
|
export const forgetAiToolApproval = async (
|
|
92
139
|
context: AiToolApprovalContext,
|
|
93
|
-
input: { toolName: string; approvalScope: string },
|
|
94
|
-
): Promise<
|
|
95
|
-
await sql
|
|
140
|
+
input: { toolName: string; approvalScope: string; conversationId?: string | null },
|
|
141
|
+
): Promise<boolean> => {
|
|
142
|
+
const rows = await sql<{ id: string }[]>`
|
|
96
143
|
DELETE FROM ai.tool_approval_preferences
|
|
97
144
|
WHERE actor_user_id = ${context.actorUserId}
|
|
98
145
|
AND tool_name = ${input.toolName}
|
|
99
146
|
AND approval_scope = ${input.approvalScope}
|
|
147
|
+
AND conversation_id IS NOT DISTINCT FROM ${input.conversationId ?? null}::uuid
|
|
148
|
+
RETURNING id
|
|
100
149
|
`;
|
|
150
|
+
return rows.length > 0;
|
|
101
151
|
};
|
|
102
152
|
|
|
103
153
|
const toIsoString = (value: Date | string): string => (value instanceof Date ? value : new Date(value)).toISOString();
|
|
@@ -106,22 +156,30 @@ const approvalPreferenceFromRow = (row: AiToolApprovalPreferenceRow): AiToolAppr
|
|
|
106
156
|
id: row.id,
|
|
107
157
|
toolName: row.tool_name,
|
|
108
158
|
approvalScope: row.approval_scope,
|
|
159
|
+
conversationId: row.conversation_id,
|
|
109
160
|
createdAt: toIsoString(row.created_at),
|
|
110
161
|
lastUsedAt: row.last_used_at ? toIsoString(row.last_used_at) : null,
|
|
111
162
|
expiresAt: row.expires_at ? toIsoString(row.expires_at) : null,
|
|
112
163
|
});
|
|
113
164
|
|
|
114
|
-
|
|
165
|
+
/** Lists the approvals that apply everywhere, or only those of one chat when `conversationId` is set. */
|
|
166
|
+
export const listAiToolApprovalPreferences = async (
|
|
167
|
+
actorUserId: string,
|
|
168
|
+
options: { conversationId?: string } = {},
|
|
169
|
+
): Promise<AiToolApprovalPreference[]> => {
|
|
170
|
+
const conversationId = options.conversationId ?? null;
|
|
115
171
|
const rows = await sql<AiToolApprovalPreferenceRow[]>`
|
|
116
172
|
SELECT
|
|
117
173
|
id,
|
|
118
174
|
tool_name,
|
|
119
175
|
approval_scope,
|
|
176
|
+
conversation_id,
|
|
120
177
|
created_at,
|
|
121
178
|
last_used_at,
|
|
122
179
|
expires_at
|
|
123
180
|
FROM ai.tool_approval_preferences
|
|
124
181
|
WHERE actor_user_id = ${actorUserId}
|
|
182
|
+
AND conversation_id IS NOT DISTINCT FROM ${conversationId}::uuid
|
|
125
183
|
AND (expires_at IS NULL OR expires_at > now())
|
|
126
184
|
ORDER BY last_used_at DESC NULLS LAST, created_at DESC, id
|
|
127
185
|
`;
|
package/src/ai/browser.ts
CHANGED
|
@@ -13,13 +13,23 @@ export { type AiTodoPlan, parseAiTodoPlan } from "./todo-contracts";
|
|
|
13
13
|
import { AI_TURN_ATTACHMENT_MAX_ITEMS } from "./limits";
|
|
14
14
|
import type { AiConversation, AiDraftContentPart } from "./types";
|
|
15
15
|
|
|
16
|
+
export {
|
|
17
|
+
type CloudAiChartColumn,
|
|
18
|
+
type CloudAiChartRow,
|
|
19
|
+
type CloudAiChartTable,
|
|
20
|
+
cloudAiChartTable,
|
|
21
|
+
} from "./chart-block";
|
|
16
22
|
export { parseAiSse } from "./client/transport";
|
|
17
23
|
export {
|
|
24
|
+
CLOUD_AI_CHART_MAX_SERIES,
|
|
25
|
+
CLOUD_AI_CHART_MAX_VALUES,
|
|
18
26
|
CLOUD_AI_TEXT_EDITOR_MAX_CHARS,
|
|
19
27
|
CloudAiCardInputSchema,
|
|
28
|
+
CloudAiChartInputSchema,
|
|
20
29
|
CloudAiLocalBashInputSchema,
|
|
21
30
|
CloudAiSurveyInputSchema,
|
|
22
31
|
CloudAiTextEditorInputSchema,
|
|
32
|
+
parseCloudAiChartInput,
|
|
23
33
|
} from "./default-tool-contracts";
|
|
24
34
|
export { guessAiMediaType } from "./file-media-type";
|
|
25
35
|
export { AI_IMAGE_INPUT_MAX_BYTES, AI_TURN_ATTACHMENT_MAX_ITEMS, AI_TURN_IMAGE_MAX_TOTAL_BYTES } from "./limits";
|
package/src/ai/capabilities.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { Tool, ToolContext, ToolResolver } from "@k2b/nessi";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { resolveCapabilityOperationTitle } from "../_internal/capabilities";
|
|
4
|
+
import { resolveCapabilityActionWording, resolveCapabilityOperationTitle } from "../_internal/capabilities";
|
|
5
|
+
import {
|
|
6
|
+
CAPABILITY_APPROVAL_REASON_FIELD,
|
|
7
|
+
CAPABILITY_APPROVAL_REASON_MAX_CHARS,
|
|
8
|
+
type CapabilityActionWording,
|
|
9
|
+
capabilityActionSubject,
|
|
10
|
+
capabilityApprovalReason,
|
|
11
|
+
capabilityApprovalText,
|
|
12
|
+
} from "../_internal/capability-sentences";
|
|
5
13
|
import { HELP_READ_MAX_CHARS, HELP_SEARCH_MAX_LIMIT, readHelpArticle } from "../_internal/help-catalog";
|
|
6
14
|
import {
|
|
7
15
|
type CapabilityActionManifest,
|
|
@@ -13,9 +21,10 @@ import {
|
|
|
13
21
|
} from "../contracts/capabilities";
|
|
14
22
|
import type { CapabilityRegistryEntry } from "../contracts/registry";
|
|
15
23
|
import type { RequestActor } from "../server";
|
|
16
|
-
import {
|
|
24
|
+
import type { HelpReader, HelpReaderFactory } from "../services/help";
|
|
17
25
|
import { type MandatePolicyV1, mandatePolicyCanPermitCapability } from "../services/mandates/policy";
|
|
18
26
|
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
27
|
+
import { DEFAULT_LOCALE } from "../shared/locale";
|
|
19
28
|
import { recordRejectedAiCapability } from "./capability-execution";
|
|
20
29
|
import { CODE_SOURCE_TOOLS } from "./code-source-contracts";
|
|
21
30
|
import { createCodeSourceTool } from "./code-source-tools";
|
|
@@ -337,9 +346,7 @@ const AiHelpDocumentSchema = AiHelpCatalogItemSchema.extend({
|
|
|
337
346
|
}).strict();
|
|
338
347
|
|
|
339
348
|
/** Search and read published Help without loading one tool per article. */
|
|
340
|
-
export const createAiHelpTools = (
|
|
341
|
-
const reader = help(locale);
|
|
342
|
-
|
|
349
|
+
export const createAiHelpTools = (reader: HelpReader): AiRuntimeTool[] => {
|
|
343
350
|
const search = defineAiTool({
|
|
344
351
|
name: "search_help",
|
|
345
352
|
description:
|
|
@@ -453,6 +460,39 @@ export const reduceAiCapabilityInputSchema = (schema: Record<string, unknown>):
|
|
|
453
460
|
export const aiCapabilityInputSchema = (schema: Record<string, unknown>): z.ZodType =>
|
|
454
461
|
z.fromJSONSchema(reduceAiCapabilityInputSchema(schema));
|
|
455
462
|
|
|
463
|
+
/** How people read calls of one loaded Action in the turn's locale; derived from the live catalog only. */
|
|
464
|
+
export const aiCapabilityActionWording = (entry: AiCapabilityCatalogEntry, locale: string | undefined): CapabilityActionWording =>
|
|
465
|
+
resolveCapabilityActionWording(entry.operation as CapabilityActionManifest, entry.app.presentation, locale ?? DEFAULT_LOCALE);
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* An Action that waits for approval lets the model say why it wants it. The field exists only for the model:
|
|
469
|
+
* Cloud removes it before review, authorization, and execution. An app field of the same name wins.
|
|
470
|
+
*/
|
|
471
|
+
const offersApprovalReason = (entry: AiCapabilityCatalogEntry): boolean => {
|
|
472
|
+
if (entry.kind !== "action" || (entry.operation as CapabilityActionManifest).approval === "none") return false;
|
|
473
|
+
const properties = entry.operation.inputSchema.properties;
|
|
474
|
+
return Boolean(properties && typeof properties === "object" && !Object.hasOwn(properties, CAPABILITY_APPROVAL_REASON_FIELD));
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const withApprovalReason = (schema: Record<string, unknown>): Record<string, unknown> => ({
|
|
478
|
+
...schema,
|
|
479
|
+
properties: {
|
|
480
|
+
...(schema.properties as Record<string, unknown>),
|
|
481
|
+
[CAPABILITY_APPROVAL_REASON_FIELD]: {
|
|
482
|
+
type: "string",
|
|
483
|
+
maxLength: CAPABILITY_APPROVAL_REASON_MAX_CHARS,
|
|
484
|
+
description:
|
|
485
|
+
"Optional: one short sentence in the user's language on why this call is needed now. Shown as your reason beside the approval; never put Action input here.",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const withoutApprovalReason = (entry: AiCapabilityCatalogEntry, args: unknown): unknown => {
|
|
491
|
+
if (!offersApprovalReason(entry) || !args || typeof args !== "object" || Array.isArray(args)) return args;
|
|
492
|
+
const { [CAPABILITY_APPROVAL_REASON_FIELD]: _reason, ...appArgs } = args as Record<string, unknown>;
|
|
493
|
+
return appArgs;
|
|
494
|
+
};
|
|
495
|
+
|
|
456
496
|
const ToolCatalogItemSchema = z
|
|
457
497
|
.object({
|
|
458
498
|
name: z.string(),
|
|
@@ -600,6 +640,8 @@ export const createLoadedAiCapabilityTools = (input: {
|
|
|
600
640
|
catalog: readonly AiCapabilityCatalogEntry[];
|
|
601
641
|
loadedNames: readonly string[];
|
|
602
642
|
actor: RequestActor;
|
|
643
|
+
/** Locale of the turn; the approval text a person reads is worded in it. */
|
|
644
|
+
locale?: string;
|
|
603
645
|
review?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<CapabilityActionReview | null>;
|
|
604
646
|
authorizeBackground?: (entry: AiCapabilityCatalogEntry, args: unknown) => Promise<void>;
|
|
605
647
|
onReview?: (callId: string, review: CapabilityActionReview) => void;
|
|
@@ -614,17 +656,28 @@ export const createLoadedAiCapabilityTools = (input: {
|
|
|
614
656
|
name: entry.providerName,
|
|
615
657
|
canonicalName: entry.name,
|
|
616
658
|
description: `${entry.title}. ${entry.description} Never retry ACTION_OUTCOME_UNKNOWN. Do not retry unchanged after INTERNAL or INVALID_APP_RESPONSE; report the provider error.`,
|
|
617
|
-
inputSchema: aiCapabilityInputSchema(
|
|
659
|
+
inputSchema: aiCapabilityInputSchema(
|
|
660
|
+
offersApprovalReason(entry) ? withApprovalReason(entry.operation.inputSchema) : entry.operation.inputSchema,
|
|
661
|
+
),
|
|
618
662
|
outputSchema: z.unknown(),
|
|
619
663
|
// Capability Actions request a custom approval after their optional
|
|
620
664
|
// live review has resolved. The review may supply an app-owned scope.
|
|
621
665
|
approval: "never",
|
|
622
|
-
}).server(async (
|
|
666
|
+
}).server(async (modelArgs, context) => {
|
|
667
|
+
const args = withoutApprovalReason(entry, modelArgs);
|
|
623
668
|
await input.authorizeBackground?.(entry, args);
|
|
624
669
|
if (!input.authorizeBackground && entry.kind === "action" && (entry.operation as CapabilityActionManifest).approval !== "none") {
|
|
625
670
|
const review = (await input.review?.(entry, args, context)) ?? null;
|
|
626
671
|
if (review && context.callId) input.onReview?.(context.callId, review);
|
|
627
|
-
|
|
672
|
+
// Text-only readers such as the CLI read what the card shows: the app's sentence, its review, and the model's reason.
|
|
673
|
+
const locale = input.locale ?? DEFAULT_LOCALE;
|
|
674
|
+
const message = capabilityApprovalText({
|
|
675
|
+
appName: entry.display.appName,
|
|
676
|
+
subject: capabilityActionSubject(aiCapabilityActionWording(entry, locale), args, { locale, timeZone: context.timeZone }),
|
|
677
|
+
review: review?.message,
|
|
678
|
+
reason: offersApprovalReason(entry) ? capabilityApprovalReason(modelArgs) : null,
|
|
679
|
+
locale,
|
|
680
|
+
});
|
|
628
681
|
if (!(await context.requestApproval(message))) {
|
|
629
682
|
if (input.actor.kind === "user") {
|
|
630
683
|
await recordRejectedAiCapability({ entry, actor: input.actor, args }).catch(() => undefined);
|
|
@@ -743,7 +796,8 @@ export const createAiToolResolver =
|
|
|
743
796
|
fullCapabilityCatalog.filter((entry) => !mandateAllows(entry)).flatMap((entry) => [entry.name, entry.providerName]),
|
|
744
797
|
);
|
|
745
798
|
const capabilityCatalog = fullCapabilityCatalog.filter((entry) => (!allowed || allowed.has(entry.name)) && mandateAllows(entry));
|
|
746
|
-
|
|
799
|
+
// The turn's actor decides which apps' Help exists for the model, like every other Help surface.
|
|
800
|
+
const helpTools = input.help ? createAiHelpTools(input.help(input.locale ?? "en", input.actor)) : [];
|
|
747
801
|
const resourceTool =
|
|
748
802
|
input.execute && (capabilityCatalog.length > 0 || input.staticTools.some((tool) => tool.def.name === "code_read"))
|
|
749
803
|
? createAiResourceReaderTool({ apps: registry, catalog: capabilityCatalog, execute: input.execute })
|
|
@@ -805,6 +859,7 @@ export const createAiToolResolver =
|
|
|
805
859
|
catalog: capabilityCatalog,
|
|
806
860
|
loadedNames,
|
|
807
861
|
actor: input.actor,
|
|
862
|
+
locale: input.locale,
|
|
808
863
|
review: input.review,
|
|
809
864
|
onReview: input.onReview,
|
|
810
865
|
authorizeBackground: input.authorizeBackground,
|
|
@@ -824,6 +879,7 @@ export const createAiToolResolver =
|
|
|
824
879
|
for (const name of loadedNames) {
|
|
825
880
|
const entry = catalogByName.get(name);
|
|
826
881
|
if (!entry) continue;
|
|
882
|
+
const wording = entry.kind === "action" ? aiCapabilityActionWording(entry, input.locale) : null;
|
|
827
883
|
presentations.set(entry.providerName, {
|
|
828
884
|
kind: "capability",
|
|
829
885
|
appId: entry.appId,
|
|
@@ -832,6 +888,9 @@ export const createAiToolResolver =
|
|
|
832
888
|
appAccent: entry.app.appAccent,
|
|
833
889
|
title: entry.display.title,
|
|
834
890
|
capabilityKind: entry.kind,
|
|
891
|
+
...(wording?.sentences ? { sentences: wording.sentences } : {}),
|
|
892
|
+
...(wording?.fields?.length ? { fields: [...wording.fields] } : {}),
|
|
893
|
+
...(offersApprovalReason(entry) ? { approvalReason: true as const } : {}),
|
|
835
894
|
});
|
|
836
895
|
// Rememberable scopes are resolved by the owning app for each concrete
|
|
837
896
|
// call and attached when its live review completes.
|