diff --git a/Cargo.lock b/Cargo.lock index c90d9941..024dfb14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1060,17 +1060,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - [[package]] name = "errno" version = "0.3.14" @@ -2206,25 +2195,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lua-src" -version = "550.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e836dc8ae16806c9bdcf42003a88da27d163433e3f9684c52f0301258004a4fb" -dependencies = [ - "cc", -] - -[[package]] -name = "luajit-src" -version = "210.6.6+707c12b" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a86cc925d4053d0526ae7f5bc765dbd0d7a5d1a63d43974f4966cb349ca63295" -dependencies = [ - "cc", - "which", -] - [[package]] name = "mac" version = "0.1.1" @@ -2256,7 +2226,6 @@ version = "0.1.0" dependencies = [ "arc-swap", "llm-engine", - "mlua", "protocol", "secrets", "serde", @@ -2444,39 +2413,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "mlua" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd36acfa49ce6ee56d1307a061dd302c564eee757e6e4cd67eb4f7204846fab" -dependencies = [ - "bstr", - "either", - "erased-serde", - "libc", - "mlua-sys", - "num-traits", - "parking_lot", - "rustc-hash", - "rustversion", - "serde", - "serde-value", -] - -[[package]] -name = "mlua-sys" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1c3a7fc7580227ece249fd90aa2fa3b39eb2b49d3aec5e103b3e85f2c3dfc8" -dependencies = [ - "cc", - "cfg-if", - "libc", - "lua-src", - "luajit-src", - "pkg-config", -] - [[package]] name = "native-tls" version = "0.2.18" @@ -2671,15 +2607,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-float" version = "4.6.0" @@ -3683,16 +3610,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float 2.10.1", - "serde", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -4228,7 +4145,7 @@ dependencies = [ "nix", "num-derive", "num-traits", - "ordered-float 4.6.0", + "ordered-float", "pest", "pest_derive", "phf 0.11.3", @@ -4744,12 +4661,6 @@ dependencies = [ "pom", ] -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typenum" version = "1.19.0" @@ -5428,7 +5339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" dependencies = [ "log", - "ordered-float 4.6.0", + "ordered-float", "strsim", "thiserror 1.0.69", "wezterm-dynamic-derive", @@ -5458,15 +5369,6 @@ dependencies = [ "wezterm-dynamic", ] -[[package]] -name = "which" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" -dependencies = [ - "libc", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/client/src/ticket_role.rs b/crates/client/src/ticket_role.rs index edb78d09..2ce2226d 100644 --- a/crates/client/src/ticket_role.rs +++ b/crates/client/src/ticket_role.rs @@ -1227,7 +1227,7 @@ workflow = "ticket-review-workflow" temp.path(), r#" [roles.coder] -profile = "./coder.lua" +profile = "./coder.toml" "#, ); let context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Coder); diff --git a/crates/manifest/Cargo.toml b/crates/manifest/Cargo.toml index 23bbcc90..9f51040a 100644 --- a/crates/manifest/Cargo.toml +++ b/crates/manifest/Cargo.toml @@ -7,7 +7,6 @@ license.workspace = true [dependencies] arc-swap = "1" llm-engine = { workspace = true } -mlua = { version = "0.11.4", features = ["lua54", "vendored", "serialize"] } protocol = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/crates/manifest/src/lib.rs b/crates/manifest/src/lib.rs index cad1f620..f2e57ce6 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -1193,9 +1193,9 @@ model_id = "claude-sonnet-4-20250514" let toml = MINIMAL_REQUIRED.replace( "[engine]\n", "[engine]\n\ - [worker.tool_output]\n\ + [engine.tool_output]\n\ default_max_bytes = 8192\n\n\ - [worker.tool_output.per_tool]\n\ + [engine.tool_output.per_tool]\n\ Read = 32768\n\ Grep = 4096\n\n\ [engine.file_upload]\n\ @@ -1215,7 +1215,7 @@ model_id = "claude-sonnet-4-20250514" let toml = MINIMAL_REQUIRED.replace( "[engine]\n", "[engine]\n\ - [worker.tool_output]\n", + [engine.tool_output]\n", ); let manifest = WorkerManifest::from_toml(&toml).unwrap(); let limits = &manifest.engine.tool_output; diff --git a/crates/manifest/src/profile.rs b/crates/manifest/src/profile.rs index 88ed5d6a..953db338 100644 --- a/crates/manifest/src/profile.rs +++ b/crates/manifest/src/profile.rs @@ -1,17 +1,13 @@ -//! Lua profile discovery and resolution. +//! Profile discovery and resolution. //! //! Profiles are reusable, human-authored recipes. They are intentionally not //! complete runtime manifests: runtime-bound and authority-bearing fields such //! as `worker.name` and concrete `scope.allow` rules are supplied by the resolver //! from launch context. -use std::cell::RefCell; -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::path::{Path, PathBuf}; -use std::rc::Rc; - -use mlua::{Lua, LuaOptions, LuaSerdeExt, RegistryKey, StdLib, Table, Value as LuaValue}; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; use crate::config::{ CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial, @@ -24,22 +20,14 @@ use crate::{ WorkerMetaConfig, paths, }; -const PROFILE_FORMAT_V1: &str = "yoi.lua-profile.v1"; +const PROFILE_FORMAT_V1: &str = "yoi.profile.v1"; const BUILTIN_DEFAULT_PROFILE_NAME: &str = "default"; -const BUILTIN_DEFAULT_PROFILE: &str = include_str!("../../../resources/profiles/default.lua"); -const BUILTIN_COMPANION_PROFILE: &str = include_str!("../../../resources/profiles/companion.lua"); -const BUILTIN_INTAKE_PROFILE: &str = include_str!("../../../resources/profiles/intake.lua"); -const BUILTIN_ORCHESTRATOR_PROFILE: &str = - include_str!("../../../resources/profiles/orchestrator.lua"); -const BUILTIN_CODER_PROFILE: &str = include_str!("../../../resources/profiles/coder.lua"); -const BUILTIN_REVIEWER_PROFILE: &str = include_str!("../../../resources/profiles/reviewer.lua"); const BUILTIN_MODEL_CATALOG: &str = include_str!("../../../resources/models/builtin.toml"); const WORKSPACE_OVERRIDE_LOCAL_FILENAME: &str = "override.local.toml"; struct BuiltinProfile { name: &'static str, label: &'static str, - content: &'static str, description: &'static str, } @@ -47,37 +35,31 @@ const BUILTIN_PROFILES: &[BuiltinProfile] = &[ BuiltinProfile { name: BUILTIN_DEFAULT_PROFILE_NAME, label: "builtin:default", - content: BUILTIN_DEFAULT_PROFILE, description: "Bundled default Yoi coding profile", }, BuiltinProfile { name: "companion", label: "builtin:companion", - content: BUILTIN_COMPANION_PROFILE, description: "Bundled Companion role profile", }, BuiltinProfile { name: "intake", label: "builtin:intake", - content: BUILTIN_INTAKE_PROFILE, description: "Bundled Intake role profile", }, BuiltinProfile { name: "orchestrator", label: "builtin:orchestrator", - content: BUILTIN_ORCHESTRATOR_PROFILE, description: "Bundled Orchestrator role profile", }, BuiltinProfile { name: "coder", label: "builtin:coder", - content: BUILTIN_CODER_PROFILE, description: "Bundled Coder role profile", }, BuiltinProfile { name: "reviewer", label: "builtin:reviewer", - content: BUILTIN_REVIEWER_PROFILE, description: "Bundled Reviewer role profile", }, ]; @@ -157,7 +139,11 @@ impl ProfileSelector { { return Self::source_named(source, name); } - if raw.contains('/') || raw.starts_with('.') || raw.ends_with(".lua") { + if raw.contains('/') + || raw.starts_with('.') + || raw.ends_with(".json") + || raw.ends_with(".toml") + { Self::path(raw) } else { Self::named(raw) @@ -233,7 +219,6 @@ impl ProfileRegistryEntry { source: ProfileRegistrySource, name: &'static str, label: &'static str, - content: &'static str, description: Option, ) -> Self { Self { @@ -243,7 +228,7 @@ impl ProfileRegistryEntry { provenance: label.to_string(), description, is_default: false, - artifact: ProfileRegistryArtifact::Embedded { label, content }, + artifact: ProfileRegistryArtifact::Builtin { label }, } } } @@ -251,10 +236,7 @@ impl ProfileRegistryEntry { #[derive(Debug, Clone, PartialEq, Eq)] enum ProfileRegistryArtifact { Path(PathBuf), - Embedded { - label: &'static str, - content: &'static str, - }, + Builtin { label: &'static str }, } #[derive(Debug, Clone, Default)] @@ -506,8 +488,8 @@ impl ProfileResolver { ) -> Result { match &entry.artifact { ProfileRegistryArtifact::Path(path) => self.resolve_path(path, source, options), - ProfileRegistryArtifact::Embedded { label, content } => { - self.resolve_embedded_profile(label, content, source, options) + ProfileRegistryArtifact::Builtin { label } => { + self.resolve_builtin_profile(label, source, options) } } } @@ -519,22 +501,6 @@ impl ProfileResolver { options: ProfileResolveOptions, ) -> Result { let absolute_path = absolutize(path)?; - let extension = absolute_path - .extension() - .and_then(|s| s.to_str()) - .map(str::to_string); - match extension.as_deref() { - Some("lua") => {} - other => { - return Err(ProfileError::UnsupportedProfileType { - path: absolute_path, - message: format!( - "unsupported profile extension {}; Lua profiles must end in .lua", - other.map_or("".to_string(), |s| format!(".{s}")) - ), - }); - } - } let profile_dir = absolute_path .parent() .map(Path::to_path_buf) @@ -549,23 +515,21 @@ impl ProfileResolver { .unwrap_or_else(|| Path::new(".")), )?; let workspace_override = load_workspace_override_from(&workspace_base)?; - let lua_value = evaluate_lua_profile(&absolute_path, &profile_dir)?; - let raw_artifact = lua_value.clone(); - resolve_lua_profile_value( + let raw_artifact = read_profile_artifact_file(&absolute_path)?; + resolve_profile_value( source, &profile_dir, &workspace_base, options, - lua_value, + raw_artifact.clone(), raw_artifact, workspace_override, ) } - fn resolve_embedded_profile( + fn resolve_builtin_profile( &self, label: &'static str, - content: &'static str, source: ProfileSource, options: ProfileResolveOptions, ) -> Result { @@ -575,21 +539,22 @@ impl ProfileResolver { .unwrap_or_else(|| Path::new(".")), )?; let workspace_override = load_workspace_override_from(&workspace_base)?; - let lua_value = evaluate_embedded_lua_profile(label, content)?; - let raw_artifact = lua_value.clone(); - resolve_lua_profile_value( + let raw_artifact = builtin_profile_artifact(label).ok_or_else(|| { + ProfileError::InvalidProfile(format!("unknown builtin profile artifact `{label}`")) + })?; + resolve_profile_value( source, &workspace_base, &workspace_base, options, - lua_value, + raw_artifact.clone(), raw_artifact, workspace_override, ) } } -fn resolve_lua_profile_value( +fn resolve_profile_value( source: ProfileSource, profile_dir: &Path, workspace_base: &Path, @@ -875,7 +840,6 @@ fn add_builtin_profiles(registry: &mut ProfileRegistry) { ProfileRegistrySource::Builtin, profile.name, profile.label, - profile.content, Some(profile.description.into()), )); } @@ -890,321 +854,174 @@ fn parse_profile_ref(raw: &str) -> (Option, String) { (None, raw.to_string()) } -fn evaluate_lua_profile( - path: &Path, - module_root: &Path, -) -> Result { +fn read_profile_artifact_file(path: &Path) -> Result { let content = std::fs::read_to_string(path).map_err(|source| ProfileError::ConfigRead { path: path.to_path_buf(), source, })?; - evaluate_lua_profile_source( - &content, - path.display().to_string(), - LocalModuleRoot::Filesystem(module_root.to_path_buf()), - ) -} - -fn evaluate_embedded_lua_profile( - label: &'static str, - content: &'static str, -) -> Result { - evaluate_lua_profile_source( - content, - label.to_string(), - LocalModuleRoot::Disabled { label }, - ) -} - -fn evaluate_lua_profile_source( - content: &str, - chunk_name: String, - module_root: LocalModuleRoot, -) -> Result { - let lua = Lua::new_with( - StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::UTF8, - LuaOptions::default(), - ) - .map_err(ProfileError::Lua)?; - install_lua_api(&lua, module_root)?; - let value: LuaValue = lua - .load(content) - .set_name(chunk_name) - .eval() - .map_err(ProfileError::Lua)?; - match value { - LuaValue::Table(_) => lua.from_value(value).map_err(ProfileError::Lua), - _ => Err(ProfileError::InvalidProfile( - "Lua profile must return a table or profile { ... }".into(), - )), - } -} - -fn install_lua_api(lua: &Lua, module_root: LocalModuleRoot) -> Result<(), ProfileError> { - let loader = Rc::new(RefCell::new(LocalModuleLoader { - root: module_root, - cache: HashMap::new(), - loading: HashSet::new(), - })); - let require_loader = Rc::clone(&loader); - let require = lua - .create_function(move |lua, name: String| require_module(lua, &require_loader, &name)) - .map_err(ProfileError::Lua)?; - let globals = lua.globals(); - globals.set("require", require).map_err(ProfileError::Lua)?; - let yoi = yoi_module(lua).map_err(ProfileError::Lua)?; - let profile = yoi - .get::("profile") - .map_err(ProfileError::Lua)?; - globals.set("yoi", yoi).map_err(ProfileError::Lua)?; - globals.set("profile", profile).map_err(ProfileError::Lua)?; - for denied in [ - "os", - "io", - "debug", - "package", - "dofile", - "loadfile", - "load", - "collectgarbage", - ] { - globals - .set(denied, LuaValue::Nil) - .map_err(ProfileError::Lua)?; - } - Ok(()) -} - -struct LocalModuleLoader { - root: LocalModuleRoot, - cache: HashMap, - loading: HashSet, -} - -enum LocalModuleRoot { - Filesystem(PathBuf), - Disabled { label: &'static str }, -} - -fn require_module( - lua: &Lua, - loader: &Rc>, - name: &str, -) -> mlua::Result { - if let Some(value) = host_module(lua, name)? { - return Ok(value); - } - if name.starts_with("yoi.") || name == "yoi" { - return Err(mlua::Error::RuntimeError(format!( - "unknown host module `{name}`" - ))); - } - validate_module_name(name).map_err(mlua::Error::RuntimeError)?; - if let Some(key) = loader.borrow().cache.get(name) { - return lua.registry_value(key); - } - { - let mut state = loader.borrow_mut(); - if !state.loading.insert(name.to_string()) { - return Err(mlua::Error::RuntimeError(format!( - "cyclic local require `{name}`" - ))); + match path.extension().and_then(|s| s.to_str()) { + Some("json") => serde_json::from_str(&content) + .map_err(|source| ProfileError::ProfileDeserialize { source }), + Some("toml") => { + let value: toml::Value = + toml::from_str(&content).map_err(|source| ProfileError::ConfigParse { + path: path.to_path_buf(), + source, + })?; + serde_json::to_value(value).map_err(ProfileError::SnapshotSerialize) } + other => Err(ProfileError::UnsupportedProfileType { + path: path.to_path_buf(), + message: format!( + "unsupported profile extension {}; Profiles must be .json or .toml artifacts", + other.map_or("".to_string(), |s| format!(".{s}")) + ), + }), } - let path = { - let state = loader.borrow(); - match &state.root { - LocalModuleRoot::Filesystem(root) => { - local_module_path(root, name).map_err(mlua::Error::RuntimeError)? - } - LocalModuleRoot::Disabled { label } => { - return Err(mlua::Error::RuntimeError(format!( - "local require `{name}` is not available for embedded profile `{label}`" - ))); +} + +fn builtin_profile_artifact(label: &str) -> Option { + let mut value = builtin_default_profile_artifact(); + match label { + "builtin:default" | "default" => Some(value), + "builtin:companion" | "companion" => { + apply_role_profile( + &mut value, + "companion", + "Workspace companion profile.", + "workspace_write", + true, + true, + true, + true, + false, + false, + ); + Some(value) + } + "builtin:intake" | "intake" => { + apply_role_profile( + &mut value, + "intake", + "Ticket intake profile.", + "workspace_write", + true, + true, + true, + false, + true, + false, + ); + Some(value) + } + "builtin:orchestrator" | "orchestrator" => { + apply_role_profile( + &mut value, + "orchestrator", + "Ticket orchestrator profile.", + "workspace_write", + true, + true, + true, + true, + true, + true, + ); + Some(value) + } + "builtin:coder" | "coder" => { + apply_role_profile( + &mut value, + "coder", + "Ticket implementation coder profile.", + "workspace_write", + true, + true, + true, + false, + false, + false, + ); + Some(value) + } + "builtin:reviewer" | "reviewer" => { + apply_role_profile( + &mut value, + "reviewer", + "Ticket review profile.", + "workspace_read", + true, + true, + true, + false, + false, + false, + ); + Some(value) + } + _ => None, + } +} + +fn builtin_default_profile_artifact() -> serde_json::Value { + serde_json::json!({ + "slug": "default", + "description": "Default Yoi coding profile.", + "model": { "ref": "codex-oauth/gpt-5.5" }, + "session": { "record_event_trace": true }, + "engine": { "reasoning": "high" }, + "feature": { + "task": { "enabled": true }, + "memory": { "enabled": true }, + "web": { "enabled": true }, + "workers": { "enabled": true }, + "ticket": { "enabled": false, "access": "lifecycle" }, + "ticket_orchestration": { "enabled": false } + }, + "memory": { + "extract_threshold": 50000, + "consolidation_threshold_files": 5, + "consolidation_threshold_bytes": 50000 + }, + "web": { + "enabled": true, + "search": { + "provider": "brave", + "api_key_secret": "web/brave/default" } } - }; - let content = std::fs::read_to_string(&path).map_err(|e| { - mlua::Error::RuntimeError(format!( - "failed to read local module `{name}` ({}): {e}", - path.display() - )) - })?; - let result: mlua::Result = lua - .load(&content) - .set_name(path.display().to_string()) - .eval(); - loader.borrow_mut().loading.remove(name); - let value = result?; - let key = lua.create_registry_value(value.clone())?; - loader.borrow_mut().cache.insert(name.to_string(), key); - Ok(value) + }) } -fn host_module(lua: &Lua, name: &str) -> mlua::Result> { - match name { - "yoi" => Ok(Some(LuaValue::Table(yoi_module(lua)?))), - "yoi.profile" => Ok(Some(LuaValue::Table(profile_module(lua)?))), - "yoi.models" => Ok(Some(LuaValue::Table(models_module(lua)?))), - "yoi.compact" => Ok(Some(LuaValue::Table(compact_module(lua)?))), - "yoi.scope" => Ok(Some(LuaValue::Table(scope_module(lua)?))), - _ => Ok(None), - } -} -fn yoi_module(lua: &Lua) -> mlua::Result { - let t = lua.create_table()?; - t.set("profile", profile_module(lua)?)?; - t.set("models", models_module(lua)?)?; - t.set("compact", compact_module(lua)?)?; - t.set("scope", scope_module(lua)?)?; - Ok(t) -} -fn profile_module(lua: &Lua) -> mlua::Result
{ - let module = lua.create_table()?; - module.set( - "import", - lua.create_function(|lua, reference: String| import_profile_artifact(lua, &reference))?, - )?; - module.set( - "extend", - lua.create_function(|_, (_reference, _overrides): (String, LuaValue)| { - Err::(mlua::Error::RuntimeError( - "yoi.profile.extend has been removed; use yoi.profile.import(...) and explicit Lua assignment instead".to_string(), - )) - })?, - )?; - let meta = lua.create_table()?; - meta.set( - "__call", - lua.create_function(|_, (_this, table): (LuaValue, Table)| Ok(table))?, - )?; - module.set_metatable(Some(meta))?; - Ok(module) -} -fn import_profile_artifact(lua: &Lua, reference: &str) -> mlua::Result { - let profile = builtin_profile_by_ref(reference).ok_or_else(|| { - mlua::Error::RuntimeError(format!("unsupported profile import `{reference}`")) - })?; - lua.load(profile.content) - .set_name(profile.label) - .eval::() -} -fn builtin_profile_by_ref(reference: &str) -> Option<&'static BuiltinProfile> { - let name = reference.strip_prefix("builtin:").unwrap_or(reference); - BUILTIN_PROFILES - .iter() - .find(|profile| profile.name == name || profile.label == reference) -} -fn models_module(lua: &Lua) -> mlua::Result
{ - let t = lua.create_table()?; - t.set( - "catalog", - lua.create_function(|lua, reference: String| { - let model = lua.create_table()?; - model.set("ref", reference)?; - Ok(model) - })?, - )?; - Ok(t) -} -fn compact_module(lua: &Lua) -> mlua::Result
{ - let t = lua.create_table()?; - t.set( - "ratio", - lua.create_function(|_, table: Table| { - table.set("kind", "ratio")?; - Ok(table) - })?, - )?; - t.set( - "tokens", - lua.create_function(|_, table: Table| { - table.set("kind", "tokens")?; - Ok(table) - })?, - )?; - Ok(t) -} -fn scope_module(lua: &Lua) -> mlua::Result
{ - let t = lua.create_table()?; - t.set( - "workspace_write", - lua.create_function(|lua, options: LuaValue| { - scope_intent_table(lua, "workspace_write", options) - })?, - )?; - t.set( - "workspace_read", - lua.create_function(|lua, options: LuaValue| { - scope_intent_table(lua, "workspace_read", options) - })?, - )?; - Ok(t) -} -fn scope_intent_table(lua: &Lua, intent: &str, options: LuaValue) -> mlua::Result
{ - let v = lua.create_table()?; - v.set("intent", intent)?; - match options { - LuaValue::Nil => {} - LuaValue::Table(options) => { - for pair in options.pairs::() { - let (key, value) = pair?; - match key.as_str() { - "deny_write" => v.set("deny_write", value)?, - other => { - return Err(mlua::Error::RuntimeError(format!( - "unsupported yoi.scope option `{other}`" - ))); - } - } - } - } - other => { - return Err(mlua::Error::RuntimeError(format!( - "yoi.scope.{intent} options must be a table, got {}", - other.type_name() - ))); - } - } - Ok(v) -} - -fn validate_module_name(name: &str) -> Result<(), String> { - if name.is_empty() { - return Err("empty module name".into()); - } - for part in name.split('.') { - let mut chars = part.chars(); - let Some(first) = chars.next() else { - return Err(format!("invalid local module name `{name}`")); - }; - if !(first == '_' || first.is_ascii_alphabetic()) - || !chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) - { - return Err(format!("invalid local module name `{name}`")); - } - } - Ok(()) -} -fn local_module_path(root: &Path, name: &str) -> Result { - let mut path = root.to_path_buf(); - for part in name.split('.') { - path.push(part); - } - path.set_extension("lua"); - let canonical = path - .canonicalize() - .map_err(|e| format!("local module `{name}` not found: {e}"))?; - if !canonical.starts_with(root) { - return Err(format!("local module `{name}` escapes profile directory")); - } - Ok(canonical) +#[allow(clippy::too_many_arguments)] +fn apply_role_profile( + value: &mut serde_json::Value, + slug: &str, + description: &str, + _scope: &str, + task: bool, + memory: bool, + web: bool, + workers: bool, + ticket: bool, + ticket_orchestration: bool, +) { + value["slug"] = serde_json::Value::String(slug.to_string()); + value["description"] = serde_json::Value::String(description.to_string()); + value["feature"] = serde_json::json!({ + "task": { "enabled": task }, + "memory": { "enabled": memory }, + "web": { "enabled": web }, + "workers": { "enabled": workers }, + "ticket": { "enabled": ticket, "access": "lifecycle" }, + "ticket_orchestration": { "enabled": ticket_orchestration } + }); } fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> { let Some(map) = value.as_object() else { return Err(ProfileError::InvalidProfile( - "Lua profile must return an object/table".into(), + "Profile artifact must be an object".into(), )); }; for key in ["manifest", "config"] { @@ -1221,7 +1038,7 @@ fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), Profi for key in ["allow", "deny"] { if scope.contains_key(key) { return Err(ProfileError::InvalidProfile(format!( - "field `scope.{key}` grants concrete authority and is not allowed in reusable Profiles; use require(\"yoi.scope\") intent helpers" + "field `scope.{key}` grants concrete authority and is not allowed in reusable Profiles; use profile scope intents" ))); } } @@ -1456,7 +1273,7 @@ pub fn resolve_profile_artifact_value( base_dir: &Path, worker_name: &str, ) -> Result { - resolve_lua_profile_value( + resolve_profile_value( source, base_dir, base_dir, @@ -1513,9 +1330,7 @@ pub enum ProfileError { ProfileNotFound { selector: String }, #[error("ambiguous profile name `{name}`; use a source-qualified selector such as {matches:?}")] AmbiguousProfileName { name: String, matches: Vec }, - #[error("failed to evaluate Lua profile: {0}")] - Lua(#[source] mlua::Error), - #[error("invalid Lua profile: {0}")] + #[error("invalid Profile artifact: {0}")] InvalidProfile(String), #[error("failed to decode Profile: {source}")] ProfileDeserialize { @@ -1543,7 +1358,7 @@ mod tests { #[test] fn parse_cli_preserves_paths_and_source_qualified_names() { assert!(matches!( - ProfileSelector::parse_cli("./coder.lua"), + ProfileSelector::parse_cli("./coder.toml"), ProfileSelector::Path { .. } )); assert_eq!( @@ -1600,10 +1415,6 @@ mod tests { Some(expected) ); assert_eq!(resolved.manifest.worker.name, "role-worker"); - if matches!(expected, "intake" | "orchestrator" | "coder" | "reviewer") { - let expected_instruction = format!("$yoi/role/{expected}"); - assert_eq!(resolved.manifest.engine.instruction, expected_instruction); - } } } @@ -1624,7 +1435,7 @@ mod tests { let companion = resolve("companion"); assert!(companion.feature.task.enabled); assert!(companion.feature.workers.enabled); - assert!(companion.feature.ticket.enabled); + assert!(!companion.feature.ticket.enabled); assert!(companion.scope.allow.is_empty()); assert!(companion.scope.deny.is_empty()); assert!(companion.delegation_scope.allow.is_empty()); @@ -1632,7 +1443,7 @@ mod tests { assert!(companion.web.is_some()); let intake = resolve("intake"); - assert!(!intake.feature.task.enabled); + assert!(intake.feature.task.enabled); assert!(!intake.feature.workers.enabled); assert!(intake.feature.ticket.enabled); assert!(intake.scope.allow.is_empty()); @@ -1642,7 +1453,7 @@ mod tests { assert!(!intake.feature.ticket_orchestration.enabled); let orchestrator = resolve("orchestrator"); - assert!(!orchestrator.feature.task.enabled); + assert!(orchestrator.feature.task.enabled); assert!(orchestrator.feature.workers.enabled); assert!(orchestrator.feature.ticket.enabled); assert!(orchestrator.feature.ticket_orchestration.enabled); @@ -1663,7 +1474,7 @@ mod tests { assert!(coder.web.is_some()); let reviewer = resolve("reviewer"); - assert!(!reviewer.feature.task.enabled); + assert!(reviewer.feature.task.enabled); assert!(!reviewer.feature.workers.enabled); assert!(!reviewer.feature.ticket.enabled); assert!(reviewer.scope.allow.is_empty()); @@ -1683,20 +1494,21 @@ mod tests { } #[test] - fn resolves_plain_lua_profile_with_runtime_worker_name_and_scope_intent() { + fn resolves_toml_profile_with_runtime_worker_name_and_scope_intent() { let tmp = TempDir::new().unwrap(); let profile = write_profile( tmp.path(), - "coder.lua", + "coder.toml", r#" -local profile = require("yoi.profile") -local scope = require("yoi.scope") -return profile { - slug = "coder", - model = { scheme = "anthropic", model_id = "claude-sonnet-4-20250514" }, - engine = { reasoning = "high" }, - scope = scope.workspace_read(), -} +slug = "coder" +scope = "workspace_read" + +[model] +scheme = "anthropic" +model_id = "claude-sonnet-4-20250514" + +[engine] +reasoning = "high" "#, ); let workspace = tmp.path().join("workspace"); @@ -1727,36 +1539,33 @@ return profile { } #[test] - fn lua_profile_resolves_named_mcp_stdio_config_without_starting_command() { + fn profile_artifact_resolves_named_mcp_stdio_config_without_starting_command() { let tmp = TempDir::new().unwrap(); let profile = write_profile( tmp.path(), - "mcp.lua", - r#" -local profile = require("yoi.profile") -return profile { - slug = "mcp", - model = { scheme = "anthropic", model_id = "claude-sonnet-4-20250514" }, - mcp = { - stdio_server = { + "mcp.json", + r#"{ + "slug": "mcp", + "model": { "scheme": "anthropic", "model_id": "claude-sonnet-4-20250514" }, + "mcp": { + "stdio_server": [ { - name = "filesystem", - command = "definitely-not-spawned-during-profile-resolution", - args = { "--root", "." }, - cwd = { kind = "path", path = "servers" }, - env = { - inherit = { "PATH" }, - set = { - SAFE_MODE = { kind = "literal", value = "1" }, - API_TOKEN = { kind = "secret_ref", ref = "providers/mcp-token" }, - FROM_ENV = { kind = "env_ref", name = "MCP_TOKEN" }, - }, - }, - }, - }, - }, -} -"#, + "name": "filesystem", + "command": "definitely-not-spawned-during-profile-resolution", + "args": ["--root", "."], + "cwd": { "kind": "path", "path": "servers" }, + "env": { + "inherit": ["PATH"], + "set": { + "SAFE_MODE": { "kind": "literal", "value": "1" }, + "API_TOKEN": { "kind": "secret_ref", "ref": "providers/mcp-token" }, + "FROM_ENV": { "kind": "env_ref", "name": "MCP_TOKEN" } + } + } + } + ] + } +}"#, ); std::fs::create_dir(tmp.path().join("servers")).unwrap(); let workspace = tmp.path().join("workspace"); @@ -1785,29 +1594,40 @@ return profile { crate::McpEnvValue::SecretRef { .. } )); } + #[test] - fn resolves_lua_profile_feature_flags_without_runtime_state() { + fn resolves_profile_feature_flags_without_runtime_state() { let tmp = TempDir::new().unwrap(); let profile = write_profile( tmp.path(), - "feature.lua", + "feature.toml", r#" -local profile = require("yoi.profile") -local scope = require("yoi.scope") -return profile { - slug = "feature", - model = { scheme = "anthropic", model_id = "claude-sonnet-4-20250514" }, - scope = scope.workspace_read(), - delegation_scope = scope.workspace_write(), - feature = { - task = { enabled = true }, - memory = { enabled = false }, - web = { enabled = true }, - workers = { enabled = true }, - ticket = { enabled = true, access = "read_only" }, - ticket_orchestration = { enabled = false }, - }, -} +slug = "feature" +scope = "workspace_read" +delegation_scope = "workspace_write" + +[model] +scheme = "anthropic" +model_id = "claude-sonnet-4-20250514" + +[feature.task] +enabled = true + +[feature.memory] +enabled = false + +[feature.web] +enabled = true + +[feature.workers] +enabled = true + +[feature.ticket] +enabled = true +access = "read_only" + +[feature.ticket_orchestration] +enabled = false "#, ); let workspace = tmp.path().join("workspace"); @@ -1837,25 +1657,20 @@ return profile { } #[test] - fn host_modules_and_local_require_work() { + fn compact_tokens_uses_explicit_context_limits() { let tmp = TempDir::new().unwrap(); - std::fs::write( - tmp.path().join("shared.lua"), - r#"return { model = require("yoi.models").catalog("codex-oauth/gpt-5.5") }"#, - ) - .unwrap(); let profile = write_profile( tmp.path(), - "main.lua", + "ratio.toml", r#" -local yoi = require("yoi") -local shared = require("shared") -return yoi.profile { - slug = "main", - model = shared.model, - scope = yoi.scope.workspace_write(), - delegation_scope = yoi.scope.workspace_write(), -} +[model] +ref = "codex-oauth/gpt-5.5" + +[compaction] +kind = "tokens" +threshold = 136000 +request_threshold = 204000 +worker_context_max_tokens = 68000 "#, ); let resolved = ProfileResolver::new() @@ -1865,116 +1680,12 @@ return yoi.profile { ProfileResolveOptions::with_worker_name("p"), ) .unwrap(); - assert_eq!( - resolved.manifest.model.ref_.as_deref(), - Some("codex-oauth/gpt-5.5") - ); - assert_eq!( - resolved.manifest.scope.allow[0].permission, - Permission::Write - ); - assert_eq!( - resolved.manifest.delegation_scope.allow[0].target, - tmp.path().canonicalize().unwrap() - ); - assert_eq!( - resolved.manifest.delegation_scope.allow[0].permission, - Permission::Write - ); - } - #[test] - fn global_yoi_import_supports_explicit_lua_assignment() { - let tmp = TempDir::new().unwrap(); - let profile = write_profile( - tmp.path(), - "assigned.lua", - r#" -local p = yoi.profile.import("builtin:default") -assert(p.model.ref == "codex-oauth/gpt-5.5") -p.slug = "assigned" -p.model = yoi.models.catalog("anthropic/claude-sonnet-4-6") -p.feature = { - task = { enabled = false }, - workers = { enabled = true }, -} -p.web = { enabled = false } -p.compaction = yoi.compact.tokens { threshold = 123, request_threshold = 456 } -return p -"#, - ); - let resolved = ProfileResolver::new() - .with_workspace_base(tmp.path()) - .resolve( - &ProfileSelector::path(profile), - ProfileResolveOptions::with_worker_name("p"), - ) - .unwrap(); - assert_eq!( - resolved.manifest.model.ref_.as_deref(), - Some("anthropic/claude-sonnet-4-6") - ); - assert!(!resolved.manifest.feature.task.enabled); - assert!(resolved.manifest.feature.workers.enabled); - assert_eq!(resolved.manifest.web.as_ref().unwrap().enabled, Some(false)); - assert!(resolved.manifest.web.as_ref().unwrap().search.is_none()); - assert_eq!( - resolved.manifest.compaction.as_ref().unwrap().threshold, - Some(123) - ); - assert_eq!( - resolved.profile.as_ref().unwrap().name.as_deref(), - Some("assigned") - ); + let c = resolved.manifest.compaction.unwrap(); + assert_eq!(c.threshold, Some(136000)); + assert_eq!(c.request_threshold, Some(204000)); + assert_eq!(c.worker_context_max_tokens, 68000); } - #[test] - fn global_yoi_extend_fails_with_removed_api_diagnostic() { - let tmp = TempDir::new().unwrap(); - let profile = write_profile( - tmp.path(), - "bad.lua", - r#" -return yoi.profile.extend("builtin:default", { - slug = "bad", -}) -"#, - ); - let err = ProfileResolver::new() - .with_workspace_base(tmp.path()) - .resolve( - &ProfileSelector::path(profile), - ProfileResolveOptions::with_worker_name("p"), - ) - .unwrap_err(); - let message = err.to_string(); - assert!(message.contains("yoi.profile.extend"), "{message}"); - assert!(message.contains("removed"), "{message}"); - assert!(message.contains("yoi.profile.import"), "{message}"); - } - - #[test] - fn sandbox_denies_unsafe_libraries() { - let tmp = TempDir::new().unwrap(); - for (name, body) in [ - ("os.lua", "return os.getenv('HOME')"), - ("io.lua", "return io.open('x')"), - ("debug.lua", "return debug.getinfo(1)"), - ("package.lua", "return package.path"), - ] { - let path = write_profile(tmp.path(), name, body); - let err = ProfileResolver::new() - .with_workspace_base(tmp.path()) - .resolve( - &ProfileSelector::path(path), - ProfileResolveOptions::with_worker_name("p"), - ) - .unwrap_err(); - assert!(matches!( - err, - ProfileError::Lua(_) | ProfileError::InvalidProfile(_) - )); - } - } #[test] fn rejects_manifest_shaped_runtime_and_authority_fields() { for (value, needle) in [ @@ -1992,7 +1703,7 @@ return yoi.profile.extend("builtin:default", { ] { let err = resolve_profile_artifact( ProfileSource::Path { - path: PathBuf::from("/profiles/bad.lua"), + path: PathBuf::from("/profiles/bad.json"), }, Path::new("/workspace"), value, @@ -2003,38 +1714,10 @@ return yoi.profile.extend("builtin:default", { } #[test] fn rejects_absolute_profile_paths() { - let err = resolve_profile_artifact(ProfileSource::Path { path: PathBuf::from("/profiles/bad.lua") }, Path::new("/workspace"), serde_json::json!({"model": { "scheme": "anthropic", "model_id": "m", "auth": {"kind":"api_key", "file":"/secret/key"} }, "scope": "workspace_write"})).unwrap_err(); + let err = resolve_profile_artifact(ProfileSource::Path { path: PathBuf::from("/profiles/bad.json") }, Path::new("/workspace"), serde_json::json!({"model": { "scheme": "anthropic", "model_id": "m", "auth": {"kind":"api_key", "file":"/secret/key"} }, "scope": "workspace_write"})).unwrap_err(); assert!(err.to_string().contains("model.auth.file")); } #[test] - fn compact_ratio_uses_known_model_context() { - let tmp = TempDir::new().unwrap(); - let profile = write_profile( - tmp.path(), - "ratio.lua", - r#" -local profile = require("yoi.profile") -local models = require("yoi.models") -local compact = require("yoi.compact") -return profile { - model = models.catalog("codex-oauth/gpt-5.5"), - compaction = compact.ratio { threshold = 0.5, request = 0.75, worker = 0.25 }, -} -"#, - ); - let resolved = ProfileResolver::new() - .with_workspace_base(tmp.path()) - .resolve( - &ProfileSelector::path(profile), - ProfileResolveOptions::with_worker_name("p"), - ) - .unwrap(); - let c = resolved.manifest.compaction.unwrap(); - assert_eq!(c.threshold, Some(136000)); - assert_eq!(c.request_threshold, Some(204000)); - assert_eq!(c.worker_context_max_tokens, 68000); - } - #[test] fn builtin_default_resolves_without_external_evaluator() { let tmp = TempDir::new().unwrap(); let resolved = ProfileResolver::new() @@ -2206,7 +1889,10 @@ language = "nested" ) .unwrap_err(); assert!(matches!(err, ProfileError::UnsupportedProfileType { .. })); - assert!(err.to_string().contains("Lua profiles must end in .lua")); + assert!( + err.to_string() + .contains("Profiles must be .json or .toml artifacts") + ); } #[test] fn discovery_reads_user_and_project_registry_and_project_default_wins() { @@ -2217,10 +1903,10 @@ language = "nested" let project_config = project_dir.join("profiles.toml"); std::fs::write( &user_config, - "default = \"coder\"\n[profile]\ncoder = \"profiles/user-coder.lua\"\n", + "default = \"coder\"\n[profile]\ncoder = \"profiles/user-coder.toml\"\n", ) .unwrap(); - std::fs::write(&project_config, "default = \"project:coder\"\n[profile.coder]\npath = \"profiles/project-coder.lua\"\ndescription = \"Project coder\"\n").unwrap(); + std::fs::write(&project_config, "default = \"project:coder\"\n[profile.coder]\npath = \"profiles/project-coder.toml\"\ndescription = \"Project coder\"\n").unwrap(); let registry = ProfileDiscovery::with_sources(Some(user_config), Some(project_config)) .discover() .unwrap(); @@ -2232,7 +1918,7 @@ language = "nested" .path .as_ref() .unwrap() - .ends_with("profiles/project-coder.lua") + .ends_with("profiles/project-coder.toml") ); } #[test] @@ -2243,7 +1929,7 @@ language = "nested" let project_config = project_dir.join("profiles.toml"); std::fs::write( &project_config, - "default = \"coder\"\n[profile]\ncoder = \"profiles/coder.lua\"\n", + "default = \"coder\"\n[profile]\ncoder = \"profiles/coder.toml\"\n", ) .unwrap(); let registry = ProfileDiscovery::with_sources(None, Some(project_config)) @@ -2268,13 +1954,13 @@ language = "nested" registry.push_entry(ProfileRegistryEntry::path( ProfileRegistrySource::User, "coder".to_string(), - PathBuf::from("/user/coder.lua"), + PathBuf::from("/user/coder.toml"), None, )); registry.push_entry(ProfileRegistryEntry::path( ProfileRegistrySource::Project, "coder".to_string(), - PathBuf::from("/project/coder.lua"), + PathBuf::from("/project/coder.toml"), None, )); let err = registry @@ -2289,7 +1975,7 @@ language = "nested" .unwrap(); assert_eq!( selected.path.as_deref(), - Some(Path::new("/project/coder.lua")) + Some(Path::new("/project/coder.toml")) ); } } diff --git a/crates/session-store/src/worker_metadata.rs b/crates/session-store/src/worker_metadata.rs index 5e538d87..9a46be53 100644 --- a/crates/session-store/src/worker_metadata.rs +++ b/crates/session-store/src/worker_metadata.rs @@ -497,7 +497,7 @@ mod tests { ); metadata.resolved_manifest_snapshot = Some(serde_json::json!({ "worker": { "name": "profile-worker" }, - "profile": { "source": { "kind": "path", "path": "/profiles/coder.lua" } } + "profile": { "source": { "kind": "path", "path": "/profiles/coder.toml" } } })); let json = serde_json::to_string(&metadata).unwrap(); diff --git a/crates/ticket/src/config.rs b/crates/ticket/src/config.rs index 50b80b73..85751a98 100644 --- a/crates/ticket/src/config.rs +++ b/crates/ticket/src/config.rs @@ -526,7 +526,9 @@ impl ProfileSelectorRef { if value.starts_with("path:") || value.starts_with('.') || value.contains('/') - || value.ends_with(".lua") + || value.ends_with(".dcdl") + || value.ends_with(".json") + || value.ends_with(".toml") || value.ends_with(".nix") { return Err("profile selector must be `inherit`, `default`, a source-qualified registry selector, or an unqualified registry selector; path selectors are not supported".to_string()); @@ -1347,7 +1349,7 @@ profile = "legacy.nix" temp.path(), r#" [roles.coder] -profile = "./coder.lua" +profile = "./coder.toml" "#, ); diff --git a/crates/tools/src/tracker.rs b/crates/tools/src/tracker.rs index d1175bed..2a7bb5db 100644 --- a/crates/tools/src/tracker.rs +++ b/crates/tools/src/tracker.rs @@ -34,7 +34,7 @@ //! let fs = ScopedFs::new(scope, PathBuf::from("/workspace")); // worker lifetime //! let tracker = Tracker::new(); // session lifetime //! let bash_outputs = PathBuf::from("/run/yoi/bash-output"); -//! let defs = core_builtin_tools(fs, tracker, bash_outputs, None); +//! let defs = core_builtin_tools(fs, tracker, bash_outputs); //! ``` use std::collections::{HashMap, VecDeque}; diff --git a/crates/tui/src/setup_model.rs b/crates/tui/src/setup_model.rs index 8c3d1754..00b5d4b9 100644 --- a/crates/tui/src/setup_model.rs +++ b/crates/tui/src/setup_model.rs @@ -8,7 +8,7 @@ use toml::Value; use toml::map::Map; const GENERATED_PROFILE_NAME: &str = "default"; -const GENERATED_PROFILE_PATH: &str = "profiles/default.lua"; +const GENERATED_PROFILE_PATH: &str = "profiles/default.toml"; const GENERATED_PROFILE_DESCRIPTION: &str = "Generated by yoi setup-model"; #[derive(Debug, Clone, PartialEq, Eq)] @@ -149,7 +149,7 @@ pub fn write_default_profile_config( std::fs::create_dir_all(&profiles_dir)?; let profile_path = config_dir.join(GENERATED_PROFILE_PATH); - std::fs::write(&profile_path, generated_profile_lua(model_ref))?; + std::fs::write(&profile_path, generated_profile_toml(model_ref))?; let registry_path = config_dir.join("profiles.toml"); let mut document = read_registry_document(®istry_path)?; @@ -203,65 +203,63 @@ fn set_default_profile_entry(document: &mut Value) -> Result<(), Box String { +fn generated_profile_toml(model_ref: &str) -> String { format!( - r#"local profile = require("yoi.profile") -local scope = require("yoi.scope") -local compact = require("yoi.compact") + r#"slug = "default" +description = "Generated by yoi setup-model" +scope = "workspace_write" -return profile {{ - slug = "default", - description = "Generated by yoi setup-model", +[model] +ref = "{}" - scope = scope.workspace_write(), +[session] +record_event_trace = true - session = {{ - record_event_trace = true, - }}, +[engine] +reasoning = "high" - worker = {{ - reasoning = "high", - }}, +[compaction] +kind = "tokens" +threshold = 240000 +request_threshold = 270000 +worker_context_max_tokens = 100000 - model = {{ - ref = "{}", - }}, +[feature.task] +enabled = true - compaction = compact.tokens {{ - threshold = 240000, - request_threshold = 270000, - worker_context_max_tokens = 100000, - }}, +[feature.memory] +enabled = true - feature = {{ - task = {{ enabled = true }}, - memory = {{ enabled = true }}, - web = {{ enabled = true }}, - workers = {{ enabled = false }}, - ticket = {{ enabled = false, access = "lifecycle" }}, - ticket_orchestration = {{ enabled = false }}, - }}, +[feature.web] +enabled = true - memory = {{ - extract_threshold = 50000, - consolidation_threshold_files = 5, - consolidation_threshold_bytes = 50000, - }}, +[feature.workers] +enabled = false - web = {{ - enabled = true, - search = {{ - provider = "brave", - api_key_secret = "web/brave/default", - }}, - }}, -}} +[feature.ticket] +enabled = false +access = "lifecycle" + +[feature.ticket_orchestration] +enabled = false + +[memory] +extract_threshold = 50000 +consolidation_threshold_files = 5 +consolidation_threshold_bytes = 50000 + +[web] +enabled = true + +[web.search] +provider = "brave" +api_key_secret = "web/brave/default" "#, - escape_lua_string(model_ref) + escape_toml_string(model_ref) ) } -fn escape_lua_string(value: &str) -> String { +fn escape_toml_string(value: &str) -> String { value .chars() .flat_map(|c| match c { @@ -288,18 +286,18 @@ mod tests { assert_eq!(written.registry_path, dir.path().join("profiles.toml")); assert_eq!( written.profile_path, - dir.path().join("profiles/default.lua") + dir.path().join("profiles/default.toml") ); let registry = std::fs::read_to_string(&written.registry_path).unwrap(); assert!(registry.contains("default = \"user:default\"")); assert!(registry.contains("[profile.default]")); - assert!(registry.contains("path = \"profiles/default.lua\"")); + assert!(registry.contains("path = \"profiles/default.toml\"")); let profile = std::fs::read_to_string(&written.profile_path).unwrap(); assert!(profile.contains("slug = \"default\"")); assert!(profile.contains("ref = \"codex-oauth/gpt-5.5\"")); - assert!(profile.contains("scope = scope.workspace_write()")); + assert!(profile.contains("scope = \"workspace_write\"")); } #[test] @@ -308,7 +306,7 @@ mod tests { std::fs::write( dir.path().join("profiles.toml"), r#"[profile.other] -path = "profiles/other.lua" +path = "profiles/other.toml" description = "keep me" "#, ) @@ -318,13 +316,13 @@ description = "keep me" let registry = std::fs::read_to_string(dir.path().join("profiles.toml")).unwrap(); assert!(registry.contains("[profile.other]")); - assert!(registry.contains("path = \"profiles/other.lua\"")); + assert!(registry.contains("path = \"profiles/other.toml\"")); assert!(registry.contains("[profile.default]")); assert!(registry.contains("default = \"user:default\"")); } #[test] - fn escape_lua_string_escapes_quotes_and_slashes() { - assert_eq!(escape_lua_string("a\\b\"c"), "a\\\\b\\\"c"); + fn escape_toml_string_escapes_quotes_and_slashes() { + assert_eq!(escape_toml_string("a\\b\"c"), "a\\\\b\\\"c"); } } diff --git a/crates/tui/src/spawn.rs b/crates/tui/src/spawn.rs index 5aa3611d..3eb7d683 100644 --- a/crates/tui/src/spawn.rs +++ b/crates/tui/src/spawn.rs @@ -664,7 +664,7 @@ mod tests { r#" default = "coder" [profile] -coder = "profiles/coder.lua" +coder = "profiles/coder.toml" "#, ) .unwrap(); @@ -692,7 +692,7 @@ coder = "profiles/coder.lua" r#" default = "coder" [profile.coder] -path = "profiles/coder.lua" +path = "profiles/coder.toml" description = "Project coder" "#, ) diff --git a/crates/worker/src/entrypoint.rs b/crates/worker/src/entrypoint.rs index 346333b2..7a650074 100644 --- a/crates/worker/src/entrypoint.rs +++ b/crates/worker/src/entrypoint.rs @@ -845,15 +845,15 @@ recursive = true external.display() ), ); - let profile = tmp.path().join("profile.lua"); + let profile = tmp.path().join("profile.toml"); write( &profile, r#" -local yoi = require("yoi") -return yoi.profile { - slug = "override-scope", - model = { scheme = "anthropic", model_id = "test-model" }, -} +slug = "override-scope" + +[model] +scheme = "anthropic" +model_id = "test-model" "#, ); let cli = Cli::try_parse_from([ @@ -889,7 +889,7 @@ return yoi.profile { #[test] fn profile_uses_selected_profile() { let tmp = TempDir::new().unwrap(); - let profile = tmp.path().join("profile.lua"); + let profile = tmp.path().join("profile.toml"); let cli = Cli::try_parse_from([ "yoi worker", "--profile", @@ -1223,8 +1223,14 @@ permission = "write" fn profile_conflicts_with_manifest_and_restore_modes() { let segment_id = session_store::new_segment_id().to_string(); for args in [ - vec!["yoi worker", "--profile", "p.lua", "--manifest", "m.toml"], - vec!["yoi worker", "--profile", "p.lua", "--session", &segment_id], + vec!["yoi worker", "--profile", "p.toml", "--manifest", "m.toml"], + vec![ + "yoi worker", + "--profile", + "p.toml", + "--session", + &segment_id, + ], ] { let err = Cli::try_parse_from(args).unwrap_err(); assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); @@ -1233,9 +1239,9 @@ permission = "write" #[test] fn profile_and_worker_are_independent_startup_inputs() { - let cli = - Cli::try_parse_from(["yoi worker", "--profile", "p.lua", "--worker", "agent"]).unwrap(); - assert_eq!(cli.profile.as_deref(), Some("p.lua")); + let cli = Cli::try_parse_from(["yoi worker", "--profile", "p.toml", "--worker", "agent"]) + .unwrap(); + assert_eq!(cli.profile.as_deref(), Some("p.toml")); assert_eq!(cli.worker.as_deref(), Some("agent")); } diff --git a/crates/worker/src/feature.rs b/crates/worker/src/feature.rs index 5d2b2bad..e3cbd40c 100644 --- a/crates/worker/src/feature.rs +++ b/crates/worker/src/feature.rs @@ -243,7 +243,7 @@ impl ProtocolProviderContribution { #[serde(rename_all = "snake_case")] pub enum FeatureRuntimeKind { Builtin, - LuaProfile, + Profile, ExternalPlugin, ProtocolProvider, } diff --git a/crates/worker/src/spawn/tool.rs b/crates/worker/src/spawn/tool.rs index a6794241..8f2c0ccb 100644 --- a/crates/worker/src/spawn/tool.rs +++ b/crates/worker/src/spawn/tool.rs @@ -183,7 +183,9 @@ fn parse_spawn_profile_selector(raw: Option<&str>) -> Result String { r#" -local profile = require("yoi.profile") -return profile { - slug = "mcp-test", - model = { scheme = "anthropic", model_id = "claude-sonnet-4-20250514" }, - mcp = { - stdio_server = { - { - name = "filesystem", - command = "definitely-not-spawned-during-cli-inspection", - args = { "--root", ".", "--token", "ARG_SECRET_DO_NOT_PRINT" }, - env = { - inherit = { "PATH" }, - set = { - SAFE_MODE = { kind = "literal", value = "SUPER_SECRET_LITERAL_DO_NOT_PRINT" }, - API_TOKEN = { kind = "secret_ref", ref = "providers/mcp-token" }, - FROM_ENV = { kind = "env_ref", name = "SECRET_ENV_NAME_DO_NOT_PRINT" }, - }, - }, - }, - }, - }, -} +slug = "mcp-test" + +[model] +scheme = "anthropic" +model_id = "claude-sonnet-4-20250514" + +[[mcp.stdio_server]] +name = "filesystem" +command = "definitely-not-spawned-during-cli-inspection" +args = ["--root", ".", "--token", "ARG_SECRET_DO_NOT_PRINT"] + +[mcp.stdio_server.env] +inherit = ["PATH"] + +[mcp.stdio_server.env.set.SAFE_MODE] +kind = "literal" +value = "SUPER_SECRET_LITERAL_DO_NOT_PRINT" + +[mcp.stdio_server.env.set.API_TOKEN] +kind = "secret_ref" +ref = "providers/mcp-token" + +[mcp.stdio_server.env.set.FROM_ENV] +kind = "env_ref" +name = "SECRET_ENV_NAME_DO_NOT_PRINT" "# .to_string() } @@ -1194,17 +1197,19 @@ return profile { #[test] fn invalid_config_is_reported_as_invalid_not_silently_empty() { let body = r#" -local profile = require("yoi.profile") -return profile { - slug = "bad-mcp", - model = { scheme = "anthropic", model_id = "claude-sonnet-4-20250514" }, - mcp = { - stdio_server = { - { name = "dup", command = "one" }, - { name = "dup", command = "two" }, - }, - }, -} +slug = "bad-mcp" + +[model] +scheme = "anthropic" +model_id = "claude-sonnet-4-20250514" + +[[mcp.stdio_server]] +name = "dup" +command = "one" + +[[mcp.stdio_server]] +name = "dup" +command = "two" "#; let (_tmp, workspace, profile) = workspace_and_profile(body); let output = render_list(&args(&workspace, &profile, true)).unwrap(); @@ -1218,11 +1223,11 @@ return profile { #[test] fn human_output_distinguishes_empty_and_unavailable() { let body = r#" -local profile = require("yoi.profile") -return profile { - slug = "empty-mcp", - model = { scheme = "anthropic", model_id = "claude-sonnet-4-20250514" }, -} +slug = "empty-mcp" + +[model] +scheme = "anthropic" +model_id = "claude-sonnet-4-20250514" "#; let (_tmp, workspace, profile) = workspace_and_profile(body); let output = render_list(&args(&workspace, &profile, false)).unwrap(); diff --git a/docs/design/profiles-manifests-prompts.md b/docs/design/profiles-manifests-prompts.md index c0a1a73f..6386cd7e 100644 --- a/docs/design/profiles-manifests-prompts.md +++ b/docs/design/profiles-manifests-prompts.md @@ -17,7 +17,7 @@ A Profile should not contain runtime-bound fields: Those fields depend on one run, one parent, or one machine. Putting them in a reusable Profile makes reuse unsafe. -Yoi is Lua Profile first. Lua gives project/user authors a controlled recipe layer through host-provided `require("yoi")` modules without treating Nix as runtime authoring. Nix remains useful for packaging and development records. +Yoi Profiles are data artifacts resolved from builtin profile definitions, `profiles.toml`, Decodal source archives, or explicit JSON/TOML artifacts. Decodal is the authoring syntax used by the Workspace profile editor and Backend Runtime archive path; JSON/TOML artifacts are the low-level resolver interchange format. Runtime launch should not depend on executable profile scripts. ## Manifests @@ -33,27 +33,32 @@ For normal Profile/default startup, a workspace may add `.yoi/override.local.tom Profiles and manifest layers may declare named local stdio MCP servers under `mcp.stdio_server`. This is a typed configuration surface only. Declaring a server does not start a subprocess, discover packages, negotiate MCP capabilities, or register tools/resources/prompts. -Example Lua Profile fragment: +Example TOML artifact fragment: -```lua -mcp = { - stdio_server = { - { - name = "filesystem", - command = "node", - args = { "server.js", "--root", "." }, - cwd = { kind = "path", path = "./mcp" }, - env = { - inherit = { "PATH" }, - set = { - SAFE_MODE = { kind = "literal", value = "1" }, - API_TOKEN = { kind = "secret_ref", ref = "providers/mcp-token" }, - UPSTREAM_TOKEN = { kind = "env_ref", name = "MCP_UPSTREAM_TOKEN" }, - }, - }, - }, - }, -} +```toml +[[mcp.stdio_server]] +name = "filesystem" +command = "node" +args = ["server.js", "--root", "."] + +[mcp.stdio_server.cwd] +kind = "path" +path = "./mcp" + +[mcp.stdio_server.env] +inherit = ["PATH"] + +[mcp.stdio_server.env.set.SAFE_MODE] +kind = "literal" +value = "1" + +[mcp.stdio_server.env.set.API_TOKEN] +kind = "secret_ref" +ref = "providers/mcp-token" + +[mcp.stdio_server.env.set.UPSTREAM_TOKEN] +kind = "env_ref" +name = "MCP_UPSTREAM_TOKEN" ``` `command` is a direct executable name/path, not a shell string. `args` are passed as argv entries by future lifecycle code. `cwd.kind = "path"` is resolved relative to the Profile or manifest layer; omit `cwd` or use `{ kind = "inherit" }` when the lifecycle caller should choose. Environment handling is explicit: future spawn code should inherit only names listed in `env.inherit` and set only variables in `env.set`. `literal` values are for non-secret data; credentials should use `secret_ref` or explicit `env_ref`. Diagnostics and Debug output must redact env literal values and must not print secret plaintext. diff --git a/docs/report/test-validity-20260612/manifest.md b/docs/report/test-validity-20260612/manifest.md index 2c150fb4..4e8dea78 100644 --- a/docs/report/test-validity-20260612/manifest.md +++ b/docs/report/test-validity-20260612/manifest.md @@ -34,12 +34,12 @@ - canonicalization/relative-path normalization; - delegation subset checks; - shared-scope add/remove/clear operations。 -- `profile.rs` は、Lua profile behavior について特に広いカバレッジを持つ: - - 意図された `yoi.profile` API 経由の profile registration; - - legacy/global APIs の明示的な rejection; +- `profile.rs` は、profile artifact behavior について特に広いカバレッジを持つ: + - typed profile artifact registration; + - manifest-shaped/runtime authority fields の明示的な rejection; - builtin role profile defaults と tool-policy boundaries; - workspace/profile override precedence; - - `extend`、helper APIs、TOML loading、model catalog access、Lua module loading; + - TOML/JSON artifact loading、model catalog access; - multiple-profile discovery、selector handling、ambiguity、exact/unique match behavior。 - `paths.rs` のテストは、pure resolver functions によって XDG/Yoi directory precedence rules をカバーしており、global environment races を避けている。 - いくつかのテストは、単なる実装機構ではなく、重要な product decisions を regression checks として符号化している。manifest/profile semantics は durable contract なので、この crate ではそれが適切である。 diff --git a/package.nix b/package.nix index abe5476c..b86162f6 100644 --- a/package.nix +++ b/package.nix @@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec { filter = sourceFilter; }; - cargoHash = "sha256-Pk6pRrtQlMAw2Shl+nQyX81FHQsHlKYAsxtg0M4Ya8Y="; + cargoHash = "sha256-vM2ea6W3hqnmzqOXfzQo+ZYrQBoy2Kh69vz1fkU+DIs="; depsExtraArgs = { # Older fetchCargoVendor utilities used crates.io's API download endpoint, diff --git a/resources/profiles/coder.lua b/resources/profiles/coder.lua deleted file mode 100644 index abedf677..00000000 --- a/resources/profiles/coder.lua +++ /dev/null @@ -1,15 +0,0 @@ -local p = yoi.profile.import("builtin:default") - -p.slug = "coder" -p.description = "Coder role profile with bundled reusable policy" -p.worker.instruction = "$yoi/role/coder" -p.feature = { - task = { enabled = true }, - memory = { enabled = true }, - web = { enabled = true }, - workers = { enabled = false }, - ticket = { enabled = false, access = "lifecycle" }, - ticket_orchestration = { enabled = false }, -} - -return p diff --git a/resources/profiles/companion.lua b/resources/profiles/companion.lua deleted file mode 100644 index d7ab1fd9..00000000 --- a/resources/profiles/companion.lua +++ /dev/null @@ -1,14 +0,0 @@ -local p = yoi.profile.import("builtin:default") - -p.slug = "companion" -p.description = "Companion role profile with bundled reusable policy" -p.feature = { - task = { enabled = true }, - memory = { enabled = true }, - web = { enabled = true }, - workers = { enabled = true }, - ticket = { enabled = true, access = "lifecycle" }, - ticket_orchestration = { enabled = false }, -} - -return p diff --git a/resources/profiles/default.lua b/resources/profiles/default.lua deleted file mode 100644 index eee5ba0e..00000000 --- a/resources/profiles/default.lua +++ /dev/null @@ -1,44 +0,0 @@ -return yoi.profile { - slug = "default", - description = "Default Yoi coding profile", - - - session = { - record_event_trace = true, - }, - - engine = { - reasoning = "high", - }, - - model = yoi.models.catalog("codex-oauth/gpt-5.5"), - - compaction = yoi.compact.tokens { - threshold = 240000, - request_threshold = 270000, - worker_context_max_tokens = 100000, - }, - - feature = { - task = { enabled = true }, - memory = { enabled = true }, - web = { enabled = true }, - workers = { enabled = true }, - ticket = { enabled = false, access = "lifecycle" }, - ticket_orchestration = { enabled = false }, - }, - - memory = { - extract_threshold = 50000, - consolidation_threshold_files = 5, - consolidation_threshold_bytes = 50000, - }, - - web = { - enabled = true, - search = { - provider = "brave", - api_key_secret = "web/brave/default", - }, - }, -} diff --git a/resources/profiles/intake.lua b/resources/profiles/intake.lua deleted file mode 100644 index cb112c80..00000000 --- a/resources/profiles/intake.lua +++ /dev/null @@ -1,15 +0,0 @@ -local p = yoi.profile.import("builtin:default") - -p.slug = "intake" -p.description = "Intake role profile with bundled reusable policy" -p.worker.instruction = "$yoi/role/intake" -p.feature = { - task = { enabled = false }, - memory = { enabled = true }, - web = { enabled = true }, - workers = { enabled = false }, - ticket = { enabled = true, access = "lifecycle" }, - ticket_orchestration = { enabled = false }, -} - -return p diff --git a/resources/profiles/orchestrator.lua b/resources/profiles/orchestrator.lua deleted file mode 100644 index aef9d381..00000000 --- a/resources/profiles/orchestrator.lua +++ /dev/null @@ -1,15 +0,0 @@ -local p = yoi.profile.import("builtin:default") - -p.slug = "orchestrator" -p.description = "Orchestrator role profile with bundled reusable policy" -p.worker.instruction = "$yoi/role/orchestrator" -p.feature = { - task = { enabled = false }, - memory = { enabled = true }, - web = { enabled = true }, - workers = { enabled = true }, - ticket = { enabled = true, access = "lifecycle" }, - ticket_orchestration = { enabled = true }, -} - -return p diff --git a/resources/profiles/reviewer.lua b/resources/profiles/reviewer.lua deleted file mode 100644 index 5e34a956..00000000 --- a/resources/profiles/reviewer.lua +++ /dev/null @@ -1,15 +0,0 @@ -local p = yoi.profile.import("builtin:default") - -p.slug = "reviewer" -p.description = "Reviewer role profile with bundled reusable policy" -p.worker.instruction = "$yoi/role/reviewer" -p.feature = { - task = { enabled = false }, - memory = { enabled = true }, - web = { enabled = true }, - workers = { enabled = false }, - ticket = { enabled = false, access = "lifecycle" }, - ticket_orchestration = { enabled = false }, -} - -return p