@botcord/daemon 0.2.4 → 0.2.6
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/dist/agent-discovery.d.ts +7 -3
- package/dist/agent-discovery.js +9 -1
- package/dist/agent-workspace.d.ts +62 -0
- package/dist/agent-workspace.js +140 -10
- package/dist/config.d.ts +49 -1
- package/dist/config.js +57 -1
- package/dist/control-channel.d.ts +1 -4
- package/dist/control-channel.js +1 -4
- package/dist/daemon-config-map.d.ts +29 -12
- package/dist/daemon-config-map.js +105 -8
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +52 -5
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.js +22 -1
- package/dist/gateway/cli-resolver.d.ts +34 -0
- package/dist/gateway/cli-resolver.js +74 -0
- package/dist/gateway/dispatcher.d.ts +66 -1
- package/dist/gateway/dispatcher.js +583 -56
- package/dist/gateway/gateway.d.ts +29 -1
- package/dist/gateway/gateway.js +10 -0
- package/dist/gateway/index.d.ts +2 -0
- package/dist/gateway/index.js +2 -0
- package/dist/gateway/policy-resolver.d.ts +57 -0
- package/dist/gateway/policy-resolver.js +123 -0
- package/dist/gateway/runtimes/acp-stream.d.ts +99 -0
- package/dist/gateway/runtimes/acp-stream.js +394 -0
- package/dist/gateway/runtimes/codex.js +7 -0
- package/dist/gateway/runtimes/hermes-agent.d.ts +83 -0
- package/dist/gateway/runtimes/hermes-agent.js +180 -0
- package/dist/gateway/runtimes/ndjson-stream.d.ts +7 -2
- package/dist/gateway/runtimes/ndjson-stream.js +16 -3
- package/dist/gateway/runtimes/openclaw-acp.d.ts +44 -0
- package/dist/gateway/runtimes/openclaw-acp.js +500 -0
- package/dist/gateway/runtimes/registry.d.ts +4 -0
- package/dist/gateway/runtimes/registry.js +22 -0
- package/dist/gateway/transcript-paths.d.ts +30 -0
- package/dist/gateway/transcript-paths.js +114 -0
- package/dist/gateway/transcript.d.ts +123 -0
- package/dist/gateway/transcript.js +147 -0
- package/dist/gateway/types.d.ts +31 -0
- package/dist/index.js +286 -27
- package/dist/mention-scan.d.ts +22 -0
- package/dist/mention-scan.js +35 -0
- package/dist/provision.d.ts +73 -3
- package/dist/provision.js +373 -12
- package/dist/system-context.d.ts +5 -4
- package/dist/system-context.js +35 -5
- package/dist/turn-text.js +20 -1
- package/dist/url-utils.d.ts +9 -0
- package/dist/url-utils.js +18 -0
- package/dist/user-auth.js +0 -2
- package/dist/working-memory.js +1 -1
- package/package.json +2 -1
- package/src/__tests__/agent-workspace.test.ts +93 -0
- package/src/__tests__/daemon-config-map.test.ts +79 -0
- package/src/__tests__/openclaw-acp.test.ts +234 -0
- package/src/__tests__/policy-resolver.test.ts +124 -0
- package/src/__tests__/policy-updated-handler.test.ts +144 -0
- package/src/__tests__/provision.test.ts +160 -0
- package/src/__tests__/system-context.test.ts +52 -0
- package/src/__tests__/url-utils.test.ts +37 -0
- package/src/agent-discovery.ts +12 -4
- package/src/agent-workspace.ts +173 -9
- package/src/config.ts +132 -4
- package/src/control-channel.ts +1 -4
- package/src/daemon-config-map.ts +156 -12
- package/src/daemon.ts +66 -5
- package/src/doctor.ts +49 -2
- package/src/gateway/__tests__/dispatcher.test.ts +440 -2
- package/src/gateway/__tests__/hermes-agent-adapter.test.ts +302 -0
- package/src/gateway/__tests__/transcript.test.ts +496 -0
- package/src/gateway/cli-resolver.ts +92 -0
- package/src/gateway/dispatcher.ts +681 -58
- package/src/gateway/gateway.ts +46 -0
- package/src/gateway/index.ts +25 -0
- package/src/gateway/policy-resolver.ts +171 -0
- package/src/gateway/runtimes/acp-stream.ts +535 -0
- package/src/gateway/runtimes/codex.ts +7 -0
- package/src/gateway/runtimes/hermes-agent.ts +206 -0
- package/src/gateway/runtimes/ndjson-stream.ts +16 -3
- package/src/gateway/runtimes/openclaw-acp.ts +606 -0
- package/src/gateway/runtimes/registry.ts +24 -0
- package/src/gateway/transcript-paths.ts +145 -0
- package/src/gateway/transcript.ts +300 -0
- package/src/gateway/types.ts +32 -0
- package/src/index.ts +295 -30
- package/src/mention-scan.ts +38 -0
- package/src/provision.ts +446 -20
- package/src/system-context.ts +41 -9
- package/src/turn-text.ts +22 -1
- package/src/url-utils.ts +17 -0
- package/src/user-auth.ts +0 -2
- package/src/working-memory.ts +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
agentHermesHomeDir,
|
|
5
|
+
agentHermesWorkspaceDir,
|
|
6
|
+
ensureAgentHermesWorkspace,
|
|
7
|
+
} from "../../agent-workspace.js";
|
|
8
|
+
import { buildCliEnv } from "../cli-resolver.js";
|
|
9
|
+
import {
|
|
10
|
+
AcpRuntimeAdapter,
|
|
11
|
+
type AcpPermissionRequest,
|
|
12
|
+
type AcpPermissionResponse,
|
|
13
|
+
type AcpUpdateCtx,
|
|
14
|
+
type AcpUpdateParams,
|
|
15
|
+
} from "./acp-stream.js";
|
|
16
|
+
import { readCommandVersion, resolveCommandOnPath, type ProbeDeps } from "./probe.js";
|
|
17
|
+
import type { RuntimeProbeResult, RuntimeRunOptions, StreamBlock } from "../types.js";
|
|
18
|
+
|
|
19
|
+
/** Resolve the `hermes-acp` executable on PATH. */
|
|
20
|
+
export function resolveHermesAcpCommand(deps: ProbeDeps = {}): string | null {
|
|
21
|
+
return resolveCommandOnPath("hermes-acp", deps);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Probe whether `hermes-acp` is installed and report its version. */
|
|
25
|
+
export function probeHermesAgent(deps: ProbeDeps = {}): RuntimeProbeResult {
|
|
26
|
+
const command = resolveHermesAcpCommand(deps);
|
|
27
|
+
if (!command) return { available: false };
|
|
28
|
+
return {
|
|
29
|
+
available: true,
|
|
30
|
+
path: command,
|
|
31
|
+
version: readCommandVersion(command, [], deps) ?? undefined,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hermes Agent adapter. Drives `hermes-acp` (the ACP stdio adapter shipped
|
|
37
|
+
* with `pip install "hermes-agent[acp]"`).
|
|
38
|
+
*
|
|
39
|
+
* ## systemContext injection
|
|
40
|
+
*
|
|
41
|
+
* Hermes discovers `AGENTS.md` from the spawn cwd upward. We point cwd at a
|
|
42
|
+
* runtime-private directory (`~/.botcord/agents/<id>/hermes-workspace/`) and
|
|
43
|
+
* write `<cwd>/AGENTS.md` from `opts.systemContext` before spawn. This is a
|
|
44
|
+
* **first-turn-only** injection: hermes persists the system prompt in the
|
|
45
|
+
* session DB and does not re-read AGENTS.md on continuation turns. The
|
|
46
|
+
* design doc tracks this as a known limitation; a follow-up PR to
|
|
47
|
+
* hermes-agent would expose a per-turn ephemeral prompt channel.
|
|
48
|
+
*
|
|
49
|
+
* ## Per-agent isolation
|
|
50
|
+
*
|
|
51
|
+
* - `HERMES_HOME` → `<agent-home>/hermes-home/` so `.env`, `state.db`,
|
|
52
|
+
* `skills/` per-agent are isolated from `~/.hermes`.
|
|
53
|
+
* - cwd → `<agent-home>/hermes-workspace/` (NOT the user-editable
|
|
54
|
+
* `<agent-home>/workspace/`) so each turn's daemon-rewritten AGENTS.md
|
|
55
|
+
* does not clobber files the user/agent edited.
|
|
56
|
+
*
|
|
57
|
+
* ## Permission policy (trustLevel → ACP outcome)
|
|
58
|
+
*
|
|
59
|
+
* `HERMES_INTERACTIVE=1` makes hermes route dangerous tool calls through the
|
|
60
|
+
* ACP `session/request_permission` reverse-call. We answer per trustLevel:
|
|
61
|
+
* - `owner` → always select an `allow_*` option
|
|
62
|
+
* - `trusted` → same; reasons go to the daemon log only
|
|
63
|
+
* - `public` → cancel (DeniedOutcome) for all writes/exec
|
|
64
|
+
*/
|
|
65
|
+
export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
66
|
+
readonly id = "hermes-agent" as const;
|
|
67
|
+
|
|
68
|
+
private readonly explicitBinary: string | undefined;
|
|
69
|
+
private resolvedBinary: string | null = null;
|
|
70
|
+
|
|
71
|
+
constructor(opts?: { binary?: string }) {
|
|
72
|
+
super();
|
|
73
|
+
this.explicitBinary = opts?.binary ?? process.env.BOTCORD_HERMES_AGENT_BIN;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
probe(): RuntimeProbeResult {
|
|
77
|
+
return probeHermesAgent();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected resolveBinary(): string {
|
|
81
|
+
if (this.explicitBinary) return this.explicitBinary;
|
|
82
|
+
if (this.resolvedBinary) return this.resolvedBinary;
|
|
83
|
+
this.resolvedBinary = resolveHermesAcpCommand() ?? "hermes-acp";
|
|
84
|
+
return this.resolvedBinary;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* hermes-acp is invoked with no positional args — ACP is pure stdio
|
|
89
|
+
* JSON-RPC. We do not forward `opts.extraArgs` because hermes-acp does
|
|
90
|
+
* not accept CLI flags for runtime config; per-agent config goes in
|
|
91
|
+
* `<HERMES_HOME>/.env`.
|
|
92
|
+
*/
|
|
93
|
+
protected buildArgs(_opts: RuntimeRunOptions): string[] {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected spawnEnv(opts: RuntimeRunOptions): NodeJS.ProcessEnv {
|
|
98
|
+
const cliEnv = buildCliEnv({
|
|
99
|
+
hubUrl: opts.hubUrl,
|
|
100
|
+
accountId: opts.accountId,
|
|
101
|
+
basePath: process.env.PATH,
|
|
102
|
+
});
|
|
103
|
+
const env: NodeJS.ProcessEnv = {
|
|
104
|
+
...process.env,
|
|
105
|
+
...cliEnv,
|
|
106
|
+
// Keep ACP stdout free of ANSI codes regardless of terminal settings.
|
|
107
|
+
NO_COLOR: "1",
|
|
108
|
+
// Route dangerous tool calls through ACP request_permission.
|
|
109
|
+
HERMES_INTERACTIVE: "1",
|
|
110
|
+
};
|
|
111
|
+
if (opts.accountId) {
|
|
112
|
+
env.HERMES_HOME = agentHermesHomeDir(opts.accountId);
|
|
113
|
+
}
|
|
114
|
+
return env;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
protected sessionCwd(opts: RuntimeRunOptions): string {
|
|
118
|
+
if (opts.accountId) return agentHermesWorkspaceDir(opts.accountId);
|
|
119
|
+
return opts.cwd;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Write systemContext to `<hermes-workspace>/AGENTS.md` atomically before
|
|
124
|
+
* spawn. NOTE: hermes only reads this file on the first turn of a session
|
|
125
|
+
* (see class-level docstring); subsequent turns keep the persisted
|
|
126
|
+
* system prompt and ignore filesystem changes.
|
|
127
|
+
*/
|
|
128
|
+
protected prepareTurn(opts: RuntimeRunOptions): void {
|
|
129
|
+
if (!opts.accountId) return;
|
|
130
|
+
const { hermesWorkspace } = ensureAgentHermesWorkspace(opts.accountId);
|
|
131
|
+
const target = path.join(hermesWorkspace, "AGENTS.md");
|
|
132
|
+
const tmp = path.join(hermesWorkspace, `.AGENTS.md.${process.pid}.tmp`);
|
|
133
|
+
mkdirSync(hermesWorkspace, { recursive: true, mode: 0o700 });
|
|
134
|
+
writeFileSync(tmp, opts.systemContext ?? "", { mode: 0o600 });
|
|
135
|
+
renameSync(tmp, target);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Spawn with the runtime-private hermes-workspace as cwd. */
|
|
139
|
+
override async run(opts: RuntimeRunOptions) {
|
|
140
|
+
const effective = opts.accountId
|
|
141
|
+
? { ...opts, cwd: agentHermesWorkspaceDir(opts.accountId) }
|
|
142
|
+
: opts;
|
|
143
|
+
return super.run(effective);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Translate ACP `session/update` notifications into StreamBlocks +
|
|
148
|
+
* assistant text. We surface the common shapes that hermes emits:
|
|
149
|
+
* - `agent_message_chunk` / `user_message_chunk` content blocks
|
|
150
|
+
* - `tool_call` / `tool_call_update`
|
|
151
|
+
* - `agent_thought_chunk`
|
|
152
|
+
*
|
|
153
|
+
* Anything else is forwarded as `kind: "other"` so subclasses /
|
|
154
|
+
* downstream channels can introspect.
|
|
155
|
+
*/
|
|
156
|
+
protected onUpdate(params: AcpUpdateParams, ctx: AcpUpdateCtx): void {
|
|
157
|
+
const update = params.update ?? {};
|
|
158
|
+
const kind = typeof update.sessionUpdate === "string" ? update.sessionUpdate : "";
|
|
159
|
+
|
|
160
|
+
let blockKind: StreamBlock["kind"] = "other";
|
|
161
|
+
|
|
162
|
+
if (kind === "agent_message_chunk") {
|
|
163
|
+
const content = (update as { content?: { type?: string; text?: string } })
|
|
164
|
+
.content;
|
|
165
|
+
if (content && content.type === "text" && typeof content.text === "string") {
|
|
166
|
+
ctx.appendAssistantText(content.text);
|
|
167
|
+
}
|
|
168
|
+
blockKind = "assistant_text";
|
|
169
|
+
} else if (kind === "agent_thought_chunk") {
|
|
170
|
+
blockKind = "system";
|
|
171
|
+
} else if (kind === "tool_call" || kind === "tool_call_update") {
|
|
172
|
+
blockKind = "tool_use";
|
|
173
|
+
} else if (kind === "user_message_chunk") {
|
|
174
|
+
blockKind = "other";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
ctx.emitBlock({ raw: params, kind: blockKind, seq: ctx.seq });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* trustLevel-driven policy. We pick the FIRST option whose `kind` matches
|
|
182
|
+
* our intent — `allow_*` for permit, otherwise cancel. ACP's
|
|
183
|
+
* DeniedOutcome carries no `optionId` / `reason` field; rationale lives
|
|
184
|
+
* in the daemon log.
|
|
185
|
+
*/
|
|
186
|
+
protected async onPermissionRequest(
|
|
187
|
+
req: AcpPermissionRequest,
|
|
188
|
+
opts: RuntimeRunOptions,
|
|
189
|
+
): Promise<AcpPermissionResponse> {
|
|
190
|
+
const options = Array.isArray(req.options) ? req.options : [];
|
|
191
|
+
const trust = opts.trustLevel;
|
|
192
|
+
|
|
193
|
+
if (trust === "owner" || trust === "trusted") {
|
|
194
|
+
const allow =
|
|
195
|
+
options.find((o) => typeof o.kind === "string" && o.kind.startsWith("allow_")) ??
|
|
196
|
+
options[0];
|
|
197
|
+
if (allow?.optionId) {
|
|
198
|
+
return { outcome: { outcome: "selected", optionId: allow.optionId } };
|
|
199
|
+
}
|
|
200
|
+
return { outcome: { outcome: "cancelled" } };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// public: deny everything that requires explicit approval
|
|
204
|
+
return { outcome: { outcome: "cancelled" } };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { buildCliEnv } from "../cli-resolver.js";
|
|
2
3
|
import { consoleLogger } from "../log.js";
|
|
3
4
|
import type {
|
|
4
5
|
RuntimeAdapter,
|
|
@@ -73,9 +74,21 @@ export abstract class NdjsonStreamAdapter implements RuntimeAdapter {
|
|
|
73
74
|
protected abstract buildArgs(opts: RuntimeRunOptions): string[];
|
|
74
75
|
protected abstract handleEvent(obj: unknown, ctx: NdjsonEventCtx): void;
|
|
75
76
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Override to tweak env (FORCE_COLOR=0, NO_COLOR=1, etc). Subclasses that
|
|
79
|
+
* override should compose with the bundled-CLI env helper so spawned
|
|
80
|
+
* `botcord` invocations stay scoped to the right hub/agent — see
|
|
81
|
+
* {@link buildCliEnv}.
|
|
82
|
+
*/
|
|
83
|
+
protected spawnEnv(opts: RuntimeRunOptions): NodeJS.ProcessEnv {
|
|
84
|
+
return {
|
|
85
|
+
...process.env,
|
|
86
|
+
...buildCliEnv({
|
|
87
|
+
hubUrl: opts.hubUrl,
|
|
88
|
+
accountId: opts.accountId,
|
|
89
|
+
basePath: process.env.PATH,
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
async run(opts: RuntimeRunOptions): Promise<RuntimeRunResult> {
|