feat: compose Subject Ticket assignment with selected dependency contracts

Adapt T-778 source 6d15569 and selected T-465 plugin plans on T-803 51f0c7e0. Preserve frozen Workspace107 and append Subject108, current assignee/target checks, recipient-scoped notification recovery and public contract generation. This is a candidate composition, not target adoption.
This commit is contained in:
2026-10-30 03:16:00 +09:00
parent 771120778b
commit 1eecccc21e
124 changed files with 15444 additions and 1629 deletions
+22
View File
@@ -1227,6 +1227,20 @@ pub trait ServerApi {
#[path] workspace_id: String,
) -> Result<WorkspacePromptProjection, RepositoryApiError>;
#[post(
"/api/w/{workspace_id}/config/source-tree/preview",
status = 200,
error_status = 400,
additional_error_statuses = [401, 403, 404, 409, 500],
bearer_auth = true,
browser_auth = true
)]
async fn workspace_config_tree_preview(
&self,
#[path] workspace_id: String,
#[body] request: ConfigCommitRequest,
) -> Result<WorkspaceConfigTreeResponse, RepositoryApiError>;
#[post(
"/api/w/{workspace_id}/config/source-tree/commit",
status = 201,
@@ -8159,6 +8173,9 @@ pub struct TicketAssignmentSummary {
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
#[cfg_attr(feature = "typescript", ts(tag = "kind", rename_all = "snake_case"))]
pub enum TicketAssignmentPrincipal {
Subject {
subject_id: String,
},
User {
account_id: String,
},
@@ -12876,6 +12893,11 @@ mod tests {
HttpMethod::Get,
"/api/w/{workspace_id}/config/projections/prompts",
),
(
"workspace_config_tree_preview",
HttpMethod::Post,
"/api/w/{workspace_id}/config/source-tree/preview",
),
(
"workspace_config_tree_commit",
HttpMethod::Post,
@@ -0,0 +1,45 @@
use server_api::{SetTicketRoleAssignmentRequest, TicketAssignmentPrincipal, TicketQueryRequest};
#[test]
fn subject_assignment_and_query_use_the_same_exact_principal_identity() {
for subject_id in ["subject-a", "subject-b"] {
let principal = serde_json::json!({ "kind": "subject", "subject_id": subject_id });
let assign: SetTicketRoleAssignmentRequest = serde_json::from_value(serde_json::json!({
"operation_id": "assign-subject", "expected_assignment_id": null, "principal": principal
}))
.unwrap();
assert!(
matches!(assign.principal, TicketAssignmentPrincipal::Subject { subject_id: ref id } if id == subject_id)
);
let query: TicketQueryRequest = serde_json::from_value(serde_json::json!({
"assigned_principal": principal
}))
.unwrap();
assert_eq!(
serde_json::to_value(query.assigned_principal).unwrap(),
principal
);
assert_eq!(serde_json::to_value(assign.principal).unwrap(), principal);
}
}
#[test]
fn assignment_principal_keeps_legacy_audit_and_rejects_subject_identity_spoofing() {
for principal in [
serde_json::json!({ "kind": "user", "account_id": "alice" }),
serde_json::json!({ "kind": "worker", "runtime_id": "runtime", "worker_id": "worker" }),
serde_json::json!({ "kind": "workspace_agent", "agent_key": "workspace-orchestrator" }),
] {
let retained: TicketAssignmentPrincipal =
serde_json::from_value(principal.clone()).unwrap();
assert_eq!(serde_json::to_value(retained).unwrap(), principal);
}
for malformed in [
serde_json::json!({ "kind": "subject" }),
serde_json::json!({ "kind": "subject", "subject_id": 1 }),
serde_json::json!({ "kind": "subject", "subject_id": "a", "worker_id": "old-body" }),
serde_json::json!({ "kind": "subject", "subject_id": "a", "agent_key": "workspace-orchestrator" }),
] {
assert!(serde_json::from_value::<TicketAssignmentPrincipal>(malformed).is_err());
}
}