Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ FROID_EXTRACTION_WORKER_INTERVAL_SECONDS=300
FROID_MCP_ENABLED=false
FROID_MCP_BIND=127.0.0.1:8080

# The HTTP listener (MCP and dashboard) always requires bearer tokens. Users
# mint their own by sending /token to the Telegram bot (rotate with /token,
# disable with /token revoke); each request is served from the token owner's
# isolated database.
# The MCP listener always requires bearer tokens. Users mint their own by
# sending /token to the Telegram bot (rotate with /token, disable with
# /token revoke); each request is served from the token owner's isolated
# database.

# Required when FROID_EMBEDDING_WORKER_ENABLED=true or FROID_EXTRACTION_WORKER_ENABLED=true
OPENAI_API_KEY=
Expand Down
32 changes: 1 addition & 31 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,36 +46,6 @@ jobs:
- name: Run tests
run: cargo test --locked --all-targets

web:
name: Web
runs-on: ubuntu-latest
defaults:
run:
working-directory: web

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: web/package-lock.json

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Test
run: npm test

- name: Build
run: npm run build

docker:
name: Docker
runs-on: ubuntu-latest
Expand All @@ -98,7 +68,7 @@ jobs:
docker-push:
name: Docker Push
runs-on: ubuntu-latest
needs: [rust, web, docker]
needs: [rust, docker]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'

steps:
Expand Down
24 changes: 0 additions & 24 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

- Rust (edition 2024, see `Cargo.toml` for the exact toolchain)
- SQLite 3 development libraries
- Node.js 20+ and npm (required only to build the dashboard webapp in `web/`)
- A Telegram bot token (required to run the server)
- An OpenAI API key (required only when embedding or extraction workers are enabled)

Expand All @@ -26,19 +25,6 @@ cargo run -- serve

Database migrations are applied automatically on startup via `sqlx::migrate!()`.

## Dashboard webapp (`web/`)

The dashboard is a Vite + React + TypeScript app with Tailwind v4 and shadcn/ui. Assets are embedded into the release Rust binary via `rust-embed`; debug builds read from `web/dist/` on disk.

```bash
cd web
npm install
npm run dev # local dev server with HMR
npm run build # produces web/dist/ consumed by cargo build
```

`web/dist/` is gitignored except for a committed `index.html` placeholder used as a fallback when assets have not been built. Running `npm run build` locally overwrites that placeholder; revert with `git restore web/dist/index.html` before committing.

## Local CI Checks

Run these before pushing. They mirror the CI pipeline exactly:
Expand All @@ -50,14 +36,6 @@ cargo clippy --locked --all-targets -- -D warnings
cargo test --locked --all-targets
```

For changes under `web/`, also run from that directory:

```bash
npm run lint
npm test
npm run build
```

All checks must pass with no errors or warnings before a branch is ready for review.

## Branch Naming
Expand All @@ -84,7 +62,6 @@ src/
├── handler.rs # MessageHandler trait
├── messages.rs # IncomingMessage / OutgoingMessage types
├── version.rs # Version populated by build.rs
├── dashboard.rs # Axum router serving the embedded web/ assets
├── adapters/
│ ├── telegram.rs # Telegram adapter (teloxide)
│ └── mcp.rs # MCP Streamable HTTP adapter
Expand All @@ -107,7 +84,6 @@ src/
└── weekly_review.rs # WeeklyReviewDeliveryWorker
migrations/ # sqlx migrations (applied automatically)
prompts/ # Versioned LLM prompt files
web/ # Vite + React + Tailwind + shadcn dashboard
```

## Architecture Notes
Expand Down
36 changes: 0 additions & 36 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ serde = { version = "1.0", features = ["derive"] }
schemars = { version = "1.2.1", features = ["chrono04"] }
rmcp = { version = "1.6", features = ["server", "transport-streamable-http-server"] }
axum = "0.8"
rust-embed = "8"
mime_guess = "2"
ulid = "1"
tower = { version = "0.5", features = ["util"] }
rand = "0.8"
Expand Down
9 changes: 0 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
FROM node:20-bookworm AS web-builder

WORKDIR /web
COPY web/package.json web/package-lock.json ./
RUN npm ci
COPY web ./
RUN npm run build

FROM rust:1-bookworm AS builder

WORKDIR /app
Expand All @@ -21,7 +13,6 @@ COPY build.rs ./
COPY migrations ./migrations
COPY prompts ./prompts
COPY src ./src
COPY --from=web-builder /web/dist ./web/dist

RUN cargo build --locked --release

Expand Down
22 changes: 8 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,33 +47,25 @@ FROID_MCP_ENABLED=true cargo run -- serve

Available tools: `journal_get`, `journal_get_recent`, `journal_search_text`, `journal_search_semantic`, `daily_review_get`, `daily_review_get_range`, `weekly_review_get`, `weekly_review_get_range`, `signals_search`.

## Dashboard webapp

Set `FROID_DASHBOARD_ENABLED=true` to serve a small React webapp at `http://127.0.0.1:8080/`. Besides message export/import and prompt editing, the dashboard API exposes journal data directly: `GET /api/entries` (recent entries), `POST /api/messages` (capture a new entry from the browser; it flows through the same extraction/embedding/review pipeline as Telegram messages), and `GET /api/reviews/daily` / `GET /api/reviews/weekly` (completed reviews, optional `from`/`to` date filters). The dashboard shares the HTTP listener with the MCP endpoint (`FROID_MCP_BIND`, default `127.0.0.1:8080`) and can be enabled independently of MCP. Assets are embedded into the release binary, so the Docker image carries everything it needs. The dashboard is protected by the same bearer check as MCP (see [Authentication](#authentication)); on first visit it asks for your token.

```bash
FROID_DASHBOARD_ENABLED=true cargo run -- serve
```

## Authentication

Authentication is always on: every request to `/mcp` and `/api` must carry a bearer token minted through the bot:
Authentication is always on: every request to `/mcp` must carry a bearer token minted through the bot:

```
Authorization: Bearer <your-token>
```

Users mint their own tokens by sending `/token` to the Telegram bot. Telegram is already the authenticated channel (the chat id arrives with the message), so the bot can safely issue HTTP credentials for exactly that identity: it generates a 256-bit random token, stores only its SHA-256 hash in the default database, and replies once with the plaintext. Each request is then served from the token owner's isolated journal database, so every user of a shared instance gets their own MCP endpoint and dashboard view. Sending `/token` again rotates the token; `/token revoke` disables access. Onboarding a user requires no operator action and no restart.
Users mint their own tokens by sending `/token` to the Telegram bot. Telegram is already the authenticated channel (the chat id arrives with the message), so the bot can safely issue HTTP credentials for exactly that identity: it generates a 256-bit random token, stores only its SHA-256 hash in the default database, and replies once with the plaintext. Each request is then served from the token owner's isolated journal database, so every user of a shared instance gets their own MCP endpoint. Sending `/token` again rotates the token; `/token revoke` disables access. Onboarding a user requires no operator action and no restart.

Requests without a valid token receive `401 Unauthorized`. The static dashboard shell (HTML/JS assets, which contain no journal data) and the `/health` probe are intentionally served without authentication; all data flows through the protected `/api` and `/mcp` routes.
Requests without a valid token receive `401 Unauthorized`; only the `/health` probe is served without authentication.

Because Telegram access is what mints HTTP credentials, `TELEGRAM_ALLOWED_USER_IDS` is the gate that decides who can become a user — set it on any instance whose bot a stranger could message.

The auth variables from earlier versions (`FROID_AUTH_TOKEN`, `FROID_AUTH_TOKENS`, `FROID_AUTH_DYNAMIC_TOKENS`, `FROID_AUTH_ENABLED`) have been removed; Froid refuses to start if one is still set, so the behavior change cannot go unnoticed.
Variables from removed features (`FROID_AUTH_TOKEN`, `FROID_AUTH_TOKENS`, `FROID_AUTH_DYNAMIC_TOKENS`, `FROID_AUTH_ENABLED`, `FROID_DASHBOARD_ENABLED`) make Froid refuse to start if still set, so behavior changes cannot go unnoticed.

## Health endpoint

Whenever the HTTP listener is running (MCP or dashboard enabled), `GET /health` answers `200 OK` with the service name and version. It is intentionally exempt from the bearer-token check so supervisors, load balancers, and container healthchecks can probe it without credentials:
Whenever the MCP listener is running, `GET /health` answers `200 OK` with the service name and version. It is intentionally exempt from the bearer-token check so supervisors, load balancers, and container healthchecks can probe it without credentials:

```bash
curl http://127.0.0.1:8080/health
Expand All @@ -89,6 +81,8 @@ All persistent state lives in `DATA_DIR` (default `data/`): the legacy database

Restoring is the reverse: stop the service and put the files back in place.

For per-user data portability, every user can pull their own journal without operator involvement: send `/export` to the bot to receive a JSON file of all raw messages, and send that file back as a document with `/import` as the caption to load it (all-or-nothing; a collision with an existing message aborts the import). Telegram bots can download files up to ~20 MB, so an enormous import would need the file-level restore path above instead.

## Managing users

Each user's journal lives in its own SQLite database under `DATA_DIR/journals/user_<chat_id>.sqlite3`. The `users` subcommands operate directly on those files, so run them while the server is stopped:
Expand All @@ -98,7 +92,7 @@ froid users list # chat id, size, last modified, path
froid users delete <chat_id> --yes
```

`users delete` permanently removes the user's entire journal (database plus WAL side-files) and refuses to run without `--yes`. Combined with the export endpoint this covers data-portability and right-to-erasure requests.
`users delete` permanently removes the user's entire journal (database plus WAL side-files) and refuses to run without `--yes`. Combined with the `/export` command this covers data-portability and right-to-erasure requests.

## Configuration

Expand Down
Loading
Loading