fix(doctor,docs): warn that the npm name 'gbrain' is unrelated + detect a shadowing npm install (#505) (#3454)

Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar.

The npm package named `gbrain` is an unrelated squatted package, so `npm install gbrain` gives users something that is not this project. Adds doctor detection that classifies real checkouts correctly, fails open, and is try/catch'd throughout. Classification rests on the bin-shape marker since this repo has no `repository` field — verified e2e.

Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN with 22/22 checks on the current base after batches 1-3 landed.

Known gap, recorded rather than hidden: the remediation commands were not executed against a real global install, and the Windows `which -a` path is unexercised.
This commit is contained in:
Time Attakc
2026-08-01 03:53:33 +08:00
committed by GitHub
parent 56454c6ba8
commit 5773736c63
7 changed files with 422 additions and 0 deletions
+7
View File
@@ -16,6 +16,13 @@ If you fetched this file by URL without cloning yet, the companion files live at
## Step 1: Install GBrain
> **NEVER install from the npm registry.** GBrain is not distributed on npm; the npm
> package named `gbrain` is an unrelated package. Do NOT run `npm install -g gbrain` or
> `bun add -g gbrain` (note the missing `github:` prefix — that's the trap). The only
> supported sources are `github:garrytan/gbrain` and a git clone, exactly as shown below.
> If an unrelated npm install is already present, remove it first
> (`npm uninstall -g gbrain` / `bun remove -g gbrain`); `gbrain doctor` also detects this.
Default path (Bun is required — gbrain is a Bun + TypeScript runtime):
```bash
+10
View File
@@ -65,6 +65,16 @@ This is the difference between a search engine and a brain. Search finds the pag
## Install
> [!WARNING]
> **GBrain is NOT distributed on npm.** The npm package named `gbrain` is an unrelated
> package with no connection to this project. Do not run `npm install -g gbrain` or
> `bun add -g gbrain` — you'll get something else, and it can shadow the real binary on
> your PATH. Install and upgrade ONLY via the documented paths below
> (`bun install -g github:garrytan/gbrain`, or `git clone` + `bun install && bun link`).
> If you already ran the npm install by mistake: `npm uninstall -g gbrain` /
> `bun remove -g gbrain`, then reinstall from GitHub. `gbrain doctor` detects a
> shadowing npm install and prints the fix.
GBrain is designed to be installed and operated by an AI agent. The fastest path is to have your agent do it for you. The CLI and MCP paths below are for people who want to wire it up themselves.
### Have your agent install it (recommended)
+17
View File
@@ -1019,6 +1019,13 @@ If you fetched this file by URL without cloning yet, the companion files live at
## Step 1: Install GBrain
> **NEVER install from the npm registry.** GBrain is not distributed on npm; the npm
> package named `gbrain` is an unrelated package. Do NOT run `npm install -g gbrain` or
> `bun add -g gbrain` (note the missing `github:` prefix — that's the trap). The only
> supported sources are `github:garrytan/gbrain` and a git clone, exactly as shown below.
> If an unrelated npm install is already present, remove it first
> (`npm uninstall -g gbrain` / `bun remove -g gbrain`); `gbrain doctor` also detects this.
Default path (Bun is required — gbrain is a Bun + TypeScript runtime):
```bash
@@ -1572,6 +1579,16 @@ This is the difference between a search engine and a brain. Search finds the pag
## Install
> [!WARNING]
> **GBrain is NOT distributed on npm.** The npm package named `gbrain` is an unrelated
> package with no connection to this project. Do not run `npm install -g gbrain` or
> `bun add -g gbrain` — you'll get something else, and it can shadow the real binary on
> your PATH. Install and upgrade ONLY via the documented paths below
> (`bun install -g github:garrytan/gbrain`, or `git clone` + `bun install && bun link`).
> If you already ran the npm install by mistake: `npm uninstall -g gbrain` /
> `bun remove -g gbrain`, then reinstall from GitHub. `gbrain doctor` detects a
> shadowing npm install and prints the fix.
GBrain is designed to be installed and operated by an AI agent. The fastest path is to have your agent do it for you. The CLI and MCP paths below are for people who want to wire it up themselves.
### Have your agent install it (recommended)
+36
View File
@@ -5602,6 +5602,42 @@ export async function buildChecks(
// Best-effort filesystem-hygiene check; never block doctor.
}
// 3f. npm_squat (#505). The npm registry name `gbrain` belongs to an
// unrelated third-party package — this project is NOT distributed on npm.
// A reflexive `npm i -g gbrain` / `bun add -g gbrain` installs something
// unrelated that can shadow the real binary on PATH. Classify every
// `gbrain` that `which -a` finds (pure helpers in
// src/core/npm-squat-check.ts) and warn when an unrelated install wins on
// PATH or the entry is broken. Skips silently when gbrain isn't on PATH
// at all (e.g. running via `bun src/cli.ts`).
try {
const { execSync } = await import('node:child_process');
let candidates: string[] = [];
try {
candidates = execSync('which -a gbrain', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
})
.split('\n')
.map((s) => s.trim())
.filter(Boolean);
} catch {
// `which` exits non-zero when gbrain isn't on PATH (or is missing
// entirely on this platform) — nothing to check.
}
const { assessGbrainBinaries } = await import('../core/npm-squat-check.ts');
const assessment = assessGbrainBinaries(candidates);
if (assessment.status !== 'skip') {
checks.push({
name: 'npm_squat',
status: assessment.status,
message: assessment.message,
});
}
} catch {
// Best-effort environment check; never block doctor.
}
// 3b-multi-source. Multi-source drift (v0.31.8 — D8 + D17 + OV12 + OV13).
// Pre-v0.30.3 putPage misrouted multi-source writes to (default, slug).
// For each non-default source with local_path set, walk the FS and surface
+1
View File
@@ -145,6 +145,7 @@ export const OPS_CHECK_NAMES: ReadonlySet<string> = new Set([
'federation_health',
'home_dir_in_worktree',
'index_audit',
'npm_squat',
'oauth_confidential_client_health',
'orphan_clones',
'pgbouncer_prepare',
+191
View File
@@ -0,0 +1,191 @@
/**
* npm-squat-check — classify `gbrain` binaries found on PATH (#505).
*
* The npm registry name `gbrain` belongs to an unrelated third-party package;
* this project is NOT distributed on npm. A reflexive `npm i -g gbrain` /
* `bun add -g gbrain` therefore installs something that is not this project
* and can shadow the real binary on PATH.
*
* Pure classification helpers (filesystem-only, no network, no shelling out)
* so `gbrain doctor` can warn with receipts. The caller supplies the candidate
* paths (typically the output of `which -a gbrain`).
*/
import { closeSync, openSync, readFileSync, readSync, realpathSync } from 'node:fs';
import { dirname, join } from 'node:path';
export type GbrainBinaryKind = 'real' | 'foreign' | 'broken' | 'unknown';
export interface ClassifiedGbrainBinary {
/** The candidate path as given (PATH entry / symlink). */
path: string;
kind: GbrainBinaryKind;
/** Human-readable evidence for the classification. */
detail: string;
}
export interface NpmSquatAssessment {
status: 'ok' | 'warn' | 'skip';
message: string;
binaries: ClassifiedGbrainBinary[];
}
/** Repository marker identifying this project's package.json. */
const REAL_REPO_MARKER = 'garrytan/gbrain';
/** The documented install/remediation path, reused in doctor output. */
export const NPM_SQUAT_REMEDIATION =
`Remove the unrelated package (\`bun remove -g gbrain\` or \`npm uninstall -g gbrain\`) ` +
`and install/upgrade only via the documented path: \`bun install -g github:${REAL_REPO_MARKER}\` ` +
`(or \`git clone https://github.com/${REAL_REPO_MARKER}.git && bun install && bun link\`).`;
/**
* A `bun build --compile` gbrain binary is a native executable, not a script.
* Sniff the magic bytes: ELF, Mach-O (thin + fat), PE.
*/
function isNativeExecutable(path: string): boolean {
let fd: number | undefined;
try {
fd = openSync(path, 'r');
const buf = Buffer.alloc(4);
if (readSync(fd, buf, 0, 4, 0) < 4) return false;
const be = buf.readUInt32BE(0);
const le = buf.readUInt32LE(0);
return (
be === 0x7f454c46 || // ELF
be === 0xcafebabe || be === 0xcafebabf || // fat Mach-O
le === 0xfeedface || le === 0xfeedfacf || // Mach-O 32/64
(buf[0] === 0x4d && buf[1] === 0x5a) // PE ("MZ")
);
} catch {
return false;
} finally {
if (fd !== undefined) closeSync(fd);
}
}
/** Walk up from `start` to the nearest parseable package.json. */
function nearestPackageJson(start: string): { dir: string; pkg: Record<string, any> } | null {
let cur = start;
for (let depth = 0; depth < 64; depth++) {
try {
const pkg = JSON.parse(readFileSync(join(cur, 'package.json'), 'utf8'));
if (pkg && typeof pkg === 'object') return { dir: cur, pkg };
} catch {
// Missing or unparseable at this level; keep walking.
}
const parent = dirname(cur);
if (parent === cur) break;
cur = parent;
}
return null;
}
/**
* Is this package.json THIS project? Two markers, either suffices:
* - repository field pointing at garrytan/gbrain (string or { url }), or
* - this repo's known bin shape (`"bin": { "gbrain": "src/cli.ts" }` — a
* git checkout / `bun install -g github:...` install carries it verbatim;
* a registry-published package ships built JS, not a bare .ts bin).
*/
function isRealGbrainPackage(pkg: Record<string, any>): boolean {
const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
if (typeof repo === 'string' && repo.includes(REAL_REPO_MARKER)) return true;
if (pkg.bin && typeof pkg.bin === 'object' && pkg.bin.gbrain === 'src/cli.ts') return true;
return false;
}
/**
* Classify one candidate `gbrain` path:
* - 'broken' : symlink that doesn't resolve / unreadable path.
* - 'real' : compiled gbrain binary, or a script whose nearest
* package.json is this project's (repo checkout / bun link /
* `bun install -g github:garrytan/gbrain`).
* - 'foreign' : nearest package.json is named "gbrain" but is NOT this
* project — an unrelated registry install.
* - 'unknown' : can't tell (no gbrain package.json above the resolved file).
*/
export function classifyGbrainBinary(path: string): ClassifiedGbrainBinary {
let resolved: string;
try {
resolved = realpathSync(path);
} catch {
return { path, kind: 'broken', detail: 'broken symlink or unreadable path' };
}
if (isNativeExecutable(resolved)) {
return { path, kind: 'real', detail: `compiled gbrain binary at ${resolved}` };
}
const found = nearestPackageJson(dirname(resolved));
if (!found || found.pkg.name !== 'gbrain') {
return { path, kind: 'unknown', detail: `no gbrain package.json found above ${resolved}` };
}
if (isRealGbrainPackage(found.pkg)) {
return { path, kind: 'real', detail: `this project's install at ${found.dir}` };
}
return {
path,
kind: 'foreign',
detail: `unrelated npm package named "gbrain" at ${found.dir}`,
};
}
/**
* Assess candidate paths in PATH precedence order (first entry wins when the
* shell runs `gbrain`).
*
* - skip : no candidates (gbrain not on PATH — nothing to check).
* - warn : the winning entry is broken, or an unrelated npm package shadows
* (appears before) the real binary — including when no real binary
* is on PATH at all.
* - ok : the winning entry is the real binary (an unrelated install
* sitting BEHIND it is noted but not a warn).
*/
export function assessGbrainBinaries(candidates: string[]): NpmSquatAssessment {
const unique = [...new Set(candidates.map((c) => c.trim()).filter(Boolean))];
if (unique.length === 0) {
return { status: 'skip', message: 'gbrain not found on PATH', binaries: [] };
}
const binaries = unique.map(classifyGbrainBinary);
const first = binaries[0]!;
const realIdx = binaries.findIndex((b) => b.kind === 'real');
const foreignIdx = binaries.findIndex((b) => b.kind === 'foreign');
if (first.kind === 'broken') {
return {
status: 'warn',
message:
`\`gbrain\` on PATH is a broken link (${first.path}). ` +
`Note: gbrain is NOT distributed on npm — the npm package named "gbrain" is unrelated. ` +
NPM_SQUAT_REMEDIATION,
binaries,
};
}
if (foreignIdx !== -1 && (realIdx === -1 || foreignIdx < realIdx)) {
const foreign = binaries[foreignIdx]!;
return {
status: 'warn',
message:
`\`gbrain\` on PATH resolves to an unrelated npm package, not this project ` +
`(${foreign.path}${foreign.detail}). gbrain is NOT distributed on npm. ` +
NPM_SQUAT_REMEDIATION,
binaries,
};
}
if (foreignIdx !== -1) {
return {
status: 'ok',
message:
`real gbrain wins on PATH (${first.path}), but an unrelated npm package named ` +
`"gbrain" is also installed (${binaries[foreignIdx]!.path}). Consider removing it: ` +
`\`bun remove -g gbrain\` / \`npm uninstall -g gbrain\`.`,
binaries,
};
}
return {
status: 'ok',
message:
first.kind === 'real'
? `gbrain on PATH is the real binary (${first.path}).`
: `no unrelated npm "gbrain" install detected on PATH (${first.path}).`,
binaries,
};
}
+160
View File
@@ -0,0 +1,160 @@
/**
* Unit tests for src/core/npm-squat-check.ts (#505).
*
* Tmp-dir fixtures only — fake package.json files and symlinks, no network,
* no real npm install.
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
assessGbrainBinaries,
classifyGbrainBinary,
} from '../src/core/npm-squat-check.ts';
let root: string;
/** Lay down a package dir with a package.json + a script bin; return bin path. */
function makePkg(dir: string, pkg: Record<string, unknown>, binRel = 'cli.js'): string {
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg));
const bin = join(dir, binRel);
mkdirSync(join(bin, '..'), { recursive: true });
writeFileSync(bin, '#!/usr/bin/env node\nconsole.log("hi");\n');
return bin;
}
let foreignLink: string; // symlink → unrelated npm package named "gbrain"
let realBinShapeLink: string; // symlink → checkout with bin.gbrain = src/cli.ts
let realRepoFieldLink: string; // symlink → package with garrytan/gbrain repository url
let brokenLink: string;
let nativeBin: string; // fake compiled binary (ELF magic)
let orphanScript: string; // script with no package.json above it
beforeAll(() => {
root = mkdtempSync(join(tmpdir(), 'npm-squat-'));
const binDir = join(root, 'bin');
mkdirSync(binDir, { recursive: true });
// Unrelated registry package: name "gbrain" but neither real marker.
const foreignBin = makePkg(
join(root, 'global', 'node_modules', 'gbrain'),
{ name: 'gbrain', version: '9.9.9', bin: { gbrain: 'cli.js' } },
);
foreignLink = join(binDir, 'gbrain-foreign');
symlinkSync(foreignBin, foreignLink);
// Real project by bin shape (repo checkout / bun link / github: install).
const realBin = makePkg(
join(root, 'checkout'),
{ name: 'gbrain', version: '0.42.0.0', bin: { gbrain: 'src/cli.ts' } },
join('src', 'cli.ts'),
);
realBinShapeLink = join(binDir, 'gbrain-real');
symlinkSync(realBin, realBinShapeLink);
// Real project by repository field.
const repoFieldBin = makePkg(
join(root, 'repo-field'),
{
name: 'gbrain',
repository: { type: 'git', url: 'git+https://github.com/garrytan/gbrain.git' },
bin: { gbrain: 'dist/cli.js' },
},
join('dist', 'cli.js'),
);
realRepoFieldLink = join(binDir, 'gbrain-repofield');
symlinkSync(repoFieldBin, realRepoFieldLink);
// Broken symlink.
brokenLink = join(binDir, 'gbrain-broken');
symlinkSync(join(root, 'does-not-exist'), brokenLink);
// Fake compiled binary: ELF magic bytes, no package.json context needed.
nativeBin = join(binDir, 'gbrain-native');
writeFileSync(nativeBin, Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00]));
// Script with no package.json anywhere above (tmpdir has none).
orphanScript = join(binDir, 'gbrain-orphan');
writeFileSync(orphanScript, '#!/bin/sh\necho hi\n');
});
afterAll(() => {
rmSync(root, { recursive: true, force: true });
});
describe('classifyGbrainBinary', () => {
test('unrelated npm package named gbrain → foreign', () => {
const c = classifyGbrainBinary(foreignLink);
expect(c.kind).toBe('foreign');
expect(c.detail).toContain('unrelated npm package');
});
test('repo checkout bin shape (src/cli.ts) → real', () => {
expect(classifyGbrainBinary(realBinShapeLink).kind).toBe('real');
});
test('garrytan/gbrain repository field → real', () => {
expect(classifyGbrainBinary(realRepoFieldLink).kind).toBe('real');
});
test('broken symlink → broken', () => {
expect(classifyGbrainBinary(brokenLink).kind).toBe('broken');
});
test('compiled native binary → real', () => {
const c = classifyGbrainBinary(nativeBin);
expect(c.kind).toBe('real');
expect(c.detail).toContain('compiled');
});
test('script with no gbrain package.json above → unknown', () => {
expect(classifyGbrainBinary(orphanScript).kind).toBe('unknown');
});
});
describe('assessGbrainBinaries', () => {
test('no candidates → skip', () => {
expect(assessGbrainBinaries([]).status).toBe('skip');
expect(assessGbrainBinaries(['', ' ']).status).toBe('skip');
});
test('foreign shadowing real → warn with remediation', () => {
const a = assessGbrainBinaries([foreignLink, realBinShapeLink]);
expect(a.status).toBe('warn');
expect(a.message).toContain('unrelated npm package');
expect(a.message).toContain('bun install -g github:garrytan/gbrain');
});
test('only foreign on PATH → warn', () => {
expect(assessGbrainBinaries([foreignLink]).status).toBe('warn');
});
test('broken entry wins on PATH → warn', () => {
const a = assessGbrainBinaries([brokenLink, realBinShapeLink]);
expect(a.status).toBe('warn');
expect(a.message).toContain('broken');
});
test('real first, foreign behind → ok but noted', () => {
const a = assessGbrainBinaries([realBinShapeLink, foreignLink]);
expect(a.status).toBe('ok');
expect(a.message).toContain('also installed');
});
test('clean real binary → ok', () => {
const a = assessGbrainBinaries([nativeBin]);
expect(a.status).toBe('ok');
expect(a.binaries[0]!.kind).toBe('real');
});
test('unknown only → ok (fail-open, no false alarm)', () => {
expect(assessGbrainBinaries([orphanScript]).status).toBe('ok');
});
test('duplicate PATH entries deduped', () => {
const a = assessGbrainBinaries([realBinShapeLink, realBinShapeLink]);
expect(a.binaries.length).toBe(1);
});
});