Skip to content

Contributing

Wayland Core is Apache-2.0. Contributions follow the rules in AGENTS.md at the repo root, which is the operative spec for coding agents and human contributors alike. The notes below highlight the architectural contracts most likely to cause a CI failure if you miss them.

Before writing code, read the files you will touch and the files that call them. The crate map in Architecture Overview describes where new functionality belongs. The rule is: place it in the lowest crate where it semantically belongs. Do not create a new crate for a single shared function.

The full workflow is:

Terminal window
cargo build # verify it compiles
cargo nextest run # run tests (preferred over cargo test)
cargo clippy # must pass zero warnings
cargo fmt --all # must produce no diff
just push # lint-fix + fmt + auto-commit-fixes + test + git push

CI runs on macOS, Linux, and Windows. A change that compiles only on one platform will fail on the other two.

Tools in wcore-tools are registered at agent startup in wcore-agent/src/bootstrap.rs. A tool that is registered is expected to be fully functional. Tools that depend on external credentials or system capabilities (API keys, a running sidecar, a platform-specific binary) are hidden when those dependencies are absent rather than registered as stubs that return errors.

Concretely: if you add a tool that requires SOME_API_KEY, gate its registration on std::env::var("SOME_API_KEY").is_ok() in bootstrap.rs. Do not register it unconditionally and return NOT_IMPLEMENTED from the handler. The agent’s tool list should reflect only what is actually callable in the current environment.

The same contract applies to MCP server tools. The default_tool_set() path in wcore-mcp/src/server.rs currently returns an empty set; see the status matrix for the current state of MCP server tool dispatch.

Two structural lints are enforced in clippy.toml and fail cargo clippy -D warnings in CI.

Every outbound HTTP request must go through wcore_egress::EgressClient. Constructing a raw reqwest::Client anywhere else in the workspace is banned via disallowed-methods:

clippy.toml
disallowed-methods = [
{ path = "reqwest::Client::new", reason = "construct via wcore_egress::EgressClient" },
{ path = "reqwest::Client::builder", reason = "construct via wcore_egress::EgressClient::builder" },
{ path = "reqwest::ClientBuilder::new", reason = "construct via wcore_egress::EgressClient::builder" },
{ path = "reqwest::get", reason = "construct via wcore_egress::EgressClient" },
]

The lint matches by resolved path, so it catches aliased imports (use reqwest::Client as HttpClient) that a text grep would miss. The only sanctioned exceptions are wcore-egress/src/client.rs (which implements the chokepoint) and one self-test in wcore-providers/src/retry.rs; both carry #[allow(clippy::disallowed_methods)] with an explanatory comment.

If you add a new crate that makes outbound HTTP calls, depend on wcore-egress and use EgressClient::builder(). Do not add a new egress exception without a review comment explaining why the egress policy cannot apply.

The workspace uses wcore_config::shell helpers for all process spawning. Direct use of Command::new("sh"), Command::new("bash"), or Command::new("cmd") is forbidden. For LLM-supplied arguments, use shell_command_argv(program, &[args]) (argv mode: no shell interpreter, metacharacters in arguments are never interpreted). Use shell_command(str) only for cases where the shell semantics are genuinely required (pipeline, redirection) and never interpolate LLM-supplied data into the string.

Provider quirks: use ProviderCompat, not conditionals

Section titled “Provider quirks: use ProviderCompat, not conditionals”

The most important architectural rule in the codebase: do not add hardcoded conditionals that detect a specific provider by URL or name. Instead, add a field to ProviderCompat in wcore-config/src/compat.rs, set its default in the appropriate preset function (e.g. openai_defaults()), and read it in the provider implementation via self.compat.field_name. This keeps all provider differences as config data rather than code branches.

When a capability exists in the codebase but is not yet fully wired to production call sites, it must be gated rather than shipped as a quietly non-functional path.

The patterns used in the codebase:

  • Cargo feature flags. Capabilities with significant compile-time deps (OTLP, Landlock, seccomp, chromiumoxide) are off by default and require an explicit --features flag. The feature flag name and its effect must be documented.
  • Env-gated runtime availability. Tools and channel adapters that need credentials check for them at startup and are absent from the tool list if the credential is missing. This is the NO-STUBS contract applied at runtime.
  • :::caution in docs. Any capability that is partial, has a known gap, or behaves differently from its description in some configurations gets a Starlight :::caution callout in its documentation page stating the real status. The status matrix is the canonical list.
  • Source-level // TODO comments with tracking references. Stubs in progress reference the task ID (e.g. // W4-TODO: wire to production call sites). A bare todo!() macro in non-test production code is a build failure (the codebase convention is to avoid it; use a typed error return instead).

Run cargo metadata before adding a new dependency to verify the change fits the dependency graph. The rules:

  • Bottom-layer crates (wcore-types, wcore-compact) must not gain internal wcore-* deps.
  • Plugin crates (wayland-*) must not depend on wcore-agent, wcore-tools, wcore-browser, or wcore-cua. Use the mirror types in wcore-plugin-api instead.
  • Circular dependencies fail the build. The wcore-plugin-api build.rs lint enforces the plugin isolation boundary.

Unit tests live in #[cfg(test)] blocks in the same file as the code they test. Integration tests live in crates/<crate>/tests/. Write tests from the spec, not from reading the implementation. Every test must check a meaningful behavior or edge case; tests that assert only the happy path without checking boundaries or error conditions are not useful.

Tests that require live external processes (real LLM API, running Chromium, Docker) must be gated behind a feature flag and excluded from the default cargo test run. See the browser-live-tests, live-docker, and harness-failure-injection features in their respective Cargo.toml files for examples of this pattern.

Commit message subject under 72 characters. Body explains the why, not the what. No “update file” or “fix bug” messages. The existing history in the repo shows the expected style; match it. Do not add Co-Authored-By: lines unless the existing history uses them.

Use just push instead of git push to run the full lint-fix + fmt + test cycle before the push lands in CI.