@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/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,
|
|
@@ -67,6 +66,7 @@ import {
|
|
|
67
66
|
} from "./http";
|
|
68
67
|
import { AI_LOCAL_TOOL_NAMES, AiLocalClientSchema } from "./local-tool-contracts";
|
|
69
68
|
import { aiMaintenanceJobs } from "./maintenance";
|
|
69
|
+
import { aiMapRoutes } from "./map-routes";
|
|
70
70
|
import { AI_MEMORY_CONTENT_MAX_CHARS, aiMemories } from "./memories";
|
|
71
71
|
import { aiMemoryLearningRuns } from "./memory-learning-runs";
|
|
72
72
|
import { createCloudAiMemoryTool } from "./memory-tool";
|
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
} from "./message-queue";
|
|
81
81
|
import { aiActorUser, aiPrefsUserId, aiUserPrefs } from "./prefs";
|
|
82
82
|
import { aiProjects } from "./projects";
|
|
83
|
+
import type { AiResourceContext } from "./projects-authorization";
|
|
83
84
|
import { projectPublicAiStoredMessages, publicAiStoredMessages } from "./public-projection";
|
|
84
85
|
import { AiRecentResourceSchema, listAiRecentResources } from "./recent-resources";
|
|
85
86
|
import { RECENT_SENT_REF_LIMIT } from "./recent-sent-refs";
|
|
@@ -224,6 +225,8 @@ const MemoryCreateSchema = z.object({
|
|
|
224
225
|
const MemoryUpdateSchema = MemoryCreateSchema.partial().refine((value) => Object.keys(value).length > 0, "Pass a changed field.");
|
|
225
226
|
const MemoryIdSchema = z.string().regex(AI_SHORT_ID_PATTERN);
|
|
226
227
|
|
|
228
|
+
const resourceContext = (c: Context<AuthContext>): AiResourceContext => ({ actor: c.get("actor"), accessSubject: c.get("accessSubject") });
|
|
229
|
+
|
|
227
230
|
const notFound = (c: Context<AuthContext>) => respond(c, fail(err.notFound("Conversation")));
|
|
228
231
|
const fileNotFound = (c: Context<AuthContext>) => respond(c, fail(err.notFound("File")));
|
|
229
232
|
|
|
@@ -232,14 +235,14 @@ const publicConversation = (conversation: AiConversation, projectId: string | nu
|
|
|
232
235
|
id: conversation.shortId,
|
|
233
236
|
projectId: conversation.projectId ? projectId : null,
|
|
234
237
|
});
|
|
235
|
-
const publicConversations = async (conversations: AiConversation[], subject:
|
|
238
|
+
const publicConversations = async (conversations: AiConversation[], subject: AiResourceContext) => {
|
|
236
239
|
const projectIds = conversations.flatMap((conversation) => (conversation.projectId ? [conversation.projectId] : []));
|
|
237
240
|
const shortIds = await aiProjects.resolveShortIds(projectIds, subject);
|
|
238
241
|
return conversations.map((conversation) =>
|
|
239
242
|
publicConversation(conversation, conversation.projectId ? (shortIds.get(conversation.projectId) ?? null) : null),
|
|
240
243
|
);
|
|
241
244
|
};
|
|
242
|
-
const publicConversationFor = async (conversation: AiConversation, subject:
|
|
245
|
+
const publicConversationFor = async (conversation: AiConversation, subject: AiResourceContext) =>
|
|
243
246
|
(await publicConversations([conversation], subject))[0]!;
|
|
244
247
|
const publicMemories = async (userId: string, memories: Awaited<ReturnType<typeof aiMemories.list>>) => {
|
|
245
248
|
const sourceIds = memories.flatMap((memory) => (memory.sourceConversationId ? [memory.sourceConversationId] : []));
|
|
@@ -260,7 +263,7 @@ const rememberLastUsedModel = (actor: RequestActor, model: import("../shared/ai-
|
|
|
260
263
|
void aiUserPrefs.update(userId, { selection: model }).catch(() => undefined);
|
|
261
264
|
};
|
|
262
265
|
|
|
263
|
-
const conversationDetail = async (conversation: AiConversation, subject:
|
|
266
|
+
const conversationDetail = async (conversation: AiConversation, subject: AiResourceContext) => {
|
|
264
267
|
const [state, timeline, publicView] = await Promise.all([
|
|
265
268
|
loadAiStreamState(conversation),
|
|
266
269
|
aiConversations.listConversationTimeline({ conversationId: conversation.id }),
|
|
@@ -297,7 +300,7 @@ const prepareConversationForMessageRetry = async (conversationId: string): Promi
|
|
|
297
300
|
|
|
298
301
|
const prepareLaunchDraft = async (
|
|
299
302
|
body: z.infer<typeof AiCreateConversationInputSchema>,
|
|
300
|
-
subject:
|
|
303
|
+
subject: AiResourceContext | null,
|
|
301
304
|
authority: import("../server").RequestAuthority | null = null,
|
|
302
305
|
) => {
|
|
303
306
|
const content = [...(body.draft?.content ?? [])];
|
|
@@ -336,6 +339,7 @@ export const aiRoutes = (() => {
|
|
|
336
339
|
.use("*", noTransform())
|
|
337
340
|
.use(rateLimit())
|
|
338
341
|
.use("*", auth.requireRole("authenticated"))
|
|
342
|
+
.route("/conversations", aiMapRoutes)
|
|
339
343
|
.get("/status", async (c) => {
|
|
340
344
|
const [status, enabled] = await Promise.all([
|
|
341
345
|
assistantAiSettingsState(c.get("accessSubject")),
|
|
@@ -446,7 +450,7 @@ export const aiRoutes = (() => {
|
|
|
446
450
|
}
|
|
447
451
|
const toolsSupported = Boolean(previewProfile?.capabilities.includes("tools"));
|
|
448
452
|
const availableSkills = toolsSupported
|
|
449
|
-
? (await aiSkills.list(c
|
|
453
|
+
? (await aiSkills.list(resourceContext(c), { authority: appAccessAuthority(c) })).filter((skill) => skill.enabled)
|
|
450
454
|
: [];
|
|
451
455
|
const skillCatalog = selectAiSkillCatalog(
|
|
452
456
|
availableSkills.filter((skill) => !skill.manualOnly).map(({ name, description }) => ({ name, description })),
|
|
@@ -461,8 +465,8 @@ export const aiRoutes = (() => {
|
|
|
461
465
|
})),
|
|
462
466
|
...(client?.localTools === "ask" ? [createCloudAiLocalBashTool(), ...createCloudAiLocalTools()] : []),
|
|
463
467
|
...(memoryEnabled ? [createCloudAiMemoryTool()] : []),
|
|
464
|
-
...(availableSkills.length ? [createCloudAiLoadSkillTool(c
|
|
465
|
-
...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(c
|
|
468
|
+
...(availableSkills.length ? [createCloudAiLoadSkillTool(resourceContext(c), appAccessAuthority(c))] : []),
|
|
469
|
+
...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(resourceContext(c), [], appAccessAuthority(c))] : []),
|
|
466
470
|
]
|
|
467
471
|
: [];
|
|
468
472
|
const memoryToolEnabled = tools.some((tool) => tool.def.name === "memory");
|
|
@@ -545,7 +549,7 @@ export const aiRoutes = (() => {
|
|
|
545
549
|
const ctx = await resolveContext(c);
|
|
546
550
|
if (ctx instanceof Response) return ctx;
|
|
547
551
|
const query = c.req.valid("query");
|
|
548
|
-
const project = query.projectId ? await aiProjects.getByShortId(query.projectId, c
|
|
552
|
+
const project = query.projectId ? await aiProjects.getByShortId(query.projectId, resourceContext(c), "read") : null;
|
|
549
553
|
if (query.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
550
554
|
return respond(
|
|
551
555
|
c,
|
|
@@ -561,7 +565,7 @@ export const aiRoutes = (() => {
|
|
|
561
565
|
unassigned: query.unassigned,
|
|
562
566
|
limit: query.limit,
|
|
563
567
|
}),
|
|
564
|
-
c
|
|
568
|
+
resourceContext(c),
|
|
565
569
|
),
|
|
566
570
|
),
|
|
567
571
|
);
|
|
@@ -570,7 +574,7 @@ export const aiRoutes = (() => {
|
|
|
570
574
|
const ctx = await resolveContext(c);
|
|
571
575
|
if (ctx instanceof Response) return ctx;
|
|
572
576
|
const query = c.req.valid("query");
|
|
573
|
-
const project = query.projectId ? await aiProjects.getByShortId(query.projectId, c
|
|
577
|
+
const project = query.projectId ? await aiProjects.getByShortId(query.projectId, resourceContext(c), "read") : null;
|
|
574
578
|
if (query.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
575
579
|
const page = await aiConversations.listConversationsPage({
|
|
576
580
|
ownerUserId: ctx.ownerUserId,
|
|
@@ -583,15 +587,15 @@ export const aiRoutes = (() => {
|
|
|
583
587
|
page: query.page ?? 1,
|
|
584
588
|
perPage: query.perPage ?? 20,
|
|
585
589
|
});
|
|
586
|
-
return respond(c, ok({ ...page, items: await publicConversations(page.items, c
|
|
590
|
+
return respond(c, ok({ ...page, items: await publicConversations(page.items, resourceContext(c)) }));
|
|
587
591
|
})
|
|
588
592
|
.post("/conversations", v("json", AiCreateConversationInputSchema), async (c) => {
|
|
589
593
|
const ctx = await resolveContext(c);
|
|
590
594
|
if (ctx instanceof Response) return ctx;
|
|
591
595
|
const body = c.req.valid("json");
|
|
592
|
-
const project = body.projectId ? await aiProjects.getByShortId(body.projectId, c
|
|
596
|
+
const project = body.projectId ? await aiProjects.getByShortId(body.projectId, resourceContext(c), "read") : null;
|
|
593
597
|
if (body.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
594
|
-
const draft = await prepareLaunchDraft(body, c
|
|
598
|
+
const draft = await prepareLaunchDraft(body, resourceContext(c), appAccessAuthority(c));
|
|
595
599
|
if (!draft.ok) return respond(c, draft);
|
|
596
600
|
const requestedCapabilityTools = (body.preloadTools ?? []).filter(
|
|
597
601
|
(requested): requested is { appId: string; kind: "query" | "action"; id: string } => "appId" in requested,
|
|
@@ -642,9 +646,7 @@ export const aiRoutes = (() => {
|
|
|
642
646
|
launchedByAppId: body.launchedByAppId,
|
|
643
647
|
});
|
|
644
648
|
// 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;
|
|
649
|
+
const currentProject = conversation.projectId ? await aiProjects.get(conversation.projectId, resourceContext(c), "read") : null;
|
|
648
650
|
if (conversation.projectId && !currentProject) return respond(c, fail(err.notFound("Project")));
|
|
649
651
|
return respond(c, ok(publicConversation(conversation, currentProject?.shortId ?? null)), 201);
|
|
650
652
|
} catch (error) {
|
|
@@ -659,7 +661,7 @@ export const aiRoutes = (() => {
|
|
|
659
661
|
if (ctx instanceof Response) return ctx;
|
|
660
662
|
const conversation = await loadConversation(c, ctx);
|
|
661
663
|
if (!conversation) return notFound(c);
|
|
662
|
-
return respond(c, ok(await conversationDetail(conversation, c
|
|
664
|
+
return respond(c, ok(await conversationDetail(conversation, resourceContext(c))));
|
|
663
665
|
})
|
|
664
666
|
.get(
|
|
665
667
|
"/conversations/:conversationId/submission/:draftRevision",
|
|
@@ -669,7 +671,7 @@ export const aiRoutes = (() => {
|
|
|
669
671
|
if (ctx instanceof Response) return ctx;
|
|
670
672
|
const conversation = await loadConversation(c, ctx);
|
|
671
673
|
if (!conversation) return notFound(c);
|
|
672
|
-
if (conversation.projectId && !(await aiProjects.get(conversation.projectId, c
|
|
674
|
+
if (conversation.projectId && !(await aiProjects.get(conversation.projectId, resourceContext(c), "read"))) return notFound(c);
|
|
673
675
|
const submission = await aiConversations.getDraftSubmission({
|
|
674
676
|
conversationId: conversation.id,
|
|
675
677
|
ownerUserId: ctx.ownerUserId,
|
|
@@ -797,7 +799,7 @@ export const aiRoutes = (() => {
|
|
|
797
799
|
pinned: body.pinned,
|
|
798
800
|
});
|
|
799
801
|
if (!updated) return notFound(c);
|
|
800
|
-
return respond(c, ok(await publicConversationFor(updated, c
|
|
802
|
+
return respond(c, ok(await publicConversationFor(updated, resourceContext(c))));
|
|
801
803
|
})
|
|
802
804
|
.put("/conversations/:conversationId/project", v("json", ConversationProjectInputSchema), async (c) => {
|
|
803
805
|
const ctx = await resolveContext(c);
|
|
@@ -805,7 +807,7 @@ export const aiRoutes = (() => {
|
|
|
805
807
|
const conversation = await loadConversation(c, ctx);
|
|
806
808
|
if (!conversation) return notFound(c);
|
|
807
809
|
const body = c.req.valid("json");
|
|
808
|
-
const project = body.projectId ? await aiProjects.getByShortId(body.projectId, c
|
|
810
|
+
const project = body.projectId ? await aiProjects.getByShortId(body.projectId, resourceContext(c), "read") : null;
|
|
809
811
|
if (body.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
810
812
|
const result = await aiConversations.setConversationProject({
|
|
811
813
|
conversationId: conversation.id,
|
|
@@ -820,7 +822,7 @@ export const aiRoutes = (() => {
|
|
|
820
822
|
}
|
|
821
823
|
return notFound(c);
|
|
822
824
|
}
|
|
823
|
-
return respond(c, ok(await publicConversationFor(result.conversation, c
|
|
825
|
+
return respond(c, ok(await publicConversationFor(result.conversation, resourceContext(c))));
|
|
824
826
|
})
|
|
825
827
|
.post("/conversations/:conversationId/pin", async (c) => {
|
|
826
828
|
const ctx = await resolveContext(c);
|
|
@@ -832,7 +834,7 @@ export const aiRoutes = (() => {
|
|
|
832
834
|
ownerUserId: ctx.ownerUserId,
|
|
833
835
|
pinned: true,
|
|
834
836
|
});
|
|
835
|
-
return updated ? respond(c, ok(await publicConversationFor(updated, c
|
|
837
|
+
return updated ? respond(c, ok(await publicConversationFor(updated, resourceContext(c)))) : notFound(c);
|
|
836
838
|
})
|
|
837
839
|
.delete("/conversations/:conversationId/pin", async (c) => {
|
|
838
840
|
const ctx = await resolveContext(c);
|
|
@@ -844,7 +846,7 @@ export const aiRoutes = (() => {
|
|
|
844
846
|
ownerUserId: ctx.ownerUserId,
|
|
845
847
|
pinned: false,
|
|
846
848
|
});
|
|
847
|
-
return updated ? respond(c, ok(await publicConversationFor(updated, c
|
|
849
|
+
return updated ? respond(c, ok(await publicConversationFor(updated, resourceContext(c)))) : notFound(c);
|
|
848
850
|
})
|
|
849
851
|
.put("/conversations/:conversationId/done", v("json", z.object({ done: z.boolean().nullable() })), async (c) => {
|
|
850
852
|
const ctx = await resolveContext(c);
|
|
@@ -860,7 +862,7 @@ export const aiRoutes = (() => {
|
|
|
860
862
|
return result.reason === "active_turn"
|
|
861
863
|
? respond(c, fail(err.conflict("Stop the current response before marking this chat done.")))
|
|
862
864
|
: notFound(c);
|
|
863
|
-
return respond(c, ok(await publicConversationFor(result.conversation, c
|
|
865
|
+
return respond(c, ok(await publicConversationFor(result.conversation, resourceContext(c))));
|
|
864
866
|
})
|
|
865
867
|
.post("/conversations/:conversationId/archive", async (c) => {
|
|
866
868
|
const ctx = await resolveContext(c);
|
|
@@ -910,7 +912,7 @@ export const aiRoutes = (() => {
|
|
|
910
912
|
error: error instanceof Error ? error.message : String(error),
|
|
911
913
|
}),
|
|
912
914
|
);
|
|
913
|
-
return restored ? respond(c, ok(await publicConversationFor(restored, c
|
|
915
|
+
return restored ? respond(c, ok(await publicConversationFor(restored, resourceContext(c)))) : notFound(c);
|
|
914
916
|
})
|
|
915
917
|
.post("/conversations/:conversationId/viewed", async (c) => {
|
|
916
918
|
const ctx = await resolveContext(c);
|
|
@@ -1007,7 +1009,7 @@ export const aiRoutes = (() => {
|
|
|
1007
1009
|
};
|
|
1008
1010
|
});
|
|
1009
1011
|
const { input, message } = aiInputToUserMessage(aiTurnInputToContent({ content: turnContent }));
|
|
1010
|
-
const project = conversation.projectId ? await aiProjects.snapshot(conversation.projectId, c
|
|
1012
|
+
const project = conversation.projectId ? await aiProjects.snapshot(conversation.projectId, resourceContext(c)) : null;
|
|
1011
1013
|
if (conversation.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
1012
1014
|
try {
|
|
1013
1015
|
const submission = {
|
|
@@ -1110,7 +1112,7 @@ export const aiRoutes = (() => {
|
|
|
1110
1112
|
? await aiConversations.getTurnRunConfig({ conversationId: conversation.id, turnId: target.loopId })
|
|
1111
1113
|
: null;
|
|
1112
1114
|
const originalProject = originalRunConfig?.kind !== "compact" ? originalRunConfig?.project : undefined;
|
|
1113
|
-
const currentProject = originalProject?.id ? await aiProjects.getByShortId(originalProject.id, c
|
|
1115
|
+
const currentProject = originalProject?.id ? await aiProjects.getByShortId(originalProject.id, resourceContext(c)) : null;
|
|
1114
1116
|
if (originalProject && !currentProject) return respond(c, fail(err.notFound("Project")));
|
|
1115
1117
|
|
|
1116
1118
|
try {
|
|
@@ -1189,7 +1191,7 @@ export const aiRoutes = (() => {
|
|
|
1189
1191
|
}
|
|
1190
1192
|
|
|
1191
1193
|
const body = c.req.valid("json");
|
|
1192
|
-
if (conversation.projectId && !(await aiProjects.get(conversation.projectId, c
|
|
1194
|
+
if (conversation.projectId && !(await aiProjects.get(conversation.projectId, resourceContext(c), "read"))) {
|
|
1193
1195
|
return respond(c, fail(err.notFound("Project")));
|
|
1194
1196
|
}
|
|
1195
1197
|
const forked = await aiConversations.forkConversation({
|
|
@@ -1198,7 +1200,7 @@ export const aiRoutes = (() => {
|
|
|
1198
1200
|
ownerUserId: ctx.ownerUserId,
|
|
1199
1201
|
title: body.title ?? conversation.title,
|
|
1200
1202
|
});
|
|
1201
|
-
return respond(c, ok(await conversationDetail(forked, c
|
|
1203
|
+
return respond(c, ok(await conversationDetail(forked, resourceContext(c))));
|
|
1202
1204
|
})
|
|
1203
1205
|
.get("/conversations/:conversationId/enrichment", async (c) => {
|
|
1204
1206
|
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) {
|