Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TaskPilot — Multi-User Tool-Calling Agent

A personal productivity agent, now multi-tenant: each user has their own account, their own private tasks, their own conversation memory, and brings their own Groq API key. Built as a progression from a single-user prototype into something with real account isolation and security properties worth naming explicitly.

⚠️ Read this before deploying anywhere real users will use it

Streamlit Community Cloud has no persistent disk. This app stores accounts, tasks, and encrypted API keys in local SQLite files (taskpilot.db, checkpoints.db). On Streamlit Community Cloud specifically, the app's filesystem gets rebuilt from a fresh git clone on redeploys and periodic restarts — local SQLite data does not reliably survive that. If you deploy this to Streamlit Community Cloud as-is, expect user accounts and tasks to eventually disappear.

This is fine for demoing the multi-user behavior (isolation, auth, agent memory) to show in a portfolio or to a small group temporarily. It is not fine for anything people are meant to rely on. Before real users depend on this:

  • Host it somewhere with a persistent volume (a VPS, Render/Railway with a disk, Fly.io with a volume, etc.), or
  • Swap SQLite for a hosted database (Supabase/Neon Postgres both have generous free tiers) — this would require rewriting db.py and auth.py's SQL layer, which currently assumes SQLite specifically.

Security model — what's actually enforced, and how it was verified

Multi-tenant apps fail in one of two ways: obviously (nothing works) or silently (User A can see User B's data because someone forgot a WHERE user_id = ? clause). The second failure mode is the dangerous one, so here's exactly what's enforced and how each guarantee was tested — not just asserted:

  • Passwords are hashed (bcrypt), never stored or logged in plaintext. Verified by a test that reads the raw DB row and asserts the plaintext password string doesn't appear in it.
  • Login errors don't leak which emails are registered. A wrong password and an unregistered email return the identical error message and object — verified by a test that asserts both error strings are equal.
  • Each user's Groq API key is encrypted at rest (Fernet, keyed by APP_SECRET_KEY), not stored in plaintext. Verified the same way — read the raw DB value, assert the real key string isn't in it — plus a round-trip test confirming it decrypts back to the original value, plus a test confirming a wrong APP_SECRET_KEY fails loudly (RuntimeError) rather than silently returning garbage.
  • Every task operation is scoped by user_id in the SQL itself (WHERE id = ? AND user_id = ?), not filtered after the fact. Task IDs are a single shared auto-increment counter across all users, so this matters: without it, User B could read/modify/delete User A's task just by guessing a task ID. Verified with tests that have User B attempt exactly that (read, complete, update, delete) against User A's real task ID and confirm every attempt fails as if the task didn't exist.
  • user_id is never an LLM-visible tool parameter. It's bound into each tool via closure at agent-build time (build_tools_for_user(user_id)), so there's no code path — including via prompt injection in a task title — by which the model could supply a different user_id. Verified by asserting "user_id" not in tool.args for every tool.
  • Conversation memory is namespaced by user internally (f"{user_id}:{thread_id}" as the actual checkpoint key), so even a colliding or guessed thread_id across two users can't cross-contaminate their conversation history.

40 tests cover this (pytest tests/ -v), including a dedicated test_multi_user_isolation.py for the cross-user attack scenarios above.

Architecture

Login/signup (bcrypt password hashing)
      │
      ▼
Per-user Groq API key (Fernet-encrypted at rest, decrypted per session)
      │
      ▼
build_agent(user_id, groq_api_key)
      │
      ├──► build_tools_for_user(user_id) — tools closed over user_id,
      │     never LLM-visible
      │
      ├──► LangGraph ReAct agent loop (tool calling)
      │           │
      │           ▼
      │     SQLite tasks table (every query scoped by user_id)
      │
      └──► SQLite checkpointer, keyed by f"{user_id}:{thread_id}"
      │     (conversation memory, isolated per user)
      │
      ▼
Streamlit UI (login gate → per-user chat + task sidebar)
FastAPI (token-based session, same isolation guarantees — see api.py docstring
for the honest caveat: in-memory tokens, a placeholder for real JWT/sessions)

Setup

# 1. Create a virtual environment
python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate

# 2. Install dependencies
pip install -r requirements.txt

# 3. Generate and set your app secret key
cp .env.example .env
python -c "import secrets; print(secrets.token_urlsafe(32))"
# paste the output as APP_SECRET_KEY in .env

# 4. Run it
streamlit run app.py

No GROQ_API_KEY needed in .env — each user enters their own key in the app's sidebar after signing up, which gets encrypted and stored per-account.

Running tests

pytest tests/ -v

40 tests, zero requiring a live Groq key: test_db.py (CRUD), test_auth.py (hashing, encryption, login edge cases), test_tools.py (agent tool contracts), and test_multi_user_isolation.py (the cross-user attack scenarios above).

Demo flow (two accounts, to show isolation working)

  1. Sign up as alice@example.com, add a Groq key, add a task: "Add a task to review the budget, high priority"
  2. Log out, sign up as bob@example.com with a different Groq key
  3. Ask TaskPilot "What's on my list?" — Bob sees an empty list, not Alice's task
  4. This is the property test_multi_user_isolation.py verifies automatically — the demo just makes it visible

What's still missing for "actually sellable"

Being direct about the gap between "multi-user foundation" and "sellable product," since that distinction matters:

  • No persistent hosting (see the warning at the top) — the single biggest gap for anything beyond a demo.
  • No billing/subscriptions — deliberately deferred per the plan; Stripe integration needs a webhook-capable backend, which Streamlit Community Cloud alone can't provide.
  • No password reset flow — a locked-out user currently has no self-service recovery path.
  • No rate limiting — a user could spam requests; not costly to you (they use their own Groq key) but worth adding for abuse prevention regardless.
  • In-memory API session tokens — fine for the Streamlit app (which doesn't use them), but the FastAPI surface's tokens don't survive a restart and wouldn't work across multiple server instances. Replace with real JWT or a Redis-backed session store before relying on the API directly.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages