diff --git a/.claude/settings.json b/.claude/settings.json index 8a4b46076..6a7cb3f6a 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -80,7 +80,7 @@ "// statusLine": "Fail-soft on purpose. scripts/fleet is FETCHED from the release bundle, never tracked, so the script is absent until a fetch has run. Measured without the -f test: the statusline exited 1 on module-not-found, once per render. The test makes an absent script render nothing instead.", "statusLine": { "type": "command", - "command": "[ -f \"$CLAUDE_PROJECT_DIR\"/scripts/fleet/spend-statusline.mts ] && node \"$CLAUDE_PROJECT_DIR\"/scripts/fleet/spend-statusline.mts || true" + "command": "bin=\"$CLAUDE_PROJECT_DIR/.claude/hooks/fleet/_dist/statusline/statusline-render-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')\" && [ -x \"$bin\" ] && exec \"$bin\" || node \"$CLAUDE_PROJECT_DIR\"/scripts/fleet/spend-statusline.mts" }, "// ": "Repository-owned Claude settings belong below this marker." } diff --git a/.config/fleet/oxlintrc.json b/.config/fleet/oxlintrc.json index 541fb5fe3..e88b7acd3 100644 --- a/.config/fleet/oxlintrc.json +++ b/.config/fleet/oxlintrc.json @@ -7,6 +7,43 @@ "suspicious": "error" }, "rules": { + "eslint/curly": "error", + "eslint/no-await-in-loop": "off", + "eslint/no-console": "off", + "eslint/no-control-regex": "off", + "eslint/no-empty": [ + "error", + { + "allowEmptyCatch": true + } + ], + "eslint/no-new": "error", + "eslint/no-proto": "error", + "eslint/no-shadow": "error", + "eslint/no-underscore-dangle": "off", + "eslint/no-unmodified-loop-condition": "off", + "eslint/no-unused-vars": [ + "error", + { + "args": "all", + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^$", + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "ignoreRestSiblings": false + } + ], + "eslint/no-useless-catch": "off", + "eslint/no-var": "error", + "eslint/prefer-const": "error", + "eslint/preserve-caught-error": "off", + "eslint/sort-imports": "off", + "import/no-cycle": "off", + "import/no-named-as-default": "off", + "import/no-named-as-default-member": "off", + "import/no-self-import": "error", + "import/no-unassigned-import": "off", "socket/bag-param-optionality-naming": "error", "socket/export-top-level-functions": "error", "socket/exported-name-has-domain-word": "error", @@ -26,10 +63,13 @@ "socket/no-deprecation": "error", "socket/no-dynamic-import-in-snapshot-hook": "error", "socket/no-dynamic-import-outside-bundle": "error", + "socket/no-error-message-assertions": "error", "socket/no-eslint-biome-config-ref": "error", "socket/no-fetch-prefer-http-request": "error", "socket/no-file-scope-oxlint-disable": "error", "socket/no-fileoverview-prefer-file": "error", + "socket/no-floating-async-map": "error", + "socket/no-force-delete": "error", "socket/no-handbuilt-file-url": "error", "socket/no-inline-defer-async": "error", "socket/no-inline-logger": "error", @@ -57,6 +97,7 @@ "socket/no-required-in-options-bag": ["warn"], "socket/no-runtime-features-below-engine-floor": "error", "socket/no-shop-metaphor-jargon": "error", + "socket/no-snapshot-hostile-builtin": "error", "socket/no-source-content-tests": "error", "socket/no-source-sniffing": "error", "socket/no-spawn-stream-double-consume": "error", @@ -67,8 +108,10 @@ "socket/no-sync-rm-in-test-lifecycle": "error", "socket/no-top-level-await": "error", "socket/no-truncated-lint-disable-reason": "error", + "socket/no-truthy-object-guard": "error", "socket/no-underscore-identifier": "error", "socket/no-use-strict-in-esm": "error", + "socket/no-vendored-alias-in-test": "error", "socket/no-vitest-empty-test": "error", "socket/no-vitest-focused-tests": "error", "socket/no-vitest-identical-title": "error", @@ -78,10 +121,10 @@ { "additionalTestBlockFunctions": ["cmdit"] } ], "socket/no-which-for-local-bin": "error", - "socket/normalize-path-before-match": "error", "socket/optional-explicit-undefined": "error", "socket/options-null-proto": "error", "socket/options-param-naming": "error", + "socket/paths-are-normalized-before-match-at-edit": "error", "socket/personal-path-placeholders": "error", "socket/prefer-all-settled": "error", "socket/prefer-async-spawn": "error", @@ -108,6 +151,7 @@ "socket/prefer-repo-root-dot-cache": "error", "socket/prefer-safe-delete": "error", "socket/prefer-separate-type-import": "error", + "socket/prefer-setup-phrasing": "error", "socket/prefer-shell-win32": "error", "socket/prefer-spawn-over-execsync": "error", "socket/prefer-stable-self-import": "error", @@ -118,6 +162,7 @@ "socket/require-async-iife-entry": "error", "socket/require-regex-comment": "error", "socket/require-vitest-globals-import": "error", + "socket/require-void-run-hook": "error", "socket/socket-api-token-env": "error", "socket/sort-array-literals": "error", "socket/sort-boolean-chains": "error", @@ -129,50 +174,12 @@ "socket/sort-source-methods": "error", "socket/terse-lint-disable-reason": "error", "socket/use-fleet-canonical-api-token-getter": "error", - "eslint/curly": "error", - "eslint/no-await-in-loop": "off", - "eslint/no-console": "off", - "eslint/no-control-regex": "off", - "eslint/no-empty": [ - "error", - { - "allowEmptyCatch": true - } - ], - "eslint/no-new": "error", - "eslint/no-underscore-dangle": "off", - "eslint/no-unmodified-loop-condition": "off", - "eslint/no-useless-catch": "off", - "eslint/no-proto": "error", - "eslint/no-shadow": "error", - "eslint/no-unused-vars": [ - "error", - { - "args": "all", - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^$", - "caughtErrors": "all", - "caughtErrorsIgnorePattern": "^_", - "destructuredArrayIgnorePattern": "^_", - "ignoreRestSiblings": false - } - ], - "eslint/no-var": "error", - "eslint/prefer-const": "error", - "eslint/preserve-caught-error": "off", - "eslint/sort-imports": "off", - "import/no-cycle": "off", - "import/no-named-as-default": "off", - "import/no-named-as-default-member": "off", - "import/no-self-import": "error", - "import/no-unassigned-import": "off", "typescript/array-type": [ "error", { "default": "array-simple" } ], - "typescript/no-extraneous-class": "off", "typescript/consistent-type-assertions": [ "error", { @@ -189,6 +196,7 @@ ], "typescript/no-explicit-any": "error", "typescript/no-extra-non-null-assertion": "error", + "typescript/no-extraneous-class": "off", "typescript/no-misused-new": "error", "typescript/no-non-null-asserted-optional-chain": "off", "typescript/no-redundant-type-constituents": "off", @@ -205,10 +213,10 @@ "typescript/triple-slash-reference": "error", "unicorn/consistent-function-scoping": "off", "unicorn/no-array-for-each": "off", - "unicorn/no-array-sort": "error", - "unicorn/no-null": "off", "unicorn/no-array-reverse": "error", + "unicorn/no-array-sort": "error", "unicorn/no-empty-file": "off", + "unicorn/no-null": "off", "unicorn/no-useless-fallback-in-spread": "off", "unicorn/numeric-separators-style": "error", "unicorn/prefer-node-protocol": "error", @@ -300,11 +308,13 @@ "**/.claude/agents/fleet/**", "**/.claude/commands/fleet/**", "**/.claude/hooks/fleet/**", + "**/.claude/rules/fleet/**", "**/.claude/skills/fleet/**", "**/.config/fleet/**", "**/.config/fleet/oxlint-plugin/**", "**/.config/repo/rolldown/**", "**/.git-hooks/**", + "**/.github/actions/fleet/**", "**/.pnpm-store/**", "**/bootstrap/**", "**/docs/agents.md/fleet/**", diff --git a/.config/fleet/tsconfig.check.json b/.config/fleet/tsconfig.check.json index ce7a4d66b..a3382bad6 100644 --- a/.config/fleet/tsconfig.check.json +++ b/.config/fleet/tsconfig.check.json @@ -1,5 +1,6 @@ { - "extends": "./tsconfig.check.base.json", + "//": "The repo half comes last so a member's own compilerOptions win. Path aliases live there: without it the type gate resolved none of a member's src/external/* mappings, because the root tsconfig is a sibling of this chain rather than an ancestor.", + "extends": ["./tsconfig.check.base.json", "../repo/tsconfig.check.json"], "compilerOptions": { "rootDir": "../.." }, @@ -10,6 +11,7 @@ "../../.config/fleet/markdownlint-rules/**/*.mts", "../../.config/fleet/oxlint-plugin/**/*.mts", "../../.config/fleet/rolldown/**/*.mts", + "../../src/**/*.d.ts", "../../test/**/*.mts" ], "exclude": [ diff --git a/.config/repo/external-tools.json b/.config/repo/external-tools.json index b15fb2809..ac90cd23b 100644 --- a/.config/repo/external-tools.json +++ b/.config/repo/external-tools.json @@ -9,7 +9,8 @@ "Required: yes (all platforms)", "Preinstalled on macOS (Xcode CLT) and most Linux distros", "Windows: https://git-scm.com/download/win or via winget/scoop" - ] + ], + "origin": "system" }, "node": { "description": "Node.js — runs the build scripts and TypeScript stripping.", @@ -20,7 +21,8 @@ "Install via volta / nvm / fnm / official installer", "The pinned version lives in .node-version at the repo root", "Docker prebakes: node-base (docker/fleet/node-base.Dockerfile) builds node from source at the .node-version pin with V8 pointer compression (--experimental-enable-pointer-compression = lower memory), replicating the vendored platformatic/node-caged recipe rather than pulling its DockerHub image. scripts/repo/build-prebakes.mts injects NODE_VERSION (.node-version) + PNPM_VERSION (pnpm pin) as build-args; every compiler-base build COPYs node --from=node-base instead of installing it." - ] + ], + "origin": "system" }, "pnpm": { "notes": [ @@ -31,47 +33,42 @@ ], "description": "Fast, disk space efficient package manager", "repository": "github:pnpm/pnpm", - "version": "11.20.0", - "soakBypass": { - "published": "2026-08-03", - "removable": "2026-08-10", - "version": "11.20.0" - }, - "release": "asset", + "version": "11.21.0", "platforms": { "darwin-arm64": { "asset": "pnpm-darwin-arm64.tar.gz", - "integrity": "sha512-3ox43Vw8fYSoRZhR92ish7Jt7plCdTFUJc/JbI1gdechbfl6tTg4/NVapzEG8ruKAViwMq0xXte3OzfB3wCxZg==" + "integrity": "sha512-l2jFPzHUG5Q23s94DgKV76erfg0vVhwG4CdKZCHkRSfct8LTWu1oLtH8WJqb84Eg5ZIy5BTMfyY+APoeLuTm0Q==" }, "darwin-x64": { - "asset": "pnpm-11.20.0.tgz", - "integrity": "sha512-mm8zCpW2ZEbqCI+vFSFAWooB8H/ecSTMmVjf7VLUu0NnN+ZbCPhfN7Rvy6N1CSVYrFEmK4FoRLIvY0Bu0Wa/7g==" + "asset": "pnpm-11.21.0.tgz", + "integrity": "sha512-UhcFvOaJkk6scvWjWHEi82JonvZXHlW6gAdv1jfBETLs/62ib61Op5xIW/3b/T1aKlsFgFp36JPeceyKbMo7sQ==" }, "linux-arm64": { "asset": "pnpm-linux-arm64.tar.gz", - "integrity": "sha512-+XqPn1raDqeOE9mfKX6CjPOYBiqQpaUhWX7CJv6I2PRjlXdGiWHqKhEaHELmBRx3iUHSctFV3eIGumBwc/OVNw==" + "integrity": "sha512-thFUlWeQIphtc8AkfTm0FPhMSyMVYOdrA2XqpJit+OEeMStRhqvF6i4yPs5k06UiVLvIFkOQszCVoyjZmezzsg==" }, "linux-arm64-musl": { "asset": "pnpm-linux-arm64-musl.tar.gz", - "integrity": "sha512-uy38eCPmeGbn8BOkkq9ZaT5s0tVollsAPmwjj0sItluq1hXyFipdPAhV8I2V6hXiAVRpMfLgYFtoMQssx1622w==" + "integrity": "sha512-bbFyhWWvphDpll91+ddNT/fk588DAJwpLqFkUTfZ4FxYFFUkgZ2YCifOFPTEZWIV+Zsa8EPojyA2hQoXnbMxXg==" }, "linux-x64": { "asset": "pnpm-linux-x64.tar.gz", - "integrity": "sha512-cSwNCOth2fSGu6boYrhJIidKMj7maAO+MbJoQ+H1qxhSMZOwkE5LGnhzJhFoRjXe3uJTNaVwEWYya0E1YvKG7A==" + "integrity": "sha512-B8OLLdn1CEgvoo2oH1RYIJ1tiiSsUNQcbeTlGytCbPsrwvLo+lVM7as3/yphexA8D/WSiiWLZXAa89t0G1lNgg==" }, "linux-x64-musl": { "asset": "pnpm-linux-x64-musl.tar.gz", - "integrity": "sha512-Ua5x4k4xR9Fq2wco0z0bigSJZ0+xzn9r/vSkBZ9Z4o0h6bl4y/2B7BVYdGZeqD5ulTFKsgzpWz+0ihXpNdUIEg==" + "integrity": "sha512-cBovlI9TaTK2RjCae2HO9u41Z1novo55EPrBJbxKGnL1Gy8nHGdwUI+BlKMSvN6eLDSn55UX7vGJCBXfJHEZLw==" }, - "win-arm64": { + "win32-arm64": { "asset": "pnpm-win32-arm64.zip", - "integrity": "sha512-eb8gM+gNPSr31ZS5kooT9OLjA+THmUGUQVtjCp7O9MYX2tKpnXFcsiJVy0UveqoJVMqhlJZV1wsspUV0obJ1ig==" + "integrity": "sha512-a7x1RBGuoqNXh3K9iA4HLbuN9QNJRnxazJ5Njf748u26Lm9j6D26Y2esi5ewhAOkkb8GTM8lcVDRwU2Ui/+H8Q==" }, - "win-x64": { + "win32-x64": { "asset": "pnpm-win32-x64.zip", - "integrity": "sha512-Lpc1lzGdjMwaDKESMkhKVsJSNr3/76l5zJSKfKYP3LhofTEu4kKJxXsQwbI7OHpDPmyVveNr1PUwiZ8PkzznOA==" + "integrity": "sha512-FFyPTtPoRjRCSEpSVhcbe1AW4Bq/e6Sm3TXewVsvr/sNjRBszNooNEwPehQq7rD3ecUb0oqjul0iBtwmb16njw==" } - } + }, + "origin": "gh-asset" }, "gh": { "description": "GitHub CLI — workflow dispatch, release downloads, PR creation in weekly-update; host for the gh-aw agentic-workflows extension.", @@ -81,7 +78,8 @@ "Preinstalled on GitHub-hosted runners", "Local: `brew install gh` / `winget install gh` / `apt install gh`", "The gh-aw extension needs gh >= 2.0.0; the pinned version above is the current latest" - ] + ], + "origin": "system" }, "gh-aw": { "description": "GitHub Agentic Workflows — `gh` extension that authors agentic workflows as `.github/workflows/.md` (markdown + frontmatter) and compiles them to a hardened `.lock.yml`. The fleet uses `engine: claude`.", @@ -93,7 +91,8 @@ "Engine secrets (set per repo, by engine): `ANTHROPIC_API_KEY` (claude), `COPILOT_GITHUB_TOKEN` (copilot — distinct from the default GITHUB_TOKEN; or use `permissions: copilot-requests: write` to bill the org via the Actions token, no PAT), `OPENAI_API_KEY` (codex), `GEMINI_API_KEY` (gemini)", "`copilot-requests: write` is Copilot-engine-only — claude/codex/gemini need no GitHub permission beyond their API key", "Any action a compiled .lock.yml references must be added to the repo's GitHub Actions allowlist (Settings → Actions → Allowed actions)" - ] + ], + "origin": "system" }, "uv": { "notes": [ @@ -102,37 +101,36 @@ ], "description": "uv — Astral Python package/project manager (pinned, SRI-verified)", "repository": "github:astral-sh/uv", - "version": "0.11.28", - "release": "asset", + "version": "0.12.4", "binaryName": "uv", "platforms": { "darwin-arm64": { "asset": "uv-aarch64-apple-darwin.tar.gz", - "integrity": "sha512-yxcruknz+sl/ZlsUW9N96qoXRsz3CHuCF2+PBDcLzbPupWa6OTyWZMWp72EM1mBsX5FY6FBUkCcGB7bzrasKeA==" + "integrity": "sha512-U8GmjOTBs+okP450e+s4LxYNKAQXljHSif08UFYM1Tfuv/xlS7Lj2Jhuz5+cLkn57Y+VLceSviBWDJlAsxoGNA==" }, "darwin-x64": { "asset": "uv-x86_64-apple-darwin.tar.gz", - "integrity": "sha512-tTEbWp5tCmJlP4ulZ2G493SLB5HF8jfo2YOPiUlmJtuE//oq6/Nb4mt8lSh0VItqMErUThJFJWpZ3ahfx+BtQg==" + "integrity": "sha512-FXqXKWEIZQMWiiWph+e96ru3uEUkgnqZl7Er4nU7pdb68Zt7zkJiRmhLFKv/0PbTDVbhUHEaFg40KSRhpLgJpQ==" }, "linux-arm64": { "asset": "uv-aarch64-unknown-linux-gnu.tar.gz", - "integrity": "sha512-RmGCO1sekfkggd152Dg6FEgR7Cj6U9LNZylLCE3kgkRyLajBSxuBtNbLl1a8/RtS3mHokS8E0PFsW3S+3IlLJw==" + "integrity": "sha512-raae5OoAl39xjllIcxFdBKIzS01rw7VvXtLslUmxR4wvAq09fP9ga6oGB2z1qho/9fqd2yLRFmp/HimYvmdbuw==" }, "linux-x64": { "asset": "uv-x86_64-unknown-linux-gnu.tar.gz", - "integrity": "sha512-NxPIaxb/4tDf9nuG/S5wOlDNmjNev5H8sfKHG+WjK9o1WjsVOEsQt9PhdO8Hp+RngSE+Ho39/eReurr0NyUq0Q==" + "integrity": "sha512-Mk4w84uq8werB8L5/U5lvrk3f38zBkvg39AAOkqMr6wv0JGoH7JHDiSHiiWDM1t5DpJryFu3ZaFO1bjl3qB/Vg==" }, - "win-x64": { + "win32-x64": { "asset": "uv-x86_64-pc-windows-msvc.zip", - "integrity": "sha512-SbgI6DfsIDU0nMripR1fny6EzT5j52nbGx/MM8Vns2Twwo33Nqqr+OEjV781vAqC5fsE6VmxWy0uUpvTEGjfJA==" + "integrity": "sha512-QZQe1n1StxqzfdTLpc1B8b1Eva8m6VdDMcnJZagkMEyN2fz8c6gYKuP/7bMP/bJWYMTw4ggiiAhiIZ//D6RGdQ==" } - } + }, + "origin": "gh-asset" }, "zizmor": { "description": "GitHub Actions security linter — audits .github/ for workflow-injection / credential-leak patterns.", "version": "1.29.0", "repository": "github:zizmorcore/zizmor", - "release": "asset", "notes": [ "Required: CI (blocks merges on medium+ findings)", "Installed by the setup-and-install composite; SRI-verified (sha512) per platform" @@ -154,7 +152,7 @@ "asset": "zizmor-x86_64-unknown-linux-gnu.tar.gz", "integrity": "sha512-+ud35oavU/AEGqKoNZVg8b/dwuFuKM/TfujGmXomnV3mScdhmavrkiNpx/9vUbQ+htk7eYfK2nRStzIVWA19KA==" }, - "win-x64": { + "win32-x64": { "asset": "zizmor-x86_64-pc-windows-msvc.zip", "integrity": "sha512-r6YP+e2GsOklaxEf9JHrixkeR0NNkVAaMGcvwLxkt1JlYJpxFizH/LzhcsMmPSrLkELf6LNB2JL8hpDa5dBAww==" } @@ -163,7 +161,8 @@ "published": "2026-08-01", "removable": "2026-08-08", "version": "1.29.0" - } + }, + "origin": "gh-asset" }, "sfw-free": { "notes": [ @@ -171,40 +170,40 @@ "Installed when neither SOCKET_API_KEY nor SOCKET_API_TOKEN is set; the enterprise flavor (sfw-enterprise) is selected when one of those is present. The two flavors share a version and install to the same `sfw` binary name." ], "description": "Socket Firewall (free tier) — package manager command wrapper", - "version": "1.13.1", + "version": "1.15.0", "repository": "github:SocketDev/sfw-free", "binaryName": "sfw", - "release": "asset", "platforms": { "darwin-arm64": { "asset": "sfw-free-macos-arm64", - "integrity": "sha512-T6wBOJGdRVSI8577lGqRNzNd6Q+1vqKyaqGgOA8G4M5MU2vcsUnXuJTgP2MMZjUqROSXUlFL0mHguuxXT2QadQ==" + "integrity": "sha512-gLeor4y8eeJeQBJ/pLhcsWXG+ntx0sCM6s/MrKtW1uANKjhOwiQBNR5Hw8d1gRhjRuW4R2cPENdjk121sOFQag==" }, "darwin-x64": { "asset": "sfw-free-macos-x86_64", - "integrity": "sha512-4G/AIY5UGU81wcepDKErY5u0nY85D8UM9nXTEPv8CR2rOV/s4IcmrkxywwZ3ipejHVQB7QmCVt0/SsqWglGikw==" + "integrity": "sha512-ZW7jS8+Tg0+ZRWzYbSIZ82LiehytQHNbmYQ3X8FeLJQmg8AY8denDCAIPtJ8+haKeX/7a3AIkpaC8jgBHwGv6Q==" }, "linux-arm64": { "asset": "sfw-free-linux-arm64", - "integrity": "sha512-FYRYR52SL+KKFldW4ogYOUnTH5OSqvtXwzGFeWi0W2x+75KZcPiGzWBbhMmh0f5QtgYLV+4qdREgmKCBEayNtA==" + "integrity": "sha512-BmdfsTfIJDigVv3ImnmZ+/Ktj54kYn8rhNIDJhFkp0X9VeNeJaFteLYrPYox2SQ3hq++qvLfbnPJnuCDRCEdlA==" }, "linux-arm64-musl": { "asset": "sfw-free-musl-linux-arm64", - "integrity": "sha512-5a5VXzMmda9baCHqcNYnFm/Y71BB589IzXlrfXapEJfxMdqs2Dwdubn84TPMgGDRaErxsuYzP1Fe/cNHPZRMnA==" + "integrity": "sha512-yEUwhNyRAGCgSQpRk5Avxk8ZWrYixzKXn7MjC5Fd93DfPTA2QGl0ijsIK334XaiWSuA5FLcmn4gZhVd4YSosAQ==" }, "linux-x64": { "asset": "sfw-free-linux-x86_64", - "integrity": "sha512-waLrsPG2a7EOv0XuvXDQZGgCZ4MTtOfZh8TmGbM6gn2B6Nh6HI+15jaoKdAS9wgdTyIqTuqU+O+NtVYd+kuFaA==" + "integrity": "sha512-PcvpMzR28A0BpUHvHB3chzakWAyGmFSsCuu5zhenAxs0RTF8AlQHJLWnOxBWkK54U+kv50HhbqnjiYMZ9KnUDw==" }, "linux-x64-musl": { "asset": "sfw-free-musl-linux-x86_64", - "integrity": "sha512-BYmolBjZVlXPmj7ilM8CP99EJTcOIha0SFAN6b9z1oc6tQvRsTYjoYKwB18X7kKwcx1jomdOYrEuW9+0t0LrkA==" + "integrity": "sha512-UzJeP11VcqrKcauaLPQR9v1PGb8U19Kli7+Oskbetf+cO5oIrNFd+/zQsUG4hJ6GtLvz0H5FTDNiAQZkg0MhrA==" }, - "win-x64": { + "win32-x64": { "asset": "sfw-free-windows-x86_64.exe", - "integrity": "sha512-YYnfwR6M/PHo72LSyKtpY3bAUG4F4ckToJqGx5Fkz4rwg1+48hkxuBaF3hdxHUdHPkfO5grDyoNgXGe7FojGcg==" + "integrity": "sha512-nWSLEwbh2NNHVMHSCc8tc95Je+XGm988EF0pXCz0X2QRoXw3ytydqfU5TFHue4xHinh5F9fGbiNywtLOMnKmgg==" } - } + }, + "origin": "gh-asset" }, "sfw-enterprise": { "notes": [ @@ -212,40 +211,40 @@ "Installed when SOCKET_API_KEY (or SOCKET_API_TOKEN) is set; otherwise the free flavor (sfw-free) is used. The two flavors share a version and install to the same `sfw` binary name." ], "description": "Socket Firewall (enterprise tier) — package manager command wrapper", - "version": "1.13.1", + "version": "1.15.0", "repository": "github:SocketDev/firewall-release", "binaryName": "sfw", - "release": "asset", "platforms": { "darwin-arm64": { "asset": "sfw-macos-arm64", - "integrity": "sha512-ZDy2C6leKyTHZFvcZZpG2eQqVzs7buk+Hs92fkaMYME829QzyxdGQVVgwEVaGJpedGdUvhksKKcvT9IynI1kxg==" + "integrity": "sha512-JmNdVTAULB0f+oRaNBFd3h0KdU1zP7e/ERGPppQdIHn2/CHROknyKndPzzkbGdKN/AIsSrSfW1c2v6v+eVc2Sw==" }, "darwin-x64": { "asset": "sfw-macos-x86_64", - "integrity": "sha512-cm76we0sn7kqPOya/ZGQpPyhjRDyFT5lHigeT5Qso+QaPL6Cmwi0FVs2L7l63j+WR/9eYPU1WjjGOto5NbWsEQ==" + "integrity": "sha512-3+3IJI3EwV9WcwNwisXmzQn0x5ThLGJDR6b+B/tAn1dlqJcRRdYIqp4ZG/cy5xD04ewLqMAOncGc0qGswx7NRQ==" }, "linux-arm64": { "asset": "sfw-linux-arm64", - "integrity": "sha512-9qPi3mobBfyq1k+pD2GDG0tZkhy16f7FXE9oGiwmwPvy5PXwnlzqEXnYld3qGsKIVwNhunev/If26oROTHbrHA==" + "integrity": "sha512-owLh6ODQTwObU+mV8XToR/qoYvIlPtUJ2Vqzcwb7+bXER544pgt7nN5o+YllWZOSyTmzjNfLALrhONEb0FGt5A==" }, "linux-arm64-musl": { "asset": "sfw-musl-linux-arm64", - "integrity": "sha512-/xdisbXTp44v7GFBUkgtxyxfzS28gjnU7MdGPqrb0q6i8EvQYoqnuG8DCN88ybJouU3RYLwagO3o/5EgS/4cWw==" + "integrity": "sha512-kcwkTTc2/YzYJv354GKWxIc6mhAGWZkuJKFpp2OrERw8YNM1eNzQLAhJgaGZoVGtXHnSe+h896GRnsUjbEinmA==" }, "linux-x64": { "asset": "sfw-linux-x86_64", - "integrity": "sha512-lu9h8UzDZt34gdCEVHBGW6goE1Ayykq413EovV5B4nG7jBK27mI0GQstzVbWXA3wWaweT39PehXGtVpdqIDGSA==" + "integrity": "sha512-pPxU6AJDTHX4NDfX31rvmUiFNOob/s+kv3XvUaH7QQvpRfyPJEuuxzkPyOTgqgyf0RSePOedG7kRPijH4T/QLQ==" }, "linux-x64-musl": { "asset": "sfw-musl-linux-x86_64", - "integrity": "sha512-R1f7/2OoX9WWeW+mGroHVjO1TtUDF5cxokUIi6qPs4hpNvvI+NFK7dgxFOc16dP+qhY0fz8ZNxz5CBgSiZKteA==" + "integrity": "sha512-N/b4w/zKYPYSy42D+8HgwieHs3JfGdFQvXHZhGJ2L4hi2QMJy5TVt3dHWQ7XYyxqvZQAOEQiwIR0pu9G1AwnKg==" }, - "win-x64": { + "win32-x64": { "asset": "sfw-windows-x86_64.exe", - "integrity": "sha512-URZXauIsdUT12E2KTc4sfsxRmJm7nRJzAgM+IYGX4Xq+X0cl/eAbH5SpYIJKpsnW9csSztW9ceyPhlM+f3neIQ==" + "integrity": "sha512-ag9R+AyD4PisdQFSEER06R3IyzSSwqZABFkhONd48ycB0oVTvX5imP//3aZ2j6DaQxCPj0ooSXo4lI8P5XS53g==" } - } + }, + "origin": "gh-asset" } } } diff --git a/.config/repo/socket-wheelhouse.json b/.config/repo/socket-wheelhouse.json index e4c4738dc..4685a4ece 100644 --- a/.config/repo/socket-wheelhouse.json +++ b/.config/repo/socket-wheelhouse.json @@ -6,8 +6,8 @@ "schemaVersion": 1, "repoName": "socket-cli", "bundle": { - "ref": "fleet-pack-0ffc6e5686ead5e7866511a9d701b397ca54ce6b", - "cascadeSha": "0ffc6e5686ead5e7866511a9d701b397ca54ce6b" + "ref": "fleet-pack-d5cde64324ccf3fc85bb41176ec24033b84c43c3", + "cascadeSha": "d5cde64324ccf3fc85bb41176ec24033b84c43c3" }, "repo": { "type": "mono" diff --git a/.config/repo/tsconfig.check.json b/.config/repo/tsconfig.check.json new file mode 100644 index 000000000..81309bad1 --- /dev/null +++ b/.config/repo/tsconfig.check.json @@ -0,0 +1,4 @@ +{ + "//": "Member-owned half of the type-check config. .config/fleet/tsconfig.check.json extends this LAST, so anything declared here wins over the fleet defaults. Its main job is compilerOptions.paths: a member's package aliases resolve for `pnpm run type` only when they are declared here, because the root tsconfig.json is a sibling of the check chain rather than an ancestor. Paths are relative to THIS file, so a src/ wrapper reads ../../src/external/. Seeded empty and extended unconditionally, so leave the file in place even with nothing in it.", + "compilerOptions": {} +} diff --git a/.config/repo/vitest.config.mts b/.config/repo/vitest.config.mts index 4076a6e4e..0abc785f9 100644 --- a/.config/repo/vitest.config.mts +++ b/.config/repo/vitest.config.mts @@ -132,11 +132,45 @@ export function resolveTestBudgetMs( return Math.round(base * resolveBudgetLoadFactor(options)) } -export function resolveFallbackMaxWorkers(): number { - if (getCI()) { +// Local ceiling on the core-scaled worker count. Past this the suite stops +// getting faster and only costs memory: measured on a 14-core box, the +// integration tier ran 147s at 16 workers, 129s at 24 and 126s at 32 — a 2% +// gain for a third more Node heaps. +const LOCAL_MAX_WORKERS_CAP = 24 +const LOCAL_MAX_WORKERS_FLOOR = 8 +// Workers per core. Deliberately oversubscribed: a fleet test mostly WAITS on a +// spawned script rather than computing, so a blocked worker leaves a core idle +// and a sibling can use it. Under-subscribing measurably hurt — the same tier +// took 170s at 6 workers and 169s at 10, slower than the 16-worker default it +// was meant to improve on. +const LOCAL_WORKERS_PER_CORE = 2 + +/** + * Worker cap for the pool before any per-repo override, which + * {@link capMaxWorkers} floors this against. + */ +export function resolveFallbackMaxWorkers( + options?: + | { cores?: number | undefined; isCI?: boolean | undefined } + | undefined, +): number { + const opts = { __proto__: null, ...options } as { + cores?: number | undefined + isCI?: boolean | undefined + } + if (opts.isCI ?? getCI()) { return 4 } - return isCoverageEnabled ? 8 : 16 + // Coverage instrumentation is memory-hungry, so it keeps the flat, lower cap + // rather than scaling up into a heap-exhaustion failure. + if (isCoverageEnabled) { + return 8 + } + const cores = Math.max(1, opts.cores ?? os.availableParallelism()) + return Math.min( + LOCAL_MAX_WORKERS_CAP, + Math.max(LOCAL_MAX_WORKERS_FLOOR, cores * LOCAL_WORKERS_PER_CORE), + ) } export function resolveConfiguredMaxWorkers(): number | undefined { const configured = readVitestSettings().maxWorkers @@ -192,7 +226,7 @@ export function mergeVitestAlias( root: string = process.cwd(), ): Record { const entries = (tier: unknown): Array<[string, string]> => - tier && typeof tier === 'object' && !Array.isArray(tier) + tier !== null && typeof tier === 'object' && !Array.isArray(tier) ? Object.entries(tier).filter( (e): e is [string, string] => typeof e[1] === 'string', ) diff --git a/.config/repo/vitest.settings.mts b/.config/repo/vitest.settings.mts index ef9b8f669..5f180f38d 100644 --- a/.config/repo/vitest.settings.mts +++ b/.config/repo/vitest.settings.mts @@ -69,8 +69,8 @@ export const SETTINGS_FILES = [ * * `scripts/repo/test-conformance.mts` runs this tier explicitly with * FLEET_TEST_CONFORMANCE=1; every other run must EXCLUDE it. Both halves live - * here because both were previously unwired: the runner set the env var and - * nothing read it, and the setting named the tier while no lane excluded it — + * here so neither can sit unwired: an env var the runner sets and nothing + * reads, or a setting that names the tier while no lane excludes it, would — * so `pnpm run cover` spawned a ~92k-scenario corpus per BUILT implementation, * three at once. Against the 60s unit budget that reads as a hung run rather * than the multi-hour sweep it actually is. @@ -85,7 +85,7 @@ export function readNonIsolatedGlobs(): string[] { export function readVitestLanes(): VitestLanes { const lanes = readVitestSettings().lanes - return lanes && typeof lanes === 'object' && !Array.isArray(lanes) + return lanes !== null && typeof lanes === 'object' && !Array.isArray(lanes) ? { mid: stringArray(lanes.mid), slow: stringArray(lanes.slow) } : {} } @@ -104,10 +104,12 @@ export function readVitestSettings(): VitestRepoConfig { try { const parsed: unknown = JSON.parse(readFileSync(file, 'utf8')) const section = - parsed && typeof parsed === 'object' && !Array.isArray(parsed) + parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as { vitest?: VitestRepoConfig | undefined }).vitest : undefined - return section && typeof section === 'object' && !Array.isArray(section) + return section !== null && + typeof section === 'object' && + !Array.isArray(section) ? section : {} } catch { diff --git a/.git-hooks/_shared/canonical-fork-scan.mts b/.git-hooks/_shared/canonical-fork-scan.mts new file mode 100644 index 000000000..3e13fe8ef --- /dev/null +++ b/.git-hooks/_shared/canonical-fork-scan.mts @@ -0,0 +1,160 @@ +/* + * @file Commit-time backstop for the fleet-fork rule. A fleet-canonical path + * (per .gitattributes `linguist-generated=true`) lives only in `template/` + * and is cascaded out via sync-scaffolding, which commits with + * `--no-verify` — a legitimate cascade commit never reaches this hook. + * Anything staged on a canonical path here was written outside the + * cascade: an Edit/Write/Bash tool call, a background Workflow `agent()` + * subagent (whose Bash reaches PreToolUse with the PARENT transcript, so + * the `no-fleet-fork-guard` PreToolUse hook cannot attribute or block it — + * see docs/agents.md/fleet/agent-delegation.md), or a hand-run git command. + * A git hook fires for every commit regardless of which process or agent + * ran `git commit`, so this closes the gap the tool-call guard cannot + * reach. + * + * Reuses the exact decision inputs `no-fleet-fork-guard` already uses + * (fleetCanonicalEntries / isPerRepoMarkerPath / isOperatorLocalPath / + * textHasFleetBlockMarkers) from + * .claude/hooks/fleet/_shared/{fleet-fork,fleet-markers}.mts, so the two + * enforcement points can never disagree about what counts as canonical. + */ + +import { readdirSync, readFileSync } from 'node:fs' +import path from 'node:path' + +import { + fleetCanonicalEntries, + isOperatorLocalPath, + isPerRepoMarkerPath, +} from '../../.claude/hooks/fleet/_shared/fleet-fork.mts' +import { textHasFleetBlockMarkers } from '../../.claude/hooks/fleet/_shared/fleet-markers.mts' + +// The template trees a live path can be cascaded from. `base` maps directly; +// `conditional` and `overrides` interpose one directory level - a capability +// name, or a member name - so each of their children is a candidate. +const TEMPLATE_ROOTS: readonly string[] = ['base', 'conditional', 'overrides'] + +/** + * Candidate template sources for a live repo-relative path. + */ +export function templateTwinPaths(repoRoot: string, file: string): string[] { + const candidates = [path.join(repoRoot, 'template', 'base', file)] + for (let i = 1, { length } = TEMPLATE_ROOTS; i < length; i += 1) { + const root = path.join(repoRoot, 'template', TEMPLATE_ROOTS[i]!) + let names: string[] + try { + names = readdirSync(root) + } catch { + continue + } + for (let j = 0, { length: namesLength } = names; j < namesLength; j += 1) { + candidates.push(path.join(root, names[j]!, file)) + } + } + return candidates +} + +/** + * Whether the live content is byte-identical to one of its template twins. + * + * Then it is cascade OUTPUT, not a fork: a fork is a live copy that DIVERGED + * from canonical. The cascade lands its own mirrors outside this hook chain, + * but when it loses the index lock to a parallel session it leaves them staged, + * and only the operator can land them. Refusing that commit leaves no reachable + * fix - the tool-call guard forbids writing the mirror by hand, and the cascade + * cannot retry while the lock is held - so the operator's only remaining route + * is skipping every hook, which is strictly worse than this exemption. + */ +export function matchesTemplateTwin( + repoRoot: string, + file: string, + content: string, +): boolean { + const candidates = templateTwinPaths(repoRoot, file) + for (let i = 0, { length } = candidates; i < length; i += 1) { + let twin: string + try { + twin = readFileSync(candidates[i]!, 'utf8') + } catch { + continue + } + if (twin === content) { + return true + } + } + return false +} + +export interface CanonicalForkFinding { + file: string +} + +function isInsideTemplateRelative(file: string): boolean { + return file === 'template' || file.startsWith('template/') +} + +/** + * Every staged path (repo-relative, POSIX-normalized, add/change/modify + * only — a caller filters deletions out via `--diff-filter=ACM`) that is + * fleet-canonical and was staged OUTSIDE the cascade. Pure aside from the + * file reads the fleet-block-marker allowance needs. + */ +export function scanCanonicalForkPaths( + stagedFiles: readonly string[], + repoRoot: string, +): CanonicalForkFinding[] { + const entries = fleetCanonicalEntries(repoRoot) + if (entries.length === 0) { + return [] + } + const findings: CanonicalForkFinding[] = [] + for (let i = 0, { length } = stagedFiles; i < length; i += 1) { + const file = stagedFiles[i]! + if (isInsideTemplateRelative(file)) { + continue + } + if (isPerRepoMarkerPath(file) || isOperatorLocalPath(file)) { + continue + } + let isCanonical = false + for ( + let j = 0, { length: entriesLength } = entries; + j < entriesLength; + j += 1 + ) { + const entry = entries[j]! + // Glob entries are best-effort excluded here too — same conservative + // call `isCanonicalRelativePath` makes, so a bad pattern can never + // over-block a commit. + if (entry.includes('*')) { + continue + } + if (file === entry || file.startsWith(`${entry}/`)) { + isCanonical = true + break + } + } + if (!isCanonical) { + continue + } + // Fleet-block allowance: a canonical file carrying `` + // markers is only PART fleet-managed — content outside the markers is + // repo-owned, so staging it is normal repo work, not a fork. + let content = '' + try { + content = readFileSync(path.join(repoRoot, file), 'utf8') + } catch { + // Unreadable (permissions, binary) — fall through as non-exempt; a + // canonical path staged unreadable is still worth surfacing. + } + if (textHasFleetBlockMarkers(content)) { + continue + } + // Byte-identical to canonical is propagation, not divergence. + if (matchesTemplateTwin(repoRoot, file, content)) { + continue + } + findings.push({ file }) + } + return findings +} diff --git a/.git-hooks/_shared/push-durable-ref.mts b/.git-hooks/_shared/push-durable-ref.mts new file mode 100644 index 000000000..5803f903c --- /dev/null +++ b/.git-hooks/_shared/push-durable-ref.mts @@ -0,0 +1,83 @@ +// A durable-backup ref: pushed to get work OFF THE MACHINE, never to be +// consumed. Nothing builds from it, nothing installs it, no CI runs it. +// +// Why the namespace exists: on a shared checkout local main carries every +// session's commits, so one session's lint debt gates another session's push. +// The work then lives on one disk. That is how an afternoon of commits was lost +// when a checkout was deleted - the commits were real, reviewed, and nowhere +// else. +// +// So the QUALITY bar is scoped to the branches people consume. A backup push +// still runs every SAFETY scan: a leaked secret or an unsigned commit is a fact +// about the bytes, and a backup ref is as public as any other. What it skips is +// lint, format, types, and dispatch drift - because a backup that has to be +// green is a backup you cannot take at the moment you need it most. + +/** + * The namespaces reserved for durable backups. A branch here is understood to + * be UNTESTED: rebase or cherry-pick from it, never merge it as-is. + */ +export const DURABLE_REF_PREFIXES: readonly string[] = [ + 'refs/heads/wip/', + 'refs/heads/worktree/', +] + +/** + * Whether `remoteRef` names a durable backup rather than a consumed branch. + * + * Matched on the FULL remote ref, so a local branch merely named `wip/x` cannot + * opt a push to `main` out of the gates. The trailing slash is required, so a + * branch called `wip-something` does not qualify by prefix accident, and a bare + * `refs/heads/wip` with nothing after it does not either. + */ +export function isDurableBackupRef(remoteRef: string): boolean { + for (let i = 0, { length } = DURABLE_REF_PREFIXES; i < length; i += 1) { + const prefix = DURABLE_REF_PREFIXES[i]! + if (remoteRef.startsWith(prefix) && remoteRef.length > prefix.length) { + return true + } + } + return false +} + +/** + * Whether this push carries ONLY durable-backup refs. + * + * Every ref must qualify. A push that updates a backup ref AND a real branch in + * one invocation is a real push, because the real branch is what people consume + * - reading it as a backup would let any ref smuggle a main update past the + * quality gates. + * + * An empty list is NOT durable. No refs means nothing was proven, and + * defaulting to "skip the gates" on an unreadable stdin is the wrong direction + * to fail. + */ +export function isDurableBackupPush(remoteRefs: readonly string[]): boolean { + if (remoteRefs.length === 0) { + return false + } + for (let i = 0, { length } = remoteRefs; i < length; i += 1) { + if (!isDurableBackupRef(remoteRefs[i]!)) { + return false + } + } + return true +} + +/** + * The branch name to back the current work up to, given a session label. + * + * Slashes and whitespace in the label would create nested refs or an invalid + * name, so everything outside the safe set collapses to a dash. The `wip/` + * prefix is not optional: it is what the gate keys on. + */ +export function durableBackupBranch(label: string): string { + const safe = label + .toLowerCase() + // Anything outside the safe set becomes one dash, so a slash cannot nest a + // ref and whitespace cannot invalidate the name. + .replace(/[^a-z0-9._-]+/g, '-') + // `^-+` and `-+$` - the dashes the collapse above leaves at either edge. + .replace(/^-+|-+$/g, '') + return `wip/${safe || 'session'}` +} diff --git a/.gitattributes b/.gitattributes index e0d13ad57..c8d31f1f8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,7 +13,8 @@ .claude/workflows linguist-generated=true .config/fleet/.markdownlint-cli2.jsonc linguist-generated=true .config/fleet/.prettierignore linguist-generated=true -.config/fleet/egress-allowlist.json linguist-generated=true +.config/fleet/external-tools.json linguist-generated=true +.config/fleet/fetch-allowlist.json linguist-generated=true .config/fleet/git-authors.json linguist-generated=true .config/fleet/lockstep.schema.json linguist-generated=true .config/fleet/markdownlint-rules linguist-generated=true @@ -28,7 +29,6 @@ .config/fleet/playwright linguist-generated=true .config/fleet/pnpm-workspace.fleet.yaml linguist-generated=true .config/fleet/rolldown/bundle-stub.mts linguist-generated=true -.config/fleet/rolldown/define-guarded.mts linguist-generated=true .config/fleet/rolldown/hook-bundle-excluded.config.mts linguist-generated=true .config/fleet/rolldown/hook-bundle-snapshot.config.mts linguist-generated=true .config/fleet/rolldown/hook-bundle.config.mts linguist-generated=true @@ -52,6 +52,7 @@ .github/actions/fleet/checkout linguist-generated=true .github/actions/fleet/cleanup-git-signing linguist-generated=true .github/actions/fleet/debug linguist-generated=true +.github/actions/fleet/download-artifact linguist-generated=true .github/actions/fleet/expose-actions-runtime linguist-generated=true .github/actions/fleet/github-payload-app-token linguist-generated=true .github/actions/fleet/github-pr-app-token linguist-generated=true @@ -64,11 +65,13 @@ .github/actions/fleet/setup linguist-generated=true .github/actions/fleet/setup-and-install linguist-generated=true .github/actions/fleet/setup-git-signing linguist-generated=true +.github/actions/fleet/setup-go-toolchain linguist-generated=true .github/actions/fleet/setup-odai linguist-generated=true .github/actions/fleet/setup-rust-cache linguist-generated=true .github/actions/fleet/setup-rust-toolchain linguist-generated=true -.github/agent-ci.Dockerfile linguist-generated=true +.github/actions/fleet/upload-artifact linguist-generated=true .github/dependabot.yml linguist-generated=true +.github/local-ci.Dockerfile linguist-generated=true .github/workflows/*.lock.yml linguist-generated=true merge=ours .github/workflows/fuzz.yml linguist-generated=true .github/workflows/get-green.yml linguist-generated=true @@ -94,7 +97,6 @@ docs/references/fleet/sfw-local-install.md linguist-generated=true patches/@socketsecurity__lib@6.7.0.patch linguist-generated=true patches/brace-expansion@5.0.9.patch linguist-generated=true patches/minimatch@10.2.6.patch linguist-generated=true -patches/taze@20.0.0.patch linguist-generated=true scripts/fleet linguist-generated=true scripts/repo/bootstrap linguist-generated=true test/fleet/_shared/lib linguist-generated=true diff --git a/.github/actions/fleet/_shared/install-tool.d.mts b/.github/actions/fleet/_shared/install-tool.d.mts new file mode 100644 index 000000000..5e36f2f65 --- /dev/null +++ b/.github/actions/fleet/_shared/install-tool.d.mts @@ -0,0 +1,13 @@ +/** + * @file Type declarations for install-tool.mjs — the dep-0 bootstrap helper + * that downloads + SRI-verifies + extracts a release asset. The .mjs is + * intentionally untyped (it runs before node_modules); this .d.mts mirrors + * the EXPORTED helpers so unit tests can import them with type-checking + * (same pattern as read-package-integrity.d.mts). Keep in step with the .mjs + * exports. + */ + +export function parseIntegrity(s: string): { + algo: 'sha256' | 'sha384' | 'sha512' + expected: string +} diff --git a/.github/actions/fleet/_shared/install-tool.mjs b/.github/actions/fleet/_shared/install-tool.mjs index 56352c938..786af9dac 100644 --- a/.github/actions/fleet/_shared/install-tool.mjs +++ b/.github/actions/fleet/_shared/install-tool.mjs @@ -6,13 +6,15 @@ * `node:crypto.createHash` computes the digest in-process, and tar/unzip * shell out, already preinstalled on every supported runner image. Usage: * node install-tool.mjs [] - * is a Subresource Integrity string: `-`. Examples: - * `sha256-67PM...=`, `sha512-l/kG...==`. The algorithm is parsed from the - * prefix; multiple algos are supported (sha256, sha384, sha512). Same - * encoding as npm package-lock.json's `integrity` field and as - * `external-tools.json`'s `integrity` field. Backward compat: a bare 64-char - * hex string is also accepted and treated as `sha256-` for - * transition. Deprecated; new call sites should pass SRI directly. Behavior: + * is a Subresource Integrity string: `-` OR `-` + * (publisher checksum form, e.g. `sha256-<64 hex>` from go.dev / rustup / + * Google's Packages index). Examples: `sha256-67PM...=`, + * `sha256-544932...c0749c`. The algorithm is parsed from the prefix; multiple + * algos are supported (sha256, sha384, sha512). Same encoding as npm + * package-lock.json's `integrity` field and as `external-tools.json`'s + * `integrity` field. Backward compat: a bare 64-char hex string is also + * accepted and treated as `sha256-` for transition. + * Deprecated; new call sites should pass SRI directly. Behavior: * * - Streams the asset to /. * - Aborts and removes the file if integrity mismatches. @@ -21,7 +23,17 @@ * - For non-archive assets, bare binaries like sfw: the asset IS the binary — * chmod +x it and rename to if provided. Exit codes: 0 success 1 * download or extraction failed 2 integrity mismatch (stderr names expected - * vs actual + the path) + * vs actual + the path). Optional `--src ` + `--date ` carry the + * object-form integrity provenance so a LIVE src check (fetch the + * publisher's current checksum, compare to the pin) + a `date` staleness + * check run AFTER the static SRI check and BEFORE extract/execute (exit 2 + * on mismatch / strict staleness). Env: SFW_INTEGRITY_MAX_AGE_DAYS sets the + * threshold (default 90); SFW_INTEGRITY_STRICT=1 fails on stale instead of + * warning. Testability: the pure `parseIntegrity` helper is EXPORTED and + * the side-effectful CLI pipeline is guarded by isMainModule(), so unit + * tests can import this file without triggering a download. Every + * composite-action _shared helper follows this pattern (see + * check-fleet-shared-scripts-are-testable). */ // composite-action helper runs on the raw runner before setup-node; @@ -33,11 +45,15 @@ import crypto from 'node:crypto' import { chmodSync, mkdirSync, + realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs' import path from 'node:path' +import { pathToFileURL } from 'node:url' + +import { verifyIntegrityProvenance } from './verify-integrity-provenance.mjs' // Composite-action helper runs on the raw runner BEFORE setup-node finishes // resolving node_modules — `@socketsecurity/lib-stable` is not on disk yet @@ -53,29 +69,32 @@ const logger = { fail: msg => console.error(msg), } -const [, , url, integrityArg, destDir, binName] = process.argv - -if (!url || !integrityArg || !destDir) { - logger.fail( - 'usage: install-tool.mjs []', - ) - process.exit(1) -} - -// Parse SRI string `-`. Bare 64-char hex is treated as -// sha256 for backward compat — deprecated, will be removed once all -// call sites pass SRI directly. -// composite-action helper runs on the raw runner before setup-node; no -// node_modules, no module boundary worth exporting across. +// Parse SRI string `-` or `-`. Bare 64-char hex is +// treated as sha256 for backward compat — deprecated, will be removed once all +// call sites pass SRI directly. The part after the `-` prefix may be +// base64 (npm/SRI form, e.g. `sha512-…==`) OR hex (publisher checksum form, +// e.g. `sha256-<64 hex chars>` from go.dev / rustup / Google's Packages +// index). A hex-length all-hex remainder is converted to base64 so the +// comparison stays in one shape. Throws on an unrecognized format (NOT +// process.exit) so the parser is unit-testable; the CLI run() wrapper lets it +// propagate to its .catch, which logs + exits 1. // every non-returning arm ends in process.exit(1); the analyzer cannot see the // never. // oxlint-disable-next-line socket/export-top-level-functions, typescript/consistent-return -- action helper -function parseIntegrity(s) { - // Parse an SRI string: (1) the algorithm (sha256/384/512), (2) the base64 - // digest after the dash. +export function parseIntegrity(s) { + // Parse an SRI string: (1) the algorithm (sha256/384/512), (2) the digest + // after the dash — base64 (npm/SRI) or hex (publisher checksum). const m = /^(sha(?:256|384|512))-(.+)$/.exec(s) if (m) { - return { algo: m[1], expected: m[2] } + const algo = m[1] + const rest = m[2] + const hexLen = algo === 'sha256' ? 64 : algo === 'sha384' ? 96 : 128 + if (rest.length === hexLen && /^[0-9a-f]+$/i.test(rest)) { + // `-` form (go.dev / rustup / Google .deb checksums) — + // convert to base64 so the comparison is shape-consistent. + return { algo, expected: Buffer.from(rest, 'hex').toString('base64') } + } + return { algo, expected: rest } } if (/^[0-9a-f]{64}$/i.test(s)) { // Bare sha256 hex — convert to SRI base64 for the comparison. @@ -84,129 +103,191 @@ function parseIntegrity(s) { expected: Buffer.from(s, 'hex').toString('base64'), } } - // pre-setup-node action; @socketsecurity/lib-stable not installed yet. - // oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet - console.error( - `× unrecognized integrity format: ${s}\n Expected SRI (e.g. sha256-base64=)`, + throw new Error( + `unrecognized integrity format: ${s}\n Expected SRI (e.g. sha256-base64=) or sha256-`, ) - process.exit(1) } -const { algo, expected } = parseIntegrity(integrityArg) - -mkdirSync(destDir, { recursive: true }) - -const assetName = path.basename(new URL(url).pathname) -const archivePath = path.join(destDir, assetName) - -const headers = { __proto__: null } -// GitHub release assets in private repos require auth. When -// GITHUB_TOKEN is in env, every Actions run sets it, forward it as -// a bearer header so the same call site works for both public and -// private release-asset URLs. -if (process.env.GITHUB_TOKEN) { - headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}` +// true when this file is the invoked script (not imported). Lets the pure +// helpers above be imported by unit tests without triggering the download / +// verify / extract pipeline. +function isMainModule() { + const entry = process.argv[1] + if (!entry) { + return false + } + try { + // realpath both sides before comparing. Node normalizes `..` in argv[1] + // but leaves symlinks in place, while import.meta.url is fully resolved, so + // a launch path under a symlinked prefix (macOS /tmp and /var/folders, a + // symlinked checkout) compares unequal and the CLI silently does nothing + // while exiting 0. + return pathToFileURL(realpathSync(entry)).href === import.meta.url + } catch { + return false + } } -// Composite-action helper runs as a standalone node script on the raw runner; -// the CJS bundle target rejects top-level await, so the download / verify / -// extract pipeline runs inside an async IIFE. -// composite-action helper runs on the raw runner before setup-node; no -// node_modules, no module boundary worth exporting across. -// every non-returning arm ends in process.exit(1); the analyzer cannot see the -// never. -// oxlint-disable-next-line socket/export-top-level-functions, typescript/consistent-return -- action helper -async function main() { - // pre-setup-node action; @socketsecurity/lib-stable not installed yet, only - // built-in fetch is available. - // oxlint-disable-next-line socket/no-fetch-prefer-http-request -- fetch only - const res = await fetch(url, { redirect: 'follow', headers }) - if (!res.ok) { - // pre-setup-node action; @socketsecurity/lib-stable not installed yet. - // oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet - console.error( - `× download failed: HTTP ${res.status} ${res.statusText} for ${url}`, +// CLI entry point. Guarded by isMainModule() so importing this file (for the +// exported parseIntegrity helper) does NOT run the download/verify/extract +// pipeline. +async function run() { + // Positionals: []. Optional flags + // --src and --date carry the object-form integrity provenance + // (forwarded by the composite actions from resolve-external-tool-asset.mjs's + // JSON output) so the live src / staleness checks run after the SRI check. + const flags = { src: '', date: '' } + const positionals = [] + for (let i = 2; i < process.argv.length; i++) { + const a = process.argv[i] + if (a === '--date' || a === '--src') { + flags[a.slice(2)] = process.argv[++i] ?? '' + } else { + positionals.push(a) + } + } + const [url, integrityArg, destDir, binName] = positionals + + if (!url || !integrityArg || !destDir) { + logger.fail( + 'usage: install-tool.mjs [] [--src ] [--date ]', ) process.exit(1) } - const bytes = new Uint8Array(await res.arrayBuffer()) - const actual = crypto.createHash(algo).update(bytes).digest('base64') - - // Compare base64 forms directly. Trailing `=` padding may differ - // npm strips it, our hash adds it — strip both sides before - // comparing so `sha512-...=` and `sha512-...` match. - const stripPadding = b64 => b64.replace(/=+$/, '') - if (stripPadding(actual) !== stripPadding(expected)) { - // pre-setup-node action; @socketsecurity/lib-stable not installed yet. - // oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet - console.error(`× ${algo} integrity mismatch for ${assetName}`) - // pre-setup-node action; same. - // oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet - console.error(` Expected: ${algo}-${expected}`) - // pre-setup-node action; same. - // oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet - console.error(` Actual: ${algo}-${actual}`) - // pre-setup-node action; same. - // oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet - console.error(` URL: ${url}`) - process.exit(2) - } + const { algo, expected } = parseIntegrity(integrityArg) - writeFileSync(archivePath, bytes) - - const lower = assetName.toLowerCase() - let extractCmd - let extractArgs - if (lower.endsWith('.tar.gz') || lower.endsWith('.tgz')) { - extractCmd = 'tar' - // Run inside the destination and pass a local basename. Git for Windows' - // tar treats an absolute `D:\\...` archive path as `host:path` and tries - // to connect to a host named D; the basename is portable across GNU tar, - // bsdtar, and the tar bundled with Git for Windows. - extractArgs = ['xzf', assetName] - } else if (lower.endsWith('.zip')) { - if (process.platform === 'win32') { - extractCmd = 'powershell' - extractArgs = [ - '-NoProfile', - '-Command', - `Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`, - ] - } else { - extractCmd = 'unzip' - extractArgs = ['-qo', archivePath, '-d', destDir] - } + mkdirSync(destDir, { recursive: true }) + + const assetName = path.basename(new URL(url).pathname) + const archivePath = path.join(destDir, assetName) + + const headers = { __proto__: null } + // GitHub release assets in private repos require auth. When + // GITHUB_TOKEN is in env, every Actions run sets it, forward it as + // a bearer header so the same call site works for both public and + // private release-asset URLs. + if (process.env.GITHUB_TOKEN) { + headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}` } - if (extractCmd) { - const r = spawnSync(extractCmd, extractArgs, { - cwd: destDir, - stdio: 'inherit', - }) - if (r.status !== 0) { - // pre-setup-node action; @socketsecurity/lib-stable not installed yet. - // oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet - console.error(`× extraction failed: ${extractCmd} exited ${r.status}`) + // Composite-action helper runs as a standalone node script on the raw runner; + // the CJS bundle target rejects top-level await, so the download / verify / + // extract pipeline runs inside an async main(). + // every non-returning arm ends in process.exit(1); the analyzer cannot see + // the never. + // oxlint-disable-next-line socket/export-top-level-functions, typescript/consistent-return -- action helper + async function main() { + // pre-setup-node action; @socketsecurity/lib-stable not installed yet, only + // built-in fetch is available. + // oxlint-disable-next-line socket/no-fetch-prefer-http-request -- fetch only + const res = await fetch(url, { redirect: 'follow', headers }) + if (!res.ok) { + // oxlint-disable-next-line socket/no-logger-glyph-prefix -- bootstrap shim; logger.fail does not print a glyph + logger.fail( + `× download failed: HTTP ${res.status} ${res.statusText} for ${url}`, + ) process.exit(1) } - // dep-0: pre-setup-node composite-action helper; @socketsecurity/lib-stable - // is not on disk yet, so safeDelete is unavailable. - // oxlint-disable-next-line socket/prefer-safe-delete -- dep-0 - rmSync(archivePath, { force: true }) - } else if (binName) { - // Bare-binary asset, no archive. Rename to bin-name and chmod. - const finalPath = path.join(destDir, binName) - renameSync(archivePath, finalPath) - chmodSync(finalPath, 0o755) - } else { - chmodSync(archivePath, 0o755) + + const bytes = new Uint8Array(await res.arrayBuffer()) + const actual = crypto.createHash(algo).update(bytes).digest('base64') + + // Compare base64 forms directly. Trailing `=` padding may differ + // npm strips it, our hash adds it — strip both sides before + // comparing so `sha512-...=` and `sha512-...` match. + const stripPadding = b64 => b64.replace(/=+$/, '') + if (stripPadding(actual) !== stripPadding(expected)) { + // oxlint-disable-next-line socket/no-logger-glyph-prefix -- bootstrap shim; logger.fail does not print a glyph + logger.fail(`× ${algo} integrity mismatch for ${assetName}`) + logger.fail(` Expected: ${algo}-${expected}`) + logger.fail(` Actual: ${algo}-${actual}`) + logger.fail(` URL: ${url}`) + process.exit(2) + } + + // ── live provenance + staleness check ──────────────────────────────── + // The static SRI check above verified the DOWNLOADED bytes against + // `value`. When --src/--date are forwarded (object-form integrity), now + // verify `value` is still the publisher's current checksum and that the + // pin is not stale. Runs BEFORE extract/execute so a stale / re-released + // / compromised pin aborts loudly (exit 2) before the asset touches disk + // for extraction. String-form integrity (no flags) is a no-op here. + if (flags.src || flags.date) { + const maxAgeEnv = Number(process.env.SFW_INTEGRITY_MAX_AGE_DAYS) + const provenance = await verifyIntegrityProvenance( + { value: integrityArg, src: flags.src, date: flags.date }, + { + assetFilename: assetName, + maxAgeDays: + Number.isFinite(maxAgeEnv) && maxAgeEnv > 0 ? maxAgeEnv : 90, + strict: process.env.SFW_INTEGRITY_STRICT === '1', + }, + ) + if (!provenance.ok) { + // oxlint-disable-next-line socket/no-logger-glyph-prefix -- bootstrap shim + logger.fail(`× integrity provenance check failed for ${assetName}`) + logger.fail(` ${provenance.reason}`) + process.exit(2) + } + } + + writeFileSync(archivePath, bytes) + + const lower = assetName.toLowerCase() + let extractCmd + let extractArgs + if (lower.endsWith('.tar.gz') || lower.endsWith('.tgz')) { + extractCmd = 'tar' + // Run inside the destination and pass a local basename. Git for Windows' + // tar treats an absolute `D:\\...` archive path as `host:path` and tries + // to connect to a host named D; the basename is portable across GNU tar, + // bsdtar, and the tar bundled with Git for Windows. + extractArgs = ['xzf', assetName] + } else if (lower.endsWith('.zip')) { + if (process.platform === 'win32') { + extractCmd = 'powershell' + extractArgs = [ + '-NoProfile', + '-Command', + `Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`, + ] + } else { + extractCmd = 'unzip' + extractArgs = ['-qo', archivePath, '-d', destDir] + } + } + + if (extractCmd) { + const r = spawnSync(extractCmd, extractArgs, { + cwd: destDir, + stdio: 'inherit', + }) + if (r.status !== 0) { + // oxlint-disable-next-line socket/no-logger-glyph-prefix -- bootstrap shim; logger.fail does not print a glyph + logger.fail(`× extraction failed: ${extractCmd} exited ${r.status}`) + process.exit(1) + } + // dep-0: pre-setup-node composite-action helper; @socketsecurity/lib-stable + // is not on disk yet, so safeDelete is unavailable. + // oxlint-disable-next-line socket/prefer-safe-delete -- dep-0 + rmSync(archivePath, { force: true }) + } else if (binName) { + // Bare-binary asset, no archive. Rename to bin-name and chmod. + const finalPath = path.join(destDir, binName) + renameSync(archivePath, finalPath) + chmodSync(finalPath, 0o755) + } else { + chmodSync(archivePath, 0o755) + } } + + void main().catch(e => { + logger.fail(e) + process.exit(1) + }) } -main().catch(e => { - // pre-setup-node action; @socketsecurity/lib-stable not installed yet. - // oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet - console.error(e) - process.exit(1) -}) +if (isMainModule()) { + void run() +} diff --git a/.github/actions/fleet/_shared/jq.d.mts b/.github/actions/fleet/_shared/jq.d.mts new file mode 100644 index 000000000..de52e3fd7 --- /dev/null +++ b/.github/actions/fleet/_shared/jq.d.mts @@ -0,0 +1,14 @@ +/** + * @file Type declarations for jq.mjs — the dep-0 bootstrap JSON reader for + * composite-action shells. The .mjs is intentionally untyped (it runs before + * node_modules); this .d.mts mirrors the EXPORTED helpers so unit tests can + * import them with type-checking. Keep in step with the .mjs exports. + */ + +export function resolveExtends( + data: unknown, + resolvedPath: string, + visited?: Set | undefined, +): unknown + +export function walkKeys(value: unknown, keys: readonly string[]): unknown diff --git a/.github/actions/fleet/_shared/jq.mjs b/.github/actions/fleet/_shared/jq.mjs index 95977dcaa..754c138ce 100644 --- a/.github/actions/fleet/_shared/jq.mjs +++ b/.github/actions/fleet/_shared/jq.mjs @@ -11,16 +11,24 @@ * the base's wholesale — the same ESLint-style semantics as * build-pipeline.mts's loadExternalToolsChain. Stdin input (`-`) cannot * resolve relative `extends` paths and is walked as-is. + * Testability: the pure `resolveExtends` + `walkKeys` helpers are EXPORTED + * and the side-effectful CLI is guarded by isMainModule(), so unit tests can + * import them without triggering a process.exit. Every composite-action + * _shared helper follows this pattern (see + * check-fleet-shared-scripts-are-testable). */ -import { readFileSync } from 'node:fs' +import { readFileSync, realpathSync } from 'node:fs' import path from 'node:path' import process from 'node:process' +import { pathToFileURL } from 'node:url' // Resolve an `extends` chain, string or array of relative paths, into a flat -// `tools` view. Fails LOUD on a circular chain or an unreadable base file — -// a silently half-resolved view surfaces later as a mysterious missing key. -function resolveExtends(data, resolvedPath, visited) { +// `tools` view. Fails LOUD (throws) on a circular chain or an unreadable base +// file — a silently half-resolved view surfaces later as a mysterious missing +// key. The CLI wrapper turns the throw into a loud process.exit. `visited` is +// the set of resolved paths already seen along the chain. +export function resolveExtends(data, resolvedPath, visited = new Set()) { if (data === null || typeof data !== 'object') { return data } @@ -35,10 +43,9 @@ function resolveExtends(data, resolvedPath, visited) { return data } if (visited.has(resolvedPath)) { - process.stderr.write( - `jq.mjs: circular extends chain — "${resolvedPath}" is referenced more than once along the inheritance path; break the cycle in the extends fields.\n`, + throw new Error( + `jq.mjs: circular extends chain — "${resolvedPath}" is referenced more than once along the inheritance path; break the cycle in the extends fields.`, ) - process.exit(1) } visited.add(resolvedPath) const tools = {} @@ -48,10 +55,9 @@ function resolveExtends(data, resolvedPath, visited) { try { baseRaw = readFileSync(basePath, 'utf8') } catch { - process.stderr.write( - `jq.mjs: extends target unreadable — "${resolvedPath}" extends "${basePath}" but that file cannot be read; fix the extends path or restore the base file.\n`, + throw new Error( + `jq.mjs: extends target unreadable — "${resolvedPath}" extends "${basePath}" but that file cannot be read; fix the extends path or restore the base file.`, ) - process.exit(1) } const base = resolveExtends(JSON.parse(baseRaw), basePath, visited) Object.assign(tools, base?.tools || {}) @@ -60,27 +66,55 @@ function resolveExtends(data, resolvedPath, visited) { return { ...data, tools } } -const [, , file, ...keys] = process.argv - -const raw = file === '-' ? readFileSync(0, 'utf8') : readFileSync(file, 'utf8') +// Walk a list of keys down a JSON value, returning the resolved value (or +// undefined when a key is absent / the walk hits a non-object). Pure. +export function walkKeys(value, keys) { + let v = value + for (let i = 0, { length } = keys; i < length; i += 1) { + const k = keys[i] + if (v == null || typeof v !== 'object') { + return undefined + } + v = v[k] + } + return v +} -let v = JSON.parse(raw) -if (file !== '-') { - v = resolveExtends(v, path.resolve(file), new Set()) +function isMainModule() { + const entry = process.argv[1] + if (!entry) { + return false + } + try { + // realpath both sides before comparing. Node normalizes `..` in argv[1] + // but leaves symlinks in place, while import.meta.url is fully resolved, so + // a launch path under a symlinked prefix (macOS /tmp and /var/folders, a + // symlinked checkout) compares unequal and the CLI silently does nothing + // while exiting 0. + return pathToFileURL(realpathSync(entry)).href === import.meta.url + } catch { + return false + } } -for (let i = 0, { length } = keys; i < length; i += 1) { - const k = keys[i] - if (v == null || typeof v !== 'object') { + +if (isMainModule()) { + const [, , file, ...keys] = process.argv + + const raw = + file === '-' ? readFileSync(0, 'utf8') : readFileSync(file, 'utf8') + + let v = JSON.parse(raw) + if (file !== '-') { + v = resolveExtends(v, path.resolve(file)) + } + v = walkKeys(v, keys) + + if (v == null || v === '') { process.exit(1) } - v = v[k] -} -if (v == null || v === '') { - process.exit(1) + // composite-action helper runs on the raw runner before setup-node; the + // action's stdout IS the contract, consumed via shell command substitution. + // oxlint-disable-next-line socket/no-console-prefer-logger -- stdout contract + console.log(typeof v === 'string' ? v : JSON.stringify(v)) } - -// composite-action helper runs on the raw runner before setup-node; the -// action's stdout IS the contract, consumed via shell command substitution. -// oxlint-disable-next-line socket/no-console-prefer-logger -- stdout contract -console.log(typeof v === 'string' ? v : JSON.stringify(v)) diff --git a/.github/actions/fleet/_shared/platform.d.mts b/.github/actions/fleet/_shared/platform.d.mts new file mode 100644 index 000000000..744d7b5e9 --- /dev/null +++ b/.github/actions/fleet/_shared/platform.d.mts @@ -0,0 +1,9 @@ +/** + * @file Type declarations for platform.mjs — the dep-0 bootstrap helper that + * prints the canonical Socket platform string for this runner. The .mjs is + * intentionally untyped (it runs before node_modules); this .d.mts mirrors + * the EXPORTED helper so unit tests can import it with type-checking. Keep + * in step with the .mjs exports. + */ + +export function canonicalPlatform(): string diff --git a/.github/actions/fleet/_shared/platform.mjs b/.github/actions/fleet/_shared/platform.mjs index fa41833a9..671422d24 100644 --- a/.github/actions/fleet/_shared/platform.mjs +++ b/.github/actions/fleet/_shared/platform.mjs @@ -7,56 +7,100 @@ * musl Node, otherwise a glibc version number). No shelling out. Usage: node * .github/actions/fleet/_shared/platform.mjs Exits non-zero on unsupported * platform/arch. + * NOTE: this script outputs `win-x64` / `win-arm64` (the legacy fleet + * shell-side shape), NOT `win32-x64` (the external-tools.json `platforms` + * keys). The resolver helper (resolve-external-tool-asset.mjs) computes its + * own `win32-*` key for schema lookup; do NOT consume this script's output as + * a platforms-map key. + * Testability: the pure `canonicalPlatform` helper is EXPORTED and the + * side-effectful stdout print is guarded by isMainModule(), so unit tests can + * import it without triggering a process.exit. Every composite-action _shared + * helper follows this pattern (see check-fleet-shared-scripts-are-testable). */ -import { existsSync, readdirSync } from 'node:fs' +import { existsSync, readdirSync, realpathSync } from 'node:fs' +import process from 'node:process' +import { pathToFileURL } from 'node:url' -const archMap = { __proto__: null, arm64: 'arm64', x64: 'x64' } -const platformMap = { - __proto__: null, - darwin: 'darwin', - linux: 'linux', - win32: 'win', -} +// Composite-action helper runs on the raw runner before setup-node; +// @socketsecurity/lib-stable not installed yet. +// oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet +const fail = msg => console.error(msg) -const arch = archMap[process.arch] -const platform = platformMap[process.platform] +// The canonical Socket platform string for THIS runner (the shell-side shape: +// `win-x64`, not `win32-x64`). Exported so tests + the resolver can share the +// libc-detection logic. Throws on an unsupported platform/arch (the CLI +// wrapper turns the throw into a loud process.exit). +export function canonicalPlatform() { + const archMap = { __proto__: null, arm64: 'arm64', x64: 'x64' } + const platformMap = { + __proto__: null, + darwin: 'darwin', + linux: 'linux', + win32: 'win', + } -if (!arch || !platform) { - // composite-action helper runs on the raw runner before setup-node; - // @socketsecurity/lib-stable not installed yet. - // oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet - console.error(`× unsupported runner: ${process.platform}-${process.arch}`) - process.exit(1) -} + const arch = archMap[process.arch] + const platform = platformMap[process.platform] -let suffix = '' -if (platform === 'linux') { - const libc = process.report?.getReport().header.glibcVersionRuntime - if (libc === 'musl') { - suffix = '-musl' - } else if (!libc) { - // glibcVersionRuntime undefined on Linux is unusual — confirm - // libc by probing for the musl dynamic loader. Both /lib/ld-musl-* - // and /lib64/ld-musl-* are valid musl ABI paths. - const probeDirs = ['/lib', '/lib64'] - const isMusl = probeDirs.some(d => { - if (!existsSync(d)) { - return false - } - try { - return readdirSync(d).some(f => f.startsWith('ld-musl-')) - } catch { - return false - } - }) - if (isMusl) { + if (!arch || !platform) { + throw new Error(`unsupported runner: ${process.platform}-${process.arch}`) + } + + let suffix = '' + if (platform === 'linux') { + const libc = process.report?.getReport?.().header.glibcVersionRuntime + if (libc === 'musl') { suffix = '-musl' + } else if (!libc) { + // glibcVersionRuntime undefined on Linux is unusual — confirm + // libc by probing for the musl dynamic loader. Both /lib/ld-musl-* + // and /lib64/ld-musl-* are valid musl ABI paths. + const probeDirs = ['/lib', '/lib64'] + const isMusl = probeDirs.some(d => { + if (!existsSync(d)) { + return false + } + try { + return readdirSync(d).some(f => f.startsWith('ld-musl-')) + } catch { + return false + } + }) + if (isMusl) { + suffix = '-musl' + } } } + + return `${platform}-${arch}${suffix}` } -// composite-action helper runs on the raw runner before setup-node; the -// action's stdout IS the contract (consumed via `id: detect` output). -// oxlint-disable-next-line socket/no-console-prefer-logger -- stdout contract -console.log(`${platform}-${arch}${suffix}`) +function isMainModule() { + const entry = process.argv[1] + if (!entry) { + return false + } + try { + // realpath both sides before comparing. Node normalizes `..` in argv[1] + // but leaves symlinks in place, while import.meta.url is fully resolved, so + // a launch path under a symlinked prefix (macOS /tmp and /var/folders, a + // symlinked checkout) compares unequal and the CLI silently does nothing + // while exiting 0. + return pathToFileURL(realpathSync(entry)).href === import.meta.url + } catch { + return false + } +} + +if (isMainModule()) { + try { + // composite-action helper runs on the raw runner before setup-node; the + // action's stdout IS the contract (consumed via `id: detect` output). + // oxlint-disable-next-line socket/no-console-prefer-logger -- stdout contract + console.log(canonicalPlatform()) + } catch (e) { + fail(`× ${e?.message ?? e}`) + process.exit(1) + } +} diff --git a/.github/actions/fleet/_shared/resolve-external-tool-asset.d.mts b/.github/actions/fleet/_shared/resolve-external-tool-asset.d.mts new file mode 100644 index 000000000..7bc478eb3 --- /dev/null +++ b/.github/actions/fleet/_shared/resolve-external-tool-asset.d.mts @@ -0,0 +1,56 @@ +/** + * @file Type declarations for resolve-external-tool-asset.mjs — the dep-0 + * bootstrap helper that resolves a pinned external-tool asset URL + SRI + * integrity for the runner's canonical platform. The .mjs is intentionally + * untyped (it runs before node_modules); this .d.mts mirrors the EXPORTED + * helpers so unit tests can import them with type-checking. Keep in step + * with the .mjs exports. + */ + +export interface GoOsArch { + readonly os: string + readonly arch: string +} + +// The .mjs uses a __proto__:null object literal keyed by the canonical 8 +// platform keys; this Record is the type mirror for a closed domain. +// oxlint-disable-next-line socket/prefer-refined-record -- closed domain +export const GO_OS_ARCH: Readonly> + +export function canonicalPlatformKey(): string + +export interface PlatformEntryLike { + readonly asset: string + readonly integrity: unknown +} + +export interface ResolvedPlatformEntry { + readonly entry: PlatformEntryLike | undefined + readonly fallbackKey: string | undefined +} + +export function resolvePlatformEntry( + // oxlint-disable-next-line socket/prefer-refined-record -- closed domain + platforms: Readonly>, + canonicalKey: string, +): ResolvedPlatformEntry + +export function integrityValue(integrity: unknown): string + +export function integrityProvenance( + integrity: unknown, +): { readonly src: string; readonly date: string } + +export function readVersionFromFile(file: string): string + +export interface ResolvedGoAsset { + readonly asset: string + readonly integrity: string + readonly version: string +} + +export function resolveGoAssetFromManifest( + manifest: unknown, + version: string, + canonicalKey: string, +): ResolvedGoAsset diff --git a/.github/actions/fleet/_shared/resolve-external-tool-asset.mjs b/.github/actions/fleet/_shared/resolve-external-tool-asset.mjs new file mode 100644 index 000000000..2d0598927 --- /dev/null +++ b/.github/actions/fleet/_shared/resolve-external-tool-asset.mjs @@ -0,0 +1,367 @@ +/** + * @file Resolve a pinned external-tool asset + SRI integrity for THIS runner, + * from scripts/fleet/setup/external-tools.json. Replaces the curl-with-no- + * checksum download dance repeated across setup-go-toolchain / + * setup-rust-toolchain / setup-odai. Emits one JSON line on stdout: + * {"asset":"","integrity":"","version":""} + * The caller passes `asset` + `integrity` to install-tool.mjs, which + * downloads + SRI-verifies BEFORE extract/execute. Usage: + * node resolve-external-tool-asset.mjs --tool + * [--version ] [--version-file ] [--tools-file ] + * --version "stable" (or omitted) → the entry's pinned `version`. + * --version-file → read a `go ` line (go.mod) and use that version. + * For `go` ONLY, a version that differs from the pin is resolved live + * against the go.dev release manifest (https://go.dev/dl/?mode=json) so a + * custom Go version still gets a SHA-256-verified download; every other tool + * requires the pinned version (the pin IS the integrity source). Exits 1 on + * any resolution failure — set -e turns a missing platform entry into a loud + * error rather than an empty-asset install-tool.mjs invocation. + * Runs on the raw runner before setup-node (composite-action helper), so it + * uses built-ins only (node:fs, node:path, node:process, fetch) — no + * socket-lib, no node_modules. + * Testability: the pure helpers (canonicalPlatformKey, resolvePlatformEntry, + * integrityValue, readVersionFromFile, resolveGoAssetFromManifest) are + * EXPORTED and the side-effectful CLI orchestration is guarded by + * isMainModule(), so unit tests import them without triggering a network + * fetch or a process.exit. Every composite-action _shared helper follows this + * pattern (see check-fleet-shared-scripts-are-testable). + */ + +import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' + +// Composite-action helper runs on the raw runner BEFORE setup-node finishes +// resolving node_modules — @socketsecurity/lib-stable is not on disk yet, so +// the logger.fail path the rest of the fleet uses is unavailable. Fall back to +// a tiny inline fail that mirrors install-tool.mjs's bootstrap logger. +function fail(msg) { + // oxlint-disable-next-line socket/no-console-prefer-logger -- no lib yet + console.error(msg) +} + +// Emit the resolver result as one JSON line on stdout (the caller reads it via +// jq.mjs). Wrapped so the stream is reached inside a function, not at module +// eval (not V8-snapshot-safe). +function emit(obj) { + // oxlint-disable-next-line socket/no-module-eval-side-effects -- bootstrap + process.stdout.write(JSON.stringify(obj)) +} + +// ── pure helpers (exported for unit tests) ──────────────────────────────── + +// Canonical → Go os/arch. Go ships no musl tarball — the glibc archive is +// statically linked and runs on musl too, so musl keys map to the glibc +// os/arch. Exported so resolveGoAssetFromManifest can use it and tests can +// assert the mapping. +export const GO_OS_ARCH = { + __proto__: null, + 'darwin-arm64': { os: 'darwin', arch: 'arm64' }, + 'darwin-x64': { os: 'darwin', arch: 'amd64' }, + 'linux-arm64': { os: 'linux', arch: 'arm64' }, + 'linux-arm64-musl': { os: 'linux', arch: 'arm64' }, + 'linux-x64': { os: 'linux', arch: 'amd64' }, + 'linux-x64-musl': { os: 'linux', arch: 'amd64' }, + 'win32-arm64': { os: 'windows', arch: 'arm64' }, + 'win32-x64': { os: 'windows', arch: 'amd64' }, +} + +// The canonical Socket platform string for THIS runner, matching the +// external-tools.json `platforms` keys (linux-x64, linux-arm64-musl, +// darwin-arm64, win32-x64, …). process.platform is `win32` on Windows (the +// schema keys are win32-*, NOT win-* — so do NOT use platform.mjs's win- +// output here). Detects musl via Node's own process.report so we don't shell +// out to ldd; falls back to probing for the musl loader when the report has +// no glibcVersionRuntime (mirrors platform.mjs). +export function canonicalPlatformKey() { + const archMap = { __proto__: null, arm64: 'arm64', x64: 'x64' } + const arch = archMap[process.arch] + if (!arch) { + throw new Error(`unsupported arch: ${process.arch}`) + } + let platform + if (process.platform === 'darwin') { + platform = 'darwin' + } else if (process.platform === 'linux') { + platform = 'linux' + } else if (process.platform === 'win32') { + platform = 'win32' + } else { + throw new Error(`unsupported platform: ${process.platform}`) + } + let suffix = '' + if (platform === 'linux') { + const libc = process.report?.getReport?.().header.glibcVersionRuntime + if (libc === 'musl') { + suffix = '-musl' + } else if (!libc) { + const isMusl = ['/lib', '/lib64'].some(d => { + if (!existsSync(d)) { + return false + } + try { + return readdirSync(d).some(f => f.startsWith('ld-musl-')) + } catch { + return false + } + }) + if (isMusl) { + suffix = '-musl' + } + } + } + return `${platform}-${arch}${suffix}` +} + +// Resolve a platform entry from a `platforms` map, with a musl → glibc +// fallback for tools that ship no musl asset (e.g. Go — the glibc archive is +// statically linked and runs on musl too). Returns { entry, fallbackKey } — +// entry is the matched PlatformEntry or undefined; fallbackKey is the glibc +// key the lookup fell back to (undefined when the canonical key hit directly +// or no fallback applied). Pure. +export function resolvePlatformEntry(platforms, canonicalKey) { + const entry = platforms[canonicalKey] + if (entry) { + return { entry, fallbackKey: undefined } + } + // musl → glibc sibling fallback (linux-x64-musl → linux-x64). + if (canonicalKey.endsWith('-musl')) { + const glibcKey = canonicalKey.slice(0, -5) + const fallback = platforms[glibcKey] + if (fallback) { + return { entry: fallback, fallbackKey: glibcKey } + } + } + return { entry: undefined, fallbackKey: undefined } +} + +// Normalize an integrity field (string SRI form OR the object provenance form +// { value, src?, date? }) to the SRI string install-tool.mjs verifies. Pure. +export function integrityValue(integrity) { + if (typeof integrity === 'object' && integrity !== null) { + return integrity.value + } + return integrity +} + +// Extract the provenance fields (src, date) from an integrity field. Returns +// { src: '', date: '' } for the string form (no provenance) so install-tool.mjs +// can forward them as --src/--date flags unconditionally. Pure. +export function integrityProvenance(integrity) { + if (typeof integrity === 'object' && integrity !== null) { + return { + src: typeof integrity.src === 'string' ? integrity.src : '', + date: typeof integrity.date === 'string' ? integrity.date : '', + } + } + return { src: '', date: '' } +} + +// Read a `go ` line from a go.mod file (the only --version-file +// consumer today). Returns '' when the file is absent or has no go directive. +// Pure given the file path (reads the filesystem). +export function readVersionFromFile(file) { + if (!file || !existsSync(file)) { + return '' + } + const src = readFileSync(file, 'utf8') + // `go .[.]` from a go.mod — the optional .patch is the + // only alternation, so the regex is self-evident in context. + // oxlint-disable-next-line socket/require-regex-comment -- go.mod directive + const m = /^go\s+(\d+\.\d+(?:\.\d+)?)/m.exec(src) + return m ? m[1] : '' +} + +// Resolve a Go asset URL + SHA-256 SRI for a custom version from the go.dev +// release manifest (https://go.dev/dl/?mode=json). Go publishes checksums for +// EVERY release, so a custom go-version still gets SRI-verified before +// extract. Returns { asset, integrity, version } or throws when the manifest +// has no matching stable release or no archive for the platform. Pure given +// the manifest object (no network). +export function resolveGoAssetFromManifest(manifest, version, canonicalKey) { + const goOsArch = GO_OS_ARCH[canonicalKey] + if (!goOsArch) { + throw new Error(`go: no os/arch mapping for ${canonicalKey}`) + } + const want = `go${version}` + const release = Array.isArray(manifest) + ? manifest.find(r => r.version === want && r.stable) + : undefined + if (!release) { + throw new Error( + `go.dev manifest has no stable release '${want}' (resolved version ${version})`, + ) + } + const file = Array.isArray(release.files) + ? release.files.find( + f => + f.os === goOsArch.os && + f.arch === goOsArch.arch && + f.kind === 'archive', + ) + : undefined + if (!file || !file.sha256 || !file.filename) { + throw new Error( + `go.dev release ${want} has no archive for ${goOsArch.os}-${goOsArch.arch}`, + ) + } + return { + asset: `https://go.dev/dl/${file.filename}`, + integrity: `sha256-${file.sha256}`, + version: String(version), + } +} + +// ── CLI orchestration (guarded) ─────────────────────────────────────────── + +function isMainModule() { + const entry = process.argv[1] + if (!entry) { + return false + } + try { + // realpath both sides before comparing. Node normalizes `..` in argv[1] + // but leaves symlinks in place, while import.meta.url is fully resolved, so + // a launch path under a symlinked prefix (macOS /tmp and /var/folders, a + // symlinked checkout) compares unequal and the CLI silently does nothing + // while exiting 0. + return pathToFileURL(realpathSync(entry)).href === import.meta.url + } catch { + return false + } +} + +function argValue(name) { + const i = process.argv.indexOf(name) + return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : '' +} + +async function main() { + const toolName = argValue('--tool') + const versionArg = argValue('--version') + const versionFile = argValue('--version-file') + const toolsFileArg = argValue('--tools-file') + + if (!toolName) { + fail( + 'usage: resolve-external-tool-asset.mjs --tool [--version ] [--version-file ] [--tools-file ]', + ) + process.exit(1) + } + + const toolsFile = + toolsFileArg || + path.join( + process.env['GITHUB_WORKSPACE'] ?? '.', + 'scripts/fleet/setup/external-tools.json', + ) + + if (!existsSync(toolsFile)) { + fail(`× external-tools.json not found at ${toolsFile}`) + process.exit(1) + } + + let toolsData + try { + toolsData = JSON.parse(readFileSync(toolsFile, 'utf8')) + } catch (e) { + fail(`× could not parse ${toolsFile}: ${e?.message ?? e}`) + process.exit(1) + } + + const tools = toolsData?.tools || {} + const tool = tools[toolName] + if (!tool) { + fail(`× no '${toolName}' entry in ${toolsFile}`) + process.exit(1) + } + if (!tool.platforms) { + fail(`× '${toolName}' has no platforms map in ${toolsFile}`) + process.exit(1) + } + + const canonicalKey = canonicalPlatformKey() + + const { entry, fallbackKey } = resolvePlatformEntry( + tool.platforms, + canonicalKey, + ) + if (fallbackKey) { + fail( + `· ${toolName}: no ${canonicalKey} asset, falling back to ${fallbackKey} (statically linked, runs on musl)`, + ) + } + if (!entry) { + fail( + `× '${toolName}' has no platform asset for ${canonicalKey} in ${toolsFile}`, + ) + process.exit(1) + } + + let resolvedVersion = '' + const fileVersion = readVersionFromFile(versionFile) + if (fileVersion) { + resolvedVersion = fileVersion + } else if (versionArg && versionArg !== 'stable') { + resolvedVersion = versionArg + } + if (!resolvedVersion) { + resolvedVersion = tool.version + } + if (!resolvedVersion) { + fail(`× no version resolved for '${toolName}' (no pin, no input)`) + process.exit(1) + } + + // Pinned-version fast path: emit the entry's asset + integrity. A version + // override on `go` is resolved live against go.dev below; every other tool + // requires the pinned version (the pin IS the integrity source). + const isGo = toolName === 'go' || tool.manager === 'go' + const pinVersion = tool.version || '' + if (!isGo || resolvedVersion === pinVersion) { + const asset = entry.asset + const integrity = integrityValue(entry.integrity) + if (!asset || !integrity) { + fail( + `× '${toolName}' ${canonicalKey} entry is missing asset or integrity in ${toolsFile}`, + ) + process.exit(1) + } + // Forward the object-form provenance (src/date) so install-tool.mjs can + // run the live src + staleness checks after the static SRI check. Empty + // for the string form (no provenance) — install-tool.mjs no-ops them. + const { src, date } = integrityProvenance(entry.integrity) + emit({ asset, integrity, version: resolvedVersion, src, date }) + return + } + + // go custom-version path: resolve the SHA-256 from the go.dev manifest. + let manifest + try { + // pre-setup-node helper: built-in fetch only. + // oxlint-disable-next-line socket/no-fetch-prefer-http-request -- bootstrap + const res = await fetch('https://go.dev/dl/?mode=json&include=all', { + redirect: 'follow', + }) + if (!res.ok) { + fail(`× go.dev manifest fetch failed: HTTP ${res.status}`) + process.exit(1) + } + manifest = await res.json() + } catch (e) { + fail(`× go.dev manifest fetch failed: ${e?.message ?? e}`) + process.exit(1) + } + + try { + emit(resolveGoAssetFromManifest(manifest, resolvedVersion, canonicalKey)) + } catch (e) { + fail(`× ${e?.message ?? e}`) + process.exit(1) + } +} + +if (isMainModule()) { + void main() +} diff --git a/.github/actions/fleet/_shared/verify-integrity-provenance.d.mts b/.github/actions/fleet/_shared/verify-integrity-provenance.d.mts new file mode 100644 index 000000000..f08e5af97 --- /dev/null +++ b/.github/actions/fleet/_shared/verify-integrity-provenance.d.mts @@ -0,0 +1,50 @@ +/** + * @file Type declarations for verify-integrity-provenance.mjs — the dep-0 + * bootstrap helper that live-verifies an integrity pin's `src` provenance + + * `date` staleness after the static SRI check. The .mjs is intentionally + * untyped (it runs before node_modules); this .d.mts mirrors the EXPORTED + * helpers so unit tests can import them with type-checking (same pattern as + * install-tool.d.mts). Keep in step with the .mjs exports. + */ + +export interface ProvenanceOpts { + readonly fetch?: typeof fetch | undefined + readonly now?: Date | (() => Date) | undefined + readonly maxAgeDays?: number | undefined + readonly strict?: boolean | undefined + readonly warn?: ((msg: string) => void) | undefined + readonly assetFilename?: string | undefined +} + +export type ProvenanceStatus = 'pass' | 'warn' | 'fail' + +export interface ProvenanceResult { + readonly ok: boolean + readonly reason: string + readonly status?: ProvenanceStatus | undefined + readonly stale?: boolean | undefined + readonly ageDays?: number | undefined +} + +export type IntegrityWithProvenance = { + readonly value: string + readonly src?: string | undefined + readonly date?: string | undefined +} + +export function parseChecksumFile( + text: string, + options?: { readonly assetFilename?: string | undefined } | undefined, +): string + +export function checksumsMatch(value: string, fetchedHex: string): boolean + +export function checkStaleness( + dateString: string, + options?: { readonly now?: Date | (() => Date) | undefined; readonly maxAgeDays?: number | undefined } | undefined, +): { readonly stale: boolean; readonly ageDays: number } | undefined + +export function verifyIntegrityProvenance( + integrity: string | IntegrityWithProvenance, + options?: ProvenanceOpts | undefined, +): Promise diff --git a/.github/actions/fleet/_shared/verify-integrity-provenance.mjs b/.github/actions/fleet/_shared/verify-integrity-provenance.mjs new file mode 100644 index 000000000..ef7ee67e2 --- /dev/null +++ b/.github/actions/fleet/_shared/verify-integrity-provenance.mjs @@ -0,0 +1,455 @@ +/** + * @file Live provenance + staleness verification for an external-tool asset's + * `integrity` pin. The schema (scripts/fleet/lib/external-tools-schema.mts) + * lets `integrity` be either a string SRI (`sha512-…==` / `sha256-`) or + * an object `{ value, src?, date? }` carrying provenance — the URL the hash + * was fetched from (`src`) and the ISO day it was pinned (`date`). The static + * SRI check in install-tool.mjs verifies the DOWNLOADED bytes against + * `value`; THIS module verifies `value` itself is still the publisher's + * current checksum (so a stale / re-released / tampered pin is caught loudly + * rather than silently installing a byte-for-byte match of a hash that no + * longer matches the source of truth) and that the pin is not too old. Flow + * (wired into install-tool.mjs AFTER the static SRI check, BEFORE + * extract/execute): download → SRI-verify against value → live-verify value + * against src → staleness-check date → extract/execute. dep-0: runs on the + * raw runner before setup-node, so it uses built-ins only (no. + * + * @socketsecurity/lib-stable). The fetch is injected via options so unit + * tests mock it without touching the network. Pure helpers + * (parseChecksumFile, checksumsMatch, checkStaleness) are EXPORTED; the + * orchestrator verifyIntegrityProvenance is EXPORTED too — the side-effectful + * CLI wiring stays in install-tool.mjs. Every composite-action _shared helper + * follows this testability pattern (see + * check-fleet-shared-scripts-are-testable). + */ + +/** + * @typedef {Object} ProvenanceOptions + * + * @property {typeof fetch} [fetch] - Fetch impl (default global fetch); tests + * inject a mock so no network is touched. + * @property {Date | () => Date} [now] - Clock for the staleness check; tests + * inject a fixed date. + * @property {number} [maxAgeDays] - Staleness threshold (default 90; env + * SFW_INTEGRITY_MAX_AGE_DAYS overrides). + * @property {boolean} [strict] - When true, a stale date FAILS instead of + * warning (env SFW_INTEGRITY_STRICT=1). + * @property {(msg: string) => void} [warn] - Warning sink (default + * console.error). + * @property {string} [assetFilename] - Basename of the asset URL; used to pick + * the matching checksum out of a multi-entry checksum file (SHASUMS, go.dev + * JSON manifest, Debian Packages index). + */ + +/** + * @typedef {Object} ProvenanceResult + * + * @property {boolean} ok - False when the pin must NOT proceed (src mismatch + * or strict-mode staleness); true otherwise. + * @property {string} reason - Human-readable outcome for logging. + * @property {boolean} [stale] - True when the date crossed the staleness + * threshold (warned, or failed under strict). + * @property {number} [ageDays] - The pin's age in days when date is present. + * @property {'pass' | 'warn' | 'fail'} [status] - Coarse outcome label. + */ + +// ── pure helpers (exported for unit tests) ─────────────────────────────── + +// The hex length for a given SRI algorithm. sha256 → 64 hex chars (32 bytes), +// sha384 → 96, sha512 → 128. Used to detect the publisher checksum hex form +// (`sha256-<64 hex>`) vs the npm SRI base64 form (`sha512-==`). +function hexLenFor(algo) { + return algo === 'sha256' ? 64 : algo === 'sha384' ? 96 : 128 +} + +// Pull the algorithm + raw digest out of an SRI-ish `value` string +// (`sha256-` / `sha512-`). Returns undefined for a non-SRI +// value. +function parseSriValue(value) { + const m = /^(sha(?:256|384|512))-(.+)$/.exec(value) + return m ? { algo: m[1], rest: m[2] } : undefined +} + +// Walk a parsed JSON value for an object carrying a `sha256` (or `sha512`) +// field, matching `assetFilename` against a sibling `filename` / `name` / +// `path` field when present. The go.dev manifest is an array of releases, +// each with a `files` array of `{ filename, sha256, … }`; rustup-style JSON +// sidecars are a single object. When no assetFilename is given, the first +// hex checksum found is returned. Returns the bare hex string or ''. +function findShaInJson(data, assetFilename) { + if (typeof data !== 'object' || data === null) { + return '' + } + if (Array.isArray(data)) { + for (let i = 0, { length } = data; i < length; i += 1) { + const hex = findShaInJson(data[i], assetFilename) + if (hex) { + return hex + } + } + return '' + } + // A leaf checksum object: has a sha256 (or sha512) field. + const hex = + typeof data.sha256 === 'string' && /^[0-9a-f]+$/i.test(data.sha256) + ? data.sha256 + : typeof data.sha512 === 'string' && /^[0-9a-f]+$/i.test(data.sha512) + ? data.sha512 + : '' + if (hex) { + if (!assetFilename) { + return hex + } + const name = + typeof data.filename === 'string' + ? data.filename + : typeof data.name === 'string' + ? data.name + : typeof data.path === 'string' + ? data.path + : '' + if (!name || basename(name) === assetFilename) { + return hex + } + return '' + } + // Recurse into nested objects / arrays (release.files, etc.). + const keys = Object.keys(data) + for (let i = 0, { length } = keys; i < length; i += 1) { + const v = data[keys[i]] + if (typeof v === 'object' && v !== null) { + const found = findShaInJson(v, assetFilename) + if (found) { + return found + } + } + } + return '' +} + +// Parse a Debian Packages index (blank-line-separated stanzas with `Filename:` +// and `SHA256:` fields) for the checksum whose Filename basename matches. +// Returns the bare hex string or ''. +function findShaInPackagesIndex(text, assetFilename) { + const stanzas = text.split(/\r?\n\s*\r?\n/) + for (let i = 0, { length } = stanzas; i < length; i += 1) { + const stanza = stanzas[i] + if (!/SHA256:/im.test(stanza)) { + continue + } + let filename = '' + let sha = '' + const lines = stanza.split(/\r?\n/) + for (let j = 0, { length: n } = lines; j < n; j += 1) { + const line = lines[j] + const fm = /^Filename:\s*(\S+)/i.exec(line) + if (fm) { + filename = fm[1] + } + const sm = /^SHA256:\s*([0-9a-f]+)/i.exec(line) + if (sm) { + sha = sm[1] + } + } + if (sha && (!assetFilename || basename(filename) === assetFilename)) { + return sha + } + } + return '' +} + +// Parse a SHASUMS-style body (` ` lines, or a single bare hex +// line) for the checksum whose filename basename matches assetFilename. When +// the body is a single bare hex line, return it. Returns the bare hex or ''. +function parseShasumsLines(text, assetFilename) { + const lines = text.split(/\r?\n/) + let firstHex = '' + for (let i = 0, { length } = lines; i < length; i += 1) { + const line = lines[i].trim() + if (!line) { + continue + } + // ` ` (coreutils shasum shape; optional leading `*` binary + // marker before the filename). + const m = /^([0-9a-f]+)\s+\*?(.+)$/.exec(line) + if (m) { + const hash = m[1] + const name = m[2].trim() + if (!firstHex) { + firstHex = hash + } + if (!assetFilename || basename(name) === assetFilename) { + return hash + } + continue + } + // A bare hex line (rustup sidecar is a single ` rustup-init` line, + // but some publishers emit the bare hash only). + if (/^[0-9a-f]+$/i.test(line)) { + if (!firstHex) { + firstHex = line + } + if (!assetFilename) { + return line + } + } + } + // Fall back to the first hash seen when nothing matched by filename — a + // single-entry sidecar (rustup) names its binary, but the asset basename + // may differ (e.g. rustup-init.exe vs rustup-init), so the first hash is + // the safe match for a one-line file. + return firstHex +} + +// POSIX basename: last path segment. Tolerant of both `/` and `\` so a Windows +// repo-relative Filename (`pool\\main\\…`) resolves the same way. +function basename(p) { + if (!p) { + return p + } + const segs = String(p).split(/[\\/]/) + return segs[segs.length - 1] || p +} + +/** + * Parse a publisher checksum file body into the bare hex hash for the asset. + * Handles the three common shapes the fleet's pinned publishers emit: + * + * - Bare hex / SHASUMS (` ` per line) — rustup sidecars. + * - JSON (go.dev release manifest: `[{ version, files: [{ filename, sha256 }] + * }]`). + * - Debian `Packages` index (stanzas with `Filename:` + `SHA256:`) — Google + * Chrome. `assetFilename` (basename of the asset URL) picks the matching + * entry out of a multi-file body; when omitted, the first checksum is + * returned. Pure. + * + * @param {string} text + * @param {{ assetFilename?: string }} [options] + * + * @returns {string} Bare lowercase hex, or '' when no checksum is found. + */ +// oxlint-disable-next-line socket/export-top-level-functions -- dep-0 helper +export function parseChecksumFile(text, options = {}) { + const opts = { __proto__: null, ...options } + const assetFilename = opts.assetFilename || '' + const trimmed = (text || '').trim() + if (!trimmed) { + return '' + } + // JSON (go.dev manifest is an array; some sidecars are a single object). + if (trimmed.startsWith('[') || trimmed.startsWith('{')) { + try { + const data = JSON.parse(trimmed) + const hex = findShaInJson(data, assetFilename) + if (hex) { + return hex.toLowerCase() + } + } catch { + // Not valid JSON — fall through to the text formats. + } + } + // Debian Packages index. + if (/^Filename:/im.test(trimmed) && /^SHA256:/im.test(trimmed)) { + const hex = findShaInPackagesIndex(trimmed, assetFilename) + if (hex) { + return hex.toLowerCase() + } + } + // SHASUMS lines or bare hex. + return parseShasumsLines(trimmed, assetFilename).toLowerCase() +} + +/** + * Compare a pinned `value` (SRI string `sha256-` / + * `sha512-`) against a fetched bare-hex checksum. Returns true when + * they name the same digest. Handles both the publisher hex form (`sha256-<64 + * hex>`) and the npm SRI base64 form (`sha512-==`) by converting the + * fetched hex to the matching shape. Pure. + * + * @param {string} value + * @param {string} fetchedHex + * + * @returns {boolean} + */ +// oxlint-disable-next-line socket/export-top-level-functions -- dep-0 helper +export function checksumsMatch(value, fetchedHex) { + const parsed = parseSriValue(value) + if (!parsed || !fetchedHex) { + return false + } + const { algo, rest } = parsed + const hex = fetchedHex.toLowerCase() + // Publisher hex form: `-`. + if (rest.length === hexLenFor(algo) && /^[0-9a-f]+$/i.test(rest)) { + return rest.toLowerCase() === hex + } + // npm SRI base64 form: convert the fetched hex to base64 + strip padding on + // both sides (npm strips trailing `=`; the in-process hash keeps it). + const asBase64 = Buffer.from(hex, 'hex').toString('base64') + return asBase64.replace(/=+$/, '') === rest.replace(/=+$/, '') +} + +/** + * Staleness check for an ISO-date pin (`YYYY-MM-DD`). Returns the age in days + * (relative to `now`) plus a `stale` flag for the threshold. Pure given `now`. + * + * @param {string} dateString + * @param {{ now?: Date | (() => Date); maxAgeDays?: number }} [options] + * + * @returns {{ stale: boolean; ageDays: number } | undefined} Undefined when no + * date / unparseable date. + */ +// oxlint-disable-next-line socket/export-top-level-functions -- dep-0 helper +export function checkStaleness(dateString, options = {}) { + const opts = { __proto__: null, ...options } + const date = (dateString || '').trim() + // ISO calendar date only (YYYY-MM-DD); a datetime is accepted by truncating. + const m = /^(\d{4}-\d{2}-\d{2})/.exec(date) + if (!m) { + return undefined + } + const pinned = Date.parse(m[1] + 'T00:00:00Z') + if (Number.isNaN(pinned)) { + return undefined + } + const nowOpt = opts.now + const now = + typeof nowOpt === 'function' + ? nowOpt() + : nowOpt instanceof Date + ? nowOpt + : new Date() + const maxAgeDays = + typeof opts.maxAgeDays === 'number' && opts.maxAgeDays > 0 + ? opts.maxAgeDays + : 90 + const ageDays = Math.floor((now.getTime() - pinned) / 86_400_000) + return { stale: ageDays > maxAgeDays, ageDays } +} + +// ── orchestrator (exported; fetch injected for tests) ──────────────────── + +/** + * Verify an `integrity` pin's live provenance + staleness. For the STRING form + * (no src/date), this is a no-op — the static SRI check is the only gate, and + * existing behavior is unchanged. For the OBJECT form: + * - `src` present → fetch the publisher's current checksum, parse it, and + * compare to `value`. A mismatch FAILS (the pin is stale / re-released / + * possibly compromised — the hash no longer matches the source of truth). + * - `date` present → staleness check; a pin older than `maxAgeDays` WARNS by + * default and FAILS only under `strict`. + * + * @param {string | { value: string; src?: string; date?: string }} integrity + * @param {ProvenanceOptions} [options] + * + * @returns {Promise} + */ +// oxlint-disable-next-line socket/export-top-level-functions -- dep-0 helper +export async function verifyIntegrityProvenance(integrity, options = {}) { + const o = { __proto__: null, ...options } + const warn = + o.warn || + (msg => { + // pre-setup-node action; @socketsecurity/lib-stable not installed yet. + // oxlint-disable-next-line socket/no-console-prefer-logger -- dep-0 + console.error(msg) + }) + + // String form (or absent) → no provenance to check (static SRI is the only + // gate). Only the object form { value, src?, date? } carries provenance. + if (typeof integrity !== 'object' || integrity === null) { + return { + ok: true, + reason: 'string integrity — no provenance check', + status: 'pass', + } + } + const value = integrity.value + const src = integrity.src + const date = integrity.date + + // ── src: live provenance check ────────────────────────────────────────── + if (src) { + const assetFilename = o.assetFilename || '' + const fetchImpl = o.fetch || fetch + let res + try { + // pre-setup-node action: built-in fetch only. + // oxlint-disable-next-line socket/no-fetch-prefer-http-request -- dep-0 + res = await fetchImpl(src, { redirect: 'follow' }) + } catch (e) { + return { + ok: false, + status: 'fail', + reason: `provenance fetch failed for ${src}: ${e?.message ?? e}`, + } + } + if (!res || !res.ok) { + return { + ok: false, + status: 'fail', + reason: `provenance fetch failed: HTTP ${res?.status ?? '?'} for ${src}`, + } + } + const text = await res.text() + const fetched = parseChecksumFile(text, { assetFilename }) + if (!fetched) { + return { + ok: false, + status: 'fail', + reason: `could not parse a checksum from ${src} for ${assetFilename || '(asset)'}`, + } + } + if (!checksumsMatch(value, fetched)) { + return { + ok: false, + status: 'fail', + reason: `provenance mismatch: pin ${value} != publisher ${fetched} from ${src}`, + } + } + } + + // ── date: staleness check ─────────────────────────────────────────────── + const maxAgeDays = + typeof o.maxAgeDays === 'number' && o.maxAgeDays > 0 ? o.maxAgeDays : 90 + const strict = o.strict || false + if (date) { + const r = checkStaleness(date, { now: o.now, maxAgeDays }) + if (r) { + if (r.stale) { + const msg = + `· integrity pin is stale: date ${date} is ${r.ageDays} days old ` + + `(threshold ${maxAgeDays}) — re-verify against ${src || 'the publisher'}` + if (strict) { + return { + ok: false, + status: 'fail', + reason: msg, + stale: true, + ageDays: r.ageDays, + } + } + warn(msg) + return { + ok: true, + status: 'warn', + reason: msg, + stale: true, + ageDays: r.ageDays, + } + } + return { + ok: true, + status: 'pass', + reason: `pin is ${r.ageDays} days old (within ${maxAgeDays})`, + ageDays: r.ageDays, + } + } + } + + return { + ok: true, + status: 'pass', + reason: src ? 'provenance verified' : 'no src/date', + } +} diff --git a/.github/actions/fleet/checkout/action.yml b/.github/actions/fleet/checkout/action.yml index ef41bcada..856fd3613 100644 --- a/.github/actions/fleet/checkout/action.yml +++ b/.github/actions/fleet/checkout/action.yml @@ -197,7 +197,19 @@ runs: set -euo pipefail DEST="${CHECKOUT_DEST:-.}" cd "${DEST}" - if ! node scripts/repo/bootstrap/fleet.mjs; then + # The PRODUCER hydrates from its own template/base; only a consumer + # fetches a release bundle. Both reach this step, because the payload is + # untracked in either case, so the branch is on which tree is present. + # Without it the wheelhouse's own jobs took the consumer path and died + # on "no --ref and no bundle.ref" - correct for a member, meaningless in + # the repo that PUBLISHES the bundle, which has no ref of its own to + # pin. Mirrors materializeFromLocalTemplate, which returns undefined + # exactly when template/base is absent. + HYDRATE_ARGS=() + if [ -d template/base ]; then + HYDRATE_ARGS+=(--from-template) + fi + if ! node scripts/repo/bootstrap/fleet.mjs "${HYDRATE_ARGS[@]+"${HYDRATE_ARGS[@]}"}"; then echo "::error title=fleet payload hydration failed::node scripts/repo/bootstrap/fleet.mjs exited non-zero" { echo " What: the thin fleet payload fetch failed on a fresh checkout." @@ -205,7 +217,7 @@ runs: echo " Saw: a non-zero exit — see this step's own output above for the reason" echo " (bundle.ref unpinned, GHCR pull AND the gh-release fallback both" echo " failed, or a bundle-verification mismatch)." - echo " Fix: confirm .config/repo/socket-wheelhouse.json carries a valid" + echo " Fix: in a MEMBER, confirm .config/repo/socket-wheelhouse.json carries a valid" echo " bundle.ref, that ghcr.io/socketdev/socket-wheelhouse/fleet-pack is" echo " reachable, and — if GHCR is down — that payload-token-client-id /" echo " payload-token-private-key are wired through to this action." @@ -333,7 +345,7 @@ runs: run: | if [ -d .github ]; then # Pass --gh-token only when a token is actually present. In a local - # agent-ci run github.token is empty, and zizmor 1.25+ treats an empty + # local-ci run github.token is empty, and zizmor 1.25+ treats an empty # `--gh-token ""` as a real token, then fatally errors ("no audit was # performed") when its online impostor-commit check can't reach the # GitHub API. Omitting the flag makes zizmor skip its online audits diff --git a/.github/actions/fleet/download-artifact/action.yml b/.github/actions/fleet/download-artifact/action.yml new file mode 100644 index 000000000..b5113eac4 --- /dev/null +++ b/.github/actions/fleet/download-artifact/action.yml @@ -0,0 +1,41 @@ +# Layer 1 — leaf action (no internal SocketDev refs). The first-party +# artifact-service client (scripts/fleet/artifact/download.mts) is the port of +# @actions/artifact's download surface; this composite is the workflow-facing +# wrapper over it. When this file bumps, the cascade is: 2b → 3 → 4 → external +# repos. See updating-workflows skill at .claude/skills/updating-workflows/. +# +# Runs expose-actions-runtime first to surface the runner-injected +# ACTIONS_RESULTS_URL + ACTIONS_RUNTIME_TOKEN the download client reads (a +# composite's `run:` steps never see them otherwise). No third-party +# actions/download-artifact dependency. Downloads one named artifact; a +# publish job that needs several uses one step per name. + +name: 'Download Artifact' +description: >- + Download one named GitHub Actions artifact from the current run into a + directory via the first-party artifact-service client + (scripts/fleet/artifact/download.mts). The zip's relative paths are + preserved under the destination. + +inputs: + name: + description: 'The artifact name to download' + required: true + path: + description: 'The directory to write the entries under' + required: true + +runs: + using: 'composite' + steps: + - uses: ./.github/actions/fleet/expose-actions-runtime + - name: Download artifact + shell: bash + env: + ARTIFACT_NAME: ${{ inputs.name }} + ARTIFACT_PATH: ${{ inputs.path }} + run: | + set -euo pipefail + node scripts/fleet/artifact/download.mts \ + --name "$ARTIFACT_NAME" \ + --path "$ARTIFACT_PATH" diff --git a/.github/actions/fleet/github-payload-app-token/action.yml b/.github/actions/fleet/github-payload-app-token/action.yml index ca087adb8..558c2909b 100644 --- a/.github/actions/fleet/github-payload-app-token/action.yml +++ b/.github/actions/fleet/github-payload-app-token/action.yml @@ -52,4 +52,5 @@ runs: OWNER: ${{ inputs.owner || github.repository_owner }} PERMISSIONS: '{"contents":"read"}' REPOSITORIES: ${{ inputs.repositories }} - run: node "${{ github.action_path }}/mint-app-installation-token.mjs" + GITHUB_ACTION_PATH: ${{ github.action_path }} + run: node "${GITHUB_ACTION_PATH}/mint-app-installation-token.mjs" diff --git a/.github/actions/fleet/github-pr-app-token/action.yml b/.github/actions/fleet/github-pr-app-token/action.yml index a2a6139f6..80b16a98f 100644 --- a/.github/actions/fleet/github-pr-app-token/action.yml +++ b/.github/actions/fleet/github-pr-app-token/action.yml @@ -47,4 +47,5 @@ runs: OWNER: ${{ inputs.owner || github.repository_owner }} PERMISSIONS: '{"contents":"write","issues":"write","pull_requests":"write"}' REPOSITORIES: ${{ inputs.repositories }} - run: node "${{ github.action_path }}/mint-app-installation-token.mjs" + GITHUB_ACTION_PATH: ${{ github.action_path }} + run: node "${GITHUB_ACTION_PATH}/mint-app-installation-token.mjs" diff --git a/.github/actions/fleet/github-release-app-token/action.yml b/.github/actions/fleet/github-release-app-token/action.yml index c65f5b6b6..87d23797a 100644 --- a/.github/actions/fleet/github-release-app-token/action.yml +++ b/.github/actions/fleet/github-release-app-token/action.yml @@ -58,4 +58,5 @@ runs: OWNER: ${{ inputs.owner || github.repository_owner }} PERMISSIONS: '{"contents":"write"}' REPOSITORIES: ${{ inputs.repositories }} - run: node "${{ github.action_path }}/mint-app-installation-token.mjs" + GITHUB_ACTION_PATH: ${{ github.action_path }} + run: node "${GITHUB_ACTION_PATH}/mint-app-installation-token.mjs" diff --git a/.github/actions/fleet/install/action.yml b/.github/actions/fleet/install/action.yml index 287cf1d74..ed493cd76 100644 --- a/.github/actions/fleet/install/action.yml +++ b/.github/actions/fleet/install/action.yml @@ -42,7 +42,8 @@ runs: # Empty for a non-thin member, where that fetch no-ops and needs no token. env: GH_TOKEN: ${{ inputs.fleet-payload-token }} - run: | + FROZEN_LOCKFILE: ${{ inputs.frozen-lockfile }} + run: | # zizmor: ignore[github-env] if [ -z "$SFW_BIN" ] || [ ! -x "$SFW_BIN" ]; then echo "Error: sfw is not installed — run the setup-and-install action first" >&2 exit 1 @@ -53,7 +54,22 @@ runs: # 1` with no clue what broke. The default reporter is verbose # but at least surfaces the cause; CI's `set -e` will still # exit on non-zero. - pnpm install ${{ inputs.frozen-lockfile == 'true' && '--frozen-lockfile' || '' }} + # frozen-lockfile is env-mapped (never interpolated into the run + # body — zizmor template-injection shape); branch on $FROZEN_LOCKFILE. + # Both directions are passed EXPLICITLY. pnpm turns frozen-lockfile ON + # by itself when CI=true, so omitting the flag is not "off" - it is + # "on", and the input then cannot express what it claims. That bites a + # repo whose fleet mirrors arrive during the `prepare` lifecycle: at the + # frozen CHECK the live mirror workspace projects do not exist yet, so + # the lockfile's importer set cannot match + # (ERR_PNPM_LOCKFILE_CONFIG_MISMATCH). Measured: a pristine wheelhouse + # clone is missing 258 of its 514 lockfile importers until hydration + # runs. + if [ "$FROZEN_LOCKFILE" = 'true' ]; then + pnpm install --frozen-lockfile + else + pnpm install --no-frozen-lockfile + fi - name: Verify @socketsecurity/lib resolvable and >= floor version shell: bash diff --git a/.github/actions/fleet/setup-odai/action.yml b/.github/actions/fleet/setup-odai/action.yml index aae89b6a2..286f74870 100644 --- a/.github/actions/fleet/setup-odai/action.yml +++ b/.github/actions/fleet/setup-odai/action.yml @@ -67,19 +67,51 @@ runs: # The image ships google-chrome-stable; install only when absent. odai # requires real Chrome — Chromium lacks optimization_guide_internal and - # cannot run the on-device model. + # cannot run the on-device model. The .deb is the version-pinned pool file + # from dl.google.com (NOT the floating _current_amd64.deb the old action + # used), SHA-256-verified against the pin in external-tools.json BEFORE + # apt-get install — no more unverified wget-and-install. - name: Ensure Google Chrome stable if: ${{ steps.gate.outputs.enabled == 'true' }} shell: bash + env: + GITHUB_ACTION_PATH: ${{ github.action_path }} + GITHUB_WORKSPACE: ${{ github.workspace }} run: | set -euo pipefail if command -v google-chrome-stable >/dev/null 2>&1; then echo "preinstalled: $(google-chrome-stable --version)" exit 0 fi - wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo apt-get install -y ./google-chrome-stable_current_amd64.deb - rm google-chrome-stable_current_amd64.deb + RESOLVER="${GITHUB_ACTION_PATH}/../_shared/resolve-external-tool-asset.mjs" + JQ="${GITHUB_ACTION_PATH}/../_shared/jq.mjs" + INSTALL_TOOL="${GITHUB_ACTION_PATH}/../_shared/install-tool.mjs" + # Resolve the pinned .deb URL + SHA-256 for linux-x64 from + # external-tools.json (the `google-chrome-stable` node-dist entry). + PLAN_JSON="$(node "$RESOLVER" --tool google-chrome-stable)" + ASSET="$(printf '%s' "$PLAN_JSON" | node "$JQ" - asset)" + INTEGRITY="$(printf '%s' "$PLAN_JSON" | node "$JQ" - integrity)" + # Forward the object-form integrity provenance (src/date) so + # install-tool.mjs live-verifies the pin against the Google Packages + # index + checks staleness AFTER the static SRI check. jq.mjs exits + # non-zero on empty; tolerate with `|| true`. + SRC="$(printf '%s' "$PLAN_JSON" | node "$JQ" - src 2>/dev/null || true)" + DATE="$(printf '%s' "$PLAN_JSON" | node "$JQ" - date 2>/dev/null || true)" + PROVENANCE_ARGS=() + [[ -n "$SRC" ]] && PROVENANCE_ARGS+=(--src "$SRC") + [[ -n "$DATE" ]] && PROVENANCE_ARGS+=(--date "$DATE") + chrome_dir="${RUNNER_TEMP:-/tmp}/chrome-deb" + rm -rf "$chrome_dir" + mkdir -p "$chrome_dir" + # Download + SRI-verify the .deb, then live-verify provenance. + # install-tool.mjs streams it, verifies sha256 in-process via + # node:crypto (aborts on mismatch, exit 2), live-verifies the pin + # against src + checks date staleness; a .deb is not a recognized + # archive so the file is left on disk chmod'd for apt-get install. + node "$INSTALL_TOOL" "$ASSET" "$INTEGRITY" "$chrome_dir" "${PROVENANCE_ARGS[@]}" + DEB="$chrome_dir/$(basename "$ASSET")" + sudo apt-get install -y "$DEB" + rm -f "$DEB" google-chrome-stable --version # Bounded cache set: the model component dirs + activation state — the @@ -110,9 +142,66 @@ runs: shell: bash env: ODAI_VERSION: ${{ inputs.odai-version }} - run: | + GITHUB_ACTION_PATH: ${{ github.action_path }} + run: | # zizmor: ignore[github-env] set -euo pipefail - npm install -g "@socketsecurity/odai@${ODAI_VERSION}" + # Provision odai via the fleet's downloadNpmPackage path (the same + # shape install/action.yml uses for agentshield) rather than a raw + # `npm install -g`. Node sees the consumer repo's node_modules + # (already installed by setup-and-install, which runs before this + # action), so the dynamic import of @socketsecurity/lib/dlx/package + # resolves the workspace copy. + JQ="${GITHUB_ACTION_PATH}/../_shared/jq.mjs" + ODAI_SPEC="@socketsecurity/odai@${ODAI_VERSION}" + # Name + option compat across the @socketsecurity/lib rename boundary: + # <= 6.0.6 exports `downloadPackage({ package })`; >= 6.0.7 renamed it + # to `downloadNpmPackage({ spec })`. Accept BOTH — pick whichever export + # the module provides and pass both option keys. `|| true` + a SKIP + # sentinel: the import can legitimately fail to resolve when the + # consumer IS @socketsecurity/lib itself and its dist/ isn't built yet + # at this setup-time step (chicken-and-egg). odai legs are fail-open, + # so skip with a warning rather than failing the whole job. + RESULT_JSON="$(ODAI_SPEC="$ODAI_SPEC" node --input-type=module -e " + let mod; + try { + mod = await import('@socketsecurity/lib-stable/dlx/package'); + } catch { + try { + mod = await import('@socketsecurity/lib/dlx/package'); + } catch (e) { + process.stdout.write(JSON.stringify({ skip: 'import-failed', error: String(e && e.message || e) })); + process.exit(0); + } + } + const download = mod.downloadNpmPackage ?? mod.downloadPackage; + if (typeof download !== 'function') { + process.stdout.write(JSON.stringify({ skip: 'no-export' })); + process.exit(0); + } + const spec = process.env.ODAI_SPEC; + const { binaryPath, installed } = await download({ + spec, + package: spec, + binaryName: 'odai', + }); + process.stdout.write(JSON.stringify({ binaryPath, installed })); + " 2>/dev/null || true)" + SKIP="$(echo "$RESULT_JSON" | node "$JQ" - skip 2>/dev/null || true)" + if [ -n "$SKIP" ]; then + echo "⚠ Skipping odai install ($SKIP) — @socketsecurity/lib/dlx/package unavailable at setup time. odai legs will clean-skip." >&2 + exit 0 + fi + BIN="$(echo "$RESULT_JSON" | node "$JQ" - binaryPath 2>/dev/null || true)" + if [ -z "$BIN" ] || [ ! -x "$BIN" ]; then + echo "× odai install failed — the npm-package downloader did not return an executable binary path" >&2 + echo " Got: $RESULT_JSON" >&2 + exit 1 + fi + # Put the odai binary on PATH for this step + subsequent steps (the + # fill step below runs `odai summarize`). + BINDIR="$(dirname "$BIN")" + echo "$BINDIR" >> "${GITHUB_PATH:-/dev/null}" + export PATH="$BINDIR:$PATH" odai --help >/dev/null # Fill-on-miss. The component download wants ~22 GB free and Chrome diff --git a/.github/actions/fleet/setup-rust-toolchain/action.yml b/.github/actions/fleet/setup-rust-toolchain/action.yml index aa9526d37..2e7f923d0 100644 --- a/.github/actions/fleet/setup-rust-toolchain/action.yml +++ b/.github/actions/fleet/setup-rust-toolchain/action.yml @@ -67,34 +67,66 @@ runs: TARGETS: ${{ inputs.targets }} COMPONENTS: ${{ inputs.components }} PROFILE: ${{ inputs.profile }} + GITHUB_ACTION_PATH: ${{ github.action_path }} + GITHUB_WORKSPACE: ${{ github.workspace }} run: | # zizmor: ignore[github-env] set -euo pipefail - # rustup is preinstalled on GitHub-hosted runners. On - # self-hosted / minimal images, we fetch rustup-init from - # sh.rustup.rs (already on the SFW bypass list). + # rustup is preinstalled on GitHub-hosted runners. On self-hosted / + # minimal images, we fetch rustup-init from static.rust-lang.org (the + # canonical rustup dist host; sh.rustup.rs / win.rustup.rs redirect + # there) — and SRI-verify its SHA-256 against the pin in + # external-tools.json BEFORE executing it. No more `curl | sh`. if ! command -v rustup >/dev/null 2>&1; then - echo "rustup not on PATH, installing via rustup-init..." + echo "rustup not on PATH, installing via rustup-init (SRI-verified)…" + + RESOLVER="${GITHUB_ACTION_PATH}/../_shared/resolve-external-tool-asset.mjs" + JQ="${GITHUB_ACTION_PATH}/../_shared/jq.mjs" + INSTALL_TOOL="${GITHUB_ACTION_PATH}/../_shared/install-tool.mjs" + + # Resolve the rustup-init asset URL + pinned SHA-256 for THIS + # runner's platform from external-tools.json (the `rustup` manager + # entry). rustup is pinned to a single version; the pin IS the + # integrity source (no per-version override path). + PLAN_JSON="$(node "$RESOLVER" --tool rustup)" + ASSET="$(printf '%s' "$PLAN_JSON" | node "$JQ" - asset)" + INTEGRITY="$(printf '%s' "$PLAN_JSON" | node "$JQ" - integrity)" + + # Forward the object-form integrity provenance (src/date) so + # install-tool.mjs live-verifies the pin against the rustup sidecar + # + checks staleness AFTER the static SRI check. jq.mjs exits + # non-zero on empty; tolerate with `|| true`. + SRC="$(printf '%s' "$PLAN_JSON" | node "$JQ" - src 2>/dev/null || true)" + DATE="$(printf '%s' "$PLAN_JSON" | node "$JQ" - date 2>/dev/null || true)" + PROVENANCE_ARGS=() + [[ -n "$SRC" ]] && PROVENANCE_ARGS+=(--src "$SRC") + [[ -n "$DATE" ]] && PROVENANCE_ARGS+=(--date "$DATE") + + # Download + SRI-verify the bare rustup-init binary, then live-verify + # provenance. install-tool.mjs streams it, verifies sha256 in-process + # via node:crypto (aborts on mismatch, exit 2), live-verifies the pin + # against src + checks date staleness, and chmod +x the binary. + rustup_dir="${RUNNER_TEMP:-/tmp}/rustup-init-bin" + rm -rf "$rustup_dir" + mkdir -p "$rustup_dir" + node "$INSTALL_TOOL" "$ASSET" "$INTEGRITY" "$rustup_dir" "${PROVENANCE_ARGS[@]}" case "$(uname -s)" in Linux|Darwin) - # rustup-init.sh is the canonical bootstrap script. # --default-toolchain none: we'll install the requested # channel explicitly in the next step. # --profile minimal: don't pull docs/clippy here; the # per-input PROFILE controls the final set. # -y: non-interactive. - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --default-toolchain none --profile minimal + "$rustup_dir/rustup-init" -y \ + --default-toolchain none --profile minimal # shellcheck disable=SC1091 . "$HOME/.cargo/env" ;; MINGW*|MSYS*|CYGWIN*) # Windows rustup-init.exe; same flags. - curl --proto '=https' --tlsv1.2 -sSfo rustup-init.exe \ - https://win.rustup.rs/x86_64 - ./rustup-init.exe -y --default-toolchain none --profile minimal - rm rustup-init.exe + "$rustup_dir/rustup-init.exe" -y \ + --default-toolchain none --profile minimal ;; *) echo "× Unsupported platform: $(uname -s)" >&2 diff --git a/.github/actions/fleet/upload-artifact/action.yml b/.github/actions/fleet/upload-artifact/action.yml new file mode 100644 index 000000000..838fe5ee1 --- /dev/null +++ b/.github/actions/fleet/upload-artifact/action.yml @@ -0,0 +1,55 @@ +# Layer 1 — leaf action (no internal SocketDev refs). The first-party +# artifact-service client (scripts/fleet/artifact/upload.mts) is the port of +# @actions/artifact's upload surface; this composite is the workflow-facing +# wrapper over it. When this file bumps, the cascade is: 2b → 3 → 4 → external +# repos. See updating-workflows skill at .claude/skills/updating-workflows/. +# +# The runner injects ACTIONS_RESULTS_URL + ACTIONS_RUNTIME_TOKEN into +# JavaScript-action processes only - a composite's `run:` steps never see them - +# so this runs expose-actions-runtime first to surface them to the upload step. +# No third-party actions/upload-artifact dependency. + +name: 'Upload Artifact' +description: >- + Upload one or more paths as a named GitHub Actions artifact via the + first-party artifact-service client (scripts/fleet/artifact/upload.mts). A + missing path or an empty set fails loud - the actions/upload-artifact + if-no-files-found: error default. + +inputs: + name: + description: 'The artifact name to upload under' + required: true + path: + description: >- + A path (file or directory) to include. Repeatable as a newline-separated + list. + required: true + if-no-files-found: + description: >- + Fail when no files match (error) or skip silently (ignore). Default + error. + required: false + default: 'error' + +runs: + using: 'composite' + steps: + - uses: ./.github/actions/fleet/expose-actions-runtime + - name: Upload artifact + shell: bash + env: + ARTIFACT_NAME: ${{ inputs.name }} + ARTIFACT_PATH: ${{ inputs.path }} + IF_NO_FILES_FOUND: ${{ inputs.if-no-files-found }} + run: | + set -euo pipefail + # The path input may be a newline-separated list; turn each line into a + # --path arg so one upload covers every named path. + args=(--name "$ARTIFACT_NAME" --if-no-files-found "$IF_NO_FILES_FOUND") + while IFS= read -r line; do + if [ -n "$line" ]; then + args+=(--path "$line") + fi + done <<< "$ARTIFACT_PATH" + node scripts/fleet/artifact/upload.mts "${args[@]}" diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 1d6651a2c..df5420b2d 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -273,6 +273,10 @@ jobs: git checkout -q --detach FETCH_HEAD - uses: ./.github/actions/fleet/setup-and-install with: + # Authorizes a thin member's bundle hydration right after checkout. + # Both stay empty on a member with no payload App, skipping the mint. + payload-token-client-id: ${{ vars.SOCKET_PAYLOAD_CLIENT_ID }} + payload-token-private-key: ${{ secrets.SOCKET_PAYLOAD_APP_PRIVATE_KEY }} # The org secret feeds both SOCKET_API_TOKEN and SOCKET_API_KEY. socket-api-token: ${{ secrets.SOCKET_API_TOKEN_FOR_CLI_AND_SFW }} - name: Smoke the JS fuzz lane @@ -315,7 +319,7 @@ jobs: fi git checkout -q --detach FETCH_HEAD - name: Go toolchain - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 (2026-06-26) + uses: ./.github/actions/fleet/setup-go-toolchain with: go-version-file: go.mod - name: Smoke go fuzz functions, 60s each @@ -465,12 +469,17 @@ jobs: # coverage-guided. Save under a run-unique key and restore the most recent # prior corpus through the shared prefix. - name: Restore the cached corpus - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 (2026-06-26) - with: - key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }} - path: fuzz/corpus/${{ matrix.target }} - restore-keys: | - fuzz-corpus-${{ matrix.target }}- + shell: bash + env: + CACHE_PATH: fuzz/corpus/${{ matrix.target }} + CACHE_KEY: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }} + CACHE_RESTORE_KEY: fuzz-corpus-${{ matrix.target }}- + run: | + set -euo pipefail + node scripts/fleet/cache/restore.mts \ + --path "$CACHE_PATH" \ + --key "$CACHE_KEY" \ + --restore-key "$CACHE_RESTORE_KEY" # ADDITIVE: layer the committed seeds on top of the cache-restored corpus # WITHOUT deleting fuzzer-discovered growth. The default reset mode would # rmtree the restored corpus, so coverage could never accumulate across @@ -492,11 +501,22 @@ jobs: # the fix + minimized-regression-commit protocol can proceed. - name: Upload crash artifacts if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 (2026-08-01) + uses: ./.github/actions/fleet/upload-artifact with: if-no-files-found: ignore name: fuzz-artifacts-${{ matrix.target }} path: fuzz/artifacts/${{ matrix.target }}/ + - name: Save the fuzz corpus + if: success() + shell: bash + env: + CACHE_PATH: fuzz/corpus/${{ matrix.target }} + CACHE_KEY: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }} + run: | + set -euo pipefail + node scripts/fleet/cache/save.mts \ + --path "$CACHE_PATH" \ + --key "$CACHE_KEY" # Last step on purpose: the restore-phase "Rust cache" call above # exported the RUST_CACHE_* job env; this saves the populated target # dirs. Runs only when the campaign succeeded — the same success gate @@ -535,6 +555,11 @@ jobs: fi git checkout -q --detach FETCH_HEAD - uses: ./.github/actions/fleet/setup-and-install + with: + # Authorizes a thin member's bundle hydration right after checkout. + # Both stay empty on a member with no payload App, skipping the mint. + payload-token-client-id: ${{ vars.SOCKET_PAYLOAD_CLIENT_ID }} + payload-token-private-key: ${{ secrets.SOCKET_PAYLOAD_APP_PRIVATE_KEY }} - name: Run the JS fuzz campaign run: | set -euo pipefail @@ -572,7 +597,7 @@ jobs: fi git checkout -q --detach FETCH_HEAD - name: Go toolchain - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 (2026-06-26) + uses: ./.github/actions/fleet/setup-go-toolchain with: go-version-file: go.mod - name: Run go fuzz functions, 10 minutes each diff --git a/.github/workflows/get-green.yml b/.github/workflows/get-green.yml index 8d3d3b49d..f0fd75880 100644 --- a/.github/workflows/get-green.yml +++ b/.github/workflows/get-green.yml @@ -88,14 +88,27 @@ jobs: env: SOCKET_API_KEY: ${{ secrets.SOCKET_API_TOKEN_FOR_CLI_AND_SFW }} steps: - - name: Checkout the failing branch - uses: ./.github/actions/fleet/checkout - with: - ref: ${{ inputs.branch }} - # Authorizes a thin member's bundle download during install. Both - # stay empty on a member with no payload App, which skips the mint. - payload-token-client-id: ${{ vars.SOCKET_PAYLOAD_CLIENT_ID }} - payload-token-private-key: ${{ secrets.SOCKET_PAYLOAD_APP_PRIVATE_KEY }} + - name: Bootstrap checkout + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + SERVER_URL: ${{ github.server_url }} + REPOSITORY: ${{ github.repository }} + TRIGGER_REF: ${{ inputs.branch }} + run: | + set -euo pipefail + git init -q + git config --local advice.detachedHead false + git remote remove origin 2>/dev/null || true + git remote add origin "${SERVER_URL}/${REPOSITORY}" + FETCH_ARGS=(--no-tags --prune --depth 1 origin "${TRIGGER_REF}") + if [ -n "${GITHUB_TOKEN}" ]; then + AUTH_B64="$(printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 | tr -d '\n')" + git -c "http.${SERVER_URL}/.extraheader=AUTHORIZATION: basic ${AUTH_B64}" fetch "${FETCH_ARGS[@]}" + else + git fetch "${FETCH_ARGS[@]}" + fi + git checkout -q --detach FETCH_HEAD - uses: ./.github/actions/fleet/setup-and-install with: @@ -136,6 +149,8 @@ jobs: shell: bash env: BRANCH: ${{ inputs.branch }} + GITHUB_TOKEN: ${{ github.token }} + SERVER_URL: ${{ github.server_url }} run: | set -euo pipefail if git diff --quiet; then @@ -153,4 +168,17 @@ jobs: git -c user.name='socket-bot' \ -c user.email='socket-bot@users.noreply.github.com' \ commit -m 'fix(deps): apply the deterministic autofixer after the update' - git push origin "HEAD:${BRANCH}" + # The bootstrap checkout above authenticated only its own fetch via + # a per-invocation `-c`, so nothing landed in .git/config — this + # push needs its own auth the same way. The job's contents: write + # GITHUB_TOKEN is enough; the header is cleared in an always() step + # below rather than left in .git/config. + AUTH_B64="$(printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 | tr -d '\n')" + git -c "http.${SERVER_URL}/.extraheader=AUTHORIZATION: basic ${AUTH_B64}" \ + push origin "HEAD:${BRANCH}" + + - name: Clear push credentials + if: ${{ always() }} + env: + SERVER_URL: ${{ github.server_url }} + run: git config --local --unset-all "http.${SERVER_URL}/.extraheader" || true diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 003efd66c..546c787da 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -58,10 +58,13 @@ jobs: # independently) to populate the workspace so the LOCAL # ./.github/actions/* composite resolves; the fleet checkout action then # re-checks-out at its own depth. - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-15) + - uses: ./.github/actions/fleet/checkout with: + # Authorizes a thin member's bundle hydration right after checkout. + # Both stay empty on a member with no payload App, skipping the mint. + payload-token-client-id: ${{ vars.SOCKET_PAYLOAD_CLIENT_ID }} + payload-token-private-key: ${{ secrets.SOCKET_PAYLOAD_APP_PRIVATE_KEY }} fetch-depth: 1 - persist-credentials: false - name: Checkout uses: ./.github/actions/fleet/checkout with: diff --git a/.github/workflows/npm-publish-dryrun.yml b/.github/workflows/npm-publish-dryrun.yml index f7ef50331..1c31a5e45 100644 --- a/.github/workflows/npm-publish-dryrun.yml +++ b/.github/workflows/npm-publish-dryrun.yml @@ -43,8 +43,12 @@ jobs: # First step must be the third-party actions/checkout (GitHub fetches it # independently) to populate the workspace so the LOCAL # ./.github/actions/* composite resolves. - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-15) + - uses: ./.github/actions/fleet/checkout with: + # Authorizes a thin member's bundle hydration right after checkout. + # Both stay empty on a member with no payload App, skipping the mint. + payload-token-client-id: ${{ vars.SOCKET_PAYLOAD_CLIENT_ID }} + payload-token-private-key: ${{ secrets.SOCKET_PAYLOAD_APP_PRIVATE_KEY }} fetch-depth: 1 # The bump derivation anchors on registry-latest + the last v-tag; # a tagless shallow checkout derives from zero on a repo whose diff --git a/.github/workflows/on-device-model-cache.yml b/.github/workflows/on-device-model-cache.yml index 36964290d..c745209fb 100644 --- a/.github/workflows/on-device-model-cache.yml +++ b/.github/workflows/on-device-model-cache.yml @@ -104,16 +104,16 @@ jobs: # user-data-dir is Chrome litter (GPU/code caches, crashpad) that would # grow the entry on every weekly refill without bounding benefit. - name: Restore any prior model profile - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 (2026-06-26) - with: - path: | - /home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceModel - /home/runner/.cache/odai/chrome-builtin/optimization_guide_model_store - /home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceClassifierModel - /home/runner/.cache/odai/chrome-builtin/Local State - key: odai-nano-Linux-x64-fill-anchor - restore-keys: | - odai-nano-Linux-x64- + shell: bash + run: | + set -euo pipefail + node scripts/fleet/cache/restore.mts \ + --path "/home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceModel" \ + --path "/home/runner/.cache/odai/chrome-builtin/optimization_guide_model_store" \ + --path "/home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceClassifierModel" \ + --path "/home/runner/.cache/odai/chrome-builtin/Local State" \ + --key "odai-nano-Linux-x64-fill-anchor" \ + --restore-key "odai-nano-Linux-x64-" - name: Build the CLI shell: bash @@ -173,14 +173,17 @@ jobs: echo "component version: ${VERSION}" - name: Save the profile cache - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 (2026-06-26) - with: - path: | - /home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceModel - /home/runner/.cache/odai/chrome-builtin/optimization_guide_model_store - /home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceClassifierModel - /home/runner/.cache/odai/chrome-builtin/Local State - key: ${{ steps.component.outputs.cache-key }} + shell: bash + env: + CACHE_KEY: ${{ steps.component.outputs.cache-key }} + run: | + set -euo pipefail + node scripts/fleet/cache/save.mts \ + --path "/home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceModel" \ + --path "/home/runner/.cache/odai/chrome-builtin/optimization_guide_model_store" \ + --path "/home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceClassifierModel" \ + --path "/home/runner/.cache/odai/chrome-builtin/Local State" \ + --key "$CACHE_KEY" verify: name: 🧊 Offline verify on a fresh runner @@ -243,15 +246,18 @@ jobs: payload-token-private-key: ${{ secrets.SOCKET_PAYLOAD_APP_PRIVATE_KEY }} - name: Restore the filled profile (miss = failure) - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 (2026-06-26) - with: - path: | - /home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceModel - /home/runner/.cache/odai/chrome-builtin/optimization_guide_model_store - /home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceClassifierModel - /home/runner/.cache/odai/chrome-builtin/Local State - key: ${{ needs.fill.outputs.cache-key }} - fail-on-cache-miss: true + shell: bash + env: + CACHE_KEY: ${{ needs.fill.outputs.cache-key }} + run: | + set -euo pipefail + node scripts/fleet/cache/restore.mts \ + --path "/home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceModel" \ + --path "/home/runner/.cache/odai/chrome-builtin/optimization_guide_model_store" \ + --path "/home/runner/.cache/odai/chrome-builtin/OptGuideOnDeviceClassifierModel" \ + --path "/home/runner/.cache/odai/chrome-builtin/Local State" \ + --key "$CACHE_KEY" \ + --fail-on-miss - name: Build the CLI shell: bash diff --git a/.github/workflows/release-reconcile.yml b/.github/workflows/release-reconcile.yml index 77541cdeb..c8abd4e59 100644 --- a/.github/workflows/release-reconcile.yml +++ b/.github/workflows/release-reconcile.yml @@ -158,10 +158,13 @@ jobs: # independently) to populate the workspace so the LOCAL # ./.github/actions/* composite resolves; setup-and-install re-checks-out # at the full depth below. - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-15) + - uses: ./.github/actions/fleet/checkout with: + # Authorizes a thin member's bundle hydration right after checkout. + # Both stay empty on a member with no payload App, skipping the mint. + payload-token-client-id: ${{ vars.SOCKET_PAYLOAD_CLIENT_ID }} + payload-token-private-key: ${{ secrets.SOCKET_PAYLOAD_APP_PRIVATE_KEY }} fetch-depth: 1 - persist-credentials: false - name: Setup + install uses: ./.github/actions/fleet/setup-and-install with: diff --git a/.github/workflows/weekly-update.yml b/.github/workflows/weekly-update.yml index b2b8410aa..5266d9348 100644 --- a/.github/workflows/weekly-update.yml +++ b/.github/workflows/weekly-update.yml @@ -79,6 +79,9 @@ jobs: set -euo pipefail ACTIONABLE='true' node scripts/fleet/weekly-update.mts --check-updates || ACTIONABLE='false' + # Report inline-action port drift (does not gate the update; a + # behind port is reported for the weekly output). + node scripts/fleet/sync-inline-action-pins.mts || true echo "actionable=${ACTIONABLE}" >> "$GITHUB_OUTPUT" update: diff --git a/.gitignore b/.gitignore index 18ed7cade..2784eac59 100644 --- a/.gitignore +++ b/.gitignore @@ -291,7 +291,8 @@ pnpm-debug.log /.claude/skills/fleet/ /.claude/workflows/ /.config/fleet/.markdownlint-cli2.jsonc -/.config/fleet/egress-allowlist.json +/.config/fleet/external-tools.json +/.config/fleet/fetch-allowlist.json /.config/fleet/git-authors.json /.config/fleet/lockstep.schema.json /.config/fleet/markdownlint-rules/ @@ -322,6 +323,7 @@ pnpm-debug.log /.github/actions/fleet/run-offline/ /.github/actions/fleet/run-script/ /.github/actions/fleet/setup-git-signing/ +/.github/actions/fleet/setup-go-toolchain/ /.github/actions/fleet/setup-odai/ /.github/actions/fleet/setup-rust-cache/ /.github/actions/fleet/setup-rust-toolchain/ @@ -332,7 +334,6 @@ pnpm-debug.log /patches/@socketsecurity__lib@6.7.0.patch /patches/brace-expansion@5.0.9.patch /patches/minimatch@10.2.6.patch -/patches/taze@20.0.0.patch /scripts/fleet/ # # diff --git a/.npmrc b/.npmrc index 2eb1a0fd8..66ae3b056 100644 --- a/.npmrc +++ b/.npmrc @@ -12,6 +12,9 @@ registry=https://registry.npmjs.org/ min-release-age=7 # min-release-age-exclude (npm >= 11.17.0): packages exempt from the soak. # Socket-owned scopes ship through Socket’s own provenance pipeline. +min-release-age-exclude[]=@abitious/* +min-release-age-exclude[]=@decmpfs/* +min-release-age-exclude[]=@node-smol/* min-release-age-exclude[]=@sdxgen/* min-release-age-exclude[]=@socketoverride/* min-release-age-exclude[]=@socketregistry/* @@ -21,6 +24,7 @@ min-release-age-exclude[]=@ultrathink/* min-release-age-exclude[]=sdxgen min-release-age-exclude[]=sfw min-release-age-exclude[]=socket +min-release-age-exclude[]=sockeye # Per-platform native binding families currently inside their 7-day soak # (pnpm pins the exact versions; see pnpm-workspace.yaml for publish/removable # dates). One glob exempts every binding in the family, and the globs are @@ -28,6 +32,8 @@ min-release-age-exclude[]=socket # Name-only npm mirror of the dated `name@version` pins the manifest’s # EXPECTED_RELEASE_AGE_EXCLUDE carries (npm matches by NAME or glob only — # npm/cli#9532 — so the version lives on the pnpm side). +min-release-age-exclude[]=dtu-github-actions +min-release-age-exclude[]=run-local-ci # Everything ABOVE this sentinel is fleet-canonical and is replaced from # the wheelhouse source on every placement. Host-only npm settings, and the diff --git a/CLAUDE.md b/CLAUDE.md index dca0b95b7..66cd58f40 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,29 +26,29 @@ - 🚨 Bump order: (0) the USER names X.Y.Z, NEVER the agent (`--dry-run` fine); (1) pre-bump wave. [`version-bumps`](docs/agents.md/fleet/version-bumps.md) - 🚨 NEVER open a pull request to land a version bump: the bump commit goes DIRECTLY on the default branch via the release App. (`.claude/hooks/fleet/no-version-bump-pr-guard/`) [`version-bumps`](docs/agents.md/fleet/version-bumps.md) - 🚨 Dot-naming `@owner/[.].[-]`: the `.target` token carries the domain. [`binary-vs-napi-naming`](docs/agents.md/fleet/binary-vs-napi-naming.md) -- 🚨 A private package is `0.0.0` and unscoped `local-`, never path-derived; the versioned repo ROOT is exempt - its version is a non-npm channel's release version plus the workspace versionSource. (`.claude/hooks/fleet/private-package-name-guard/`) (`scripts/fleet/check/private-packages-are-unpublishable.mts`) [`private-package-identity`](docs/agents.md/fleet/private-package-identity.md) +- 🚨 A private package is `0.0.0` and unscoped `local-`, never path-derived. (`.claude/hooks/fleet/private-package-name-guard/`) (`scripts/fleet/check/private-packages-are-unpublishable.mts`) [`private-package-identity`](docs/agents.md/fleet/private-package-identity.md) - 🚨 Every `release.publishedPackages` entry must be non-private and the set carries ONE version: npm SKIPS a private package while the release stays green. (`scripts/fleet/check/published-packages-are-release-ready.mts`) [`private-package-identity`](docs/agents.md/fleet/private-package-identity.md) - 🚨 External refs pin the SHA and comment the label (` # v3.2.1`; branch pins ` # main `); integrity is verified on download AND extract with `sha256:` hashes. (`scripts/fleet/check/external-refs-carry-sha-and-label.mts`) [`immutable-references`](docs/agents.md/fleet/immutable-references.md) - 🚨 Workflows/skills/scripts invoking `claude` CLI or the Claude Agent SDK MUST set all four lockdown flags; `permissionMode` must be `dontAsk`/`acceptEdits`/`plan`, never a permissive default. [`locking-down-claude`](docs/agents.md/fleet/locking-down-claude.md) -- 🚨 **`pnpm`, from the repo root**: no `npx`/`dlx`, `--experimental-strip-types`, `tsx`/`ts-node`, `cd && pnpm`, or `corepack`. [`tooling`](docs/agents.md/fleet/tooling.md) [`database`](docs/agents.md/fleet/database.md) +- 🚨 **`pnpm`, from the repo root**: no `npx`/`dlx`, `--experimental-strip-types`, `tsx`/`ts-node`, `cd && pnpm`, or `corepack`. [`tooling`](docs/agents.md/fleet/tooling.md) [`database`](docs/agents.md/fleet/database.md) (`.claude/hooks/fleet/corepack-guard/`) +- 🚨 Reach for the repo SCRIPT, never the raw tool - and never hand the operator a raw command a script wraps (`pnpm run gh:auth login`, not `gh auth login`). (`.claude/hooks/fleet/prefer-script-emission-guard/`) - zsh does not word-split `$var`: a space-joined list in a variable passes as ONE arg; pass lists via `$(cat f)` / `${=var}` / xargs. [`tooling`](docs/agents.md/fleet/tooling.md) - 🚨 rg's `-r` never clusters: `rg -rln` parses as `--replace 'ln'` and corrupts output; spell `-r` separately. [`tooling`](docs/agents.md/fleet/tooling.md) - 🚨 7-day `minimumReleaseAge` soak, every ecosystem (manifest+lock+gate). [`multi-ecosystem-soak`](docs/agents.md/fleet/multi-ecosystem-soak.md) [`tooling`](docs/agents.md/fleet/tooling.md) [`prompt-injection`](docs/agents.md/fleet/prompt-injection.md) - 🚨 Never silently phone home: every dep + external tool is telemetry-OFF, fail-closed; any new telemetry/analytics SDK must pass `check --all` gate. [`telemetry-lockdown`](docs/agents.md/fleet/telemetry-lockdown.md) - 🚨 The sfw CA is a PERSISTENT per-user pair (`pnpm run setup:sfw-ca`), never sfw's per-invocation tmpdir CA. An ephemeral CA can't enter an OS trust store, so pnpm's Rust tarball fetcher / cargo / uv / go fail `UnknownIssuer` on any uncached download. [`sfw-persistent-ca`](docs/agents.md/fleet/sfw-persistent-ca.md) - 🚨 Dedup the install tree: no avoidable cross-major duplicate, and every `@socketregistry/*` hardened drop-in is redirected via `overrides:`. [`tooling`](docs/agents.md/fleet/tooling.md) -- 🚨 An override's value is MEASURED, never predicted (`scripts/fleet/measure-ecosystem-impact.mts`): report surviving gateways + the clique verdict beside every cut %, and the root set with every number; a clique never prunes like a tree. [`ecosystem-impact-measurement`](docs/agents.md/fleet/ecosystem-impact-measurement.md) +- 🚨 An override's value is MEASURED, never predicted (`scripts/fleet/measure-ecosystem-impact.mts`): report surviving gateways + the clique verdict beside every cut %, and the root set with every number. [`ecosystem-impact-measurement`](docs/agents.md/fleet/ecosystem-impact-measurement.md) - 🚨 `pnpm run fix --all` runs the fleet doctor: auto-fixes missing `catalog:` entries, reports soak-window install failures loud. [`fleet-doctor`](docs/agents.md/fleet/fleet-doctor.md) -- **headroom-ai** (telemetry-locked) wire proxy compresses tool_result, the sole compression layer (no custom hook). [`token-minification`](docs/agents.md/fleet/token-minification.md) - 🚨 A peer agent's number or verdict is a LEAD: re-measure it, or attribute it; never restate it as your own finding. (`.claude/hooks/fleet/stop-claim-verify-nudge/`) [`a-peers-claim-is-a-lead`](docs/agents.md/fleet/a-peers-claim-is-a-lead.md) - 🚨 Fix a lint/type/test error or broken comment in your reading window in a sibling commit; investigate before blaming a tool or session. [`judgment-and-self-evaluation`](docs/agents.md/fleet/judgment-and-self-evaluation.md) - 🚨 "stop"/"pause" means stop FORWARD action: finish the in-flight commit, never interrupt a running one, never freeze in a broken state. (`.claude/hooks/fleet/stop-means-commit-guard/`) [`stop-means-finish-the-commit`](docs/agents.md/fleet/stop-means-finish-the-commit.md) - 🚨 Scope work into chunks that land: each verifiable alone, committed before the next starts; a mechanical sweep is batched, not one pass. (`.claude/hooks/fleet/uncommitted-sweep-nudge/`) [`scope-work-into-landable-chunks`](docs/agents.md/fleet/scope-work-into-landable-chunks.md) - 🚨 Finish a change, then commit it; never end a turn with a dirty worktree. [`worktree-hygiene`](docs/agents.md/fleet/worktree-hygiene.md) - 🚨 Smallest chunks, land ASAP; never checkout/switch mid-queue; a local fast-forward isn't landed until pushed. [`worktree-hygiene`](docs/agents.md/fleet/worktree-hygiene.md) -- 🚨 Before reaching for a revert (git checkout/restore/reset to discard work), try fix forward — edit the file to the desired state instead. (`scripts/fleet/whose-work.mts`, `no-revert-guard`) [`fix-forward-not-revert`](docs/agents.md/fleet/fix-forward-not-revert.md) +- 🚨 Before reaching for a revert (git checkout/restore/reset to discard work), try fix forward - edit the file to the desired state instead. (`scripts/fleet/whose-work.mts`, `no-revert-guard`) [`fix-forward-not-revert`](docs/agents.md/fleet/fix-forward-not-revert.md) - 🚨 Land often; `auto-land-on-stop` groups this session's own-work into signed commits on local main at turn-end. [`parallel-claude-sessions`](docs/agents.md/fleet/parallel-claude-sessions.md) -- 🚨 Before deleting a branch as redundant, verify its content is contained in the kept branch - a squash can silently drop work; stale worktrees and merged branches pile up, so the sweep nudge makes cleanup a decision. (`.claude/hooks/fleet/branch-worktree-sweep-nudge/`) [`worktree-hygiene`](docs/agents.md/fleet/worktree-hygiene.md) +- 🚨 Before deleting a branch as redundant, verify its content is contained in the kept branch - a squash can silently drop work. (`.claude/hooks/fleet/branch-worktree-sweep-nudge/`) [`worktree-hygiene`](docs/agents.md/fleet/worktree-hygiene.md) - 🚨 Never use a push or CI as the error-discovery loop: `pnpm run preflight` runs every gate stage locally in ONE pass, and a `template/` edit is unverifiable until it cascades. (`scripts/fleet/preflight.mts`) [`preflight-before-the-gate`](docs/agents.md/fleet/preflight-before-the-gate.md) - 🚨 Never name leftover work and drop it: fix it, or leave an explicit `Follow-up:` / `- [ ]` handle - the next session is almost always this one. (`.claude/hooks/fleet/deferred-residue-guard/`) [`no-deferred-residue`](docs/agents.md/fleet/no-deferred-residue.md) - 🚨 Push to origin main only behind the full pre-push gate, then monitor CI to green. [`push-policy`](docs/agents.md/fleet/push-policy.md) @@ -62,6 +62,7 @@ - 🚨 Bypassing a hook needs the user to type `Allow bypass` verbatim; the `bypass` word is optional only for low-risk guards. [`bypass-phrases`](docs/agents.md/fleet/bypass-phrases.md) - 🚨 Closing a High/Critical finding requires searching the repo for the same shape before marking it done. [`agent-delegation`](docs/agents.md/fleet/agent-delegation.md) [`tooling`](docs/agents.md/fleet/tooling.md) - 🚨 A Workflow `agent()` subagent has no Task tools; inline the full spec, the orchestrator does the bookkeeping. [`agent-delegation`](docs/agents.md/fleet/agent-delegation.md) +- Each assistant/subagent picks a team alias; the orchestrator (primary session) alone awards ⭐ for notable judgment calls, tracked in a dated ledger. Don't be chatty - one line at session close, only when something was star-worthy. [`team-stars`](docs/agents.md/fleet/team-stars.md) - A background Workflow, Agent, or Bash task silent past 2 minutes may be thrashing; verify it's progressing or stop it. [`long-running-tasks`](docs/agents.md/fleet/long-running-tasks.md) - 🚨 `git clone` must include both `--depth=1` and `--single-branch`; a bare clone missing either is blocked. [`tooling`](docs/agents.md/fleet/tooling.md) - 🚨 Inside an untrusted repo, resolution is the attack surface; sanitize PATH and apply git hygiene flags to every spawn. [`untrusted-cwd`](docs/agents.md/fleet/untrusted-cwd.md) @@ -75,9 +76,9 @@ - 🚨 A `claude-fable-5` spawn must check `result.refused`/`result.servedByFallback` and must never set a thinking budget. [`fable-fallback`](docs/agents.md/fleet/fable-fallback.md) - 🚨 Non-trivial build/design work routes through `delegating-execution`: big-brain plan, floor execute, big-brain review, floor follow-up. [`delegating-execution`](docs/agents.md/fleet/delegating-execution.md) - Named on-demand sync: "cascade ``" = one slice, "dogfood ``" = self-sync, "cascade `` to ``" = one member. [`vocabulary`](docs/agents.md/fleet/vocabulary.md) -- 🚨 Every fleet member is THIN: untrack the wholly-fleet payload, fetch it from the release bundle; keep hybrid files + the dep-0 fetcher tracked, never bundled. Only the wheelhouse, the bundle's producer, is fat. [`fleet-pack-distribution`](docs/agents.md/fleet/fleet-pack-distribution.md) +- 🚨 Every fleet member is THIN: untrack the wholly-fleet payload, fetch it from the release bundle. [`fleet-pack-distribution`](docs/agents.md/fleet/fleet-pack-distribution.md) - 🚨 Drift across fleet repos is a defect: when two repos pin different versions of a resource, opt for the latest. [`drift-watch`](docs/agents.md/fleet/drift-watch.md) -- 🚨 A Socket-published pin NEVER moves down; fix the regressed package upstream. The only sanctioned lower pin is a `FLEET_CATALOG_HOLDS` entry, which must cascade in the same wave. (`scripts/fleet/check/socket-pins-are-never-lowered.mts`) [`drift-watch`](docs/agents.md/fleet/drift-watch.md) +- 🚨 A Socket-published pin NEVER moves down. (`scripts/fleet/check/socket-pins-are-never-lowered.mts`) [`drift-watch`](docs/agents.md/fleet/drift-watch.md) - 🚨 Port an upstream at its LATEST release: `git fetch --tags`, pin NEWEST before a `.gitmodules`/`lockstep.json` version-pin change. [`lockstep`](docs/agents.md/fleet/lockstep.md) [`drift-watch`](docs/agents.md/fleet/drift-watch.md) - 🚨 Local-only cascade commits + superseded worktrees silently block future pushes; cleanup runs automatically at the start of every cascade wave. [`stranded-cascades`](docs/agents.md/fleet/stranded-cascades.md) - 🚨 Edit fleet-canonical files ONLY in `template/...`. [`no-local-fork`](docs/agents.md/fleet/no-local-fork.md) @@ -99,6 +100,9 @@ - 🚨 Fleet `socket/*` doctrine (no-status-emoji, personal-path-placeholders, max-file-lines) is enforced across Rust/Go/C++ source by one scanner. [`lint-parity-across-languages`](docs/agents.md/fleet/lint-parity-across-languages.md) - 🚨 Match the microarch pin to who controls the target: portable-by-default via runtime CPU dispatch. (`scripts/fleet/check/build-microarch-is-portable.mts`) [`portable-microarch`](docs/agents.md/fleet/portable-microarch.md) - 🚨 Docs alone don't enforce: every rule spans document + hook + lint rule + script; shared logic DRY'd into `_shared/` libs. [`code-is-law`](docs/agents.md/fleet/code-is-law.md) [`gated-extension-point`](docs/agents.md/fleet/gated-extension-point.md) +- 🚨 Search for the existing enforcer before writing one: a doctrine usually already names a check, hook, or predicate, and the failure is that it sits inert or unwired, not absent. (`scripts/fleet/check/hooks-have-no-guard-nudge-overlap.mts`) [`code-is-law`](docs/agents.md/fleet/code-is-law.md) +- 🚨 A feature is not done until it has: code-as-law check script, unit/integration/e2e tests, preflight wiring, and maintains 90%+ coverage. [`feature-completeness`](docs/agents.md/fleet/feature-completeness.md) +- 🚨 An AI agent acts ONLY through fleet scripts/hooks/skills (code is law). (`scripts/fleet/check/working-tree-is-clean.mts`) [`agent-actions-via-scripts`](docs/agents.md/fleet/agent-actions-via-scripts.md) - Fleet-wide data (rosters, pins, pricing) lives in ONE canonical file; consumers derive, never hand-maintain a copy. [`single-source-of-truth`](docs/agents.md/fleet/single-source-of-truth.md) - 🚨 Per-repo config lives in ONE member surface: a new `.config/*.{json,yaml,toml}` is blocked; add a section to `.config/repo/socket-wheelhouse.json` instead. [`config-segregation`](docs/agents.md/fleet/config-segregation.md) - 🚨 One `.gitignore` per repo: every ignore entry lives in the ROOT `.gitignore` (fleet block + repo-owned block). [`single-gitignore`](docs/agents.md/fleet/single-gitignore.md) @@ -106,13 +110,14 @@ - 🚨 `/* c8 ignore next N */` is broken for multi-line bodies: use `/* c8 ignore start - */` … `/* c8 ignore stop */`; single-line `next` is fine. [`c8-ignore-directives`](docs/agents.md/fleet/c8-ignore-directives.md) - 🚨 A repo declaring a language capability (cargo/go/cpp) gets that lane in `pnpm run cover` automatically, and NO lane may report success while measuring nothing (tool-absent = explicit skip; ran-but-zero = exit 1). (`scripts/fleet/check/coverage-lanes-are-wired.mts`) [`coverage-lanes`](docs/agents.md/fleet/coverage-lanes.md) - 🚨 New features ship covered and the gains LOCK: a Cover threshold trails measured coverage by at most 1.5 points and never moves down; `--fix` ratchets it. (`scripts/fleet/check/coverage-thresholds-are-ratcheted.mts`) [`coverage-ratchet`](docs/agents.md/fleet/coverage-ratchet.md) +- When idle or lacking tasks, increasing coverage toward 90%+ is the default pickup. [`feature-completeness`](docs/agents.md/fleet/feature-completeness.md) - 🚨 A path is constructed exactly once; each package's own `paths.mts` is the canonical owner, inherited via `export *`. [`path-hygiene`](docs/agents.md/fleet/path-hygiene.md) - External-spec-conformance runners use a canonical 4-tier layout; the allowlist lives in a separate config file, never inline. [`conformance-runners`](docs/agents.md/fleet/conformance-runners.md) - A conformance gate for an upstream reimplementation reuses the upstream's OWN test suite via a shim and runs COPIES of the needed test files from an `os.tmpdir()` scratch dir, never in the pinned `upstream/` tree. [`lockstep`](docs/agents.md/fleet/lockstep.md) -- 🚨 Repo-root `upstream/` is the ONLY submodule home, build source or test corpus alike, never `packages/*/upstream/*` or `test/fixtures/*`; shallow single-branch (`shallow = true` + `branch`), `ref`/`sha256:` pin via `gen/gitmodules-hash --set`. (`scripts/fleet/check/submodules-are-rooted-in-upstream.mts`) [`upstream-references`](docs/agents.md/fleet/upstream-references.md) +- 🚨 Repo-root `upstream/` is the ONLY submodule home, build source or test corpus alike, never `packages/*/upstream/*` or `test/fixtures/*`. (`scripts/fleet/check/submodules-are-rooted-in-upstream.mts`) [`upstream-references`](docs/agents.md/fleet/upstream-references.md) - 🚨 Never git-track an `upstream/` gitlink; upstream references are `.gitmodules`-only, and the `ref`+`sha256:` there ARE the pin. [`upstream-references`](docs/agents.md/fleet/upstream-references.md) - 🚨 A copyleft upstream (AGPL/GPL) is RUN and OBSERVED via its own tests only; never read or derive from its implementation. [`copyleft-boundaries`](docs/agents.md/fleet/copyleft-boundaries.md) -- 🚨 Normalize a path-like variable with `normalizePath`/`toUnixPath` before any separator-sensitive op (regex match, `.split('/')`, `.startsWith('/')`, `.includes('/')`). [`normalize-path-before-match`](docs/agents.md/fleet/normalize-path-before-match.md) +- 🚨 Normalize a path-like variable with `normalizePath`/`toUnixPath` before any separator-sensitive op (regex match, `.split('/')`, `.startsWith('/')`, `.includes('/')`). [`paths-are-normalized-before-match-at-edit`](docs/agents.md/fleet/paths-are-normalized-before-match-at-edit.md) - Never `Bash(run_in_background: true)` for a test/build run or a `git commit`/`rebase`/`merge`/`cherry-pick`. [`no-live-network-in-tests`](docs/agents.md/fleet/no-live-network-in-tests.md) - 🚨 Tests are vitest via `pnpm test` / `pnpm test `; never `node --test`, never `--` before the path. [`test-layout`](docs/agents.md/fleet/test-layout.md) - 🚨 A committed test reference-output fixture is `*.golden.json`, never `*.expected.json`. [`golden-fixtures`](docs/agents.md/fleet/golden-fixtures.md) @@ -125,7 +130,7 @@ - 🚨 npm-family auth (npm/pnpm/yarn publish/login) uses BROWSER auth (`--auth-type=web`); NEVER pass or suggest `--otp=`. [`token-hygiene`](docs/agents.md/fleet/token-hygiene.md) - 🚨 Verify state before acting: read a resource's published state before any create/claim/publish (`npm view` / `gh release view`). (`.claude/hooks/fleet/verify-before-publish-guard/`) [`verify-state-before-acting`](docs/agents.md/fleet/verify-state-before-acting.md) - 🚨 Publish through the pipeline, never locally: no `npm|pnpm publish` / `pnpm stage publish` / `cargo publish` / direct `npm-publish.mts` runs. [`version-bumps`](docs/agents.md/fleet/version-bumps.md) -- 🚨 ONE npm upload invocation fleet-wide (`registry-infra/npm/publish-command.mts`); no npm token ever reaches CI, `direct` is only ever a LOCAL `0.0.0` name reservation, and a `Skipped OIDC` run that exits 0 still fails. (`scripts/fleet/check/publish-entrypoints-are-fleet-composed.mts`) [`trusted-publishing-posture`](docs/agents.md/fleet/trusted-publishing-posture.md) +- 🚨 ONE npm upload invocation fleet-wide (`registry-infra/npm/publish-command.mts`). (`scripts/fleet/check/publish-entrypoints-are-fleet-composed.mts`) [`trusted-publishing-posture`](docs/agents.md/fleet/trusted-publishing-posture.md) - 🚨 npm sits behind bot management: reuse the seeded session, and PAUSE a human-verification challenge for the operator via `runChallengeAware`; never blind-retry into a rate limit. [`npm-anti-bot-rhythm`](docs/agents.md/fleet/npm-anti-bot-rhythm.md) - 🚨 Validate what SHIPS, not the source tree: the packed tarball's bytes (closed entry allowlist, regular files only, no `..`/backslash entries, bin exec bits) plus a leak scan of packed AND decompressed bytes. [`artifact-hygiene`](docs/agents.md/fleet/artifact-hygiene.md) - 🚨 A `github-action` member ships the committed `dist/` at a tag: only rebuild-and-diff proves currency (git ancestry proves staleness alone), and a floating `v` alias either tracks its line's newest release or does not exist. (`scripts/fleet/check/github-action-aliases-are-not-frozen.mts`) [`github-action-release-contract`](docs/agents.md/fleet/github-action-release-contract.md) @@ -133,6 +138,7 @@ - 🚨 Commits on `main`/`master` must be signed. [`commit-signing`](docs/agents.md/fleet/commit-signing.md) [`git-config-write-guard`](docs/agents.md/fleet/git-config-write-guard.md) [`security-stack`](docs/agents.md/fleet/security-stack.md) - Skills/commands/agent-instruction docs are THIN wrappers; defer heavy lifting to a backing `.mts`. [`agents-and-skills`](docs/agents.md/fleet/agents-and-skills.md) [`agent-delegation`](docs/agents.md/fleet/agent-delegation.md) [`security-stack`](docs/agents.md/fleet/security-stack.md) - Fleet/repo segmentation on every surface: hooks `{fleet,repo}//`, actions `.github/actions/{fleet,repo}//`; a `-guard` BLOCKS, a `-nudge` NUDGES. [`hook-registry`](docs/agents.md/fleet/hook-registry.md) +- 🚨 Guard output is pithy: silent on pass, nudges one line, blocks ≤3 lines + bypass; tests assert error type/code, never exact messages. (`scripts/fleet/check/guard-blocks-are-pithy.mts`, `socket/no-error-message-assertions`) [`quiet-guards`](docs/agents.md/fleet/quiet-guards.md) - 🚨 npm-run-all2 is REMOVED; order-independent script groups use pnpm's regexp form (`pnpm run "/^lint:/"`). [`script-aggregation`](docs/agents.md/fleet/script-aggregation.md) - Stale GitHub Actions run history is pruned weekly by `scripts/fleet/prune-workflow-runs.mts`; never mass-delete by hand. [`workflow-run-retention`](docs/agents.md/fleet/workflow-run-retention.md) - 🚨 Actions cache over 10 GB silently LRU-evicts itself (green CI, cold rebuilds); `scripts/fleet/prune-actions-caches.mts` holds it under 8 GB weekly. [`workflow-run-retention`](docs/agents.md/fleet/workflow-run-retention.md) diff --git a/package.json b/package.json index a0316f9ff..5ff9a8955 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "check": "node scripts/fleet/check.mts", "check:all": "node scripts/fleet/check.mts --all", "check:paths": "node scripts/fleet/check/paths-are-canonical.mts", - "ci:local": "node scripts/fleet/agent-ci-skip-locks.mts run --all --quiet --pause-on-failure --github-token", + "ci:local": "node scripts/fleet/local-ci-skip-locks.mts run --all --quiet --pause-on-failure --github-token", "claude": "pnpm --filter @socketsecurity/cli run claude --", "clean": "node scripts/fleet/clean.mts", "clean:cache": "node scripts/repo/clean-cache.mts", @@ -94,7 +94,8 @@ "gh:auth": "node scripts/fleet/gh-auth.mts", "offload:model": "node scripts/fleet/offload-model.mts", "setup:fireconnect": "node scripts/fleet/setup/fireconnect.mts", - "setup:offload-providers": "node scripts/fleet/setup/offload-providers.mts" + "setup:offload-providers": "node scripts/fleet/setup/offload-providers.mts", + "backup": "node scripts/fleet/backup-work.mts" }, "devDependencies": { "@anthropic-ai/claude-code": "catalog:", @@ -119,7 +120,6 @@ "@pnpm/lockfile.detect-dep-types": "catalog:", "@pnpm/lockfile.fs": "catalog:", "@pnpm/logger": "catalog:", - "@redwoodjs/agent-ci": "catalog:", "@shadscan/cli": "catalog:", "@sinclair/typebox": "catalog:", "@socketregistry/hyrious__bun.lockb": "catalog:", @@ -149,6 +149,7 @@ "@types/which": "catalog:", "@types/yargs-parser": "catalog:", "@typescript/native-preview": "catalog:", + "@ultrathink/acorn.rs.wasm": "catalog:", "@vitest/coverage-v8": "catalog:", "@vitest/ui": "catalog:", "browserslist": "catalog:", @@ -191,6 +192,7 @@ "postject": "catalog:", "regjsparser": "catalog:", "rolldown": "catalog:", + "run-local-ci": "catalog:", "semver": "catalog:", "shell-quote": "catalog:", "ssri": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ec039364..bd63fc179 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,12 +286,9 @@ catalogs: '@pnpm/logger': specifier: 1100.0.0 version: 1100.0.0 - '@redwoodjs/agent-ci': - specifier: 0.18.0 - version: 0.18.0 '@shadscan/cli': - specifier: 0.11.0 - version: 0.11.0 + specifier: 0.16.0 + version: 0.16.0 '@socketregistry/hyrious__bun.lockb': specifier: 1.0.19 version: 1.0.19 @@ -380,8 +377,8 @@ catalogs: specifier: 1.0.2 version: 1.0.2 chrome-devtools-mcp: - specifier: 1.6.0 - version: 1.6.0 + specifier: 1.7.0 + version: 1.7.0 cmd-shim: specifier: 7.0.0 version: 7.0.0 @@ -458,11 +455,11 @@ catalogs: specifier: 10.2.0 version: 10.2.0 oxfmt: - specifier: 0.62.0 - version: 0.62.0 + specifier: 0.63.0 + version: 0.63.0 oxlint: - specifier: 1.77.0 - version: 1.77.0 + specifier: 1.78.0 + version: 1.78.0 oxlint-tsgolint: specifier: 7.0.2001 version: 7.0.2001 @@ -481,6 +478,9 @@ catalogs: regjsparser: specifier: 0.13.2 version: 0.13.2 + run-local-ci: + specifier: 0.18.1 + version: 0.18.1 ssri: specifier: 12.0.0 version: 12.0.0 @@ -491,8 +491,8 @@ catalogs: specifier: 3.2.0 version: 3.2.0 taze: - specifier: 19.17.1 - version: 19.17.1 + specifier: 20.0.1 + version: 20.0.1 terminal-link: specifier: 2.1.1 version: 2.1.1 @@ -554,7 +554,7 @@ overrides: ssri@>=12: 13.0.1 string-width@>=5: 8.2.2 tinyexec: 1.3.0 - typebox: 1.3.11 + typebox: 1.3.13 undici@<6: 6.28.0 update-notifier@>=4.0.0: 7.3.1 uuid: 11.1.1 @@ -575,7 +575,7 @@ overrides: markdown-it: 14.3.0 picomatch: 4.0.5 protobufjs: 7.6.5 - rolldown: 1.2.3 + rolldown: 1.2.4 sharp: 0.35.3 string-width: 8.2.2 strip-ansi: 7.2.0 @@ -711,12 +711,9 @@ importers: '@pnpm/logger': specifier: 'catalog:' version: 1100.0.0 - '@redwoodjs/agent-ci': - specifier: 'catalog:' - version: 0.18.0(supports-color@7.2.0) '@shadscan/cli': specifier: 'catalog:' - version: 0.11.0 + version: 0.16.0 '@sinclair/typebox': specifier: 0.34.52 version: 0.34.52 @@ -801,6 +798,9 @@ importers: '@typescript/native-preview': specifier: 'catalog:' version: 7.0.0-dev.20260510.1 + '@ultrathink/acorn.rs.wasm': + specifier: 'catalog:' + version: 0.1.1 '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -818,7 +818,7 @@ importers: version: 1.0.2 chrome-devtools-mcp: specifier: 'catalog:' - version: 1.6.0 + version: 1.7.0 cmd-shim: specifier: 'catalog:' version: 7.0.0 @@ -902,10 +902,10 @@ importers: version: 10.2.0 oxfmt: specifier: 'catalog:' - version: 0.62.0 + version: 0.63.0 oxlint: specifier: 'catalog:' - version: 1.77.0(oxlint-tsgolint@7.0.2001) + version: 1.78.0(oxlint-tsgolint@7.0.2001) oxlint-tsgolint: specifier: 'catalog:' version: 7.0.2001 @@ -925,8 +925,11 @@ importers: specifier: 'catalog:' version: 0.13.2 rolldown: - specifier: 1.2.3 - version: 1.2.3 + specifier: 1.2.4 + version: 1.2.4 + run-local-ci: + specifier: 'catalog:' + version: 0.18.1(supports-color@7.2.0) semver: specifier: 7.8.5 version: 7.8.5 @@ -941,7 +944,7 @@ importers: version: 4.0.2 taze: specifier: 'catalog:' - version: 19.17.1 + version: 20.0.1 terminal-link: specifier: 'catalog:' version: 2.1.1 @@ -949,8 +952,8 @@ importers: specifier: 'catalog:' version: 10.1.1 typebox: - specifier: 1.3.11 - version: 1.3.11 + specifier: 1.3.13 + version: 1.3.13 typescript: specifier: 'catalog:' version: 7.0.2 @@ -973,6 +976,12 @@ importers: specifier: 'catalog:' version: 4.4.3 + .claude/hooks/fleet/account-snapshot-recorder: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/actionlint-on-workflow-edit: dependencies: '@socketsecurity/lib-stable': @@ -989,11 +998,7 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/agents-skills-mirror-nudge: - dependencies: - '@socketsecurity/lib-stable': - specifier: npm:@socketsecurity/lib@6.7.0 - version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + .claude/hooks/fleet/agent-prompt-budget-guard: devDependencies: '@types/node': specifier: 'catalog:' @@ -1077,7 +1082,7 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/brew-supply-chain-guard: + .claude/hooks/fleet/brew-supply-chain-is-hardened-at-edit: devDependencies: '@types/node': specifier: 'catalog:' @@ -1161,15 +1166,6 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/cdn-allowlist-guard: - devDependencies: - '@socketsecurity/lib-stable': - specifier: npm:@socketsecurity/lib@6.7.0 - version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' - '@types/node': - specifier: 'catalog:' - version: 26.2.0 - .claude/hooks/fleet/changelog-entry-shape-nudge: dependencies: '@socketsecurity/lib-stable': @@ -1263,6 +1259,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/clipboard-snippet-nudge: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/clone-reviewed-repo-nudge: devDependencies: '@socketsecurity/lib-stable': @@ -1292,6 +1294,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/codify-footgun-nudge: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/commit-author-guard: dependencies: '@socketsecurity/lib-stable': @@ -1351,6 +1359,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/copy-on-select-hint-nudge: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/corrupt-rebase-guard: dependencies: '@socketsecurity/lib-stable': @@ -1371,17 +1385,13 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/dated-citation-guard: - dependencies: - '@socketsecurity/lib-stable': - specifier: npm:@socketsecurity/lib@6.7.0 - version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + .claude/hooks/fleet/default-branch-guard: devDependencies: '@types/node': specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/default-branch-guard: + .claude/hooks/fleet/defer-to-script-nudge: devDependencies: '@types/node': specifier: 'catalog:' @@ -1428,6 +1438,10 @@ importers: version: 26.2.0 .claude/hooks/fleet/disowned-dirt-guard: + dependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' devDependencies: '@types/node': specifier: 'catalog:' @@ -1480,7 +1494,17 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/error-message-quality-nudge: + .claude/hooks/fleet/env-kill-switches-are-absent-at-edit: + dependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + + .claude/hooks/fleet/error-messages-are-thorough-at-edit: dependencies: '@ultrathink/acorn.rs.wasm': specifier: 'catalog:' @@ -1496,6 +1520,15 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/fetch-allowlist-is-respected-at-edit: + devDependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/file-size-nudge: dependencies: '@socketsecurity/lib-stable': @@ -1561,13 +1594,19 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/gitignore-is-single-file-at-edit: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/gitmodules-comment-guard: dependencies: '@socketsecurity/lib-stable': specifier: npm:@socketsecurity/lib@6.7.0 version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' - .claude/hooks/fleet/golden-fixture-naming-guard: + .claude/hooks/fleet/golden-fixtures-are-named-golden-at-edit: devDependencies: '@types/node': specifier: 'catalog:' @@ -1579,11 +1618,21 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/headroom-proxy-start: + .claude/hooks/fleet/handoff-request-guard: dependencies: '@socketsecurity/lib-stable': specifier: npm:@socketsecurity/lib@6.7.0 version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + + .claude/hooks/fleet/history-rewrite-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 .claude/hooks/fleet/honeypot-echo-guard: dependencies: @@ -1646,6 +1695,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/link-protocol-dep-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/lock-step-ref-nudge: dependencies: '@socketsecurity/lib-stable': @@ -1666,7 +1721,7 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/markdown-filename-guard: + .claude/hooks/fleet/markdown-filenames-are-canonical-at-edit: dependencies: '@socketsecurity/lib-stable': specifier: npm:@socketsecurity/lib@6.7.0 @@ -1689,13 +1744,19 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/memories-are-codified-at-edit: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/memory-codify-nudge: devDependencies: '@types/node': specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/memory-enforcement-stamp-guard: + .claude/hooks/fleet/memory-discovery-nudge: devDependencies: '@types/node': specifier: 'catalog:' @@ -1721,6 +1782,18 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/model-policy-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + + .claude/hooks/fleet/model-spawn-policy-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/module-noun-name-guard: dependencies: '@socketsecurity/lib-stable': @@ -1741,6 +1814,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/no-amend-foreign-commit-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/no-blanket-file-exclusion-guard: devDependencies: '@types/node': @@ -1779,6 +1858,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/no-clipboard-access-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/no-copyleft-source-read: dependencies: '@socketsecurity/lib-stable': @@ -1850,16 +1935,6 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/no-env-kill-switch-guard: - dependencies: - '@socketsecurity/lib-stable': - specifier: npm:@socketsecurity/lib@6.7.0 - version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 26.2.0 - .claude/hooks/fleet/no-ext-issue-ref-guard: dependencies: '@socketsecurity/lib-stable': @@ -1928,6 +2003,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/no-ignoring-tracked-file-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/no-meta-comments-guard: dependencies: '@ultrathink/acorn.rs.wasm': @@ -2066,16 +2147,6 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/no-private-path-in-source-guard: - dependencies: - '@ultrathink/acorn.rs.wasm': - specifier: 'catalog:' - version: 0.1.1 - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 26.2.0 - .claude/hooks/fleet/no-private-ref-in-tests-docs-guard: dependencies: '@socketsecurity/lib-stable': @@ -2120,6 +2191,10 @@ importers: version: 26.2.0 .claude/hooks/fleet/no-removal-comment-nudge: + dependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' devDependencies: '@types/node': specifier: 'catalog:' @@ -2148,6 +2223,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/no-screenshot-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/no-self-referential-symlink-guard: dependencies: '@socketsecurity/lib-stable': @@ -2158,6 +2239,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/no-shell-injection-bypass-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/no-strip-types-guard: dependencies: shell-quote: @@ -2232,17 +2319,13 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/no-unmocked-net-guard: - dependencies: - '@socketsecurity/lib-stable': - specifier: npm:@socketsecurity/lib@6.7.0 - version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + .claude/hooks/fleet/no-unmocked-ai-guard: devDependencies: '@types/node': specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/no-upstream-gitlink-guard: + .claude/hooks/fleet/no-unmocked-net-guard: dependencies: '@socketsecurity/lib-stable': specifier: npm:@socketsecurity/lib@6.7.0 @@ -2272,6 +2355,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/no-vitest-double-dash-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/no-wheelhouse-pr-guard: devDependencies: '@types/node': @@ -2290,6 +2379,26 @@ importers: specifier: 1.10.0 version: 1.10.0 + .claude/hooks/fleet/notion-backup-nudge: + dependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + + .claude/hooks/fleet/notion-replace-content-guard: + dependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/npm-2fa-needs-pty-guard: devDependencies: '@types/node': @@ -2316,6 +2425,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/operate-from-repo-root-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/options-param-naming-guard: dependencies: '@ultrathink/acorn.rs.wasm': @@ -2354,7 +2469,7 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/package-manager-auto-update-guard: + .claude/hooks/fleet/package-manager-auto-update-is-disabled-at-edit: devDependencies: '@socketsecurity/lib-stable': specifier: npm:@socketsecurity/lib@6.7.0 @@ -2381,6 +2496,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/parallel-agent-spawn-nudge: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/parallel-agent-staging-guard: dependencies: shell-quote: @@ -2445,6 +2566,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/playwright-launch-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/pnpm-filter-zero-match-nudge: devDependencies: '@types/node': @@ -2470,6 +2597,40 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/pr-body-style-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + + .claude/hooks/fleet/pr-comment-brevity-guard: + devDependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + + .claude/hooks/fleet/pr-comment-shape-nudge: + devDependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + + .claude/hooks/fleet/pr-merge-conflict-guard: + dependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/pr-vs-push-default-nudge: dependencies: '@socketsecurity/lib-stable': @@ -2502,6 +2663,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/prefer-fff-search-nudge: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/prefer-fn-decl-guard: dependencies: '@socketsecurity/lib-stable': @@ -2512,6 +2679,16 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/prefer-inline-small-dependency-nudge: + dependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/prefer-json-clone-guard: dependencies: '@ultrathink/acorn.rs.wasm': @@ -2583,6 +2760,16 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/private-paths-are-absent-at-edit: + dependencies: + '@ultrathink/acorn.rs.wasm': + specifier: 'catalog:' + version: 0.1.1 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/proc-environ-exfil-guard: dependencies: '@socketsecurity/lib-stable': @@ -2713,6 +2900,16 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/rule-citations-are-generic-at-edit: + dependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/rust-target-sweep-nudge: dependencies: '@socketsecurity/lib-stable': @@ -2847,6 +3044,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/snapshot-hostile-require-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/soak-exclude-date-guard: dependencies: '@socketsecurity/lib-stable': @@ -2873,6 +3076,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/spend-warning-nudge: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/squash-freeze-boundary-guard: devDependencies: '@types/node': @@ -2889,6 +3098,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/stale-node-modules-nudge: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/stale-process-sweeper: dependencies: '@socketsecurity/lib-stable': @@ -2921,6 +3136,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/synthesized-script-edit-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/target-arch-env-guard: dependencies: '@socketsecurity/lib-stable': @@ -3029,6 +3250,16 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/upstream-gitlinks-are-absent-at-edit: + dependencies: + '@socketsecurity/lib-stable': + specifier: npm:@socketsecurity/lib@6.7.0 + version: '@socketsecurity/lib@6.7.0(patch_hash=6013bc5930e8dc12861356e4d5a6affc9c860745c8c0cfdabb4ec9fc635009af)(typescript@7.0.2)' + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/upstream-is-read-only-guard: devDependencies: '@types/node': @@ -3059,6 +3290,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/verify-before-publish-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/verify-render-pre-commit-nudge: dependencies: '@socketsecurity/lib-stable': @@ -3092,6 +3329,18 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/vscode-folder-open-task-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + + .claude/hooks/fleet/waiting-discipline-nudge: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/wheelhouse-drift-guard: dependencies: '@socketsecurity/lib-stable': @@ -3102,6 +3351,12 @@ importers: specifier: 'catalog:' version: 26.2.0 + .claude/hooks/fleet/workflow-agent-task-tools-nudge: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + .claude/hooks/fleet/workflow-multiline-body-guard: dependencies: '@socketsecurity/lib-stable': @@ -3112,7 +3367,7 @@ importers: specifier: 'catalog:' version: 26.2.0 - .claude/hooks/fleet/workflow-uses-comment-guard: + .claude/hooks/fleet/workflow-sha-pins-are-stamped-at-edit: dependencies: '@socketsecurity/lib-stable': specifier: npm:@socketsecurity/lib@6.7.0 @@ -3184,6 +3439,8 @@ importers: .config/fleet/oxlint-plugin/fleet/no-dynamic-import-outside-bundle: {} + .config/fleet/oxlint-plugin/fleet/no-error-message-assertions: {} + .config/fleet/oxlint-plugin/fleet/no-eslint-biome-config-ref: {} .config/fleet/oxlint-plugin/fleet/no-fetch-prefer-http-request: {} @@ -3192,6 +3449,10 @@ importers: .config/fleet/oxlint-plugin/fleet/no-fileoverview-prefer-file: {} + .config/fleet/oxlint-plugin/fleet/no-floating-async-map: {} + + .config/fleet/oxlint-plugin/fleet/no-force-delete: {} + .config/fleet/oxlint-plugin/fleet/no-handbuilt-file-url: {} .config/fleet/oxlint-plugin/fleet/no-inline-defer-async: {} @@ -3268,10 +3529,14 @@ importers: .config/fleet/oxlint-plugin/fleet/no-truncated-lint-disable-reason: {} + .config/fleet/oxlint-plugin/fleet/no-truthy-object-guard: {} + .config/fleet/oxlint-plugin/fleet/no-underscore-identifier: {} .config/fleet/oxlint-plugin/fleet/no-use-strict-in-esm: {} + .config/fleet/oxlint-plugin/fleet/no-vendored-alias-in-test: {} + .config/fleet/oxlint-plugin/fleet/no-vitest-empty-test: {} .config/fleet/oxlint-plugin/fleet/no-vitest-focused-tests: {} @@ -3284,14 +3549,14 @@ importers: .config/fleet/oxlint-plugin/fleet/no-which-for-local-bin: {} - .config/fleet/oxlint-plugin/fleet/normalize-path-before-match: {} - .config/fleet/oxlint-plugin/fleet/optional-explicit-undefined: {} .config/fleet/oxlint-plugin/fleet/options-null-proto: {} .config/fleet/oxlint-plugin/fleet/options-param-naming: {} + .config/fleet/oxlint-plugin/fleet/paths-are-normalized-before-match-at-edit: {} + .config/fleet/oxlint-plugin/fleet/personal-path-placeholders: {} .config/fleet/oxlint-plugin/fleet/prefer-all-settled: {} @@ -3368,6 +3633,8 @@ importers: .config/fleet/oxlint-plugin/fleet/require-vitest-globals-import: {} + .config/fleet/oxlint-plugin/fleet/require-void-run-hook: {} + .config/fleet/oxlint-plugin/fleet/socket-api-token-env: {} .config/fleet/oxlint-plugin/fleet/sort-array-literals: {} @@ -3574,8 +3841,8 @@ importers: specifier: 'catalog:' version: 10.2.0 rolldown: - specifier: 1.2.3 - version: 1.2.3 + specifier: 1.2.4 + version: 1.2.4 semver: specifier: 7.8.5 version: 7.8.5 @@ -4305,127 +4572,127 @@ packages: resolution: {integrity: sha512-dcuqqqU1Cpm9CLGEkCkpT/cpJ6H6a+hs0rP+iD8Tgwb+CPPZtX/hCfdIrqYyZ2RtYLxgc3S6KqC81AZAwEUPew==} engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} - '@oxc-project/types@0.143.0': - resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} + '@oxc-project/types@0.144.0': + resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} - '@oxfmt/binding-android-arm-eabi@0.62.0': - resolution: {integrity: sha512-pdsv0C4gPjJ8H1+sd8u0BDx+yLACTL+rgeMIOL1ln4ihSnhw8CWXtYWgvcSkyTfgGBIzFKab+d8rx9Xl4en/Kw==} + '@oxfmt/binding-android-arm-eabi@0.63.0': + resolution: {integrity: sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.62.0': - resolution: {integrity: sha512-WC3YQ7uS/KtDrjmqwBviwFKe9qeoi+eXx8aX1z/ffG23Md75myjrJaQqTuJvdOLPoa4EYTjDWH0dHXfwulCVog==} + '@oxfmt/binding-android-arm64@0.63.0': + resolution: {integrity: sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.62.0': - resolution: {integrity: sha512-GM8Yf3LjjaR1I8PD0SfeoIlwhsh9GvSF+cQ8sf624Yxnjsyumn95aFzYfKJVefblfDIiOAnZ7QVm2sa21Er/0Q==} + '@oxfmt/binding-darwin-arm64@0.63.0': + resolution: {integrity: sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.62.0': - resolution: {integrity: sha512-d5THp7F8bCxLqNogEXDORRsQD6dosf3EyFtnXfBer6v+8tGdcWIjoDX9WaXrrF/26zOmL8qHpPTKCEvpBDmZkQ==} + '@oxfmt/binding-darwin-x64@0.63.0': + resolution: {integrity: sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.62.0': - resolution: {integrity: sha512-1DnrtXGZooOZ0fHgAXZUaDQzBVh1CM2MNW4oBXyQ2aWKvCHjyljvT9fgBkOM0fEOb96X5eqtcfJ0YUVt9jj66g==} + '@oxfmt/binding-freebsd-x64@0.63.0': + resolution: {integrity: sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.62.0': - resolution: {integrity: sha512-4pQDHOYRH+Huqe0StIaWyvk2CVl/aTaqSrbZpA3/pLS2xH24ME7lBgYprhQF2fRkHBzhGGGKliwxFsDdHwx59g==} + '@oxfmt/binding-linux-arm-gnueabihf@0.63.0': + resolution: {integrity: sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.62.0': - resolution: {integrity: sha512-X0jAaZJFMCVKhB6YyWVTQ/wN2DLsBcZKSMqTS76bF6riT+XZdtg2FPEdjDvdVbunO9cG+tWiVaEs4Zs38lxYog==} + '@oxfmt/binding-linux-arm-musleabihf@0.63.0': + resolution: {integrity: sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.62.0': - resolution: {integrity: sha512-682Z8T5s8T5ATArYtsejKvbIfd8LEAXyyDkKkoZVq8HND7Vx8TYLlrDjDSeYfodMeVwHOgkj13lJYR8cj6vUSg==} + '@oxfmt/binding-linux-arm64-gnu@0.63.0': + resolution: {integrity: sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.62.0': - resolution: {integrity: sha512-lk25fAl7KWaLWVJcW0CHEXB7QlQZtx5eDkjpaGMK0hzXTjUe0Wmlu8IKuFHoviSOcEJedRTs4VE/506VqGxGew==} + '@oxfmt/binding-linux-arm64-musl@0.63.0': + resolution: {integrity: sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.62.0': - resolution: {integrity: sha512-SFyNqHQLwySceWNLhiSldx7wPXRAzP0L0WcW9GegP3uWrpZGJiZlQO85NbHAFPEfxR9PhZ9qSnZryEh7+v+4Gw==} + '@oxfmt/binding-linux-ppc64-gnu@0.63.0': + resolution: {integrity: sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.62.0': - resolution: {integrity: sha512-KYj55C1ywJfHo6+aKDuEmUtVEdJALsC5GwayDGsI6FGz2GxFqNr/mA8nxVsNbJzm7sE5MRqTQ9ziImSzhYXysA==} + '@oxfmt/binding-linux-riscv64-gnu@0.63.0': + resolution: {integrity: sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.62.0': - resolution: {integrity: sha512-BhZDNo5GOU5nC378RhD0/XpvaEBHsH3HLgJp8YZX3A0InC7oivzA63HsRmiXFLtLSHAstEVrDf6fbC7Rs8Jh/A==} + '@oxfmt/binding-linux-riscv64-musl@0.63.0': + resolution: {integrity: sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.62.0': - resolution: {integrity: sha512-UyAFmyHkgSgUJ/wOM4p3U8AC2yAFvRH5PNBs7TnK0fObTT/XSWcdr/lAzPSWaekHaZFaMeFZyk9n93Joq3J93A==} + '@oxfmt/binding-linux-s390x-gnu@0.63.0': + resolution: {integrity: sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.62.0': - resolution: {integrity: sha512-1iYMP0leytWazFubD/WnINJuIrzRPuoL1aWEJdlGezEzDbTxcd29R4r8IUzP2oWeKst5V02uMJgR2NILlPlG6w==} + '@oxfmt/binding-linux-x64-gnu@0.63.0': + resolution: {integrity: sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.62.0': - resolution: {integrity: sha512-4rA/URtJSTVNVAQz6Q8wf7SaRvOXVy+TizriT9hs/Y1XhLR/R+92uWKRQG8yFWRAIEBbFHJ6WevQcl/G9SXEfw==} + '@oxfmt/binding-linux-x64-musl@0.63.0': + resolution: {integrity: sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.62.0': - resolution: {integrity: sha512-mSZuFHU2ar1KLUjXpI2QBQcJ1VsOB3mOCgQXuXCpKs19dgh4u+OaovNfrWDfiJb+ihJ2+f7YFcaO9bS2dlTCXA==} + '@oxfmt/binding-openharmony-arm64@0.63.0': + resolution: {integrity: sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.62.0': - resolution: {integrity: sha512-OfwuhkcjDlqC4EgDojtiV9mzpLqeB9KqTOWPOjLEYBVdDCVSxqW3qzp/xcIxsbtI0UgGCnKvAqYKyY25kf5JZw==} + '@oxfmt/binding-win32-arm64-msvc@0.63.0': + resolution: {integrity: sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.62.0': - resolution: {integrity: sha512-P9uDDNFRzghO3X8QAzhkjKhK7JvtABsVn8UYtFX7uor12IAnwNt8nNIctvfWj1JkQU/kE+fmLRPiw7XlrIHsZw==} + '@oxfmt/binding-win32-ia32-msvc@0.63.0': + resolution: {integrity: sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.62.0': - resolution: {integrity: sha512-dlI5SY7XYQCiCBafntWagCR6HcAJB/NpsLtdlPx8x08+Osz8Ok1HHz1GZuusegCe/VoJ6pAnF5a4pd5OZAq7qQ==} + '@oxfmt/binding-win32-x64-msvc@0.63.0': + resolution: {integrity: sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4460,124 +4727,124 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.77.0': - resolution: {integrity: sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ==} + '@oxlint/binding-android-arm-eabi@1.78.0': + resolution: {integrity: sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.77.0': - resolution: {integrity: sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA==} + '@oxlint/binding-android-arm64@1.78.0': + resolution: {integrity: sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.77.0': - resolution: {integrity: sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA==} + '@oxlint/binding-darwin-arm64@1.78.0': + resolution: {integrity: sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.77.0': - resolution: {integrity: sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA==} + '@oxlint/binding-darwin-x64@1.78.0': + resolution: {integrity: sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.77.0': - resolution: {integrity: sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg==} + '@oxlint/binding-freebsd-x64@1.78.0': + resolution: {integrity: sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.77.0': - resolution: {integrity: sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA==} + '@oxlint/binding-linux-arm-gnueabihf@1.78.0': + resolution: {integrity: sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.77.0': - resolution: {integrity: sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ==} + '@oxlint/binding-linux-arm-musleabihf@1.78.0': + resolution: {integrity: sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.77.0': - resolution: {integrity: sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw==} + '@oxlint/binding-linux-arm64-gnu@1.78.0': + resolution: {integrity: sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.77.0': - resolution: {integrity: sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g==} + '@oxlint/binding-linux-arm64-musl@1.78.0': + resolution: {integrity: sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.77.0': - resolution: {integrity: sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg==} + '@oxlint/binding-linux-ppc64-gnu@1.78.0': + resolution: {integrity: sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.77.0': - resolution: {integrity: sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q==} + '@oxlint/binding-linux-riscv64-gnu@1.78.0': + resolution: {integrity: sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.77.0': - resolution: {integrity: sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA==} + '@oxlint/binding-linux-riscv64-musl@1.78.0': + resolution: {integrity: sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.77.0': - resolution: {integrity: sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw==} + '@oxlint/binding-linux-s390x-gnu@1.78.0': + resolution: {integrity: sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.77.0': - resolution: {integrity: sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw==} + '@oxlint/binding-linux-x64-gnu@1.78.0': + resolution: {integrity: sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.77.0': - resolution: {integrity: sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ==} + '@oxlint/binding-linux-x64-musl@1.78.0': + resolution: {integrity: sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.77.0': - resolution: {integrity: sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw==} + '@oxlint/binding-openharmony-arm64@1.78.0': + resolution: {integrity: sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.77.0': - resolution: {integrity: sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA==} + '@oxlint/binding-win32-arm64-msvc@1.78.0': + resolution: {integrity: sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.77.0': - resolution: {integrity: sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA==} + '@oxlint/binding-win32-ia32-msvc@1.78.0': + resolution: {integrity: sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.77.0': - resolution: {integrity: sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw==} + '@oxlint/binding-win32-x64-msvc@1.78.0': + resolution: {integrity: sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4729,97 +4996,92 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} - '@redwoodjs/agent-ci@0.18.0': - resolution: {integrity: sha512-GfuCg7dPAE0carKABi4e4KseH1O3DSZORCIb61yI44RoZ5dWqSv1Gcsj0xwPlXADHpTiHV4nq995lsHrrvfPhw==} - engines: {node: '>=22'} - hasBin: true - - '@rolldown/binding-android-arm64@1.2.3': - resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} + '@rolldown/binding-android-arm64@1.2.4': + resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.3': - resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} + '@rolldown/binding-darwin-arm64@1.2.4': + resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.3': - resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} + '@rolldown/binding-darwin-x64@1.2.4': + resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.3': - resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} + '@rolldown/binding-freebsd-x64@1.2.4': + resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.3': - resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.3': - resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} + '@rolldown/binding-linux-arm64-gnu@1.2.4': + resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.3': - resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} + '@rolldown/binding-linux-arm64-musl@1.2.4': + resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.3': - resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.3': - resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} + '@rolldown/binding-linux-s390x-gnu@1.2.4': + resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.3': - resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} + '@rolldown/binding-linux-x64-gnu@1.2.4': + resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.3': - resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} + '@rolldown/binding-linux-x64-musl@1.2.4': + resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.3': - resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} + '@rolldown/binding-openharmony-arm64@1.2.4': + resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.3': - resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} + '@rolldown/binding-win32-arm64-msvc@1.2.4': + resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.3': - resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} + '@rolldown/binding-win32-x64-msvc@1.2.4': + resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4830,8 +5092,8 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@shadscan/cli@0.11.0': - resolution: {integrity: sha512-lA6CK7jxopwoSxapT03RzDc51amXux509NGA5Um3OF1kY0oRRuvdW/mOHSjdyATkZU7zLsC5oZx6Y7y2taE5cg==} + '@shadscan/cli@0.16.0': + resolution: {integrity: sha512-CTAfeoKYITalPQMOO15sCSa3wRmV1gChzm4qLv/dR2bh7fQOH9ZHNS8w++J+Qtb5+PAj8WAv7B3rTCCVBJczpw==} engines: {node: '>=18'} hasBin: true @@ -5129,9 +5391,6 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@26.1.2': - resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} - '@types/node@26.2.0': resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} @@ -5738,8 +5997,8 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} - chrome-devtools-mcp@1.6.0: - resolution: {integrity: sha512-VZX6f/OjQSYhy2BGGRs+y3LsrsAQAz/HwZCWKBLVyST/4r/3zjVEjjVW7gMCVbRDuspnVdcp5hQDPrQ5UFrdZw==} + chrome-devtools-mcp@1.7.0: + resolution: {integrity: sha512-6xFW7oiUxTxZuHcfyYBkKQtmttjCbfifKZMSEk5CV8H2FucvKweYiJr8CblddYHtYjA4C14K9VAs1r49906RBA==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} hasBin: true peerDependencies: @@ -5959,8 +6218,8 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dtu-github-actions@0.18.0: - resolution: {integrity: sha512-GZuZCZTmjUr5MD9eLen4iEFifHk5pPtUumBHC4vk64HZ7KKqBxhhtrXULGL++BuQJw0MK6M3jLTfbuknOy6Nzw==} + dtu-github-actions@0.18.1: + resolution: {integrity: sha512-BoK1sWdcOMKq58aGaChMbEMhrOJ+2ZPvlvASFXFkduq++6k/ZYi5c9RXaeLXcuTRZDOj/5lvJR4+j93+BXepSw==} engines: {node: '>=22'} ecc-agentshield@1.4.0: @@ -6803,10 +7062,6 @@ packages: resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} engines: {node: '>=8'} - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -6992,8 +7247,8 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - oxfmt@0.62.0: - resolution: {integrity: sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ==} + oxfmt@0.63.0: + resolution: {integrity: sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -7009,8 +7264,8 @@ packages: resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==} hasBin: true - oxlint@1.77.0: - resolution: {integrity: sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==} + oxlint@1.78.0: + resolution: {integrity: sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -7034,9 +7289,6 @@ packages: resolution: {integrity: sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==} engines: {node: '>=18'} - package-manager-detector@1.7.0: - resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==} - package-manager-detector@1.8.0: resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} @@ -7126,6 +7378,11 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + playwright-core@1.62.1: resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} engines: {node: '>=20'} @@ -7296,8 +7553,8 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rolldown@1.2.3: - resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} + rolldown@1.2.4: + resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -7305,8 +7562,8 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} - run-local-ci@0.18.0: - resolution: {integrity: sha512-nbmnxYwILzoG7gpvn+2JhQMBZ4zgJqOxDpVySYZTuRfUm+8xm0x8nkDMeM/jFvGtunX4UkO3kmCXL/kMho7P8Q==} + run-local-ci@0.18.1: + resolution: {integrity: sha512-x+hLy4c4aP04lTd/cmogcJhuDJqakmX8mBIB4UrUMGrvdNbsK3nl6xGQaaQojjb4eeQAlf8hCXyl0qgj9rGyog==} engines: {node: '>=22'} hasBin: true @@ -7515,8 +7772,8 @@ packages: resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} engines: {node: '>=18'} - taze@19.17.1: - resolution: {integrity: sha512-ZUrU86BRNGJRZzCWOaMWC1ohYogHpRlQcGYuTClW8CpSE3iWjHE/jPuqpsOA/lKijL7O3algqq7l2bbdLCfrxg==} + taze@20.0.1: + resolution: {integrity: sha512-eMaxsasw9WZfwXeiRNynqo9uSukB876oHlk+obmaaW4z8ue+LvFXSZC7O+RSABOlWNUqZmxXcDJS/O7MxW6tlw==} hasBin: true teex@1.0.1: @@ -7627,8 +7884,8 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} - typebox@1.3.11: - resolution: {integrity: sha512-tZGKIS02Opbh4EYMmAEVuXl+y3EQJ9ZlkitLZ1CmFCY4ZOqr/xWR9dDSCFmIjNDICGMWkOqMh6WxGTyRXqcSGQ==} + typebox@1.3.13: + resolution: {integrity: sha512-rzv/3uBDgWGb5b4+Rqb/WoDFCc+5N0iRC/7kgQyk/mJJeumQjHFFAZ+X+Zs+CNxnNnJOkhIMrQc2irTzfEbKjw==} typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} @@ -7733,8 +7990,8 @@ packages: resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} engines: {node: ^20.17.0 || >=22.9.0} - verkit@0.3.1: - resolution: {integrity: sha512-w2Eo8LSIIoW7qxNBzT7/17k+bh8plXo7G3dHjEIDqPlnluhzaxr9JX8F28VSYEtDvc1/a3WBDih6xNUZseebXg==} + verkit@0.3.2: + resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==} engines: {node: '>=18.12.0'} vite@8.2.0: @@ -7962,7 +8219,7 @@ snapshots: '@antfu/ni@30.3.0': dependencies: fzf: 0.5.2 - package-manager-detector: 1.7.0 + package-manager-detector: 1.8.0 tinyexec: 1.3.0 tinyglobby: 0.2.17 @@ -8709,63 +8966,63 @@ snapshots: dependencies: '@optique/core': 1.1.0 - '@oxc-project/types@0.143.0': {} + '@oxc-project/types@0.144.0': {} - '@oxfmt/binding-android-arm-eabi@0.62.0': + '@oxfmt/binding-android-arm-eabi@0.63.0': optional: true - '@oxfmt/binding-android-arm64@0.62.0': + '@oxfmt/binding-android-arm64@0.63.0': optional: true - '@oxfmt/binding-darwin-arm64@0.62.0': + '@oxfmt/binding-darwin-arm64@0.63.0': optional: true - '@oxfmt/binding-darwin-x64@0.62.0': + '@oxfmt/binding-darwin-x64@0.63.0': optional: true - '@oxfmt/binding-freebsd-x64@0.62.0': + '@oxfmt/binding-freebsd-x64@0.63.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.62.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.63.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.62.0': + '@oxfmt/binding-linux-arm-musleabihf@0.63.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.62.0': + '@oxfmt/binding-linux-arm64-gnu@0.63.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.62.0': + '@oxfmt/binding-linux-arm64-musl@0.63.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.62.0': + '@oxfmt/binding-linux-ppc64-gnu@0.63.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.62.0': + '@oxfmt/binding-linux-riscv64-gnu@0.63.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.62.0': + '@oxfmt/binding-linux-riscv64-musl@0.63.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.62.0': + '@oxfmt/binding-linux-s390x-gnu@0.63.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.62.0': + '@oxfmt/binding-linux-x64-gnu@0.63.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.62.0': + '@oxfmt/binding-linux-x64-musl@0.63.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.62.0': + '@oxfmt/binding-openharmony-arm64@0.63.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.62.0': + '@oxfmt/binding-win32-arm64-msvc@0.63.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.62.0': + '@oxfmt/binding-win32-ia32-msvc@0.63.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.62.0': + '@oxfmt/binding-win32-x64-msvc@0.63.0': optional: true '@oxlint-tsgolint/darwin-arm64@7.0.2001': @@ -8786,61 +9043,61 @@ snapshots: '@oxlint-tsgolint/win32-x64@7.0.2001': optional: true - '@oxlint/binding-android-arm-eabi@1.77.0': + '@oxlint/binding-android-arm-eabi@1.78.0': optional: true - '@oxlint/binding-android-arm64@1.77.0': + '@oxlint/binding-android-arm64@1.78.0': optional: true - '@oxlint/binding-darwin-arm64@1.77.0': + '@oxlint/binding-darwin-arm64@1.78.0': optional: true - '@oxlint/binding-darwin-x64@1.77.0': + '@oxlint/binding-darwin-x64@1.78.0': optional: true - '@oxlint/binding-freebsd-x64@1.77.0': + '@oxlint/binding-freebsd-x64@1.78.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.77.0': + '@oxlint/binding-linux-arm-gnueabihf@1.78.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.77.0': + '@oxlint/binding-linux-arm-musleabihf@1.78.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.77.0': + '@oxlint/binding-linux-arm64-gnu@1.78.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.77.0': + '@oxlint/binding-linux-arm64-musl@1.78.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.77.0': + '@oxlint/binding-linux-ppc64-gnu@1.78.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.77.0': + '@oxlint/binding-linux-riscv64-gnu@1.78.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.77.0': + '@oxlint/binding-linux-riscv64-musl@1.78.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.77.0': + '@oxlint/binding-linux-s390x-gnu@1.78.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.77.0': + '@oxlint/binding-linux-x64-gnu@1.78.0': optional: true - '@oxlint/binding-linux-x64-musl@1.77.0': + '@oxlint/binding-linux-x64-musl@1.78.0': optional: true - '@oxlint/binding-openharmony-arm64@1.77.0': + '@oxlint/binding-openharmony-arm64@1.78.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.77.0': + '@oxlint/binding-win32-arm64-msvc@1.78.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.77.0': + '@oxlint/binding-win32-ia32-msvc@1.78.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.77.0': + '@oxlint/binding-win32-x64-msvc@1.78.0': optional: true '@playwright/mcp@0.0.79': @@ -9011,63 +9268,58 @@ snapshots: dependencies: quansync: 1.0.0 - '@redwoodjs/agent-ci@0.18.0(supports-color@7.2.0)': - dependencies: - run-local-ci: 0.18.0(supports-color@7.2.0) - transitivePeerDependencies: - - supports-color - - '@rolldown/binding-android-arm64@1.2.3': + '@rolldown/binding-android-arm64@1.2.4': optional: true - '@rolldown/binding-darwin-arm64@1.2.3': + '@rolldown/binding-darwin-arm64@1.2.4': optional: true - '@rolldown/binding-darwin-x64@1.2.3': + '@rolldown/binding-darwin-x64@1.2.4': optional: true - '@rolldown/binding-freebsd-x64@1.2.3': + '@rolldown/binding-freebsd-x64@1.2.4': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.3': + '@rolldown/binding-linux-arm64-gnu@1.2.4': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.3': + '@rolldown/binding-linux-arm64-musl@1.2.4': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.3': + '@rolldown/binding-linux-ppc64-gnu@1.2.4': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.3': + '@rolldown/binding-linux-s390x-gnu@1.2.4': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.3': + '@rolldown/binding-linux-x64-gnu@1.2.4': optional: true - '@rolldown/binding-linux-x64-musl@1.2.3': + '@rolldown/binding-linux-x64-musl@1.2.4': optional: true - '@rolldown/binding-openharmony-arm64@1.2.3': + '@rolldown/binding-openharmony-arm64@1.2.4': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.3': + '@rolldown/binding-win32-arm64-msvc@1.2.4': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.3': + '@rolldown/binding-win32-x64-msvc@1.2.4': optional: true '@rolldown/pluginutils@1.0.1': {} '@sec-ant/readable-stream@0.4.1': {} - '@shadscan/cli@0.11.0': + '@shadscan/cli@0.16.0': dependencies: cross-spawn: 7.0.6 jsonc-parser: 3.3.1 picocolors: 1.1.1 + playwright-core: 1.61.1 tinyglobby: 0.2.15 typescript: 5.9.3 zod: 4.1.13 @@ -9237,7 +9489,7 @@ snapshots: '@types/adm-zip@0.5.8': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.2.0 '@types/braces@3.0.5': {} @@ -9294,10 +9546,6 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@26.1.2': - dependencies: - undici-types: 8.3.0 - '@types/node@26.2.0': dependencies: undici-types: 8.3.0 @@ -9851,7 +10099,7 @@ snapshots: chownr@3.0.0: {} - chrome-devtools-mcp@1.6.0: {} + chrome-devtools-mcp@1.7.0: {} chunkify@5.0.0: {} @@ -10058,7 +10306,7 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dtu-github-actions@0.18.0(supports-color@7.2.0): + dtu-github-actions@0.18.1(supports-color@7.2.0): dependencies: body-parser: 2.3.0(supports-color@7.2.0) minimatch: 10.2.6(patch_hash=83f1ea5b333d1b6fe1b36f93ccb222aa02e5dd468b2c646e285d7d53d234e174) @@ -11090,20 +11338,16 @@ snapshots: minipass-flush@1.0.7: dependencies: - minipass: 3.3.6 + minipass: 7.1.3 minipass-pipeline@1.2.4: dependencies: - minipass: 3.3.6 + minipass: 7.1.3 minipass-sized@2.0.0: dependencies: minipass: 7.1.3 - minipass@3.3.6: - dependencies: - yallist: 4.0.0 - minipass@7.1.3: {} minizlib@3.1.0: @@ -11289,29 +11533,29 @@ snapshots: outvariant@1.4.3: {} - oxfmt@0.62.0: + oxfmt@0.63.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.62.0 - '@oxfmt/binding-android-arm64': 0.62.0 - '@oxfmt/binding-darwin-arm64': 0.62.0 - '@oxfmt/binding-darwin-x64': 0.62.0 - '@oxfmt/binding-freebsd-x64': 0.62.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.62.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.62.0 - '@oxfmt/binding-linux-arm64-gnu': 0.62.0 - '@oxfmt/binding-linux-arm64-musl': 0.62.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.62.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.62.0 - '@oxfmt/binding-linux-riscv64-musl': 0.62.0 - '@oxfmt/binding-linux-s390x-gnu': 0.62.0 - '@oxfmt/binding-linux-x64-gnu': 0.62.0 - '@oxfmt/binding-linux-x64-musl': 0.62.0 - '@oxfmt/binding-openharmony-arm64': 0.62.0 - '@oxfmt/binding-win32-arm64-msvc': 0.62.0 - '@oxfmt/binding-win32-ia32-msvc': 0.62.0 - '@oxfmt/binding-win32-x64-msvc': 0.62.0 + '@oxfmt/binding-android-arm-eabi': 0.63.0 + '@oxfmt/binding-android-arm64': 0.63.0 + '@oxfmt/binding-darwin-arm64': 0.63.0 + '@oxfmt/binding-darwin-x64': 0.63.0 + '@oxfmt/binding-freebsd-x64': 0.63.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.63.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.63.0 + '@oxfmt/binding-linux-arm64-gnu': 0.63.0 + '@oxfmt/binding-linux-arm64-musl': 0.63.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.63.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.63.0 + '@oxfmt/binding-linux-riscv64-musl': 0.63.0 + '@oxfmt/binding-linux-s390x-gnu': 0.63.0 + '@oxfmt/binding-linux-x64-gnu': 0.63.0 + '@oxfmt/binding-linux-x64-musl': 0.63.0 + '@oxfmt/binding-openharmony-arm64': 0.63.0 + '@oxfmt/binding-win32-arm64-msvc': 0.63.0 + '@oxfmt/binding-win32-ia32-msvc': 0.63.0 + '@oxfmt/binding-win32-x64-msvc': 0.63.0 oxlint-tsgolint@7.0.2001: optionalDependencies: @@ -11322,27 +11566,27 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 7.0.2001 '@oxlint-tsgolint/win32-x64': 7.0.2001 - oxlint@1.77.0(oxlint-tsgolint@7.0.2001): + oxlint@1.78.0(oxlint-tsgolint@7.0.2001): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.77.0 - '@oxlint/binding-android-arm64': 1.77.0 - '@oxlint/binding-darwin-arm64': 1.77.0 - '@oxlint/binding-darwin-x64': 1.77.0 - '@oxlint/binding-freebsd-x64': 1.77.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.77.0 - '@oxlint/binding-linux-arm-musleabihf': 1.77.0 - '@oxlint/binding-linux-arm64-gnu': 1.77.0 - '@oxlint/binding-linux-arm64-musl': 1.77.0 - '@oxlint/binding-linux-ppc64-gnu': 1.77.0 - '@oxlint/binding-linux-riscv64-gnu': 1.77.0 - '@oxlint/binding-linux-riscv64-musl': 1.77.0 - '@oxlint/binding-linux-s390x-gnu': 1.77.0 - '@oxlint/binding-linux-x64-gnu': 1.77.0 - '@oxlint/binding-linux-x64-musl': 1.77.0 - '@oxlint/binding-openharmony-arm64': 1.77.0 - '@oxlint/binding-win32-arm64-msvc': 1.77.0 - '@oxlint/binding-win32-ia32-msvc': 1.77.0 - '@oxlint/binding-win32-x64-msvc': 1.77.0 + '@oxlint/binding-android-arm-eabi': 1.78.0 + '@oxlint/binding-android-arm64': 1.78.0 + '@oxlint/binding-darwin-arm64': 1.78.0 + '@oxlint/binding-darwin-x64': 1.78.0 + '@oxlint/binding-freebsd-x64': 1.78.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.78.0 + '@oxlint/binding-linux-arm-musleabihf': 1.78.0 + '@oxlint/binding-linux-arm64-gnu': 1.78.0 + '@oxlint/binding-linux-arm64-musl': 1.78.0 + '@oxlint/binding-linux-ppc64-gnu': 1.78.0 + '@oxlint/binding-linux-riscv64-gnu': 1.78.0 + '@oxlint/binding-linux-riscv64-musl': 1.78.0 + '@oxlint/binding-linux-s390x-gnu': 1.78.0 + '@oxlint/binding-linux-x64-gnu': 1.78.0 + '@oxlint/binding-linux-x64-musl': 1.78.0 + '@oxlint/binding-openharmony-arm64': 1.78.0 + '@oxlint/binding-win32-arm64-msvc': 1.78.0 + '@oxlint/binding-win32-ia32-msvc': 1.78.0 + '@oxlint/binding-win32-x64-msvc': 1.78.0 oxlint-tsgolint: 7.0.2001 p-limit@3.1.0: @@ -11355,8 +11599,6 @@ snapshots: p-map@7.0.6: {} - package-manager-detector@1.7.0: {} - package-manager-detector@1.8.0: {} pacote@21.5.1(supports-color@7.2.0): @@ -11452,6 +11694,8 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 + playwright-core@1.61.1: {} + playwright-core@1.62.1: {} playwright-core@1.63.0-alpha-2026-08-05: {} @@ -11521,7 +11765,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.2 - '@types/node': 26.1.2 + '@types/node': 26.2.0 long: 5.3.2 proxy-agent-negotiate@1.1.0: {} @@ -11608,33 +11852,33 @@ snapshots: rfdc@1.4.1: {} - rolldown@1.2.3: + rolldown@1.2.4: dependencies: - '@oxc-project/types': 0.143.0 + '@oxc-project/types': 0.144.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.3 - '@rolldown/binding-darwin-arm64': 1.2.3 - '@rolldown/binding-darwin-x64': 1.2.3 - '@rolldown/binding-freebsd-x64': 1.2.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 - '@rolldown/binding-linux-arm64-gnu': 1.2.3 - '@rolldown/binding-linux-arm64-musl': 1.2.3 - '@rolldown/binding-linux-ppc64-gnu': 1.2.3 - '@rolldown/binding-linux-s390x-gnu': 1.2.3 - '@rolldown/binding-linux-x64-gnu': 1.2.3 - '@rolldown/binding-linux-x64-musl': 1.2.3 - '@rolldown/binding-openharmony-arm64': 1.2.3 - '@rolldown/binding-win32-arm64-msvc': 1.2.3 - '@rolldown/binding-win32-x64-msvc': 1.2.3 + '@rolldown/binding-android-arm64': 1.2.4 + '@rolldown/binding-darwin-arm64': 1.2.4 + '@rolldown/binding-darwin-x64': 1.2.4 + '@rolldown/binding-freebsd-x64': 1.2.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.4 + '@rolldown/binding-linux-arm64-gnu': 1.2.4 + '@rolldown/binding-linux-arm64-musl': 1.2.4 + '@rolldown/binding-linux-ppc64-gnu': 1.2.4 + '@rolldown/binding-linux-s390x-gnu': 1.2.4 + '@rolldown/binding-linux-x64-gnu': 1.2.4 + '@rolldown/binding-linux-x64-musl': 1.2.4 + '@rolldown/binding-openharmony-arm64': 1.2.4 + '@rolldown/binding-win32-arm64-msvc': 1.2.4 + '@rolldown/binding-win32-x64-msvc': 1.2.4 run-applescript@7.1.0: {} - run-local-ci@0.18.0(supports-color@7.2.0): + run-local-ci@0.18.1(supports-color@7.2.0): dependencies: '@actions/workflow-parser': 0.3.43 dockerode: 5.0.1(supports-color@7.2.0) - dtu-github-actions: 0.18.0(supports-color@7.2.0) + dtu-github-actions: 0.18.1(supports-color@7.2.0) minimatch: 10.2.6(patch_hash=83f1ea5b333d1b6fe1b36f93ccb222aa02e5dd468b2c646e285d7d53d234e174) yaml: 2.9.0 transitivePeerDependencies: @@ -11865,7 +12109,7 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - taze@19.17.1: + taze@20.0.1: dependencies: '@antfu/ni': 30.3.0 '@henrygd/queue': 1.2.0 @@ -11879,7 +12123,7 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 unconfig: 7.5.0 - verkit: 0.3.1 + verkit: 0.3.2 yaml: 2.9.0 teex@1.0.1: @@ -12000,7 +12244,7 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typebox@1.3.11: {} + typebox@1.3.13: {} typescript@5.9.3: {} @@ -12123,14 +12367,14 @@ snapshots: validate-npm-package-name@7.0.2: {} - verkit@0.3.1: {} + verkit@0.3.2: {} vite@8.2.0(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 postcss: 8.5.23 - rolldown: 1.2.3 + rolldown: 1.2.4 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 26.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ecf0c9c4e..76f51eedd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -56,20 +56,20 @@ catalog: # platform-binary optionalDependencies, despite the monorepo's internal # workspace layout. dtu-github-actions is its GitHub-Actions parser, pinned # explicitly (not left transitive) so its version is uniform fleet-wide. - '@redwoodjs/agent-ci': 0.18.0 '@ultrathink/acorn.rs.wasm': 0.1.1 'brace-expansion': 5.0.9 - 'dtu-github-actions': 0.18.0 + 'dtu-github-actions': 0.18.1 # shadscan — shadcn UI audit CLI for the design skills (missing UI # fundamentals report). Exact-pinned; the whole package is younger than # the soak window, so it carries a minimumReleaseAgeExclude entry below. - '@shadscan/cli': 0.11.0 + '@shadscan/cli': 0.16.0 '@sinclair/typebox': 0.34.52 + 'run-local-ci': 0.18.1 # typebox 1.x — the unscoped rewrite of @sinclair/typebox. Both names are # pinned while the fleet migrates; the 0.x entry is deleted once no member # imports the scoped name. 1.3.10 is inside the 7-day soak, so it carries a # dated minimumReleaseAgeExclude entry above. - 'typebox': 1.3.11 + 'typebox': 1.3.13 '@socketregistry/packageurl-js': 1.5.2 # -stable aliases: pnpm `overrides:` can't redirect a package's own # name when used INSIDE that same package — Node ESM treats it as a @@ -97,7 +97,7 @@ catalog: # derivation, version compare, config validation). Runs standalone in vitest. # published: 2026-07-08 (past 7-day soak). See .claude/skills/fleet/property-testing. 'fast-check': 4.9.0 - 'magic-string': 1.1.0 + 'magic-string': 1.2.0 'markdownlint-cli2': 0.23.2 'mdast-util-from-markdown': 2.0.3 # GFM pair for render-faithful markdown parsing (tables, footnotes, @@ -123,8 +123,8 @@ catalog: # candidates before simulating what a port actually cuts. Published # 2026-06-08, past the 7-day soak. 'npm-high-impact': 1.13.0 - 'oxfmt': 0.62.0 - 'oxlint': 1.77.0 + 'oxfmt': 0.63.0 + 'oxlint': 1.78.0 'oxlint-tsgolint': 7.0.2001 # parse5 — the HTML parser half of the markdown story. mdast hands raw HTML # back as an opaque `html` node (README ``/`` blocks), so @@ -149,7 +149,7 @@ catalog: 'regjsparser': 0.13.2 'semver': 7.8.5 'shell-quote': 1.10.0 - 'taze': 19.17.1 + 'taze': 20.0.1 # vite 8.0.16 swaps esbuild → rolldown natively (bundles rolldown # 1.0.2) and patches the <=8.0.15 advisories (GHSA-fx2h-pf6j-xcff # high, GHSA-v6wh-96g9-6wx3 medium). Tracked here so the pnpm @@ -159,7 +159,7 @@ catalog: 'vitest': 4.1.10 '@vitest/coverage-v8': 4.1.10 c8: 12.0.0 - 'chrome-devtools-mcp': 1.6.0 + 'chrome-devtools-mcp': 1.7.0 # Playwright MCP server — agent-driven browsing with the fleet agent-banner # init script (see .config/fleet/playwright/). '@playwright/mcp': 0.0.79 @@ -177,7 +177,7 @@ catalog: # Use in rolldown.config.mts for any repo with a Rolldown build. # published: 2026-05-07 (26 days, past 7-day soak) 'comptime': 0.1.0 - 'rolldown': 1.2.3 + 'rolldown': 1.2.4 'rolldown-plugin-dts': 0.28.0 'svgo': 4.0.2 'vite': 8.2.0 @@ -400,6 +400,14 @@ minimumReleaseAgeExclude: # markdownlint-cli2 0.23.1's vulnerable 5.2.1 exact-pin. Soak bypassed per # the high-severity security exception; exact-pinned so a future version # re-soaks. + - '@abitious/*' + - '@decmpfs/*' + - '@node-smol/*' + - 'sockeye' + # published: 2026-08-21 | removable: 2026-08-28 + - 'dtu-github-actions@0.18.1' + # published: 2026-08-21 | removable: 2026-08-28 + - 'run-local-ci@0.18.1' # Refuse transitive (sub-) deps declared via git/tarball/local-tarball # specs. Direct git deps in this repo are still allowed; this only diff --git a/scripts/repo/bootstrap/fleet.d.mts b/scripts/repo/bootstrap/fleet.d.mts index 11d1856be..306889ee6 100644 --- a/scripts/repo/bootstrap/fleet.d.mts +++ b/scripts/repo/bootstrap/fleet.d.mts @@ -1,5 +1,6 @@ //#region scripts/repo/gen/bootstrap/src/helpers.d.mts type FleetCommentStyle = 'hash' | 'html' | 'json' | 'slash'; +declare const HYBRID_BUNDLE_PATHS: ReadonlySet; interface BundleManifest { readonly files: Record; readonly generatedPaths?: readonly string[] | undefined; @@ -17,6 +18,10 @@ interface BundleManifest { interface InstallConfig { readonly bundle?: string | undefined; readonly dest?: string | undefined; + /** + * Materialize mirrors from this checkout's own template/base (producer). + */ + readonly fromTemplate?: boolean | undefined; readonly dryRun?: boolean | undefined; readonly exitCode?: boolean | undefined; readonly ifCurrent?: boolean | undefined; @@ -178,6 +183,14 @@ interface MemberBuildShape { * ship-everything so a config problem can never withhold payload. */ declare function readBuildShape(dest: string): MemberBuildShape; +/** + * The member's declared capabilities — the `capabilities` map in its + * wheelhouse settings file (an empty or ABSENT map declares NONE, matching + * the cascade-side gate). Drives the manifest's capability-scoped hook + * groups: a `@capability`-tagged hook is placed only when the member + * declares the capability. + */ +declare function readDeclaredCapabilities(dest: string): string[]; /** * Read the member's full pinned `bundle` block (ref + cascadeSha) from the * wheelhouse settings file. The lock-step verify + the `fleet:status` verb need @@ -321,10 +334,26 @@ declare function ghcrTokenUrl(repo: string, registry: string): string; */ declare function tokenFromBody(body: Buffer): string | undefined; /** - * Obtain an anonymous pull token. Hits the documented token endpoint first; on - * anything but a usable token, falls back to the 401 WWW-Authenticate challenge - * form (probe /v2/, follow the advertised realm). Fails loud when no token can - * be obtained. + * `Authorization: Basic` for GHCR's token endpoint, built from the workflow + * token when one is in the environment. + * + * A PUBLIC package needs none of this - anonymous pull is the common path and + * stays first. A package that is private, or newly published and not yet made + * public, answers the anonymous request with 403 and no token, which reads as + * "confirm the package is public" and is unactionable inside a job that already + * holds a credential for the same repo. GHCR accepts the workflow token as the + * password with any username. + * + * Returns undefined when no token is in the environment, so a local run keeps + * its anonymous behavior. Never logged: the value only ever becomes a header. + */ +declare function ghcrBasicAuthHeader(env: Record): string | undefined; +/** + * Obtain a pull token. Hits the documented token endpoint first; on anything + * but a usable token, falls back to the 401 WWW-Authenticate challenge form + * (probe /v2/, follow the advertised realm), and finally retries the challenge + * WITH the workflow token when the environment carries one. Fails loud when no + * token can be obtained. */ declare function getGhcrToken(repo: string, registry: string, httpFn?: GhcrHttpGetFn): Promise; /** @@ -454,6 +483,26 @@ interface InstallFilesResult { refreshedTracked: string[]; } declare function installFiles(filesDir: string, dest: string, manifest: BundleManifest, options?: InstallFilesOptions): InstallFilesResult; +/** + * Materialize the fleet mirrors in a PRODUCER checkout from its own + * `template/base`, rather than from a fetched bundle. + * + * The wheelhouse holds the canon locally, so it has no bundle to fetch and is + * not a fleet-pack consumer. That is the only reason its mirrors stayed in + * version control: nothing else could put them back. Producing the payload does + * not require tracking the output, so this is the producer's belt. + * + * Why it must live in this dep-0 entry and not in the cascade: the cascade + * cannot load without the payload it would be materializing. + * `template/base/scripts/fleet/land-work.mts` and its siblings import the LIVE + * `.claude/hooks/fleet/_shared/**`, so a checkout whose mirrors are absent dies + * at module resolution before any fixer runs. Same reason the fetcher cannot + * ship inside the bundle it fetches. + * + * Returns undefined when `template/base` is absent, which is every consumer: + * the caller then knows this checkout is not a producer and fetches instead. + */ +declare function materializeFromLocalTemplate(dest: string, manifest: BundleManifest, options?: InstallFilesOptions): InstallFilesResult | undefined; /** * Untrack the bundle's GENERATED build outputs (`manifest.generatedPaths`) * from the git index after placement. The bundle SHIPS these files — placement @@ -485,6 +534,13 @@ declare function installSettingsSegment(segmentsDir: string, dest: string, manif declare function installWorkspaceSegment(segmentsDir: string, dest: string, manifest: BundleManifest): number; declare const SYNC_FLEET_SCRIPT = "node scripts/repo/bootstrap/fleet.mjs"; declare const PREPARE_FETCH = "node scripts/repo/bootstrap/prepare.mts"; +/** + * The PRODUCER belt: materialize the mirrors from this checkout's own + * `template/base` instead of fetching a bundle. The wheelhouse's counterpart to + * PREPARE_FETCH, and it runs in the same slot for the same reason — the + * git-hooks installer it precedes is itself one of the untracked mirrors. + */ +declare const PREPARE_FROM_TEMPLATE = "node scripts/repo/bootstrap/fleet.mjs --from-template"; declare const FLEET_STATUS_SCRIPT = "node scripts/repo/bootstrap/fleet.mjs --status"; /** * Wire the consumer's package.json for thin distribution: a `sync-fleet` script @@ -499,6 +555,15 @@ declare function normalizeManifestEntryPath(entry: { path: string; }): string; interface FleetFileManifest { + /** + * Hook payloads gated on a member capability (stamped from each hook's + * `// @capability ` header at pack build time): placed only when the + * member declares it. + */ + capabilityScopedFiles?: ReadonlyArray<{ + capability: string; + files: readonly string[]; + }> | undefined; files: Record; movedPaths?: ReadonlyArray<{ from: string; @@ -529,6 +594,16 @@ interface FleetFileManifest { * config), returns the manifest untouched — a config problem must never * withhold payload. */ +/** + * Drop the manifest's capability-scoped hook payloads the member does not + * declare, so a `@capability cargo` hook never lands in a repo with no cargo + * capability — the pack-side twin of the cascade's dirMirrorSkipPredicate + * capability gate. Fails OPEN on an unknown capabilities read (absent or + * malformed settings file): a config problem must never withhold payload. + * The prune sees the same filtered set, so a wrongly placed copy heals on + * the next fetch. + */ +declare function filterManifestForCapabilities(manifest: T, capabilities: readonly string[] | undefined): T; declare function filterManifestForShape(manifest: T, shape: { from: string | undefined; type: string | undefined; @@ -923,4 +998,4 @@ declare function runStatus(config: InstallConfig): Promise; declare function installFleet(config: InstallConfig): Promise; declare function isMainModule(): boolean; //#endregion -export { AuthChallenge, BundleConfig, BundleFetchFn, BundleManifest, ERR_BUNDLE_BEHIND_LOCAL, ERR_LOCKSTEP_MISMATCH, FLEET_STATUS_SCRIPT, FetchedBundle, FetchedFiles, FleetBlockSpan, FleetCommentStyle, FleetFileManifest, GHCR_HOST, GhcrHttpGetFn, GhcrHttpOptions, GhcrHttpResponse, InstallConfig, InstallFilesOptions, InstallFilesResult, LockStepConfig, LockStepErrorParts, LockStepInputs, LockStepState, LockStepStateName, MANIFEST_ACCEPT, MemberBuildShape, MergeWorkspaceConfig, NoticeDecisionInputs, NoticeStore, OciLayer, OciManifest, PREPARE_FETCH, PullBundleConfig, RefValidation, SETTINGS_CANDIDATES, SYNC_FLEET_SCRIPT, SegmentEntry, SettingsSegmentEntry, SpliceConfig, TarExtractConfig, UPDATE_NOTIFIER_OPT_OUT_ENV, UntrackFleetPackConfig, WorkspaceSegmentEntry, YamlEntryBody, YamlEntryChunk, YamlKeyBlock, applyMovedPaths, assertLockStep, beginMarker, computeSha256, endMarker, errorMessage, extractFleetBlockLines, extractManifestFromTarball, fetchBlob, fetchBundleSource, fetchOciManifest, filterManifestForShape, findFleetBlockSpans, firstHeader, fleetPackOwnedPaths, formatLockStepError, formatUpdateNotice, getGhcrToken, ghcrBundleRepo, ghcrFetchBundle, ghcrTokenUrl, httpGet, installFiles, installFleet, installSegments, installSettingsSegment, installWorkspaceSegment, isBundleBehindLocalTemplate, isMainModule, listOciTags, lockStepExitCode, maybeShowUpdateNotice, mergeWorkspaceYaml, mergeYamlKeyBlock, nextTagPageUrl, normalizeBundlePath, normalizeManifestEntryPath, packBeginMarker, packEndMarker, packTemplateSha, parseArgs, parseWwwAuthenticate, parseYamlEntryChunks, parseYamlKeyBlocks, pickBundleLayer, printStatusReport, pruneStaleFleetFiles, pullFleetBundleTarball, readAppliedFiles, readAppliedRef, readBuildShape, readBundleConfig, readBundleRef, readManifest, readNoticeStore, refreshFleetPackIgnores, removeTombstonedPaths, resolveLockStepState, resolveNewestRef, resolveRepoRoot, resolveSettingsPath, run, runStatus, segmentFileName, sha256Hex, shouldShowNotice, spliceFleetBlock, splicePackBlock, spliceYamlSeparatorRun, statusJson, stripLegacyPackBlock, stripLegacyUntrackEntriesFromFleetBlock, tarExecutable, tarExtractArgs, tokenFromBody, untrackFleetPackPaths, untrackGeneratedOutputs, validateBundleBlock, validateCascadeSha, validateRef, verifyBundleFiles, verifySegments, wirePackageJson, writeAppliedFiles, writeAppliedRef, writeNoticeStore }; \ No newline at end of file +export { AuthChallenge, BundleConfig, BundleFetchFn, BundleManifest, ERR_BUNDLE_BEHIND_LOCAL, ERR_LOCKSTEP_MISMATCH, FLEET_STATUS_SCRIPT, FetchedBundle, FetchedFiles, FleetBlockSpan, FleetCommentStyle, FleetFileManifest, GHCR_HOST, GhcrHttpGetFn, GhcrHttpOptions, GhcrHttpResponse, HYBRID_BUNDLE_PATHS, InstallConfig, InstallFilesOptions, InstallFilesResult, LockStepConfig, LockStepErrorParts, LockStepInputs, LockStepState, LockStepStateName, MANIFEST_ACCEPT, MemberBuildShape, MergeWorkspaceConfig, NoticeDecisionInputs, NoticeStore, OciLayer, OciManifest, PREPARE_FETCH, PREPARE_FROM_TEMPLATE, PullBundleConfig, RefValidation, SETTINGS_CANDIDATES, SYNC_FLEET_SCRIPT, SegmentEntry, SettingsSegmentEntry, SpliceConfig, TarExtractConfig, UPDATE_NOTIFIER_OPT_OUT_ENV, UntrackFleetPackConfig, WorkspaceSegmentEntry, YamlEntryBody, YamlEntryChunk, YamlKeyBlock, applyMovedPaths, assertLockStep, beginMarker, computeSha256, endMarker, errorMessage, extractFleetBlockLines, extractManifestFromTarball, fetchBlob, fetchBundleSource, fetchOciManifest, filterManifestForCapabilities, filterManifestForShape, findFleetBlockSpans, firstHeader, fleetPackOwnedPaths, formatLockStepError, formatUpdateNotice, getGhcrToken, ghcrBasicAuthHeader, ghcrBundleRepo, ghcrFetchBundle, ghcrTokenUrl, httpGet, installFiles, installFleet, installSegments, installSettingsSegment, installWorkspaceSegment, isBundleBehindLocalTemplate, isMainModule, listOciTags, lockStepExitCode, materializeFromLocalTemplate, maybeShowUpdateNotice, mergeWorkspaceYaml, mergeYamlKeyBlock, nextTagPageUrl, normalizeBundlePath, normalizeManifestEntryPath, packBeginMarker, packEndMarker, packTemplateSha, parseArgs, parseWwwAuthenticate, parseYamlEntryChunks, parseYamlKeyBlocks, pickBundleLayer, printStatusReport, pruneStaleFleetFiles, pullFleetBundleTarball, readAppliedFiles, readAppliedRef, readBuildShape, readBundleConfig, readBundleRef, readDeclaredCapabilities, readManifest, readNoticeStore, refreshFleetPackIgnores, removeTombstonedPaths, resolveLockStepState, resolveNewestRef, resolveRepoRoot, resolveSettingsPath, run, runStatus, segmentFileName, sha256Hex, shouldShowNotice, spliceFleetBlock, splicePackBlock, spliceYamlSeparatorRun, statusJson, stripLegacyPackBlock, stripLegacyUntrackEntriesFromFleetBlock, tarExecutable, tarExtractArgs, tokenFromBody, untrackFleetPackPaths, untrackGeneratedOutputs, validateBundleBlock, validateCascadeSha, validateRef, verifyBundleFiles, verifySegments, wirePackageJson, writeAppliedFiles, writeAppliedRef, writeNoticeStore }; \ No newline at end of file diff --git a/scripts/repo/bootstrap/fleet.mjs b/scripts/repo/bootstrap/fleet.mjs index 876ce40a8..0e739200e 100644 --- a/scripts/repo/bootstrap/fleet.mjs +++ b/scripts/repo/bootstrap/fleet.mjs @@ -6,6 +6,7 @@ import { mkdirSync, mkdtempSync, readFileSync, + readdirSync, realpathSync, renameSync, rmSync, @@ -13,7 +14,7 @@ import { writeFileSync, } from 'node:fs' import os from 'node:os' -import path from 'node:path' +import path, { dirname, resolve, sep } from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' import crypto from 'node:crypto' @@ -41,22 +42,55 @@ function getDep0Logger() { return dep0Logger } /** - * Fail-open recursive delete. The dep-0 fetcher cannot import the lib - * `safeDeleteSync`, so it wraps node's `rmSync` with the same force + recursive - * fail-open semantics: a missing path is a no-op, never a throw. + * Whether `candidate` sits strictly INSIDE `root` - a descendant, never `root` + * itself and never above it. + * + * The prune walk builds its target with `path.join(dest, rel)` where `rel` + * comes from a state file on disk. `path.join(dest, '.')` is `dest`, and + * `path.join(dest, '..')` is its parent, so a single stray line in that record + * turns a per-file prune into a recursive delete of the checkout or of the + * directory holding it. Comparing resolved paths is the only check a caller + * cannot get wrong. + */ +function isInsidePath(root, candidate) { + const resolvedRoot = resolve(root) + const resolvedCandidate = resolve(candidate) + if (resolvedCandidate === resolvedRoot) return false + return resolvedCandidate.startsWith(`${resolvedRoot}${sep}`) +} +/** + * Fail-open recursive delete, CONTAINED to `root`. The dep-0 fetcher cannot + * import the lib `safeDeleteSync`, so it wraps node's `rmSync` with the same + * force + recursive fail-open semantics: a missing path is a no-op, never a + * throw. + * + * `root` is required and not optional on purpose. This deletes recursively with + * force, so the one thing every caller must state is the boundary it may not + * cross. A target outside `root` throws instead of deleting: the alternative is + * a warning nobody reads about a tree that is already gone. * * A read-only target gets ONE retry after a chmod +w. The installer locks the - * files it places (0444/0555), and Windows refuses to unlink a read-only file — + * files it places (0444/0555), and Windows refuses to unlink a read-only file - * POSIX does not, it checks the parent directory, which the lock never touches. */ -function rm(targetPath) { +function rm(targetPath, root) { + if (!isInsidePath(root, targetPath)) + throw new Error( + `refusing to delete outside the install root.\n Where: ${resolve(targetPath)}\n Saw: a target that is not a descendant of ${resolve(root)}\n Fix: this is a bug in the caller - a prune entry resolved to the root or above it. Report the manifest or applied-files line that produced it.`, + ) + rmForce(targetPath) +} +/** + * The unguarded force delete, for a path this module minted itself. + */ +function rmForce(targetPath) { try { rmSync(targetPath, { force: true, recursive: true, }) } catch (e) { - const code = errorCode(e) + const code = errorCode$1(e) if (code !== 'EACCES' && code !== 'EPERM') throw e chmodSync(targetPath, (statSync(targetPath).mode & 511) | 128) rmSync(targetPath, { @@ -69,7 +103,7 @@ function rm(targetPath) { * The `errno` string of a thrown filesystem error (`EACCES`, `EPERM`, …), or * undefined for anything that is not one. Dep-0: no lib `isErrnoException`. */ -function errorCode(e) { +function errorCode$1(e) { if (e instanceof Error) { const { code } = e return code @@ -86,6 +120,7 @@ const dep0Logger = { //#endregion //#region scripts/repo/gen/bootstrap/src/helpers.mts +const HYBRID_BUNDLE_PATHS = /* @__PURE__ */ new Set(['.gitignore', 'CLAUDE.md']) /** * Normalize bundle-manifest paths to their portable `/` wire format. */ @@ -378,6 +413,23 @@ function readBuildShape(dest) { } } } +/** + * The member's declared capabilities — the `capabilities` map in its + * wheelhouse settings file (an empty or ABSENT map declares NONE, matching + * the cascade-side gate). Drives the manifest's capability-scoped hook + * groups: a `@capability`-tagged hook is placed only when the member + * declares the capability. + */ +function readDeclaredCapabilities(dest) { + const p = resolveSettingsPath(dest) + if (!p) return [] + try { + const json = JSON.parse(readFileSync(p, 'utf8')) + return Object.keys(json.capabilities ?? {}) + } catch { + return [] + } +} /** * Read the member's full pinned `bundle` block (ref + cascadeSha) from the * wheelhouse settings file. The lock-step verify + the `fleet:status` verb need @@ -538,12 +590,14 @@ const ALWAYS_TRACKED_GITHUB_PREFIXES = [ '.github/actions/fleet/cache-pnpm-store/', '.github/actions/fleet/checkout/', '.github/actions/fleet/debug/', + '.github/actions/fleet/download-artifact/', '.github/actions/fleet/expose-actions-runtime/', '.github/actions/fleet/github-payload-app-token/', '.github/actions/fleet/github-status-check/', '.github/actions/fleet/install/', '.github/actions/fleet/setup-and-install/', '.github/actions/fleet/setup/', + '.github/actions/fleet/upload-artifact/', '.github/dependabot.yml', '.github/workflows/', ] @@ -644,6 +698,101 @@ function computeHybridPaths(manifest) { return hybridPaths } +//#endregion +//#region scripts/repo/gen/bootstrap/src/local-template-manifest.mts +const PACKAGE_MANAGER_DIRS = /* @__PURE__ */ new Set(['.venv', 'node_modules']) +/** + * Every regular file beneath `dir`, as paths relative to `dir`, skipping any + * package-manager directory. Bare-node walk: this module is dep-0 and must not + * reach for a glob library. + */ +function walkFilesRelative(dir, prefix, out) { + let entries + try { + entries = readdirSync(dir, { withFileTypes: true }) + } catch { + return + } + for (let i = 0, { length } = entries; i < length; i += 1) { + const entry = entries[i] + const rel = prefix ? `${prefix}/${entry.name}` : entry.name + if (entry.isDirectory()) { + if (PACKAGE_MANAGER_DIRS.has(entry.name)) continue + walkFilesRelative(path.join(dir, entry.name), rel, out) + } else if (entry.isFile()) out.push(rel) + } +} +/** + * Every hybrid path the expansion must leave alone: what the manifest declares + * as a segment, plus the static mirror in `helpers.mts`. + * + * The mirror is load-bearing rather than belt-and-braces. A manifest built for + * a LOCAL template carries no `segments` at all - the segment list is written + * by the release-bundle producer - so a manifest-only check finds nothing to + * skip on exactly the path where the clobber happens. + */ +function hybridBundlePaths(manifest) { + const hybrids = computeHybridPaths(manifest) + for (const rel of HYBRID_BUNDLE_PATHS) hybrids.add(normalizeBundlePath(rel)) + return hybrids +} +/** + * Expand a manifest into one entry per FILE that `filesDir` actually carries. + * + * Four shapes need handling, and only the first is one `installFiles` already + * deals with: + * + * - A file entry with a source: kept as-is. + * - A DIRECTORY entry: expanded into every file beneath it, each inheriting the + * directory's flags. 39 of the manifest's entries are whole-tree mirror roots + * (`scripts/fleet`, `.claude/hooks/fleet`, `docs/agents.md/fleet`) and they + * are the bulk of the payload. Expanding rather than special-casing keeps the + * always-tracked skip, the canonical splice and the per-file read-only lock + * all applying, with no second placement path to drift from the first. + * - An entry with NO source: dropped. The manifest describes every shape the + * fleet can deliver, including conditional entries seeded by other fixers + * (`.cargo/config.darwin-signing.toml` under `hasRust`); 94 of them have no + * template source here. + * - A HYBRID entry: dropped. Its live copy is half member-owned - the cascade + * splices the fleet block in and the repo keeps its own cutouts - so the + * template holds only one of the two halves, and copying it over the live + * file silently drops the other. `.gitignore` is the costly case: its repo + * region carries the mirror-untrack block, so one `--from-template` + * materialize re-tracked 2,973 mirrors and left a tree that read as clean. + * The cascade's block splicer owns these files; a whole-file copy never + * does. + */ +function expandManifestForLocalTemplate(filesDir, manifest) { + const files = Object.create(null) + const hybrids = hybridBundlePaths(manifest) + const rels = Object.keys(manifest.files) + for (let i = 0, { length } = rels; i < length; i += 1) { + const rel = rels[i] + const entry = manifest.files[rel] + if (hybrids.has(normalizeBundlePath(rel))) continue + const source = path.join(filesDir, normalizeBundlePath(rel)) + let stat + try { + stat = statSync(source) + } catch { + continue + } + if (stat.isFile()) { + files[rel] = entry + continue + } + if (!stat.isDirectory()) continue + const nested = [] + walkFilesRelative(source, '', nested) + for (let j = 0, { length: nestedLength } = nested; j < nestedLength; j += 1) + files[`${rel}/${nested[j]}`] = entry + } + return { + ...manifest, + files, + } +} + //#endregion //#region scripts/repo/gen/bootstrap/src/placement-lock.mts /** @@ -676,7 +825,7 @@ function ensureWritableTarget(target) { chmodSync(target, mode | 128) } catch { /* c8 ignore start - chmod on a file this process owns only fails under root or an OS immutable flag (macOS chflags uchg), so a portable unit test cannot reach this fallback. */ - rm(target) + rm(target, dirname(target)) } } /** @@ -1168,7 +1317,7 @@ function applyMovedPaths(dest, manifest) { const fromAbs = path.join(dest, from) if (!existsSync(fromAbs)) continue const toAbs = path.join(dest, to) - if (existsSync(toAbs)) rm(fromAbs) + if (existsSync(toAbs)) rm(fromAbs, dest) else { mkdirSync(path.dirname(toAbs), { recursive: true }) renameSync(fromAbs, toAbs) @@ -1200,7 +1349,7 @@ function removeTombstonedPaths(dest, manifest) { continue const abs = path.join(dest, rel) if (existsSync(abs)) { - rm(abs) + rm(abs, dest) removed += 1 } } @@ -1232,7 +1381,7 @@ function pruneStaleFleetFiles(dest, manifest, previousFiles) { if (kept.has(rel)) continue const abs = path.join(dest, rel) if (existsSync(abs)) { - rm(abs) + rm(abs, dest) pruned += 1 } } @@ -1302,6 +1451,39 @@ function installFiles(filesDir, dest, manifest, options) { refreshedTracked, } } +/** + * Materialize the fleet mirrors in a PRODUCER checkout from its own + * `template/base`, rather than from a fetched bundle. + * + * The wheelhouse holds the canon locally, so it has no bundle to fetch and is + * not a fleet-pack consumer. That is the only reason its mirrors stayed in + * version control: nothing else could put them back. Producing the payload does + * not require tracking the output, so this is the producer's belt. + * + * Why it must live in this dep-0 entry and not in the cascade: the cascade + * cannot load without the payload it would be materializing. + * `template/base/scripts/fleet/land-work.mts` and its siblings import the LIVE + * `.claude/hooks/fleet/_shared/**`, so a checkout whose mirrors are absent dies + * at module resolution before any fixer runs. Same reason the fetcher cannot + * ship inside the bundle it fetches. + * + * Returns undefined when `template/base` is absent, which is every consumer: + * the caller then knows this checkout is not a producer and fetches instead. + */ +function materializeFromLocalTemplate(dest, manifest, options) { + const filesDir = path.join(dest, 'template', 'base') + if (!existsSync(filesDir)) return + const shaped = filterManifestForCapabilities( + filterManifestForShape(manifest, readBuildShape(dest)), + readDeclaredCapabilities(dest), + ) + return installFiles( + filesDir, + dest, + expandManifestForLocalTemplate(filesDir, shaped), + options, + ) +} /** * Untrack the bundle's GENERATED build outputs (`manifest.generatedPaths`) * from the git index after placement. The bundle SHIPS these files — placement @@ -1434,6 +1616,14 @@ function installWorkspaceSegment(segmentsDir, dest, manifest) { } const SYNC_FLEET_SCRIPT = 'node scripts/repo/bootstrap/fleet.mjs' const PREPARE_FETCH = 'node scripts/repo/bootstrap/prepare.mts' +/** + * The PRODUCER belt: materialize the mirrors from this checkout's own + * `template/base` instead of fetching a bundle. The wheelhouse's counterpart to + * PREPARE_FETCH, and it runs in the same slot for the same reason — the + * git-hooks installer it precedes is itself one of the untracked mirrors. + */ +const PREPARE_FROM_TEMPLATE = + 'node scripts/repo/bootstrap/fleet.mjs --from-template' const FLEET_STATUS_SCRIPT = 'node scripts/repo/bootstrap/fleet.mjs --status' /** * Wire the consumer's package.json for thin distribution: a `sync-fleet` script @@ -1489,6 +1679,35 @@ function normalizeManifestEntryPath(entry) { * config), returns the manifest untouched — a config problem must never * withhold payload. */ +/** + * Drop the manifest's capability-scoped hook payloads the member does not + * declare, so a `@capability cargo` hook never lands in a repo with no cargo + * capability — the pack-side twin of the cascade's dirMirrorSkipPredicate + * capability gate. Fails OPEN on an unknown capabilities read (absent or + * malformed settings file): a config problem must never withhold payload. + * The prune sees the same filtered set, so a wrongly placed copy heals on + * the next fetch. + */ +function filterManifestForCapabilities(manifest, capabilities) { + const groups = manifest.capabilityScopedFiles + if (!groups?.length || capabilities === void 0) return manifest + const declared = new Set(capabilities) + const excluded = /* @__PURE__ */ new Set() + for (let i = 0, { length } = groups; i < length; i += 1) { + const group = groups[i] + if (declared.has(group.capability)) continue + for (let j = 0, { length: flen } = group.files; j < flen; j += 1) + excluded.add(normalizeBundlePath(group.files[j])) + } + if (!excluded.size) return manifest + const files = {} + for (const { 0: rel, 1: hash } of Object.entries(manifest.files)) + if (!excluded.has(normalizeBundlePath(rel))) files[rel] = hash + return { + ...manifest, + files, + } +} function filterManifestForShape(manifest, shape) { const groups = manifest.shapeScopedFiles if (!groups?.length || shape.from === void 0) return manifest @@ -1931,6 +2150,124 @@ function formatUpdateNotice(config) { return [top, ...lines.map(l => `│ ${l.padEnd(width)} │`), bottom].join('\n') } +//#endregion +//#region scripts/repo/gen/bootstrap/src/network-errors.mts +const TLS_CODES = [ + 'CERT_HAS_EXPIRED', + 'DEPTH_ZERO_SELF_SIGNED_CERT', + 'ERR_TLS_CERT_ALTNAME_INVALID', + 'SELF_SIGNED_CERT_IN_CHAIN', + 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', +] +/** + * Read the `code` off an unknown throwable. Pure, and tolerant: a rejected + * promise can carry a string, an AggregateError, or nothing useful at all. + */ +function errorCode(error) { + if (typeof error !== 'object' || error === null) return '' + const code = error.code + if (typeof code === 'string') return code + const errors = error.errors + if (Array.isArray(errors) && errors.length > 0) return errorCode(errors[0]) + return '' +} +/** + * Classify a transport failure. Pure over the error, so every branch is + * testable without a socket. + */ +function classifyNetworkError(error) { + const code = errorCode(error) + if (TLS_CODES.includes(code)) + return { + code, + kind: 'tls', + retryable: false, + } + switch (code) { + case 'ENOTFOUND': + return { + code, + kind: 'dns', + retryable: false, + } + case 'EAI_AGAIN': + return { + code, + kind: 'dns', + retryable: true, + } + case 'ECONNREFUSED': + return { + code, + kind: 'refused', + retryable: false, + } + case 'ETIMEDOUT': + case 'ESOCKETTIMEDOUT': + case 'UND_ERR_CONNECT_TIMEOUT': + return { + code, + kind: 'timeout', + retryable: true, + } + case 'ECONNRESET': + case 'EPIPE': + return { + code, + kind: 'reset', + retryable: true, + } + default: + return { + code, + kind: 'unknown', + retryable: false, + } + } +} +/** + * The action most likely to clear each failure kind. One line, imperative, and + * specific enough to run. + */ +function fixFor(failure, host) { + switch (failure.kind) { + case 'dns': + return failure.retryable + ? 'run the same command again; the resolver was briefly unavailable.' + : `confirm you are online and that ${host} resolves (\`nslookup ${host}\`). Behind a split-DNS VPN, connect it first.` + case 'refused': + return `something rejected the connection to ${host} rather than the registry refusing it — check an HTTP(S)_PROXY setting or a firewall rule.` + case 'reset': + return 'run the same command again; the connection dropped mid-transfer.' + case 'timeout': + return 'run the same command again; if it repeats, check whether a proxy is intercepting the connection.' + case 'tls': + return 'the certificate chain did not verify. Inside the sandbox, point NODE_EXTRA_CA_CERTS at the persistent sfw CA (`pnpm run setup:sfw-ca`); never disable TLS verification to get past this.' + default: + return 'run the same command again; if it repeats, report the code above with the URL.' + } +} +/** + * The fail-loud message for a fetch that could not complete: what broke, where, + * what was seen against what was wanted, and the fix. Says outright whether a + * retry is worth it, so nobody has to guess from an errno. + */ +function networkFailureMessage(config) { + const cfg = { + __proto__: null, + ...config, + } + const failure = classifyNetworkError(cfg.error) + let host = cfg.url + try { + host = new URL(cfg.url).host + } catch {} + const detail = + cfg.error instanceof Error ? cfg.error.message : String(cfg.error) + const saw = failure.code ? `${failure.code} — ${detail}` : detail + return `${cfg.what} could not reach ${host}.\n Where: ${cfg.url}\n Saw: ${saw}\n Wanted: an HTTP response from ${host}\n Retry: ${failure.retryable ? 'yes, this is transient' : 'no, the same attempt fails the same way'}\n Fix: ${fixFor(failure, host)}` +} + //#endregion //#region scripts/repo/gen/bootstrap/src/ghcr-fetch.mts const GHCR_HOST = 'ghcr.io' @@ -1943,6 +2280,7 @@ const MANIFEST_ACCEPT = [ 'application/vnd.docker.distribution.manifest.list.v2+json', ].join(', ') const MAX_REDIRECTS = 5 +const REQUEST_TIMEOUT_MS = 3e4 /** * Read the first value of a possibly-array HTTP header. */ @@ -1959,36 +2297,54 @@ function httpGet(url, options) { } function httpGetWithRedirects(url, headers, redirectCount) { return new Promise((resolve, reject) => { - https - .get(url, { headers }, res => { - const status = res.statusCode ?? 0 - const location = firstHeader(res.headers['location']) - if ( - status >= 300 && - status < 400 && - location && - redirectCount < MAX_REDIRECTS - ) { - res.resume() - const nextUrl = new URL(location, url).toString() - const nextHeaders = Object.create(null) - for (const key of Object.keys(headers)) - if (key.toLowerCase() !== 'authorization') - nextHeaders[key] = headers[key] - resolve(httpGetWithRedirects(nextUrl, nextHeaders, redirectCount + 1)) - return - } - const chunks = [] - res.on('data', chunk => chunks.push(chunk)) - res.on('end', () => { - resolve({ - body: Buffer.concat(chunks), - headers: res.headers, - status, - }) + const req = https.get(url, { headers }, res => { + const status = res.statusCode ?? 0 + const location = firstHeader(res.headers['location']) + if ( + status >= 300 && + status < 400 && + location && + redirectCount < MAX_REDIRECTS + ) { + res.resume() + const nextUrl = new URL(location, url).toString() + const nextHeaders = Object.create(null) + for (const key of Object.keys(headers)) + if (key.toLowerCase() !== 'authorization') + nextHeaders[key] = headers[key] + resolve(httpGetWithRedirects(nextUrl, nextHeaders, redirectCount + 1)) + return + } + const chunks = [] + res.on('data', chunk => chunks.push(chunk)) + res.on('end', () => { + resolve({ + body: Buffer.concat(chunks), + headers: res.headers, + status, }) }) - .on('error', reject) + }) + req.setTimeout(REQUEST_TIMEOUT_MS, () => { + req.destroy( + Object.assign( + /* @__PURE__ */ new Error(`timed out after ${REQUEST_TIMEOUT_MS}ms`), + { code: 'ETIMEDOUT' }, + ), + ) + }) + req.on('error', e => { + reject( + new Error( + networkFailureMessage({ + error: e, + url, + what: 'install-fleet: fetching the fleet bundle', + }), + { cause: e }, + ), + ) + }) }) } /** @@ -2029,10 +2385,30 @@ function tokenFromBody(body) { } } /** - * Obtain an anonymous pull token. Hits the documented token endpoint first; on - * anything but a usable token, falls back to the 401 WWW-Authenticate challenge - * form (probe /v2/, follow the advertised realm). Fails loud when no token can - * be obtained. + * `Authorization: Basic` for GHCR's token endpoint, built from the workflow + * token when one is in the environment. + * + * A PUBLIC package needs none of this - anonymous pull is the common path and + * stays first. A package that is private, or newly published and not yet made + * public, answers the anonymous request with 403 and no token, which reads as + * "confirm the package is public" and is unactionable inside a job that already + * holds a credential for the same repo. GHCR accepts the workflow token as the + * password with any username. + * + * Returns undefined when no token is in the environment, so a local run keeps + * its anonymous behavior. Never logged: the value only ever becomes a header. + */ +function ghcrBasicAuthHeader(env) { + const token = env['GH_TOKEN'] || env['GITHUB_TOKEN'] + if (!token) return + return `Basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}` +} +/** + * Obtain a pull token. Hits the documented token endpoint first; on anything + * but a usable token, falls back to the 401 WWW-Authenticate challenge form + * (probe /v2/, follow the advertised realm), and finally retries the challenge + * WITH the workflow token when the environment carries one. Fails loud when no + * token can be obtained. */ async function getGhcrToken(repo, registry, httpFn = httpGet) { const primary = await httpFn(ghcrTokenUrl(repo, registry), { @@ -2057,10 +2433,24 @@ async function getGhcrToken(repo, registry, httpFn = httpGet) { const res = await httpFn(`${challenge.realm}?${params.toString()}`, { headers: { accept: 'application/json' }, }) - const token = tokenFromBody(res.body) + let token = tokenFromBody(res.body) + if (!token) { + const authorization = ghcrBasicAuthHeader(process.env) + if (authorization) + token = tokenFromBody( + ( + await httpFn(`${challenge.realm}?${params.toString()}`, { + headers: { + accept: 'application/json', + authorization, + }, + }) + ).body, + ) + } if (!token) - throw new Error(`Cannot obtain a GHCR anonymous pull token. - Where: ${challenge.realm} for repo ${repo}\n Saw: HTTP ${res.status} with no token in the body\n Fix: confirm the package is public and speaks the OCI token flow.`) + throw new Error(`Cannot obtain a GHCR pull token. + Where: ${challenge.realm} for repo ${repo}\n Saw: HTTP ${res.status} with no token in the body, anonymously or with the workflow token\n Fix: make the package public, or give the job a token with read:packages on it.`) return token } /** @@ -2573,6 +2963,7 @@ function parseArgs(argv) { ref: '', repo: DEFAULT_REPO, status: false, + fromTemplate: false, thin: false, wire: false, } @@ -2585,6 +2976,7 @@ function parseArgs(argv) { else if (arg === '--exit-code') opts.exitCode = true else if (arg === '--if-current') opts.ifCurrent = true else if (arg === '--json') opts.json = true + else if (arg === '--from-template') opts.fromTemplate = true else if (arg === '--manifest') opts.manifest = argv[++i] else if (arg === '--no-header') opts.noHeader = true else if (arg === '--quiet') opts.quiet = true @@ -2764,9 +3156,9 @@ async function installFleet(config) { return 1 } } - const memberManifest = filterManifestForShape( - manifest, - readBuildShape(dest), + const memberManifest = filterManifestForCapabilities( + filterManifestForShape(manifest, readBuildShape(dest)), + readDeclaredCapabilities(dest), ) const fileCount = Object.keys(memberManifest.files).length const segmentCount = @@ -2825,7 +3217,7 @@ async function installFleet(config) { ) return 0 } finally { - rm(tmp) + rm(tmp, os.tmpdir()) } } function isMainModule() { @@ -2837,11 +3229,54 @@ function isMainModule() { return false } } +/** + * The `--from-template` verb: materialize this checkout's fleet mirrors from + * its own `template/base`, then report what was placed. + * + * Exit 1 when the checkout carries no `template/base` — a consumer ran the + * producer verb, a wiring mistake worth failing on rather than silently + * no-opping into an unusable tree. + */ +function runFromTemplate(config) { + const dest = path.resolve(config.dest ?? repoRoot) + const manifestPath = path.join( + dest, + 'scripts', + 'repo', + 'sync-scaffolding', + 'manifest', + 'fleet-files.json', + ) + if (!existsSync(manifestPath)) { + logger.error( + `install-fleet: --from-template: no mirror manifest at ${manifestPath}.`, + ) + return 1 + } + const result = materializeFromLocalTemplate( + dest, + JSON.parse(readFileSync(manifestPath, 'utf8')), + { refreshTracked: config.refreshTracked }, + ) + if (result === void 0) { + logger.error( + 'install-fleet: --from-template: no template/base here — that verb is for the payload PRODUCER; a consumer fetches its bundle.', + ) + return 1 + } + if (!config.quiet) + logger.log( + `install-fleet: materialized ${result.placed} file(s) from template/base (${result.skippedAlwaysTracked} always-tracked left alone).`, + ) + return 0 +} if (isMainModule()) { const parsed = parseArgs(process.argv.slice(2)) process.exitCode = parsed.status ? await runStatus(parsed) - : await installFleet(parsed) + : parsed.fromTemplate + ? runFromTemplate(parsed) + : await installFleet(parsed) } //#endregion @@ -2850,8 +3285,10 @@ export { ERR_LOCKSTEP_MISMATCH, FLEET_STATUS_SCRIPT, GHCR_HOST, + HYBRID_BUNDLE_PATHS, MANIFEST_ACCEPT, PREPARE_FETCH, + PREPARE_FROM_TEMPLATE, SETTINGS_CANDIDATES, SYNC_FLEET_SCRIPT, UPDATE_NOTIFIER_OPT_OUT_ENV, @@ -2866,6 +3303,7 @@ export { fetchBlob, fetchBundleSource, fetchOciManifest, + filterManifestForCapabilities, filterManifestForShape, findFleetBlockSpans, firstHeader, @@ -2873,6 +3311,7 @@ export { formatLockStepError, formatUpdateNotice, getGhcrToken, + ghcrBasicAuthHeader, ghcrBundleRepo, ghcrFetchBundle, ghcrTokenUrl, @@ -2886,6 +3325,7 @@ export { isMainModule, listOciTags, lockStepExitCode, + materializeFromLocalTemplate, maybeShowUpdateNotice, mergeWorkspaceYaml, mergeYamlKeyBlock, @@ -2908,6 +3348,7 @@ export { readBuildShape, readBundleConfig, readBundleRef, + readDeclaredCapabilities, readManifest, readNoticeStore, refreshFleetPackIgnores, diff --git a/scripts/repo/bootstrap/prepare.mts b/scripts/repo/bootstrap/prepare.mts index 746b7f9a1..25cb95eb7 100644 --- a/scripts/repo/bootstrap/prepare.mts +++ b/scripts/repo/bootstrap/prepare.mts @@ -125,6 +125,21 @@ export function fetchBundle(): void { log('no scripts/repo/bootstrap/fleet.mjs beside me — skipping bundle fetch') return } + // The PRODUCER branch. A checkout carrying `template/base` holds the canon + // locally: there is no bundle to fetch and no pin to compare, so it + // materializes from its own template instead. Everything after this step — + // the pnpm-workspace repair and the reconcile install — is identical, and is + // exactly what a producer needs too: the mirrors it just placed include ~380 + // workspace package.json files that the first install could not see. + // Branching here rather than writing a second doctor keeps one code path. + if (existsSync(path.join(REPO_ROOT, 'template', 'base'))) { + if (!tryRun('node', [fleet, '--from-template'])) { + log( + 'materialize (fleet.mjs --from-template) reported a problem — continuing', + ) + } + return + } const pinnedRef = readPinnedRef(REPO_ROOT) const appliedRef = readAppliedRefLocal(REPO_ROOT) if (isAppliedRefCurrentOrNewer(pinnedRef, appliedRef)) { @@ -145,7 +160,7 @@ export function fetchBundle(): void { */ const SETTINGS_CANDIDATES_LOCAL = [ '.config/repo/socket-wheelhouse.json', - '.config/socket-wheelhouse.json', + '.config/socket-wheelhouse.json', // loose-config-ref: allow -- migration read '.socket-wheelhouse.json', ] as const @@ -401,46 +416,6 @@ export function reconcileInstall(): boolean { }) } -/** - * Step 4: regenerate the cross-tool `.agents/skills/` mirror when it is absent - * or drifted. The mirror is a git-untracked generated copy of - * `.claude/skills/{fleet,repo}/` (generator: - * `scripts/fleet/gen/agents-skills-mirror.mts`). Nothing else in the install - * path regenerates it, so a fresh clone + `pnpm install` leaves Codex/OpenCode - * skill discovery empty until a cascade or manual run. This closes that gap. - * - * Drift-gated: runs the generator's `--check` mode first (reads only, no - * membership gate), and regenerates only when drift is detected. Silent when - * `.claude/skills/` is absent (a thin member before hydration has nothing to - * mirror) or the generator script is missing. Fail-open: any error is - * swallowed so a `pnpm install` never breaks on it. - */ -export function regenSkillsMirrorIfDrifted(): void { - const claudeSkills = path.join(REPO_ROOT, '.claude', 'skills') - if (!existsSync(claudeSkills)) { - return - } - const generator = path.join( - REPO_ROOT, - 'scripts', - 'fleet', - 'gen', - 'agents-skills-mirror.mts', - ) - if (!existsSync(generator)) { - return - } - // --check exits 0 when the mirror is in sync, 1 when drifted. A spawn - // failure (missing node_modules, crash) also returns false from tryRun — in - // every non-zero case we try a regen, and if that fails too, we swallow it. - if (tryRun('node', [generator, '--check'])) { - return - } - if (!tryRun('node', [generator])) { - log('skills mirror regen reported a problem — continuing') - } -} - /** * Step 2: repair `pnpm-workspace.yaml` `packages:` to list the fleet dirs. */ @@ -490,7 +465,6 @@ export async function runPrepare(): Promise { log('reconcile `pnpm install --ignore-scripts` failed') return 1 } - regenSkillsMirrorIfDrifted() await maybeNotifyUpdate() return 0 }