Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
{
"// <fleet>": "Managed by socket-wheelhouse; edit the template, then cascade.",
"// auth": "No apiKeyHelper. Claude Code spawns that runner with neither env nor cwd, so it could never read AI_BALANCER_ENABLED, and a helper that returns nothing renders as 'apiKeyHelper failed: did not return a value' on every launch. The balancer route instead exports ANTHROPIC_API_KEY into CLAUDE_ENV_FILE from the ai-balancer-proxy-start SessionStart hook, which DOES receive the env and so can gate on the flag. Nothing persists in settings, so the claude.ai login is the default and needs no undo.",
"// env": "The fleet no-phone-home knobs, one copy per delivery surface. FLEET_ENV (.github/actions/fleet/setup/fleet-env.json) is the source; the shell-rc bridge and the CI workflow env already derive from it, and this block is the third surface β€” a Claude Code session and every tool it spawns. Kept in lockstep by claude-settings-env-matches-fleet-env.mts, so a knob added there fails this file until it is added here too.",
"// env": "The fleet no-phone-home knobs, one copy per delivery surface. FLEET_ENV (.github/actions/fleet/setup/fleet-env.json) is the source; the shell-rc bridge and the CI workflow env already derive from it, and this block is the third surface β€” a Claude Code session and every tool it spawns. Kept in lockstep by agent/settings-env-matches-fleet-env.mts, so a knob added there fails this file until it is added here too.",
"env": {
"AI_BALANCER_ENABLED": "1",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"COREPACK_ENABLE_PROJECT_SPEC": "0",
"DISABLE_TELEMETRY": "1",
"DO_NOT_TRACK": "1",
"NO_UPDATE_NOTIFIER": "1",
"OTEL_SDK_DISABLED": "true"
"OTEL_SDK_DISABLED": "true",
"SFW_TELEMETRY_DISABLED": "true"
},
"hooks": {
"PostToolUse": [
Expand Down
7 changes: 4 additions & 3 deletions .config/fleet/oxlintrc.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .git-hooks/_shared/canonical/source.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from './git.mts'
import type { CanonicalGitRead } from './git.mts'

function canonicalMemberSlug(root: string): string | undefined {
export function canonicalMemberSlug(root: string): string | undefined {
const remote = canonicalGitText(root, ['remote', 'get-url', 'origin'])?.trim()
// Accept the three GitHub transports, retaining the organization segment.
const match =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
// it has nothing to say about a commit a published tag has already frozen.

import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
import { debugCheck } from './check-output.mts'
import { debugCheck } from '../check-output.mts'

import { containsAiAttribution } from '../../.claude/hooks/fleet/_shared/ai-attribution.mts'
import { git } from './git.mts'
import { containsAiAttribution } from '../../../.claude/hooks/fleet/_shared/ai-attribution.mts'
import { git } from '../git.mts'
import {
reportReleaseTagExemption,
resolveRewritableCommits,
} from './push-release-tags.mts'
} from './release-tags.mts'

import type { ReleaseTagOptions } from './push-release-tags.mts'
import type { ReleaseTagOptions } from './release-tags.mts'

const logger = getDefaultLogger()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,25 @@ import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize'

import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
import { debugCheck } from './check-output.mts'
import { debugCheck } from '../check-output.mts'

import { readFileForScan, shouldSkipFile } from './file-scan.mts'
import { gitLines } from './git.mts'
import { stripTemplateLayer, suppressionFor } from './scan-core.mts'
import { readFileForScan, shouldSkipFile } from '../file-scan.mts'
import { gitLines } from '../git.mts'
import { stripTemplateLayer, suppressionFor } from '../scan-core.mts'

import type { LineHit } from './scan-core.mts'
import { scanCrossRepoPaths, scanLoggerLeaks } from './scan-code-refs.mts'
import type { LineHit } from '../scan-core.mts'
import { scanCrossRepoPaths, scanLoggerLeaks } from '../scan-code-refs.mts'
import {
scanAwsKeys,
scanGitHubTokens,
scanPersonalPaths,
scanPrivateKeys,
scanSocketApiKeys,
} from './scan-secrets.mts'
} from '../scan-secrets.mts'
import {
scanAiConfigPoison,
scanProgrammaticClaudeLockdown,
} from './scan-supply-chain.mts'
} from '../scan-supply-chain.mts'

const logger = getDefaultLogger()

Expand Down
65 changes: 65 additions & 0 deletions .git-hooks/_shared/push/pr-commit-count.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'

interface OpenPr {
baseRefName?: string | undefined
headRefName?: string | undefined
}

export function checkPrCommitCount(
remote: string,
localSha: string,
remoteRef: string,
): string | undefined {
if (!remoteRef.startsWith('refs/heads/') || /^0+$/u.test(localSha)) {
return undefined
}
const branch = remoteRef.slice('refs/heads/'.length)
const listed = spawnSync(
'gh',
[
'pr',
'list',
'--state',
'open',
'--head',
branch,
'--json',
'baseRefName,headRefName',
'--limit',
'2',
],
{ encoding: 'utf8', timeout: 5000 },
)
if (listed.status !== 0) {
return undefined
}
let prs: OpenPr[]
try {
const parsed: unknown = JSON.parse(String(listed.stdout))
if (!Array.isArray(parsed)) {
return undefined
}
prs = parsed as OpenPr[]
} catch {
return undefined
}
for (let i = 0, { length } = prs; i < length; i += 1) {
const pr = prs[i]!
if (pr.headRefName !== branch || !pr.baseRefName) {
continue
}
const counted = spawnSync(
'git',
['rev-list', '--count', `${remote}/${pr.baseRefName}..${localSha}`],
{ encoding: 'utf8', timeout: 5000 },
)
const commits = Number(String(counted.stdout ?? '').trim())
if (counted.status !== 0 || !Number.isSafeInteger(commits)) {
return `PR branch ${branch}: cannot count commits above ${pr.baseRefName}; fetch ${remote} and retry.`
}
if (commits !== 1) {
return `PR branch ${branch}: expected one commit above ${pr.baseRefName}, found ${commits}; squash onto the PR base before pushing.`
}
}
return undefined
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'

import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
import { debugCheck } from './check-output.mts'
import { debugCheck } from '../check-output.mts'

import { git } from './git.mts'
import { git } from '../git.mts'

const logger = getDefaultLogger()

Expand Down Expand Up @@ -83,7 +83,7 @@ export const computeRange = (
// This base is wider than "new work": a history repair that reattaches an
// orphaned release tag puts already-published commits back in front of it.
// Gates whose only remedy is a rewrite subtract those via
// `resolveRewritableCommits` in ./push-release-tags.mts rather than
// `resolveRewritableCommits` in ./release-tags.mts rather than
// demanding a rewrite that would re-orphan the tag.
const def = defaultBranchOf(remote)
const baseRef = `${remote}/${def}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@

import { joinAnd } from '@socketsecurity/lib-stable/arrays/join'

import { debugCheck } from './check-output.mts'
import { debugCheck } from '../check-output.mts'

import { git, gitLines } from './git.mts'
import { git, gitLines } from '../git.mts'

// How many exempt commits the notice names before it summarizes the rest.
const EXEMPT_SAMPLE_LIMIT = 5
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
sharedFleetTsconfigCheckJsonPath,
sharedTypescriptBinTscPath,
} from '../../scripts/fleet/paths/util.mts'
} from '../../../scripts/fleet/paths/util.mts'
// Pre-push repo-level gates that run against the working-tree state (not a
// commit range): submodule pristine-ness, soak-bypass date annotations, the
// fast lint/format gate, and the wheelhouse-only hook-dispatch-table drift check.
Expand All @@ -18,38 +18,38 @@ import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize'

import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'

import { gitLines } from './git.mts'
import { gitLines } from '../git.mts'
import {
debugCheck,
showCheckOutput,
showCheckResult,
} from './check-output.mts'
} from '../check-output.mts'
import {
dirtyEntry,
readTypecheckVerdict,
typecheckCacheKey,
waitForTypecheckTurn,
writeTypecheckVerdict,
} from './typecheck-cache.mts'
} from '../typecheck-cache.mts'

// The repo-wide fixer lock, the same one lint.mts and fix.mts take. Sharing
// it is deliberate: a push's typecheck should also serialize against a
// running `pnpm run fix`, since both read the whole working tree.
import {
acquireFixerLock,
fixerLockPath,
} from '../../scripts/fleet/process/fixer-lock.mts'
} from '../../../scripts/fleet/process/fixer-lock.mts'
// One owner for the path, per `paths-are-constructed-once`: a cascaded file is
// tracked twice (source + live mirror), so a literal spelled here counts as
// two construction sites on its own.
import { HEAVY_JOB_BUSY_EXIT_CODE } from '../../scripts/fleet/process/heavy-job/admission.mts'
import { HEAVY_JOB_BUSY_EXIT_CODE } from '../../../scripts/fleet/process/heavy-job/admission.mts'
import {
FLEET_TYPE_SCRIPT,
TYPECHECK_CACHE_DIR,
} from '../../scripts/fleet/paths.mts'
} from '../../../scripts/fleet/paths.mts'

import type { TypecheckVerdict } from './typecheck-cache.mts'
import { scanSoakExcludeDateAnnotations } from './scan-supply-chain.mts'
import type { TypecheckVerdict } from '../typecheck-cache.mts'
import { scanSoakExcludeDateAnnotations } from '../scan-supply-chain.mts'

const logger = getDefaultLogger()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { existsSync, readFileSync } from 'node:fs'
import process from 'node:process'

import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
import { debugCheck } from './check-output.mts'
import { debugCheck } from '../check-output.mts'

import { git, gitLines } from './git.mts'
import { git, gitLines } from '../git.mts'

const logger = getDefaultLogger()

Expand Down
30 changes: 24 additions & 6 deletions .git-hooks/_shared/run-step.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ run_pkg_step_bounded() {
# seconds, and the budget is the hang ceiling that keeps a deadlock (e.g. the
# Socket Firewall sfw proxy + a worker blocking on each other) from ever hanging
# the commit past PRECOMMIT_STEP_BUDGET_S. A real lint/test FAILURE (clean
# non-zero before the budget) still BLOCKS the commit β€” only a budget-exceeding
# HANG is skipped, and the pre-push `--all` gate + CI run the full suite. The
# non-zero, including during timeout cleanup) still BLOCKS the commit β€” only a
# budget-exceeding HANG is skipped. The pre-push `--all` gate + CI run the full suite. The
# ceiling is enforced by scripts/fleet/check/precommit-steps-are-bounded.mts,
# which fails if a heavy step is invoked un-bounded or the budget drifts above
# its cap.
Expand All @@ -156,7 +156,7 @@ run_step_bounded() {
return 1
fi
set -m
{ "$@" >"$step_log" 2>&1; } &
{ exec "$@" >"$step_log" 2>&1; } &
job=$!
set +m
fi
Expand All @@ -168,11 +168,29 @@ run_step_bounded() {
while kill -0 "$job" 2>/dev/null; do
if [ "$elapsed" -ge "$PRECOMMIT_STEP_BUDGET_S" ]; then
# Budget blown β€” a deadlock or an over-broad related-set. Take out the
# whole group (sfw wrapper + workers), TERM then KILL, and fail open.
# whole group (sfw wrapper + workers), TERM then KILL.
# The kills run in an stderr-discarded subshell so the shell's
# "Terminated" job-control notice doesn't leak into the commit output.
{ kill -- -"$job"; sleep 1; kill -9 -- -"$job"; } 2>/dev/null
wait "$job" 2>/dev/null
timeout_signalled=false
{
if kill -- -"$job"; then timeout_signalled=true; fi
sleep 1
kill -9 -- -"$job"
} 2>/dev/null
if wait "$job" 2>/dev/null; then
status=0
else
status=$?
fi
case "$status:$timeout_signalled" in
0:*|137:true|143:true) ;;
*)
show_step_output
printf '\n========== pre-commit: %s FAILED (exit %s) ==========\n' "$step_name" "$status"
printf '\n========== full log: %s ==========\n' "$step_log"
return "$status"
;;
esac
cat "$step_log" 2>/dev/null
rm -f "$step_log"
printf '\n========== pre-commit: %s SKIPPED (budget %ss exceeded) ==========\n' \
Expand Down
1 change: 1 addition & 0 deletions .git-hooks/_shared/scan-core.mts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {

export const stripTemplateLayer = (p: string): string =>
p
.replace(/^template\/base\/(?:conditional|universal)\//, 'template/')
.replace(/^template\/(?:base|mono|solo)\//, 'template/')
.replace(/^template\/overrides\/[^/]+\//, 'template/')

Expand Down
24 changes: 15 additions & 9 deletions .git-hooks/fleet/pre-push.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
// already-merged history. Release tags do bound it in the other direction:
// the force-push fallback widens the base to remote/<default_branch>, which
// can sweep in commits a published tag already froze, so the AI-attribution
// gate subtracts tag-reachable commits (../_shared/push-release-tags.mts).
// gate subtracts tag-reachable commits (../_shared/push/release-tags.mts).
//
// Stdin format, provided by git: one push line per ref, each line:
// <local_ref> <local_sha> <remote_ref> <remote_sha>
//
// This entry point is a thin orchestrator: each gate lives in a focused
// `../_shared/push-*.mts` leaf, and `main` sequences them per push line.
// `../_shared/push/*.mts` leaf, and `main` sequences them per push line.

import process from 'node:process'

Expand All @@ -35,19 +35,20 @@ import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
// assumes native .mts type stripping.
import { splitLines } from '../_shared/helpers.mts'
import { debugCheck } from '../_shared/check-output.mts'
import { scanCommitMessages } from '../_shared/push-commit-messages.mts'
import { scanFilesInRange } from '../_shared/push-file-scan.mts'
import { computeRange } from '../_shared/push-range.mts'
import { scanCommitMessages } from '../_shared/push/commit-messages.mts'
import { scanFilesInRange } from '../_shared/push/file-scan.mts'
import { computeRange } from '../_shared/push/range.mts'
import {
checkSubmodules,
scanDispatchDrift,
scanFastChecks,
scanSoakAnnotations,
scanTypeCheck,
} from '../_shared/push-repo-gates.mts'
import { scanSignedCommits } from '../_shared/push-signatures.mts'
import { isDurableBackupPush } from '../_shared/push-durable-ref.mts'
import { isSquashHistoryRepo } from '../_shared/push-squash-history.mts'
} from '../_shared/push/repo-gates.mts'
import { scanSignedCommits } from '../_shared/push/signatures.mts'
import { isDurableBackupPush } from '../_shared/push/durable-ref.mts'
import { isSquashHistoryRepo } from '../_shared/push/squash-history.mts'
import { checkPrCommitCount } from '../_shared/push/pr-commit-count.mts'

const logger = getDefaultLogger()

Expand Down Expand Up @@ -93,6 +94,11 @@ const main = async (): Promise<number> => {
continue
}
pushedRemoteRefs.push(remoteRef)
const prCommitError = checkPrCommitCount(remote, localSha, remoteRef)
if (prCommitError) {
logger.fail(prCommitError)
totalErrors += 1
}
const range = computeRange(remote, localRef, localSha, remoteSha)
// `computeRange` returns `undefined` for skip cases (tags, deletions, new
// branches); use loose equality so both `null` and `undefined` skip. A
Expand Down
Loading