@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.
- package/package.json +6 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +145 -89
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +7 -1
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +202 -22
- package/src/ai/chat/action-text.ts +1 -1
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +56 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/http.ts +1 -0
- package/src/ai/migrate.ts +13 -0
- package/src/ai/open-questions.ts +4 -8
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +69 -31
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +168 -33
- package/src/ai/types.ts +10 -3
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +46 -38
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +5 -3
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +148 -54
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +1 -1
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/index.ts +14 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +11 -7
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +9 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +3 -2
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +3 -2
- package/src/ssr/index.ts +1 -1
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/global.css +70 -6
- 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 {
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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(
|
|
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
|
|
package/src/ai/skill-tool.ts
CHANGED
|
@@ -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(
|
|
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 = (
|
|
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 };
|
package/src/ai/skills-routes.ts
CHANGED
|
@@ -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, {
|
|
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(
|
|
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
|
|
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(
|
|
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 &&
|
|
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) {
|