fix: restore standalone logical workspace scope

This commit is contained in:
2026-09-24 19:58:03 +09:00
parent 3cd535cc8a
commit 1649e1ad89
9 changed files with 257 additions and 33 deletions
+39 -3
View File
@@ -8,6 +8,7 @@ use crate::profile::ProfileError;
pub const BUILTIN_PROFILE_CATALOG_ID: &str = "builtin-profiles-v2";
pub const BUILTIN_DEFAULT_PROFILE: &str = "builtin:default";
pub const BUILTIN_STANDALONE_PROFILE: &str = "builtin:standalone";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuiltinProfileImport {
@@ -25,10 +26,15 @@ pub struct BuiltinProfileResource {
}
const BASE_PATH: &str = "profiles/base.dcdl";
const DEFAULT_PATH: &str = "profiles/default.dcdl";
const BASE_IMPORT: &[BuiltinProfileImport] = &[BuiltinProfileImport {
specifier: "./base.dcdl",
resolved_path: BASE_PATH,
}];
const DEFAULT_IMPORT: &[BuiltinProfileImport] = &[BuiltinProfileImport {
specifier: "./default.dcdl",
resolved_path: DEFAULT_PATH,
}];
const NO_IMPORTS: &[BuiltinProfileImport] = &[];
pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[
@@ -41,11 +47,18 @@ pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[
},
BuiltinProfileResource {
selector: Some(BUILTIN_DEFAULT_PROFILE),
path: "profiles/default.dcdl",
path: DEFAULT_PATH,
source: include_str!("../../../resources/profiles/default.dcdl"),
description: "Standalone Yoi coding profile.",
description: "Default Yoi coding profile.",
imports: BASE_IMPORT,
},
BuiltinProfileResource {
selector: Some(BUILTIN_STANDALONE_PROFILE),
path: "profiles/standalone.dcdl",
source: include_str!("../../../resources/profiles/standalone.dcdl"),
description: "Standalone Yoi coding profile.",
imports: DEFAULT_IMPORT,
},
BuiltinProfileResource {
selector: Some("builtin:coder"),
path: "profiles/coder.dcdl",
@@ -281,7 +294,11 @@ mod tests {
assert_eq!(catalog.entrypoints.len() + 1, catalog.sources.len());
assert_eq!(
catalog.entrypoints.get(BUILTIN_DEFAULT_PROFILE),
Some(&"profiles/default.dcdl".to_owned())
Some(&DEFAULT_PATH.to_owned())
);
assert_eq!(
catalog.entrypoints.get(BUILTIN_STANDALONE_PROFILE),
Some(&"profiles/standalone.dcdl".to_owned())
);
assert!(catalog.digest().starts_with("sha256:"));
}
@@ -300,12 +317,31 @@ mod tests {
assert_eq!(value["feature"]["manage_workdir"]["enabled"], false);
}
#[test]
fn standalone_profile_inherits_default_and_overrides_workspace_policy() {
let value = resolve_builtin_profile_artifact(BUILTIN_STANDALONE_PROFILE)
.expect("evaluate built-in standalone")
.expect("standalone exists");
assert_eq!(value["slug"], "standalone");
assert_eq!(value["model"]["ref"], "codex-oauth/gpt-5.6-sol");
assert_eq!(value["scope"]["intent"], "workspace_write");
assert_eq!(value["scope"]["symlink_policy"], "logical");
assert_eq!(value["delegation_scope"]["intent"], "workspace_write");
assert_eq!(value["delegation_scope"]["symlink_policy"], "logical");
assert_eq!(value["feature"]["sub_worker"]["enabled"], true);
assert_eq!(value["feature"]["memory"]["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/standalone.dcdl", "./default.dcdl").as_deref(),
Some(DEFAULT_PATH)
);
assert_eq!(
resolve_import_path("profiles/default.dcdl", "../outside.dcdl"),
None
+2 -2
View File
@@ -10,8 +10,8 @@ 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,
BUILTIN_STANDALONE_PROFILE, BuiltinProfileCatalogSnapshot, BuiltinProfileImport,
BuiltinProfileResource, builtin_profile_catalog_snapshot, builtin_profile_entrypoints,
};
pub use config::{
CompactionConfigPartial, EngineManifestConfig, FileUploadLimitsPartial,
+27 -12
View File
@@ -20,8 +20,8 @@ use crate::config::{
use crate::model::{AuthRef, ModelManifest};
use crate::{
EngineManifestConfig, McpConfig, McpStdioCwdPolicy, Permission, ResolveError, ScopeConfig,
ScopeRule, SkillsConfig, WebConfig, WorkerManifest, WorkerManifestConfig, WorkerMetaConfig,
paths,
ScopeRule, SkillsConfig, SymlinkPolicy, WebConfig, WorkerManifest, WorkerManifestConfig,
WorkerMetaConfig, paths,
};
const PROFILE_FORMAT_V1: &str = "yoi.profile.v1";
@@ -698,6 +698,8 @@ struct ProfileScopeTable {
intent: ProfileScopeIntent,
#[serde(default)]
deny_write: Vec<PathBuf>,
#[serde(default)]
symlink_policy: SymlinkPolicy,
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
@@ -938,10 +940,14 @@ fn profile_scope_intent_to_config(
default_intent: Option<ProfileScopeIntent>,
field: &'static str,
) -> Result<ScopeConfig, ProfileError> {
let (intent, deny_write) = match scope {
Some(ProfileScopeConfig::Table(table)) => (Some(table.intent), table.deny_write),
Some(ProfileScopeConfig::String(intent)) => (Some(intent), Vec::new()),
None => (default_intent, Vec::new()),
let (intent, deny_write, symlink_policy) = match scope {
Some(ProfileScopeConfig::Table(table)) => {
(Some(table.intent), table.deny_write, table.symlink_policy)
}
Some(ProfileScopeConfig::String(intent)) => {
(Some(intent), Vec::new(), SymlinkPolicy::default())
}
None => (default_intent, Vec::new(), SymlinkPolicy::default()),
};
let Some(intent) = intent else {
return Ok(ScopeConfig::default());
@@ -969,7 +975,7 @@ fn profile_scope_intent_to_config(
target: workspace_base.to_path_buf(),
permission,
recursive: true,
symlink_policy: Default::default(),
symlink_policy,
}],
deny,
})
@@ -1247,6 +1253,7 @@ mod tests {
for name in [
"default",
"standalone",
"intake",
"orchestrator",
"companion",
@@ -1386,12 +1393,12 @@ mod tests {
}
#[test]
fn builtin_default_resolves_as_a_standalone_local_capability_profile() {
fn builtin_standalone_resolves_as_a_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"),
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, "standalone"),
ProfileResolveOptions::with_worker_name("standalone-worker"),
ProfileExecutionTarget::Standalone,
)
@@ -1405,17 +1412,25 @@ mod tests {
path: None,
provenance: Some(provenance),
..
} if name == "default" && provenance.starts_with("profiles/default.dcdl#sha256:")
} if name == "standalone" && provenance.starts_with("profiles/standalone.dcdl#sha256:")
));
assert_eq!(
resolved.manifest.model.ref_.as_deref(),
Some("codex-oauth/gpt-5.6-sol")
);
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()
rule.permission == protocol::Permission::Write
&& rule.target == tmp.path()
&& rule.symlink_policy == protocol::SymlinkPolicy::Logical
}));
assert!(resolved.manifest.delegation_scope.allow.iter().any(|rule| {
rule.permission == protocol::Permission::Write && rule.target == tmp.path()
rule.permission == protocol::Permission::Write
&& rule.target == tmp.path()
&& rule.symlink_policy == protocol::SymlinkPolicy::Logical
}));
assert!(!resolved.manifest.feature.memory.profile.enabled);
assert!(!resolved.manifest.feature.ticket.enabled);