@getbourdon/participants 0.2.3 → 0.2.4
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/base.d.ts +109 -0
- package/dist/base.js +88 -0
- package/dist/index.d.ts +42 -1083
- package/dist/index.js +53 -6590
- package/dist/network-base.d.ts +108 -0
- package/dist/network-base.js +248 -0
- package/dist/readers/cascade.d.ts +49 -0
- package/dist/readers/cascade.js +428 -0
- package/dist/readers/claude-code-automations.d.ts +62 -0
- package/dist/readers/claude-code-automations.js +693 -0
- package/dist/readers/claude-code.d.ts +50 -0
- package/dist/readers/claude-code.js +583 -0
- package/dist/readers/claude-desktop-code.d.ts +54 -0
- package/dist/readers/claude-desktop-code.js +526 -0
- package/dist/readers/claude-desktop-cowork.d.ts +47 -0
- package/dist/readers/claude-desktop-cowork.js +659 -0
- package/dist/readers/codex-automations.d.ts +55 -0
- package/dist/readers/codex-automations.js +519 -0
- package/dist/readers/codex-sessions.d.ts +88 -0
- package/dist/readers/codex-sessions.js +194 -0
- package/dist/readers/copilot-automations.d.ts +64 -0
- package/dist/readers/copilot-automations.js +530 -0
- package/dist/readers/copilot-cli.d.ts +45 -0
- package/dist/readers/copilot-cli.js +416 -0
- package/dist/readers/copilot-vscode.d.ts +54 -0
- package/dist/readers/copilot-vscode.js +553 -0
- package/dist/readers/copilot.d.ts +59 -0
- package/dist/readers/copilot.js +413 -0
- package/dist/readers/cursor-automations.d.ts +57 -0
- package/dist/readers/cursor-automations.js +685 -0
- package/dist/readers/cursor.d.ts +50 -0
- package/dist/readers/cursor.js +670 -0
- package/dist/readers/github-copilot.d.ts +34 -0
- package/dist/readers/github-copilot.js +242 -0
- package/dist/readers/hermes.d.ts +37 -0
- package/dist/readers/hermes.js +426 -0
- package/dist/readers/openclaw.d.ts +78 -0
- package/dist/readers/openclaw.js +386 -0
- package/dist/registry.d.ts +39 -0
- package/dist/registry.js +98 -0
- package/dist/sqlite-base.d.ts +65 -0
- package/dist/sqlite-base.js +163 -0
- package/package.json +6 -6
|
@@ -0,0 +1,386 @@
|
|
|
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 { execFileSync } from "node:child_process";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
import { Visibility, filterForFederation, makeAgentInfo, makeEntity, makeManifest, makeSession, makeVisibilityPolicy, } from "@getbourdon/l5";
|
|
33
|
+
import { redactText } from "@getbourdon/redaction";
|
|
34
|
+
import { ParticipantDiscoveryError, SPEC_VERSION, } from "../base.js";
|
|
35
|
+
// -- Constants -----------------------------------------------------------------
|
|
36
|
+
const AGENT_ID = "openclaw";
|
|
37
|
+
const AGENT_TYPE = "other";
|
|
38
|
+
const DISPLAY_NAME = "OpenClaw (quarantined)";
|
|
39
|
+
const ROLE_NARRATIVE = "OpenClaw personal AI assistant — federated as a QUARANTINED member: " +
|
|
40
|
+
"its reads are limited to granted namespaces and its writes are staged " +
|
|
41
|
+
"for operator review.";
|
|
42
|
+
/** First OpenClaw release that patches CVE-2026-25253 (one-click RCE). */
|
|
43
|
+
export const MIN_PATCHED_VERSION = "2026.1.29";
|
|
44
|
+
const DEFAULT_OPENCLAW_URL = "http://127.0.0.1:8080";
|
|
45
|
+
const SESSION_LIMIT = 50;
|
|
46
|
+
const ENTITY_LIMIT = 100;
|
|
47
|
+
// -- Helpers -------------------------------------------------------------------
|
|
48
|
+
function isRecord(v) {
|
|
49
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `_safe_native_memory_text(v)` in Python == `redact_text(v, limit=180)`. The
|
|
53
|
+
* TS redaction SSOT exposes `redactText(value, limit = 180)` (positional), so
|
|
54
|
+
* we pass the cap explicitly to make the surface's 180-char limit load-bearing.
|
|
55
|
+
*/
|
|
56
|
+
function safeNativeMemoryText(value) {
|
|
57
|
+
return redactText(value, 180);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `"2026.1.29"` -> `[2026, 1, 29]`. Tolerates suffixes like `"2026.1.29-beta"`;
|
|
61
|
+
* returns null when nothing numeric parses. Faithful port of `_parse_version`.
|
|
62
|
+
*/
|
|
63
|
+
function parseVersion(value) {
|
|
64
|
+
const parts = [];
|
|
65
|
+
for (const chunk of String(value ?? "").trim().split(".")) {
|
|
66
|
+
const match = /^(\d+)/.exec(chunk);
|
|
67
|
+
if (!match)
|
|
68
|
+
break;
|
|
69
|
+
parts.push(Number(match[1]));
|
|
70
|
+
}
|
|
71
|
+
return parts.length > 0 ? parts : null;
|
|
72
|
+
}
|
|
73
|
+
/** Tuple comparison mirroring Python's `tuple < tuple` (lexicographic, shorter
|
|
74
|
+
* is smaller when it is a prefix). */
|
|
75
|
+
function versionLessThan(a, b) {
|
|
76
|
+
const n = Math.max(a.length, b.length);
|
|
77
|
+
for (let i = 0; i < n; i += 1) {
|
|
78
|
+
const av = i < a.length ? a[i] : undefined;
|
|
79
|
+
const bv = i < b.length ? b[i] : undefined;
|
|
80
|
+
if (av === undefined)
|
|
81
|
+
return true; // a is a shorter prefix of b
|
|
82
|
+
if (bv === undefined)
|
|
83
|
+
return false; // b is a shorter prefix of a
|
|
84
|
+
if (av < bv)
|
|
85
|
+
return true;
|
|
86
|
+
if (av > bv)
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/** `_status_version`: first non-empty of version / openclaw_version / app_version. */
|
|
92
|
+
function statusVersion(status) {
|
|
93
|
+
for (const key of ["version", "openclaw_version", "app_version"]) {
|
|
94
|
+
const value = status[key];
|
|
95
|
+
if (typeof value === "string" && value.trim())
|
|
96
|
+
return value.trim();
|
|
97
|
+
}
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
/** `_status_auth_enabled`: auth_enabled / authEnabled / auth.enabled. */
|
|
101
|
+
function statusAuthEnabled(status) {
|
|
102
|
+
for (const key of ["auth_enabled", "authEnabled"]) {
|
|
103
|
+
if (key in status)
|
|
104
|
+
return Boolean(status[key]);
|
|
105
|
+
}
|
|
106
|
+
const auth = status["auth"];
|
|
107
|
+
if (isRecord(auth))
|
|
108
|
+
return Boolean(auth["enabled"]);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Hard handshake preconditions (spec D9). Returns the version on success.
|
|
113
|
+
* Raises {@link ParticipantDiscoveryError} with the exact reason AND the fix —
|
|
114
|
+
* these are refusals, not warnings.
|
|
115
|
+
*/
|
|
116
|
+
export function verifyInstance(status, url) {
|
|
117
|
+
const version = statusVersion(status);
|
|
118
|
+
const parsed = parseVersion(version);
|
|
119
|
+
const minimum = parseVersion(MIN_PATCHED_VERSION);
|
|
120
|
+
if (parsed === null) {
|
|
121
|
+
throw new ParticipantDiscoveryError(`OpenClaw at ${url} did not report a parseable version ` +
|
|
122
|
+
`(got ${JSON.stringify(version)}). Refusing handshake: cannot verify the ` +
|
|
123
|
+
`CVE-2026-25253 patch level. Fix: upgrade OpenClaw to ` +
|
|
124
|
+
`>= ${MIN_PATCHED_VERSION} and ensure /api/status reports it.`);
|
|
125
|
+
}
|
|
126
|
+
if (versionLessThan(parsed, minimum)) {
|
|
127
|
+
throw new ParticipantDiscoveryError(`OpenClaw at ${url} runs ${version}, which predates the ` +
|
|
128
|
+
`CVE-2026-25253 patch (one-click RCE, CVSS 8.8). Refusing ` +
|
|
129
|
+
`handshake. Fix: upgrade OpenClaw to >= ${MIN_PATCHED_VERSION}.`);
|
|
130
|
+
}
|
|
131
|
+
if (!statusAuthEnabled(status)) {
|
|
132
|
+
throw new ParticipantDiscoveryError(`OpenClaw at ${url} has authentication DISABLED (the exposed-` +
|
|
133
|
+
"instance default). Refusing handshake. Fix: enable auth in " +
|
|
134
|
+
"your OpenClaw config (set auth.enabled=true / OPENCLAW_AUTH=1), " +
|
|
135
|
+
"restart the instance, and set OPENCLAW_TOKEN for Bourdon.");
|
|
136
|
+
}
|
|
137
|
+
return version;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Mirror `_before`: return true when `dateText` is strictly before `since`.
|
|
141
|
+
* Reproduces `datetime.fromisoformat(dateText.replace("Z","+00:00"))` with the
|
|
142
|
+
* naive→UTC coercion, and returns false on a parse (ValueError) miss so the
|
|
143
|
+
* caller keeps the session.
|
|
144
|
+
*/
|
|
145
|
+
function before(dateText, since) {
|
|
146
|
+
const parsed = fromIsoformatUtc(dateText.replace(/Z/g, "+00:00"));
|
|
147
|
+
if (parsed === null)
|
|
148
|
+
return false; // ValueError -> keep
|
|
149
|
+
return parsed.getTime() < since.getTime();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A best-effort `datetime.fromisoformat` for the two shapes OpenClaw emits
|
|
153
|
+
* (date-only and datetime, with or without an offset). A naive value (no
|
|
154
|
+
* offset) is interpreted as UTC — matching the Python `parsed.replace(
|
|
155
|
+
* tzinfo=timezone.utc)`. Returns null on an unparseable value.
|
|
156
|
+
*/
|
|
157
|
+
function fromIsoformatUtc(text) {
|
|
158
|
+
const value = text.trim();
|
|
159
|
+
if (!value)
|
|
160
|
+
return null;
|
|
161
|
+
const hasOffset = /[+-]\d{2}:?\d{2}$/.test(value);
|
|
162
|
+
// date-only ISO is already interpreted as UTC midnight by Date.parse;
|
|
163
|
+
// a naive datetime ("...T..") must be pinned to UTC with a trailing Z.
|
|
164
|
+
const normalized = hasOffset || !value.includes("T") ? value : `${value}Z`;
|
|
165
|
+
const ms = Date.parse(normalized);
|
|
166
|
+
return Number.isNaN(ms) ? null : new Date(ms);
|
|
167
|
+
}
|
|
168
|
+
/** Python `str(now_utc, "%Y-%m-%dT%H:%M:%SZ")` — second precision, Z suffix. */
|
|
169
|
+
function nowIsoSeconds() {
|
|
170
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Synchronous GET via a tiny `curl` shell-out — the base contract is
|
|
174
|
+
* synchronous (matching the Python `urllib` reference), so `fetch` cannot be
|
|
175
|
+
* used. `-w` appends the HTTP status on a trailing sentinel line for parsing.
|
|
176
|
+
* Throws when curl itself fails (connection refused / timeout / DNS), which the
|
|
177
|
+
* caller maps to "instance unreachable".
|
|
178
|
+
*/
|
|
179
|
+
function curlGet(url, token, timeoutSec) {
|
|
180
|
+
const args = ["-sS", "-o", "-", "--max-time", String(timeoutSec)];
|
|
181
|
+
if (token) {
|
|
182
|
+
args.push("-H", `Authorization: Bearer ${token}`);
|
|
183
|
+
}
|
|
184
|
+
args.push("-w", "\n__BOURDON_STATUS__%{http_code}", url);
|
|
185
|
+
const raw = execFileSync("curl", args, {
|
|
186
|
+
encoding: "utf8",
|
|
187
|
+
timeout: (timeoutSec + 2) * 1000,
|
|
188
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
189
|
+
});
|
|
190
|
+
const statusMatch = /\n__BOURDON_STATUS__(\d+)$/.exec(raw);
|
|
191
|
+
const status = statusMatch ? Number(statusMatch[1]) : 0;
|
|
192
|
+
const body = statusMatch ? raw.slice(0, statusMatch.index) : raw;
|
|
193
|
+
return { status, body };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Minimal HTTP client for a local OpenClaw instance. Read-only: status,
|
|
197
|
+
* sessions, memories. Failures raise {@link ParticipantDiscoveryError} with the
|
|
198
|
+
* reason — the participant decides how to surface them.
|
|
199
|
+
*
|
|
200
|
+
* Error mapping (faithful to the Python `urllib` client): an HTTP >= 400
|
|
201
|
+
* response -> "returned HTTP {code}"; a transport failure / timeout / non-JSON
|
|
202
|
+
* body -> "instance unreachable at {url}".
|
|
203
|
+
*/
|
|
204
|
+
export class OpenClawApiClient {
|
|
205
|
+
url;
|
|
206
|
+
token;
|
|
207
|
+
timeout;
|
|
208
|
+
constructor(url, token, timeout = 5.0) {
|
|
209
|
+
this.url = url.replace(/\/+$/, "");
|
|
210
|
+
this.token = token;
|
|
211
|
+
this.timeout = timeout;
|
|
212
|
+
}
|
|
213
|
+
get(path) {
|
|
214
|
+
let resp;
|
|
215
|
+
try {
|
|
216
|
+
resp = curlGet(this.url + path, this.token, this.timeout);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
throw new ParticipantDiscoveryError(`OpenClaw instance unreachable at ${this.url} (${String(err)})`);
|
|
220
|
+
}
|
|
221
|
+
if (resp.status >= 400) {
|
|
222
|
+
throw new ParticipantDiscoveryError(`OpenClaw API ${path} returned HTTP ${resp.status}`);
|
|
223
|
+
}
|
|
224
|
+
if (resp.status === 0) {
|
|
225
|
+
throw new ParticipantDiscoveryError(`OpenClaw instance unreachable at ${this.url} (no HTTP response)`);
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
return JSON.parse(resp.body);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
throw new ParticipantDiscoveryError(`OpenClaw instance unreachable at ${this.url} (${String(err)})`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
status() {
|
|
235
|
+
const data = this.get("/api/status");
|
|
236
|
+
return isRecord(data) ? data : {};
|
|
237
|
+
}
|
|
238
|
+
sessions() {
|
|
239
|
+
return this.readCollection("/api/sessions", "sessions");
|
|
240
|
+
}
|
|
241
|
+
memories() {
|
|
242
|
+
return this.readCollection("/api/memories", "memories");
|
|
243
|
+
}
|
|
244
|
+
/** Shared `sessions()`/`memories()` body: try/except-returns-[], unwrap the
|
|
245
|
+
* `{ "<key>": [...] }` envelope, and keep only object rows. */
|
|
246
|
+
readCollection(path, key) {
|
|
247
|
+
let data;
|
|
248
|
+
try {
|
|
249
|
+
data = this.get(path);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
if (err instanceof ParticipantDiscoveryError)
|
|
253
|
+
return [];
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
if (isRecord(data)) {
|
|
257
|
+
data = data[key] || [];
|
|
258
|
+
}
|
|
259
|
+
if (!Array.isArray(data))
|
|
260
|
+
return [];
|
|
261
|
+
return data.filter(isRecord);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// -- Participant ---------------------------------------------------------------
|
|
265
|
+
/** Bourdon participant for OpenClaw (quarantined class). */
|
|
266
|
+
export class OpenClawParticipant {
|
|
267
|
+
agentId = AGENT_ID;
|
|
268
|
+
agentType = AGENT_TYPE;
|
|
269
|
+
displayName = DISPLAY_NAME;
|
|
270
|
+
/** Trust marker consumed by `bourdon agent add/set-tier` + export-all:
|
|
271
|
+
* registering as trusted needs --i-understand-the-risk; exports stage. */
|
|
272
|
+
quarantinedClass = true;
|
|
273
|
+
url;
|
|
274
|
+
client;
|
|
275
|
+
static defaultNativePath(home) {
|
|
276
|
+
// Network-shaped: shown in the wizard for orientation only. The actual
|
|
277
|
+
// store is the instance API (OPENCLAW_URL).
|
|
278
|
+
return join(home ?? homedir(), ".openclaw");
|
|
279
|
+
}
|
|
280
|
+
constructor(url, token, client) {
|
|
281
|
+
this.url = (url || process.env.OPENCLAW_URL || DEFAULT_OPENCLAW_URL).replace(/\/+$/, "");
|
|
282
|
+
this.client = client ?? new OpenClawApiClient(this.url, token ?? process.env.OPENCLAW_TOKEN);
|
|
283
|
+
}
|
|
284
|
+
get nativePath() {
|
|
285
|
+
return this.url;
|
|
286
|
+
}
|
|
287
|
+
// -- protocol ----------------------------------------------------------------
|
|
288
|
+
discover() {
|
|
289
|
+
const status = this.client.status();
|
|
290
|
+
const version = verifyInstance(status, this.url);
|
|
291
|
+
return {
|
|
292
|
+
path: this.url,
|
|
293
|
+
version,
|
|
294
|
+
metadata: { auth_enabled: true, transport: "http" },
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
exportSessions(since, limit = SESSION_LIMIT) {
|
|
298
|
+
this.discover(); // handshake gate applies to every read
|
|
299
|
+
const sessions = [];
|
|
300
|
+
for (const row of this.client.sessions()) {
|
|
301
|
+
const date = String(row["updated_at"] || row["started_at"] || row["date"] || "").trim();
|
|
302
|
+
if (!date)
|
|
303
|
+
continue;
|
|
304
|
+
if (since !== undefined && before(date, since))
|
|
305
|
+
continue;
|
|
306
|
+
const title = safeNativeMemoryText(String(row["title"] || row["summary"] || ""));
|
|
307
|
+
const projects = Array.isArray(row["projects"]) ? row["projects"].slice(0, 5) : [];
|
|
308
|
+
sessions.push(makeSession({
|
|
309
|
+
date,
|
|
310
|
+
cwd: undefined,
|
|
311
|
+
project_focus: projects.map((p) => safeNativeMemoryText(String(p))),
|
|
312
|
+
key_actions: title ? [title] : [],
|
|
313
|
+
}));
|
|
314
|
+
if (sessions.length >= limit)
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
return sessions;
|
|
318
|
+
}
|
|
319
|
+
exportL5(since) {
|
|
320
|
+
const store = this.discover();
|
|
321
|
+
const entities = [];
|
|
322
|
+
for (const row of this.client.memories().slice(0, ENTITY_LIMIT)) {
|
|
323
|
+
const name = safeNativeMemoryText(String(row["name"] || row["title"] || ""));
|
|
324
|
+
if (!name)
|
|
325
|
+
continue;
|
|
326
|
+
const tags = Array.isArray(row["tags"]) ? row["tags"].slice(0, 8) : [];
|
|
327
|
+
entities.push(makeEntity({
|
|
328
|
+
name,
|
|
329
|
+
type: String(row["type"] || "topic"),
|
|
330
|
+
summary: safeNativeMemoryText(String(row["summary"] || "")),
|
|
331
|
+
tags: tags.map((t) => safeNativeMemoryText(String(t))),
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
// Quarantined-class content defaults to TEAM, never PUBLIC: it only
|
|
335
|
+
// federates beyond the local library after an explicit operator promotion
|
|
336
|
+
// AND a team-level read.
|
|
337
|
+
const policy = makeVisibilityPolicy({ default: Visibility.TEAM });
|
|
338
|
+
return makeManifest({
|
|
339
|
+
spec_version: SPEC_VERSION,
|
|
340
|
+
agent: makeAgentInfo({
|
|
341
|
+
id: AGENT_ID,
|
|
342
|
+
type: AGENT_TYPE,
|
|
343
|
+
instance: this.url,
|
|
344
|
+
role_narrative: ROLE_NARRATIVE,
|
|
345
|
+
}),
|
|
346
|
+
last_updated: nowIsoSeconds(),
|
|
347
|
+
capabilities: [`openclaw ${store.version}`],
|
|
348
|
+
recent_sessions: this.exportSessions(since),
|
|
349
|
+
known_entities: filterForFederation(entities, policy),
|
|
350
|
+
visibility_policy: policy,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
healthCheck() {
|
|
354
|
+
let store;
|
|
355
|
+
try {
|
|
356
|
+
store = this.discover();
|
|
357
|
+
}
|
|
358
|
+
catch (exc) {
|
|
359
|
+
if (exc instanceof ParticipantDiscoveryError) {
|
|
360
|
+
const reason = exc.message;
|
|
361
|
+
let fix = "see the refusal reason above";
|
|
362
|
+
if (reason.includes("upgrade OpenClaw")) {
|
|
363
|
+
fix = `upgrade the OpenClaw instance to >= ${MIN_PATCHED_VERSION}`;
|
|
364
|
+
}
|
|
365
|
+
else if (reason.includes("authentication DISABLED")) {
|
|
366
|
+
fix = "enable auth on the OpenClaw instance, then set OPENCLAW_TOKEN";
|
|
367
|
+
}
|
|
368
|
+
else if (reason.includes("unreachable")) {
|
|
369
|
+
fix =
|
|
370
|
+
"start OpenClaw locally or set OPENCLAW_URL to the instance address";
|
|
371
|
+
}
|
|
372
|
+
return { status: "blocked", reason, proposedFix: fix, details: {} };
|
|
373
|
+
}
|
|
374
|
+
// health_check must never raise.
|
|
375
|
+
return {
|
|
376
|
+
status: "degraded",
|
|
377
|
+
reason: exc instanceof Error ? exc.message : String(exc),
|
|
378
|
+
details: {},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
status: "ok",
|
|
383
|
+
details: { url: this.url, version: store.version, quarantined: true },
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Participant discovery — a STATIC TS registry replacing Python's `pkgutil`
|
|
3
|
+
* package scan + entry-point duck-type (`participants/__init__.py`).
|
|
4
|
+
*
|
|
5
|
+
* Python scans the `participants/` package at runtime and duck-types each class
|
|
6
|
+
* by `_PARTICIPANT_MARKER_ATTRS`. A filesystem package scan does not survive
|
|
7
|
+
* bundling (`npx bourdon` is a single file), so the JS side declares an explicit
|
|
8
|
+
* {@link FIRST_PARTY} array. The three preserved invariants:
|
|
9
|
+
* - output sorted by `agentId`
|
|
10
|
+
* - first-wins dedupe on `agentId`
|
|
11
|
+
* - log-and-skip a participant that throws on construction (one broken reader
|
|
12
|
+
* must NOT abort discovery / crash the CLI)
|
|
13
|
+
*
|
|
14
|
+
* The marker-attr duck-typing collapses into the {@link BourdonParticipant}
|
|
15
|
+
* interface conformance (checked structurally via {@link isParticipant}).
|
|
16
|
+
*/
|
|
17
|
+
import { type BourdonParticipant } from "./base.js";
|
|
18
|
+
/** A zero-arg-constructible participant class. */
|
|
19
|
+
export type ParticipantCtor = new () => BourdonParticipant;
|
|
20
|
+
/**
|
|
21
|
+
* The first-party reader registry. Order here is irrelevant — discovery sorts by
|
|
22
|
+
* `agentId`. Covers the base agents, their background-run "automations" variants,
|
|
23
|
+
* the Claude-desktop surfaces, and the quarantined-class openclaw network reader.
|
|
24
|
+
* codex — the ~2.6k-line turn-compiler reader — is the only one still deferred.
|
|
25
|
+
*/
|
|
26
|
+
export declare const FIRST_PARTY: ParticipantCtor[];
|
|
27
|
+
/**
|
|
28
|
+
* Instantiate each registered participant, duck-type the contract, sort by
|
|
29
|
+
* `agentId`, and first-wins dedupe. A class that throws on construction is
|
|
30
|
+
* logged and skipped — never propagated.
|
|
31
|
+
*/
|
|
32
|
+
export declare function discoverParticipants(ctors?: ParticipantCtor[]): BourdonParticipant[];
|
|
33
|
+
/**
|
|
34
|
+
* Optional Layer 2: third-party plugins declared via a `package.json`
|
|
35
|
+
* `"bourdon": { participants: { ... } }` field. Gated behind `BOURDON_PLUGINS=1`
|
|
36
|
+
* and a no-op today (no external participant exists yet) — the resolver ships so
|
|
37
|
+
* the seam is wired, defaulted off so `npx bourdon` stays predictable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolvePluginParticipants(): ParticipantCtor[];
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Participant discovery — a STATIC TS registry replacing Python's `pkgutil`
|
|
3
|
+
* package scan + entry-point duck-type (`participants/__init__.py`).
|
|
4
|
+
*
|
|
5
|
+
* Python scans the `participants/` package at runtime and duck-types each class
|
|
6
|
+
* by `_PARTICIPANT_MARKER_ATTRS`. A filesystem package scan does not survive
|
|
7
|
+
* bundling (`npx bourdon` is a single file), so the JS side declares an explicit
|
|
8
|
+
* {@link FIRST_PARTY} array. The three preserved invariants:
|
|
9
|
+
* - output sorted by `agentId`
|
|
10
|
+
* - first-wins dedupe on `agentId`
|
|
11
|
+
* - log-and-skip a participant that throws on construction (one broken reader
|
|
12
|
+
* must NOT abort discovery / crash the CLI)
|
|
13
|
+
*
|
|
14
|
+
* The marker-attr duck-typing collapses into the {@link BourdonParticipant}
|
|
15
|
+
* interface conformance (checked structurally via {@link isParticipant}).
|
|
16
|
+
*/
|
|
17
|
+
import { isParticipant } from "./base.js";
|
|
18
|
+
import { CascadeParticipant } from "./readers/cascade.js";
|
|
19
|
+
import { ClaudeCodeParticipant } from "./readers/claude-code.js";
|
|
20
|
+
import { ClaudeCodeAutomationsParticipant } from "./readers/claude-code-automations.js";
|
|
21
|
+
import { ClaudeDesktopCodeParticipant } from "./readers/claude-desktop-code.js";
|
|
22
|
+
import { ClaudeDesktopCoworkParticipant } from "./readers/claude-desktop-cowork.js";
|
|
23
|
+
import { CodexAutomationsParticipant } from "./readers/codex-automations.js";
|
|
24
|
+
import { CopilotParticipant } from "./readers/copilot.js";
|
|
25
|
+
import { CopilotAutomationsParticipant } from "./readers/copilot-automations.js";
|
|
26
|
+
import { CopilotCliParticipant } from "./readers/copilot-cli.js";
|
|
27
|
+
import { CopilotVscodeParticipant } from "./readers/copilot-vscode.js";
|
|
28
|
+
import { CursorParticipant } from "./readers/cursor.js";
|
|
29
|
+
import { CursorAutomationsParticipant } from "./readers/cursor-automations.js";
|
|
30
|
+
import { GitHubCopilotParticipant } from "./readers/github-copilot.js";
|
|
31
|
+
import { HermesParticipant } from "./readers/hermes.js";
|
|
32
|
+
import { OpenClawParticipant } from "./readers/openclaw.js";
|
|
33
|
+
/**
|
|
34
|
+
* The first-party reader registry. Order here is irrelevant — discovery sorts by
|
|
35
|
+
* `agentId`. Covers the base agents, their background-run "automations" variants,
|
|
36
|
+
* the Claude-desktop surfaces, and the quarantined-class openclaw network reader.
|
|
37
|
+
* codex — the ~2.6k-line turn-compiler reader — is the only one still deferred.
|
|
38
|
+
*/
|
|
39
|
+
export const FIRST_PARTY = [
|
|
40
|
+
CascadeParticipant,
|
|
41
|
+
ClaudeCodeParticipant,
|
|
42
|
+
ClaudeCodeAutomationsParticipant,
|
|
43
|
+
ClaudeDesktopCodeParticipant,
|
|
44
|
+
ClaudeDesktopCoworkParticipant,
|
|
45
|
+
CodexAutomationsParticipant,
|
|
46
|
+
CopilotParticipant,
|
|
47
|
+
CopilotAutomationsParticipant,
|
|
48
|
+
CopilotCliParticipant,
|
|
49
|
+
CopilotVscodeParticipant,
|
|
50
|
+
CursorParticipant,
|
|
51
|
+
CursorAutomationsParticipant,
|
|
52
|
+
GitHubCopilotParticipant,
|
|
53
|
+
HermesParticipant,
|
|
54
|
+
OpenClawParticipant,
|
|
55
|
+
];
|
|
56
|
+
/** Minimal logger so a skipped participant is visible without a dep. */
|
|
57
|
+
const logger = {
|
|
58
|
+
warn(msg) {
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.warn(`[bourdon.participants] ${msg}`);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Instantiate each registered participant, duck-type the contract, sort by
|
|
65
|
+
* `agentId`, and first-wins dedupe. A class that throws on construction is
|
|
66
|
+
* logged and skipped — never propagated.
|
|
67
|
+
*/
|
|
68
|
+
export function discoverParticipants(ctors = FIRST_PARTY) {
|
|
69
|
+
const found = new Map();
|
|
70
|
+
for (const Ctor of [...ctors, ...resolvePluginParticipants()]) {
|
|
71
|
+
let instance;
|
|
72
|
+
try {
|
|
73
|
+
instance = new Ctor();
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
logger.warn(`Skipping participant ${Ctor.name || "<anonymous>"}: construction failed: ${String(err)}`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!isParticipant(instance))
|
|
80
|
+
continue;
|
|
81
|
+
if (!found.has(instance.agentId))
|
|
82
|
+
found.set(instance.agentId, instance);
|
|
83
|
+
}
|
|
84
|
+
return [...found.values()].sort((a, b) => a.agentId < b.agentId ? -1 : a.agentId > b.agentId ? 1 : 0);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Optional Layer 2: third-party plugins declared via a `package.json`
|
|
88
|
+
* `"bourdon": { participants: { ... } }` field. Gated behind `BOURDON_PLUGINS=1`
|
|
89
|
+
* and a no-op today (no external participant exists yet) — the resolver ships so
|
|
90
|
+
* the seam is wired, defaulted off so `npx bourdon` stays predictable.
|
|
91
|
+
*/
|
|
92
|
+
export function resolvePluginParticipants() {
|
|
93
|
+
if (process.env.BOURDON_PLUGINS !== "1")
|
|
94
|
+
return [];
|
|
95
|
+
// No external participant package exists yet; the resolver is intentionally a
|
|
96
|
+
// no-op stub. When one ships, resolve its declared ctors here.
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared read-only SQLite + normalization helpers — port of
|
|
3
|
+
* `participants/_sqlite_base.py`.
|
|
4
|
+
*
|
|
5
|
+
* A SQLite-backed participant (hermes today; cursor/copilot_cli in the follow-on
|
|
6
|
+
* slice) reads a foreign agent's native DB read-only so a live writer's lock can
|
|
7
|
+
* never block the export, probes the schema defensively (native schemas drift
|
|
8
|
+
* across versions), and turns epoch timestamps + paths into the L5 shapes.
|
|
9
|
+
*
|
|
10
|
+
* `better-sqlite3` is a NATIVE addon. It is loaded lazily through `createRequire`
|
|
11
|
+
* inside a try/catch so this package — and the file/network readers — keep
|
|
12
|
+
* working even on a toolchain where the addon failed to build: in that case
|
|
13
|
+
* {@link sqliteAvailable} returns false and the sqlite readers degrade rather
|
|
14
|
+
* than crashing import. Nothing here applies visibility or builds manifests;
|
|
15
|
+
* that stays in the participant (per PARTICIPANT_CONTRACT.md).
|
|
16
|
+
*/
|
|
17
|
+
import type DatabaseT from "better-sqlite3";
|
|
18
|
+
/** A better-sqlite3 connection (re-exported type alias for reader signatures). */
|
|
19
|
+
export type SqliteDatabase = DatabaseT.Database;
|
|
20
|
+
/** True when the better-sqlite3 native addon loaded successfully. Tests gate
|
|
21
|
+
* the SQLite-reader assertions on this so a missing addon skips, not fails. */
|
|
22
|
+
export declare function sqliteAvailable(): boolean;
|
|
23
|
+
export interface OpenReadonlyOptions {
|
|
24
|
+
/** `immutable=1`: skip locks + ignore the WAL. Only for genuinely static
|
|
25
|
+
* snapshots; against a live writer it risks stale reads / SQLITE_CORRUPT. */
|
|
26
|
+
immutable?: boolean;
|
|
27
|
+
/** SQLite busy-timeout in milliseconds (Python's `timeout=2.0` seconds). */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Open a SQLite DB read-only so a live writer is never disturbed. The default
|
|
32
|
+
* (`immutable` omitted) is the safe `mode=ro`: it respects SQLite locking and
|
|
33
|
+
* reads the `-wal` sidecar of a live, actively-writing agent. Raises on a
|
|
34
|
+
* genuine open failure; callers wanting the never-raises contract use
|
|
35
|
+
* {@link tryOpenReadonly}.
|
|
36
|
+
*/
|
|
37
|
+
export declare function openReadonly(dbPath: string, opts?: OpenReadonlyOptions): SqliteDatabase;
|
|
38
|
+
/**
|
|
39
|
+
* Like {@link openReadonly} but returns null instead of raising — for the
|
|
40
|
+
* export/health path where a missing or corrupt DB must degrade to "no rows".
|
|
41
|
+
*/
|
|
42
|
+
export declare function tryOpenReadonly(dbPath: string, opts?: OpenReadonlyOptions): SqliteDatabase | null;
|
|
43
|
+
/** True if `name` is a table in the connected DB (swallows errors → false). */
|
|
44
|
+
export declare function tableExists(db: SqliteDatabase, name: string): boolean;
|
|
45
|
+
/** Column names for `table` (empty set if absent / on error). PRAGMA cannot be
|
|
46
|
+
* parameterized, matching the Python f-string. */
|
|
47
|
+
export declare function tableColumns(db: SqliteDatabase, table: string): Set<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Convert epoch seconds (number/string) to an ISO `YYYY-MM-DD` UTC date, or
|
|
50
|
+
* null for missing/unparseable values. Mirrors `epoch_to_iso_date`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function epochToIsoDate(value: unknown): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Convert epoch seconds to a full ISO 8601 UTC datetime, or null. The offset is
|
|
55
|
+
* rendered `+00:00` (Python `datetime(...).isoformat()` style) rather than `Z`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function epochToIsoDatetime(value: unknown): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* Derive a lowercase project key from a working-directory path: the basename
|
|
60
|
+
* lowercased, or null when the cwd is empty or its basename is "generic"
|
|
61
|
+
* (carries no project identity). Mirrors `project_key_from_cwd`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function projectKeyFromCwd(cwd: string | null | undefined, genericNames?: ReadonlySet<string>): string | null;
|
|
64
|
+
/** Turn a slug-ish key (`my-cool_project`) into a Title Case label. */
|
|
65
|
+
export declare function friendlyLabel(key: string): string;
|