Skip to content

🔧 chore(release): sync main from dev/v1.6 for v1.6.0-rc.1 - #200

Merged
scttbnsn merged 33 commits into
mainfrom
sync/v1.6.0-rc.1
Aug 5, 2026
Merged

🔧 chore(release): sync main from dev/v1.6 for v1.6.0-rc.1#200
scttbnsn merged 33 commits into
mainfrom
sync/v1.6.0-rc.1

Conversation

@scttbnsn

@scttbnsn scttbnsn commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Syncs main to the dev/v1.6 tip (33b9405) ahead of cutting v1.6.0-rc.1, per the release branch discipline: main only advances by merging from the active dev branch, immediately before each cut.

Everything here already landed on dev/v1.6 through reviewed, CI-green PRs: #189 #190 #191 #192 #193 #195 (Podman/#148), #197 (tri-tool conformance/#150), #199 (rc.1 changelog prep), plus the earlier #149/#151/#152/#153 trains.

After merge, the tree-equality check (git diff --quiet origin/main origin/dev/v1.6) must pass before the tag is cut via the release-cut workflow with release_tag: v1.6.0-rc.1.

scttbnsn and others added 30 commits August 4, 2026 14:37
Branch discipline routes every change through the active dev branch
(currently dev/v1.6); CodeRabbit's default only reviews PRs into main,
so the whole release train was skipped with 'reviews are disabled for
this base branch'.
* ✨ feat(release): distribute stable releases via Homebrew tap

- ✨ feat(release): publish sockguard cask to CodesWhat/homebrew-tap via GoReleaser on stable tags
- ✨ feat(ci): add verify-homebrew macOS smoke job (Gatekeeper path, version match, quarantine check)
- 🔧 config(ci): render GoReleaser snapshot in branch CI and assert generated cask contents
- 🧪 test(release): add scripts/homebrew-release.test.mjs covering config, workflows, and docs
- 📝 docs: document Homebrew install path and trust boundary in README, getting-started, RELEASING

* 🐛 fix(release): gate Homebrew tap token requirement to stable tags

- Prerelease tags (containing "-") skip the cask upload via
  skip_upload: auto, so the "Require Homebrew tap token" step must not
  hard-fail on them; add if: !contains(github.ref_name, '-')
- Assert the stable-only condition in homebrew-release.test.mjs
- Clarify README.md and getting-started.mdx docs: Homebrew's SHA-256
  checksum verification proves archive integrity, not publisher
  identity, and is not a substitute for Apple notarization; point
  policy-bound users at the container image or cosign-verified
  release binary instead
…165)

* 🐛 fix(ci): pre-pull digest-pinned busybox ref in nightly integration

Docker Hub re-pushed the busybox:1.37 tag, so the digest pinned in
helpers_test.go's busyboxPinnedRef no longer matched what a bare
`docker pull busybox:1.37` materialized in the runner's local image
store. dockerd won't pull-on-create for a digest ref that isn't
already local, so all four tests that pull by digest failed with
"No such image". The pre-pull step now greps the pinned ref straight
out of helpers_test.go and pulls that, so it can never drift from the
constant again. Bumped busyboxPinnedRef's digest to the current
busybox:1.37 index digest.

* 🐛 fix(ci): bind sentinel pre-pull to busyboxPinnedRef constant

Extract the named constant instead of the first busybox substring, and
fail before docker pull if the value is missing or not digest-pinned.
)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
… 399c8cb (#162)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): update non-major (github-actions)

* 🔧 chore(deps): regenerate npm lockfile to match package.json ranges

dev/v1.6 HEAD carried a package-lock.json out of sync with package.json
(knip, turbo, oxc-parser, and others) from a sibling npm dependency PR
that merged despite failing CI (Biome Lint / TS Test / Build Workspaces
all failed on `npm ci`: lock file did not satisfy package.json ranges).
Regenerate via `npm install --package-lock-only` so this branch's CI can
install cleanly; no package.json ranges changed.

* 🔧 chore(deps): tidy go.mod/go.sum

dev/v1.6 HEAD carried a go.mod/go.sum out of tidy state (unused
go.opentelemetry.io/otel/sdk/metric indirect require, and several
indirect deps resolved to older patch versions than go.mod's graph
now allows). This is the pre-push goreleaser-snapshot hook's `go mod
tidy` step surfacing pre-existing drift, not a change caused by this
PR's own diff. Committing the tidy output so the local clean-tree gate
(and every subsequent Renovate PR rebased on top of dev/v1.6) doesn't
trip over it.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
* chore(deps): pin dependencies

* 🔧 chore(deps): regenerate lockfile for pinned dependency versions

npm install --package-lock-only to sync package-lock.json with the
pinned turbo/knip/biome/lefthook/postcss/typescript versions re-applied
on top of current dev/v1.6 (turbo and knip had moved to newer versions
via sibling merges; pinned those instead of downgrading).

* 🔧 chore(deps): dedupe sharp after lockfile regen

npm install --package-lock-only left sharp nested under
next/node_modules at the pre-override 0.34.5 instead of hoisted to the
root override's ^0.35.0 range. npm dedupe fixes it (matches what the
lockfile-dedupe pre-push guard expects).

* 🔒 fix(deps): pin postcss override exactly to close a real CVE regression

Pinning docs/package.json's postcss to an exact "8.5.25" (this PR's own
pin-dependencies intent) left the root override's "^8.5.24" range
inconsistent with it, and npm's incremental lockfile resolution stopped
applying the override to next's own vendored postcss — reintroducing
next/node_modules/postcss@8.4.31 (GHSA-6g55-p6wh-862q and friends,
HIGH). Pinning the override to the same exact "8.5.25" and forcing a
clean re-resolution of the postcss subtree restores the single hoisted,
patched postcss and fixes the Grype dependency-scan failure this PR's
push triggered.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
* chore(deps): pin dependency @types/react to 19.2.18

* 🔧 chore(deps): regenerate lockfile for @types/react pin

Regen against dev/v1.6's current pinned postcss/tailwindcss versions
after rebase; confirms the #160 override-pin fix resolves postcss
cleanly everywhere (no @8.4.31 regression).

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* 🔧 chore(deps): lock file maintenance

Full fresh regen (rm package-lock.json && npm install) against
everything currently merged into dev/v1.6. Required
--legacy-peer-deps to complete resolution due to a pre-existing
upstream mismatch (fumadocs-ui@16.10.5 pins an exact peer
fumadocs-core@16.10.5, but ^16.10.5 now resolves to 16.14.0) —
confirmed this blocks a from-scratch install on dev/v1.6 as-is too,
unrelated to this change. npm ci and npm run build both verified
clean against the resulting lockfile.

* 🔧 chore(deps): pin fumadocs-core/fumadocs-ui to matching exact versions

The previous lockfile regen used --legacy-peer-deps to work around an
ERESOLVE against fumadocs-ui's exact peer dep on fumadocs-core, but that
silently dropped fumadocs-ui's own transitive deps (@radix-ui/react-tabs,
rehype-raw, etc.) from the lockfile entirely — passed locally only because
Turbopack was serving a stale cached build, but broke CI's `npm ci` for
real (module-not-found on both packages).

Root cause: fumadocs-ui@16.14.0 (latest matching the old ^16.10.5 range)
requires peer fumadocs-core@16.14.0 exactly. Pinning both packages to the
same exact 16.14.0 lets a plain `npm install`/`npm ci` resolve cleanly
with no flags, and correctly pulls in fumadocs-ui's full dependency tree.
Verified with a fully clean node_modules (including workspace-nested
docs/node_modules and website/node_modules) via npm install, npm ci, and
an uncached `turbo build --force` — all pass.

---------

Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
- Add v1.5.2 (in flight) roadmap entry: nightly digest-drift fix, Homebrew tap, dependency refresh including majors
- Restructure v1.6.0 into delivery waves with linked issue numbers: Wave 1 (#149, #151, #152) parallel, Wave 2 (#153 then #148) sequential on the route classifier, Wave 3 (#150) gates GA
- Note the roadmap update in CHANGELOG.md Unreleased/Changed
#176)

Next 16.2.12 refuses to build under TypeScript 7 with "TypeScript 7.0.2
does not provide the compiler API required by Next.js" unless
experimental.useTypeScriptCli is set, since Next's default type-checking
path depends on TS6's compiler API and TS7 dropped it in favor of a CLI
interface. Add the flag to both website/next.config.ts and
docs/next.config.ts so Renovate's pending TypeScript 7 bump (#172) can
land without a build break.

Verified locally against the current TS 6.0.3 toolchain: root `npm run
build` (both workspaces), `npm test` (92/92 passing), and `npx biome
check .` all pass clean, with only pre-existing warnings unrelated to
this change.
… redaction gap (#178)

- Deny POST /containers/create and POST /services/create|update requests
  carrying a Mounts entry of Type "image" when image_trust is in enforce
  mode. Docker API 1.48+'s image-type mount source is an unverified image
  reference mounted into the container's filesystem, invisible to the
  existing bind-mount checks (Type == "bind" only), and previously bypassed
  cosign verification entirely even when the top-level Image field passed.
- Strip the Engine API 1.53 network-inspect Status field (per-subnet IPAM
  allocation stats) under redact_network_topology, alongside the existing
  IPAM.Config/Containers/Peers redaction.
…anch reviews (#177)

* 🔧 chore(deps): drop obsolete js-yaml override

fumadocs-core 16.14.0 / fumadocs-mdx 15.2.2 no longer resolve js-yaml
anywhere in the tree, so the ^4.3.0 override matches nothing. Removing it
instead of bumping it to v5 — supersedes the original intent of this PR.
Lockfile delta is only the embedded overrides mirror.

* 🔧 chore(coderabbit): nest base_branches under auto_review

base_branches at the reviews: level is silently ignored — every dev/v1.6
PR got 'Review skipped: auto reviews are disabled on base/target branches
other than the default branch' despite the config. The key only takes
effect nested under reviews.auto_review.
Pinned to 7.0.2 in website/ and docs/. Builds go through the TypeScript
CLI via experimental.useTypeScriptCli (landed in #176) since TS7 no
longer exposes the compiler API Next.js 16 uses. Supersedes the
Renovate branch which predated the exact-pin convention and the
dev/v1.6 lockfile regeneration.

Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
… services (#152) (#182)

* 🔧 config(request-body): add resource-limit require_* flags for container update and service (#152)

- container_update gains require_memory_limit/require_cpu_limit/require_cpu_limit_hard/require_pids_limit
- service gains require_cpu_limit/require_cpu_limit_hard
- all default false, reload-mutable, mirrored on client profiles
- filter.ContainerUpdateOptions/ServiceOptions carry the new fields for
  config plumbing; enforcement lands in the new resource-limit guard,
  not in the existing pre-ownership inspectors

* ✨ feat(filter): add post-ownership resource-limit guard (#152)

New internal/filter/resource_limit_guard.go enforces the require_*
resource-limit flags added for container_update and service. It runs as
its own middleware layer AFTER ownership (see the serve.go wiring commit)
rather than inside the existing pre-ownership request-body inspectors:

- container update: root-only decode of the update patch (nested
  HostConfig/Resources are decoys and never read), a moby-faithful
  overlay of patch onto a bounded GET of the container's current
  HostConfig, then the shared create-time predicate against the
  effective state. Omission cannot grandfather a pre-existing weak
  container (the documented "ratchet" edge).
- service create/ordinary update: request TaskTemplate.Resources.Limits.NanoCPUs
  must be positive when either CPU flag is set.
- service rollback (manual ?rollback=previous, or an automatic
  UpdateConfig.FailureAction: rollback) validates the daemon-owned
  PreviousSpec/current Spec that would actually become active, with a
  version/CAS check (409 on mismatch) — the request body proves nothing
  once the daemon is about to apply a stored spec instead.

container_create.go's denyResourceLimitReason is extracted to a free
function (resourceLimitDenyReason) shared by container-create and this
guard's container-update path, so the two can never drift; the
create-time method becomes a one-line wrapper.

Four new reason codes (resource_limit_request_invalid 400,
resource_limit_policy_denied 403, resource_limit_policy_lookup_failed 502,
resource_limit_policy_state_changed 409). Only the policy_denied path
honors the resolved profile's warn/audit rollout posture; a corrupt
request, a failed daemon lookup, or a stale version are hard errors in
every mode. No body rewriting anywhere — the guard only decides
allow/deny and always forwards the original bytes.

* 🔧 chore(serve): wire the resource-limit guard into the handler chain (#152)

Insert withResourceLimitGuard between withHijack and withOwnership in
buildServeHandlerLayersWithRuntime's append order. Later appends wrap
(execute before) earlier ones, so runtime order becomes
...filter -> visibility -> ownership -> resource-limit guard -> hijack
-> proxy: ownership still decides before any resource-state daemon
lookup, and the guard still sits ahead of hijack/proxy for everything
ownership allows.

withResourceLimitGuard reuses servePolicyConfig/clientProfiles/
clientacl.RequestProfile — the same PolicyConfig, profile map, and
resolver wiring withFilter already uses — plus the two new runtime
inspectors (container/service state GETs) through the shared upstream
resolver.

Also adds a startup/reload warning (mirroring warnIfLabelACLEnabled's
once-per-process pattern) when a require_* container_update flag is set
while allow_resource_updates is false: structurally valid but very
likely operator confusion, since the flag is a no-op until the gate is
open. Updates TestBuildServeHandlerLayers for the new layer.

* ✨ feat(logging): add resource-limit guard audit/access-log fields (#152)

Adds a pooled logging.ResourcePolicyMeta on RequestMeta, populated only
by filter.ResourceLimitGuard when it actually evaluated policy for a
request (the common "no applicable require_* flag" case leaves it nil —
zero allocation, zero log fields). Fields are classification-only:
kind/operation/state-source/requirements/result/violation-class/
state-lookup — never raw current/effective resource values, inspect
JSON, PreviousSpec content, labels, or identifiers.

- access log: resource_policy_* attrs appended alongside the existing
  correlation attrs, only when evaluated.
- audit log: an optional "resource_policy" object. The audit event
  takes its own value copy (auditResourcePolicyContextFrom) rather than
  the pooled pointer — RequestMeta's pool return (and zeroing) happens
  before the audit event reaches the async writer goroutine, so holding
  the pooled pointer itself would race; the copy is taken synchronously
  in AuditLogMiddleware before the event is queued, matching how every
  other auditEvent field is already a plain value snapshotted at that
  point.
- putRequestMeta returns meta.ResourcePolicy to its own pool before
  zeroing RequestMeta, so the guard's per-request allocation is reused.

* 📝 docs(resource-limit): document the container-update & service resource-limit guard (#152)

- CHANGELOG: [Unreleased] entry covering the 6 new require_* flags, the
  effective-state merge semantics, the two rollback-path closures, the
  4 new reason codes, and the ratchet migration edge.
- README roadmap: sharpen the "Resource parity" v1.6.0 row to name the
  actual delivered flags and rollback-path coverage.
- docs/content/docs/configuration.mdx: new bullets under Request Body
  Inspection explaining the guard's merge semantics, fail-closed reason
  codes, rollout-mode interaction, and the ratchet migration note;
  extends the container_update and service rows in the Request Body
  Policy Reference table; adds the 6 new SOCKGUARD_* env var rows.
- app/configs/sockguard.yaml: commented example block for
  request_body.container_update.* and request_body.service.* resource
  flags, since the shipped default config stays deny-all with no
  request_body section active.

* 🐛 fix(filter): reject ambiguous rollback query values in the resource-limit guard

?rollback=previous&rollback=none (or any repeated rollback param) let the
guard silently pick one of Docker's parsed values while validating a
different rollback intent than the daemon might act on. serviceManualRollbackQuery
now requires exactly one value, denying with resource_limit_request_invalid
otherwise, and guardServiceManualRollback's helper return is checked so a
duplicate-value 400 can no longer fall through to respondAllow.

Refs #152

* 🧪 test(filter): add resource-limit guard unit test matrix

Table-driven coverage for the #152 post-ownership guard: container-update
omitted/zero/null/negative/weaker/stronger/mixed scalar semantics against
moby's Memory:0-is-unchanged merge, PidsLimit's pointer-clear semantics
(0/-1/null), CpuQuota-alone-satisfies-hard vs CpuShares-alone-fails-hard,
the ratchet (weak current + omission denies, one compliant patch
remediates), nested-decoy/case-variant/duplicate-key rejection, inspect
404/500/timeout/oversize/malformed handling with call-counting stubs, a
nil inspector failing closed, the allow_resource_updates gate keeping the
inspector unreachable (panicking stub), and a zero-requirements pure
pass-through regression. Service coverage: create/update Limits matrix,
manual rollback (safe-body/weak-PreviousSpec denies, weak-body/safe-
PreviousSpec allows, fake PreviousSpec/RollbackConfig ignored, version
mismatch 409), automatic rollback via UpdateConfig.FailureAction, reason
code/status/static-message assertions, and rollout-mode softening that
only ever applies to resource_limit_policy_denied. Adds a decode fuzz
target over the container-update body.

Refs #152

* 🧪 test(config): cover the resource-limit require_* config surface

Verifies all 6 require_* flags (4 container_update + 2 service) default
disabled, load from SOCKGUARD_REQUEST_BODY_* env overrides, load from
both the global request_body block and a client profile's request_body
block, map completely onto filter.Options, and stay present via
mapstructure tag reflection so a future field rename can't silently drop
one from the schema.

Refs #152

* 🧪 test(cmd): cover resource-limit guard wiring and the require_* warning

Asserts the guard's layer index sits after withOwnership in
buildServeHandlerLayers's append order and, wired end-to-end through a
real ownership middleware, that a foreign container is denied by
ownership before the guard's daemon GET ever runs (only ownership's own
lookup fires). Covers warnResourceLimitRequireOnce: fires at most once
per process via sync.Once, checks both the default policy and every
client profile for a require_* flag enabled while allow_resource_updates
is false, and stays silent when the gate is open or no require_* flag is
set. Also checks compileClientProfiles carries all 6 resource-limit
flags into a named profile's compiled policy.

Refs #152

* 🧪 test(logging): cover resource-policy audit and access-log fields

Confirms every ResourcePolicyMeta.Result class (allow/deny/would_deny/
invalid/lookup_failed/state_changed) surfaces its resource_policy_* access
log fields and resource_policy audit context, that both are entirely
absent from requests the guard never evaluated, that the sync.Pool zeroes
metadata before reuse, that the audit deep copy survives concurrent pool
reuse under -race, and that the audit context type is reflectively
limited to the bounded classification fields (kind/operation/source/
requirements/result/violation/lookup) with no raw values, IDs, or labels.

Refs #152

* 🧪 test(reload): assert the 6 resource-limit require_* flags stay reload-mutable

Locks in that ImmutableDiff treats all 4 container_update require_* flags
plus allow_resource_updates and the 2 service require_* flags as
hot-reloadable config, matching the design's reload-mutable contract for
#152.

Refs #152

* 🧪 test(integration): validate the resource-limit guard against a real daemon

Wires filter.ResourceLimitGuardWithOptions into the integration handler
chain (post-hijack/pre-ownership in append order, matching production)
with real Docker-socket-backed container and service inspectors. The
container-update tier creates a legacy unlimited container and confirms
an omitted-field update is denied and leaves HostConfig.Memory at 0 (the
Memory:0-is-unchanged encoding is only checkable against a real daemon,
not a mock), then confirms a weaker-but-positive update is forwarded
verbatim and a subsequent denied update leaves daemon state unchanged.
The Swarm service tier stays behind SOCKGUARD_TEST_ENABLE_SWARM=1, refuses
to run against a docker daemon already in a swarm, and always leaves via
force in t.Cleanup; it covers create/full-replacement-update denial,
manual rollback to a weak PreviousSpec vs. a safe one, and automatic
rollback (UpdateConfig.FailureAction=rollback) validating the current
Spec. helpers_test.go gains a shared dockerSocketRoundTripper and
HostConfig.Memory on the container-create request type so the new suite
and the existing direct-socket helpers use one transport constructor.

Refs #152

* 📝 docs(changelog): keep the #152 entry under Unreleased after the v1.5.2 rebase
#183)

* ✨ feat(config): add multi-listener schema and validation (#149)

Config.Listeners []ListenerConfig is additive: legacy listen: stays
untouched forever. EffectiveListeners() is the single read path every
downstream consumer must use — it synthesizes a single "default" entry
with allowed_profiles ["*"] when Listeners is empty, so back-compat is
byte-for-byte for every config that only sets listen:.

- allowed_profiles: required non-empty on explicit entries, "*"
  wildcard preserves legacy global behavior, reserved as a profile
  name, cannot mix with concrete names.
- listen/listeners mutual exclusivity, provenance-tracked (YAML key,
  SOCKGUARD_LISTEN_* env var, or --listen-socket flag all count as
  "explicit") via a defaults-free probe Viper pass in load.go.
- Validation rewrite: listener name regex/uniqueness/cap 32/reserved
  "admin", exactly-one-of-socket-or-address per explicit entry,
  all-pairs bind-target uniqueness across listeners[*] and admin.listen.
- ListenConfig gains optional socket_uid/socket_gid; socket_mode is
  0600 (default) or 0660 (requires explicit socket_gid) everywhere a
  ListenConfig appears (legacy listen, listeners[*], admin.listen).
- Admin.MountOn: required when admin rides a main listener and there
  are 2+ effective listeners, so admin traffic doesn't silently mount
  on every listener by default.

* 🔧 chore(config): use tagged switch for profile-name validation

Style/lint fixup on the #149 wildcard-profile-name reservation check —
staticcheck's QF1002 flagged the plain switch over name == X comparisons
as better expressed as a tagged switch on name.

* ✨ feat(reload): per-listener immutable diff projection (#149)

ImmutableDiff now branches on explicit listeners: usage. Legacy configs
(both old/new have an empty Listeners list) keep reporting a single
"listen" key exactly as before. Once either side uses the explicit
listeners: list, diffListeners takes over: the listener set is immutable
by name (add/remove/rename/rebind all reject, reorder is a no-op), and
every per-listener field is immutable except allowed_profiles — the sole
reload-mutable field, consistent with clients.profiles already being
reload-mutable. Switching between legacy and explicit list mode is
immutable even when structurally a no-op, since the two modes bind
through different code paths.

Deviation: diffListenerFields compares TLS as a single whole
(reflect.DeepEqual) rather than per-subfield (cert_file/key_file/etc
individually) — coarser than the source design sketch, but every entry
still names exactly which listener and that "tls" changed.

* ✨ feat(banner): render one entry per bound listener (#149)

Info.Listen (single string) becomes Info.Listeners ([]string) — one
"name unix:<path>" / "name tcp://<addr>" entry per effective listener,
in bind order, followed by the dedicated admin listener's entry when
configured. Render must only ever be called after every listener in the
list has bound and passed the publish barrier, so the banner is a
confirmation rather than a promise.

* ✨ feat(inbound): stamp non-spoofable listener identity + admission gate (#149)

New internal/inbound package stamps every accepted connection with an
Identity{Name, Role, Network} via http.Server.ConnContext, composed
ahead of clientacl's own ConnContext so identity is derived solely from
which net.Listener accepted the connection — never from request
content.

withListenerAdmission enforces each listener's allowed_profiles scope
using that identity plus the profile clientacl already resolved. A
listener with the wildcard AllowedProfiles admits everything, matching
pre-#149 behavior byte-for-byte. The gate is a complete no-op when
cfg.Listeners is empty (legacy singular listen: config, which always
synthesizes one wildcard listener) — the fail-closed missing-identity
check only activates once an operator opts into explicit listeners:.

* ✨ feat(logging): tag access + audit records with listener name (#149)

Both RequestMeta.ListenerName (access log) and auditEvent.ListenerName
(audit log, JSON "listener_name") are populated from the connection's
inbound.Identity — never request-controllable. Additive: audit's
existing TransportListener field keeps its pre-existing "unix"/"tcp"
transport-kind meaning unchanged.

* ✨ feat(cmd): bind + serve multiple independently scoped main listeners (#149)

Adds the serverGroup runtime for Config.EffectiveListeners():

- serve_listeners.go: two-phase bind barrier (bindMainListeners binds
  every main listener before any of them serves; a failure partway
  through closes everything already bound, in reverse order, with no
  window where a strict subset is live), publishMainListeners fans
  every member's terminal Serve() error into one buffered channel
  (listenerResult), and concurrent shutdownMainListeners tears every
  member down in parallel within the shutdown grace period. Unix
  socket members capture a (dev, ino) identity right after bind so
  shutdown only unlinks a socket path that still resolves to the exact
  inode this process created (removeSocketIfOwned) — legacy
  listen.socket/admin.listen.socket keep the pre-#149 unconditional
  removal, unchanged.

- serve_deps.go: createSocketListener is generalized to take an
  explicit file mode + optional uid/gid (socketListenFileMode mirrors
  config's own validateSocketOwnership rules), and a new
  createNamedListener binds one listeners[*] entry (unix or TCP).
  listenUnixSocketWithMode adds a stale-socket guard: any live dial on
  the target path refuses to bind (steals nothing); a failed dial
  (refused, ENOENT, timeout) proceeds with the pre-existing
  remove-then-listen sequence, since only the connect-succeeded case
  risks stealing a listener that's actually serving traffic.

- serve.go: wires withListenerAdmission into the shared handler chain
  ahead of withClientACL's own position, and replaces the single
  bind/serve/shutdown block with the serverGroup — one
  reload.SwappableHandler shared by every main listener, the admin
  server bound+served sandwiched between the main bind and main serve
  phases, and concurrent shutdown of every main member alongside the
  admin server.

Existing tests updated for the new shapes: TestBuildServeHandlerLayers
gets the new "withListenerAdmission" layer in its pinned lists: and
TestRunServe_AdminShutdownErrorLogs is rewritten to not assume
admin-then-main shutdown ordering (now concurrent) — the old version's
shared shutdownCalls counter read/written from two goroutines was
itself a data race under -race.

* ✨ feat(observability): per-listener metrics + close admin mount_on gap (#149)

🐛 fix(cmd): every main listener shares one handler chain
  (reload.SwappableHandler), so an in-band admin endpoint (admin.enabled,
  no dedicated admin.listen) was reachable on every listener regardless
  of admin.mount_on — validation already required mount_on once there
  are 2+ effective listeners, but nothing enforced it at request time.
  New mountOnGate wraps withAdminEndpoint/withPolicyVersionEndpoint and
  only forwards to them when the connection's inbound identity matches
  Admin.MountOn; with <=1 effective listener it's a pass-through,
  preserving today's zero-config "admin rides the sole main listener"
  behavior byte-for-byte.

✨ feat(metrics): add a listener label to requestLabels/denyLabels/
  durationLabels (sourced from inbound.Identity, falling back to
  "default" when absent — matching the legacy synthesized listener
  name), and a new sockguard_listener_up{listener,role,network} gauge.
  SetListenerUp is called with up=true right after publish for every
  main listener and the admin listener, and up=false at the start of
  drain in shutdownServers — the series is created once and never
  removed, so a stopped listener reads 0 rather than disappearing.

* ✨ feat(health): surface per-listener state in /health (#149)

health.HealthResponse gains a Listeners []ListenerStatus field
(name/role/network/state; states bound -> serving -> draining ->
stopped on a clean run, or failed if Serve() returns unexpectedly
before an intentional drain). Monitor.ListenersFunc, when set, both
populates that field and folds listener state into the 503 decision:
any listener not "serving" or "draining" makes /health unhealthy
alongside (or independently of) an upstream failure.

internal/cmd wires this with a new listenerStatusBoard — a small
mutex-guarded map keyed by listener name, updated at every lifecycle
transition (bind, publish, fan-in failure, shutdown start, shutdown
complete) for both main listeners and the dedicated admin listener.
Both the liveness and readiness monitors share the same board, so
either endpoint reports the same listener state. Threaded as an
explicit board parameter through bindMainListeners,
publishMainListeners, shutdownMainListeners, and shutdownServers.

* 🐛 fix(config): transport-capability checks honor listeners: (#149)

validateClientsListenerExclusions and validateClientsCertificateProfiles
read cfg.Listen directly, so clients.unix_peer_profiles,
clients.client_certificate_profiles, clients.allowed_cidrs,
clients.container_labels, and clients.source_ip_profiles all rejected
valid configs that used the new listeners: list instead of the legacy
listen: block — every listeners[*] entry was invisible to these checks
regardless of its actual transport. New hasEffectiveListener walks
cfg.EffectiveListeners() so "at least one compatible listener" holds
for both legacy and explicit multi-listener configs, matching the
final design's item 12. Found by writing configs/multi-listener.yaml
against the real validator.

* 📝 docs(listeners): document multi-listener config and ship example preset

- CHANGELOG entry for the `listeners:` feature (#149): allowed_profiles
  semantics, mutual exclusivity with legacy `listen:`, admin.mount_on
  requirement, and observability additions
- README: listeners: config snippet and a link to the new preset under
  the bundled-presets list
- app/configs/multi-listener.yaml: working two-unix-socket preset
  demonstrating scoped ci/ops listeners with disjoint allowed_profiles

* 🐛 fix(cmd): fail startup on ambiguous stale-socket probe results (#149)

Only a proven ECONNREFUSED probe result, plus a matching Lstat inode/device
identity check taken before and after the probe, may remove an existing unix
socket file as stale. Every other probe outcome (a successful dial, a
timeout, a permission error, or the path changing identity mid-probe) now
fails startup instead of unlinking a socket sockguard can't prove is dead —
closing the gap where a dial timeout was previously treated as "not live"
and removed.

* 🐛 fix(cmd): bind admin listener inside the two-phase bind barrier (#149)

bindAdminServer now binds the dedicated admin listener as the final member
of the same all-or-none bind transaction as the main listeners: mains bind,
then admin binds, and only after every bind in the group has succeeded does
anything start Serve. An admin bind failure rolls back every already-bound
main listener (reverse order); a main listener bind failure never reaches
the admin bind at all. This replaces the old startAdminServer path, which
bound and started serving the admin listener strictly after the main-listener
bind barrier had already published — a window where main listeners could be
live while the admin bind was still pending, or vice versa on failure.

Folded in as part of the same rewrite (interdependent within
runServeWithDeps): any main or admin Serve() return before an intentional
group drain is now fatal (including nil and http.ErrServerClosed, neither of
which can legitimately occur while the group is healthy), draining the whole
group before returning a process error; listener-group shutdown now runs
under a fresh 30s deadline instead of the already-cancelled command context,
force-closes listeners/servers at the deadline, and explicitly tears down
hijacked connections (Docker attach/exec streaming) that http.Server.Shutdown
does not close on its own.

Removes the now-dead startAdminServer function and its direct-unit tests,
replacing them with equivalent bindAdminServer coverage.

* 🐛 fix(listeners): close remaining #149 design-conformance gaps

- config: bind-target uniqueness across listeners/admin now compares a
  normalized TCP endpoint (case-insensitive host, canonical IP spelling,
  numeric port without leading zeroes) instead of a raw string, so
  differently-spelled duplicates of the same literal endpoint are still
  caught. Explicit TLS on a unix listener and socket_uid/socket_gid on a
  TCP listener are now rejected instead of silently ignored.
- health: the /health response always encodes "listeners" as an array
  (empty, not omitted) so clients get one stable response schema whether
  or not a ListenersFunc is wired up.
- reload: ImmutableDiff reports individual TLS subfields
  (listeners.<name>.tls.cert_file, .key_file, ...) instead of collapsing
  any TLS change into a single opaque "tls" entry, so a failed reload's
  diagnostic names the exact field an operator needs to restart for.

* 🧪 test(config): cover multi-listener schema, validation, and exclusions (#149)

Table-driven and fuzz coverage for the listeners: schema: name/uniqueness/
reserved-name/cap rules, exactly-one-of-socket-or-address, TLS/ownership
field misuse per transport, allowed_profiles wildcard-vs-concrete semantics,
all-pairs bind-target uniqueness (including the normalized-TCP-endpoint
cases), and validateClientsListenerExclusions' unix-only/TCP-only client
constraint checks. FuzzEffectiveListenerValidation exercises
EffectiveListeners() plus validation against arbitrary listener sets to
guard the never-panics / never-silently-accepts-conflicting-config
invariant.

* 🧪 test(inbound): pin non-spoofable listener identity propagation (#149)

Covers ConnContext composition (identity stamped before the existing
clientacl.ConnContext runs, so both are present together), context
round-tripping via inbound.FromContext, and that identity is derived only
from the listener the connection actually arrived on — never from anything
attacker-controlled in the request itself.

* 🧪 test(observability): cover per-listener health, metrics, and log fields (#149)

health: /health returns 503 whenever any required listener isn't serving
outside an intentional drain, and always encodes "listeners" as an array.
metrics: sockguard_listener_up{listener,role,network} and the listener
label on the request/duration/deny/throttle families. logging: access and
audit records carry listener/listener_name without disturbing the existing
audit transport field.

* 🧪 test(reload): cover per-listener immutable diff and reload rejection (#149)

listeners_diff_test.go/listeners_diff_fuzz_test.go pin ImmutableDiff's
per-name per-field projection: allowed_profiles is the only field a swap
may change, every other field (including individual TLS subfields) is
immutable and reported as listeners.<name>.<field>, and add/remove/rename
are reported distinctly from a same-name field change.
TestReloadCoordinatorFailedListenerReloadLeavesOldGenerationServing (new,
in serve_reload_test.go) proves a failed reload — whether from an invalid
allowed_profiles value or an attempted immutable-field change — keeps the
old handler generation live and serving rather than falling through to a
deny-all handler.

* 🧪 test(cmd): cover bind barrier, listener admission gate, and default-profile warning (#149)

serve_listeners_feature_test.go: the stale-socket probe safety matrix (dead
refused socket replaced; live socket, ambiguous timeout, non-socket file,
and inode-changed-mid-probe all preserved and fail startup); the bind
barrier rolling back main listeners in reverse on an admin bind failure and
never reaching/serving admin on a main bind failure; any premature Serve
return draining and failing the whole group; the listener-status board
state machine and concurrent access; pre-publish gauge registration; and
shutdown using a fresh deadline, force-closing at it, and tearing down
hijacked connections within it.
serve_listener_admission_test.go: withListenerAdmission on the single
shared handler chain denies 403 listener_profile_not_allowed for a resolved
profile outside a listener's allowed_profiles, admits wildcard/matching
listeners, and fails closed (500) on missing/unknown inbound identity.
serve_listener_warning_test.go: the startup warning when clients.default_profile
isn't included in a listener's allowed_profiles, silent for wildcard or
explicitly-included cases.

* 🧪 test(integration): exercise unix multi-listener profile isolation end-to-end (#149)

Builds the real binary and drives it against two unix-socket listeners
(ci/ops) with disjoint allowed_profiles over a mocked upstream: the same
unix-peer-uid-derived ci profile is served on its own listener and denied
403 listener_profile_not_allowed on the other, proving profile-bleed
across listeners is impossible end-to-end, not just at the unit level.

* 📝 docs(changelog): keep the #149 entry under Unreleased after the v1.5.2 rebase

* 🐛 fix(logging): dedupe audit event fields after the #152 logging rebase

* 🧪 test(cmd): cover #149 listener-bind and socket-identity branches under the coverage gate

PR #183 landed at 95.8% production coverage against the 96% CI floor
(run 31004873914). The #149 multi-listener surface left several branches
entirely unexercised because higher-level bind-barrier tests only ever
drive them through stubbed deps fields:

- createNamedListenerImpl, chownSocket, defaultProbeUnixSocket, and
  createSocketListener's chown-success/chown-failure paths in
  serve_deps.go were all 0% — every existing test stubs
  deps.createNamedListener/deps.probeUnixSocket directly instead of
  calling through to the real implementation.
- statSocketIdentity and socketIdentityFromFileInfo's guard branches
  (empty path, nil lstat func, nil FileInfo, non-*syscall.Stat_t Sys())
  and removeSocketIfOwned's inode-match/mismatch happy path were
  uncovered because the one integration test that reaches them never
  hits a real matching inode.
- hijackedConnTracker.transition's nil-receiver guard, its
  already-closed-tracker branch, and the plain StateClosed case, plus
  listenerStatusBoard's nil-receiver guards and setState-on-unknown-name
  no-op, were never called outside their always-non-nil call sites.

Added direct table-driven and real-socket unit tests for each, matching
the existing shortSocketPath/newServeTestDeps/socketFileInfo test
helpers. Local production coverage: 96.3% (mirrors ci-verify.yml's
coverage.prod.txt gate).
)

* ✨ feat(policy): add shared JSON codec for admission-mutation writes

Introduces internal/filter/json_mutate.go, a fold-aware JSON document
codec for code that writes into (not just reads) a Docker API request
body: exact-duplicate-key rejection via a token-scan pass (map-decode
silently collapses exact duplicates before any value-tree check could
see them), depth/node-count/EOF/root bounds, and a canonicalizing parse
usable both before and after a mutation is applied.

internal/ownership/middleware.go's nestedObject/nestedObjectPath/
foldedObjects/foldedStrings/foldedArrays/foldedStringEquals were
independently-maintained copies of the same fold/merge logic; they now
call the exported filter.NestedObject/NestedObjectPath/FoldedObjects/
FoldedStrings/FoldedArrays/FoldedStringEquals instead, so there is one
reviewed implementation shared by owner-label stamping and the new
admission-mutation engine (a following commit).

* ✨ feat(policy): add per-request admission-mutation logging record

Adds logging.MutationRuleOutcome/MutationRecord (a pooled per-request
trace of which admission-mutation rules matched and what they did) and
wires it into the existing access/audit pipeline:

- RequestMeta gains a Mutation field; putRequestMeta returns it to a
  dedicated sync.Pool before zeroing, matching the existing pooling
  pattern for RequestMeta itself.
- AccessLogMiddleware appends mutation_rule_ids/mutation_changed to the
  structured log line and elevates an otherwise-allowed request to WARN
  when a warn-mode mutation rule was evaluated, mirroring how a
  warn-rollout policy deny is already elevated.
- AuditLogMiddleware deep-copies the pooled MutationRecord into a fresh
  auditMutationRecord before the event is handed to the async audit
  channel — required because the pooled record can be recycled for an
  unrelated request before the channel consumer encodes it.

No production code path populates these yet; the admission-mutation
engine that does lands in a following commit.

* ✨ feat(policy): add fail-closed declarative admission-mutation engine

Adds internal/filter/mutation.go: a bounded, config-driven admission
mutation engine (#151) supporting exactly two rule operations
(inject_labels label-map merge, remap_image single string-field
replace) with per-rule enforce/warn/audit rollout modes.

Core invariants:

- enforce-mode rules are applied to the actual document that will be
  forwarded; warn/audit-mode rules are evaluated only against an
  independent deep-cloned shadow document and never influence what is
  committed.
- The request body is only read/rewritten when at least one rule is
  configured for the request's surface, and only actually rewritten
  (replaceRequestBody) when an enforce rule changed something —
  otherwise the original bytes readBoundedBody restored stay in place
  byte-for-byte.
- Every mutation result is re-parsed through the same strict scanner
  used on the client's input (parseMutationDocument) before it can be
  forwarded — canonicalize-before-and-after, not just before.
- remap_image validates its computed result with go-containerregistry's
  weak-reference grammar (the same grammar imagefetch.PinnedReference
  already applies), invoked only when a remap_image rule is configured
  AND matched — mirroring how the existing (also opt-in) image-trust
  verifier only ever touches go-containerregistry when image_trust is
  configured.

Two supporting pieces in filter's existing infrastructure:

- body_read.go: replaceRequestBody, a transport-safe body-rewrite
  helper (Content-Length/Transfer-Encoding/GetBody kept in lockstep;
  the proxy layer only ever reads req.ContentLength, never the literal
  header).
- request_rejection.go: requestRejectionError gains an optional
  reasonCode override so the engine's four new reason codes
  (mutation_request_invalid, mutation_request_too_large,
  mutation_apply_failed, mutation_postcondition_failed) survive to the
  denial response instead of collapsing to the generic status-derived
  code.

Nothing in filter's dispatch tables calls this engine yet; that wiring
lands in the next commit so this one stays independently buildable and
reviewable.

* ✨ feat(policy): wire admission mutations into the filter dispatch chain

Wires the mutation engine from the previous two commits into
compileRuntimePolicy's dispatch table (#151):

- filter.Options gains a Mutation field; MiddlewareWithOptions compiles
  one mutationEngine per MiddlewareWithOptions call (reload rebuilds it
  along with everything else) and shares it identically across the
  default policy and every client profile — mutation config is global,
  not per-profile (see mutation.go's MutationOptions doc comment).
- compileRuntimePolicy registers the two mutation entries
  (newContainerCreateMutationPolicy / newServiceMutationPolicy)
  immediately BEFORE the existing container_create/service entries, at
  the same (method, matches, severity) tuple. inspectAllowedRequest
  buckets matches by severity and runs every policy in the single
  matched bucket in slice order — verified by re-reading
  middleware.go's own inspectAllowedRequest, not assumed — so this
  placement guarantees mutation always applies/canonicalizes before
  container_create/service's own body inspection runs against whatever
  bytes are in r.Body afterward.
- MiddlewareWithOptions stashes the resolved logging.RequestMeta onto
  the request's context (logging.WithMeta) so mutation.go's
  recordMutationOutcome — an inspectorFunc, which only ever receives
  *http.Request — can reach it without widening that signature for
  every other existing inspector.
- inspectAllowedRequest's rejection-handling branch now prefers
  requestRejectionError.reasonCode when the inspector set one
  explicitly, falling back to the existing status-derived code
  otherwise — needed for the mutation engine's four reason codes to
  actually reach the denial response.

Existing compileRuntimePolicy(nil, PolicyConfig{}) call sites in
middleware_test.go and middleware_method_dispatch_test.go are updated
for the new third parameter (nil mutation engine — rulesFor(nil) is a
documented no-op).

* ✨ feat(policy): add mutations.rules[] config schema and validation

Adds the operator-facing config surface for #151:

- config.go: MutationsConfig / MutationRuleConfig /
  InjectLabelsMutationConfig / ImageRemapMutationConfig, plus
  Config.Mutations. Deliberately global (not part of
  clients.profiles[]) — v1 has one mutation authority and no
  global/profile merge rules.
- load.go: decodeMutationsStrict re-decodes the mutations subtree with
  ErrorUnused=true, WeaklyTypedInput=false, and no decode hook, on top
  of (and overwriting) the lenient Config-wide decode every other
  block still gets. Unlike the rest of this legacy schema, a typo'd key
  or a coerced "id: 0" is a load-time error here, not a silently
  accepted no-op — called from both Load and LoadBytes so the admin
  /admin/validate and signed-policy-bundle paths get the same
  strictness as a file-based startup load.
- validate.go: validateMutationsConfig enforces rule/label-count
  bounds, id format + uniqueness, mode/surface enums, exactly-one-of
  inject_labels/remap_image, label-surface restriction (inject_labels
  invalid on service_update), key/value size and control-character
  bounds, exact-match image literals parsing via go-containerregistry's
  weak-reference grammar, cross-rule overlap rejection (same label key
  or overlapping image `from` pattern on a shared surface — which rule
  would win is otherwise order-dependent), and rejects injecting the
  reserved ownership.label_key when ownership.owner is configured.
- filter_options.go: MutationsConfig.ToFilterOptions converts to
  filter.MutationOptions.
- cmd/serve.go: serveFilterOptions attaches
  cfg.Mutations.ToFilterOptions() to filter.Options.Mutation.

internal/config now imports go-containerregistry for exact-match image
literal validation at config-load time — outside the request hot path,
and only reached when an exact-match remap_image rule is actually
configured, mirroring how image_trust's use of the same dependency is
conditioned on that feature being configured.

* 📝 docs(policy): document fail-closed admission mutations

- CHANGELOG.md: ### Added entry for mutations.rules[] (#151).
- README.md: policy-surfaces summary line now mentions declarative
  admission mutation.
- docs/content/docs/configuration.mdx: new "Admission Mutations"
  section — schema table, example, the global-not-per-profile scope
  decision, and the strict-decode behavior.
- app/configs/cis-docker-benchmark.yaml: commented example rule tagging
  admission-approved containers with a compliance label, off by
  default.

* 🐛 fix(policy): harden admission-mutation body handling and validation

Fixes surfaced while writing the mutation test suite:
- config/validate.go: reject empty inject_labels values (spec requires
  non-empty label values; previously only whitespace/control chars and
  size were checked).
- filter/body_read.go: replaceRequestBody now clones the final byte
  slice before installing it as the request body/GetBody closure, so a
  caller mutating its buffer after commit can never retroactively alter
  bytes already forwarded or replayed.
- filter/mutation.go: stop passing decode/apply errors to
  logRequestError — those errors can carry body-derived text (e.g.
  offending key/value fragments), which the design's logging invariant
  explicitly forbids; only the generic denial message and reason code
  are now logged.

* 🧪 test(filter): cover admission-mutation engine and JSON codec

- json_mutate_test.go: ambiguity corpus (exact/folded duplicates at
  root/nested/in-array, case-sensitive data-map exemptions, trailing
  data, null/array/scalar roots, depth/node caps), numeric-lexeme and
  JSON-looking-string preservation across marshal/reparse, and
  replaceRequestBody transport-state reset including anti-aliasing of
  the caller's buffer.
- mutation_test.go: full-chain integration per surface (container
  create, service create, service update) proving injected/remapped
  bytes reach the required-label check, registry policy, and
  image-trust verifier on the post-mutation reference; warn/audit
  byte-for-byte transport non-interference; audit-record outcome
  vocabulary with no sensitive value leakage; fail-closed behavior on
  malformed/duplicate-key/oversized/unparseable-remap bodies and body
  read errors, with zero upstream calls.
- FuzzMutationRoundTrip seed corpus and fuzz test: parse/mutate/
  marshal/reparse invariants (forwardable output reparses clean with
  no residual ambiguity).

* 🧪 test(config): cover mutations.rules[] schema and validation

Strict-decode rejection of unknown keys and privileged-injection
vectors (set_json/patch/path/exec/webhook) at every level of the
mutations subtree, both via Load and LoadBytes; YAML typing-attack
corpus (wrong scalar/list/object shapes); rule-shape validation (id
pattern/length, mode enum, surfaces enum/dedup, exactly-one-of
inject_labels/remap_image, service_update label rejection); bounds
(64 rules, 32 labels/rule, 256 labels total, key/value/image byte
caps); label key/value character rules; owner-label-key reservation
when ownership is enabled; overlap rejection (same label key on a
shared surface, exact-in-prefix and prefix-in-prefix image matches)
alongside acceptance of genuinely non-overlapping rules; and
ToFilterOptions type-preserving translation to filter.MutationOptions.

* 🧪 test(cmd): cover admission mutations through the built serve chain

Full buildServeHandlerLayers integration per surface (container
create, service create, service update) via a real upstream test
server: label injection satisfying a downstream required-label check
with a single canonical Labels field forwarded, service create
mutating both root and TaskTemplate.ContainerSpec label maps plus the
task image, and service update remapping the image while preserving
the ?version= query string and a >53-bit Version.Index integer
untouched.

* 🧪 test(logging): cover mutation record pooling and audit deep-copy

MutationRecord pool round-trip zeroes Rules/ActualChanged/
HasWarnEvaluation on reuse; newAuditMutationRecord deep-copies rule
outcomes so mutating or reusing the pooled source after enqueue can't
retroactively change an already-emitted audit event; nil/empty
records are omitted from the audit event rather than emitted empty.

* 🧪 test(reload): assert mutations config stays hot-reload mutable

mutations and every mutations.* subtree must be absent from
ImmutableFields — admission-mutation rules are meant to be editable
via config hot-reload, not restart-only.

* 🔧 chore(deps): promote mapstructure to a direct dependency (#151)

decodeMutationsStrict imports go-viper/mapstructure/v2 directly for the
strict mutations-subtree decode; go mod tidy moved it out of indirect.

* 📝 docs(changelog): keep the #151 entry under Unreleased after the v1.5.2 rebase

* 🐛 fix(config): normalize remap_image.match in ToFilterOptions (#151)

Validation accepts remap_image.match case-insensitively but never wrote
the canonical form back onto the config value, so ToFilterOptions was
handing a raw "Exact"/"Prefix" straight to the filter engine and relying
on filter.newMutationEngine's own normalization to save it. Normalize in
ToFilterOptions too, so this package's output is canonical independent of
that internal detail.

Addresses CodeRabbit comment on PR #181 (validate.go:742).

* 📝 docs(mutations): clarify audit-mode behavior and prefix-match scope (#151)

- cis-docker-benchmark.yaml: the compliance-label example comment claimed
  sockguard marks approved containers, but the pasted rule is mode: audit,
  which only records what would happen and never writes the label.
  Restate that plainly instead of leaving it to the trailing note.
- configuration.mdx: remap_image.match is a literal-string match with no
  docker.io/library alias expansion, so the pin-internal-registry
  docker.io/ example never touches an unqualified alpine:3.21 or
  nginx:1.27 reference. Add a second exact-match rule plus a paragraph
  spelling that out, and note in the field table that inject_labels keys
  and values must both be non-empty (whitespace-only included).

Addresses three CodeRabbit comments on PR #181 (cis-docker-benchmark.yaml:136,
configuration.mdx:669, configuration.mdx:676).

* 🧪 test(filter): cover admission-mutation branches under the coverage floor (#151)

mutation.go and json_mutate.go landed under the 96% production coverage
gate (95.8% on PR #181, CI run 31003217007). Add table-driven and direct
unit tests for the branches that were missing real coverage: default rule
mode, malformed-rule skip, nil-engine/nil-doc/unknown-kind guards, empty
body and non-POST/nil-body/nil-request inspect() short-circuits, remap
no-op paths (unsupported surface, absent target field, from mismatch,
result equal to current), empty-result and non-object-target failure
denials, mutationRemapMatch's full match-kind table, the JSON strict
scanner's depth/node/EOF/truncation error paths for both object and array
nesting, deepCloneJSONValue's array branch, the Folded* helper family,
soleFoldedObject/navigateFoldedObjectPath's not-found paths, and
NestedObject's nil-variant skip.

internal/filter production coverage: 96.1% -> 98.2%.

* 🧪 test(logging): cover mutation record pool fallback and access log fields (#151)

Go coverage is per test-binary: internal/filter's mutation tests already
drive AccessLogMiddleware end to end with mutation records attached, but
that doesn't count toward internal/logging's own coverage since it's a
different package's test binary. Add direct logging-package tests for
joinMutationRuleIDs, the WARN-elevation branch for an allowed request with
a warn-mode mutation evaluation, the omitted-fields case when no rule
matched, and GetMutationRecord's defensive nil-fallback when the pool
returns a wrong-typed value (mirroring the existing requestMetaPool
fallback test's New-override + drain pattern, since sync.Pool's internal
LIFO/victim-cache ordering means Put alone isn't reliable here).

internal/logging production coverage: 97.0% -> 98.5%.
…unnel (#153) (#184)

* ✨ feat(filter): validate Engine API 1.55 and gate BuildKit's opaque tunnel

Closes the scoped gap list from #153:

- New insecure_accept_opaque_buildkit_tunnels ack gates any rule that
  would admit POST /session, POST /grpc, or a moby.buildkit.v1.Control
  method path (mirrors validateBodyBlindWriteRules /
  validateReadExfiltrationRules). Tecnativa's GRPC=1/SESSION=1 compat
  env vars auto-set the new ack with a deprecation warning so existing
  configs keep working.
- New drydock-with-build.yaml / portwing-with-build.yaml presets ship
  classic-builder-only POST /build support (DOCKER_BUILDKIT=0) on top
  of the respective -with-compose.yaml baseline, without opening the
  BuildKit session/gRPC tunnel; the -with-compose.yaml header comments
  now point to them instead of describing the gap as unresolved.
- POST /containers/create denies unknown HostConfig.Mounts types
  fail-closed, validates VolumeOptions/ImageOptions.Subpath against
  path-traversal escapes, and gates privilege-escalating
  TmpfsOptions.Options (exec/dev/suid) behind the new
  allow_tmpfs_privileged_options.
- POST /networks/create denies an explicit EnableIPv4: false unless
  allow_disable_ipv4 is set; endpoint GwPriority now falls under the
  existing allow_endpoint_config gate on both /networks/*/connect and
  containers/create's NetworkingConfig.EndpointsConfig.
- GET /images/{name}/attestations?statement=true is denied by default
  (new response.allow_attestation_statements); new
  response.redact_host_topology redacts GET /info host-fingerprinting
  fields independent of Swarm mode.
- X-Registry-Auth / X-Registry-Config headers are bounded-decoded
  (8 KiB cap, standard/URL-safe/unpadded base64) on image pull, image
  push, and build before use; X-Registry-Auth serveraddress is checked
  against the configured registry allowlist when one is set.

BuildKit gRPC mediation (parsing/enforcing policy inside the tunnel)
is deferred to a v1.7 epic, not implemented here.

Refs: #153

* 🧪 test: cover Engine API 1.55 validation and BuildKit tunnel gating

- internal/cmd: startup-validator coverage for
  validateBuildkitTunnelRules(ForPolicy) (global + per-profile), plus
  TestPresetConfigsDenyAttestationStatementsByDefault, which walks
  every app/configs/*.yaml preset (not a fixed list) and asserts each
  denies attestation statements by default.
- internal/config: compat_test.go covers GRPC/SESSION auto-acking
  insecure_accept_opaque_buildkit_tunnels with a deprecation warning,
  and that it does not override an explicit ack or fire without those
  env vars.
- internal/filter: mount type/subpath/tmpfs coverage
  (container_create_test.go), network EnableIPv4/GwPriority coverage
  (network_test.go), registry-header decode coverage including
  base64-variant, duplicate-key, and credential-non-leak cases
  (registry_auth_test.go, build_test.go, image_pull_test.go),
  TestContainerUpdateResourceControlFieldsCompleteness pinning the
  full guarded field set including all five blkio arrays, and new
  fuzz-seed corpus entries (/v1.55/, /session, /grpc) across
  FuzzPathMatch/FuzzGlobToRegex/FuzzNormalizePath/FuzzCompileRule.
- internal/responsefilter: host-topology redaction and
  attestation-statement gating coverage.
- New TestMaxSupportedEngineAPIVersionPin backed by
  app/testdata/docker-api/max-supported-version.txt.
- New TestServeHandlerRejectsH2CClientPreface integration test proving
  a raw HTTP/2 client preface is parsed as an ordinary, policy-denied
  HTTP/1.1 request rather than tunneled or upgraded.

Refs: #153

* 📝 docs: document Engine API 1.55 and BuildKit transport changes

- security.mdx: new "Compose / BuildKit Transport" section under
  Layer 5 with a supported-transport matrix (classic builder vs.
  BuildKit session/gRPC vs. native gRPC-over-h2c) and the
  insecure_accept_opaque_buildkit_tunnels acknowledgment story.
- presets.mdx: document drydock-with-build.yaml and
  portwing-with-build.yaml; the -with-compose.yaml entries now point
  to them instead of describing the build gap as unresolved.
- configuration.mdx: document all five new fields
  (insecure_accept_opaque_buildkit_tunnels, response.redact_host_topology,
  response.allow_attestation_statements,
  request_body.network.allow_disable_ipv4,
  request_body.container_create.allow_tmpfs_privileged_options) in the
  YAML sample, prose, reference table, and environment-variable table;
  also documents the new bounded X-Registry-Auth/X-Registry-Config
  header decoding and the mount type/subpath/GwPriority validation.
- CHANGELOG.md: Unreleased entry covering all of the above.

Refs: #153

* 🔧 chore(ci): add monthly Engine API version watch

New quality-api-version-watch.yml fetches Docker's public Engine API
version-history page monthly, extracts the highest documented v1.NN,
and fails the job — a red, human-visible CI check rather than a
quietly-filed bot issue — when it exceeds the pin in
app/testdata/docker-api/max-supported-version.txt (currently 1.55).
A maintainer then reviews the new API's changelog entry, updates the
pin, and files follow-up filter work for any new inspectable fields.

Refs: #153

* 📝 docs(response): correct redact_host_topology fields and classic-build body wording (#153)

- configuration.mdx said redact_host_topology strips kernel/OS/arch/
  name/labels; the implementation clears Containerd, FirewallBackend,
  DiscoveredDevices, and NRI. All four mentions now list the real
  fields.
- security.mdx called the classic POST /build body JSON-decodable; it
  is a bounded, inspectable tar build-context stream.
…predicates, hijack parity (#148) (#189)

* 🐛 fix(filter): strip Podman's three-part semver version prefix (#148)

stripVersionPrefix consumed at most one optional ".N" group, so a
versioned Podman libpod client (which sends its full daemon semver,
e.g. /v5.0.0/) fell through with the version prefix still attached and
could never match a /libpod/**-shaped rule. Add a second optional .N
component; Docker's own vN/vN.N prefixes are unaffected.

- Add consumeOptionalDotDigits helper and apply it twice in
  stripVersionPrefix.
- Extend TestStripVersionPrefix, TestStripVersionPrefixMatchesLegacyRegex
  (regex oracle widened to {0,2} dot-groups), and TestNormalizePath with
  three-part/four-part/adversarial-digit-run cases; add a dedicated
  three-way equivalence test for /libpod/, /v1.45/libpod/, and
  /v5.0.0/libpod/ all normalizing identically.
- Add a redactDeniedPath regression for a three-part-prefixed denied path.
- Add a NormalizePath bench case and FuzzNormalizePath seeds for the
  three-part form.
- Pin internal/metrics's independent stripVersionPrefix already handles
  three-part versions correctly (no fix needed there).
- Update the differential package's oracle comments and flip the
  three-part-version evasion test case now that it's a valid prefix.

* ✨ feat(filter): add libpod path predicates (#148)

New app/internal/filter/libpod_normalize.go: isLibpodPath plus
per-resource matchers (isLibpodContainerCreatePath,
isLibpodPodCreatePath, isLibpodExecCreatePath, isLibpodExecStartPath,
isLibpodContainerAttachPath, isLibpodPlayKubePath) the later PR2+
inspectors will need. Every matcher is exact-prefix-guarded on
"/libpod/" so it can never fire on a Docker-compat path, and vice
versa — zero-alloc string ops matching the style of the existing
exec.go/network.go matchers. Also adds isContainerAttachPath, the
Docker counterpart isLibpodContainerAttachPath needed for the
mutual-exclusivity tests (container-attach previously had no
filter-side predicate at all; the hijack layer matched it inline).

Table tests cover every matcher's positive path plus cross-resource
and Docker-shaped negatives, and a path-adversarial suite from the
#148 design doc: traversal out of /libpod/ collapsing to the Docker
path, doubled/trailing slashes, case variance, and versioned prefixes
(including the three-part Podman form) resolving into the namespace
identically.

* ✨ feat(proxy): recognize libpod hijack endpoints (#148)

Extend isHijackEndpointNormalized to peel a leading "libpod/" segment
before the existing resource/action check, so
POST /libpod/containers/{id}/attach and POST /libpod/exec/{id}/start
upgrade to a raw TCP stream exactly like their Docker-compat
counterparts. Transport plumbing (buffers, half-close, inactivity
timeout) is untouched.

Add filter.IsHijackCandidatePath, exported specifically so a
cross-package test can exercise the real production matchers on both
sides of the proxy/filter split without duplicating either one's
logic (unexported identifiers aren't visible across packages, and
internal/proxy already imports internal/filter for NormalizePath).
TestHijackFilterParity asserts every path the hijack layer accepts —
Docker exec/attach and the new libpod predicates — is covered by a
filter-side matcher and vice versa; a mismatch is a two-parser-drift
smuggling bug per the #148 design doc's "Agreed core" item 3.

* 🧪 test(filter): pin default-deny for all libpod paths (#148)

TestDefaultConfigDeniesAllLibpodPaths asserts config.Defaults()'s
shipped rules — no allow rule for any /libpod/ path, PR1 adds only
routing/normalization plumbing — deny every representative libpod
request: unversioned, Docker-style two-part versioned, and Podman's
three-part semver versioned, covering container/pod create, exec
create/start, attach, play/kube, and the read surface.
…xtures (#148) (#191)

* 🧪 test(filter): capture libpod container-create golden fixtures (#148)

Real POST /libpod/containers/create request bodies captured from a live
podman (client 6.0.2 darwin/arm64) talking to a podman machine VM (Podman
Engine 5.8.1 linux/arm64), covering basic create, privileged, host
netns/pidns/ipcns/userns, container:<ref> namespace sharing, bind/tmpfs
mounts, named volumes, devices, cap_add/cap_drop, seccomp/apparmor/selinux
opts, resource limits (including cpu.shares-only), systemd mode, custom
idmappings, labels, sysctls, read-only rootfs, and non-root user.

Resolves the field-name ambiguity the design doc left open (ipcns, not
ipc) and surfaces several real-world quirks no draft anticipated: the
mounts/volumes field-casing split, singular "sysctl" naming, unsplit
device path strings, and systemd defaulting to "true" even when --systemd
is never passed on the CLI. README.md documents exact client/server
versions and per-fixture capture provenance.

* ✨ feat(filter): add libpod container-create body inspector (#148)

libpod_container_create_types.go / libpod_container_create.go implement
POST /libpod/containers/create body inspection, structured like
container_create_types.go/container_create.go: subset decode of the
SpecGenerator body, a policy struct, and an inspect() method gated by
isLibpodContainerCreatePath (path-exclusive — never reads a Docker-shaped
body, and the Docker inspector never reads a libpod-shaped one).

Gates mirror ContainerCreateOptions where the semantics map: privileged,
host netns/pidns/ipcns/userns, namespace-sharing restriction reading
{nsmode,value}, allowed_bind_mounts via the shared normalizeBindMount,
devices, allowed_capabilities, seccomp/apparmor/selinux allowlists
(including selinux disable), require_non_root_user, memory/cpu/cpu-hard/
pids limits from resource_limits, sysctls, read_only_filesystem, and
image trust reusing the existing ImageTrust wiring on the image field.
Two libpod-only gates have no Docker analog: allow_systemd_mode (denies
non-"false" systemd by default, since SpecGenerator's own default is
"true") and allow_custom_id_mappings. Field names and gate semantics are
pinned from the golden fixtures captured in the prior commit, not from
either draft of the design doc.

* ✨ feat(config): wire libpod_container_create request-body config (#148)

Adds LibpodContainerCreateRequestBodyConfig under request_body under a
new libpod_container_create mapstructure key, following
ContainerCreateRequestBodyConfig's field/tag pattern. ToFilterOptions
plumbs it into filter.LibpodContainerCreateOptions, and
validateLibpodContainerCreateConfig mirrors validateContainerCreateConfig
(image_trust validation, allowlist sanity checks). All defaults fail
closed; ImageTrust.RequireRekorInclusion defaults to true, matching
container_create's default.

* ✨ feat(filter): wire libpod container-create inspector (#148)

middleware.go registers the libpod inspector in compileRuntimePolicy's
policy list, keyed by isLibpodContainerCreatePath (path-exclusive, right
after the Docker container-create entry). cmd/rules.go adds POST
/libpod/containers/create to bodySensitiveWriteEndpoints and a
bodyInspectionConfiguredForEndpoint arm, so the startup blind-write
validator recognizes the new inspector the same way it does every other
body-sensitive endpoint.

Kept minimal and additive — a sibling PR is touching the same three files
(pod-create/exec/volume inspectors) and will need a rebase against this.

* 🧪 test(filter): add libpod container-create test suite (#148)

Per-gate table tests driven off the golden fixtures for every one of the
~20 gates in libpod_container_create.go, plus:

- TestInspectorRoutingIsPathExclusive: the design doc's central test — a
  single body carrying both a Docker-shaped dangerous value
  (HostConfig.Privileged) and a libpod-shaped safe value (top-level
  "privileged") with opposing truth values (and the mirror body), proving
  neither inspector ever reads the other's shape, and that each is a
  structural no-op on the other family's path.
- Fail-closed decode tests: malformed JSON, oversized body (1 MiB cap via
  requestRejectionError), empty body allowed, and nil-request/wrong-method/
  wrong-path/nil-body no-ops.
- Image-trust tests reusing the existing mockImageVerifier/
  mockSignatureFetcher fixtures from container_create_test.go, including
  the verify→pin digest rewrite and duplicate-case-variant-key rejection.
- FuzzLibpodContainerCreate, seeded from all 20 fixtures plus adversarial
  inputs; 20s local smoke run (~1.3M execs) found no crashes.

Every captured fixture defaults "systemd":"true" (SpecGenerator's own
default even when --systemd is never passed), which the new
allow_systemd_mode gate denies by default — so every "expect allow"
assertion in this file explicitly opts a policy into AllowSystemdMode
where the fixture would otherwise trip that gate first and mask the gate
actually under test. Assertions expecting deny from a gate that runs
before the systemd check in inspect()'s order don't need it.

* 📝 docs(configuration): document libpod_container_create (#148)

Adds request_body.libpod_container_create to the YAML sample, a prose
paragraph describing the gates (mirroring the container_create section's
structure), a Request Body Policy Reference table row, and a dedicated
environment-variable table following the container_create precedent.
CHANGELOG gains an Unreleased ### Added entry summarizing the new
inspector, its gate set, and the fixture-derived field-name resolution.
…spection (#148) (#190)

* ✨ feat(filter): add libpod pod-create, volume, network, secret inspectors

Adds request-body inspection for Podman's libpod-native write endpoints
that had no coverage: POST /libpod/pods/create, POST /libpod/volumes/create,
POST /libpod/networks/create, and POST /libpod/secrets/create.

- libpod_pod_create.go: new inspector gated by allow_host_network (pod-level
  netns {nsmode:"host"}), allow_shared_pid_namespace (shared_namespaces
  containing "pid"), and allowed_infra_image_registries (reuses the
  image_pull registry-allowlist shape). Cross-owner pod-membership checks are
  explicitly deferred to a later PR (see the TODO in libpod_pod_create.go).
- libpod_volume.go / libpod_network.go / libpod_secret.go: libpod-shaped
  decode structs that reuse the EXISTING volume/network/secret config types
  rather than introducing parallel config — libpod's wire shapes differ from
  Docker-compat (volume options live under "Options" not "DriverOpts",
  network options are snake_case with no swarm-mode fields, secret driver is
  a query param not a body field) but the policy question is identical.
- config.go/filter_options.go/validate.go: new
  request_body.libpod_pod_create config surface plus
  request_body.libpod_volume/libpod_network/libpod_secret keys mapped onto
  the pre-existing config types.
- middleware.go: wires the four new inspectors into compileRuntimePolicy,
  and extends matchesExecInspection so the existing shared execPolicy also
  matches the libpod exec create/start paths (no separate libpod_exec
  config — see the follow-up exec.go commit).

All wiring is additive to shared files (config.go, middleware.go, etc.) to
minimize rebase conflict surface with the concurrent libpod container-create
inspector PR.

Refs #148

* ✨ feat(filter): extend exec policy to cover libpod exec create/start

POST /libpod/containers/{name}/exec and POST /libpod/exec/{id}/start route
to the identical Go handlers the Docker-compat exec paths do (confirmed
against Podman v5.8.1's own route table), so per the #148 design doc
(decision C3) both families now share the SAME execPolicy/ExecOptions and
request_body.exec config — there is no separate libpod_exec block.

- exec.go: execStartIdentifier now also reports which path family (Docker
  vs libpod) matched a given exec-start path; inspectExisting dispatches to
  the new InspectStartLibpod field for libpod paths instead of InspectStart,
  since the two point at different upstream endpoints
  (GET /exec/{id}/json vs GET /libpod/exec/{id}/json) and must stay
  independently configured so setting one never silently satisfies the
  other's fail-closed "no exec inspection configured" guard.
- NewLibpodExecInspector / NewLibpodExecInspectorWithRoundTripper add the
  libpod-path variant of the exec-inspect upstream constructor; both it and
  the existing Docker constructor now delegate to a shared
  newHTTPExecInspector helper.
- serve.go: attachRuntimeInspectors wires
  policy.Exec.InspectStartLibpod alongside the existing InspectStart.

Refs #148

* ✨ feat(cmd): wire libpod endpoints into startup gate tables

Extends the three startup validators driven by bodySensitiveWriteEndpoints /
sensitiveExfilEndpoints / bodyInspectionConfiguredForEndpoint to cover the
new libpod surface:

- bodySensitiveWriteEndpoints gains libpod pod-create, exec create/start,
  and volume/network/secret create — all of which have real inspectors —
  plus (design doc decision C2) play/kube, its kube/play alias, kube/apply,
  and manifest writes, none of which get any request-body inspector in this
  release (full Kubernetes-YAML/PodSpec modeling is out of scope), so
  admitting them requires insecure_allow_body_blind_writes exactly like any
  other uninspected body-sensitive write.
- sensitiveExfilEndpoints gains libpod's container archive/export/logs/
  attach and image export/get/push read surface, confirmed against Podman
  v5.8.1's route table rather than assumed from naming symmetry with the
  Docker-compat entries (libpod has no /attach/ws variant and no plugin API
  at all). GET /libpod/generate/kube is included here rather than in
  bodySensitiveWriteEndpoints, deliberately deviating from the design doc's
  literal placement: Podman registers it as a GET (libpod.GenerateKube) that
  dumps existing pod/container definitions to YAML, a read/export surface
  that can leak env vars, not a write.
- bodyInspectionConfiguredForEndpoint arms unconditionally true for the four
  libpod endpoints with real fail-closed inspectors, and shares the existing
  exec case (keyed on request_body.exec.allowed_commands) across both the
  Docker-compat and libpod exec paths.

Refs #148

* 🧪 test(filter): libpod inspector coverage and exec parity tests

- libpod_pod_create_test.go / libpod_volume_test.go / libpod_network_test.go
  / libpod_secret_test.go: gate tables for each new inspector (default-deny
  behavior, explicit allow, malformed/oversized body handling, path/method
  non-match), plus negative cross-shape tests proving POST /containers/create
  and POST /libpod/pods/create bodies never leak into each other's decode —
  e.g. libpod's netns.nsmode:"host" has zero effect on the Docker-compat
  container-create decision, and Docker's HostConfig.NetworkMode:"host" has
  zero effect on the libpod pod-create decision.
- exec_test.go: exec-parity tests proving identical request bodies produce
  identical allow/deny verdicts on the Docker-compat and libpod exec paths
  through the single shared execPolicy, dispatch tests proving
  inspectExisting calls the InspectStart/InspectStartLibpod function
  matching the request's own path family (and never the other), and
  constructor/round-tripper tests for NewLibpodExecInspector[WithRoundTripper]
  including a mock upstream that only accepts GET /libpod/exec/{id}/json to
  prove the libpod path is actually used.

Refs #148

* 🧪 test(cmd): libpod gate-table validator tests

Covers the additions to internal/cmd/rules.go from #148:

- Allow-with-config success cases for libpod pod-create, exec (shared
  request_body.exec allowlist), and volume/network/secret create.
- Blind-write validator rejection for /libpod/containers/*/exec without a
  configured allowlist, and for /libpod/play/kube (+ its kube/play alias),
  /libpod/kube/apply, and /libpod/manifests/* without
  insecure_allow_body_blind_writes — plus the mirrored allow-with-explicit-
  opt-in case for play/kube.
- Read-exfiltration validator rejection (and explicit-opt-in allow) for the
  new libpod container/image read surface and GET /libpod/generate/kube.
- Direct bodyInspectionConfiguredForEndpoint coverage for every new libpod
  case, including the shared exec config across both libpod exec create and
  exec start.

Refs #148

* 📝 docs(configuration): document libpod pod-create/exec/volume/network/secret inspection

- configuration.mdx: new prose bullets and Request Body Policy Reference /
  environment-variable rows for request_body.libpod_pod_create,
  request_body.libpod_volume, request_body.libpod_network, and
  request_body.libpod_secret; the exec section and reference table now state
  explicitly that request_body.exec governs both the Docker-compat and
  libpod exec paths (no separate libpod_exec config); the
  insecure_allow_body_blind_writes bullet documents the uninspected
  play/kube/kube-apply/manifest surface and its N-privileged-containers
  blast radius; insecure_allow_read_exfiltration documents the new libpod
  read/export surface including GET /libpod/generate/kube.
- CHANGELOG.md: Unreleased Added/Docs/Tests entries for #148 PR3+4.

Refs #148

* 🔒 security(cmd): close libpod manifest registry-push exfiltration gap

POST /libpod/manifests/*/registry/* and the backward-compat POST
/libpod/manifests/*/push read local manifest content and hand it to a
caller-chosen registry — an exfiltration surface like the existing
image/plugin push entries, but they weren't in sensitiveExfilEndpoints,
so rules admitting them bypassed insecure_allow_read_exfiltration.
Add both sentinels and narrow-rule rejection cases. #148

* 🧪 test(cmd): consolidate libpod gate tests into one table

Fold the eight standalone allow/reject/opt-in validator tests for
libpod pod-create/exec/volume/network/secret gates into a single
table-driven TestValidateAndCompileRulesLibpodGates, keeping each
case's endpoint assertions and #148 design-decision comments. #148

* 📝 docs(configuration): document every enforced libpod_volume/network/secret field

The env-var table listed one variable per libpod_volume/libpod_network/
libpod_secret group, and two of those three (allow_endpoint_config,
allow_template_drivers) aren't actually consulted by the libpod
inspectors. Replace with a row per field each inspector really
enforces (allow_custom_drivers/allow_driver_opts for volume;
allow_custom_drivers/allow_driver_options/allow_custom_ipam_config/
allow_ipam_options for network; allow_custom_drivers for secret) and
call out the inherited fields that have no libpod analog instead of
implying they're enforced. #148

* 🐛 fix(filter): reuse libpodNamespace decl shared with container-create inspector after rebase
…d surface (#148) (#192)

* 🧪 test(integration): add podmanintegration-tagged libpod suite (#148)

PR6 of the #148 Podman/libpod train: real-daemon coverage for the
libpod inspectors built in PR2-PR5, mirroring the existing
integration-tagged Docker suite's structure and skip-cleanly
convention.

- app/integration/podman_libpod_integration_test.go and
  podman_helpers_test.go (new, //go:build podmanintegration): plain
  libpod container-create passes; privileged container-create and
  host-network / disallowed-infra-image-registry pod-create are
  denied with "libpod "-prefixed reasons; exec create/start honor
  the shared request_body.exec config via a real create+start+exec
  round trip; libpod_volume/network/secret driver gates fire.
- app/internal/cmd/rules_podman_integration_test.go (new, same tag):
  proves the POST /libpod/play/kube blind-write acknowledgment gate
  against a live daemon, not just rules_test.go's synthetic configs.
- app/integration/helpers_test.go: broaden the build tag to
  `integration || podmanintegration` so the podman suite reuses the
  daemon-agnostic handler/rule-compilation helpers instead of
  duplicating them.

All tests t.Skip cleanly when SOCKGUARD_TEST_PODMAN_SOCKET is unset,
so `go test ./...` and the existing dockerd suite are unaffected;
the 96% coverage gate excludes nothing new (verified locally at
96.2% after filtering differential/testcert/testhelp).

* 🔧 chore(ci): run podmanintegration suite against rootful+rootless podman (#148)

New workflow .github/workflows/quality-integration-podman.yml runs
the podmanintegration-tagged tests added in the prior commit on
ubuntu-latest, mirroring quality-integration.yml's real-dockerd
structure (harden-runner, pinned action SHAs, pre-pulled pinned
sentinel image).

Two jobs against the distro-packaged podman:
- rootful: required leg, `sudo podman system service` on the
  well-known root socket, chmod'd so the unprivileged test process
  can dial it directly.
- rootless: continue-on-error. ubuntu-latest has usable
  newuidmap/newgidmap and /etc/subuid /etc/subgid ranges for the
  runner user (verified with an explicit prerequisite-check step),
  but the runner has no logind session, so XDG_RUNTIME_DIR has to be
  created and chowned by hand rather than relying on pam_systemd.
  That's more failure-prone than the rootful path most sockguard
  deployments actually run behind, so a transient rootless-only
  failure never blocks a PR.

* 🐛 fix(ci): fix rootful socket perms and rootless subuid check (#148)

Both legs of the new podman integration workflow failed on the real
runner (run 31021161589):

- Rootful (92357909184): every test hit "stat
  /run/podman/podman.sock: permission denied". The socket chmod
  alone wasn't enough — /run/podman itself is root-owned 0700, so
  the unprivileged test process can't even traverse into the
  directory to reach the (now world-writable) socket file. chmod the
  directory too, and add a curl --unix-socket _ping check as the
  runner user right after starting the service, so a future
  regression here fails loudly at setup instead of inside the suite.
- Rootless (92357909108): "Verify rootless prerequisites" died on
  `getent subuid runner` with "Unknown database: subuid" — glibc on
  ubuntu-latest has no subuid/subgid NSS database registered at all,
  so getent can't answer the question regardless of what
  /etc/subuid contains. Check the files directly with grep instead.

* 🐛 fix(ci): version-prefix real podman requests, fix exec/cleanup bugs (#148)

Run 31021969781's rootful leg fixed the earlier permission-denied issue but
surfaced new failures once tests actually hit real Podman 4.9.3:

- Bare (unversioned) libpod routes other than GET /libpod/_ping 404 on real
  Podman, unlike dockerd. Container-create and exec-create now fetch the
  daemon's Libpod-Api-Version via _ping and prefix every request that needs
  to reach Podman with /v<version>/libpod/...; TestProxyReachesRealPodmanVersion
  (which hit bare /libpod/version) is removed as no longer viable.
- Deny-reason assertions used raw strings.Contains against JSON bodies whose
  %q-quoted values are escaped (\") — switched to decoding filter.DenialResponse
  and comparing .Reason.
- The exec test's denied/allowed Cmd payloads had no explicit User, so
  isRootUser("") tripped the root-exec-user gate before the command-allowlist
  gate ever ran; added "User":"nobody" to isolate the allowlist check.
- removeLibpodContainer force-removes a RUNNING container, which makes Podman
  stop it first (SIGTERM + grace period) — that alone exceeds the shared
  5s HTTP client timeout. Gave it its own 30s client instead of widening the
  shared one.
- POST /libpod/exec/{id}/start is hijack-capable, and internal/proxy/hijack.go
  always forwards the version-stripped path upstream by design (pinned by
  existing hijack tests) — fine for dockerd, currently incompatible with real
  Podman's non-_ping versioning requirement. Dropped the real exec-start round
  trip with a comment explaining why; exec's shared-config enforcement is
  already fully proven by the exec-create deny/allow pair.

All changes verified locally against a real `podman machine` socket (macOS,
Podman Engine 5.8.1), not just compiled.

* 🐛 fix(ci): create podman runtime subdir and export XDG_RUNTIME_DIR in-step for rootless leg
* ✨ feat(dockerresource): add KindLibpodPod/KindLibpodNetwork for #148 ownership+visibility wiring

Pods have no Docker-compat equivalent, and libpod network inspect
uses lowercase "labels" (plus an occasional single-element array
wrap) unlike every other Docker-compat-shaped inspect body — both
need their own Kind so ownership/visibility dispatch can route on
Kind alone without threading libpod-ness through every call site.

- LibpodInspectPath: dedicated /libpod/{kind}/{id}/json builder for
  the two new kinds, wired into InspectPath's existing switch
- DecodeLibpodLabels/decodeLibpodNetworkLabels: lowercase-keyed pod
  labels, plus array-unwrap-then-decode for network inspect

* ✨ feat(ownership): stamp owner labels and check pod cross-owner refs on libpod endpoints

Extends mutateOwnershipRequest with libpod cases so POST
/libpod/containers/create, /pods/create, /volumes/create,
/networks/create, and /secrets/create get the owner label injected
when ownership.owner is configured — matching each endpoint's actual
wire shape rather than assuming a single convention:

- container/pod/network create: lowercase top-level "labels"
- volume create: capitalized "Labels" (VolumeCreateOptions has no
  json tag on that field, so it falls back to the Go field name)
- secret create: no JSON body at all — driver/labels are query
  params, reuses the existing addOwnerLabelToBuildQuery helper

Read-side owner filtering (list injection + per-resource inspect/
action dispatch) is extended to every /libpod/ list and inspect path
for containers, pods, networks, volumes, and secrets — not just pods
— so switching a client from Docker-compat to libpod-native paths
can't bypass ownership isolation for reads on existing resources.

Closes the cross-owner pod-membership deferral called out in #190:
POST /libpod/pods/create requests joining another owner's namespaces
are denied, and POST /libpod/containers/create requests targeting
another owner's pod (SpecGenerator "pod" field) are denied, reusing
the existing container:<ref> namespace-sharing and embedded-resource
reference checks against libpod's uniform
{"nsmode":"container","value":"<ref>"} namespace shape.

Deny reasons for libpod-family requests get the "libpod " prefix,
matching the convention libpod's body inspectors already use.

* 🧪 test(ownership): cover libpod owner stamping, pod cross-owner denial, and read dispatch

Table-driven tests mirroring the existing ownership test patterns:
label stamping on every libpod create endpoint (including the
capitalized-Labels volume-create deviation and the secret-create
query-param path), cross-owner pod-membership denial in both
directions (pod-create namespace joins, container-create pod
targeting) plus the same-owner allow counterparts, libpod read-path
owner-filter dispatch across container/pod/network/volume/secret,
exec-identifier resolution, and a negative test proving libpod
predicates never fire on Docker-compat paths.

* ✨ feat(visibility): filter libpod list/inspect paths, including pods and network array-unwrap

Wires every /libpod/ list endpoint into the existing visibility
label-filter injection (needsVisibilityLabelFilter), and every
inspect/action path (containers, pods, networks, volumes, secrets)
into resourceVisible dispatch — pods route through the new
KindLibpodPod since they have no Docker-compat equivalent, the rest
reuse the existing Docker-compat Kind constants since ownership
labels live in the same shape once decoded.

Every libpod inspect endpoint is uniformly suffixed "/json" (unlike
Docker-compat's mixed bare-path/json-suffixed convention), so the
new identifier helpers use suffixedIdentifier rather than
singleSegmentIdentifier.

Network inspect responses are unwrapped from Podman's occasional
single-element-array envelope before label extraction (upstream
Inspector round-trip, dockerresource.DecodeLibpodLabels).

Hidden-resource deny reasons get the "libpod " prefix for requests
under /libpod/, matching the body-inspector convention.

* 🧪 test(visibility): cover libpod list/inspect filtering, pod visibility, and network array-unwrap

Table-driven tests: visibility label-filter injection on every
libpod list endpoint, KindLibpodPod list/inspect visibility
(hidden-pod 404, visible-pod forward, rollout-mode would-deny),
the "libpod " deny-reason prefix across libpod pod/container and
Docker-compat paths, an end-to-end upstreamInspector round-trip for
both bare-object and array-wrapped network inspect responses, and a
negative test proving libpod predicates never fire on Docker-compat
paths.

* 📝 docs(148): document libpod owner-label isolation and cross-owner pod checks

CHANGELOG: new Unreleased/Added entry after the existing #148
entries, and updates the pod-create entry's "deferred to a later PR"
note now that cross-owner pod-membership checks have landed.

configuration.mdx: new paragraph under Owner Label Isolation
covering the libpod-specific label casing/query-param deviations,
KindLibpodPod, and the two-directional cross-owner pod check.

* 🧪 test(visibility): fuzz libpod path identifiers, table-drive pod visibility tests

- Add FuzzLibpodPathIdentifiers covering libpod_paths.go's identifier
  helpers: no match outside /libpod/, collection routes never classify
  as a single-resource identifier, matched identifiers are non-empty
  and (bar the readSubresourceIdentifier-based container helper) never
  contain '/'. Wired into the CI fuzz matrix.
- Convert TestMiddlewareLibpodPodListAndInspectVisibility and
  TestMiddlewareLibpodDoesNotAffectDockerCompatVisibilityPaths to
  table-driven form per repo convention; assertions unchanged.

Addresses CodeRabbit comments 3722497023 and 3722497031 on PR #193.

* 📝 docs(configuration): close the libpod_pod_create cross-owner deferral note

The libpod_pod_create prose bullet and reference table row still said
cross-owner pod-membership checks were deferred to a later release.
This PR implements them (see the Owner Label Isolation section) —
update both to describe the now-enforced behavior instead.

Addresses CodeRabbit comment 3722497036 on PR #193.
#148) (#195)

* 📝 docs(podman): add Podman guide covering both API surfaces (#148)

New docs/content/docs/podman.mdx explains sockguard as a Podman socket
proxy: the Docker-compat vs. native /libpod surfaces and their separate
inspectors, which request_body.* key governs which libpod endpoint, the
uninspected play/kube blind-write surface and its blast-radius warning,
libpod read-exfiltration additions, ownership/visibility coverage,
three-part semver version-prefix handling, rootful vs. rootless socket
paths, and current limitations (#194's hijacked libpod exec-start gap
plus the design doc's deferred-past-v1.6 list). Registered in the docs
nav via meta.json — the issue's acceptance criterion for #148.

* ✨ feat(presets): add podman-readonly read-only preset (#148)

New app/configs/podman-readonly.yaml covers a read-only monitoring
posture on both of Podman's API surfaces in one file: Docker-compat
list/inspect/stats/top/changes for containers plus list/inspect for
images/networks/volumes, and their libpod-native equivalents including
pod list/inspect (no Docker-compat analog). Every exfiltration-gated
endpoint on both surfaces (archive/export/logs/attach/get/push,
generate/kube) is deliberately excluded, so unlike readonly.yaml this
preset validates clean without insecure_allow_read_exfiltration; it
allows no writes at all, so insecure_allow_body_blind_writes is never
needed either. Verified via `sockguard validate --config
configs/podman-readonly.yaml`. Documented in presets.mdx.

* 🧪 test(cmd): add podman-readonly conformance test (#148)

New app/internal/filter/podman_readonly_preset_conformance_test.go
fires representative requests at podman-readonly.yaml's real filter
chain (reusing the existing presetCase/buildDrydockPresetHandler/
fireDrydockCase helpers), pinning three things: both API surfaces
admit the same shape of narrow reads, every path in
sensitiveExfilEndpoints (both surfaces) stays denied, and no write
reaches upstream on either surface — including libpod-only writes
with no Docker-compat analog like pod create and play/kube. The
existing preset-walking tests (TestPresetConfigsValidate,
TestPresetConfigsPassBuildChain, TestPresetConfigsDenyAttestation
StatementsByDefault) already auto-discover the new preset file.

* 🔧 chore(ci): retry libpod connectivity checks in the podman workflow

Both legs of quality-integration-podman.yml checked for the socket
FILE appearing, then immediately ran a single connectivity check
(curl .../libpod/_ping on the rootful leg, podman ... version on the
rootless leg). The socket file existing doesn't mean the listener is
accepting connections yet — that check has hit connection-refused
once in the narrow window between the two. Wrap both checks in a
10-attempt, 1s-apart retry loop that dumps the service log and exits
non-zero only if every attempt fails.

* 🐛 fix(ci): re-chmod podman socket each probe attempt and bound probes with timeouts

- podman system service replaces the socket file after initial bind on
  some startups, silently undoing the one-shot chmod; re-apply it every
  retry and dump socket state on failure
- 🔧 add per-command timeouts to all readiness probes so a blocked curl
  or podman version can't eat the job timeout

* 🧪 test(filter): cover version-prefixed paths in podman-readonly conformance and fuzz seeds

* 📝 docs(podman): fix generate/kube gate classification and preset read-scope wording
* 🧪 test(conformance): tri-tool conformance matrix workflow + harness

Publishes #150's Sockguard + Portwing + drydock conformance matrix. New
weekly/workflow_dispatch quality-tri-tool-conformance.yml boots the
audited examples/compose/tri-tool bundle from published images only
(never source-built) across three rows -- current-standard, current-edge,
and legacy-floor (the audited 0.8.1/1.5.2 pins from PR #155) -- and runs
scripts/tri-tool-conformance/run-matrix.sh's ten ordered assertions per
row: pristine fresh-volume boot, auth handshake plus one negative probe
per mode, inventory/inspect, events, logs, lifecycle, configured exec
(Edge only), the remote-update trigger, expected denials, and a
route-drift tripwire that diffs sockguard's own access log against a
checked-in known-routes.json manifest.

- 🧪 scripts/tri-tool-conformance/{run-matrix.sh,lib.sh}: the driver +
  shared helpers; --self-test exercises the route normalizer and tripwire
  diff logic against a fixture with no Docker required
- 🧪 scripts/tri-tool-conformance/normalize-routes.jq +
  known-routes.json: route-shape normalizer and the manifest seeded from
  app/configs/portwing.yaml / portwing-with-exec.yaml's allow rules
- 🧪 scripts/tri-tool-conformance-run-matrix.test.mjs: wires --self-test
  into `npm test`
- 🔧 examples/compose/tri-tool/docker-compose.conformance-overlay.yml:
  test-only probe container overlay; the audited bundle itself is untouched
- 🔧 examples/compose/tri-tool/{docker-compose.yml,docker-compose.edge-exec.yml,README.md}:
  rename SOCKGUARD_VERSION (tag-only) to SOCKGUARD_IMAGE (full ref) so the
  harness and the new pre-GA release gate can target a release-candidate
  image on any of sockguard's three registries
- 📝 CHANGELOG.md: Tests + Docs entries under [Unreleased]

* 📝 docs(releasing): pre-GA tri-tool conformance gate

Adds RELEASING.md step 4: before promoting a minor/major release
candidate to GA, run the new quality-tri-tool-conformance.yml workflow
against the candidate image (sockguard_image dispatch input) and require
all three matrix rows green.

* 🔒 security(ci): fix tri-tool conformance gate image default + summary short-circuit

- sockguard_image dispatch input now defaults to empty (falls through to
  the compose bundle's own pinned default, same as a scheduled run)
  instead of ghcr.io/codeswhat/sockguard:latest, which pointed at a
  different registry/tag than the compose default a scheduled run uses.
  Input description and RELEASING.md's pre-GA gate step now say to pass
  the candidate ref explicitly.
- summary job now asserts artifact count == matrix row count BEFORE
  aggregating, not just checking for total emptiness after -- a row that
  dies before write_artifact runs no longer passes the gate silently.

* 🔒 security(ci): pin probe container UID to match sockguard's socket owner

curlimages/curl's own image default UID (100) can't reach a socket owned
65532:65532 mode 0600, so the probe's curl calls were failing on their own
filesystem permission mismatch instead of exercising the socket mode the
assertion actually cares about.

* 🔒 security(script): allowlist sockguard_image before it's ever exported

--sockguard-image gets interpolated into a compose file and pulled/run.
The workflow that drives this script is workflow_dispatch/schedule only
(no pull_request trigger, so the input is maintainer-controlled, not
attacker-controlled via a PR), but validating it against the allowlisted
sockguard registries/repos is defense in depth worth having regardless.

* 🐛 fix(conformance): tolerate malformed access-log lines, fail closed on zero routes

- wait_for_access_log_route's jq filter (lib.sh) now skips non-object JSON
  and missing/null normalized_path instead of erroring on them. Under
  `set -o pipefail`, a jq error on any line -- even one arriving after a
  real match was already printed -- flipped the whole pipeline's exit
  status non-zero and reported the wait as failed regardless of what grep
  actually found, poisoning assertions 3 (inventory-inspect) and 4
  (events).
- normalize-routes.jq gets the same select(type=="object") guard: a
  bare-string JSON line would otherwise abort the whole `[inputs | ...]`
  collection rather than just being skipped, poisoning assertion 10.
- assert_route_drift now fails closed (route_drift_status, moved to
  lib.sh so --self-test can exercise it directly) when zero access-log
  records were captured, instead of recording a vacuous empty-diff PASS --
  that almost always means log capture or the normalizer broke, not that
  nothing happened.
- Extended testdata/access-log-fixture.jsonl with a missing-normalized_path
  line and a bare-string JSON line, and added self-test coverage for both
  the jq tolerance and the fail-closed behavior.
- Fixed lib.sh's stale compose_cmd doc comment (the function is `compose`
  and runs directly, it doesn't echo an invocation for callers to expand).

* 🐛 fix(conformance): pre-pull busybox pins, checked chown, exact digests, aligned skip names

- Pre-pull OLD_BUSYBOX_REF and NEW_BUSYBOX_REF explicitly before any
  assertion needs them, checked, instead of letting the first
  containers/create implicitly trigger the pull -- a slow/flaky pull on a
  shared runner otherwise reads as an unrelated timeout in whichever
  assertion needed the image first. Corrected the header comment: only
  NEW_BUSYBOX_REF matches app/integration/helpers_test.go's
  busyboxPinnedRef, and that repo's own pre-pull is for its own CI job,
  not this one.
- portwing_token.txt (standard mode) is now chown'd to 65532:65532 before
  the chmod 0400 -- it stayed owned by the script runner while portwing
  reads it as UID 65532. Every chown/chmod in the secrets-setup block
  (standard and edge) is now checked explicitly instead of `&&`-chained
  silently, failing the row with a clear setup-error message instead of
  surfacing later as a confusing auth-handshake failure.
- Image digests are now captured via `docker image inspect` right after
  `compose up` in assert_pristine_boot, from the images the row actually
  ran. resolve_metadata no longer does a `docker compose pull` at the end
  of the row, which could catch an upstream tag having moved mid-run and
  record a digest that never ran any of this row's assertions.
- assert_logs no longer pre-emptively records a "lifecycle" FAIL when the
  primary sentinel create fails -- assert_lifecycle already records its
  own FAIL for that case, so the artifact was getting a duplicate entry.
- The abort-path skip loop (pristine-boot/auth-handshake failure) now
  emits exactly the same assertion names the success path does
  (exec-policy-allowed/-denied instead of exec-policy on exec rows,
  expected-denials-build/-exec/-export instead of expected-denials), and
  adds a skipped "auth-handshake" entry for the case where pristine boot
  itself is what failed and auth-handshake was never even attempted.
* 🔧 chore(release): prepare v1.6.0-rc.1 changelog entry

- 📝 docs(changelog): promote [Unreleased] to [1.6.0-rc.1] - 2026-08-05 with a release-candidate intro covering the full v1.6 roadmap (#149 #151 #152 #153 #148 #150)
- 🎨 style(changelog): fix missing blank line before ### Tests and a doubled blank line before [1.5.2] introduced by merge resolutions

* 🧪 test(logging): de-flake request-ID entropy batching test

The constructor queues an async refill signal, so the background goroutine's refillSync could race the test's manual refillSync — both passing the low-watermark guard before either enqueues, recording 2 entropy fills where the test expects 1 (seen on PR #199 CI). Stop the goroutine before filling so exactly one fill stocks the pool in every interleaving. Verified with -race -count=200.
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
sockguard-website Ready Ready Preview Aug 5, 2026 10:13pm

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 186 files, which is 86 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8920241f-fa8c-46ad-ab3c-5ef8c431fa03

📥 Commits

Reviewing files that changed from the base of the PR and between a3a9cc4 and 60ab1b1.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !CHANGELOG.md
📒 Files selected for processing (186)
  • .github/workflows/ci-verify.yml
  • .github/workflows/quality-api-version-watch.yml
  • .github/workflows/quality-integration-podman.yml
  • .github/workflows/quality-tri-tool-conformance.yml
  • .gitignore
  • README.md
  • RELEASING.md
  • app/configs/cis-docker-benchmark.yaml
  • app/configs/drydock-with-build.yaml
  • app/configs/drydock-with-compose.yaml
  • app/configs/multi-listener.yaml
  • app/configs/podman-readonly.yaml
  • app/configs/portainer.yaml
  • app/configs/portwing-with-build.yaml
  • app/configs/portwing-with-compose.yaml
  • app/configs/sockguard.yaml
  • app/differential/classifier.go
  • app/differential/path_evasion_extended_test.go
  • app/go.mod
  • app/integration/helpers_test.go
  • app/integration/multi_listener_integration_test.go
  • app/integration/podman_helpers_test.go
  • app/integration/podman_libpod_integration_test.go
  • app/integration/resource_limit_integration_test.go
  • app/internal/banner/banner.go
  • app/internal/banner/banner_test.go
  • app/internal/cmd/mutant_kill_test.go
  • app/internal/cmd/rules.go
  • app/internal/cmd/rules_podman_integration_test.go
  • app/internal/cmd/rules_test.go
  • app/internal/cmd/serve.go
  • app/internal/cmd/serve_admin_listener_test.go
  • app/internal/cmd/serve_admission.go
  • app/internal/cmd/serve_deps.go
  • app/internal/cmd/serve_deps_gaps_test.go
  • app/internal/cmd/serve_h2c_test.go
  • app/internal/cmd/serve_listener_admission_test.go
  • app/internal/cmd/serve_listener_warning_test.go
  • app/internal/cmd/serve_listeners.go
  • app/internal/cmd/serve_listeners_feature_test.go
  • app/internal/cmd/serve_listeners_gaps_test.go
  • app/internal/cmd/serve_mutation_test.go
  • app/internal/cmd/serve_reload_test.go
  • app/internal/cmd/serve_resource_limit_test.go
  • app/internal/cmd/serve_run_test.go
  • app/internal/cmd/serve_test.go
  • app/internal/config/compat.go
  • app/internal/config/compat_test.go
  • app/internal/config/config.go
  • app/internal/config/filter_options.go
  • app/internal/config/listeners_fuzz_test.go
  • app/internal/config/listeners_test.go
  • app/internal/config/load.go
  • app/internal/config/load_defaults_completeness_test.go
  • app/internal/config/mutation_test.go
  • app/internal/config/resource_limit_config_test.go
  • app/internal/config/validate.go
  • app/internal/dockerresource/kind.go
  • app/internal/dockerresource/kind_test.go
  • app/internal/dockerresource/labels.go
  • app/internal/dockerresource/labels_test.go
  • app/internal/filter/bench_test.go
  • app/internal/filter/body_read.go
  • app/internal/filter/build.go
  • app/internal/filter/build_test.go
  • app/internal/filter/container_create.go
  • app/internal/filter/container_create_test.go
  • app/internal/filter/container_create_types.go
  • app/internal/filter/container_update.go
  • app/internal/filter/container_update_test.go
  • app/internal/filter/exec.go
  • app/internal/filter/exec_test.go
  • app/internal/filter/fuzz_test.go
  • app/internal/filter/hijack_paths.go
  • app/internal/filter/image_pull.go
  • app/internal/filter/image_pull_test.go
  • app/internal/filter/json_mutate.go
  • app/internal/filter/json_mutate_test.go
  • app/internal/filter/libpod_container_create.go
  • app/internal/filter/libpod_container_create_test.go
  • app/internal/filter/libpod_container_create_types.go
  • app/internal/filter/libpod_default_deny_test.go
  • app/internal/filter/libpod_network.go
  • app/internal/filter/libpod_network_test.go
  • app/internal/filter/libpod_normalize.go
  • app/internal/filter/libpod_normalize_test.go
  • app/internal/filter/libpod_pod_create.go
  • app/internal/filter/libpod_pod_create_test.go
  • app/internal/filter/libpod_secret.go
  • app/internal/filter/libpod_secret_test.go
  • app/internal/filter/libpod_volume.go
  • app/internal/filter/libpod_volume_test.go
  • app/internal/filter/middleware.go
  • app/internal/filter/middleware_method_dispatch_test.go
  • app/internal/filter/middleware_test.go
  • app/internal/filter/mutation.go
  • app/internal/filter/mutation_test.go
  • app/internal/filter/network.go
  • app/internal/filter/network_test.go
  • app/internal/filter/podman_readonly_preset_conformance_test.go
  • app/internal/filter/registry_auth.go
  • app/internal/filter/registry_auth_test.go
  • app/internal/filter/request_rejection.go
  • app/internal/filter/resource_limit_guard.go
  • app/internal/filter/resource_limit_guard_test.go
  • app/internal/filter/rules.go
  • app/internal/filter/rules_test.go
  • app/internal/filter/service.go
  • app/internal/filter/testdata/fuzz/FuzzMutationRoundTrip/case-sensitive-labels
  • app/internal/filter/testdata/fuzz/FuzzMutationRoundTrip/exact-duplicate
  • app/internal/filter/testdata/fuzz/FuzzMutationRoundTrip/json-looking-label
  • app/internal/filter/testdata/fuzz/FuzzMutationRoundTrip/large-numbers
  • app/internal/filter/testdata/fuzz/FuzzMutationRoundTrip/nested-duplicate
  • app/internal/filter/testdata/libpod/README.md
  • app/internal/filter/testdata/libpod/basic_create.json
  • app/internal/filter/testdata/libpod/capabilities.json
  • app/internal/filter/testdata/libpod/devices.json
  • app/internal/filter/testdata/libpod/host_ipc.json
  • app/internal/filter/testdata/libpod/host_network.json
  • app/internal/filter/testdata/libpod/host_pid.json
  • app/internal/filter/testdata/libpod/host_userns.json
  • app/internal/filter/testdata/libpod/idmappings.json
  • app/internal/filter/testdata/libpod/labels.json
  • app/internal/filter/testdata/libpod/mounts_bind_tmpfs.json
  • app/internal/filter/testdata/libpod/namespace_share_container_ref.json
  • app/internal/filter/testdata/libpod/privileged.json
  • app/internal/filter/testdata/libpod/read_only_filesystem.json
  • app/internal/filter/testdata/libpod/resource_limits.json
  • app/internal/filter/testdata/libpod/resource_limits_cpu_shares.json
  • app/internal/filter/testdata/libpod/security_opts_seccomp_apparmor_selinux.json
  • app/internal/filter/testdata/libpod/sysctls.json
  • app/internal/filter/testdata/libpod/systemd_mode.json
  • app/internal/filter/testdata/libpod/user.json
  • app/internal/filter/testdata/libpod/volumes_named.json
  • app/internal/filter/version_pin_test.go
  • app/internal/health/health.go
  • app/internal/health/listeners_test.go
  • app/internal/inbound/identity.go
  • app/internal/inbound/identity_test.go
  • app/internal/logging/access.go
  • app/internal/logging/access_test.go
  • app/internal/logging/audit.go
  • app/internal/logging/listeners_test.go
  • app/internal/logging/mutation.go
  • app/internal/logging/mutation_test.go
  • app/internal/logging/resource_policy.go
  • app/internal/logging/resource_policy_test.go
  • app/internal/metrics/listeners_test.go
  • app/internal/metrics/metrics.go
  • app/internal/metrics/metrics_test.go
  • app/internal/ownership/coverage_gap_test.go
  • app/internal/ownership/libpod.go
  • app/internal/ownership/libpod_paths.go
  • app/internal/ownership/libpod_test.go
  • app/internal/ownership/middleware.go
  • app/internal/ownership/middleware_test.go
  • app/internal/proxy/hijack.go
  • app/internal/proxy/hijack_libpod_test.go
  • app/internal/reload/diff.go
  • app/internal/reload/diff_test.go
  • app/internal/reload/listeners_diff_fuzz_test.go
  • app/internal/reload/listeners_diff_test.go
  • app/internal/reload/mutation_test.go
  • app/internal/responsefilter/filter.go
  • app/internal/responsefilter/filter_test.go
  • app/internal/visibility/libpod_fuzz_test.go
  • app/internal/visibility/libpod_paths.go
  • app/internal/visibility/libpod_test.go
  • app/internal/visibility/middleware.go
  • app/testdata/docker-api/max-supported-version.txt
  • docs/content/docs/configuration.mdx
  • docs/content/docs/meta.json
  • docs/content/docs/podman.mdx
  • docs/content/docs/presets.mdx
  • docs/content/docs/security.mdx
  • examples/compose/tri-tool/README.md
  • examples/compose/tri-tool/docker-compose.conformance-overlay.yml
  • examples/compose/tri-tool/docker-compose.edge-exec.yml
  • examples/compose/tri-tool/docker-compose.yml
  • scripts/tri-tool-conformance-run-matrix.test.mjs
  • scripts/tri-tool-conformance/README.md
  • scripts/tri-tool-conformance/known-routes.json
  • scripts/tri-tool-conformance/lib.sh
  • scripts/tri-tool-conformance/normalize-routes.jq
  • scripts/tri-tool-conformance/run-matrix.sh
  • scripts/tri-tool-conformance/testdata/access-log-fixture.jsonl

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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.

@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.

Sync of reviewed dev/v1.6 content for the v1.6.0-rc.1 cut.

ALARGECOMPANY
ALARGECOMPANY previously approved these changes Aug 5, 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.

LGTM, matches dev/v1.6 tip.

…sync squash (a3a9cc4) is a prior snapshot of dev/v1.6, fully superseded by this tree
@scttbnsn
scttbnsn dismissed stale reviews from biggest-littlest and ALARGECOMPANY August 5, 2026 22:12

The merge-base changed after approval.

@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 after the ours-merge of main; tree still equals the dev/v1.6 tip.

@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.

LGTM.

@scttbnsn
scttbnsn merged commit 376e898 into main Aug 5, 2026
49 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