@databricks/appkit 0.63.0 → 0.65.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 (93) hide show
  1. package/CLAUDE.md +3 -2
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/cli/commands/doctor/resolve-targets.js +1 -1
  4. package/dist/cli/commands/plugin/create/scaffold.js +2 -2
  5. package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
  6. package/dist/core/agent/agent-dirs.js +14 -0
  7. package/dist/core/agent/agent-dirs.js.map +1 -0
  8. package/dist/core/agent/create-agent.d.ts +5 -7
  9. package/dist/core/agent/create-agent.d.ts.map +1 -1
  10. package/dist/core/agent/create-agent.js +28 -7
  11. package/dist/core/agent/create-agent.js.map +1 -1
  12. package/dist/core/agent/load-agents.d.ts.map +1 -1
  13. package/dist/core/agent/load-agents.js +4 -4
  14. package/dist/core/agent/load-agents.js.map +1 -1
  15. package/dist/core/agent/load-code-agents.js +115 -0
  16. package/dist/core/agent/load-code-agents.js.map +1 -0
  17. package/dist/core/agent/types.d.ts +20 -6
  18. package/dist/core/agent/types.d.ts.map +1 -1
  19. package/dist/core/agent/types.js.map +1 -1
  20. package/dist/core/appkit.js +1 -1
  21. package/dist/index.d.ts +12 -12
  22. package/dist/index.js +2 -2
  23. package/dist/plugin/plugin.d.ts +2 -2
  24. package/dist/plugins/agents/agents.d.ts +38 -7
  25. package/dist/plugins/agents/agents.d.ts.map +1 -1
  26. package/dist/plugins/agents/agents.js +147 -39
  27. package/dist/plugins/agents/agents.js.map +1 -1
  28. package/dist/plugins/agents/tool-approval-gate.js.map +1 -1
  29. package/dist/plugins/ai-search/ai-search.d.ts +1 -1
  30. package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
  31. package/dist/plugins/ai-search/ai-search.js +3 -1
  32. package/dist/plugins/ai-search/ai-search.js.map +1 -1
  33. package/dist/plugins/analytics/analytics.d.ts +2 -2
  34. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  35. package/dist/plugins/analytics/analytics.js +3 -1
  36. package/dist/plugins/analytics/analytics.js.map +1 -1
  37. package/dist/plugins/files/plugin.d.ts +5 -4
  38. package/dist/plugins/files/plugin.d.ts.map +1 -1
  39. package/dist/plugins/files/plugin.js +2 -1
  40. package/dist/plugins/files/plugin.js.map +1 -1
  41. package/dist/plugins/genie/genie.d.ts +2 -2
  42. package/dist/plugins/genie/genie.d.ts.map +1 -1
  43. package/dist/plugins/genie/genie.js +3 -1
  44. package/dist/plugins/genie/genie.js.map +1 -1
  45. package/dist/plugins/jobs/plugin.d.ts +3 -2
  46. package/dist/plugins/jobs/plugin.d.ts.map +1 -1
  47. package/dist/plugins/jobs/plugin.js +2 -1
  48. package/dist/plugins/jobs/plugin.js.map +1 -1
  49. package/dist/plugins/lakebase/lakebase.d.ts +4 -4
  50. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  51. package/dist/plugins/lakebase/lakebase.js +3 -1
  52. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  53. package/dist/plugins/server/index.d.ts +1 -1
  54. package/dist/plugins/server/index.d.ts.map +1 -1
  55. package/dist/plugins/server/index.js +3 -1
  56. package/dist/plugins/server/index.js.map +1 -1
  57. package/dist/plugins/serving/serving.d.ts +1 -0
  58. package/dist/plugins/serving/serving.d.ts.map +1 -1
  59. package/dist/plugins/serving/serving.js +2 -1
  60. package/dist/plugins/serving/serving.js.map +1 -1
  61. package/dist/plugins/ui-variants/index.js +3 -1
  62. package/dist/plugins/ui-variants/index.js.map +1 -1
  63. package/dist/registry/index.d.ts +1 -1
  64. package/dist/registry/index.js +1 -1
  65. package/dist/registry/manifest-loader.d.ts +20 -1
  66. package/dist/registry/manifest-loader.d.ts.map +1 -1
  67. package/dist/registry/manifest-loader.js +23 -1
  68. package/dist/registry/manifest-loader.js.map +1 -1
  69. package/dist/schemas/manifest.d.ts +1 -1
  70. package/dist/shared/src/agent.d.ts +1 -1
  71. package/dist/shared/src/index.d.ts +1 -1
  72. package/dist/shared/src/naming.js +13 -1
  73. package/dist/shared/src/naming.js.map +1 -1
  74. package/dist/shared/src/schemas/manifest.d.ts +1 -1
  75. package/dist/shared/src/schemas/manifest.js +463 -0
  76. package/dist/shared/src/schemas/manifest.js.map +1 -0
  77. package/dist/testing/fixtures.d.ts +1 -1
  78. package/dist/tsdown/index.d.ts +58 -0
  79. package/dist/tsdown/index.d.ts.map +1 -0
  80. package/dist/tsdown/index.js +71 -0
  81. package/dist/tsdown/index.js.map +1 -0
  82. package/docs/api/appkit/Function.createAgent.md +1 -3
  83. package/docs/api/appkit/Function.defineManifest.md +32 -0
  84. package/docs/api/appkit/Interface.AgentDefinition.md +12 -1
  85. package/docs/api/appkit/Interface.AgentsPluginConfig.md +6 -15
  86. package/docs/api/appkit/TypeAlias.AgentEvent.md +1 -1
  87. package/docs/api/appkit/Variable.agents.md +2 -2
  88. package/docs/api/appkit.md +47 -46
  89. package/docs/plugins/agents.md +85 -21
  90. package/docs/plugins/testing.md +2 -2
  91. package/llms.txt +3 -2
  92. package/package.json +5 -1
  93. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"serving.js","names":["manifest","servingConnector.stream","servingConnector.invoke"],"sources":["../../../src/plugins/serving/serving.ts"],"sourcesContent":["import path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\n\nimport type express from \"express\";\nimport type { IAppRouter } from \"shared\";\n\nimport * as servingConnector from \"../../connectors/serving/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { type ExecutionResult, Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport { servingInvokeDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { filterRequestBody, loadEndpointSchemas } from \"./schema-filter\";\nimport type {\n EndpointConfig,\n IServingConfig,\n ServingEndpointMethods,\n ServingFactory,\n} from \"./types\";\n\nconst logger = createLogger(\"serving\");\n\nclass EndpointNotFoundError extends Error {\n constructor(alias: string) {\n super(`Unknown endpoint alias: ${alias}`);\n }\n}\n\nclass EndpointNotConfiguredError extends Error {\n constructor(alias: string, envVar: string) {\n super(\n `Endpoint '${alias}' is not configured: env var '${envVar}' is not set`,\n );\n }\n}\n\ninterface ResolvedEndpoint {\n name: string;\n}\n\nexport class ServingPlugin extends Plugin {\n static manifest = manifest as PluginManifest<\"serving\">;\n\n protected static description =\n \"Authenticated proxy to Databricks Model Serving endpoints\";\n declare protected config: IServingConfig;\n\n private readonly endpoints: Record<string, EndpointConfig>;\n private readonly isNamedMode: boolean;\n private schemaAllowlists = new Map<string, Set<string>>();\n\n constructor(config: IServingConfig) {\n super(config);\n this.config = config;\n\n if (config.endpoints) {\n this.endpoints = config.endpoints;\n this.isNamedMode = true;\n } else {\n this.endpoints = {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n this.isNamedMode = false;\n }\n }\n\n async setup(): Promise<void> {\n const cacheFile = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-serving-types-cache.json\",\n );\n this.schemaAllowlists = await loadEndpointSchemas(cacheFile);\n if (this.schemaAllowlists.size > 0) {\n logger.debug(\n \"Loaded schema allowlists for %d endpoint(s)\",\n this.schemaAllowlists.size,\n );\n }\n }\n\n static getResourceRequirements(\n config: IServingConfig,\n ): ResourceRequirement[] {\n const endpoints = config.endpoints ?? {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n\n return Object.entries(endpoints).map(([alias, endpointConfig]) => ({\n type: ResourceType.SERVING_ENDPOINT,\n alias: `serving-${alias}`,\n resourceKey: `serving-${alias}`,\n description: `Model Serving endpoint for \"${alias}\" inference`,\n permission: \"CAN_QUERY\" as const,\n fields: {\n name: {\n env: endpointConfig.env,\n description: `Serving endpoint name for \"${alias}\"`,\n },\n },\n required: true,\n }));\n }\n\n private resolveAndFilter(\n alias: string,\n body: Record<string, unknown>,\n ): { endpoint: ResolvedEndpoint; filteredBody: Record<string, unknown> } {\n const config = this.endpoints[alias];\n if (!config) {\n throw new EndpointNotFoundError(alias);\n }\n\n const name = process.env[config.env];\n if (!name) {\n throw new EndpointNotConfiguredError(alias, config.env);\n }\n\n const endpoint: ResolvedEndpoint = { name };\n const filteredBody = filterRequestBody(\n body,\n this.schemaAllowlists,\n alias,\n this.config.filterMode,\n );\n return { endpoint, filteredBody };\n }\n\n // All serving routes use OBO (On-Behalf-Of) by default, consistent with the\n // Genie and Files plugins. This ensures per-user CAN_QUERY permissions are enforced.\n injectRoutes(router: IAppRouter) {\n if (this.isNamedMode) {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleStream(req, res);\n },\n });\n } else {\n // Unnamed mode: register both /invoke and /:alias/invoke patterns.\n // The type generator creates a \"default\" alias, so clients may use either URL.\n const invokeHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleInvoke(req, res);\n };\n const streamHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleStream(req, res);\n };\n\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"invoke-named\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/stream\",\n handler: streamHandler,\n });\n this.route(router, {\n name: \"stream-named\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: streamHandler,\n });\n }\n }\n\n async _handleInvoke(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n try {\n const result = await this.invoke(alias, rawBody);\n if (!result.ok) {\n res.status(result.status).json({ error: result.message });\n return;\n }\n res.json(result.data);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invocation failed\";\n if (err instanceof EndpointNotFoundError) {\n res.status(404).json({ error: message });\n } else if (\n err instanceof EndpointNotConfiguredError ||\n message.startsWith(\"Unknown request parameters:\")\n ) {\n res.status(400).json({ error: message });\n } else {\n res.status(502).json({ error: message });\n }\n }\n }\n\n async _handleStream(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n let endpoint: ResolvedEndpoint;\n let filteredBody: Record<string, unknown>;\n try {\n ({ endpoint, filteredBody } = this.resolveAndFilter(alias, rawBody));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid request\";\n const status = err instanceof EndpointNotFoundError ? 404 : 400;\n res.status(status).json({ error: message });\n return;\n }\n\n const timeout = this.config.timeout ?? 120_000;\n const workspaceClient = getWorkspaceClient();\n\n // Pipe raw SSE bytes from the upstream endpoint directly to the client.\n // No parsing/re-serialization — the upstream response is already valid SSE.\n let rawStream: ReadableStream<Uint8Array>;\n try {\n rawStream = await servingConnector.stream(\n workspaceClient,\n endpoint.name,\n filteredBody,\n );\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"Streaming request failed\";\n res.status(502).json({ error: message });\n return;\n }\n\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n res.flushHeaders();\n\n const nodeStream = Readable.fromWeb(\n rawStream as import(\"stream/web\").ReadableStream,\n );\n const abortController = new AbortController();\n const timeoutId = setTimeout(() => abortController.abort(), timeout);\n\n req.on(\"close\", () => abortController.abort());\n\n try {\n await pipeline(nodeStream, res, { signal: abortController.signal });\n } catch (err) {\n // AbortError is expected on client disconnect or timeout\n if (err instanceof Error && err.name !== \"AbortError\") {\n logger.warn(\"Stream pipe error: %s\", err.message);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async invoke(\n alias: string,\n body: Record<string, unknown>,\n ): Promise<ExecutionResult<unknown>> {\n const { endpoint, filteredBody } = this.resolveAndFilter(alias, body);\n const workspaceClient = getWorkspaceClient();\n const timeout = this.config.timeout ?? 120_000;\n\n return this.execute(\n () =>\n servingConnector.invoke(workspaceClient, endpoint.name, filteredBody),\n {\n default: {\n ...servingInvokeDefaults,\n timeout,\n },\n },\n );\n }\n\n clientConfig(): Record<string, unknown> {\n return {\n isNamedMode: this.isNamedMode,\n aliases: Object.keys(this.endpoints),\n };\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n protected createEndpointAPI(alias: string): ServingEndpointMethods {\n return {\n invoke: (body: Record<string, unknown>) => this.invoke(alias, body),\n };\n }\n\n exports(): ServingFactory {\n const resolveEndpoint = (alias?: string) => {\n const resolved = alias ?? \"default\";\n const spApi = this.createEndpointAPI(resolved);\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const userPlugin = this.asUser(req) as ServingPlugin;\n return userPlugin.createEndpointAPI(resolved);\n },\n };\n };\n return resolveEndpoint as ServingFactory;\n }\n}\n\n/**\n * @internal\n */\nexport const serving = toPlugin(ServingPlugin);\n"],"mappings":";;;;;;;;;;;;;;;;;;AAuBA,MAAM,SAAS,aAAa,UAAU;AAEtC,IAAM,wBAAN,cAAoC,MAAM;CACxC,YAAY,OAAe;AACzB,QAAM,2BAA2B,QAAQ;;;AAI7C,IAAM,6BAAN,cAAyC,MAAM;CAC7C,YAAY,OAAe,QAAgB;AACzC,QACE,aAAa,MAAM,gCAAgC,OAAO,cAC3D;;;AAQL,IAAa,gBAAb,cAAmC,OAAO;CACxC,OAAO,WAAWA;CAElB,OAAiB,cACf;CAGF,AAAiB;CACjB,AAAiB;CACjB,AAAQ,mCAAmB,IAAI,KAA0B;CAEzD,YAAY,QAAwB;AAClC,QAAM,OAAO;AACb,OAAK,SAAS;AAEd,MAAI,OAAO,WAAW;AACpB,QAAK,YAAY,OAAO;AACxB,QAAK,cAAc;SACd;AACL,QAAK,YAAY,EACf,SAAS,EAAE,KAAK,oCAAoC,EACrD;AACD,QAAK,cAAc;;;CAIvB,MAAM,QAAuB;AAQ3B,OAAK,mBAAmB,MAAM,oBAPZ,KAAK,KACrB,QAAQ,KAAK,EACb,gBACA,eACA,UACA,mCACD,CAC2D;AAC5D,MAAI,KAAK,iBAAiB,OAAO,EAC/B,QAAO,MACL,+CACA,KAAK,iBAAiB,KACvB;;CAIL,OAAO,wBACL,QACuB;EACvB,MAAM,YAAY,OAAO,aAAa,EACpC,SAAS,EAAE,KAAK,oCAAoC,EACrD;AAED,SAAO,OAAO,QAAQ,UAAU,CAAC,KAAK,CAAC,OAAO,qBAAqB;GACjE,MAAM,aAAa;GACnB,OAAO,WAAW;GAClB,aAAa,WAAW;GACxB,aAAa,+BAA+B,MAAM;GAClD,YAAY;GACZ,QAAQ,EACN,MAAM;IACJ,KAAK,eAAe;IACpB,aAAa,8BAA8B,MAAM;IAClD,EACF;GACD,UAAU;GACX,EAAE;;CAGL,AAAQ,iBACN,OACA,MACuE;EACvE,MAAM,SAAS,KAAK,UAAU;AAC9B,MAAI,CAAC,OACH,OAAM,IAAI,sBAAsB,MAAM;EAGxC,MAAM,OAAO,QAAQ,IAAI,OAAO;AAChC,MAAI,CAAC,KACH,OAAM,IAAI,2BAA2B,OAAO,OAAO,IAAI;AAUzD,SAAO;GAAE,UAP0B,EAAE,MAAM;GAOxB,cANE,kBACnB,MACA,KAAK,kBACL,OACA,KAAK,OAAO,WACb;GACgC;;CAKnC,aAAa,QAAoB;AAC/B,MAAI,KAAK,aAAa;AACpB,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS,OAAO,KAAsB,QAA0B;AAC9D,WAAM,KAAK,OAAO,IAAI,CAAC,cAAc,KAAK,IAAI;;IAEjD,CAAC;AAEF,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS,OAAO,KAAsB,QAA0B;AAC9D,WAAM,KAAK,OAAO,IAAI,CAAC,cAAc,KAAK,IAAI;;IAEjD,CAAC;SACG;GAGL,MAAM,gBAAgB,OACpB,KACA,QACG;AACH,QAAI,OAAO,UAAU;AACrB,UAAM,KAAK,OAAO,IAAI,CAAC,cAAc,KAAK,IAAI;;GAEhD,MAAM,gBAAgB,OACpB,KACA,QACG;AACH,QAAI,OAAO,UAAU;AACrB,UAAM,KAAK,OAAO,IAAI,CAAC,cAAc,KAAK,IAAI;;AAGhD,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS;IACV,CAAC;AACF,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS;IACV,CAAC;AACF,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS;IACV,CAAC;AACF,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS;IACV,CAAC;;;CAIN,MAAM,cACJ,KACA,KACe;EACf,MAAM,EAAE,UAAU,IAAI;EACtB,MAAM,UAAU,IAAI;AAEpB,MAAI;GACF,MAAM,SAAS,MAAM,KAAK,OAAO,OAAO,QAAQ;AAChD,OAAI,CAAC,OAAO,IAAI;AACd,QAAI,OAAO,OAAO,OAAO,CAAC,KAAK,EAAE,OAAO,OAAO,SAAS,CAAC;AACzD;;AAEF,OAAI,KAAK,OAAO,KAAK;WACd,KAAK;GACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,OAAI,eAAe,sBACjB,KAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC;YAExC,eAAe,8BACf,QAAQ,WAAW,8BAA8B,CAEjD,KAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC;OAExC,KAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC;;;CAK9C,MAAM,cACJ,KACA,KACe;EACf,MAAM,EAAE,UAAU,IAAI;EACtB,MAAM,UAAU,IAAI;EAEpB,IAAI;EACJ,IAAI;AACJ,MAAI;AACF,IAAC,CAAE,UAAU,gBAAiB,KAAK,iBAAiB,OAAO,QAAQ;WAC5D,KAAK;GACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;GACrD,MAAM,SAAS,eAAe,wBAAwB,MAAM;AAC5D,OAAI,OAAO,OAAO,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC;AAC3C;;EAGF,MAAM,UAAU,KAAK,OAAO,WAAW;EACvC,MAAM,kBAAkB,oBAAoB;EAI5C,IAAI;AACJ,MAAI;AACF,eAAY,MAAMC,OAChB,iBACA,SAAS,MACT,aACD;WACM,KAAK;GACZ,MAAM,UACJ,eAAe,QAAQ,IAAI,UAAU;AACvC,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC;AACxC;;AAGF,MAAI,UAAU,gBAAgB,mCAAmC;AACjE,MAAI,UAAU,iBAAiB,yBAAyB;AACxD,MAAI,UAAU,qBAAqB,KAAK;AACxC,MAAI,cAAc;EAElB,MAAM,aAAa,SAAS,QAC1B,UACD;EACD,MAAM,kBAAkB,IAAI,iBAAiB;EAC7C,MAAM,YAAY,iBAAiB,gBAAgB,OAAO,EAAE,QAAQ;AAEpE,MAAI,GAAG,eAAe,gBAAgB,OAAO,CAAC;AAE9C,MAAI;AACF,SAAM,SAAS,YAAY,KAAK,EAAE,QAAQ,gBAAgB,QAAQ,CAAC;WAC5D,KAAK;AAEZ,OAAI,eAAe,SAAS,IAAI,SAAS,aACvC,QAAO,KAAK,yBAAyB,IAAI,QAAQ;YAE3C;AACR,gBAAa,UAAU;;;CAI3B,MAAM,OACJ,OACA,MACmC;EACnC,MAAM,EAAE,UAAU,iBAAiB,KAAK,iBAAiB,OAAO,KAAK;EACrE,MAAM,kBAAkB,oBAAoB;EAC5C,MAAM,UAAU,KAAK,OAAO,WAAW;AAEvC,SAAO,KAAK,cAERC,OAAwB,iBAAiB,SAAS,MAAM,aAAa,EACvE,EACE,SAAS;GACP,GAAG;GACH;GACD,EACF,CACF;;CAGH,eAAwC;AACtC,SAAO;GACL,aAAa,KAAK;GAClB,SAAS,OAAO,KAAK,KAAK,UAAU;GACrC;;CAGH,MAAM,WAA0B;AAC9B,OAAK,cAAc,UAAU;;CAG/B,AAAU,kBAAkB,OAAuC;AACjE,SAAO,EACL,SAAS,SAAkC,KAAK,OAAO,OAAO,KAAK,EACpE;;CAGH,UAA0B;EACxB,MAAM,mBAAmB,UAAmB;GAC1C,MAAM,WAAW,SAAS;AAE1B,UAAO;IACL,GAFY,KAAK,kBAAkB,SAAS;IAG5C,SAAS,QAAyB;AAEhC,YADmB,KAAK,OAAO,IAAI,CACjB,kBAAkB,SAAS;;IAEhD;;AAEH,SAAO;;;;;;AAOX,MAAa,UAAU,SAAS,cAAc"}
1
+ {"version":3,"file":"serving.js","names":["manifest","servingConnector.stream","servingConnector.invoke"],"sources":["../../../src/plugins/serving/serving.ts"],"sourcesContent":["import path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\n\nimport type express from \"express\";\nimport type { IAppRouter } from \"shared\";\n\nimport * as servingConnector from \"../../connectors/serving/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { type ExecutionResult, Plugin, toPlugin } from \"../../plugin\";\nimport type { ResourceRequirement } from \"../../registry\";\nimport { defineManifest, ResourceType } from \"../../registry\";\nimport { servingInvokeDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { filterRequestBody, loadEndpointSchemas } from \"./schema-filter\";\nimport type {\n EndpointConfig,\n IServingConfig,\n ServingEndpointMethods,\n ServingFactory,\n} from \"./types\";\n\nconst logger = createLogger(\"serving\");\n\nclass EndpointNotFoundError extends Error {\n constructor(alias: string) {\n super(`Unknown endpoint alias: ${alias}`);\n }\n}\n\nclass EndpointNotConfiguredError extends Error {\n constructor(alias: string, envVar: string) {\n super(\n `Endpoint '${alias}' is not configured: env var '${envVar}' is not set`,\n );\n }\n}\n\ninterface ResolvedEndpoint {\n name: string;\n}\n\nexport class ServingPlugin extends Plugin {\n static manifest = defineManifest<\"serving\">(manifest);\n\n protected static description =\n \"Authenticated proxy to Databricks Model Serving endpoints\";\n declare protected config: IServingConfig;\n\n private readonly endpoints: Record<string, EndpointConfig>;\n private readonly isNamedMode: boolean;\n private schemaAllowlists = new Map<string, Set<string>>();\n\n constructor(config: IServingConfig) {\n super(config);\n this.config = config;\n\n if (config.endpoints) {\n this.endpoints = config.endpoints;\n this.isNamedMode = true;\n } else {\n this.endpoints = {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n this.isNamedMode = false;\n }\n }\n\n async setup(): Promise<void> {\n const cacheFile = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-serving-types-cache.json\",\n );\n this.schemaAllowlists = await loadEndpointSchemas(cacheFile);\n if (this.schemaAllowlists.size > 0) {\n logger.debug(\n \"Loaded schema allowlists for %d endpoint(s)\",\n this.schemaAllowlists.size,\n );\n }\n }\n\n static getResourceRequirements(\n config: IServingConfig,\n ): ResourceRequirement[] {\n const endpoints = config.endpoints ?? {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n\n return Object.entries(endpoints).map(([alias, endpointConfig]) => ({\n type: ResourceType.SERVING_ENDPOINT,\n alias: `serving-${alias}`,\n resourceKey: `serving-${alias}`,\n description: `Model Serving endpoint for \"${alias}\" inference`,\n permission: \"CAN_QUERY\" as const,\n fields: {\n name: {\n env: endpointConfig.env,\n description: `Serving endpoint name for \"${alias}\"`,\n },\n },\n required: true,\n }));\n }\n\n private resolveAndFilter(\n alias: string,\n body: Record<string, unknown>,\n ): { endpoint: ResolvedEndpoint; filteredBody: Record<string, unknown> } {\n const config = this.endpoints[alias];\n if (!config) {\n throw new EndpointNotFoundError(alias);\n }\n\n const name = process.env[config.env];\n if (!name) {\n throw new EndpointNotConfiguredError(alias, config.env);\n }\n\n const endpoint: ResolvedEndpoint = { name };\n const filteredBody = filterRequestBody(\n body,\n this.schemaAllowlists,\n alias,\n this.config.filterMode,\n );\n return { endpoint, filteredBody };\n }\n\n // All serving routes use OBO (On-Behalf-Of) by default, consistent with the\n // Genie and Files plugins. This ensures per-user CAN_QUERY permissions are enforced.\n injectRoutes(router: IAppRouter) {\n if (this.isNamedMode) {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleStream(req, res);\n },\n });\n } else {\n // Unnamed mode: register both /invoke and /:alias/invoke patterns.\n // The type generator creates a \"default\" alias, so clients may use either URL.\n const invokeHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleInvoke(req, res);\n };\n const streamHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleStream(req, res);\n };\n\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"invoke-named\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/stream\",\n handler: streamHandler,\n });\n this.route(router, {\n name: \"stream-named\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: streamHandler,\n });\n }\n }\n\n async _handleInvoke(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n try {\n const result = await this.invoke(alias, rawBody);\n if (!result.ok) {\n res.status(result.status).json({ error: result.message });\n return;\n }\n res.json(result.data);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invocation failed\";\n if (err instanceof EndpointNotFoundError) {\n res.status(404).json({ error: message });\n } else if (\n err instanceof EndpointNotConfiguredError ||\n message.startsWith(\"Unknown request parameters:\")\n ) {\n res.status(400).json({ error: message });\n } else {\n res.status(502).json({ error: message });\n }\n }\n }\n\n async _handleStream(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n let endpoint: ResolvedEndpoint;\n let filteredBody: Record<string, unknown>;\n try {\n ({ endpoint, filteredBody } = this.resolveAndFilter(alias, rawBody));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid request\";\n const status = err instanceof EndpointNotFoundError ? 404 : 400;\n res.status(status).json({ error: message });\n return;\n }\n\n const timeout = this.config.timeout ?? 120_000;\n const workspaceClient = getWorkspaceClient();\n\n // Pipe raw SSE bytes from the upstream endpoint directly to the client.\n // No parsing/re-serialization — the upstream response is already valid SSE.\n let rawStream: ReadableStream<Uint8Array>;\n try {\n rawStream = await servingConnector.stream(\n workspaceClient,\n endpoint.name,\n filteredBody,\n );\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"Streaming request failed\";\n res.status(502).json({ error: message });\n return;\n }\n\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n res.flushHeaders();\n\n const nodeStream = Readable.fromWeb(\n rawStream as import(\"stream/web\").ReadableStream,\n );\n const abortController = new AbortController();\n const timeoutId = setTimeout(() => abortController.abort(), timeout);\n\n req.on(\"close\", () => abortController.abort());\n\n try {\n await pipeline(nodeStream, res, { signal: abortController.signal });\n } catch (err) {\n // AbortError is expected on client disconnect or timeout\n if (err instanceof Error && err.name !== \"AbortError\") {\n logger.warn(\"Stream pipe error: %s\", err.message);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async invoke(\n alias: string,\n body: Record<string, unknown>,\n ): Promise<ExecutionResult<unknown>> {\n const { endpoint, filteredBody } = this.resolveAndFilter(alias, body);\n const workspaceClient = getWorkspaceClient();\n const timeout = this.config.timeout ?? 120_000;\n\n return this.execute(\n () =>\n servingConnector.invoke(workspaceClient, endpoint.name, filteredBody),\n {\n default: {\n ...servingInvokeDefaults,\n timeout,\n },\n },\n );\n }\n\n clientConfig(): Record<string, unknown> {\n return {\n isNamedMode: this.isNamedMode,\n aliases: Object.keys(this.endpoints),\n };\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n protected createEndpointAPI(alias: string): ServingEndpointMethods {\n return {\n invoke: (body: Record<string, unknown>) => this.invoke(alias, body),\n };\n }\n\n exports(): ServingFactory {\n const resolveEndpoint = (alias?: string) => {\n const resolved = alias ?? \"default\";\n const spApi = this.createEndpointAPI(resolved);\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const userPlugin = this.asUser(req) as ServingPlugin;\n return userPlugin.createEndpointAPI(resolved);\n },\n };\n };\n return resolveEndpoint as ServingFactory;\n }\n}\n\n/**\n * @internal\n */\nexport const serving = toPlugin(ServingPlugin);\n"],"mappings":"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"}
@@ -2,6 +2,8 @@ import { createLogger } from "../../logging/logger.js";
2
2
  import { Plugin } from "../../plugin/plugin.js";
3
3
  import { toPlugin } from "../../plugin/to-plugin.js";
4
4
  import "../../plugin/index.js";
5
+ import { defineManifest } from "../../registry/manifest-loader.js";
6
+ import "../../registry/index.js";
5
7
  import { FileChoiceStore } from "./choice-sink.js";
6
8
  import manifest_default from "./manifest.js";
7
9
 
@@ -17,7 +19,7 @@ const logger = createLogger("ui-variants");
17
19
  * per `<Variants>` id — and the plugin only records; it never edits source.
18
20
  */
19
21
  var UiVariantsPlugin = class extends Plugin {
20
- static manifest = manifest_default;
22
+ static manifest = defineManifest(manifest_default);
21
23
  static description = "Dev-only recorder for the <Variants> UI picker";
22
24
  store;
23
25
  constructor(config = {}) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/ui-variants/index.ts"],"sourcesContent":["import type { BasePluginConfig, IAppRouter } from \"shared\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { FileChoiceStore, type UiChoiceRecord } from \"./choice-sink\";\nimport manifest from \"./manifest.json\";\n\nconst logger = createLogger(\"ui-variants\");\n\n/** Untrusted body of `POST /api/ui-variants/confirm`; validated in the handler. */\ninterface ConfirmRequestBody {\n blockId?: unknown;\n chosenIndex?: unknown;\n label?: unknown;\n note?: unknown;\n}\n\n/**\n * Dev-only recorder backing the `<Variants>` UI picker.\n *\n * When a developer confirms a variant in the browser, the `<Variants>`\n * component POSTs to `/api/ui-variants/confirm`. This plugin records the choice\n * into a keyed JSONL file, which a coding agent reads to finalize the chosen\n * variant into the component source. Recording is a keyed upsert — one entry\n * per `<Variants>` id — and the plugin only records; it never edits source.\n */\nclass UiVariantsPlugin extends Plugin<BasePluginConfig> {\n static manifest = manifest as PluginManifest<\"uiVariants\">;\n\n protected static description =\n \"Dev-only recorder for the <Variants> UI picker\";\n\n private readonly store: FileChoiceStore;\n\n constructor(config: BasePluginConfig = {} as BasePluginConfig) {\n super(config);\n this.store = new FileChoiceStore();\n }\n\n injectRoutes(router: IAppRouter): void {\n this.route(router, {\n name: \"confirm\",\n method: \"post\",\n path: \"/confirm\",\n handler: async (req, res) => {\n // Dev-only: the recorder exists solely to drive the local edit loop,\n // so it must never run in a deployed app.\n if (process.env.NODE_ENV !== \"development\") {\n res.status(403).json({\n ok: false,\n message: \"ui-variants confirm is only available in development\",\n });\n return;\n }\n\n const body = (req.body ?? {}) as ConfirmRequestBody;\n const { blockId, chosenIndex } = body;\n\n if (typeof blockId !== \"string\" || blockId.length === 0) {\n res.status(400).json({\n ok: false,\n message: \"`blockId` (non-empty string) is required\",\n });\n return;\n }\n if (typeof chosenIndex !== \"number\" || !Number.isInteger(chosenIndex)) {\n res.status(400).json({\n ok: false,\n message: \"`chosenIndex` (integer) is required\",\n });\n return;\n }\n\n const record: UiChoiceRecord = {\n ts: new Date().toISOString(),\n blockId,\n chosenIndex,\n ...(typeof body.label === \"string\" ? { label: body.label } : {}),\n ...(typeof body.note === \"string\" ? { note: body.note } : {}),\n };\n\n try {\n await this.store.record(record);\n } catch (error) {\n logger.error(\"Failed to record UI variant choice\", {\n error,\n blockId,\n });\n res\n .status(500)\n .json({ ok: false, message: \"Failed to record variant choice\" });\n return;\n }\n\n logger.info(\"Recorded UI variant choice\", {\n blockId,\n chosenIndex,\n label: record.label,\n });\n res.json({ ok: true, blockId, chosenIndex });\n },\n });\n }\n}\n\nexport const uiVariants = toPlugin(UiVariantsPlugin);\n"],"mappings":";;;;;;;;AAQA,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;AAmB1C,IAAM,mBAAN,cAA+B,OAAyB;CACtD,OAAO,WAAWA;CAElB,OAAiB,cACf;CAEF,AAAiB;CAEjB,YAAY,SAA2B,EAAE,EAAsB;AAC7D,QAAM,OAAO;AACb,OAAK,QAAQ,IAAI,iBAAiB;;CAGpC,aAAa,QAA0B;AACrC,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAK,QAAQ;AAG3B,QAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;IAGF,MAAM,OAAQ,IAAI,QAAQ,EAAE;IAC5B,MAAM,EAAE,SAAS,gBAAgB;AAEjC,QAAI,OAAO,YAAY,YAAY,QAAQ,WAAW,GAAG;AACvD,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;AAEF,QAAI,OAAO,gBAAgB,YAAY,CAAC,OAAO,UAAU,YAAY,EAAE;AACrE,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;IAGF,MAAM,SAAyB;KAC7B,qBAAI,IAAI,MAAM,EAAC,aAAa;KAC5B;KACA;KACA,GAAI,OAAO,KAAK,UAAU,WAAW,EAAE,OAAO,KAAK,OAAO,GAAG,EAAE;KAC/D,GAAI,OAAO,KAAK,SAAS,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,EAAE;KAC7D;AAED,QAAI;AACF,WAAM,KAAK,MAAM,OAAO,OAAO;aACxB,OAAO;AACd,YAAO,MAAM,sCAAsC;MACjD;MACA;MACD,CAAC;AACF,SACG,OAAO,IAAI,CACX,KAAK;MAAE,IAAI;MAAO,SAAS;MAAmC,CAAC;AAClE;;AAGF,WAAO,KAAK,8BAA8B;KACxC;KACA;KACA,OAAO,OAAO;KACf,CAAC;AACF,QAAI,KAAK;KAAE,IAAI;KAAM;KAAS;KAAa,CAAC;;GAE/C,CAAC;;;AAIN,MAAa,aAAa,SAAS,iBAAiB"}
1
+ {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/ui-variants/index.ts"],"sourcesContent":["import type { BasePluginConfig, IAppRouter } from \"shared\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { defineManifest } from \"../../registry\";\nimport { FileChoiceStore, type UiChoiceRecord } from \"./choice-sink\";\nimport manifest from \"./manifest.json\";\n\nconst logger = createLogger(\"ui-variants\");\n\n/** Untrusted body of `POST /api/ui-variants/confirm`; validated in the handler. */\ninterface ConfirmRequestBody {\n blockId?: unknown;\n chosenIndex?: unknown;\n label?: unknown;\n note?: unknown;\n}\n\n/**\n * Dev-only recorder backing the `<Variants>` UI picker.\n *\n * When a developer confirms a variant in the browser, the `<Variants>`\n * component POSTs to `/api/ui-variants/confirm`. This plugin records the choice\n * into a keyed JSONL file, which a coding agent reads to finalize the chosen\n * variant into the component source. Recording is a keyed upsert — one entry\n * per `<Variants>` id — and the plugin only records; it never edits source.\n */\nclass UiVariantsPlugin extends Plugin<BasePluginConfig> {\n static manifest = defineManifest<\"uiVariants\">(manifest);\n\n protected static description =\n \"Dev-only recorder for the <Variants> UI picker\";\n\n private readonly store: FileChoiceStore;\n\n constructor(config: BasePluginConfig = {} as BasePluginConfig) {\n super(config);\n this.store = new FileChoiceStore();\n }\n\n injectRoutes(router: IAppRouter): void {\n this.route(router, {\n name: \"confirm\",\n method: \"post\",\n path: \"/confirm\",\n handler: async (req, res) => {\n // Dev-only: the recorder exists solely to drive the local edit loop,\n // so it must never run in a deployed app.\n if (process.env.NODE_ENV !== \"development\") {\n res.status(403).json({\n ok: false,\n message: \"ui-variants confirm is only available in development\",\n });\n return;\n }\n\n const body = (req.body ?? {}) as ConfirmRequestBody;\n const { blockId, chosenIndex } = body;\n\n if (typeof blockId !== \"string\" || blockId.length === 0) {\n res.status(400).json({\n ok: false,\n message: \"`blockId` (non-empty string) is required\",\n });\n return;\n }\n if (typeof chosenIndex !== \"number\" || !Number.isInteger(chosenIndex)) {\n res.status(400).json({\n ok: false,\n message: \"`chosenIndex` (integer) is required\",\n });\n return;\n }\n\n const record: UiChoiceRecord = {\n ts: new Date().toISOString(),\n blockId,\n chosenIndex,\n ...(typeof body.label === \"string\" ? { label: body.label } : {}),\n ...(typeof body.note === \"string\" ? { note: body.note } : {}),\n };\n\n try {\n await this.store.record(record);\n } catch (error) {\n logger.error(\"Failed to record UI variant choice\", {\n error,\n blockId,\n });\n res\n .status(500)\n .json({ ok: false, message: \"Failed to record variant choice\" });\n return;\n }\n\n logger.info(\"Recorded UI variant choice\", {\n blockId,\n chosenIndex,\n label: record.label,\n });\n res.json({ ok: true, blockId, chosenIndex });\n },\n });\n }\n}\n\nexport const uiVariants = toPlugin(UiVariantsPlugin);\n"],"mappings":";;;;;;;;;;AAQA,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;AAmB1C,IAAM,mBAAN,cAA+B,OAAyB;CACtD,OAAO,WAAW,eAA6BA,iBAAS;CAExD,OAAiB,cACf;CAEF,AAAiB;CAEjB,YAAY,SAA2B,EAAE,EAAsB;AAC7D,QAAM,OAAO;AACb,OAAK,QAAQ,IAAI,iBAAiB;;CAGpC,aAAa,QAA0B;AACrC,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAK,QAAQ;AAG3B,QAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;IAGF,MAAM,OAAQ,IAAI,QAAQ,EAAE;IAC5B,MAAM,EAAE,SAAS,gBAAgB;AAEjC,QAAI,OAAO,YAAY,YAAY,QAAQ,WAAW,GAAG;AACvD,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;AAEF,QAAI,OAAO,gBAAgB,YAAY,CAAC,OAAO,UAAU,YAAY,EAAE;AACrE,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;IAGF,MAAM,SAAyB;KAC7B,qBAAI,IAAI,MAAM,EAAC,aAAa;KAC5B;KACA;KACA,GAAI,OAAO,KAAK,UAAU,WAAW,EAAE,OAAO,KAAK,OAAO,GAAG,EAAE;KAC/D,GAAI,OAAO,KAAK,SAAS,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,EAAE;KAC7D;AAED,QAAI;AACF,WAAM,KAAK,MAAM,OAAO,OAAO;aACxB,OAAO;AACd,YAAO,MAAM,sCAAsC;MACjD;MACA;MACD,CAAC;AACF,SACG,OAAO,IAAI,CACX,KAAK;MAAE,IAAI;MAAO,SAAS;MAAmC,CAAC;AAClE;;AAGF,WAAO,KAAK,8BAA8B;KACxC;KACA;KACA,OAAO,OAAO;KACf,CAAC;AACF,QAAI,KAAK;KAAE,IAAI;KAAM;KAAS;KAAa,CAAC;;GAE/C,CAAC;;;AAIN,MAAa,aAAa,SAAS,iBAAiB"}
@@ -1,5 +1,5 @@
1
1
  import { ResourceFieldEntry } from "../shared/src/schemas/manifest.js";
2
2
  import { ResourcePermission, ResourceType } from "./types.generated.js";
3
3
  import { ConfigSchema, PluginManifest, ResourceEntry, ResourceRequirement, ValidationResult } from "./types.js";
4
- import { getPluginManifest, getResourceRequirements } from "./manifest-loader.js";
4
+ import { defineManifest, getPluginManifest, getResourceRequirements } from "./manifest-loader.js";
5
5
  import { ResourceRegistry } from "./resource-registry.js";
@@ -1,6 +1,6 @@
1
1
  import { PERMISSIONS_BY_TYPE, PERMISSION_HIERARCHY_BY_TYPE, ResourceType } from "./types.generated.js";
2
2
  import "./types.js";
3
- import { getPluginManifest, getResourceRequirements } from "./manifest-loader.js";
3
+ import { defineManifest, getPluginManifest, getResourceRequirements } from "./manifest-loader.js";
4
4
  import { ResourceRegistry } from "./resource-registry.js";
5
5
 
6
6
  export { };
@@ -5,6 +5,25 @@ import { ResourcePermission, ResourceType } from "./types.generated.js";
5
5
  import { PluginManifest } from "./types.js";
6
6
 
7
7
  //#region src/registry/manifest-loader.d.ts
8
+ /**
9
+ * Validates a raw manifest (typically a `manifest.json` import) against the
10
+ * canonical Zod schema and returns it as a strict {@link PluginManifest}.
11
+ *
12
+ * Plugins declare `static manifest = defineManifest<"my-plugin">(manifestJson)`
13
+ * instead of casting. A plain `as PluginManifest` can't work: a JSON import
14
+ * widens every field to `string`, and `PluginManifest.resources[].type` is the
15
+ * nominal `ResourceType` enum, so the structural JSON shape never assigns. The
16
+ * single internal assertion here bridges that gap in one audited place — after
17
+ * `parse()` has confirmed the values are real `ResourceType`/permission strings
18
+ * — rather than every plugin repeating `as unknown as PluginManifest`.
19
+ *
20
+ * Pass the plugin name as `TName` so the literal is preserved: `toPlugin`
21
+ * derives the typed plugin key from `manifest.name`, and a widened `string`
22
+ * there would collapse the typed plugin registry.
23
+ *
24
+ * @throws {ZodError} If the manifest doesn't match the schema.
25
+ */
26
+ declare function defineManifest<TName extends string = string>(manifest: unknown): PluginManifest<TName>;
8
27
  /**
9
28
  * Loads and validates the manifest from a plugin constructor.
10
29
  * Normalizes string type/permission to strict ResourceType/ResourcePermission.
@@ -42,5 +61,5 @@ declare function getResourceRequirements(plugin: PluginConstructor): {
42
61
  fields: Record<string, ResourceFieldEntry>;
43
62
  }[];
44
63
  //#endregion
45
- export { getPluginManifest, getResourceRequirements };
64
+ export { defineManifest, getPluginManifest, getResourceRequirements };
46
65
  //# sourceMappingURL=manifest-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-loader.d.ts","names":[],"sources":["../../src/registry/manifest-loader.ts"],"mappings":";;;;;;;;;;;AA6DA;;;;iBAAgB,iBAAA,CAAkB,MAAA,EAAQ,iBAAA,GAAoB,cAAA;;;;;AA4F9D;;;;;;;;;;;;;;iBAAgB,uBAAA,CAAwB,MAAA,EAAQ,iBAAA"}
1
+ {"version":3,"file":"manifest-loader.d.ts","names":[],"sources":["../../src/registry/manifest-loader.ts"],"mappings":";;;;;;;;;;;AA+BA;;;;;;;;;;AAwDA;;;;iBAxDgB,cAAA,+BAAA,CACd,QAAA,YACC,cAAA,CAAe,KAAA;;;;;AAkJlB;;;;iBA5FgB,iBAAA,CAAkB,MAAA,EAAQ,iBAAA,GAAoB,cAAA;;;;;;;;;;;;;;;;;;;iBA4F9C,uBAAA,CAAwB,MAAA,EAAQ,iBAAA"}
@@ -1,3 +1,4 @@
1
+ import { pluginManifestSchema } from "../shared/src/schemas/manifest.js";
1
2
  import { createLogger } from "../logging/logger.js";
2
3
  import { ConfigurationError } from "../errors/configuration.js";
3
4
  import "../errors/index.js";
@@ -6,6 +7,27 @@ import "./types.js";
6
7
 
7
8
  //#region src/registry/manifest-loader.ts
8
9
  const logger = createLogger("manifest-loader");
10
+ /**
11
+ * Validates a raw manifest (typically a `manifest.json` import) against the
12
+ * canonical Zod schema and returns it as a strict {@link PluginManifest}.
13
+ *
14
+ * Plugins declare `static manifest = defineManifest<"my-plugin">(manifestJson)`
15
+ * instead of casting. A plain `as PluginManifest` can't work: a JSON import
16
+ * widens every field to `string`, and `PluginManifest.resources[].type` is the
17
+ * nominal `ResourceType` enum, so the structural JSON shape never assigns. The
18
+ * single internal assertion here bridges that gap in one audited place — after
19
+ * `parse()` has confirmed the values are real `ResourceType`/permission strings
20
+ * — rather than every plugin repeating `as unknown as PluginManifest`.
21
+ *
22
+ * Pass the plugin name as `TName` so the literal is preserved: `toPlugin`
23
+ * derives the typed plugin key from `manifest.name`, and a widened `string`
24
+ * there would collapse the typed plugin registry.
25
+ *
26
+ * @throws {ZodError} If the manifest doesn't match the schema.
27
+ */
28
+ function defineManifest(manifest) {
29
+ return pluginManifestSchema.parse(manifest);
30
+ }
9
31
  function normalizeType(s) {
10
32
  const v = Object.values(ResourceType).find((x) => x === s);
11
33
  if (v !== void 0) return v;
@@ -93,5 +115,5 @@ function getResourceRequirements(plugin) {
93
115
  }
94
116
 
95
117
  //#endregion
96
- export { getPluginManifest, getResourceRequirements };
118
+ export { defineManifest, getPluginManifest, getResourceRequirements };
97
119
  //# sourceMappingURL=manifest-loader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-loader.js","names":[],"sources":["../../src/registry/manifest-loader.ts"],"sourcesContent":["import type { PluginConstructor } from \"shared\";\n\nimport { ConfigurationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type {\n PluginManifest,\n ResourcePermission,\n ResourceRequirement,\n} from \"./types\";\nimport { PERMISSIONS_BY_TYPE, ResourceType } from \"./types\";\n\nconst logger = createLogger(\"manifest-loader\");\n\n/** Loose resource from shared/manifest (string type and permission). */\ninterface LooseResource {\n type: string;\n alias: string;\n resourceKey: string;\n description: string;\n permission: string;\n fields: Record<string, { env: string; description?: string }>;\n}\n\nfunction normalizeType(s: string): ResourceType {\n const v = Object.values(ResourceType).find((x) => x === s);\n if (v !== undefined) return v;\n throw new ConfigurationError(\n `Invalid resource type: \"${s}\". Valid: ${Object.values(ResourceType).join(\", \")}`,\n );\n}\n\nfunction normalizePermission(\n type: ResourceType,\n s: string,\n): ResourcePermission {\n const allowed = PERMISSIONS_BY_TYPE[type];\n if (allowed.includes(s as ResourcePermission)) return s as ResourcePermission;\n throw new ConfigurationError(\n `Invalid permission \"${s}\" for type ${type}. Valid: ${allowed.join(\", \")}`,\n );\n}\n\nfunction normalizeResource(r: LooseResource): ResourceRequirement {\n const type = normalizeType(r.type);\n const permission = normalizePermission(type, r.permission);\n return {\n ...r,\n type,\n permission,\n required: false,\n };\n}\n\n/**\n * Loads and validates the manifest from a plugin constructor.\n * Normalizes string type/permission to strict ResourceType/ResourcePermission.\n *\n * @param plugin - The plugin constructor class\n * @returns The validated, normalized plugin manifest\n * @throws {ConfigurationError} If the manifest is missing, invalid, or has invalid resource type/permission\n */\nexport function getPluginManifest(plugin: PluginConstructor): PluginManifest {\n const pluginName = plugin.manifest?.name || plugin.name || \"unknown\";\n\n if (!plugin.manifest) {\n throw new ConfigurationError(\n `Plugin ${pluginName} is missing a manifest. All plugins must declare a static manifest property.`,\n );\n }\n\n const raw = plugin.manifest;\n\n if (!raw.name || typeof raw.name !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${pluginName} manifest has missing or invalid 'name' field`,\n );\n }\n\n if (!raw.displayName || typeof raw.displayName !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has missing or invalid 'displayName' field`,\n );\n }\n\n if (!raw.description || typeof raw.description !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has missing or invalid 'description' field`,\n );\n }\n\n if (!raw.resources) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest is missing 'resources' field`,\n );\n }\n\n if (!Array.isArray(raw.resources.required)) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has invalid 'resources.required' field (expected array)`,\n );\n }\n\n if (\n raw.resources.optional !== undefined &&\n !Array.isArray(raw.resources.optional)\n ) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has invalid 'resources.optional' field (expected array)`,\n );\n }\n\n const required = raw.resources.required.map((r) => {\n const norm = normalizeResource(r as LooseResource);\n const { required: _, ...rest } = norm;\n return rest;\n });\n const optional = (raw.resources.optional || []).map((r) => {\n const norm = normalizeResource(r as LooseResource);\n const { required: _, ...rest } = norm;\n return rest;\n });\n\n logger.debug(\n \"Loaded manifest for plugin %s: %d required resources, %d optional resources\",\n raw.name,\n required.length,\n optional.length,\n );\n\n return {\n ...raw,\n resources: { required, optional },\n };\n}\n\n/**\n * Gets the resource requirements from a plugin's manifest.\n *\n * Combines required and optional resources into a single array with the\n * `required` flag set appropriately.\n *\n * @param plugin - The plugin constructor class\n * @returns Combined array of required and optional resources\n * @throws {ConfigurationError} If the plugin manifest is missing or invalid\n *\n * @example\n * ```typescript\n * const resources = getResourceRequirements(AnalyticsPlugin);\n * for (const resource of resources) {\n * console.log(`${resource.type}: ${resource.description} (required: ${resource.required})`);\n * }\n * ```\n */\nexport function getResourceRequirements(plugin: PluginConstructor) {\n const manifest = getPluginManifest(plugin);\n\n const required = manifest.resources.required.map((r) => ({\n ...r,\n required: true,\n }));\n const optional = (manifest.resources.optional || []).map((r) => ({\n ...r,\n required: false,\n }));\n\n return [...required, ...optional];\n}\n\n/**\n * Validates a manifest object structure.\n *\n * @param manifest - The manifest object to validate\n * @returns true if the manifest is valid, false otherwise\n *\n * @internal\n */\nexport function isValidManifest(manifest: unknown): manifest is PluginManifest {\n if (!manifest || typeof manifest !== \"object\") {\n return false;\n }\n\n const m = manifest as Record<string, unknown>;\n\n // Check required fields\n if (typeof m.name !== \"string\") return false;\n if (typeof m.displayName !== \"string\") return false;\n if (typeof m.description !== \"string\") return false;\n\n // Check resources structure\n if (!m.resources || typeof m.resources !== \"object\") return false;\n\n const resources = m.resources as Record<string, unknown>;\n if (!Array.isArray(resources.required)) return false;\n\n // Optional field can be missing or must be an array\n if (resources.optional !== undefined && !Array.isArray(resources.optional)) {\n return false;\n }\n\n return true;\n}\n"],"mappings":";;;;;;;AAWA,MAAM,SAAS,aAAa,kBAAkB;AAY9C,SAAS,cAAc,GAAyB;CAC9C,MAAM,IAAI,OAAO,OAAO,aAAa,CAAC,MAAM,MAAM,MAAM,EAAE;AAC1D,KAAI,MAAM,OAAW,QAAO;AAC5B,OAAM,IAAI,mBACR,2BAA2B,EAAE,YAAY,OAAO,OAAO,aAAa,CAAC,KAAK,KAAK,GAChF;;AAGH,SAAS,oBACP,MACA,GACoB;CACpB,MAAM,UAAU,oBAAoB;AACpC,KAAI,QAAQ,SAAS,EAAwB,CAAE,QAAO;AACtD,OAAM,IAAI,mBACR,uBAAuB,EAAE,aAAa,KAAK,WAAW,QAAQ,KAAK,KAAK,GACzE;;AAGH,SAAS,kBAAkB,GAAuC;CAChE,MAAM,OAAO,cAAc,EAAE,KAAK;CAClC,MAAM,aAAa,oBAAoB,MAAM,EAAE,WAAW;AAC1D,QAAO;EACL,GAAG;EACH;EACA;EACA,UAAU;EACX;;;;;;;;;;AAWH,SAAgB,kBAAkB,QAA2C;CAC3E,MAAM,aAAa,OAAO,UAAU,QAAQ,OAAO,QAAQ;AAE3D,KAAI,CAAC,OAAO,SACV,OAAM,IAAI,mBACR,UAAU,WAAW,8EACtB;CAGH,MAAM,MAAM,OAAO;AAEnB,KAAI,CAAC,IAAI,QAAQ,OAAO,IAAI,SAAS,SACnC,OAAM,IAAI,mBACR,UAAU,WAAW,+CACtB;AAGH,KAAI,CAAC,IAAI,eAAe,OAAO,IAAI,gBAAgB,SACjD,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,sDACpB;AAGH,KAAI,CAAC,IAAI,eAAe,OAAO,IAAI,gBAAgB,SACjD,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,sDACpB;AAGH,KAAI,CAAC,IAAI,UACP,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,wCACpB;AAGH,KAAI,CAAC,MAAM,QAAQ,IAAI,UAAU,SAAS,CACxC,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,mEACpB;AAGH,KACE,IAAI,UAAU,aAAa,UAC3B,CAAC,MAAM,QAAQ,IAAI,UAAU,SAAS,CAEtC,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,mEACpB;CAGH,MAAM,WAAW,IAAI,UAAU,SAAS,KAAK,MAAM;EAEjD,MAAM,EAAE,UAAU,GAAG,GAAG,SADX,kBAAkB,EAAmB;AAElD,SAAO;GACP;CACF,MAAM,YAAY,IAAI,UAAU,YAAY,EAAE,EAAE,KAAK,MAAM;EAEzD,MAAM,EAAE,UAAU,GAAG,GAAG,SADX,kBAAkB,EAAmB;AAElD,SAAO;GACP;AAEF,QAAO,MACL,+EACA,IAAI,MACJ,SAAS,QACT,SAAS,OACV;AAED,QAAO;EACL,GAAG;EACH,WAAW;GAAE;GAAU;GAAU;EAClC;;;;;;;;;;;;;;;;;;;;AAqBH,SAAgB,wBAAwB,QAA2B;CACjE,MAAM,WAAW,kBAAkB,OAAO;CAE1C,MAAM,WAAW,SAAS,UAAU,SAAS,KAAK,OAAO;EACvD,GAAG;EACH,UAAU;EACX,EAAE;CACH,MAAM,YAAY,SAAS,UAAU,YAAY,EAAE,EAAE,KAAK,OAAO;EAC/D,GAAG;EACH,UAAU;EACX,EAAE;AAEH,QAAO,CAAC,GAAG,UAAU,GAAG,SAAS"}
1
+ {"version":3,"file":"manifest-loader.js","names":[],"sources":["../../src/registry/manifest-loader.ts"],"sourcesContent":["import { type PluginConstructor, pluginManifestSchema } from \"shared\";\n\nimport { ConfigurationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type {\n PluginManifest,\n ResourcePermission,\n ResourceRequirement,\n} from \"./types\";\nimport { PERMISSIONS_BY_TYPE, ResourceType } from \"./types\";\n\nconst logger = createLogger(\"manifest-loader\");\n\n/**\n * Validates a raw manifest (typically a `manifest.json` import) against the\n * canonical Zod schema and returns it as a strict {@link PluginManifest}.\n *\n * Plugins declare `static manifest = defineManifest<\"my-plugin\">(manifestJson)`\n * instead of casting. A plain `as PluginManifest` can't work: a JSON import\n * widens every field to `string`, and `PluginManifest.resources[].type` is the\n * nominal `ResourceType` enum, so the structural JSON shape never assigns. The\n * single internal assertion here bridges that gap in one audited place — after\n * `parse()` has confirmed the values are real `ResourceType`/permission strings\n * — rather than every plugin repeating `as unknown as PluginManifest`.\n *\n * Pass the plugin name as `TName` so the literal is preserved: `toPlugin`\n * derives the typed plugin key from `manifest.name`, and a widened `string`\n * there would collapse the typed plugin registry.\n *\n * @throws {ZodError} If the manifest doesn't match the schema.\n */\nexport function defineManifest<TName extends string = string>(\n manifest: unknown,\n): PluginManifest<TName> {\n return pluginManifestSchema.parse(\n manifest,\n ) as unknown as PluginManifest<TName>;\n}\n\n/** Loose resource from shared/manifest (string type and permission). */\ninterface LooseResource {\n type: string;\n alias: string;\n resourceKey: string;\n description: string;\n permission: string;\n fields: Record<string, { env: string; description?: string }>;\n}\n\nfunction normalizeType(s: string): ResourceType {\n const v = Object.values(ResourceType).find((x) => x === s);\n if (v !== undefined) return v;\n throw new ConfigurationError(\n `Invalid resource type: \"${s}\". Valid: ${Object.values(ResourceType).join(\", \")}`,\n );\n}\n\nfunction normalizePermission(\n type: ResourceType,\n s: string,\n): ResourcePermission {\n const allowed = PERMISSIONS_BY_TYPE[type];\n if (allowed.includes(s as ResourcePermission)) return s as ResourcePermission;\n throw new ConfigurationError(\n `Invalid permission \"${s}\" for type ${type}. Valid: ${allowed.join(\", \")}`,\n );\n}\n\nfunction normalizeResource(r: LooseResource): ResourceRequirement {\n const type = normalizeType(r.type);\n const permission = normalizePermission(type, r.permission);\n return {\n ...r,\n type,\n permission,\n required: false,\n };\n}\n\n/**\n * Loads and validates the manifest from a plugin constructor.\n * Normalizes string type/permission to strict ResourceType/ResourcePermission.\n *\n * @param plugin - The plugin constructor class\n * @returns The validated, normalized plugin manifest\n * @throws {ConfigurationError} If the manifest is missing, invalid, or has invalid resource type/permission\n */\nexport function getPluginManifest(plugin: PluginConstructor): PluginManifest {\n const pluginName = plugin.manifest?.name || plugin.name || \"unknown\";\n\n if (!plugin.manifest) {\n throw new ConfigurationError(\n `Plugin ${pluginName} is missing a manifest. All plugins must declare a static manifest property.`,\n );\n }\n\n const raw = plugin.manifest;\n\n if (!raw.name || typeof raw.name !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${pluginName} manifest has missing or invalid 'name' field`,\n );\n }\n\n if (!raw.displayName || typeof raw.displayName !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has missing or invalid 'displayName' field`,\n );\n }\n\n if (!raw.description || typeof raw.description !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has missing or invalid 'description' field`,\n );\n }\n\n if (!raw.resources) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest is missing 'resources' field`,\n );\n }\n\n if (!Array.isArray(raw.resources.required)) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has invalid 'resources.required' field (expected array)`,\n );\n }\n\n if (\n raw.resources.optional !== undefined &&\n !Array.isArray(raw.resources.optional)\n ) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has invalid 'resources.optional' field (expected array)`,\n );\n }\n\n const required = raw.resources.required.map((r) => {\n const norm = normalizeResource(r as LooseResource);\n const { required: _, ...rest } = norm;\n return rest;\n });\n const optional = (raw.resources.optional || []).map((r) => {\n const norm = normalizeResource(r as LooseResource);\n const { required: _, ...rest } = norm;\n return rest;\n });\n\n logger.debug(\n \"Loaded manifest for plugin %s: %d required resources, %d optional resources\",\n raw.name,\n required.length,\n optional.length,\n );\n\n return {\n ...raw,\n resources: { required, optional },\n };\n}\n\n/**\n * Gets the resource requirements from a plugin's manifest.\n *\n * Combines required and optional resources into a single array with the\n * `required` flag set appropriately.\n *\n * @param plugin - The plugin constructor class\n * @returns Combined array of required and optional resources\n * @throws {ConfigurationError} If the plugin manifest is missing or invalid\n *\n * @example\n * ```typescript\n * const resources = getResourceRequirements(AnalyticsPlugin);\n * for (const resource of resources) {\n * console.log(`${resource.type}: ${resource.description} (required: ${resource.required})`);\n * }\n * ```\n */\nexport function getResourceRequirements(plugin: PluginConstructor) {\n const manifest = getPluginManifest(plugin);\n\n const required = manifest.resources.required.map((r) => ({\n ...r,\n required: true,\n }));\n const optional = (manifest.resources.optional || []).map((r) => ({\n ...r,\n required: false,\n }));\n\n return [...required, ...optional];\n}\n\n/**\n * Validates a manifest object structure.\n *\n * @param manifest - The manifest object to validate\n * @returns true if the manifest is valid, false otherwise\n *\n * @internal\n */\nexport function isValidManifest(manifest: unknown): manifest is PluginManifest {\n if (!manifest || typeof manifest !== \"object\") {\n return false;\n }\n\n const m = manifest as Record<string, unknown>;\n\n // Check required fields\n if (typeof m.name !== \"string\") return false;\n if (typeof m.displayName !== \"string\") return false;\n if (typeof m.description !== \"string\") return false;\n\n // Check resources structure\n if (!m.resources || typeof m.resources !== \"object\") return false;\n\n const resources = m.resources as Record<string, unknown>;\n if (!Array.isArray(resources.required)) return false;\n\n // Optional field can be missing or must be an array\n if (resources.optional !== undefined && !Array.isArray(resources.optional)) {\n return false;\n }\n\n return true;\n}\n"],"mappings":";;;;;;;;AAWA,MAAM,SAAS,aAAa,kBAAkB;;;;;;;;;;;;;;;;;;;AAoB9C,SAAgB,eACd,UACuB;AACvB,QAAO,qBAAqB,MAC1B,SACD;;AAaH,SAAS,cAAc,GAAyB;CAC9C,MAAM,IAAI,OAAO,OAAO,aAAa,CAAC,MAAM,MAAM,MAAM,EAAE;AAC1D,KAAI,MAAM,OAAW,QAAO;AAC5B,OAAM,IAAI,mBACR,2BAA2B,EAAE,YAAY,OAAO,OAAO,aAAa,CAAC,KAAK,KAAK,GAChF;;AAGH,SAAS,oBACP,MACA,GACoB;CACpB,MAAM,UAAU,oBAAoB;AACpC,KAAI,QAAQ,SAAS,EAAwB,CAAE,QAAO;AACtD,OAAM,IAAI,mBACR,uBAAuB,EAAE,aAAa,KAAK,WAAW,QAAQ,KAAK,KAAK,GACzE;;AAGH,SAAS,kBAAkB,GAAuC;CAChE,MAAM,OAAO,cAAc,EAAE,KAAK;CAClC,MAAM,aAAa,oBAAoB,MAAM,EAAE,WAAW;AAC1D,QAAO;EACL,GAAG;EACH;EACA;EACA,UAAU;EACX;;;;;;;;;;AAWH,SAAgB,kBAAkB,QAA2C;CAC3E,MAAM,aAAa,OAAO,UAAU,QAAQ,OAAO,QAAQ;AAE3D,KAAI,CAAC,OAAO,SACV,OAAM,IAAI,mBACR,UAAU,WAAW,8EACtB;CAGH,MAAM,MAAM,OAAO;AAEnB,KAAI,CAAC,IAAI,QAAQ,OAAO,IAAI,SAAS,SACnC,OAAM,IAAI,mBACR,UAAU,WAAW,+CACtB;AAGH,KAAI,CAAC,IAAI,eAAe,OAAO,IAAI,gBAAgB,SACjD,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,sDACpB;AAGH,KAAI,CAAC,IAAI,eAAe,OAAO,IAAI,gBAAgB,SACjD,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,sDACpB;AAGH,KAAI,CAAC,IAAI,UACP,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,wCACpB;AAGH,KAAI,CAAC,MAAM,QAAQ,IAAI,UAAU,SAAS,CACxC,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,mEACpB;AAGH,KACE,IAAI,UAAU,aAAa,UAC3B,CAAC,MAAM,QAAQ,IAAI,UAAU,SAAS,CAEtC,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,mEACpB;CAGH,MAAM,WAAW,IAAI,UAAU,SAAS,KAAK,MAAM;EAEjD,MAAM,EAAE,UAAU,GAAG,GAAG,SADX,kBAAkB,EAAmB;AAElD,SAAO;GACP;CACF,MAAM,YAAY,IAAI,UAAU,YAAY,EAAE,EAAE,KAAK,MAAM;EAEzD,MAAM,EAAE,UAAU,GAAG,GAAG,SADX,kBAAkB,EAAmB;AAElD,SAAO;GACP;AAEF,QAAO,MACL,+EACA,IAAI,MACJ,SAAS,QACT,SAAS,OACV;AAED,QAAO;EACL,GAAG;EACH,WAAW;GAAE;GAAU;GAAU;EAClC;;;;;;;;;;;;;;;;;;;;AAqBH,SAAgB,wBAAwB,QAA2B;CACjE,MAAM,WAAW,kBAAkB,OAAO;CAE1C,MAAM,WAAW,SAAS,UAAU,SAAS,KAAK,OAAO;EACvD,GAAG;EACH,UAAU;EACX,EAAE;CACH,MAAM,YAAY,SAAS,UAAU,YAAY,EAAE,EAAE,KAAK,OAAO;EAC/D,GAAG;EACH,UAAU;EACX,EAAE;AAEH,QAAO,CAAC,GAAG,UAAU,GAAG,SAAS"}
@@ -1136,5 +1136,5 @@ type ResourceFieldEntry = z.infer<typeof resourceFieldEntrySchema>;
1136
1136
  type PluginScaffoldingRules = z.infer<typeof pluginScaffoldingRulesSchema>;
1137
1137
  type PluginManifest = z.infer<typeof pluginManifestSchema>;
1138
1138
  //#endregion
1139
- export { DiscoveryDescriptor, PluginManifest, PluginScaffoldingRules, ResourceFieldEntry };
1139
+ export { DiscoveryDescriptor, PluginManifest, PluginScaffoldingRules, ResourceFieldEntry, pluginManifestSchema };
1140
1140
  //# sourceMappingURL=manifest.d.ts.map
@@ -117,7 +117,7 @@ type AgentEvent = {
117
117
  * is awaiting human approval — fires for tools annotated with
118
118
  * `effect: "write" | "update" | "destructive"` (preferred) or the
119
119
  * legacy `destructive: true` boolean. Clients should render an approval
120
- * prompt and POST to `/chat/approve` with the matching `approvalId` and
120
+ * prompt and POST to `/api/agents/approve` with the matching `approvalId` and
121
121
  * a `decision` of `approve` or `deny`.
122
122
  */
123
123
  type: "approval_pending";
@@ -1,5 +1,5 @@
1
1
  import { AgentAdapter, AgentEvent, AgentInput, AgentRunContext, AgentToolDefinition, Message, Thread, ThreadStore, ToolAnnotations, ToolCall, ToolEffect, ToolProvider } from "./agent.js";
2
- import { ResourceFieldEntry } from "./schemas/manifest.js";
2
+ import { ResourceFieldEntry, pluginManifestSchema } from "./schemas/manifest.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";
5
5
  import { PluginExecuteConfig, PluginExecutionSettings, RetryConfig, StreamConfig, StreamExecuteHandler, StreamExecutionSettings, TelemetryConfig } from "./execute.js";
@@ -1,5 +1,17 @@
1
1
  //#region ../shared/src/naming.ts
2
2
  /**
3
+ * Plugin-name casing helpers. A dependency-free leaf module so both the
4
+ * runtime (appkit) and the bundled CLI can import it without pulling in the
5
+ * heavier `plugin.ts` graph.
6
+ */
7
+ /**
8
+ * Canonical plugin-name charset: a lowercase-initial camelCase JS identifier
9
+ * (e.g. `aiSearch`). The name doubles as the JS binding and the accessor key,
10
+ * so kebab is not allowed. Single source of truth for the manifest schema and
11
+ * the plugin-tree generators.
12
+ */
13
+ const PLUGIN_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
14
+ /**
3
15
  * camelCase to kebab-case (e.g. `"aiSearch"` -> `"ai-search"`). Used to derive
4
16
  * HTTP route prefixes and folder paths from the canonical camelCase plugin
5
17
  * name. A no-op for single-word names.
@@ -9,5 +21,5 @@ function camelToKebab(name) {
9
21
  }
10
22
 
11
23
  //#endregion
12
- export { camelToKebab };
24
+ export { PLUGIN_NAME_PATTERN, camelToKebab };
13
25
  //# sourceMappingURL=naming.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"naming.js","names":[],"sources":["../../../../shared/src/naming.ts"],"sourcesContent":["/**\n * Plugin-name casing helpers. A dependency-free leaf module so both the\n * runtime (appkit) and the bundled CLI can import it without pulling in the\n * heavier `plugin.ts` graph.\n */\n\n/**\n * Canonical plugin-name charset: a lowercase-initial camelCase JS identifier\n * (e.g. `aiSearch`). The name doubles as the JS binding and the accessor key,\n * so kebab is not allowed. Single source of truth for the manifest schema and\n * the plugin-tree generators.\n */\nexport const PLUGIN_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/;\n\n/** kebab-case to camelCase (e.g. `\"ai-search\"` -> `\"aiSearch\"`). */\nexport function kebabToCamel(name: string): string {\n return name.replace(/-+([a-z0-9])/g, (_, c: string) => c.toUpperCase());\n}\n\n/**\n * camelCase to kebab-case (e.g. `\"aiSearch\"` -> `\"ai-search\"`). Used to derive\n * HTTP route prefixes and folder paths from the canonical camelCase plugin\n * name. A no-op for single-word names.\n */\nexport function camelToKebab(name: string): string {\n return name.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n}\n"],"mappings":";;;;;;AAwBA,SAAgB,aAAa,MAAsB;AACjD,QAAO,KAAK,QAAQ,WAAW,MAAM,IAAI,EAAE,aAAa,GAAG"}
1
+ {"version":3,"file":"naming.js","names":[],"sources":["../../../../shared/src/naming.ts"],"sourcesContent":["/**\n * Plugin-name casing helpers. A dependency-free leaf module so both the\n * runtime (appkit) and the bundled CLI can import it without pulling in the\n * heavier `plugin.ts` graph.\n */\n\n/**\n * Canonical plugin-name charset: a lowercase-initial camelCase JS identifier\n * (e.g. `aiSearch`). The name doubles as the JS binding and the accessor key,\n * so kebab is not allowed. Single source of truth for the manifest schema and\n * the plugin-tree generators.\n */\nexport const PLUGIN_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/;\n\n/** kebab-case to camelCase (e.g. `\"ai-search\"` -> `\"aiSearch\"`). */\nexport function kebabToCamel(name: string): string {\n return name.replace(/-+([a-z0-9])/g, (_, c: string) => c.toUpperCase());\n}\n\n/**\n * camelCase to kebab-case (e.g. `\"aiSearch\"` -> `\"ai-search\"`). Used to derive\n * HTTP route prefixes and folder paths from the canonical camelCase plugin\n * name. A no-op for single-word names.\n */\nexport function camelToKebab(name: string): string {\n return name.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n}\n"],"mappings":";;;;;;;;;;;;AAYA,MAAa,sBAAsB;;;;;;AAYnC,SAAgB,aAAa,MAAsB;AACjD,QAAO,KAAK,QAAQ,WAAW,MAAM,IAAI,EAAE,aAAa,GAAG"}
@@ -1107,5 +1107,5 @@ declare const pluginManifestSchema: z.ZodObject<{
1107
1107
  type ResourceFieldEntry = z.infer<typeof resourceFieldEntrySchema>;
1108
1108
  type PluginManifest = z.infer<typeof pluginManifestSchema>;
1109
1109
  //#endregion
1110
- export { PluginManifest, ResourceFieldEntry };
1110
+ export { PluginManifest, ResourceFieldEntry, pluginManifestSchema };
1111
1111
  //# sourceMappingURL=manifest.d.ts.map