Skip to content

Commit 557bd05

Browse files
Merge remote-tracking branch 'upstream/main' into dev
2 parents df10eec + 77afde7 commit 557bd05

2 files changed

Lines changed: 118 additions & 7 deletions

File tree

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
297297
model_flag_raw,
298298
permission_mode,
299299
output_format,
300-
} => print_status_snapshot(&model, model_flag_raw.as_deref(), permission_mode, output_format)?,
300+
allowed_tools,
301+
} => print_status_snapshot(
302+
&model,
303+
model_flag_raw.as_deref(),
304+
permission_mode,
305+
output_format,
306+
allowed_tools.as_ref(),
307+
)?,
301308
CliAction::Sandbox { output_format } => print_sandbox_status_snapshot(output_format)?,
302309
CliAction::Prompt {
303310
prompt,
@@ -446,6 +453,7 @@ enum CliAction {
446453
model_flag_raw: Option<String>,
447454
permission_mode: PermissionMode,
448455
output_format: CliOutputFormat,
456+
allowed_tools: Option<AllowedToolSet>,
449457
},
450458
Sandbox {
451459
output_format: CliOutputFormat,
@@ -792,9 +800,14 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
792800
if let Some(action) = parse_local_help_action(&rest) {
793801
return action;
794802
}
795-
if let Some(action) =
796-
parse_single_word_command_alias(&rest, &model, model_flag_raw.as_deref(), permission_mode_override, output_format)
797-
{
803+
if let Some(action) = parse_single_word_command_alias(
804+
&rest,
805+
&model,
806+
model_flag_raw.as_deref(),
807+
permission_mode_override,
808+
output_format,
809+
allowed_tools.clone(),
810+
) {
798811
return action;
799812
}
800813

@@ -999,6 +1012,7 @@ fn parse_single_word_command_alias(
9991012
model_flag_raw: Option<&str>,
10001013
permission_mode_override: Option<PermissionMode>,
10011014
output_format: CliOutputFormat,
1015+
allowed_tools: Option<AllowedToolSet>,
10021016
) -> Option<Result<CliAction, String>> {
10031017
if rest.is_empty() {
10041018
return None;
@@ -1043,6 +1057,7 @@ fn parse_single_word_command_alias(
10431057
model_flag_raw: model_flag_raw.map(str::to_string), // #148
10441058
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
10451059
output_format,
1060+
allowed_tools,
10461061
})),
10471062
"sandbox" => Some(Ok(CliAction::Sandbox { output_format })),
10481063
"doctor" => Some(Ok(CliAction::Doctor { output_format })),
@@ -3430,6 +3445,7 @@ fn run_resume_command(
34303445
default_permission_mode().as_str(),
34313446
&context,
34323447
None, // #148: resumed sessions don't have flag provenance
3448+
None,
34333449
)),
34343450
})
34353451
}
@@ -5737,6 +5753,7 @@ fn print_status_snapshot(
57375753
model_flag_raw: Option<&str>,
57385754
permission_mode: PermissionMode,
57395755
output_format: CliOutputFormat,
5756+
allowed_tools: Option<&AllowedToolSet>,
57405757
) -> Result<(), Box<dyn std::error::Error>> {
57415758
let usage = StatusUsage {
57425759
message_count: 0,
@@ -5770,6 +5787,7 @@ fn print_status_snapshot(
57705787
permission_mode.as_str(),
57715788
&context,
57725789
Some(&provenance),
5790+
allowed_tools,
57735791
))?
57745792
),
57755793
}
@@ -5787,6 +5805,7 @@ fn status_json_value(
57875805
// that don't have provenance (legacy resume paths) pass None, in which
57885806
// case both new fields are omitted.
57895807
provenance: Option<&ModelProvenance>,
5808+
allowed_tools: Option<&AllowedToolSet>,
57905809
) -> serde_json::Value {
57915810
// #143: top-level `status` marker so claws can distinguish
57925811
// a clean run from a degraded run (config parse failed but other fields
@@ -5796,6 +5815,7 @@ fn status_json_value(
57965815
let degraded = context.config_load_error.is_some();
57975816
let model_source = provenance.map(|p| p.source.as_str());
57985817
let model_raw = provenance.and_then(|p| p.raw.clone());
5818+
let allowed_tool_entries = allowed_tools.map(|tools| tools.iter().cloned().collect::<Vec<_>>());
57995819
json!({
58005820
"kind": "status",
58015821
"status": if degraded { "degraded" } else { "ok" },
@@ -5804,6 +5824,11 @@ fn status_json_value(
58045824
"model_source": model_source,
58055825
"model_raw": model_raw,
58065826
"permission_mode": permission_mode,
5827+
"allowed_tools": {
5828+
"source": if allowed_tools.is_some() { "flag" } else { "default" },
5829+
"restricted": allowed_tools.is_some(),
5830+
"entries": allowed_tool_entries,
5831+
},
58075832
"usage": {
58085833
"messages": usage.message_count,
58095834
"turns": usage.turns,
@@ -10143,6 +10168,18 @@ mod tests {
1014310168
assert!(error.contains("unsupported tool in --allowedTools: teleport"));
1014410169
}
1014510170

10171+
#[test]
10172+
fn rejects_empty_allowed_tools_flag() {
10173+
for raw in ["", ",,"] {
10174+
let error = parse_args(&["--allowedTools".to_string(), raw.to_string()])
10175+
.expect_err("empty allowedTools should be rejected");
10176+
assert!(
10177+
error.contains("--allowedTools was provided with no usable tool names"),
10178+
"unexpected error for {raw:?}: {error}"
10179+
);
10180+
}
10181+
}
10182+
1014610183
#[test]
1014710184
fn parses_system_prompt_options() {
1014810185
let args = vec![
@@ -10597,7 +10634,14 @@ mod tests {
1059710634
cumulative: runtime::TokenUsage::default(),
1059810635
estimated_tokens: 0,
1059910636
};
10600-
let json = super::status_json_value(Some("test-model"), usage, "workspace-write", &context, None);
10637+
let json = super::status_json_value(
10638+
Some("test-model"),
10639+
usage,
10640+
"workspace-write",
10641+
&context,
10642+
None,
10643+
None,
10644+
);
1060110645
assert_eq!(
1060210646
json.get("status").and_then(|v| v.as_str()),
1060310647
Some("degraded"),
@@ -10616,6 +10660,44 @@ mod tests {
1061610660
);
1061710661
assert!(json.get("workspace").is_some(), "workspace field still reported");
1061810662
assert!(json.get("sandbox").is_some(), "sandbox field still reported");
10663+
assert_eq!(
10664+
json.pointer("/allowed_tools/source").and_then(|v| v.as_str()),
10665+
Some("default"),
10666+
"default status should expose unrestricted tool source: {json}"
10667+
);
10668+
assert_eq!(
10669+
json.pointer("/allowed_tools/restricted").and_then(|v| v.as_bool()),
10670+
Some(false),
10671+
"default status should expose unrestricted tool state: {json}"
10672+
);
10673+
10674+
let allowed: super::AllowedToolSet = ["read_file", "grep_search"]
10675+
.into_iter()
10676+
.map(str::to_string)
10677+
.collect();
10678+
let restricted_json = super::status_json_value(
10679+
Some("test-model"),
10680+
usage,
10681+
"workspace-write",
10682+
&context,
10683+
None,
10684+
Some(&allowed),
10685+
);
10686+
assert_eq!(
10687+
restricted_json
10688+
.pointer("/allowed_tools/source")
10689+
.and_then(|v| v.as_str()),
10690+
Some("flag"),
10691+
"flag status should expose allow-list source: {restricted_json}"
10692+
);
10693+
assert_eq!(
10694+
restricted_json
10695+
.pointer("/allowed_tools/entries")
10696+
.and_then(|v| v.as_array())
10697+
.map(Vec::len),
10698+
Some(2),
10699+
"flag status should expose allow-list entries: {restricted_json}"
10700+
);
1061910701

1062010702
// Clean path: no config error → status: "ok", config_load_error: null.
1062110703
let clean_cwd = root.join("project-with-clean-config");
@@ -10624,8 +10706,14 @@ mod tests {
1062410706
super::status_context(None).expect("clean status_context should succeed")
1062510707
});
1062610708
assert!(clean_context.config_load_error.is_none());
10627-
let clean_json =
10628-
super::status_json_value(Some("test-model"), usage, "workspace-write", &clean_context, None);
10709+
let clean_json = super::status_json_value(
10710+
Some("test-model"),
10711+
usage,
10712+
"workspace-write",
10713+
&clean_context,
10714+
None,
10715+
None,
10716+
);
1062910717
assert_eq!(
1063010718
clean_json.get("status").and_then(|v| v.as_str()),
1063110719
Some("ok"),
@@ -10702,6 +10790,7 @@ mod tests {
1070210790
model_flag_raw: None, // #148: no --model flag passed
1070310791
permission_mode: PermissionMode::DangerFullAccess,
1070410792
output_format: CliOutputFormat::Text,
10793+
allowed_tools: None,
1070510794
}
1070610795
);
1070710796
assert_eq!(

rust/crates/tools/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ impl GlobalToolRegistry {
240240
}
241241
}
242242

243+
if allowed.is_empty() {
244+
return Err(format!(
245+
"--allowedTools was provided with no usable tool names (got `{}`). Omit the flag to allow all tools.",
246+
values.join(" ")
247+
));
248+
}
249+
243250
Ok(Some(allowed))
244251
}
245252

@@ -6904,6 +6911,21 @@ mod tests {
69046911
assert!(empty_permission.contains("unsupported plugin permission: "));
69056912
}
69066913

6914+
#[test]
6915+
fn allowed_tools_rejects_empty_token_lists() {
6916+
let registry = GlobalToolRegistry::builtin();
6917+
6918+
for raw in ["", ",,", " "] {
6919+
let err = registry
6920+
.normalize_allowed_tools(&[raw.to_string()])
6921+
.expect_err("empty allow-list input should be rejected");
6922+
assert!(
6923+
err.contains("--allowedTools was provided with no usable tool names"),
6924+
"unexpected error for {raw:?}: {err}"
6925+
);
6926+
}
6927+
}
6928+
69076929
#[test]
69086930
fn runtime_tools_extend_registry_definitions_permissions_and_search() {
69096931
let registry = GlobalToolRegistry::builtin()

0 commit comments

Comments
 (0)