From b6eb5209a4a90fb200d22874f4bf0e02de051176 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 1 Sep 2026 13:20:31 +0500 Subject: [PATCH] feat: a release rendered from the monorepo Rendered from NDDev-it-com/setup-systems by tools/render_public_trees.py at 0.0.52. This tree is generated: fix the source and the renderer, never this checkout. --- CHANGELOG.md | 34 ++++++++ Cargo.lock | 8 +- Cargo.toml | 8 +- README.md | 2 +- crates/harness-runtime/src/software.rs | 20 +++-- crates/harness-runtime/src/wire.rs | 68 ++++++++++++++++ crates/setup-core/src/software.rs | 104 ++++++++++++++++++++++++- install.ps1 | 2 +- install.sh | 2 +- 9 files changed, 226 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30181a5..67097b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,40 @@ sibling project the same week this note was added. ## [Unreleased] +## [0.0.52] - 2026-09-01 + +An operation that reads no bundle refuses one by name. Only +`install` and `replace` read a bundle, and the plan bound the five bundle +names into its artifact for every operation — so a `remove` plan carrying a +fully-named bundle answered `planned, valid: true` with the digests echoed, +and the apply removed everything with the bundle bytes untouched. Accept and +ignore, measured on the released 0.0.50; the only loud refusal was the argv +parser's, on a partial flag set. + +A plan that echoes inputs its apply will never read lies about what approving +it means. The consumer's `end_state` design for `remove` (their ADR-0129) +assumed the loud refusal existed for providers that do not yet declare the +field; their declaration gate was in fact the only net. This release is the +second: `unsupported_operation`, naming the two operations that do take a +bundle. When `remove` learns to read one — the agreed `end_state` extension, +kit 0.2.8+ — the refusal narrows to the operations that still read none. + +No released consumer sends a bundle on `remove`; their remove plans are built +with no bundle bound, so nothing anyone runs changes behaviour under this +release except the request that was already a contradiction. + +And the plan's software entry point names the member **this platform** +actually gets. It was derived from the table's first row — a Unix member — +so on Windows a cursor plan promised `bin/agent` while the apply, resolving +this host's artifact, wrote `bin/agent.cmd`; `remove` looked for the wrong +name too and left the launcher behind. Found by the consumer's six-leg +matrix on `0.0.50`, Windows only, both architectures, cursor only — the one +harness whose Windows member is a batch launcher while its Unix members are +extensionless. Every reader of the exposed name — plan, apply's answer, +launch, remove, rollback — now derives it from this platform's artifact, +with the first-row hint kept only for a platform the vendor never published +for. + ## [0.0.51] - 2026-09-01 `remove` names both of its cases. It said *"anything you put under diff --git a/Cargo.lock b/Cargo.lock index b3096c7..578cb69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,7 +66,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "harness-runtime" -version = "0.0.51" +version = "0.0.52" dependencies = [ "provider-v3", "serde", @@ -128,7 +128,7 @@ dependencies = [ [[package]] name = "opencode-setup-system" -version = "0.0.51" +version = "0.0.52" dependencies = [ "harness-runtime", "provider-v3", @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "provider-v3" -version = "0.0.51" +version = "0.0.52" dependencies = [ "serde", "serde_json", @@ -209,7 +209,7 @@ dependencies = [ [[package]] name = "setup-core" -version = "0.0.51" +version = "0.0.52" dependencies = [ "miniz_oxide", "serde", diff --git a/Cargo.toml b/Cargo.toml index be6f06b..df7c020 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.0.51" +version = "0.0.52" edition = "2024" rust-version = "1.89" license = "AGPL-3.0-or-later" @@ -23,9 +23,9 @@ sha2 = "0.11" # `setup-core::archive`); an inflate loop is not, because its bugs are # memory-safety bugs and it is not improved by being hand-written here. miniz_oxide = "0.9" -setup-core = { path = "crates/setup-core", version = "0.0.51" } -provider-v3 = { path = "crates/provider-v3", version = "0.0.51" } -harness-runtime = { path = "crates/harness-runtime", version = "0.0.51" } +setup-core = { path = "crates/setup-core", version = "0.0.52" } +provider-v3 = { path = "crates/provider-v3", version = "0.0.52" } +harness-runtime = { path = "crates/harness-runtime", version = "0.0.52" } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/README.md b/README.md index e3c7c29..07dbb80 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ release is a convenience, not the authorised copy. ```bash docker run --rm -v "$HOME/.config:/config" \ - ghcr.io/nddev-opennetwork/opencode-setup-system:0.0.51 \ + ghcr.io/nddev-opennetwork/opencode-setup-system:0.0.52 \ status --target /config/ --json ``` diff --git a/crates/harness-runtime/src/software.rs b/crates/harness-runtime/src/software.rs index 44f0050..0e9f5a9 100644 --- a/crates/harness-runtime/src/software.rs +++ b/crates/harness-runtime/src/software.rs @@ -149,10 +149,17 @@ pub(crate) fn plan( // Not always the command: pi's entry point is JavaScript, and Windows runs // a file by its extension rather than by a shebang, so what is exposed // there is `pi.cmd`. The plan states what a caller will actually be able to - // run, which is the whole point of naming it. + // run, which is the whole point of naming it -- and it is derived from + // **this platform's** member, not the table's first row. The hint + // derivation promised `bin/agent` on Windows while the apply wrote + // `bin/agent.cmd`, because cursor's Windows member is a `.cmd` and its + // Unix members are extensionless; the consumer's matrix caught the plan + // lying on exactly the one platform where the two rows classify apart. + let (os, arch) = platform_of_this_host(); + let member = declared.member_on(os, arch); let entry_point = format!( "bin/{}", - setup_core::software::exposed_name(declared.command, declared.member_hint()) + setup_core::software::exposed_name(declared.command, member) ); let exposed = root.join(&entry_point); @@ -160,7 +167,7 @@ pub(crate) fn plan( // is already under the prefix belongs in the plan. Without it an install // and an update produced byte-identical effects -- two names for one act, // and neither said what was about to be replaced. - let present = software::Present::under_named(&root, declared.command, declared.member_hint()); + let present = software::Present::under_named(&root, declared.command, member); if operation == Operation::SoftwareRemove { let mut effects = vec![ @@ -205,7 +212,6 @@ pub(crate) fn plan( )); } - let (os, arch) = platform_of_this_host(); let artifact = declared.artifact_for(os, arch)?; let mut effects = Vec::new(); if present.holds(declared.version) { @@ -336,7 +342,7 @@ pub(crate) fn apply( // the prefix could have been emptied in between. The plan's digest binds // what was decided, not what the disk still holds. if operation == Operation::SoftwareUpdate - && software::Present::under_named(&root, declared.command, declared.member_hint()) + && software::Present::under_named(&root, declared.command, declared.member_here()) .versions .is_empty() { @@ -368,7 +374,7 @@ pub(crate) fn apply( "version": installed.version, "entry_point": format!( "bin/{}", - setup_core::software::exposed_name(declared.command, declared.member_hint()) + setup_core::software::exposed_name(declared.command, artifact.member) ), "executable": installed.executable.to_string_lossy(), "files": installed.files, @@ -420,7 +426,7 @@ pub(crate) fn launch( // one platform where they differ. let executable = root.join("bin").join(setup_core::software::exposed_name( declared.command, - declared.member_hint(), + declared.member_here(), )); let found = executable.metadata().map_err(|error| { diff --git a/crates/harness-runtime/src/wire.rs b/crates/harness-runtime/src/wire.rs index fb654dd..68ed319 100644 --- a/crates/harness-runtime/src/wire.rs +++ b/crates/harness-runtime/src/wire.rs @@ -525,6 +525,30 @@ fn honourable(harness: &Harness, request: &PlanRequest) -> Result<()> { } } + // The same rule for a bundle, and this one was worse than silently + // dropped: only `install` and `replace` read one, but the plan **bound** + // the five names into its artifact for every operation, so a remove plan + // echoed a `bundle_digest` its apply would never read. Measured on the + // released 0.0.50 while the consumer designed remove's `end_state` + // extension (their ADR-0129): plan `planned, valid: true`, apply removed + // everything, dummy bundle untouched -- accept and ignore, twice, exit 0. + // Their rollout story assumed the loud refusal existed; now it does. When + // remove learns to read a bundle (kit 0.2.8+, `end_state`), this narrows + // to the operations that still read none. + if request.bundle.is_some() + && !matches!(request.operation, Operation::Install | Operation::Replace) + { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + format!( + "{} reads no bundle, so one named for it would be echoed into \ + the plan and never read; install and replace are the \ + operations that take one", + request.operation + ), + )); + } + // A profile this build never advertised cannot be honoured, and recording // it in a plan would be worse than refusing: the apply would run under the // only posture this build has while the artifact claimed another. @@ -4610,6 +4634,50 @@ mod tests { ); } + #[test] + fn a_bundle_on_an_operation_that_reads_none_is_refused_not_echoed() { + // Measured on the released 0.0.50 before this refusal existed, while + // the consumer designed remove's `end_state` extension (their + // ADR-0129): a remove plan carrying all five bundle names answered + // `planned, valid: true` with the digests echoed, and the apply then + // removed everything with the bundle bytes untouched -- accept and + // ignore, exit 0 twice, even for a 20-byte dummy ZIP. A plan that + // echoes inputs apply will never read lies about what approving it + // means, and the consumer's rollout story for `end_state` assumed a + // loud refusal that did not exist. This is that refusal. When remove + // learns to read a bundle (kit 0.2.8+), it narrows rather than lifts: + // the operations that read none keep refusing. + let target = seeded("bundle-on-remove"); + let (bytes, bundle_digest, artifact) = bundle_bytes(&[("AGENTS.md", "x\n", 0o644)]); + let artifact_path = target.join("..").join("bundle-on-remove.zip"); + fs::write(&artifact_path, &bytes).unwrap(); + + let mut plan_args = vec![ + "--operation".to_owned(), + "remove".to_owned(), + "--provider-release-digest".to_owned(), + RELEASE.to_owned(), + "--operation-id".to_owned(), + "operation_01NOBUNDLE".to_owned(), + "--expires-at".to_owned(), + far_future().to_owned(), + ]; + plan_args.extend(bundle_flags( + &artifact_path, + &bundle_digest, + &artifact, + bytes.len(), + )); + let borrowed: Vec<&str> = plan_args.iter().map(String::as_str).collect(); + let error = refuse(args("plan-operation", &target, &borrowed)); + assert_eq!(error.reason(), Some(WireReason::UnsupportedOperation)); + assert!( + error.detail().contains("reads no bundle"), + "{}", + error.detail() + ); + } + #[test] fn a_bundle_installs_over_the_wire_and_leaves_unowned_files_alone() { let target = seeded("bundle-install"); diff --git a/crates/setup-core/src/software.rs b/crates/setup-core/src/software.rs index 95b5536..5bc2831 100644 --- a/crates/setup-core/src/software.rs +++ b/crates/setup-core/src/software.rs @@ -366,6 +366,33 @@ impl Software { } } + /// The member of **this platform's** artifact, or the hint when the table + /// has no row for it. + /// + /// The hint is the first row of the table, and deriving an exposed name + /// from it is correct exactly until one platform's member classifies + /// differently from the first row's. Cursor is that shape: Unix members + /// carry no extension, the Windows member is a `.cmd` — so a plan built + /// from the hint promised `bin/agent` while the apply, which resolves this + /// host's artifact, wrote `bin/agent.cmd`. The consumer's six-leg matrix + /// found it on released `0.0.50`, Windows only, both architectures, with + /// *"the planned entry point is not on disk"*. Every reader of an exposed + /// name goes through here now; the hint remains only as the fallback for + /// a platform the vendor never published for, where nothing was ever + /// installed under any name. + #[must_use] + pub fn member_on(&self, os: &str, arch: &str) -> &'static str { + self.artifact_for(os, arch) + .map_or_else(|_| self.member_hint(), |artifact| artifact.member) + } + + /// [`Self::member_on`] for the machine this process runs on. + #[must_use] + pub fn member_here(&self) -> &'static str { + let (os, arch) = crate::platform_of_this_host(); + self.member_on(os, arch) + } + /// The artifact for one platform, or the reason there is not one. /// /// # Errors @@ -744,7 +771,7 @@ pub fn remove(software: &Software, root: &Path) -> Result { // update, roll back, take the bad one off -- deleted the command that was // running the good one and left a complete version tree nothing could start. let exposed_version = - Present::under_named(root, software.command, software.member_hint()).exposed; + Present::under_named(root, software.command, software.member_here()).exposed; fs::remove_dir_all(&version_root).map_err(|error| { Error::new( @@ -768,7 +795,7 @@ pub fn remove(software: &Software, root: &Path) -> Result { let exposed = root .join("bin") - .join(exposed_name(software.command, software.member_hint())); + .join(exposed_name(software.command, software.member_here())); if exposed.symlink_metadata().is_ok() { fs::remove_file(&exposed).map_err(|error| { Error::new( @@ -851,7 +878,7 @@ fn executable_candidates( /// Refuses a version that is not installed, naming the ones that are, and a /// version tree that holds no executable this build can find. pub fn rollback(software: &Software, root: &Path, to: &str) -> Result { - let present = Present::under_named(root, software.command, software.member_hint()); + let present = Present::under_named(root, software.command, software.member_here()); if !present.versions.iter().any(|found| found == to) { return Err(Error::new( ReasonCode::InvalidTarget, @@ -891,7 +918,7 @@ pub fn rollback(software: &Software, root: &Path, to: &str) -> Result let exposed = root .join("bin") - .join(exposed_name(software.command, software.member_hint())); + .join(exposed_name(software.command, software.member_here())); expose(executable, &exposed, to, software.command)?; Ok(Installed { version: to.to_owned(), @@ -1255,6 +1282,75 @@ mod tests { } } + /// A cursor-shaped table: Unix members extensionless, the Windows member a + /// batch launcher. The one shape in the estate where the first row's member + /// and the Windows row's member classify differently. + const MIXED_ARTIFACTS: &[Artifact] = &[ + Artifact { + platform: "linux/arm64", + url: "https://example.invalid/linux-arm64.tgz", + bytes: 0, + sha256: "sha256:0", + shape: Shape::GzipTar, + member: "dist-package/cursor-agent", + }, + Artifact { + platform: "windows/x86_64", + url: "https://example.invalid/windows-x86_64.zip", + bytes: 0, + sha256: "sha256:0", + shape: Shape::Zip, + member: "dist-package/cursor-agent.cmd", + }, + ]; + + /// The name a plan states on Windows is the name Windows actually gets. + /// + /// Found by the consumer's six-leg matrix on released `0.0.50`, cursor, + /// both Windows architectures: `software_install` answered success and + /// their postcondition read *"the planned entry point is not on disk"*. + /// The plan derived its entry point from `member_hint()` — the **first** + /// artifact in the table, a Unix member with no extension, so + /// `exposed_name_on(.., windows)` classified it `Native` and the plan + /// promised `bin/agent`; the apply resolved this host's artifact, whose + /// member is a `.cmd`, and wrote `bin/agent.cmd`. Four Unix legs agreed + /// with themselves; the one platform where the two derivations differ is + /// the one the plan lied on. Our own evidence stayed green because its + /// readback derives the name the same way the apply does — self-consistent + /// is not correct. + #[test] + fn the_windows_entry_point_names_the_member_windows_actually_gets() { + let sw = Software { + command: "agent", + delivery: Delivery::Artifacts(MIXED_ARTIFACTS), + ..software() + }; + // The derivation the plan uses for this platform. + let planned = exposed_name_on(sw.command, sw.member_on("windows", "x86_64"), true); + // The derivation the apply uses: this platform's artifact, directly. + let written = exposed_name_on( + sw.command, + sw.artifact_for("windows", "x86_64").unwrap().member, + true, + ); + assert_eq!(planned, written, "the plan and the apply name one file"); + assert_eq!(planned, "agent.cmd"); + // The control: the first-row derivation this replaced answers wrongly + // on exactly this shape, which is what made the defect Windows-only + // and cursor-only. + assert_eq!(exposed_name_on(sw.command, sw.member_hint(), true), "agent"); + } + + /// A platform the table does not carry falls back to the hint. + /// + /// `software_remove` plans on hosts the vendor never published for, and a + /// name that errors there would refuse a removal of nothing. + #[test] + fn a_platform_without_an_artifact_falls_back_to_the_hint() { + let sw = software(); + assert_eq!(sw.member_on("windows", "x86_64"), sw.member_hint()); + } + /// A build that has never been bumped names exactly one version. /// /// The absence is the point: `software_update` has nothing to move from diff --git a/install.ps1 b/install.ps1 index bdbbe09..e4b379f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -7,7 +7,7 @@ # powershell -ExecutionPolicy Bypass -File install.ps1 -Version 0.1.0 [CmdletBinding()] param( - [string]$Version = "0.0.51", + [string]$Version = "0.0.52", [string]$InstallDir = "$env:LOCALAPPDATA\Programs\opencode-setup-system" ) $ErrorActionPreference = "Stop" diff --git a/install.sh b/install.sh index 0df6243..491fee4 100644 --- a/install.sh +++ b/install.sh @@ -14,7 +14,7 @@ set -eu REPO="NDDev-OpenNetwork/opencode-setup-system" BINARY="opencode-setup-system" -VERSION="${1:-0.0.51}" +VERSION="${1:-0.0.52}" PREFIX="${OPENCODE_INSTALL_DIR:-$HOME/.local/bin}" case "$(uname -s)" in