From 89f5925e2d3c341e7182b0b6ba09e7d7023976e0 Mon Sep 17 00:00:00 2001 From: colafornia Date: Sat, 14 Nov 2026 21:57:03 +0800 Subject: [PATCH] feat(cli): initialize chat defaults and workspace roots from stdin Generated-by: Google Antigravity --- products/maka/cli/README.md | 2 +- products/maka/cli/README.zh-CN.md | 2 +- products/maka/cli/src/initialize.rs | 51 +++++-- .../maka/cli/tests/integration/initialize.rs | 128 +++++++++++++++++- 4 files changed, 169 insertions(+), 14 deletions(-) diff --git a/products/maka/cli/README.md b/products/maka/cli/README.md index 656c15788c..4aefaacd55 100644 --- a/products/maka/cli/README.md +++ b/products/maka/cli/README.md @@ -31,7 +31,7 @@ Run `maka` or `maka terminal-shell` for the [NSH terminal workbench](../terminal `maka --help` lists the CLI commands and default terminal options; `maka terminal-shell --help` shows the terminal options alone. The interactive entry requires terminal stdin and stdout. When either is redirected it exits with an error before opening or creating a Host; use explicit Host, application, Code or ACP commands for automation. -`host init --settings-stdin` initializes a new State Root and applies optional proxy settings, model connections, credentials and default target JSON from stdin before returning `rootId`. For example: +`host init --settings-stdin` initializes a new State Root and applies optional proxy settings, chat defaults (`chatDefaults.sandboxMode`), project directory roots (`projectDirectoryRoots`), model connections, credentials and default target JSON from stdin before returning `rootId`. For example: ```json { diff --git a/products/maka/cli/README.zh-CN.md b/products/maka/cli/README.zh-CN.md index 9f93bb507b..1aff387c27 100644 --- a/products/maka/cli/README.zh-CN.md +++ b/products/maka/cli/README.zh-CN.md @@ -31,7 +31,7 @@ `maka --help` 列出 CLI 命令和默认终端参数,`maka terminal-shell --help` 单独查看终端参数。交互入口要求 stdin 和 stdout 都连接终端;任一被重定向时,在打开或创建 Host 前报错退出。自动化使用显式 Host、application、Code 或 ACP 命令。 -`host init --settings-stdin` 初始化新的 State Root,并在返回 `rootId` 前从 stdin 读取并写入可选的代理配置、模型连接、凭据与默认目标模型 JSON。例如: +`host init --settings-stdin` 初始化新的 State Root,并在返回 `rootId` 前从 stdin 读取并写入可选的代理配置、会话默认配置(`chatDefaults.sandboxMode`)、项目根目录列表(`projectDirectoryRoots`)、模型连接、凭据与默认目标模型 JSON。例如: ```json { diff --git a/products/maka/cli/src/initialize.rs b/products/maka/cli/src/initialize.rs index e1987cde0d..346f4fe838 100644 --- a/products/maka/cli/src/initialize.rs +++ b/products/maka/cli/src/initialize.rs @@ -27,7 +27,7 @@ use maka_agent_facts::{ CredentialMutationResult, ModelInfo, ModelOverride, SetCredentialInput, SetDefaultConnectionTargetInput, UpdateCatalogConnectionInput, policy::{ - NetworkProxy, + ChatDefaults, NetworkProxy, RuntimePolicyMutationResult, network_update::{CredentialUpdate, Update, UpdateResult}, }, validation, @@ -41,7 +41,7 @@ use maka_config::{ oauth::enrollment::{LoginCompletion, LoginPreparation}, }; use maka_event_log::root::{RootNamespaces, RootOwner, initialize}; -use maka_host_runtime::HostError; +use maka_host_runtime::{DirectoryRootSpec, HostError}; use maka_plugin_core::scope::Scope; use serde::Deserialize; use serde_json::{Value, json}; @@ -70,6 +70,8 @@ struct Settings { proxy_password: Option, connections: Option>, default_target: Option, + chat_defaults: Option, + project_directory_roots: Option>, } #[derive(Deserialize)] @@ -99,6 +101,8 @@ struct PreparedSettings { proxy_update: Option, connections: Vec, default_target: Option, + chat_defaults: Option, + project_directory_roots: Option>, } struct PreparedConnection { @@ -114,18 +118,34 @@ impl Init { let id = if self.settings_stdin { let settings = Settings::parse(&settings_input().await?)?; let prepared = settings.prepare().await?; + if let (Some(roots), Ok(canonical_root)) = ( + &prepared.project_directory_roots, + self.root.root.canonicalize(), + ) && roots.iter().any(|r| r.path.starts_with(&canonical_root)) + { + return Err("invalid initial settings: invalid project directory roots".into()); + } // Never reconfigure an existing root or race its running Host. let owner = Arc::new(RootOwner::create(&self.root.root, &namespaces)?); let id = owner.root_id().to_owned(); - let store = Arc::new(ConfigurationStore::for_root(owner).await?); + let store = Arc::new(ConfigurationStore::for_root(owner.clone()).await?); + let mut policy_revision = 0; if let Some(update) = prepared.proxy_update { let now = u64::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis())?; - if !matches!( - store.update_network_proxy(update, now).await?, - UpdateResult::Committed { .. } - ) { + let UpdateResult::Committed { revision, .. } = + store.update_network_proxy(update, now).await? + else { return Err("initial proxy settings were not committed".into()); - } + }; + policy_revision = revision; + } + if let Some(chat_defaults) = prepared.chat_defaults { + let RuntimePolicyMutationResult::Committed { .. } = store + .set_chat_defaults(policy_revision, chat_defaults) + .await? + else { + return Err("initial chat defaults were not committed".into()); + }; } let mut catalog_revision = 0; let mut connection_ids = BTreeMap::new(); @@ -279,6 +299,9 @@ impl Init { } } store.shutdown().await?; + if let Some(roots) = prepared.project_directory_roots { + crate::application::configure_workspace_locations(owner, roots).await?; + } id } else { initialize(&self.root.root, &namespaces)? @@ -327,7 +350,7 @@ impl Settings { serde_json::from_slice(bytes).map_err(|_| std::io::Error::other("invalid initial settings")) } - async fn prepare(&self) -> Result { + async fn prepare(self) -> Result { let proxy_update = self.proxy_update()?; let connections = match &self.connections { Some(connections) if !connections.is_empty() => { @@ -485,10 +508,20 @@ impl Settings { } None => None, }; + let project_directory_roots = match self.project_directory_roots { + Some(roots) => Some( + tokio::task::spawn_blocking(move || DirectoryRootSpec::normalize(roots)) + .await? + .map_err(|_| "invalid initial settings: invalid project directory roots")?, + ), + None => None, + }; Ok(PreparedSettings { proxy_update, connections, default_target, + chat_defaults: self.chat_defaults, + project_directory_roots, }) } diff --git a/products/maka/cli/tests/integration/initialize.rs b/products/maka/cli/tests/integration/initialize.rs index 07583015b6..70a3b92ef6 100644 --- a/products/maka/cli/tests/integration/initialize.rs +++ b/products/maka/cli/tests/integration/initialize.rs @@ -18,7 +18,7 @@ */ use std::{ - io::Write, + io::{BufRead, BufReader, Write}, path::Path, process::{Child, Command, Stdio}, sync::Arc, @@ -27,7 +27,7 @@ use std::{ use maka_agent_facts::{ configuration::{ModelSelection, policy::RuntimePolicy}, - execution::ThinkingLevel, + execution::{SandboxMode, ThinkingLevel}, }; use maka_config::ConfigurationStore; use maka_event_log::root::{RootNamespaces, RootOwner}; @@ -53,6 +53,7 @@ fn initialize(root: &Path, timeout: &str) -> Child { #[tokio::test] async fn public_init_persists_settings_from_stdin_and_rejects_reconfiguration() { let directory = tempfile::tempdir().unwrap(); + let workspace_dir = tempfile::tempdir().unwrap(); let root = directory.path().join("root"); let mut child = initialize(&root, "15000"); let mut proxy = RuntimePolicy::default().network_proxy; @@ -63,6 +64,8 @@ async fn public_init_persists_settings_from_stdin_and_rejects_reconfiguration() let mut input = serde_json::to_vec(&serde_json::json!({ "networkProxy": proxy, "proxyPassword": "private-bootstrap-proxy-secret", + "chatDefaults": { "sandboxMode": "danger-full-access" }, + "projectDirectoryRoots": [{ "label": "workspace", "path": workspace_dir.path() }], })) .unwrap(); // Complete valid input remains accepted beyond the former byte ceiling. @@ -80,8 +83,12 @@ async fn public_init_persists_settings_from_stdin_and_rejects_reconfiguration() assert_eq!(response["rootId"], owner.root_id()); let store = ConfigurationStore::for_root(owner).await.unwrap(); let policy = store.runtime_policy().await.unwrap(); - assert_eq!(policy.revision, 1); + assert_eq!(policy.revision, 2); assert_eq!(policy.policy.network_proxy, proxy); + assert_eq!( + policy.policy.chat_defaults.sandbox_mode, + SandboxMode::DangerFullAccess + ); assert_eq!( store .network_configuration() @@ -93,6 +100,93 @@ async fn public_init_persists_settings_from_stdin_and_rejects_reconfiguration() ); store.close().await.unwrap(); + #[cfg(unix)] + let socket_temp = tempfile::Builder::new() + .prefix("mkt-") + .tempdir_in("/tmp") + .unwrap(); + let mut acp_command = Command::new(crate::maka_executable()); + acp_command + .args(["acp", "--root"]) + .arg(&root) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + #[cfg(unix)] + acp_command.env("TMPDIR", socket_temp.path()); + let mut acp = acp_command.spawn().unwrap(); + let mut acp_stdin = acp.stdin.take().unwrap(); + let workspace_cwd = + maka_fs_tools::workspace::project::host_path(&workspace_dir.path().canonicalize().unwrap()) + .unwrap() + .to_owned(); + let mut reader = BufReader::new(acp.stdout.take().unwrap()); + let mut line = String::new(); + let initialize_request = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { "protocolVersion": 1, "clientCapabilities": {} } + }); + writeln!(acp_stdin, "{initialize_request}").unwrap(); + acp_stdin.flush().unwrap(); + let mut init_ok = false; + while reader.read_line(&mut line).unwrap() > 0 { + let message: serde_json::Value = serde_json::from_str(&line).unwrap(); + if message["id"] == 1 { + assert!(message.get("error").is_none(), "{message}"); + init_ok = true; + line.clear(); + break; + } + line.clear(); + } + assert!(init_ok, "initialize response from maka acp"); + + let session_request = serde_json::json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "session/new", + "params": { "cwd": workspace_cwd, "mcpServers": [] } + }); + writeln!(acp_stdin, "{session_request}").unwrap(); + acp_stdin.flush().unwrap(); + let mut session_response = None; + while reader.read_line(&mut line).unwrap() > 0 { + let message: serde_json::Value = serde_json::from_str(&line).unwrap(); + if message["id"] == 2 { + session_response = Some(message); + break; + } + line.clear(); + } + drop(acp_stdin); + let _ = acp.wait(); + let mut retire = Command::new(crate::maka_executable()); + retire.args(["host", "retire", "--root"]).arg(&root); + #[cfg(unix)] + retire.env("TMPDIR", socket_temp.path()); + let _ = retire.output(); + + let session_response = session_response.expect("session/new response from maka acp"); + assert!( + session_response.get("error").is_none(), + "{session_response}" + ); + assert!( + session_response["result"]["sessionId"] + .as_str() + .is_some_and(|id| !id.is_empty()), + "{session_response}" + ); + let sandbox_option = session_response["result"]["configOptions"] + .as_array() + .unwrap() + .iter() + .find(|option| option["id"] == "sandbox_mode") + .unwrap(); + assert_eq!(sandbox_option["currentValue"], "danger_full_access"); + let mut child = initialize(&root, "15000"); child.stdin.take().unwrap().write_all(b"{}").unwrap(); assert!(!child.wait_with_output().unwrap().status.success()); @@ -331,6 +425,13 @@ fn public_init_rejects_invalid_settings_without_exposing_input() { "defaultTarget": { "slug": "eval-gateway", "modelId": "eval-model" } })) .unwrap(), + serde_json::to_vec(&serde_json::json!({ + "projectDirectoryRoots": [{ + "label": "bad-root", + "path": "/private-secret/nonexistent-workspace-root" + }] + })) + .unwrap(), ] { let directory = tempfile::tempdir().unwrap(); let root = directory.path().join("root"); @@ -347,6 +448,27 @@ fn public_init_rejects_invalid_settings_without_exposing_input() { assert!(!diagnostic.contains("private-secret")); assert!(!root.exists()); } + + let directory = tempfile::tempdir().unwrap(); + let root = directory.path().join("existing-empty-root"); + std::fs::create_dir(&root).unwrap(); + let input = serde_json::to_vec(&serde_json::json!({ + "projectDirectoryRoots": [{ + "label": "self-root", + "path": &root + }] + })) + .unwrap(); + let mut child = initialize(&root, "15000"); + let _ = child.stdin.take().unwrap().write_all(&input); + let output = child.wait_with_output().unwrap(); + assert!(!output.status.success()); + let diagnostic = String::from_utf8_lossy(&output.stderr); + assert!( + diagnostic.contains("invalid initial settings"), + "{diagnostic}" + ); + assert!(!root.join(".maka-storage-root.json").exists()); } #[test]