@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.
Files changed (93) hide show
  1. package/dist/agent-discovery.d.ts +7 -3
  2. package/dist/agent-discovery.js +9 -1
  3. package/dist/agent-workspace.d.ts +62 -0
  4. package/dist/agent-workspace.js +140 -10
  5. package/dist/config.d.ts +49 -1
  6. package/dist/config.js +57 -1
  7. package/dist/control-channel.d.ts +1 -4
  8. package/dist/control-channel.js +1 -4
  9. package/dist/daemon-config-map.d.ts +29 -12
  10. package/dist/daemon-config-map.js +105 -8
  11. package/dist/daemon.d.ts +2 -0
  12. package/dist/daemon.js +52 -5
  13. package/dist/doctor.d.ts +27 -1
  14. package/dist/doctor.js +22 -1
  15. package/dist/gateway/cli-resolver.d.ts +34 -0
  16. package/dist/gateway/cli-resolver.js +74 -0
  17. package/dist/gateway/dispatcher.d.ts +66 -1
  18. package/dist/gateway/dispatcher.js +583 -56
  19. package/dist/gateway/gateway.d.ts +29 -1
  20. package/dist/gateway/gateway.js +10 -0
  21. package/dist/gateway/index.d.ts +2 -0
  22. package/dist/gateway/index.js +2 -0
  23. package/dist/gateway/policy-resolver.d.ts +57 -0
  24. package/dist/gateway/policy-resolver.js +123 -0
  25. package/dist/gateway/runtimes/acp-stream.d.ts +99 -0
  26. package/dist/gateway/runtimes/acp-stream.js +394 -0
  27. package/dist/gateway/runtimes/codex.js +7 -0
  28. package/dist/gateway/runtimes/hermes-agent.d.ts +83 -0
  29. package/dist/gateway/runtimes/hermes-agent.js +180 -0
  30. package/dist/gateway/runtimes/ndjson-stream.d.ts +7 -2
  31. package/dist/gateway/runtimes/ndjson-stream.js +16 -3
  32. package/dist/gateway/runtimes/openclaw-acp.d.ts +44 -0
  33. package/dist/gateway/runtimes/openclaw-acp.js +500 -0
  34. package/dist/gateway/runtimes/registry.d.ts +4 -0
  35. package/dist/gateway/runtimes/registry.js +22 -0
  36. package/dist/gateway/transcript-paths.d.ts +30 -0
  37. package/dist/gateway/transcript-paths.js +114 -0
  38. package/dist/gateway/transcript.d.ts +123 -0
  39. package/dist/gateway/transcript.js +147 -0
  40. package/dist/gateway/types.d.ts +31 -0
  41. package/dist/index.js +286 -27
  42. package/dist/mention-scan.d.ts +22 -0
  43. package/dist/mention-scan.js +35 -0
  44. package/dist/provision.d.ts +73 -3
  45. package/dist/provision.js +373 -12
  46. package/dist/system-context.d.ts +5 -4
  47. package/dist/system-context.js +35 -5
  48. package/dist/turn-text.js +20 -1
  49. package/dist/url-utils.d.ts +9 -0
  50. package/dist/url-utils.js +18 -0
  51. package/dist/user-auth.js +0 -2
  52. package/dist/working-memory.js +1 -1
  53. package/package.json +2 -1
  54. package/src/__tests__/agent-workspace.test.ts +93 -0
  55. package/src/__tests__/daemon-config-map.test.ts +79 -0
  56. package/src/__tests__/openclaw-acp.test.ts +234 -0
  57. package/src/__tests__/policy-resolver.test.ts +124 -0
  58. package/src/__tests__/policy-updated-handler.test.ts +144 -0
  59. package/src/__tests__/provision.test.ts +160 -0
  60. package/src/__tests__/system-context.test.ts +52 -0
  61. package/src/__tests__/url-utils.test.ts +37 -0
  62. package/src/agent-discovery.ts +12 -4
  63. package/src/agent-workspace.ts +173 -9
  64. package/src/config.ts +132 -4
  65. package/src/control-channel.ts +1 -4
  66. package/src/daemon-config-map.ts +156 -12
  67. package/src/daemon.ts +66 -5
  68. package/src/doctor.ts +49 -2
  69. package/src/gateway/__tests__/dispatcher.test.ts +440 -2
  70. package/src/gateway/__tests__/hermes-agent-adapter.test.ts +302 -0
  71. package/src/gateway/__tests__/transcript.test.ts +496 -0
  72. package/src/gateway/cli-resolver.ts +92 -0
  73. package/src/gateway/dispatcher.ts +681 -58
  74. package/src/gateway/gateway.ts +46 -0
  75. package/src/gateway/index.ts +25 -0
  76. package/src/gateway/policy-resolver.ts +171 -0
  77. package/src/gateway/runtimes/acp-stream.ts +535 -0
  78. package/src/gateway/runtimes/codex.ts +7 -0
  79. package/src/gateway/runtimes/hermes-agent.ts +206 -0
  80. package/src/gateway/runtimes/ndjson-stream.ts +16 -3
  81. package/src/gateway/runtimes/openclaw-acp.ts +606 -0
  82. package/src/gateway/runtimes/registry.ts +24 -0
  83. package/src/gateway/transcript-paths.ts +145 -0
  84. package/src/gateway/transcript.ts +300 -0
  85. package/src/gateway/types.ts +32 -0
  86. package/src/index.ts +295 -30
  87. package/src/mention-scan.ts +38 -0
  88. package/src/provision.ts +446 -20
  89. package/src/system-context.ts +41 -9
  90. package/src/turn-text.ts +22 -1
  91. package/src/url-utils.ts +17 -0
  92. package/src/user-auth.ts +0 -2
  93. 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
- /** Override to tweak env (FORCE_COLOR=0, NO_COLOR=1, etc). */
77
- protected spawnEnv(_opts: RuntimeRunOptions): NodeJS.ProcessEnv {
78
- return process.env;
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> {