Skip to content

fix(sns-cli): resolve shared local network config in vendored dfx-core - #10914

Open
claude[bot] wants to merge 11 commits into
masterfrom
fix-sns-dfx-core-vendored-local-network
Open

fix(sns-cli): resolve shared local network config in vendored dfx-core#10914
claude[bot] wants to merge 11 commits into
masterfrom
fix-sns-dfx-core-vendored-local-network

Conversation

@claude

@claude claude Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What broke

#10640 vendored a subset of dfx-core into rs/sns/dfx-core-vendored, replacing ic-sns-cli's external dfx-core dependency. This broke local-network resolution for downstream consumers, notably dfinity/snsdemo's CI (dfinity/snsdemo#611, dfinity/snsdemo#612), which now fails with:

Error: Failed to build agent for network `local` and identity `None`
...
Caused by:
    0: Failed to fetch root key from network ...
    1: error sending request for url (http://127.0.0.1:8000/api/v2/status)

snsdemo's shared local network is configured (via ~/.config/dfx/networks.json) to bind on 127.0.0.1:8080, not the default 127.0.0.1:8000/127.0.0.1:4943, and snsdemo's project dfx.json does not declare its own networks.local entry.

Why

resolve_local_network() in rs/sns/dfx-core-vendored/src/network.rs decided "project-scoped local network" vs. "shared local network" purely by whether any dfx.json existed above the working directory (via find_project_root()):

let (data_directory, default_address) = match find_project_root() {
    Some(project_root) => (
        project_root.join(".dfx").join("network").join("local"),
        project_local_address(&project_root),
    ),
    None => (
        get_shared_network_data_directory("local")...,
        DEFAULT_SHARED_LOCAL_ADDRESS.to_string(),
    ),
};

This doesn't check whether that dfx.json actually declares its own networks.local entry. A project like snsdemo's, whose dfx.json has no networks key at all, was wrongly routed into the "project-scoped" branch and given the hardcoded 127.0.0.1:8000 default — instead of falling back to the shared network and reading its actually-configured bind address.

This mirrors a gap the PR's own description called out: the shared-network config reading present in the real dfx-core (create_shared_network_descriptor, which reads networks.json's local entry and only falls back to a hardcoded default when that entry is itself absent) was not carried over to the vendored subset. Real dfx-core only takes the project-config branch for a network the project's dfx.json actually declares (create_project_network_descriptor returns None — not an error — when the network isn't present, letting resolution fall through to the shared config).

What this PR changes

In rs/sns/dfx-core-vendored/src/network.rs:

  • Replaced find_project_root() (found a dfx.json, unconditionally treated as project-scoped) with find_project_local_network(), which returns the project root and its configured local bind address only when the nearest dfx.json actually has a networks.local entry. Otherwise it returns None, so resolution falls back to the shared network, matching dfx's create_project_network_descriptor semantics.
  • Added shared_local_address(), which reads the actual configured bind from the shared ~/.config/dfx/networks.json (via get_user_dfx_config_dir()), falling back to the existing 127.0.0.1:4943 default only when that file doesn't exist or has no local entry. Note the shared networks.json has no top-level networks key (it is the network map), unlike a project's dfx.json.
  • Updated resolve_local_network() to use these instead of hardcoding DEFAULT_SHARED_LOCAL_ADDRESS for every dfx-project-adjacent-but-not-declaring case.
  • Updated the module doc comment to describe the corrected behavior.
  • Added unit tests covering: a project dfx.json with no networks key falling back to the shared network's configured bind (the regression case); a project dfx.json that does declare its own local network taking precedence; and the shared-network default applying when neither config declares local.
  • Added a rust_test target to BUILD.bazel for the new tests (serialized via --test-threads=1, since they mutate the process's working directory and the shared-config-directory override).

No change to the external dependency surface — this stays a minimal, in-place bugfix to the vendored subset rather than reintroducing the full dfx-core dependency.

resolve_local_network() in rs/sns/dfx-core-vendored/src/network.rs decided
"project-scoped local network" vs. "shared local network" purely by whether
any dfx.json existed above the working directory, without checking whether
that dfx.json actually declares a networks.local entry. Projects whose
dfx.json has no networks key at all (e.g. snsdemo's) were wrongly treated as
project-scoped and given the hardcoded 127.0.0.1:8000 default, instead of
falling back to the shared network and reading its configured bind from
~/.config/dfx/networks.json (which can be customized, e.g. 127.0.0.1:8080).

This mirrors dfx-core's own create_project_network_descriptor /
create_shared_network_descriptor split: a project's dfx.json only wins for a
network it actually declares; otherwise dfx (and now this vendored subset)
falls back to the shared networks.json, defaulting to 127.0.0.1:4943 only
when that file has no local entry either.

Adds unit tests covering: project dfx.json without a networks key falling
back to the shared network's configured bind; a project dfx.json that does
declare its own local network taking precedence; and the shared-network
default applying when neither config declares local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
@github-actions github-actions Bot added the fix label Jul 27, 2026
Comment thread rs/sns/dfx-core-vendored/BUILD.bazel Outdated
Comment thread rs/sns/dfx-core-vendored/BUILD.bazel Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Per Daniel Wong's review on #10914:

- Rewrite convoluted doc comments (module doc, find_project_local_network,
  shared_local_address) using short declarative sentences.
- Restructure find_project_local_network as a guard-clause loop: the "no
  dfx.json here, try the parent" case returns/continues early, and the
  "found dfx.json" case is the fallthrough with least indentation.
- Replace and_then/map combinator chains for reading dfx.json and
  networks.json with explicit match statements and early return.
- Distinguish "dfx.json/networks.json absent or has no local network"
  (expected, falls through to None/default) from "present but malformed
  JSON, or bind is present with the wrong type" (a real error, now
  surfaced via new NetworkResolutionError variants instead of being
  silently treated the same as "not found").
- Restructure the "bind" lookup as two statements (look up the key, then
  decide what to do with it) instead of one long dot chain.
- Move the test module into its own network_tests.rs file, referenced via
  #[path = "network_tests.rs"], and hoist the DFX_CONFIG_ROOT use to the
  top of that file.
- Use distinctive, non-coincidental bind values in test fixtures/asserts
  (e.g. "shared:2718", "dfx-json:9999") so a passing test can't be an
  accident; strengthen the precedence test to also configure a distinct
  shared network; and change the "no local network" test to use a
  dfx.json/networks.json that exist but don't declare local, instead of
  files that don't exist at all.
- Drop the stale "regression test" comment and the post-assert manual
  temp-dir cleanup (redundant, and skipped whenever the assert fails).
- BUILD.bazel: exclude test-only sources from the main rust_library, and
  give dfx-core-vendored_test its own srcs/deps (matching rs/sns/cli's
  pattern) instead of depending on `crate`. Lead the --test-threads=1
  comment with the important reason (tests mutate the file system).

Verified via a standalone scratch crate (workspace cargo is blocked by an
unrelated private-dep 403): cargo check, cargo test -- --test-threads=1
(3/3 passing), and rustfmt --check all clean. bazel is unavailable in this
sandbox, so the BUILD.bazel restructuring is checked by inspection against
rs/sns/cli's BUILD.bazel only, not by an actual bazel build/test run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
@daniel-wong-dfinity-org-twin
daniel-wong-dfinity-org-twin marked this pull request as ready for review July 27, 2026 14:13
@daniel-wong-dfinity-org-twin
daniel-wong-dfinity-org-twin requested a review from a team as a code owner July 27, 2026 14:13
github-actions[bot]

This comment was marked as resolved.

@claude

This comment was marked as resolved.

Per Daniel Wong's review on #10914: the per-test temp directories weren't
actually the whole isolation story. find_project_local_network() read
std::env::current_dir(), and shared_local_address() (via
get_user_dfx_config_dir()) read the process-global DFX_CONFIG_ROOT static.
Tests pointed both at their own temp dirs by mutating this global state
(set_current_dir, and swapping DFX_CONFIG_ROOT's Mutex contents), which
raced across parallel test threads even though each test's own files were
isolated.

Both functions had exactly one production call site each, entirely within
this crate, so thread the values through as explicit parameters instead:

- find_project_local_network now takes start_dir: &Path instead of calling
  std::env::current_dir() itself.
- get_user_dfx_config_dir_with_override is a new sibling of
  get_user_dfx_config_dir that takes the config root override as a
  parameter instead of reading DFX_CONFIG_ROOT; the original stays as a
  thin wrapper for the two existing (unchanged) production callers.
- shared_local_address takes an Option<&Path> override, using the ambient
  DFX_CONFIG_ROOT-backed path when None.
- resolve_local_network is now a thin wrapper around
  resolve_local_network_with(start_dir, config_root_override), which the
  tests call directly with explicit temp paths.

Tests no longer touch any process-global state, so --test-threads=1 comes
off the Bazel target. Verified with a standalone scratch crate (real Bazel
is unavailable in this sandbox): cargo check/clippy/test all clean, and 30
repeated `cargo test -- --test-threads=8` runs all passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
Comment thread rs/sns/dfx-core-vendored/BUILD.bazel Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network_tests.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
claude and others added 2 commits July 28, 2026 10:18
…ored network resolution

Per Daniel Wong's round-4 review on #10914:

- Verified each new NetworkResolutionError variant against real dfx-core
  (dfx-core 0.4.0, commit 9f41b205390626aa358e8a2a96498bb26e632f13):
  Config::from_file/from_slice and NetworksConfig::new/from_file always
  propagate a read or deserialization failure as Err (dfx.json/networks.json
  are parsed into typed structs, so a wrong-typed "bind" fails the same way).
  Added ReadProjectDfxJsonFailed and ReadSharedNetworksJsonFailed so a read
  failure on an existing (but unreadable) file is a real Err instead of being
  swept under the rug -- previously it was treated as "keep walking up to the
  parent directory looking for another dfx.json", which is invented behavior
  that has no dfx-core equivalent and is worse than the original pre-refactor
  behavior (which just returned None immediately). networks.json's "doesn't
  exist" vs. "exists but unreadable" cases are now distinguished the same way
  dfx-core's NetworksConfig::new does.
- Simplified two `match`-based error-mapping blocks to `.map_err(...)?`.
- Comment cleanup: added "Read it.", "Parse dfx.json.", and "Get network.local
  out of dfx.json..." comments; dropped a now-redundant "Look up bind ..."
  comment and reworded its neighbor; dropped an unnecessary second sentence
  from shared_local_address's doc comment.
- network_tests.rs: replaced the hand-rolled unique_temp_dir helper with
  tempfile::TempDir (already a workspace dependency, and already used
  elsewhere in rs/sns) for automatic cleanup, and folded the last test's
  intermediate `result` binding into a single, full-struct assert_eq.
- BUILD.bazel: adopted the DEPENDENCIES/DEV_DEPENDENCIES pattern (still in
  active use for rs/sns et al., see rs/nervous_system/feature_test.md) to
  de-duplicate deps between the library and test targets, and added tempfile
  to DEV_DEPENDENCIES.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
@daniel-wong-dfinity-org-twin
daniel-wong-dfinity-org-twin dismissed github-actions[bot]’s stale review July 28, 2026 11:05

Does not affect canister behavior.

Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/config/directories.rs
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment on lines +56 to +88
// dfx-core's `Config::from_file` propagates a read failure on an existing
// dfx.json as an Err (via `crate::fs::read(path)?`), rather than treating
// it the same as dfx.json being absent.
#[error("Failed to read {0}")]
ReadProjectDfxJsonFailed(PathBuf, #[source] std::io::Error),

// dfx-core's `Config::from_slice` deserializes dfx.json into a typed
// `ConfigInterface` and propagates any resulting serde error as an Err.
#[error("Failed to parse {0} as JSON")]
ParseProjectDfxJsonFailed(PathBuf, #[source] serde_json::Error),

// Same fidelity note as `ParseProjectDfxJsonFailed`: dfx-core's
// `ConfigLocalProvider::bind` is a typed `Option<String>` field, so a
// non-string "bind" fails that same `Config::from_slice` deserialization.
#[error("{path}'s local network has a \"bind\" value that is not a string: {value}")]
InvalidProjectLocalNetworkBind {
path: PathBuf,
value: serde_json::Value,
},

// dfx-core's `NetworksConfig::new` only defaults when networks.json does
// not exist. If it exists but can't be read, `NetworksConfig::from_file`
// propagates the error (via `crate::fs::read(path)?`) instead of
// defaulting.
#[error("Failed to read {0}")]
ReadSharedNetworksJsonFailed(PathBuf, #[source] std::io::Error),

// Mirrors dfx-core's `NetworksConfig::from_file`, which propagates a
// deserialization error as an Err.
#[error("Failed to parse {0} as JSON")]
ParseSharedNetworksJsonFailed(PathBuf, #[source] serde_json::Error),

// Same fidelity note as `InvalidProjectLocalNetworkBind`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is all of this copied from dfx-core?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Breaking down the whole NetworkResolutionError enum, since that's what's in view here:

  • NetworkNotFound, DetermineSharedNetworkDirectoryFailed, ReadWebserverPortFailed, ParsePortValueFailed — copied (name + shape) from NetworkConfigError (dfx-core's error/network_config.rs).
  • DetermineCurrentWorkingDirFailed — copied verbatim (name + message) from LoadDfxConfigError::DetermineCurrentWorkingDirFailed (error/load_dfx_config.rs, used by Config::from_current_dir).
  • DetermineSharedConfigDirectoryFailed — NOT copied, bespoke. dfx-core wraps this transparently as NetworkConfigError::Config(#[from] ConfigError); we give it its own name since this crate doesn't use #[from]-transparent wrapping here.
  • ReadProjectDfxJsonFailed, ParseProjectDfxJsonFailed, InvalidProjectLocalNetworkBind, and their shared-network counterparts (ReadSharedNetworksJsonFailed, ParseSharedNetworksJsonFailed, InvalidSharedLocalNetworkBind) — NOT copied, bespoke. dfx-core has no equivalents at this layer at all: it loads dfx.json/networks.json through separate Config/NetworksConfig model types (errors: LoadDfxConfigError, LoadNetworksConfigError/StructuredFileError) that network/provider.rs never sees directly. This crate folds that read+parse step straight into network.rs, so there's no upstream variant to copy — only the underlying behavior (propagate as Err, don't swallow) is verified against dfx-core, per r3664702254.

Summary: behavior is checked against dfx-core throughout; names are copied where dfx-core has a matching variant at this layer, bespoke where it spreads that failure across model types this crate doesn't replicate.

This comment was marked as duplicate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, it didn't match before this round. Restructured it to actually copy dfx-core's real layered types instead.

Went back to real dfx-core (src/dfx-core/src/network/provider.rs, config/model/dfinity.rs, error/{load_dfx_config,load_networks_config,structured_file}.rs) to check each bespoke variant individually:

  • ReadProjectDfxJsonFailed / ParseProjectDfxJsonFailed -- these DO have a real upstream equivalent I missed last round: LoadDfxConfigError::ReadFile(#[from] ReadFileError) and LoadDfxConfigError::DeserializeValueFailed(Box<PathBuf>, serde_json::Error), from Config::from_file/from_slice. Added error/load_dfx_config.rs with a trimmed LoadDfxConfigError (name + shape copied), and replaced both variants with NetworkResolutionError::LoadProjectDfxJsonFailed(#[from] LoadDfxConfigError).
  • ReadSharedNetworksJsonFailed / ParseSharedNetworksJsonFailed / DetermineSharedConfigDirectoryFailed -- same story: real upstream equivalent is LoadNetworksConfigError::GetConfigPathFailed(ConfigError) / LoadConfigFromFileFailed(StructuredFileError), from NetworksConfig::new/from_file. Added error/load_networks_config.rs with LoadNetworksConfigError (copied name + shape), replaced all three with NetworkResolutionError::LoadSharedNetworksConfigFailed(#[from] LoadNetworksConfigError).
  • InvalidProjectLocalNetworkBind / InvalidSharedLocalNetworkBind -- re-verified there's still no dedicated upstream variant for these, but now I see why: dfx-core deserializes dfx.json/networks.json straight into a typed ConfigInterface/NetworksConfigInterface (where bind: Option<String>), so a non-string "bind" fails as part of the ordinary DeserializeValueFailed/DeserializeJsonFileFailed parse -- it's not sufficiently distinct there to warrant its own variant. Our code was hand-walking serde_json::Value, which is what forced a bespoke "invalid bind" variant to exist at all. Fixed by deserializing into small typed structs (ProjectDfxJson, LocalNetworkBind, mirroring ConfigNetwork::ConfigLocalProvider) via the crate's existing load_json_file/StructuredFileError helper (already used elsewhere in this crate, e.g. identity_manager.rs) -- so a non-string bind now fails the same typed deserialization dfx-core would fail, and both bespoke variants are gone entirely.

NetworkNotFound, DetermineCurrentWorkingDirFailed, DetermineSharedNetworkDirectoryFailed, ReadWebserverPortFailed, ParsePortValueFailed are unchanged -- these were already exact copies, reconfirmed.

Net: NetworkResolutionError shrank from 11 variants to 7, and every variant now either is an exact copy or wraps a real upstream error type by name and structure. Added two regression tests for the non-string-bind path (project + shared) since removing the bespoke variants changed which type surfaces that error.

@daniel-wong-dfinity-org-twin daniel-wong-dfinity-org-twin Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So, we are now matching dfx-core, right?? Again, this is a yes or no question, so please, do not spew a giant mountain of text on me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Found one real remaining divergence in this enum, now fixed (28492d9):

ReadWebserverPortFailed and ParsePortValueFailed were hand-rolling (PathBuf, io::Error)/(PathBuf, ParseIntError) pairs, and get_running_webserver_address called raw std::fs::read_to_string directly — instead of reusing this crate's own ReadToStringError (error/fs.rs) and crate::fs::read_to_string helper, which already exist in this crate and already mirror dfx-core's fs::read_to_string/ReadToStringError (src/dfx-core/src/fs/mod.rs, error/fs.rs).

Real dfx-core (error/network_config.rs):

ReadWebserverPortFailed(#[source] ReadToStringError),
...
#[error("Failed to parse contents of {0} as a port value")]
ParsePortValueFailed(Box<PathBuf>, #[source] Box<ParseIntError>),

and network/provider.rs::get_running_webserver_bind_address:

let s = crate::fs::read_to_string(&path).map_err(ReadWebserverPortFailed)?;
...
.map_err(|e| ParsePortValueFailed(Box::new(path), Box::new(e)))?;

Changed our ReadWebserverPortFailed to wrap ReadToStringError (dropping the separate PathBuf field, since ReadToStringError already carries the path), boxed both ParsePortValueFailed fields to match, matched its exact message text ("Failed to parse contents of {0} as a port value"), and switched get_running_webserver_address to call crate::fs::read_to_string instead of std::fs::read_to_string.

Everything else in the enum: re-checked against error/network_config.rs, error/load_dfx_config.rs, error/load_networks_config.rs line by line again and it's unchanged from the breakdown in r3706320341/r3711753026 — NetworkNotFound, DetermineSharedNetworkDirectoryFailed are exact copies of NetworkConfigError variants; DetermineCurrentWorkingDirFailed is an exact copy of LoadDfxConfigError::DetermineCurrentWorkingDirFailed; LoadProjectDfxJsonFailed/LoadSharedNetworksConfigFailed wrap the real, separately-vendored LoadDfxConfigError/LoadNetworksConfigError types by name and shape. No remaining bespoke/invented variants at this layer. is_ic, the four DEFAULT_* constants, resolve_network, and get_running_webserver_address's core logic (including the localhost:port[::1] comment) were also re-diffed against network_descriptor.rs/config/model/dfinity.rs/network/provider.rs and already match exactly.

(Replying here per your note that this is the same underlying issue as the newer comment on line 70 — leaving a short pointer over there.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes.

Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
…DFX_CONFIG_ROOT ambiently

- resolve_local_network now propagates a std::env::current_dir() failure as
  Err(DetermineCurrentWorkingDirFailed), matching dfx-core's
  Config::from_current_dir instead of silently treating it as "no project
  root found". resolve_local_network_with's start_dir is now &Path, not
  Option<&Path>.
- resolve_local_network now reads the DFX_CONFIG_ROOT override itself and
  passes it explicitly; shared_local_address no longer falls back to the
  ambient get_user_dfx_config_dir() when None, so None unambiguously means
  "DFX_CONFIG_ROOT is not set" rather than also meaning "check ambient state
  here".
- Fix networks.local typo in a code comment.
- Add a doc comment on get_user_dfx_config_dir stating its common-case return
  value (~/.config/dfx).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
.join(network))
}

/// Returns the user's dfx config directory. Usually `~/.config/dfx`: see

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Returns the user's dfx config directory.

This is vacuous. Never do this type of shit. All this does is take the name of the code, and turns it into proper prose. Nobody benefits from that. Never ever do this horrible garbage.

Usually, the alternative is to define jargon used by the name. In this case, we could try to answer, "WTF even IS a 'user dfx config directory'??" It is not merely any old directory where a "user" might decide to randomly shove "configuration" for dfx. Rather, it is a special place that dfx goes into to look for networks.json (and other shit?). From the typical value, ~/.config/dfx, people would generally be able to figure this out. Therefore, a full explanation is probably not needed in this case. I was just using it as an example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deleted the vacuous sentence entirely — kept only the "usually ~/.config/dfx, see get_user_dfx_config_dir_with_override" part (directories.rs:29-31), per your clarification that no further explanation is needed here.

Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment on lines +56 to +88
// dfx-core's `Config::from_file` propagates a read failure on an existing
// dfx.json as an Err (via `crate::fs::read(path)?`), rather than treating
// it the same as dfx.json being absent.
#[error("Failed to read {0}")]
ReadProjectDfxJsonFailed(PathBuf, #[source] std::io::Error),

// dfx-core's `Config::from_slice` deserializes dfx.json into a typed
// `ConfigInterface` and propagates any resulting serde error as an Err.
#[error("Failed to parse {0} as JSON")]
ParseProjectDfxJsonFailed(PathBuf, #[source] serde_json::Error),

// Same fidelity note as `ParseProjectDfxJsonFailed`: dfx-core's
// `ConfigLocalProvider::bind` is a typed `Option<String>` field, so a
// non-string "bind" fails that same `Config::from_slice` deserialization.
#[error("{path}'s local network has a \"bind\" value that is not a string: {value}")]
InvalidProjectLocalNetworkBind {
path: PathBuf,
value: serde_json::Value,
},

// dfx-core's `NetworksConfig::new` only defaults when networks.json does
// not exist. If it exists but can't be read, `NetworksConfig::from_file`
// propagates the error (via `crate::fs::read(path)?`) instead of
// defaulting.
#[error("Failed to read {0}")]
ReadSharedNetworksJsonFailed(PathBuf, #[source] std::io::Error),

// Mirrors dfx-core's `NetworksConfig::from_file`, which propagates a
// deserialization error as an Err.
#[error("Failed to parse {0} as JSON")]
ParseSharedNetworksJsonFailed(PathBuf, #[source] serde_json::Error),

// Same fidelity note as `InvalidProjectLocalNetworkBind`.

This comment was marked as duplicate.

Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment on lines +161 to +166
/// Does the actual work of [`resolve_local_network`], but takes the working
/// directory and the `DFX_CONFIG_ROOT` override as explicit parameters
/// instead of reading them from process-global state (`std::env::current_dir`,
/// and the `DFX_CONFIG_ROOT` mutex). This lets tests supply both directly,
/// instead of mutating that global state, which would otherwise race across
/// parallel test threads.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Does the actual work of [`resolve_local_network`], but takes the working
/// directory and the `DFX_CONFIG_ROOT` override as explicit parameters
/// instead of reading them from process-global state (`std::env::current_dir`,
/// and the `DFX_CONFIG_ROOT` mutex). This lets tests supply both directly,
/// instead of mutating that global state, which would otherwise race across
/// parallel test threads.
/// Does the actual work of [`resolve_local_network`], but takes the working
/// directory and the `DFX_CONFIG_ROOT` environment variable override as explicit parameters
/// instead of reading them, making this more testable.

(Per standing order, reflow as necessary.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied verbatim (network.rs:162-164).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reflow!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied verbatim last round, but that left an unwrapped 92-character line — actually reflowed now (28492d9).

Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
let start_dir = std::env::current_dir()
.map_err(NetworkResolutionError::DetermineCurrentWorkingDirFailed)?;
let config_root_override = DFX_CONFIG_ROOT.lock().unwrap().clone();
resolve_local_network_with(&start_dir, config_root_override.as_deref().map(Path::new))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you move the dot stuff to the previous statement?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — .as_deref().map(Path::new) now its own statement right after the let dfx_config_root = DFX_CONFIG_ROOT.lock().unwrap().clone(); line, instead of being inlined into the resolve_local_network_with(...) call (network.rs:154-159).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No, I mean, can't you just do

let dfx_config_root = one
    .big()
    .long()
    .dot()
    .chain();

not two separate ones?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — single statement now (network.rs:165). Chaining .as_deref().map(Path::new) directly onto the .clone() temporary in one let doesn't compile (E0716, temporary dropped while borrowed), so instead the single statement produces an owned Option<PathBuf> via .map(PathBuf::from), and .as_deref() moves to the call site: resolve_local_network_with(&working_directory, dfx_config_root.as_deref()).

Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
claude added 3 commits August 4, 2026 11:06
…olution

- directories.rs: delete vacuous "Returns the user's dfx config directory."
  sentence; keep the non-vacuous "usually ~/.config/dfx" explanation.
- network.rs: rename start_dir -> working_directory and
  config_root_override -> dfx_config_root throughout; reflow the
  resolve_local_network_with doc per suggestion; trim the
  find_project_local_network doc to end after "the user's project.";
  drop the ", rather than ... value directly." aside from
  shared_local_address's doc; replace every "plays the role of" phrasing
  with a direct statement of what value is passed; attach
  .as_deref().map(Path::new) to its own statement instead of the call site.
- Restructure NetworkResolutionError's JSON-loading variants to actually
  mirror dfx-core's real layering instead of inventing flat ones: add
  error/load_dfx_config.rs (LoadDfxConfigError, copied name+shape from
  dfx-core) and error/load_networks_config.rs (LoadNetworksConfigError,
  ditto), and deserialize dfx.json/networks.json into small typed structs
  (mirroring ConfigNetwork::ConfigLocalProvider) via the crate's existing
  load_json_file/StructuredFileError helper, so a non-string "bind" now
  fails the same typed deserialization dfx-core would fail, instead of
  needing a bespoke InvalidLocalNetworkBind variant.
- Add regression tests for the non-string "bind" error path on both the
  project and shared JSON files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
…ndored-local-network

# Conflicts:
#	Cargo.lock
ReadWebserverPortFailed/ParsePortValueFailed were hand-rolling a
(PathBuf, io::Error) pair and calling raw std::fs::read_to_string,
instead of reusing this crate's own ReadToStringError (error/fs.rs)
and crate::fs::read_to_string helper -- both of which already mirror
dfx-core's fs::read_to_string/ReadToStringError. Real dfx-core's
NetworkConfigError::ReadWebserverPortFailed wraps ReadToStringError
(carrying the path itself) rather than a bare io::Error, and
ParsePortValueFailed boxes its PathBuf/ParseIntError fields with the
message "Failed to parse contents of {0} as a port value"
(error/network_config.rs). Copied both exactly, and switched
get_running_webserver_address to use crate::fs::read_to_string.

Also reflowed the resolve_local_network_with doc comment, which had
an unwrapped 92-character line left over from applying a review
suggestion verbatim without reflowing it as the suggestion itself
asked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment on lines +208 to +209
/// parsed as JSON -- including a "bind" value that is present but not a
/// string, which fails that same parse. Unlike "no dfx.json" or "dfx.json has

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of em dash, just explain what happens when "bind" is not a string in a separate sentence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — network.rs:213-216, now: "Returns Err if the nearest dfx.json exists, but cannot be read or parsed as JSON. A "bind" value that is present but not a string fails that same parse, so it returns Err too."

Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment on lines +235 to +233
// Found the nearest dfx.json! Read it.
let content = std::fs::read(&dfx_json).map_err(|err| {
NetworkResolutionError::ReadProjectDfxJsonFailed(dfx_json.clone(), err)
})?;

// Parse dfx.json.
let dfx_json_value: serde_json::Value =
serde_json::from_slice(&content).map_err(|err| {
NetworkResolutionError::ParseProjectDfxJsonFailed(dfx_json.clone(), err)
})?;

// Get network.local out of dfx.json. If it doesn't exist, return None.
let Some(local) = dfx_json_value
.get("networks")
.and_then(|networks| networks.get("local"))
else {
// Found the nearest dfx.json! Read and parse it.
let project_dfx_json: ProjectDfxJson = load_dfx_json(&dfx_json)?;

// Get networks.local out of dfx.json. If it doesn't exist, return None.
let Some(local) = project_dfx_json.networks.get("local") else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This change is more radical than I had expected. Why did you do it this way instead of something more incremental? I'm not asking you to back out of this radical change. Just asking a question.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because an incremental patch (keeping manual serde_json::Value navigation, just adding a hand-rolled "is this a string" check for bind) would itself have been invented behavior with no real dfx-core equivalent — the exact thing you flagged earlier in this review (r3711415583: "you invented some shit here").

dfx-core doesn't have a dedicated non-string-bind check anywhere: it deserializes dfx.json/networks.json straight into typed ConfigInterface/NetworksConfigInterface structs, where bind: Option<String> is an ordinary typed field, so a wrongly-typed value just fails the normal deserialization dfx-core already does for any other reason (missing field, wrong type, etc.). To actually reproduce that — not just approximate its outward behavior — this code had to deserialize into equivalent typed structs (ProjectDfxJson/LocalNetworkBind), which in turn meant using the real, separately-vendored LoadDfxConfigError/LoadNetworksConfigError types dfx-core produces from that deserialization, instead of the bespoke flat variants (InvalidProjectLocalNetworkBind etc.) added in the previous round.

Comment thread rs/sns/dfx-core-vendored/src/network.rs Outdated
Comment on lines +287 to +309
/// `config_root_override`, when `Some`, is used in place of the shared dfx
/// config directory's usual location, the same way the real `DFX_CONFIG_ROOT`
/// environment variable would. This lets a caller (in particular, a test)
/// supply a value directly, instead of mutating the process-global
/// `DFX_CONFIG_ROOT`, which would otherwise race across parallel test
/// threads. `None` uses the ambient `DFX_CONFIG_ROOT` override, if any (see
/// `get_user_dfx_config_dir`).
fn shared_local_address(
config_root_override: Option<&Path>,
) -> Result<String, NetworkResolutionError> {
let networks_json = match config_root_override {
Some(config_root_override) => {
get_user_dfx_config_dir_with_override(Some(config_root_override))
}
None => get_user_dfx_config_dir(),
}
.map_err(NetworkResolutionError::DetermineSharedConfigDirectoryFailed)?
.join("networks.json");
/// `dfx_config_root` is the value of the `DFX_CONFIG_ROOT` environment
/// variable, read by the caller (`resolve_local_network`) and passed in
/// explicitly. When `None`, `DFX_CONFIG_ROOT` is not set, and the shared dfx
/// config directory falls back to its real default location (see
/// `get_user_dfx_config_dir_with_override`).
fn shared_local_address(dfx_config_root: Option<&Path>) -> Result<String, NetworkResolutionError> {
let networks = load_shared_networks_config(dfx_config_root)?;

let Some(local) = networks.get("local") else {
// No "local" entry: use the default.
return Ok(DEFAULT_SHARED_LOCAL_ADDRESS.to_string());
};

// Fall back to default.
Ok(local
.bind
.clone()
.unwrap_or_else(|| DEFAULT_SHARED_LOCAL_ADDRESS.to_string()))
}

/// Reads and parses the shared `networks.json`, defaulting to an empty map
/// when the file doesn't exist. Mirrors dfx-core's `NetworksConfig::new` +
/// `NetworksConfig::from_file`.
fn load_shared_networks_config(
dfx_config_root: Option<&Path>,
) -> Result<SharedNetworksJson, LoadNetworksConfigError> {
let networks_json = get_user_dfx_config_dir_with_override(dfx_config_root)
.map_err(LoadNetworksConfigError::GetConfigPathFailed)?
.join("networks.json");

if !networks_json.is_file() {
// No networks.json: use the default. Mirrors dfx-core's
// `NetworksConfig::new`, which only defaults when the file doesn't
// exist -- if it exists but can't be read, that's an Err instead (see
// `ReadSharedNetworksJsonFailed` below).
return Ok(DEFAULT_SHARED_LOCAL_ADDRESS.to_string());
// exist -- if it exists but can't be read or parsed, that's an Err
// instead (see `LoadConfigFromFileFailed` below).
return Ok(SharedNetworksJson::new());
}

// Read it.
let content = std::fs::read(&networks_json).map_err(|err| {
NetworkResolutionError::ReadSharedNetworksJsonFailed(networks_json.clone(), err)
})?;

// Parse networks.json.
let networks_json_value: serde_json::Value =
serde_json::from_slice(&content).map_err(|err| {
NetworkResolutionError::ParseSharedNetworksJsonFailed(networks_json.clone(), err)
})?;

let Some(local) = networks_json_value.get("local") else {
// No "local" entry: use the default.
return Ok(DEFAULT_SHARED_LOCAL_ADDRESS.to_string());
};

let bind = local.get("bind");
// Fall back to default. If it's not a string, return Err.
let bind = match bind {
None => DEFAULT_SHARED_LOCAL_ADDRESS.to_string(),
Some(bind) => match bind.as_str() {
Some(bind) => bind.to_string(),
None => {
return Err(NetworkResolutionError::InvalidSharedLocalNetworkBind {
path: networks_json,
value: bind.clone(),
});
}
},
};

Ok(bind)
load_json_file(&networks_json).map_err(LoadNetworksConfigError::LoadConfigFromFileFailed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ditto.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — network.rs:310-311, now: "...which only defaults when the file doesn't exist. If it exists but can't be read or parsed, that's an Err instead..."

…g em dashes

Per Daniel Wong's review on #10914:

- resolve_local_network: merge the two `let dfx_config_root` statements
  into one. Chaining `.as_deref().map(Path::new)` directly onto the
  `DFX_CONFIG_ROOT.lock().unwrap().clone()` temporary in a single `let`
  doesn't compile (E0716: temporary value dropped while borrowed, since
  the cloned `Option<OsString>` only lives to the end of that statement
  but the derived `Option<&Path>` needs to survive into the next line).
  Fixed by binding an owned `Option<PathBuf>` instead and deferring
  `.as_deref()` to the call site, which is one statement and compiles.
- find_project_local_network doc comment: replace the em-dash aside
  about a non-string "bind" value with its own declarative sentence.
- load_shared_networks_config: same em-dash-to-sentence fix in the
  networks.json-not-found comment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants