@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,302 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { HermesAgentAdapter } from "../runtimes/hermes-agent.js";
|
|
13
|
+
import { agentHermesWorkspaceDir } from "../../agent-workspace.js";
|
|
14
|
+
|
|
15
|
+
// Spawn a tiny Node "ACP server" we control instead of the real hermes-acp.
|
|
16
|
+
const tmpRoot = mkdtempSync(path.join(os.tmpdir(), "gateway-hermes-"));
|
|
17
|
+
|
|
18
|
+
const originalHome = process.env.HOME;
|
|
19
|
+
const agentHomeRoot = mkdtempSync(path.join(os.tmpdir(), "gateway-hermes-home-"));
|
|
20
|
+
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
process.env.HOME = agentHomeRoot;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(() => {
|
|
26
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
27
|
+
else process.env.HOME = originalHome;
|
|
28
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
29
|
+
rmSync(agentHomeRoot, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Mock ACP server. Reads newline-delimited JSON-RPC messages from stdin
|
|
34
|
+
* and replies on stdout. The script body controls the per-test scenario.
|
|
35
|
+
*/
|
|
36
|
+
function makeAcpServer(name: string, script: string): string {
|
|
37
|
+
const body = `
|
|
38
|
+
const lines = [];
|
|
39
|
+
let buf = "";
|
|
40
|
+
process.stdin.setEncoding("utf8");
|
|
41
|
+
function send(obj) { process.stdout.write(JSON.stringify(obj) + "\\n"); }
|
|
42
|
+
function reply(req, result) { send({ jsonrpc: "2.0", id: req.id, result }); }
|
|
43
|
+
function err(req, code, message) { send({ jsonrpc: "2.0", id: req.id, error: { code, message } }); }
|
|
44
|
+
function notify(method, params) { send({ jsonrpc: "2.0", method, params }); }
|
|
45
|
+
let nextReqId = 1000;
|
|
46
|
+
const pending = new Map();
|
|
47
|
+
function request(method, params) {
|
|
48
|
+
const id = nextReqId++;
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
pending.set(id, { resolve, reject });
|
|
51
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function handle(msg) {
|
|
55
|
+
${script}
|
|
56
|
+
}
|
|
57
|
+
process.stdin.on("data", async (chunk) => {
|
|
58
|
+
buf += chunk;
|
|
59
|
+
let idx;
|
|
60
|
+
while ((idx = buf.indexOf("\\n")) !== -1) {
|
|
61
|
+
const line = buf.slice(0, idx).trim();
|
|
62
|
+
buf = buf.slice(idx + 1);
|
|
63
|
+
if (!line) continue;
|
|
64
|
+
let msg;
|
|
65
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
66
|
+
// Server received a response to a request it sent
|
|
67
|
+
if (typeof msg.id === "number" && (msg.result !== undefined || msg.error !== undefined) && pending.has(msg.id)) {
|
|
68
|
+
const p = pending.get(msg.id);
|
|
69
|
+
pending.delete(msg.id);
|
|
70
|
+
if (msg.error) p.reject(msg.error); else p.resolve(msg.result);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
try { await handle(msg); } catch (e) { process.stderr.write("mock error: " + e + "\\n"); }
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
process.stdin.on("end", () => process.exit(0));
|
|
77
|
+
`;
|
|
78
|
+
const p = path.join(tmpRoot, name);
|
|
79
|
+
writeFileSync(p, `#!/usr/bin/env node\n${body}\n`, { mode: 0o755 });
|
|
80
|
+
chmodSync(p, 0o755);
|
|
81
|
+
return p;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface RunOpts {
|
|
85
|
+
sessionId?: string | null;
|
|
86
|
+
trustLevel?: "owner" | "trusted" | "public";
|
|
87
|
+
systemContext?: string;
|
|
88
|
+
accountId?: string;
|
|
89
|
+
onBlock?: (b: unknown) => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runAdapter(script: string, opts: RunOpts = {}) {
|
|
93
|
+
const adapter = new HermesAgentAdapter({ binary: script });
|
|
94
|
+
const ctrl = new AbortController();
|
|
95
|
+
return adapter.run({
|
|
96
|
+
text: "hello",
|
|
97
|
+
sessionId: opts.sessionId ?? null,
|
|
98
|
+
accountId: opts.accountId ?? "ag_hermes_test",
|
|
99
|
+
cwd: tmpRoot,
|
|
100
|
+
signal: ctrl.signal,
|
|
101
|
+
trustLevel: opts.trustLevel ?? "owner",
|
|
102
|
+
systemContext: opts.systemContext,
|
|
103
|
+
onBlock: opts.onBlock as never,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe("HermesAgentAdapter", () => {
|
|
108
|
+
it("happy path: initialize → session/new → session/prompt with streamed text", async () => {
|
|
109
|
+
const script = makeAcpServer(
|
|
110
|
+
"happy.js",
|
|
111
|
+
`
|
|
112
|
+
if (msg.method === "initialize") {
|
|
113
|
+
reply(msg, { protocolVersion: 1, agentInfo: { name: "mock", version: "0.0.1" }, agentCapabilities: {} });
|
|
114
|
+
} else if (msg.method === "session/new") {
|
|
115
|
+
reply(msg, { sessionId: "sess-001" });
|
|
116
|
+
} else if (msg.method === "session/prompt") {
|
|
117
|
+
notify("session/update", { sessionId: msg.params.sessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "hi " } } });
|
|
118
|
+
notify("session/update", { sessionId: msg.params.sessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "world" } } });
|
|
119
|
+
reply(msg, { stopReason: "end_turn" });
|
|
120
|
+
process.stdin.pause();
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
`,
|
|
124
|
+
);
|
|
125
|
+
const blocks: any[] = [];
|
|
126
|
+
const res = await runAdapter(script, { onBlock: (b) => blocks.push(b) });
|
|
127
|
+
expect(res.error).toBeUndefined();
|
|
128
|
+
expect(res.newSessionId).toBe("sess-001");
|
|
129
|
+
expect(res.text).toBe("hi world");
|
|
130
|
+
expect(blocks.map((b) => b.kind)).toContain("assistant_text");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("session/load success → reuses incoming sessionId", async () => {
|
|
134
|
+
const script = makeAcpServer(
|
|
135
|
+
"load.js",
|
|
136
|
+
`
|
|
137
|
+
if (msg.method === "initialize") {
|
|
138
|
+
reply(msg, { protocolVersion: 1 });
|
|
139
|
+
} else if (msg.method === "session/load") {
|
|
140
|
+
// Hermes returns null/empty body without sessionId — adapter must
|
|
141
|
+
// reuse opts.sessionId.
|
|
142
|
+
reply(msg, {});
|
|
143
|
+
} else if (msg.method === "session/new") {
|
|
144
|
+
reply(msg, { sessionId: "should-not-be-used" });
|
|
145
|
+
} else if (msg.method === "session/prompt") {
|
|
146
|
+
notify("session/update", { sessionId: msg.params.sessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "resumed" } } });
|
|
147
|
+
reply(msg, { stopReason: "end_turn" });
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
`,
|
|
151
|
+
);
|
|
152
|
+
const res = await runAdapter(script, { sessionId: "sess-existing" });
|
|
153
|
+
expect(res.newSessionId).toBe("sess-existing");
|
|
154
|
+
expect(res.text).toBe("resumed");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("session/load failure → falls back to session/new", async () => {
|
|
158
|
+
const script = makeAcpServer(
|
|
159
|
+
"load-miss.js",
|
|
160
|
+
`
|
|
161
|
+
if (msg.method === "initialize") {
|
|
162
|
+
reply(msg, { protocolVersion: 1 });
|
|
163
|
+
} else if (msg.method === "session/load") {
|
|
164
|
+
err(msg, -32000, "not found");
|
|
165
|
+
} else if (msg.method === "session/new") {
|
|
166
|
+
reply(msg, { sessionId: "sess-fresh" });
|
|
167
|
+
} else if (msg.method === "session/prompt") {
|
|
168
|
+
reply(msg, { stopReason: "end_turn" });
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
`,
|
|
172
|
+
);
|
|
173
|
+
const res = await runAdapter(script, { sessionId: "sess-stale" });
|
|
174
|
+
expect(res.newSessionId).toBe("sess-fresh");
|
|
175
|
+
expect(res.error).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("owner trust → request_permission selects an allow_* option", async () => {
|
|
179
|
+
const script = makeAcpServer(
|
|
180
|
+
"perm-allow.js",
|
|
181
|
+
`
|
|
182
|
+
if (msg.method === "initialize") {
|
|
183
|
+
reply(msg, { protocolVersion: 1 });
|
|
184
|
+
} else if (msg.method === "session/new") {
|
|
185
|
+
reply(msg, { sessionId: "sess-perm" });
|
|
186
|
+
} else if (msg.method === "session/prompt") {
|
|
187
|
+
const outcome = await request("session/request_permission", {
|
|
188
|
+
sessionId: msg.params.sessionId,
|
|
189
|
+
toolCall: { name: "shell", rawInput: { cmd: "ls" } },
|
|
190
|
+
options: [
|
|
191
|
+
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
192
|
+
{ optionId: "deny", name: "Deny", kind: "reject_once" }
|
|
193
|
+
]
|
|
194
|
+
});
|
|
195
|
+
notify("session/update", { sessionId: msg.params.sessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "outcome=" + JSON.stringify(outcome) } } });
|
|
196
|
+
reply(msg, { stopReason: "end_turn" });
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
`,
|
|
200
|
+
);
|
|
201
|
+
const res = await runAdapter(script, { trustLevel: "owner" });
|
|
202
|
+
expect(res.text).toContain('"outcome":"selected"');
|
|
203
|
+
expect(res.text).toContain('"optionId":"allow"');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("public trust → request_permission cancels", async () => {
|
|
207
|
+
const script = makeAcpServer(
|
|
208
|
+
"perm-deny.js",
|
|
209
|
+
`
|
|
210
|
+
if (msg.method === "initialize") {
|
|
211
|
+
reply(msg, { protocolVersion: 1 });
|
|
212
|
+
} else if (msg.method === "session/new") {
|
|
213
|
+
reply(msg, { sessionId: "sess-perm" });
|
|
214
|
+
} else if (msg.method === "session/prompt") {
|
|
215
|
+
const outcome = await request("session/request_permission", {
|
|
216
|
+
sessionId: msg.params.sessionId,
|
|
217
|
+
options: [
|
|
218
|
+
{ optionId: "allow", kind: "allow_once" },
|
|
219
|
+
{ optionId: "deny", kind: "reject_once" }
|
|
220
|
+
]
|
|
221
|
+
});
|
|
222
|
+
notify("session/update", { sessionId: msg.params.sessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: JSON.stringify(outcome) } } });
|
|
223
|
+
reply(msg, { stopReason: "end_turn" });
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
`,
|
|
227
|
+
);
|
|
228
|
+
const res = await runAdapter(script, { trustLevel: "public" });
|
|
229
|
+
expect(res.text).toContain('"outcome":"cancelled"');
|
|
230
|
+
expect(res.text).not.toContain("optionId");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("writes systemContext to <hermes-workspace>/AGENTS.md before spawn", async () => {
|
|
234
|
+
const script = makeAcpServer(
|
|
235
|
+
"ctx.js",
|
|
236
|
+
`
|
|
237
|
+
if (msg.method === "initialize") { reply(msg, { protocolVersion: 1 }); }
|
|
238
|
+
else if (msg.method === "session/new") { reply(msg, { sessionId: "sess-ctx" }); }
|
|
239
|
+
else if (msg.method === "session/prompt") { reply(msg, { stopReason: "end_turn" }); process.exit(0); }
|
|
240
|
+
`,
|
|
241
|
+
);
|
|
242
|
+
await runAdapter(script, {
|
|
243
|
+
accountId: "ag_ctx_test",
|
|
244
|
+
systemContext: "MEMORY: remember X\nDIGEST: room Y",
|
|
245
|
+
});
|
|
246
|
+
const ws = agentHermesWorkspaceDir("ag_ctx_test");
|
|
247
|
+
const agentsMd = path.join(ws, "AGENTS.md");
|
|
248
|
+
expect(existsSync(agentsMd)).toBe(true);
|
|
249
|
+
const body = readFileSync(agentsMd, "utf8");
|
|
250
|
+
expect(body).toContain("MEMORY: remember X");
|
|
251
|
+
expect(body).toContain("DIGEST: room Y");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("session/new uses hermes-workspace as cwd, not the route cwd", async () => {
|
|
255
|
+
const script = makeAcpServer(
|
|
256
|
+
"cwd-echo.js",
|
|
257
|
+
`
|
|
258
|
+
if (msg.method === "initialize") { reply(msg, { protocolVersion: 1 }); }
|
|
259
|
+
else if (msg.method === "session/new") {
|
|
260
|
+
reply(msg, { sessionId: "sess-cwd" });
|
|
261
|
+
// Echo back the cwd we received via a streamed update on prompt.
|
|
262
|
+
this.__cwd = msg.params && msg.params.cwd;
|
|
263
|
+
} else if (msg.method === "session/prompt") {
|
|
264
|
+
notify("session/update", { sessionId: msg.params.sessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: this.__cwd || "" } } });
|
|
265
|
+
reply(msg, { stopReason: "end_turn" });
|
|
266
|
+
process.exit(0);
|
|
267
|
+
}
|
|
268
|
+
`,
|
|
269
|
+
);
|
|
270
|
+
const res = await runAdapter(script, { accountId: "ag_cwd_test" });
|
|
271
|
+
expect(res.text).toBe(agentHermesWorkspaceDir("ag_cwd_test"));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("returns early when signal is already aborted", async () => {
|
|
275
|
+
const script = makeAcpServer("noop.js", "");
|
|
276
|
+
const adapter = new HermesAgentAdapter({ binary: script });
|
|
277
|
+
const ctrl = new AbortController();
|
|
278
|
+
ctrl.abort();
|
|
279
|
+
const res = await adapter.run({
|
|
280
|
+
text: "x",
|
|
281
|
+
sessionId: null,
|
|
282
|
+
accountId: "ag_hermes_test",
|
|
283
|
+
cwd: tmpRoot,
|
|
284
|
+
signal: ctrl.signal,
|
|
285
|
+
trustLevel: "owner",
|
|
286
|
+
});
|
|
287
|
+
expect(res.text).toBe("");
|
|
288
|
+
expect(res.error).toMatch(/aborted before spawn/);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("surfaces non-zero exit with stderr snippet", async () => {
|
|
292
|
+
const p = path.join(tmpRoot, "boom.js");
|
|
293
|
+
writeFileSync(
|
|
294
|
+
p,
|
|
295
|
+
`#!/usr/bin/env node\nprocess.stderr.write("hermes auth required\\n"); process.exit(2);\n`,
|
|
296
|
+
{ mode: 0o755 },
|
|
297
|
+
);
|
|
298
|
+
chmodSync(p, 0o755);
|
|
299
|
+
const res = await runAdapter(p);
|
|
300
|
+
expect(res.error).toBeDefined();
|
|
301
|
+
});
|
|
302
|
+
});
|