Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion products/maka/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion products/maka/cli/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
51 changes: 42 additions & 9 deletions products/maka/cli/src/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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};
Expand Down Expand Up @@ -70,6 +70,8 @@ struct Settings {
proxy_password: Option<String>,
connections: Option<Vec<ConnectionSettings>>,
default_target: Option<DefaultTargetSettings>,
chat_defaults: Option<ChatDefaults>,
project_directory_roots: Option<Vec<DirectoryRootSpec>>,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -99,6 +101,8 @@ struct PreparedSettings {
proxy_update: Option<Update>,
connections: Vec<PreparedConnection>,
default_target: Option<DefaultTargetSettings>,
chat_defaults: Option<ChatDefaults>,
project_directory_roots: Option<Vec<DirectoryRootSpec>>,
}

struct PreparedConnection {
Expand All @@ -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();
Expand Down Expand Up @@ -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)?
Expand Down Expand Up @@ -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<PreparedSettings, HostError> {
async fn prepare(self) -> Result<PreparedSettings, HostError> {
let proxy_update = self.proxy_update()?;
let connections = match &self.connections {
Some(connections) if !connections.is_empty() => {
Expand Down Expand Up @@ -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,
})
}

Expand Down
128 changes: 125 additions & 3 deletions products/maka/cli/tests/integration/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

use std::{
io::Write,
io::{BufRead, BufReader, Write},
path::Path,
process::{Child, Command, Stdio},
sync::Arc,
Expand All @@ -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};
Expand All @@ -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;
Expand All @@ -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.
Expand All @@ -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()
Expand All @@ -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());
Expand Down Expand Up @@ -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");
Expand All @@ -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]
Expand Down
Loading