Skip to content

Commit 031863a

Browse files
CopilotbadMade
andauthored
Merge origin/main: resolve session_store conflict, fix task.py self-import, remove unused sys import
Co-authored-by: badMade <106821302+badMade@users.noreply.github.com>
2 parents 24ba751 + 598b5a2 commit 031863a

6 files changed

Lines changed: 84 additions & 8 deletions

File tree

.jules/sentinel.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## 2024-04-10 - Path Traversal in Session Storage
2+
**Vulnerability:** Path traversal existed in both the Python (`src/session_store.py`) and Rust (`rust/crates/runtime/src/session_control.rs`) implementations because unsanitized `session_id` strings were used directly in file paths.
3+
**Learning:** Both reference implementations lacked central validation logic for system identifiers derived from external/user input.
4+
**Prevention:** Always validate and restrict identifier parameters (like session IDs) by checking for explicit disallow-lists (like path separators `/`, `\`, and directory traversal markers `.`, `..`) before using them in file operations.

docs/issue_task_proposals.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Codebase issue scan: proposed fix tasks
2+
3+
## 1) Naming consistency task
4+
- **Issue:** The HTTP user agent string uses `clawd-rust-tools/0.1`, while the codebase appears to use a mix of `claw*` and `clawd*` identifiers across repository/product naming, telemetry, env vars, and file prefixes.
5+
- **Impact:** Inconsistent telemetry/log tagging and harder grep/observability across services when searching for either `claw` or `clawd` identifiers.
6+
- **Task proposal:** Decide on the intended prefix convention for these identifiers (`claw*` vs `clawd*`), update the user-agent token to match that convention (ideally via a centrally-defined crate/version-derived user-agent constant), and update any tests that assert the old literal.
7+
- **Acceptance criteria:** The chosen naming convention is applied consistently to the user-agent token and any related references/tests updated by this task; no behavior changes beyond identifier consistency.
8+
9+
## 2) Bug fix task
10+
- **Issue:** `src.main` ignores filtering flags (e.g., `--no-plugin-commands`, `--no-skill-commands` for `commands`; `--simple-mode`, `--no-mcp`, `--deny-tool` for `tools`) when `--query ...` is used. This happens because the query paths call `render_command_index(...)` or `render_tool_index(...)` directly without applying the include/exclude filters.
11+
- **Impact:** CLI behavior is inconsistent and user-provided filtering flags are silently ignored for query usage.
12+
- **Task proposal:** Refactor command and tool index rendering so query and non-query paths share the same filtered source (e.g., call `get_commands(...)`/`get_tools(...)` first, then apply query search over that filtered set).
13+
- **Acceptance criteria:** `commands --query X --no-plugin-commands` and `tools --query Y --no-mcp` produce outputs that respect each exclusion flag.
14+
15+
## 3) Code comment/documentation discrepancy task
16+
- **Issue:** The lane completion module-level docs say completion is detected when "Code pushed (has output file)", but `detect_lane_completion(...)` does not inspect `output_file`; it only trusts the external boolean `has_pushed`.
17+
- **Impact:** The inline documentation overstates what this function validates itself, which can mislead future maintainers and reviewers.
18+
- **Task proposal:** Align docs/comments with implementation **or** update implementation to validate `output.output_file` semantics directly (and document exact source-of-truth for "pushed").
19+
- **Acceptance criteria:** Comments/docs and runtime behavior agree on how push status is determined.
20+
21+
## 4) Test improvement task
22+
- **Issue:** Snapshot-size tests in `tests/test_porting_workspace.py` use hard minimums (`>=150` commands, `>=100` tools, coverage thresholds), which are brittle for legitimate snapshot churn and can produce noisy failures.
23+
- **Impact:** Reduced test signal quality; harmless snapshot updates can fail CI even when behavior remains correct.
24+
- **Task proposal:** Replace absolute magic-number thresholds with invariant-based assertions (e.g., non-empty snapshots, monotonic schema checks, command/tool lookup sanity) plus a single snapshot-contract check tied to explicit version metadata.
25+
- **Acceptance criteria:** Tests fail only on semantic regressions, not ordinary curated snapshot updates.

patch_pr.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import sys
2-
31

42
def replace_in_file(filepath, search_str, replace_str):
53
with open(filepath, "r") as f:

rust/crates/runtime/src/session_control.rs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,17 @@ impl SessionStore {
7474
&self.workspace_root
7575
}
7676

77-
#[must_use]
78-
pub fn create_handle(&self, session_id: &str) -> SessionHandle {
77+
pub fn create_handle(&self, session_id: &str) -> Result<SessionHandle, SessionControlError> {
78+
if !is_valid_session_id(session_id) {
79+
return Err(SessionControlError::Format(format!(
80+
"Invalid session ID: {session_id}"
81+
)));
82+
}
7983
let id = session_id.to_string();
8084
let path = self
8185
.sessions_root
8286
.join(format!("{id}.{PRIMARY_SESSION_EXTENSION}"));
83-
SessionHandle { id, path }
87+
Ok(SessionHandle { id, path })
8488
}
8589

8690
pub fn resolve_reference(&self, reference: &str) -> Result<SessionHandle, SessionControlError> {
@@ -116,6 +120,11 @@ impl SessionStore {
116120
}
117121

118122
pub fn resolve_managed_path(&self, session_id: &str) -> Result<PathBuf, SessionControlError> {
123+
if !is_valid_session_id(session_id) {
124+
return Err(SessionControlError::Format(format!(
125+
"Invalid session ID: {session_id}"
126+
)));
127+
}
119128
for extension in [PRIMARY_SESSION_EXTENSION, LEGACY_SESSION_EXTENSION] {
120129
let path = self.sessions_root.join(format!("{session_id}.{extension}"));
121130
if path.exists() {
@@ -223,7 +232,7 @@ impl SessionStore {
223232
) -> Result<ForkedManagedSession, SessionControlError> {
224233
let parent_session_id = session.session_id.clone();
225234
let forked = session.fork(branch_name);
226-
let handle = self.create_handle(&forked.session_id);
235+
let handle = self.create_handle(&forked.session_id)?;
227236
let branch_name = forked
228237
.fork
229238
.as_ref()
@@ -343,12 +352,25 @@ pub fn create_managed_session_handle_for(
343352
base_dir: impl AsRef<Path>,
344353
session_id: &str,
345354
) -> Result<SessionHandle, SessionControlError> {
355+
if !is_valid_session_id(session_id) {
356+
return Err(SessionControlError::Format(format!(
357+
"Invalid session ID: {session_id}"
358+
)));
359+
}
346360
let id = session_id.to_string();
347361
let path =
348362
managed_sessions_dir_for(base_dir)?.join(format!("{id}.{PRIMARY_SESSION_EXTENSION}"));
349363
Ok(SessionHandle { id, path })
350364
}
351365

366+
#[must_use]
367+
pub fn is_valid_session_id(session_id: &str) -> bool {
368+
if session_id.is_empty() || session_id == "." || session_id.contains("..") {
369+
return false;
370+
}
371+
!session_id.contains(['/', '\\'])
372+
}
373+
352374
pub fn resolve_session_reference(reference: &str) -> Result<SessionHandle, SessionControlError> {
353375
resolve_session_reference_for(env::current_dir()?, reference)
354376
}
@@ -397,6 +419,11 @@ pub fn resolve_managed_session_path_for(
397419
base_dir: impl AsRef<Path>,
398420
session_id: &str,
399421
) -> Result<PathBuf, SessionControlError> {
422+
if !is_valid_session_id(session_id) {
423+
return Err(SessionControlError::Format(format!(
424+
"Invalid session ID: {session_id}"
425+
)));
426+
}
400427
let directory = managed_sessions_dir_for(base_dir)?;
401428
for extension in [PRIMARY_SESSION_EXTENSION, LEGACY_SESSION_EXTENSION] {
402429
let path = directory.join(format!("{session_id}.{extension}"));
@@ -713,7 +740,9 @@ mod tests {
713740
session
714741
.push_user_text(text)
715742
.expect("session message should save");
716-
let handle = store.create_handle(&session.session_id);
743+
let handle = store
744+
.create_handle(&session.session_id)
745+
.expect("handle should create");
717746
let session = session.with_persistence_path(handle.path.clone());
718747
session
719748
.save_to_path(&handle.path)

src/session_store.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,19 @@ class StoredSession:
1616
DEFAULT_SESSION_DIR = Path(".port_sessions")
1717

1818

19+
def validate_session_id(session_id: str) -> None:
20+
if "/" in session_id or "\\" in session_id:
21+
raise ValueError(f"Invalid session ID: contains path separators ({session_id})")
22+
if session_id in (".", ".."):
23+
raise ValueError(f"Invalid session ID: cannot be '.' or '..' ({session_id})")
24+
if ".." in session_id:
25+
raise ValueError(
26+
f"Invalid session ID: contains directory traversal ('..') ({session_id})"
27+
)
28+
29+
1930
def save_session(session: StoredSession, directory: Path | None = None) -> Path:
31+
validate_session_id(session.session_id)
2032
target_dir = directory or DEFAULT_SESSION_DIR
2133
target_dir.mkdir(parents=True, exist_ok=True)
2234
path = target_dir / f"{session.session_id}.json"
@@ -25,6 +37,7 @@ def save_session(session: StoredSession, directory: Path | None = None) -> Path:
2537

2638

2739
def load_session(session_id: str, directory: Path | None = None) -> StoredSession:
40+
validate_session_id(session_id)
2841
target_dir = directory or DEFAULT_SESSION_DIR
2942
data = json.loads((target_dir / f"{session_id}.json").read_text())
3043
return StoredSession(

src/task.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
from __future__ import annotations
22

3-
from .task import PortingTask
3+
from dataclasses import dataclass
4+
5+
6+
@dataclass(frozen=True)
7+
class PortingTask:
8+
name: str
9+
description: str
10+
411

512
__all__ = ["PortingTask"]

0 commit comments

Comments
 (0)