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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/defect.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ labels: []
## Environment

- Operating system and architecture:
- `<tool>-setup-system --version`:
- `opencode-setup-system --version`:
- Product version being configured:

## Target state, if relevant
Expand Down
14 changes: 9 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
contents: read
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/rust-ci.yml@d92026cf31a10a0eeaa532e1f323c7cfdfbfac5b # 0.1.29
with:
toolchain: '1.98.0'
toolchain: '1.98.1'
# The three-OS matrix is the evidence ADR-0113 asks for, and standard
# hosted runners are free with unlimited minutes on a public repository.
test_matrix_os: '["ubuntu-latest", "macos-latest", "windows-latest"]'
Expand Down Expand Up @@ -99,7 +99,7 @@ jobs:
- name: Set up Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
toolchain: '1.98.0'
toolchain: '1.98.1'
# The same reason the release job gives. This job's whole subject is
# the bytes that come out of the build, so restoring someone else's
# `target/` and then reading its import table would answer a question
Expand Down Expand Up @@ -181,9 +181,13 @@ jobs:
# bypassed, or read by someone with no reason to trust it.
#
# Measured on Linux across all seven binaries, then on macOS by
# `0.0.8`'s own run, and the two agree exactly: the six that declare
# `launch` import `execvp` and nothing else; antigravity, which
# declares none, imports no spawn symbol at all, on either platform.
# `0.0.8`'s own run, and the two agree exactly: every build that
# declares `launch` imports `execvp` and nothing else. `0.0.8` still
# carried the era when antigravity declared none and imported no
# spawn symbol at all -- all seven declare it now (antigravity's is
# a documented-home binding), so today the negative arm is enforced
# on nobody, and stays because the day a launch slips out of a
# declaration is the day the spawn symbol must fail here.
# So the linker drops what nothing calls and the absence is real
# rather than incidental -- which is what had to be true before this
# could be enforced anywhere.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/evidence.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
- name: Set up Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
toolchain: '1.98.0'
toolchain: '1.98.1'

- name: Prove this is the native architecture, not emulation
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
- name: Set up Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
toolchain: '1.98.0'
toolchain: '1.98.1'
# This action caches by default, and a release must not build on top
# of a cache. A run with weaker trust than this one can write the
# cache a release would restore, so a poisoned entry would be baked
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ instructions, skills, agents, commands, plugins and settings together, in one st

```bash
opencode-setup-system list
opencode-setup-system install baseline --target ~/.tool-config
opencode-setup-system status --target ~/.tool-config
opencode-setup-system select full-auto --target ~/.tool-config
opencode-setup-system diff --target ~/.tool-config
opencode-setup-system reinstall --target ~/.tool-config
opencode-setup-system backups --target ~/.tool-config
opencode-setup-system hold --backup slot-000000000001 --reason "before the experiment" --target ~/.tool-config
opencode-setup-system restore --backup slot-000000000001 --target ~/.tool-config
opencode-setup-system remove --target ~/.tool-config
opencode-setup-system install baseline --target ~/.config/opencode
opencode-setup-system status --target ~/.config/opencode
opencode-setup-system select full-auto --target ~/.config/opencode
opencode-setup-system diff --target ~/.config/opencode
opencode-setup-system reinstall --target ~/.config/opencode
opencode-setup-system backups --target ~/.config/opencode
opencode-setup-system hold --backup slot-000000000001 --reason "before the experiment" --target ~/.config/opencode
opencode-setup-system restore --backup slot-000000000001 --target ~/.config/opencode
opencode-setup-system remove --target ~/.config/opencode
```

Every command takes an explicit `--target`. There is no default and no fallback
Expand All @@ -47,8 +47,8 @@ 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 |
| `baseline` | a working floor: instructions plus the shared autonomous posture |
| `minimal` | instructions plus the shared autonomous posture, and nothing else |
| `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 |

Expand Down
21 changes: 8 additions & 13 deletions SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,18 @@ A provider that advertised an operation it cannot perform would let a caller ask
for something that cannot be honoured, which is worse than not offering it.

All five core operations do work: `backup`, `restore`, `remove`, `install` and
`replace`, both from the local setup catalog and from an `ai-stp-bundle/1`
`replace`, both from the local setup catalog and from an `ai-stp-bundle/2`
arriving over the wire.

## Using this against a home you already have

**An owned namespace is removed whole.** The table below says what this build
owns; `remove` deletes each of those paths entirely, and a backup slot holds
what was there first. That includes content this build never wrote -- if the
product itself put a key in a configuration file this provider owns, `remove`
takes the file, not the keys this provider added to it.

Measured, with the real product: launching Codex through `launch` and running
`mcp add` writes `~/.codex/config.toml` with an `[mcp_servers.*]` entry; a
later `install` captures that file into a slot and replaces it; a later
`remove` deletes it. The entry is not lost -- `backups` lists the slot as
*before install, setup none*, and restoring it returns the file byte for byte
-- but it is not in the target either.
**Removal follows receipts, not namespaces.** The table below says what this
build owns; `remove` withdraws the files this provider recorded writing, and
in a JSON file it owns it strips the keys it added rather than taking the
file. Anything under those paths this build never wrote stays. Emptying every
owned namespace is a separate, explicitly named operation: `reset`.

No credential-free command is measured writing this product's home -- the dated measurement lives in `references/` and the absence is recorded, not assumed. The receipt discipline is the same for whatever arrives later: a file this provider wrote is captured into a slot before the next `install`, withdrawn by `remove`, and returned byte for byte by `restore`.

So: point `--target` at a home you are willing to have managed. `backups
--target <dir>` names every earlier state and which setup each preceded, and
Expand Down
42 changes: 37 additions & 5 deletions crates/harness-runtime/src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,10 @@ pub struct Examined {
/// half is the part that took the measuring:
///
/// * `SKILL.md`, and a file directly under `agents/`, are entry points. Cursor's
/// own generator writes `{name, description}` for exactly these.
/// own generator writes `{name, description}` for exactly these. A codex
/// `agents/<name>.toml` is the same obligation in TOML: the product refuses
/// the file by name when `name` or `description` is absent, so the check
/// reads the keys instead of frontmatter.
/// * A file under `references/` is **not** -- it is a document a skill links to,
/// and requiring frontmatter there would be inventing a rule.
/// * A file under `commands/` is **not**. Cursor's loader builds
Expand Down Expand Up @@ -357,10 +360,18 @@ pub fn undescribed(setups: &[Setup]) -> Examined {
found.push(format!("{} cannot read {name:?}", setup.manifest.id));
continue;
};
let named = if std::path::Path::new(&name)
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("toml"))
{
toml_names
} else {
frontmatter_names
};
for key in ["name", "description"] {
if !frontmatter_names(&text, key) {
if !named(&text, key) {
found.push(format!(
"{} ships {name:?} with no `{key}` in its frontmatter, and a component \
"{} ships {name:?} with no `{key}` the product reads, and a component \
the product cannot describe is one the model cannot choose",
setup.manifest.id
));
Expand Down Expand Up @@ -767,13 +778,34 @@ fn is_entry_point(relative: &str) -> bool {
if leaf.eq_ignore_ascii_case("SKILL.md") {
return true;
}
// `agents/<name>.md`, and only directly under it.
// `agents/<name>.md` and `agents/<name>.toml`, and only directly under it.
// The `.toml` form is how codex declares a role file; it is measured there
// to carry the same two keys in TOML spelling.
parts.len() >= 2
&& parts[parts.len() - 2] == "agents"
&& std::path::Path::new(leaf)
.extension()
.and_then(std::ffi::OsStr::to_str)
.is_some_and(|extension| extension.eq_ignore_ascii_case("md"))
.is_some_and(|extension| {
extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("toml")
})
}

/// Whether a TOML entry point assigns `key` at the top level.
///
/// Read by structure rather than by searching the whole file: only lines
/// before the first `[section]` header count, because a `description` three
/// tables down belongs to that table and not to the agent the file names.
fn toml_names(text: &str, key: &str) -> bool {
text.lines()
.take_while(|line| !line.trim_start().starts_with('['))
.any(|line| {
let line = line.trim_start();
line.starts_with(key)
&& line[key.len()..]
.trim_start_matches([' ', '\t'])
.starts_with('=')
})
}

/// Whether a file opens with YAML frontmatter naming `key`.
Expand Down
16 changes: 5 additions & 11 deletions crates/harness-runtime/src/facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! *facts about a product*, verified against its official documentation and
//! recorded in a baseline — not behaviour, and not code.
//!
//! Holding them as data rather than as five copies of a dispatcher means a
//! Holding them as data rather than as seven copies of a dispatcher means a
//! change to the shared logic lands in one place, and a change to a product's
//! surface lands in exactly one struct with a test binding it to that product's
//! baseline.
Expand Down Expand Up @@ -137,7 +137,7 @@ pub struct Harness {
/// makes the name *visible*, which is the part that was missing: the
/// answer was true about what it examined and silent about what decides.
///
/// Empty for six of the seven, and empty because they were asked -- a
/// Empty for three of the seven, and empty because they were asked -- a
/// product whose alternate spellings nobody has measured belongs here as
/// nothing rather than as a guess.
pub shadowing_names: &'static [Shadow],
Expand Down Expand Up @@ -203,9 +203,9 @@ pub struct Harness {
pub projection_kinds: &'static [ProjectionKind],
/// Second targets this provider owns, if any.
///
/// Empty for six of the seven. Antigravity is the exception because the
/// product genuinely keeps a workspace copy of five of its surfaces, and
/// `ai_stp#424`/`#425` are the consumer asking for exactly that route.
/// No build leaves this empty today: each declares a `user_root` scope for
/// products that read the shared `~/.agents` convention, a `project`
/// scope for surfaces the product reads from a workspace, or both.
pub scoped_projections: &'static [Scoped],
/// The largest file count a bundle may carry.
pub max_files: u64,
Expand Down Expand Up @@ -234,12 +234,6 @@ 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<Software>,
/// Target-relative path of the user-global instruction attachment.
///
Expand Down
6 changes: 3 additions & 3 deletions crates/harness-runtime/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
//! The provider command runtime every NDDev setup system shares.
//!
//! Five products, one set of commands. What differs between them is not
//! Seven products, one set of commands. What differs between them is not
//! behaviour but *facts*: which directory a product configures, which files
//! inside it this provider owns, and which files belong to the product and must
//! be left alone. [`Harness`] holds those facts; [`wire::dispatch`] performs the
//! commands over them.
//!
//! Written this way, a change to the shared logic lands once instead of five
//! Written this way, a change to the shared logic lands once instead of seven
//! times, and a change to one product's surface lands in exactly one struct that
//! a test binds to that product's verified baseline.
//!
//! # What this runtime does and does not do
//!
//! It performs all five core operations. `backup`, `restore` and `remove` read
//! the target, a backup slot, or the provider's own state. `install` and
//! `replace` materialize an `ai-stp-bundle/1` the consumer sends, or a complete
//! `replace` materialize an `ai-stp-bundle/2` the consumer sends, or a complete
//! setup from the local catalog when the owner asks for one by name.
//!
//! The software lifecycle is optional in the contract, and a harness declares
Expand Down
6 changes: 3 additions & 3 deletions crates/opencode-setup-system/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ pub const OPENCODE: Harness = Harness {
// writing -- the removal refuses rather than widening when it cannot read
// the record, the capture takes ours and not a neighbour's, and a restore
// leaves a neighbour's file as it was. Five of the seven products read
// this root and one declared it; the reason was simply not re-read when
// the thing it described changed.
// this root, and all five declare it now; the reason was simply not
// re-read when the thing it described changed.
scoped_projections: &[Scoped {
target_scope: TargetScope::UserRoot,
// Distinct from the global identity, because the digest binds a
Expand Down Expand Up @@ -566,7 +566,7 @@ mod tests {
}
/// Three postures, on every one of the seven.
///
/// `baseline` is a working floor, `minimal` is the product's own defaults,
/// `baseline` is a working floor, `minimal` is the shared autonomous posture and nothing else,
/// and `full-auto` asks nothing and sandboxes nothing. A caller who learns
/// them on one product knows them on all seven, which is the whole reason
/// the names are the estate's rather than each harness's.
Expand Down
6 changes: 3 additions & 3 deletions crates/provider-v3/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ use crate::zip;
/// The digest domain for a bundle manifest.
pub const BUNDLE_DOMAIN: &str = "ai-stp:bundle:v1";

/// The original format tag, kept byte-identical during the v2 rollout.
/// The adaptation-bound format, the only tag a production bundle carries.
pub const BUNDLE_FORMAT: &str = "ai-stp-bundle/2";

/// The adaptation-bound format.
/// The original format tag, retired by the v2 rollout and refused on read.
#[cfg(test)]
const RETIRED_BUNDLE_FORMAT_V1: &str = "ai-stp-bundle/1";

Expand Down Expand Up @@ -237,7 +237,7 @@ pub struct ComponentAdaptationBinding {
pub struct Manifest {
/// Schema of this manifest.
pub schema_version: u32,
/// Always `ai-stp-bundle/1`.
/// Always `ai-stp-bundle/2`.
pub bundle_format: String,
/// The *bundle* protocol, which is 1. Not the provider protocol.
pub protocol_version: u32,
Expand Down
4 changes: 2 additions & 2 deletions crates/provider-v3/src/vocabulary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,8 @@ impl ProjectionKind {

/// A target a projection profile owns, other than the product's own home.
///
/// The kit's schema enumerates exactly one value today, and the global scope is
/// deliberately not among them: the global profile is declared by
/// The kit's schema enumerates exactly two values today, and the global scope
/// is deliberately not among them: the global profile is declared by
/// `projection_profile` itself, and two statements about one fact are a defect
/// even while they agree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
Expand Down
2 changes: 1 addition & 1 deletion install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[CmdletBinding()]
param(
[string]$Version = "0.0.81",
[string]$InstallDir = "$env:LOCALAPPDATA\Programs\opencode-setup-system"
[string]$InstallDir = $(if ($env:OPENCODE_INSTALL_DIR) { $env:OPENCODE_INSTALL_DIR } else { "$env:LOCALAPPDATA\Programs\opencode-setup-system" })
)
$ErrorActionPreference = "Stop"

Expand Down
2 changes: 1 addition & 1 deletion references/opencode-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@
"version": "1.18.33",
"verified_at": "2026-09-28T14:27:53+00:00"
},
"setup_catalogue_digest": "sha256:2bc4f39b26da94b2b65f5ea2a3fc8a8b788611d9676e7bce405453f6c4c4daff",
"setup_catalogue_digest": "sha256:7a5ac4414efe1db02165df91cabb8453b4ed8c94423154f2afdb8c9a870e980d",
"previous_software_artifacts": {
"command": "opencode",
"shape": "gzip-tar",
Expand Down
10 changes: 6 additions & 4 deletions scripts/evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,12 @@ def plan(
]
)
if answer.get("reason") == "unsupported_platform":
# An honest answer, not a failure. Cursor publishes no Windows build,
# and the provider says so by name rather than planning something it
# could not apply. Treating that as a red would make this job report a
# vendor's product range as a defect of ours.
# An honest answer, not a failure. When a vendor's published manifest
# carries no build for a declared platform, the provider says so by
# name rather than planning something it could not apply. Treating
# that as a red would make this job report a vendor's product range
# as a defect of ours. No harness answers it today; the path stays
# wired because the reason is contract, not decoration.
raise NothingToProve(str(answer.get("detail", "")))
if answer.get("state") != "planned":
raise Failed(
Expand Down
4 changes: 2 additions & 2 deletions setups/nddev-builder/home/commands/nddev-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ If changing provider implementation, also run this tree's checks:

```bash
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked --all-targets
```

If a command is not present, say so rather than working around it.
4 changes: 2 additions & 2 deletions setups/nddev-builder/home/commands/nddev-validate.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ For provider implementation changes, also run this checkout's checks:

```bash
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked --all-targets
```

If a command here is not present, say so rather than working around it.
Expand Down
Loading
Loading