Skip to content

Merge dev into main: Convex removal + TinyFish chat web tools#17

Merged
AdamEXu merged 50 commits into
mainfrom
dev
Jul 8, 2026
Merged

Merge dev into main: Convex removal + TinyFish chat web tools#17
AdamEXu merged 50 commits into
mainfrom
dev

Conversation

@AdamEXu

@AdamEXu AdamEXu commented Jul 8, 2026

Copy link
Copy Markdown
Member

Promotes dev to main. dev is a clean superset of main (no divergence).

What's included

Testing performed on the merged dev HEAD (local)

Backend :3111 + frontend :3112 brought up via local dev setup; a seeded, chat-entitled session used for auth.

  • Chat end-to-end (backend API + real-browser UI via Playwright): send message → SSE snapshotdeltaterminal; assistant streamed the expected reply. UI showed user/assistant bubbles, composer, thread persistence, and generation states.
  • App-wide SSE /api/events: connects and emits 15s heartbeats.
  • Auth/session enforcement: 401 without cookie; dashboard shell renders with valid session.
  • ⚠️ Cosmetic, pre-existing (not from these merges): 404 /icons/refresh.svg, 404 /favicon.ico.
  • ℹ️ TinyFish web tools not exercised locally (no TINYFISH_API_KEY set); they degrade gracefully and base chat is unaffected.

🤖 Generated with Claude Code

AdamEXu and others added 30 commits July 7, 2026 10:58
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>
AdamEXu and others added 20 commits July 7, 2026 14:03
…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

@github-advanced-security github-advanced-security 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.

Bandit found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Comment thread backend/auth/routes.py
"""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()))
@AdamEXu AdamEXu merged commit 17a716a into main Jul 8, 2026
7 of 8 checks passed
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