@k2b/cloud 0.6.0 → 0.8.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/README.md +2 -2
- package/package.json +22 -11
- package/scripts/README.md +10 -0
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +20 -35
- package/scripts/preload.ts +16 -12
- package/scripts/sync-recovery-smoke.ts +22 -6
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/build-metadata.ts +8 -1
- package/src/_internal/capabilities.ts +131 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +80 -37
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/readiness.ts +4 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +18 -151
- package/src/access/PrincipalPicker.tsx +110 -0
- package/src/access/ResourceApiKeys.tsx +1 -1
- package/src/access/ui.ts +1 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +201 -0
- package/src/ai/browser.ts +4 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +229 -90
- package/src/ai/chat/builtin-tools.tsx +2 -4
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +83 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +25 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +31 -47
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +21 -2
- package/src/ai/chat/tool-groups.ts +58 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +14 -10
- package/src/ai/chat-tasks.ts +297 -43
- package/src/ai/client/controller.ts +165 -39
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +29 -11
- package/src/ai/client/transport.ts +6 -0
- package/src/ai/code-capability-routes.ts +105 -0
- package/src/ai/code-capability-transport.ts +39 -0
- package/src/ai/code-execution.ts +53 -0
- package/src/ai/code-mode-skill.ts +118 -0
- package/src/ai/code-runtime-tools.ts +179 -0
- package/src/ai/code-source-contracts.ts +270 -0
- package/src/ai/code-source-tools.ts +188 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +99 -3
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +49 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +189 -33
- package/src/ai/fetch-file-tool.ts +2 -2
- package/src/ai/file-content-version.ts +27 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +8 -1
- package/src/ai/files-store.ts +212 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +42 -10
- package/src/ai/http.ts +26 -8
- package/src/ai/index.ts +26 -3
- package/src/ai/inference-calls.ts +197 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/live.ts +1 -1
- package/src/ai/markdown-pdf-tool.ts +2 -2
- package/src/ai/memories.ts +30 -27
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +64 -32
- package/src/ai/memory-tool.ts +1 -5
- package/src/ai/memory-workflow-evidence.ts +9 -4
- package/src/ai/message-queue.ts +178 -0
- package/src/ai/migrate.ts +189 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +75 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +18 -43
- package/src/ai/projects.ts +33 -14
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +192 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +241 -11
- package/src/ai/run-timeout.ts +12 -0
- package/src/ai/runtime-tools.ts +7 -5
- package/src/ai/runtime.ts +74 -17
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +4 -5
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +58 -0
- package/src/ai/skill-seeds.ts +114 -20
- package/src/ai/skill-tool.ts +21 -5
- package/src/ai/skills-routes.ts +49 -4
- package/src/ai/skills.ts +358 -163
- package/src/ai/store.ts +416 -201
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +12 -1
- package/src/ai/system-prompt.ts +14 -2
- package/src/ai/task-contracts.ts +84 -0
- package/src/ai/task-execution.ts +92 -0
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +32 -0
- package/src/ai/todo-tool.ts +19 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +3 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +60 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +80 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-linux-identities.ts +10 -4
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/app-approval.ts +2 -2
- package/src/api/auth.ts +1 -1
- package/src/api/capabilities.ts +324 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +8 -2
- package/src/api/mcp.ts +45 -34
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +67 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +6 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +116 -35
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +77 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +10 -1
- package/src/cli/admin/instance.ts +2 -2
- package/src/cli/admin/linux.ts +4 -2
- package/src/cli/admin/sync.ts +1 -1
- package/src/cli/capabilities.ts +42 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/define-env.ts +97 -0
- package/src/config/env.ts +147 -51
- package/src/config/index.ts +3 -1
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +75 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +5 -2
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +5 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +18 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/index.ts +1 -0
- package/src/desktop/solid.tsx +1 -1
- package/src/index.ts +8 -6
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +3 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/notification-sender.ts +12 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/app-approval.ts +11 -10
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +8 -5
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/identity/runtime-config.ts +6 -5
- package/src/services/index.ts +15 -0
- package/src/services/ipa/posix.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +21 -4
- package/src/services/mandates/policy.ts +165 -2
- package/src/services/notifications/batches.ts +125 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +131 -72
- package/src/services/pdf/index.ts +11 -8
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +29 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/app.ts +5 -1
- package/src/services/settings/core-settings.ts +26 -35
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/index.ts +3 -3
- package/src/services/settings/store.ts +58 -84
- package/src/services/webauthn.ts +1 -1
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +3 -0
- package/src/shared/markdown/extensions/info-blocks.ts +1 -1
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +30 -16
- package/src/ssr/AppLaunchpad.island.tsx +33 -136
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +22 -23
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MinimalLayout.tsx +1 -1
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +11 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/index.ts +2 -2
- package/src/workflows/ai/runtime.ts +19 -90
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai/types.ts +6 -82
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +4 -2
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/server/services/freeipa/test-certificates.ts +0 -44
- package/src/services/notifications/live.ts +0 -50
- package/src/services/session/test-fixture.ts +0 -18
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
package/src/ai/skills.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { type SQL, type SQLQuery, sql } from "bun";
|
|
3
|
+
import { AuthenticatedPrincipalSchema } from "../contracts/shared";
|
|
2
4
|
import type { AccessSubject } from "../server";
|
|
3
5
|
import {
|
|
4
6
|
buildAccessPrincipalCondition,
|
|
@@ -8,8 +10,10 @@ import {
|
|
|
8
10
|
type PermissionLevel,
|
|
9
11
|
type Principal,
|
|
10
12
|
} from "../server/services/access";
|
|
13
|
+
import { accessRevision } from "../server/services/access-revision";
|
|
11
14
|
import { toPgUuidArray } from "../services/postgres";
|
|
12
15
|
import { mountAiSkillFilePath } from "./file-mount";
|
|
16
|
+
import { aiProjects } from "./projects";
|
|
13
17
|
import { AI_SHORT_ID_PATTERN, withAiShortIdForDb } from "./short-id";
|
|
14
18
|
import {
|
|
15
19
|
type AiSkillExtraFrontmatter,
|
|
@@ -21,6 +25,8 @@ import {
|
|
|
21
25
|
validateAiSkillName,
|
|
22
26
|
validateAiSkillReferences,
|
|
23
27
|
} from "./skill-format";
|
|
28
|
+
import { aiSkillSearchSql, withAiSkillSearch } from "./skill-search";
|
|
29
|
+
import { getBuiltinAiSkillTemplates } from "./skill-seeds";
|
|
24
30
|
import type { AiSkillFileToolContent, AiSkillFileToolStat } from "./types";
|
|
25
31
|
|
|
26
32
|
export type AiSkillPermission = Exclude<PermissionLevel, "none">;
|
|
@@ -55,7 +61,22 @@ export type AiSkillAccess = {
|
|
|
55
61
|
createdAt: string;
|
|
56
62
|
};
|
|
57
63
|
|
|
64
|
+
export type AiSkillTemplate = {
|
|
65
|
+
key: string;
|
|
66
|
+
version: number;
|
|
67
|
+
name: string;
|
|
68
|
+
description: string;
|
|
69
|
+
instructions: string;
|
|
70
|
+
extraFrontmatter?: AiSkillExtraFrontmatter;
|
|
71
|
+
references?: readonly AiSkillReferenceInput[];
|
|
72
|
+
};
|
|
73
|
+
|
|
58
74
|
export type AiSkillAdminListItem = {
|
|
75
|
+
revision: number;
|
|
76
|
+
templateId: string | null;
|
|
77
|
+
templateVersion: number | null;
|
|
78
|
+
currentTemplateVersion: number | null;
|
|
79
|
+
templateStatus: "current" | "modified" | "update_available" | null;
|
|
59
80
|
id: string;
|
|
60
81
|
shortId: string;
|
|
61
82
|
name: string;
|
|
@@ -86,6 +107,9 @@ type SkillRow = {
|
|
|
86
107
|
id: string;
|
|
87
108
|
short_id: string;
|
|
88
109
|
managed_key: string | null;
|
|
110
|
+
template_id: string | null;
|
|
111
|
+
template_version: number | null;
|
|
112
|
+
template_hash: string | null;
|
|
89
113
|
name: string;
|
|
90
114
|
description: string;
|
|
91
115
|
instructions: string;
|
|
@@ -106,13 +130,14 @@ type SkillAccessRow = {
|
|
|
106
130
|
display_name: string | null;
|
|
107
131
|
};
|
|
108
132
|
|
|
109
|
-
type SkillSummaryRow = SkillRow & {
|
|
133
|
+
type SkillSummaryRow = Pick<SkillRow, "id" | "short_id" | "name" | "description" | "revision" | "created_at" | "updated_at"> & {
|
|
110
134
|
permission: AiSkillPermission;
|
|
111
135
|
reference_count: number;
|
|
112
136
|
enabled: boolean;
|
|
113
137
|
};
|
|
114
138
|
|
|
115
139
|
type AdminSkillRow = SkillRow & {
|
|
140
|
+
content_references: AiSkillReference[] | string;
|
|
116
141
|
reference_count: number;
|
|
117
142
|
access_count: number;
|
|
118
143
|
admin_count: number;
|
|
@@ -162,15 +187,17 @@ const principalForSubject = (subject: AccessSubject): Principal =>
|
|
|
162
187
|
: { type: "service_account", serviceAccountId: subject.serviceAccountId };
|
|
163
188
|
|
|
164
189
|
const accessMatch = (subject: AccessSubject | null): SQLQuery =>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
190
|
+
subject === null
|
|
191
|
+
? sql`FALSE`
|
|
192
|
+
: buildAccessPrincipalCondition({
|
|
193
|
+
subject,
|
|
194
|
+
columns: {
|
|
195
|
+
userId: sql`access.user_id`,
|
|
196
|
+
groupId: sql`access.group_id`,
|
|
197
|
+
serviceAccountId: sql`access.service_account_id`,
|
|
198
|
+
authenticatedOnly: sql`access.authenticated_only`,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
174
201
|
|
|
175
202
|
const getRow = async (skillId: string, db: SQL = sql): Promise<SkillRow | null> =>
|
|
176
203
|
(await db<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid`)[0] ?? null;
|
|
@@ -181,17 +208,24 @@ const getRowByShortId = async (shortId: string, db: SQL = sql): Promise<SkillRow
|
|
|
181
208
|
const getRowByName = async (name: string, db: SQL = sql): Promise<SkillRow | null> =>
|
|
182
209
|
(await db<SkillRow[]>`SELECT * FROM ai.skills WHERE name = ${name}`)[0] ?? null;
|
|
183
210
|
|
|
211
|
+
// Project links grant read only. Their creator's later permissions are irrelevant.
|
|
212
|
+
const effectivePermission = (skillId: SQLQuery, subject: AccessSubject | null) => sql`
|
|
213
|
+
SELECT permission FROM (
|
|
214
|
+
SELECT access.permission FROM ai.skill_access link JOIN auth.access access ON access.id=link.access_id
|
|
215
|
+
WHERE link.skill_id=${skillId} AND ${accessMatch(subject)}
|
|
216
|
+
UNION ALL
|
|
217
|
+
SELECT 'read'::auth.permission_level FROM ai.project_skills link
|
|
218
|
+
JOIN ai.project_access project_access ON project_access.project_id=link.project_id
|
|
219
|
+
JOIN auth.access access ON access.id=project_access.access_id
|
|
220
|
+
WHERE link.skill_id=${skillId} AND access.permission <> 'none' AND ${accessMatch(subject)}
|
|
221
|
+
) grants ORDER BY CASE permission WHEN 'admin' THEN 3 WHEN 'write' THEN 2 WHEN 'read' THEN 1 ELSE 0 END DESC LIMIT 1
|
|
222
|
+
`;
|
|
223
|
+
|
|
184
224
|
const permissionFor = async (skillId: string, subject: AccessSubject | null, db: SQL = sql): Promise<AiSkillPermission | "none"> => {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
JOIN auth.access access ON access.id = skill_access.access_id
|
|
190
|
-
WHERE skill_access.skill_id = ${skillId}::uuid AND ${match}
|
|
191
|
-
ORDER BY CASE access.permission WHEN 'admin' THEN 3 WHEN 'write' THEN 2 WHEN 'read' THEN 1 ELSE 0 END DESC
|
|
192
|
-
LIMIT 1
|
|
193
|
-
`;
|
|
194
|
-
return rows[0]?.permission ?? "none";
|
|
225
|
+
const [row] = await db<
|
|
226
|
+
{ permission: AiSkillPermission | null }[]
|
|
227
|
+
>`SELECT (${effectivePermission(sql`${skillId}::uuid`, subject)}) AS permission`;
|
|
228
|
+
return row?.permission ?? "none";
|
|
195
229
|
};
|
|
196
230
|
|
|
197
231
|
const enabledFor = async (skillId: string, subject: AccessSubject | null, db: SQL = sql): Promise<boolean> => {
|
|
@@ -280,6 +314,7 @@ const createSkillAccess = async (
|
|
|
280
314
|
input: { principal: Principal; permission: AiSkillPermission },
|
|
281
315
|
db: SQL,
|
|
282
316
|
): Promise<AiSkillAccess> => {
|
|
317
|
+
AuthenticatedPrincipalSchema.parse(input.principal);
|
|
283
318
|
const created = await createAccess(input, db);
|
|
284
319
|
if (!created.ok) throw new Error(created.error.message);
|
|
285
320
|
let shortId = "";
|
|
@@ -300,6 +335,10 @@ const createSkillAccess = async (
|
|
|
300
335
|
return entry;
|
|
301
336
|
};
|
|
302
337
|
|
|
338
|
+
const checkAccessRevision = async (skillId: string, expected: string | undefined, db: SQL) => {
|
|
339
|
+
if (expected !== undefined && accessRevision(await listSkillAccess(skillId, db)) !== expected) throw new AiSkillRevisionConflictError();
|
|
340
|
+
};
|
|
341
|
+
|
|
303
342
|
const updateSkillAccess = async (skillId: string, accessId: string, permission: AiSkillPermission, db: SQL): Promise<boolean> => {
|
|
304
343
|
const [bound] = await db<{ id: string; permission: AiSkillPermission }[]>`
|
|
305
344
|
SELECT access.id, access.permission
|
|
@@ -361,26 +400,92 @@ const validatedFields = (input: {
|
|
|
361
400
|
}
|
|
362
401
|
};
|
|
363
402
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
403
|
+
// JSON object key order and reference insertion order are not content changes.
|
|
404
|
+
const canonicalJson = (value: unknown): string => {
|
|
405
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
406
|
+
if (value !== null && typeof value === "object") {
|
|
407
|
+
return `{${Object.entries(value)
|
|
408
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
409
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
|
|
410
|
+
.join(",")}}`;
|
|
411
|
+
}
|
|
412
|
+
return JSON.stringify(value);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const contentHash = (fields: ReturnType<typeof validatedFields>): string =>
|
|
416
|
+
createHash("sha256")
|
|
417
|
+
.update(
|
|
418
|
+
canonicalJson({
|
|
419
|
+
...fields,
|
|
420
|
+
references: [...fields.references].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
|
|
421
|
+
}),
|
|
422
|
+
)
|
|
423
|
+
.digest("hex");
|
|
424
|
+
|
|
425
|
+
const rowContentHash = async (row: SkillRow, db: SQL = sql): Promise<string> =>
|
|
426
|
+
contentHash({
|
|
427
|
+
name: row.name,
|
|
428
|
+
description: row.description,
|
|
429
|
+
instructions: row.instructions,
|
|
430
|
+
extraFrontmatter: jsonObject(row.extra_frontmatter),
|
|
431
|
+
references: await listReferences(row.id, db),
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const validateTemplate = (input: AiSkillTemplate) => {
|
|
435
|
+
if (!input.key.trim() || !Number.isSafeInteger(input.version) || input.version < 1 || input.version > 2147483647)
|
|
436
|
+
throw new AiSkillInputError("A template needs a stable ID and a positive integer version.");
|
|
437
|
+
const fields = validatedFields(input);
|
|
438
|
+
return { fields, hash: contentHash(fields) };
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const replaceTemplate = async (row: SkillRow, template: AiSkillTemplate, db: SQL): Promise<void> => {
|
|
442
|
+
const { fields, hash } = validateTemplate(template);
|
|
443
|
+
await db`UPDATE ai.skills SET name = ${fields.name}, description = ${fields.description},
|
|
444
|
+
instructions = ${fields.instructions}, extra_frontmatter = (${JSON.stringify(fields.extraFrontmatter)}::text)::jsonb,
|
|
445
|
+
template_id = ${template.key}, template_version = ${template.version}, template_hash = ${hash},
|
|
446
|
+
revision = revision + 1, updated_at = now() WHERE id = ${row.id}::uuid`;
|
|
447
|
+
await db`DELETE FROM ai.skill_references WHERE skill_id = ${row.id}::uuid`;
|
|
448
|
+
for (const ref of fields.references)
|
|
449
|
+
await db`INSERT INTO ai.skill_references (skill_id, path, content)
|
|
450
|
+
VALUES (${row.id}::uuid, ${ref.path}, ${ref.content})`;
|
|
451
|
+
};
|
|
367
452
|
|
|
368
453
|
const skillSearchPattern = (search?: string): string | null => {
|
|
369
454
|
const value = search?.trim().toLowerCase();
|
|
370
455
|
return value ? `%${value.replace(/[\\%_]/g, (char) => `\\${char}`)}%` : null;
|
|
371
456
|
};
|
|
372
457
|
|
|
373
|
-
const toAdminSkill = (row: AdminSkillRow): AiSkillAdminListItem =>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
458
|
+
const toAdminSkill = (row: AdminSkillRow): AiSkillAdminListItem => {
|
|
459
|
+
const template = getBuiltinAiSkillTemplates().find((entry) => entry.key === row.template_id);
|
|
460
|
+
return {
|
|
461
|
+
revision: Number(row.revision),
|
|
462
|
+
templateId: row.template_id,
|
|
463
|
+
templateVersion: row.template_version,
|
|
464
|
+
currentTemplateVersion: template?.version ?? null,
|
|
465
|
+
templateStatus: !row.template_id
|
|
466
|
+
? null
|
|
467
|
+
: template && template.version > Number(row.template_version)
|
|
468
|
+
? "update_available"
|
|
469
|
+
: contentHash({
|
|
470
|
+
name: row.name,
|
|
471
|
+
description: row.description,
|
|
472
|
+
instructions: row.instructions,
|
|
473
|
+
extraFrontmatter: jsonObject(row.extra_frontmatter),
|
|
474
|
+
references: snapshotFiles(row.content_references),
|
|
475
|
+
}) === row.template_hash
|
|
476
|
+
? "current"
|
|
477
|
+
: "modified",
|
|
478
|
+
id: row.id,
|
|
479
|
+
shortId: row.short_id,
|
|
480
|
+
name: row.name,
|
|
481
|
+
description: row.description,
|
|
482
|
+
referenceCount: Number(row.reference_count),
|
|
483
|
+
accessCount: Number(row.access_count),
|
|
484
|
+
adminCount: Number(row.admin_count),
|
|
485
|
+
createdAt: iso(row.created_at),
|
|
486
|
+
updatedAt: iso(row.updated_at),
|
|
487
|
+
};
|
|
488
|
+
};
|
|
384
489
|
|
|
385
490
|
const skillSnapshotFiles = (skill: AiSkill): { path: string; content: string }[] => [
|
|
386
491
|
{
|
|
@@ -390,90 +495,111 @@ const skillSnapshotFiles = (skill: AiSkill): { path: string; content: string }[]
|
|
|
390
495
|
...skill.references.map((reference) => ({ path: `${skill.name}/${reference.path}`, content: reference.content })),
|
|
391
496
|
];
|
|
392
497
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
498
|
+
type SkillListOptions = { query?: string; enabledOnly?: boolean; limit?: number };
|
|
499
|
+
async function listSkillSummaries(
|
|
500
|
+
subject: AccessSubject | null,
|
|
501
|
+
options: SkillListOptions & {
|
|
502
|
+
project?: { id: string; linked: boolean };
|
|
503
|
+
manageOnly?: boolean;
|
|
504
|
+
offset?: number;
|
|
505
|
+
} = {},
|
|
506
|
+
): Promise<AiSkillSummary[]> {
|
|
507
|
+
const userId = subject?.type === "user" ? subject.userId : null;
|
|
508
|
+
const query = (options.query ?? "").trim().slice(0, 200);
|
|
509
|
+
const limit = Math.max(1, Math.min(201, options.limit ?? 200));
|
|
510
|
+
const rows = await withAiSkillSearch(query, async (bm25) => {
|
|
511
|
+
const search = aiSkillSearchSql(query, bm25);
|
|
512
|
+
return sql<SkillSummaryRow[]>`
|
|
513
|
+
SELECT skill.id, skill.short_id, skill.name, skill.description, skill.revision, skill.created_at, skill.updated_at,
|
|
514
|
+
access.permission,
|
|
515
|
+
count(DISTINCT reference.path)::int AS reference_count,
|
|
516
|
+
(disabled.user_id IS NULL) AS enabled
|
|
517
|
+
FROM ai.skills skill
|
|
518
|
+
JOIN LATERAL (${effectivePermission(sql`skill.id`, subject)}) access ON true
|
|
519
|
+
LEFT JOIN ai.skill_references reference ON reference.skill_id = skill.id
|
|
520
|
+
LEFT JOIN ai.skill_user_disabled disabled
|
|
521
|
+
ON disabled.skill_id = skill.id AND disabled.user_id = ${userId}::uuid
|
|
522
|
+
WHERE access.permission <> 'none'
|
|
523
|
+
AND (${!options.manageOnly} OR access.permission='admin')
|
|
524
|
+
AND ${options.project ? sql`EXISTS(SELECT 1 FROM ai.project_skills link WHERE link.skill_id=skill.id AND link.project_id=${options.project.id}::uuid) = ${options.project.linked}` : sql`true`}
|
|
525
|
+
AND (${!options.enabledOnly} OR disabled.user_id IS NULL) AND ${search.matches}
|
|
526
|
+
GROUP BY skill.id, disabled.user_id, access.permission
|
|
527
|
+
ORDER BY ${search.rank} DESC, ${search.textRank} DESC, skill.name, skill.id
|
|
528
|
+
LIMIT ${limit} OFFSET ${options.offset ?? 0}
|
|
529
|
+
`;
|
|
530
|
+
});
|
|
531
|
+
return rows.map((row) => ({
|
|
532
|
+
id: row.id,
|
|
533
|
+
shortId: row.short_id,
|
|
534
|
+
name: row.name,
|
|
535
|
+
description: row.description,
|
|
536
|
+
permission: row.permission,
|
|
537
|
+
revision: Number(row.revision),
|
|
538
|
+
referenceCount: Number(row.reference_count),
|
|
539
|
+
enabled: row.enabled,
|
|
540
|
+
createdAt: iso(row.created_at),
|
|
541
|
+
updatedAt: iso(row.updated_at),
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
412
544
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
(${JSON.stringify(fields.extraFrontmatter)}::text)::jsonb
|
|
425
|
-
)
|
|
426
|
-
RETURNING *
|
|
427
|
-
`,
|
|
428
|
-
);
|
|
429
|
-
row = rows[0]!;
|
|
430
|
-
} else {
|
|
431
|
-
const references = await listReferences(row.id, tx);
|
|
432
|
-
const changed =
|
|
433
|
-
row.name !== fields.name ||
|
|
434
|
-
row.description !== fields.description ||
|
|
435
|
-
row.instructions !== fields.instructions ||
|
|
436
|
-
JSON.stringify(jsonObject(row.extra_frontmatter)) !== JSON.stringify(fields.extraFrontmatter) ||
|
|
437
|
-
!sameReferences(references, fields.references);
|
|
438
|
-
if (changed) {
|
|
439
|
-
[row] = await tx<SkillRow[]>`
|
|
440
|
-
UPDATE ai.skills
|
|
441
|
-
SET name = ${fields.name}, description = ${fields.description}, instructions = ${fields.instructions},
|
|
442
|
-
extra_frontmatter = (${JSON.stringify(fields.extraFrontmatter)}::text)::jsonb,
|
|
443
|
-
managed_key = NULL, revision = revision + 1, updated_at = now()
|
|
444
|
-
WHERE id = ${row.id}::uuid
|
|
445
|
-
RETURNING *
|
|
446
|
-
`;
|
|
447
|
-
} else {
|
|
448
|
-
[row] = await tx<SkillRow[]>`
|
|
449
|
-
UPDATE ai.skills SET managed_key = NULL WHERE id = ${row.id}::uuid RETURNING *
|
|
450
|
-
`;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
545
|
+
async function linkedSkillProjects(skillId: string, subject: AccessSubject | null) {
|
|
546
|
+
const links = await sql<
|
|
547
|
+
{ projectId: string }[]
|
|
548
|
+
>`SELECT project_id AS "projectId" FROM ai.project_skills WHERE skill_id=${skillId}::uuid ORDER BY project_id`;
|
|
549
|
+
const result: Array<{ projectId: string; shortId: string | null; name: string | null }> = [];
|
|
550
|
+
for (const link of links) {
|
|
551
|
+
const project = await aiProjects.get(link.projectId, subject);
|
|
552
|
+
result.push({ ...link, shortId: project?.shortId ?? null, name: project?.name ?? null });
|
|
553
|
+
}
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
453
556
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
557
|
+
export const aiSkills = {
|
|
558
|
+
async seedOnce(template: AiSkillTemplate): Promise<void> {
|
|
559
|
+
const { fields, hash } = validateTemplate(template);
|
|
560
|
+
await sql.begin(async (tx) => {
|
|
561
|
+
const [claimed] = await tx`INSERT INTO ai.skill_seeds (key, catalog_version)
|
|
562
|
+
VALUES (${template.key}, ${template.version}) ON CONFLICT (key) DO NOTHING RETURNING key`;
|
|
563
|
+
// Seed lock always precedes the Skill lock, including administrator adoption/reset.
|
|
564
|
+
const [seed] = await tx<{ skill_id: string | null; catalog_version: number }[]>`
|
|
565
|
+
SELECT skill_id, catalog_version FROM ai.skill_seeds WHERE key = ${template.key} FOR UPDATE`;
|
|
566
|
+
if (!seed || seed.catalog_version > template.version) return;
|
|
567
|
+
await tx`UPDATE ai.skill_seeds SET catalog_version = ${template.version} WHERE key = ${template.key}`;
|
|
568
|
+
if (seed.skill_id) {
|
|
569
|
+
const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${seed.skill_id}::uuid FOR UPDATE`;
|
|
570
|
+
if (!row || row.template_id !== template.key || Number(row.template_version) >= template.version) return;
|
|
571
|
+
if ((await rowContentHash(row, tx)) !== row.template_hash) return;
|
|
572
|
+
// A user may have claimed the new name. Keep the existing Skill rather than fail startup.
|
|
573
|
+
const [collision] = await tx`SELECT id FROM ai.skills WHERE name = ${fields.name} AND id <> ${row.id}::uuid`;
|
|
574
|
+
if (!collision) {
|
|
575
|
+
try {
|
|
576
|
+
await tx.savepoint((attempt) => replaceTemplate(row, template, attempt));
|
|
577
|
+
} catch (error) {
|
|
578
|
+
// A concurrent normal create may claim the name after our read.
|
|
579
|
+
if (!(typeof error === "object" && error !== null && "constraint" in error && error.constraint === "idx_ai_skills_name"))
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
462
582
|
}
|
|
583
|
+
return;
|
|
463
584
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
585
|
+
// Old seed markers and name collisions never authorize adoption or replacement.
|
|
586
|
+
if (!claimed) return;
|
|
587
|
+
const [collision] = await tx`SELECT id FROM ai.skills WHERE name = ${fields.name}`;
|
|
588
|
+
if (collision) return;
|
|
589
|
+
const [row] = await withAiShortIdForDb(
|
|
590
|
+
tx,
|
|
591
|
+
"idx_ai_skills_short_id",
|
|
592
|
+
(attempt, shortId) => attempt<SkillRow[]>`
|
|
593
|
+
INSERT INTO ai.skills (short_id, name, description, instructions, extra_frontmatter, template_id, template_version, template_hash)
|
|
594
|
+
VALUES (${shortId}, ${fields.name}, ${fields.description}, ${fields.instructions},
|
|
595
|
+
(${JSON.stringify(fields.extraFrontmatter)}::text)::jsonb, ${template.key}, ${template.version}, ${hash}) ON CONFLICT (name) DO NOTHING RETURNING *`,
|
|
596
|
+
);
|
|
597
|
+
if (!row) return;
|
|
598
|
+
for (const ref of fields.references)
|
|
599
|
+
await tx`INSERT INTO ai.skill_references (skill_id, path, content)
|
|
600
|
+
VALUES (${row!.id}::uuid, ${ref.path}, ${ref.content})`;
|
|
601
|
+
await tx`UPDATE ai.skill_seeds SET skill_id = ${row!.id}::uuid WHERE key = ${template.key}`;
|
|
602
|
+
await createSkillAccess(row!.id, { principal: { type: "authenticated" }, permission: "read" }, tx);
|
|
477
603
|
});
|
|
478
604
|
},
|
|
479
605
|
|
|
@@ -510,39 +636,48 @@ export const aiSkills = {
|
|
|
510
636
|
});
|
|
511
637
|
},
|
|
512
638
|
|
|
513
|
-
async list(subject: AccessSubject | null): Promise<AiSkillSummary[]> {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
639
|
+
async list(subject: AccessSubject | null, options: SkillListOptions = {}): Promise<AiSkillSummary[]> {
|
|
640
|
+
return listSkillSummaries(subject, options);
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
async projectSkills(
|
|
644
|
+
projectId: string,
|
|
645
|
+
subject: AccessSubject | null,
|
|
646
|
+
options: { query?: string; page?: number; available?: boolean } = {},
|
|
647
|
+
) {
|
|
648
|
+
if (!(await aiProjects.get(projectId, subject, options.available ? "admin" : "read"))) return null;
|
|
649
|
+
const page = Math.max(1, Math.min(100000, Math.trunc(options.page ?? 1)));
|
|
650
|
+
const skills = await listSkillSummaries(subject, {
|
|
651
|
+
query: options.query,
|
|
652
|
+
limit: 31,
|
|
653
|
+
offset: (page - 1) * 30,
|
|
654
|
+
project: { id: projectId, linked: !options.available },
|
|
655
|
+
manageOnly: options.available,
|
|
656
|
+
});
|
|
657
|
+
return { items: skills.slice(0, 30), hasNext: skills.length > 30, page };
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
async linkProject(skillId: string, projectId: string, linked: boolean, subject: AccessSubject | null): Promise<boolean> {
|
|
661
|
+
if (!(await aiProjects.get(projectId, subject, "admin"))) return false;
|
|
662
|
+
return sql.begin(async (tx) => {
|
|
663
|
+
const [row] = await tx`SELECT id FROM ai.skills WHERE id=${skillId}::uuid FOR UPDATE`;
|
|
664
|
+
if (!row || !hasPermission(await permissionFor(skillId, subject, tx), "admin")) return false;
|
|
665
|
+
if (linked)
|
|
666
|
+
await tx`INSERT INTO ai.project_skills(project_id,skill_id) VALUES(${projectId}::uuid,${skillId}::uuid) ON CONFLICT DO NOTHING`;
|
|
667
|
+
else await tx`DELETE FROM ai.project_skills WHERE project_id=${projectId}::uuid AND skill_id=${skillId}::uuid`;
|
|
668
|
+
return true;
|
|
669
|
+
});
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
async linkedProjects(skillId: string, subject: AccessSubject | null) {
|
|
673
|
+
if (!(await this.get(skillId, subject, "admin"))) return null;
|
|
674
|
+
return linkedSkillProjects(skillId, subject);
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
async search(subject: AccessSubject | null, query: string, limit = 30): Promise<{ skills: AiSkillSummary[]; more: boolean }> {
|
|
678
|
+
const size = Math.max(1, Math.min(200, limit));
|
|
679
|
+
const skills = await aiSkills.list(subject, { query, enabledOnly: true, limit: size + 1 });
|
|
680
|
+
return { skills: skills.slice(0, size), more: skills.length > size };
|
|
546
681
|
},
|
|
547
682
|
|
|
548
683
|
async get(skillId: string, subject: AccessSubject | null, required: AiSkillPermission = "read"): Promise<AiSkill | null> {
|
|
@@ -607,39 +742,95 @@ export const aiSkills = {
|
|
|
607
742
|
},
|
|
608
743
|
|
|
609
744
|
async listAccess(skillId: string, subject: AccessSubject | null): Promise<AiSkillAccess[] | null> {
|
|
610
|
-
|
|
611
|
-
|
|
745
|
+
return sql.begin(async (tx) => {
|
|
746
|
+
const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id=${skillId}::uuid FOR UPDATE`;
|
|
747
|
+
if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "admin")) return null;
|
|
748
|
+
return listSkillAccess(skillId, tx);
|
|
749
|
+
});
|
|
612
750
|
},
|
|
613
751
|
|
|
614
752
|
async grantAccess(
|
|
615
753
|
skillId: string,
|
|
616
754
|
subject: AccessSubject | null,
|
|
617
755
|
input: { principal: Principal; permission: AiSkillPermission },
|
|
756
|
+
expectedAccessRevision?: string,
|
|
618
757
|
): Promise<AiSkillAccess | null> {
|
|
619
758
|
return sql.begin(async (tx) => {
|
|
620
759
|
const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
|
|
621
760
|
if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "admin")) return null;
|
|
761
|
+
await checkAccessRevision(skillId, expectedAccessRevision, tx);
|
|
622
762
|
return createSkillAccess(skillId, input, tx);
|
|
623
763
|
});
|
|
624
764
|
},
|
|
625
765
|
|
|
626
|
-
async updateAccess(
|
|
766
|
+
async updateAccess(
|
|
767
|
+
skillId: string,
|
|
768
|
+
accessId: string,
|
|
769
|
+
subject: AccessSubject | null,
|
|
770
|
+
permission: AiSkillPermission,
|
|
771
|
+
expectedAccessRevision?: string,
|
|
772
|
+
): Promise<boolean> {
|
|
627
773
|
return sql.begin(async (tx) => {
|
|
628
774
|
const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
775
|
+
if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "admin")) return false;
|
|
776
|
+
await checkAccessRevision(skillId, expectedAccessRevision, tx);
|
|
777
|
+
return updateSkillAccess(skillId, accessId, permission, tx);
|
|
632
778
|
});
|
|
633
779
|
},
|
|
634
780
|
|
|
635
|
-
async revokeAccess(skillId: string, accessId: string, subject: AccessSubject | null): Promise<boolean> {
|
|
781
|
+
async revokeAccess(skillId: string, accessId: string, subject: AccessSubject | null, expectedAccessRevision?: string): Promise<boolean> {
|
|
636
782
|
return sql.begin(async (tx) => {
|
|
637
783
|
const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
|
|
638
|
-
|
|
784
|
+
if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "admin")) return false;
|
|
785
|
+
await checkAccessRevision(skillId, expectedAccessRevision, tx);
|
|
786
|
+
return revokeSkillAccess(skillId, accessId, tx);
|
|
639
787
|
});
|
|
640
788
|
},
|
|
641
789
|
|
|
642
790
|
admin: {
|
|
791
|
+
linkedProjects: linkedSkillProjects,
|
|
792
|
+
// Platform-owned recovery API: callers must enforce platform-admin authorization.
|
|
793
|
+
async applyTemplate(
|
|
794
|
+
skillId: string,
|
|
795
|
+
input: {
|
|
796
|
+
templateId: string;
|
|
797
|
+
templateVersion: number;
|
|
798
|
+
expectedRevision: number;
|
|
799
|
+
mode: "associate" | "reset";
|
|
800
|
+
},
|
|
801
|
+
): Promise<boolean> {
|
|
802
|
+
const template = getBuiltinAiSkillTemplates().find((entry) => entry.key === input.templateId);
|
|
803
|
+
if (!template) throw new AiSkillInputError("Unknown Skill template.");
|
|
804
|
+
if (template.version !== input.templateVersion) throw new AiSkillRevisionConflictError();
|
|
805
|
+
const { hash } = validateTemplate(template);
|
|
806
|
+
return sql.begin(async (tx) => {
|
|
807
|
+
await tx`INSERT INTO ai.skill_seeds (key, catalog_version) VALUES (${template.key}, ${template.version})
|
|
808
|
+
ON CONFLICT (key) DO NOTHING`;
|
|
809
|
+
const [seed] = await tx<{ skill_id: string | null; catalog_version: number }[]>`
|
|
810
|
+
SELECT skill_id, catalog_version FROM ai.skill_seeds WHERE key = ${template.key} FOR UPDATE`;
|
|
811
|
+
const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
|
|
812
|
+
if (!row) return false;
|
|
813
|
+
if (
|
|
814
|
+
Number(row.revision) !== input.expectedRevision ||
|
|
815
|
+
seed!.catalog_version > template.version ||
|
|
816
|
+
Number(row.template_version) > template.version
|
|
817
|
+
)
|
|
818
|
+
throw new AiSkillRevisionConflictError();
|
|
819
|
+
if (seed!.skill_id && seed!.skill_id !== row.id)
|
|
820
|
+
throw new AiSkillInputError("This template is already linked, possibly to a deleted Skill.");
|
|
821
|
+
if (input.mode === "associate") {
|
|
822
|
+
if (row.template_id) throw new AiSkillInputError("This Skill already has a template.");
|
|
823
|
+
await tx`UPDATE ai.skills SET template_id = ${template.key}, template_version = ${template.version},
|
|
824
|
+
template_hash = ${hash}, managed_key = NULL, revision = revision + 1, updated_at = now() WHERE id = ${row.id}::uuid`;
|
|
825
|
+
} else {
|
|
826
|
+
if (row.template_id !== template.key) throw new AiSkillInputError("Associate this Skill with its template first.");
|
|
827
|
+
await replaceTemplate(row, template, tx);
|
|
828
|
+
}
|
|
829
|
+
await tx`UPDATE ai.skill_seeds SET skill_id = ${row.id}::uuid, catalog_version = ${template.version} WHERE key = ${template.key}`;
|
|
830
|
+
return true;
|
|
831
|
+
});
|
|
832
|
+
},
|
|
833
|
+
|
|
643
834
|
async list(params: {
|
|
644
835
|
search?: string;
|
|
645
836
|
page?: number;
|
|
@@ -659,6 +850,8 @@ export const aiSkills = {
|
|
|
659
850
|
`;
|
|
660
851
|
const rows = await sql<AdminSkillRow[]>`
|
|
661
852
|
SELECT skill.*,
|
|
853
|
+
(SELECT COALESCE(jsonb_agg(jsonb_build_object('path', content.path, 'content', content.content)), '[]'::jsonb)
|
|
854
|
+
FROM ai.skill_references content WHERE content.skill_id = skill.id) AS content_references,
|
|
662
855
|
count(DISTINCT reference.path)::int AS reference_count,
|
|
663
856
|
count(DISTINCT skill_access.access_id)::int AS access_count,
|
|
664
857
|
count(DISTINCT skill_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
|
|
@@ -704,6 +897,8 @@ export const aiSkills = {
|
|
|
704
897
|
async getByShortId(shortId: string): Promise<AiSkillAdminListItem | null> {
|
|
705
898
|
const rows = await sql<AdminSkillRow[]>`
|
|
706
899
|
SELECT skill.*,
|
|
900
|
+
(SELECT COALESCE(jsonb_agg(jsonb_build_object('path', content.path, 'content', content.content)), '[]'::jsonb)
|
|
901
|
+
FROM ai.skill_references content WHERE content.skill_id = skill.id) AS content_references,
|
|
707
902
|
count(DISTINCT reference.path)::int AS reference_count,
|
|
708
903
|
count(DISTINCT skill_access.access_id)::int AS access_count,
|
|
709
904
|
count(DISTINCT skill_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
|
|
@@ -885,17 +1080,15 @@ export const aiSkills = {
|
|
|
885
1080
|
},
|
|
886
1081
|
|
|
887
1082
|
async listTurnFiles(turnId: string, subject: AccessSubject | null): Promise<AiSkillFileToolStat[]> {
|
|
888
|
-
const match = accessMatch(subject);
|
|
889
1083
|
const userId = subject?.type === "user" ? subject.userId : null;
|
|
890
1084
|
const rows = await sql<SnapshotRow[]>`
|
|
891
1085
|
SELECT DISTINCT snapshot.skill_name, snapshot.description, snapshot.revision, snapshot.instructions,
|
|
892
1086
|
snapshot.files, snapshot.loaded_at
|
|
893
1087
|
FROM ai.turn_skill_snapshots snapshot
|
|
894
|
-
JOIN
|
|
895
|
-
JOIN auth.access access ON access.id = skill_access.access_id
|
|
1088
|
+
JOIN LATERAL (${effectivePermission(sql`snapshot.skill_id`, subject)}) access ON true
|
|
896
1089
|
LEFT JOIN ai.skill_user_disabled disabled
|
|
897
1090
|
ON disabled.skill_id = snapshot.skill_id AND disabled.user_id = ${userId}::uuid
|
|
898
|
-
WHERE snapshot.turn_id = ${turnId}::uuid AND access.permission <> 'none'
|
|
1091
|
+
WHERE snapshot.turn_id = ${turnId}::uuid AND access.permission <> 'none'
|
|
899
1092
|
AND disabled.user_id IS NULL
|
|
900
1093
|
ORDER BY snapshot.skill_name
|
|
901
1094
|
`;
|
|
@@ -914,21 +1107,23 @@ export const aiSkills = {
|
|
|
914
1107
|
const stat = files.find((file) => file.path === path);
|
|
915
1108
|
if (!stat) return null;
|
|
916
1109
|
const [skillName] = path.split("/", 1);
|
|
917
|
-
const match = accessMatch(subject);
|
|
918
1110
|
const userId = subject?.type === "user" ? subject.userId : null;
|
|
919
1111
|
const [row] = await sql<SnapshotRow[]>`
|
|
920
1112
|
SELECT DISTINCT snapshot.skill_name, snapshot.description, snapshot.revision, snapshot.instructions,
|
|
921
1113
|
snapshot.files, snapshot.loaded_at
|
|
922
1114
|
FROM ai.turn_skill_snapshots snapshot
|
|
923
|
-
JOIN
|
|
924
|
-
JOIN auth.access access ON access.id = skill_access.access_id
|
|
1115
|
+
JOIN LATERAL (${effectivePermission(sql`snapshot.skill_id`, subject)}) access ON true
|
|
925
1116
|
LEFT JOIN ai.skill_user_disabled disabled
|
|
926
1117
|
ON disabled.skill_id = snapshot.skill_id AND disabled.user_id = ${userId}::uuid
|
|
927
1118
|
WHERE snapshot.turn_id = ${turnId}::uuid AND snapshot.skill_name = ${skillName}
|
|
928
|
-
AND access.permission <> 'none' AND
|
|
1119
|
+
AND access.permission <> 'none' AND disabled.user_id IS NULL
|
|
929
1120
|
LIMIT 1
|
|
930
1121
|
`;
|
|
931
1122
|
const file = row ? snapshotFiles(row.files).find((entry) => entry.path === path) : null;
|
|
932
1123
|
return file ? { ...stat, bytes: new TextEncoder().encode(file.content) } : null;
|
|
933
1124
|
},
|
|
934
1125
|
};
|
|
1126
|
+
|
|
1127
|
+
export const seedCloudAiSkills = async (): Promise<void> => {
|
|
1128
|
+
for (const seed of getBuiltinAiSkillTemplates()) await aiSkills.seedOnce(seed);
|
|
1129
|
+
};
|