feat(appkit): plugin infrastructure — attachContext + PluginContext mediator#303
Merged
MarioCadenas merged 11 commits intomainfrom May 7, 2026
Merged
feat(appkit): plugin infrastructure — attachContext + PluginContext mediator#303MarioCadenas merged 11 commits intomainfrom
MarioCadenas merged 11 commits intomainfrom
Conversation
This was referenced Apr 21, 2026
b328cf2 to
5a7a4df
Compare
a5642df to
e26795b
Compare
5a7a4df to
a384b1e
Compare
e26795b to
d73e138
Compare
a384b1e to
68e05d3
Compare
d73e138 to
26f43e5
Compare
68e05d3 to
b765708
Compare
26f43e5 to
2ffa31d
Compare
b765708 to
6712ce7
Compare
3a4deb7 to
ca9cfca
Compare
6712ce7 to
7077eb0
Compare
aa95c27 to
71a986d
Compare
a70bbcc to
3343203
Compare
71a986d to
82d8ea0
Compare
3343203 to
85663cf
Compare
a8418ea to
c9c986d
Compare
a63b7a4 to
47c1c68
Compare
c9c986d to
5fde8aa
Compare
47c1c68 to
6f83621
Compare
4a388b1 to
a7ebc57
Compare
pkosiec
approved these changes
May 6, 2026
Member
pkosiec
left a comment
There was a problem hiding this comment.
LGTM but please verify the agentic review findings before merge 👍
Introduces the tool-authoring primitives that peer plugins use to expose
their capabilities as agent tools, and updates analytics, files, genie,
and lakebase to implement the ToolProvider interface.
Tool helpers land in core/agent/ (not plugins/agents/) from day one so
peer plugins can depend on them without reaching across the sibling
boundary:
core/agent/types.ts — ToolkitEntry, AgentDefinition shape
core/agent/build-toolkit.ts — converts ToolRegistry → ToolkitEntry map
core/agent/tools/
define-tool.ts — defineTool() + ToolRegistry
function-tool.ts — FunctionTool interface + helpers
hosted-tools.ts — HostedTool / mcpServer() types
sql-policy.ts — assertReadOnlySql guard
tool.ts — tool() Zod-schema factory
json-schema.ts — Zod → JSON Schema converter
index.ts — public barrel
MCP client (AppKitMcpClient) and host-policy live in
plugins/agents/tools/ at this stage; a later commit promotes them to
connectors/mcp/ once the connector layer exists.
Add a file-level rationale (policy/auth, narrow scope, zero extra deps) and point the class JSDoc at it to avoid duplicating the same story in two places. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Single pass over volumes: connectors, toolkit tools, and policy warnings. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
… ./plugins barrel - Keep v2/1 beta.ts comment block; retain Databricks + tool-primitive exports - Restore JobsConnectorConfig, ga-exports.generated, and jobs plugin types on index - Remove broken export from ./plugins (no plugins/index.ts on this branch) Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
- organizeImports in core tools barrel, analytics, files, genie, lakebase, mcp-client - drop stale noExplicitAny biome-ignore (rule is off; suppressions flagged) - remove unused DownloadResponse import; use vi.mocked + cast in lakebase test Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Vitest mockReturnValueOnce is checked against pg.Pool; connect must return Promise<PoolClient>. Use a stub client cast to PoolClient for the failure case. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Expose defineTool, MCP client, toolkit helpers alongside existing beta tool exports so Knip recognizes core/agent/tools/index as used entry surface. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
dca3d96 to
a2c725e
Compare
The acknowledgement field added no real defense beyond the implicit opt-in (exposeAsAgentTool is itself an explicit, undefined-by-default field), and created asymmetry with sibling SP-bound SQL surfaces (analytics, genie). It would also drift once OBO lands. Real protections - read-only SQL classifier, BEGIN READ ONLY/ROLLBACK transaction wrapping, destructive-call HITL approval gate, and the startup warn log - are unchanged. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
a2c725e to
8b67c5a
Compare
…nContext mediator
Third layer: the substrate every downstream PR relies on. No user-
facing API changes here; the surface for this PR is the mediator
pattern, lifecycle semantics, and factory stamping.
`Plugin` constructors become pure — no `CacheManager.getInstanceSync()`,
no `TelemetryManager.getProvider()`, no `PluginContext` wiring inside
`constructor()`. That work moves to a new lifecycle method:
```ts
interface BasePlugin {
attachContext?(deps: {
context?: unknown;
telemetryConfig?: TelemetryOptions;
}): void;
}
```
`createApp` calls `attachContext()` on every plugin after all
constructors have run, before `setup()`. This lets factories return
`PluginData` tuples at module scope without pulling core services into
the import graph — a prerequisite for later PRs that construct agent
definitions before `createApp`.
`packages/appkit/src/core/plugin-context.ts` — new class that mediates
all inter-plugin communication:
- **Route buffering**: `addRoute()` / `addMiddleware()` buffer until
the server plugin calls `registerAsRouteTarget()`, then flush via
`addExtension()`. Eliminates plugin-ordering fragility.
- **ToolProvider registry**: `registerToolProvider(name, plugin)` +
live `getToolProviders()`. Typed discovery of tool-exposing plugins.
- **User-scoped tool execution**: `executeTool(req, pluginName,
localName, args, signal?)` resolves the provider, wraps in
`asUser(req)` for OBO, opens a telemetry span, applies a 30s
timeout, dispatches, returns.
- **Lifecycle hooks**: `onLifecycle('setup:complete' | 'server:ready'
| 'shutdown', cb)` + `emitLifecycle(event)`. Callback errors don't
block siblings.
`packages/appkit/src/plugin/to-plugin.ts` — the factory now attaches a
read-only `pluginName` property to the returned function. Later PRs'
`fromPlugin(factory)` reads it to identify which plugin a factory
refers to without needing to construct an instance. `NamedPluginFactory`
type exported for consumers who want to type-constrain factories.
`ServerPlugin.setup()` no longer calls `extendRoutes()` synchronously.
It subscribes to the `setup:complete` lifecycle event via
`PluginContext` and starts the HTTP server there. This ensures that
any deferred-phase plugin (agents plugin in a later PR) has had a
chance to register routes via `PluginContext.addRoute()` before the
server binds. Removes the `plugins` field from `ServerConfig` (routes
are now discovered via the context, not a config snapshot).
- 25 new PluginContext tests (route buffering, tool provider registry,
executeTool paths, lifecycle hooks, plugin metadata)
- Updated AppKit lifecycle tests to inject `context` instead of
`plugins`
- Full appkit vitest suite: 1237 tests passing
- Typecheck clean across all 8 workspace projects
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
a7ebc57 to
91e66e1
Compare
PR #303 agentic review applied (P1 + cheap P2 + P3 cleanup): - Snapshot lifecycle hooks before iteration so a callback that registers another hook for the same event does not re-enter the loop. - Add attachContext to EXCLUDED_FROM_PROXY so asUser() never proxies internal binding lifecycle into user context. - Use SpanStatusCode.OK / .ERROR instead of magic numbers; the previous code: 0 was UNSET (no-op for setStatus), so the success path was silently unreported in OTel traces. - Return getPlugins() as ReadonlyMap to prevent external mutation of the live plugin registry. - Strengthen isToolProvider to also require asUser, narrow to a ToolProviderPlugin shape, and drop the (entry.plugin as any).asUser cast in executeTool. - Guard double registerAsRouteTarget with logger.warn + ignore. - Guard duplicate registerToolProvider name with logger.warn. - Drop the ToolProviderEntry indirection; store ToolProviderPlugin directly keyed by name. Tests cover Set-mutation safety, double registerAsRouteTarget, duplicate tool-provider, the asUser requirement on isToolProvider, and the SpanStatusCode assertions on success and failure paths. Also adds plugin/to-plugin.ts to the knip ignore list. NamedPluginFactory is consumed only by downstream branches (fromPlugin) and was being flagged as unused on this branch in isolation. Findings #8 (configurable executeTool timeout), #9 (double context injection), and #10 (BasePluginConfig context cast) are advisory and deferred to a follow-up. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
…text Typedoc reflects the new attachContext lifecycle method and the PluginContext-typed context field added in 91e66e1. Fixes the docs:build sync gate failing on agent/v2/3-plugin-infra CI. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third layer: the substrate every downstream PR relies on. No user-
facing API changes here; the surface for this PR is the mediator
pattern, lifecycle semantics, and factory stamping.
Split Plugin construction from context binding
Pluginconstructors become pure — noCacheManager.getInstanceSync(),no
TelemetryManager.getProvider(), noPluginContextwiring insideconstructor(). That work moves to a new lifecycle method:createAppcallsattachContext()on every plugin after allconstructors have run, before
setup(). This lets factories returnPluginDatatuples at module scope without pulling core services intothe import graph — a prerequisite for later PRs that construct agent
definitions before
createApp.PluginContext mediator
packages/appkit/src/core/plugin-context.ts— new class that mediatesall inter-plugin communication:
addRoute()/addMiddleware()buffer untilthe server plugin calls
registerAsRouteTarget(), then flush viaaddExtension(). Eliminates plugin-ordering fragility.registerToolProvider(name, plugin)+live
getToolProviders(). Typed discovery of tool-exposing plugins.executeTool(req, pluginName, localName, args, signal?)resolves the provider, wraps inasUser(req)for OBO, opens a telemetry span, applies a 30stimeout, dispatches, returns.
onLifecycle('setup:complete' | 'server:ready' | 'shutdown', cb)+emitLifecycle(event). Callback errors don'tblock siblings.
toPluginstampspluginNamepackages/appkit/src/plugin/to-plugin.ts— the factory now attaches aread-only
pluginNameproperty to the returned function. Later PRs'fromPlugin(factory)reads it to identify which plugin a factoryrefers to without needing to construct an instance.
NamedPluginFactorytype exported for consumers who want to type-constrain factories.
Server plugin defers start to
setup:completeServerPlugin.setup()no longer callsextendRoutes()synchronously.It subscribes to the
setup:completelifecycle event viaPluginContextand starts the HTTP server there. This ensures thatany deferred-phase plugin (agents plugin in a later PR) has had a
chance to register routes via
PluginContext.addRoute()before theserver binds. Removes the
pluginsfield fromServerConfig(routesare now discovered via the context, not a config snapshot).
Test plan
executeTool paths, lifecycle hooks, plugin metadata)
contextinstead ofpluginsSigned-off-by: MarioCadenas MarioCadenas@users.noreply.github.com
PR Stack
agents()plugin +createAgent(def)+ markdown-driven agents — feat(appkit): agents() plugin, createAgent(def), and markdown-driven agents #304fromPlugin()DX +runAgentplugins arg + toolkit-resolver — feat(appkit): fromPlugin() DX, runAgent plugins arg, shared toolkit-resolver #305Demo
agent-demo.mp4