ticket: use boolean ticket capabilities

This commit is contained in:
Keisuke Hirata 2026-07-21 06:13:26 +09:00
parent 064965d350
commit 7f7d2fe7fc
No known key found for this signature in database
12 changed files with 239 additions and 208 deletions

View File

@ -19,8 +19,8 @@ use crate::plugin::PluginConfig;
use crate::{ use crate::{
CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits, CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits,
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConfig, ScopeConfig, SessionConfig, McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConfig, ScopeConfig, SessionConfig,
SkillsConfig, TicketFeatureAccessConfig, TicketFeatureConfig, ToolOutputLimits, SkillsConfig, TicketFeatureConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule,
ToolPermissionConfig, ToolPermissionRule, WebConfig, WorkerManifest, WorkerMeta, WebConfig, WorkerManifest, WorkerMeta,
}; };
/// Partial-form Worker manifest. Every field is optional; one or more /// Partial-form Worker manifest. Every field is optional; one or more
@ -117,19 +117,24 @@ impl FeatureFlagConfigPartial {
} }
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(default, deny_unknown_fields)]
pub struct TicketFeatureConfigPartial { pub struct TicketFeatureConfigPartial {
#[serde(default)]
pub enabled: Option<bool>, pub enabled: Option<bool>,
#[serde(default)] pub authoring: Option<bool>,
pub preset: Option<TicketFeatureAccessConfig>, pub thread: Option<bool>,
pub intake: Option<bool>,
pub orchestration_control: Option<bool>,
} }
impl TicketFeatureConfigPartial { impl TicketFeatureConfigPartial {
fn merge(self, other: Self) -> Self { fn merge(self, other: Self) -> Self {
Self { Self {
enabled: other.enabled.or(self.enabled), enabled: other.enabled.or(self.enabled),
preset: other.preset.or(self.preset), authoring: other.authoring.or(self.authoring),
thread: other.thread.or(self.thread),
intake: other.intake.or(self.intake),
orchestration_control: other.orchestration_control.or(self.orchestration_control),
} }
} }
} }
@ -179,7 +184,10 @@ impl From<TicketFeatureConfigPartial> for TicketFeatureConfig {
fn from(value: TicketFeatureConfigPartial) -> Self { fn from(value: TicketFeatureConfigPartial) -> Self {
Self { Self {
enabled: value.enabled.unwrap_or_default(), enabled: value.enabled.unwrap_or_default(),
preset: value.preset.unwrap_or_default(), authoring: value.authoring.unwrap_or_default(),
thread: value.thread.unwrap_or_default(),
intake: value.intake.unwrap_or_default(),
orchestration_control: value.orchestration_control.unwrap_or_default(),
} }
} }
} }
@ -188,7 +196,10 @@ impl From<TicketFeatureConfig> for TicketFeatureConfigPartial {
fn from(value: TicketFeatureConfig) -> Self { fn from(value: TicketFeatureConfig) -> Self {
Self { Self {
enabled: Some(value.enabled), enabled: Some(value.enabled),
preset: Some(value.preset), authoring: Some(value.authoring),
thread: Some(value.thread),
intake: Some(value.intake),
orchestration_control: Some(value.orchestration_control),
} }
} }
} }
@ -1757,7 +1768,10 @@ enabled = true
[feature.ticket] [feature.ticket]
enabled = true enabled = true
preset = "read_only" authoring = false
thread = false
intake = false
orchestration_control = false
"#, "#,
) )
.unwrap(); .unwrap();
@ -1787,10 +1801,10 @@ preset = "read_only"
.unwrap(); .unwrap();
assert!(manifest.feature.task.enabled); assert!(manifest.feature.task.enabled);
assert!(manifest.feature.ticket.enabled); assert!(manifest.feature.ticket.enabled);
assert_eq!( assert!(!manifest.feature.ticket.authoring);
manifest.feature.ticket.preset, assert!(!manifest.feature.ticket.thread);
TicketFeatureAccessConfig::ReadOnly assert!(!manifest.feature.ticket.intake);
); assert!(!manifest.feature.ticket.orchestration_control);
assert!(!manifest.feature.memory.enabled); assert!(!manifest.feature.memory.enabled);
} }
@ -1803,14 +1817,18 @@ enabled = true
[feature.ticket] [feature.ticket]
enabled = true enabled = true
preset = "read_only" authoring = false
thread = false
intake = false
orchestration_control = false
"#, "#,
) )
.unwrap(); .unwrap();
let upper = WorkerManifestConfig::from_toml( let upper = WorkerManifestConfig::from_toml(
r#" r#"
[feature.ticket] [feature.ticket]
preset = "orchestration_control" thread = true
orchestration_control = true
[feature.web] [feature.web]
enabled = true enabled = true
@ -1844,10 +1862,10 @@ enabled = true
.unwrap(); .unwrap();
assert!(manifest.feature.memory.enabled); assert!(manifest.feature.memory.enabled);
assert!(manifest.feature.ticket.enabled); assert!(manifest.feature.ticket.enabled);
assert_eq!( assert!(!manifest.feature.ticket.authoring);
manifest.feature.ticket.preset, assert!(manifest.feature.ticket.thread);
TicketFeatureAccessConfig::OrchestrationControl assert!(!manifest.feature.ticket.intake);
); assert!(manifest.feature.ticket.orchestration_control);
assert!(manifest.feature.web.enabled); assert!(manifest.feature.web.enabled);
assert!(!manifest.feature.workers.enabled); assert!(!manifest.feature.workers.enabled);
} }

View File

@ -153,38 +153,18 @@ impl Default for FeatureFlagConfig {
} }
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct TicketFeatureConfig { pub struct TicketFeatureConfig {
#[serde(default)] #[serde(default)]
pub enabled: bool, pub enabled: bool,
#[serde(default)] #[serde(default)]
pub preset: TicketFeatureAccessConfig, pub authoring: bool,
} #[serde(default)]
pub thread: bool,
impl Default for TicketFeatureConfig { #[serde(default)]
fn default() -> Self { pub intake: bool,
Self { #[serde(default)]
enabled: false, pub orchestration_control: bool,
preset: TicketFeatureAccessConfig::default(),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TicketFeatureAccessConfig {
ReadOnly,
WorkspaceAuthoring,
Intake,
OrchestrationControl,
WorkReport,
Review,
}
impl Default for TicketFeatureAccessConfig {
fn default() -> Self {
Self::WorkspaceAuthoring
}
} }
/// External Agent Skills (`SKILL.md`) ingest configuration. Skills are /// External Agent Skills (`SKILL.md`) ingest configuration. Skills are

View File

@ -16,8 +16,8 @@ use crate::model::{AuthRef, ModelManifest};
use crate::plugin::PluginConfig; use crate::plugin::PluginConfig;
use crate::{ use crate::{
EngineManifestConfig, McpConfig, McpStdioCwdPolicy, MemoryConfig, Permission, ResolveError, EngineManifestConfig, McpConfig, McpStdioCwdPolicy, MemoryConfig, Permission, ResolveError,
ScopeConfig, ScopeRule, SkillsConfig, TicketFeatureAccessConfig, WebConfig, WorkerManifest, ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifest, WorkerManifestConfig,
WorkerManifestConfig, WorkerMetaConfig, paths, WorkerMetaConfig, paths,
}; };
const PROFILE_FORMAT_V1: &str = "yoi.profile.v1"; const PROFILE_FORMAT_V1: &str = "yoi.profile.v1";
@ -971,7 +971,7 @@ fn builtin_default_profile_artifact() -> serde_json::Value {
"memory": { "enabled": true }, "memory": { "enabled": true },
"web": { "enabled": true }, "web": { "enabled": true },
"workers": { "enabled": true }, "workers": { "enabled": true },
"ticket": { "enabled": true, "preset": "workspace_authoring" } "ticket": { "enabled": true, "authoring": true, "thread": true }
}, },
"memory": { "memory": {
"extract_threshold": 50000, "extract_threshold": 50000,
@ -1005,18 +1005,19 @@ fn apply_role_profile(
value["feature"]["memory"] = serde_json::json!({ "enabled": memory }); value["feature"]["memory"] = serde_json::json!({ "enabled": memory });
value["feature"]["web"] = serde_json::json!({ "enabled": web }); value["feature"]["web"] = serde_json::json!({ "enabled": web });
value["feature"]["workers"] = serde_json::json!({ "enabled": workers }); value["feature"]["workers"] = serde_json::json!({ "enabled": workers });
let ticket_access = match slug { let ticket = match slug {
"companion" => TicketFeatureAccessConfig::WorkspaceAuthoring, "companion" => serde_json::json!({ "enabled": true, "authoring": true, "thread": true }),
"intake" => TicketFeatureAccessConfig::Intake, "intake" => {
"orchestrator" => TicketFeatureAccessConfig::OrchestrationControl, serde_json::json!({ "enabled": true, "authoring": true, "thread": true, "intake": true })
"coder" => TicketFeatureAccessConfig::WorkReport, }
"reviewer" => TicketFeatureAccessConfig::Review, "orchestrator" => {
_ => TicketFeatureAccessConfig::WorkspaceAuthoring, serde_json::json!({ "enabled": true, "thread": true, "orchestration_control": true })
}
"coder" => serde_json::json!({ "enabled": true, "thread": true }),
"reviewer" => serde_json::json!({ "enabled": true, "thread": true }),
_ => serde_json::json!({ "enabled": true, "authoring": true, "thread": true }),
}; };
value["feature"]["ticket"] = serde_json::json!({ value["feature"]["ticket"] = ticket;
"enabled": true,
"preset": ticket_access,
});
} }
fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> { fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> {
@ -1442,10 +1443,10 @@ mod tests {
assert_eq!(companion.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert_eq!(companion.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
assert!(companion.web.is_some()); assert!(companion.web.is_some());
assert!(companion.feature.ticket.enabled); assert!(companion.feature.ticket.enabled);
assert_eq!( assert!(companion.feature.ticket.authoring);
companion.feature.ticket.preset, assert!(companion.feature.ticket.thread);
TicketFeatureAccessConfig::WorkspaceAuthoring assert!(!companion.feature.ticket.intake);
); assert!(!companion.feature.ticket.orchestration_control);
assert_eq!( assert_eq!(
companion.compaction.as_ref().unwrap().threshold, companion.compaction.as_ref().unwrap().threshold,
Some(240000) Some(240000)
@ -1467,10 +1468,11 @@ mod tests {
assert!(intake.feature.task.enabled); assert!(intake.feature.task.enabled);
assert!(!intake.feature.workers.enabled); assert!(!intake.feature.workers.enabled);
assert!(intake.feature.ticket.enabled); assert!(intake.feature.ticket.enabled);
assert_eq!( assert!(intake.feature.ticket.enabled);
intake.feature.ticket.preset, assert!(intake.feature.ticket.authoring);
TicketFeatureAccessConfig::Intake assert!(intake.feature.ticket.thread);
); assert!(intake.feature.ticket.intake);
assert!(!intake.feature.ticket.orchestration_control);
assert!(intake.scope.allow.is_empty()); assert!(intake.scope.allow.is_empty());
assert!(intake.delegation_scope.allow.is_empty()); assert!(intake.delegation_scope.allow.is_empty());
assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
@ -1481,10 +1483,11 @@ mod tests {
assert!(orchestrator.feature.task.enabled); assert!(orchestrator.feature.task.enabled);
assert!(orchestrator.feature.workers.enabled); assert!(orchestrator.feature.workers.enabled);
assert!(orchestrator.feature.ticket.enabled); assert!(orchestrator.feature.ticket.enabled);
assert_eq!( assert!(orchestrator.feature.ticket.enabled);
orchestrator.feature.ticket.preset, assert!(!orchestrator.feature.ticket.authoring);
TicketFeatureAccessConfig::OrchestrationControl assert!(orchestrator.feature.ticket.thread);
); assert!(!orchestrator.feature.ticket.intake);
assert!(orchestrator.feature.ticket.orchestration_control);
assert!(orchestrator.scope.allow.is_empty()); assert!(orchestrator.scope.allow.is_empty());
assert!(orchestrator.delegation_scope.allow.is_empty()); assert!(orchestrator.delegation_scope.allow.is_empty());
assert_eq!( assert_eq!(
@ -1503,19 +1506,20 @@ mod tests {
assert!(coder.web.is_some()); assert!(coder.web.is_some());
assert!(coder.compaction.is_some()); assert!(coder.compaction.is_some());
assert!(coder.feature.ticket.enabled); assert!(coder.feature.ticket.enabled);
assert_eq!( assert!(coder.feature.ticket.enabled);
coder.feature.ticket.preset, assert!(!coder.feature.ticket.authoring);
TicketFeatureAccessConfig::WorkReport assert!(coder.feature.ticket.thread);
); assert!(!coder.feature.ticket.intake);
assert!(!coder.feature.ticket.orchestration_control);
let reviewer = resolve("reviewer"); let reviewer = resolve("reviewer");
assert!(reviewer.feature.task.enabled); assert!(reviewer.feature.task.enabled);
assert!(!reviewer.feature.workers.enabled); assert!(!reviewer.feature.workers.enabled);
assert!(reviewer.feature.ticket.enabled); assert!(reviewer.feature.ticket.enabled);
assert_eq!( assert!(reviewer.feature.ticket.enabled);
reviewer.feature.ticket.preset, assert!(!reviewer.feature.ticket.authoring);
TicketFeatureAccessConfig::Review assert!(reviewer.feature.ticket.thread);
); assert!(!reviewer.feature.ticket.intake);
assert!(!reviewer.feature.ticket.orchestration_control);
assert!(reviewer.scope.allow.is_empty()); assert!(reviewer.scope.allow.is_empty());
assert!(reviewer.delegation_scope.allow.is_empty()); assert!(reviewer.delegation_scope.allow.is_empty());
assert_eq!(reviewer.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert_eq!(reviewer.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
@ -1664,7 +1668,10 @@ enabled = true
[feature.ticket] [feature.ticket]
enabled = true enabled = true
preset = "read_only" authoring = false
thread = false
intake = false
orchestration_control = false
"#, "#,
); );
let workspace = tmp.path().join("workspace"); let workspace = tmp.path().join("workspace");
@ -1682,10 +1689,10 @@ preset = "read_only"
assert!(resolved.manifest.feature.web.enabled); assert!(resolved.manifest.feature.web.enabled);
assert!(resolved.manifest.feature.workers.enabled); assert!(resolved.manifest.feature.workers.enabled);
assert!(resolved.manifest.feature.ticket.enabled); assert!(resolved.manifest.feature.ticket.enabled);
assert_eq!( assert!(!resolved.manifest.feature.ticket.authoring);
resolved.manifest.feature.ticket.preset, assert!(!resolved.manifest.feature.ticket.thread);
crate::TicketFeatureAccessConfig::ReadOnly assert!(!resolved.manifest.feature.ticket.intake);
); assert!(!resolved.manifest.feature.ticket.orchestration_control);
assert_eq!( assert_eq!(
resolved.manifest.delegation_scope.allow[0].target, resolved.manifest.delegation_scope.allow[0].target,
workspace workspace
@ -1768,14 +1775,11 @@ worker_context_max_tokens = 68000
resolved.manifest.model.ref_.as_deref(), resolved.manifest.model.ref_.as_deref(),
Some("codex-oauth/gpt-5.5") Some("codex-oauth/gpt-5.5")
); );
assert!(resolved.manifest.scope.allow.is_empty());
assert!(resolved.manifest.delegation_scope.allow.is_empty());
assert!(resolved.manifest.session.record_event_trace);
assert!(resolved.manifest.feature.ticket.enabled); assert!(resolved.manifest.feature.ticket.enabled);
assert_eq!( assert!(resolved.manifest.feature.ticket.authoring);
resolved.manifest.feature.ticket.preset, assert!(resolved.manifest.feature.ticket.thread);
crate::TicketFeatureAccessConfig::WorkspaceAuthoring assert!(!resolved.manifest.feature.ticket.intake);
); assert!(!resolved.manifest.feature.ticket.orchestration_control);
assert_eq!( assert_eq!(
resolved.profile.as_ref().unwrap().name.as_deref(), resolved.profile.as_ref().unwrap().name.as_deref(),
Some("default") Some("default")

View File

@ -238,7 +238,8 @@ enabled = false
[feature.ticket] [feature.ticket]
enabled = true enabled = true
preset = "workspace_authoring" authoring = true
thread = true
[memory] [memory]
extract_threshold = 50000 extract_threshold = 50000
@ -296,7 +297,7 @@ mod tests {
assert!(profile.contains("ref = \"codex-oauth/gpt-5.5\"")); assert!(profile.contains("ref = \"codex-oauth/gpt-5.5\""));
assert!(profile.contains("scope = \"workspace_write\"")); assert!(profile.contains("scope = \"workspace_write\""));
assert!( assert!(
profile.contains("[feature.ticket]\nenabled = true\npreset = \"workspace_authoring\"") profile.contains("[feature.ticket]\nenabled = true\nauthoring = true\nthread = true")
); );
} }

View File

@ -4,7 +4,6 @@ use std::sync::atomic::Ordering;
use llm_engine::EngineError; use llm_engine::EngineError;
use llm_engine::llm_client::client::LlmClient; use llm_engine::llm_client::client::LlmClient;
use manifest::TicketFeatureAccessConfig;
use session_store::WorkerMetadataStore; use session_store::WorkerMetadataStore;
use session_store::{LogEntry, Store}; use session_store::{LogEntry, Store};
use ticket::LocalTicketBackend; use ticket::LocalTicketBackend;
@ -547,9 +546,7 @@ fn install_ticket_event_companion_notify_hook<C, St>(
} }
let ticket_feature = &worker.manifest().feature.ticket; let ticket_feature = &worker.manifest().feature.ticket;
if !ticket_feature.enabled if !ticket_feature.enabled || !ticket_feature.orchestration_control {
|| ticket_feature.preset != TicketFeatureAccessConfig::OrchestrationControl
{
return; return;
} }
@ -675,25 +672,11 @@ where
feature_registry.add_module(task_feature); feature_registry.add_module(task_feature);
} }
if feature_config.ticket.enabled { if feature_config.ticket.enabled {
let ticket_access = match feature_config.ticket.preset { let ticket_access = crate::feature::builtin::ticket::TicketFeatureAccess {
TicketFeatureAccessConfig::ReadOnly => { authoring: feature_config.ticket.authoring,
crate::feature::builtin::ticket::TicketFeatureAccess::ReadOnly thread: feature_config.ticket.thread,
} intake: feature_config.ticket.intake,
TicketFeatureAccessConfig::WorkspaceAuthoring => { orchestration_control: feature_config.ticket.orchestration_control,
crate::feature::builtin::ticket::TicketFeatureAccess::WorkspaceAuthoring
}
TicketFeatureAccessConfig::Intake => {
crate::feature::builtin::ticket::TicketFeatureAccess::Intake
}
TicketFeatureAccessConfig::OrchestrationControl => {
crate::feature::builtin::ticket::TicketFeatureAccess::OrchestrationControl
}
TicketFeatureAccessConfig::WorkReport => {
crate::feature::builtin::ticket::TicketFeatureAccess::WorkReport
}
TicketFeatureAccessConfig::Review => {
crate::feature::builtin::ticket::TicketFeatureAccess::Review
}
}; };
// Ticket tools are typed operations over the current workspace Ticket backend. // Ticket tools are typed operations over the current workspace Ticket backend.
// Runtime-hosted Workers prefer the workspace API URI carried by the // Runtime-hosted Workers prefer the workspace API URI carried by the

View File

@ -14,7 +14,7 @@ use ticket::{
TicketIntakeSummary, TicketListQuery, TicketRef, TicketRelation, TicketRelationKind, TicketIntakeSummary, TicketListQuery, TicketRef, TicketRelation, TicketRelationKind,
TicketRelationView, TicketReview, TicketStateChange, TicketSummary, TicketRelationView, TicketReview, TicketStateChange, TicketSummary,
config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig}, config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig},
tool::{TicketToolBackend, ticket_tool_description, ticket_tools}, tool::{TICKET_TOOL_NAMES, TicketToolBackend, ticket_tool_description, ticket_tools},
}; };
use crate::feature::{ use crate::feature::{
@ -27,23 +27,88 @@ const FEATURE_NAME: &str = "Ticket tools";
const FEATURE_DESCRIPTION: &str = "Typed local Ticket work-item operations over a bounded backend root. \ const FEATURE_DESCRIPTION: &str = "Typed local Ticket work-item operations over a bounded backend root. \
The tools operate through the ticket crate backend and do not grant generic filesystem write scope."; The tools operate through the ticket crate backend and do not grant generic filesystem write scope.";
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum TicketFeatureAccess { pub struct TicketFeatureAccess {
/// Status/diagnostic access for views that must not mutate Tickets. pub authoring: bool,
ReadOnly, pub thread: bool,
/// User/Companion authoring access for shaping current Ticket specs and queueing work. pub intake: bool,
WorkspaceAuthoring, pub orchestration_control: bool,
/// Intake access for creating/refining planning Tickets and marking them ready.
Intake,
/// Orchestrator control access for state/thread/relation/orchestration operations.
OrchestrationControl,
/// Coder-style access for reading Tickets and writing implementation reports/comments.
WorkReport,
/// Reviewer-style access for reading Tickets and writing review/comment thread events.
Review,
} }
const READ_ONLY_TOOL_NAMES: [&str; 6] = [ impl TicketFeatureAccess {
pub const fn read_only() -> Self {
Self {
authoring: false,
thread: false,
intake: false,
orchestration_control: false,
}
}
pub const fn workspace_authoring() -> Self {
Self {
authoring: true,
thread: true,
intake: false,
orchestration_control: false,
}
}
pub const fn intake() -> Self {
Self {
authoring: true,
thread: true,
intake: true,
orchestration_control: false,
}
}
pub const fn orchestration_control() -> Self {
Self {
authoring: false,
thread: true,
intake: false,
orchestration_control: true,
}
}
pub const fn work_report() -> Self {
Self {
authoring: false,
thread: true,
intake: false,
orchestration_control: false,
}
}
pub const fn review() -> Self {
Self {
authoring: false,
thread: true,
intake: false,
orchestration_control: false,
}
}
pub fn tool_names(self) -> Vec<&'static str> {
TICKET_TOOL_NAMES
.iter()
.copied()
.filter(|name| self.allows_tool(name))
.collect()
}
fn allows_tool(self, name: &str) -> bool {
READ_ONLY_TOOL_NAMES.contains(&name)
|| (self.authoring && AUTHORING_TOOL_NAMES.contains(&name))
|| (self.thread && THREAD_TOOL_NAMES.contains(&name))
|| (self.intake && INTAKE_TOOL_NAMES.contains(&name))
|| (self.orchestration_control
&& ORCHESTRATION_CONTROL_ADDITIONAL_TOOL_NAMES.contains(&name))
}
}
const READ_ONLY_TOOL_NAMES: &[&str] = &[
"TicketList", "TicketList",
"TicketShow", "TicketShow",
"TicketDependencyCheck", "TicketDependencyCheck",
@ -52,7 +117,19 @@ const READ_ONLY_TOOL_NAMES: [&str; 6] = [
"TicketOrchestrationPlanQuery", "TicketOrchestrationPlanQuery",
]; ];
const WORKSPACE_AUTHORING_TOOL_NAMES: [&str; 12] = [ const AUTHORING_TOOL_NAMES: &[&str] = &[
"TicketCreate",
"TicketEditItem",
"TicketQueue",
"TicketClose",
"TicketRelationRecord",
];
const THREAD_TOOL_NAMES: &[&str] = &["TicketComment", "TicketReview"];
const INTAKE_TOOL_NAMES: &[&str] = &["TicketIntakeReady"];
const WORKSPACE_AUTHORING_TOOL_NAMES: &[&str] = &[
"TicketCreate", "TicketCreate",
"TicketEditItem", "TicketEditItem",
"TicketList", "TicketList",
@ -65,22 +142,10 @@ const WORKSPACE_AUTHORING_TOOL_NAMES: [&str; 12] = [
"TicketDoctor", "TicketDoctor",
"TicketRelationRecord", "TicketRelationRecord",
"TicketRelationQuery", "TicketRelationQuery",
"TicketOrchestrationPlanQuery",
]; ];
const INTAKE_TOOL_NAMES: [&str; 10] = [ const ORCHESTRATION_CONTROL_TOOL_NAMES: &[&str] = &[
"TicketCreate",
"TicketEditItem",
"TicketList",
"TicketShow",
"TicketComment",
"TicketIntakeReady",
"TicketDependencyCheck",
"TicketDoctor",
"TicketRelationRecord",
"TicketRelationQuery",
];
const ORCHESTRATION_CONTROL_TOOL_NAMES: [&str; 12] = [
"TicketList", "TicketList",
"TicketShow", "TicketShow",
"TicketComment", "TicketComment",
@ -95,40 +160,13 @@ const ORCHESTRATION_CONTROL_TOOL_NAMES: [&str; 12] = [
"TicketOrchestrationPlanQuery", "TicketOrchestrationPlanQuery",
]; ];
const WORK_REPORT_TOOL_NAMES: [&str; 7] = [ const ORCHESTRATION_CONTROL_ADDITIONAL_TOOL_NAMES: &[&str] = &[
"TicketList", "TicketWorkflowState",
"TicketShow", "TicketClose",
"TicketComment", "TicketRelationRecord",
"TicketDependencyCheck", "TicketOrchestrationPlanRecord",
"TicketDoctor",
"TicketRelationQuery",
"TicketOrchestrationPlanQuery",
]; ];
const REVIEW_TOOL_NAMES: [&str; 8] = [
"TicketList",
"TicketShow",
"TicketComment",
"TicketReview",
"TicketDependencyCheck",
"TicketDoctor",
"TicketRelationQuery",
"TicketOrchestrationPlanQuery",
];
impl TicketFeatureAccess {
pub fn tool_names(self) -> &'static [&'static str] {
match self {
Self::ReadOnly => &READ_ONLY_TOOL_NAMES,
Self::WorkspaceAuthoring => &WORKSPACE_AUTHORING_TOOL_NAMES,
Self::Intake => &INTAKE_TOOL_NAMES,
Self::OrchestrationControl => &ORCHESTRATION_CONTROL_TOOL_NAMES,
Self::WorkReport => &WORK_REPORT_TOOL_NAMES,
Self::Review => &REVIEW_TOOL_NAMES,
}
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum TicketFeatureBackend { pub enum TicketFeatureBackend {
Local { Local {
@ -173,7 +211,7 @@ pub struct TicketFeature {
impl TicketFeature { impl TicketFeature {
pub fn new(backend_root: impl Into<PathBuf>) -> Self { pub fn new(backend_root: impl Into<PathBuf>) -> Self {
Self::new_with_access(backend_root, TicketFeatureAccess::WorkspaceAuthoring) Self::new_with_access(backend_root, TicketFeatureAccess::workspace_authoring())
} }
pub fn new_with_access(backend_root: impl Into<PathBuf>, access: TicketFeatureAccess) -> Self { pub fn new_with_access(backend_root: impl Into<PathBuf>, access: TicketFeatureAccess) -> Self {
@ -198,7 +236,7 @@ impl TicketFeature {
} }
pub fn for_workspace(workspace: impl AsRef<Path>) -> Self { pub fn for_workspace(workspace: impl AsRef<Path>) -> Self {
Self::for_workspace_with_access(workspace, TicketFeatureAccess::WorkspaceAuthoring) Self::for_workspace_with_access(workspace, TicketFeatureAccess::workspace_authoring())
} }
pub fn for_workspace_with_access( pub fn for_workspace_with_access(
@ -237,7 +275,7 @@ impl TicketFeature {
self.access self.access
} }
fn enabled_tool_names(&self) -> &'static [&'static str] { fn enabled_tool_names(&self) -> Vec<&'static str> {
self.access.tool_names() self.access.tool_names()
} }
@ -298,7 +336,7 @@ impl FeatureModule for TicketFeature {
let enabled_tool_names = self.enabled_tool_names(); let enabled_tool_names = self.enabled_tool_names();
for name in enabled_tool_names { for name in enabled_tool_names {
descriptor = descriptor.with_tool(ToolDeclaration::new( descriptor = descriptor.with_tool(ToolDeclaration::new(
*name, name,
ticket_tool_description(name, self.record_language.as_deref()), ticket_tool_description(name, self.record_language.as_deref()),
)); ));
} }
@ -701,9 +739,10 @@ mod tests {
#[test] #[test]
fn read_only_descriptor_declares_only_state_tools() { fn read_only_descriptor_declares_only_state_tools() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let feature = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::ReadOnly); let feature =
ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::read_only());
let descriptor = feature.descriptor(); let descriptor = feature.descriptor();
assert_eq!(feature.access(), TicketFeatureAccess::ReadOnly); assert_eq!(feature.access(), TicketFeatureAccess::read_only());
assert_eq!(descriptor.tools.len(), READ_ONLY_TOOL_NAMES.len()); assert_eq!(descriptor.tools.len(), READ_ONLY_TOOL_NAMES.len());
assert_eq!( assert_eq!(
descriptor descriptor
@ -720,10 +759,13 @@ mod tests {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let feature = ticket_tools_feature_with_access( let feature = ticket_tools_feature_with_access(
temp.path(), temp.path(),
TicketFeatureAccess::OrchestrationControl, TicketFeatureAccess::orchestration_control(),
); );
let descriptor = feature.descriptor(); let descriptor = feature.descriptor();
assert_eq!(feature.access(), TicketFeatureAccess::OrchestrationControl); assert_eq!(
feature.access(),
TicketFeatureAccess::orchestration_control()
);
assert_eq!( assert_eq!(
descriptor descriptor
.tools .tools
@ -735,11 +777,13 @@ mod tests {
} }
#[test] #[test]
fn semantic_access_presets_expose_role_capability_surfaces() { fn additive_ticket_capabilities_expose_expected_tool_surfaces() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let workspace_authoring = let workspace_authoring = ticket_tools_feature_with_access(
ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::WorkspaceAuthoring); temp.path(),
TicketFeatureAccess::workspace_authoring(),
);
let workspace_descriptor = workspace_authoring.descriptor(); let workspace_descriptor = workspace_authoring.descriptor();
let workspace_tools = workspace_descriptor let workspace_tools = workspace_descriptor
.tools .tools
@ -753,7 +797,7 @@ mod tests {
let orchestration = ticket_tools_feature_with_access( let orchestration = ticket_tools_feature_with_access(
temp.path(), temp.path(),
TicketFeatureAccess::OrchestrationControl, TicketFeatureAccess::orchestration_control(),
); );
let orchestration_descriptor = orchestration.descriptor(); let orchestration_descriptor = orchestration.descriptor();
let orchestration_tools = orchestration_descriptor let orchestration_tools = orchestration_descriptor
@ -769,7 +813,7 @@ mod tests {
assert!(!orchestration_tools.contains(&"TicketQueue")); assert!(!orchestration_tools.contains(&"TicketQueue"));
let work_report = let work_report =
ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::WorkReport); ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::work_report());
let work_report_descriptor = work_report.descriptor(); let work_report_descriptor = work_report.descriptor();
let work_report_tools = work_report_descriptor let work_report_tools = work_report_descriptor
.tools .tools
@ -777,10 +821,10 @@ mod tests {
.map(|tool| tool.name.as_str()) .map(|tool| tool.name.as_str())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert!(work_report_tools.contains(&"TicketComment")); assert!(work_report_tools.contains(&"TicketComment"));
assert!(!work_report_tools.contains(&"TicketReview")); assert!(work_report_tools.contains(&"TicketReview"));
assert!(!work_report_tools.contains(&"TicketWorkflowState")); assert!(!work_report_tools.contains(&"TicketWorkflowState"));
let review = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::Review); let review = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::review());
let review_descriptor = review.descriptor(); let review_descriptor = review.descriptor();
let review_tools = review_descriptor let review_tools = review_descriptor
.tools .tools
@ -800,7 +844,7 @@ mod tests {
let report = FeatureRegistryBuilder::new() let report = FeatureRegistryBuilder::new()
.with_module(ticket_tools_feature_with_access( .with_module(ticket_tools_feature_with_access(
temp.path(), temp.path(),
TicketFeatureAccess::ReadOnly, TicketFeatureAccess::read_only(),
)) ))
.install_into_pending(&mut pending_tools, &mut hooks); .install_into_pending(&mut pending_tools, &mut hooks);
@ -833,7 +877,8 @@ language = "Japanese"
"#, "#,
); );
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)); make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
let feature = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::ReadOnly); let feature =
ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::read_only());
let descriptor = feature.descriptor(); let descriptor = feature.descriptor();
let descriptor_description = descriptor let descriptor_description = descriptor
.tools .tools
@ -867,7 +912,7 @@ language = "Japanese"
let report = FeatureRegistryBuilder::new() let report = FeatureRegistryBuilder::new()
.with_module(ticket_tools_feature_with_access( .with_module(ticket_tools_feature_with_access(
temp.path(), temp.path(),
TicketFeatureAccess::WorkspaceAuthoring, TicketFeatureAccess::workspace_authoring(),
)) ))
.install_into_pending(&mut pending_tools, &mut hooks); .install_into_pending(&mut pending_tools, &mut hooks);
@ -906,7 +951,7 @@ language = "Japanese"
let report = FeatureRegistryBuilder::new() let report = FeatureRegistryBuilder::new()
.with_module(ticket_tools_feature_with_access( .with_module(ticket_tools_feature_with_access(
temp.path(), temp.path(),
TicketFeatureAccess::WorkspaceAuthoring, TicketFeatureAccess::workspace_authoring(),
)) ))
.install_into_pending(&mut pending_tools, &mut hooks); .install_into_pending(&mut pending_tools, &mut hooks);

View File

@ -8,6 +8,6 @@ import "./default.dcdl" // {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = false; }; workers = { enabled = false; };
ticket = { enabled = true; preset = "work_report"; }; ticket = { enabled = true; thread = true; };
}; };
} }

View File

@ -8,6 +8,6 @@ import "./default.dcdl" // {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = true; }; workers = { enabled = true; };
ticket = { enabled = true; preset = "workspace_authoring"; }; ticket = { enabled = true; authoring = true; thread = true; };
}; };
} }

View File

@ -26,7 +26,7 @@ feature = {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = true; }; workers = { enabled = true; };
ticket = { enabled = true; preset = "workspace_authoring"; }; ticket = { enabled = true; authoring = true; thread = true; };
}; };
memory = { memory = {

View File

@ -8,6 +8,6 @@ import "./default.dcdl" // {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = false; }; workers = { enabled = false; };
ticket = { enabled = true; preset = "intake"; }; ticket = { enabled = true; authoring = true; thread = true; intake = true; };
}; };
} }

View File

@ -8,6 +8,6 @@ import "./default.dcdl" // {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = true; }; workers = { enabled = true; };
ticket = { enabled = true; preset = "orchestration_control"; }; ticket = { enabled = true; thread = true; orchestration_control = true; };
}; };
} }

View File

@ -8,6 +8,6 @@ import "./default.dcdl" // {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = false; }; workers = { enabled = false; };
ticket = { enabled = true; preset = "review"; }; ticket = { enabled = true; thread = true; };
}; };
} }