Skip to content

✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - #226

Merged
scttbnsn merged 3 commits into
dev/v1.7from
feat/185-phase3-control-mediation
Aug 10, 2026
Merged

✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership#226
scttbnsn merged 3 commits into
dev/v1.7from
feat/185-phase3-control-mediation

Conversation

@scttbnsn

@scttbnsn scttbnsn commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Phase 3 of #185 (BuildKit gRPC mediation): per-message mediation for moby.buildkit.v1.Control on the /grpc tunnel.

What changed

  • Control/Solve and Control/Status now get per-message gRPC decode + policy mediation instead of phase 2's byte-verbatim relay: the single unary request frame is buffered (readUnaryGRPCMessage — compressed frames, malformed framing, and multi-message streams all fail closed), decoded with the vendored buildkitproto stubs, checked, and on admission the original frame bytes forward untouched — never a re-encode.
  • Solve checks: entitlements (security.insecure denied outright, network.host gated on allow_host_network, unknown entitlements denied), fixed frontend allowlist (""/dockerfile.v0 only — gateway.v0 would relocate the hard-denied LLBBridge surface), strict FrontendAttrs key allowlist with remote-context and force-network-mode=host gating, cache import/export type+registry allowlists, exporter type+push-registry allowlists, and a protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize (buildkit_schema_unsupported).
  • Beyond-the-checklist denials: deprecated singular cache/exporter fields, EnableSessionExporter, and non-empty SourcePolicy are denied with no enabling knob — each would let a structurally "known" field bypass the allowlists or reach other hard-denied surface.
  • Ref ownership: the session registry gains a refcounted ownership index keyed by client identity + profile (never a client-supplied UUID/ref alone). Status is denied buildkit_ref_not_owned unless its ref belongs to an admitted Solve from the same identity+profile; refs release on session close; MaxRefsPerSession bounds registration.
  • Config: new request_body.buildkit.control.solve.* allowlist knobs following existing conventions (empty list = deny, per-profile falls out of ClientProfileConfig, env override via existing reflection binding). allow_host_network/allow_remote_context are threaded from the sibling request_body.build block rather than duplicated.

Validation

  • go build, go vet, golangci-lint run clean
  • go test -race ./internal/buildkitproxy/... and full suite green
  • internal/buildkitproxy at 100.0% statement coverage; repo-wide production profile at 96.2% (gate: 96%)

Part of #185 (phases 4–6 remain — do not auto-close).

Changelog

  • ✨ Added per-message mediation for BuildKit Control/Solve and Control/Status.
  • ✨ Added protobuf framing and recursive unknown-field validation.
  • ✨ Added Solve policy checks for entitlements, frontends, cache settings, exporters, registries, source policies, deprecated fields, and session exporters.
  • ✨ Added session ref ownership tracking by client identity and profile.
  • ✨ Added per-session ref limits with a default of 256.
  • ✨ Added Solve cache and exporter allowlists.
  • 🔧 Reused request_body.build permissions for host networking and remote contexts.
  • 🔧 Preserved admitted original gRPC frame bytes during forwarding.
  • 🔧 Added gRPC status handling for invalid arguments and failed preconditions.
  • 🐛 Released session refs when sessions close.
  • 🔒 Denied Status requests for refs not owned by the same client identity and profile.
  • 🔒 Applied default-deny behavior to unspecified cache and exporter values.
  • ✨ Added unit and end-to-end coverage for mediation, framing, policy validation, ownership, limits, and cleanup.

Concerns

  • Verify all callers update for the changed BuildkitRequestBodyConfig.ToPolicy(build BuildRequestBodyConfig) signature.
  • Verify profile and environment overrides cannot bypass Solve allowlists.
  • Verify concurrent session close and ref admission cannot leave stale ownership entries.
  • Verify unknown protobuf fields remain denied in every nested mediated message.
  • Verify denied requests never reach the BuildKit daemon.

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

vercel Bot commented Aug 10, 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 10, 2026 2:24am

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@scttbnsn, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f4c5922f-38f1-4974-aa8d-af6e2a76067e

📥 Commits

Reviewing files that changed from the base of the PR and between 9c2d5a5 and 8fc3058.

📒 Files selected for processing (9)
  • app/internal/buildkitproxy/bridge.go
  • app/internal/buildkitproxy/bridge_controlmediated_test.go
  • app/internal/buildkitproxy/grpcstatus.go
  • app/internal/buildkitproxy/session.go
  • app/internal/buildkitproxy/session_test.go
  • app/internal/buildkitproxy/solve.go
  • app/internal/buildkitproxy/solve_test.go
  • app/internal/config/buildkit_config_test.go
  • app/internal/config/buildkit_options.go
📝 Walkthrough

Walkthrough

The proxy now mediates Control/Solve and Control/Status. It validates unary gRPC framing, protobuf schemas, entitlements, frontends, caches, exporters, registries, and source policies. It preserves admitted request frames during forwarding. The session registry tracks reference ownership and enforces per-session limits. BuildKit configuration translation now includes build-level controls and Solve allowlists. Existing generic forwarding tests use non-mediated RPCs.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/185-phase3-control-mediation

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (8)
app/internal/buildkitproxy/framing.go (1)

81-84: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Read once into the frame buffer instead of allocating twice.

The payload is allocated, then copied into a second buffer of the same size. Read straight into the frame and slice the payload.

♻️ Single allocation
-	payload = make([]byte, length)
-	if _, err := io.ReadFull(r, payload); err != nil {
+	frame = make([]byte, grpcMessageHeaderLen+length)
+	copy(frame, header[:])
+	payload = frame[grpcMessageHeaderLen:]
+	if _, err := io.ReadFull(r, payload); err != nil {
 		return nil, nil, fmt.Errorf("%w: reading message payload: %w", errUnaryFrameProtocolError, err)
 	}

Then drop the trailing reassembly block and return frame, payload, nil.

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

In `@app/internal/buildkitproxy/framing.go` around lines 81 - 84, Update the
framing read logic around the frame construction to allocate only the complete
frame buffer, read the payload directly into its payload region after the gRPC
header, and derive the payload slice from that region. Remove the separate
payload allocation and copy/reassembly block while preserving the existing
header handling and return values.
app/internal/buildkitproxy/framing_test.go (1)

140-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use table-driven tests for the new cases.

Both sites add one-case tests. Put each case in a test case table.

  • app/internal/buildkitproxy/framing_test.go#L140-L153: add a reader factory and expected error substring to TestReadUnaryGRPCMessage.
  • app/internal/config/buildkit_config_test.go#L119-L130: add build flags and expected SolvePolicy fields to a policy-translation test table.

As per coding guidelines, **/*_test.go requires table-driven tests; httptest.NewServer applies when a test is an integration test.

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

In `@app/internal/buildkitproxy/framing_test.go` around lines 140 - 153, Convert
the new trailing-probe error case in
app/internal/buildkitproxy/framing_test.go#L140-L153 into a table entry within
TestReadUnaryGRPCMessage, adding a reader factory and expected error substring
while preserving the existing assertions. Also convert the new
policy-translation case in app/internal/config/buildkit_config_test.go#L119-L130
into the existing table, adding its build flags and expected SolvePolicy fields;
both sites require direct changes.

Source: Coding guidelines

app/internal/config/buildkit_config_test.go (1)

327-359: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add fuzz coverage for Solve allowlist parsing.

Add a Fuzz... test for request_body.buildkit.control.solve configuration parsing. Feed arbitrary cache type, registry, exporter type, and exporter registry values through the Load boundary. Assert that parsing does not panic and that invalid values fail validation.

As per coding guidelines, app/**/*_test.go requires fuzz tests for config parsing.

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

In `@app/internal/config/buildkit_config_test.go` around lines 327 - 359, Add a
Fuzz... test alongside the existing Buildkit configuration tests, targeting
request_body.buildkit.control.solve parsing through Load. Feed arbitrary cache
type, cache registry, exporter type, and exporter registry values via
environment-backed configuration, assert Load never panics, and verify invalid
values fail validation while valid values are accepted.

Source: Coding guidelines

app/internal/buildkitproxy/bridge_controlmediated_test.go (1)

115-137: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The ref-cap subtest does not assert the daemon was spared.

Every other denial subtest asserts daemonCalled stays false. This one uses echoDaemonHandler() and checks only the status code. A regression that registers the ref, denies the client, but still forwards the second Solve to the daemon would pass.

Add the same daemon-not-called assertion for the second Solve, or assert tb.registry.OwnsRef(tb.session.Key, "ref-2") is false.

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

In `@app/internal/buildkitproxy/bridge_controlmediated_test.go` around lines 115 -
137, The ref-cap test must verify that the denied second Solve is not forwarded
to the daemon. In the “ref cap exceeded denies the second solve” subtest, add
the existing daemon-called assertion after validating the second response, or
verify via tb.registry.OwnsRef that ref-2 was not registered.

Source: Path instructions

app/internal/buildkitproxy/bridge.go (1)

400-403: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Admitted refs are registered before the daemon accepts the Solve.

PutRef runs first, then forwardWithBody. If the daemon rejects the Solve (bad definition, unsupported frontend, transport failure), the ref stays in refOwners for the session lifetime. It holds a MaxRefsPerSession slot and keeps Control/Status admitted for a ref no build ever started.

Same trust boundary, so this is not an escalation. It does let a client burn the whole 256-slot cap with Solves the daemon refuses, and it makes the cap a weaker DoS bound than the doc comment on Limits.MaxRefsPerSession claims. Consider releasing the ref when forwardWithBody ends in a transport failure.

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

In `@app/internal/buildkitproxy/bridge.go` around lines 400 - 403, The Solve path
registers the admitted ref via PutRef before forwarding, but failed transport
forwarding leaves it retained in refOwners. Update the flow around
forwardWithBody to release that ref when forwarding ends with a transport
failure, while preserving the admission for successfully accepted Solves and
existing audit behavior.
app/internal/buildkitproxy/session_test.go (1)

261-312: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add concurrent coverage for PutRef against Close.

tryPutRef's doc comment justifies its locking against "concurrent PutRef call on the same session from another HTTP/2 stream", and Close deliberately drops r.mu between the session delete and the ref release. No test drives either path concurrently, so -race never exercises the ordering these comments claim to handle.

Add a subtest that runs N goroutines calling reg.PutRef(s, ...) while another calls reg.Close(s.ID), then asserts reg.refOwners holds no entry for the key afterwards. That assertion is what fails under the leak described on session.go lines 174-208.

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

In `@app/internal/buildkitproxy/session_test.go` around lines 261 - 312, Add a
concurrent subtest to TestSessionRegistryCloseReleasesRefs that starts N
goroutines calling PutRef on the same session while another goroutine calls
Close(s.ID), wait for all goroutines to finish, and assert refOwners has no
entry for the session key. Use synchronization to coordinate completion and
exercise the Close/tryPutRef ordering under the race detector.

Source: Coding guidelines

app/internal/buildkitproxy/session.go (1)

225-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the exported Session.PutRef / Session.OwnsRef helpers.

Production callers use registry.PutRef and registry.OwnsRef; Session.PutRef/ref and Session.OwnsRef only work on s.Refs, skip MaxRefsPerSession, and do not update refOwners / the identity+profile ownership index. Delete them unless they must remain for tests; if kept, unexport them as putRefLocal / ownsRefLocal with a tests-only dependency path.

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

In `@app/internal/buildkitproxy/session.go` around lines 225 - 257, Remove the
exported Session.PutRef and Session.OwnsRef helpers and update any remaining
callers to use SessionRegistry.PutRef and SessionRegistry.OwnsRef, preserving
MaxRefsPerSession and refOwners/index bookkeeping. If tests require local-only
behavior, rename them to unexported putRefLocal and ownsRefLocal and keep their
use confined to test support.

Source: Path instructions

app/internal/buildkitproxy/solve_test.go (1)

33-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a fuzz test for evaluateSolveRequest.

evaluateSolveRequest is the filter-matching entry point for an untrusted wire payload, and the table covers only well-formed inputs. The coding guidelines require fuzz tests for filter matching.

♻️ Proposed addition
func FuzzEvaluateSolveRequest(f *testing.F) {
	f.Add(malformedPayload)
	f.Add(mustMarshal(f, &control.SolveRequest{Ref: "r", Frontend: "dockerfile.v0"}))

	policy := Policy{Control: ControlPolicy{Solve: SolvePolicy{Allow: true}}}
	f.Fuzz(func(t *testing.T, payload []byte) {
		req, d := evaluateSolveRequest(payload, policy)
		if (req == nil) == (d == nil) {
			t.Fatalf("exactly one of req and denial must be non-nil; req=%v denial=%v", req != nil, d != nil)
		}
	})
}

mustMarshal takes *testing.T; change its parameter to testing.TB so both the table test and the fuzz seed corpus can call it.

As per coding guidelines: "Use fuzz tests for filter matching and config parsing."

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

In `@app/internal/buildkitproxy/solve_test.go` around lines 33 - 43, Add a
FuzzEvaluateSolveRequest fuzz test that seeds malformed and valid marshaled
SolveRequest payloads, invokes evaluateSolveRequest with an allowing policy, and
asserts exactly one of the returned request or denial is non-nil. Update
mustMarshal to accept testing.TB so it can be reused by both regular and fuzz
tests.

Source: Coding guidelines

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

Inline comments:
In `@app/internal/buildkitproxy/bridge_controlmediated_test.go`:
- Around line 34-35: Replace the plain daemonCalled booleans with atomic.Bool at
all five test sites, update the daemon handlers to atomically store true, and
update the assertions to atomically load the value. Add the required atomic
package import and preserve the existing test behavior and failure messages.

In `@app/internal/buildkitproxy/bridge.go`:
- Around line 366-383: Add an explicit default branch to the switch on method in
the mediation logic, assigning d to a permission-denied result for unsupported
mediated methods. Preserve the existing Solve and Status evaluation paths,
ensuring any method admitted by isControlMediatedMethod but missing a switch
case is denied rather than forwarded with nil denial state.
- Around line 392-399: Update the Solve admission path around PutRef to reject
an empty SolveRequest.Ref before registering it, preventing empty refs from
passing later ownership checks; preserve valid-ref registration and existing
resource-exhausted handling. When PutRef denies admission because
MaxRefsPerSession is exceeded, record the distinct audit code
buildkit_ref_limit_exceeded instead of buildkit_policy_denied.

In `@app/internal/buildkitproxy/session.go`:
- Around line 174-208: Make SessionRegistry.Close hold r.mu continuously while
removing the session and decrementing all refs in refOwners, preventing PutRef
from publishing after closure; update app/internal/buildkitproxy/session.go
lines 174-208 accordingly. In app/internal/buildkitproxy/session_test.go lines
261-312, add a concurrent subtest that races PutRef(s, ...) with Close(s.ID) and
asserts refOwners has no entry for the session key afterward.

In `@app/internal/buildkitproxy/solve.go`:
- Around line 339-344: Update checkSolveSourcePolicy to also deny
SolveRequest.SourcePolicySession when it is set, alongside the existing
non-empty inline SourcePolicy.Rules check. Preserve the current
permission-denied response and allow requests only when neither policy source is
present.
- Around line 289-291: Replace the interpolated client-provided type in the
denial returned by the cache-type validation near entry.GetType() with fixed
text, and make the corresponding exporter-type denial near exp.GetType() fixed
as well. Remove the fmt import if it is no longer used, while preserving the
existing permission-denied code and policy keys.
- Around line 118-125: Update isRemoteContextRef to recognize scp-like Git
references in the form <user>@<host>:<path>, while preserving the existing URL
and prefix checks. Add the requested regexp import, use it for this host/path
pattern, and add a solve_test.go case covering bob@example.com:org/repo.git so
checkSolveFrontend enables AllowRemoteContext.
- Around line 174-189: Extend the SolveRequest validation flow around
checkSolveEntitlements, checkSolveFrontend, checkSolveCache,
checkSolveExporters, and checkSolveSourcePolicy to explicitly gate
FrontendInputs, SourcePolicySession, CompatibilityVersion, and ProxyNetwork
before forwarding. Add dedicated deny helpers/checks for these untrusted fields,
or document clear per-field forwarding justifications in the file-level comment
and cover them with tests.
- Around line 318-324: Update the image-export validation around exp.GetType()
to parse attrs["push"] with strconv.ParseBool and deny parse failures as a
policy/schema error; only continue registry validation when the parsed value is
true. Split attrs["name"] on commas and validate every image reference with
registryHostFromImageRef against solvePolicy.AllowedExporterRegistries, denying
if any reference is invalid or disallowed.

In `@app/internal/config/validate.go`:
- Around line 1597-1599: Update normalizeAllowedRegistryHost and the related
allowlist comparison to use registryHostFromImageRef semantics: lowercase
hostnames while preserving ports, such as localhost:5000. Apply the same
normalization to configured cache and exporter registry entries and referenced
image hosts so equivalent values like registry:5000 and Registry:5000 pass
validation.

---

Nitpick comments:
In `@app/internal/buildkitproxy/bridge_controlmediated_test.go`:
- Around line 115-137: The ref-cap test must verify that the denied second Solve
is not forwarded to the daemon. In the “ref cap exceeded denies the second
solve” subtest, add the existing daemon-called assertion after validating the
second response, or verify via tb.registry.OwnsRef that ref-2 was not
registered.

In `@app/internal/buildkitproxy/bridge.go`:
- Around line 400-403: The Solve path registers the admitted ref via PutRef
before forwarding, but failed transport forwarding leaves it retained in
refOwners. Update the flow around forwardWithBody to release that ref when
forwarding ends with a transport failure, while preserving the admission for
successfully accepted Solves and existing audit behavior.

In `@app/internal/buildkitproxy/framing_test.go`:
- Around line 140-153: Convert the new trailing-probe error case in
app/internal/buildkitproxy/framing_test.go#L140-L153 into a table entry within
TestReadUnaryGRPCMessage, adding a reader factory and expected error substring
while preserving the existing assertions. Also convert the new
policy-translation case in app/internal/config/buildkit_config_test.go#L119-L130
into the existing table, adding its build flags and expected SolvePolicy fields;
both sites require direct changes.

In `@app/internal/buildkitproxy/framing.go`:
- Around line 81-84: Update the framing read logic around the frame construction
to allocate only the complete frame buffer, read the payload directly into its
payload region after the gRPC header, and derive the payload slice from that
region. Remove the separate payload allocation and copy/reassembly block while
preserving the existing header handling and return values.

In `@app/internal/buildkitproxy/session_test.go`:
- Around line 261-312: Add a concurrent subtest to
TestSessionRegistryCloseReleasesRefs that starts N goroutines calling PutRef on
the same session while another goroutine calls Close(s.ID), wait for all
goroutines to finish, and assert refOwners has no entry for the session key. Use
synchronization to coordinate completion and exercise the Close/tryPutRef
ordering under the race detector.

In `@app/internal/buildkitproxy/session.go`:
- Around line 225-257: Remove the exported Session.PutRef and Session.OwnsRef
helpers and update any remaining callers to use SessionRegistry.PutRef and
SessionRegistry.OwnsRef, preserving MaxRefsPerSession and refOwners/index
bookkeeping. If tests require local-only behavior, rename them to unexported
putRefLocal and ownsRefLocal and keep their use confined to test support.

In `@app/internal/buildkitproxy/solve_test.go`:
- Around line 33-43: Add a FuzzEvaluateSolveRequest fuzz test that seeds
malformed and valid marshaled SolveRequest payloads, invokes
evaluateSolveRequest with an allowing policy, and asserts exactly one of the
returned request or denial is non-nil. Update mustMarshal to accept testing.TB
so it can be reused by both regular and fuzz tests.

In `@app/internal/config/buildkit_config_test.go`:
- Around line 327-359: Add a Fuzz... test alongside the existing Buildkit
configuration tests, targeting request_body.buildkit.control.solve parsing
through Load. Feed arbitrary cache type, cache registry, exporter type, and
exporter registry values via environment-backed configuration, assert Load never
panics, and verify invalid values fail validation while valid values are
accepted.
🪄 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: 4a25306f-8adb-4af8-80cd-677ac88422ab

📥 Commits

Reviewing files that changed from the base of the PR and between fd241bf and 9c2d5a5.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !CHANGELOG.md
📒 Files selected for processing (23)
  • app/internal/buildkitproxy/bridge.go
  • app/internal/buildkitproxy/bridge_controlmediated_test.go
  • app/internal/buildkitproxy/bridge_test.go
  • app/internal/buildkitproxy/framing.go
  • app/internal/buildkitproxy/framing_test.go
  • app/internal/buildkitproxy/grpcstatus.go
  • app/internal/buildkitproxy/limits.go
  • app/internal/buildkitproxy/mediator.go
  • app/internal/buildkitproxy/mediator_test.go
  • app/internal/buildkitproxy/policy.go
  • app/internal/buildkitproxy/protowalk.go
  • app/internal/buildkitproxy/protowalk_test.go
  • app/internal/buildkitproxy/session.go
  • app/internal/buildkitproxy/session_test.go
  • app/internal/buildkitproxy/solve.go
  • app/internal/buildkitproxy/solve_test.go
  • app/internal/cmd/rules.go
  • app/internal/cmd/serve.go
  • app/internal/config/buildkit_config_test.go
  • app/internal/config/buildkit_options.go
  • app/internal/config/config.go
  • app/internal/config/filter_options.go
  • app/internal/config/validate.go

Comment thread app/internal/buildkitproxy/bridge_controlmediated_test.go Outdated
Comment thread app/internal/buildkitproxy/bridge.go
Comment thread app/internal/buildkitproxy/bridge.go
Comment thread app/internal/buildkitproxy/session.go
Comment thread app/internal/buildkitproxy/solve.go
Comment thread app/internal/buildkitproxy/solve.go
Comment thread app/internal/buildkitproxy/solve.go
Comment thread app/internal/buildkitproxy/solve.go
Comment thread app/internal/buildkitproxy/solve.go
Comment thread app/internal/config/validate.go
…tion

- 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.
@scttbnsn
scttbnsn merged commit 1c19db6 into dev/v1.7 Aug 10, 2026
49 checks passed
@scttbnsn
scttbnsn deleted the feat/185-phase3-control-mediation branch August 11, 2026 03:49
scttbnsn added a commit that referenced this pull request Aug 12, 2026
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#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

* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#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).

* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#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.

* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#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 (#227)

* ✨ 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.

* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)

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.

* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)

Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.

* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)

Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.

* chore(commits): retire gitmoji for plain Conventional Commits (#235)

- 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

* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)

- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)

* fix(deps): update non-major (npm) (#229)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#230)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): roll release metadata forward to v1.7.0 (#236)

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

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
scttbnsn added a commit that referenced this pull request Aug 14, 2026
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#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

* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#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).

* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#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.

* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#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 (#227)

* ✨ 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.

* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)

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.

* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)

Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.

* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)

Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.

* chore(commits): retire gitmoji for plain Conventional Commits (#235)

- 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

* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)

- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)

* fix(deps): update non-major (npm) (#229)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#230)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): roll release metadata forward to v1.7.0 (#236)

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

* ci: make the soak harness and its gates actually honest

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.

* docs(readme): fix lying and dead badges

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.

* chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap 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.

* fix(bench): stop ReadMemStats-per-iteration from stalling the monthly 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 (#244)

* ci(fuzz): cover every fuzz target across tiers

* chore(deps): dedupe magic-string lock entry

* test(fuzz): reject stale workflow targets

* chore(deps): pin Renovate to dev/v1.7 (#245)

* ci(checks): remove emoji from required job names (#246)

* fix(build): align Go module root with repository (#248)

* 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 (#251)

* fix(deps): update nanoid to 3.3.18

* test(security): guard nanoid lock versions

* ci(review): expose bounded promotion review gaps (#250)

* ci(review): expose unreviewed promotion ranges

* fix(ci): mirror bridge eligibility

* ci(review): restore reviewed promotion changes (#252)

* ci(review): restore reviewed promotion changes

* fix(ci): report incomplete mutation results

* test(ci): strengthen incomplete-report contract

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
scttbnsn added a commit that referenced this pull request Aug 14, 2026
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#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

* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#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).

* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#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.

* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#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 (#227)

* ✨ 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.

* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)

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.

* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)

Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.

* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)

Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.

* chore(commits): retire gitmoji for plain Conventional Commits (#235)

- 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

* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)

- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)

* fix(deps): update non-major (npm) (#229)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#230)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): roll release metadata forward to v1.7.0 (#236)

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

* ci: make the soak harness and its gates actually honest

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.

* docs(readme): fix lying and dead badges

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.

* chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap 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.

* fix(bench): stop ReadMemStats-per-iteration from stalling the monthly 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 (#244)

* ci(fuzz): cover every fuzz target across tiers

* chore(deps): dedupe magic-string lock entry

* test(fuzz): reject stale workflow targets

* chore(deps): pin Renovate to dev/v1.7 (#245)

* ci(checks): remove emoji from required job names (#246)

* fix(build): align Go module root with repository (#248)

* 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 (#251)

* fix(deps): update nanoid to 3.3.18

* test(security): guard nanoid lock versions

* ci(review): expose bounded promotion review gaps (#250)

* ci(review): expose unreviewed promotion ranges

* fix(ci): mirror bridge eligibility

* ci(review): restore reviewed promotion changes (#252)

* ci(review): restore reviewed promotion changes

* fix(ci): report incomplete mutation results

* test(ci): strengthen incomplete-report contract

* feat(website): add cookieless PostHog analytics (#253)

* feat(website): add cookieless PostHog analytics

* fix(website): make route manifest checks race safe

* fix(website): preserve pre-init analytics events

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
scttbnsn added a commit that referenced this pull request Aug 14, 2026
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#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

* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#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).

* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#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.

* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#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 (#227)

* ✨ 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.

* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)

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.

* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)

Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.

* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)

Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.

* chore(commits): retire gitmoji for plain Conventional Commits (#235)

- 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

* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)

- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)

* fix(deps): update non-major (npm) (#229)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#230)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): roll release metadata forward to v1.7.0 (#236)

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

* ci: make the soak harness and its gates actually honest

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.

* docs(readme): fix lying and dead badges

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.

* chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap 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.

* fix(bench): stop ReadMemStats-per-iteration from stalling the monthly 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 (#244)

* ci(fuzz): cover every fuzz target across tiers

* chore(deps): dedupe magic-string lock entry

* test(fuzz): reject stale workflow targets

* chore(deps): pin Renovate to dev/v1.7 (#245)

* ci(checks): remove emoji from required job names (#246)

* fix(build): align Go module root with repository (#248)

* 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 (#251)

* fix(deps): update nanoid to 3.3.18

* test(security): guard nanoid lock versions

* ci(review): expose bounded promotion review gaps (#250)

* ci(review): expose unreviewed promotion ranges

* fix(ci): mirror bridge eligibility

* ci(review): restore reviewed promotion changes (#252)

* ci(review): restore reviewed promotion changes

* fix(ci): report incomplete mutation results

* test(ci): strengthen incomplete-report contract

* feat(website): add cookieless PostHog analytics (#253)

* feat(website): add cookieless PostHog analytics

* fix(website): make route manifest checks race safe

* fix(website): preserve pre-init analytics events

* fix(website): remove Go Report Card badge

Remove the Go Report Card badge and CSP allowance from the website.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
scttbnsn added a commit that referenced this pull request Aug 15, 2026
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#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

* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#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).

* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#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.

* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#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 (#227)

* ✨ 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.

* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)

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.

* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)

Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.

* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)

Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.

* chore(commits): retire gitmoji for plain Conventional Commits (#235)

- 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

* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)

- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)

* fix(deps): update non-major (npm) (#229)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#230)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): roll release metadata forward to v1.7.0 (#236)

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

* ci: make the soak harness and its gates actually honest

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.

* docs(readme): fix lying and dead badges

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.

* chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap 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.

* fix(bench): stop ReadMemStats-per-iteration from stalling the monthly 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 (#244)

* ci(fuzz): cover every fuzz target across tiers

* chore(deps): dedupe magic-string lock entry

* test(fuzz): reject stale workflow targets

* chore(deps): pin Renovate to dev/v1.7 (#245)

* ci(checks): remove emoji from required job names (#246)

* fix(build): align Go module root with repository (#248)

* 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 (#251)

* fix(deps): update nanoid to 3.3.18

* test(security): guard nanoid lock versions

* ci(review): expose bounded promotion review gaps (#250)

* ci(review): expose unreviewed promotion ranges

* fix(ci): mirror bridge eligibility

* ci(review): restore reviewed promotion changes (#252)

* ci(review): restore reviewed promotion changes

* fix(ci): report incomplete mutation results

* test(ci): strengthen incomplete-report contract

* feat(website): add cookieless PostHog analytics (#253)

* feat(website): add cookieless PostHog analytics

* fix(website): make route manifest checks race safe

* fix(website): preserve pre-init analytics events

* fix(website): remove Go Report Card badge

Remove the Go Report Card badge and CSP allowance from the website.

* ci: migrate to reusable workflows with temporary bridges (#258)

* ci: migrate to reusable workflows with temporary bridges

Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA
go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so
lint/test/release logic is shared with the rest of the org. Reusable check
runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which
doesn't match the plain context names the branch-protection ruleset
currently requires, so seven "legacy-*" bridge jobs mirror those exact
names, fail-closed on the reusable call's result, until the ruleset itself
is migrated separately to require the new reusable contexts.

CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs
javascript-typescript coverage the central workflow doesn't provide); so do
dependency-review and commit-message, which aren't part of the required
context set. The qlty OIDC coverage-upload step is dropped rather than
routed through go-ci's qlty job, matching portwing's precedent. Go Lint and
Go Test now additionally run on the weekly schedule trigger since go-ci has
no per-job schedule gate — an accepted strict superset of prior behavior.

* fix(ci): close a file-system race in the reusable-workflow contract test

assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync
and then re-opened it by path with readFileSync — a TOCTOU window where the
file on disk could change between the check and the read (CodeQL
js/file-system-race, alert #278). Open the script once and reuse that file
descriptor for both the executable-bit stat and the content read.

* fix(ci): address CodeRabbit findings on the reusable-workflow migration

- fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test,
  build-workspaces) with `github.event_name != 'schedule'`, matching
  node-ci's own schedule skip, so scheduled runs don't fail these
  always()-gated bridges closed
- test(ci): update the bridge contract test to require the schedule
  guard on the node-ci bridges while keeping the go-ci bridges on
  plain always()
- chore(lefthook): drop the obsolete `command -v goreleaser` skip guard
  from goreleaser-snapshot; the adapter runs goreleaser via `go run`
  with a pinned module and never needs the global binary
- fix(ci): tighten the quarantine-removal assertion in
  go-release-check.sh to match the exact generated Cask command
  instead of a loose substring

---------

Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
scttbnsn added a commit that referenced this pull request Aug 16, 2026
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#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

* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#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).

* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#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.

* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#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 (#227)

* ✨ 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.

* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)

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.

* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)

Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.

* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)

Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.

* chore(commits): retire gitmoji for plain Conventional Commits (#235)

- 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

* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)

- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)

* fix(deps): update non-major (npm) (#229)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#230)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): roll release metadata forward to v1.7.0 (#236)

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

* ci: make the soak harness and its gates actually honest

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.

* docs(readme): fix lying and dead badges

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.

* chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap 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.

* fix(bench): stop ReadMemStats-per-iteration from stalling the monthly 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 (#244)

* ci(fuzz): cover every fuzz target across tiers

* chore(deps): dedupe magic-string lock entry

* test(fuzz): reject stale workflow targets

* chore(deps): pin Renovate to dev/v1.7 (#245)

* ci(checks): remove emoji from required job names (#246)

* fix(build): align Go module root with repository (#248)

* 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 (#251)

* fix(deps): update nanoid to 3.3.18

* test(security): guard nanoid lock versions

* ci(review): expose bounded promotion review gaps (#250)

* ci(review): expose unreviewed promotion ranges

* fix(ci): mirror bridge eligibility

* ci(review): restore reviewed promotion changes (#252)

* ci(review): restore reviewed promotion changes

* fix(ci): report incomplete mutation results

* test(ci): strengthen incomplete-report contract

* feat(website): add cookieless PostHog analytics (#253)

* feat(website): add cookieless PostHog analytics

* fix(website): make route manifest checks race safe

* fix(website): preserve pre-init analytics events

* fix(website): remove Go Report Card badge

Remove the Go Report Card badge and CSP allowance from the website.

* ci: migrate to reusable workflows with temporary bridges (#258)

* ci: migrate to reusable workflows with temporary bridges

Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA
go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so
lint/test/release logic is shared with the rest of the org. Reusable check
runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which
doesn't match the plain context names the branch-protection ruleset
currently requires, so seven "legacy-*" bridge jobs mirror those exact
names, fail-closed on the reusable call's result, until the ruleset itself
is migrated separately to require the new reusable contexts.

CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs
javascript-typescript coverage the central workflow doesn't provide); so do
dependency-review and commit-message, which aren't part of the required
context set. The qlty OIDC coverage-upload step is dropped rather than
routed through go-ci's qlty job, matching portwing's precedent. Go Lint and
Go Test now additionally run on the weekly schedule trigger since go-ci has
no per-job schedule gate — an accepted strict superset of prior behavior.

* fix(ci): close a file-system race in the reusable-workflow contract test

assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync
and then re-opened it by path with readFileSync — a TOCTOU window where the
file on disk could change between the check and the read (CodeQL
js/file-system-race, alert #278). Open the script once and reuse that file
descriptor for both the executable-bit stat and the content read.

* fix(ci): address CodeRabbit findings on the reusable-workflow migration

- fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test,
  build-workspaces) with `github.event_name != 'schedule'`, matching
  node-ci's own schedule skip, so scheduled runs don't fail these
  always()-gated bridges closed
- test(ci): update the bridge contract test to require the schedule
  guard on the node-ci bridges while keeping the go-ci bridges on
  plain always()
- chore(lefthook): drop the obsolete `command -v goreleaser` skip guard
  from goreleaser-snapshot; the adapter runs goreleaser via `go run`
  with a pinned module and never needs the global binary
- fix(ci): tighten the quarantine-removal assertion in
  go-release-check.sh to match the exact generated Cask command
  instead of a loose substring

---------

Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>

* fix(website): forward $raw_user_agent and $host for cookieless ingestion (#261)

PostHog's cookieless server-hash ingestion reads $raw_user_agent and $host
straight off event properties and silently drops the event with a
cookieless_missing_user_agent/cookieless_missing_host warning if either is
absent. before_send's allowlist rebuild stripped both, so every event was
being dropped at ingestion. Require and forward both fields (and only those
two); $ip stays server-filled by the PostHog proxy. docs/ carries the same
change via the existing analytics-runtime sync.

* ci: remove temporary status bridges (#260)

* ci: remove temporary status bridges

The main branch-protection ruleset now requires the reusable "Go CI / ..."
and "Node CI / ..." contexts directly, so the seven legacy-* jobs that
mirrored the old plain context names are no longer needed. Updates the
contract tests to assert the bridges stay removed and pins the ruleset's
nine required contexts.

* test(ci): detect unquoted retired context names

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
scttbnsn added a commit that referenced this pull request Aug 16, 2026
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#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

* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#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).

* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#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.

* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#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 (#227)

* ✨ 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.

* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)

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.

* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)

Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.

* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)

Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.

* chore(commits): retire gitmoji for plain Conventional Commits (#235)

- 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

* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)

- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)

* fix(deps): update non-major (npm) (#229)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#230)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): roll release metadata forward to v1.7.0 (#236)

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

* ci: make the soak harness and its gates actually honest

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.

* docs(readme): fix lying and dead badges

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.

* chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap 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.

* fix(bench): stop ReadMemStats-per-iteration from stalling the monthly 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 (#244)

* ci(fuzz): cover every fuzz target across tiers

* chore(deps): dedupe magic-string lock entry

* test(fuzz): reject stale workflow targets

* chore(deps): pin Renovate to dev/v1.7 (#245)

* ci(checks): remove emoji from required job names (#246)

* fix(build): align Go module root with repository (#248)

* 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 (#251)

* fix(deps): update nanoid to 3.3.18

* test(security): guard nanoid lock versions

* ci(review): expose bounded promotion review gaps (#250)

* ci(review): expose unreviewed promotion ranges

* fix(ci): mirror bridge eligibility

* ci(review): restore reviewed promotion changes (#252)

* ci(review): restore reviewed promotion changes

* fix(ci): report incomplete mutation results

* test(ci): strengthen incomplete-report contract

* feat(website): add cookieless PostHog analytics (#253)

* feat(website): add cookieless PostHog analytics

* fix(website): make route manifest checks race safe

* fix(website): preserve pre-init analytics events

* fix(website): remove Go Report Card badge

Remove the Go Report Card badge and CSP allowance from the website.

* ci: migrate to reusable workflows with temporary bridges (#258)

* ci: migrate to reusable workflows with temporary bridges

Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA
go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so
lint/test/release logic is shared with the rest of the org. Reusable check
runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which
doesn't match the plain context names the branch-protection ruleset
currently requires, so seven "legacy-*" bridge jobs mirror those exact
names, fail-closed on the reusable call's result, until the ruleset itself
is migrated separately to require the new reusable contexts.

CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs
javascript-typescript coverage the central workflow doesn't provide); so do
dependency-review and commit-message, which aren't part of the required
context set. The qlty OIDC coverage-upload step is dropped rather than
routed through go-ci's qlty job, matching portwing's precedent. Go Lint and
Go Test now additionally run on the weekly schedule trigger since go-ci has
no per-job schedule gate — an accepted strict superset of prior behavior.

* fix(ci): close a file-system race in the reusable-workflow contract test

assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync
and then re-opened it by path with readFileSync — a TOCTOU window where the
file on disk could change between the check and the read (CodeQL
js/file-system-race, alert #278). Open the script once and reuse that file
descriptor for both the executable-bit stat and the content read.

* fix(ci): address CodeRabbit findings on the reusable-workflow migration

- fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test,
  build-workspaces) with `github.event_name != 'schedule'`, matching
  node-ci's own schedule skip, so scheduled runs don't fail these
  always()-gated bridges closed
- test(ci): update the bridge contract test to require the schedule
  guard on the node-ci bridges while keeping the go-ci bridges on
  plain always()
- chore(lefthook): drop the obsolete `command -v goreleaser` skip guard
  from goreleaser-snapshot; the adapter runs goreleaser via `go run`
  with a pinned module and never needs the global binary
- fix(ci): tighten the quarantine-removal assertion in
  go-release-check.sh to match the exact generated Cask command
  instead of a loose substring

---------

Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>

* fix(website): forward $raw_user_agent and $host for cookieless ingestion (#261)

PostHog's cookieless server-hash ingestion reads $raw_user_agent and $host
straight off event properties and silently drops the event with a
cookieless_missing_user_agent/cookieless_missing_host warning if either is
absent. before_send's allowlist rebuild stripped both, so every event was
being dropped at ingestion. Require and forward both fields (and only those
two); $ip stays server-filled by the PostHog proxy. docs/ carries the same
change via the existing analytics-runtime sync.

* ci: remove temporary status bridges (#260)

* ci: remove temporary status bridges

The main branch-protection ruleset now requires the reusable "Go CI / ..."
and "Node CI / ..." contexts directly, so the seven legacy-* jobs that
mirrored the old plain context names are no longer needed. Updates the
contract tests to assert the bridges stay removed and pins the ruleset's
nine required contexts.

* test(ci): detect unquoted retired context names

* docs(readme): add warpchart growth chart (#263)

* docs(readme): add warpchart growth chart

* docs(readme): keep warpchart chart inside centering container

- docs(readme): inline HTML anchor/img so the chart stays in the centered div
- docs(changelog): record the README addition

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
scttbnsn added a commit that referenced this pull request Aug 16, 2026
… main (#268)

* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#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

* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#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).

* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#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.

* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#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 (#227)

* ✨ 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.

* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)

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.

* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)

Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.

* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)

Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.

* chore(commits): retire gitmoji for plain Conventional Commits (#235)

- 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

* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)

- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)

* fix(deps): update non-major (npm) (#229)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#230)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): roll release metadata forward to v1.7.0 (#236)

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

* ci: make the soak harness and its gates actually honest

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.

* docs(readme): fix lying and dead badges

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.

* chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap 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.

* fix(bench): stop ReadMemStats-per-iteration from stalling the monthly 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 (#244)

* ci(fuzz): cover every fuzz target across tiers

* chore(deps): dedupe magic-string lock entry

* test(fuzz): reject stale workflow targets

* chore(deps): pin Renovate to dev/v1.7 (#245)

* ci(checks): remove emoji from required job names (#246)

* fix(build): align Go module root with repository (#248)

* 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 (#251)

* fix(deps): update nanoid to 3.3.18

* test(security): guard nanoid lock versions

* ci(review): expose bounded promotion review gaps (#250)

* ci(review): expose unreviewed promotion ranges

* fix(ci): mirror bridge eligibility

* ci(review): restore reviewed promotion changes (#252)

* ci(review): restore reviewed promotion changes

* fix(ci): report incomplete mutation results

* test(ci): strengthen incomplete-report contract

* feat(website): add cookieless PostHog analytics (#253)

* feat(website): add cookieless PostHog analytics

* fix(website): make route manifest checks race safe

* fix(website): preserve pre-init analytics events

* fix(website): remove Go Report Card badge

Remove the Go Report Card badge and CSP allowance from the website.

* ci: migrate to reusable workflows with temporary bridges (#258)

* ci: migrate to reusable workflows with temporary bridges

Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA
go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so
lint/test/release logic is shared with the rest of the org. Reusable check
runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which
doesn't match the plain context names the branch-protection ruleset
currently requires, so seven "legacy-*" bridge jobs mirror those exact
names, fail-closed on the reusable call's result, until the ruleset itself
is migrated separately to require the new reusable contexts.

CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs
javascript-typescript coverage the central workflow doesn't provide); so do
dependency-review and commit-message, which aren't part of the required
context set. The qlty OIDC coverage-upload step is dropped rather than
routed through go-ci's qlty job, matching portwing's precedent. Go Lint and
Go Test now additionally run on the weekly schedule trigger since go-ci has
no per-job schedule gate — an accepted strict superset of prior behavior.

* fix(ci): close a file-system race in the reusable-workflow contract test

assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync
and then re-opened it by path with readFileSync — a TOCTOU window where the
file on disk could change between the check and the read (CodeQL
js/file-system-race, alert #278). Open the script once and reuse that file
descriptor for both the executable-bit stat and the content read.

* fix(ci): address CodeRabbit findings on the reusable-workflow migration

- fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test,
  build-workspaces) with `github.event_name != 'schedule'`, matching
  node-ci's own schedule skip, so scheduled runs don't fail these
  always()-gated bridges closed
- test(ci): update the bridge contract test to require the schedule
  guard on the node-ci bridges while keeping the go-ci bridges on
  plain always()
- chore(lefthook): drop the obsolete `command -v goreleaser` skip guard
  from goreleaser-snapshot; the adapter runs goreleaser via `go run`
  with a pinned module and never needs the global binary
- fix(ci): tighten the quarantine-removal assertion in
  go-release-check.sh to match the exact generated Cask command
  instead of a loose substring

---------

Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>

* fix(website): forward $raw_user_agent and $host for cookieless ingestion (#261)

PostHog's cookieless server-hash ingestion reads $raw_user_agent and $host
straight off event properties and silently drops the event with a
cookieless_missing_user_agent/cookieless_missing_host warning if either is
absent. before_send's allowlist rebuild stripped both, so every event was
being dropped at ingestion. Require and forward both fields (and only those
two); $ip stays server-filled by the PostHog proxy. docs/ carries the same
change via the existing analytics-runtime sync.

* ci: remove temporary status bridges (#260)

* ci: remove temporary status bridges

The main branch-protection ruleset now requires the reusable "Go CI / ..."
and "Node CI / ..." contexts directly, so the seven legacy-* jobs that
mirrored the old plain context names are no longer needed. Updates the
contract tests to assert the bridges stay removed and pins the ruleset's
nine required contexts.

* test(ci): detect unquoted retired context names

* docs(readme): add warpchart growth chart (#263)

* docs(readme): add warpchart growth chart

* docs(readme): keep warpchart chart inside centering container

- docs(readme): inline HTML anchor/img so the chart stays in the centered div
- docs(changelog): record the README addition

* fix(deps): update golang.org/x/mod to v0.40.0 (#265)

Clears OpenSSF Scorecard alert #8's two actionable findings, GO-2026-6179
and GO-2026-6180 (sumdb transparency-log verification bypasses), fixed in
x/mod v0.40.0. The third finding, GO-2026-5932 (unmaintained
x/crypto/openpgp), has no fixed version and is already a documented,
narrowly-scoped exception in .grype.yaml. govulncheck reports none of the
three as reachable; this is a manifest-level cleanup.

* docs(changelog): backfill unreleased entries for CI migration, analytics, and bench fix (#266)

Three merged, changelog-worthy changes were missing from [Unreleased]:
the reusable-workflow CI migration (#258/#260), cookieless PostHog
analytics with its ingestion-property fix (#253/#261), and the monthly
benchmark ReadMemStats stall fix (#243). Surfaced by a repo audit;
CLAUDE.md wants changelog updates atomic with each change, so this
closes the gap in one pass.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
scttbnsn added a commit that referenced this pull request Aug 19, 2026
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#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

* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#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).

* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#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.

* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#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 (#227)

* ✨ 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.

* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)

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.

* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)

Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.

* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)

Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.

* chore(commits): retire gitmoji for plain Conventional Commits (#235)

- 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

* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)

- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)

* fix(deps): update non-major (npm) (#229)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#230)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): roll release metadata forward to v1.7.0 (#236)

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

* ci: make the soak harness and its gates actually honest

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.

* docs(readme): fix lying and dead badges

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.

* chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap 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.

* fix(bench): stop ReadMemStats-per-iteration from stalling the monthly 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 (#244)

* ci(fuzz): cover every fuzz target across tiers

* chore(deps): dedupe magic-string lock entry

* test(fuzz): reject stale workflow targets

* chore(deps): pin Renovate to dev/v1.7 (#245)

* ci(checks): remove emoji from required job names (#246)

* fix(build): align Go module root with repository (#248)

* 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 (#251)

* fix(deps): update nanoid to 3.3.18

* test(security): guard nanoid lock versions

* ci(review): expose bounded promotion review gaps (#250)

* ci(review): expose unreviewed promotion ranges

* fix(ci): mirror bridge eligibility

* ci(review): restore reviewed promotion changes (#252)

* ci(review): restore reviewed promotion changes

* fix(ci): report incomplete mutation results

* test(ci): strengthen incomplete-report contract

* feat(website): add cookieless PostHog analytics (#253)

* feat(website): add cookieless PostHog analytics

* fix(website): make route manifest checks race safe

* fix(website): preserve pre-init analytics events

* fix(website): remove Go Report Card badge

Remove the Go Report Card badge and CSP allowance from the website.

* ci: migrate to reusable workflows with temporary bridges (#258)

* ci: migrate to reusable workflows with temporary bridges

Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA
go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so
lint/test/release logic is shared with the rest of the org. Reusable check
runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which
doesn't match the plain context names the branch-protection ruleset
currently requires, so seven "legacy-*" bridge jobs mirror those exact
names, fail-closed on the reusable call's result, until the ruleset itself
is migrated separately to require the new reusable contexts.

CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs
javascript-typescript coverage the central workflow doesn't provide); so do
dependency-review and commit-message, which aren't part of the required
context set. The qlty OIDC coverage-upload step is dropped rather than
routed through go-ci's qlty job, matching portwing's precedent. Go Lint and
Go Test now additionally run on the weekly schedule trigger since go-ci has
no per-job schedule gate — an accepted strict superset of prior behavior.

* fix(ci): close a file-system race in the reusable-workflow contract test

assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync
and then re-opened it by path with readFileSync — a TOCTOU window where the
file on disk could change between the check and the read (CodeQL
js/file-system-race, alert #278). Open the script once and reuse that file
descriptor for both the executable-bit stat and the content read.

* fix(ci): address CodeRabbit findings on the reusable-workflow migration

- fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test,
  build-workspaces) with `github.event_name != 'schedule'`, matching
  node-ci's own schedule skip, so scheduled runs don't fail these
  always()-gated bridges closed
- test(ci): update the bridge contract test to require the schedule
  guard on the node-ci bridges while keeping the go-ci bridges on
  plain always()
- chore(lefthook): drop the obsolete `command -v goreleaser` skip guard
  from goreleaser-snapshot; the adapter runs goreleaser via `go run`
  with a pinned module and never needs the global binary
- fix(ci): tighten the quarantine-removal assertion in
  go-release-check.sh to match the exact generated Cask command
  instead of a loose substring

---------

Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>

* fix(website): forward $raw_user_agent and $host for cookieless ingestion (#261)

PostHog's cookieless server-hash ingestion reads $raw_user_agent and $host
straight off event properties and silently drops the event with a
cookieless_missing_user_agent/cookieless_missing_host warning if either is
absent. before_send's allowlist rebuild stripped both, so every event was
being dropped at ingestion. Require and forward both fields (and only those
two); $ip stays server-filled by the PostHog proxy. docs/ carries the same
change via the existing analytics-runtime sync.

* ci: remove temporary status bridges (#260)

* ci: remove temporary status bridges

The main branch-protection ruleset now requires the reusable "Go CI / ..."
and "Node CI / ..." contexts directly, so the seven legacy-* jobs that
mirrored the old plain context names are no longer needed. Updates the
contract tests to assert the bridges stay removed and pins the ruleset's
nine required contexts.

* test(ci): detect unquoted retired context names

* docs(readme): add warpchart growth chart (#263)

* docs(readme): add warpchart growth chart

* docs(readme): keep warpchart chart inside centering container

- docs(readme): inline HTML anchor/img so the chart stays in the centered div
- docs(changelog): record the README addition

* fix(deps): update golang.org/x/mod to v0.40.0 (#265)

Clears OpenSSF Scorecard alert #8's two actionable findings, GO-2026-6179
and GO-2026-6180 (sumdb transparency-log verification bypasses), fixed in
x/mod v0.40.0. The third finding, GO-2026-5932 (unmaintained
x/crypto/openpgp), has no fixed version and is already a documented,
narrowly-scoped exception in .grype.yaml. govulncheck reports none of the
three as reachable; this is a manifest-level cleanup.

* docs(changelog): backfill unreleased entries for CI migration, analytics, and bench fix (#266)

Three merged, changelog-worthy changes were missing from [Unreleased]:
the reusable-workflow CI migration (#258/#260), cookieless PostHog
analytics with its ingestion-property fix (#253/#261), and the monthly
benchmark ReadMemStats stall fix (#243). Surfaced by a repo audit;
CLAUDE.md wants changelog updates atomic with each change, so this
closes the gap in one pass.

* ci(security): close PR-gate gaps for dependency-review, gitleaks, actionlint, and Grype (#277)

* ci(security): close PR-gate gaps for dependency-review, gitleaks, actionlint, and Grype

The Dependency Graph repo setting is now on, so the dependency-review job's
hard `if: false` guard comes out and it actually runs on every PR again. A
new Gitleaks job adds the checksum-pinned CLI secrets scan sockguard was
missing (careerrat#72's pattern), with a repo-scoped .gitleaks.toml
allowlisting three verified-synthetic test-fixture strings. A new Actionlint
job lints this repo's own workflows, which zizmor alone didn't cover. Grype
image scanning moves into ci-verify.yml's required Docker Build job so it
gates the image the PR pipeline actually builds, and security-grype.yml's
grype-image job stays scheduled/manual-only to avoid a double scan.

Refs: sockguard#271

* fix(ci): resolve pre-existing shellcheck findings the new actionlint gate surfaces

The Actionlint job added in the prior commit lints every workflow file, not
just the ones it touched, and shellcheck flagged five real issues elsewhere:
an unquoted go install target, two multi-append blocks that collapse into a
single grouped redirect, an ls-based count replaced with a nullglob array,
and one sudo-before-redirect false alarm (the redirect target is /tmp, which
never needed root) resolved with an annotated inline suppression instead of
rerouting the log through `sudo tee`.

* ci: wire knip, shellcheck, and zizmor as real lint gates (#276)

* ci: wire knip, shellcheck, and zizmor as real lint gates

Closes the drift the linters.md registry audit flagged: knip only ran
in lefthook pre-push, never in CI; shellcheck and markdownlint existed
only as CodeRabbit review plugins, never as lefthook/CI gates. A
linter that only runs in one place is drift, not defense in depth.

- node-lint.sh now runs `npx knip` alongside biome, so it rides the
  existing required "Node CI / Biome Lint" check — the reusable
  node-ci.yml workflow has no dedicated knip input.
- Add scripts/ci/shellcheck.sh, which lints every tracked *.sh file
  and hard-fails locally with an install hint if shellcheck is
  missing (no silent skip). Wire it into lefthook pre-push and a new
  "Shellcheck" job in ci-verify.yml.
- Fix the one shellcheck finding it surfaced: unused loop variables in
  benchmarks/run.sh (SC2034), renamed to `_`.
- The lefthook zizmor step now hard-fails with an install hint instead
  of silently skipping when the binary is absent, matching the
  blocking-local-gate pattern used elsewhere in the org (e.g.
  drydock's scan-secrets.sh) for tools CI enforces unconditionally.

* fix(ci): cover shellcheck's pre-0.10 SC2317 code in run-matrix.sh disables

* test(ci): cover shellcheck.sh in the fixed-script contract

CodeRabbit review: the runner is mode 100755 but assertFixedScripts() never
inspected it, so its markers went unchecked. Also corrects the lefthook
comment, which named the node-ci lint job instead of the standalone
shellcheck job.

* ci(review): wire Greptile as a label-gated second-opinion reviewer (#281)

* ci(review): wire Greptile as a label-gated second-opinion reviewer

Adds a thin `.github/workflows/greptile.yml` caller that summons
CodesWhat/.github's reusable greptile-summon.yml only when a PR carries
the `second-opinion` label, and a matching `.coderabbit.yaml`
`labeling_instructions` entry so CodeRabbit can apply that label itself
on security-sensitive diffs, large refactors, or a tiebreaker with the
author's intent. AGENTS.md now explains what the dormant `greptile.json`
config is for, so it doesn't read as dead weight.

Closes #269

* fix(review): enable suggested_labels so auto_apply_labels can fire

suggested_labels: false disables label suggestions entirely, including
labeling_instructions, so CodeRabbit would never apply second-opinion and
the greptile.yml caller would never trigger.

* docs(readme): migrate to the codified house README shape (#278)

* docs(readme): migrate to the codified house README shape

Reorders the header stack (logo/grabber/badge wall/hr/release note
before Contents), regroups the badge wall into three ordered rows
(identity, quality/security, social proof), adds the qlty
maintainability badge, drops all decorative emoji from headings, the
Contents list, and the Features table's icon column, reconciles the
previously split Community sections into one with a single
Issues/Discussions/Discord routing sentence, and fixes a stale
anchor link in the tri-tool example README.

Closes #270

* docs(readme): disambiguate the two Documentation contents entries

The external docs-site link and the in-page section anchor both read
Documentation in the same Contents list. The external one is now Docs
site; the anchor keeps the label that matches its section heading.

* build(deps): dedupe nested magic-string to 1.2.1

The lockfile-dedupe pre-push gate resolves against the live registry, so
the newly published 1.2.1 made it fail on every branch until the nested
copy under fumadocs-mdx is realigned.

* build(release): add GoReleaser SBOMs and qualify SLSA claim to Build L2 (#279)

* build(release): add GoReleaser SBOMs and qualify SLSA claim to Build L2

- build(release): add a sboms: block to app/.goreleaser.yaml (syft,
  CycloneDX per archive), mirroring portwing's existing pattern
- ci(release): install syft via anchore/sbom-action/download-syft in
  release-from-tag.yml ahead of the GoReleaser run; skip sbom cataloging
  in the shared snapshot/config-check script when syft isn't on PATH so
  the local pre-push hook and the CI "GoReleaser Config" gate keep
  passing without needing syft, without hiding a real failure
- docs(releasing): qualify "SLSA attestation"/"SLSA build provenance" to
  "SLSA Build L2" per the house provenance standard, and note that v*
  tags are protected against deletion/rewrite by a repo tag ruleset, so
  the Cosign signatures and SLSA Build L2 attestations are the signature
  of record instead of a git-tag signature
- docs(changelog): record the SBOM and SLSA-qualification change under
  [Unreleased] / Security

* build(release): restore syft --enrich all in the SBOM args

Setting args: replaces GoReleaser's entire default slice, so switching the
output to cyclonedx dropped --enrich all and shipped SBOMs without
dependency license data.

* build(deps): dedupe nested magic-string to 1.2.1

Same registry-driven lockfile-dedupe gate failure as #278; realigns the
nested copy under fumadocs-mdx so pushes pass.

* fix: replace the retired star-history.com chart with Warpchart (#283)

* docs(readme): drop the broken star-history.com chart embed

GitHub restricted stargazer API access, so the star-history.com SVG now
serves an error card reading 'GitHub restricted access to star data'
directly above the working Warpchart embed. Warpchart accumulates its own
time series and is unaffected, so it replaces rather than duplicates the
old chart. Also gives the section the house h2 heading it lacked.

* build(deps): dedupe nested magic-string to 1.2.1

* fix(website): replace the retired star-history.com chart with Warpchart

GitHub restricted the stargazer API that star-history.com replays per
request, so its SVG returns an error card at HTTP 200 — the landing page
was serving that to every visitor. Warpchart accumulates its own time
series and honours theme=dark, so the light/dark swap is preserved. The
CSP img-src allowlist moves with it, in lockstep with the pinned test.

* docs(changelog): reconcile the stale Warpchart entry with the star-history removal

The #264 entry said the Warpchart chart sits alongside the existing
chart. This PR removes that chart, so the clause is now false and
contradicts the entry directly above it. Drop the clause and add the
issue citation the entry was missing.

* fix(deps): update non-major (gomod) (#272)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#274)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update non-major (npm) (#273)

* fix(deps): update non-major (npm)

* test(website): pin posthog-js assertion to 1.417.1

The npm dependency bump upgraded posthog-js from 1.417.0 to 1.417.1
but the analytics integration test still asserted the old exact
version, failing CI.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>

* ci(security): pin runners, comment reusable pins, and start harden-runner egress block (#282)

* ci(security): pin runners, comment reusable pins, and start harden-runner egress block

Pins all 33 runs-on: ubuntu-latest references to ubuntu-24.04 across every
workflow so a GitHub-side default-image bump can never silently change CI
without a diff to review, and adds freeze-date comments to the two frozen
CodesWhat/.github reusable-workflow SHA pins in ci-verify.yml (that repo
tags no releases, so a SHA is the only anchor). Updates four stale
"ubuntu-latest" prose comments in quality-integration-podman.yml to match.

Starts stage 1 of a 3-stage harden-runner egress rollout, modeled on
drydock's own live posture: 15 jobs across 8 workflow files with a
deterministic, single-vendor egress footprint move from egress-policy:
audit to block with an explicit allowed-endpoints list and
disable-sudo: true, plus ci-verify.yml's go-ci caller now passes
workflow-security-egress-policy: block through to the reusable workflow's
zizmor job. Jobs with variable or multi-registry egress (Docker builds
against Chainguard's CDN, dockerd/podman integration pulling from Docker
Hub, the tri-tool conformance matrix, CodeQL's query-bundle CDN) stay
audit pending real telemetry. DAST (testssl) and OpenSSF Scorecard stay
audit permanently and now say so in-file. Release jobs are untouched
until two audited release-candidate cuts have run clean.

Bumps scripts/required-ci-contexts.test.mjs's REQUIRED_CONTEXTS from 9 to
13, adding Dependency Review, Gitleaks, Actionlint, and Shellcheck, which
already exist as PR-gating jobs in ci-verify.yml but were never added to
the pinned required-context list. Ships ahead of the branch-protection
ruleset update that will actually require them.

* ci(security): keep gosec on audit and allow go.dev for setup-go

Two fixes to the stage-1 egress rollout, both found by running the jobs
rather than reading them.

gosec back to audit: it runs as a container action, and block mode drops
container egress silently instead of rejecting it, so every module lookup
burns a full TCP timeout. The job takes ~2 minutes on every other branch
and exceeded timeout-minutes: 15 twice here, with no denied domain logged
to explain it. Same reasoning already applied to Docker Build and
grype-image, which stay on audit for the container-pull footprint.

govulncheck gains go.dev: setup-go checks the actions/go-versions manifest
first and only falls back to go.dev/dl. A warm runner never hits it, so
the gap stays invisible until a cache miss takes the job down.

* build(deps): tidy go.mod after the gomod bump

Renovate's non-major gomod update (#272) landed without a go mod tidy, so
dev/v1.7 carries go-jose/v4, letsencrypt/boulder and titanous/rocacheck as
indirect requirements nothing resolves to anymore. The goreleaser release
hook runs go mod tidy anyway, so this only decides whether the tree is
tidy before the tag or during it.

* ci(security): add ZAP baseline passive DAST scan for getsockguard.com (#280)

* ci(security): add ZAP baseline passive DAST scan for getsockguard.com

getsockguard.com and its /docs subpath are static surfaces (no auth,
no forms, no server-side handlers), so the DAST tier here is a ZAP
baseline passive scan (headers, CSP, cookie flags) against both
deployed targets on a weekly schedule plus workflow_dispatch. No
pull_request trigger: DAST scans what's already deployed, so it's
scheduled/advisory and never gates a PR. allow_issue_writing is off
so a red run stays a triage signal in the Actions tab instead of an
auto-filed issue, keeping the job's permissions at contents: read.
.zap/rules.tsv suppresses one informational false positive (rule
10049) that the site's immutable, content-hashed _next/static assets
reliably trigger. harden-runner runs egress-policy: audit as a
permanent exception, since the job scans a live Vercel-fronted
third-party endpoint with no stable destination set to allowlist.

* ci(security): withhold the repo token from the ZAP action

The action's token input defaults to ${{ github.token }} and exists only
to file findings as a GitHub issue, which allow_issue_writing: false
already turns off. Passing it empty means this third-party JS action
never receives a repository token at all, instead of relying on the
job's contents:read scope to bound what it could do with one.

* build(deps): realign the lockfile after an upstream magic-string publish

The lockfile-dedupe pre-push gate runs npm dedupe --dry-run against the
live registry, so magic-string 1.2.1 shipping upstream turns every branch
red with no local change. Nothing here depends on it directly; this just
resettles the transitive tree under fumadocs-mdx.

* chore(deps): lock file maintenance (#275)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>

* ci(gating): require the security scanners and drop emoji from job names (#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:.

* chore(release): prepare the v1.7.1 changelog and roadmap (#286)

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.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
scttbnsn added a commit that referenced this pull request Aug 19, 2026
…gning fixes to main (#293)

* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#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

* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#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).

* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#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.

* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#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 (#227)

* ✨ 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.

* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)

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.

* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)

Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.

* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)

Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.

* chore(commits): retire gitmoji for plain Conventional Commits (#235)

- 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

* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)

- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)

* fix(deps): update non-major (npm) (#229)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#230)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): roll release metadata forward to v1.7.0 (#236)

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

* ci: make the soak harness and its gates actually honest

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.

* docs(readme): fix lying and dead badges

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.

* chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap 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.

* fix(bench): stop ReadMemStats-per-iteration from stalling the monthly 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 (#244)

* ci(fuzz): cover every fuzz target across tiers

* chore(deps): dedupe magic-string lock entry

* test(fuzz): reject stale workflow targets

* chore(deps): pin Renovate to dev/v1.7 (#245)

* ci(checks): remove emoji from required job names (#246)

* fix(build): align Go module root with repository (#248)

* 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 (#251)

* fix(deps): update nanoid to 3.3.18

* test(security): guard nanoid lock versions

* ci(review): expose bounded promotion review gaps (#250)

* ci(review): expose unreviewed promotion ranges

* fix(ci): mirror bridge eligibility

* ci(review): restore reviewed promotion changes (#252)

* ci(review): restore reviewed promotion changes

* fix(ci): report incomplete mutation results

* test(ci): strengthen incomplete-report contract

* feat(website): add cookieless PostHog analytics (#253)

* feat(website): add cookieless PostHog analytics

* fix(website): make route manifest checks race safe

* fix(website): preserve pre-init analytics events

* fix(website): remove Go Report Card badge

Remove the Go Report Card badge and CSP allowance from the website.

* ci: migrate to reusable workflows with temporary bridges (#258)

* ci: migrate to reusable workflows with temporary bridges

Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA
go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so
lint/test/release logic is shared with the rest of the org. Reusable check
runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which
doesn't match the plain context names the branch-protection ruleset
currently requires, so seven "legacy-*" bridge jobs mirror those exact
names, fail-closed on the reusable call's result, until the ruleset itself
is migrated separately to require the new reusable contexts.

CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs
javascript-typescript coverage the central workflow doesn't provide); so do
dependency-review and commit-message, which aren't part of the required
context set. The qlty OIDC coverage-upload step is dropped rather than
routed through go-ci's qlty job, matching portwing's precedent. Go Lint and
Go Test now additionally run on the weekly schedule trigger since go-ci has
no per-job schedule gate — an accepted strict superset of prior behavior.

* fix(ci): close a file-system race in the reusable-workflow contract test

assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync
and then re-opened it by path with readFileSync — a TOCTOU window where the
file on disk could change between the check and the read (CodeQL
js/file-system-race, alert #278). Open the script once and reuse that file
descriptor for both the executable-bit stat and the content read.

* fix(ci): address CodeRabbit findings on the reusable-workflow migration

- fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test,
  build-workspaces) with `github.event_name != 'schedule'`, matching
  node-ci's own schedule skip, so scheduled runs don't fail these
  always()-gated bridges closed
- test(ci): update the bridge contract test to require the schedule
  guard on the node-ci bridges while keeping the go-ci bridges on
  plain always()
- chore(lefthook): drop the obsolete `command -v goreleaser` skip guard
  from goreleaser-snapshot; the adapter runs goreleaser via `go run`
  with a pinned module and never needs the global binary
- fix(ci): tighten the quarantine-removal assertion in
  go-release-check.sh to match the exact generated Cask command
  instead of a loose substring

---------

Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>

* fix(website): forward $raw_user_agent and $host for cookieless ingestion (#261)

PostHog's cookieless server-hash ingestion reads $raw_user_agent and $host
straight off event properties and silently drops the event with a
cookieless_missing_user_agent/cookieless_missing_host warning if either is
absent. before_send's allowlist rebuild stripped both, so every event was
being dropped at ingestion. Require and forward both fields (and only those
two); $ip stays server-filled by the PostHog proxy. docs/ carries the same
change via the existing analytics-runtime sync.

* ci: remove temporary status bridges (#260)

* ci: remove temporary status bridges

The main branch-protection ruleset now requires the reusable "Go CI / ..."
and "Node CI / ..." contexts directly, so the seven legacy-* jobs that
mirrored the old plain context names are no longer needed. Updates the
contract tests to assert the bridges stay removed and pins the ruleset's
nine required contexts.

* test(ci): detect unquoted retired context names

* docs(readme): add warpchart growth chart (#263)

* docs(readme): add warpchart growth chart

* docs(readme): keep warpchart chart inside centering container

- docs(readme): inline HTML anchor/img so the chart stays in the centered div
- docs(changelog): record the README addition

* fix(deps): update golang.org/x/mod to v0.40.0 (#265)

Clears OpenSSF Scorecard alert #8's two actionable findings, GO-2026-6179
and GO-2026-6180 (sumdb transparency-log verification bypasses), fixed in
x/mod v0.40.0. The third finding, GO-2026-5932 (unmaintained
x/crypto/openpgp), has no fixed version and is already a documented,
narrowly-scoped exception in .grype.yaml. govulncheck reports none of the
three as reachable; this is a manifest-level cleanup.

* docs(changelog): backfill unreleased entries for CI migration, analytics, and bench fix (#266)

Three merged, changelog-worthy changes were missing from [Unreleased]:
the reusable-workflow CI migration (#258/#260), cookieless PostHog
analytics with its ingestion-property fix (#253/#261), and the monthly
benchmark ReadMemStats stall fix (#243). Surfaced by a repo audit;
CLAUDE.md wants changelog updates atomic with each change, so this
closes the gap in one pass.

* ci(security): close PR-gate gaps for dependency-review, gitleaks, actionlint, and Grype (#277)

* ci(security): close PR-gate gaps for dependency-review, gitleaks, actionlint, and Grype

The Dependency Graph repo setting is now on, so the dependency-review job's
hard `if: false` guard comes out and it actually runs on every PR again. A
new Gitleaks job adds the checksum-pinned CLI secrets scan sockguard was
missing (careerrat#72's pattern), with a repo-scoped .gitleaks.toml
allowlisting three verified-synthetic test-fixture strings. A new Actionlint
job lints this repo's own workflows, which zizmor alone didn't cover. Grype
image scanning moves into ci-verify.yml's required Docker Build job so it
gates the image the PR pipeline actually builds, and security-grype.yml's
grype-image job stays scheduled/manual-only to avoid a double scan.

Refs: sockguard#271

* fix(ci): resolve pre-existing shellcheck findings the new actionlint gate surfaces

The Actionlint job added in the prior commit lints every workflow file, not
just the ones it touched, and shellcheck flagged five real issues elsewhere:
an unquoted go install target, two multi-append blocks that collapse into a
single grouped redirect, an ls-based count replaced with a nullglob array,
and one sudo-before-redirect false alarm (the redirect target is /tmp, which
never needed root) resolved with an annotated inline suppression instead of
rerouting the log through `sudo tee`.

* ci: wire knip, shellcheck, and zizmor as real lint gates (#276)

* ci: wire knip, shellcheck, and zizmor as real lint gates

Closes the drift the linters.md registry audit flagged: knip only ran
in lefthook pre-push, never in CI; shellcheck and markdownlint existed
only as CodeRabbit review plugins, never as lefthook/CI gates. A
linter that only runs in one place is drift, not defense in depth.

- node-lint.sh now runs `npx knip` alongside biome, so it rides the
  existing required "Node CI / Biome Lint" check — the reusable
  node-ci.yml workflow has no dedicated knip input.
- Add scripts/ci/shellcheck.sh, which lints every tracked *.sh file
  and hard-fails locally with an install hint if shellcheck is
  missing (no silent skip). Wire it into lefthook pre-push and a new
  "Shellcheck" job in ci-verify.yml.
- Fix the one shellcheck finding it surfaced: unused loop variables in
  benchmarks/run.sh (SC2034), renamed to `_`.
- The lefthook zizmor step now hard-fails with an install hint instead
  of silently skipping when the binary is absent, matching the
  blocking-local-gate pattern used elsewhere in the org (e.g.
  drydock's scan-secrets.sh) for tools CI enforces unconditionally.

* fix(ci): cover shellcheck's pre-0.10 SC2317 code in run-matrix.sh disables

* test(ci): cover shellcheck.sh in the fixed-script contract

CodeRabbit review: the runner is mode 100755 but assertFixedScripts() never
inspected it, so its markers went unchecked. Also corrects the lefthook
comment, which named the node-ci lint job instead of the standalone
shellcheck job.

* ci(review): wire Greptile as a label-gated second-opinion reviewer (#281)

* ci(review): wire Greptile as a label-gated second-opinion reviewer

Adds a thin `.github/workflows/greptile.yml` caller that summons
CodesWhat/.github's reusable greptile-summon.yml only when a PR carries
the `second-opinion` label, and a matching `.coderabbit.yaml`
`labeling_instructions` entry so CodeRabbit can apply that label itself
on security-sensitive diffs, large refactors, or a tiebreaker with the
author's intent. AGENTS.md now explains what the dormant `greptile.json`
config is for, so it doesn't read as dead weight.

Closes #269

* fix(review): enable suggested_labels so auto_apply_labels can fire

suggested_labels: false disables label suggestions entirely, including
labeling_instructions, so CodeRabbit would never apply second-opinion and
the greptile.yml caller would never trigger.

* docs(readme): migrate to the codified house README shape (#278)

* docs(readme): migrate to the codified house README shape

Reorders the header stack (logo/grabber/badge wall/hr/release note
before Contents), regroups the badge wall into three ordered rows
(identity, quality/security, social proof), adds the qlty
maintainability badge, drops all decorative emoji from headings, the
Contents list, and the Features table's icon column, reconciles the
previously split Community sections into one with a single
Issues/Discussions/Discord routing sentence, and fixes a stale
anchor link in the tri-tool example README.

Closes #270

* docs(readme): disambiguate the two Documentation contents entries

The external docs-site link and the in-page section anchor both read
Documentation in the same Contents list. The external one is now Docs
site; the anchor keeps the label that matches its section heading.

* build(deps): dedupe nested magic-string to 1.2.1

The lockfile-dedupe pre-push gate resolves against the live registry, so
the newly published 1.2.1 made it fail on every branch until the nested
copy under fumadocs-mdx is realigned.

* build(release): add GoReleaser SBOMs and qualify SLSA claim to Build L2 (#279)

* build(release): add GoReleaser SBOMs and qualify SLSA claim to Build L2

- build(release): add a sboms: block to app/.goreleaser.yaml (syft,
  CycloneDX per archive), mirroring portwing's existing pattern
- ci(release): install syft via anchore/sbom-action/download-syft in
  release-from-tag.yml ahead of the GoReleaser run; skip sbom cataloging
  in the shared snapshot/config-check script when syft isn't on PATH so
  the local pre-push hook and the CI "GoReleaser Config" gate keep
  passing without needing syft, without hiding a real failure
- docs(releasing): qualify "SLSA attestation"/"SLSA build provenance" to
  "SLSA Build L2" per the house provenance standard, and note that v*
  tags are protected against deletion/rewrite by a repo tag ruleset, so
  the Cosign signatures and SLSA Build L2 attestations are the signature
  of record instead of a git-tag signature
- docs(changelog): record the SBOM and SLSA-qualification change under
  [Unreleased] / Security

* build(release): restore syft --enrich all in the SBOM args

Setting args: replaces GoReleaser's entire default slice, so switching the
output to cyclonedx dropped --enrich all and shipped SBOMs without
dependency license data.

* build(deps): dedupe nested magic-string to 1.2.1

Same registry-driven lockfile-dedupe gate failure as #278; realigns the
nested copy under fumadocs-mdx so pushes pass.

* fix: replace the retired star-history.com chart with Warpchart (#283)

* docs(readme): drop the broken star-history.com chart embed

GitHub restricted stargazer API access, so the star-history.com SVG now
serves an error card reading 'GitHub restricted access to star data'
directly above the working Warpchart embed. Warpchart accumulates its own
time series and is unaffected, so it replaces rather than duplicates the
old chart. Also gives the section the house h2 heading it lacked.

* build(deps): dedupe nested magic-string to 1.2.1

* fix(website): replace the retired star-history.com chart with Warpchart

GitHub restricted the stargazer API that star-history.com replays per
request, so its SVG returns an error card at HTTP 200 — the landing page
was serving that to every visitor. Warpchart accumulates its own time
series and honours theme=dark, so the light/dark swap is preserved. The
CSP img-src allowlist moves with it, in lockstep with the pinned test.

* docs(changelog): reconcile the stale Warpchart entry with the star-history removal

The #264 entry said the Warpchart chart sits alongside the existing
chart. This PR removes that chart, so the clause is now false and
contradicts the entry directly above it. Drop the clause and add the
issue citation the entry was missing.

* fix(deps): update non-major (gomod) (#272)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update non-major (github-actions) (#274)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update non-major (npm) (#273)

* fix(deps): update non-major (npm)

* test(website): pin posthog-js assertion to 1.417.1

The npm dependency bump upgraded posthog-js from 1.417.0 to 1.417.1
but the analytics integration test still asserted the old exact
version, failing CI.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>

* ci(security): pin runners, comment reusable pins, and start harden-runner egress block (#282)

* ci(security): pin runners, comment reusable pins, and start harden-runner egress block

Pins all 33 runs-on: ubuntu-latest references to ubuntu-24.04 across every
workflow so a GitHub-side default-image bump can never silently change CI
without a diff to review, and adds freeze-date comments to the two frozen
CodesWhat/.github reusable-workflow SHA pins in ci-verify.yml (that repo
tags no releases, so a SHA is the only anchor). Updates four stale
"ubuntu-latest" prose comments in quality-integration-podman.yml to match.

Starts stage 1 of a 3-stage harden-runner egress rollout, modeled on
drydock's own live posture: 15 jobs across 8 workflow files with a
deterministic, single-vendor egress footprint move from egress-policy:
audit to block with an explicit allowed-endpoints list and
disable-sudo: true, plus ci-verify.yml's go-ci caller now passes
workflow-security-egress-policy: block through to the reusable workflow's
zizmor job. Jobs with variable or multi-registry egress (Docker builds
against Chainguard's CDN, dockerd/podman integration pulling from Docker
Hub, the tri-tool conformance matrix, CodeQL's query-bundle CDN) stay
audit pending real telemetry. DAST (testssl) and OpenSSF Scorecard stay
audit permanently and now say so in-file. Release jobs are untouched
until two audited release-candidate cuts have run clean.

Bumps scripts/required-ci-contexts.test.mjs's REQUIRED_CONTEXTS from 9 to
13, adding Dependency Review, Gitleaks, Actionlint, and Shellcheck, which
already exist as PR-gating jobs in ci-verify.yml but were never added to
the pinned required-context list. Ships ahead of the branch-protection
ruleset update that will actually require them.

* ci(security): keep gosec on audit and allow go.dev for setup-go

Two fixes to the stage-1 egress rollout, both found by running the jobs
rather than reading them.

gosec back to audit: it runs as a container action, and block mode drops
container egress silently instead of rejecting it, so every module lookup
burns a full TCP timeout. The job takes ~2 minutes on every other branch
and exceeded timeout-minutes: 15 twice here, with no denied domain logged
to explain it. Same reasoning already applied to Docker Build and
grype-image, which stay on audit for the container-pull footprint.

govulncheck gains go.dev: setup-go checks the actions/go-versions manifest
first and only falls back to go.dev/dl. A warm runner never hits it, so
the gap stays invisible until a cache miss takes the job down.

* build(deps): tidy go.mod after the gomod bump

Renovate's non-major gomod update (#272) landed without a go mod tidy, so
dev/v1.7 carries go-jose/v4, letsencrypt/boulder and titanous/rocacheck as
indirect requirements nothing resolves to anymore. The goreleaser release
hook runs go mod tidy anyway, so this only decides whether the tree is
tidy before the tag or during it.

* ci(security): add ZAP baseline passive DAST scan for getsockguard.com (#280)

* ci(security): add ZAP baseline passive DAST scan for getsockguard.com

getsockguard.com and its /docs subpath are static surfaces (no auth,
no forms, no server-side handlers), so the DAST tier here is a ZAP
baseline passive scan (headers, CSP, cookie flags) against both
deployed targets on a weekly schedule plus workflow_dispatch. No
pull_request trigger: DAST scans what's already deployed, so it's
scheduled/advisory and never gates a PR. allow_issue_writing is off
so a red run stays a triage signal in the Actions tab instead of an
auto-filed issue, keeping the job's permissions at contents: read.
.zap/rules.tsv suppresses one informational false positive (rule
10049) that the site's immutable, content-hashed _next/static assets
reliably trigger. harden-runner runs egress-policy: audit as a
permanent exception, since the job scans a live Vercel-fronted
third-party endpoint with no stable destination set to allowlist.

* ci(security): withhold the repo token from the ZAP action

The action's token input defaults to ${{ github.token }} and exists only
to file findings as a GitHub issue, which allow_issue_writing: false
already turns off. Passing it empty means this third-party JS action
never receives a repository token at all, instead of relying on the
job's contents:read scope to bound what it could do with one.

* build(deps): realign the lockfile after an upstream magic-string publish

The lockfile-dedupe pre-push gate runs npm dedupe --dry-run against the
live registry, so magic-string 1.2.1 shipping upstream turns every branch
red with no local change. Nothing here depends on it directly; this just
resettles the transitive tree under fumadocs-mdx.

* chore(deps): lock file maintenance (#275)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>

* ci(gating): require the security scanners and drop emoji from job names (#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:.

* chore(release): prepare the v1.7.1 changelog and roadmap (#286)

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 (#290)

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

* ci(quality): resolve a current sockguard for the current-* conformance 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 (#292)

* 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

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
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.

1 participant