Skip to content

Commit 8375474

Browse files
ntammineediclaudelostmygithubaccount
authored
feat: add --query-policy flag for Otto headless mode (#63)
* 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> * fix: rustfmt run_tui call Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add missing query_policy field to test OttoChatRequest initializers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add missing query_policy to JS and Python binding crates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Cody Peterson <54814569+lostmygithubaccount@users.noreply.github.com>
1 parent c3bd0f5 commit 8375474

File tree

7 files changed

+52
-4
lines changed

7 files changed

+52
-4
lines changed

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

Lines changed: 19 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,14 @@ 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(
462+
client,
463+
runtime_uuid,
464+
otto_model,
465+
context_label,
466+
thread_id,
467+
query_policy,
468+
)
451469
}
452470
}
453471
}

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-core/tests/client_http.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ fn otto_streaming_interrupted_when_sse_closes_without_terminal_event() {
417417
runtime_uuid: None,
418418
thread_id: None,
419419
model: None,
420+
query_policy: None,
420421
};
421422

422423
let mut observed_thread_id = None;
@@ -490,6 +491,7 @@ fn otto_streaming_completes_on_thread_done() {
490491
runtime_uuid: None,
491492
thread_id: None,
492493
model: None,
494+
query_policy: None,
493495
};
494496

495497
let mut text = String::new();
@@ -551,6 +553,7 @@ fn otto_streaming_completes_on_thread_stopped() {
551553
runtime_uuid: None,
552554
thread_id: None,
553555
model: None,
556+
query_policy: None,
554557
};
555558

556559
let mut text = String::new();
@@ -613,6 +616,7 @@ fn otto_streaming_cancelled_by_callback() {
613616
runtime_uuid: None,
614617
thread_id: None,
615618
model: None,
619+
query_policy: None,
616620
};
617621

618622
let mut delta_count = 0;
@@ -678,6 +682,7 @@ fn otto_streaming_handles_response_error_event() {
678682
runtime_uuid: None,
679683
thread_id: None,
680684
model: None,
685+
query_policy: None,
681686
};
682687

683688
let mut text = String::new();
@@ -751,6 +756,7 @@ fn otto_streaming_dispatches_tool_call_events() {
751756
runtime_uuid: None,
752757
thread_id: None,
753758
model: None,
759+
query_policy: None,
754760
};
755761

756762
let mut events_log: Vec<String> = Vec::new();
@@ -827,6 +833,7 @@ fn otto_streaming_skips_heartbeats_and_comments() {
827833
runtime_uuid: None,
828834
thread_id: None,
829835
model: None,
836+
query_policy: None,
830837
};
831838

832839
let mut text = String::new();
@@ -882,6 +889,7 @@ fn otto_streaming_sse_endpoint_returns_error() {
882889
runtime_uuid: None,
883890
thread_id: None,
884891
model: None,
892+
query_policy: None,
885893
};
886894

887895
let result = client.otto_streaming(&request, |_| ControlFlow::Continue(()), |_| {});
@@ -922,6 +930,7 @@ fn otto_streaming_thread_post_returns_error() {
922930
runtime_uuid: None,
923931
thread_id: None,
924932
model: None,
933+
query_policy: None,
925934
};
926935

927936
let result = client.otto_streaming(&request, |_| ControlFlow::Continue(()), |_| {});
@@ -968,6 +977,7 @@ fn otto_non_streaming_errors_on_interrupted_stream() {
968977
runtime_uuid: None,
969978
thread_id: None,
970979
model: None,
980+
query_policy: None,
971981
};
972982

973983
// otto() (non-streaming) should return an error for interrupted streams
@@ -1028,6 +1038,7 @@ fn otto_streaming_retries_409_on_follow_up() {
10281038
runtime_uuid: None,
10291039
thread_id: Some("t-existing".into()),
10301040
model: None,
1041+
query_policy: None,
10311042
};
10321043

10331044
let mut text = String::new();
@@ -1075,6 +1086,7 @@ fn otto_streaming_409_on_new_thread_returns_error() {
10751086
runtime_uuid: None,
10761087
thread_id: None, // new thread — no retry
10771088
model: None,
1089+
query_policy: None,
10781090
};
10791091

10801092
let result = client.otto_streaming(&request, |_| ControlFlow::Continue(()), |_| {});
@@ -1124,6 +1136,7 @@ fn otto_streaming_on_thread_id_called_before_events() {
11241136
runtime_uuid: None,
11251137
thread_id: None,
11261138
model: None,
1139+
query_policy: None,
11271140
};
11281141

11291142
let callback_order = std::sync::Mutex::new(Vec::<String>::new());
@@ -1185,6 +1198,7 @@ fn otto_streaming_empty_sse_stream_returns_interrupted() {
11851198
runtime_uuid: None,
11861199
thread_id: None,
11871200
model: None,
1201+
query_policy: None,
11881202
};
11891203

11901204
let response = client
@@ -1222,6 +1236,7 @@ fn otto_streaming_missing_thread_id_in_response() {
12221236
runtime_uuid: None,
12231237
thread_id: None,
12241238
model: None,
1239+
query_policy: None,
12251240
};
12261241

12271242
let result = client.otto_streaming(&request, |_| ControlFlow::Continue(()), |_| {});
@@ -1357,6 +1372,7 @@ fn otto_streaming_response_error_without_message() {
13571372
runtime_uuid: None,
13581373
thread_id: None,
13591374
model: None,
1375+
query_policy: None,
13601376
};
13611377

13621378
let response = client

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ impl Client {
378378
let otto_model = client.resolve_otto_model(provider.as_deref(), model.as_deref()).map_err(to_napi_err)?;
379379
let runtime_uuid = client.resolve_optional_runtime_target(workspace.as_deref(), deployment.as_deref(), uuid.as_deref()).map_err(to_napi_err)?;
380380
let thread_id = client.resolve_otto_thread(conversation.as_deref(), thread_id).map_err(to_napi_err)?;
381-
let request = models::OttoChatRequest { prompt, runtime_uuid, thread_id, model: otto_model };
381+
let request = models::OttoChatRequest { prompt, runtime_uuid, thread_id, model: otto_model, query_policy: None };
382382
client.otto(&request).map_err(to_napi_err)
383383
}))
384384
}
@@ -391,7 +391,7 @@ impl Client {
391391
let otto_model = client.resolve_otto_model(provider.as_deref(), model.as_deref()).map_err(to_napi_err)?;
392392
let runtime_uuid = client.resolve_optional_runtime_target(workspace.as_deref(), deployment.as_deref(), uuid.as_deref()).map_err(to_napi_err)?;
393393
let thread_id = client.resolve_otto_thread(conversation.as_deref(), thread_id).map_err(to_napi_err)?;
394-
let request = models::OttoChatRequest { prompt, runtime_uuid, thread_id, model: otto_model };
394+
let request = models::OttoChatRequest { prompt, runtime_uuid, thread_id, model: otto_model, query_policy: None };
395395
client.otto_streaming(&request, |event| { if let models::StreamEvent::TextDelta(delta) = event { on_delta.call(Ok(delta), ThreadsafeFunctionCallMode::NonBlocking); } std::ops::ControlFlow::Continue(()) }, |_| {}).map_err(to_napi_err)
396396
}))
397397
}

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-py/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,7 @@ impl Client {
536536
runtime_uuid,
537537
thread_id,
538538
model: otto_model,
539+
query_policy: None,
539540
};
540541
self.inner.otto(&request)
541542
})

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

Lines changed: 8 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 --------------------------------------------------
@@ -3515,6 +3521,7 @@ mod tests {
35153521
runtime_uuid: None,
35163522
thread_id: None,
35173523
model: None,
3524+
query_policy: None,
35183525
});
35193526

35203527
// The main loop guard is: if !app.interrupting && let Some(req) = app.take_pending_request()

0 commit comments

Comments
 (0)