yoi/crates/manifest/src/profile.rs

1555 lines
51 KiB
Rust

//! Lua 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 `pod.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 crate::config::{CompactionConfigPartial, PermissionConfigPartial, SessionConfigPartial};
use crate::model::{AuthRef, ModelManifest};
use crate::{
MemoryConfig, Permission, PodManifest, PodManifestConfig, PodMetaConfig, ResolveError,
ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifestConfig, paths,
};
const PROFILE_FORMAT_V1: &str = "insomnia.lua-profile.v1";
const BUILTIN_DEFAULT_PROFILE_NAME: &str = "default";
const DEFAULT_POD_NAME: &str = "insomnia";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProfileRegistrySource {
Builtin,
User,
Project,
}
impl ProfileRegistrySource {
pub fn as_str(self) -> &'static str {
match self {
Self::Builtin => "builtin",
Self::User => "user",
Self::Project => "project",
}
}
fn parse(raw: &str) -> Option<Self> {
match raw {
"builtin" => Some(Self::Builtin),
"user" => Some(Self::User),
"project" => Some(Self::Project),
_ => None,
}
}
}
impl std::fmt::Display for ProfileRegistrySource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ProfileSelector {
Path {
path: PathBuf,
},
Named {
#[serde(default, skip_serializing_if = "Option::is_none")]
source: Option<ProfileRegistrySource>,
name: String,
},
Default,
}
impl ProfileSelector {
pub fn path(path: impl Into<PathBuf>) -> Self {
Self::Path { path: path.into() }
}
pub fn named(name: impl Into<String>) -> Self {
Self::Named {
source: None,
name: name.into(),
}
}
pub fn source_named(source: ProfileRegistrySource, name: impl Into<String>) -> Self {
Self::Named {
source: Some(source),
name: name.into(),
}
}
pub fn parse_cli(raw: &str) -> Self {
if raw == "default" {
return Self::Default;
}
if let Some(path) = raw.strip_prefix("path:") {
return Self::path(path);
}
if let Some((prefix, name)) = raw.split_once(':')
&& let Some(source) = ProfileRegistrySource::parse(prefix)
{
return Self::source_named(source, name);
}
if raw.contains('/') || raw.starts_with('.') || raw.ends_with(".lua") {
Self::path(raw)
} else {
Self::named(raw)
}
}
pub fn display_label(&self) -> String {
match self {
Self::Path { path } => path.display().to_string(),
Self::Named { source, name } => match source {
Some(source) => format!("{source}:{name}"),
None => name.clone(),
},
Self::Default => "default".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ProfileSource {
Path {
path: PathBuf,
},
Registry {
source: ProfileRegistrySource,
name: String,
path: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfileRegistryEntry {
pub source: ProfileRegistrySource,
pub name: String,
pub path: PathBuf,
pub description: Option<String>,
pub is_default: bool,
}
impl ProfileRegistryEntry {
pub fn qualified_name(&self) -> String {
format!("{}:{}", self.source, self.name)
}
}
#[derive(Debug, Clone, Default)]
pub struct ProfileRegistry {
entries: Vec<ProfileRegistryEntry>,
default: Option<ProfileDefault>,
}
impl ProfileRegistry {
pub fn entries(&self) -> &[ProfileRegistryEntry] {
&self.entries
}
pub fn default_entry(&self) -> Result<&ProfileRegistryEntry, ProfileError> {
let default = self
.default
.as_ref()
.ok_or(ProfileError::NoDefaultProfile)?;
self.select_named(default.source, &default.name)
}
pub fn select(
&self,
selector: &ProfileSelector,
) -> Result<&ProfileRegistryEntry, ProfileError> {
match selector {
ProfileSelector::Path { .. } => Err(ProfileError::InvalidProfile(
"path selectors are not registry entries".into(),
)),
ProfileSelector::Default => self.default_entry(),
ProfileSelector::Named { source, name } => self.select_named(*source, name),
}
}
fn select_named(
&self,
source: Option<ProfileRegistrySource>,
name: &str,
) -> Result<&ProfileRegistryEntry, ProfileError> {
let matches: Vec<_> = self
.entries
.iter()
.filter(|entry| entry.name == name && source.is_none_or(|s| s == entry.source))
.collect();
match matches.as_slice() {
[entry] => Ok(*entry),
[] => Err(ProfileError::ProfileNotFound {
selector: source.map_or_else(|| name.to_string(), |s| format!("{s}:{name}")),
}),
_ => Err(ProfileError::AmbiguousProfileName {
name: name.to_string(),
matches: matches.iter().map(|entry| entry.qualified_name()).collect(),
}),
}
}
fn push_entry(&mut self, entry: ProfileRegistryEntry) {
self.entries.push(entry);
}
fn set_default(&mut self, default: ProfileDefault) {
self.default = Some(default);
}
fn set_builtin_default_if_available(&mut self) {
if self.default.is_some() {
return;
}
if self
.select_named(
Some(ProfileRegistrySource::Builtin),
BUILTIN_DEFAULT_PROFILE_NAME,
)
.is_ok()
{
self.default = Some(ProfileDefault {
source: Some(ProfileRegistrySource::Builtin),
name: BUILTIN_DEFAULT_PROFILE_NAME.to_string(),
});
}
}
fn mark_default_flags(&mut self) {
let Some(default) = self.default.clone() else {
return;
};
let Ok(default_entry) = self.select_named(default.source, &default.name) else {
return;
};
let source = default_entry.source;
let name = default_entry.name.clone();
for entry in &mut self.entries {
entry.is_default = entry.source == source && entry.name == name;
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ProfileDefault {
source: Option<ProfileRegistrySource>,
name: String,
}
#[derive(Debug, Clone)]
pub struct ProfileDiscovery {
builtin_dir: Option<PathBuf>,
user_config: Option<PathBuf>,
project_config: Option<PathBuf>,
}
impl ProfileDiscovery {
pub fn for_cwd(cwd: &Path) -> Self {
Self {
builtin_dir: paths::builtin_profiles_dir(),
user_config: paths::user_profiles_path(),
project_config: find_project_profiles_from(cwd),
}
}
pub fn with_sources(
builtin_dir: Option<PathBuf>,
user_config: Option<PathBuf>,
project_config: Option<PathBuf>,
) -> Self {
Self {
builtin_dir,
user_config,
project_config,
}
}
pub fn discover(&self) -> Result<ProfileRegistry, ProfileError> {
let mut registry = ProfileRegistry::default();
if let Some(dir) = &self.builtin_dir {
discover_profile_dir(&mut registry, ProfileRegistrySource::Builtin, dir)?;
}
if let Some(path) = &self.user_config {
load_profile_registry_file(&mut registry, ProfileRegistrySource::User, path)?;
}
if let Some(path) = &self.project_config {
load_profile_registry_file(&mut registry, ProfileRegistrySource::Project, path)?;
}
registry.set_builtin_default_if_available();
registry.mark_default_flags();
Ok(registry)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileManifestSnapshot {
pub source: ProfileSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<ProfileMetadata>,
}
#[derive(Debug, Clone)]
pub struct ResolvedProfile {
pub source: ProfileSource,
pub profile: Option<ProfileMetadata>,
pub manifest: PodManifest,
pub manifest_snapshot: serde_json::Value,
pub raw_artifact: serde_json::Value,
}
#[derive(Debug, Clone, Default)]
pub struct ProfileResolveOptions {
pub pod_name: Option<String>,
}
impl ProfileResolveOptions {
pub fn with_pod_name(name: impl Into<String>) -> Self {
Self {
pod_name: Some(name.into()),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ProfileResolver {
workspace_base: Option<PathBuf>,
}
impl ProfileResolver {
pub fn new() -> Self {
Self::default()
}
pub fn with_workspace_base(mut self, workspace_base: impl Into<PathBuf>) -> Self {
self.workspace_base = Some(workspace_base.into());
self
}
pub fn resolve(
&self,
selector: &ProfileSelector,
options: ProfileResolveOptions,
) -> Result<ResolvedProfile, ProfileError> {
match selector {
ProfileSelector::Path { path } => self.resolve_path(
path,
ProfileSource::Path {
path: absolutize(path)?,
},
options,
),
ProfileSelector::Named { .. } | ProfileSelector::Default => {
let cwd = std::env::current_dir().map_err(|source| ProfileError::CommandIo {
path: PathBuf::from("."),
source,
})?;
let registry = ProfileDiscovery::for_cwd(&cwd).discover()?;
self.resolve_from_registry(selector, &registry, options)
}
}
}
/// Resolve a registry/default selector against an already-discovered
/// registry. Callers such as SpawnPod use this to bind discovery to the
/// Pod's cwd instead of the process current directory.
pub fn resolve_from_registry(
&self,
selector: &ProfileSelector,
registry: &ProfileRegistry,
options: ProfileResolveOptions,
) -> Result<ResolvedProfile, ProfileError> {
match selector {
ProfileSelector::Path { .. } => Err(ProfileError::InvalidProfile(
"path selectors are not registry entries".into(),
)),
ProfileSelector::Named { .. } | ProfileSelector::Default => {
let entry = registry.select(selector)?.clone();
self.resolve_path(
&entry.path,
ProfileSource::Registry {
source: entry.source,
name: entry.name,
path: absolutize(&entry.path)?,
},
options,
)
}
}
}
fn resolve_path(
&self,
path: &Path,
source: ProfileSource,
options: ProfileResolveOptions,
) -> Result<ResolvedProfile, ProfileError> {
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("<none>".to_string(), |s| format!(".{s}"))
),
});
}
}
let profile_dir = absolute_path
.parent()
.map(Path::to_path_buf)
.ok_or_else(|| ProfileError::InvalidPath {
path: absolute_path.clone(),
message: "profile path has no parent directory".into(),
})?;
let profile_dir = canonicalize_existing_dir(&profile_dir)?;
let workspace_base = absolutize(
self.workspace_base
.as_deref()
.unwrap_or_else(|| Path::new(".")),
)?;
let lua_value = evaluate_lua_profile(&absolute_path, &profile_dir)?;
let raw_artifact = lua_value.clone();
resolve_lua_profile_value(
source,
&profile_dir,
&workspace_base,
options,
lua_value,
raw_artifact,
)
}
}
fn resolve_lua_profile_value(
source: ProfileSource,
profile_dir: &Path,
workspace_base: &Path,
options: ProfileResolveOptions,
value: serde_json::Value,
raw_artifact: serde_json::Value,
) -> Result<ResolvedProfile, ProfileError> {
if !workspace_base.is_absolute() {
return Err(ProfileError::InvalidPath {
path: workspace_base.to_path_buf(),
message: "profile workspace base must be absolute".into(),
});
}
reject_manifest_shaped_profile(&value)?;
let profile: ProfileConfig = serde_json::from_value(value.clone())
.map_err(|source| ProfileError::ProfileDeserialize { source })?;
validate_profile_paths(&profile)?;
let pod_name = options
.pod_name
.unwrap_or_else(|| derive_pod_name(&source, profile.slug.as_deref()));
let profile_meta = Some(ProfileMetadata {
name: profile.slug.clone().or_else(|| source_name(&source)),
description: profile.description.clone(),
format: Some(PROFILE_FORMAT_V1.to_string()),
});
let compaction = profile_compaction_to_partial(profile.compaction, &profile.model)?;
let config = PodManifestConfig {
pod: PodMetaConfig {
name: Some(pod_name),
prompt_pack: None,
},
model: profile.model.unwrap_or_default(),
worker: profile.worker.unwrap_or_default(),
scope: profile_scope_to_config(profile.scope, workspace_base),
session: profile.session,
permissions: profile.permissions,
compaction,
web: profile.web,
memory: profile.memory,
skills: profile.skills,
};
let config = PodManifestConfig::builtin_defaults().merge(config.resolve_paths(profile_dir));
let mut manifest = PodManifest::try_from(config).map_err(ProfileError::ManifestResolve)?;
manifest.profile = Some(ProfileManifestSnapshot {
source: source.clone(),
profile: profile_meta.clone(),
});
let manifest_snapshot =
serde_json::to_value(&manifest).map_err(ProfileError::SnapshotSerialize)?;
Ok(ResolvedProfile {
source,
profile: profile_meta,
manifest,
manifest_snapshot,
raw_artifact,
})
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct ProfileConfig {
#[serde(default)]
slug: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
model: Option<ModelManifest>,
#[serde(default)]
worker: Option<WorkerManifestConfig>,
#[serde(default)]
scope: Option<ProfileScopeConfig>,
#[serde(default)]
session: Option<SessionConfigPartial>,
#[serde(default)]
permissions: Option<PermissionConfigPartial>,
#[serde(default)]
compaction: Option<serde_json::Value>,
#[serde(default)]
web: Option<WebConfig>,
#[serde(default)]
memory: Option<MemoryConfig>,
#[serde(default)]
skills: Option<SkillsConfig>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum ProfileScopeConfig {
Intent { intent: ProfileScopeIntent },
String(ProfileScopeIntent),
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ProfileScopeIntent {
WorkspaceRead,
WorkspaceWrite,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RatioCompaction {
#[allow(dead_code)]
kind: String,
#[serde(default)]
threshold: Option<f64>,
#[serde(default, alias = "request")]
request_threshold: Option<f64>,
#[serde(default, alias = "worker")]
worker_context_max_tokens: Option<f64>,
}
#[derive(Debug, Deserialize)]
struct ProfileRegistryDocument {
#[serde(default)]
default: Option<String>,
#[serde(default, alias = "entries")]
profile: BTreeMap<String, ProfileEntryConfig>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ProfileEntryConfig {
Path(String),
Table {
path: PathBuf,
#[serde(default)]
description: Option<String>,
},
}
impl ProfileEntryConfig {
fn into_parts(self) -> (PathBuf, Option<String>) {
match self {
Self::Path(path) => (PathBuf::from(path), None),
Self::Table { path, description } => (path, description),
}
}
}
fn load_profile_registry_file(
registry: &mut ProfileRegistry,
source: ProfileRegistrySource,
path: &Path,
) -> Result<(), ProfileError> {
if !path.exists() {
return Ok(());
}
let content = std::fs::read_to_string(path).map_err(|source| ProfileError::ConfigRead {
path: path.to_path_buf(),
source,
})?;
let config: ProfileRegistryDocument =
toml::from_str(&content).map_err(|source| ProfileError::ConfigParse {
path: path.to_path_buf(),
source,
})?;
let base = path.parent().unwrap_or_else(|| Path::new("."));
for (name, entry_config) in config.profile {
let (entry_path, description) = entry_config.into_parts();
registry.push_entry(ProfileRegistryEntry {
source,
name,
path: join_if_relative(base, &entry_path),
description,
is_default: false,
});
}
if let Some(default) = config.default {
let (default_source, default_name) = parse_profile_ref(&default);
registry.set_default(ProfileDefault {
source: default_source.or(Some(source)),
name: default_name,
});
}
Ok(())
}
fn find_project_profiles_from(start: &Path) -> Option<PathBuf> {
let start = start
.canonicalize()
.ok()
.unwrap_or_else(|| start.to_path_buf());
let mut cur: Option<&Path> = Some(start.as_path());
while let Some(dir) = cur {
let candidate = dir.join(".insomnia").join("profiles.toml");
if candidate.is_file() {
return Some(candidate);
}
cur = dir.parent();
}
None
}
fn discover_profile_dir(
registry: &mut ProfileRegistry,
source: ProfileRegistrySource,
dir: &Path,
) -> Result<(), ProfileError> {
if !dir.is_dir() {
return Ok(());
}
for entry in std::fs::read_dir(dir).map_err(|source| ProfileError::ConfigRead {
path: dir.to_path_buf(),
source,
})? {
let entry = entry.map_err(|source| ProfileError::ConfigRead {
path: dir.to_path_buf(),
source,
})?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("lua") {
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
registry.push_entry(ProfileRegistryEntry {
source,
name: name.to_string(),
path,
description: None,
is_default: false,
});
}
} else if path.is_dir() {
let profile = path.join("profile.lua");
if profile.is_file()
&& let Some(name) = path.file_name().and_then(|s| s.to_str())
{
registry.push_entry(ProfileRegistryEntry {
source,
name: name.to_string(),
path: profile,
description: None,
is_default: false,
});
}
}
}
Ok(())
}
fn parse_profile_ref(raw: &str) -> (Option<ProfileRegistrySource>, String) {
if let Some((prefix, name)) = raw.split_once(':')
&& let Some(source) = ProfileRegistrySource::parse(prefix)
{
return (Some(source), name.to_string());
}
(None, raw.to_string())
}
fn evaluate_lua_profile(
path: &Path,
module_root: &Path,
) -> Result<serde_json::Value, ProfileError> {
let content = std::fs::read_to_string(path).map_err(|source| ProfileError::ConfigRead {
path: path.to_path_buf(),
source,
})?;
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.to_path_buf())?;
let value: LuaValue = lua
.load(&content)
.set_name(path.display().to_string())
.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: PathBuf) -> 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)?;
globals
.set(
"profile",
lua.create_function(|_, table: Table| Ok(table))
.map_err(ProfileError::Lua)?,
)
.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: PathBuf,
cache: HashMap<String, RegistryKey>,
loading: HashSet<String>,
}
fn require_module(
lua: &Lua,
loader: &Rc<RefCell<LocalModuleLoader>>,
name: &str,
) -> mlua::Result<LuaValue> {
if let Some(value) = host_module(lua, name)? {
return Ok(value);
}
if name.starts_with("insomnia.") || name == "insomnia" {
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}`"
)));
}
}
let path = local_module_path(&loader.borrow().root, name).map_err(mlua::Error::RuntimeError)?;
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<LuaValue> = 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<Option<LuaValue>> {
match name {
"insomnia" => {
let t = lua.create_table()?;
t.set("profile", profile_function(lua)?)?;
t.set("models", models_module(lua)?)?;
t.set("compact", compact_module(lua)?)?;
t.set("scope", scope_module(lua)?)?;
Ok(Some(LuaValue::Table(t)))
}
"insomnia.profile" => Ok(Some(LuaValue::Function(profile_function(lua)?))),
"insomnia.models" => Ok(Some(LuaValue::Table(models_module(lua)?))),
"insomnia.compact" => Ok(Some(LuaValue::Table(compact_module(lua)?))),
"insomnia.scope" => Ok(Some(LuaValue::Table(scope_module(lua)?))),
_ => Ok(None),
}
}
fn profile_function(lua: &Lua) -> mlua::Result<mlua::Function> {
lua.create_function(|_, table: Table| Ok(table))
}
fn models_module(lua: &Lua) -> mlua::Result<Table> {
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<Table> {
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<Table> {
let t = lua.create_table()?;
t.set(
"workspace_write",
lua.create_function(|lua, ()| {
let v = lua.create_table()?;
v.set("intent", "workspace_write")?;
Ok(v)
})?,
)?;
t.set(
"workspace_read",
lua.create_function(|lua, ()| {
let v = lua.create_table()?;
v.set("intent", "workspace_read")?;
Ok(v)
})?,
)?;
Ok(t)
}
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<PathBuf, String> {
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)
}
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(),
));
};
for key in ["manifest", "config"] {
if map.contains_key(key) {
return Err(ProfileError::InvalidProfile(format!(
"field `{key}` is a complete Manifest artifact boundary and is not allowed in reusable Profiles; use --manifest for complete Manifests"
)));
}
}
if map.contains_key("pod") {
return Err(ProfileError::InvalidProfile("field `pod` is runtime-bound and is not allowed in reusable Profiles; pass the Pod name via CLI/TUI runtime inputs".into()));
}
if let Some(scope) = map.get("scope").and_then(|v| v.as_object()) {
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(\"insomnia.scope\") intent helpers"
)));
}
}
}
Ok(())
}
fn validate_profile_paths(profile: &ProfileConfig) -> Result<(), ProfileError> {
if let Some(model) = &profile.model {
reject_absolute_auth_file(&model.auth, "model.auth.file")?;
}
if let Some(compaction) = &profile.compaction
&& let Some(model) = compaction.get("model")
{
let model: ModelManifest = serde_json::from_value(model.clone())
.map_err(|source| ProfileError::ProfileDeserialize { source })?;
reject_absolute_auth_file(&model.auth, "compaction.model.auth.file")?;
}
if let Some(memory) = &profile.memory
&& let Some(root) = &memory.workspace_root
&& root.is_absolute()
{
return Err(ProfileError::InvalidProfile("field `memory.workspace_root` is a resolved path and is not allowed in reusable Profiles".into()));
}
if let Some(skills) = &profile.skills {
for dir in &skills.directories {
if dir.is_absolute() {
return Err(ProfileError::InvalidProfile(
"field `skills.directories` must be profile-relative in reusable Profiles"
.into(),
));
}
}
}
Ok(())
}
fn reject_absolute_auth_file(
auth: &Option<AuthRef>,
field: &'static str,
) -> Result<(), ProfileError> {
if let Some(AuthRef::ApiKey {
file: Some(file), ..
}) = auth
&& file.is_absolute()
{
return Err(ProfileError::InvalidProfile(format!(
"field `{field}` is a resolved path and is not allowed in reusable Profiles"
)));
}
Ok(())
}
fn profile_scope_to_config(
scope: Option<ProfileScopeConfig>,
workspace_base: &Path,
) -> ScopeConfig {
let intent = match scope {
Some(ProfileScopeConfig::Intent { intent }) | Some(ProfileScopeConfig::String(intent)) => {
intent
}
None => ProfileScopeIntent::WorkspaceWrite,
};
let permission = match intent {
ProfileScopeIntent::WorkspaceRead => Permission::Read,
ProfileScopeIntent::WorkspaceWrite => Permission::Write,
};
ScopeConfig {
allow: vec![ScopeRule {
target: workspace_base.to_path_buf(),
permission,
recursive: true,
}],
deny: Vec::new(),
}
}
fn profile_compaction_to_partial(
value: Option<serde_json::Value>,
model: &Option<ModelManifest>,
) -> Result<Option<CompactionConfigPartial>, ProfileError> {
let Some(value) = value else {
return Ok(None);
};
let Some(kind) = value.get("kind").and_then(|v| v.as_str()) else {
return serde_json::from_value(value)
.map(Some)
.map_err(|source| ProfileError::ProfileDeserialize { source });
};
match kind {
"tokens" => {
let mut obj = value.as_object().cloned().unwrap_or_default();
obj.remove("kind");
serde_json::from_value(serde_json::Value::Object(obj))
.map(Some)
.map_err(|source| ProfileError::ProfileDeserialize { source })
}
"ratio" => {
let ratio: RatioCompaction = serde_json::from_value(value)
.map_err(|source| ProfileError::ProfileDeserialize { source })?;
let context = model_context_window(model.as_ref()).ok_or_else(|| ProfileError::InvalidProfile("compact.ratio requires model.context_window/max_context_window or a known model ref; use compact.tokens for explicit token values".into()))?;
Ok(Some(CompactionConfigPartial {
threshold: ratio.threshold.map(|r| ratio_tokens(context, r)),
request_threshold: ratio.request_threshold.map(|r| ratio_tokens(context, r)),
worker_context_max_tokens: ratio
.worker_context_max_tokens
.map(|r| ratio_tokens(context, r)),
..Default::default()
}))
}
other => Err(ProfileError::InvalidProfile(format!(
"unknown compaction helper kind `{other}`"
))),
}
}
fn ratio_tokens(context: u64, ratio: f64) -> u64 {
((context as f64) * ratio).floor() as u64
}
fn model_context_window(model: Option<&ModelManifest>) -> Option<u64> {
let model = model?;
if let Some(max) = model.max_context_window {
return Some(model.context_window.map_or(max, |ctx| ctx.min(max)));
}
if let Some(ctx) = model.context_window {
return Some(ctx);
}
builtin_model_context_window(model.ref_.as_deref()?)
}
fn builtin_model_context_window(reference: &str) -> Option<u64> {
let (provider, model_id) = reference.split_once('/')?;
let path = paths::resource_dir()?.join("models").join("builtin.toml");
let content = std::fs::read_to_string(path).ok()?;
let parsed: toml::Value = toml::from_str(&content).ok()?;
for entry in parsed.get("model")?.as_array()? {
let table = entry.as_table()?;
if table.get("provider")?.as_str()? == provider && table.get("id")?.as_str()? == model_id {
let context = table.get("context_window")?.as_integer()? as u64;
let max = table
.get("max_context_window")
.and_then(|v| v.as_integer())
.map(|v| v as u64);
return Some(max.map_or(context, |max| context.min(max)));
}
}
None
}
fn derive_pod_name(source: &ProfileSource, slug: Option<&str>) -> String {
if matches!(source, ProfileSource::Registry { source: ProfileRegistrySource::Builtin, name, .. } if name == BUILTIN_DEFAULT_PROFILE_NAME)
|| slug == Some(BUILTIN_DEFAULT_PROFILE_NAME)
{
return DEFAULT_POD_NAME.to_string();
}
let raw = slug
.map(str::to_string)
.or_else(|| source_name(source))
.unwrap_or_else(|| DEFAULT_POD_NAME.to_string());
sanitise_pod_name(&raw)
}
fn source_name(source: &ProfileSource) -> Option<String> {
match source {
ProfileSource::Path { path } => path
.file_stem()
.and_then(|s| s.to_str())
.map(str::to_string),
ProfileSource::Registry { name, .. } => Some(name.clone()),
}
}
fn sanitise_pod_name(raw: &str) -> String {
let name: String = raw
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') {
c
} else {
'-'
}
})
.collect();
if name.is_empty() {
DEFAULT_POD_NAME.to_string()
} else {
name
}
}
fn canonicalize_existing_dir(path: &Path) -> Result<PathBuf, ProfileError> {
path.canonicalize()
.map_err(|source| ProfileError::CommandIo {
path: path.to_path_buf(),
source,
})
}
fn absolutize(path: &Path) -> Result<PathBuf, ProfileError> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(std::env::current_dir()
.map_err(|source| ProfileError::CommandIo {
path: PathBuf::from("."),
source,
})?
.join(path))
}
}
fn join_if_relative(base: &Path, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
base.join(path)
}
}
pub fn resolve_profile_artifact(
source: ProfileSource,
base_dir: &Path,
raw_artifact: serde_json::Value,
) -> Result<ResolvedProfile, ProfileError> {
resolve_lua_profile_value(
source,
base_dir,
base_dir,
ProfileResolveOptions::default(),
raw_artifact.clone(),
raw_artifact,
)
}
#[derive(Debug, thiserror::Error)]
pub enum ProfileError {
#[error("invalid profile path {}: {message}", .path.display())]
InvalidPath { path: PathBuf, message: String },
#[error("unsupported profile type {}: {message}", .path.display())]
UnsupportedProfileType { path: PathBuf, message: String },
#[error("failed to access profile path {}: {source}", .path.display())]
CommandIo {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to read profile registry config {}: {source}", .path.display())]
ConfigRead {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to parse profile registry config {}: {source}", .path.display())]
ConfigParse {
path: PathBuf,
#[source]
source: toml::de::Error,
},
#[error("no default profile is configured")]
NoDefaultProfile,
#[error("profile not found: {selector}")]
ProfileNotFound { selector: String },
#[error("ambiguous profile name `{name}`; use a source-qualified selector such as {matches:?}")]
AmbiguousProfileName { name: String, matches: Vec<String> },
#[error("failed to evaluate Lua profile: {0}")]
Lua(#[source] mlua::Error),
#[error("invalid Lua profile: {0}")]
InvalidProfile(String),
#[error("failed to decode Profile: {source}")]
ProfileDeserialize {
#[source]
source: serde_json::Error,
},
#[error("failed to resolve Profile into Manifest: {0}")]
ManifestResolve(#[source] ResolveError),
#[error("failed to serialize resolved manifest snapshot: {0}")]
SnapshotSerialize(#[source] serde_json::Error),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ReasoningControl, ReasoningEffort, SchemeKind};
use std::sync::{Mutex, MutexGuard, OnceLock};
use tempfile::TempDir;
fn env_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
struct EnvGuard {
vars: Vec<(&'static str, Option<String>)>,
_lock: MutexGuard<'static, ()>,
}
impl EnvGuard {
fn new(overrides: &[(&'static str, Option<&str>)]) -> Self {
let lock = env_lock();
let names = [
"INSOMNIA_CONFIG_DIR",
"INSOMNIA_RESOURCE_DIR",
"INSOMNIA_HOME",
"XDG_CONFIG_HOME",
"HOME",
];
let saved: Vec<_> = names.iter().map(|n| (*n, std::env::var(n).ok())).collect();
unsafe {
for (n, _) in &saved {
std::env::remove_var(n);
}
for (n, v) in overrides {
if let Some(v) = v {
std::env::set_var(n, v);
}
}
}
Self {
vars: saved,
_lock: lock,
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
for (n, v) in &self.vars {
match v {
Some(v) => std::env::set_var(n, v),
None => std::env::remove_var(n),
}
}
}
}
}
fn write_profile(dir: &Path, name: &str, body: &str) -> PathBuf {
let path = dir.join(name);
std::fs::write(&path, body).unwrap();
path
}
#[test]
fn parse_cli_preserves_paths_and_source_qualified_names() {
assert!(matches!(
ProfileSelector::parse_cli("./coder.lua"),
ProfileSelector::Path { .. }
));
assert_eq!(
ProfileSelector::parse_cli("project:coder"),
ProfileSelector::source_named(ProfileRegistrySource::Project, "coder")
);
assert_eq!(
ProfileSelector::parse_cli("coder"),
ProfileSelector::named("coder")
);
assert_eq!(
ProfileSelector::parse_cli("default"),
ProfileSelector::Default
);
}
#[test]
fn builtin_default_profile_is_registered_as_default() {
let registry = ProfileDiscovery::with_sources(paths::builtin_profiles_dir(), None, None)
.discover()
.unwrap();
let default = registry.default_entry().unwrap();
assert_eq!(default.source, ProfileRegistrySource::Builtin);
assert_eq!(default.name, BUILTIN_DEFAULT_PROFILE_NAME);
assert!(default.is_default);
assert!(default.path.ends_with("resources/profiles/default.lua"));
}
#[test]
fn resolves_plain_lua_profile_with_runtime_pod_name_and_scope_intent() {
let tmp = TempDir::new().unwrap();
let profile = write_profile(
tmp.path(),
"coder.lua",
r#"
local profile = require("insomnia.profile")
local scope = require("insomnia.scope")
return profile {
slug = "coder",
model = { scheme = "anthropic", model_id = "claude-sonnet-4-20250514" },
worker = { reasoning = "high" },
scope = scope.workspace_read(),
}
"#,
);
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace).unwrap();
let resolved = ProfileResolver::new()
.with_workspace_base(&workspace)
.resolve(
&ProfileSelector::path(&profile),
ProfileResolveOptions::with_pod_name("runtime-pod"),
)
.unwrap();
assert_eq!(resolved.manifest.pod.name, "runtime-pod");
assert_eq!(resolved.manifest.model.scheme, Some(SchemeKind::Anthropic));
assert_eq!(
resolved.manifest.worker.reasoning,
Some(ReasoningControl::Effort(ReasoningEffort::High))
);
assert_eq!(resolved.manifest.scope.allow[0].target, workspace);
assert_eq!(
resolved.manifest.scope.allow[0].permission,
Permission::Read
);
assert_eq!(
resolved.profile.as_ref().unwrap().name.as_deref(),
Some("coder")
);
}
#[test]
fn host_modules_and_local_require_work() {
let tmp = TempDir::new().unwrap();
std::fs::write(
tmp.path().join("shared.lua"),
r#"return { model = require("insomnia.models").catalog("codex-oauth/gpt-5.5") }"#,
)
.unwrap();
let profile = write_profile(
tmp.path(),
"main.lua",
r#"
local insomnia = require("insomnia")
local shared = require("shared")
return insomnia.profile {
slug = "main",
model = shared.model,
scope = insomnia.scope.workspace_write(),
}
"#,
);
let resolved = ProfileResolver::new()
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::path(profile),
ProfileResolveOptions::with_pod_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
);
}
#[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_pod_name("p"),
)
.unwrap_err();
assert!(matches!(
err,
ProfileError::Lua(_) | ProfileError::InvalidProfile(_)
));
}
}
#[test]
fn rejects_manifest_shaped_runtime_and_authority_fields() {
for (value, needle) in [
(serde_json::json!({"manifest": {}}), "manifest"),
(serde_json::json!({"config": {}}), "config"),
(serde_json::json!({"pod": {"name": "bad"}}), "pod"),
(
serde_json::json!({"model": {"ref": "codex-oauth/gpt-5.5"}, "scope": {"allow": []}}),
"scope.allow",
),
(
serde_json::json!({"model": {"ref": "codex-oauth/gpt-5.5"}, "scope": {"deny": []}}),
"scope.deny",
),
] {
let err = resolve_profile_artifact(
ProfileSource::Path {
path: PathBuf::from("/profiles/bad.lua"),
},
Path::new("/workspace"),
value,
)
.unwrap_err();
assert!(err.to_string().contains(needle), "unexpected error: {err}");
}
}
#[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();
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("insomnia.profile")
local models = require("insomnia.models")
local compact = require("insomnia.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_pod_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()
.with_workspace_base(tmp.path())
.resolve(&ProfileSelector::Default, ProfileResolveOptions::default())
.unwrap();
assert_eq!(resolved.manifest.pod.name, "insomnia");
assert_eq!(
resolved.manifest.model.ref_.as_deref(),
Some("codex-oauth/gpt-5.5")
);
assert_eq!(resolved.manifest.scope.allow[0].target, tmp.path());
assert_eq!(
resolved.manifest.scope.allow[0].permission,
Permission::Write
);
assert!(resolved.manifest.session.record_event_trace);
assert_eq!(
resolved.profile.as_ref().unwrap().name.as_deref(),
Some("default")
);
}
#[test]
fn unsupported_profile_extension_has_clear_diagnostic() {
let tmp = TempDir::new().unwrap();
let path = write_profile(tmp.path(), "legacy.txt", "{}");
let err = ProfileResolver::new()
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::path(path),
ProfileResolveOptions::default(),
)
.unwrap_err();
assert!(matches!(err, ProfileError::UnsupportedProfileType { .. }));
assert!(err.to_string().contains("Lua profiles must end in .lua"));
}
#[test]
fn discovery_reads_user_and_project_registry_and_project_default_wins() {
let tmp = TempDir::new().unwrap();
let user_config = tmp.path().join("profiles.toml");
let project_dir = tmp.path().join("project/.insomnia");
std::fs::create_dir_all(&project_dir).unwrap();
let project_config = project_dir.join("profiles.toml");
std::fs::write(
&user_config,
"default = \"coder\"\n[profile]\ncoder = \"profiles/user-coder.lua\"\n",
)
.unwrap();
std::fs::write(&project_config, "default = \"project:coder\"\n[profile.coder]\npath = \"profiles/project-coder.lua\"\ndescription = \"Project coder\"\n").unwrap();
let registry =
ProfileDiscovery::with_sources(None, Some(user_config), Some(project_config))
.discover()
.unwrap();
let default = registry.default_entry().unwrap();
assert_eq!(default.source, ProfileRegistrySource::Project);
assert_eq!(default.name, "coder");
assert!(default.path.ends_with("profiles/project-coder.lua"));
}
#[test]
fn default_marks_direct_profile_entry() {
let tmp = TempDir::new().unwrap();
let project_dir = tmp.path().join("project/.insomnia");
std::fs::create_dir_all(&project_dir).unwrap();
let project_config = project_dir.join("profiles.toml");
std::fs::write(
&project_config,
"default = \"coder\"\n[profile]\ncoder = \"profiles/coder.lua\"\n",
)
.unwrap();
let registry = ProfileDiscovery::with_sources(None, None, Some(project_config))
.discover()
.unwrap();
let default = registry.default_entry().unwrap();
assert_eq!(default.source, ProfileRegistrySource::Project);
assert_eq!(default.name, "coder");
assert!(default.is_default);
assert_eq!(
registry
.entries()
.iter()
.filter(|entry| entry.is_default)
.count(),
1
);
}
#[test]
fn unqualified_ambiguous_names_fail_closed() {
let mut registry = ProfileRegistry::default();
registry.push_entry(ProfileRegistryEntry {
source: ProfileRegistrySource::User,
name: "coder".to_string(),
path: PathBuf::from("/user/coder.lua"),
description: None,
is_default: false,
});
registry.push_entry(ProfileRegistryEntry {
source: ProfileRegistrySource::Project,
name: "coder".to_string(),
path: PathBuf::from("/project/coder.lua"),
description: None,
is_default: false,
});
let err = registry
.select(&ProfileSelector::named("coder"))
.unwrap_err();
assert!(matches!(err, ProfileError::AmbiguousProfileName { .. }));
let selected = registry
.select(&ProfileSelector::source_named(
ProfileRegistrySource::Project,
"coder",
))
.unwrap();
assert_eq!(selected.path, PathBuf::from("/project/coder.lua"));
}
}