diff --git a/.claude/hooks/claude-md-section-size-guard/README.md b/.claude/hooks/claude-md-section-size-guard/README.md index 444027e..54c4b97 100644 --- a/.claude/hooks/claude-md-section-size-guard/README.md +++ b/.claude/hooks/claude-md-section-size-guard/README.md @@ -4,7 +4,7 @@ PreToolUse hook that caps the body length of individual `### ` sections inside t ## What it does -Complements `claude-md-size-guard` (40KB byte cap on the whole block) by enforcing a per-section line cap inside the block. Each `### Section heading` inside the `` markers gets at most **8 body lines** (configurable via `CLAUDE_MD_FLEET_SECTION_MAX_LINES`). +Complements `claude-md-size-guard` (48KB byte cap on the whole block) by enforcing a per-section line cap inside the block. Each `### Section heading` inside the `` markers gets at most **8 body lines** (configurable via `CLAUDE_MD_FLEET_SECTION_MAX_LINES`). Sections that exceed 8 lines should have a long-form companion at `docs/claude.md/fleet/.md` and the inline body should shrink to 1-2 sentences plus a link. The cap was 20 initially (during the bootstrap when several fleet sections were 12-19 lines); it tightened to 8 once those sections were outsourced. @@ -24,7 +24,7 @@ When a section exceeds the cap, the hook prints: ## Why a per-section cap, not just the byte cap -The failure mode this hook addresses: an operator can grow a single rule from 2 lines to 60 lines of detailed prose without ever tripping the 40KB byte cap — until enough other sections accrete that an unrelated 1-line addition breaks the build. The per-section cap catches this directly, at the moment the long content is written, when the operator has the long-form text in hand and can immediately drop it into a `docs/claude.md/fleet/.md` companion. +The failure mode this hook addresses: an operator can grow a single rule from 2 lines to 60 lines of detailed prose without ever tripping the 48KB byte cap — until enough other sections accrete that an unrelated 1-line addition breaks the build. The per-section cap catches this directly, at the moment the long content is written, when the operator has the long-form text in hand and can immediately drop it into a `docs/claude.md/fleet/.md` companion. ## Override diff --git a/.claude/hooks/claude-md-size-guard/README.md b/.claude/hooks/claude-md-size-guard/README.md index ead7e36..910c722 100644 --- a/.claude/hooks/claude-md-size-guard/README.md +++ b/.claude/hooks/claude-md-size-guard/README.md @@ -1,10 +1,10 @@ # claude-md-size-guard -PreToolUse Edit/Write hook that blocks CLAUDE.md edits which would push the **fleet-canonical block** (between `` / `` markers) above 40 KB. +PreToolUse Edit/Write hook that blocks CLAUDE.md edits which would push the **fleet-canonical block** (between `` / `` markers) above 48 KB. ## Why -The fleet block is byte-identical across every `socket-*` repo. Every byte added there costs N copies of in-context tokens fleet-wide. Per-repo content outside the markers is paid once. Capping the fleet block at 40 KB: +The fleet block is byte-identical across every `socket-*` repo. Every byte added there costs N copies of in-context tokens fleet-wide. Per-repo content outside the markers is paid once. Capping the fleet block at 48 KB: - Forces new fleet rules to be **terse + reference-deferred** (link to `docs/references/.md`). - Leaves headroom for per-repo content. Per-repo CLAUDE.md additions are NOT capped here. @@ -16,7 +16,7 @@ The hook fires on Edit/Write tool calls. For Write, it inspects `content`. For E ## Cap -- **Default:** 40 KB (40 960 bytes). +- **Default:** 48 KB (49 152 bytes). Sized to leave per-repo CLAUDE.md additions ample room outside the fleet block. - **Override:** set `CLAUDE_MD_FLEET_BLOCK_BYTES=` in env (rarely needed; bumping the cap should be a deliberate fleet-wide decision). ## Failing open diff --git a/.claude/hooks/claude-md-size-guard/index.mts b/.claude/hooks/claude-md-size-guard/index.mts index 06338ef..efd5011 100644 --- a/.claude/hooks/claude-md-size-guard/index.mts +++ b/.claude/hooks/claude-md-size-guard/index.mts @@ -2,7 +2,7 @@ // Claude Code PreToolUse hook — claude-md-size-guard. // // Blocks Edit/Write tool calls that would push the CLAUDE.md -// fleet-canonical block above the 40KB size cap. The fleet block lives +// fleet-canonical block above the 48KB size cap. The fleet block lives // between `` and `` // markers; everything outside is per-repo content owned by the host // repo (different cap, evaluated separately). @@ -24,8 +24,9 @@ // remediation (move detail into `docs/references/.md`). // // Cap policy: -// - Default: 40 KB (40_960 bytes). Override per-repo by setting -// `CLAUDE_MD_FLEET_BLOCK_BYTES` in the env (rarely needed). +// - Default: 48 KB (49_152 bytes). Sized to leave room for per-repo +// CLAUDE.md additions outside the fleet block. Override per-repo by +// setting `CLAUDE_MD_FLEET_BLOCK_BYTES` in the env (rarely needed). // - Whole-file cap: NOT enforced here. Per-repo content can grow // freely; this hook only protects the fleet block. // @@ -44,7 +45,7 @@ import process from 'node:process' import { readStdin } from '../_shared/transcript.mts' -const DEFAULT_CAP_BYTES = 40 * 1024 +const DEFAULT_CAP_BYTES = 48 * 1024 const FLEET_BEGIN_MARKER = '` would silently mismatch if reordered) — append `// socket-hook: allow regex-alternation-order` on those lines. - **String-equality disjunctions** — `x === 'a' || x === 'b' || x === 'c'` reads with the comparand strings in alpha order. The De Morgan dual `x !== 'a' && x !== 'b'` (negative-membership check) follows the same rule. The `||` chain short-circuits regardless of operand order; sorting reduces diff churn when adding new comparands and makes "is X in this set?" checks visually consistent. Auto-fixable when every clause has the same left operand and uses string-literal comparands. Mixed shape (different left, different operator, non-string right) is skipped — those are usually genuine ordering-sensitive predicates and the autofix would change semantics. -- **Boolean identifier chains** — `agentshieldOk && zizmorOk && sfwOk` reads with the names in alpha order: `agentshieldOk && sfwOk && zizmorOk`. Same rule for `||` chains. The lint rule fires only when every leaf is a bare `Identifier` (no calls, no member access, no literals, no negations) — those have side-effect or short-circuit semantics where order can be observable. Duplicate identifiers and chains with interior comments are skipped (the autofix would lose information). Enforced by `socket/sort-boolean-chains`. +- **Boolean identifier chains** — `agentshieldOk && zizmorOk && sfwOk` reads with the names in alpha order: `agentshieldOk && sfwOk && zizmorOk`. Same rule for `||` chains. The lint rule fires only when (1) every leaf is a bare `Identifier` (no calls, no member access, no literals, no negations) — those have side-effect or short-circuit semantics where order can be observable — AND (2) the chain has **3 or more operands**. Two-operand chains like `useHttp && oauthEnabled` are guard patterns where order carries narrative ("in HTTP mode, did OAuth get enabled?") that alpha-sort would destroy; only length-3+ chains are unambiguously flag lists. Duplicate identifiers and chains with interior comments are skipped (the autofix would lose information). Enforced by `socket/sort-boolean-chains`. - **TypeScript union of string literals** — `type Source = 'download' | 'path' | 'vfs'` (not `'vfs' | 'path' | 'download'`). Members are interchangeable at the type level; alpha order makes "which values can this take?" answerable without scanning. Applies to type aliases, inline parameter unions, and template-literal type alternatives. Position-bearing unions (rare — e.g. a discriminator where order encodes priority) keep their meaningful order; append `// socket-hook: allow union-order` on those lines. ## Default diff --git a/packages/build-infra/lib/release-checksums/consumer.mts b/packages/build-infra/lib/release-checksums/consumer.mts index 33222be..f069ffd 100644 --- a/packages/build-infra/lib/release-checksums/consumer.mts +++ b/packages/build-infra/lib/release-checksums/consumer.mts @@ -35,34 +35,41 @@ export interface ChecksumsResult { const checksumCache = new Map() +/** + * Clear the checksum cache. Useful for testing or forcing re-download. + */ +export function clearChecksumCache(): void { + checksumCache.clear() +} + interface GetChecksumsOptions { /** * The producing repo whose releases we're verifying against. */ repoConfig: RepoConfig /** - * Tool name prefix used in the producing repo's release tag (e.g. `iocraft`). + * Tool name prefix used in the producing repo's release tag (e.g. `lief`). */ tool: string /** * Specific tag to fetch. If omitted, uses the embedded tag, then `latest`. */ - releaseTag?: string + releaseTag?: string | undefined /** * Where to cache the downloaded `checksums.txt`. Defaults to * `/build/temp`. */ - tempDir?: string + tempDir?: string | undefined /** * Suppress info/warn logging (errors still log). */ - quiet?: boolean + quiet?: boolean | undefined /** * If true (default), use embedded checksums when available even if a network * fetch could find newer ones. Set false to force a network fetch — useful * when bumping checksums. */ - preferEmbedded?: boolean + preferEmbedded?: boolean | undefined } /** @@ -208,10 +215,3 @@ export async function getReleaseChecksums( return { checksums: {}, source: 'network', tag } } } - -/** - * Clear the checksum cache. Useful for testing or forcing re-download. - */ -export function clearChecksumCache(): void { - checksumCache.clear() -} diff --git a/packages/build-infra/lib/release-checksums/core.mts b/packages/build-infra/lib/release-checksums/core.mts index 7be5e63..aaa1ae5 100644 --- a/packages/build-infra/lib/release-checksums/core.mts +++ b/packages/build-infra/lib/release-checksums/core.mts @@ -29,7 +29,7 @@ const __dirname = path.dirname(__filename) // --------------------------------------------------------------------------- export interface ToolConfig { - description?: string + description?: string | undefined tag: string checksums: Record } @@ -37,10 +37,10 @@ export interface ToolConfig { export type EmbeddedChecksums = Record export interface VerifyResult { - actual?: string - expected?: string - source?: string - skipped?: boolean + actual?: string | undefined + expected?: string | undefined + source?: string | undefined + skipped?: boolean | undefined valid: boolean } @@ -55,27 +55,16 @@ export interface VerifyResult { let embeddedChecksums: EmbeddedChecksums | undefined | null -export function getEmbeddedChecksums(): EmbeddedChecksums | undefined { - if (embeddedChecksums === null) { - return undefined - } - if (embeddedChecksums === undefined) { - try { - const checksumPath = path.join( - __dirname, - '..', - '..', - 'release-assets.json', - ) - embeddedChecksums = JSON.parse( - readFileSync(checksumPath, 'utf8'), - ) as EmbeddedChecksums - } catch { - embeddedChecksums = null - return undefined - } +/** + * Compute SHA256 hash of a file as lowercase hex. + */ +export async function computeFileHash(filePath: string): Promise { + const hash = crypto.createHash('sha256') + const stream = createReadStream(filePath) + for await (const chunk of stream) { + hash.update(chunk) } - return embeddedChecksums + return hash.digest('hex') } export function getEmbeddedChecksum( @@ -97,20 +86,27 @@ export function getEmbeddedChecksum( return { checksum, tag: toolConfig.tag } } -// --------------------------------------------------------------------------- -// Format primitives. -// --------------------------------------------------------------------------- - -/** - * Compute SHA256 hash of a file as lowercase hex. - */ -export async function computeFileHash(filePath: string): Promise { - const hash = crypto.createHash('sha256') - const stream = createReadStream(filePath) - for await (const chunk of stream) { - hash.update(chunk) +export function getEmbeddedChecksums(): EmbeddedChecksums | undefined { + if (embeddedChecksums === null) { + return undefined } - return hash.digest('hex') + if (embeddedChecksums === undefined) { + try { + const checksumPath = path.join( + __dirname, + '..', + '..', + 'release-assets.json', + ) + embeddedChecksums = JSON.parse( + readFileSync(checksumPath, 'utf8'), + ) as EmbeddedChecksums + } catch { + embeddedChecksums = undefined + return undefined + } + } + return embeddedChecksums } /** @@ -136,15 +132,11 @@ export function parseChecksums(content: string): Record { return checksums } -// --------------------------------------------------------------------------- -// Verify. -// --------------------------------------------------------------------------- - interface VerifyOptions { filePath: string assetName: string tool: string - quiet?: boolean + quiet?: boolean | undefined } /** diff --git a/packages/build-infra/release-assets.schema.json b/packages/build-infra/release-assets.schema.json index d049c86..4044659 100644 --- a/packages/build-infra/release-assets.schema.json +++ b/packages/build-infra/release-assets.schema.json @@ -29,7 +29,7 @@ }, "tag": { "type": "string", - "description": "Full release tag, including the tool prefix (e.g. `iocraft-20260424-18f0f46`).", + "description": "Full release tag, including the tool prefix (e.g. `lief-20260507-76c1796`).", "minLength": 1 }, "checksums": { diff --git a/scripts/install-token-minifier.mts b/scripts/install-token-minifier.mts index 2430a2e..46af914 100644 --- a/scripts/install-token-minifier.mts +++ b/scripts/install-token-minifier.mts @@ -94,7 +94,7 @@ interface CatalogYamlMap { * line-shaped (key: value) and we only need the @socketsecurity/* entries the * proxy actually references. */ -function readNeededCatalogEntries(): CatalogYamlMap { +export function readNeededCatalogEntries(): CatalogYamlMap { const yamlPath = path.join(WHEELHOUSE_ROOT, 'pnpm-workspace.yaml') const text = readFileSync(yamlPath, 'utf8') const lines = text.split('\n') @@ -140,7 +140,7 @@ function readNeededCatalogEntries(): CatalogYamlMap { * catalog aliases the package source declares. Keeps imports of * `@socketsecurity/lib-stable/...` resolvable from inside the install. */ -function writeInstallWorkspaceYaml(catalog: CatalogYamlMap): void { +export function writeInstallWorkspaceYaml(catalog: CatalogYamlMap): void { const lines = ['catalog:'] for (const [k, v] of Object.entries(catalog)) { // Quote values that aren't bare versions (e.g. `npm:foo@1.0.0`). @@ -161,7 +161,7 @@ function writeInstallWorkspaceYaml(catalog: CatalogYamlMap): void { * Stripping `bin`/`exports` keeps pnpm from trying to wire global binaries at * install time — we drop our own shim explicitly. */ -function writeInstallPackageJson(sourceVersion: string): void { +export function writeInstallPackageJson(sourceVersion: string): void { const sourcePkg = JSON.parse( readFileSync(path.join(PKG_SOURCE_DIR, 'package.json'), 'utf8'), ) @@ -190,7 +190,7 @@ function writeInstallPackageJson(sourceVersion: string): void { * `fs.cp` with recursive + force is the cross-platform equivalent of `cp -r`. * Force overwrites stale files on reinstall. */ -function copySource(): void { +export function copySource(): void { // Use sync fs API for consistency with the rest of the script — this // is a one-shot install, not a hot path. `cpSync` exists since // Node 20; the recursive option is required for directories. @@ -207,14 +207,14 @@ function copySource(): void { * when the recorded x-source-version in the dest's package.json differs from * the source. */ -function readSourceVersion(): string { +export function readSourceVersion(): string { const pkg = JSON.parse( readFileSync(path.join(PKG_SOURCE_DIR, 'package.json'), 'utf8'), ) return pkg.version ?? '0.0.0' } -function readInstalledVersion(): string | undefined { +export function readInstalledVersion(): string | undefined { const installedPkgPath = path.join(INSTALL_DIR, 'package.json') if (!existsSync(installedPkgPath)) { return undefined @@ -227,7 +227,7 @@ function readInstalledVersion(): string | undefined { } } -function pnpmInstallAtDest(quiet: boolean): void { +export function pnpmInstallAtDest(quiet: boolean): void { const result = spawnSync( 'pnpm', [ @@ -248,7 +248,7 @@ function pnpmInstallAtDest(quiet: boolean): void { } } -function writeBinShim(): void { +export function writeBinShim(): void { // Shim execs the proxy's top-level bin/ entry. Source lives at // INSTALL_DIR/bin/, NOT under node_modules/ — so Node 22+ can strip // types from the .mts file at runtime. `node` is on PATH on every diff --git a/scripts/validate-bundle-deps.mts b/scripts/validate-bundle-deps.mts index 111007b..c71bb93 100644 --- a/scripts/validate-bundle-deps.mts +++ b/scripts/validate-bundle-deps.mts @@ -23,10 +23,15 @@ const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.join(__dirname, '..') -// Node.js builtins to ignore (including node: prefix variants) +// Node.js builtins to ignore (including node: prefix variants). +// node:smol-* are Socket SEA-bundled optional builtins (smol-util, smol-primordial); +// they appear in dist behind `mod.isBuiltin('node:smol-util')` guards and are only +// resolvable in SEA binaries, so they should never be expected in dependencies. +const SOCKET_SEA_BUILTINS = ['node:smol-util', 'node:smol-primordial'] const BUILTIN_MODULES = new Set([ ...builtinModules, ...builtinModules.map(m => `node:${m}`), + ...SOCKET_SEA_BUILTINS, ]) /** @@ -364,16 +369,14 @@ async function validateBundleDeps(): Promise { // externals + bundled are Set — use for...of, the // canonical fix for set / map / iterable iteration. - for (let i = 0, { length } = externals; i < length; i += 1) { - const ext = externals[i]! + for (const ext of externals) { const packageName = getPackageName(ext) if (packageName && !BUILTIN_MODULES.has(packageName)) { allExternals.add(packageName) } } - for (let i = 0, { length } = bundled; i < length; i += 1) { - const bun = bundled[i]! + for (const bun of bundled) { allBundled.add(bun) } } diff --git a/scripts/validate-file-size.mts b/scripts/validate-file-size.mts index 19b624a..41b3bcc 100644 --- a/scripts/validate-file-size.mts +++ b/scripts/validate-file-size.mts @@ -23,6 +23,19 @@ const rootPath = path.join(__dirname, '..') // Maximum file size: 2MB (2,097,152 bytes) const MAX_FILE_SIZE = 2 * 1024 * 1024 +// Allowlisted large files: fleet-canonical assets whose size is bounded by +// the upstream they ship, not by repo authoring. acorn.wasm is the AST +// parser shared by AST-based oxlint plugin rules + hooks; its ~3MB is the +// upstream build artifact. Two paths because socket-lib vendors its own +// copy at vendor/acorn-wasm/ (so the lib package's own AST helpers can +// load without a node_modules round-trip). Adding a path here is +// intentional — it should only happen for files the fleet jointly owns, +// not per-repo binary leaks. +const ALLOWED_LARGE_FILES = new Set([ + '.claude/hooks/_shared/acorn/acorn.wasm', + 'vendor/acorn-wasm/acorn.wasm', +]) + // Directories to skip const SKIP_DIRS = new Set([ '.cache', @@ -95,6 +108,9 @@ async function scanDirectory( const stats = await fs.stat(fullPath) if (stats.size > MAX_FILE_SIZE) { const relativePath = path.relative(rootPath, fullPath) + if (ALLOWED_LARGE_FILES.has(relativePath)) { + continue + } violations.push({ file: relativePath, size: stats.size,