Skip to content

Self-host the automations on your own box (no gcloud/Cloud Run/GCS) - #36

Merged
krMaynard merged 4 commits into
mainfrom
claude/selfhost-availability-sync
Jul 22, 2026
Merged

Self-host the automations on your own box (no gcloud/Cloud Run/GCS)#36
krMaynard merged 4 commits into
mainfrom
claude/selfhost-availability-sync

Conversation

@krMaynard

Copy link
Copy Markdown
Owner

What & why

Picks up the merged availability-sync feature (#34) and makes it runnable on an always-on box without gcloud/Cloud Run, per the request to "use my box as the server."

The deploy migration (#35) is red because CI hard-fails the paid Gemini eval tests without a GEMINI_API_KEY secret, and the Cloud Run deploy also needs WIF/vars that aren't configured. Self-hosting sidesteps all of that: main.py already uses file-based OAuth when GOOGLE_CREDENTIALS_FILE is set, and /availability-sync only touches the Calendar API (no GCS, no Gemini).

Changes

  • deploy/selfhost/assistant.service (Flask server), availability-sync.service + .timer (local Cloud Scheduler, every 30 min), env.example, and a step-by-step guide.
  • mint_token.py — one-time OAuth consent → token.json; the service refreshes silently afterward and never opens a browser (key for a headless box).
  • README — "Run it on your own box" section.

No changes to app code — this is purely the run/operate path.

Verified on the host (km-tc)

  • .venv built on Python 3.14; pip install -r requirements.txt clean.
  • python -m pytest tests/19 passed, 8 skipped (the availability suite is fully green; the Gemini evals skip without a key locally).
  • assistant.service active on :8080; availability-sync.timer enabled, next run scheduled; the timer→curl→endpoint chain fires and returns the designed 400 (AVAILABILITY_CALENDAR_ID not configured) — no hang.

Remaining (owner-only, documented in the guide)

  1. Create a Desktop-app OAuth client → credentials.json; run mint_token.py (or reuse the existing token from the GCS bucket).
  2. Create an "Availability" calendar, share it read-only, set AVAILABILITY_CALENDAR_ID in .env.

Then the 30-min sync goes green automatically.

🤖 Generated with Claude Code

Lets the automation run on any always-on box without gcloud/Cloud Run:
main.py already uses file-based OAuth when GOOGLE_CREDENTIALS_FILE is set,
and /availability-sync only calls the Calendar API (no GCS/Gemini).

- deploy/selfhost/: assistant.service (Flask server) + availability-sync
  .service/.timer (local Cloud Scheduler, every 30 min) + env.example + guide
- mint_token.py: one-time OAuth consent -> token.json; the service then
  refreshes silently and never opens a browser
- README: "Run it on your own box" section

Verified on the host: server active on :8080, timer scheduled, the sync
endpoint returns its clean 400 until AVAILABILITY_CALENDAR_ID + creds are set.
Tests: 19 passed, 8 skipped (Gemini evals skip without a key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wz2GRBVwpwfUkDwoZUyCy6
@krMaynard

Copy link
Copy Markdown
Owner Author

Self-review

Checked

  • pyflakes-equivalent: python -c "import main" and import mint_token both clean; no unused imports added.
  • mint_token.py reuses the existing assistant.auth.get_google_credentials rather than duplicating the flow, and pre-checks the client-secret path with a clear error.
  • systemd units use %h (portable), EnvironmentFile=.env (gitignored), and Restart=on-failure. Timer uses OnUnitActiveSec=30min + Persistent=true.
  • No secrets committed — .env, token.json, credentials.json are already in .gitignore; git status confirms only README/deploy/mint_token staged.
  • Tests: 19 passed, 8 skipped locally.

Known rough edge (documented, not a blocker)

  • Until AVAILABILITY_CALENDAR_ID + creds are set, each 30-min timer run logs a curl (22) 400 "failed" line. That's intentional (curl -f surfaces real errors later); it self-clears once config is filled. Happy to switch the probe to tolerate the pre-config 400 if you'd prefer a clean journal in the meantime.
  • Footgun guarded by docs: a headless service would hang on the interactive consent flow if AVAILABILITY_CALENDAR_ID is set but token.json is missing — the guide sequences token-minting before enabling the timer. Could add a route-level "no token → 503" guard in a follow-up if you want defense-in-depth.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces self-hosting capabilities, allowing the automation tool to run locally using systemd units and file-based OAuth instead of relying on Google Cloud Run and Cloud Scheduler. The feedback focuses on improving the portability, robustness, and security of this self-hosted setup. Key recommendations include using relative paths in the environment template to avoid hardcoded home directories, dynamically referencing the configured port in the systemd sync service with added curl retries, making the systemd environment file optional, and securing OAuth token files with restricted permissions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread deploy/selfhost/env.example Outdated
Comment thread deploy/selfhost/availability-sync.service Outdated
Comment thread deploy/selfhost/assistant.service Outdated
Comment thread deploy/selfhost/README.md
Extends the self-host path so email processing runs on the box too, with
zero recurring cloud cost (no Cloud Run / Scheduler / GCS — only Gemini
usage, per new email).

- state.py: lazy GCS import + local-file backends for user_context and
  last_standup_date (processed_ids already had one); StateStore selector —
  GCS_BUCKET_NAME wins (Cloud Run unchanged), else STATE_DIR local files.
- main.py: use the state store instead of hard-requiring GCS_BUCKET_NAME.
- auth.py: OAUTH_NONINTERACTIVE guard so a headless service fails loudly
  instead of hanging on the browser consent; lazy storage import.
- deploy/selfhost: email-processing .service/.timer (every 5 min);
  assistant.service sets OAUTH_NONINTERACTIVE=1; env.example ships cheapest
  Gemini defaults (ENABLE_ACTION_ITEMS=false, lite model) + STATE_DIR.
- tests/test_state.py: local backends + selector (8 tests).

Verified on the host: local state works with the GCS SDK import blocked;
server restarted; /availability-sync -> clean 400, / -> clean 500 via the
noninteractive guard (no hang). Tests: 27 passed, 8 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wz2GRBVwpwfUkDwoZUyCy6
@krMaynard

Copy link
Copy Markdown
Owner Author

Self-review (round 2 — local state + email timer)

Checked

  • Imports are side-effect-free (import main, mint_token, assistant.auth, assistant.state clean; mint_token guarded under __main__).
  • GCS-free proof: reloaded assistant.state with google.cloud.* imports blocked — LocalStateStore round-trips processed_ids + user_context fine. So google-cloud-storage is genuinely optional locally.
  • Non-breaking: get_state_store() returns GcsStateStore whenever GCS_BUCKET_NAME is set, so Cloud Run behavior is identical; main.py's only remaining GCS_BUCKET_NAME ref is the credentials fallback, which local mode skips.
  • No-hang proof: POST / with no token returned 500 (OAUTH_NONINTERACTIVE) in <1s, not a hang; server log shows the explicit RuntimeError.
  • Atomic local writes (temp + os.replace), dir auto-created, corrupt files degrade to empty. state/ gitignored; no secrets committed.
  • Tests: 27 passed, 8 skipped.

Notes / follow-ups

  • GEMINI_MODEL=gemini-2.0-flash-lite is the cheap default for analyze_email (it's the model already used for the context call). If classification quality dips on tricky mail, bump to gemini-3.5-flash — it's a one-line env change.
  • Standup is intentionally out of scope; its route + local backend still work if you enable a timer later.
  • Pre-config timer runs log expected non-zero curl exits (400/500) until creds are in place — self-clears once configured.

@krMaynard krMaynard changed the title Self-host path for availability sync (systemd, no gcloud) Self-host the automations on your own box (no gcloud/Cloud Run/GCS) Jul 13, 2026
…ken perms

- env.example: relative paths (no hardcoded /home/km) — resolve against the
  service WorkingDirectory.
- availability-sync/email-processing services: Environment=PORT=8080 default +
  EnvironmentFile so curl targets ${PORT}; add --retry/--retry-connrefused to
  ride out server startup.
- all units: EnvironmentFile prefixed '-' so a missing .env doesn't hard-fail.
- auth.py: chmod token.json 0600 after writing (owner-only refresh token);
  guide adds `chmod 600 credentials.json token.json` and uses a generic host.

Verified: tests 27 passed / 8 skipped; units parse; timer-triggered
availability-sync reached :8080 via ${PORT} and returned the expected 400.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wz2GRBVwpwfUkDwoZUyCy6
@krMaynard
krMaynard marked this pull request as ready for review July 17, 2026 16:45
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@krMaynard krMaynard added the codex-review Queued for automated Codex review label Jul 18, 2026
@krMaynard

Copy link
Copy Markdown
Owner Author

Codex review

Found and fixed one self-hosting configuration bug: systemd EnvironmentFile syntax only ignores lines that start with #; trailing comment text is preserved inside an unquoted value. The copied sample would therefore corrupt GEMINI_API_KEY and CALENDAR_ID. Commit 047c53d moves those explanations to standalone comment lines.

Validation: full offline suite — 27 passed, 8 skipped; no remaining inline-comment assignments; git diff --check passed. The state backend uses atomic owner-only temporary files, the OAuth token is chmod 0600, headless OAuth fails instead of hanging, and all prior review threads are resolved.

@krMaynard krMaynard added codex-reviewed Latest revision reviewed by Codex and removed codex-review Queued for automated Codex review labels Jul 18, 2026
@krMaynard
krMaynard merged commit 7b492a4 into main Jul 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex-reviewed Latest revision reviewed by Codex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant