@k2b/cloud 0.27.0 → 0.29.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 +7 -5
- package/src/_internal/cli-plugins.ts +18 -0
- package/src/_internal/define-app.ts +21 -5
- package/src/_internal/registry-validation.ts +30 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/PermissionEditor.tsx +11 -4
- package/src/ai/assistant-models.ts +25 -4
- package/src/ai/audio-tool.ts +3 -10
- package/src/ai/browser-code-contracts.ts +33 -63
- package/src/ai/browser.ts +1 -0
- package/src/ai/capabilities.ts +35 -9
- package/src/ai/chat/blocks.tsx +113 -24
- package/src/ai/chat/builtin-tools.tsx +3 -1
- package/src/ai/chat/composer-adapter.ts +8 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +15 -3
- package/src/ai/chat/message-actions.tsx +124 -102
- package/src/ai/chat/message-utils.ts +57 -6
- package/src/ai/chat/messages.ts +214 -0
- package/src/ai/chat/presentation.tsx +67 -28
- package/src/ai/chat/tool-groups.ts +1 -1
- package/src/ai/chat/tool-names.ts +157 -0
- package/src/ai/chat/turn-error.ts +21 -0
- package/src/ai/chat/turn-layout.ts +12 -0
- package/src/ai/chat/turn-view.tsx +69 -10
- package/src/ai/chat/user-message.tsx +19 -14
- package/src/ai/chat/visual-tools.tsx +1 -1
- package/src/ai/chat-tasks.ts +16 -3
- package/src/ai/client/controller.ts +23 -6
- package/src/ai/code-mode-skill.ts +21 -25
- package/src/ai/code-runtime-tools.ts +5 -1
- package/src/ai/code-source-contracts.ts +2 -2
- package/src/ai/compaction.ts +22 -3
- package/src/ai/data-analysis-skill.ts +3 -3
- package/src/ai/default-tools.ts +23 -17
- package/src/ai/executor.ts +262 -99
- package/src/ai/file-media-type.ts +4 -0
- package/src/ai/files-store.ts +17 -10
- package/src/ai/http.ts +11 -10
- package/src/ai/index.ts +3 -0
- package/src/ai/memories.ts +2 -0
- package/src/ai/memory-learning.ts +59 -5
- package/src/ai/memory-workflow-evidence.ts +35 -10
- package/src/ai/open-tool-calls.ts +87 -0
- package/src/ai/projects-routes.ts +5 -1
- package/src/ai/projects.ts +5 -1
- package/src/ai/protocol.ts +34 -1
- package/src/ai/provider.ts +16 -1
- package/src/ai/quota-provider.ts +4 -1
- package/src/ai/routes.ts +26 -10
- package/src/ai/run-timeout.ts +4 -5
- package/src/ai/runtime.ts +8 -0
- package/src/ai/settings.ts +54 -17
- package/src/ai/skill-seeds.ts +14 -6
- package/src/ai/store.ts +102 -34
- package/src/ai/structured.ts +2 -1
- package/src/ai/system-prompt.ts +9 -22
- package/src/ai/transcription.ts +58 -13
- package/src/ai/turn-failure.ts +104 -0
- package/src/ai/turn-guidance.ts +505 -0
- package/src/ai/turn-policy.ts +2 -1
- package/src/ai/types.ts +29 -1
- package/src/ai/ui.tsx +1 -0
- package/src/api/admin-core-settings.ts +16 -1
- package/src/api/admin-outgoing-mail.ts +185 -5
- package/src/api/me.ts +42 -1
- package/src/browser/FileChooser.tsx +82 -295
- package/src/browser/FileSaver.tsx +407 -0
- package/src/browser/app-badges.ts +16 -0
- package/src/browser/choose-files.browser-harness.tsx +22 -6
- package/src/browser/choose-files.tsx +23 -36
- package/src/browser/file-chooser-messages.ts +66 -0
- package/src/browser/file-providers.ts +258 -8
- package/src/browser/files.tsx +26 -3
- package/src/browser/notifications.ts +37 -1
- package/src/browser/provider-folder.tsx +301 -0
- package/src/browser/provider-list.ts +39 -0
- package/src/browser/save-files.browser-harness.tsx +35 -0
- package/src/browser/save-files.tsx +107 -0
- package/src/cli/admin/index.ts +3 -1
- package/src/cli/admin/notifications.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +104 -7
- package/src/contracts/app.ts +10 -0
- package/src/contracts/commands.ts +3 -1
- package/src/contracts/notification-group.ts +6 -0
- package/src/contracts/notification-types.ts +6 -0
- package/src/contracts/outgoing-mail.ts +189 -1
- package/src/contracts/registry.ts +1 -0
- package/src/contracts/user-notifications.ts +74 -0
- package/src/server/middleware/request-logger.ts +11 -2
- package/src/services/accounts/avatar.ts +5 -2
- package/src/services/help/store.ts +2 -1
- package/src/services/image-metadata.ts +438 -0
- package/src/services/index.ts +12 -1
- package/src/services/logging/redaction.ts +50 -0
- package/src/services/mandates/index.ts +4 -2
- package/src/services/mandates/policy.ts +20 -0
- package/src/services/notifications/batches.ts +139 -79
- package/src/services/notifications/browser.ts +48 -15
- package/src/services/notifications/channels.ts +33 -13
- package/src/services/notifications/dispatcher.ts +88 -6
- package/src/services/notifications/email-frame.fixture.html +51 -0
- package/src/services/notifications/{email.ts → email-frame.ts} +12 -35
- package/src/services/notifications/email-mail.ts +101 -0
- package/src/services/notifications/index.ts +49 -36
- package/src/services/notifications/observability.ts +9 -1
- package/src/services/notifications/platform.ts +33 -5
- package/src/services/notifications/preview.ts +23 -0
- package/src/services/notifications/quiet.ts +153 -0
- package/src/services/notifications/runtime.ts +9 -3
- package/src/services/notifications/user.ts +6 -0
- package/src/services/outgoing-mail/admin.ts +84 -0
- package/src/services/outgoing-mail/attachments.ts +135 -0
- package/src/services/outgoing-mail/bulk.ts +11 -0
- package/src/services/outgoing-mail/dispatcher.ts +231 -0
- package/src/services/outgoing-mail/drain.ts +60 -0
- package/src/services/outgoing-mail/enqueue.ts +180 -0
- package/src/services/outgoing-mail/index.ts +103 -10
- package/src/services/outgoing-mail/message.ts +14 -0
- package/src/services/outgoing-mail/messages.ts +431 -0
- package/src/services/outgoing-mail/retention.ts +48 -0
- package/src/services/outgoing-mail/runtime.ts +72 -0
- package/src/services/outgoing-mail/send.ts +130 -0
- package/src/services/outgoing-mail/store.ts +115 -4
- package/src/services/outgoing-mail/sync.ts +39 -0
- package/src/services/outgoing-mail/transport.ts +5 -1
- package/src/services/pdf/markdown.ts +22 -4
- package/src/services/pdf/template-preview.ts +2 -1
- package/src/services/postgres.ts +15 -0
- package/src/services/settings/app.ts +3 -0
- package/src/services/settings/core-settings.ts +18 -0
- package/src/services/settings/store.ts +4 -1
- package/src/services/weather/geo.ts +6 -2
- package/src/shared/ai-platform-prompt.ts +48 -4
- package/src/shared/markdown/extensions/links.ts +28 -20
- package/src/shared/markdown/index.ts +14 -5
- package/src/shared/markdown/shared.ts +0 -7
- package/src/shared/redirect.ts +2 -1
- package/src/shared/template-rendering.ts +120 -7
- package/src/ssr/AppBadge.tsx +30 -0
- package/src/ssr/AppLaunchpadPanel.tsx +47 -26
- package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
- package/src/ssr/IslandError.tsx +33 -0
- package/src/ssr/Layout.tsx +3 -0
- package/src/ssr/LayoutHelp.tsx +4 -2
- package/src/ssr/RailApps.island.tsx +9 -2
- package/src/ssr/app-badges.ts +93 -0
- package/src/ssr/platform-messages.ts +1 -1
- package/src/ssr/rail-messages.ts +4 -0
- package/src/ssr/rail-navigation.ts +2 -0
- package/src/styles/effects.css +38 -17
- package/src/styles/file-chooser.css +25 -0
- package/src/styles/tokens.css +2 -0
- package/src/styles/utilities-markdown-editor.css +4 -39
- package/src/styles/utilities-markdown-table.css +14 -17
- package/src/styles/utilities-navigation.css +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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/*",
|
|
31
31
|
"./browser": "./src/server/api-client.ts",
|
|
32
32
|
"./browser/app-approval": "./src/browser/app-approval.ts",
|
|
33
|
+
"./browser/app-badges": "./src/browser/app-badges.ts",
|
|
33
34
|
"./browser/live": "./src/browser/live.ts",
|
|
34
35
|
"./browser/mermaid": "./src/browser/mermaid.ts",
|
|
35
36
|
"./browser/reload": "./src/browser/reload.ts",
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
"./cli/admin": "./src/cli/admin/index.ts",
|
|
53
54
|
"./shared": "./src/shared/index.ts",
|
|
54
55
|
"./services": "./src/services/index.ts",
|
|
56
|
+
"./services/image-metadata": "./src/services/image-metadata.ts",
|
|
55
57
|
"./services/document-extraction": "./src/services/document-extraction.ts",
|
|
56
58
|
"./services/ipa/service-account": null,
|
|
57
59
|
"./services/*": "./src/services/*",
|
|
@@ -98,10 +100,10 @@
|
|
|
98
100
|
"@simplewebauthn/server": "14.0.3",
|
|
99
101
|
"@tabler/icons": "3.48.0",
|
|
100
102
|
"@tailwindcss/typography": "0.5.20",
|
|
101
|
-
"@k2b/nessi": "0.
|
|
102
|
-
"@k2b/ssr": "0.
|
|
103
|
-
"@k2b/ui": "0.
|
|
104
|
-
"@k2b/stdlib": "0.
|
|
103
|
+
"@k2b/nessi": "0.17.0",
|
|
104
|
+
"@k2b/ssr": "0.15.0",
|
|
105
|
+
"@k2b/ui": "0.19.0",
|
|
106
|
+
"@k2b/stdlib": "0.28.0",
|
|
105
107
|
"@k2b/sync": "7.0.0",
|
|
106
108
|
"@nats-io/jetstream": "3.4.0",
|
|
107
109
|
"@nats-io/transport-node": "3.4.0",
|
|
@@ -94,6 +94,24 @@ export const buildCliPlugin = async (params: {
|
|
|
94
94
|
minify: true,
|
|
95
95
|
// Playwright references this optional BiDi adapter lazily; Cloud uses CDP.
|
|
96
96
|
external: ["chromium-bidi/*"],
|
|
97
|
+
plugins: [
|
|
98
|
+
{
|
|
99
|
+
name: "playwright-metadata",
|
|
100
|
+
setup(build) {
|
|
101
|
+
build.onLoad({ filter: /[/\\]playwright-core[/\\]lib[/\\](?:coreBundle|serverRegistry|package)\.js$/ }, async ({ path }) => ({
|
|
102
|
+
// Computed package-relative JSON requires evade Bun's dependency
|
|
103
|
+
// graph and retain the container's absolute path. Static requires
|
|
104
|
+
// carry the version and browser registry in the single-file plugin.
|
|
105
|
+
contents: (await readFile(path, "utf8")).replace(
|
|
106
|
+
/require\(import_path\d*\.default\.join\(packageRoot, "(package|browsers)\.json"\)\)/g,
|
|
107
|
+
(_, file: string) => `require("../${file}.json")`,
|
|
108
|
+
),
|
|
109
|
+
loader: "js",
|
|
110
|
+
resolveDir: dirname(path),
|
|
111
|
+
}));
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
97
115
|
});
|
|
98
116
|
if (!result.success) {
|
|
99
117
|
throw new Error(`CLI module "${name}" failed to bundle:\n${result.logs.map((log) => String(log)).join("\n")}`);
|
|
@@ -86,6 +86,7 @@ import { configurePostgresApplicationName } from "./postgres-application-name";
|
|
|
86
86
|
import { getProcessSync, startProcessSync } from "./process-sync";
|
|
87
87
|
import { APP_READINESS_PATH, appReadinessResponse } from "./readiness";
|
|
88
88
|
import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry } from "./registry";
|
|
89
|
+
import { canonicalRoute, isOwnNavBadge } from "./registry-validation";
|
|
89
90
|
import { ensureRuntimeWatcher, getCurrentRuntime, stopRuntimeWatcher } from "./runtime-watcher";
|
|
90
91
|
import { servePublicAsset } from "./static-assets";
|
|
91
92
|
import { createStatusPreservingSsrHandler } from "./status-preserving-ssr";
|
|
@@ -144,6 +145,13 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
144
145
|
section: "primary" | "more" | "hidden";
|
|
145
146
|
requiresAuth?: boolean;
|
|
146
147
|
requiresRoles?: Role[];
|
|
148
|
+
/**
|
|
149
|
+
* Same-origin path of a `GET` route this app serves below one of its
|
|
150
|
+
* `routes`. After the page loads, the shell reads it with the person's
|
|
151
|
+
* session and shows the returned `AppNavBadge` count on the app's icon in
|
|
152
|
+
* the rail and the app grid. Protect it like any other route.
|
|
153
|
+
*/
|
|
154
|
+
badge?: string;
|
|
147
155
|
};
|
|
148
156
|
/**
|
|
149
157
|
* Settings owned by this app, declared as a map of dotted-key → definition.
|
|
@@ -332,11 +340,6 @@ export type AppDefinition<S extends AppSettingsMap = {}, N extends NotificationD
|
|
|
332
340
|
// ── Implementation ──────────────────────────────────────────────────────────
|
|
333
341
|
|
|
334
342
|
const isPwaPath = (route: string): boolean => route === "/pwa" || route.startsWith("/pwa/");
|
|
335
|
-
/** A route as the gateway reads it: trimmed and without empty segments, so `//pwa/x/` is `/pwa/x`. */
|
|
336
|
-
const canonicalRoute = (route: string): string => {
|
|
337
|
-
const trimmed = route.trim();
|
|
338
|
-
return trimmed.startsWith("/") ? `/${trimmed.split("/").filter(Boolean).join("/")}` : trimmed;
|
|
339
|
-
};
|
|
340
343
|
|
|
341
344
|
/**
|
|
342
345
|
* `/pwa` belongs to the mobile app shell, `/pwa/_auth` to Core, and `/pwa/<id>`
|
|
@@ -357,6 +360,14 @@ const validateAppPwaPart = (opts: Pick<AppOptions, "id" | "routes" | "pwa">): Ap
|
|
|
357
360
|
return { href: `/pwa/${opts.id}`, requiresRoles: opts.pwa.requiresRoles ? [...opts.pwa.requiresRoles] : undefined };
|
|
358
361
|
};
|
|
359
362
|
|
|
363
|
+
/** The badge route is read by the browser through the gateway, so it must be one of this app's own paths. */
|
|
364
|
+
const validateAppNavBadge = (opts: Pick<AppOptions, "id" | "routes" | "nav">): void => {
|
|
365
|
+
const badge = opts.nav?.badge;
|
|
366
|
+
if (badge !== undefined && !isOwnNavBadge(badge, opts.routes)) {
|
|
367
|
+
throw new Error(`App "${opts.id}" declares nav.badge "${badge}"; it must be a same-origin path below one of its routes`);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
360
371
|
export const defineApp = <
|
|
361
372
|
const S extends AppSettingsMap = {},
|
|
362
373
|
const N extends NotificationDefinitionMap = {},
|
|
@@ -369,6 +380,7 @@ export const defineApp = <
|
|
|
369
380
|
const notifications = bindNotificationDefinitions(opts.id, opts.notifications);
|
|
370
381
|
const cliModules = validateAppCliModules(opts.id, opts.cli);
|
|
371
382
|
const pwaPart = validateAppPwaPart(opts);
|
|
383
|
+
validateAppNavBadge(opts);
|
|
372
384
|
|
|
373
385
|
// ── 0. Register declared settings into the runtime registry ──────────
|
|
374
386
|
// SETTINGS_MAP is the single source of truth for validation in store.ts
|
|
@@ -387,6 +399,9 @@ export const defineApp = <
|
|
|
387
399
|
rootDir,
|
|
388
400
|
componentRoots: [resolve(env.APP_DIR ?? rootDir, "src"), fileURLToPath(new URL("../", import.meta.url))],
|
|
389
401
|
basePath: opts.basePath,
|
|
402
|
+
// Every island instance already mounts inside its own error boundary; this
|
|
403
|
+
// only gives the fallback Cloud's look and the reader's language.
|
|
404
|
+
errorFallback: fileURLToPath(new URL("../ssr/IslandError.tsx", import.meta.url)),
|
|
390
405
|
template: ({ body, scripts, title, description, theme, lang, performanceRoute, pwa }) => {
|
|
391
406
|
const themeFixed = theme !== undefined;
|
|
392
407
|
// App pages paint the canvas before any stylesheet and give it to the
|
|
@@ -571,6 +586,7 @@ export const defineApp = <
|
|
|
571
586
|
section: meta.nav?.section ?? "hidden",
|
|
572
587
|
requiresAuth: meta.nav?.requiresAuth,
|
|
573
588
|
requiresRoles: meta.nav?.requiresRoles,
|
|
589
|
+
badge: meta.nav?.badge,
|
|
574
590
|
adminHref: meta.adminHref,
|
|
575
591
|
}
|
|
576
592
|
: undefined,
|
|
@@ -7,6 +7,32 @@ const isString = (value: unknown): value is string => typeof value === "string";
|
|
|
7
7
|
const isStringArray = (value: unknown): value is string[] => Array.isArray(value) && value.every(isString);
|
|
8
8
|
const isStringRecord = (value: unknown): value is Record<string, string> => isRecord(value) && Object.values(value).every(isString);
|
|
9
9
|
|
|
10
|
+
/** An absolute path on the current origin: one leading slash, no backslash or whitespace. */
|
|
11
|
+
const SAME_ORIGIN_PATH = /^\/(?![\/\\])[^\\\s]*$/;
|
|
12
|
+
|
|
13
|
+
/** A route as the gateway reads it: trimmed and without empty segments, so `//pwa/x/` is `/pwa/x`. */
|
|
14
|
+
export const canonicalRoute = (route: string): string => {
|
|
15
|
+
const trimmed = route.trim();
|
|
16
|
+
return trimmed.startsWith("/") ? `/${trimmed.split("/").filter(Boolean).join("/")}` : trimmed;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether a `nav.badge` path stays below one of the app's own `routes`. It is
|
|
21
|
+
* checked on the path the browser actually requests, after dot segments, and
|
|
22
|
+
* never starts with `//`, which a URL resolver reads as another host.
|
|
23
|
+
*/
|
|
24
|
+
export const isOwnNavBadge = (badge: string, routes: readonly string[]): boolean => {
|
|
25
|
+
if (!SAME_ORIGIN_PATH.test(badge)) return false;
|
|
26
|
+
const path = new URL(badge, "https://cloud.invalid").pathname;
|
|
27
|
+
return (
|
|
28
|
+
!path.startsWith("//") &&
|
|
29
|
+
routes.some((route) => {
|
|
30
|
+
const prefix = canonicalRoute(route);
|
|
31
|
+
return prefix === "/" || path === prefix || path.startsWith(`${prefix}/`);
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
10
36
|
const invalid = (path: string, expected: string): string => `${path} must be ${expected}`;
|
|
11
37
|
|
|
12
38
|
export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
@@ -61,6 +87,9 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
61
87
|
if (value.nav.requiresRoles !== undefined && !isStringArray(value.nav.requiresRoles)) {
|
|
62
88
|
return invalid("nav.requiresRoles", "an array of strings");
|
|
63
89
|
}
|
|
90
|
+
if (value.nav.badge !== undefined && (!isString(value.nav.badge) || !isOwnNavBadge(value.nav.badge, value.routes))) {
|
|
91
|
+
return invalid("nav.badge", "a same-origin path below one of the entry's routes");
|
|
92
|
+
}
|
|
64
93
|
}
|
|
65
94
|
if (value.adminNav !== undefined) {
|
|
66
95
|
if (
|
|
@@ -98,7 +127,7 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
98
127
|
!link.label.trim() ||
|
|
99
128
|
(link.description !== undefined && !isString(link.description)) ||
|
|
100
129
|
!isString(link.href) ||
|
|
101
|
-
|
|
130
|
+
!SAME_ORIGIN_PATH.test(link.href) ||
|
|
102
131
|
(link.icon !== undefined && !isString(link.icon)) ||
|
|
103
132
|
(link.keywords !== undefined && !isStringArray(link.keywords)),
|
|
104
133
|
)
|
|
@@ -55,6 +55,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
|
|
|
55
55
|
// Registry stores roles as serialized strings; the source type is
|
|
56
56
|
// Role[] and round-trip is value-preserving.
|
|
57
57
|
requiresRoles: e.nav.requiresRoles as Role[] | undefined,
|
|
58
|
+
badge: e.nav.badge,
|
|
58
59
|
}
|
|
59
60
|
: undefined,
|
|
60
61
|
help: e.help ? { ...e.help } : undefined,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mutation } from "@k2b/stdlib/solid";
|
|
2
2
|
import { IconButton, Placeholder, prompts, SelectChip, Tooltip, useLocale } from "@k2b/ui";
|
|
3
|
-
import { For, Show } from "solid-js";
|
|
3
|
+
import { createMemo, For, Show } from "solid-js";
|
|
4
4
|
import { createStore } from "solid-js/store";
|
|
5
5
|
import { CloudAvatar } from "../account/Avatar";
|
|
6
6
|
import type { AccessEntry, PermissionLevel, Principal, ServiceAccountKind } from "../contracts/shared";
|
|
@@ -52,6 +52,13 @@ type PermissionEditorProps = {
|
|
|
52
52
|
* account), its row can neither be lowered nor removed. */
|
|
53
53
|
revokeAccess: (accessId: string) => Promise<void>;
|
|
54
54
|
|
|
55
|
+
/** The entries that still apply when the resource lets one entry shadow
|
|
56
|
+
* another, such as a `none` that overrides the same principal's `admin`.
|
|
57
|
+
* The editor counts managers only among them, so pass the same function
|
|
58
|
+
* the service applies before `ensureManagerRemains()`. Defaults to every
|
|
59
|
+
* entry. */
|
|
60
|
+
effectiveEntries?: (entries: readonly AccessEntry[]) => readonly AccessEntry[];
|
|
61
|
+
|
|
55
62
|
/** Allow granting `public` access from this editor. */
|
|
56
63
|
allowPublic?: boolean;
|
|
57
64
|
|
|
@@ -218,10 +225,10 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
218
225
|
});
|
|
219
226
|
const busy = () => grantMut.loading() || updateMut.loading() || revokeMut.loading();
|
|
220
227
|
// The service refuses to remove the last manager; the row says so up front.
|
|
221
|
-
const lastManagerId = () => {
|
|
222
|
-
const managers = entries.filter(isManagerEntry);
|
|
228
|
+
const lastManagerId = createMemo(() => {
|
|
229
|
+
const managers = (props.effectiveEntries?.(entries) ?? entries).filter(isManagerEntry);
|
|
223
230
|
return managers.length === 1 ? managers[0]!.id : null;
|
|
224
|
-
};
|
|
231
|
+
});
|
|
225
232
|
|
|
226
233
|
return (
|
|
227
234
|
<div class="flex flex-col gap-3">
|
|
@@ -2,8 +2,9 @@ import type { AccessSubject, RequestActor } from "../server";
|
|
|
2
2
|
import { aiModelAccess } from "./model-access";
|
|
3
3
|
import { personalAiModelPolicy } from "./personal-agent";
|
|
4
4
|
import { listAiModels, toPublicAiSettingsState } from "./settings";
|
|
5
|
-
import { resolveAiAudioModel } from "./transcription";
|
|
6
|
-
import type { AiChatTurnRunConfig, AiModelPolicy, AiSettingsError } from "./types";
|
|
5
|
+
import { type AiResolvedAudioModel, AiTranscriptionError, resolveAiAudioModel } from "./transcription";
|
|
6
|
+
import type { AiChatTurnRunConfig, AiDataBoundary, AiModelPolicy, AiSettingsError } from "./types";
|
|
7
|
+
import { isAiSettingsError } from "./validate";
|
|
7
8
|
|
|
8
9
|
export const aiChatAccessSubject = (actor: RequestActor | undefined): AccessSubject | null => {
|
|
9
10
|
if (!actor) return null;
|
|
@@ -20,14 +21,34 @@ export const isAssistantChatTurn = (config: AiChatTurnRunConfig): boolean => con
|
|
|
20
21
|
export const listAssistantAiModels = async (subject: AccessSubject | null, policy: AiModelPolicy = personalAiModelPolicy) =>
|
|
21
22
|
aiModelAccess.filterModels(await listAiModels(policy), subject);
|
|
22
23
|
|
|
24
|
+
/** The one audio check behind the composer microphone, dictation, and offering and running transcribe_audio. */
|
|
25
|
+
export const resolveAssistantAudioModel = async (
|
|
26
|
+
subject: AccessSubject | null,
|
|
27
|
+
allowedDataBoundaries?: AiDataBoundary[],
|
|
28
|
+
): Promise<AiResolvedAudioModel> => {
|
|
29
|
+
const model = await resolveAiAudioModel({ allowedDataBoundaries });
|
|
30
|
+
try {
|
|
31
|
+
await aiModelAccess.assertAllowed(model.profile.id, subject);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (!isAiSettingsError(error) || error.aiError.code !== "model_access_denied") throw error;
|
|
34
|
+
throw Object.assign(
|
|
35
|
+
new AiTranscriptionError(
|
|
36
|
+
"transcription_access_denied",
|
|
37
|
+
"Audio transcription is not available to you: you do not have access to the audio model. An administrator can grant access in the AI settings.",
|
|
38
|
+
),
|
|
39
|
+
{ aiError: error.aiError },
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return model;
|
|
43
|
+
};
|
|
44
|
+
|
|
23
45
|
export const assistantAiSettingsState = async (subject: AccessSubject | null) => {
|
|
24
46
|
const [status, models, audioModelConfigured] = await Promise.all([
|
|
25
47
|
toPublicAiSettingsState(personalAiModelPolicy.allowedDataBoundaries),
|
|
26
48
|
listAssistantAiModels(subject),
|
|
27
49
|
(async () => {
|
|
28
50
|
try {
|
|
29
|
-
|
|
30
|
-
await aiModelAccess.assertAllowed(model.profile.id, subject);
|
|
51
|
+
await resolveAssistantAudioModel(subject, personalAiModelPolicy.allowedDataBoundaries);
|
|
31
52
|
return true;
|
|
32
53
|
} catch {
|
|
33
54
|
return false;
|
package/src/ai/audio-tool.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { aiChatAccessSubject } from "./assistant-models";
|
|
2
|
+
import { aiChatAccessSubject, resolveAssistantAudioModel } from "./assistant-models";
|
|
3
3
|
import { aiProjectFilePathFromMount } from "./file-mount";
|
|
4
4
|
import { aiFileStore, normalizeAiFilePath } from "./files-store";
|
|
5
|
-
import { aiModelAccess } from "./model-access";
|
|
6
5
|
import { defineAiTool } from "./tools";
|
|
7
|
-
import { type AiResolvedAudioModel,
|
|
6
|
+
import { type AiResolvedAudioModel, runAiTranscription } from "./transcription";
|
|
8
7
|
|
|
9
8
|
export const CloudAiTranscribeAudioInputSchema = z.object({
|
|
10
9
|
path: z.string().trim().min(1).describe("Absolute path of the stored audio file. Project files are below /project."),
|
|
@@ -53,13 +52,7 @@ export const createCloudAiTranscribeAudioTool = (options: { resolveModel?: () =>
|
|
|
53
52
|
signal: ctx.signal,
|
|
54
53
|
allowedDataBoundaries: ctx.allowedDataBoundaries,
|
|
55
54
|
attribution: { conversationId: ctx.conversationId, turnId: ctx.turnId },
|
|
56
|
-
resolveModel:
|
|
57
|
-
options.resolveModel ??
|
|
58
|
-
(async () => {
|
|
59
|
-
const resolved = await resolveAiAudioModel({ allowedDataBoundaries: ctx.allowedDataBoundaries });
|
|
60
|
-
await aiModelAccess.assertAllowed(resolved.profile.id, aiChatAccessSubject(ctx.actor));
|
|
61
|
-
return resolved;
|
|
62
|
-
}),
|
|
55
|
+
resolveModel: options.resolveModel ?? (() => resolveAssistantAudioModel(aiChatAccessSubject(ctx.actor), ctx.allowedDataBoundaries)),
|
|
63
56
|
});
|
|
64
57
|
ctx.signal.throwIfAborted();
|
|
65
58
|
const producerCallKey = `${ctx.turnId}:${ctx.callId}`;
|
|
@@ -10,20 +10,20 @@ export const CODE_RUNTIME_TOOL_NAMES = [
|
|
|
10
10
|
"code_run",
|
|
11
11
|
"code_action",
|
|
12
12
|
"code_inspect",
|
|
13
|
-
"code_interact",
|
|
14
13
|
"code_stop",
|
|
15
14
|
"code_open",
|
|
16
15
|
"code_export",
|
|
17
16
|
"code_present",
|
|
17
|
+
"code_check",
|
|
18
18
|
"code_secret",
|
|
19
19
|
] as const;
|
|
20
20
|
export const CodeRunInput = z
|
|
21
21
|
.object({
|
|
22
|
-
id: id.optional(),
|
|
22
|
+
id: id.optional().describe("Saved script resource to run; an app with an index.html interface opens with code_open instead."),
|
|
23
23
|
resourceId: id
|
|
24
24
|
.optional()
|
|
25
25
|
.describe(
|
|
26
|
-
"Optional data context for one-off code. Requires Manage; uses this resource database and shared files/KV without changing its source.
|
|
26
|
+
"Optional data context for one-off code. Requires Manage; uses this resource database and shared files/KV without changing its source. Runs use real shared and personal data; writes and capability effects keep normal permissions and approvals.",
|
|
27
27
|
),
|
|
28
28
|
version: z
|
|
29
29
|
.number()
|
|
@@ -36,14 +36,12 @@ export const CodeRunInput = z
|
|
|
36
36
|
.min(1)
|
|
37
37
|
.max(1024 * 1024)
|
|
38
38
|
.optional()
|
|
39
|
-
.describe("One-off JavaScript/TypeScript
|
|
39
|
+
.describe("One-off JavaScript/TypeScript script exporting one function. Use code OR a saved resource id; no title or icon needed."),
|
|
40
40
|
inputPaths: z
|
|
41
41
|
.array(z.string().min(1).max(500))
|
|
42
42
|
.max(64)
|
|
43
43
|
.default([])
|
|
44
|
-
.describe(
|
|
45
|
-
"Explicit current-chat input files. Scripts can read them; app test runs expose them only through the simulated picker. User apps never receive chat files.",
|
|
46
|
-
),
|
|
44
|
+
.describe("Explicit current-chat input files; the script reads them from its context files. Apps never receive chat files."),
|
|
47
45
|
})
|
|
48
46
|
.strict()
|
|
49
47
|
.refine((input) => Number(input.id !== undefined) + Number(input.code !== undefined) === 1, "Provide exactly one of id or code")
|
|
@@ -91,66 +89,38 @@ export const CodeInspectInput = z
|
|
|
91
89
|
.max(30000)
|
|
92
90
|
.default(0)
|
|
93
91
|
.describe("Wait up to this duration for background work to finish before returning its real state; never restarts work."),
|
|
94
|
-
nodeId: z.string().min(1).max(80).optional(),
|
|
95
|
-
offset: z.number().int().min(0).default(0),
|
|
96
|
-
limit: z.number().int().min(1).max(100).default(20),
|
|
97
92
|
})
|
|
98
93
|
.strict();
|
|
99
|
-
const
|
|
94
|
+
export const CodeStopInput = z.object({ runId }).strict();
|
|
95
|
+
export const CodeOpenInput = z.object({ id }).strict();
|
|
96
|
+
const AppFilePath = z
|
|
97
|
+
.string()
|
|
98
|
+
.min(1)
|
|
99
|
+
.max(180)
|
|
100
|
+
.regex(/^[a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)*$/)
|
|
101
|
+
.refine((path) => path.split("/").every((part) => part !== "." && part !== ".."), "Expected a relative path without . or ..");
|
|
102
|
+
export const CodePresentInput = z
|
|
100
103
|
.object({
|
|
101
|
-
id:
|
|
102
|
-
event: z
|
|
103
|
-
.discriminatedUnion("type", [
|
|
104
|
-
z
|
|
105
|
-
.object({
|
|
106
|
-
type: z.literal("change"),
|
|
107
|
-
value: z.union([
|
|
108
|
-
z.string(),
|
|
109
|
-
z.number().finite(),
|
|
110
|
-
z.boolean(),
|
|
111
|
-
z.null(),
|
|
112
|
-
z.array(z.string()),
|
|
113
|
-
z.object({ start: z.iso.date().nullable(), end: z.iso.date().nullable() }).strict(),
|
|
114
|
-
]),
|
|
115
|
-
})
|
|
116
|
-
.strict(),
|
|
117
|
-
z.object({ type: z.literal("select"), key: z.string().nullable() }).strict(),
|
|
118
|
-
z.object({ type: z.literal("view"), value: z.enum(["chart", "table"]) }).strict(),
|
|
119
|
-
z.object({ type: z.literal("refresh") }).strict(),
|
|
120
|
-
z.object({ type: z.literal("request"), request: z.record(z.string(), z.json()) }).strict(),
|
|
121
|
-
])
|
|
104
|
+
id: id
|
|
122
105
|
.optional()
|
|
123
|
-
.describe(
|
|
124
|
-
|
|
125
|
-
)
|
|
126
|
-
answer: z.json().optional().describe("Only for the pending modal ID: the modal answer, or null to cancel. Do not use for controls."),
|
|
127
|
-
})
|
|
128
|
-
.strict();
|
|
129
|
-
const validInteraction = (input: z.infer<typeof CodeInteraction>) => input.event === undefined || input.answer === undefined;
|
|
130
|
-
export const CodeInteractInput = CodeInteraction.partial({ id: true })
|
|
131
|
-
.extend({
|
|
132
|
-
runId,
|
|
133
|
-
// Three normal 15-second callback budgets share the existing 45-second call budget.
|
|
134
|
-
steps: z
|
|
135
|
-
.array(CodeInteraction.refine(validInteraction, "Supply event OR answer"))
|
|
106
|
+
.describe("Saved app with an index.html interface; the card runs it live with its data. Omit title to use the app title."),
|
|
107
|
+
files: z
|
|
108
|
+
.array(z.object({ path: AppFilePath, content: z.string().max(1024 * 1024) }).strict())
|
|
136
109
|
.min(1)
|
|
137
|
-
.max(
|
|
110
|
+
.max(64)
|
|
138
111
|
.optional()
|
|
139
|
-
.describe(
|
|
140
|
-
|
|
141
|
-
),
|
|
112
|
+
.describe("One-off app for this chat: index.html plus optional style.css, app.js and other modules. It has no saved data."),
|
|
113
|
+
title: z.string().trim().min(1).max(120).optional().describe("Card title; required with files."),
|
|
142
114
|
})
|
|
143
115
|
.strict()
|
|
144
|
-
.refine(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
export const CodeOpenInput = z.object({ id }).strict();
|
|
153
|
-
export const CodePresentInput = z.object({ runId, title: z.string().trim().min(1).max(120) }).strict();
|
|
116
|
+
.refine((input) => Number(input.id !== undefined) + Number(input.files !== undefined) === 1, "Provide exactly one of id or files")
|
|
117
|
+
.refine((input) => input.id !== undefined || input.title !== undefined, "A one-off app needs a title")
|
|
118
|
+
.refine((input) => !input.files || input.files.some((file) => file.path === "index.html"), "files must contain index.html");
|
|
119
|
+
export const CodeCheckInput = z
|
|
120
|
+
.object({ id: id.optional(), files: CodePresentInput.shape.files })
|
|
121
|
+
.strict()
|
|
122
|
+
.refine((input) => Number(input.id !== undefined) + Number(input.files !== undefined) === 1, "Provide exactly one of id or files")
|
|
123
|
+
.refine((input) => !input.files || input.files.some((file) => file.path === "index.html"), "files must contain index.html");
|
|
154
124
|
export const CodeExportInput = z
|
|
155
125
|
.object({ runId, name: z.string().min(1).max(180).describe("Captured output filename returned by the snapshot.") })
|
|
156
126
|
.strict();
|
|
@@ -179,11 +149,11 @@ export const CodeRuntimeInput = z.discriminatedUnion("operation", [
|
|
|
179
149
|
CodeRunInput.safeExtend({ operation: z.literal("run") }),
|
|
180
150
|
CodeActionInput.safeExtend({ operation: z.literal("action") }),
|
|
181
151
|
CodeInspectInput.extend({ operation: z.literal("inspect") }),
|
|
182
|
-
CodeInteractInput.safeExtend({ operation: z.literal("interact") }),
|
|
183
152
|
CodeStopInput.extend({ operation: z.literal("stop") }),
|
|
184
153
|
CodeOpenInput.extend({ operation: z.literal("open") }),
|
|
185
154
|
CodeSecretInput.extend({ operation: z.literal("secret") }),
|
|
186
|
-
CodePresentInput.
|
|
155
|
+
CodePresentInput.safeExtend({ operation: z.literal("present") }),
|
|
156
|
+
CodeCheckInput.safeExtend({ operation: z.literal("check") }),
|
|
187
157
|
CodeExportInput.extend({ operation: z.literal("export") }),
|
|
188
158
|
]);
|
|
189
159
|
export type CodeRuntimeInput = z.infer<typeof CodeRuntimeInput>;
|
|
@@ -197,12 +167,12 @@ export function parseCodeToolInput(name: string, args: unknown): CodeRuntimeInpu
|
|
|
197
167
|
return { operation: "run", ...CodeRunInput.parse(args) };
|
|
198
168
|
case "code_inspect":
|
|
199
169
|
return { operation: "inspect", ...CodeInspectInput.parse(args) };
|
|
200
|
-
case "code_interact":
|
|
201
|
-
return { operation: "interact", ...CodeInteractInput.parse(args) };
|
|
202
170
|
case "code_stop":
|
|
203
171
|
return { operation: "stop", ...CodeStopInput.parse(args) };
|
|
204
172
|
case "code_open":
|
|
205
173
|
return { operation: "open", ...CodeOpenInput.parse(args) };
|
|
174
|
+
case "code_check":
|
|
175
|
+
return { operation: "check", ...CodeCheckInput.parse(args) };
|
|
206
176
|
case "code_present":
|
|
207
177
|
return { operation: "present", ...CodePresentInput.parse(args) };
|
|
208
178
|
case "code_export":
|
package/src/ai/browser.ts
CHANGED
package/src/ai/capabilities.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import type { CapabilityRegistryEntry } from "../contracts/registry";
|
|
15
15
|
import type { RequestActor } from "../server";
|
|
16
16
|
import { createHelpReader, type HelpReaderFactory } from "../services/help";
|
|
17
|
+
import { type MandatePolicyV1, mandatePolicyCanPermitCapability } from "../services/mandates/policy";
|
|
17
18
|
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
18
19
|
import { recordRejectedAiCapability } from "./capability-execution";
|
|
19
20
|
import { CODE_SOURCE_TOOLS } from "./code-source-contracts";
|
|
@@ -76,7 +77,7 @@ export type AiRememberableCapabilityApprovals = ReadonlyMap<string, string>;
|
|
|
76
77
|
* Why `load_tools` could not make a requested name callable:
|
|
77
78
|
* - `unknown`: no tool has this exact name;
|
|
78
79
|
* - `not_offered_in_turn`: the tool exists, but this turn's client or task does not provide it;
|
|
79
|
-
* - `not_allowed`: the conversation's fixed tool scope
|
|
80
|
+
* - `not_allowed`: the conversation's fixed tool scope or task grants exclude it;
|
|
80
81
|
* - `app_offline`: the app operation is not in the live registry now.
|
|
81
82
|
*/
|
|
82
83
|
export const AI_TOOL_UNAVAILABLE_REASONS = ["unknown", "not_offered_in_turn", "not_allowed", "app_offline"] as const;
|
|
@@ -475,8 +476,8 @@ export const createAiToolMetaTools = (input: {
|
|
|
475
476
|
store: ToolStateStore;
|
|
476
477
|
maxLoadedTools?: number;
|
|
477
478
|
unavailableLoadedNames?: readonly string[];
|
|
478
|
-
/**
|
|
479
|
-
|
|
479
|
+
/** Whether this turn offers app operations, offers none, or its fixed scope or task grants exclude all of them. */
|
|
480
|
+
appOperations?: "not_offered" | "not_allowed" | "offered";
|
|
480
481
|
/** Why a requested name that is not in `catalog` cannot be loaded; defaults to `unknown`. */
|
|
481
482
|
unavailableReason?: (name: string) => AiToolUnavailableReason;
|
|
482
483
|
}): AiRuntimeTool[] => {
|
|
@@ -494,9 +495,11 @@ export const createAiToolMetaTools = (input: {
|
|
|
494
495
|
const liveAppDirectory =
|
|
495
496
|
directoryEntries.length > 0
|
|
496
497
|
? ` Live capability apps: ${directoryEntries.join(", ")}${hiddenAppCount > 0 ? `, and ${hiddenAppCount} more` : ""}.`
|
|
497
|
-
: input.
|
|
498
|
+
: input.appOperations === "not_offered"
|
|
498
499
|
? " App operations are not offered in this turn."
|
|
499
|
-
:
|
|
500
|
+
: input.appOperations === "not_allowed"
|
|
501
|
+
? " This chat's tool scope or task grants allow no app operations. Do not search for app operations in this turn; continue with the available tools or tell the user which grant is missing."
|
|
502
|
+
: " No Cloud app publishes operations right now. Do not search for app operations again in this turn; tell the user which app is not reachable instead of claiming a permanent product limitation.";
|
|
500
503
|
const unavailableLoadedNames = input.unavailableLoadedNames ?? [];
|
|
501
504
|
const unavailableLoadedNotice =
|
|
502
505
|
unavailableLoadedNames.length > 0
|
|
@@ -542,7 +545,7 @@ export const createAiToolMetaTools = (input: {
|
|
|
542
545
|
const load = defineAiTool({
|
|
543
546
|
name: "load_tools",
|
|
544
547
|
description:
|
|
545
|
-
"Load stable capability ids such as mail.conversation.list, or exact built-in names returned by search_tools, as ordinary tools for the next model turn. Skills may name capability ids directly, so load them without searching first. Call each loaded tool by the `call` name returned for it. `unavailable` gives a reason per name: unknown means no tool has this exact name, so look it up once with search_tools instead of guessing; not_offered_in_turn means this client or task does not provide the tool; not_allowed means this chat's fixed tool scope
|
|
548
|
+
"Load stable capability ids such as mail.conversation.list, or exact built-in names returned by search_tools, as ordinary tools for the next model turn. Skills may name capability ids directly, so load them without searching first. Call each loaded tool by the `call` name returned for it. `unavailable` gives a reason per name: unknown means no tool has this exact name, so look it up once with search_tools instead of guessing; not_offered_in_turn means this client or task does not provide the tool; not_allowed means this chat's fixed tool scope or task grants exclude it; app_offline means its app is not reachable now. For the last three, do not search or load the name again in this turn.",
|
|
546
549
|
inputSchema: z.object({ names: z.array(z.string().trim().min(1)).min(1).max(25) }).strict(),
|
|
547
550
|
outputSchema: z
|
|
548
551
|
.object({
|
|
@@ -689,6 +692,8 @@ export const createAiToolResolver =
|
|
|
689
692
|
actor: RequestActor;
|
|
690
693
|
staticTools: AiRuntimeTool[];
|
|
691
694
|
allowedTools?: readonly string[] | null;
|
|
695
|
+
/** Undefined for interactive runs; null fails closed when a background mandate cannot be loaded. */
|
|
696
|
+
mandatePolicy?: MandatePolicyV1 | null;
|
|
692
697
|
/** Built-in tools that exist but that this turn does not offer, such as client tools its client did not declare or tools outside `allowedTools`. */
|
|
693
698
|
unofferedTools?: readonly string[];
|
|
694
699
|
runtimeContext?: Omit<AiToolPreparationContext, "actor" | "conversationId">;
|
|
@@ -725,7 +730,19 @@ export const createAiToolResolver =
|
|
|
725
730
|
}
|
|
726
731
|
const allowed = input.allowedTools == null ? null : new Set(input.allowedTools);
|
|
727
732
|
const fullCapabilityCatalog = buildAiCapabilityCatalog(registry, input.locale);
|
|
728
|
-
const
|
|
733
|
+
const mandateAllows = (entry: AiCapabilityCatalogEntry) =>
|
|
734
|
+
input.mandatePolicy === undefined ||
|
|
735
|
+
(input.mandatePolicy !== null &&
|
|
736
|
+
mandatePolicyCanPermitCapability(input.mandatePolicy, {
|
|
737
|
+
appId: entry.appId,
|
|
738
|
+
capabilityId: entry.operation.localId,
|
|
739
|
+
kind: entry.kind,
|
|
740
|
+
approval: entry.kind === "action" && "approval" in entry.operation ? entry.operation.approval : undefined,
|
|
741
|
+
}));
|
|
742
|
+
const mandateExcluded = new Set(
|
|
743
|
+
fullCapabilityCatalog.filter((entry) => !mandateAllows(entry)).flatMap((entry) => [entry.name, entry.providerName]),
|
|
744
|
+
);
|
|
745
|
+
const capabilityCatalog = fullCapabilityCatalog.filter((entry) => (!allowed || allowed.has(entry.name)) && mandateAllows(entry));
|
|
729
746
|
const helpTools = input.help ? createAiHelpTools(input.help, input.locale) : [];
|
|
730
747
|
const resourceTool =
|
|
731
748
|
input.execute && (capabilityCatalog.length > 0 || input.staticTools.some((tool) => tool.def.name === "code_read"))
|
|
@@ -753,24 +770,33 @@ export const createAiToolResolver =
|
|
|
753
770
|
// App operation IDs always contain a dot; built-in names never do.
|
|
754
771
|
const unavailableReason = (requested: string): AiToolUnavailableReason => {
|
|
755
772
|
const name = loadedByProviderName.get(requested) ?? requested;
|
|
773
|
+
if (mandateExcluded.has(name)) return "not_allowed";
|
|
756
774
|
if (allowed && outOfScope.has(name) && !allowed.has(name)) return "not_allowed";
|
|
757
775
|
if (unoffered.has(name)) return "not_offered_in_turn";
|
|
758
776
|
if (!name.includes(".")) return "unknown";
|
|
759
777
|
if (!input.listRegistry) return "not_offered_in_turn";
|
|
760
778
|
return liveRegistry === null || persistedLoadedNames.includes(name) ? "app_offline" : "unknown";
|
|
761
779
|
};
|
|
780
|
+
// Independent of which apps are live: the fixed scope or task grants name no app operation at all.
|
|
781
|
+
const scopeExcludesAppOperations =
|
|
782
|
+
input.mandatePolicy === null ||
|
|
783
|
+
(input.mandatePolicy?.grants !== undefined && !input.mandatePolicy.grants.some((grant) => "appId" in grant)) ||
|
|
784
|
+
(allowed !== null && ![...allowed].some((name) => name.includes(".")));
|
|
762
785
|
const eagerNames = new Set(builtIns.map((tool) => tool.def.name).filter((name) => !CLOUD_AI_DEFERRED_BUILTIN_TOOL_NAMES.has(name)));
|
|
763
786
|
const activeBuiltIns = builtIns.filter((tool) => eagerNames.has(tool.def.name) || loadedNames.includes(tool.def.name));
|
|
764
787
|
const runtimeTools = [
|
|
765
788
|
...createAiToolMetaTools({
|
|
766
|
-
apps:
|
|
789
|
+
apps:
|
|
790
|
+
allowed || input.mandatePolicy !== undefined
|
|
791
|
+
? registry.filter((app) => capabilityCatalog.some((entry) => entry.appId === app.appId))
|
|
792
|
+
: registry,
|
|
767
793
|
catalog,
|
|
768
794
|
eagerNames,
|
|
769
795
|
conversationId: input.conversationId,
|
|
770
796
|
store: input.store,
|
|
771
797
|
maxLoadedTools: input.maxLoadedTools,
|
|
772
798
|
unavailableLoadedNames,
|
|
773
|
-
|
|
799
|
+
appOperations: !input.listRegistry ? "not_offered" : scopeExcludesAppOperations ? "not_allowed" : "offered",
|
|
774
800
|
unavailableReason,
|
|
775
801
|
}),
|
|
776
802
|
...activeBuiltIns,
|