@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.
Files changed (137) hide show
  1. package/package.json +9 -2
  2. package/src/_internal/app-queries.ts +73 -0
  3. package/src/_internal/capabilities.ts +137 -9
  4. package/src/_internal/define-app.ts +2 -0
  5. package/src/_internal/registry-validation.ts +17 -4
  6. package/src/_internal/registry.ts +32 -4
  7. package/src/_internal/resource-principals.ts +36 -0
  8. package/src/access/GroupCoverage.tsx +20 -5
  9. package/src/access/PermissionEditor.tsx +11 -2
  10. package/src/access/PrincipalAvatar.tsx +30 -21
  11. package/src/access/messages.ts +4 -4
  12. package/src/account/EntitySearch.tsx +12 -0
  13. package/src/ai/capabilities.ts +4 -2
  14. package/src/ai/chat/map-messages.ts +270 -0
  15. package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
  16. package/src/ai/chat/map-notebook.tsx +301 -0
  17. package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
  18. package/src/ai/chat/map-widget.css +107 -0
  19. package/src/ai/chat/map-widget.tsx +1279 -0
  20. package/src/ai/chat/message-utils.ts +1 -0
  21. package/src/ai/chat/tool-names.ts +2 -0
  22. package/src/ai/chat/turn-layout.ts +2 -1
  23. package/src/ai/chat/turn-view.tsx +17 -1
  24. package/src/ai/default-tools.ts +3 -0
  25. package/src/ai/dictation-runtime.ts +6 -2
  26. package/src/ai/executor.ts +22 -4
  27. package/src/ai/index.ts +1 -0
  28. package/src/ai/map-contracts.ts +209 -0
  29. package/src/ai/map-execution.ts +14 -0
  30. package/src/ai/map-notebook-save.ts +276 -0
  31. package/src/ai/map-notebook.ts +32 -0
  32. package/src/ai/map-opening-hours.ts +40 -0
  33. package/src/ai/map-routes.ts +84 -0
  34. package/src/ai/map-service.ts +288 -0
  35. package/src/ai/map-store.ts +137 -0
  36. package/src/ai/map-tool.ts +73 -0
  37. package/src/ai/migrate.ts +3 -0
  38. package/src/ai/project-tool.ts +4 -4
  39. package/src/ai/projects-authorization.ts +53 -0
  40. package/src/ai/projects-routes.ts +56 -46
  41. package/src/ai/projects.ts +100 -79
  42. package/src/ai/protocol.ts +3 -1
  43. package/src/ai/recent-resources.ts +3 -3
  44. package/src/ai/routes.ts +32 -30
  45. package/src/ai/skill-tool.ts +4 -4
  46. package/src/ai/skills-routes.ts +27 -26
  47. package/src/ai/skills.ts +120 -81
  48. package/src/ai/store.ts +19 -6
  49. package/src/ai/tools.ts +14 -0
  50. package/src/ai/types.ts +3 -0
  51. package/src/api/accounts-entities.ts +6 -3
  52. package/src/api/admin-ai-skills.ts +4 -1
  53. package/src/api/auth.ts +9 -2
  54. package/src/api/capabilities.ts +90 -12
  55. package/src/api/mcp.ts +4 -2
  56. package/src/browser/notifications.ts +25 -8
  57. package/src/browser/session.ts +39 -0
  58. package/src/browser/shared-text-history-validation.ts +151 -0
  59. package/src/browser/shared-text-history.ts +464 -0
  60. package/src/browser/shared-text-offline-authorized.ts +29 -0
  61. package/src/browser/shared-text-offline.ts +351 -0
  62. package/src/browser/shared-text-provider.ts +714 -0
  63. package/src/browser/shared-text-table-contract.ts +150 -0
  64. package/src/browser/shared-text.ts +19 -0
  65. package/src/capabilities/discussion-providers.ts +99 -0
  66. package/src/capabilities/executions.ts +2 -2
  67. package/src/capabilities/migrate.ts +10 -1
  68. package/src/capabilities/resource-readers.ts +58 -0
  69. package/src/capabilities/server-invoke.ts +23 -13
  70. package/src/capabilities/server.ts +17 -6
  71. package/src/capabilities/testing.ts +13 -0
  72. package/src/contracts/capabilities.ts +137 -4
  73. package/src/contracts/discussions.ts +91 -0
  74. package/src/contracts/geo-document.ts +60 -0
  75. package/src/contracts/geo-snapshot.ts +86 -0
  76. package/src/contracts/geo.ts +14 -0
  77. package/src/contracts/index.ts +2 -0
  78. package/src/contracts/outgoing-mail.ts +1 -1
  79. package/src/contracts/resource-types.ts +21 -0
  80. package/src/contracts/shared.ts +2 -1
  81. package/src/geo/AddressAutocomplete.tsx +213 -0
  82. package/src/geo/messages.ts +29 -0
  83. package/src/geo/navigation.ts +25 -0
  84. package/src/geo/ui.ts +2 -0
  85. package/src/server/api/respond.ts +10 -2
  86. package/src/server/index.ts +1 -0
  87. package/src/server/services/access.ts +23 -10
  88. package/src/server/services/index.ts +1 -0
  89. package/src/services/accounts/app.ts +56 -17
  90. package/src/services/accounts/entities.ts +36 -16
  91. package/src/services/accounts/group-sql.ts +23 -0
  92. package/src/services/accounts/groups.ts +26 -17
  93. package/src/services/accounts/identities.ts +8 -1
  94. package/src/services/accounts/identity-reconciliation.ts +3 -1
  95. package/src/services/accounts/local-groups.ts +141 -27
  96. package/src/services/accounts/machine-group-management.ts +94 -0
  97. package/src/services/accounts/users.ts +38 -8
  98. package/src/services/geo/cache.ts +2 -1
  99. package/src/services/geo/geoapify.ts +3 -0
  100. package/src/services/identity/invocation-token.ts +110 -0
  101. package/src/services/mandates/index.ts +2 -2
  102. package/src/services/mandates/policy.ts +1 -0
  103. package/src/services/pdf/geo-snapshot.ts +107 -0
  104. package/src/services/pdf/index.ts +2 -0
  105. package/src/services/session/index.ts +12 -3
  106. package/src/shared/email-html.ts +1 -0
  107. package/src/shared/markdown/extensions/katex.ts +8 -1
  108. package/src/shared/markdown/extensions/tables.ts +14 -1
  109. package/src/shared/markdown/index.ts +22 -11
  110. package/src/shared-text/anchors.ts +29 -0
  111. package/src/shared-text/awareness.ts +87 -0
  112. package/src/shared-text/changes.ts +27 -0
  113. package/src/shared-text/documents.ts +428 -77
  114. package/src/shared-text/events.ts +12 -2
  115. package/src/shared-text/index.ts +16 -1
  116. package/src/shared-text/log.ts +24 -5
  117. package/src/shared-text/snapshots.ts +69 -23
  118. package/src/shared-text/structural-history.ts +377 -0
  119. package/src/shared-text/table-actions.ts +432 -0
  120. package/src/shared-text/types.ts +24 -5
  121. package/src/ssr/AdminArea.tsx +2 -2
  122. package/src/ssr/AdminLayout.tsx +1 -1
  123. package/src/ssr/AdminSidebar.tsx +1 -1
  124. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  125. package/src/ssr/CloudShell.island.tsx +19 -3
  126. package/src/ssr/Layout.tsx +1 -1
  127. package/src/ssr/MobileProfileActions.tsx +2 -2
  128. package/src/ssr/ProfilePreferences.tsx +2 -1
  129. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  130. package/src/ssr/admin-navigation.ts +1 -11
  131. package/src/ssr/app-launchpad.tsx +6 -1
  132. package/src/ssr/platform-messages.ts +2 -0
  133. package/src/ssr/profile-actions.ts +3 -6
  134. package/src/ssr/shell-data.tsx +1 -1
  135. package/src/styles/global.css +2 -0
  136. package/src/workflows/language/expressions.ts +3 -2
  137. 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: AccessSubject) => {
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: AccessSubject) =>
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: AccessSubject) => {
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: AccessSubject | null,
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.get("accessSubject"), { authority: appAccessAuthority(c) })).filter((skill) => skill.enabled)
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.get("accessSubject"), appAccessAuthority(c))] : []),
465
- ...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(c.get("accessSubject"), [], appAccessAuthority(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.get("accessSubject"), "read") : null;
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.get("accessSubject"),
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.get("accessSubject"), "read") : null;
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.get("accessSubject")) }));
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.get("accessSubject"), "read") : null;
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.get("accessSubject"), appAccessAuthority(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.get("accessSubject"))));
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.get("accessSubject"), "read"))) return notFound(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.get("accessSubject"))));
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.get("accessSubject"), "read") : null;
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.get("accessSubject"))));
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.get("accessSubject")))) : notFound(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.get("accessSubject")))) : notFound(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.get("accessSubject"))));
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.get("accessSubject")))) : notFound(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.get("accessSubject")) : null;
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.get("accessSubject")) : null;
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.get("accessSubject"), "read"))) {
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.get("accessSubject"))));
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);
@@ -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: AccessSubject | null,
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: AccessSubject, authority: RequestAuthority | null = null) =>
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: AccessSubject | null,
95
+ subject: AiResourceContext | null,
96
96
  localSkills: readonly AiSkillCatalogEntry[] = [],
97
97
  authority: RequestAuthority | null = null,
98
98
  ) =>
@@ -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.get("accessSubject") ?? null;
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.get("accessSubject") ?? null, "read", appAccessAuthority(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.get("accessSubject") ?? null,
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.get("accessSubject") ?? null;
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.get("accessSubject") ?? null;
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.get("accessSubject");
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.get("accessSubject") ?? null, "write", appAccessAuthority(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.get("accessSubject") ?? null, c.req.valid("json"), appAccessAuthority(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.get("accessSubject") ?? null, "admin", appAccessAuthority(c));
196
- return skill && (await aiSkills.delete(skill.id, c.get("accessSubject") ?? null, appAccessAuthority(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.get("accessSubject") ?? null;
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.get("accessSubject") ?? null, "admin", appAccessAuthority(c));
208
- const access = skill ? await aiSkills.listAccess(skill.id, c.get("accessSubject") ?? null, appAccessAuthority(c)) : null;
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.get("accessSubject") ?? null, "admin", appAccessAuthority(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.get("accessSubject") ?? null, c.req.valid("json"), undefined, appAccessAuthority(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.get("accessSubject") ?? null, "admin", appAccessAuthority(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.get("accessSubject") ?? null,
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.get("accessSubject") ?? null, "admin", appAccessAuthority(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) {