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