@k2b/cloud 0.44.0 → 0.45.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 +6 -2
- 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/dictation-runtime.ts +6 -2
- package/src/ai/executor.ts +9 -3
- package/src/ai/index.ts +1 -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/recent-resources.ts +3 -3
- package/src/ai/routes.ts +30 -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/api/accounts-entities.ts +6 -3
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/auth.ts +9 -2
- 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/contracts/shared.ts +2 -1
- 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 +30 -13
- package/src/services/accounts/entities.ts +29 -16
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +16 -13
- package/src/services/accounts/identities.ts +8 -1
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +69 -4
- package/src/services/accounts/users.ts +38 -8
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +1 -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/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/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +2 -1
package/src/ai/routes.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { ErrorResponseSchema } from "../contracts";
|
|
|
8
8
|
import { APPROVAL_CLIENT_HEADER } from "../contracts/approvals";
|
|
9
9
|
import { capabilityActionPolicy } from "../contracts/capabilities";
|
|
10
10
|
import {
|
|
11
|
-
type AccessSubject,
|
|
12
11
|
type AuthContext,
|
|
13
12
|
auth,
|
|
14
13
|
err,
|
|
@@ -80,6 +79,7 @@ import {
|
|
|
80
79
|
} from "./message-queue";
|
|
81
80
|
import { aiActorUser, aiPrefsUserId, aiUserPrefs } from "./prefs";
|
|
82
81
|
import { aiProjects } from "./projects";
|
|
82
|
+
import type { AiResourceContext } from "./projects-authorization";
|
|
83
83
|
import { projectPublicAiStoredMessages, publicAiStoredMessages } from "./public-projection";
|
|
84
84
|
import { AiRecentResourceSchema, listAiRecentResources } from "./recent-resources";
|
|
85
85
|
import { RECENT_SENT_REF_LIMIT } from "./recent-sent-refs";
|
|
@@ -224,6 +224,8 @@ const MemoryCreateSchema = z.object({
|
|
|
224
224
|
const MemoryUpdateSchema = MemoryCreateSchema.partial().refine((value) => Object.keys(value).length > 0, "Pass a changed field.");
|
|
225
225
|
const MemoryIdSchema = z.string().regex(AI_SHORT_ID_PATTERN);
|
|
226
226
|
|
|
227
|
+
const resourceContext = (c: Context<AuthContext>): AiResourceContext => ({ actor: c.get("actor"), accessSubject: c.get("accessSubject") });
|
|
228
|
+
|
|
227
229
|
const notFound = (c: Context<AuthContext>) => respond(c, fail(err.notFound("Conversation")));
|
|
228
230
|
const fileNotFound = (c: Context<AuthContext>) => respond(c, fail(err.notFound("File")));
|
|
229
231
|
|
|
@@ -232,14 +234,14 @@ const publicConversation = (conversation: AiConversation, projectId: string | nu
|
|
|
232
234
|
id: conversation.shortId,
|
|
233
235
|
projectId: conversation.projectId ? projectId : null,
|
|
234
236
|
});
|
|
235
|
-
const publicConversations = async (conversations: AiConversation[], subject:
|
|
237
|
+
const publicConversations = async (conversations: AiConversation[], subject: AiResourceContext) => {
|
|
236
238
|
const projectIds = conversations.flatMap((conversation) => (conversation.projectId ? [conversation.projectId] : []));
|
|
237
239
|
const shortIds = await aiProjects.resolveShortIds(projectIds, subject);
|
|
238
240
|
return conversations.map((conversation) =>
|
|
239
241
|
publicConversation(conversation, conversation.projectId ? (shortIds.get(conversation.projectId) ?? null) : null),
|
|
240
242
|
);
|
|
241
243
|
};
|
|
242
|
-
const publicConversationFor = async (conversation: AiConversation, subject:
|
|
244
|
+
const publicConversationFor = async (conversation: AiConversation, subject: AiResourceContext) =>
|
|
243
245
|
(await publicConversations([conversation], subject))[0]!;
|
|
244
246
|
const publicMemories = async (userId: string, memories: Awaited<ReturnType<typeof aiMemories.list>>) => {
|
|
245
247
|
const sourceIds = memories.flatMap((memory) => (memory.sourceConversationId ? [memory.sourceConversationId] : []));
|
|
@@ -260,7 +262,7 @@ const rememberLastUsedModel = (actor: RequestActor, model: import("../shared/ai-
|
|
|
260
262
|
void aiUserPrefs.update(userId, { selection: model }).catch(() => undefined);
|
|
261
263
|
};
|
|
262
264
|
|
|
263
|
-
const conversationDetail = async (conversation: AiConversation, subject:
|
|
265
|
+
const conversationDetail = async (conversation: AiConversation, subject: AiResourceContext) => {
|
|
264
266
|
const [state, timeline, publicView] = await Promise.all([
|
|
265
267
|
loadAiStreamState(conversation),
|
|
266
268
|
aiConversations.listConversationTimeline({ conversationId: conversation.id }),
|
|
@@ -297,7 +299,7 @@ const prepareConversationForMessageRetry = async (conversationId: string): Promi
|
|
|
297
299
|
|
|
298
300
|
const prepareLaunchDraft = async (
|
|
299
301
|
body: z.infer<typeof AiCreateConversationInputSchema>,
|
|
300
|
-
subject:
|
|
302
|
+
subject: AiResourceContext | null,
|
|
301
303
|
authority: import("../server").RequestAuthority | null = null,
|
|
302
304
|
) => {
|
|
303
305
|
const content = [...(body.draft?.content ?? [])];
|
|
@@ -446,7 +448,7 @@ export const aiRoutes = (() => {
|
|
|
446
448
|
}
|
|
447
449
|
const toolsSupported = Boolean(previewProfile?.capabilities.includes("tools"));
|
|
448
450
|
const availableSkills = toolsSupported
|
|
449
|
-
? (await aiSkills.list(c
|
|
451
|
+
? (await aiSkills.list(resourceContext(c), { authority: appAccessAuthority(c) })).filter((skill) => skill.enabled)
|
|
450
452
|
: [];
|
|
451
453
|
const skillCatalog = selectAiSkillCatalog(
|
|
452
454
|
availableSkills.filter((skill) => !skill.manualOnly).map(({ name, description }) => ({ name, description })),
|
|
@@ -461,8 +463,8 @@ export const aiRoutes = (() => {
|
|
|
461
463
|
})),
|
|
462
464
|
...(client?.localTools === "ask" ? [createCloudAiLocalBashTool(), ...createCloudAiLocalTools()] : []),
|
|
463
465
|
...(memoryEnabled ? [createCloudAiMemoryTool()] : []),
|
|
464
|
-
...(availableSkills.length ? [createCloudAiLoadSkillTool(c
|
|
465
|
-
...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(c
|
|
466
|
+
...(availableSkills.length ? [createCloudAiLoadSkillTool(resourceContext(c), appAccessAuthority(c))] : []),
|
|
467
|
+
...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(resourceContext(c), [], appAccessAuthority(c))] : []),
|
|
466
468
|
]
|
|
467
469
|
: [];
|
|
468
470
|
const memoryToolEnabled = tools.some((tool) => tool.def.name === "memory");
|
|
@@ -545,7 +547,7 @@ export const aiRoutes = (() => {
|
|
|
545
547
|
const ctx = await resolveContext(c);
|
|
546
548
|
if (ctx instanceof Response) return ctx;
|
|
547
549
|
const query = c.req.valid("query");
|
|
548
|
-
const project = query.projectId ? await aiProjects.getByShortId(query.projectId, c
|
|
550
|
+
const project = query.projectId ? await aiProjects.getByShortId(query.projectId, resourceContext(c), "read") : null;
|
|
549
551
|
if (query.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
550
552
|
return respond(
|
|
551
553
|
c,
|
|
@@ -561,7 +563,7 @@ export const aiRoutes = (() => {
|
|
|
561
563
|
unassigned: query.unassigned,
|
|
562
564
|
limit: query.limit,
|
|
563
565
|
}),
|
|
564
|
-
c
|
|
566
|
+
resourceContext(c),
|
|
565
567
|
),
|
|
566
568
|
),
|
|
567
569
|
);
|
|
@@ -570,7 +572,7 @@ export const aiRoutes = (() => {
|
|
|
570
572
|
const ctx = await resolveContext(c);
|
|
571
573
|
if (ctx instanceof Response) return ctx;
|
|
572
574
|
const query = c.req.valid("query");
|
|
573
|
-
const project = query.projectId ? await aiProjects.getByShortId(query.projectId, c
|
|
575
|
+
const project = query.projectId ? await aiProjects.getByShortId(query.projectId, resourceContext(c), "read") : null;
|
|
574
576
|
if (query.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
575
577
|
const page = await aiConversations.listConversationsPage({
|
|
576
578
|
ownerUserId: ctx.ownerUserId,
|
|
@@ -583,15 +585,15 @@ export const aiRoutes = (() => {
|
|
|
583
585
|
page: query.page ?? 1,
|
|
584
586
|
perPage: query.perPage ?? 20,
|
|
585
587
|
});
|
|
586
|
-
return respond(c, ok({ ...page, items: await publicConversations(page.items, c
|
|
588
|
+
return respond(c, ok({ ...page, items: await publicConversations(page.items, resourceContext(c)) }));
|
|
587
589
|
})
|
|
588
590
|
.post("/conversations", v("json", AiCreateConversationInputSchema), async (c) => {
|
|
589
591
|
const ctx = await resolveContext(c);
|
|
590
592
|
if (ctx instanceof Response) return ctx;
|
|
591
593
|
const body = c.req.valid("json");
|
|
592
|
-
const project = body.projectId ? await aiProjects.getByShortId(body.projectId, c
|
|
594
|
+
const project = body.projectId ? await aiProjects.getByShortId(body.projectId, resourceContext(c), "read") : null;
|
|
593
595
|
if (body.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
594
|
-
const draft = await prepareLaunchDraft(body, c
|
|
596
|
+
const draft = await prepareLaunchDraft(body, resourceContext(c), appAccessAuthority(c));
|
|
595
597
|
if (!draft.ok) return respond(c, draft);
|
|
596
598
|
const requestedCapabilityTools = (body.preloadTools ?? []).filter(
|
|
597
599
|
(requested): requested is { appId: string; kind: "query" | "action"; id: string } => "appId" in requested,
|
|
@@ -642,9 +644,7 @@ export const aiRoutes = (() => {
|
|
|
642
644
|
launchedByAppId: body.launchedByAppId,
|
|
643
645
|
});
|
|
644
646
|
// A replay may return a conversation assigned to a Project since creation.
|
|
645
|
-
const currentProject = conversation.projectId
|
|
646
|
-
? await aiProjects.get(conversation.projectId, c.get("accessSubject"), "read")
|
|
647
|
-
: null;
|
|
647
|
+
const currentProject = conversation.projectId ? await aiProjects.get(conversation.projectId, resourceContext(c), "read") : null;
|
|
648
648
|
if (conversation.projectId && !currentProject) return respond(c, fail(err.notFound("Project")));
|
|
649
649
|
return respond(c, ok(publicConversation(conversation, currentProject?.shortId ?? null)), 201);
|
|
650
650
|
} catch (error) {
|
|
@@ -659,7 +659,7 @@ export const aiRoutes = (() => {
|
|
|
659
659
|
if (ctx instanceof Response) return ctx;
|
|
660
660
|
const conversation = await loadConversation(c, ctx);
|
|
661
661
|
if (!conversation) return notFound(c);
|
|
662
|
-
return respond(c, ok(await conversationDetail(conversation, c
|
|
662
|
+
return respond(c, ok(await conversationDetail(conversation, resourceContext(c))));
|
|
663
663
|
})
|
|
664
664
|
.get(
|
|
665
665
|
"/conversations/:conversationId/submission/:draftRevision",
|
|
@@ -669,7 +669,7 @@ export const aiRoutes = (() => {
|
|
|
669
669
|
if (ctx instanceof Response) return ctx;
|
|
670
670
|
const conversation = await loadConversation(c, ctx);
|
|
671
671
|
if (!conversation) return notFound(c);
|
|
672
|
-
if (conversation.projectId && !(await aiProjects.get(conversation.projectId, c
|
|
672
|
+
if (conversation.projectId && !(await aiProjects.get(conversation.projectId, resourceContext(c), "read"))) return notFound(c);
|
|
673
673
|
const submission = await aiConversations.getDraftSubmission({
|
|
674
674
|
conversationId: conversation.id,
|
|
675
675
|
ownerUserId: ctx.ownerUserId,
|
|
@@ -797,7 +797,7 @@ export const aiRoutes = (() => {
|
|
|
797
797
|
pinned: body.pinned,
|
|
798
798
|
});
|
|
799
799
|
if (!updated) return notFound(c);
|
|
800
|
-
return respond(c, ok(await publicConversationFor(updated, c
|
|
800
|
+
return respond(c, ok(await publicConversationFor(updated, resourceContext(c))));
|
|
801
801
|
})
|
|
802
802
|
.put("/conversations/:conversationId/project", v("json", ConversationProjectInputSchema), async (c) => {
|
|
803
803
|
const ctx = await resolveContext(c);
|
|
@@ -805,7 +805,7 @@ export const aiRoutes = (() => {
|
|
|
805
805
|
const conversation = await loadConversation(c, ctx);
|
|
806
806
|
if (!conversation) return notFound(c);
|
|
807
807
|
const body = c.req.valid("json");
|
|
808
|
-
const project = body.projectId ? await aiProjects.getByShortId(body.projectId, c
|
|
808
|
+
const project = body.projectId ? await aiProjects.getByShortId(body.projectId, resourceContext(c), "read") : null;
|
|
809
809
|
if (body.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
810
810
|
const result = await aiConversations.setConversationProject({
|
|
811
811
|
conversationId: conversation.id,
|
|
@@ -820,7 +820,7 @@ export const aiRoutes = (() => {
|
|
|
820
820
|
}
|
|
821
821
|
return notFound(c);
|
|
822
822
|
}
|
|
823
|
-
return respond(c, ok(await publicConversationFor(result.conversation, c
|
|
823
|
+
return respond(c, ok(await publicConversationFor(result.conversation, resourceContext(c))));
|
|
824
824
|
})
|
|
825
825
|
.post("/conversations/:conversationId/pin", async (c) => {
|
|
826
826
|
const ctx = await resolveContext(c);
|
|
@@ -832,7 +832,7 @@ export const aiRoutes = (() => {
|
|
|
832
832
|
ownerUserId: ctx.ownerUserId,
|
|
833
833
|
pinned: true,
|
|
834
834
|
});
|
|
835
|
-
return updated ? respond(c, ok(await publicConversationFor(updated, c
|
|
835
|
+
return updated ? respond(c, ok(await publicConversationFor(updated, resourceContext(c)))) : notFound(c);
|
|
836
836
|
})
|
|
837
837
|
.delete("/conversations/:conversationId/pin", async (c) => {
|
|
838
838
|
const ctx = await resolveContext(c);
|
|
@@ -844,7 +844,7 @@ export const aiRoutes = (() => {
|
|
|
844
844
|
ownerUserId: ctx.ownerUserId,
|
|
845
845
|
pinned: false,
|
|
846
846
|
});
|
|
847
|
-
return updated ? respond(c, ok(await publicConversationFor(updated, c
|
|
847
|
+
return updated ? respond(c, ok(await publicConversationFor(updated, resourceContext(c)))) : notFound(c);
|
|
848
848
|
})
|
|
849
849
|
.put("/conversations/:conversationId/done", v("json", z.object({ done: z.boolean().nullable() })), async (c) => {
|
|
850
850
|
const ctx = await resolveContext(c);
|
|
@@ -860,7 +860,7 @@ export const aiRoutes = (() => {
|
|
|
860
860
|
return result.reason === "active_turn"
|
|
861
861
|
? respond(c, fail(err.conflict("Stop the current response before marking this chat done.")))
|
|
862
862
|
: notFound(c);
|
|
863
|
-
return respond(c, ok(await publicConversationFor(result.conversation, c
|
|
863
|
+
return respond(c, ok(await publicConversationFor(result.conversation, resourceContext(c))));
|
|
864
864
|
})
|
|
865
865
|
.post("/conversations/:conversationId/archive", async (c) => {
|
|
866
866
|
const ctx = await resolveContext(c);
|
|
@@ -910,7 +910,7 @@ export const aiRoutes = (() => {
|
|
|
910
910
|
error: error instanceof Error ? error.message : String(error),
|
|
911
911
|
}),
|
|
912
912
|
);
|
|
913
|
-
return restored ? respond(c, ok(await publicConversationFor(restored, c
|
|
913
|
+
return restored ? respond(c, ok(await publicConversationFor(restored, resourceContext(c)))) : notFound(c);
|
|
914
914
|
})
|
|
915
915
|
.post("/conversations/:conversationId/viewed", async (c) => {
|
|
916
916
|
const ctx = await resolveContext(c);
|
|
@@ -1007,7 +1007,7 @@ export const aiRoutes = (() => {
|
|
|
1007
1007
|
};
|
|
1008
1008
|
});
|
|
1009
1009
|
const { input, message } = aiInputToUserMessage(aiTurnInputToContent({ content: turnContent }));
|
|
1010
|
-
const project = conversation.projectId ? await aiProjects.snapshot(conversation.projectId, c
|
|
1010
|
+
const project = conversation.projectId ? await aiProjects.snapshot(conversation.projectId, resourceContext(c)) : null;
|
|
1011
1011
|
if (conversation.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
1012
1012
|
try {
|
|
1013
1013
|
const submission = {
|
|
@@ -1110,7 +1110,7 @@ export const aiRoutes = (() => {
|
|
|
1110
1110
|
? await aiConversations.getTurnRunConfig({ conversationId: conversation.id, turnId: target.loopId })
|
|
1111
1111
|
: null;
|
|
1112
1112
|
const originalProject = originalRunConfig?.kind !== "compact" ? originalRunConfig?.project : undefined;
|
|
1113
|
-
const currentProject = originalProject?.id ? await aiProjects.getByShortId(originalProject.id, c
|
|
1113
|
+
const currentProject = originalProject?.id ? await aiProjects.getByShortId(originalProject.id, resourceContext(c)) : null;
|
|
1114
1114
|
if (originalProject && !currentProject) return respond(c, fail(err.notFound("Project")));
|
|
1115
1115
|
|
|
1116
1116
|
try {
|
|
@@ -1189,7 +1189,7 @@ export const aiRoutes = (() => {
|
|
|
1189
1189
|
}
|
|
1190
1190
|
|
|
1191
1191
|
const body = c.req.valid("json");
|
|
1192
|
-
if (conversation.projectId && !(await aiProjects.get(conversation.projectId, c
|
|
1192
|
+
if (conversation.projectId && !(await aiProjects.get(conversation.projectId, resourceContext(c), "read"))) {
|
|
1193
1193
|
return respond(c, fail(err.notFound("Project")));
|
|
1194
1194
|
}
|
|
1195
1195
|
const forked = await aiConversations.forkConversation({
|
|
@@ -1198,7 +1198,7 @@ export const aiRoutes = (() => {
|
|
|
1198
1198
|
ownerUserId: ctx.ownerUserId,
|
|
1199
1199
|
title: body.title ?? conversation.title,
|
|
1200
1200
|
});
|
|
1201
|
-
return respond(c, ok(await conversationDetail(forked, c
|
|
1201
|
+
return respond(c, ok(await conversationDetail(forked, resourceContext(c))));
|
|
1202
1202
|
})
|
|
1203
1203
|
.get("/conversations/:conversationId/enrichment", async (c) => {
|
|
1204
1204
|
const ctx = await resolveContext(c);
|
package/src/ai/skill-tool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type { AccessSubject } from "../server";
|
|
3
2
|
import type { RequestAuthority } from "../server/middleware/auth";
|
|
4
3
|
import { AI_SKILL_FILE_MOUNT } from "./file-mount";
|
|
4
|
+
import type { AiResourceContext } from "./projects-authorization";
|
|
5
5
|
import { AI_SHORT_ID_PATTERN } from "./short-id";
|
|
6
6
|
import { type AiSkillCatalogEntry, mergeAiSkillCatalog, rankAiSkillCatalog, searchAiSkillCatalog } from "./skill-catalog";
|
|
7
7
|
import { AI_SKILL_NAME_MAX_CHARS, AI_SKILL_NAME_PATTERN } from "./skill-format";
|
|
@@ -12,7 +12,7 @@ import { defineAiTool } from "./tools";
|
|
|
12
12
|
export async function loadSelectedAiSkills(
|
|
13
13
|
ids: readonly string[],
|
|
14
14
|
turnId: string,
|
|
15
|
-
subject:
|
|
15
|
+
subject: AiResourceContext | null,
|
|
16
16
|
allowed: boolean,
|
|
17
17
|
authority: RequestAuthority | null = null,
|
|
18
18
|
) {
|
|
@@ -58,7 +58,7 @@ export const CloudAiLoadSkillOutputSchema = z.object({
|
|
|
58
58
|
files: z.array(z.string()),
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
export const createCloudAiLoadSkillTool = (subject:
|
|
61
|
+
export const createCloudAiLoadSkillTool = (subject: AiResourceContext, authority: RequestAuthority | null = null) =>
|
|
62
62
|
defineAiTool({
|
|
63
63
|
name: "load_skill",
|
|
64
64
|
description:
|
|
@@ -92,7 +92,7 @@ export const CloudAiSearchSkillsOutputSchema = z.object({
|
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
export const createCloudAiSearchSkillsTool = (
|
|
95
|
-
subject:
|
|
95
|
+
subject: AiResourceContext | null,
|
|
96
96
|
localSkills: readonly AiSkillCatalogEntry[] = [],
|
|
97
97
|
authority: RequestAuthority | null = null,
|
|
98
98
|
) =>
|
package/src/ai/skills-routes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Hono, type MiddlewareHandler } from "hono";
|
|
1
|
+
import { type Context, Hono, type MiddlewareHandler } from "hono";
|
|
2
2
|
import { HTTPException } from "hono/http-exception";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { getApp } from "../_internal/registry";
|
|
@@ -10,6 +10,7 @@ import { appAccessAuthority, canUseApp } from "../services/app-access";
|
|
|
10
10
|
import { recordAppAccessDenied } from "../services/app-access-metrics";
|
|
11
11
|
import { localizedAppName } from "../ssr/runtime";
|
|
12
12
|
import { aiProjects } from "./projects";
|
|
13
|
+
import { AiResourceCredentialError } from "./projects-authorization";
|
|
13
14
|
import {
|
|
14
15
|
AI_SKILL_DESCRIPTION_MAX_CHARS,
|
|
15
16
|
AI_SKILL_EXTRA_FRONTMATTER_MAX_CHARS,
|
|
@@ -61,6 +62,11 @@ const publicSkill = (c: RouteContext, skill: AiSkill): AiSkill => ({ ...skill, i
|
|
|
61
62
|
const isUniqueViolation = (error: unknown): boolean =>
|
|
62
63
|
typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === "23505";
|
|
63
64
|
|
|
65
|
+
const resourceSubject = (c: Context<AuthContext>) => ({
|
|
66
|
+
actor: c.get("actor"),
|
|
67
|
+
accessSubject: c.get("accessSubject"),
|
|
68
|
+
});
|
|
69
|
+
|
|
64
70
|
type AiSkillsRouteDependencies = {
|
|
65
71
|
limit?: MiddlewareHandler<AuthContext>;
|
|
66
72
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
@@ -85,6 +91,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
|
85
91
|
// The list combines skills from separate grants; the writer of one can rename it next to another skill's text.
|
|
86
92
|
.use("*", noTransform())
|
|
87
93
|
.onError((error, c) => {
|
|
94
|
+
if (error instanceof AiResourceCredentialError) return respond(c, fail(err.forbidden(error.message)));
|
|
88
95
|
if (error instanceof AiSkillAppForbiddenError) return respond(c, fail(err.forbidden(error.message)));
|
|
89
96
|
if (error instanceof HTTPException) return error.getResponse();
|
|
90
97
|
throw error;
|
|
@@ -100,7 +107,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
|
100
107
|
.refine(({ q, name }) => q === undefined || name === undefined, "Use either q or name, not both."),
|
|
101
108
|
),
|
|
102
109
|
async (c) => {
|
|
103
|
-
const subject = c
|
|
110
|
+
const subject = resourceSubject(c);
|
|
104
111
|
const { q, name } = c.req.valid("query");
|
|
105
112
|
const skills =
|
|
106
113
|
name !== undefined
|
|
@@ -112,7 +119,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
|
112
119
|
},
|
|
113
120
|
)
|
|
114
121
|
.get("/:skillId", async (c) => {
|
|
115
|
-
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c
|
|
122
|
+
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "read", appAccessAuthority(c));
|
|
116
123
|
return skill ? respond(c, ok({ skill: publicSkill(c, skill) })) : respond(c, fail(err.notFound("Skill")));
|
|
117
124
|
})
|
|
118
125
|
.use("*", requireCoreAccess)
|
|
@@ -128,7 +135,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
|
128
135
|
}),
|
|
129
136
|
),
|
|
130
137
|
async (c) => {
|
|
131
|
-
const subject = c
|
|
138
|
+
const subject = resourceSubject(c),
|
|
132
139
|
query = c.req.valid("query");
|
|
133
140
|
const project = await aiProjects.getByShortId(c.req.param("projectId")!, subject, query.available === "true" ? "admin" : "read");
|
|
134
141
|
const result = project
|
|
@@ -145,7 +152,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
|
145
152
|
},
|
|
146
153
|
)
|
|
147
154
|
.get("/:skillId/projects", async (c) => {
|
|
148
|
-
const subject = c
|
|
155
|
+
const subject = resourceSubject(c);
|
|
149
156
|
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin", appAccessAuthority(c));
|
|
150
157
|
const projects = skill ? await aiSkills.linkedProjects(skill.id, subject, appAccessAuthority(c)) : null;
|
|
151
158
|
return projects
|
|
@@ -153,7 +160,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
|
153
160
|
: respond(c, fail(err.notFound("Skill")));
|
|
154
161
|
})
|
|
155
162
|
.put("/:skillId/projects/:projectId", v("json", z.object({ linked: z.boolean() }).strict()), async (c) => {
|
|
156
|
-
const subject = c
|
|
163
|
+
const subject = resourceSubject(c);
|
|
157
164
|
const [skill, project] = await Promise.all([
|
|
158
165
|
aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin", appAccessAuthority(c)),
|
|
159
166
|
aiProjects.getByShortId(c.req.param("projectId")!, subject, "admin"),
|
|
@@ -164,7 +171,7 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
|
164
171
|
: respond(c, fail(err.notFound("Skill or Project")));
|
|
165
172
|
})
|
|
166
173
|
.post("/", dependencies.authenticate ?? auth.requireRole("authenticated"), v("json", SkillFieldsSchema), async (c) => {
|
|
167
|
-
const subject = c
|
|
174
|
+
const subject = resourceSubject(c);
|
|
168
175
|
if (!subject) return respond(c, fail(err.forbidden("Skills require an authenticated access subject.")));
|
|
169
176
|
try {
|
|
170
177
|
return respond(
|
|
@@ -179,10 +186,10 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
|
179
186
|
}
|
|
180
187
|
})
|
|
181
188
|
.put("/:skillId", v("json", UpdateSkillSchema), async (c) => {
|
|
182
|
-
const existing = await aiSkills.getByShortId(c.req.param("skillId")!, c
|
|
189
|
+
const existing = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "write", appAccessAuthority(c));
|
|
183
190
|
if (!existing) return respond(c, fail(err.notFound("Skill")));
|
|
184
191
|
try {
|
|
185
|
-
const skill = await aiSkills.update(existing.id, c
|
|
192
|
+
const skill = await aiSkills.update(existing.id, resourceSubject(c), c.req.valid("json"), appAccessAuthority(c));
|
|
186
193
|
return skill ? respond(c, ok({ skill: publicSkill(c, skill) })) : respond(c, fail(err.notFound("Skill")));
|
|
187
194
|
} catch (error) {
|
|
188
195
|
if (isUniqueViolation(error)) return respond(c, fail(err.conflict("A skill with this name already exists.")));
|
|
@@ -192,37 +199,37 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
|
192
199
|
}
|
|
193
200
|
})
|
|
194
201
|
.delete("/:skillId", async (c) => {
|
|
195
|
-
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c
|
|
196
|
-
return skill && (await aiSkills.delete(skill.id, c
|
|
202
|
+
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "admin", appAccessAuthority(c));
|
|
203
|
+
return skill && (await aiSkills.delete(skill.id, resourceSubject(c), appAccessAuthority(c)))
|
|
197
204
|
? respond(c, ok({ deleted: true }))
|
|
198
205
|
: respond(c, fail(err.notFound("Skill")));
|
|
199
206
|
})
|
|
200
207
|
.put("/:skillId/enabled", dependencies.authenticate ?? auth.requireRole("authenticated"), v("json", SkillEnabledSchema), async (c) => {
|
|
201
|
-
const subject = c
|
|
208
|
+
const subject = resourceSubject(c);
|
|
202
209
|
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, subject, "read", appAccessAuthority(c));
|
|
203
210
|
const enabled = skill ? await aiSkills.setEnabled(skill.id, subject, c.req.valid("json").enabled, appAccessAuthority(c)) : null;
|
|
204
211
|
return enabled === null ? respond(c, fail(err.notFound("Skill"))) : respond(c, ok({ enabled }));
|
|
205
212
|
})
|
|
206
213
|
.get("/:skillId/access", async (c) => {
|
|
207
|
-
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c
|
|
208
|
-
const access = skill ? await aiSkills.listAccess(skill.id, c
|
|
214
|
+
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "admin", appAccessAuthority(c));
|
|
215
|
+
const access = skill ? await aiSkills.listAccess(skill.id, resourceSubject(c), appAccessAuthority(c)) : null;
|
|
209
216
|
return access ? respond(c, ok({ access })) : respond(c, fail(err.notFound("Skill")));
|
|
210
217
|
})
|
|
211
218
|
.post("/:skillId/access", v("json", SkillAccessSchema), async (c) => {
|
|
212
|
-
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c
|
|
219
|
+
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "admin", appAccessAuthority(c));
|
|
213
220
|
const access = skill
|
|
214
|
-
? await aiSkills.grantAccess(skill.id, c
|
|
221
|
+
? await aiSkills.grantAccess(skill.id, resourceSubject(c), c.req.valid("json"), undefined, appAccessAuthority(c))
|
|
215
222
|
: null;
|
|
216
223
|
return access ? respond(c, ok({ access }), 201) : respond(c, fail(err.notFound("Skill")));
|
|
217
224
|
})
|
|
218
225
|
.patch("/:skillId/access/:accessId", v("json", SkillAccessUpdateSchema), async (c) => {
|
|
219
|
-
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c
|
|
226
|
+
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "admin", appAccessAuthority(c));
|
|
220
227
|
try {
|
|
221
228
|
return skill &&
|
|
222
229
|
(await aiSkills.updateAccess(
|
|
223
230
|
skill.id,
|
|
224
231
|
c.req.param("accessId")!,
|
|
225
|
-
c
|
|
232
|
+
resourceSubject(c),
|
|
226
233
|
c.req.valid("json").permission,
|
|
227
234
|
undefined,
|
|
228
235
|
appAccessAuthority(c),
|
|
@@ -235,16 +242,10 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
|
|
|
235
242
|
}
|
|
236
243
|
})
|
|
237
244
|
.delete("/:skillId/access/:accessId", async (c) => {
|
|
238
|
-
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c
|
|
245
|
+
const skill = await aiSkills.getByShortId(c.req.param("skillId")!, resourceSubject(c), "admin", appAccessAuthority(c));
|
|
239
246
|
try {
|
|
240
247
|
return skill &&
|
|
241
|
-
(await aiSkills.revokeAccess(
|
|
242
|
-
skill.id,
|
|
243
|
-
c.req.param("accessId")!,
|
|
244
|
-
c.get("accessSubject") ?? null,
|
|
245
|
-
undefined,
|
|
246
|
-
appAccessAuthority(c),
|
|
247
|
-
))
|
|
248
|
+
(await aiSkills.revokeAccess(skill.id, c.req.param("accessId")!, resourceSubject(c), undefined, appAccessAuthority(c)))
|
|
248
249
|
? respond(c, ok({ deleted: true }))
|
|
249
250
|
: respond(c, fail(err.notFound("Access entry")));
|
|
250
251
|
} catch (error) {
|