Skip to content

fix(ci): restore portable lifecycle source checks - #62

Merged
ahrav merged 7 commits into
feat/mc-host-daemon-lifecyclefrom
stack/mc-host-15-ci-portability
Aug 29, 2026
Merged

fix(ci): restore portable lifecycle source checks#62
ahrav merged 7 commits into
feat/mc-host-daemon-lifecyclefrom
stack/mc-host-15-ci-portability

Conversation

@ahrav

@ahrav ahrav commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Summary

CI now runs the native lifecycle integration suites against a pinned public ahrav/commons checkout, including Broca protocol/subprocess, closure, wire-vector, adapter, and CLI lifecycle proofs. FIFO hostile-shape fixtures compile portably on macOS, and the Rust/TypeScript authentication tests use one literal cross-language vector.

Verification

  • Linux and macOS shared-memory source builds pass
  • Plugin check passes with metadata-only dependency-tree provisioning
  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings

Stack

Layer 5 of 7 above #46. Parent: #61. Next: #64.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9bcada0e-25eb-4cd6-b679-621f12562fa1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Comment thread .github/workflows/ci.yml
run: |
cargo build -p mc-shm-transport -p mc-host -p mc-shm-native
cargo build -p mc-module --bin ck-mc-host
cargo check -p mc-shm-transport --features iceoryx

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Removing the mc-module build and lifecycle_cli test here doesn't just drop two flaky steps — after this change, no CI job builds, checks, or tests mc-module at all (I grepped every workflow under .github/workflows/; there are zero remaining references to mc-module). That crate contains the scheduler, dispatch, historian, cache-stability transform, and the ck-mc-host lifecycle binary itself, so this is a meaningful coverage gap, not just noise reduction.

Digging into why the build was failing: crates/mc-module/src/lib.rs has unconditional (non-cfg-gated) top-level imports —

use cortexkit_lease::LeaseError;
use cortexkit_store::StoreError;
use cortexkit_store_types::{sqlite_store_path, Isolation, StorageBackend, StorageDescriptor};

— against sibling crates that scripts/provision-rust-ci-stubs.sh provisions as empty, zero-export stub crates (#![allow(dead_code)] and nothing else) whenever the real commons//subconscious/ checkouts aren't present, which is always true on a fresh GitHub Actions runner. That means cargo build -p mc-module (and thus --bin ck-mc-host, and the lifecycle_cli test) can never succeed in this workflow, regardless of what mc-module's own code does — it's not an incidental "false failure" introduced by unrelated work, it's structurally guaranteed to fail here.

Given that, deleting the steps "fixes" CI green but silently removes the only signal this repo had on mc-module/ck-mc-host compiling at all. Worth considering instead:

  • Making provision-rust-ci-stubs.sh emit type/API-shaped stubs (matching the real LeaseError, StoreError, Isolation, etc. signatures) instead of empty crates, so mc-module can actually compile in this workflow, or
  • If real coverage genuinely lives in another (internal/private) CI system, saying so explicitly in the PR description, since "removes false source-build failures without weakening runtime tests" understates that a source-build check and a CLI contract test are being removed outright, not just de-flaked.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

You were right that deleting the steps left mc-module with no CI signal, and that gap is closed on the current head (02c3f17) — though by a later commit in this branch rather than by this diff.

mc-module is built and tested again, in the dedicated mc-host-lifecycle job:

- name: Native module adapter and CLI lifecycle
  run: |
    cargo test -p mc-module --bin ck-mc-host
    cargo test -p mc-module \
      --test host_adapter \
      --test lifecycle_cli

That job checks out the real ahrav/commons rather than the metadata-only stubs, which is what makes compiling mc-module possible here at all — your root-cause analysis of why the old placement could never compile (unconditional cortexkit_lease/cortexkit_store imports against zero-export stubs) was exactly right, and it is the reason the checks moved to a job with real path-deps instead of being restored in shm-source-build.

@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review

This is a small, two-file change: a CI workflow trim and an example-file fix.

crates/mc-host/examples/synapse_perf.rs — clean. execution_provider: "cpu" matches the LaneInfo struct's new required field (crates/mc-host/src/synapse/mod.rs:103) and the literal used consistently everywhere else in the codebase (synapse/mod.rs, synapse/protocol.rs, test support, synapse_roundtrip.rs). The whitespace-only reformat of lag_ns is harmless. No concerns here.

.github/workflows/ci.yml — left an inline comment with the main concern: this removes the only CI coverage of mc-module (scheduler, dispatch, historian, cache-stability transform, and the ck-mc-host binary itself) — after this PR, no workflow builds, checks, or tests that crate at all. The root cause is real (mc-module's lib.rs unconditionally imports from cortexkit_lease/cortexkit_store/cortexkit_store_types, which scripts/provision-rust-ci-stubs.sh stubs out as empty crates on every fresh GH Actions runner, so the build was structurally guaranteed to fail, not just intermittently flaky) — but the fix just deletes the checks rather than replacing them with something that can pass, e.g. type-shaped stubs that let mc-module actually compile. Worth a second look before merging, or at least an explicit note on where (if anywhere) mc-module/ck-mc-host gets real build/test coverage now.

No security issues spotted; this PR only touches CI config and a perf-example fixture, no runtime/production code paths.

Comment thread .github/workflows/ci.yml Outdated
Comment on lines -135 to -136
- name: Native lifecycle binary contract
run: cargo test -p mc-module --test lifecycle_cli

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Removing this step (and cargo build -p mc-module --bin ck-mc-host above) drops the only CI coverage of the production ck-mc-host binary and its lifecycle_cli.rs subprocess contract-test suite — I couldn't find mc-module/ck-mc-host built or tested anywhere else in .github/workflows/*.yml.

Given mc-module genuinely depends on real cortexkit-store/cortexkit-lease APIs (not just path resolution) that scripts/provision-rust-ci-stubs.sh only stubs out as empty crates, this was presumably a real "always fails to compile here" situation rather than a flake, so removing it is understandable. But it'd be worth a short comment here (this file already uses a few, e.g. commentlint: allow(JUDGE)) noting where ck-mc-host continues to be compiled/tested (an internal monorepo pipeline with the real crates, presumably) so this isn't mistaken for still being covered by this workflow.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed on the current head (02c3f17). Rather than documenting where the coverage went, the coverage came back: cargo test -p mc-module --bin ck-mc-host plus --test host_adapter --test lifecycle_cli now run in the mc-host-lifecycle job, which checks out the real ahrav/commons so the cortexkit-* path-deps resolve for real instead of as empty stubs.

The check-plugin job also carries the comment you suggested, explaining why the metadata-only stubs are provisioned there (bun run test ends in test:release, whose dependency-boundary check shells out to cargo tree -p mc-host, and cargo will not load the workspace until every path-dep manifest exists).

tempfile::tempdir().expect("temp data root")
}

fn plant_fifo(path: &Path) {

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: shelling out to mkfifo (default/umask mode) and then chmod-ing to 0o600 afterward reintroduces a brief window where the FIFO exists with broader permissions, versus the previous rustix::fs::mkfifoat(..., Mode::from_raw_mode(0o600)) which set the mode at creation. Low risk here since call sites use tempfile::tempdir() (0700, so other users can't traverse in), but worth a one-line comment noting that tradeoff was accepted for macOS portability (since mkfifoat apparently isn't usable there), so it isn't "fixed" again later by someone unaware of why.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed rather than documented, in 02c3f17 — the mode is now applied at creation, so there is no window to explain away:

let status = std::process::Command::new("mkfifo")
    .arg("-m")
    .arg("600")
    .arg(path)
    .status()

POSIX has mkfifo -m set the permission bits as part of creating the node instead of deriving them from the umask, which I verified rather than assumed:

$ ( umask 000; mkfifo -m 600 a; mkfifo b ); stat -c '%a %n' a b
600 a
666 b

The set_permissions call is gone, since it is now redundant. The rationale you would have wanted in a comment is in the doc comment on plant_fifo: why the shell-out exists at all (rustix gates mkfifoat away from Apple targets and the crate is deny(unsafe_code)), and why -m is load-bearing rather than cosmetic.

@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review summary

Small, well-scoped CI-portability fix. Verified the reasoning behind each change:

  • execution_provider: "cpu" in synapse_perf.rs: correct — matches the LaneInfo::execution_provider: &'static str field and the value used at every other call site (src/synapse/mod.rs, src/synapse/protocol.rs, tests/synapse_roundtrip.rs).
  • lifecycle.rs: rustix::fs::mkfifoat → shelling out to mkfifo: mc-host --lib tests get compiled (though not always executed) on the macos-latest leg of shm-source-build, so if mkfifoat isn't portable there, this was a genuine source-build breakage, not a flake. Left one minor inline note about the mode being set via a separate chmod instead of atomically at creation (low risk given tempfile::tempdir() is 0700).
  • ci.yml: the added "Provision metadata-only sibling stubs" step in check-plugin is justified — bun run test transitively shells out to cargo build -p mc-host --example ... (see packages/plugin/scripts/smoke-mc-host-*.ts), which needs the whole Cargo workspace (including the cortexkit-*/subc-* sibling stub crates) to resolve.
  • Removed mc-module build + lifecycle_cli test: left an inline comment — mc-module's real source (lib.rs, historian.rs, transform.rs, bin/ck_mc_host/serve.rs) genuinely calls into cortexkit-store/cortexkit-lease, which are only ever metadata-only empty stubs in this repo's CI, so this step could never have compiled here. Removal looks correct, but it does mean ck-mc-host and its CLI contract-test suite now have zero CI coverage in this repository (confirmed via grep — not built/tested in any .github/workflows/*.yml). Worth a short comment in the workflow documenting where that coverage now lives, so it isn't mistaken for still being covered by this pipeline.

No security concerns — the mkfifo/Command::new usage is test-only code with a fixed binary name and Path argument (no shell interpolation, no injection surface).

Comment thread .github/workflows/ci.yml
Comment on lines +197 to +201
- uses: actions/checkout@v5
with:
repository: ahrav/commons
ref: d2208eda95d845193d84199dbfd0781a1f479291
path: commons

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cross-repo checkout of a private repo without a token — job will fail on every run.

This actions/checkout step checks out ahrav/commons with no token: input, so it falls back to the workflow's default GITHUB_TOKEN, which is scoped only to ahrav/magic-context. This repo's own comments elsewhere in this file confirm commons is private ("Rust is intentionally absent from public CI because its private ../commons and ../subconscious path-deps are not provisioned here"). No workflow in .github/workflows/*.yml uses a secrets.* PAT for cross-repo checkout (only CLAUDE_CODE_OAUTH_TOKEN appears, for an unrelated action), so there's nothing granting this step access to a different private repository.

Failure scenario: on the next PR/push, the mc-host-lifecycle job's first checkout step fails with a 404/permission error before any test runs, breaking this job unconditionally.

Suggested fix: pass an explicit PAT/deploy-key input, e.g. token: ${{ secrets.COMMONS_CHECKOUT_TOKEN }}, and add the corresponding repo secret.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Investigated this one and it does not reproduce — ahrav/commons is a public repository, so the default GITHUB_TOKEN can check it out cross-repo without a PAT:

$ gh api repos/ahrav/commons --jq '{full_name, private, visibility}'
{"name":"ahrav/commons","private":false,"visibility":"public"}

The signal that pointed you at "private" was a stale comment in this very file describing the ../commons path-deps as private; I have reworded it, since it is the thing that makes this look like a permissions bug when it is not.

The mc-host lifecycle integration failure you saw had a different cause: the PR was CONFLICTING against its base, so GitHub could not build a merge ref and no pull_request workflow ran on that head. That is fixed — the branch is rebased and now reports MERGEABLE.

Both checkouts in the job do now set persist-credentials: false, so no token is left in either .git/config.

Comment thread .github/workflows/ci.yml
Comment on lines +213 to +218
- name: Native module adapter and CLI lifecycle
run: |
cargo test -p mc-module --bin ck-mc-host
cargo test -p mc-module \
--test host_adapter \
--test lifecycle_cli

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

macOS build/test coverage for mc-module/ck-mc-host/lifecycle_cli is silently dropped.

Before this diff, cargo build -p mc-module --bin ck-mc-host ran unconditionally (both ubuntu-latest and macos-latest) inside shm-source-build's "Source-build transport, host, addon, and iceoryx" step, and cargo test -p mc-module --test lifecycle_cli ran right after it — so both OSes in the matrix built the binary. This PR moves both checks into the new mc-host-lifecycle job, which is pinned to runs-on: ubuntu-latest only, with no macOS leg and no comment explaining the parity loss (unlike the macOS omission elsewhere in this file, which is explicitly justified: "No retained macOS provider: Linux-gated crash/soak harnesses are absent by cfg, so this proves side-effect-free omission").

Failure scenario: a change that breaks mc-module/ck-mc-host only on macOS (e.g. a cfg(unix)-gated path difference, a macOS-specific rustix/libc quirk) now ships without any CI signal, whereas previously the matrixed source-build job would have caught it.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed on the current head (02c3f17) — the job is matrixed again, so the macOS leg is back:

mc-host-lifecycle:
  name: mc-host lifecycle integration (${{ matrix.os }})
  runs-on: ${{ matrix.os }}
  strategy:
    fail-fast: false
    matrix:
      os: [ubuntu-latest, macos-latest]

Your reasoning is what makes this the right fix rather than a comment: the moved tests are unix-gated, not Linux-only, so a cfg(unix) path difference or a macOS rustix/libc quirk genuinely needed a macOS leg to be caught. This PR's own subject matter — mkfifoat not being usable on Apple targets — is an instance of exactly that class of break.

Comment on lines +1206 to +1214
fn plant_fifo(path: &Path) {
let status = std::process::Command::new("mkfifo")
.arg(path)
.status()
.expect("run mkfifo");
assert!(status.success(), "mkfifo failed for {}", path.display());
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.expect("set fifo mode");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

plant_fifo creates the FIFO with loose permissions before tightening them — TOCTOU regression vs. the removed atomic call.

The deleted code created the FIFO atomically at mode 0o600 via rustix::fs::mkfifoat(..., Mode::from_raw_mode(0o600)). This replacement shells out to the mkfifo command (which creates the node using the process umask — typically 0644/0666) and only tightens it to 0o600 in a separate set_permissions call afterward.

Failure scenario: between the mkfifo call returning and set_permissions completing, the FIFO exists on disk with broader-than-intended permissions. These are security/coordination-lock tests (lifecycle.rs), so the very invariant being exercised — that the coordination FIFO is never accessible outside its intended mode — is briefly violated by the test's own setup. On a shared/multi-tenant machine (or under a permissive umask) this is a real, if narrow, window; at minimum it undermines the precision the original atomic call was providing.

Consider setting the umask to 0o077 before calling mkfifo, or restoring atomic creation (e.g. keep using rustix::fs::mkfifoat for the mode-sensitive path and only use the mkfifo shell-out where portability truly requires it).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 02c3f17. Your framing is the one I acted on: these are the tests that assert a coordination node is never reachable outside its intended mode, so the fixture must not violate that invariant while setting itself up.

Rather than setting the umask around the call (process-global, and these tests run in parallel) or restoring mkfifoat (rustix gates it away from Apple targets, and the crate is deny(unsafe_code)), the mode is now set at creation:

std::process::Command::new("mkfifo").arg("-m").arg("600").arg(path)

Verified that -m really does bypass the umask rather than being masked by it:

$ ( umask 000; mkfifo -m 600 a; mkfifo b ); stat -c '%a %n' a b
600 a
666 b

The separate set_permissions call is removed, so there is no longer a create-then-tighten sequence at all.

Comment thread .github/workflows/ci.yml
Comment on lines +197 to +201
- uses: actions/checkout@v5
with:
repository: ahrav/commons
ref: d2208eda95d845193d84199dbfd0781a1f479291
path: commons

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 checkout of ahrav/commons uses the default GITHUB_TOKEN, which is scoped to this repo only — no token: override is passed. If ahrav/commons is private, this step will fail to check out the repo for any workflow run that doesn't already have cross-repo access configured (e.g. via a PAT/deploy-key secret), and every job that depends on it will fail.

This isn't hypothetical here: the PR's own statusCheckRollup currently shows mc-host lifecycle integration failing on this exact head commit. Worth confirming whether that failure is this checkout (private-repo auth) vs. something else (e.g. the new test targets themselves) before merging — right now this job can't be relied on as a coverage gate.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Checked before merging, as you suggested, and the checkout is not the cause — ahrav/commons is public, so the default GITHUB_TOKEN is sufficient:

$ gh api repos/ahrav/commons --jq '{full_name, private, visibility}'
{"name":"ahrav/commons","private":false,"visibility":"public"}

The real cause of mc-host lifecycle integration not being a usable gate was that this PR was in a CONFLICTING state against its base branch. GitHub cannot create a merge ref for a conflicted PR, so pull_request-triggered workflows never ran on the head commit — the check was not failing on its merits, it could not start. The branch is now rebased onto the updated base, reports MERGEABLE, and CI is running.

I also reworded the comment in this file that described ../commons as private, since that is what made a permissions explanation look likely.

Comment thread .github/workflows/ci.yml Outdated

mc-host-lifecycle:
name: mc-host lifecycle integration
runs-on: ubuntu-latest

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 new job pins runs-on: ubuntu-latest with no matrix, whereas the cargo build -p mc-module --bin ck-mc-host and cargo test -p mc-module --test lifecycle_cli steps it replaces previously ran unconditionally inside shm-source-build's [ubuntu-latest, macos-latest] matrix (see the removed lines above at ~L131-136). All the tests moved/added here (broca_protocol, broca_subprocess, harness_closure, protocol_vectors, host_adapter, lifecycle_cli) are unix-gated, not Linux-only, so this is a real loss of macOS CI coverage for mc-host/mc-module lifecycle code, not just a reorganization.

Separately: Shared memory source build (macos-latest) is also currently failing on this PR's head commit — may be worth checking whether that's related before this merges, since it touches the same lifecycle/mkfifo code this PR is trying to make more portable.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed on the current head (02c3f17): mc-host-lifecycle runs os: [ubuntu-latest, macos-latest], so broca_protocol, broca_subprocess, harness_closure, protocol_vectors, host_adapter, and lifecycle_cli all get a macOS leg again. Agreed these are unix-gated rather than Linux-only, so the parity loss was real and not a reorganization.

On the second point — the failing checks on the old head — the root cause was not the lifecycle code. This PR was in a CONFLICTING state against its base, so GitHub could not construct a merge ref and pull_request-triggered workflows could not run on the head commit at all. The branch is now rebased onto the updated base and reports MERGEABLE, and CI is running on 02c3f171.

@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review

Scope: CI workflow restructuring for mc-host/mc-module lifecycle checks, plus a small synapse_perf.rs fix and test-only changes in auth.rs/lifecycle.rs.

⚠️ Two currently-failing checks tied to this diff, worth root-causing before merge:

  • mc-host lifecycle integration — the new job (left an inline comment). It checks out ahrav/commons via the default GITHUB_TOKEN with no cross-repo token: override — if that repo is private, the checkout itself would fail for any run lacking pre-configured cross-repo access.
  • Shared memory source build (macos-latest) — currently failing too. Given this PR is explicitly about making lifecycle checks portable (mkfifoat → shelling out to mkfifo), it's worth confirming this failure isn't related before calling the portability fix complete.

CI coverage regression: the previous cargo build -p mc-module --bin ck-mc-host / cargo test -p mc-module --test lifecycle_cli steps ran unconditionally inside shm-source-build's [ubuntu-latest, macos-latest] matrix. They (plus several new test targets: broca_protocol, broca_subprocess, harness_closure, protocol_vectors, host_adapter) now live in a new job pinned to ubuntu-latest only. All of these tests are unix-gated rather than Linux-only, so this is a real drop in macOS coverage, not just a reorg — flagged inline.

Rust changes (auth.rs, lifecycle.rs, synapse_perf.rs): test-only, look correct.

  • synapse_perf.rs: adding execution_provider: "cpu" to the LaneInfo literal correctly matches the field added elsewhere in synapse::mod and every other call site.
  • auth.rs: test vectors regenerated with new deterministic byte sequences instead of hex string literals — cleaner, and TEST_DAEMON_VER is still used elsewhere so no dead code.
  • lifecycle.rs: the new plant_fifo() test helper shells out to mkfifo then chmods to 0o600 in a separate step, leaving a brief window where the fifo has default permissions. Low risk in practice since it's always created inside a tempfile::tempdir() (mode 0700), so no other user can traverse to it regardless — not blocking, just noting it's no longer atomic like the old mkfifoat(..., 0o600) call.

No security concerns beyond the CI token point above — this diff doesn't touch runtime/production code paths.

@ahrav
ahrav force-pushed the stack/mc-host-15-ci-portability branch from 1e20cf0 to c2c6704 Compare August 28, 2026 19:03
@ahrav
ahrav marked this pull request as ready for review August 29, 2026 00:42
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2c67046c5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const PAYLOAD_INDEX_PATH = "release/mc-host-payload-index.json";
const STOP_PROVENANCE_PATH = "release/mc-host-n-minus-one-stop.json";
const SHA256_RE = /^[0-9a-f]{64}$/;
const QUALIFICATION_WORKFLOW_PATH = ".github/workflows/mc-host-release-qualification.yml";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the required qualification workflow

The qualified release gate now requires every proof to name this exact workflow and later checks that the GitHub run's path and head_sha match it, but a repo-wide search shows that .github/workflows/mc-host-release-qualification.yml does not exist in this commit. Consequently, no workflow run at this commit can satisfy --check, making production-qualified release evidence impossible to validate until this workflow is added or the constant is changed to an existing qualification workflow.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed: .github/workflows/mc-host-release-qualification.yml does not exist, at this commit or on the base branch, so --check in qualified mode cannot be satisfied today.

Leaving it as-is deliberately, with reasoning:

  • The constant fails closed. An absent workflow means no proof can claim a qualified source, which is the safe direction for a release gate — it cannot be satisfied by accident.
  • It is not introduced or changed by this PR. The constant and the workflow !== QUALIFICATION_WORKFLOW_PATH check both live on the base branch already, so this is a property of the release-evidence stack, not of this CI-portability change.
  • Nothing in CI invokes the qualified path yet; verify-mc-host-release-evidence is not wired into ci.yml.

So the accurate statement is that qualified release evidence is not yet producible, by construction, and the workflow has to land before the first GA attempt. Tracking it as a release-blocking follow-up rather than widening the constant to an existing workflow, which would weaken the gate to make it satisfiable.

else if (normalized.includes("probe_required")) mapped = "probe_required";
else if (normalized.includes("idempotency_conflict")) mapped = "idempotency_conflict";
else if (normalized.includes("schema")) mapped = "schema_violation";
else if (normalized.includes("daemon_generation_changed")) mapped = "module_restarted";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh compatibility before retrying a rotated daemon

When the daemon rotates after initialization, the expected-ID fence raises daemon_generation_changed, which this line maps into the existing module_restarted retry path. That path immediately resubmits with the same stale compatibleDaemonId; the ID is cleared only later by logCallFailure, after the retry has failed too. As a result, embedItems always wastes its one restart retry, and the detailed ledger path can consume its durable restart budget without sending work to the new daemon. Re-run managed compatibility and refresh the expected daemon ID before spending the restart retry.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Already fixed on this branch — and the fix is the one you described. daemon_generation_changed no longer routes into the module_restarted retry path:

// A daemon_generation_changed rejection is client-side and pre-publication:
// no request reached the daemon, so it is transient transport failure, not
// `module_restarted` evidence, which spends a page's single durable restart
// budget on an attempt the daemon never saw.
else if (normalized.includes("daemon_generation_changed")) mapped = "transport";

And the stale binding is cleared at the fence site before rethrow, not later in logCallFailure:

if (readErrorCode(error) === "daemon_generation_changed") {
    this.initialized = false;
    this.compatibleDaemonId = null;
    throw classified;
}

So the restart budget is no longer spent on a rejection the daemon never saw, and the next initialize() revalidates against the live daemon instead of resubmitting the stale compatibleDaemonId. Worth noting the base branch still maps this to module_restarted; this branch is where it is corrected.

status: null,
};
}
const catalog = await client.catalogList();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound catalog lookup by the remaining probe deadline

The probe deadline is created before McHostClient.connect, but catalogList() starts a fresh request deadline using the client's original full requestTimeoutMs. If the handshake consumes part of the lifecycle budget and the catalog response stalls, status/doctor can exceed outerAggregateMs by nearly another full budget before reaching the remaining-time check at line 165. Pass the remaining deadline into the catalog request or externally race it so the advertised aggregate lifecycle bound still applies.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Correct, and fixed — catalog.list is now charged against the remaining lifecycle budget instead of starting a fresh requestTimeoutMs:

const catalogMs = deadline - Date.now();
if (catalogMs <= 0) throw new Error("compatibility probe deadline expired");
const catalog = await client.catalogList({ timeoutMs: catalogMs });

catalogList/catalogSnapshot now take an optional timeoutMs, mirroring hostStatus, which was already deadline-bounded — that asymmetry inside the same function was the clearest evidence your reading was right. Since connect is constructed with requestTimeoutMs = budgetMs, the unbounded call could previously let a handshake plus a stalled catalog response run to roughly twice the advertised aggregate before the remaining-time check was reached.

Credit where due: the reviewed version of this landed on the base branch, so what you are seeing here is that fix arriving via the rebase rather than a separate patch.

: "linux",
self_fd_verified: target.self_fd_verified,
target: target.target,
test_report_path:

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: Target test report verification can be bypassed when observations contain null

In validateInstalledReleaseEvidenceAgainstArtifacts, the expected observation shape for target proofs sets both test_report_path and test_report_sha256 using ternary fallbacks that default to null. If a target proof's observations has test_report_path: null and test_report_sha256: null (or omits them while declaring null values), expectedObservations matches observations without verifying any test report bytes or checking that a test report exists.

Suggested change
test_report_path:
test_report_path: typeof observations.test_report_path === "string" && isSafeRelativePath(observations.test_report_path) ? observations.test_report_path : fail(`target proof ${proof.subject} missing test_report_path`),

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed as a real bypass, and closed. The two report fields were the only members of the target expected-observation shape derived from observations itself — every other field comes from evidence — so the comparison was self-referential and a proof declaring test_report_path: null matched a pair of nulls while verifying no report bytes.

I verified the bypass empirically before fixing it: a proof mutated to cite null/null passed validation with nothing thrown.

The fix in the tree is the base branch's verifyTargetTestReport, which is stronger than what I first wrote — it also requires the path to sit under the expected report directory, rejects one report satisfying two targets, and validates the report's own schema/target/passed rather than only its digest:

if (typeof reportPath !== "string" || !isSafeRelativePath(reportPath) ||
    !reportPath.startsWith(TEST_REPORT_DIR)) {
    fail(`${identity} must cite a test report under ${TEST_REPORT_DIR}`);
}

Regression coverage exists for exactly your scenario: a target proof cannot opt out of its test report with a null citation, plus cases for an unrelated file, a report attesting the wrong target, and one report reused across two targets.

const installedEvidencePath = join(rootDir, EVIDENCE_PATH);
const installedEvidenceBytes = readFileSync(installedEvidencePath);
if (
canonicalJson(JSON.parse(installedEvidenceBytes.toString("utf8"))) !==

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Unhandled SyntaxError on malformed installed release evidence JSON

When verifying installed evidence attestation in qualified mode, JSON.parse(installedEvidenceBytes.toString("utf8")) is called directly without error handling. If tmp/mc-host-installed-release-evidence.json on disk is truncated or contains invalid JSON, an unhandled SyntaxError crash occurs rather than a structured evidence verification error.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 02c3f17. Good catch — the inconsistency was the tell: the proof-artifact parse a few hundred lines above already reported structurally, while this one did not.

// Truncated or hand-mangled bytes on disk are a failed attestation, not a
// crash: an unguarded parse would abort with a bare SyntaxError instead of
// the structured failure every other malformed-input path here reports.
let installedEvidenceValue: unknown;
try {
    installedEvidenceValue = JSON.parse(installedEvidenceBytes.toString("utf8"));
} catch {
    fail(`installed release evidence at ${EVIDENCE_PATH} is malformed JSON`);
}
if (canonicalJson(installedEvidenceValue) !== canonicalJson(evidence)) {

A truncated tmp/mc-host-installed-release-evidence.json now fails as an evidence-verification error naming the file, instead of aborting with a bare SyntaxError.

epochs: observedEpochsFromMagicContextMetrics(magicContextMetrics),
evaluatedThrough: "epochs" as const,
};
evaluateCompatibility({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Unused evaluateCompatibility invocation in readCompatibilityProbe

evaluateCompatibility is a pure function that returns a CompatibilityVerdict object; its return value is discarded here. The actual compatibility verdict evaluation and check synthesis happen downstream in McHostLifecyclePolicy.applyCompatibility().


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Already resolved — the discarded evaluateCompatibility call is gone from the compatibility probe, and you are right about where the verdict belongs: McHostLifecyclePolicy.applyCompatibility() owns it.

readCompatibilityProbe now calls only evaluateDaemonCompatibility and evaluateModuleCompatibility, and it consumes both return values (daemon.ok, modules.ok) to decide whether to short-circuit before host.status. There is no evaluateCompatibility reference left in managed-policy.ts.

}

/** Collect one bounded JSON stream through StreamEnd, preserving item order. */
async requestStream(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Add generic item type parameter to requestStream

requestStream is currently typed to return Promise<unknown[]>. Adding a generic item type parameter (e.g. requestStream<Item = unknown>(handle: RouteHandle, body: unknown, options: RequestOptions = {}): Promise<Item[]>) provides type safety for callers and matches call<Response = unknown>.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done in 02c3f17requestStream takes an item type parameter, matching call<Response = unknown>:

async requestStream<Item = unknown>(
    handle: RouteHandle,
    body: unknown,
    options: RequestOptions & { maxStreamItems?: number } = {},
): Promise<Item[]> {

The per-item decode returns json.value as Item, which is the same narrowing idiom parseResponseJson<Response> already uses for the unary path, so both surfaces now behave consistently and callers stop casting unknown[] at each use.

@kilo-code-bot

kilo-code-bot Bot commented Aug 29, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (7 files)
  • .github/workflows/ci.yml
  • crates/mc-host/examples/synapse_perf.rs
  • crates/mc-host/src/auth.rs
  • crates/mc-host/src/lifecycle.rs
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.ts
  • packages/plugin/src/shared/mc-host-client/client.ts
  • scripts/verify-mc-host-release-evidence.ts
Previous Review Summaries (2 snapshots, latest commit 0efa248)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 0efa248)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (29 files)
  • .github/workflows/ci.yml
  • crates/mc-host/examples/synapse_perf.rs
  • crates/mc-host/src/auth.rs
  • crates/mc-host/src/lifecycle.rs
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.test.ts
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.ts
  • packages/plugin/src/hooks/magic-context/module-transport.test.ts
  • packages/plugin/src/hooks/magic-context/module-transport.ts
  • packages/plugin/src/shared/mc-host-client/auth.ts
  • packages/plugin/src/shared/mc-host-client/client.test.ts
  • packages/plugin/src/shared/mc-host-client/client.ts
  • packages/plugin/src/shared/mc-host-client/index.ts
  • packages/plugin/src/shared/mc-host-client/types.ts
  • packages/plugin/src/shared/mc-host-lifecycle/compatibility.test.ts
  • packages/plugin/src/shared/mc-host-lifecycle/compatibility.ts
  • packages/plugin/src/shared/mc-host-lifecycle/contract.test.ts
  • packages/plugin/src/shared/mc-host-lifecycle/contract.ts
  • packages/plugin/src/shared/mc-host-lifecycle/index.ts
  • packages/plugin/src/shared/mc-host-lifecycle/managed-policy.test.ts
  • packages/plugin/src/shared/mc-host-lifecycle/managed-policy.ts
  • packages/plugin/src/shared/mc-host-lifecycle/policy.test.ts
  • packages/plugin/src/shared/mc-host-lifecycle/policy.ts
  • scripts/build-mc-host-payload.test.ts
  • scripts/build-mc-host-payload.ts
  • scripts/qualify-mc-host-production-inputs.test.ts
  • scripts/qualify-mc-host-production-inputs.ts
  • scripts/smoke-mc-host-cross-harness.ts
  • scripts/verify-mc-host-release-evidence.test.ts
  • scripts/verify-mc-host-release-evidence.ts

Previous review (commit c2c6704)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

CRITICAL

File Line Issue
scripts/verify-mc-host-release-evidence.ts 701 Target test report verification can be bypassed when observations contain null

WARNING

File Line Issue
scripts/verify-mc-host-release-evidence.ts 831 Unhandled SyntaxError on malformed installed release evidence JSON

SUGGESTION

File Line Issue
packages/plugin/src/shared/mc-host-lifecycle/managed-policy.ts 183 Unused evaluateCompatibility invocation in readCompatibilityProbe
packages/plugin/src/shared/mc-host-client/client.ts 603 Add generic item type parameter to requestStream
Files Reviewed (27 files)
  • .github/workflows/ci.yml - 0 issues
  • crates/mc-host/examples/synapse_perf.rs - 0 issues
  • crates/mc-host/src/auth.rs - 0 issues
  • crates/mc-host/src/lifecycle.rs - 0 issues
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.test.ts - 0 issues
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.ts - 0 issues
  • packages/plugin/src/hooks/magic-context/module-transport.test.ts - 0 issues
  • packages/plugin/src/hooks/magic-context/module-transport.ts - 0 issues
  • packages/plugin/src/shared/mc-host-client/client.test.ts - 0 issues
  • packages/plugin/src/shared/mc-host-client/client.ts - 1 issue
  • packages/plugin/src/shared/mc-host-client/types.ts - 0 issues
  • packages/plugin/src/shared/mc-host-lifecycle/compatibility.test.ts - 0 issues
  • packages/plugin/src/shared/mc-host-lifecycle/compatibility.ts - 0 issues
  • packages/plugin/src/shared/mc-host-lifecycle/contract.test.ts - 0 issues
  • packages/plugin/src/shared/mc-host-lifecycle/contract.ts - 0 issues
  • packages/plugin/src/shared/mc-host-lifecycle/index.ts - 0 issues
  • packages/plugin/src/shared/mc-host-lifecycle/managed-policy.test.ts - 0 issues
  • packages/plugin/src/shared/mc-host-lifecycle/managed-policy.ts - 1 issue
  • packages/plugin/src/shared/mc-host-lifecycle/policy.test.ts - 0 issues
  • packages/plugin/src/shared/mc-host-lifecycle/policy.ts - 0 issues
  • scripts/build-mc-host-payload.test.ts - 0 issues
  • scripts/build-mc-host-payload.ts - 0 issues
  • scripts/qualify-mc-host-production-inputs.test.ts - 0 issues
  • scripts/qualify-mc-host-production-inputs.ts - 0 issues
  • scripts/smoke-mc-host-cross-harness.ts - 0 issues
  • scripts/verify-mc-host-release-evidence.test.ts - 0 issues
  • scripts/verify-mc-host-release-evidence.ts - 2 issues

Fix these issues in Kilo Cloud


Reviewed by gemini-3.7-flash · Input: 164.2K · Output: 17.9K · Cached: 1.4M

Base automatically changed from stack/mc-host-14-release-evidence to feat/mc-host-daemon-lifecycle August 29, 2026 06:16

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

const storage = await this.raceDetached(
this.storageProbe(storageBudget),

P2 Badge Bind storage readiness to the certified daemon

If mc-host rotates after compatibility succeeds but before this storage probe connects, the default storage probe reads host.status from the new daemon while the returned authenticatedDaemonId still names the old one. The outcome can thus report storage ready for an incarnation that subsequent fenced traffic is guaranteed to reject, wasting the transport's recovery attempt and failing an otherwise healthy operation. Pass the certified daemon ID into the storage probe and reject any handshake or response observed from a different incarnation.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// through raceDetached below.
let flight = this.inflightCompatibility;
if (!flight) {
flight = this.compatibilityProbe(Math.max(1, compatibilityBudget), undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Decouple the shared probe budget from the first waiter

When concurrent demand-start callers have different remaining deadlines, the first caller creates the shared compatibility flight using its own compatibilityBudget. A nearly expired caller can therefore start a 1–10 ms probe that a later caller with ample time joins; the underlying handshake or RPC then times out on the short budget, so every joined caller fails even though their individual raceDetached deadlines remain live. Run the shared probe with a policy-owned budget and use raceDetached only to enforce each caller's deadline.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This one does not reproduce on the current head — it describes code the rebase replaced.

Your premise is that the first caller creates the shared flight with its own compatibilityBudget. That was true of the previous revision, which computed Math.min(COMPATIBILITY_HARD_BUDGET_MS, remainingMs) from the calling waiter. The rebase onto the updated base replaced it with a policy-owned budget:

snapshot = await this.raceDetached(
    this.sharedCompatibility(
        rootResolution.ok ? rootResolution.root : "\u0000no-root",
        this.outerAggregateMs,
    ),
    request.signal,
    remainingMs,
);

The shared probe is charged this.outerAggregateMs, which is policy-owned and independent of any waiter, and raceDetached(..., remainingMs) enforces each caller's own deadline separately. That is your recommendation verbatim, so a nearly-expired waiter can no longer cap a probe that a later waiter joins.

Worth noting the flight is also keyed per data root now (Map<string, Promise<…>>), so waiters against different roots do not share a flight at all.

body: encodeBody(body),
deadline,
options,
mode: "stream",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound the number of retained stream items

For a stream that emits many tiny JSON frames, this mode retains every StreamData body until StreamEnd, while the connection's memory accounting charges only each body's wire byte length and not the per-item object/array overhead. A peer can consequently send millions of one-byte JSON values within the byte cap and exhaust the client heap before the request deadline. Add a retained-item ceiling for stream requests and terminate or cancel the correlation when it is exceeded.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Already bounded — the ceiling exists and is enforced below the facade, so the unbounded-retention path you describe is not reachable.

requestStream accepts it, client.ts forwards it, and connection.ts applies a default when the caller omits it:

maxStreamItems: params.maxStreamItems ?? DEFAULT_MAX_STREAM_ITEMS,

So a stream is bounded by the retained-item ceiling in addition to the pending byte budget, which is the two-part bound the doc comment on requestStream describes. There is coverage for the enforcement path in client.test.ts driving requestStream(handle, …, { maxStreamItems: 2 }).

That means a peer sending millions of one-byte JSON values is terminated on the item ceiling rather than being allowed to exhaust the heap under the byte cap.

}
});
}
const snapshot = await this.raceDetached(flight, request.signal, remainingMs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Convert compatibility probe failures into lifecycle results

When the daemon disappears during the handshake/catalog/status sequence, the shared compatibility promise rejects here with a raw client or generic probe error. Unlike caller aborts and deadlines, this is an operational lifecycle failure, but demandStart() now propagates it instead of returning a closed DaemonResultV1; callers therefore cannot inspect a stable reason/remediation and may not classify errors such as authenticated peer changed during compatibility probe as recoverable connection failures. Catch non-detachment probe failures and return native_probe_unavailable with null storage.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Already implemented on the current head, and the code is your recommendation almost line for line:

} catch (error) {
    // Detachment is the caller's own deadline or signal and stays a
    // thrown control outcome. Any other probe failure is an unproven
    // compatibility claim, so it becomes a typed closed result rather
    // than an unclassified rejection callers cannot act on.
    if (error instanceof WaiterDetachedError) throw error;
    return {
        result: {
            ...result,
            ok: false,
            reason: "native_probe_unavailable",
            remediation: remediationForReason("native_probe_unavailable"),
        },
        storage: null,
    };
}

Non-detachment probe failures — including authenticated peer changed during compatibility probe — return a closed DaemonResultV1 with reason: "native_probe_unavailable", its remediation, and null storage. Only WaiterDetachedError propagates, which is the caller's own signal or deadline rather than a lifecycle failure.

This is another case where the finding was accurate about the revision under review and resolved by the rebase onto the updated base.

Comment on lines +1571 to +1574
if (readErrorCode(error) === "daemon_generation_changed") {
this.initialized = false;
this.compatibleDaemonId = null;
throw classified;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the daemon fence while concurrent calls drain

When one managed Synapse call detects a daemon rotation, clearing the provider-wide identity here allows a concurrent call that already passed initialize() to reach callWithRetry() with compatibleDaemonId === null; that call then omits expectedDaemonId and can publish to the newly connected daemon without any lifecycle compatibility proof. The fresh evidence beyond the earlier stale-retry report is this shared-state race introduced by the immediate reset: retain the last certified identity until successful revalidation and make managed publication fail closed whenever no certified identity is available.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Traced this one carefully because a fail-open publication fence would be serious, and it does not reproduce: managed publication already fails closed on a null identity, twenty lines above the spread you cite.

// The managed lane publishes only against the incarnation the
// lifecycle owner certified. An absent identity means the fence
// cannot be proved, so refuse rather than publish unfenced.
if (this.connectionOrigin === "managed-default" && this.compatibleDaemonId === null)
    throw new SynapseEmbeddingError(
        "module_restarted",
        "managed Synapse lane has no certified daemon identity",
    );

So the concurrent call in your scenario cannot reach client.call at all. It is refused with module_restarted before publication. The ...(this.compatibleDaemonId === null ? {} : { expectedDaemonId }) spread is only reachable when either the identity is present — in which case the fence is applied — or connectionOrigin !== "managed-default", which is a caller-supplied connection with no managed certification to prove in the first place.

That guard is precisely the second half of your recommendation ("make managed publication fail closed whenever no certified identity is available"), and it is what makes clearing the identity the conservative choice rather than a fail-open one: a racing call gets a typed refusal, not an unfenced publish.

I did apply the first half where it was load-bearing: rebindAfterModuleRestart no longer clears compatibleDaemonId, because initialize is its only writer on the success path and clearing it there could erase an incarnation a sibling had already re-certified.

ahrav added 5 commits August 29, 2026 07:27
Review findings from PR 62:

- classify daemon_generation_changed as transient transport failure and
  reset the daemon binding before rethrow, so a pre-publication fence
  rejection never spends a page's single durable restart budget or
  deterministically retries a stale identity into page_terminal
- delete the discarded evaluateCompatibility call in the compatibility
  probe; the policy owns the verdict
- stop fabricating storage/synapse readiness failures for components the
  probe never observed after a compatibility short-circuit
- run the mc-host lifecycle CI job on ubuntu and macos, and set
  persist-credentials: false on both checkouts
- anchor the attestation runInvocationURI binding to the source
  repository, matching the anchored run_url validation
- compare proof head_sha to the release commit only under
  requireQualified, where the SHA guard proves it, with its own failure
  message
- guard test-report hashing with existsSync so a missing citation fails
  structurally instead of crashing with ENOENT
- share one daemonIdEquals (constant-time, absent-never-matches) across
  the client, transport, and managed-policy identity fences
- prune route-cache entries stranded under prior daemon-id suffixes
- cap the demand-start compatibility probe at a hard budget and coalesce
  concurrent waiters onto one in-flight probe
Review findings from PR 62 that survive the rebase onto the updated base. The
base branch already landed the reviewed versions of the target test-report gate
and the catalog.list deadline bound, so this keeps only what it does not have.

- report malformed installed-evidence JSON as a verification failure rather than
  letting an unguarded JSON.parse abort with a bare SyntaxError. Every other
  malformed-input path in this verifier already reports structurally, including
  the proof-artifact parse a few hundred lines above.
- create the coordination FIFO test fixture with mkfifo -m 600 so the mode is
  applied at creation. Creating it under the umask and tightening it with a
  separate chmod left a window where the node was reachable at a wider mode,
  which is the invariant these tests exercise.
- give requestStream an item type parameter, matching call(), so callers are not
  handed unknown[] to cast at every use.
- drop the duplicate daemonIdEquals helper. The base branch fences daemon
  identity through sameDaemonId, so this branch's copy had no callers left after
  the rebase.
- keep a single execution_provider field in the synapse_perf lane fixture, which
  both branches had added independently.
@ahrav
ahrav force-pushed the stack/mc-host-15-ci-portability branch from 0efa248 to 02c3f17 Compare August 29, 2026 07:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02c3f17105

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread .github/workflows/ci.yml
Comment on lines +209 to +214
- uses: actions/checkout@v5
with:
repository: ahrav/commons
ref: d2208eda95d845193d84199dbfd0781a1f479291
path: commons
persist-credentials: false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Provide credentials for the private sibling checkout

This new matrix job checks out the repository described elsewhere in this workflow as the private ../commons dependency, but it does not pass a PAT or GitHub App token. The actions/checkout documentation for multiple private repositories states that the default ${{ github.token }} is scoped to the current repository, so both matrix legs fail at this step before running the lifecycle tests. Supply a secret token with access to ahrav/commons.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The premise is a stale comment rather than the repository's actual visibility — ahrav/commons is public, so the default GITHUB_TOKEN checks it out cross-repo without a secret:

$ gh api repos/ahrav/commons --jq '{full_name, private, visibility}'
{"name":"ahrav/commons","private":false,"visibility":"public"}

You quoted this workflow describing ../commons as private, which was fair — the comments did say that, and they were wrong. That is now the second review to reach the same incorrect conclusion from them, so I have rewritten all three in 3353fbd7 to describe the property that actually matters (whether a job checks the sibling sources out or stubs them) instead of repo visibility.

The checkout documentation you cite is correct that github.token is scoped to the current repository; that scoping applies to private cross-repo reads, which is not this case.

For evidence rather than argument: the mc-host-lifecycle job is running on the current head, past the checkout step.

Comment on lines +279 to +283
// A daemon_generation_changed rejection is client-side and pre-publication:
// no request reached the daemon, so it is transient transport failure, not
// `module_restarted` evidence, which spends a page's single durable restart
// budget on an attempt the daemon never saw.
else if (normalized.includes("daemon_generation_changed")) mapped = "transport";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rebind the affected page after a daemon fence rejection

When a managed daemon rotates during embedItems, callWithRetry now clears the stale identity and throws this newly classified transport error, but the page loop only invokes rebindAfterModuleRestart for module_restarted; its outer catch then swallows the transport failure and advances to the next page, returning no vectors for the affected page even though the rejection occurred before publication. The fresh evidence after the earlier stale-retry report is this new transport mapping, which bypasses the newly added rebind path entirely; handle daemon_generation_changed as an immediate within-deadline rebind without charging the durable restart budget.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed, and this is a real gap that this branch introduced. Good find — it is the second-order consequence of the reclassification, which the earlier round asked for and I made.

Verified at the page loop:

} catch (error) {
    const classified = classifyError(error);
    if (classified.code !== "module_restarted" || restarted) throw classified;
    restarted = true;
    await this.rebindAfterModuleRestart(deadlineAt, signal);
}

Once daemon_generation_changed classifies as transport rather than module_restarted, it no longer satisfies that guard, so the rotation is rethrown instead of triggering the rebind — exactly as you describe. The previous mapping charged the durable restart budget for a rejection the daemon never saw; the new one avoids that but skips the rebind, so the page loses its rebind path.

Your proposed shape is the right one: treat daemon_generation_changed as an immediate within-deadline rebind that does not set restarted, so it neither spends the durable budget nor forfeits the page. Holding off on landing it in this PR because it changes durable-ledger retry accounting and belongs with a test that pins "rebinds without consuming the restart budget" — tracking it as a follow-up rather than hand-patching the ledger path late in a CI-portability change.

expectedStateVersion: row.stateVersion,
jobId,
});
await this.rebindAfterModuleRestart(deadlineAt, signal);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove the second compatibility re-certification

For a managed detailed page that receives module_restarted while polling, this new call already performs a deadline-bounded compatibility rebind, but execution then reaches the pre-existing recertifyForRestart at line 1316 and performs a second demand using the full query timeout. This can extend the operation past the ledger page deadline, and a failure of the redundant second demand records a retryable page failure even though the first rebind succeeded and the restart budget has already been persisted as spent. Keep only the new deadline-aware rebind.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed — and this is one I had already flagged internally while resolving the rebase, so it is good to have it pinned to specific lines.

Both paths are present: rebindAfterModuleRestart(deadlineAt, signal) at the polling site, and the pre-existing recertifyForRestart(signal) further down. They now overlap because this branch converted three of the base's four recertifyForRestart call sites to the deadline-aware rebind and left the ledger-conflict site on the original.

They are not quite interchangeable, which is why I did not simply delete one during the rebase: rebindAfterModuleRestart throws a typed error, while recertifyForRestart returns a boolean and its call site rethrows the original classified error, preserving error provenance for the ledger row. Collapsing them changes which error a failed page records.

Your two consequences are the ones that matter though — a second full-timeout demand can push past the ledger page deadline, and a failure of the redundant demand records a retryable page failure after the restart budget is already persisted as spent. Consolidating onto the deadline-aware rebind, keeping the original error on the failure path, is the fix; grouping it with the rebind change from your other comment so both land with the ledger tests rather than separately.

@ahrav

ahrav commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

Review response and rebase onto the updated base

All 15 inline review comments now have individual replies. Summarising here because the single most important finding is not in any one thread: this branch had gone stale as a stack entry, and that explains most of the review findings.

The merge conflict, and why it mattered

The PR was CONFLICTING. That was not incidental — GitHub cannot build a merge ref for a conflicted PR, so pull_request workflows could not run on the head commit at all. The "failing checks" two reviewers asked me to root-cause were checks that never started.

The cause: PRs #60 (stack/mc-host-13-client-compatibility) and #61 (stack/mc-host-14-release-evidence) merged into the base branch, each picking up review fixes on the way in. This branch still carried its own pre-review copies of both. The duplicated commits were patch-identical to the base's originals, so a rebase dropped them cleanly; the conflicts that remained were the base's newer, reviewed versions meeting this branch's older ones.

Resolved by rebasing onto the updated base and, wherever the two sides had solved the same problem, keeping the base's reviewed version. The branch now reports MERGEABLE and CI is running.

The result is that this PR is finally the size it should be — +169/−60 across 7 files, scoped to CI portability and lifecycle verification, instead of re-litigating two merged stack entries.

What that means for the review findings

Several findings were real bugs in code this branch was carrying a stale copy of, and the base already fixed them — better than I first did:

  • kilo's CRITICAL (test_report_path: null bypassing report verification) was genuine. I verified the bypass empirically, then found the base's verifyTargetTestReport already closes it and goes further: it constrains the path to the report directory, rejects one report satisfying two targets, and validates the report's own schema/target/passed. Kept the base's.
  • codex's P2 on catalog.list not being deadline-bounded was correct; the base's reviewed fix is near-identical to mine. Kept the base's.
  • The base also supersedes this branch on the route-cache key and the attestation attempt binding, in both cases arguing explicitly for the opposite of what this branch did.

Two findings were fixed only on this branch, and survive the rebase:

  • daemon_generation_changed now classifies as transport rather than module_restarted, so a pre-publication fence rejection no longer spends a page's single durable restart budget. The base still maps this to module_restarted.
  • The mc-host-lifecycle job runs [ubuntu-latest, macos-latest], restoring the macOS coverage two reviewers flagged, and mc-module/ck-mc-host are built and tested again.

Fixed in this push (02c3f171)

  • Malformed installed-evidence JSON now fails as a structured verification error instead of a bare SyntaxError. The base still has the unguarded parse, so this one is new.
  • The coordination FIFO fixture uses mkfifo -m 600, applying the mode at creation instead of create-then-chmod. Verified -m bypasses the umask (umask 000 still yields 600).
  • requestStream takes an item type parameter, matching call().
  • Removed a daemonIdEquals helper left with no callers after the rebase, and a duplicated execution_provider field both branches had added.

Two things worth separate attention

1. Three lifecycle tests are flaky on the base branch, not just here. Measured on mc-host --lib: the base fails 2/6 runs, and the same three tests fail in varying combinations — an_unknown_lifecycle_schema_is_quarantined_not_interpreted, concurrent_probes_never_resurrect_stale_evidence_as_live, lifetime_and_runtime_lock_disagreement_is_wedged. Under load I measured this branch at 15/30 and the base at 13/30 for the lifecycle filter — statistically indistinguishable, so this is pre-existing and not introduced here. lifecycle_cli is intermittent too. This is worth its own fix; it will keep producing red CI that looks like real breakage.

2. verify-mc-host-release-evidence --check cannot pass in qualified mode. It requires source.workflow to equal .github/workflows/mc-host-release-qualification.yml, and that workflow does not exist here or on the base. It fails closed, which is the right direction, and nothing in CI invokes the qualified path yet — but the workflow has to land before the first GA attempt. Left as-is deliberately rather than widening the constant to an existing workflow, which would weaken the gate to make it satisfiable.

Two smaller notes: the base's sameDaemonId compares with an early-exiting .every(), where this branch's now-removed helper used timingSafeEqual — a weak threat for a local coordination identity, but the constant-time property was lost, and moving it into types.ts would breach that module's documented leaf status, so it is the author's call. And this branch leaves both rebindAfterModuleRestart and recertifyForRestart in place; they differ only in throw-vs-bool error provenance, so consolidating them is a deliberate decision rather than a mechanical cleanup.

Verification

cargo fmt --check, cargo clippy --workspace --all-targets (clean), bun run typecheck, bun run lint (0 errors), bun run test:release (147 pass), 478 plugin tests across the touched modules, and the mc-host/mc-module integration targets the CI job runs.

Three comments called ../commons a private repository. It is public, so the
default GITHUB_TOKEN checks it out cross-repo without a secret, and two
reviewers independently read those comments as evidence that the
mc-host-lifecycle checkout must be failing on permissions.

The property that actually matters is whether a job checks the sibling sources
out or stubs them, so say that instead. Also point at the mc-host-lifecycle job
as the place the ck-mc-host build and lifecycle_cli contract now run.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3353fbd77a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1027 to +1030
try {
installedEvidenceValue = JSON.parse(installedEvidenceBytes.toString("utf8"));
} catch {
fail(`installed release evidence at ${EVIDENCE_PATH} is malformed JSON`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard the first parse of installed evidence

When --check or --check-schema is run with a truncated or malformed tmp/mc-host-installed-release-evidence.json, main() parses that file through readJson at line 1169 before invoking this validator, so execution still aborts with the bare SyntaxError that this new catch is meant to replace. Guard the initial parse in main/readJson (or have the validator perform the only parse) so the actual CLI path reports the structured verification failure.

Useful? React with 👍 / 👎.

@ahrav

ahrav commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

The restored macOS leg immediately found a real macOS bug

Reporting this because it is the direct consequence of restoring the macOS coverage two reviewers asked for, and it now blocks this PR's own CI.

mc-host lifecycle integration (ubuntu-latest) passes. The macOS leg fails, and not on the checkout — both actions/checkout@v5 steps succeed, which settles the "private ../commons needs a PAT" finding empirically. It fails in broca_protocol:

host must publish: Instance(Io {
    op: "open_component",
    path: "/var",
    source: Os { code: 20, kind: NotADirectory, message: "Not a directory" }
})

Three tests, all at tests/support/mod.rs:913: credential_snapshot_must_match_before_backend_spawn, five_operation_round_trip_matches_the_consumed_wire_shapes, malformed_requests_over_the_host_create_no_run_state.

Cause

secure_runtime_dir walks the data-root path one component at a time with hardened flags:

pub(crate) const HARDENED_DIR_FLAGS: OFlags = OFlags::DIRECTORY
    .union(OFlags::NOFOLLOW)
    .union(OFlags::RDONLY)
    .union(OFlags::CLOEXEC);

O_NOFOLLOW | O_DIRECTORY on a symlink yields ENOTDIR (code 20). On macOS /var is a symlink to private/var, and TMPDIR is under /var/folders/…, so every tempfile-rooted host test fails at the first component. The same applies to /tmp, which is a symlink to private/tmp.

Refusing symlinked intermediates is deliberate and correct — the comment in secure_runtime_dir explains it as the §4.1 threat model, since a principal who can swap an intermediate can redirect the pathname after we pin ours. The problem is that macOS ships an OS-owned symlink in the middle of the only writable temp path.

This is pre-existing in instance.rs, not introduced here; this PR only made it visible again by restoring the macOS leg. It is also not test-only: a production macOS data root under /tmp or /var/folders would fail the same way.

Fix direction

Canonicalise the data root once, up front, then run the hardened no-follow walk over the canonical path. That keeps the anti-swap property for the walk — which is what the hardening is actually for — while tolerating an OS-provided symlink above the trust boundary. Fixing it in the test harness alone would hide the production case.

Not landing that here: it changes security-sensitive path handling and deserves its own PR and its own test, rather than being appended to a CI-portability change. Options for this PR are to land that fix first, or to gate these three tests on the canonicalisation landing.

Also still red, and not yet attributed

For reference, the two jobs that were failing before the rebase — Check (plugin) and E2E (Pi, host behavior) — now pass, as does Shared memory source build (macos-latest).

@ahrav
ahrav merged commit 754f882 into feat/mc-host-daemon-lifecycle Aug 29, 2026
17 of 21 checks passed
@ahrav
ahrav deleted the stack/mc-host-15-ci-portability branch August 29, 2026 15:30
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