diff --git a/.yoi/tickets/00001KWZ5KERY/item.md b/.yoi/tickets/00001KWZ5KERY/item.md index dd93428b..379451ba 100644 --- a/.yoi/tickets/00001KWZ5KERY/item.md +++ b/.yoi/tickets/00001KWZ5KERY/item.md @@ -2,7 +2,7 @@ title: 'Migrate Profiles to Decodal ProfileSourceArchive for Runtime launch' state: 'inprogress' created_at: '2026-07-07T20:51:35Z' -updated_at: '2026-07-08T09:57:38Z' +updated_at: '2026-07-08T10:21:42Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-08T09:11:22Z' diff --git a/.yoi/tickets/00001KWZ5KERY/thread.md b/.yoi/tickets/00001KWZ5KERY/thread.md index d1e51a0e..29e5b2c5 100644 --- a/.yoi/tickets/00001KWZ5KERY/thread.md +++ b/.yoi/tickets/00001KWZ5KERY/thread.md @@ -166,4 +166,28 @@ Validation performed: - nix build .#yoi --no-link +--- + + + +## Implementation report + +Follow-up review fixes for Decodal ProfileSourceArchive launch: + +- Replaced unsupported `tool_enabled` fields in embedded Decodal profiles with existing typed ProfileConfig fields (`feature`, plus the existing model/session/engine/memory/web fields where applicable) so archive resolution can materialize through Decodal and deserialize under `deny_unknown_fields`. +- Removed archive selector fallback to `default`; missing selectors now return `ProfileArchiveError::MissingEntrypoint`. +- Changed Workspace Backend embedded archive construction to reject unknown `Builtin` and `Named` selectors instead of mapping them to the default profile. +- Added tests that resolve every embedded builtin `.dcdl` via the real Backend-built ProfileSourceArchive + Decodal + ProfileConfig path. +- Added tests for unknown Builtin/Named selector rejection and no-default-fallback archive resolution. + +Validation performed: +- git diff --check +- cargo test -p worker-runtime --features ws-server,fs-store +- cargo test -p yoi-workspace-server +- cargo check -p yoi +- cd web/workspace && deno task check && deno task test +- yoi ticket doctor +- nix build .#yoi --no-link + + --- diff --git a/crates/worker-runtime/src/profile_archive.rs b/crates/worker-runtime/src/profile_archive.rs index d0e9e9fd..cc7a6d09 100644 --- a/crates/worker-runtime/src/profile_archive.rs +++ b/crates/worker-runtime/src/profile_archive.rs @@ -323,7 +323,6 @@ impl VerifiedProfileSourceArchive { .manifest .entrypoints .get(selector) - .or_else(|| self.manifest.entrypoints.get("default")) .ok_or_else(|| ProfileArchiveError::MissingEntrypoint(selector.to_string()))?; let mut engine = Engine::new(ArchiveSourceLoader { archive: self }); let source = self @@ -534,6 +533,17 @@ mod tests { assert_eq!(manifest.worker.name, "archive-worker"); } + #[test] + fn unknown_selector_does_not_fallback_to_default() { + let archive = sample_archive(); + let verified = archive.verify().unwrap(); + let root = tempfile::tempdir().unwrap(); + assert!(matches!( + verified.resolve_profile("missing", root.path(), "archive-worker"), + Err(ProfileArchiveError::MissingEntrypoint(selector)) if selector == "missing" + )); + } + #[test] fn archive_loader_rejects_undeclared_import() { let archive = sample_archive(); diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index 9d0ea8d9..290657d3 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -1339,13 +1339,18 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime { .profile .clone() .unwrap_or_else(|| embedded_profile_selector(&request.intent)); - let config_bundle = match self - .runtime - .store_config_bundle(default_embedded_config_bundle(&profile)) - { + let config_bundle = match default_embedded_config_bundle(&profile).and_then(|bundle| { + self.runtime + .store_config_bundle(bundle) + .map_err(|err| err.to_string()) + }) { Ok(availability) => availability.reference, Err(error) => { - diagnostics.push(embedded_runtime_diagnostic(&error)); + diagnostics.push(diagnostic( + "embedded_profile_source_archive_invalid", + DiagnosticSeverity::Error, + error, + )); return WorkerSpawnResult { state: WorkerOperationState::Rejected, worker: None, @@ -2020,7 +2025,18 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime { .profile .clone() .unwrap_or_else(|| embedded_profile_selector(&request.intent)); - let sync = self.sync_config_bundle(default_embedded_config_bundle(&profile)); + let sync = match default_embedded_config_bundle(&profile) { + Ok(bundle) => self.sync_config_bundle(bundle), + Err(error) => ConfigBundleSyncResult { + state: WorkerOperationState::Rejected, + availability: None, + diagnostics: vec![diagnostic( + "remote_profile_source_archive_invalid", + DiagnosticSeverity::Error, + error, + )], + }, + }; let Some(config_bundle) = sync.availability.map(|availability| availability.reference) else { return WorkerSpawnResult { @@ -2349,14 +2365,14 @@ fn embedded_worker_execution_status_label( } } -fn default_embedded_config_bundle(profile: &ProfileSelector) -> ConfigBundle { +fn default_embedded_config_bundle(profile: &ProfileSelector) -> Result { let id = format!( "workspace-runtime-{}", embedded_profile_label(profile) .unwrap_or_else(|| "default".to_string()) .replace([':', '/', ' '], "-") ); - ConfigBundle { + Ok(ConfigBundle { metadata: ConfigBundleMetadata { id, digest: String::new(), @@ -2373,13 +2389,16 @@ fn default_embedded_config_bundle(profile: &ProfileSelector) -> ConfigBundle { label: embedded_profile_label(profile), }], declarations: Vec::new(), - profile_source_archive: Some(default_profile_source_archive(profile)), + profile_source_archive: Some(default_profile_source_archive(profile)?), } - .with_computed_digest() + .with_computed_digest()) } -fn default_profile_source_archive(profile: &ProfileSelector) -> ProfileSourceArchive { +fn default_profile_source_archive( + profile: &ProfileSelector, +) -> Result { let selected = embedded_profile_label(profile).unwrap_or_else(|| "default".to_string()); + let selected_path = embedded_profile_path(profile)?; let mut entrypoints = BTreeMap::new(); entrypoints.insert("default".to_string(), "profiles/default.dcdl".to_string()); entrypoints.insert( @@ -2389,7 +2408,7 @@ fn default_profile_source_archive(profile: &ProfileSelector) -> ProfileSourceArc for slug in ["companion", "intake", "orchestrator", "coder", "reviewer"] { entrypoints.insert(format!("builtin:{slug}"), format!("profiles/{slug}.dcdl")); } - entrypoints.insert(selected, embedded_profile_path(profile)); + entrypoints.insert(selected, selected_path); let mut sources = BTreeMap::new(); sources.insert( @@ -2423,22 +2442,22 @@ fn default_profile_source_archive(profile: &ProfileSelector) -> ProfileSourceArc imports: BTreeMap::new(), sources, }) - .expect("builtin Decodal profile source archive is valid") + .map_err(|err| err.to_string()) } -fn embedded_profile_path(profile: &ProfileSelector) -> String { +fn embedded_profile_path(profile: &ProfileSelector) -> Result { match profile { - ProfileSelector::RuntimeDefault => "profiles/default.dcdl".to_string(), - ProfileSelector::Builtin(name) | ProfileSelector::Named(name) => { - match name.strip_prefix("builtin:").unwrap_or(name) { - "companion" => "profiles/companion.dcdl".to_string(), - "intake" => "profiles/intake.dcdl".to_string(), - "orchestrator" => "profiles/orchestrator.dcdl".to_string(), - "coder" => "profiles/coder.dcdl".to_string(), - "reviewer" => "profiles/reviewer.dcdl".to_string(), - _ => "profiles/default.dcdl".to_string(), - } - } + ProfileSelector::RuntimeDefault => Ok("profiles/default.dcdl".to_string()), + ProfileSelector::Builtin(name) => match name.strip_prefix("builtin:").unwrap_or(name) { + "default" => Ok("profiles/default.dcdl".to_string()), + "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()), + other => Err(format!("unknown builtin profile selector: builtin:{other}")), + }, + ProfileSelector::Named(name) => Err(format!("unknown named profile selector: {name}")), } } @@ -2945,6 +2964,49 @@ mod tests { use std::sync::{Arc, Mutex}; use std::thread; + #[test] + fn embedded_builtin_decodal_profiles_resolve_through_archive() { + let root = tempfile::tempdir().unwrap(); + for selector in [ + ProfileSelector::RuntimeDefault, + ProfileSelector::Builtin("builtin:companion".to_string()), + ProfileSelector::Builtin("builtin:intake".to_string()), + ProfileSelector::Builtin("builtin:orchestrator".to_string()), + ProfileSelector::Builtin("builtin:coder".to_string()), + ProfileSelector::Builtin("builtin:reviewer".to_string()), + ] { + let bundle = default_embedded_config_bundle(&selector).unwrap(); + let archive = bundle + .profile_source_archive + .as_ref() + .unwrap() + .verify() + .unwrap(); + let selector_key = match &selector { + ProfileSelector::RuntimeDefault => "default".to_string(), + ProfileSelector::Builtin(name) => name.clone(), + ProfileSelector::Named(name) => name.clone(), + }; + let manifest = archive + .resolve_profile(&selector_key, root.path(), "embedded-test-worker") + .unwrap(); + assert_eq!(manifest.worker.name, "embedded-test-worker"); + } + } + + #[test] + fn embedded_archive_rejects_unknown_selectors() { + assert!( + default_embedded_config_bundle(&ProfileSelector::Builtin( + "builtin:missing".to_string() + )) + .is_err() + ); + assert!( + default_embedded_config_bundle(&ProfileSelector::Named("custom".to_string())).is_err() + ); + } + fn test_config_bundle() -> ConfigBundle { ConfigBundle { metadata: worker_runtime::config_bundle::ConfigBundleMetadata { diff --git a/resources/profiles/coder.dcdl b/resources/profiles/coder.dcdl index bdd296a0..6e6e241b 100644 --- a/resources/profiles/coder.dcdl +++ b/resources/profiles/coder.dcdl @@ -2,12 +2,11 @@ slug = "coder"; description = "Ticket implementation coder profile."; scope = "workspace_write"; -tool_enabled = { - bash = true; - read = true; - write = true; - edit = true; - grep = true; - glob = true; - memory = true; +feature = { + task = { enabled = true; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = false; }; + ticket = { enabled = false; access = "lifecycle"; }; + ticket_orchestration = { enabled = false; }; }; diff --git a/resources/profiles/companion.dcdl b/resources/profiles/companion.dcdl index 21951a82..e18736d9 100644 --- a/resources/profiles/companion.dcdl +++ b/resources/profiles/companion.dcdl @@ -2,14 +2,11 @@ slug = "companion"; description = "Workspace companion profile."; scope = "workspace_write"; -tool_enabled = { - bash = true; - read = true; - write = true; - edit = true; - grep = true; - glob = true; - web_search = true; - web_fetch = true; - memory = true; +feature = { + task = { enabled = true; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = true; }; + ticket = { enabled = false; access = "lifecycle"; }; + ticket_orchestration = { enabled = false; }; }; diff --git a/resources/profiles/default.dcdl b/resources/profiles/default.dcdl index dac27d15..4fb756e4 100644 --- a/resources/profiles/default.dcdl +++ b/resources/profiles/default.dcdl @@ -1,21 +1,38 @@ slug = "default"; -description = "General Yoi companion profile."; +description = "Default Yoi coding profile."; scope = "workspace_write"; model = { - scheme = "codex"; - model_id = "gpt-5"; - auth = { kind = "codex_oauth"; }; + ref = "codex-oauth/gpt-5.5"; }; -tool_enabled = { - bash = true; - read = true; - write = true; - edit = true; - grep = true; - glob = true; - web_search = true; - web_fetch = true; - memory = true; +session = { + record_event_trace = true; +}; + +engine = { + reasoning = "high"; +}; + +feature = { + task = { enabled = true; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = true; }; + ticket = { enabled = false; access = "lifecycle"; }; + ticket_orchestration = { enabled = false; }; +}; + +memory = { + extract_threshold = 50000; + consolidation_threshold_files = 5; + consolidation_threshold_bytes = 50000; +}; + +web = { + enabled = true; + search = { + provider = "brave"; + api_key_secret = "web/brave/default"; + }; }; diff --git a/resources/profiles/intake.dcdl b/resources/profiles/intake.dcdl index 59ef69e3..ae2c1d72 100644 --- a/resources/profiles/intake.dcdl +++ b/resources/profiles/intake.dcdl @@ -2,12 +2,11 @@ slug = "intake"; description = "Ticket intake profile."; scope = "workspace_write"; -tool_enabled = { - bash = true; - read = true; - write = true; - edit = true; - grep = true; - glob = true; - memory = true; +feature = { + task = { enabled = false; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = false; }; + ticket = { enabled = true; access = "lifecycle"; }; + ticket_orchestration = { enabled = false; }; }; diff --git a/resources/profiles/orchestrator.dcdl b/resources/profiles/orchestrator.dcdl index cc68e62f..0012818c 100644 --- a/resources/profiles/orchestrator.dcdl +++ b/resources/profiles/orchestrator.dcdl @@ -2,12 +2,11 @@ slug = "orchestrator"; description = "Ticket orchestrator profile."; scope = "workspace_write"; -tool_enabled = { - bash = true; - read = true; - write = true; - edit = true; - grep = true; - glob = true; - memory = true; +feature = { + task = { enabled = true; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = true; }; + ticket = { enabled = true; access = "lifecycle"; }; + ticket_orchestration = { enabled = true; }; }; diff --git a/resources/profiles/reviewer.dcdl b/resources/profiles/reviewer.dcdl index 8de04f17..91c41e5c 100644 --- a/resources/profiles/reviewer.dcdl +++ b/resources/profiles/reviewer.dcdl @@ -2,10 +2,11 @@ slug = "reviewer"; description = "Ticket review profile."; scope = "workspace_read"; -tool_enabled = { - bash = true; - read = true; - grep = true; - glob = true; - memory = true; +feature = { + task = { enabled = true; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = false; }; + ticket = { enabled = false; access = "lifecycle"; }; + ticket_orchestration = { enabled = false; }; };