feat: unify builtin Profile resolution authority

This commit is contained in:
2026-08-30 00:28:58 +09:00
parent 402ae0d466
commit 16c0fc704d
11 changed files with 698 additions and 369 deletions
Generated
+1
View File
@@ -2629,6 +2629,7 @@ version = "0.1.0"
dependencies = [
"agen",
"arc-swap",
"decodal",
"protocol",
"secrets",
"serde",
+1
View File
@@ -7,6 +7,7 @@ license.workspace = true
[dependencies]
arc-swap = "1"
agen = { workspace = true }
decodal.workspace = true
protocol = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
+318
View File
@@ -0,0 +1,318 @@
use std::collections::BTreeMap;
use decodal::{Data, Engine, ImportLoader, LoadedImport};
use serde_json::{Map, Number, Value};
use sha2::{Digest, Sha256};
use crate::profile::ProfileError;
pub const BUILTIN_PROFILE_CATALOG_ID: &str = "builtin-profiles-v2";
pub const BUILTIN_DEFAULT_PROFILE: &str = "builtin:default";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuiltinProfileImport {
pub specifier: &'static str,
pub resolved_path: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuiltinProfileResource {
pub selector: Option<&'static str>,
pub path: &'static str,
pub source: &'static str,
pub description: &'static str,
pub imports: &'static [BuiltinProfileImport],
}
const BASE_PATH: &str = "profiles/base.dcdl";
const BASE_IMPORT: &[BuiltinProfileImport] = &[BuiltinProfileImport {
specifier: "./base.dcdl",
resolved_path: BASE_PATH,
}];
const NO_IMPORTS: &[BuiltinProfileImport] = &[];
pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[
BuiltinProfileResource {
selector: None,
path: BASE_PATH,
source: include_str!("../../../resources/profiles/base.dcdl"),
description: "Shared built-in Profile defaults.",
imports: NO_IMPORTS,
},
BuiltinProfileResource {
selector: Some(BUILTIN_DEFAULT_PROFILE),
path: "profiles/default.dcdl",
source: include_str!("../../../resources/profiles/default.dcdl"),
description: "Standalone Yoi coding profile.",
imports: BASE_IMPORT,
},
BuiltinProfileResource {
selector: Some("builtin:coder"),
path: "profiles/coder.dcdl",
source: include_str!("../../../resources/profiles/coder.dcdl"),
description: "Ticket implementation with direct Reviewer SubWorkers.",
imports: BASE_IMPORT,
},
BuiltinProfileResource {
selector: Some("builtin:companion"),
path: "profiles/companion.dcdl",
source: include_str!("../../../resources/profiles/companion.dcdl"),
description: "General assistance with Workspace tools.",
imports: BASE_IMPORT,
},
BuiltinProfileResource {
selector: Some("builtin:intake"),
path: "profiles/intake.dcdl",
source: include_str!("../../../resources/profiles/intake.dcdl"),
description: "Read-only intake and planning.",
imports: BASE_IMPORT,
},
BuiltinProfileResource {
selector: Some("builtin:reviewer"),
path: "profiles/reviewer.dcdl",
source: include_str!("../../../resources/profiles/reviewer.dcdl"),
description: "Independent review of a published Merge Request source.",
imports: BASE_IMPORT,
},
BuiltinProfileResource {
selector: Some("builtin:orchestrator"),
path: "profiles/orchestrator.dcdl",
source: include_str!("../../../resources/profiles/orchestrator.dcdl"),
description: "Workspace orchestration and Worker control.",
imports: BASE_IMPORT,
},
BuiltinProfileResource {
selector: Some("builtin:memory-consolidation"),
path: "profiles/memory-consolidation.dcdl",
source: include_str!("../../../resources/profiles/memory-consolidation.dcdl"),
description: "Internal Memory consolidation service.",
imports: BASE_IMPORT,
},
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuiltinProfileCatalogSnapshot {
pub id: &'static str,
pub sources: BTreeMap<String, String>,
pub entrypoints: BTreeMap<String, String>,
pub imports: BTreeMap<String, String>,
}
impl BuiltinProfileCatalogSnapshot {
pub fn digest(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(self.id.as_bytes());
for (path, source) in &self.sources {
hasher.update((path.len() as u64).to_le_bytes());
hasher.update(path.as_bytes());
hasher.update((source.len() as u64).to_le_bytes());
hasher.update(source.as_bytes());
}
for (selector, path) in &self.entrypoints {
hasher.update((selector.len() as u64).to_le_bytes());
hasher.update(selector.as_bytes());
hasher.update((path.len() as u64).to_le_bytes());
hasher.update(path.as_bytes());
}
for (request, resolved_path) in &self.imports {
hasher.update((request.len() as u64).to_le_bytes());
hasher.update(request.as_bytes());
hasher.update((resolved_path.len() as u64).to_le_bytes());
hasher.update(resolved_path.as_bytes());
}
format!("sha256:{:x}", hasher.finalize())
}
}
pub fn builtin_profile_catalog_snapshot() -> BuiltinProfileCatalogSnapshot {
let mut sources = BTreeMap::new();
let mut entrypoints = BTreeMap::new();
let mut imports = BTreeMap::new();
for resource in BUILTIN_PROFILE_RESOURCES {
sources.insert(resource.path.to_owned(), resource.source.to_owned());
for import in resource.imports {
imports.insert(
format!("{}\0{}", resource.path, import.specifier),
import.resolved_path.to_owned(),
);
}
if let Some(selector) = resource.selector {
entrypoints.insert(selector.to_owned(), resource.path.to_owned());
}
}
BuiltinProfileCatalogSnapshot {
id: BUILTIN_PROFILE_CATALOG_ID,
sources,
entrypoints,
imports,
}
}
pub fn builtin_profile_entrypoints() -> impl Iterator<Item = &'static BuiltinProfileResource> {
BUILTIN_PROFILE_RESOURCES
.iter()
.filter(|resource| resource.selector.is_some())
}
pub(crate) fn resolve_builtin_profile_artifact(
selector: &str,
) -> Result<Option<Value>, ProfileError> {
let catalog = builtin_profile_catalog_snapshot();
let Some(entrypoint) = catalog.entrypoints.get(selector) else {
return Ok(None);
};
let source = catalog
.sources
.get(entrypoint)
.expect("built-in Profile entrypoint must name a source")
.clone();
let mut engine = Engine::new(BuiltinProfileImportLoader {
sources: catalog.sources,
});
let module = engine
.add_root_source(entrypoint, entrypoint, &source)
.map_err(|error| ProfileError::BuiltinProfileEvaluation {
selector: selector.to_owned(),
message: format!("{error:?}"),
})?;
let value =
engine
.eval_module(module)
.map_err(|error| ProfileError::BuiltinProfileEvaluation {
selector: selector.to_owned(),
message: format!("{error:?}"),
})?;
let data =
engine
.materialize(&value)
.map_err(|error| ProfileError::BuiltinProfileEvaluation {
selector: selector.to_owned(),
message: format!("{error:?}"),
})?;
Ok(Some(data_to_json(&data)))
}
#[derive(Debug)]
struct BuiltinProfileImportLoader {
sources: BTreeMap<String, String>,
}
impl ImportLoader for BuiltinProfileImportLoader {
fn load(
&mut self,
current_key: Option<&str>,
specifier: &str,
) -> decodal::Result<LoadedImport> {
let current_key = current_key.ok_or_else(|| {
decodal::Diagnostic::new(
decodal::DiagnosticKind::Import,
decodal::Span::default(),
format!("built-in Profile import `{specifier}` has no source context"),
)
})?;
let resolved = resolve_import_path(current_key, specifier).ok_or_else(|| {
decodal::Diagnostic::new(
decodal::DiagnosticKind::Import,
decodal::Span::default(),
format!("built-in Profile import `{specifier}` from `{current_key}` is invalid"),
)
})?;
let source = self.sources.get(&resolved).ok_or_else(|| {
decodal::Diagnostic::new(
decodal::DiagnosticKind::Import,
decodal::Span::default(),
format!("built-in Profile import `{specifier}` from `{current_key}` was not found"),
)
})?;
Ok(LoadedImport::source(
resolved.clone(),
resolved,
source.clone(),
))
}
}
fn resolve_import_path(current_key: &str, specifier: &str) -> Option<String> {
let current_parent = current_key
.rsplit_once('/')
.map_or("", |(parent, _)| parent);
let joined = if let Some(relative) = specifier.strip_prefix("./") {
format!("{current_parent}/{relative}")
} else {
return None;
};
if joined
.split('/')
.any(|segment| segment.is_empty() || segment == "." || segment == "..")
{
return None;
}
Some(joined)
}
fn data_to_json(data: &Data) -> Value {
match data {
Data::Bool(value) => Value::Bool(*value),
Data::Int(value) => Value::Number(Number::from(*value)),
Data::Float(value) => Number::from_f64(*value)
.map(Value::Number)
.unwrap_or(Value::Null),
Data::String(value) => Value::String(value.clone()),
Data::Array(values) => Value::Array(values.iter().map(data_to_json).collect()),
Data::Object(fields) => Value::Object(
fields
.iter()
.map(|field| (field.name.clone(), data_to_json(&field.value)))
.collect::<Map<_, _>>(),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalog_has_one_explicit_entrypoint_for_each_builtin_profile() {
let catalog = builtin_profile_catalog_snapshot();
assert_eq!(catalog.sources.len(), BUILTIN_PROFILE_RESOURCES.len());
assert_eq!(catalog.entrypoints.len() + 1, catalog.sources.len());
assert_eq!(
catalog.entrypoints.get(BUILTIN_DEFAULT_PROFILE),
Some(&"profiles/default.dcdl".to_owned())
);
assert!(catalog.digest().starts_with("sha256:"));
}
#[test]
fn default_profile_evaluates_from_the_shared_resource_graph() {
let value = resolve_builtin_profile_artifact(BUILTIN_DEFAULT_PROFILE)
.expect("evaluate built-in default")
.expect("default exists");
assert_eq!(value["slug"], "default");
assert_eq!(value["feature"]["task"]["enabled"], true);
assert_eq!(value["feature"]["sub_worker"]["enabled"], true);
assert_eq!(value["feature"]["memory"]["enabled"], false);
assert_eq!(value["feature"]["ticket"]["enabled"], false);
assert_eq!(value["feature"]["worker"]["enabled"], false);
assert_eq!(value["feature"]["manage_workdir"]["enabled"], false);
}
#[test]
fn imports_cannot_escape_the_builtin_resource_catalog() {
assert_eq!(
resolve_import_path("profiles/default.dcdl", "./base.dcdl").as_deref(),
Some("profiles/base.dcdl")
);
assert_eq!(
resolve_import_path("profiles/default.dcdl", "../outside.dcdl"),
None
);
assert_eq!(
resolve_import_path("profiles/default.dcdl", "/outside.dcdl"),
None
);
}
}
+10 -9
View File
@@ -566,15 +566,16 @@ impl WorkerManifestConfig {
})
}
/// Base config populated with the in-code defaults listed in
/// [`crate::defaults`]. Profile and one-file Manifest resolvers start
/// from this layer so every per-field default lives at exactly one
/// call site (the `defaults` module).
/// Base config populated with the in-code per-field defaults listed in
/// [`crate::defaults`]. This is not a selectable Profile and does not
/// enable a launch capability surface. Profile and one-file Manifest
/// resolvers start from this layer so every per-field default lives at
/// exactly one call site (the `defaults` module).
///
/// `TryFrom<WorkerManifestConfig>` also reads the same constants as a
/// belt-and-suspenders fallback, so a manually-constructed config
/// that skips this layer still resolves to the same values.
pub fn builtin_defaults() -> Self {
pub fn resolution_defaults() -> Self {
Self {
engine: EngineManifestConfig {
tool_output: ToolOutputLimitsPartial {
@@ -1973,7 +1974,7 @@ enabled = false
"#,
)
.unwrap();
let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults()
let manifest: WorkerManifest = WorkerManifestConfig::resolution_defaults()
.merge(cfg)
.merge(WorkerManifestConfig {
worker: WorkerMetaConfig {
@@ -2074,7 +2075,7 @@ enabled = true
"#,
)
.unwrap();
let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults()
let manifest: WorkerManifest = WorkerManifestConfig::resolution_defaults()
.merge(base)
.merge(upper)
.merge(WorkerManifestConfig {
@@ -2137,7 +2138,7 @@ permission = "write"
#[test]
fn builtin_defaults_populates_worker_limit_defaults() {
let cfg = WorkerManifestConfig::builtin_defaults();
let cfg = WorkerManifestConfig::resolution_defaults();
assert_eq!(
cfg.engine.tool_output.default_max_bytes,
Some(defaults::TOOL_OUTPUT_MAX_BYTES)
@@ -2172,7 +2173,7 @@ permission = "write"
},
..Default::default()
};
let merged = WorkerManifestConfig::builtin_defaults().merge(overlay);
let merged = WorkerManifestConfig::resolution_defaults().merge(overlay);
let manifest: WorkerManifest = merged.try_into().unwrap();
assert_eq!(
manifest.engine.tool_output.default_max_bytes,
+11 -4
View File
@@ -1,3 +1,4 @@
mod builtin_profile;
mod config;
pub mod defaults;
mod model;
@@ -7,6 +8,11 @@ pub mod plugin;
mod profile;
mod scope;
pub use builtin_profile::{
BUILTIN_DEFAULT_PROFILE, BUILTIN_PROFILE_CATALOG_ID, BUILTIN_PROFILE_RESOURCES,
BuiltinProfileCatalogSnapshot, BuiltinProfileImport, BuiltinProfileResource,
builtin_profile_catalog_snapshot, builtin_profile_entrypoints,
};
pub use config::{
CompactionConfigPartial, EngineManifestConfig, FileUploadLimitsPartial,
PermissionConfigPartial, ResolveError, SessionConfigPartial, ToolOutputLimitsPartial,
@@ -17,10 +23,11 @@ pub use model::{
};
pub use paths::user_profiles_path;
pub use profile::{
ProfileDiscovery, ProfileError, ProfileManifestSnapshot, ProfileMetadata, ProfileRegistry,
ProfileRegistryEntry, ProfileRegistrySource, ProfileResolveOptions, ProfileResolver,
ProfileSelector, ProfileSource, ResolvedProfile, resolve_profile_artifact,
resolve_profile_artifact_value,
ProfileDiscovery, ProfileError, ProfileExecutionTarget, ProfileManifestSnapshot,
ProfileMetadata, ProfileRegistry, ProfileRegistryEntry, ProfileRegistrySource,
ProfileResolveOptions, ProfileResolver, ProfileSelector, ProfileSource, ResolvedProfile,
WorkspaceAuthorityRequirement, resolve_profile_artifact, resolve_profile_artifact_value,
validate_profile_execution_target,
};
pub use protocol::{Permission, ScopeRule};
pub use scope::{DelegationScope, Scope, ScopeError, SharedScope};
+254 -256
View File
@@ -6,9 +6,14 @@
//! from launch context.
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::path::{Path, PathBuf};
use crate::builtin_profile::{
BUILTIN_DEFAULT_PROFILE, builtin_profile_catalog_snapshot, builtin_profile_entrypoints,
resolve_builtin_profile_artifact,
};
use crate::config::{
CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial,
};
@@ -23,45 +28,6 @@ use crate::{
const PROFILE_FORMAT_V1: &str = "yoi.profile.v1";
const BUILTIN_MODEL_CATALOG: &str = include_str!("../../../resources/models/builtin.toml");
struct BuiltinProfile {
name: &'static str,
label: &'static str,
description: &'static str,
}
const BUILTIN_PROFILES: &[BuiltinProfile] = &[
BuiltinProfile {
name: "companion",
label: "builtin:companion",
description: "Bundled Companion role profile",
},
BuiltinProfile {
name: "intake",
label: "builtin:intake",
description: "Bundled Intake role profile",
},
BuiltinProfile {
name: "orchestrator",
label: "builtin:orchestrator",
description: "Bundled Orchestrator role profile",
},
BuiltinProfile {
name: "coder",
label: "builtin:coder",
description: "Bundled Coder role profile",
},
BuiltinProfile {
name: "reviewer",
label: "builtin:reviewer",
description: "Bundled Reviewer role profile",
},
BuiltinProfile {
name: "memory-consolidation",
label: "builtin:memory-consolidation",
description: "Bundled Memory staging consolidation profile",
},
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProfileRegistrySource {
@@ -159,6 +125,108 @@ impl ProfileSelector {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProfileExecutionTarget {
Workspace,
Standalone,
}
impl fmt::Display for ProfileExecutionTarget {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Workspace => formatter.write_str("workspace"),
Self::Standalone => formatter.write_str("standalone"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum WorkspaceAuthorityRequirement {
Flow,
ManageWorkdir,
Memory,
MergeRequest,
Objective,
Orchestration,
Plugins,
Ticket,
Worker,
}
impl fmt::Display for WorkspaceAuthorityRequirement {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Flow => formatter.write_str("feature.flow"),
Self::ManageWorkdir => formatter.write_str("feature.manage_workdir"),
Self::Memory => formatter.write_str("feature.memory"),
Self::MergeRequest => formatter.write_str("feature.merge_request"),
Self::Objective => formatter.write_str("feature.objective"),
Self::Orchestration => formatter.write_str("feature.orchestration"),
Self::Plugins => formatter.write_str("feature.plugins or plugin packages"),
Self::Ticket => formatter.write_str("feature.ticket"),
Self::Worker => formatter.write_str("feature.worker"),
}
}
}
pub fn validate_profile_execution_target(
manifest: &WorkerManifest,
target: ProfileExecutionTarget,
) -> Result<(), ProfileError> {
if target == ProfileExecutionTarget::Workspace {
return Ok(());
}
let feature = &manifest.feature;
let mut requirements = BTreeSet::new();
if feature.flow.enabled {
requirements.insert(WorkspaceAuthorityRequirement::Flow);
}
if feature.manage_workdir.enabled {
requirements.insert(WorkspaceAuthorityRequirement::ManageWorkdir);
}
if feature.memory.enabled || feature.memory.staging {
requirements.insert(WorkspaceAuthorityRequirement::Memory);
}
if feature.merge_request.show
|| feature.merge_request.open
|| feature.merge_request.review
|| feature.merge_request.readiness_check
|| feature.merge_request.complete
{
requirements.insert(WorkspaceAuthorityRequirement::MergeRequest);
}
if feature.objective.enabled {
requirements.insert(WorkspaceAuthorityRequirement::Objective);
}
if feature.orchestration.enabled {
requirements.insert(WorkspaceAuthorityRequirement::Orchestration);
}
if feature.plugins.enabled || !manifest.plugins.is_empty() {
requirements.insert(WorkspaceAuthorityRequirement::Plugins);
}
if feature.ticket.enabled
|| feature.ticket.authoring
|| feature.ticket.thread
|| feature.ticket.intake
|| feature.ticket.workflow
{
requirements.insert(WorkspaceAuthorityRequirement::Ticket);
}
if feature.worker.enabled {
requirements.insert(WorkspaceAuthorityRequirement::Worker);
}
if requirements.is_empty() {
Ok(())
} else {
Err(ProfileError::UnsupportedExecutionTarget {
target,
requirements: requirements.into_iter().collect(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ProfileSource {
@@ -217,13 +285,14 @@ impl ProfileRegistryEntry {
source: ProfileRegistrySource,
name: &'static str,
label: &'static str,
provenance: String,
description: Option<String>,
) -> Self {
Self {
source,
name: name.to_string(),
path: None,
provenance: label.to_string(),
provenance,
description,
is_default: false,
artifact: ProfileRegistryArtifact::Builtin { label },
@@ -321,12 +390,16 @@ pub struct ProfileDiscovery {
}
impl ProfileDiscovery {
pub fn for_cwd(_cwd: &Path) -> Self {
pub fn user_settings() -> Self {
Self {
user_config: paths::user_profiles_path(),
project_config: None,
}
}
pub fn for_cwd(_cwd: &Path) -> Self {
Self::user_settings()
}
pub fn with_sources(user_config: Option<PathBuf>, project_config: Option<PathBuf>) -> Self {
Self {
user_config,
@@ -412,15 +485,22 @@ impl ProfileResolver {
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()?;
let registry = ProfileDiscovery::user_settings().discover()?;
self.resolve_from_registry(selector, &registry, options)
}
}
}
pub fn resolve_for_target(
&self,
selector: &ProfileSelector,
options: ProfileResolveOptions,
target: ProfileExecutionTarget,
) -> Result<ResolvedProfile, ProfileError> {
let resolved = self.resolve(selector, options)?;
validate_profile_execution_target(&resolved.manifest, target)?;
Ok(resolved)
}
/// Resolve a registry/default selector against an already-discovered
/// registry. Callers such as SubWorkerSpawn use this to bind discovery to the
/// Worker's cwd instead of the process current directory.
@@ -503,7 +583,7 @@ impl ProfileResolver {
.as_deref()
.unwrap_or_else(|| Path::new(".")),
)?;
let raw_artifact = builtin_profile_artifact(label).ok_or_else(|| {
let raw_artifact = resolve_builtin_profile_artifact(label)?.ok_or_else(|| {
ProfileError::InvalidProfile(format!("unknown builtin profile artifact `{label}`"))
})?;
resolve_profile_value(
@@ -565,7 +645,8 @@ fn resolve_profile_value(
memory: profile.memory.map(Into::into),
skills: profile.skills,
};
let config = WorkerManifestConfig::builtin_defaults().merge(config.resolve_paths(profile_dir));
let config =
WorkerManifestConfig::resolution_defaults().merge(config.resolve_paths(profile_dir));
let mut manifest = WorkerManifest::try_from(config).map_err(ProfileError::ManifestResolve)?;
manifest.profile = Some(ProfileManifestSnapshot {
source: source.clone(),
@@ -759,14 +840,30 @@ fn load_profile_registry_file(
}
fn add_builtin_profiles(registry: &mut ProfileRegistry) {
for profile in BUILTIN_PROFILES {
let catalog = builtin_profile_catalog_snapshot();
let digest = catalog.digest();
for profile in builtin_profile_entrypoints() {
let label = profile
.selector
.expect("built-in Profile entrypoint must have a selector");
let name = label
.strip_prefix("builtin:")
.expect("built-in Profile selector must be source-qualified");
registry.push_entry(ProfileRegistryEntry::embedded(
ProfileRegistrySource::Builtin,
profile.name,
profile.label,
name,
label,
format!("{}#{digest}", profile.path),
Some(profile.description.into()),
));
}
registry.set_default(ProfileDefault {
source: Some(ProfileRegistrySource::Builtin),
name: BUILTIN_DEFAULT_PROFILE
.strip_prefix("builtin:")
.expect("built-in default selector must be source-qualified")
.to_owned(),
});
}
fn parse_profile_ref(raw: &str) -> (Option<ProfileRegistrySource>, String) {
@@ -804,201 +901,6 @@ fn read_profile_artifact_file(path: &Path) -> Result<serde_json::Value, ProfileE
}
}
fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
let mut value = builtin_base_profile_artifact();
match label {
"builtin:companion" | "companion" => {
apply_role_profile(
&mut value,
"companion",
"Workspace companion profile.",
"workspace_write",
true,
true,
true,
true,
);
Some(value)
}
"builtin:intake" | "intake" => {
apply_role_profile(
&mut value,
"intake",
"Ticket intake profile.",
"workspace_write",
true,
true,
true,
false,
);
Some(value)
}
"builtin:orchestrator" | "orchestrator" => {
apply_role_profile(
&mut value,
"orchestrator",
"Ticket orchestrator profile.",
"workspace_write",
true,
true,
true,
false,
);
Some(value)
}
"builtin:coder" | "coder" => {
apply_role_profile(
&mut value,
"coder",
"Ticket implementation coder profile.",
"workspace_write",
true,
true,
true,
true,
);
Some(value)
}
"builtin:reviewer" | "reviewer" => {
apply_role_profile(
&mut value,
"reviewer",
"Ticket review profile.",
"workspace_read",
true,
true,
true,
false,
);
Some(value)
}
"builtin:memory-consolidation" | "memory-consolidation" => {
value["slug"] = serde_json::Value::String("memory-consolidation".to_string());
value["description"] =
serde_json::Value::String("Memory staging consolidation profile.".to_string());
value["feature"]["task"] = serde_json::json!({ "enabled": false });
value["feature"]["memory"] = serde_json::json!({ "enabled": true, "staging": true });
value["feature"]["web"] = serde_json::json!({ "enabled": false });
value["feature"]["sub_worker"] = serde_json::json!({ "enabled": false });
value["feature"]["objective"] = serde_json::json!({ "enabled": false });
value["feature"]["ticket"] = serde_json::json!({ "enabled": false, "thread": false });
Some(value)
}
_ => None,
}
}
fn builtin_base_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" },
"compaction": {
"kind": "tokens",
"threshold": 240000,
"request_threshold": 270000,
"worker_context_max_tokens": 100000
},
"feature": {
"task": { "enabled": true },
"memory": { "enabled": true },
"web": { "enabled": true },
"image": { "enabled": true },
"sub_worker": { "enabled": true },
"worker": { "enabled": false },
"objective": { "enabled": true },
"ticket": { "enabled": true, "authoring": true, "thread": true }
},
"memory": {
"extract_threshold": 50000,
"consolidation_threshold_files": 5,
"consolidation_threshold_bytes": 50000
},
"web": {
"enabled": true,
"search": {
"provider": "brave",
"api_key_secret": "web/brave/default"
}
}
})
}
#[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,
sub_worker: bool,
) {
value["slug"] = serde_json::Value::String(slug.to_string());
value["description"] = serde_json::Value::String(description.to_string());
value["feature"]["task"] = serde_json::json!({ "enabled": task });
value["feature"]["memory"] = serde_json::json!({ "enabled": memory });
value["feature"]["web"] = serde_json::json!({ "enabled": web });
value["feature"]["image"] = serde_json::json!({ "enabled": true });
value["feature"]["sub_worker"] = serde_json::json!({ "enabled": sub_worker });
value["feature"]["flow"] = serde_json::json!({ "enabled": slug == "coder" });
value["feature"]["worker"] = serde_json::json!({
"enabled": slug == "orchestrator",
"direct_spawn": slug != "orchestrator"
});
value["feature"]["manage_workdir"] = serde_json::json!({
"enabled": matches!(slug, "companion" | "orchestrator")
});
value["feature"]["orchestration"] = serde_json::json!({ "enabled": slug == "orchestrator" });
let ticket = match slug {
"companion" => serde_json::json!({ "enabled": true, "authoring": true, "thread": true }),
"intake" => {
serde_json::json!({ "enabled": true, "authoring": true, "thread": true, "intake": true })
}
"orchestrator" => {
serde_json::json!({ "enabled": true, "thread": true, "workflow": true })
}
"coder" => serde_json::json!({ "enabled": true, "thread": true }),
"reviewer" => serde_json::json!({ "enabled": true, "thread": true }),
_ => serde_json::json!({ "enabled": true, "authoring": true, "thread": true }),
};
value["feature"]["ticket"] = ticket;
let merge_request = match slug {
"coder" => serde_json::json!({
"show": true,
"open": true,
"review": false,
"readiness_check": false,
"complete": false
}),
"reviewer" => serde_json::json!({
"show": true,
"open": false,
"review": true,
"readiness_check": false,
"complete": false
}),
"orchestrator" => serde_json::json!({
"show": true,
"open": false,
"review": false,
"readiness_check": true,
"complete": true
}),
_ => serde_json::json!({
"show": false,
"open": false,
"review": false,
"readiness_check": false,
"complete": false
}),
};
value["feature"]["merge_request"] = merge_request;
}
fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> {
let Some(map) = value.as_object() else {
return Err(ProfileError::InvalidProfile(
@@ -1288,6 +1190,13 @@ pub enum ProfileError {
#[source]
source: toml::de::Error,
},
#[error("failed to evaluate built-in Profile `{selector}`: {message}")]
BuiltinProfileEvaluation { selector: String, message: String },
#[error("Profile requires unsupported {target} launch authorities: {requirements:?}")]
UnsupportedExecutionTarget {
target: ProfileExecutionTarget,
requirements: Vec<WorkspaceAuthorityRequirement>,
},
#[error("no default profile is configured")]
NoDefaultProfile,
#[error("profile resolution requires an explicit runtime Worker name")]
@@ -1341,18 +1250,21 @@ mod tests {
);
}
#[test]
fn builtin_profiles_do_not_define_an_implicit_default() {
fn builtin_default_is_explicit_registry_authority() {
let registry = ProfileDiscovery::with_sources(None, None)
.discover()
.unwrap();
assert!(matches!(
registry.default_entry(),
Err(ProfileError::NoDefaultProfile)
));
assert!(matches!(
registry.select(&ProfileSelector::Default),
Err(ProfileError::NoDefaultProfile)
));
let default = registry.default_entry().unwrap();
assert_eq!(default.source, ProfileRegistrySource::Builtin);
assert_eq!(default.name, "default");
assert_eq!(default.qualified_name(), BUILTIN_DEFAULT_PROFILE);
assert!(default.is_default);
assert!(
default
.provenance
.starts_with("profiles/default.dcdl#sha256:")
);
assert_eq!(registry.select(&ProfileSelector::Default).unwrap(), default);
}
#[test]
fn builtin_role_profiles_are_registered_and_resolve() {
@@ -1407,6 +1319,92 @@ mod tests {
}
}
#[test]
fn builtin_default_resolves_as_a_standalone_local_capability_profile() {
let tmp = TempDir::new().unwrap();
let resolved = ProfileResolver::new()
.with_workspace_base(tmp.path())
.resolve_for_target(
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, "default"),
ProfileResolveOptions::with_worker_name("standalone-worker"),
ProfileExecutionTarget::Standalone,
)
.unwrap();
assert!(matches!(
&resolved.source,
ProfileSource::Registry {
source: ProfileRegistrySource::Builtin,
name,
path: None,
provenance: Some(provenance),
..
} if name == "default" && provenance.starts_with("profiles/default.dcdl#sha256:")
));
assert!(resolved.manifest.feature.task.enabled);
assert!(resolved.manifest.feature.web.enabled);
assert!(resolved.manifest.feature.image.enabled);
assert!(resolved.manifest.feature.sub_worker.enabled);
assert!(resolved.manifest.scope.allow.iter().any(|rule| {
rule.permission == protocol::Permission::Write && rule.target == tmp.path()
}));
assert!(resolved.manifest.delegation_scope.allow.iter().any(|rule| {
rule.permission == protocol::Permission::Write && rule.target == tmp.path()
}));
assert!(!resolved.manifest.feature.memory.enabled);
assert!(!resolved.manifest.feature.ticket.enabled);
assert!(!resolved.manifest.feature.objective.enabled);
assert!(!resolved.manifest.feature.flow.enabled);
assert!(!resolved.manifest.feature.worker.enabled);
assert!(!resolved.manifest.feature.manage_workdir.enabled);
assert!(!resolved.manifest.feature.plugins.enabled);
assert!(resolved.manifest.plugins.is_empty());
}
#[test]
fn standalone_rejects_profiles_that_require_workspace_authority() {
let tmp = TempDir::new().unwrap();
let error = ProfileResolver::new()
.with_workspace_base(tmp.path())
.resolve_for_target(
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, "coder"),
ProfileResolveOptions::with_worker_name("standalone-worker"),
ProfileExecutionTarget::Standalone,
)
.unwrap_err();
let diagnostic = error.to_string();
let ProfileError::UnsupportedExecutionTarget {
target,
requirements,
} = error
else {
panic!("unexpected error: {error}");
};
assert_eq!(target, ProfileExecutionTarget::Standalone);
assert!(requirements.contains(&WorkspaceAuthorityRequirement::Memory));
assert!(requirements.contains(&WorkspaceAuthorityRequirement::MergeRequest));
assert!(requirements.contains(&WorkspaceAuthorityRequirement::Ticket));
assert!(!diagnostic.contains(tmp.path().to_string_lossy().as_ref()));
}
#[test]
fn repository_markers_do_not_change_builtin_profile_authority() {
let tmp = TempDir::new().unwrap();
let nested = tmp.path().join("repository/nested");
std::fs::create_dir_all(&nested).unwrap();
std::fs::create_dir_all(tmp.path().join("repository/.yoi")).unwrap();
std::fs::write(
tmp.path().join("repository/.yoi/profiles.toml"),
"default = { source = 'project', name = 'shadow' }\n",
)
.unwrap();
let discovery = ProfileDiscovery::for_cwd(&nested);
assert_eq!(discovery.user_config, paths::user_profiles_path());
assert!(discovery.project_config.is_none());
}
#[test]
fn builtin_coder_uses_sub_worker_control_without_worker_control() {
let tmp = TempDir::new().unwrap();
+1 -1
View File
@@ -422,7 +422,7 @@ impl ProfileRuntimeWorkerFactory {
fn restore_fallback_manifest(
worker_name: &str,
) -> Result<(manifest::WorkerManifest, PromptCatalogSource), String> {
let mut config = manifest::WorkerManifestConfig::builtin_defaults();
let mut config = manifest::WorkerManifestConfig::resolution_defaults();
config.worker.name = Some(worker_name.to_string());
let manifest = manifest::WorkerManifest::try_from(config)
.map_err(|err| format!("failed to build restore fallback manifest: {err}"))?;
+5 -4
View File
@@ -184,15 +184,16 @@ fn load_spawn_config_json(
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
let config = serde_json::from_str::<WorkerManifestConfig>(config_json)
.map_err(|e| format!("failed to parse --spawn-config-json: {e}"))?;
let manifest = WorkerManifest::try_from(WorkerManifestConfig::builtin_defaults().merge(config))
.map_err(|e| format!("failed to resolve --spawn-config-json: {e}"))?;
let manifest =
WorkerManifest::try_from(WorkerManifestConfig::resolution_defaults().merge(config))
.map_err(|e| format!("failed to resolve --spawn-config-json: {e}"))?;
Ok((manifest, PromptCatalogSource::builtins_only()))
}
fn load_builtin_default_manifest(
worker_name: &str,
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
let mut config = WorkerManifestConfig::builtin_defaults();
let mut config = WorkerManifestConfig::resolution_defaults();
config.worker.name = Some(worker_name.to_string());
let manifest = WorkerManifest::try_from(config)
.map_err(|e| format!("failed to resolve builtin worker defaults: {e}"))?;
@@ -259,7 +260,7 @@ fn load_single_manifest(
absolute_path.display()
)
})?;
let mut config = WorkerManifestConfig::builtin_defaults().merge(
let mut config = WorkerManifestConfig::resolution_defaults().merge(
WorkerManifestConfig::from_toml(&toml)
.map_err(|e| format!("failed to parse manifest {}: {e}", path.display()))?
.resolve_paths(base_dir),
+5 -6
View File
@@ -403,11 +403,10 @@ impl Tool for SubWorkerSpawnTool {
allow: scope_allow.clone(),
deny: Vec::new(),
};
let mut child_manifest =
WorkerManifest::try_from(WorkerManifestConfig::builtin_defaults().merge(child_config))
.map_err(|error| {
ToolError::ExecutionFailed(format!("resolve child manifest: {error}"))
})?;
let mut child_manifest = WorkerManifest::try_from(
WorkerManifestConfig::resolution_defaults().merge(child_config),
)
.map_err(|error| ToolError::ExecutionFailed(format!("resolve child manifest: {error}")))?;
// Delegated children stay bound to their scoped session and cannot use
// Workspace attachment tools to replace it with parent-level authority.
child_manifest.feature.manage_workdir.enabled = false;
@@ -1631,7 +1630,7 @@ max_tokens = 3333
Some(true)
);
let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults()
let manifest: WorkerManifest = WorkerManifestConfig::resolution_defaults()
.merge(parsed)
.try_into()
.unwrap();
+58 -89
View File
@@ -9,7 +9,6 @@ use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
collections::BTreeMap,
future::Future,
path::PathBuf,
pin::Pin,
@@ -3722,94 +3721,6 @@ fn builtin_profile_config_bundle(
.with_computed_digest())
}
fn builtin_profile_source_archive(
profile: &ProfileSelector,
) -> Result<ProfileSourceArchive, String> {
let selected = embedded_profile_label(profile)
.ok_or_else(|| "profile selector must identify a concrete profile".to_string())?;
let selected_path = embedded_profile_path(profile)?;
let mut entrypoints = BTreeMap::new();
for slug in [
"companion",
"intake",
"orchestrator",
"coder",
"reviewer",
"memory-consolidation",
] {
entrypoints.insert(format!("builtin:{slug}"), format!("profiles/{slug}.dcdl"));
}
entrypoints.insert(selected, selected_path);
let mut sources = BTreeMap::new();
sources.insert(
"profiles/base.dcdl".to_string(),
include_str!("../../../resources/profiles/base.dcdl").to_string(),
);
sources.insert(
"profiles/companion.dcdl".to_string(),
include_str!("../../../resources/profiles/companion.dcdl").to_string(),
);
sources.insert(
"profiles/intake.dcdl".to_string(),
include_str!("../../../resources/profiles/intake.dcdl").to_string(),
);
sources.insert(
"profiles/orchestrator.dcdl".to_string(),
include_str!("../../../resources/profiles/orchestrator.dcdl").to_string(),
);
sources.insert(
"profiles/coder.dcdl".to_string(),
include_str!("../../../resources/profiles/coder.dcdl").to_string(),
);
sources.insert(
"profiles/reviewer.dcdl".to_string(),
include_str!("../../../resources/profiles/reviewer.dcdl").to_string(),
);
sources.insert(
"profiles/memory-consolidation.dcdl".to_string(),
include_str!("../../../resources/profiles/memory-consolidation.dcdl").to_string(),
);
let mut imports = BTreeMap::new();
for slug in [
"companion",
"intake",
"orchestrator",
"coder",
"reviewer",
"memory-consolidation",
] {
imports.insert(
format!("profiles/{slug}.dcdl\0./base.dcdl"),
"profiles/base.dcdl".to_string(),
);
}
ProfileSourceArchive::build(ProfileSourceArchiveInput {
id: "builtin-decodal-profiles-v1".to_string(),
entrypoints,
imports,
sources,
})
.map_err(|err| err.to_string())
}
fn embedded_profile_path(profile: &ProfileSelector) -> Result<String, String> {
match profile {
ProfileSelector::Builtin(name) => match name.strip_prefix("builtin:").unwrap_or(name) {
"companion" => Ok("profiles/companion.dcdl".to_string()),
"intake" => Ok("profiles/intake.dcdl".to_string()),
"orchestrator" => Ok("profiles/orchestrator.dcdl".to_string()),
"coder" => Ok("profiles/coder.dcdl".to_string()),
"reviewer" => Ok("profiles/reviewer.dcdl".to_string()),
"memory-consolidation" => Ok("profiles/memory-consolidation.dcdl".to_string()),
other => Err(format!("unknown builtin profile selector: builtin:{other}")),
},
ProfileSelector::Named(name) => Err(format!("unknown named profile selector: {name}")),
}
}
fn embedded_profile_label(profile: &ProfileSelector) -> Option<String> {
Some(match profile {
ProfileSelector::Builtin(name) | ProfileSelector::Named(name) => {
@@ -3825,6 +3736,33 @@ fn embedded_profile_label(profile: &ProfileSelector) -> Option<String> {
})
}
fn builtin_profile_source_archive(
profile: &ProfileSelector,
) -> Result<ProfileSourceArchive, String> {
let selected_profile = match profile {
ProfileSelector::Builtin(name) => name.clone(),
ProfileSelector::Named(name) => {
return Err(format!(
"embedded runtime does not provide named Profile `{name}`"
));
}
};
let catalog = manifest::builtin_profile_catalog_snapshot();
if !catalog.entrypoints.contains_key(&selected_profile) {
return Err(format!(
"embedded runtime does not provide Profile `{selected_profile}`"
));
}
ProfileSourceArchive::build(ProfileSourceArchiveInput {
id: catalog.id.to_owned(),
sources: catalog.sources,
entrypoints: catalog.entrypoints,
imports: catalog.imports,
})
.map_err(|error| format!("failed to build built-in Profile source archive: {error}"))
}
const MEMORY_CONSOLIDATION_PROFILE: &str = "memory-consolidation";
const MEMORY_CONSOLIDATION_SINGLETON_KEY: &str = "workspace-memory-consolidation";
const WORKSPACE_ORCHESTRATOR_PROFILE: &str = "orchestrator";
@@ -4451,6 +4389,37 @@ mod tests {
assert!(companion.feature.manage_workdir.enabled);
assert!(companion.feature.sub_worker.enabled);
assert!(!companion.feature.worker.enabled);
let default_from_archive = archive
.resolve_profile("builtin:default", root.path(), "embedded-test-default")
.unwrap();
let default_from_native = manifest::ProfileResolver::new()
.with_workspace_base(root.path())
.resolve(
&manifest::ProfileSelector::source_named(
manifest::ProfileRegistrySource::Builtin,
"default",
),
manifest::ProfileResolveOptions::with_worker_name("embedded-test-default"),
)
.unwrap()
.manifest;
let mut archive_value = serde_json::to_value(&default_from_archive).unwrap();
let mut native_value = serde_json::to_value(&default_from_native).unwrap();
let archive_profile = archive_value
.as_object_mut()
.and_then(|value| value.remove("profile"))
.expect("archive resolution records Profile provenance");
let native_profile = native_value
.as_object_mut()
.and_then(|value| value.remove("profile"))
.expect("native resolution records Profile provenance");
assert_eq!(archive_value, native_value);
assert_eq!(archive_profile["source"]["kind"], "archive");
assert_eq!(native_profile["source"]["kind"], "registry");
assert!(default_from_archive.feature.sub_worker.enabled);
assert!(!default_from_archive.feature.ticket.enabled);
assert!(!default_from_archive.feature.objective.enabled);
let coder = archive
.resolve_profile("builtin:coder", root.path(), "embedded-test-coder")
.unwrap();
+34
View File
@@ -0,0 +1,34 @@
import "./base.dcdl" // {
slug = "default";
description = "Standalone Yoi coding profile.";
scope = "workspace_write";
delegation_scope = "workspace_write";
feature = {
task = { enabled = true; };
memory = { enabled = false; staging = false; };
web = { enabled = true; };
image = { enabled = true; };
sub_worker = { enabled = true; };
flow = { enabled = false; };
worker = { enabled = false; direct_spawn = true; };
objective = { enabled = false; };
manage_workdir = { enabled = false; };
ticket = {
enabled = false;
authoring = false;
thread = false;
intake = false;
workflow = false;
};
merge_request = {
show = false;
open = false;
review = false;
readiness_check = false;
complete = false;
};
orchestration = { enabled = false; };
plugins = { enabled = false; };
};
}