@misaka-net/misakanet-setup 0.5.6 → 0.7.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.
@@ -226,6 +226,94 @@ function failureText(payload) {
226
226
  return '';
227
227
  }
228
228
 
229
+ /**
230
+ * Deterministic redaction — the query must not carry the machine's secrets out of it.
231
+ *
232
+ * Issue #3692: the rules block used to *ask* the agent to redact, and an instruction is
233
+ * only a request. When the model did not comply, the internal table name, the home
234
+ * directory path or the token left the machine inside a search query. This table is the
235
+ * mechanical version of that rule: it runs on the error text before the fragment is
236
+ * printed into the agent's context, and again before anything is sent over the network.
237
+ * Redacting only at the network call would not be enough — the un-redacted text would
238
+ * still be in the agent's context, and the agent's own `misakanet_search` call would
239
+ * carry it.
240
+ *
241
+ * The ids are the server's (`workers/lib/redact-patterns.json`), so both ends name the
242
+ * same classes and the client never invents a rule the server does not know. Real
243
+ * domains and personal names are deliberately NOT redacted: the corpus is indexed by error
244
+ * text, and over-redaction would cost search quality for a leak that is not in scope.
245
+ *
246
+ * One path segment: anything but a space, a separator or punctuation that would end the
247
+ * segment (so a trailing comma or bracket in prose stays outside the match).
248
+ */
249
+ const SEG = "[^\\s/\\\\'\"<>,;:!?()\\[\\]{}]";
250
+ // A path only counts as a *filesystem* path when it starts at a known root, or when its
251
+ // last segment carries a file extension. Without that, every API route in the corpus
252
+ // ("/api/v1/users", "/oauth/token") would be swallowed — and the corpus is indexed by
253
+ // error text, so a swallowed route destroys the lesson it belongs to. The roots are
254
+ // case-sensitive, except "Users" and "Volumes", which are written in capitals by
255
+ // convention but arrive in every casing.
256
+ const FS_ROOTS = "(?:home|root|var|opt|etc|usr|tmp|mnt|srv|data|private|workspace|app|code|project"
257
+ + "|[Uu][Ss][Ee][Rr][Ss]|[Vv][Oo][Ll][Uu][Mm][Ee][Ss])";
258
+ const FILE_EXT = "[A-Za-z][A-Za-z0-9_\\-]*";
259
+
260
+ const REDACT_RULES = [
261
+ // A Unix absolute path: either it starts at a known filesystem root, or its last
262
+ // segment has a file extension ("/opt/app/config.yaml"). The look-behind keeps it out
263
+ // of a URL ("https://host/a/b" — the slash follows a word character) and out of a
264
+ // relative path; "file://" is matched as a whole, because its inner path is a path.
265
+ { id: 'abs_path', pattern: `(?<![A-Za-z0-9_:/~.\\-])(?:file://)?/(?:${FS_ROOTS}(?:/${SEG}+)+|(?:${SEG}+/)*${SEG}*\\.${FILE_EXT})`, flags: 'g', replacement: '<PATH>' },
266
+ // A Windows path, with either separator. The look-behind keeps the "s://" of a URL out.
267
+ { id: 'windows_path', pattern: `(?<![A-Za-z0-9_])[A-Za-z]:[\\\\/](?:${SEG}+[\\\\/])*${SEG}*`, flags: 'g', replacement: '<PATH>' },
268
+ // The secret rules are the server's shapes with two additions its copy does not need: a
269
+ // word boundary (so "task_identifier" is not read as the key "sk_identifier") and a floor
270
+ // of six characters on the bearer value (so the phrase "Bearer token" in prose survives).
271
+ // A real credential is longer than that; a real key is not glued to the end of a word.
272
+ { id: 'bearer_token', pattern: '(?<![A-Za-z0-9])(?:Bearer|Authorization)\\s+[A-Za-z0-9\\-._~+/]{6,}=*', flags: 'gi', replacement: '[REDACTED:bearer_token]' },
273
+ { id: 'github_token', pattern: '(?<![A-Za-z0-9])(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9]{10,}', flags: 'g', replacement: '[REDACTED:github_token]' },
274
+ { id: 'api_key', pattern: '(?<![A-Za-z0-9])(?:sk|pk|rk|ak)[_-][A-Za-z0-9]{10,}', flags: 'g', replacement: '[REDACTED:api_key]' },
275
+ // The top-level domain has to be alphabetic: "foo@1.2.3" is an npm version spec, and
276
+ // eating it would destroy the version the reporter was talking about.
277
+ { id: 'email', pattern: '[A-Za-z0-9._%+\\-]+@[A-Za-z0-9\\-]+(?:\\.[A-Za-z0-9\\-]*)*\\.[A-Za-z]{2,}', flags: 'g', replacement: '<REDACTED:email>' },
278
+ ];
279
+
280
+ const COMPILED_REDACT_RULES = REDACT_RULES.map((rule) => ({
281
+ ...rule,
282
+ regex: new RegExp(rule.pattern, rule.flags),
283
+ }));
284
+
285
+ /**
286
+ * Replace every secret the table knows about. Never throws: a broken rule must not become
287
+ * a failed hook (the hook's contract is that it cannot break a session).
288
+ */
289
+ function redact(text) {
290
+ let out = String(text);
291
+ for (const rule of COMPILED_REDACT_RULES) {
292
+ try {
293
+ out = out.replace(rule.regex, rule.replacement);
294
+ } catch (err) {
295
+ debug(`redaction rule ${rule.id} failed: ${err}`);
296
+ }
297
+ }
298
+ return out;
299
+ }
300
+
301
+ /**
302
+ * Cut the query to a length, without cutting a placeholder in half: a truncated
303
+ * `<REDACTED:email` is noise the agent would carry into its own call.
304
+ *
305
+ * Counted in code points, not UTF-16 units, so this and the Python twin cut in the same
306
+ * place — the parity test compares their stdout byte for byte.
307
+ */
308
+ function clip(text, limit) {
309
+ const chars = Array.from(text);
310
+ if (chars.length <= limit) return text;
311
+ let cut = chars.slice(0, limit).join('');
312
+ const open = cut.lastIndexOf('<');
313
+ if (open !== -1 && cut.indexOf('>', open) === -1) cut = cut.slice(0, open);
314
+ return cut;
315
+ }
316
+
229
317
  function target() {
230
318
  const configured = (process.env.MISAKANET_ENDPOINT || CANONICAL_ENDPOINT).trim();
231
319
  const envToken = (process.env.MISAKANET_TOKEN || '').trim();
@@ -251,7 +339,9 @@ async function search(query) {
251
339
  signal: controller.signal,
252
340
  body: JSON.stringify({
253
341
  jsonrpc: '2.0', id: 1, method: 'tools/call',
254
- params: { name: 'misakanet_search', arguments: { query: query.slice(0, 300), top: 1, detail: 'summary' } },
342
+ // Redacted again on the way out: the fragment is already redacted, but this is the
343
+ // only place where the text actually leaves the machine, so it gets its own gate.
344
+ params: { name: 'misakanet_search', arguments: { query: redact(query).slice(0, 300), top: 1, detail: 'summary' } },
255
345
  }),
256
346
  });
257
347
  clearTimeout(timer);
@@ -302,7 +392,9 @@ function promptMode(payload) {
302
392
  async function failureMode(payload) {
303
393
  const text = failureText(payload);
304
394
  if (!text) return;
305
- const fragment = text.split(/\s+/).join(' ').slice(0, 120);
395
+ // Redact first, cut second: the query the agent is told to search with must not carry a
396
+ // path, an address or a token, and the agent may copy it into its own later call.
397
+ const fragment = clip(redact(text).split(/\s+/).join(' '), 120);
306
398
  const lines = [
307
399
  '[MisakaNet] 刚刚有一次工具调用失败。在**重试或换修法之前**先查一次(第二次盲试就是『重复犯错』):',
308
400
  ` misakanet_search(query=${JSON.stringify(fragment)}) # 查不到就用 kind="question" 提 intake,别猜`,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@misaka-net/misakanet-setup",
3
- "version": "0.5.6",
4
- "description": "One-command setup: teach your Claude Code / Codex / Hermes / OpenClaw / codewhale to search MisakaNet failure lessons before repeating a mistake, and to distil the session at a checkpoint.",
3
+ "version": "0.7.0",
4
+ "description": "MisakaNet failure lessons for your agents: writes an MCP endpoint into each agent config, rules blocks where the client takes one, and a Claude Code hook. At most one anonymous request leaves this machine at install; then only the agent's calls — unless MISAKANET_HOOK_FETCH=1 (off by default) the hook sends the failed call's error text or command.",
5
5
  "bin": {
6
6
  "misakanet-setup": "bin/misakanet-setup.mjs"
7
7
  },