refactor: rename pod crate to worker

This commit is contained in:
2026-06-26 00:05:57 +09:00
parent 4c677640f4
commit 6c59fe927b
194 changed files with 6637 additions and 6146 deletions
+181 -175
View File
@@ -1,10 +1,10 @@
//! Partial-form of [`crate::PodManifest`] used as cascade layers.
//! Partial-form of [`crate::WorkerManifest`] used as cascade layers.
//!
//! `PodManifestConfig` mirrors `PodManifest` but every field is optional
//! `WorkerManifestConfig` mirrors `WorkerManifest` but every field is optional
//! so individual layers (builtin defaults, user manifest, project
//! manifest, programmatic overlay) can be partial. Layers are combined
//! via [`PodManifestConfig::merge`] and the final config is converted to
//! a validated [`PodManifest`] via `TryFrom`.
//! via [`WorkerManifestConfig::merge`] and the final config is converted to
//! a validated [`WorkerManifest`] via `TryFrom`.
use std::collections::{BTreeSet, HashMap};
use std::num::NonZeroU32;
@@ -17,19 +17,19 @@ use crate::defaults;
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
use crate::plugin::PluginConfig;
use crate::{
CompactionConfig, FeatureConfig, FeatureFlagConfig, FileUploadLimits, McpConfig, McpEnvValue,
McpStdioCwdPolicy, MemoryConfig, PodManifest, PodMeta, ScopeConfig, SessionConfig,
CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits,
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConfig, ScopeConfig, SessionConfig,
SkillsConfig, TicketFeatureAccessConfig, TicketFeatureConfig, ToolOutputLimits,
ToolPermissionConfig, ToolPermissionRule, WebConfig, WorkerManifest,
ToolPermissionConfig, ToolPermissionRule, WebConfig, WorkerManifest, WorkerMeta,
};
/// Partial-form Pod manifest. Every field is optional; one or more
/// instances merge via [`PodManifestConfig::merge`] before being
/// converted to a validated [`PodManifest`] via `TryFrom`.
/// Partial-form Worker manifest. Every field is optional; one or more
/// instances merge via [`WorkerManifestConfig::merge`] before being
/// converted to a validated [`WorkerManifest`] via `TryFrom`.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PodManifestConfig {
pub struct WorkerManifestConfig {
#[serde(default)]
pub pod: PodMetaConfig,
pub worker: WorkerMetaConfig,
/// `[model]` セクションは partial でも完成形でも同じ
/// [`ModelManifest`] を使う。ref / inline の両形を受け入れるための
/// 全 Optional 構造なので、カスケード層と最終マニフェストで型を
@@ -37,7 +37,7 @@ pub struct PodManifestConfig {
#[serde(default)]
pub model: ModelManifest,
#[serde(default)]
pub worker: WorkerManifestConfig,
pub engine: EngineManifestConfig,
#[serde(default)]
pub scope: ScopeConfig,
/// Scope that may be subdelegated to spawned child Pods. Defaults empty.
@@ -83,7 +83,7 @@ pub struct FeatureConfigPartial {
#[serde(default)]
pub web: Option<FeatureFlagConfigPartial>,
#[serde(default)]
pub pods: Option<FeatureFlagConfigPartial>,
pub workers: Option<FeatureFlagConfigPartial>,
#[serde(default)]
pub ticket: Option<TicketFeatureConfigPartial>,
#[serde(default)]
@@ -98,7 +98,7 @@ impl FeatureConfigPartial {
task: merge_option(self.task, other.task, FeatureFlagConfigPartial::merge),
memory: merge_option(self.memory, other.memory, FeatureFlagConfigPartial::merge),
web: merge_option(self.web, other.web, FeatureFlagConfigPartial::merge),
pods: merge_option(self.pods, other.pods, FeatureFlagConfigPartial::merge),
workers: merge_option(self.workers, other.workers, FeatureFlagConfigPartial::merge),
ticket: merge_option(self.ticket, other.ticket, TicketFeatureConfigPartial::merge),
ticket_orchestration: merge_option(
self.ticket_orchestration,
@@ -150,7 +150,10 @@ impl From<FeatureConfigPartial> for FeatureConfig {
.map(FeatureFlagConfig::from)
.unwrap_or_default(),
web: value.web.map(FeatureFlagConfig::from).unwrap_or_default(),
pods: value.pods.map(FeatureFlagConfig::from).unwrap_or_default(),
workers: value
.workers
.map(FeatureFlagConfig::from)
.unwrap_or_default(),
ticket: value
.ticket
.map(TicketFeatureConfig::from)
@@ -207,7 +210,7 @@ impl From<FeatureConfig> for FeatureConfigPartial {
task: Some(value.task.into()),
memory: Some(value.memory.into()),
web: Some(value.web.into()),
pods: Some(value.pods.into()),
workers: Some(value.workers.into()),
ticket: Some(value.ticket.into()),
ticket_orchestration: Some(value.ticket_orchestration.into()),
plugins: Some(value.plugins.into()),
@@ -216,18 +219,18 @@ impl From<FeatureConfig> for FeatureConfigPartial {
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PodMetaConfig {
pub struct WorkerMetaConfig {
#[serde(default)]
pub name: Option<String>,
/// Optional `PromptCatalog` manifest pack override. See
/// [`crate::PodMeta::prompt_pack`] for semantics. Relative paths
/// are resolved through [`PodManifestConfig::resolve_paths`].
/// [`crate::WorkerMeta::prompt_pack`] for semantics. Relative paths
/// are resolved through [`WorkerManifestConfig::resolve_paths`].
#[serde(default)]
pub prompt_pack: Option<PathBuf>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WorkerManifestConfig {
pub struct EngineManifestConfig {
#[serde(default)]
pub instruction: Option<String>,
#[serde(default)]
@@ -318,8 +321,8 @@ pub struct CompactionConfigPartial {
pub model: Option<ModelManifest>,
}
/// Errors raised when converting a [`PodManifestConfig`] to a validated
/// [`PodManifest`] via `TryFrom`.
/// Errors raised when converting a [`WorkerManifestConfig`] to a validated
/// [`WorkerManifest`] via `TryFrom`.
#[derive(Debug, thiserror::Error)]
pub enum ResolveError {
#[error("missing required field: {0}")]
@@ -359,7 +362,7 @@ pub(crate) fn reject_removed_manifest_fields(s: &str) -> Result<(), toml::de::Er
Ok(())
}
impl PodManifestConfig {
impl WorkerManifestConfig {
/// Parse a partial manifest from a TOML string. Unknown top-level or
/// nested fields emit a `tracing::warn!` and are ignored; use
/// `tracing_subscriber` with `WARN` enabled to surface them to the
@@ -378,12 +381,12 @@ impl PodManifestConfig {
/// from this layer so every per-field default lives at exactly one
/// call site (the `defaults` module).
///
/// `TryFrom<PodManifestConfig>` also reads the same constants as a
/// `TryFrom<WorkerManifestConfig>` also reads the same constants as a
/// belt-and-suspenders fallback, so a manually-constructed config
/// that skips this layer still resolves to the same values.
pub fn builtin_defaults() -> Self {
Self {
worker: WorkerManifestConfig {
engine: EngineManifestConfig {
tool_output: ToolOutputLimitsPartial {
default_max_bytes: Some(defaults::TOOL_OUTPUT_MAX_BYTES),
per_tool: HashMap::new(),
@@ -415,7 +418,7 @@ impl PodManifestConfig {
base.display()
);
resolve_auth_file(&mut self.model.auth, base);
if let Some(ref mut pack) = self.pod.prompt_pack {
if let Some(ref mut pack) = self.worker.prompt_pack {
*pack = join_if_relative(base, pack);
}
for rule in &mut self.scope.allow {
@@ -457,11 +460,11 @@ impl PodManifestConfig {
/// fields from `self`. Map entries merge key-wise with `upper`
/// winning on conflict. Scope rules from both layers accumulate
/// (see [`ScopeConfig`] semantics).
pub fn merge(self, upper: PodManifestConfig) -> Self {
pub fn merge(self, upper: WorkerManifestConfig) -> Self {
Self {
pod: self.pod.merge(upper.pod),
model: self.model.merge(upper.model),
worker: self.worker.merge(upper.worker),
model: self.model.merge(upper.model),
engine: self.engine.merge(upper.engine),
scope: merge_scope(self.scope, upper.scope),
delegation_scope: merge_scope(self.delegation_scope, upper.delegation_scope),
session: merge_option(self.session, upper.session, SessionConfigPartial::merge),
@@ -575,7 +578,7 @@ impl MemoryConfig {
}
}
impl PodMetaConfig {
impl WorkerMetaConfig {
fn merge(self, upper: Self) -> Self {
Self {
name: upper.name.or(self.name),
@@ -584,7 +587,7 @@ impl PodMetaConfig {
}
}
impl WorkerManifestConfig {
impl EngineManifestConfig {
fn merge(self, upper: Self) -> Self {
Self {
instruction: upper.instruction.or(self.instruction),
@@ -696,9 +699,9 @@ fn join_if_relative(base: &Path, p: &Path) -> PathBuf {
}
}
/// Invariant check: every path in a fully-resolved [`PodManifestConfig`]
/// Invariant check: every path in a fully-resolved [`WorkerManifestConfig`]
/// must be absolute. Relative paths are resolved per-layer via
/// [`PodManifestConfig::resolve_paths`]; if one reaches `TryFrom` it
/// [`WorkerManifestConfig::resolve_paths`]; if one reaches `TryFrom` it
/// indicates a caller skipped the per-layer resolve step.
fn ensure_absolute(field: &'static str, path: &Path) -> Result<(), ResolveError> {
if path.is_absolute() {
@@ -871,45 +874,48 @@ fn bounded_label(value: &str) -> String {
out
}
impl TryFrom<PodManifestConfig> for PodManifest {
impl TryFrom<WorkerManifestConfig> for WorkerManifest {
type Error = ResolveError;
fn try_from(cfg: PodManifestConfig) -> Result<Self, Self::Error> {
let name = cfg.pod.name.ok_or(ResolveError::MissingField("pod.name"))?;
let prompt_pack = cfg.pod.prompt_pack;
fn try_from(cfg: WorkerManifestConfig) -> Result<Self, Self::Error> {
let name = cfg
.worker
.name
.ok_or(ResolveError::MissingField("worker.name"))?;
let prompt_pack = cfg.worker.prompt_pack;
if let Some(ref p) = prompt_pack {
ensure_absolute("pod.prompt_pack", p)?;
ensure_absolute("worker.prompt_pack", p)?;
}
validate_model_paths(&cfg.model, "model.auth.file")?;
let worker = WorkerManifest {
let engine = EngineManifest {
instruction: cfg
.worker
.engine
.instruction
.unwrap_or_else(|| defaults::DEFAULT_INSTRUCTION.to_string()),
language: cfg
.worker
.engine
.language
.unwrap_or_else(|| defaults::WORKER_LANGUAGE.to_string()),
max_tokens: cfg.worker.max_tokens,
max_turns: cfg.worker.max_turns,
temperature: cfg.worker.temperature,
top_p: cfg.worker.top_p,
top_k: cfg.worker.top_k,
stop_sequences: cfg.worker.stop_sequences.unwrap_or_default(),
reasoning: cfg.worker.reasoning,
max_tokens: cfg.engine.max_tokens,
max_turns: cfg.engine.max_turns,
temperature: cfg.engine.temperature,
top_p: cfg.engine.top_p,
top_k: cfg.engine.top_k,
stop_sequences: cfg.engine.stop_sequences.unwrap_or_default(),
reasoning: cfg.engine.reasoning,
tool_output: ToolOutputLimits {
default_max_bytes: cfg
.worker
.engine
.tool_output
.default_max_bytes
.unwrap_or(defaults::TOOL_OUTPUT_MAX_BYTES),
per_tool: cfg.worker.tool_output.per_tool,
per_tool: cfg.engine.tool_output.per_tool,
},
file_upload: FileUploadLimits {
max_bytes: cfg
.worker
.engine
.file_upload
.max_bytes
.unwrap_or(defaults::FILE_UPLOAD_MAX_BYTES),
@@ -1007,10 +1013,10 @@ impl TryFrom<PodManifestConfig> for PodManifest {
validate_mcp_config(&cfg.mcp)?;
Ok(PodManifest {
pod: PodMeta { name, prompt_pack },
Ok(WorkerManifest {
worker: WorkerMeta { name, prompt_pack },
model: cfg.model,
worker,
engine,
scope: cfg.scope,
delegation_scope: cfg.delegation_scope,
session,
@@ -1041,9 +1047,9 @@ mod tests {
AuthRef::ApiKey { file: Some(path) }
}
fn minimal_valid() -> PodManifestConfig {
PodManifestConfig {
pod: PodMetaConfig {
fn minimal_valid() -> WorkerManifestConfig {
WorkerManifestConfig {
worker: WorkerMetaConfig {
name: Some("test".into()),
prompt_pack: None,
},
@@ -1052,10 +1058,10 @@ mod tests {
model_id: Some("claude-sonnet-4-20250514".into()),
..Default::default()
},
worker: WorkerManifestConfig::default(),
engine: EngineManifestConfig::default(),
scope: ScopeConfig {
allow: vec![ScopeRule {
target: abs("/pod"),
target: abs("/worker"),
permission: Permission::Write,
recursive: true,
}],
@@ -1076,8 +1082,8 @@ mod tests {
#[test]
fn resolve_minimal_succeeds() {
let manifest: PodManifest = minimal_valid().try_into().unwrap();
assert_eq!(manifest.pod.name, "test");
let manifest: WorkerManifest = minimal_valid().try_into().unwrap();
assert_eq!(manifest.worker.name, "test");
assert_eq!(manifest.model.scheme, Some(SchemeKind::Anthropic));
assert!(manifest.permissions.is_none());
}
@@ -1113,7 +1119,7 @@ mod tests {
},
});
let manifest: PodManifest = cfg.try_into().unwrap();
let manifest: WorkerManifest = cfg.try_into().unwrap();
assert_eq!(manifest.mcp.stdio_servers.len(), 1);
let server = &manifest.mcp.stdio_servers[0];
@@ -1137,7 +1143,7 @@ mod tests {
env: crate::McpEnvConfig::default(),
});
let err = PodManifest::try_from(cfg).unwrap_err();
let err = WorkerManifest::try_from(cfg).unwrap_err();
assert!(matches!(
err,
ResolveError::InvalidMcpConfig {
@@ -1157,7 +1163,7 @@ mod tests {
});
}
let err = PodManifest::try_from(cfg).unwrap_err();
let err = WorkerManifest::try_from(cfg).unwrap_err();
assert!(matches!(
err,
ResolveError::InvalidMcpConfig {
@@ -1186,7 +1192,7 @@ mod tests {
},
});
let err = PodManifest::try_from(cfg).unwrap_err();
let err = WorkerManifest::try_from(cfg).unwrap_err();
let rendered = err.to_string();
assert!(rendered.contains("secret_ref"));
assert!(!rendered.contains("bad secret id with spaces"));
@@ -1208,7 +1214,7 @@ mod tests {
env: crate::McpEnvConfig::default(),
});
let manifest: PodManifest = cfg.try_into().unwrap();
let manifest: WorkerManifest = cfg.try_into().unwrap();
assert_eq!(
manifest.mcp.stdio_servers[0].command,
"definitely-not-a-command-yoi-must-spawn"
@@ -1222,7 +1228,7 @@ mod tests {
record_event_trace: Some(true),
});
let manifest: PodManifest = cfg.try_into().unwrap();
let manifest: WorkerManifest = cfg.try_into().unwrap();
assert!(manifest.session.record_event_trace);
}
@@ -1234,7 +1240,7 @@ mod tests {
rules: Vec::new(),
});
let err = PodManifest::try_from(cfg).unwrap_err();
let err = WorkerManifest::try_from(cfg).unwrap_err();
assert!(matches!(
err,
@@ -1261,7 +1267,7 @@ mod tests {
],
});
let manifest: PodManifest = cfg.try_into().unwrap();
let manifest: WorkerManifest = cfg.try_into().unwrap();
let permissions = manifest.permissions.unwrap();
assert_eq!(permissions.default_action, crate::ToolPermissionAction::Ask);
@@ -1318,7 +1324,7 @@ mod tests {
fn try_from_invariant_rejects_lingering_relative_auth_file() {
let mut cfg = minimal_valid();
cfg.model.auth = Some(api_key_file_auth(PathBuf::from("keys/relative")));
let err = PodManifest::try_from(cfg).unwrap_err();
let err = WorkerManifest::try_from(cfg).unwrap_err();
assert!(matches!(
err,
ResolveError::RelativePath {
@@ -1332,7 +1338,7 @@ mod tests {
fn try_from_invariant_rejects_lingering_relative_scope_target() {
let mut cfg = minimal_valid();
cfg.scope.allow[0].target = PathBuf::from("docs");
let err = PodManifest::try_from(cfg).unwrap_err();
let err = WorkerManifest::try_from(cfg).unwrap_err();
assert!(matches!(
err,
ResolveError::RelativePath {
@@ -1343,25 +1349,25 @@ mod tests {
}
#[test]
fn resolve_rejects_missing_pod_name() {
fn resolve_rejects_missing_worker_name() {
let mut cfg = minimal_valid();
cfg.pod.name = None;
let err = PodManifest::try_from(cfg).unwrap_err();
assert!(matches!(err, ResolveError::MissingField("pod.name")));
cfg.worker.name = None;
let err = WorkerManifest::try_from(cfg).unwrap_err();
assert!(matches!(err, ResolveError::MissingField("worker.name")));
}
#[test]
fn resolve_accepts_empty_scope_for_profile_launch_policy() {
let mut cfg = minimal_valid();
cfg.scope.allow.clear();
let manifest = PodManifest::try_from(cfg).unwrap();
let manifest = WorkerManifest::try_from(cfg).unwrap();
assert!(manifest.scope.allow.is_empty());
}
#[test]
fn merge_scalar_upper_wins() {
let lower = PodManifestConfig {
pod: PodMetaConfig {
let lower = WorkerManifestConfig {
worker: WorkerMetaConfig {
name: Some("lower".into()),
prompt_pack: None,
},
@@ -1371,30 +1377,30 @@ mod tests {
},
..Default::default()
};
let upper = PodManifestConfig {
pod: PodMetaConfig {
let upper = WorkerManifestConfig {
worker: WorkerMetaConfig {
name: Some("upper".into()),
prompt_pack: None,
},
..Default::default()
};
let merged = lower.merge(upper);
assert_eq!(merged.pod.name.as_deref(), Some("upper"));
assert_eq!(merged.worker.name.as_deref(), Some("upper"));
// model_id not present in upper — retain lower
assert_eq!(merged.model.model_id.as_deref(), Some("lower-model"));
}
#[test]
fn merge_worker_reasoning_upper_wins() {
let lower = PodManifestConfig {
worker: WorkerManifestConfig {
let lower = WorkerManifestConfig {
engine: EngineManifestConfig {
reasoning: Some(ReasoningControl::Effort(ReasoningEffort::Low)),
..Default::default()
},
..Default::default()
};
let upper = PodManifestConfig {
worker: WorkerManifestConfig {
let upper = WorkerManifestConfig {
engine: EngineManifestConfig {
reasoning: Some(ReasoningControl::BudgetTokens(4096)),
..Default::default()
},
@@ -1404,15 +1410,15 @@ mod tests {
let merged = lower.merge(upper);
assert_eq!(
merged.worker.reasoning,
merged.engine.reasoning,
Some(ReasoningControl::BudgetTokens(4096))
);
}
#[test]
fn merge_worker_generation_settings_upper_wins() {
let lower = PodManifestConfig {
worker: WorkerManifestConfig {
let lower = WorkerManifestConfig {
engine: EngineManifestConfig {
top_p: Some(0.8),
top_k: Some(20),
stop_sequences: Some(vec!["lower".into()]),
@@ -1420,8 +1426,8 @@ mod tests {
},
..Default::default()
};
let upper = PodManifestConfig {
worker: WorkerManifestConfig {
let upper = WorkerManifestConfig {
engine: EngineManifestConfig {
top_p: Some(0.9),
stop_sequences: Some(vec!["upper".into()]),
..Default::default()
@@ -1431,14 +1437,14 @@ mod tests {
let merged = lower.merge(upper);
assert_eq!(merged.worker.top_p, Some(0.9));
assert_eq!(merged.worker.top_k, Some(20));
assert_eq!(merged.worker.stop_sequences, Some(vec!["upper".into()]));
assert_eq!(merged.engine.top_p, Some(0.9));
assert_eq!(merged.engine.top_k, Some(20));
assert_eq!(merged.engine.stop_sequences, Some(vec!["upper".into()]));
}
#[test]
fn merge_scope_accumulates_allow_and_deny() {
let lower = PodManifestConfig {
let lower = WorkerManifestConfig {
scope: ScopeConfig {
allow: vec![ScopeRule {
target: abs("/a"),
@@ -1449,7 +1455,7 @@ mod tests {
},
..Default::default()
};
let upper = PodManifestConfig {
let upper = WorkerManifestConfig {
scope: ScopeConfig {
allow: vec![ScopeRule {
target: abs("/b"),
@@ -1471,7 +1477,7 @@ mod tests {
#[test]
fn merge_permissions_accumulates_rules_and_upper_default_wins() {
let lower = PodManifestConfig {
let lower = WorkerManifestConfig {
permissions: Some(PermissionConfigPartial {
default_action: Some(crate::ToolPermissionAction::Allow),
rules: vec![ToolPermissionRule {
@@ -1482,7 +1488,7 @@ mod tests {
}),
..Default::default()
};
let upper = PodManifestConfig {
let upper = WorkerManifestConfig {
permissions: Some(PermissionConfigPartial {
default_action: Some(crate::ToolPermissionAction::Deny),
rules: vec![ToolPermissionRule {
@@ -1507,8 +1513,8 @@ mod tests {
#[test]
fn merge_tool_output_per_tool_keywise() {
let lower = PodManifestConfig {
worker: WorkerManifestConfig {
let lower = WorkerManifestConfig {
engine: EngineManifestConfig {
tool_output: ToolOutputLimitsPartial {
default_max_bytes: Some(8192),
per_tool: [("Read".to_string(), 1024)].into_iter().collect(),
@@ -1517,8 +1523,8 @@ mod tests {
},
..Default::default()
};
let upper = PodManifestConfig {
worker: WorkerManifestConfig {
let upper = WorkerManifestConfig {
engine: EngineManifestConfig {
tool_output: ToolOutputLimitsPartial {
default_max_bytes: None,
per_tool: [("Read".to_string(), 2048), ("Grep".to_string(), 512)]
@@ -1530,7 +1536,7 @@ mod tests {
..Default::default()
};
let merged = lower.merge(upper);
let to = &merged.worker.tool_output;
let to = &merged.engine.tool_output;
assert_eq!(to.default_max_bytes, Some(8192));
assert_eq!(to.per_tool.get("Read"), Some(&2048));
assert_eq!(to.per_tool.get("Grep"), Some(&512));
@@ -1538,8 +1544,8 @@ mod tests {
#[test]
fn merge_file_upload_max_bytes_upper_wins() {
let lower = PodManifestConfig {
worker: WorkerManifestConfig {
let lower = WorkerManifestConfig {
engine: EngineManifestConfig {
file_upload: FileUploadLimitsPartial {
max_bytes: Some(8192),
},
@@ -1547,8 +1553,8 @@ mod tests {
},
..Default::default()
};
let upper = PodManifestConfig {
worker: WorkerManifestConfig {
let upper = WorkerManifestConfig {
engine: EngineManifestConfig {
file_upload: FileUploadLimitsPartial {
max_bytes: Some(54_321),
},
@@ -1557,11 +1563,11 @@ mod tests {
..Default::default()
};
let merged = lower.merge(upper);
assert_eq!(merged.worker.file_upload.max_bytes, Some(54_321));
assert_eq!(merged.engine.file_upload.max_bytes, Some(54_321));
}
#[test]
fn merge_option_struct_field_wise() {
let lower = PodManifestConfig {
let lower = WorkerManifestConfig {
compaction: Some(CompactionConfigPartial {
threshold: Some(50_000),
prune_protected_tokens: Some(5_000),
@@ -1577,7 +1583,7 @@ mod tests {
}),
..Default::default()
};
let upper = PodManifestConfig {
let upper = WorkerManifestConfig {
compaction: Some(CompactionConfigPartial {
threshold: Some(80_000),
..Default::default()
@@ -1604,32 +1610,32 @@ mod tests {
#[test]
fn from_toml_type_mismatch_is_hard_error() {
let bad = r#"
[pod]
[worker]
name = "x"
[worker]
[engine]
max_tokens = "not-a-number"
"#;
assert!(PodManifestConfig::from_toml(bad).is_err());
assert!(WorkerManifestConfig::from_toml(bad).is_err());
}
#[test]
fn from_toml_accepts_unknown_field() {
// Unknown keys are warn-and-ignored, not hard errors.
// `pod.pwd` specifically is silently dropped after the
// `worker.pwd` specifically is silently dropped after the
// path-resolution ticket — keep it in the fixture to exercise
// that code path.
let ok = r#"
[pod]
[worker]
name = "x"
pwd = "/obsolete"
[worker]
[engine]
max_tokens = 1000
unknown_future_field = "tolerated"
"#;
let cfg = PodManifestConfig::from_toml(ok).unwrap();
assert_eq!(cfg.worker.max_tokens, Some(1000));
let cfg = WorkerManifestConfig::from_toml(ok).unwrap();
assert_eq!(cfg.engine.max_tokens, Some(1000));
}
#[test]
@@ -1638,7 +1644,7 @@ unknown_future_field = "tolerated"
[compaction]
prune_protected_turns = 3
"#;
let err = PodManifestConfig::from_toml(bad).unwrap_err();
let err = WorkerManifestConfig::from_toml(bad).unwrap_err();
assert!(
err.to_string().contains("compaction.prune_protected_turns"),
"unexpected error: {err}"
@@ -1651,7 +1657,7 @@ prune_protected_turns = 3
[memory]
extract_worker_max_input_tokens = 30000
"#;
let err = PodManifestConfig::from_toml(bad).unwrap_err();
let err = WorkerManifestConfig::from_toml(bad).unwrap_err();
assert!(
err.to_string()
.contains("memory.extract_worker_max_input_tokens"),
@@ -1661,7 +1667,7 @@ extract_worker_max_input_tokens = 30000
#[test]
fn from_toml_accepts_extract_worker_max_turns() {
let cfg = PodManifestConfig::from_toml(
let cfg = WorkerManifestConfig::from_toml(
r#"
[memory]
extract_worker_max_turns = 2
@@ -1673,36 +1679,36 @@ extract_worker_max_turns = 2
#[test]
fn from_toml_accepts_worker_reasoning_string_or_integer() {
let effort = PodManifestConfig::from_toml(
let effort = WorkerManifestConfig::from_toml(
r#"
[worker]
[engine]
reasoning = "xhigh"
"#,
)
.unwrap();
assert_eq!(
effort.worker.reasoning,
effort.engine.reasoning,
Some(ReasoningControl::Effort(ReasoningEffort::XHigh))
);
let budget = PodManifestConfig::from_toml(
let budget = WorkerManifestConfig::from_toml(
r#"
[worker]
[engine]
reasoning = -1
"#,
)
.unwrap();
assert_eq!(
budget.worker.reasoning,
budget.engine.reasoning,
Some(ReasoningControl::BudgetTokens(-1))
);
}
#[test]
fn from_toml_accepts_worker_generation_settings() {
let cfg = PodManifestConfig::from_toml(
let cfg = WorkerManifestConfig::from_toml(
r#"
[worker]
[engine]
top_p = 0.9
top_k = 40
stop_sequences = ["\n\n", "</stop>"]
@@ -1710,17 +1716,17 @@ stop_sequences = ["\n\n", "</stop>"]
)
.unwrap();
assert_eq!(cfg.worker.top_p, Some(0.9));
assert_eq!(cfg.worker.top_k, Some(40));
assert_eq!(cfg.engine.top_p, Some(0.9));
assert_eq!(cfg.engine.top_k, Some(40));
assert_eq!(
cfg.worker.stop_sequences,
cfg.engine.stop_sequences,
Some(vec!["\n\n".into(), "</stop>".into()])
);
}
#[test]
fn from_toml_accepts_worker_max_turns() {
let cfg = PodManifestConfig::from_toml(
let cfg = WorkerManifestConfig::from_toml(
r#"
[compaction]
worker_max_turns = 7
@@ -1736,7 +1742,7 @@ worker_max_turns = 7
let mut cfg = minimal_valid();
cfg.compaction = Some(CompactionConfigPartial::default());
let manifest = PodManifest::try_from(cfg).unwrap();
let manifest = WorkerManifest::try_from(cfg).unwrap();
assert_eq!(
manifest.compaction.unwrap().worker_max_turns,
@@ -1746,18 +1752,18 @@ worker_max_turns = 7
#[test]
fn feature_flags_default_disabled_in_resolved_manifest() {
let manifest: PodManifest = minimal_valid().try_into().unwrap();
let manifest: WorkerManifest = minimal_valid().try_into().unwrap();
assert!(!manifest.feature.task.enabled);
assert!(!manifest.feature.memory.enabled);
assert!(!manifest.feature.web.enabled);
assert!(!manifest.feature.pods.enabled);
assert!(!manifest.feature.workers.enabled);
assert!(!manifest.feature.ticket.enabled);
assert!(!manifest.feature.ticket_orchestration.enabled);
}
#[test]
fn from_toml_parses_explicit_feature_flags() {
let cfg = PodManifestConfig::from_toml(
let cfg = WorkerManifestConfig::from_toml(
r#"
[feature.task]
enabled = true
@@ -1771,10 +1777,10 @@ enabled = true
"#,
)
.unwrap();
let manifest: PodManifest = PodManifestConfig::builtin_defaults()
let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults()
.merge(cfg)
.merge(PodManifestConfig {
pod: PodMetaConfig {
.merge(WorkerManifestConfig {
worker: WorkerMetaConfig {
name: Some("feature-test".into()),
prompt_pack: None,
},
@@ -1785,7 +1791,7 @@ enabled = true
},
scope: ScopeConfig {
allow: vec![ScopeRule {
target: abs("/pod"),
target: abs("/worker"),
permission: Permission::Read,
recursive: true,
}],
@@ -1807,7 +1813,7 @@ enabled = true
#[test]
fn feature_flags_merge_as_partial_profile_layers() {
let base = PodManifestConfig::from_toml(
let base = WorkerManifestConfig::from_toml(
r#"
[feature.memory]
enabled = true
@@ -1818,7 +1824,7 @@ access = "read_only"
"#,
)
.unwrap();
let upper = PodManifestConfig::from_toml(
let upper = WorkerManifestConfig::from_toml(
r#"
[feature.ticket]
access = "lifecycle"
@@ -1828,11 +1834,11 @@ enabled = true
"#,
)
.unwrap();
let manifest: PodManifest = PodManifestConfig::builtin_defaults()
let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults()
.merge(base)
.merge(upper)
.merge(PodManifestConfig {
pod: PodMetaConfig {
.merge(WorkerManifestConfig {
worker: WorkerMetaConfig {
name: Some("feature-merge-test".into()),
prompt_pack: None,
},
@@ -1843,7 +1849,7 @@ enabled = true
},
scope: ScopeConfig {
allow: vec![ScopeRule {
target: abs("/pod"),
target: abs("/worker"),
permission: Permission::Read,
recursive: true,
}],
@@ -1860,7 +1866,7 @@ enabled = true
TicketFeatureAccessConfig::Lifecycle
);
assert!(manifest.feature.web.enabled);
assert!(!manifest.feature.pods.enabled);
assert!(!manifest.feature.workers.enabled);
}
#[test]
@@ -1871,20 +1877,20 @@ enabled = true
target = "/abs/project"
permission = "write"
"#;
let cfg = PodManifestConfig::from_toml(toml).unwrap();
assert!(cfg.pod.name.is_none());
let cfg = WorkerManifestConfig::from_toml(toml).unwrap();
assert!(cfg.worker.name.is_none());
assert_eq!(cfg.scope.allow.len(), 1);
}
#[test]
fn builtin_defaults_populates_worker_limit_defaults() {
let cfg = PodManifestConfig::builtin_defaults();
let cfg = WorkerManifestConfig::builtin_defaults();
assert_eq!(
cfg.worker.tool_output.default_max_bytes,
cfg.engine.tool_output.default_max_bytes,
Some(defaults::TOOL_OUTPUT_MAX_BYTES)
);
assert_eq!(
cfg.worker.file_upload.max_bytes,
cfg.engine.file_upload.max_bytes,
Some(defaults::FILE_UPLOAD_MAX_BYTES)
);
}
@@ -1892,10 +1898,10 @@ permission = "write"
#[test]
fn builtin_defaults_merged_into_minimal_resolves_with_defaults() {
// Starting from builtin_defaults and overlaying only the
// required fields must resolve to a PodManifest carrying the
// required fields must resolve to a WorkerManifest carrying the
// centralised default values.
let overlay = PodManifestConfig {
pod: PodMetaConfig {
let overlay = WorkerManifestConfig {
worker: WorkerMetaConfig {
name: Some("x".into()),
prompt_pack: None,
},
@@ -1906,7 +1912,7 @@ permission = "write"
},
scope: ScopeConfig {
allow: vec![ScopeRule {
target: abs("/pod"),
target: abs("/worker"),
permission: Permission::Write,
recursive: true,
}],
@@ -1914,22 +1920,22 @@ permission = "write"
},
..Default::default()
};
let merged = PodManifestConfig::builtin_defaults().merge(overlay);
let manifest: PodManifest = merged.try_into().unwrap();
let merged = WorkerManifestConfig::builtin_defaults().merge(overlay);
let manifest: WorkerManifest = merged.try_into().unwrap();
assert_eq!(
manifest.worker.tool_output.default_max_bytes,
manifest.engine.tool_output.default_max_bytes,
defaults::TOOL_OUTPUT_MAX_BYTES
);
assert_eq!(
manifest.worker.file_upload.max_bytes,
manifest.engine.file_upload.max_bytes,
defaults::FILE_UPLOAD_MAX_BYTES
);
}
#[test]
fn end_to_end_cascade() {
let builtin = PodManifestConfig::default();
let user = PodManifestConfig::from_toml(
let builtin = WorkerManifestConfig::default();
let user = WorkerManifestConfig::from_toml(
r#"
[model]
scheme = "anthropic"
@@ -1937,7 +1943,7 @@ model_id = "claude-sonnet-4-20250514"
"#,
)
.unwrap();
let project = PodManifestConfig::from_toml(
let project = WorkerManifestConfig::from_toml(
r#"
[[scope.allow]]
target = "/abs/project"
@@ -1945,17 +1951,17 @@ permission = "write"
"#,
)
.unwrap();
let overlay = PodManifestConfig::from_toml(
let overlay = WorkerManifestConfig::from_toml(
r#"
[pod]
[worker]
name = "dbg"
"#,
)
.unwrap();
let merged = builtin.merge(user).merge(project).merge(overlay);
let manifest: PodManifest = merged.try_into().unwrap();
assert_eq!(manifest.pod.name, "dbg");
let manifest: WorkerManifest = merged.try_into().unwrap();
assert_eq!(manifest.worker.name, "dbg");
assert_eq!(manifest.model.scheme, Some(SchemeKind::Anthropic));
assert_eq!(manifest.scope.allow.len(), 1);
}
@@ -1981,7 +1987,7 @@ name = "dbg"
cfg.skills = Some(SkillsConfig {
directories: vec![PathBuf::from("relative/skills")],
});
let err = PodManifest::try_from(cfg).unwrap_err();
let err = WorkerManifest::try_from(cfg).unwrap_err();
assert!(matches!(
err,
ResolveError::RelativePath {
@@ -1993,13 +1999,13 @@ name = "dbg"
#[test]
fn skills_merge_extends_directories() {
let lower = PodManifestConfig {
let lower = WorkerManifestConfig {
skills: Some(SkillsConfig {
directories: vec![PathBuf::from("/a")],
}),
..Default::default()
};
let upper = PodManifestConfig {
let upper = WorkerManifestConfig {
skills: Some(SkillsConfig {
directories: vec![PathBuf::from("/b")],
}),
@@ -2013,13 +2019,13 @@ name = "dbg"
#[test]
fn from_toml_parses_skills_section() {
let toml = r#"
[pod]
[worker]
name = "x"
[skills]
directories = [".claude/skills", ".cursor/skills"]
"#;
let cfg = PodManifestConfig::from_toml(toml).unwrap();
let cfg = WorkerManifestConfig::from_toml(toml).unwrap();
let dirs = cfg.skills.unwrap().directories;
assert_eq!(
dirs,
@@ -2032,14 +2038,14 @@ directories = [".claude/skills", ".cursor/skills"]
#[test]
fn merge_preserves_ref() {
let lower = PodManifestConfig {
let lower = WorkerManifestConfig {
model: ModelManifest {
ref_: Some("anthropic/claude-sonnet-4-6".into()),
..Default::default()
},
..Default::default()
};
let upper = PodManifestConfig {
let upper = WorkerManifestConfig {
model: ModelManifest {
// only override auth
auth: Some(AuthRef::None),
+2 -2
View File
@@ -1,7 +1,7 @@
//! Single source of truth for manifest default values.
//!
//! Every default that would otherwise be duplicated between serde
//! `#[serde(default = "...")]` attributes (on [`crate::PodManifest`])
//! `#[serde(default = "...")]` attributes (on [`crate::WorkerManifest`])
//! and the cascade resolution in [`crate::config`] lives here as a
//! `pub const`. Both paths read from this module, so changing a
//! default requires editing exactly one line.
@@ -48,7 +48,7 @@ pub const COMPACT_OVERVIEW_DEADLINE_TOKENS: u64 = 40_000;
pub const DEFAULT_INSTRUCTION: &str = "$yoi/default";
/// Default language policy used by the main worker for normal prose
/// responses. See [`crate::WorkerManifest::language`].
/// responses. See [`crate::EngineManifest::language`].
pub const WORKER_LANGUAGE: &str =
"match the user's language unless they explicitly request another language";
+93 -93
View File
@@ -7,9 +7,9 @@ mod profile;
mod scope;
pub use config::{
CompactionConfigPartial, FileUploadLimitsPartial, PermissionConfigPartial, PodManifestConfig,
PodMetaConfig, ResolveError, SessionConfigPartial, ToolOutputLimitsPartial,
WorkerManifestConfig,
CompactionConfigPartial, EngineManifestConfig, FileUploadLimitsPartial,
PermissionConfigPartial, ResolveError, SessionConfigPartial, ToolOutputLimitsPartial,
WorkerManifestConfig, WorkerMetaConfig,
};
pub use model::{
AuthRef, ModelCapability, ModelManifest, ReasoningControl, ReasoningEffort, SchemeKind,
@@ -32,20 +32,20 @@ use std::path::PathBuf;
use serde::de::Error as _;
use serde::{Deserialize, Serialize};
/// Declarative configuration for a Pod.
/// Declarative configuration for a Worker.
///
/// Parsed from a TOML manifest file. Describes the model, system prompt,
/// and directory scope (required). The Pod's working directory is **not**
/// and directory scope (required). The Worker's working directory is **not**
/// part of the manifest — it is the process's `std::env::current_dir()`
/// at construction time.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PodManifest {
pub pod: PodMeta,
pub struct WorkerManifest {
pub worker: WorkerMeta,
pub model: ModelManifest,
pub worker: WorkerManifest,
/// Direct filesystem authority for this Pod's own tools.
pub engine: EngineManifest,
/// Direct filesystem authority for this Worker's own tools.
pub scope: ScopeConfig,
/// Filesystem authority this Pod may pass to spawned children. Missing
/// Filesystem authority this Worker may pass to spawned children. Missing
/// metadata/config defaults to no delegation authority.
#[serde(default)]
pub delegation_scope: ScopeConfig,
@@ -91,14 +91,14 @@ pub struct PodManifest {
#[serde(default)]
pub skills: Option<SkillsConfig>,
/// Optional profile provenance for manifests produced by profile resolution.
/// Stored only after profile resolution so Pod restore can prefer the
/// Stored only after profile resolution so Worker restore can prefer the
/// validated snapshot over current profile files or one-file Manifest input.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<profile::ProfileManifestSnapshot>,
}
/// Explicit built-in feature/tool-surface enablement. These flags are
/// profile/config data only: they do not carry runtime Pod names, sockets,
/// profile/config data only: they do not carry runtime Worker names, sockets,
/// sessions, secrets, or resolved host state. Tool registration still applies
/// the normal scope, host-authority, backend, memory, and network checks.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -110,7 +110,7 @@ pub struct FeatureConfig {
#[serde(default)]
pub web: FeatureFlagConfig,
#[serde(default)]
pub pods: FeatureFlagConfig,
pub workers: FeatureFlagConfig,
#[serde(default)]
pub ticket: TicketFeatureConfig,
#[serde(default)]
@@ -125,7 +125,7 @@ impl Default for FeatureConfig {
task: FeatureFlagConfig::disabled(),
memory: FeatureFlagConfig::disabled(),
web: FeatureFlagConfig::disabled(),
pods: FeatureFlagConfig::disabled(),
workers: FeatureFlagConfig::disabled(),
ticket: TicketFeatureConfig::default(),
ticket_orchestration: FeatureFlagConfig::disabled(),
plugins: FeatureFlagConfig::disabled(),
@@ -197,7 +197,7 @@ pub struct SkillsConfig {
/// Skills *roots*. Children of each root must be individual
/// `<name>/SKILL.md` bundles; the directory itself is not a skill.
/// Resolved against the manifest base directory before
/// [`PodManifest`] is materialised.
/// [`WorkerManifest`] is materialised.
#[serde(default)]
pub directories: Vec<PathBuf>,
}
@@ -206,7 +206,7 @@ pub struct SkillsConfig {
///
/// The manifest layer records local stdio MCP server declarations but never
/// starts them. Future lifecycle code must opt in to spawning and must keep MCP
/// process authority separate from Plugin permissions and `pod::feature` flags.
/// process authority separate from Plugin permissions and `worker::feature` flags.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct McpConfig {
@@ -363,7 +363,7 @@ pub struct WebFetchConfig {
/// Memory subsystem configuration. Presence in the manifest enables
/// memory; `workspace_root` pins the memory workspace explicitly. When it
/// is absent, memory resolution searches upward from the Pod's pwd for a
/// is absent, memory resolution searches upward from the Worker's pwd for a
/// `.yoi/memory` marker rather than treating `.yoi` project records alone
/// as a memory root.
///
@@ -396,7 +396,7 @@ pub struct MemoryConfig {
#[serde(default)]
pub language: Option<String>,
/// Optional model for the extract worker. When `None`,
/// the main pod model is cloned via `clone_boxed()`. Lightweight
/// the main engine model is cloned via `clone_boxed()`. Lightweight
/// reasoning-capable models (Haiku / 4o-mini / Flash class) are
/// recommended.
#[serde(default)]
@@ -414,7 +414,7 @@ pub struct MemoryConfig {
#[serde(default)]
pub extract_worker_max_turns: Option<u32>,
/// Optional model for the consolidation worker. When
/// `None`, the main pod model is cloned via `clone_boxed()`.
/// `None`, the main engine model is cloned via `clone_boxed()`.
/// Reasoning-class models are recommended.
#[serde(default)]
pub consolidation_model: Option<ModelManifest>,
@@ -432,12 +432,12 @@ pub struct MemoryConfig {
pub consolidation_threshold_bytes: Option<u64>,
}
/// Pod metadata.
/// Worker metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PodMeta {
pub struct WorkerMeta {
pub name: String,
/// Optional path to a TOML override file read as the top layer of
/// `pod::PromptCatalog`. Subject to the same relative-path
/// `worker::PromptCatalog`. Subject to the same relative-path
/// resolution as other manifest paths (joined against the
/// manifest's base directory). `None` leaves the 4th overlay layer
/// empty; auto-discovered user and workspace packs still apply.
@@ -453,7 +453,7 @@ pub struct PodMeta {
/// Worker-level configuration embedded in the manifest.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerManifest {
pub struct EngineManifest {
/// Reference to the instruction prompt asset used as the body of
/// the worker's system prompt. Uses the `PromptLoader` prefix
/// addressing scheme (`$yoi/...`, `$user/...`,
@@ -572,7 +572,7 @@ impl ToolOutputLimits {
/// Declarative scope configuration.
///
/// A Pod may only touch paths whose effective permission (computed from
/// A Worker may only touch paths whose effective permission (computed from
/// allow/deny rules below) is at least `Read` / `Write`. See
/// [`Scope`] for the resolved runtime form.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -653,7 +653,7 @@ pub struct CompactionConfig {
/// Safety-net (between-requests) compaction threshold.
///
/// Checked by `PodInterceptor::pre_llm_request` inside a turn. When
/// Checked by `WorkerInterceptor::pre_llm_request` inside a turn. When
/// current occupancy exceeds this value, the run yields so that the
/// Controller can compact before the next LLM request. `None`
/// disables the between-requests check.
@@ -802,7 +802,7 @@ impl Default for CompactionConfig {
}
}
impl PodManifest {
impl WorkerManifest {
/// Parse a manifest from a TOML string.
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
config::reject_removed_manifest_fields(s)?;
@@ -818,14 +818,14 @@ mod tests {
use super::*;
const MINIMAL_REQUIRED: &str = r#"
[pod]
[worker]
name = "test-agent"
[model]
scheme = "anthropic"
model_id = "claude-sonnet-4-20250514"
[worker]
[engine]
[[scope.allow]]
target = "/abs/scope"
@@ -834,8 +834,8 @@ permission = "write"
#[test]
fn parse_minimal_manifest() {
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
assert_eq!(manifest.pod.name, "test-agent");
let manifest = WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap();
assert_eq!(manifest.worker.name, "test-agent");
assert_eq!(manifest.model.scheme, Some(SchemeKind::Anthropic));
assert_eq!(
manifest.model.model_id.as_deref(),
@@ -846,19 +846,19 @@ permission = "write"
assert!(manifest.scope.deny.is_empty());
assert!(manifest.delegation_scope.allow.is_empty());
assert!(manifest.delegation_scope.deny.is_empty());
assert_eq!(manifest.worker.instruction, defaults::DEFAULT_INSTRUCTION);
assert!(manifest.worker.top_p.is_none());
assert!(manifest.worker.top_k.is_none());
assert!(manifest.worker.stop_sequences.is_empty());
assert_eq!(manifest.engine.instruction, defaults::DEFAULT_INSTRUCTION);
assert!(manifest.engine.top_p.is_none());
assert!(manifest.engine.top_k.is_none());
assert!(manifest.engine.stop_sequences.is_empty());
assert!(manifest.web.is_none());
}
#[test]
fn deserialize_old_manifest_snapshot_defaults_to_no_delegation() {
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
let manifest = WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap();
let mut snapshot = serde_json::to_value(&manifest).unwrap();
snapshot.as_object_mut().unwrap().remove("delegation_scope");
let restored: PodManifest = serde_json::from_value(snapshot).unwrap();
let restored: WorkerManifest = serde_json::from_value(snapshot).unwrap();
assert_eq!(restored.scope.allow.len(), 1);
assert!(restored.delegation_scope.allow.is_empty());
assert!(restored.delegation_scope.deny.is_empty());
@@ -870,7 +870,7 @@ permission = "write"
"{}\n[web]\nenabled = true\n\n[web.search]\nprovider = \"brave\"\napi_key_secret = \"web/brave/default\"\ntimeout_secs = 12\n\n[web.fetch]\ntimeout_secs = 7\nredirect_limit = 3\nmax_response_bytes = 12345\nmax_output_bytes = 2048\n",
MINIMAL_REQUIRED
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let web = manifest.web.unwrap();
assert_eq!(web.enabled, Some(true));
let search = web.search.unwrap();
@@ -886,7 +886,7 @@ permission = "write"
#[test]
fn parse_full_manifest() {
let toml = r#"
[pod]
[worker]
name = "code-reviewer"
[model]
@@ -894,7 +894,7 @@ scheme = "anthropic"
model_id = "claude-sonnet-4-20250514"
auth = { kind = "api_key", file = "/abs/keys/anthropic" }
[worker]
[engine]
instruction = "$user/reviewer"
max_tokens = 4096
temperature = 0.3
@@ -924,21 +924,21 @@ permission = "write"
target = "/abs/project/tasks/private"
permission = "write"
"#;
let manifest = PodManifest::from_toml(toml).unwrap();
assert_eq!(manifest.pod.name, "code-reviewer");
let manifest = WorkerManifest::from_toml(toml).unwrap();
assert_eq!(manifest.worker.name, "code-reviewer");
let file = match manifest.model.auth.as_ref() {
Some(AuthRef::ApiKey { file, .. }) => file.as_deref(),
_ => panic!("expected ApiKey"),
};
assert_eq!(file, Some(std::path::Path::new("/abs/keys/anthropic")));
assert_eq!(manifest.worker.instruction, "$user/reviewer");
assert_eq!(manifest.worker.max_tokens, Some(4096));
assert_eq!(manifest.worker.temperature, Some(0.3));
assert_eq!(manifest.worker.top_p, Some(0.9));
assert_eq!(manifest.worker.top_k, Some(40));
assert_eq!(manifest.worker.stop_sequences, vec!["\n\n", "</stop>"]);
assert_eq!(manifest.engine.instruction, "$user/reviewer");
assert_eq!(manifest.engine.max_tokens, Some(4096));
assert_eq!(manifest.engine.temperature, Some(0.3));
assert_eq!(manifest.engine.top_p, Some(0.9));
assert_eq!(manifest.engine.top_k, Some(40));
assert_eq!(manifest.engine.stop_sequences, vec!["\n\n", "</stop>"]);
assert_eq!(
manifest.worker.reasoning,
manifest.engine.reasoning,
Some(ReasoningControl::Effort(ReasoningEffort::Medium))
);
let allow = &manifest.scope.allow;
@@ -960,16 +960,16 @@ permission = "write"
#[test]
fn reject_missing_scope() {
let toml = r#"
[pod]
[worker]
name = "missing-scope"
[model]
scheme = "anthropic"
model_id = "claude-sonnet-4-20250514"
[worker]
[engine]
"#;
assert!(PodManifest::from_toml(toml).is_err());
assert!(WorkerManifest::from_toml(toml).is_err());
}
#[test]
@@ -984,7 +984,7 @@ model_id = "claude-sonnet-4-20250514"
[plugins.enabled.config]\n\
greeting = \"hello\"\n"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
assert_eq!(manifest.plugins.enabled.len(), 1);
let enabled = &manifest.plugins.enabled[0];
assert_eq!(enabled.id, "project:example");
@@ -1005,37 +1005,37 @@ model_id = "claude-sonnet-4-20250514"
#[test]
fn parse_max_turns() {
let toml = MINIMAL_REQUIRED.replace("[worker]\n", "[worker]\nmax_turns = 50\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
assert_eq!(manifest.worker.max_turns.unwrap().get(), 50);
let toml = MINIMAL_REQUIRED.replace("[engine]\n", "[engine]\nmax_turns = 50\n");
let manifest = WorkerManifest::from_toml(&toml).unwrap();
assert_eq!(manifest.engine.max_turns.unwrap().get(), 50);
}
#[test]
fn parse_reasoning_budget() {
let toml = MINIMAL_REQUIRED.replace("[worker]\n", "[worker]\nreasoning = -1\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
let toml = MINIMAL_REQUIRED.replace("[engine]\n", "[engine]\nreasoning = -1\n");
let manifest = WorkerManifest::from_toml(&toml).unwrap();
assert_eq!(
manifest.worker.reasoning,
manifest.engine.reasoning,
Some(ReasoningControl::BudgetTokens(-1))
);
}
#[test]
fn omitted_max_turns_is_none() {
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
assert!(manifest.worker.max_turns.is_none());
let manifest = WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap();
assert!(manifest.engine.max_turns.is_none());
}
#[test]
fn reject_max_turns_zero() {
let toml = MINIMAL_REQUIRED.replace("[worker]\n", "[worker]\nmax_turns = 0\n");
assert!(PodManifest::from_toml(&toml).is_err());
let toml = MINIMAL_REQUIRED.replace("[engine]\n", "[engine]\nmax_turns = 0\n");
assert!(WorkerManifest::from_toml(&toml).is_err());
}
#[test]
fn parse_compaction_config() {
let toml = format!("{MINIMAL_REQUIRED}\n[compaction]\nthreshold = 80000\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let c = manifest.compaction.unwrap();
assert_eq!(c.prune_protected_tokens, 8000);
assert_eq!(c.prune_min_savings, 4096);
@@ -1048,7 +1048,7 @@ model_id = "claude-sonnet-4-20250514"
#[test]
fn reject_removed_prune_protected_turns_field() {
let toml = format!("{MINIMAL_REQUIRED}\n[compaction]\nprune_protected_turns = 3\n");
let err = PodManifest::from_toml(&toml).unwrap_err();
let err = WorkerManifest::from_toml(&toml).unwrap_err();
assert!(
err.to_string().contains("compaction.prune_protected_turns"),
"unexpected error: {err}"
@@ -1062,7 +1062,7 @@ model_id = "claude-sonnet-4-20250514"
[compaction]\n\
worker_max_turns = 7\n"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let c = manifest.compaction.unwrap();
assert_eq!(c.worker_max_turns, Some(7));
}
@@ -1075,7 +1075,7 @@ model_id = "claude-sonnet-4-20250514"
threshold = 80000\n\
request_threshold = 90000\n"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let c = manifest.compaction.unwrap();
assert_eq!(c.threshold, Some(80000));
assert_eq!(c.request_threshold, Some(90000));
@@ -1088,7 +1088,7 @@ model_id = "claude-sonnet-4-20250514"
[compaction]\n\
request_threshold = 90000\n"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let c = manifest.compaction.unwrap();
assert_eq!(c.threshold, None);
assert_eq!(c.request_threshold, Some(90000));
@@ -1104,7 +1104,7 @@ model_id = "claude-sonnet-4-20250514"
scheme = \"gemini\"\n\
model_id = \"gemini-2.0-flash\"\n"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let c = manifest.compaction.unwrap();
let p = c.model.unwrap();
assert_eq!(p.scheme, Some(SchemeKind::Gemini));
@@ -1113,20 +1113,20 @@ model_id = "claude-sonnet-4-20250514"
#[test]
fn omitted_compaction_is_none() {
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
let manifest = WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap();
assert!(manifest.compaction.is_none());
}
#[test]
fn omitted_memory_is_none() {
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
let manifest = WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap();
assert!(manifest.memory.is_none());
}
#[test]
fn empty_memory_section_enables_with_default_root() {
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let mem = manifest.memory.expect("memory section parsed");
assert!(mem.workspace_root.is_none());
assert_eq!(mem.inject_summary, None);
@@ -1135,7 +1135,7 @@ model_id = "claude-sonnet-4-20250514"
#[test]
fn memory_section_with_inject_summary_false() {
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\ninject_summary = false\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let mem = manifest.memory.unwrap();
assert_eq!(mem.inject_summary, Some(false));
}
@@ -1143,7 +1143,7 @@ model_id = "claude-sonnet-4-20250514"
#[test]
fn memory_section_with_explicit_root() {
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\nworkspace_root = \"/some/where\"\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let mem = manifest.memory.unwrap();
assert_eq!(
mem.workspace_root.unwrap(),
@@ -1154,7 +1154,7 @@ model_id = "claude-sonnet-4-20250514"
#[test]
fn memory_section_with_language() {
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\nlanguage = \"Japanese\"\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let mem = manifest.memory.unwrap();
assert_eq!(mem.language.as_deref(), Some("Japanese"));
}
@@ -1163,62 +1163,62 @@ model_id = "claude-sonnet-4-20250514"
fn reject_unknown_scheme() {
let toml =
MINIMAL_REQUIRED.replace("scheme = \"anthropic\"", "scheme = \"unknown_scheme\"");
assert!(PodManifest::from_toml(&toml).is_err());
assert!(WorkerManifest::from_toml(&toml).is_err());
}
#[test]
fn omitted_limits_fall_back_to_defaults() {
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
let limits = &manifest.worker.tool_output;
let manifest = WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap();
let limits = &manifest.engine.tool_output;
assert_eq!(limits.default_max_bytes, defaults::TOOL_OUTPUT_MAX_BYTES);
assert!(limits.per_tool.is_empty());
assert_eq!(
manifest.worker.file_upload.max_bytes,
manifest.engine.file_upload.max_bytes,
defaults::FILE_UPLOAD_MAX_BYTES
);
}
#[test]
fn worker_language_defaults_and_parses() {
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
assert_eq!(manifest.worker.language, defaults::WORKER_LANGUAGE);
let manifest = WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap();
assert_eq!(manifest.engine.language, defaults::WORKER_LANGUAGE);
let toml = MINIMAL_REQUIRED.replace("[worker]\n", "[worker]\nlanguage = \"Japanese\"\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
assert_eq!(manifest.worker.language, "Japanese");
let toml = MINIMAL_REQUIRED.replace("[engine]\n", "[engine]\nlanguage = \"Japanese\"\n");
let manifest = WorkerManifest::from_toml(&toml).unwrap();
assert_eq!(manifest.engine.language, "Japanese");
}
#[test]
fn parse_worker_output_limits() {
let toml = MINIMAL_REQUIRED.replace(
"[worker]\n",
"[worker]\n\
"[engine]\n",
"[engine]\n\
[worker.tool_output]\n\
default_max_bytes = 8192\n\n\
[worker.tool_output.per_tool]\n\
Read = 32768\n\
Grep = 4096\n\n\
[worker.file_upload]\n\
[engine.file_upload]\n\
max_bytes = 12345\n",
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let limits = &manifest.worker.tool_output;
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let limits = &manifest.engine.tool_output;
assert_eq!(limits.default_max_bytes, 8192);
assert_eq!(limits.limit_for("Read"), 32768);
assert_eq!(limits.limit_for("Grep"), 4096);
assert_eq!(limits.limit_for("Unknown"), 8192);
assert_eq!(manifest.worker.file_upload.max_bytes, 12345);
assert_eq!(manifest.engine.file_upload.max_bytes, 12345);
}
#[test]
fn empty_tool_output_section_uses_default_max_bytes() {
let toml = MINIMAL_REQUIRED.replace(
"[worker]\n",
"[worker]\n\
"[engine]\n",
"[engine]\n\
[worker.tool_output]\n",
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let limits = &manifest.worker.tool_output;
let manifest = WorkerManifest::from_toml(&toml).unwrap();
let limits = &manifest.engine.tool_output;
assert_eq!(limits.default_max_bytes, defaults::TOOL_OUTPUT_MAX_BYTES);
assert!(limits.per_tool.is_empty());
}
+2 -2
View File
@@ -1,6 +1,6 @@
//! LLM モデル宣言型
//!
//! Pod マニフェストの `[model]` セクションで記述する型。`ref`(プロバイダ
//! Worker マニフェストの `[model]` セクションで記述する型。`ref`(プロバイダ
//! とモデルを両方指し示す短縮形)と inline 指定(`scheme` / `model_id`
//! 直書き)の両方を受け入れるため、すべてのフィールドを `Option` として
//! 持つ 1 つの型 [`ModelManifest`] に統合している。実解決(ref をプロバイダ
@@ -18,7 +18,7 @@ use serde::{Deserialize, Serialize};
// マニフェストで任意に override できるよう型だけ再エクスポートする。
pub use llm_engine::llm_client::capability::{ModelCapability, ReasoningControl, ReasoningEffort};
/// Pod マニフェストの `[model]` セクション。
/// Worker マニフェストの `[model]` セクション。
///
/// - ref だけ書く: `[model] ref = "anthropic/claude-sonnet-4-6"`
/// - ref + 一部 override: ref で基底を引き、`auth` 等だけ書き換え
+20 -20
View File
@@ -6,7 +6,7 @@
//! `providers.toml`, `models.toml`, `prompts/`, `prompts.toml` 等
//! - **`data_dir`** — プログラムが書く永続データ。`sessions/` 等
//! - **`runtime_dir`** — 再起動で消えてよいランタイム状態。socket,
//! `pods.json`, `pid` ファイル等
//! `workers.json`, `pid` ファイル等
//!
//! ## 解決順 (優先順位高 → 低)
//!
@@ -46,7 +46,7 @@ pub fn data_dir() -> Option<PathBuf> {
)
}
/// ランタイムディレクトリ。socket, `pods.json`, Pod ごとの `pid` /
/// ランタイムディレクトリ。socket, `workers.json`, Worker ごとの `pid` /
/// `status.json` 等が置かれる。再起動で消えて構わない。
pub fn runtime_dir() -> Option<PathBuf> {
resolve_runtime_dir_from_parts(
@@ -61,7 +61,7 @@ pub fn runtime_dir() -> Option<PathBuf> {
/// `<config_dir>/profiles.toml` — user profile registry/default configuration.
///
/// This is application/profile selection configuration, not a Pod manifest
/// This is application/profile selection configuration, not a Worker manifest
/// layer.
pub fn user_profiles_path() -> Option<PathBuf> {
user_profiles_path_from_config_dir(config_dir())
@@ -88,24 +88,24 @@ pub fn sessions_dir() -> Option<PathBuf> {
sessions_dir_from_data_dir(data_dir())
}
/// `<runtime_dir>/pods.json` — machine-wide Pod allocation registry。
/// `<runtime_dir>/workers.json` — machine-wide Worker allocation registry。
pub fn pod_registry_path() -> Option<PathBuf> {
pod_registry_path_from_runtime_dir(runtime_dir())
}
/// `<runtime_dir>/<pod_name>/` — Pod ごとのランタイムディレクトリ。
pub fn pod_runtime_dir(pod_name: &str) -> Option<PathBuf> {
pod_runtime_dir_from_runtime_dir(runtime_dir(), pod_name)
/// `<runtime_dir>/<worker_name>/` — Worker ごとのランタイムディレクトリ。
pub fn worker_runtime_dir(worker_name: &str) -> Option<PathBuf> {
worker_runtime_dir_from_runtime_dir(runtime_dir(), worker_name)
}
/// `<runtime_dir>/<pod_name>/sock` — Pod の Unix socket パス。
/// `<runtime_dir>/<worker_name>/sock` — Worker の Unix socket パス。
///
/// Pod プロセス内で実際に socket を作成するのは `pod` crate の
/// `RuntimeDir::socket_path()` で、Pod 名が分かっている外部 (TUI の
/// Worker プロセス内で実際に socket を作成するのは `worker` crate の
/// `RuntimeDir::socket_path()` で、Worker 名が分かっている外部 (TUI の
/// attach フロー等) からの**予測**はこの関数で行う。両者は同じパス
/// を返すことが期待される。
pub fn pod_socket_path(pod_name: &str) -> Option<PathBuf> {
pod_socket_path_from_runtime_dir(runtime_dir(), pod_name)
pub fn pod_socket_path(worker_name: &str) -> Option<PathBuf> {
pod_socket_path_from_runtime_dir(runtime_dir(), worker_name)
}
// ---- internals --------------------------------------------------------------
@@ -184,21 +184,21 @@ fn sessions_dir_from_data_dir(data_dir: Option<PathBuf>) -> Option<PathBuf> {
}
fn pod_registry_path_from_runtime_dir(runtime_dir: Option<PathBuf>) -> Option<PathBuf> {
Some(runtime_dir?.join("pods.json"))
Some(runtime_dir?.join("workers.json"))
}
fn pod_runtime_dir_from_runtime_dir(
fn worker_runtime_dir_from_runtime_dir(
runtime_dir: Option<PathBuf>,
pod_name: &str,
worker_name: &str,
) -> Option<PathBuf> {
Some(runtime_dir?.join(pod_name))
Some(runtime_dir?.join(worker_name))
}
fn pod_socket_path_from_runtime_dir(
runtime_dir: Option<PathBuf>,
pod_name: &str,
worker_name: &str,
) -> Option<PathBuf> {
Some(pod_runtime_dir_from_runtime_dir(runtime_dir, pod_name)?.join("sock"))
Some(worker_runtime_dir_from_runtime_dir(runtime_dir, worker_name)?.join("sock"))
}
/// 空文字列の env は未設定として扱う。`std::env::var` は `Ok("")` と
@@ -397,10 +397,10 @@ mod tests {
);
assert_eq!(
pod_registry_path_from_runtime_dir(runtime_dir.clone()).unwrap(),
PathBuf::from("/sand/run/pods.json")
PathBuf::from("/sand/run/workers.json")
);
assert_eq!(
pod_runtime_dir_from_runtime_dir(runtime_dir.clone(), "foo").unwrap(),
worker_runtime_dir_from_runtime_dir(runtime_dir.clone(), "foo").unwrap(),
PathBuf::from("/sand/run/foo")
);
assert_eq!(
+78 -77
View File
@@ -2,7 +2,7 @@
//!
//! Profiles are reusable, human-authored recipes. They are intentionally not
//! complete runtime manifests: runtime-bound and authority-bearing fields such
//! as `pod.name` and concrete `scope.allow` rules are supplied by the resolver
//! as `worker.name` and concrete `scope.allow` rules are supplied by the resolver
//! from launch context.
use std::cell::RefCell;
@@ -19,9 +19,9 @@ use crate::config::{
use crate::model::{AuthRef, ModelManifest};
use crate::plugin::PluginConfig;
use crate::{
McpConfig, McpStdioCwdPolicy, MemoryConfig, Permission, PodManifest, PodManifestConfig,
PodMetaConfig, ResolveError, ScopeConfig, ScopeRule, SkillsConfig, WebConfig,
WorkerManifestConfig, paths,
EngineManifestConfig, McpConfig, McpStdioCwdPolicy, MemoryConfig, Permission, ResolveError,
ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifest, WorkerManifestConfig,
WorkerMetaConfig, paths,
};
const PROFILE_FORMAT_V1: &str = "yoi.lua-profile.v1";
@@ -408,26 +408,26 @@ pub struct WorkspaceOverrideSnapshot {
#[derive(Debug)]
struct WorkspaceOverrideLayer {
path: PathBuf,
config: PodManifestConfig,
config: WorkerManifestConfig,
}
#[derive(Debug, Clone)]
pub struct ResolvedProfile {
pub source: ProfileSource,
pub profile: Option<ProfileMetadata>,
pub manifest: PodManifest,
pub manifest: WorkerManifest,
pub manifest_snapshot: serde_json::Value,
pub raw_artifact: serde_json::Value,
}
#[derive(Debug, Clone, Default)]
pub struct ProfileResolveOptions {
pub pod_name: Option<String>,
pub worker_name: Option<String>,
}
impl ProfileResolveOptions {
pub fn with_pod_name(name: impl Into<String>) -> Self {
pub fn with_worker_name(name: impl Into<String>) -> Self {
Self {
pod_name: Some(name.into()),
worker_name: Some(name.into()),
}
}
}
@@ -469,8 +469,8 @@ impl ProfileResolver {
}
}
/// Resolve a registry/default selector against an already-discovered
/// registry. Callers such as SpawnPod use this to bind discovery to the
/// Pod's cwd instead of the process current directory.
/// registry. Callers such as SpawnWorker use this to bind discovery to the
/// Worker's cwd instead of the process current directory.
pub fn resolve_from_registry(
&self,
selector: &ProfileSelector,
@@ -604,22 +604,22 @@ fn resolve_lua_profile_value(
let profile: ProfileConfig = serde_json::from_value(value.clone())
.map_err(|source| ProfileError::ProfileDeserialize { source })?;
validate_profile_paths(&profile)?;
let pod_name = options
.pod_name
.ok_or(ProfileError::MissingRuntimePodName)?;
let worker_name = options
.worker_name
.ok_or(ProfileError::MissingRuntimeWorkerName)?;
let profile_meta = Some(ProfileMetadata {
name: profile.slug.clone().or_else(|| source_name(&source)),
description: profile.description.clone(),
format: Some(PROFILE_FORMAT_V1.to_string()),
});
let compaction = profile_compaction_to_partial(profile.compaction, &profile.model)?;
let config = PodManifestConfig {
pod: PodMetaConfig {
name: Some(pod_name),
let config = WorkerManifestConfig {
worker: WorkerMetaConfig {
name: Some(worker_name),
prompt_pack: None,
},
model: profile.model.unwrap_or_default(),
worker: profile.worker.unwrap_or_default(),
engine: profile.engine.unwrap_or_default(),
scope: profile_scope_to_config(profile.scope, workspace_base)?,
delegation_scope: profile_delegation_scope_to_config(
profile.delegation_scope,
@@ -635,7 +635,8 @@ fn resolve_lua_profile_value(
memory: profile.memory,
skills: profile.skills,
};
let mut config = PodManifestConfig::builtin_defaults().merge(config.resolve_paths(profile_dir));
let mut config =
WorkerManifestConfig::builtin_defaults().merge(config.resolve_paths(profile_dir));
let workspace_override_snapshot = if let Some(override_layer) = workspace_override {
let override_base =
override_layer
@@ -652,7 +653,7 @@ fn resolve_lua_profile_value(
} else {
None
};
let mut manifest = PodManifest::try_from(config).map_err(ProfileError::ManifestResolve)?;
let mut manifest = WorkerManifest::try_from(config).map_err(ProfileError::ManifestResolve)?;
manifest.profile = Some(ProfileManifestSnapshot {
source: source.clone(),
profile: profile_meta.clone(),
@@ -679,7 +680,7 @@ struct ProfileConfig {
#[serde(default)]
model: Option<ModelManifest>,
#[serde(default)]
worker: Option<WorkerManifestConfig>,
engine: Option<EngineManifestConfig>,
#[serde(default)]
scope: Option<ProfileScopeConfig>,
#[serde(default)]
@@ -814,16 +815,16 @@ fn load_workspace_override_file(path: &Path) -> Result<WorkspaceOverrideLayer, P
path: path.to_path_buf(),
source,
})?;
let config = PodManifestConfig::from_toml(&content).map_err(|source| {
let config = WorkerManifestConfig::from_toml(&content).map_err(|source| {
ProfileError::WorkspaceOverrideParse {
path: path.to_path_buf(),
source,
}
})?;
if config.pod.name.is_some() {
if config.worker.name.is_some() {
return Err(ProfileError::InvalidWorkspaceOverride {
path: path.to_path_buf(),
message: "workspace-local manifest overrides cannot set pod.name; Pod identity is a runtime input".into(),
message: "workspace-local manifest overrides cannot set worker.name; Worker identity is a runtime input".into(),
});
}
Ok(WorkspaceOverrideLayer {
@@ -1209,8 +1210,8 @@ fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), Profi
)));
}
}
if map.contains_key("pod") {
return Err(ProfileError::InvalidProfile("field `pod` is runtime-bound and is not allowed in reusable Profiles; pass the Pod name via CLI/TUI runtime inputs".into()));
if map.contains_key("worker") {
return Err(ProfileError::InvalidProfile("field `worker` is runtime-bound and is not allowed in reusable Profiles; pass the Worker name via CLI/TUI runtime inputs".into()));
}
if let Some(scope) = map.get("scope").and_then(|v| v.as_object()) {
for key in ["allow", "deny"] {
@@ -1442,7 +1443,7 @@ pub fn resolve_profile_artifact(
source,
base_dir,
base_dir,
ProfileResolveOptions::with_pod_name("artifact-pod"),
ProfileResolveOptions::with_worker_name("artifact-worker"),
raw_artifact.clone(),
raw_artifact,
None,
@@ -1489,8 +1490,8 @@ pub enum ProfileError {
InvalidWorkspaceOverride { path: PathBuf, message: String },
#[error("no default profile is configured")]
NoDefaultProfile,
#[error("profile resolution requires an explicit runtime Pod name")]
MissingRuntimePodName,
#[error("profile resolution requires an explicit runtime Worker name")]
MissingRuntimeWorkerName,
#[error("profile not found: {selector}")]
ProfileNotFound { selector: String },
#[error("ambiguous profile name `{name}`; use a source-qualified selector such as {matches:?}")]
@@ -1574,17 +1575,17 @@ mod tests {
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, expected),
ProfileResolveOptions::with_pod_name("role-pod"),
ProfileResolveOptions::with_worker_name("role-worker"),
)
.unwrap();
assert_eq!(
resolved.profile.as_ref().unwrap().name.as_deref(),
Some(expected)
);
assert_eq!(resolved.manifest.pod.name, "role-pod");
assert_eq!(resolved.manifest.worker.name, "role-worker");
if matches!(expected, "intake" | "orchestrator" | "coder" | "reviewer") {
let expected_instruction = format!("$yoi/role/{expected}");
assert_eq!(resolved.manifest.worker.instruction, expected_instruction);
assert_eq!(resolved.manifest.engine.instruction, expected_instruction);
}
}
}
@@ -1597,7 +1598,7 @@ mod tests {
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, role),
ProfileResolveOptions::with_pod_name("role-pod"),
ProfileResolveOptions::with_worker_name("role-worker"),
)
.unwrap()
.manifest
@@ -1605,7 +1606,7 @@ mod tests {
let companion = resolve("companion");
assert!(companion.feature.task.enabled);
assert!(companion.feature.pods.enabled);
assert!(companion.feature.workers.enabled);
assert!(companion.feature.ticket.enabled);
assert!(companion.scope.allow.is_empty());
assert!(companion.scope.deny.is_empty());
@@ -1615,7 +1616,7 @@ mod tests {
let intake = resolve("intake");
assert!(!intake.feature.task.enabled);
assert!(!intake.feature.pods.enabled);
assert!(!intake.feature.workers.enabled);
assert!(intake.feature.ticket.enabled);
assert!(intake.scope.allow.is_empty());
assert!(intake.delegation_scope.allow.is_empty());
@@ -1625,7 +1626,7 @@ mod tests {
let orchestrator = resolve("orchestrator");
assert!(!orchestrator.feature.task.enabled);
assert!(orchestrator.feature.pods.enabled);
assert!(orchestrator.feature.workers.enabled);
assert!(orchestrator.feature.ticket.enabled);
assert!(orchestrator.feature.ticket_orchestration.enabled);
assert!(orchestrator.scope.allow.is_empty());
@@ -1638,7 +1639,7 @@ mod tests {
let coder = resolve("coder");
assert!(coder.feature.task.enabled);
assert!(!coder.feature.pods.enabled);
assert!(!coder.feature.workers.enabled);
assert!(coder.scope.allow.is_empty());
assert!(coder.delegation_scope.allow.is_empty());
assert_eq!(coder.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
@@ -1646,7 +1647,7 @@ mod tests {
let reviewer = resolve("reviewer");
assert!(!reviewer.feature.task.enabled);
assert!(!reviewer.feature.pods.enabled);
assert!(!reviewer.feature.workers.enabled);
assert!(!reviewer.feature.ticket.enabled);
assert!(reviewer.scope.allow.is_empty());
assert!(reviewer.delegation_scope.allow.is_empty());
@@ -1655,17 +1656,17 @@ mod tests {
}
#[test]
fn profile_resolution_requires_runtime_pod_name() {
fn profile_resolution_requires_runtime_worker_name() {
let tmp = TempDir::new().unwrap();
let err = ProfileResolver::new()
.with_workspace_base(tmp.path())
.resolve(&ProfileSelector::Default, ProfileResolveOptions::default())
.unwrap_err();
assert!(matches!(err, ProfileError::MissingRuntimePodName));
assert!(matches!(err, ProfileError::MissingRuntimeWorkerName));
}
#[test]
fn resolves_plain_lua_profile_with_runtime_pod_name_and_scope_intent() {
fn resolves_plain_lua_profile_with_runtime_worker_name_and_scope_intent() {
let tmp = TempDir::new().unwrap();
let profile = write_profile(
tmp.path(),
@@ -1676,7 +1677,7 @@ local scope = require("yoi.scope")
return profile {
slug = "coder",
model = { scheme = "anthropic", model_id = "claude-sonnet-4-20250514" },
worker = { reasoning = "high" },
engine = { reasoning = "high" },
scope = scope.workspace_read(),
}
"#,
@@ -1687,13 +1688,13 @@ return profile {
.with_workspace_base(&workspace)
.resolve(
&ProfileSelector::path(&profile),
ProfileResolveOptions::with_pod_name("runtime-pod"),
ProfileResolveOptions::with_worker_name("runtime-worker"),
)
.unwrap();
assert_eq!(resolved.manifest.pod.name, "runtime-pod");
assert_eq!(resolved.manifest.worker.name, "runtime-worker");
assert_eq!(resolved.manifest.model.scheme, Some(SchemeKind::Anthropic));
assert_eq!(
resolved.manifest.worker.reasoning,
resolved.manifest.engine.reasoning,
Some(ReasoningControl::Effort(ReasoningEffort::High))
);
assert_eq!(resolved.manifest.scope.allow[0].target, workspace);
@@ -1748,7 +1749,7 @@ return profile {
.with_workspace_base(&workspace)
.resolve(
&ProfileSelector::path(&profile),
ProfileResolveOptions::with_pod_name("runtime-pod"),
ProfileResolveOptions::with_worker_name("runtime-worker"),
)
.unwrap();
@@ -1785,7 +1786,7 @@ return profile {
task = { enabled = true },
memory = { enabled = false },
web = { enabled = true },
pods = { enabled = true },
workers = { enabled = true },
ticket = { enabled = true, access = "read_only" },
ticket_orchestration = { enabled = false },
},
@@ -1798,14 +1799,14 @@ return profile {
.with_workspace_base(&workspace)
.resolve(
&ProfileSelector::path(&profile),
ProfileResolveOptions::with_pod_name("runtime-pod"),
ProfileResolveOptions::with_worker_name("runtime-worker"),
)
.unwrap();
assert_eq!(resolved.manifest.pod.name, "runtime-pod");
assert_eq!(resolved.manifest.worker.name, "runtime-worker");
assert!(resolved.manifest.feature.task.enabled);
assert!(!resolved.manifest.feature.memory.enabled);
assert!(resolved.manifest.feature.web.enabled);
assert!(resolved.manifest.feature.pods.enabled);
assert!(resolved.manifest.feature.workers.enabled);
assert!(resolved.manifest.feature.ticket.enabled);
assert_eq!(
resolved.manifest.feature.ticket.access,
@@ -1844,7 +1845,7 @@ return yoi.profile {
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::path(profile),
ProfileResolveOptions::with_pod_name("p"),
ProfileResolveOptions::with_worker_name("p"),
)
.unwrap();
assert_eq!(
@@ -1877,7 +1878,7 @@ p.slug = "assigned"
p.model = yoi.models.catalog("anthropic/claude-sonnet-4-6")
p.feature = {
task = { enabled = false },
pods = { enabled = true },
workers = { enabled = true },
}
p.web = { enabled = false }
p.compaction = yoi.compact.tokens { threshold = 123, request_threshold = 456 }
@@ -1888,7 +1889,7 @@ return p
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::path(profile),
ProfileResolveOptions::with_pod_name("p"),
ProfileResolveOptions::with_worker_name("p"),
)
.unwrap();
assert_eq!(
@@ -1896,7 +1897,7 @@ return p
Some("anthropic/claude-sonnet-4-6")
);
assert!(!resolved.manifest.feature.task.enabled);
assert!(resolved.manifest.feature.pods.enabled);
assert!(resolved.manifest.feature.workers.enabled);
assert_eq!(resolved.manifest.web.as_ref().unwrap().enabled, Some(false));
assert!(resolved.manifest.web.as_ref().unwrap().search.is_none());
assert_eq!(
@@ -1925,7 +1926,7 @@ return yoi.profile.extend("builtin:default", {
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::path(profile),
ProfileResolveOptions::with_pod_name("p"),
ProfileResolveOptions::with_worker_name("p"),
)
.unwrap_err();
let message = err.to_string();
@@ -1948,7 +1949,7 @@ return yoi.profile.extend("builtin:default", {
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::path(path),
ProfileResolveOptions::with_pod_name("p"),
ProfileResolveOptions::with_worker_name("p"),
)
.unwrap_err();
assert!(matches!(
@@ -1962,7 +1963,7 @@ return yoi.profile.extend("builtin:default", {
for (value, needle) in [
(serde_json::json!({"manifest": {}}), "manifest"),
(serde_json::json!({"config": {}}), "config"),
(serde_json::json!({"pod": {"name": "bad"}}), "pod"),
(serde_json::json!({"worker": {"name": "bad"}}), "worker"),
(
serde_json::json!({"model": {"ref": "codex-oauth/gpt-5.5"}, "scope": {"allow": []}}),
"scope.allow",
@@ -2008,7 +2009,7 @@ return profile {
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::path(profile),
ProfileResolveOptions::with_pod_name("p"),
ProfileResolveOptions::with_worker_name("p"),
)
.unwrap();
let c = resolved.manifest.compaction.unwrap();
@@ -2023,10 +2024,10 @@ return profile {
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, "default"),
ProfileResolveOptions::with_pod_name("runtime-workspace"),
ProfileResolveOptions::with_worker_name("runtime-workspace"),
)
.unwrap();
assert_eq!(resolved.manifest.pod.name, "runtime-workspace");
assert_eq!(resolved.manifest.worker.name, "runtime-workspace");
assert_eq!(
resolved.manifest.model.ref_.as_deref(),
Some("codex-oauth/gpt-5.5")
@@ -2060,9 +2061,9 @@ return profile {
std::fs::write(
&override_path,
r#"
[pod]
prompt_pack = "prompts.toml"
[worker]
prompt_pack = "prompts.toml"
[engine]
language = "ja"
[session]
record_event_trace = false
@@ -2074,15 +2075,15 @@ record_event_trace = false
.with_workspace_base(&nested)
.resolve(
&ProfileSelector::Default,
ProfileResolveOptions::with_pod_name("runtime-pod"),
ProfileResolveOptions::with_worker_name("runtime-worker"),
)
.unwrap();
assert_eq!(resolved.manifest.pod.name, "runtime-pod");
assert_eq!(resolved.manifest.worker.language, "ja");
assert_eq!(resolved.manifest.worker.name, "runtime-worker");
assert_eq!(resolved.manifest.engine.language, "ja");
assert!(!resolved.manifest.session.record_event_trace);
assert_eq!(
resolved.manifest.pod.prompt_pack.as_deref(),
resolved.manifest.worker.prompt_pack.as_deref(),
Some(yoi_dir.join("prompts.toml").as_path())
);
assert!(resolved.manifest.scope.allow.is_empty());
@@ -2111,9 +2112,9 @@ record_event_trace = false
std::fs::write(
parent_yoi.join(WORKSPACE_OVERRIDE_LOCAL_FILENAME),
r#"
[pod]
prompt_pack = "parent-prompts.toml"
[worker]
prompt_pack = "parent-prompts.toml"
[engine]
language = "parent"
"#,
)
@@ -2122,9 +2123,9 @@ language = "parent"
std::fs::write(
&nested_override_path,
r#"
[pod]
prompt_pack = "nested-prompts.toml"
[worker]
prompt_pack = "nested-prompts.toml"
[engine]
language = "nested"
"#,
)
@@ -2134,13 +2135,13 @@ language = "nested"
.with_workspace_base(&child)
.resolve(
&ProfileSelector::Default,
ProfileResolveOptions::with_pod_name("runtime-pod"),
ProfileResolveOptions::with_worker_name("runtime-worker"),
)
.unwrap();
assert_eq!(resolved.manifest.worker.language, "nested");
assert_eq!(resolved.manifest.engine.language, "nested");
assert_eq!(
resolved.manifest.pod.prompt_pack.as_deref(),
resolved.manifest.worker.prompt_pack.as_deref(),
Some(nested_yoi.join("nested-prompts.toml").as_path())
);
assert_eq!(
@@ -2155,13 +2156,13 @@ language = "nested"
}
#[test]
fn workspace_local_override_rejects_runtime_pod_name() {
fn workspace_local_override_rejects_runtime_worker_name() {
let tmp = TempDir::new().unwrap();
let yoi_dir = tmp.path().join(".yoi");
std::fs::create_dir_all(&yoi_dir).unwrap();
std::fs::write(
yoi_dir.join(WORKSPACE_OVERRIDE_LOCAL_FILENAME),
"[pod]\nname = \"not-local\"\n",
"[worker]\nname = \"not-local\"\n",
)
.unwrap();
@@ -2169,11 +2170,11 @@ language = "nested"
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::Default,
ProfileResolveOptions::with_pod_name("runtime-pod"),
ProfileResolveOptions::with_worker_name("runtime-worker"),
)
.unwrap_err();
assert!(matches!(err, ProfileError::InvalidWorkspaceOverride { .. }));
assert!(err.to_string().contains("pod.name"));
assert!(err.to_string().contains("worker.name"));
}
#[test]
+8 -8
View File
@@ -1,8 +1,8 @@
//! Runtime representation of a Pod's access scope.
//! Runtime representation of a Worker's access scope.
//!
//! Built from [`crate::ScopeConfig`] via [`Scope::from_config`]. Every
//! rule `target` must already be an absolute path — per-layer path
//! resolution runs earlier, inside [`crate::PodManifestConfig::resolve_paths`].
//! resolution runs earlier, inside [`crate::WorkerManifestConfig::resolve_paths`].
//! All rule `target` paths inside the [`Scope`] are canonicalised (where
//! possible) so access checks are pure path comparisons.
@@ -14,7 +14,7 @@ use arc_swap::{ArcSwap, Guard};
use crate::{Permission, ScopeConfig, ScopeRule};
/// Parsed, pwd-resolved set of allow/deny rules for a Pod.
/// Parsed, pwd-resolved set of allow/deny rules for a Worker.
///
/// Read/write access decisions are pure functions of the path being
/// queried and these rules — see [`Scope::permission_at`].
@@ -32,7 +32,7 @@ struct ResolvedRule {
recursive: bool,
}
/// Parsed filesystem authority this Pod may pass to spawned children.
/// Parsed filesystem authority this Worker may pass to spawned children.
///
/// Unlike [`Scope`], an empty allow list is valid and means no delegation
/// authority. Direct tools never consult this type.
@@ -173,7 +173,7 @@ impl Scope {
///
/// Every `target` in `config` must already be absolute — per-layer
/// resolution happens upstream in
/// [`crate::PodManifestConfig::resolve_paths`] so that cascade merge
/// [`crate::WorkerManifestConfig::resolve_paths`] so that cascade merge
/// operates on fully-qualified paths. A lingering relative target
/// here signals an upstream bug and is rejected.
pub fn from_config(config: &ScopeConfig) -> Result<Self, ScopeError> {
@@ -266,7 +266,7 @@ impl Scope {
/// Allow rules with their targets resolved to absolute paths.
///
/// Used by the pod-registry, where every Pod's allocation
/// Used by the pod-registry, where every Worker's allocation
/// must be expressed in absolute terms so prefix comparisons are
/// meaningful across processes.
pub fn allow_rules(&self) -> Vec<ScopeRule> {
@@ -324,7 +324,7 @@ impl Scope {
/// Build a new [`Scope`] equal to `self` with `extra_deny` appended
/// to the deny set. Used by dynamic-scope shrink paths
/// (e.g. SpawnPod-style delegation that strips Write from the
/// (e.g. SpawnWorker-style delegation that strips Write from the
/// spawner without touching its allow rules).
pub fn with_added_deny_rules(
&self,
@@ -420,7 +420,7 @@ impl Scope {
/// not lose each other's contributions.
///
/// All clones share the same underlying state — a `SharedScope` cloned
/// out to multiple consumers (Pod, ScopedFs, future grant/revoke
/// out to multiple consumers (Worker, ScopedFs, future grant/revoke
/// callers) sees every update.
#[derive(Debug, Clone)]
pub struct SharedScope {