Skip to content

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
mainfrom
claude/gifted-pasteur-4xrelv
Draft

Deep-Sixed wants to merge 2 commits into
mainfrom
claude/gifted-pasteur-4xrelv

Conversation

@Deep-Sixed

@Deep-Sixed Deep-Sixed commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Fixes all three findings from the review of #36–#40, all in the intake code #36 added.

Head: e790142
Base: main @ d0a40f9
Two commits:

#1 — a malformed Gmail read token stopped serve

A token that could not even be constructed (a stray newline, a space, a blank --mailbox) raised ValueError out of _intake_service. So serve exited before binding. ingest of a local .eml, which needs no Gmail grant, failed the same way whenever --mailbox was 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:

Where What the operator sees
serve startup lines Error (credential_malformed): Gmail intake is off. <reason>
GET /intake/sources gmail: {"available": false, "error": "credential_malformed", "detail": "<reason>"}
Inbox "Gmail intake is off: the read credential supplied at launch is malformed…", plus the code and reason, placed as text
POST /intake/gmail 409 intake_unavailable, with the detail naming the malformed credential

EML 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.

Mutation Result
CLI stops catching the credential error FAIL — 4 tests
discovery drops error/detail FAIL — 5
Gmail command gives the generic "no credential" refusal FAIL — 4
startup error line removed FAIL
page ignores gmail.error first version of the static test passed. I checked it in Chromium, where the mutant showed "not configured". So I tightened the test to pin the branch, and it now FAILS
service accepts both a reader and a reason FAIL

The page was also checked by hand in Chromium against a real serve launched 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 gmail still stops serve with a traceback, and gmail-ingest still tracebacks on a malformed read token. Both reproduce locally. The decision given was for the read token in serve, 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/eml is 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):

  1. write the answer
  2. shut its own send side, so the client sees the whole answer and an orderly end of it at once
  3. read and discard whatever the client is still sending, bounded at 16 MiB and 5 s
  4. close

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 name connection, 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.

Mutation Result
unfixed surface — deterministic orderly-close test FAIL 20/20 runs
unfixed surface — over-ceiling full-body test (×5 per run) FAIL 10/10, plus 1 pass in an earlier 3
drain with no time bound FAIL — thread held on a silent client
drain without shutting the send side first FAIL — no early end-of-stream

#3 — gmail-ingest --limit 0 said "the mailbox could not be read"

The limit is now checked before the try that turns every failure into SourceUnreadable, against the reader's own rule. The CLI also refuses a non-positive --limit before Gmail or the database is touched. Removing either check fails its tests.

Verification on e790142

1704 tests (1696 → 1704). Also clean:

  • Ruff check and format
  • verify_tree: 100 publishable text files, 5 reviewed renders, 1 reviewed screenshot
  • verify_secrets: PASS
  • verify_wheel: PASS

Not verified: Windows locally. CI's Windows cells are the first real evidence there.

🤖 Generated with Claude Code

https://claude.ai/code/session_01B8kjhqYENShpeZ16QDnsyo

Deep-Sixed and others added 2 commits September 26, 2026 01:01
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
@Deep-Sixed Deep-Sixed changed the title Let a refusal reach the browser, and a bad limit stay the caller's Let a refusal reach the browser, a bad limit stay the caller's, and a bad read token not stop serve Sep 28, 2026
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.

1 participant