fix(acp): reject a relative cwd when a session starts
ACP requires cwd and every additionalDirectories entry to be absolute. session/new, session/load and session/fork took whatever they were given, and the later set_current_dir resolved a relative path against the directory the editor spawned the process in. All three now return -32602 before any session state is created. Fixes #136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seto Elkahfi committed
Oct 4, 2026 at 23:59 UTC
a3613f888372b0cdb4fc7c6510544024d7d76872
2 files changed
+62
CHANGELOG.md
+8
@@ -9,6 +9,14 @@
9
the wire. The `unstable_auth_methods` feature is gone because agent auth
10
methods are stable in 2.x.
11
12
+### Fixed
13
+
14
+- **A relative `cwd` is rejected when a session starts.** ACP requires the
15
+ working directory and every `additionalDirectories` entry to be absolute.
16
+ `session/new`, `session/load` and `session/fork` used to accept a relative
17
+ one and resolve it against wherever the editor spawned the process. They now
18
+ answer with an invalid-params error (#136).
19
+
20
## 1.5.13
21
22
### Added
src/main.rs
+54
@@ -1472,6 +1472,33 @@ impl SiGitAgent {
1472
}
1473
}
1474
1475
+/// Reject session roots that are not absolute paths.
1476
+///
1477
+/// ACP requires `cwd` and every `additionalDirectories` entry to be absolute.
1478
+/// A relative one would otherwise be resolved against wherever the editor
1479
+/// happened to spawn this process, which is not the project the user opened.
1480
+fn validate_session_roots(
1481
+ cwd: &std::path::Path,
1482
+ additional: &[PathBuf],
1483
+) -> agent_client_protocol::Result<()> {
1484
+ if !cwd.is_absolute() {
1485
+ return Err(agent_client_protocol::Error::new(
1486
+ -32602,
1487
+ format!("cwd must be an absolute path, got {}", cwd.display()),
1488
+ ));
1489
+ }
1490
+ if let Some(relative) = additional.iter().find(|dir| !dir.is_absolute()) {
1491
+ return Err(agent_client_protocol::Error::new(
1492
+ -32602,
1493
+ format!(
1494
+ "additionalDirectories entries must be absolute paths, got {}",
1495
+ relative.display()
1496
+ ),
1497
+ ));
1498
+ }
1499
+ Ok(())
1500
+}
1501
+
1502
// ── ACP handler implementations ───────────────────────────────────────────────
1503
1504
impl SiGitAgent {
@@ -1849,6 +1876,8 @@ impl SiGitAgent {
1876
cx: &ConnectionTo<Client>,
1877
args: LoadSessionRequest,
1878
) -> agent_client_protocol::Result<LoadSessionResponse> {
1879
+ validate_session_roots(&args.cwd, &args.additional_directories)?;
1880
+
1881
log::info!(
1882
"load_session: id={}, cwd={}, additional_directories={:?}",
1883
args.session_id,
@@ -1919,6 +1948,8 @@ impl SiGitAgent {
1948
cx: &ConnectionTo<Client>,
1949
args: ForkSessionRequest,
1950
) -> agent_client_protocol::Result<ForkSessionResponse> {
1951
+ validate_session_roots(&args.cwd, &args.additional_directories)?;
1952
+
1953
let new_id = SessionId::new(uuid::Uuid::new_v4().to_string());
1954
log::info!(
1955
"fork_session: from={} new={new_id}, cwd={}, additional_directories={:?}",
@@ -1986,6 +2017,8 @@ impl SiGitAgent {
2017
cx: &ConnectionTo<Client>,
2018
args: NewSessionRequest,
2019
) -> agent_client_protocol::Result<NewSessionResponse> {
2020
+ validate_session_roots(&args.cwd, &args.additional_directories)?;
2021
+
2022
let session_id = SessionId::new(uuid::Uuid::new_v4().to_string());
2023
log::info!(
2024
"new_session: id={session_id}, cwd={}, additional_directories={:?}",
@@ -5493,6 +5526,27 @@ mod tests {
5526
assert_eq!(longest_backtick_run("one ` two `` three ``` four"), 3);
5527
}
5528
5529
+ #[test]
5530
+ fn session_roots_must_be_absolute() {
5531
+ let absolute = std::env::temp_dir();
5532
+ let relative = PathBuf::from("relative").join("dir");
5533
+
5534
+ assert!(validate_session_roots(&absolute, &[]).is_ok());
5535
+ assert!(validate_session_roots(&absolute, std::slice::from_ref(&absolute)).is_ok());
5536
+
5537
+ let error = validate_session_roots(&relative, &[]).unwrap_err();
5538
+ assert_eq!(i32::from(error.code), -32602);
5539
+ assert!(error.message.contains("cwd"), "{}", error.message);
5540
+
5541
+ let error = validate_session_roots(&absolute, &[absolute.clone(), relative]).unwrap_err();
5542
+ assert_eq!(i32::from(error.code), -32602);
5543
+ assert!(
5544
+ error.message.contains("additionalDirectories"),
5545
+ "{}",
5546
+ error.message
5547
+ );
5548
+ }
5549
+
5550
#[test]
5551
fn tool_call_locations_require_a_known_tool_and_string_path() {
5552
assert!(tool_call_locations("read_file", "{not json").is_empty());