From 10dc9c3e230a2f71e24dbebc7249c02801349663 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 31 Aug 2026 06:21:10 +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.47. This tree is generated: fix the source and the renderer, never this checkout. --- .github/workflows/evidence.yml | 39 +- CHANGELOG.md | 140 +++++ Cargo.lock | 8 +- Cargo.toml | 8 +- README.md | 14 +- crates/harness-runtime/src/facts.rs | 29 + crates/harness-runtime/src/surfaces.rs | 134 ++++- crates/harness-runtime/src/wire.rs | 200 ++++++- crates/opencode-setup-system/src/main.rs | 41 +- install.ps1 | 2 +- install.sh | 2 +- provider-kit/v3/KIT-IDENTITY.json | 7 +- provider-kit/v3/SHA256SUMS | 5 +- provider-kit/v3/conformance-cases.json | 52 +- provider-kit/v3/manifest.json | 3 +- provider-kit/v3/status-response.schema.json | 540 ++++++++++++++++++ references/opencode-baseline.json | 11 +- scripts/evidence.py | 37 +- setups/full-auto/home/AGENTS.md | 6 +- setups/nddev-builder/home/AGENTS.md | 6 +- setups/nddev-builder/home/opencode.json | 3 +- .../references/authoring-instructions.md | 6 + .../nddev-builder/references/surfaces.md | 4 +- setups/nddev-builder/setup.json | 6 +- 24 files changed, 1205 insertions(+), 98 deletions(-) create mode 100644 provider-kit/v3/status-response.schema.json diff --git a/.github/workflows/evidence.yml b/.github/workflows/evidence.yml index b595bde..4003079 100644 --- a/.github/workflows/evidence.yml +++ b/.github/workflows/evidence.yml @@ -25,13 +25,36 @@ concurrency: jobs: lifecycle: - name: lifecycle (${{ matrix.os }}) - runs-on: ${{ matrix.os }} + name: lifecycle (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} timeout-minutes: 45 strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + # The release publishes six native provider binaries and every product + # table names the same six host ids. Three runners exercised vendor + # bytes until 2026-08-31, leaving Linux arm64, Windows arm64 and macOS + # x86_64 as build-only claims. Use exact hosted labels rather than + # `*-latest`, whose architecture can move without this file changing. + include: + - runner: ubuntu-24.04 + platform: linux/x86_64 + runner_arch: X64 + - runner: ubuntu-24.04-arm + platform: linux/arm64 + runner_arch: ARM64 + - runner: windows-2025 + platform: windows/x86_64 + runner_arch: X64 + - runner: windows-11-arm + platform: windows/arm64 + runner_arch: ARM64 + - runner: macos-15-intel + platform: macos/x86_64 + runner_arch: X64 + - runner: macos-15 + platform: macos/arm64 + runner_arch: ARM64 defaults: run: shell: bash @@ -46,6 +69,16 @@ jobs: with: toolchain: '1.98.0' + - name: Prove this is the native architecture, not emulation + env: + EXPECTED_RUNNER_ARCH: ${{ matrix.runner_arch }} + EVIDENCE_PLATFORM: ${{ matrix.platform }} + EVIDENCE_RUNNER: ${{ matrix.runner }} + run: | + set -euo pipefail + test "$RUNNER_ARCH" = "$EXPECTED_RUNNER_ARCH" + echo "native $EVIDENCE_PLATFORM on $EVIDENCE_RUNNER ($RUNNER_ARCH)" + - run: cargo build --locked --release - name: The whole software lifecycle, against the vendor's own bytes diff --git a/CHANGELOG.md b/CHANGELOG.md index b5a3b5e..ff1ea57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,146 @@ sibling project the same week this note was added. ## [Unreleased] +## [0.0.47] - 2026-08-31 + +A target caught part-way through a change can be recovered from the daily +command again, and the reason it could not is that neither side of this pair +had anything to be wrong against. + +**An interrupted operation was reported, in a name nothing reads.** Every one +of these systems has kept a durable record of a mutation in flight since the +beginning, and published it. The tool that installs and updates them decides +whether recovery is owed by looking at two other things entirely -- a state +value none of these systems emits, and a key none of them had. So a target +holding an unsettled record answered *managed*, both of those questions came +back no, and the recovery command that exists to settle it was never reached +from the path anybody uses. + +The fact was in the answer the whole time, under a name the reader does not +know. It is now also under the name the reader does know, saying which of the +two things is owed: a restore, when the effect may be partial, or a tail to +clear, when the effect landed and only cleanup remains. A settled target says +so explicitly rather than staying silent, because silence is what a system +that does not speak this looks like -- and that is exactly the state these +were in. + +It is a separate field rather than a fourth value of the one that says what is +in the directory. That one is read by everything, and overloading it would +make every existing reader wrong about a target that is merely mid-operation, +which is the failure being closed rather than a new place to put it. + +**And the reason this was not catchable.** The kit these systems are built +against declares the shape of one answer and no others. The response this +defect lived in has no schema at all, and the case file names none of its +thirty-three fields. Both sides invented it independently; it works because +the names happened to agree, and where they did not, nothing failed. + +So there is now a record of what that answer actually contains, taken by +running every one of the seven against a directory that is empty, one holding +somebody else's files, and one this provider manages. Checked both ways: a +field published and not recorded is one the far side cannot know about, and a +field recorded and no longer published is a promise that stopped being kept. +It does not make the contract -- that belongs to the tool these are built for +-- but it makes the published set a fact with a reader, so the next field is a +difference somebody sees rather than a discovery months later. + +**Closed before this release published.** The consumer turned that measured +shape into provider-kit `0.2.7`: a checksummed closed status schema and a +conformance case carrying the required, verified and enum sets. These trees +vendor it in the same release. The local status record remains only as a +derived readable projection, so there is one contract rather than the two this +section was written about. + +**One more name a target can carry that this provider does not own.** Reported +beside a target still described as clean, for one system whose product reads a +second spelling of its settings file and both spellings of every component +directory. Clean is a statement about the bytes this provider wrote and never +was a statement about what the product obeys. + +**A posture stopped emptying directories it could never fill.** Selecting a +setup replaces everything this provider owns, which is what makes switching +between them predictable -- and it was doing that to places nothing here could +ever put anything: no component routes to them and no posture ships files +there. Every posture agreed they were empty, so the emptiness was not a +statement any of them made, and the only thing ever in such a directory was +somebody else's. Twelve of them, across five of these systems; one held a +person's key bindings and a plain switch of posture took them. + +They are still owned, which is the point. A backup captures them, the recorded +identity notices when they change, and removing the setup takes them -- +returning a directory to unmanaged is a different act from changing posture +inside it. Only the emptying stopped. + +**And the list of things this provider promises never to touch now means it.** +It named three effects of ownership and prevented two: those paths were not +copied into a backup and not folded into the recorded identity. The third went +through, because replacing a directory removed it whole and never asked. One +product writes a person's marketplace sources inside a directory this provider +owns, and a change of posture took the file. + +**One system stopped installing a program that cannot start.** It was installed +from a package whose entry point is a script needing an interpreter the host +supplies, so on a machine without a recent enough one the install succeeded and +the program could not run, with nothing in between saying so. The vendor also +publishes complete standalone builds for every platform declared here, and +those carry their own runtime: the whole lifecycle was exercised with that +interpreter deliberately unreachable. Two different layouts inside one release, +read from the archives rather than from the platform names. + +**Two files that shape what the model is told** are now owned by that same +system. Its own documentation names both, and this record had neither -- not +owned, not declined, absent, which is the one state a surface must not be in. +They are kept rather than replaced, for the reason above. + +**And every one of these products was asked whether it can be told to stop +updating itself.** Three can, and one of those had been told to since an +earlier release without the answer ever being written down where anything +checks it. Four cannot, and that is now recorded as a measured absence rather +than as an empty field, which reads the same and means something else. + +**And one system's record was re-asked at the version it pins.** Every row of +it described a release eight versions back, of a product that moved those eight +in a few hours. Re-measured against the pinned bytes rather than re-read: seven +surfaces hold and now say which release they are about, and the one covering +plugins gains the shape it was missing -- a plugin there is a directory holding +a manifest, and a single file with the right name is silently nothing. + +That correction went the other way first. The probe that found "nothing loads +here" was itself malformed, and the control is what said so; without it a +working namespace would have been changed because an instrument answered about +itself. The vendor's own installer also writes a pair this record had no row +for -- an installed-plugin directory and the registry that lists it, useless +without each other -- and that pair is now named as something never read, +captured or removed, alongside the credentials. + +**The development setup no longer downgrades the development agent.** +`nddev-builder` used to start from each product's conservative baseline, so +installing the authoring toolkit brought approval prompts and sandboxes back on +the very path meant for autonomous implementation. It now starts from that +product's own `full-auto` posture and adds knowledge without changing authority. + +**Every software lifecycle now has two real releases to cross.** Cursor and +Antigravity were the last two with no previous pin. Their immediately preceding +artifacts had already been measured in this repository before the pins moved; +the immutable identifiers still resolve, and the missing Windows bytes were +fetched and hashed before the records were completed. Cursor's no-launch +declaration also found a flaw in the evidence script: it tried to prove an +inactive removal by calling a command the provider intentionally omits. The +exposure is checked without inventing launch now, and the full two-release +sequence passes against both products on native Linux x86_64. + +**Vendor-byte evidence names all six native hosts.** Exact hosted labels cover +Linux, Windows and macOS on x86_64 and arm64, and each job asserts the runner +architecture before it builds. The previous matrix covered three operating +systems and only half of their architecture pairs, while the release already +published all six provider binaries. + +**And the estate table reads launch from the declaration that decides it.** It +used to infer from software plus an environment-variable name, which called +Cursor launchable after its own `LaunchBinding::Partial` had withdrawn the +capability. The generated README and SUPPORT pages now say five, not six, and +name Cursor and Antigravity separately. + ## [0.0.45] - 2026-08-31 A posture that said it changed nothing was changing the thing it named, and diff --git a/Cargo.lock b/Cargo.lock index 7560369..8d373df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,7 +66,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "harness-runtime" -version = "0.0.45" +version = "0.0.47" dependencies = [ "provider-v3", "serde", @@ -128,7 +128,7 @@ dependencies = [ [[package]] name = "opencode-setup-system" -version = "0.0.45" +version = "0.0.47" dependencies = [ "harness-runtime", "provider-v3", @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "provider-v3" -version = "0.0.45" +version = "0.0.47" dependencies = [ "serde", "serde_json", @@ -209,7 +209,7 @@ dependencies = [ [[package]] name = "setup-core" -version = "0.0.45" +version = "0.0.47" dependencies = [ "miniz_oxide", "serde", diff --git a/Cargo.toml b/Cargo.toml index 67ef0e2..8bcdb28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.0.45" +version = "0.0.47" 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.45" } -provider-v3 = { path = "crates/provider-v3", version = "0.0.45" } -harness-runtime = { path = "crates/harness-runtime", version = "0.0.45" } +setup-core = { path = "crates/setup-core", version = "0.0.47" } +provider-v3 = { path = "crates/provider-v3", version = "0.0.47" } +harness-runtime = { path = "crates/harness-runtime", version = "0.0.47" } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/README.md b/README.md index ed252b7..1ff9329 100644 --- a/README.md +++ b/README.md @@ -40,20 +40,22 @@ to a configuration home: a change aimed at a guessed path is a change aimed at someone else's state. The documented home is printed by `--help` so it can be copied, not resolved. -## Three postures +## Four setups -`list` names every setup this build carries. Three of them mean the same thing -on all seven setup systems, so what you learn here you know there: +`list` names every setup this build carries. All four mean the same thing on all +seven setup systems, expressed in each product's own format: | | | | --- | --- | | `baseline` | a working floor: instructions plus a conservative configuration | | `minimal` | the product's own defaults, and the state a restore proves it can reach | | `full-auto` | nothing asked and nothing sandboxed, in this product's own keys | +| `nddev-builder` | the full-auto posture plus the product-native NDDev authoring toolkit | `full-auto` is a **setup posture** — keys in a configuration file this product -reads. It is not an execution profile and it grants no environment: what it -changes is what the product asks *you*. +reads. `nddev-builder` keeps that posture and adds authoring knowledge; selecting +the toolkit must not silently return development to an approval loop. Neither is +an external execution profile and neither grants environment by itself. **A backup is captured before every change**, so `restore` always has something to return to. `restore` with no reference means the most recent backup that @@ -177,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.45 \ + ghcr.io/nddev-opennetwork/opencode-setup-system:0.0.47 \ status --target /config/ --json ``` diff --git a/crates/harness-runtime/src/facts.rs b/crates/harness-runtime/src/facts.rs index ba730cb..50fc57a 100644 --- a/crates/harness-runtime/src/facts.rs +++ b/crates/harness-runtime/src/facts.rs @@ -141,6 +141,34 @@ pub struct Harness { /// product whose alternate spellings nobody has measured belongs here as /// nothing rather than as a guess. pub shadowing_names: &'static [Shadow], + + /// Owned namespaces this provider can never put content into. + /// + /// Selecting a setup is exact state: every owned namespace is emptied and + /// then filled from the payload, so a posture that ships nothing for a + /// namespace is saying *there is nothing here*. That is right wherever the + /// provider could have put something -- a component kind routes there, or a + /// setup in this build's catalogue carries files there -- because then the + /// emptiness is a statement the posture is entitled to make. + /// + /// **These are the namespaces where it is not entitled to.** Nothing routes + /// to them and no setup fills them, so every posture agrees there is + /// nothing, and the only content they ever hold is somebody else's. + /// Emptying them imputes an opinion no setup has. + /// + /// Measured 2026-08-31 across the seven: twelve of them, in five harnesses. + /// A person's keybindings under one, a plugin directory under another, and + /// a `select minimal` took both. + /// + /// They stay **owned**, which is the point: a backup still captures them, + /// the identity still hashes them so drift is visible, and `remove` still + /// takes them -- returning the target to unmanaged is a different statement + /// from switching posture within it. Only the emptying-on-selection stops. + /// + /// `no_custody_namespace_could_ever_be_filled` checks both directions + /// against the baseline and the embedded catalogue, so this list cannot + /// drift from the two facts that define it. + pub custody_namespaces: &'static [&'static str], /// Product-owned paths this provider never reads and never writes. /// /// Excluded from backups so a slot never holds credentials, and excluded @@ -789,6 +817,7 @@ mod tests { profile_id: "sample/native-files/1", native_namespaces: &["AGENTS.md", "settings.json", "skills"], shadowing_names: &[], + custody_namespaces: &[], never_touch: &[".credentials.json", "sessions"], foreign_homes: &[], permission_profiles: &["default"], diff --git a/crates/harness-runtime/src/surfaces.rs b/crates/harness-runtime/src/surfaces.rs index d3e43b1..a6fa1b2 100644 --- a/crates/harness-runtime/src/surfaces.rs +++ b/crates/harness-runtime/src/surfaces.rs @@ -512,11 +512,6 @@ fn the_home_and_what_moves_it( } } -/// Every way a declaration and its baseline can disagree, in a stable order. -/// -/// Empty is the only passing answer. Each string names one disagreement and is -/// written to be read on its own, because a test failure shows the list and -/// nothing else. /// One owned surface, after its row has been checked. struct Owned<'a> { path: &'a str, @@ -1101,10 +1096,11 @@ fn evidence_is_recorded(harness: &Harness, baseline: &Value, found: &mut Vec) { + let Some(rows) = baseline + .get(BLOCK) + .and_then(|block| block.get("surfaces")) + .and_then(Value::as_array) + else { + return; + }; + // A build with no catalogue cannot answer what is fillable: every namespace + // would read as unfillable because no setup exists to fill it. Test + // fixtures are the only builds in that state, and a guard that reports on + // one is reporting about the fixture rather than about a harness. + if harness.embedded_setups.is_empty() { + return; + } + let under = |namespace: &str, relative: &str| { + relative == namespace + || relative + .strip_prefix(namespace) + .is_some_and(|rest| rest.starts_with('/')) + }; + for namespace in harness.native_namespaces { + let routes = rows.iter().any(|row| { + row.get("path").and_then(Value::as_str) == Some(*namespace) + && row + .get("kinds") + .and_then(Value::as_array) + .is_some_and(|kinds| !kinds.is_empty()) + }); + let filled = harness.embedded_setups.iter().any(|(embedded, _)| { + embedded + .split_once("/home/") + .is_some_and(|(_, relative)| under(namespace, relative)) + }); + let listed = harness.custody_namespaces.contains(namespace); + if listed && (routes || filled) { + found.push(format!( + "{namespace:?} is declared as custody and something can fill it: \ + {}. A posture is entitled to say a fillable namespace is empty.", + if routes { + "a kind routes there" + } else { + "a setup carries files there" + } + )); + } + if !listed && !routes && !filled { + found.push(format!( + "{namespace:?} is owned, routes no kind and no setup fills it, and is \ + not declared as custody -- so selecting any posture empties it, and \ + the only thing ever in it is somebody else's" + )); + } + } + for namespace in harness.custody_namespaces { + if !harness.native_namespaces.contains(namespace) { + found.push(format!( + "{namespace:?} is declared as custody and is not owned at all" + )); + } + } +} + +/// Whether the update switch a launch forces is one the baseline measured. +/// +/// `updates_off_env` puts a literal into every provider-managed launch. A name +/// one letter from the real one -- `DISABLE_AUTOUPDATER` where the product +/// reads `DISABLE_AUTOUPDATE`, or the reverse -- looks right, changes nothing, +/// and leaves the product free to replace bytes this provider pinned, recorded +/// the digest of, and offers a rollback beside. +/// +/// The list it is checked against exists to hold what was read out of a pinned +/// artifact. It had no reader until 2026-08-31, which is the condition a stale +/// fact needs: the `windows` host row sat under `unsupported` for weeks while +/// the provider installed Windows, for exactly that reason. +fn the_switch_it_sets_was_measured(harness: &Harness, baseline: &Value, found: &mut Vec) { + if harness.updates_off_env.is_empty() { + return; + } + let measured = baseline + .get("source_verified_runtime_flags") + .and_then(Value::as_array) + .map(|flags| { + flags + .iter() + .filter_map(Value::as_str) + .any(|flag| flag == harness.updates_off_env) + }); + match measured { + Some(true) => {} + Some(false) => found.push(format!( + "launch sets {} and the baseline's measured runtime flags do not name it", + harness.updates_off_env + )), + None => found.push(format!( + "launch sets {} and this baseline records no source-verified runtime flags at all", + harness.updates_off_env + )), + } +} /// Every way a declaration and its baseline can disagree, in a stable order. /// @@ -1148,6 +1262,8 @@ pub fn disagreements(harness: &Harness, baseline: &Value) -> Vec { owned_paths_fold_together(harness, &mut found); silent_about_routing_nothing(harness, baseline, &mut found); writes_where_nothing_is_routed(harness, baseline, &mut found); + custody_is_what_nothing_can_fill(harness, baseline, &mut found); + the_switch_it_sets_was_measured(harness, baseline, &mut found); evidence_is_recorded(harness, baseline, &mut found); an_absence_is_not_recorded_beside_the_thing(baseline, &mut found); diff --git a/crates/harness-runtime/src/wire.rs b/crates/harness-runtime/src/wire.rs index 364801a..34e46a2 100644 --- a/crates/harness-runtime/src/wire.rs +++ b/crates/harness-runtime/src/wire.rs @@ -18,7 +18,7 @@ use std::collections::BTreeMap; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::time::SystemTime; use provider_v3::argv::{Bundle as ArgvBundle, Invocation, PlanRequest}; @@ -200,6 +200,35 @@ fn observe(harness: &Harness, target: &Path) -> Result<(Target, std::path::PathB Ok((resolved, control, pool)) } +/// What an unsettled operation owes, in the vocabulary the consumer reads. +/// +/// `state` answers *what is in this directory* and has three values that +/// say nothing about the last operation. The journal has always carried +/// that, and `status` has always published it -- under `journal`, a key the +/// consumer never reads. Measured 2026-08-31 against `ai-stp-cli 0.0.10`: +/// both of its recovery paths gate on `state == "recovery_required"` or on +/// `cleanup_state`, and a target of ours holding a `prepared` journal +/// answers `managed` with neither. So the fact was in the answer, under a +/// name the reader does not know, and the recovery it exists to trigger +/// could not fire against any of the seven. +/// +/// A separate key rather than a fourth `state`: `state` is read by +/// everything and means the directory, and overloading it would make every +/// existing reader wrong about a target that is merely mid-operation. +fn cleanup_owed(journal: Option<&Journal>) -> &'static str { + match journal.map(|entry| entry.phase) { + // The effect may be partial and `recover-operation` restores the + // pre-operation target. Something is owed before this target is read + // as anything. + Some(Phase::Prepared) => "required", + // The effect is complete; recovery clears the tail only. + Some(Phase::Committed) => "pending", + // Said rather than omitted. An absent key is what a provider that does + // not speak this looks like, and that is the state this one was in. + None => "none", + } +} + /// Names the product reads that this provider does not own, and that are here. /// /// `state: "managed"` and a clean `target_digest` are statements about the @@ -315,10 +344,13 @@ fn status(harness: &Harness, target: &Path) -> Result { // that disagreement into an answer the consumer trusts. let shadowed = shadowed_here(harness, resolved.root()); + let cleanup_state = cleanup_owed(journal.as_ref()); + let mut answer = serde_json::Map::new(); answer.extend(flat); for (key, value) in [ ("shadowed_by", serde_json::json!(shadowed)), + ("cleanup_state", serde_json::json!(cleanup_state)), ("state", serde_json::json!(state)), ("target_digest", serde_json::json!(identity)), ( @@ -1248,8 +1280,22 @@ fn replace_managed_from( let mut written = Vec::new(); for namespace in harness.native_namespaces { let destination = target.root().join(namespace); - remove_path(&destination)?; let source = payload.join(namespace); + // **A posture may assert emptiness only where it could have put + // something.** Exact state empties every owned namespace and refills it + // from the payload, which is what makes switching posture + // deterministic. For a namespace no kind routes to and no setup fills, + // every posture agrees there is nothing -- so the emptiness is not a + // statement any of them made, and the only content there is somebody + // else's. Measured: a `select minimal` took a person's keybindings. + // + // Still owned, and the payload branch below still runs: a backup + // captures it, the identity hashes it, and `remove` takes it. Switching + // posture and returning a target to unmanaged are different statements. + if harness.custody_namespaces.contains(namespace) && !source.exists() { + continue; + } + remove_keeping(&destination, target.root(), harness.never_touch)?; if !source.exists() { continue; } @@ -1629,6 +1675,48 @@ fn remove_managed( Ok(()) } +/// Remove `path`, keeping anything the harness promised never to touch. +/// +/// `never_touch` named three effects of ownership and protected two of them: a +/// backup does not capture these paths and an identity does not hash them. +/// The third -- deletion -- went straight through, because replacement removes +/// a namespace whole and never asked. The name promised more than it did. +/// +/// It matters where a product writes its own record inside a directory this +/// provider owns: grok's `plugins/known_marketplaces.json` is a person's +/// marketplace sources, and a posture switch took it. Measured 2026-08-31 with +/// the released binary. +/// +/// Only paths *under* `path` are considered, and each is spared in place: the +/// directory that holds one survives with that file in it and nothing else, +/// which is what preserving a sibling means. +fn remove_keeping(path: &Path, root: &Path, spared: &[&str]) -> Result<()> { + let keep: Vec = spared.iter().map(|name| root.join(name)).collect(); + if !keep.iter().any(|held| held.starts_with(path)) { + return remove_path(path); + } + let Ok(metadata) = fs::symlink_metadata(path) else { + return Ok(()); + }; + if !metadata.is_dir() { + // A file that is itself spared, or one nothing spares. + return if keep.iter().any(|held| held == path) { + Ok(()) + } else { + remove_path(path) + }; + } + let Ok(entries) = fs::read_dir(path) else { + return Ok(()); + }; + for entry in entries.flatten() { + remove_keeping(&entry.path(), root, spared)?; + } + // Gone when the last thing in it went; kept when something is still held. + let _ = fs::remove_dir(path); + Ok(()) +} + fn remove_path(path: &Path) -> Result<()> { let Ok(metadata) = fs::symlink_metadata(path) else { return Ok(()); @@ -2001,6 +2089,7 @@ pub(crate) mod tests_support { profile_id: "test/native-files/1", native_namespaces: &["AGENTS.md", "settings.json", "skills"], shadowing_names: &[], + custody_namespaces: &[], never_touch: &[".credentials.json", "sessions"], foreign_homes: &[], permission_profiles: &["default"], @@ -2850,6 +2939,45 @@ mod tests { ); } + #[test] + fn replacement_spares_a_never_touch_path_inside_a_namespace_it_empties() { + const SPARES: Harness = Harness { + never_touch: &["skills/their-record.json"], + ..TEST + }; + // `never_touch` named three effects of ownership and stopped two: a + // backup does not capture these and an identity does not hash them. + // Replacement removed a namespace whole and never asked, so the third + // went through. Measured with the released grok binary: a posture + // switch took `plugins/known_marketplaces.json`, which the product's + // own `plugin marketplace` command writes for a person. + let target = seeded("spared"); + let inside = target.join("skills").join("their-record.json"); + fs::write(&inside, b"a person's own file, inside a namespace we own").unwrap(); + let beside = target.join("skills").join("nothing-spares-this.md"); + fs::write(&beside, b"an ordinary sibling").unwrap(); + + // The control first: with nothing spared, replacement takes both. + let payload = scratch("spared-payload").join("target"); + fs::create_dir_all(payload.join("skills")).unwrap(); + fs::write(payload.join("skills").join("ours.md"), b"ours").unwrap(); + let resolved = Target::resolve(&target, TEST.control_directory).unwrap(); + replace_managed_from(&TEST, &resolved, &payload, None).unwrap(); + assert!(!inside.exists(), "nothing spared it and it survived"); + assert!(!beside.exists()); + + // And with it named, exactly it survives. + fs::write(&inside, b"a person's own file, inside a namespace we own").unwrap(); + fs::write(&beside, b"an ordinary sibling").unwrap(); + replace_managed_from(&SPARES, &resolved, &payload, None).unwrap(); + assert!(inside.exists(), "the named path was taken anyway"); + assert!(!beside.exists(), "a sibling nothing names survived"); + assert!( + target.join("skills").join("ours.md").exists(), + "our own payload did not land beside it" + ); + } + #[test] fn status_names_a_file_the_product_reads_and_this_provider_does_not_own() { // `state` and `target_digest` are statements about the bytes this @@ -4021,6 +4149,74 @@ mod tests { assert_eq!(error.reason(), Some(WireReason::RecoveryRequired)); } + #[test] + fn an_unsettled_operation_is_named_in_the_key_the_consumer_reads() { + // `status` has published the journal since the beginning, under + // `journal`. Measured 2026-08-31 against `ai-stp-cli 0.0.10`: it never + // reads that key. Both of its recovery paths gate on + // `state == "recovery_required"` or on `cleanup_state`, and a target + // holding a prepared journal answered `managed` with neither -- so the + // fact was in the answer, under a name the reader does not know, and + // the recovery it exists to trigger could not fire. + // + // This asserts the consumer's own condition rather than our field, so + // it fails if the value stops satisfying the thing that reads it. + fn recovery_fires(answer: &serde_json::Value) -> bool { + answer["state"] == "recovery_required" + || matches!( + answer["cleanup_state"].as_str(), + Some("pending" | "required" | "in_progress") + ) + } + + let target = seeded("cleanup-state"); + plan_then_apply(&target, "backup", &[]); + + // The control, and the half that matters: a settled target must not + // ask for recovery. A field that always fires sends every caller into + // a restore it does not need. + let settled = run(args("status", &target, &[])); + assert_eq!(settled["cleanup_state"], "none"); + assert!( + !recovery_fires(&settled), + "a settled target asked for recovery" + ); + + let control = target.join(TEST.control_directory); + let entry = |phase| Journal { + schema_version: JOURNAL_SCHEMA, + phase, + operation_id: "operation_01INTERRUPTED".to_owned(), + operation: "restore".to_owned(), + plan_digest: RELEASE.to_owned(), + target_precondition_digest: RELEASE.to_owned(), + backup_ref: None, + target_scope: None, + }; + + // Prepared: the effect may be partial and a restore is owed. + entry(Phase::Prepared).publish_prepared(&control).unwrap(); + let interrupted = run(args("status", &target, &[])); + assert_eq!(interrupted["cleanup_state"], "required"); + assert_eq!( + interrupted["state"], "managed", + "state still describes the directory" + ); + assert!(recovery_fires(&interrupted)); + + // Committed: the effect landed and only the tail is left. Promoted + // rather than written, because `publish_prepared` sets the phase + // itself -- the first draft of this test wrote `Phase::Committed` into + // it and got `required` back, which is the API refusing to let a test + // fake a phase the program never reaches that way. + entry(Phase::Prepared) + .promote_to_committed(&control) + .unwrap(); + let tail = run(args("status", &target, &[])); + assert_eq!(tail["cleanup_state"], "pending"); + assert!(recovery_fires(&tail)); + } + #[test] fn recovery_from_prepared_returns_the_exact_pre_operation_target() { let target = seeded("recover"); diff --git a/crates/opencode-setup-system/src/main.rs b/crates/opencode-setup-system/src/main.rs index f0ad71c..15a4c7c 100644 --- a/crates/opencode-setup-system/src/main.rs +++ b/crates/opencode-setup-system/src/main.rs @@ -109,6 +109,12 @@ pub const OPENCODE: Harness = Harness { ], // The product's own: credentials and runtime caches. Never read, never // written, and never copied into a backup slot. + // Owned, and nothing this build can install ever lands here: no + // component kind routes to them and no setup in this catalogue + // carries files there. So a posture selecting itself must not empty + // them -- every posture agrees there is nothing, which makes the + // emptiness a statement none of them made. + custody_namespaces: &["tui.json"], never_touch: &["auth.json", "cache"], // No near neighbour measured for this product. A marker listed here is a // refusal waiting to happen, so nothing is listed without evidence. @@ -253,41 +259,6 @@ mod tests { ); } - /// The switch this provider sets at launch is one the baseline measured. - /// - /// `source_verified_runtime_flags` was a list of four environment names - /// that nothing in this repository read. That is the exact condition the - /// `windows` host row was in while it said `unsupported` and this provider - /// installed Windows: true when written, with no reader, and therefore no - /// way to notice when it stopped being true. Setting `updates_off_env` to a - /// name absent from that list would mean the launch environment carries a - /// literal nobody measured in the product. - #[test] - fn native_declaration_names_the_switch_it_sets() { - if OPENCODE.updates_off_env.is_empty() { - return; - } - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../references") - .join(format!("{TOOL}-baseline.json")); - let baseline: serde_json::Value = - serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap(); - let measured: Vec<&str> = match baseline["source_verified_runtime_flags"].as_array() { - Some(flags) => flags.iter().filter_map(|flag| flag.as_str()).collect(), - None => panic!( - "launch sets {} and {TOOL}-baseline.json records no \ - source-verified runtime flags at all", - OPENCODE.updates_off_env, - ), - }; - assert!( - measured.contains(&OPENCODE.updates_off_env), - "launch sets {} and {TOOL}-baseline.json does not record measuring \ - it in the product; the measured set is {measured:?}", - OPENCODE.updates_off_env, - ); - } - #[test] fn the_catalog_this_binary_carries_is_the_one_in_the_tree() { let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); diff --git a/install.ps1 b/install.ps1 index 455b257..de15b19 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.45", + [string]$Version = "0.0.47", [string]$InstallDir = "$env:LOCALAPPDATA\Programs\opencode-setup-system" ) $ErrorActionPreference = "Stop" diff --git a/install.sh b/install.sh index 3f95a32..507d8c6 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.45}" +VERSION="${1:-0.0.47}" PREFIX="${OPENCODE_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 da01ce5..4cd55c0 100644 --- a/provider-kit/v3/KIT-IDENTITY.json +++ b/provider-kit/v3/KIT-IDENTITY.json @@ -1,11 +1,12 @@ { - "aggregate_digest": "sha256:938918b137b4433f7d6b1c2d9da1823fc0c1e05b8a422b32ce721bb16c04fcd2", + "aggregate_digest": "sha256:9dd94310e656459294d4b80e3bb15ce30e98e4f45ca348ac99cda22b90cce9f0", "files": [ "conformance-cases.json", "manifest.json", - "provider-info.schema.json" + "provider-info.schema.json", + "status-response.schema.json" ], - "kit_version": "0.2.6", + "kit_version": "0.2.7", "protocol_version": 3, "schema": "ai-stp-provider-kit-identity/1" } diff --git a/provider-kit/v3/SHA256SUMS b/provider-kit/v3/SHA256SUMS index 9107b50..1277ca3 100644 --- a/provider-kit/v3/SHA256SUMS +++ b/provider-kit/v3/SHA256SUMS @@ -1,3 +1,4 @@ -999e8edaa86a3138e3c7c3a2e354e54c171d2a97194184d275593310c3c626ab conformance-cases.json -d76b80a99275b998f3705ff2f5710cda76519290ce70ba9b41b8c67b4e9fff91 manifest.json +fe04d03b15cfe8d5b61835eafd3ea788074684e2be69d419f9c22a37ea0461b7 conformance-cases.json +2d37b51636c29f3976ba83cd296befa67217790cc141bf18037146e2f8148059 manifest.json 6b0e5f02c6ebc1503f4d6a1aa39b93df58cfd9d92037803244fe260bdb9235c4 provider-info.schema.json +2c5e01df3b02369832b74842b128c42b6b84d1e1455db3800474e54e71b645e4 status-response.schema.json diff --git a/provider-kit/v3/conformance-cases.json b/provider-kit/v3/conformance-cases.json index 400c0b7..efbf60a 100644 --- a/provider-kit/v3/conformance-cases.json +++ b/provider-kit/v3/conformance-cases.json @@ -95,5 +95,55 @@ "status", "validate-bundle" ], - "schema": "ai-stp-provider-conformance-cases/1" + "schema": "ai-stp-provider-conformance-cases/1", + "status_response": { + "cleanup_state_values": [ + "none", + "pending", + "required" + ], + "required_fields": [ + "backups", + "canonical_target", + "cleanup_state", + "harness_id", + "journal", + "protocol_version", + "provider_id", + "provider_state", + "shadowed_by", + "state", + "target_digest", + "target_identity_digest" + ], + "schema": "status-response.schema.json", + "state_values": [ + "managed", + "unmanaged", + "missing" + ], + "verified_fields": [ + "state_schema", + "provider_version", + "provider_build_digest", + "provider_release_digest", + "setup_stable_id", + "setup_version", + "setup_version_passport_digest", + "setup_definition_digest", + "component_refs", + "bundle_format", + "bundle_digest", + "artifact_digest", + "projection_profile_digest", + "provider_plan_digest", + "operation_id", + "target_precondition_digest", + "native_ownership", + "written_paths", + "backup_ref", + "previous_verified_identity", + "drift_state" + ] + } } diff --git a/provider-kit/v3/manifest.json b/provider-kit/v3/manifest.json index 3a90e5e..1c37c0e 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.6", + "kit_version": "0.2.7", "operation_network": { "backup": [ { @@ -188,6 +188,7 @@ ], "requirements": "specs/active/SPEC-008-provider-installation.md", "schema": "ai-stp-provider-conformance-kit/1", + "status_response_schema": "status-response.schema.json", "unsupported_reasons": [ "unsupported_operation", "unsupported_component_kind", diff --git a/provider-kit/v3/status-response.schema.json b/provider-kit/v3/status-response.schema.json new file mode 100644 index 0000000..f28f384 --- /dev/null +++ b/provider-kit/v3/status-response.schema.json @@ -0,0 +1,540 @@ +{ + "$defs": { + "digest": { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + } + }, + "$id": "https://nddev.asia/schemas/provider-protocol/v3/status-response.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "provider_state": { + "properties": { + "drift_state": { + "const": "clean" + }, + "present": { + "const": true + }, + "readable": { + "const": true + } + }, + "required": [ + "present", + "readable", + "drift_state" + ], + "type": "object" + } + }, + "required": [ + "provider_state" + ] + }, + "then": { + "required": [ + "state_schema", + "provider_version", + "provider_build_digest", + "provider_release_digest", + "setup_stable_id", + "setup_version", + "setup_version_passport_digest", + "setup_definition_digest", + "component_refs", + "bundle_format", + "bundle_digest", + "artifact_digest", + "projection_profile_digest", + "provider_plan_digest", + "operation_id", + "target_precondition_digest", + "native_ownership", + "written_paths", + "backup_ref", + "previous_verified_identity", + "drift_state" + ] + } + } + ], + "properties": { + "artifact_digest": { + "oneOf": [ + { + "$ref": "#/$defs/digest" + }, + { + "type": "null" + } + ] + }, + "backup_ref": { + "oneOf": [ + { + "pattern": "^slot-[0-9]{12}$", + "type": "string" + }, + { + "type": "null" + } + ] + }, + "backups": { + "items": { + "additionalProperties": false, + "allOf": [ + { + "else": { + "properties": { + "hold_reason": { + "type": "null" + } + } + }, + "if": { + "properties": { + "held": { + "const": true + } + }, + "required": [ + "held" + ] + }, + "then": { + "properties": { + "hold_reason": { + "minLength": 1, + "type": "string" + } + } + } + } + ], + "properties": { + "backup_ref": { + "pattern": "^slot-[0-9]{12}$", + "type": "string" + }, + "held": { + "type": "boolean" + }, + "hold_reason": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "operation": { + "minLength": 1, + "type": "string" + }, + "setup_id": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "backup_ref", + "operation", + "setup_id", + "held", + "hold_reason" + ], + "type": "object" + }, + "type": "array" + }, + "bundle_digest": { + "oneOf": [ + { + "$ref": "#/$defs/digest" + }, + { + "type": "null" + } + ] + }, + "bundle_format": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "canonical_target": { + "minLength": 1, + "type": "string" + }, + "cleanup_state": { + "enum": [ + "none", + "pending", + "required" + ], + "type": "string" + }, + "component_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "drift_state": { + "enum": [ + "clean", + "local_drift", + "unknown" + ], + "type": "string" + }, + "harness_id": { + "minLength": 1, + "type": "string" + }, + "journal": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "operation": { + "minLength": 1, + "type": "string" + }, + "operation_id": { + "minLength": 1, + "type": "string" + }, + "phase": { + "enum": [ + "prepared", + "committed" + ], + "type": "string" + } + }, + "required": [ + "phase", + "operation", + "operation_id" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "native_ownership": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "operation_id": { + "minLength": 1, + "type": "string" + }, + "previous_verified_identity": { + "oneOf": [ + { + "$ref": "#/$defs/digest" + }, + { + "type": "null" + } + ] + }, + "projection_profile_digest": { + "oneOf": [ + { + "$ref": "#/$defs/digest" + }, + { + "type": "null" + } + ] + }, + "protocol_version": { + "const": 3 + }, + "provider_build_digest": { + "$ref": "#/$defs/digest" + }, + "provider_id": { + "minLength": 1, + "type": "string" + }, + "provider_plan_digest": { + "oneOf": [ + { + "$ref": "#/$defs/digest" + }, + { + "type": "null" + } + ] + }, + "provider_release_digest": { + "oneOf": [ + { + "$ref": "#/$defs/digest" + }, + { + "type": "null" + } + ] + }, + "provider_state": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "present": { + "const": false + } + }, + "required": [ + "present" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "detail": { + "minLength": 1, + "type": "string" + }, + "found_schema": { + "minimum": 0, + "type": "integer" + }, + "present": { + "const": true + }, + "readable": { + "const": false + } + }, + "required": [ + "present", + "readable", + "found_schema", + "detail" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "backup_ref": { + "oneOf": [ + { + "pattern": "^slot-[0-9]{12}$", + "type": "string" + }, + { + "type": "null" + } + ] + }, + "drift_state": { + "enum": [ + "clean", + "local_drift", + "unknown" + ], + "type": "string" + }, + "operation_id": { + "minLength": 1, + "type": "string" + }, + "present": { + "const": true + }, + "readable": { + "const": true + }, + "recorded_identity": { + "$ref": "#/$defs/digest" + }, + "setup_stable_id": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "setup_version": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "present", + "readable", + "setup_stable_id", + "setup_version", + "operation_id", + "backup_ref", + "recorded_identity", + "drift_state" + ], + "type": "object" + } + ] + }, + "provider_version": { + "minLength": 1, + "type": "string" + }, + "setup_definition_digest": { + "oneOf": [ + { + "$ref": "#/$defs/digest" + }, + { + "type": "null" + } + ] + }, + "setup_stable_id": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "setup_version": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "setup_version_passport_digest": { + "oneOf": [ + { + "$ref": "#/$defs/digest" + }, + { + "type": "null" + } + ] + }, + "shadowed_by": { + "items": { + "additionalProperties": false, + "properties": { + "effect": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "over": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "over", + "effect" + ], + "type": "object" + }, + "type": "array" + }, + "state": { + "enum": [ + "managed", + "unmanaged", + "missing" + ], + "type": "string" + }, + "state_schema": { + "minimum": 1, + "type": "integer" + }, + "target_digest": { + "$ref": "#/$defs/digest" + }, + "target_identity_digest": { + "$ref": "#/$defs/digest" + }, + "target_precondition_digest": { + "$ref": "#/$defs/digest" + }, + "written_paths": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "backups", + "canonical_target", + "cleanup_state", + "harness_id", + "journal", + "protocol_version", + "provider_id", + "provider_state", + "shadowed_by", + "state", + "target_digest", + "target_identity_digest" + ], + "type": "object" +} diff --git a/references/opencode-baseline.json b/references/opencode-baseline.json index 4f820c3..0355797 100644 --- a/references/opencode-baseline.json +++ b/references/opencode-baseline.json @@ -43,7 +43,8 @@ "opencode-darwin-x64": "identical", "opencode-windows-x64": "signature-only" }, - "x64_baseline_builds_note": "The vendor publishes a `-baseline` package beside each x64 build, for CPUs without AVX2, and its own launcher tries them as a fallback list and keeps whichever answers `--version`. This provider selects one package per platform and never falls back, so a review filed every non-AVX2 x64 host as incorrectly covered. Measured 2026-08-31 against the pinned 1.18.25 packages, by comparing the program inside each pair rather than the tarballs: there is no second build. Linux and macOS are byte-identical to their `-baseline` siblings, and the Windows pair differs in 1694 bytes of 179MB -- an Authenticode countersignature three seconds later and the header fields signing rewrites -- with the same size and the same AVX2 instruction counts. So the finding is empty at this pin, and empty is a property of this release rather than of the design: `refresh_software_pins.py` now compares the pair at every refresh and says so when they stop matching, which is the point at which the host scope would need a CPU dimension. The distinction the vendor keeps the names for is the fallback list, not a second program." + "x64_baseline_builds_note": "The vendor publishes a `-baseline` package beside each x64 build, for CPUs without AVX2, and its own launcher tries them as a fallback list and keeps whichever answers `--version`. This provider selects one package per platform and never falls back, so a review filed every non-AVX2 x64 host as incorrectly covered. Measured 2026-08-31 against the pinned 1.18.25 packages, by comparing the program inside each pair rather than the tarballs: there is no second build. Linux and macOS are byte-identical to their `-baseline` siblings, and the Windows pair differs in 1694 bytes of 179MB -- an Authenticode countersignature three seconds later and the header fields signing rewrites -- with the same size and the same AVX2 instruction counts. So the finding is empty at this pin, and empty is a property of this release rather than of the design: `refresh_software_pins.py` now compares the pair at every refresh and says so when they stop matching, which is the point at which the host scope would need a CPU dimension. The distinction the vendor keeps the names for is the fallback list, not a second program.", + "musl_precondition": "**The vendor publishes musl builds and this provider declares `linux-musl` unsupported, and the reason is not the vendor's.** Measured 2026-08-31: `opencode-linux-x64-musl` and `opencode-linux-arm64-musl` exist and differ from their glibc siblings in bytes, so they are real second builds rather than names -- unlike the `-baseline` pairs recorded above.\n\nWhat blocks the row is this provider's own release matrix: six targets, `{x86_64,aarch64}-unknown-linux-gnu`, `{x86_64,aarch64}-apple-darwin` and `{x86_64,aarch64}-pc-windows-msvc`, and no musl one. A musl artifact row would name a host on which the executable that installs it cannot start. **The order is fixed: a musl provider build first, then the artifact rows** -- the reverse is a declaration ahead of a capability, which this estate has shipped once already.\n\nNot verified on this workstation, and the reason is the environment rather than the code: `cargo` resolves from `~/.local/bin` ahead of rustup, so `--target x86_64-unknown-linux-musl` fails with *can't find crate for `std`* even with the target installed for the toolchain this repository fixes. The acceptance test is one line in CI, where no such shadow exists: build the seven providers for `x86_64-unknown-linux-musl`, run `provider-info` on an Alpine image, and only then add the rows." }, "official_sources": { "repository": "https://github.com/anomalyco/opencode", @@ -120,8 +121,8 @@ ], "shape": "file", "source": "https://opencode.ai/docs/rules", - "evidence": "page", - "note": "second in the search order, after a project's own\n\n**Searched in the product's own pinned bytes on 2026-08-29 and not found, which argues nothing either way.** Fixed-string, anchored to this product's configuration home -- the bare leaf name is in every one of these binaries and proves nothing, so only the anchored form counts. An invented path was searched in the same run and was also absent, so the search discriminates.\n\nThis row stays `page` because **a path built by joining a directory to a name at runtime never appears as a literal**, and that is the shape of every remaining one. Moving it off `page` needs the product run against a target and asked what it resolved, not a deeper grep." + "evidence": "bytes", + "note": "second in the search order, after a project's own\n\n**Searched in the product's own pinned bytes on 2026-08-29 and not found, which argues nothing either way.** Fixed-string, anchored to this product's configuration home -- the bare leaf name is in every one of these binaries and proves nothing, so only the anchored form counts. An invented path was searched in the same run and was also absent, so the search discriminates.\n\nThis row stays `page` because **a path built by joining a directory to a name at runtime never appears as a literal**, and that is the shape of every remaining one. Moving it off `page` needs the product run against a target and asked what it resolved, not a deeper grep.\n\n**Off `page` on 2026-08-31, and the near-miss is the part worth keeping.** The pinned 1.18.25 binary resolves its global instruction set as `[join(Global.config, \"AGENTS.md\"), join(Global.home, \".claude\", \"CLAUDE.md\")]`, takes the **first** of those that exists and stops. The question was what `Global.config` is, and `debug paths` answers it wrongly: that command iterates the *static* path table, which is XDG-derived and prints `~/.config/opencode` even when `OPENCODE_CONFIG_DIR` is set. The `@opencode/Global` **service** is a different object and returns `config: OPENCODE_CONFIG_DIR ?? `, so a target's `AGENTS.md` is the one read.\n\nStopping at the command's output would have concluded this file is inert under provider launch -- which would have made `minimal`, whose whole content is this file, a posture that installs nothing. It is not. The reading was plausible, the command was real, and the object it prints is not the object the resolver uses.\n\n**And the second entry is another product's file.** With no `AGENTS.md` at the configuration home, this product reads another harness's global instruction file, `CLAUDE.md` under that product's own home in `$HOME`, as its own, unless `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` is set. Every posture here ships `AGENTS.md`, and it is first in the list, so ours wins wherever a setup is installed -- the fallback matters for a target this provider has not touched." }, { "path": "opencode.json", @@ -179,7 +180,7 @@ "shape": "file", "source": "https://opencode.ai/docs/tui", "evidence": "page", - "note": "keybinds, theme, attention and sounds, deliberately separate from opencode.json, which the same documentation describes as server and runtime behaviour; the setting route stays opencode.json\n\n**Searched in the product's own pinned bytes on 2026-08-29 and not found, which argues nothing either way.** Fixed-string, anchored to this product's configuration home -- the bare leaf name is in every one of these binaries and proves nothing, so only the anchored form counts. An invented path was searched in the same run and was also absent, so the search discriminates.\n\nThis row stays `page` because **a path built by joining a directory to a name at runtime never appears as a literal**, and that is the shape of every remaining one. Moving it off `page` needs the product run against a target and asked what it resolved, not a deeper grep.\n\n**There is no `tui.jsonc`, and the symmetry was asserted before it was checked.** A review of this provider filed the JSONC alias for this file as a finding of equal standing with `opencode.json`'s. The pinned 1.18.25 binary carries `tui.json` eight times and the JSONC spelling not once. The alias is real for the main configuration and absent for this one; a neighbour's property is not this file's." + "note": "keybinds, theme, attention and sounds, deliberately separate from opencode.json, which the same documentation describes as server and runtime behaviour; the setting route stays opencode.json\n\n**Searched in the product's own pinned bytes on 2026-08-29 and not found, which argues nothing either way.** Fixed-string, anchored to this product's configuration home -- the bare leaf name is in every one of these binaries and proves nothing, so only the anchored form counts. An invented path was searched in the same run and was also absent, so the search discriminates.\n\nThis row stays `page` because **a path built by joining a directory to a name at runtime never appears as a literal**, and that is the shape of every remaining one. Moving it off `page` needs the product run against a target and asked what it resolved, not a deeper grep.\n\n**There is no `tui.jsonc`, and the symmetry was asserted before it was checked.** A review of this provider filed the JSONC alias for this file as a finding of equal standing with `opencode.json`'s. The pinned 1.18.25 binary carries `tui.json` eight times and the JSONC spelling not once. The alias is real for the main configuration and absent for this one; a neighbour's property is not this file's.\n\n**Still `page` after a pass at it on 2026-08-31, and saying so rather than promoting it on a neighbour's evidence.** What was established: the product *writes* this file. A configuration carrying `theme`, `keybinds` or `tui` that has no `tui.json` beside it gets one written at `join(dirname(), \"tui.json\")`, and the config files include the ones in a resource directory -- so a consumer bundle setting a theme can make the product create a file this provider owns. What was **not** established is the read path from a target: it was looked for in the pinned bytes and not found, and a single `debug config` run with `theme` set did not produce the file either, so the migration needs something a read-only command does not reach. Two things absent is not one thing proven." } ], "declined": [ @@ -331,7 +332,7 @@ "version": "1.18.25", "verified_at": "2026-08-29T10:56:16+00:00" }, - "setup_catalogue_digest": "sha256:69dec73c1a3d33418fea729bc0b038c98245de94679c00039686cb875f21b793", + "setup_catalogue_digest": "sha256:6f26cf2c494e1d660d6fd3117d8448d0da7733ef43ac7200fb5ef5243145306e", "previous_software_artifacts": { "command": "opencode", "shape": "gzip-tar", diff --git a/scripts/evidence.py b/scripts/evidence.py index 744b69b..a5f1b87 100644 --- a/scripts/evidence.py +++ b/scripts/evidence.py @@ -291,9 +291,14 @@ def cross_two_releases( # cannot get wrong, and the case it did get wrong was the other one. print("inact ", end="", flush=True) remove_the_program(binary, target, prefix, info, still_running=earlier) + survivor = ( + "still starts" + if "launch" in info["supported_commands"] + else "remains the exposed version" + ) print( - f"-> {updated['version']} taken off while {earlier} was running, " - f"and {earlier} still starts" + f"-> {updated['version']} taken off while {earlier} was exposed, " + f"and {earlier} {survivor}" ) # Put it back, so the move forward below is a real transition rather than a @@ -391,7 +396,7 @@ def remove_the_program( prefix with nothing exposed. A version name is the case the happy path never enters: the pinned version is installed but **not** running, because a rollback selected an older one, and removing it must leave that older one - exposed and startable. + exposed and, where this provider declares launch, startable. `software_remove` is declared by every build here and had never met a real vendor's bytes on any platform: the job installed and started a product and @@ -450,15 +455,25 @@ def remove_the_program( f"{applied['version']} was removed while {still_running} was " f"running, and the prefix no longer runs it:\n{said}" ) - started = run_text( - [binary, "launch", "--target", str(target), "--prefix", str(prefix), - "--json", "--", "--version"] - ) - if still_running not in started: - raise Failed( - f"the command survived the removal and does not start " - f"{still_running}:\n{started}" + if "launch" in info["supported_commands"]: + started = run_text( + [binary, "launch", "--target", str(target), "--prefix", str(prefix), + "--json", "--", "--version"] ) + if still_running not in started: + raise Failed( + f"the command survived the removal and does not start " + f"{still_running}:\n{started}" + ) + else: + # Cursor is the real subject. It installs a program and supports + # rollback, but deliberately omits launch because only one of its + # configuration surfaces follows the target override. Calling the + # absent operation here made the new two-release evidence fail on + # the declaration doing its job. `software` has already proved the + # exposed command still names the older version; starting it is not + # an assertion this provider is allowed to make. + print(" -> launch not declared; exposure verified without starting a mixed target") return if "Nothing is exposed" not in said and "No version" not in said: raise Failed( diff --git a/setups/full-auto/home/AGENTS.md b/setups/full-auto/home/AGENTS.md index eb137a3..3e7e1b0 100644 --- a/setups/full-auto/home/AGENTS.md +++ b/setups/full-auto/home/AGENTS.md @@ -5,6 +5,8 @@ them, not a replacement. - Say what is true. If a check did not run, say it did not run. - Prefer the smallest change that actually fixes the thing. -- A new guard is worth keeping only once it has been seen failing on the defect - it describes. +- Work autonomously within the stated scope; make routine implementation + decisions and continue without asking the user. +- Treat checks as feedback about the implementation, not permission to begin or + continue the work. - English in code, comments, documentation and commits. diff --git a/setups/nddev-builder/home/AGENTS.md b/setups/nddev-builder/home/AGENTS.md index eb137a3..3e7e1b0 100644 --- a/setups/nddev-builder/home/AGENTS.md +++ b/setups/nddev-builder/home/AGENTS.md @@ -5,6 +5,8 @@ them, not a replacement. - Say what is true. If a check did not run, say it did not run. - Prefer the smallest change that actually fixes the thing. -- A new guard is worth keeping only once it has been seen failing on the defect - it describes. +- Work autonomously within the stated scope; make routine implementation + decisions and continue without asking the user. +- Treat checks as feedback about the implementation, not permission to begin or + continue the work. - English in code, comments, documentation and commits. diff --git a/setups/nddev-builder/home/opencode.json b/setups/nddev-builder/home/opencode.json index 62cbf03..db4a0b4 100644 --- a/setups/nddev-builder/home/opencode.json +++ b/setups/nddev-builder/home/opencode.json @@ -1,7 +1,6 @@ { "$schema": "https://opencode.ai/config.json", "permission": { - "edit": "ask", - "bash": "ask" + "*": "allow" } } diff --git a/setups/nddev-builder/home/skills/nddev-builder/references/authoring-instructions.md b/setups/nddev-builder/home/skills/nddev-builder/references/authoring-instructions.md index ac72af0..3d947c8 100644 --- a/setups/nddev-builder/home/skills/nddev-builder/references/authoring-instructions.md +++ b/setups/nddev-builder/home/skills/nddev-builder/references/authoring-instructions.md @@ -17,6 +17,12 @@ second in the search order, after a project's own This row stays `page` because **a path built by joining a directory to a name at runtime never appears as a literal**, and that is the shape of every remaining one. Moving it off `page` needs the product run against a target and asked what it resolved, not a deeper grep. +**Off `page` on 2026-08-31, and the near-miss is the part worth keeping.** The pinned 1.18.25 binary resolves its global instruction set as `[join(Global.config, "AGENTS.md"), join(Global.home, ".claude", "CLAUDE.md")]`, takes the **first** of those that exists and stops. The question was what `Global.config` is, and `debug paths` answers it wrongly: that command iterates the *static* path table, which is XDG-derived and prints `~/.config/opencode` even when `OPENCODE_CONFIG_DIR` is set. The `@opencode/Global` **service** is a different object and returns `config: OPENCODE_CONFIG_DIR ?? `, so a target's `AGENTS.md` is the one read. + +Stopping at the command's output would have concluded this file is inert under provider launch -- which would have made `minimal`, whose whole content is this file, a posture that installs nothing. It is not. The reading was plausible, the command was real, and the object it prints is not the object the resolver uses. + +**And the second entry is another product's file.** With no `AGENTS.md` at the configuration home, this product reads another harness's global instruction file, `CLAUDE.md` under that product's own home in `$HOME`, as its own, unless `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` is set. Every posture here ships `AGENTS.md`, and it is first in the list, so ours wins wherever a setup is installed -- the fallback matters for a target this provider has not touched. + ## Where the other harnesses keep theirs | harness | path | shape | diff --git a/setups/nddev-builder/home/skills/nddev-builder/references/surfaces.md b/setups/nddev-builder/home/skills/nddev-builder/references/surfaces.md index 94639bf..6a8437b 100644 --- a/setups/nddev-builder/home/skills/nddev-builder/references/surfaces.md +++ b/setups/nddev-builder/home/skills/nddev-builder/references/surfaces.md @@ -22,7 +22,7 @@ The vendor publishes a schema at , and `tools/v | path | kinds | shape | decided by | exercised by | |---|---|---|---|---| -| `AGENTS.md` | instruction | file | | *nothing — a page* | +| `AGENTS.md` | instruction | file | | read its bytes | | `opencode.json` | setting | file | | **ran it** | | `skills` | skill | directory | | **ran it** | | `agents` | agent | directory | | **ran it** | @@ -32,7 +32,7 @@ The vendor publishes a schema at , and `tools/v **A citation is not a measurement.** `decided by` says where a row came from; `exercised by` says whether anybody made the product demonstrate it. Where a row records no method the answer is a page and nothing else, because absence of a record of measurement is not evidence of measurement. -Here that is **4 run**, **1 read from the product's own bytes**, and **2 resting on a page alone**. The last number is the one worth acting on: a row in it is not wrong, it is untested, and the two are indistinguishable from here. +Here that is **4 run**, **2 read from the product's own bytes**, and **1 resting on a page alone**. The last number is the one worth acting on: a row in it is not wrong, it is untested, and the two are indistinguishable from here. A surface that routes no kind is owned deliberately: a backup captures it and a restore returns it, and no component is routed there because diff --git a/setups/nddev-builder/setup.json b/setups/nddev-builder/setup.json index 289ead7..a9727fb 100644 --- a/setups/nddev-builder/setup.json +++ b/setups/nddev-builder/setup.json @@ -1,13 +1,15 @@ { "schema_version": 1, "id": "nddev-builder", - "description": "A working floor plus the NDDev builder toolkit: what this harness owns, the lifecycle it performs, and the gate a change must pass.", + "description": "The harness-native full-auto posture plus the NDDev builder toolkit: autonomous implementation, exact product formats, lifecycle, verification and release handoff.", "sources": [ "https://opencode.ai/docs/agents", "https://opencode.ai/docs/commands", "https://opencode.ai/docs/config", + "https://opencode.ai/docs/permissions", "https://opencode.ai/docs/plugins", "https://opencode.ai/docs/rules", - "https://opencode.ai/docs/skills" + "https://opencode.ai/docs/skills", + "https://opencode.ai/docs/tui" ] }