From 259346306aea7eb59330007552228b070a7288e9 Mon Sep 17 00:00:00 2001 From: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com> Date: Sat, 31 Oct 2026 10:53:28 -0500 Subject: [PATCH] fix(usage): enforce safe-integer token bounds in Python and reject empty/boolean cost amounts in TypeScript (#104) - Enforce 0 <= value <= 2**53 - 1 on token fields and rollup sums in Python usage.py, and validate currency/pricing_version string types in CostObservation. - Reject non-number/non-string or empty/whitespace amount values in TypeScript CostObservation.asEventValue. Closes #104 Signed-off-by: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com> --- packages/typescript/src/usage.ts | 3 ++ .../typescript/test/cost-boundaries.test.ts | 15 ++++++++++ src/agentrust_telemetry/usage.py | 29 +++++++++++++++---- tests/test_usage.py | 17 +++++++++++ 4 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 packages/typescript/test/cost-boundaries.test.ts diff --git a/packages/typescript/src/usage.ts b/packages/typescript/src/usage.ts index f1d3473..7d97902 100644 --- a/packages/typescript/src/usage.ts +++ b/packages/typescript/src/usage.ts @@ -11,6 +11,9 @@ export type CostSource = "provider" | "caller" | "price_resolver" | "estimate"; export class CostObservation { constructor(readonly amount: number | string, readonly currency: string, readonly source: CostSource, readonly pricingVersion?: string) {} asEventValue(): Record { + if (typeof this.amount !== "number" && (typeof this.amount !== "string" || !this.amount.trim())) { + throw new Error("cost amount must be a finite non-negative number"); + } const amount = Number(this.amount); if (!Number.isFinite(amount) || amount < 0) throw new Error("cost amount must be a finite non-negative number"); if (!/^[A-Z]{3}$/.test(this.currency)) throw new Error("cost currency must be a three-letter uppercase ASCII code"); diff --git a/packages/typescript/test/cost-boundaries.test.ts b/packages/typescript/test/cost-boundaries.test.ts new file mode 100644 index 0000000..4b357af --- /dev/null +++ b/packages/typescript/test/cost-boundaries.test.ts @@ -0,0 +1,15 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import {CostObservation} from "../src/index.js"; + +test("cost accepts numeric zero and explicit numeric strings", () => { + for (const amount of [0, "0", 1.25, "1.25"]) { + assert.equal(new CostObservation(amount, "USD", "provider").asEventValue().amount, Number(amount)); + } +}); + +test("cost refuses values that Number would silently coerce", () => { + for (const amount of [true, false, null, [], [1], {}, "", " ", "\t\n"]) { + assert.throws(() => new CostObservation(amount as number, "USD", "provider").asEventValue(), /finite non-negative/); + } +}); diff --git a/src/agentrust_telemetry/usage.py b/src/agentrust_telemetry/usage.py index d249f98..4d71869 100644 --- a/src/agentrust_telemetry/usage.py +++ b/src/agentrust_telemetry/usage.py @@ -18,6 +18,7 @@ "cache_write_tokens", "reasoning_tokens", ) +_MAX_SAFE_INTEGER = 9007199254740991 @dataclass(frozen=True) @@ -30,18 +31,23 @@ class CostObservation: pricing_version: str | None = None def as_event_value(self) -> dict[str, Any]: - if isinstance(self.amount, bool): + if isinstance(self.amount, bool) or not isinstance(self.amount, (int, float, Decimal)): raise TypeError("cost amount must be a finite non-negative number") amount = float(self.amount) if not math.isfinite(amount) or amount < 0: raise ValueError("cost amount must be a finite non-negative number") - if len(self.currency) != 3 or not self.currency.isascii() or not self.currency.isupper(): + if ( + not isinstance(self.currency, str) + or len(self.currency) != 3 + or not self.currency.isascii() + or not self.currency.isupper() + ): raise ValueError("cost currency must be a three-letter uppercase ASCII code") if self.source not in {"provider", "caller", "price_resolver", "estimate"}: raise ValueError("cost source is not supported") value = {"amount": amount, "currency": self.currency, "source": self.source} if self.pricing_version is not None: - if not self.pricing_version: + if not isinstance(self.pricing_version, str) or not self.pricing_version: raise ValueError("pricing_version must be non-empty") value["pricing_version"] = self.pricing_version return value @@ -84,7 +90,11 @@ def usage_record( } for field in TOKEN_FIELDS: value = optional[field] - if value is not None and (isinstance(value, bool) or not isinstance(value, int)): + if value is not None and ( + isinstance(value, bool) + or not isinstance(value, int) + or not 0 <= value <= _MAX_SAFE_INTEGER + ): raise TypeError(f"{field} must be a non-negative integer") payload.update({key: value for key, value in optional.items() if value is not None}) if cost is not None: @@ -120,7 +130,11 @@ def add(self, event: dict[str, Any]) -> bool: raise ValueError("usage event must have a non-empty event_id") for field in TOKEN_FIELDS: value = event.get(field) - if value is not None and (isinstance(value, bool) or not isinstance(value, int)): + if value is not None and ( + isinstance(value, bool) + or not isinstance(value, int) + or not 0 <= value <= _MAX_SAFE_INTEGER + ): raise TypeError(f"{field} must be a non-negative integer") if "cost" in event: amount = event["cost"]["amount"] @@ -162,7 +176,10 @@ def rollup( observed = [event[field] for event in matches if field in event] coverage[field] = len(observed) if observed: - payload[field] = sum(observed) + total = sum(observed) + if total > _MAX_SAFE_INTEGER: + raise ValueError(f"{field} rollup exceeds the safe-integer range") + payload[field] = total costs = [event["cost"] for event in matches if "cost" in event] currencies = {cost["currency"] for cost in costs} diff --git a/tests/test_usage.py b/tests/test_usage.py index ef770eb..33719da 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -44,6 +44,23 @@ def test_cost_requires_finite_amount_and_explicit_provenance(self): def test_bool_is_not_accepted_as_token_integer(self): with self.assertRaisesRegex(TypeError, "input_tokens"): self.leaf(input_tokens=True) + with self.assertRaisesRegex(TypeError, "input_tokens"): + self.leaf(input_tokens=2**53) + with self.assertRaisesRegex(TypeError, "input_tokens"): + self.leaf(input_tokens=-1) + acc = UsageAccumulator() + acc.add(self.leaf(input_tokens=2**53 - 1)) + acc.add(self.leaf(input_tokens=1)) + with self.assertRaisesRegex(ValueError, "safe-integer"): + acc.rollup( + self.factory, + scope="agent_run", + run_id="run-1", + agent_id="agent-1", + workflow_id="workflow-1", + operation="chat", + ) + def test_rollup_deduplicates_and_reports_partial_coverage(self): first = self.leaf(cost=CostObservation(0.10, "USD", "provider"))