@k2b/cloud 0.44.0 → 0.46.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 -2
- package/src/_internal/app-queries.ts +73 -0
- package/src/_internal/capabilities.ts +137 -9
- package/src/_internal/define-app.ts +2 -0
- package/src/_internal/registry-validation.ts +17 -4
- package/src/_internal/registry.ts +32 -4
- package/src/_internal/resource-principals.ts +36 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +11 -2
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/ai/capabilities.ts +4 -2
- package/src/ai/chat/map-messages.ts +270 -0
- package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
- package/src/ai/chat/map-notebook.tsx +301 -0
- package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
- package/src/ai/chat/map-widget.css +107 -0
- package/src/ai/chat/map-widget.tsx +1279 -0
- package/src/ai/chat/message-utils.ts +1 -0
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -1
- package/src/ai/chat/turn-view.tsx +17 -1
- package/src/ai/default-tools.ts +3 -0
- package/src/ai/dictation-runtime.ts +6 -2
- package/src/ai/executor.ts +22 -4
- package/src/ai/index.ts +1 -0
- package/src/ai/map-contracts.ts +209 -0
- package/src/ai/map-execution.ts +14 -0
- package/src/ai/map-notebook-save.ts +276 -0
- package/src/ai/map-notebook.ts +32 -0
- package/src/ai/map-opening-hours.ts +40 -0
- package/src/ai/map-routes.ts +84 -0
- package/src/ai/map-service.ts +288 -0
- package/src/ai/map-store.ts +137 -0
- package/src/ai/map-tool.ts +73 -0
- package/src/ai/migrate.ts +3 -0
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +56 -46
- package/src/ai/projects.ts +100 -79
- package/src/ai/protocol.ts +3 -1
- package/src/ai/recent-resources.ts +3 -3
- package/src/ai/routes.ts +32 -30
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/ai/store.ts +19 -6
- package/src/ai/tools.ts +14 -0
- package/src/ai/types.ts +3 -0
- package/src/api/accounts-entities.ts +6 -3
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/auth.ts +9 -2
- package/src/api/capabilities.ts +90 -12
- package/src/api/mcp.ts +4 -2
- package/src/browser/notifications.ts +25 -8
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/discussion-providers.ts +99 -0
- package/src/capabilities/executions.ts +2 -2
- package/src/capabilities/migrate.ts +10 -1
- package/src/capabilities/resource-readers.ts +58 -0
- package/src/capabilities/server-invoke.ts +23 -13
- package/src/capabilities/server.ts +17 -6
- package/src/capabilities/testing.ts +13 -0
- package/src/contracts/capabilities.ts +137 -4
- package/src/contracts/discussions.ts +91 -0
- package/src/contracts/geo-document.ts +60 -0
- package/src/contracts/geo-snapshot.ts +86 -0
- package/src/contracts/geo.ts +14 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/resource-types.ts +21 -0
- package/src/contracts/shared.ts +2 -1
- package/src/geo/AddressAutocomplete.tsx +213 -0
- package/src/geo/messages.ts +29 -0
- package/src/geo/navigation.ts +25 -0
- package/src/geo/ui.ts +2 -0
- package/src/server/api/respond.ts +10 -2
- package/src/server/index.ts +1 -0
- package/src/server/services/access.ts +23 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +56 -17
- package/src/services/accounts/entities.ts +36 -16
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +26 -17
- package/src/services/accounts/identities.ts +8 -1
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +141 -27
- package/src/services/accounts/machine-group-management.ts +94 -0
- package/src/services/accounts/users.ts +38 -8
- package/src/services/geo/cache.ts +2 -1
- package/src/services/geo/geoapify.ts +3 -0
- package/src/services/identity/invocation-token.ts +110 -0
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +1 -0
- package/src/services/pdf/geo-snapshot.ts +107 -0
- package/src/services/pdf/index.ts +2 -0
- package/src/services/session/index.ts +12 -3
- package/src/shared/email-html.ts +1 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +428 -77
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +2 -2
- package/src/ssr/AdminLayout.tsx +1 -1
- package/src/ssr/AdminSidebar.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +19 -3
- package/src/ssr/Layout.tsx +1 -1
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-navigation.ts +1 -11
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/shell-data.tsx +1 -1
- package/src/styles/global.css +2 -0
- package/src/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +2 -1
package/src/ai/skills.ts
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import { type SQL, type SQLQuery, sql } from "bun";
|
|
2
2
|
import { listApps } from "../_internal/registry";
|
|
3
3
|
import { AuthenticatedPrincipalSchema, type ServiceAccountKind } from "../contracts/shared";
|
|
4
|
-
import type { AccessSubject } from "../server";
|
|
5
4
|
import type { RequestAuthority } from "../server/middleware/auth";
|
|
6
5
|
import {
|
|
7
6
|
buildAccessPrincipalCondition,
|
|
8
7
|
createAccess,
|
|
9
8
|
deleteAccess,
|
|
9
|
+
ensureManagerRemains,
|
|
10
10
|
hasPermission,
|
|
11
11
|
type PermissionLevel,
|
|
12
12
|
type Principal,
|
|
13
13
|
} from "../server/services/access";
|
|
14
14
|
import { accessRevision } from "../server/services/access-revision";
|
|
15
|
-
import {
|
|
16
|
-
import { usableAppIds } from "../services/app-access";
|
|
15
|
+
import { evaluateAppAccess, usableAppIds } from "../services/app-access";
|
|
17
16
|
import { logger } from "../services/logging";
|
|
18
17
|
import { toPgTextArray, toPgUuidArray } from "../services/postgres";
|
|
19
18
|
import { mountAiSkillFilePath } from "./file-mount";
|
|
20
19
|
import { aiProjects } from "./projects";
|
|
20
|
+
import {
|
|
21
|
+
type AiResourceContext,
|
|
22
|
+
aiResourceAccessSubject,
|
|
23
|
+
aiResourcePermissionCap,
|
|
24
|
+
requireAiResourceCreation,
|
|
25
|
+
} from "./projects-authorization";
|
|
21
26
|
import { AI_SHORT_ID_PATTERN, withAiShortIdForDb } from "./short-id";
|
|
22
27
|
import { type AiSkillContent, appSkillStatus, contentHash } from "./skill-content";
|
|
23
28
|
import {
|
|
@@ -67,7 +72,7 @@ export type AiSkillAccess = {
|
|
|
67
72
|
permission: AiSkillPermission;
|
|
68
73
|
displayName?: string;
|
|
69
74
|
avatarHash?: string | null;
|
|
70
|
-
/** Kind of a `service_account` principal
|
|
75
|
+
/** Kind of a `service_account` principal, including manager eligibility. */
|
|
71
76
|
serviceAccountKind?: ServiceAccountKind;
|
|
72
77
|
createdAt: string;
|
|
73
78
|
};
|
|
@@ -200,13 +205,21 @@ const jsonObject = (value: AiSkillExtraFrontmatter | string): AiSkillExtraFrontm
|
|
|
200
205
|
const snapshotFiles = (value: SnapshotRow["files"]): { path: string; content: string }[] =>
|
|
201
206
|
typeof value === "string" ? (JSON.parse(value) as { path: string; content: string }[]) : value;
|
|
202
207
|
|
|
203
|
-
const
|
|
204
|
-
subject
|
|
208
|
+
const aiResourceUserId = (input: AiResourceContext | null): string | null => {
|
|
209
|
+
const subject = aiResourceAccessSubject(input);
|
|
210
|
+
return subject?.type === "user" ? subject.userId : null;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const principalForSubject = (input: AiResourceContext): Principal => {
|
|
214
|
+
const subject = aiResourceAccessSubject(input)!;
|
|
215
|
+
return subject.type === "user"
|
|
205
216
|
? { type: "user", userId: subject.userId }
|
|
206
217
|
: { type: "service_account", serviceAccountId: subject.serviceAccountId };
|
|
218
|
+
};
|
|
207
219
|
|
|
208
|
-
const accessMatch = (
|
|
209
|
-
subject
|
|
220
|
+
const accessMatch = (input: AiResourceContext | null, audience: SkillAudience): SQLQuery => {
|
|
221
|
+
const subject = aiResourceAccessSubject(input);
|
|
222
|
+
return subject === null || (aiResourcePermissionCap(input) === "none" && audience.brokerReadAppIds.length === 0)
|
|
210
223
|
? sql`FALSE`
|
|
211
224
|
: buildAccessPrincipalCondition({
|
|
212
225
|
subject,
|
|
@@ -217,6 +230,7 @@ const accessMatch = (subject: AccessSubject | null): SQLQuery =>
|
|
|
217
230
|
authenticatedOnly: sql`access.authenticated_only`,
|
|
218
231
|
},
|
|
219
232
|
});
|
|
233
|
+
};
|
|
220
234
|
|
|
221
235
|
export const getRow = async (skillId: string, db: SQL = sql): Promise<SkillRow | null> =>
|
|
222
236
|
(await db<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid`)[0] ?? null;
|
|
@@ -231,38 +245,64 @@ const getRowByName = async (name: string, db: SQL = sql): Promise<SkillRow | nul
|
|
|
231
245
|
* Resolve before opening a transaction: role lookup uses its own pool connection, and a transaction that waits for a
|
|
232
246
|
* second connection can exhaust the pool under concurrent requests.
|
|
233
247
|
*/
|
|
234
|
-
type SkillAudience = {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
: [];
|
|
248
|
+
type SkillAudience = {
|
|
249
|
+
roles: readonly string[];
|
|
250
|
+
usableAppIds: readonly string[];
|
|
251
|
+
declaredAppIds: readonly string[];
|
|
252
|
+
brokerReadAppIds: readonly string[];
|
|
253
|
+
};
|
|
254
|
+
const subjectRoles = async (subject: AiResourceContext | null, authority: RequestAuthority | null = null): Promise<SkillAudience> => {
|
|
255
|
+
const actor = authority?.actor ?? subject?.actor;
|
|
256
|
+
const roles = actor?.kind === "user" ? actor.user.roles : (actor?.delegatedUser?.roles ?? []);
|
|
244
257
|
const apps = await listApps().catch(() => {
|
|
245
258
|
logger("ai:skills").warn("App registry unavailable; app Skills are unavailable");
|
|
246
259
|
return [];
|
|
247
260
|
});
|
|
261
|
+
const usable = [...(await usableAppIds(authority, apps))];
|
|
262
|
+
const resourceActor = subject?.actor;
|
|
263
|
+
// App Skill brokers support independent machines, including resource bindings.
|
|
264
|
+
// Keep identity matching and credential checks even for legacy undeclared apps.
|
|
265
|
+
const brokerAuthority: RequestAuthority | null =
|
|
266
|
+
authority &&
|
|
267
|
+
resourceActor?.kind === "service_account" &&
|
|
268
|
+
!resourceActor.delegatedUser &&
|
|
269
|
+
["standalone", "agent", "resource_bound"].includes(resourceActor.serviceAccount.kind) &&
|
|
270
|
+
subject?.accessSubject.type === "service_account" &&
|
|
271
|
+
subject.accessSubject.serviceAccountId === resourceActor.serviceAccount.id
|
|
272
|
+
? { ...authority, actor: resourceActor, accessSubject: subject.accessSubject, scopes: resourceActor.scopes }
|
|
273
|
+
: null;
|
|
248
274
|
return {
|
|
249
275
|
roles,
|
|
250
|
-
usableAppIds:
|
|
276
|
+
usableAppIds: usable,
|
|
251
277
|
declaredAppIds: apps.filter((app) => app.access !== undefined).map((app) => app.id),
|
|
278
|
+
brokerReadAppIds: usable.filter(
|
|
279
|
+
(id) =>
|
|
280
|
+
evaluateAppAccess(authority, { id, access: { kind: "signedIn" } }) &&
|
|
281
|
+
evaluateAppAccess(brokerAuthority, { id, access: { kind: "signedIn" } }),
|
|
282
|
+
),
|
|
252
283
|
};
|
|
253
284
|
};
|
|
254
285
|
|
|
255
286
|
// App access applies to every grant; the older navigation audience keeps its legacy grant behavior.
|
|
256
|
-
const effectivePermission = (skillId: SQLQuery, subject:
|
|
257
|
-
SELECT
|
|
287
|
+
const effectivePermission = (skillId: SQLQuery, subject: AiResourceContext | null, roles: SkillAudience) => sql`
|
|
288
|
+
SELECT CASE
|
|
289
|
+
WHEN permission = 'none' THEN permission
|
|
290
|
+
WHEN ${aiResourcePermissionCap(subject)} = 'none' THEN
|
|
291
|
+
CASE WHEN EXISTS (
|
|
292
|
+
SELECT 1 FROM ai.app_skills app WHERE app.skill_id=${skillId}
|
|
293
|
+
AND app.app_id = ANY(${toPgTextArray([...roles.brokerReadAppIds])}::text[])
|
|
294
|
+
) THEN 'read'::auth.permission_level ELSE 'none'::auth.permission_level END
|
|
295
|
+
WHEN ${aiResourcePermissionCap(subject)} = 'read' THEN 'read'::auth.permission_level
|
|
296
|
+
WHEN ${aiResourcePermissionCap(subject)} = 'write' AND permission = 'admin' THEN 'write'::auth.permission_level
|
|
297
|
+
ELSE permission END AS permission FROM (
|
|
258
298
|
SELECT access.permission, (access.user_id IS NOT NULL OR access.group_id IS NOT NULL OR access.service_account_id IS NOT NULL) AS direct
|
|
259
299
|
FROM ai.skill_access link JOIN auth.access access ON access.id=link.access_id
|
|
260
|
-
WHERE link.skill_id=${skillId} AND ${accessMatch(subject)}
|
|
300
|
+
WHERE link.skill_id=${skillId} AND ${accessMatch(subject, roles)}
|
|
261
301
|
UNION ALL
|
|
262
302
|
SELECT 'read'::auth.permission_level, false AS direct FROM ai.project_skills link
|
|
263
303
|
JOIN ai.project_access project_access ON project_access.project_id=link.project_id
|
|
264
304
|
JOIN auth.access access ON access.id=project_access.access_id
|
|
265
|
-
WHERE link.skill_id=${skillId} AND access.permission <> 'none' AND ${accessMatch(subject)}
|
|
305
|
+
WHERE link.skill_id=${skillId} AND access.permission <> 'none' AND ${accessMatch(subject, roles)}
|
|
266
306
|
) grants WHERE NOT EXISTS (
|
|
267
307
|
SELECT 1 FROM ai.app_skills app WHERE app.skill_id=${skillId}
|
|
268
308
|
AND (NOT app.available OR NOT(app.app_id = ANY(${toPgTextArray([...roles.usableAppIds])}::text[])) OR (NOT (app.app_id = ANY(${toPgTextArray([...roles.declaredAppIds])}::text[])) AND NOT grants.direct AND app.required_roles IS NOT NULL AND NOT(app.required_roles && ${toPgTextArray([...roles.roles])}::text[])))
|
|
@@ -271,7 +311,7 @@ const effectivePermission = (skillId: SQLQuery, subject: AccessSubject | null, r
|
|
|
271
311
|
|
|
272
312
|
const permissionFor = async (
|
|
273
313
|
skillId: string,
|
|
274
|
-
subject:
|
|
314
|
+
subject: AiResourceContext | null,
|
|
275
315
|
db: SQL,
|
|
276
316
|
roles: SkillAudience,
|
|
277
317
|
): Promise<AiSkillPermission | "none"> => {
|
|
@@ -283,7 +323,7 @@ const permissionFor = async (
|
|
|
283
323
|
|
|
284
324
|
const canMutate = async (
|
|
285
325
|
skillId: string,
|
|
286
|
-
subject:
|
|
326
|
+
subject: AiResourceContext | null,
|
|
287
327
|
required: AiSkillPermission,
|
|
288
328
|
db: SQL,
|
|
289
329
|
roles: SkillAudience,
|
|
@@ -297,12 +337,13 @@ const canMutate = async (
|
|
|
297
337
|
return false;
|
|
298
338
|
};
|
|
299
339
|
|
|
300
|
-
const enabledFor = async (skillId: string, subject:
|
|
301
|
-
|
|
340
|
+
const enabledFor = async (skillId: string, subject: AiResourceContext | null, db: SQL = sql): Promise<boolean> => {
|
|
341
|
+
const accessSubject = aiResourceAccessSubject(subject);
|
|
342
|
+
if (accessSubject?.type !== "user") return true;
|
|
302
343
|
const [disabled] = await db<{ disabled: boolean }[]>`
|
|
303
344
|
SELECT true AS disabled
|
|
304
345
|
FROM ai.skill_user_disabled
|
|
305
|
-
WHERE skill_id = ${skillId}::uuid AND user_id = ${subject
|
|
346
|
+
WHERE skill_id = ${skillId}::uuid AND user_id = ${aiResourceUserId(subject)}::uuid
|
|
306
347
|
`;
|
|
307
348
|
return !disabled;
|
|
308
349
|
};
|
|
@@ -314,7 +355,7 @@ export const listReferences = async (skillId: string, db: SQL = sql): Promise<Ai
|
|
|
314
355
|
ORDER BY path
|
|
315
356
|
`;
|
|
316
357
|
|
|
317
|
-
const toSkill = async (row: SkillRow, subject:
|
|
358
|
+
const toSkill = async (row: SkillRow, subject: AiResourceContext | null, db: SQL, roles: SkillAudience): Promise<AiSkill | null> => {
|
|
318
359
|
const permission = await permissionFor(row.id, subject, db, roles);
|
|
319
360
|
if (permission === "none") return null;
|
|
320
361
|
const references = await listReferences(row.id, db);
|
|
@@ -343,7 +384,7 @@ const toSkill = async (row: SkillRow, subject: AccessSubject | null, db: SQL, ro
|
|
|
343
384
|
|
|
344
385
|
const requireSkill = async (
|
|
345
386
|
row: SkillRow | null,
|
|
346
|
-
subject:
|
|
387
|
+
subject: AiResourceContext | null,
|
|
347
388
|
required: AiSkillPermission,
|
|
348
389
|
db: SQL,
|
|
349
390
|
roles: SkillAudience,
|
|
@@ -420,40 +461,33 @@ const checkAccessRevision = async (skillId: string, expected: string | undefined
|
|
|
420
461
|
};
|
|
421
462
|
|
|
422
463
|
const updateSkillAccess = async (skillId: string, accessId: string, permission: AiSkillPermission, db: SQL): Promise<boolean> => {
|
|
423
|
-
const [bound] = await db<{ id: string
|
|
424
|
-
SELECT access.id
|
|
464
|
+
const [bound] = await db<{ id: string }[]>`
|
|
465
|
+
SELECT access.id
|
|
425
466
|
FROM ai.skill_access skill_access
|
|
426
467
|
JOIN auth.access access ON access.id = skill_access.access_id
|
|
427
468
|
WHERE skill_access.skill_id = ${skillId}::uuid AND skill_access.short_id = ${accessId}
|
|
428
469
|
`;
|
|
429
470
|
if (!bound) return false;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if ((admins?.count ?? 0) <= 1) throw new AiSkillLastAdminError();
|
|
437
|
-
}
|
|
471
|
+
const before = await listSkillAccess(skillId, db);
|
|
472
|
+
const guarded = ensureManagerRemains({
|
|
473
|
+
before,
|
|
474
|
+
after: before.map((entry) => (entry.shortId === accessId ? { ...entry, permission } : entry)),
|
|
475
|
+
});
|
|
476
|
+
if (!guarded.ok) throw new AiSkillLastAdminError();
|
|
438
477
|
return (await db`UPDATE auth.access SET permission = ${permission}::auth.permission_level WHERE id = ${bound.id}::uuid`).count === 1;
|
|
439
478
|
};
|
|
440
479
|
|
|
441
480
|
const revokeSkillAccess = async (skillId: string, accessId: string, db: SQL): Promise<boolean> => {
|
|
442
|
-
const [bound] = await db<{ access_id: string
|
|
443
|
-
SELECT skill_access.access_id
|
|
481
|
+
const [bound] = await db<{ access_id: string }[]>`
|
|
482
|
+
SELECT skill_access.access_id
|
|
444
483
|
FROM ai.skill_access skill_access
|
|
445
484
|
JOIN auth.access access ON access.id = skill_access.access_id
|
|
446
485
|
WHERE skill_access.skill_id = ${skillId}::uuid AND skill_access.short_id = ${accessId}
|
|
447
486
|
`;
|
|
448
487
|
if (!bound) return false;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
JOIN auth.access access ON access.id = skill_access.access_id
|
|
453
|
-
WHERE skill_access.skill_id = ${skillId}::uuid AND access.permission = 'admin'
|
|
454
|
-
`;
|
|
455
|
-
if ((admins?.count ?? 0) <= 1) throw new AiSkillLastAdminError();
|
|
456
|
-
}
|
|
488
|
+
const before = await listSkillAccess(skillId, db);
|
|
489
|
+
const guarded = ensureManagerRemains({ before, after: before.filter((entry) => entry.shortId !== accessId) });
|
|
490
|
+
if (!guarded.ok) throw new AiSkillLastAdminError();
|
|
457
491
|
const deleted = await deleteAccess({ id: bound.access_id }, db);
|
|
458
492
|
if (!deleted.ok) throw new Error(deleted.error.message);
|
|
459
493
|
return true;
|
|
@@ -554,7 +588,7 @@ type SkillListOptions = {
|
|
|
554
588
|
limit?: number;
|
|
555
589
|
};
|
|
556
590
|
async function listSkillSummaries(
|
|
557
|
-
subject:
|
|
591
|
+
subject: AiResourceContext | null,
|
|
558
592
|
options: SkillListOptions & {
|
|
559
593
|
project?: { id: string; linked: boolean };
|
|
560
594
|
manageOnly?: boolean;
|
|
@@ -562,7 +596,7 @@ async function listSkillSummaries(
|
|
|
562
596
|
} = {},
|
|
563
597
|
): Promise<AiSkillSummary[]> {
|
|
564
598
|
const roles = await subjectRoles(subject, options.authority);
|
|
565
|
-
const userId = subject
|
|
599
|
+
const userId = aiResourceUserId(subject);
|
|
566
600
|
const query = (options.query ?? "").trim().slice(0, 200);
|
|
567
601
|
const limit = Math.max(1, Math.min(201, options.limit ?? 200));
|
|
568
602
|
const rows = await withAiSkillSearch(query, async (bm25) => {
|
|
@@ -607,7 +641,7 @@ async function listSkillSummaries(
|
|
|
607
641
|
}));
|
|
608
642
|
}
|
|
609
643
|
|
|
610
|
-
async function linkedSkillProjects(skillId: string, subject:
|
|
644
|
+
async function linkedSkillProjects(skillId: string, subject: AiResourceContext | null) {
|
|
611
645
|
const links = await sql<
|
|
612
646
|
{ projectId: string }[]
|
|
613
647
|
>`SELECT project_id AS "projectId" FROM ai.project_skills WHERE skill_id=${skillId}::uuid ORDER BY project_id`;
|
|
@@ -621,8 +655,12 @@ async function linkedSkillProjects(skillId: string, subject: AccessSubject | nul
|
|
|
621
655
|
|
|
622
656
|
export type AiConversationSkillUse = { name: string; description: string; turns: number; lastLoadedAt: string };
|
|
623
657
|
|
|
624
|
-
const listSkillTurnFiles = async (
|
|
625
|
-
|
|
658
|
+
const listSkillTurnFiles = async (
|
|
659
|
+
turnId: string,
|
|
660
|
+
subject: AiResourceContext | null,
|
|
661
|
+
roles: SkillAudience,
|
|
662
|
+
): Promise<AiSkillFileToolStat[]> => {
|
|
663
|
+
const userId = aiResourceUserId(subject);
|
|
626
664
|
const rows = await sql<SnapshotRow[]>`
|
|
627
665
|
SELECT DISTINCT snapshot.skill_name, snapshot.description, snapshot.revision, snapshot.instructions,
|
|
628
666
|
snapshot.files, snapshot.loaded_at
|
|
@@ -671,7 +709,7 @@ export const aiSkills = {
|
|
|
671
709
|
|
|
672
710
|
async create(
|
|
673
711
|
input: {
|
|
674
|
-
subject:
|
|
712
|
+
subject: AiResourceContext;
|
|
675
713
|
name: string;
|
|
676
714
|
description: string;
|
|
677
715
|
instructions: string;
|
|
@@ -680,6 +718,7 @@ export const aiSkills = {
|
|
|
680
718
|
},
|
|
681
719
|
authority: RequestAuthority | null = null,
|
|
682
720
|
): Promise<AiSkill> {
|
|
721
|
+
requireAiResourceCreation(input.subject);
|
|
683
722
|
const fields = validatedFields(input);
|
|
684
723
|
const roles = await subjectRoles(input.subject, authority);
|
|
685
724
|
return sql.begin(async (tx) => {
|
|
@@ -706,13 +745,13 @@ export const aiSkills = {
|
|
|
706
745
|
});
|
|
707
746
|
},
|
|
708
747
|
|
|
709
|
-
async list(subject:
|
|
748
|
+
async list(subject: AiResourceContext | null, options: SkillListOptions = {}): Promise<AiSkillSummary[]> {
|
|
710
749
|
return listSkillSummaries(subject, options);
|
|
711
750
|
},
|
|
712
751
|
|
|
713
752
|
async projectSkills(
|
|
714
753
|
projectId: string,
|
|
715
|
-
subject:
|
|
754
|
+
subject: AiResourceContext | null,
|
|
716
755
|
options: { query?: string; page?: number; available?: boolean; authority?: RequestAuthority | null } = {},
|
|
717
756
|
) {
|
|
718
757
|
if (!(await aiProjects.get(projectId, subject, options.available ? "admin" : "read"))) return null;
|
|
@@ -732,7 +771,7 @@ export const aiSkills = {
|
|
|
732
771
|
skillId: string,
|
|
733
772
|
projectId: string,
|
|
734
773
|
linked: boolean,
|
|
735
|
-
subject:
|
|
774
|
+
subject: AiResourceContext | null,
|
|
736
775
|
authority: RequestAuthority | null = null,
|
|
737
776
|
): Promise<boolean> {
|
|
738
777
|
if (!(await aiProjects.get(projectId, subject, "admin"))) return false;
|
|
@@ -747,13 +786,13 @@ export const aiSkills = {
|
|
|
747
786
|
});
|
|
748
787
|
},
|
|
749
788
|
|
|
750
|
-
async linkedProjects(skillId: string, subject:
|
|
789
|
+
async linkedProjects(skillId: string, subject: AiResourceContext | null, authority: RequestAuthority | null = null) {
|
|
751
790
|
if (!(await this.get(skillId, subject, "admin", authority))) return null;
|
|
752
791
|
return linkedSkillProjects(skillId, subject);
|
|
753
792
|
},
|
|
754
793
|
|
|
755
794
|
async search(
|
|
756
|
-
subject:
|
|
795
|
+
subject: AiResourceContext | null,
|
|
757
796
|
query: string,
|
|
758
797
|
limit = 30,
|
|
759
798
|
options: { modelInvocableOnly?: boolean; authority?: RequestAuthority | null } = {},
|
|
@@ -765,7 +804,7 @@ export const aiSkills = {
|
|
|
765
804
|
|
|
766
805
|
async get(
|
|
767
806
|
skillId: string,
|
|
768
|
-
subject:
|
|
807
|
+
subject: AiResourceContext | null,
|
|
769
808
|
required: AiSkillPermission = "read",
|
|
770
809
|
authority: RequestAuthority | null = null,
|
|
771
810
|
): Promise<AiSkill | null> {
|
|
@@ -774,7 +813,7 @@ export const aiSkills = {
|
|
|
774
813
|
|
|
775
814
|
async getByShortId(
|
|
776
815
|
shortId: string,
|
|
777
|
-
subject:
|
|
816
|
+
subject: AiResourceContext | null,
|
|
778
817
|
required: AiSkillPermission = "read",
|
|
779
818
|
authority: RequestAuthority | null = null,
|
|
780
819
|
): Promise<AiSkill | null> {
|
|
@@ -783,7 +822,7 @@ export const aiSkills = {
|
|
|
783
822
|
|
|
784
823
|
async getByName(
|
|
785
824
|
name: string,
|
|
786
|
-
subject:
|
|
825
|
+
subject: AiResourceContext | null,
|
|
787
826
|
required: AiSkillPermission = "read",
|
|
788
827
|
authority: RequestAuthority | null = null,
|
|
789
828
|
): Promise<AiSkill | null> {
|
|
@@ -792,7 +831,7 @@ export const aiSkills = {
|
|
|
792
831
|
|
|
793
832
|
async update(
|
|
794
833
|
skillId: string,
|
|
795
|
-
subject:
|
|
834
|
+
subject: AiResourceContext | null,
|
|
796
835
|
input: {
|
|
797
836
|
expectedRevision: number;
|
|
798
837
|
name: string;
|
|
@@ -828,7 +867,7 @@ export const aiSkills = {
|
|
|
828
867
|
});
|
|
829
868
|
},
|
|
830
869
|
|
|
831
|
-
async delete(skillId: string, subject:
|
|
870
|
+
async delete(skillId: string, subject: AiResourceContext | null, authority: RequestAuthority | null = null): Promise<boolean> {
|
|
832
871
|
const roles = await subjectRoles(subject, authority);
|
|
833
872
|
return sql.begin(async (tx) => {
|
|
834
873
|
const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
|
|
@@ -844,7 +883,7 @@ export const aiSkills = {
|
|
|
844
883
|
|
|
845
884
|
async listAccess(
|
|
846
885
|
skillId: string,
|
|
847
|
-
subject:
|
|
886
|
+
subject: AiResourceContext | null,
|
|
848
887
|
authority: RequestAuthority | null = null,
|
|
849
888
|
): Promise<AiSkillAccess[] | null> {
|
|
850
889
|
const roles = await subjectRoles(subject, authority);
|
|
@@ -857,7 +896,7 @@ export const aiSkills = {
|
|
|
857
896
|
|
|
858
897
|
async grantAccess(
|
|
859
898
|
skillId: string,
|
|
860
|
-
subject:
|
|
899
|
+
subject: AiResourceContext | null,
|
|
861
900
|
input: { principal: Principal; permission: AiSkillPermission },
|
|
862
901
|
expectedAccessRevision?: string,
|
|
863
902
|
authority: RequestAuthority | null = null,
|
|
@@ -874,7 +913,7 @@ export const aiSkills = {
|
|
|
874
913
|
async updateAccess(
|
|
875
914
|
skillId: string,
|
|
876
915
|
accessId: string,
|
|
877
|
-
subject:
|
|
916
|
+
subject: AiResourceContext | null,
|
|
878
917
|
permission: AiSkillPermission,
|
|
879
918
|
expectedAccessRevision?: string,
|
|
880
919
|
authority: RequestAuthority | null = null,
|
|
@@ -891,7 +930,7 @@ export const aiSkills = {
|
|
|
891
930
|
async revokeAccess(
|
|
892
931
|
skillId: string,
|
|
893
932
|
accessId: string,
|
|
894
|
-
subject:
|
|
933
|
+
subject: AiResourceContext | null,
|
|
895
934
|
expectedAccessRevision?: string,
|
|
896
935
|
authority: RequestAuthority | null = null,
|
|
897
936
|
): Promise<boolean> {
|
|
@@ -1032,23 +1071,23 @@ export const aiSkills = {
|
|
|
1032
1071
|
|
|
1033
1072
|
async setEnabled(
|
|
1034
1073
|
skillId: string,
|
|
1035
|
-
subject:
|
|
1074
|
+
subject: AiResourceContext | null,
|
|
1036
1075
|
enabled: boolean,
|
|
1037
1076
|
authority: RequestAuthority | null = null,
|
|
1038
1077
|
): Promise<boolean | null> {
|
|
1039
|
-
if (subject
|
|
1078
|
+
if (!aiResourceUserId(subject)) return null;
|
|
1040
1079
|
const roles = await subjectRoles(subject, authority);
|
|
1041
1080
|
return sql.begin(async (tx) => {
|
|
1042
1081
|
if (!(await requireSkill(await getRow(skillId, tx), subject, "read", tx, roles))) return null;
|
|
1043
1082
|
if (enabled) {
|
|
1044
1083
|
await tx`
|
|
1045
1084
|
DELETE FROM ai.skill_user_disabled
|
|
1046
|
-
WHERE skill_id = ${skillId}::uuid AND user_id = ${subject
|
|
1085
|
+
WHERE skill_id = ${skillId}::uuid AND user_id = ${aiResourceUserId(subject)}::uuid
|
|
1047
1086
|
`;
|
|
1048
1087
|
} else {
|
|
1049
1088
|
await tx`
|
|
1050
1089
|
INSERT INTO ai.skill_user_disabled (skill_id, user_id)
|
|
1051
|
-
VALUES (${skillId}::uuid, ${subject
|
|
1090
|
+
VALUES (${skillId}::uuid, ${aiResourceUserId(subject)}::uuid)
|
|
1052
1091
|
ON CONFLICT (skill_id, user_id) DO NOTHING
|
|
1053
1092
|
`;
|
|
1054
1093
|
}
|
|
@@ -1058,7 +1097,7 @@ export const aiSkills = {
|
|
|
1058
1097
|
|
|
1059
1098
|
async setReference(
|
|
1060
1099
|
skillId: string,
|
|
1061
|
-
subject:
|
|
1100
|
+
subject: AiResourceContext | null,
|
|
1062
1101
|
input: { expectedRevision: number; path: string; content: string },
|
|
1063
1102
|
authority: RequestAuthority | null = null,
|
|
1064
1103
|
): Promise<AiSkill | null> {
|
|
@@ -1075,7 +1114,7 @@ export const aiSkills = {
|
|
|
1075
1114
|
|
|
1076
1115
|
async setReferences(
|
|
1077
1116
|
skillId: string,
|
|
1078
|
-
subject:
|
|
1117
|
+
subject: AiResourceContext | null,
|
|
1079
1118
|
input: { expectedRevision: number; references: readonly AiSkillReferenceInput[] },
|
|
1080
1119
|
authority: RequestAuthority | null = null,
|
|
1081
1120
|
): Promise<AiSkill | null> {
|
|
@@ -1109,7 +1148,7 @@ export const aiSkills = {
|
|
|
1109
1148
|
|
|
1110
1149
|
async removeReference(
|
|
1111
1150
|
skillId: string,
|
|
1112
|
-
subject:
|
|
1151
|
+
subject: AiResourceContext | null,
|
|
1113
1152
|
input: { expectedRevision: number; path: string },
|
|
1114
1153
|
authority: RequestAuthority | null = null,
|
|
1115
1154
|
): Promise<AiSkill | null> {
|
|
@@ -1135,7 +1174,7 @@ export const aiSkills = {
|
|
|
1135
1174
|
async loadForTurn(
|
|
1136
1175
|
turnId: string,
|
|
1137
1176
|
selector: string | { id: string },
|
|
1138
|
-
subject:
|
|
1177
|
+
subject: AiResourceContext | null,
|
|
1139
1178
|
authority: RequestAuthority | null = null,
|
|
1140
1179
|
): Promise<AiLoadedSkillSnapshot | null> {
|
|
1141
1180
|
const name = typeof selector === "string" ? validateAiSkillName(selector) : null;
|
|
@@ -1181,7 +1220,7 @@ export const aiSkills = {
|
|
|
1181
1220
|
|
|
1182
1221
|
async listTurnFiles(
|
|
1183
1222
|
turnId: string,
|
|
1184
|
-
subject:
|
|
1223
|
+
subject: AiResourceContext | null,
|
|
1185
1224
|
authority: RequestAuthority | null = null,
|
|
1186
1225
|
): Promise<AiSkillFileToolStat[]> {
|
|
1187
1226
|
return listSkillTurnFiles(turnId, subject, await subjectRoles(subject, authority));
|
|
@@ -1190,7 +1229,7 @@ export const aiSkills = {
|
|
|
1190
1229
|
async readTurnFile(
|
|
1191
1230
|
turnId: string,
|
|
1192
1231
|
path: string,
|
|
1193
|
-
subject:
|
|
1232
|
+
subject: AiResourceContext | null,
|
|
1194
1233
|
authority: RequestAuthority | null = null,
|
|
1195
1234
|
): Promise<AiSkillFileToolContent | null> {
|
|
1196
1235
|
const roles = await subjectRoles(subject, authority);
|
|
@@ -1198,7 +1237,7 @@ export const aiSkills = {
|
|
|
1198
1237
|
const stat = files.find((file) => file.path === path);
|
|
1199
1238
|
if (!stat) return null;
|
|
1200
1239
|
const [skillName] = path.split("/", 1);
|
|
1201
|
-
const userId = subject
|
|
1240
|
+
const userId = aiResourceUserId(subject);
|
|
1202
1241
|
const [row] = await sql<SnapshotRow[]>`
|
|
1203
1242
|
SELECT DISTINCT snapshot.skill_name, snapshot.description, snapshot.revision, snapshot.instructions,
|
|
1204
1243
|
snapshot.files, snapshot.loaded_at
|
package/src/ai/store.ts
CHANGED
|
@@ -7,12 +7,14 @@ import { type CapabilityActionReviewPresentation, CapabilityActionReviewPresenta
|
|
|
7
7
|
import { logger } from "../services/logging";
|
|
8
8
|
import { toPgTextArray } from "../services/postgres";
|
|
9
9
|
import { AI_TOOL_CHECK_STALE_MS, type AiActionCheck, type AiStoredActionReview } from "./action-review";
|
|
10
|
+
import { AiMapSchema } from "./map-contracts";
|
|
10
11
|
|
|
11
12
|
export { AI_TOOL_CHECK_STALE_MS };
|
|
12
13
|
|
|
13
14
|
import { AI_WEBSITE_APPROVAL_TOOL } from "./approvals";
|
|
14
15
|
import { projectSettledAiToolChecks } from "./capability-result";
|
|
15
16
|
import { aiTurnErrorText } from "./chat/turn-error";
|
|
17
|
+
import { projectAiMaps } from "./map-store";
|
|
16
18
|
import { AI_MEMORY_LEARNING_DEFAULT_ENABLED } from "./prefs";
|
|
17
19
|
import type { AiTurnBlock } from "./protocol";
|
|
18
20
|
import { collectRecentSentResourceRefs, RECENT_SENT_CONVERSATION_SCAN_LIMIT, RECENT_SENT_MESSAGE_SCAN_LIMIT } from "./recent-sent-refs";
|
|
@@ -1515,6 +1517,17 @@ export const aiConversations: AiConversationService = {
|
|
|
1515
1517
|
SELECT ${target.id}::uuid, path, bytes, media_type, size, origin, dictation_recorded_at, updated_at, version
|
|
1516
1518
|
FROM ai.files WHERE conversation_id = ${input.sourceConversationId}::uuid
|
|
1517
1519
|
`;
|
|
1520
|
+
// A fork owns independent snapshots. Rewrite only copied map references, never the source conversation.
|
|
1521
|
+
const mapRows = await tx<
|
|
1522
|
+
{ id: string; snapshot: unknown; retry: unknown }[]
|
|
1523
|
+
>`SELECT id,snapshot,retry FROM ai.maps WHERE conversation_id=${input.sourceConversationId}::uuid AND (id IN (SELECT (message->'result'->'map'->>'id')::uuid FROM ai.messages WHERE conversation_id=${target.id}::uuid AND role='tool_result' AND message->>'name'='map') OR snapshot->>'originCallId' IN (SELECT block->>'id' FROM ai.messages CROSS JOIN LATERAL jsonb_array_elements(message->'content') block WHERE conversation_id=${target.id}::uuid AND role='assistant' AND block->>'type'='tool_call' AND block->>'name'='map'))`;
|
|
1524
|
+
for (const saved of mapRows) {
|
|
1525
|
+
const id = crypto.randomUUID();
|
|
1526
|
+
const snapshot = AiMapSchema.parse(typeof saved.snapshot === "string" ? JSON.parse(saved.snapshot) : saved.snapshot);
|
|
1527
|
+
const next = { ...snapshot, id, conversationId: target.short_id };
|
|
1528
|
+
await tx`INSERT INTO ai.maps(id,conversation_id,revision,snapshot,retry) VALUES(${id}::uuid,${target.id}::uuid,${snapshot.revision},${JSON.stringify(next)}::text::jsonb,${saved.retry ? JSON.stringify(saved.retry) : null}::text::jsonb)`;
|
|
1529
|
+
await tx`UPDATE ai.messages SET message=jsonb_set(message,'{result,map}',${JSON.stringify({ ...next })}::text::jsonb) WHERE conversation_id=${target.id}::uuid AND role='tool_result' AND message->>'name'='map' AND message->'result'->'map'->>'id'=${saved.id}`;
|
|
1530
|
+
}
|
|
1518
1531
|
return rowToConversation(target);
|
|
1519
1532
|
});
|
|
1520
1533
|
},
|
|
@@ -2569,7 +2582,7 @@ export const aiConversations: AiConversationService = {
|
|
|
2569
2582
|
AND NOT (kind = 'summary' AND compacted_at IS NOT NULL)
|
|
2570
2583
|
ORDER BY seq ASC, (kind = 'summary')::int ASC
|
|
2571
2584
|
`;
|
|
2572
|
-
return rows.map(rowToMessage);
|
|
2585
|
+
return projectAiMaps(rows.map(rowToMessage));
|
|
2573
2586
|
},
|
|
2574
2587
|
|
|
2575
2588
|
listMessagesPage: async (input) => {
|
|
@@ -2598,7 +2611,7 @@ export const aiConversations: AiConversationService = {
|
|
|
2598
2611
|
AND NOT (kind = 'summary' AND compacted_at IS NOT NULL)
|
|
2599
2612
|
ORDER BY seq ASC, (kind = 'summary')::int ASC
|
|
2600
2613
|
`;
|
|
2601
|
-
const messages = rows.map(rowToMessage);
|
|
2614
|
+
const messages = await projectAiMaps(rows.map(rowToMessage));
|
|
2602
2615
|
const oldestSeq = messages[0]?.seq;
|
|
2603
2616
|
if (oldestSeq === undefined) return { messages, hasMore: false };
|
|
2604
2617
|
const older = await sql<{ exists: boolean }[]>`
|
|
@@ -2639,7 +2652,7 @@ export const aiConversations: AiConversationService = {
|
|
|
2639
2652
|
`;
|
|
2640
2653
|
const page = rows.filter((row) => Number(row.page_rank) <= limit);
|
|
2641
2654
|
return {
|
|
2642
|
-
messages: page.map(rowToMessage),
|
|
2655
|
+
messages: await projectAiMaps(page.map(rowToMessage)),
|
|
2643
2656
|
...(rows.some((row) => Number(row.page_rank) > limit) && page.at(-1) ? { nextCursor: String(page.at(-1)!.seq) } : {}),
|
|
2644
2657
|
};
|
|
2645
2658
|
},
|
|
@@ -2758,7 +2771,7 @@ export const aiConversations: AiConversationService = {
|
|
|
2758
2771
|
AND compacted_at IS NULL
|
|
2759
2772
|
ORDER BY seq ASC
|
|
2760
2773
|
`;
|
|
2761
|
-
return rows.map(rowToMessage);
|
|
2774
|
+
return projectAiMaps(rows.map(rowToMessage));
|
|
2762
2775
|
},
|
|
2763
2776
|
|
|
2764
2777
|
listTurnMessages: async (input) => {
|
|
@@ -2773,7 +2786,7 @@ export const aiConversations: AiConversationService = {
|
|
|
2773
2786
|
AND (${input.includeCompacted ?? false} OR compacted_at IS NULL)
|
|
2774
2787
|
ORDER BY seq ASC
|
|
2775
2788
|
`;
|
|
2776
|
-
return rows.map(rowToMessage);
|
|
2789
|
+
return projectAiMaps(rows.map(rowToMessage));
|
|
2777
2790
|
},
|
|
2778
2791
|
|
|
2779
2792
|
setMessageFeedback: async (input) => {
|
|
@@ -3968,7 +3981,7 @@ export const aiConversations: AiConversationService = {
|
|
|
3968
3981
|
).map(rowToMessage)
|
|
3969
3982
|
: await aiConversations.listContextMessages({ conversationId: input.conversationId });
|
|
3970
3983
|
return projectSettledAiToolChecks(
|
|
3971
|
-
rows.map((row) => ({
|
|
3984
|
+
(await projectAiMaps(rows, true)).map((row) => ({
|
|
3972
3985
|
seq: row.seq,
|
|
3973
3986
|
kind: row.kind,
|
|
3974
3987
|
meta: row.meta,
|
package/src/ai/tools.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { z } from "zod";
|
|
|
4
4
|
import type { CapabilityActionCallReview } from "../contracts/capabilities";
|
|
5
5
|
import type { RequestActor } from "../server";
|
|
6
6
|
import { aiToolNeedsApproval } from "./approvals";
|
|
7
|
+
import { resolveAiToolResultMaxChars } from "./tool-result-budget";
|
|
7
8
|
import type {
|
|
8
9
|
AiApprovalTarget,
|
|
9
10
|
AiDataBoundary,
|
|
@@ -64,6 +65,8 @@ export const defineAiTool = <TInput extends z.ZodType, TOutput extends z.ZodType
|
|
|
64
65
|
timeZone?: string;
|
|
65
66
|
/** Localized short status; never include payloads or secrets. */
|
|
66
67
|
reportProgress?: (message: string) => Promise<void>;
|
|
68
|
+
/** A validated result snapshot while a server tool is still running. */
|
|
69
|
+
reportResult?: (result: z.infer<TOutput>) => Promise<void>;
|
|
67
70
|
/** Asks like `requestApproval`, and lets the person remember the approval of `target`. */
|
|
68
71
|
requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
|
|
69
72
|
/** Relays a decision for an effect another platform component consumes and executes. */
|
|
@@ -117,6 +120,8 @@ export type AiToolPreparationContext = {
|
|
|
117
120
|
locale?: string;
|
|
118
121
|
timeZone?: string;
|
|
119
122
|
reportToolProgress?: (callId: string, message: string) => Promise<void>;
|
|
123
|
+
reportToolResult?: (callId: string, result: unknown) => Promise<void>;
|
|
124
|
+
maxToolResultChars?: number;
|
|
120
125
|
/** Uses the approved snapshot immediately before the tool executes its effect. */
|
|
121
126
|
consumeApproval?: (approvalCallId: string, input: unknown) => Promise<AiApprovalUse>;
|
|
122
127
|
/** Links a relayed decision to the record the effect owner will consume. */
|
|
@@ -241,6 +246,15 @@ export const prepareAiTools = (input: AiToolPreparationContext & { tools?: AiRun
|
|
|
241
246
|
locale: input.locale,
|
|
242
247
|
timeZone: input.timeZone,
|
|
243
248
|
reportProgress: callId && report ? (message) => report(callId, message) : undefined,
|
|
249
|
+
reportResult:
|
|
250
|
+
callId && input.reportToolResult
|
|
251
|
+
? async (result) => {
|
|
252
|
+
const validated = tool.def.outputSchema.parse(result);
|
|
253
|
+
const maximum = input.maxToolResultChars ?? resolveAiToolResultMaxChars({ configuredMaxChars: Number.MAX_SAFE_INTEGER });
|
|
254
|
+
if (JSON.stringify(validated).length > maximum) throw new Error("AI_TOOL_INTERIM_RESULT_TOO_LARGE");
|
|
255
|
+
await input.reportToolResult!(callId, validated);
|
|
256
|
+
}
|
|
257
|
+
: undefined,
|
|
244
258
|
reportWebsiteReceipts:
|
|
245
259
|
callId && input.reportWebsiteReceipts ? (receipts) => input.reportWebsiteReceipts!(callId, receipts) : undefined,
|
|
246
260
|
});
|