@agent-finops/core 0.9.6 → 0.9.7
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 +3 -3
- package/dist/actionPlanner.js +4 -1
- package/dist/actionVerification.d.ts +54 -54
- package/dist/activitySnapshot.d.ts +57 -57
- package/dist/agentEconomicsReceipt.d.ts +62 -62
- package/dist/analyze.js +59 -18
- package/dist/contextHealth.js +15 -4
- package/dist/cutList.d.ts +23 -0
- package/dist/cutList.js +268 -11
- package/dist/deadContext.js +10 -3
- package/dist/insights.js +41 -23
- package/dist/planMath.js +25 -4
- package/dist/projectIndexStore.d.ts +8 -8
- package/dist/qualitativeIndexCache.d.ts +8 -8
- package/dist/toolInvocations.js +9 -1
- package/dist/untrustedLabel.d.ts +70 -0
- package/dist/untrustedLabel.js +161 -0
- package/package.json +1 -1
|
@@ -112,9 +112,9 @@ declare const documentSchema: z.ZodObject<{
|
|
|
112
112
|
not_separately_reported: "not_separately_reported";
|
|
113
113
|
}>;
|
|
114
114
|
cacheWriteTokens: z.ZodEnum<{
|
|
115
|
-
partial: "partial";
|
|
116
115
|
observed: "observed";
|
|
117
116
|
not_separately_reported: "not_separately_reported";
|
|
117
|
+
partial: "partial";
|
|
118
118
|
}>;
|
|
119
119
|
thoughtTokens: z.ZodEnum<{
|
|
120
120
|
observed: "observed";
|
|
@@ -129,8 +129,8 @@ declare const documentSchema: z.ZodObject<{
|
|
|
129
129
|
calculated_partial: "calculated_partial";
|
|
130
130
|
}>;
|
|
131
131
|
reportedTotalTokens: z.ZodEnum<{
|
|
132
|
-
not_reported: "not_reported";
|
|
133
132
|
provider_reported: "provider_reported";
|
|
133
|
+
not_reported: "not_reported";
|
|
134
134
|
}>;
|
|
135
135
|
}, z.core.$strict>>;
|
|
136
136
|
sourceVersion: z.ZodOptional<z.ZodString>;
|
|
@@ -189,9 +189,9 @@ declare const documentSchema: z.ZodObject<{
|
|
|
189
189
|
activity: z.ZodOptional<z.ZodObject<{
|
|
190
190
|
summary: z.ZodString;
|
|
191
191
|
kind: z.ZodEnum<{
|
|
192
|
-
file: "file";
|
|
193
|
-
agent: "agent";
|
|
194
192
|
project: "project";
|
|
193
|
+
agent: "agent";
|
|
194
|
+
file: "file";
|
|
195
195
|
task: "task";
|
|
196
196
|
automation: "automation";
|
|
197
197
|
}>;
|
|
@@ -331,9 +331,9 @@ declare const documentSchema: z.ZodObject<{
|
|
|
331
331
|
not_separately_reported: "not_separately_reported";
|
|
332
332
|
}>;
|
|
333
333
|
cacheWriteTokens: z.ZodEnum<{
|
|
334
|
-
partial: "partial";
|
|
335
334
|
observed: "observed";
|
|
336
335
|
not_separately_reported: "not_separately_reported";
|
|
336
|
+
partial: "partial";
|
|
337
337
|
}>;
|
|
338
338
|
thoughtTokens: z.ZodEnum<{
|
|
339
339
|
observed: "observed";
|
|
@@ -348,8 +348,8 @@ declare const documentSchema: z.ZodObject<{
|
|
|
348
348
|
calculated_partial: "calculated_partial";
|
|
349
349
|
}>;
|
|
350
350
|
reportedTotalTokens: z.ZodEnum<{
|
|
351
|
-
not_reported: "not_reported";
|
|
352
351
|
provider_reported: "provider_reported";
|
|
352
|
+
not_reported: "not_reported";
|
|
353
353
|
}>;
|
|
354
354
|
}, z.core.$strict>>;
|
|
355
355
|
sourceVersion: z.ZodOptional<z.ZodString>;
|
|
@@ -408,9 +408,9 @@ declare const documentSchema: z.ZodObject<{
|
|
|
408
408
|
activity: z.ZodOptional<z.ZodObject<{
|
|
409
409
|
summary: z.ZodString;
|
|
410
410
|
kind: z.ZodEnum<{
|
|
411
|
-
file: "file";
|
|
412
|
-
agent: "agent";
|
|
413
411
|
project: "project";
|
|
412
|
+
agent: "agent";
|
|
413
|
+
file: "file";
|
|
414
414
|
task: "task";
|
|
415
415
|
automation: "automation";
|
|
416
416
|
}>;
|
|
@@ -83,9 +83,9 @@ declare const valueSchema: z.ZodObject<{
|
|
|
83
83
|
not_separately_reported: "not_separately_reported";
|
|
84
84
|
}>;
|
|
85
85
|
cacheWriteTokens: z.ZodEnum<{
|
|
86
|
-
partial: "partial";
|
|
87
86
|
observed: "observed";
|
|
88
87
|
not_separately_reported: "not_separately_reported";
|
|
88
|
+
partial: "partial";
|
|
89
89
|
}>;
|
|
90
90
|
thoughtTokens: z.ZodEnum<{
|
|
91
91
|
observed: "observed";
|
|
@@ -100,8 +100,8 @@ declare const valueSchema: z.ZodObject<{
|
|
|
100
100
|
calculated_partial: "calculated_partial";
|
|
101
101
|
}>;
|
|
102
102
|
reportedTotalTokens: z.ZodEnum<{
|
|
103
|
-
not_reported: "not_reported";
|
|
104
103
|
provider_reported: "provider_reported";
|
|
104
|
+
not_reported: "not_reported";
|
|
105
105
|
}>;
|
|
106
106
|
}, z.core.$strict>>;
|
|
107
107
|
sourceVersion: z.ZodOptional<z.ZodString>;
|
|
@@ -160,9 +160,9 @@ declare const valueSchema: z.ZodObject<{
|
|
|
160
160
|
activity: z.ZodOptional<z.ZodObject<{
|
|
161
161
|
summary: z.ZodString;
|
|
162
162
|
kind: z.ZodEnum<{
|
|
163
|
-
file: "file";
|
|
164
|
-
agent: "agent";
|
|
165
163
|
project: "project";
|
|
164
|
+
agent: "agent";
|
|
165
|
+
file: "file";
|
|
166
166
|
task: "task";
|
|
167
167
|
automation: "automation";
|
|
168
168
|
}>;
|
|
@@ -309,9 +309,9 @@ export declare const qualitativeEntryValueSchema: z.ZodObject<{
|
|
|
309
309
|
not_separately_reported: "not_separately_reported";
|
|
310
310
|
}>;
|
|
311
311
|
cacheWriteTokens: z.ZodEnum<{
|
|
312
|
-
partial: "partial";
|
|
313
312
|
observed: "observed";
|
|
314
313
|
not_separately_reported: "not_separately_reported";
|
|
314
|
+
partial: "partial";
|
|
315
315
|
}>;
|
|
316
316
|
thoughtTokens: z.ZodEnum<{
|
|
317
317
|
observed: "observed";
|
|
@@ -326,8 +326,8 @@ export declare const qualitativeEntryValueSchema: z.ZodObject<{
|
|
|
326
326
|
calculated_partial: "calculated_partial";
|
|
327
327
|
}>;
|
|
328
328
|
reportedTotalTokens: z.ZodEnum<{
|
|
329
|
-
not_reported: "not_reported";
|
|
330
329
|
provider_reported: "provider_reported";
|
|
330
|
+
not_reported: "not_reported";
|
|
331
331
|
}>;
|
|
332
332
|
}, z.core.$strict>>;
|
|
333
333
|
sourceVersion: z.ZodOptional<z.ZodString>;
|
|
@@ -386,9 +386,9 @@ export declare const qualitativeEntryValueSchema: z.ZodObject<{
|
|
|
386
386
|
activity: z.ZodOptional<z.ZodObject<{
|
|
387
387
|
summary: z.ZodString;
|
|
388
388
|
kind: z.ZodEnum<{
|
|
389
|
-
file: "file";
|
|
390
|
-
agent: "agent";
|
|
391
389
|
project: "project";
|
|
390
|
+
agent: "agent";
|
|
391
|
+
file: "file";
|
|
392
392
|
task: "task";
|
|
393
393
|
automation: "automation";
|
|
394
394
|
}>;
|
package/dist/toolInvocations.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { safeUntrustedLabel, WITHHELD_FILE_LABEL } from "./untrustedLabel.js";
|
|
4
5
|
/** Parse ONE transcript's content. Exported for tests. Returns the per-file pieces the aggregator needs. */
|
|
5
6
|
export function parseClaudeCodeInvocations(content, sinceMs) {
|
|
6
7
|
const counts = new Map();
|
|
@@ -605,8 +606,15 @@ function explicitReadFile(toolName, input) {
|
|
|
605
606
|
return name && name !== "." && name !== "/" ? name : undefined;
|
|
606
607
|
}
|
|
607
608
|
function buildSessionContextSignal(input) {
|
|
609
|
+
// File names come off transcript tool-call metadata, so they are untrusted,
|
|
610
|
+
// and they travel as DATA rather than prose: {name, count} objects that the
|
|
611
|
+
// MCP tools hand to an agent verbatim. Neutralizing the sentence built from
|
|
612
|
+
// this array while the array itself stayed raw gave the human the redaction
|
|
613
|
+
// and the agent the payload — backwards, on the one surface where injected
|
|
614
|
+
// text can actually steer a coding agent. Neutralize at the source, so every
|
|
615
|
+
// consumer (Glance, MCP, CLI, the action planner) gets the same safe name.
|
|
608
616
|
const fileReads = [...input.fileReads.entries()]
|
|
609
|
-
.map(([name, count]) => ({ name, count }))
|
|
617
|
+
.map(([name, count]) => ({ name: safeUntrustedLabel(name, WITHHELD_FILE_LABEL), count }))
|
|
610
618
|
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
|
|
611
619
|
return {
|
|
612
620
|
agent: input.agent,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE place that decides what an untrusted NAME is allowed to become before it
|
|
3
|
+
* is interpolated into a sentence this product wrote.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists at all. Every user-facing string here is built by templating a
|
|
6
|
+
* fragment the user did not author — a folder name off disk, a model id off a
|
|
7
|
+
* provider response, an operation label off an adapter — into prose that a
|
|
8
|
+
* coding agent will later read as instructions. The renderers cannot be the
|
|
9
|
+
* ones to make that safe:
|
|
10
|
+
*
|
|
11
|
+
* - The `--full` terminal readout does not sanitize at all.
|
|
12
|
+
* - The Markdown/Apply sanitizers that BLANK on a directive hit delete the
|
|
13
|
+
* whole string, and the whole string is mostly OUR sentence. In 0.9.7 that
|
|
14
|
+
* deleted the entire recommendation for 8 of 11 ordinary repo basenames,
|
|
15
|
+
* because `write-ahead-log` sat 41 characters in front of our own word
|
|
16
|
+
* "tokens" — and the terminal kept printing the finding, so two surfaces
|
|
17
|
+
* disagreed about a dollar figure.
|
|
18
|
+
*
|
|
19
|
+
* So the check runs HERE, on the fragment alone, before it reaches any
|
|
20
|
+
* template. A fragment carries only the user's text, so an ordinary name has
|
|
21
|
+
* nothing of ours to pair with; and once the fragment is safe, every surface
|
|
22
|
+
* can render the finished sentence verbatim and they all agree.
|
|
23
|
+
*
|
|
24
|
+
* The rule for anyone adding a producer: if you interpolate a value that came
|
|
25
|
+
* off disk or off the wire into a string a user or an agent will read, wrap it
|
|
26
|
+
* in {@link safeUntrustedLabel} at the point of interpolation. Not at the
|
|
27
|
+
* renderer. Not once per surface. Here.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* What an untrusted label becomes when the name itself reads like an
|
|
31
|
+
* instruction. Each says WHY, because "withheld" with no reason reads like the
|
|
32
|
+
* product failed rather than declined: `diagnose` still shows the real folder
|
|
33
|
+
* name, so this is only about not REPEATING a name that looked like an
|
|
34
|
+
* instruction inside a sentence an agent will read.
|
|
35
|
+
*
|
|
36
|
+
* Every one of these must survive the report layer's own sanitizer UNCHANGED —
|
|
37
|
+
* a marker in brackets would be stripped there and two surfaces would disagree
|
|
38
|
+
* about a string whose whole job is agreeing. Parentheses survive; brackets do
|
|
39
|
+
* not.
|
|
40
|
+
*
|
|
41
|
+
* The project label sits in appositive and prepositional slots ("X — median day
|
|
42
|
+
* carried…", "the heaviest sessions in X"), so it carries the reason as prose.
|
|
43
|
+
* The rest sit in ATTRIBUTIVE slots ("Cache repeated X calls"), where a clause
|
|
44
|
+
* would not parse, so they carry the short parenthetical form.
|
|
45
|
+
*/
|
|
46
|
+
export declare const WITHHELD_PROJECT_LABEL = "a project whose name reads like an instruction";
|
|
47
|
+
export declare const WITHHELD_MODEL_LABEL = "(model name reads like an instruction; withheld)";
|
|
48
|
+
export declare const WITHHELD_OPERATION_LABEL = "(operation name reads like an instruction; withheld)";
|
|
49
|
+
export declare const WITHHELD_AGENT_LABEL = "(agent name reads like an instruction; withheld)";
|
|
50
|
+
export declare const WITHHELD_CLIENT_LABEL = "(client name reads like an instruction; withheld)";
|
|
51
|
+
/**
|
|
52
|
+
* For a breakdown key whose dimension is decided at runtime — the same slot
|
|
53
|
+
* holds a client, a project, an agent, a model, or an operation depending on
|
|
54
|
+
* which grouping won.
|
|
55
|
+
*/
|
|
56
|
+
export declare const WITHHELD_ENTITY_LABEL = "(name reads like an instruction; withheld)";
|
|
57
|
+
export declare const WITHHELD_FILE_LABEL = "(file name reads like an instruction; withheld)";
|
|
58
|
+
export declare const WITHHELD_PLAN_LABEL = "(plan label reads like an instruction; withheld)";
|
|
59
|
+
/** Map a list of untrusted keys for display, keeping order and length. */
|
|
60
|
+
export declare function safeUntrustedLabels(values: readonly string[], withheld?: string): string[];
|
|
61
|
+
/**
|
|
62
|
+
* Neutralize ONE untrusted fragment before it is interpolated into
|
|
63
|
+
* product-authored prose.
|
|
64
|
+
*
|
|
65
|
+
* Over-triggering here is cheap and under-triggering is not: a false positive
|
|
66
|
+
* costs one name while the finding and its dollars survive, so the patterns
|
|
67
|
+
* stay strict.
|
|
68
|
+
*/
|
|
69
|
+
export declare function safeUntrustedLabel(value: string, withheld: string): string;
|
|
70
|
+
//# sourceMappingURL=untrustedLabel.d.ts.map
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE place that decides what an untrusted NAME is allowed to become before it
|
|
3
|
+
* is interpolated into a sentence this product wrote.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists at all. Every user-facing string here is built by templating a
|
|
6
|
+
* fragment the user did not author — a folder name off disk, a model id off a
|
|
7
|
+
* provider response, an operation label off an adapter — into prose that a
|
|
8
|
+
* coding agent will later read as instructions. The renderers cannot be the
|
|
9
|
+
* ones to make that safe:
|
|
10
|
+
*
|
|
11
|
+
* - The `--full` terminal readout does not sanitize at all.
|
|
12
|
+
* - The Markdown/Apply sanitizers that BLANK on a directive hit delete the
|
|
13
|
+
* whole string, and the whole string is mostly OUR sentence. In 0.9.7 that
|
|
14
|
+
* deleted the entire recommendation for 8 of 11 ordinary repo basenames,
|
|
15
|
+
* because `write-ahead-log` sat 41 characters in front of our own word
|
|
16
|
+
* "tokens" — and the terminal kept printing the finding, so two surfaces
|
|
17
|
+
* disagreed about a dollar figure.
|
|
18
|
+
*
|
|
19
|
+
* So the check runs HERE, on the fragment alone, before it reaches any
|
|
20
|
+
* template. A fragment carries only the user's text, so an ordinary name has
|
|
21
|
+
* nothing of ours to pair with; and once the fragment is safe, every surface
|
|
22
|
+
* can render the finished sentence verbatim and they all agree.
|
|
23
|
+
*
|
|
24
|
+
* The rule for anyone adding a producer: if you interpolate a value that came
|
|
25
|
+
* off disk or off the wire into a string a user or an agent will read, wrap it
|
|
26
|
+
* in {@link safeUntrustedLabel} at the point of interpolation. Not at the
|
|
27
|
+
* renderer. Not once per surface. Here.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* What an untrusted label becomes when the name itself reads like an
|
|
31
|
+
* instruction. Each says WHY, because "withheld" with no reason reads like the
|
|
32
|
+
* product failed rather than declined: `diagnose` still shows the real folder
|
|
33
|
+
* name, so this is only about not REPEATING a name that looked like an
|
|
34
|
+
* instruction inside a sentence an agent will read.
|
|
35
|
+
*
|
|
36
|
+
* Every one of these must survive the report layer's own sanitizer UNCHANGED —
|
|
37
|
+
* a marker in brackets would be stripped there and two surfaces would disagree
|
|
38
|
+
* about a string whose whole job is agreeing. Parentheses survive; brackets do
|
|
39
|
+
* not.
|
|
40
|
+
*
|
|
41
|
+
* The project label sits in appositive and prepositional slots ("X — median day
|
|
42
|
+
* carried…", "the heaviest sessions in X"), so it carries the reason as prose.
|
|
43
|
+
* The rest sit in ATTRIBUTIVE slots ("Cache repeated X calls"), where a clause
|
|
44
|
+
* would not parse, so they carry the short parenthetical form.
|
|
45
|
+
*/
|
|
46
|
+
export const WITHHELD_PROJECT_LABEL = "a project whose name reads like an instruction";
|
|
47
|
+
export const WITHHELD_MODEL_LABEL = "(model name reads like an instruction; withheld)";
|
|
48
|
+
export const WITHHELD_OPERATION_LABEL = "(operation name reads like an instruction; withheld)";
|
|
49
|
+
export const WITHHELD_AGENT_LABEL = "(agent name reads like an instruction; withheld)";
|
|
50
|
+
export const WITHHELD_CLIENT_LABEL = "(client name reads like an instruction; withheld)";
|
|
51
|
+
/**
|
|
52
|
+
* For a breakdown key whose dimension is decided at runtime — the same slot
|
|
53
|
+
* holds a client, a project, an agent, a model, or an operation depending on
|
|
54
|
+
* which grouping won.
|
|
55
|
+
*/
|
|
56
|
+
export const WITHHELD_ENTITY_LABEL = "(name reads like an instruction; withheld)";
|
|
57
|
+
export const WITHHELD_FILE_LABEL = "(file name reads like an instruction; withheld)";
|
|
58
|
+
export const WITHHELD_PLAN_LABEL = "(plan label reads like an instruction; withheld)";
|
|
59
|
+
/** Map a list of untrusted keys for display, keeping order and length. */
|
|
60
|
+
export function safeUntrustedLabels(values, withheld = WITHHELD_ENTITY_LABEL) {
|
|
61
|
+
return values.map((value) => safeUntrustedLabel(value, withheld));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Neutralize ONE untrusted fragment before it is interpolated into
|
|
65
|
+
* product-authored prose.
|
|
66
|
+
*
|
|
67
|
+
* Over-triggering here is cheap and under-triggering is not: a false positive
|
|
68
|
+
* costs one name while the finding and its dollars survive, so the patterns
|
|
69
|
+
* stay strict.
|
|
70
|
+
*/
|
|
71
|
+
export function safeUntrustedLabel(value, withheld) {
|
|
72
|
+
// Control characters and line breaks are structure, not name: a label that
|
|
73
|
+
// can open a new line can forge a new instruction on every surface at once.
|
|
74
|
+
const collapsed = value
|
|
75
|
+
.replace(/[\u0000-\u001F\u007F]/gu, " ")
|
|
76
|
+
.replace(/\s+/gu, " ")
|
|
77
|
+
.trim();
|
|
78
|
+
if (!collapsed)
|
|
79
|
+
return withheld;
|
|
80
|
+
return looksLikeDirectiveFragment(collapsed) ? withheld : collapsed;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Characters that are invisible to the reader but split a word for the
|
|
84
|
+
* matcher: zero-width spaces and joiners, bidi controls, variation selectors,
|
|
85
|
+
* the soft hyphen, the BOM. `i\u200Bgnore all previous instructions` reads as
|
|
86
|
+
* an instruction and matched nothing. Stripped for DETECTION ONLY — the label
|
|
87
|
+
* that gets printed is always the original text.
|
|
88
|
+
*/
|
|
89
|
+
const INVISIBLE_SEPARATORS = /[\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u206A-\u206F\u3164\uFE00-\uFE0F\uFEFF\uFFA0]/gu;
|
|
90
|
+
/**
|
|
91
|
+
* The eight Latin/Cyrillic confusables that carry the directive verbs we look
|
|
92
|
+
* for: `\u0456gnore`, `d\u0435lete`, `\u0455ystem:` are indistinguishable on screen
|
|
93
|
+
* and invisible to an ASCII pattern. Folded for DETECTION ONLY.
|
|
94
|
+
*/
|
|
95
|
+
const CONFUSABLE_FOLD = new Map([
|
|
96
|
+
["\u0430", "a"], ["\u0435", "e"], ["\u043E", "o"], ["\u0440", "p"],
|
|
97
|
+
["\u0441", "c"], ["\u0445", "x"], ["\u0455", "s"], ["\u0456", "i"]
|
|
98
|
+
]);
|
|
99
|
+
/**
|
|
100
|
+
* The fragment is read TWICE, because a name and an instruction disagree about
|
|
101
|
+
* what a hyphen means.
|
|
102
|
+
*
|
|
103
|
+
* As ONE IDENTIFIER (`-` behaves like `_`): `ignore-list` is a directory, so
|
|
104
|
+
* the blunt single-word patterns cannot fire on it. This is what keeps ordinary
|
|
105
|
+
* repo names whole.
|
|
106
|
+
*
|
|
107
|
+
* As SEPARATED WORDS (`-` and `_` are spaces): `ignore-all-previous-instructions`
|
|
108
|
+
* is an instruction wearing a filename's punctuation. Only the PAIRED patterns
|
|
109
|
+
* run in this pass — each needs a directive verb next to an injection-flavored
|
|
110
|
+
* object — so an ordinary compound name has nothing to pair with. The unpaired
|
|
111
|
+
* verb list and the execute/run pattern deliberately stay out: `run-command-service`
|
|
112
|
+
* is a real directory, and a name-shaped `run-shell` cannot instruct anything.
|
|
113
|
+
*/
|
|
114
|
+
function looksLikeDirectiveFragment(value) {
|
|
115
|
+
const folded = value
|
|
116
|
+
.normalize("NFKC")
|
|
117
|
+
.replace(INVISIBLE_SEPARATORS, "")
|
|
118
|
+
.replace(/[\u0430\u0435\u043E\u0440\u0441\u0445\u0455\u0456]/gu, (char) => CONFUSABLE_FOLD.get(char) ?? char);
|
|
119
|
+
// A dot joins a filename the way a hyphen joins an identifier, so the
|
|
120
|
+
// identifier pass folds it too: `override.ts` and `ignore.md` are files, not
|
|
121
|
+
// instructions. The separated pass splits on it for the same reason it splits
|
|
122
|
+
// on hyphens — `ignore.all.previous.instructions` is prose wearing punctuation.
|
|
123
|
+
const asIdentifier = folded.replace(/[-.]/gu, "_");
|
|
124
|
+
const asWords = folded.replace(/[-_.]+/gu, " ");
|
|
125
|
+
return IDENTIFIER_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(asIdentifier)) ||
|
|
126
|
+
SEPARATED_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(asWords));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A directive needs a QUANTIFIER, not just a noun.
|
|
130
|
+
*
|
|
131
|
+
* `cache write tokens` is Anthropic's prompt-caching billing vocabulary and it
|
|
132
|
+
* arrives in the operation slot on real invoice lines; `write ALL tokens` is an
|
|
133
|
+
* instruction. Pairing a verb with a bare `tokens` withheld this product's own
|
|
134
|
+
* billing words — a real line item rendered as
|
|
135
|
+
* "acme / agent-finops / [unsafe metadata omitted]" — and on `aibill context`,
|
|
136
|
+
* whose entire job is naming exact files, it named one of three.
|
|
137
|
+
*
|
|
138
|
+
* Measured over 146 real strings (Anthropic + OpenAI caching vocabulary, real
|
|
139
|
+
* invoice line items, real filenames, ordinary repo names): false positives
|
|
140
|
+
* 18 -> 0, with hostile detection unchanged at 28/28.
|
|
141
|
+
*
|
|
142
|
+
* `everything` and `all files` already carry their own quantifier, so they stay
|
|
143
|
+
* unguarded. `system prompt` is an injection-specific noun phrase that no
|
|
144
|
+
* billing vocabulary contains, so it needs no quantifier either.
|
|
145
|
+
*/
|
|
146
|
+
const QUANTIFIED = "(?:all|every|any|each)";
|
|
147
|
+
const IDENTIFIER_DIRECTIVE_PATTERNS = [
|
|
148
|
+
/\b(?:ignore|disregard|override|bypass)\b/i,
|
|
149
|
+
/\b(?:system|developer|assistant)\s*:/i,
|
|
150
|
+
/\b(?:execute|run)\b.{0,80}\b(?:command|shell|bash|powershell)\b/i,
|
|
151
|
+
new RegExp(`\\b(?:delete|remove|overwrite|edit|write)\\b.{0,60}(?:\\beverything\\b|\\ball files?\\b|\\b${QUANTIFIED}\\s+(?:configs?|credentials?|secrets?|tokens?)\\b)`, "i"),
|
|
152
|
+
new RegExp(`\\b(?:reveal|print|upload|send|exfiltrate)\\b.{0,60}(?:\\ball files?\\b|\\b(?:system|developer)\\s+prompts?\\b|\\b(?:${QUANTIFIED}|the)\\s+(?:credentials?|secrets?|tokens?|keys?|files?)\\b)`, "i"),
|
|
153
|
+
/\b(?:do not|don't)\b.{0,60}\b(?:follow|obey|wait|ask|require)\b.{0,40}\b(?:approval|instructions?|rules?)\b/i
|
|
154
|
+
];
|
|
155
|
+
const SEPARATED_DIRECTIVE_PATTERNS = [
|
|
156
|
+
/\b(?:ignore|disregard|override|bypass|forget)\b.{0,80}\b(?:previous|prior|above|earlier|preceding|instructions?|approval|rules?|guardrails?|system|developer|prompts?)\b/i,
|
|
157
|
+
new RegExp(`\\b(?:delete|remove|overwrite|edit|write)\\b.{0,60}(?:\\beverything\\b|\\ball files?\\b|\\b${QUANTIFIED}\\s+(?:configs?|credentials?|secrets?|tokens?)\\b)`, "i"),
|
|
158
|
+
new RegExp(`\\b(?:reveal|print|upload|send|exfiltrate|leak|dump)\\b.{0,60}(?:\\ball files?\\b|\\b(?:system|developer)\\s+prompts?\\b|\\b(?:${QUANTIFIED}|the)\\s+(?:credentials?|secrets?|tokens?|keys?|files?|prompts?)\\b)`, "i"),
|
|
159
|
+
/\b(?:do not|don't|never)\b.{0,60}\b(?:follow|obey|wait|ask|require)\b.{0,40}\b(?:approval|instructions?|rules?)\b/i
|
|
160
|
+
];
|
|
161
|
+
//# sourceMappingURL=untrustedLabel.js.map
|