Skip to content

Commit 441d382

Browse files
authored
fix: Hardcoded config (#11)
1 parent 9836ba0 commit 441d382

9 files changed

Lines changed: 88 additions & 75 deletions

File tree

CONTRIBUTING.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,20 @@ In your crate `src/lib.rs`, implement `registry_definition()` using `safe_pkgs_c
8484

8585
Update `app_registry_definitions()` in `src/main.rs` to include your crate's `registry_definition()`.
8686

87-
### 7) Update central check-support policy only if needed
87+
### 7) Declare unsupported checks in your registry crate
8888

89-
If your registry cannot support specific checks, add exclusions in `app_registry_check_support()` in `src/main.rs`.
90-
Do not duplicate support rules across registry crates.
89+
If your registry cannot support specific checks, set `excluded_checks` on the `RegistryDefinition` returned by your crate's `registry_definition()`:
90+
91+
```rust
92+
RegistryDefinition {
93+
key: "myregistry",
94+
create_client,
95+
create_lockfile_parser: Some(create_lockfile_parser),
96+
excluded_checks: &["install_script"],
97+
}
98+
```
99+
100+
No changes to `src/main.rs` or any other crate are required.
91101

92102
## Add a New Check
93103

@@ -136,9 +146,9 @@ In your crate `src/lib.rs`:
136146

137147
- Update `app_check_factories()` in `src/main.rs` to include `safe_pkgs_check_<name>::create_check`.
138148

139-
### 7) Declare registry support in one place
149+
### 7) Declare registry support in the registry crate
140150

141-
If a registry cannot support your new check, update central policy in `app_registry_check_support()` in `src/main.rs`.
151+
If a registry cannot support your new check, add the check ID to `excluded_checks` in that registry crate's `registry_definition()`.
142152
If no override is needed, no registry changes are required.
143153

144154
### 8) Verify support map

crates/checks/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ Conventions:
2222
App wiring:
2323

2424
- The binary chooses enabled checks in `src/main.rs` via `app_check_factories()`.
25-
- Registry check-support compatibility is centralized in `app_registry_check_support()` in `src/main.rs`.
25+
- Registry check-support compatibility is declared via `excluded_checks` on each registry crate's `RegistryDefinition`.
2626
- The orchestrator in `src/checks.rs` runs factories and handles ordering/config gating.

crates/core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,8 @@ pub struct RegistryDefinition {
667667
pub key: &'static str,
668668
pub create_client: fn() -> Arc<dyn RegistryClient>,
669669
pub create_lockfile_parser: Option<fn() -> Arc<dyn LockfileParser>>,
670+
/// Check IDs this registry does not support.
671+
pub excluded_checks: &'static [CheckId],
670672
}
671673

672674
pub trait RegistryPlugin: Send + Sync {

crates/registry/cargo/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub fn registry_definition() -> RegistryDefinition {
1212
key: "cargo",
1313
create_client,
1414
create_lockfile_parser: Some(create_lockfile_parser),
15+
excluded_checks: &["install_script"],
1516
}
1617
}
1718

crates/registry/npm/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub fn registry_definition() -> RegistryDefinition {
1212
key: "npm",
1313
create_client,
1414
create_lockfile_parser: Some(create_lockfile_parser),
15+
excluded_checks: &[],
1516
}
1617
}
1718

crates/registry/pypi/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub fn registry_definition() -> RegistryDefinition {
1212
key: "pypi",
1313
create_client,
1414
create_lockfile_parser: Some(create_lockfile_parser),
15+
excluded_checks: &["install_script"],
1516
}
1617
}
1718

src/main.rs

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ enum Commands {
6767
/// Path to a dependency file or project directory
6868
path: String,
6969
/// Registry for dependency file parsing and package checks
70-
#[arg(long, default_value = "npm")]
70+
#[arg(long, default_value_t = crate::registries::default_lockfile_registry_key().to_string())]
7171
registry: String,
7272
},
7373
/// Print check support for registries
@@ -87,17 +87,6 @@ pub(crate) fn app_registry_definitions() -> Vec<registries::RegistryDefinition>
8787
]
8888
}
8989

90-
const NO_INSTALL_SCRIPT_SUPPORT: &[registries::CheckId] = &["install_script"];
91-
92-
/// Central registry/check compatibility policy.
93-
pub(crate) fn app_registry_check_support(registry_key: &str) -> registries::RegistryCheckSupport {
94-
match registry_key {
95-
// Central compatibility policy: these registries don't expose install scripts.
96-
"cargo" | "pypi" => registries::RegistryCheckSupport::AllExcept(NO_INSTALL_SCRIPT_SUPPORT),
97-
_ => registries::RegistryCheckSupport::All,
98-
}
99-
}
100-
10190
/// Returns check factories wired into this application build.
10291
pub(crate) fn app_check_factories() -> Vec<safe_pkgs_core::CheckFactory> {
10392
vec![
@@ -168,25 +157,15 @@ mod tests {
168157
}
169158

170159
#[test]
171-
fn registry_check_support_disables_install_script_for_non_npm() {
172-
match app_registry_check_support("npm") {
173-
registries::RegistryCheckSupport::All => {}
174-
_ => panic!("npm should support all checks by default"),
175-
}
176-
177-
match app_registry_check_support("cargo") {
178-
registries::RegistryCheckSupport::AllExcept(disallowed) => {
179-
assert_eq!(disallowed, NO_INSTALL_SCRIPT_SUPPORT);
180-
}
181-
_ => panic!("cargo should exclude install_script"),
182-
}
160+
fn registry_definitions_excluded_checks_are_correct() {
161+
let defs = app_registry_definitions();
162+
let npm = defs.iter().find(|d| d.key == "npm").expect("npm definition");
163+
let cargo = defs.iter().find(|d| d.key == "cargo").expect("cargo definition");
164+
let pypi = defs.iter().find(|d| d.key == "pypi").expect("pypi definition");
183165

184-
match app_registry_check_support("pypi") {
185-
registries::RegistryCheckSupport::AllExcept(disallowed) => {
186-
assert_eq!(disallowed, NO_INSTALL_SCRIPT_SUPPORT);
187-
}
188-
_ => panic!("pypi should exclude install_script"),
189-
}
166+
assert!(npm.excluded_checks.is_empty());
167+
assert!(cargo.excluded_checks.contains(&"install_script"));
168+
assert!(pypi.excluded_checks.contains(&"install_script"));
190169
}
191170

192171
#[test]

src/mcp/server.rs

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ fn package_registry_schema(generator: &mut SchemaGenerator) -> Schema {
2525
"default".into(),
2626
serde_json::json!(crate::registries::default_package_registry_key()),
2727
);
28+
schema.insert(
29+
"description".into(),
30+
serde_json::json!(format!(
31+
"Package registry. Supported: {}. Defaults to \"{}\".",
32+
crate::registries::supported_package_registry_keys().join("\", \""),
33+
crate::registries::default_package_registry_key(),
34+
)),
35+
);
2836
schema
2937
}
3038

@@ -38,13 +46,40 @@ fn lockfile_registry_schema(generator: &mut SchemaGenerator) -> Schema {
3846
"default".into(),
3947
serde_json::json!(crate::registries::default_lockfile_registry_key()),
4048
);
49+
schema.insert(
50+
"description".into(),
51+
serde_json::json!(format!(
52+
"Registry for parsing and checks. Supported: {}. Defaults to \"{}\".",
53+
crate::registries::supported_lockfile_registry_keys().join("\", \""),
54+
crate::registries::default_lockfile_registry_key(),
55+
)),
56+
);
4157
schema
4258
}
4359

4460
fn default_lockfile_registry() -> String {
4561
crate::registries::default_lockfile_registry_key().to_string()
4662
}
4763

64+
fn lockfile_path_schema(generator: &mut SchemaGenerator) -> Schema {
65+
let mut schema = String::json_schema(generator);
66+
let registry_files = crate::registries::supported_lockfile_registry_keys()
67+
.into_iter()
68+
.filter_map(|key| {
69+
crate::registries::supported_lockfile_files_for_registry(key)
70+
.map(|files| format!("{key}: {}", files.join("/")))
71+
})
72+
.collect::<Vec<_>>()
73+
.join(", ");
74+
schema.insert(
75+
"description".into(),
76+
serde_json::json!(format!(
77+
"Path to a dependency file or project directory for batch safety checks ({registry_files}). Defaults to current working directory."
78+
)),
79+
);
80+
schema
81+
}
82+
4883
/// Parameters for the `check_package` MCP tool.
4984
#[derive(Debug, Deserialize, JsonSchema)]
5085
pub struct PackageQuery {
@@ -60,30 +95,19 @@ pub struct PackageQuery {
6095
/// Optional version. Uses latest when omitted.
6196
pub version: Option<String>,
6297

63-
#[schemars(
64-
description = "Package registry. Defaults to \"npm\". Supported: \"npm\", \"cargo\", \"pypi\"."
65-
)]
6698
#[serde(default = "default_package_registry")]
6799
#[schemars(schema_with = "package_registry_schema")]
68-
/// Registry key (`npm`, `cargo`, `pypi`).
69100
pub registry: String,
70101
}
71102

72103
/// Parameters for the `check_lockfile` MCP tool.
73104
#[derive(Debug, Deserialize, JsonSchema)]
74105
pub struct LockfileQuery {
75-
#[schemars(
76-
description = "Path to a dependency file or project directory for batch safety checks. npm: package-lock.json/package.json, cargo: Cargo.lock/Cargo.toml, pypi: requirements.txt/pyproject.toml. Defaults to current working directory."
77-
)]
78-
/// Path to a lockfile/manifest file or project directory.
106+
#[schemars(schema_with = "lockfile_path_schema")]
79107
pub path: Option<String>,
80108

81-
#[schemars(
82-
description = "Registry used for parsing and checks. Defaults to \"npm\". Supported: \"npm\", \"cargo\", \"pypi\"."
83-
)]
84109
#[serde(default = "default_lockfile_registry")]
85110
#[schemars(schema_with = "lockfile_registry_schema")]
86-
/// Registry key used for parser + checks.
87111
pub registry: String,
88112
}
89113

@@ -167,13 +191,21 @@ impl SafePkgsServer {
167191
#[tool_handler]
168192
impl ServerHandler for SafePkgsServer {
169193
fn get_info(&self) -> ServerInfo {
194+
let lockfile_keywords = crate::registries::supported_lockfile_registry_keys()
195+
.into_iter()
196+
.filter_map(|key| crate::registries::supported_lockfile_files_for_registry(key))
197+
.flatten()
198+
.map(|f| format!("`{f}`"))
199+
.collect::<Vec<_>>()
200+
.join(", ");
201+
let instructions = format!(
202+
"Dependency safety policy: (1) For single dependency intent keywords (`add`, `install`, `update`, `upgrade`, `bump`, `pin`), call `check_package` first and do not edit files before the result. (2) For batch/file intent keywords ({lockfile_keywords}, `install deps`, `audit lockfile`), call `check_lockfile` first. (3) Enforce gating: if `allow=false`, do not proceed; return risk, reasons, and machine-readable evidence."
203+
);
170204
ServerInfo {
171205
protocol_version: ProtocolVersion::V_2024_11_05,
172206
capabilities: ServerCapabilities::builder().enable_tools().build(),
173207
server_info: Implementation::from_build_env(),
174-
instructions: Some(
175-
"Dependency safety policy: (1) For single dependency intent keywords (`add`, `install`, `update`, `upgrade`, `bump`, `pin`), call `check_package` first and do not edit files before the result. (2) For batch/file intent keywords (`package-lock.json`, `package.json`, `Cargo.lock`, `Cargo.toml`, `requirements.txt`, `pyproject.toml`, `install deps`, `audit lockfile`), call `check_lockfile` first. (3) Enforce gating: if `allow=false`, do not proceed; return risk, reasons, and machine-readable evidence.".into(),
176-
),
208+
instructions: Some(instructions.into()),
177209
}
178210
}
179211
}

src/registries/mod.rs

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,6 @@ pub use safe_pkgs_core::{
88
CheckId, LockfileParser, RegistryClient, RegistryDefinition, RegistryPlugin, normalize_check_id,
99
};
1010

11-
/// Central check-support mode for a registry.
12-
#[derive(Clone, Copy)]
13-
pub enum RegistryCheckSupport {
14-
/// Registry supports all known checks.
15-
All,
16-
/// Registry supports all checks except the listed ids.
17-
AllExcept(&'static [CheckId]),
18-
}
19-
2011
/// Runtime registry catalog built from app-registered definitions.
2112
#[derive(Clone)]
2213
pub struct RegistryCatalog {
@@ -65,14 +56,14 @@ impl RegistryCatalog {
6556
registry_definitions()
6657
.iter()
6758
.flat_map(|def| {
68-
let support_mode = crate::app_registry_check_support(def.key);
59+
let excluded = def.excluded_checks;
6960
known_checks
7061
.iter()
7162
.copied()
7263
.map(move |check| CheckSupportRow {
7364
registry: def.key,
7465
check,
75-
supported: check_is_supported(support_mode, check),
66+
supported: check_is_supported(excluded, check),
7667
})
7768
})
7869
.collect()
@@ -87,8 +78,7 @@ pub fn register_default_catalog() -> RegistryCatalog {
8778
let mut plugins_by_key = HashMap::new();
8879
let known_checks = known_check_ids();
8980
for def in registry_definitions() {
90-
let support_mode = crate::app_registry_check_support(def.key);
91-
let supported_checks = supported_checks(support_mode, &known_checks);
81+
let supported_checks = supported_checks(def.excluded_checks, &known_checks);
9282
let plugin = Arc::new(RegisteredPlugin {
9383
key: def.key,
9484
client: (def.create_client)(),
@@ -173,7 +163,7 @@ pub fn default_package_registry_key() -> &'static str {
173163
registry_definitions()
174164
.first()
175165
.map(|def| def.key)
176-
.unwrap_or("npm")
166+
.expect("at least one registry must be registered")
177167
}
178168

179169
/// Returns the default lockfile registry key.
@@ -182,7 +172,7 @@ pub fn default_lockfile_registry_key() -> &'static str {
182172
.iter()
183173
.find(|def| def.create_lockfile_parser.is_some())
184174
.map(|def| def.key)
185-
.unwrap_or("npm")
175+
.expect("at least one lockfile-capable registry must be registered")
186176
}
187177

188178
#[derive(Clone)]
@@ -225,22 +215,19 @@ fn known_check_ids() -> Vec<CheckId> {
225215
.collect()
226216
}
227217

228-
fn supported_checks(mode: RegistryCheckSupport, known_checks: &[CheckId]) -> Vec<CheckId> {
218+
fn supported_checks(excluded: &[CheckId], known_checks: &[CheckId]) -> Vec<CheckId> {
229219
known_checks
230220
.iter()
231221
.copied()
232-
.filter(|check| check_is_supported(mode, check))
222+
.filter(|check| check_is_supported(excluded, check))
233223
.collect()
234224
}
235225

236-
fn check_is_supported(mode: RegistryCheckSupport, check: CheckId) -> bool {
226+
fn check_is_supported(excluded: &[CheckId], check: CheckId) -> bool {
237227
let normalized_check = normalize_check_id(check);
238-
match mode {
239-
RegistryCheckSupport::All => true,
240-
RegistryCheckSupport::AllExcept(disallowed) => !disallowed
241-
.iter()
242-
.any(|value| normalize_check_id(value) == normalized_check),
243-
}
228+
!excluded
229+
.iter()
230+
.any(|value| normalize_check_id(value) == normalized_check)
244231
}
245232

246233
#[cfg(test)]

0 commit comments

Comments
 (0)