Skip to content

Commit 650f216

Browse files
committed
Worked for 7m 39s
Stdio-only enforcement is now covered and passing across both repos, with local-only MCP behavior preserved and hai.ai URLs unchanged. What’s covered haisdk Rust bridge ignores JACS_MCP_ARGS and forces empty args (stdio-local): main.rs:1264 haisdk Rust tests added for that behavior: main.rs:1334, main.rs:1356 haisdk docs updated to state JACS_MCP_ARGS is ignored: README.md:31 JACS CLI mcp run no longer accepts forwarded runtime args, only optional --bin: cli.rs:1053, cli.rs:1740 JACS tests added for help/output contract and rejection of --transport: cli_tests.rs:1171, cli_tests.rs:1184 JACS README updated with stdio-only statement: README.md:74 Existing Node/Python passthrough enforcement and tests were also validated: Node logic/tests: jacs.ts:25, jacs-passthrough.test.ts:88 Python logic/tests: cli.py:292, test_cli_passthrough.py:81 Test results (current run) haisdk/node: npm test → 221 passed, 11 skipped haisdk/python: uv run pytest -q → 203 passed, 1 skipped haisdk/go: go test ./... → pass haisdk/rust: cargo test -q → pass (all crates/tests completed) JACS/jacs: cargo test --features cli --test cli_tests → 35 passed, 0 failed jacs mcp run --help now shows only --bin under stdio usage There is no repo-wide numeric coverage gate configured today, but the relevant stdio/local-mode paths now have direct, useful tests and all suites pass.
1 parent c9217d3 commit 650f216

File tree

13 files changed

+842
-95
lines changed

13 files changed

+842
-95
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ jacs mcp run
7171
jacs mcp install --from-cargo
7272
```
7373

74+
`jacs mcp run` is local stdio-only transport. Runtime transport override args are intentionally not accepted.
75+
7476
### Homebrew (macOS)
7577

7678
```bash

jacs-mcp/src/jacs_tools.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ fn update_embedded_state_content(doc: &mut serde_json::Value, new_content: &str)
115115
new_hash
116116
}
117117

118+
/// Extract verification validity from `verify_a2a_artifact` details JSON.
119+
/// Defaults to `false` on malformed/missing fields to avoid optimistic trust.
120+
fn extract_verify_a2a_valid(details_json: &str) -> bool {
121+
serde_json::from_str::<serde_json::Value>(details_json)
122+
.ok()
123+
.and_then(|v| v.get("valid").and_then(|b| b.as_bool()))
124+
.unwrap_or(false)
125+
}
126+
118127
// =============================================================================
119128
// Request/Response Types
120129
// =============================================================================
@@ -3326,11 +3335,7 @@ impl JacsMcpServer {
33263335

33273336
match self.agent.verify_a2a_artifact(&params.wrapped_artifact) {
33283337
Ok(details_json) => {
3329-
// Check if the result indicates valid
3330-
let valid = serde_json::from_str::<serde_json::Value>(&details_json)
3331-
.ok()
3332-
.and_then(|v| v.get("valid").and_then(|b| b.as_bool()))
3333-
.unwrap_or(true); // Default to true if field missing but no error
3338+
let valid = extract_verify_a2a_valid(&details_json);
33343339
let result = VerifyA2aArtifactResult {
33353340
success: true,
33363341
valid,
@@ -4012,6 +4017,21 @@ mod tests {
40124017
assert!(validate_agent_id("550e8400-e29b-41d4-a716").is_err()); // Too short
40134018
}
40144019

4020+
#[test]
4021+
fn test_extract_verify_a2a_valid_true() {
4022+
assert!(extract_verify_a2a_valid(r#"{"valid":true}"#));
4023+
}
4024+
4025+
#[test]
4026+
fn test_extract_verify_a2a_valid_missing_defaults_false() {
4027+
assert!(!extract_verify_a2a_valid(r#"{"status":"ok"}"#));
4028+
}
4029+
4030+
#[test]
4031+
fn test_extract_verify_a2a_valid_invalid_json_defaults_false() {
4032+
assert!(!extract_verify_a2a_valid("not-json"));
4033+
}
4034+
40154035
#[test]
40164036
fn test_is_registration_allowed_default() {
40174037
// When env var is not set, should return false

jacs/src/agent/payloads.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,15 @@ impl PayloadTraits for Agent {
8282
let date = self.get_document_signature_date(&document_key)?;
8383
let agent_id = self.get_document_signature_agent_id(&document_key)?;
8484

85-
let max_replay_seconds = max_replay_time_delta_seconds.unwrap_or(1);
85+
// Default payload freshness window: 5 minutes.
86+
// Can be overridden per call, or globally with JACS_PAYLOAD_MAX_REPLAY_SECONDS.
87+
let max_replay_seconds = max_replay_time_delta_seconds
88+
.or_else(|| {
89+
std::env::var("JACS_PAYLOAD_MAX_REPLAY_SECONDS")
90+
.ok()
91+
.and_then(|v| v.parse::<u64>().ok())
92+
})
93+
.unwrap_or(300);
8694
let current_time = std::time::SystemTime::now()
8795
.duration_since(std::time::UNIX_EPOCH)?
8896
.as_secs();

jacs/src/bin/cli.rs

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use clap::{Arg, ArgAction, Command, crate_name, value_parser};
1+
use clap::{crate_name, value_parser, Arg, ArgAction, Command};
22

3-
use jacs::agent::Agent;
43
use jacs::agent::boilerplate::BoilerPlate;
54
use jacs::agent::document::DocumentTraits;
5+
use jacs::agent::Agent;
66
use jacs::cli_utils::create::handle_agent_create;
77
use jacs::cli_utils::create::handle_config_create;
88
use jacs::cli_utils::default_set_file_list;
@@ -13,7 +13,7 @@ use jacs::cli_utils::document::{
1313
use jacs::config::load_config_12factor_optional;
1414
// use jacs::create_task; // unused
1515
use jacs::dns::bootstrap as dns_bootstrap;
16-
use jacs::shutdown::{ShutdownGuard, install_signal_handler};
16+
use jacs::shutdown::{install_signal_handler, ShutdownGuard};
1717
use jacs::{load_agent, load_agent_with_dns_strict};
1818

1919
use reqwest;
@@ -27,7 +27,8 @@ use std::time::{SystemTime, UNIX_EPOCH};
2727

2828
const CLI_PASSWORD_FILE_ENV: &str = "JACS_PASSWORD_FILE";
2929
const DEFAULT_LEGACY_PASSWORD_FILE: &str = "./jacs_keys/.jacs_password";
30-
const DEFAULT_MCP_RELEASE_BASE_URL: &str = "https://github.com/HumanAssisted/JACS/releases/download";
30+
const DEFAULT_MCP_RELEASE_BASE_URL: &str =
31+
"https://github.com/HumanAssisted/JACS/releases/download";
3132
const DEFAULT_MCP_RELEASE_TAG_PREFIX: &str = "cli/v";
3233

3334
fn quickstart_password_bootstrap_help() -> &'static str {
@@ -458,9 +459,8 @@ fn install_jacs_mcp_prebuilt(
458459
Ok(())
459460
}
460461

461-
fn run_jacs_mcp_binary(binary: &str, forwarded_args: &[String]) -> Result<(), Box<dyn Error>> {
462+
fn run_jacs_mcp_binary(binary: &str) -> Result<(), Box<dyn Error>> {
462463
let status = std::process::Command::new(binary)
463-
.args(forwarded_args)
464464
.status()
465465
.map_err(|e| -> Box<dyn Error> {
466466
Box::new(std::io::Error::other(format!(
@@ -1057,13 +1057,6 @@ pub fn main() -> Result<(), Box<dyn Error>> {
10571057
.long("bin")
10581058
.value_parser(value_parser!(String))
10591059
.help("Path to jacs-mcp binary (default: JACS_MCP_BIN or PATH)"),
1060-
)
1061-
.arg(
1062-
Arg::new("args")
1063-
.help("Arguments forwarded to jacs-mcp")
1064-
.num_args(0..)
1065-
.trailing_var_arg(true)
1066-
.allow_hyphen_values(true),
10671060
),
10681061
),
10691062
)
@@ -1713,12 +1706,16 @@ pub fn main() -> Result<(), Box<dyn Error>> {
17131706
},
17141707
Some(("mcp", mcp_matches)) => match mcp_matches.subcommand() {
17151708
Some(("install", install_matches)) => {
1716-
let version = install_matches.get_one::<String>("version").map(|s| s.as_str());
1709+
let version = install_matches
1710+
.get_one::<String>("version")
1711+
.map(|s| s.as_str());
17171712
let force = *install_matches.get_one::<bool>("force").unwrap_or(&false);
17181713
let from_cargo = *install_matches
17191714
.get_one::<bool>("from-cargo")
17201715
.unwrap_or(&false);
1721-
let bin_dir = install_matches.get_one::<String>("bin-dir").map(|s| s.as_str());
1716+
let bin_dir = install_matches
1717+
.get_one::<String>("bin-dir")
1718+
.map(|s| s.as_str());
17221719
let url_override = install_matches.get_one::<String>("url").map(|s| s.as_str());
17231720
let dry_run = *install_matches.get_one::<bool>("dry-run").unwrap_or(&false);
17241721

@@ -1740,19 +1737,14 @@ pub fn main() -> Result<(), Box<dyn Error>> {
17401737
})
17411738
.unwrap_or_else(|| "jacs-mcp".to_string());
17421739

1743-
let forwarded_args: Vec<String> = run_matches
1744-
.get_many::<String>("args")
1745-
.map(|vals| vals.map(|v| v.to_string()).collect())
1746-
.unwrap_or_default();
1747-
1748-
run_jacs_mcp_binary(&binary, &forwarded_args)?;
1740+
run_jacs_mcp_binary(&binary)?;
17491741
}
17501742
_ => println!("please enter subcommand see jacs mcp --help"),
17511743
},
17521744
Some(("a2a", a2a_matches)) => match a2a_matches.subcommand() {
17531745
Some(("assess", assess_matches)) => {
1746+
use jacs::a2a::trust::{assess_a2a_agent, A2ATrustPolicy};
17541747
use jacs::a2a::AgentCard;
1755-
use jacs::a2a::trust::{A2ATrustPolicy, assess_a2a_agent};
17561748

17571749
let source = assess_matches.get_one::<String>("source").unwrap();
17581750
let policy_str = assess_matches
@@ -1865,8 +1857,8 @@ pub fn main() -> Result<(), Box<dyn Error>> {
18651857
println!(" Added to local trust store with key: {}", key);
18661858
}
18671859
Some(("discover", discover_matches)) => {
1860+
use jacs::a2a::trust::{assess_a2a_agent, A2ATrustPolicy};
18681861
use jacs::a2a::AgentCard;
1869-
use jacs::a2a::trust::{A2ATrustPolicy, assess_a2a_agent};
18701862

18711863
let base_url = discover_matches.get_one::<String>("url").unwrap();
18721864
let json_output = *discover_matches.get_one::<bool>("json").unwrap_or(&false);

jacs/tests/cli_tests.rs

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,9 @@ fn test_mcp_help_shows_install_and_run() -> Result<(), Box<dyn Error>> {
11001100
.success()
11011101
.stdout(predicate::str::contains("install"))
11021102
.stdout(predicate::str::contains("run"))
1103-
.stdout(predicate::str::contains("Install and run the JACS MCP server"));
1103+
.stdout(predicate::str::contains(
1104+
"Install and run the JACS MCP server",
1105+
));
11041106
Ok(())
11051107
}
11061108

@@ -1110,9 +1112,13 @@ fn test_mcp_install_dry_run_shows_prebuilt_plan() -> Result<(), Box<dyn Error>>
11101112
cmd.arg("mcp").arg("install").arg("--dry-run");
11111113
cmd.assert()
11121114
.success()
1113-
.stdout(predicate::str::contains("Dry run: MCP prebuilt install plan"))
1115+
.stdout(predicate::str::contains(
1116+
"Dry run: MCP prebuilt install plan",
1117+
))
11141118
.stdout(predicate::str::contains("jacs-mcp-"))
1115-
.stdout(predicate::str::contains("github.com/HumanAssisted/JACS/releases/download"));
1119+
.stdout(predicate::str::contains(
1120+
"github.com/HumanAssisted/JACS/releases/download",
1121+
));
11161122
Ok(())
11171123
}
11181124

@@ -1124,9 +1130,9 @@ fn test_mcp_install_dry_run_custom_url() -> Result<(), Box<dyn Error>> {
11241130
.arg("--dry-run")
11251131
.arg("--url")
11261132
.arg("https://example.invalid/jacs-mcp.tar.gz");
1127-
cmd.assert()
1128-
.success()
1129-
.stdout(predicate::str::contains("https://example.invalid/jacs-mcp.tar.gz"));
1133+
cmd.assert().success().stdout(predicate::str::contains(
1134+
"https://example.invalid/jacs-mcp.tar.gz",
1135+
));
11301136
Ok(())
11311137
}
11321138

@@ -1142,7 +1148,9 @@ fn test_mcp_install_from_cargo_dry_run_shows_cargo_plan() -> Result<(), Box<dyn
11421148
cmd.assert()
11431149
.success()
11441150
.stdout(predicate::str::contains("Dry run: MCP cargo install plan"))
1145-
.stdout(predicate::str::contains("cargo install jacs-mcp --locked --version 0.8.0"));
1151+
.stdout(predicate::str::contains(
1152+
"cargo install jacs-mcp --locked --version 0.8.0",
1153+
));
11461154
Ok(())
11471155
}
11481156

@@ -1153,8 +1161,31 @@ fn test_mcp_run_missing_binary_shows_install_hint() -> Result<(), Box<dyn Error>
11531161
.arg("run")
11541162
.arg("--bin")
11551163
.arg("/definitely/not/a/real/jacs-mcp");
1164+
cmd.assert().failure().stderr(predicate::str::contains(
1165+
"Install it with `jacs mcp install`",
1166+
));
1167+
Ok(())
1168+
}
1169+
1170+
#[test]
1171+
fn test_mcp_run_help_mentions_stdio_and_no_forwarded_args() -> Result<(), Box<dyn Error>> {
1172+
let mut cmd = Command::cargo_bin("jacs")?;
1173+
cmd.arg("mcp").arg("run").arg("--help");
11561174
cmd.assert()
1157-
.failure()
1158-
.stderr(predicate::str::contains("Install it with `jacs mcp install`"));
1175+
.success()
1176+
.stdout(predicate::str::contains("stdio transport"))
1177+
.stdout(predicate::str::contains("--bin <bin>"))
1178+
.stdout(predicate::str::contains("[args]").not())
1179+
.stdout(predicate::str::contains("Arguments forwarded").not());
1180+
Ok(())
1181+
}
1182+
1183+
#[test]
1184+
fn test_mcp_run_rejects_forwarded_runtime_args() -> Result<(), Box<dyn Error>> {
1185+
let mut cmd = Command::cargo_bin("jacs")?;
1186+
cmd.arg("mcp").arg("run").arg("--transport").arg("http");
1187+
cmd.assert().failure().stderr(predicate::str::contains(
1188+
"unexpected argument '--transport'",
1189+
));
11591190
Ok(())
11601191
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use jacs::agent::payloads::PayloadTraits;
2+
use serde_json::json;
3+
use serial_test::serial;
4+
use std::time::Duration;
5+
6+
mod utils;
7+
use utils::load_test_agent_one;
8+
9+
struct EnvVarGuard {
10+
key: &'static str,
11+
previous: Option<String>,
12+
}
13+
14+
impl EnvVarGuard {
15+
fn set(key: &'static str, value: &str) -> Self {
16+
let previous = std::env::var(key).ok();
17+
// SAFETY: this test module is serial; env mutation is isolated.
18+
unsafe {
19+
std::env::set_var(key, value);
20+
}
21+
Self { key, previous }
22+
}
23+
24+
fn unset(key: &'static str) -> Self {
25+
let previous = std::env::var(key).ok();
26+
// SAFETY: this test module is serial; env mutation is isolated.
27+
unsafe {
28+
std::env::remove_var(key);
29+
}
30+
Self { key, previous }
31+
}
32+
}
33+
34+
impl Drop for EnvVarGuard {
35+
fn drop(&mut self) {
36+
// SAFETY: this test module is serial; env mutation is isolated.
37+
unsafe {
38+
if let Some(value) = &self.previous {
39+
std::env::set_var(self.key, value);
40+
} else {
41+
std::env::remove_var(self.key);
42+
}
43+
}
44+
}
45+
}
46+
47+
#[test]
48+
#[serial]
49+
fn verify_payload_default_window_allows_recent_message() {
50+
let _guard = EnvVarGuard::unset("JACS_PAYLOAD_MAX_REPLAY_SECONDS");
51+
let mut agent = load_test_agent_one();
52+
let signed = agent
53+
.sign_payload(json!({ "test": "default-window" }))
54+
.expect("payload signing should succeed");
55+
56+
std::thread::sleep(Duration::from_secs(2));
57+
58+
let payload = agent
59+
.verify_payload(signed, None)
60+
.expect("default 5-minute replay window should accept a 2-second-old payload");
61+
assert_eq!(payload["test"], "default-window");
62+
}
63+
64+
#[test]
65+
#[serial]
66+
fn verify_payload_env_override_can_be_strict() {
67+
let _guard = EnvVarGuard::set("JACS_PAYLOAD_MAX_REPLAY_SECONDS", "1");
68+
let mut agent = load_test_agent_one();
69+
let signed = agent
70+
.sign_payload(json!({ "test": "strict-env-window" }))
71+
.expect("payload signing should succeed");
72+
73+
std::thread::sleep(Duration::from_secs(2));
74+
75+
let err = agent
76+
.verify_payload(signed, None)
77+
.expect_err("strict 1-second env replay window should reject old payload");
78+
assert!(
79+
err.to_string().contains("Signature too old"),
80+
"unexpected error: {}",
81+
err
82+
);
83+
}
84+
85+
#[test]
86+
#[serial]
87+
fn verify_payload_explicit_argument_overrides_env_window() {
88+
let _guard = EnvVarGuard::set("JACS_PAYLOAD_MAX_REPLAY_SECONDS", "1");
89+
let mut agent = load_test_agent_one();
90+
let signed = agent
91+
.sign_payload(json!({ "test": "explicit-override" }))
92+
.expect("payload signing should succeed");
93+
94+
std::thread::sleep(Duration::from_secs(2));
95+
96+
let payload = agent
97+
.verify_payload(signed, Some(300))
98+
.expect("explicit replay window should override strict env setting");
99+
assert_eq!(payload["test"], "explicit-override");
100+
}

0 commit comments

Comments
 (0)