Skip to content

fix(aggregator): enforce strict Content-Length and producer validation in server and use loads_unique in CheckpointLog (#151) - #155

Open
drk-ram wants to merge 1 commit into
agentrust-io:mainfrom
drk-ram:drk-ram-fix-aggregator-151
Open

drk-ram wants to merge 1 commit into
agentrust-io:mainfrom
drk-ram:drk-ram-fix-aggregator-151

Conversation

@drk-ram

@drk-ram drk-ram commented Oct 23, 2026 •

Copy link
Copy Markdown

What and why

Closes #151

  • Require strict ASCII decimal Content-Length (raw_length.isascii() and raw_length.isdecimal()), reject conflicting duplicate Content-Length headers while preserving HTTP 400 on missing Content-Length (self.headers.get_all("Content-Length") or []), and require non-empty string producer values (when present) in aggregator/server.py.
  • Reject non-finite JSON constants (NaN, Infinity, -Infinity) in trace_verify._verify.loads_unique and use loads_unique(latest_path.read_text(encoding="utf-8")) in aggregator/_mmr_log.py (CheckpointLog.__init__).

Validation

  • python -m unittest discover -s tests -q — passed (409 tests)
  • python -m pytest tests/test_aggregator.py tests/test_intake_and_chain_hardening.py -q — passed (66 passed, 4 subtests passed)
  • python tools/validate_registry.py — passed (OK: 3 day file(s) valid)
  • python tools/validate_producers.py — passed (OK: 3 producer key file(s) valid)
  • python -m ruff check aggregator/ src/trace_verify/_verify.py tests/test_aggregator.py — passed (All checks passed!)

Registry impact

  • This PR does not modify any existing line under registry/

Existing entries are append-only. If you are changing one, say why here: mirrors
and auditors may already have observed the current commit hashes, and a rewrite
diverges them.

Checks

  • pytest passes
  • Registry and producer files still validate (tools/validate_registry.py,
    tools/validate_producers.py)
  • If the anchor format or verification semantics changed, docs/anchor-format.md
    is updated in the same PR, and existing inclusion proofs still verify
  • Commits are signed off (DCO)

@drk-ram
drk-ram requested review from a team, lywinged and rajnisht7 as code owners October 23, 2026 16:02
@drk-ram
drk-ram force-pushed the drk-ram-fix-aggregator-151 branch from 806d99f to 74bc66a Compare October 23, 2026 16:03

@rajnisht7 rajnisht7 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Content-Length strictness and the loads_unique switch for checkpoints looks good
a few things needs tighten:

  1. Explicit `producer: null still accepted
producer = body.get("producer")
if producer is not None and ...

body.get() returns None for both a missing key and an explicit JSON null, so:

{"producer": null, "claims": [...]}

gets 200. whereas issues asks that a present producer must be a non-empty string. kindly distinguish presence:

if "producer" in body:
    producer = body["producer"]
    if not isinstance(producer, str) or not producer:
        raise ValueError(...)
else:
    producer = None

and add None to the bad-producer test cases.

  1. loads_unique does not reject NaN / InfinityIssue #151 called out both duplicate-key and non-finite-constant rejection, and proposed loads_unique. The helper only rejects duplicates (object_pairs_hook); NaN / Infinity / -Infinity are still accepted. Required checkpoint fields with those values later fail type checks in from_dict, and extra fields are ignored, so this is not a forgery path, but the PR does not fully match the issue’s stated scope.
    Either:
    make loads_unique (or a checkpoint-specific loader) reject non-finites via parse_constant, and add tests, or narrow the issue/PR text to “duplicate-key + UTF-8 + dict check”.
  2. Tests- Current cases are a good start but miss:
  • producer: null / None
  • Unicode decimal digits that old int() accepted (e.g. \u0662) — \u00b2 was already rejected by the old path
  • a couple of valid boundaries ("0", leading zeros, MAX_BODY_BYTES)

non-blocking:

  • Very long digit Content-Length ("9"*5000) passes the new digit check then raises a raw ValueError from int(); outer do_POST catches it as 400, but restoring a try/except so _read_json_body always raises _BodyError would keep the helper contract clean.

@drk-ram

drk-ram commented Oct 23, 2026

Copy link
Copy Markdown
Author

Thanks @rajnisht7! Addressed in the latest commit: (1) aggregator/server.py: explicitly rejected {"producer": null} when "producer" is present in the request body, and caught ValueError on int(raw_length) for huge digit strings (>4300 digits) to return HTTP 413 cleanly. (2) src/trace_verify/_verify.py: added parse_constant to loads_unique so NaN/Infinity/-Infinity are rejected in CheckpointLog and all loads_unique callers. (3) tests/test_aggregator.py: added tests for "producer": None, \u0662 (Arabic-Indic digit), "0", "002", huge digit Content-Length, and NaN/Infinity in latest-checkpoint.json.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed this head; unittest discovery passes 407 tests. The source changes address the stated strict Content-Length, present producer, and persisted JSON boundaries. Before treating the new regression coverage as complete, please split test_non_decimal_content_length_and_non_string_producer_rejected into independent cases: its first Content-Length assertion prevents producer, huge-length and persisted-checkpoint assertions from running when that first guard is absent. Demonstrate each boundary fails with its own check disabled, retaining test names and valid controls. I am leaving this as a comment rather than approving on a green suite alone.

@rajnisht7 rajnisht7 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with @dinakarjs, the new test_non_decimal_content_length_and_non_string_producer_rejected is one big method that chains multiple independent security checks. If the first assertion (unicode Content-Length) fails, the rest (producer, huge length, checkpoint cases) never run, so we lose failure isolation.
A few other gaps in the regression coverage:

  • No -Infinity case (only NaN and Infinity are tested for checkpoints).
  • No exact MAX_BODY_BYTES boundary (== 410241024 should pass the length gate, +1 should 413).
  • loads_unique’s new contract (non-finites + duplicates) is only tested indirectly via CheckpointLog. Prefer direct unit tests for it.
  • No HTTP /batch tests for non-finite values or duplicate keys in the request body (the behavior works, but isn’t locked down).
  • Duplicate Content-Length headers aren’t rejected, .get() only sees the first value, and mismatched lengths can hang the connection. Worth rejecting when values differ.

@drk-ram

drk-ram commented Oct 25, 2026

Copy link
Copy Markdown
Author

Thanks @dinakarjs and @rajnisht7! Addressed all points in the latest commit: (1) split test_non_decimal_content_length_and_non_string_producer_rejected into three independent test methods in tests/test_aggregator.py (test_content_length_rejects_non_decimal_and_conflicting_duplicates_and_enforces_max_body_bytes, test_post_batch_rejects_invalid_producer_and_duplicate_or_nonfinite_json, and test_loads_unique_and_checkpoint_log_reject_duplicates_and_nonfinite_constants); (2) rejected conflicting duplicate Content-Length headers in AggregatorHandler._read_json_body (aggregator/server.py); and (3) added coverage for -Infinity, exact MAX_BODY_BYTES vs MAX_BODY_BYTES + 1, direct loads_unique unit assertions, and HTTP /batch requests carrying duplicate keys or NaN/Infinity/-Infinity.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 2ee5b29. The split regressions and duplicate-header handling are present, but HTTPMessage.get_all returns None for an absent Content-Length; the new comprehension raises TypeError and disconnects the client instead of the existing 400 refusal. test_missing_negative_and_malformed_lengths_are_400 reproduces RemoteDisconnected. Ruff also flags the unused local json import in CheckpointLog.

Signed author-adoption companion: dinakarjs#3, commit c5b137a. It normalizes missing get_all results to an empty list and removes the unused import. The existing regression fails before and passes after; full repaired suite: 409 passed, with repository Ruff and whitespace clean. No new header policy.

@drk-ram please adopt and obtain independent review of the combined head. This is COMMENT because I contributed the fix, not independent approval. Hosted checks remain required. Prepared with OpenAI Codex.

…n in server and use loads_unique in CheckpointLog (agentrust-io#151)

- Require strict ASCII decimal Content-Length headers, reject conflicting duplicate Content-Length values while preserving HTTP 400 on missing Content-Length, and require non-empty string top-level producer values (when present) in aggregator/server.py.
- Reject non-finite JSON constants (NaN, Infinity, -Infinity) in trace_verify._verify.loads_unique and use loads_unique in aggregator/_mmr_log.py (CheckpointLog.__init__).
- Add isolated regression tests in tests/test_aggregator.py for Content-Length validation/limits, producer validation, duplicate/non-finite JSON batch requests, and loads_unique / CheckpointLog persistence.

Closes agentrust-io#151

Signed-off-by: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com>
@drk-ram
drk-ram force-pushed the drk-ram-fix-aggregator-151 branch from 2ee5b29 to 31c3722 Compare October 29, 2026 16:07
@drk-ram

drk-ram commented Oct 29, 2026

Copy link
Copy Markdown
Author

Thanks @rajnisht7 and @dinakarjs! Adopted the (self.headers.get_all("Content-Length") or []) normalization in aggregator/server.py so missing Content-Length returns HTTP 400 (test_missing_negative_and_malformed_lengths_are_400 passes), removed the unused import json in CheckpointLog.__init__ (aggregator/_mmr_log.py), and rebased onto origin/main. Full unittest discovery (409 tests), tests/test_aggregator.py, and Ruff pass at 31c3722.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review at exact head 31c3722, Linux/Python 3.12.14 with the repository's hash-locked CI/witness/runtime dependencies and the local editable checkout.

Verified adoption of the missing-Content-Length normalization and removal of the unused import. Full unittest discovery: 409 tests pass; repository Ruff passes. The existing missing/malformed-length regression now exercises the HTTP refusal path successfully, closing my earlier follow-up. I contributed the correction, so this records incorporation rather than independent approval. Normal hosted checks and independent review of the combined head remain required.

Reviewed with OpenAI Codex.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(aggregator): enforce strict Content-Length and producer validation in server and use loads_unique in CheckpointLog

3 participants