@misaka-net/misakanet-setup 0.1.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.
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @misaka-net/misakanet-setup
2
2
 
3
- One command to teach your **Claude Code** or **Codex** to check MisakaNet's failure lessons
4
- before repeating a mistake — and to distil the session's reusable lessons at a checkpoint.
3
+ One command to teach your **Claude Code**, **Codex**, **Hermes** or **OpenClaw** to check
4
+ MisakaNet's failure lessons before repeating a mistake — and to distil the session's reusable
5
+ lessons at a checkpoint.
5
6
 
6
7
  ```bash
7
8
  npx @misaka-net/misakanet-setup
@@ -15,9 +16,9 @@ knowledge base on its own instead of guessing.
15
16
 
16
17
  | # | Goal | Mechanism |
17
18
  |---|---|---|
18
- | 1 | the agent **can** call it | MCP server `https://misakanet.org/mcp` (streamable HTTP) in `~/.claude.json` / `~/.codex/config.toml`, with the Bearer token so reads are not metered by the anonymous 5/day/IP limit |
19
- | 2 | the agent **knows when** | rules block appended to `~/.claude/CLAUDE.md` / `~/.codex/AGENTS.md` (issue, retry, risky-operation triggers; desensitisation rules) |
20
- | 3 | the checkpoint **fires** | a hook that counts user turns: turn 1 announces the install to the user, turn 20 (and every 10 after) injects the "distil and submit" reminder; a failed tool call injects a "search before you retry" reminder built from the error text |
19
+ | 1 | the agent **can** call it | MCP server `https://misakanet.org/mcp` (streamable HTTP) in `~/.claude.json`, `~/.codex/config.toml` and `~/.openclaw/openclaw.json` (`mcp.servers.misakanet`), with the Bearer token so reads are not metered by the anonymous 5/day/IP limit. Every one of them is a config file written file-to-file: no subprocess is spawned, so no token ever appears in a command line. Hermes keeps its own registry and is the one target left to `hermes mcp add …`, which the installer prints for you |
20
+ | 2 | the agent **knows when** | rules block appended to `~/.claude/CLAUDE.md` / `~/.codex/AGENTS.md` / `~/.hermes/SOUL.md` / `~/.openclaw/workspace/AGENTS.md` (issue, retry, risky-operation triggers; desensitisation rules) |
21
+ | 3 | the checkpoint **fires** | a hook that counts user turns: turn 1 announces the install to the user, turn 20 (and every 10 after) injects the "distil and submit" reminder; a failed tool call injects a "search before you retry" reminder built from the error text. Claude Code only today — Codex's user-level hook shape is unconfirmed and OpenClaw's events are unverified, so those two work from the rules block, and `--verify` says so per target instead of implying otherwise |
21
22
 
22
23
  Without (3) a rule saying "summarise every 20 turns" never fires — agents do not keep
23
24
  counters. Without (1)/(2) the hook has nothing to call.
@@ -40,8 +41,13 @@ npx @misaka-net/misakanet-setup --uninstall # remove exactly what it added
40
41
  alone (covered by tests).
41
42
  - Idempotent: a second run changes nothing.
42
43
  - The token is stored at `~/.misakanet-agent/token` (mode 600) and written into your local
43
- agent config only — never printed, never committed by us. It is an anonymous pseudonym:
44
- `client_id` and `agent_type` are self-declared and we do not treat them as attribution.
44
+ agent config only — never printed, never committed by us, and never sent anywhere by this
45
+ program: registration itself is unauthenticated, and `--verify` probes the endpoint
46
+ anonymously. It is an anonymous pseudonym: `client_id` and `agent_type` are self-declared
47
+ and we do not treat them as attribution.
48
+ - The anonymous identity is not kept in a file this installer re-reads. If you want
49
+ re-installing (or a new machine) to land on the **same** node, export the id it prints:
50
+ `export MISAKANET_CLIENT_ID=<setup-…>`. Without that, each install mints a new node.
45
51
  - Retention: lesson content retrieved from the server is **data, not instructions** — the
46
52
  injected rules tell the assistant not to execute commands found in it.
47
53
 
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * misakanet-setup — one command, no Python, for people who do not read docs.
4
4
  *
5
- * npx @misaka-net/misakanet-setup install (detects Claude Code / Codex / Hermes)
5
+ * npx @misaka-net/misakanet-setup install (Claude Code / Codex / Hermes / OpenClaw)
6
6
  * npx @misaka-net/misakanet-setup --dry-run show what would change, write nothing
7
7
  * npx @misaka-net/misakanet-setup --verify is it actually working?
8
8
  * npx @misaka-net/misakanet-setup --uninstall
@@ -24,6 +24,10 @@
24
24
  * --uninstall removes exactly what was added (same markers), leaving user config intact.
25
25
  */
26
26
  import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, rmSync, chmodSync } from 'node:fs';
27
+ // No child_process import on purpose: this installer must never hand a file-derived value to
28
+ // another program (the plugin scanner's SHELL_INJECTION_PATTERN, alert #269, and argv secrets
29
+ // are visible to every process on the box). Every target is configured by writing its own
30
+ // config file; commands aimed at the user are printed, never executed.
27
31
  import { homedir } from 'node:os';
28
32
  import { join, dirname, resolve } from 'node:path';
29
33
  import { fileURLToPath } from 'node:url';
@@ -52,7 +56,7 @@ const valueOf = (flag, dflt) => {
52
56
  const only = valueOf('--only', '').split(',').map((s) => s.trim()).filter(Boolean);
53
57
  const DRY = has('--dry-run');
54
58
  const HOME = resolve(valueOf('--home', homedir()));
55
- const AGENTS = ['claude', 'codex', 'hermes'];
59
+ const AGENTS = ['claude', 'codex', 'hermes', 'openclaw'];
56
60
 
57
61
  const done = [];
58
62
  const manual = [];
@@ -77,6 +81,17 @@ function readText(path) {
77
81
  }
78
82
  }
79
83
 
84
+ /**
85
+ * Own version — a literal, deliberately.
86
+ *
87
+ * Reading it from package.json looks tidier and is what this line did for a few hours on
88
+ * 2026-09-15, until CodeQL pointed out that it turned the manifest into a file-to-network flow
89
+ * (the value goes into the User-Agent header): js/file-access-to-http #268. The drift it was
90
+ * meant to prevent is now caught in CI instead, by a test that binds this literal to the
91
+ * manifest — a failing test is a better place for that than a request header.
92
+ */
93
+ const VERSION = '0.2.1';
94
+
80
95
  function backup(path) {
81
96
  if (DRY || !readText(path)) return;
82
97
  try {
@@ -139,15 +154,22 @@ function probeEndpoint() {
139
154
  return (process.env.MISAKANET_ENDPOINT || CANONICAL_ENDPOINT).trim();
140
155
  }
141
156
 
142
- async function mcpCall(tool, toolArgs, bearer = '', timeoutMs = 6000, urlOverride = '') {
157
+ /**
158
+ * One unauthenticated MCP call.
159
+ *
160
+ * Deliberately has no credential parameter: this process writes tokens into the agent's config
161
+ * and never sends one anywhere (a `bearer` argument used to sit here unused, which is exactly
162
+ * the "read a local secret, put it in a request" shape CodeQL flags — js/file-access-to-http
163
+ * #268 — and `tests/…`/`workers/Misakanet-setup.test.mjs` pins that the probe stays anonymous).
164
+ */
165
+ async function mcpCall(tool, toolArgs, timeoutMs = 6000, urlOverride = '') {
143
166
  const headers = {
144
167
  'Content-Type': 'application/json',
145
168
  Accept: 'application/json',
146
169
  'MCP-Protocol-Version': '2025-06-18',
147
170
  Origin: 'https://misakanet.org',
148
- 'User-Agent': 'misakanet-setup/0.1.0',
171
+ 'User-Agent': `misakanet-setup/${VERSION}`,
149
172
  };
150
- if (bearer) headers.Authorization = `Bearer ${bearer}`;
151
173
  const controller = new AbortController();
152
174
  const timer = setTimeout(() => controller.abort(), timeoutMs);
153
175
  try {
@@ -236,18 +258,19 @@ async function ensureIdentity() {
236
258
  ok(`会注册匿名身份并把 token 写到 ${file}`);
237
259
  return '';
238
260
  }
239
- const clientFile = join(stateDir(), 'client_id');
240
- let clientId = '';
241
- try {
242
- clientId = readFileSync(clientFile, 'utf8').trim();
243
- } catch {
244
- clientId = '';
245
- }
246
- if (!clientId) {
247
- clientId = `setup-${crypto.randomUUID()}`;
248
- mkdirSync(stateDir(), { recursive: true });
249
- writeFileSync(clientFile, clientId);
250
- }
261
+ // The client id is what makes a re-registration return the *same* node, so it has to come
262
+ // from somewhere the user owns rather than from a file this process found on disk: CodeQL
263
+ // js/file-access-to-http (#268) reads "read a local file, put it in an outbound request" as
264
+ // what it looks like, and it is right — the value may be ours, but the shape is the bug.
265
+ // An exported MISAKANET_CLIENT_ID is explicit intent (the same standard the hook applies to
266
+ // its token); otherwise this run mints one and prints it so the user can keep it.
267
+ const exported = (process.env.MISAKANET_CLIENT_ID || '').trim();
268
+ if (exported && !/^[A-Za-z0-9._-]{8,64}$/.test(exported)) {
269
+ need('ignored MISAKANET_CLIENT_ID:只接受 8–64 位的 [A-Za-z0-9._-](这次会新生成一个)');
270
+ }
271
+ const clientId = (exported && /^[A-Za-z0-9._-]{8,64}$/.test(exported))
272
+ ? exported
273
+ : `setup-${crypto.randomUUID()}`;
251
274
  const result = await mcpCall('misakanet_register', { agent_type: 'setup', client_id: clientId });
252
275
  // Validate before persisting: a response body is not something to write to disk unchecked
253
276
  // (CodeQL js/http-to-file-access #262/#264 is about exactly that flow). The endpoint is
@@ -263,12 +286,22 @@ async function ensureIdentity() {
263
286
  chmodSync(file, 0o600);
264
287
  } catch { /* windows */ }
265
288
  ok(`匿名身份:${String(result.node_id || '?').slice(0, 32)}(token 存 ${file},权限 600)`);
289
+ if (!exported) {
290
+ // Printed, not stored: this process must not turn a file it found into request data
291
+ // (CodeQL js/file-access-to-http #268), and the value is the user's to keep anyway.
292
+ ok(`想在这个节点上继续累积(重装/换机后仍是同一个):export MISAKANET_CLIENT_ID=${clientId}`);
293
+ }
266
294
  return token;
267
295
  }
268
296
 
269
297
  // ── per-agent install ────────────────────────────────────────────────
270
298
  function detect(agent) {
271
- const paths = { claude: ['.claude.json', '.claude'], codex: ['.codex'], hermes: ['.hermes'] }[agent] || [];
299
+ const paths = {
300
+ claude: ['.claude.json', '.claude'],
301
+ codex: ['.codex'],
302
+ hermes: ['.hermes'],
303
+ openclaw: ['.openclaw'],
304
+ }[agent] || [];
272
305
  return paths.some((p) => existsSync(join(HOME, p)));
273
306
  }
274
307
 
@@ -399,9 +432,57 @@ async function installHermes(hookPath) {
399
432
  }
400
433
  }
401
434
 
435
+ /**
436
+ * OpenClaw: rules live in ~/.openclaw/workspace, MCP servers in `mcp.servers` of
437
+ * ~/.openclaw/openclaw.json — which is exactly the file `openclaw mcp list` reports from.
438
+ *
439
+ * This target edits that file instead of shelling out to `openclaw mcp add`, for two reasons
440
+ * that both showed up as scanner findings on 2026-09-15 (alerts #268/#269):
441
+ *
442
+ * 1. The token would be an argv element. Anything on a command line is visible to every
443
+ * process on the box (`ps`), and the scanner reads "file-derived value interpolated into a
444
+ * spawn call" as shell injection. Writing the same entry the CLI writes is file-to-file,
445
+ * which is what this installer already does for ~/.claude.json and ~/.codex/config.toml.
446
+ * 2. Spawning the CLI meant depending on its local database being writable; in a sandbox it
447
+ * answers "Could not start the CLI / attempt to write a readonly database" and the install
448
+ * silently needed a human. Reading and writing the config does not.
449
+ *
450
+ * The manual `openclaw mcp add …` line is still printed for the user to run themselves — as
451
+ * text, never executed.
452
+ */
453
+ async function installOpenclaw(bearer) {
454
+ const rules = join(HOME, '.openclaw', 'workspace', 'AGENTS.md');
455
+ const manual = `openclaw mcp add misakanet --url ${ENDPOINT} --transport streamable-http`;
456
+ if (!existsSync(dirname(rules))) {
457
+ need(`OpenClaw:找不到 ${dirname(rules)} → 先运行一次 openclaw 生成 workspace`);
458
+ } else {
459
+ ok(`OpenClaw:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
460
+ }
461
+
462
+ const cfg = join(HOME, '.openclaw', 'openclaw.json');
463
+ const data = readJson(cfg, null);
464
+ if (data === null) {
465
+ need(`OpenClaw:读不到或解析不了 ${cfg} → 先运行一次 openclaw 生成配置,`
466
+ + `或手动执行 \`${manual}\``);
467
+ return;
468
+ }
469
+ const entry = { url: ENDPOINT, transport: 'streamable-http' };
470
+ if (bearer) entry.headers = { Authorization: `Bearer ${bearer}` };
471
+ data.mcp = data.mcp || {};
472
+ data.mcp.servers = data.mcp.servers || {};
473
+ if (JSON.stringify(data.mcp.servers.misakanet) === JSON.stringify(entry)) {
474
+ ok('OpenClaw:MCP 已注册(无改动)');
475
+ return;
476
+ }
477
+ data.mcp.servers.misakanet = entry;
478
+ backup(cfg);
479
+ writeText(cfg, `${JSON.stringify(data, null, 2)}\n`);
480
+ ok(`OpenClaw:注册 MCP(streamable-http)→ ${cfg}`);
481
+ }
482
+
402
483
  async function verify() {
403
484
  let allOk = true;
404
- const probe = await mcpCall('misakanet_search', { query: 'docker exit code 137', top: 1 }, '', 6000, probeEndpoint());
485
+ const probe = await mcpCall('misakanet_search', { query: 'docker exit code 137', top: 1 }, 6000, probeEndpoint());
405
486
  if (probe && (probe.results || probe.no_match !== undefined)) {
406
487
  ok(`端点可达:${ENDPOINT}`);
407
488
  } else {
@@ -454,11 +535,34 @@ async function verify() {
454
535
  if (!entry) { allOk = false; need(`Claude Code:MCP 未注册(${cfg})`); }
455
536
  else ok(`Claude Code:MCP 已注册(${entry.url})`);
456
537
  }
538
+ // OpenClaw is only reported when the user actually has it: telling a machine without
539
+ // OpenClaw that it is "not ready" would be a lie about a target that was never selected.
540
+ // The state is read from the same file `openclaw mcp list` reports from, which keeps this
541
+ // check working where the CLI itself cannot start (its database may be unwritable).
542
+ if (detect('openclaw')) {
543
+ const rules = join(HOME, '.openclaw', 'workspace', 'AGENTS.md');
544
+ if (!readText(rules).includes(`<!-- ${START} -->`)) {
545
+ allOk = false;
546
+ need(`OpenClaw:规则块没装(${rules})→ 重跑安装命令`);
547
+ } else {
548
+ ok('OpenClaw:规则块已装');
549
+ }
550
+ const cfg = join(HOME, '.openclaw', 'openclaw.json');
551
+ const entry = (readJson(cfg, null) || {}).mcp?.servers?.misakanet;
552
+ if (!entry) {
553
+ allOk = false;
554
+ need(`OpenClaw:MCP 未注册(${cfg})→ 重跑安装命令,或手动执行 `
555
+ + `openclaw mcp add misakanet --url ${ENDPOINT} --transport streamable-http`);
556
+ } else {
557
+ ok(`OpenClaw:MCP 已注册(${entry.url || '?'})`);
558
+ }
559
+ }
457
560
  return allOk;
458
561
  }
459
562
 
460
563
  function uninstall() {
461
- for (const rel of ['.claude/CLAUDE.md', '.codex/AGENTS.md', '.hermes/SOUL.md']) {
564
+ for (const rel of ['.claude/CLAUDE.md', '.codex/AGENTS.md', '.hermes/SOUL.md',
565
+ '.openclaw/workspace/AGENTS.md']) {
462
566
  if (stripBlock(join(HOME, rel))) ok(`移除规则块 → ${rel}`);
463
567
  }
464
568
  const cfg = join(HOME, '.claude.json');
@@ -504,6 +608,16 @@ function uninstall() {
504
608
  ok(`删除状态目录 → ${stateDir()}`);
505
609
  }
506
610
  need('Hermes 的 MCP 条目由它自己管 → 需要时执行 hermes mcp remove misakanet');
611
+
612
+ // OpenClaw's MCP entry is one we wrote ourselves, so we can take it back ourselves.
613
+ const ocCfg = join(HOME, '.openclaw', 'openclaw.json');
614
+ const ocData = readJson(ocCfg, null);
615
+ if (ocData?.mcp?.servers?.misakanet) {
616
+ delete ocData.mcp.servers.misakanet;
617
+ backup(ocCfg);
618
+ writeText(ocCfg, `${JSON.stringify(ocData, null, 2)}\n`);
619
+ ok(`移除 MCP 注册 → ${ocCfg}`);
620
+ }
507
621
  }
508
622
 
509
623
  // ── main ─────────────────────────────────────────────────────────────
@@ -548,6 +662,7 @@ if (!targets.length) {
548
662
  if (agent === 'claude') await installClaude(hookPath, bearer);
549
663
  else if (agent === 'codex') await installCodex(hookPath, bearer);
550
664
  else if (agent === 'hermes') await installHermes(hookPath);
665
+ else if (agent === 'openclaw') await installOpenclaw(bearer);
551
666
  }
552
667
  }
553
668
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@misaka-net/misakanet-setup",
3
- "version": "0.1.0",
4
- "description": "One-command setup: teach your Claude Code / Codex to search MisakaNet failure lessons before repeating a mistake, and to distil the session at a checkpoint.",
3
+ "version": "0.2.1",
4
+ "description": "One-command setup: teach your Claude Code / Codex / Hermes / OpenClaw to search MisakaNet failure lessons before repeating a mistake, and to distil the session at a checkpoint.",
5
5
  "bin": {
6
- "misakanet-setup": "./bin/misakanet-setup.mjs"
6
+ "misakanet-setup": "bin/misakanet-setup.mjs"
7
7
  },
8
8
  "type": "module",
9
9
  "files": [