78 Commits

  • test(ci): coverage for round-1 fixes (quoted write-all, dedup, lifecycle scope)
    Three test changes in response to the round-1 review:
    
    1. **Add quoted-write-all coverage** (cubic P0 follow-up).
       Two new cases assert the regex now matches the double-quoted and
       single-quoted YAML forms of `permissions: "write-all"`:
         - `rejects double-quoted permissions: "write-all"`
         - `rejects single-quoted permissions: 'write-all'`
       Both fixtures trigger only the persist-credentials gate, so they
       exercise the WRITE_ALL_PATTERN OR-clause in isolation.
    
    2. **Add expression+ref dedup coverage** (greptile P2 follow-up).
       `emits a single violation when both expressionPattern and refPattern
       match the same step` — uses `refs/pull/${{ … head.sha }}/merge` as
       the fixture (which matches both patterns) and counts ERROR lines for
       the `pull_request_target` rule, asserting exactly one. Re-introducing
       the duplicate-push bug would re-fail this test immediately.
    
    3. **Drop the `npm ci without --ignore-scripts under write-all` test**
       (greptile P2). That test happened to pass under the previous
       `--ignore-scripts` regex, but `UNSAFE_INSTALL_PATTERNS` (added in
       `f7035b56`) fires unconditionally for every workflow regardless of
       permissions. So the test was exercising a pre-existing code path
       that has nothing to do with WRITE_ALL_PATTERN. Reviewer flagged this
       could mislead future contributors into thinking lifecycle-script
       enforcement is gated on write permissions.
    
       Replaced by the surrounding `rejects checkout credential persistence
       in workflows with permissions: write-all` test (already present) and
       the new quoted-form tests above, which all exercise the actual
       persist-credentials gate that the WRITE_ALL_PATTERN clause newly
       activates.
    
    Test count: 22 → 24 (added 3 new, dropped 1). All green; `yarn lint`
    clean.
    
    The cohort comment above the write-all block was also tightened to
    explicitly note that "the lifecycle-script gate already fires
    unconditionally for every workflow" so the next reader sees the
    distinction up front.
  • fix(ci): flag refs/pull checkouts under pull_request_target
    The `pull_request_target` rule's `expressionPattern` matches only
    the canonical `github.event.pull_request.head.{ref,sha,repo.full_name}`
    interpolations. It does not match the second canonical form of
    the same exploit — fetching `refs/pull/<N>/{head,merge}` directly:
    
      - uses: actions/checkout@v4
        with:
          ref: refs/pull/${{ github.event.pull_request.number }}/merge
    
    The merge-ref variant is what GitHub's own security guidance calls
    out as the highest-severity privilege-escalation pattern under
    `pull_request_target`: it materialises the PR's merge commit
    (attacker code spliced with base), executes inside a workflow that
    has full repo-scoped tokens, and gives the attacker the chance to
    exfiltrate secrets or push to default branches. `refs/pull/N/head`
    is functionally equivalent — same source, same trust boundary.
    
    Reproduced on `main` before this commit:
    
      $ cat /tmp/bad.yml
      name: bad
      on: { pull_request_target: { types: [opened] } }
      permissions: { contents: read }
      jobs:
        do:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
              with:
                ref: refs/pull/${{ github.event.pull_request.number }}/merge
                persist-credentials: false
            - run: npm ci --ignore-scripts
    
      $ ECC_WORKFLOWS_DIR=/tmp node scripts/ci/validate-workflow-security.js
      Validated workflow security for 1 workflow files
      $ echo $?
      0
    
    Expected: violation flagging the refs/pull checkout under pull_request_target.
    Actual: passes silently.
    
    Fix: add a `refPattern` to the `pull_request_target` rule:
    
        /^\s*ref:\s*['"]?[^'"\n]*refs\/(?:remotes\/)?pull\/[^'"\n\s]+/m
    
    and apply it per checkout step inside the existing
    event-gated loop. The pattern matches the ref VALUE so it catches
    all interpolation shapes — `refs/pull/123/head`,
    `refs/pull/${{ github.event.pull_request.number }}/merge`,
    `${{ env.FOO }}/refs/pull/N/head` — without enumerating the
    possible interpolations themselves.
    
    Scoping: the rule is already gated on the workflow containing
    `pull_request_target:`, so non-privileged `pull_request` workflows
    that legitimately check out a PR ref are not affected.
    
    After this commit the reproduction above exits 1 with:
    
      ERROR: bad.yml:10 - pull_request_target must not checkout an untrusted pull_request head ref/repository
    
    Three new regression tests in `tests/ci/validate-workflow-security.test.js`:
      - rejects pull_request_target + refs/pull/<N>/merge
      - rejects pull_request_target + hardcoded refs/pull/<N>/head
      - allows pull_request_target with no `with.ref:` (base-ref checkout —
        the safe pattern from GitHub's own guidance)
    
    Test count: 17 → 20 in this file; full `yarn test` still green.
    
    Together with the previous commit, this closes the two
    independent `validate-workflow-security.js` bypasses I found.
  • fix(ci): treat 'permissions: write-all' as a write-permission gate
    `WRITE_PERMISSION_PATTERN` in `validate-workflow-security.js`
    enumerates named GitHub Actions scopes (`contents: write`,
    `issues: write`, etc.) to decide whether a workflow needs to:
      - disable `persist-credentials` on `actions/checkout`
      - pass `--ignore-scripts` to `npm ci`
    
    The pattern misses the top-level shorthand `permissions:
    write-all`, which is the strictly broader form — it grants every
    named scope write access in a single line. As a result, a
    workflow that opts into write-all currently slips both gates.
    
    Reproduced on `main` before this commit:
    
      $ cat /tmp/bad.yml
      name: bad
      on: [push]
      permissions: write-all
      jobs:
        do:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - run: npm ci
    
      $ ECC_WORKFLOWS_DIR=/tmp node scripts/ci/validate-workflow-security.js
      Validated workflow security for 1 workflow files
      $ echo $?
      0
    
    Expected: at least two violations (missing `persist-credentials:
    false`, missing `--ignore-scripts`).
    Actual: passes silently.
    
    Fix: add a sibling pattern `WRITE_ALL_PATTERN` that matches
    `^\s*permissions:\s*write-all\b` and OR it with
    `WRITE_PERMISSION_PATTERN` at the single gate. Both top-level
    and job-level `permissions:` blocks satisfy the `^\s*` prefix.
    
    After this commit the reproduction above exits 1 with:
    
      ERROR: bad.yml:8 - workflows with write permissions must disable checkout credential persistence
      ERROR: bad.yml:9 - workflows with write permissions must install npm dependencies with --ignore-scripts
    
    Three new regression tests in `tests/ci/validate-workflow-security.test.js`:
      - rejects write-all + credential-persisting checkout
      - rejects write-all + `npm ci` without `--ignore-scripts`
      - allows write-all when both gates are satisfied (no over-block)
    
    Test count: 14 → 17 in this file; full `yarn test` still green.
    
    A separate `refs/pull/N/merge` bypass under `pull_request_target`
    exists in the same validator and is fixed in the next commit.
  • feat: add command registry and coverage checks (#1906)
    Salvages the useful parts of #1897 without generated .caliber state or stale counts.
    
    - adds a deterministic command registry generator and drift check
    - commits the current command registry for 75 commands
    - validates the rc.1 README catalog summary against live counts
    - adds a single Ubuntu Node 20 coverage job instead of running coverage in every matrix cell
    
    Co-authored-by: jodunk <jodunk@users.noreply.github.com>
  • docs: add supply-chain incident response playbook
    Add a repo-level supply-chain incident response playbook for npm/GitHub Actions package-registry incidents, anchored on the May 2026 TanStack compromise and prior Shai-Hulud-style npm incidents.
    
    - add `docs/security/supply-chain-incident-response.md` with exposure checks, immediate response steps, workflow rules, publication rules, and escalation triggers
    - link the playbook from `SECURITY.md`
    - reject `pull_request_target` workflows that restore or save shared dependency caches
    - add a regression test for the new `pull_request_target + actions/cache` guardrail
    
    Validation:
    - node tests/ci/validate-workflow-security.test.js (12 passed, 0 failed)
    - node scripts/ci/validate-workflow-security.js (validated 7 workflow files)
    - npx markdownlint-cli 'SECURITY.md' 'docs/security/supply-chain-incident-response.md'
    - npx markdownlint-cli '**/*.md' --ignore node_modules
    - git diff --check
    - node tests/run-all.js (2377 passed, 0 failed)
    - GitHub CI for #1848 green across Ubuntu, Windows, and macOS
    
    No release, tag, npm publish, plugin tag, marketplace submission, or announcement was performed.
  • ci: require npm audit signature checks
    Require npm registry signature verification wherever workflow npm audit checks run.
    
    - add npm audit signatures to CI Security Scan and maintenance security audit jobs
    - teach the workflow security validator to reject npm audit without signature verification
    - keep the repair and Copilot prompt tests portable across Windows path/case and CRLF frontmatter behavior
    
    Validation:
    - node tests/run-all.js (2376 passed, 0 failed)
    - CI current-head matrix green on #1846
  • ci: harden workflow install boundaries
    - run non-test workflow installs with npm ci --ignore-scripts where lifecycle scripts are not needed\n- reject plain npm ci in workflows with write permissions\n- reject actions/cache in id-token: write workflows to reduce OIDC publish cache-poisoning risk
  • docs: salvage focused stale PR contributions
    - add Vite and Redis pattern skills from closed stale PRs
    
    - add frontend-slides support assets
    
    - port skill-comply runner fixes and LLM prompt/provider regressions
    
    - harden agent frontmatter validation and sync catalog counts
  • fix: harden CI validators
    Ports personal-path validator hardening and quoted checkout detection onto current main.
  • fix: sync skill frontmatter and catalog counts
    Adds missing skill frontmatter, normalizes strict YAML metadata, syncs README catalog counts, and extends catalog validation for README/plugin/marketplace count drift.
  • fix(ci): flag SKILL.md frontmatter defects in validate-skills (#1669)
    * fix(ci): flag SKILL.md frontmatter defects in validate-skills
    
    Issue #1663 reported two SKILL.md frontmatter defects (missing `name:`
    on skill-stocktake; literal block-scalar `description: |-` on
    openclaw-persona-forge) that PR #1664 addresses at the data level.
    
    This change is complementary: it extends `scripts/ci/validate-skills.js`
    to catch the same class of defect statically going forward, so the
    frontmatter-vs-renderer problems do not silently reappear as new skills
    land.
    
    ## Checks added
    - Frontmatter must declare a `name:` field.
    - Frontmatter `description:` must not use a literal block scalar
      (`|` / `|-` / `|+`) — these preserve internal newlines and break
      flat-table renderers keyed off `description`. Folded (`>`) and inline
      strings are accepted.
    
    ## Behavior
    - Frontmatter findings default to WARN (exit 0) so this PR does not
      break CI while the two known offenders are still on main. Pass
      `--strict` or set `CI_STRICT_SKILLS=1` to promote them to ERROR
      (exit 1). Structural findings (missing / empty SKILL.md) remain
      errors as before.
    - Today against main, the validator reports exactly two warnings —
      the same two files called out in #1663 — and exits 0. When #1664
      lands, the validator reports zero warnings, at which point strict
      mode can be enabled in CI.
    
    ## Parser notes
    - Bespoke frontmatter parser mirrors the style of `validate-agents.js`
      (tolerant of UTF-8 BOM and CRLF; no new npm dependency).
    - Block-scalar continuation lines are skipped so keys inside a block
      scalar are not mistaken for top-level keys.
    - Hidden directories (`.something/`) under skills/ are now skipped.
    
    ## Tests
    Adds five focused tests to `tests/ci/validators.test.js`:
    - warns when frontmatter is missing `name` (default mode)
    - errors when frontmatter is missing `name` (--strict mode)
    - warns on literal block-scalar description (|-)
    - accepts folded (>) and inline descriptions under --strict
    - skips hidden directories under skills/
    
    ## Docs
    Adds two bullets to the `Skill Checklist` in CONTRIBUTING.md covering
    the two rules now surfaced by the validator.
    
    Refs #1663. Complements (does not compete with) #1664.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * fix(ci): harden SKILL.md frontmatter checks after bot review
    
    Address findings from CodeRabbit, Greptile, and cubic on #1669:
    
    - Guard empty or whitespace-only `name:` values. Previously
      `name:    ` silently passed because the presence check only
      tested key-set membership; now inspectFrontmatter captures
      trimmed values and validate flags an explicit 'name is empty'
      WARN/ERROR.
    - Broaden block-scalar detection to cover YAML 1.2 indent
      indicators (`|2`, `|-2`, `>2-`) and trailing comments
      (`|-  # note`). The old regex required a bare `|`/`>` with
      optional `+`/`-`, which let valid-but-disallowed forms slip
      through.
    - Update CONTRIBUTING.md checklist to list `|+` alongside `|`
      and `|-` for parity with the validator.
    - Extend runSkillsValidator to accept env overrides and add four
      regression tests: empty name, |+ description, |-2 + comment, and
      CI_STRICT_SKILLS=1.
    
    * fix(ci): address round-2 review on validate-skills frontmatter
    
    - Tighten extractFrontmatter closing delimiter to require a newline or
      end-of-file after the closing `---`, so body lines beginning with
      `---text` are not parsed as frontmatter (CodeRabbit).
    - Strip both trailing and comment-only values in inspectFrontmatter, so
      `name: # todo` is surfaced as empty rather than silently passing
      (cubic P2).
    - Extract validateSkillDir helper so the per-directory validation
      block moves out of validateSkills, keeping both functions under the
      50-line guideline (CodeRabbit nit).
    - Hoist runSkillsValidator to module scope in the test harness and
      share the spawnSync import with execFileSync so the helper stops
      re-requiring child_process on every invocation (CodeRabbit nit).
    - Add regression tests: comment-only `name:` values must fail strict
      mode; `---trailing` body lines must not be parsed as frontmatter.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * Update tests/ci/validators.test.js
    
    Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
  • fix(tests): resolve Windows CI test failures (#701)
    * fix(tests): skip bash tests on Windows and fix USERPROFILE in resolve-ecc-root
    
    - hooks.test.js: add SKIP_BASH guard for 8 bash-dependent tests
      (detect-project.sh, observe.sh) while keeping 207 Node.js tests running
    - resolve-ecc-root.test.js: add USERPROFILE to env overrides in 2
      INLINE_RESOLVE tests so os.homedir() resolves correctly on Windows
    
    Generated with [Claude Code](https://claude.ai/code)
    via [Happy](https://happy.engineering)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    Co-Authored-By: Happy <yesreply@happy.engineering>
    
    * fix(tests): handle BOM in shebang stripping and skip worktree tests on Windows
    
    - validators.test.js: replace regex stripShebang with character-code
      approach that handles UTF-8 BOM before shebang line
    - detect-project-worktree.test.js: skip entire file on Windows since
      tests invoke bash scripts directly
    
    Generated with [Claude Code](https://claude.ai/code)
    via [Happy](https://happy.engineering)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    Co-Authored-By: Happy <yesreply@happy.engineering>
    
    ---------
    
    Co-authored-by: Claude <noreply@anthropic.com>
    Co-authored-by: Happy <yesreply@happy.engineering>
  • fix: resolve Windows CI failures and markdown lint (#667)
    - Replace node -e with temp file execution in validator tests to avoid
      Windows shebang parsing failures (node -e cannot handle scripts that
      originally contained #!/usr/bin/env node shebangs)
    - Remove duplicate blank line in skills/rust-patterns/SKILL.md (MD012)
  • fix(ci): enforce catalog count integrity (#525)
    * fix(ci): enforce catalog count integrity
    
    * test: harden catalog structure parsing