Skip to content

Networks#39

Merged
enegalan merged 3 commits into
mainfrom
27-feat-networks
Jul 5, 2026
Merged

Networks#39
enegalan merged 3 commits into
mainfrom
27-feat-networks

Conversation

@enegalan

@enegalan enegalan commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added a Networks view for browsing, searching, inspecting, and removing networks.
    • Added proxy settings in the app, including HTTP, HTTPS, and no-proxy entries.
  • Bug Fixes
    • Improved build list formatting when a build has no context.
  • Chores
    • Updated release/version info to 0.6.0.
    • Refreshed change log and project guidance to match the latest app structure.

enegalan added 2 commits July 6, 2026 01:04
- Added a new **Networks** screen in the sidebar for listing and inspecting networks, including details such as driver, scope, gateway, and options.
- Implemented backend API endpoints for network management: `GET /v1/networks`, `GET /v1/networks/{name}`, and `DELETE /v1/networks/{name}`.
- Enhanced configuration to support HTTP/HTTPS proxy settings, persisted in the config and applied to containerd in the VM.
- Updated changelog to reflect the addition of network management features and improvements.
- Introduced validation for proxy settings in the API, ensuring proper handling of HTTP/HTTPS configurations.
- Bumped version to 0.6.0 in both backend and UI configurations.
- Added a new section in CHANGELOG for version 0.6.0, highlighting redesigned proxy settings with improved layout and usability.
- Updated CHANGELOG entries to reflect user-facing changes and improvements in the application.
@enegalan enegalan linked an issue Jul 5, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b585f5d4-b5f6-4d4b-8a9f-bb9cd0ce08e6

📥 Commits

Reviewing files that changed from the base of the PR and between 48194cb and e6b6d0b.

📒 Files selected for processing (13)
  • backend/internal/api/handlers.go
  • backend/internal/api/networks.go
  • backend/internal/api/runtime_errors.go
  • backend/internal/buildhistory/inspect.go
  • backend/internal/buildhistory/inspect_test.go
  • backend/internal/runtime/errors.go
  • backend/internal/runtime/lima.go
  • backend/internal/runtime/lima.yaml
  • backend/internal/runtime/mock.go
  • backend/internal/runtime/network.go
  • backend/internal/runtime/proxy.go
  • ui/lib/app_shell.dart
  • ui/lib/screens/networks_screen.dart
📝 Walkthrough

Walkthrough

This PR adds Docker/nerdctl network management (list/inspect/remove) across backend runtime implementations (Lima, Native, Mock), exposes new /v1/networks API endpoints, and adds a Flutter Networks screen. It also introduces HTTP/HTTPS/no-proxy configuration support end-to-end (config, API validation, runtime application, and settings UI), a buildhistory label-derived context fix, a builds-list subtitle formatting fix, and documentation/changelog/version updates.

Changes

Networks and proxy feature

Layer / File(s) Summary
Runtime data types and interface contract
backend/internal/runtime/runtime.go, backend/internal/runtime/proxy.go, backend/internal/runtime/errors.go
Adds Network/NetworkDetail types, ProxyConfig type, extends Runtime interface with network/proxy methods, updates New constructor, and adds emptyNetworksIfStopped helper.
Network listing/inspection parsing logic
backend/internal/runtime/network.go, backend/test/runtime/network_test.go
Implements docker/nerdctl network parsing, merging, driver/subnet/gateway resolution, pseudo-network handling, and native CNI inspect enrichment, with tests.
Runtime implementations wiring networks and proxy
backend/internal/runtime/lima.go, lima.yaml, localhost_proxy.go, mock.go, native.go, backend/cmd/calf/main.go
Wires ListNetworks/InspectNetwork/RemoveNetwork/ApplyProxy into Lima/Native/Mock, adds proxy resync on state changes, forced localhost proxy sync, and VM proxy provisioning.
Backend API endpoints for networks and proxy config
backend/internal/api/networks.go, server.go, handlers.go, backend/internal/config/config.go, config.yaml, backend/test/api/api_test.go
Adds network HTTP handlers/routes, proxy config fields, validation for proxy URLs/no-proxy entries, and applies proxy changes with tests.
UI API client models and methods
ui/lib/api/client.dart, ui/test/widget_test.dart
Adds NetworkItem/NetworkDetail models and client methods, extends Config with proxy fields, updates test fakes.
Networks screen UI
ui/lib/screens/networks_screen.dart, ui/lib/app_shell.dart
Adds network list/detail screen with search, polling, removal, and wires it into app navigation.
Proxy settings UI
ui/lib/app_shell.dart
Adds proxy input fields, no-proxy list management, and validation in the settings screen.
Build history label fix and unrelated UI fix
backend/internal/buildhistory/inspect.go, ui/lib/screens/resources_screen.dart
Adds Labels field and label-derived context resolution; fixes builds-list subtitle formatting when context is empty.
Docs, changelog, roadmap, and version bump
.cursor/rules/calf.mdc, CLAUDE.md, CHANGELOG.md, ROADMAP.md, backend/version/version.go, ui/pubspec.yaml
Updates docs/rules, condenses changelog entries, checks off roadmap items, and bumps version to 0.6.0.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
    participant UI as NetworksScreen
    participant ApiClient
    participant Server as Server (API)
    participant Runtime

    UI->>ApiClient: fetchNetworks()
    ApiClient->>Server: GET /v1/networks
    Server->>Runtime: ListNetworks(ctx)
    Runtime-->>Server: []Network
    Server-->>ApiClient: 200 OK JSON
    ApiClient-->>UI: List<NetworkItem>

    UI->>ApiClient: removeNetwork(name)
    ApiClient->>Server: DELETE /v1/networks/{name}
    Server->>Runtime: RemoveNetwork(ctx, name)
    Runtime-->>Server: ok / error
    Server-->>ApiClient: 200 OK {"status":"ok"}
    ApiClient-->>UI: void
Loading
sequenceDiagram
    participant SettingsUI as SettingsScreen
    participant ApiClient
    participant Server as Server (API)
    participant Runtime

    SettingsUI->>ApiClient: updateConfig(httpProxy, httpsProxy, noProxy)
    ApiClient->>Server: PUT /v1/config
    Server->>Server: validateProxyUpdate(req)
    Server->>Server: save trimmed proxy config
    Server->>Runtime: ApplyProxy(ctx, ProxyConfig)
    Runtime-->>Server: ok / warning logged
    Server-->>ApiClient: 200 OK config response
    ApiClient-->>SettingsUI: updated Config
Loading

Possibly related PRs

  • enegalan/calf#9: Both PRs modify backend/internal/api/server.go route registration in Server.Handler(), extended here to add /v1/networks routes.
  • enegalan/calf#19: Extends the Lima/native runtime and Runtime interface introduced in that PR with proxy handling and new network-management methods.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is too vague to convey the main change; it only says "Networks" without describing what changed. Use a concise, specific title that summarizes the primary change, such as adding network management support.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 27-feat-networks

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: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/internal/runtime/lima.go (1)

64-99: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cancel the watcher when the runtime stops

Start() launches watchPortProxies on every successful start, and Stop() never cancels it. Because the loop only exits on ctx.Done(), a stop/start cycle can leave old pollers running until that context ends; keep a dedicated cancel/guard for the watcher and close it in Stop().

🤖 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 `@backend/internal/runtime/lima.go` around lines 64 - 99, The Lima runtime
watcher is not being stopped, so repeated Start/Stop cycles can leave old
watchPortProxies loops running. Add a dedicated watcher context/cancel or
similar guard in the Lima type and start watchPortProxies with that
lifecycle-managed context from Start; then make Stop() cancel it and clear the
running state so only one watcher exists per runtime instance.
🧹 Nitpick comments (5)
backend/internal/api/handlers.go (1)

264-292: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant double SplitHostPort call.

validateNoProxyEntry calls net.SplitHostPort(host) once in the if condition and again inside the block to get the actual host value. This works but re-parses the same string twice unnecessarily.

♻️ Proposed simplification
-	if _, _, err := net.SplitHostPort(host); err == nil {
-		host, _, err = net.SplitHostPort(host)
-		if err == nil {
-			if net.ParseIP(host) != nil {
-				return nil
-			}
-		}
-	}
+	if splitHost, _, err := net.SplitHostPort(host); err == nil {
+		host = splitHost
+		if net.ParseIP(host) != nil {
+			return 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 `@backend/internal/api/handlers.go` around lines 264 - 292, The
validateNoProxyEntry function in handlers.go is redundantly calling
net.SplitHostPort twice on the same host value. Refactor that block so the host
and port are extracted once, then reuse the parsed host for the net.ParseIP and
isValidDomain checks; keep the existing validation behavior and error messages
unchanged.
backend/internal/api/networks.go (1)

16-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Repeated error-handling block could be a small helper.

The writeRuntimeErrorwriteError(..., err.Error()) pattern is duplicated three times in this file. A small helper (e.g., s.writeRuntimeOrInternalError(w, err)) would reduce duplication.

Also applies to: 47-55, 59-66

🤖 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 `@backend/internal/api/networks.go` around lines 16 - 24, The same
runtime-error fallback pattern is duplicated in this file, so extract it into a
small helper on the handler type, such as s.writeRuntimeOrInternalError(w, err),
and use it in the ListNetworks flow and the other repeated runtime calls. Keep
the helper’s behavior identical to the current writeRuntimeError followed by
writeError(..., err.Error()) fallback so the existing methods remain unchanged
while removing duplication.
backend/internal/buildhistory/inspect.go (1)

103-109: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer stdlib path.Dir over manual string splitting.

stripLastComponent reimplements what path.Dir (Unix-style paths, matching container/Compose paths) already provides.

♻️ Proposed simplification
-func stripLastComponent(path string) string {
-	idx := strings.LastIndex(path, "/")
-	if idx < 0 {
-		return path
-	}
-	return path[:idx]
-}
+func stripLastComponent(p string) string {
+	return path.Dir(p)
+}

(Requires importing "path", distinct from "path/filepath" to keep the Unix-style separator semantics used by container paths.)

🤖 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 `@backend/internal/buildhistory/inspect.go` around lines 103 - 109, The
stripLastComponent helper is manually reimplementing directory extraction;
replace the string-based slash handling with the stdlib path.Dir function in
stripLastComponent. Add/import path (not path/filepath) so Unix-style container
path semantics are preserved, and keep the function behavior aligned with the
current callers in inspect.go.
backend/internal/runtime/network.go (1)

249-265: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Per-network inspect failures are silently dropped without any diagnostic trail.

networkInspectMetadataBatch continues on every inspectNetworkMetadata error and always returns a nil error, so callers (and the dead if err != nil branch in listNetworks at Line 101-104) can never learn which networks failed to enrich or why. As per coding guidelines, **/*.go code should handle each specific error case individually so failures are identifiable rather than swallowed by a generic catch-all.

Consider collecting failed names and surfacing them (e.g., as a partial error or log) instead of silently continuing:

 func networkInspectMetadataBatch(ctx context.Context, run commandRunner, names []string) (map[string]enrichedNetworkInspectRow, error) {
 	rows := make(map[string]enrichedNetworkInspectRow, len(names))
+	var failed []string
 	for _, name := range names {
 		if isPseudoNetwork(name) {
 			continue
 		}
 
 		row, err := inspectNetworkMetadata(ctx, run, name)
 		if err != nil {
-			continue
+			failed = append(failed, name)
+			continue
 		}
 
 		rows[name] = row
 	}
-
-	return rows, nil
+	if len(failed) > 0 {
+		return rows, fmt.Errorf("failed to inspect networks: %s", strings.Join(failed, ", "))
+	}
+	return rows, nil
 }

As per coding guidelines, "Do not use generic catch-alls; handle each specific error case individually so logs, UI, and API responses identify exactly what failed."

🤖 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 `@backend/internal/runtime/network.go` around lines 249 - 265,
networkInspectMetadataBatch currently swallows every inspectNetworkMetadata
failure and returns nil, which hides per-network enrichment problems. Update
networkInspectMetadataBatch to track failed network names and surface the
specific inspectNetworkMetadata error for each one, either by returning a
partial/aggregated error or logging the failure with the network name. Then
adjust listNetworks to handle the returned error path meaningfully instead of
relying on the dead err check, using the existing inspectNetworkMetadata and
networkInspectMetadataBatch symbols to keep failures identifiable.

Source: Coding guidelines

backend/internal/runtime/mock.go (1)

662-675: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Network mocking reuses ContainersErr/ContainerErr instead of dedicated fields.

ListNetworks triggers on m.ContainersErr and RemoveNetwork on m.ContainerErr — both named for container-specific test scenarios. A test that sets ContainersErr to simulate a container-listing failure will unexpectedly also break ListNetworks, and vice versa. Dedicated NetworksErr/NetworkErr fields would keep test scenarios isolated.

Also applies to: 707-724

🤖 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 `@backend/internal/runtime/mock.go` around lines 662 - 675, `ListNetworks` and
`RemoveNetwork` are incorrectly sharing container-specific error fields, so
network test cases can interfere with container scenarios. Update the `Mock`
runtime to use dedicated network error fields (for example `NetworksErr` and
`NetworkErr`) in the `ListNetworks` and `RemoveNetwork` methods, and wire those
new fields through the mock struct and any related setup so
`ContainersErr`/`ContainerErr` remain container-only.
🤖 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 `@backend/internal/api/handlers.go`:
- Around line 140-147: The `updateConfig` flow in `handlers.go` can exceed the
default `ApiClient.updateConfig().timeout(timeout)` because `ApplyProxy` is
allowed to run for up to 2 minutes, causing the UI request to fail before the
backend finishes. Update the `updateConfig` path to either use a longer client
timeout for proxy changes or return the save response before
`s.runtime.ApplyProxy` completes, and keep the fix centered on the
`proxyChanged`, `ApplyProxy`, and `ApiClient.updateConfig()` flow.

In `@backend/internal/api/networks.go`:
- Around line 46-66: handleNetworkAction has the same missing-timeout problem in
its GET and DELETE paths: both InspectNetwork and RemoveNetwork are called with
r.Context() directly. Update handleNetworkAction to create and use a context
with an explicit timeout or cancellation before calling s.runtime.InspectNetwork
and s.runtime.RemoveNetwork, matching the pattern used for other HTTP handlers
in this file.
- Around line 8-30: handleNetworks currently passes r.Context() straight into
s.runtime.ListNetworks without any explicit timeout, so a blocking VM call can
hang the request. Update handleNetworks to create a bounded context with
timeout/cancellation before calling ListNetworks, follow the same pattern used
in handleConfig for runtime operations, and ensure the derived context is always
canceled after the call. Keep the change localized to handleNetworks and the
ListNetworks invocation.

In `@backend/internal/buildhistory/inspect.go`:
- Around line 40-51: parseInspectDetail currently swallows json.Unmarshal
failures and returns a default InspectDetail, making malformed docker inspect
output indistinguishable from empty data. Change parseInspectDetail to return an
error alongside the InspectDetail, propagate the unmarshal failure instead of
falling back silently, and update the callers that depend on it (including
Inspect, enrichHistoryBuild, and resolveBuildSourcePaths) to handle and surface
that error. Also update the successful empty-input path to return an explicit
nil error so the function cleanly separates “no data” from “parse failure.”
- Around line 81-101: resolveContextFromLabels is checking compose label paths
directly with os.Stat, which fails for Lima VM paths. Update the path handling
in resolveContextFromLabels to translate com.docker.compose.project.working_dir
and com.docker.compose.project.config_files from the VM path to the host mount
root before stat’ing them, then return the translated working directory or
stripLastComponent from the translated config file path.

In `@backend/internal/runtime/lima.go`:
- Around line 92-95: The proxy application in Start() is too strict: it
currently returns the ApplyProxy error and aborts startup, unlike handleConfig
which only warns and continues. Update the Start flow around l.ApplyProxy so
proxy-apply failures are logged as warnings and do not fail the entire start
sequence, matching the non-fatal behavior used in the config-update path.
- Around line 29-36: `Lima` has unsynchronized shared state: `Status()` and the
background watcher race on `lastState`, and `ApplyProxy()` can race with
`ensureTemplate()` on `proxy`. Update the `Lima` type and its `NewLima`,
`Status`, `ApplyProxy`, and `ensureTemplate` call paths to protect these fields
with a mutex or equivalent synchronization, making sure all reads and writes of
`l.lastState` and `l.proxy` happen under the same lock.

In `@backend/internal/runtime/lima.yaml`:
- Around line 65-87: The proxy update in validateProxyUpdate/lima.yaml is still
interpolating raw proxy values into a root-run shell script, so escape or quote
the HTTP_PROXY, HTTPS_PROXY, and NO_PROXY values before embedding them in the
generated heredocs. Also make sure the generated
/etc/systemd/system/containerd.service.d/calf-proxy.conf and
/etc/profile.d/calf-proxy.sh files are written with restrictive permissions such
as 0600 when created through the sudo tee blocks.

In `@backend/internal/runtime/mock.go`:
- Around line 707-724: Mock.RemoveNetwork should match the runtime contract by
refusing removal when the runtime is not running. Add the same running-state
guard used by ListNetworks/InspectNetwork and the real lima.go/native.go
RemoveNetwork implementations, returning ErrRuntimeNotRunning before mutating
m.Networks. Keep the existing ContainerErr handling, but ensure the
runtime-state check happens first so tests can cover the “remove while stopped”
path.
- Around line 677-705: InspectNetwork currently returns a generic fmt.Errorf for
missing networks, which causes handleNetworkAction to treat the failure as an
internal error instead of a not-found response. Update the Mock.InspectNetwork
path to return a dedicated not-found sentinel for the missing network case, then
adjust handleNetworkAction to recognize that sentinel and translate it to
http.StatusNotFound while preserving ErrRuntimeNotRunning handling as-is.

In `@backend/internal/runtime/network.go`:
- Around line 81-138: The listNetworks helper returns immediately when
queryNerdctlNetworks fails, so it never applies the docker fallback used
elsewhere. Update listNetworks to continue with queryDockerNetworks and
mergeNetworksByName even when nerdctl lookup fails, using the Docker results
when nerdctl is unavailable; keep the existing metadata enrichment path in
networkInspectMetadataBatch and preserve defaultNetworkScope handling for
entries without inspect data.

In `@backend/internal/runtime/proxy.go`:
- Around line 15-62: The proxy values interpolated by applyProxyInVM are not
fully shell-safe, so malicious characters can still be executed through the bash
-lc script. Fix shellQuote to return a properly shell-escaped value that handles
spaces and shell metacharacters, and ensure applyProxyInVM uses that escaped
output consistently for every HTTPProxy/HTTPSProxy/NoProxy insertion in both the
containerd drop-in and /etc/profile.d script.

In `@ui/lib/app_shell.dart`:
- Around line 269-275: Apply should not be enabled when proxy validation errors
exist. Update the `_dirty`/Apply gating in `AppShell` so it also considers
`_httpProxyError` and `_httpsProxyError`, and keep the button disabled whenever
either validation error is set. If you choose to allow submission instead, then
`applyConfig` must surface the backend 400 instead of silently clearing
`_saving`; otherwise users can’t tell why the apply failed.

In `@ui/lib/screens/networks_screen.dart`:
- Around line 270-274: The NetworkDetailView _removeNetwork flow currently calls
removeNetwork, onRemoved, and onBack with no failure handling, so exceptions can
escape and the UI may close as if removal succeeded. Update the _removeNetwork
method in NetworkDetailView to catch removeNetwork failures, set the existing
_error state so the view can surface the problem, and only call onRemoved/onBack
after a successful removal; use the same error-handling pattern already present
in the list-view _removeNetwork for guidance.

---

Outside diff comments:
In `@backend/internal/runtime/lima.go`:
- Around line 64-99: The Lima runtime watcher is not being stopped, so repeated
Start/Stop cycles can leave old watchPortProxies loops running. Add a dedicated
watcher context/cancel or similar guard in the Lima type and start
watchPortProxies with that lifecycle-managed context from Start; then make
Stop() cancel it and clear the running state so only one watcher exists per
runtime instance.

---

Nitpick comments:
In `@backend/internal/api/handlers.go`:
- Around line 264-292: The validateNoProxyEntry function in handlers.go is
redundantly calling net.SplitHostPort twice on the same host value. Refactor
that block so the host and port are extracted once, then reuse the parsed host
for the net.ParseIP and isValidDomain checks; keep the existing validation
behavior and error messages unchanged.

In `@backend/internal/api/networks.go`:
- Around line 16-24: The same runtime-error fallback pattern is duplicated in
this file, so extract it into a small helper on the handler type, such as
s.writeRuntimeOrInternalError(w, err), and use it in the ListNetworks flow and
the other repeated runtime calls. Keep the helper’s behavior identical to the
current writeRuntimeError followed by writeError(..., err.Error()) fallback so
the existing methods remain unchanged while removing duplication.

In `@backend/internal/buildhistory/inspect.go`:
- Around line 103-109: The stripLastComponent helper is manually reimplementing
directory extraction; replace the string-based slash handling with the stdlib
path.Dir function in stripLastComponent. Add/import path (not path/filepath) so
Unix-style container path semantics are preserved, and keep the function
behavior aligned with the current callers in inspect.go.

In `@backend/internal/runtime/mock.go`:
- Around line 662-675: `ListNetworks` and `RemoveNetwork` are incorrectly
sharing container-specific error fields, so network test cases can interfere
with container scenarios. Update the `Mock` runtime to use dedicated network
error fields (for example `NetworksErr` and `NetworkErr`) in the `ListNetworks`
and `RemoveNetwork` methods, and wire those new fields through the mock struct
and any related setup so `ContainersErr`/`ContainerErr` remain container-only.

In `@backend/internal/runtime/network.go`:
- Around line 249-265: networkInspectMetadataBatch currently swallows every
inspectNetworkMetadata failure and returns nil, which hides per-network
enrichment problems. Update networkInspectMetadataBatch to track failed network
names and surface the specific inspectNetworkMetadata error for each one, either
by returning a partial/aggregated error or logging the failure with the network
name. Then adjust listNetworks to handle the returned error path meaningfully
instead of relying on the dead err check, using the existing
inspectNetworkMetadata and networkInspectMetadataBatch symbols to keep failures
identifiable.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 81700c26-6b34-4dd1-a65e-6198af111993

📥 Commits

Reviewing files that changed from the base of the PR and between 020cfb0 and 48194cb.

📒 Files selected for processing (29)
  • .cursor/rules/calf.mdc
  • CHANGELOG.md
  • CLAUDE.md
  • ROADMAP.md
  • backend/cmd/calf/main.go
  • backend/internal/api/handlers.go
  • backend/internal/api/networks.go
  • backend/internal/api/server.go
  • backend/internal/buildhistory/inspect.go
  • backend/internal/config/config.go
  • backend/internal/config/config.yaml
  • backend/internal/runtime/errors.go
  • backend/internal/runtime/lima.go
  • backend/internal/runtime/lima.yaml
  • backend/internal/runtime/localhost_proxy.go
  • backend/internal/runtime/mock.go
  • backend/internal/runtime/native.go
  • backend/internal/runtime/network.go
  • backend/internal/runtime/proxy.go
  • backend/internal/runtime/runtime.go
  • backend/test/api/api_test.go
  • backend/test/runtime/network_test.go
  • backend/version/version.go
  • ui/lib/api/client.dart
  • ui/lib/app_shell.dart
  • ui/lib/screens/networks_screen.dart
  • ui/lib/screens/resources_screen.dart
  • ui/pubspec.yaml
  • ui/test/widget_test.dart

Comment thread backend/internal/api/handlers.go
Comment thread backend/internal/api/networks.go
Comment thread backend/internal/api/networks.go
Comment thread backend/internal/buildhistory/inspect.go Outdated
Comment on lines +81 to +101
func resolveContextFromLabels(labels map[string]string) string {
if workingDir, ok := labels["com.docker.compose.project.working_dir"]; ok && workingDir != "" {
if _, err := os.Stat(workingDir); err == nil {
return workingDir
}
}

if configFiles, ok := labels["com.docker.compose.project.config_files"]; ok {
for _, part := range strings.Split(configFiles, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
if _, err := os.Stat(part); err == nil {
return stripLastComponent(part)
}
}
}

return ""
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how Lima mounts the host filesystem into the VM (path parity assumption)
rg -n -A5 -B5 'mounts|writable|hostpath' backend/internal/runtime/lima.yaml

Repository: enegalan/calf

Length of output: 364


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== inspect.go ==\n'
ast-grep outline backend/internal/buildhistory/inspect.go --view expanded || true
wc -l backend/internal/buildhistory/inspect.go

printf '\n== relevant slice ==\n'
sed -n '1,220p' backend/internal/buildhistory/inspect.go

printf '\n== runtime files ==\n'
fd -a '.*' backend/internal/runtime | sed -n '1,200p'

printf '\n== search for compose labels / build history ==\n'
rg -n 'com\.docker\.compose\.project\.(working_dir|config_files)|resolveContextFromLabels|buildhistory|working_dir|config_files' backend -S

Repository: enegalan/calf

Length of output: 7094


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== runtime/lima.go ==\n'
wc -l backend/internal/runtime/lima.go
sed -n '1,240p' backend/internal/runtime/lima.go

printf '\n== runtime/lima.yaml ==\n'
sed -n '1,220p' backend/internal/runtime/lima.yaml

printf '\n== compose migration / label sources ==\n'
sed -n '1,220p' backend/internal/migration/compose_migration.go

printf '\n== buildhistory inspect tests ==\n'
wc -l backend/internal/buildhistory/inspect_test.go
sed -n '1,240p' backend/internal/buildhistory/inspect_test.go

Repository: enegalan/calf

Length of output: 15142


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== native runtime ==\n'
wc -l backend/internal/runtime/native.go
sed -n '1,260p' backend/internal/runtime/native.go

printf '\n== docker socket / path mapping helpers ==\n'
rg -n 'VMPath|HostPath|dockerSocket|mountPoint|/mnt/calf|/mnt/lima-cidata|mounted|forward' backend/internal/runtime backend/internal/migration -S

Repository: enegalan/calf

Length of output: 10463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== runDocker / socket plumbing ==\n'
rg -n 'func runDocker|defaultDockerSocket|DockerSocket\(\)|runDocker\(' backend/internal -S

printf '\n== buildhistory files ==\n'
sed -n '1,220p' backend/internal/buildhistory/history.go
sed -n '1,220p' backend/internal/buildhistory/logs.go

Repository: enegalan/calf

Length of output: 8491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== build path handling ==\n'
rg -n 'RunBuild|buildx|compose|working_dir|config_files|Context' backend/internal/runtime backend/internal/api backend/internal/migration -S

printf '\n== defaultDockerSocket / Lima socket ==\n'
sed -n '620,680p' backend/internal/runtime/lima.go

printf '\n== VMPath helper ==\n'
sed -n '110,150p' backend/internal/migration/docker_desktop.go

Repository: enegalan/calf

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== defaultDockerSocket + Lima path helpers ==\n'
sed -n '620,675p' backend/internal/runtime/lima.go
sed -n '150,210p' backend/internal/runtime/build_enrich.go
sed -n '1,220p' backend/internal/api/builds.go

printf '\n== VMPath helper and compose mount root ==\n'
sed -n '110,145p' backend/internal/migration/docker_desktop.go
sed -n '540,575p' backend/internal/migration/docker_desktop.go

Repository: enegalan/calf

Length of output: 8825


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== runBuild implementations ==\n'
sed -n '70,130p' backend/internal/runtime/nerdctl.go
sed -n '330,390p' backend/internal/runtime/mock.go
sed -n '330,380p' backend/internal/runtime/native.go
sed -n '330,380p' backend/internal/runtime/lima.go

printf '\n== build result enrichment / path resolution ==\n'
sed -n '1,120p' backend/internal/runtime/build_enrich.go
sed -n '120,210p' backend/internal/runtime/build_enrich.go

printf '\n== runtime build struct ==\n'
sed -n '1,120p' backend/internal/runtime/build.go

Repository: enegalan/calf

Length of output: 13630


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== build job and source-path resolution ==\n'
sed -n '220,420p' backend/internal/api/builds.go

printf '\n== build source / staging helpers ==\n'
rg -n 'stage|staging|mountsRoot|VMPath|RunBuild\\(|Context:' backend/internal/api backend/internal/migration backend/internal/runtime -S

Repository: enegalan/calf

Length of output: 5730


Translate Lima build-history paths before os.Stat. Native works as-is, but Lima records compose labels as VM paths (for example /mnt/calf/...), while this check runs on the host. That makes resolveContextFromLabels drop valid build contexts for Lima-backed builds. Map the label back to the host mount root before checking.

🤖 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 `@backend/internal/buildhistory/inspect.go` around lines 81 - 101,
resolveContextFromLabels is checking compose label paths directly with os.Stat,
which fails for Lima VM paths. Update the path handling in
resolveContextFromLabels to translate com.docker.compose.project.working_dir and
com.docker.compose.project.config_files from the VM path to the host mount root
before stat’ing them, then return the translated working directory or
stripLastComponent from the translated config file path.

Comment thread backend/internal/runtime/mock.go
Comment thread backend/internal/runtime/network.go
Comment on lines +15 to +62
func applyProxyInVM(ctx context.Context, run commandRunner, proxy ProxyConfig) error {
httpProxy := shellQuote(proxy.HTTPProxy)
httpsProxy := shellQuote(proxy.HTTPSProxy)
noProxy := shellQuote(proxy.NoProxy)

script := fmt.Sprintf(`set -eux -o pipefail
sudo mkdir -p /etc/systemd/system/containerd.service.d
sudo tee /etc/systemd/system/containerd.service.d/calf-proxy.conf >/dev/null <<'EOF'
[Service]
Environment="HTTP_PROXY=%s"
Environment="HTTPS_PROXY=%s"
Environment="NO_PROXY=%s"
Environment="http_proxy=%s"
Environment="https_proxy=%s"
Environment="no_proxy=%s"
EOF
sudo tee /etc/profile.d/calf-proxy.sh >/dev/null <<'EOF'
export HTTP_PROXY=%s
export HTTPS_PROXY=%s
export NO_PROXY=%s
export http_proxy=%s
export https_proxy=%s
export no_proxy=%s
EOF
sudo systemctl daemon-reload
if systemctl is-active --quiet containerd; then
sudo systemctl restart containerd
fi
if systemctl is-active --quiet docker; then
sudo systemctl restart docker
fi
`,
httpProxy, httpsProxy, noProxy, httpProxy, httpsProxy, noProxy,
httpProxy, httpsProxy, noProxy, httpProxy, httpsProxy, noProxy,
)

_, err := run(ctx, "bash", "-lc", script)
return err
}

func shellQuote(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}

return strings.NewReplacer(`'`, `'"'"'`).Replace(value)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files | rg '^(backend/internal/runtime/proxy\.go|backend/internal/runtime/.*\.go|backend/internal/.*\.go)$'

printf '\n== Outline proxy.go ==\n'
ast-grep outline backend/internal/runtime/proxy.go --view expanded || true

printf '\n== Relevant call sites ==\n'
rg -n "applyProxyInVM|ApplyProxy|ProxyConfig|HTTPProxy|HTTPSProxy|NoProxy|shellQuote" backend/internal/runtime backend/internal -g '*.go'

printf '\n== Candidate validation/config definitions ==\n'
rg -n "type ProxyConfig|struct .*Proxy|Validate\\(|validation|proxy" backend/internal -g '*.go'

Repository: enegalan/calf

Length of output: 15776


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Read proxy.go with line numbers ==\n'
sed -n '1,220p' backend/internal/runtime/proxy.go | cat -n

printf '\n== Read native/lima runtime callers ==\n'
for f in backend/internal/runtime/native.go backend/internal/runtime/lima.go backend/internal/runtime/mock.go; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    sed -n '1,240p' "$f" | cat -n
  fi
done

Repository: enegalan/calf

Length of output: 23679


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Search for proxy validation / sanitization in API and models ==\n'
rg -n "HTTPProxy|HTTPSProxy|NoProxy|ProxyConfig|proxy.*validate|validate.*proxy|TrimSpace\\(|NewReplacer|strings\\.ReplaceAll\\(.*\\n" backend -g '*.go'

printf '\n== Search for write paths to proxy settings ==\n'
rg -n "ApplyProxy\\(|applyProxyInVM\\(|proxy settings|proxy config" backend -g '*.go'

Repository: enegalan/calf

Length of output: 411


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Proxy validation in API handlers ==\n'
sed -n '100,310p' backend/internal/api/handlers.go | cat -n

printf '\n== Proxy config schema ==\n'
sed -n '1,120p' backend/internal/config/config.go | cat -n

Repository: enegalan/calf

Length of output: 9944


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Where validateProxyUpdate is called ==\n'
rg -n "validateProxyUpdate\\(" backend/internal/api/handlers.go backend/internal/api -g '*.go'

printf '\n== Rest of handlers.go around validator ==\n'
sed -n '1,240p' backend/internal/api/handlers.go | cat -n

Repository: enegalan/calf

Length of output: 8679


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat >/tmp/proxy_url_probe.go <<'EOF'
package main

import (
	"fmt"
	"net/url"
)

func main() {
	tests := []string{
		"http://proxy.example.com",
		"http://proxy.example.com/?q=$(id)",
		"http://proxy.example.com/?q=foo;bar",
		"http://proxy.example.com/?q=foo bar",
		"http://proxy.example.com/?q=foo\nbar",
	}
	for _, raw := range tests {
		u, err := url.Parse(raw)
		if err != nil {
			fmt.Printf("PARSE_ERR\t%q\t%v\n", raw, err)
			continue
		}
		fmt.Printf("PARSE_OK\t%q\tscheme=%q host=%q path=%q rawquery=%q\n", raw, u.Scheme, u.Host, u.Path, u.RawQuery)
	}
}
EOF

go run /tmp/proxy_url_probe.go

Repository: enegalan/calf

Length of output: 727


Escape proxy values before interpolating them into the shell script. validateProxyURL() still accepts proxy URLs with shell metacharacters in the query/fragment (for example http://proxy.example.com/?q=$(id) or spaces), and shellQuote() doesn’t make these substitutions shell-safe. On the Native path this runs through bash -lc on the host, so a crafted proxy value can execute commands and persist into /etc/profile.d/calf-proxy.sh and the containerd drop-in.

🤖 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 `@backend/internal/runtime/proxy.go` around lines 15 - 62, The proxy values
interpolated by applyProxyInVM are not fully shell-safe, so malicious characters
can still be executed through the bash -lc script. Fix shellQuote to return a
properly shell-escaped value that handles spaces and shell metacharacters, and
ensure applyProxyInVM uses that escaped output consistently for every
HTTPProxy/HTTPSProxy/NoProxy insertion in both the containerd drop-in and
/etc/profile.d script.

Comment thread ui/lib/app_shell.dart
Comment thread ui/lib/screens/networks_screen.dart
…rk operations

- Updated network handling methods to use context.Background() for better control over timeouts.
- Introduced a new helper function for consistent error handling in network-related API responses.
- Enhanced proxy application logic to log warnings for non-fatal errors during startup.
- Refactored network inspection and removal methods to improve error reporting and handling.
- Updated tests to reflect changes in error handling and context management.
@enegalan enegalan merged commit 8b7cddf into main Jul 5, 2026
3 checks passed
@enegalan enegalan deleted the 27-feat-networks branch July 5, 2026 23:50
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.

feat: Networks

1 participant