Files
9router/AGENTS.md
T
MUH. IQRAM BAHRING 25df5e6a9d chore(guards): add durable markers so fixes aren't silently dropped
Introduce AGENTS.md (root, primary agent instruction file) documenting six
hard-won fixes with explicit DO NOT / WHY, plus executable enforcement so a
future AI cannot delete or reintroduce them:

1. package-lock.json must be generated with npm 10 (Docker's npm 10.9.8).
   npm 11 drops the top-level @emnapi/core + @emnapi/runtime entries npm 10
   needs, breaking the tag-triggered Docker build at `npm ci` (happened on
   v1.0.14). Add scripts/verify-lockfile-npm10.mjs + .npmrc + a Dockerfile
   fail-fast check + a CI step + tests/unit/lockfile-npm10-guard.test.js.
   Also re-fix the lockfile itself (regenerated with npm 10.9.8).
2. Tests must never write to the real ~/.9router DB (isolateDataDir).
3. Hidden providers must not leak into Usage (usageProviders !p.hidden).
4. codebuddy-intl connection test + OAuth identity.
5. Fork-only features that must survive upstream syncs.
6. Upstream sync procedure.

Each marker cross-references AGENTS.md and the covering test. CLAUDE.md now
points to AGENTS.md at the top. Verified: build ok, guard script passes,
full suite leaves the real DB count unchanged (38), 0 new regressions.
2026-09-19 12:52:19 +08:00

161 lines
7.6 KiB
Markdown

# AGENTS.md — MIBP fork guardrails
> **READ THIS BEFORE EDITING.** This is the **MIBP fork** of `decolua/9router`
> (`github.com/mhiqrambg/9router-mibp-version`). Upstream is tracked as the
> `upstream` git remote. This file records hard-won fixes that are easy to
> silently delete or reintroduce. Each entry has a **DO NOT** and a **WHY**.
>
> If you are about to change something listed here, stop and read the whole
> entry first. If you believe an entry is obsolete, say so explicitly and get
> confirmation before removing it — do **not** quietly drop it.
---
## 1. `package-lock.json` MUST be generated with npm 10 (Docker's npm)
**DO NOT** regenerate `package-lock.json` with your global `npm` (npm 11+).
**DO NOT** delete `package-lock.json` or add it to `.gitignore`.
**WHY:** The Docker image pins `node:22-alpine` by digest (see `Dockerfile`
`ARG NODE_IMAGE`), which ships **npm 10.9.8**. The `Dockerfile` runs `npm ci`
against the committed lockfile. npm 11 **drops the top-level optional entries**
`@emnapi/core` and `@emnapi/runtime` that npm 10's platform-complete resolve
requires. The tag-triggered **"Build and Push Docker Image"** workflow then
fails at `npm ci`:
```
npm error `npm ci` can only install packages when your package.json and package-lock.json are in sync.
npm error Missing: @emnapi/runtime@1.11.3 from lock file
npm error Missing: @emnapi/core@1.11.3 from lock file
```
This **already happened on tag `v1.0.14`** (run 35422006940) and on earlier
releases (`a2c6187a`, `470c8ca7`, `79294644`, `76b139b3`).
**Correct way to (re)generate the lockfile:**
```bash
npx -y npm@10.9.8 install --package-lock-only
node scripts/verify-lockfile-npm10.mjs # must print ✅
```
**Enforcement:** `scripts/verify-lockfile-npm10.mjs` fails (exit 1) if the
top-level `@emnapi/core` / `@emnapi/runtime` entries are missing. Run it after
any dependency change; CI runs it before the Docker build.
---
## 2. Tests must NEVER write to the real database (`~/.9router`)
**DO NOT** remove `tests/setup/isolateDataDir.js` from `tests/vitest.config.js`
`setupFiles`. **DO NOT** delete `tests/unit/test-data-dir-isolation.test.js`.
**WHY:** Route-level tests (e.g. `zed-live-models.test.js`,
`zed-native-auth.test.js`) call the real `createProviderConnection`, which
persists to `$DATA_DIR/db/data.sqlite`. With `DATA_DIR` unset — the default for
`npx vitest run` — that resolved to the developer's **real** `~/.9router` DB and
appended fake connections on every run: `zed-live-*@example.com`,
`guard-*@example.com`, `zed "Account N"` (token `decrypted-token-xyz`), and the
fake provider `kimchi-nope`. These then showed up in the Usage dashboard. 42
such rows had to be cleaned out of the live DB once.
**Isolation contract:**
- Default: `DATA_DIR` → throwaway temp dir (auto-removed on exit).
- `RUN_REAL=1` or an explicit `DATA_DIR` → opt out (used by `*.real.test.js`
suites that intentionally read live credentials).
**Verify a change is safe:** run the suite, then confirm the real DB is
untouched:
```bash
node -e 'const D=require("better-sqlite3");const db=new D(process.env.HOME+"/.9router/db/data.sqlite",{readonly:true});console.log(db.prepare("SELECT COUNT(*) c FROM providerConnections").get().c)'
```
The count must be identical before and after `npx vitest run`.
---
## 3. Hidden providers must not leak into the Usage page
**DO NOT** remove the `!p.hidden` filter in `src/shared/utils/usageProviders.js`
(`buildUsageProviderList`). **DO NOT** inline the old unfiltered
`Object.values(FREE_PROVIDERS).filter(p => p.noAuth && ...)` logic back into
`src/shared/components/UsageStats.js`.
**WHY:** The Usage page auto-adds every `noAuth` free provider so connectionless
providers (e.g. `opencode`) still appear. It must also honor the registry
`hidden` flag, matching the Providers page (`providers/page.js` filters
`!info.hidden`). Without it, `devin-cli` and `mimo-free` (both
`category:"free"`, `noAuth:true`, `hidden:true`) appear in Usage with zero
connections and zero traffic.
**Covered by:** `tests/unit/usage-provider-list.test.js`.
---
## 4. `codebuddy-intl` connection test + OAuth identity
**DO NOT** remove the `"codebuddy-intl"` entry from `OAUTH_TEST_CONFIG` in
`src/app/api/providers/[id]/test/testUtils.js`.
**DO NOT** remove `email` / `displayName` from `codebuddy-intl`'s `mapTokens`
(`src/lib/oauth/providers/codebuddy-intl.js`).
**DO NOT** remove `backfillCodeBuddyIntlIdentity` or its calls in
`GET /api/providers` and `/api/providers/client`.
**WHY (two bugs, both fixed 2026-09-19):**
1. **Test Connection** returned `"Provider test not supported"` because
`codebuddy-intl` was missing from `OAUTH_TEST_CONFIG`, so
`testOAuthConnection` bailed before probing. It now probes the Keycloak
realm's `userinfo` endpoint (URL derived from the token's `iss` claim).
2. **OAuth logins** were named `"Account N"` with no email. The access token is
a Keycloak JWT carrying `email`/`name`; `mapTokens` now extracts them, and a
run-once backfill self-heals pre-existing rows.
**Covered by:** `tests/unit/codebuddy-intl-connection.test.js`,
`tests/unit/codebuddy-intl-backfill.test.js`.
---
## 5. Fork-only features — NEVER drop during an upstream sync
When merging `upstream/master`, these fork additions must survive conflict
resolution. If a merge conflict touches them, **resolve fork-priority** and
re-verify after.
| Area | Key files / markers |
|---|---|
| **Freebuff provider** | `open-sse/executors/freebuff.js`, `open-sse/providers/registry/freebuff.js`, `open-sse/services/usage/freebuff.js`, `src/lib/oauth/providers/freebuff.js`, `public/providers/freebuff.png`, and its entries in `open-sse/executors/index.js` + `open-sse/providers/registry/index.js` |
| **Proxy-pool fitness** | `open-sse/services/proxyPoolFitness.js`, `open-sse/services/poolGeo.js`, `src/lib/network/poolEgressProbe.js`, `src/lib/network/stateSweeper.js`, `src/app/(dashboard)/dashboard/proxy-fitness/`, `src/app/api/proxy-pools/**` |
| **Docker hardening** | `Dockerfile`: digest-pinned `NODE_IMAGE`, tracked `package-lock.json`, `npm ci`, `HEALTHCHECK`. `.github/workflows/docker-publish.yml`. |
| **dompurify security override** | `package.json` `overrides.dompurify` + the direct `dompurify` dependency |
| **MIBP branding** | `README.md`, `docker-compose.yml`, `.env.example`, the `MIBP Edition` link in `src/app/(dashboard)/dashboard/profile/page.js` |
| **Cline free-tier models** | `open-sse/providers/registry/cline.js` `authModes: ["oauth","apikey"]` + `cline-free/*` models; `open-sse/shared/clineAuth.js` product headers |
---
## 6. Upstream sync procedure
- Remote layout: `origin` = this fork, `upstream` = `decolua/9router`.
- **Merge, do not rebase** — the fork already has merge-based history; rebasing
rewrites public history.
- Work on a branch (`sync/upstream-<ver>`), tag a rollback point
(`backup/pre-sync-<ver>`), then fast-forward `master`.
- After resolving conflicts: `npm run build`, `npx vitest run`, and the
baseline scripts (`tests/__baseline__/verify-providers.mjs`,
`verify-oauth-urls.mjs`, `verify-alias.mjs`).
- The suite is **not** expected to be all-green (see `CLAUDE.md`). Judge only
for **new** regressions vs. a pre-merge run; a handful of upstream test-drift
failures are known.
- Bump `package.json` version (fork cadence `1.0.x`); `CHANGELOG.md` mirrors
upstream and is **not** edited by the fork.
- Regenerate the lockfile with npm 10 (see §1) after any dependency change.
---
## 7. Do not re-add `package-lock.json` to `.gitignore`
**WHY:** Upstream does not track a lockfile, but this fork **must** — the
Docker build depends on `npm ci` against a committed, npm-10-compatible
lockfile. The fork explicitly removed `package-lock.json` from `.gitignore`
(commit `76b139b3`).