Skip to content

feat: refresh __Secure-1PSIDTS so the session survives past an hour - #1

Merged
cheungringo420 merged 2 commits into
pr-80from
feat/auto-refresh-1psidts
Aug 19, 2026
Merged

feat: refresh __Secure-1PSIDTS so the session survives past an hour#1
cheungringo420 merged 2 commits into
pr-80from
feat/auto-refresh-1psidts

Conversation

@cheungringo420

@cheungringo420 cheungringo420 commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Based on pr-80 (upstream PR Sophomoresty#80 head, 434cb65) — the branch this deployment actually runs.

Problem

A cookie file written once goes stale in roughly an hour. Google rotates __Secure-1PSIDTS and stops honouring the previous value; a browser keeps up because it keeps calling accounts.google.com/RotateCookies, but a file-backed jar never does.

The failure is easy to misread, which is what made it expensive to diagnose:

  • anonymous Gemini still answers text prompts, so the text path looks healthy
  • content-push.googleapis.com accepts uploads with no credentials at all (verified: zero cookies, bogus X-Tenant-Id — still returns a /contrib_service/ttl_1d/... ref), so Image uploaded: in the log proves nothing

Only the account-only work fails, and it surfaces as BardErrorInfo [1100] on image input rather than as anything auth-shaped.

Why not passive Set-Cookie capture

Measured against the live endpoint — Gemini's own replies carry only NID, never a rotated __Secure-1PSIDTS:

[GET /app (urllib)]               Set-Cookie count=1  -> NID
[POST StreamGenerate (curl_cffi)] Set-Cookie count=1  -> NID

So the rotation endpoint has to be called explicitly.

Change

A daemon thread polls accounts.google.com/RotateCookies every cookie_refresh_interval_sec (default 540s; the endpoint's own hint in its 401 body is 600) and merges any new value back into cookie_file.

  • writes back only __Secure-1PSIDTS / __Secure-3PSIDTS, leaving long-lived cookies alone
  • re-reads the file under a lock before merging, so a concurrent write from a cookie-sync browser extension is merged into rather than overwritten
  • atomic os.replace where possible, falling back to an in-place rewrite on EBUSY — a bind-mounted cookie file cannot be replaced, and that is the normal Docker layout
  • the temp file holds a full copy of the session, so it is removed on every path including failures
  • write_failed is reported distinctly from unchanged, with a hint, so a read-only cookie file cannot look like "nothing to do"
  • updates gemini.py's mtime-keyed cache so in-flight requests pick the new value up
  • 401 leaves the file byte-for-byte unchanged and backs the loop off ×4
  • handles both the JSON and plain-cookie-string file formats
  • cookie_refresh_enabled: false to opt out; never creates the file

Verification

  • 78 unit tests pass (54 existing + 24 new) — python -m unittest discover -s tests
  • ruff check clean on every changed file (the remaining E722 is pre-existing in server.py, untouched)

Live against Google from the deployment VM, signed-in session — success path:

before: ...51ZNoXeA5FZan6zCNGYEAA
rotate -> True __Secure-1PSIDTS,__Secure-3PSIDTS
after : ...ZdqCoiiO_Xw-GHOnrPqEAA
VALUE ROTATED: True     temp files: none     mode: 0o600
cache serves persisted value: True     cookies intact: 11 | sapisid: True

Live — signed-out session: rotate -> False unauthorized, file byte-identical, no temp file.

Live — read-only bind mount: rotate -> False write_failed with the hint, file byte-identical, no temp file.

The first three of those behaviours were found by live testing: the initial cut failed with EBUSY on the bind mount, left a full copy of the session in the directory, and reported the failure as unchanged. Fixed in the second commit.

Deployment note

docker-compose.yml mounts the auth file read-only:

- ./gemini-auth.json:/app/gemini-auth.json:ro

Drop the :ro or the refresher can never persist anything — it will log write_failed every cycle and the session will still expire.

Rate limiting

RotateCookies returns 429 if called repeatedly in quick succession, which is worth knowing before lowering cookie_refresh_interval_sec.

🤖 Generated with Claude Code

Google rotates __Secure-1PSIDTS and stops honouring the previous value, so a
cookie file that is written once goes stale within roughly an hour and the
session silently drops to logged-out.

The failure is easy to misread. Anonymous Gemini still answers text prompts and
content-push.googleapis.com accepts uploads with no credentials at all, so both
of those paths keep looking healthy; only the account-only work fails, surfacing
as BardErrorInfo [1100] on image input rather than as an auth error.

Watching Set-Cookie on StreamGenerate replies does not help — those carry only
NID, never a rotated __Secure-1PSIDTS. The rotation endpoint has to be called
explicitly, so a daemon thread polls accounts.google.com/RotateCookies every
cookie_refresh_interval_sec (default 540s; the endpoint's own hint is 600) and
merges any new value back into cookie_file.

- writes only __Secure-1PSIDTS / __Secure-3PSIDTS, leaving the long-lived
  cookies in the jar alone
- re-reads the file under a lock before merging, so a concurrent write from a
  cookie-sync browser extension is merged into rather than overwritten
- writes through a temp file + os.replace at mode 600, so a crash cannot leave
  a truncated session file
- updates gemini.py's mtime-keyed cache so in-flight requests see the new value
- a 401 (signed out) leaves the file byte-for-byte unchanged and backs the loop
  off instead of hammering the endpoint
- supports both the JSON and plain-cookie-string file formats
- opt out with cookie_refresh_enabled=false when an external tool owns the file

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 19, 2026 06:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…a copy

Live-testing against the real deployment (Docker, cookie file bind-mounted)
turned up three defects in the first cut:

- os.replace() cannot swap a bind-mounted file -- the mount point is busy --
  so every rotation failed with EBUSY on the exact layout this is meant for.
  Fall back to rewriting the existing inode when replace reports EBUSY/EXDEV.
- the temp file was left behind whenever replace failed, so a full copy of the
  session sat in the directory unreferenced. Remove it on every path.
- that failure was reported as "unchanged", making a cookie file the process
  cannot write look like there was simply nothing to do. persist_rotated_cookies
  now returns an explicit status and a read-only file says so, with a hint.

Verified against Google from the deployment VM: the read-only mount now reports
`write_failed` with the hint, leaves the file byte-identical, and leaves no temp
file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants