@johpaz/hive-sdk 0.2.0 → 0.3.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.
- package/CHANGELOG.md +306 -0
- package/README.md +11 -3
- package/package.json +10 -4
- package/packages/core/src/agent/agent-catalog.ts +81 -24
- package/packages/core/src/agent/compaction.ts +20 -1
- package/packages/core/src/agent/context-compiler.ts +6 -3
- package/packages/core/src/agent/conversation-store.ts +136 -2
- package/packages/core/src/agent/curator.ts +12 -3
- package/packages/core/src/agent/llm-providers/nvidia.ts +39 -0
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +38 -2
- package/packages/core/src/agent/playbook-selector.ts +18 -3
- package/packages/core/src/agent/prompt-builder.ts +2 -2
- package/packages/core/src/agent/providers/index.ts +36 -1
- package/packages/core/src/agent/reflector.ts +32 -9
- package/packages/core/src/agent/skill-selector.ts +2 -2
- package/packages/core/src/agent/thread-store.ts +43 -0
- package/packages/core/src/agent/tool-selector.ts +2 -0
- package/packages/core/src/api/createAgent.ts +66 -1
- package/packages/core/src/artifacts/index.ts +15 -0
- package/packages/core/src/artifacts/store.ts +77 -2
- package/packages/core/src/canvas/index.ts +9 -0
- package/packages/core/src/ethics/EthicsGuard.ts +7 -1
- package/packages/core/src/events/index.ts +18 -0
- package/packages/core/src/events/tool-narration.ts +4 -0
- package/packages/core/src/gateway/channel-notify.ts +103 -6
- package/packages/core/src/gateway/durable-queue.ts +13 -1
- package/packages/core/src/gateway/index.ts +3 -0
- package/packages/core/src/gateway/job-store.ts +6 -0
- package/packages/core/src/harness/executors.ts +493 -0
- package/packages/core/src/harness/index.ts +12 -2
- package/packages/core/src/hooks/index.ts +203 -0
- package/packages/core/src/images/index.ts +161 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/multimodal/vision-service.ts +45 -13
- package/packages/core/src/resilience/index.ts +13 -0
- package/packages/core/src/scheduler/CronScheduler.ts +48 -21
- package/packages/core/src/scheduler/cron/expression.ts +165 -0
- package/packages/core/src/scheduler/cron/index.ts +10 -0
- package/packages/core/src/scheduler/cron/job.ts +339 -0
- package/packages/core/src/scheduler/cron/next-run.ts +121 -0
- package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
- package/packages/core/src/scheduler/index.ts +21 -3
- package/packages/core/src/scheduler/integration.ts +16 -5
- package/packages/core/src/scheduler/types.ts +3 -18
- package/packages/core/src/services/agents.ts +268 -0
- package/packages/core/src/services/cron.ts +257 -0
- package/packages/core/src/services/endpoints.ts +289 -0
- package/packages/core/src/services/ethics.ts +107 -0
- package/packages/core/src/services/images.ts +212 -0
- package/packages/core/src/services/index.ts +112 -0
- package/packages/core/src/services/mcp.ts +201 -0
- package/packages/core/src/services/memory.ts +133 -0
- package/packages/core/src/services/models.ts +179 -0
- package/packages/core/src/services/providers.ts +152 -0
- package/packages/core/src/services/setup.ts +222 -0
- package/packages/core/src/services/skills.ts +241 -0
- package/packages/core/src/services/swarms.ts +307 -0
- package/packages/core/src/services/tools.ts +106 -0
- package/packages/core/src/sessions/index.ts +5 -3
- package/packages/core/src/sessions/resolve.ts +108 -0
- package/packages/core/src/skills/SkillLoader.ts +8 -1
- package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
- package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
- package/packages/core/src/skills/bundled-data.generated.ts +110 -12
- package/packages/core/src/storage/bootstrap.ts +74 -5
- package/packages/core/src/storage/collections.ts +106 -1
- package/packages/core/src/storage/crypto.ts +24 -7
- package/packages/core/src/storage/index.ts +2 -1
- package/packages/core/src/storage/onboarding.ts +59 -43
- package/packages/core/src/storage/reconcile.ts +6 -1
- package/packages/core/src/storage/seed.ts +89 -11
- package/packages/core/src/swarm/types.ts +3 -18
- package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
- package/packages/core/src/tool-runtime/index.ts +129 -14
- package/packages/core/src/tools/agents/index.ts +18 -60
- package/packages/core/src/tools/cli/index.ts +55 -0
- package/packages/core/src/tools/core/index.ts +50 -2
- package/packages/core/src/tools/cron/index.ts +4 -4
- package/packages/core/src/tools/images/index.ts +130 -0
- package/packages/core/src/tools/index.ts +14 -1
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
- package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
- package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
|
@@ -4,6 +4,9 @@ import { dirname, join } from "node:path"
|
|
|
4
4
|
import { availableParallelism } from "node:os"
|
|
5
5
|
import type { Config } from "../config/loader.ts"
|
|
6
6
|
import { loadConfig } from "../config/loader.ts"
|
|
7
|
+
import { logger } from "../utils/logger.ts"
|
|
8
|
+
import { embeddedToolWorkerPath } from "./embedded-worker.generated.ts"
|
|
9
|
+
import { hasHooks, runBeforeToolCall, runAfterToolCall } from "../hooks/index.ts"
|
|
7
10
|
|
|
8
11
|
export type ToolCallLike = {
|
|
9
12
|
id: string
|
|
@@ -105,7 +108,19 @@ type WorkerSlot = {
|
|
|
105
108
|
job?: QueuedJob
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
|
|
111
|
+
let cachedWorkerEntry: string | null | undefined
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Locate the tool worker entry, or null when this build ships without one.
|
|
115
|
+
*
|
|
116
|
+
* Returning null instead of throwing is deliberate: workers are an
|
|
117
|
+
* optimization, and a packaging gap must never take a whole turn down with it
|
|
118
|
+
* (it did until v1.0.3 — every desktop install failed each multi-tool turn
|
|
119
|
+
* with "Tool worker entry not found"). The caller degrades to the main thread.
|
|
120
|
+
*/
|
|
121
|
+
function resolveWorkerEntry(): string | null {
|
|
122
|
+
if (cachedWorkerEntry !== undefined) return cachedWorkerEntry
|
|
123
|
+
|
|
109
124
|
const candidates = [
|
|
110
125
|
new URL("./tool-worker.js", import.meta.url),
|
|
111
126
|
new URL("./tool-worker.ts", import.meta.url),
|
|
@@ -115,7 +130,8 @@ function resolveWorkerEntry(): string {
|
|
|
115
130
|
|
|
116
131
|
for (const candidate of candidates) {
|
|
117
132
|
if (existsSync(fileURLToPath(candidate))) {
|
|
118
|
-
|
|
133
|
+
cachedWorkerEntry = candidate.href
|
|
134
|
+
return cachedWorkerEntry
|
|
119
135
|
}
|
|
120
136
|
}
|
|
121
137
|
|
|
@@ -135,13 +151,25 @@ function resolveWorkerEntry(): string {
|
|
|
135
151
|
|
|
136
152
|
for (const filePath of fallbacks) {
|
|
137
153
|
if (existsSync(filePath)) {
|
|
138
|
-
|
|
154
|
+
cachedWorkerEntry = filePath
|
|
155
|
+
return cachedWorkerEntry
|
|
139
156
|
}
|
|
140
157
|
}
|
|
141
158
|
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
// Standalone executable: the worker was embedded at compile time and lives in
|
|
160
|
+
// the virtual bunfs, where existsSync() reports false — so it is taken on
|
|
161
|
+
// trust. It is only ever set by scripts/build-gateway.ts.
|
|
162
|
+
if (embeddedToolWorkerPath) {
|
|
163
|
+
cachedWorkerEntry = embeddedToolWorkerPath
|
|
164
|
+
return cachedWorkerEntry
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
logger.warn(
|
|
168
|
+
"[tool-runtime] No tool worker entry found — running tool batches on the main thread (sequentially)",
|
|
169
|
+
{ tried: [...candidates.map((candidate) => fileURLToPath(candidate)), ...fallbacks] }
|
|
144
170
|
)
|
|
171
|
+
cachedWorkerEntry = null
|
|
172
|
+
return null
|
|
145
173
|
}
|
|
146
174
|
|
|
147
175
|
function serializeError(error: unknown): SerializedError {
|
|
@@ -187,6 +215,7 @@ const DEFAULT_MAIN_THREAD_TOOL_NAMES = new Set([
|
|
|
187
215
|
"browser_navigate",
|
|
188
216
|
"browser_screenshot",
|
|
189
217
|
"artifact_inspect",
|
|
218
|
+
"artifact_read",
|
|
190
219
|
"browser_click",
|
|
191
220
|
"browser_type",
|
|
192
221
|
"browser_extract",
|
|
@@ -255,10 +284,12 @@ class ToolWorkerPool {
|
|
|
255
284
|
private workers: WorkerSlot[] = []
|
|
256
285
|
private queue: QueuedJob[] = []
|
|
257
286
|
private readonly maxWorkers: number
|
|
287
|
+
private readonly workerEntry: string
|
|
258
288
|
private disposed = false
|
|
259
289
|
|
|
260
|
-
constructor(maxWorkers: number) {
|
|
290
|
+
constructor(maxWorkers: number, workerEntry: string) {
|
|
261
291
|
this.maxWorkers = Math.max(1, maxWorkers)
|
|
292
|
+
this.workerEntry = workerEntry
|
|
262
293
|
}
|
|
263
294
|
|
|
264
295
|
execute(job: Omit<QueuedJob, "resolve" | "settled" | "startedAt">): Promise<ToolBatchResult> {
|
|
@@ -385,7 +416,7 @@ class ToolWorkerPool {
|
|
|
385
416
|
}
|
|
386
417
|
|
|
387
418
|
private createSlot(): WorkerSlot {
|
|
388
|
-
const worker = new Worker(
|
|
419
|
+
const worker = new Worker(this.workerEntry, { type: "module" })
|
|
389
420
|
const slot: WorkerSlot = { worker, busy: false }
|
|
390
421
|
|
|
391
422
|
worker.onmessage = (event: MessageEvent<WorkerMessage>) => {
|
|
@@ -500,11 +531,15 @@ class ToolWorkerPool {
|
|
|
500
531
|
job.resolve(result)
|
|
501
532
|
|
|
502
533
|
if (restart) {
|
|
534
|
+
// Un worker que abortó o murió se descarta, y el reemplazo lo crea
|
|
535
|
+
// `drain()` solo si queda trabajo. Antes se levantaba uno nuevo en el
|
|
536
|
+
// acto: sobre un aborto —el caso típico al terminar un turno o una
|
|
537
|
+
// suite— eso deja un worker recién arrancado que nadie va a usar y que
|
|
538
|
+
// hay que terminar a mitad del arranque. Bun se cae con SIGSEGV al
|
|
539
|
+
// cerrar el proceso en ese estado (CI: workers_spawned 13, terminated 11).
|
|
503
540
|
slot.worker.terminate()
|
|
504
541
|
const index = this.workers.indexOf(slot)
|
|
505
|
-
if (index >= 0)
|
|
506
|
-
this.workers[index] = this.createSlot()
|
|
507
|
-
}
|
|
542
|
+
if (index >= 0) this.workers.splice(index, 1)
|
|
508
543
|
} else {
|
|
509
544
|
slot.busy = false
|
|
510
545
|
slot.job = undefined
|
|
@@ -550,15 +585,90 @@ function resolveToolTimeout(
|
|
|
550
585
|
return baseTimeoutMs
|
|
551
586
|
}
|
|
552
587
|
|
|
553
|
-
function getPool(maxWorkers: number): ToolWorkerPool {
|
|
588
|
+
function getPool(maxWorkers: number): ToolWorkerPool | null {
|
|
589
|
+
const workerEntry = resolveWorkerEntry()
|
|
590
|
+
if (!workerEntry) return null
|
|
591
|
+
|
|
554
592
|
if (!sharedPool || sharedPoolSize !== maxWorkers) {
|
|
555
|
-
sharedPool = new ToolWorkerPool(maxWorkers)
|
|
593
|
+
sharedPool = new ToolWorkerPool(maxWorkers, workerEntry)
|
|
556
594
|
sharedPoolSize = maxWorkers
|
|
557
595
|
}
|
|
558
596
|
return sharedPool
|
|
559
597
|
}
|
|
560
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Ejecuta un lote de tool calls, aplicando los hooks alrededor.
|
|
601
|
+
*
|
|
602
|
+
* Los hooks se resuelven acá y no dentro del despacho porque hay dos caminos
|
|
603
|
+
* —hilo principal y pool de workers— con puntos de retorno distintos.
|
|
604
|
+
* Engancharlos abajo dejaría la mitad de las llamadas sin revisar, que en un
|
|
605
|
+
* hook de política es peor que no tenerlo.
|
|
606
|
+
*
|
|
607
|
+
* Una tool bloqueada **no se ejecuta**: se saca del lote y su lugar se rellena
|
|
608
|
+
* con el motivo, para que el modelo sepa por qué no se hizo en vez de
|
|
609
|
+
* reintentar a ciegas.
|
|
610
|
+
*/
|
|
561
611
|
export async function executeToolBatch(options: ExecuteToolBatchOptions): Promise<ToolBatchResult[]> {
|
|
612
|
+
const bloqueadas = new Map<string, string>()
|
|
613
|
+
|
|
614
|
+
if (hasHooks("beforeToolCall")) {
|
|
615
|
+
for (const toolCall of options.toolCalls) {
|
|
616
|
+
const args = typeof toolCall.function.arguments === "string"
|
|
617
|
+
? (() => { try { return JSON.parse(toolCall.function.arguments) } catch { return {} } })()
|
|
618
|
+
: (toolCall.function.arguments ?? {})
|
|
619
|
+
|
|
620
|
+
const motivo = await runBeforeToolCall({
|
|
621
|
+
toolName: toolCall.function.name,
|
|
622
|
+
args: args as Record<string, unknown>,
|
|
623
|
+
agentId: options.toolConfig?.agent_id as string | undefined,
|
|
624
|
+
userId: options.toolConfig?.user_id as string | undefined,
|
|
625
|
+
threadId: options.toolConfig?.thread_id as string | undefined,
|
|
626
|
+
})
|
|
627
|
+
if (motivo) bloqueadas.set(toolCall.id, motivo)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const permitidas = options.toolCalls.filter((tc) => !bloqueadas.has(tc.id))
|
|
632
|
+
const ejecutadas = permitidas.length > 0
|
|
633
|
+
? await executeToolBatchInner({ ...options, toolCalls: permitidas })
|
|
634
|
+
: []
|
|
635
|
+
|
|
636
|
+
// Se reconstruye el orden original: el modelo espera una respuesta por cada
|
|
637
|
+
// llamada que hizo, en el orden en que las hizo.
|
|
638
|
+
const porId = new Map(ejecutadas.map((r) => [r.toolCall.id, r]))
|
|
639
|
+
const resultados = options.toolCalls.map((toolCall) => {
|
|
640
|
+
const bloqueo = bloqueadas.get(toolCall.id)
|
|
641
|
+
if (bloqueo) {
|
|
642
|
+
return {
|
|
643
|
+
toolCall,
|
|
644
|
+
toolName: toolCall.function.name,
|
|
645
|
+
result: toolErrorResult(toolCall.function.name, bloqueo),
|
|
646
|
+
ok: false,
|
|
647
|
+
durationMs: 0,
|
|
648
|
+
} as ToolBatchResult
|
|
649
|
+
}
|
|
650
|
+
return porId.get(toolCall.id)!
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
if (hasHooks("afterToolCall")) {
|
|
654
|
+
for (const r of resultados) {
|
|
655
|
+
await runAfterToolCall({
|
|
656
|
+
toolName: r.toolName,
|
|
657
|
+
args: {},
|
|
658
|
+
result: r.result,
|
|
659
|
+
ok: r.ok,
|
|
660
|
+
durationMs: r.durationMs,
|
|
661
|
+
agentId: options.toolConfig?.agent_id as string | undefined,
|
|
662
|
+
userId: options.toolConfig?.user_id as string | undefined,
|
|
663
|
+
threadId: options.toolConfig?.thread_id as string | undefined,
|
|
664
|
+
})
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return resultados
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async function executeToolBatchInner(options: ExecuteToolBatchOptions): Promise<ToolBatchResult[]> {
|
|
562
672
|
const runtimeConfig = resolveRuntimeConfig(options.workerPool)
|
|
563
673
|
const hiveConfig = options.hiveConfig ?? loadConfig()
|
|
564
674
|
const mainThreadToolNames = [
|
|
@@ -578,7 +688,13 @@ export async function executeToolBatch(options: ExecuteToolBatchOptions): Promis
|
|
|
578
688
|
}))
|
|
579
689
|
}
|
|
580
690
|
|
|
581
|
-
|
|
691
|
+
// A null pool means workers are disabled, unnecessary (single call), or
|
|
692
|
+
// unavailable in this build — all three degrade to the main thread.
|
|
693
|
+
const pool = runtimeConfig.enabled && runtimeConfig.parallelToolCalls && options.toolCalls.length > 1
|
|
694
|
+
? getPool(runtimeConfig.maxWorkers)
|
|
695
|
+
: null
|
|
696
|
+
|
|
697
|
+
if (!pool) {
|
|
582
698
|
const results: ToolBatchResult[] = []
|
|
583
699
|
for (const toolCall of options.toolCalls) {
|
|
584
700
|
const startedAt = performance.now()
|
|
@@ -605,7 +721,6 @@ export async function executeToolBatch(options: ExecuteToolBatchOptions): Promis
|
|
|
605
721
|
return results
|
|
606
722
|
}
|
|
607
723
|
|
|
608
|
-
const pool = getPool(runtimeConfig.maxWorkers)
|
|
609
724
|
const batchId = crypto.randomUUID()
|
|
610
725
|
const abortHandler = () => pool.abortBatch(batchId, "Tool execution aborted")
|
|
611
726
|
options.signal?.addEventListener("abort", abortHandler, { once: true })
|
|
@@ -33,22 +33,9 @@ export const memoryWriteTool: Tool = {
|
|
|
33
33
|
required: ["title", "content"],
|
|
34
34
|
},
|
|
35
35
|
execute: async (params: Record<string, unknown>) => {
|
|
36
|
-
const title = params.title as string;
|
|
37
|
-
const content = params.content as string;
|
|
38
|
-
|
|
39
36
|
try {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const now = Date.now();
|
|
43
|
-
await memoryCol.put(title, {
|
|
44
|
-
id: title,
|
|
45
|
-
title,
|
|
46
|
-
content,
|
|
47
|
-
created_at: existing?.doc.created_at ?? now,
|
|
48
|
-
updated_at: now,
|
|
49
|
-
}, existing ? { expectedVersion: existing.version } : { expectedVersion: 0 });
|
|
50
|
-
|
|
51
|
-
return { ok: true, title, message: "Memory saved." };
|
|
37
|
+
const entry = await writeMemory(params.title as string, params.content as string);
|
|
38
|
+
return { ok: true, title: entry.title, message: "Memory saved." };
|
|
52
39
|
} catch (error) {
|
|
53
40
|
return { ok: false, error: `Failed to save memory: ${(error as Error).message}` };
|
|
54
41
|
}
|
|
@@ -69,21 +56,15 @@ export const memoryReadTool: Tool = {
|
|
|
69
56
|
},
|
|
70
57
|
execute: async (params: Record<string, unknown>) => {
|
|
71
58
|
const title = params.title as string;
|
|
72
|
-
|
|
73
59
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (!entry) {
|
|
78
|
-
return { ok: false, error: `Memory not found: ${title}` };
|
|
79
|
-
}
|
|
80
|
-
|
|
60
|
+
const entry = await readMemory(title);
|
|
61
|
+
if (!entry) return { ok: false, error: `Memory not found: ${title}` };
|
|
81
62
|
return {
|
|
82
63
|
ok: true,
|
|
83
|
-
title: entry.
|
|
84
|
-
content: entry.
|
|
85
|
-
createdAt: new Date(entry.
|
|
86
|
-
updatedAt: new Date(entry.
|
|
64
|
+
title: entry.title,
|
|
65
|
+
content: entry.content,
|
|
66
|
+
createdAt: new Date(entry.createdAt).toISOString(),
|
|
67
|
+
updatedAt: new Date(entry.updatedAt).toISOString(),
|
|
87
68
|
};
|
|
88
69
|
} catch (error) {
|
|
89
70
|
return { ok: false, error: `Failed to read memory: ${(error as Error).message}` };
|
|
@@ -102,15 +83,11 @@ export const memoryListTool: Tool = {
|
|
|
102
83
|
},
|
|
103
84
|
execute: async () => {
|
|
104
85
|
try {
|
|
105
|
-
const
|
|
106
|
-
const notes = (await memoryCol.scan({}))
|
|
107
|
-
.map(e => e.doc)
|
|
108
|
-
.sort((a, b) => b.updated_at - a.updated_at);
|
|
109
|
-
|
|
86
|
+
const entries = await listMemories();
|
|
110
87
|
return {
|
|
111
88
|
ok: true,
|
|
112
|
-
count:
|
|
113
|
-
entries:
|
|
89
|
+
count: entries.length,
|
|
90
|
+
entries: entries.map((n) => ({ title: n.title, createdAt: new Date(n.createdAt).toISOString() })),
|
|
114
91
|
};
|
|
115
92
|
} catch (error) {
|
|
116
93
|
return { ok: false, error: `Failed to list memories: ${(error as Error).message}` };
|
|
@@ -132,23 +109,9 @@ export const memorySearchTool: Tool = {
|
|
|
132
109
|
},
|
|
133
110
|
execute: async (params: Record<string, unknown>) => {
|
|
134
111
|
const query = params.query as string;
|
|
135
|
-
const needle = query.toLowerCase();
|
|
136
|
-
|
|
137
112
|
try {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
.map(e => e.doc)
|
|
141
|
-
.filter(n => n.content.toLowerCase().includes(needle) || n.title.toLowerCase().includes(needle));
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
ok: true,
|
|
145
|
-
query,
|
|
146
|
-
count: notes.length,
|
|
147
|
-
results: notes.map((n) => ({
|
|
148
|
-
title: n.title,
|
|
149
|
-
snippet: n.content.slice(0, 200) + (n.content.length > 200 ? "..." : ""),
|
|
150
|
-
})),
|
|
151
|
-
};
|
|
113
|
+
const results = await searchMemories(query);
|
|
114
|
+
return { ok: true, query, count: results.length, results };
|
|
152
115
|
} catch (error) {
|
|
153
116
|
return { ok: false, error: `Failed to search memories: ${(error as Error).message}` };
|
|
154
117
|
}
|
|
@@ -169,17 +132,9 @@ export const memoryDeleteTool: Tool = {
|
|
|
169
132
|
},
|
|
170
133
|
execute: async (params: Record<string, unknown>) => {
|
|
171
134
|
const title = params.title as string;
|
|
172
|
-
|
|
173
135
|
try {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (!existing) {
|
|
178
|
-
return { ok: false, error: `Memory not found: ${title}` };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
await memoryCol.delete(title);
|
|
182
|
-
|
|
136
|
+
const borrada = await deleteMemory(title);
|
|
137
|
+
if (!borrada) return { ok: false, error: `Memory not found: ${title}` };
|
|
183
138
|
return { ok: true, title, message: "Memory deleted." };
|
|
184
139
|
} catch (error) {
|
|
185
140
|
return { ok: false, error: `Failed to delete memory: ${(error as Error).message}` };
|
|
@@ -1202,6 +1157,9 @@ export const busReadTool: Tool = {
|
|
|
1202
1157
|
|
|
1203
1158
|
import crypto from "crypto";
|
|
1204
1159
|
import { getAvailableModelsTool } from "./get-available-models.ts";
|
|
1160
|
+
// Las tools de memoria son envoltorios: la implementación vive en services/memory.ts,
|
|
1161
|
+
// para que una UI pueda usarla sin pasar por el formato que espera el modelo.
|
|
1162
|
+
import { writeMemory, readMemory, listMemories, searchMemories, deleteMemory } from "../../services/memory.ts";
|
|
1205
1163
|
|
|
1206
1164
|
export function createTools(): Tool[] {
|
|
1207
1165
|
return [
|
|
@@ -10,6 +10,7 @@ import type { Tool } from "../types.ts";
|
|
|
10
10
|
import { logger } from "../../utils/logger.ts";
|
|
11
11
|
import { resolveInWorkspace, getWorkspace, expandPath } from "../filesystem/workspace-guard.ts";
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
|
+
import { loadConfig } from "../../config/loader.ts";
|
|
13
14
|
|
|
14
15
|
const log = logger.child("cli-exec");
|
|
15
16
|
|
|
@@ -28,6 +29,53 @@ const BLOCKED_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
|
|
|
28
29
|
{ pattern: /format\s+[a-z]:/i, reason: "disk format (Windows)" },
|
|
29
30
|
];
|
|
30
31
|
|
|
32
|
+
/** El primer token del comando: `git status` → `git`. Es lo que se compara. */
|
|
33
|
+
function commandName(command: string): string {
|
|
34
|
+
const limpio = command.trim().replace(/^\s*(sudo|env|nohup)\s+/i, "");
|
|
35
|
+
return (limpio.split(/[\s;|&<>]/)[0] ?? "").split("/").pop() ?? "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Aplica la política de comandos que el usuario configuró.
|
|
40
|
+
*
|
|
41
|
+
* `tools.exec.allowlist` y `.denylist` existían en el esquema de configuración y
|
|
42
|
+
* **no las leía nadie**: alguien podía escribir `denylist: ["curl", "rm"]`
|
|
43
|
+
* creyendo que restringía a sus agentes, y no restringía nada. En una opción de
|
|
44
|
+
* seguridad eso es peor que no tenerla, porque da confianza falsa.
|
|
45
|
+
*
|
|
46
|
+
* `BLOCKED_PATTERNS` sigue siendo incondicional: lo catastrófico se bloquea
|
|
47
|
+
* haya o no configuración. Esta capa es la que el usuario controla.
|
|
48
|
+
*
|
|
49
|
+
* Precedencia: si hay allowlist, sólo eso se permite (es la postura más
|
|
50
|
+
* restrictiva y la que alguien espera al escribirla). La denylist se aplica
|
|
51
|
+
* después, para poder tener una allowlist amplia con excepciones puntuales.
|
|
52
|
+
*
|
|
53
|
+
* Devuelve el motivo del rechazo, o `null` si no hay objeción.
|
|
54
|
+
*/
|
|
55
|
+
function checkExecPolicy(command: string): string | null {
|
|
56
|
+
const exec = loadConfig().tools?.exec;
|
|
57
|
+
if (!exec) return null;
|
|
58
|
+
|
|
59
|
+
if (exec.enabled === false) {
|
|
60
|
+
return "La ejecución de comandos está deshabilitada en la configuración";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const nombre = commandName(command).toLowerCase();
|
|
64
|
+
if (!nombre) return null;
|
|
65
|
+
|
|
66
|
+
const permitidos = exec.allowlist?.map((c) => c.toLowerCase());
|
|
67
|
+
if (permitidos?.length && !permitidos.includes(nombre)) {
|
|
68
|
+
return `Comando no permitido: "${nombre}". Permitidos: ${permitidos.join(", ")}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const denegados = exec.denylist?.map((c) => c.toLowerCase());
|
|
72
|
+
if (denegados?.includes(nombre)) {
|
|
73
|
+
return `Comando denegado por la configuración: "${nombre}"`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null; // sin objeción
|
|
77
|
+
}
|
|
78
|
+
|
|
31
79
|
export const cliExecTool: Tool = {
|
|
32
80
|
name: "cli_exec",
|
|
33
81
|
// Long-running commands need a generous runtime ceiling (the tool allows
|
|
@@ -75,6 +123,13 @@ export const cliExecTool: Tool = {
|
|
|
75
123
|
return { ok: false, error: `Working directory not found: ${cwd}` };
|
|
76
124
|
}
|
|
77
125
|
|
|
126
|
+
// ── Política configurable por el usuario ──────────────────────────────────
|
|
127
|
+
const objecion = checkExecPolicy(command);
|
|
128
|
+
if (objecion) {
|
|
129
|
+
log.warn(`bloqueado por configuración: ${command}`);
|
|
130
|
+
return { ok: false, error: objecion };
|
|
131
|
+
}
|
|
132
|
+
|
|
78
133
|
// ── Dangerous pattern check ────────────────────────────────────────────────
|
|
79
134
|
for (const { pattern, reason } of BLOCKED_PATTERNS) {
|
|
80
135
|
if (pattern.test(command)) {
|
|
@@ -136,6 +136,40 @@ function translateQueryToEnglish(query: string): string {
|
|
|
136
136
|
|
|
137
137
|
// ─── search_knowledge ────────────────────────────────────────────────────────
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Las tools que el agente que pregunta puede realmente usar.
|
|
141
|
+
*
|
|
142
|
+
* El descubrimiento buscaba contra el índice global y devolvía cualquier
|
|
143
|
+
* coincidencia, sin mirar la lista blanca de quien preguntaba. La ejecución sí
|
|
144
|
+
* estaba protegida —`context-compiler.ts` recorta `allTools`, así que una tool
|
|
145
|
+
* fuera de la lista no se puede resolver ni llamar— pero el agente igual veía su
|
|
146
|
+
* nombre y su descripción. Filtrar acá cierra esa fuga y, de paso, deja de
|
|
147
|
+
* ofrecerle al modelo capacidades que no va a poder usar, que es una forma
|
|
148
|
+
* segura de hacerle perder un turno.
|
|
149
|
+
*
|
|
150
|
+
* Sin agente en el contexto (una llamada suelta, un test) no se filtra nada.
|
|
151
|
+
*/
|
|
152
|
+
async function allowedToolNames(agentId?: string): Promise<Set<string> | null> {
|
|
153
|
+
if (!agentId) return null;
|
|
154
|
+
try {
|
|
155
|
+
const agents = await col<AgentDoc>("agents");
|
|
156
|
+
const entry = await agents.get(agentId);
|
|
157
|
+
if (!entry) return null;
|
|
158
|
+
|
|
159
|
+
const raw = entry.doc.tool_allowlist_json ?? entry.doc.tools_json;
|
|
160
|
+
if (!raw) return null; // sin lista declarada, descubrimiento abierto
|
|
161
|
+
|
|
162
|
+
const { expandToolAllowlist } = await import("../../agent/delegation-runtime.ts");
|
|
163
|
+
const patrones = JSON.parse(raw) as string[];
|
|
164
|
+
if (!Array.isArray(patrones) || patrones.length === 0) return new Set();
|
|
165
|
+
|
|
166
|
+
const { MINIMAL_TOOLS } = await import("../../agent/minimal-loadout.ts");
|
|
167
|
+
return new Set([...MINIMAL_TOOLS, ...expandToolAllowlist(patrones)]);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
139
173
|
export const searchKnowledgeTool: Tool = {
|
|
140
174
|
name: "search_knowledge",
|
|
141
175
|
description: "Busca en TODO el conocimiento de Hive: tools nativas, MCP, skills, agentes de catálogo y playbook.",
|
|
@@ -158,10 +192,11 @@ export const searchKnowledgeTool: Tool = {
|
|
|
158
192
|
},
|
|
159
193
|
required: ["query"],
|
|
160
194
|
},
|
|
161
|
-
execute: async (params: Record<string, unknown
|
|
195
|
+
execute: async (params: Record<string, unknown>, config?: any) => {
|
|
162
196
|
const query = params.query as string;
|
|
163
197
|
const type = (params.type as string) ?? "all";
|
|
164
198
|
const limit = (params.limit as number) ?? 10;
|
|
199
|
+
const usuarioActual = (config?.configurable?.user_id as string) ?? "";
|
|
165
200
|
const MIN_RESULTS_FOR_BILINGUAL = 2;
|
|
166
201
|
|
|
167
202
|
// Map the tool's `type` param onto capability types
|
|
@@ -187,6 +222,8 @@ export const searchKnowledgeTool: Tool = {
|
|
|
187
222
|
|
|
188
223
|
const result: any = { query, type, tools: [], skills: [], playbook: [], toolsmcp: [], agents: [] };
|
|
189
224
|
|
|
225
|
+
const permitidas = await allowedToolNames(config?.configurable?.agent_id);
|
|
226
|
+
|
|
190
227
|
// ─── Hydration from HiveDB collections (index stores only ids + search text) ──
|
|
191
228
|
|
|
192
229
|
const coreCatalog = new Map(CORE_TOOL_CATALOG.map(t => [t.name, t]));
|
|
@@ -198,6 +235,10 @@ export const searchKnowledgeTool: Tool = {
|
|
|
198
235
|
const agentsCol = await col<AgentDoc>("agents");
|
|
199
236
|
|
|
200
237
|
async function hydrateTool(hit: CapabilityHit): Promise<any | null> {
|
|
238
|
+
// Ofrecerle al modelo una tool que no puede ejecutar es hacerle perder
|
|
239
|
+
// un turno, además de contarle qué existe fuera de su alcance.
|
|
240
|
+
if (permitidas && !permitidas.has(hit.rawId)) return null;
|
|
241
|
+
|
|
201
242
|
const entry = await toolsCol.get(hit.rawId);
|
|
202
243
|
if (entry) {
|
|
203
244
|
const row = entry.doc;
|
|
@@ -232,6 +273,10 @@ export const searchKnowledgeTool: Tool = {
|
|
|
232
273
|
const entry = await playbookCol.get(hit.rawId);
|
|
233
274
|
const p = entry?.doc;
|
|
234
275
|
if (!p || !p.active) return null;
|
|
276
|
+
// Mismo alcance que la inyección en el prompt: lo global más lo propio.
|
|
277
|
+
// Sin esto, el agente puede buscar en el playbook y leerle a un usuario
|
|
278
|
+
// lo que aprendió de otro, por la puerta de al lado.
|
|
279
|
+
if (p.user_id !== "" && p.user_id !== usuarioActual) return null;
|
|
235
280
|
return {
|
|
236
281
|
id: p.id, rule: p.rule, category: p.category,
|
|
237
282
|
applicable_to: p.applicable_to ? JSON.parse(p.applicable_to) : null,
|
|
@@ -353,7 +398,10 @@ export const notifyTool: Tool = {
|
|
|
353
398
|
|
|
354
399
|
log.info(`[notify] Sending to ${channel}/${userId}: ${message.substring(0, 80)}`);
|
|
355
400
|
|
|
356
|
-
|
|
401
|
+
// El aviso tiene que volver al hilo del que salió: sin el threadId,
|
|
402
|
+
// `notifyChannel` no sabe a qué conversación del canal responder.
|
|
403
|
+
const threadId = config?.configurable?.thread_id as string | undefined;
|
|
404
|
+
const result = await sendToUserChannel(channel, userId, message, { threadId })
|
|
357
405
|
if (!result.ok) throw new Error(`Channel send failed: ${result.error}`)
|
|
358
406
|
return result
|
|
359
407
|
},
|
|
@@ -12,7 +12,7 @@ import type { Tool } from "../types.ts";
|
|
|
12
12
|
import { col, toIndexable } from "../../storage/hive.ts";
|
|
13
13
|
import type { UserDoc, UserIdentityDoc, ChannelDoc, CronJobDoc, TaskRunDoc } from "../../storage/collections.ts";
|
|
14
14
|
import { logger } from "../../utils/logger.ts";
|
|
15
|
-
import { Cron } from "
|
|
15
|
+
import { Cron } from "../../scheduler/cron/index.ts";
|
|
16
16
|
|
|
17
17
|
const log = logger.child("CronTools");
|
|
18
18
|
|
|
@@ -114,9 +114,9 @@ export const cronCreateTool: Tool = {
|
|
|
114
114
|
tool_name: { type: "string", description: "Specific tool to execute (optional)" },
|
|
115
115
|
max_runs: { type: "number", description: "Maximum executions (optional, null = unlimited)" },
|
|
116
116
|
channel: { type: "string", description: "Notification channel (system, telegram, discord, whatsapp, cli)" },
|
|
117
|
-
start_at: { type: "string", description: "ISO 8601 datetime: start of execution window
|
|
118
|
-
stop_at: { type: "string", description: "ISO 8601 datetime: end of execution window
|
|
119
|
-
dom_and_dow: { type: "boolean", description: "If true, both day-of-month AND day-of-week must match
|
|
117
|
+
start_at: { type: "string", description: "ISO 8601 datetime: start of execution window. Optional." },
|
|
118
|
+
stop_at: { type: "string", description: "ISO 8601 datetime: end of execution window. Optional." },
|
|
119
|
+
dom_and_dow: { type: "boolean", description: "If true, both day-of-month AND day-of-week must match. Default: false (OR logic)" },
|
|
120
120
|
},
|
|
121
121
|
required: ["name", "task", "task_type"],
|
|
122
122
|
},
|