@k2b/cloud 0.7.0 → 0.9.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 (190) hide show
  1. package/package.json +5 -4
  2. package/scripts/README.md +10 -0
  3. package/scripts/build.ts +14 -1
  4. package/scripts/preload.ts +7 -0
  5. package/scripts/sync-recovery-smoke.ts +21 -6
  6. package/src/_internal/build-metadata.ts +8 -1
  7. package/src/_internal/capabilities.ts +35 -12
  8. package/src/_internal/define-app.ts +28 -11
  9. package/src/_internal/page-responses.ts +1 -1
  10. package/src/_internal/readiness.ts +4 -1
  11. package/src/_internal/runtime-context.ts +1 -1
  12. package/src/access/PermissionEditor.tsx +15 -9
  13. package/src/access/PrincipalPicker.tsx +9 -3
  14. package/src/access/ResourceApiKeys.tsx +1 -1
  15. package/src/access/ui.ts +1 -2
  16. package/src/ai/admin.ts +2 -2
  17. package/src/ai/assistant-models.ts +1 -1
  18. package/src/ai/audio-tool.ts +1 -1
  19. package/src/ai/browser-code-contracts.ts +174 -52
  20. package/src/ai/browser.ts +4 -2
  21. package/src/ai/capabilities.ts +3 -3
  22. package/src/ai/chat/blocks.tsx +127 -50
  23. package/src/ai/chat/builtin-tools.tsx +2 -4
  24. package/src/ai/chat/capability-table.tsx +1 -1
  25. package/src/ai/chat/composer-adapter.ts +48 -13
  26. package/src/ai/chat/message-utils.ts +18 -5
  27. package/src/ai/chat/presentation.tsx +24 -11
  28. package/src/ai/chat/tool-disclosure.tsx +6 -2
  29. package/src/ai/chat/tool-groups.ts +8 -2
  30. package/src/ai/chat-quotas.ts +2 -2
  31. package/src/ai/chat-task-contracts.ts +9 -13
  32. package/src/ai/chat-tasks.ts +39 -18
  33. package/src/ai/client/controller.ts +87 -38
  34. package/src/ai/client/projection.ts +13 -6
  35. package/src/ai/client/transport.ts +4 -2
  36. package/src/ai/code-capability-routes.ts +22 -27
  37. package/src/ai/code-capability-transport.ts +2 -1
  38. package/src/ai/code-execution.ts +53 -0
  39. package/src/ai/code-mode-skill.ts +9 -5
  40. package/src/ai/code-runtime-tools.ts +16 -3
  41. package/src/ai/code-source-contracts.ts +100 -32
  42. package/src/ai/code-source-tools.ts +49 -6
  43. package/src/ai/compaction.ts +3 -3
  44. package/src/ai/default-tools.ts +87 -14
  45. package/src/ai/dictation-runtime.ts +1 -1
  46. package/src/ai/draft-content.ts +27 -12
  47. package/src/ai/enrich.ts +1 -1
  48. package/src/ai/executor.ts +8 -12
  49. package/src/ai/fetch-file-tool.ts +2 -2
  50. package/src/ai/file-content-version.ts +6 -1
  51. package/src/ai/file-tools.ts +8 -2
  52. package/src/ai/files-store.ts +55 -17
  53. package/src/ai/grids-skill.ts +2 -0
  54. package/src/ai/http.ts +18 -7
  55. package/src/ai/index.ts +24 -25
  56. package/src/ai/inference-calls.ts +3 -4
  57. package/src/ai/live.ts +1 -1
  58. package/src/ai/markdown-pdf-tool.ts +2 -2
  59. package/src/ai/memories.ts +30 -27
  60. package/src/ai/memory-learning-runs.ts +1 -1
  61. package/src/ai/memory-learning.ts +5 -5
  62. package/src/ai/memory-tool.ts +1 -5
  63. package/src/ai/memory-workflow-evidence.ts +9 -4
  64. package/src/ai/message-queue.ts +23 -12
  65. package/src/ai/migrate.ts +6 -4
  66. package/src/ai/model-pricing.ts +4 -4
  67. package/src/ai/pdf-render-worker.ts +3 -2
  68. package/src/ai/projects-routes.ts +15 -40
  69. package/src/ai/projects.ts +18 -13
  70. package/src/ai/quota-provider.ts +6 -5
  71. package/src/ai/quota-report.ts +5 -5
  72. package/src/ai/quotas.ts +3 -3
  73. package/src/ai/routes.ts +60 -34
  74. package/src/ai/run-timeout.ts +3 -1
  75. package/src/ai/runtime-tools.ts +6 -7
  76. package/src/ai/runtime.ts +38 -20
  77. package/src/ai/settings.ts +1 -1
  78. package/src/ai/short-id.ts +4 -5
  79. package/src/ai/skill-search.ts +21 -10
  80. package/src/ai/skill-seeds.ts +16 -7
  81. package/src/ai/skill-tool.ts +6 -1
  82. package/src/ai/skills-routes.ts +32 -15
  83. package/src/ai/skills.ts +54 -28
  84. package/src/ai/store.ts +80 -47
  85. package/src/ai/structured.ts +6 -2
  86. package/src/ai/system-prompt.ts +7 -5
  87. package/src/ai/task-contracts.ts +84 -0
  88. package/src/ai/task-execution.ts +92 -0
  89. package/src/ai/todo-contracts.ts +24 -12
  90. package/src/ai/todo-tool.ts +16 -13
  91. package/src/ai/tools.ts +1 -1
  92. package/src/ai/transcription.ts +4 -4
  93. package/src/ai/turn-timing.ts +1 -1
  94. package/src/ai/ui.tsx +2 -1
  95. package/src/ai/usage.ts +1 -1
  96. package/src/ai/vision-tool.ts +38 -17
  97. package/src/api/admin-ai-quotas.ts +14 -8
  98. package/src/api/admin-ai-skills.ts +3 -3
  99. package/src/api/admin-linux-identities.ts +10 -4
  100. package/src/api/admin-rail.ts +2 -2
  101. package/src/api/app-approval.ts +2 -2
  102. package/src/api/auth/schemas.ts +7 -1
  103. package/src/api/auth.ts +4 -3
  104. package/src/api/capabilities.ts +45 -13
  105. package/src/api/index.ts +5 -5
  106. package/src/api/mcp.ts +5 -12
  107. package/src/browser/CloudResourceSearch.tsx +1 -1
  108. package/src/browser/command-bridge.ts +1 -1
  109. package/src/browser/command-shortcuts.ts +1 -1
  110. package/src/browser/commands.ts +8 -7
  111. package/src/browser/testing.ts +2 -1
  112. package/src/capabilities/client.ts +1 -1
  113. package/src/capabilities/command-link.ts +1 -1
  114. package/src/capabilities/server.ts +2 -1
  115. package/src/cli/admin/ai-quotas.ts +5 -5
  116. package/src/cli/admin/app-credentials.ts +7 -9
  117. package/src/cli/admin/index.ts +3 -3
  118. package/src/cli/admin/instance.ts +2 -2
  119. package/src/cli/admin/linux.ts +4 -2
  120. package/src/cli/admin/sync.ts +1 -1
  121. package/src/cli/capabilities.ts +42 -29
  122. package/src/config/define-env.ts +97 -0
  123. package/src/config/env.ts +147 -54
  124. package/src/config/index.ts +3 -1
  125. package/src/contracts/capabilities.ts +8 -1
  126. package/src/contracts/index.ts +5 -7
  127. package/src/contracts/registry.ts +2 -0
  128. package/src/contracts/shared.ts +12 -3
  129. package/src/desktop/index.ts +1 -0
  130. package/src/desktop/solid.tsx +1 -1
  131. package/src/index.ts +8 -2
  132. package/src/server/app-assets.ts +27 -0
  133. package/src/server/index.ts +3 -2
  134. package/src/server/ratelimit.ts +1 -1
  135. package/src/server/services/access-revision.ts +3 -3
  136. package/src/server/services/access.ts +3 -1
  137. package/src/services/accounts/notification-sender.ts +12 -2
  138. package/src/services/announcements/index.ts +4 -4
  139. package/src/services/app-approval.ts +11 -10
  140. package/src/services/auth-flows/magic-link.ts +13 -1
  141. package/src/services/identity/key-config.ts +6 -4
  142. package/src/services/identity/runtime-config.ts +6 -5
  143. package/src/services/ipa/posix.ts +1 -1
  144. package/src/services/ipa/sync.ts +1 -1
  145. package/src/services/logging/index.ts +1 -1
  146. package/src/services/logging/trace.ts +1 -1
  147. package/src/services/mandates/index.ts +5 -2
  148. package/src/services/mandates/policy.ts +106 -6
  149. package/src/services/notifications/batches.ts +1 -0
  150. package/src/services/pdf/gotenberg.ts +50 -14
  151. package/src/services/pdf/index.ts +11 -11
  152. package/src/services/public-http.ts +1 -1
  153. package/src/services/rail-shortcuts.ts +1 -1
  154. package/src/services/rail-snapshot.ts +1 -1
  155. package/src/services/request-cache-redis.ts +2 -1
  156. package/src/services/session/user.ts +1 -1
  157. package/src/services/settings/app.ts +5 -1
  158. package/src/services/settings/core-settings.ts +6 -8
  159. package/src/services/settings/index.ts +3 -3
  160. package/src/services/settings/store.ts +3 -3
  161. package/src/services/webauthn.ts +1 -1
  162. package/src/shared/index.ts +3 -6
  163. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  164. package/src/ssr/AdminSidebar.tsx +2 -2
  165. package/src/ssr/AppLaunchpad.island.tsx +12 -2
  166. package/src/ssr/AppLaunchpadPanel.tsx +4 -4
  167. package/src/ssr/GlobalSearchDialog.tsx +6 -6
  168. package/src/ssr/GlobalSearchTrigger.island.tsx +2 -2
  169. package/src/ssr/Layout.tsx +4 -3
  170. package/src/ssr/LayoutHeader.tsx +1 -1
  171. package/src/ssr/LayoutHelp.tsx +4 -6
  172. package/src/ssr/LayoutHelpTrigger.island.tsx +2 -2
  173. package/src/ssr/LayoutRail.tsx +3 -2
  174. package/src/ssr/MinimalLayout.tsx +1 -1
  175. package/src/ssr/MobileNavigation.tsx +2 -2
  176. package/src/ssr/ProfilePreferences.island.tsx +1 -1
  177. package/src/ssr/RailApps.island.tsx +7 -7
  178. package/src/ssr/admin-navigation.ts +6 -2
  179. package/src/ssr/index.ts +1 -0
  180. package/src/ssr/islands/index.ts +2 -2
  181. package/src/ssr/mobile-menu-history.ts +1 -1
  182. package/src/ssr/rail-navigation.ts +3 -2
  183. package/src/ssr/request-path.ts +12 -0
  184. package/src/workflows/ai/index.ts +2 -2
  185. package/src/workflows/ai/runtime.ts +16 -91
  186. package/src/workflows/ai/store.ts +1 -1
  187. package/src/workflows/ai/types.ts +6 -82
  188. package/src/workflows/store/index.ts +2 -2
  189. package/src/server/services/freeipa/test-certificates.ts +0 -44
  190. package/src/services/session/test-fixture.ts +0 -18
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@k2b/cloud",
3
- "version": "0.7.0",
4
- "description": "Modular Hono+SolidJS framework for building per-app docker services behind a dynamic gateway. Powers cloud.stuve-ulm.de.",
3
+ "version": "0.9.0",
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": {
7
7
  "type": "git",
@@ -18,6 +18,7 @@
18
18
  "!src/**/*.test.ts",
19
19
  "!src/**/*.test.tsx",
20
20
  "!src/**/*.integration.test.ts",
21
+ "!src/**/*.test-fixture.ts",
21
22
  "!scripts/**/*.test.ts",
22
23
  "!scripts/runtime-recovery-*.ts"
23
24
  ],
@@ -85,7 +86,7 @@
85
86
  "scripts": {
86
87
  "test": "bun test --isolate --path-ignore-patterns '**/*.behavior.test.tsx' --path-ignore-patterns '**/*.dom.test.tsx' && bun run test:dom",
87
88
  "test:dom": "bun test --isolate --conditions=browser --preload ../ui/test/solid-dom-preload.ts behavior.test.tsx dom.test.tsx",
88
- "typecheck": "tsc -p tsconfig.json --noEmit --pretty false 2>&1 | grep -v node_modules | (! grep -q 'error TS')"
89
+ "typecheck": "tsc -p tsconfig.json --noEmit --pretty false"
89
90
  },
90
91
  "dependencies": {
91
92
  "@firecrawl/anydoc": "0.1.9",
@@ -97,7 +98,7 @@
97
98
  "@tailwindcss/typography": "0.5.20",
98
99
  "@k2b/nessi": "0.12.0",
99
100
  "@k2b/ssr": "0.14.0",
100
- "@k2b/ui": "0.3.0",
101
+ "@k2b/ui": "0.4.1",
101
102
  "@k2b/stdlib": "0.25.0",
102
103
  "@k2b/sync": "6.4.0",
103
104
  "@nats-io/transport-node": "3.4.0",
@@ -0,0 +1,10 @@
1
+ # Cloud platform scripts
2
+
3
+ - `build.ts` — production build for one Cloud app (`APP_ID`); used by the Dockerfiles and `bun run build` targets.
4
+ - `preload.ts` — dev-mode preload that registers the SSR plugin and builds CSS before app code loads.
5
+ - `build-pdf-renderer.ts` — packages the PDF decoder and native assets for bundles that import PDF vision.
6
+ - `app-favicon.ts` — generates app favicons from Tabler icons; imported by `build.ts` and `preload.ts`.
7
+ - `browser-performance.ts` — bundles the web-vitals browser asset for core; imported by `build.ts` and `preload.ts`.
8
+ - `runtime-recovery-acceptance.ts` — shared-runtime recovery acceptance on disposable Docker containers: `bun packages/cloud/scripts/runtime-recovery-acceptance.ts` (also `bun run test:runtime-recovery`).
9
+ - `runtime-recovery-worker.ts` — the containerized fixture started by `runtime-recovery-acceptance.ts`; not run directly.
10
+ - `sync-recovery-smoke.ts` — two-phase NATS recovery smoke: `bun packages/cloud/scripts/sync-recovery-smoke.ts prepare --namespace cloud-recovery-smoke-<unique>`, restart the fleet, then `... recover --namespace <same>`.
package/scripts/build.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  * _ssr/<island>.js hydration bundles (auto-emitted by the SSR plugin)
18
18
  * public/<id>/app.css Tailwind, if the app has src/styles/app.css
19
19
  * public/<id>/... anything from <appDir>/public/
20
+ * assets/... anything from <appDir>/src/assets/ (appAssetPath)
20
21
  *
21
22
  * If the app needs additional artefacts (core's global.css, logo, katex),
22
23
  * it ships a `scripts/build-extras.ts` that this script runs at the end.
@@ -47,6 +48,10 @@ const release = process.env.CLOUD_RELEASE?.trim() || "local";
47
48
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(release)) {
48
49
  throw new Error(`Invalid CLOUD_RELEASE: ${JSON.stringify(release)}`);
49
50
  }
51
+ const version = process.env.CLOUD_VERSION?.trim() || "0.0.0-local";
52
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
53
+ throw new Error(`Invalid CLOUD_VERSION: ${JSON.stringify(version)}`);
54
+ }
50
55
  const syncPackagePath = Bun.resolveSync("@k2b/sync/package.json", frameworkDir);
51
56
  const syncPackage = JSON.parse(await readFile(syncPackagePath, "utf8")) as {
52
57
  version?: unknown;
@@ -121,9 +126,11 @@ try {
121
126
  minify: true,
122
127
  metafile: true,
123
128
  define: {
129
+ __CLOUD_VERSION__: JSON.stringify(version),
124
130
  __CLOUD_RELEASE__: JSON.stringify(release),
125
131
  __CLOUD_SYNC_VERSION__: JSON.stringify(syncVersion),
126
132
  __CLOUD_PDF_RENDER_WORKER__: JSON.stringify("./pdf-render/worker.js"),
133
+ __CLOUD_APP_ASSETS__: JSON.stringify("./assets/"),
127
134
  },
128
135
  plugins: [(app?.plugin ?? plugin)()],
129
136
  });
@@ -134,7 +141,7 @@ if (!server.success) {
134
141
  for (const m of server.logs) console.error(m);
135
142
  throw new Error("Server bundle failed");
136
143
  }
137
- if (Object.keys(server.metafile?.inputs ?? {}).some(path => path.endsWith("/ai/pdf-render.ts"))) {
144
+ if (Object.keys(server.metafile?.inputs ?? {}).some((path) => path.endsWith("/ai/pdf-render.ts"))) {
138
145
  await buildPdfRenderer(dist);
139
146
  }
140
147
 
@@ -162,6 +169,12 @@ if (existsSync(appPublic)) {
162
169
  await cp(appPublic, resolve(distPublic, appId), { recursive: true });
163
170
  }
164
171
 
172
+ // Server-side assets the bundle reads through appAssetPath().
173
+ const appAssets = resolve(appDir, "src/assets");
174
+ if (existsSync(appAssets)) {
175
+ await cp(appAssets, resolve(dist, "assets"), { recursive: true });
176
+ }
177
+
165
178
  if (appId !== "core" && app) {
166
179
  await writeAppFavicon({
167
180
  publicDir: distPublic,
@@ -132,6 +132,13 @@ if (appId === "core" && existsSync(globalCssEntry)) {
132
132
  const appPublicDir = resolve(publicDir, appId);
133
133
  await mkdir(appPublicDir, { recursive: true });
134
134
 
135
+ // Static assets from <appDir>/public, same as step 3 of the production build.
136
+ const appPublicSource = resolve(appDir, "public");
137
+ // A standalone application's public directory can be the serving root itself; never copy a tree into itself.
138
+ if (existsSync(appPublicSource) && appPublicSource !== publicDir && !appPublicDir.startsWith(`${appPublicSource}/`)) {
139
+ await cp(appPublicSource, appPublicDir, { recursive: true });
140
+ }
141
+
135
142
  if (appId !== "core" && app) {
136
143
  await writeAppFavicon({
137
144
  publicDir,
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * Isolated two-process recovery acceptance for the local NATS cluster.
3
3
  *
4
- * SYNC_RECOVERY_NAMESPACE=cloud-recovery-smoke-<unique> bun packages/cloud/scripts/sync-recovery-smoke.ts prepare
4
+ * bun packages/cloud/scripts/sync-recovery-smoke.ts prepare --namespace cloud-recovery-smoke-<unique>
5
5
  * Stop the application fleet across the printed nextTick boundary, keeping NATS up.
6
6
  * Restart the fleet, then run the same command with `recover`.
7
7
  * No provider or application-domain effects; successful recovery deletes only
8
8
  * the exact test streams after checking their broker identity metadata.
9
9
  */
10
- import { env } from "../src/config/env";
10
+
11
+ import { parseArgs } from "node:util";
11
12
  import { createSync, type SyncResourceSummary, type Worker } from "@k2b/sync";
12
13
  import { connect, type NatsConnection } from "@nats-io/transport-node";
14
+ import { env } from "../src/config/env";
13
15
 
14
16
  const APPLICATION = "cloud-recovery-smoke";
15
17
  const JOB_ID = "restart-job";
@@ -49,11 +51,24 @@ const removeTestStreams = async (connection: NatsConnection, namespace: string,
49
51
  };
50
52
 
51
53
  export const main = async (): Promise<void> => {
52
- const phase = Bun.argv[2];
54
+ const { values: options, positionals } = parseArgs({
55
+ args: Bun.argv.slice(2),
56
+ allowPositionals: true,
57
+ options: { namespace: { type: "string" }, help: { type: "boolean", default: false } },
58
+ });
59
+ if (options.help) {
60
+ console.log(`Usage: bun packages/cloud/scripts/sync-recovery-smoke.ts <prepare|recover> --namespace cloud-recovery-smoke-<unique>
61
+
62
+ Two-phase recovery acceptance against the application's NATS servers (NATS_SERVERS, default nats://127.0.0.1:4222).
63
+ Run "prepare", restart the application fleet while NATS stays up, then run "recover" with the same namespace.
64
+ `);
65
+ return;
66
+ }
67
+ const phase = positionals[0];
53
68
  check(phase === "prepare" || phase === "recover", "Pass prepare or recover");
54
- const namespace = process.env.SYNC_RECOVERY_NAMESPACE?.trim() ?? "";
55
- check(/^cloud-recovery-smoke-[A-Za-z0-9_-]{6,60}$/.test(namespace), "Set a unique SYNC_RECOVERY_NAMESPACE=cloud-recovery-smoke-<suffix>");
56
- const servers = (process.env.NATS_SERVERS ?? "nats://127.0.0.1:4222")
69
+ const namespace = options.namespace?.trim() ?? "";
70
+ check(/^cloud-recovery-smoke-[A-Za-z0-9_-]{6,60}$/.test(namespace), "Pass a unique --namespace cloud-recovery-smoke-<suffix>");
71
+ const servers = (env.NATS_SERVERS ?? "nats://127.0.0.1:4222")
57
72
  .split(",")
58
73
  .map((value) => value.trim())
59
74
  .filter(Boolean);
@@ -1,9 +1,16 @@
1
1
  import type { AppRuntimeMetadata } from "../contracts/registry";
2
2
 
3
+ declare const __CLOUD_VERSION__: string;
3
4
  declare const __CLOUD_RELEASE__: string;
4
5
  declare const __CLOUD_SYNC_VERSION__: string;
5
6
 
6
- export const appRuntimeMetadata: AppRuntimeMetadata = {
7
+ /** Cloud version and release label baked into the bundle by `scripts/build.ts`. */
8
+ export const buildMetadata = {
9
+ version: typeof __CLOUD_VERSION__ === "string" ? __CLOUD_VERSION__ : "0.0.0-local",
7
10
  release: typeof __CLOUD_RELEASE__ === "string" ? __CLOUD_RELEASE__ : "development",
11
+ } as const;
12
+
13
+ export const appRuntimeMetadata: AppRuntimeMetadata = {
14
+ ...buildMetadata,
8
15
  syncVersion: typeof __CLOUD_SYNC_VERSION__ === "string" ? __CLOUD_SYNC_VERSION__ : "unknown",
9
16
  };
@@ -10,9 +10,9 @@ import {
10
10
  type CapabilityActionManifest,
11
11
  type CapabilityActionReviewResult,
12
12
  CapabilityActionReviewSchema,
13
- type CapabilityDefinitions,
14
13
  type CapabilityCommandManifest,
15
14
  CapabilityCommandManifestSchema,
15
+ type CapabilityDefinitions,
16
16
  type CapabilityError,
17
17
  CapabilityErrorSchema,
18
18
  type CapabilityExecutionContext,
@@ -433,7 +433,11 @@ const compileOperationSchemas = (definition: CapabilityQueryDefinition | Capabil
433
433
  inputSchema,
434
434
  dataSchema,
435
435
  resultZodSchema,
436
- schemaHash: capabilityHash({ inputSchema, dataSchema, ...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}) }),
436
+ schemaHash: capabilityHash({
437
+ inputSchema,
438
+ dataSchema,
439
+ ...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
440
+ }),
437
441
  };
438
442
  };
439
443
 
@@ -469,8 +473,13 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
469
473
  for (const [localId, definition] of Object.entries(definitions.queries ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
470
474
  registerLocalId(localId, "Query");
471
475
  const label = `Query ${localId}`;
472
- if (definition.stream && ((definition.stream.direction === "write" && !("idempotency" in definition)) || definition.stream.maxBytes <= 0)) throw new Error(`${label}: write streams require an Action and a positive byte budget`);
473
- if (definition.stream?.direction === "write" && "idempotency" in definition && definition.idempotency !== "required") throw new Error(`${label}: write streams require idempotency`);
476
+ if (
477
+ definition.stream &&
478
+ ((definition.stream.direction === "write" && !("idempotency" in definition)) || definition.stream.maxBytes <= 0)
479
+ )
480
+ throw new Error(`${label}: write streams require an Action and a positive byte budget`);
481
+ if (definition.stream?.direction === "write" && "idempotency" in definition && definition.idempotency !== "required")
482
+ throw new Error(`${label}: write streams require idempotency`);
474
483
  const schemas = compileOperationSchemas(definition, label);
475
484
  if (definition.universalSearch) {
476
485
  const expectedInput = projectSchema(UniversalSearchInputSchema, "Universal Search input", "input");
@@ -540,8 +549,13 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
540
549
  if (definition.approval === "rememberable" && !definition.review) {
541
550
  throw new Error(`${label} must provide a review before approval can be remembered`);
542
551
  }
543
- if (definition.stream && ((definition.stream.direction === "write" && !("idempotency" in definition)) || definition.stream.maxBytes <= 0)) throw new Error(`${label}: write streams require an Action and a positive byte budget`);
544
- if (definition.stream?.direction === "write" && "idempotency" in definition && definition.idempotency !== "required") throw new Error(`${label}: write streams require idempotency`);
552
+ if (
553
+ definition.stream &&
554
+ ((definition.stream.direction === "write" && !("idempotency" in definition)) || definition.stream.maxBytes <= 0)
555
+ )
556
+ throw new Error(`${label}: write streams require an Action and a positive byte budget`);
557
+ if (definition.stream?.direction === "write" && "idempotency" in definition && definition.idempotency !== "required")
558
+ throw new Error(`${label}: write streams require idempotency`);
545
559
  const schemas = compileOperationSchemas(definition, label);
546
560
  const manifest = {
547
561
  localId,
@@ -736,13 +750,14 @@ export const parseCapabilityManifest = (value: unknown, expectedAppId: string):
736
750
  );
737
751
  }
738
752
  assertClosedObjectInput(inputSchema, `Operation ${operation.localId} input`);
739
- if (
740
- operation.stream?.direction === "write" &&
741
- (!("idempotency" in operation) || operation.idempotency !== "required")
742
- ) {
753
+ if (operation.stream?.direction === "write" && (!("idempotency" in operation) || operation.idempotency !== "required")) {
743
754
  throw new Error(`Operation ${operation.localId} write streams require an idempotent Action`);
744
755
  }
745
- const expectedSchemaHash = capabilityHash({ inputSchema: operation.inputSchema, dataSchema: operation.dataSchema, ...(operation.stream ? { stream: operation.stream } : {}) });
756
+ const expectedSchemaHash = capabilityHash({
757
+ inputSchema: operation.inputSchema,
758
+ dataSchema: operation.dataSchema,
759
+ ...(operation.stream ? { stream: operation.stream } : {}),
760
+ });
746
761
  if (operation.schemaHash !== expectedSchemaHash)
747
762
  throw new Error(`Operation ${operation.localId} schemaHash does not match its schemas`);
748
763
  if ("universalSearch" in operation && operation.universalSearch) {
@@ -1086,7 +1101,15 @@ export const invokeCompiledCapability = async (params: {
1086
1101
  return normalizeProviderError(invoked.error, "Capability returned an invalid error", params.onUnexpectedError);
1087
1102
  }
1088
1103
  const stream = invoked.data.stream;
1089
- if (stream && (Buffer.byteLength(stream.id) > 2048 || !operation.definition.stream || stream.direction !== operation.definition.stream.direction || stream.size > operation.definition.stream.maxBytes || Date.parse(stream.expiresAt) <= Date.now() || Date.parse(stream.expiresAt) > Date.now() + 86_400_000))
1104
+ if (
1105
+ stream &&
1106
+ (Buffer.byteLength(stream.id) > 2048 ||
1107
+ !operation.definition.stream ||
1108
+ stream.direction !== operation.definition.stream.direction ||
1109
+ stream.size > operation.definition.stream.maxBytes ||
1110
+ Date.parse(stream.expiresAt) <= Date.now() ||
1111
+ Date.parse(stream.expiresAt) > Date.now() + 86_400_000)
1112
+ )
1090
1113
  return invalidProviderError("Stream is outside its declared policy or 24 hour lifetime");
1091
1114
  const validated = validateCapabilityResult(params.compiled, operation, invoked.data, params.onUnexpectedError);
1092
1115
  return validated.ok
@@ -1,6 +1,6 @@
1
- import { invokeCapabilityStream } from "./capability-streams";
2
1
  import { registerHelp } from "../services/help";
3
2
  import { preloadLayoutHelp } from "../ssr/help";
3
+ import { invokeCapabilityStream } from "./capability-streams";
4
4
  import { bindProcessApplicationId, clearProcessApplicationId } from "./process-identity";
5
5
  /**
6
6
  * defineApp() — The single entry point for every cloud app.
@@ -72,8 +72,8 @@ import { compileCapabilities, invokeCompiledCapability, reviewCompiledCapability
72
72
  import { createHeartbeat } from "./heartbeat";
73
73
  import { compileHelp } from "./help";
74
74
  import { createPageResponses } from "./page-responses";
75
- import { getProcessSync, startProcessSync } from "./process-sync";
76
75
  import { configurePostgresApplicationName } from "./postgres-application-name";
76
+ import { getProcessSync, startProcessSync } from "./process-sync";
77
77
  import { APP_READINESS_PATH, appReadinessResponse } from "./readiness";
78
78
  import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry } from "./registry";
79
79
  import { ensureRuntimeWatcher, getCurrentRuntime, stopRuntimeWatcher } from "./runtime-watcher";
@@ -303,7 +303,7 @@ export const defineApp = <
303
303
  opts: AppOptions<S, N, AppId>,
304
304
  ): AppDefinition<S, N, AppId> => {
305
305
  configurePostgresApplicationName(opts.id);
306
- const isDevelopment = process.env.NODE_ENV === "development";
306
+ const isDevelopment = env.IS_DEVELOPMENT;
307
307
  const notifications = bindNotificationDefinitions(opts.id, opts.notifications);
308
308
 
309
309
  // ── 0. Register declared settings into the runtime registry ──────────
@@ -321,7 +321,7 @@ export const defineApp = <
321
321
  dev: isDevelopment,
322
322
  verbose: true,
323
323
  rootDir,
324
- componentRoots: [resolve(process.env.APP_DIR ?? rootDir, "src"), fileURLToPath(new URL("../", import.meta.url))],
324
+ componentRoots: [resolve(env.APP_DIR ?? rootDir, "src"), fileURLToPath(new URL("../", import.meta.url))],
325
325
  basePath: opts.basePath,
326
326
  template: ({ body, scripts, title, description, theme, lang, performanceRoute }) => {
327
327
  const themeFixed = theme !== undefined;
@@ -673,13 +673,30 @@ export const defineApp = <
673
673
  };
674
674
  });
675
675
  for (const kind of ["queries", "actions"] as const) {
676
- server.post(`/api/_internal/capabilities/v1/streams/${kind}/:capabilityId/:verb`, capabilityAuth(kind), auth.requireOAuthScope(kind === "queries" ? "read" : "write", "admin"), async c => {
677
- const actor = c.get("actor");
678
- return invokeCapabilityStream({ compiled: compiledCapabilities, kind, localId: c.req.param("capabilityId") ?? "", verb: c.req.param("verb") ?? "", request: c.req.raw,
679
- context: { actor, accessSubject: c.get("accessSubject"), user: actor.kind === "user" ? actor.user : actor.delegatedUser,
680
- locale: resolveLocale(c.req.raw.headers, await get<string>("app.locale")),
681
- requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(), origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)), signal: c.req.raw.signal } });
682
- });
676
+ server.post(
677
+ `/api/_internal/capabilities/v1/streams/${kind}/:capabilityId/:verb`,
678
+ capabilityAuth(kind),
679
+ auth.requireOAuthScope(kind === "queries" ? "read" : "write", "admin"),
680
+ async (c) => {
681
+ const actor = c.get("actor");
682
+ return invokeCapabilityStream({
683
+ compiled: compiledCapabilities,
684
+ kind,
685
+ localId: c.req.param("capabilityId") ?? "",
686
+ verb: c.req.param("verb") ?? "",
687
+ request: c.req.raw,
688
+ context: {
689
+ actor,
690
+ accessSubject: c.get("accessSubject"),
691
+ user: actor.kind === "user" ? actor.user : actor.delegatedUser,
692
+ locale: resolveLocale(c.req.raw.headers, await get<string>("app.locale")),
693
+ requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(),
694
+ origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
695
+ signal: c.req.raw.signal,
696
+ },
697
+ });
698
+ },
699
+ );
683
700
  }
684
701
  const capabilityReadScope = auth.requireOAuthScope("read", "admin");
685
702
  const capabilityWriteScope = auth.requireOAuthScope("write", "admin");
@@ -1,8 +1,8 @@
1
1
  import type { HtmlFn } from "@k2b/ssr";
2
2
  import type { Context } from "hono";
3
3
  import type { ClientErrorStatusCode, ServerErrorStatusCode } from "hono/utils/http-status";
4
- import { preloadLayoutAnnouncements } from "../server/middleware/settings";
5
4
  import { getLocale } from "../server/locale";
5
+ import { preloadLayoutAnnouncements } from "../server/middleware/settings";
6
6
  import { createLoginRedirectUrl } from "../shared/redirect";
7
7
  import type { PageOptions } from "./define-app";
8
8
 
@@ -1,3 +1,6 @@
1
+ import { buildMetadata } from "./build-metadata";
2
+
1
3
  export const APP_READINESS_PATH = "/_cloud/ready";
2
4
 
3
- export const appReadinessResponse = (appId: string): Response => Response.json({ status: "ready", appId });
5
+ export const appReadinessResponse = (appId: string): Response =>
6
+ Response.json({ status: "ready", appId, version: buildMetadata.version, release: buildMetadata.release });
@@ -39,7 +39,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
39
39
  appearance: e.appearance,
40
40
  adminHref: e.nav?.adminHref,
41
41
  adminNav: e.adminNav?.map((group) => ({
42
- ...(group.section ? {section:group.section} : {}),
42
+ ...(group.section ? { section: group.section } : {}),
43
43
  id: group.id,
44
44
  label: group.label,
45
45
  links: group.links.map((link) => ({ ...link })),
@@ -1,10 +1,10 @@
1
- import PrincipalPicker from "./PrincipalPicker";
2
1
  import { mutation } from "@k2b/stdlib/solid";
3
- import { Button, IconButton, Placeholder, prompts, SelectChip, Tooltip, useLocale } from "@k2b/ui";
2
+ import { IconButton, Placeholder, prompts, SelectChip, Tooltip, useLocale } from "@k2b/ui";
4
3
  import { createSignal, For, Show } from "solid-js";
5
4
  import { CloudAvatar } from "../account/Avatar";
6
5
  import type { AccessEntry, PermissionLevel, Principal } from "../contracts/shared";
7
6
  import { accessMessages } from "./messages";
7
+ import PrincipalPicker from "./PrincipalPicker";
8
8
 
9
9
  // ─────────────────────────────────────────────────────────────────────────
10
10
  // Public API
@@ -147,7 +147,8 @@ export default function PermissionEditor(props: PermissionEditorProps) {
147
147
  const canEdit = () => props.canEdit !== false;
148
148
  const allowPublic = () => props.allowPublic === true;
149
149
  const allowAuthenticated = () => props.allowAuthenticated !== false;
150
- const allowed = (principal: Principal) => resolveAllowedLevels(typeof props.allowedLevels === "function" ? props.allowedLevels(principal) : props.allowedLevels, t());
150
+ const allowed = (principal: Principal) =>
151
+ resolveAllowedLevels(typeof props.allowedLevels === "function" ? props.allowedLevels(principal) : props.allowedLevels, t());
151
152
 
152
153
  // Defensive dev-warning: an empty allowedLevels array makes the editor
153
154
  // unable to grant anything.
@@ -229,12 +230,17 @@ export default function PermissionEditor(props: PermissionEditorProps) {
229
230
  level on pick. The user upgrades via the row pill if they want
230
231
  a higher level. KISS: one decision per step. */}
231
232
  <Show when={canEdit()}>
232
- <PrincipalPicker existing={entries().map(e=>e.principal)} allowPublic={allowPublic()} allowAuthenticated={allowAuthenticated()} allowServiceAccounts={props.allowServiceAccounts}
233
- disabled={busy()} onSelect={(principal,display)=>{
234
- const permission=allowed(principal)[0]?.level;
235
- if(permission&&!busy())grantMut.mutate({principal,permission,display});
236
- }}/>
237
-
233
+ <PrincipalPicker
234
+ existing={entries().map((e) => e.principal)}
235
+ allowPublic={allowPublic()}
236
+ allowAuthenticated={allowAuthenticated()}
237
+ allowServiceAccounts={props.allowServiceAccounts}
238
+ disabled={busy()}
239
+ onSelect={(principal, display) => {
240
+ const permission = allowed(principal)[0]?.level;
241
+ if (permission && !busy()) grantMut.mutate({ principal, permission, display });
242
+ }}
243
+ />
238
244
  </Show>
239
245
  </div>
240
246
  );
@@ -91,9 +91,15 @@ export default function PrincipalPicker(props: {
91
91
  return (
92
92
  <Combobox
93
93
  disabled={props.disabled}
94
- placeholder={props.allowAuthenticated !== false || props.allowPublic
95
- ? (props.allowServiceAccounts ? t().addAll : t().addAudience)
96
- : (props.allowServiceAccounts ? t().addService : t().addBasic)}
94
+ placeholder={
95
+ props.allowAuthenticated !== false || props.allowPublic
96
+ ? props.allowServiceAccounts
97
+ ? t().addAll
98
+ : t().addAudience
99
+ : props.allowServiceAccounts
100
+ ? t().addService
101
+ : t().addBasic
102
+ }
97
103
  fetchData={load}
98
104
  onSelect={(option) => {
99
105
  const principal = principals.get(option.id);
@@ -1,6 +1,6 @@
1
1
  import { dates } from "@k2b/stdlib";
2
2
  import { mutation as mutations } from "@k2b/stdlib/solid";
3
- import { NoticeCard, Button, CopyButton, DateTimePicker, Placeholder, prompts, Select, Tag, TextInput, useLocale } from "@k2b/ui";
3
+ import { Button, CopyButton, DateTimePicker, NoticeCard, Placeholder, prompts, Select, Tag, TextInput, useLocale } from "@k2b/ui";
4
4
  import { createEffect, createSignal, For, Show } from "solid-js";
5
5
  import type { PermissionLevel, ServiceAccountCredential } from "../contracts/shared";
6
6
  import { accessMessages } from "./messages";
package/src/access/ui.ts CHANGED
@@ -8,11 +8,10 @@
8
8
 
9
9
  export type { AllowedLevel, GrantableLevel } from "./PermissionEditor";
10
10
  export { default as PermissionEditor } from "./PermissionEditor";
11
+ export { default as PrincipalPicker, principalKey } from "./PrincipalPicker";
11
12
  export type {
12
13
  ResourceApiKey,
13
14
  ResourceApiKeyPermissionOption,
14
15
  ResourceApiKeysProps,
15
16
  } from "./ResourceApiKeys";
16
17
  export { default as ResourceApiKeys } from "./ResourceApiKeys";
17
-
18
- export { default as PrincipalPicker, principalKey } from "./PrincipalPicker";
package/src/ai/admin.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { aiQuotas, AiQuotaError } from "./quotas";
2
- export { quotaReport, quotaAdminConfig } from "./quota-report";
1
+ export { quotaAdminConfig, quotaReport } from "./quota-report";
2
+ export { AiQuotaError, aiQuotas } from "./quotas";
3
3
  /** Server-only AI administration and usage accounting. */
4
4
 
5
5
  export { type AiModelAccessDraft, type AiModelAccessMap, type AiModelAccessState, aiModelAccess } from "./model-access";
@@ -1,8 +1,8 @@
1
- import { resolveAiAudioModel } from "./transcription";
2
1
  import type { AccessSubject, RequestActor } from "../server";
3
2
  import { aiModelAccess } from "./model-access";
4
3
  import { personalAiModelPolicy } from "./personal-agent";
5
4
  import { listAiModels, toPublicAiSettingsState } from "./settings";
5
+ import { resolveAiAudioModel } from "./transcription";
6
6
  import type { AiChatTurnRunConfig, AiModelPolicy, AiSettingsError } from "./types";
7
7
 
8
8
  export const aiChatAccessSubject = (actor: RequestActor | undefined): AccessSubject | null => {
@@ -4,7 +4,7 @@ import { aiProjectFilePathFromMount } from "./file-mount";
4
4
  import { aiFileStore, normalizeAiFilePath } from "./files-store";
5
5
  import { aiModelAccess } from "./model-access";
6
6
  import { defineAiTool } from "./tools";
7
- import { resolveAiAudioModel, runAiTranscription, type AiResolvedAudioModel } from "./transcription";
7
+ import { type AiResolvedAudioModel, resolveAiAudioModel, runAiTranscription } from "./transcription";
8
8
 
9
9
  export const CloudAiTranscribeAudioInputSchema = z.object({
10
10
  path: z.string().trim().min(1).describe("Absolute path of the stored audio file. Project files are below /project."),