fix: reject unknown profile archive selectors

This commit is contained in:
Keisuke Hirata 2026-07-08 19:21:52 +09:00
parent a823d414d0
commit a2833dadca
No known key found for this signature in database
10 changed files with 189 additions and 81 deletions

View File

@ -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'

View File

@ -166,4 +166,28 @@ Validation performed:
- nix build .#yoi --no-link
---
<!-- event: implementation_report author: hare at: 2026-07-08T10:21:42Z -->
## 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
---

View File

@ -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();

View File

@ -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<ConfigBundle, String> {
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<ProfileSourceArchive, String> {
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<String, String> {
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 {

View File

@ -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; };
};

View File

@ -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; };
};

View File

@ -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";
};
};

View File

@ -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; };
};

View File

@ -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; };
};

View File

@ -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; };
};