Repository navigation
feat(trajectory): receive and upload bug report packages - #926
Conversation
A Bug Report Receiver cannot trust the archive it is handed, so the
package is validated by streaming through it once: nothing is extracted,
member bytes are hashed in passing and compared with the manifest in
bugreport.json, and the decompressed volume is bounded before it is
produced. The bounded gunzip layer exists because tarfile hands a PAX or
GNU extended header's declared size straight to one read() call, so a
header claiming gigabytes would be decompressed whole into memory.
Checks run in archive order: size, gzip and tar framing, member type and
name rules (one root directory named by a report id, no links or special
files, no directory that carries data, no duplicate names), the metadata
file's schema and id, the manifest against the hashed members, the
needs_review/risk_accepted invariant, and a proper end-of-archive block
with nothing but padding after it. Absolute paths found in the metadata
are reported as notices rather than rejected.
The report id pattern is now public as REPORT_ID with is_report_id(),
and record.json's upload block gains a state enum ("", uploaded, failed)
with optional uploaded_at and error fields, validated when present.
Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
tarfile expands every sparse flavour (old GNU type S, PAX GNU.sparse.* 0.0, 0.1 and 1.0) before it hands the member back, and a 1.0 map is read from the data stream in 512-byte pieces into a list whose size the per-read cap cannot see; a few hundred compressed bytes could allocate tens of megabytes per map entry batch before any check ran. The inspector now reads with a TarInfo subclass that cuts each sparse handler off at its entry, and only plain file types or directories pass the type check. Parser errors are now converted at the tarfile and JSON boundaries: a bare ValueError from tarfile internals (malformed PAX numbers) or from the JSON decoder (an integer past its digit limit) used to escape as something other than PackageError. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
The Bug Report Receiver is a standalone aiohttp service that takes a Bug Report Package in, checks it with package_check without extracting it, and lands the original bytes under <save_dir>/<UTC date>/ beside a receipt sidecar. It authenticates nobody and serves no listing or download; reachability is the network's job. The sidecar is written first and the package is renamed into place last, so a package in a date directory is a committed one: the same bytes again return the stored receipt, different bytes are refused with 409, and a sidecar that is missing or fails its contract is rebuilt from the package at startup or on lookup. A sidecar without a package is a failed commit and is removed. Uploads of one report id are serialised by a reference counted lock so a late arrival never gets a second lock. Inspection runs in a worker thread; cancelling the request stops it cooperatively through the new should_stop hook and waits for it before the temporary file is removed. Content-Encoding is refused, chunked bodies need a length, and every answer is JSON. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
The receiver created a day's directory behind an exists() check with a plain mkdir, and commits run in worker threads serialised only per report id. Two different reports arriving first thing in a UTC day both passed the check, one mkdir raised FileExistsError, and that legitimate upload answered 500 with its package dropped. The directory is now created idempotently: a lost race is fine, an existing non-directory still fails, and the 0700 mode is applied only by the creator. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Admission now runs before a byte of body is read: a global concurrency slot (503 without queueing), a token bucket per client address (429 with Retry-After), the daily byte and package budgets (507 / 429), and a free-disk reserve that counts uploads still in flight (507). A request is charged to the UTC day it was admitted on and lands in that day's directory, and the ledger settles by what the commit task actually did: a package renamed into place while the request was timing out or its client had gone is still counted, and only an uncommitted upload gives its reservation back. Behind a trusted proxy the client is read from X-Forwarded-For right to left, skipping only hops that are our own proxies and stopping at the peer when a hop is not an address at all, so a forged chain cannot pick its own rate-limit bucket. Idle and total timeouts answer 408 and stop the inspection thread cooperatively. Every request logs one line without body content. serve() runs the application on a TCP site with handler cancellation on, hands the bound address to a ready callback (port 0 is allowed), stops on SIGTERM/SIGINT or an event, and restores the signal handlers it replaced; run_receiver() is the blocking entry point. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Settling a request removed the temporary file first and only then charged or released the reservation and freed the concurrency slot, so an unlink that failed for any reason other than absence skipped both. A few such failures would pin every slot until the process restarted. The steps are now nested so none can skip the next: the commit thread is awaited and the budget settled by what it did, the temporary file is removed or logged and left for the next startup, and the slot is always released. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
raven/trajectory/upload.py is the client side of the Bug Report Receiver and carries no user interface of its own: it decides where a package goes, sends it, checks the receipt, and writes the outcome into the report's record, so the report-bug command, the interactive browser and a future web page call the same functions. The destination is one string: DEFAULT_UPLOAD_URL as shipped (empty here, meaning no upload) overridden by RAVEN_BUGREPORT_URL when that variable is set, even to an empty value. A configured URL must be https or http on this machine unless RAVEN_BUGREPORT_ALLOW_HTTP says plaintext is acceptable. The whole exchange runs under one wall-clock deadline that cancels the request outright (per-operation socket timeouts cannot bound a peer that trickles bytes), the response is read as raw bytes under a size cap with compressed answers refused rather than inflated, and a receipt counts only when every field agrees with the request. A failed attempt is recorded as failed unless the report was already uploaded; a package that vanished or changed is refused without touching the record. report-bug and the browser's Report a bug resolve the URL before any side effect, disclose "Will upload to <origin>" on the confirmation page, upload once the package lands and show the receipt or the failure with the retry path. The browser's Bug reports screen gains an Upload action, and raven trajectory upload-bug-report uploads an existing report again. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…they escape A receiver URL was only checked for scheme and host, so a malformed IPv6 literal raised a bare ValueError into the command, and a port that was not a number or out of range passed and then failed inside httpx after the package had been collected, pinned and packed. The URL is now parsed the way the request will parse it, port included, and every parse error is a configuration error raised before any side effect. A response nested deeply enough to overflow the JSON decoder raised RecursionError past the upload error boundary; it now counts as not JSON, a malformed receipt on success and a plain status on failure. The test fixtures stop system-configured proxies as well as environment ones (urllib falls back to the macOS settings when the environment names none), through one shared helper. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
raven trajectory receiver --save-dir DIR runs the Bug Report Receiver on the command line: bind address and port (0 picks one and prints it), trusted proxy networks, the package and daily limits in MiB, the free-disk reserve, rate limiting, concurrency and timeouts all map onto ReceiverConfig, a bad CIDR is a usage error, and a save dir the process cannot prepare is reported in one line instead of a traceback. The trajectory debugging page gains a "Send and receive bug reports" section in both languages: what the receiver stores and where, the deployment rules (loopback by default, a TLS proxy for anything else, the nginx location and --trusted-proxy), the RAVEN_BUGREPORT_URL table the clients read, the retry command, and what is and is not checked. An integration test drives a real receiver process, a real report-bug run and a real retry over the printed address and the environment variable. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
The listening address, the save directory, an upload failure and the retry command were printed through rich's wrapping, which splits a long path or the upload-bug-report command across lines at eighty columns and leaves nothing a reader can copy. They are now printed soft-wrapped. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
The subsections of the new "Send and receive bug reports" section had no pinned anchors on the Chinese page, so Markdown would have numbered them and the links the English page gets for free would not resolve there. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
… never listens The "Cannot prepare" diagnostic was the one line still printed through rich's wrapping, so a long temporary path split "Permission denied" over two lines; it is soft-wrapped like the other diagnostics, and the test now forces a forty-column console and a deep path. The end-to-end helper started the receiver with a blocking reader thread and a per-line timeout, so a child that never listened was left running with its reader stuck. It now uses an asyncio subprocess under one deadline for the whole start, kills and waits for the child on every exit but success, and reads both streams for the diagnostics, since the command reports a refused directory on stdout. Two tests cover a silent child and a refusing one. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: malformed JSON and receiver URLs still escape the new validation boundaries.
I reviewed the full github/main...HEAD diff and the surrounding package builder, receiver/store, upload client, CLI/browser callers, tests, and commit history. I also checked AGENTS.md/CLAUDE.md, the trajectory terms added to CONTEXT.md, backward compatibility for existing records and upload-disabled installs, architecture boundaries, and whether tests were weakened; I found no separate issues in those areas.
Verification:
uv run pytest tests/test_trajectory_package_check.py tests/test_trajectory_receiver.py tests/test_trajectory_upload.py tests/test_trajectory_bugreport.py tests/test_cli_trajectory_commands.py tests/test_cli_trajectory_browse.py -q: 627 passed afteruv sync --extra dev. The initial run had 627 fixture-setup errors because the declaredeveros-memorydev extra was absent.uv run pytest tests/integration/test_bug_report_upload_e2e.py -m integration --override-ini norecursedirs='' -q: 3 passed.- Source-language, large-file, Ruff, and
git diff --checkchecks passed. (makeis unavailable in this environment, so I ran the repository check scripts directly.)
…n boundaries A bugreport.json nested deeper than the interpreter allows made inspect_package raise RecursionError through the receiver, which answered 500 instead of 400 bad_package; the same depth in a receipt sidecar made the startup repair raise instead of rebuilding the sidecar. Both decoders now treat decoder recursion as invalid JSON. The upload URL validator refuses credentials (without echoing them) and any query or fragment, since the endpoint path appended to such a URL would be swallowed; the disclosure line strips userinfo regardless. The tarfile ValueError test patches TarInfo.fromtarfile, the entry that TarFile.next calls on every Python version; frombuf is bypassed by the private _frombuf since CPython 3.12.15, which left the test asserting nothing on CI. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: URL validation must stop echoing rejected secrets.
The package-metadata and sidecar-recursion findings are fixed, verified, replied to, and resolved. The URL thread remains open because malformed credential-bearing URLs can fail during urlsplit or parts.port before the credential check and the generic error echoes the complete URL; query/fragment rejection also echoes query tokens.
For this revision I reviewed the complete delta from 674371624e2e, rechecked it against the current github/main...HEAD change and the affected package parser, receiver/store, uploader, CLI/browser callers, docs, tests, and history. I covered AGENTS.md/CLAUDE.md and CONTEXT.md rules, architecture boundaries, backward compatibility, and test-strength changes; no other issue survived refutation.
Verification:
uv run pytest tests/test_trajectory_package_check.py tests/test_trajectory_receiver.py tests/test_trajectory_upload.py tests/test_trajectory_bugreport.py tests/test_cli_trajectory_commands.py tests/test_cli_trajectory_browse.py -q: 641 passed.uv run pytest tests/integration/test_bug_report_upload_e2e.py -m integration --override-ini norecursedirs='' -q: 3 passed.- Ruff, source-language, large-file, and
git diff --checkchecks passed.
Every UploadConfigError used to quote the URL as typed, so a parse failure (a bad port, a broken IPv6 host), a wrong scheme, a port out of range or the query rule echoed credentials and query strings onto the terminal. The messages now show the URL cut on the string before any parsing: userinfo and everything from the first "?" or "#" are dropped, leaving scheme, host, port and path, which is what a reader needs to spot the typo. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
The reported URL leaks are fixed: malformed host/port and IPv6 failures redact userinfo, and query/fragment values are elided before the error reaches the CLI or record path. The original URL thread is resolved.
Nonblocking follow-up: _shown still exposes text before an unescaped ? or # that appears before @, and the raw port-parser exception can repeat that token. This needs a deliberately malformed credential URL and has an immediate configuration escape hatch, so it does not meet the fourth-round blocking bar.
Coverage: I reviewed the current delta against the previously reviewed full branch diff, the repository rules (AGENTS.md/CLAUDE.md and runtime context), affected callers and history, backward compatibility, the new tests for weakening, and the environment-to-validation-to-CLI/record security boundary. Verification: uv run pytest on the six focused unit files (662 passed); uv run pytest -m integration tests/integration/test_bug_report_upload_e2e.py -q (3 passed); Ruff, source-language, large-file, and git diff --check all passed.
A credential URL with an unescaped "?" or "#" before the "@" rendered its userinfo, because the query cut ran before the userinfo cut, and the port parser's own message quoted the same token back. The renderer now removes everything up to the last "@" before the first "/" first, then cuts at the first "?" or "#", and parse failures no longer append the parser's text. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
The remaining URL-error disclosure is fixed: malformed userinfo is removed before query/fragment truncation, and parser-controlled error text is no longer copied into the user-facing message. Adversarial checks across the previous and newly added URL shapes did not expose the test secrets.
Coverage: I reviewed the revision delta against the previously reviewed full branch diff, repository rules and runtime context, affected CLI/record callers and history, backward compatibility, the security boundary, and the test changes for weakening. Verification: the six focused unit files passed (670 tests), the real-process upload integration file passed (3 tests), and Ruff, source-language, large-file, and git diff --check passed. All threads I opened remain resolved.
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: the two newly reported package resource-boundary failures remain unresolved.
This updates my whole-record stance after the new comments: one thread reports recursive tar extended headers escaping as RecursionError with an HTTP 500 and excess memory use; the other reports notice amplification creating sidecars outside the configured daily-byte accounting. These are @0xKT's findings, not mine, so I am not re-grading them, replying in their threads, or resolving them. The author and that reviewer need to settle the reported facts.
The unchanged revision's existing relevant tests still pass: 386 package/receiver/upload/bug-report unit tests and 3 real-process integration tests. That green run does not exercise the new reproductions and therefore does not answer the blockers. All threads I opened remain resolved.
…he receiver tarfile fetches the header after a PAX or GNU long header by recursing once per header and holds the whole chain at once, so an archive chaining thousands of them escaped inspect_package as RecursionError (answered 500, not 400) and held depth times the per-read cap in memory. _StrictTarInfo now counts the extended headers before a member on the TarFile and refuses more than four with header_chain_too_long; the tar boundary also maps a RecursionError to a PackageError in case another shape ever reaches it. A small package whose metadata is mostly absolute paths produced one notice per hit, growing the sidecar to tens of megabytes that the daily byte budget never saw. The path scan now stops after MAX_NOTICES hits (the scanner takes a limit), the sidecar digest keeps the first 1000 characters of each field, the daily ledger counts a sidecar with its package both when a commit is settled and when a day is backfilled, and the docs say so. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: the two resource-boundary threads remain open pending their reviewer's confirmation.
I independently verified the new revision's substance. Extended-header chains are cut off before recursive accumulation, RecursionError is contained as a package error, and the HTTP regressions cover 400 responses. Metadata scanning and digest fields are bounded, the former multi-megabyte sidecar amplification is reduced below 8 KiB in the regression, and landed/backfilled sidecar bytes are included in the ledger. I found no new blocker of my own.
Named nonblocking follow-up: admission still reserves only the declared package bytes, so settlement can put landed_bytes above max_bytes by the bounded sidecar size (a direct ledger check with max 100, reserve 90, settle 110 ends at 110). This change introduces that accounting edge and it is reachable near the limit, but the now-bounded sidecar overhead and existing free-disk floor give operators a working escape hatch, so it does not meet the late-round blocking bar. This is evidence about the implementation, not a re-grading of @0xKT's open finding.
Coverage: I reviewed the eight-file delta against the previously reviewed branch, repository rules and runtime context, affected callers and history, backward compatibility, the resource-boundary architecture, and the tests for weakening. Verification: six focused unit files passed (684 tests), the real-process integration file passed (3 tests), and Ruff, source-language, large-file, and git diff --check passed. All threads I opened remain resolved; I did not resolve or reply in the other reviewer's threads.
# Conflicts: # tests/test_trajectory_bugreport.py
… notices With the export gate now reading parsed values instead of JSON text, the receiver's triage notices follow: _check_metadata runs find_absolute_paths_in_json over the decoded bugreport.json, so a literal backslash in a description no longer produces a notice, and each notice names the field it came from. The JSON scanner takes the same limit the text scanner does, so the notice cap keeps bounding the walk. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
There was a problem hiding this comment.
Blocking: the two reviewer-owned resource-boundary threads remain open pending their owner's confirmation.
The new parsed-value reconciliation works for ordinary metadata and the existing fixes remain intact. I added two nonblocking crafted-input follow-ups inline: the recursive JSON walker has a narrow decode/walk recursion gap, and its new field-path label can copy an oversized object key into one notice. Neither meets the late-round blocking bar: the first leaves the receiver running after the rejected request, and the second is bounded by the metadata/package limits and counted by the ledger.
Coverage: I separated the current-main merge from the branch commit, reviewed the actual github/main...HEAD diff plus the three-file new commit, repository rules and runtime context, callers/history, backward compatibility, the resource-boundary architecture, and tests for weakening. Verification: six focused unit files passed (701 tests), the real-process integration file passed (3 tests), and Ruff, source-language, large-file, and git diff --check passed. The older threads I opened remain resolved; the two new inline threads are explicitly nonblocking suggestions. I did not resolve or re-grade the other reviewer's threads.
…ice labels json.loads accepts nesting close to the interpreter's recursion limit, so the recursive walk behind find_absolute_paths_in_json overflowed on a document the decoder had just accepted and escaped inspect_package as a RecursionError. The walk now runs on an explicit stack in the same document order. The field label of a receiver notice is cut to 120 characters, so a long object key cannot enlarge the sidecar past the bounds the digest and notice caps set. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: the two reviewer-owned resource-boundary threads remain unresolved.
The current delta fixes both nonblocking findings I opened: parsed metadata is walked iteratively in document order, and rendered field labels are bounded. I reproduced the original depth-992 input and 100,000-character key successfully, replied to the author, and resolved both of my threads. I found no new defect in this revision.
Coverage: I reviewed the five-file delta against the previously reviewed branch, repository rules and runtime context, callers/history, backward compatibility, resource limits, and tests for weakening. Verification: six focused unit files passed (706 tests), the real-process integration file passed (3 tests), and Ruff, source-language, large-file, and git diff --check passed. All threads I opened are resolved; I did not resolve or re-grade the other reviewer's two still-open blockers.
…nner The deadline tests asserted the upload ended within 1 s of a 0.3 s total timeout, and a busy CI shard delivered the cancellation after 1.5 s. The error message already names the path that raised; the elapsed bound only has to show the request was cut short of the alternatives (a 1 s io_timeout, a 3 s drip), so it is now 2.5 s and a parameter of the helper. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: the two reviewer-owned resource-boundary threads remain unresolved.
The new delta only widens the deadline tests' elapsed-time ceiling from 1.0s to 2.5s. I tried to refute whether that weakens the behavior check: it still requires the retryable UploadError and the total-deadline-specific exceeded 0.3s message, which distinguishes this path from the I/O-timeout error, while remaining below the server's 3s drip. The upload test file passes, and the three deadline cases passed in five consecutive runs.
I covered the repository rules, this revision's diff, the exercised upload caller/error semantics, backward compatibility, whether the test was weakened to green, and the relevant timeout architecture. I found no new defect in this revision. The existing chained-header and sidecar-budget threads were opened by another reviewer and remain unresolved, so I am preserving their blocking stance rather than re-grading or resolving them.
Verification:
uv run pytest tests/test_trajectory_upload.py -q- 88 passed- five runs of
uv run pytest tests/test_trajectory_upload.py -k deadline -q- 3 passed each git diff --check github/main...HEAD- passed
## Summary Bump the package version from 0.2.4 to 0.2.5 for the next release. Only pyproject.toml and uv.lock change. Since v0.2.4, main merged 115 PRs: 30 features, 74 fixes, three perf, three docs, three test, one refactor and one build. The larger features are work on registered machines (#920, #922, #934, #947, #983, #1005), the host as an agent manager (#1010), the knowledge reader (#489), Personas (#709), the everos-cloud-memory plugin (#883) with everos 1.4.2 (#950), and the trajectory view with bug report export and upload (#877, #926, #948). None carries a BREAKING CHANGE footer. The maintainer chose a patch bump, the way v0.2.1 through v0.2.4 were for their features. The release workflow now also builds the everos-cloud-memory wheel (#883), a step no release has run yet, so a v0.2.5-rc1 tag goes first to dry-run the pipeline. The stable tag and release notes follow once this lands. uv.lock is regenerated with uv 0.12.12. uv 0.9.x rewrites the dependency markers of five unrelated packages, and uv 0.13.x raises the lock revision from 3 to 5. uv 0.12.12 leaves only the raven version line changed. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [x] Other ## Verification - `uvx --from uv==0.12.12 uv lock` on a clean worktree cut from origin/main: Updated raven v0.2.4 -> v0.2.5 - `git diff --numstat origin/main..HEAD`: one line each in pyproject.toml and uv.lock - `uvx --from uv==0.13.3 uv lock --check`: exit 0, so the `uv export --locked` step in release.yml accepts the lock - `PYTHONPATH=. python3 scripts/check_commit_messages.py origin/main..HEAD`: exit 0 - `npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs`: exit 0 - No test run: the change is version metadata only. - [ ] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk No code change. Rollback is reverting this commit before any v0.2.5 tag is pushed. - [ ] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
Summary
Bug Report Packages can now leave the machine and be received on the other end.
Receiver.
raven trajectory receiver --save-dir DIRruns a standalone aiohttpservice that takes packages in, checks them in a stream without extracting
anything (bounded decompression with a per-header cap, sparse members refused,
member rules, manifest hashes, the
needs_review/risk_acceptedinvariant),stores the original bytes under
<save-dir>/<UTC day>/next to a receiptsidecar, and answers idempotently by report id (same bytes: the stored receipt;
different bytes: 409). The sidecar is written before the package is renamed into
place, so a package in a date directory is always a committed one; a damaged
sidecar is rebuilt from the package. There is no authentication by design: abuse
is bounded by a per-address token bucket, a concurrency cap, daily byte and
package budgets, a free-disk reserve and idle/total timeouts, and the budget is
settled by what the commit task actually did, not by the HTTP outcome.
X-Forwarded-Foris believed only behind a--trusted-proxynetwork.Upload module.
raven/trajectory/upload.pycarries no user interface, so theCLI, the interactive browser and a future web page call the same functions. The
destination is one string:
DEFAULT_UPLOAD_URLas shipped (empty here, meaningno upload) overridden by
RAVEN_BUGREPORT_URL(set it empty to switch uploadingoff). A URL must be https or http on this machine unless
RAVEN_BUGREPORT_ALLOW_HTTP=1. The whole exchange runs under one deadline thatcancels the request, the response is read as raw bytes under a cap (compressed
answers refused), a receipt counts only when every field agrees with the
request, and the outcome is written into
record.json.upload.Wiring.
report-bugand the browser's Report a bug resolve the URL beforeany side effect, disclose
Will upload to <origin>on the confirmation page,upload once the package lands, and on failure keep the package and name the
retry command. The browser's Bug reports screen gains an Upload action and
raven trajectory upload-bug-report REPORT_IDsends a filed report again.Docs: a "Send and receive bug reports" section on the trajectory debugging page
(en + zh), two CONTEXT.md terms (Bug Report Receiver, Upload Receipt), CHANGELOG.
Type
Verification
uv run pytest tests/test_trajectory_package_check.py tests/test_trajectory_receiver.py tests/test_trajectory_upload.py tests/test_cli_trajectory_commands.py tests/test_cli_trajectory_browse.py tests/test_trajectory_bugreport.py tests/test_docs_site.py -q-> all pass (about 480 new tests across them)uv run pytest tests/integration/test_bug_report_upload_e2e.py -q -o addopts=""-> 3 passed (a real receiver process, a realreport-bugrun, a real retry, SIGTERM shutdown)uv run pytest tests/ -q -p no:cacheprovider-> 28620 passed, 52 failed, 144 skipped; the 52 failures are pre-existing on this machine (network/proxy/NAT64-dependent tests) and identical to themainbaselineuv run ruff check . && uv run ruff format --check .-> cleanmake lint-imports-> 10 contracts kept;make check-source-language,make check-large-files-> passmake docs-build(mkdocs--strict) -> passManual: receiver +
report-bug --yes(201), repeat (200), different content under the same id (409), the interactive browser driven through a pty, failure with the receiver down and retry withupload-bug-report, a 128 MiB upload refused with 413 after ~2 MB both directly and through an nginx container (client_max_body_size). The nginx-to-receiver hop itself could not be exercised on this machine (rootless docker cannot reach host services).Relevant tests pass locally
Relevant lint / type checks pass locally
User-facing docs or screenshots are updated when needed
Risk
Off by default: with
RAVEN_BUGREPORT_URLunset and the shipped default empty,report-bugand the browser behave exactly as before; the receiver is a separate process nobody runs by accident.The receiver authenticates nobody on purpose; the docs require a TLS proxy for anything beyond loopback and say not to expose the port publicly. Packages are redacted for credentials, not anonymized, so
--save-diris sensitive.Rollback: unset the variable and stop the receiver. The new
record.json.uploadfields are optional and older readers ignore them; the receiver's on-disk layout is plain files by day.Security impact considered
Backward compatibility considered
Rollback path is clear for risky changes
Related Issues
N/A