@databricks/appkit 0.83.0 → 0.84.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 (210) hide show
  1. package/CLAUDE.md +15 -1
  2. package/dist/agents/databricks.d.ts +15 -3
  3. package/dist/agents/databricks.d.ts.map +1 -1
  4. package/dist/agents/databricks.js +25 -8
  5. package/dist/agents/databricks.js.map +1 -1
  6. package/dist/appkit/package.js +1 -1
  7. package/dist/beta.d.ts +2 -2
  8. package/dist/cache/index.d.ts.map +1 -1
  9. package/dist/cache/index.js +6 -1
  10. package/dist/cache/index.js.map +1 -1
  11. package/dist/cli/commands/agent/eval.js +1 -1
  12. package/dist/cli/commands/generate-types.js +1 -1
  13. package/dist/cli/commands/plugin/sync/sync.js +28 -15
  14. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  15. package/dist/cli/commands/registry/add.js +3 -11
  16. package/dist/cli/commands/registry/add.js.map +1 -1
  17. package/dist/cli/commands/registry/config-writer.js +1 -1
  18. package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
  19. package/dist/connectors/lakebase/routing-pool.js +4 -10
  20. package/dist/connectors/lakebase/routing-pool.js.map +1 -1
  21. package/dist/context/caller-context.d.ts +4 -1
  22. package/dist/context/caller-context.d.ts.map +1 -1
  23. package/dist/context/caller-context.js.map +1 -1
  24. package/dist/context/execution-context.d.ts +29 -1
  25. package/dist/context/execution-context.d.ts.map +1 -1
  26. package/dist/context/execution-context.js +60 -8
  27. package/dist/context/execution-context.js.map +1 -1
  28. package/dist/context/index.d.ts +2 -2
  29. package/dist/context/index.js +2 -1
  30. package/dist/context/request-scope.d.ts +2 -0
  31. package/dist/context/request-scope.js +39 -0
  32. package/dist/context/request-scope.js.map +1 -0
  33. package/dist/context/resource-capabilities.js +59 -0
  34. package/dist/context/resource-capabilities.js.map +1 -0
  35. package/dist/context/scoped-api.js +104 -0
  36. package/dist/context/scoped-api.js.map +1 -0
  37. package/dist/context/service-context.d.ts.map +1 -1
  38. package/dist/context/service-context.js +1 -1
  39. package/dist/context/service-context.js.map +1 -1
  40. package/dist/context/user-context.d.ts +16 -19
  41. package/dist/context/user-context.d.ts.map +1 -1
  42. package/dist/context/user-context.js +30 -3
  43. package/dist/context/user-context.js.map +1 -1
  44. package/dist/core/agent/load-agents.d.ts.map +1 -1
  45. package/dist/core/agent/load-agents.js +5 -2
  46. package/dist/core/agent/load-agents.js.map +1 -1
  47. package/dist/core/agent/run-agent.d.ts +19 -6
  48. package/dist/core/agent/run-agent.d.ts.map +1 -1
  49. package/dist/core/agent/run-agent.js +53 -17
  50. package/dist/core/agent/run-agent.js.map +1 -1
  51. package/dist/core/agent/types.d.ts +18 -1
  52. package/dist/core/agent/types.d.ts.map +1 -1
  53. package/dist/core/agent/types.js.map +1 -1
  54. package/dist/core/appkit.d.ts +4 -3
  55. package/dist/core/appkit.d.ts.map +1 -1
  56. package/dist/core/appkit.js +30 -9
  57. package/dist/core/appkit.js.map +1 -1
  58. package/dist/core/plugin-context.d.ts +14 -15
  59. package/dist/core/plugin-context.d.ts.map +1 -1
  60. package/dist/core/plugin-context.js +24 -14
  61. package/dist/core/plugin-context.js.map +1 -1
  62. package/dist/errors/base.d.ts +2 -2
  63. package/dist/errors/base.js +2 -2
  64. package/dist/errors/base.js.map +1 -1
  65. package/dist/errors/identity-expired.d.ts +14 -0
  66. package/dist/errors/identity-expired.d.ts.map +1 -0
  67. package/dist/errors/identity-expired.js +33 -0
  68. package/dist/errors/identity-expired.js.map +1 -0
  69. package/dist/errors/index.js +1 -0
  70. package/dist/index.d.ts +8 -5
  71. package/dist/index.js +5 -2
  72. package/dist/logging/logger.js +1 -1
  73. package/dist/plugin/execution-result.d.ts +4 -1
  74. package/dist/plugin/execution-result.d.ts.map +1 -1
  75. package/dist/plugin/interceptors/telemetry.js +6 -4
  76. package/dist/plugin/interceptors/telemetry.js.map +1 -1
  77. package/dist/plugin/plugin.d.ts +13 -21
  78. package/dist/plugin/plugin.d.ts.map +1 -1
  79. package/dist/plugin/plugin.js +41 -122
  80. package/dist/plugin/plugin.js.map +1 -1
  81. package/dist/plugins/agents/agents.d.ts +12 -0
  82. package/dist/plugins/agents/agents.d.ts.map +1 -1
  83. package/dist/plugins/agents/agents.js +65 -15
  84. package/dist/plugins/agents/agents.js.map +1 -1
  85. package/dist/plugins/agents/auth-mode.js +42 -0
  86. package/dist/plugins/agents/auth-mode.js.map +1 -0
  87. package/dist/plugins/agents/index.d.ts +1 -1
  88. package/dist/plugins/agents/mlflow.js +11 -3
  89. package/dist/plugins/agents/mlflow.js.map +1 -1
  90. package/dist/plugins/agents/tool-dispatch.js +9 -1
  91. package/dist/plugins/agents/tool-dispatch.js.map +1 -1
  92. package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
  93. package/dist/plugins/ai-search/ai-search.js +4 -4
  94. package/dist/plugins/ai-search/ai-search.js.map +1 -1
  95. package/dist/plugins/analytics/analytics.d.ts +2 -2
  96. package/dist/plugins/analytics/analytics.js +6 -6
  97. package/dist/plugins/analytics/analytics.js.map +1 -1
  98. package/dist/plugins/database/crud/contract.js +2 -2
  99. package/dist/plugins/database/crud/contract.js.map +1 -1
  100. package/dist/plugins/files/plugin.d.ts +10 -4
  101. package/dist/plugins/files/plugin.d.ts.map +1 -1
  102. package/dist/plugins/files/plugin.js +34 -13
  103. package/dist/plugins/files/plugin.js.map +1 -1
  104. package/dist/plugins/genie/genie.d.ts +9 -0
  105. package/dist/plugins/genie/genie.d.ts.map +1 -1
  106. package/dist/plugins/genie/genie.js +12 -3
  107. package/dist/plugins/genie/genie.js.map +1 -1
  108. package/dist/plugins/genie/manifest.js +1 -0
  109. package/dist/plugins/jobs/plugin.js +2 -2
  110. package/dist/plugins/jobs/plugin.js.map +1 -1
  111. package/dist/plugins/lakebase/lakebase.d.ts +10 -17
  112. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  113. package/dist/plugins/lakebase/lakebase.js +12 -17
  114. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  115. package/dist/plugins/server/client-config-sanitizer.js +1 -4
  116. package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
  117. package/dist/plugins/server/dev-obo-middleware.js +60 -0
  118. package/dist/plugins/server/dev-obo-middleware.js.map +1 -0
  119. package/dist/plugins/server/index.d.ts.map +1 -1
  120. package/dist/plugins/server/index.js +5 -2
  121. package/dist/plugins/server/index.js.map +1 -1
  122. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
  123. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  124. package/dist/plugins/server/static-server.js +3 -3
  125. package/dist/plugins/server/static-server.js.map +1 -1
  126. package/dist/plugins/server/utils.js +3 -3
  127. package/dist/plugins/server/utils.js.map +1 -1
  128. package/dist/plugins/server/vite-dev-server.js +4 -4
  129. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  130. package/dist/plugins/serving/manifest.js +1 -0
  131. package/dist/plugins/serving/serving.js +6 -6
  132. package/dist/plugins/serving/serving.js.map +1 -1
  133. package/dist/schemas/manifest.d.ts +35 -1
  134. package/dist/schemas/manifest.d.ts.map +1 -1
  135. package/dist/schemas/manifest.js +112 -4
  136. package/dist/schemas/manifest.js.map +1 -1
  137. package/dist/shared/src/dev-obo.js +86 -0
  138. package/dist/shared/src/dev-obo.js.map +1 -0
  139. package/dist/shared/src/index.d.ts +1 -1
  140. package/dist/shared/src/plugin.d.ts +12 -1
  141. package/dist/shared/src/plugin.d.ts.map +1 -1
  142. package/dist/shared/src/schemas/manifest.d.ts +41 -33
  143. package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
  144. package/dist/shared/src/schemas/manifest.js +43 -4
  145. package/dist/shared/src/schemas/manifest.js.map +1 -1
  146. package/dist/stream/stream-manager.d.ts.map +1 -1
  147. package/dist/stream/stream-manager.js +5 -2
  148. package/dist/stream/stream-manager.js.map +1 -1
  149. package/dist/telemetry/execution-span-processor.js +21 -0
  150. package/dist/telemetry/execution-span-processor.js.map +1 -0
  151. package/dist/telemetry/telemetry-manager.js +2 -1
  152. package/dist/telemetry/telemetry-manager.js.map +1 -1
  153. package/dist/testing/create-test-app.d.ts +2 -2
  154. package/dist/testing/create-test-app.js.map +1 -1
  155. package/dist/testing/test-plugin-context.d.ts +3 -12
  156. package/dist/testing/test-plugin-context.d.ts.map +1 -1
  157. package/dist/testing/test-plugin-context.js +24 -16
  158. package/dist/testing/test-plugin-context.js.map +1 -1
  159. package/dist/type-generator/database/generate.js +3 -3
  160. package/dist/type-generator/database/generate.js.map +1 -1
  161. package/dist/type-generator/migration.js +2 -2
  162. package/dist/type-generator/migration.js.map +1 -1
  163. package/dist/type-generator/serving/server-file-extractor.js +3 -3
  164. package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
  165. package/dist/utils/is-plain-object.js +11 -0
  166. package/dist/utils/is-plain-object.js.map +1 -0
  167. package/docs/api/appkit/Class.AppKitError.md +2 -1
  168. package/docs/api/appkit/Class.AuthenticationError.md +1 -1
  169. package/docs/api/appkit/Class.ConfigurationError.md +1 -1
  170. package/docs/api/appkit/Class.ConnectionError.md +1 -1
  171. package/docs/api/appkit/Class.DatabaseValidationError.md +1 -1
  172. package/docs/api/appkit/Class.ExecutionError.md +1 -1
  173. package/docs/api/appkit/Class.IdentityExpiredError.md +190 -0
  174. package/docs/api/appkit/Class.InitializationError.md +1 -1
  175. package/docs/api/appkit/Class.Plugin.md +8 -12
  176. package/docs/api/appkit/Class.ServerError.md +1 -1
  177. package/docs/api/appkit/Class.ServiceContext.md +169 -0
  178. package/docs/api/appkit/Class.TunnelError.md +1 -1
  179. package/docs/api/appkit/Class.ValidationError.md +1 -1
  180. package/docs/api/appkit/Function.createApp.md +12 -12
  181. package/docs/api/appkit/Function.getCallerContext.md +12 -0
  182. package/docs/api/appkit/Function.getCurrentUserId.md +14 -0
  183. package/docs/api/appkit/Function.getExecutionContext.md +1 -1
  184. package/docs/api/appkit/Function.getUserContext.md +16 -0
  185. package/docs/api/appkit/Function.isInUserContext.md +12 -0
  186. package/docs/api/appkit/Function.isUserContext.md +20 -0
  187. package/docs/api/appkit/Function.runAgent.md +1 -1
  188. package/docs/api/appkit/Function.runInCallerContext.md +27 -0
  189. package/docs/api/appkit/Function.runInUserContext.md +29 -0
  190. package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
  191. package/docs/api/appkit/Interface.AgentsPluginConfig.md +11 -0
  192. package/docs/api/appkit/Interface.CallerContext.md +13 -0
  193. package/docs/api/appkit/Interface.IndexConfig.md +1 -1
  194. package/docs/api/appkit/Interface.PluginManifest.md +9 -1
  195. package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
  196. package/docs/api/appkit/Interface.RunAgentInput.md +45 -1
  197. package/docs/api/appkit/TypeAlias.AgentAuth.md +8 -0
  198. package/docs/api/appkit/TypeAlias.AppKitApi.md +35 -0
  199. package/docs/api/appkit/TypeAlias.ExecutionContext.md +4 -1
  200. package/docs/api/appkit/TypeAlias.ExecutionResult.md +65 -0
  201. package/docs/api/appkit/TypeAlias.ScopedPluginMap.md +12 -0
  202. package/docs/api/appkit/TypeAlias.UserContext.md +109 -0
  203. package/docs/api/appkit/TypeAlias.UserScopedApp.md +39 -0
  204. package/docs/api/appkit.md +14 -0
  205. package/docs/plugins/agents.md +45 -0
  206. package/docs/plugins/execution-context.md +101 -50
  207. package/docs/plugins/lakebase.md +6 -82
  208. package/llms.txt +15 -1
  209. package/package.json +1 -1
  210. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","names":[],"sources":["../../../../../shared/src/schemas/manifest.ts"],"sourcesContent":["/**\n * Zod-authoring module for AppKit plugin manifest schemas.\n *\n * Single source of truth for the plugin manifest contract. JSON Schema\n * artifacts published at the docs URL are emitted from these schemas via\n * `tools/generate-json-schema.ts` and live only in `docs/static/schemas/`\n * (no package-internal copies).\n *\n * - Cross-field constraints (cycle/dangling-reference checks, `<PROFILE>`\n * placeholder, post-scaffold instruction non-empty) are refinements\n * co-located with the shape they constrain. Validation is driven through\n * the Standard Schema interface from `validate-manifest.ts`.\n * - `templateFieldEntrySchema` is a transform that emits `origin` from\n * `localOnly`/`value`/`resolve`. The input slot is still allowed so\n * re-parsing previously-synced template manifests does not fail, but the\n * transform always overwrites it — drift-by-construction for hand-edits.\n * - `discoveryDescriptorSchema` is a discriminated union over a `type`\n * literal. The `kind` variant references one of the well-known\n * `resourceKind` values for which AppKit owns the CLI command map (see\n * `RESOURCE_KIND_COMMANDS` below). The `cli` variant is the escape hatch\n * carrying the existing free-form fields (with the `<PROFILE>`\n * refinement). Hierarchical context for volumes (catalog/schema parent\n * walk) is encoded via the kind's `parents` array, not via dependsOn.\n * - Scaffolding rule items carry a `maxLength` (120 chars) so\n * `rules.never[]` / `rules.must[]` / `rules.should[]` stay short\n * directives by contract, and the canonical `TEMPLATE_SCAFFOLDING`\n * constant lives co-located with the scaffolding schemas (sync.ts\n * imports it).\n */\n\nimport { z } from \"zod\";\n\nimport { PLUGIN_NAME_PATTERN } from \"../naming\";\n\n// ── Resource type + per-type permission enums ────────────────────────────\n\nexport const resourceTypeSchema = z\n .enum([\n \"secret\",\n \"job\",\n \"sql_warehouse\",\n \"serving_endpoint\",\n \"volume\",\n \"vector_search_index\",\n \"uc_function\",\n \"uc_connection\",\n \"database\",\n \"postgres\",\n \"genie_space\",\n \"experiment\",\n \"app\",\n ])\n .describe(\"Type of Databricks resource\");\n\n/** Apps user_api_scopes for resource types with confirmed OBO support. */\nexport const SCOPE_BY_TYPE = {\n sql_warehouse: \"sql\", // sql:restricted-query is the read-only variant.\n serving_endpoint: \"model-serving\",\n genie_space: \"genie\",\n volume: \"files\",\n vector_search_index: \"vector-search\",\n uc_connection: \"catalog.connections\",\n // uc_function uses sql, or mcp.functions through managed MCP. Confirm later.\n // experiment and job are SP-only; there is no mlflow or jobs scope.\n} as const satisfies Partial<Record<ResourceType, string>>;\n\n// A postgres user_api_scope exists, so Lakebase is platform-OBO-capable.\n// It stays app-only for v1 because the connector connects as the SP today (audit A7).\nexport const APP_ONLY_RESOURCE_TYPES: ReadonlySet<ResourceType> = new Set([\n \"secret\",\n \"database\",\n \"postgres\",\n]);\n\n/** Capabilities that need a user_api_scope but have no resource ID. */\nexport const capabilityScopeSchema = z.enum([\n \"ai-gateway\",\n \"mcp.external\",\n \"mcp.functions\",\n \"workspace.workspace\",\n \"catalog.catalogs:read\",\n \"catalog.schemas:read\",\n \"catalog.tables:read\",\n]);\n\nexport type CapabilityScope = z.infer<typeof capabilityScopeSchema>;\n\nexport const secretPermissionSchema = z\n .enum([\"READ\", \"WRITE\", \"MANAGE\"])\n .describe(\"Permission for secret resources (order: weakest to strongest)\");\n\nexport const jobPermissionSchema = z\n .enum([\"CAN_VIEW\", \"CAN_MANAGE_RUN\", \"CAN_MANAGE\"])\n .describe(\"Permission for job resources (order: weakest to strongest)\");\n\nexport const sqlWarehousePermissionSchema = z\n .enum([\"CAN_USE\", \"CAN_MANAGE\"])\n .describe(\n \"Permission for SQL warehouse resources (order: weakest to strongest)\",\n );\n\nexport const servingEndpointPermissionSchema = z\n .enum([\"CAN_VIEW\", \"CAN_QUERY\", \"CAN_MANAGE\"])\n .describe(\n \"Permission for serving endpoint resources (order: weakest to strongest)\",\n );\n\nexport const volumePermissionSchema = z\n .enum([\"READ_VOLUME\", \"WRITE_VOLUME\"])\n .describe(\"Permission for Unity Catalog volume resources\");\n\nexport const vectorSearchIndexPermissionSchema = z\n .enum([\"SELECT\"])\n .describe(\"Permission for vector search index resources\");\n\nexport const ucFunctionPermissionSchema = z\n .enum([\"EXECUTE\"])\n .describe(\"Permission for Unity Catalog function resources\");\n\nexport const ucConnectionPermissionSchema = z\n .enum([\"USE_CONNECTION\"])\n .describe(\"Permission for Unity Catalog connection resources\");\n\nexport const databasePermissionSchema = z\n .enum([\"CAN_CONNECT_AND_CREATE\"])\n .describe(\"Permission for database resources\");\n\nexport const postgresPermissionSchema = z\n .enum([\"CAN_CONNECT_AND_CREATE\"])\n .describe(\"Permission for Postgres resources\");\n\nexport const genieSpacePermissionSchema = z\n .enum([\"CAN_VIEW\", \"CAN_RUN\", \"CAN_EDIT\", \"CAN_MANAGE\"])\n .describe(\n \"Permission for Genie Space resources (order: weakest to strongest)\",\n );\n\nexport const experimentPermissionSchema = z\n .enum([\"CAN_READ\", \"CAN_EDIT\", \"CAN_MANAGE\"])\n .describe(\n \"Permission for MLflow experiment resources (order: weakest to strongest)\",\n );\n\nexport const appPermissionSchema = z\n .enum([\"CAN_USE\"])\n .describe(\"Permission for Databricks App resources\");\n\n// ── Discovery descriptor (discriminated union) ───────────────────────────\n\n/**\n * Well-known Databricks resource kinds for which AppKit owns the CLI\n * command map. Plugins reference one of these via the `kind` variant of the\n * discovery descriptor; everything else falls back to the free-form `cli`\n * variant.\n *\n * Kept narrow on purpose: each entry costs an addition to\n * `RESOURCE_KIND_COMMANDS` below, which is the single source of truth for\n * how that kind is enumerated.\n */\nexport const resourceKindSchema = z\n .enum([\n \"warehouse\",\n \"genie_space\",\n \"postgres_project\",\n \"postgres_branch\",\n \"postgres_database\",\n \"volume\",\n ])\n .describe(\n \"Well-known Databricks resource kind whose listing command is owned by AppKit (see RESOURCE_KIND_COMMANDS).\",\n );\n\nexport const kindDiscoveryDescriptorSchema = z\n .object({\n type: z\n .literal(\"kind\")\n .describe(\n \"Discriminator: 'kind' uses the AppKit-owned command map for the named resourceKind.\",\n ),\n resourceKind: resourceKindSchema.describe(\n \"Reference to a well-known Databricks resource kind. AppKit owns the CLI command, response shape, and unwrap rules.\",\n ),\n select: z\n .string()\n .optional()\n .describe(\n \"Field name in the parsed CLI response used as the selected value (e.g., 'id'). Defaults to the kind's natural identifier when omitted.\",\n ),\n display: z\n .string()\n .optional()\n .describe(\n \"Field name in the parsed CLI response shown to the user in selection UI. Defaults to `select` if omitted.\",\n ),\n dependsOn: z\n .string()\n .optional()\n .describe(\n \"Name of a sibling field within the same resource that must be resolved first. Used to express ordering dependencies between resource fields.\",\n ),\n shortcut: z\n .string()\n .optional()\n .describe(\n \"Single-value fast-path command that returns exactly one value, skipping interactive selection.\",\n ),\n })\n .strict()\n .describe(\n \"Discovery via a well-known resource kind. AppKit owns the CLI command and unwrap rules for the named kind.\",\n );\n\n/**\n * Shell metacharacters rejected on free-form CLI command strings. Catches the\n * common foot-guns (statement separators, pipes, redirects via shell, command\n * substitution, newlines). Not a security boundary on its own — executors must\n * always pass these via argv, never shell-exec the string. Angle brackets are\n * permitted because `<PROFILE>` (and future `<…>` placeholders) are part of\n * the command-template convention.\n */\nconst SHELL_METACHAR_RE = /[;|&`$\\n\\r]/;\n\nexport const cliDiscoveryDescriptorSchema = z\n .object({\n type: z\n .literal(\"cli\")\n .describe(\n \"Discriminator: 'cli' uses a free-form Databricks CLI command supplied by the plugin.\",\n ),\n cliCommand: z\n .string()\n .describe(\n \"Databricks CLI command that lists resources. Must include <PROFILE>. Shell metacharacters (;|&`$ and newlines) are rejected; for first-party Databricks resources prefer the `kind` variant which uses AppKit's typed command map.\",\n ),\n selectField: z\n .string()\n .describe(\n \"jq-style path to the field used as the selected value (e.g., '.id', '.name').\",\n ),\n displayField: z\n .string()\n .optional()\n .describe(\n \"jq-style path to the field shown to the user in selection UI. Defaults to selectField if omitted.\",\n ),\n dependsOn: z\n .string()\n .optional()\n .describe(\n \"Name of a sibling field within the same resource that must be resolved first. Used to express ordering dependencies between resource fields.\",\n ),\n shortcut: z\n .string()\n .optional()\n .describe(\n \"Single-value fast-path command that returns exactly one value, skipping interactive selection. Shell metacharacters are rejected.\",\n ),\n })\n .strict()\n .refine((descriptor) => descriptor.cliCommand.includes(\"<PROFILE>\"), {\n message: \"must include <PROFILE> placeholder\",\n path: [\"cliCommand\"],\n })\n .refine((descriptor) => !SHELL_METACHAR_RE.test(descriptor.cliCommand), {\n message:\n \"must not contain shell metacharacters (;|&`$ or newlines); use the `kind` variant for typed Databricks resources\",\n path: [\"cliCommand\"],\n })\n .refine(\n (descriptor) =>\n descriptor.shortcut === undefined ||\n !SHELL_METACHAR_RE.test(descriptor.shortcut),\n {\n message: \"must not contain shell metacharacters (;|&`$ or newlines)\",\n path: [\"shortcut\"],\n },\n )\n .describe(\n \"Discovery via a free-form Databricks CLI command. Escape hatch — prefer the `kind` variant when a typed resourceKind covers the resource. This shape is intentionally minimal and may tighten further in future versions.\",\n );\n\nexport const discoveryDescriptorSchema = z\n .discriminatedUnion(\"type\", [\n kindDiscoveryDescriptorSchema,\n cliDiscoveryDescriptorSchema,\n ])\n .describe(\n \"Describes how the CLI discovers values for a resource field. 'kind' references a well-known Databricks resource kind whose command is owned by AppKit; 'cli' is the escape hatch carrying a free-form Databricks CLI command.\",\n );\n\n// ── Resource kind → CLI command map ──────────────────────────────────────\n\n/**\n * Descriptor for how a well-known resource kind is listed via the\n * Databricks CLI.\n *\n * - `command` is the CLI invocation template. It carries two kinds of\n * placeholders:\n * - `<PROFILE>` — substituted with the user's CLI profile by the runner.\n * - `{<fieldName>}` — substituted with the resolved value of the named\n * sibling field (used for `dependsOn` chains).\n * - `unwrap`, when set, is the JSON path into the response wrapper (e.g.,\n * `\"warehouses\"` for `{ warehouses: [...] }`). Omitted when the response\n * is already a flat array.\n * - `parents`, when set, lists transient query inputs the runner must\n * collect (as free-text prompts) before invoking the command. Each\n * `parents[i]` value substitutes the matching `{name}` placeholder in\n * the command string. Unlike `dependsOn` (which references a sibling\n * field on the same resource), `parents` covers inputs that aren't\n * persisted as fields on the resource.\n */\nexport type ResourceKindCommand = {\n command: string;\n unwrap?: string;\n parents?: readonly string[];\n};\n\n/**\n * Single source of truth for AppKit-owned discovery commands.\n *\n * To add a new resource kind: extend `resourceKindSchema` and add an entry\n * here. Plugins reference the kind via `discovery: { type: \"kind\",\n * resourceKind: \"...\" }` and inherit the command + response shape.\n *\n * `unwrap` defaults are unset: the existing core plugin manifests use simple\n * jq paths (`.id`, `.name`, `.full_name`), implying the listed CLI commands\n * return flat arrays. Refine in a follow-up if a kind's CLI returns wrapped\n * data.\n *\n * Volume's catalog/schema parent context is supplied via the `parents`\n * array, which the runner collects from the user as free-text prompts\n * before invoking the listing command.\n */\nexport const RESOURCE_KIND_COMMANDS: Record<\n z.infer<typeof resourceKindSchema>,\n { command: string; unwrap?: string; parents?: readonly string[] }\n> = {\n warehouse: {\n command: \"databricks warehouses list --profile <PROFILE> --output json\",\n },\n genie_space: {\n command: \"databricks genie list-spaces --profile <PROFILE> --output json\",\n },\n postgres_project: {\n command:\n \"databricks postgres list-projects --profile <PROFILE> --output json\",\n },\n postgres_branch: {\n // {project} is a placeholder for the resolved value of the `project`\n // sibling field (declared via `dependsOn: \"project\"` on the kind variant).\n // The Databricks CLI requires the parent project resource name (format\n // `projects/{project_id}`) as a positional argument.\n command:\n \"databricks postgres list-branches {project} --profile <PROFILE> --output json\",\n },\n postgres_database: {\n // {branch} is a placeholder for the resolved value of the `branch`\n // sibling field (declared via `dependsOn: \"branch\"` on the kind variant).\n command:\n \"databricks postgres list-databases {branch} --profile <PROFILE> --output json\",\n },\n volume: {\n // `parents` declares free-text user prompts the runner must collect before\n // invoking the discovery command. Each `parents[i]` value substitutes the\n // matching `{name}` placeholder in the command string above. Unlike\n // `dependsOn` (which references a sibling field on the same resource),\n // `parents` covers transient query inputs that aren't persisted as fields.\n command:\n \"databricks volumes list {catalog} {schema} --profile <PROFILE> --output json\",\n parents: [\"catalog\", \"schema\"] as const,\n },\n};\n\n// ── Resource field entry (plugin manifest variant) ───────────────────────\n\nexport const resourceFieldEntrySchema = z\n .object({\n env: z\n .string()\n .regex(/^[A-Z][A-Z0-9_]*$/)\n .optional()\n .describe(\"Environment variable name for this field\"),\n description: z\n .string()\n .optional()\n .describe(\"Human-readable description for this field\"),\n bundleIgnore: z\n .boolean()\n .optional()\n .describe(\n \"When true, this field is excluded from Databricks bundle configuration (databricks.yml) generation.\",\n ),\n examples: z\n .array(z.string())\n .optional()\n .describe(\"Example values showing the expected format for this field\"),\n localOnly: z\n .boolean()\n .optional()\n .describe(\n \"When true, this field is only generated for local .env files. The Databricks Apps platform auto-injects it at deploy time.\",\n ),\n value: z\n .string()\n .optional()\n .describe(\n \"Static value for this field. Used when no prompted or resolved value exists.\",\n ),\n resolve: z\n .string()\n .regex(/^[a-z_]+:[a-zA-Z]+$/)\n .optional()\n .describe(\n \"Named resolver prefixed by resource type (e.g., 'postgres:host'). The CLI resolves this value during the init prompt flow.\",\n ),\n discovery: discoveryDescriptorSchema.optional(),\n })\n .strict()\n .describe(\n \"Defines a single field for a resource. Each field has its own environment variable and optional description. Single-value types use one key (e.g. id); multi-value types (database, secret) use multiple (e.g. instance_name, database_name or scope, key).\",\n );\n\n// ── Resource requirement (per-type permission discriminator) ─────────────\n\n/**\n * Build a per-type variant. Each variant fixes `type` to a literal and constrains\n * `permission` to the matching enum, mirroring the existing JSON Schema's\n * `allOf + if/then` block. `fields` and the rest of the shape come from a\n * shared base.\n */\nconst resourceRequirementBaseShape = {\n alias: z\n .string()\n .min(1)\n .describe(\n \"Human-readable label for UI/display only. Deduplication uses resourceKey, not alias.\",\n ),\n resourceKey: z\n .string()\n .regex(/^[a-z][a-z0-9-]*$/)\n .describe(\n \"Stable key for machine use: deduplication, env naming, composite keys, app.yaml. Required for registry lookup.\",\n ),\n description: z\n .string()\n .min(1)\n .describe(\"Human-readable description of why this resource is needed\"),\n fields: z\n .record(z.string(), resourceFieldEntrySchema)\n .refine((obj) => Object.keys(obj).length >= 1, {\n message: \"fields must contain at least one entry\",\n })\n .optional()\n .describe(\n \"Map of field name to env and optional description. Single-value types use one key (e.g. id); multi-value (database, secret) use multiple (e.g. instance_name, database_name or scope, key).\",\n ),\n};\n\n/**\n * Adds the cycle/dangling-reference cross-field check to a resource variant.\n * Iterates the resource's `fields`, validates each `discovery.dependsOn` target\n * is a sibling field name, then runs DFS over the dependsOn graph to detect\n * cycles. Issue paths target either the offending field's `dependsOn` slot or\n * the resource itself for cycles.\n */\nfunction refineResourceDependsOn(\n resource: { fields?: Record<string, { discovery?: { dependsOn?: string } }> },\n ctx: z.core.$RefinementCtx,\n): void {\n if (!resource.fields) return;\n const fieldNames = new Set(Object.keys(resource.fields));\n\n // Pass 1: validate dependsOn references and build the dependency graph.\n const deps = new Map<string, string>();\n for (const [name, field] of Object.entries(resource.fields)) {\n const dep = field.discovery?.dependsOn;\n if (!dep) continue;\n if (!fieldNames.has(dep)) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"fields\", name, \"discovery\", \"dependsOn\"],\n message: `references non-existent sibling field '${dep}'`,\n });\n }\n deps.set(name, dep);\n }\n\n // Pass 2: detect cycles via DFS. Emit one issue per cycle found.\n const visited = new Set<string>();\n const visiting = new Set<string>();\n\n function dfs(node: string, chain: string[]): string[] | null {\n if (visiting.has(node)) return [...chain, node];\n if (visited.has(node)) return null;\n visiting.add(node);\n const next = deps.get(node);\n if (next) {\n const cycle = dfs(next, [...chain, node]);\n if (cycle) return cycle;\n }\n visiting.delete(node);\n visited.add(node);\n return null;\n }\n\n for (const node of deps.keys()) {\n if (visited.has(node)) continue;\n const cycle = dfs(node, []);\n if (cycle) {\n ctx.addIssue({\n code: \"custom\",\n path: [],\n message: `discovery.dependsOn creates a cycle: ${cycle.join(\" → \")}`,\n });\n // One cycle error per resource is enough.\n break;\n }\n }\n}\n\nfunction makeResourceVariant<\n TType extends z.ZodLiteral<string>,\n TPerm extends z.ZodTypeAny,\n>(typeLiteral: TType, permission: TPerm) {\n return z\n .object({\n type: typeLiteral,\n ...resourceRequirementBaseShape,\n permission: permission.describe(\n \"Required permission level. Validated per resource type.\",\n ),\n })\n .strict()\n .superRefine(refineResourceDependsOn);\n}\n\nexport const resourceRequirementSchema = z\n .discriminatedUnion(\"type\", [\n makeResourceVariant(z.literal(\"secret\"), secretPermissionSchema),\n makeResourceVariant(z.literal(\"job\"), jobPermissionSchema),\n makeResourceVariant(\n z.literal(\"sql_warehouse\"),\n sqlWarehousePermissionSchema,\n ),\n makeResourceVariant(\n z.literal(\"serving_endpoint\"),\n servingEndpointPermissionSchema,\n ),\n makeResourceVariant(z.literal(\"volume\"), volumePermissionSchema),\n makeResourceVariant(\n z.literal(\"vector_search_index\"),\n vectorSearchIndexPermissionSchema,\n ),\n makeResourceVariant(z.literal(\"uc_function\"), ucFunctionPermissionSchema),\n makeResourceVariant(\n z.literal(\"uc_connection\"),\n ucConnectionPermissionSchema,\n ),\n makeResourceVariant(z.literal(\"database\"), databasePermissionSchema),\n makeResourceVariant(z.literal(\"postgres\"), postgresPermissionSchema),\n makeResourceVariant(z.literal(\"genie_space\"), genieSpacePermissionSchema),\n makeResourceVariant(z.literal(\"experiment\"), experimentPermissionSchema),\n makeResourceVariant(z.literal(\"app\"), appPermissionSchema),\n ])\n .describe(\n \"Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements().\",\n );\n\n// ── Config schema (recursive) ────────────────────────────────────────────\n\nexport const configSchemaPropertySchema: z.ZodType = z.lazy(() =>\n z\n .object({\n type: z.enum([\n \"object\",\n \"array\",\n \"string\",\n \"number\",\n \"boolean\",\n \"integer\",\n ]),\n description: z.string().optional(),\n default: z.unknown().optional(),\n enum: z.array(z.unknown()).optional(),\n properties: z.record(z.string(), configSchemaPropertySchema).optional(),\n items: configSchemaPropertySchema.optional(),\n minimum: z.number().optional(),\n maximum: z.number().optional(),\n minLength: z.number().int().min(0).optional(),\n maxLength: z.number().int().min(0).optional(),\n required: z.array(z.string()).optional(),\n // `additionalProperties` is a standard JSON Schema keyword used by core\n // plugin manifests (e.g., serving, ai-search, genie) to constrain\n // dictionary-shaped properties. Allowed on nested property entries as\n // either a boolean or a sub-schema, mirroring JSON Schema semantics.\n additionalProperties: z\n .union([z.boolean(), configSchemaPropertySchema])\n .optional(),\n })\n .strict(),\n);\n\nexport const configSchemaSchema: z.ZodType = z.lazy(() =>\n z\n .object({\n type: z.enum([\"object\", \"array\", \"string\", \"number\", \"boolean\"]),\n properties: z.record(z.string(), configSchemaPropertySchema).optional(),\n items: configSchemaSchema.optional(),\n required: z.array(z.string()).optional(),\n additionalProperties: z.boolean().optional(),\n })\n .strict(),\n);\n\n// ── Plugin-level scaffolding rules ───────────────────────────────────────\n\n/**\n * Per-item upper bound on plugin-level scaffolding rule strings. Matches the\n * template-level `SCAFFOLDING_RULE_MAX_LENGTH` defined below — rules at both\n * levels are short directives, not prose. The literal value lives here (and\n * not by reference to the template-side constant) because this declaration\n * is read in source order and the template constant is declared later.\n */\nconst PLUGIN_SCAFFOLDING_RULE_MAX_LENGTH = 120;\n\nconst pluginScaffoldingRuleItemSchema = z\n .string()\n .min(1)\n .max(\n PLUGIN_SCAFFOLDING_RULE_MAX_LENGTH,\n `rule entries must be ≤ ${PLUGIN_SCAFFOLDING_RULE_MAX_LENGTH} chars`,\n );\n\nexport const pluginScaffoldingRulesSchema = z\n .object({\n must: z\n .array(pluginScaffoldingRuleItemSchema)\n .optional()\n .describe(\"Actions the scaffolding agent must always perform.\"),\n should: z\n .array(pluginScaffoldingRuleItemSchema)\n .optional()\n .describe(\"Recommended actions for the scaffolding agent.\"),\n never: z\n .array(pluginScaffoldingRuleItemSchema)\n .optional()\n .describe(\"Actions the scaffolding agent must never perform.\"),\n })\n .strict()\n .superRefine((rules, ctx) => {\n // (a) Reject duplicate entries within any single array.\n const buckets: Array<[\"must\" | \"should\" | \"never\", string[] | undefined]> =\n [\n [\"must\", rules.must],\n [\"should\", rules.should],\n [\"never\", rules.never],\n ];\n for (const [bucketName, items] of buckets) {\n if (!items) continue;\n const seen = new Map<string, number>();\n items.forEach((item, idx) => {\n const prev = seen.get(item);\n if (prev === undefined) {\n seen.set(item, idx);\n return;\n }\n ctx.addIssue({\n code: \"custom\",\n path: [bucketName, idx],\n message: `duplicate rule entry: \"${item}\" already declared at index ${prev}`,\n });\n });\n }\n // (b) Reject a string that appears in more than one of must/should/never.\n type Bucket = \"must\" | \"should\" | \"never\";\n const owner = new Map<string, Bucket>();\n for (const [bucketName, items] of buckets) {\n if (!items) continue;\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n const existing = owner.get(item);\n if (existing === undefined) {\n owner.set(item, bucketName);\n continue;\n }\n if (existing !== bucketName) {\n ctx.addIssue({\n code: \"custom\",\n path: [bucketName, i],\n message: `rule entry \"${item}\" appears in both '${existing}' and '${bucketName}'; rules must belong to exactly one bucket`,\n });\n }\n }\n }\n })\n .describe(\n \"Structured rules for scaffolding agents declared at the plugin level. Each rule is a short directive (≤120 chars).\",\n );\n\n// ── Plugin manifest (root) ───────────────────────────────────────────────\n\nexport const pluginManifestSchema = z\n .object({\n scopes: z\n .array(capabilityScopeSchema)\n .optional()\n .describe(\"Capability-only user_api_scopes with no resource ID.\"),\n $schema: z\n .string()\n .optional()\n .describe(\"Reference to the JSON Schema for validation\"),\n name: z\n .string()\n .regex(PLUGIN_NAME_PATTERN)\n .describe(\n \"Plugin identifier and JS binding. Must start with a lowercase letter; camelCase for multi-word names (e.g. aiSearch).\",\n ),\n displayName: z\n .string()\n .min(1)\n .describe(\"Human-readable display name for UI and CLI\"),\n description: z\n .string()\n .min(1)\n .describe(\"Brief description of what the plugin does\"),\n resources: z\n .object({\n required: z\n .array(resourceRequirementSchema)\n .describe(\n \"Resources that must be available for the plugin to function\",\n ),\n optional: z\n .array(resourceRequirementSchema)\n .describe(\n \"Resources that enhance functionality but are not mandatory\",\n ),\n })\n .strict()\n .describe(\"Databricks resource requirements for this plugin\"),\n config: z\n .object({\n schema: configSchemaSchema.optional(),\n })\n .strict()\n .optional()\n .describe(\"Configuration schema for the plugin\"),\n author: z.string().optional().describe(\"Author name or organization\"),\n version: z\n .string()\n .regex(/^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$/)\n .optional()\n .describe(\"Plugin version (semver format)\"),\n repository: z\n .url()\n .optional()\n .describe(\"URL to the plugin's source repository\"),\n keywords: z\n .array(z.string())\n .optional()\n .describe(\"Keywords for plugin discovery\"),\n license: z.string().optional().describe(\"SPDX license identifier\"),\n onSetupMessage: z\n .string()\n .optional()\n .describe(\n \"Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning).\",\n ),\n hidden: z\n .boolean()\n .optional()\n .describe(\n \"When true, this plugin is excluded from the template plugins manifest (appkit.plugins.json) during sync.\",\n ),\n devOnly: z\n .boolean()\n .optional()\n .describe(\n \"When true, this plugin is only registered when NODE_ENV === 'development'. In any other environment createApp skips it entirely (not constructed, no routes, resources not validated). Use for dev-only tooling that must never run in a deployed app.\",\n ),\n stability: z\n .enum([\"beta\", \"ga\"])\n .optional()\n .describe(\n \"Plugin stability level. Beta plugins may have breaking API changes between minor releases but are on a path to GA. GA (general availability) plugins follow semver strictly.\",\n ),\n deprecated: z\n .boolean()\n .optional()\n .describe(\n \"When true, the plugin is deprecated. It still ships and functions, but tooling (e.g. `appkit plugin list`) may hide or flag it. The recommended replacement is noted in the plugin description.\",\n ),\n scaffolding: z\n .object({\n rules: pluginScaffoldingRulesSchema\n .optional()\n .describe(\n \"Structured rules for scaffolding agents declared at the plugin level.\",\n ),\n })\n .strict()\n .optional()\n .describe(\n \"Plugin-level scaffolding metadata consumed by scaffolding agents. Symmetric with template-level `scaffolding`.\",\n ),\n })\n .strict()\n .describe(\n \"Schema for Databricks AppKit plugin manifest files. Defines plugin metadata, resource requirements, and configuration options.\",\n );\n\n// ── Origin enum ──────────────────────────────────────────────────────────\n\nexport const originSchema = z\n .enum([\"user\", \"platform\", \"static\", \"cli\"])\n .describe(\n \"How the field value is determined. Computed during sync, not authored by plugin developers.\",\n );\n\n// ── Template field entry (origin computed by transform) ─────────────────\n\n/**\n * Derives the canonical origin of a resource field value from its shape.\n *\n * - `localOnly: true` → `\"platform\"` (auto-injected by the Databricks Apps\n * platform at deploy time; takes precedence over `value`/`resolve`).\n * - `value !== undefined` → `\"static\"` (hardcoded value).\n * - `resolve !== undefined` → `\"cli\"` (resolved by the CLI during init).\n * - else → `\"user\"` (user must provide the value at init time).\n *\n * The single source for this rule: `plugin sync`'s transform stamps `origin`\n * with it, and consumers that read an authored manifest (no stamped `origin`)\n * derive it through this rather than re-implementing the cascade.\n */\nexport function computeOriginFromField(field: {\n localOnly?: boolean;\n value?: string;\n resolve?: string;\n}): z.infer<typeof originSchema> {\n if (field.localOnly) return \"platform\";\n if (field.value !== undefined) return \"static\";\n if (field.resolve !== undefined) return \"cli\";\n return \"user\";\n}\n\n/**\n * Template field entry: extends the plugin manifest field entry with an\n * optional `origin` input slot, then runs a `.transform()` that overwrites\n * `origin` with the computed value. Allowing `origin` on input means\n * re-parsing a previously-synced template manifest does not fail; emitting\n * `origin` always means hand-edits in synced JSON are silently corrected\n * on the next parse — drift-by-construction.\n */\nexport const templateFieldEntrySchema = resourceFieldEntrySchema\n .extend({ origin: originSchema.optional() })\n .transform((field) => ({\n ...field,\n origin: computeOriginFromField(field),\n }));\n\n// ── Template resource requirement (uses templateFieldEntrySchema) ────────\n\nconst templateResourceRequirementBaseShape = {\n alias: z\n .string()\n .min(1)\n .describe(\"Human-readable label for UI/display only.\"),\n resourceKey: z\n .string()\n .regex(/^[a-z][a-z0-9-]*$/)\n .describe(\n \"Stable key for machine use: deduplication, env naming, composite keys.\",\n ),\n description: z\n .string()\n .min(1)\n .describe(\"Human-readable description of why this resource is needed\"),\n fields: z\n .record(z.string(), templateFieldEntrySchema)\n .refine((obj) => Object.keys(obj).length >= 1, {\n message: \"fields must contain at least one entry\",\n })\n .optional()\n .describe(\"Map of field name to field entry with computed origin.\"),\n};\n\nfunction makeTemplateResourceVariant<\n TType extends z.ZodLiteral<string>,\n TPerm extends z.ZodTypeAny,\n>(typeLiteral: TType, permission: TPerm) {\n return z\n .object({\n type: typeLiteral,\n ...templateResourceRequirementBaseShape,\n permission: permission.describe(\n \"Required permission level. Validated per resource type.\",\n ),\n })\n .strict()\n .superRefine(refineResourceDependsOn);\n}\n\nexport const templateResourceRequirementSchema = z\n .discriminatedUnion(\"type\", [\n makeTemplateResourceVariant(z.literal(\"secret\"), secretPermissionSchema),\n makeTemplateResourceVariant(z.literal(\"job\"), jobPermissionSchema),\n makeTemplateResourceVariant(\n z.literal(\"sql_warehouse\"),\n sqlWarehousePermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"serving_endpoint\"),\n servingEndpointPermissionSchema,\n ),\n makeTemplateResourceVariant(z.literal(\"volume\"), volumePermissionSchema),\n makeTemplateResourceVariant(\n z.literal(\"vector_search_index\"),\n vectorSearchIndexPermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"uc_function\"),\n ucFunctionPermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"uc_connection\"),\n ucConnectionPermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"database\"),\n databasePermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"postgres\"),\n postgresPermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"genie_space\"),\n genieSpacePermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"experiment\"),\n experimentPermissionSchema,\n ),\n makeTemplateResourceVariant(z.literal(\"app\"), appPermissionSchema),\n ])\n .describe(\n \"Resource requirement with template-specific field entries (includes computed origin).\",\n );\n\n// ── Template plugin (extends plugin manifest) ────────────────────────────\n\nexport const templatePluginSchema = z\n .object({\n name: z\n .string()\n .regex(PLUGIN_NAME_PATTERN)\n .describe(\n \"Plugin identifier and JS binding. Must start with a lowercase letter; camelCase for multi-word names (e.g. aiSearch).\",\n ),\n displayName: z\n .string()\n .min(1)\n .describe(\"Human-readable display name for UI and CLI\"),\n description: z\n .string()\n .min(1)\n .describe(\"Brief description of what the plugin does\"),\n package: z\n .string()\n .min(1)\n .describe(\"NPM package name that provides this plugin\"),\n requiredByTemplate: z\n .boolean()\n .optional()\n .describe(\n \"When true, this plugin is required by the template and cannot be deselected during CLI init. The user will only be prompted to configure its resources. When absent or false, the plugin is optional and the user can choose whether to include it.\",\n ),\n onSetupMessage: z\n .string()\n .optional()\n .describe(\n \"Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning).\",\n ),\n stability: z\n .enum([\"beta\", \"ga\"])\n .optional()\n .describe(\n \"Plugin stability level. Beta is heading to GA; APIs may change between minor releases. GA (general availability) follows semver.\",\n ),\n deprecated: z\n .boolean()\n .optional()\n .describe(\n \"When true, the plugin is deprecated. It still ships and functions, but tooling (e.g. `appkit plugin list`) may hide or flag it. The recommended replacement is noted in the plugin description.\",\n ),\n scaffolding: z\n .object({\n rules: pluginScaffoldingRulesSchema\n .optional()\n .describe(\n \"Structured rules for scaffolding agents propagated from the plugin manifest.\",\n ),\n })\n .strict()\n .optional()\n .describe(\n \"Plugin-level scaffolding metadata propagated from the plugin manifest.\",\n ),\n resources: z\n .object({\n required: z\n .array(templateResourceRequirementSchema)\n .describe(\n \"Resources that must be available for the plugin to function\",\n ),\n optional: z\n .array(templateResourceRequirementSchema)\n .describe(\n \"Resources that enhance functionality but are not mandatory\",\n ),\n })\n .strict()\n .describe(\"Databricks resource requirements for this plugin\"),\n })\n .strict()\n .describe(\"Plugin manifest with package source information\");\n\n// ── Scaffolding descriptor ───────────────────────────────────────────────\n\nexport const scaffoldingFlagSchema = z\n .object({\n description: z.string().describe(\"Human-readable description of the flag.\"),\n required: z.boolean().optional().describe(\"Whether this flag is required.\"),\n pattern: z\n .string()\n .optional()\n .describe(\"Regex pattern for validating the flag value.\"),\n default: z.string().optional().describe(\"Default value for this flag.\"),\n })\n .strict()\n .describe(\"A flag for the scaffolding command.\");\n\n/**\n * Per-item upper bound on scaffolding rule strings. The intent is to enforce\n * \"short directive\" by contract — long paragraphs fail validation and force\n * authors to split prose into discrete actionable items.\n */\nconst SCAFFOLDING_RULE_MAX_LENGTH = 120;\n\nconst scaffoldingRuleItemSchema = z\n .string()\n .max(\n SCAFFOLDING_RULE_MAX_LENGTH,\n `rule item must be ≤ ${SCAFFOLDING_RULE_MAX_LENGTH} chars`,\n );\n\nexport const scaffoldingRulesSchema = z\n .object({\n never: z\n .array(scaffoldingRuleItemSchema)\n .optional()\n .describe(\"Actions the scaffolding agent must never perform.\"),\n must: z\n .array(scaffoldingRuleItemSchema)\n .optional()\n .describe(\"Actions the scaffolding agent must always perform.\"),\n should: z\n .array(scaffoldingRuleItemSchema)\n .optional()\n .describe(\n \"Recommended actions for the scaffolding agent (parity with plugin-level rules).\",\n ),\n })\n .strict()\n .describe(\"Structured rules for scaffolding agents.\");\n\nexport const scaffoldingDescriptorSchema = z\n .object({\n command: z\n .string()\n .describe(\"The scaffolding command (e.g., 'databricks apps init').\"),\n flags: z\n .record(z.string(), scaffoldingFlagSchema)\n .optional()\n .describe(\"Map of flag name to flag descriptor.\"),\n rules: scaffoldingRulesSchema\n .optional()\n .describe(\"Structured rules for scaffolding agents.\"),\n })\n .strict()\n .describe(\n \"Describes the scaffolding command, flags, and rules for project initialization.\",\n );\n\n/**\n * Canonical scaffolding descriptor for the `databricks apps init` command,\n * embedded in v2.0 template manifests to guide scaffolding agents.\n *\n * Co-located with `scaffoldingDescriptorSchema` so any change to the rule set\n * (or the schema's `maxLength` ceiling) shows up next to its consumer. The\n * `satisfies` annotation gives compile-time validation that the literal\n * matches the schema's input shape; if a `must`/`never` entry exceeds the\n * `maxLength` ceiling at runtime, `scaffoldingDescriptorSchema.parse` would\n * surface the breach in tests.\n */\nexport const TEMPLATE_SCAFFOLDING = {\n command: \"databricks apps init\",\n flags: {\n \"--name\": {\n description:\n \"Project name — sets {{.projectName}} in package.json, databricks.yml, and .env. Required for non-interactive scaffolding.\",\n required: true,\n pattern: \"^[a-z][a-z0-9-]*$\",\n },\n \"--template\": {\n description: \"Template path (local directory or GitHub URL)\",\n required: false,\n },\n \"--version\": {\n description: \"AppKit version to use; defaults to auto-detected\",\n required: false,\n },\n \"--features\": {\n description:\n \"Plugins to enable (comma-separated, no spaces; must match keys in this manifest's plugins map)\",\n required: false,\n pattern: \"^[a-zA-Z0-9_-]+(,[a-zA-Z0-9_-]+)*$\",\n },\n \"--set\": {\n description:\n \"Set resource values (format: plugin.resourceKey.field=value, repeatable)\",\n required: false,\n },\n \"--output-dir\": {\n description: \"Directory to write the project to\",\n required: false,\n },\n \"--description\": {\n description: \"App description\",\n required: false,\n },\n \"--run\": {\n description: \"Run the app after creation (none, dev, dev-remote)\",\n required: false,\n },\n \"--auto-approve\": {\n description:\n \"Pass as a bare flag (no value) to skip prompts for optional resources. Not recommended for agent-driven init — conflicts with the 'ask user when in doubt' rule.\",\n required: false,\n },\n \"--profile\": {\n description:\n \"Databricks CLI profile to use for authentication (global flag)\",\n required: false,\n },\n },\n rules: {\n must: [\n \"Treat `databricks apps init` output as starter code, not requirements; adapt or replace it to match the requested app\",\n \"Keep all secrets and credentials only in app.yaml, databricks.yml, and/or .env\",\n ],\n should: [\"ask user when in doubt of resource to use for plugin\"],\n never: [\n \"guess resources when multiple or no options are available\",\n \"embed secrets in files that will go to the client-bundle\",\n ],\n },\n} satisfies z.infer<typeof scaffoldingDescriptorSchema>;\n\n// ── Template plugins manifest (root) ─────────────────────────────────────\n\nexport const templatePluginsManifestSchema = z\n .object({\n $schema: z\n .string()\n .optional()\n .describe(\"Reference to the JSON Schema for validation\"),\n version: z\n .enum([\"1.0\", \"1.1\", \"2.0\"])\n .describe(\"Schema version for the template plugins manifest\"),\n plugins: z\n .record(z.string(), templatePluginSchema)\n .describe(\"Map of plugin name to plugin manifest with package source\"),\n scaffolding: scaffoldingDescriptorSchema\n .optional()\n .describe(\n \"Describes the scaffolding command and its configuration for project initialization.\",\n ),\n })\n .strict()\n .superRefine((value, ctx) => {\n if (value.version === \"2.0\" && !value.scaffolding) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"scaffolding\"],\n message: \"scaffolding is required when version is '2.0'\",\n });\n }\n })\n .describe(\n \"Aggregated plugin manifest for AppKit templates. Read by Databricks CLI during init to discover available plugins and their resource requirements.\",\n );\n\n// ── Inferred types ───────────────────────────────────────────────────────\n\nexport type ResourceType = z.infer<typeof resourceTypeSchema>;\nexport type SecretPermission = z.infer<typeof secretPermissionSchema>;\nexport type JobPermission = z.infer<typeof jobPermissionSchema>;\nexport type SqlWarehousePermission = z.infer<\n typeof sqlWarehousePermissionSchema\n>;\nexport type ServingEndpointPermission = z.infer<\n typeof servingEndpointPermissionSchema\n>;\nexport type VolumePermission = z.infer<typeof volumePermissionSchema>;\nexport type VectorSearchIndexPermission = z.infer<\n typeof vectorSearchIndexPermissionSchema\n>;\nexport type UcFunctionPermission = z.infer<typeof ucFunctionPermissionSchema>;\nexport type UcConnectionPermission = z.infer<\n typeof ucConnectionPermissionSchema\n>;\nexport type DatabasePermission = z.infer<typeof databasePermissionSchema>;\nexport type PostgresPermission = z.infer<typeof postgresPermissionSchema>;\nexport type GenieSpacePermission = z.infer<typeof genieSpacePermissionSchema>;\nexport type ExperimentPermission = z.infer<typeof experimentPermissionSchema>;\nexport type AppPermission = z.infer<typeof appPermissionSchema>;\nexport type ResourceKind = z.infer<typeof resourceKindSchema>;\nexport type KindDiscoveryDescriptor = z.infer<\n typeof kindDiscoveryDescriptorSchema\n>;\nexport type CliDiscoveryDescriptor = z.infer<\n typeof cliDiscoveryDescriptorSchema\n>;\nexport type DiscoveryDescriptor = z.infer<typeof discoveryDescriptorSchema>;\nexport type ResourceFieldEntry = z.infer<typeof resourceFieldEntrySchema>;\nexport type ResourceRequirement = z.infer<typeof resourceRequirementSchema>;\nexport type ConfigSchemaProperty = z.infer<typeof configSchemaPropertySchema>;\nexport type ConfigSchema = z.infer<typeof configSchemaSchema>;\nexport type PluginScaffoldingRules = z.infer<\n typeof pluginScaffoldingRulesSchema\n>;\nexport type PluginManifest = z.infer<typeof pluginManifestSchema>;\nexport type Origin = z.infer<typeof originSchema>;\n// Template-side types use `z.input` so callers can construct a TemplatePlugin\n// from a parsed PluginManifest before the field-level origin transform runs.\n// `writeManifest` parses every field through `templateFieldEntrySchema` at\n// write-time, so the on-disk shape always has origin populated. The runtime\n// invariant: origin is *always* present after sync writes; the type slot\n// stays optional so the in-memory pipeline does not need to fabricate origin\n// before assignment.\nexport type TemplateFieldEntry = z.input<typeof templateFieldEntrySchema>;\nexport type TemplateResourceRequirement = z.input<\n typeof templateResourceRequirementSchema\n>;\nexport type TemplatePlugin = z.input<typeof templatePluginSchema>;\nexport type ScaffoldingFlag = z.infer<typeof scaffoldingFlagSchema>;\nexport type ScaffoldingRules = z.infer<typeof scaffoldingRulesSchema>;\nexport type ScaffoldingDescriptor = z.infer<typeof scaffoldingDescriptorSchema>;\nexport type TemplatePluginsManifest = z.input<\n typeof templatePluginsManifestSchema\n>;\n"],"mappings":"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"}
1
+ {"version":3,"file":"manifest.js","names":[],"sources":["../../../../../shared/src/schemas/manifest.ts"],"sourcesContent":["/**\n * Zod-authoring module for AppKit plugin manifest schemas.\n *\n * Single source of truth for the plugin manifest contract. JSON Schema\n * artifacts published at the docs URL are emitted from these schemas via\n * `tools/generate-json-schema.ts` and live only in `docs/static/schemas/`\n * (no package-internal copies).\n *\n * - Cross-field constraints (cycle/dangling-reference checks, `<PROFILE>`\n * placeholder, post-scaffold instruction non-empty) are refinements\n * co-located with the shape they constrain. Validation is driven through\n * the Standard Schema interface from `validate-manifest.ts`.\n * - `templateFieldEntrySchema` is a transform that emits `origin` from\n * `localOnly`/`value`/`resolve`. The input slot is still allowed so\n * re-parsing previously-synced template manifests does not fail, but the\n * transform always overwrites it — drift-by-construction for hand-edits.\n * - `discoveryDescriptorSchema` is a discriminated union over a `type`\n * literal. The `kind` variant references one of the well-known\n * `resourceKind` values for which AppKit owns the CLI command map (see\n * `RESOURCE_KIND_COMMANDS` below). The `cli` variant is the escape hatch\n * carrying the existing free-form fields (with the `<PROFILE>`\n * refinement). Hierarchical context for volumes (catalog/schema parent\n * walk) is encoded via the kind's `parents` array, not via dependsOn.\n * - Scaffolding rule items carry a `maxLength` (120 chars) so\n * `rules.never[]` / `rules.must[]` / `rules.should[]` stay short\n * directives by contract, and the canonical `TEMPLATE_SCAFFOLDING`\n * constant lives co-located with the scaffolding schemas (sync.ts\n * imports it).\n */\n\nimport { z } from \"zod\";\n\nimport { PLUGIN_NAME_PATTERN } from \"../naming\";\n\n// ── Resource type + per-type permission enums ────────────────────────────\n\nexport const resourceTypeSchema = z\n .enum([\n \"secret\",\n \"job\",\n \"sql_warehouse\",\n \"serving_endpoint\",\n \"volume\",\n \"vector_search_index\",\n \"uc_function\",\n \"uc_connection\",\n \"database\",\n \"postgres\",\n \"genie_space\",\n \"experiment\",\n \"app\",\n ])\n .describe(\"Type of Databricks resource\");\n\n/** Apps user_api_scopes for resource types with confirmed OBO support. */\nexport const SCOPE_BY_TYPE = {\n sql_warehouse: \"sql\", // sql:restricted-query is the read-only variant.\n serving_endpoint: \"model-serving\",\n genie_space: \"genie\",\n volume: \"files\",\n vector_search_index: \"vector-search\",\n uc_connection: \"catalog.connections\",\n // uc_function uses sql, or mcp.functions through managed MCP. Confirm later.\n // experiment and job are SP-only; there is no mlflow or jobs scope.\n} as const satisfies Partial<Record<ResourceType, string>>;\n\n// A postgres user_api_scope exists, so Lakebase is platform-OBO-capable.\n// It stays app-only for v1 because the connector connects as the SP today (audit A7).\nexport const APP_ONLY_RESOURCE_TYPES: ReadonlySet<ResourceType> = new Set([\n \"secret\",\n \"database\",\n \"postgres\",\n]);\n\n/**\n * How a resource type binds in `databricks.yml` as a DABs app resource. Owned\n * here so the CLI consumes it as data instead of hardcoding a per-type map.\n *\n * - `yamlKey`: the DABs YAML key under the resource entry (e.g. `sql_warehouse`,\n * `uc_securable`).\n * - `varFields`: `[manifestField, dabsField]` pairs. Each becomes a\n * `${var.<resourceKey>_<manifestField>}` reference written to `dabsField`.\n * - `staticFields`: `[dabsField, value]` constant pairs (e.g.\n * `securable_type` = `VOLUME`).\n *\n * Permission is not here; it stays the per-resource `permission` field.\n */\nexport interface ResourceBinding {\n readonly yamlKey: string;\n readonly varFields: ReadonlyArray<readonly [string, string]>;\n readonly staticFields?: ReadonlyArray<readonly [string, string]>;\n}\n\n/**\n * DABs binding spec per resource type. Faithful port of the CLI's\n * `appResourceSpecs`. App-only types still bind (as the service principal). The\n * `app` type is intentionally absent: bundles do not yet support it as an app\n * resource, matching the commented-out CLI entry.\n *\n * TODO(sdk-migration): anchor these yamlKeys to the Apps SDK once the modular migration\n * (analytics-migration-sdk / #562) adds @databricks/sdk-apps:\n * 1. add @databricks/sdk-apps; re-export AppResource via packages/shared/src/workspace-client/modular.ts\n * (direct @databricks/sdk-* imports are banned by the repo lint rule)\n * 2. the new AppResource is a $case union: kinds = NonNullable<AppResource[\"resource\"]>[\"$case\"]\n * (camelCase: sqlWarehouse | servingEndpoint | genieSpace | ucSecurable | ...)\n * 3. map camelCase $case -> snake_case yamlKey and assert every table entry is covered\n * (the new SDK models postgres/experiment/app, so the old skew exceptions are not needed)\n */\nexport const DABS_BINDING_BY_TYPE = {\n sql_warehouse: { yamlKey: \"sql_warehouse\", varFields: [[\"id\", \"id\"]] },\n job: { yamlKey: \"job\", varFields: [[\"id\", \"id\"]] },\n serving_endpoint: {\n yamlKey: \"serving_endpoint\",\n varFields: [[\"name\", \"name\"]],\n },\n experiment: {\n yamlKey: \"experiment\",\n varFields: [[\"experimentId\", \"experiment_id\"]],\n },\n secret: {\n yamlKey: \"secret\",\n varFields: [\n [\"scope\", \"scope\"],\n [\"key\", \"key\"],\n ],\n },\n database: {\n yamlKey: \"database\",\n varFields: [\n [\"instance_name\", \"instance_name\"],\n [\"database_name\", \"database_name\"],\n ],\n },\n postgres: {\n yamlKey: \"postgres\",\n varFields: [\n [\"branch\", \"branch\"],\n [\"database\", \"database\"],\n ],\n },\n genie_space: {\n yamlKey: \"genie_space\",\n varFields: [\n [\"name\", \"name\"],\n [\"id\", \"space_id\"],\n ],\n },\n volume: {\n yamlKey: \"uc_securable\",\n varFields: [[\"path\", \"securable_full_name\"]],\n staticFields: [[\"securable_type\", \"VOLUME\"]],\n },\n uc_function: {\n yamlKey: \"uc_securable\",\n varFields: [[\"id\", \"securable_full_name\"]],\n staticFields: [[\"securable_type\", \"FUNCTION\"]],\n },\n uc_connection: {\n yamlKey: \"uc_securable\",\n varFields: [[\"id\", \"securable_full_name\"]],\n staticFields: [[\"securable_type\", \"CONNECTION\"]],\n },\n vector_search_index: {\n yamlKey: \"uc_securable\",\n varFields: [[\"id\", \"securable_full_name\"]],\n staticFields: [[\"securable_type\", \"TABLE\"]],\n },\n} as const satisfies Partial<Record<ResourceType, ResourceBinding>>;\n\n/** Capabilities that need a user_api_scope but have no resource ID. */\nexport const capabilityScopeSchema = z.enum([\n \"ai-gateway\",\n \"mcp.external\",\n \"mcp.functions\",\n \"workspace.workspace\",\n \"catalog.catalogs:read\",\n \"catalog.schemas:read\",\n \"catalog.tables:read\",\n]);\n\n/**\n * Every Apps user_api_scope (short names; the long forms such as\n * `dashboards.genie` are deprecated aliases). A plugin declares one in its\n * `scopes` when it always calls on behalf of the user, or when the capability\n * has no resource ID.\n */\nexport const userApiScopeSchema = z.enum([\n \"sql\",\n \"sql:restricted-query\",\n \"genie\",\n \"postgres\",\n \"model-serving\",\n \"files\",\n \"vector-search\",\n \"catalog.connections\",\n ...capabilityScopeSchema.options,\n]);\n\nexport type UserApiScope = z.infer<typeof userApiScopeSchema>;\n\nexport const secretPermissionSchema = z\n .enum([\"READ\", \"WRITE\", \"MANAGE\"])\n .describe(\"Permission for secret resources (order: weakest to strongest)\");\n\nexport const jobPermissionSchema = z\n .enum([\"CAN_VIEW\", \"CAN_MANAGE_RUN\", \"CAN_MANAGE\"])\n .describe(\"Permission for job resources (order: weakest to strongest)\");\n\nexport const sqlWarehousePermissionSchema = z\n .enum([\"CAN_USE\", \"CAN_MANAGE\"])\n .describe(\n \"Permission for SQL warehouse resources (order: weakest to strongest)\",\n );\n\nexport const servingEndpointPermissionSchema = z\n .enum([\"CAN_VIEW\", \"CAN_QUERY\", \"CAN_MANAGE\"])\n .describe(\n \"Permission for serving endpoint resources (order: weakest to strongest)\",\n );\n\nexport const volumePermissionSchema = z\n .enum([\"READ_VOLUME\", \"WRITE_VOLUME\"])\n .describe(\"Permission for Unity Catalog volume resources\");\n\nexport const vectorSearchIndexPermissionSchema = z\n .enum([\"SELECT\"])\n .describe(\"Permission for vector search index resources\");\n\nexport const ucFunctionPermissionSchema = z\n .enum([\"EXECUTE\"])\n .describe(\"Permission for Unity Catalog function resources\");\n\nexport const ucConnectionPermissionSchema = z\n .enum([\"USE_CONNECTION\"])\n .describe(\"Permission for Unity Catalog connection resources\");\n\nexport const databasePermissionSchema = z\n .enum([\"CAN_CONNECT_AND_CREATE\"])\n .describe(\"Permission for database resources\");\n\nexport const postgresPermissionSchema = z\n .enum([\"CAN_CONNECT_AND_CREATE\"])\n .describe(\"Permission for Postgres resources\");\n\nexport const genieSpacePermissionSchema = z\n .enum([\"CAN_VIEW\", \"CAN_RUN\", \"CAN_EDIT\", \"CAN_MANAGE\"])\n .describe(\n \"Permission for Genie Space resources (order: weakest to strongest)\",\n );\n\nexport const experimentPermissionSchema = z\n .enum([\"CAN_READ\", \"CAN_EDIT\", \"CAN_MANAGE\"])\n .describe(\n \"Permission for MLflow experiment resources (order: weakest to strongest)\",\n );\n\nexport const appPermissionSchema = z\n .enum([\"CAN_USE\"])\n .describe(\"Permission for Databricks App resources\");\n\n// ── Discovery descriptor (discriminated union) ───────────────────────────\n\n/**\n * Well-known Databricks resource kinds for which AppKit owns the CLI\n * command map. Plugins reference one of these via the `kind` variant of the\n * discovery descriptor; everything else falls back to the free-form `cli`\n * variant.\n *\n * Kept narrow on purpose: each entry costs an addition to\n * `RESOURCE_KIND_COMMANDS` below, which is the single source of truth for\n * how that kind is enumerated.\n */\nexport const resourceKindSchema = z\n .enum([\n \"warehouse\",\n \"genie_space\",\n \"postgres_project\",\n \"postgres_branch\",\n \"postgres_database\",\n \"volume\",\n ])\n .describe(\n \"Well-known Databricks resource kind whose listing command is owned by AppKit (see RESOURCE_KIND_COMMANDS).\",\n );\n\nexport const kindDiscoveryDescriptorSchema = z\n .object({\n type: z\n .literal(\"kind\")\n .describe(\n \"Discriminator: 'kind' uses the AppKit-owned command map for the named resourceKind.\",\n ),\n resourceKind: resourceKindSchema.describe(\n \"Reference to a well-known Databricks resource kind. AppKit owns the CLI command, response shape, and unwrap rules.\",\n ),\n select: z\n .string()\n .optional()\n .describe(\n \"Field name in the parsed CLI response used as the selected value (e.g., 'id'). Defaults to the kind's natural identifier when omitted.\",\n ),\n display: z\n .string()\n .optional()\n .describe(\n \"Field name in the parsed CLI response shown to the user in selection UI. Defaults to `select` if omitted.\",\n ),\n dependsOn: z\n .string()\n .optional()\n .describe(\n \"Name of a sibling field within the same resource that must be resolved first. Used to express ordering dependencies between resource fields.\",\n ),\n shortcut: z\n .string()\n .optional()\n .describe(\n \"Single-value fast-path command that returns exactly one value, skipping interactive selection.\",\n ),\n })\n .strict()\n .describe(\n \"Discovery via a well-known resource kind. AppKit owns the CLI command and unwrap rules for the named kind.\",\n );\n\n/**\n * Shell metacharacters rejected on free-form CLI command strings. Catches the\n * common foot-guns (statement separators, pipes, redirects via shell, command\n * substitution, newlines). Not a security boundary on its own — executors must\n * always pass these via argv, never shell-exec the string. Angle brackets are\n * permitted because `<PROFILE>` (and future `<…>` placeholders) are part of\n * the command-template convention.\n */\nconst SHELL_METACHAR_RE = /[;|&`$\\n\\r]/;\n\nexport const cliDiscoveryDescriptorSchema = z\n .object({\n type: z\n .literal(\"cli\")\n .describe(\n \"Discriminator: 'cli' uses a free-form Databricks CLI command supplied by the plugin.\",\n ),\n cliCommand: z\n .string()\n .describe(\n \"Databricks CLI command that lists resources. Must include <PROFILE>. Shell metacharacters (;|&`$ and newlines) are rejected; for first-party Databricks resources prefer the `kind` variant which uses AppKit's typed command map.\",\n ),\n selectField: z\n .string()\n .describe(\n \"jq-style path to the field used as the selected value (e.g., '.id', '.name').\",\n ),\n displayField: z\n .string()\n .optional()\n .describe(\n \"jq-style path to the field shown to the user in selection UI. Defaults to selectField if omitted.\",\n ),\n dependsOn: z\n .string()\n .optional()\n .describe(\n \"Name of a sibling field within the same resource that must be resolved first. Used to express ordering dependencies between resource fields.\",\n ),\n shortcut: z\n .string()\n .optional()\n .describe(\n \"Single-value fast-path command that returns exactly one value, skipping interactive selection. Shell metacharacters are rejected.\",\n ),\n })\n .strict()\n .refine((descriptor) => descriptor.cliCommand.includes(\"<PROFILE>\"), {\n message: \"must include <PROFILE> placeholder\",\n path: [\"cliCommand\"],\n })\n .refine((descriptor) => !SHELL_METACHAR_RE.test(descriptor.cliCommand), {\n message:\n \"must not contain shell metacharacters (;|&`$ or newlines); use the `kind` variant for typed Databricks resources\",\n path: [\"cliCommand\"],\n })\n .refine(\n (descriptor) =>\n descriptor.shortcut === undefined ||\n !SHELL_METACHAR_RE.test(descriptor.shortcut),\n {\n message: \"must not contain shell metacharacters (;|&`$ or newlines)\",\n path: [\"shortcut\"],\n },\n )\n .describe(\n \"Discovery via a free-form Databricks CLI command. Escape hatch — prefer the `kind` variant when a typed resourceKind covers the resource. This shape is intentionally minimal and may tighten further in future versions.\",\n );\n\nexport const discoveryDescriptorSchema = z\n .discriminatedUnion(\"type\", [\n kindDiscoveryDescriptorSchema,\n cliDiscoveryDescriptorSchema,\n ])\n .describe(\n \"Describes how the CLI discovers values for a resource field. 'kind' references a well-known Databricks resource kind whose command is owned by AppKit; 'cli' is the escape hatch carrying a free-form Databricks CLI command.\",\n );\n\n// ── Resource kind → CLI command map ──────────────────────────────────────\n\n/**\n * Descriptor for how a well-known resource kind is listed via the\n * Databricks CLI.\n *\n * - `command` is the CLI invocation template. It carries two kinds of\n * placeholders:\n * - `<PROFILE>` — substituted with the user's CLI profile by the runner.\n * - `{<fieldName>}` — substituted with the resolved value of the named\n * sibling field (used for `dependsOn` chains).\n * - `unwrap`, when set, is the JSON path into the response wrapper (e.g.,\n * `\"warehouses\"` for `{ warehouses: [...] }`). Omitted when the response\n * is already a flat array.\n * - `parents`, when set, lists transient query inputs the runner must\n * collect (as free-text prompts) before invoking the command. Each\n * `parents[i]` value substitutes the matching `{name}` placeholder in\n * the command string. Unlike `dependsOn` (which references a sibling\n * field on the same resource), `parents` covers inputs that aren't\n * persisted as fields on the resource.\n */\nexport type ResourceKindCommand = {\n command: string;\n unwrap?: string;\n parents?: readonly string[];\n};\n\n/**\n * Single source of truth for AppKit-owned discovery commands.\n *\n * To add a new resource kind: extend `resourceKindSchema` and add an entry\n * here. Plugins reference the kind via `discovery: { type: \"kind\",\n * resourceKind: \"...\" }` and inherit the command + response shape.\n *\n * `unwrap` defaults are unset: the existing core plugin manifests use simple\n * jq paths (`.id`, `.name`, `.full_name`), implying the listed CLI commands\n * return flat arrays. Refine in a follow-up if a kind's CLI returns wrapped\n * data.\n *\n * Volume's catalog/schema parent context is supplied via the `parents`\n * array, which the runner collects from the user as free-text prompts\n * before invoking the listing command.\n */\nexport const RESOURCE_KIND_COMMANDS: Record<\n z.infer<typeof resourceKindSchema>,\n { command: string; unwrap?: string; parents?: readonly string[] }\n> = {\n warehouse: {\n command: \"databricks warehouses list --profile <PROFILE> --output json\",\n },\n genie_space: {\n command: \"databricks genie list-spaces --profile <PROFILE> --output json\",\n },\n postgres_project: {\n command:\n \"databricks postgres list-projects --profile <PROFILE> --output json\",\n },\n postgres_branch: {\n // {project} is a placeholder for the resolved value of the `project`\n // sibling field (declared via `dependsOn: \"project\"` on the kind variant).\n // The Databricks CLI requires the parent project resource name (format\n // `projects/{project_id}`) as a positional argument.\n command:\n \"databricks postgres list-branches {project} --profile <PROFILE> --output json\",\n },\n postgres_database: {\n // {branch} is a placeholder for the resolved value of the `branch`\n // sibling field (declared via `dependsOn: \"branch\"` on the kind variant).\n command:\n \"databricks postgres list-databases {branch} --profile <PROFILE> --output json\",\n },\n volume: {\n // `parents` declares free-text user prompts the runner must collect before\n // invoking the discovery command. Each `parents[i]` value substitutes the\n // matching `{name}` placeholder in the command string above. Unlike\n // `dependsOn` (which references a sibling field on the same resource),\n // `parents` covers transient query inputs that aren't persisted as fields.\n command:\n \"databricks volumes list {catalog} {schema} --profile <PROFILE> --output json\",\n parents: [\"catalog\", \"schema\"] as const,\n },\n};\n\n// ── Resource field entry (plugin manifest variant) ───────────────────────\n\nexport const resourceFieldEntrySchema = z\n .object({\n env: z\n .string()\n .regex(/^[A-Z][A-Z0-9_]*$/)\n .optional()\n .describe(\"Environment variable name for this field\"),\n description: z\n .string()\n .optional()\n .describe(\"Human-readable description for this field\"),\n bundleIgnore: z\n .boolean()\n .optional()\n .describe(\n \"When true, this field is excluded from Databricks bundle configuration (databricks.yml) generation.\",\n ),\n examples: z\n .array(z.string())\n .optional()\n .describe(\"Example values showing the expected format for this field\"),\n localOnly: z\n .boolean()\n .optional()\n .describe(\n \"When true, this field is only generated for local .env files. The Databricks Apps platform auto-injects it at deploy time.\",\n ),\n value: z\n .string()\n .optional()\n .describe(\n \"Static value for this field. Used when no prompted or resolved value exists.\",\n ),\n resolve: z\n .string()\n .regex(/^[a-z_]+:[a-zA-Z]+$/)\n .optional()\n .describe(\n \"Named resolver prefixed by resource type (e.g., 'postgres:host'). The CLI resolves this value during the init prompt flow.\",\n ),\n discovery: discoveryDescriptorSchema.optional(),\n })\n .strict()\n .describe(\n \"Defines a single field for a resource. Each field has its own environment variable and optional description. Single-value types use one key (e.g. id); multi-value types (database, secret) use multiple (e.g. instance_name, database_name or scope, key).\",\n );\n\n// ── Resource requirement (per-type permission discriminator) ─────────────\n\n/**\n * Build a per-type variant. Each variant fixes `type` to a literal and constrains\n * `permission` to the matching enum, mirroring the existing JSON Schema's\n * `allOf + if/then` block. `fields` and the rest of the shape come from a\n * shared base.\n */\nconst resourceRequirementBaseShape = {\n alias: z\n .string()\n .min(1)\n .describe(\n \"Human-readable label for UI/display only. Deduplication uses resourceKey, not alias.\",\n ),\n resourceKey: z\n .string()\n .regex(/^[a-z][a-z0-9-]*$/)\n .describe(\n \"Stable key for machine use: deduplication, env naming, composite keys, app.yaml. Required for registry lookup.\",\n ),\n description: z\n .string()\n .min(1)\n .describe(\"Human-readable description of why this resource is needed\"),\n fields: z\n .record(z.string(), resourceFieldEntrySchema)\n .refine((obj) => Object.keys(obj).length >= 1, {\n message: \"fields must contain at least one entry\",\n })\n .optional()\n .describe(\n \"Map of field name to env and optional description. Single-value types use one key (e.g. id); multi-value (database, secret) use multiple (e.g. instance_name, database_name or scope, key).\",\n ),\n};\n\n/**\n * Adds the cycle/dangling-reference cross-field check to a resource variant.\n * Iterates the resource's `fields`, validates each `discovery.dependsOn` target\n * is a sibling field name, then runs DFS over the dependsOn graph to detect\n * cycles. Issue paths target either the offending field's `dependsOn` slot or\n * the resource itself for cycles.\n */\nfunction refineResourceDependsOn(\n resource: { fields?: Record<string, { discovery?: { dependsOn?: string } }> },\n ctx: z.core.$RefinementCtx,\n): void {\n if (!resource.fields) return;\n const fieldNames = new Set(Object.keys(resource.fields));\n\n // Pass 1: validate dependsOn references and build the dependency graph.\n const deps = new Map<string, string>();\n for (const [name, field] of Object.entries(resource.fields)) {\n const dep = field.discovery?.dependsOn;\n if (!dep) continue;\n if (!fieldNames.has(dep)) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"fields\", name, \"discovery\", \"dependsOn\"],\n message: `references non-existent sibling field '${dep}'`,\n });\n }\n deps.set(name, dep);\n }\n\n // Pass 2: detect cycles via DFS. Emit one issue per cycle found.\n const visited = new Set<string>();\n const visiting = new Set<string>();\n\n function dfs(node: string, chain: string[]): string[] | null {\n if (visiting.has(node)) return [...chain, node];\n if (visited.has(node)) return null;\n visiting.add(node);\n const next = deps.get(node);\n if (next) {\n const cycle = dfs(next, [...chain, node]);\n if (cycle) return cycle;\n }\n visiting.delete(node);\n visited.add(node);\n return null;\n }\n\n for (const node of deps.keys()) {\n if (visited.has(node)) continue;\n const cycle = dfs(node, []);\n if (cycle) {\n ctx.addIssue({\n code: \"custom\",\n path: [],\n message: `discovery.dependsOn creates a cycle: ${cycle.join(\" → \")}`,\n });\n // One cycle error per resource is enough.\n break;\n }\n }\n}\n\nfunction makeResourceVariant<\n TType extends z.ZodLiteral<string>,\n TPerm extends z.ZodTypeAny,\n>(typeLiteral: TType, permission: TPerm) {\n return z\n .object({\n type: typeLiteral,\n ...resourceRequirementBaseShape,\n permission: permission.describe(\n \"Required permission level. Validated per resource type.\",\n ),\n })\n .strict()\n .superRefine(refineResourceDependsOn);\n}\n\nexport const resourceRequirementSchema = z\n .discriminatedUnion(\"type\", [\n makeResourceVariant(z.literal(\"secret\"), secretPermissionSchema),\n makeResourceVariant(z.literal(\"job\"), jobPermissionSchema),\n makeResourceVariant(\n z.literal(\"sql_warehouse\"),\n sqlWarehousePermissionSchema,\n ),\n makeResourceVariant(\n z.literal(\"serving_endpoint\"),\n servingEndpointPermissionSchema,\n ),\n makeResourceVariant(z.literal(\"volume\"), volumePermissionSchema),\n makeResourceVariant(\n z.literal(\"vector_search_index\"),\n vectorSearchIndexPermissionSchema,\n ),\n makeResourceVariant(z.literal(\"uc_function\"), ucFunctionPermissionSchema),\n makeResourceVariant(\n z.literal(\"uc_connection\"),\n ucConnectionPermissionSchema,\n ),\n makeResourceVariant(z.literal(\"database\"), databasePermissionSchema),\n makeResourceVariant(z.literal(\"postgres\"), postgresPermissionSchema),\n makeResourceVariant(z.literal(\"genie_space\"), genieSpacePermissionSchema),\n makeResourceVariant(z.literal(\"experiment\"), experimentPermissionSchema),\n makeResourceVariant(z.literal(\"app\"), appPermissionSchema),\n ])\n .describe(\n \"Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements().\",\n );\n\n// ── Config schema (recursive) ────────────────────────────────────────────\n\nexport const configSchemaPropertySchema: z.ZodType = z.lazy(() =>\n z\n .object({\n type: z.enum([\n \"object\",\n \"array\",\n \"string\",\n \"number\",\n \"boolean\",\n \"integer\",\n ]),\n description: z.string().optional(),\n default: z.unknown().optional(),\n enum: z.array(z.unknown()).optional(),\n properties: z.record(z.string(), configSchemaPropertySchema).optional(),\n items: configSchemaPropertySchema.optional(),\n minimum: z.number().optional(),\n maximum: z.number().optional(),\n minLength: z.number().int().min(0).optional(),\n maxLength: z.number().int().min(0).optional(),\n required: z.array(z.string()).optional(),\n // `additionalProperties` is a standard JSON Schema keyword used by core\n // plugin manifests (e.g., serving, ai-search, genie) to constrain\n // dictionary-shaped properties. Allowed on nested property entries as\n // either a boolean or a sub-schema, mirroring JSON Schema semantics.\n additionalProperties: z\n .union([z.boolean(), configSchemaPropertySchema])\n .optional(),\n })\n .strict(),\n);\n\nexport const configSchemaSchema: z.ZodType = z.lazy(() =>\n z\n .object({\n type: z.enum([\"object\", \"array\", \"string\", \"number\", \"boolean\"]),\n properties: z.record(z.string(), configSchemaPropertySchema).optional(),\n items: configSchemaSchema.optional(),\n required: z.array(z.string()).optional(),\n additionalProperties: z.boolean().optional(),\n })\n .strict(),\n);\n\n// ── Plugin-level scaffolding rules ───────────────────────────────────────\n\n/**\n * Per-item upper bound on plugin-level scaffolding rule strings. Matches the\n * template-level `SCAFFOLDING_RULE_MAX_LENGTH` defined below — rules at both\n * levels are short directives, not prose. The literal value lives here (and\n * not by reference to the template-side constant) because this declaration\n * is read in source order and the template constant is declared later.\n */\nconst PLUGIN_SCAFFOLDING_RULE_MAX_LENGTH = 120;\n\nconst pluginScaffoldingRuleItemSchema = z\n .string()\n .min(1)\n .max(\n PLUGIN_SCAFFOLDING_RULE_MAX_LENGTH,\n `rule entries must be ≤ ${PLUGIN_SCAFFOLDING_RULE_MAX_LENGTH} chars`,\n );\n\nexport const pluginScaffoldingRulesSchema = z\n .object({\n must: z\n .array(pluginScaffoldingRuleItemSchema)\n .optional()\n .describe(\"Actions the scaffolding agent must always perform.\"),\n should: z\n .array(pluginScaffoldingRuleItemSchema)\n .optional()\n .describe(\"Recommended actions for the scaffolding agent.\"),\n never: z\n .array(pluginScaffoldingRuleItemSchema)\n .optional()\n .describe(\"Actions the scaffolding agent must never perform.\"),\n })\n .strict()\n .superRefine((rules, ctx) => {\n // (a) Reject duplicate entries within any single array.\n const buckets: Array<[\"must\" | \"should\" | \"never\", string[] | undefined]> =\n [\n [\"must\", rules.must],\n [\"should\", rules.should],\n [\"never\", rules.never],\n ];\n for (const [bucketName, items] of buckets) {\n if (!items) continue;\n const seen = new Map<string, number>();\n items.forEach((item, idx) => {\n const prev = seen.get(item);\n if (prev === undefined) {\n seen.set(item, idx);\n return;\n }\n ctx.addIssue({\n code: \"custom\",\n path: [bucketName, idx],\n message: `duplicate rule entry: \"${item}\" already declared at index ${prev}`,\n });\n });\n }\n // (b) Reject a string that appears in more than one of must/should/never.\n type Bucket = \"must\" | \"should\" | \"never\";\n const owner = new Map<string, Bucket>();\n for (const [bucketName, items] of buckets) {\n if (!items) continue;\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n const existing = owner.get(item);\n if (existing === undefined) {\n owner.set(item, bucketName);\n continue;\n }\n if (existing !== bucketName) {\n ctx.addIssue({\n code: \"custom\",\n path: [bucketName, i],\n message: `rule entry \"${item}\" appears in both '${existing}' and '${bucketName}'; rules must belong to exactly one bucket`,\n });\n }\n }\n }\n })\n .describe(\n \"Structured rules for scaffolding agents declared at the plugin level. Each rule is a short directive (≤120 chars).\",\n );\n\n// ── Plugin manifest (root) ───────────────────────────────────────────────\n\nexport const pluginManifestSchema = z\n .object({\n scopes: z\n .array(userApiScopeSchema)\n .optional()\n .describe(\n \"user_api_scopes the plugin always needs, whatever its resources are bound as: calls it makes on behalf of the user unconditionally, or capabilities with no resource ID.\",\n ),\n $schema: z\n .string()\n .optional()\n .describe(\"Reference to the JSON Schema for validation\"),\n name: z\n .string()\n .regex(PLUGIN_NAME_PATTERN)\n .describe(\n \"Plugin identifier and JS binding. Must start with a lowercase letter; camelCase for multi-word names (e.g. aiSearch).\",\n ),\n displayName: z\n .string()\n .min(1)\n .describe(\"Human-readable display name for UI and CLI\"),\n description: z\n .string()\n .min(1)\n .describe(\"Brief description of what the plugin does\"),\n resources: z\n .object({\n required: z\n .array(resourceRequirementSchema)\n .describe(\n \"Resources that must be available for the plugin to function\",\n ),\n optional: z\n .array(resourceRequirementSchema)\n .describe(\n \"Resources that enhance functionality but are not mandatory\",\n ),\n })\n .strict()\n .describe(\"Databricks resource requirements for this plugin\"),\n config: z\n .object({\n schema: configSchemaSchema.optional(),\n })\n .strict()\n .optional()\n .describe(\"Configuration schema for the plugin\"),\n author: z.string().optional().describe(\"Author name or organization\"),\n version: z\n .string()\n .regex(/^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$/)\n .optional()\n .describe(\"Plugin version (semver format)\"),\n repository: z\n .url()\n .optional()\n .describe(\"URL to the plugin's source repository\"),\n keywords: z\n .array(z.string())\n .optional()\n .describe(\"Keywords for plugin discovery\"),\n license: z.string().optional().describe(\"SPDX license identifier\"),\n onSetupMessage: z\n .string()\n .optional()\n .describe(\n \"Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning).\",\n ),\n hidden: z\n .boolean()\n .optional()\n .describe(\n \"When true, this plugin is excluded from the template plugins manifest (appkit.plugins.json) during sync.\",\n ),\n devOnly: z\n .boolean()\n .optional()\n .describe(\n \"When true, this plugin is only registered when NODE_ENV === 'development'. In any other environment createApp skips it entirely (not constructed, no routes, resources not validated). Use for dev-only tooling that must never run in a deployed app.\",\n ),\n stability: z\n .enum([\"beta\", \"ga\"])\n .optional()\n .describe(\n \"Plugin stability level. Beta plugins may have breaking API changes between minor releases but are on a path to GA. GA (general availability) plugins follow semver strictly.\",\n ),\n deprecated: z\n .boolean()\n .optional()\n .describe(\n \"When true, the plugin is deprecated. It still ships and functions, but tooling (e.g. `appkit plugin list`) may hide or flag it. The recommended replacement is noted in the plugin description.\",\n ),\n scaffolding: z\n .object({\n rules: pluginScaffoldingRulesSchema\n .optional()\n .describe(\n \"Structured rules for scaffolding agents declared at the plugin level.\",\n ),\n })\n .strict()\n .optional()\n .describe(\n \"Plugin-level scaffolding metadata consumed by scaffolding agents. Symmetric with template-level `scaffolding`.\",\n ),\n })\n .strict()\n .describe(\n \"Schema for Databricks AppKit plugin manifest files. Defines plugin metadata, resource requirements, and configuration options.\",\n );\n\n// ── Origin enum ──────────────────────────────────────────────────────────\n\nexport const originSchema = z\n .enum([\"user\", \"platform\", \"static\", \"cli\"])\n .describe(\n \"How the field value is determined. Computed during sync, not authored by plugin developers.\",\n );\n\n// ── Template field entry (origin computed by transform) ─────────────────\n\n/**\n * Derives the canonical origin of a resource field value from its shape.\n *\n * - `localOnly: true` → `\"platform\"` (auto-injected by the Databricks Apps\n * platform at deploy time; takes precedence over `value`/`resolve`).\n * - `value !== undefined` → `\"static\"` (hardcoded value).\n * - `resolve !== undefined` → `\"cli\"` (resolved by the CLI during init).\n * - else → `\"user\"` (user must provide the value at init time).\n *\n * The single source for this rule: `plugin sync`'s transform stamps `origin`\n * with it, and consumers that read an authored manifest (no stamped `origin`)\n * derive it through this rather than re-implementing the cascade.\n */\nexport function computeOriginFromField(field: {\n localOnly?: boolean;\n value?: string;\n resolve?: string;\n}): z.infer<typeof originSchema> {\n if (field.localOnly) return \"platform\";\n if (field.value !== undefined) return \"static\";\n if (field.resolve !== undefined) return \"cli\";\n return \"user\";\n}\n\n/**\n * Template field entry: extends the plugin manifest field entry with an\n * optional `origin` input slot, then runs a `.transform()` that overwrites\n * `origin` with the computed value. Allowing `origin` on input means\n * re-parsing a previously-synced template manifest does not fail; emitting\n * `origin` always means hand-edits in synced JSON are silently corrected\n * on the next parse — drift-by-construction.\n */\nexport const templateFieldEntrySchema = resourceFieldEntrySchema\n .extend({ origin: originSchema.optional() })\n .transform((field) => ({\n ...field,\n origin: computeOriginFromField(field),\n }));\n\n// ── Template resource requirement (uses templateFieldEntrySchema) ────────\n\nconst templateResourceRequirementBaseShape = {\n alias: z\n .string()\n .min(1)\n .describe(\"Human-readable label for UI/display only.\"),\n resourceKey: z\n .string()\n .regex(/^[a-z][a-z0-9-]*$/)\n .describe(\n \"Stable key for machine use: deduplication, env naming, composite keys.\",\n ),\n description: z\n .string()\n .min(1)\n .describe(\"Human-readable description of why this resource is needed\"),\n fields: z\n .record(z.string(), templateFieldEntrySchema)\n .refine((obj) => Object.keys(obj).length >= 1, {\n message: \"fields must contain at least one entry\",\n })\n .optional()\n .describe(\"Map of field name to field entry with computed origin.\"),\n scope: z\n .string()\n .min(1)\n .optional()\n .describe(\n \"Apps user_api_scope for this resource type. Present only when the type can run on behalf of the user. Resolved by sync from SCOPE_BY_TYPE.\",\n ),\n appOnly: z\n .literal(true)\n .optional()\n .describe(\n \"Present only when the type always runs as the app service principal and must be bound (secret, database, postgres). Resolved by sync from APP_ONLY_RESOURCE_TYPES.\",\n ),\n binding: z\n .object({\n yamlKey: z\n .string()\n .min(1)\n .describe(\"DABs YAML key under the resource entry.\"),\n varFields: z\n .array(z.tuple([z.string(), z.string()]))\n .describe(\n \"[manifestField, dabsField] pairs. Each becomes ${var.<resourceKey>_<manifestField>} written to dabsField.\",\n ),\n staticFields: z\n .array(z.tuple([z.string(), z.string()]))\n .optional()\n .describe(\"[dabsField, value] constant pairs.\"),\n })\n .strict()\n .optional()\n .describe(\n \"How this resource type binds in databricks.yml as a DABs app resource. Resolved by sync from DABS_BINDING_BY_TYPE.\",\n ),\n};\n\nfunction makeTemplateResourceVariant<\n TType extends z.ZodLiteral<string>,\n TPerm extends z.ZodTypeAny,\n>(typeLiteral: TType, permission: TPerm) {\n return z\n .object({\n type: typeLiteral,\n ...templateResourceRequirementBaseShape,\n permission: permission.describe(\n \"Required permission level. Validated per resource type.\",\n ),\n })\n .strict()\n .superRefine(refineResourceDependsOn);\n}\n\nexport const templateResourceRequirementSchema = z\n .discriminatedUnion(\"type\", [\n makeTemplateResourceVariant(z.literal(\"secret\"), secretPermissionSchema),\n makeTemplateResourceVariant(z.literal(\"job\"), jobPermissionSchema),\n makeTemplateResourceVariant(\n z.literal(\"sql_warehouse\"),\n sqlWarehousePermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"serving_endpoint\"),\n servingEndpointPermissionSchema,\n ),\n makeTemplateResourceVariant(z.literal(\"volume\"), volumePermissionSchema),\n makeTemplateResourceVariant(\n z.literal(\"vector_search_index\"),\n vectorSearchIndexPermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"uc_function\"),\n ucFunctionPermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"uc_connection\"),\n ucConnectionPermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"database\"),\n databasePermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"postgres\"),\n postgresPermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"genie_space\"),\n genieSpacePermissionSchema,\n ),\n makeTemplateResourceVariant(\n z.literal(\"experiment\"),\n experimentPermissionSchema,\n ),\n makeTemplateResourceVariant(z.literal(\"app\"), appPermissionSchema),\n ])\n .describe(\n \"Resource requirement with template-specific field entries (includes computed origin).\",\n );\n\n// ── Template plugin (extends plugin manifest) ────────────────────────────\n\nexport const templatePluginSchema = z\n .object({\n name: z\n .string()\n .regex(PLUGIN_NAME_PATTERN)\n .describe(\n \"Plugin identifier and JS binding. Must start with a lowercase letter; camelCase for multi-word names (e.g. aiSearch).\",\n ),\n displayName: z\n .string()\n .min(1)\n .describe(\"Human-readable display name for UI and CLI\"),\n description: z\n .string()\n .min(1)\n .describe(\"Brief description of what the plugin does\"),\n package: z\n .string()\n .min(1)\n .describe(\"NPM package name that provides this plugin\"),\n requiredByTemplate: z\n .boolean()\n .optional()\n .describe(\n \"When true, this plugin is required by the template and cannot be deselected during CLI init. The user will only be prompted to configure its resources. When absent or false, the plugin is optional and the user can choose whether to include it.\",\n ),\n onSetupMessage: z\n .string()\n .optional()\n .describe(\n \"Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning).\",\n ),\n stability: z\n .enum([\"beta\", \"ga\"])\n .optional()\n .describe(\n \"Plugin stability level. Beta is heading to GA; APIs may change between minor releases. GA (general availability) follows semver.\",\n ),\n deprecated: z\n .boolean()\n .optional()\n .describe(\n \"When true, the plugin is deprecated. It still ships and functions, but tooling (e.g. `appkit plugin list`) may hide or flag it. The recommended replacement is noted in the plugin description.\",\n ),\n scaffolding: z\n .object({\n rules: pluginScaffoldingRulesSchema\n .optional()\n .describe(\n \"Structured rules for scaffolding agents propagated from the plugin manifest.\",\n ),\n })\n .strict()\n .optional()\n .describe(\n \"Plugin-level scaffolding metadata propagated from the plugin manifest.\",\n ),\n resources: z\n .object({\n required: z\n .array(templateResourceRequirementSchema)\n .describe(\n \"Resources that must be available for the plugin to function\",\n ),\n optional: z\n .array(templateResourceRequirementSchema)\n .describe(\n \"Resources that enhance functionality but are not mandatory\",\n ),\n })\n .strict()\n .describe(\"Databricks resource requirements for this plugin\"),\n scopes: z\n .array(userApiScopeSchema)\n .min(1)\n .optional()\n .describe(\n \"user_api_scopes the plugin always needs, copied from the plugin manifest. Omitted when empty.\",\n ),\n })\n .strict()\n .describe(\"Plugin manifest with package source information\");\n\n// ── Scaffolding descriptor ───────────────────────────────────────────────\n\nexport const scaffoldingFlagSchema = z\n .object({\n description: z.string().describe(\"Human-readable description of the flag.\"),\n required: z.boolean().optional().describe(\"Whether this flag is required.\"),\n pattern: z\n .string()\n .optional()\n .describe(\"Regex pattern for validating the flag value.\"),\n default: z.string().optional().describe(\"Default value for this flag.\"),\n })\n .strict()\n .describe(\"A flag for the scaffolding command.\");\n\n/**\n * Per-item upper bound on scaffolding rule strings. The intent is to enforce\n * \"short directive\" by contract — long paragraphs fail validation and force\n * authors to split prose into discrete actionable items.\n */\nconst SCAFFOLDING_RULE_MAX_LENGTH = 120;\n\nconst scaffoldingRuleItemSchema = z\n .string()\n .max(\n SCAFFOLDING_RULE_MAX_LENGTH,\n `rule item must be ≤ ${SCAFFOLDING_RULE_MAX_LENGTH} chars`,\n );\n\nexport const scaffoldingRulesSchema = z\n .object({\n never: z\n .array(scaffoldingRuleItemSchema)\n .optional()\n .describe(\"Actions the scaffolding agent must never perform.\"),\n must: z\n .array(scaffoldingRuleItemSchema)\n .optional()\n .describe(\"Actions the scaffolding agent must always perform.\"),\n should: z\n .array(scaffoldingRuleItemSchema)\n .optional()\n .describe(\n \"Recommended actions for the scaffolding agent (parity with plugin-level rules).\",\n ),\n })\n .strict()\n .describe(\"Structured rules for scaffolding agents.\");\n\nexport const scaffoldingDescriptorSchema = z\n .object({\n command: z\n .string()\n .describe(\"The scaffolding command (e.g., 'databricks apps init').\"),\n flags: z\n .record(z.string(), scaffoldingFlagSchema)\n .optional()\n .describe(\"Map of flag name to flag descriptor.\"),\n rules: scaffoldingRulesSchema\n .optional()\n .describe(\"Structured rules for scaffolding agents.\"),\n })\n .strict()\n .describe(\n \"Describes the scaffolding command, flags, and rules for project initialization.\",\n );\n\n/**\n * Canonical scaffolding descriptor for the `databricks apps init` command,\n * embedded in v2.0 template manifests to guide scaffolding agents.\n *\n * Co-located with `scaffoldingDescriptorSchema` so any change to the rule set\n * (or the schema's `maxLength` ceiling) shows up next to its consumer. The\n * `satisfies` annotation gives compile-time validation that the literal\n * matches the schema's input shape; if a `must`/`never` entry exceeds the\n * `maxLength` ceiling at runtime, `scaffoldingDescriptorSchema.parse` would\n * surface the breach in tests.\n */\nexport const TEMPLATE_SCAFFOLDING = {\n command: \"databricks apps init\",\n flags: {\n \"--name\": {\n description:\n \"Project name — sets {{.projectName}} in package.json, databricks.yml, and .env. Required for non-interactive scaffolding.\",\n required: true,\n pattern: \"^[a-z][a-z0-9-]*$\",\n },\n \"--template\": {\n description: \"Template path (local directory or GitHub URL)\",\n required: false,\n },\n \"--version\": {\n description: \"AppKit version to use; defaults to auto-detected\",\n required: false,\n },\n \"--features\": {\n description:\n \"Plugins to enable (comma-separated, no spaces; must match keys in this manifest's plugins map)\",\n required: false,\n pattern: \"^[a-zA-Z0-9_-]+(,[a-zA-Z0-9_-]+)*$\",\n },\n \"--set\": {\n description:\n \"Set resource values (format: plugin.resourceKey.field=value, repeatable)\",\n required: false,\n },\n \"--output-dir\": {\n description: \"Directory to write the project to\",\n required: false,\n },\n \"--description\": {\n description: \"App description\",\n required: false,\n },\n \"--run\": {\n description: \"Run the app after creation (none, dev, dev-remote)\",\n required: false,\n },\n \"--auto-approve\": {\n description:\n \"Pass as a bare flag (no value) to skip prompts for optional resources. Not recommended for agent-driven init — conflicts with the 'ask user when in doubt' rule.\",\n required: false,\n },\n \"--profile\": {\n description:\n \"Databricks CLI profile to use for authentication (global flag)\",\n required: false,\n },\n },\n rules: {\n must: [\n \"Treat `databricks apps init` output as starter code, not requirements; adapt or replace it to match the requested app\",\n \"Keep all secrets and credentials only in app.yaml, databricks.yml, and/or .env\",\n ],\n should: [\"ask user when in doubt of resource to use for plugin\"],\n never: [\n \"guess resources when multiple or no options are available\",\n \"embed secrets in files that will go to the client-bundle\",\n ],\n },\n} satisfies z.infer<typeof scaffoldingDescriptorSchema>;\n\n// ── Template plugins manifest (root) ─────────────────────────────────────\n\nexport const templatePluginsManifestSchema = z\n .object({\n $schema: z\n .string()\n .optional()\n .describe(\"Reference to the JSON Schema for validation\"),\n version: z\n .enum([\"1.0\", \"1.1\", \"2.0\"])\n .describe(\"Schema version for the template plugins manifest\"),\n plugins: z\n .record(z.string(), templatePluginSchema)\n .describe(\"Map of plugin name to plugin manifest with package source\"),\n scaffolding: scaffoldingDescriptorSchema\n .optional()\n .describe(\n \"Describes the scaffolding command and its configuration for project initialization.\",\n ),\n })\n .strict()\n .superRefine((value, ctx) => {\n if (value.version === \"2.0\" && !value.scaffolding) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"scaffolding\"],\n message: \"scaffolding is required when version is '2.0'\",\n });\n }\n })\n .describe(\n \"Aggregated plugin manifest for AppKit templates. Read by Databricks CLI during init to discover available plugins and their resource requirements.\",\n );\n\n// ── Inferred types ───────────────────────────────────────────────────────\n\nexport type ResourceType = z.infer<typeof resourceTypeSchema>;\nexport type SecretPermission = z.infer<typeof secretPermissionSchema>;\nexport type JobPermission = z.infer<typeof jobPermissionSchema>;\nexport type SqlWarehousePermission = z.infer<\n typeof sqlWarehousePermissionSchema\n>;\nexport type ServingEndpointPermission = z.infer<\n typeof servingEndpointPermissionSchema\n>;\nexport type VolumePermission = z.infer<typeof volumePermissionSchema>;\nexport type VectorSearchIndexPermission = z.infer<\n typeof vectorSearchIndexPermissionSchema\n>;\nexport type UcFunctionPermission = z.infer<typeof ucFunctionPermissionSchema>;\nexport type UcConnectionPermission = z.infer<\n typeof ucConnectionPermissionSchema\n>;\nexport type DatabasePermission = z.infer<typeof databasePermissionSchema>;\nexport type PostgresPermission = z.infer<typeof postgresPermissionSchema>;\nexport type GenieSpacePermission = z.infer<typeof genieSpacePermissionSchema>;\nexport type ExperimentPermission = z.infer<typeof experimentPermissionSchema>;\nexport type AppPermission = z.infer<typeof appPermissionSchema>;\nexport type ResourceKind = z.infer<typeof resourceKindSchema>;\nexport type KindDiscoveryDescriptor = z.infer<\n typeof kindDiscoveryDescriptorSchema\n>;\nexport type CliDiscoveryDescriptor = z.infer<\n typeof cliDiscoveryDescriptorSchema\n>;\nexport type DiscoveryDescriptor = z.infer<typeof discoveryDescriptorSchema>;\nexport type ResourceFieldEntry = z.infer<typeof resourceFieldEntrySchema>;\nexport type ResourceRequirement = z.infer<typeof resourceRequirementSchema>;\nexport type ConfigSchemaProperty = z.infer<typeof configSchemaPropertySchema>;\nexport type ConfigSchema = z.infer<typeof configSchemaSchema>;\nexport type PluginScaffoldingRules = z.infer<\n typeof pluginScaffoldingRulesSchema\n>;\nexport type PluginManifest = z.infer<typeof pluginManifestSchema>;\nexport type Origin = z.infer<typeof originSchema>;\n// Template-side types use `z.input` so callers can construct a TemplatePlugin\n// from a parsed PluginManifest before the field-level origin transform runs.\n// `writeManifest` parses every field through `templateFieldEntrySchema` at\n// write-time, so the on-disk shape always has origin populated. The runtime\n// invariant: origin is *always* present after sync writes; the type slot\n// stays optional so the in-memory pipeline does not need to fabricate origin\n// before assignment.\nexport type TemplateFieldEntry = z.input<typeof templateFieldEntrySchema>;\nexport type TemplateResourceRequirement = z.input<\n typeof templateResourceRequirementSchema\n>;\nexport type TemplatePlugin = z.input<typeof templatePluginSchema>;\nexport type ScaffoldingFlag = z.infer<typeof scaffoldingFlagSchema>;\nexport type ScaffoldingRules = z.infer<typeof scaffoldingRulesSchema>;\nexport type ScaffoldingDescriptor = z.infer<typeof scaffoldingDescriptorSchema>;\nexport type TemplatePluginsManifest = z.input<\n typeof templatePluginsManifestSchema\n>;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"stream-manager.d.ts","names":[],"sources":["../../src/stream/stream-manager.ts"],"mappings":";;;;;cAmBa,aAAA;EAAA,QACH,gBAAA;EAAA,QACA,cAAA;EAAA,QACA,SAAA;EAAA,QACA,YAAA;EAAA,QACA,SAAA;EAAA,QACA,iBAAA;cAEI,OAAA,GAAU,YAAA;EAahB,MAAA,CACJ,GAAA,EAAK,YAAA,EACL,OAAA,GAAU,MAAA,EAAQ,WAAA,KAAgB,cAAA,sBAClC,OAAA,GAAU,YAAA,EACV,QAAA,YACC,OAAA;EAsCH,QAAA,CAAA;EAaA,cAAA,CAAA;EAAA,QAKc,uBAAA;EAAA,QA+EA,gBAAA;EAAA,QAmFA,6BAAA;EAAA,QA0GN,eAAA;EAAA,QA6BA,yBAAA;EAAA,QAaA,wBAAA;EAAA,QAwBA,eAAA;EAAA,QAYA,gBAAA;EAAA,QAUA,mBAAA;EAAA,QAuBA,wBAAA;EAAA,QA8BA,gBAAA;AAAA"}
1
+ {"version":3,"file":"stream-manager.d.ts","names":[],"sources":["../../src/stream/stream-manager.ts"],"mappings":";;;;;cAqBa,aAAA;EAAA,QACH,gBAAA;EAAA,QACA,cAAA;EAAA,QACA,SAAA;EAAA,QACA,YAAA;EAAA,QACA,SAAA;EAAA,QACA,iBAAA;cAEI,OAAA,GAAU,YAAA;EAahB,MAAA,CACJ,GAAA,EAAK,YAAA,EACL,OAAA,GAAU,MAAA,EAAQ,WAAA,KAAgB,cAAA,sBAClC,OAAA,GAAU,YAAA,EACV,QAAA,YACC,OAAA;EAsCH,QAAA,CAAA;EAaA,cAAA,CAAA;EAAA,QAKc,uBAAA;EAAA,QA+EA,gBAAA;EAAA,QAmFA,6BAAA;EAAA,QA+GN,eAAA;EAAA,QA6BA,yBAAA;EAAA,QAaA,wBAAA;EAAA,QAwBA,eAAA;EAAA,QAYA,gBAAA;EAAA,QAUA,mBAAA;EAAA,QAuBA,wBAAA;EAAA,QA8BA,gBAAA;AAAA"}
@@ -1,6 +1,8 @@
1
1
  import { AppKitError } from "../errors/base.js";
2
+ import { IdentityExpiredError } from "../errors/identity-expired.js";
2
3
  import { ExecutionError } from "../errors/execution.js";
3
4
  import { createLogger } from "../logging/logger.js";
5
+ import { normalizeIdentityError } from "../context/execution-context.js";
4
6
  import { EventRingBuffer } from "./buffers.js";
5
7
  import { streamDefaults } from "./defaults.js";
6
8
  import { SSEErrorCode } from "./types.js";
@@ -162,10 +164,11 @@ var StreamManager = class {
162
164
  streamEntry.lastAccess = Date.now();
163
165
  }
164
166
  this._finalizeStream(streamEntry);
165
- } catch (error) {
167
+ } catch (caught) {
168
+ const error = normalizeIdentityError(caught);
166
169
  const rawMsg = error instanceof Error ? error.message : "Internal server error";
167
170
  const clientMsg = error instanceof AppKitError ? error.clientMessage : "Internal server error";
168
- const upstreamCode = error instanceof ExecutionError ? error.errorCode : void 0;
171
+ const upstreamCode = error instanceof IdentityExpiredError ? error.code : error instanceof ExecutionError ? error.errorCode : void 0;
169
172
  const errorEventId = randomUUID();
170
173
  const errorCode = this._categorizeError(error);
171
174
  if (errorCode === SSEErrorCode.STREAM_ABORTED) {
@@ -1 +1 @@
1
- {"version":3,"file":"stream-manager.js","names":[],"sources":["../../src/stream/stream-manager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport { context } from \"@opentelemetry/api\";\nimport type { IAppResponse, StreamConfig } from \"shared\";\n\nimport { AppKitError } from \"../errors/base\";\nimport { ExecutionError } from \"../errors/execution\";\nimport { createLogger } from \"../logging/logger\";\nimport { EventRingBuffer } from \"./buffers\";\nimport { streamDefaults } from \"./defaults\";\nimport { SSEWriter } from \"./sse-writer\";\nimport { StreamRegistry } from \"./stream-registry\";\nimport { clearGraceTimer, clearRemovalTimer } from \"./timers\";\nimport { SSEErrorCode, type StreamEntry, type StreamOperation } from \"./types\";\nimport { StreamValidator } from \"./validator\";\n\nconst logger = createLogger(\"stream\");\n\n// main entry point for Server-Sent events streaming\nexport class StreamManager {\n private activeOperations: Set<StreamOperation>;\n private streamRegistry: StreamRegistry;\n private sseWriter: SSEWriter;\n private maxEventSize: number;\n private bufferTTL: number;\n private disconnectGraceMs: number;\n\n constructor(options?: StreamConfig) {\n this.streamRegistry = new StreamRegistry(\n options?.maxActiveStreams ?? streamDefaults.maxActiveStreams,\n );\n this.sseWriter = new SSEWriter();\n this.maxEventSize = options?.maxEventSize ?? streamDefaults.maxEventSize;\n this.bufferTTL = options?.bufferTTL ?? streamDefaults.bufferTTL;\n this.disconnectGraceMs =\n options?.disconnectGraceMs ?? streamDefaults.disconnectGraceMs;\n this.activeOperations = new Set();\n }\n\n // main streaming method - handles new connection and reconnection\n async stream(\n res: IAppResponse,\n handler: (signal: AbortSignal) => AsyncGenerator<any, void, unknown>,\n options?: StreamConfig,\n ownerKey?: string,\n ): Promise<void> {\n const { streamId } = options || {};\n\n // check if response is already closed\n if (res.writableEnded || res.destroyed) {\n return;\n }\n\n // setup SSE headers\n this.sseWriter.setupHeaders(res);\n\n // handle reconnection\n if (streamId && StreamValidator.validateStreamId(streamId)) {\n const existingStream = this.streamRegistry.get(streamId);\n if (existingStream) {\n // Enforce per-user binding: the stream's owner key must match the\n // requesting caller's owner key. This prevents cross-user stream\n // takeover via guessed/leaked stream IDs (the SSE registry was\n // previously a global lookup with no authorization step).\n if (existingStream.ownerKey !== ownerKey) {\n this.sseWriter.writeError(\n res,\n randomUUID(),\n \"Stream not found or access denied\",\n SSEErrorCode.STREAM_FORBIDDEN,\n );\n res.end();\n return;\n }\n return this._attachToExistingStream(res, existingStream, options);\n }\n }\n\n // if stream does not exist, create a new one\n return this._createNewStream(res, handler, options, ownerKey);\n }\n\n // abort all active operations\n abortAll(): void {\n // pending disconnect-grace timers are cleared by streamRegistry.clear() below\n this.activeOperations.forEach((operation) => {\n if (operation.heartbeat) clearInterval(operation.heartbeat);\n operation.controller.abort(\n new DOMException(\"Server shutdown\", \"AbortError\"),\n );\n });\n this.activeOperations.clear();\n this.streamRegistry.clear();\n }\n\n // get the number of active operations\n getActiveCount(): number {\n return this.activeOperations.size;\n }\n\n // attach to existing stream\n private async _attachToExistingStream(\n res: IAppResponse,\n streamEntry: StreamEntry,\n options?: StreamConfig,\n ): Promise<void> {\n // handle reconnection - replay missed events\n const lastEventId = res.req?.headers[\"last-event-id\"];\n\n if (StreamValidator.validateEventId(lastEventId)) {\n // cast to string after validation\n const validEventId = lastEventId as string;\n if (streamEntry.eventBuffer.has(validEventId)) {\n const missedEvents =\n streamEntry.eventBuffer.getEventsSince(validEventId);\n // broadcast missed events to client\n for (const event of missedEvents) {\n if (options?.userSignal?.aborted) break;\n this.sseWriter.writeBufferedEvent(res, event);\n }\n } else {\n // buffer overflow - send warning\n this.sseWriter.writeBufferOverflowWarning(res, validEventId);\n }\n }\n\n // a reconnecting client cancels the pending disconnect-grace abort\n clearGraceTimer(streamEntry);\n\n // a reconnect cancels any pending registry removal so the entry isn't\n // pulled out from under the newly attached client\n clearRemovalTimer(streamEntry);\n\n // add client to stream entry\n streamEntry.clients.add(res);\n streamEntry.lastAccess = Date.now();\n\n // start heartbeat\n const combinedSignal = this._combineSignals(\n streamEntry.abortController.signal,\n options?.userSignal,\n );\n const heartbeat = this.sseWriter.startHeartbeat(res, combinedSignal);\n\n // track operation\n const streamOperation: StreamOperation = {\n controller: streamEntry.abortController,\n type: \"stream\",\n heartbeat,\n };\n this.activeOperations.add(streamOperation);\n\n // handle client disconnect\n res.on(\"close\", () => {\n clearInterval(heartbeat);\n streamEntry.clients.delete(res);\n this.activeOperations.delete(streamOperation);\n\n // grace-abort instead of aborting now, so a reconnect can resume\n if (streamEntry.clients.size === 0 && !streamEntry.isCompleted) {\n this._scheduleGraceAbort(streamEntry);\n }\n\n // cleanup if stream is completed and no clients are connected\n this._scheduleRemovalAfterTTL(streamEntry);\n });\n\n // if stream is completed, close connection\n if (streamEntry.isCompleted) {\n res.end();\n // cleanup operation\n this.activeOperations.delete(streamOperation);\n clearInterval(heartbeat);\n // we deliberately ended this client, so drop it from the entry and\n // schedule removal now instead of relying on the transport's `close`\n // event (the close handler's later delete is a safe no-op)\n streamEntry.clients.delete(res);\n this._scheduleRemovalAfterTTL(streamEntry);\n }\n }\n private async _createNewStream(\n res: IAppResponse,\n handler: (signal: AbortSignal) => AsyncGenerator<any, void, unknown>,\n options?: StreamConfig,\n ownerKey?: string,\n ): Promise<void> {\n const streamId = options?.streamId ?? randomUUID();\n\n // abort stream if response is closed\n if (res.writableEnded || res.destroyed) {\n return;\n }\n\n const abortController = new AbortController();\n\n // create event buffer\n const eventBuffer = new EventRingBuffer(\n options?.bufferSize ?? streamDefaults.bufferSize,\n );\n const maxEventSize = options?.maxEventSize ?? this.maxEventSize;\n\n // setup signals and heartbeat\n const combinedSignal = this._combineSignals(\n abortController.signal,\n options?.userSignal,\n );\n const heartbeat = this.sseWriter.startHeartbeat(res, combinedSignal);\n\n // capture the current trace context at stream creation time\n const traceContext = context.active();\n\n // abort stream if response is closed\n if (res.writableEnded || res.destroyed) {\n clearInterval(heartbeat);\n return;\n }\n\n // create stream entry\n const streamEntry: StreamEntry = {\n streamId,\n ownerKey,\n generator: handler(combinedSignal),\n eventBuffer,\n clients: new Set([res]),\n isCompleted: false,\n lastAccess: Date.now(),\n abortController,\n traceContext,\n maxEventSize,\n };\n this.streamRegistry.add(streamEntry);\n\n // track operation\n const streamOperation: StreamOperation = {\n controller: abortController,\n type: \"stream\",\n heartbeat,\n };\n this.activeOperations.add(streamOperation);\n\n res.on(\"close\", () => {\n clearInterval(heartbeat);\n this.activeOperations.delete(streamOperation);\n streamEntry.clients.delete(res);\n\n // grace-abort instead of aborting now, so a reconnect can resume\n if (streamEntry.clients.size === 0 && !streamEntry.isCompleted) {\n this._scheduleGraceAbort(streamEntry);\n }\n\n // if the stream already finished (completed or errored), schedule\n // registry removal once the buffer TTL elapses so completed streams\n // don't accumulate in the registry forever\n this._scheduleRemovalAfterTTL(streamEntry);\n });\n\n await this._processGeneratorInBackground(streamEntry);\n\n // cleanup\n clearInterval(heartbeat);\n this.activeOperations.delete(streamOperation);\n }\n\n private async _processGeneratorInBackground(\n streamEntry: StreamEntry,\n ): Promise<void> {\n // run the entire generator processing within the captured trace context\n return context.with(streamEntry.traceContext, async () => {\n try {\n // retrieve all events from generator\n for await (const event of streamEntry.generator) {\n if (streamEntry.abortController.signal.aborted) break;\n const eventId = randomUUID();\n const eventData = JSON.stringify(event);\n const { maxEventSize } = streamEntry;\n\n // UTF-8 bytes, not `String.length`: non-ASCII payloads used to slip\n // past a limit they exceeded on the wire.\n if (Buffer.byteLength(eventData, \"utf8\") > maxEventSize) {\n const errorMsg = `Event exceeds max size of ${maxEventSize} bytes`;\n const errorCode = SSEErrorCode.INVALID_REQUEST;\n // broadcast error to all connected clients\n this._broadcastErrorToClients(\n streamEntry,\n eventId,\n errorMsg,\n errorCode,\n );\n continue;\n }\n\n // buffer event for reconnection\n streamEntry.eventBuffer.add({\n id: eventId,\n type: event.type,\n data: eventData,\n timestamp: Date.now(),\n });\n\n // broadcast to all connected clients\n this._broadcastEventsToClients(streamEntry, eventId, event);\n streamEntry.lastAccess = Date.now();\n }\n\n this._finalizeStream(streamEntry);\n } catch (error) {\n // Two distinct messages: a *raw* one for server-side logs (full\n // detail, statement fragments, correlation IDs) and a *client*\n // one for the SSE payload (sanitized, stable, safe to render in\n // a UI). Mixing them leaks upstream wording to anyone connected\n // to the stream — see CWE-209.\n const rawMsg =\n error instanceof Error ? error.message : \"Internal server error\";\n const clientMsg =\n error instanceof AppKitError\n ? error.clientMessage\n : \"Internal server error\";\n // Upstream structured code (e.g. RESULT_TOO_LARGE_FOR_JSON_FALLBACK,\n // NOT_IMPLEMENTED). UI should branch on this, not on `error`.\n const upstreamCode =\n error instanceof ExecutionError ? error.errorCode : undefined;\n const errorEventId = randomUUID();\n const errorCode = this._categorizeError(error);\n\n // client cancellation is a normal control-flow signal, not a failure\n if (errorCode === SSEErrorCode.STREAM_ABORTED) {\n logger.info(\"Stream aborted by client (code=%s)\", errorCode);\n this._finalizeStream(streamEntry);\n return;\n }\n\n logger.error(\n \"Stream execution failed: %s (code=%s upstreamCode=%s)\",\n rawMsg,\n errorCode,\n upstreamCode ?? \"n/a\",\n );\n\n const payload: Record<string, unknown> = {\n error: clientMsg,\n code: errorCode,\n };\n if (upstreamCode) payload.errorCode = upstreamCode;\n\n // buffer error event\n streamEntry.eventBuffer.add({\n id: errorEventId,\n type: \"error\",\n data: JSON.stringify(payload),\n timestamp: Date.now(),\n });\n\n // send error event to all connected clients\n this._broadcastErrorToClients(\n streamEntry,\n errorEventId,\n clientMsg,\n errorCode,\n true,\n upstreamCode,\n );\n // the broadcast above already ended the connected clients; finalize\n // still detaches them so removal is scheduled without waiting on a\n // transport `close` event\n this._finalizeStream(streamEntry);\n }\n });\n }\n\n private _combineSignals(\n internalSignal?: AbortSignal,\n userSignal?: AbortSignal,\n ): AbortSignal {\n if (!userSignal) return internalSignal || new AbortController().signal;\n\n const signals = [internalSignal, userSignal].filter(\n Boolean,\n ) as AbortSignal[];\n const controller = new AbortController();\n\n signals.forEach((signal) => {\n if (signal?.aborted) {\n controller.abort(signal.reason);\n return;\n }\n\n signal?.addEventListener(\n \"abort\",\n () => {\n controller.abort(signal.reason);\n },\n { once: true },\n );\n });\n return controller.signal;\n }\n\n // broadcast events to all connected clients\n private _broadcastEventsToClients(\n streamEntry: StreamEntry,\n eventId: string,\n event: any,\n ): void {\n for (const client of streamEntry.clients) {\n if (!client.writableEnded) {\n this.sseWriter.writeEvent(client, eventId, event);\n }\n }\n }\n\n // broadcast error to all connected clients\n private _broadcastErrorToClients(\n streamEntry: StreamEntry,\n eventId: string,\n errorMessage: string,\n errorCode: SSEErrorCode,\n closeClients: boolean = false,\n upstreamCode?: string,\n ): void {\n for (const client of streamEntry.clients) {\n if (!client.writableEnded) {\n this.sseWriter.writeError(\n client,\n eventId,\n errorMessage,\n errorCode,\n upstreamCode,\n );\n if (closeClients) {\n client.end();\n }\n }\n }\n }\n\n private _finalizeStream(streamEntry: StreamEntry): void {\n streamEntry.isCompleted = true;\n clearGraceTimer(streamEntry);\n this._closeAllClients(streamEntry);\n this._scheduleRemovalAfterTTL(streamEntry);\n }\n\n // close all connected clients and remove them from the stream entry.\n // We are deliberately terminating these connections, so cleanup must not\n // depend on the transport emitting a `close` event for each client (it may\n // never fire). The close handlers' later `clients.delete(...)` calls remain\n // safe no-ops.\n private _closeAllClients(streamEntry: StreamEntry): void {\n for (const client of streamEntry.clients) {\n if (!client.writableEnded) {\n client.end();\n }\n }\n streamEntry.clients.clear();\n }\n\n // abort the generator after the grace window unless a client reconnects first\n private _scheduleGraceAbort(streamEntry: StreamEntry): void {\n // clear any existing timer to avoid stacking\n clearGraceTimer(streamEntry);\n\n const timer = setTimeout(() => {\n streamEntry.disconnectGraceTimer = undefined;\n if (streamEntry.clients.size === 0 && !streamEntry.isCompleted) {\n streamEntry.abortController.abort(\n new DOMException(\"Client disconnected (grace expired)\", \"AbortError\"),\n );\n }\n }, this.disconnectGraceMs);\n\n // never keep the process alive solely for a grace timer\n timer.unref?.();\n streamEntry.disconnectGraceTimer = timer;\n }\n\n // schedule registry removal once a finished (completed or errored) stream\n // has no connected clients. The event buffer stays available for\n // reconnect replay for `bufferTTL` after the last client disconnects;\n // after that the stream entry is removed from the registry so it can be\n // garbage collected.\n private _scheduleRemovalAfterTTL(streamEntry: StreamEntry): void {\n if (!streamEntry.isCompleted || streamEntry.clients.size > 0) {\n return;\n }\n\n // at most one removal timer per stream: rescheduling replaces any\n // pending timer instead of stacking a new one (each pending timer pins\n // the entry's buffer/generator/trace context for the full TTL)\n clearRemovalTimer(streamEntry);\n\n // mark the moment the stream became idle so a reconnect during the TTL\n // window (which refreshes lastAccess) makes the pending timer a no-op\n streamEntry.lastAccess = Date.now();\n\n streamEntry.removalTimer = setTimeout(() => {\n streamEntry.removalTimer = undefined;\n // safety net: no-op if a client reconnected during the TTL window;\n // a fresh removal is scheduled when that client disconnects\n if (\n streamEntry.clients.size === 0 &&\n Date.now() - streamEntry.lastAccess >= this.bufferTTL\n ) {\n this.streamRegistry.remove(streamEntry.streamId);\n }\n }, this.bufferTTL);\n\n // don't keep the process alive just to clean up finished streams\n streamEntry.removalTimer.unref?.();\n }\n\n private _categorizeError(error: unknown): SSEErrorCode {\n if (error instanceof Error) {\n const message = error.message.toLowerCase();\n if (message.includes(\"timeout\") || message.includes(\"timed out\")) {\n return SSEErrorCode.TIMEOUT;\n }\n\n if (message.includes(\"unavailable\") || message.includes(\"econnrefused\")) {\n return SSEErrorCode.TEMPORARY_UNAVAILABLE;\n }\n\n if (error.name === \"AbortError\") {\n return SSEErrorCode.STREAM_ABORTED;\n }\n\n // Defense-in-depth: upstream layers (SQL client, cache) may wrap an\n // AbortError into ExecutionError, losing `name` but keeping the message.\n if (\n message.includes(\"operation was aborted\") ||\n message.includes(\"the request was aborted\") ||\n message.includes(\"statement was canceled\")\n ) {\n return SSEErrorCode.STREAM_ABORTED;\n }\n\n // Detect upstream API errors (e.g., from Databricks SDK ApiError)\n if (\n \"statusCode\" in error &&\n typeof (error as any).statusCode === \"number\"\n ) {\n return SSEErrorCode.UPSTREAM_ERROR;\n }\n }\n\n return SSEErrorCode.INTERNAL_ERROR;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"stream-manager.js","names":[],"sources":["../../src/stream/stream-manager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport { context } from \"@opentelemetry/api\";\nimport type { IAppResponse, StreamConfig } from \"shared\";\n\nimport { normalizeIdentityError } from \"../context/execution-context\";\nimport { AppKitError } from \"../errors/base\";\nimport { ExecutionError } from \"../errors/execution\";\nimport { IdentityExpiredError } from \"../errors/identity-expired\";\nimport { createLogger } from \"../logging/logger\";\nimport { EventRingBuffer } from \"./buffers\";\nimport { streamDefaults } from \"./defaults\";\nimport { SSEWriter } from \"./sse-writer\";\nimport { StreamRegistry } from \"./stream-registry\";\nimport { clearGraceTimer, clearRemovalTimer } from \"./timers\";\nimport { SSEErrorCode, type StreamEntry, type StreamOperation } from \"./types\";\nimport { StreamValidator } from \"./validator\";\n\nconst logger = createLogger(\"stream\");\n\n// main entry point for Server-Sent events streaming\nexport class StreamManager {\n private activeOperations: Set<StreamOperation>;\n private streamRegistry: StreamRegistry;\n private sseWriter: SSEWriter;\n private maxEventSize: number;\n private bufferTTL: number;\n private disconnectGraceMs: number;\n\n constructor(options?: StreamConfig) {\n this.streamRegistry = new StreamRegistry(\n options?.maxActiveStreams ?? streamDefaults.maxActiveStreams,\n );\n this.sseWriter = new SSEWriter();\n this.maxEventSize = options?.maxEventSize ?? streamDefaults.maxEventSize;\n this.bufferTTL = options?.bufferTTL ?? streamDefaults.bufferTTL;\n this.disconnectGraceMs =\n options?.disconnectGraceMs ?? streamDefaults.disconnectGraceMs;\n this.activeOperations = new Set();\n }\n\n // main streaming method - handles new connection and reconnection\n async stream(\n res: IAppResponse,\n handler: (signal: AbortSignal) => AsyncGenerator<any, void, unknown>,\n options?: StreamConfig,\n ownerKey?: string,\n ): Promise<void> {\n const { streamId } = options || {};\n\n // check if response is already closed\n if (res.writableEnded || res.destroyed) {\n return;\n }\n\n // setup SSE headers\n this.sseWriter.setupHeaders(res);\n\n // handle reconnection\n if (streamId && StreamValidator.validateStreamId(streamId)) {\n const existingStream = this.streamRegistry.get(streamId);\n if (existingStream) {\n // Enforce per-user binding: the stream's owner key must match the\n // requesting caller's owner key. This prevents cross-user stream\n // takeover via guessed/leaked stream IDs (the SSE registry was\n // previously a global lookup with no authorization step).\n if (existingStream.ownerKey !== ownerKey) {\n this.sseWriter.writeError(\n res,\n randomUUID(),\n \"Stream not found or access denied\",\n SSEErrorCode.STREAM_FORBIDDEN,\n );\n res.end();\n return;\n }\n return this._attachToExistingStream(res, existingStream, options);\n }\n }\n\n // if stream does not exist, create a new one\n return this._createNewStream(res, handler, options, ownerKey);\n }\n\n // abort all active operations\n abortAll(): void {\n // pending disconnect-grace timers are cleared by streamRegistry.clear() below\n this.activeOperations.forEach((operation) => {\n if (operation.heartbeat) clearInterval(operation.heartbeat);\n operation.controller.abort(\n new DOMException(\"Server shutdown\", \"AbortError\"),\n );\n });\n this.activeOperations.clear();\n this.streamRegistry.clear();\n }\n\n // get the number of active operations\n getActiveCount(): number {\n return this.activeOperations.size;\n }\n\n // attach to existing stream\n private async _attachToExistingStream(\n res: IAppResponse,\n streamEntry: StreamEntry,\n options?: StreamConfig,\n ): Promise<void> {\n // handle reconnection - replay missed events\n const lastEventId = res.req?.headers[\"last-event-id\"];\n\n if (StreamValidator.validateEventId(lastEventId)) {\n // cast to string after validation\n const validEventId = lastEventId as string;\n if (streamEntry.eventBuffer.has(validEventId)) {\n const missedEvents =\n streamEntry.eventBuffer.getEventsSince(validEventId);\n // broadcast missed events to client\n for (const event of missedEvents) {\n if (options?.userSignal?.aborted) break;\n this.sseWriter.writeBufferedEvent(res, event);\n }\n } else {\n // buffer overflow - send warning\n this.sseWriter.writeBufferOverflowWarning(res, validEventId);\n }\n }\n\n // a reconnecting client cancels the pending disconnect-grace abort\n clearGraceTimer(streamEntry);\n\n // a reconnect cancels any pending registry removal so the entry isn't\n // pulled out from under the newly attached client\n clearRemovalTimer(streamEntry);\n\n // add client to stream entry\n streamEntry.clients.add(res);\n streamEntry.lastAccess = Date.now();\n\n // start heartbeat\n const combinedSignal = this._combineSignals(\n streamEntry.abortController.signal,\n options?.userSignal,\n );\n const heartbeat = this.sseWriter.startHeartbeat(res, combinedSignal);\n\n // track operation\n const streamOperation: StreamOperation = {\n controller: streamEntry.abortController,\n type: \"stream\",\n heartbeat,\n };\n this.activeOperations.add(streamOperation);\n\n // handle client disconnect\n res.on(\"close\", () => {\n clearInterval(heartbeat);\n streamEntry.clients.delete(res);\n this.activeOperations.delete(streamOperation);\n\n // grace-abort instead of aborting now, so a reconnect can resume\n if (streamEntry.clients.size === 0 && !streamEntry.isCompleted) {\n this._scheduleGraceAbort(streamEntry);\n }\n\n // cleanup if stream is completed and no clients are connected\n this._scheduleRemovalAfterTTL(streamEntry);\n });\n\n // if stream is completed, close connection\n if (streamEntry.isCompleted) {\n res.end();\n // cleanup operation\n this.activeOperations.delete(streamOperation);\n clearInterval(heartbeat);\n // we deliberately ended this client, so drop it from the entry and\n // schedule removal now instead of relying on the transport's `close`\n // event (the close handler's later delete is a safe no-op)\n streamEntry.clients.delete(res);\n this._scheduleRemovalAfterTTL(streamEntry);\n }\n }\n private async _createNewStream(\n res: IAppResponse,\n handler: (signal: AbortSignal) => AsyncGenerator<any, void, unknown>,\n options?: StreamConfig,\n ownerKey?: string,\n ): Promise<void> {\n const streamId = options?.streamId ?? randomUUID();\n\n // abort stream if response is closed\n if (res.writableEnded || res.destroyed) {\n return;\n }\n\n const abortController = new AbortController();\n\n // create event buffer\n const eventBuffer = new EventRingBuffer(\n options?.bufferSize ?? streamDefaults.bufferSize,\n );\n const maxEventSize = options?.maxEventSize ?? this.maxEventSize;\n\n // setup signals and heartbeat\n const combinedSignal = this._combineSignals(\n abortController.signal,\n options?.userSignal,\n );\n const heartbeat = this.sseWriter.startHeartbeat(res, combinedSignal);\n\n // capture the current trace context at stream creation time\n const traceContext = context.active();\n\n // abort stream if response is closed\n if (res.writableEnded || res.destroyed) {\n clearInterval(heartbeat);\n return;\n }\n\n // create stream entry\n const streamEntry: StreamEntry = {\n streamId,\n ownerKey,\n generator: handler(combinedSignal),\n eventBuffer,\n clients: new Set([res]),\n isCompleted: false,\n lastAccess: Date.now(),\n abortController,\n traceContext,\n maxEventSize,\n };\n this.streamRegistry.add(streamEntry);\n\n // track operation\n const streamOperation: StreamOperation = {\n controller: abortController,\n type: \"stream\",\n heartbeat,\n };\n this.activeOperations.add(streamOperation);\n\n res.on(\"close\", () => {\n clearInterval(heartbeat);\n this.activeOperations.delete(streamOperation);\n streamEntry.clients.delete(res);\n\n // grace-abort instead of aborting now, so a reconnect can resume\n if (streamEntry.clients.size === 0 && !streamEntry.isCompleted) {\n this._scheduleGraceAbort(streamEntry);\n }\n\n // if the stream already finished (completed or errored), schedule\n // registry removal once the buffer TTL elapses so completed streams\n // don't accumulate in the registry forever\n this._scheduleRemovalAfterTTL(streamEntry);\n });\n\n await this._processGeneratorInBackground(streamEntry);\n\n // cleanup\n clearInterval(heartbeat);\n this.activeOperations.delete(streamOperation);\n }\n\n private async _processGeneratorInBackground(\n streamEntry: StreamEntry,\n ): Promise<void> {\n // run the entire generator processing within the captured trace context\n return context.with(streamEntry.traceContext, async () => {\n try {\n // retrieve all events from generator\n for await (const event of streamEntry.generator) {\n if (streamEntry.abortController.signal.aborted) break;\n const eventId = randomUUID();\n const eventData = JSON.stringify(event);\n const { maxEventSize } = streamEntry;\n\n // UTF-8 bytes, not `String.length`: non-ASCII payloads used to slip\n // past a limit they exceeded on the wire.\n if (Buffer.byteLength(eventData, \"utf8\") > maxEventSize) {\n const errorMsg = `Event exceeds max size of ${maxEventSize} bytes`;\n const errorCode = SSEErrorCode.INVALID_REQUEST;\n // broadcast error to all connected clients\n this._broadcastErrorToClients(\n streamEntry,\n eventId,\n errorMsg,\n errorCode,\n );\n continue;\n }\n\n // buffer event for reconnection\n streamEntry.eventBuffer.add({\n id: eventId,\n type: event.type,\n data: eventData,\n timestamp: Date.now(),\n });\n\n // broadcast to all connected clients\n this._broadcastEventsToClients(streamEntry, eventId, event);\n streamEntry.lastAccess = Date.now();\n }\n\n this._finalizeStream(streamEntry);\n } catch (caught) {\n const error = normalizeIdentityError(caught);\n // Two distinct messages: a *raw* one for server-side logs (full\n // detail, statement fragments, correlation IDs) and a *client*\n // one for the SSE payload (sanitized, stable, safe to render in\n // a UI). Mixing them leaks upstream wording to anyone connected\n // to the stream — see CWE-209.\n const rawMsg =\n error instanceof Error ? error.message : \"Internal server error\";\n const clientMsg =\n error instanceof AppKitError\n ? error.clientMessage\n : \"Internal server error\";\n // Upstream structured code (e.g. RESULT_TOO_LARGE_FOR_JSON_FALLBACK,\n // NOT_IMPLEMENTED). UI should branch on this, not on `error`.\n const upstreamCode =\n error instanceof IdentityExpiredError\n ? error.code\n : error instanceof ExecutionError\n ? error.errorCode\n : undefined;\n const errorEventId = randomUUID();\n const errorCode = this._categorizeError(error);\n\n // client cancellation is a normal control-flow signal, not a failure\n if (errorCode === SSEErrorCode.STREAM_ABORTED) {\n logger.info(\"Stream aborted by client (code=%s)\", errorCode);\n this._finalizeStream(streamEntry);\n return;\n }\n\n logger.error(\n \"Stream execution failed: %s (code=%s upstreamCode=%s)\",\n rawMsg,\n errorCode,\n upstreamCode ?? \"n/a\",\n );\n\n const payload: Record<string, unknown> = {\n error: clientMsg,\n code: errorCode,\n };\n if (upstreamCode) payload.errorCode = upstreamCode;\n\n // buffer error event\n streamEntry.eventBuffer.add({\n id: errorEventId,\n type: \"error\",\n data: JSON.stringify(payload),\n timestamp: Date.now(),\n });\n\n // send error event to all connected clients\n this._broadcastErrorToClients(\n streamEntry,\n errorEventId,\n clientMsg,\n errorCode,\n true,\n upstreamCode,\n );\n // the broadcast above already ended the connected clients; finalize\n // still detaches them so removal is scheduled without waiting on a\n // transport `close` event\n this._finalizeStream(streamEntry);\n }\n });\n }\n\n private _combineSignals(\n internalSignal?: AbortSignal,\n userSignal?: AbortSignal,\n ): AbortSignal {\n if (!userSignal) return internalSignal || new AbortController().signal;\n\n const signals = [internalSignal, userSignal].filter(\n Boolean,\n ) as AbortSignal[];\n const controller = new AbortController();\n\n signals.forEach((signal) => {\n if (signal?.aborted) {\n controller.abort(signal.reason);\n return;\n }\n\n signal?.addEventListener(\n \"abort\",\n () => {\n controller.abort(signal.reason);\n },\n { once: true },\n );\n });\n return controller.signal;\n }\n\n // broadcast events to all connected clients\n private _broadcastEventsToClients(\n streamEntry: StreamEntry,\n eventId: string,\n event: any,\n ): void {\n for (const client of streamEntry.clients) {\n if (!client.writableEnded) {\n this.sseWriter.writeEvent(client, eventId, event);\n }\n }\n }\n\n // broadcast error to all connected clients\n private _broadcastErrorToClients(\n streamEntry: StreamEntry,\n eventId: string,\n errorMessage: string,\n errorCode: SSEErrorCode,\n closeClients: boolean = false,\n upstreamCode?: string,\n ): void {\n for (const client of streamEntry.clients) {\n if (!client.writableEnded) {\n this.sseWriter.writeError(\n client,\n eventId,\n errorMessage,\n errorCode,\n upstreamCode,\n );\n if (closeClients) {\n client.end();\n }\n }\n }\n }\n\n private _finalizeStream(streamEntry: StreamEntry): void {\n streamEntry.isCompleted = true;\n clearGraceTimer(streamEntry);\n this._closeAllClients(streamEntry);\n this._scheduleRemovalAfterTTL(streamEntry);\n }\n\n // close all connected clients and remove them from the stream entry.\n // We are deliberately terminating these connections, so cleanup must not\n // depend on the transport emitting a `close` event for each client (it may\n // never fire). The close handlers' later `clients.delete(...)` calls remain\n // safe no-ops.\n private _closeAllClients(streamEntry: StreamEntry): void {\n for (const client of streamEntry.clients) {\n if (!client.writableEnded) {\n client.end();\n }\n }\n streamEntry.clients.clear();\n }\n\n // abort the generator after the grace window unless a client reconnects first\n private _scheduleGraceAbort(streamEntry: StreamEntry): void {\n // clear any existing timer to avoid stacking\n clearGraceTimer(streamEntry);\n\n const timer = setTimeout(() => {\n streamEntry.disconnectGraceTimer = undefined;\n if (streamEntry.clients.size === 0 && !streamEntry.isCompleted) {\n streamEntry.abortController.abort(\n new DOMException(\"Client disconnected (grace expired)\", \"AbortError\"),\n );\n }\n }, this.disconnectGraceMs);\n\n // never keep the process alive solely for a grace timer\n timer.unref?.();\n streamEntry.disconnectGraceTimer = timer;\n }\n\n // schedule registry removal once a finished (completed or errored) stream\n // has no connected clients. The event buffer stays available for\n // reconnect replay for `bufferTTL` after the last client disconnects;\n // after that the stream entry is removed from the registry so it can be\n // garbage collected.\n private _scheduleRemovalAfterTTL(streamEntry: StreamEntry): void {\n if (!streamEntry.isCompleted || streamEntry.clients.size > 0) {\n return;\n }\n\n // at most one removal timer per stream: rescheduling replaces any\n // pending timer instead of stacking a new one (each pending timer pins\n // the entry's buffer/generator/trace context for the full TTL)\n clearRemovalTimer(streamEntry);\n\n // mark the moment the stream became idle so a reconnect during the TTL\n // window (which refreshes lastAccess) makes the pending timer a no-op\n streamEntry.lastAccess = Date.now();\n\n streamEntry.removalTimer = setTimeout(() => {\n streamEntry.removalTimer = undefined;\n // safety net: no-op if a client reconnected during the TTL window;\n // a fresh removal is scheduled when that client disconnects\n if (\n streamEntry.clients.size === 0 &&\n Date.now() - streamEntry.lastAccess >= this.bufferTTL\n ) {\n this.streamRegistry.remove(streamEntry.streamId);\n }\n }, this.bufferTTL);\n\n // don't keep the process alive just to clean up finished streams\n streamEntry.removalTimer.unref?.();\n }\n\n private _categorizeError(error: unknown): SSEErrorCode {\n if (error instanceof Error) {\n const message = error.message.toLowerCase();\n if (message.includes(\"timeout\") || message.includes(\"timed out\")) {\n return SSEErrorCode.TIMEOUT;\n }\n\n if (message.includes(\"unavailable\") || message.includes(\"econnrefused\")) {\n return SSEErrorCode.TEMPORARY_UNAVAILABLE;\n }\n\n if (error.name === \"AbortError\") {\n return SSEErrorCode.STREAM_ABORTED;\n }\n\n // Defense-in-depth: upstream layers (SQL client, cache) may wrap an\n // AbortError into ExecutionError, losing `name` but keeping the message.\n if (\n message.includes(\"operation was aborted\") ||\n message.includes(\"the request was aborted\") ||\n message.includes(\"statement was canceled\")\n ) {\n return SSEErrorCode.STREAM_ABORTED;\n }\n\n // Detect upstream API errors (e.g., from Databricks SDK ApiError)\n if (\n \"statusCode\" in error &&\n typeof (error as any).statusCode === \"number\"\n ) {\n return SSEErrorCode.UPSTREAM_ERROR;\n }\n }\n\n return SSEErrorCode.INTERNAL_ERROR;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { ServiceContext } from "../context/service-context.js";
2
+ import { getCallerContext, getCurrentActorId } from "../context/execution-context.js";
3
+
4
+ //#region src/telemetry/execution-span-processor.ts
5
+ /** Attach identity at span creation, including cache, tool, and connector spans. */
6
+ var ExecutionSpanProcessor = class {
7
+ onStart(span, _parent) {
8
+ const caller = getCallerContext();
9
+ span.setAttribute("appkit.execution.principal", caller ? "user" : "app");
10
+ span.setAttribute("appkit.execution.principal_id", caller?.principal.userId ?? (ServiceContext.isInitialized() ? ServiceContext.get().serviceUserId : "app"));
11
+ const actor = getCurrentActorId();
12
+ if (actor) span.setAttribute("appkit.execution.actor_id", actor);
13
+ }
14
+ onEnd(_span) {}
15
+ async forceFlush() {}
16
+ async shutdown() {}
17
+ };
18
+
19
+ //#endregion
20
+ export { ExecutionSpanProcessor };
21
+ //# sourceMappingURL=execution-span-processor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execution-span-processor.js","names":[],"sources":["../../src/telemetry/execution-span-processor.ts"],"sourcesContent":["import type { Context } from \"@opentelemetry/api\";\nimport type {\n ReadableSpan,\n Span,\n SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\n\nimport {\n getCallerContext,\n getCurrentActorId,\n} from \"../context/execution-context\";\nimport { ServiceContext } from \"../context/service-context\";\n\n/** Attach identity at span creation, including cache, tool, and connector spans. */\nexport class ExecutionSpanProcessor implements SpanProcessor {\n onStart(span: Span, _parent: Context): void {\n const caller = getCallerContext();\n span.setAttribute(\"appkit.execution.principal\", caller ? \"user\" : \"app\");\n span.setAttribute(\n \"appkit.execution.principal_id\",\n caller?.principal.userId ??\n (ServiceContext.isInitialized()\n ? ServiceContext.get().serviceUserId\n : \"app\"),\n );\n const actor = getCurrentActorId();\n if (actor) span.setAttribute(\"appkit.execution.actor_id\", actor);\n }\n onEnd(_span: ReadableSpan): void {}\n async forceFlush(): Promise<void> {}\n async shutdown(): Promise<void> {}\n}\n"],"mappings":";;;;;AAcA,IAAa,yBAAb,MAA6D;CAC3D,QAAQ,MAAY,SAAwB;EAC1C,MAAM,SAAS,kBAAkB;AACjC,OAAK,aAAa,8BAA8B,SAAS,SAAS,MAAM;AACxE,OAAK,aACH,iCACA,QAAQ,UAAU,WACf,eAAe,eAAe,GAC3B,eAAe,KAAK,CAAC,gBACrB,OACP;EACD,MAAM,QAAQ,mBAAmB;AACjC,MAAI,MAAO,MAAK,aAAa,6BAA6B,MAAM;;CAElE,MAAM,OAA2B;CACjC,MAAM,aAA4B;CAClC,MAAM,WAA0B"}
@@ -1,4 +1,5 @@
1
1
  import { createLogger } from "../logging/logger.js";
2
+ import { ExecutionSpanProcessor } from "./execution-span-processor.js";
2
3
  import { TelemetryProvider } from "./telemetry-provider.js";
3
4
  import { AppKitSampler } from "./trace-sampler.js";
4
5
  import { metrics } from "@opentelemetry/api";
@@ -132,7 +133,7 @@ var TelemetryManager = class TelemetryManager {
132
133
  this.tracerProvider = new NodeTracerProvider({
133
134
  resource: this.resource,
134
135
  sampler: new AppKitSampler(),
135
- spanProcessors: this.spanProcessors
136
+ spanProcessors: [new ExecutionSpanProcessor(), ...this.spanProcessors]
136
137
  });
137
138
  this.tracerProvider.register();
138
139
  logger.debug("Tracer provider started with %d span processor(s)", this.spanProcessors.length);
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry-manager.js","names":[],"sources":["../../src/telemetry/telemetry-manager.ts"],"sourcesContent":["import { metrics } from \"@opentelemetry/api\";\nimport { logs } from \"@opentelemetry/api-logs\";\nimport { getNodeAutoInstrumentations } from \"@opentelemetry/auto-instrumentations-node\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-proto\";\nimport { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-proto\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-proto\";\nimport {\n type Instrumentation,\n registerInstrumentations as otelRegisterInstrumentations,\n} from \"@opentelemetry/instrumentation\";\nimport {\n detectResources,\n envDetector,\n hostDetector,\n processDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n} from \"@opentelemetry/sdk-logs\";\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport {\n BatchSpanProcessor,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from \"@opentelemetry/semantic-conventions\";\nimport type { TelemetryOptions } from \"shared\";\n\nimport { createLogger } from \"../logging/logger\";\nimport { TelemetryProvider } from \"./telemetry-provider\";\nimport { AppKitSampler } from \"./trace-sampler\";\nimport type { TelemetryConfig } from \"./types\";\n\nconst logger = createLogger(\"telemetry\");\n\n/**\n * Owns the app's OpenTelemetry providers, split into two phases so plugins can\n * contribute trace span processors before the tracer provider is built.\n *\n * - `initialize()` runs at app bootstrap, before plugin setup. It registers the\n * meter and logger providers eagerly, because OTel's metrics API has no lazy\n * proxy: a counter/histogram bound against the NoOp meter (as every connector\n * and the cache do in their constructors) stays NoOp for the process lifetime.\n * It does NOT register a tracer provider.\n * - `registerSpanProcessor()` is called by plugins during `setup()` to add a\n * span processor (e.g. an MLflow exporter) to the not-yet-built tracer.\n * - `start()` runs after all plugin `setup()` completes. It builds the single\n * global tracer provider with the OTLP processor (if configured) plus every\n * contributed processor. Deferring is safe for traces: OTel's ProxyTracer\n * rebinds tracers obtained before registration, and no span is emitted during\n * setup.\n */\nexport class TelemetryManager {\n private static readonly DEFAULT_EXPORT_INTERVAL_MS = 10000;\n private static readonly DEFAULT_FALLBACK_APP_NAME = \"databricks-app\";\n\n private static instance?: TelemetryManager;\n private resource?: Resource;\n private meterProvider?: MeterProvider;\n private loggerProvider?: LoggerProvider;\n private tracerProvider?: NodeTracerProvider;\n private readonly spanProcessors: SpanProcessor[] = [];\n private started = false;\n private shutdownPromise?: Promise<void>;\n\n /**\n * Create a scoped telemetry provider for a specific plugin.\n * The plugin's name will be used as the default tracer/meter name.\n * @param pluginName - The name of the plugin to create scoped telemetry for\n * @param telemetryConfig - The telemetry configuration for the plugin\n * @returns A scoped telemetry instance for the plugin\n */\n static getProvider(\n pluginName: string,\n telemetryConfig?: TelemetryOptions,\n ): TelemetryProvider {\n const globalManager = TelemetryManager.getInstance();\n return new TelemetryProvider(pluginName, globalManager, telemetryConfig);\n }\n\n private constructor() {}\n\n static getInstance(): TelemetryManager {\n if (!TelemetryManager.instance) {\n TelemetryManager.instance = new TelemetryManager();\n }\n return TelemetryManager.instance;\n }\n\n static initialize(config: Partial<TelemetryConfig> = {}): void {\n const instance = TelemetryManager.getInstance();\n instance._initialize(config);\n }\n\n /**\n * Contribute a span processor to the not-yet-built tracer provider. Called by\n * plugins during `setup()`. No-op with a warning once `start()` has run, since\n * a started provider's processors are immutable in OTel JS 2.x.\n */\n static registerSpanProcessor(processor: SpanProcessor): void {\n TelemetryManager.getInstance()._registerSpanProcessor(processor);\n }\n\n private _registerSpanProcessor(processor: SpanProcessor): void {\n if (this.started) {\n logger.warn(\n \"registerSpanProcessor called after start(); processor ignored. \" +\n \"Contribute span processors during plugin setup().\",\n );\n return;\n }\n this.spanProcessors.push(processor);\n }\n\n /**\n * Phase 1: register the meter and logger providers eagerly (before plugin\n * setup), so metric instruments bound in connector/cache constructors attach\n * to real meters. The tracer provider is deferred to `start()`.\n *\n * When no OTLP endpoint is configured, meter/logger registration is skipped;\n * a contributed span processor can still bring up tracing in `start()`.\n */\n private _initialize(config: Partial<TelemetryConfig>): void {\n if (this.resource) return;\n this.resource = this.createResource(config);\n\n // OTLP exporters need an endpoint. Without one there is nothing to export\n // metrics/logs to, so skip those providers — but still capture the resource\n // and let `start()` bring up a tracer if a plugin contributed a processor.\n if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {\n return;\n }\n\n try {\n this.meterProvider = new MeterProvider({\n resource: this.resource,\n readers: [\n new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({ headers: config.headers }),\n exportIntervalMillis:\n config.exportIntervalMs ||\n TelemetryManager.DEFAULT_EXPORT_INTERVAL_MS,\n }),\n ],\n });\n metrics.setGlobalMeterProvider(this.meterProvider);\n\n this.loggerProvider = new LoggerProvider({\n resource: this.resource,\n processors: [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({ headers: config.headers }),\n ),\n ],\n });\n logs.setGlobalLoggerProvider(this.loggerProvider);\n\n // The OTLP trace exporter is the first span processor; contributed\n // processors join it in `start()`.\n this.spanProcessors.push(\n new BatchSpanProcessor(\n new OTLPTraceExporter({ headers: config.headers }),\n ),\n );\n\n this.registerInstrumentations(this.getDefaultInstrumentations());\n logger.debug(\"Meter/logger providers initialized\");\n } catch (error) {\n logger.error(\"Failed to initialize: %O\", error);\n }\n }\n\n /**\n * Phase 2: build and register the global tracer provider. Called by core\n * after every plugin's `setup()` completes, so all contributed span\n * processors are known. No-op when nothing needs tracing (no OTLP endpoint\n * and no contributed processor), preserving \"no telemetry unless configured\".\n *\n * `NodeTracerProvider.register()` installs the async-hooks context manager and\n * W3C propagators — the same wiring `NodeSDK.start()` did — so span nesting\n * across awaits is preserved.\n */\n static start(): void {\n TelemetryManager.getInstance()._start();\n }\n\n private _start(): void {\n if (this.started) return;\n this.started = true;\n\n if (this.spanProcessors.length === 0) {\n return;\n }\n\n try {\n this.tracerProvider = new NodeTracerProvider({\n resource: this.resource,\n sampler: new AppKitSampler(),\n spanProcessors: this.spanProcessors,\n });\n this.tracerProvider.register();\n logger.debug(\n \"Tracer provider started with %d span processor(s)\",\n this.spanProcessors.length,\n );\n } catch (error) {\n logger.error(\"Failed to start tracer provider: %O\", error);\n }\n }\n\n /**\n * Register OpenTelemetry instrumentations.\n * Can be called at any time, but recommended to call in plugin constructor.\n * @param instrumentations - Array of OpenTelemetry instrumentations to register\n */\n registerInstrumentations(instrumentations: Instrumentation[]): void {\n otelRegisterInstrumentations({\n // Instrumentations bind to the global providers registered by start()\n // (tracer) and _initialize() (meter/logger).\n instrumentations,\n });\n }\n\n private createResource(config: Partial<TelemetryConfig>): Resource {\n const serviceName =\n config.serviceName ||\n process.env.OTEL_SERVICE_NAME ||\n process.env.DATABRICKS_APP_NAME ||\n TelemetryManager.DEFAULT_FALLBACK_APP_NAME;\n const initialResource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion ?? undefined,\n });\n const detectedResource = detectResources({\n detectors: [envDetector, hostDetector, processDetector],\n });\n return initialResource.merge(detectedResource);\n }\n\n private getDefaultInstrumentations(): Instrumentation[] {\n return [\n ...getNodeAutoInstrumentations({\n //\n // enabled as a part of the server plugin\n //\n \"@opentelemetry/instrumentation-http\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-express\": {\n enabled: false,\n },\n //\n // reduce noise\n //\n \"@opentelemetry/instrumentation-fs\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-dns\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-net\": {\n enabled: false,\n },\n }),\n ];\n }\n\n /**\n * Flush and shut down the tracer, meter, and logger providers.\n *\n * Idempotent: the provider references are cleared synchronously and concurrent\n * or repeated calls await the same in-flight flush. Awaited by the core\n * lifecycle manager during graceful shutdown — that manager owns the\n * process signal handlers, so telemetry no longer registers its own.\n *\n * Survives re-`initialize()`. `shutdownPromise` is deliberately *not* cleared\n * when the flush settles, and that is safe: the memo is only ever reassigned\n * for whatever providers are currently live, so a stale resolved promise can\n * only be returned when there is nothing to flush. The covering test asserts\n * every provider set across repeated initialize/shutdown cycles is flushed.\n */\n async shutdown(): Promise<void> {\n const providers = [\n this.tracerProvider,\n this.meterProvider,\n this.loggerProvider,\n ].filter((p): p is NonNullable<typeof p> => p !== undefined);\n\n if (providers.length > 0) {\n this.tracerProvider = undefined;\n this.meterProvider = undefined;\n this.loggerProvider = undefined;\n this.shutdownPromise = (async () => {\n await Promise.all(\n providers.map(async (provider) => {\n try {\n await provider.shutdown();\n } catch (error) {\n logger.error(\"Error shutting down: %O\", error);\n }\n }),\n );\n })();\n }\n\n return this.shutdownPromise;\n }\n\n /**\n * Drop the singleton so the next {@link getInstance} builds a fresh manager.\n *\n * Does not flush: callers `shutdown()` first, then reset — the order\n * `LifecycleManager.shutdown()` uses.\n *\n * @internal\n */\n static reset(): void {\n TelemetryManager.instance = undefined;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"telemetry-manager.js","names":[],"sources":["../../src/telemetry/telemetry-manager.ts"],"sourcesContent":["import { metrics } from \"@opentelemetry/api\";\nimport { logs } from \"@opentelemetry/api-logs\";\nimport { getNodeAutoInstrumentations } from \"@opentelemetry/auto-instrumentations-node\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-proto\";\nimport { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-proto\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-proto\";\nimport {\n type Instrumentation,\n registerInstrumentations as otelRegisterInstrumentations,\n} from \"@opentelemetry/instrumentation\";\nimport {\n detectResources,\n envDetector,\n hostDetector,\n processDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n} from \"@opentelemetry/sdk-logs\";\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport {\n BatchSpanProcessor,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from \"@opentelemetry/semantic-conventions\";\nimport type { TelemetryOptions } from \"shared\";\n\nimport { createLogger } from \"../logging/logger\";\nimport { ExecutionSpanProcessor } from \"./execution-span-processor\";\nimport { TelemetryProvider } from \"./telemetry-provider\";\nimport { AppKitSampler } from \"./trace-sampler\";\nimport type { TelemetryConfig } from \"./types\";\n\nconst logger = createLogger(\"telemetry\");\n\n/**\n * Owns the app's OpenTelemetry providers, split into two phases so plugins can\n * contribute trace span processors before the tracer provider is built.\n *\n * - `initialize()` runs at app bootstrap, before plugin setup. It registers the\n * meter and logger providers eagerly, because OTel's metrics API has no lazy\n * proxy: a counter/histogram bound against the NoOp meter (as every connector\n * and the cache do in their constructors) stays NoOp for the process lifetime.\n * It does NOT register a tracer provider.\n * - `registerSpanProcessor()` is called by plugins during `setup()` to add a\n * span processor (e.g. an MLflow exporter) to the not-yet-built tracer.\n * - `start()` runs after all plugin `setup()` completes. It builds the single\n * global tracer provider with the OTLP processor (if configured) plus every\n * contributed processor. Deferring is safe for traces: OTel's ProxyTracer\n * rebinds tracers obtained before registration, and no span is emitted during\n * setup.\n */\nexport class TelemetryManager {\n private static readonly DEFAULT_EXPORT_INTERVAL_MS = 10000;\n private static readonly DEFAULT_FALLBACK_APP_NAME = \"databricks-app\";\n\n private static instance?: TelemetryManager;\n private resource?: Resource;\n private meterProvider?: MeterProvider;\n private loggerProvider?: LoggerProvider;\n private tracerProvider?: NodeTracerProvider;\n private readonly spanProcessors: SpanProcessor[] = [];\n private started = false;\n private shutdownPromise?: Promise<void>;\n\n /**\n * Create a scoped telemetry provider for a specific plugin.\n * The plugin's name will be used as the default tracer/meter name.\n * @param pluginName - The name of the plugin to create scoped telemetry for\n * @param telemetryConfig - The telemetry configuration for the plugin\n * @returns A scoped telemetry instance for the plugin\n */\n static getProvider(\n pluginName: string,\n telemetryConfig?: TelemetryOptions,\n ): TelemetryProvider {\n const globalManager = TelemetryManager.getInstance();\n return new TelemetryProvider(pluginName, globalManager, telemetryConfig);\n }\n\n private constructor() {}\n\n static getInstance(): TelemetryManager {\n if (!TelemetryManager.instance) {\n TelemetryManager.instance = new TelemetryManager();\n }\n return TelemetryManager.instance;\n }\n\n static initialize(config: Partial<TelemetryConfig> = {}): void {\n const instance = TelemetryManager.getInstance();\n instance._initialize(config);\n }\n\n /**\n * Contribute a span processor to the not-yet-built tracer provider. Called by\n * plugins during `setup()`. No-op with a warning once `start()` has run, since\n * a started provider's processors are immutable in OTel JS 2.x.\n */\n static registerSpanProcessor(processor: SpanProcessor): void {\n TelemetryManager.getInstance()._registerSpanProcessor(processor);\n }\n\n private _registerSpanProcessor(processor: SpanProcessor): void {\n if (this.started) {\n logger.warn(\n \"registerSpanProcessor called after start(); processor ignored. \" +\n \"Contribute span processors during plugin setup().\",\n );\n return;\n }\n this.spanProcessors.push(processor);\n }\n\n /**\n * Phase 1: register the meter and logger providers eagerly (before plugin\n * setup), so metric instruments bound in connector/cache constructors attach\n * to real meters. The tracer provider is deferred to `start()`.\n *\n * When no OTLP endpoint is configured, meter/logger registration is skipped;\n * a contributed span processor can still bring up tracing in `start()`.\n */\n private _initialize(config: Partial<TelemetryConfig>): void {\n if (this.resource) return;\n this.resource = this.createResource(config);\n\n // OTLP exporters need an endpoint. Without one there is nothing to export\n // metrics/logs to, so skip those providers — but still capture the resource\n // and let `start()` bring up a tracer if a plugin contributed a processor.\n if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {\n return;\n }\n\n try {\n this.meterProvider = new MeterProvider({\n resource: this.resource,\n readers: [\n new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({ headers: config.headers }),\n exportIntervalMillis:\n config.exportIntervalMs ||\n TelemetryManager.DEFAULT_EXPORT_INTERVAL_MS,\n }),\n ],\n });\n metrics.setGlobalMeterProvider(this.meterProvider);\n\n this.loggerProvider = new LoggerProvider({\n resource: this.resource,\n processors: [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({ headers: config.headers }),\n ),\n ],\n });\n logs.setGlobalLoggerProvider(this.loggerProvider);\n\n // The OTLP trace exporter is the first span processor; contributed\n // processors join it in `start()`.\n this.spanProcessors.push(\n new BatchSpanProcessor(\n new OTLPTraceExporter({ headers: config.headers }),\n ),\n );\n\n this.registerInstrumentations(this.getDefaultInstrumentations());\n logger.debug(\"Meter/logger providers initialized\");\n } catch (error) {\n logger.error(\"Failed to initialize: %O\", error);\n }\n }\n\n /**\n * Phase 2: build and register the global tracer provider. Called by core\n * after every plugin's `setup()` completes, so all contributed span\n * processors are known. No-op when nothing needs tracing (no OTLP endpoint\n * and no contributed processor), preserving \"no telemetry unless configured\".\n *\n * `NodeTracerProvider.register()` installs the async-hooks context manager and\n * W3C propagators — the same wiring `NodeSDK.start()` did — so span nesting\n * across awaits is preserved.\n */\n static start(): void {\n TelemetryManager.getInstance()._start();\n }\n\n private _start(): void {\n if (this.started) return;\n this.started = true;\n\n if (this.spanProcessors.length === 0) {\n return;\n }\n\n try {\n this.tracerProvider = new NodeTracerProvider({\n resource: this.resource,\n sampler: new AppKitSampler(),\n spanProcessors: [new ExecutionSpanProcessor(), ...this.spanProcessors],\n });\n this.tracerProvider.register();\n logger.debug(\n \"Tracer provider started with %d span processor(s)\",\n this.spanProcessors.length,\n );\n } catch (error) {\n logger.error(\"Failed to start tracer provider: %O\", error);\n }\n }\n\n /**\n * Register OpenTelemetry instrumentations.\n * Can be called at any time, but recommended to call in plugin constructor.\n * @param instrumentations - Array of OpenTelemetry instrumentations to register\n */\n registerInstrumentations(instrumentations: Instrumentation[]): void {\n otelRegisterInstrumentations({\n // Instrumentations bind to the global providers registered by start()\n // (tracer) and _initialize() (meter/logger).\n instrumentations,\n });\n }\n\n private createResource(config: Partial<TelemetryConfig>): Resource {\n const serviceName =\n config.serviceName ||\n process.env.OTEL_SERVICE_NAME ||\n process.env.DATABRICKS_APP_NAME ||\n TelemetryManager.DEFAULT_FALLBACK_APP_NAME;\n const initialResource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion ?? undefined,\n });\n const detectedResource = detectResources({\n detectors: [envDetector, hostDetector, processDetector],\n });\n return initialResource.merge(detectedResource);\n }\n\n private getDefaultInstrumentations(): Instrumentation[] {\n return [\n ...getNodeAutoInstrumentations({\n //\n // enabled as a part of the server plugin\n //\n \"@opentelemetry/instrumentation-http\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-express\": {\n enabled: false,\n },\n //\n // reduce noise\n //\n \"@opentelemetry/instrumentation-fs\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-dns\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-net\": {\n enabled: false,\n },\n }),\n ];\n }\n\n /**\n * Flush and shut down the tracer, meter, and logger providers.\n *\n * Idempotent: the provider references are cleared synchronously and concurrent\n * or repeated calls await the same in-flight flush. Awaited by the core\n * lifecycle manager during graceful shutdown — that manager owns the\n * process signal handlers, so telemetry no longer registers its own.\n *\n * Survives re-`initialize()`. `shutdownPromise` is deliberately *not* cleared\n * when the flush settles, and that is safe: the memo is only ever reassigned\n * for whatever providers are currently live, so a stale resolved promise can\n * only be returned when there is nothing to flush. The covering test asserts\n * every provider set across repeated initialize/shutdown cycles is flushed.\n */\n async shutdown(): Promise<void> {\n const providers = [\n this.tracerProvider,\n this.meterProvider,\n this.loggerProvider,\n ].filter((p): p is NonNullable<typeof p> => p !== undefined);\n\n if (providers.length > 0) {\n this.tracerProvider = undefined;\n this.meterProvider = undefined;\n this.loggerProvider = undefined;\n this.shutdownPromise = (async () => {\n await Promise.all(\n providers.map(async (provider) => {\n try {\n await provider.shutdown();\n } catch (error) {\n logger.error(\"Error shutting down: %O\", error);\n }\n }),\n );\n })();\n }\n\n return this.shutdownPromise;\n }\n\n /**\n * Drop the singleton so the next {@link getInstance} builds a fresh manager.\n *\n * Does not flush: callers `shutdown()` first, then reset — the order\n * `LifecycleManager.shutdown()` uses.\n *\n * @internal\n */\n static reset(): void {\n TelemetryManager.instance = undefined;\n }\n}\n"],"mappings":"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"}