pi-roundtable 0.7.5 → 0.7.6
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/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/kit/diagnostics.ts +64 -10
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.6] - 2026-10-03
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- `scrubDiagnostic` takes time linear in the text at any bound (`diagnosticChars: Infinity` included): every pattern starts at the beginning of a run through a lookbehind, and a value is read only for a secret-named field.
|
|
13
|
+
- `scrubDiagnostic` no longer masks what is not a secret: plural `tokens` counters (`max_tokens`, `input_tokens`), a numeric value of a token name, setting names such as `password_policy` or `token_limit`, and the word after `Basic`, `Bearer` or `token` unless it has a credential shape. A value ends at `}` and `]`.
|
|
14
|
+
- Drawing: `permissive` also accepts an empty relationship-map node id (the argument schema allows it), draws a sigil whose intention has no letters or is blank, and ignores a sacred-geometry color canvas cannot parse.
|
|
15
|
+
|
|
8
16
|
## [0.7.5] - 2026-10-03
|
|
9
17
|
|
|
10
18
|
### Added
|
package/package.json
CHANGED
package/src/kit/diagnostics.ts
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Every pattern here either starts with a literal or starts at the beginning of a run of its own
|
|
3
|
+
* character class (a lookbehind, never a leading `\b` before a repeatable class), so scrubbing
|
|
4
|
+
* takes time linear in the text at any length.
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
/** Tool text that may name a credential: userinfo in URLs, bearer values, and well-known token shapes. */
|
|
2
|
-
const CREDENTIALS: [RegExp, string][] = [
|
|
3
|
-
[
|
|
8
|
+
const CREDENTIALS: [RegExp, string | ((...match: string[]) => string)][] = [
|
|
9
|
+
[/(?<![a-z0-9+.-])([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1[redacted]@"],
|
|
4
10
|
[
|
|
5
11
|
/\b(authorization|proxy-authorization)\s*[:=]\s*[^\r\n]+/gi,
|
|
6
12
|
"$1: [redacted]",
|
|
7
13
|
],
|
|
8
|
-
[/\b(bearer|basic|token)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 [redacted]"],
|
|
9
14
|
[
|
|
10
|
-
/\b(
|
|
11
|
-
"
|
|
15
|
+
/\b(bearer|basic|token)(\s+)([A-Za-z0-9._~+/=-]{8,})/gi,
|
|
16
|
+
(match, scheme = "", _space = "", value = "") =>
|
|
17
|
+
looksLikeCredential(value) ? `${scheme} [redacted]` : match,
|
|
12
18
|
],
|
|
13
|
-
// Assignments, query parameters and JSON fields whose name says it is a secret, in any case.
|
|
14
19
|
[
|
|
15
|
-
/\b([
|
|
16
|
-
"
|
|
20
|
+
/\b(gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9_-]{16,}|xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{30,})\b/g,
|
|
21
|
+
"[redacted]",
|
|
17
22
|
],
|
|
18
23
|
[
|
|
19
24
|
/\b(cookie|set-cookie|x-api-key|x-auth-token|x-access-token)\s*:\s*[^\r\n]+/gi,
|
|
@@ -26,15 +31,64 @@ const CREDENTIALS: [RegExp, string][] = [
|
|
|
26
31
|
],
|
|
27
32
|
];
|
|
28
33
|
|
|
34
|
+
/** A bare word such as "authentication" is not a credential; a value with digits, symbols or inner capitals, or a very long one, may be. */
|
|
35
|
+
function looksLikeCredential(value: string): boolean {
|
|
36
|
+
return (
|
|
37
|
+
/[0-9+/=_~]/.test(value) || /[a-z][A-Z]/.test(value) || value.length >= 32
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Names that say the value is a secret. A plural `tokens` counts tokens, and is not one. */
|
|
42
|
+
const SECRET_NAME =
|
|
43
|
+
/token(?!s(?![a-z]))|secret|password|passwd|api[_-]?key|credential/i;
|
|
44
|
+
/** What a name may continue with after the keyword and still hold a setting, not a secret. */
|
|
45
|
+
const SETTING_SUFFIX =
|
|
46
|
+
/^[_-]?(?:policy|policies|rules?|length|count|limit|max|min|type|format|expir\w*|ttl|lifetime|required|enabled|strategy|file|path|url|name|field|prompt|hint|window|budget|usage)(?![a-z])/i;
|
|
47
|
+
|
|
48
|
+
/** Whether the name says its value is a secret; a count is not a token, so a token name needs a non-numeric value. */
|
|
49
|
+
function secretKind(name: string): "secret" | "token" | undefined {
|
|
50
|
+
const keyword = SECRET_NAME.exec(name);
|
|
51
|
+
if (!keyword) return undefined;
|
|
52
|
+
const rest = name.slice(keyword.index + keyword[0].length);
|
|
53
|
+
if (SETTING_SUFFIX.test(rest)) return undefined;
|
|
54
|
+
return /^token/i.test(keyword[0]) ? "token" : "secret";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The name and separator of an assignment, query parameter or JSON field; the value is read separately. */
|
|
58
|
+
const ASSIGNMENT = /(?<![a-z0-9_-])([a-z0-9_-]+)(["']?\s*[:=]\s*["']?)/gi;
|
|
59
|
+
const VALUE = /[^\s"',&;}\]]+/y;
|
|
60
|
+
|
|
61
|
+
/** Masks the value of every assignment whose name says it is a secret, in any case. */
|
|
62
|
+
function maskAssignments(text: string): string {
|
|
63
|
+
let out = "";
|
|
64
|
+
let cursor = 0;
|
|
65
|
+
for (const match of text.matchAll(ASSIGNMENT)) {
|
|
66
|
+
const start = match.index ?? 0;
|
|
67
|
+
const end = start + match[0].length;
|
|
68
|
+
if (start < cursor) continue;
|
|
69
|
+
// The value is read only for a secret name, so ordinary text costs nothing more to pass.
|
|
70
|
+
const kind = secretKind(match[1] ?? "");
|
|
71
|
+
if (!kind) continue;
|
|
72
|
+
VALUE.lastIndex = end;
|
|
73
|
+
const value = VALUE.exec(text)?.[0];
|
|
74
|
+
if (value === undefined || (kind === "token" && /^[0-9]+$/.test(value)))
|
|
75
|
+
continue;
|
|
76
|
+
out += `${text.slice(cursor, end)}[redacted]`;
|
|
77
|
+
cursor = end + value.length;
|
|
78
|
+
}
|
|
79
|
+
return out + text.slice(cursor);
|
|
80
|
+
}
|
|
81
|
+
|
|
29
82
|
/**
|
|
30
83
|
* Text from a failed subprocess or worker, safe to show a model or the owner: credentials are
|
|
31
84
|
* masked, control characters are removed, and the length is bounded. Empty input returns "".
|
|
32
85
|
*/
|
|
33
86
|
export function scrubDiagnostic(text: string, max = 600): string {
|
|
34
|
-
//
|
|
87
|
+
// Only what could survive the final cut is scrubbed, so the work follows the bound asked for.
|
|
35
88
|
let clean = text.slice(0, Math.max(max * 4, 4096));
|
|
36
89
|
for (const [pattern, replacement] of CREDENTIALS)
|
|
37
|
-
clean = clean.replace(pattern, replacement);
|
|
90
|
+
clean = clean.replace(pattern, replacement as string);
|
|
91
|
+
clean = maskAssignments(clean);
|
|
38
92
|
clean = Array.from(clean, (char) => {
|
|
39
93
|
const code = char.charCodeAt(0);
|
|
40
94
|
// Keep tab and newline; every other control character becomes a space.
|