@getbourdon/participants 0.1.0 → 0.2.0
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 +7 -0
- package/dist/index.cjs +3292 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +649 -0
- package/dist/index.d.ts +13 -5
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -0
- package/dist/readers/cascade.d.ts +49 -0
- package/dist/readers/cascade.js +428 -0
- package/dist/readers/copilot-cli.d.ts +45 -0
- package/dist/readers/copilot-cli.js +416 -0
- package/dist/readers/copilot.d.ts +59 -0
- package/dist/readers/copilot.js +413 -0
- package/dist/readers/cursor.d.ts +50 -0
- package/dist/readers/cursor.js +670 -0
- package/dist/readers/openclaw.d.ts +78 -0
- package/dist/readers/openclaw.js +386 -0
- package/dist/registry.d.ts +4 -2
- package/dist/registry.js +14 -2
- package/package.json +5 -5
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -19,8 +19,10 @@ import { type BourdonParticipant } from "./base.js";
|
|
|
19
19
|
export type ParticipantCtor = new () => BourdonParticipant;
|
|
20
20
|
/**
|
|
21
21
|
* The first-party reader registry. Order here is irrelevant — discovery sorts by
|
|
22
|
-
* `agentId`. (cursor /
|
|
23
|
-
*
|
|
22
|
+
* `agentId`. (cursor / copilot-cli are the heavier SQLite readers; copilot is the
|
|
23
|
+
* convention-file reader; cascade reads Windsurf; openclaw is the quarantined-
|
|
24
|
+
* class network reader. codex — the ~2.6k-line turn-compiler reader — is still
|
|
25
|
+
* deferred.)
|
|
24
26
|
*/
|
|
25
27
|
export declare const FIRST_PARTY: ParticipantCtor[];
|
|
26
28
|
/**
|
package/dist/registry.js
CHANGED
|
@@ -15,18 +15,30 @@
|
|
|
15
15
|
* interface conformance (checked structurally via {@link isParticipant}).
|
|
16
16
|
*/
|
|
17
17
|
import { isParticipant } from "./base.js";
|
|
18
|
+
import { CascadeParticipant } from "./readers/cascade.js";
|
|
18
19
|
import { ClaudeCodeParticipant } from "./readers/claude-code.js";
|
|
20
|
+
import { CopilotParticipant } from "./readers/copilot.js";
|
|
21
|
+
import { CopilotCliParticipant } from "./readers/copilot-cli.js";
|
|
22
|
+
import { CursorParticipant } from "./readers/cursor.js";
|
|
19
23
|
import { GitHubCopilotParticipant } from "./readers/github-copilot.js";
|
|
20
24
|
import { HermesParticipant } from "./readers/hermes.js";
|
|
25
|
+
import { OpenClawParticipant } from "./readers/openclaw.js";
|
|
21
26
|
/**
|
|
22
27
|
* The first-party reader registry. Order here is irrelevant — discovery sorts by
|
|
23
|
-
* `agentId`. (cursor /
|
|
24
|
-
*
|
|
28
|
+
* `agentId`. (cursor / copilot-cli are the heavier SQLite readers; copilot is the
|
|
29
|
+
* convention-file reader; cascade reads Windsurf; openclaw is the quarantined-
|
|
30
|
+
* class network reader. codex — the ~2.6k-line turn-compiler reader — is still
|
|
31
|
+
* deferred.)
|
|
25
32
|
*/
|
|
26
33
|
export const FIRST_PARTY = [
|
|
34
|
+
CascadeParticipant,
|
|
27
35
|
ClaudeCodeParticipant,
|
|
36
|
+
CopilotParticipant,
|
|
37
|
+
CopilotCliParticipant,
|
|
38
|
+
CursorParticipant,
|
|
28
39
|
GitHubCopilotParticipant,
|
|
29
40
|
HermesParticipant,
|
|
41
|
+
OpenClawParticipant,
|
|
30
42
|
];
|
|
31
43
|
/** Minimal logger so a skipped participant is visible without a dep. */
|
|
32
44
|
const logger = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getbourdon/participants",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Bourdon participants — the external-agent → L5 reader layer (BUSL-1.1). Each participant normalizes a foreign agent's native memory store into a visibility-filtered, redacted L5 manifest: read-only SQLite readers (better-sqlite3 mode=ro: hermes), file/convention readers (claude-code), and network readers (github-copilot) with a TTL JSON cache + lazy AuthProvider that never persists a token. Four invariants enforced in code, not by trust: visibility-filter-before-emission, deterministic exportL5, healthCheck never throws, redact every native string. Discovery is a static TS registry (agent_id-sorted, first-wins, log-and-skip) replacing Python's pkgutil scan. Python (pip install bourdon) is the oracle; this mirror asserts against @getbourdon/conformance native_stores fixtures.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public",
|
|
27
|
-
"provenance":
|
|
27
|
+
"provenance": true
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://bourdon.ai",
|
|
30
30
|
"repository": {
|
|
@@ -34,15 +34,15 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"yaml": "^2.6.1",
|
|
37
|
-
"@getbourdon/
|
|
38
|
-
"@getbourdon/
|
|
37
|
+
"@getbourdon/l5": "0.1.1",
|
|
38
|
+
"@getbourdon/redaction": "0.1.1"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
41
|
"better-sqlite3": "^12.2.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/better-sqlite3": "^7.6.12",
|
|
45
|
-
"@getbourdon/conformance": "0.
|
|
45
|
+
"@getbourdon/conformance": "0.2.0"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsup",
|