Repository navigation
Let a refusal reach the browser, a bad limit stay the caller's, and a bad read token not stop serve - #43
Draft
Deep-Sixed wants to merge 2 commits into
Draft
Let a refusal reach the browser, a bad limit stay the caller's, and a bad read token not stop serve#43Deep-Sixed wants to merge 2 commits into
Deep-Sixed wants to merge 2 commits into
Conversation
Two findings from the review of #36-#40, both in the intake code #36 added. A refusal decided on the envelope -- 413, 415, the namespace 400 -- is answered before a byte of the body is read, which is right, and then the socket was closed on whatever the client had sent, which is not. Closing on unread bytes is a reset, and a reset can overtake the response; on Windows it discards a response the client has not read yet. The page always sends the file it was given, so an operator choosing a 3 MB message could see "Failed to fetch" instead of the ceiling that refused it. The surface now settles every response the same way: write the answer, shut its own send side, then read and discard whatever the client is still sending, bounded at 16 MiB and five seconds, and only then close. Nothing about what is decided, or when, changes -- the answer is written before the drain begins and the drained bytes are never parsed, stored or kept. The socket is reached as `self.request`, socketserver's own name for it, because this package is mechanically forbidden the word `connection` and that guard is right to be. The suite could not see this. Every pre-read refusal was asserted with transmit=False, which proves the surface decides before reading and cannot prove what a browser sees, because a browser always sends the body. That was my choice in #39 and #40, and I did not say what it left untested. These tests send it: unfixed surface, deterministic orderly-close test FAIL 20/20 runs unfixed surface, over-ceiling full-body test FAIL 10/10, then 1 pass in 3 drain with no time bound FAIL (thread held) drain without shutting the send side first FAIL (no early end-of-stream) The small-body 415 and 400 full-body cases never fail on Linux; they are there for Windows, where the reset destroys the response rather than trailing it. `gmail-ingest --limit 0` reported "the mailbox could not be read". The service checked label_ids before its try and let limit fall inside it, where every failure becomes SourceUnreadable -- and only after the identity read had spent the credential. The limit is now checked where label_ids is, against the reader's own rule, and the CLI refuses a non-positive --limit before Gmail or the database is touched. service limit check removed FAIL (4 cases) CLI limit check removed FAIL Finding #1 of the review, a malformed read token stopping serve, is not in this change: whether that launch should refuse or start without Gmail is a decision still open. Gates: 1686 -> 1696 tests, ruff check and format clean, verify_tree 100/5/1, verify_secrets and verify_wheel PASS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8kjhqYENShpeZ16QDnsyo
A read token that could not even be constructed (a stray newline, a space, a blank
--mailbox) raised out of _intake_service, so `serve` stopped before binding and `ingest`
of a local .eml, which needs no Gmail grant, failed the same way.
Per the operator's decision, the launch now starts. The intake service carries the
credential's own reason instead of a reader: /intake/sources reports gmail
{"available": false, "error": "credential_malformed", "detail": ...}, serve prints an
"Error (credential_malformed)" line at startup, the Inbox shows the reason where the
Gmail control would be, and /intake/gmail answers 409 intake_unavailable naming it.
EML intake and decisions are unaffected. The reason is the credential class's message,
which never repeats the token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8kjhqYENShpeZ16QDnsyo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes all three findings from the review of #36–#40, all in the intake code #36 added.
Head:
e790142Base:
main @ d0a40f9Two commits:
eb349f7covers Bound fresh-database WAL negotiation under concurrent first start #2 and Canonicalize equivalent job URLs for stable deduplication #3.e790142covers Extract inbound recruiting messages with durable provenance #1, as decided: start, and report the error after startup.#1 — a malformed Gmail read token stopped
serveA token that could not even be constructed (a stray newline, a space, a blank
--mailbox) raisedValueErrorout of_intake_service. Soserveexited before binding.ingestof a local.eml, which needs no Gmail grant, failed the same way whenever--mailboxwas given.The decision was: start, with an error once running. So the launch now starts, and the service carries the credential's own reason in place of a reader:
servestartup linesError (credential_malformed): Gmail intake is off. <reason>GET /intake/sourcesgmail: {"available": false, "error": "credential_malformed", "detail": "<reason>"}POST /intake/gmail409 intake_unavailable, with the detail naming the malformed credentialEML intake, decisions and drafting are unaffected. The reason is the credential class's own message, which never repeats the token. The tests assert that no fragment of the token reaches discovery or the refusal. A launch that simply has no read token gets no error line: not configuring Gmail is a choice, not a mistake.
error/detailgmail.errorThe page was also checked by hand in Chromium against a real
servelaunched with a malformed token: the error line printed, and the Inbox showed the code and reason. That check is not part of the suite; the repo has no browser runner in CI.Not changed, same shape: a malformed compose token (
CAREERSIGNAL_GMAIL_COMPOSE_TOKEN) with--provider gmailstill stopsservewith a traceback, andgmail-ingeststill tracebacks on a malformed read token. Both reproduce locally. The decision given was for the read token inserve, so they're left for a separate call.#2 — a refusal made before reading reached the browser as a network failure
A 413, 415 or namespace 400 on
/intake/emlis decided on the envelope, before a byte of the body is read. That part is right. The socket was then closed with whatever the client had sent still unread, and that is a reset. A reset can overtake the response, and on Windows it discards a response the client hasn't read yet. The page always sends the file it was given, so an operator choosing a 3 MB message could see "Failed to fetch" instead of the ceiling that refused it.The surface now ends every response the same way (
_settle):What is decided, and when, is unchanged. The answer is written before the drain begins, and drained bytes are never parsed, stored or kept. The socket is reached as
self.request, because the package's guard forbids the nameconnection, and I chose not to weaken it.Every pre-read refusal was previously asserted with
transmit=False. That proves the surface decides before reading, but it can't show what a browser sees, because a browser always sends the body. The new tests send the body.#3 —
gmail-ingest --limit 0said "the mailbox could not be read"The limit is now checked before the
trythat turns every failure intoSourceUnreadable, against the reader's own rule. The CLI also refuses a non-positive--limitbefore Gmail or the database is touched. Removing either check fails its tests.Verification on
e7901421704 tests (1696 → 1704). Also clean:
verify_tree: 100 publishable text files, 5 reviewed renders, 1 reviewed screenshotverify_secrets: PASSverify_wheel: PASSNot verified: Windows locally. CI's Windows cells are the first real evidence there.
🤖 Generated with Claude Code
https://claude.ai/code/session_01B8kjhqYENShpeZ16QDnsyo