Skip to content

refactor(daemon): Cargo workspace — relay-policy and relay-ops read-only by clippy, fuzzable crates - #962

Merged
rapp992 merged 3 commits into
mainfrom
loop/issue-889-step2
Oct 9, 2026
Merged

rapp992 merged 3 commits into
mainfrom
loop/issue-889-step2

Conversation

@rapp992

@rapp992 rapp992 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Closes #889

This is step 2 of 2. Step 1 was #935 (merged).

⚠ This PR edits docs/agents/security-surface.txt, retiring moved paths only. The maintainer authorised this pattern in step 1.

  • Retired: daemon/src/{executor,cancel,raw_exec,templates,tunnel,identity,enroll,renew}.rs and daemon/src/operations/**. Every one of these files moved under daemon/crates/, which daemon/crates/** already covers.
  • Added: one sentence in the 2026-10-07 note.
  • Nothing else changes. Every bin path that stayed is still listed.

What changed

The relay-daemon's execution, Operation and transport code now lives in three workspace crates:

  • relay-exec is the only crate that may spawn a process.
  • relay-ops cannot write files, enforced by clippy.
  • relay-transport holds the sanctioned identity writer.

The bin keeps its name, its path and its release build (-p relay-daemon). main.rs, dispatch.rs, scheduler.rs, dedup.rs, shutdown.rs, config.rs, selfref.rs, services.rs, policy_check.rs, version.rs and catalog.rs stay in the bin.

Runtime behaviour does not change. git diff -M shows 17 renames at 76–100% similarity (renew.rs is a 100% rename).

Crate layout

Crate Holds Clippy
relay-exec executor, cancel, not_executed, limits (ResourceLimits and clean_io_error, moved from config.rs) The read-only config, exec ban included. Its one sanctioned Command::new keeps its statement-level allow.
relay-ops operations/* (registry, schema, the four Natives, template), templates, raw_exec The read-only config.
relay-transport tunnel, identity, enroll, renew, rand_unit No own config; it inherits the root exec ban. It cannot be read-only: identity writes the node key and cert.

Crate graph:

  • relay-exec → relay-proto
  • relay-ops → relay-proto, relay-policy, relay-exec
  • relay-transport → relay-proto, relay-log
  • the bin → all of the above

Edits that are not byte-identical moves

Four would-be dependency cycles forced these:

  • ResourceLimits and clean_io_error moved to relay_exec::limits. MAX_OUTPUT_BYTES stays in config.rs.
  • EnrollConfig::from_config became the bin's config::enroll_config. The body is identical; its three tests moved with it, keeping their names.
  • The READ_CONCURRENCY definition moved to relay-ops registry; scheduler re-exports it.
  • rand_unit moved to the relay-transport crate root.

Also changed:

  • Visibility: pub(crate) became pub where the bin needs the item. file_read was narrowed to pub(crate). TemplateSet::is_empty was added because clippy's len_without_is_empty lint fires now that len is reachable.
  • Doc links: cross-crate doc links were reworded to plain text.
  • New #[allow(clippy::disallowed_methods)] attributes: statement-level allows on test cleanups in executor, raw_exec and file_read, one module-level allow on templates' mod tests, and the production allow on file_read's read-only open (see below).
  • file_read's open is split: let opened = …open(path); under the allow, then match opened outside it. The open itself is unchanged (.read(true) plus O_NONBLOCK|O_NOCTTY).
  • Test-only items: TemplateSet::{for_test, empty_static} and identity::mode_assert moved from cfg(test) to cfg(any(test, feature = "test-util")), because the bin's tests reach them across crates.

Guards

Every step-1 guard now covers the new crates:

  • READ_ONLY gains relay-exec and relay-ops.
  • may_depend_on gains the arms relay-exec → {relay-proto} and relay-ops → {relay-proto, relay-policy, relay-exec, relay-log}.
  • Each new crate's dependency keys are pinned exactly.
  • The cfg allowlist, mount pins, compiled-source dep-info census and suppression census cover both crates.
  • The probe script's crate list is extended.
  • Ban probes now cover tokio entries in relay-exec (54 entries fire) and relay-ops (129 fire).
  • Path-keyed guards (policy_never_opened_for_write, log_fields_escaped, virtual_clock_guard) are repointed with no count changed.

New guards in daemon/tests/clippy_write_ban.rs:

  • Spawn-family census. The spawn family (Command, pre_exec, the libc exec/fork/execveat/clone/syscall names and SYS_exec*/SYS_fork*/SYS_clone*, including aliased and brace-imported forms) appears only in relay-exec's executor, exactly 3 times.
  • unsafe census. Every unsafe in the daemon (bin, every crate, tests included) is pinned per file: 6 sites, none in the bin or relay-transport.
  • file_read's allowed statement is pinned to its exact text.
  • Production and test suppressions are pinned apart. Each production allow is pinned to the statement it covers, and templates' allow must sit on mod tests.
  • ExecRequest census. relay-ops builds ExecRequest at exactly the four pinned sites.
  • Feature tables. Every member's [features] table is pinned, and on host and musl cargo tree -e normal -p relay-daemon must resolve no test-util or ban-probe feature.
  • Probe-script coverage. The probe script's crate list must equal READ_ONLY.

New ban entries. libc::execveat, libc::clone and libc::syscall are added to the root daemon/clippy.toml and to the five byte-identical read-only copies.

  • libc::execveat carries allow-invalid = true: the libc crate has no execveat on musl. The gnu passes still enforce the entry, and probe lines prove it fires.
  • libc::clone3 is not in the libc crate, so only the census names it.

Operator-visible

Log targets of the moved modules change. Their tracing targets become relay_exec::…, relay_ops::… and relay_transport::….

  • A module-keyed RUST_LOG directive on relay_daemon::{executor,raw_exec,templates,tunnel,…} no longer matches.
  • The decision records relay_daemon::allow and relay_daemon::deny are unchanged.
  • docs/operations/relay-daemon-install.md §6 now documents the per-crate targets. Step 1 had flagged this without documenting it.

Agent-maintained citations are repointed to the module or crate:

  • docs/operations/{daemon-privilege-posture,demo-fleet}.md;
  • two Console design docs;
  • comments in relay-daemon.service, docker/fixtures/dev-fleet-service and the demo catalog.toml.

The demo Template's fingerprint may change. The catalog.toml change is a comment, but the file is loaded as a Template, so its fingerprint may change. Nothing pins it.

The release binary behaves identically but is not bit-identical. Panic locations and symbol paths now name crates/….

Verification

  • make verify green, run twice: after implementation and after the security fix cycle. Command: RELAY_SMOKE_PORT=20889 GOLANGCI_LINT_CACHE=<worktree>/.dev-loop/golangci-cache make verify, exit 0.

Lanes covered:

  • cargo fmt.
  • Clippy -D warnings in debug/all-targets, host release and musl release.
  • check-daemon-bans: policy 106, log 31, proto 31, exec 54, ops 129.
  • Workspace cargo test: clippy_write_ban (39 tests), the structural guards, dev_fleet_fixtures and shutdown_signal.
  • The strict-umask lanes.
  • The Go lanes.
  • Docs and security-surface self-tests.
  • The smoke test and test-integration.
  • The cross-binary suite and fleet E2E (TestCrossBinary|TestFleetE2E ok).
  • Workflow lints (zizmor).
  • cargo-deny: bans, licenses and sources ok.

Also run:

  • A stable cargo build of daemon/fuzz; its lockfile is unchanged.
  • 39 revert checks. Each injected a violation, saw the expected guard fail, and was restored. The table is in the plan details below. They include every new guard, and in each case the injected violation passes clippy, so the new guard is what catches it.

Gates skipped:

  • The milestone half of check-crosslinks (gh api cannot list milestones from this environment). CI runs it.
  • cargo +nightly fuzz build: no nightly toolchain here.
  • The govulncheck scan, which is CI-only.

/proto is untouched, so the proto gates do not apply.

Pre-existing: the musl clippy pass warns that relay-log's libc::renameat2 entry does not resolve. This comes from step 1's config and is not a lint failure.

Branch: this branch carries a merge of origin/main. The rust-toolchain repin from #956, which this branch had cherry-picked, is now on main, so it adds nothing to this diff.

Reviews

  • Claude plan review: REVISE → APPROVE (2 cycles). Cycle 1 required fixing the agent-maintained operator docs in this PR. It also added the unsafe census, statement-level test allows, the file_read pin and ban-probe = [].
  • Claude code review: APPROVE (cycle 1), then APPROVE (cycle 2) with three non-blocking suggestions; see Unresolved.
  • Claude security review:
    • Cycle 1: CONCERNS (5 findings).
    • One fix cycle followed. It closed all five: bin spawn routes, per-file suppression counts, file_read's allow scope, the relay-ops read-only overclaim, and unchecked [features] tables.
    • Re-review: CONCERNS. Two residual gaps remain in the new guards; see Unresolved.
  • Codex: once, final diff — absent (reason=not-installed). The gate ran with one reviewer.
  • Scope-partial: n/a.

Capability spec

  • The governing spec in docs/specs/ is still true of the code after this change
  • Or: no capability spec covers this area yet

Spec: docs/specs/execution.md and docs/specs/policy.md are legacy (aspirational), so they are context only. No claims are named and none are promoted. Every moved test keeps its name, so test-name proof rows in the migrated specs still resolve.

Security surface

  • Touches none of: /proto, Policy, execution, transport, mTLS, auth, blast caps, label authority
  • Or: names the invariant below and has a human reviewing it deliberately

Invariants touched (none amended):

  • Add autonomous dev-loop harness (scripts/dev-loop) #1 (exec): strengthened. relay-exec is the one spawning crate. Its exec ban stays live in its own tests (test allows are statement-level). A textual spawn-family census and an unsafe census back clippy across every crate and the bin. The new libc exec-family ban entries cover the bin too.
  • #2 / [needs-human] Wire proto codegen: buf → Go relay types + Rust tonic/prost build.rs (#7) #12 (Policy, Template trust roots): strengthened.
    • templates (the trust-root loader) and the Operations now sit in a write-banned crate.
    • The one sanctioned open in that crate (file_read, ADR-0018) is pinned statement-exact.
    • Policy evaluation is untouched.
    • The relay-ops crate doc states the limit: the ban covers in-process writes only. Spawned children are bounded by Policy, Templates, selfref and the unit's ProtectSystem.
  • #4 (dedup): unchanged. dedup stays in the bin; only its test helper import moves.
  • #6 (mTLS): unchanged. Transport moves with visibility edits only, and enroll_config still fails closed on scheme.
  • Loop hardening: review-parser fix, strict ordering, escalate-closes, lock, docs #13 (risk class): unchanged. The registry moved verbatim.

Paths on the security surface:

  • daemon/Cargo.toml and Cargo.lock;
  • every daemon/crates/** file this PR adds or moves;
  • daemon/clippy.toml and the five read-only clippy.toml copies;
  • the ban_probe.rs files;
  • daemon/src/{config,main,scheduler}.rs;
  • daemon/tests/{clippy_write_ban,policy_never_opened_for_write}.rs;
  • daemon/packaging/relay-daemon.service (comment only);
  • scripts/clippy-ban-probe.sh;
  • docs/agents/security-surface.txt (moved paths only).

Unresolved

The review budgets are spent: one security fix cycle and two code-review cycles. These need a human decision: fix them here, or accept them as tracked in #961.

  1. suppression_sites can mark production code as test code. Security re-review rated this CONCERNS and recommended fixing it in this PR. A #[cfg(test)] on a bare block, an if/match statement or a braced macro call swallows the following statement up to the next top-level ;. With a test allow deleted in the same file, a production allow can hide behind it at equal counts. It takes two deliberate edits in a security-surface file. The fix is about 5 lines and fails closed: count a #[cfg(test)] span only for item-like targets.
  2. No test requires the exec-ban entries. Rated CONCERNS (low). Deleting libc::execveat, clone and syscall from every config and both probe files at once fails no test. The same is true of step 1's exec entries. The fix is a required_exec_ban() set, plus an assertion that allow-invalid appears on execveat only.
  3. The spawn census header overclaims. It says "any aliased spelling", but use libc as l; l::syscall taken as a function item escapes the regex. Clippy's resolved-path ban and the unsafe census still catch it.

Escalations

Plan, review record and revert checks

IMPLEMENTATION PLAN (revision 2)

Issue: daemon: Cargo workspace — relay-policy and relay-ops read-only by clippy, fuzzable crates (#889), STEP 2 of 2 (closes #889)
Spec: docs/specs/execution.md, docs/specs/policy.md Binding: legacy (both aspirational), context only
Claims: none. This is a pure move. Migrated specs nearby (policy-preflight.md, policy-catalog.md) cite tests by name only. check-docs.sh finds those names anywhere under daemon/, and every moved test keeps its name, so every proof row still resolves.

Summary

This step moves the exec, ops and transport code out of daemon/src into three workspace crates:

  • relay-exec: the only crate that may spawn a process. It gets the read-only clippy config, and its one sanctioned Command::new keeps its statement-level allow.
  • relay-ops: read-only by clippy.
  • relay-transport: holds the sanctioned identity writer. It inherits the root exec-ban config.

It then extends every step-1 guard to the new layout and adds four new guards:

  1. a widened spawn-site census;
  2. an unsafe census for the read-only crates;
  3. an exact-chain pin on file_read's now-suppressed open;
  4. a test that the probe script covers exactly the READ_ONLY set.

It also:

  • retires the moved daemon/src lines in security-surface.txt;
  • fixes the agent-maintained operator docs and comments that the move makes wrong.

No behaviour changes.

Changes from v1, by review finding:

The module graph today (measured from use / crate:: / super:: edges)

not_executed → gen
config       → gen, enroll(state_dir_from_env, node_labels_from_env), operations::system_facts(gather_facts)
executor     → config(ResourceLimits, clean_io_error), gen, not_executed
cancel       → shutdown            (an intra-doc link only, cancel.rs:21)
raw_exec     → config(ResourceLimits), executor, gen, not_executed, policy(+decision_log)
templates    → gen, hardened_open, operations::{registry,template}, policy
operations/* → gen, not_executed, config(ResourceLimits, clean_io_error), executor, policy,
               templates (template.rs:67), raw_exec (registry.rs:44),
               scheduler::READ_CONCURRENCY (registry.rs:45 — a real edge),
               crate::dispatch / crate::scheduler (doc links only: registry.rs:145, mod.rs:60)
scheduler    → gen, operations, templates
services     → dedup, policy, scheduler, templates
identity     → (none; tests: test_support)
tunnel       → identity, log_field
enroll       → config::Config (enroll.rs:12, from_config :94), gen, identity, log_field
renew        → enroll(EnrollError), gen, identity, log_field, crate::rand_unit (main.rs:101; renew.rs:216/223/273)
version      → env!("CARGO_PKG_VERSION*") of the bin package
policy_check → dispatch, config, policy, selfref, templates, gen
selfref      → config, enroll, operations, policy
catalog      → #[cfg(test)]; gen, operations, policy; include_str!("../packaging/…", "../policy.default.toml")
dedup (tests)→ identity::mode_assert (#[cfg(test)] pub(crate))
bin tests    → templates::TemplateSet::{for_test, empty_static} (#[cfg(test)]) from dispatch/scheduler/policy_check
dispatch     → config::MAX_OUTPUT_BYTES (dispatch.rs:25 — the ONLY user of that constant)

Four edges would form cycles across the named crates. Each is cut as below; nothing else changes.

  1. config ↔ exec/ops. Executor, raw_exec, file_read, pkg_install, service_restart and template need ResourceLimits and clean_io_error, but Config needs enroll and ops. Cut: move ResourceLimits (with its impls) and clean_io_error to relay-exec/src/limits.rs. Config and MAX_OUTPUT_BYTES stay in the bin. MAX_OUTPUT_BYTES is not on any cycle (review #6): only dispatch.rs uses it, and the executor gets the value through ExecRequest::max_output_bytes.
  2. enroll → Config (bin). Cut: EnrollConfig::from_config (body and tests) moves into the bin as config::enroll_config(&Config). The rest of enroll moves.
  3. registry → scheduler::READ_CONCURRENCY (bin). Cut: the constant's definition moves to relay-ops registry.rs, and scheduler.rs re-exports it.
  4. renew → crate::rand_unit (main.rs). Cut: rand_unit moves to the relay-transport crate root, and main.rs imports it.

Resulting crate DAG:

  • relay-proto ← relay-log ← relay-policy
  • relay-exec → {relay-proto}
  • relay-ops → {relay-proto, relay-policy, relay-exec}
  • relay-transport → {relay-proto, relay-log}
  • the bin → all of them

There are no cycles.

What stays in the bin, and why

  • main.rs, dispatch.rs, scheduler.rs, dedup.rs, shutdown.rs: named by the issue.
  • selfref.rs: maintainer decision.
  • config.rs: needs enroll and ops. The lexer guard also pins it as a Policy-path-naming module.
  • version.rs: reads the bin package's CARGO_PKG_VERSION*, the ADR-0014 stamp.
  • services.rs: depends on dedup and scheduler.
  • policy_check.rs: depends on dispatch and selfref.
  • catalog.rs: test-only, and its include_str! paths are relative to src/. Keeping it avoids a data-file change in a read-only crate (ALLOWED_DATA_FILES).

Placement inside the new crates:

  • raw_exec goes to relay-ops: registry.rs calls run_raw_exec, and raw_exec needs Policy, which relay-exec may not depend on.
  • templates and operations share a crate because they depend on each other.

Affected files: omitted here for length — the diff is the record (54 files).

Key decisions:

  • relay-exec gets the full read-only config, exec ban included.
    • Its spawn permission is the existing statement-level allow at executor.rs:138.
    • The step-1 rule that the read-only configs are byte-identical holds.
    • A second spawn anywhere in relay-exec, tests included, fails clippy, because its test allows are statement-level on remove_file only.
  • Review #3, both remedies.
    • Statement-level allows wherever the banned statements are few: relay-exec executor.rs (4), relay-ops raw_exec.rs (1) and file_read.rs (4). The exec ban stays live in those test modules.
    • templates.rs keeps one module-level allow (about 17 fixture statements), and the widened spawn-family census covers it.
    • The census also covers every non-probe source in every crate and the bin. So an aliased Command, a brace-imported libc exec/fork, or a pre_exec outside relay-exec fails a test even where a clippy allow is in scope.
    • Doing only one of the two leaves either the templates module or a future module-level allow uncovered.
  • unsafe census pinned per file, tests included (review #2). This restores the backstop relay-policy gets from forbid(unsafe_code), which relay-exec (pre_exec) and relay-ops (getifaddrs) cannot use. relay-policy keeps its forbid.
  • file_read chain pinned in clippy_write_ban.rs (review #4). That is where sanctioned suppressions in read-only crates are reviewed. The lexer guard is kept whole, and only its paths change.
  • ban-probe = [] for both new crates (review #5): the step-1 form. The script's --workspace lint resolves tokio::fs and tokio::process, and a resolution failure is loud, not silent.
  • MAX_OUTPUT_BYTES stays in config.rs (review #6). It is not on a cycle, and only dispatch.rs (bin) uses it. Leaving it keeps dispatch.rs, daemon-privilege-posture.md:166 and three Go comments (job.go:144, job_test.go:861, dayone.go:69) correct with no edit.
  • relay-transport has no clippy.toml. It inherits the root exec ban. It cannot be read-only because identity.rs is a sanctioned writer (lexer allowlist).
  • Cutting the four cycles (move the limits, relocate from_config, move the READ_CONCURRENCY definition, move rand_unit) is the minimum the named crates need. Rejected alternatives:
    • Config in relay-transport: it would drag ops into transport and put the Policy-path-naming module beside the identity writer.
    • A use relay_exec::limits as config alias: misleading.
  • Bin-test reach uses test-util features (the step-1 pattern): relay-ops TemplateSet::{for_test, empty_static} and relay-transport identity::mode_assert. Extended test (d) keeps them bin-dev-only.
  • The fuzz targets are unchanged. The acceptance criteria require only that the fuzz crate builds on stable. New targets over the relay-ops Template/schema parsers and the relay-policy TOML loader are a follow-up for the maintainer to file (review Implement the Go Relay binary: plaintext gRPC server handling Hello/Ping #8).
  • What does not move byte-identically (each stated in the PR):
    • the config.rs split (limits out, enroll_config in, two doc lines);
    • from_config → config::enroll_config (body verbatim) and its tests' call sites;
    • the rand_unit and READ_CONCURRENCY definitions;
    • three cfgs widened to any(test, feature = "test-util");
    • new #[allow(clippy::disallowed_methods, reason=…)] attributes (relay-exec 4; relay-ops 1 production + 6 test);
    • four doc links reworded, plus the ResourceLimits doc phrase;
    • crate::config:: → crate::limits:: paths;
    • pub(crate) → pub where required.

Test strategy:

  • Existing tests. No claim IDs. The proof is that every existing test passes with its name and assertion unchanged: cargo test at the workspace root, which includes dev_fleet_fixtures, shutdown_signal and every structural guard. Moved unit tests now run in their own crate's test binary.
  • New tests in daemon/tests/clippy_write_ban.rs:
    • spawn_family_appears_only_in_relay_exec_executor
    • spawn_scanner_catches_aliases_and_the_libc_family
    • unsafe_in_read_only_crates_is_exactly_the_pinned_map
    • file_read_open_is_exactly_the_pinned_read_only_chain
    • ban_probe_script_covers_exactly_the_read_only_crates
  • Extended tables, proven by the existing tests: read_only_clippy_configs_are_identical_and_carry_the_write_ban, clippy_config_files_are_exactly_the_expected_set, read_only_crates_depend_only_on_permitted_first_party_crates, read_only_crates_have_exactly_the_pinned_dependencies, lint_suppressions_are_exactly_the_pinned_map, path_mounts_and_includes_are_exactly_the_pinned_map, compiled_sources_of_read_only_crates_stay_inside_their_crate, compiled_sources_carry_no_unpinned_suppression_or_mount, read_only_crates_use_only_allowlisted_cfgs, ban_probe_mount_is_exactly_the_pinned_text, every_probeable_clippy_entry_has_a_probe_line, test_support_is_dev_only_and_no_dependency_enables_test_only_features.
  • make check-daemon-bans must report ok relay-exec (54 entries fire) and ok relay-ops (127 entries fire), plus the step-1 three (104/31/31).
  • Revert checks. Inject each violation, see it fail, restore. Record each result in the PR.
    • Existing guards, extended:
      1. std::fs::write in relay-ops non-test code → make lint-daemon fails, in all three passes.
      2. std::fs::write in relay-exec non-test code → lint fails.
      3. std::process::Command::new in relay-ops non-test code → lint fails, and so does the spawn census.
      4. Command::new in relay-transport or a bin module → lint fails (root ban), and so does the spawn census.
      5. Delete executor.rs:138's allow → lint fails.
      6. Delete file_read's production allow → lint fails.
      7. Add relay-transport to relay-ops' deps → may_depend_on fails.
      8. Add tempfile to relay-exec → the pinned-keys test fails.
      9. A stray #[allow(clippy::disallowed_methods)] in relay-ops → the suppression census fails.
      10. #[cfg(not(debug_assertions))] in relay-exec → the cfg test fails.
      11. Delete one tokio probe line in relay-ops → the count test and the probe script both fail.
      12. Mistype a tokio path in all five configs → the probe script reports a config warning or an unflagged line.
      13. A fs::write in relay-ops templates.rs production → lexer policy_modules_never_write fails.
      14. An unwrapped wire field in relay-transport tunnel.rs → log_fields_escaped fails.
      15. Enable test-util on relay-transport in a normal dependency → (d) fails.
    • New guards, one revert check each:
      16. Statement-level allows (#3): add let _ = std::process::Command::new("x"); inside relay-exec executor.rs mod tests → clippy fails. This proves the exec ban is live in relay-exec's tests.
      17. Spawn census, alias form (h): inside relay-ops templates.rs mod tests, which is module-allowed, add use tokio::process::Command as C; and let _ = C::new("x");. Clippy passes, because the module allow covers it. The spawn census fails.
      18. Spawn census, libc form (h): in the same module, add use libc::{fork}; with let _ = fork; → the spawn census fails.
      19. unsafe census (i): in relay-ops system_facts.rs production, add // SAFETY: test and let _ = unsafe { libc::dup2(0, 0) };. Clippy passes, because dup2 is not on the ban list. The unsafe census fails.
      20. file_read chain (j), numeric flag: change libc::O_NONBLOCK | libc::O_NOCTTY to libc::O_NONBLOCK | 0o2. Clippy passes (allowed statement). The chain test fails.
      21. file_read chain (j), write mode: add .write(true) to the same chain → the chain test fails.
      22. Probe-script crate list (k): drop relay-exec from PROBE_CRATES → ban_probe_script_covers_exactly_the_read_only_crates fails.
  • Also run:
    • make verify;
    • cargo build --release --locked -p relay-daemon, host and --target x86_64-unknown-linux-musl;
    • cargo deny --workspace --locked check bans licenses sources;
    • cd daemon/fuzz && cargo build (stable);
    • make test-crossbinary and make opschema-check (the shipped binary's behaviour and the Operation schema are unchanged);
    • scripts/security-surface-test.sh, and scripts/check-docs.sh for the doc edits.
  • Environment: 16 GB / 4 CPUs, one cargo build at a time. Do not run verify while another build holds the target lock.

Constraints and risks:

  • Invariants touched (none amended):

  • AGENTS.md "pure move" discipline. The non-byte-identical edits listed under Key decisions are forced by the graph or by doc correctness. The enroll_config relocation is the one judgement call; the PR states it plainly as a deviation, not an escalation.

  • Operator-visible change: tracing targets of the moved modules become relay_exec::…, relay_ops::… and relay_transport::…, so module-keyed RUST_LOG directives on relay_daemon::{executor,raw_exec,templates,tunnel,…} stop matching. The decision records (relay_daemon::allow / relay_daemon::deny) are unchanged. relay-daemon-install.md §6 now documents this, and the PR repeats it.

  • The release binary is behaviour-identical, not bit-identical: panic locations and symbol paths now name crates/….

  • Note for reviewers (review Wire proto codegen: buf → Go relay types + Rust tonic/prost build.rs #7): the lexer guard strips only a bare #[cfg(test)] (its header item (d)). It will now read TemplateSet::{for_test, empty_static}, which sit under cfg(any(test, feature = "test-util")), as production code. That is harmless (they contain no open or write), but the PR says so.

  • Follow-ups the PR body lists, for the maintainer to file. The runner files no issues. These are the human-owned citations that go stale:

    • docs/adr/0015 (:18, :57, operations/schema.rs and mod.rs);
    • docs/adr/0034:131 (renew.rs);
    • docs/adr/0036 (:30, :61-62, :69, :147: identity.rs, templates.rs, file_read.rs, mode_assert);
    • docs/security/threat-model.md:264 (executor.rs) and :302 (raw_exec.rs);
    • docs/security/secret-handling-audit.md:82-83 (enroll.rs, identity.rs);
    • Go comments: relay/internal/capability/refusal.go:38, nodestate/nodestate.go:216 and :327, controlplane/mcp/tools_operations.go:90, controlplane/console/dayone.go:44, console/job.go:839, console/job_test.go:1109, opvocab/opvocab.go:35 and :108-111, opvocab/opvocab_test.go:115, registry/label.go:95, plus step 1's refusal.go:28 and nodestate.go:327 (policy.rs).

    Suggested title: "docs: repoint human-owned daemon path citations after the daemon: Cargo workspace — relay-policy and relay-ops read-only by clippy, fuzzable crates #889 workspace split".

  • Second follow-up for the maintainer to file (review Implement the Go Relay binary: plaintext gRPC server handling Hello/Ping #8): "daemon/fuzz: fuzz targets over relay-ops Template/schema parsing and relay-policy TOML loading". This is the issue's "fuzzable crates" goal. The crates are now importable, but the targets were not in this issue's acceptance criteria.

  • Ordering risk. The guards (steps 10-13), the probe script (14), the probe headers (9) and the surface file (15) must land in the same commit series as the moves, or cargo test and check-daemon-bans fail mid-series.

  • Lockfile: CI builds --locked, so the updated daemon/Cargo.lock must be committed.

  • Gate skips to report:

    • /proto is untouched, so the proto gates do not apply.
    • Report a skipped check-crosslinks (gh milestones) if it skips again.
    • There is no nightly toolchain, so cargo +nightly fuzz build is not run; say so.
  • No ESCALATE. No migrated spec claim, ADR or invariant conflicts with this plan. The PR still needs human merge and security review.


Addendum (coordinator, folding in plan review cycle 2's suggestions — APPROVE):

  • enroll.rs mod tests: also remove the now-unused use crate::gen::Facts; and Mutex from use std::sync::{Arc, Mutex}; (only fake_config/ENV_LOCK used them).
  • operations/system_facts.rs:5 module doc names EnrollConfig::from_config; reword to "the bin's config::enroll_config" and list it among the non-byte-identical edits.
  • PR note: identity::mode_assert also moves under cfg(any(test, feature = "test-util")), so the lexer reads it as production (harmless: metadata only, identity.rs is allowlisted).
  • PR note: the docker/fixtures/templates.d/relay-demo/catalog.toml comment edit may change that demo Template file's fingerprint (nothing pins it).
  • Follow-ups (human-owned path citations; fuzz targets): the coordinator files them as issues before the PR opens and cites their numbers.

Revert checks (cycle 2 developer record)

All restored: yes. Each file was copied back from a saved copy, cmp against the copy matched, Cargo.lock is unchanged, and git diff --stat matched the pre-check baseline (14 files, +623/−70).

# Injection Command Observed failure Restored
1 std::fs::write in relay-ops production cargo clippy --workspace --all-targets, then --lib --release, then --lib --release --target x86_64-unknown-linux-musl (the three passes of make lint-daemon, run separately) use of a disallowed method std::fs::write at relay-ops/src/lib.rs:34, in all three passes yes
2 std::fs::write in relay-exec production clippy all-targets use of a disallowed method std::fs::write at relay-exec/src/lib.rs:29 yes
3 std::process::Command::new in relay-ops production clippy, then --test clippy_write_ban spawn_family_appears_only clippy: disallowed method std::process::Command::new. Census: left: {executor.rs: 3, relay-ops/src/lib.rs: 1} yes
4a Command::new in relay-transport clippy, then the spawn census clippy fails at relay-transport/src/lib.rs:40; census {…, relay-transport/src/lib.rs: 1} yes
4b Command::new in the bin (shutdown.rs) clippy, then the spawn census clippy fails at src/shutdown.rs:24; census {…, src/shutdown.rs: 1} yes
5 Delete the allow on executor.rs's Command::new clippy disallowed method tokio::process::Command::new at executor.rs:138 yes
6 Delete file_read's production allow clippy disallowed method OpenOptionsExt::custom_flags at file_read.rs:127 yes
7 Add relay-transport to relay-ops' dependencies --test clippy_write_ban read_only_crates_depend_only_on_permitted relay-ops: [dependencies] depends on relay-transport; … may depend on [relay-proto, relay-policy, relay-exec, relay-log] only yes
8 Add regex to relay-exec's dependencies read_only_crates_have_exactly_the_pinned_dependencies left: {regex, relay-proto, rustix, tokio} right: {relay-proto, rustix, tokio} yes
9 Stray #[allow(clippy::disallowed_methods)] in relay-ops lib.rs lint_suppressions_are_exactly_the_pinned_map and production_and_test_suppressions_are_pinned_apart both fail: the new relay-ops/src/lib.rs: 1 entry, and the production list gains ("[allow(clippy::disallowed_methods)]", "fn__inj(){}") yes
10 #[cfg(not(debug_assertions))] in relay-exec read_only_crates_use_only_allowlisted_cfgs relay-exec/src/lib.rs: conditional compilation ["[cfg(not(debug_assertions))]"] is not allowlisted yes
11 Delete one tokio probe line in relay-ops every_probeable_clippy_entry_has_a_probe_line, then make check-daemon-bans count test: relay-ops: ban_probe.rs has 128 probe lines for 129 probe-able entries. The probe script still passed (128 fire), so the plan's "and the script fails" was wrong yes
12 tokio::fs::File::set_lenn in all five configs make check-daemon-bans `tokio::fs::File::set_lenn` does not refer to a reachable function in four configs, then probe lines clippy did NOT flag yes
13 fs::write in templates.rs production --test policy_never_opened_for_write policy_modules_never_write fails: crates/relay-ops/src/templates.rs:57: fs:: path writer yes
14 Unwrapped error = err.to_string() in relay-transport tunnel.rs --test log_fields_escaped tunnel.rs:[197]: field error = err.to_string() … is neither wrapped in log_field::untrusted nor an allowlisted local field yes
15 features = ["test-util"] on relay-transport in the bin's [dependencies] test_support_is_dev_only… relay-daemon: [dependencies] enables test-util on relay-transport; only the bin's [dev-dependencies] may yes
16 std::process::Command::new inside relay-exec executor.rs mod tests clippy disallowed method std::process::Command::new at executor.rs:589 yes
17 use tokio::process::Command as C; C::new("x") in templates.rs mod tests clippy, then the spawn census clippy passes (module allow). Census: {…, templates.rs: 1} yes
18 use libc::{fork}; let _ = fork; in the same module clippy, then the spawn census clippy passes. Census: {…, templates.rs: 2} yes
19 unsafe { libc::dup2(0, 0) } in system_facts.rs production clippy, then unsafe_anywhere clippy passes. Census: system_facts.rs: 2 against pinned 1 yes
20 O_NONBLOCK | 0o2 in file_read clippy, then file_read_open clippy passes. Guard (j): left: "letopened=…custom_flags(libc::O_NONBLOCK|0o2).open(path);" yes
21 .write(true) added to the file_read chain clippy, then file_read_open clippy passes. Guard (j): left: "…read(true).write(true).custom_flags(…)…" yes
22 Drop relay-exec from PROBE_CRATES ban_probe_script_covers_exactly_the_read_only_crates left lacks relay-exec, right has it yes
N1 libc::SYS_execveat and SYS_clone3 consts in the bin (dispatch.rs) clippy, then the spawn census clippy raised no ban error (only an unrelated cast lint on my constant). Census: {…, src/dispatch.rs: 2} yes
N1b use libc::{syscall as sc}; in the bin clippy, then the census clippy passes. Census: {…, src/dispatch.rs: 1} yes
N2a let _ = libc::execveat; in the bin clippy disallowed method libc::execveat at dispatch.rs:24 yes
N2b let _ = libc::clone; in the bin clippy disallowed method libc::clone yes
N2c let _ = libc::syscall; in the bin clippy disallowed method libc::syscall yes
N3a unsafe { libc::dup2 } in the bin clippy, then unsafe_anywhere clippy passes. Census: "src/dispatch.rs": 1 yes
N3b unsafe { std::mem::zeroed::<u8>() } in relay-transport unsafe_anywhere "crates/relay-transport/src/lib.rs": 1 yes
N4 Move a test allow onto production code in executor.rs, at constant total lint_suppressions_are_exactly…, then production_and_test_suppressions_are_pinned_apart the old total-count guard passes; the split guard fails yes
N5 Move templates.rs's allow from mod tests to impl TemplateSet the same old guard, then templates_test_allow_sits_on_mod_tests… and the split guard old guard passes. New: templates.rs's one suppression must sit directly between #[cfg(test)] and mod tests, and the split test also fails yes
N6 Restore the merged statement with f.set_modified(UNIX_EPOCH) in the match arm clippy, then file_read_open and the split guard clippy passes (suppressed); both fail on the statement text yes
N7 An ExecRequest { ..a } construction in relay-ops lib.rs relay_ops_builds_exec_requests_only_at_the_pinned_sites left gains "crates/relay-ops/src/lib.rs": 1 yes
N8 default = ["test-util"] in relay-ops [features] every_features_table… and no_test_util… left: {ban-probe, default, test-util}; and relay-ops resolves with test-util in the shipped build (features {default, test-util}) yes
N9 features = ["test-util"] on relay-ops in [workspace.dependencies] test_support_is_dev_only, every_features_table, no_test_util… the two static guards pass; only the resolved check fails: relay-ops resolves with test-util in the shipped build (target None, features {test-util}) yes

This PR was produced by the dev-loop and must be merged by a human — nothing here merges autonomously.

🤖 Generated with Claude Code

https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek


Generated by Claude Code

claude added 3 commits October 9, 2026 05:26
Upstream moved the v1 tag to e2a55d2 (dtolnay/rust-toolchain#187,
"Safely handle action inputs": multi-line-safe GITHUB_OUTPUT writes and
argv-array rustup flags instead of string-built command lines). zizmor's
online ref-version-mismatch audit now fails every PR's lint step because
our six SHA pins, commented '# v1', point at the old head 7e38f4b. The
new commit descends from the old pin and changes only action.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek
(cherry picked from commit 0a3a00a)
…nly by clippy, fuzzable crates

relay-exec (sole spawning crate), relay-ops (write-banned) and relay-transport move out of
daemon/src; step-1 guards extended to them, plus spawn, unsafe, ExecRequest and feature censuses.

Closes #889
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek
@rapp992 rapp992 added the loop:needs-human dev-loop: escalated — human must review/merge label Oct 9, 2026 — with Claude
@rapp992
rapp992 merged commit 9daf055 into main Oct 9, 2026
16 checks passed
@rapp992
rapp992 deleted the loop/issue-889-step2 branch October 9, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

loop:needs-human dev-loop: escalated — human must review/merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

daemon: Cargo workspace — relay-policy and relay-ops read-only by clippy, fuzzable crates

2 participants