Offline source-record identity inspector
Status: first implementation for #315. Contract: source-record identity. Native telemetry capture and adapter qualification: NOT RUN.
Run the supplied-record diagnostic
From the repository root, using Python 3.11 or newer and the standard library:
python scripts/observation_identity.py docs/observability/examples/identity.synthetic.jsonl
python -m unittest discover -s tests -p test_observation_identity.py -v
The first command reads one explicit file and prints one aggregate JSON object.
It does not execute observations, retrieve artifacts, scan directories, call a
model, access a network service, write a report file, or modify the input. It is
not wired into harness.py, a hook, a scheduler, an exporter or a required check.
Exit 0 means a report was produced, including conflicts, unavailable identity,
and empty input. Exit 2 means the entire input was unreadable or outside the
supported profile. There is no partial-success report for rejected input. Stderr
uses fixed diagnostic codes without source values, identifiers or paths. The
normal argparse --help response is exit 0, not an observation report.
Every report retains execution_verified: false, gate_eligible: false,
merge_verdict: null, and capture_completeness: "unknown". Neither the exit code
nor a consistent identity state is product acceptance or merge authority.
Closed input profile
This tool implements agent-harness-observation-identity/0, a deliberately
narrow profile of the existing v0 envelope, not a full v0 validator or native-log
parser. Unsupported fields fail before any counts are emitted. Do not strip fields
from an arbitrary native log merely to make the tool accept it: projection must
be performed under a separately reviewed content-off normalization contract.
Top-level object keys are closed by record kind:
| Kind | Required fields | Optional fields |
|---|---|---|
span |
record_kind, name, attributes, trace_id, span_id, span_kind, start_time, end_time, duration_ms, status |
parent_span_id |
event |
record_kind, name, attributes, time |
trace_id and span_id together |
Span names are invoke_agent, plan, chat, execute_tool,
agent_harness.tool.execution, or agent_harness.tool.permission_wait, optionally
followed by one space and one safe token. Event names are exactly
agent_harness.edit.applied, agent_harness.verify.result,
agent_harness.request.attempt, agent_harness.quota.observed, and
agent_harness.subagent.completed. Supporting an event name does not mean its
full native payload is supported. In particular, quota and cost values are not
accepted by this identity-only profile.
Trace/span IDs are nonzero lowercase hexadecimal strings of 32/16 characters. A supplied non-null parent ID must be 16 such characters. Null parent means a claimed known root; omission remains distinct. Event context is correlation only, not its identity key. The tool does not validate parent topology or native origin.
span_kind is INTERNAL or CLIENT; status is UNSET, OK, or ERROR.
Timestamps use UTC YYYY-MM-DDTHH:MM:SS[.fraction]Z, with one to nine fractional
digits when supplied; calendar validation rejects impossible dates and leap
seconds. Duration is a finite, nonnegative number no greater than 2^63 - 1.
The inspector does not reconcile elapsed time with wall-clock intervals or infer
which clock produced a duration. It compares supplied values, not timing accuracy.
Accepted attributes
All keys in this table use their literal dotted spelling inside attributes.
Safe tokens use the canonical identity grammar: case-sensitive ASCII, 1 to 128
characters, [A-Za-z0-9][A-Za-z0-9_.:-]{0,127}. Grammar validation does not prove
that an identifier is non-sensitive, genuine, unique or stable.
| Attributes | Accepted values |
|---|---|
agent_harness.schema.version |
Required string "0" |
agent_harness.content.capture |
Required string "off"; redacted-content mode is unsupported |
agent_harness.source.name |
Required safe token |
agent_harness.source.version, agent_harness.source.namespace, agent_harness.source.instance.id, agent_harness.source.record.id |
Optional safe tokens |
agent_harness.normalization.profile |
Safe token identifying the original mapping plus privacy revision; needed for qualified deduplication, not part of the source key |
agent_harness.source.record.identity |
native_span, producer_record or unavailable; omission means unavailable |
gen_ai.tool.call.id, gen_ai.conversation.id, gen_ai.response.id |
Optional safe correlation tokens, never fallback source keys |
agent_harness.outcome |
success, failure, cancelled, denied, unknown |
agent_harness.phase |
plan, tool, edit, verify |
agent_harness.verify.result |
pass, fail, error, not_run; only a claim, no authenticated check |
agent_harness.request.attempt_count |
Integer from 1 through 2^63 - 1 |
gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_write.input_tokens, gen_ai.usage.reasoning.output_tokens |
Integer from 0 through 2^63 - 1; compared for conflicts, never summed |
agent_harness.unavailable |
Map of absent supported field/attribute names to not_exposed, not_observed, not_applicable, redacted, incomplete, or invalid |
Booleans are not numbers. A gap claim for a present field is rejected. Native resource objects, commands, raw errors, prompts, file/body references, arbitrary extensions, monetary amounts and additional GenAI attributes are unsupported. Missing an otherwise valid identity component or normalization profile is an unavailable observation, not a malformed file; no key is manufactured for it.
The record’s normalization profile is an origin claim distinct from this tool’s
output profile. A producer changes its token whenever its field mapping,
privacy filtering, or normalization rules change. The inspector compares the token
but cannot authenticate its meaning. Known profile mismatches conflict under one
source key, rather than creating separate keys that hide a collision. Unsupported
profile shapes are rejected; no version-specific native mapper runs here.
Counting and conflict semantics
The source key follows the canonical contract. Equality compares the complete
accepted parsed JSON object with sorted object keys. Missing fields, null parent,
changed source/profile versions and changed observations remain distinct. Integer
and floating-point representations remain conservatively different (1000 versus
1000.0); insignificant JSON whitespace and object order are ignored. JSON floating
values follow Python binary64 parsing, not arbitrary-precision source-byte equality.
No payload digest is used as a source key or published as an execution receipt.
Any unequal representation under a key makes all rows in that group conflicting, including repeat exports of an earlier variant. They never contribute to consistent counts. An unknown-profile row is unavailable rather than silently joined to a known profile; its presence keeps the overall deduplicated total null.
| Report field | Meaning |
|---|---|
received_records |
All accepted input rows |
distinct_keys |
Keyed groups, including conflicting ones |
consistent_records |
One observation per non-conflicting keyed group |
duplicate_records |
Extra identical rows in consistent groups only |
conflicting_keys, conflicting_records |
Conflicting groups and all their rows |
unavailable_identity_records |
Rows lacking qualified source-key/profile information |
deduplicated_records |
Consistent-group count only when nonempty input has no conflicts or unavailable rows; otherwise null |
identity_state |
conflicting, otherwise unavailable, otherwise consistent, otherwise empty |
The partition is received = consistent + duplicate + conflicting + unavailable.
The report has no tool/job totals, per-source identifier listing, token sum, cost
sum, completeness guarantee, verified revision or overall success field. Different
sources that lie about the same namespace/profile cannot be detected from identical
claims. Wrapper spans and child events remain different source records, never
extra tool invocations or independently delivered jobs.
Synthetic fixture and verification scope
identity.synthetic.jsonl is authored synthetic data, not a captured agent loop. Its eight rows contain a repeated span export, a second span for the same call, three conflicting variants of one event, a sequence reused after a producer restart, and one event without an instance.
Expected aggregate: received 8, distinct keys 4, consistent 3, duplicate 1, conflicting keys 1, conflicting rows 3, unavailable 1, deduplicated null, state conflicting. Reordering the rows produces the same report. The unit suite executes the inspector against this fixture, not a native agent.
Tests also cover profile changes/missingness, namespace and tuple-boundary collisions, events sharing a span/time, retained errors, malformed JSON/types, resource limits, input nonmutation, safe diagnostics, file replacement and a POSIX FIFO refusal. Platform-dependent controls state their limitations. Existing CI unittest discovery picks up this suite; no workflow was edited. The pre-existing explicit Ruff/Black/compile lists do not automatically include these new modules. Run those tools on the two named Python files when available, and report that result separately rather than claiming broader lint coverage from a green workflow.
Resource and filesystem limits
Input: one regular file, at most 4 MiB, 5,000 rows, and 64 KiB per row. Both raw input and canonical accepted representations are bounded. LF separates records; CRLF is accepted, a final newline is optional, and blank records are rejected. UTF-8 BOM, duplicate JSON keys, non-finite numbers, parser-depth failures and unsupported nested structures are refused. The pure aggregation entry point also applies row and canonical-byte limits. It expects finite in-process iterables of JSON-shaped records, not adversarial executable Python objects.
Leaf symlinks/reparse points, directories, FIFOs and explicit UNC/device-style paths are refused. Regular hard links are readable, not identity evidence. The file is checked before opening, after opening and after reading; available nonblocking/no-follow flags prevent a POSIX FIFO/symlink swap from becoming a read. This is not a general filesystem sandbox: hostile concurrent parent-directory replacement, network-mounted paths, restored metadata and arbitrary non-cooperating writers are outside its guarantee. Use a reviewed, stable, local synthetic file.
Next bounded qualification and handoff
Do not reimplement identity or add another ledger. First identify one native version, one authorized content-off capture route and the origin/lifetime/profile metadata it can really preserve. Use a disposable synthetic task with one normal tool and one failed/denied tool plus an independently recorded verifier result. Re-import the same safe observation export, compare source IDs by inspection, and record unavailable lifetime information honestly. Do not force a live rate limit, enable raw bodies or infer retry usage from final-attempt tokens.
Native mapping belongs in CLAUDE_CODE.md; logical-job accounting stays with #288/#296, and the broader observation programme stays #299. The next trial requires a real local runtime and approved capture details. Synthetic inspector tests alone do not qualify that runtime or earn a measured benchmark row.