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.
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 theupstreamgit 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=1or an explicitDATA_DIR→ opt out (used by*.real.test.jssuites 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):
- Test Connection returned
"Provider test not supported"becausecodebuddy-intlwas missing fromOAUTH_TEST_CONFIG, sotestOAuthConnectionbailed before probing. It now probes the Keycloak realm'suserinfoendpoint (URL derived from the token'sissclaim). - OAuth logins were named
"Account N"with no email. The access token is a Keycloak JWT carryingemail/name;mapTokensnow 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-forwardmaster. - 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.jsonversion (fork cadence1.0.x);CHANGELOG.mdmirrors 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).