catence 0.2.1-beta.2 → 0.2.1-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +4 -4
  2. package/config.example.json +1 -0
  3. package/dist/contracts/release.d.ts +1 -1
  4. package/dist/contracts/release.js +1 -1
  5. package/dist/contracts/runtime.d.ts +4 -0
  6. package/dist/core/query/catalog.d.ts +1 -1
  7. package/dist/core/query/catalog.js +16 -10
  8. package/dist/core/query/repository.d.ts +1 -1
  9. package/dist/core/query/repository.js +31 -2
  10. package/dist/core/retrieval/index.js +23 -5
  11. package/dist/core/runtime/athlete-file.d.ts +49 -0
  12. package/dist/core/runtime/athlete-file.js +177 -0
  13. package/dist/core/runtime/configuration.js +2 -0
  14. package/dist/core/runtime/console-accounts.d.ts +52 -0
  15. package/dist/core/runtime/console-accounts.js +103 -0
  16. package/dist/core/runtime/mcp-keys.d.ts +65 -0
  17. package/dist/core/runtime/mcp-keys.js +171 -0
  18. package/dist/core/runtime/oauth/model.d.ts +44 -0
  19. package/dist/core/runtime/oauth/model.js +345 -0
  20. package/dist/core/runtime/oauth/public-client.d.ts +28 -0
  21. package/dist/core/runtime/oauth/public-client.js +35 -0
  22. package/dist/core/runtime/oauth/settings.d.ts +33 -0
  23. package/dist/core/runtime/oauth/settings.js +96 -0
  24. package/dist/core/runtime/secrets.d.ts +15 -0
  25. package/dist/core/runtime/secrets.js +48 -7
  26. package/dist/elt/application/autosync.d.ts +71 -0
  27. package/dist/elt/application/autosync.js +173 -0
  28. package/dist/elt/application/detached-sync.d.ts +7 -2
  29. package/dist/elt/application/detached-sync.js +10 -4
  30. package/dist/elt/application/management.d.ts +7 -0
  31. package/dist/elt/application/management.js +103 -7
  32. package/dist/elt/ingestion/importer.d.ts +11 -1
  33. package/dist/elt/ingestion/importer.js +63 -14
  34. package/dist/elt/ingestion/providers/intervals.d.ts +7 -0
  35. package/dist/elt/ingestion/providers/intervals.js +21 -2
  36. package/dist/elt/normalization/normalizers.js +122 -0
  37. package/dist/elt/storage/database.d.ts +6 -1
  38. package/dist/elt/storage/database.js +7 -1
  39. package/dist/elt/storage/migrations.js +45 -1
  40. package/dist/elt/storage/progress-sidecar.d.ts +9 -0
  41. package/dist/elt/storage/progress-sidecar.js +31 -16
  42. package/dist/interfaces/cli/commands.js +117 -2
  43. package/dist/interfaces/http/cli-options.d.ts +64 -1
  44. package/dist/interfaces/http/cli-options.js +214 -2
  45. package/dist/interfaces/http/oauth.d.ts +16 -0
  46. package/dist/interfaces/http/oauth.js +228 -0
  47. package/dist/interfaces/http/server.d.ts +60 -1
  48. package/dist/interfaces/http/server.js +810 -16
  49. package/dist/interfaces/mcp/main.js +35 -3
  50. package/dist/interfaces/mcp/server.d.ts +21 -1
  51. package/dist/interfaces/mcp/server.js +107 -21
  52. package/dist/runtime/index.d.ts +15 -3
  53. package/dist/runtime/index.js +9 -2
  54. package/docs/architecture.md +1 -1
  55. package/docs/configuration.md +76 -6
  56. package/docs/deployment/docker.md +173 -15
  57. package/docs/deployment/local-console.md +132 -21
  58. package/docs/deployment/local-mcp.md +190 -11
  59. package/docs/development.md +5 -3
  60. package/docs/llm-providers.md +4 -4
  61. package/package.json +11 -4
  62. package/pyproject.toml +1 -1
  63. package/python/catence/providers/garmin/cli.py +55 -7
  64. package/python/catence/providers/garmin/progress.py +71 -35
  65. package/python/catence/providers/garmin/staging.py +11 -4
  66. package/python/catence/providers/garmin/worker.py +420 -206
  67. package/uv.lock +3 -3
@@ -26,6 +26,8 @@ explained in the [beta release notes](../../README.md#beta-releases).
26
26
  | Provider credentials | per-athlete secret file, mode 0600 | `catence-data secret set` |
27
27
  | API keys | process environment only | `.env` |
28
28
  | Chat history + preferences | data home | `console/chat-history.sqlite3` |
29
+ | Console logins + athlete grants | data home (mode 0600, bcrypt hashes) | `console/accounts.json` |
30
+ | Per-account MCP keys + OAuth state | data home (registry 0600; `oauth/` dir 0700) | `mcp-keys.json`, `oauth/` |
29
31
 
30
32
  Credentials never land in `config.json`; profiles reference environment
31
33
  variable *names* (`apiKeyEnv`, `apiBaseEnv`, `apiVersionEnv`). See
@@ -69,10 +71,16 @@ Script options (run `./deploy-console.sh --help` for the full list):
69
71
 
70
72
  ## 2. Fill in `catence-deploy/.env`
71
73
 
74
+ The Console always needs `CHAINLIT_AUTH_SECRET`, plus at least one login
75
+ source: the **environment break-glass account**
76
+ (`CATENCE_CONSOLE_USERNAME` + `CATENCE_CONSOLE_PASSWORD_HASH`, set together or
77
+ not at all) or a **stored Console account** in the data volume (see
78
+ [Console accounts](#console-accounts)). Add one model-provider key either way.
79
+
72
80
  ```sh
73
- CATENCE_CONSOLE_USERNAME=coach
74
- CATENCE_CONSOLE_PASSWORD_HASH='…' # generate: ./catence-deploy/hash-password.sh
75
81
  CHAINLIT_AUTH_SECRET='…' # generate: openssl rand -hex 32
82
+ CATENCE_CONSOLE_USERNAME=coach # environment break-glass account …
83
+ CATENCE_CONSOLE_PASSWORD_HASH='…' # generate: ./catence-deploy/hash-password.sh
76
84
  OPENAI_API_KEY='…' # or ANTHROPIC_API_KEY / OPENCODE_GO_API_KEY …
77
85
  OPENAI_API_BASE='' # only for OpenAI-compatible endpoints
78
86
  OPENCODE_GO_API_KEY='…' # the OpenCode Go base URLs are pre-filled below
@@ -80,9 +88,14 @@ OPENCODE_GO_API_BASE='https://opencode.ai/zen/go/v1'
80
88
  OPENCODE_GO_MESSAGES_API_BASE='https://opencode.ai/zen/go'
81
89
  ```
82
90
 
83
- Generate the Console login hash with the scaffold helper (it prompts on the
84
- terminal, never echoes, and uses the compose project so it works whatever image
85
- tag the script built):
91
+ Leave the username/hash pair empty to log in with stored accounts instead: the
92
+ script accepts an existing `console/accounts.json` in the data volume and
93
+ starts without the pair. Set both or neither — a lone username without a hash
94
+ fails the deploy with a clear error.
95
+
96
+ Generate the hash for the environment pair with the scaffold helper (it
97
+ prompts on the terminal, never echoes, and uses the compose project so it works
98
+ whatever image tag the script built):
86
99
 
87
100
  ```sh
88
101
  ./catence-deploy/hash-password.sh
@@ -102,6 +115,67 @@ Then start the stack (or re-run the script, which re-reads the saved `.env`):
102
115
  docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env up -d
103
116
  ```
104
117
 
118
+ ### Console accounts
119
+
120
+ Stored accounts are the multi-user path: each account has a role and an
121
+ athlete grant list, and the Console enforces both server-side on logins,
122
+ roster/dashboard requests, and agent turns. Accounts live in
123
+ `console/accounts.json` in the data volume (mode 0600, bcrypt hashes only).
124
+
125
+ Manage them with the `users` command inside the container — via the generated
126
+ helper:
127
+
128
+ ```sh
129
+ ./catence-deploy/users.sh add martina --athlete martina # member: one granted athlete
130
+ ./catence-deploy/users.sh add coach --admin # admin: every athlete
131
+ ./catence-deploy/users.sh list
132
+ ./catence-deploy/users.sh grant martina sam
133
+ ./catence-deploy/users.sh revoke martina sam
134
+ ./catence-deploy/users.sh passwd martina
135
+ ./catence-deploy/users.sh set-role martina admin
136
+ ./catence-deploy/users.sh remove martina
137
+ ```
138
+
139
+ `users.sh` uses `docker compose exec`, so the stack must be running. The long
140
+ forms are:
141
+
142
+ ```sh
143
+ # Interactive: prompts for the password twice
144
+ docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env exec console \
145
+ /opt/catence-console/bin/catence-console users add martina --athlete martina
146
+
147
+ # Non-interactive: read the password from an environment variable
148
+ docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm -T \
149
+ -e CATENCE_CONSOLE_NEW_PASSWORD='…' \
150
+ --entrypoint /opt/catence-console/bin/catence-console console \
151
+ users add martina --athlete martina --password-env CATENCE_CONSOLE_NEW_PASSWORD
152
+ ```
153
+
154
+ Before the first start there is no running container **and** no login yet —
155
+ the Console refuses to start without one — so create the first account with
156
+ the one-off form (it prompts for the password):
157
+
158
+ ```sh
159
+ docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm \
160
+ --entrypoint /opt/catence-console/bin/catence-console console users add coach --admin
161
+ ```
162
+
163
+ Roles: `admin` sees every athlete; `member` sees only explicitly granted
164
+ athletes. A member with zero grants can sign in and browse, but has no athlete
165
+ to scope a chat to. The environment pair is a never-persisted break-glass
166
+ admin: it keeps working when the accounts file is lost or the last admin is
167
+ locked out, and unsetting the two variables revokes it.
168
+
169
+ Accounts gate the Console only. The MCP server has no authentication by
170
+ default — the caller's `athleteId` is trusted — so port 8787 still needs
171
+ loopback or network-layer restriction unless you switch on the bearer gate,
172
+ per-account keys, or OAuth (see
173
+ [Connecting to the Docker-based MCP from outside](#connecting-to-the-docker-based-mcp-from-outside)).
174
+
175
+ The accounts file is the only copy of your stored logins: keep it in backups
176
+ of the data volume (or bind-mounted home), along with `config.json` and the
177
+ athlete stores.
178
+
105
179
  ## 3. Initialize an athlete store
106
180
 
107
181
  The data volume already contains a seeded `config.json`, so `setup` accepts
@@ -184,8 +258,9 @@ export OPENCODE_GO_API_KEY='…' # any non-empty value pass
184
258
  ## 7. Access the Console
185
259
 
186
260
  Open `http://127.0.0.1:8000` (or the `--bind:--port` you configured) and log in
187
- with `CATENCE_CONSOLE_USERNAME` and the password whose hash is in
188
- `CATENCE_CONSOLE_PASSWORD_HASH`.
261
+ with a stored Console account (see [Console accounts](#console-accounts)) or
262
+ with the environment break-glass pair (`CATENCE_CONSOLE_USERNAME` and the
263
+ password whose hash is in `CATENCE_CONSOLE_PASSWORD_HASH`).
189
264
 
190
265
  ## 8. Verify with doctor
191
266
 
@@ -395,11 +470,12 @@ CATENCE_MCP_BIND=0.0.0.0 ./scripts/deploy-console.sh beta
395
470
  After deployment, the MCP server is available at
396
471
  `http://<tailscale-ip>:8787/mcp` from any device on your tailnet.
397
472
 
398
- **Security note:** The MCP server has **no authentication** — it trusts the
399
- caller's `athleteId` parameter. When exposing on `0.0.0.0`, you **must**
400
- restrict access at the network layer (Tailscale ACLs, firewall rules, etc.).
401
- The Console UI remains on loopback (`127.0.0.1:8000`) and should still be
402
- fronted by Cloudflare Tunnel or a reverse proxy.
473
+ **Security note:** Without the bearer gate, per-account keys, or OAuth, the MCP
474
+ server has **no authentication** — it trusts the caller's `athleteId`
475
+ parameter. When exposing on `0.0.0.0`, you **must** restrict access at the
476
+ network layer (Tailscale ACLs, firewall rules, etc.). The Console UI remains
477
+ on loopback (`127.0.0.1:8000`) and should still be fronted by Cloudflare
478
+ Tunnel or a reverse proxy.
403
479
 
404
480
  **Examples: common MCP clients**
405
481
 
@@ -425,14 +501,90 @@ claude mcp add --transport http catence -- http://<tailscale-ip>:8787/mcp
425
501
 
426
502
  ### Why not expose port 8787 directly?
427
503
 
428
- - The MCP server has **no authentication** — it trusts the caller's
429
- `athleteId` parameter.
504
+ - Without the bearer gate, per-account keys, or OAuth, the MCP server has
505
+ **no authentication** — it trusts the caller's `athleteId` parameter.
430
506
  - Exposing it would allow any network caller to read all athlete data in the
431
507
  catalog.
432
508
  - The Console proxies dashboard requests through its **authenticated**
433
509
  same-origin route.
434
510
  - SSH port forwarding keeps the trust boundary at your SSH credentials.
435
511
 
512
+ ### Bearer-token authentication for remote MCP clients
513
+
514
+ To serve the MCP endpoint to a remote MCP client (for example a Mistral Work
515
+ custom connector) over a Cloudflare Tunnel, switch on the native
516
+ bearer-token gate. The deploy scaffold arms it at `/data/mcp-token` (inside
517
+ the data volume, so it survives rebuilds); the file's presence is the switch
518
+ and Catence generates the token itself:
519
+
520
+ ```sh
521
+ # enable from the host (or use the Console Settings once logged in):
522
+ docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env \
523
+ exec console catence-data --home /data mcp enable
524
+
525
+ # print the token for the client's credential flow:
526
+ docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env \
527
+ exec console catence-data --home /data mcp token
528
+
529
+ # disable again:
530
+ docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env \
531
+ exec console catence-data --home /data mcp disable
532
+ ```
533
+
534
+ With the gate enabled, every runtime endpoint except `/health` and
535
+ `/api/v1/health` answers `401` without an exact `Authorization: Bearer
536
+ <token>` header — including `/mcp` and the JSON APIs. Toggling takes effect
537
+ without a restart; the Console attaches the header automatically to its own
538
+ runtime calls.
539
+
540
+ Keep `CATENCE_MCP_BIND=127.0.0.1` (the default): the tunnel client on the
541
+ host forwards to the host-loopback mapping, so the port still never binds a
542
+ LAN interface. Point the tunnel ingress for the MCP hostname at
543
+ `http://127.0.0.1:8787` and complete the client's credential flow with the
544
+ token from `mcp token`. Rotate with `mcp disable && mcp enable` and
545
+ re-authenticate the client. The only additional setup the tunnel needs is
546
+ the ingress rule for the MCP hostname.
547
+
548
+ A custom host-side token file is still possible: set
549
+ `CATENCE_MCP_TOKEN_FILE=<host path>` in `.env`. The file is then bind-mounted
550
+ read-only at the same path, so it must already exist on the host — enabling
551
+ from the Console cannot create it.
552
+
553
+ #### Per-account MCP keys and OAuth 2.1 (Docker)
554
+
555
+ The runtime reads three more environment variables. The deploy script passes
556
+ them into the container, writes them into `catence-deploy/.env`, and validates
557
+ them before rendering the compose file:
558
+
559
+ | Variable | Default | Meaning |
560
+ | --- | --- | --- |
561
+ | `CATENCE_OAUTH_ENABLED` | empty (disabled) | `1` or `true` runs the OAuth 2.1 authorization server so ChatGPT/Codex connect with their OAuth flow. Startup default — the Console (Settings → MCP Exposure → OAuth 2.1 sign-in) overrides it at runtime. Requires `CATENCE_PUBLIC_URL` when enabled. |
562
+ | `CATENCE_PUBLIC_URL` | empty | Public `https` URL of this server (the tunnel URL; no query string or fragment). It becomes the advertised OAuth issuer. Startup default — overridable in the Console. |
563
+ | `CATENCE_MCP_KEYS_FILE` | `/data/mcp-keys.json` | Per-account MCP key registry, a container path — no host mount needed. Managed with `catence-data mcp key` or the Console (Settings → MCP Exposure → API keys). |
564
+
565
+ ```sh
566
+ # Per-account key for a Console account (the raw token is printed exactly once):
567
+ docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env \
568
+ exec console catence-data --home /data mcp key add --account coach
569
+
570
+ # OAuth for ChatGPT/Codex — preferred: enable in the Console
571
+ # (Settings → MCP Exposure → OAuth 2.1 sign-in; applies without a restart).
572
+ # Startup defaults, set in catence-deploy/.env and re-run the script:
573
+ # CATENCE_OAUTH_ENABLED=1
574
+ # CATENCE_PUBLIC_URL=https://catence-mcp.example.com
575
+ ./scripts/deploy-console.sh beta
576
+ ```
577
+
578
+ The script refuses to deploy when `CATENCE_OAUTH_ENABLED` is set without a
579
+ valid https `CATENCE_PUBLIC_URL`. Once a valid Console settings file exists
580
+ (`<catalog-home>/oauth/settings.json`, inside the data volume), it overrides
581
+ these startup values at runtime. The checked-in dev compose
582
+ (`docker-compose.beta.yml`) accepts `CATENCE_OAUTH_ENABLED` and
583
+ `CATENCE_PUBLIC_URL` the same way (empty default = disabled). See
584
+ [local-mcp.md](local-mcp.md#per-account-api-keys) and
585
+ [local-mcp.md](local-mcp.md#oauth-21-chatgptcodex) for the full semantics and
586
+ the client matrix.
587
+
436
588
  ## Exposing the Console safely
437
589
 
438
590
  Keep the Console port bound to `127.0.0.1` (the default) and front it with a
@@ -472,11 +624,17 @@ docker run --rm -it -v catence-data:/data --entrypoint catence-data catence-cons
472
624
 
473
625
  Use the same `--entrypoint catence-data` pattern for `secret set` and `sync`.
474
626
 
627
+ Console logins work the same way: keep the environment pair together (or
628
+ neither variable), or seed a stored account into the volume with the `users`
629
+ command (see [Console accounts](#console-accounts)).
630
+
475
631
  ## Updating
476
632
 
477
633
  Docker deployments: re-run `deploy-console.sh beta` (or `stable`); it
478
634
  re-resolves the newest versions from the registries and rebuilds. The data
479
- volume and your `config.json` are preserved.
635
+ volume is preserved, so your `config.json`, the stored Console accounts
636
+ (`console/accounts.json`), chat history, and the athlete stores all survive
637
+ the rebuild.
480
638
 
481
639
  ```sh
482
640
  curl -fsSL https://raw.githubusercontent.com/rifusaki/catence/beta/scripts/deploy-console.sh | bash -s beta
@@ -2,7 +2,8 @@
2
2
 
3
3
  Catence Console is a password-protected local web chat that starts a matching
4
4
  Catence runtime on loopback. It uses Chainlit password login and fails closed
5
- unless all three authentication variables are supplied.
5
+ unless `CHAINLIT_AUTH_SECRET` and at least one login source — a stored Console
6
+ account or the environment account pair — are configured.
6
7
 
7
8
  This guide covers the **local** path: the npm package plus the Python Console
8
9
  on your machine. The Docker path (one container built from the public
@@ -27,6 +28,7 @@ model.
27
28
  | Provider credentials | per-athlete secret file, mode 0600 | `catence-data secret set` |
28
29
  | API keys | process environment only | shell exports |
29
30
  | Chat history + preferences | data home | `console/chat-history.sqlite3` |
31
+ | Console logins + athlete grants | data home (mode 0600, bcrypt hashes) | `console/accounts.json` |
30
32
 
31
33
  Credentials never land in `config.json`; profiles reference environment
32
34
  variable *names* (`apiKeyEnv`, `apiBaseEnv`, `apiVersionEnv`). See
@@ -75,6 +77,12 @@ printf %s 'strava-client-secret' | catence-data --athlete alex secret set --prov
75
77
  Set only the providers you actually sync; `secret set` accepts each field
76
78
  independently.
77
79
 
80
+ Administrators can also manage any athlete's credentials from **Settings →
81
+ Athletes → Credentials** in the Console. The dialog is write-only — it shows
82
+ each field as configured or not and lets you replace or remove values, but
83
+ values are never displayed. `catence-data --athlete <id> secret remove
84
+ --provider <provider> --field <field>` mirrors the dialog for scripted setups.
85
+
78
86
  ## 4. Sync data and build retrieval context
79
87
 
80
88
  ```sh
@@ -95,15 +103,28 @@ To add OpenCode Go models, run the
95
103
 
96
104
  ```sh
97
105
  export OPENAI_API_KEY='…' # or ANTHROPIC_API_KEY / OPENCODE_GO_API_KEY …
98
- export CATENCE_CONSOLE_USERNAME='coach'
99
- export CATENCE_CONSOLE_PASSWORD_HASH="$(catence-console auth hash-password)"
100
106
  export CHAINLIT_AUTH_SECRET="$(openssl rand -hex 32)"
101
107
  ```
102
108
 
103
- The Console fails closed unless `CATENCE_CONSOLE_USERNAME`,
104
- `CATENCE_CONSOLE_PASSWORD_HASH` (a bcrypt hash), and `CHAINLIT_AUTH_SECRET`
105
- are all set. There is one shared account; the username/password pair grants a
106
- single `console-owner` role.
109
+ `CHAINLIT_AUTH_SECRET` is always required: it signs Chainlit sessions. On top
110
+ of it, the Console needs at least one login source:
111
+
112
+ - **Stored accounts** (the multi-user path). Each account carries a role and
113
+ an athlete grant list and lives in `<home>/console/accounts.json`. Create
114
+ the first one with the users CLI, which prompts for the password twice:
115
+
116
+ ```sh
117
+ catence-console users add coach --admin
118
+ ```
119
+
120
+ - **The environment break-glass account**: set both
121
+ `CATENCE_CONSOLE_USERNAME` and `CATENCE_CONSOLE_PASSWORD_HASH` (the bcrypt
122
+ hash printed by `catence-console auth hash-password`). The pair is never
123
+ persisted and always acts as an admin — the way back in when the accounts
124
+ file is lost.
125
+
126
+ Roles, grants, and the rest of the CLI are described under
127
+ [Console accounts](#console-accounts).
107
128
 
108
129
  ## 7. Start the Console
109
130
 
@@ -121,7 +142,7 @@ catence-console doctor
121
142
 
122
143
  ### Manage models in the Console
123
144
 
124
- The **Models** page (header button) manages the model list without editing
145
+ The **Settings → Models** tab manages the model list without editing
125
146
  `config.json` by hand:
126
147
 
127
148
  - Enable/disable toggles hide models from the chat's Model dropdown. Disabled
@@ -137,11 +158,87 @@ The **Models** page (header button) manages the model list without editing
137
158
  strict parser used at startup before it replaces the file, and secrets stay
138
159
  forbidden (only environment-variable names are ever stored).
139
160
 
140
- The dashboard header has a **Sync data** button that starts the same detached
161
+ **Settings → Status** has a **Sync data** button that starts the same detached
141
162
  sync as `catence-data sync --provider all` through the authenticated Console
142
163
  origin, shows live progress while the run is active, and displays the last
143
- completed sync afterwards. Each manual sync also refreshes OpenCode Go model
144
- profiles first; a discovery failure never blocks the data sync.
164
+ completed sync afterwards; the Dashboard keeps a read-only “Syncing…” pill and
165
+ links there when a write lock makes its data unavailable. Each manual sync also
166
+ refreshes OpenCode Go model profiles first; a discovery failure never blocks the
167
+ data sync.
168
+
169
+ ### Add athletes in the Console
170
+
171
+ Admins can add a catalog athlete from **Settings → Athletes** (athlete id,
172
+ display name, and optionally “set as default”). The form calls the runtime's
173
+ `POST /api/v1/athletes` through the Console, which rejects members with
174
+ `403 admin_required` and otherwise performs the same catalog write as
175
+ `catence-data athlete add --id <id> --label <label>`. The id must match
176
+ `^[a-z][a-z0-9-]{0,62}$` and must not already exist (`409 athlete_exists`);
177
+ single-store runtimes without a catalog refuse with `400 catalog_required`.
178
+ The roster refreshes immediately, so a new athlete can be granted to members
179
+ and selected in chat without restarting the Console.
180
+
181
+ ## Console accounts
182
+
183
+ Console accounts are Catence's multi-user login model, enforced entirely by
184
+ the Console: sign-in, the athlete roster, dashboard and athlete-file requests,
185
+ and the athlete scoping of every chat turn are all checked server-side against
186
+ the account's role and athlete grants. The MCP server itself is untouched — it
187
+ has no authentication and trusts the `athleteId` each call names (see
188
+ [local-mcp.md](local-mcp.md#streamable-http-server)); accounts only gate what
189
+ goes through the Console.
190
+
191
+ | Role | Athlete access |
192
+ | --- | --- |
193
+ | `admin` | Every athlete in the catalog (`athletes: "all"`) |
194
+ | `member` | Only the explicitly granted athletes; zero grants allowed |
195
+
196
+ Accounts live in `<home>/console/accounts.json` (mode 0600, bcrypt hashes
197
+ only), where `<home>` is `$CATENCE_HOME` or `~/.catence`. The Console re-reads
198
+ the file on every login, so new accounts, password resets, and grant changes
199
+ apply without a restart.
200
+
201
+ ### Manage accounts with the users CLI
202
+
203
+ ```sh
204
+ catence-console users add coach --admin # admin: all athletes
205
+ catence-console users add martina --athlete martina # member with one grant
206
+ catence-console users add sam # member, zero grants
207
+ catence-console users list
208
+ catence-console users passwd martina
209
+ catence-console users set-role sam member
210
+ catence-console users grant sam martina
211
+ catence-console users revoke sam martina
212
+ catence-console users remove sam
213
+ ```
214
+
215
+ - `add <username> (--admin | --athlete <id> [--athlete <id> …])` — members
216
+ default to zero grants; `--admin` cannot be combined with `--athlete` and
217
+ always stores access to every athlete.
218
+ - `add` and `passwd` prompt for the password twice (never echoed). For
219
+ automation, `--password-env VAR` reads the password from environment
220
+ variable `VAR` instead, so it never appears in the command line or shell
221
+ history.
222
+ - `grant`/`revoke` change a member's athlete list; revoking the last grant
223
+ leaves a zero-grant member. `set-role` promotes or demotes: promoting to
224
+ `admin` coerces the grants to `"all"`, demoting to `member` keeps them.
225
+ - `add`/`grant`/`revoke` validate athlete ids against `catalog.json` when it
226
+ exists, and `--home <dir>` points any subcommand at a non-default catalog.
227
+
228
+ A member with zero grants can sign in and browse the Console, but there is no
229
+ athlete to scope a chat to: the Athlete selector is disabled with *"You don't
230
+ have access to any athlete yet. Ask an administrator to grant you access."*
231
+ and every chat turn is answered with the same notice. A member that requests
232
+ an athlete outside their grants receives `403 athlete_forbidden`; the Console
233
+ never forwards the request to the runtime.
234
+
235
+ ### The environment break-glass account
236
+
237
+ `CATENCE_CONSOLE_USERNAME` and `CATENCE_CONSOLE_PASSWORD_HASH` must be set
238
+ together. When they are, the pair always acts as an `admin` and is never
239
+ written to `accounts.json`: unsetting the two variables revokes it. Use it as
240
+ the recovery path when the accounts file is lost or a lockout leaves nobody
241
+ who can grant access — not as the everyday login.
145
242
 
146
243
  ## Console serve options
147
244
 
@@ -167,12 +264,18 @@ chat starts.
167
264
 
168
265
  ## How multi-athlete works in the Console
169
266
 
170
- The Console shares the same per-athlete stores as MCP, but scoping is
171
- **server-owned, not model-owned**:
267
+ The Console shares the same per-athlete stores as MCP. Access is layered:
268
+ per-account grants at the login, then server-owned scoping per chat.
172
269
 
270
+ - **Per-account grants.** Each stored account is an `admin` (every athlete) or
271
+ a `member` with an explicit grant list (see
272
+ [Console accounts](#console-accounts)). The **Athlete** selector and the
273
+ proxied roster are narrowed to the account's grants; requests that name an
274
+ ungranted athlete are refused with `403 athlete_forbidden` before they reach
275
+ the runtime, and a zero-grant member sees no athletes at all.
173
276
  - The settings panel has an **Athlete** selector built from
174
277
  `GET /api/v1/athletes` on the runtime. The default is the catalog's
175
- `defaultAthleteId`.
278
+ `defaultAthleteId` (for a member, the first granted athlete).
176
279
  - On every personal-data tool call, the Console **forces** the selected
177
280
  athleteId onto the arguments — the model cannot name or switch athletes, and
178
281
  a chat's system message states: *"This Console chat is scoped to athleteId X.
@@ -183,10 +286,14 @@ The Console shares the same per-athlete stores as MCP, but scoping is
183
286
  announces *"This chat is scoped to athlete **<id>**."*
184
287
  - `list_athletes` is exempt from forcing so the roster can be rendered.
185
288
 
186
- The dashboard is fetched through the authenticated Console origin: Chainlit
187
- middleware proxies `GET /api/v1/dashboard` and `GET /api/v1/athletes` to the
188
- runtime only when the Console's JWT cookie is valid (otherwise 401), so raw
189
- port 8787 does not need to be exposed.
289
+ Console pages and chats reach the runtime through the authenticated Console
290
+ origin: Chainlit middleware proxies the Console's API surface — `GET
291
+ /api/v1/dashboard`, `GET`/`POST /api/v1/athletes` (POST is admin-only),
292
+ `GET`/`PUT /api/v1/athlete-secrets` and `POST /api/v1/athlete-secrets/remove`
293
+ (all admin-only), `GET /api/v1/threads/<id>/generation`, and the models,
294
+ accounts, tool-server, and sync routes — to the runtime only when the Console's
295
+ JWT cookie is valid (otherwise 401), so raw port 8787 does not need to be
296
+ exposed.
190
297
 
191
298
  ## Model discovery (OpenCode Go)
192
299
 
@@ -205,8 +312,8 @@ disappeared from the live catalog.
205
312
  npm run discover:opencode-go -- --write ~/.catence/config.json
206
313
  ```
207
314
 
208
- Inside the Console's Models page, **Discover OpenCode Go models** runs the
209
- same merge for the current home without touching athlete data (runtime route
315
+ Inside **Settings → Models**, **Discover OpenCode Go models** runs the same
316
+ merge for the current home without touching athlete data (runtime route
210
317
  `POST /api/v1/models/discover`, proxied behind the Console login). Models can
211
318
  also be edited in place there — the update action rewrites the `console`
212
319
  section atomically and never stores secret values.
@@ -260,4 +367,8 @@ uv tool install --upgrade catence-console
260
367
  environment variables; set them in the shell and restart the Console.
261
368
  - **Runtime not reachable** — `catence-console doctor --home "$HOME/.catence"
262
369
  --mcp-url http://127.0.0.1:8787/mcp` checks the handshake; confirm the
263
- runtime is running on that port and the protocol versions agree.
370
+ runtime is running on that port and the protocol versions agree.
371
+ - **Locked out of the Console** — sign in with the environment break-glass
372
+ account (`CATENCE_CONSOLE_USERNAME`/`CATENCE_CONSOLE_PASSWORD_HASH`) and
373
+ restore access with `catence-console users` (reset a password, re-grant
374
+ athletes, or add a new admin).