@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.
- package/README.md +133 -16
- package/bin/misakanet-setup.mjs +1391 -139
- package/hook/checkpoint_reminder.mjs +94 -2
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
},
|