Skip to content

🚀 deploy(release): v0.9.2 — clear the open backlog - #83

Merged
scttbnsn merged 8 commits into
mainfrom
dev/v0.9
Aug 4, 2026
Merged

🚀 deploy(release): v0.9.2 — clear the open backlog#83
scttbnsn merged 8 commits into
mainfrom
dev/v0.9

Conversation

@scttbnsn

@scttbnsn scttbnsn commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Clears everything that was open on the tracker and cuts v0.9.2.

What was open

  • 1 issue: Renovate's Dependency Dashboard (Dependency Dashboard #40), with 12 queued updates and 4 "abandoned" flags
  • 0 discussions, 0 PRs
  • 2 code-scanning alerts, one open since June 12
  • 4 merged-but-undeleted branches
  • A pile of uncommitted competitive-landscape work sitting on a stale local base

What's in here

Competitive landscape — the primary-source audit of Portainer Agent, Komodo Periphery, Arcane, Hawser, Docker-native access, and socket proxies. Corrects stale Komodo auth/edge claims, adds the Arcane comparison route, folds comparison-rows.ts into per-competitor route data, and records the pre-v1 review gate plus explicit security non-goals. This work was written against the pre-v0.9.0 tree, so the merge conflicts were resolved claim-by-claim: the audit wins on content, the portwing.codeswhat.com domain and Drydock v1.6.0-rc.11+ version strings from v0.9.x win where they overlap.

Both code-scanning alerts closed, and one was a real bug. buildCommand checked for a stack's .env.drydock with a plain os.Stat, which follows symlinks — a symlink planted at that path resolves outside STACKS_DIR and gets handed to docker compose --env-file. The stack-file writer already refused to create symlinks, so this needed a symlink planted by other means, but the check now goes through os.Root like the writer does, and non-regular files are ignored. Covered by two new tests. The log-injection alert was the single unsanitized slog call in tunnel.go — every sibling line already used applog.Sanitize.

Dashboard cleared, including the majors. Every pinned action moved to current, actions/checkout v7 and actions/setup-go v7 included — both are ESM migrations, and checkout v7's fork-ref restriction doesn't apply since no workflow uses pull_request_target or workflow_run. TypeScript 7 needed baseUrl dropped from docs/tsconfig.json (removed in TS 7; the website config was already on the paths-only form). npm 12 needed engines.node to state its actual requirement instead of a looser >=22.0.0. The js-yaml override was dead — nothing in the tree resolves to it. The 4 "abandoned" packages are stable by design, not unmaintained, so renovate.json now says so.

Branch hygiene — the 4 merged branches are deleted, along with 11 more stale local ones.

Verification

Local gate from RELEASING.md, all green: gofmt, go vet, go test -race ./... (17 packages), golangci-lint (0 issues), govulncheck (0 reachable), goreleaser check + snapshot (all archives, deb/rpm, cask, checksums). Website and docs typecheck and build, /compare/arcane renders. actionlint and zizmor 1.29 clean on the rewritten workflows. Lefthook pre-push gate passed.

Changelog

✨ Added

  • Added competitive landscape documentation and navigation.
  • Added Arcane comparison route data and website comparison coverage.
  • Added v1.0 competitive-review gates and post-v1 roadmap items.
  • Added tests for unsafe .env.drydock paths and rate-limiter edge cases.

🔧 Changed

  • Updated competitor comparison data and corrected Komodo claims.
  • Updated release documentation and all v0.9.2 version references.
  • Updated pinned GitHub Actions, dependencies, Node.js, TypeScript, and Docker image digests.
  • Disabled pre-push test caching with -count=1.
  • Marked selected stable libraries as exempt from Renovate abandonment reporting.

🐛 Fixed

  • Rejected symlinked, traversing, directory, and missing .env.drydock paths.
  • Sanitized Docker execID and error messages before logging.
  • Validated X-Real-IP before rate limiting.
  • Rejected trusted-proxy and invalid X-Real-IP fallback values.
  • Preserved active rate-limit windows during expiry cleanup.

🗑️ Removed

  • Removed the legacy ComparisonRow interface and comparisonRows dataset.
  • Removed the obsolete js-yaml npm override.
  • Deleted four merged branches and eleven stale local branches.

🔒 Security

  • Prevented .env.drydock symlink escapes outside STACKS_DIR.
  • Prevented non-regular files from being passed as Compose environment files.
  • Prevented malformed or trusted X-Real-IP values from creating rate-limit buckets.
  • Sanitized potentially sensitive values in resize failure logs.
  • Updated security and supply-chain workflow actions.

Concerns

  • Verify that no imports reference the removed comparisonRows export.
  • Verify that the July 28, 2026 review date is intentional and source-backed.
  • Verify all competitor claims against primary sources.
  • Confirm os.Root support on every target platform.
  • Confirm action and dependency updates support the required Node.js and Go versions.
  • Confirm release-config checks cover every supported version-bearing file.

Primary-source comparison of Portainer Agent, Komodo Periphery, Arcane
Agent, Hawser, Docker-native access, socket proxies, and adjacent agents,
plus the website and docs surfaces that carry those claims.

- 📝 docs(landscape): add COMPETITIVE-LANDSCAPE.md and the docs page
- ✨ feat(website): add the Arcane comparison route, sitemap entry, and llms.txt links
- 🔄 refactor(website): fold comparison-rows.ts into per-competitor route data
- 📝 docs(claims): correct stale Komodo auth/edge claims and separate agent
  features from Drydock controller responsibilities
- 📝 docs(roadmap): record the pre-v1 competitive review gate, post-v1
  candidates, and explicit security non-goals
- 🔧 config(git): ignore the generated website/vercel.json
- 🐛 fix(compose): stat .env.drydock through os.Root so a planted symlink
  cannot redirect --env-file outside STACKS_DIR, and ignore non-regular
  files (go/path-injection, alert #18)
- 🐛 fix(edge): sanitize execID and error on the initial resize-failure log,
  the only unsanitized log call in tunnel.go (go/log-injection, alert #190)
- 🧪 test(compose): cover env-file symlink escape and non-regular env file
- 📦 deps(actions): repin every action to current, incl. actions/checkout v7
  and actions/setup-go v7 (ESM migrations; no pull_request_target or
  workflow_run in any workflow, so the v7 fork-ref restriction is moot)
- 📦 deps(docker): bump the wolfi-base digest
- 📦 deps(npm): TypeScript 7, Next 16.3, @types/node 26, npm 12, plus the
  in-range tree
- 🔧 config(ts): drop baseUrl from docs/tsconfig.json, removed in TypeScript 7,
  matching the paths-only form website/tsconfig.json already used
- 🗑️ remove(npm): drop the dead js-yaml override, nothing resolves to it
- 🔧 config(renovate): stop reporting stable-by-design libraries as abandoned
- 📝 docs(changelog): close the v0.9.2 entry and reopen Unreleased
- 📝 docs(roadmap): mark v0.9.2 as the current pre-v1 release
- 📝 docs(compare): retire the stale v0.8.1 version claims on the
  comparison pages
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
getportwing Ready Ready Preview Aug 4, 2026 8:06pm

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request updates release metadata from v0.9.1 to v0.9.2, refreshes pinned CI and release actions, and updates Wolfi image digests. It secures Compose environment-file lookup, validates forwarded client IPs, preserves active rate-limit entries, and sanitizes resize-failure logs. It adds competitive-landscape documentation and comparison data for Arcane and Hawser. It updates website tooling, API examples, installation instructions, verification examples, and roadmap content.

Possibly related issues

  • Dependency Dashboard #40 — Applies dependency, GitHub Actions, Docker digest, npm, and runner updates from the Renovate dashboard.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev/v0.9

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@COMPETITIVE-LANDSCAPE.md`:
- Line 43: Align the current comparison version scope across
COMPETITIVE-LANDSCAPE.md:43-43 and docs/design/ed25519-auth.md:406-410 by
changing each Portwing matrix label to the prepared v0.9.2 release version, or
consistently marking both matrices as historical. Ensure the current-comparison
heading and table labels use the same scope in both locations.

In `@internal/docker/compose.go`:
- Around line 360-363: Update the error handling around cm.rootedPath in the
compose setup flow to wrap failures with fmt.Errorf, adding clear context while
preserving the original error via %w before returning it.

In `@package.json`:
- Around line 20-25: Regenerate the dependency lockfiles for package.json (lines
20-25), docs/package.json (lines 18-31), and website/package.json (lines 22-34),
ensuring the root lockfile records both workspace lockfile entries; then run npm
ci to validate the combined installation.

In `@website/src/app/compare/page.tsx`:
- Around line 115-116: Update the comparison page content around the
reviewed-date text to identify the specific reviewed product releases and add a
visible link to the versioned comparison evidence record. Keep the existing
documentation-sourcing statement and review date, using the project’s
established evidence URL or reference symbol if available.

In `@website/src/lib/comparison-route-data/portainer.tsx`:
- Line 17: Update the Portainer Agent entry in the comparison route data to
remove the incorrect “~300 MB node image” implementation and size claim.
Preserve the existing “No” value if it represents container-only distribution,
and replace only the inaccurate descriptive text with a factually correct
description consistent with the official Go-based agent images.

In `@website/src/lib/comparison-route-data/watchtower.tsx`:
- Around line 24-26: Update the Watchtower metrics entries in the comparison
data: revise the activity highlight and the existing “Prometheus metrics|No”
table row to indicate that Watchtower provides an opt-in experimental Prometheus
endpoint at /v1/metrics enabled by --http-api-metrics, while preserving the
Portwing metrics description.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 93d1bdba-1e7b-4643-bf0e-9e839c3c3597

📥 Commits

Reviewing files that changed from the base of the PR and between 82c3b59 and c6e3b21.

⛔ Files ignored due to path filters (3)
  • CHANGELOG.md is excluded by !CHANGELOG.md
  • package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • website/public/llms.txt is excluded by !website/public/**
📒 Files selected for processing (43)
  • .github/workflows/ci.yml
  • .github/workflows/quality-bench-monthly.yml
  • .github/workflows/quality-fuzz-monthly.yml
  • .github/workflows/quality-fuzz-nightly.yml
  • .github/workflows/quality-integration.yml
  • .github/workflows/quality-mutation-monthly.yml
  • .github/workflows/quality-soak-weekly.yml
  • .github/workflows/release-cut.yml
  • .github/workflows/release.yml
  • .github/workflows/security-grype.yml
  • .github/workflows/security-scorecard.yml
  • .gitignore
  • COMPETITIVE-LANDSCAPE.md
  • Dockerfile
  • Dockerfile.release
  • README.md
  • ROADMAP.md
  • docs/content/docs/competitive-landscape.mdx
  • docs/content/docs/index.mdx
  • docs/content/docs/meta.json
  • docs/design/ed25519-auth.md
  • docs/package.json
  • docs/tsconfig.json
  • internal/docker/compose.go
  • internal/docker/compose_helpers_test.go
  • internal/edge/tunnel.go
  • package.json
  • renovate.json
  • website/package.json
  • website/src/app/compare/page.tsx
  • website/src/app/data/comparison-rows.ts
  • website/src/app/data/roadmap.ts
  • website/src/app/sitemap.ts
  • website/src/components/compare-matrix.tsx
  • website/src/components/compare-section.tsx
  • website/src/lib/comparison-route-data.tsx
  • website/src/lib/comparison-route-data/arcane.tsx
  • website/src/lib/comparison-route-data/diun.tsx
  • website/src/lib/comparison-route-data/hawser.tsx
  • website/src/lib/comparison-route-data/komodo.tsx
  • website/src/lib/comparison-route-data/portainer.tsx
  • website/src/lib/comparison-route-data/watchtower.tsx
  • website/src/lib/comparison-route.tsx
💤 Files with no reviewable changes (2)
  • docs/tsconfig.json
  • website/src/app/data/comparison-rows.ts

Comment thread COMPETITIVE-LANDSCAPE.md
“Not documented” means the reviewed primary documentation did not establish
the capability. It is intentionally different from a definitive “no.”

| Capability | Portwing v0.8.1 | Portainer Agent | Komodo Periphery | Arcane Agent | Hawser |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use one Portwing version for current comparison claims.

The PR prepares v0.9.2, while these new current comparison tables identify Portwing as v0.8.1. This makes the reviewed capability scope ambiguous.

  • COMPETITIVE-LANDSCAPE.md#L43-L43: change the Portwing matrix label to the release version, or mark the matrix as historical.
  • docs/design/ed25519-auth.md#L406-L410: align the current-comparison heading and Portwing table label with the same version scope.
📍 Affects 2 files
  • COMPETITIVE-LANDSCAPE.md#L43-L43 (this comment)
  • docs/design/ed25519-auth.md#L406-L410
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@COMPETITIVE-LANDSCAPE.md` at line 43, Align the current comparison version
scope across COMPETITIVE-LANDSCAPE.md:43-43 and
docs/design/ed25519-auth.md:406-410 by changing each Portwing matrix label to
the prepared v0.9.2 release version, or consistently marking both matrices as
historical. Ensure the current-comparison heading and table labels use the same
scope in both locations.

Comment on lines +360 to +363
rootedEnv, err := cm.rootedPath(stackDir, ".env.drydock")
if err != nil {
return "", err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Wrap the rootedPath error.

Add context before returning this error.

Proposed change
  rootedEnv, err := cm.rootedPath(stackDir, ".env.drydock")
  if err != nil {
-   return "", err
+   return "", fmt.Errorf("resolving env file path: %w", err)
  }

As per coding guidelines: **/*.go: Wrap errors with fmt.Errorf("context: %w", err) and use log/slog for structured logging only.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rootedEnv, err := cm.rootedPath(stackDir, ".env.drydock")
if err != nil {
return "", err
}
rootedEnv, err := cm.rootedPath(stackDir, ".env.drydock")
if err != nil {
return "", fmt.Errorf("resolving env file path: %w", err)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/docker/compose.go` around lines 360 - 363, Update the error handling
around cm.rootedPath in the compose setup flow to wrap failures with fmt.Errorf,
adding clear context while preserving the original error via %w before returning
it.

Source: Coding guidelines

Comment thread package.json
Comment on lines +20 to +25
"postcss": "^8.5.25",
"sharp": "^0.35.3"
},
"packageManager": "npm@11.13.0",
"packageManager": "npm@12.0.2",
"engines": {
"node": ">=22.0.0"
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Manifest declarations:"
rg -n '"postcss"|"next"|"typescript"|"`@types/node`"|"`@types/react-dom`"|"packageManager"' \
  package.json docs/package.json website/package.json

echo
echo "Committed lockfiles:"
fd -t f 'package-lock.json' . -x sh -c '
  echo "== $1 =="
  rg -n '"'"'"version": "(16\.3\.0|7\.0\.2|8\.5\.25)"|node_modules/(next|typescript|postcss)' "$1" || true
' sh {}

Repository: CodesWhat/portwing

Length of output: 1100


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Lockfile candidates:"
git ls-files '*package-lock.json' || true

for f in $(git ls-files '*package-lock.json'); do
  echo
  echo "== $f =="
  printf 'Lockfile lines: '
  wc -l < "$f"
  printf 'PostCSS/next/typescript/node package ref lines in lockfile:\n'
  rg -n '"(postcss|next|typescript|`@types/node`|`@types/react-dom`|`@tailwindcss/postcss`|tailwindcss)",?|"version": "([0-9]+\.[0-9]+\.[0-9]+)",?|/node_modules/(postcss|next|typescript|`@types/node`|`@types/react-dom`|`@tailwindcss/postcss`|tailwindcss)'\''' "$f" || true
done

echo
echo "Diff summary:"
git diff --stat || true

echo
echo "Root package excerpt:"
sed -n '1,80p' package.json

Repository: CodesWhat/portwing

Length of output: 16081


Regenerate the root package-lock.json.

npm ci will not install the changed workspaces from this combined lockfile because it only contains the root package metadata and cannot record website/package-lock.json / docs/package-lock.json entries. Add or refresh the workspace lockfiles, then run npm ci.

📍 Affects 3 files
  • package.json#L20-L25 (this comment)
  • docs/package.json#L18-L31
  • website/package.json#L22-L34
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 20 - 25, Regenerate the dependency lockfiles for
package.json (lines 20-25), docs/package.json (lines 18-31), and
website/package.json (lines 22-34), ensuring the root lockfile records both
workspace lockfile entries; then run npm ci to validate the combined
installation.

Comment on lines +115 to +116
Current agent capabilities, controller tradeoffs, and deliberate non-goals — sourced
from published product documentation and reviewed July 28, 2026.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Link the reviewed versions and evidence.

Line 115 states a July 28, 2026 review date. This page does not show the reviewed product versions or link to the evidence record. Add a visible link to the versioned comparison evidence and identify the reviewed releases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/src/app/compare/page.tsx` around lines 115 - 116, Update the
comparison page content around the reviewed-date text to identify the specific
reviewed product releases and add a visible link to the versioned comparison
evidence record. Keep the existing documentation-sourcing statement and review
date, using the project’s established evidence URL or reference symbol if
available.

Host file APIs and Swarm aggregation|Yes|Intentional non-goals|competitor
Prometheus agent scrape endpoint|Not documented|Yes|self
MCP server (read-only)|Not documented|Yes|self
Single lightweight binary|No (~300 MB node image)|Yes (~10 MB Go binary)|self

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the Portainer Agent deployment data.

Line 17 calls Portainer Agent a ~300 MB node image. The official v2.39.5 Linux AMD64 images are 32.38 MB and 35.95 MB, and the agent source is Go. Keep No if container-only distribution is the distinction, but remove the incorrect implementation and size claim. (hub.docker.com)

Proposed correction
-Single lightweight binary|No (~300 MB node image)|Yes (~10 MB Go binary)|self
+Single lightweight binary|No (containerized Go agent; 32–36 MB Linux AMD64 image)|Yes (~10 MB Go binary)|self
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Single lightweight binary|No (~300 MB node image)|Yes (~10 MB Go binary)|self
Single lightweight binary|No (containerized Go agent; 32–36 MB Linux AMD64 image)|Yes (~10 MB Go binary)|self
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/src/lib/comparison-route-data/portainer.tsx` at line 17, Update the
Portainer Agent entry in the comparison route data to remove the incorrect “~300
MB node image” implementation and size claim. Preserve the existing “No” value
if it represents container-only distribution, and replace only the inaccurate
descriptive text with a factually correct description consistent with the
official Go-based agent images.

Comment on lines +24 to +26
packagecheck|Maintained Signed Releases|Watchtower's upstream repository is archived. Portwing remains actively maintained and every release ships per-archive CycloneDX SBOMs, an image SBOM attestation, cosign image signatures, and SLSA build provenance. This verifies Portwing itself; it is not workload image-signature enforcement.
activity|Prometheus Metrics|Portwing exposes agent health, request counts, and latency histograms. Watchtower has no metrics endpoint.
bot|MCP Server (AI-Native)|Portwing ships a read-only MCP server so AI tools can inspect containers and events. Watchtower has no MCP support.
bot|MCP Server (AI-Native)|Portwing ships five read-only MCP tools for container and host inspection. Watchtower has no documented MCP support.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the Watchtower metrics claim.

Line 25 says Watchtower has no metrics endpoint. Upstream documents an opt-in experimental Prometheus endpoint at /v1/metrics behind --http-api-metrics. Update this highlight and the existing Prometheus metrics|No table row. (containrrr.dev)

Proposed correction
-Prometheus metrics|No|Yes|self
+Prometheus metrics|Yes (experimental, opt-in `/v1/metrics`)|Yes|tie
...
-activity|Prometheus Metrics|Portwing exposes agent health, request counts, and latency histograms. Watchtower has no metrics endpoint.
+activity|Prometheus Metrics|Portwing exposes agent health, request counts, and latency histograms. Watchtower exposes an opt-in experimental Prometheus endpoint at `/v1/metrics`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/src/lib/comparison-route-data/watchtower.tsx` around lines 24 - 26,
Update the Watchtower metrics entries in the comparison data: revise the
activity highlight and the existing “Prometheus metrics|No” table row to
indicate that Watchtower provides an opt-in experimental Prometheus endpoint at
/v1/metrics enabled by --http-api-metrics, while preserving the Portwing metrics
description.

clientIP required every X-Forwarded-For hop to parse as an IP address but
returned the X-Real-IP fallback verbatim. Behind a configured trusted proxy a
caller could send a distinct arbitrary string per request, mint a fresh
limiter bucket each time, and walk past the 10-failures-per-minute throttle on
the auth path, while writing that string into audit records as the actor.

The fallback now applies the same rules as the chain walk: parse as an IP and
not itself a trusted proxy, else use the direct peer.

- 🧪 test(server): cover non-IP and trusted-proxy X-Real-IP values
- 🔧 config(lefthook): add -count=1 to the pre-push race run so a cached pass
  cannot mask a newly-introduced race
@scttbnsn

scttbnsn commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Found a third security issue while sweeping for more of the same class, so it's in this PR too.

RateLimiter.clientIP required every X-Forwarded-For hop to parse as an IP address, but returned the X-Real-IP fallback header verbatim. Behind a configured trusted proxy, a caller could send a distinct arbitrary string per request — each one mints its own limiter bucket, which walks straight past the 10-failures-per-minute throttle on the authentication path. The same unvalidated string also landed in audit records as the actor.

The fallback now applies the same rules the chain walk already did: parse as an IP, and not itself a trusted proxy, otherwise fall back to the direct peer. Two regression tests cover non-IP values (including a newline-bearing one) and trusted-proxy values.

CodeQL didn't flag this one — the logging path was already sanitized, so there was no taint sink for it to find. The bug was in what the value was used for, not where it was printed.

Also added -count=1 to the pre-push -race run in lefthook.yml. Without it the Go test cache can serve a stale pass and mask a newly-introduced race locally. That change is self-demonstrating: the pre-push race run on this commit went from 6.6s to 66s, so it had been skipping most packages.

The release-contract gate caught a half-finished bump: ROADMAP.md had been
moved to v0.9.2 while the other surfaces it pins still said v0.9.1.

- 📝 docs(version): bump README, COMPATIBILITY, openapi, llms.txt, site-config,
  get-started, installation, verification, standalone-mode, api-reference,
  observability, security-model, and the observability example
- 🔧 config(contract): pin the contract to 0.9.2 and derive the CHANGELOG date
  from a variable instead of a hardcoded one
- ✨ feat(contract): reject any stale previous-version reference outside the
  changelog. The enumerated checks only cover surfaces someone remembered to
  enumerate — this immediately caught a "v0.9.1 is the current release"
  callout in README and an rpm example beside an already-checked deb example
@scttbnsn

scttbnsn commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

The GoReleaser Config failure was real and worth keeping.

scripts/package-release-config-test.sh pins every version-bearing surface to the current release. I'd moved ROADMAP.md to v0.9.2 and left the other eleven surfaces on v0.9.1, so the gate failed exactly as designed — a half-finished version bump is precisely what it exists to catch.

Fixed by doing the bump properly across README, COMPATIBILITY, openapi.yaml, llms.txt, site-config, get-started, installation, verification, standalone-mode, api-reference, observability, security-model, and the observability example. The contract's CHANGELOG date is now derived from a variable instead of a hardcoded 2026-08-01.

I also added a catch-all, because the enumerated checks had holes: they verified the new version is present on twelve named surfaces, but never that the old one is gone. So git grep for the previous version across doc/config surfaces, excluding the changelog, now fails the contract. It found two things the twelve enumerated checks missed on its first run:

  • a > **v0.9.1 is the current release.** callout near the top of README
  • an rpm install example sitting directly beside an already-checked deb example

Enumerating surfaces only ever finds the surfaces someone remembered to enumerate.

- 📦 deps(docker): bump the wolfi-base runtime digest to 003627d. The
  changelog already claimed this pin was current and it wasn't; d2ad9a7
  was two rebuilds behind, so the runtime rootfs was missing the newest
  upstream Wolfi package fixes.
- 📦 deps(ci): move the Homebrew cask gate from macos-15 to macos-26. The
  job only runs post-publish on GA tags and has passed on every release,
  so the image swap is the whole change.

@biggest-littlest biggest-littlest left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving the v0.9.2 cut. Reviewed the two security fixes (X-Real-IP validation before it keys the limiter, os.Root containment on the compose env-file stat), the release-contract hardening, and the dependency refresh.

ALARGECOMPANY
ALARGECOMPANY previously approved these changes Aug 4, 2026

@ALARGECOMPANY ALARGECOMPANY left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving the v0.9.2 cut. Reviewed the two security fixes (X-Real-IP validation before it keys the limiter, os.Root containment on the compose env-file stat), the release-contract hardening, and the dependency refresh.

CI read 95.9% against a 96% floor. main was already sitting exactly on 96.0
with no margin, so the three uncovered error branches statRootedEnvFile added
were enough to tip it red.

- 🧪 test(docker): cover statRootedEnvFile's two reachable error paths — a
  stack dir escaping STACKS_DIR (rejected lexically, before any syscall) and
  a missing stacks dir (caught at os.OpenRoot). The filepath.Abs branch stays
  uncovered; it only fails if os.Getwd does.
- 🔄 refactor(server): split the rate limiter's prune loop out of cleanup into
  sweepExpired(now). The rules were unreachable in tests only because the
  ticker period is hardcoded at five minutes.
- 🧪 test(server): cover sweepExpired, including the two entries that must
  survive a sweep — in-flight ones, whose firstFail is not final and whose
  concurrency count would be lost, and zero-firstFail ones.
- 🎨 style(docs): qlty fmt the two competitive-landscape files (MD012).
- 📝 docs(ci): correct the coverage-gate comment. It claimed ~1.5% of slack
  below an achieved 97.5%; actual is 96.3% against a floor of 96, and linux
  CI reads ~0.1 under a darwin dev box.

@biggest-littlest biggest-littlest left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving on 4338246. Adds the coverage the two new statRootedEnvFile error branches were missing, splits the rate limiter's prune loop into sweepExpired so its rules are testable without the five-minute ticker, and clears the markdownlint findings on the competitive-landscape files.

@ALARGECOMPANY ALARGECOMPANY left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving on 4338246. Adds the coverage the two new statRootedEnvFile error branches were missing, splits the rate limiter's prune loop into sweepExpired so its rules are testable without the five-minute ticker, and clears the markdownlint findings on the competitive-landscape files.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/package-release-config-test.sh`:
- Around line 73-76: Update the git grep invocation in the stale-version check
to scan all tracked files instead of restricting matches to the current
extension allowlist, while retaining only the necessary exclusions for
CHANGELOG.md and scripts/package-release-config-test.sh. Ensure files such as
Dockerfile, package.json, and TOML configuration are included.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cf6547f9-2fb6-435e-9bd5-8108a23d8ddd

📥 Commits

Reviewing files that changed from the base of the PR and between c6e3b21 and 4338246.

⛔ Files ignored due to path filters (2)
  • CHANGELOG.md is excluded by !CHANGELOG.md
  • website/public/llms.txt is excluded by !website/public/**
📒 Files selected for processing (22)
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • COMPATIBILITY.md
  • COMPETITIVE-LANDSCAPE.md
  • Dockerfile
  • README.md
  • api/openapi.yaml
  • docs/content/docs/api-reference.mdx
  • docs/content/docs/competitive-landscape.mdx
  • docs/content/docs/installation.mdx
  • docs/content/docs/observability.mdx
  • docs/content/docs/security-model.mdx
  • docs/content/docs/standalone-mode.mdx
  • docs/content/docs/verification.mdx
  • examples/observability/docker-compose.yml
  • internal/docker/compose_helpers_test.go
  • internal/server/coverage2_test.go
  • internal/server/middleware.go
  • lefthook.yml
  • scripts/package-release-config-test.sh
  • website/src/components/get-started.tsx
  • website/src/lib/site-config.ts
💤 Files with no reviewable changes (2)
  • docs/content/docs/competitive-landscape.mdx
  • COMPETITIVE-LANDSCAPE.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • Dockerfile
  • internal/docker/compose_helpers_test.go
  • README.md
  • .github/workflows/ci.yml
  • .github/workflows/release.yml

Comment on lines +73 to +76
if git grep -n -F -- "$previous_version" -- \
'*.md' '*.mdx' '*.ts' '*.tsx' '*.yaml' '*.yml' '*.txt' \
':(exclude)CHANGELOG.md' \
':(exclude)scripts/package-release-config-test.sh'; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scan all tracked release and configuration files.

The extension allowlist skips files such as Dockerfile, package.json, and TOML configuration. A stale 0.9.1 reference in those files passes this release gate. Search all tracked files and retain only the required exclusions.

Expand the stale-version check
 if git grep -n -F -- "$previous_version" -- \
-	'*.md' '*.mdx' '*.ts' '*.tsx' '*.yaml' '*.yml' '*.txt' \
 	':(exclude)CHANGELOG.md' \
 	':(exclude)scripts/package-release-config-test.sh'; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if git grep -n -F -- "$previous_version" -- \
'*.md' '*.mdx' '*.ts' '*.tsx' '*.yaml' '*.yml' '*.txt' \
':(exclude)CHANGELOG.md' \
':(exclude)scripts/package-release-config-test.sh'; then
if git grep -n -F -- "$previous_version" -- \
':(exclude)CHANGELOG.md' \
':(exclude)scripts/package-release-config-test.sh'; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/package-release-config-test.sh` around lines 73 - 76, Update the git
grep invocation in the stale-version check to scan all tracked files instead of
restricting matches to the current extension allowlist, while retaining only the
necessary exclusions for CHANGELOG.md and
scripts/package-release-config-test.sh. Ensure files such as Dockerfile,
package.json, and TOML configuration are included.

@scttbnsn
scttbnsn merged commit 44cfd21 into main Aug 4, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants