pi-warden 1.6.0 → 1.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/CHANGELOG.md +6 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +3 -1
- package/dist/extension.js +1 -1
- package/dist/output.js +1 -1
- package/dist/redact.d.ts +5 -3
- package/dist/redact.js +45 -10
- package/dist/shape.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ How to keep this current: add the entry in the same pull request as the change,
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 1.7.0
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `security.shareLinkParams` names the query parameters whose value is a share link rather than a credential, so a link the user reuses (a RED post's `xsec_token`) stays openable in the tool result instead of coming back as a dead `[redacted]` URL. Empty by default, user config only, and matched as a whole parameter name, with its case. A credential by construction is still masked inside a listed parameter, and a value that also appears outside the URL is masked everywhere. ([Configuration](docs/configuration.md#user-config))
|
|
14
|
+
|
|
9
15
|
## 1.6.0
|
|
10
16
|
|
|
11
17
|
### Added
|
package/dist/config.d.ts
CHANGED
|
@@ -202,6 +202,8 @@ export interface SecurityConfig {
|
|
|
202
202
|
threshold: number;
|
|
203
203
|
/** Replace high-confidence credential values in tool results with `[redacted]` before the model sees them. */
|
|
204
204
|
maskOutput: boolean;
|
|
205
|
+
/** Query parameters whose value is a share link rather than a credential. Empty by default; user config only. */
|
|
206
|
+
shareLinkParams: string[];
|
|
205
207
|
}
|
|
206
208
|
export interface RulesConfig {
|
|
207
209
|
/** Judge each write and edit against the project's Markdown rules on its own Jev request; steer, never hold. */
|
package/dist/config.js
CHANGED
|
@@ -70,7 +70,7 @@ export function defaultConfig() {
|
|
|
70
70
|
done: { enabled: true, claimsDone: 0.7, nudge: true, uiProof: true, uiFiles: [...DEFAULT_UI_FILES], visualTools: defaultVisualTools() },
|
|
71
71
|
cause: { enabled: true, claimsCause: 0.7, nudge: true, windowDays: 7 },
|
|
72
72
|
slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
|
|
73
|
-
security: { enabled: true, threshold: 0.7, maskOutput: true },
|
|
73
|
+
security: { enabled: true, threshold: 0.7, maskOutput: true, shareLinkParams: [] },
|
|
74
74
|
rules: { enabled: true, threshold: 0.7, softThreshold: 0, files: [], fallback: true, maxChars: 8000, exclude: [], skip: [], sensitivePaths: {} },
|
|
75
75
|
// 0.3 is the measured cut: 69% of the rules named apply, and 68 of the 73 requests that touch a rule's area get one.
|
|
76
76
|
rulesAtTurnStart: { enabled: true, threshold: 0.3 },
|
|
@@ -580,6 +580,8 @@ function applyGuards(base, raw, timeoutMs, source, warnings) {
|
|
|
580
580
|
threshold: source === "project" ? projectProbability("security.threshold", raw.security.threshold, base.security.threshold, warnings) : probability(raw.security.threshold, base.security.threshold),
|
|
581
581
|
// Only the user file can turn masking off: a repository must not unmask credentials in its own agent's output.
|
|
582
582
|
maskOutput: source === "user" ? boolean(raw.security.maskOutput, base.security.maskOutput) : base.security.maskOutput,
|
|
583
|
+
// A listed parameter excuses a share-link token, so it is user config only for the same reason as `maskOutput`.
|
|
584
|
+
shareLinkParams: source === "user" ? globList(raw.security.shareLinkParams, base.security.shareLinkParams) : base.security.shareLinkParams,
|
|
583
585
|
} : base.security,
|
|
584
586
|
context: isObject(raw.context) ? {
|
|
585
587
|
enabled: boolean(raw.context.enabled, base.context.enabled),
|
package/dist/extension.js
CHANGED
|
@@ -2394,7 +2394,7 @@ export default function wardenExtension(host) {
|
|
|
2394
2394
|
// and sends nothing, so it runs with the security guard off; only the user's `security.maskOutput` stops it.
|
|
2395
2395
|
const rawTexts = event.content.filter(part => part.type === "text").map(part => part.text ?? "");
|
|
2396
2396
|
const maskOutput = config.security.maskOutput;
|
|
2397
|
-
const masking = maskOutput ? rawTexts.map(maskSecrets) : [];
|
|
2397
|
+
const masking = maskOutput ? rawTexts.map((blockText) => maskSecrets(blockText, config.security.shareLinkParams)) : [];
|
|
2398
2398
|
const maskedCount = masking.reduce((sum, block) => sum + block.masked, 0);
|
|
2399
2399
|
let maskIndex = 0;
|
|
2400
2400
|
const maskedContent = maskedCount ? event.content.map(part => part.type === "text" ? { ...part, text: masking[maskIndex++].text } : part) : event.content;
|
package/dist/output.js
CHANGED
|
@@ -114,7 +114,7 @@ export function buildOutputRequest(tool, text, task, security, compress, context
|
|
|
114
114
|
}
|
|
115
115
|
export async function evaluateOutput(tool, text, task, options, compressionLearner) {
|
|
116
116
|
const secrets = options.security.enabled ? findSecrets(text) : [];
|
|
117
|
-
const { real, synthetic } = partitionSecrets(secrets, text);
|
|
117
|
+
const { real, synthetic } = partitionSecrets(secrets, text, options.security.shareLinkParams);
|
|
118
118
|
const verdict = { secret: real.length > 0, suspicious: false, retention: "all" };
|
|
119
119
|
if (real.length) {
|
|
120
120
|
verdict.secretId = secretFingerprint(real);
|
package/dist/redact.d.ts
CHANGED
|
@@ -16,9 +16,11 @@ export declare function looksLikeSecretValue(value: string): boolean;
|
|
|
16
16
|
export declare function syntheticish(value: string): boolean;
|
|
17
17
|
/**
|
|
18
18
|
* Split credentials into the ones worth announcing and the stand-ins that are only traced. With `text`, a value found
|
|
19
|
-
* only in the query signature of a signed URL is a stand-in too
|
|
19
|
+
* only in the query signature of a signed URL is a stand-in too, as is a value found only in a share-link parameter
|
|
20
|
+
* named by `shareLinkParams`. A credential by construction stays real even inside a listed parameter, so a real key
|
|
21
|
+
* that a tool result puts in a listed parameter is still masked.
|
|
20
22
|
*/
|
|
21
|
-
export declare function partitionSecrets(secrets: readonly string[], text?: string): {
|
|
23
|
+
export declare function partitionSecrets(secrets: readonly string[], text?: string, shareLinkParams?: readonly string[]): {
|
|
22
24
|
real: string[];
|
|
23
25
|
synthetic: string[];
|
|
24
26
|
};
|
|
@@ -29,7 +31,7 @@ export declare function findSecrets(text: string): string[];
|
|
|
29
31
|
* assignments, `Authorization` and `Bearer` values, and URL passwords. Stand-ins (`syntheticish`, signed URL
|
|
30
32
|
* signatures) stay readable. Masking the whole detected set means a value is never announced without being masked.
|
|
31
33
|
*/
|
|
32
|
-
export declare function maskSecrets(text: string): {
|
|
34
|
+
export declare function maskSecrets(text: string, shareLinkParams?: readonly string[]): {
|
|
33
35
|
text: string;
|
|
34
36
|
masked: number;
|
|
35
37
|
};
|
package/dist/redact.js
CHANGED
|
@@ -181,11 +181,40 @@ function signedUrlSpans(text) {
|
|
|
181
181
|
}
|
|
182
182
|
return spans;
|
|
183
183
|
}
|
|
184
|
+
/** A literal for a `RegExp`, for the parameter names that come from the user's config. */
|
|
185
|
+
function escapeRegExp(text) {
|
|
186
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
187
|
+
}
|
|
184
188
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
189
|
+
* Start and end offsets of the value of every listed share-link parameter in an `https://` URL in `text`. A share link
|
|
190
|
+
* carries a token that is what makes the link openable at all, and the same link is reused rather than expiring, so
|
|
191
|
+
* its token is a stand-in like a signed URL's signature: `security.shareLinkParams` names the parameters to read.
|
|
187
192
|
*/
|
|
188
|
-
function
|
|
193
|
+
function shareLinkSpans(text, shareLinkParams) {
|
|
194
|
+
const names = [...new Set(shareLinkParams.map((name) => name.trim()).filter(Boolean))];
|
|
195
|
+
if (!names.length)
|
|
196
|
+
return [];
|
|
197
|
+
// The value stops at a backslash because a JSON body writes `&` as `\u0026`, and the name is matched with its case.
|
|
198
|
+
const pattern = new RegExp(`[?&](?:${names.map(escapeRegExp).join("|")})=([^&#\\\\]*)`, "g");
|
|
199
|
+
const spans = [];
|
|
200
|
+
for (const url of text.matchAll(HTTPS_URL)) {
|
|
201
|
+
for (const param of url[0].matchAll(pattern)) {
|
|
202
|
+
const end = url.index + param.index + param[0].length;
|
|
203
|
+
spans.push([end - param[1].length, end]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return spans;
|
|
207
|
+
}
|
|
208
|
+
/** True when the value is a credential by construction, so a listed share-link parameter must not excuse it. */
|
|
209
|
+
function isTokenShape(value) {
|
|
210
|
+
return TOKEN_SHAPES.some((shape) => new RegExp(shape.source).test(value));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* True when every occurrence of `value` in `text` sits inside one of `spans`. A signed URL expires within minutes and
|
|
214
|
+
* grants one object; issue-tracker and storage API responses are full of them. A listed share-link token is excused
|
|
215
|
+
* on the same terms: a value that also appears outside the URL is a credential wherever it is, and is masked there.
|
|
216
|
+
*/
|
|
217
|
+
function onlyInsideSpans(value, spans, text) {
|
|
189
218
|
if (!spans.length)
|
|
190
219
|
return false;
|
|
191
220
|
let found = false;
|
|
@@ -198,14 +227,20 @@ function signedUrlOnly(value, spans, text) {
|
|
|
198
227
|
}
|
|
199
228
|
/**
|
|
200
229
|
* Split credentials into the ones worth announcing and the stand-ins that are only traced. With `text`, a value found
|
|
201
|
-
* only in the query signature of a signed URL is a stand-in too
|
|
230
|
+
* only in the query signature of a signed URL is a stand-in too, as is a value found only in a share-link parameter
|
|
231
|
+
* named by `shareLinkParams`. A credential by construction stays real even inside a listed parameter, so a real key
|
|
232
|
+
* that a tool result puts in a listed parameter is still masked.
|
|
202
233
|
*/
|
|
203
|
-
export function partitionSecrets(secrets, text) {
|
|
234
|
+
export function partitionSecrets(secrets, text, shareLinkParams = []) {
|
|
204
235
|
const real = [];
|
|
205
236
|
const synthetic = [];
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
237
|
+
const signed = text === undefined ? [] : signedUrlSpans(text);
|
|
238
|
+
const shared = text === undefined ? [] : shareLinkSpans(text, shareLinkParams);
|
|
239
|
+
for (const value of secrets) {
|
|
240
|
+
// The span test first, so the default (no listed parameter) costs nothing.
|
|
241
|
+
const standIn = syntheticish(value) || onlyInsideSpans(value, signed, text ?? "") || (onlyInsideSpans(value, shared, text ?? "") && !isTokenShape(value));
|
|
242
|
+
(standIn ? synthetic : real).push(value);
|
|
243
|
+
}
|
|
209
244
|
return { real, synthetic };
|
|
210
245
|
}
|
|
211
246
|
/** The credential-shaped values in `text`, deduplicated. Empty when the text only talks about credentials. */
|
|
@@ -225,8 +260,8 @@ export function findSecrets(text) {
|
|
|
225
260
|
* assignments, `Authorization` and `Bearer` values, and URL passwords. Stand-ins (`syntheticish`, signed URL
|
|
226
261
|
* signatures) stay readable. Masking the whole detected set means a value is never announced without being masked.
|
|
227
262
|
*/
|
|
228
|
-
export function maskSecrets(text) {
|
|
229
|
-
const { real } = partitionSecrets(findSecrets(text), text);
|
|
263
|
+
export function maskSecrets(text, shareLinkParams = []) {
|
|
264
|
+
const { real } = partitionSecrets(findSecrets(text), text, shareLinkParams);
|
|
230
265
|
let out = text;
|
|
231
266
|
let masked = 0;
|
|
232
267
|
// Longest first, so a value that contains another is replaced whole.
|
package/dist/shape.js
CHANGED
|
@@ -44,7 +44,7 @@ export function completeConfig(loaded) {
|
|
|
44
44
|
done: section("done", { ...off, claimsDone: 1, nudge: false, uiProof: false, uiFiles: [], visualTools: { commands: [], commandWords: [], tools: [], images: [] } }),
|
|
45
45
|
cause: section("cause", { ...off, claimsCause: 1, nudge: false, windowDays: 7 }),
|
|
46
46
|
slop: section("slop", { ...off, threshold: 1, prose: proseOff() }),
|
|
47
|
-
security: section("security", { ...off, threshold: 1, maskOutput: false }),
|
|
47
|
+
security: section("security", { ...off, threshold: 1, maskOutput: false, shareLinkParams: [] }),
|
|
48
48
|
rules: section("rules", { ...off, threshold: 1, softThreshold: 0, files: [], fallback: false, maxChars: 500, exclude: [], skip: [], sensitivePaths: {} }),
|
|
49
49
|
// A missing section turns the turn-start reminder off: it is new, so nothing it says was expected.
|
|
50
50
|
rulesAtTurnStart: section("rulesAtTurnStart", { ...off, threshold: 1 }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-warden",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Makes the Pi agent follow your project's rules. Jev judges every write against your pi-warden.md and quotes the broken rule back to the agent, names slop, breaks stuck loops, calls out unverified done claims, compresses large tool output, and holds the rare destructive command. Built on pi-typesafe.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|