Files
ruflo/.github/workflows/ci.yml
rUvandClaude 1e7a33a768 dream(security): #3384 add PATH/search-path family to validateEnv() denylist (evaluated, ACCEPT-scoped) (#3385)
* dream(security): #3384 add PATH/search-path family to validateEnv() denylist (evaluated, ACCEPT-scoped)

validateEnv()'s loader-hijack denylist blocked LD_PRELOAD/NODE_OPTIONS-class
vars but not PATH itself or the equivalent interpreter/VCS search-path
family (CWE-427) -- flagged grade-A by last night's security scan.

Adds PATH, PYTHONPATH, PERL5LIB, RUBYLIB, GIT_EXTERNAL_DIFF,
GIT_SSH_COMMAND, BASH_ENV, IFS to the denylist, closing the only
MCP-reachable exposure (terminal_create/terminal_execute). An independent
adversarial critic found the lookup was case-sensitive (path/Path bypass
it on case-insensitive Windows); fixed same session via .toUpperCase()
case-folding, which also hardens the original 9 names.

13 new discriminating tests (32/32 total), baseline-fails/candidate-passes
verified via git-stash isolation and independently reproduced twice by
the critic. Full suite failed-file set byte-identical to baseline (36
pre-existing environmental failures) -- zero new failures.

Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01Sq3BMM7SqC5dzEaQiogNyp

* docs(dream-cycle): update LEDGER.md with 2026-09-21 security row

Records tonight's PATH/search-path denylist fix, the new 2026-09-13/14
no-run gap (confirmed via git ls-remote), the 09-15..09-20 backlog
(confirmed real, not transcribed per established convention), and a
correction that search_pull_requests's merged field is stale/unreliable
(pull_request_read is authoritative).

Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01Sq3BMM7SqC5dzEaQiogNyp

* fix(ci): build+overlay local @claude-flow/cli-core before running the test ratchet

PR #3385's own new tests (validate-input-env.test.ts) passed locally but
failed in CI's Test Suite job: package-lock.json resolves
@claude-flow/cli-core from the published npm registry tarball, not an
npm workspace link (unlike @claude-flow/security, which the previous
step in this same job already handles for the same reason). Since
scripts/ci-test-ratchet.mjs runs vitest from repo root,
v3/@claude-flow/cli's tests resolve @claude-flow/cli-core by walking up
to root node_modules -- the stale published dist/, never the local
source. Building the local package and overlaying its dist/ onto the
installed one (same version, so not a version bump) makes CI test the
actual PR diff instead of a stale registry snapshot, without touching
package-lock.json.

Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01Sq3BMM7SqC5dzEaQiogNyp

* dream(security): #3384 re-validate persisted session.env at execute time

Post-review fix on PR #3385: terminal_execute() trusted session.env from
the on-disk store unconditionally after terminal_create's one-time
validateEnv() call. A session persisted before a denylist update (or a
store.json edited/restored out of band) could carry a now-denylisted
env var (PATH, etc.) forever, silently bypassing tonight's fix -- and
the original 9-name denylist -- for every execute on that pre-existing
session.

terminal_execute now re-validates session.env via validateEnv() and
drops it to {} on failure rather than merging it unchecked, so a stale/
tampered session degrades to a clean environment instead of blocking.
2 new tests (stash-isolated: fails on the pre-fix handler, passes on
the fix); full package suite unaffected (same 36 pre-existing
environmental failures as before this commit).

Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01Sq3BMM7SqC5dzEaQiogNyp

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-26 10:07:43 -05:00

352 lines
13 KiB
YAML

name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
# Run tests daily at 2 AM UTC
- cron: '0 2 * * *'
env:
NODE_VERSION: '20'
jobs:
# Code quality and security checks
security:
name: Security & Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
uses: ./.github/actions/npm-ci-retry
- name: Run security audit
run: |
npm audit --audit-level=high || echo "⚠️ Security vulnerabilities found (non-blocking)"
npm audit --production --audit-level=moderate || echo "⚠️ Production vulnerabilities found (non-blocking)"
continue-on-error: true
- name: Lint code
run: npm run lint
- name: Type check
run: npm run typecheck || echo "⚠️ Type checking skipped (TypeScript compiler crash)"
continue-on-error: true
- name: Check for outdated dependencies
run: npm outdated || true
continue-on-error: true
- name: License compliance check
run: npx license-checker --onlyAllow 'MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;CC0-1.0' || true
continue-on-error: true
# All tests
test:
name: Test Suite
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
uses: ./.github/actions/npm-ci-retry
- name: Build workspace-linked packages needed at runtime by tests
# `npm ci` at repo root links @claude-flow/security in via the root
# npm workspace (package-lock.json records it as a `link: true`
# entry resolving to v3/@claude-flow/security, not a registry
# fetch) but never builds it, so importers hit
# ERR_MODULE_NOT_FOUND on its dist/index.js at runtime — a
# pre-existing gap (see v3/@claude-flow/cli/__tests__/policy-runtime.test.ts's
# long-standing scripts/ci-test-baseline.txt entry for the same
# root cause) that a second, unrelated package now also needs
# built for: dual-mode.test.ts/dual-mode-stdin-2947.test.ts import
# @claude-flow/security for the first time in @claude-flow/codex.
# (Complements, not duplicates, the codex package-lock.json fix
# above it in history — that made npm ci link the package for
# codex at all; this makes the linked package actually runnable.)
run: npm run build --workspace=@claude-flow/security
- name: Build local @claude-flow/cli-core and overlay onto the installed package
# dream-cycle 2026-09-21: unlike @claude-flow/security above,
# package-lock.json resolves @claude-flow/cli-core from the
# published npm registry tarball (not an npm workspace `link`),
# even though its version (3.7.0-alpha.5) matches the local
# v3/@claude-flow/cli-core source exactly. `node scripts/ci-test-ratchet.mjs`
# below runs vitest from repo root, so v3/@claude-flow/cli's tests
# resolve `@claude-flow/cli-core` by walking up to the root's
# node_modules — the stale published dist/, not local source edits.
# This surfaced when validate-input-env.test.ts's new assertions
# (PR #3385) passed locally but failed in CI against the untouched
# published denylist. Building the local source and overlaying its
# dist/ onto the installed package (same version, so this is not a
# version bump) makes CI test the actual PR diff instead of a
# stale registry snapshot, without touching package-lock.json.
run: |
npm run build --prefix v3/@claude-flow/cli-core
rm -rf node_modules/@claude-flow/cli-core/dist
cp -r v3/@claude-flow/cli-core/dist node_modules/@claude-flow/cli-core/dist
- name: Run all tests
# #3030: the historical suite has known debt, but new failing files
# are release-blocking. The committed file-level baseline may only
# shrink; replacing an old failure with a new one still fails.
run: node scripts/ci-test-ratchet.mjs
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.os }}
path: .test-results/vitest.json
if-no-files-found: error
# Documentation generation
docs:
name: Documentation & Examples
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Check documentation
run: |
echo "✅ Documentation check passed"
ls -la README.md CHANGELOG.md
# Build and package
build:
name: Build & Package (${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: [security, test]
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
include:
- os: ubuntu-latest
platform: linux
- os: macos-latest
platform: darwin
- os: windows-latest
platform: win32
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: |
# Retries absorb the transient sharp/libvips "socket hang up" /
# 503 download flake (see .github/actions/npm-ci-retry) — this
# step predates that composite action and can't call it directly
# since it needs the OS-conditional --omit=optional fallback too.
for attempt in 1 2 3; do
if [ "${{ runner.os }}" == "Linux" ]; then
npm ci --legacy-peer-deps && exit 0
else
(npm ci --legacy-peer-deps --omit=optional || npm ci --legacy-peer-deps --force) && exit 0
fi
echo "npm ci failed (attempt $attempt/3) — retrying in 15s"
sleep 15
done
echo "npm ci failed after 3 attempts"
exit 1
shell: bash
- name: Build project
run: |
echo "Building project for ${{ matrix.platform }}..."
npm run build:ts
- name: Test CLI binary (Unix)
if: runner.os != 'Windows'
run: |
chmod +x ./v3/@claude-flow/cli/bin/cli.js
node ./v3/@claude-flow/cli/bin/cli.js --version
continue-on-error: true
- name: Test CLI binary (Windows)
if: runner.os == 'Windows'
run: |
node ./v3/@claude-flow/cli/bin/cli.js --version
continue-on-error: true
- name: Daemon survives parent exit (Windows, regression #1766)
if: runner.os == 'Windows'
timeout-minutes: 15
shell: pwsh
run: |
# We install the published `@claude-flow/cli@alpha` inside an isolated
# temp dir rather than running the source-tree CLI directly, because
# the source-tree CLI imports the sibling workspace package
# `@claude-flow/cli-core` (split out in #1764) which isn't always
# resolvable from a source checkout. Skipping the `ruflo` umbrella
# (just a thin wrapper) keeps the install footprint smaller on the
# cold-cache Windows runner.
$ErrorActionPreference = 'Stop'
$tmp = Join-Path $env:RUNNER_TEMP 'daemon-1766'
if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp }
New-Item -ItemType Directory -Path $tmp | Out-Null
Set-Location $tmp
Write-Host "::group::Phase 1: install @claude-flow/cli@alpha"
$installSw = [System.Diagnostics.Stopwatch]::StartNew()
npm init -y | Out-Null
npm install '@claude-flow/cli@alpha' --no-audit --no-fund --omit=optional --omit=dev
$installSw.Stop()
Write-Host "Install took $($installSw.Elapsed.TotalSeconds)s"
$cli = Join-Path $tmp 'node_modules/@claude-flow/cli/bin/cli.js'
if (-not (Test-Path $cli)) { throw "CLI not found at $cli after npm install" }
$cliVer = (Get-Content (Join-Path $tmp 'node_modules/@claude-flow/cli/package.json') | ConvertFrom-Json).version
Write-Host "Testing daemon survival for @claude-flow/cli@$cliVer"
Write-Host "::endgroup::"
Write-Host "::group::Phase 2: spawn daemon via cmd.exe (parent exits when node returns)"
# Use raw .NET Process.Start with cmd.exe /c as the spawn parent.
# cmd.exe is a clean Windows parent that exits the moment node exits,
# giving us a deterministic "parent gone" signal — without the PS7
# `Start-Process -Wait -NoNewWindow -RedirectStandard*` hang where
# the parent waits indefinitely on output streams it inherited.
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = 'cmd.exe'
$psi.Arguments = "/c node `"$cli`" daemon start"
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.WorkingDirectory = $tmp
# No stdio redirect — let cmd inherit nothing meaningful, daemon's
# fork() opts use stdio:'ignore' anyway so nothing leaks back here.
$spawnSw = [System.Diagnostics.Stopwatch]::StartNew()
$proc = [System.Diagnostics.Process]::Start($psi)
if (-not $proc.WaitForExit(120000)) {
$proc.Kill($true)
throw "FAIL: cmd.exe parent did not exit in 120s — daemon start hung"
}
$spawnSw.Stop()
Write-Host "cmd.exe parent exited in $($spawnSw.Elapsed.TotalSeconds)s with code $($proc.ExitCode)"
Write-Host "::endgroup::"
Write-Host "::group::Phase 3: verify daemon survives parent exit"
$pidFile = Join-Path $tmp '.claude-flow/daemon.pid'
if (-not (Test-Path $pidFile)) {
throw "FAIL: pid file $pidFile was never written — daemon failed to start"
}
$daemonPid = (Get-Content $pidFile).Trim()
Write-Host "Daemon recorded PID = $daemonPid"
# Original #1766 symptom: daemon died within ~1s of parent exit.
# Wait 5s — well past any plausible delayed-teardown race.
Start-Sleep -Seconds 5
$alive = Get-Process -Id $daemonPid -ErrorAction SilentlyContinue
if ($null -eq $alive) {
throw "FAIL: daemon PID $daemonPid is no longer running 5s after parent exit (regression of #1766 in @claude-flow/cli@$cliVer)"
}
Write-Host "PASS: daemon PID $daemonPid alive 5s after parent exit (@claude-flow/cli@$cliVer)"
Write-Host "::endgroup::"
Write-Host "::group::Phase 4: cleanup"
# Force-kill is fine here (CI ephemeral runner) — saves time vs the
# interactive `daemon stop` path which on Windows shells out to ps/grep.
Stop-Process -Id $daemonPid -Force -ErrorAction SilentlyContinue
Write-Host "::endgroup::"
- name: Package build
run: |
npm pack
ls -la *.tgz
shell: bash
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts-${{ matrix.platform }}
path: |
dist/
bin/
*.tgz
# Deployment (only on main branch)
deploy:
name: Deploy & Release
runs-on: ubuntu-latest
needs: [build]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download Linux build
uses: actions/download-artifact@v4
with:
name: build-artifacts-linux
path: dist-linux/
- name: Download macOS build
uses: actions/download-artifact@v4
with:
name: build-artifacts-darwin
path: dist-darwin/
- name: Download Windows build
uses: actions/download-artifact@v4
with:
name: build-artifacts-win32
path: dist-windows/
- name: Prepare for deployment
run: |
echo "✅ Ready for deployment"
echo "Version: $(node -p "require('./package.json').version")"
echo "Platform builds:"
ls -la dist-*/
# Final status check
status:
name: CI Status
runs-on: ubuntu-latest
needs: [security, test, build]
if: always()
steps:
- name: Check overall status
run: |
echo "✅ CI Pipeline completed"
echo "Security: ${{ needs.security.result }}"
echo "Test: ${{ needs.test.result }}"
echo "Build: ${{ needs.build.result }}"