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'
|
title: 'Migrate Profiles to Decodal ProfileSourceArchive for Runtime launch'
|
||||||
state: 'inprogress'
|
state: 'inprogress'
|
||||||
created_at: '2026-07-07T20:51:35Z'
|
created_at: '2026-07-07T20:51:35Z'
|
||||||
updated_at: '2026-07-08T09:57:38Z'
|
updated_at: '2026-07-08T10:21:42Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'workspace-panel'
|
queued_by: 'workspace-panel'
|
||||||
queued_at: '2026-07-08T09:11:22Z'
|
queued_at: '2026-07-08T09:11:22Z'
|
||||||
|
|
|
||||||
|
|
@ -166,4 +166,28 @@ Validation performed:
|
||||||
- nix build .#yoi --no-link
|
- 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
|
.manifest
|
||||||
.entrypoints
|
.entrypoints
|
||||||
.get(selector)
|
.get(selector)
|
||||||
.or_else(|| self.manifest.entrypoints.get("default"))
|
|
||||||
.ok_or_else(|| ProfileArchiveError::MissingEntrypoint(selector.to_string()))?;
|
.ok_or_else(|| ProfileArchiveError::MissingEntrypoint(selector.to_string()))?;
|
||||||
let mut engine = Engine::new(ArchiveSourceLoader { archive: self });
|
let mut engine = Engine::new(ArchiveSourceLoader { archive: self });
|
||||||
let source = self
|
let source = self
|
||||||
|
|
@ -534,6 +533,17 @@ mod tests {
|
||||||
assert_eq!(manifest.worker.name, "archive-worker");
|
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]
|
#[test]
|
||||||
fn archive_loader_rejects_undeclared_import() {
|
fn archive_loader_rejects_undeclared_import() {
|
||||||
let archive = sample_archive();
|
let archive = sample_archive();
|
||||||
|
|
|
||||||
|
|
@ -1339,13 +1339,18 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
|
||||||
.profile
|
.profile
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| embedded_profile_selector(&request.intent));
|
.unwrap_or_else(|| embedded_profile_selector(&request.intent));
|
||||||
let config_bundle = match self
|
let config_bundle = match default_embedded_config_bundle(&profile).and_then(|bundle| {
|
||||||
.runtime
|
self.runtime
|
||||||
.store_config_bundle(default_embedded_config_bundle(&profile))
|
.store_config_bundle(bundle)
|
||||||
{
|
.map_err(|err| err.to_string())
|
||||||
|
}) {
|
||||||
Ok(availability) => availability.reference,
|
Ok(availability) => availability.reference,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
diagnostics.push(embedded_runtime_diagnostic(&error));
|
diagnostics.push(diagnostic(
|
||||||
|
"embedded_profile_source_archive_invalid",
|
||||||
|
DiagnosticSeverity::Error,
|
||||||
|
error,
|
||||||
|
));
|
||||||
return WorkerSpawnResult {
|
return WorkerSpawnResult {
|
||||||
state: WorkerOperationState::Rejected,
|
state: WorkerOperationState::Rejected,
|
||||||
worker: None,
|
worker: None,
|
||||||
|
|
@ -2020,7 +2025,18 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||||
.profile
|
.profile
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| embedded_profile_selector(&request.intent));
|
.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)
|
let Some(config_bundle) = sync.availability.map(|availability| availability.reference)
|
||||||
else {
|
else {
|
||||||
return WorkerSpawnResult {
|
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!(
|
let id = format!(
|
||||||
"workspace-runtime-{}",
|
"workspace-runtime-{}",
|
||||||
embedded_profile_label(profile)
|
embedded_profile_label(profile)
|
||||||
.unwrap_or_else(|| "default".to_string())
|
.unwrap_or_else(|| "default".to_string())
|
||||||
.replace([':', '/', ' '], "-")
|
.replace([':', '/', ' '], "-")
|
||||||
);
|
);
|
||||||
ConfigBundle {
|
Ok(ConfigBundle {
|
||||||
metadata: ConfigBundleMetadata {
|
metadata: ConfigBundleMetadata {
|
||||||
id,
|
id,
|
||||||
digest: String::new(),
|
digest: String::new(),
|
||||||
|
|
@ -2373,13 +2389,16 @@ fn default_embedded_config_bundle(profile: &ProfileSelector) -> ConfigBundle {
|
||||||
label: embedded_profile_label(profile),
|
label: embedded_profile_label(profile),
|
||||||
}],
|
}],
|
||||||
declarations: Vec::new(),
|
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 = embedded_profile_label(profile).unwrap_or_else(|| "default".to_string());
|
||||||
|
let selected_path = embedded_profile_path(profile)?;
|
||||||
let mut entrypoints = BTreeMap::new();
|
let mut entrypoints = BTreeMap::new();
|
||||||
entrypoints.insert("default".to_string(), "profiles/default.dcdl".to_string());
|
entrypoints.insert("default".to_string(), "profiles/default.dcdl".to_string());
|
||||||
entrypoints.insert(
|
entrypoints.insert(
|
||||||
|
|
@ -2389,7 +2408,7 @@ fn default_profile_source_archive(profile: &ProfileSelector) -> ProfileSourceArc
|
||||||
for slug in ["companion", "intake", "orchestrator", "coder", "reviewer"] {
|
for slug in ["companion", "intake", "orchestrator", "coder", "reviewer"] {
|
||||||
entrypoints.insert(format!("builtin:{slug}"), format!("profiles/{slug}.dcdl"));
|
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();
|
let mut sources = BTreeMap::new();
|
||||||
sources.insert(
|
sources.insert(
|
||||||
|
|
@ -2423,22 +2442,22 @@ fn default_profile_source_archive(profile: &ProfileSelector) -> ProfileSourceArc
|
||||||
imports: BTreeMap::new(),
|
imports: BTreeMap::new(),
|
||||||
sources,
|
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 {
|
match profile {
|
||||||
ProfileSelector::RuntimeDefault => "profiles/default.dcdl".to_string(),
|
ProfileSelector::RuntimeDefault => Ok("profiles/default.dcdl".to_string()),
|
||||||
ProfileSelector::Builtin(name) | ProfileSelector::Named(name) => {
|
ProfileSelector::Builtin(name) => match name.strip_prefix("builtin:").unwrap_or(name) {
|
||||||
match name.strip_prefix("builtin:").unwrap_or(name) {
|
"default" => Ok("profiles/default.dcdl".to_string()),
|
||||||
"companion" => "profiles/companion.dcdl".to_string(),
|
"companion" => Ok("profiles/companion.dcdl".to_string()),
|
||||||
"intake" => "profiles/intake.dcdl".to_string(),
|
"intake" => Ok("profiles/intake.dcdl".to_string()),
|
||||||
"orchestrator" => "profiles/orchestrator.dcdl".to_string(),
|
"orchestrator" => Ok("profiles/orchestrator.dcdl".to_string()),
|
||||||
"coder" => "profiles/coder.dcdl".to_string(),
|
"coder" => Ok("profiles/coder.dcdl".to_string()),
|
||||||
"reviewer" => "profiles/reviewer.dcdl".to_string(),
|
"reviewer" => Ok("profiles/reviewer.dcdl".to_string()),
|
||||||
_ => "profiles/default.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::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
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 {
|
fn test_config_bundle() -> ConfigBundle {
|
||||||
ConfigBundle {
|
ConfigBundle {
|
||||||
metadata: worker_runtime::config_bundle::ConfigBundleMetadata {
|
metadata: worker_runtime::config_bundle::ConfigBundleMetadata {
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@ slug = "coder";
|
||||||
description = "Ticket implementation coder profile.";
|
description = "Ticket implementation coder profile.";
|
||||||
scope = "workspace_write";
|
scope = "workspace_write";
|
||||||
|
|
||||||
tool_enabled = {
|
feature = {
|
||||||
bash = true;
|
task = { enabled = true; };
|
||||||
read = true;
|
memory = { enabled = true; };
|
||||||
write = true;
|
web = { enabled = true; };
|
||||||
edit = true;
|
workers = { enabled = false; };
|
||||||
grep = true;
|
ticket = { enabled = false; access = "lifecycle"; };
|
||||||
glob = true;
|
ticket_orchestration = { enabled = false; };
|
||||||
memory = true;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,11 @@ slug = "companion";
|
||||||
description = "Workspace companion profile.";
|
description = "Workspace companion profile.";
|
||||||
scope = "workspace_write";
|
scope = "workspace_write";
|
||||||
|
|
||||||
tool_enabled = {
|
feature = {
|
||||||
bash = true;
|
task = { enabled = true; };
|
||||||
read = true;
|
memory = { enabled = true; };
|
||||||
write = true;
|
web = { enabled = true; };
|
||||||
edit = true;
|
workers = { enabled = true; };
|
||||||
grep = true;
|
ticket = { enabled = false; access = "lifecycle"; };
|
||||||
glob = true;
|
ticket_orchestration = { enabled = false; };
|
||||||
web_search = true;
|
|
||||||
web_fetch = true;
|
|
||||||
memory = true;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,38 @@
|
||||||
slug = "default";
|
slug = "default";
|
||||||
description = "General Yoi companion profile.";
|
description = "Default Yoi coding profile.";
|
||||||
scope = "workspace_write";
|
scope = "workspace_write";
|
||||||
|
|
||||||
model = {
|
model = {
|
||||||
scheme = "codex";
|
ref = "codex-oauth/gpt-5.5";
|
||||||
model_id = "gpt-5";
|
|
||||||
auth = { kind = "codex_oauth"; };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
tool_enabled = {
|
session = {
|
||||||
bash = true;
|
record_event_trace = true;
|
||||||
read = true;
|
};
|
||||||
write = true;
|
|
||||||
edit = true;
|
engine = {
|
||||||
grep = true;
|
reasoning = "high";
|
||||||
glob = true;
|
};
|
||||||
web_search = true;
|
|
||||||
web_fetch = true;
|
feature = {
|
||||||
memory = true;
|
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.";
|
description = "Ticket intake profile.";
|
||||||
scope = "workspace_write";
|
scope = "workspace_write";
|
||||||
|
|
||||||
tool_enabled = {
|
feature = {
|
||||||
bash = true;
|
task = { enabled = false; };
|
||||||
read = true;
|
memory = { enabled = true; };
|
||||||
write = true;
|
web = { enabled = true; };
|
||||||
edit = true;
|
workers = { enabled = false; };
|
||||||
grep = true;
|
ticket = { enabled = true; access = "lifecycle"; };
|
||||||
glob = true;
|
ticket_orchestration = { enabled = false; };
|
||||||
memory = true;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@ slug = "orchestrator";
|
||||||
description = "Ticket orchestrator profile.";
|
description = "Ticket orchestrator profile.";
|
||||||
scope = "workspace_write";
|
scope = "workspace_write";
|
||||||
|
|
||||||
tool_enabled = {
|
feature = {
|
||||||
bash = true;
|
task = { enabled = true; };
|
||||||
read = true;
|
memory = { enabled = true; };
|
||||||
write = true;
|
web = { enabled = true; };
|
||||||
edit = true;
|
workers = { enabled = true; };
|
||||||
grep = true;
|
ticket = { enabled = true; access = "lifecycle"; };
|
||||||
glob = true;
|
ticket_orchestration = { enabled = true; };
|
||||||
memory = true;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ slug = "reviewer";
|
||||||
description = "Ticket review profile.";
|
description = "Ticket review profile.";
|
||||||
scope = "workspace_read";
|
scope = "workspace_read";
|
||||||
|
|
||||||
tool_enabled = {
|
feature = {
|
||||||
bash = true;
|
task = { enabled = true; };
|
||||||
read = true;
|
memory = { enabled = true; };
|
||||||
grep = true;
|
web = { enabled = true; };
|
||||||
glob = true;
|
workers = { enabled = false; };
|
||||||
memory = true;
|
ticket = { enabled = false; access = "lifecycle"; };
|
||||||
|
ticket_orchestration = { enabled = false; };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user