Skip to content

feat(mc-host): materialize immutable harness closures - #53

Draft
ahrav wants to merge 1 commit into
stack/mc-host-05-payload-packagesfrom
stack/mc-host-06-harness-runtime
Draft

feat(mc-host): materialize immutable harness closures#53
ahrav wants to merge 1 commit into
stack/mc-host-05-payload-packagesfrom
stack/mc-host-06-harness-runtime

Conversation

@ahrav

@ahrav ahrav commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • materialize qualified OpenCode and Pi runtime graphs into daemon-owned closures
  • enforce fixed argv, provider-scoped credentials, and snapshot fingerprints
  • retain closures independently of mutable package/cache sources

Stack

PR 6 of 10. Base: stack/mc-host-05-payload-packages.

Validation

  • harness closure and Broca subprocess suites
  • source deletion, tamper, argv, credential, and cleanup tests

Post-Deploy Monitoring & Validation

Watch harness_unavailable subreasons, Broca child cleanup failures, and closure validation errors for one release cycle. Roll back if credential values enter diagnostics or closure descendants survive shutdown. Owner: mc-host maintainers.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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

(Harness::Pi, "openai-codex") => "openai",
_ => provider,
};
let actual = presented

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical: every real Send will fail credential_snapshot_mismatch.

CredentialVerifier::verify requires presented.get(canonical_provider) to succeed, but:

  • ck_mc_host/serve.rs:494 unconditionally uses BrocaComponent::new_with_credentials (was BrocaComponent::new before this PR), so credential_verifier is always Some in production.
  • control.rs:373-374 defaults a missing credential_fingerprints field to an empty BTreeMap.
  • BindIdentity in packages/plugin/src/shared/mc-host-client/types.ts has no credential_fingerprints field, and nothing in this diff adds one to the client.

So presented is always empty for real client requests, and verify() always returns credential_snapshot_mismatch before any Send can succeed.

If this is intentionally deferred to a later PR in the stack (client-side fingerprint population), please note that dependency explicitly — right now this PR alone appears to make Broca non-functional end-to-end.

let init = storage_init(&root)?;

let env = EnvSnapshot::capture().map_err(|_| "environment snapshot exceeds bounds")?;
let env = EnvSnapshot::capture_from(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Compounds the credential issue above. This changes the daemon's EnvSnapshot source from EnvSnapshot::capture() (full OS process environment) to EnvSnapshot::capture_from(envelope.credentials...). I couldn't find anywhere on the TS/launcher side (bootstrap.ts, the launcher envelope construction) that populates envelope.credentials — it appears to always be empty in the current wiring.

If that's correct, EnvSnapshot::provider_row will return CredentialMissing for every provider even when ANTHROPIC_API_KEY/OPENAI_API_KEY/GEMINI_API_KEY are genuinely set in the daemon process's real environment — independent of the fingerprint-mismatch issue in broca/mod.rs. Worth confirming this is intentional (and where credentials gets populated) before this ships.

cancel: CancellationToken,
model_ref: String,
) -> BackendTerminal {
let mut child_env = match env.provider_row("pi", &request.provider) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New precheck breaks the alias-then-canonical fallback this function exists for.

run_pi now calls env.provider_row("pi", &request.provider) up front, using request.provider (always the canonical form, e.g. "openai") — identically on both the "aliased" first attempt and the "canonical" retry in run_pi_with_provider_fallback. Since credential_failure returns ErrorClass::Permanent (not AuthRequired), a subscription-authenticated user (no OPENAI_API_KEY/GEMINI_API_KEY env var) now fails immediately on the first attempt, and the fallback retry condition (error.class == ErrorClass::AuthRequired, in run_pi_with_provider_fallback) never triggers anyway — even if it did, the retry would hit the exact same precheck and fail the same way.

This contradicts the doc comment directly above run_pi_with_provider_fallback, which says it specifically supports "a user authenticated through the direct openai/google API-key providers [who] has no credentials under the subscription-extension aliases" — that fallback path looks dead for the case it was built for.

.validate(&digest)
.map_err(|e| generation_failure(&e))?;
Ok(digest)
let launcher = validated.open_verified_file(PRODUCTION_LAUNCHER).ok();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Launcher-verification failure is silently swallowed, defeating the provenance check this PR introduces.

validated.open_verified_file(PRODUCTION_LAUNCHER).ok() discards any Err from hash/size/mode verification (same pattern at line 669). That None is indistinguishable from "no launcher present (dev fixture)" by the time it reaches spawn::spawn_detached (spawn.rs), which then falls back to fexecve'ing std::env::current_exe() — the currently-running CLI — instead of the verified generation launcher.

So a corrupted, deleted, or tampered payload/bin/ck-mc-host in a staged generation doesn't abort start/restart; it silently substitutes a different executable instead of failing closed with something like native_payload_invalid. Consider propagating the verification error instead of .ok()-swallowing it.

// short-lived CLI can have open; raise if the CLI ever holds
// more.
// bounded loop above the CLI's descriptor ceiling.
if libc::syscall(libc::SYS_close_range, 4u32, u32::MAX, 0u32) < 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

libc::SYS_close_range has no macOS definition in the libc crate, but this function now has a #[cfg(target_os = "macos")] libc::execve(...) branch a few lines below (line 164), implying macOS is a real target for this binary (consistent with host_target() returning darwin-arm64/darwin-x64 elsewhere in this PR). This line has no cfg guard, so building for macOS will fail with "cannot find value SYS_close_range in module libc". Since there's no macOS CI job for mc-module, this wouldn't be caught before release.


/// Resolves a listed node to a closure-owned path after revalidating it
/// through the retained `files/` descriptor.
pub fn resolve_node(&self, node_path: &str) -> Result<PathBuf, HarnessClosureError> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verify-then-use-by-path TOCTOU gap. resolve_node opens and hash-verifies the file via open_relative_file + verify_node_file on an fd, then discards that fd and returns a PathBuf string. Callers (broca/pi.rs, broca/opencode.rs) pass that path to std::process::Command::new(path), which re-opens by path at spawn time. If the file at that path is replaced between this verification and the later path-based execve in Command::spawn(), the substituted binary runs without ever having been checked — the fd-based verification here provides no binding to what actually gets executed.

Related: open_relative_file (used here and by copy_node) only checks intermediate path components are directories, never calling verify_owned_directory on them the way the full-tree validate()/validate_tree() path does (owner-only 0700, correct uid). So a loosened intermediate directory permission is caught by store.validate() but not by resolve_node(), and this asymmetry isn't covered by the new test suite.

STORE_OPEN_WAITING => self
.store_open
.waiting_report(now)
.expect("waiting phase has a waiting report"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This changed from .unwrap_or_else(|| DISPATCH_HEALTH.report(now)) to .expect("waiting phase has a waiting report"). store_open.phase is loaded once at line 12586 to select this match arm, then loaded again inside waiting_report() (line 288) to decide whether to return Some. If a concurrent transition from STORE_OPEN_WAITING to STORE_OPENED (e.g. coordinator.phase.store(STORE_OPENED, Ordering::Release)) happens between those two loads, waiting_report returns None and this now panics instead of falling back gracefully — turning a routine health-check race into a task panic.

} => {
spawn::ignore_sigpipe();
emit(cmd_start(payload_dir.as_deref()))
match serve::read_launcher_envelope() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

start/restart now unconditionally call serve::read_launcher_envelope(), which does stdin.lock().read_to_end(...) before doing anything else. This is a new stdin dependency: running ck-mc-host start --payload-dir <dir> directly from an interactive terminal (stdin attached to a TTY, not redirected/piped) will now hang indefinitely waiting for EOF, whereas previously start/restart had no stdin read in the parent process. Only wrapper-invoked usage that explicitly closes/redirects stdin is unaffected — worth confirming that's the only supported invocation path.

) -> Result<String, CredentialRowError> {
const DOMAIN: &str = "subc-broca-credential-v1";
const CANONICALIZATION: &str = "harness-provider-name-length-value/1";
let canonical_provider = match (harness, provider) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor maintainability note: this same ("pi", "google-antigravity") => "google" / ("pi", "openai-codex") => "openai" alias table is now duplicated 3-4 times (here, provider_row just above, CredentialVerifier::verify in broca/mod.rs, and pi_model_ref in pi.rs). They currently agree, but adding/renaming a provider alias in only one copy would silently desync credential lookup from fingerprint verification. Worth factoring into one shared function.

@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown

Automated review summary

Reviewed via a multi-agent pass (parallel angle-scanning plus independent verification against the actual source) over the ~6.2k line diff. Inline comments posted on the specific lines below; full findings summarized here for convenience.

Likely blocking - please confirm before merge:

  1. Every real Send looks like it will fail credential_snapshot_mismatch. ck_mc_host/serve.rs now unconditionally builds BrocaComponent::new_with_credentials (was BrocaComponent::new), so CredentialVerifier::verify requires a credential_fingerprints entry per provider - but control.rs defaults a missing field to {}, and nothing in packages/plugin/src/shared/mc-host-client/types.ts (BindIdentity) ever populates credential_fingerprints. (crates/mc-host/src/broca/mod.rs:73)
  2. Compounding issue: EnvSnapshot is no longer built from the real OS environment. serve.rs switched from EnvSnapshot::capture() to EnvSnapshot::capture_from(envelope.credentials...). I could not find where envelope.credentials gets populated on the launcher/TS side - if it is never populated, provider_row returns CredentialMissing for every provider regardless of perf(search): remove six search and clustering hot-path costs #1. (crates/mc-module/src/bin/ck_mc_host/serve.rs:483)
  3. The Pi alias-to-canonical credential fallback looks dead. The new precheck in run_pi calls provider_row("pi", request.provider) - the canonical provider - identically on both the aliased and canonical attempts in run_pi_with_provider_fallback, and its failure is ErrorClass::Permanent (not AuthRequired), so the retry condition never fires. This contradicts the fallbacks own doc comment describing exactly the subscription-auth case it is supposed to handle. (crates/mc-host/src/broca/pi.rs:222)

If #1-#3 are intentionally deferred to a later PR in this stack (client-side wiring lands separately), it would help to call that dependency out explicitly in the PR description - as written this PR alone appears to make Broca non-functional end-to-end.

Security / correctness:

  1. Launcher-verification failures are silently swallowed. resolve_generations .ok() on open_verified_file(PRODUCTION_LAUNCHER) turns a tamper/corruption detection into None, and spawn_detached treats None as "no launcher" and falls back to fexecve-ing current_exe() instead of failing closed. (crates/mc-module/src/bin/ck-mc-host.rs:652, also line 669)
  2. resolve_node has a verify-by-fd, exec-by-path TOCTOU gap, and its open_relative_file helper skips the owner-only directory checks that the full validate()/validate_tree() path enforces - an asymmetry not covered by the new test suite. (crates/mc-host/src/harness_closure.rs:135)
  3. libc::SYS_close_range has no macOS definition but is called with no cfg guard in a function that now has a macOS-specific execve branch a few lines below - this will fail to compile for macOS targets, with no macOS CI job to catch it. (crates/mc-module/src/bin/ck_mc_host/spawn.rs:157)
  4. New .expect() on a TOCTOU-able double-read of store_open.phase in health() - a concurrent phase transition between the two loads now panics instead of falling back gracefully (was .unwrap_or_else). (crates/mc-module/src/lib.rs:12596)

Behavioral regressions / minor:

  1. start/restart now unconditionally block reading stdin to EOF via read_launcher_envelope() - running the CLI directly from an interactive terminal (no redirect) will hang. (crates/mc-module/src/bin/ck-mc-host.rs:1284)
  2. The provider-alias canonicalization table (google-antigravity to google, openai-codex to openai) is now duplicated across 3-4 call sites (subprocess.rs::provider_row, subprocess.rs::credential_fingerprint, broca/mod.rs::CredentialVerifier::verify, pi.rs::pi_model_ref) - currently consistent, but a future edit to only one copy would silently desync credential lookup from fingerprint verification. (crates/mc-host/src/broca/subprocess.rs:190)

Smaller notes (not inline-commented, lower confidence/severity):

  • harness_closure.rs new hex() duplicates the existing helper in instance.rs verbatim; validate_hash duplicates synapse::bundle::validate_sha256_hex.
  • resolve_node() re-reads and re-hashes closure files from disk on every request with no caching, adding avoidable I/O/CPU to the hot request path.
  • validate_manifest only checks one direction of the Native-kind edge to NativeAddon-kind node relationship.
  • scripts/qualify-mc-host-production-inputs.ts: an unsupported_provider dynamic-import guard checks a map key (providers["amazon-bedrock"]) that can never be populated, making that safety check dead code; the harness list ["opencode", "pi"] is hardcoded at 5+ call sites; several inline SHA-256 digest computations duplicate the existing sha256File helper in smoke-mc-host-synapse.ts.
  • smoke-mc-host-synapse.ts invokes Linux-only tools (uname, ldd) before the platform guard that is supposed to gate them, so a non-Linux run crashes with a raw ENOENT instead of the intended clean failure message.
  • runtime.rs health-probe loop now runs immediately and polls every ~50ms with no backoff while activation_in_progress(), which could call handler.health() ~600x during a ~30s activation window.

Nice test coverage on the new harness_closure.rs module (tamper, mode, path-traversal, symlink cases all look solid) - the issues above are mostly at the seams between this PR and the rest of the stack, plus a couple of pre-existing patterns (TOCTOU-by-path) that got new callers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant