Conversation
The documented production entrypoint called app.run(..., debug=True), which enables the Werkzeug interactive debugger and full stack traces for anyone who can reach the port — a remote code execution risk. Debug is now opt-in via the FLASK_DEBUG env var (truthy values only) and is forced off whenever FLASK_ENV=production, so it defaults to OFF. This matches the existing truthy-parsing and Config.is_production() patterns in config.py. host/port/threaded behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
complete_section_run() stamped sections.last_successful_sync_at with only a `WHERE section_id = ?` guard. A zombie worker whose lease had been stolen (its section_sync_runs UPDATE matches 0 rows because owner_token/status no longer match) would still mark the section as freshly synced, wrongly suppressing re-sync for a full interval and causing tombstone flapping. Gate the sections update on the rowcount of the lease-owner-guarded section_sync_runs UPDATE: if 0 rows were affected, the lease was lost, so skip the sections stamp (the section correctly stays due) and log a warning. The happy path for the legitimate owner is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_download_schoology_binary() fetched attachment/download URLs taken from scraped Schoology data with requests.get(..., allow_redirects=True) and no validation, letting a malicious URL point the server at internal services (localhost, RFC1918 hosts, cloud metadata at 169.254.169.254, file:// etc.) and surface the response bytes to students. Fix: - Add validate_outbound_url(): allows only http/https, resolves the hostname and rejects any URL whose IPs are non-public (loopback, RFC1918, link-local, CGNAT, multicast, reserved — IPv4 and IPv6), or that fails to resolve. - Follow redirects manually (allow_redirects=False, bounded at 5 hops) and re-validate every hop, since Schoology download URLs redirect to presigned CDN/S3 hosts and a fixed host allowlist would break them. - Only attach OAuth credentials when the hop is on our own Schoology API host, so credentials can never be replayed to redirect targets. Existing timeout and error handling are preserved; Google Drive links are unaffected (they go through the Drive API, not this fetch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… via Convex The /api/chat/generations/<id>/events endpoint authorized live streaming only by comparing the Redis snapshot's userId, and that check failed open: when the snapshot was missing or its userId was unset, any authenticated user could stream another user's generation. The internal enqueue endpoint seeded the snapshot without a user_id, so every generation had a race window between enqueue and worker startup where the snapshot carried no owner at all. Fix (authoritative, fail-closed): - Add a lightweight getGenerationOwner query through the existing Convex chat bridge (chatBridge.ts -> chatInternal.ts) that returns the owning userId for a generation, using ctx.db.normalizeId so malformed ids resolve to null instead of throwing. - At SSE connect time, backend/api/routes.py now resolves ownership from Convex (the source of truth for chatGenerations) and rejects with 404 generation_not_found unless the caller's user id matches. Lookup errors return 503 rather than allowing the stream (no fail-open path remains). Defense in depth: - kickoffBackendGeneration now passes the owner's userId to the internal enqueue endpoint, and internal_chat/routes.py seeds the Redis snapshot with it, so the snapshot carries ownership from queue time onward. - Annotate kickoffBackendGeneration's handler return type to break the TS type-inference cycle introduced by the new same-module internal.* reference (same pattern chatBridge.ts already uses). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three defects in the chat page (frontend/src/app/dashboard/chat/page.tsx)
plus a supporting Convex query:
1. Typed message destroyed on send failure. The composer input was cleared
before the sendMessage mutation resolved, so a rejected send silently
discarded what the user typed. The input is now cleared only after the
mutation succeeds; on failure the text is preserved and an inline error
("Couldn't send your message. Please try again.") is surfaced. The error
clears when the user edits the input.
2. Transient loading/auth blip rendered a permanent dead-end. The page treated
any non-entitled result the same as a still-loading one. `threads` is
`undefined` while loading (or before Convex auth is ready) and `null` only
when genuinely not entitled; these are now distinguished. A loading state is
shown while data is pending so the terminal "Chat isn't available" screen
only appears for real non-entitlement.
3. Tool-call state lost on SSE reconnect. The SSE stream only replays tool_call
events going forward, so on a reconnect (e.g. remount) already-emitted tool
calls were dropped from local state. Added a `listToolCalls` Convex query
(reads the durable chatToolCalls table for the active generation) and
hydrate streaming state from it — live SSE updates win for known calls,
persisted rows fill in any the client is missing.
Verified: `tsc --noEmit` passes clean on the frontend. Full Next.js build not
run (slow); changes eyeballed against existing React/Convex patterns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uth-ready latch - Clear sendError when switching threads or starting a new chat so a stale failure pill doesn't carry into a fresh context - Render the Stop-generating button and send-error pill in one stacked container so they can't overlap at the same fixed position - Let a persisted tool call that reached a terminal status override a stale non-terminal local entry (SSE stream died mid-generation without remount) - Let a successful Convex token refetch flip authReady to true after an earlier transient failure, instead of latching false forever Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MAIN_DB_PATH and SESSIONS_DB_PATH were bare cwd-relative strings
("main.db" / "api_sessions.db"). Because sqlite3.connect resolves
relative paths against the current working directory, launching the
app from any directory other than backend/ would silently create or
open a DIFFERENT database file, losing users and sessions.
Anchor both to BACKEND_ROOT (Path(__file__).resolve().parent) with an
optional env override, matching the existing SCRAPER_DB_PATH pattern
one line below.
Behavior-preserving: both constants are only ever passed to
get_conn / sqlite3.connect (db/sessions.py, db/tokens.py, db/users.py,
db/mobile.py, db/job_leases.py, db/init.py); nothing relies on them
being relative. Since the app is launched from backend/, BACKEND_ROOT
resolves to backend/, so backend/main.db and backend/api_sessions.db
(the existing files) remain the targets. The change only removes the
cwd fragility.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
[AI/Fable 5] Disable Flask debug mode by default (gate behind FLASK_DEBUG)
…ship [AI/Fable 5] Guard section-sync completion by lease ownership
…ssrf [AI/Fable 5] Harden scraper attachment downloads against SSRF
…ship [AI/Fable 5] Fix IDOR in chat SSE stream: verify generation ownership via Convex
[AI/Fable 5] Fix three chat page reliability bugs
… tokens
First automated tests for the backend (60 tests, all passing). Coverage is
deliberately limited to Convex-independent modules so the suite survives the
in-progress Convex removal:
- db/encryption.py: Fernet roundtrip, tamper/garbage/cross-key rejection,
non-determinism, plus the SHA-256 request-token lookup hash (pinned).
- auth/jwt_utils.py: RS256 mint/verify roundtrip, expiry, tampered
signature/payload, wrong audience/issuer, alg=none, attacker-key forgery,
unique jti, JWKS consistency with the signing key.
- db/job_leases.py: Schoology refresh lease acquire/contention/expiry-steal
and the owner_token guard on release.
- services/scraper/store.py: section sync run leases — acquire, heartbeat
extension, stale steal (old run failed, attempt count carried), and that a
superseded owner can no longer heartbeat/complete/fail the active run.
- db/mobile.py: refresh-token rotation, reuse detection revoking the device
token family, expiry/device-mismatch rejection, single-use auth codes and
web session tickets.
Run with:
cd backend && pip install -r requirements-test.txt && python -m pytest
Tests are hermetic: conftest provisions throwaway Fernet/RSA keys via env
vars before importing app modules, and all SQLite DBs are per-test tmp files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ript (P1+P7) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ard main/sessions DB paths - tests/conftest.py executed twice (as `conftest` via pytest and as `tests.conftest` via the explicit import in test_jwt_utils.py), and the second pass regenerated every secret and overwrote os.environ after app modules had already captured the first-pass values. Make all secret provisioning idempotent so both module identities agree. This also fixes test_wrong_issuer_rejected, which meant to sign with the real key (issuer being the only invalid claim) but was actually signing with an unrelated regenerated key, passing for the wrong reason. - test_module_key_matches_configured_key now asserts against the actual ENCRYPTION_KEY env var instead of the module's own re-exported constant (the tautological form was masking the desync above). - conftest now also points MAIN_DB_PATH/SESSIONS_DB_PATH at a nonexistent sentinel, mirroring the SCRAPER_DB_PATH guard, so a future test that forgets the main_db fixture fails loudly instead of writing a real main.db into the cwd. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the chatInternal.ts state machine (transition guards, idempotency, COALESCE fallbacks, stale-generation reaper) to backend/db/chat_store.py, user app-state to app_users.py, and schoologyCache.ts diff/tombstone logic to schoology_cache_store.py, with a permanent smoke gate in scripts/smoke_stores.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the repository automation this public repo was missing entirely: - CI: ci-frontend (typecheck hard gate; lint/build non-blocking ratchets) and backend (ruff check + byte-compile) on PRs to main - Security scanning: CodeQL (JS/TS + Python, security-extended) and bandit SAST (SARIF -> Security tab) - Dependabot: weekly pnpm (/frontend), pip (/backend), and github-actions updates, grouped minor+patch - Governance: LICENSE (MIT), SECURITY.md with private-advisory reporting, PR template with an IDOR/SSRF/authz security checklist, CODEOWNERS, issue templates, CONTRIBUTING.md - Tooling config: backend/ruff.toml (F,I select), requirements-dev.txt, root .pre-commit-config.yaml (ruff + detect-private-key + large-file guard) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`pnpm lint` was hard-crashing (TypeError: Converting circular structure to JSON) because eslint.config.mjs pulled next/core-web-vitals via the legacy FlatCompat shim, which chokes on eslint-plugin-react-hooks@7's circular plugin object. Switch to eslint-config-next's native flat exports so eslint runs again. Also add a `typecheck` script (tsc --noEmit) — the CI hard gate — and pin packageManager to pnpm@10.30.3 plus a .nvmrc (node 22). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…igns Applies ruff's safe autofixes (import sorting/pruning across ~30 files) so the new `ruff check` CI gate is green, and fixes the non-autofixable findings it surfaced: - services/chat/service.py: `list[dict[str, Any]]` used with no import of `Any` (F821) — a latent runtime NameError. Add `from typing import Any`. - api/routes.py: drop two unused `result =` assignments (kept the calls). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First tests in the repo, targeting the security-sensitive pure functions behind the recent fix/* branches: - test_encryption.py: token encrypt/decrypt round-trip; fails closed on tampered/garbage ciphertext - test_middleware.py: @auth_required returns 401 without a valid session, injects the user with one - test_jwt.py: audience/issuer/exp enforcement; rejects alg=none and tampered signatures conftest.py sets a generated Fernet key + required env before import so runs are hermetic. 17 tests, all passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rypt/encrypt
Strengthen backend/tests/test_encryption.py from characterization toward
specification for db/encryption.py. Adds side-effect assertions that
distinguish the empty-input guard from the exception fallback: both return
None, so return-value checks alone let the guard-removal mutant survive. The
new tests pin that ""/None short-circuit silently while genuinely invalid
input flows through (and logs from) the except branch, plus a symmetric
encrypt("")->None guard check.
Verified by manual mutation: removing the decrypt guard, changing the
error-log string, and removing the encrypt guard each now turn a test RED
(all three previously survived).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set up mutmut (>= 3.6) so the team can run mutation tests on the backend security modules going forward. - Add mutmut to backend/requirements-test.txt. - Configure [tool.mutmut] in backend/pyproject.toml. mutmut 3.x reads this section from pyproject.toml and is pytest-native (no free-form runner string), so source_paths pins the five tested modules and pytest_add_cli_args=["-q"] is the equivalent of `python -m pytest -q`. also_copy lists the first-party imports (config.py, db/pool.py, db/init.py and package __init__.py files) the isolated ./mutants/ run needs. - Add backend/tests/README.md with the exact setup / test / mutation commands. - git-ignore mutmut build artifacts (backend/mutants/, .mutmut-cache, html/). Verified end-to-end: `mutmut run "db.encryption.*"` mutates all five modules and executes (4 killed / 1 survived on encryption). Full suite: 60 passed. No app/source or Convex files touched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… level Shift test_scraper_section_leases.py from characterization toward specification: add owner-guard, boundary, timestamp and attempt-count assertions verified to go RED under hand-applied mutations, and add two strict-xfail regression guards proving complete_section_run stamps the sections table without an owner check (the bug fixed by unmerged PR #2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shift the job-lease suite from characterization toward specification and
close the gaps mutation testing exposed in db/job_leases.py.
Mutation score (mutmut, module-scoped): 40/47 killed -> 46/47 killed.
The single remaining survivor is an EQUIVALENT mutant ("BEGIN IMMEDIATE"
-> "begin immediate"; SQLite SQL is case-insensitive) and is unkillable.
Added assertions:
- Exact expiry boundary: acquire at now+TTL succeeds (strict `>` guard),
and one second before expiry is refused -> kills the `> -> >=` off-by-one
and pins the expiry to exactly now+TTL.
- Write side effects: a successful acquire persists THIS owner's token and
started_at/lease_expires_at; a steal overwrites the owner and re-stamps
the expiry (direct DB-row inspection).
- Release guard, direct assertions: release by owner deletes the row;
release by a non-owner leaves the row intact with the original token
(the owner_token WHERE predicate); release is scoped to the target user.
- Time helpers as spec: to_db_time normalizes non-UTC input to a UTC
offset; parse_db_time returns None for empty, treats a naive string as
UTC, and converts an offset-bearing string to UTC (asserting zero offset,
not just the instant) -> kills the astimezone(None) and tzinfo-skip
mutants.
Suite green: 19/19 in test_job_leases.py, 70/70 full backend suite.
No source code touched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shift auth/jwt_utils.py tests from characterization to specification. Manual mutation analysis (mutmut 3.6 could not be configured in this split-module layout — it copies only the mutated path into mutants/, breaking config/tests.conftest imports and mutating 0 files). Verified 12 mutants across every category are now killed: - verify: HS/RS alg confusion (HMAC-with-public-key), algorithms list widening, missing aud/iss claim enforcement, expiry boundary - create_token: audience->TTL branch, Convex 24h constant, str(sub), typ header constant, extra_claims merge guard, explicit-TTL override - JWKS/_int_to_base64url: minimal big-endian byte-length (off-by-one leading-zero mutants), unpadded base64url, e==65537 - _load_env_key_pair: both-or-neither PEM guard (and/or logic) 38 assertions added (32 -> 70). Full backend suite green (84 passed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chat generation now writes directly to SQLite via chat_store (launcher replaces the internal_chat blueprint, reaper thread replaces the Convex cron); schoology/user/auth routes read and write local stores; the frontend replaces Convex reactive queries with useLiveQuery over the /api/events SSE channel; chat page keeps streamed text on terminal instead of waiting for a reactive re-push. Deletes frontend/convex/, the bridge layers, and the aud=convex JWT surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shift backend/tests/test_mobile_tokens.py from characterization to specification for db/mobile.py. Mutation score (mutmut, module-scoped) rose from 205/388 to 369/388 killed; the 154 "no-coverage" mutants are now all covered, and the remaining 19 survivors are equivalent mutants (SQLite case-insensitive SQL, the unreachable rowcount!=1 race branch, and the limit=100-vs-101 default). Added coverage for the security side effects, not just the happy path: - rotation stamps (issued/expires/last_used) on both old and new tokens - reuse family revocation is scoped to the compromised user+device only - reuse of a revoked+expired token is invalid, not a reuse signal - exact-instant expiry boundaries for tokens, codes, tickets, oauth reqs - COALESCE idempotency across all revoke paths (first stamp preserved) - device re-registration clears revoked_at (un-revoke), created_at frozen - Schoology oauth request single-use/expiry, notification-event outbox (sorted payload, availability window, limit floor, corrupt-payload fallback, pending-only state transitions, error truncation) No source code changed; full suite green (98 passed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CodeQL flagged a log-injection sink: request.args was logged verbatim at DEBUG, letting an attacker forge log lines via CR/LF and leaking the OAuth code/state into logs. Log only sanitized key names instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_to_cache (P8) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #2 (fix/scraper-lease-ownership) is already merged into dev, so the strict markers would turn into XPASS suite failures the moment this branch integrates there (verified on a dev trial-merge: 2 failed). Non-strict keeps both trees green; delete the markers once the branches converge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cryption tests, drop superseded test_jwt.py The baseline suite (PR #7) and this branch both added conftest.py and test_encryption.py. The baseline versions are strictly stronger (idempotent secret provisioning, MAIN/SESSIONS_DB_PATH sentinel guards, env-var key binding), so they win. test_jwt.py is superseded by the baseline's test_jwt_utils.py, which covers the same surface without the wall-clock sleep; test_middleware.py is unique to this branch and stays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The backend workflow lint+compile gates never executed the tests this PR ships. Install requirements.txt + requirements-test.txt and run pytest as a hard gate, and also trigger on pushes to dev (the integration branch). Import-sort the baseline test files so the merged tree passes its own ruff gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…c and polling - Order messages by insertion (rowid) so a turn's user prompt always precedes its assistant reply; migration sorts by _creationTime - Publish chat.thread.updated on existing-thread send so the sidebar reorders instantly in every tab - Preserve partial streamed text when the reaper terminates a stale generation; skip the write lock on idle reaper ticks - Dedupe the optimistic user bubble against its own message event; resync messages and active-generation state after SSE reconnect - Batch N+1 course queries; stop background-tab polling and double focus refetches; drop stale convex dep from nested requirements Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
[AI/Fable 5] Anchor main/sessions DB paths to backend root
[AI/Fable 5] Add first automated-test baseline: encryption, JWT, job leases, mobile tokens
[AI/Opus 4.8] Strengthen encryption tests: kill guard & side-effect mutants
[AI/Opus 4.8] test: reproducible mutmut mutation-testing tooling
…r-store [AI/Opus 4.8] Strengthen scraper section-lease tests (mutation-guided, +1 real bug guarded)
…ases [AI/Opus 4.8] Strengthen job_leases tests to kill mutation survivors
[AI/Opus 4.8] Strengthen jwt_utils tests: kill JWT verification & JWKS mutants
[AI/Opus 4.8] Strengthen mobile token tests (mutation 205→369 killed)
# Conflicts: # backend/api/routes.py # backend/services/schoology/client.py # backend/tests/test_mobile_tokens.py # backend/tests/test_scraper_section_leases.py
The Redis-snapshot-only check fails open when the snapshot is absent (LRU eviction, post-terminal state), streaming a generation to any authenticated user. Check the authoritative owner in chat.db before streaming — restores the fail-closed guarantee PR #4 provided via Convex, now against the new SQLite source of truth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repository hardening: CI, security scanning, and governance
Integrate the Convex removal with everything on dev (#1-#14). Resolutions: - api/routes.py: keep #15's fail-closed SQLite ownership check (restores PR #4's IDOR guarantee against the new source of truth). - services/schoology/client.py: graft PR #3's full SSRF hardening (validate_outbound_url + per-hop re-validating redirect loop) onto #15's rewritten client, which only had host-scoped auth. - auth/routes.py: keep PR #8's log-injection fix (log arg keys, not values). - Convex modules/frontend deleted by #15 (convex_sync, internal_chat, convex/, ConvexClientProvider): keep deleted. - chat/page.tsx: take #15's SSE rewrite, which already re-implements PR #5's input-preservation and loading-vs-not-entitled behavior. Tool-call rehydration on reconnect remains a known follow-up (needs an SSE-era endpoint). - tests/test_jwt_utils.py: drop tests for the Convex JWT audience and JWKS document #15 removed; algorithm-confusion coverage retargeted to the mobile audience, not deleted. Verified on the merged tree: ruff clean, 142 passed + 2 xpassed, smoke_stores 36/36, app imports clean, frontend tsc --noEmit clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These files predate the ruff config #8 added, so CI's ruff gate flagged them once the branch was merged with dev. Pure formatting — import sorting and one placeholder-less f-string; no logic change (compile + full pytest still green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Integrate TinyFish's free Search and Fetch APIs as chat tools so the assistant can read external links (e.g. a course-catalog PDF that Schoology only exposes as a link) and search the public web — capabilities the chat did not have before. - services/tinyfish.py: thin requests-based client for the Fetch (POST api.fetch.tinyfish.ai) and Search (GET api.search.tinyfish.ai) REST endpoints, authenticated with a single shared X-API-Key. No new dependency and no Python 3.11+ floor (avoids the tinyfish SDK). - services/chat/web_tools.py: fetch_url and web_search tool definitions + execution, returning the existing ToolExecutionResult shape. Output is bounded (per-URL char cap, capped URL/result counts). - services/chat/service.py: offer the web tools on every generation, independent of schoology_connected (fetching a public URL needs no Schoology auth), and dispatch web tool names to execute_web_tool. - config.py: TINYFISH_API_KEY env var + a validate() notice when unset. - prompts/chat_system.txt: tell the model about the new tools. Set TINYFISH_API_KEY in backend/.env to enable. Key from agent.tinyfish.ai/api-keys. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Web-tool error messages raised by the TinyFish client are streamed to the chat UI as errorText, so "TinyFish fetch error: ..." etc. would surface to end users. Reword the raised messages to generic "Web fetch"/"Web search" wording and make the not-configured message user-friendly. Internal identifiers (module, exception classes, env var, endpoints, comments) are not user-facing and are left unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add TinyFish web tools (fetch_url + web_search) to chat
Remove Convex: Flask + SQLite + SSE becomes the only backend
There was a problem hiding this comment.
Bandit found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
| """Handle Google OAuth callback""" | ||
| logger.debug("Google OAuth callback hit") | ||
| logger.debug(f"Request args: {request.args}") | ||
| logger.debug("Request args keys: %s", sorted(request.args.keys())) |
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.
Promotes
devtomain.devis a clean superset ofmain(no divergence).What's included
fetch_url+web_searchtools added to chat; user-facing branding scrubbed from error messages.dev.Testing performed on the merged
devHEAD (local)Backend
:3111+ frontend:3112brought up via local dev setup; a seeded, chat-entitled session used for auth.snapshot→delta→terminal; assistant streamed the expected reply. UI showed user/assistant bubbles, composer, thread persistence, and generation states./api/events: connects and emits 15s heartbeats./icons/refresh.svg, 404/favicon.ico.TINYFISH_API_KEYset); they degrade gracefully and base chat is unaffected.🤖 Generated with Claude Code