From 3e479bc908c154d6fc5c4ec80da50a641703580a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 19:26:50 +0000 Subject: [PATCH 1/6] chore: defense - cover .vscode in CODEOWNERS Refresh DEFENSE_IN_DEPTH.md to the current catalog and record the digest-pinned Dev Container image in SECURITY.md. Co-authored-by: Jared Wray --- .github/CODEOWNERS | 1 + DEFENSE_IN_DEPTH.md | 18 ++++++++++-------- SECURITY.md | 3 ++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8339057..3741824 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,7 @@ # High-risk paths. Last matching pattern wins. # Root-anchored so nested copies (e.g. skills/**/scripts/) are not owned here. /.github/ @jaredwray +/.vscode/ @jaredwray /.cursor/ @jaredwray /.devcontainer/ @jaredwray /scripts/ @jaredwray diff --git a/DEFENSE_IN_DEPTH.md b/DEFENSE_IN_DEPTH.md index 1b3abf2..d7f076b 100644 --- a/DEFENSE_IN_DEPTH.md +++ b/DEFENSE_IN_DEPTH.md @@ -11,8 +11,9 @@ Profile: npm library · public ## 2. CODEOWNERS and cloud bootstrap -- [x] `.github/CODEOWNERS` covers `/.github/`, `/.cursor/`, `/.devcontainer/`, `/scripts/` with owners the maintainer names — PR #184 +- [ ] `.github/CODEOWNERS` covers `/.github/`, `/.vscode/`, `/.cursor/`, `/.devcontainer/`, `/scripts/` with owners the maintainer names (PR pending) - [x] Codespaces and Cursor Cloud Agents bootstrap Aikido Safe Chain via scripts/setup-cloud-environment.sh (--ci shims, frozen lockfile) — PR #185 +- [x] Dev Container `image` pinned by digest (`name:@sha256:`; not a floating tag) — PR #208 ## 3. Dependencies (pnpm) @@ -31,6 +32,7 @@ Profile: npm library · public - [x] Every action pinned to a full commit SHA (`npx actions-up`) — PR #188 - [x] Every job installs Socket Firewall (`SocketDev/action` SHA-pinned, `firewall-version` pinned); `pnpm install` / `npm install` run as `sfw pnpm install` / `sfw npm install` — PR #189 - [x] `.github/workflows/check-workflows.yaml` lints workflows with zizmor on every PR — PR #190 +- [ ] Workflow `name:` and job `name:` contain no spaces (kebab-case) so they can be set as required status checks - [x] `persist-credentials: false` on checkouts that don't push — PR #191 - [x] No `pull_request_target` on workflows that run untrusted PR code — verified - [x] Artifact-publishing workflows disable `actions/setup-node` default caching (`package-manager-cache: false`) to prevent cache poisoning — PR #192 @@ -38,11 +40,11 @@ Profile: npm library · public ## 5. npm publishing — npm libraries only -- [x] OIDC trusted publishing configured **stage-only** on npmjs.com for the publish workflow — it can stage, never publish live — verified (maintainer) +- [x] OIDC trusted publishing configured **stage-only** on npmjs.com for the publish workflow — it can stage, never publish live (manual) — verified (maintainer) - [x] `.github/workflows/release.yaml` packs then stages with `pnpm stage publish ./packed/*.tgz --no-git-checks` — PR #193 -- [x] Maintainer promotes staged versions with 2FA — verified (maintainer) -- [x] Drydock connected — staged releases reviewed before promotion — verified (maintainer) -- [x] No direct publish rights: package requires 2FA and disallows tokens — verified (maintainer) +- [x] Maintainer promotes staged versions with 2FA (manual) — verified (maintainer) +- [x] Drydock connected — staged releases reviewed before promotion (manual) — verified (maintainer) +- [x] No direct publish rights: package requires 2FA and disallows tokens (manual) — verified (maintainer) - [x] `package.json` `repository.url` accurate so provenance maps to this repo — verified ## 6. Security tooling @@ -53,6 +55,6 @@ Profile: npm library · public ## 7. Repository lockdown -- [x] `lockdown-repo.sh` applied; `--check` with `--required-checks "test,zizmor"` and `--allowed-actions "codecov/*,cloudflare/*"` passes (PRs required on the default branch, merges blocked unless required status checks pass, tag ruleset, immutable releases, fork-PR approval, read-only workflow tokens, Actions allowlist, secret scanning, Dependabot disabled, private vulnerability reporting as applicable) — PR #195 -- [x] Phishing-resistant 2FA (passkeys / hardware keys) on the GitHub and npm accounts — verified (maintainer) -- [x] Recovery codes stored offline in a password manager — verified (maintainer) +- [x] Phishing-resistant 2FA (passkeys / hardware keys) on the GitHub and npm accounts (manual) — verified (maintainer) +- [x] Recovery codes stored offline in a password manager (manual) — verified (maintainer) +- [x] `lockdown-repo.sh` applied by a repo admin (never committed to this repo) — PR #195. Latest `--check` (`--required-checks "test,zizmor"`, `--allowed-actions "codecov/*,cloudflare/*"`) still fails the branch ruleset: it has no owner `pull_request` bypass. The tag ruleset has no repository-admin bypass. Remaining settings were unreadable here (non-admin token, HTTP 403). Admin re-apply is still required. diff --git a/SECURITY.md b/SECURITY.md index 7bcfd5f..6f35f77 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,5 +27,6 @@ This repository follows the [defense-in-depth](https://github.com/jaredwray/agen - Workflow runs from outside collaborators always require maintainer approval, and only allowlisted GitHub Actions can run. - CI runs with read-only permissions (only jobs whose purpose is mutating the repo get `contents: write`); generated output is an artifact, never committed back; every action is pinned to a full commit SHA; Socket Firewall (`sfw`) wraps `pnpm install` / `npm install`; workflows are security-linted with zizmor on every PR. - Codespaces and Cursor Cloud Agents install through Aikido Safe Chain; package-manager shims must not be bypassed. +- The Codespaces Dev Container image is pinned by digest (`name:@sha256:`), not a floating tag. - Dependencies install through pnpm with a 7-day cooldown on new versions, lifecycle scripts blocked by default, and `trustPolicy: no-downgrade`. Socket reviews every dependency change; Aikido scans every build. -- npm releases are staged, never published directly: CI publishes via stage-only OIDC trusted publishing, Drydock reviews the exact staged artifact, and a maintainer promotes it with 2FA. There are no npm tokens. +- npm releases are staged, never published directly: CI publishes via stage-only OIDC trusted publishing, Drydock reviews the exact staged artifact, and a maintainer promotes it with 2FA. There are no npm publish tokens. From d294fc19aae330aa17f19d5a2802be3d0a55786a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 19:27:01 +0000 Subject: [PATCH 2/6] chore: defense - use kebab-case workflow and job names GitHub rulesets cannot require a status check whose name contains a space. Co-authored-by: Jared Wray --- .github/workflows/check-workflows.yaml | 2 +- .github/workflows/codeql.yaml | 2 +- .github/workflows/deploy-site.yaml | 2 +- .github/workflows/release.yaml | 2 +- DEFENSE_IN_DEPTH.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check-workflows.yaml b/.github/workflows/check-workflows.yaml index f2d5150..989d8b3 100644 --- a/.github/workflows/check-workflows.yaml +++ b/.github/workflows/check-workflows.yaml @@ -1,4 +1,4 @@ -name: Check Workflows +name: check-workflows on: push: diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 03b22c6..f72ac08 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -23,7 +23,7 @@ permissions: jobs: analyze: - name: Analyze + name: analyze runs-on: ubuntu-latest permissions: actions: read diff --git a/.github/workflows/deploy-site.yaml b/.github/workflows/deploy-site.yaml index 708934c..d51b41d 100644 --- a/.github/workflows/deploy-site.yaml +++ b/.github/workflows/deploy-site.yaml @@ -10,7 +10,7 @@ permissions: jobs: setup-build-deploy: - name: Deploy Website + name: deploy-website runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 009cdf3..23f9745 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,7 +10,7 @@ permissions: jobs: aikido-gate: - name: Aikido release gate + name: aikido-gate runs-on: ubuntu-latest permissions: contents: read diff --git a/DEFENSE_IN_DEPTH.md b/DEFENSE_IN_DEPTH.md index d7f076b..d864fa5 100644 --- a/DEFENSE_IN_DEPTH.md +++ b/DEFENSE_IN_DEPTH.md @@ -32,7 +32,7 @@ Profile: npm library · public - [x] Every action pinned to a full commit SHA (`npx actions-up`) — PR #188 - [x] Every job installs Socket Firewall (`SocketDev/action` SHA-pinned, `firewall-version` pinned); `pnpm install` / `npm install` run as `sfw pnpm install` / `sfw npm install` — PR #189 - [x] `.github/workflows/check-workflows.yaml` lints workflows with zizmor on every PR — PR #190 -- [ ] Workflow `name:` and job `name:` contain no spaces (kebab-case) so they can be set as required status checks +- [ ] Workflow `name:` and job `name:` contain no spaces (kebab-case) so they can be set as required status checks (PR pending) - [x] `persist-credentials: false` on checkouts that don't push — PR #191 - [x] No `pull_request_target` on workflows that run untrusted PR code — verified - [x] Artifact-publishing workflows disable `actions/setup-node` default caching (`package-manager-cache: false`) to prevent cache poisoning — PR #192 From 0583cef7dcb241daaf601d9b1ae82e51acf64d82 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 19:27:46 +0000 Subject: [PATCH 3/6] chore: defense - lint fork PRs with zizmor annotations Fork pull requests cannot upload SARIF. Same-repo runs still use Advanced Security. Co-authored-by: Jared Wray --- .github/workflows/check-workflows.yaml | 9 ++++++++- scripts/setup-cloud-environment.sh | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) mode change 100644 => 100755 scripts/setup-cloud-environment.sh diff --git a/.github/workflows/check-workflows.yaml b/.github/workflows/check-workflows.yaml index 989d8b3..c811618 100644 --- a/.github/workflows/check-workflows.yaml +++ b/.github/workflows/check-workflows.yaml @@ -13,7 +13,10 @@ jobs: permissions: contents: read actions: read - security-events: write # SARIF upload to code scanning + # SARIF upload. GitHub grants read-only on fork PRs regardless; permission + # values cannot be expressions, so the write grant stays and the zizmor + # step below skips Advanced Security on forks. + security-events: write steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -26,3 +29,7 @@ jobs: firewall-version: "1.15.2" - name: Run zizmor uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 + with: + # Fork PRs cannot upload SARIF (read-only token). Lint with annotations instead. + advanced-security: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + annotations: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} diff --git a/scripts/setup-cloud-environment.sh b/scripts/setup-cloud-environment.sh old mode 100644 new mode 100755 index 0c052e6..23b1ee4 --- a/scripts/setup-cloud-environment.sh +++ b/scripts/setup-cloud-environment.sh @@ -21,8 +21,13 @@ if [[ ! -f pnpm-lock.yaml ]]; then exit 1 fi -if [[ -f package.json ]] && grep -q '"packageManager"' package.json && command -v corepack >/dev/null; then - corepack enable +if ! command -v pnpm >/dev/null \ + && [[ -f package.json ]] \ + && grep -q '"packageManager"' package.json \ + && command -v corepack >/dev/null; then + mkdir -p "$SAFE_CHAIN_BIN" + corepack enable --install-directory "$SAFE_CHAIN_BIN" pnpm + export PATH="${SAFE_CHAIN_BIN}:${PATH}" fi if ! command -v pnpm >/dev/null; then @@ -36,7 +41,12 @@ trap 'rm -f "$installer"' EXIT curl -fsSL "$SAFE_CHAIN_INSTALLER_URL" -o "$installer" echo "${SAFE_CHAIN_INSTALLER_SHA256} ${installer}" | sha256sum -c - -sh "$installer" --ci +# NVM auto-selects from the current directory when sourced. Run outside the +# repository so .nvmrc cannot break the installer's optional legacy scan. +( + cd / + sh "$installer" --ci +) export PATH="${SAFE_CHAIN_SHIMS}:${SAFE_CHAIN_BIN}:${PATH}" From c17afbc5ef686d647f8a46af39a428f8632143f1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 19:27:46 +0000 Subject: [PATCH 4/6] chore: defense - refresh Safe Chain bootstrap and Dev Container features Run the pinned installer outside the repo so .nvmrc cannot break it, and add the GitHub CLI and Docker-in-Docker features. Co-authored-by: Jared Wray --- .devcontainer/devcontainer.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2adf1bb..a73f375 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,11 @@ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:5.2.1-24-trixie@sha256:7a81958053c4e3b5b20fa122f7a422d32dd0eebe87a6726f7b14483585ea60fd", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/docker-in-docker:4": { + "moby": false + } + }, "postCreateCommand": "bash ./scripts/setup-cloud-environment.sh" } From 9b3e1e0d5984d4edb8ed31e1aceae64f752c7057 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 19:27:46 +0000 Subject: [PATCH 5/6] chore: defense - drop the unused unrs-resolver build exception The package is not in the lockfile, so the lifecycle-script allowlist no longer names it. Co-authored-by: Jared Wray --- DEFENSE_IN_DEPTH.md | 2 +- pnpm-workspace.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/DEFENSE_IN_DEPTH.md b/DEFENSE_IN_DEPTH.md index d864fa5..bd40413 100644 --- a/DEFENSE_IN_DEPTH.md +++ b/DEFENSE_IN_DEPTH.md @@ -20,7 +20,7 @@ Profile: npm library · public - [x] `packageManager: pnpm@11.3+` pinned in `package.json` — verified `pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c` - [x] 7-day cooldown: `minimumReleaseAge: 10080`, `minimumReleaseAgeStrict: true`, `minimumReleaseAgeIgnoreMissingTime: false`; no first-party `minimumReleaseAgeExclude` — PR #186 - [x] `trustPolicy: no-downgrade`; no first-party `trustPolicyExclude` — PR #187 -- [x] Lifecycle scripts blocked: `strictDepBuilds: true`, `dangerouslyAllowAllBuilds: false`, `allowBuilds: {}` baseline — verified (third-party `allowBuilds` exceptions: esbuild, sharp, unrs-resolver, workerd) +- [x] Lifecycle scripts blocked: `strictDepBuilds: true`, `dangerouslyAllowAllBuilds: false`, `allowBuilds: {}` baseline — verified (third-party `allowBuilds` exceptions: esbuild, sharp, workerd) - [x] `blockExoticSubdeps: true` — verified - [x] Lockfile committed; CI installs with `pnpm install --frozen-lockfile` — verified - [x] No `.github/dependabot.yml`; other dependency-update tools (if any) open PRs only — never auto-merge — verified diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8b04372..03d0f46 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,5 +9,4 @@ trustPolicy: no-downgrade allowBuilds: esbuild: true sharp: true - unrs-resolver: true workerd: true From 591ff83121b85f99c22e79c3422538d37c61d431 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 19:30:11 +0000 Subject: [PATCH 6/6] chore: defense - record PR 212 on the open checklist items Co-authored-by: Jared Wray --- DEFENSE_IN_DEPTH.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DEFENSE_IN_DEPTH.md b/DEFENSE_IN_DEPTH.md index bd40413..d8c6402 100644 --- a/DEFENSE_IN_DEPTH.md +++ b/DEFENSE_IN_DEPTH.md @@ -11,7 +11,7 @@ Profile: npm library · public ## 2. CODEOWNERS and cloud bootstrap -- [ ] `.github/CODEOWNERS` covers `/.github/`, `/.vscode/`, `/.cursor/`, `/.devcontainer/`, `/scripts/` with owners the maintainer names (PR pending) +- [ ] `.github/CODEOWNERS` covers `/.github/`, `/.vscode/`, `/.cursor/`, `/.devcontainer/`, `/scripts/` with owners the maintainer names (PR #212 pending) - [x] Codespaces and Cursor Cloud Agents bootstrap Aikido Safe Chain via scripts/setup-cloud-environment.sh (--ci shims, frozen lockfile) — PR #185 - [x] Dev Container `image` pinned by digest (`name:@sha256:`; not a floating tag) — PR #208 @@ -32,7 +32,7 @@ Profile: npm library · public - [x] Every action pinned to a full commit SHA (`npx actions-up`) — PR #188 - [x] Every job installs Socket Firewall (`SocketDev/action` SHA-pinned, `firewall-version` pinned); `pnpm install` / `npm install` run as `sfw pnpm install` / `sfw npm install` — PR #189 - [x] `.github/workflows/check-workflows.yaml` lints workflows with zizmor on every PR — PR #190 -- [ ] Workflow `name:` and job `name:` contain no spaces (kebab-case) so they can be set as required status checks (PR pending) +- [ ] Workflow `name:` and job `name:` contain no spaces (kebab-case) so they can be set as required status checks (PR #212 pending) - [x] `persist-credentials: false` on checkouts that don't push — PR #191 - [x] No `pull_request_target` on workflows that run untrusted PR code — verified - [x] Artifact-publishing workflows disable `actions/setup-node` default caching (`package-manager-cache: false`) to prevent cache poisoning — PR #192