fix: reject unknown profile archive selectors
This commit is contained in:
parent
a823d414d0
commit
a2833dadca
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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; };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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; };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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; };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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; };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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; };
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user