@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.
@@ -1,3 +1,4 @@
1
+ import { safeUntrustedLabel, WITHHELD_FILE_LABEL, WITHHELD_PROJECT_LABEL } from "./untrustedLabel.js";
1
2
  import { loadAgentInventory } from "./agentInventory.js";
2
3
  import { computeDeadContext } from "./deadContext.js";
3
4
  import { localAgentFormatSupports } from "./localAgentFormats/registry.js";
@@ -74,7 +75,9 @@ export function buildContextHealth(input = {}) {
74
75
  if ((contextChurn.repeatedReadEvents ?? 0) > 0) {
75
76
  const files = contextChurn.repeatedFiles
76
77
  .slice(0, 3)
77
- .map((file) => `${file.file} ×${file.readCount}`)
78
+ // Basenames, but a basename is still a name the user did not author,
79
+ // and this sentence is returned verbatim by the MCP context-health tool.
80
+ .map((file) => `${safeUntrustedLabel(file.file, WITHHELD_FILE_LABEL)} ×${file.readCount}`)
78
81
  .join(", ");
79
82
  evidence.push({
80
83
  kind: "context_churn",
@@ -231,6 +234,9 @@ function contextDecision(input) {
231
234
  const MIN_EXACT_COMPARISONS = 2;
232
235
  const MIN_SESSION_TYPE_COMPARISONS = 2;
233
236
  const MAX_DISPLAY_RATIO = 20;
237
+ function safeOptionalProject(value) {
238
+ return value === undefined ? undefined : safeUntrustedLabel(value, WITHHELD_PROJECT_LABEL);
239
+ }
234
240
  function buildCurrentSession(sessions, now, activeWithinMinutes) {
235
241
  const latest = latestContextSession(sessions);
236
242
  if (!latest)
@@ -273,7 +279,9 @@ function buildCurrentSession(sessions, now, activeWithinMinutes) {
273
279
  return {
274
280
  status: ageMs <= activeWithinMinutes * 60_000 ? "active" : "recent",
275
281
  agent: latest.agent,
276
- project: latest.project,
282
+ project: latest.project === undefined
283
+ ? undefined
284
+ : safeUntrustedLabel(latest.project, WITHHELD_PROJECT_LABEL),
277
285
  totalTokens: latest.totalTokens,
278
286
  contextTokens: latest.contextTokens,
279
287
  usageSource: latest.usageSource,
@@ -308,7 +316,7 @@ function contextSessions(calls) {
308
316
  return {
309
317
  key,
310
318
  agent: latest.agent,
311
- project: latest.project ?? ordered[0]?.project,
319
+ project: safeOptionalProject(latest.project ?? ordered[0]?.project),
312
320
  lastActivityAt: latest.timestamp,
313
321
  totalTokens: turnUsage?.totalTokens ?? 0,
314
322
  contextTokens: turnUsage?.contextTokens ?? 0,
@@ -350,8 +358,11 @@ function buildContextChurn(latest, invocations) {
350
358
  signal.sessionId &&
351
359
  latest.key === `${signal.agent}:${signal.sessionId}`))
352
360
  : undefined;
361
+ // Neutralized at the source in toolInvocations.ts; re-applied here because a
362
+ // signal can also arrive from a cache written by an earlier version, and
363
+ // because this array is handed to an agent verbatim by the MCP tool.
353
364
  const repeatedFiles = (currentSignal?.repeatedFileReads ?? [])
354
- .map((file) => ({ file: file.name, readCount: file.count }));
365
+ .map((file) => ({ file: safeUntrustedLabel(file.name, WITHHELD_FILE_LABEL), readCount: file.count }));
355
366
  return {
356
367
  currentSessionEvidence: !latest
357
368
  ? "no_current_session"
package/dist/cutList.d.ts CHANGED
@@ -40,7 +40,30 @@ export type CutAction = {
40
40
  * by two actions (see {@link buildRecommendedPlan}).
41
41
  */
42
42
  recordIds: string[];
43
+ /**
44
+ * Median DAY's summed input+cache tokens for this candidate, computed over
45
+ * calendar days (not over records — a project running two models emits two
46
+ * records per day, and calling that "per day" would halve the number).
47
+ *
48
+ * Exists so a grouped render can tell two members apart by the quantity that
49
+ * explains their dollars instead of by the rounded dollar alone. Optional and
50
+ * absent on candidates with no day-level evidence; every renderer MUST drop
51
+ * the figure rather than print a placeholder.
52
+ */
53
+ medianDailyInputTokens?: number;
43
54
  };
55
+ /**
56
+ * Compact token count for prose: 2,140,000 -> "2.1M", 8,300 -> "8.3K".
57
+ * Lives in core because the cut-list guidance strings are built here, and is
58
+ * re-used by the renderers so one candidate's token magnitude reads the same
59
+ * on every surface.
60
+ *
61
+ * The ladder runs to T. It used to stop at B, so a fleet-scale window printed
62
+ * "4212.7B" — a number the reader has to count digits on to place, from a
63
+ * product whose whole claim is arithmetic you can read at a glance. Past T the
64
+ * mantissa gets thousands separators rather than a fourteenth silent digit.
65
+ */
66
+ export declare function formatTokenCount(tokens: number): string;
44
67
  /**
45
68
  * A non-overlapping "recommended plan" plus the leftover overlapping
46
69
  * opportunities. The recommended-plan total is the only savings number safe to
package/dist/cutList.js CHANGED
@@ -1,5 +1,37 @@
1
1
  import { hasCallLevelProvenance, hasModeledWorkloadEvidence, hasPricedEvidence } from "./schema.js";
2
2
  import { localAgentFormatSupports } from "./localAgentFormats/registry.js";
3
+ import { safeUntrustedLabel, WITHHELD_AGENT_LABEL, WITHHELD_MODEL_LABEL, WITHHELD_OPERATION_LABEL, WITHHELD_PROJECT_LABEL } from "./untrustedLabel.js";
4
+ /**
5
+ * Compact token count for prose: 2,140,000 -> "2.1M", 8,300 -> "8.3K".
6
+ * Lives in core because the cut-list guidance strings are built here, and is
7
+ * re-used by the renderers so one candidate's token magnitude reads the same
8
+ * on every surface.
9
+ *
10
+ * The ladder runs to T. It used to stop at B, so a fleet-scale window printed
11
+ * "4212.7B" — a number the reader has to count digits on to place, from a
12
+ * product whose whole claim is arithmetic you can read at a glance. Past T the
13
+ * mantissa gets thousands separators rather than a fourteenth silent digit.
14
+ */
15
+ export function formatTokenCount(tokens) {
16
+ if (!Number.isFinite(tokens))
17
+ return "0";
18
+ const value = Math.max(0, tokens);
19
+ if (value >= 1_000_000_000_000)
20
+ return `${formatMantissa(value / 1_000_000_000_000)}T`;
21
+ if (value >= 1_000_000_000)
22
+ return `${(value / 1_000_000_000).toFixed(1)}B`;
23
+ if (value >= 1_000_000)
24
+ return `${(value / 1_000_000).toFixed(1)}M`;
25
+ if (value >= 1_000)
26
+ return `${(value / 1_000).toFixed(1)}K`;
27
+ return String(Math.round(value));
28
+ }
29
+ /** Top of the ladder: keep one decimal until the integer part needs commas. */
30
+ function formatMantissa(value) {
31
+ return value >= 1_000
32
+ ? Math.round(value).toLocaleString("en-US")
33
+ : value.toFixed(1);
34
+ }
3
35
  /**
4
36
  * Select a non-overlapping subset of cut actions, highest-savings first. An
5
37
  * action is added only if none of its records were already claimed by a
@@ -126,10 +158,18 @@ function modelDowngradeActions(records) {
126
158
  const affectedSpendUsd = roundMoney(sumRecords(groupRecords));
127
159
  const windowSavings = affectedSpendUsd * (1 - rule.costRetained);
128
160
  const monthlySavings = roundMoney(toMonthly(windowSavings, window));
161
+ // Both untrusted. The model gate is anchored but open-ended
162
+ // (`^claude-opus-4(?:[.-].*)?$` accepts anything after the dash), and
163
+ // `downgradeSafeOperation` is a SUBSTRING allowlist — "summary" anywhere
164
+ // in the label passes the whole label through. Neutralize before either
165
+ // reaches a title or an instruction. IDs keep the raw values: an id is
166
+ // identity, not display, and it is rendered through a blanking guard.
167
+ const modelLabel = safeUntrustedLabel(model, WITHHELD_MODEL_LABEL);
168
+ const operationLabel = safeUntrustedLabel(operation, WITHHELD_OPERATION_LABEL);
129
169
  actions.push({
130
170
  id: `downgrade-${slug(model)}-${slug(operation)}`,
131
- title: `Move ${model} ${operation} calls to ${target}`,
132
- action: `Route ${groupRecords.length} ${operation} call${groupRecords.length === 1 ? "" : "s"} from ${model} to ${target} (keep ${model} only when output is rejected).`,
171
+ title: `Move ${modelLabel} ${operationLabel} calls to ${target}`,
172
+ action: `Route ${groupRecords.length} ${operationLabel} call${groupRecords.length === 1 ? "" : "s"} from ${modelLabel} to ${target} (keep ${modelLabel} only when output is rejected).`,
133
173
  estimatedMonthlySavingsUsd: monthlySavings,
134
174
  affectedSpendUsd,
135
175
  recordCount: groupRecords.length,
@@ -155,6 +195,30 @@ function contextTrimActions(records) {
155
195
  : `connected::${operation}`;
156
196
  byOperation.set(key, [...(byOperation.get(key) ?? []), record]);
157
197
  }
198
+ // ONE denominator, and it is the set the sentence is already talking about:
199
+ // this agent's OWN flagged projects. It is both the share's denominator and
200
+ // the rank clause's population, so the percentage, the rank, and the entry's
201
+ // own dollars two lines below all reconcile, and the members of one fan-out
202
+ // sum to 100%.
203
+ //
204
+ // 0.9.7 shipped three denominators in one sentence — a machine-wide local
205
+ // total for the share, this agent's flagged projects for the rank, and the
206
+ // entry's own total underneath. Every figure was individually true, which is
207
+ // exactly why the sentence read as an arithmetic error. Never reintroduce a
208
+ // denominator the sentence does not name.
209
+ const flaggedSpendByAgent = new Map();
210
+ for (const [key, groupRecords] of byOperation) {
211
+ const [scope, agentId] = key.split("::");
212
+ if (scope !== "local" || !agentId)
213
+ continue;
214
+ // roundMoney, matching `affectedSpendUsd` exactly: comparing a rounded
215
+ // group against unrounded peers made a candidate outrank ITSELF and
216
+ // report "rank 2 of 8" for the largest project on the list.
217
+ flaggedSpendByAgent.set(agentId, [
218
+ ...(flaggedSpendByAgent.get(agentId) ?? []),
219
+ roundMoney(sumRecords(groupRecords))
220
+ ]);
221
+ }
158
222
  const actions = [];
159
223
  for (const [key, groupRecords] of byOperation) {
160
224
  const affectedSpendUsd = roundMoney(sumRecords(groupRecords));
@@ -164,7 +228,26 @@ function contextTrimActions(records) {
164
228
  : key.replace(/^connected::/, "");
165
229
  const count = groupRecords.length;
166
230
  const agent = groupRecords[0]?.agentId ?? "coding-agent";
231
+ // `agent` stays raw as the map key and the id slug; only the DISPLAY
232
+ // form is neutralized. Bounded to the adapter enum for local rows in
233
+ // practice, but nothing in the type system enforces that.
234
+ const agentLabel = safeUntrustedLabel(agent, WITHHELD_AGENT_LABEL);
167
235
  const project = groupRecords[0]?.projectId;
236
+ const evidence = sessionAggregates ? dailyContextEvidence(groupRecords) : null;
237
+ const flaggedSpend = flaggedSpendByAgent.get(agent) ?? [];
238
+ // The project label is UNTRUSTED — it is a directory name off the user's
239
+ // disk. Neutralize it ONCE, here, so the title, the guidance, and every
240
+ // surface that re-derives a label from the title all carry the same
241
+ // already-safe text and cannot disagree about it.
242
+ const projectLabel = project
243
+ ? safeUntrustedLabel(project, WITHHELD_PROJECT_LABEL)
244
+ : "Unattributed";
245
+ // The connected path's operation label is a raw provider/adapter string
246
+ // with NO allowlist in front of it, and it lands in both the title and the
247
+ // instruction. It has to be neutralized here for the same reason the
248
+ // project label is: the report layer's prose sanitizer never blanks, and
249
+ // it is only safe to never blank because core neutralized first.
250
+ const operationLabel = safeUntrustedLabel(operation, WITHHELD_OPERATION_LABEL);
168
251
  // Large token volume proves exposure, not that context is removable or
169
252
  // what quality/cost delta a change would produce. Context remains an
170
253
  // inspect-only action until matched before/after evidence exists.
@@ -174,11 +257,18 @@ function contextTrimActions(records) {
174
257
  ? `inspect-context-${slug(agent)}-${slug(project ?? "unattributed")}`
175
258
  : `inspect-context-${slug(operation)}`,
176
259
  title: sessionAggregates
177
- ? `Investigate cumulative context in ${agent}${project ? ` · ${project}` : " · Unattributed"}`
178
- : `Inspect oversized context on ${operation}`,
260
+ ? `Investigate cumulative context in ${agentLabel} · ${projectLabel}`
261
+ : `Inspect oversized context on ${operationLabel}`,
179
262
  action: sessionAggregates
180
- ? `${count} day + agent + model + project aggregate${count === 1 ? "" : "s"} contained at least 100k summed input/cache tokens. Inspect per-session context, compactions, repeated reads, and measured instruction-file size before proposing one reversible change.`
181
- : `${count} call-level ${operation} record${count === 1 ? "" : "s"} exceeded 100k input tokens. Inspect retrieved chunks and prompt history, then run a matched before/after before claiming savings.`,
263
+ ? localContextTrimGuidance({
264
+ count,
265
+ agent: agentLabel,
266
+ projectLabel,
267
+ evidence,
268
+ groupSpendUsd: affectedSpendUsd,
269
+ flaggedSpend
270
+ })
271
+ : `${count} call-level ${operationLabel} record${count === 1 ? "" : "s"} exceeded 100k input tokens. Inspect retrieved chunks and prompt history, then run a matched before/after before claiming savings.`,
182
272
  estimatedMonthlySavingsUsd: monthlySavings,
183
273
  affectedSpendUsd,
184
274
  recordCount: count,
@@ -186,11 +276,172 @@ function contextTrimActions(records) {
186
276
  impactBasis: "observed_value_no_counterfactual",
187
277
  recordIds: groupRecords.map((record) => record.id),
188
278
  confidence: combinedConfidence(groupRecords.map((record) => record.costConfidence)),
189
- kind: "context_trim"
279
+ kind: "context_trim",
280
+ ...(evidence ? { medianDailyInputTokens: evidence.medianDailyInputTokens } : {})
190
281
  });
191
282
  }
192
283
  return actions;
193
284
  }
285
+ function dailyContextEvidence(records) {
286
+ if (records.length === 0)
287
+ return null;
288
+ const byDay = new Map();
289
+ for (const record of records) {
290
+ const day = record.timestamp.slice(0, 10);
291
+ const current = byDay.get(day) ?? { input: 0, output: 0 };
292
+ current.input += record.inputTokens;
293
+ current.output += record.outputTokens;
294
+ byDay.set(day, current);
295
+ }
296
+ const days = [...byDay.entries()].sort((left, right) => left[0].localeCompare(right[0]));
297
+ const first = days[0];
298
+ if (!first)
299
+ return null;
300
+ // ONE REAL DAY, not two independent medians. The sentence says "median day
301
+ // carried X input+cache tokens against Y output"; if X and Y came from
302
+ // different calendar days that sentence would describe a day that never
303
+ // happened. So: order the days by input+cache, take the middle OBSERVED day
304
+ // (the lower of the two middles on an even sample, never their average), and
305
+ // read every median figure off that one day.
306
+ const byInput = [...days].sort((left, right) => left[1].input - right[1].input || left[0].localeCompare(right[0]));
307
+ const medianDay = byInput[Math.floor((byInput.length - 1) / 2)];
308
+ const medianDailyInputTokens = medianDay[1].input;
309
+ const medianDailyOutputTokens = medianDay[1].output;
310
+ const peak = days.reduce((best, entry) => (entry[1].input > best[1].input ? entry : best), first);
311
+ const spendByModel = new Map();
312
+ for (const record of records) {
313
+ spendByModel.set(record.model, (spendByModel.get(record.model) ?? 0) + (record.amountUsd ?? 0));
314
+ }
315
+ return {
316
+ activeDays: days.length,
317
+ medianDailyInputTokens,
318
+ medianDailyOutputTokens,
319
+ peakDay: peak[0],
320
+ peakDayInputTokens: peak[1].input,
321
+ peakOverMedian: medianDailyInputTokens > 0 ? peak[1].input / medianDailyInputTokens : 0,
322
+ inputPerOutput: medianDailyOutputTokens > 0
323
+ ? medianDailyInputTokens / medianDailyOutputTokens
324
+ : null,
325
+ models: [...spendByModel.entries()]
326
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
327
+ .map(([model]) => model)
328
+ };
329
+ }
330
+ /**
331
+ * The guidance a local context candidate carries.
332
+ *
333
+ * Two hard constraints shape it:
334
+ *
335
+ * 1. TRUTH. Every clause states something OBSERVED — a median, a date, a
336
+ * ratio, a share of a single-basis total. Nothing predicts a reduction,
337
+ * nothing prices a change, and no clause is emitted when the evidence
338
+ * behind it is absent. Sharper means more specific about what was seen,
339
+ * never more confident about what would happen.
340
+ *
341
+ * 2. GEOMETRY. The grouped terminal/report render quotes this string from its
342
+ * LARGEST member and drops everything before the first ". " (see
343
+ * `groupedCutActionLines` / `rankCutCandidates` in @agent-finops/report).
344
+ * So sentence 1 carries only per-member counts, and sentence 2 onward
345
+ * NAMES the project it describes — otherwise one project's median would be
346
+ * read as the whole group's. For the same reason sentence 1 interpolates
347
+ * no free text: a label containing ". " would truncate the string at the
348
+ * wrong point.
349
+ */
350
+ function localContextTrimGuidance(input) {
351
+ const { count, evidence } = input;
352
+ // Idempotent: `contextTrimActions` already neutralized the label for the
353
+ // title, and re-running the check on an already-neutral label is a no-op.
354
+ // Repeating it here keeps the guarantee attached to the function that does
355
+ // the interpolating, not to one of its callers.
356
+ const projectLabel = safeUntrustedLabel(input.projectLabel, WITHHELD_PROJECT_LABEL);
357
+ const plural = count === 1 ? "" : "s";
358
+ if (!evidence) {
359
+ // No day-level evidence to quote. Stay honest and short rather than
360
+ // reciting a checklist the product cannot perform.
361
+ return `${count} day + agent + model + project aggregate${plural} each carried at least 100k summed input/cache tokens. ` +
362
+ `Inspect the heaviest sessions in ${projectLabel} before proposing one reversible change.`;
363
+ }
364
+ const dayPlural = evidence.activeDays === 1 ? "" : "s";
365
+ const sentences = [
366
+ `${count} day + agent + model + project aggregate${plural} over ${evidence.activeDays} active day${dayPlural}.`
367
+ ];
368
+ const ratio = evidence.inputPerOutput === null ? null : formatRatio(evidence.inputPerOutput);
369
+ sentences.push(evidence.medianDailyOutputTokens > 0
370
+ ? `${projectLabel} — median day carried ${formatTokenCount(evidence.medianDailyInputTokens)} input+cache tokens ` +
371
+ `against ${formatTokenCount(evidence.medianDailyOutputTokens)} output${ratio ? ` (${ratio}:1)` : ""}.`
372
+ : `${projectLabel} — median day carried ${formatTokenCount(evidence.medianDailyInputTokens)} input+cache tokens ` +
373
+ "with no output tokens recorded that day.");
374
+ // Only when there is a real lead. A uniform project gets no false one.
375
+ if (evidence.peakOverMedian >= 2) {
376
+ sentences.push(`Heaviest day ${evidence.peakDay} carried ${formatTokenCount(evidence.peakDayInputTokens)}, ` +
377
+ `${formatRatio(evidence.peakOverMedian)}× the median day; dates are each session's last activity.`);
378
+ }
379
+ const share = concentrationClause(input);
380
+ if (share)
381
+ sentences.push(share);
382
+ if (evidence.models.length > 1) {
383
+ // Model ids come off the same untrusted logs the project name does.
384
+ const shown = evidence.models
385
+ .slice(0, 3)
386
+ .map((model) => safeUntrustedLabel(model, WITHHELD_MODEL_LABEL));
387
+ const rest = evidence.models.length - shown.length;
388
+ sentences.push(`${evidence.models.length} models ran there: ${shown.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}.`);
389
+ }
390
+ sentences.push(evidence.peakOverMedian >= 2
391
+ ? `Inspect the sessions behind ${evidence.peakDay} before proposing one reversible change.`
392
+ : `Inspect the heaviest sessions in ${projectLabel} before proposing one reversible change.`);
393
+ return sentences.join(" ");
394
+ }
395
+ /**
396
+ * "…holds 76% of the flagged claude-code value observed in this window (rank 1
397
+ * of 8 flagged projects)." An accounting fact about ONE denominator — not a
398
+ * savings claim, not a projection.
399
+ *
400
+ * The denominator is this agent's own flagged total, which is also the rank
401
+ * clause's population and the sum of the per-project dollars the same readout
402
+ * prints two lines below. So the percentage, the rank, and the entry's own
403
+ * total reconcile, and the members of one fan-out sum to 100%.
404
+ *
405
+ * 0.9.7 divided by a machine-wide local total instead: `--full` printed a
406
+ * by-project table saying 83%, this sentence said 46%, and the entry's own
407
+ * dollars implied 76% — three true numbers that read as an arithmetic error.
408
+ * No clamp is needed now and none belongs here: the numerator is one member of
409
+ * the set in the denominator, so a ratio above 1 would be a real bug worth
410
+ * seeing rather than a cosmetic one worth hiding.
411
+ */
412
+ function concentrationClause(input) {
413
+ const { agent, groupSpendUsd, flaggedSpend } = input;
414
+ const flaggedTotalUsd = flaggedSpend.reduce((total, spend) => total + spend, 0);
415
+ if (!(flaggedTotalUsd > 0) || !(groupSpendUsd > 0))
416
+ return null;
417
+ const ratio = groupSpendUsd / flaggedTotalUsd;
418
+ if (!Number.isFinite(ratio) || ratio <= 0)
419
+ return null;
420
+ const percent = Math.round(ratio * 100);
421
+ const rank = flaggedSpend.filter((spend) => spend > groupSpendUsd).length + 1;
422
+ const total = flaggedSpend.length;
423
+ // A ceiling, mirroring the "under 1%" floor. The ordinary solo-dev shape is
424
+ // one main repo and two small side projects: at 99.5%–99.9% the rounded
425
+ // figure prints "100%" on the same screen as an across-line that lists two
426
+ // more flagged projects WITH DOLLARS — which reads as exactly the
427
+ // arithmetic error this sentence's denominator was fixed to kill. "over 99%"
428
+ // is the same fact without the contradiction. A lone flagged project keeps
429
+ // the literal 100%, because there is nothing beside it to contradict.
430
+ const share = percent < 1
431
+ ? "under 1%"
432
+ : percent >= 100 && total > 1 ? "over 99%" : `${percent}%`;
433
+ const rankClause = total > 1 ? ` (rank ${rank} of ${total} flagged projects)` : "";
434
+ return `That project holds ${share} of the flagged ${agent} value observed in this window${rankClause}.`;
435
+ }
436
+ /**
437
+ * "519", "4.4" — integers once the ratio is big enough that a decimal is noise.
438
+ * Shared by the input:output ratio and the "N× the median day" multiple so the
439
+ * two never disagree about precision. Separated above 999, because a bare
440
+ * "1904762:1" is a digit-counting exercise, not a figure.
441
+ */
442
+ function formatRatio(value) {
443
+ return value >= 10 ? Math.round(value).toLocaleString("en-US") : value.toFixed(1);
444
+ }
194
445
  function cacheActions(records) {
195
446
  const window = windowDays(records);
196
447
  const counts = new Map();
@@ -226,10 +477,13 @@ function cacheActions(records) {
226
477
  const affectedSpendUsd = roundMoney(sumRecords(groupRecords));
227
478
  const windowSavings = sumRecords(avoidableRecords);
228
479
  const monthlySavings = roundMoney(toMonthly(windowSavings, window));
480
+ // No allowlist stands in front of this one at all — any operation label
481
+ // with a stable fingerprint reaches the title and the instruction.
482
+ const operationLabel = safeUntrustedLabel(operation, WITHHELD_OPERATION_LABEL);
229
483
  actions.push({
230
484
  id: `cache-${slug(operation)}-${stableSuffix(key)}`,
231
- title: `Cache repeated ${operation} calls`,
232
- action: `Keep the earliest ${operation} call as the canonical miss and cache the ${avoidableRecords.length} subsequent call${avoidableRecords.length === 1 ? "" : "s"} with the same adapter-provided input fingerprint.`,
485
+ title: `Cache repeated ${operationLabel} calls`,
486
+ action: `Keep the earliest ${operationLabel} call as the canonical miss and cache the ${avoidableRecords.length} subsequent call${avoidableRecords.length === 1 ? "" : "s"} with the same adapter-provided input fingerprint.`,
233
487
  estimatedMonthlySavingsUsd: monthlySavings,
234
488
  affectedSpendUsd,
235
489
  recordCount: groupRecords.length,
@@ -274,10 +528,13 @@ function batchActions(records) {
274
528
  const retainedCost = batchCostRetainedByProvider[groupRecords[0].source.provider];
275
529
  const windowSavings = affectedSpendUsd * (1 - retainedCost);
276
530
  const monthlySavings = roundMoney(toMonthly(windowSavings, window));
531
+ // `batchSafeOperation` is a SUBSTRING allowlist: "summar" anywhere in the
532
+ // label admits the whole label, injected prose included.
533
+ const operationLabel = safeUntrustedLabel(operation, WITHHELD_OPERATION_LABEL);
277
534
  actions.push({
278
535
  id: `batch-${slug(operation)}-${stableSuffix(key)}`,
279
- title: `Move ${operation} calls to the Batch API`,
280
- action: `Submit ${groupRecords.length} ${operation} call${groupRecords.length === 1 ? "" : "s"} through the provider's Batch API (flat 50% off; results within 24h, fine for offline work).`,
536
+ title: `Move ${operationLabel} calls to the Batch API`,
537
+ action: `Submit ${groupRecords.length} ${operationLabel} call${groupRecords.length === 1 ? "" : "s"} through the provider's Batch API (flat 50% off; results within 24h, fine for offline work).`,
281
538
  estimatedMonthlySavingsUsd: monthlySavings,
282
539
  affectedSpendUsd,
283
540
  recordCount: groupRecords.length,
@@ -1,4 +1,5 @@
1
1
  import { loadAgentInventory } from "./agentInventory.js";
2
+ import { safeUntrustedLabel, WITHHELD_ENTITY_LABEL, WITHHELD_FILE_LABEL } from "./untrustedLabel.js";
2
3
  import { findPricingRule } from "./modelPricing.js";
3
4
  import { loadToolInvocations } from "./toolInvocations.js";
4
5
  /**
@@ -97,15 +98,21 @@ export function computeDeadContext(items, invocations, config) {
97
98
  }
98
99
  dead.push({
99
100
  kind: item.kind,
100
- name: item.name,
101
+ // Skill, subagent, slash-command, hook and MCP SERVER names, read off
102
+ // disk. They are printed by name on the readout and in the artifact.
103
+ name: safeUntrustedLabel(item.name, WITHHELD_ENTITY_LABEL),
101
104
  scope: item.scope,
102
105
  activation: item.activation,
106
+ // The STRUCTURED siblings travel with the name to every surface the name
107
+ // does, including the Apply artifact. Neutralizing the name and leaving
108
+ // the path beside it raw is the same inversion Blocker A was.
109
+ // `host` is the InventoryHost enum, not free text — bounded by the type.
103
110
  host: item.host,
104
111
  invocationTracking: item.invocationTracking,
105
112
  alwaysLoadedTokens: item.alwaysLoadedTokens,
106
113
  weightConfidence: item.weightConfidence,
107
- path: item.path,
108
- ownerDirs: item.ownerDirs
114
+ path: item.path === undefined ? undefined : safeUntrustedLabel(item.path, WITHHELD_FILE_LABEL),
115
+ ownerDirs: item.ownerDirs?.map((dir) => safeUntrustedLabel(dir, WITHHELD_FILE_LABEL))
109
116
  });
110
117
  }
111
118
  dead.sort((a, b) => b.alwaysLoadedTokens - a.alwaysLoadedTokens);
package/dist/insights.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { hasCallLevelProvenance, hasPricedEvidence, spendComparisonKey, spendInsightSchema } from "./schema.js";
2
+ import { safeUntrustedLabel, safeUntrustedLabels, WITHHELD_ENTITY_LABEL, WITHHELD_OPERATION_LABEL } from "./untrustedLabel.js";
2
3
  const confidenceRank = {
3
4
  verified: 0,
4
5
  estimated: 1,
@@ -32,7 +33,10 @@ function spikeInsights(records, summary) {
32
33
  const topProject = topBreakdown(currentRecords, (record) => record.projectId);
33
34
  const topModels = breakdown(currentRecords, (record) => record.model).slice(0, 2).map((entry) => entry.key);
34
35
  const deltaUsd = roundMoney(anomaly.currentAmountUsd - anomaly.previousAmountUsd);
35
- const likelyOwner = topAgent?.key ?? topProject?.key ?? topClient?.key ?? "an unassigned owner";
36
+ // Ownership lead is an agent/project/client id off the records it lands
37
+ // mid-sentence in `summary`, so it is neutralized at the interpolation
38
+ // point like every other untrusted fragment.
39
+ const likelyOwner = safeEntity(topAgent?.key ?? topProject?.key ?? topClient?.key ?? "an unassigned owner");
36
40
  const cohortSuffix = stableSuffix(anomaly.comparisonKey ?? "legacy");
37
41
  const isProviderBilledCost = currentRecords.length > 0 && currentRecords.every((record) => record.usageGranularity === "billing_bucket" &&
38
42
  record.costConfidence === "verified");
@@ -50,14 +54,14 @@ function spikeInsights(records, summary) {
50
54
  { label: `Previous cohort ${isProviderBilledCost ? "spend" : "value"}`, value: formatUsd(anomaly.previousAmountUsd) },
51
55
  { label: `Current cohort ${isProviderBilledCost ? "spend" : "value"}`, value: formatUsd(anomaly.currentAmountUsd) },
52
56
  { label: `${evidenceLabel} increase`, value: formatUsd(deltaUsd), detail: `${formatMultiplier(anomaly.multiplier)} day-over-day multiplier` },
53
- topAgent ? { label: "Ownership lead", value: topAgent.key, detail: `${formatUsd(topAgent.amountUsd)} across ${topAgent.recordCount} cohort records` } : undefined,
54
- topClient ? { label: "Client concentration", value: topClient.key, detail: `${formatUsd(topClient.amountUsd)} on spike day` } : undefined,
55
- topModels.length > 0 ? { label: "Dominant models", value: topModels.join(", ") } : undefined
57
+ topAgent ? { label: "Ownership lead", value: safeEntity(topAgent.key), detail: `${formatUsd(topAgent.amountUsd)} across ${topAgent.recordCount} cohort records` } : undefined,
58
+ topClient ? { label: "Client concentration", value: safeEntity(topClient.key), detail: `${formatUsd(topClient.amountUsd)} on spike day` } : undefined,
59
+ topModels.length > 0 ? { label: "Dominant models", value: safeUntrustedLabels(topModels).join(", ") } : undefined
56
60
  ]),
57
- affectedClients: keysFrom(currentRecords, (record) => record.clientId),
58
- affectedProjects: keysFrom(currentRecords, (record) => record.projectId),
59
- affectedAgents: keysFrom(currentRecords, (record) => record.agentId),
60
- affectedModels: keysFrom(currentRecords, (record) => record.model),
61
+ affectedClients: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.clientId)),
62
+ affectedProjects: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.projectId)),
63
+ affectedAgents: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.agentId)),
64
+ affectedModels: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.model)),
61
65
  estimatedImpactUsd: deltaUsd,
62
66
  confidence: anomaly.confidence,
63
67
  recommendedAction: `Review and reconcile the provider-cohort records from ${anomaly.key}, confirm the accountable owner, and obtain run-level evidence before diagnosing behavior or changing a policy.`,
@@ -81,24 +85,27 @@ function agentCostDriverInsights(records, summary) {
81
85
  const topModel = topBreakdown(agentRecords, (record) => record.model);
82
86
  const hasRunLevelEvidence = agentRecords.length > 0 && agentRecords.every(hasCallLevelProvenance);
83
87
  return [{
84
- id: `agent-spend-concentration-${topAgent.key}`,
88
+ // The id is a STRUCTURED field beside the neutralized title, and it is
89
+ // rendered (`Canonical candidate ID: ...`). Slug the neutralized form, not
90
+ // the raw key.
91
+ id: `agent-spend-concentration-${slug(safeEntity(topAgent.key))}`,
85
92
  kind: "optimization_opportunity",
86
93
  severity: "medium",
87
- title: `${topAgent.key} spend concentration needs owner and budget review`,
88
- summary: `${topAgent.key} is attached to ${formatPercent(share)} of tracked spend. Concentration alone does not prove abnormal behavior or an avoidable dollar amount${hasRunLevelEvidence ? "." : "; the evidence is aggregate rather than run-level."}`,
94
+ title: `${safeEntity(topAgent.key)} spend concentration needs owner and budget review`,
95
+ summary: `${safeEntity(topAgent.key)} is attached to ${formatPercent(share)} of tracked spend. Concentration alone does not prove abnormal behavior or an avoidable dollar amount${hasRunLevelEvidence ? "." : "; the evidence is aggregate rather than run-level."}`,
89
96
  evidence: compactEvidence([
90
97
  { label: "Attributed spend", value: formatUsd(topAgent.amountUsd), detail: `${topAgent.recordCount} ${hasRunLevelEvidence ? "call-level" : "aggregate"} record${topAgent.recordCount === 1 ? "" : "s"}` },
91
98
  { label: "Share of tracked spend", value: formatPercent(share) },
92
- topModel ? { label: "Dominant model or billing label", value: topModel.key, detail: `${formatUsd(topModel.amountUsd)} in this concentration` } : undefined,
93
- topOperation ? { label: "Operation label", value: topOperation.key, detail: hasRunLevelEvidence ? "Call-level attribution" : "Not verified as one call or run" } : undefined
99
+ topModel ? { label: "Dominant model or billing label", value: safeEntity(topModel.key), detail: `${formatUsd(topModel.amountUsd)} in this concentration` } : undefined,
100
+ topOperation ? { label: "Operation label", value: safeEntity(topOperation.key), detail: hasRunLevelEvidence ? "Call-level attribution" : "Not verified as one call or run" } : undefined
94
101
  ]),
95
- affectedClients: keysFrom(agentRecords, (record) => record.clientId),
96
- affectedProjects: keysFrom(agentRecords, (record) => record.projectId),
97
- affectedAgents: [topAgent.key],
98
- affectedModels: keysFrom(agentRecords, (record) => record.model),
102
+ affectedClients: safeUntrustedLabels(keysFrom(agentRecords, (record) => record.clientId)),
103
+ affectedProjects: safeUntrustedLabels(keysFrom(agentRecords, (record) => record.projectId)),
104
+ affectedAgents: [safeEntity(topAgent.key)],
105
+ affectedModels: safeUntrustedLabels(keysFrom(agentRecords, (record) => record.model)),
99
106
  estimatedImpactUsd: 0,
100
107
  confidence: topAgent.confidence,
101
- recommendedAction: `Confirm who owns ${topAgent.key}, reconcile the spend to its approved budget, and collect behavioral evidence before setting a cap or savings target.`,
108
+ recommendedAction: `Confirm who owns ${safeEntity(topAgent.key)}, reconcile the spend to its approved budget, and collect behavioral evidence before setting a cap or savings target.`,
102
109
  verificationNeeded: "Confirm the budget owner and expected range; concentration alone is not behavioral evidence."
103
110
  }];
104
111
  }
@@ -113,7 +120,7 @@ function contextBloatInsights(records) {
113
120
  const scopedRecords = topOperation
114
121
  ? highInputRecords.filter((record) => record.operation === topOperation.key)
115
122
  : highInputRecords;
116
- const operationLabel = topOperation?.key ?? "large-context calls";
123
+ const operationLabel = safeUntrustedLabel(topOperation?.key ?? "large-context calls", WITHHELD_OPERATION_LABEL);
117
124
  const totalInputTokens = scopedRecords.reduce((total, record) => total + record.inputTokens, 0);
118
125
  const scopedSpend = roundMoney(sumRecords(scopedRecords));
119
126
  if (scopedSpend < 20) {
@@ -131,16 +138,27 @@ function contextBloatInsights(records) {
131
138
  { label: "Spend attached to large context", value: formatUsd(scopedSpend) },
132
139
  { label: "Dominant operation", value: operationLabel }
133
140
  ],
134
- affectedClients: keysFrom(scopedRecords, (record) => record.clientId),
135
- affectedProjects: keysFrom(scopedRecords, (record) => record.projectId),
136
- affectedAgents: keysFrom(scopedRecords, (record) => record.agentId),
137
- affectedModels: keysFrom(scopedRecords, (record) => record.model),
141
+ affectedClients: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.clientId)),
142
+ affectedProjects: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.projectId)),
143
+ affectedAgents: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.agentId)),
144
+ affectedModels: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.model)),
138
145
  estimatedImpactUsd: 0,
139
146
  confidence: combinedConfidence(scopedRecords.map((record) => record.costConfidence)),
140
147
  recommendedAction: `Inspect representative ${operationLabel} prompts locally and run a matched before/after with the same acceptance criteria before proposing one reversible context change.`,
141
148
  verificationNeeded: "Measure token and quality deltas on matched calls; no savings counterfactual is present yet."
142
149
  }];
143
150
  }
151
+ /**
152
+ * A breakdown key rendered for a HUMAN. The same slot holds a client, a
153
+ * project, an agent, a model or an operation depending on which grouping won,
154
+ * so it takes the dimension-neutral marker.
155
+ *
156
+ * Display only. The raw key is still what `records.filter(...)` matches on —
157
+ * rewriting a matching key would silently empty the cohort behind the finding.
158
+ */
159
+ function safeEntity(value) {
160
+ return safeUntrustedLabel(value, WITHHELD_ENTITY_LABEL);
161
+ }
144
162
  function topBreakdown(records, select) {
145
163
  return breakdown(records, select)[0];
146
164
  }
package/dist/planMath.js CHANGED
@@ -1,3 +1,15 @@
1
+ import { safeUntrustedLabel, WITHHELD_ENTITY_LABEL, WITHHELD_PLAN_LABEL } from "./untrustedLabel.js";
2
+ /**
3
+ * The plan label and the limit signal are read out of the agent's own local
4
+ * config files, so both are untrusted text that lands mid-sentence in a
5
+ * headline the readout, the report and `doctor` all print verbatim.
6
+ */
7
+ function safePlanLabel(value) {
8
+ return safeUntrustedLabel(value, WITHHELD_PLAN_LABEL);
9
+ }
10
+ function safeLimitSignal(value) {
11
+ return safeUntrustedLabel(value, WITHHELD_ENTITY_LABEL);
12
+ }
1
13
  export const subscriptionPlans = [
2
14
  { id: "claude-pro", provider: "anthropic", agent: "claude-code", name: "Claude Pro", monthlyUsd: 20, coversUpToUsd: 50 },
3
15
  { id: "claude-max-5x", provider: "anthropic", agent: "claude-code", name: "Claude Max 5x", monthlyUsd: 100, coversUpToUsd: 250 },
@@ -61,21 +73,21 @@ export function computePlanChecks(records, detectedPlans = []) {
61
73
  const nextTier = subscriptionPlans.find((plan) => plan.agent === agent && plan.coversUpToUsd > detectedKnown.coversUpToUsd);
62
74
  // A local limit signal upgrades "might hit limits" to hard evidence.
63
75
  const evidence = detected?.limitSignal
64
- ? `local metadata reports ${detected.limitSignal}`
76
+ ? `local metadata reports ${safeLimitSignal(detected.limitSignal)}`
65
77
  : `if the provider reports active rate limits`;
66
78
  upgradeHint = nextTier
67
79
  ? `API-equivalent projection exceeds the rough ${detectedKnown.name} comparison threshold (~$${detectedKnown.coversUpToUsd}/mo); ${evidence}. ${nextTier.name} ($${nextTier.monthlyUsd}/mo) is the next listed tier, but verify account limits before changing plans; trimming context (below) may buy headroom.`
68
80
  : `API-equivalent projection exceeds the rough ${detectedKnown.name} comparison threshold (~$${detectedKnown.coversUpToUsd}/mo); verify account limits before changing plans. Trimming context (below) may buy headroom.`;
69
81
  }
70
82
  else if (detected?.limitSignal) {
71
- upgradeHint = `local metadata reports ${detected.limitSignal}; verify the live provider window. Trimming context (below) may buy headroom.`;
83
+ upgradeHint = `local metadata reports ${safeLimitSignal(detected.limitSignal)}; verify the live provider window. Trimming context (below) may buy headroom.`;
72
84
  }
73
85
  }
74
86
  else if (detected) {
75
87
  // Detected a plan we can't price (e.g. an unrecognized tier): state the
76
88
  // fact, then fall back to suggestion math without pretending certainty.
77
89
  headline =
78
- `${agent}: ~${formatUsd(monthly)}/mo at API rates (${basis}) — compared with ${detected.planLabel} ` +
90
+ `${agent}: ~${formatUsd(monthly)}/mo at API rates (${basis}) — compared with ${safePlanLabel(detected.planLabel)} ` +
79
91
  `(label detected locally; price not in our table)` +
80
92
  (suggested ? `; reference listed plan: ${suggested.name} ($${suggested.monthlyUsd}/mo).` : `.`);
81
93
  }
@@ -100,7 +112,16 @@ export function computePlanChecks(records, detectedPlans = []) {
100
112
  suggestedPlan: detectedKnown ?? suggested,
101
113
  monthlySavingsVsApiUsd: effectiveSavings,
102
114
  valueMultiple,
103
- detectedPlan: detected,
115
+ // The STRUCTURED sibling of the headline. Neutralizing the sentence and
116
+ // shipping the raw label beside it in the same object is the inversion
117
+ // that let a hostile name reach an agent while the human saw a redaction.
118
+ detectedPlan: detected === undefined ? undefined : {
119
+ ...detected,
120
+ planLabel: safePlanLabel(detected.planLabel),
121
+ ...(detected.limitSignal === undefined
122
+ ? {}
123
+ : { limitSignal: safeLimitSignal(detected.limitSignal) })
124
+ },
104
125
  upgradeHint,
105
126
  headline
106
127
  });