Skip to content

Commit db4264c

Browse files
committed
Gate morph_edit_file mentions in system prompt and bash hint on MORPH_API_KEY presence
1 parent 33b8ab6 commit db4264c

3 files changed

Lines changed: 38 additions & 22 deletions

File tree

src/repl/conversation.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ impl ConversationHistory {
5050
"- When creating or editing code, use the write_file tool"
5151
};
5252

53+
let write_scope_tools = if has_morph {
54+
"write_file, edit_file, and morph_edit_file"
55+
} else {
56+
"write_file and edit_file"
57+
};
58+
5359
let mut system_text = format!(
5460
r#"You are Sofos, an AI coding assistant. You have access to tools that allow you to:
5561
{}
@@ -72,7 +78,7 @@ Prefer read-only commands and dry-runs; if a potentially destructive action seem
7278
7379
Outside Workspace Access (three separate scopes, each prompted independently):
7480
- Read scope: read_file and list_directory can access absolute or ~/ paths. If not pre-configured, the user is prompted to allow access and can optionally remember the decision.
75-
- Write scope: write_file, edit_file, and morph_edit_file can write to absolute or ~/ paths. The user is prompted for Write access separately from Read.
81+
- Write scope: {} can write to absolute or ~/ paths. The user is prompted for Write access separately from Read.
7682
- Bash scope: bash commands can reference absolute or ~/ paths. The user is prompted for Bash path access. Use absolute paths (not ..) for external directories.
7783
- All three scopes are independent — Read access does not grant Write or Bash access.
7884
- When accessing external paths, just use the absolute or ~/ path directly. If not yet allowed, the user will be prompted interactively.
@@ -105,7 +111,8 @@ Your goal is to help users with coding tasks efficiently and accurately.
105111
Always use the metric system for all measurements. If the user uses other units, convert them and answer in metric.
106112
Show imperial units only when the user explicitly asks for them."#,
107113
features.join("\n"),
108-
edit_instruction
114+
edit_instruction,
115+
write_scope_tools
109116
);
110117

111118
// Append custom instructions if provided

src/tools/bashexec.rs

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ pub struct BashExecutor {
8989
workspace: PathBuf,
9090
/// Whether interactive prompts (stdin) are available
9191
interactive: bool,
92+
/// Whether `morph_edit_file` is exposed (drives error-message hints)
93+
has_morph: bool,
9294
/// Session-scoped temporary permissions (not persisted to config)
9395
session_allowed: Arc<Mutex<HashSet<String>>>,
9496
session_denied: Arc<Mutex<HashSet<String>>>,
@@ -98,10 +100,11 @@ pub struct BashExecutor {
98100
}
99101

100102
impl BashExecutor {
101-
pub fn new(workspace: PathBuf, interactive: bool) -> Result<Self> {
103+
pub fn new(workspace: PathBuf, interactive: bool, has_morph: bool) -> Result<Self> {
102104
Ok(Self {
103105
workspace,
104106
interactive,
107+
has_morph,
105108
session_allowed: Arc::new(Mutex::new(HashSet::new())),
106109
session_denied: Arc::new(Mutex::new(HashSet::new())),
107110
bash_path_session_allowed: Arc::new(Mutex::new(HashSet::new())),
@@ -634,10 +637,15 @@ impl BashExecutor {
634637
if command_without_stderr_redirect.contains('>')
635638
|| command_without_stderr_redirect.contains(">>")
636639
{
640+
let edit_hint = if self.has_morph {
641+
"edit_file/morph_edit_file"
642+
} else {
643+
"edit_file"
644+
};
637645
return format!(
638646
"Command '{}' contains output redirection ('>' or '>>')\n\
639-
Hint: Use write_file tool to create or edit_file/morph_edit_file to modify files. Note: '2>&1' is allowed.",
640-
command
647+
Hint: Use write_file tool to create or {} to modify files. Note: '2>&1' is allowed.",
648+
command, edit_hint
641649
);
642650
}
643651

@@ -804,7 +812,7 @@ mod tests {
804812

805813
#[test]
806814
fn test_safe_commands() {
807-
let executor = BashExecutor::new(PathBuf::from("."), false).unwrap();
815+
let executor = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
808816

809817
// Note: These tests check the command structure safety only
810818
// Actual permission checking is done by PermissionManager
@@ -825,7 +833,7 @@ mod tests {
825833

826834
#[test]
827835
fn test_unsafe_command_structures() {
828-
let executor = BashExecutor::new(PathBuf::from("."), false).unwrap();
836+
let executor = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
829837

830838
// Test structural safety issues (not permission-based)
831839
assert!(!executor.is_safe_command_structure("echo hello > file.txt"));
@@ -838,7 +846,7 @@ mod tests {
838846

839847
#[test]
840848
fn test_path_traversal_blocked() {
841-
let executor = BashExecutor::new(PathBuf::from("."), false).unwrap();
849+
let executor = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
842850

843851
assert!(!executor.is_safe_command_structure("cat ../file.txt"));
844852
assert!(!executor.is_safe_command_structure("ls ../../etc"));
@@ -851,7 +859,7 @@ mod tests {
851859
fn test_absolute_paths_pass_structural_check() {
852860
// Absolute paths are no longer blocked by is_safe_command_structure.
853861
// They are handled by check_bash_external_paths which asks the user.
854-
let executor = BashExecutor::new(PathBuf::from("."), false).unwrap();
862+
let executor = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
855863

856864
assert!(executor.is_safe_command_structure("/bin/ls"));
857865
assert!(executor.is_safe_command_structure("cat /etc/passwd"));
@@ -864,7 +872,7 @@ mod tests {
864872
use tempfile;
865873

866874
let temp_dir = tempfile::tempdir().unwrap();
867-
let executor = BashExecutor::new(temp_dir.path().to_path_buf(), false).unwrap();
875+
let executor = BashExecutor::new(temp_dir.path().to_path_buf(), false, false).unwrap();
868876

869877
let result = executor.execute("seq 1 2000000");
870878

@@ -897,7 +905,7 @@ ask = []
897905
)
898906
.unwrap();
899907

900-
let executor = BashExecutor::new(temp_dir.path().to_path_buf(), false).unwrap();
908+
let executor = BashExecutor::new(temp_dir.path().to_path_buf(), false, false).unwrap();
901909

902910
// Even without creating the file, permission check should block before execution
903911
let result = executor.execute("cat ./test/secret.txt");
@@ -912,7 +920,7 @@ ask = []
912920

913921
#[test]
914922
fn test_safe_git_commands() {
915-
let executor = BashExecutor::new(PathBuf::from("."), false).unwrap();
923+
let executor = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
916924

917925
// Safe read-only git commands
918926
assert!(executor.is_safe_command_structure("git status"));
@@ -987,7 +995,7 @@ ask = []
987995

988996
#[test]
989997
fn test_dangerous_git_commands() {
990-
let executor = BashExecutor::new(PathBuf::from("."), false).unwrap();
998+
let executor = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
991999

9921000
// Remote operations (data leakage risk)
9931001
assert!(!executor.is_safe_command_structure("git push"));
@@ -1050,7 +1058,7 @@ ask = []
10501058

10511059
#[test]
10521060
fn test_git_commands_in_chains() {
1053-
let executor = BashExecutor::new(PathBuf::from("."), false).unwrap();
1061+
let executor = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
10541062

10551063
// Safe commands in chains
10561064
assert!(executor.is_safe_command_structure("git status && git log"));
@@ -1066,7 +1074,7 @@ ask = []
10661074

10671075
#[test]
10681076
fn test_error_messages_are_informative() {
1069-
let executor = BashExecutor::new(PathBuf::from("."), false).unwrap();
1077+
let executor = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
10701078

10711079
let reason = executor.get_git_rejection_reason("git push origin main");
10721080
assert!(reason.contains("git push origin main"));
@@ -1078,7 +1086,7 @@ ask = []
10781086
fn test_tilde_paths_pass_structural_check() {
10791087
// Tilde paths are no longer blocked by is_safe_command_structure.
10801088
// They are handled by check_bash_external_paths which asks the user.
1081-
let executor = BashExecutor::new(PathBuf::from("."), false).unwrap();
1089+
let executor = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
10821090

10831091
assert!(executor.is_safe_command_structure("ls ~/tmp"));
10841092
assert!(executor.is_safe_command_structure("cat ~/file.txt"));
@@ -1094,7 +1102,7 @@ ask = []
10941102
// way to prompt, so the executor returns a clear error pointing
10951103
// at the interactive-mode requirement.
10961104
let temp_dir = tempfile::tempdir().unwrap();
1097-
let executor = BashExecutor::new(temp_dir.path().to_path_buf(), false).unwrap();
1105+
let executor = BashExecutor::new(temp_dir.path().to_path_buf(), false, false).unwrap();
10981106

10991107
for cmd in &[
11001108
"git checkout main",
@@ -1131,7 +1139,7 @@ ask = []
11311139
// The error message mentions the dangerous-op reason, not the
11321140
// interactive-confirmation hint.
11331141
let temp_dir = tempfile::tempdir().unwrap();
1134-
let executor = BashExecutor::new(temp_dir.path().to_path_buf(), false).unwrap();
1142+
let executor = BashExecutor::new(temp_dir.path().to_path_buf(), false, false).unwrap();
11351143

11361144
for cmd in &["git checkout -f main", "git checkout -b new-branch"] {
11371145
let result = executor.execute(cmd);
@@ -1155,7 +1163,7 @@ ask = []
11551163
// `check_bash_external_path`, which deny in non-interactive mode
11561164
// when no grant is configured.
11571165
let temp_dir = tempfile::tempdir().unwrap();
1158-
let executor = BashExecutor::new(temp_dir.path().to_path_buf(), false).unwrap();
1166+
let executor = BashExecutor::new(temp_dir.path().to_path_buf(), false, false).unwrap();
11591167

11601168
let result = executor.execute("grep --include=/etc/passwd pattern .");
11611169

@@ -1172,7 +1180,7 @@ ask = []
11721180

11731181
#[test]
11741182
fn test_session_scoped_permissions_persist() {
1175-
let executor = BashExecutor::new(PathBuf::from("."), false).unwrap();
1183+
let executor = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
11761184

11771185
// Simulate adding a command to session_allowed
11781186
{
@@ -1201,7 +1209,7 @@ ask = []
12011209

12021210
#[test]
12031211
fn test_session_permissions_shared_across_clones() {
1204-
let executor1 = BashExecutor::new(PathBuf::from("."), false).unwrap();
1212+
let executor1 = BashExecutor::new(PathBuf::from("."), false, false).unwrap();
12051213
let executor2 = executor1.clone();
12061214

12071215
// Add permission via executor1

src/tools/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,10 +281,11 @@ impl ToolExecutor {
281281
}
282282
};
283283

284+
let has_morph = morph_client.is_some();
284285
Ok(Self {
285286
fs_tool: FileSystemTool::new(workspace.clone())?,
286287
code_search_tool,
287-
bash_executor: BashExecutor::new(workspace, interactive)?,
288+
bash_executor: BashExecutor::new(workspace, interactive, has_morph)?,
288289
morph_client,
289290
mcp_manager,
290291
safe_mode,

0 commit comments

Comments
 (0)