Simplify Galaxy permissions and preserve explicit user authorization

This commit is contained in:
2026-10-02 01:58:37 -05:00
parent 5a70995502
commit 2796fdd114
37 changed files with 1818 additions and 3530 deletions
+8
View File
@@ -251,6 +251,7 @@ fn prepare_rig_turn_for_provider(
tool_result_archive,
messages_sent,
global_rules,
permission_mode,
mcp_context,
..
} = params;
@@ -292,6 +293,13 @@ fn prepare_rig_turn_for_provider(
}
}
let mut system_prompt = build_system_prompt(&input, &tools, &global_rules, mode);
system_prompt.push_str(match permission_mode {
crate::ai::execution_profiles::PermissionMode::ReadOnly => "\n## Permission Mode: Read Only\nOnly inspection and search tools are available. Describe proposed changes without executing commands, editing, delegating, or invoking MCP/computer use.\n",
crate::ai::execution_profiles::PermissionMode::AskFirst => "\n## Permission Mode: Ask First\nGalaxy asks for approval before actions and remembers exact approved commands in their working directory. Do not request blanket permission in prose; submit the concrete tool action for the approval pane.\n",
crate::ai::execution_profiles::PermissionMode::GalaxyDecides => "\n## Permission Mode: Galaxy Decides\nGalaxy's independent terminal-use model resolves tool permissions without asking the user. Treat denials as binding. Revise the action scope or provide concrete missing justification addressing the stated risk, or use a safer approach. Never repeat an unchanged denied request, seek user approval to override it, or claim a denied action ran. Work autonomously and report concrete blockers when no authorized approach remains. For routine inspection, prefer the permission fast path: pwd, simple ls commands, git --version, git --no-pager -c core.fsmonitor=false status --short, or git --no-pager -c core.fsmonitor=false diff --no-ext-diff --no-textconv --stat (optionally --cached). Shell expansions, chaining, redirects and other command forms still require model review.\n",
});
system_prompt.push_str("Approval of a long-running command covers monitoring and interaction with that same conversation-owned command while you retain control. Use the existing command block; do not request permission again or restart the command just to interact with it. A new command requires its own permission decision.\n");
let mut new_messages = input_messages(input, tool_results, max_context_tokens);
let mut persistent_messages = message_history;
+1 -1
View File
@@ -52,7 +52,7 @@ pub(crate) fn action_from_tool_call(
wait_until_completion: optional_boolean(input, "wait_until_complete")?
.unwrap_or(false),
uses_pager: Some(optional_boolean(input, "uses_pager")?.unwrap_or(false)),
rationale: None,
rationale: optional_string(input, "rationale")?,
citations: Vec::new(),
},
"read_files" => AIAgentActionType::ReadFiles(ReadFilesRequest {
+22
View File
@@ -46,6 +46,28 @@ fn shell_calls_become_domain_actions_without_a_proto_round_trip() {
));
}
#[test]
fn shell_permission_rationale_is_separate_from_executable_command() {
let action = action_from_tool_call(
"task-1",
&call(
"run_shell_command",
serde_json::json!({
"command": "git push origin master",
"rationale": "The user requested a normal push to origin/master."
}),
),
&SkillPathOrigin::Local,
&HashMap::new(),
)
.unwrap();
assert!(matches!(action.action,
AIAgentActionType::RequestCommandOutput { command, rationale: Some(rationale), .. }
if command == "git push origin master"
&& rationale == "The user requested a normal push to origin/master."
));
}
#[test]
fn transfer_control_calls_become_domain_actions() {
let action = action_from_tool_call(