@getbourdon/participants 0.2.0 → 0.2.1

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.
@@ -1,426 +0,0 @@
1
- /**
2
- * Hermes participant — normalize Hermes Agent (Nous Research) memory + sessions
3
- * into L5. Port of `participants/hermes.py`.
4
- *
5
- * Reads two surfaces under `~/.hermes/` (override `$HERMES_HOME`):
6
- * * `state.db` — a SQLite store (`sessions` + `messages` tables). Recent
7
- * non-archived sessions + project entities (from cwd basename) come from it.
8
- * * `memories/{memory.md,user.md}` — durable cross-session facts (one
9
- * Markdown bullet/line per memory). The highest-signal entities.
10
- *
11
- * Read-only (`mode=ro`), deterministic, visibility-enforced-before-emission,
12
- * and redaction on every native string. `healthCheck` never throws; `discover`
13
- * raises only {@link ParticipantDiscoveryError}.
14
- */
15
- import { readFileSync, statSync } from "node:fs";
16
- import { homedir } from "node:os";
17
- import { join } from "node:path";
18
- import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
19
- import { redactText } from "@getbourdon/redaction";
20
- import { CONTRACT_VERSION, ParticipantDiscoveryError, SPEC_VERSION, } from "../base.js";
21
- import { epochToIsoDate, friendlyLabel, projectKeyFromCwd, tableColumns, tableExists, tryOpenReadonly, } from "../sqlite-base.js";
22
- const AGENT_ID = "hermes";
23
- const AGENT_TYPE = "code-assistant";
24
- const DISPLAY_NAME = "Hermes Agent";
25
- const ROLE_NARRATIVE = "General-purpose tool-calling assistant (Nous Research). Operates across CLI, " +
26
- "TUI, and messaging gateways (Slack/Telegram/Discord/WhatsApp); runs terminal, " +
27
- "file, web, and delegation toolsets. Carries durable cross-session memory and a " +
28
- "curated skill library. Federates session + memory context, not vendor account.";
29
- const DEFAULT_SESSION_LIMIT = 100;
30
- /** Hermes memory stores live as Markdown under `~/.hermes/memories/`. */
31
- const MEMORY_FILENAMES = ["memory.md", "user.md"];
32
- const DEFAULT_POLICY = makeVisibilityPolicy({
33
- default: Visibility.TEAM,
34
- private_tags: ["personal", "financial", "credential", "secret", "health", "family", "legal"],
35
- team_tags: ["hermes-memory", "hermes-session", "hermes-project"],
36
- });
37
- /** cwd basenames that carry no project identity. */
38
- const GENERIC_PROJECT_NAMES = new Set([
39
- "",
40
- "root",
41
- "home",
42
- "tmp",
43
- "temp",
44
- "desktop",
45
- "documents",
46
- "downloads",
47
- "src",
48
- ]);
49
- // -- Path resolution ----------------------------------------------------------
50
- /**
51
- * Conventional Hermes home used by the setup wizard's detection.
52
- * Precedence: explicit `home` > `$HERMES_HOME` > `~/.hermes`.
53
- */
54
- export function defaultNativePath(home) {
55
- if (home)
56
- return join(home, ".hermes");
57
- const env = process.env.HERMES_HOME;
58
- if (env)
59
- return env;
60
- return join(homedir(), ".hermes");
61
- }
62
- function isDir(p) {
63
- try {
64
- return statSync(p).isDirectory();
65
- }
66
- catch {
67
- return false;
68
- }
69
- }
70
- function isFile(p) {
71
- try {
72
- return statSync(p).isFile();
73
- }
74
- catch {
75
- return false;
76
- }
77
- }
78
- /** Redact secrets, collapse whitespace, and clamp length. */
79
- function bounded(value, limit = 240) {
80
- const cleaned = redactText((value || "").replace(/\s+/g, " ").trim(), limit);
81
- return cleaned.trim();
82
- }
83
- function projectKey(cwd) {
84
- return projectKeyFromCwd(cwd, GENERIC_PROJECT_NAMES);
85
- }
86
- // -- SQLite (read-only) -------------------------------------------------------
87
- /** Pull recent, non-archived sessions newest-first. Never raises. */
88
- function collectSessionRows(stateDb, limit = DEFAULT_SESSION_LIMIT) {
89
- if (!isFile(stateDb))
90
- return [];
91
- const conn = tryOpenReadonly(stateDb);
92
- if (!conn)
93
- return [];
94
- const out = [];
95
- try {
96
- if (!tableExists(conn, "sessions"))
97
- return [];
98
- // `archived` may be absent in older schemas — guard the column.
99
- const cols = tableColumns(conn, "sessions");
100
- const archivedClause = cols.has("archived") ? "WHERE COALESCE(archived, 0) = 0" : "";
101
- const rows = conn
102
- .prepare(`SELECT id, source, model, title, cwd, started_at, ended_at,
103
- message_count, tool_call_count
104
- FROM sessions
105
- ${archivedClause}
106
- ORDER BY started_at DESC
107
- LIMIT ?`)
108
- .all(limit);
109
- for (const r of rows) {
110
- const day = epochToIsoDate(r["started_at"]);
111
- if (!day)
112
- continue;
113
- out.push({
114
- id: String(r["id"]),
115
- source: String(r["source"] ?? "").toLowerCase(),
116
- model: r["model"],
117
- title: r["title"] ?? null,
118
- cwd: r["cwd"] ?? null,
119
- date: day,
120
- messageCount: Number(r["message_count"] ?? 0) || 0,
121
- toolCallCount: Number(r["tool_call_count"] ?? 0) || 0,
122
- });
123
- }
124
- }
125
- catch {
126
- /* degrade to whatever rows we gathered */
127
- }
128
- finally {
129
- conn.close();
130
- }
131
- return out;
132
- }
133
- /** Distinct tool names used in a session, for key_actions evidence. */
134
- function toolNamesForSession(conn, sessionId) {
135
- if (!tableExists(conn, "messages"))
136
- return [];
137
- try {
138
- const rows = conn
139
- .prepare(`SELECT DISTINCT tool_name FROM messages
140
- WHERE session_id = ? AND tool_name IS NOT NULL AND tool_name != ''`)
141
- .all(sessionId);
142
- const names = new Set();
143
- for (const r of rows) {
144
- if (r.tool_name)
145
- names.add(r.tool_name);
146
- }
147
- return [...names].sort();
148
- }
149
- catch {
150
- return [];
151
- }
152
- }
153
- // -- Memory parsing -----------------------------------------------------------
154
- const BULLET_RE = /^\s*[-*]\s+(.*)$/;
155
- /** Return memory entries (one per bullet / non-empty line). Never raises. */
156
- function parseMemoryFile(path) {
157
- let text;
158
- try {
159
- text = readFileSync(path, "utf8");
160
- }
161
- catch {
162
- return [];
163
- }
164
- const entries = [];
165
- for (const line of text.split(/\r?\n/)) {
166
- const m = BULLET_RE.exec(line);
167
- const candidate = m ? (m[1] ?? "") : line.trim();
168
- if (candidate && !candidate.startsWith("#"))
169
- entries.push(candidate);
170
- }
171
- return entries;
172
- }
173
- /** Map memory store name → list of entry strings. */
174
- function collectMemoryEntries(home) {
175
- const memDir = join(home, "memories");
176
- const result = [];
177
- if (!isDir(memDir))
178
- return result;
179
- for (const fname of MEMORY_FILENAMES) {
180
- const path = join(memDir, fname);
181
- if (isFile(path)) {
182
- const entries = parseMemoryFile(path);
183
- if (entries.length) {
184
- const stem = fname.replace(/\.md$/, "");
185
- result.push([stem, entries]);
186
- }
187
- }
188
- }
189
- return result;
190
- }
191
- // -- Entity / session builders ------------------------------------------------
192
- function buildSession(record, toolNames) {
193
- const key = projectKey(record.cwd);
194
- const focus = [];
195
- if (key)
196
- focus.push(key);
197
- const keyActions = [];
198
- if (record.title)
199
- keyActions.push(bounded(record.title, 160));
200
- if (toolNames.length) {
201
- keyActions.push("Tools: " + toolNames.slice(0, 8).join(", "));
202
- }
203
- else if (record.toolCallCount) {
204
- keyActions.push(`${record.toolCallCount} tool call(s)`);
205
- }
206
- return makeSession({
207
- date: record.date,
208
- cwd: record.cwd || undefined,
209
- project_focus: focus,
210
- key_actions: keyActions,
211
- visibility: Visibility.TEAM,
212
- });
213
- }
214
- /** Turn a single memory line into a team-visibility entity. */
215
- function memoryEntity(store, entry) {
216
- const tag = store === "user" ? "hermes-user-memory" : "hermes-memory";
217
- return makeEntity({
218
- name: bounded(entry, 80),
219
- type: store === "user" ? "preference" : "fact",
220
- summary: bounded(entry, 240),
221
- tags: [tag],
222
- visibility: Visibility.TEAM,
223
- });
224
- }
225
- function projectEntities(records) {
226
- const counts = new Map();
227
- for (const r of records) {
228
- const key = projectKey(r.cwd);
229
- if (key)
230
- counts.set(key, (counts.get(key) ?? 0) + 1);
231
- }
232
- const entities = [];
233
- for (const [key, count] of counts) {
234
- let last;
235
- for (const r of records) {
236
- if (projectKey(r.cwd) === key) {
237
- if (last === undefined || r.date > last)
238
- last = r.date;
239
- }
240
- }
241
- entities.push(makeEntity({
242
- name: friendlyLabel(key),
243
- type: "project",
244
- summary: `Workspace observed across ${count} Hermes session(s).`,
245
- aliases: [key],
246
- last_touched: last,
247
- tags: ["hermes-project"],
248
- visibility: Visibility.TEAM,
249
- }));
250
- }
251
- return entities;
252
- }
253
- // -- Participant --------------------------------------------------------------
254
- /** External participant for local Hermes Agent memory + sessions. */
255
- export class HermesParticipant {
256
- agentId = AGENT_ID;
257
- agentType = AGENT_TYPE;
258
- displayName = DISPLAY_NAME;
259
- nativePath;
260
- home;
261
- static defaultNativePath(home) {
262
- return defaultNativePath(home);
263
- }
264
- /** `hermesHome` is the `.hermes` directory itself (matching the Python ctor). */
265
- constructor(hermesHome) {
266
- if (hermesHome) {
267
- this.home = hermesHome;
268
- }
269
- else {
270
- const path = defaultNativePath();
271
- this.home = isDir(path) ? path : null;
272
- }
273
- this.nativePath = this.home ?? join(homedir(), ".hermes");
274
- }
275
- sources() {
276
- const home = this.home;
277
- const stateDb = home ? join(home, "state.db") : null;
278
- const memDir = home ? join(home, "memories") : null;
279
- const skillsDir = home ? join(home, "skills") : null;
280
- return {
281
- hermes_home: home,
282
- state_db: stateDb && isFile(stateDb) ? stateDb : null,
283
- memories_dir: memDir && isDir(memDir) ? memDir : null,
284
- skills_dir: skillsDir && isDir(skillsDir) ? skillsDir : null,
285
- };
286
- }
287
- discover() {
288
- const sources = this.sources();
289
- if (!Object.values(sources).some((v) => v)) {
290
- throw new ParticipantDiscoveryError("No Hermes memory sources found. Expected ~/.hermes/ (set $HERMES_HOME to override).");
291
- }
292
- if (!sources["state_db"] && !sources["memories_dir"]) {
293
- throw new ParticipantDiscoveryError(`Hermes home '${this.nativePath}' exists but has no state.db or memories/ — ` +
294
- "nothing to federate yet.");
295
- }
296
- return {
297
- path: this.nativePath,
298
- version: "hermes-home-v1",
299
- metadata: { sources },
300
- };
301
- }
302
- exportSessions(since, limit = DEFAULT_SESSION_LIMIT) {
303
- if (this.home === null)
304
- return [];
305
- const stateDb = join(this.home, "state.db");
306
- const cutoff = since ? since.toISOString().slice(0, 10) : null;
307
- const records = collectSessionRows(stateDb, limit);
308
- if (!records.length)
309
- return [];
310
- const conn = tryOpenReadonly(stateDb);
311
- const out = [];
312
- try {
313
- for (const rec of records) {
314
- if (cutoff && rec.date < cutoff)
315
- continue;
316
- const tools = conn ? toolNamesForSession(conn, rec.id) : [];
317
- out.push(buildSession(rec, tools));
318
- }
319
- }
320
- finally {
321
- if (conn)
322
- conn.close();
323
- }
324
- return out;
325
- }
326
- exportL5(since) {
327
- const store = this.discover();
328
- const sources = (store.metadata["sources"] ?? {});
329
- const capabilities = Object.entries(sources)
330
- .filter(([k, v]) => v && k !== "hermes_home")
331
- .map(([k]) => k)
332
- .sort();
333
- const home = this.home;
334
- const records = home ? collectSessionRows(join(home, "state.db"), DEFAULT_SESSION_LIMIT) : [];
335
- // Entities: project workspaces + memory-derived facts/preferences.
336
- const entities = projectEntities(records);
337
- const seen = new Set();
338
- for (const e of entities) {
339
- seen.add(`${e.type || "topic"}${e.name.toLowerCase()}`);
340
- }
341
- if (home) {
342
- for (const [storeName, entries] of collectMemoryEntries(home)) {
343
- for (const entry of entries) {
344
- const ent = memoryEntity(storeName, entry);
345
- const key = `${ent.type || "topic"}${ent.name.toLowerCase()}`;
346
- if (seen.has(key))
347
- continue;
348
- seen.add(key);
349
- entities.push(ent);
350
- }
351
- }
352
- }
353
- // Visibility enforced here, before emission (contract requirement).
354
- const visible = filterForFederation(entities, DEFAULT_POLICY);
355
- const sessions = this.exportSessions(since ?? new Date(0), DEFAULT_SESSION_LIMIT);
356
- return makeManifest({
357
- spec_version: SPEC_VERSION,
358
- agent: makeAgentInfo({
359
- id: this.agentId,
360
- type: this.agentType,
361
- role_narrative: ROLE_NARRATIVE,
362
- spec_version_compat: CONTRACT_VERSION,
363
- }),
364
- last_updated: new Date().toISOString(),
365
- capabilities,
366
- recent_sessions: sessions,
367
- known_entities: visible,
368
- visibility_policy: DEFAULT_POLICY,
369
- });
370
- }
371
- healthCheck() {
372
- const details = {
373
- hermes_home: this.nativePath,
374
- state_db: "missing",
375
- memories_dir: "missing",
376
- skills_dir: "missing",
377
- };
378
- try {
379
- if (this.home === null) {
380
- return {
381
- status: "blocked",
382
- reason: "~/.hermes/ not found — Hermes Agent not installed here",
383
- details,
384
- proposedFix: "Install Hermes Agent and run a session once, then `bourdon export-all`.",
385
- };
386
- }
387
- const sources = this.sources();
388
- for (const key of ["state_db", "memories_dir", "skills_dir"]) {
389
- if (sources[key])
390
- details[key] = sources[key];
391
- }
392
- if (sources["state_db"]) {
393
- const records = collectSessionRows(join(this.home, "state.db"), 1);
394
- if (records.length)
395
- return { status: "ok", details };
396
- return {
397
- status: "degraded",
398
- reason: "state.db present but no readable sessions yet",
399
- details,
400
- proposedFix: "Run at least one Hermes session, then `bourdon export-all`.",
401
- };
402
- }
403
- if (sources["memories_dir"]) {
404
- return {
405
- status: "degraded",
406
- reason: "memories/ present but state.db missing — session history unavailable",
407
- details,
408
- };
409
- }
410
- return {
411
- status: "blocked",
412
- reason: "Hermes home has neither state.db nor memories/",
413
- details,
414
- proposedFix: "Run a Hermes session to populate ~/.hermes/state.db.",
415
- };
416
- }
417
- catch (err) {
418
- // healthCheck must NEVER raise (contract).
419
- return {
420
- status: "degraded",
421
- reason: `unexpected error: ${String(err)}`,
422
- details,
423
- };
424
- }
425
- }
426
- }
@@ -1,78 +0,0 @@
1
- /**
2
- * OpenClaw participant — network-shaped, QUARANTINED-CLASS. Port of
3
- * `participants/openclaw.py` (v0.9.0, spec R4/D9).
4
- *
5
- * OpenClaw is the highest-demand adapter target in the ecosystem and also its
6
- * highest-risk agent class: CVE-2026-25253 (one-click RCE, CVSS 8.8, first
7
- * patched in 2026.1.29), tens of thousands of internet-exposed instances (~93%
8
- * without authentication), ClawHub's malicious-skill problem, and auth disabled
9
- * by default on port 8080. Bourdon therefore treats OpenClaw differently from
10
- * every on-disk participant:
11
- *
12
- * 1. **Network-shaped** — state is read from the OpenClaw instance's local
13
- * HTTP API, not on-disk artifacts. Because the {@link BourdonParticipant}
14
- * contract is SYNCHRONOUS (`discover`/`exportL5` return values, not
15
- * Promises) and `fetch` is async, the reads go through a tiny synchronous
16
- * `curl` shell-out (the same idiom as the github-copilot reader).
17
- * 2. **Hard handshake gate** — {@link OpenClawParticipant.discover} refuses to
18
- * talk to an instance that is unpatched (< 2026.1.29) or has authentication
19
- * disabled. These are refusals with exact reasons AND fixes, not warnings,
20
- * and every read re-runs the gate.
21
- * 3. **Quarantined-class** — `quarantinedClass = true`: registering the agent
22
- * trusted requires `--i-understand-the-risk`, and exports stage for
23
- * operator review (spec D6: quarantine follows the content, not the
24
- * invoker). Its content federates under TEAM, never PUBLIC.
25
- *
26
- * We gate on instance hygiene (version, auth) — NOT on auditing the user's
27
- * installed skills; that's ClawSecure et al.'s job (spec non-goal 3).
28
- */
29
- import { type L5ManifestModel, type SessionModel } from "@getbourdon/l5";
30
- import { type AgentStore, type BourdonParticipant, type HealthStatus } from "../base.js";
31
- /** First OpenClaw release that patches CVE-2026-25253 (one-click RCE). */
32
- export declare const MIN_PATCHED_VERSION = "2026.1.29";
33
- /**
34
- * Hard handshake preconditions (spec D9). Returns the version on success.
35
- * Raises {@link ParticipantDiscoveryError} with the exact reason AND the fix —
36
- * these are refusals, not warnings.
37
- */
38
- export declare function verifyInstance(status: Record<string, unknown>, url: string): string;
39
- /**
40
- * Minimal HTTP client for a local OpenClaw instance. Read-only: status,
41
- * sessions, memories. Failures raise {@link ParticipantDiscoveryError} with the
42
- * reason — the participant decides how to surface them.
43
- *
44
- * Error mapping (faithful to the Python `urllib` client): an HTTP >= 400
45
- * response -> "returned HTTP {code}"; a transport failure / timeout / non-JSON
46
- * body -> "instance unreachable at {url}".
47
- */
48
- export declare class OpenClawApiClient {
49
- readonly url: string;
50
- private readonly token;
51
- readonly timeout: number;
52
- constructor(url: string, token?: string, timeout?: number);
53
- private get;
54
- status(): Record<string, unknown>;
55
- sessions(): Record<string, unknown>[];
56
- memories(): Record<string, unknown>[];
57
- /** Shared `sessions()`/`memories()` body: try/except-returns-[], unwrap the
58
- * `{ "<key>": [...] }` envelope, and keep only object rows. */
59
- private readCollection;
60
- }
61
- /** Bourdon participant for OpenClaw (quarantined class). */
62
- export declare class OpenClawParticipant implements BourdonParticipant {
63
- agentId: string;
64
- agentType: string;
65
- displayName: string;
66
- /** Trust marker consumed by `bourdon agent add/set-tier` + export-all:
67
- * registering as trusted needs --i-understand-the-risk; exports stage. */
68
- quarantinedClass: boolean;
69
- readonly url: string;
70
- private readonly client;
71
- static defaultNativePath(home?: string): string;
72
- constructor(url?: string, token?: string, client?: OpenClawApiClient);
73
- get nativePath(): string;
74
- discover(): AgentStore;
75
- exportSessions(since?: Date, limit?: number): SessionModel[];
76
- exportL5(since?: Date): L5ManifestModel;
77
- healthCheck(): HealthStatus;
78
- }