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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
name: rust
permissions:
contents: read
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/rust-ci.yml@1ab6708b62ec7bd17f2d8a519c6fcc39edb22243
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/rust-ci.yml@e90a2c7c6c25cd1fb9ac1c68adbc7535f8b17e1e
with:
toolchain: '1.98.0'
# The three-OS matrix is the evidence ADR-0113 asks for, and standard
Expand All @@ -46,7 +46,7 @@ jobs:
name: supply-chain
permissions:
contents: read
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/rust-supply-chain.yml@1ab6708b62ec7bd17f2d8a519c6fcc39edb22243
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/rust-supply-chain.yml@e90a2c7c6c25cd1fb9ac1c68adbc7535f8b17e1e
with:
enable_deny: true
enable_audit: true
Expand All @@ -56,7 +56,7 @@ jobs:
name: actionlint
permissions:
contents: read
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/actionlint.yml@1ab6708b62ec7bd17f2d8a519c6fcc39edb22243
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/actionlint.yml@e90a2c7c6c25cd1fb9ac1c68adbc7535f8b17e1e

pr-hygiene:
name: pr-hygiene
Expand All @@ -68,7 +68,7 @@ jobs:
# that would use them is off.
issues: write # the stale bot closes threads
pull-requests: write # the labeler applies labels
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/pr-hygiene.yml@1ab6708b62ec7bd17f2d8a519c6fcc39edb22243
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/pr-hygiene.yml@e90a2c7c6c25cd1fb9ac1c68adbc7535f8b17e1e


boundary:
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
actions: read # CodeQL reads the workflow definitions it analyses
contents: read
security-events: write # CodeQL publishes its findings to code scanning
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-codeql.yml@1ab6708b62ec7bd17f2d8a519c6fcc39edb22243
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-codeql.yml@e90a2c7c6c25cd1fb9ac1c68adbc7535f8b17e1e
with:
languages: '["rust", "actions"]'
queries: security-and-quality
Expand All @@ -37,7 +37,7 @@ jobs:
permissions:
contents: read
pull-requests: write # writes the review it produces
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-dependency-review.yml@1ab6708b62ec7bd17f2d8a519c6fcc39edb22243
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-dependency-review.yml@e90a2c7c6c25cd1fb9ac1c68adbc7535f8b17e1e
with:
fail_on_severity: moderate

Expand All @@ -52,24 +52,24 @@ jobs:
contents: read
id-token: write # mints the OIDC token Scorecard's publication is signed with
security-events: write # Scorecard publishes its findings to code scanning
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-scorecard.yml@1ab6708b62ec7bd17f2d8a519c6fcc39edb22243
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-scorecard.yml@e90a2c7c6c25cd1fb9ac1c68adbc7535f8b17e1e

osv:
name: osv
permissions:
contents: read
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/osv-scan.yml@1ab6708b62ec7bd17f2d8a519c6fcc39edb22243
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/osv-scan.yml@e90a2c7c6c25cd1fb9ac1c68adbc7535f8b17e1e

zizmor:
name: zizmor
permissions:
actions: read # the SARIF upload reads this run to attach its results
contents: read
security-events: write # zizmor publishes its findings to code scanning
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/zizmor-sarif.yml@1ab6708b62ec7bd17f2d8a519c6fcc39edb22243
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/zizmor-sarif.yml@e90a2c7c6c25cd1fb9ac1c68adbc7535f8b17e1e

secret-scan:
name: secret-scan
permissions:
contents: read
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/secret-scan.yml@1ab6708b62ec7bd17f2d8a519c6fcc39edb22243
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/secret-scan.yml@e90a2c7c6c25cd1fb9ac1c68adbc7535f8b17e1e
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,34 @@ sibling project the same week this note was added.

## [Unreleased]

## [0.0.57] - 2026-09-02

A kind declared only by a scoped profile validates and plans under
that scope. `validate-bundle` compared a bundle's component kinds against the
global profile whatever the surface, so a kind a provider implements only
elsewhere — codex's `skill`, which lives under `~/.agents` — was refused, and
every scoped plan carrying it with it. Now `validate-bundle` accepts a kind
any declared profile implements, the question it can answer with no scope in
its argv, and a plan under a scope checks kinds against that scope's profile;
a global plan carrying a scoped-only kind refuses by name. Found by the
consumer's user_root slice: four providers passed because they declare the
kind globally too, codex had never passed.

A target the system cannot canonicalize is taken as given. Inside the
consumer's Windows AppContainer, `GetFinalPathNameByHandle` cannot map a
volume back to a drive letter for any path, so every `status` refused with
"cannot be canonicalized" while `provider-info` answered — measured by the
consumer four ways. Now, once the directory has been inspected and its final
component is not a link, a failed canonicalization falls back to the lexical
absolute path; the verbatim `\?\` prefix is dropped everywhere so one
directory carries one `canonical_target` string inside and outside a
container; and the operating system's own error travels in the refusal
detail. The seven public clones the render check makes now identify as the
job's token rather than as a shared address.

Antigravity is pinned at 1.1.24, published since the previous release. One
Cursor citation moved and is re-cited.

## [0.0.56] - 2026-09-02

`status_request_fields` is declared: `["target_scope"]`, the same
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

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

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ members = [
]

[workspace.package]
version = "0.0.56"
version = "0.0.57"
edition = "2024"
rust-version = "1.89"
license = "AGPL-3.0-or-later"
Expand All @@ -23,9 +23,9 @@ sha2 = "0.11"
# `setup-core::archive`); an inflate loop is not, because its bugs are
# memory-safety bugs and it is not improved by being hand-written here.
miniz_oxide = "0.9"
setup-core = { path = "crates/setup-core", version = "0.0.56" }
provider-v3 = { path = "crates/provider-v3", version = "0.0.56" }
harness-runtime = { path = "crates/harness-runtime", version = "0.0.56" }
setup-core = { path = "crates/setup-core", version = "0.0.57" }
provider-v3 = { path = "crates/provider-v3", version = "0.0.57" }
harness-runtime = { path = "crates/harness-runtime", version = "0.0.57" }

[workspace.lints.rust]
unsafe_code = "forbid"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ release is a convenience, not the authorised copy.

```bash
docker run --rm -v "$HOME/.config:/config" \
ghcr.io/nddev-opennetwork/opencode-setup-system:0.0.56 \
ghcr.io/nddev-opennetwork/opencode-setup-system:0.0.57 \
status --target /config/<dir> --json
```

Expand Down
31 changes: 31 additions & 0 deletions crates/harness-runtime/src/facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,37 @@ impl Harness {
Self::within(self.owned_projection(scope), path)
}

/// The component kinds the profile at `scope` declares.
#[must_use]
pub fn kinds_at(&self, scope: Option<TargetScope>) -> &'static [ComponentKind] {
match self.scoped_for(scope) {
Some(scoped) => scoped.component_kinds,
None => self.component_kinds,
}
}

/// Whether *any* profile this provider declares implements a kind.
///
/// The same question `owns_anywhere` answers for a path, asked of a kind.
/// `validate-bundle` is handed no scope, so it can only ask whether this
/// provider could install the bundle at all -- and a kind declared only by
/// a scoped profile (codex's `skill`, which lives under `~/.agents` and
/// not under `~/.codex`) is one it can. Found by the consumer's `user_root`
/// slice on 2026-09-02: four providers passed because they declare the
/// kind globally as well, codex was refused at `validate-bundle` and had
/// never passed it.
#[must_use]
pub fn implements_anywhere(&self, kind: &str) -> bool {
self.component_kinds
.iter()
.chain(
self.scoped_projections
.iter()
.flat_map(|scoped| scoped.component_kinds.iter()),
)
.any(|declared| declared.as_str() == kind)
}

/// Whether *any* target this provider declares owns a path.
///
/// `validate-bundle` is handed a bundle and a target and no scope — that is
Expand Down
111 changes: 103 additions & 8 deletions crates/harness-runtime/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ fn verified_bundle(harness: &Harness, bundle: &ArgvBundle, surface: Surface) ->
},
)?;
check_within_surface(harness, verified.files.keys(), surface)?;
check_declared_kinds(harness, &verified)?;
check_declared_kinds(harness, &verified, surface)?;
Ok(verified)
}

Expand All @@ -117,21 +117,41 @@ fn verified_bundle(harness: &Harness, bundle: &ArgvBundle, surface: Surface) ->
/// conversion report, which is why a provider that never reads that report
/// cannot tell it has been handed a kind it does not implement. It would simply
/// write the files and report success for a component it does not understand.
fn check_declared_kinds(harness: &Harness, bundle: &Bundle) -> Result<()> {
///
/// Asked of the same surface the paths are checked against. This read the
/// global list whatever the surface, so a kind declared only by a scoped
/// profile -- codex's `skill`, which lives under `~/.agents` -- was refused by
/// `validate-bundle` and by every scoped plan, while the four providers that
/// also declare the kind globally passed. The consumer's `user_root` slice
/// found it on 2026-09-02; codex had never passed. `AnyDeclared` now means
/// any profile, and a scope means that scope's profile, exactly as for paths.
fn check_declared_kinds(harness: &Harness, bundle: &Bundle, surface: Surface) -> Result<()> {
for entry in &bundle.manifest.conversion_report.entries {
if entry.component_type.is_empty() {
continue;
}
let known = harness
.component_kinds
.iter()
.any(|kind| kind.as_str() == entry.component_type);
let known = match surface {
Surface::AnyDeclared => harness.implements_anywhere(&entry.component_type),
Surface::At(scope) => harness
.kinds_at(scope)
.iter()
.any(|kind| kind.as_str() == entry.component_type),
};
if !known {
return Err(Error::refuse(
WireReason::UnsupportedComponentKind,
format!(
"the bundle declares component {:?} as kind {:?}, which {} does not implement",
entry.stable_id, entry.component_type, harness.provider_id
"the bundle declares component {:?} as kind {:?}, which {} does not implement{}",
entry.stable_id,
entry.component_type,
harness.provider_id,
match surface {
Surface::AnyDeclared => String::new(),
Surface::At(scope) => format!(
" at {}",
scope.map_or("the global profile", provider_v3::TargetScope::as_str)
),
}
),
));
}
Expand Down Expand Up @@ -5506,6 +5526,81 @@ mod tests {
);
}

/// A kind declared only by a scoped profile -- codex's `skill` under
/// `~/.agents` -- is one this provider implements, and `validate-bundle`
/// has no scope to ask about: it must say yes. A scoped plan under that
/// scope says yes; a global plan says no, by name, because the home does
/// not route the kind. Found by the consumer's `user_root` slice: codex had
/// never passed `validate-bundle` with a skill.
#[test]
fn a_kind_declared_only_by_a_scope_validates_and_plans_under_that_scope() {
let mut harness = TEST;
harness.component_kinds = &[
provider_v3::ComponentKind::Instruction,
provider_v3::ComponentKind::Setting,
];
let target = seeded("scoped-only-kind");
let (bytes, bundle_digest, artifact) = bundle_bytes_declaring(
&[("shared/probe/SKILL.md", "probe\n", 0o644)],
Some("skill"),
);
let artifact_path = target.join("..").join("scoped-kind.zip");
fs::write(&artifact_path, &bytes).unwrap();
let flags = bundle_flags(&artifact_path, &bundle_digest, &artifact, bytes.len());
let borrowed: Vec<&str> = flags.iter().map(String::as_str).collect();

let validated = run_for(&harness, args("validate-bundle", &target, &borrowed));
assert_eq!(validated["valid"], true, "{validated}");

let mut scoped = vec![
"--operation".to_owned(),
"install".to_owned(),
"--provider-release-digest".to_owned(),
RELEASE.to_owned(),
"--operation-id".to_owned(),
"operation_01SCOPEDKIND".to_owned(),
"--expires-at".to_owned(),
far_future().to_owned(),
"--target-scope".to_owned(),
"user_root".to_owned(),
];
scoped.extend(flags.clone());
let borrowed: Vec<&str> = scoped.iter().map(String::as_str).collect();
let planned = run_for(&harness, args("plan-operation", &target, &borrowed));
assert_eq!(planned["state"], "planned", "{planned}");

// The same kind on a path the home owns: the surface passes and the
// kind is the refusal, named with the profile that lacks it.
let (bytes, bundle_digest, artifact) =
bundle_bytes_declaring(&[("skills/probe.md", "probe\n", 0o644)], Some("skill"));
let home_path = target.join("..").join("global-kind.zip");
fs::write(&home_path, &bytes).unwrap();
let mut global = vec![
"--operation".to_owned(),
"install".to_owned(),
"--provider-release-digest".to_owned(),
RELEASE.to_owned(),
"--operation-id".to_owned(),
"operation_01GLOBALKIND".to_owned(),
"--expires-at".to_owned(),
far_future().to_owned(),
];
global.extend(bundle_flags(
&home_path,
&bundle_digest,
&artifact,
bytes.len(),
));
let borrowed: Vec<&str> = global.iter().map(String::as_str).collect();
let error = refuse_for(&harness, args("plan-operation", &target, &borrowed));
assert_eq!(error.reason(), Some(WireReason::UnsupportedComponentKind));
assert!(
error.detail().contains("at the global profile"),
"{}",
error.detail()
);
}

/// A harness shaped like cursor's project scope, for the workspace cases.
fn project_shaped() -> Harness {
let mut harness = TEST;
Expand Down
Loading
Loading