From ad6f2fa81bd946763aa892eba2c0318d2fd6f563 Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Fri, 25 Sep 2026 19:15:50 +0500 Subject: [PATCH] feat: a release of this repository Published at 0.0.76. Propose changes through this repository's issues and pull requests. --- CHANGELOG.md | 10 ++ Cargo.lock | 8 +- Cargo.toml | 8 +- README.md | 2 +- .../harness-runtime/src/instruction_region.rs | 35 ++++- crates/harness-runtime/src/wire.rs | 121 +++++++++++++++--- install.ps1 | 2 +- install.sh | 2 +- 8 files changed, 154 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e05eda..a995fe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,16 @@ cut and that this clone does not carry. ## [Unreleased] +## [0.0.76] - 2026-09-25 + +The shared instruction attachment now refuses unreadable or invalid +UTF-8 instruction files and partial, reversed or duplicate ownership markers. +Setup replacement and withdrawal also refuse ambiguous instruction bytes +instead of dropping user content; an invalid UTF-8 setup instruction payload +is not silently converted. The seven providers keep their 0.0.75 software +pins and setup content. Control-attachment lifecycle and fresh-session native +qualification remain separate open work. + ## [0.0.75] - 2026-09-25 All seven setup systems refresh their software pins from current vendor diff --git a/Cargo.lock b/Cargo.lock index 3991fa5..3e68316 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ [[package]] name = "cursor-setup-system" -version = "0.0.75" +version = "0.0.76" dependencies = [ "harness-runtime", "provider-v3", @@ -76,7 +76,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "harness-runtime" -version = "0.0.75" +version = "0.0.76" dependencies = [ "provider-v3", "serde", @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "provider-v3" -version = "0.0.75" +version = "0.0.76" dependencies = [ "serde", "serde_json", @@ -209,7 +209,7 @@ dependencies = [ [[package]] name = "setup-core" -version = "0.0.75" +version = "0.0.76" dependencies = [ "miniz_oxide", "serde", diff --git a/Cargo.toml b/Cargo.toml index 1c8627f..3fc570c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.0.75" +version = "0.0.76" 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.75" } -provider-v3 = { path = "crates/provider-v3", version = "0.0.75" } -harness-runtime = { path = "crates/harness-runtime", version = "0.0.75" } +setup-core = { path = "crates/setup-core", version = "0.0.76" } +provider-v3 = { path = "crates/provider-v3", version = "0.0.76" } +harness-runtime = { path = "crates/harness-runtime", version = "0.0.76" } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/README.md b/README.md index 69b7d3f..00769fc 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/cursor-setup-system:0.0.75 \ + ghcr.io/nddev-opennetwork/cursor-setup-system:0.0.76 \ status --target /config/ --json ``` diff --git a/crates/harness-runtime/src/instruction_region.rs b/crates/harness-runtime/src/instruction_region.rs index dde1517..95d2f6f 100644 --- a/crates/harness-runtime/src/instruction_region.rs +++ b/crates/harness-runtime/src/instruction_region.rs @@ -6,13 +6,27 @@ //! region as payload they are free to empty: [`preserve_in_replacement`] and //! [`keep_region_on_withdraw`] are the two hooks. -use std::path::Path; +use std::{io, path::Path}; /// Visible begin marker. HTML comments are refused — Claude strips them. pub const BEGIN: &str = ":::begin-ai-stp"; /// Visible end marker. Inclusive of the following newline when present. pub const END: &str = ":::end-ai-stp"; +/// Refuse ambiguous ownership markers before a caller plans or writes bytes. +#[must_use] +pub fn markers_well_formed(existing: &str) -> bool { + match (existing.find(BEGIN), existing.find(END)) { + (None, None) => true, + (Some(begin), Some(end)) => { + begin < end + && !existing[begin + BEGIN.len()..].contains(BEGIN) + && !existing[end + END.len()..].contains(END) + } + _ => false, + } +} + /// The marked region, including both markers, or `None` when either is missing /// or they are out of order. #[must_use] @@ -136,9 +150,12 @@ pub fn is_attachment(relative: &str, named: Option<&str>) -> bool { named.is_some_and(|path| path == relative) } -/// UTF-8 text of a file, or empty when it is missing. -pub fn read_utf8(path: &Path) -> String { - std::fs::read_to_string(path).unwrap_or_default() +/// UTF-8 text of a file, or empty only when it is missing. +pub fn read_utf8(path: &Path) -> io::Result { + match std::fs::read_to_string(path) { + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(String::new()), + result => result, + } } #[cfg(test)] @@ -147,6 +164,16 @@ mod tests { const SECTION: &str = ":::begin-ai-stp\nhello\n:::end-ai-stp\n"; + #[test] + fn partial_reversed_or_duplicate_markers_are_ambiguous() { + assert!(markers_well_formed("no attachment")); + assert!(markers_well_formed(SECTION)); + assert!(!markers_well_formed(":::begin-ai-stp\n")); + assert!(!markers_well_formed(":::end-ai-stp\n")); + assert!(!markers_well_formed(":::end-ai-stp\n:::begin-ai-stp\n")); + assert!(!markers_well_formed(&format!("{SECTION}{SECTION}"))); + } + #[test] fn empty_file_receives_the_section() { let (updated, wrote) = patch("", SECTION); diff --git a/crates/harness-runtime/src/wire.rs b/crates/harness-runtime/src/wire.rs index 7942c82..6b7d979 100644 --- a/crates/harness-runtime/src/wire.rs +++ b/crates/harness-runtime/src/wire.rs @@ -782,13 +782,15 @@ fn plan_instruction_patch( "patch_instruction_region needs --instruction-section with marked bytes", )); }; - if crate::instruction_region::extract(section).is_none() { + if !crate::instruction_region::markers_well_formed(section) + || crate::instruction_region::extract(section).is_none() + { return Err(Error::refuse( WireReason::UnsupportedOperation, - "instruction_section must contain :::begin-ai-stp and :::end-ai-stp", + "instruction_section needs exactly one ordered :::begin-ai-stp and :::end-ai-stp pair", )); } - let existing = crate::instruction_region::read_utf8(&target.root().join(relative)); + let existing = read_instruction_text(&target.root().join(relative))?; let (updated, wrote) = crate::instruction_region::patch(&existing, section); let effects = if wrote { vec![format!("patch instruction region at {relative}")] @@ -2879,16 +2881,26 @@ fn write_host_file( }; let outgoing = if crate::instruction_region::is_attachment(relative, harness.instruction_region) { - fs::read_to_string(&destination) - .ok() - .map(|existing| { - crate::instruction_region::preserve_in_replacement( - &existing, - &String::from_utf8_lossy(&outgoing), - ) - .into_bytes() - }) - .unwrap_or(outgoing) + let existing = read_instruction_text(&destination)?; + let incoming = std::str::from_utf8(&outgoing).map_err(|error| { + Error::refuse( + WireReason::ProviderUnavailable, + format!( + "instruction surface {} is not UTF-8: {error}", + destination.display() + ), + ) + })?; + if !crate::instruction_region::markers_well_formed(incoming) { + return Err(Error::refuse( + WireReason::ProviderUnavailable, + format!( + "instruction surface {} has ambiguous markers", + destination.display() + ), + )); + } + crate::instruction_region::preserve_in_replacement(&existing, incoming).into_bytes() } else { outgoing }; @@ -2932,16 +2944,38 @@ fn withdraw_written( if preserve_json_keys { forget_written_fields(harness, target, relative); } - if crate::instruction_region::is_attachment(relative, harness.instruction_region) - && let Ok(existing) = fs::read_to_string(&destination) - && let Some(region) = crate::instruction_region::keep_region_on_withdraw(&existing) - { - lock::atomic_write(&destination, region.as_bytes()).map_err(Error::from)?; - return Ok(()); + if crate::instruction_region::is_attachment(relative, harness.instruction_region) { + let existing = read_instruction_text(&destination)?; + if let Some(region) = crate::instruction_region::keep_region_on_withdraw(&existing) { + lock::atomic_write(&destination, region.as_bytes()).map_err(Error::from)?; + return Ok(()); + } } remove_keeping(&destination, target.root(), harness.never_touch) } +fn read_instruction_text(path: &Path) -> Result { + let existing = crate::instruction_region::read_utf8(path).map_err(|error| { + Error::refuse( + WireReason::ProviderUnavailable, + format!( + "cannot read instruction surface {}: {error}", + path.display() + ), + ) + })?; + if !crate::instruction_region::markers_well_formed(&existing) { + return Err(Error::refuse( + WireReason::ProviderUnavailable, + format!( + "instruction surface {} has ambiguous markers", + path.display() + ), + )); + } + Ok(existing) +} + fn strip_json_keys(path: &Path, keys: &[String]) -> Result { let Ok(bytes) = fs::read(path) else { return Ok(false); @@ -8385,6 +8419,55 @@ mod tests { ); } + #[test] + fn instruction_patch_refuses_invalid_encoding_and_ambiguous_markers() { + let target = seeded("invalid-instruction-region"); + let path = target.join("AGENTS.md"); + for bytes in [ + vec![0xff, 0xfe, b'X'], + b"keep\n:::begin-ai-stp\n".to_vec(), + b":::end-ai-stp\n:::begin-ai-stp\n".to_vec(), + format!("{INSTRUCTION_SECTION}{INSTRUCTION_SECTION}").into_bytes(), + ] { + fs::write(&path, &bytes).unwrap(); + let error = refuse(args( + "plan-operation", + &target, + &[ + "--operation", + "patch_instruction_region", + "--provider-release-digest", + RELEASE, + "--operation-id", + "operation_01TEST", + "--expires-at", + far_future(), + "--instruction-section", + INSTRUCTION_SECTION, + ], + )); + assert_eq!(error.reason(), Some(WireReason::ProviderUnavailable)); + assert_eq!(fs::read(&path).unwrap(), bytes); + } + } + + #[test] + fn setup_writes_and_withdrawal_refuse_ambiguous_instruction_bytes() { + let target = seeded("ambiguous-setup-instruction"); + let path = target.join("AGENTS.md"); + let bytes = b"user content\n:::begin-ai-stp\n"; + fs::write(&path, bytes).unwrap(); + let resolved = Target::resolve(&target, TEST.control_directory).unwrap(); + let write_error = + write_host_file(&TEST, &resolved, "AGENTS.md", b"new setup\n", false).unwrap_err(); + assert_eq!(write_error.reason(), Some(WireReason::ProviderUnavailable)); + assert_eq!(fs::read(&path).unwrap(), bytes); + + let remove_error = withdraw_written(&TEST, &resolved, "AGENTS.md", false).unwrap_err(); + assert_eq!(remove_error.reason(), Some(WireReason::ProviderUnavailable)); + assert_eq!(fs::read(&path).unwrap(), bytes); + } + #[test] fn a_harness_without_an_instruction_surface_refuses_the_patch() { let mut mute = TEST; diff --git a/install.ps1 b/install.ps1 index 2028c89..47cf2a3 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.75", + [string]$Version = "0.0.76", [string]$InstallDir = "$env:LOCALAPPDATA\Programs\cursor-setup-system" ) $ErrorActionPreference = "Stop" diff --git a/install.sh b/install.sh index 6a188cc..dd818d2 100644 --- a/install.sh +++ b/install.sh @@ -14,7 +14,7 @@ set -eu REPO="NDDev-OpenNetwork/cursor-setup-system" BINARY="cursor-setup-system" -VERSION="${1:-0.0.75}" +VERSION="${1:-0.0.76}" PREFIX="${CURSOR_INSTALL_DIR:-$HOME/.local/bin}" case "$(uname -s)" in