@k2b/cloud 0.45.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/src/_internal/app-queries.ts +73 -0
- package/src/_internal/capabilities.ts +137 -9
- package/src/_internal/define-app.ts +2 -0
- package/src/_internal/registry-validation.ts +17 -4
- package/src/_internal/registry.ts +32 -4
- package/src/_internal/resource-principals.ts +36 -0
- package/src/ai/capabilities.ts +4 -2
- package/src/ai/chat/map-messages.ts +270 -0
- package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
- package/src/ai/chat/map-notebook.tsx +301 -0
- package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
- package/src/ai/chat/map-widget.css +107 -0
- package/src/ai/chat/map-widget.tsx +1279 -0
- package/src/ai/chat/message-utils.ts +1 -0
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -1
- package/src/ai/chat/turn-view.tsx +17 -1
- package/src/ai/default-tools.ts +3 -0
- package/src/ai/executor.ts +13 -1
- package/src/ai/map-contracts.ts +209 -0
- package/src/ai/map-execution.ts +14 -0
- package/src/ai/map-notebook-save.ts +276 -0
- package/src/ai/map-notebook.ts +32 -0
- package/src/ai/map-opening-hours.ts +40 -0
- package/src/ai/map-routes.ts +84 -0
- package/src/ai/map-service.ts +288 -0
- package/src/ai/map-store.ts +137 -0
- package/src/ai/map-tool.ts +73 -0
- package/src/ai/migrate.ts +3 -0
- package/src/ai/protocol.ts +3 -1
- package/src/ai/routes.ts +2 -0
- package/src/ai/store.ts +19 -6
- package/src/ai/tools.ts +14 -0
- package/src/ai/types.ts +3 -0
- package/src/api/capabilities.ts +90 -12
- package/src/api/mcp.ts +4 -2
- package/src/browser/notifications.ts +25 -8
- package/src/capabilities/discussion-providers.ts +99 -0
- package/src/capabilities/executions.ts +2 -2
- package/src/capabilities/migrate.ts +10 -1
- package/src/capabilities/resource-readers.ts +58 -0
- package/src/capabilities/server-invoke.ts +23 -13
- package/src/capabilities/server.ts +17 -6
- package/src/capabilities/testing.ts +13 -0
- package/src/contracts/capabilities.ts +137 -4
- package/src/contracts/discussions.ts +91 -0
- package/src/contracts/geo-document.ts +60 -0
- package/src/contracts/geo-snapshot.ts +86 -0
- package/src/contracts/geo.ts +14 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/resource-types.ts +21 -0
- package/src/geo/AddressAutocomplete.tsx +213 -0
- package/src/geo/messages.ts +29 -0
- package/src/geo/navigation.ts +25 -0
- package/src/geo/ui.ts +2 -0
- package/src/services/accounts/app.ts +28 -6
- package/src/services/accounts/entities.ts +11 -4
- package/src/services/accounts/groups.ts +10 -4
- package/src/services/accounts/local-groups.ts +87 -38
- package/src/services/accounts/machine-group-management.ts +94 -0
- package/src/services/geo/cache.ts +2 -1
- package/src/services/geo/geoapify.ts +3 -0
- package/src/services/identity/invocation-token.ts +110 -0
- package/src/services/pdf/geo-snapshot.ts +107 -0
- package/src/services/pdf/index.ts +2 -0
- package/src/ssr/AdminArea.tsx +2 -2
- package/src/ssr/AdminLayout.tsx +1 -1
- package/src/ssr/AdminSidebar.tsx +1 -1
- package/src/styles/global.css +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"./server": "./src/server/index.ts",
|
|
31
31
|
"./server/*": "./src/server/*",
|
|
32
32
|
"./geo/ui": "./src/geo/ui.ts",
|
|
33
|
+
"./geo/document": "./src/contracts/geo-document.ts",
|
|
33
34
|
"./browser": "./src/server/api-client.ts",
|
|
34
35
|
"./browser/session": "./src/browser/session.ts",
|
|
35
36
|
"./browser/shared-text": "./src/browser/shared-text.ts",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"./shared": "./src/shared/index.ts",
|
|
67
68
|
"./shared-text": "./src/shared-text/index.ts",
|
|
68
69
|
"./services": "./src/services/index.ts",
|
|
70
|
+
"./services/pdf": "./src/services/pdf/index.ts",
|
|
69
71
|
"./services/image-metadata": "./src/services/image-metadata.ts",
|
|
70
72
|
"./services/document-extraction": "./src/services/document-extraction.ts",
|
|
71
73
|
"./services/outbox": "./src/services/outbox.ts",
|
|
@@ -94,6 +96,7 @@
|
|
|
94
96
|
"./config": "./src/config/index.ts",
|
|
95
97
|
"./config/*": "./src/config/*",
|
|
96
98
|
"./contracts": "./src/contracts/index.ts",
|
|
99
|
+
"./contracts/geo-snapshot": "./src/contracts/geo-snapshot.ts",
|
|
97
100
|
"./contracts/notifications": "./src/contracts/notification-types.ts",
|
|
98
101
|
"./contracts/*": "./src/contracts/*",
|
|
99
102
|
"./events/testing": "./src/events/testing.ts",
|
|
@@ -125,7 +128,7 @@
|
|
|
125
128
|
"@tailwindcss/typography": "0.5.20",
|
|
126
129
|
"@k2b/nessi": "0.18.0",
|
|
127
130
|
"@k2b/ssr": "0.20.1",
|
|
128
|
-
"@k2b/ui": "0.
|
|
131
|
+
"@k2b/ui": "0.36.0",
|
|
129
132
|
"@k2b/stdlib": "0.30.0",
|
|
130
133
|
"@k2b/sync": "7.0.1",
|
|
131
134
|
"@nats-io/jetstream": "3.4.0",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { CAPABILITY_MAX_REQUEST_BYTES } from "../contracts/capabilities";
|
|
4
|
+
import { resolveLocale } from "../server/locale";
|
|
5
|
+
import { verifyAppQueryInvocation } from "../services/identity/invocation-token";
|
|
6
|
+
import { get } from "../services/settings";
|
|
7
|
+
import { readBoundedJson } from "./bounded-json";
|
|
8
|
+
import { type CompiledCapabilities, invokeCompiledCapability, serializeCapabilityProviderResult } from "./capabilities";
|
|
9
|
+
import { withCapabilityRequestTimeout } from "./capability-timeout";
|
|
10
|
+
|
|
11
|
+
/** Framework-only route: never installs an app actor in the ordinary auth middleware. */
|
|
12
|
+
export const createAppQueryRoutes = (compiled: CompiledCapabilities) =>
|
|
13
|
+
new Hono().post("/:capabilityId", (c) =>
|
|
14
|
+
withCapabilityRequestTimeout(c, "query", async () => {
|
|
15
|
+
const operation = compiled.queries.get(c.req.param("capabilityId"));
|
|
16
|
+
const deny = () => c.json({ code: "FORBIDDEN", message: "A permitted app query invocation is required" }, 403);
|
|
17
|
+
if (operation?.manifest.appOnly !== true || operation.manifest.stream) return deny();
|
|
18
|
+
const parsed = await readBoundedJson(c.req.raw, CAPABILITY_MAX_REQUEST_BYTES);
|
|
19
|
+
if (
|
|
20
|
+
!parsed.ok ||
|
|
21
|
+
!parsed.data ||
|
|
22
|
+
typeof parsed.data !== "object" ||
|
|
23
|
+
Array.isArray(parsed.data) ||
|
|
24
|
+
Object.keys(parsed.data).length !== 1 ||
|
|
25
|
+
!("input" in parsed.data)
|
|
26
|
+
)
|
|
27
|
+
return c.json({ code: "VALIDATION_FAILED", message: "Expected a bounded input object" }, 400);
|
|
28
|
+
const token = c.req.header("authorization")?.match(/^Bearer (\S+)$/)?.[1];
|
|
29
|
+
if (!token) return deny();
|
|
30
|
+
let claims: Awaited<ReturnType<typeof verifyAppQueryInvocation>>;
|
|
31
|
+
try {
|
|
32
|
+
claims = await verifyAppQueryInvocation(token, {
|
|
33
|
+
appId: compiled.manifest.appId,
|
|
34
|
+
capabilityId: operation.manifest.localId,
|
|
35
|
+
schemaHash: operation.manifest.schemaHash,
|
|
36
|
+
input: parsed.data.input,
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
return c.json({ code: "APP_UNAVAILABLE", message: "Invocation verification unavailable" }, 503);
|
|
40
|
+
}
|
|
41
|
+
if (!claims) return deny();
|
|
42
|
+
// Revoking/expiring the workload credential also invalidates already-issued query tokens.
|
|
43
|
+
const [active] = await sql<{ id: string }[]>`
|
|
44
|
+
SELECT c.id FROM auth.service_account_credentials c JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
45
|
+
WHERE c.id = ${claims.credential_id}::uuid AND sa.id = ${claims.service_account_id}::uuid
|
|
46
|
+
AND c.kind = 'api_token' AND c.status = 'active' AND (c.expires_at IS NULL OR c.expires_at > now())
|
|
47
|
+
AND 'identity:invoke' = ANY(c.scopes) AND sa.status = 'active' AND sa.kind = 'resource_bound'
|
|
48
|
+
AND sa.delegated_user_id IS NULL AND sa.app_id = ${claims.sub} AND sa.resource_type = 'cloud.app' AND sa.resource_id = ${claims.sub}
|
|
49
|
+
`;
|
|
50
|
+
if (!active) return deny();
|
|
51
|
+
const result = await invokeCompiledCapability({
|
|
52
|
+
compiled,
|
|
53
|
+
kind: "query",
|
|
54
|
+
localId: operation.manifest.localId,
|
|
55
|
+
input: parsed.data.input,
|
|
56
|
+
expectedSchemaHash: claims.schema_hash,
|
|
57
|
+
context: {
|
|
58
|
+
actor: { kind: "app", appId: claims.sub },
|
|
59
|
+
accessSubject: { type: "none" },
|
|
60
|
+
user: null,
|
|
61
|
+
locale: resolveLocale(c.req.raw.headers, await get<string>("app.locale")),
|
|
62
|
+
requestId: claims.request_id,
|
|
63
|
+
origin: "app",
|
|
64
|
+
signal: c.req.raw.signal,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const response = serializeCapabilityProviderResult(result);
|
|
68
|
+
return new Response(response.body, {
|
|
69
|
+
status: response.status,
|
|
70
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
71
|
+
});
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
@@ -30,12 +30,14 @@ import {
|
|
|
30
30
|
CapabilityActionPolicySchema,
|
|
31
31
|
type CapabilityActionReviewResult,
|
|
32
32
|
type CapabilityActionSentences,
|
|
33
|
+
type CapabilityAppExecutionContext,
|
|
33
34
|
type CapabilityCalendarSourceDeclaration,
|
|
34
35
|
CapabilityCalendarSourceManifestSchema,
|
|
35
36
|
type CapabilityCommandDefinition,
|
|
36
37
|
type CapabilityCommandManifest,
|
|
37
38
|
CapabilityCommandManifestSchema,
|
|
38
39
|
type CapabilityDefinitions,
|
|
40
|
+
CapabilityDiscussionsManifestSchema,
|
|
39
41
|
type CapabilityError,
|
|
40
42
|
CapabilityErrorSchema,
|
|
41
43
|
type CapabilityExecutionContext,
|
|
@@ -51,16 +53,21 @@ import {
|
|
|
51
53
|
type CapabilityQueryDefinition,
|
|
52
54
|
type CapabilityQueryManifest,
|
|
53
55
|
type CapabilityResourceTypeDefinition,
|
|
56
|
+
type CapabilityResourceTypeModel,
|
|
54
57
|
type CapabilitySearchTagDefinition,
|
|
55
58
|
type CapabilityStreamDefinition,
|
|
56
59
|
type CapabilityUniversalSearchDefinition,
|
|
57
60
|
CloudResourceViewSchema,
|
|
58
61
|
capabilityActionPolicy,
|
|
59
62
|
capabilityResultSchema,
|
|
63
|
+
ResourceReadableDataSchema,
|
|
64
|
+
ResourceReadableInputSchema,
|
|
60
65
|
UniversalSearchDataSchema,
|
|
61
66
|
UniversalSearchInputSchema,
|
|
62
67
|
} from "../contracts/capabilities";
|
|
68
|
+
import { discussionsIssues } from "../contracts/discussions";
|
|
63
69
|
import { fileProviderIssues } from "../contracts/file-provider";
|
|
70
|
+
import { typeViewIssues } from "../contracts/resource-types";
|
|
64
71
|
import { canonicalLocale, localeFallbackChain, normalizeLocale } from "../shared/locale";
|
|
65
72
|
import { capabilityActionReceiptIssues, capabilityActionReviewIssues, capabilityQueryChecksActionOutcome } from "./capability-reviews";
|
|
66
73
|
import {
|
|
@@ -71,6 +78,10 @@ import {
|
|
|
71
78
|
parseCapabilitySentence,
|
|
72
79
|
resolveCapabilityActionSentences,
|
|
73
80
|
} from "./capability-sentences";
|
|
81
|
+
import { activeResourcePrincipals } from "./resource-principals";
|
|
82
|
+
|
|
83
|
+
/** App-only queries never enter a person catalog. */
|
|
84
|
+
export const isPersonVisibleCapabilityQuery = (query: CapabilityQueryManifest): boolean => query.appOnly !== true;
|
|
74
85
|
|
|
75
86
|
type JsonSchema = Record<string, unknown>;
|
|
76
87
|
const MAX_CAPABILITY_MANIFEST_BYTES = 256 * 1024;
|
|
@@ -96,6 +107,7 @@ const DECLARATION_FIELDS = {
|
|
|
96
107
|
commands: true,
|
|
97
108
|
fileProvider: true,
|
|
98
109
|
calendarSource: true,
|
|
110
|
+
discussions: true,
|
|
99
111
|
events: true,
|
|
100
112
|
subscribes: true,
|
|
101
113
|
} satisfies DefinedFields<CapabilityDefinitions>;
|
|
@@ -114,8 +126,11 @@ const TYPE_FIELDS = {
|
|
|
114
126
|
color: true,
|
|
115
127
|
reader: true,
|
|
116
128
|
pages: true,
|
|
129
|
+
container: true,
|
|
130
|
+
access: true,
|
|
117
131
|
} satisfies DefinedFields<CapabilityResourceTypeDefinition>;
|
|
118
132
|
const QUERY_FIELDS = {
|
|
133
|
+
appOnly: true,
|
|
119
134
|
title: true,
|
|
120
135
|
description: true,
|
|
121
136
|
input: true,
|
|
@@ -138,7 +153,7 @@ const ACTION_FIELDS = {
|
|
|
138
153
|
policy: true,
|
|
139
154
|
review: true,
|
|
140
155
|
run: true,
|
|
141
|
-
} satisfies DefinedFields<CapabilityActionDefinition
|
|
156
|
+
} satisfies DefinedFields<Omit<CapabilityActionDefinition, "appOnly">>;
|
|
142
157
|
const COMMAND_FIELDS = {
|
|
143
158
|
title: true,
|
|
144
159
|
description: true,
|
|
@@ -170,8 +185,14 @@ const assertDefinedStreamFields = (stream: CapabilityStreamDefinition | undefine
|
|
|
170
185
|
if (stream) assertDefinedFields(stream, stream.direction === "write" ? WRITE_STREAM_FIELDS : READ_STREAM_FIELDS, `${label} stream`);
|
|
171
186
|
};
|
|
172
187
|
|
|
188
|
+
type AppQueryDefinition = Omit<CapabilityQueryDefinition, "appOnly" | "run"> & {
|
|
189
|
+
appOnly: true;
|
|
190
|
+
run: (input: unknown, context: CapabilityAppExecutionContext) => Promise<CapabilityInvocationResult<unknown>>;
|
|
191
|
+
};
|
|
192
|
+
type QueryDefinition = CapabilityQueryDefinition | AppQueryDefinition;
|
|
193
|
+
|
|
173
194
|
export type CompiledCapabilityQuery = {
|
|
174
|
-
definition:
|
|
195
|
+
definition: QueryDefinition;
|
|
175
196
|
manifest: CapabilityQueryManifest;
|
|
176
197
|
resultSchema: z.ZodType;
|
|
177
198
|
};
|
|
@@ -621,7 +642,7 @@ const assertLocalId = (localId: string, label: string): void => {
|
|
|
621
642
|
if (!parsed.success) throw new Error(`${label} key "${localId}" is invalid: ${parsed.error.issues[0]?.message ?? "invalid id"}`);
|
|
622
643
|
};
|
|
623
644
|
|
|
624
|
-
const normalizeSearchTags = (definition:
|
|
645
|
+
const normalizeSearchTags = (definition: QueryDefinition, label: string) => {
|
|
625
646
|
if (!definition.universalSearch) return undefined;
|
|
626
647
|
assertDefinedFields(definition.universalSearch, UNIVERSAL_SEARCH_FIELDS, `${label} universalSearch`);
|
|
627
648
|
const seen = new Set<string>();
|
|
@@ -647,7 +668,7 @@ const normalizeSearchTags = (definition: CapabilityQueryDefinition, label: strin
|
|
|
647
668
|
return { tags, ...(definition.universalSearch.scopeTypes ? { scopeTypes: [...new Set(definition.universalSearch.scopeTypes)] } : {}) };
|
|
648
669
|
};
|
|
649
670
|
|
|
650
|
-
const compileOperationSchemas = (definition:
|
|
671
|
+
const compileOperationSchemas = (definition: QueryDefinition | CapabilityActionDefinition, label: string) => {
|
|
651
672
|
assertText(definition.title, `${label} title`, 120);
|
|
652
673
|
assertText(definition.description, `${label} description`, 1000);
|
|
653
674
|
const inputSchema = projectSchema(definition.input, `${label} input`, "input");
|
|
@@ -667,6 +688,7 @@ const compileOperationSchemas = (definition: CapabilityQueryDefinition | Capabil
|
|
|
667
688
|
schemaHash: capabilityHash({
|
|
668
689
|
inputSchema,
|
|
669
690
|
dataSchema,
|
|
691
|
+
...("appOnly" in definition && definition.appOnly !== undefined ? { appOnly: definition.appOnly } : {}),
|
|
670
692
|
...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
|
|
671
693
|
}),
|
|
672
694
|
};
|
|
@@ -688,6 +710,26 @@ export const compileCapabilities = (
|
|
|
688
710
|
}
|
|
689
711
|
if (definitions.fileProvider) assertDefinedFields(definitions.fileProvider, FILE_PROVIDER_FIELDS, "fileProvider");
|
|
690
712
|
|
|
713
|
+
if (definitions.discussions !== undefined) CapabilityDiscussionsManifestSchema.parse(definitions.discussions);
|
|
714
|
+
for (const [localId, type] of Object.entries(definitions.types ?? {})) {
|
|
715
|
+
if (type.access !== undefined) {
|
|
716
|
+
assertDefinedFields(type.access, { readable: true }, `Resource type ${localId} access`);
|
|
717
|
+
if (type.access.readable !== undefined && typeof type.access.readable !== "function")
|
|
718
|
+
throw new Error(`Resource type ${localId} access.readable must be a function`);
|
|
719
|
+
}
|
|
720
|
+
const visited = new Set([localId]);
|
|
721
|
+
let container = type.container;
|
|
722
|
+
while (container !== undefined) {
|
|
723
|
+
CapabilityLocalIdSchema.parse(container);
|
|
724
|
+
const parent = definitions.types?.[container];
|
|
725
|
+
if (!parent || !Object.hasOwn(definitions.types ?? {}, container))
|
|
726
|
+
throw new Error(`Resource type ${localId}: unknown container ${container}`);
|
|
727
|
+
if (visited.has(container)) throw new Error(`Resource type ${localId}: container cycle at ${container}`);
|
|
728
|
+
visited.add(container);
|
|
729
|
+
container = parent.container;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
691
733
|
const localIds = new Set<string>();
|
|
692
734
|
const registerLocalId = (localId: string, kind: string): void => {
|
|
693
735
|
assertLocalId(localId, kind);
|
|
@@ -722,11 +764,55 @@ export const compileCapabilities = (
|
|
|
722
764
|
});
|
|
723
765
|
const typeIds = new Set(types.map((type) => qualifiedId(appId, type.localId)));
|
|
724
766
|
|
|
767
|
+
for (const [localId, definition] of Object.entries(definitions.queries ?? {}))
|
|
768
|
+
if (Object.hasOwn(definition, "appOnly")) throw new Error(`Query ${localId}: appOnly is reserved for platform-generated reader checks`);
|
|
769
|
+
const generated: Record<string, AppQueryDefinition> = {};
|
|
770
|
+
for (const [localId, type] of Object.entries(definitions.types ?? {})) {
|
|
771
|
+
const readable = type.access?.readable;
|
|
772
|
+
if (!readable) continue;
|
|
773
|
+
const queryId = `${localId}.readable`;
|
|
774
|
+
if (Object.hasOwn(definitions.queries ?? {}, queryId)) throw new Error(`Generated Query ${queryId} collides with a declared Query`);
|
|
775
|
+
const container = type.container;
|
|
776
|
+
const input = container
|
|
777
|
+
? ResourceReadableInputSchema.extend({
|
|
778
|
+
container: ResourceReadableInputSchema.shape.container
|
|
779
|
+
.unwrap()
|
|
780
|
+
.extend({ type: z.literal(qualifiedId(appId, container)).describe("Declared container type.") })
|
|
781
|
+
.describe("Expected current container of the resource."),
|
|
782
|
+
})
|
|
783
|
+
: ResourceReadableInputSchema;
|
|
784
|
+
generated[queryId] = {
|
|
785
|
+
title: "Check resource readers",
|
|
786
|
+
description: `Check current principal readability of ${type.title} without granting content access.`,
|
|
787
|
+
input,
|
|
788
|
+
data: ResourceReadableDataSchema,
|
|
789
|
+
openWorld: false,
|
|
790
|
+
appOnly: true,
|
|
791
|
+
run: async (value: unknown, context) => {
|
|
792
|
+
if (!isAppCapabilityContext(context))
|
|
793
|
+
return { ok: false, error: { code: "FORBIDDEN", message: "App authority required", status: 403 } };
|
|
794
|
+
const parsed = input.parse(value);
|
|
795
|
+
const principals = await activeResourcePrincipals(parsed.principals);
|
|
796
|
+
const key = (principal: (typeof principals)[number]) => JSON.stringify(principal);
|
|
797
|
+
const requested = new Set(principals.map(key));
|
|
798
|
+
const result = ResourceReadableDataSchema.parse({ readable: await readable({ ...parsed, principals }, context) });
|
|
799
|
+
if (result.readable.some((principal) => !requested.has(key(principal))))
|
|
800
|
+
throw new Error(`Resource type ${localId} readable returned an unrequested principal`);
|
|
801
|
+
return { ok: true, data: { data: result } };
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
}
|
|
725
805
|
const queries = new Map<string, CompiledCapabilityQuery>();
|
|
726
|
-
for (const [localId, definition] of Object.entries(definitions.queries
|
|
806
|
+
for (const [localId, definition] of Object.entries({ ...definitions.queries, ...generated }).sort(([left], [right]) =>
|
|
807
|
+
left.localeCompare(right),
|
|
808
|
+
)) {
|
|
727
809
|
registerLocalId(localId, "Query");
|
|
728
810
|
const label = `Query ${localId}`;
|
|
729
811
|
assertDefinedFields(definition, QUERY_FIELDS, label);
|
|
812
|
+
if (definition.appOnly !== undefined) {
|
|
813
|
+
z.literal(true).parse(definition.appOnly);
|
|
814
|
+
if (definition.stream) throw new Error(`${label}: app-only queries cannot use streams`);
|
|
815
|
+
}
|
|
730
816
|
assertDefinedStreamFields(definition.stream, label);
|
|
731
817
|
if (
|
|
732
818
|
definition.stream &&
|
|
@@ -756,6 +842,7 @@ export const compileCapabilities = (
|
|
|
756
842
|
...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
|
|
757
843
|
openWorld: definition.openWorld,
|
|
758
844
|
universalSearch: normalizeSearchTags(definition, label),
|
|
845
|
+
...(definition.appOnly !== undefined ? { appOnly: definition.appOnly } : {}),
|
|
759
846
|
} satisfies CapabilityQueryManifest;
|
|
760
847
|
for (const type of manifest.universalSearch?.scopeTypes ?? []) {
|
|
761
848
|
if (!typeIds.has(qualifiedId(appId, type))) throw new Error(`${label} declares an unknown search scope type ${type}`);
|
|
@@ -770,6 +857,7 @@ export const compileCapabilities = (
|
|
|
770
857
|
if (!type.reader) continue;
|
|
771
858
|
const reader = queries.get(type.reader);
|
|
772
859
|
if (!reader) throw new Error(`Resource type ${type.localId} reader ${type.reader} must name an existing Query`);
|
|
860
|
+
if (reader.manifest.appOnly) throw new Error(`Resource type ${type.localId} reader cannot be app-only`);
|
|
773
861
|
assertCanonicalReaderInput(reader.manifest.inputSchema, `Resource type ${type.localId} reader ${type.reader} input`);
|
|
774
862
|
}
|
|
775
863
|
const actions = new Map<string, CompiledCapabilityAction>();
|
|
@@ -931,7 +1019,25 @@ export const compileCapabilities = (
|
|
|
931
1019
|
const resourcePages = Object.entries(definitions.types ?? {})
|
|
932
1020
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
933
1021
|
.flatMap(([type, definition]) => (definition.pages ?? []).map((path) => ({ type, path })));
|
|
1022
|
+
const resourceTypes: CapabilityResourceTypeModel[] = types.flatMap((type) => {
|
|
1023
|
+
const declaration = definitions.types?.[type.localId];
|
|
1024
|
+
const model: CapabilityResourceTypeModel = {
|
|
1025
|
+
type: type.localId,
|
|
1026
|
+
...(declaration?.container ? { container: declaration.container } : {}),
|
|
1027
|
+
...(declaration?.access?.readable ? { readable: true } : {}),
|
|
1028
|
+
};
|
|
1029
|
+
const reader = type.reader ? queries.get(type.reader) : undefined;
|
|
1030
|
+
if (
|
|
1031
|
+
reader &&
|
|
1032
|
+
typeViewIssues({ types, queries: [...queries.values()].map((query) => query.manifest), resourceTypes: [model] }, type.localId)
|
|
1033
|
+
.length === 0
|
|
1034
|
+
)
|
|
1035
|
+
model.view = true;
|
|
1036
|
+
return model.container || model.readable || model.view ? [model] : [];
|
|
1037
|
+
});
|
|
934
1038
|
const manifestBase = {
|
|
1039
|
+
...(resourceTypes.length ? { resourceTypes } : {}),
|
|
1040
|
+
...(definitions.discussions ? { discussions: definitions.discussions } : {}),
|
|
935
1041
|
...(resourcePages.length ? { resourcePages } : {}),
|
|
936
1042
|
protocolVersion: CAPABILITY_PROTOCOL_VERSION,
|
|
937
1043
|
appId,
|
|
@@ -952,6 +1058,9 @@ export const compileCapabilities = (
|
|
|
952
1058
|
...manifestBase,
|
|
953
1059
|
manifestHash: capabilityHash(manifestBase),
|
|
954
1060
|
});
|
|
1061
|
+
const discussionIssues = discussionsIssues(manifest);
|
|
1062
|
+
if (discussionIssues.length)
|
|
1063
|
+
throw new Error(`discussions: ${discussionIssues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
955
1064
|
const calendarIssues = calendarSourceIssues(manifest);
|
|
956
1065
|
if (calendarIssues.length)
|
|
957
1066
|
throw new Error(
|
|
@@ -1124,6 +1233,8 @@ export const resolveCapabilityManifestPresentation = (
|
|
|
1124
1233
|
}
|
|
1125
1234
|
: {}),
|
|
1126
1235
|
...(current.subscribes ? { subscribes: current.subscribes } : {}),
|
|
1236
|
+
...(current.resourceTypes ? { resourceTypes: current.resourceTypes } : {}),
|
|
1237
|
+
...(current.discussions ? { discussions: current.discussions } : {}),
|
|
1127
1238
|
...(current.resourcePages ? { resourcePages: current.resourcePages } : {}),
|
|
1128
1239
|
...(current.calendarSource ? { calendarSource: current.calendarSource } : {}),
|
|
1129
1240
|
...(current.fileProvider ? { fileProvider: current.fileProvider } : {}),
|
|
@@ -1157,9 +1268,11 @@ const assertUsableOperation = (operation: CapabilityQueryManifest | CapabilityAc
|
|
|
1157
1268
|
if (operation.stream?.direction === "write" && (!("idempotency" in operation) || operation.idempotency !== "required")) {
|
|
1158
1269
|
throw new Error(`Operation ${operation.localId} write streams require an idempotent Action`);
|
|
1159
1270
|
}
|
|
1271
|
+
if ("appOnly" in operation && operation.appOnly !== undefined && operation.stream) throw new Error("App-only queries cannot use streams");
|
|
1160
1272
|
const expectedSchemaHash = capabilityHash({
|
|
1161
1273
|
inputSchema: operation.inputSchema,
|
|
1162
1274
|
dataSchema: operation.dataSchema,
|
|
1275
|
+
...("appOnly" in operation && operation.appOnly !== undefined ? { appOnly: operation.appOnly } : {}),
|
|
1163
1276
|
...(operation.stream ? { stream: operation.stream } : {}),
|
|
1164
1277
|
});
|
|
1165
1278
|
if (operation.schemaHash !== expectedSchemaHash) throw new Error(`Operation ${operation.localId} schemaHash does not match its schemas`);
|
|
@@ -1542,13 +1655,17 @@ const cancelledInvocation = (locale: string) => ({
|
|
|
1542
1655
|
},
|
|
1543
1656
|
});
|
|
1544
1657
|
|
|
1658
|
+
const isAppCapabilityContext = (
|
|
1659
|
+
context: CapabilityExecutionContext | CapabilityAppExecutionContext,
|
|
1660
|
+
): context is CapabilityAppExecutionContext => context.actor.kind === "app";
|
|
1661
|
+
|
|
1545
1662
|
export const invokeCompiledCapability = async (params: {
|
|
1546
1663
|
compiled: CompiledCapabilities;
|
|
1547
1664
|
kind: "query" | "action";
|
|
1548
1665
|
localId: string;
|
|
1549
1666
|
input: unknown;
|
|
1550
1667
|
expectedSchemaHash: string | null;
|
|
1551
|
-
context: CapabilityExecutionContext;
|
|
1668
|
+
context: CapabilityExecutionContext | CapabilityAppExecutionContext;
|
|
1552
1669
|
onUnexpectedError?: (error: unknown) => void;
|
|
1553
1670
|
}): Promise<CapabilityInvocationResult<unknown>> => {
|
|
1554
1671
|
if (!hasOAuthScope(params.context.authority, params.kind === "query" ? "read" : "write", "admin"))
|
|
@@ -1564,6 +1681,8 @@ export const invokeCompiledCapability = async (params: {
|
|
|
1564
1681
|
},
|
|
1565
1682
|
};
|
|
1566
1683
|
}
|
|
1684
|
+
if (!isAppCapabilityContext(params.context) && "appOnly" in operation.definition && operation.definition.appOnly !== undefined)
|
|
1685
|
+
return { ok: false, error: { code: "FORBIDDEN", message: "App authority required", status: 403 } };
|
|
1567
1686
|
if (params.expectedSchemaHash !== operation.manifest.schemaHash) {
|
|
1568
1687
|
return {
|
|
1569
1688
|
ok: false,
|
|
@@ -1575,7 +1694,7 @@ export const invokeCompiledCapability = async (params: {
|
|
|
1575
1694
|
},
|
|
1576
1695
|
};
|
|
1577
1696
|
}
|
|
1578
|
-
let context: CapabilityExecutionContext = {
|
|
1697
|
+
let context: CapabilityExecutionContext | CapabilityAppExecutionContext = {
|
|
1579
1698
|
...params.context,
|
|
1580
1699
|
via: capabilityVia(params.context.origin),
|
|
1581
1700
|
};
|
|
@@ -1624,7 +1743,7 @@ export const invokeCompiledCapability = async (params: {
|
|
|
1624
1743
|
},
|
|
1625
1744
|
};
|
|
1626
1745
|
}
|
|
1627
|
-
context = { ...context, idempotencyKey: idempotencyKey.data };
|
|
1746
|
+
if (!isAppCapabilityContext(context)) context = { ...context, idempotencyKey: idempotencyKey.data };
|
|
1628
1747
|
}
|
|
1629
1748
|
const input = operation.definition.input.safeParse(params.input);
|
|
1630
1749
|
if (!input.success) {
|
|
@@ -1652,7 +1771,16 @@ export const invokeCompiledCapability = async (params: {
|
|
|
1652
1771
|
// cross into application code for the first time with an aborted invocation.
|
|
1653
1772
|
if (context.signal.aborted) return cancelledInvocation(context.locale);
|
|
1654
1773
|
try {
|
|
1655
|
-
|
|
1774
|
+
let invoked: CapabilityInvocationResult<unknown>;
|
|
1775
|
+
if (isAppCapabilityContext(context)) {
|
|
1776
|
+
const definition = operation.definition;
|
|
1777
|
+
if (params.kind !== "query" || !("appOnly" in definition) || definition.appOnly !== true)
|
|
1778
|
+
return { ok: false, error: { code: "FORBIDDEN", message: "App caller is not allowed", status: 403 } };
|
|
1779
|
+
invoked = await definition.run(input.data, context);
|
|
1780
|
+
} else {
|
|
1781
|
+
if (operation.definition.appOnly) return { ok: false, error: { code: "FORBIDDEN", message: "App authority required", status: 403 } };
|
|
1782
|
+
invoked = await operation.definition.run(input.data, context);
|
|
1783
|
+
}
|
|
1656
1784
|
if (!invoked.ok) {
|
|
1657
1785
|
return normalizeProviderError(invoked.error, "Capability returned an invalid error", params.onUnexpectedError);
|
|
1658
1786
|
}
|
|
@@ -12,6 +12,7 @@ import { registerPageSwap } from "../ssr/page-swap";
|
|
|
12
12
|
import { preloadPanelConsent } from "../ssr/panel-consent";
|
|
13
13
|
import { appAccessManagementPath } from "./app-access-path";
|
|
14
14
|
import { bindResourceAppAccess, RESOURCE_ACCESS_PATH } from "./app-access-resources";
|
|
15
|
+
import { createAppQueryRoutes } from "./app-queries";
|
|
15
16
|
import { registerApplicationPackage, resolveApplicationPackage } from "./application-package";
|
|
16
17
|
import { invokeCapabilityStream } from "./capability-streams";
|
|
17
18
|
import { withCapabilityRequestTimeout } from "./capability-timeout";
|
|
@@ -916,6 +917,7 @@ export const defineApp = <
|
|
|
916
917
|
server.route("/api/_internal/events/v1", createAppEventDeliveryRoutes(compiledCapabilities));
|
|
917
918
|
|
|
918
919
|
if (compiledCapabilities) {
|
|
920
|
+
server.route("/api/_internal/capabilities/v1/app-queries", createAppQueryRoutes(compiledCapabilities));
|
|
919
921
|
const invoke = async (c: Context<AuthContext>, kind: "query" | "action" | "review") =>
|
|
920
922
|
withCapabilityRequestTimeout(c, kind, async () => {
|
|
921
923
|
// Framework-owned endpoints run before the app's settings middleware,
|
|
@@ -160,8 +160,8 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
160
160
|
return invalid("widgets", "an array of valid widgets");
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
-
if (value.platformPermissions !== undefined && !
|
|
164
|
-
return invalid("platformPermissions", "an array of
|
|
163
|
+
if (value.platformPermissions !== undefined && !isStringArray(value.platformPermissions))
|
|
164
|
+
return invalid("platformPermissions", "an array of permission IDs");
|
|
165
165
|
if (value.settingKeys !== undefined && !isStringArray(value.settingKeys)) return invalid("settingKeys", "an array of strings");
|
|
166
166
|
if (
|
|
167
167
|
value.cliModules !== undefined &&
|
|
@@ -198,5 +198,18 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
198
198
|
return null;
|
|
199
199
|
};
|
|
200
200
|
|
|
201
|
-
export const asAppRegistryEntry = (value: unknown): AppRegistryEntry | null =>
|
|
202
|
-
validateAppRegistryEntry(value)
|
|
201
|
+
export const asAppRegistryEntry = (value: unknown): AppRegistryEntry | null => {
|
|
202
|
+
if (validateAppRegistryEntry(value) !== null) return null;
|
|
203
|
+
const entry = value as AppRegistryEntry;
|
|
204
|
+
return {
|
|
205
|
+
...entry,
|
|
206
|
+
...(entry.platformPermissions !== undefined
|
|
207
|
+
? {
|
|
208
|
+
platformPermissions: entry.platformPermissions.flatMap((permission) => {
|
|
209
|
+
const parsed = PlatformPermissionSchema.safeParse(permission);
|
|
210
|
+
return parsed.success ? [parsed.data] : [];
|
|
211
|
+
}),
|
|
212
|
+
}
|
|
213
|
+
: {}),
|
|
214
|
+
};
|
|
215
|
+
};
|
|
@@ -3,6 +3,7 @@ import type { EphemeralConfig, MutexConfig } from "@k2b/sync";
|
|
|
3
3
|
import type { AppAppearanceColor } from "../contracts/app";
|
|
4
4
|
import { calendarSourceIssues } from "../contracts/calendar-source";
|
|
5
5
|
import type { CapabilityManifest, CapabilityPresentationCatalog } from "../contracts/capabilities";
|
|
6
|
+
import { discussionsIssues } from "../contracts/discussions";
|
|
6
7
|
import { fileProviderIssues } from "../contracts/file-provider";
|
|
7
8
|
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
8
9
|
import { resourcePageIssue } from "../contracts/resource-pages";
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
import { resolveAppPresentations } from "../shared/app-presentation";
|
|
19
20
|
import { compileCapabilityPresentation, parseCapabilityManifest } from "./capabilities";
|
|
20
21
|
import { lazySync } from "./process-sync";
|
|
21
|
-
import { validateAppRegistryEntry } from "./registry-validation";
|
|
22
|
+
import { asAppRegistryEntry, validateAppRegistryEntry } from "./registry-validation";
|
|
22
23
|
import { watchRegistryChanges } from "./registry-watch";
|
|
23
24
|
import { WidgetSettingsDeclarationSchema } from "./widget-settings";
|
|
24
25
|
|
|
@@ -150,7 +151,7 @@ export const readAppRegistrySnapshot = async (): Promise<AppRegistrySnapshot> =>
|
|
|
150
151
|
continue;
|
|
151
152
|
}
|
|
152
153
|
loggedInvalidReasons.delete(entry.key);
|
|
153
|
-
const value = entry.value
|
|
154
|
+
const value = asAppRegistryEntry(entry.value)!;
|
|
154
155
|
const updatedAt = entry.updatedAt.getTime();
|
|
155
156
|
apps.push({
|
|
156
157
|
...value,
|
|
@@ -194,7 +195,7 @@ export const getApp = async (appId: string): Promise<AppRegistryEntry | null> =>
|
|
|
194
195
|
: validateAppRegistryEntry(entry.value);
|
|
195
196
|
if (!reason) {
|
|
196
197
|
loggedInvalidReasons.delete(key);
|
|
197
|
-
return entry.value;
|
|
198
|
+
return asAppRegistryEntry(entry.value);
|
|
198
199
|
}
|
|
199
200
|
reportInvalidEntry({ key, version: entry.revision, reason });
|
|
200
201
|
return null;
|
|
@@ -237,6 +238,7 @@ const reportLeftOutEntries = (sent: unknown, manifest: CapabilityManifest): void
|
|
|
237
238
|
});
|
|
238
239
|
if (groups.calendarSource !== undefined && !manifest.calendarSource) leftOut.push("calendarSource");
|
|
239
240
|
if (groups.fileProvider !== undefined && !manifest.fileProvider) leftOut.push("fileProvider");
|
|
241
|
+
if (groups.discussions !== undefined && !manifest.discussions) leftOut.push("discussions");
|
|
240
242
|
if (leftOut.length === 0) return;
|
|
241
243
|
console.warn(
|
|
242
244
|
JSON.stringify({
|
|
@@ -320,6 +322,32 @@ const withValidCalendarSource = (manifest: CapabilityManifest): CapabilityManife
|
|
|
320
322
|
return rest;
|
|
321
323
|
};
|
|
322
324
|
|
|
325
|
+
const checkedDiscussions = new Map<string, { manifestHash: string; valid: boolean }>();
|
|
326
|
+
const withValidDiscussions = (manifest: CapabilityManifest): CapabilityManifest => {
|
|
327
|
+
if (!manifest.discussions) return manifest;
|
|
328
|
+
const checked = checkedDiscussions.get(manifest.appId);
|
|
329
|
+
let valid = checked?.manifestHash === manifest.manifestHash ? checked.valid : undefined;
|
|
330
|
+
if (valid === undefined) {
|
|
331
|
+
const issues = discussionsIssues(manifest);
|
|
332
|
+
valid = issues.length === 0;
|
|
333
|
+
checkedDiscussions.set(manifest.appId, { manifestHash: manifest.manifestHash, valid });
|
|
334
|
+
if (!valid)
|
|
335
|
+
console.error(
|
|
336
|
+
JSON.stringify({
|
|
337
|
+
level: "error",
|
|
338
|
+
source: "capability-registry",
|
|
339
|
+
message: "Ignored an incompatible discussions provider",
|
|
340
|
+
appId: manifest.appId,
|
|
341
|
+
manifestHash: manifest.manifestHash,
|
|
342
|
+
issues,
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (valid) return manifest;
|
|
347
|
+
const { discussions: _ignored, ...rest } = manifest;
|
|
348
|
+
return rest;
|
|
349
|
+
};
|
|
350
|
+
|
|
323
351
|
/** Ignores record fields from a newer release; the endpoint always comes from the live app registry. */
|
|
324
352
|
export const resolveLiveCapabilityRegistryEntry = (
|
|
325
353
|
key: string,
|
|
@@ -334,7 +362,7 @@ export const resolveLiveCapabilityRegistryEntry = (
|
|
|
334
362
|
if (!endpoint) return null;
|
|
335
363
|
try {
|
|
336
364
|
const read = parseCapabilityManifest(record.manifest, app.id);
|
|
337
|
-
const supported = withValidCalendarSource(withValidFileProvider(read));
|
|
365
|
+
const supported = withValidDiscussions(withValidCalendarSource(withValidFileProvider(read)));
|
|
338
366
|
const manifest = supported.resourcePages
|
|
339
367
|
? {
|
|
340
368
|
...supported,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import type { ResourcePrincipal } from "../contracts/capabilities";
|
|
3
|
+
import { decodeAccountCategoryEnabled } from "../services/account-category-policy";
|
|
4
|
+
import { toPgTextArray } from "../services/postgres";
|
|
5
|
+
|
|
6
|
+
/** One bounded identity lookup before any provider-owned readability check. */
|
|
7
|
+
export const activeResourcePrincipals = async (principals: ResourcePrincipal[]): Promise<ResourcePrincipal[]> => {
|
|
8
|
+
if (!principals.length) return [];
|
|
9
|
+
const users = principals.flatMap((principal) => (principal.type === "user" ? [principal.userId] : []));
|
|
10
|
+
const accounts = principals.flatMap((principal) => (principal.type === "service_account" ? [principal.serviceAccountId] : []));
|
|
11
|
+
const rows = await sql<{ type: ResourcePrincipal["type"]; id: string; category_enabled: string | null }[]>`
|
|
12
|
+
SELECT 'user' AS type, u.id, category.value AS category_enabled
|
|
13
|
+
FROM auth.users u
|
|
14
|
+
LEFT JOIN settings.entries category ON category.key = 'user.category.' ||
|
|
15
|
+
CASE WHEN u.provider = 'ipa' THEN 'freeipa' WHEN u.profile = 'guest' THEN 'guest' ELSE 'login' END || '.enabled'
|
|
16
|
+
WHERE u.id = ANY(${toPgTextArray(users)}::uuid[])
|
|
17
|
+
AND (u.account_expires IS NULL OR u.account_expires > clock_timestamp())
|
|
18
|
+
UNION ALL
|
|
19
|
+
SELECT 'service_account', sa.id, category.value
|
|
20
|
+
FROM auth.service_accounts sa
|
|
21
|
+
LEFT JOIN auth.users du ON du.id = sa.delegated_user_id
|
|
22
|
+
LEFT JOIN settings.entries category ON sa.kind = 'user_delegated' AND category.key = 'user.category.' ||
|
|
23
|
+
CASE WHEN du.provider = 'ipa' THEN 'freeipa' WHEN du.profile = 'guest' THEN 'guest' ELSE 'login' END || '.enabled'
|
|
24
|
+
WHERE sa.id = ANY(${toPgTextArray(accounts)}::uuid[]) AND sa.status = 'active'
|
|
25
|
+
AND (sa.kind <> 'user_delegated' OR (du.id IS NOT NULL AND (du.account_expires IS NULL OR du.account_expires > clock_timestamp())))
|
|
26
|
+
`;
|
|
27
|
+
const enabled = new Map<string | null, boolean>();
|
|
28
|
+
const active = new Set<string>();
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
if (!enabled.has(row.category_enabled)) enabled.set(row.category_enabled, await decodeAccountCategoryEnabled(row.category_enabled));
|
|
31
|
+
if (enabled.get(row.category_enabled)) active.add(`${row.type}:${row.id}`);
|
|
32
|
+
}
|
|
33
|
+
return principals.filter((principal) =>
|
|
34
|
+
active.has(`${principal.type}:${(principal.type === "user" ? principal.userId : principal.serviceAccountId).toLowerCase()}`),
|
|
35
|
+
);
|
|
36
|
+
};
|
package/src/ai/capabilities.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { Tool, ToolContext, ToolResolver } from "@k2b/nessi";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { resolveCapabilityActionWording, resolveCapabilityOperationTitle } from "../_internal/capabilities";
|
|
4
|
+
import { isPersonVisibleCapabilityQuery, resolveCapabilityActionWording, resolveCapabilityOperationTitle } from "../_internal/capabilities";
|
|
5
5
|
import { capabilityReviewView } from "../_internal/capability-reviews";
|
|
6
6
|
import {
|
|
7
7
|
CAPABILITY_APPROVAL_REASON_FIELD,
|
|
@@ -164,7 +164,9 @@ export const buildAiCapabilityCatalog = (apps: CapabilityRegistryEntry[], locale
|
|
|
164
164
|
: app.appName;
|
|
165
165
|
return [
|
|
166
166
|
...app.manifest.actions.map((operation) => ({ app, appName, operation, kind: "action" as const })),
|
|
167
|
-
...app.manifest.queries
|
|
167
|
+
...app.manifest.queries
|
|
168
|
+
.filter(isPersonVisibleCapabilityQuery)
|
|
169
|
+
.map((operation) => ({ app, appName, operation, kind: "query" as const })),
|
|
168
170
|
];
|
|
169
171
|
})
|
|
170
172
|
.map(
|