Skip to content

Commit 4f934c2

Browse files
ntammineediclaude
andcommitted
feat: add --query-policy flag for Otto headless mode
Threads the query_policy parameter through the CLI, TUI, core model, and MCP server to support Otto headless execution modes. The flag controls approval-boundary behavior for headless sessions: safe (default) auto-runs read-only queries, unsafe auto-approves all queries, and strict rejects all queries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bc92c2a commit 4f934c2

File tree

4 files changed

+25
-2
lines changed

4 files changed

+25
-2
lines changed

crates/ascend-tools-cli/src/otto.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ pub(crate) enum OttoCommands {
196196
/// Resume the most recent conversation
197197
#[arg(long, conflicts_with_all = ["thread", "conversation"])]
198198
resume: bool,
199+
200+
/// Query execution policy: safe (default), unsafe, strict
201+
#[arg(long, value_parser = ["safe", "unsafe", "strict"])]
202+
query_policy: Option<String>,
199203
},
200204
/// Manage Otto providers
201205
Provider {
@@ -247,6 +251,10 @@ pub(crate) enum OttoCommands {
247251
/// Resume the most recent conversation
248252
#[arg(long, conflicts_with = "conversation")]
249253
resume: bool,
254+
255+
/// Query execution policy: safe (default), unsafe, strict
256+
#[arg(long, value_parser = ["safe", "unsafe", "strict"])]
257+
query_policy: Option<String>,
250258
},
251259
}
252260

@@ -285,6 +293,7 @@ pub(crate) fn handle_otto_cmd(
285293
thread,
286294
conversation,
287295
resume,
296+
query_policy,
288297
} => {
289298
let runtime_uuid = client.resolve_optional_runtime_target(
290299
workspace.as_deref(),
@@ -304,6 +313,7 @@ pub(crate) fn handle_otto_cmd(
304313
runtime_uuid,
305314
thread_id,
306315
model: client.resolve_otto_model(provider.as_deref(), model.as_deref())?,
316+
query_policy,
307317
};
308318

309319
match output {
@@ -434,6 +444,7 @@ pub(crate) fn handle_otto_cmd(
434444
model,
435445
conversation,
436446
resume,
447+
query_policy,
437448
} => {
438449
let runtime_uuid = client.resolve_optional_runtime_target(
439450
workspace.as_deref(),
@@ -447,7 +458,7 @@ pub(crate) fn handle_otto_cmd(
447458
.or(deployment.as_deref().map(|d| format!("deployment:{d}")));
448459
let thread_id =
449460
crate::conversation::resolve_conversation_flag(client, None, conversation, resume)?;
450-
ascend_tools_tui::run_tui(client, runtime_uuid, otto_model, context_label, thread_id)
461+
ascend_tools_tui::run_tui(client, runtime_uuid, otto_model, context_label, thread_id, query_policy)
451462
}
452463
}
453464
}

crates/ascend-tools-core/src/models.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ pub struct OttoChatRequest {
267267
pub thread_id: Option<String>,
268268
#[serde(skip_serializing_if = "Option::is_none")]
269269
pub model: Option<OttoModel>,
270+
/// Query execution policy for headless sessions.
271+
/// "safe" (default): auto-runs read-only queries, rejects writes.
272+
/// "unsafe": auto-approves all queries. "strict": rejects all queries.
273+
#[serde(skip_serializing_if = "Option::is_none")]
274+
pub query_policy: Option<String>,
270275
}
271276

272277
/// Terminal status for an Otto stream.

crates/ascend-tools-mcp/src/server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ impl AscendMcpServer {
524524
runtime_uuid,
525525
thread_id,
526526
model,
527+
query_policy: None,
527528
})
528529
})
529530
.await

crates/ascend-tools-tui/src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ struct App {
318318
provider_label: Option<String>,
319319
model_label: String,
320320
context_label: Option<String>,
321+
query_policy: Option<String>,
321322
pending_request: Option<OttoChatRequest>,
322323
should_quit: bool,
323324
spinner_frame: usize,
@@ -348,6 +349,7 @@ impl App {
348349
model_label: String,
349350
context_label: Option<String>,
350351
thread_id: Option<String>,
352+
query_policy: Option<String>,
351353
) -> Self {
352354
Self {
353355
messages: Vec::new(),
@@ -367,6 +369,7 @@ impl App {
367369
provider_label,
368370
model_label,
369371
context_label,
372+
query_policy,
370373
pending_request: None,
371374
should_quit: false,
372375
spinner_frame: 0,
@@ -823,6 +826,7 @@ impl App {
823826
runtime_uuid: self.runtime_uuid.clone(),
824827
thread_id: self.thread_id.clone(),
825828
model: self.otto_model.clone(),
829+
query_policy: self.query_policy.clone(),
826830
});
827831
self.streaming = true;
828832
self.stream_buffer.clear();
@@ -2399,6 +2403,7 @@ pub fn run_tui(
23992403
otto_model: Option<OttoModel>,
24002404
context_label: Option<String>,
24012405
thread_id: Option<String>,
2406+
query_policy: Option<String>,
24022407
) -> Result<()> {
24032408
// Setup terminal
24042409
terminal::enable_raw_mode()?;
@@ -2452,6 +2457,7 @@ pub fn run_tui(
24522457
String::new(),
24532458
context_label,
24542459
thread_id.clone(),
2460+
query_policy,
24552461
);
24562462

24572463
// If resuming a conversation, load its history in the background
@@ -2653,7 +2659,7 @@ mod tests {
26532659
use std::sync::atomic::AtomicU64;
26542660

26552661
fn test_app() -> App {
2656-
App::new(None, None, None, String::new(), None, None)
2662+
App::new(None, None, None, String::new(), None, None, None)
26572663
}
26582664

26592665
// -- Stream lifecycle --------------------------------------------------

0 commit comments

Comments
 (0)