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`:
- `codex-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, agents, commands, hooks and settings together, in one step.

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

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`.

Measured, with the real product: launching Codex CLI 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` withdraws 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.

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
16 changes: 8 additions & 8 deletions crates/codex-setup-system/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ pub const CODEX: Harness = Harness {
// `developer_instructions` is refused by name when absent. The consumer
// reproduced it against the same binary before either side moved.
//
// The stanza form still works and excludes its own file from the scan,
// so this setup's builder role stays the pair -- a setup owning two
// files it declares, which is a different thing from a component, and
// was the half of the old reasoning that was true.
// The stanza form still works and excludes its own file from the scan;
// this setup's builder role ships as the standalone
// `agents/nddev-builder.toml` -- one file, declared like any other
// payload member.
ComponentKind::Agent,
],
projection_kinds: &[
Expand Down Expand Up @@ -671,10 +671,10 @@ mod tests {
"{}",
examined.problems.join("\n ")
);
// codex ships no skill and no agent file: its skills are `user_root` only and its agent is a role declared in `config.toml`. **Zero is the right number and it is the reason this count exists** -- the assertion below it was green here while examining nothing, and nobody could tell that from the six harnesses where it examined something.
// Codex ships no SKILL.md and no markdown agent: its skills are `user_root` only and its agent is the standalone `agents/nddev-builder.toml`. **One is the right number and it is the reason this count exists** -- the assertion below it was green on every harness while the guard examined nothing at all here, and the count is what makes the subject visible.
assert_eq!(
examined.entry_points, 0,
"the description guard examined {} entry points, not 0",
examined.entry_points, 1,
"the description guard examined {} entry points, not 1",
examined.entry_points
);
}
Expand Down Expand Up @@ -708,7 +708,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
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/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\codex-setup-system"
[string]$InstallDir = $(if ($env:CODEX_INSTALL_DIR) { $env:CODEX_INSTALL_DIR } else { "$env:LOCALAPPDATA\Programs\codex-setup-system" })
)
$ErrorActionPreference = "Stop"

Expand Down
2 changes: 1 addition & 1 deletion references/codex-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@
"version": "0.158.0",
"verified_at": "2026-09-28T14:26:56+00:00"
},
"setup_catalogue_digest": "sha256:911bd311e0797e8222218ba12d80dfc0d0a8399419447fb86edf37d54137cff4",
"setup_catalogue_digest": "sha256:38ea677a1b5ebbb65bd1347ac5e3ae22778eee45a2a3f8361f943c08500319a4",
"previous_software_artifacts": {
"command": "codex",
"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
Loading
Loading