@k2b/cloud 0.41.0 → 0.42.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 (227) hide show
  1. package/package.json +6 -5
  2. package/src/_internal/bounded-json.ts +14 -2
  3. package/src/_internal/capabilities.ts +87 -8
  4. package/src/_internal/capability-reviews.ts +6 -0
  5. package/src/_internal/capability-timeout.ts +71 -0
  6. package/src/_internal/define-app.ts +145 -89
  7. package/src/_internal/http-drain.ts +56 -0
  8. package/src/_internal/registry-validation.ts +3 -0
  9. package/src/_internal/runtime-context.ts +1 -0
  10. package/src/_internal/status-preserving-ssr.ts +2 -0
  11. package/src/access/PermissionEditor.tsx +16 -17
  12. package/src/access/PrincipalPicker.tsx +45 -10
  13. package/src/access/messages.ts +8 -0
  14. package/src/access/principal-label.ts +18 -0
  15. package/src/access/ui.ts +1 -0
  16. package/src/account/EntitySearch.tsx +5 -4
  17. package/src/ai/action-review.ts +2 -1
  18. package/src/ai/app-skill-store.ts +1 -1
  19. package/src/ai/capabilities.ts +21 -3
  20. package/src/ai/capability-execution.ts +14 -12
  21. package/src/ai/chat/action-card.ts +7 -1
  22. package/src/ai/chat/action-messages.ts +14 -0
  23. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  24. package/src/ai/chat/action-question.tsx +202 -22
  25. package/src/ai/chat/action-text.ts +1 -1
  26. package/src/ai/chat-tasks.ts +2 -1
  27. package/src/ai/client/controller.ts +150 -32
  28. package/src/ai/client/transport.ts +14 -2
  29. package/src/ai/code-capability-routes.ts +27 -23
  30. package/src/ai/code-runtime-tools.ts +14 -16
  31. package/src/ai/code-source-tools.ts +16 -18
  32. package/src/ai/executor.ts +56 -21
  33. package/src/ai/fetch-file-tool.ts +67 -76
  34. package/src/ai/http.ts +1 -0
  35. package/src/ai/migrate.ts +13 -0
  36. package/src/ai/open-questions.ts +4 -8
  37. package/src/ai/projects.ts +1 -2
  38. package/src/ai/protocol.ts +2 -2
  39. package/src/ai/recent-resources.ts +12 -5
  40. package/src/ai/review-content.ts +77 -0
  41. package/src/ai/routes.ts +69 -31
  42. package/src/ai/runtime.ts +1 -0
  43. package/src/ai/skill-tool.ts +17 -6
  44. package/src/ai/skills-routes.ts +72 -27
  45. package/src/ai/skills.ts +139 -60
  46. package/src/ai/store.ts +168 -33
  47. package/src/ai/types.ts +10 -3
  48. package/src/ai/usage.ts +2 -2
  49. package/src/api/admin-core-settings.ts +15 -2
  50. package/src/api/admin-geo.ts +54 -0
  51. package/src/api/admin-linux-identities.ts +6 -3
  52. package/src/api/admin-outgoing-mail.ts +29 -15
  53. package/src/api/approvals-loader.ts +3 -3
  54. package/src/api/approvals.ts +51 -0
  55. package/src/api/apps.ts +4 -2
  56. package/src/api/capabilities.ts +258 -181
  57. package/src/api/capability-streams.ts +21 -13
  58. package/src/api/cli-plugins.ts +4 -1
  59. package/src/api/geo-glyphs.ts +11 -0
  60. package/src/api/geo.ts +128 -0
  61. package/src/api/help.ts +12 -4
  62. package/src/api/index.ts +5 -0
  63. package/src/api/mcp.ts +46 -38
  64. package/src/api/me-panel-consent.ts +8 -5
  65. package/src/api/search.ts +21 -14
  66. package/src/api/sync-ops.ts +19 -13
  67. package/src/api/widgets.ts +42 -18
  68. package/src/approvals/Approvals.tsx +5 -3
  69. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  70. package/src/browser/CloudResourceSearch.tsx +3 -13
  71. package/src/browser/PanelLeaveNotice.tsx +18 -0
  72. package/src/browser/navigation-search.ts +78 -25
  73. package/src/browser/panel-leave.ts +69 -0
  74. package/src/browser/panel-messages.ts +12 -0
  75. package/src/browser/panel-slot.ts +173 -24
  76. package/src/browser/panel.ts +3 -0
  77. package/src/browser/resource-search-messages.ts +2 -0
  78. package/src/capabilities/client.ts +28 -1
  79. package/src/capabilities/executions.ts +22 -3
  80. package/src/capabilities/geo-client.ts +57 -0
  81. package/src/capabilities/response.ts +2 -1
  82. package/src/capabilities/review-storage.ts +24 -0
  83. package/src/capabilities/reviews.ts +17 -3
  84. package/src/capabilities/server.ts +36 -16
  85. package/src/cli/access.ts +15 -23
  86. package/src/cli/admin/linux.ts +13 -2
  87. package/src/cli/admin/outgoing-mail.ts +26 -4
  88. package/src/cli/capabilities.ts +19 -0
  89. package/src/config/env.ts +0 -6
  90. package/src/contracts/app-access.ts +48 -0
  91. package/src/contracts/app.ts +4 -1
  92. package/src/contracts/approvals.ts +2 -2
  93. package/src/contracts/avatar.ts +186 -0
  94. package/src/contracts/capabilities.ts +148 -54
  95. package/src/contracts/geo-admin.ts +127 -0
  96. package/src/contracts/geo-map.ts +19 -0
  97. package/src/contracts/geo-route.ts +168 -0
  98. package/src/contracts/geo.ts +248 -0
  99. package/src/contracts/index.ts +4 -0
  100. package/src/contracts/outgoing-mail.ts +17 -4
  101. package/src/contracts/registry.ts +2 -0
  102. package/src/events/app-access.ts +68 -0
  103. package/src/events/live-delivery.ts +128 -0
  104. package/src/events/live.ts +14 -130
  105. package/src/geo/CloudMap.tsx +95 -0
  106. package/src/geo/map-messages.ts +17 -0
  107. package/src/geo/ui.ts +1 -0
  108. package/src/index.ts +2 -0
  109. package/src/server/client-ip.ts +36 -0
  110. package/src/server/index.ts +3 -4
  111. package/src/server/middleware/app-access.ts +312 -0
  112. package/src/server/middleware/auth.ts +1 -1
  113. package/src/server/middleware/invocation.ts +11 -0
  114. package/src/server/middleware/rate-limit.ts +4 -4
  115. package/src/server/services/access.ts +13 -9
  116. package/src/server/services/index.ts +0 -2
  117. package/src/server/services/services.ts +0 -2
  118. package/src/services/accounts/posix.ts +21 -7
  119. package/src/services/allowed-network-fetch.ts +16 -10
  120. package/src/services/app-access-metrics.ts +24 -0
  121. package/src/services/app-access.ts +123 -0
  122. package/src/services/app-discovery.ts +27 -0
  123. package/src/services/approvals/content.ts +40 -0
  124. package/src/services/approvals/index.ts +14 -6
  125. package/src/services/approvals/list.ts +6 -11
  126. package/src/services/approvals/question.ts +11 -7
  127. package/src/services/downstream-budget.ts +87 -0
  128. package/src/services/geo/admin.ts +262 -0
  129. package/src/services/geo/bucket.ts +67 -0
  130. package/src/services/geo/cache.ts +57 -0
  131. package/src/services/geo/costs.ts +5 -0
  132. package/src/services/geo/errors.ts +41 -0
  133. package/src/services/geo/fixtures/README.md +17 -0
  134. package/src/services/geo/fixtures/along.json +355 -0
  135. package/src/services/geo/fixtures/autocomplete.json +218 -0
  136. package/src/services/geo/fixtures/geocode.json +129 -0
  137. package/src/services/geo/fixtures/places.json +355 -0
  138. package/src/services/geo/fixtures/reverse.json +61 -0
  139. package/src/services/geo/fixtures/routing.json +57 -0
  140. package/src/services/geo/geoapify-route.ts +93 -0
  141. package/src/services/geo/geoapify.ts +281 -0
  142. package/src/services/geo/ledger.ts +122 -0
  143. package/src/services/geo/map.ts +160 -0
  144. package/src/services/geo/migrate.ts +25 -0
  145. package/src/services/geo/polyline.ts +184 -0
  146. package/src/services/geo/route-chain.ts +145 -0
  147. package/src/services/geo/route-costs.ts +9 -0
  148. package/src/services/geo/route-geometry.ts +39 -0
  149. package/src/services/geo/route-plan.ts +172 -0
  150. package/src/services/geo/route-service.ts +433 -0
  151. package/src/services/geo/service.ts +300 -0
  152. package/src/services/geo/settings.ts +49 -0
  153. package/src/services/help/index.ts +16 -7
  154. package/src/services/help/types.ts +6 -1
  155. package/src/services/identity/invocation-actor.ts +20 -2
  156. package/src/services/identity/issue-invocation.ts +67 -0
  157. package/src/services/identity/key-ring.ts +5 -3
  158. package/src/services/identity/runtime-config.ts +23 -0
  159. package/src/services/index.ts +9 -0
  160. package/src/services/mandates/index.ts +40 -3
  161. package/src/services/network-security.ts +6 -3
  162. package/src/services/network-transport.ts +99 -0
  163. package/src/services/notifications/catalog.ts +3 -2
  164. package/src/services/notifications/runtime.ts +1 -0
  165. package/src/services/notifications/web-push-transport.ts +45 -38
  166. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  167. package/src/services/outgoing-mail/index.ts +55 -1
  168. package/src/services/outgoing-mail/messages.ts +82 -3
  169. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  170. package/src/services/outgoing-mail/retention.ts +13 -1
  171. package/src/services/outgoing-mail/store.ts +36 -6
  172. package/src/services/outgoing-mail/test-send.ts +34 -4
  173. package/src/services/outgoing-mail/transport.ts +1 -1
  174. package/src/services/pdf/gotenberg.ts +11 -38
  175. package/src/services/pdf/template-preview.ts +1 -1
  176. package/src/services/public-http.ts +28 -40
  177. package/src/services/settings/app.ts +4 -1
  178. package/src/services/settings/core-settings.ts +88 -0
  179. package/src/services/settings/defaults.ts +8 -2
  180. package/src/services/settings/errors.ts +11 -0
  181. package/src/services/settings/geo-budget.ts +40 -0
  182. package/src/services/settings/index.ts +4 -3
  183. package/src/services/settings/store.ts +25 -17
  184. package/src/services/weather/geo.ts +29 -111
  185. package/src/services/weather/index.ts +4 -3
  186. package/src/services/weather/locations.ts +8 -5
  187. package/src/services/weather/migrate.ts +1 -0
  188. package/src/services/weather/settings.ts +0 -16
  189. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  190. package/src/shared/avatar-art.ts +400 -0
  191. package/src/shared/index.ts +2 -0
  192. package/src/shared/network-address.ts +22 -3
  193. package/src/shared/template-rendering.ts +36 -4
  194. package/src/shared-text/documents.ts +1 -1
  195. package/src/shared-text/index.ts +2 -0
  196. package/src/shared-text/log.ts +51 -3
  197. package/src/shared-text/snapshots.ts +340 -0
  198. package/src/ssr/AdminLayout.tsx +4 -2
  199. package/src/ssr/CloudShell.island.tsx +6 -0
  200. package/src/ssr/Layout.tsx +65 -15
  201. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  202. package/src/ssr/LayoutHelp.tsx +3 -2
  203. package/src/ssr/PanelRuntime.island.tsx +11 -7
  204. package/src/ssr/PwaLayout.tsx +2 -2
  205. package/src/ssr/RailApps.tsx +6 -0
  206. package/src/ssr/ShellRuntime.island.tsx +23 -7
  207. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  208. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
  209. package/src/ssr/admin-navigation.ts +1 -0
  210. package/src/ssr/app-badges.ts +6 -1
  211. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  212. package/src/ssr/help.ts +3 -2
  213. package/src/ssr/index.ts +1 -1
  214. package/src/ssr/layout-context.ts +2 -0
  215. package/src/ssr/mobile-menu-history.ts +4 -0
  216. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  217. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  218. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  219. package/src/ssr/page-swap.ts +28 -0
  220. package/src/ssr/page-trail-runtime.ts +32 -13
  221. package/src/ssr/panel-consent.ts +2 -2
  222. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  223. package/src/ssr/platform-messages.ts +8 -0
  224. package/src/ssr/runtime.ts +17 -0
  225. package/src/ssr/shell-data.tsx +10 -2
  226. package/src/styles/global.css +70 -6
  227. package/src/server/services/geo.ts +0 -215
@@ -0,0 +1,77 @@
1
+ import { type SQL, sql } from "bun";
2
+ import { capabilityRequestHash } from "../capabilities/claims";
3
+ import { parseStoredReviewContent } from "../capabilities/review-storage";
4
+ import { CAPABILITY_MAX_REVIEW_CONTENT_BYTES, type CapabilityActionReviewPresentation } from "../contracts/capabilities";
5
+ import type { AiStoredActionReview } from "./action-review";
6
+
7
+ export type ReviewContentLocation = { conversationId: string; turnId: string; callId: string };
8
+
9
+ export const reviewContentHref = ({ conversationId, turnId, callId }: ReviewContentLocation): string =>
10
+ `/api/approvals/conversations/${encodeURIComponent(conversationId)}/turns/${encodeURIComponent(turnId)}/reviews/${encodeURIComponent(callId)}`;
11
+
12
+ export const compactCapabilityReview = (
13
+ review: CapabilityActionReviewPresentation,
14
+ location: ReviewContentLocation,
15
+ ): CapabilityActionReviewPresentation => {
16
+ if (!("sentence" in review)) return review;
17
+ const { content, ...compact } = review;
18
+ const digest = content ? capabilityRequestHash(content) : compact.contentRef?.digest;
19
+ return digest ? { ...compact, contentRef: { href: reviewContentHref(location), digest } } : compact;
20
+ };
21
+
22
+ /** The existing message snapshot owns both the content and its integrity reference. */
23
+ export const storedReviewWithContent = (review: AiStoredActionReview, location: ReviewContentLocation): AiStoredActionReview =>
24
+ "content" in review && review.content
25
+ ? { ...review, contentRef: { href: reviewContentHref(location), digest: capabilityRequestHash(review.content) } }
26
+ : review;
27
+
28
+ /** Rebind the locator on a fork; never make the new conversation depend on access to its source. */
29
+ export const compactStoredReview = (review: AiStoredActionReview, location?: ReviewContentLocation): AiStoredActionReview => {
30
+ if ("legacy" in review) return review;
31
+ const { content: _content, ...compact } = review;
32
+ return compact.contentRef && location
33
+ ? { ...compact, contentRef: { ...compact.contentRef, href: reviewContentHref(location) } }
34
+ : compact;
35
+ };
36
+
37
+ /** Project before transferring JSONB out of Postgres, not after loading every full review into Core. */
38
+ export const compactReviewMapSql = (reviews: SQL.Query<unknown>) => sql`(
39
+ SELECT COALESCE(jsonb_object_agg(entry.key, entry.value - 'content'), '{}'::jsonb)
40
+ FROM jsonb_each(CASE WHEN jsonb_typeof(${reviews}) = 'object' THEN ${reviews} ELSE '{}'::jsonb END) entry
41
+ )`;
42
+
43
+ export const compactReviewMetaSql = (meta: SQL.Query<unknown>) => sql`CASE
44
+ WHEN ${meta} ? 'toolReviews' THEN jsonb_set(${meta}, '{toolReviews}', ${compactReviewMapSql(sql`${meta}->'toolReviews'`)})
45
+ ELSE ${meta} END`;
46
+
47
+ /** Caller has already authorized this conversation. Read one exact persisted call from either existing message store. */
48
+ export const readReviewContent = async (location: ReviewContentLocation, digest: string, ownerUserId: string, signal?: AbortSignal) => {
49
+ signal?.throwIfAborted();
50
+ const query = sql<{ content: unknown }[]>`
51
+ SELECT meta->'toolReviews'->${location.callId}->'content' AS content
52
+ FROM (
53
+ SELECT meta, conversation_id, loop_id, role, seq FROM ai.messages
54
+ UNION ALL
55
+ SELECT meta, conversation_id, loop_id, role, seq FROM ai.task_messages
56
+ ) messages
57
+ WHERE conversation_id=${location.conversationId}::uuid AND loop_id=${location.turnId} AND role='assistant'
58
+ AND EXISTS (SELECT 1 FROM ai.conversations c WHERE c.id=messages.conversation_id
59
+ AND c.created_by_user_id=${ownerUserId}::uuid AND c.archived_at IS NULL)
60
+ AND meta->'toolReviews'->${location.callId}->'contentRef'->>'digest'=${digest}
61
+ AND octet_length((meta->'toolReviews'->${location.callId}->'content')::text) <= ${2 * CAPABILITY_MAX_REVIEW_CONTENT_BYTES + 2}
62
+ ORDER BY seq DESC LIMIT 1`;
63
+ // JSONB adds spaces after punctuation; at most twice the compact JSON budget. The exact limit is checked below.
64
+ const abort = () => query.cancel();
65
+ signal?.addEventListener("abort", abort, { once: true });
66
+ if (signal?.aborted) abort();
67
+ let rows: { content: unknown }[];
68
+ try {
69
+ rows = await query;
70
+ } finally {
71
+ signal?.removeEventListener("abort", abort);
72
+ }
73
+ signal?.throwIfAborted();
74
+ const row = rows[0];
75
+ const parsed = parseStoredReviewContent(row?.content);
76
+ return parsed.success && capabilityRequestHash(parsed.data) === digest ? parsed.data : null;
77
+ };
package/src/ai/routes.ts CHANGED
@@ -2,8 +2,8 @@ import { type Context, Hono } from "hono";
2
2
  import { bodyLimit } from "hono/body-limit";
3
3
  import { describeRoute } from "hono-openapi";
4
4
  import { z } from "zod";
5
+ import { CAPABILITY_DISCOVERY_TIMEOUT_MS, capabilityTimeoutMs } from "../_internal/capability-timeout";
5
6
  import { getApp, listCapabilities } from "../_internal/registry";
6
- import { QUERY_TIMEOUT_MS } from "../api/capabilities";
7
7
  import { ErrorResponseSchema } from "../contracts";
8
8
  import { APPROVAL_CLIENT_HEADER } from "../contracts/approvals";
9
9
  import { capabilityActionPolicy } from "../contracts/capabilities";
@@ -27,6 +27,8 @@ import { isAppSessionActor } from "../server/actor";
27
27
  import { isRequestCredentialCurrent } from "../server/middleware/auth";
28
28
  import { noTransform } from "../server/middleware/no-transform";
29
29
  import { REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "../server/request-timeout";
30
+ import { appAccessAuthority } from "../services/app-access";
31
+ import { discoverableAppEntries } from "../services/app-discovery";
30
32
  import { approvalDecisionChannel, approvals } from "../services/approvals";
31
33
  import { pendingApprovalReview } from "../services/approvals/question";
32
34
  import { ImageMetadataError } from "../services/image-metadata";
@@ -99,7 +101,14 @@ import { AI_SHORT_ID_PATTERN } from "./short-id";
99
101
  import { selectAiSkillCatalog } from "./skill-catalog";
100
102
  import { createCloudAiLoadSkillTool, createCloudAiSearchSkillsTool } from "./skill-tool";
101
103
  import { aiSkills } from "./skills";
102
- import { AI_TOOL_CHECK_STALE_MS, aiConversations, claimAiToolCheck, readAiToolCheck, settleAiToolCheck } from "./store";
104
+ import {
105
+ AI_TOOL_CHECK_STALE_MS,
106
+ AiConversationCreationError,
107
+ aiConversations,
108
+ claimAiToolCheck,
109
+ readAiToolCheck,
110
+ settleAiToolCheck,
111
+ } from "./store";
103
112
  import { createAiConversationStreamResponse, loadAiStreamState } from "./stream";
104
113
  import { composeAiSystemPrompt } from "./system-prompt";
105
114
  import { aiToolAudit } from "./tool-audit";
@@ -286,13 +295,17 @@ const prepareConversationForMessageRetry = async (conversationId: string): Promi
286
295
  return (await aiConversations.getActiveTurn({ conversationId })) ? "busy" : "ready";
287
296
  };
288
297
 
289
- const prepareLaunchDraft = async (body: z.infer<typeof AiCreateConversationInputSchema>, subject: AccessSubject | null) => {
298
+ const prepareLaunchDraft = async (
299
+ body: z.infer<typeof AiCreateConversationInputSchema>,
300
+ subject: AccessSubject | null,
301
+ authority: import("../server").RequestAuthority | null = null,
302
+ ) => {
290
303
  const content = [...(body.draft?.content ?? [])];
291
304
  if (body.skills?.length && body.allowedTools && !body.allowedTools.includes("load_skill")) {
292
305
  return fail(err.badInput("Linked Skills require load_skill in allowedTools."));
293
306
  }
294
307
  for (const name of new Set(body.skills ?? [])) {
295
- const skill = await aiSkills.getByName(name, subject);
308
+ const skill = await aiSkills.getByName(name, subject, "read", authority);
296
309
  if (!skill?.enabled) return fail(err.badInput(`Skill is unavailable: ${name}`));
297
310
  if (!content.some((part) => part.type === "resource" && part.ref.type === "core.ai.skill" && part.ref.id === skill.shortId)) {
298
311
  content.push({ type: "resource", ref: { type: "core.ai.skill", id: skill.shortId }, title: skill.name, icon: "ti ti-sparkles" });
@@ -433,7 +446,9 @@ export const aiRoutes = (() => {
433
446
  if (preferredId) previewProfile = selectAiModelProfile(state, ctx.modelPolicy, preferredId);
434
447
  }
435
448
  const toolsSupported = Boolean(previewProfile?.capabilities.includes("tools"));
436
- const availableSkills = toolsSupported ? (await aiSkills.list(c.get("accessSubject"))).filter((skill) => skill.enabled) : [];
449
+ const availableSkills = toolsSupported
450
+ ? (await aiSkills.list(c.get("accessSubject"), { authority: appAccessAuthority(c) })).filter((skill) => skill.enabled)
451
+ : [];
437
452
  const skillCatalog = selectAiSkillCatalog(
438
453
  availableSkills.filter((skill) => !skill.manualOnly).map(({ name, description }) => ({ name, description })),
439
454
  previewProfile?.contextWindow ?? 0,
@@ -447,8 +462,8 @@ export const aiRoutes = (() => {
447
462
  })),
448
463
  ...(client?.localTools === "ask" ? [createCloudAiLocalBashTool(), ...createCloudAiLocalTools()] : []),
449
464
  ...(memoryEnabled ? [createCloudAiMemoryTool()] : []),
450
- ...(availableSkills.length ? [createCloudAiLoadSkillTool(c.get("accessSubject"))] : []),
451
- ...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(c.get("accessSubject"))] : []),
465
+ ...(availableSkills.length ? [createCloudAiLoadSkillTool(c.get("accessSubject"), appAccessAuthority(c))] : []),
466
+ ...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(c.get("accessSubject"), [], appAccessAuthority(c))] : []),
452
467
  ]
453
468
  : [];
454
469
  const memoryToolEnabled = tools.some((tool) => tool.def.name === "memory");
@@ -497,7 +512,7 @@ export const aiRoutes = (() => {
497
512
  c.header("Cache-Control", "no-store");
498
513
  const ctx = await resolveContext(c);
499
514
  if (ctx instanceof Response) return ctx;
500
- setRequestTimeout(c, Math.ceil(QUERY_TIMEOUT_MS / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
515
+ setRequestTimeout(c, Math.ceil(CAPABILITY_DISCOVERY_TIMEOUT_MS / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
501
516
  try {
502
517
  const resources = await listAiRecentResources({
503
518
  ownerUserId: ctx.ownerUserId,
@@ -577,7 +592,7 @@ export const aiRoutes = (() => {
577
592
  const body = c.req.valid("json");
578
593
  const project = body.projectId ? await aiProjects.getByShortId(body.projectId, c.get("accessSubject"), "read") : null;
579
594
  if (body.projectId && !project) return respond(c, fail(err.notFound("Project")));
580
- const draft = await prepareLaunchDraft(body, c.get("accessSubject"));
595
+ const draft = await prepareLaunchDraft(body, c.get("accessSubject"), appAccessAuthority(c));
581
596
  if (!draft.ok) return respond(c, draft);
582
597
  const requestedCapabilityTools = (body.preloadTools ?? []).filter(
583
598
  (requested): requested is { appId: string; kind: "query" | "action"; id: string } => "appId" in requested,
@@ -585,7 +600,7 @@ export const aiRoutes = (() => {
585
600
  let catalog: ReturnType<typeof buildAiCapabilityCatalog> = [];
586
601
  if (requestedCapabilityTools.length) {
587
602
  try {
588
- catalog = buildAiCapabilityCatalog(await listCapabilities());
603
+ catalog = buildAiCapabilityCatalog(await discoverableAppEntries(appAccessAuthority(c), await listCapabilities()));
589
604
  } catch {
590
605
  return respond(c, fail(err.internal("The live capability catalog is unavailable.")));
591
606
  }
@@ -616,24 +631,29 @@ export const aiRoutes = (() => {
616
631
  if (body.allowedTools && preloadTools.some((name) => !body.allowedTools!.includes(name!))) {
617
632
  return respond(c, fail(err.badInput("Preloaded tools must be included in allowedTools.")));
618
633
  }
619
- return respond(
620
- c,
621
- ok(
622
- publicConversation(
623
- await aiConversations.createConversation({
624
- ownerUserId: ctx.ownerUserId,
625
- title: body.title,
626
- projectId: project?.id,
627
- draft: draft.data.content,
628
- preloadTools: preloadTools.map((name) => name!),
629
- allowedTools: body.allowedTools,
630
- launchedByAppId: body.launchedByAppId,
631
- }),
632
- project?.shortId ?? null,
633
- ),
634
- ),
635
- 201,
636
- );
634
+ try {
635
+ const conversation = await aiConversations.createConversation({
636
+ requestId: body.requestId,
637
+ ownerUserId: ctx.ownerUserId,
638
+ title: body.title,
639
+ projectId: project?.id,
640
+ draft: draft.data.content,
641
+ preloadTools: preloadTools.map((name) => name!),
642
+ allowedTools: body.allowedTools,
643
+ launchedByAppId: body.launchedByAppId,
644
+ });
645
+ // A replay may return a conversation assigned to a Project since creation.
646
+ const currentProject = conversation.projectId
647
+ ? await aiProjects.get(conversation.projectId, c.get("accessSubject"), "read")
648
+ : null;
649
+ if (conversation.projectId && !currentProject) return respond(c, fail(err.notFound("Project")));
650
+ return respond(c, ok(publicConversation(conversation, currentProject?.shortId ?? null)), 201);
651
+ } catch (error) {
652
+ if (error instanceof AiConversationCreationError) {
653
+ return respond(c, fail(error.reason === "conflict" ? err.conflict(error.message) : err.notFound("Conversation")));
654
+ }
655
+ throw error;
656
+ }
637
657
  })
638
658
  .get("/conversations/:conversationId", async (c) => {
639
659
  const ctx = await resolveContext(c);
@@ -642,6 +662,24 @@ export const aiRoutes = (() => {
642
662
  if (!conversation) return notFound(c);
643
663
  return respond(c, ok(await conversationDetail(conversation, c.get("accessSubject"))));
644
664
  })
665
+ .get(
666
+ "/conversations/:conversationId/submission/:draftRevision",
667
+ v("param", z.object({ conversationId: z.string().regex(AI_SHORT_ID_PATTERN), draftRevision: z.coerce.number().int().min(0) })),
668
+ async (c) => {
669
+ const ctx = await resolveContext(c);
670
+ if (ctx instanceof Response) return ctx;
671
+ const conversation = await loadConversation(c, ctx);
672
+ if (!conversation) return notFound(c);
673
+ if (conversation.projectId && !(await aiProjects.get(conversation.projectId, c.get("accessSubject"), "read"))) return notFound(c);
674
+ const submission = await aiConversations.getDraftSubmission({
675
+ conversationId: conversation.id,
676
+ ownerUserId: ctx.ownerUserId,
677
+ revision: c.req.valid("param").draftRevision,
678
+ });
679
+ // Null is deliberately not a claim that the request was rejected.
680
+ return respond(c, ok({ submission }));
681
+ },
682
+ )
645
683
  .get("/conversations/:conversationId/messages", v("query", MessagesPageQuerySchema), async (c) => {
646
684
  const ctx = await resolveContext(c);
647
685
  if (ctx instanceof Response) return ctx;
@@ -1223,7 +1261,7 @@ export const aiRoutes = (() => {
1223
1261
  if (!turn || !callId) return notFound(c);
1224
1262
  const target = { conversationId: conversation.id, turnId: turn.id, callId };
1225
1263
  // The app's Query may take its whole budget; the request must outlast it.
1226
- setRequestTimeout(c, Math.ceil(QUERY_TIMEOUT_MS / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
1264
+ setRequestTimeout(c, Math.ceil(capabilityTimeoutMs("query") / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
1227
1265
  try {
1228
1266
  const messages = await aiConversations.listTurnMessages({
1229
1267
  conversationId: conversation.id,
@@ -1256,7 +1294,7 @@ export const aiRoutes = (() => {
1256
1294
  message.message.role === "assistant"
1257
1295
  ? message.message.content.find((block) => block.type === "tool_call" && block.id === callId)
1258
1296
  : undefined;
1259
- const catalog = buildAiCapabilityCatalog(await listCapabilities());
1297
+ const catalog = buildAiCapabilityCatalog(await discoverableAppEntries(appAccessAuthority(c), await listCapabilities()));
1260
1298
  const presentation = message.meta?.toolPresentations?.[callId];
1261
1299
  const entry = catalog.find(
1262
1300
  (entry) =>
@@ -1371,7 +1409,7 @@ export const aiRoutes = (() => {
1371
1409
  const result = publicPendingAction(action, conversation.shortId, turn.shortId);
1372
1410
  if (action.type !== "approval_request") return result;
1373
1411
  const record = records.find((record) => record.callId === action.callId);
1374
- const request = record?.approvalId ? await approvals.get(record.approvalId) : null;
1412
+ const request = record?.approvalId ? await approvals.get(record.approvalId, undefined, true) : null;
1375
1413
  const reviewCallId = record?.kind === "custom_approval" ? action.callId.replace(/-approval-[0-9]+$/, "") : action.callId;
1376
1414
  const stored = reviews.get(reviewCallId);
1377
1415
  let review: AiStoredActionReview | null = stored && !isLegacyActionReview(stored) ? stored : null;
package/src/ai/runtime.ts CHANGED
@@ -55,6 +55,7 @@ const log = logger("ai:runtime");
55
55
 
56
56
  const AI_WORKER_ID = `worker-${crypto.randomUUID()}`;
57
57
  const AI_TURN_HEARTBEAT_MS = 3_000;
58
+ // Local turn orchestration (tools and approval waits included), not a model-provider quota.
58
59
  const AI_TURN_WORKER_CONCURRENCY = 8;
59
60
  const AI_TURN_MAX_ATTEMPTS = 5;
60
61
 
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { AccessSubject } from "../server";
3
+ import type { RequestAuthority } from "../server/middleware/auth";
3
4
  import { AI_SKILL_FILE_MOUNT } from "./file-mount";
4
5
  import { AI_SHORT_ID_PATTERN } from "./short-id";
5
6
  import { type AiSkillCatalogEntry, mergeAiSkillCatalog, rankAiSkillCatalog, searchAiSkillCatalog } from "./skill-catalog";
@@ -8,11 +9,17 @@ import { aiSkills } from "./skills";
8
9
  import { defineAiTool } from "./tools";
9
10
 
10
11
  /** Explicit selections use the same permission check and turn snapshot as load_skill. */
11
- export async function loadSelectedAiSkills(ids: readonly string[], turnId: string, subject: AccessSubject | null, allowed: boolean) {
12
+ export async function loadSelectedAiSkills(
13
+ ids: readonly string[],
14
+ turnId: string,
15
+ subject: AccessSubject | null,
16
+ allowed: boolean,
17
+ authority: RequestAuthority | null = null,
18
+ ) {
12
19
  const loaded = [];
13
20
  for (const id of new Set(ids)) {
14
21
  if (!subject || !allowed) throw new Error("The selected Skill requires a model and tool scope that allow Skills.");
15
- const skill = await aiSkills.loadForTurn(turnId, { id }, subject);
22
+ const skill = await aiSkills.loadForTurn(turnId, { id }, subject, authority);
16
23
  if (!skill) throw new Error(`Selected Skill ${id} is unavailable or access was revoked.`);
17
24
  loaded.push({
18
25
  name: skill.name,
@@ -51,7 +58,7 @@ export const CloudAiLoadSkillOutputSchema = z.object({
51
58
  files: z.array(z.string()),
52
59
  });
53
60
 
54
- export const createCloudAiLoadSkillTool = (subject: AccessSubject) =>
61
+ export const createCloudAiLoadSkillTool = (subject: AccessSubject, authority: RequestAuthority | null = null) =>
55
62
  defineAiTool({
56
63
  name: "load_skill",
57
64
  description:
@@ -62,7 +69,7 @@ export const createCloudAiLoadSkillTool = (subject: AccessSubject) =>
62
69
  }).server(async (input, ctx) => {
63
70
  if (!ctx.turnId) throw new Error("load_skill requires a turn context.");
64
71
  const selector = input.id ? { id: input.id } : input.name;
65
- const snapshot = selector ? await aiSkills.loadForTurn(ctx.turnId, selector, subject) : null;
72
+ const snapshot = selector ? await aiSkills.loadForTurn(ctx.turnId, selector, subject, authority) : null;
66
73
  if (!snapshot) throw new Error(`Skill "${input.id ?? input.name}" is unavailable or access was revoked.`);
67
74
  return {
68
75
  name: snapshot.name,
@@ -84,7 +91,11 @@ export const CloudAiSearchSkillsOutputSchema = z.object({
84
91
  more: z.boolean(),
85
92
  });
86
93
 
87
- export const createCloudAiSearchSkillsTool = (subject: AccessSubject | null, localSkills: readonly AiSkillCatalogEntry[] = []) =>
94
+ export const createCloudAiSearchSkillsTool = (
95
+ subject: AccessSubject | null,
96
+ localSkills: readonly AiSkillCatalogEntry[] = [],
97
+ authority: RequestAuthority | null = null,
98
+ ) =>
88
99
  defineAiTool({
89
100
  name: "search_skills",
90
101
  description:
@@ -94,7 +105,7 @@ export const createCloudAiSearchSkillsTool = (subject: AccessSubject | null, loc
94
105
  approval: "never",
95
106
  }).server(async (input) => {
96
107
  const result = subject
97
- ? await aiSkills.search(subject, input.query, input.limit + localSkills.length, { modelInvocableOnly: true })
108
+ ? await aiSkills.search(subject, input.query, input.limit + localSkills.length, { modelInvocableOnly: true, authority })
98
109
  : { skills: [], more: false };
99
110
  const cloud = result.skills.map(({ name, description }) => ({ name, description }));
100
111
  if (!localSkills.length) return { skills: cloud, more: result.more };
@@ -1,9 +1,13 @@
1
1
  import { Hono, type MiddlewareHandler } from "hono";
2
2
  import { HTTPException } from "hono/http-exception";
3
3
  import { z } from "zod";
4
+ import { getApp } from "../_internal/registry";
4
5
  import { AuthenticatedPrincipalSchema } from "../contracts/shared";
5
6
  import { type AuthContext, auth, err, fail, ok, rateLimit, respond, v } from "../server";
7
+ import { appAccessDeniedBody } from "../server/middleware/app-access";
6
8
  import { noTransform } from "../server/middleware/no-transform";
9
+ import { appAccessAuthority, canUseApp } from "../services/app-access";
10
+ import { recordAppAccessDenied } from "../services/app-access-metrics";
7
11
  import { localizedAppName } from "../ssr/runtime";
8
12
  import { aiProjects } from "./projects";
9
13
  import {
@@ -62,6 +66,20 @@ type AiSkillsRouteDependencies = {
62
66
  authenticate?: MiddlewareHandler<AuthContext>;
63
67
  };
64
68
 
69
+ // Core exempts this mixed router from its method-independent public-path gate.
70
+ // Only list/get are brokers; management retains Core entry and its user-backed policy.
71
+ const requireCoreAccess: MiddlewareHandler<AuthContext> = async (c, next) => {
72
+ const core = await getApp("core").catch(() => null);
73
+ const allowed = core && (await canUseApp(appAccessAuthority(c), core).catch(() => false));
74
+ if (!allowed) {
75
+ if (core) recordAppAccessDenied(core.id);
76
+ c.header("Cache-Control", "private, no-store");
77
+ c.header("X-Cloud-App-Access-Denied", "core");
78
+ return c.json(appAccessDeniedBody(core ?? { id: "core", name: "Core" }), 403);
79
+ }
80
+ await next();
81
+ };
82
+
65
83
  const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
66
84
  new Hono<AuthContext>()
67
85
  // The list combines skills from separate grants; the writer of one can rename it next to another skill's text.
@@ -86,13 +104,19 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
86
104
  const { q, name } = c.req.valid("query");
87
105
  const skills =
88
106
  name !== undefined
89
- ? await aiSkills.list(subject, { name })
107
+ ? await aiSkills.list(subject, { name, authority: appAccessAuthority(c) })
90
108
  : q === undefined
91
- ? await aiSkills.list(subject)
92
- : (await aiSkills.search(subject, q)).skills;
109
+ ? await aiSkills.list(subject, { authority: appAccessAuthority(c) })
110
+ : (await aiSkills.search(subject, q, 30, { authority: appAccessAuthority(c) })).skills;
93
111
  return respond(c, ok({ skills: skills.map(publicSummary(c)) }));
94
112
  },
95
113
  )
114
+ .get("/:skillId", async (c) => {
115
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "read", appAccessAuthority(c));
116
+ return skill ? respond(c, ok({ skill: publicSkill(c, skill) })) : respond(c, fail(err.notFound("Skill")));
117
+ })
118
+ .use("*", requireCoreAccess)
119
+ .use("*", auth.requireUser())
96
120
  .get(
97
121
  "/project-links/:projectId",
98
122
  v(
@@ -108,7 +132,12 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
108
132
  query = c.req.valid("query");
109
133
  const project = await aiProjects.getByShortId(c.req.param("projectId")!, subject, query.available === "true" ? "admin" : "read");
110
134
  const result = project
111
- ? await aiSkills.projectSkills(project.id, subject, { query: query.q, page: query.page, available: query.available === "true" })
135
+ ? await aiSkills.projectSkills(project.id, subject, {
136
+ query: query.q,
137
+ page: query.page,
138
+ available: query.available === "true",
139
+ authority: appAccessAuthority(c),
140
+ })
112
141
  : null;
113
142
  return result
114
143
  ? respond(c, ok({ ...result, items: result.items.map(publicSummary(c)) }))
@@ -117,8 +146,8 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
117
146
  )
118
147
  .get("/:skillId/projects", async (c) => {
119
148
  const subject = c.get("accessSubject") ?? null;
120
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin");
121
- const projects = skill ? await aiSkills.linkedProjects(skill.id, subject) : null;
149
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin", appAccessAuthority(c));
150
+ const projects = skill ? await aiSkills.linkedProjects(skill.id, subject, appAccessAuthority(c)) : null;
122
151
  return projects
123
152
  ? respond(c, ok({ projects: projects.map((project) => ({ id: project.shortId, name: project.name })) }))
124
153
  : respond(c, fail(err.notFound("Skill")));
@@ -126,11 +155,11 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
126
155
  .put("/:skillId/projects/:projectId", v("json", z.object({ linked: z.boolean() }).strict()), async (c) => {
127
156
  const subject = c.get("accessSubject") ?? null;
128
157
  const [skill, project] = await Promise.all([
129
- aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin"),
158
+ aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin", appAccessAuthority(c)),
130
159
  aiProjects.getByShortId(c.req.param("projectId")!, subject, "admin"),
131
160
  ]);
132
161
  const linked = c.req.valid("json").linked;
133
- return skill && project && (await aiSkills.linkProject(skill.id, project.id, linked, subject))
162
+ return skill && project && (await aiSkills.linkProject(skill.id, project.id, linked, subject, appAccessAuthority(c)))
134
163
  ? respond(c, ok({ linked }))
135
164
  : respond(c, fail(err.notFound("Skill or Project")));
136
165
  })
@@ -138,22 +167,22 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
138
167
  const subject = c.get("accessSubject");
139
168
  if (!subject) return respond(c, fail(err.forbidden("Skills require an authenticated access subject.")));
140
169
  try {
141
- return respond(c, ok({ skill: publicSkill(c, await aiSkills.create({ subject, ...c.req.valid("json") })) }), 201);
170
+ return respond(
171
+ c,
172
+ ok({ skill: publicSkill(c, await aiSkills.create({ subject, ...c.req.valid("json") }, appAccessAuthority(c))) }),
173
+ 201,
174
+ );
142
175
  } catch (error) {
143
176
  if (isUniqueViolation(error)) return respond(c, fail(err.conflict("A skill with this name already exists.")));
144
177
  if (error instanceof AiSkillInputError) return respond(c, fail(err.badInput(error.message)));
145
178
  throw error;
146
179
  }
147
180
  })
148
- .get("/:skillId", async (c) => {
149
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null);
150
- return skill ? respond(c, ok({ skill: publicSkill(c, skill) })) : respond(c, fail(err.notFound("Skill")));
151
- })
152
181
  .put("/:skillId", v("json", UpdateSkillSchema), async (c) => {
153
- const existing = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "write");
182
+ const existing = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "write", appAccessAuthority(c));
154
183
  if (!existing) return respond(c, fail(err.notFound("Skill")));
155
184
  try {
156
- const skill = await aiSkills.update(existing.id, c.get("accessSubject") ?? null, c.req.valid("json"));
185
+ const skill = await aiSkills.update(existing.id, c.get("accessSubject") ?? null, c.req.valid("json"), appAccessAuthority(c));
157
186
  return skill ? respond(c, ok({ skill: publicSkill(c, skill) })) : respond(c, fail(err.notFound("Skill")));
158
187
  } catch (error) {
159
188
  if (isUniqueViolation(error)) return respond(c, fail(err.conflict("A skill with this name already exists.")));
@@ -163,32 +192,41 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
163
192
  }
164
193
  })
165
194
  .delete("/:skillId", async (c) => {
166
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin");
167
- return skill && (await aiSkills.delete(skill.id, c.get("accessSubject") ?? null))
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)))
168
197
  ? respond(c, ok({ deleted: true }))
169
198
  : respond(c, fail(err.notFound("Skill")));
170
199
  })
171
200
  .put("/:skillId/enabled", dependencies.authenticate ?? auth.requireRole("authenticated"), v("json", SkillEnabledSchema), async (c) => {
172
201
  const subject = c.get("accessSubject") ?? null;
173
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, subject);
174
- const enabled = skill ? await aiSkills.setEnabled(skill.id, subject, c.req.valid("json").enabled) : null;
202
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, subject, "read", appAccessAuthority(c));
203
+ const enabled = skill ? await aiSkills.setEnabled(skill.id, subject, c.req.valid("json").enabled, appAccessAuthority(c)) : null;
175
204
  return enabled === null ? respond(c, fail(err.notFound("Skill"))) : respond(c, ok({ enabled }));
176
205
  })
177
206
  .get("/:skillId/access", async (c) => {
178
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin");
179
- const access = skill ? await aiSkills.listAccess(skill.id, c.get("accessSubject") ?? null) : null;
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;
180
209
  return access ? respond(c, ok({ access })) : respond(c, fail(err.notFound("Skill")));
181
210
  })
182
211
  .post("/:skillId/access", v("json", SkillAccessSchema), async (c) => {
183
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin");
184
- const access = skill ? await aiSkills.grantAccess(skill.id, c.get("accessSubject") ?? null, c.req.valid("json")) : null;
212
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin", appAccessAuthority(c));
213
+ const access = skill
214
+ ? await aiSkills.grantAccess(skill.id, c.get("accessSubject") ?? null, c.req.valid("json"), undefined, appAccessAuthority(c))
215
+ : null;
185
216
  return access ? respond(c, ok({ access }), 201) : respond(c, fail(err.notFound("Skill")));
186
217
  })
187
218
  .patch("/:skillId/access/:accessId", v("json", SkillAccessUpdateSchema), async (c) => {
188
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin");
219
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin", appAccessAuthority(c));
189
220
  try {
190
221
  return skill &&
191
- (await aiSkills.updateAccess(skill.id, c.req.param("accessId")!, c.get("accessSubject") ?? null, c.req.valid("json").permission))
222
+ (await aiSkills.updateAccess(
223
+ skill.id,
224
+ c.req.param("accessId")!,
225
+ c.get("accessSubject") ?? null,
226
+ c.req.valid("json").permission,
227
+ undefined,
228
+ appAccessAuthority(c),
229
+ ))
192
230
  ? respond(c, ok({ updated: true }))
193
231
  : respond(c, fail(err.notFound("Access entry")));
194
232
  } catch (error) {
@@ -197,9 +235,16 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
197
235
  }
198
236
  })
199
237
  .delete("/:skillId/access/:accessId", async (c) => {
200
- const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin");
238
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, c.get("accessSubject") ?? null, "admin", appAccessAuthority(c));
201
239
  try {
202
- return skill && (await aiSkills.revokeAccess(skill.id, c.req.param("accessId")!, c.get("accessSubject") ?? null))
240
+ 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
+ ))
203
248
  ? respond(c, ok({ deleted: true }))
204
249
  : respond(c, fail(err.notFound("Access entry")));
205
250
  } catch (error) {