@johpaz/hive-core 1.0.8 → 1.0.10
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.
- package/package.json +10 -9
- package/src/agent/ethics.ts +70 -68
- package/src/agent/index.ts +48 -17
- package/src/agent/providers/index.ts +11 -5
- package/src/agent/soul.ts +19 -15
- package/src/agent/user.ts +19 -15
- package/src/agent/workspace.ts +6 -6
- package/src/agents/index.ts +4 -0
- package/src/agents/inter-agent-bus.test.ts +264 -0
- package/src/agents/inter-agent-bus.ts +279 -0
- package/src/agents/registry.test.ts +275 -0
- package/src/agents/registry.ts +273 -0
- package/src/agents/router.test.ts +229 -0
- package/src/agents/router.ts +251 -0
- package/src/agents/team-coordinator.test.ts +401 -0
- package/src/agents/team-coordinator.ts +480 -0
- package/src/canvas/canvas-manager.test.ts +159 -0
- package/src/canvas/canvas-manager.ts +219 -0
- package/src/canvas/canvas-tools.ts +189 -0
- package/src/canvas/index.ts +2 -0
- package/src/channels/whatsapp.ts +12 -12
- package/src/config/loader.ts +12 -9
- package/src/events/event-bus.test.ts +98 -0
- package/src/events/event-bus.ts +171 -0
- package/src/gateway/server.ts +131 -35
- package/src/index.ts +9 -1
- package/src/multi-agent/manager.ts +12 -12
- package/src/plugins/api.ts +129 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/loader.test.ts +285 -0
- package/src/plugins/loader.ts +363 -0
- package/src/resilience/circuit-breaker.test.ts +129 -0
- package/src/resilience/circuit-breaker.ts +223 -0
- package/src/security/google-chat.test.ts +219 -0
- package/src/security/google-chat.ts +269 -0
- package/src/security/index.ts +5 -0
- package/src/security/pairing.test.ts +302 -0
- package/src/security/pairing.ts +250 -0
- package/src/security/rate-limit.test.ts +239 -0
- package/src/security/rate-limit.ts +270 -0
- package/src/security/signal.test.ts +92 -0
- package/src/security/signal.ts +321 -0
- package/src/state/store.test.ts +190 -0
- package/src/state/store.ts +310 -0
- package/src/storage/sqlite.ts +3 -3
- package/src/tools/cron.ts +42 -2
- package/src/tools/dynamic-registry.test.ts +226 -0
- package/src/tools/dynamic-registry.ts +258 -0
- package/src/tools/fs.test.ts +127 -0
- package/src/tools/fs.ts +364 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/read.ts +23 -19
- package/src/utils/logger.ts +112 -33
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Logger, ChildLogger } from "../utils/logger.ts";
|
|
2
|
+
import type { eventBus, EventMap, EventKey } from "../events/event-bus.ts";
|
|
3
|
+
import type { StateStore } from "../state/store.ts";
|
|
4
|
+
import type { Tool } from "../tools/registry.ts";
|
|
5
|
+
|
|
6
|
+
export interface PluginManifest {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
author?: string;
|
|
11
|
+
dependencies?: string[];
|
|
12
|
+
hiveVersion?: string;
|
|
13
|
+
main?: string;
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ToolDefinition {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
parameters: Record<string, ParameterDefinition>;
|
|
21
|
+
execute: (args: Record<string, unknown>) => Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ParameterDefinition {
|
|
25
|
+
type: "string" | "number" | "boolean" | "object" | "array";
|
|
26
|
+
description?: string;
|
|
27
|
+
required?: boolean;
|
|
28
|
+
default?: unknown;
|
|
29
|
+
enum?: string[];
|
|
30
|
+
items?: ParameterDefinition;
|
|
31
|
+
properties?: Record<string, ParameterDefinition>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ChannelDefinition {
|
|
35
|
+
name: string;
|
|
36
|
+
type: string;
|
|
37
|
+
config: Record<string, unknown>;
|
|
38
|
+
start: () => Promise<void>;
|
|
39
|
+
stop: () => Promise<void>;
|
|
40
|
+
send: (sessionId: string, message: unknown) => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CLICommand {
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
handler: (args: string[], options: Record<string, unknown>) => Promise<void>;
|
|
47
|
+
options?: Record<string, CommandOption>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CommandOption {
|
|
51
|
+
alias?: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
type?: "string" | "boolean" | "number";
|
|
54
|
+
default?: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PluginContext {
|
|
58
|
+
pluginName: string;
|
|
59
|
+
logger: ChildLogger;
|
|
60
|
+
config: Record<string, unknown>;
|
|
61
|
+
registerTool: (tool: ToolDefinition) => void;
|
|
62
|
+
unregisterTool: (name: string) => void;
|
|
63
|
+
registerChannel: (channel: ChannelDefinition) => void;
|
|
64
|
+
unregisterChannel: (name: string) => void;
|
|
65
|
+
registerCommand: (command: CLICommand) => void;
|
|
66
|
+
unregisterCommand: (name: string) => void;
|
|
67
|
+
events: {
|
|
68
|
+
emit: <K extends EventKey>(event: K, data: EventMap[K]) => void;
|
|
69
|
+
on: <K extends EventKey>(event: K, handler: (data: EventMap[K]) => void | Promise<void>) => () => void;
|
|
70
|
+
once: <K extends EventKey>(event: K, handler: (data: EventMap[K]) => void | Promise<void>) => void;
|
|
71
|
+
};
|
|
72
|
+
state: {
|
|
73
|
+
get: () => ReturnType<StateStore["getState"]>;
|
|
74
|
+
subscribe: (listener: (state: ReturnType<StateStore["getState"]>) => void) => () => void;
|
|
75
|
+
};
|
|
76
|
+
getTool: (name: string) => ToolDefinition | undefined;
|
|
77
|
+
getTools: () => ToolDefinition[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface InboundMessage {
|
|
81
|
+
sessionId: string;
|
|
82
|
+
channel: string;
|
|
83
|
+
userId: string;
|
|
84
|
+
content: string;
|
|
85
|
+
timestamp: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface AgentResponse {
|
|
89
|
+
sessionId: string;
|
|
90
|
+
content: string;
|
|
91
|
+
toolsUsed: string[];
|
|
92
|
+
duration: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface PluginToolCall {
|
|
96
|
+
name: string;
|
|
97
|
+
args: Record<string, unknown>;
|
|
98
|
+
sessionId: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type MiddlewareNext = () => Promise<void>;
|
|
102
|
+
export type MiddlewareResult = Promise<void>;
|
|
103
|
+
|
|
104
|
+
export interface HivePlugin {
|
|
105
|
+
name: string;
|
|
106
|
+
version: string;
|
|
107
|
+
dependencies?: string[];
|
|
108
|
+
manifest?: PluginManifest;
|
|
109
|
+
|
|
110
|
+
activate(context: PluginContext): Promise<void>;
|
|
111
|
+
deactivate(): Promise<void>;
|
|
112
|
+
|
|
113
|
+
onMessage?: (message: InboundMessage, next: MiddlewareNext) => MiddlewareResult;
|
|
114
|
+
onAgentResponse?: (response: AgentResponse, next: MiddlewareNext) => MiddlewareResult;
|
|
115
|
+
onToolCall?: (call: PluginToolCall, next: () => Promise<unknown>) => Promise<unknown>;
|
|
116
|
+
onError?: (error: Error, context: Record<string, unknown>) => Promise<void>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface PluginState {
|
|
120
|
+
name: string;
|
|
121
|
+
status: "inactive" | "activating" | "active" | "deactivating" | "error";
|
|
122
|
+
version: string;
|
|
123
|
+
enabled: boolean;
|
|
124
|
+
error?: string;
|
|
125
|
+
loadedAt?: number;
|
|
126
|
+
activatedAt?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type PluginConstructor = new () => HivePlugin;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { PluginLoader } from "../plugins/loader.ts";
|
|
5
|
+
|
|
6
|
+
const TEST_PLUGIN_DIR = "/tmp/hive-plugins-test";
|
|
7
|
+
|
|
8
|
+
describe("PluginLoader", () => {
|
|
9
|
+
let loader: PluginLoader;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
if (fs.existsSync(TEST_PLUGIN_DIR)) {
|
|
13
|
+
fs.rmSync(TEST_PLUGIN_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
fs.mkdirSync(TEST_PLUGIN_DIR, { recursive: true });
|
|
16
|
+
loader = new PluginLoader({ pluginDir: TEST_PLUGIN_DIR, enableSandbox: false });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (fs.existsSync(TEST_PLUGIN_DIR)) {
|
|
21
|
+
fs.rmSync(TEST_PLUGIN_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should discover plugins", async () => {
|
|
26
|
+
const pluginName = "test-plugin";
|
|
27
|
+
const pluginPath = path.join(TEST_PLUGIN_DIR, pluginName);
|
|
28
|
+
fs.mkdirSync(pluginPath, { recursive: true });
|
|
29
|
+
|
|
30
|
+
fs.writeFileSync(
|
|
31
|
+
path.join(pluginPath, "manifest.json"),
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
name: pluginName,
|
|
34
|
+
version: "1.0.0",
|
|
35
|
+
main: "index.js",
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const discovered = await loader.discover();
|
|
40
|
+
expect(discovered).toContain(pluginName);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should load a valid plugin", async () => {
|
|
44
|
+
const pluginName = "echo-plugin";
|
|
45
|
+
const pluginPath = path.join(TEST_PLUGIN_DIR, pluginName);
|
|
46
|
+
fs.mkdirSync(pluginPath, { recursive: true });
|
|
47
|
+
|
|
48
|
+
fs.writeFileSync(
|
|
49
|
+
path.join(pluginPath, "manifest.json"),
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
name: pluginName,
|
|
52
|
+
version: "1.0.0",
|
|
53
|
+
main: "index.js",
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
path.join(pluginPath, "index.js"),
|
|
59
|
+
`
|
|
60
|
+
class EchoPlugin {
|
|
61
|
+
name = 'echo-plugin';
|
|
62
|
+
version = '1.0.0';
|
|
63
|
+
|
|
64
|
+
async activate(context) {
|
|
65
|
+
context.registerTool({
|
|
66
|
+
name: 'echo',
|
|
67
|
+
description: 'Echoes input',
|
|
68
|
+
parameters: {
|
|
69
|
+
message: { type: 'string', description: 'Message to echo' }
|
|
70
|
+
},
|
|
71
|
+
execute: async ({ message }) => message
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async deactivate() {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default EchoPlugin;
|
|
79
|
+
`
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await loader.load(pluginName);
|
|
83
|
+
|
|
84
|
+
const tool = loader.getTool("echo-plugin:echo");
|
|
85
|
+
expect(tool).toBeDefined();
|
|
86
|
+
expect(tool?.description).toBe("Echoes input");
|
|
87
|
+
|
|
88
|
+
const result = await tool?.execute({ message: "hello" });
|
|
89
|
+
expect(result).toBe("hello");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should track plugin state", async () => {
|
|
93
|
+
const pluginName = "state-plugin";
|
|
94
|
+
const pluginPath = path.join(TEST_PLUGIN_DIR, pluginName);
|
|
95
|
+
fs.mkdirSync(pluginPath, { recursive: true });
|
|
96
|
+
|
|
97
|
+
fs.writeFileSync(
|
|
98
|
+
path.join(pluginPath, "manifest.json"),
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
name: pluginName,
|
|
101
|
+
version: "2.0.0",
|
|
102
|
+
main: "index.js",
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
path.join(pluginPath, "index.js"),
|
|
108
|
+
`
|
|
109
|
+
class StatePlugin {
|
|
110
|
+
name = 'state-plugin';
|
|
111
|
+
version = '2.0.0';
|
|
112
|
+
async activate() {}
|
|
113
|
+
async deactivate() {}
|
|
114
|
+
}
|
|
115
|
+
export default StatePlugin;
|
|
116
|
+
`
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
await loader.load(pluginName);
|
|
120
|
+
|
|
121
|
+
const state = loader.getPluginState(pluginName);
|
|
122
|
+
expect(state?.status).toBe("active");
|
|
123
|
+
expect(state?.version).toBe("2.0.0");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should unload plugin and cleanup tools", async () => {
|
|
127
|
+
const pluginName = "cleanup-plugin";
|
|
128
|
+
const pluginPath = path.join(TEST_PLUGIN_DIR, pluginName);
|
|
129
|
+
fs.mkdirSync(pluginPath, { recursive: true });
|
|
130
|
+
|
|
131
|
+
fs.writeFileSync(
|
|
132
|
+
path.join(pluginPath, "manifest.json"),
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
name: pluginName,
|
|
135
|
+
version: "1.0.0",
|
|
136
|
+
main: "index.js",
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
fs.writeFileSync(
|
|
141
|
+
path.join(pluginPath, "index.js"),
|
|
142
|
+
`
|
|
143
|
+
class CleanupPlugin {
|
|
144
|
+
name = 'cleanup-plugin';
|
|
145
|
+
version = '1.0.0';
|
|
146
|
+
async activate(ctx) {
|
|
147
|
+
ctx.registerTool({
|
|
148
|
+
name: 'test_tool',
|
|
149
|
+
description: 'Test',
|
|
150
|
+
parameters: {},
|
|
151
|
+
execute: async () => 'ok'
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async deactivate() {}
|
|
155
|
+
}
|
|
156
|
+
export default CleanupPlugin;
|
|
157
|
+
`
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await loader.load(pluginName);
|
|
161
|
+
expect(loader.getTool("cleanup-plugin:test_tool")).toBeDefined();
|
|
162
|
+
|
|
163
|
+
await loader.unload(pluginName);
|
|
164
|
+
|
|
165
|
+
expect(loader.getTool("cleanup-plugin:test_tool")).toBeUndefined();
|
|
166
|
+
const state = loader.getPluginState(pluginName);
|
|
167
|
+
expect(state?.status).toBe("inactive");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should reload plugin", async () => {
|
|
171
|
+
const pluginName = "reload-plugin";
|
|
172
|
+
const pluginPath = path.join(TEST_PLUGIN_DIR, pluginName);
|
|
173
|
+
fs.mkdirSync(pluginPath, { recursive: true });
|
|
174
|
+
|
|
175
|
+
fs.writeFileSync(
|
|
176
|
+
path.join(pluginPath, "manifest.json"),
|
|
177
|
+
JSON.stringify({
|
|
178
|
+
name: pluginName,
|
|
179
|
+
version: "1.0.0",
|
|
180
|
+
main: "index.js",
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
fs.writeFileSync(
|
|
185
|
+
path.join(pluginPath, "index.js"),
|
|
186
|
+
`
|
|
187
|
+
class ReloadPlugin {
|
|
188
|
+
name = 'reload-plugin';
|
|
189
|
+
version = '1.0.0';
|
|
190
|
+
async activate() {}
|
|
191
|
+
async deactivate() {}
|
|
192
|
+
}
|
|
193
|
+
export default ReloadPlugin;
|
|
194
|
+
`
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
await loader.load(pluginName);
|
|
198
|
+
expect(loader.getPlugin(pluginName)).toBeDefined();
|
|
199
|
+
|
|
200
|
+
await loader.reload(pluginName);
|
|
201
|
+
|
|
202
|
+
expect(loader.getPlugin(pluginName)).toBeDefined();
|
|
203
|
+
const state = loader.getPluginState(pluginName);
|
|
204
|
+
expect(state?.status).toBe("active");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should handle disabled plugins", async () => {
|
|
208
|
+
const pluginName = "disabled-plugin";
|
|
209
|
+
const pluginPath = path.join(TEST_PLUGIN_DIR, pluginName);
|
|
210
|
+
fs.mkdirSync(pluginPath, { recursive: true });
|
|
211
|
+
|
|
212
|
+
fs.writeFileSync(
|
|
213
|
+
path.join(pluginPath, "manifest.json"),
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
name: pluginName,
|
|
216
|
+
version: "1.0.0",
|
|
217
|
+
main: "index.js",
|
|
218
|
+
enabled: false,
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
fs.writeFileSync(
|
|
223
|
+
path.join(pluginPath, "index.js"),
|
|
224
|
+
`class Disabled { name = 'disabled'; version = '1.0.0'; async activate() {} async deactivate() {} }
|
|
225
|
+
export default Disabled;`
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await loader.load(pluginName);
|
|
229
|
+
|
|
230
|
+
const state = loader.getPluginState(pluginName);
|
|
231
|
+
expect(state?.status).toBe("inactive");
|
|
232
|
+
expect(loader.getPlugin(pluginName)).toBeUndefined();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should handle missing main file", async () => {
|
|
236
|
+
const pluginName = "missing-main";
|
|
237
|
+
const pluginPath = path.join(TEST_PLUGIN_DIR, pluginName);
|
|
238
|
+
fs.mkdirSync(pluginPath, { recursive: true });
|
|
239
|
+
|
|
240
|
+
fs.writeFileSync(
|
|
241
|
+
path.join(pluginPath, "manifest.json"),
|
|
242
|
+
JSON.stringify({
|
|
243
|
+
name: pluginName,
|
|
244
|
+
version: "1.0.0",
|
|
245
|
+
main: "nonexistent.js",
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
await expect(loader.load(pluginName)).rejects.toThrow();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("should list all plugin states", async () => {
|
|
253
|
+
const states = loader.getAllPluginStates();
|
|
254
|
+
expect(Array.isArray(states)).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should load all discovered plugins", async () => {
|
|
258
|
+
const plugin1Path = path.join(TEST_PLUGIN_DIR, "plugin-one");
|
|
259
|
+
fs.mkdirSync(plugin1Path, { recursive: true });
|
|
260
|
+
fs.writeFileSync(
|
|
261
|
+
path.join(plugin1Path, "manifest.json"),
|
|
262
|
+
JSON.stringify({ name: "plugin-one", version: "1.0.0", main: "index.js" })
|
|
263
|
+
);
|
|
264
|
+
fs.writeFileSync(
|
|
265
|
+
path.join(plugin1Path, "index.js"),
|
|
266
|
+
`class P1 { name='plugin-one'; version='1.0.0'; async activate(){} async deactivate(){} } export default P1;`
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const plugin2Path = path.join(TEST_PLUGIN_DIR, "plugin-two");
|
|
270
|
+
fs.mkdirSync(plugin2Path, { recursive: true });
|
|
271
|
+
fs.writeFileSync(
|
|
272
|
+
path.join(plugin2Path, "manifest.json"),
|
|
273
|
+
JSON.stringify({ name: "plugin-two", version: "1.0.0", main: "index.js" })
|
|
274
|
+
);
|
|
275
|
+
fs.writeFileSync(
|
|
276
|
+
path.join(plugin2Path, "index.js"),
|
|
277
|
+
`class P2 { name='plugin-two'; version='1.0.0'; async activate(){} async deactivate(){} } export default P2;`
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
await loader.loadAll();
|
|
281
|
+
|
|
282
|
+
const states = loader.getAllPluginStates();
|
|
283
|
+
expect(states.length).toBe(2);
|
|
284
|
+
});
|
|
285
|
+
});
|