ticket: split ticket feature access presets

This commit is contained in:
2026-07-20 19:05:55 +09:00
parent a40d9c2027
commit 2c01e7672b
7 changed files with 533 additions and 27 deletions
+7 -1
View File
@@ -128,8 +128,12 @@ impl FeatureFlagConfigPartial {
pub struct TicketFeatureConfigPartial {
#[serde(default)]
pub enabled: Option<bool>,
/// Legacy access field. Prefer `preset` for new profile/DCDL authoring.
#[serde(default)]
pub access: Option<TicketFeatureAccessConfig>,
/// Semantic Ticket access preset for profile/DCDL authoring.
#[serde(default)]
pub preset: Option<TicketFeatureAccessConfig>,
}
impl TicketFeatureConfigPartial {
@@ -137,6 +141,7 @@ impl TicketFeatureConfigPartial {
Self {
enabled: other.enabled.or(self.enabled),
access: other.access.or(self.access),
preset: other.preset.or(self.preset),
}
}
}
@@ -190,7 +195,7 @@ impl From<TicketFeatureConfigPartial> for TicketFeatureConfig {
fn from(value: TicketFeatureConfigPartial) -> Self {
Self {
enabled: value.enabled.unwrap_or_default(),
access: value.access.unwrap_or_default(),
access: value.preset.or(value.access).unwrap_or_default(),
}
}
}
@@ -200,6 +205,7 @@ impl From<TicketFeatureConfig> for TicketFeatureConfigPartial {
Self {
enabled: Some(value.enabled),
access: Some(value.access),
preset: None,
}
}
}
+6
View File
@@ -180,6 +180,12 @@ impl Default for TicketFeatureConfig {
#[serde(rename_all = "snake_case")]
pub enum TicketFeatureAccessConfig {
ReadOnly,
WorkspaceAuthoring,
Intake,
OrchestrationControl,
WorkReport,
Review,
/// Legacy broad mutation preset retained as a migration shim.
Lifecycle,
}
+34 -2
View File
@@ -16,8 +16,8 @@ use crate::model::{AuthRef, ModelManifest};
use crate::plugin::PluginConfig;
use crate::{
EngineManifestConfig, McpConfig, McpStdioCwdPolicy, MemoryConfig, Permission, ResolveError,
ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifest, WorkerManifestConfig,
WorkerMetaConfig, paths,
ScopeConfig, ScopeRule, SkillsConfig, TicketFeatureAccessConfig, WebConfig, WorkerManifest,
WorkerManifestConfig, WorkerMetaConfig, paths,
};
const PROFILE_FORMAT_V1: &str = "yoi.profile.v1";
@@ -1012,6 +1012,18 @@ fn apply_role_profile(
value["feature"]["memory"] = serde_json::json!({ "enabled": memory });
value["feature"]["web"] = serde_json::json!({ "enabled": web });
value["feature"]["workers"] = serde_json::json!({ "enabled": workers });
let ticket_access = match slug {
"companion" => TicketFeatureAccessConfig::WorkspaceAuthoring,
"intake" => TicketFeatureAccessConfig::Intake,
"orchestrator" => TicketFeatureAccessConfig::OrchestrationControl,
"coder" => TicketFeatureAccessConfig::WorkReport,
"reviewer" => TicketFeatureAccessConfig::Review,
_ => TicketFeatureAccessConfig::Lifecycle,
};
value["feature"]["ticket"] = serde_json::json!({
"enabled": true,
"preset": ticket_access,
});
value["feature"]["ticket_orchestration"] =
serde_json::json!({ "enabled": ticket_orchestration });
}
@@ -1439,6 +1451,10 @@ mod tests {
assert_eq!(companion.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
assert!(companion.web.is_some());
assert!(companion.feature.ticket.enabled);
assert_eq!(
companion.feature.ticket.access,
TicketFeatureAccessConfig::WorkspaceAuthoring
);
assert!(!companion.feature.ticket_orchestration.enabled);
assert_eq!(
companion.compaction.as_ref().unwrap().threshold,
@@ -1461,6 +1477,10 @@ mod tests {
assert!(intake.feature.task.enabled);
assert!(!intake.feature.workers.enabled);
assert!(intake.feature.ticket.enabled);
assert_eq!(
intake.feature.ticket.access,
TicketFeatureAccessConfig::Intake
);
assert!(intake.scope.allow.is_empty());
assert!(intake.delegation_scope.allow.is_empty());
assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
@@ -1472,6 +1492,10 @@ mod tests {
assert!(orchestrator.feature.task.enabled);
assert!(orchestrator.feature.workers.enabled);
assert!(orchestrator.feature.ticket.enabled);
assert_eq!(
orchestrator.feature.ticket.access,
TicketFeatureAccessConfig::OrchestrationControl
);
assert!(orchestrator.feature.ticket_orchestration.enabled);
assert!(orchestrator.scope.allow.is_empty());
assert!(orchestrator.delegation_scope.allow.is_empty());
@@ -1491,12 +1515,20 @@ mod tests {
assert!(coder.web.is_some());
assert!(coder.compaction.is_some());
assert!(coder.feature.ticket.enabled);
assert_eq!(
coder.feature.ticket.access,
TicketFeatureAccessConfig::WorkReport
);
assert!(!coder.feature.ticket_orchestration.enabled);
let reviewer = resolve("reviewer");
assert!(reviewer.feature.task.enabled);
assert!(!reviewer.feature.workers.enabled);
assert!(reviewer.feature.ticket.enabled);
assert_eq!(
reviewer.feature.ticket.access,
TicketFeatureAccessConfig::Review
);
assert!(!reviewer.feature.ticket_orchestration.enabled);
assert!(reviewer.scope.allow.is_empty());
assert!(reviewer.delegation_scope.allow.is_empty());