From c3a7ecfe99a4ae2a3d06dee1f2af28f613812655 Mon Sep 17 00:00:00 2001 From: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:14:38 -0500 Subject: [PATCH] fix(otel,metrics): reject bare string classification_values, whitespace-padded entries, and reserved '_other' bucket (#113) Signed-off-by: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com> --- packages/typescript/src/metrics.ts | 3 ++- packages/typescript/test/metrics.test.ts | 2 ++ src/agentrust_telemetry/otel.py | 16 +++++++++++++--- tests/test_otel_projectors.py | 4 ++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/typescript/src/metrics.ts b/packages/typescript/src/metrics.ts index 55e055d..21ec8de 100644 --- a/packages/typescript/src/metrics.ts +++ b/packages/typescript/src/metrics.ts @@ -7,9 +7,10 @@ export class OTelMetricEmitter { readonly #actionCount: Counter; readonly #actionDuration: Histogram; readonly #dataFlowCount: Counter; readonly #tokenCount: Counter; readonly #cost: Counter; readonly #classifications: ReadonlySet; constructor(meter: Meter, options: {classificationValues?: Iterable} = {}) { + if (typeof options.classificationValues === "string") throw new Error("classificationValues must be an iterable of strings, not a string"); this.#classifications = new Set(options.classificationValues ?? []); if (this.#classifications.size > 64) throw new Error("classificationValues cannot contain more than 64 entries"); - for (const value of this.#classifications) if (!value || value.length > 128) throw new Error("classificationValues must be non-empty and at most 128 characters"); + for (const value of this.#classifications) if (typeof value !== "string" || !value || value.length > 128 || value !== value.trim() || value === "_other") throw new Error("classificationValues must be non-empty and at most 128 characters"); this.#policyCount = meter.createCounter("agentrust.policy.decisions", {unit: "{decision}", description: "Policy decisions"}); this.#policyDuration = meter.createHistogram("agentrust.policy.evaluation.duration", {unit: "s", description: "Policy evaluation duration"}); this.#approvalCount = meter.createCounter("agentrust.approval.events", {unit: "{event}", description: "Approval lifecycle events"}); diff --git a/packages/typescript/test/metrics.test.ts b/packages/typescript/test/metrics.test.ts index 2feb8c2..a6b378b 100644 --- a/packages/typescript/test/metrics.test.ts +++ b/packages/typescript/test/metrics.test.ts @@ -25,6 +25,8 @@ test("classification dimensions are bounded by an explicit allowlist", () => { emitter.emit(fixture("data-flow.json")); assert.equal(meter.instruments.get("agentrust.data_flow.events")!.calls[0]!.attributes!["agentrust.data_flow.classification"], "_other"); assert.throws(() => new OTelMetricEmitter(new RecordingMeter() as unknown as Meter, {classificationValues: Array.from({length: 65}, (_, i) => `class-${i}`)}), /more than 64/); + assert.throws(() => new OTelMetricEmitter(new RecordingMeter() as unknown as Meter, {classificationValues: "public" as unknown as Iterable}), /not a string/); + for (const bad of [["_other"], [" public "], [123 as unknown as string]]) assert.throws(() => new OTelMetricEmitter(new RecordingMeter() as unknown as Meter, {classificationValues: bad}), /classificationValues/); }); test("policy, approval, and action metrics expose only bounded semantic dimensions", () => { diff --git a/src/agentrust_telemetry/otel.py b/src/agentrust_telemetry/otel.py index fec7c97..84ac11f 100644 --- a/src/agentrust_telemetry/otel.py +++ b/src/agentrust_telemetry/otel.py @@ -35,11 +35,21 @@ def __init__( *, classification_values: frozenset[str] = frozenset(), ) -> None: - if len(classification_values) > 64: + if isinstance(classification_values, (str, bytes)): + raise ValueError("classification_values must be an iterable of strings, not a string") + normalized = frozenset(classification_values) + if len(normalized) > 64: raise ValueError("classification_values cannot contain more than 64 entries") - if any(not value or len(value) > 128 for value in classification_values): + if any( + not isinstance(value, str) + or not value + or len(value) > 128 + or value != value.strip() + or value == "_other" + for value in normalized + ): raise ValueError("classification_values must be non-empty and at most 128 characters") - self._classification_values = classification_values + self._classification_values = normalized self._policy_count = meter.create_counter( "agentrust.policy.decisions", unit="{decision}", description="Policy decisions" ) diff --git a/tests/test_otel_projectors.py b/tests/test_otel_projectors.py index 1dbc7c9..ddce8b2 100644 --- a/tests/test_otel_projectors.py +++ b/tests/test_otel_projectors.py @@ -142,6 +142,10 @@ def create_histogram(self, *args, **kwargs): OTelMetricEmitter( Meter(), classification_values=frozenset(f"class-{i}" for i in range(65)) ) + for bad_vals in ("public", b"public", frozenset({"_other"}), frozenset({" public "}), frozenset({123})): + with self.subTest(bad_vals=bad_vals): + with self.assertRaises(ValueError): + OTelMetricEmitter(Meter(), classification_values=bad_vals) def test_action_metrics_exclude_action_and_agent_identifiers(self): from opentelemetry.sdk.metrics import MeterProvider