@k2b/cloud 0.29.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/package.json +4 -2
  2. package/src/_internal/app-presentation.ts +22 -1
  3. package/src/_internal/app-roles.ts +14 -0
  4. package/src/_internal/capabilities.ts +113 -5
  5. package/src/_internal/capability-sentences.ts +413 -0
  6. package/src/_internal/define-app.ts +24 -5
  7. package/src/_internal/registry-validation.ts +5 -0
  8. package/src/_internal/registry.ts +50 -6
  9. package/src/_internal/widget-declarations.ts +53 -0
  10. package/src/access/PermissionEditor.tsx +43 -24
  11. package/src/ai/app-skill-store.ts +298 -0
  12. package/src/ai/app-skills.ts +105 -0
  13. package/src/ai/approval-routes.ts +40 -4
  14. package/src/ai/approvals.ts +68 -10
  15. package/src/ai/browser.ts +10 -0
  16. package/src/ai/capabilities.ts +68 -9
  17. package/src/ai/chart-block.ts +380 -0
  18. package/src/ai/chat/action-sentences.ts +49 -0
  19. package/src/ai/chat/blocks.tsx +97 -40
  20. package/src/ai/chat/chart-block.tsx +79 -0
  21. package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
  22. package/src/ai/chat/message-actions.tsx +3 -1
  23. package/src/ai/chat/message-utils.ts +3 -0
  24. package/src/ai/chat/messages.ts +26 -2
  25. package/src/ai/chat/tool-groups.ts +66 -12
  26. package/src/ai/chat/tool-names.ts +6 -2
  27. package/src/ai/chat/turn-layout.ts +10 -2
  28. package/src/ai/chat/turn-view.tsx +123 -36
  29. package/src/ai/chat/web-tools.tsx +95 -1
  30. package/src/ai/client/controller.ts +6 -3
  31. package/src/ai/code-runtime-tools.ts +109 -61
  32. package/src/ai/code-source-contracts.ts +25 -21
  33. package/src/ai/code-source-tools.ts +3 -1
  34. package/src/ai/default-tool-contracts.ts +198 -0
  35. package/src/ai/default-tools.ts +25 -1
  36. package/src/ai/executor.ts +189 -35
  37. package/src/ai/fetch-file-tool.ts +41 -4
  38. package/src/ai/firecrawl-tools.ts +87 -4
  39. package/src/ai/index.ts +17 -1
  40. package/src/ai/migrate.ts +172 -14
  41. package/src/ai/protocol.ts +22 -1
  42. package/src/ai/routes.ts +11 -0
  43. package/src/ai/runtime-tools.ts +1 -0
  44. package/src/ai/runtime.ts +46 -7
  45. package/src/ai/skill-content.ts +54 -0
  46. package/src/ai/skills-routes.ts +22 -13
  47. package/src/ai/skills.ts +177 -231
  48. package/src/ai/store.ts +66 -1
  49. package/src/ai/tool-audit.ts +11 -1
  50. package/src/ai/tool-sentences.ts +38 -0
  51. package/src/ai/tools.ts +91 -3
  52. package/src/ai/types.ts +47 -1
  53. package/src/ai/web-provenance.ts +103 -0
  54. package/src/api/admin-ai-skills.ts +35 -16
  55. package/src/api/capabilities.ts +3 -2
  56. package/src/api/fanout.ts +128 -0
  57. package/src/api/help.ts +8 -3
  58. package/src/api/mcp.ts +4 -1
  59. package/src/api/search.ts +31 -106
  60. package/src/api/widgets.ts +291 -101
  61. package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
  62. package/src/browser/CloudResourceSearch.tsx +1 -1
  63. package/src/browser/command-shortcuts.ts +21 -6
  64. package/src/browser/ndjson.ts +34 -0
  65. package/src/browser/reload.ts +46 -0
  66. package/src/browser/search-stream.ts +4 -26
  67. package/src/browser/spotlight-position.ts +4 -1
  68. package/src/browser/widgets.ts +30 -0
  69. package/src/capabilities/executions.ts +3 -0
  70. package/src/capabilities/server.ts +20 -2
  71. package/src/cli/access.ts +48 -14
  72. package/src/cli/admin/ai-skills.ts +59 -39
  73. package/src/cli/commands.ts +20 -0
  74. package/src/cli/index.ts +16 -0
  75. package/src/cli/plugin.ts +30 -0
  76. package/src/contracts/announcement-cookie.ts +62 -0
  77. package/src/contracts/announcements.ts +14 -52
  78. package/src/contracts/app.ts +34 -2
  79. package/src/contracts/capabilities.ts +24 -0
  80. package/src/contracts/pwa-paths.ts +7 -2
  81. package/src/contracts/pwa.ts +2 -7
  82. package/src/contracts/registry.ts +10 -1
  83. package/src/contracts/shared.ts +5 -0
  84. package/src/contracts/widgets.ts +67 -2
  85. package/src/desktop/solid.tsx +6 -7
  86. package/src/index.ts +1 -0
  87. package/src/server/help.ts +8 -1
  88. package/src/server/index.ts +1 -0
  89. package/src/server/widget-request.ts +27 -0
  90. package/src/services/accounts/users.ts +15 -0
  91. package/src/services/approvals/index.ts +225 -0
  92. package/src/services/approvals/migrate.ts +33 -0
  93. package/src/services/help/index.ts +18 -5
  94. package/src/services/help/types.ts +3 -2
  95. package/src/services/identity/constants.ts +2 -0
  96. package/src/services/identity/key-ring.ts +123 -42
  97. package/src/services/identity/runtime-config.ts +13 -2
  98. package/src/shared/app-presentation.ts +11 -0
  99. package/src/shared/email-html.ts +43 -8
  100. package/src/ssr/AdminLayout.tsx +4 -7
  101. package/src/ssr/AppLaunchpad.island.tsx +1 -1
  102. package/src/ssr/AppLaunchpadPanel.tsx +12 -3
  103. package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
  104. package/src/ssr/GlobalSearchDialog.tsx +1 -43
  105. package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
  106. package/src/ssr/Layout.tsx +38 -14
  107. package/src/ssr/LayoutFooter.tsx +1 -1
  108. package/src/ssr/LayoutHeader.tsx +17 -20
  109. package/src/ssr/LayoutHelp.tsx +1 -1
  110. package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
  111. package/src/ssr/LayoutRail.tsx +1 -2
  112. package/src/ssr/PageTrail.tsx +23 -0
  113. package/src/ssr/PageTrailRow.island.tsx +20 -0
  114. package/src/ssr/PageTrailRuntime.island.tsx +26 -0
  115. package/src/ssr/ProfilePreferences.island.tsx +7 -3
  116. package/src/ssr/PwaRuntime.island.tsx +1 -1
  117. package/src/ssr/app-navigation.ts +5 -7
  118. package/src/ssr/global-search-host.tsx +55 -0
  119. package/src/ssr/help.ts +3 -3
  120. package/src/ssr/islands/index.ts +1 -0
  121. package/src/ssr/layout-runtime.ts +60 -0
  122. package/src/ssr/mobile-menu-history.ts +1 -1
  123. package/src/ssr/page-trail-runtime.ts +313 -0
  124. package/src/ssr/page-trail.browser-harness.tsx +216 -0
  125. package/src/ssr/page-trail.ts +59 -0
  126. package/src/ssr/platform-messages.ts +4 -0
  127. package/src/ssr/rail-context.ts +4 -3
  128. package/src/ssr/runtime.ts +14 -1
  129. package/src/styles/effects.css +21 -4
  130. package/src/styles/global.css +35 -2
  131. package/src/styles/utilities-layout.css +24 -1
  132. package/src/styles/utilities-navigation.css +6 -18
  133. package/src/ai/code-mode-skill.ts +0 -114
  134. package/src/ai/data-analysis-skill.ts +0 -10
  135. package/src/ai/grids-skill.ts +0 -158
  136. package/src/ai/skill-seeds.ts +0 -560
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k2b/cloud",
3
- "version": "0.29.1",
3
+ "version": "0.31.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": {
@@ -38,6 +38,7 @@
38
38
  "./browser/resource-clipboard": "./src/browser/resource-clipboard.ts",
39
39
  "./browser/commands": "./src/browser/commands.ts",
40
40
  "./browser/search": "./src/browser/search.ts",
41
+ "./browser/widgets": "./src/browser/widgets.ts",
41
42
  "./browser/testing": "./src/browser/testing.ts",
42
43
  "./browser/resource-picker": "./src/browser/resource-picker.tsx",
43
44
  "./browser/files": "./src/browser/files.tsx",
@@ -72,6 +73,7 @@
72
73
  "./ssr": "./src/ssr/index.ts",
73
74
  "./ssr/islands": "./src/ssr/islands/index.ts",
74
75
  "./ssr/WorkspaceNavigation.island": "./src/ssr/WorkspaceNavigation.island.tsx",
76
+ "./ssr/layout-runtime": "./src/ssr/layout-runtime.ts",
75
77
  "./ssr/*": "./src/ssr/*",
76
78
  "./config": "./src/config/index.ts",
77
79
  "./config/*": "./src/config/*",
@@ -103,7 +105,7 @@
103
105
  "@tailwindcss/typography": "0.5.20",
104
106
  "@k2b/nessi": "0.17.0",
105
107
  "@k2b/ssr": "0.15.0",
106
- "@k2b/ui": "0.20.0",
108
+ "@k2b/ui": "0.22.0",
107
109
  "@k2b/stdlib": "0.28.0",
108
110
  "@k2b/sync": "7.0.0",
109
111
  "@nats-io/jetstream": "3.4.0",
@@ -1,4 +1,5 @@
1
- import type { AppMeta, AppPresentationCatalog, AppPresentationTranslation } from "../contracts/app";
1
+ import type { AppMeta, AppPresentationCatalog, AppPresentationTranslation, AppWidgetTranslation } from "../contracts/app";
2
+ import { WIDGET_DESCRIPTION_MAX_LENGTH, WIDGET_TITLE_MAX_LENGTH } from "../contracts/widgets";
2
3
  import { canonicalLocale } from "../shared/locale";
3
4
 
4
5
  export const APP_PRESENTATION_MAX_BYTES = 64 * 1024;
@@ -21,6 +22,24 @@ const labels = (values: Readonly<Record<string, string>> | undefined, allowed: S
21
22
  return normalized;
22
23
  };
23
24
 
25
+ const widgetTexts = (
26
+ values: Readonly<Record<string, AppWidgetTranslation>>,
27
+ allowed: Set<string>,
28
+ label: string,
29
+ ): Record<string, AppWidgetTranslation> => {
30
+ const normalized: Record<string, AppWidgetTranslation> = {};
31
+ for (const [id, value] of Object.entries(values)) {
32
+ if (!allowed.has(id)) throw new Error(`${label} contains unknown key ${JSON.stringify(id)}`);
33
+ normalized[id] = {
34
+ ...(value.title !== undefined ? { title: text(value.title, `${label}.${id}.title`, WIDGET_TITLE_MAX_LENGTH) } : {}),
35
+ ...(value.description !== undefined
36
+ ? { description: text(value.description, `${label}.${id}.description`, WIDGET_DESCRIPTION_MAX_LENGTH) }
37
+ : {}),
38
+ };
39
+ }
40
+ return normalized;
41
+ };
42
+
24
43
  /** Validate and canonicalize one static app-presentation catalog before it enters the registry. */
25
44
  export const compileAppPresentation = (app: AppMeta, catalog: AppPresentationCatalog | undefined): AppPresentationCatalog | undefined => {
26
45
  if (!catalog) return undefined;
@@ -36,6 +55,7 @@ export const compileAppPresentation = (app: AppMeta, catalog: AppPresentationCat
36
55
  const adminHrefs = new Set((app.adminNav ?? []).flatMap((group) => group.links.map((link) => link.href)));
37
56
  const searchHrefs = new Set((app.searchLinks ?? []).map((link) => link.href));
38
57
  const legalHrefs = new Set((app.legalLinks ?? []).map((link) => link.href));
58
+ const widgetIds = new Set((app.widgets ?? []).map((widget) => widget.id));
39
59
  const translations: Record<string, AppPresentationTranslation> = {};
40
60
 
41
61
  for (const [locale, translation] of Object.entries(catalog.translations)) {
@@ -62,6 +82,7 @@ export const compileAppPresentation = (app: AppMeta, catalog: AppPresentationCat
62
82
  }
63
83
  : {}),
64
84
  ...(translation.legalLinks ? { legalLinks: labels(translation.legalLinks, legalHrefs, `${canonical}.legalLinks`) } : {}),
85
+ ...(translation.widgets ? { widgets: widgetTexts(translation.widgets, widgetIds, `${canonical}.widgets`) } : {}),
65
86
  };
66
87
  }
67
88
 
@@ -0,0 +1,14 @@
1
+ // Types only: the shell evaluates this on every page and must not load the account schemas.
2
+ import type { AppRegistryNav } from "../contracts/registry";
3
+ import type { Role, User } from "../contracts/shared";
4
+
5
+ /** An app's declared roles: absent roles admit everyone, and `guest` matches the guest profile. */
6
+ export const hasAnyAppRole = (user: User | undefined, roles: readonly Role[] | undefined): boolean =>
7
+ !roles || (!!user && roles.some((role) => (role === "guest" ? user.profile === "guest" : user.roles.includes(role))));
8
+
9
+ /**
10
+ * Who may see an app's Help and Skills: its navigation roles, or administrators for an app reached only through the
11
+ * admin area (`adminHref` without its own `href`). `undefined` admits every signed-in user.
12
+ */
13
+ export const appAudienceRoles = (nav: AppRegistryNav | undefined): readonly Role[] | undefined =>
14
+ nav?.requiresRoles ?? (nav && !nav.href && nav.adminHref ? ["admin"] : undefined);
@@ -10,6 +10,7 @@ import {
10
10
  type CapabilityActionManifest,
11
11
  type CapabilityActionReviewResult,
12
12
  CapabilityActionReviewSchema,
13
+ type CapabilityActionSentences,
13
14
  type CapabilityCommandDefinition,
14
15
  type CapabilityCommandManifest,
15
16
  CapabilityCommandManifestSchema,
@@ -38,6 +39,14 @@ import {
38
39
  } from "../contracts/capabilities";
39
40
  import { fileProviderIssues } from "../contracts/file-provider";
40
41
  import { canonicalLocale, localeFallbackChain, normalizeLocale } from "../shared/locale";
42
+ import {
43
+ CAPABILITY_SENTENCE_KEYS,
44
+ CAPABILITY_SENTENCE_MAX_CHARS,
45
+ type CapabilityActionWording,
46
+ capabilitySentenceFields,
47
+ parseCapabilitySentence,
48
+ resolveCapabilityActionSentences,
49
+ } from "./capability-sentences";
41
50
 
42
51
  type JsonSchema = Record<string, unknown>;
43
52
  const MAX_CAPABILITY_MANIFEST_BYTES = 256 * 1024;
@@ -274,6 +283,56 @@ const compileSchemaPresentation = (
274
283
  return Object.fromEntries(entries);
275
284
  };
276
285
 
286
+ const isAction = (
287
+ operation: CapabilityQueryManifest | CapabilityActionManifest | CapabilityCommandManifest,
288
+ ): operation is CapabilityActionManifest => "idempotency" in operation;
289
+
290
+ /**
291
+ * Checks one Action's sentences against its schemas: every placeholder names a field the Action declares,
292
+ * and only `done` reads result data, because the other sentences are shown before or instead of a result.
293
+ */
294
+ export const compileActionSentences = (
295
+ value: unknown,
296
+ operation: Pick<CapabilityActionManifest, "inputSchema" | "dataSchema">,
297
+ label: string,
298
+ strict = true,
299
+ ): CapabilityActionSentences => {
300
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
301
+ const copy = value as Record<string, unknown>;
302
+ const extra = Object.keys(copy).find((key) => !(CAPABILITY_SENTENCE_KEYS as readonly string[]).includes(key));
303
+ if (strict && extra) throw new Error(`${label} contains unsupported field "${extra}"`);
304
+ const paths = { input: schemaFieldPaths(operation.inputSchema), data: schemaFieldPaths(operation.dataSchema) };
305
+ const sentences: Record<string, string> = {};
306
+ for (const key of CAPABILITY_SENTENCE_KEYS) {
307
+ const template = copy[key];
308
+ if (template === undefined) continue;
309
+ if (typeof template !== "string") throw new Error(`${label}.${key} must be text`);
310
+ assertText(template, `${label}.${key}`, CAPABILITY_SENTENCE_MAX_CHARS);
311
+ const parsed = parseCapabilitySentence(template.trim());
312
+ if ("error" in parsed) throw new Error(`${label}.${key} ${parsed.error}`);
313
+ for (const part of parsed.parts) {
314
+ if (typeof part === "string") continue;
315
+ if (part.source === "data" && key !== "done") throw new Error(`${label}.${key} may read {data.${part.path}} only in done`);
316
+ if (!paths[part.source].has(part.path)) {
317
+ throw new Error(`${label}.${key} placeholder {${part.source}.${part.path}} does not exist in the projected ${part.source} schema`);
318
+ }
319
+ }
320
+ sentences[key] = template.trim();
321
+ }
322
+ if (Object.keys(sentences).length === 0) throw new Error(`${label} needs at least one sentence`);
323
+ return sentences;
324
+ };
325
+
326
+ /** A reader keeps an entry's other copy when only its sentences come from a newer or broken producer. */
327
+ const readActionSentences = (value: unknown, operation: CapabilityActionManifest, label: string, strict: boolean) => {
328
+ if (strict) return compileActionSentences(value, operation, label, strict);
329
+ try {
330
+ return compileActionSentences(value, operation, label, strict);
331
+ } catch {
332
+ return undefined;
333
+ }
334
+ };
335
+
277
336
  const compileOperationPresentation = (
278
337
  value: unknown,
279
338
  operation: CapabilityQueryManifest | CapabilityActionManifest | CapabilityCommandManifest,
@@ -283,7 +342,9 @@ const compileOperationPresentation = (
283
342
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
284
343
  const translation = value as Record<string, unknown>;
285
344
  const allowed = new Set(
286
- "path" in operation ? ["title", "description", "input"] : ["title", "description", "input", "data", "searchTags"],
345
+ "path" in operation
346
+ ? ["title", "description", "input"]
347
+ : ["title", "description", "input", "data", "searchTags", ...(isAction(operation) ? ["sentences"] : [])],
287
348
  );
288
349
  const extra = Object.keys(translation).find((key) => !allowed.has(key));
289
350
  if (strict && extra) throw new Error(`${label} contains unsupported field "${extra}"`);
@@ -330,12 +391,17 @@ const compileOperationPresentation = (
330
391
  }
331
392
  const input = compileSchemaPresentation(translation.input, operation.inputSchema, `${label}.input`);
332
393
  const data = "dataSchema" in operation ? compileSchemaPresentation(translation.data, operation.dataSchema, `${label}.data`) : undefined;
394
+ const sentences =
395
+ translation.sentences !== undefined && isAction(operation)
396
+ ? readActionSentences(translation.sentences, operation, `${label}.sentences`, strict)
397
+ : undefined;
333
398
  return {
334
399
  ...(typeof translation.title === "string" ? { title: translation.title.trim() } : {}),
335
400
  ...(typeof translation.description === "string" ? { description: translation.description.trim() } : {}),
336
401
  ...(input ? { input } : {}),
337
402
  ...(data ? { data } : {}),
338
403
  ...(searchTags ? { searchTags } : {}),
404
+ ...(sentences ? { sentences } : {}),
339
405
  };
340
406
  };
341
407
 
@@ -353,7 +419,7 @@ export const compileCapabilityPresentation = (
353
419
  if (value === undefined) return undefined;
354
420
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Capability presentation must be an object");
355
421
  const catalog = value as Record<string, unknown>;
356
- if (strict && Object.keys(catalog).some((key) => key !== "baseLocale" && key !== "translations")) {
422
+ if (strict && Object.keys(catalog).some((key) => key !== "baseLocale" && key !== "sentences" && key !== "translations")) {
357
423
  throw new Error("Capability presentation contains unsupported fields");
358
424
  }
359
425
  const baseLocale = typeof catalog.baseLocale === "string" ? canonicalLocale(catalog.baseLocale) : undefined;
@@ -365,6 +431,23 @@ export const compileCapabilityPresentation = (
365
431
  const queries = new Map(manifest.queries.map((operation) => [operation.localId, operation]));
366
432
  const actions = new Map(manifest.actions.map((operation) => [operation.localId, operation]));
367
433
  const commands = new Map(manifest.commands.map((operation) => [operation.localId, operation]));
434
+ let sentences: Record<string, CapabilityActionSentences> | undefined;
435
+ if (catalog.sentences !== undefined) {
436
+ if (!catalog.sentences || typeof catalog.sentences !== "object" || Array.isArray(catalog.sentences)) {
437
+ throw new Error("Capability presentation sentences must be an object");
438
+ }
439
+ sentences = Object.fromEntries(
440
+ Object.entries(catalog.sentences as Record<string, unknown>).flatMap(([localId, entry]) => {
441
+ const action = actions.get(localId);
442
+ if (!action) {
443
+ if (strict) throw new Error(`Action sentences reference unknown localId "${localId}"`);
444
+ return [];
445
+ }
446
+ const compiled = readActionSentences(entry, action, `Action ${localId}.sentences`, strict);
447
+ return compiled ? [[localId, compiled]] : [];
448
+ }),
449
+ );
450
+ }
368
451
  const translations: Record<string, CapabilityPresentationTranslation> = {};
369
452
  for (const [locale, rawTranslation] of Object.entries(catalog.translations as Record<string, unknown>)) {
370
453
  const canonical = canonicalLocale(locale);
@@ -431,7 +514,7 @@ export const compileCapabilityPresentation = (
431
514
  ...(translatedCommands ? { commands: translatedCommands } : {}),
432
515
  };
433
516
  }
434
- return { baseLocale, translations };
517
+ return { baseLocale, ...(sentences && Object.keys(sentences).length > 0 ? { sentences } : {}), translations };
435
518
  };
436
519
 
437
520
  const projectSchema = (schema: z.ZodType, label: string, io: "input" | "output"): JsonSchema => {
@@ -798,6 +881,31 @@ export const resolveCapabilityOperationTitle = (
798
881
  operation.title,
799
882
  );
800
883
 
884
+ /**
885
+ * Everything a reader needs to word calls of one Action in the requested locale: its title, the app's
886
+ * sentences, and the labelled fields of the generic sentence. Derived only from the live manifest and
887
+ * presentation catalog, the same way for every app.
888
+ */
889
+ export const resolveCapabilityActionWording = (
890
+ operation: CapabilityActionManifest,
891
+ catalog: CapabilityPresentationCatalog | undefined,
892
+ requestedLocale: string,
893
+ ): CapabilityActionWording => {
894
+ const overlays = catalog ? presentationOverlays(catalog, requestedLocale) : [];
895
+ const inputDescriptions = Object.assign({}, ...overlays.map((translation) => translation.actions?.[operation.localId]?.input ?? {}));
896
+ const sentences = resolveCapabilityActionSentences(operation.localId, catalog, requestedLocale);
897
+ return {
898
+ title: resolveCapabilityOperationTitle(operation, "actions", catalog, requestedLocale),
899
+ ...(sentences ? { sentences } : {}),
900
+ fields: capabilitySentenceFields({
901
+ inputSchema: operation.inputSchema,
902
+ dataSchema: operation.dataSchema,
903
+ sentences,
904
+ inputDescriptions,
905
+ }),
906
+ };
907
+ };
908
+
801
909
  /** Resolve only human presentation; stable IDs, flags, tags, aliases, and data shapes stay untouched. */
802
910
  export const resolveCapabilityManifestPresentation = (
803
911
  manifest: CapabilityManifest,
@@ -1361,9 +1469,9 @@ export const reviewCompiledCapability = async (params: {
1361
1469
  return normalizeProviderError(reviewed.error, "Capability review returned an invalid error", params.onUnexpectedError);
1362
1470
  }
1363
1471
  const parsed = CapabilityActionReviewSchema.safeParse(reviewed.data);
1472
+ // A rememberable Action may leave out its scope for arguments that should ask every time; any other Action has none.
1364
1473
  const approvalScopeIsValid =
1365
- parsed.success &&
1366
- (operation.manifest.approval === "rememberable" ? parsed.data.approvalScope !== undefined : parsed.data.approvalScope === undefined);
1474
+ parsed.success && (operation.manifest.approval === "rememberable" || parsed.data.approvalScope === undefined);
1367
1475
  if (!parsed.success || !approvalScopeIsValid) {
1368
1476
  try {
1369
1477
  params.onUnexpectedError?.(