@namzu/sdk 0.1.5-rc.1-fix → 0.1.5

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 (224) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +279 -129
  3. package/dist/bridge/tools/connector/adapter.d.ts +2 -2
  4. package/dist/bridge/tools/connector/adapter.d.ts.map +1 -1
  5. package/dist/bridge/tools/connector/adapter.js +3 -1
  6. package/dist/bridge/tools/connector/adapter.js.map +1 -1
  7. package/dist/connector/BaseConnector.d.ts +2 -1
  8. package/dist/connector/BaseConnector.d.ts.map +1 -1
  9. package/dist/connector/BaseConnector.js.map +1 -1
  10. package/dist/connector/builtins/http.d.ts +1 -1
  11. package/dist/connector/builtins/http.d.ts.map +1 -1
  12. package/dist/connector/builtins/http.js +1 -1
  13. package/dist/connector/builtins/http.js.map +1 -1
  14. package/dist/connector/builtins/webhook.d.ts +1 -1
  15. package/dist/connector/builtins/webhook.d.ts.map +1 -1
  16. package/dist/connector/builtins/webhook.js +1 -1
  17. package/dist/connector/builtins/webhook.js.map +1 -1
  18. package/dist/contracts/api.d.ts.map +1 -1
  19. package/dist/contracts/index.d.ts +1 -0
  20. package/dist/contracts/index.d.ts.map +1 -1
  21. package/dist/contracts/index.js +7 -0
  22. package/dist/contracts/index.js.map +1 -1
  23. package/dist/index.d.ts +5 -33
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +5 -23
  26. package/dist/index.js.map +1 -1
  27. package/dist/manager/connector/environment.d.ts +4 -4
  28. package/dist/manager/connector/environment.d.ts.map +1 -1
  29. package/dist/manager/connector/environment.js.map +1 -1
  30. package/dist/manager/connector/lifecycle.d.ts +2 -2
  31. package/dist/manager/connector/lifecycle.d.ts.map +1 -1
  32. package/dist/manager/connector/lifecycle.js.map +1 -1
  33. package/dist/manager/connector/tenant.d.ts +3 -3
  34. package/dist/manager/connector/tenant.d.ts.map +1 -1
  35. package/dist/manager/connector/tenant.js.map +1 -1
  36. package/dist/manager/index.d.ts +2 -1
  37. package/dist/manager/index.d.ts.map +1 -1
  38. package/dist/manager/index.js +2 -1
  39. package/dist/manager/index.js.map +1 -1
  40. package/dist/manager/run/emergency.d.ts.map +1 -1
  41. package/dist/manager/run/emergency.js +44 -12
  42. package/dist/manager/run/emergency.js.map +1 -1
  43. package/dist/manager/run/persistence.d.ts +0 -2
  44. package/dist/manager/run/persistence.d.ts.map +1 -1
  45. package/dist/manager/run/persistence.js +0 -1
  46. package/dist/manager/run/persistence.js.map +1 -1
  47. package/dist/plugin/__tests__/enable-contributions.test.d.ts +2 -0
  48. package/dist/plugin/__tests__/enable-contributions.test.d.ts.map +1 -0
  49. package/dist/plugin/__tests__/enable-contributions.test.js +265 -0
  50. package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -0
  51. package/dist/plugin/__tests__/lifecycle.test.js +30 -0
  52. package/dist/plugin/__tests__/lifecycle.test.js.map +1 -1
  53. package/dist/plugin/lifecycle.d.ts +4 -1
  54. package/dist/plugin/lifecycle.d.ts.map +1 -1
  55. package/dist/plugin/lifecycle.js +157 -49
  56. package/dist/plugin/lifecycle.js.map +1 -1
  57. package/dist/rag/vector-store.d.ts +2 -2
  58. package/dist/rag/vector-store.d.ts.map +1 -1
  59. package/dist/rag/vector-store.js.map +1 -1
  60. package/dist/registry/ManagedRegistry.d.ts +6 -0
  61. package/dist/registry/ManagedRegistry.d.ts.map +1 -1
  62. package/dist/registry/ManagedRegistry.js +9 -3
  63. package/dist/registry/ManagedRegistry.js.map +1 -1
  64. package/dist/registry/agent/definitions.d.ts +2 -7
  65. package/dist/registry/agent/definitions.d.ts.map +1 -1
  66. package/dist/registry/agent/definitions.js +3 -41
  67. package/dist/registry/agent/definitions.js.map +1 -1
  68. package/dist/registry/connector/definitions.d.ts +2 -7
  69. package/dist/registry/connector/definitions.d.ts.map +1 -1
  70. package/dist/registry/connector/definitions.js +3 -41
  71. package/dist/registry/connector/definitions.js.map +1 -1
  72. package/dist/registry/connector/scoped.d.ts +5 -4
  73. package/dist/registry/connector/scoped.d.ts.map +1 -1
  74. package/dist/registry/connector/scoped.js.map +1 -1
  75. package/dist/registry/index.d.ts +1 -0
  76. package/dist/registry/index.d.ts.map +1 -1
  77. package/dist/registry/index.js +1 -0
  78. package/dist/registry/index.js.map +1 -1
  79. package/dist/registry/tool/execute.d.ts +2 -4
  80. package/dist/registry/tool/execute.d.ts.map +1 -1
  81. package/dist/registry/tool/execute.js +4 -21
  82. package/dist/registry/tool/execute.js.map +1 -1
  83. package/dist/run/index.d.ts +4 -4
  84. package/dist/run/index.d.ts.map +1 -1
  85. package/dist/run/index.js +3 -3
  86. package/dist/run/index.js.map +1 -1
  87. package/dist/run/reporter.d.ts +0 -2
  88. package/dist/run/reporter.d.ts.map +1 -1
  89. package/dist/run/reporter.js +0 -1
  90. package/dist/run/reporter.js.map +1 -1
  91. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.d.ts +2 -0
  92. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.d.ts.map +1 -0
  93. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js +236 -0
  94. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js.map +1 -0
  95. package/dist/runtime/query/__tests__/plugin-hooks.test.d.ts +2 -0
  96. package/dist/runtime/query/__tests__/plugin-hooks.test.d.ts.map +1 -0
  97. package/dist/runtime/query/__tests__/plugin-hooks.test.js +21 -0
  98. package/dist/runtime/query/__tests__/plugin-hooks.test.js.map +1 -0
  99. package/dist/runtime/query/executor.d.ts +6 -0
  100. package/dist/runtime/query/executor.d.ts.map +1 -1
  101. package/dist/runtime/query/executor.js +110 -6
  102. package/dist/runtime/query/executor.js.map +1 -1
  103. package/dist/runtime/query/index.d.ts.map +1 -1
  104. package/dist/runtime/query/index.js +8 -6
  105. package/dist/runtime/query/index.js.map +1 -1
  106. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  107. package/dist/runtime/query/iteration/index.js +13 -16
  108. package/dist/runtime/query/iteration/index.js.map +1 -1
  109. package/dist/runtime/query/plugin-hooks.d.ts +11 -0
  110. package/dist/runtime/query/plugin-hooks.d.ts.map +1 -0
  111. package/dist/runtime/query/plugin-hooks.js +28 -0
  112. package/dist/runtime/query/plugin-hooks.js.map +1 -0
  113. package/dist/runtime/query/tooling.d.ts +2 -0
  114. package/dist/runtime/query/tooling.d.ts.map +1 -1
  115. package/dist/runtime/query/tooling.js +1 -0
  116. package/dist/runtime/query/tooling.js.map +1 -1
  117. package/dist/store/index.d.ts +5 -1
  118. package/dist/store/index.d.ts.map +1 -1
  119. package/dist/store/index.js +4 -1
  120. package/dist/store/index.js.map +1 -1
  121. package/dist/store/run/disk.d.ts +0 -2
  122. package/dist/store/run/disk.d.ts.map +1 -1
  123. package/dist/store/run/disk.js +0 -1
  124. package/dist/store/run/disk.js.map +1 -1
  125. package/dist/store/task/__tests__/disk-concurrency.test.d.ts +2 -0
  126. package/dist/store/task/__tests__/disk-concurrency.test.d.ts.map +1 -0
  127. package/dist/store/task/__tests__/disk-concurrency.test.js +91 -0
  128. package/dist/store/task/__tests__/disk-concurrency.test.js.map +1 -0
  129. package/dist/store/task/disk.d.ts +6 -0
  130. package/dist/store/task/disk.d.ts.map +1 -1
  131. package/dist/store/task/disk.js +150 -36
  132. package/dist/store/task/disk.js.map +1 -1
  133. package/dist/types/connector/core.d.ts +2 -2
  134. package/dist/types/connector/core.d.ts.map +1 -1
  135. package/dist/types/connector/definition.d.ts +7 -7
  136. package/dist/types/connector/definition.d.ts.map +1 -1
  137. package/dist/types/connector/mcp.d.ts +4 -4
  138. package/dist/types/connector/mcp.d.ts.map +1 -1
  139. package/dist/types/connector/scope.d.ts +3 -2
  140. package/dist/types/connector/scope.d.ts.map +1 -1
  141. package/dist/types/connector/scope.js.map +1 -1
  142. package/dist/types/connector/tenant.d.ts +4 -4
  143. package/dist/types/connector/tenant.d.ts.map +1 -1
  144. package/dist/types/plugin/index.d.ts +4 -21
  145. package/dist/types/plugin/index.d.ts.map +1 -1
  146. package/dist/types/plugin/index.js +1 -10
  147. package/dist/types/plugin/index.js.map +1 -1
  148. package/dist/types/rag/knowledge-base.d.ts +2 -2
  149. package/dist/types/rag/knowledge-base.d.ts.map +1 -1
  150. package/dist/types/rag/scope.d.ts +2 -1
  151. package/dist/types/rag/scope.d.ts.map +1 -1
  152. package/dist/types/rag/storage.d.ts +3 -3
  153. package/dist/types/rag/storage.d.ts.map +1 -1
  154. package/dist/types/rag/vector.d.ts +3 -3
  155. package/dist/types/rag/vector.d.ts.map +1 -1
  156. package/dist/types/run/config.d.ts +0 -6
  157. package/dist/types/run/config.d.ts.map +1 -1
  158. package/dist/vault/InMemoryCredentialVault.d.ts +3 -3
  159. package/dist/vault/InMemoryCredentialVault.d.ts.map +1 -1
  160. package/dist/vault/InMemoryCredentialVault.js.map +1 -1
  161. package/package.json +1 -1
  162. package/src/bridge/tools/connector/adapter.ts +5 -3
  163. package/src/connector/BaseConnector.ts +2 -1
  164. package/src/connector/builtins/http.ts +1 -1
  165. package/src/connector/builtins/webhook.ts +1 -1
  166. package/src/contracts/api.ts +3 -0
  167. package/src/contracts/index.ts +10 -0
  168. package/src/index.ts +46 -41
  169. package/src/manager/connector/environment.ts +5 -5
  170. package/src/manager/connector/lifecycle.ts +2 -2
  171. package/src/manager/connector/tenant.ts +8 -3
  172. package/src/manager/index.ts +2 -1
  173. package/src/manager/run/emergency.ts +45 -16
  174. package/src/manager/run/persistence.ts +0 -4
  175. package/src/plugin/__tests__/enable-contributions.test.ts +295 -0
  176. package/src/plugin/__tests__/lifecycle.test.ts +44 -0
  177. package/src/plugin/lifecycle.ts +186 -57
  178. package/src/rag/vector-store.ts +2 -2
  179. package/src/registry/ManagedRegistry.ts +14 -3
  180. package/src/registry/agent/definitions.ts +3 -53
  181. package/src/registry/connector/definitions.ts +3 -53
  182. package/src/registry/connector/scoped.ts +7 -6
  183. package/src/registry/index.ts +1 -0
  184. package/src/registry/tool/execute.ts +4 -22
  185. package/src/run/index.ts +4 -4
  186. package/src/run/reporter.ts +0 -4
  187. package/src/runtime/query/__tests__/executor-plugin-hooks.test.ts +305 -0
  188. package/src/runtime/query/__tests__/plugin-hooks.test.ts +28 -0
  189. package/src/runtime/query/executor.ts +148 -7
  190. package/src/runtime/query/index.ts +16 -6
  191. package/src/runtime/query/iteration/index.ts +29 -16
  192. package/src/runtime/query/plugin-hooks.ts +34 -0
  193. package/src/runtime/query/tooling.ts +3 -0
  194. package/src/store/index.ts +6 -1
  195. package/src/store/run/disk.ts +0 -4
  196. package/src/store/task/__tests__/disk-concurrency.test.ts +118 -0
  197. package/src/store/task/disk.ts +150 -37
  198. package/src/types/connector/core.ts +2 -2
  199. package/src/types/connector/definition.ts +7 -7
  200. package/src/types/connector/mcp.ts +4 -4
  201. package/src/types/connector/scope.ts +3 -2
  202. package/src/types/connector/tenant.ts +4 -4
  203. package/src/types/plugin/index.ts +0 -14
  204. package/src/types/rag/knowledge-base.ts +2 -2
  205. package/src/types/rag/scope.ts +3 -1
  206. package/src/types/rag/storage.ts +3 -3
  207. package/src/types/rag/vector.ts +3 -3
  208. package/src/types/run/config.ts +0 -9
  209. package/src/vault/InMemoryCredentialVault.ts +3 -3
  210. package/dist/manager/agent/index.d.ts +0 -2
  211. package/dist/manager/agent/index.d.ts.map +0 -1
  212. package/dist/manager/agent/index.js +0 -2
  213. package/dist/manager/agent/index.js.map +0 -1
  214. package/dist/registry/agent/index.d.ts +0 -2
  215. package/dist/registry/agent/index.d.ts.map +0 -1
  216. package/dist/registry/agent/index.js +0 -2
  217. package/dist/registry/agent/index.js.map +0 -1
  218. package/dist/router/index.d.ts +0 -2
  219. package/dist/router/index.d.ts.map +0 -1
  220. package/dist/router/index.js +0 -2
  221. package/dist/router/index.js.map +0 -1
  222. package/src/manager/agent/index.ts +0 -1
  223. package/src/registry/agent/index.ts +0 -1
  224. package/src/router/index.ts +0 -1
@@ -0,0 +1,295 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { PluginRegistry } from '../../registry/plugin/index.js'
3
+ import type { PluginId } from '../../types/ids/index.js'
4
+ import type { PluginDefinition } from '../../types/plugin/index.js'
5
+ import type { ToolDefinition, ToolRegistryContract } from '../../types/tool/index.js'
6
+ import type { Logger } from '../../utils/logger.js'
7
+ import { PluginLifecycleManager } from '../lifecycle.js'
8
+
9
+ const mockConnect = vi.fn(async (): Promise<unknown> => undefined)
10
+ const mockDisconnect = vi.fn(async (): Promise<void> => undefined)
11
+ const mockListTools = vi.fn(async (): Promise<unknown[]> => [])
12
+
13
+ vi.mock('../../connector/mcp/client.js', () => ({
14
+ MCPClient: vi.fn().mockImplementation(() => ({
15
+ id: 'mcp-client-mock',
16
+ connect: mockConnect,
17
+ disconnect: mockDisconnect,
18
+ listTools: mockListTools,
19
+ })),
20
+ }))
21
+
22
+ vi.mock('../../connector/mcp/adapter.js', () => ({
23
+ mcpToolToToolDefinition: vi.fn((mcpTool: { name: string }) => ({
24
+ name: mcpTool.name,
25
+ description: `mcp tool ${mcpTool.name}`,
26
+ inputSchema: { parse: vi.fn() } as any,
27
+ async execute() {
28
+ return { success: true, output: 'ok' }
29
+ },
30
+ })),
31
+ }))
32
+
33
+ function makeLogger(): Logger {
34
+ const s = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
35
+ return { ...s, child: vi.fn(() => ({ ...s, child: vi.fn() })) } as unknown as Logger
36
+ }
37
+
38
+ function makePluginRegistry(base: Partial<PluginDefinition>): {
39
+ registry: PluginRegistry
40
+ state: { current: PluginDefinition }
41
+ } {
42
+ const state = {
43
+ current: {
44
+ id: 'plugin_test' as PluginId,
45
+ manifest: base.manifest ?? {
46
+ name: 'test-plugin',
47
+ version: '0.0.1',
48
+ description: 'test',
49
+ },
50
+ scope: 'project',
51
+ status: 'installed',
52
+ rootDir: '/tmp/plugin',
53
+ installedAt: 0,
54
+ ...base,
55
+ } as PluginDefinition,
56
+ }
57
+ const registry = {
58
+ register: vi.fn((def: PluginDefinition) => {
59
+ state.current = def
60
+ }),
61
+ unregister: vi.fn(),
62
+ getOrThrow: vi.fn(() => state.current),
63
+ findByName: vi.fn(),
64
+ getAll: vi.fn(() => [state.current]),
65
+ } as unknown as PluginRegistry
66
+ return { registry, state }
67
+ }
68
+
69
+ function makeToolRegistry(): ToolRegistryContract {
70
+ const names: string[] = []
71
+ return {
72
+ register: vi.fn((tool: ToolDefinition) => {
73
+ names.push(tool.name)
74
+ }),
75
+ unregister: vi.fn((name: string) => {
76
+ const i = names.indexOf(name)
77
+ if (i >= 0) names.splice(i, 1)
78
+ }),
79
+ listNames: vi.fn(() => [...names]),
80
+ has: vi.fn((name: string) => names.includes(name)),
81
+ get: vi.fn(),
82
+ execute: vi.fn(),
83
+ getAvailability: vi.fn(),
84
+ } as unknown as ToolRegistryContract
85
+ }
86
+
87
+ describe('PluginLifecycleManager enable() contribution types', () => {
88
+ const pluginId = 'plugin_test' as PluginId
89
+
90
+ beforeEach(() => {
91
+ mockConnect.mockReset()
92
+ mockDisconnect.mockReset()
93
+ mockListTools.mockReset()
94
+ })
95
+
96
+ describe('unsupported contribution types', () => {
97
+ it('throws when manifest declares skills', async () => {
98
+ const { registry } = makePluginRegistry({
99
+ manifest: {
100
+ name: 'p',
101
+ version: '0.0.1',
102
+ description: 't',
103
+ skills: ['./s'],
104
+ },
105
+ })
106
+ const mgr = new PluginLifecycleManager({
107
+ pluginRegistry: registry,
108
+ toolRegistry: makeToolRegistry(),
109
+ log: makeLogger(),
110
+ })
111
+ await expect(mgr.enable(pluginId)).rejects.toThrow(/\[skills\]/)
112
+ })
113
+
114
+ it('throws when manifest declares connectors', async () => {
115
+ const { registry } = makePluginRegistry({
116
+ manifest: {
117
+ name: 'p',
118
+ version: '0.0.1',
119
+ description: 't',
120
+ connectors: ['./c'],
121
+ },
122
+ })
123
+ const mgr = new PluginLifecycleManager({
124
+ pluginRegistry: registry,
125
+ toolRegistry: makeToolRegistry(),
126
+ log: makeLogger(),
127
+ })
128
+ await expect(mgr.enable(pluginId)).rejects.toThrow(/\[connectors\]/)
129
+ })
130
+
131
+ it('throws when manifest declares personas', async () => {
132
+ const { registry } = makePluginRegistry({
133
+ manifest: {
134
+ name: 'p',
135
+ version: '0.0.1',
136
+ description: 't',
137
+ personas: ['./pp'],
138
+ },
139
+ })
140
+ const mgr = new PluginLifecycleManager({
141
+ pluginRegistry: registry,
142
+ toolRegistry: makeToolRegistry(),
143
+ log: makeLogger(),
144
+ })
145
+ await expect(mgr.enable(pluginId)).rejects.toThrow(/\[personas\]/)
146
+ })
147
+
148
+ it('lists all unsupported types together when multiple declared', async () => {
149
+ const { registry } = makePluginRegistry({
150
+ manifest: {
151
+ name: 'p',
152
+ version: '0.0.1',
153
+ description: 't',
154
+ skills: ['./s'],
155
+ connectors: ['./c'],
156
+ personas: ['./pp'],
157
+ },
158
+ })
159
+ const mgr = new PluginLifecycleManager({
160
+ pluginRegistry: registry,
161
+ toolRegistry: makeToolRegistry(),
162
+ log: makeLogger(),
163
+ })
164
+ await expect(mgr.enable(pluginId)).rejects.toThrow(/skills, connectors, personas/)
165
+ })
166
+ })
167
+
168
+ describe('mcpServers wiring', () => {
169
+ it('registers namespaced tools for each MCP server tool', async () => {
170
+ mockConnect.mockResolvedValue(undefined)
171
+ mockListTools.mockResolvedValue([
172
+ { name: 'read_file', inputSchema: { type: 'object' } },
173
+ { name: 'write_file', inputSchema: { type: 'object' } },
174
+ ])
175
+ const { registry } = makePluginRegistry({
176
+ manifest: {
177
+ name: 'fs-plugin',
178
+ version: '0.0.1',
179
+ description: 't',
180
+ mcpServers: [{ name: 'fs', command: '/bin/true' }],
181
+ },
182
+ })
183
+ const toolRegistry = makeToolRegistry()
184
+ const mgr = new PluginLifecycleManager({
185
+ pluginRegistry: registry,
186
+ toolRegistry,
187
+ log: makeLogger(),
188
+ })
189
+
190
+ await mgr.enable(pluginId)
191
+
192
+ expect(mockConnect).toHaveBeenCalledOnce()
193
+ expect(mockListTools).toHaveBeenCalledOnce()
194
+ expect(toolRegistry.listNames()).toEqual([
195
+ 'fs-plugin:mcp__fs__read_file',
196
+ 'fs-plugin:mcp__fs__write_file',
197
+ ])
198
+ })
199
+
200
+ it('disconnects MCP clients and unregisters tools on disable', async () => {
201
+ mockConnect.mockResolvedValue(undefined)
202
+ mockDisconnect.mockResolvedValue(undefined)
203
+ mockListTools.mockResolvedValue([{ name: 'ping', inputSchema: { type: 'object' } }])
204
+ const { registry } = makePluginRegistry({
205
+ manifest: {
206
+ name: 'net',
207
+ version: '0.0.1',
208
+ description: 't',
209
+ mcpServers: [{ name: 'srv', command: '/bin/true' }],
210
+ },
211
+ })
212
+ const toolRegistry = makeToolRegistry()
213
+ const mgr = new PluginLifecycleManager({
214
+ pluginRegistry: registry,
215
+ toolRegistry,
216
+ log: makeLogger(),
217
+ })
218
+
219
+ await mgr.enable(pluginId)
220
+ expect(toolRegistry.listNames()).toContain('net:mcp__srv__ping')
221
+
222
+ await mgr.disable(pluginId)
223
+ expect(mockDisconnect).toHaveBeenCalledOnce()
224
+ expect(toolRegistry.listNames()).toEqual([])
225
+ })
226
+
227
+ it('disconnects MCP clients before unregistering tools on disable', async () => {
228
+ mockConnect.mockResolvedValue(undefined)
229
+ mockDisconnect.mockResolvedValue(undefined)
230
+ mockListTools.mockResolvedValue([{ name: 'ping', inputSchema: { type: 'object' } }])
231
+ const { registry } = makePluginRegistry({
232
+ manifest: {
233
+ name: 'net',
234
+ version: '0.0.1',
235
+ description: 't',
236
+ mcpServers: [{ name: 'srv', command: '/bin/true' }],
237
+ },
238
+ })
239
+ const events: string[] = []
240
+ const toolRegistry = {
241
+ register: vi.fn(),
242
+ unregister: vi.fn(() => events.push('unregister')),
243
+ listNames: vi.fn(() => []),
244
+ has: vi.fn(),
245
+ get: vi.fn(),
246
+ execute: vi.fn(),
247
+ getAvailability: vi.fn(),
248
+ } as unknown as ToolRegistryContract
249
+ mockDisconnect.mockImplementation(async () => {
250
+ events.push('disconnect')
251
+ })
252
+ const mgr = new PluginLifecycleManager({
253
+ pluginRegistry: registry,
254
+ toolRegistry,
255
+ log: makeLogger(),
256
+ })
257
+
258
+ await mgr.enable(pluginId)
259
+ await mgr.disable(pluginId)
260
+
261
+ expect(events).toEqual(['disconnect', 'unregister'])
262
+ })
263
+
264
+ it('rolls back tools and MCP clients when connect fails mid-enable', async () => {
265
+ mockConnect
266
+ .mockResolvedValueOnce(undefined) // first server connects
267
+ .mockRejectedValueOnce(new Error('connect refused')) // second fails
268
+ mockDisconnect.mockResolvedValue(undefined)
269
+ mockListTools.mockResolvedValue([{ name: 't', inputSchema: { type: 'object' } }])
270
+ const { registry } = makePluginRegistry({
271
+ manifest: {
272
+ name: 'multi',
273
+ version: '0.0.1',
274
+ description: 't',
275
+ mcpServers: [
276
+ { name: 'a', command: '/bin/true' },
277
+ { name: 'b', command: '/bin/false' },
278
+ ],
279
+ },
280
+ })
281
+ const toolRegistry = makeToolRegistry()
282
+ const mgr = new PluginLifecycleManager({
283
+ pluginRegistry: registry,
284
+ toolRegistry,
285
+ log: makeLogger(),
286
+ })
287
+
288
+ await expect(mgr.enable(pluginId)).rejects.toThrow(/connect refused/)
289
+
290
+ // Rollback: first server's tools unregistered, first client disconnected.
291
+ expect(toolRegistry.listNames()).toEqual([])
292
+ expect(mockDisconnect).toHaveBeenCalledOnce()
293
+ })
294
+ })
295
+ })
@@ -426,5 +426,49 @@ describe('PluginLifecycleManager', () => {
426
426
  await expect(executePromise).resolves.not.toThrow()
427
427
  })
428
428
  })
429
+
430
+ describe('RunEvent emission', () => {
431
+ it('should emit plugin_hook_executing and plugin_hook_completed when emitRunEvent provided', async () => {
432
+ const emitted: any[] = []
433
+ const emitRunEvent = vi.fn(async (event: any) => {
434
+ emitted.push(event)
435
+ })
436
+
437
+ const handler = vi.fn(
438
+ async (): Promise<PluginHookResult> => ({ action: 'modify', input: { x: 1 } }),
439
+ )
440
+
441
+ manager['hookHandlers'].set('pre_tool_use', [{ pluginId: mockPluginId, handler }])
442
+
443
+ await manager.executeHooks(
444
+ 'pre_tool_use',
445
+ { runId: mockRunId, toolName: 't', toolInput: {} },
446
+ emitRunEvent,
447
+ )
448
+
449
+ expect(emitted).toHaveLength(2)
450
+ expect(emitted[0]).toMatchObject({
451
+ type: 'plugin_hook_executing',
452
+ runId: mockRunId,
453
+ pluginId: mockPluginId,
454
+ hookEvent: 'pre_tool_use',
455
+ })
456
+ expect(emitted[1]).toMatchObject({
457
+ type: 'plugin_hook_completed',
458
+ runId: mockRunId,
459
+ pluginId: mockPluginId,
460
+ hookEvent: 'pre_tool_use',
461
+ })
462
+ expect(emitted[1].result).toEqual({ action: 'modify', input: { x: 1 } })
463
+ })
464
+
465
+ it('should not emit RunEvents when emitRunEvent omitted', async () => {
466
+ const handler = vi.fn(async (): Promise<PluginHookResult> => ({ action: 'continue' }))
467
+ manager['hookHandlers'].set('run_start', [{ pluginId: mockPluginId, handler }])
468
+ await expect(manager.executeHooks('run_start', { runId: mockRunId })).resolves.toHaveLength(
469
+ 1,
470
+ )
471
+ })
472
+ })
429
473
  })
430
474
  })
@@ -1,5 +1,7 @@
1
1
  import { join } from 'node:path'
2
2
  import { pathToFileURL } from 'node:url'
3
+ import { mcpToolToToolDefinition } from '../connector/mcp/adapter.js'
4
+ import { MCPClient } from '../connector/mcp/client.js'
3
5
  import { HOOK_TIMEOUT_MS, PLUGIN_NAMESPACE_SEPARATOR } from '../constants/plugin/index.js'
4
6
  import type { PluginRegistry } from '../registry/plugin/index.js'
5
7
  import type { PluginId } from '../types/ids/index.js'
@@ -11,14 +13,21 @@ import type {
11
13
  PluginHookEvent,
12
14
  PluginHookResult,
13
15
  PluginLifecycleEvent,
16
+ PluginMCPServerConfig,
14
17
  PluginScope,
15
18
  } from '../types/plugin/index.js'
19
+ import type { RunEvent } from '../types/run/index.js'
16
20
  import type { ToolDefinition, ToolRegistryContract } from '../types/tool/index.js'
17
21
  import { toErrorMessage } from '../utils/error.js'
18
22
  import { generatePluginId } from '../utils/id.js'
19
23
  import type { Logger } from '../utils/logger.js'
20
24
  import { loadPluginManifest } from './loader.js'
21
25
 
26
+ interface PluginContributionRecord {
27
+ toolNames: string[]
28
+ mcpClients: MCPClient[]
29
+ }
30
+
22
31
  export interface PluginLifecycleManagerConfig {
23
32
  pluginRegistry: PluginRegistry
24
33
  toolRegistry: ToolRegistryContract
@@ -34,7 +43,7 @@ export class PluginLifecycleManager {
34
43
  PluginHookEvent,
35
44
  Array<{ pluginId: PluginId; handler: PluginHookDefinition['handler'] }>
36
45
  > = new Map()
37
- private pluginContributions: Map<PluginId, string[]> = new Map()
46
+ private pluginContributions: Map<PluginId, PluginContributionRecord> = new Map()
38
47
  private hookTimeoutMs: number
39
48
  private log: Logger
40
49
 
@@ -99,70 +108,80 @@ export class PluginLifecycleManager {
99
108
  )
100
109
  }
101
110
 
102
- const registeredToolNames: string[] = []
103
111
  const { manifest } = plugin
104
112
 
105
- // Load tools
106
- if (manifest.tools && manifest.tools.length > 0) {
107
- for (const toolPath of manifest.tools) {
108
- const absolutePath = join(plugin.rootDir, toolPath)
109
- const fileUrl = pathToFileURL(absolutePath).href
110
- const mod = (await import(fileUrl)) as { tools?: ToolDefinition[] }
111
-
112
- if (!mod.tools || !Array.isArray(mod.tools)) {
113
- throw new Error(
114
- `Plugin "${manifest.name}": tool module "${toolPath}" must export a "tools" array`,
115
- )
116
- }
113
+ // Unsupported contribution types must fail fast (Convention #0, #5).
114
+ // SDK lacks removable registries / instance factories / persona loader design
115
+ // for these categories. Remove from manifest or upgrade runtime.
116
+ const unsupported: string[] = []
117
+ if (manifest.skills?.length) unsupported.push('skills')
118
+ if (manifest.connectors?.length) unsupported.push('connectors')
119
+ if (manifest.personas?.length) unsupported.push('personas')
120
+ if (unsupported.length > 0) {
121
+ throw new Error(
122
+ `Plugin "${manifest.name}": contribution type(s) [${unsupported.join(', ')}] not yet supported by the runtime. Remove from manifest or upgrade @namzu/sdk.`,
123
+ )
124
+ }
117
125
 
118
- for (const tool of mod.tools) {
119
- const namespacedName = manifest.name + PLUGIN_NAMESPACE_SEPARATOR + tool.name
120
- const namespacedTool: ToolDefinition = { ...tool, name: namespacedName }
121
- this.toolRegistry.register(namespacedTool, 'deferred')
122
- registeredToolNames.push(namespacedName)
126
+ const contributions: PluginContributionRecord = { toolNames: [], mcpClients: [] }
127
+
128
+ try {
129
+ // Load tools
130
+ if (manifest.tools && manifest.tools.length > 0) {
131
+ for (const toolPath of manifest.tools) {
132
+ const absolutePath = join(plugin.rootDir, toolPath)
133
+ const fileUrl = pathToFileURL(absolutePath).href
134
+ const mod = (await import(fileUrl)) as { tools?: ToolDefinition[] }
135
+
136
+ if (!mod.tools || !Array.isArray(mod.tools)) {
137
+ throw new Error(
138
+ `Plugin "${manifest.name}": tool module "${toolPath}" must export a "tools" array`,
139
+ )
140
+ }
141
+
142
+ for (const tool of mod.tools) {
143
+ const namespacedName = manifest.name + PLUGIN_NAMESPACE_SEPARATOR + tool.name
144
+ const namespacedTool: ToolDefinition = { ...tool, name: namespacedName }
145
+ this.toolRegistry.register(namespacedTool, 'deferred')
146
+ contributions.toolNames.push(namespacedName)
147
+ }
123
148
  }
124
149
  }
125
- }
126
150
 
127
- // Load hooks
128
- if (manifest.hooks && manifest.hooks.length > 0) {
129
- for (const hookPath of manifest.hooks) {
130
- const absolutePath = join(plugin.rootDir, hookPath)
131
- const fileUrl = pathToFileURL(absolutePath).href
132
- const mod = (await import(fileUrl)) as { hooks?: PluginHookDefinition[] }
133
-
134
- if (!mod.hooks || !Array.isArray(mod.hooks)) {
135
- throw new Error(
136
- `Plugin "${manifest.name}": hook module "${hookPath}" must export a "hooks" array`,
137
- )
151
+ // Load hooks
152
+ if (manifest.hooks && manifest.hooks.length > 0) {
153
+ for (const hookPath of manifest.hooks) {
154
+ const absolutePath = join(plugin.rootDir, hookPath)
155
+ const fileUrl = pathToFileURL(absolutePath).href
156
+ const mod = (await import(fileUrl)) as { hooks?: PluginHookDefinition[] }
157
+
158
+ if (!mod.hooks || !Array.isArray(mod.hooks)) {
159
+ throw new Error(
160
+ `Plugin "${manifest.name}": hook module "${hookPath}" must export a "hooks" array`,
161
+ )
162
+ }
163
+
164
+ for (const hook of mod.hooks) {
165
+ const handlers = this.hookHandlers.get(hook.event) ?? []
166
+ handlers.push({ pluginId, handler: hook.handler })
167
+ this.hookHandlers.set(hook.event, handlers)
168
+ }
138
169
  }
170
+ }
139
171
 
140
- for (const hook of mod.hooks) {
141
- const handlers = this.hookHandlers.get(hook.event) ?? []
142
- handlers.push({ pluginId, handler: hook.handler })
143
- this.hookHandlers.set(hook.event, handlers)
172
+ // Start MCP servers and adapt their tools
173
+ if (manifest.mcpServers && manifest.mcpServers.length > 0) {
174
+ for (const serverConfig of manifest.mcpServers) {
175
+ await this.attachMCPServer(manifest.name, serverConfig, contributions)
144
176
  }
145
177
  }
178
+ } catch (err) {
179
+ await this.rollbackContributions(pluginId, contributions)
180
+ throw err
146
181
  }
147
182
 
148
- // Log unsupported contribution types
149
- if (manifest.skills && manifest.skills.length > 0) {
150
- this.log.info(`Plugin "${manifest.name}": skills contribution not yet supported`)
151
- }
152
- if (manifest.mcpServers && manifest.mcpServers.length > 0) {
153
- this.log.info(`Plugin "${manifest.name}": MCP servers contribution not yet supported`)
154
- }
155
- if (manifest.connectors && manifest.connectors.length > 0) {
156
- this.log.info(`Plugin "${manifest.name}": connectors contribution not yet supported`)
157
- }
158
- if (manifest.personas && manifest.personas.length > 0) {
159
- this.log.info(`Plugin "${manifest.name}": personas contribution not yet supported`)
160
- }
161
-
162
- // Track contributions for cleanup
163
- this.pluginContributions.set(pluginId, registeredToolNames)
183
+ this.pluginContributions.set(pluginId, contributions)
164
184
 
165
- // Update status to enabled
166
185
  const enabled: PluginDefinition = {
167
186
  ...plugin,
168
187
  status: 'enabled',
@@ -178,10 +197,76 @@ export class PluginLifecycleManager {
178
197
 
179
198
  this.log.info(`Plugin enabled: ${manifest.name}`, {
180
199
  pluginId,
181
- toolCount: registeredToolNames.length,
200
+ toolCount: contributions.toolNames.length,
201
+ mcpServerCount: contributions.mcpClients.length,
182
202
  })
183
203
  }
184
204
 
205
+ private async attachMCPServer(
206
+ pluginName: string,
207
+ config: PluginMCPServerConfig,
208
+ contributions: PluginContributionRecord,
209
+ ): Promise<void> {
210
+ const client = new MCPClient({
211
+ serverName: config.name,
212
+ transport: {
213
+ type: 'stdio',
214
+ command: config.command,
215
+ args: config.args ? [...config.args] : undefined,
216
+ env: config.env ? { ...config.env } : undefined,
217
+ },
218
+ })
219
+
220
+ await client.connect()
221
+ contributions.mcpClients.push(client)
222
+
223
+ const mcpTools = await client.listTools()
224
+ for (const mcpTool of mcpTools) {
225
+ const baseDef = mcpToolToToolDefinition(mcpTool, client, config.name)
226
+ // Double-underscore between serverName and toolName so that e.g.
227
+ // (server="fs", tool="read_file") and (server="fs_read", tool="file")
228
+ // produce distinct final names.
229
+ const namespacedName = `${pluginName}${PLUGIN_NAMESPACE_SEPARATOR}mcp__${config.name}__${mcpTool.name}`
230
+ const namespacedTool: ToolDefinition = { ...baseDef, name: namespacedName }
231
+ this.toolRegistry.register(namespacedTool, 'deferred')
232
+ contributions.toolNames.push(namespacedName)
233
+ }
234
+ }
235
+
236
+ private async rollbackContributions(
237
+ pluginId: PluginId,
238
+ contributions: PluginContributionRecord,
239
+ ): Promise<void> {
240
+ for (const name of contributions.toolNames) {
241
+ try {
242
+ this.toolRegistry.unregister(name)
243
+ } catch (unregErr) {
244
+ this.log.warn('Rollback: tool unregister failed', {
245
+ tool: name,
246
+ error: toErrorMessage(unregErr),
247
+ })
248
+ }
249
+ }
250
+ for (const client of contributions.mcpClients) {
251
+ try {
252
+ await client.disconnect()
253
+ } catch (discErr) {
254
+ this.log.warn('Rollback: MCP disconnect failed', {
255
+ clientId: client.id,
256
+ error: toErrorMessage(discErr),
257
+ })
258
+ }
259
+ }
260
+ for (const [event, handlers] of this.hookHandlers) {
261
+ const filtered = handlers.filter((h) => h.pluginId !== pluginId)
262
+ if (filtered.length === 0) {
263
+ this.hookHandlers.delete(event)
264
+ } else {
265
+ this.hookHandlers.set(event, filtered)
266
+ }
267
+ }
268
+ }
269
+
185
270
  async disable(pluginId: PluginId): Promise<void> {
186
271
  const plugin = this.pluginRegistry.getOrThrow(pluginId)
187
272
 
@@ -191,9 +276,25 @@ export class PluginLifecycleManager {
191
276
  )
192
277
  }
193
278
 
194
- // Unregister contributed tools
195
- const toolNames = this.pluginContributions.get(pluginId) ?? []
196
- for (const name of toolNames) {
279
+ const contributions = this.pluginContributions.get(pluginId) ?? {
280
+ toolNames: [],
281
+ mcpClients: [],
282
+ }
283
+
284
+ // Disconnect MCP clients first so no new tool calls can reach them mid-teardown.
285
+ for (const client of contributions.mcpClients) {
286
+ try {
287
+ await client.disconnect()
288
+ } catch (err) {
289
+ this.log.warn('MCP client disconnect failed during disable', {
290
+ clientId: client.id,
291
+ error: toErrorMessage(err),
292
+ })
293
+ }
294
+ }
295
+
296
+ // Unregister contributed tools (plugin tools + MCP-adapted tools)
297
+ for (const name of contributions.toolNames) {
197
298
  this.toolRegistry.unregister(name)
198
299
  }
199
300
 
@@ -207,7 +308,6 @@ export class PluginLifecycleManager {
207
308
  }
208
309
  }
209
310
 
210
- // Clear contributions tracking
211
311
  this.pluginContributions.delete(pluginId)
212
312
 
213
313
  // Update status to disabled
@@ -248,6 +348,7 @@ export class PluginLifecycleManager {
248
348
  async executeHooks(
249
349
  event: PluginHookEvent,
250
350
  context: Omit<PluginHookContext, 'pluginId' | 'event'>,
351
+ emitRunEvent?: (event: RunEvent) => Promise<void>,
251
352
  ): Promise<PluginHookResult[]> {
252
353
  const handlers = this.hookHandlers.get(event)
253
354
  if (!handlers || handlers.length === 0) {
@@ -271,16 +372,30 @@ export class PluginLifecycleManager {
271
372
  }
272
373
  }
273
374
 
375
+ // Track input overlay so chained `modify` actions compose: each subsequent
376
+ // hook sees the input produced by the previous hook's modify.
377
+ let toolInputOverlay = context.toolInput
378
+
274
379
  for (const idx of indicesToProcess) {
275
380
  const hookEntry = handlers[idx]
276
381
  if (!hookEntry) continue
277
382
  const { pluginId, handler: handlerFn } = hookEntry
278
383
  const hookContext: PluginHookContext = {
279
384
  ...context,
385
+ toolInput: toolInputOverlay,
280
386
  pluginId,
281
387
  event,
282
388
  }
283
389
 
390
+ if (emitRunEvent) {
391
+ await emitRunEvent({
392
+ type: 'plugin_hook_executing',
393
+ runId: context.runId,
394
+ pluginId,
395
+ hookEvent: event,
396
+ })
397
+ }
398
+
284
399
  const start = performance.now()
285
400
  let result: PluginHookResult
286
401
 
@@ -305,8 +420,22 @@ export class PluginLifecycleManager {
305
420
  durationMs,
306
421
  })
307
422
 
423
+ if (emitRunEvent) {
424
+ await emitRunEvent({
425
+ type: 'plugin_hook_completed',
426
+ runId: context.runId,
427
+ pluginId,
428
+ hookEvent: event,
429
+ result,
430
+ })
431
+ }
432
+
308
433
  results.push(result)
309
434
 
435
+ if (result.action === 'modify') {
436
+ toolInputOverlay = result.input
437
+ }
438
+
310
439
  // Handle flow control: check priority order: error > skip > retry > resume > modify > continue
311
440
  // Short-circuit on error or skip; return immediately on resume or retry
312
441
  if (result.action === 'error' || result.action === 'skip') {