Compare commits

...
Author SHA1 Message Date
Time Attakc 09be4ae85d Merge branch 'master' into build/npm-squat-guard 2026-07-28 00:25:08 -07:00
Time Attakc 54e66f03ee Merge branch 'master' into build/npm-squat-guard 2026-07-27 17:49:57 -07:00
Garry TanandClaude Fable 5 89a3179e96 fix(doctor,docs): warn that the npm name 'gbrain' is unrelated + detect a shadowing npm install (#505)
GBrain is not distributed on npm; the npm package named 'gbrain' is an
unrelated package. Users who reflexively run 'npm i -g gbrain' /
'bun add -g gbrain' get something that is not this project and can shadow
the real binary on PATH.

- README.md + INSTALL_FOR_AGENTS.md: prominent warning at the top of the
  install sections — install/upgrade only via the documented paths
  ('bun install -g github:garrytan/gbrain' or git clone + bun link).
- New 'npm_squat' doctor check: classifies every 'gbrain' from
  'which -a' via pure helpers in src/core/npm-squat-check.ts
  (real compiled binary / repo checkout vs unrelated registry install vs
  broken link), warns with remediation when an unrelated install wins on
  PATH, skips silently when gbrain isn't on PATH.
- Unit tests with tmp-dir fixtures (no network, no real npm install).
- llms bundles regenerated (README/INSTALL_FOR_AGENTS are inlined).

Closes #505

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 16:21:53 -07:00
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
@@ -1006,6 +1006,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
@@ -1559,6 +1566,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
@@ -5592,6 +5592,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);
});
});