diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0b85d2bdf..4b1d2356b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "perun", "description": "Perun: a suite of skills for building with AI agents. Deep code review, token and cost tools (spend reports, auto-compact with handoff, model-tier guidance), gated agentic delivery (merge train, lanes, destructive-command guard, auto-update), an idea critic, and product, growth, positioning, business-ops and output-safety specialists.", "skills": "./.claude/skills", - "version": "1.628.0", + "version": "1.629.0", "author": { "name": "deep-code-review contributors" }, diff --git a/.claude/skills/agentic-delivery/SKILL.md b/.claude/skills/agentic-delivery/SKILL.md index 8aa79109a..1ceacd82f 100644 --- a/.claude/skills/agentic-delivery/SKILL.md +++ b/.claude/skills/agentic-delivery/SKILL.md @@ -8,7 +8,7 @@ description: >- license: MIT metadata: author: deep-code-review contributors - version: "1.628.0" + version: "1.629.0" --- # Agentic delivery diff --git a/.claude/skills/agentic-delivery/VERSION b/.claude/skills/agentic-delivery/VERSION index 415cdbbbb..629194afd 100644 --- a/.claude/skills/agentic-delivery/VERSION +++ b/.claude/skills/agentic-delivery/VERSION @@ -1 +1 @@ -1.628.0 +1.629.0 diff --git a/.claude/skills/deep-code-review/SKILL.md b/.claude/skills/deep-code-review/SKILL.md index a55e4a255..57420e92c 100644 --- a/.claude/skills/deep-code-review/SKILL.md +++ b/.claude/skills/deep-code-review/SKILL.md @@ -8,7 +8,7 @@ description: >- license: MIT metadata: author: deep-code-review contributors - version: "1.628.0" + version: "1.629.0" --- # Deep Code Review diff --git a/.claude/skills/deep-code-review/VERSION b/.claude/skills/deep-code-review/VERSION index 415cdbbbb..629194afd 100644 --- a/.claude/skills/deep-code-review/VERSION +++ b/.claude/skills/deep-code-review/VERSION @@ -1 +1 @@ -1.628.0 +1.629.0 diff --git a/.claude/skills/idea-critic/SKILL.md b/.claude/skills/idea-critic/SKILL.md index 1dfce3a52..5e487f7a6 100644 --- a/.claude/skills/idea-critic/SKILL.md +++ b/.claude/skills/idea-critic/SKILL.md @@ -8,7 +8,7 @@ description: >- license: MIT metadata: author: deep-code-review contributors - version: "1.628.0" + version: "1.629.0" --- # Veles: the evil twin (idea-critic) diff --git a/.claude/skills/idea-critic/VERSION b/.claude/skills/idea-critic/VERSION index 415cdbbbb..629194afd 100644 --- a/.claude/skills/idea-critic/VERSION +++ b/.claude/skills/idea-critic/VERSION @@ -1 +1 @@ -1.628.0 +1.629.0 diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index c42b088ac..8d8dff67b 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "perun", - "version": "1.628.0", + "version": "1.629.0", "description": "Perun: a skill suite for AI-agent teams: deep code review, token and cost tools, gated delivery, an idea critic and product specialists.", "skills": "./.claude/skills", "interface": { diff --git a/CHANGELOG.md b/CHANGELOG.md index d26dc4b42..efb6ad540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,35 @@ All notable changes to this repository are documented here. Format loosely follows Keep a Changelog; versioning follows Semantic Versioning. +## [1.629.0] — 2026-10-31 + +### Fixed +- The `install.sh` end-of-run summary no longer lists a running session's transcripts (`.claude/projects/`), its todos (`.claude/todos/`) and other files outside the paths install writes as "Generated". It found the files a run wrote by scanning whole host dirs (`.claude`, `.cursor`, `.agents` and the other hosts) for anything newer than the run's start stamp, so under a home target those files appeared as if install had created them. It now scans only the subpaths install itself writes: the installed skill dirs, each host's `skills/INDEX.md`, `.claude/commands`, `.claude/agents/delivery-lane.md`, `.claude/house-defaults.md`, the install record and flags files, `settings.local.json` and the `.new` and `.bak` siblings of the settings files, the CI-gate files and their `.new` siblings, `.perun/policy.json` and `AGENTS.md`. The rows for a project target are otherwise unchanged; the one difference is that `.github/skills/INDEX.md` and `.github/workflows/dcr-gates.yml` no longer print twice with `--with-extra-hosts --with-gates`, because `.github` used to be a scan root twice. The summary still lists another process's write to a path it does scan: a new file in `.claude/commands` (scanned whole) or an edit to `.claude/settings.local.json` during the run; an exact per-file record of what install wrote would lift that limit. +- `scripts/test-install-hygiene.sh` gains 6 cases (15 in all). Two plant files with a 2099 mtime, one each in `.claude/projects/` and `.claude/todos/`, and a generic stray in every host dir and in `.perun`, `.githooks`, `scripts` and `.github/workflows`, and assert none of them appear in the summary (both fail on the previous `install.sh`). One asserts that every file a `--full --with-gates --with-terse-replies --with-extra-hosts --with-codex` install wrote, the policy and settings files included, still appears, and one asserts a `(N files)` row for every installed skill dir. Two install into an existing target with edited gate files, a `settings.local.json` and a stale operating-layer `.new`, and assert the `.new`, `.new-1`, `.bak` and local-flags rows appear, so dropping a trailing `*` from a scan root fails. The installs run with `PERUN_ROLE` unset. + +### Fixed +- `vendor_drift.py` no longer reports a clean install as drift. A canonical file whose first line says "(install placeholder)" is compared as `docs/` under the canonical source root, the file `install.sh` copies over it. When that docs file is missing, the stub itself is the reference, so an installed full copy reads as drift (fail closed). Before, the check `update-installed.sh` runs after an update flagged the two placeholder reference files on every clean install. +- A version bump of an unedited install no longer leaves a skill backup. `install.sh` now also drops the fresh backup when it equals what the marker says the previous install wrote (read before the new install rewrites the marker), next to the existing "identical to the fresh copy" rule. Only bytecode under `__pycache__` is ignored in that comparison, like `hash_dir`. An edited or extended copy, a pre-Perun copy (no marker entry), a backup holding any other file under `__pycache__` or any entry the previous install did not write (an empty directory, a FIFO), a symlink anywhere in it, and an unreadable marker all keep the backup. Deletion still goes through the existing per-file walk, never a shell `rm` on a computed path. +- Slash commands and `--with-gates` outputs no longer pile up `.new` copies. `write_gate_file` writes nothing when the destination, or an existing `.new` / `.new-N` next to it, already holds the shipped file, so an identical re-run no longer adds `.new-1`, `.new-2` and so on. A command that install wrote and nobody edited (its sha256 still matches the marker) is refreshed in place on a version bump and re-recorded, so uninstall still removes it. Install deletes no file it did not create in that run, so a `.new` / `.new-N` already next to it stays. Only a regular file named exactly `.new` or `.new-N` counts as already holding the shipped file; a symlink or a lookalike name never does. An edited command, a pre-Perun file and any `.new` that differs from the destination are unchanged: the file is never touched and a new `.new` is written. `--with-gates` outputs are not recorded in the marker, so they are never refreshed in place: an unedited one still gets a `.new` when its shipped template changes. +- `scripts/test_install_update_hygiene.py` (registered in `scripts/ci-tests.d/install-update-hygiene.sh`) covers all three on a fictional release source in a scratch directory with a scratch `HOME`: consecutive version bumps add no backup and no `.new`, while an edited skill, a pre-Perun copy, an edited command, a lookalike `.new-` name, a symlinked `.new`, an empty directory in a backup, a pre-existing `.new` of any kind and a differing `.new` are all kept. The existing `scripts/test_install_commands.py::test_existing_file_kept_new_written_and_uninstall_keeps_user_file` pinned the old behavior of a second identical `review.md.new-1` and now expects a single `review.md.new`. + +### Fixed +- The machine-wide heavy-job throttle and the exclusive train lease were inert inside a Claude Code sandbox: `perun_policy.py heavy-acquire` could not create its lease file under the read-only user cache dir, exited 2, and `review_checks.sh` then ran its scanners unleased, so no sandboxed session ever left a lease for the others to count. On macOS only, writers (`heavy-acquire`, `heavy-release`, the `heavy-exclusive*` commands and `stage_run.py`) now use the first writable of the usual lease dir and a per-user fallback dir under the system temp dir, and every reader (the slot count, the exclusive holder, the `heavy_gate.py` hook and `heavy-load`) checks both, deduplicated by real path. The fallback is used only when its root, its `perun` dir and its `heavy` dir are real directories (not symlinks) owned by the current user and the root has mode 0700; otherwise it is skipped and behaviour is as before. On other platforms nothing changes. `PERUN_HEAVY_DIR` still pins exactly one dir; `PERUN_HEAVY_FALLBACK_ROOT` moves the fallback root for tests. +- Every lease, holder-file and queue-marker read now goes through `perun_policy.read_lease`, which opens without blocking or following a symlink and skips anything that is not a regular file, so a FIFO planted in a lease dir can no longer hang a read: the `heavy_gate.py` hook, `heavy-load`, the slot and queue checks of `heavy-acquire`, and `stage_run.py`. `stage_run.py` still drops a stale lease it finds outside the primary lease dir, but never signals the process group such a lease names. +- Accepted limit, documented in `perun_policy._fallback` (the forged holder is pinned by a test): every sandboxed session of the same user can write the shared fallback dir, so one of them can forge a holder (a lease or `heavy.exclusive` naming a live pid) and make the hook and the train gate delay or deny heavy commands, or plant a FIFO where a sandboxed writer is about to create its temp or `.waiting` file and stall that writer. That is a same-user delay, not a way to run or signal anything. +- `scripts/test_lease_sandbox_fallback.py` (registered in `scripts/ci-tests.d/lease-sandbox-fallback.sh`): fixtures only, never the real temp dir; the fallback cases run on macOS only. It covers an unwritable primary falling back and releasing, a fallback holder counting against the slots of a process whose primary is writable, an exclusive fallback holder blocking acquire, the hook seeing fallback holders, a symlinked, foreign-owned or loosely moded fallback being refused, and a pinned dir never using the fallback. +- `scripts/test_lease_sandbox_hardening.py` (registered in `scripts/ci-tests.d/lease-sandbox-hardening.sh`): FIFOs in a pinned dir and in the fallback never block the hook, `heavy-load`, `heavy-acquire`, `stage_run.py` or the train gate (every subprocess has a timeout), `read_lease` rejects symlinks, directories and FIFOs without leaking a descriptor, no fallback is read, written or created off macOS, a symlinked or foreign-owned `perun` or `heavy` component is refused, a writable lease dir under a read-only parent falls back, a forged fallback holder delays as documented, and the orphan reap never signals a group named by a lease outside the primary dir. +- `scripts/test_prepush_gate_cache.py` pins `PERUN_HEAVY_DIR` so the real `stage_run.py` it runs never counts leases from the machine-wide fallback dir. + +### Fixed +- `review_checks.sh` diff mode: a row's `"new"` flag (`true` when its file and line are on a line `REF...HEAD` adds, else `false`) is now `null`, meaning unknown, for every row in a file that differs from `HEAD` in the working tree, whether edited (staged or not) or untracked. Most rows are read from the working tree while the flag comes from the committed diff, so a `true` or `false` there could name the wrong line. `--full` and `--history` still omit the key and no row is dropped. With K null rows the stderr count line reads `N of T rows are on lines this diff adds, K unknown (file differs from HEAD)`; with none it is unchanged. The files come from `git --no-optional-locks status --porcelain -z --no-renames -uall`: it leaves `.git/index` alone (`git diff HEAD` rewrote it for a file that only changed its mtime, even with that flag) and takes no revision, so a root file named `HEAD` cannot break it. If the read fails, `"new"` is omitted from every row and stderr says `review_checks: could not list files that differ from HEAD; "new" omitted`, where it used to vanish silently. Ignored untracked files and `assume-unchanged` edits are not listed. `references/method-situational.md` documents the third value. +- `scripts/test-review-checks-new-rows.sh` gains 16 cases (32 in all): an unstaged edit, a staged edit to a path with a space, an untracked file named by a fake `tsc`, the clean files that keep `true` or `false` (one the diff does not touch), the exact count of null rows and the count line, `.git/index` left alone after a mtime-only change, a root file named `HEAD`, and an unreadable index. Eight of them fail on the script before this change. + +### Added +- deep-code-review `scripts/route_auth_map.py DIR`: report-only, stdlib-only lead-finder for missing authentication (CWE-306/862). It inventories every HTTP handler (Next.js `app/**/route` exports, `pages/api/**`, `"use server"` exports, Express/Router/Hono/Fastify `.get/.post/...` calls, FastAPI and Flask decorators) and prints one `path:line: SEV: METHOD route auth=` row each. Evidence is an identity call (editable `IDENTITY` list), a Next `middleware.ts`/`proxy.ts` whose matcher covers the route (the file names an identity check, or can answer 401/403 under any name, so a custom-named session gate counts; a quoted `/path` elsewhere in the file is read as an allow-list entry, so that route and what is under it read none (a trailing `(.*)`, `/*` or `/:name*` is dropped, so a Clerk-style `createRouteMatcher(["/sign-in(.*)"])` list counts; only a route that starts with the literal is taken out, so `includes("/hook")` or `endsWith("/hook")` leaves `/api/hook` reading `middleware`), unless the literal is a prefix of a matcher entry or sits in the condition of an `if` whose body can refuse (a scope check such as `if (p.startsWith("/api/")) { return <401> }`, nested refusals included; a braceless body runs over a line break when the next line starts with `.`, `?`, `:`, `&&` or `||` or this one ends in `?`, `:`, `&&` or `||`); conditions are not evaluated, so a negated refusal reads its path as gated and a literal held in a collection or variable is read as an exemption wherever it is used; a scope check written as a ternary, an `&&` chain, a `switch` `case` or a body that only redirects to a login page is no `if` with a refusing body, so its literal reads as an exemption and the route it gates reads none; a regex or computed allow-list is not read, and a 401/403 that checks no identity (CSRF, geo, rate limit) still reads `middleware`), or a router-level `.use()` before the route; a shared-secret header compare counts but reads `token`. A mutating handler with no evidence is HIGH, a GET with none is INFO, and a `route-auth-ok` comment marks a reviewed public route. `--paths` prints the unique GET and ANY-method routes, one per line, for the anonymous GET sweep. Exit 0 on a readable DIR, 2 on a usage error; silence is not proof of authentication. +- `review_checks.sh --full` runs it as tool `route-auth` (HIGH rows as rule `missing-auth`, SKIPPED when the repo has no handler files), and `references/security-appsec.md` routes it from the Anonymous GET sweep. `scripts/test_route_auth_map.py` (registered in `scripts/ci-tests.d`) pins every rule with golden rows over fictional fixtures and mutation-checks each one. +size-budget-raise: .claude/skills/deep-code-review/references/security-appsec.md 27998→28245 one sentence and the command routing the Anonymous GET sweep to route_auth_map.py + ## [1.628.0] — 2026-10-30 ### Added diff --git a/SHA256SUMS b/SHA256SUMS index 620cd564d..75100d0c5 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -20,8 +20,8 @@ ef9e423362b12d0dd73c8e165faf32e9f53c4d4ac215b57c2e34002a88713838 .claude/skills 9521b057ff3db92d1e60923ebd88170713914f7e235ceb45717eb39419daf811 .claude/skills/agentic-ceo/scripts/weekly_receipt.py 7e2179751e9dd2fb371b3e9fb01fca987d69605af1e8e6e609ff9b0d0abb384b .claude/skills/agentic-delivery/INDEX-scripts.md 47fff02dcb4401d5a9b42b2b3b7f7c123125eece55e7f9adee8cfe488c70a146 .claude/skills/agentic-delivery/INDEX.md -6eea84fd7db01737e685f8b97ee5bd7b845a1672f232f5d362769f7bb75eac83 .claude/skills/agentic-delivery/SKILL.md -cee20fa16a8c48df6dfdcbfd6d593097424f687f01993d8f99661789e2401a2e .claude/skills/agentic-delivery/VERSION +0908eb84e129f09d35b869fc2f4829dae514f493c0e9a35d560be0b0d88fc3b0 .claude/skills/agentic-delivery/SKILL.md +c3928436efc2dcc3d67d251cec0e86098547dd4f135a3e1273bc4725d5499b84 .claude/skills/agentic-delivery/VERSION 94144c101f82399eb6c18f688321a00e1c195dd0ab334f3a5daa2543b40ff9f5 .claude/skills/agentic-delivery/evals/evals.json fba7699a2d0836b51b7c838d019b1f0cd20570522a362e8a32b8dacfb911298e .claude/skills/agentic-delivery/evals/triggers.json 39140ac0b40907dc2279bb3234ce1d8ffb4db56e8658c7f89cc370f777df0592 .claude/skills/agentic-delivery/references/cost-quality-guardrails.md @@ -206,8 +206,8 @@ d98f3e4380da3496061ba63be7beebe75017ab4ee1734eda9a3e798e19007b0e .claude/skills 18f20db537ab784460ad33cf218f7a01ac0a172fc38413f14c0e19e1ce6821cc .claude/skills/contribution/scripts/share_learning.py 57ab71eae0e846ea4e5e8287da26679a5a010cf576fed89ca37c94e0c900180f .claude/skills/deep-code-review/INDEX-scripts.md 9fcb6a002fb7e3fe28163199783968f80b920744c83280cff211a348215c9c0d .claude/skills/deep-code-review/INDEX.md -0a06dbe95a174c6233c3edfc792d9a8ae4318cd7270ed6cb0d0f885369a2a631 .claude/skills/deep-code-review/SKILL.md -cee20fa16a8c48df6dfdcbfd6d593097424f687f01993d8f99661789e2401a2e .claude/skills/deep-code-review/VERSION +a3d823187b8bab01cc9345102057b858d7eaffa9246dfa83521e3636a0dbc152 .claude/skills/deep-code-review/SKILL.md +c3928436efc2dcc3d67d251cec0e86098547dd4f135a3e1273bc4725d5499b84 .claude/skills/deep-code-review/VERSION b97d95bc93cc23a8bc689e2e23db261a138367bcbc69d2f8a0452d5c2c6dfb68 .claude/skills/deep-code-review/evals/evals.json 13dbb52e4bf61b1093302e094151def167371d26e7cf67a026f3beeb09d34378 .claude/skills/deep-code-review/evals/triggers.json b84e3a2b88dc189f041784e76b97e68ad879915814e00f4b37cd27513746d6f1 .claude/skills/deep-code-review/references/a11y-aria.md @@ -426,8 +426,8 @@ e951eaa63fd0d1870701d69c89c29ef4fe74dbad9b680ab6ef0a4e993fb8bc20 .claude/skills fd316c63bbf1cc0ea64322d7d923d66692eaf7b545c056367bfecca35146c862 .claude/skills/growth-analytics/references/full-map.md 2f1d7499e9ed7ae59f550b5c00043e465b2a4e885821ad153438e2e33fcccb45 .claude/skills/growth-analytics/scripts/readout_check.py 67e71ec40535ad0b3931c51f3decf323bce809ed4c9586c6d422b7ad5698129f .claude/skills/idea-critic/INDEX.md -c4f659a3999b569b0fa909d092d4200d39ad13306408c1537b92658bf788dc43 .claude/skills/idea-critic/SKILL.md -cee20fa16a8c48df6dfdcbfd6d593097424f687f01993d8f99661789e2401a2e .claude/skills/idea-critic/VERSION +47c6a04bcb63ec82fb918ed8f818b98280333077a344e5757279d65a50868072 .claude/skills/idea-critic/SKILL.md +c3928436efc2dcc3d67d251cec0e86098547dd4f135a3e1273bc4725d5499b84 .claude/skills/idea-critic/VERSION 9f77d5d6d13c99fd16cd8c398795d8be226d74bae5a5bf65369ed94a59fa56b7 .claude/skills/idea-critic/evals/evals.json 0e1c0db300661a1ab1a427a7157c75e2ed67130ab24d5c6cb02ec3ff9e793f58 .claude/skills/idea-critic/evals/heldout.json d8098c5ced63e8b8608867dd144091049563a0553cf62d27ce05b9d52ac54266 .claude/skills/idea-critic/evals/triggers.json diff --git a/changelog.d/install-summary-own-writes.md b/changelog.d/install-summary-own-writes.md deleted file mode 100644 index 2bedde347..000000000 --- a/changelog.d/install-summary-own-writes.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed -- The `install.sh` end-of-run summary no longer lists a running session's transcripts (`.claude/projects/`), its todos (`.claude/todos/`) and other files outside the paths install writes as "Generated". It found the files a run wrote by scanning whole host dirs (`.claude`, `.cursor`, `.agents` and the other hosts) for anything newer than the run's start stamp, so under a home target those files appeared as if install had created them. It now scans only the subpaths install itself writes: the installed skill dirs, each host's `skills/INDEX.md`, `.claude/commands`, `.claude/agents/delivery-lane.md`, `.claude/house-defaults.md`, the install record and flags files, `settings.local.json` and the `.new` and `.bak` siblings of the settings files, the CI-gate files and their `.new` siblings, `.perun/policy.json` and `AGENTS.md`. The rows for a project target are otherwise unchanged; the one difference is that `.github/skills/INDEX.md` and `.github/workflows/dcr-gates.yml` no longer print twice with `--with-extra-hosts --with-gates`, because `.github` used to be a scan root twice. The summary still lists another process's write to a path it does scan: a new file in `.claude/commands` (scanned whole) or an edit to `.claude/settings.local.json` during the run; an exact per-file record of what install wrote would lift that limit. -- `scripts/test-install-hygiene.sh` gains 6 cases (15 in all). Two plant files with a 2099 mtime, one each in `.claude/projects/` and `.claude/todos/`, and a generic stray in every host dir and in `.perun`, `.githooks`, `scripts` and `.github/workflows`, and assert none of them appear in the summary (both fail on the previous `install.sh`). One asserts that every file a `--full --with-gates --with-terse-replies --with-extra-hosts --with-codex` install wrote, the policy and settings files included, still appears, and one asserts a `(N files)` row for every installed skill dir. Two install into an existing target with edited gate files, a `settings.local.json` and a stale operating-layer `.new`, and assert the `.new`, `.new-1`, `.bak` and local-flags rows appear, so dropping a trailing `*` from a scan root fails. The installs run with `PERUN_ROLE` unset. diff --git a/changelog.d/install-update-hygiene.md b/changelog.d/install-update-hygiene.md deleted file mode 100644 index a1ad7e0cd..000000000 --- a/changelog.d/install-update-hygiene.md +++ /dev/null @@ -1,5 +0,0 @@ -### Fixed -- `vendor_drift.py` no longer reports a clean install as drift. A canonical file whose first line says "(install placeholder)" is compared as `docs/` under the canonical source root, the file `install.sh` copies over it. When that docs file is missing, the stub itself is the reference, so an installed full copy reads as drift (fail closed). Before, the check `update-installed.sh` runs after an update flagged the two placeholder reference files on every clean install. -- A version bump of an unedited install no longer leaves a skill backup. `install.sh` now also drops the fresh backup when it equals what the marker says the previous install wrote (read before the new install rewrites the marker), next to the existing "identical to the fresh copy" rule. Only bytecode under `__pycache__` is ignored in that comparison, like `hash_dir`. An edited or extended copy, a pre-Perun copy (no marker entry), a backup holding any other file under `__pycache__` or any entry the previous install did not write (an empty directory, a FIFO), a symlink anywhere in it, and an unreadable marker all keep the backup. Deletion still goes through the existing per-file walk, never a shell `rm` on a computed path. -- Slash commands and `--with-gates` outputs no longer pile up `.new` copies. `write_gate_file` writes nothing when the destination, or an existing `.new` / `.new-N` next to it, already holds the shipped file, so an identical re-run no longer adds `.new-1`, `.new-2` and so on. A command that install wrote and nobody edited (its sha256 still matches the marker) is refreshed in place on a version bump and re-recorded, so uninstall still removes it. Install deletes no file it did not create in that run, so a `.new` / `.new-N` already next to it stays. Only a regular file named exactly `.new` or `.new-N` counts as already holding the shipped file; a symlink or a lookalike name never does. An edited command, a pre-Perun file and any `.new` that differs from the destination are unchanged: the file is never touched and a new `.new` is written. `--with-gates` outputs are not recorded in the marker, so they are never refreshed in place: an unedited one still gets a `.new` when its shipped template changes. -- `scripts/test_install_update_hygiene.py` (registered in `scripts/ci-tests.d/install-update-hygiene.sh`) covers all three on a fictional release source in a scratch directory with a scratch `HOME`: consecutive version bumps add no backup and no `.new`, while an edited skill, a pre-Perun copy, an edited command, a lookalike `.new-` name, a symlinked `.new`, an empty directory in a backup, a pre-existing `.new` of any kind and a differing `.new` are all kept. The existing `scripts/test_install_commands.py::test_existing_file_kept_new_written_and_uninstall_keeps_user_file` pinned the old behavior of a second identical `review.md.new-1` and now expects a single `review.md.new`. diff --git a/changelog.d/lease-sandbox-writable.md b/changelog.d/lease-sandbox-writable.md deleted file mode 100644 index 8d744d695..000000000 --- a/changelog.d/lease-sandbox-writable.md +++ /dev/null @@ -1,7 +0,0 @@ -### Fixed -- The machine-wide heavy-job throttle and the exclusive train lease were inert inside a Claude Code sandbox: `perun_policy.py heavy-acquire` could not create its lease file under the read-only user cache dir, exited 2, and `review_checks.sh` then ran its scanners unleased, so no sandboxed session ever left a lease for the others to count. On macOS only, writers (`heavy-acquire`, `heavy-release`, the `heavy-exclusive*` commands and `stage_run.py`) now use the first writable of the usual lease dir and a per-user fallback dir under the system temp dir, and every reader (the slot count, the exclusive holder, the `heavy_gate.py` hook and `heavy-load`) checks both, deduplicated by real path. The fallback is used only when its root, its `perun` dir and its `heavy` dir are real directories (not symlinks) owned by the current user and the root has mode 0700; otherwise it is skipped and behaviour is as before. On other platforms nothing changes. `PERUN_HEAVY_DIR` still pins exactly one dir; `PERUN_HEAVY_FALLBACK_ROOT` moves the fallback root for tests. -- Every lease, holder-file and queue-marker read now goes through `perun_policy.read_lease`, which opens without blocking or following a symlink and skips anything that is not a regular file, so a FIFO planted in a lease dir can no longer hang a read: the `heavy_gate.py` hook, `heavy-load`, the slot and queue checks of `heavy-acquire`, and `stage_run.py`. `stage_run.py` still drops a stale lease it finds outside the primary lease dir, but never signals the process group such a lease names. -- Accepted limit, documented in `perun_policy._fallback` (the forged holder is pinned by a test): every sandboxed session of the same user can write the shared fallback dir, so one of them can forge a holder (a lease or `heavy.exclusive` naming a live pid) and make the hook and the train gate delay or deny heavy commands, or plant a FIFO where a sandboxed writer is about to create its temp or `.waiting` file and stall that writer. That is a same-user delay, not a way to run or signal anything. -- `scripts/test_lease_sandbox_fallback.py` (registered in `scripts/ci-tests.d/lease-sandbox-fallback.sh`): fixtures only, never the real temp dir; the fallback cases run on macOS only. It covers an unwritable primary falling back and releasing, a fallback holder counting against the slots of a process whose primary is writable, an exclusive fallback holder blocking acquire, the hook seeing fallback holders, a symlinked, foreign-owned or loosely moded fallback being refused, and a pinned dir never using the fallback. -- `scripts/test_lease_sandbox_hardening.py` (registered in `scripts/ci-tests.d/lease-sandbox-hardening.sh`): FIFOs in a pinned dir and in the fallback never block the hook, `heavy-load`, `heavy-acquire`, `stage_run.py` or the train gate (every subprocess has a timeout), `read_lease` rejects symlinks, directories and FIFOs without leaking a descriptor, no fallback is read, written or created off macOS, a symlinked or foreign-owned `perun` or `heavy` component is refused, a writable lease dir under a read-only parent falls back, a forged fallback holder delays as documented, and the orphan reap never signals a group named by a lease outside the primary dir. -- `scripts/test_prepush_gate_cache.py` pins `PERUN_HEAVY_DIR` so the real `stage_run.py` it runs never counts leases from the machine-wide fallback dir. diff --git a/changelog.d/review-checks-new-null.md b/changelog.d/review-checks-new-null.md deleted file mode 100644 index 78d3c4330..000000000 --- a/changelog.d/review-checks-new-null.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed -- `review_checks.sh` diff mode: a row's `"new"` flag (`true` when its file and line are on a line `REF...HEAD` adds, else `false`) is now `null`, meaning unknown, for every row in a file that differs from `HEAD` in the working tree, whether edited (staged or not) or untracked. Most rows are read from the working tree while the flag comes from the committed diff, so a `true` or `false` there could name the wrong line. `--full` and `--history` still omit the key and no row is dropped. With K null rows the stderr count line reads `N of T rows are on lines this diff adds, K unknown (file differs from HEAD)`; with none it is unchanged. The files come from `git --no-optional-locks status --porcelain -z --no-renames -uall`: it leaves `.git/index` alone (`git diff HEAD` rewrote it for a file that only changed its mtime, even with that flag) and takes no revision, so a root file named `HEAD` cannot break it. If the read fails, `"new"` is omitted from every row and stderr says `review_checks: could not list files that differ from HEAD; "new" omitted`, where it used to vanish silently. Ignored untracked files and `assume-unchanged` edits are not listed. `references/method-situational.md` documents the third value. -- `scripts/test-review-checks-new-rows.sh` gains 16 cases (32 in all): an unstaged edit, a staged edit to a path with a space, an untracked file named by a fake `tsc`, the clean files that keep `true` or `false` (one the diff does not touch), the exact count of null rows and the count line, `.git/index` left alone after a mtime-only change, a root file named `HEAD`, and an unreadable index. Eight of them fail on the script before this change. diff --git a/changelog.d/route-auth-map.md b/changelog.d/route-auth-map.md deleted file mode 100644 index 65b75b61a..000000000 --- a/changelog.d/route-auth-map.md +++ /dev/null @@ -1,4 +0,0 @@ -### Added -- deep-code-review `scripts/route_auth_map.py DIR`: report-only, stdlib-only lead-finder for missing authentication (CWE-306/862). It inventories every HTTP handler (Next.js `app/**/route` exports, `pages/api/**`, `"use server"` exports, Express/Router/Hono/Fastify `.get/.post/...` calls, FastAPI and Flask decorators) and prints one `path:line: SEV: METHOD route auth=` row each. Evidence is an identity call (editable `IDENTITY` list), a Next `middleware.ts`/`proxy.ts` whose matcher covers the route (the file names an identity check, or can answer 401/403 under any name, so a custom-named session gate counts; a quoted `/path` elsewhere in the file is read as an allow-list entry, so that route and what is under it read none (a trailing `(.*)`, `/*` or `/:name*` is dropped, so a Clerk-style `createRouteMatcher(["/sign-in(.*)"])` list counts; only a route that starts with the literal is taken out, so `includes("/hook")` or `endsWith("/hook")` leaves `/api/hook` reading `middleware`), unless the literal is a prefix of a matcher entry or sits in the condition of an `if` whose body can refuse (a scope check such as `if (p.startsWith("/api/")) { return <401> }`, nested refusals included; a braceless body runs over a line break when the next line starts with `.`, `?`, `:`, `&&` or `||` or this one ends in `?`, `:`, `&&` or `||`); conditions are not evaluated, so a negated refusal reads its path as gated and a literal held in a collection or variable is read as an exemption wherever it is used; a scope check written as a ternary, an `&&` chain, a `switch` `case` or a body that only redirects to a login page is no `if` with a refusing body, so its literal reads as an exemption and the route it gates reads none; a regex or computed allow-list is not read, and a 401/403 that checks no identity (CSRF, geo, rate limit) still reads `middleware`), or a router-level `.use()` before the route; a shared-secret header compare counts but reads `token`. A mutating handler with no evidence is HIGH, a GET with none is INFO, and a `route-auth-ok` comment marks a reviewed public route. `--paths` prints the unique GET and ANY-method routes, one per line, for the anonymous GET sweep. Exit 0 on a readable DIR, 2 on a usage error; silence is not proof of authentication. -- `review_checks.sh --full` runs it as tool `route-auth` (HIGH rows as rule `missing-auth`, SKIPPED when the repo has no handler files), and `references/security-appsec.md` routes it from the Anonymous GET sweep. `scripts/test_route_auth_map.py` (registered in `scripts/ci-tests.d`) pins every rule with golden rows over fictional fixtures and mutation-checks each one. -size-budget-raise: .claude/skills/deep-code-review/references/security-appsec.md 27998→28245 one sentence and the command routing the Anonymous GET sweep to route_auth_map.py diff --git a/gemini-extension.json b/gemini-extension.json index 9e1f356bb..c35ba4a5a 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "perun", - "version": "1.628.0", + "version": "1.629.0", "description": "Perun: a skill suite for AI-agent teams: deep code review, token and cost tools, gated delivery, an idea critic and product specialists.", "contextFileName": "AGENTS.md" }