Repository navigation
fix(harness): clean every trace span before it leaves for Observe (TH-9504) - #280
khushalsonawat wants to merge 12 commits into
Conversation
Tool input, prompts, errors and recorded exceptions reach the platform's Observe project in span text when a hosted job carries FI keys, and no cleaner saw them. A cleaning exporter now copies each span with every string put through the kit's cleaner, and sits in front of fi's HTTP exporter as the provider's only processor. add_span_processor isn't the install step: on fi 1.0.0 it turns export off, and on 1.2.x it runs beside the raw export. begin() checks that one processor, the cleaner, remains after the instrumentors, and otherwise turns tracing off, taking every processor off the provider first. A value cut at one of traceai's lengths loses a trailing address whole, so a numeric password split from its "@" isn't kept as a port. A span that can't be copied is dropped, never sent raw. CI now runs these tests. TH-9504 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…pt' into feat/TH-9504-clean-trace-spans Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
| def _take(value: Any, flat: list[str]) -> Any: | ||
| """Put each string of an attribute value in flat; return where to find it again.""" | ||
| if isinstance(value, str): | ||
| flat.append(_cut(value)) |
There was a problem hiding this comment.
P3. The cleaning runs as CPU work on the export thread, in the same process as voice runs. A batch of 512 spans, each with 20 attributes of 5,000 URL-dense characters, took 4.1 s in one export call locally. That holds the GIL away from the audio pipeline.
The input is extreme, but traceai fills 5,000-character tool outputs routinely.
Fix: skip strings without :// before they reach _redact (most text has none), or cap the batch size for this processor.
There was a problem hiding this comment.
Not changed, with a reason. _redact_text already returns at once for a string with no '://' (agent.py on #278), so ordinary prose and most tool output cost almost nothing. Your 4.1 s batch is 512 spans × 20 attributes × 5,000 URL-dense characters, about 50 MB of URLs. Skipping strings without '://' here would also skip TH-9500's bare-key patterns once they land, and TH-9500 brings its own literal pre-check with a speed budget of under 1 s per 5 MiB. If you want a hard bound, I'd cap the processor's batch size rather than skip text; say if so.
There was a problem hiding this comment.
A correction and an update. My reply above is out of date: with TH-9500 (#308), the cleaner also runs key patterns on text without ://, so skipping those strings isn't free any more.
I tried chunking the export (64 spans per call, with a yield between calls). It didn't help, so I've dropped it. A reviewer measured it with your shape: 512 spans × 20 attributes × 5,000 URL-dense characters, an asyncio loop with a 1 ms timer, and the export in another thread (macOS, one run each):
| Export time | Mean stall per wake | Worst stall | |
|---|---|---|---|
| idle loop | – | 0.8 ms | – |
| one call (this PR) | 8.3 s | 11.2 ms | 25.4 ms |
| 64-span chunks with a yield | 11.5 s | 10.9 ms | 100.7 ms |
- Why it doesn't help: CPython already hands the GIL over every 5 ms, so the stall follows the total cleaning CPU, and chunking doesn't reduce that. A span count doesn't bound the work either: one span holding a 2 MB value took 0.64 s on its own.
- What it costs: with Observe unreachable, each chunk gets the exporter's own retry budget. 256 spans took 26.7 s to flush instead of 6.6 s.
So your point is still open. On a heavy batch, cleaning adds about 10 ms per loop wake on the voice process. A real fix has to cut the cleaning CPU itself, or move it out of the process's GIL. Batch size can't fix it. I've added it to the PR's known limits. I'm leaving this thread unresolved for you.
…ture-agi/agent-learning-kit into feat/TH-9504-clean-trace-spans Signed-off-by: khushalsonawat <khushalsonawat@gmail.com> # Conflicts: # src/fi/alk/harness/tools.py
…om/future-agi/agent-learning-kit into feat/TH-9504-clean-trace-spans Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…secrets, and clean by key and resource Review on #280 (Dileep). A JSON span value holding a URL with a query lost its closing quote and comma to the URL cleaner, so Observe showed broken text: JSON values are now cleaned as a structure and dumped back. The job's declared secret values, which the other outbound channels already replace, are now replaced in spans too; both entrypoints pass them to begin(). A string whose attribute key names a secret is masked, as the cleaner's key rule does elsewhere, while numbers stay untouched. The resource is cleaned once per resource and reused. TH-9504 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…-trace-spans Both sides added tests at the end of tests/test_harness_sandbox_server.py; both are kept. Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…ture-agi/agent-learning-kit into feat/TH-9504-clean-trace-spans Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…ture too A message can carry a tool's input dumped as a JSON string inside JSON; cleaned as text, a URL with a query in it would take the rest of the string. Raised by Ash from #278's review. TH-9504 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…9504-clean-trace-spans
…r the cut Platform settings in the simulator channel and the authoring secrets file (model names, the Observe project) are no longer replaced in spans; only values whose alias names a credential are, plus the guest-booking PIN and target number. The resource is never given declared values. Cut and JSON reading run on the value as recorded and declared values are replaced after; JSON keys are cleaned; JSON deeper than 64 levels is cleaned as text, and one span that fails to gather is dropped alone. Refs: TH-9504 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…sk a PIN The authoring secrets file is split by where a value comes from, not by its name: every customer value goes whole, and the SIMULATOR_* values and the gateway file each give only their credentials, never merged. A short all-digit secret such as a 4-digit PIN is masked as a whole number. A secret is also masked in its JSON-escaped forms, and a traceai cut is made on the masked text so no part of an adjacent secret stays. Cleaned JSON is dumped as ASCII. Refs: TH-9504 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…tform settings Picking secrets by name failed both ways, so the rule now fails closed: every value in secrets.json, the simulator channel and the gateway file is masked, except names on NON_SECRET_SETTINGS (model names, providers, the Observe project, locations, base URLs). In secrets.json only the platform's own SIMULATOR_* and FI_* names may be kept. Each source is read on its own, so a name in two files masks both values. Refs: TH-9504 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Hand-resolved merge hunks, for review. Merge
41718e66had a conflict in one hunk ofsrc/fi/alk/harness/tools.py,_save_contract:os.replace) with a call towrite_contract(contract, destination)andpath = destination / CONTRACT_FILE.said = (...)instead of returning_ok(...)directly.write_contractlines and the base'ssaidreply. That's the same resolution Ash made on [TH-9491] fix: keep URL credentials in an agent's description out of what the kit sends and stores #278 (e8499e78). The base now holds it as merged, sotools.pyis no longer in this PR's diff againstfeat/TH-8804-source-subfolder.One other merge was resolved by hand. In
50552115, both sides added tests at the end oftests/test_harness_sandbox_server.py, and both sets are kept unchanged. That file isn't in this PR's diff either. The rest (071abf6d,f1d2c49c,e7e0764f,796cb120) merged cleanly.Description
When a hosted job carries FI keys, the kit's traces go to the platform's own Observe project. Those spans carry the customer's text: tool input and output (up to 5,000 characters), prompts, tool error text, and recorded exceptions with their stack traces. No cleaner saw any of it, so a URL with a password or key in the agent's prompt reached Observe as written.
This change puts a cleaning exporter in front of fi-instrumentation's HTTP exporter, as the provider's only processor. Every string in every span goes through the kit's one cleaner (
_redact, TH-9491) before it's sent.Type of Change
Changes
A cleaning exporter (
_CleaningExporterinobservability.py). For each batch it gathers every string into one flat list: the span name, attribute values (strings inside tuples too), event names, event attributes, link attributes and the status description. It cleans them in one_redactcall, then sends a copy of each span.http.request.header.authorizationorapi_keyvalue is masked whole), never to numbers, so token counts such asgen_ai.usage.input_tokensstay.***, except names onNON_SECRET_SETTINGS: 27 platform settings such as model and provider names, the Observe project, locations and base URLs, which a span keeps. An unknown name is masked.project_name.(2),(3), so no value is lost. JSON nested deeper than 64 levels is cleaned as text, and a span that fails while being gathered is dropped alone.The install (
begin()). Straight afterregister(), the provider's processor tuple is replaced with oneBatchSpanProcessoraround the cleaner, and the old processors are shut down.add_span_processorisn't the install step:uv.lockpins), it shuts down the default processor and turns export off;pip install .resolves), it adds a processor beside the raw export.A run-time check. After the instrumentors are turned on,
begin()checks that exactly one processor remains and that it exports through the cleaner. If not, tracing is off for the job:On fi 1.0.0, a provider's shutdown alone leaves an added processor posting.
Values cut by traceai. traceai cuts text at 5,000 characters (ending in
...), 2,000, 1,000 and 500. A cut can leave a numeric password looking like a port, which the URL cleaner keeps._redact.Fails closed:
FAILURE, including an error in_redactor in the inner exporter.CI:
tests/harness/test_observability.pyis added tosdk-smoke.yml, aftertests/providers.The private surface touched, the same in fi-instrumentation 1.0.0 and 1.2.2, and in OpenTelemetry SDK 1.39.1 and 1.42.1:
TracerProvider._active_span_processor._span_processors;_default_processor;ReadableSpan,Event,Link,BoundedAttributesandBoundedList.If any of these changes, tracing turns off or spans are dropped. It never exports raw.
Testing
test_observability.py(in CI): 70 passed and 1 skipped, on both fi 1.0.0 with SDK 1.39.1 (the kit's venv) and fi 1.2.2 with SDK 1.42.1, run locally.begin(). A customer secret is masked under a platform name, a credential name and an arbitrary name, and a name in both files masks both values. The model and the project come through.begin()andregister(), with fake keys and a closed local port, so nothing leaves the machine.clean-installon the pull_request run fails one speed budget,test_contract_model.py::test_a_crafted_file_of_block_comments_does_not_hold_up_the_check[a-file-of-openers](0.5 s), at about 0.54–0.66 s. That file isn't in this PR. The base fails it on its own push run,477efe88(https://github.com/future-agi/agent-learning-kit/actions/runs/43169044737, 0.84 s, plustest_a_quote_that_grows_when_normalised_costs_little). This PR's own push run passes (https://github.com/future-agi/agent-learning-kit/actions/runs/43173801082)._default_processorline), which is caught on 1.0.0. The bugs include:add_span_processorused;_redact;...dropped;Known limits
uv.lock. CI resolves the newest fi (1.2.2 today). This PR's 1.0.0 run is local, in the kit's venv. A CI step installing withuv export --frozenwould close the gap; it isn't added here.claude_agent.tool.inputand.output(_attributes.py:50-51);_hooks.py:68,:88; tool input:167, output:266);_client_wrapper.py:91,:356);_hooks.py:200;_client_wrapper.py:381,:386);_hooks.py:196,:284,:356);_client_wrapper.py:412, which the URL cleaner refuses whole).curl -s https://api.example.com/health._redactcall once that lands, with no change here.fi/alk/telemetry) goes through only declared environment values today (TH-9530). The hosted chat workspace checkpoint is a file channel (TH-9521).Related Issues
Refs: TH-9504
Related: #278 (TH-9491), #225 (TH-8804), TH-9500, TH-9530, TH-9521