fix: restore standalone logical workspace scope
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use manifest::{
|
||||
ProfileExecutionTarget, ProfileResolveOptions, ProfileResolver, ProfileSelector,
|
||||
ResolvedProfile,
|
||||
BUILTIN_DEFAULT_PROFILE, BUILTIN_STANDALONE_PROFILE, ProfileDiscovery, ProfileExecutionTarget,
|
||||
ProfileRegistry, ProfileRegistrySource, ProfileResolveOptions, ProfileResolver,
|
||||
ProfileSelector, ResolvedProfile, validate_profile_execution_target,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use worker::PromptCatalogSource;
|
||||
@@ -56,16 +57,23 @@ impl StandaloneLaunchConfig {
|
||||
return Err(StandaloneLaunchError::PathProfileUnsupported);
|
||||
}
|
||||
let cwd = canonical_directory(&self.cwd)?;
|
||||
let registry = ProfileDiscovery::user_settings()
|
||||
.discover()
|
||||
.map_err(|_| StandaloneLaunchError::ProfileResolutionFailed)?;
|
||||
let selector = standalone_profile_selector(&self.profile, ®istry)
|
||||
.map_err(|_| StandaloneLaunchError::ProfileResolutionFailed)?;
|
||||
let profile = ProfileResolver::new()
|
||||
.with_workspace_base(&cwd)
|
||||
.resolve_for_target(
|
||||
&self.profile,
|
||||
.resolve_from_registry(
|
||||
&selector,
|
||||
®istry,
|
||||
ProfileResolveOptions {
|
||||
worker_name: Some(self.worker_name),
|
||||
},
|
||||
ProfileExecutionTarget::Standalone,
|
||||
)
|
||||
.map_err(|_| StandaloneLaunchError::ProfileResolutionFailed)?;
|
||||
validate_profile_execution_target(&profile.manifest, ProfileExecutionTarget::Standalone)
|
||||
.map_err(|_| StandaloneLaunchError::ProfileResolutionFailed)?;
|
||||
|
||||
Ok(ResolvedStandaloneLaunch {
|
||||
cwd,
|
||||
@@ -76,6 +84,23 @@ impl StandaloneLaunchConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn standalone_profile_selector(
|
||||
requested: &ProfileSelector,
|
||||
registry: &ProfileRegistry,
|
||||
) -> Result<ProfileSelector, manifest::ProfileError> {
|
||||
if requested != &ProfileSelector::Default
|
||||
|| registry.default_entry()?.qualified_name() != BUILTIN_DEFAULT_PROFILE
|
||||
{
|
||||
return Ok(requested.clone());
|
||||
}
|
||||
Ok(ProfileSelector::source_named(
|
||||
ProfileRegistrySource::Builtin,
|
||||
BUILTIN_STANDALONE_PROFILE
|
||||
.strip_prefix("builtin:")
|
||||
.expect("built-in standalone selector must be source-qualified"),
|
||||
))
|
||||
}
|
||||
|
||||
fn canonical_directory(path: &Path) -> Result<PathBuf, StandaloneLaunchError> {
|
||||
let path = std::fs::canonicalize(path)
|
||||
.map_err(|_| StandaloneLaunchError::WorkingDirectoryUnavailable)?;
|
||||
@@ -84,3 +109,51 @@ fn canonical_directory(path: &Path) -> Result<PathBuf, StandaloneLaunchError> {
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn builtin_default_maps_to_standalone_only_for_standalone_launch() {
|
||||
let registry = ProfileDiscovery::with_sources(None, None)
|
||||
.discover()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
standalone_profile_selector(&ProfileSelector::Default, ®istry).unwrap(),
|
||||
ProfileSelector::source_named(ProfileRegistrySource::Builtin, "standalone")
|
||||
);
|
||||
let explicit_default =
|
||||
ProfileSelector::source_named(ProfileRegistrySource::Builtin, "default");
|
||||
assert_eq!(
|
||||
standalone_profile_selector(&explicit_default, ®istry).unwrap(),
|
||||
explicit_default
|
||||
);
|
||||
assert_eq!(
|
||||
registry.default_entry().unwrap().qualified_name(),
|
||||
BUILTIN_DEFAULT_PROFILE
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operator_default_remains_the_standalone_default() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let registry_path = temp.path().join("profiles.toml");
|
||||
std::fs::write(
|
||||
®istry_path,
|
||||
"default = 'operator'\n[profile]\noperator = 'operator.toml'\n",
|
||||
)
|
||||
.unwrap();
|
||||
let registry = ProfileDiscovery::with_sources(Some(registry_path), None)
|
||||
.discover()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
standalone_profile_selector(&ProfileSelector::Default, ®istry).unwrap(),
|
||||
ProfileSelector::Default
|
||||
);
|
||||
assert_eq!(
|
||||
registry.default_entry().unwrap().qualified_name(),
|
||||
"user:operator"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,7 +809,12 @@ permission = "write"
|
||||
let mut persisted = Vec::new();
|
||||
write_worker_record(&mut persisted, &decoded).unwrap();
|
||||
let persisted: serde_json::Value = serde_json::from_slice(&persisted).unwrap();
|
||||
assert_eq!(persisted["manifest"]["schema_version"], 2);
|
||||
let current_manifest =
|
||||
manifest::write_persisted_worker_manifest_snapshot(&decoded.manifest).unwrap();
|
||||
assert_eq!(
|
||||
persisted["manifest"]["schema_version"],
|
||||
current_manifest["schema_version"]
|
||||
);
|
||||
assert_eq!(
|
||||
persisted["manifest"]["manifest"]["feature"]["memory"]["profile"]["enabled"],
|
||||
false
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use manifest::ProfileDiscovery;
|
||||
use manifest::{BUILTIN_DEFAULT_PROFILE, BUILTIN_STANDALONE_PROFILE, ProfileDiscovery};
|
||||
use ratatui::layout::{Constraint, Direction, Layout};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
@@ -224,12 +224,20 @@ fn run_picker(
|
||||
}
|
||||
|
||||
fn profile_choices(registry: &manifest::ProfileRegistry) -> Vec<ProfileChoice> {
|
||||
let use_standalone_fallback = registry
|
||||
.default_entry()
|
||||
.is_ok_and(|entry| entry.qualified_name() == BUILTIN_DEFAULT_PROFILE);
|
||||
registry
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let selector = entry.qualified_name();
|
||||
let default_marker = if entry.is_default { " (default)" } else { "" };
|
||||
let is_default = if use_standalone_fallback {
|
||||
selector == BUILTIN_STANDALONE_PROFILE
|
||||
} else {
|
||||
entry.is_default
|
||||
};
|
||||
let default_marker = if is_default { " (default)" } else { "" };
|
||||
let mut label = format!("{selector}{default_marker}");
|
||||
if let Some(description) = &entry.description {
|
||||
label.push_str(" — ");
|
||||
@@ -238,7 +246,7 @@ fn profile_choices(registry: &manifest::ProfileRegistry) -> Vec<ProfileChoice> {
|
||||
ProfileChoice {
|
||||
selector,
|
||||
label,
|
||||
is_default: entry.is_default,
|
||||
is_default,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -370,8 +378,8 @@ mod tests {
|
||||
fn choices() -> Vec<ProfileChoice> {
|
||||
vec![
|
||||
ProfileChoice {
|
||||
selector: "builtin:default".to_owned(),
|
||||
label: "builtin:default (default) — Default".to_owned(),
|
||||
selector: "builtin:standalone".to_owned(),
|
||||
label: "builtin:standalone (default) — Standalone".to_owned(),
|
||||
is_default: true,
|
||||
},
|
||||
ProfileChoice {
|
||||
@@ -386,7 +394,7 @@ mod tests {
|
||||
fn default_form_preserves_old_spawn_layout_defaults() {
|
||||
let form = SpawnForm::new(None, "yoi".to_owned(), choices());
|
||||
assert_eq!(form.worker_name, "yoi");
|
||||
assert_eq!(form.selected_profile().selector, "builtin:default");
|
||||
assert_eq!(form.selected_profile().selector, "builtin:standalone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -398,7 +406,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(form.selected_profile().selector, "builtin:coder");
|
||||
form.apply_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
|
||||
assert_eq!(form.selected_profile().selector, "builtin:default");
|
||||
assert_eq!(form.selected_profile().selector, "builtin:standalone");
|
||||
form.apply_key(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
|
||||
assert_eq!(form.selected_profile().selector, "builtin:coder");
|
||||
}
|
||||
@@ -445,7 +453,7 @@ mod tests {
|
||||
|
||||
assert!(rendered.contains("spawn worker"));
|
||||
assert!(rendered.contains("name: yoi"));
|
||||
assert!(rendered.contains("profile: builtin:default (default) — Default"));
|
||||
assert!(rendered.contains("profile: builtin:standalone (default) — Standalone"));
|
||||
assert!(rendered.contains("enter spawn · left/right edit · esc cancel"));
|
||||
}
|
||||
|
||||
@@ -456,10 +464,27 @@ mod tests {
|
||||
.unwrap();
|
||||
let choices = profile_choices(®istry);
|
||||
let default = choices.iter().find(|choice| choice.is_default).unwrap();
|
||||
assert_eq!(default.selector, "builtin:default");
|
||||
assert_eq!(default.selector, "builtin:standalone");
|
||||
assert!(default.label.contains("(default)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operator_profile_default_overrides_standalone_fallback() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let registry_path = temp.path().join("profiles.toml");
|
||||
std::fs::write(
|
||||
®istry_path,
|
||||
"default = 'operator'\n[profile]\noperator = 'operator.toml'\n",
|
||||
)
|
||||
.unwrap();
|
||||
let registry = ProfileDiscovery::with_sources(Some(registry_path), None)
|
||||
.discover()
|
||||
.unwrap();
|
||||
let choices = profile_choices(®istry);
|
||||
let default = choices.iter().find(|choice| choice.is_default).unwrap();
|
||||
assert_eq!(default.selector, "user:operator");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_worker_name_comes_from_sanitised_directory_basename() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Standalone workspace scope defaults reject logical symlinks
|
||||
|
||||
## Symptom
|
||||
|
||||
A standalone-style Worker rooted at the repository cannot read the repository-local alias:
|
||||
|
||||
```text
|
||||
yoi.local -> /home/hare/.local/share/yoi/
|
||||
```
|
||||
|
||||
`Read`, `Glob`, and `Grep` reject paths through that alias because the resolved target is outside the repository scope, even though the alias itself is below the Worker root.
|
||||
|
||||
## Cause
|
||||
|
||||
The Workdir provider supports both symlink identities:
|
||||
|
||||
- `Resolved` matches the provider-resolved target and is the least-authority default.
|
||||
- `Logical` matches the path presented through the Workdir and intentionally permits an in-scope alias to reach its target.
|
||||
|
||||
Focused provider tests confirm both behaviors:
|
||||
|
||||
- an explicit logical policy can read, list, glob, and grep through an external directory alias;
|
||||
- the default resolved policy rejects a symlink escape.
|
||||
|
||||
Before correction, the standalone launch path did not select the implemented logical behavior. `StandaloneLaunchConfig::resolve` canonicalized the cwd and resolved `builtin:default`. That profile declared `scope = "workspace_write"` and `delegation_scope = "workspace_write"`. `profile_scope_intent_to_config` converted each workspace intent into a recursive root rule with:
|
||||
|
||||
```rust
|
||||
symlink_policy: Default::default()
|
||||
```
|
||||
|
||||
`SymlinkPolicy::default()` is `Resolved`. Consequently, a request for `yoi.local/...` is checked using `/home/hare/.local/share/yoi/...`, which is not below the repository root and is denied.
|
||||
|
||||
This behavior was introduced when selective Workdir symlink policies were added in commit `8a3e06bc`. Provider support for explicit logical rules was added, but reusable profile workspace intents and standalone launch wiring retained the new least-authority default. Reusable profiles also expose only `workspace_read`/`workspace_write` intents, so `builtin:default` cannot opt into logical identity through its profile declaration.
|
||||
|
||||
## Scope of the finding
|
||||
|
||||
This explains the `yoi.local` rejection. It does not explain a rejection of an ordinary path such as `crates` or `Cargo.toml`: those contain no symlink. An error such as:
|
||||
|
||||
```text
|
||||
Workdir path `crates` exceeds the provider attachment scope
|
||||
```
|
||||
|
||||
means the restored parent attachment scope does not cover the Workdir's own logical root. That is a separate restore/attachment authority defect and cannot be fixed by requesting `symlink_policy: logical` on a Reviewer child.
|
||||
|
||||
## Implemented correction
|
||||
|
||||
A dedicated `builtin:standalone` profile now imports `builtin:default` and overrides only the standalone-specific contract:
|
||||
|
||||
- model: `codex-oauth/gpt-5.6-sol`;
|
||||
- direct workspace scope: `workspace_write` with `Logical` symlink identity;
|
||||
- delegation scope: `workspace_write` with `Logical` symlink identity.
|
||||
|
||||
Profile scope tables now accept an explicit `symlink_policy`, while the string shorthand and all existing profiles retain the least-authority `Resolved` default. Standalone launch and its TUI picker select `builtin:standalone` only when the registry still has the built-in `builtin:default` fallback; an operator-configured user default retains precedence. The global registry default remains `builtin:default`, so omitted SubWorker profiles do not acquire standalone-specific identity or policy.
|
||||
|
||||
Regression coverage verifies the built-in resource inheritance, model selection, default registry selection, and the concrete direct/delegation scope policies.
|
||||
|
||||
Existing persisted Worker manifest snapshots require explicit consideration: restore reuses the saved resolved scope, so the new profile will not repair already-restored Workers until they are replaced or their persisted authority is migrated deliberately.
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./base.dcdl" // {
|
||||
slug = "default";
|
||||
description = "Standalone Yoi coding profile.";
|
||||
description = "Default Yoi coding profile.";
|
||||
scope = "workspace_write";
|
||||
delegation_scope = "workspace_write";
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import "./default.dcdl" // {
|
||||
slug = "standalone";
|
||||
description = "Standalone Yoi coding profile.";
|
||||
model = { ref = "codex-oauth/gpt-5.6-sol"; };
|
||||
scope = {
|
||||
intent = "workspace_write";
|
||||
symlink_policy = "logical";
|
||||
};
|
||||
delegation_scope = {
|
||||
intent = "workspace_write";
|
||||
symlink_policy = "logical";
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user