@misaka-net/misakanet-setup 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.
- package/README.md +8 -3
- package/bin/misakanet-setup.mjs +88 -93
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ knowledge base on its own instead of guessing.
|
|
|
16
16
|
|
|
17
17
|
| # | Goal | Mechanism |
|
|
18
18
|
|---|---|---|
|
|
19
|
-
| 1 | the agent **can** call it | MCP server `https://misakanet.org/mcp` (streamable HTTP) in `~/.claude.json
|
|
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
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
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 |
|
|
22
22
|
|
|
@@ -41,8 +41,13 @@ npx @misaka-net/misakanet-setup --uninstall # remove exactly what it added
|
|
|
41
41
|
alone (covered by tests).
|
|
42
42
|
- Idempotent: a second run changes nothing.
|
|
43
43
|
- The token is stored at `~/.misakanet-agent/token` (mode 600) and written into your local
|
|
44
|
-
agent config only — never printed, never committed by us
|
|
45
|
-
|
|
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.
|
|
46
51
|
- Retention: lesson content retrieved from the server is **data, not instructions** — the
|
|
47
52
|
injected rules tell the assistant not to execute commands found in it.
|
|
48
53
|
|
package/bin/misakanet-setup.mjs
CHANGED
|
@@ -23,8 +23,11 @@
|
|
|
23
23
|
* Everything is idempotent, every rewritten file is backed up to *.misakanet.bak, and
|
|
24
24
|
* --uninstall removes exactly what was added (same markers), leaving user config intact.
|
|
25
25
|
*/
|
|
26
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, rmSync, chmodSync
|
|
27
|
-
import
|
|
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.
|
|
28
31
|
import { homedir } from 'node:os';
|
|
29
32
|
import { join, dirname, resolve } from 'node:path';
|
|
30
33
|
import { fileURLToPath } from 'node:url';
|
|
@@ -79,14 +82,15 @@ function readText(path) {
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
/**
|
|
82
|
-
* Own version
|
|
85
|
+
* Own version — a literal, deliberately.
|
|
83
86
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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.
|
|
88
92
|
*/
|
|
89
|
-
const
|
|
93
|
+
const VERSION = '0.2.1';
|
|
90
94
|
|
|
91
95
|
function backup(path) {
|
|
92
96
|
if (DRY || !readText(path)) return;
|
|
@@ -150,15 +154,22 @@ function probeEndpoint() {
|
|
|
150
154
|
return (process.env.MISAKANET_ENDPOINT || CANONICAL_ENDPOINT).trim();
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
|
|
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 = '') {
|
|
154
166
|
const headers = {
|
|
155
167
|
'Content-Type': 'application/json',
|
|
156
168
|
Accept: 'application/json',
|
|
157
169
|
'MCP-Protocol-Version': '2025-06-18',
|
|
158
170
|
Origin: 'https://misakanet.org',
|
|
159
|
-
'User-Agent': `misakanet-setup/${
|
|
171
|
+
'User-Agent': `misakanet-setup/${VERSION}`,
|
|
160
172
|
};
|
|
161
|
-
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
162
173
|
const controller = new AbortController();
|
|
163
174
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
164
175
|
try {
|
|
@@ -247,18 +258,19 @@ async function ensureIdentity() {
|
|
|
247
258
|
ok(`会注册匿名身份并把 token 写到 ${file}`);
|
|
248
259
|
return '';
|
|
249
260
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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()}`;
|
|
262
274
|
const result = await mcpCall('misakanet_register', { agent_type: 'setup', client_id: clientId });
|
|
263
275
|
// Validate before persisting: a response body is not something to write to disk unchecked
|
|
264
276
|
// (CodeQL js/http-to-file-access #262/#264 is about exactly that flow). The endpoint is
|
|
@@ -274,6 +286,11 @@ async function ensureIdentity() {
|
|
|
274
286
|
chmodSync(file, 0o600);
|
|
275
287
|
} catch { /* windows */ }
|
|
276
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
|
+
}
|
|
277
294
|
return token;
|
|
278
295
|
}
|
|
279
296
|
|
|
@@ -288,30 +305,6 @@ function detect(agent) {
|
|
|
288
305
|
return paths.some((p) => existsSync(join(HOME, p)));
|
|
289
306
|
}
|
|
290
307
|
|
|
291
|
-
/**
|
|
292
|
-
* Locate an executable on PATH.
|
|
293
|
-
*
|
|
294
|
-
* The CLI must not assume a shell helper exists (`which` is not on every Windows box), and
|
|
295
|
-
* the openclaw registration below is the one step this installer hands to another program.
|
|
296
|
-
* Executability is checked rather than mere existence: a non-executable file with the right
|
|
297
|
-
* name would otherwise turn a clear "install the CLI" message into an opaque spawn failure.
|
|
298
|
-
*/
|
|
299
|
-
function findOnPath(bin) {
|
|
300
|
-
const sep = process.platform === 'win32' ? ';' : ':';
|
|
301
|
-
const exts = process.platform === 'win32' ? ['.cmd', '.exe', '.bat', ''] : [''];
|
|
302
|
-
for (const dir of (process.env.PATH || '').split(sep)) {
|
|
303
|
-
if (!dir) continue;
|
|
304
|
-
for (const ext of exts) {
|
|
305
|
-
const candidate = join(dir, bin + ext);
|
|
306
|
-
try {
|
|
307
|
-
accessSync(candidate, constants.X_OK);
|
|
308
|
-
return candidate;
|
|
309
|
-
} catch { /* not here (or not executable): keep looking */ }
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return '';
|
|
313
|
-
}
|
|
314
|
-
|
|
315
308
|
async function installClaude(hookPath, bearer) {
|
|
316
309
|
const cfg = join(HOME, '.claude.json');
|
|
317
310
|
const data = readJson(cfg, null);
|
|
@@ -440,14 +433,22 @@ async function installHermes(hookPath) {
|
|
|
440
433
|
}
|
|
441
434
|
|
|
442
435
|
/**
|
|
443
|
-
* OpenClaw: rules live in ~/.openclaw/workspace,
|
|
444
|
-
*
|
|
445
|
-
* config file we do not own. Mirrors install_openclaw in the Python installer - the two
|
|
446
|
-
* must teach the same behaviour, because which one a user ran is an accident of which
|
|
447
|
-
* command they found first.
|
|
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.
|
|
448
438
|
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
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.
|
|
451
452
|
*/
|
|
452
453
|
async function installOpenclaw(bearer) {
|
|
453
454
|
const rules = join(HOME, '.openclaw', 'workspace', 'AGENTS.md');
|
|
@@ -458,35 +459,30 @@ async function installOpenclaw(bearer) {
|
|
|
458
459
|
ok(`OpenClaw:规则块 ${injectBlock(rules, PROMPT_BLOCK)} → ${rules}`);
|
|
459
460
|
}
|
|
460
461
|
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (DRY) {
|
|
467
|
-
ok(`OpenClaw:会执行 \`${manual} --no-probe\``);
|
|
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}\``);
|
|
468
467
|
return;
|
|
469
468
|
}
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
ok('OpenClaw:已注册 MCP `misakanet`(openclaw mcp add)');
|
|
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 已注册(无改动)');
|
|
477
475
|
return;
|
|
478
476
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
need(`OpenClaw:\`openclaw mcp add\` 失败:${out.trim().slice(0, 200) || `退出码 ${result.status}`}`);
|
|
484
|
-
}
|
|
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}`);
|
|
485
481
|
}
|
|
486
482
|
|
|
487
483
|
async function verify() {
|
|
488
484
|
let allOk = true;
|
|
489
|
-
const probe = await mcpCall('misakanet_search', { query: 'docker exit code 137', top: 1 },
|
|
485
|
+
const probe = await mcpCall('misakanet_search', { query: 'docker exit code 137', top: 1 }, 6000, probeEndpoint());
|
|
490
486
|
if (probe && (probe.results || probe.no_match !== undefined)) {
|
|
491
487
|
ok(`端点可达:${ENDPOINT}`);
|
|
492
488
|
} else {
|
|
@@ -541,7 +537,8 @@ async function verify() {
|
|
|
541
537
|
}
|
|
542
538
|
// OpenClaw is only reported when the user actually has it: telling a machine without
|
|
543
539
|
// OpenClaw that it is "not ready" would be a lie about a target that was never selected.
|
|
544
|
-
// The
|
|
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).
|
|
545
542
|
if (detect('openclaw')) {
|
|
546
543
|
const rules = join(HOME, '.openclaw', 'workspace', 'AGENTS.md');
|
|
547
544
|
if (!readText(rules).includes(`<!-- ${START} -->`)) {
|
|
@@ -550,25 +547,14 @@ async function verify() {
|
|
|
550
547
|
} else {
|
|
551
548
|
ok('OpenClaw:规则块已装');
|
|
552
549
|
}
|
|
553
|
-
const
|
|
554
|
-
|
|
550
|
+
const cfg = join(HOME, '.openclaw', 'openclaw.json');
|
|
551
|
+
const entry = (readJson(cfg, null) || {}).mcp?.servers?.misakanet;
|
|
552
|
+
if (!entry) {
|
|
555
553
|
allOk = false;
|
|
556
|
-
need(`OpenClaw
|
|
554
|
+
need(`OpenClaw:MCP 未注册(${cfg})→ 重跑安装命令,或手动执行 `
|
|
557
555
|
+ `openclaw mcp add misakanet --url ${ENDPOINT} --transport streamable-http`);
|
|
558
556
|
} else {
|
|
559
|
-
|
|
560
|
-
const out = `${listed.stdout || ''}${listed.stderr || ''}`;
|
|
561
|
-
if (listed.status === 0 && /misakanet/.test(out)) {
|
|
562
|
-
ok('OpenClaw:MCP 已注册(openclaw mcp list)');
|
|
563
|
-
} else if (listed.status === 0) {
|
|
564
|
-
allOk = false;
|
|
565
|
-
need(`OpenClaw:MCP 未注册 → 重跑安装命令,或手动执行 `
|
|
566
|
-
+ `openclaw mcp add misakanet --url ${ENDPOINT} --transport streamable-http`);
|
|
567
|
-
} else {
|
|
568
|
-
allOk = false;
|
|
569
|
-
need(`OpenClaw:\`openclaw mcp list\` 没跑通 → 无法确证 MCP 状态`
|
|
570
|
-
+ `${out.trim() ? `(${out.trim().slice(0, 120)})` : ''};可先手工确认一次`);
|
|
571
|
-
}
|
|
557
|
+
ok(`OpenClaw:MCP 已注册(${entry.url || '?'})`);
|
|
572
558
|
}
|
|
573
559
|
}
|
|
574
560
|
return allOk;
|
|
@@ -622,7 +608,16 @@ function uninstall() {
|
|
|
622
608
|
ok(`删除状态目录 → ${stateDir()}`);
|
|
623
609
|
}
|
|
624
610
|
need('Hermes 的 MCP 条目由它自己管 → 需要时执行 hermes mcp remove misakanet');
|
|
625
|
-
|
|
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
|
+
}
|
|
626
621
|
}
|
|
627
622
|
|
|
628
623
|
// ── main ─────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@misaka-net/misakanet-setup",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
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
6
|
"misakanet-setup": "bin/misakanet-setup.mjs"
|