Conversation
) (#223) * ✨ feat(filter): per-field endpoint-config gates on network connect request_body.network.allow_endpoint_config previously admitted Docker's whole EndpointSettings object as one opt-in. New EndpointConfigOptions gates each Engine API 1.55-era field independently (static addressing, link-local IPs, MAC pinning, GwPriority, aliases) on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig, so operators can allow benign settings like Aliases without also allowing address pinning. - denyEndpointConfigReason now checks the legacy flag first (unchanged behavior when true), then each granular field in turn, naming the specific field in the denial reason - endpointHasStaticIPConfig split into endpointHasStaticAddressFields and endpointHasLinkLocalIPs so static-IP and link-local gating can vary independently, without changing endpointHasStaticIPConfig's own behavior (mutation_kill_test.go pins it) - denyRootMacAddressReason respects AllowMACPinning independent of the legacy flag - Links and DriverOpts keep no granular escape hatch — still denied without the legacy allow_endpoint_config flag - libpod_network.go doc comment notes there's no libpod-native network-connect endpoint to gate Refs: #186 * 🧪 test(filter): cover per-field endpoint-config precedence Table-driven coverage for #186: granular allows each field individually and still denies the others, static-addressing and link-local-IPs gate independently of each other, Links/DriverOpts stay denied even with every other granular field allowed, root MacAddress respects AllowMACPinning independent of the legacy flag, and the legacy allow_endpoint_config: true flag takes precedence over a granular block that would otherwise deny. * ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion Adds request_body.network.endpoint_config (and its libpod_network mirror, kept for default-posture symmetry though never consulted): allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority default false; allow_aliases defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Compose aliases. - ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions' DenyAliases so ~15 pre-existing filter-package tests that construct NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep seeing aliases allowed, while the config layer's documented default stays true - Cross-wired into ContainerCreateOptions.EndpointConfig alongside the existing AllowEndpointConfig cross-wire, matching the established "network config, not duplicated under container_create" pattern - New Config.explicitNetworkEndpointConfig field + explicitKeysSet helper (generalized from the #149 explicitLegacyListenSet pattern) track whether the endpoint_config block was actually set via YAML or a SOCKGUARD_* env var, since its merged value is never the Go zero value (AllowAliases defaults true) and so can't be detected by zero-value comparison - validateNetworkEndpointConfig rejects allow_endpoint_config: true set together with an explicit endpoint_config block: the legacy flag already admits every field the granular block could, so the two are mutually exclusive by design (fail-closed on ambiguous intent) Refs: #186 * 🧪 test(config): cover endpoint_config defaults, env vars, and validation - filter_options_test.go: extends the existing full-policy mapping test with EndpointConfig, plus a dedicated test proving the granular block cross-wires into ContainerCreateOptions - network_endpoint_config_test.go (new): Defaults() has AllowAliases true and every other granular field false; all 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are honored; setting allow_endpoint_config: true together with an explicit endpoint_config block is a validation error whether set via YAML or environment variables; the legacy flag alone and the granular block alone each validate cleanly; a pure-defaults config is never flagged as having an explicit endpoint_config block despite AllowAliases defaulting true * 📝 docs(network): document endpoint_config granular gates (#186) - configuration.mdx: endpoint_config field mapping/precedence prose, updated container_create/network policy table rows, and 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows - security.mdx: mentions the granular alternative alongside the existing allow_endpoint_config description, notes Links/DriverOpts have no per-field escape hatch - migration.mdx: new note under the v1.4->v1.5 endpoint-config item explaining the granular block is opt-in and backward compatible, and that setting both forms is a validation error - CHANGELOG.md: new Unreleased section with the full feature writeup - README.md, website/src/lib/site-content.ts: roadmap bullet updated to reflect the granular gates are implemented ahead of the v1.7.0 BuildKit-mediation epic * 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes Comment-only change to the drydock/drydock-with-compose/drydock-with-build presets' existing allow_endpoint_config macvlan/static-IP callout, pointing at the new per-field endpoint_config gates (#186) as an alternative to the all-or-nothing flag. Comments are exempt from compose_example_sync_test.go's structural (parsed policy) comparison, so this doesn't require touching the example compose files. * 🧪 test(filter): fuzz per-field endpoint-config gates Add FuzzNetworkConnectEndpointConfigGates and FuzzContainerCreateEndpointConfigGates, generating network-connect / container-create endpoint payloads plus granular EndpointConfigOptions combinations and asserting the fail-closed invariant CodeRabbit flagged on #223: with AllowEndpointConfig off, a field whose own granular gate is also off must produce a non-empty denial reason, and Links/DriverOpts (no granular gate at all) must always be denied. * 🧪 test(config): fuzz endpoint_config YAML parsing Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the #186 request_body.network.endpoint_config block (empty block, a single granular field, and the allow_endpoint_config/endpoint_config mutual- exclusion combination) and fuzzing LoadBytes then Validate on success — addresses a CodeRabbit finding on #223 that config parsing for the new block had no fuzz coverage. * 📝 docs(presets): correct endpoint_config wording in drydock comments The network-policy comment block in drydock.yaml / drydock-with-compose.yaml / drydock-with-build.yaml described endpoint-config policy as legacy-flag-only, which was already stale against #186: Aliases default to allowed but can be denied via endpoint_config.allow_aliases: false, and static IP addressing and MAC pinning each have their own granular gate now. Only Links and DriverOpts remain ungated and still require allow_endpoint_config: true. * 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts The #186 granular endpoint_config migration note had landed inside the "From Sockguard v1.4 to v1.5" section even though the feature ships in v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add the missing caveat that Links and DriverOpts have no granular gate on either network connect or container-create: workflows that need either must keep allow_endpoint_config: true and accept whole-object access. * 📝 docs(readme): correct endpoint_config narrowing description README.md and site-content.ts's v1.7.0 roadmap item both said allow_endpoint_config "has already been narrowed to per-field gates" / implied network-connect-only scope. Correct both to describe what #186 actually ships: request_body.network.endpoint_config.* provides the per-field gates, covering endpoint settings on both network connect and container-create, while allow_endpoint_config: true remains the broad whole-object override. * 📝 docs(presets): name all five granular endpoint gates in preset comments
…y) (#224) * ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation Part of issue #185's phase 1 (schema + policy foundation). Vendors a curated subset of BuildKit v0.32.0's protobuf schema under internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/ ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory kept as bare method names only), the full LLB op graph, session auth/ secrets/ssh/filesync/upload, and the gRPC health service — generated via a pinned, reproducible buf+protoc-gen-go pipeline (scripts/generate-buildkit-proto.sh) with no protoc or grpc-go dependency anywhere in the chain. - Every vendored file's go_package is retargeted into this module so generated imports resolve without requiring github.com/moby/buildkit as an actual Go dependency. - PROVENANCE.md records source URLs, upstream tags, curation notes, and sha256 checksums for every vendored file, plus what was deliberately left out (LLBBridge, Exporter, PolicyVerifier, containerd content, OTLP trace) and why. - provenance_test.go golden-tests the vendored tree against the manifest in both directions: any drift in a listed file's checksum, or any unlisted .proto file on disk, fails the build. Package is internal, protobuf-only, and never imports grpc-go — see doc.go. * ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types Part of issue #185's phase 1. internal/buildkitproxy holds the committed disposition table (mediate / passthrough / deny) for every gRPC method either opaque tunnel endpoint (POST /grpc's moby.buildkit.v1.Control, POST /session's callback services) can carry, matching the #185 synthesis inventory exactly: Control's Solve/Status are mediate, Info/ListWorkers are passthrough, everything else on Control has no enabling knob at all. Classify(endpoint, service, method) denies anything not explicitly listed — LLBBridge, nested Control/Session, containerd content, OTLP trace, Exporter, PolicyVerifier, and FileSync/TarStream are all captured in DeniedExamples and asserted deny. policy.go defines the runtime-facing Policy type (Control/Session sub-policies with per-RPC Allow flags and allowlists) that internal/config's BuildkitRequestBodyConfig.ToPolicy will translate into — buildkitproxy never imports config, so translation only flows one direction, same convention internal/filter's options types use. registry_test.go asserts every registry entry and DeniedExamples entry classifies as expected plus unknown-method deny; descriptor_manifest_test.go cross-checks both against the real vendored FileDescriptors from internal/buildkitproto via protoreflect, so the registry can't silently drift from the schema it's meant to describe. * ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's existing config/filter/startup-validation machinery, with no change in runtime behavior for any endpoint sockguard currently allows. - config.go: new nested request_body.buildkit block (control.{allow_info, allow_list_workers,allow_status,solve.allow} and session.{health,auth, secrets,ssh,file_sync,file_send,upload}), available at the top level and per client profile like every other request_body.* surface. Presence, not an enabled flag, is what matters — the Go zero value denies everything, identical to a config that never mentions "buildkit:". - buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into buildkitproxy.Policy, one direction only (buildkitproxy never imports config), mirroring RequestBodyConfig.ToFilterOptions. - validate.go: validateBuildkitConfig checks the allowlist fields (registry hosts via the existing normalizer; realms/scopes/IDs for non-empty, unpadded entries). validateBuildkitAckMutualExclusion rejects insecure_accept_opaque_buildkit_tunnels=true combined with a configured request_body.buildkit block, top-level or per profile — mediation supersedes the wholesale ack. - filter/buildkit.go + middleware.go: a new critical-severity inspector denies POST /session and POST /grpc unconditionally whenever request_body.buildkit is configured, since phase 1 ships no mediator to act on the richer policy yet — configuring the block must never look like it opened an opaque pass-through. - cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a /session or /grpc rule at startup when request_body.buildkit is configured (previously only the insecure ack satisfied this), with the admission/runtime split documented explicitly at the call site and pointing at filter.buildkitPolicy.inspect for the actual guarantee. * 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion Table-driven coverage for issue #185 phase 1's request_body.buildkit surface: zero-value defaults deny everything and pass Validate; ToPolicy() translates every field into buildkitproxy.Policy correctly; every allowlist validator (allowed_registries, allowed_realms, allowed_scopes, allowed_ids) rejects malformed entries with a dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual exclusion is checked both top-level and per client profile, in both directions (rejected together, accepted alone); and SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing Viper reflection-based default registration for both a nested bool (control.allow_info) and a nested string-slice (session.secrets.allowed_ids), confirming no map/slice caveat blocks env override for this config surface. Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML (well-formed, mistyped fields, the ack/mediation conflict, whitespace/ empty allowlist entries, unicode) so mutation spends its budget exploring the new schema's key space specifically, per the repo convention that config parsing gets fuzz coverage. Ran a 15s local fuzz pass with no crashes. * 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation Unreleased entry for issue #185 phase 1: the vendored protobuf subset, the method classification registry, and the request_body.buildkit config surface, with an explicit note that runtime mediation itself (and any change in what /session and /grpc actually do) lands in later phases of the epic. * 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency internal/buildkitproto's generated code imports google.golang.org/protobuf directly (issue #185 phase 1) rather than pulling it in transitively through another dependency, so go mod tidy correctly reclassifies it from indirect to direct in go.mod. No other dependency changed; go.sum is unaffected. * 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate Verified locally: production total with the exclusion is 96.1% (vs 80.7% with 9.5k generated protobuf statements counted); the hand-written buildkitproxy package stays in the gate. * 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1 - 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector - 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count) - 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test The ops.proto trimmed-provenance finding was refuted: the vendored file is byte-identical to upstream v0.32.0 except the documented go_package retarget (upstream sha256 matches PROVENANCE.md's recorded value).
…225) * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing * ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams Adds the real BuildKit gRPC mediator on top of phase 1's classification registry. bridge.go/upgrade.go/mediator.go terminate the hijacked POST /session and POST /grpc connections as h2c using golang.org/x/net/http2 (Server.ServeConn against the Docker client, Transport{AllowHTTP:true} dialing the daemon) and bridge them at the http.Handler level — one gRPC stream per *http.Request, original bytes preserved verbatim (no protobuf re-encode; that's phases 3-5). POST /session keeps buildkitd's reversed client/server roles, wired through a single symmetric bridgeLegs abstraction shared with /grpc. Every stream is classified through Classify() before being forwarded; a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays open. limits.go adds defense-in-depth DoS caps (max concurrent streams, a denied-stream budget over a rolling window) — numeric defaults are sockguard's own judgment call, not specified by the #185 synthesis. session.go adds a session registry keyed by client identity + profile, never the client-supplied UUID alone (recorded only as advisory metadata). Any genuine bridge/transport error tears the whole tunnel down; a Deny or size-cap trip does not. * 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed Deviation from the literal task text ("Passthrough and Mediate both forward in Phase 2"): forwarding a method's *category* isn't enough on its own, or configuring any single request_body.buildkit field would silently forward every other Mediate/Passthrough method too, defeating the point of the granular per-field switches. policy.go adds Policy.Allowed(endpoint, service, method) as a second, necessary gate consulted alongside Classify() in bridge.go's handleStream — audited under a distinct buildkit_policy_denied reason code, separate from Classify's own buildkit_method_denied. * 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright filter/buildkit.go's inspect() no longer unconditionally denies POST /session and POST /grpc once request_body.buildkit is configured — those now pass through to internal/buildkitproxy's mediator (wired into the hijack tier via cmd/serve.go's new withBuildkitMediator layer). The literal /moby.buildkit.v1.Control/<Method> probe path stays hard-denied regardless of the method's own classification: it carries no h2c upgrade, so there's no tunnel for any mediator to terminate. * 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive integration-style tests of the bridge/mediator without mocking http2 internals. A small clientLegConn interface on bridge (satisfied structurally by *http2.ClientConn) lets forward()'s harder-to-reach error branches — a genuine RoundTrip failure vs. a response-body copy failure vs. the request size-cap trip — be exercised deterministically via a hand-rolled fake instead of racing real connection teardown. internal/buildkitproxy is at 100% statement coverage. * 📦 deps(go.mod): promote golang.org/x/net to a direct dependency Approved exception for the mediation path only (golang.org/x/net/http2). No grpc-go anywhere in the chain; google.golang.org/protobuf was already direct. * 📝 docs(changelog): document phase 2 Fixes: #185 * 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge - 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter - 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy - 🔒 validate the daemon's 101 response headers before bridging - 🧪 convert standalone buildkitproxy tests to table-driven form - 📝 correct the stale opaque-pass-through comment in cmd/rules.go * 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic in limitedReadCloser.Read wrap negative and panic on the slice; compare by subtraction instead and pin it with a table row.
…226) * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%. * 🎨 style(buildkit): gofmt solve_test.go * 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy.
* ✨ feat(buildkit): #185 phase 4 — credential session mediation - moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken, GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and policy mediation instead of phase 2's blanket byte-verbatim relay: every RPC's Host is checked against request_body.buildkit.session.auth's already-existing allowed_registries (now actually consumed for the first time, and normalized the same lowercase/index.docker.io->docker.io way as Solve's registry allowlists so config-side case can't silently fail to match); FetchToken additionally requires its Realm to match allowed_realms and every requested scope to be a member of allowed_scopes, since a registry's token realm can live on a different host than the registry itself and the registry-host allowlist alone can't bound where a resulting bearer token is minted for. - moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact allowed_ids match; a non-empty annotations map is denied outright — researched against BuildKit v0.32.0's own secret request/response path, which never populates or reads that field, so there's no reviewed semantics to admit instead of silently forwarding it. - moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids. ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no unary message to decode) is gated on the agent ID carried in gRPC metadata (buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is relayed; the stream itself is never decoded, buffered as a message, or logged in either direction. - Every admitted RPC forwards the client's original frame bytes verbatim, exactly like phase 3's Solve/Status; every response — which for these RPCs carries the actual credential/secret/token content — is never decoded on any path. - A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512) throttles Auth/Secrets/SSH call volume independent of the connection-wide denied-stream abuse budget, since an admitted call never trips that budget; exceeding it denies RESOURCE_EXHAUSTED/ buildkit_credential_call_limit_exceeded without counting against the abuse budget. - Audit events for these RPCs carry the registry host in the clear (low-cardinality, operator-facing) but only a truncated SHA-256 of secret/ SSH-agent IDs, never the raw identifier. - FileSync/FileSend/Upload are untouched (phase 5's scope). internal/ buildkitproxy stays at 100% statement coverage; repo-wide production coverage holds at 96.3%. * 🔒 security(buildkit): log normalized registry host in auth audit events normalizeAuthHost trims surrounding whitespace before the allowlist comparison, so a Host like "ghcr.io\n" is admitted while the raw field — CR/LF included — reached the audit log's registry_host attr, letting an admitted client forge audit log lines. Log the normalized host (the value policy actually compared) wrapped in logging.SafeString, matching every other attr audit emits. Regression test asserts an admitted whitespace-padded host audits the canonical form through a live bridge. * 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators Real-world secret/SSH IDs are short guessable operator labels ("default", "npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline dictionary in milliseconds, leaking the very identifier shortHash exists to withhold. A per-process crypto/rand salt keeps same-ID correlation within one process while breaking dictionary recovery, with a regression test pinning the salted-vs-unsalted difference. Also adds FuzzEvaluateGetSecretRequest over the credential decode/match surface (never panic, never admit outside policy), widening mustMarshal to testing.TB for fuzz seeding.
Mediates the three remaining byte-verbatim streaming Mediate methods
(moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy,
moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a
new streaming primitive (streamRelayReader/relayValidatedFrames) that
decodes each gRPC length-prefixed frame, validates it, and forwards the
original bytes verbatim on admission.
FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets
structural validation only; response direction (PACKET_STAT/DATA) gets
path-traversal/absolute-path/NUL checks on every Stat path and symlink
target, plus file-count/per-file-byte/total-byte/path-length caps. The
"dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA
is buffered independently by its fsutil-assigned ID (never per-stream —
fsutil's real sender interleaves multiple files across four parallel
workers) until that file's own EOF, then the same RUN-instruction/
syntax-frontend inspection the classic /build path uses runs against the
reassembled bytes before release; a denial discards the held bytes
without releasing anything. Holding is skipped when allow_run_instructions
is already set.
FileSend/DiffCopy requires an admitted Control/Solve from the same
SessionKey and gets a pure byte-count cap with no decode at all —
moby/buildkit's own server dispatches FileSend to one of two different
wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI)
depending on the negotiated exporter, with no signal sockguard can see to
tell them apart.
Upload/Pull is bound to a one-use token: an admitted Solve's
FrontendAttrs naming a context/context:<name> value shaped like
http://buildkit-session/<id> registers <id> as a one-use key, and a Pull
must present the matching urlhost/urlpath metadata and consume it
atomically. Upload's BytesMessage frames are unambiguous and get the
same decode-plus-unknown-fields check as every other mediated message.
New request_body.buildkit.session.{file_sync,file_send,upload} cap
config (max_files, max_total_bytes, max_path_length, max_file_bytes,
max_bytes), threaded through Limits/Policy via a new per-session
effectiveLimits merge. FileSync/TarStream stays hard-denied with no new
knob. internal/buildkitproxy stays at 100% statement coverage.
- chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(release): release-level inference restricts legacy prefixes to emoji only - ci(verify): commit check diffs against the PR base branch, not origin/main - docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING - test(release): word-prefix rejection and composed emoji coverage
…up (#234) - fix(ci): integration setup tolerates podman's socket unlink/rebind race - fix(ci): rootful podman socket restricted to runner primary group (0750/0660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(release): roll release metadata forward to v1.7.0 Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase 1-6b BuildKit gRPC mediation work plus #186's endpoint-config narrowing), bumps the website version, Helm chart, and README latest-stable banner to match, flips the v1.7.0 roadmap milestone to released, and adds a v1.8.0 placeholder covering the one gap phase 1-6b left open (RUN-instruction inspection for frontend-less Solve requests, per #185). Updates page-data.test.mjs's hardcoded next/latest-released assertions to match. * chore(deps): regenerate next-env.d.ts after the Next upgrade The npm bumps in #229 moved Next to a version that emits an extra root-params.d.ts reference. The generated files were never refreshed, so every local build dirtied the tree and tripped the clean-tree pre-push hook.
The roadmap conflicted: main's rewrite dropped the three-tool compatibility table that dev had just updated to record BuildKit gRPC mediation shipping. Kept main's structure and put the table back under the compatibility boundary section so the shipped status still appears in the v1.7.0 docs.
The soak harness never passed --listen-socket/--upstream-socket, so sockguard died ~40s into every run. Fixed, and verified locally at 15s/conc-10 with zero errors across all four scenarios. quality-soak-weekly.yml and quality-go-bench-monthly.yml piped into tee under the default shell, which reports tee's exit code, so a failing run read as green; both now use shell: bash. Benchmark artifacts upload on failure too. The zizmor cache-poisoning ignore claimed the repo was private. ci-verify.yml never publishes (goreleaser runs --skip=publish) so it keeps an accurate ignore; release-from-tag.yml genuinely publishes, so caching is disabled there instead of suppressed.
Removes the Go Report Card and pkg.go.dev badges, which no longer resolve. The mutation-score badge is now generated from the monthly run rather than hardcoded: quality-mutation-monthly.yml commits .github/badges/mutation-score.json and the README reads it through a shields endpoint badge, seeded at 97.8% from run 30692172568. The badge only publishes when all six package reports parse, so a partial run can't overwrite it with a skewed score.
…map tense) (#242) * build(docker): add .dockerignore to trim the build context docker build runs with context: . in three workflows (including the required Docker Build check), shipping the full ~2GB repo root — node_modules, website/docs node builds, worktrees — when the Dockerfile only needs app/. * ci(quality-mutation): guard the tee pipeline with shell: bash The step piped gremlins output through tee without shell: bash, so it ran under the default bash -e {0} (no -o pipefail) and reported tee's exit code instead of gremlins'. Matches the guard already used in the soak and benchmark workflows. * chore(lint): add markdownlint and yamllint config for CodeRabbit .coderabbit.yaml enables both tools and CodeRabbit reviews every PR, but nothing in CI or lefthook runs either one locally, so they've been running on stock defaults. Tunes them the same way drydock does: skip line-length, duplicate-heading, inline-HTML, and first-line-heading noise for markdownlint; skip document-start, truthy, and line-length noise for yamllint. * docs(readme): mark v1.7.0 shipped in the roadmap section The roadmap blurb still called v1.7.0 "the first committed epic in the v1.7.0 milestone" in future tense with no shipped subsection, while the top note and Recent Updates described it as already released. v1.7.0 GA published 2026-08-11 (#185, #186); all three places now agree. * build(deps): dedupe lockfile drift on magic-string npm dedupe found fumadocs-mdx's nested magic-string could converge to the newer 1.1.1 patch already satisfying its ^1.1.0 range; the pre-push lockfile-dedupe gate blocks on any drift, unrelated to the hygiene fixes in this branch.
… job (#243) benchmarkInspectPolicy wrapped per-iteration request setup in b.StopTimer()/b.StartTimer(), which forces two runtime.ReadMemStats() calls per loop iteration outside the timed window. Those calls are invisible in ns/op but scale with b.N, and since the wrapped benchmarks are cheap (tens to low-hundreds of ns/op), b.Loop() calibrated N into the hundreds of thousands to over a million, turning a ~1s measurement into 40-90s of real wall time per rep. That's why quality-go-bench-monthly.yml has been getting cancelled at its 30m ceiling every month since these benchmarks landed in May (the May 1 run predates the file and succeeded): the job was never actually hung, just quietly paying millions of hidden ReadMemStats calls with nothing to show for it in the logs. Drop the StopTimer/StartTimer calls to match every other benchmark in the repo, which already builds per-iteration fixtures inline inside the timed loop. Verified locally: the full `-count=5 ./...` suite that used to blow past 30 minutes without finishing one package now completes in ~13 minutes. Bump timeout-minutes to 40 for headroom on slower CI hardware.
* ci(fuzz): cover every fuzz target across tiers * chore(deps): dedupe magic-string lock entry * test(fuzz): reject stale workflow targets
* fix(build): establish repository-root Go module * refactor(go): stage canonical import migration * ci(security): activate canonical Gosec scan * fix(module): address staged review findings * fix(module): close staged review gaps * refactor(go): migrate service package imports * build(go): update toolchain to 1.26.6 * fix(module): keep staged package boundaries coherent * refactor(go): complete canonical import migration * refactor(go): remove staged compatibility packages * test(go): harden final layout checks * test(go): detect lowercase Docker instructions
* fix(deps): update nanoid to 3.3.18 * test(security): guard nanoid lock versions
* ci(review): expose unreviewed promotion ranges * fix(ci): mirror bridge eligibility
* ci(review): restore reviewed promotion changes * fix(ci): report incomplete mutation results * test(ci): strengthen incomplete-report contract
* feat(website): add cookieless PostHog analytics * fix(website): make route manifest checks race safe * fix(website): preserve pre-init analytics events
Remove the Go Report Card badge and CSP allowance from the website.
…es (#285) * ci(gating): require the security scanners and drop emoji from job names - ci(gating): move security-grype.yml's paths filter from the trigger to a changes job so its contexts always report - ci(gating): make the Commit Message job blocking, exempting promotion PRs - ci(style): strip emoji from all workflow and job names - test(ci): pin the required-context list at 17 A required status check on a job inside a workflow that never triggers produces no check run at all, so branch protection can never see it go green. The paths filter therefore has to live in a job, not the trigger. Closes #284 * ci(gating): restrict the promotion-PR exemption to same-repo branches github.head_ref is attacker-chosen on a fork, so a fork branch named dev/anything targeting main would have skipped the commit-message gate. Gate the exemption on head.repo.full_name matching github.repository. Caught by CodeRabbit. * ci(style): strip emoji from the ZAP workflow and pin it with a test security-zap-baseline.yml landed from #280 one merge after the original sweep, still carrying emoji in its workflow and job names. A check context is matched by exact string, so the whole set has to stay clean, not just the names required today. Test walks every workflow's name:/run-name:.
Renames Unreleased to 1.7.1 per RELEASING.md step 5, backfills the nine missing PR citations, corrects the now-stale note that Dependency Review, Gitleaks, and Actionlint aren't required yet, and drops the shipped SBOM clause from the roadmap's supply-chain row.
* ci(release): stop the cut gate timing out before CI can finish release-cut.yml overrode the verify-ci-success action's max-attempts down to 20 (5 min), but a CI: Verify run on main takes ~6m45s. The v1.7.1 cut failed on its first attempt with the run it was waiting for succeeding 9s later. Drop the override so both callers inherit the action's 40-attempt default, and make the poll log say whether a run is in progress or absent -- those were printing the same line. Closes #288 * ci(release): say 'has not completed' rather than 'is in progress' The count is status != completed, which covers queued/waiting/requested too, so the old wording claimed more than the number supports. Renamed in_progress_count to noncompleted_count to match. Caught by CodeRabbit.
…e rows (#291) * ci(quality): resolve a current sockguard for the current-* conformance rows current-standard and current-edge floated portwing and drydock to latest but left sockguard on the audited-floor 1.5.1, so 1.6.0, 1.7.0 and 1.7.1 were never covered by the weekly suite. Resolve those two rows from tag history instead, concrete rather than latest so a red run names its own inputs. legacy-floor's pins and the explicit sockguard_image override are unchanged. Refs #289 * fix(conformance): resolve current-* rows to a published sockguard image resolve_latest_sockguard_version stopped at the newest stable git tag, which is not the same thing as the newest pullable image. A tag lands minutes before release-from-tag finishes publishing its image, and a failed publish job leaves a tag with no image behind indefinitely -- either window handed the row an unpullable reference. Walk tags newest-first and stop at the first one docker manifest inspect finds published on Docker Hub (a bare codeswhat/* ref is Hub, not GHCR). Every skipped tag is announced on stderr so a broken publish surfaces instead of quietly passing as an older version, and an unreachable registry is fatal rather than a reason to walk back. - test(conformance): cover all four resolver paths in --self-test with a stubbed registry, keeping the self-test network-free - docs(conformance): correct README and CHANGELOG, which both described resolution as purely local Fixes: #289 * test(conformance): make the resolver self-test independent of tag history The new resolver cases read the real `git tag` list, so they passed locally and failed on the Node test gate, which checks out at the default depth 1 with no tags: every case took the "no stable v* tags found" exit, and none-published still reported PASS -- non-zero for entirely the wrong reason. Stub `git` the same way `docker` is already stubbed, over a fixed synthetic tag list. The self-test stops depending on checkout depth, the expectations become exact constants rather than whichever release is newest this week, and the prerelease filter gets pinned (v2.1.0-rc.1 must never win).
* ci(release): sign and attest the per-platform release archives Only sockguard-v<tag>.tar.gz carried a .sig/.pem and a provenance attestation, and that file is the git-archive SOURCE snapshot built by the separate `release` job. The archives users actually download, sockguard_<version>_<os>_<arch>.tar.gz, shipped with a CycloneDX SBOM and nothing else. The near-identical filenames are what hid it. Add two keyless cosign `signs:` blocks so every archive and checksums.txt get a .sig + .pem, and attest build provenance over app/dist/*.tar.gz. The goreleaser job gains id-token: write and attestations: write, plus a cosign install, since the signs blocks shell out to it. - docs(verification): document verifying the per-platform archives, and spell out the source-vs-binary filename distinction Verified with a real tagged build against a stubbed cosign (GoReleaser disables signing under --snapshot, so a snapshot run proves nothing): all ten expected signature artifacts are produced. The sigstore round trip itself only exercises on the next release cut. Fixes: #271 * test(release): make QA-6 verify the archives the docs tell people to verify scripts/verify-published-release.sh exists to fail the tag when docs/content/docs/verification.mdx and the pipeline drift apart. It could only ever do that for the source tarball, so the per-platform archive, checksums.txt, and provenance commands this branch documents would have passed QA-6 without being run even once. It now verify-blobs the linux/amd64 archive and checksums.txt alongside the source tarball, runs the documented `sha256sum --check --ignore-missing`, and runs `gh attestation verify` against the compiled archive. One archive stands in for all four: a single signs: block in a single job signs them together, so a config or identity fault fails them together. - test(release): pin the new coverage so it can't silently regress
…errors (#294) assert_remote_update_trigger polled drydock's unversioned /api/containers and /api/triggers/docker/update, which 410 as of drydock v1.6.0. The 410 body got fed straight into jq, whose type error was discarded by 2>/dev/null, so the assertion decayed into the same empty-result timeout a real store-sync failure produces -- and that misreported a harness bug as a drydock regression (CodesWhat/drydock#802) for about a month. Nothing was ever wrong with drydock. Both calls move to /api/v1. The poll checks the HTTP status before handing the body to jq, asserts the {data: [...]} envelope rather than relying on .data[]?, and surfaces jq failures, so this class of break now fails on the spot with its own message. drydock 1.6.x's 400 'No update available for this container' is accepted as the same documented refusal 1.5.2 gives as 404, matched on the body so a genuine shape regression still fails. Verified on a dispatched conformance run: all three rows green (https://github.com/CodesWhat/sockguard/actions/runs/32317295410). Fixes: #289
…296) npm dedupe --dry-run compares against the installed tree, not the lockfile, so a checkout whose node_modules predates the last few Renovate bumps reported every one of them as drift. The remediation was worse than useless: npm dedupe for real installs the right versions, so the hook passed afterwards and looked like the advice worked, but it wrote no lockfile change, so there was nothing to commit. Probe the environment first with npm install --dry-run. If the installed tree doesn't match the lockfile, say so, name npm ci, and stop without using the word drift. Only once that probe is clean does the dedupe report mean anything. Logic moves out of the inline run: block into scripts/lockfile-dedupe.mjs with 13 tests, and the stale-install path was verified end to end against a real broken install rather than only in unit tests. Fixes: #295
|
Deployment failed for project sockguard-website with the following error: Learn More: https://vercel.com/codeswhat?upgradeToPro=build-rate-limit |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (2)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe lockfile hook now invokes a dedicated CLI. The CLI separates stale Possibly related PRs
Merge Risk: ⚪ Minimal · up to The conformance harness now validates the expected trigger-refusal responses and surfaces unexpected errors, while the lockfile hook distinguishes stale installs from real lockfile changes. No actionable merge-blocking risk remains after normal checks and review. 🚥 Pre-merge checks | ✅ 1 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.
Inline comments:
In `@scripts/tri-tool-conformance/run-matrix.sh`:
- Around line 1162-1176: Update the trigger refusal validation in the matrix
assertion so every accepted refusal validates its response body: accept only 404
responses containing “trigger not found” and 400 responses containing “no update
available” (case-insensitive). Remove 501 from the accepted outcomes, and ensure
arbitrary 404/501 responses fail while preserving the existing record_result
reporting.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5bfc52ef-5477-4b69-ba71-de542f1f9fd5
⛔ Files ignored due to path filters (1)
CHANGELOG.mdis excluded by!CHANGELOG.md
📒 Files selected for processing (5)
lefthook.ymlscripts/lockfile-dedupe.mjsscripts/lockfile-dedupe.test.mjsscripts/tri-tool-conformance/README.mdscripts/tri-tool-conformance/run-matrix.sh
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.
Assertion 8's 404|501 arm passed on the bare status. Express answers an unknown route with 404 'Cannot POST /...', so a wrong trigger URL would have passed the assertion while testing nothing -- the same shape as the store-poll bug this series exists to close. Both accepted refusals are now identified by their response body. 501 leaves the accepting arm. No row has ever returned it: the artifacts record 404 on legacy-floor (drydock 1.5.2) and 400 on both current-* rows (1.6.x). The 501 in the bundle README is Portwing's own trigger endpoint, a different service from the drydock:3000 API this posts to. The first cut asserted the literal 'trigger not found', which the README has claimed since #211 and which a real run disproved: drydock answers {"error":"Remote update trigger portwing.docker.update not found"}, with the middle varying by row. The match is 'trigger .*not found', and the 404 PASS detail now records drydock's actual error string so a future assertion can be written against evidence rather than a doc claim. Verified on run 32323886778: all three rows and the self-test green. Also carries an unrelated build(deps) commit deduping fumadocs-mdx's nested magic-string, which the pre-push lockfile gate required before it would let the branch push.
f9e9cc8
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
biggest-littlest
left a comment
There was a problem hiding this comment.
Re-approved after #299 landed on dev/v1.7. CodeRabbit reviewed the new commits and confirmed the body-checked refusals; conformance green on all three rows in run 32323886778.
Promotion of
dev/v1.7intomain. Tree-level diff is two changes, six files:#294 — the conformance harness stops swallowing its own errors (closes #289)
Assertion 8 polled drydock's unversioned
/api/containersand/api/triggers/docker/update, which became 410 tombstones in drydock v1.6.0. The 410 body fed straight intojq '.data // .', the type error went to2>/dev/null, and what came back was an empty result — bit-for-bit what a genuine store-sync failure produces. The row timed out withsentinel never appeared in drydock's store, which names drydock in the failure text, and that is how a harness bug got reported as a shipped drydock regression and sat for about a month. Nothing was ever wrong with drydock; CodesWhat/drydock#802 is retitled and my misattributing comments on #289 are corrected there.Both calls move to
/api/v1. The poll captures the HTTP status with--write-out '%{http_code}'and recordsharness error, not a conformance failurewith the status and body on any non-2xx, and surfaces jq failures instead of returning empty. It also asserts the{data: [...]}envelope rather than relying on.data[]?, which returns empty with exit 0 on a missing, null, or scalar.data— an emptydataarray is left alone, since that is the ordinary not-synced-yet state.Fixing the path then exposed the step behind it, which nothing could reach before: drydock 1.6.x refuses an unconfigured trigger with
400"No update available for this container" where 1.5.2 refuses with404"trigger not found". Both are the audited bundle's documented boundary, and 1.6.x getting far enough to evaluate the container proves it accepted the request shape, which is what the assertion actually checks. That400now passes, matched on the response body rather than on a drydock version, so drydock's other400— "Invalid trigger request body", a real shape regression — still fails.Verified on a dispatched run, not inferred. No PR check in this repo exercises assertion 8, which is exactly why it rotted quietly. Run 32317295410 is green on
current-standard,current-edge,legacy-floor, and the harness self-test.#296 — the lockfile-dedupe hook stops misnaming the cause (closes #295)
npm dedupe --dry-runcompares against the installed tree, not the lockfile, so a checkout whosenode_modulespredates the last few Renovate bumps reported every one of them asLockfile drift detected — run 'npm dedupe' and commit the result. The advice was worse than useless:npm dedupefor real installs the right versions, so the hook passed afterwards and looked like the advice worked, but it wrote no lockfile change, so there was nothing to commit.The hook now probes
npm install --dry-runfirst. If the installed tree does not match the lockfile it says so, namesnpm ci, and stops without using the word drift. Only once that probe is clean does the dedupe report get to mean anything. Logic moved out of the inlinerun:block intoscripts/lockfile-dedupe.mjswith 13 tests, and the stale-install path was verified end to end against a deliberately broken install rather than only in unit tests.Notes
The commit list is long because of squash-merge ancestry; tree equality is the check that matters and the file diff above is the whole of it. The
Commit Messagegate takes themain:dev/*promotion exemption — the eight legacy gitmoji commits from the #185 BuildKit series were validated when they landed on the dev branch, and that history is published and never rewritten.Known-ignorable red:
Vercel(deployment rate limited, 24h) andqlty check(out of minutes). Neither is a required context.Fixes: #289
Fixes: #295
Changelog
scripts/lockfile-dedupe.mjswith 13 tests.node_modulesdetection from genuine lockfile drift.Concerns
current-standardandcurrent-edgetest and report current sockguard versions.legacy-floorretains its audited version pins.remote-update-triggerfailure modes.Added after the first review: #299 — body-check every accepted trigger refusal
CodeRabbit's review of this promotion found a MAJOR issue in #294's work, and it was right. Assertion 8's
404|501arm passed on the bare status. Express answers an unknown route with404 Cannot POST /..., so a wrong trigger URL would have passed the assertion while testing nothing — the same shape as the store-poll bug this series exists to close.501also left the accepting arm. It was accepted unvalidated and no row has ever returned it: the artifacts record404onlegacy-floor(drydock 1.5.2) and400on bothcurrent-*rows (1.6.x). The501in the bundle README is Portwing's own trigger endpoint, a different service from thedrydock:3000API this posts to.Fixing it turned up a second thing. My first cut asserted the literal
"trigger not found", which the README has claimed since #211 and which run 32323163685 disproved by failinglegacy-floor:That doc claim had never been checked, because nothing ever read the body. The middle varies by row (it's the agent-qualified trigger name), so the match is
trigger .*not found, which still rejects{"error":"Not Found"}and theCannot POSTbody. The404PASS detail now records drydock's actualerrorstring, so the next assertion can be written against evidence rather than a doc claim.Verified: run 32323886778, all three rows and the self-test green.
package-lock.jsonalso appears in the diff now: the pre-pushlockfile-dedupegate blocked #299's push with genuine drift infumadocs-mdx's nestedmagic-string(1.2.1 → 1.2.2). Unlike the false-positive class #296 fixes,npm dedupehere produced a real three-line change. Kept as its ownbuild(deps)commit.