feat: unify builtin Profile resolution authority
This commit is contained in:
Generated
+1
@@ -2629,6 +2629,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"agen",
|
"agen",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
"decodal",
|
||||||
"protocol",
|
"protocol",
|
||||||
"secrets",
|
"secrets",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ license.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
arc-swap = "1"
|
arc-swap = "1"
|
||||||
agen = { workspace = true }
|
agen = { workspace = true }
|
||||||
|
decodal.workspace = true
|
||||||
protocol = { workspace = true }
|
protocol = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -566,15 +566,16 @@ impl WorkerManifestConfig {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Base config populated with the in-code defaults listed in
|
/// Base config populated with the in-code per-field defaults listed in
|
||||||
/// [`crate::defaults`]. Profile and one-file Manifest resolvers start
|
/// [`crate::defaults`]. This is not a selectable Profile and does not
|
||||||
/// from this layer so every per-field default lives at exactly one
|
/// enable a launch capability surface. Profile and one-file Manifest
|
||||||
/// call site (the `defaults` module).
|
/// 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
|
/// `TryFrom<WorkerManifestConfig>` also reads the same constants as a
|
||||||
/// belt-and-suspenders fallback, so a manually-constructed config
|
/// belt-and-suspenders fallback, so a manually-constructed config
|
||||||
/// that skips this layer still resolves to the same values.
|
/// that skips this layer still resolves to the same values.
|
||||||
pub fn builtin_defaults() -> Self {
|
pub fn resolution_defaults() -> Self {
|
||||||
Self {
|
Self {
|
||||||
engine: EngineManifestConfig {
|
engine: EngineManifestConfig {
|
||||||
tool_output: ToolOutputLimitsPartial {
|
tool_output: ToolOutputLimitsPartial {
|
||||||
@@ -1973,7 +1974,7 @@ enabled = false
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults()
|
let manifest: WorkerManifest = WorkerManifestConfig::resolution_defaults()
|
||||||
.merge(cfg)
|
.merge(cfg)
|
||||||
.merge(WorkerManifestConfig {
|
.merge(WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
@@ -2074,7 +2075,7 @@ enabled = true
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults()
|
let manifest: WorkerManifest = WorkerManifestConfig::resolution_defaults()
|
||||||
.merge(base)
|
.merge(base)
|
||||||
.merge(upper)
|
.merge(upper)
|
||||||
.merge(WorkerManifestConfig {
|
.merge(WorkerManifestConfig {
|
||||||
@@ -2137,7 +2138,7 @@ permission = "write"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_defaults_populates_worker_limit_defaults() {
|
fn builtin_defaults_populates_worker_limit_defaults() {
|
||||||
let cfg = WorkerManifestConfig::builtin_defaults();
|
let cfg = WorkerManifestConfig::resolution_defaults();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cfg.engine.tool_output.default_max_bytes,
|
cfg.engine.tool_output.default_max_bytes,
|
||||||
Some(defaults::TOOL_OUTPUT_MAX_BYTES)
|
Some(defaults::TOOL_OUTPUT_MAX_BYTES)
|
||||||
@@ -2172,7 +2173,7 @@ permission = "write"
|
|||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let merged = WorkerManifestConfig::builtin_defaults().merge(overlay);
|
let merged = WorkerManifestConfig::resolution_defaults().merge(overlay);
|
||||||
let manifest: WorkerManifest = merged.try_into().unwrap();
|
let manifest: WorkerManifest = merged.try_into().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
manifest.engine.tool_output.default_max_bytes,
|
manifest.engine.tool_output.default_max_bytes,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod builtin_profile;
|
||||||
mod config;
|
mod config;
|
||||||
pub mod defaults;
|
pub mod defaults;
|
||||||
mod model;
|
mod model;
|
||||||
@@ -7,6 +8,11 @@ pub mod plugin;
|
|||||||
mod profile;
|
mod profile;
|
||||||
mod scope;
|
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::{
|
pub use config::{
|
||||||
CompactionConfigPartial, EngineManifestConfig, FileUploadLimitsPartial,
|
CompactionConfigPartial, EngineManifestConfig, FileUploadLimitsPartial,
|
||||||
PermissionConfigPartial, ResolveError, SessionConfigPartial, ToolOutputLimitsPartial,
|
PermissionConfigPartial, ResolveError, SessionConfigPartial, ToolOutputLimitsPartial,
|
||||||
@@ -17,10 +23,11 @@ pub use model::{
|
|||||||
};
|
};
|
||||||
pub use paths::user_profiles_path;
|
pub use paths::user_profiles_path;
|
||||||
pub use profile::{
|
pub use profile::{
|
||||||
ProfileDiscovery, ProfileError, ProfileManifestSnapshot, ProfileMetadata, ProfileRegistry,
|
ProfileDiscovery, ProfileError, ProfileExecutionTarget, ProfileManifestSnapshot,
|
||||||
ProfileRegistryEntry, ProfileRegistrySource, ProfileResolveOptions, ProfileResolver,
|
ProfileMetadata, ProfileRegistry, ProfileRegistryEntry, ProfileRegistrySource,
|
||||||
ProfileSelector, ProfileSource, ResolvedProfile, resolve_profile_artifact,
|
ProfileResolveOptions, ProfileResolver, ProfileSelector, ProfileSource, ResolvedProfile,
|
||||||
resolve_profile_artifact_value,
|
WorkspaceAuthorityRequirement, resolve_profile_artifact, resolve_profile_artifact_value,
|
||||||
|
validate_profile_execution_target,
|
||||||
};
|
};
|
||||||
pub use protocol::{Permission, ScopeRule};
|
pub use protocol::{Permission, ScopeRule};
|
||||||
pub use scope::{DelegationScope, Scope, ScopeError, SharedScope};
|
pub use scope::{DelegationScope, Scope, ScopeError, SharedScope};
|
||||||
|
|||||||
+254
-256
@@ -6,9 +6,14 @@
|
|||||||
//! from launch context.
|
//! from launch context.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::fmt;
|
||||||
use std::path::{Path, PathBuf};
|
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::{
|
use crate::config::{
|
||||||
CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial,
|
CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial,
|
||||||
};
|
};
|
||||||
@@ -23,45 +28,6 @@ use crate::{
|
|||||||
const PROFILE_FORMAT_V1: &str = "yoi.profile.v1";
|
const PROFILE_FORMAT_V1: &str = "yoi.profile.v1";
|
||||||
const BUILTIN_MODEL_CATALOG: &str = include_str!("../../../resources/models/builtin.toml");
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum ProfileRegistrySource {
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum ProfileSource {
|
pub enum ProfileSource {
|
||||||
@@ -217,13 +285,14 @@ impl ProfileRegistryEntry {
|
|||||||
source: ProfileRegistrySource,
|
source: ProfileRegistrySource,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
|
provenance: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
source,
|
source,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
path: None,
|
path: None,
|
||||||
provenance: label.to_string(),
|
provenance,
|
||||||
description,
|
description,
|
||||||
is_default: false,
|
is_default: false,
|
||||||
artifact: ProfileRegistryArtifact::Builtin { label },
|
artifact: ProfileRegistryArtifact::Builtin { label },
|
||||||
@@ -321,12 +390,16 @@ pub struct ProfileDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProfileDiscovery {
|
impl ProfileDiscovery {
|
||||||
pub fn for_cwd(_cwd: &Path) -> Self {
|
pub fn user_settings() -> Self {
|
||||||
Self {
|
Self {
|
||||||
user_config: paths::user_profiles_path(),
|
user_config: paths::user_profiles_path(),
|
||||||
project_config: None,
|
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 {
|
pub fn with_sources(user_config: Option<PathBuf>, project_config: Option<PathBuf>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
user_config,
|
user_config,
|
||||||
@@ -412,15 +485,22 @@ impl ProfileResolver {
|
|||||||
options,
|
options,
|
||||||
),
|
),
|
||||||
ProfileSelector::Named { .. } | ProfileSelector::Default => {
|
ProfileSelector::Named { .. } | ProfileSelector::Default => {
|
||||||
let cwd = std::env::current_dir().map_err(|source| ProfileError::CommandIo {
|
let registry = ProfileDiscovery::user_settings().discover()?;
|
||||||
path: PathBuf::from("."),
|
|
||||||
source,
|
|
||||||
})?;
|
|
||||||
let registry = ProfileDiscovery::for_cwd(&cwd).discover()?;
|
|
||||||
self.resolve_from_registry(selector, ®istry, options)
|
self.resolve_from_registry(selector, ®istry, 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
|
/// Resolve a registry/default selector against an already-discovered
|
||||||
/// registry. Callers such as SubWorkerSpawn use this to bind discovery to the
|
/// registry. Callers such as SubWorkerSpawn use this to bind discovery to the
|
||||||
/// Worker's cwd instead of the process current directory.
|
/// Worker's cwd instead of the process current directory.
|
||||||
@@ -503,7 +583,7 @@ impl ProfileResolver {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or_else(|| Path::new(".")),
|
.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}`"))
|
ProfileError::InvalidProfile(format!("unknown builtin profile artifact `{label}`"))
|
||||||
})?;
|
})?;
|
||||||
resolve_profile_value(
|
resolve_profile_value(
|
||||||
@@ -565,7 +645,8 @@ fn resolve_profile_value(
|
|||||||
memory: profile.memory.map(Into::into),
|
memory: profile.memory.map(Into::into),
|
||||||
skills: profile.skills,
|
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)?;
|
let mut manifest = WorkerManifest::try_from(config).map_err(ProfileError::ManifestResolve)?;
|
||||||
manifest.profile = Some(ProfileManifestSnapshot {
|
manifest.profile = Some(ProfileManifestSnapshot {
|
||||||
source: source.clone(),
|
source: source.clone(),
|
||||||
@@ -759,14 +840,30 @@ fn load_profile_registry_file(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn add_builtin_profiles(registry: &mut ProfileRegistry) {
|
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(
|
registry.push_entry(ProfileRegistryEntry::embedded(
|
||||||
ProfileRegistrySource::Builtin,
|
ProfileRegistrySource::Builtin,
|
||||||
profile.name,
|
name,
|
||||||
profile.label,
|
label,
|
||||||
|
format!("{}#{digest}", profile.path),
|
||||||
Some(profile.description.into()),
|
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) {
|
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> {
|
fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> {
|
||||||
let Some(map) = value.as_object() else {
|
let Some(map) = value.as_object() else {
|
||||||
return Err(ProfileError::InvalidProfile(
|
return Err(ProfileError::InvalidProfile(
|
||||||
@@ -1288,6 +1190,13 @@ pub enum ProfileError {
|
|||||||
#[source]
|
#[source]
|
||||||
source: toml::de::Error,
|
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")]
|
#[error("no default profile is configured")]
|
||||||
NoDefaultProfile,
|
NoDefaultProfile,
|
||||||
#[error("profile resolution requires an explicit runtime Worker name")]
|
#[error("profile resolution requires an explicit runtime Worker name")]
|
||||||
@@ -1341,18 +1250,21 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_profiles_do_not_define_an_implicit_default() {
|
fn builtin_default_is_explicit_registry_authority() {
|
||||||
let registry = ProfileDiscovery::with_sources(None, None)
|
let registry = ProfileDiscovery::with_sources(None, None)
|
||||||
.discover()
|
.discover()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(matches!(
|
let default = registry.default_entry().unwrap();
|
||||||
registry.default_entry(),
|
assert_eq!(default.source, ProfileRegistrySource::Builtin);
|
||||||
Err(ProfileError::NoDefaultProfile)
|
assert_eq!(default.name, "default");
|
||||||
));
|
assert_eq!(default.qualified_name(), BUILTIN_DEFAULT_PROFILE);
|
||||||
assert!(matches!(
|
assert!(default.is_default);
|
||||||
registry.select(&ProfileSelector::Default),
|
assert!(
|
||||||
Err(ProfileError::NoDefaultProfile)
|
default
|
||||||
));
|
.provenance
|
||||||
|
.starts_with("profiles/default.dcdl#sha256:")
|
||||||
|
);
|
||||||
|
assert_eq!(registry.select(&ProfileSelector::Default).unwrap(), default);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_role_profiles_are_registered_and_resolve() {
|
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]
|
#[test]
|
||||||
fn builtin_coder_uses_sub_worker_control_without_worker_control() {
|
fn builtin_coder_uses_sub_worker_control_without_worker_control() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ impl ProfileRuntimeWorkerFactory {
|
|||||||
fn restore_fallback_manifest(
|
fn restore_fallback_manifest(
|
||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
) -> Result<(manifest::WorkerManifest, PromptCatalogSource), String> {
|
) -> 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());
|
config.worker.name = Some(worker_name.to_string());
|
||||||
let manifest = manifest::WorkerManifest::try_from(config)
|
let manifest = manifest::WorkerManifest::try_from(config)
|
||||||
.map_err(|err| format!("failed to build restore fallback manifest: {err}"))?;
|
.map_err(|err| format!("failed to build restore fallback manifest: {err}"))?;
|
||||||
|
|||||||
@@ -184,15 +184,16 @@ fn load_spawn_config_json(
|
|||||||
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||||
let config = serde_json::from_str::<WorkerManifestConfig>(config_json)
|
let config = serde_json::from_str::<WorkerManifestConfig>(config_json)
|
||||||
.map_err(|e| format!("failed to parse --spawn-config-json: {e}"))?;
|
.map_err(|e| format!("failed to parse --spawn-config-json: {e}"))?;
|
||||||
let manifest = WorkerManifest::try_from(WorkerManifestConfig::builtin_defaults().merge(config))
|
let manifest =
|
||||||
.map_err(|e| format!("failed to resolve --spawn-config-json: {e}"))?;
|
WorkerManifest::try_from(WorkerManifestConfig::resolution_defaults().merge(config))
|
||||||
|
.map_err(|e| format!("failed to resolve --spawn-config-json: {e}"))?;
|
||||||
Ok((manifest, PromptCatalogSource::builtins_only()))
|
Ok((manifest, PromptCatalogSource::builtins_only()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_builtin_default_manifest(
|
fn load_builtin_default_manifest(
|
||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||||
let mut config = WorkerManifestConfig::builtin_defaults();
|
let mut config = WorkerManifestConfig::resolution_defaults();
|
||||||
config.worker.name = Some(worker_name.to_string());
|
config.worker.name = Some(worker_name.to_string());
|
||||||
let manifest = WorkerManifest::try_from(config)
|
let manifest = WorkerManifest::try_from(config)
|
||||||
.map_err(|e| format!("failed to resolve builtin worker defaults: {e}"))?;
|
.map_err(|e| format!("failed to resolve builtin worker defaults: {e}"))?;
|
||||||
@@ -259,7 +260,7 @@ fn load_single_manifest(
|
|||||||
absolute_path.display()
|
absolute_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let mut config = WorkerManifestConfig::builtin_defaults().merge(
|
let mut config = WorkerManifestConfig::resolution_defaults().merge(
|
||||||
WorkerManifestConfig::from_toml(&toml)
|
WorkerManifestConfig::from_toml(&toml)
|
||||||
.map_err(|e| format!("failed to parse manifest {}: {e}", path.display()))?
|
.map_err(|e| format!("failed to parse manifest {}: {e}", path.display()))?
|
||||||
.resolve_paths(base_dir),
|
.resolve_paths(base_dir),
|
||||||
|
|||||||
@@ -403,11 +403,10 @@ impl Tool for SubWorkerSpawnTool {
|
|||||||
allow: scope_allow.clone(),
|
allow: scope_allow.clone(),
|
||||||
deny: Vec::new(),
|
deny: Vec::new(),
|
||||||
};
|
};
|
||||||
let mut child_manifest =
|
let mut child_manifest = WorkerManifest::try_from(
|
||||||
WorkerManifest::try_from(WorkerManifestConfig::builtin_defaults().merge(child_config))
|
WorkerManifestConfig::resolution_defaults().merge(child_config),
|
||||||
.map_err(|error| {
|
)
|
||||||
ToolError::ExecutionFailed(format!("resolve child manifest: {error}"))
|
.map_err(|error| ToolError::ExecutionFailed(format!("resolve child manifest: {error}")))?;
|
||||||
})?;
|
|
||||||
// Delegated children stay bound to their scoped session and cannot use
|
// Delegated children stay bound to their scoped session and cannot use
|
||||||
// Workspace attachment tools to replace it with parent-level authority.
|
// Workspace attachment tools to replace it with parent-level authority.
|
||||||
child_manifest.feature.manage_workdir.enabled = false;
|
child_manifest.feature.manage_workdir.enabled = false;
|
||||||
@@ -1631,7 +1630,7 @@ max_tokens = 3333
|
|||||||
Some(true)
|
Some(true)
|
||||||
);
|
);
|
||||||
|
|
||||||
let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults()
|
let manifest: WorkerManifest = WorkerManifestConfig::resolution_defaults()
|
||||||
.merge(parsed)
|
.merge(parsed)
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use serde::de::DeserializeOwned;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
|
||||||
future::Future,
|
future::Future,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
@@ -3722,94 +3721,6 @@ fn builtin_profile_config_bundle(
|
|||||||
.with_computed_digest())
|
.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> {
|
fn embedded_profile_label(profile: &ProfileSelector) -> Option<String> {
|
||||||
Some(match profile {
|
Some(match profile {
|
||||||
ProfileSelector::Builtin(name) | ProfileSelector::Named(name) => {
|
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_PROFILE: &str = "memory-consolidation";
|
||||||
const MEMORY_CONSOLIDATION_SINGLETON_KEY: &str = "workspace-memory-consolidation";
|
const MEMORY_CONSOLIDATION_SINGLETON_KEY: &str = "workspace-memory-consolidation";
|
||||||
const WORKSPACE_ORCHESTRATOR_PROFILE: &str = "orchestrator";
|
const WORKSPACE_ORCHESTRATOR_PROFILE: &str = "orchestrator";
|
||||||
@@ -4451,6 +4389,37 @@ mod tests {
|
|||||||
assert!(companion.feature.manage_workdir.enabled);
|
assert!(companion.feature.manage_workdir.enabled);
|
||||||
assert!(companion.feature.sub_worker.enabled);
|
assert!(companion.feature.sub_worker.enabled);
|
||||||
assert!(!companion.feature.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
|
let coder = archive
|
||||||
.resolve_profile("builtin:coder", root.path(), "embedded-test-coder")
|
.resolve_profile("builtin:coder", root.path(), "embedded-test-coder")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -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; };
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user