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

7.6 KiB

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:

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:

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).