diff --git a/CHANGELOG.md b/CHANGELOG.md index 050a26c..2916e48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,20 @@ cut and that this clone does not carry. ## [Unreleased] +## [0.0.73] - 2026-09-19 + +nddev-builder guidance is refreshed against each harness's current native +extension model. Software artifacts are refreshed from verified vendor bytes: +Claude Code 2.1.278, Codex 0.155.1, Grok Build 1.0.38, Cursor +2026.09.18-9a7762b and Antigravity CLI 1.2.7. OpenCode remains 1.18.31 and +Pi 0.85.1. Public reusable workflows pin ci-workflows 0.1.20 +(26749820fad5bde1f1726636af5b04d329ed2fe5). Previous artifact pins remain +available for rollback. + +Providers gain the optional patch_instruction_region operation, which replaces +the managed instruction region in place while preserving unmanaged native +state. Setup content, postures and ownership are unchanged. + ## [0.0.72] - 2026-09-16 nddev-builder guidance is refreshed against each harness's current native diff --git a/Cargo.lock b/Cargo.lock index 8cc2896..35d837c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "codex-setup-system" -version = "0.0.72" +version = "0.0.73" dependencies = [ "harness-runtime", "provider-v3", @@ -76,7 +76,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "harness-runtime" -version = "0.0.72" +version = "0.0.73" dependencies = [ "provider-v3", "serde", @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "provider-v3" -version = "0.0.72" +version = "0.0.73" dependencies = [ "serde", "serde_json", @@ -209,7 +209,7 @@ dependencies = [ [[package]] name = "setup-core" -version = "0.0.72" +version = "0.0.73" dependencies = [ "miniz_oxide", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5ddc5ef..9fa5891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.0.72" +version = "0.0.73" 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.72" } -provider-v3 = { path = "crates/provider-v3", version = "0.0.72" } -harness-runtime = { path = "crates/harness-runtime", version = "0.0.72" } +setup-core = { path = "crates/setup-core", version = "0.0.73" } +provider-v3 = { path = "crates/provider-v3", version = "0.0.73" } +harness-runtime = { path = "crates/harness-runtime", version = "0.0.73" } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/README.md b/README.md index 1ea9b86..e49df13 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/codex-setup-system:0.0.72 \ + ghcr.io/nddev-opennetwork/codex-setup-system:0.0.73 \ status --target /config/ --json ``` diff --git a/crates/codex-setup-system/src/main.rs b/crates/codex-setup-system/src/main.rs index 9566023..cc4c408 100644 --- a/crates/codex-setup-system/src/main.rs +++ b/crates/codex-setup-system/src/main.rs @@ -305,6 +305,7 @@ pub const CODEX: Harness = Harness { // one on a disk it was never shipped to. embedded_setups: include!(concat!(env!("OUT_DIR"), "/embedded_setups.rs")), software: Some(software::SOFTWARE), + instruction_region: Some("AGENTS.md"), }; fn main() -> ExitCode { diff --git a/crates/codex-setup-system/src/software.rs b/crates/codex-setup-system/src/software.rs index 7618617..49a76d9 100644 --- a/crates/codex-setup-system/src/software.rs +++ b/crates/codex-setup-system/src/software.rs @@ -20,103 +20,103 @@ use harness_runtime::{Artifact, Delivery, Previous, Shape, Software}; pub(crate) const ARTIFACTS: &[Artifact] = &[ Artifact { platform: "linux/arm64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-linux-arm64.tgz", - bytes: 122_610_794, - sha256: "sha256:a2315b5f64bfeaff79b71e0d35505ba8c22cc1e96cab9dc950b614c804105b24", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-linux-arm64.tgz", + bytes: 135_126_766, + sha256: "sha256:7da3e7bea6db4751d1b837f046c9cec08918ac7f05427dd0b5bcf64ea6c85964", shape: Shape::GzipTar, member: "package/vendor/aarch64-unknown-linux-musl/bin/codex", }, Artifact { platform: "linux/x86_64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-linux-x64.tgz", - bytes: 129_654_638, - sha256: "sha256:e27c83a49e6031685ee7f956c12aad5f16484d3a80181dd3fea930fb96b3832b", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-linux-x64.tgz", + bytes: 142_140_011, + sha256: "sha256:f110cccdd50b0be8130b84f45b3144ea775c233f1c8bd8226da6ee719d63d206", shape: Shape::GzipTar, member: "package/vendor/x86_64-unknown-linux-musl/bin/codex", }, Artifact { platform: "macos/arm64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-darwin-arm64.tgz", - bytes: 116_501_639, - sha256: "sha256:2a98662d79316a59993c7233e3e25a1aa1d42da4b45904585d33a5a7da1cade1", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-darwin-arm64.tgz", + bytes: 127_465_533, + sha256: "sha256:93cc218b25b71c8da3edb50a013fbd22acf8f39058fb64083fefff638a084976", shape: Shape::GzipTar, member: "package/vendor/aarch64-apple-darwin/bin/codex", }, Artifact { platform: "macos/x86_64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-darwin-x64.tgz", - bytes: 124_308_807, - sha256: "sha256:92c493533c53c433c4d94252251daba4f379ccba06a9964d260abb47a535dce1", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-darwin-x64.tgz", + bytes: 135_811_357, + sha256: "sha256:819db6dbb57a56cc21383a30331fc4115696e02f4c47ba7f686a285a2173fadd", shape: Shape::GzipTar, member: "package/vendor/x86_64-apple-darwin/bin/codex", }, Artifact { platform: "windows/arm64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-win32-arm64.tgz", - bytes: 132_888_437, - sha256: "sha256:a072b19e67fd65f2827c925a9d3c89b1a55da70b8e24cc21b81c800a77cb3d53", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-win32-arm64.tgz", + bytes: 135_509_012, + sha256: "sha256:72525256ac769a23381236a374c71679927c988dd10b7a19d342c98193216b7f", shape: Shape::GzipTar, member: "package/vendor/aarch64-pc-windows-msvc/bin/codex.exe", }, Artifact { platform: "windows/x86_64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-win32-x64.tgz", - bytes: 142_162_836, - sha256: "sha256:27eedae55e37ed1da4078b36a6ef9e4ffe8546a60fd6df5ee6408fdd16aff8ea", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-win32-x64.tgz", + bytes: 145_165_338, + sha256: "sha256:727fd5bfaeed16fe8fdeb57d1c0faa688f48edda4a0541323a103a3138925350", shape: Shape::GzipTar, member: "package/vendor/x86_64-pc-windows-msvc/bin/codex.exe", }, ]; -/// The artifacts 0.153.4 was published as, kept so +/// The artifacts 0.154.0 was published as, kept so /// `software_update` has a version to move from and `rollback` a tree to /// return to. Measured from bytes when it was the current pin. pub(crate) const PREVIOUS_ARTIFACTS: &[Artifact] = &[ Artifact { platform: "linux/arm64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-linux-arm64.tgz", - bytes: 121_707_000, - sha256: "sha256:439c0dd0d6923f607b4e5cd1e3079c12f0b86f6e5007f07e377d6ad25e2d7bb9", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-linux-arm64.tgz", + bytes: 122_610_794, + sha256: "sha256:a2315b5f64bfeaff79b71e0d35505ba8c22cc1e96cab9dc950b614c804105b24", shape: Shape::GzipTar, member: "package/vendor/aarch64-unknown-linux-musl/bin/codex", }, Artifact { platform: "linux/x86_64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-linux-x64.tgz", - bytes: 129_272_137, - sha256: "sha256:54818cb9fce3360cc6e44cfc5a96952cd5c1243efb43cbe488e11dda84663e08", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-linux-x64.tgz", + bytes: 129_654_638, + sha256: "sha256:e27c83a49e6031685ee7f956c12aad5f16484d3a80181dd3fea930fb96b3832b", shape: Shape::GzipTar, member: "package/vendor/x86_64-unknown-linux-musl/bin/codex", }, Artifact { platform: "macos/arm64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-darwin-arm64.tgz", - bytes: 115_672_312, - sha256: "sha256:535d301b49131abfda3264f959fb0defa40bbc306976d98ddbc15c424636c55c", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-darwin-arm64.tgz", + bytes: 116_501_639, + sha256: "sha256:2a98662d79316a59993c7233e3e25a1aa1d42da4b45904585d33a5a7da1cade1", shape: Shape::GzipTar, member: "package/vendor/aarch64-apple-darwin/bin/codex", }, Artifact { platform: "macos/x86_64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-darwin-x64.tgz", - bytes: 123_544_033, - sha256: "sha256:5e468958503c60e940b1b1af3fe2064c16fd141f1607111caf99f2c0a0e80725", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-darwin-x64.tgz", + bytes: 124_308_807, + sha256: "sha256:92c493533c53c433c4d94252251daba4f379ccba06a9964d260abb47a535dce1", shape: Shape::GzipTar, member: "package/vendor/x86_64-apple-darwin/bin/codex", }, Artifact { platform: "windows/arm64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-win32-arm64.tgz", - bytes: 132_173_674, - sha256: "sha256:6d0bf07e04810f0ed4ad2984f6c9f0547bb9e0e1bb1f8fe9ecce0de3376bbfef", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-win32-arm64.tgz", + bytes: 132_888_437, + sha256: "sha256:a072b19e67fd65f2827c925a9d3c89b1a55da70b8e24cc21b81c800a77cb3d53", shape: Shape::GzipTar, member: "package/vendor/aarch64-pc-windows-msvc/bin/codex.exe", }, Artifact { platform: "windows/x86_64", - url: "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-win32-x64.tgz", - bytes: 141_495_386, - sha256: "sha256:05f473573f38b3f4dc9484e6807a511a1bb0e128dfa50816dafc5f79e553cdca", + url: "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-win32-x64.tgz", + bytes: 142_162_836, + sha256: "sha256:27eedae55e37ed1da4078b36a6ef9e4ffe8546a60fd6df5ee6408fdd16aff8ea", shape: Shape::GzipTar, member: "package/vendor/x86_64-pc-windows-msvc/bin/codex.exe", }, @@ -124,12 +124,12 @@ pub(crate) const PREVIOUS_ARTIFACTS: &[Artifact] = &[ /// Codex's program, and where its bytes come from. pub(crate) const SOFTWARE: Software = Software { - version: "0.154.0", + version: "0.155.1", command: "codex", delivery: Delivery::Artifacts(ARTIFACTS), unsupported: &[], previous: Some(Previous { - version: "0.153.4", + version: "0.154.0", artifacts: PREVIOUS_ARTIFACTS, }), }; diff --git a/crates/harness-runtime/src/facts.rs b/crates/harness-runtime/src/facts.rs index 632cf60..9271d9e 100644 --- a/crates/harness-runtime/src/facts.rs +++ b/crates/harness-runtime/src/facts.rs @@ -234,7 +234,20 @@ pub struct Harness { /// [`Delivery::Manager`], which is a different statement -- the product is /// installable, but not by fetching bytes whose digest was fixed in advance /// -- and the refusal says which. + /// How the product's own software is installed, when this build can do it. + /// + /// `None` means the software lifecycle is not offered at all. So does a + /// [`Delivery::Manager`], which is a different statement -- the product is + /// installable, but not by fetching bytes whose digest was fixed in advance + /// -- and the refusal says which. pub software: Option, + /// Target-relative path of the user-global instruction attachment. + /// + /// `None` when the product has no catalogued global instruction surface + /// (Antigravity). Present, this build implements `patch_instruction_region` + /// against that path. The bytes travel on `--instruction-section`; they are + /// not a setup payload. + pub instruction_region: Option<&'static str>, } /// A second set of ownings, for a target that is not the product's own home. @@ -751,21 +764,44 @@ impl Harness { /// manager this provider does not run. #[must_use] pub fn operations(&self) -> &'static [Operation] { - match (self.can_launch(), self.software) { + match ( + self.can_launch(), + self.software, + self.instruction_region.is_some(), + ) { ( true, Some(Software { delivery: Delivery::Artifacts(_), .. }), + true, ) => Operation::ALL, + ( + true, + Some(Software { + delivery: Delivery::Artifacts(_), + .. + }), + false, + ) => Operation::ALL_WITHOUT_INSTRUCTION, + ( + false, + Some(Software { + delivery: Delivery::Artifacts(_), + .. + }), + true, + ) => Operation::CORE_AND_SOFTWARE_AND_INSTRUCTION, ( false, Some(Software { delivery: Delivery::Artifacts(_), .. }), + false, ) => Operation::CORE_AND_SOFTWARE, + (_, _, true) => Operation::CORE_AND_INSTRUCTION, _ => Operation::CORE, } } @@ -968,6 +1004,7 @@ mod tests { max_files: 4096, max_bytes: 1024, kit_identity: r#"{"aggregate_digest":"sha256:aa","protocol_version":3}"#, + instruction_region: None, }; #[test] @@ -1009,6 +1046,7 @@ mod tests { Operation::SoftwareInstall, Operation::SoftwareUpdate, Operation::SoftwareRemove, + Operation::PatchInstructionRegion, ] { assert!( !info.declares(optional), @@ -1017,6 +1055,18 @@ mod tests { } } + #[test] + fn an_instruction_surface_without_software_still_declares_the_patch() { + let named = Harness { + instruction_region: Some("AGENTS.md"), + ..SAMPLE + }; + let info = named.provider_info().unwrap(); + assert!(info.declares(Operation::PatchInstructionRegion)); + assert!(!info.declares(Operation::SoftwareInstall)); + assert!(!info.declares(Operation::Launch)); + } + #[test] fn the_build_digest_is_reproducible_and_binds_the_kit() { let once = SAMPLE.build_digest().unwrap(); diff --git a/crates/harness-runtime/src/human.rs b/crates/harness-runtime/src/human.rs index a5b9e56..61ab40c 100644 --- a/crates/harness-runtime/src/human.rs +++ b/crates/harness-runtime/src/human.rs @@ -1154,6 +1154,8 @@ fn mutate( // The human surface removes whole and carries no bundle, so no path // has a second sentence. end_state: Vec::new(), + instruction_path: None, + instruction_text: None, effects: effect_lines(harness, &effect, applied.setup_id.as_deref()), })?; let plan_digest = artifact.digest()?; @@ -1251,6 +1253,9 @@ fn effect_lines(harness: &Harness, effect: &Effect<'_>, setup_id: Option<&str>) )); lines } + Effect::PatchInstruction { path, .. } => { + vec![capture, format!("patch instruction region at {path}")] + } } } diff --git a/crates/harness-runtime/src/instruction_region.rs b/crates/harness-runtime/src/instruction_region.rs new file mode 100644 index 0000000..dde1517 --- /dev/null +++ b/crates/harness-runtime/src/instruction_region.rs @@ -0,0 +1,224 @@ +//! Marked user-global instruction attachment. +//! +//! The consumer owns the section text. This kernel splices it into one +//! target-relative file, preserves every byte outside the markers, and is +//! idempotent on identical bytes. Whole-setup apply/remove must not treat the +//! region as payload they are free to empty: [`preserve_in_replacement`] and +//! [`keep_region_on_withdraw`] are the two hooks. + +use std::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"; + +/// The marked region, including both markers, or `None` when either is missing +/// or they are out of order. +#[must_use] +pub fn extract(existing: &str) -> Option<&str> { + let begin = existing.find(BEGIN)?; + let end = existing.find(END)?; + if end < begin { + return None; + } + let mut end_at = end + END.len(); + if existing[end_at..].starts_with('\n') { + end_at += 1; + } + Some(&existing[begin..end_at]) +} + +/// Replace the marked region or append it. Preserve every other byte. +#[must_use] +pub fn splice(existing: &str, section: &str) -> String { + match extract(existing) { + None if existing.is_empty() => section.to_owned(), + None => { + let mut held = existing.to_owned(); + if !held.ends_with('\n') { + held.push('\n'); + } + held.push_str(section); + held + } + Some(held) => match existing.find(BEGIN) { + None => existing.to_owned(), + Some(begin) => format!( + "{}{}{}", + &existing[..begin], + section, + &existing[begin + held.len()..] + ), + }, + } +} + +/// Pure region patch. `wrote` is false when the marked bytes already match. +/// +/// An empty file takes `section` whole so bytes the consumer placed *before* +/// the markers (Cursor `alwaysApply` YAML) survive the first write. Later +/// calls splice only the marked region. +#[must_use] +pub fn patch(existing: &str, section: &str) -> (String, bool) { + let desired = extract(section).unwrap_or(section); + if let Some(current) = extract(existing) + && current == desired + { + return (existing.to_owned(), false); + } + if existing.is_empty() { + return (section.to_owned(), true); + } + (splice(existing, desired), true) +} + +/// Keep an existing attachment when a setup writes the same path. +#[must_use] +pub fn preserve_in_replacement(existing: &str, incoming: &str) -> String { + let Some(region) = extract(existing) else { + return incoming.to_owned(); + }; + if extract(incoming).is_some() { + let (updated, _) = patch(incoming, region); + return updated; + } + let spliced = splice(incoming, region); + let owned = owned_prefix(existing); + if owned.is_empty() || spliced.starts_with("---\n") { + return spliced; + } + format!("{owned}{spliced}") +} + +/// After withdrawing a recorded file, keep the attachment remainder when one +/// existed. Returns `None` when the file had no attachment (caller deletes). +/// +/// Cursor `alwaysApply` YAML sits before the markers. That prefix is the +/// initialize attachment, not setup payload, so it survives withdraw. +#[must_use] +pub fn keep_region_on_withdraw(existing: &str) -> Option { + let region = extract(existing)?; + let owned = owned_prefix(existing); + if owned.is_empty() { + Some(region.to_owned()) + } else { + Some(format!("{owned}{region}")) + } +} + +/// YAML frontmatter initialize placed before the markers, or empty. +/// +/// Not "everything before BEGIN": a setup body can sit between the fence and +/// the region, and withdraw must still drop that body. +fn owned_prefix(existing: &str) -> &str { + const OPEN: &str = "---\n"; + const CLOSE: &str = "\n---\n"; + if !existing.starts_with(OPEN) { + return ""; + } + let Some(rel) = existing[OPEN.len()..].find(CLOSE) else { + return ""; + }; + let mut end = OPEN.len() + rel + CLOSE.len(); + if existing[end..].starts_with('\n') { + end += 1; + } + if extract(&existing[end..]).is_none() && !existing[end..].contains(BEGIN) { + return ""; + } + &existing[..end] +} + +/// True when `relative` is the attachment path this harness named. +#[must_use] +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() +} + +#[cfg(test)] +mod tests { + use super::*; + + const SECTION: &str = ":::begin-ai-stp\nhello\n:::end-ai-stp\n"; + + #[test] + fn empty_file_receives_the_section() { + let (updated, wrote) = patch("", SECTION); + assert!(wrote); + assert_eq!(updated, SECTION); + } + + #[test] + fn user_bytes_outside_the_markers_are_kept() { + let (updated, wrote) = patch("keep-me\n", SECTION); + assert!(wrote); + assert_eq!(updated, format!("keep-me\n{SECTION}")); + } + + #[test] + fn identical_bytes_are_a_no_write() { + let first = splice("keep-me\n", SECTION); + let (second, wrote) = patch(&first, SECTION); + assert!(!wrote); + assert_eq!(second, first); + } + + #[test] + fn first_write_keeps_bytes_before_the_markers() { + let section = "---\nalwaysApply: true\n---\n\n:::begin-ai-stp\nhello\n:::end-ai-stp\n"; + let (updated, wrote) = patch("", section); + assert!(wrote); + assert_eq!(updated, section); + assert!(updated.starts_with("---\n")); + + let next = "---\nalwaysApply: true\n---\n\n:::begin-ai-stp\nchanged\n:::end-ai-stp\n"; + let (spliced, wrote_again) = patch(&updated, next); + assert!(wrote_again); + assert!(spliced.starts_with("---\n")); + assert_eq!(spliced.matches("alwaysApply: true").count(), 1); + assert_eq!(extract(&spliced), extract(next)); + assert_eq!( + extract(&spliced), + Some(":::begin-ai-stp\nchanged\n:::end-ai-stp\n") + ); + } + + #[test] + fn replacement_of_a_setup_file_keeps_the_region() { + let existing = splice("old-setup\n", SECTION); + let outgoing = preserve_in_replacement(&existing, "new-setup\n"); + assert!(outgoing.contains("new-setup")); + assert_eq!(extract(&outgoing), Some(SECTION)); + } + + #[test] + fn withdraw_leaves_the_region_and_drops_the_rest() { + let existing = splice("setup-bytes\n", SECTION); + assert_eq!(keep_region_on_withdraw(&existing).as_deref(), Some(SECTION)); + assert_eq!(keep_region_on_withdraw("just setup"), None); + } + + #[test] + fn yaml_frontmatter_survives_setup_replace_and_withdraw() { + let section = "---\nalwaysApply: true\n---\n\n:::begin-ai-stp\nhello\n:::end-ai-stp\n"; + let (first, _) = patch("", section); + let replaced = preserve_in_replacement(&first, "new-setup\n"); + assert!(replaced.starts_with("---\n")); + assert_eq!(replaced.matches("alwaysApply: true").count(), 1); + assert!(replaced.contains("new-setup")); + assert_eq!(extract(&replaced), extract(section)); + + let remainder = keep_region_on_withdraw(&replaced).unwrap_or_default(); + assert!(!remainder.is_empty()); + assert!(remainder.starts_with("---\n")); + assert!(remainder.contains("alwaysApply: true")); + assert!(!remainder.contains("new-setup")); + assert_eq!(extract(&remainder), extract(section)); + } +} diff --git a/crates/harness-runtime/src/lib.rs b/crates/harness-runtime/src/lib.rs index 9e27700..a7b945f 100644 --- a/crates/harness-runtime/src/lib.rs +++ b/crates/harness-runtime/src/lib.rs @@ -37,6 +37,7 @@ pub mod catalog; pub mod expiry; pub mod facts; pub mod human; +pub(crate) mod instruction_region; pub mod probe; pub(crate) mod software; pub mod surfaces; diff --git a/crates/harness-runtime/src/wire.rs b/crates/harness-runtime/src/wire.rs index 92399d4..7942c82 100644 --- a/crates/harness-runtime/src/wire.rs +++ b/crates/harness-runtime/src/wire.rs @@ -654,6 +654,8 @@ fn honourable(harness: &Harness, request: &PlanRequest) -> Result<()> { } } + honour_instruction_section(harness, request)?; + // 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 @@ -722,6 +724,80 @@ fn honourable(harness: &Harness, request: &PlanRequest) -> Result<()> { Ok(()) } +fn honour_instruction_section(harness: &Harness, request: &PlanRequest) -> Result<()> { + if request.instruction_section.is_some() + && request.operation != Operation::PatchInstructionRegion + { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + format!( + "{} takes no instruction section; patch_instruction_region is the \ + operation that reads --instruction-section", + request.operation + ), + )); + } + if request.operation != Operation::PatchInstructionRegion { + return Ok(()); + } + if harness.instruction_region.is_none() { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + format!( + "{} does not declare a user-global instruction surface", + harness.provider_id + ), + )); + } + if request + .instruction_section + .as_deref() + .is_none_or(str::is_empty) + { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + "patch_instruction_region needs --instruction-section with marked bytes", + )); + } + Ok(()) +} + +fn plan_instruction_patch( + harness: &Harness, + target: &Target, + request: &PlanRequest, +) -> Result<(Vec, String, String)> { + let Some(relative) = harness.instruction_region else { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + format!( + "{} does not declare a user-global instruction surface", + harness.provider_id + ), + )); + }; + let Some(section) = request.instruction_section.as_deref() else { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + "patch_instruction_region needs --instruction-section with marked bytes", + )); + }; + if crate::instruction_region::extract(section).is_none() { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + "instruction_section must contain :::begin-ai-stp and :::end-ai-stp", + )); + } + let existing = crate::instruction_region::read_utf8(&target.root().join(relative)); + let (updated, wrote) = crate::instruction_region::patch(&existing, section); + let effects = if wrote { + vec![format!("patch instruction region at {relative}")] + } else { + vec![format!("instruction region at {relative} already matches")] + }; + Ok((effects, relative.to_owned(), updated)) +} + /// Produce a plan without touching the target. /// What a mutation **takes away** before it writes, in the words the result /// line already uses. @@ -854,6 +930,15 @@ fn plan(harness: &Harness, target: &Path, request: &PlanRequest) -> Result { + let mut capture = owned.clone(); + if let Some(path) = harness.instruction_region + && !capture.iter().any(|held| held == path) + { + capture.push(path.to_owned()); + } + capture + } _ => owned.clone(), }; refuse_uncapturable(&resolved, &capture)?; @@ -863,6 +948,8 @@ fn plan(harness: &Harness, target: &Path, request: &PlanRequest) -> Result = None; let mut software_version_held: Option = None; let mut end_state = Vec::new(); + let mut instruction_path = None; + let mut instruction_text = None; let (effects, backup_ref, restore_target_digest) = match request.operation { Operation::SoftwareInstall | Operation::SoftwareUpdate | Operation::SoftwareRemove => { let (planned, effects, version) = software::plan( @@ -929,6 +1016,12 @@ fn plan(harness: &Harness, target: &Path, request: &PlanRequest) -> Result (bundle_effects(harness, request)?, None, None), + Operation::PatchInstructionRegion => { + let (lines, path, text) = plan_instruction_patch(harness, &resolved, request)?; + instruction_path = Some(path); + instruction_text = Some(text); + (lines, None, None) + } other @ Operation::Launch => { return Err(Error::refuse( WireReason::UnsupportedOperation, @@ -968,6 +1061,8 @@ fn plan(harness: &Harness, target: &Path, request: &PlanRequest) -> Result { /// Each declared file's bytes and mode, by target-relative path. files: &'a BTreeMap, u32)>, }, + /// Splice a marked instruction region into one owned-or-neighbour file. + PatchInstruction { + /// Target-relative path. + path: String, + /// Full file text after splicing. + text: String, + }, } /// One authorized mutation, whatever surface asked for it. @@ -1734,6 +1836,11 @@ fn apply( files: &ready.files, } } + Operation::PatchInstructionRegion => { + let path = string_field(&artifact, "instruction_path")?; + let text = string_field(&artifact, "instruction_text")?; + Effect::PatchInstruction { path, text } + } other => { return Err(Error::refuse( WireReason::UnsupportedOperation, @@ -1975,6 +2082,22 @@ pub(crate) fn perform( Effect::MaterializeBundle { files } => { write_bundle_files(harness, &resolved, files, mutation.target_scope) } + Effect::PatchInstruction { path, text } => { + let destination = resolved.root().join(path); + if let Some(parent) = destination.parent() { + fs::create_dir_all(parent).map_err(|error| { + Error::from( + setup_core::Error::new( + setup_core::ReasonCode::StateUnavailable, + format!("cannot create {}", parent.display()), + ) + .with_source(error), + ) + })?; + } + lock::atomic_write(&destination, text.as_bytes()).map_err(Error::from)?; + Ok(previous_written.clone()) + } // Keeping a predecessor's stamp aside writes nothing to the target, so // the inventory is what it was. Same reason as `Backup` above. Effect::Adopt { stamp } => { @@ -2754,6 +2877,21 @@ fn write_host_file( } else { bytes.to_vec() }; + 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) + } else { + outgoing + }; if merge_json && let Some(keys) = json_top_keys(bytes) { remember_written_fields(harness, target, relative, keys)?; } @@ -2794,6 +2932,13 @@ 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(()); + } remove_keeping(&destination, target.root(), harness.never_touch) } @@ -3317,6 +3462,7 @@ pub(crate) mod tests_support { max_files: 4096, max_bytes: 64 * 1024 * 1024, kit_identity: r#"{"aggregate_digest":"sha256:aa","protocol_version":3}"#, + instruction_region: Some("AGENTS.md"), }; } @@ -8154,6 +8300,7 @@ mod tests { assert!(info.declares(Operation::SoftwareInstall)); assert!(info.declares(Operation::SoftwareUpdate)); assert!(info.declares(Operation::SoftwareRemove)); + assert!(info.declares(Operation::PatchInstructionRegion)); } #[test] @@ -8166,6 +8313,10 @@ mod tests { assert!(!info.declares(Operation::SoftwareInstall)); assert!(!info.declares(Operation::SoftwareUpdate)); assert!(!info.declares(Operation::SoftwareRemove)); + assert!( + info.declares(Operation::PatchInstructionRegion), + "the instruction attachment is not a software lifecycle" + ); let error = software::plan( &bare, @@ -8176,6 +8327,103 @@ mod tests { .unwrap_err(); assert_eq!(error.reason(), Some(WireReason::UnsupportedOperation)); } + + const INSTRUCTION_SECTION: &str = ":::begin-ai-stp\nhello from ai-stp\n:::end-ai-stp\n"; + + #[test] + fn patch_instruction_region_splices_markers_and_survives_a_recorded_withdraw() { + let target = seeded("patch-instruction-region"); + let applied = plan_then_apply( + &target, + "patch_instruction_region", + &["--instruction-section", INSTRUCTION_SECTION], + ); + assert_eq!(applied["state"], "verified", "{applied}"); + let after_patch = fs::read_to_string(target.join("AGENTS.md")).unwrap(); + assert!( + after_patch.starts_with("# first\n"), + "user bytes outside the markers were dropped: {after_patch:?}" + ); + assert_eq!( + crate::instruction_region::extract(&after_patch), + Some(INSTRUCTION_SECTION) + ); + assert!( + !recorded_written(&target).contains(&"AGENTS.md".to_owned()), + "the attachment was recorded as a setup receipt: {:?}", + recorded_written(&target) + ); + + install_global(&target, "with-agents", &[("AGENTS.md", "# setup\n", 0o644)]); + let after_install = fs::read_to_string(target.join("AGENTS.md")).unwrap(); + assert!( + after_install.contains("# setup"), + "the setup did not land: {after_install:?}" + ); + assert_eq!( + crate::instruction_region::extract(&after_install), + Some(INSTRUCTION_SECTION), + "install emptied the attachment: {after_install:?}" + ); + assert!( + recorded_written(&target).contains(&"AGENTS.md".to_owned()), + "install did not record the setup file: {:?}", + recorded_written(&target) + ); + + let removed = plan_then_apply(&target, "remove", &[]); + assert_eq!(removed["state"], "verified", "{removed}"); + let after_remove = fs::read_to_string(target.join("AGENTS.md")).unwrap(); + assert_eq!( + crate::instruction_region::extract(&after_remove), + Some(INSTRUCTION_SECTION), + "withdraw deleted the attachment: {after_remove:?}" + ); + assert!( + !after_remove.contains("# setup"), + "withdraw left setup bytes beside the region: {after_remove:?}" + ); + } + + #[test] + fn a_harness_without_an_instruction_surface_refuses_the_patch() { + let mut mute = TEST; + mute.instruction_region = None; + let target = seeded("no-instruction-surface"); + let error = refuse_for( + &mute, + 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::UnsupportedOperation)); + assert!( + error + .detail() + .contains("does not declare a user-global instruction surface"), + "{}", + error.detail() + ); + assert!( + !mute + .provider_info() + .unwrap() + .declares(Operation::PatchInstructionRegion) + ); + } /// `status` says which slots retention may not take, and whose they are. /// /// The pool has known this since held slots shipped; `status` did not diff --git a/crates/provider-v3/src/argv.rs b/crates/provider-v3/src/argv.rs index aa95412..18d0d68 100644 --- a/crates/provider-v3/src/argv.rs +++ b/crates/provider-v3/src/argv.rs @@ -118,6 +118,10 @@ const fn plan_usage(command: Command) -> Usage { "--target-scope", "which scope this target is; accepted and not yet acted on", ), + ( + "--instruction-section", + "marked user-global instruction bytes for patch_instruction_region", + ), ], note: "Produce a plan. Always pure: reads the target and the local disk, opens no socket.", } @@ -360,7 +364,13 @@ pub struct PlanRequest { /// declarable since `0.0.7` and operable never, because no request field /// carried a scope; this is the field, arriving one release ahead of the /// behaviour on purpose. + /// Which scope the consumer resolved this target to be. pub target_scope: Option, + /// Marked instruction bytes for `patch_instruction_region`. + /// + /// Absent on every other operation. The consumer sends this only after a + /// provider declares both the operation and `instruction_section`. + pub instruction_section: Option, } /// Read `--target-scope`, refusing a value this build does not know. @@ -521,6 +531,7 @@ where prefix: flags.take_prefix()?, software_version: flags.take_optional("--software-version"), target_scope: take_target_scope(&mut flags)?, + instruction_section: flags.take_optional("--instruction-section"), }, } } @@ -565,6 +576,15 @@ struct Flags { /// two different things and only one of them would happen. const REPEATABLE: &[&str] = &["--software-artifact"]; +/// True when `token` is another flag, not a value that happens to start with +/// dashes. Cursor's `alwaysApply` payload begins with YAML `---`; treating +/// every `--` prefix as a missing value refused that first write. +fn looks_like_flag(token: &str) -> bool { + token + .strip_prefix("--") + .is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_alphabetic())) +} + impl Flags { /// Split a bare `--` off the end, keeping what follows verbatim. /// @@ -601,7 +621,7 @@ impl Flags { let Some(value) = tokens.get(index + 1) else { return Err(local(format!("{token} has no value"))); }; - if value.starts_with("--") { + if looks_like_flag(value) { return Err(local(format!("{token} has no value"))); } let seen = values.entry(token.clone()).or_default(); @@ -1021,6 +1041,57 @@ mod tests { assert_eq!(error.reason(), Some(WireReason::UnsupportedOperation)); } + #[test] + fn patch_instruction_region_parses_the_marked_section() { + let tokens = with_target( + "plan-operation", + &[ + "--operation", + "patch_instruction_region", + "--provider-release-digest", + DIGEST, + "--operation-id", + "operation_01TEST", + "--expires-at", + "2026-08-23T15:00:00Z", + "--instruction-section", + ":::begin-ai-stp\nhello\n:::end-ai-stp\n", + ], + ); + let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else { + panic!("expected a plan invocation"); + }; + assert_eq!(request.operation, Operation::PatchInstructionRegion); + assert_eq!( + request.instruction_section.as_deref(), + Some(":::begin-ai-stp\nhello\n:::end-ai-stp\n") + ); + } + + #[test] + fn patch_instruction_region_parses_yaml_frontmatter_before_markers() { + let section = "---\nalwaysApply: true\n---\n\n:::begin-ai-stp\nhello\n:::end-ai-stp\n"; + let tokens = with_target( + "plan-operation", + &[ + "--operation", + "patch_instruction_region", + "--provider-release-digest", + DIGEST, + "--instruction-section", + section, + "--operation-id", + "operation_01TEST", + "--expires-at", + "2026-08-23T15:00:00Z", + ], + ); + let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else { + panic!("expected a plan invocation"); + }; + assert_eq!(request.instruction_section.as_deref(), Some(section)); + } + /// A consumer that sends a scope must not be refused by a build that cannot /// yet use it. /// diff --git a/crates/provider-v3/src/info.rs b/crates/provider-v3/src/info.rs index b94fda9..1efdc2e 100644 --- a/crates/provider-v3/src/info.rs +++ b/crates/provider-v3/src/info.rs @@ -409,6 +409,7 @@ impl ProviderInfo { TargetScope::REQUEST_FIELD.to_owned(), EndState::REQUEST_FIELD.to_owned(), "capture_mode".to_owned(), + "instruction_section".to_owned(), ], // Declared 2026-09-02 in the same order as the two above: kit 0.2.9 // names the member (`provider-info.schema.json`), ai-stp-cli diff --git a/crates/provider-v3/src/plan.rs b/crates/provider-v3/src/plan.rs index 597abec..4385242 100644 --- a/crates/provider-v3/src/plan.rs +++ b/crates/provider-v3/src/plan.rs @@ -256,6 +256,12 @@ pub struct PlanArtifact { /// verifying. Every other operation has one sentence per path already. #[serde(skip_serializing_if = "Vec::is_empty")] pub end_state: Vec, + /// Target-relative path of a `patch_instruction_region` write. + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_path: Option, + /// Full file text after splicing. Apply writes these bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_text: Option, /// What applying it will do, in order. Never empty. pub effects: Vec, } @@ -305,6 +311,10 @@ pub struct PlanInputs<'a> { /// Per-path end states, for a `remove` that carries a bundle. Empty /// otherwise, and refused on any other operation. pub end_state: Vec, + /// Target-relative attachment path, when this is a region patch. + pub instruction_path: Option, + /// Full file text after splicing, when this is a region patch. + pub instruction_text: Option, /// What applying it will do. Never empty. pub effects: Vec, } @@ -418,6 +428,8 @@ impl PlanArtifact { software_version: inputs.software_version.map(str::to_owned), software_artifacts: inputs.software_artifacts, end_state: inputs.end_state, + instruction_path: inputs.instruction_path, + instruction_text: inputs.instruction_text, effects: inputs.effects, }) } @@ -567,6 +579,8 @@ mod tests { native_capture: None, permission_profile: Some("default".to_owned()), expires_at: "2026-08-23T15:00:00Z", + instruction_path: None, + instruction_text: None, effects: vec!["write settings.json".to_owned()], } } diff --git a/crates/provider-v3/src/vocabulary.rs b/crates/provider-v3/src/vocabulary.rs index f18cafb..3f9c2c1 100644 --- a/crates/provider-v3/src/vocabulary.rs +++ b/crates/provider-v3/src/vocabulary.rs @@ -135,6 +135,12 @@ pub enum Operation { SoftwareRemove, /// Start the product. Optional. Launch, + /// Patch a marked user-global instruction region. Optional. + /// + /// The consumer sends the marked bytes on `--instruction-section`. This is + /// not a setup install: whole-setup apply must not treat the region as + /// payload it is free to empty. + PatchInstructionRegion, } impl Operation { @@ -150,6 +156,7 @@ impl Operation { Self::SoftwareUpdate, Self::SoftwareRemove, Self::Launch, + Self::PatchInstructionRegion, ]; /// The operations every provider must support. @@ -174,6 +181,44 @@ impl Operation { Self::SoftwareRemove, ]; + /// [`CORE`] plus the instruction-region patch, without software or launch. + pub const CORE_AND_INSTRUCTION: &'static [Self] = &[ + Self::Backup, + Self::Install, + Self::Remove, + Self::Replace, + Self::Restore, + Self::PatchInstructionRegion, + ]; + + /// [`CORE_AND_SOFTWARE`] plus the instruction-region patch, without launch. + pub const CORE_AND_SOFTWARE_AND_INSTRUCTION: &'static [Self] = &[ + Self::Backup, + Self::Install, + Self::Remove, + Self::Replace, + Self::Restore, + Self::Reset, + Self::SoftwareInstall, + Self::SoftwareUpdate, + Self::SoftwareRemove, + Self::PatchInstructionRegion, + ]; + + /// [`ALL`] without the instruction-region patch. + pub const ALL_WITHOUT_INSTRUCTION: &'static [Self] = &[ + Self::Install, + Self::Replace, + Self::Backup, + Self::Restore, + Self::Remove, + Self::Reset, + Self::SoftwareInstall, + Self::SoftwareUpdate, + Self::SoftwareRemove, + Self::Launch, + ]; + /// The optional operations that install the product itself. /// /// Declared together or not at all. A provider offering to install but not @@ -198,6 +243,7 @@ impl Operation { Self::SoftwareUpdate => "software_update", Self::SoftwareRemove => "software_remove", Self::Launch => "launch", + Self::PatchInstructionRegion => "patch_instruction_region", } } diff --git a/install.ps1 b/install.ps1 index f0bbcd5..1bb44d3 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.72", + [string]$Version = "0.0.73", [string]$InstallDir = "$env:LOCALAPPDATA\Programs\codex-setup-system" ) $ErrorActionPreference = "Stop" diff --git a/install.sh b/install.sh index 9da9b72..3ccd8f5 100644 --- a/install.sh +++ b/install.sh @@ -14,7 +14,7 @@ set -eu REPO="NDDev-OpenNetwork/codex-setup-system" BINARY="codex-setup-system" -VERSION="${1:-0.0.72}" +VERSION="${1:-0.0.73}" PREFIX="${CODEX_INSTALL_DIR:-$HOME/.local/bin}" case "$(uname -s)" in diff --git a/provider-kit/v3/KIT-IDENTITY.json b/provider-kit/v3/KIT-IDENTITY.json index ca45c47..1ed9e58 100644 --- a/provider-kit/v3/KIT-IDENTITY.json +++ b/provider-kit/v3/KIT-IDENTITY.json @@ -1,12 +1,12 @@ { - "aggregate_digest": "sha256:6ab195790be1c7dfacf6e6ab5a892244097cc2002f97090ff9a4b3c3174ed357", + "aggregate_digest": "sha256:e2a35eaf2e0f834913962d9a6337948eeb78290b4ad8c3af7674620215610b0e", "files": [ "conformance-cases.json", "manifest.json", "provider-info.schema.json", "status-response.schema.json" ], - "kit_version": "0.2.11", + "kit_version": "0.2.13", "protocol_version": 3, "schema": "ai-stp-provider-kit-identity/1" } diff --git a/provider-kit/v3/SHA256SUMS b/provider-kit/v3/SHA256SUMS index 6f9f240..e4d165d 100644 --- a/provider-kit/v3/SHA256SUMS +++ b/provider-kit/v3/SHA256SUMS @@ -1,4 +1,4 @@ fe04d03b15cfe8d5b61835eafd3ea788074684e2be69d419f9c22a37ea0461b7 conformance-cases.json -ae323ef26648fbaf441aa179008d0899ec79af88b3ae1e738e5bc75b1bbc8f30 manifest.json -64514da0c61012b5323987d72ee75f5ce8d6451ed557331a634b17aeac8d8966 provider-info.schema.json +cfc55bb6f301589ded449c12ba0309e12238db7bbce3f5e63fc45e9731d4bee5 manifest.json +2f28fef790dc2685e9790f4ae3123b1c6d581c94f87d15b75823ea8d575036aa provider-info.schema.json 7e67760f24bbc42a1242dad7ad3b378eef7ae15023c6813a33d582dfd2232293 status-response.schema.json diff --git a/provider-kit/v3/manifest.json b/provider-kit/v3/manifest.json index 9a4afcc..7516384 100644 --- a/provider-kit/v3/manifest.json +++ b/provider-kit/v3/manifest.json @@ -39,7 +39,7 @@ ], "decision": "docs/adr/ADR-0061-capability-negotiated-provider-protocol-v3.md", "generated_from": "apps/cli/src/ai_stp_cli/provider/protocol_v3.py", - "kit_version": "0.2.11", + "kit_version": "0.2.13", "operation_network": { "backup": [ { @@ -67,6 +67,16 @@ "phase": "execute" } ], + "patch_instruction_region": [ + { + "network_requirement": "none", + "phase": "plan" + }, + { + "network_requirement": "none", + "phase": "apply" + } + ], "remove": [ { "network_requirement": "none", @@ -151,6 +161,7 @@ ], "optional_operations": [ "launch", + "patch_instruction_region", "reset", "software_install", "software_remove", diff --git a/provider-kit/v3/provider-info.schema.json b/provider-kit/v3/provider-info.schema.json index 53ba00d..cc88b58 100644 --- a/provider-kit/v3/provider-info.schema.json +++ b/provider-kit/v3/provider-info.schema.json @@ -20,6 +20,7 @@ "enum": [ "capture_mode", "end_state", + "instruction_section", "target_scope" ], "type": "string" @@ -267,7 +268,8 @@ "software_install", "software_update", "software_remove", - "launch" + "launch", + "patch_instruction_region" ], "type": "string" }, diff --git a/references/codex-baseline.json b/references/codex-baseline.json index 2826f3c..8a37cf0 100644 --- a/references/codex-baseline.json +++ b/references/codex-baseline.json @@ -21,12 +21,12 @@ "desktop_documented_product": "ChatGPT", "desktop_pinned_cli_packaging": "Codex.app/Codex.dmg", "installer_asset": "install.sh", - "installer_release": "rust-v0.154.0", - "installed_version": "0.154.0", + "installer_release": "rust-v0.155.1", + "installed_version": "0.155.1", "standalone_layout": "packages/standalone" }, "verified_version_ref": "build/version.json:codex_cli_tested", - "verified_at": "2026-09-16T13:21:19+00:00", + "verified_at": "2026-09-19T14:51:09+00:00", "native_surfaces": { "verified_at": "2026-08-31", "config_home": "~/.codex", @@ -212,89 +212,89 @@ "shape": "gzip-tar", "platforms": { "linux/arm64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-linux-arm64.tgz", - "bytes": 122610794, - "sha256": "sha256:a2315b5f64bfeaff79b71e0d35505ba8c22cc1e96cab9dc950b614c804105b24", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-linux-arm64.tgz", + "bytes": 135126766, + "sha256": "sha256:7da3e7bea6db4751d1b837f046c9cec08918ac7f05427dd0b5bcf64ea6c85964", "member": "package/vendor/aarch64-unknown-linux-musl/bin/codex" }, "linux/x86_64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-linux-x64.tgz", - "bytes": 129654638, - "sha256": "sha256:e27c83a49e6031685ee7f956c12aad5f16484d3a80181dd3fea930fb96b3832b", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-linux-x64.tgz", + "bytes": 142140011, + "sha256": "sha256:f110cccdd50b0be8130b84f45b3144ea775c233f1c8bd8226da6ee719d63d206", "member": "package/vendor/x86_64-unknown-linux-musl/bin/codex" }, "macos/arm64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-darwin-arm64.tgz", - "bytes": 116501639, - "sha256": "sha256:2a98662d79316a59993c7233e3e25a1aa1d42da4b45904585d33a5a7da1cade1", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-darwin-arm64.tgz", + "bytes": 127465533, + "sha256": "sha256:93cc218b25b71c8da3edb50a013fbd22acf8f39058fb64083fefff638a084976", "member": "package/vendor/aarch64-apple-darwin/bin/codex" }, "macos/x86_64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-darwin-x64.tgz", - "bytes": 124308807, - "sha256": "sha256:92c493533c53c433c4d94252251daba4f379ccba06a9964d260abb47a535dce1", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-darwin-x64.tgz", + "bytes": 135811357, + "sha256": "sha256:819db6dbb57a56cc21383a30331fc4115696e02f4c47ba7f686a285a2173fadd", "member": "package/vendor/x86_64-apple-darwin/bin/codex" }, "windows/arm64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-win32-arm64.tgz", - "bytes": 132888437, - "sha256": "sha256:a072b19e67fd65f2827c925a9d3c89b1a55da70b8e24cc21b81c800a77cb3d53", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-win32-arm64.tgz", + "bytes": 135509012, + "sha256": "sha256:72525256ac769a23381236a374c71679927c988dd10b7a19d342c98193216b7f", "member": "package/vendor/aarch64-pc-windows-msvc/bin/codex.exe" }, "windows/x86_64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-win32-x64.tgz", - "bytes": 142162836, - "sha256": "sha256:27eedae55e37ed1da4078b36a6ef9e4ffe8546a60fd6df5ee6408fdd16aff8ea", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.155.1-win32-x64.tgz", + "bytes": 145165338, + "sha256": "sha256:727fd5bfaeed16fe8fdeb57d1c0faa688f48edda4a0541323a103a3138925350", "member": "package/vendor/x86_64-pc-windows-msvc/bin/codex.exe" } }, - "version": "0.154.0", - "verified_at": "2026-09-16T13:21:19+00:00" + "version": "0.155.1", + "verified_at": "2026-09-19T14:51:09+00:00" }, - "setup_catalogue_digest": "sha256:798c777e0ec0accbd155d666d896c1aad341779c8c8a457fdeccaecfb8f5499e", + "setup_catalogue_digest": "sha256:bddb409bb05f14d42c9a9891b8579e4a289136d433c76f0b115efc5d1eae4882", "previous_software_artifacts": { "command": "codex", "shape": "gzip-tar", "platforms": { "linux/arm64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-linux-arm64.tgz", - "bytes": 121707000, - "sha256": "sha256:439c0dd0d6923f607b4e5cd1e3079c12f0b86f6e5007f07e377d6ad25e2d7bb9", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-linux-arm64.tgz", + "bytes": 122610794, + "sha256": "sha256:a2315b5f64bfeaff79b71e0d35505ba8c22cc1e96cab9dc950b614c804105b24", "member": "package/vendor/aarch64-unknown-linux-musl/bin/codex" }, "linux/x86_64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-linux-x64.tgz", - "bytes": 129272137, - "sha256": "sha256:54818cb9fce3360cc6e44cfc5a96952cd5c1243efb43cbe488e11dda84663e08", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-linux-x64.tgz", + "bytes": 129654638, + "sha256": "sha256:e27c83a49e6031685ee7f956c12aad5f16484d3a80181dd3fea930fb96b3832b", "member": "package/vendor/x86_64-unknown-linux-musl/bin/codex" }, "macos/arm64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-darwin-arm64.tgz", - "bytes": 115672312, - "sha256": "sha256:535d301b49131abfda3264f959fb0defa40bbc306976d98ddbc15c424636c55c", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-darwin-arm64.tgz", + "bytes": 116501639, + "sha256": "sha256:2a98662d79316a59993c7233e3e25a1aa1d42da4b45904585d33a5a7da1cade1", "member": "package/vendor/aarch64-apple-darwin/bin/codex" }, "macos/x86_64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-darwin-x64.tgz", - "bytes": 123544033, - "sha256": "sha256:5e468958503c60e940b1b1af3fe2064c16fd141f1607111caf99f2c0a0e80725", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-darwin-x64.tgz", + "bytes": 124308807, + "sha256": "sha256:92c493533c53c433c4d94252251daba4f379ccba06a9964d260abb47a535dce1", "member": "package/vendor/x86_64-apple-darwin/bin/codex" }, "windows/arm64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-win32-arm64.tgz", - "bytes": 132173674, - "sha256": "sha256:6d0bf07e04810f0ed4ad2984f6c9f0547bb9e0e1bb1f8fe9ecce0de3376bbfef", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-win32-arm64.tgz", + "bytes": 132888437, + "sha256": "sha256:a072b19e67fd65f2827c925a9d3c89b1a55da70b8e24cc21b81c800a77cb3d53", "member": "package/vendor/aarch64-pc-windows-msvc/bin/codex.exe" }, "windows/x86_64": { - "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.4-win32-x64.tgz", - "bytes": 141495386, - "sha256": "sha256:05f473573f38b3f4dc9484e6807a511a1bb0e128dfa50816dafc5f79e553cdca", + "url": "https://registry.npmjs.org/@openai/codex/-/codex-0.154.0-win32-x64.tgz", + "bytes": 142162836, + "sha256": "sha256:27eedae55e37ed1da4078b36a6ef9e4ffe8546a60fd6df5ee6408fdd16aff8ea", "member": "package/vendor/x86_64-pc-windows-msvc/bin/codex.exe" } }, - "version": "0.153.4", - "verified_at": "2026-09-07T03:12:33+00:00" + "version": "0.154.0", + "verified_at": "2026-09-16T13:21:19+00:00" }, "capability_features": { "measured_at": "2026-08-30", diff --git a/setups/nddev-builder/home/agents/nddev-builder.toml b/setups/nddev-builder/home/agents/nddev-builder.toml index 0de08af..bf65b6e 100644 --- a/setups/nddev-builder/home/agents/nddev-builder.toml +++ b/setups/nddev-builder/home/agents/nddev-builder.toml @@ -55,7 +55,7 @@ what it owns, ask the binary: `codex-setup-system provider-info`. | `developer_instructions` | **yes** | The role's prompt. Refused when absent, and it is the one a reader leaves out. | | `nickname_candidates` | no | Names an agent spawned with this role may be given. | | `model` | no | Model override for sessions spawned with this role. | -| `model_reasoning_effort` | no | Reasoning effort for the selected model. | +| `model_reasoning_effort` | no | `low`, `medium`, `high`, `xhigh`, `max` or `ultra` where the selected model supports them; unset means the model's own default, not the parent's effort. | | `sandbox_mode` | no | Narrow the spawned session's sandbox policy. | | `mcp_servers` | no | Role-specific MCP server configuration. | | `skills.config` | no | Skills made available to the spawned session. |