@databricks/appkit 0.31.0 → 0.32.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 (152) hide show
  1. package/CLAUDE.md +1 -0
  2. package/NOTICE.md +1 -0
  3. package/dist/appkit/package.js +1 -1
  4. package/dist/beta.d.ts +14 -1
  5. package/dist/beta.js +12 -1
  6. package/dist/connectors/index.js +3 -0
  7. package/dist/connectors/mcp/client.d.ts +60 -0
  8. package/dist/connectors/mcp/client.d.ts.map +1 -0
  9. package/dist/connectors/mcp/client.js +197 -0
  10. package/dist/connectors/mcp/client.js.map +1 -0
  11. package/dist/connectors/mcp/host-policy.d.ts +51 -0
  12. package/dist/connectors/mcp/host-policy.d.ts.map +1 -0
  13. package/dist/connectors/mcp/host-policy.js +168 -0
  14. package/dist/connectors/mcp/host-policy.js.map +1 -0
  15. package/dist/connectors/mcp/index.d.ts +3 -0
  16. package/dist/connectors/mcp/index.js +4 -0
  17. package/dist/connectors/mcp/types.d.ts +16 -0
  18. package/dist/connectors/mcp/types.d.ts.map +1 -0
  19. package/dist/context/index.js +1 -1
  20. package/dist/core/agent/build-toolkit.d.ts +2 -0
  21. package/dist/core/agent/build-toolkit.js +50 -0
  22. package/dist/core/agent/build-toolkit.js.map +1 -0
  23. package/dist/core/agent/consume-adapter-stream.js +33 -0
  24. package/dist/core/agent/consume-adapter-stream.js.map +1 -0
  25. package/dist/core/agent/create-agent.d.ts +27 -0
  26. package/dist/core/agent/create-agent.d.ts.map +1 -0
  27. package/dist/core/agent/create-agent.js +50 -0
  28. package/dist/core/agent/create-agent.js.map +1 -0
  29. package/dist/core/agent/load-agents.d.ts +67 -0
  30. package/dist/core/agent/load-agents.d.ts.map +1 -0
  31. package/dist/core/agent/load-agents.js +228 -0
  32. package/dist/core/agent/load-agents.js.map +1 -0
  33. package/dist/core/agent/normalize-result.js +39 -0
  34. package/dist/core/agent/normalize-result.js.map +1 -0
  35. package/dist/core/agent/run-agent.d.ts +34 -0
  36. package/dist/core/agent/run-agent.d.ts.map +1 -0
  37. package/dist/core/agent/run-agent.js +146 -0
  38. package/dist/core/agent/run-agent.js.map +1 -0
  39. package/dist/core/agent/system-prompt.js +38 -0
  40. package/dist/core/agent/system-prompt.js.map +1 -0
  41. package/dist/core/agent/tools/define-tool.d.ts +54 -0
  42. package/dist/core/agent/tools/define-tool.d.ts.map +1 -0
  43. package/dist/core/agent/tools/define-tool.js +50 -0
  44. package/dist/core/agent/tools/define-tool.js.map +1 -0
  45. package/dist/core/agent/tools/function-tool.d.ts +27 -0
  46. package/dist/core/agent/tools/function-tool.d.ts.map +1 -0
  47. package/dist/core/agent/tools/function-tool.js +21 -0
  48. package/dist/core/agent/tools/function-tool.js.map +1 -0
  49. package/dist/core/agent/tools/hosted-tools.d.ts +47 -0
  50. package/dist/core/agent/tools/hosted-tools.d.ts.map +1 -0
  51. package/dist/core/agent/tools/hosted-tools.js +67 -0
  52. package/dist/core/agent/tools/hosted-tools.js.map +1 -0
  53. package/dist/core/agent/tools/index.d.ts +5 -0
  54. package/dist/core/agent/tools/index.js +7 -0
  55. package/dist/core/agent/tools/json-schema.js +24 -0
  56. package/dist/core/agent/tools/json-schema.js.map +1 -0
  57. package/dist/core/agent/tools/sql-policy.js +256 -0
  58. package/dist/core/agent/tools/sql-policy.js.map +1 -0
  59. package/dist/core/agent/tools/tool.d.ts +34 -0
  60. package/dist/core/agent/tools/tool.d.ts.map +1 -0
  61. package/dist/core/agent/tools/tool.js +41 -0
  62. package/dist/core/agent/tools/tool.js.map +1 -0
  63. package/dist/core/agent/types.d.ts +214 -0
  64. package/dist/core/agent/types.d.ts.map +1 -0
  65. package/dist/core/agent/types.js +12 -0
  66. package/dist/core/agent/types.js.map +1 -0
  67. package/dist/core/appkit.d.ts +1 -0
  68. package/dist/core/appkit.d.ts.map +1 -1
  69. package/dist/core/appkit.js +31 -4
  70. package/dist/core/appkit.js.map +1 -1
  71. package/dist/core/plugin-context.d.ts +133 -0
  72. package/dist/core/plugin-context.d.ts.map +1 -0
  73. package/dist/core/plugin-context.js +220 -0
  74. package/dist/core/plugin-context.js.map +1 -0
  75. package/dist/index.d.ts +11 -11
  76. package/dist/internal-telemetry/appkit-log.js +19 -0
  77. package/dist/internal-telemetry/appkit-log.js.map +1 -0
  78. package/dist/internal-telemetry/config.js +15 -0
  79. package/dist/internal-telemetry/config.js.map +1 -0
  80. package/dist/internal-telemetry/index.js +4 -0
  81. package/dist/internal-telemetry/reporter.js +132 -0
  82. package/dist/internal-telemetry/reporter.js.map +1 -0
  83. package/dist/plugin/index.d.ts +1 -1
  84. package/dist/plugin/plugin.d.ts +18 -3
  85. package/dist/plugin/plugin.d.ts.map +1 -1
  86. package/dist/plugin/plugin.js +26 -2
  87. package/dist/plugin/plugin.js.map +1 -1
  88. package/dist/plugin/to-plugin.d.ts +15 -4
  89. package/dist/plugin/to-plugin.d.ts.map +1 -1
  90. package/dist/plugin/to-plugin.js +14 -4
  91. package/dist/plugin/to-plugin.js.map +1 -1
  92. package/dist/plugins/agents/agents.d.ts +4 -0
  93. package/dist/plugins/agents/agents.js +882 -0
  94. package/dist/plugins/agents/agents.js.map +1 -0
  95. package/dist/plugins/agents/defaults.js +13 -0
  96. package/dist/plugins/agents/defaults.js.map +1 -0
  97. package/dist/plugins/agents/event-channel.js +64 -0
  98. package/dist/plugins/agents/event-channel.js.map +1 -0
  99. package/dist/plugins/agents/event-translator.js +224 -0
  100. package/dist/plugins/agents/event-translator.js.map +1 -0
  101. package/dist/plugins/agents/index.d.ts +4 -0
  102. package/dist/plugins/agents/index.js +6 -0
  103. package/dist/plugins/agents/manifest.js +27 -0
  104. package/dist/plugins/agents/manifest.js.map +1 -0
  105. package/dist/plugins/agents/schemas.js +51 -0
  106. package/dist/plugins/agents/schemas.js.map +1 -0
  107. package/dist/plugins/agents/thread-store.js +58 -0
  108. package/dist/plugins/agents/thread-store.js.map +1 -0
  109. package/dist/plugins/agents/tool-approval-gate.js +75 -0
  110. package/dist/plugins/agents/tool-approval-gate.js.map +1 -0
  111. package/dist/plugins/analytics/analytics.d.ts +17 -2
  112. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  113. package/dist/plugins/analytics/analytics.js +33 -0
  114. package/dist/plugins/analytics/analytics.js.map +1 -1
  115. package/dist/plugins/files/plugin.d.ts +22 -3
  116. package/dist/plugins/files/plugin.d.ts.map +1 -1
  117. package/dist/plugins/files/plugin.js +102 -2
  118. package/dist/plugins/files/plugin.js.map +1 -1
  119. package/dist/plugins/genie/genie.d.ts +15 -2
  120. package/dist/plugins/genie/genie.d.ts.map +1 -1
  121. package/dist/plugins/genie/genie.js +45 -0
  122. package/dist/plugins/genie/genie.js.map +1 -1
  123. package/dist/plugins/jobs/plugin.d.ts +2 -1
  124. package/dist/plugins/jobs/plugin.d.ts.map +1 -1
  125. package/dist/plugins/jobs/plugin.js +1 -1
  126. package/dist/plugins/lakebase/index.d.ts +2 -2
  127. package/dist/plugins/lakebase/index.js +1 -1
  128. package/dist/plugins/lakebase/lakebase.d.ts +33 -4
  129. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  130. package/dist/plugins/lakebase/lakebase.js +77 -5
  131. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  132. package/dist/plugins/lakebase/types.d.ts +38 -1
  133. package/dist/plugins/lakebase/types.d.ts.map +1 -1
  134. package/dist/plugins/server/index.d.ts +12 -1
  135. package/dist/plugins/server/index.d.ts.map +1 -1
  136. package/dist/plugins/server/index.js +39 -5
  137. package/dist/plugins/server/index.js.map +1 -1
  138. package/dist/plugins/server/types.d.ts +0 -3
  139. package/dist/plugins/server/types.d.ts.map +1 -1
  140. package/dist/plugins/serving/serving.d.ts +2 -1
  141. package/dist/plugins/serving/serving.d.ts.map +1 -1
  142. package/dist/shared/src/agent.d.ts +63 -1
  143. package/dist/shared/src/agent.d.ts.map +1 -1
  144. package/dist/shared/src/index.d.ts +1 -1
  145. package/dist/shared/src/plugin.d.ts +8 -0
  146. package/dist/shared/src/plugin.d.ts.map +1 -1
  147. package/docs/api/appkit/Class.Plugin.md +65 -23
  148. package/docs/api/appkit/Function.createApp.md +10 -8
  149. package/docs/privacy.md +41 -0
  150. package/llms.txt +1 -0
  151. package/package.json +4 -2
  152. package/sbom.cdx.json +1 -1
@@ -3,7 +3,11 @@ import { createLakebasePool, getLakebaseOrmConfig, getLakebasePgConfig, getUsern
3
3
  import { Plugin } from "../../plugin/plugin.js";
4
4
  import { toPlugin } from "../../plugin/to-plugin.js";
5
5
  import "../../plugin/index.js";
6
+ import { buildToolkitEntries } from "../../core/agent/build-toolkit.js";
7
+ import { defineTool, executeFromRegistry, toolsFromRegistry } from "../../core/agent/tools/define-tool.js";
8
+ import { assertReadOnlySql } from "../../core/agent/tools/sql-policy.js";
6
9
  import manifest_default from "./manifest.js";
10
+ import { z } from "zod";
7
11
 
8
12
  //#region src/plugins/lakebase/lakebase.ts
9
13
  const logger = createLogger("lakebase");
@@ -28,10 +32,6 @@ var LakebasePlugin = class extends Plugin {
28
32
  /** Plugin manifest declaring metadata and resource requirements */
29
33
  static manifest = manifest_default;
30
34
  pool = null;
31
- constructor(config) {
32
- super(config);
33
- this.config = config;
34
- }
35
35
  /**
36
36
  * Initializes the Lakebase connection pool.
37
37
  * Called automatically by AppKit during the plugin setup phase.
@@ -67,6 +67,33 @@ var LakebasePlugin = class extends Plugin {
67
67
  return this.pool.query(text, values);
68
68
  }
69
69
  /**
70
+ * Execute a single statement inside a `BEGIN READ ONLY … ROLLBACK`
71
+ * transaction on a dedicated client.
72
+ *
73
+ * The three commands MUST share a connection — a naive
74
+ * `pool.query("BEGIN READ ONLY; <stmt>; ROLLBACK")` batch cannot accept
75
+ * parameter values (PostgreSQL's Extended Query protocol rejects multi-
76
+ * statement prepared queries), which would silently break every
77
+ * parameterized query the agent tool issues.
78
+ *
79
+ * Returns the raw `rows` array for the user's statement. Side effects the
80
+ * statement may attempt (writes, writable-function side effects) are
81
+ * rejected by PostgreSQL under the read-only transaction posture.
82
+ */
83
+ async runReadOnlyStatement(text, values) {
84
+ const client = await this.pool.connect();
85
+ try {
86
+ await client.query("BEGIN READ ONLY");
87
+ return (await client.query(text, values)).rows;
88
+ } finally {
89
+ try {
90
+ await client.query("ROLLBACK");
91
+ } finally {
92
+ client.release();
93
+ }
94
+ }
95
+ }
96
+ /**
70
97
  * Gracefully drains and closes the connection pool.
71
98
  * Called automatically by AppKit during shutdown.
72
99
  */
@@ -88,6 +115,51 @@ var LakebasePlugin = class extends Plugin {
88
115
  * - `getOrmConfig()` — Returns a config object compatible with Drizzle, TypeORM, Sequelize, etc.
89
116
  * - `getPgConfig()` — Returns a `pg.PoolConfig` object for manual pool construction
90
117
  */
118
+ /**
119
+ * Agent tool registry. Empty by default — the Lakebase plugin does NOT
120
+ * expose its SQL connection to LLM agents unless the developer explicitly
121
+ * opts in via `config.exposeAsAgentTool`. See {@link buildQueryTool}.
122
+ */
123
+ tools = {};
124
+ constructor(config) {
125
+ super(config);
126
+ this.config = config;
127
+ if (config.exposeAsAgentTool) {
128
+ this.tools = { query: this.buildQueryTool(config.exposeAsAgentTool) };
129
+ logger.warn("Lakebase agent tool is enabled (readOnly=%s). Every agent with access to this plugin can execute SQL against the Lakebase database as the service principal.", config.exposeAsAgentTool.readOnly !== false);
130
+ }
131
+ }
132
+ buildQueryTool(opt) {
133
+ const readOnly = opt.readOnly !== false;
134
+ return defineTool({
135
+ description: readOnly ? "Execute a read-only SQL query against the Lakebase PostgreSQL database. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted. Use $1, $2, etc. as placeholders and pass values separately. Runs as the application's service principal." : "Execute a parameterized SQL statement against the Lakebase PostgreSQL database. Use $1, $2, etc. as placeholders and pass values separately. Runs as the application's service principal. This tool can modify data; every invocation requires explicit human approval.",
136
+ schema: z.object({
137
+ text: z.string().describe("SQL statement with $1, $2, ... placeholders for parameters"),
138
+ values: z.array(z.unknown()).optional().describe("Parameter values corresponding to placeholders")
139
+ }),
140
+ annotations: {
141
+ readOnly,
142
+ destructive: !readOnly,
143
+ idempotent: false
144
+ },
145
+ handler: async (args) => {
146
+ if (readOnly) {
147
+ assertReadOnlySql(args.text);
148
+ return this.runReadOnlyStatement(args.text, args.values);
149
+ }
150
+ return (await this.query(args.text, args.values)).rows;
151
+ }
152
+ });
153
+ }
154
+ getAgentTools() {
155
+ return toolsFromRegistry(this.tools);
156
+ }
157
+ async executeAgentTool(name, args, signal) {
158
+ return executeFromRegistry(this.tools, name, args, signal);
159
+ }
160
+ toolkit(opts) {
161
+ return buildToolkitEntries(this.name, this.tools, opts);
162
+ }
91
163
  exports() {
92
164
  return {
93
165
  pool: this.pool,
@@ -103,5 +175,5 @@ var LakebasePlugin = class extends Plugin {
103
175
  const lakebase = toPlugin(LakebasePlugin);
104
176
 
105
177
  //#endregion
106
- export { lakebase };
178
+ export { LakebasePlugin, lakebase };
107
179
  //# sourceMappingURL=lakebase.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lakebase.js","names":["manifest"],"sources":["../../../src/plugins/lakebase/lakebase.ts"],"sourcesContent":["import type { Pool, QueryResult, QueryResultRow } from \"pg\";\nimport {\n createLakebasePool,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n} from \"../../connectors/lakebase\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport manifest from \"./manifest.json\";\nimport type { ILakebaseConfig } from \"./types\";\n\nconst logger = createLogger(\"lakebase\");\n\n/**\n * AppKit plugin for Databricks Lakebase Autoscaling.\n *\n * Wraps `@databricks/lakebase` to provide a standard `pg.Pool` with automatic\n * OAuth token refresh, integrated with AppKit's logger and OpenTelemetry setup.\n *\n * @example\n * ```ts\n * import { createApp, lakebase, server } from \"@databricks/appkit\";\n *\n * const AppKit = await createApp({\n * plugins: [server(), lakebase()],\n * });\n *\n * const result = await AppKit.lakebase.query(\"SELECT * FROM users WHERE id = $1\", [userId]);\n * ```\n */\nclass LakebasePlugin extends Plugin {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"lakebase\">;\n\n protected declare config: ILakebaseConfig;\n private pool: Pool | null = null;\n\n constructor(config: ILakebaseConfig) {\n super(config);\n this.config = config;\n }\n\n /**\n * Initializes the Lakebase connection pool.\n * Called automatically by AppKit during the plugin setup phase.\n *\n * Resolves the PostgreSQL username via {@link getUsernameWithApiLookup},\n * which tries config, env vars, and finally the Databricks workspace API.\n */\n async setup() {\n const poolConfig = this.config.pool;\n const user = await getUsernameWithApiLookup(poolConfig);\n this.pool = createLakebasePool({ ...poolConfig, user });\n logger.info(\"Lakebase pool initialized\");\n }\n\n /**\n * Executes a parameterized SQL query against the Lakebase pool.\n *\n * @param text - SQL query string, using `$1`, `$2`, ... placeholders\n * @param values - Parameter values corresponding to placeholders\n * @returns Query result with typed rows\n *\n * @example\n * ```ts\n * const result = await AppKit.lakebase.query<{ id: number; name: string }>(\n * \"SELECT id, name FROM users WHERE active = $1\",\n * [true],\n * );\n * ```\n */\n async query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n return this.pool!.query<T>(text, values);\n }\n\n /**\n * Gracefully drains and closes the connection pool.\n * Called automatically by AppKit during shutdown.\n */\n abortActiveOperations(): void {\n super.abortActiveOperations();\n if (this.pool) {\n logger.info(\"Closing Lakebase pool\");\n this.pool.end().catch((err) => {\n logger.error(\"Error closing Lakebase pool: %O\", err);\n });\n this.pool = null;\n }\n }\n\n /**\n * Returns the plugin's public API, accessible via `AppKit.lakebase`.\n *\n * - `pool` — The raw `pg.Pool` instance, for use with ORMs or advanced scenarios\n * - `query` — Convenience method for executing parameterized SQL queries\n * - `getOrmConfig()` — Returns a config object compatible with Drizzle, TypeORM, Sequelize, etc.\n * - `getPgConfig()` — Returns a `pg.PoolConfig` object for manual pool construction\n */\n exports() {\n return {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n pool: this.pool!,\n query: this.query.bind(this),\n getOrmConfig: () => getLakebaseOrmConfig(this.config.pool),\n getPgConfig: () => getLakebasePgConfig(this.config.pool),\n };\n }\n}\n\n/**\n * @internal\n */\nexport const lakebase = toPlugin(LakebasePlugin);\n"],"mappings":";;;;;;;;AAaA,MAAM,SAAS,aAAa,WAAW;;;;;;;;;;;;;;;;;;AAmBvC,IAAM,iBAAN,cAA6B,OAAO;;CAElC,OAAO,WAAWA;CAGlB,AAAQ,OAAoB;CAE5B,YAAY,QAAyB;AACnC,QAAM,OAAO;AACb,OAAK,SAAS;;;;;;;;;CAUhB,MAAM,QAAQ;EACZ,MAAM,aAAa,KAAK,OAAO;EAC/B,MAAM,OAAO,MAAM,yBAAyB,WAAW;AACvD,OAAK,OAAO,mBAAmB;GAAE,GAAG;GAAY;GAAM,CAAC;AACvD,SAAO,KAAK,4BAA4B;;;;;;;;;;;;;;;;;CAkB1C,MAAM,MACJ,MACA,QACyB;AAEzB,SAAO,KAAK,KAAM,MAAS,MAAM,OAAO;;;;;;CAO1C,wBAA8B;AAC5B,QAAM,uBAAuB;AAC7B,MAAI,KAAK,MAAM;AACb,UAAO,KAAK,wBAAwB;AACpC,QAAK,KAAK,KAAK,CAAC,OAAO,QAAQ;AAC7B,WAAO,MAAM,mCAAmC,IAAI;KACpD;AACF,QAAK,OAAO;;;;;;;;;;;CAYhB,UAAU;AACR,SAAO;GAEL,MAAM,KAAK;GACX,OAAO,KAAK,MAAM,KAAK,KAAK;GAC5B,oBAAoB,qBAAqB,KAAK,OAAO,KAAK;GAC1D,mBAAmB,oBAAoB,KAAK,OAAO,KAAK;GACzD;;;;;;AAOL,MAAa,WAAW,SAAS,eAAe"}
1
+ {"version":3,"file":"lakebase.js","names":["manifest"],"sources":["../../../src/plugins/lakebase/lakebase.ts"],"sourcesContent":["import type { Pool, QueryResult, QueryResultRow } from \"pg\";\nimport type { AgentToolDefinition, ToolProvider } from \"shared\";\nimport { z } from \"zod\";\nimport {\n createLakebasePool,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n} from \"../../connectors/lakebase\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport manifest from \"./manifest.json\";\nimport type { ILakebaseConfig } from \"./types\";\n\nconst logger = createLogger(\"lakebase\");\n\n/**\n * AppKit plugin for Databricks Lakebase Autoscaling.\n *\n * Wraps `@databricks/lakebase` to provide a standard `pg.Pool` with automatic\n * OAuth token refresh, integrated with AppKit's logger and OpenTelemetry setup.\n *\n * @example\n * ```ts\n * import { createApp, lakebase, server } from \"@databricks/appkit\";\n *\n * const AppKit = await createApp({\n * plugins: [server(), lakebase()],\n * });\n *\n * const result = await AppKit.lakebase.query(\"SELECT * FROM users WHERE id = $1\", [userId]);\n * ```\n */\nexport class LakebasePlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"lakebase\">;\n\n protected declare config: ILakebaseConfig;\n private pool: Pool | null = null;\n\n /**\n * Initializes the Lakebase connection pool.\n * Called automatically by AppKit during the plugin setup phase.\n *\n * Resolves the PostgreSQL username via {@link getUsernameWithApiLookup},\n * which tries config, env vars, and finally the Databricks workspace API.\n */\n async setup() {\n const poolConfig = this.config.pool;\n const user = await getUsernameWithApiLookup(poolConfig);\n this.pool = createLakebasePool({ ...poolConfig, user });\n logger.info(\"Lakebase pool initialized\");\n }\n\n /**\n * Executes a parameterized SQL query against the Lakebase pool.\n *\n * @param text - SQL query string, using `$1`, `$2`, ... placeholders\n * @param values - Parameter values corresponding to placeholders\n * @returns Query result with typed rows\n *\n * @example\n * ```ts\n * const result = await AppKit.lakebase.query<{ id: number; name: string }>(\n * \"SELECT id, name FROM users WHERE active = $1\",\n * [true],\n * );\n * ```\n */\n async query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n return this.pool!.query<T>(text, values);\n }\n\n /**\n * Execute a single statement inside a `BEGIN READ ONLY … ROLLBACK`\n * transaction on a dedicated client.\n *\n * The three commands MUST share a connection — a naive\n * `pool.query(\"BEGIN READ ONLY; <stmt>; ROLLBACK\")` batch cannot accept\n * parameter values (PostgreSQL's Extended Query protocol rejects multi-\n * statement prepared queries), which would silently break every\n * parameterized query the agent tool issues.\n *\n * Returns the raw `rows` array for the user's statement. Side effects the\n * statement may attempt (writes, writable-function side effects) are\n * rejected by PostgreSQL under the read-only transaction posture.\n */\n private async runReadOnlyStatement(\n text: string,\n values?: unknown[],\n ): Promise<unknown[]> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup()\n const client = await this.pool!.connect();\n try {\n await client.query(\"BEGIN READ ONLY\");\n const result = await client.query(text, values);\n return result.rows;\n } finally {\n try {\n await client.query(\"ROLLBACK\");\n } finally {\n client.release();\n }\n }\n }\n\n /**\n * Gracefully drains and closes the connection pool.\n * Called automatically by AppKit during shutdown.\n */\n abortActiveOperations(): void {\n super.abortActiveOperations();\n if (this.pool) {\n logger.info(\"Closing Lakebase pool\");\n this.pool.end().catch((err) => {\n logger.error(\"Error closing Lakebase pool: %O\", err);\n });\n this.pool = null;\n }\n }\n\n /**\n * Returns the plugin's public API, accessible via `AppKit.lakebase`.\n *\n * - `pool` — The raw `pg.Pool` instance, for use with ORMs or advanced scenarios\n * - `query` — Convenience method for executing parameterized SQL queries\n * - `getOrmConfig()` — Returns a config object compatible with Drizzle, TypeORM, Sequelize, etc.\n * - `getPgConfig()` — Returns a `pg.PoolConfig` object for manual pool construction\n */\n\n /**\n * Agent tool registry. Empty by default — the Lakebase plugin does NOT\n * expose its SQL connection to LLM agents unless the developer explicitly\n * opts in via `config.exposeAsAgentTool`. See {@link buildQueryTool}.\n */\n private tools: Record<string, ReturnType<typeof this.buildQueryTool>> = {};\n\n constructor(config: ILakebaseConfig) {\n super(config);\n this.config = config;\n if (config.exposeAsAgentTool) {\n this.tools = { query: this.buildQueryTool(config.exposeAsAgentTool) };\n logger.warn(\n \"Lakebase agent tool is enabled (readOnly=%s). Every agent with access to this plugin can execute SQL against the Lakebase database as the service principal.\",\n config.exposeAsAgentTool.readOnly !== false,\n );\n }\n }\n\n private buildQueryTool(\n opt: NonNullable<ILakebaseConfig[\"exposeAsAgentTool\"]>,\n ) {\n const readOnly = opt.readOnly !== false;\n return defineTool({\n description: readOnly\n ? \"Execute a read-only SQL query against the Lakebase PostgreSQL database. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted. Use $1, $2, etc. as placeholders and pass values separately. Runs as the application's service principal.\"\n : \"Execute a parameterized SQL statement against the Lakebase PostgreSQL database. Use $1, $2, etc. as placeholders and pass values separately. Runs as the application's service principal. This tool can modify data; every invocation requires explicit human approval.\",\n schema: z.object({\n text: z\n .string()\n .describe(\n \"SQL statement with $1, $2, ... placeholders for parameters\",\n ),\n values: z\n .array(z.unknown())\n .optional()\n .describe(\"Parameter values corresponding to placeholders\"),\n }),\n annotations: {\n readOnly,\n destructive: !readOnly,\n idempotent: false,\n },\n handler: async (args) => {\n if (readOnly) {\n assertReadOnlySql(args.text);\n return this.runReadOnlyStatement(args.text, args.values);\n }\n const result = await this.query(args.text, args.values);\n return result.rows;\n },\n });\n }\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n exports() {\n return {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n pool: this.pool!,\n query: this.query.bind(this),\n getOrmConfig: () => getLakebaseOrmConfig(this.config.pool),\n getPgConfig: () => getLakebasePgConfig(this.config.pool),\n };\n }\n}\n\n/**\n * @internal\n */\nexport const lakebase = toPlugin(LakebasePlugin);\n"],"mappings":";;;;;;;;;;;;AAsBA,MAAM,SAAS,aAAa,WAAW;;;;;;;;;;;;;;;;;;AAmBvC,IAAa,iBAAb,cAAoC,OAA+B;;CAEjE,OAAO,WAAWA;CAGlB,AAAQ,OAAoB;;;;;;;;CAS5B,MAAM,QAAQ;EACZ,MAAM,aAAa,KAAK,OAAO;EAC/B,MAAM,OAAO,MAAM,yBAAyB,WAAW;AACvD,OAAK,OAAO,mBAAmB;GAAE,GAAG;GAAY;GAAM,CAAC;AACvD,SAAO,KAAK,4BAA4B;;;;;;;;;;;;;;;;;CAkB1C,MAAM,MACJ,MACA,QACyB;AAEzB,SAAO,KAAK,KAAM,MAAS,MAAM,OAAO;;;;;;;;;;;;;;;;CAiB1C,MAAc,qBACZ,MACA,QACoB;EAEpB,MAAM,SAAS,MAAM,KAAK,KAAM,SAAS;AACzC,MAAI;AACF,SAAM,OAAO,MAAM,kBAAkB;AAErC,WADe,MAAM,OAAO,MAAM,MAAM,OAAO,EACjC;YACN;AACR,OAAI;AACF,UAAM,OAAO,MAAM,WAAW;aACtB;AACR,WAAO,SAAS;;;;;;;;CAStB,wBAA8B;AAC5B,QAAM,uBAAuB;AAC7B,MAAI,KAAK,MAAM;AACb,UAAO,KAAK,wBAAwB;AACpC,QAAK,KAAK,KAAK,CAAC,OAAO,QAAQ;AAC7B,WAAO,MAAM,mCAAmC,IAAI;KACpD;AACF,QAAK,OAAO;;;;;;;;;;;;;;;;CAkBhB,AAAQ,QAAgE,EAAE;CAE1E,YAAY,QAAyB;AACnC,QAAM,OAAO;AACb,OAAK,SAAS;AACd,MAAI,OAAO,mBAAmB;AAC5B,QAAK,QAAQ,EAAE,OAAO,KAAK,eAAe,OAAO,kBAAkB,EAAE;AACrE,UAAO,KACL,gKACA,OAAO,kBAAkB,aAAa,MACvC;;;CAIL,AAAQ,eACN,KACA;EACA,MAAM,WAAW,IAAI,aAAa;AAClC,SAAO,WAAW;GAChB,aAAa,WACT,8PACA;GACJ,QAAQ,EAAE,OAAO;IACf,MAAM,EACH,QAAQ,CACR,SACC,6DACD;IACH,QAAQ,EACL,MAAM,EAAE,SAAS,CAAC,CAClB,UAAU,CACV,SAAS,iDAAiD;IAC9D,CAAC;GACF,aAAa;IACX;IACA,aAAa,CAAC;IACd,YAAY;IACb;GACD,SAAS,OAAO,SAAS;AACvB,QAAI,UAAU;AACZ,uBAAkB,KAAK,KAAK;AAC5B,YAAO,KAAK,qBAAqB,KAAK,MAAM,KAAK,OAAO;;AAG1D,YADe,MAAM,KAAK,MAAM,KAAK,MAAM,KAAK,OAAO,EACzC;;GAEjB,CAAC;;CAGJ,gBAAuC;AACrC,SAAO,kBAAkB,KAAK,MAAM;;CAGtC,MAAM,iBACJ,MACA,MACA,QACkB;AAClB,SAAO,oBAAoB,KAAK,OAAO,MAAM,MAAM,OAAO;;CAG5D,QAAQ,MAAwD;AAC9D,SAAO,oBAAoB,KAAK,MAAM,KAAK,OAAO,KAAK;;CAGzD,UAAU;AACR,SAAO;GAEL,MAAM,KAAK;GACX,OAAO,KAAK,MAAM,KAAK,KAAK;GAC5B,oBAAoB,qBAAqB,KAAK,OAAO,KAAK;GAC1D,mBAAmB,oBAAoB,KAAK,OAAO,KAAK;GACzD;;;;;;AAOL,MAAa,WAAW,SAAS,eAAe"}
@@ -3,6 +3,36 @@ import "../../shared/src/index.js";
3
3
  import { LakebasePoolConfig } from "../../connectors/lakebase/index.js";
4
4
 
5
5
  //#region src/plugins/lakebase/types.d.ts
6
+ /**
7
+ * Opt-in configuration for exposing Lakebase as an agent-callable SQL tool.
8
+ *
9
+ * This tool executes LLM-authored SQL against the Lakebase pool. The pool is
10
+ * **always bound to the application's service-principal credentials**, so any
11
+ * agent that can call this tool effectively has full SP access to the database
12
+ * regardless of which end user initiated the request — setting
13
+ * `exposeAsAgentTool` is itself the deliberate opt-in. The plugin emits a
14
+ * startup `warn` log whenever the tool is enabled.
15
+ *
16
+ * When `readOnly: true` (default when opted in), every statement is:
17
+ * 1. Classified by {@link @databricks/appkit's sql-policy classifier}; anything
18
+ * that isn't a pure `SELECT`/`WITH`/`SHOW`/`EXPLAIN`/`DESCRIBE` is rejected.
19
+ * 2. Executed inside a `BEGIN READ ONLY … ROLLBACK` transaction so the
20
+ * PostgreSQL server rejects writes that slip past the classifier (e.g., a
21
+ * `SELECT` over a function with side effects).
22
+ *
23
+ * When `readOnly: false`, the tool is annotated `destructive: true` and the
24
+ * agents plugin will require human approval for every invocation (see
25
+ * `AgentsPluginConfig.approval`).
26
+ */
27
+ interface LakebaseExposeAsAgentTool {
28
+ /**
29
+ * Enforce read-only execution. Defaults to `true`. Set to `false` to allow
30
+ * destructive statements — highly discouraged outside of tightly controlled
31
+ * single-user deployments. Combined with the `destructive: true` annotation,
32
+ * the agents plugin will require explicit human approval for each call.
33
+ */
34
+ readOnly?: boolean;
35
+ }
6
36
  /**
7
37
  * Configuration for the Lakebase plugin.
8
38
  *
@@ -19,7 +49,14 @@ interface ILakebaseConfig extends BasePluginConfig {
19
49
  * Common overrides: `max` (pool size), `connectionTimeoutMillis`, `idleTimeoutMillis`.
20
50
  */
21
51
  pool?: Partial<LakebasePoolConfig>;
52
+ /**
53
+ * Opt-in to expose Lakebase as an agent-callable SQL tool. By default no
54
+ * agent tool is registered — the Lakebase plugin only exposes its API to
55
+ * application code. See {@link LakebaseExposeAsAgentTool} for the privilege
56
+ * implications of enabling this.
57
+ */
58
+ exposeAsAgentTool?: LakebaseExposeAsAgentTool;
22
59
  }
23
60
  //#endregion
24
- export { ILakebaseConfig };
61
+ export { ILakebaseConfig, LakebaseExposeAsAgentTool };
25
62
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/lakebase/types.ts"],"mappings":";;;;;;;;AAWA;;;;;UAAiB,eAAA,SAAwB,gBAAA;EAAgB;;;;;;EAOvD,IAAA,GAAO,OAAA,CAAQ,kBAAA;AAAA"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/lakebase/types.ts"],"mappings":";;;;;;;;AAwBA;;;;;AAkBA;;;;;;;;;;;;;UAlBiB,yBAAA;EAgCK;;;;;;EAzBpB,QAAA;AAAA;;;;;;;;;UAWe,eAAA,SAAwB,gBAAA;;;;;;;EAOvC,IAAA,GAAO,OAAA,CAAQ,kBAAA;;;;;;;EAOf,iBAAA,GAAoB,yBAAA;AAAA"}
@@ -1,6 +1,7 @@
1
1
  import { PluginPhase, TelemetryOptions, ToPlugin } from "../../shared/src/plugin.js";
2
2
  import "../../shared/src/index.js";
3
3
  import { Plugin } from "../../plugin/plugin.js";
4
+ import { NamedPluginFactory } from "../../plugin/to-plugin.js";
4
5
  import "../../plugin/index.js";
5
6
  import { PluginManifest } from "../../registry/types.js";
6
7
  import "../../registry/index.js";
@@ -29,6 +30,7 @@ import express from "express";
29
30
  * },
30
31
  * });
31
32
  * ```
33
+ *
32
34
  */
33
35
  declare class ServerPlugin extends Plugin {
34
36
  static DEFAULT_CONFIG: {
@@ -48,6 +50,8 @@ declare class ServerPlugin extends Plugin {
48
50
  private rawBodyPaths;
49
51
  static phase: PluginPhase;
50
52
  constructor(config: ServerConfig);
53
+ attachContext(deps?: Parameters<Plugin["attachContext"]>[0]): void;
54
+ /** Setup the server plugin. */
51
55
  setup(): Promise<void>;
52
56
  /** Get the server configuration. */
53
57
  getConfig(): {
@@ -86,6 +90,13 @@ declare class ServerPlugin extends Plugin {
86
90
  * @returns The server plugin instance for chaining.
87
91
  */
88
92
  extend(fn: (app: express.Application) => void): this;
93
+ /**
94
+ * Register a server extension from another plugin during setup.
95
+ * Unlike extend(), this is designed for internal plugin-to-plugin
96
+ * coordination where extensions are registered before the server starts
97
+ * listening — typically called by PluginContext when flushing buffered routes.
98
+ */
99
+ addExtension(fn: (app: express.Application) => void): void;
89
100
  /**
90
101
  * Setup the routes with the plugins.
91
102
  *
@@ -131,7 +142,7 @@ declare class ServerPlugin extends Plugin {
131
142
  /**
132
143
  * @internal
133
144
  */
134
- declare const server: ToPlugin<typeof ServerPlugin, ServerConfig, "server">;
145
+ declare const server: ToPlugin<typeof ServerPlugin, ServerConfig, "server"> & NamedPluginFactory<"server">;
135
146
  //#endregion
136
147
  export { server };
137
148
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/plugins/server/index.ts"],"mappings":";;;;;;;;;;;;;;;;AAgDA;;;;;;;;;;;;;;;;cAAa,YAAA,SAAqB,MAAA;EAAA,OAClB,cAAA;;;;;SAMP,QAAA,EAAuB,cAAA;EAAA,QACtB,iBAAA;EAAA,QACA,MAAA;EAAA,QACA,aAAA;EAAA,QACA,sBAAA;EADA;EAAA,QAGA,kBAAA;EAAA,UACU,MAAA,EAAQ,YAAA;EAAA,QAClB,gBAAA;EAAA,QACA,YAAA;EAAA,OACD,KAAA,EAAO,WAAA;cAEF,MAAA,EAAQ,YAAA;EAmBd,KAAA,CAAA,GAAK,OAAA;EArBG;EAwBd,SAAA,CAAA;IAAA;;;;;gBAHW,gBAAA;EAAA;;;;;;;;;EAiBL,KAAA,CAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,WAAA;EA6D/B;;;;;;;;EAAA,SAAA,CAAA,GAAa,MAAA;EA2IE;;;;;;;;;EA1Hf,MAAA,CAAO,EAAA,GAAK,GAAA,EAAK,OAAA,CAAQ,WAAA;;;;;;;;UAYX,YAAA;;;;;;AAuQhB;UArMgB,aAAA;EAAA,eA4CC,cAAA;EAyJE;;;;;EAAA,QAvIH,iBAAA;EAAA,QAqBN,cAAA;EAAA,QA+BM,iBAAA;EAmFG;;;;EAlCjB,OAAA,CAAA;yEAIgB,GAAA,EAAK,OAAA,CAAQ,WAAA;qBAtQhB,MAAA;;;;;;;kBAAU,gBAAA;IAAA;;;;;;;cAoSZ,MAAA,EAAM,QAAA,QAAA,YAAA,EAAA,YAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/plugins/server/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;AAkDA;;;;;;;;;;;;;;;;cAAa,YAAA,SAAqB,MAAA;EAAA,OAClB,cAAA;;;;EADwB;EAAA,OAO/B,QAAA,EAAuB,cAAA;EAAA,QACtB,iBAAA;EAAA,QACA,MAAA;EAAA,QACA,aAAA;EAAA,QACA,sBAAA;EAJsB;EAAA,QAMtB,kBAAA;EAAA,UACU,MAAA,EAAQ,YAAA;EAAA,QAClB,gBAAA;EAAA,QACA,YAAA;EAAA,OACD,KAAA,EAAO,WAAA;cAEF,MAAA,EAAQ,YAAA;EAepB,aAAA,CAAc,IAAA,GAAM,UAAA,CAAW,MAAA;EAnBvB;EA6BF,KAAA,CAAA,GAAK,OAAA;EA3BJ;EA8BP,SAAA,CAAA;IAAA;;;;;gBAHW,gBAAA;EAAA;EAAL;;;;;;;;EAiBA,KAAA,CAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,WAAA;;;;;;;;;EA8D/B,SAAA,CAAA,GAAa,MAAA;EAiBY;;;;;;;;;EAAzB,MAAA,CAAO,EAAA,GAAK,GAAA,EAAK,OAAA,CAAQ,WAAA;EAqIV;;;;;;EA1Hf,YAAA,CAAa,EAAA,GAAK,GAAA,EAAK,OAAA,CAAQ,WAAA;EAwPF;;;;;;;EAAA,QA7Of,YAAA;;;;;;;UAmEA,aAAA;EAAA,eA4CC,cAAA;EAoLJ;;;;;EAAA,QAlKG,iBAAA;EAAA,QAqBN,cAAA;EAAA,QA+BM,iBAAA;EA8GG;;;;EA1DjB,OAAA,CAAA;IA0DiB,qEAtDD,GAAA,EAAK,OAAA,CAAQ,WAAA,YAsDZ;qBA1UJ,MAAA;;;;;;;kBAAU,gBAAA;IAAA;;;;;;;cA0UZ,MAAA,EAAM,QAAA,QAAA,YAAA,EAAA,YAAA,cAAA,kBAAA"}
@@ -3,6 +3,8 @@ import { ServerError } from "../../errors/server.js";
3
3
  import { init_errors } from "../../errors/index.js";
4
4
  import { instrumentations } from "../../telemetry/instrumentations.js";
5
5
  import "../../telemetry/index.js";
6
+ import { TelemetryReporter } from "../../internal-telemetry/reporter.js";
7
+ import "../../internal-telemetry/index.js";
6
8
  import { Plugin } from "../../plugin/plugin.js";
7
9
  import { toPlugin } from "../../plugin/to-plugin.js";
8
10
  import "../../plugin/index.js";
@@ -44,6 +46,7 @@ const devListenPortSpan = 100;
44
46
  * },
45
47
  * });
46
48
  * ```
49
+ *
47
50
  */
48
51
  var ServerPlugin = class ServerPlugin extends Plugin {
49
52
  static DEFAULT_CONFIG = {
@@ -62,14 +65,19 @@ var ServerPlugin = class ServerPlugin extends Plugin {
62
65
  rawBodyPaths = /* @__PURE__ */ new Set();
63
66
  static phase = "deferred";
64
67
  constructor(config) {
65
- if ("autoStart" in config) throw new ServerError("server({ autoStart }) has been removed. The server is now started automatically by createApp.\n\nRun `npx appkit codemod on-plugins-ready --write` to auto-migrate.");
66
68
  super(config);
69
+ if ("autoStart" in config) throw new ServerError("server({ autoStart }) has been removed. The server is now started automatically by createApp.\n\nRun `npx appkit codemod on-plugins-ready --write` to auto-migrate.");
67
70
  this.config = config;
68
71
  this.serverApplication = express();
69
72
  this.server = null;
70
73
  this.serverExtensions = [];
74
+ }
75
+ attachContext(deps = {}) {
76
+ super.attachContext(deps);
71
77
  this.telemetry.registerInstrumentations([instrumentations.http, instrumentations.express]);
78
+ this.context?.registerAsRouteTarget(this);
72
79
  }
80
+ /** Setup the server plugin. */
73
81
  async setup() {}
74
82
  /** Get the server configuration. */
75
83
  getConfig() {
@@ -85,6 +93,7 @@ var ServerPlugin = class ServerPlugin extends Plugin {
85
93
  * @returns The express application.
86
94
  */
87
95
  async start() {
96
+ this.serverApplication.use(requestMetricsMiddleware);
88
97
  this.serverApplication.use(express.json({ type: (req) => {
89
98
  const urlPath = req.url?.split("?")[0];
90
99
  if (urlPath && this.rawBodyPaths.has(urlPath)) return false;
@@ -130,6 +139,15 @@ var ServerPlugin = class ServerPlugin extends Plugin {
130
139
  return this;
131
140
  }
132
141
  /**
142
+ * Register a server extension from another plugin during setup.
143
+ * Unlike extend(), this is designed for internal plugin-to-plugin
144
+ * coordination where extensions are registered before the server starts
145
+ * listening — typically called by PluginContext when flushing buffered routes.
146
+ */
147
+ addExtension(fn) {
148
+ this.serverExtensions.push(fn);
149
+ }
150
+ /**
133
151
  * Setup the routes with the plugins.
134
152
  *
135
153
  * This method goes through all the plugins and injects the routes into the server application.
@@ -139,7 +157,8 @@ var ServerPlugin = class ServerPlugin extends Plugin {
139
157
  async extendRoutes() {
140
158
  const endpoints = {};
141
159
  const pluginConfigs = {};
142
- if (!this.config.plugins) return {
160
+ const plugins = this.context?.getPlugins();
161
+ if (!plugins || plugins.size === 0) return {
143
162
  endpoints,
144
163
  pluginConfigs
145
164
  };
@@ -147,7 +166,7 @@ var ServerPlugin = class ServerPlugin extends Plugin {
147
166
  res.status(200).json({ status: "ok" });
148
167
  });
149
168
  this.registerEndpoint("health", "/health");
150
- for (const plugin of Object.values(this.config.plugins)) {
169
+ for (const plugin of plugins.values()) {
151
170
  if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;
152
171
  if (plugin?.injectRoutes && typeof plugin.injectRoutes === "function") {
153
172
  const router = express.Router();
@@ -245,8 +264,10 @@ var ServerPlugin = class ServerPlugin extends Plugin {
245
264
  logger.info("Starting graceful shutdown...");
246
265
  if (this.viteDevServer) await this.viteDevServer.close();
247
266
  if (this.remoteTunnelController) this.remoteTunnelController.cleanup();
248
- if (this.config.plugins) {
249
- for (const plugin of Object.values(this.config.plugins)) if (plugin.abortActiveOperations) try {
267
+ TelemetryReporter.getInstance()?.stop();
268
+ const shutdownPlugins = this.context?.getPlugins();
269
+ if (shutdownPlugins) {
270
+ for (const plugin of shutdownPlugins.values()) if (plugin.abortActiveOperations) try {
250
271
  plugin.abortActiveOperations();
251
272
  } catch (err) {
252
273
  logger.error("Error aborting operations for plugin %s: %O", plugin.name, err);
@@ -283,6 +304,19 @@ var ServerPlugin = class ServerPlugin extends Plugin {
283
304
  }
284
305
  };
285
306
  const EXCLUDED_PLUGINS = [ServerPlugin.manifest.name];
307
+ /** @internal Exported for unit tests. */
308
+ function requestMetricsMiddleware(req, res, next) {
309
+ const startMs = Date.now();
310
+ res.on("finish", () => {
311
+ const reporter = TelemetryReporter.getInstance();
312
+ if (!reporter) return;
313
+ const routePath = req.route?.path;
314
+ if (!routePath) return;
315
+ const template = `${req.baseUrl ?? ""}${routePath}`;
316
+ reporter.recordRequest(req.method, template, res.statusCode, Date.now() - startMs);
317
+ });
318
+ next();
319
+ }
286
320
  /**
287
321
  * @internal
288
322
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport getPort, { portNumbers } from \"get-port\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { ServerError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */\nconst devListenPortSpan = 100;\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * The server is started automatically by `createApp` after all plugins are set up\n * and the optional `onPluginsReady` callback has run.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"server\">;\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n /** Bound listen port after optional dev-time resolution. */\n private resolvedListenPort?: number;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n if (\"autoStart\" in config) {\n throw new ServerError(\n \"server({ autoStart }) has been removed. \" +\n \"The server is now started automatically by createApp.\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n }\n super(config);\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n }\n\n async setup() {}\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(\n express.json({\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints, pluginConfigs);\n\n const listenPort = await this.resolveListenPort();\n\n const server = this.serverApplication.listen(\n listenPort,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server has not started yet.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * Call this inside the `onPluginsReady` callback of `createApp` to register\n * custom Express routes or middleware before the server starts listening.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n */\n extend(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints,\n * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n if (!this.config.plugins) return { endpoints, pluginConfigs };\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of Object.values(this.config.plugins)) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n pluginConfigs,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n /**\n * In development, prefers {@link ServerConfig.port} / env / default (8000), then\n * scans upward using `get-port`'s `portNumbers()` on the listen host until one binds.\n * In non-development, uses config / env / default only (no fallback).\n */\n private async resolveListenPort(): Promise<number> {\n const requested = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n\n if (process.env.NODE_ENV !== \"development\") {\n this.resolvedListenPort = requested;\n return requested;\n }\n\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n const upper = Math.min(requested + devListenPortSpan - 1, 65_535);\n const port = await getPort({\n host,\n port: portNumbers(requested, upper),\n });\n this.resolvedListenPort = port;\n if (port !== requested) {\n logger.info(\"Port %d was busy, picking %d\", requested, port);\n }\n return port;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port =\n this.resolvedListenPort ??\n this.config.port ??\n ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n private async _gracefulShutdown() {\n logger.info(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n // 1. abort active operations from plugins\n if (this.config.plugins) {\n for (const plugin of Object.values(this.config.plugins)) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n logger.debug(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n logger.debug(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n /** @deprecated Server is now started automatically by createApp. */\n start() {\n throw new ServerError(\n \"server.start() has been removed. Use the onPluginsReady callback instead:\\n\\n\" +\n \" createApp({\\n\" +\n \" plugins: [server(), ...],\\n\" +\n \" onPluginsReady(appkit) {\\n\" +\n \" appkit.server.extend(...);\\n\" +\n \" },\\n\" +\n \" });\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n },\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport getPort, { portNumbers } from \"get-port\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { ServerError } from \"../../errors\";\nimport { TelemetryReporter } from \"../../internal-telemetry\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */\nconst devListenPortSpan = 100;\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * The server is started automatically by `createApp` after all plugins are set up\n * and the optional `onPluginsReady` callback has run.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"server\">;\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n /** Bound listen port after optional dev-time resolution. */\n private resolvedListenPort?: number;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n if (\"autoStart\" in config) {\n throw new ServerError(\n \"server({ autoStart }) has been removed. \" +\n \"The server is now started automatically by createApp.\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n }\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n }\n\n attachContext(deps: Parameters<Plugin[\"attachContext\"]>[0] = {}): void {\n super.attachContext(deps);\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n this.context?.registerAsRouteTarget(this);\n }\n\n /** Setup the server plugin. */\n async setup() {}\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(requestMetricsMiddleware);\n this.serverApplication.use(\n express.json({\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints, pluginConfigs);\n\n const listenPort = await this.resolveListenPort();\n\n const server = this.serverApplication.listen(\n listenPort,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server has not started yet.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * Call this inside the `onPluginsReady` callback of `createApp` to register\n * custom Express routes or middleware before the server starts listening.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n */\n extend(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Register a server extension from another plugin during setup.\n * Unlike extend(), this is designed for internal plugin-to-plugin\n * coordination where extensions are registered before the server starts\n * listening — typically called by PluginContext when flushing buffered routes.\n */\n addExtension(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints,\n * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n const plugins = this.context?.getPlugins();\n if (!plugins || plugins.size === 0) return { endpoints, pluginConfigs };\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of plugins.values()) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n pluginConfigs,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n /**\n * In development, prefers {@link ServerConfig.port} / env / default (8000), then\n * scans upward using `get-port`'s `portNumbers()` on the listen host until one binds.\n * In non-development, uses config / env / default only (no fallback).\n */\n private async resolveListenPort(): Promise<number> {\n const requested = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n\n if (process.env.NODE_ENV !== \"development\") {\n this.resolvedListenPort = requested;\n return requested;\n }\n\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n const upper = Math.min(requested + devListenPortSpan - 1, 65_535);\n const port = await getPort({\n host,\n port: portNumbers(requested, upper),\n });\n this.resolvedListenPort = port;\n if (port !== requested) {\n logger.info(\"Port %d was busy, picking %d\", requested, port);\n }\n return port;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port =\n this.resolvedListenPort ??\n this.config.port ??\n ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n private async _gracefulShutdown() {\n logger.info(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n TelemetryReporter.getInstance()?.stop();\n\n // 1. abort active operations from plugins\n const shutdownPlugins = this.context?.getPlugins();\n if (shutdownPlugins) {\n for (const plugin of shutdownPlugins.values()) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n logger.debug(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n logger.debug(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n /** @deprecated Server is now started automatically by createApp. */\n start() {\n throw new ServerError(\n \"server.start() has been removed. Use the onPluginsReady callback instead:\\n\\n\" +\n \" createApp({\\n\" +\n \" plugins: [server(), ...],\\n\" +\n \" onPluginsReady(appkit) {\\n\" +\n \" appkit.server.extend(...);\\n\" +\n \" },\\n\" +\n \" });\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n },\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/** @internal Exported for unit tests. */\nexport function requestMetricsMiddleware(\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const startMs = Date.now();\n res.on(\"finish\", () => {\n const reporter = TelemetryReporter.getInstance();\n if (!reporter) return;\n const routePath = (req.route as { path?: string } | undefined)?.path;\n if (!routePath) return;\n const baseUrl = req.baseUrl ?? \"\";\n const template = `${baseUrl}${routePath}`;\n reporter.recordRequest(\n req.method,\n template,\n res.statusCode,\n Date.now() - startMs,\n );\n });\n next();\n}\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
@@ -1,12 +1,9 @@
1
1
  import { BasePluginConfig } from "../../shared/src/plugin.js";
2
2
  import "../../shared/src/index.js";
3
- import { Plugin } from "../../plugin/plugin.js";
4
- import "../../plugin/index.js";
5
3
 
6
4
  //#region src/plugins/server/types.d.ts
7
5
  interface ServerConfig extends BasePluginConfig {
8
6
  port?: number;
9
- plugins?: Record<string, Plugin>;
10
7
  staticPath?: string;
11
8
  host?: string;
12
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/server/types.ts"],"mappings":";;;;;;UAGiB,YAAA,SAAqB,gBAAA;EACpC,IAAA;EACA,OAAA,GAAU,MAAA,SAAe,MAAA;EACzB,UAAA;EACA,IAAA;AAAA"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/server/types.ts"],"mappings":";;;;UAEiB,YAAA,SAAqB,gBAAA;EACpC,IAAA;EACA,UAAA;EACA,IAAA;AAAA"}
@@ -2,6 +2,7 @@ import { IAppRouter, ToPlugin } from "../../shared/src/plugin.js";
2
2
  import "../../shared/src/index.js";
3
3
  import { ExecutionResult } from "../../plugin/execution-result.js";
4
4
  import { Plugin } from "../../plugin/plugin.js";
5
+ import { NamedPluginFactory } from "../../plugin/to-plugin.js";
5
6
  import "../../plugin/index.js";
6
7
  import { PluginManifest, ResourceRequirement } from "../../registry/types.js";
7
8
  import "../../registry/index.js";
@@ -32,7 +33,7 @@ declare class ServingPlugin extends Plugin {
32
33
  /**
33
34
  * @internal
34
35
  */
35
- declare const serving: ToPlugin<typeof ServingPlugin, IServingConfig, "serving">;
36
+ declare const serving: ToPlugin<typeof ServingPlugin, IServingConfig, "serving"> & NamedPluginFactory<"serving">;
36
37
  //#endregion
37
38
  export { ServingPlugin, serving };
38
39
  //# sourceMappingURL=serving.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serving.d.ts","names":[],"sources":["../../../src/plugins/serving/serving.ts"],"mappings":";;;;;;;;;;;cAyCa,aAAA,SAAsB,MAAA;EAAA,OAC1B,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UAEC,MAAA,EAAQ,cAAA;EAAA,iBAET,SAAA;EAAA,iBACA,WAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,cAAA;EAed,KAAA,CAAA,GAAS,OAAA;EAAA,OAiBR,uBAAA,CACL,MAAA,EAAQ,cAAA,GACP,mBAAA;EAAA,QAqBK,gBAAA;EA0BR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAgEf,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA0BG,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA2DG,MAAA,CACJ,KAAA,UACA,IAAA,EAAM,MAAA,oBACL,OAAA,CAAQ,eAAA;EAiBX,YAAA,CAAA,GAAgB,MAAA;EAOV,QAAA,CAAA,GAAY,OAAA;EAAA,UAIR,iBAAA,CAAkB,KAAA,WAAgB,sBAAA;EAM5C,OAAA,CAAA,GAAW,cAAA;AAAA;;;;cAmBA,OAAA,EAAO,QAAA,QAAA,aAAA,EAAA,cAAA"}
1
+ {"version":3,"file":"serving.d.ts","names":[],"sources":["../../../src/plugins/serving/serving.ts"],"mappings":";;;;;;;;;;;;cAyCa,aAAA,SAAsB,MAAA;EAAA,OAC1B,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UAEC,MAAA,EAAQ,cAAA;EAAA,iBAET,SAAA;EAAA,iBACA,WAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,cAAA;EAed,KAAA,CAAA,GAAS,OAAA;EAAA,OAiBR,uBAAA,CACL,MAAA,EAAQ,cAAA,GACP,mBAAA;EAAA,QAqBK,gBAAA;EA0BR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAgEf,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA0BG,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA2DG,MAAA,CACJ,KAAA,UACA,IAAA,EAAM,MAAA,oBACL,OAAA,CAAQ,eAAA;EAiBX,YAAA,CAAA,GAAgB,MAAA;EAOV,QAAA,CAAA,GAAY,OAAA;EAAA,UAIR,iBAAA,CAAkB,KAAA,WAAgB,sBAAA;EAM5C,OAAA,CAAA,GAAW,cAAA;AAAA;;;;cAmBA,OAAA,EAAO,QAAA,QAAA,aAAA,EAAA,cAAA,eAAA,kBAAA"}
@@ -1,8 +1,39 @@
1
1
  import { JSONSchema7 } from "json-schema";
2
2
 
3
3
  //#region ../shared/src/agent.d.ts
4
+ /**
5
+ * Semantic hint for what the tool does to the world. Drives both the
6
+ * agents-plugin approval gate and the client's approval-card styling.
7
+ *
8
+ * - `read` — observes only; never needs approval.
9
+ * - `write` — creates or appends new state (e.g. saving a new view). Approval
10
+ * required by default. Rendered as a low-severity "writes" card.
11
+ * - `update` — mutates existing state in place (e.g. renaming, toggling).
12
+ * Approval required. Rendered as a medium-severity "updates" card.
13
+ * - `destructive` — deletes or irreversibly mutates (e.g. dropping a view).
14
+ * Approval required. Rendered as a high-severity "destructive" card.
15
+ *
16
+ * Prefer this over the legacy `readOnly`/`destructive` booleans: it lets the
17
+ * UI distinguish "captured a screenshot" from "deleted a dashboard", both of
18
+ * which today are lumped under a single red "destructive" label.
19
+ */
20
+ type ToolEffect = "read" | "write" | "update" | "destructive";
4
21
  interface ToolAnnotations {
22
+ /**
23
+ * Preferred semantic label. When set, drives both the approval gate (fires
24
+ * for `write`/`update`/`destructive`) and the approval-card styling.
25
+ */
26
+ effect?: ToolEffect;
27
+ /**
28
+ * @deprecated Prefer {@link effect}. Retained for backward compatibility
29
+ * with tools authored against the original flags and for MCP interop.
30
+ */
5
31
  readOnly?: boolean;
32
+ /**
33
+ * @deprecated Prefer {@link effect} with value `"destructive"`. Retained
34
+ * so existing annotations continue to force the approval gate, and so
35
+ * MCP-style consumers that only read `destructive` still see the hint.
36
+ */
6
37
  destructive?: boolean;
7
38
  idempotent?: boolean;
8
39
  requiresUserContext?: boolean;
@@ -13,6 +44,10 @@ interface AgentToolDefinition {
13
44
  parameters: JSONSchema7;
14
45
  annotations?: ToolAnnotations;
15
46
  }
47
+ interface ToolProvider {
48
+ getAgentTools(): AgentToolDefinition[];
49
+ executeAgentTool(name: string, args: unknown, signal?: AbortSignal): Promise<unknown>;
50
+ }
16
51
  interface Message {
17
52
  id: string;
18
53
  role: "user" | "assistant" | "system" | "tool";
@@ -26,6 +61,20 @@ interface ToolCall {
26
61
  name: string;
27
62
  args: unknown;
28
63
  }
64
+ interface Thread {
65
+ id: string;
66
+ userId: string;
67
+ messages: Message[];
68
+ createdAt: Date;
69
+ updatedAt: Date;
70
+ }
71
+ interface ThreadStore {
72
+ create(userId: string): Promise<Thread>;
73
+ get(threadId: string, userId: string): Promise<Thread | null>;
74
+ list(userId: string): Promise<Thread[]>;
75
+ addMessage(threadId: string, userId: string, message: Message): Promise<void>;
76
+ delete(threadId: string, userId: string): Promise<boolean>;
77
+ }
29
78
  type AgentEvent = {
30
79
  type: "message_delta";
31
80
  content: string;
@@ -52,6 +101,19 @@ type AgentEvent = {
52
101
  } | {
53
102
  type: "metadata";
54
103
  data: Record<string, unknown>;
104
+ } | {
105
+ /**
106
+ * Emitted by the agents plugin (not adapters) when a tool call annotated
107
+ * `destructive: true` is awaiting human approval. Clients should render
108
+ * an approval prompt and POST to `/chat/approve` with the matching
109
+ * `approvalId` and a `decision` of `approve` or `deny`.
110
+ */
111
+ type: "approval_pending";
112
+ approvalId: string;
113
+ streamId: string;
114
+ toolName: string;
115
+ args: unknown;
116
+ annotations?: ToolAnnotations;
55
117
  };
56
118
  interface AgentInput {
57
119
  messages: Message[];
@@ -68,5 +130,5 @@ interface AgentAdapter {
68
130
  run(input: AgentInput, context: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown>;
69
131
  }
70
132
  //#endregion
71
- export { AgentAdapter, AgentEvent, AgentInput, AgentRunContext, AgentToolDefinition, Message, ToolAnnotations, ToolCall };
133
+ export { AgentAdapter, AgentEvent, AgentInput, AgentRunContext, AgentToolDefinition, Message, Thread, ThreadStore, ToolAnnotations, ToolCall, ToolEffect, ToolProvider };
72
134
  //# sourceMappingURL=agent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","names":[],"sources":["../../../../shared/src/agent.ts"],"mappings":";;;UAMiB,eAAA;EACf,QAAA;EACA,WAAA;EACA,UAAA;EACA,mBAAA;AAAA;AAAA,UAGe,mBAAA;EACf,IAAA;EACA,WAAA;EACA,UAAA,EAAY,WAAA;EACZ,WAAA,GAAc,eAAA;AAAA;AAAA,UAgBC,OAAA;EACf,EAAA;EACA,IAAA;EACA,OAAA;EACA,UAAA;EACA,SAAA,GAAY,QAAA;EACZ,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,QAAA;EACf,EAAA;EACA,IAAA;EACA,IAAA;AAAA;AAAA,KA2BU,UAAA;EACN,IAAA;EAAuB,OAAA;AAAA;EACvB,IAAA;EAAiB,OAAA;AAAA;EACjB,IAAA;EAAmB,MAAA;EAAgB,IAAA;EAAc,IAAA;AAAA;EAEjD,IAAA;EACA,MAAA;EACA,MAAA;EACA,KAAA;AAAA;EAEA,IAAA;EAAkB,OAAA;AAAA;EAElB,IAAA;EACA,MAAA;EACA,KAAA;AAAA;EAEA,IAAA;EAAkB,IAAA,EAAM,MAAA;AAAA;AAAA,UA0Gb,UAAA;EACf,QAAA,EAAU,OAAA;EACV,KAAA,EAAO,mBAAA;EACP,QAAA;EACA,MAAA,GAAS,WAAA;AAAA;AAAA,UAGM,eAAA;;EAEf,WAAA,GAAc,IAAA,UAAc,IAAA,cAAkB,OAAA;EAC9C,MAAA,GAAS,WAAA;AAAA;AAAA,UAGM,YAAA;EACf,GAAA,CACE,KAAA,EAAO,UAAA,EACP,OAAA,EAAS,eAAA,GACR,cAAA,CAAe,UAAA;AAAA"}
1
+ {"version":3,"file":"agent.d.ts","names":[],"sources":["../../../../shared/src/agent.ts"],"mappings":";;;;;AAsBA;;;;;AAEA;;;;;;;;;KAFY,UAAA;AAAA,UAEK,eAAA;EAkBI;AAGrB;;;EAhBE,MAAA,GAAS,UAAA;EAiBT;;;;EAZA,QAAA;EAec;;;AAGhB;;EAZE,WAAA;EACA,UAAA;EACA,mBAAA;AAAA;AAAA,UAGe,mBAAA;EACf,IAAA;EACA,WAAA;EACA,UAAA,EAAY,WAAA;EACZ,WAAA,GAAc,eAAA;AAAA;AAAA,UAGC,YAAA;EACf,aAAA,IAAiB,mBAAA;EACjB,gBAAA,CACE,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;AAAA;AAAA,UAOY,OAAA;EACf,EAAA;EACA,IAAA;EACA,OAAA;EACA,UAAA;EACA,SAAA,GAAY,QAAA;EACZ,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,QAAA;EACf,EAAA;EACA,IAAA;EACA,IAAA;AAAA;AAAA,UAGe,MAAA;EACf,EAAA;EACA,MAAA;EACA,QAAA,EAAU,OAAA;EACV,SAAA,EAAW,IAAA;EACX,SAAA,EAAW,IAAA;AAAA;AAAA,UAOI,WAAA;EACf,MAAA,CAAO,MAAA,WAAiB,OAAA,CAAQ,MAAA;EAChC,GAAA,CAAI,QAAA,UAAkB,MAAA,WAAiB,OAAA,CAAQ,MAAA;EAC/C,IAAA,CAAK,MAAA,WAAiB,OAAA,CAAQ,MAAA;EAC9B,UAAA,CAAW,QAAA,UAAkB,MAAA,UAAgB,OAAA,EAAS,OAAA,GAAU,OAAA;EAChE,MAAA,CAAO,QAAA,UAAkB,MAAA,WAAiB,OAAA;AAAA;AAAA,KAOhC,UAAA;EACN,IAAA;EAAuB,OAAA;AAAA;EACvB,IAAA;EAAiB,OAAA;AAAA;EACjB,IAAA;EAAmB,MAAA;EAAgB,IAAA;EAAc,IAAA;AAAA;EAEjD,IAAA;EACA,MAAA;EACA,MAAA;EACA,KAAA;AAAA;EAEA,IAAA;EAAkB,OAAA;AAAA;EAElB,IAAA;EACA,MAAA;EACA,KAAA;AAAA;EAEA,IAAA;EAAkB,IAAA,EAAM,MAAA;AAAA;EAvBc;;;;;;EA+BtC,IAAA;EACA,UAAA;EACA,QAAA;EACA,QAAA;EACA,IAAA;EACA,WAAA,GAAc,eAAA;AAAA;AAAA,UA6HH,UAAA;EACf,QAAA,EAAU,OAAA;EACV,KAAA,EAAO,mBAAA;EACP,QAAA;EACA,MAAA,GAAS,WAAA;AAAA;AAAA,UAGM,eAAA;EAUa;EAR5B,WAAA,GAAc,IAAA,UAAc,IAAA,cAAkB,OAAA;EAC9C,MAAA,GAAS,WAAA;AAAA;AAAA,UAGM,YAAA;EACf,GAAA,CACE,KAAA,EAAO,UAAA,EACP,OAAA,EAAS,eAAA,GACR,cAAA,CAAe,UAAA;AAAA"}
@@ -1,4 +1,4 @@
1
- import { AgentAdapter, AgentEvent, AgentInput, AgentRunContext, AgentToolDefinition, Message, ToolAnnotations, ToolCall } from "./agent.js";
1
+ import { AgentAdapter, AgentEvent, AgentInput, AgentRunContext, AgentToolDefinition, Message, Thread, ThreadStore, ToolAnnotations, ToolCall, ToolEffect, ToolProvider } from "./agent.js";
2
2
  import { ResourceFieldEntry } from "./schemas/plugin-manifest.generated.js";
3
3
  import { BasePlugin, BasePluginConfig, HttpMethod, IAppRequest, IAppResponse, IAppRouter, PluginConstructor, PluginData, PluginEndpointMap, PluginExports, PluginManifest, PluginMap, PluginPhase, ResourceRequirement, RouteConfig, TelemetryOptions, ToPlugin, WithAsUser } from "./plugin.js";
4
4
  import { CacheConfig, CacheEntry, CacheStorage } from "./cache.js";