@johpaz/hive-core 1.0.7 → 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.
Files changed (53) hide show
  1. package/package.json +10 -9
  2. package/src/agent/ethics.ts +70 -68
  3. package/src/agent/index.ts +48 -17
  4. package/src/agent/providers/index.ts +11 -5
  5. package/src/agent/soul.ts +19 -15
  6. package/src/agent/user.ts +19 -15
  7. package/src/agent/workspace.ts +6 -6
  8. package/src/agents/index.ts +4 -0
  9. package/src/agents/inter-agent-bus.test.ts +264 -0
  10. package/src/agents/inter-agent-bus.ts +279 -0
  11. package/src/agents/registry.test.ts +275 -0
  12. package/src/agents/registry.ts +273 -0
  13. package/src/agents/router.test.ts +229 -0
  14. package/src/agents/router.ts +251 -0
  15. package/src/agents/team-coordinator.test.ts +401 -0
  16. package/src/agents/team-coordinator.ts +480 -0
  17. package/src/canvas/canvas-manager.test.ts +159 -0
  18. package/src/canvas/canvas-manager.ts +219 -0
  19. package/src/canvas/canvas-tools.ts +189 -0
  20. package/src/canvas/index.ts +2 -0
  21. package/src/channels/whatsapp.ts +12 -12
  22. package/src/config/loader.ts +12 -9
  23. package/src/events/event-bus.test.ts +98 -0
  24. package/src/events/event-bus.ts +171 -0
  25. package/src/gateway/server.ts +131 -35
  26. package/src/index.ts +9 -1
  27. package/src/multi-agent/manager.ts +12 -12
  28. package/src/plugins/api.ts +129 -0
  29. package/src/plugins/index.ts +2 -0
  30. package/src/plugins/loader.test.ts +285 -0
  31. package/src/plugins/loader.ts +363 -0
  32. package/src/resilience/circuit-breaker.test.ts +129 -0
  33. package/src/resilience/circuit-breaker.ts +223 -0
  34. package/src/security/google-chat.test.ts +219 -0
  35. package/src/security/google-chat.ts +269 -0
  36. package/src/security/index.ts +5 -0
  37. package/src/security/pairing.test.ts +302 -0
  38. package/src/security/pairing.ts +250 -0
  39. package/src/security/rate-limit.test.ts +239 -0
  40. package/src/security/rate-limit.ts +270 -0
  41. package/src/security/signal.test.ts +92 -0
  42. package/src/security/signal.ts +321 -0
  43. package/src/state/store.test.ts +190 -0
  44. package/src/state/store.ts +310 -0
  45. package/src/storage/sqlite.ts +3 -3
  46. package/src/tools/cron.ts +42 -2
  47. package/src/tools/dynamic-registry.test.ts +226 -0
  48. package/src/tools/dynamic-registry.ts +258 -0
  49. package/src/tools/fs.test.ts +127 -0
  50. package/src/tools/fs.ts +364 -0
  51. package/src/tools/index.ts +1 -0
  52. package/src/tools/read.ts +23 -19
  53. package/src/utils/logger.ts +112 -33
@@ -0,0 +1,258 @@
1
+ import { z } from "zod";
2
+ import { eventBus } from "../events/event-bus.ts";
3
+ import { logger } from "../utils/logger.ts";
4
+ import type { ToolDefinition, ParameterDefinition } from "../plugins/api.ts";
5
+ import type { Tool } from "./registry.ts";
6
+
7
+ function parameterToZod(param: ParameterDefinition): z.ZodTypeAny {
8
+ let schema: z.ZodTypeAny;
9
+
10
+ switch (param.type) {
11
+ case "string":
12
+ schema = z.string();
13
+ if (param.enum && param.enum.length > 0) {
14
+ schema = z.enum([param.enum[0]!, ...param.enum.slice(1)]);
15
+ }
16
+ break;
17
+ case "number":
18
+ schema = z.number();
19
+ break;
20
+ case "boolean":
21
+ schema = z.boolean();
22
+ break;
23
+ case "array":
24
+ schema = param.items
25
+ ? z.array(parameterToZod(param.items))
26
+ : z.array(z.unknown());
27
+ break;
28
+ case "object":
29
+ if (param.properties) {
30
+ const shape: Record<string, z.ZodTypeAny> = {};
31
+ for (const [key, value] of Object.entries(param.properties)) {
32
+ shape[key] = parameterToZod(value);
33
+ }
34
+ schema = z.object(shape);
35
+ } else {
36
+ schema = z.record(z.string(), z.unknown());
37
+ }
38
+ break;
39
+ default:
40
+ schema = z.unknown();
41
+ }
42
+
43
+ if (!param.required && param.default === undefined) {
44
+ schema = schema.optional();
45
+ }
46
+
47
+ return schema;
48
+ }
49
+
50
+ function buildSchemaFromParameters(
51
+ parameters: Record<string, ParameterDefinition>
52
+ ): z.ZodObject<any> {
53
+ const shape: Record<string, z.ZodTypeAny> = {};
54
+
55
+ for (const [key, param] of Object.entries(parameters)) {
56
+ shape[key] = parameterToZod(param);
57
+ }
58
+
59
+ return z.object(shape);
60
+ }
61
+
62
+ export interface RegisteredTool extends ToolDefinition {
63
+ schema: z.ZodObject<any>;
64
+ registeredAt: number;
65
+ pluginName?: string;
66
+ }
67
+
68
+ export class DynamicToolRegistry {
69
+ private tools: Map<string, RegisteredTool> = new Map();
70
+ private log = logger.child("tools");
71
+
72
+ register(definition: ToolDefinition, pluginName?: string): void {
73
+ if (this.tools.has(definition.name)) {
74
+ this.log.warn(`Tool ${definition.name} already registered, replacing`);
75
+ }
76
+
77
+ const schema = buildSchemaFromParameters(definition.parameters);
78
+
79
+ const wrappedExecute = this.wrapWithValidation(
80
+ definition.execute,
81
+ schema,
82
+ definition.name
83
+ );
84
+
85
+ const registered: RegisteredTool = {
86
+ ...definition,
87
+ execute: wrappedExecute,
88
+ schema,
89
+ registeredAt: Date.now(),
90
+ pluginName,
91
+ };
92
+
93
+ this.tools.set(definition.name, registered);
94
+
95
+ eventBus.emit("tool:completed", {
96
+ toolName: definition.name,
97
+ result: { registered: true },
98
+ duration: 0,
99
+ success: true,
100
+ });
101
+
102
+ this.log.info(`Tool registered: ${definition.name}`, {
103
+ plugin: pluginName ?? "core",
104
+ });
105
+ }
106
+
107
+ unregister(name: string): void {
108
+ if (!this.tools.has(name)) {
109
+ this.log.warn(`Tool ${name} not found, cannot unregister`);
110
+ return;
111
+ }
112
+
113
+ this.tools.delete(name);
114
+
115
+ this.log.info(`Tool unregistered: ${name}`);
116
+ }
117
+
118
+ private wrapWithValidation(
119
+ fn: (args: Record<string, unknown>) => Promise<unknown>,
120
+ schema: z.ZodObject<any>,
121
+ toolName: string
122
+ ): (args: Record<string, unknown>) => Promise<unknown> {
123
+ return async (args: Record<string, unknown>) => {
124
+ const startTime = Date.now();
125
+
126
+ try {
127
+ const validated = schema.parse(args);
128
+ const result = await fn(validated);
129
+
130
+ const duration = Date.now() - startTime;
131
+ eventBus.emit("tool:completed", {
132
+ toolName,
133
+ result,
134
+ duration,
135
+ success: true,
136
+ });
137
+
138
+ return result;
139
+ } catch (error) {
140
+ const duration = Date.now() - startTime;
141
+
142
+ if (error instanceof z.ZodError) {
143
+ const zodErr = error as z.ZodError;
144
+ const errorMessages = zodErr.issues
145
+ .map((e: z.ZodIssue) => `${e.path.join(".")}: ${e.message}`)
146
+ .join(", ");
147
+
148
+ const validationError = new Error(
149
+ `Validation failed for tool ${toolName}: ${errorMessages}`
150
+ );
151
+ (validationError as any).zodError = error;
152
+
153
+ eventBus.emit("tool:error", {
154
+ toolName,
155
+ error: validationError,
156
+ args,
157
+ });
158
+
159
+ throw validationError;
160
+ }
161
+
162
+ eventBus.emit("tool:error", {
163
+ toolName,
164
+ error: error as Error,
165
+ args,
166
+ });
167
+
168
+ throw error;
169
+ }
170
+ };
171
+ }
172
+
173
+ get(name: string): RegisteredTool | undefined {
174
+ return this.tools.get(name);
175
+ }
176
+
177
+ has(name: string): boolean {
178
+ return this.tools.has(name);
179
+ }
180
+
181
+ list(): RegisteredTool[] {
182
+ return Array.from(this.tools.values());
183
+ }
184
+
185
+ listByPlugin(pluginName: string): RegisteredTool[] {
186
+ return this.list().filter((t) => t.pluginName === pluginName);
187
+ }
188
+
189
+ listNames(): string[] {
190
+ return Array.from(this.tools.keys());
191
+ }
192
+
193
+ clear(): void {
194
+ this.tools.clear();
195
+ this.log.info("All tools cleared");
196
+ }
197
+
198
+ toToolInterface(name: string): Tool | undefined {
199
+ const registered = this.tools.get(name);
200
+ if (!registered) return undefined;
201
+
202
+ return {
203
+ name: registered.name,
204
+ description: registered.description,
205
+ parameters: {
206
+ type: "object",
207
+ properties: this.parametersToProperties(registered.parameters),
208
+ required: Object.entries(registered.parameters)
209
+ .filter(([, p]) => p.required)
210
+ .map(([key]) => key),
211
+ },
212
+ execute: registered.execute,
213
+ };
214
+ }
215
+
216
+ private parametersToProperties(
217
+ parameters: Record<string, ParameterDefinition>
218
+ ): Record<string, { type: string; description?: string }> {
219
+ const properties: Record<string, { type: string; description?: string }> = {};
220
+
221
+ for (const [key, param] of Object.entries(parameters)) {
222
+ properties[key] = {
223
+ type: param.type,
224
+ description: param.description,
225
+ };
226
+ }
227
+
228
+ return properties;
229
+ }
230
+
231
+ getStats(): {
232
+ total: number;
233
+ byPlugin: Record<string, number>;
234
+ oldestRegistration?: number;
235
+ newestRegistration?: number;
236
+ } {
237
+ const byPlugin: Record<string, number> = {};
238
+ let oldest: number | undefined;
239
+ let newest: number | undefined;
240
+
241
+ for (const tool of this.tools.values()) {
242
+ const plugin = tool.pluginName ?? "core";
243
+ byPlugin[plugin] = (byPlugin[plugin] ?? 0) + 1;
244
+
245
+ if (!oldest || tool.registeredAt < oldest) oldest = tool.registeredAt;
246
+ if (!newest || tool.registeredAt > newest) newest = tool.registeredAt;
247
+ }
248
+
249
+ return {
250
+ total: this.tools.size,
251
+ byPlugin,
252
+ oldestRegistration: oldest,
253
+ newestRegistration: newest,
254
+ };
255
+ }
256
+ }
257
+
258
+ export const dynamicToolRegistry = new DynamicToolRegistry();
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import * as fs from "fs/promises";
3
+ import * as path from "path";
4
+ import type { Config } from "../config/loader.ts";
5
+ import { createFSReadTool, createFSWriteTool, createFSListTool, createFSMkdirTool, createFSDeleteTool } from "./fs.ts";
6
+
7
+ const TEST_DIR = "/tmp/hive-fs-test";
8
+
9
+ describe("FileSystem Tools", () => {
10
+ const config = {
11
+ tools: {
12
+ fs: {
13
+ allowedPaths: [TEST_DIR],
14
+ sandboxed: true,
15
+ },
16
+ },
17
+ } as unknown as Config;
18
+
19
+ beforeEach(async () => {
20
+ await fs.mkdir(TEST_DIR, { recursive: true });
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await fs.rm(TEST_DIR, { recursive: true, force: true });
25
+ });
26
+
27
+ describe("fs_read", () => {
28
+ it("should read file content", async () => {
29
+ const tool = createFSReadTool(config);
30
+ const testFile = path.join(TEST_DIR, "test.txt");
31
+ await fs.writeFile(testFile, "Hello World");
32
+
33
+ const result = (await tool.execute({ path: testFile })) as {
34
+ success: boolean;
35
+ content: string;
36
+ };
37
+
38
+ expect(result.success).toBe(true);
39
+ expect(result.content).toBe("Hello World");
40
+ });
41
+
42
+ it("should reject paths outside allowed paths", async () => {
43
+ const tool = createFSReadTool(config);
44
+
45
+ await expect(tool.execute({ path: "/etc/passwd" })).rejects.toThrow("not in allowed paths");
46
+ });
47
+ });
48
+
49
+ describe("fs_write", () => {
50
+ it("should write content to file", async () => {
51
+ const tool = createFSWriteTool(config);
52
+ const testFile = path.join(TEST_DIR, "output.txt");
53
+
54
+ const result = (await tool.execute({ path: testFile, content: "Test content" })) as {
55
+ success: boolean;
56
+ bytesWritten: number;
57
+ };
58
+
59
+ expect(result.success).toBe(true);
60
+ expect(result.bytesWritten).toBe(12);
61
+
62
+ const content = await fs.readFile(testFile, "utf-8");
63
+ expect(content).toBe("Test content");
64
+ });
65
+ });
66
+
67
+ describe("fs_list", () => {
68
+ it("should list directory contents", async () => {
69
+ const tool = createFSListTool(config);
70
+ await fs.writeFile(path.join(TEST_DIR, "file1.txt"), "content");
71
+ await fs.mkdir(path.join(TEST_DIR, "subdir"));
72
+
73
+ const result = (await tool.execute({ path: TEST_DIR })) as {
74
+ success: boolean;
75
+ count: number;
76
+ entries: Array<{ name: string; type: string }>;
77
+ };
78
+
79
+ expect(result.success).toBe(true);
80
+ expect(result.count).toBe(2);
81
+ expect(result.entries).toContainEqual(
82
+ expect.objectContaining({ name: "file1.txt", type: "file" })
83
+ );
84
+ expect(result.entries).toContainEqual(
85
+ expect.objectContaining({ name: "subdir", type: "directory" })
86
+ );
87
+ });
88
+ });
89
+
90
+ describe("fs_mkdir", () => {
91
+ it("should create directory", async () => {
92
+ const tool = createFSMkdirTool(config);
93
+ const newDir = path.join(TEST_DIR, "newdir");
94
+
95
+ const result = (await tool.execute({ path: newDir })) as { success: boolean };
96
+
97
+ expect(result.success).toBe(true);
98
+
99
+ const stat = await fs.stat(newDir);
100
+ expect(stat.isDirectory()).toBe(true);
101
+ });
102
+ });
103
+
104
+ describe("fs_delete", () => {
105
+ it("should delete file", async () => {
106
+ const tool = createFSDeleteTool(config);
107
+ const testFile = path.join(TEST_DIR, "todelete.txt");
108
+ await fs.writeFile(testFile, "delete me");
109
+
110
+ const result = (await tool.execute({ path: testFile })) as { success: boolean };
111
+
112
+ expect(result.success).toBe(true);
113
+ await expect(fs.stat(testFile)).rejects.toThrow();
114
+ });
115
+
116
+ it("should delete directory with recursive flag", async () => {
117
+ const tool = createFSDeleteTool(config);
118
+ const testDir = path.join(TEST_DIR, "toDelete");
119
+ await fs.mkdir(testDir);
120
+ await fs.writeFile(path.join(testDir, "file.txt"), "content");
121
+
122
+ const result = (await tool.execute({ path: testDir, recursive: true })) as { success: boolean };
123
+
124
+ expect(result.success).toBe(true);
125
+ });
126
+ });
127
+ });