Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<dir> --json
```

Expand Down
35 changes: 31 additions & 4 deletions crates/harness-runtime/src/instruction_region.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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<String> {
match std::fs::read_to_string(path) {
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(String::new()),
result => result,
}
}

#[cfg(test)]
Expand All @@ -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);
Expand Down
121 changes: 102 additions & 19 deletions crates/harness-runtime/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")]
Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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<String> {
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<bool> {
let Ok(bytes) = fs::read(path) else {
return Ok(false);
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading