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
+19 -1
View File
@@ -548,7 +548,10 @@ fn install_ticket_event_companion_notify_hook<C, St>(
let ticket_feature = &worker.manifest().feature.ticket;
if !ticket_feature.enabled
|| !matches!(ticket_feature.access, TicketFeatureAccessConfig::Lifecycle)
|| !matches!(
ticket_feature.access,
TicketFeatureAccessConfig::OrchestrationControl | TicketFeatureAccessConfig::Lifecycle
)
{
return;
}
@@ -679,6 +682,21 @@ where
TicketFeatureAccessConfig::ReadOnly => {
crate::feature::builtin::ticket::TicketFeatureAccess::ReadOnly
}
TicketFeatureAccessConfig::WorkspaceAuthoring => {
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
}
TicketFeatureAccessConfig::Lifecycle => {
crate::feature::builtin::ticket::TicketFeatureAccess::Lifecycle
}
+159 -12
View File
@@ -17,8 +17,7 @@ use ticket::{
tool::{
TICKET_BASE_READ_ONLY_TOOL_NAMES, TICKET_BASE_TOOL_NAMES,
TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES,
TICKET_READ_ONLY_TOOL_NAMES, TICKET_TOOL_NAMES, TicketToolBackend, ticket_tool_description,
ticket_tools,
TicketToolBackend, ticket_tool_description, ticket_tools,
},
};
@@ -34,24 +33,96 @@ The tools operate through the ticket crate backend and do not grant generic file
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TicketFeatureAccess {
/// Status/diagnostic access for views such as Companion that must not mutate Tickets.
/// Status/diagnostic access for views that must not mutate Tickets.
ReadOnly,
/// Full Ticket lifecycle access, including the read-only tools and all mutating Ticket tools.
/// User/Companion authoring access for shaping current Ticket specs and queueing work.
WorkspaceAuthoring,
/// 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,
/// Legacy full lifecycle access retained as a migration shim.
Lifecycle,
}
const WORKSPACE_AUTHORING_BASE_TOOL_NAMES: [&str; 10] = [
"TicketCreate",
"TicketEditItem",
"TicketList",
"TicketShow",
"TicketComment",
"TicketReview",
"TicketQueue",
"TicketClose",
"TicketDependencyCheck",
"TicketDoctor",
];
const INTAKE_BASE_TOOL_NAMES: [&str; 8] = [
"TicketCreate",
"TicketEditItem",
"TicketList",
"TicketShow",
"TicketComment",
"TicketIntakeReady",
"TicketDependencyCheck",
"TicketDoctor",
];
const ORCHESTRATION_CONTROL_BASE_TOOL_NAMES: [&str; 8] = [
"TicketList",
"TicketShow",
"TicketComment",
"TicketReview",
"TicketWorkflowState",
"TicketClose",
"TicketDependencyCheck",
"TicketDoctor",
];
const WORK_REPORT_BASE_TOOL_NAMES: [&str; 5] = [
"TicketList",
"TicketShow",
"TicketComment",
"TicketDependencyCheck",
"TicketDoctor",
];
const REVIEW_BASE_TOOL_NAMES: [&str; 6] = [
"TicketList",
"TicketShow",
"TicketComment",
"TicketReview",
"TicketDependencyCheck",
"TicketDoctor",
];
const RELATION_WRITE_TOOL_NAMES: [&str; 2] = ["TicketRelationRecord", "TicketRelationQuery"];
impl TicketFeatureAccess {
pub fn base_tool_names(self) -> &'static [&'static str] {
match self {
Self::ReadOnly => &TICKET_BASE_READ_ONLY_TOOL_NAMES,
Self::WorkspaceAuthoring => &WORKSPACE_AUTHORING_BASE_TOOL_NAMES,
Self::Intake => &INTAKE_BASE_TOOL_NAMES,
Self::OrchestrationControl => &ORCHESTRATION_CONTROL_BASE_TOOL_NAMES,
Self::WorkReport => &WORK_REPORT_BASE_TOOL_NAMES,
Self::Review => &REVIEW_BASE_TOOL_NAMES,
Self::Lifecycle => &TICKET_BASE_TOOL_NAMES,
}
}
pub fn orchestration_tool_names(self) -> &'static [&'static str] {
match self {
Self::ReadOnly => &TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES,
Self::Lifecycle => &TICKET_ORCHESTRATION_TOOL_NAMES,
Self::ReadOnly | Self::WorkReport | Self::Review => {
&TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES
}
Self::WorkspaceAuthoring | Self::Intake => &RELATION_WRITE_TOOL_NAMES,
Self::OrchestrationControl | Self::Lifecycle => &TICKET_ORCHESTRATION_TOOL_NAMES,
}
}
}
@@ -200,12 +271,6 @@ impl TicketFeature {
}
fn enabled_tool_names(&self) -> Vec<&'static str> {
if self.include_base_tools && self.include_orchestration_tools {
return match self.access {
TicketFeatureAccess::ReadOnly => TICKET_READ_ONLY_TOOL_NAMES.to_vec(),
TicketFeatureAccess::Lifecycle => TICKET_TOOL_NAMES.to_vec(),
};
}
let mut names = Vec::new();
if self.include_base_tools {
names.extend_from_slice(self.access.base_tool_names());
@@ -421,6 +486,20 @@ impl TicketBackend for WorkspaceHttpTicketBackend {
)
}
fn edit_item(&self, id: TicketIdOrSlug, edit: ticket::TicketItemEdit) -> TicketResult<Ticket> {
expect_ticket_result!(
self.invoke(TicketBackendOperation::EditItem { id, edit }),
TicketBackendOperationResult::Ticket
)
}
fn dependency_check(&self, id: TicketIdOrSlug) -> TicketResult<ticket::TicketDependencyCheck> {
expect_ticket_result!(
self.invoke(TicketBackendOperation::DependencyCheck { id }),
TicketBackendOperationResult::DependencyCheck
)
}
fn add_event(&self, id: TicketIdOrSlug, event: NewTicketEvent) -> TicketResult<()> {
match self.invoke(TicketBackendOperation::AddEvent { id, event })? {
TicketBackendOperationResult::Unit => Ok(()),
@@ -715,6 +794,74 @@ mod tests {
);
}
#[test]
fn semantic_access_presets_expose_role_capability_surfaces() {
let temp = TempDir::new().unwrap();
let workspace_authoring = ticket_tools_feature_with_options(
temp.path(),
Some(TicketFeatureAccess::WorkspaceAuthoring),
false,
);
let workspace_descriptor = workspace_authoring.descriptor();
let workspace_tools = workspace_descriptor
.tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>();
assert!(workspace_tools.contains(&"TicketCreate"));
assert!(workspace_tools.contains(&"TicketEditItem"));
assert!(workspace_tools.contains(&"TicketQueue"));
assert!(!workspace_tools.contains(&"TicketWorkflowState"));
let orchestration = ticket_tools_feature_with_options(
temp.path(),
Some(TicketFeatureAccess::OrchestrationControl),
true,
);
let orchestration_descriptor = orchestration.descriptor();
let orchestration_tools = orchestration_descriptor
.tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>();
assert!(orchestration_tools.contains(&"TicketWorkflowState"));
assert!(orchestration_tools.contains(&"TicketDependencyCheck"));
assert!(orchestration_tools.contains(&"TicketRelationRecord"));
assert!(orchestration_tools.contains(&"TicketOrchestrationPlanRecord"));
assert!(!orchestration_tools.contains(&"TicketEditItem"));
assert!(!orchestration_tools.contains(&"TicketQueue"));
let work_report = ticket_tools_feature_with_options(
temp.path(),
Some(TicketFeatureAccess::WorkReport),
false,
);
let work_report_descriptor = work_report.descriptor();
let work_report_tools = work_report_descriptor
.tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>();
assert!(work_report_tools.contains(&"TicketComment"));
assert!(!work_report_tools.contains(&"TicketReview"));
assert!(!work_report_tools.contains(&"TicketWorkflowState"));
let review = ticket_tools_feature_with_options(
temp.path(),
Some(TicketFeatureAccess::Review),
false,
);
let review_descriptor = review.descriptor();
let review_tools = review_descriptor
.tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>();
assert!(review_tools.contains(&"TicketReview"));
assert!(!review_tools.contains(&"TicketWorkflowState"));
}
#[test]
fn read_only_installation_does_not_expose_mutating_tools() {
let temp = TempDir::new().unwrap();