Synced from monorepo
Synced from monorepo Changes: - Release a shell session's resources in one drop - Make the tools blocking-wait cap client-configurable and self-describing - Recognize API "exceeds budget" errors as context overflow - Retry /btw on model overload - Carry running background tasks and subagents across compaction - Require round-trip time for SDK liveness checks - Background-subagent completion reminders with a selectable delivery surface - Make a PTY shell reap itself until it reaches the registry - Recover the OS error code from a TLS-phase connection reset - Consume the attached-client signal and report why idle is withheld - Treat `.grok/sandbox.toml` edits as protected so auto mode prompts before writing - Surface history/search in the Ctrl+. cheatsheet and keep it working in history view - Delete sessions from the dashboard and welcome list - Release a session's activity record when the session ends - Stop charging auth-retry budget for fail-closed 401s; reset it across suspends - Scope skills watches on project vendor roots - Make [stop] cancel in-flight compaction - Make the leader soak measure the leader, not its harness Source-Revision: 8d69c91f02bcacf01e98d5aebbf2f92547c45738
This commit is contained in:
parent
dd04f397b1
commit
a422116582
165 changed files with 15161 additions and 1969 deletions
|
|
@ -271,6 +271,9 @@ pub(crate) struct SessionFlags {
|
|||
/// Mutual exclusivity with Build plan profiles: profiles are omitted and a
|
||||
/// warn is logged when plan flags are also set (K12).
|
||||
pub chat_mode: bool,
|
||||
/// Local-workspace stamp for ACP `_meta` (scrub still strips envId / Direct hub).
|
||||
#[cfg(feature = "local-workspace")]
|
||||
pub local_workspace: Option<crate::app::session_startup::LocalWorkspaceConfig>,
|
||||
/// Effective screen mode label (`ScreenMode::meta_label`), stamped into
|
||||
/// every `PromptRequest._meta.screenMode` for minimal-vs-regular usage
|
||||
/// telemetry. `None` (key omitted) only under `Default` in tests; real
|
||||
|
|
@ -327,6 +330,10 @@ impl SessionFlags {
|
|||
}
|
||||
if self.chat_mode {
|
||||
meta.insert("x.ai/session".into(), serde_json::json!({ "kind": "chat" }));
|
||||
#[cfg(feature = "local-workspace")]
|
||||
if let Some(ref lw) = self.local_workspace {
|
||||
stamp_local_workspace_meta(&mut meta, lw);
|
||||
}
|
||||
}
|
||||
if !self.ask_user {
|
||||
meta.insert("askUserQuestion".into(), serde_json::json!(false));
|
||||
|
|
@ -346,33 +353,107 @@ impl SessionFlags {
|
|||
///
|
||||
/// `x.ai/cloud_existing_workspace` is intentionally omitted: scrub keeps it
|
||||
/// iff `x.ai/local_workspace.mode == "attach"`.
|
||||
#[allow(dead_code)]
|
||||
pub(super) const CHAT_FORBIDDEN_WORKSPACE_BIND_KEYS: &[&str] = &[
|
||||
"envId",
|
||||
"x.ai/cloud_server_id",
|
||||
];
|
||||
/// FS-only tool ids for local existing workspace (chat attach/own).
|
||||
#[cfg(feature = "local-workspace")]
|
||||
pub(super) const LOCAL_WORKSPACE_FS_ONLY_TOOL_IDS: &[&str] = &[
|
||||
"workspace.fs_list",
|
||||
"workspace.fs_exists",
|
||||
"workspace.fs_read_file",
|
||||
"workspace.fs_write_file",
|
||||
"workspace.fs_delete_file",
|
||||
"workspace.put_files",
|
||||
"workspace.get_files",
|
||||
];
|
||||
/// Stamp `_meta["x.ai/session"].kind = "chat"` and strip Build `agentProfile` (K12).
|
||||
pub(super) fn apply_chat_kind_meta(meta: &mut Option<acp::Meta>) {
|
||||
let obj = meta.get_or_insert_with(acp::Meta::new);
|
||||
obj.insert("x.ai/session".into(), serde_json::json!({ "kind": "chat" }));
|
||||
obj.remove("agentProfile");
|
||||
}
|
||||
/// Stamp chat+local intent. Attach also stamps `x.ai/cloud_existing_workspace`.
|
||||
/// Own leaves `server_id` unset — shell supervisor mints before handshake.
|
||||
///
|
||||
/// Never stamps `envId` or `x.ai/cloud_server_id`.
|
||||
#[cfg(feature = "local-workspace")]
|
||||
pub(super) fn stamp_local_workspace_meta(
|
||||
meta: &mut serde_json::Map<String, serde_json::Value>,
|
||||
cfg: &crate::app::session_startup::LocalWorkspaceConfig,
|
||||
) {
|
||||
use crate::app::session_startup::LocalWorkspaceMode;
|
||||
let mut local = serde_json::Map::new();
|
||||
let mode = match cfg.mode {
|
||||
LocalWorkspaceMode::Attach => "attach",
|
||||
LocalWorkspaceMode::Own => "own",
|
||||
};
|
||||
local.insert("mode".into(), serde_json::json!(mode));
|
||||
if let Some(ref sid) = cfg.server_id {
|
||||
local.insert("server_id".into(), serde_json::json!(sid));
|
||||
}
|
||||
if let Some(ref cwd) = cfg.cwd {
|
||||
local
|
||||
.insert("cwd".into(), serde_json::json!(cwd.to_string_lossy().into_owned()));
|
||||
}
|
||||
meta.insert("x.ai/local_workspace".into(), serde_json::Value::Object(local));
|
||||
tracing::info!(
|
||||
target: crate::views::welcome::workspace_mode::WORKSPACE_MODE_LOG,
|
||||
event = "acp_meta_stamped",
|
||||
mode,
|
||||
server_id = cfg.server_id.as_deref(),
|
||||
cwd = cfg.cwd.as_ref().map(|p| p.display().to_string()),
|
||||
"stamped x.ai/local_workspace onto session meta"
|
||||
);
|
||||
if cfg.mode == LocalWorkspaceMode::Attach && let Some(ref sid) = cfg.server_id {
|
||||
let mut existing = serde_json::Map::new();
|
||||
existing.insert("server_id".into(), serde_json::json!(sid));
|
||||
if let Some(ref cwd) = cfg.cwd {
|
||||
existing
|
||||
.insert(
|
||||
"cwd".into(),
|
||||
serde_json::json!(cwd.to_string_lossy().into_owned()),
|
||||
);
|
||||
}
|
||||
meta.insert(
|
||||
"x.ai/cloud_existing_workspace".into(),
|
||||
serde_json::Value::Object(existing),
|
||||
);
|
||||
}
|
||||
}
|
||||
/// Apply [`stamp_local_workspace_meta`] onto optional ACP meta.
|
||||
#[cfg(feature = "local-workspace")]
|
||||
pub(super) fn apply_local_workspace_meta(
|
||||
meta: &mut Option<acp::Meta>,
|
||||
cfg: &crate::app::session_startup::LocalWorkspaceConfig,
|
||||
) {
|
||||
let obj = meta.get_or_insert_with(acp::Meta::new);
|
||||
stamp_local_workspace_meta(obj, cfg);
|
||||
}
|
||||
/// Shared chat create/load/worktree meta finalize: kind + local stamp + scrub.
|
||||
pub(super) fn finalize_chat_session_meta(
|
||||
meta: &mut Option<acp::Meta>,
|
||||
is_chat_path: bool,
|
||||
#[allow(unused_variables)]
|
||||
#[cfg_attr(not(feature = "local-workspace"), allow(unused_variables))]
|
||||
session_flags: &SessionFlags,
|
||||
) {
|
||||
if !is_chat_path {
|
||||
return;
|
||||
}
|
||||
apply_chat_kind_meta(meta);
|
||||
#[cfg(feature = "local-workspace")]
|
||||
if let Some(ref lw) = session_flags.local_workspace {
|
||||
apply_local_workspace_meta(meta, lw);
|
||||
}
|
||||
scrub_chat_workspace_bind_meta(meta);
|
||||
}
|
||||
/// Remove client workspace-bind keys from chat create/load meta (defense in depth).
|
||||
///
|
||||
/// Narrow scrub exception: keep `x.ai/cloud_existing_workspace` when local
|
||||
/// intent is attach. Never keep `envId` or Direct hub `x.ai/cloud_server_id`.
|
||||
/// intent is **attach**. Own stamps intent only (shell mints `server_id`).
|
||||
/// Never keep `envId` or Direct hub `x.ai/cloud_server_id`.
|
||||
pub(super) fn scrub_chat_workspace_bind_meta(meta: &mut Option<acp::Meta>) {
|
||||
let Some(obj) = meta.as_mut() else {
|
||||
return;
|
||||
|
|
@ -380,10 +461,80 @@ pub(super) fn scrub_chat_workspace_bind_meta(meta: &mut Option<acp::Meta>) {
|
|||
for key in CHAT_FORBIDDEN_WORKSPACE_BIND_KEYS {
|
||||
obj.remove(*key);
|
||||
}
|
||||
#[cfg(feature = "local-workspace")]
|
||||
{
|
||||
let allow_existing_attach = obj
|
||||
.get("x.ai/local_workspace")
|
||||
.and_then(|v| v.get("mode"))
|
||||
.and_then(|m| m.as_str()) == Some("attach");
|
||||
if !allow_existing_attach {
|
||||
obj.remove("x.ai/cloud_existing_workspace");
|
||||
}
|
||||
}
|
||||
{
|
||||
obj.remove("x.ai/cloud_existing_workspace");
|
||||
}
|
||||
}
|
||||
/// Params for shell ACP `x.ai/session/add_local_workspace`.
|
||||
///
|
||||
/// v1 surface is **shell ACP-only** (no pager slash/command wiring). Pager
|
||||
/// dogfood / headless clients call the extension directly with this payload.
|
||||
/// No remove path until session end.
|
||||
#[cfg(feature = "local-workspace")]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn mid_session_add_local_workspace_params(
|
||||
session_id: &str,
|
||||
cfg: &crate::app::session_startup::LocalWorkspaceConfig,
|
||||
) -> serde_json::Value {
|
||||
let mut meta = serde_json::Map::new();
|
||||
stamp_local_workspace_meta(&mut meta, cfg);
|
||||
let mut opt = Some(meta);
|
||||
scrub_chat_workspace_bind_meta(&mut opt);
|
||||
serde_json::json!({
|
||||
"sessionId": session_id,
|
||||
"meta": opt.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
/// Fail closed on operator attestation outside the FS-only allowlist.
|
||||
/// `None` / empty attested set → uncheckable → refuse. Live server is not probed.
|
||||
#[cfg(feature = "local-workspace")]
|
||||
pub(crate) fn reject_non_fs_only_advertised_tools(
|
||||
advertised_tool_ids: Option<&[&str]>,
|
||||
) -> Result<(), String> {
|
||||
let Some(ids) = advertised_tool_ids else {
|
||||
return Err(
|
||||
"operator attestation GROK_CHAT_LOCAL_WORKSPACE_ADVERTISED_TOOLS is unset \
|
||||
(uncheckable); refuse attach. Live workspace_server was not inspected — set \
|
||||
the env to a comma-separated FS-only catalog."
|
||||
.into(),
|
||||
);
|
||||
};
|
||||
if ids.is_empty() {
|
||||
return Err(
|
||||
"operator attestation GROK_CHAT_LOCAL_WORKSPACE_ADVERTISED_TOOLS is empty \
|
||||
(uncheckable); refuse attach. Live workspace_server was not inspected."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let forbidden: Vec<&str> = ids
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| !LOCAL_WORKSPACE_FS_ONLY_TOOL_IDS.contains(id))
|
||||
.collect();
|
||||
if forbidden.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(
|
||||
format!(
|
||||
"operator attestation lists tools outside the FS-only allowlist: {}. \
|
||||
Live workspace_server was not inspected. Fix \
|
||||
GROK_CHAT_LOCAL_WORKSPACE_ADVERTISED_TOOLS or restart workspace_server \
|
||||
with --require-explicit-toolset and an FS-only catalog.",
|
||||
forbidden.join(", ")
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
/// Metadata returned from effect execution so the event loop can patch
|
||||
/// state that requires a spawned task handle (e.g., auth AbortHandle).
|
||||
#[derive(Default)]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ pub(crate) use helpers::{
|
|||
EffectMeta, RestoreProgressMsg, SessionFlags, persist_permission_mode_and_notify,
|
||||
persist_setting, sanitize_user_error,
|
||||
};
|
||||
#[cfg(feature = "local-workspace")]
|
||||
pub(crate) use helpers::reject_non_fs_only_advertised_tools;
|
||||
use helpers::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use agent_client_protocol as acp;
|
||||
|
|
@ -707,7 +709,7 @@ pub(crate) fn execute(
|
|||
}
|
||||
});
|
||||
}
|
||||
Effect::FetchSessionList { query, seq } => {
|
||||
Effect::FetchSessionList { query, seq, kind_filter } => {
|
||||
let tx = acp_tx.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
tasks
|
||||
|
|
@ -721,6 +723,19 @@ pub(crate) fn execute(
|
|||
} else {
|
||||
params["allowRelax"] = serde_json::Value::Bool(true);
|
||||
}
|
||||
if let Some(kinds) = &kind_filter {
|
||||
params["_meta"] = serde_json::json!({
|
||||
"x.ai/facetFilters": { "kind": kinds },
|
||||
});
|
||||
tracing::info!(
|
||||
target: "grok.pager.workspace_mode",
|
||||
event = "session_list_fetch",
|
||||
kind_filter = ?kinds,
|
||||
query = ?query,
|
||||
seq,
|
||||
"FetchSessionList with kind facet filter"
|
||||
);
|
||||
}
|
||||
let request = acp::ExtRequest::new(
|
||||
"x.ai/session/list",
|
||||
serde_json::value::to_raw_value(¶ms)
|
||||
|
|
@ -3543,6 +3558,7 @@ pub(crate) fn execute(
|
|||
}
|
||||
Effect::SendBtw { agent_id, session_id, question, minimal_request_id } => {
|
||||
let tx = acp_tx.clone();
|
||||
let is_api_key_auth = session_flags.is_api_key_auth;
|
||||
tasks
|
||||
.spawn(async move {
|
||||
let request = acp::ExtRequest::new(
|
||||
|
|
@ -3577,9 +3593,7 @@ pub(crate) fn execute(
|
|||
Err(e) => {
|
||||
TaskResult::BtwResponse {
|
||||
agent_id,
|
||||
result: Err(
|
||||
sanitize_user_error(&format!("side question failed: {e}")),
|
||||
),
|
||||
result: Err(format_acp_error(&e, is_api_key_auth)),
|
||||
minimal_request_id,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1523,6 +1523,7 @@ async fn fetch_session_list_pushes_query_and_echoes_seq() {
|
|||
let mut tasks = run(Effect::FetchSessionList {
|
||||
query: Some("hit".into()),
|
||||
seq: 7,
|
||||
kind_filter: None,
|
||||
});
|
||||
match tasks.join_next().await.expect("task").expect("no panic") {
|
||||
TaskResult::SessionListLoaded { sessions, scope, seq, query, .. } => {
|
||||
|
|
@ -1539,6 +1540,7 @@ async fn fetch_session_list_pushes_query_and_echoes_seq() {
|
|||
let mut tasks = run(Effect::FetchSessionList {
|
||||
query: None,
|
||||
seq: 8,
|
||||
kind_filter: None,
|
||||
});
|
||||
match tasks.join_next().await.expect("task").expect("no panic") {
|
||||
TaskResult::SessionListLoaded { scope, seq, query, .. } => {
|
||||
|
|
@ -1554,6 +1556,7 @@ async fn fetch_session_list_pushes_query_and_echoes_seq() {
|
|||
let mut tasks = run(Effect::FetchSessionList {
|
||||
query: Some("fail-me".into()),
|
||||
seq: 9,
|
||||
kind_filter: None,
|
||||
});
|
||||
match tasks.join_next().await.expect("task").expect("no panic") {
|
||||
TaskResult::SessionListFailed { error, seq, query } => {
|
||||
|
|
@ -1589,6 +1592,50 @@ async fn fetch_session_list_pushes_query_and_echoes_seq() {
|
|||
assert_eq!(captured[2]["query"], "fail-me");
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn fetch_session_list_sends_kind_facet_filter() {
|
||||
use std::sync::{Arc, Mutex};
|
||||
use xai_acp_lib::AcpAgentMessage;
|
||||
let captured: Arc<Mutex<Vec<serde_json::Value>>> = Arc::default();
|
||||
let captured_for_task = captured.clone();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
tokio::spawn(async move {
|
||||
while let Some(msg) = rx.recv().await {
|
||||
if let AcpAgentMessage::ExtMethod(args) = msg {
|
||||
let params: serde_json::Value = serde_json::from_str(
|
||||
args.request.params.get(),
|
||||
)
|
||||
.expect("params JSON");
|
||||
captured_for_task.lock().unwrap().push(params);
|
||||
let body = serde_json::json!({ "result": { "sessions": [] } });
|
||||
let raw = serde_json::value::RawValue::from_string(body.to_string())
|
||||
.expect("ser");
|
||||
let _ = args.response_tx.send(Ok(acp::ExtResponse::new(Arc::from(raw))));
|
||||
}
|
||||
}
|
||||
});
|
||||
let (progress_tx, _progress_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let mut tasks = JoinSet::new();
|
||||
execute(
|
||||
Effect::FetchSessionList {
|
||||
query: None,
|
||||
seq: 1,
|
||||
kind_filter: Some(vec!["build".into()]),
|
||||
},
|
||||
&mut tasks,
|
||||
&tx,
|
||||
Path::new("."),
|
||||
&SessionFlags::default(),
|
||||
&progress_tx,
|
||||
);
|
||||
let _ = tasks.join_next().await;
|
||||
let captured = captured.lock().unwrap();
|
||||
assert_eq!(captured.len(), 1);
|
||||
assert_eq!(
|
||||
captured[0]["_meta"]["x.ai/facetFilters"]["kind"],
|
||||
serde_json::json!(["build"])
|
||||
);
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn fetch_workflows_list_sends_session_id() {
|
||||
use std::sync::{Arc, Mutex};
|
||||
use xai_acp_lib::AcpAgentMessage;
|
||||
|
|
@ -1979,7 +2026,7 @@ fn to_meta_chat_mode_stamps_kind_and_omits_agent_profile() {
|
|||
..Default::default()
|
||||
};
|
||||
let meta = flags.to_meta().expect("chat_mode must emit meta");
|
||||
assert_eq!(meta["x.ai/session"] ["kind"], "chat");
|
||||
assert_eq!(meta["x.ai/session"]["kind"], "chat");
|
||||
assert!(
|
||||
meta.get("agentProfile").is_none(),
|
||||
"K12: chat mode must omit Build agentProfile"
|
||||
|
|
@ -2006,7 +2053,7 @@ fn load_meta_chat_kind_alone_stamps_kind_and_strips_profile() {
|
|||
scrub_chat_workspace_bind_meta(&mut meta);
|
||||
}
|
||||
let meta = meta.expect("chat_kind must produce meta");
|
||||
assert_eq!(meta["x.ai/session"] ["kind"], "chat");
|
||||
assert_eq!(meta["x.ai/session"]["kind"], "chat");
|
||||
assert!(
|
||||
meta.get("agentProfile").is_none(),
|
||||
"entry chat_kind must strip Build agentProfile"
|
||||
|
|
@ -2040,7 +2087,7 @@ fn chat_create_meta_never_includes_workspace_bind_keys_when_cloud_fields_set() {
|
|||
apply_chat_kind_meta(&mut meta);
|
||||
scrub_chat_workspace_bind_meta(&mut meta);
|
||||
let meta = meta.expect("chat create must emit meta");
|
||||
assert_eq!(meta["x.ai/session"] ["kind"], "chat");
|
||||
assert_eq!(meta["x.ai/session"]["kind"], "chat");
|
||||
assert_chat_meta_has_no_workspace_bind_keys(
|
||||
&serde_json::Value::Object(meta.clone()),
|
||||
);
|
||||
|
|
@ -2064,11 +2111,165 @@ fn chat_load_meta_never_includes_workspace_bind_keys() {
|
|||
}
|
||||
scrub_chat_workspace_bind_meta(&mut meta);
|
||||
let meta = meta.expect("chat load must emit meta");
|
||||
assert_eq!(meta["x.ai/session"] ["kind"], "chat");
|
||||
assert_eq!(meta["x.ai/session"]["kind"], "chat");
|
||||
assert_chat_meta_has_no_workspace_bind_keys(
|
||||
&serde_json::Value::Object(meta.clone()),
|
||||
);
|
||||
}
|
||||
/// Attach stamp keeps existing workspace + local intent; envId / Direct hub stay stripped.
|
||||
#[cfg(feature = "local-workspace")]
|
||||
#[test]
|
||||
fn scrub_chat_workspace_matrix_attach_exception() {
|
||||
use crate::app::session_startup::{LocalWorkspaceConfig, LocalWorkspaceMode};
|
||||
let mut meta = Some(acp::Meta::new());
|
||||
{
|
||||
let obj = meta.as_mut().unwrap();
|
||||
obj.insert("envId".into(), serde_json::json!("env-x"));
|
||||
obj.insert("x.ai/cloud_server_id".into(), serde_json::json!("hub-x"));
|
||||
obj.insert(
|
||||
"x.ai/cloud_existing_workspace".into(),
|
||||
serde_json::json!({"server_id": "srv-x", "cwd": "/ws"}),
|
||||
);
|
||||
}
|
||||
scrub_chat_workspace_bind_meta(&mut meta);
|
||||
let scrubbed = meta.as_ref().unwrap();
|
||||
assert!(scrubbed.get("envId").is_none());
|
||||
assert!(scrubbed.get("x.ai/cloud_server_id").is_none());
|
||||
assert!(scrubbed.get("x.ai/cloud_existing_workspace").is_none());
|
||||
let mut meta = Some(acp::Meta::new());
|
||||
apply_local_workspace_meta(
|
||||
&mut meta,
|
||||
&LocalWorkspaceConfig {
|
||||
mode: LocalWorkspaceMode::Attach,
|
||||
cwd: Some(std::path::PathBuf::from("/tmp/repo")),
|
||||
server_id: Some("srv-dogfood".into()),
|
||||
},
|
||||
);
|
||||
{
|
||||
let obj = meta.as_mut().unwrap();
|
||||
obj.insert("envId".into(), serde_json::json!("env-must-go"));
|
||||
obj.insert("x.ai/cloud_server_id".into(), serde_json::json!("hub-must-go"));
|
||||
}
|
||||
scrub_chat_workspace_bind_meta(&mut meta);
|
||||
let scrubbed = meta.as_ref().unwrap();
|
||||
assert!(scrubbed.get("envId").is_none(), "envId must stay scrubbed");
|
||||
assert!(
|
||||
scrubbed.get("x.ai/cloud_server_id").is_none(),
|
||||
"Direct hub must stay scrubbed"
|
||||
);
|
||||
assert_eq!(
|
||||
scrubbed["x.ai/cloud_existing_workspace"]["server_id"],
|
||||
"srv-dogfood"
|
||||
);
|
||||
assert_eq!(scrubbed["x.ai/local_workspace"]["mode"], "attach");
|
||||
assert_eq!(scrubbed["x.ai/local_workspace"]["server_id"], "srv-dogfood");
|
||||
assert_eq!(scrubbed["x.ai/local_workspace"]["cwd"], "/tmp/repo");
|
||||
}
|
||||
#[cfg(feature = "local-workspace")]
|
||||
#[test]
|
||||
fn to_meta_chat_attach_stamps_local_and_existing() {
|
||||
use crate::app::session_startup::{LocalWorkspaceConfig, LocalWorkspaceMode};
|
||||
let flags = SessionFlags {
|
||||
chat_mode: true,
|
||||
local_workspace: Some(LocalWorkspaceConfig {
|
||||
mode: LocalWorkspaceMode::Attach,
|
||||
cwd: Some(std::path::PathBuf::from("/tmp/repo")),
|
||||
server_id: Some("srv-1".into()),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let meta = flags.to_meta().expect("meta");
|
||||
assert_eq!(meta["x.ai/session"]["kind"], "chat");
|
||||
assert_eq!(meta["x.ai/local_workspace"]["mode"], "attach");
|
||||
assert_eq!(meta["x.ai/cloud_existing_workspace"]["server_id"], "srv-1");
|
||||
assert!(meta.get("envId").is_none());
|
||||
assert!(meta.get("x.ai/cloud_server_id").is_none());
|
||||
}
|
||||
#[cfg(feature = "local-workspace")]
|
||||
#[test]
|
||||
fn to_meta_chat_own_stamps_intent_without_existing() {
|
||||
use crate::app::session_startup::{LocalWorkspaceConfig, LocalWorkspaceMode};
|
||||
let flags = SessionFlags {
|
||||
chat_mode: true,
|
||||
local_workspace: Some(LocalWorkspaceConfig {
|
||||
mode: LocalWorkspaceMode::Own,
|
||||
cwd: Some(std::path::PathBuf::from("/tmp/repo-own")),
|
||||
server_id: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let meta = flags.to_meta().expect("meta");
|
||||
assert_eq!(meta["x.ai/local_workspace"]["mode"], "own");
|
||||
assert_eq!(meta["x.ai/local_workspace"]["cwd"], "/tmp/repo-own");
|
||||
assert!(meta["x.ai/local_workspace"].get("server_id").is_none());
|
||||
assert!(
|
||||
meta.get("x.ai/cloud_existing_workspace").is_none(),
|
||||
"own must not stamp existing; shell mints server_id"
|
||||
);
|
||||
assert!(meta.get("envId").is_none());
|
||||
}
|
||||
#[cfg(feature = "local-workspace")]
|
||||
#[test]
|
||||
fn mid_session_add_params_scrub_envid() {
|
||||
use crate::app::session_startup::{LocalWorkspaceConfig, LocalWorkspaceMode};
|
||||
let params = mid_session_add_local_workspace_params(
|
||||
"sess-1",
|
||||
&LocalWorkspaceConfig {
|
||||
mode: LocalWorkspaceMode::Attach,
|
||||
cwd: Some(std::path::PathBuf::from("/tmp/repo")),
|
||||
server_id: Some("srv-add".into()),
|
||||
},
|
||||
);
|
||||
assert_eq!(params["sessionId"], "sess-1");
|
||||
assert_eq!(params["meta"]["x.ai/local_workspace"]["mode"], "attach");
|
||||
assert_eq!(
|
||||
params["meta"]["x.ai/cloud_existing_workspace"]["server_id"],
|
||||
"srv-add"
|
||||
);
|
||||
assert!(params["meta"].get("envId").is_none());
|
||||
}
|
||||
#[cfg(feature = "local-workspace")]
|
||||
#[test]
|
||||
fn reject_non_fs_only_advertised_tools_matrix() {
|
||||
let fs_only = ["workspace.fs_list", "workspace.fs_read_file", "workspace.put_files"];
|
||||
assert!(reject_non_fs_only_advertised_tools(Some(&fs_only[..])).is_ok());
|
||||
assert!(
|
||||
reject_non_fs_only_advertised_tools(None)
|
||||
.unwrap_err()
|
||||
.contains("uncheckable")
|
||||
);
|
||||
assert!(
|
||||
reject_non_fs_only_advertised_tools(Some(&[][..]))
|
||||
.unwrap_err()
|
||||
.contains("empty")
|
||||
);
|
||||
let with_exec = ["workspace.fs_list", "workspace.bash", "terminal.exec"];
|
||||
let err = reject_non_fs_only_advertised_tools(Some(&with_exec[..])).unwrap_err();
|
||||
assert!(err.contains("FS-only"), "{err}");
|
||||
assert!(err.contains("workspace.bash"), "{err}");
|
||||
assert!(err.contains("terminal.exec"), "{err}");
|
||||
}
|
||||
#[cfg(feature = "local-workspace")]
|
||||
#[test]
|
||||
fn finalize_chat_session_meta_stamps_attach_on_worktree_path() {
|
||||
use crate::app::session_startup::{LocalWorkspaceConfig, LocalWorkspaceMode};
|
||||
let flags = SessionFlags {
|
||||
chat_mode: false,
|
||||
local_workspace: Some(LocalWorkspaceConfig {
|
||||
mode: LocalWorkspaceMode::Attach,
|
||||
cwd: Some(std::path::PathBuf::from("/tmp/repo")),
|
||||
server_id: Some("srv-wt".into()),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let mut meta = flags.to_meta();
|
||||
finalize_chat_session_meta(&mut meta, true, &flags);
|
||||
let meta = meta.expect("meta");
|
||||
assert_eq!(meta["x.ai/session"]["kind"], "chat");
|
||||
assert_eq!(meta["x.ai/local_workspace"]["mode"], "attach");
|
||||
assert_eq!(meta["x.ai/cloud_existing_workspace"]["server_id"], "srv-wt");
|
||||
assert!(meta.get("envId").is_none());
|
||||
}
|
||||
#[test]
|
||||
fn to_meta_yolo_suppresses_auto_mode() {
|
||||
let flags = SessionFlags {
|
||||
|
|
|
|||
Loading…
Reference in a new issue