@botcord/daemon 0.2.5 → 0.2.8
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 +4 -0
- package/dist/agent-discovery.js +8 -0
- package/dist/agent-workspace.d.ts +62 -0
- package/dist/agent-workspace.js +140 -8
- package/dist/config.d.ts +64 -1
- package/dist/config.js +73 -1
- package/dist/daemon-config-map.d.ts +27 -9
- package/dist/daemon-config-map.js +105 -8
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +76 -6
- 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 +31 -1
- package/dist/gateway/dispatcher.js +337 -29
- 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 +309 -27
- package/dist/mention-scan.d.ts +22 -0
- package/dist/mention-scan.js +35 -0
- package/dist/openclaw-discovery.d.ts +28 -0
- package/dist/openclaw-discovery.js +228 -0
- package/dist/provision.d.ts +113 -1
- package/dist/provision.js +564 -12
- package/dist/system-context.d.ts +5 -4
- package/dist/system-context.js +35 -5
- package/dist/url-utils.d.ts +9 -0
- package/dist/url-utils.js +18 -0
- package/package.json +3 -2
- 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__/openclaw-discovery.test.ts +150 -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 +265 -0
- package/src/__tests__/system-context.test.ts +52 -0
- package/src/__tests__/url-utils.test.ts +37 -0
- package/src/agent-discovery.ts +8 -0
- package/src/agent-workspace.ts +173 -7
- package/src/config.ts +168 -4
- package/src/daemon-config-map.ts +154 -9
- package/src/daemon.ts +96 -6
- package/src/doctor.ts +49 -2
- package/src/gateway/__tests__/dispatcher.test.ts +65 -0
- 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 +394 -26
- 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 +321 -30
- package/src/mention-scan.ts +38 -0
- package/src/openclaw-discovery.ts +262 -0
- package/src/provision.ts +682 -14
- package/src/system-context.ts +41 -9
- package/src/url-utils.ts +17 -0
package/src/doctor.ts
CHANGED
|
@@ -31,9 +31,34 @@ export interface DoctorHttpResult {
|
|
|
31
31
|
error?: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** One endpoint probe entry, mirrored from `RuntimeEndpointProbe`. */
|
|
35
|
+
export interface DoctorRuntimeEndpoint {
|
|
36
|
+
name: string;
|
|
37
|
+
url: string;
|
|
38
|
+
reachable: boolean;
|
|
39
|
+
version?: string;
|
|
40
|
+
error?: string;
|
|
41
|
+
agents?: Array<{
|
|
42
|
+
id: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
workspace?: string;
|
|
45
|
+
model?: { name?: string; provider?: string };
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Optional warning surfaced by the doctor: e.g. botcord plugin loaded on
|
|
49
|
+
* the gateway (would form a daemon → openclaw → botcord → Hub loop).
|
|
50
|
+
*/
|
|
51
|
+
warnings?: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Augmented runtime entry that may carry endpoint probe results. */
|
|
55
|
+
export interface DoctorRuntimeEntry extends RuntimeProbeEntry {
|
|
56
|
+
endpoints?: DoctorRuntimeEndpoint[];
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
/** Input for the rendered doctor output. */
|
|
35
60
|
export interface DoctorInput {
|
|
36
|
-
runtimes:
|
|
61
|
+
runtimes: DoctorRuntimeEntry[];
|
|
37
62
|
channels: ChannelProbeResult[];
|
|
38
63
|
}
|
|
39
64
|
|
|
@@ -226,10 +251,32 @@ export function renderDoctor(input: DoctorInput): string {
|
|
|
226
251
|
lines.push(
|
|
227
252
|
`${pad("RUNTIME", widths.runtime)} ${pad("NAME", widths.name)} ${pad("STATUS", widths.status)} ${pad("VERSION", widths.version)} PATH`,
|
|
228
253
|
);
|
|
229
|
-
for (
|
|
254
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
255
|
+
const r = rows[i];
|
|
256
|
+
const e = input.runtimes[i];
|
|
230
257
|
lines.push(
|
|
231
258
|
`${pad(r.runtime, widths.runtime)} ${pad(r.name, widths.name)} ${pad(r.status, widths.status)} ${pad(r.version, widths.version)} ${r.path}`,
|
|
232
259
|
);
|
|
260
|
+
if (e.endpoints && e.endpoints.length > 0) {
|
|
261
|
+
for (const ep of e.endpoints) {
|
|
262
|
+
const mark = ep.reachable ? "✓" : "✗";
|
|
263
|
+
const detail = ep.reachable
|
|
264
|
+
? ep.version ?? "ok"
|
|
265
|
+
: ep.error ?? "unreachable";
|
|
266
|
+
lines.push(` gateway ${pad(`"${ep.name}"`, 16)} ${pad(ep.url, 40)} ${mark} ${detail}`);
|
|
267
|
+
if (ep.agents && ep.agents.length > 0) {
|
|
268
|
+
// RFC §3.8.4: list by `id` (stable key); show display name when distinct.
|
|
269
|
+
lines.push(
|
|
270
|
+
` agents (id): ${ep.agents
|
|
271
|
+
.map((a) => (a.name && a.name !== a.id ? `${a.id} (${a.name})` : a.id))
|
|
272
|
+
.join(", ")}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (ep.warnings) {
|
|
276
|
+
for (const w of ep.warnings) lines.push(` WARN: ${w}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
233
280
|
}
|
|
234
281
|
const available = input.runtimes.filter((e) => e.result.available).length;
|
|
235
282
|
lines.push(`\n${available}/${input.runtimes.length} runtimes available`);
|
|
@@ -1551,6 +1551,71 @@ describe("Dispatcher", () => {
|
|
|
1551
1551
|
expect(composedItemCounts[1]).toBeLessThanOrEqual(8);
|
|
1552
1552
|
});
|
|
1553
1553
|
|
|
1554
|
+
it("attentionGate=false skips the runtime turn but still acks and runs onInbound (PR3)", async () => {
|
|
1555
|
+
const runtime = new FakeRuntime();
|
|
1556
|
+
const { store, dir } = await makeStore();
|
|
1557
|
+
tempDirs.push(dir);
|
|
1558
|
+
const channel = new FakeChannel();
|
|
1559
|
+
const channels = new Map<string, ChannelAdapter>([[channel.id, channel]]);
|
|
1560
|
+
const accept = vi.fn(async () => {});
|
|
1561
|
+
const onInbound = vi.fn();
|
|
1562
|
+
const attentionGate = vi.fn(async () => false);
|
|
1563
|
+
const dispatcher = new Dispatcher({
|
|
1564
|
+
config: baseConfig(),
|
|
1565
|
+
channels,
|
|
1566
|
+
runtime: () => runtime,
|
|
1567
|
+
sessionStore: store,
|
|
1568
|
+
log: silentLogger(),
|
|
1569
|
+
onInbound,
|
|
1570
|
+
attentionGate,
|
|
1571
|
+
});
|
|
1572
|
+
await dispatcher.handle(makeEnvelope({ id: "m_gated", text: "hello" }, { accept }));
|
|
1573
|
+
expect(accept).toHaveBeenCalledTimes(1);
|
|
1574
|
+
expect(onInbound).toHaveBeenCalledTimes(1);
|
|
1575
|
+
expect(attentionGate).toHaveBeenCalledTimes(1);
|
|
1576
|
+
expect(runtime.calls.length).toBe(0);
|
|
1577
|
+
expect(channel.sends.length).toBe(0);
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
it("attentionGate=true wakes the runtime as usual (PR3)", async () => {
|
|
1581
|
+
const runtime = new FakeRuntime({ reply: "ok", newSessionId: "sid-1" });
|
|
1582
|
+
const { store, dir } = await makeStore();
|
|
1583
|
+
tempDirs.push(dir);
|
|
1584
|
+
const channel = new FakeChannel();
|
|
1585
|
+
const channels = new Map<string, ChannelAdapter>([[channel.id, channel]]);
|
|
1586
|
+
const dispatcher = new Dispatcher({
|
|
1587
|
+
config: baseConfig(),
|
|
1588
|
+
channels,
|
|
1589
|
+
runtime: () => runtime,
|
|
1590
|
+
sessionStore: store,
|
|
1591
|
+
log: silentLogger(),
|
|
1592
|
+
attentionGate: () => true,
|
|
1593
|
+
});
|
|
1594
|
+
await dispatcher.handle(makeEnvelope({ id: "m_wake" }));
|
|
1595
|
+
expect(runtime.calls.length).toBe(1);
|
|
1596
|
+
expect(channel.sends.length).toBe(1);
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
it("attentionGate throwing fails open and runs the turn (PR3)", async () => {
|
|
1600
|
+
const runtime = new FakeRuntime({ reply: "ok", newSessionId: "sid-1" });
|
|
1601
|
+
const { store, dir } = await makeStore();
|
|
1602
|
+
tempDirs.push(dir);
|
|
1603
|
+
const channel = new FakeChannel();
|
|
1604
|
+
const channels = new Map<string, ChannelAdapter>([[channel.id, channel]]);
|
|
1605
|
+
const dispatcher = new Dispatcher({
|
|
1606
|
+
config: baseConfig(),
|
|
1607
|
+
channels,
|
|
1608
|
+
runtime: () => runtime,
|
|
1609
|
+
sessionStore: store,
|
|
1610
|
+
log: silentLogger(),
|
|
1611
|
+
attentionGate: () => {
|
|
1612
|
+
throw new Error("boom");
|
|
1613
|
+
},
|
|
1614
|
+
});
|
|
1615
|
+
await dispatcher.handle(makeEnvelope({ id: "m_wake_throw" }));
|
|
1616
|
+
expect(runtime.calls.length).toBe(1);
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1554
1619
|
it("owner-chat detection: dashboard_user_chat in non-rm_oc room still sends reply", async () => {
|
|
1555
1620
|
const runtime = new FakeRuntime({ reply: "ok", newSessionId: "sid-1" });
|
|
1556
1621
|
const { dispatcher, channel } = await scaffold({
|
|
@@ -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
|
+
});
|