fix: enforce ServerApi as the wire authority
This commit is contained in:
+214
-118
@@ -537,8 +537,13 @@ pub trait ServerApi {
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] operation_id: String,
|
||||
) -> Result<WorkspaceDeletionOperationResponse, RepositoryApiError>;
|
||||
|
||||
#[get("/api/workspace", status = 200, error_status = 400)]
|
||||
#[get(
|
||||
"/api/workspace",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
bearer_auth = true,
|
||||
browser_auth = true
|
||||
)]
|
||||
async fn workspace_current(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -833,12 +838,13 @@ pub trait ServerApi {
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
) -> Result<WorkspaceConfigTreeResponse, RepositoryApiError>;
|
||||
|
||||
#[get(
|
||||
"/api/w/{workspace_id}/config/projections/prompts",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 500]
|
||||
additional_error_statuses = [401, 403, 404, 500],
|
||||
bearer_auth = true,
|
||||
browser_auth = true,
|
||||
)]
|
||||
async fn workspace_prompt_projection(
|
||||
&self,
|
||||
@@ -899,47 +905,51 @@ pub trait ServerApi {
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
) -> Result<ProfileSettingsResponse, RepositoryApiError>;
|
||||
|
||||
#[get(
|
||||
"/api/w/{workspace_id}/flows",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 500]
|
||||
additional_error_statuses = [401, 403, 404, 500],
|
||||
bearer_auth = true,
|
||||
browser_auth = true,
|
||||
)]
|
||||
async fn flow_list(
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
) -> Result<FlowSourceListResponse, RepositoryApiError>;
|
||||
|
||||
#[put(
|
||||
"/api/w/{workspace_id}/flows",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500]
|
||||
additional_error_statuses = [401, 403, 404, 409, 500],
|
||||
bearer_auth = true,
|
||||
browser_auth = true,
|
||||
)]
|
||||
async fn flow_put(
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: PutFlowRequest,
|
||||
) -> Result<FlowSourceRecord, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/flows/resolve",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 500]
|
||||
additional_error_statuses = [401, 403, 404, 500],
|
||||
bearer_auth = true,
|
||||
browser_auth = true,
|
||||
)]
|
||||
async fn flow_resolve(
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: FlowSourceResolveRequest,
|
||||
) -> Result<ResolvedFlowSource, RepositoryApiError>;
|
||||
|
||||
#[get(
|
||||
"/api/w/{workspace_id}/flows/{flow_id}",
|
||||
status = 200,
|
||||
error_status = 404,
|
||||
additional_error_statuses = [400, 401, 403, 500]
|
||||
additional_error_statuses = [400, 401, 403, 500],
|
||||
bearer_auth = true,
|
||||
browser_auth = true,
|
||||
)]
|
||||
async fn flow_get(
|
||||
&self,
|
||||
@@ -973,24 +983,26 @@ pub trait ServerApi {
|
||||
#[path] workspace_id: String,
|
||||
#[query] query: MemoryStagingQuery,
|
||||
) -> Result<MemoryStagingListResponse, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/memory/backend",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 500]
|
||||
additional_error_statuses = [401, 403, 404, 500],
|
||||
bearer_auth = true,
|
||||
browser_auth = true,
|
||||
)]
|
||||
async fn memory_backend(
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: MemoryBackendRequest,
|
||||
) -> Result<MemoryBackendResponse, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/memory/consolidation",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503]
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
|
||||
bearer_auth = true,
|
||||
browser_auth = true,
|
||||
)]
|
||||
async fn memory_consolidation(
|
||||
&self,
|
||||
@@ -1037,12 +1049,13 @@ pub trait ServerApi {
|
||||
#[path] workspace_id: String,
|
||||
#[path] name: String,
|
||||
) -> Result<SkillDetailResponse, RepositoryApiError>;
|
||||
|
||||
#[get(
|
||||
"/api/w/{workspace_id}/skills/{name}/activate",
|
||||
status = 200,
|
||||
error_status = 404,
|
||||
additional_error_statuses = [400, 401, 403, 409, 500]
|
||||
additional_error_statuses = [400, 401, 403, 409, 500],
|
||||
bearer_auth = true,
|
||||
browser_auth = true,
|
||||
)]
|
||||
async fn skill_activate(
|
||||
&self,
|
||||
@@ -1112,24 +1125,24 @@ pub trait ServerApi {
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
) -> Result<BrowserWorkspaceOrchestratorResponse, RepositoryApiError>;
|
||||
|
||||
#[get(
|
||||
"/api/w/{workspace_id}/worker-control/workers",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 500]
|
||||
additional_error_statuses = [401, 403, 404, 500],
|
||||
openapi = false,
|
||||
)]
|
||||
async fn worker_control_list(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
) -> Result<WorkerControlListResponse, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/worker-control/workers",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503]
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
|
||||
openapi = false,
|
||||
)]
|
||||
async fn worker_control_spawn(
|
||||
&self,
|
||||
@@ -1137,12 +1150,12 @@ pub trait ServerApi {
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: CreateWorkspaceWorkerRequest,
|
||||
) -> Result<BrowserCreateWorkerResponse, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/input",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503]
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
|
||||
openapi = false,
|
||||
)]
|
||||
async fn worker_control_input(
|
||||
&self,
|
||||
@@ -1152,12 +1165,12 @@ pub trait ServerApi {
|
||||
#[path] worker_id: String,
|
||||
#[body] request: RuntimeWorkerInputRequest,
|
||||
) -> Result<RuntimeWorkerInputResult, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/cancel",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503]
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
|
||||
openapi = false,
|
||||
)]
|
||||
async fn worker_control_cancel(
|
||||
&self,
|
||||
@@ -1167,12 +1180,12 @@ pub trait ServerApi {
|
||||
#[path] worker_id: String,
|
||||
#[body] request: RuntimeWorkerLifecycleRequest,
|
||||
) -> Result<RuntimeWorkerLifecycleResult, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/stop",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503, 504]
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503, 504],
|
||||
openapi = false,
|
||||
)]
|
||||
async fn worker_control_stop(
|
||||
&self,
|
||||
@@ -1182,12 +1195,12 @@ pub trait ServerApi {
|
||||
#[path] worker_id: String,
|
||||
#[body] request: RuntimeWorkerLifecycleRequest,
|
||||
) -> Result<RuntimeWorkerLifecycleResult, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/worker-control/workers/{runtime_id}/{worker_id}/restore",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503, 504]
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503, 504],
|
||||
openapi = false,
|
||||
)]
|
||||
async fn worker_control_restore(
|
||||
&self,
|
||||
@@ -1196,26 +1209,24 @@ pub trait ServerApi {
|
||||
#[path] runtime_id: String,
|
||||
#[path] worker_id: String,
|
||||
) -> Result<WorkerRestoreResponse, RepositoryApiError>;
|
||||
|
||||
#[get(
|
||||
"/api/w/{workspace_id}/worker-observation/sessions",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 500],
|
||||
openapi = false
|
||||
openapi = false,
|
||||
)]
|
||||
async fn worker_observation_sessions(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
) -> Result<WorkerObservationSessionsResponse, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/worker-observation/session",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 500, 502, 504],
|
||||
openapi = false
|
||||
openapi = false,
|
||||
)]
|
||||
async fn worker_observation_capture(
|
||||
&self,
|
||||
@@ -1223,12 +1234,12 @@ pub trait ServerApi {
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: WorkerObservationSubjectRef,
|
||||
) -> Result<WorkerObservationCaptureResponse, RepositoryApiError>;
|
||||
|
||||
#[get(
|
||||
"/api/w/{workspace_id}/worker-discovery/workers",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 500]
|
||||
additional_error_statuses = [401, 403, 404, 500],
|
||||
openapi = false,
|
||||
)]
|
||||
async fn workspace_worker_discovery(
|
||||
&self,
|
||||
@@ -1303,12 +1314,12 @@ pub trait ServerApi {
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
) -> Result<WorkerLaunchOptionsResponse, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/runtime/v1/workspaces/{workspace_id}/resources/fetch",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 404, 410, 502, 504]
|
||||
additional_error_statuses = [401, 404, 410, 502, 504],
|
||||
openapi = false,
|
||||
)]
|
||||
async fn runtime_resource_fetch(
|
||||
&self,
|
||||
@@ -1420,12 +1431,12 @@ pub trait ServerApi {
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: CompanionCancelRequest,
|
||||
) -> Result<CompanionMessageResponse, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/workers/remove",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 503]
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 503],
|
||||
openapi = false,
|
||||
)]
|
||||
async fn workspace_worker_remove(
|
||||
&self,
|
||||
@@ -2214,13 +2225,12 @@ pub trait ServerApi {
|
||||
#[path] working_directory_id: String,
|
||||
#[body] request: WorkingDirectoryRemovalRequest,
|
||||
) -> Result<WorkingDirectoryRemovalResponse, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/workers/self/workdir-attachments",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
|
||||
bearer_auth = true
|
||||
openapi = false,
|
||||
)]
|
||||
async fn current_worker_workdir_attach(
|
||||
&self,
|
||||
@@ -2228,13 +2238,12 @@ pub trait ServerApi {
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: CurrentWorkerWorkdirAttachRequest,
|
||||
) -> Result<CurrentWorkerWorkdirAttachmentResponse, RepositoryApiError>;
|
||||
|
||||
#[delete(
|
||||
"/api/w/{workspace_id}/workers/self/workdir-attachments/{alias}",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
|
||||
bearer_auth = true
|
||||
openapi = false,
|
||||
)]
|
||||
async fn current_worker_workdir_detach(
|
||||
&self,
|
||||
@@ -2242,13 +2251,12 @@ pub trait ServerApi {
|
||||
#[path] workspace_id: String,
|
||||
#[path] alias: String,
|
||||
) -> Result<CurrentWorkerWorkdirAttachmentResponse, RepositoryApiError>;
|
||||
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/workers/self/workdir-session/operations",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
|
||||
bearer_auth = true
|
||||
openapi = false,
|
||||
)]
|
||||
async fn current_worker_workdir_operation(
|
||||
&self,
|
||||
@@ -2359,8 +2367,7 @@ pub trait ServerApi {
|
||||
#[path] working_directory_id: String,
|
||||
#[body] request: WorkingDirectoryRemovalRequest,
|
||||
) -> Result<WorkingDirectoryRemovalResponse, RepositoryApiError>;
|
||||
|
||||
#[get("/api/tickets", status = 200, error_status = 400, additional_error_statuses = [500])]
|
||||
#[get("/api/tickets", status = 200, error_status = 400, additional_error_statuses = [500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_list_alias(
|
||||
&self,
|
||||
#[query] query: TicketListHttpQuery,
|
||||
@@ -2387,61 +2394,53 @@ pub trait ServerApi {
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: TicketQueryRequest,
|
||||
) -> Result<TicketQueryResponse, RepositoryApiError>;
|
||||
|
||||
#[get("/api/tickets/{id}", status = 200, error_status = 404, additional_error_statuses = [400, 500])]
|
||||
#[get("/api/tickets/{id}", status = 200, error_status = 404, additional_error_statuses = [400, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_get_alias(
|
||||
&self,
|
||||
#[path] id: String,
|
||||
) -> Result<TicketDetail, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/default-intake-ready-body", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/default-intake-ready-body", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_default_intake_ready_body(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: DefaultIntakeReadyBodyRequest,
|
||||
) -> Result<TextResponse, RepositoryApiError>;
|
||||
|
||||
#[get("/api/w/{workspace_id}/tickets/search", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[get("/api/w/{workspace_id}/tickets/search", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_summary_search(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
#[query] query: TicketSummarySearchQuery,
|
||||
) -> Result<TicketRecordSummaryList, RepositoryApiError>;
|
||||
|
||||
#[get("/api/w/{workspace_id}/tickets/doctor", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[get("/api/w/{workspace_id}/tickets/doctor", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_doctor(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
) -> Result<TicketDoctorResponse, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/relations/search", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/relations/search", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_relation_query(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: TicketRelationSearchRequest,
|
||||
) -> Result<TicketRelationRecordList, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/orchestration-plans/search", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/orchestration-plans/search", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_orchestration_plan_query(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: TicketOrchestrationPlanSearchRequest,
|
||||
) -> Result<TicketOrchestrationPlanRecordList, RepositoryApiError>;
|
||||
|
||||
#[get("/api/w/{workspace_id}/tickets/{id}/record", status = 200, error_status = 404, additional_error_statuses = [400, 401, 403, 500])]
|
||||
#[get("/api/w/{workspace_id}/tickets/{id}/record", status = 200, error_status = 404, additional_error_statuses = [400, 401, 403, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_record_get(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
#[path] id: String,
|
||||
) -> Result<TicketRecord, RepositoryApiError>;
|
||||
|
||||
#[patch("/api/w/{workspace_id}/tickets/{id}/item", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[patch("/api/w/{workspace_id}/tickets/{id}/item", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_record_item_edit(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2449,16 +2448,14 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: EditTicketRecordItemRequest,
|
||||
) -> Result<TicketRecord, RepositoryApiError>;
|
||||
|
||||
#[get("/api/w/{workspace_id}/tickets/{id}/dependency-check", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[get("/api/w/{workspace_id}/tickets/{id}/dependency-check", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_dependency_check(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
#[path] id: String,
|
||||
) -> Result<TicketDependencyCheckResponse, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/thread-events", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/thread-events", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_thread_event_add(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2466,8 +2463,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: TicketThreadEventRequest,
|
||||
) -> Result<(), RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/state-changes", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/state-changes", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_state_change_add(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2475,8 +2471,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: TicketStateChangeRequest,
|
||||
) -> Result<(), RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/intake-summaries", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/intake-summaries", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_intake_summary_add(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2484,8 +2479,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: TicketIntakeSummaryRequest,
|
||||
) -> Result<(), RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/state-fields/{field}", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/state-fields/{field}", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_state_field_set(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2494,8 +2488,7 @@ pub trait ServerApi {
|
||||
#[path] field: String,
|
||||
#[body] request: TicketStateChangeRequest,
|
||||
) -> Result<(), RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/workflow-state", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/workflow-state", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_workflow_state_set(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2503,8 +2496,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: TicketStateChangeRequest,
|
||||
) -> Result<(), RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/workflow/mark-ready", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/workflow/mark-ready", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_mark_ready_record(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2512,8 +2504,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: TicketMarkReadyRequest,
|
||||
) -> Result<TicketRecord, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/workflow/queue", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/workflow/queue", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_queue_record(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2535,8 +2526,7 @@ pub trait ServerApi {
|
||||
#[path] merge_request_id: String,
|
||||
#[query] query: MergeRequestThreadQuery,
|
||||
) -> Result<MergeRequestDetailResponse, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], openapi = false)]
|
||||
async fn merge_request_open(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2544,23 +2534,20 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: OpenMergeRequestRequest,
|
||||
) -> Result<PublicMergeRequest, RepositoryApiError>;
|
||||
|
||||
#[get("/api/w/{workspace_id}/tickets/{id}/merge-request/readiness", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[get("/api/w/{workspace_id}/tickets/{id}/merge-request/readiness", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn merge_request_readiness(
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
#[path] id: String,
|
||||
) -> Result<MergeRequestReadinessResponse, RepositoryApiError>;
|
||||
|
||||
#[get("/api/w/{workspace_id}/tickets/{id}/merge-request/thread", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[get("/api/w/{workspace_id}/tickets/{id}/merge-request/thread", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn merge_request_thread(
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
#[path] id: String,
|
||||
#[query] query: MergeRequestThreadQuery,
|
||||
) -> Result<MergeRequestThreadResponse, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request/repair-source", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request/repair-source", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], browser_auth = true)]
|
||||
async fn merge_request_selector_repair(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2568,16 +2555,14 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: RepairMergeRequestSelectorRequest,
|
||||
) -> Result<PublicMergeRequest, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/internal/reviewer-child-sessions", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/internal/reviewer-child-sessions", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], openapi = false)]
|
||||
async fn merge_request_reviewer_child_register(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: RegisterReviewerChildSessionRequest,
|
||||
) -> Result<(), RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request/review-capabilities", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request/review-capabilities", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], openapi = false)]
|
||||
async fn merge_request_review_capability_register(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2585,16 +2570,14 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: RegisterMergeRequestReviewCapabilityRequest,
|
||||
) -> Result<(), RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request/reviews", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request/reviews", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], openapi = false)]
|
||||
async fn merge_request_review_submit(
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
#[path] id: String,
|
||||
#[body] request: SubmitMergeRequestReviewRequest,
|
||||
) -> Result<ReviewEvent, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request/reviews/revoke", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request/reviews/revoke", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], openapi = false)]
|
||||
async fn merge_request_review_revoke(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2602,8 +2585,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: RevokeMergeRequestReviewRequest,
|
||||
) -> Result<ReviewRevokedEvent, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request/complete", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/merge-request/complete", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], openapi = false)]
|
||||
async fn merge_request_complete(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2611,8 +2593,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: CompleteMergeRequestRequest,
|
||||
) -> Result<MergeEvent, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/workflow/close", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/workflow/close", status = 204, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_close_record(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2620,16 +2601,14 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: TicketCloseRecordRequest,
|
||||
) -> Result<(), RepositoryApiError>;
|
||||
|
||||
#[get("/api/w/{workspace_id}/tickets/{id}/relation-view", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[get("/api/w/{workspace_id}/tickets/{id}/relation-view", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_relation_view(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
#[path] id: String,
|
||||
) -> Result<TicketRelationRecordView, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/relations", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/relations", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_relation_record(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2637,8 +2616,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: CreateTicketRelationRequest,
|
||||
) -> Result<TicketRelationRecord, RepositoryApiError>;
|
||||
|
||||
#[delete("/api/w/{workspace_id}/tickets/{id}/relations", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[delete("/api/w/{workspace_id}/tickets/{id}/relations", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_relation_remove(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2646,8 +2624,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: TicketRelationRemoveRequest,
|
||||
) -> Result<TicketRelationRecord, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/orchestration-plans", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/orchestration-plans", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 409, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_orchestration_plan_record(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
@@ -2670,8 +2647,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: BrowserEditTicketRequest,
|
||||
) -> Result<TicketDetail, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/show", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[post("/api/w/{workspace_id}/tickets/{id}/show", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn ticket_show(
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
@@ -2751,8 +2727,7 @@ pub trait ServerApi {
|
||||
#[path] id: String,
|
||||
#[body] request: BrowserCloseTicketRequest,
|
||||
) -> Result<TicketDetail, RepositoryApiError>;
|
||||
|
||||
#[get("/api/objectives", status = 200, error_status = 400, additional_error_statuses = [500])]
|
||||
#[get("/api/objectives", status = 200, error_status = 400, additional_error_statuses = [500], bearer_auth = true, browser_auth = true)]
|
||||
async fn objective_list_alias(
|
||||
&self,
|
||||
#[query] query: ObjectiveListQuery,
|
||||
@@ -2771,15 +2746,13 @@ pub trait ServerApi {
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: ObjectiveCreateRequest,
|
||||
) -> Result<ObjectiveDetail, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/objectives/query", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[post("/api/w/{workspace_id}/objectives/query", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn objective_query(
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: ObjectiveQueryRequest,
|
||||
) -> Result<ObjectiveQueryResponse, RepositoryApiError>;
|
||||
|
||||
#[get("/api/objectives/{id}", status = 200, error_status = 404, additional_error_statuses = [400, 500])]
|
||||
#[get("/api/objectives/{id}", status = 200, error_status = 404, additional_error_statuses = [400, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn objective_get_alias(
|
||||
&self,
|
||||
#[path] id: String,
|
||||
@@ -2799,8 +2772,7 @@ pub trait ServerApi {
|
||||
#[path] objective_id: String,
|
||||
#[body] request: ObjectiveEditRequest,
|
||||
) -> Result<ObjectiveDetail, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/objectives/{objective_id}/show", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500])]
|
||||
#[post("/api/w/{workspace_id}/objectives/{objective_id}/show", status = 200, error_status = 400, additional_error_statuses = [401, 403, 404, 500], bearer_auth = true, browser_auth = true)]
|
||||
async fn objective_show(
|
||||
&self,
|
||||
#[path] workspace_id: String,
|
||||
@@ -10555,6 +10527,130 @@ mod openapi_artifact_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_api_openapi_security_matches_workspace_server_auth_semantics() {
|
||||
const PUBLIC_UNAUTHENTICATED: &[&str] = &[
|
||||
"health",
|
||||
"auth_config",
|
||||
"auth_bootstrap_user",
|
||||
"auth_passkey_registration_options",
|
||||
"auth_passkey_login_options",
|
||||
"auth_device_login_start",
|
||||
"auth_device_login_poll",
|
||||
"auth_whoami",
|
||||
];
|
||||
// These operations are authenticated by Runtime request-source proof, Worker source
|
||||
// identity, or a one-use Reviewer capability. They remain generated Rust client/Axum
|
||||
// operations, but must not appear as unauthenticated operations in the public OpenAPI.
|
||||
const SIGNED_INTERNAL: &[&str] = &[
|
||||
"current_worker_workdir_attach",
|
||||
"current_worker_workdir_detach",
|
||||
"current_worker_workdir_operation",
|
||||
"merge_request_complete",
|
||||
"merge_request_open",
|
||||
"merge_request_review_capability_register",
|
||||
"merge_request_review_revoke",
|
||||
"merge_request_review_submit",
|
||||
"merge_request_reviewer_child_register",
|
||||
"runtime_resource_fetch",
|
||||
"worker_control_cancel",
|
||||
"worker_control_input",
|
||||
"worker_control_list",
|
||||
"worker_control_restore",
|
||||
"worker_control_spawn",
|
||||
"worker_control_stop",
|
||||
"worker_observation_capture",
|
||||
"worker_observation_sessions",
|
||||
"workspace_worker_discovery",
|
||||
"workspace_worker_remove",
|
||||
];
|
||||
// Session observation retains its existing explicit non-OpenAPI boundary independently of
|
||||
// the Runtime/Worker-source signed operation inventory above.
|
||||
const OTHER_OPENAPI_EXCLUDED: &[&str] = &["worker_session"];
|
||||
const BROWSER_ONLY: &[&str] = &["merge_request_selector_repair"];
|
||||
|
||||
let document = canonical_openapi_document().expect("canonical OpenAPI contract must build");
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(&document.to_json().expect("document must serialize"))
|
||||
.expect("document must be JSON");
|
||||
let paths = value["paths"]
|
||||
.as_object()
|
||||
.expect("OpenAPI paths must exist");
|
||||
let mut documented = std::collections::BTreeMap::new();
|
||||
for path in paths.values() {
|
||||
for operation in path
|
||||
.as_object()
|
||||
.expect("OpenAPI path item must be an object")
|
||||
.values()
|
||||
{
|
||||
let operation_id = operation["operationId"]
|
||||
.as_str()
|
||||
.expect("OpenAPI operationId must be a string");
|
||||
assert!(
|
||||
documented.insert(operation_id, operation).is_none(),
|
||||
"duplicate OpenAPI operationId {operation_id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let operations = ServerApiMetadata::OPERATIONS;
|
||||
assert_eq!(
|
||||
documented.len() + SIGNED_INTERNAL.len() + OTHER_OPENAPI_EXCLUDED.len(),
|
||||
operations.len(),
|
||||
"every ServerApi operation must have one bounded OpenAPI auth disposition"
|
||||
);
|
||||
for operation in operations {
|
||||
let operation_id = operation.operation_id;
|
||||
if SIGNED_INTERNAL.contains(&operation_id)
|
||||
|| OTHER_OPENAPI_EXCLUDED.contains(&operation_id)
|
||||
{
|
||||
assert!(
|
||||
!documented.contains_key(operation_id),
|
||||
"internal/excluded operation {operation_id} must not appear public in OpenAPI"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let documented_operation = documented
|
||||
.get(operation_id)
|
||||
.unwrap_or_else(|| panic!("missing public OpenAPI operation {operation_id}"));
|
||||
let expected = if PUBLIC_UNAUTHENTICATED.contains(&operation_id) {
|
||||
&[][..]
|
||||
} else if BROWSER_ONLY.contains(&operation_id) {
|
||||
&["browserSession"][..]
|
||||
} else {
|
||||
&["bearerAuth", "browserSession"][..]
|
||||
};
|
||||
let security = documented_operation.get("security");
|
||||
if expected.is_empty() {
|
||||
assert!(
|
||||
security.is_none(),
|
||||
"public unauthenticated operation {operation_id} must not require security"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let security = security
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.unwrap_or_else(|| panic!("authenticated operation {operation_id} lacks security"));
|
||||
let mut actual = security
|
||||
.iter()
|
||||
.map(|requirement| {
|
||||
let requirement = requirement.as_object().unwrap_or_else(|| {
|
||||
panic!("security requirement for {operation_id} must be an object")
|
||||
});
|
||||
assert_eq!(
|
||||
requirement.len(),
|
||||
1,
|
||||
"security alternatives for {operation_id} must remain independent OR choices"
|
||||
);
|
||||
requirement.keys().next().expect("one security scheme").as_str()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
actual.sort_unstable();
|
||||
assert_eq!(actual, expected, "security mismatch for {operation_id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_and_workspace_catalog_contract_is_strict_and_excludes_cookie_routes() {
|
||||
let document = canonical_openapi_document().expect("canonical OpenAPI contract must build");
|
||||
|
||||
@@ -662,27 +662,6 @@ fn validate_workdir_operations(
|
||||
request: Some("WorkingDirectoryRemovalRequest"),
|
||||
successes: &[("200", "WorkingDirectoryRemovalResponse")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/workers/self/workdir-attachments",
|
||||
method: "post",
|
||||
operation_id: "current_worker_workdir_attach",
|
||||
request: Some("CurrentWorkerWorkdirAttachRequest"),
|
||||
successes: &[("200", "CurrentWorkerWorkdirAttachmentResponse")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/workers/self/workdir-attachments/{alias}",
|
||||
method: "delete",
|
||||
operation_id: "current_worker_workdir_detach",
|
||||
request: None,
|
||||
successes: &[("200", "CurrentWorkerWorkdirAttachmentResponse")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/workers/self/workdir-session/operations",
|
||||
method: "post",
|
||||
operation_id: "current_worker_workdir_operation",
|
||||
request: Some("CurrentWorkerWorkdirOperationRequest"),
|
||||
successes: &[("200", "WorkdirSessionOperationResult")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/external-workdir-grants",
|
||||
method: "post",
|
||||
@@ -1140,13 +1119,10 @@ fn validate_operation_contracts(
|
||||
&format!("{} response {status}", operation.operation_id),
|
||||
)?;
|
||||
let schema = json_content_schema(response, operation.operation_id)?;
|
||||
let expected = successes.get(status.as_str()).copied().unwrap_or_else(|| {
|
||||
if operation.operation_id == "current_worker_workdir_operation" {
|
||||
"WorkdirOperationApiError"
|
||||
} else {
|
||||
"RepositoryApiError"
|
||||
}
|
||||
});
|
||||
let expected = successes
|
||||
.get(status.as_str())
|
||||
.copied()
|
||||
.unwrap_or("RepositoryApiError");
|
||||
require_schema_ref(schema, expected, operation.operation_id)?;
|
||||
roots.insert(expected.to_owned());
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ use crate::records::{
|
||||
ObjectiveQueryItem, ObjectiveQueryRequest, ObjectiveQueryResponse, ObjectiveResourceSummary,
|
||||
ObjectiveShowRequest, ObjectiveSummary, ProjectRecordList, QueryPage, TicketActionEligibility,
|
||||
TicketAssignmentPrincipalSummary, TicketAssignmentSummary, TicketDetail, TicketEventDetail,
|
||||
TicketEvidenceEvent, TicketEvidenceSummary, TicketListPageRequest, TicketMergeRequestSummary,
|
||||
TicketQueryItem, TicketQueryRequest, TicketQueryResponse, TicketRelationView,
|
||||
TicketEvidenceEvent, TicketEvidenceSummary, TicketListProjectionRequest,
|
||||
TicketMergeRequestSummary, TicketQueryItem, TicketQueryRequest, TicketQueryResponse,
|
||||
TicketRoleAssignmentSummary, TicketShowRequest, TicketSummary, TicketSummaryPage,
|
||||
summarize_body, truncate_body,
|
||||
summarize_body, ticket_relation_view_from_domain, truncate_body,
|
||||
};
|
||||
use crate::store::{
|
||||
ControlPlaneStore, MemoryDocumentRecord, MemoryStagingRecord, MemoryStagingResolutionRecord,
|
||||
@@ -48,7 +48,7 @@ impl<T> WorkspaceAuthority for T where T: ObjectiveAuthority + TicketAuthority +
|
||||
|
||||
pub trait TicketAuthority {
|
||||
fn list_tickets(&self, limit: usize) -> Result<ProjectRecordList<TicketSummary>>;
|
||||
fn list_ticket_page(&self, request: TicketListPageRequest) -> Result<TicketSummaryPage>;
|
||||
fn list_ticket_page(&self, request: TicketListProjectionRequest) -> Result<TicketSummaryPage>;
|
||||
fn query_tickets(&self, query: TicketQueryRequest) -> Result<TicketQueryResponse>;
|
||||
fn ticket(&self, id: &str) -> Result<TicketDetail>;
|
||||
fn show_ticket(&self, id: &str, query: TicketShowRequest) -> Result<TicketDetail>;
|
||||
@@ -86,14 +86,14 @@ pub trait MemoryAuthority {
|
||||
fn ensure_memory_document(&self) -> Result<MemoryDocument>;
|
||||
fn memory_document(&self) -> Result<MemoryDocument>;
|
||||
fn update_memory_document(&self, body_md: &str) -> Result<MemoryDocument>;
|
||||
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<MemoryStagingEntry>>;
|
||||
fn memory_staging_record(&self, candidate_id: &str) -> Result<MemoryStagingEntry>;
|
||||
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<InternalMemoryStagingEntry>>;
|
||||
fn memory_staging_record(&self, candidate_id: &str) -> Result<InternalMemoryStagingEntry>;
|
||||
fn upsert_memory_staging_record(
|
||||
&self,
|
||||
candidate_id: &str,
|
||||
raw_json: &str,
|
||||
source_path: Option<&str>,
|
||||
) -> Result<MemoryStagingEntry>;
|
||||
) -> Result<InternalMemoryStagingEntry>;
|
||||
fn close_memory_staging_record(
|
||||
&self,
|
||||
candidate_id: &str,
|
||||
@@ -114,7 +114,7 @@ pub struct MemoryDocument {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MemoryStagingEntry {
|
||||
pub struct InternalMemoryStagingEntry {
|
||||
pub candidate_id: String,
|
||||
pub raw_json: String,
|
||||
pub source_path: Option<String>,
|
||||
@@ -966,7 +966,7 @@ impl SqliteWorkspaceAuthority {
|
||||
&ticket.meta.id,
|
||||
)?)
|
||||
.ok_or_else(|| Error::Store(format!("missing resource key for {}", ticket.meta.id)))?;
|
||||
let mut relations: TicketRelationView = ticket.relations.into();
|
||||
let mut relations = ticket_relation_view_from_domain(ticket.relations);
|
||||
for relation in &mut relations.outgoing {
|
||||
relation.target_resource_key = self.store.resource_key(
|
||||
&self.workspace_id,
|
||||
@@ -1073,7 +1073,7 @@ impl TicketAuthority for SqliteWorkspaceAuthority {
|
||||
})
|
||||
}
|
||||
|
||||
fn list_ticket_page(&self, request: TicketListPageRequest) -> Result<TicketSummaryPage> {
|
||||
fn list_ticket_page(&self, request: TicketListProjectionRequest) -> Result<TicketSummaryPage> {
|
||||
let limit = request.limit.unwrap_or(30).clamp(1, 100);
|
||||
let mut states = request.states;
|
||||
states.sort();
|
||||
@@ -1587,7 +1587,7 @@ impl MemoryAuthority for SqliteWorkspaceAuthority {
|
||||
Ok(memory_document_from_record(record))
|
||||
}
|
||||
|
||||
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<MemoryStagingEntry>> {
|
||||
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<InternalMemoryStagingEntry>> {
|
||||
self.store
|
||||
.list_memory_staging_records(&self.workspace_id, limit)
|
||||
.map(|records| {
|
||||
@@ -1598,7 +1598,7 @@ impl MemoryAuthority for SqliteWorkspaceAuthority {
|
||||
})
|
||||
}
|
||||
|
||||
fn memory_staging_record(&self, candidate_id: &str) -> Result<MemoryStagingEntry> {
|
||||
fn memory_staging_record(&self, candidate_id: &str) -> Result<InternalMemoryStagingEntry> {
|
||||
validate_memory_candidate_id(candidate_id)?;
|
||||
self.store
|
||||
.get_memory_staging_record(&self.workspace_id, candidate_id)?
|
||||
@@ -1613,7 +1613,7 @@ impl MemoryAuthority for SqliteWorkspaceAuthority {
|
||||
candidate_id: &str,
|
||||
raw_json: &str,
|
||||
source_path: Option<&str>,
|
||||
) -> Result<MemoryStagingEntry> {
|
||||
) -> Result<InternalMemoryStagingEntry> {
|
||||
validate_memory_candidate_id(candidate_id)?;
|
||||
validate_json_object(raw_json, "raw_json")?;
|
||||
let imported_at = now_rfc3339();
|
||||
@@ -2634,8 +2634,8 @@ fn memory_document_from_record(record: MemoryDocumentRecord) -> MemoryDocument {
|
||||
}
|
||||
}
|
||||
|
||||
fn memory_staging_from_record(record: MemoryStagingRecord) -> MemoryStagingEntry {
|
||||
MemoryStagingEntry {
|
||||
fn memory_staging_from_record(record: MemoryStagingRecord) -> InternalMemoryStagingEntry {
|
||||
InternalMemoryStagingEntry {
|
||||
candidate_id: record.candidate_id,
|
||||
raw_json: record.raw_json,
|
||||
source_path: record.source_path,
|
||||
@@ -3330,7 +3330,7 @@ VALUES ('workspace-test', 'ticket', 4);
|
||||
assert_eq!(incoming_relation.items.len(), 1);
|
||||
assert_eq!(incoming_relation.items[0].id, "00000000001J5");
|
||||
let summary_page = authority
|
||||
.list_ticket_page(TicketListPageRequest {
|
||||
.list_ticket_page(TicketListProjectionRequest {
|
||||
states: vec!["planning".to_string(), "ready".to_string()],
|
||||
limit: Some(1),
|
||||
cursor: None,
|
||||
@@ -3338,7 +3338,7 @@ VALUES ('workspace-test', 'ticket', 4);
|
||||
.unwrap();
|
||||
assert_eq!(summary_page.items.len(), 1);
|
||||
assert!(summary_page.page.has_more);
|
||||
let mismatched_summary_cursor = authority.list_ticket_page(TicketListPageRequest {
|
||||
let mismatched_summary_cursor = authority.list_ticket_page(TicketListProjectionRequest {
|
||||
states: vec!["done".to_string()],
|
||||
limit: Some(1),
|
||||
cursor: summary_page.page.next_cursor,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@ pub mod workspace_signing_identity;
|
||||
mod workspace_subscription;
|
||||
|
||||
pub use authority::{
|
||||
MemoryAuthority, MemoryDocument, MemoryStagingEntry, MemoryStagingResolution,
|
||||
InternalMemoryStagingEntry, MemoryAuthority, MemoryDocument, MemoryStagingResolution,
|
||||
ObjectiveAuthority, SqliteWorkspaceAuthority, TicketAuthority, TicketMergeRevisionSource,
|
||||
WorkspaceAuthority,
|
||||
};
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use server_api::{
|
||||
ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkedTicketSummary, ObjectiveResourceSummary,
|
||||
ObjectiveSummary, QueryPage,
|
||||
InvalidProjectRecord, MergeRequestListItem, MergeRequestListResponse,
|
||||
MergeRequestRefDiagnostic, ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkSummary,
|
||||
ObjectiveLinkedTicketSummary, ObjectiveQueryItem, ObjectiveQueryRequest,
|
||||
ObjectiveQueryResponse, ObjectiveResourceSummary, ObjectiveShowRequest, ObjectiveSummary,
|
||||
QueryPage, TicketActionEligibility,
|
||||
TicketAssignmentPrincipal as TicketAssignmentPrincipalSummary, TicketAssignmentSummary,
|
||||
TicketDetail, TicketDetailDerivedRelation as DerivedTicketRelation,
|
||||
TicketDetailRelation as TicketRelation, TicketDetailRelationBlocker as TicketRelationBlocker,
|
||||
TicketDetailRelationNotice as TicketRelationNotice,
|
||||
TicketDetailRelationView as TicketRelationView, TicketEventDetail, TicketEvidenceEvent,
|
||||
TicketEvidenceSummary, TicketListItemSummary as TicketSummary, TicketListResponse,
|
||||
TicketMergeRequestSummary, TicketQueryItem, TicketQueryRequest, TicketQueryResponse,
|
||||
TicketRoleAssignmentSummary, TicketShowRequest,
|
||||
};
|
||||
|
||||
const SUMMARY_BODY_LIMIT: usize = 240;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ProjectRecordList<T> {
|
||||
pub items: Vec<T>,
|
||||
pub invalid_records: Vec<InvalidProjectRecord>,
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TicketSummaryPage {
|
||||
pub items: Vec<TicketSummary>,
|
||||
pub page: QueryPage,
|
||||
@@ -21,418 +31,67 @@ pub struct TicketSummaryPage {
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct InvalidProjectRecord {
|
||||
pub label: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketSummary {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
pub priority: String,
|
||||
pub updated_at: Option<String>,
|
||||
pub queued_by: Option<String>,
|
||||
pub queued_at: Option<String>,
|
||||
pub workspace_action_priority: String,
|
||||
pub record_source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct TicketListPageRequest {
|
||||
#[serde(default)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct TicketListProjectionRequest {
|
||||
pub states: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub limit: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketListResponse {
|
||||
pub workspace_id: String,
|
||||
pub limit: usize,
|
||||
pub items: Vec<TicketSummary>,
|
||||
pub page: QueryPage,
|
||||
pub invalid_records: Vec<InvalidProjectRecord>,
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketDetail {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
pub readiness: Option<String>,
|
||||
pub priority: String,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub item_revision: String,
|
||||
pub queued_by: Option<String>,
|
||||
pub queued_at: Option<String>,
|
||||
pub repository_key: Option<String>,
|
||||
pub ref_selector: Option<String>,
|
||||
pub risk_flags: Vec<String>,
|
||||
pub body: String,
|
||||
pub body_truncated: bool,
|
||||
pub event_count: usize,
|
||||
pub events: Vec<TicketEventDetail>,
|
||||
pub event_page: QueryPage,
|
||||
pub artifact_count: usize,
|
||||
pub artifacts: Vec<String>,
|
||||
pub relations: TicketRelationView,
|
||||
pub linked_objectives: Vec<ObjectiveLinkSummary>,
|
||||
pub implementation_reports: Vec<TicketEvidenceEvent>,
|
||||
pub assignments: Vec<TicketRoleAssignmentSummary>,
|
||||
pub current_coder: Option<TicketAssignmentSummary>,
|
||||
pub assignment_diagnostics: Vec<String>,
|
||||
pub action_eligibility: TicketActionEligibility,
|
||||
pub merge_request: Option<TicketMergeRequestSummary>,
|
||||
pub evidence: TicketEvidenceSummary,
|
||||
pub resolution: Option<String>,
|
||||
pub record_source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketEventDetail {
|
||||
pub sequence: usize,
|
||||
pub event_ref: String,
|
||||
pub kind: String,
|
||||
pub author: Option<String>,
|
||||
pub at: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub from: Option<String>,
|
||||
pub to: Option<String>,
|
||||
pub reason: Option<String>,
|
||||
pub state_field: Option<String>,
|
||||
pub heading: Option<String>,
|
||||
pub body: Option<String>,
|
||||
pub attributes: std::collections::BTreeMap<String, String>,
|
||||
pub references: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketRelation {
|
||||
pub ticket_id: String,
|
||||
pub kind: String,
|
||||
pub target: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_resource_key: Option<String>,
|
||||
pub note: Option<String>,
|
||||
pub author: String,
|
||||
pub at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DerivedTicketRelation {
|
||||
pub source_ticket: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub source_resource_key: Option<String>,
|
||||
pub inverse_kind: String,
|
||||
pub forward_kind: String,
|
||||
pub note: Option<String>,
|
||||
pub author: String,
|
||||
pub at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketRelationBlocker {
|
||||
pub blocking_ticket: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub blocking_resource_key: Option<String>,
|
||||
pub reason_kind: String,
|
||||
pub relation_kind: String,
|
||||
pub note: Option<String>,
|
||||
pub blocking_state: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketRelationNotice {
|
||||
pub related_ticket: String,
|
||||
pub kind: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketRelationView {
|
||||
pub outgoing: Vec<TicketRelation>,
|
||||
pub incoming: Vec<DerivedTicketRelation>,
|
||||
pub blockers: Vec<TicketRelationBlocker>,
|
||||
pub notices: Vec<TicketRelationNotice>,
|
||||
}
|
||||
|
||||
impl From<ticket::TicketRelationView> for TicketRelationView {
|
||||
fn from(value: ticket::TicketRelationView) -> Self {
|
||||
Self {
|
||||
outgoing: value
|
||||
.outgoing
|
||||
.into_iter()
|
||||
.map(|relation| TicketRelation {
|
||||
ticket_id: relation.ticket_id,
|
||||
kind: relation.kind.as_str().to_string(),
|
||||
target: relation.target,
|
||||
target_resource_key: None,
|
||||
note: relation.note,
|
||||
author: relation.author,
|
||||
at: relation.at,
|
||||
})
|
||||
.collect(),
|
||||
incoming: value
|
||||
.incoming
|
||||
.into_iter()
|
||||
.map(|relation| DerivedTicketRelation {
|
||||
source_ticket: relation.source_ticket,
|
||||
source_resource_key: None,
|
||||
inverse_kind: relation.inverse_kind,
|
||||
forward_kind: relation.forward_kind.as_str().to_string(),
|
||||
note: relation.note,
|
||||
author: relation.author,
|
||||
at: relation.at,
|
||||
})
|
||||
.collect(),
|
||||
blockers: value
|
||||
.blockers
|
||||
.into_iter()
|
||||
.map(|blocker| TicketRelationBlocker {
|
||||
blocking_ticket: blocker.blocking_ticket,
|
||||
blocking_resource_key: None,
|
||||
reason_kind: blocker.reason_kind,
|
||||
relation_kind: blocker.relation_kind.as_str().to_string(),
|
||||
note: blocker.note,
|
||||
blocking_state: blocker.blocking_state.as_str().to_string(),
|
||||
})
|
||||
.collect(),
|
||||
notices: value
|
||||
.notices
|
||||
.into_iter()
|
||||
.map(|notice| TicketRelationNotice {
|
||||
related_ticket: notice.related_ticket,
|
||||
kind: notice.kind.as_str().to_string(),
|
||||
message: notice.message,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
pub(crate) fn ticket_relation_view_from_domain(
|
||||
value: ticket::TicketRelationView,
|
||||
) -> TicketRelationView {
|
||||
TicketRelationView {
|
||||
outgoing: value
|
||||
.outgoing
|
||||
.into_iter()
|
||||
.map(|relation| TicketRelation {
|
||||
ticket_id: relation.ticket_id,
|
||||
kind: relation.kind.as_str().to_string(),
|
||||
target: relation.target,
|
||||
target_resource_key: None,
|
||||
note: relation.note,
|
||||
author: relation.author,
|
||||
at: relation.at,
|
||||
})
|
||||
.collect(),
|
||||
incoming: value
|
||||
.incoming
|
||||
.into_iter()
|
||||
.map(|relation| DerivedTicketRelation {
|
||||
source_ticket: relation.source_ticket,
|
||||
source_resource_key: None,
|
||||
inverse_kind: relation.inverse_kind,
|
||||
forward_kind: relation.forward_kind.as_str().to_string(),
|
||||
note: relation.note,
|
||||
author: relation.author,
|
||||
at: relation.at,
|
||||
})
|
||||
.collect(),
|
||||
blockers: value
|
||||
.blockers
|
||||
.into_iter()
|
||||
.map(|blocker| TicketRelationBlocker {
|
||||
blocking_ticket: blocker.blocking_ticket,
|
||||
blocking_resource_key: None,
|
||||
reason_kind: blocker.reason_kind,
|
||||
relation_kind: blocker.relation_kind.as_str().to_string(),
|
||||
note: blocker.note,
|
||||
blocking_state: blocker.blocking_state.as_str().to_string(),
|
||||
})
|
||||
.collect(),
|
||||
notices: value
|
||||
.notices
|
||||
.into_iter()
|
||||
.map(|notice| TicketRelationNotice {
|
||||
related_ticket: notice.related_ticket,
|
||||
kind: notice.kind.as_str().to_string(),
|
||||
message: notice.message,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ObjectiveLinkSummary {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketEvidenceEvent {
|
||||
pub event_ref: String,
|
||||
pub sequence: usize,
|
||||
pub kind: String,
|
||||
pub at: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub excerpt: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketAssignmentSummary {
|
||||
pub assignment_id: String,
|
||||
pub runtime_id: String,
|
||||
pub worker_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub worker_resource_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketRoleAssignmentSummary {
|
||||
pub assignment_id: String,
|
||||
pub role: String,
|
||||
pub principal: TicketAssignmentPrincipalSummary,
|
||||
pub assigned_by: String,
|
||||
pub assigned_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum TicketAssignmentPrincipalSummary {
|
||||
User {
|
||||
account_id: String,
|
||||
},
|
||||
Worker {
|
||||
runtime_id: String,
|
||||
worker_id: String,
|
||||
},
|
||||
WorkspaceAgent {
|
||||
agent_key: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketActionEligibility {
|
||||
pub can_assign_orchestrator: bool,
|
||||
pub can_unassign_orchestrator: bool,
|
||||
pub can_queue: bool,
|
||||
pub can_start_manual_coder: bool,
|
||||
pub queue_tickets: Vec<String>,
|
||||
pub blockers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketMergeRequestSummary {
|
||||
pub merge_request_id: String,
|
||||
pub repository_key: String,
|
||||
pub state: String,
|
||||
pub review_status: String,
|
||||
pub selector_from: Option<String>,
|
||||
pub selector_to: String,
|
||||
pub updated_at: String,
|
||||
pub current_subject_ref: Option<String>,
|
||||
pub review_subject_ref: Option<String>,
|
||||
pub review_requested_at: Option<String>,
|
||||
pub review_submitted_at: Option<String>,
|
||||
pub review_excerpt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct MergeRequestRefDiagnostic {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct MergeRequestListItem {
|
||||
pub summary: TicketMergeRequestSummary,
|
||||
pub ticket_ids: Vec<String>,
|
||||
pub thread_event_count: usize,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub ref_diagnostics: Vec<MergeRequestRefDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct MergeRequestListResponse {
|
||||
pub items: Vec<MergeRequestListItem>,
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct TicketEvidenceSummary {
|
||||
pub has_merge_request: bool,
|
||||
pub has_current_subject_ref: bool,
|
||||
pub has_review_request: bool,
|
||||
pub has_commit: bool,
|
||||
pub review_status: Option<String>,
|
||||
pub approved_current_subject: bool,
|
||||
pub review_after_rescope: bool,
|
||||
pub unresolved_request_changes: bool,
|
||||
pub complete_for_integration: bool,
|
||||
pub missing: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct TicketQueryRequest {
|
||||
pub query: Option<String>,
|
||||
#[serde(default)]
|
||||
pub states: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub event_kinds: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub evidence: Vec<String>,
|
||||
pub review_status: Option<String>,
|
||||
#[serde(default)]
|
||||
pub attention: Vec<String>,
|
||||
pub related_ticket_id: Option<String>,
|
||||
pub relation_kind: Option<String>,
|
||||
pub linked_objective_id: Option<String>,
|
||||
pub updated_after: Option<String>,
|
||||
pub updated_before: Option<String>,
|
||||
pub sort: Option<String>,
|
||||
pub limit: Option<usize>,
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketQueryItem {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
pub readiness: Option<String>,
|
||||
pub priority: String,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub item_revision: String,
|
||||
pub workspace_action_priority: String,
|
||||
pub matched_fields: Vec<String>,
|
||||
pub snippet: Option<String>,
|
||||
pub matching_event: Option<TicketEvidenceEvent>,
|
||||
pub linked_objective_ids: Vec<String>,
|
||||
pub linked_objective_keys: Vec<String>,
|
||||
pub relation_count: usize,
|
||||
pub blocker_count: usize,
|
||||
pub unresolved_blocker_count: usize,
|
||||
pub unresolved_review_count: usize,
|
||||
pub evidence: TicketEvidenceSummary,
|
||||
pub merge_request: Option<TicketMergeRequestSummary>,
|
||||
pub current_coder: Option<TicketAssignmentSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TicketQueryResponse {
|
||||
pub items: Vec<TicketQueryItem>,
|
||||
pub page: QueryPage,
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct TicketShowRequest {
|
||||
pub event_limit: Option<usize>,
|
||||
pub event_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct ObjectiveQueryRequest {
|
||||
pub query: Option<String>,
|
||||
#[serde(default)]
|
||||
pub states: Vec<String>,
|
||||
pub linked_ticket_id: Option<String>,
|
||||
pub updated_after: Option<String>,
|
||||
pub updated_before: Option<String>,
|
||||
pub sort: Option<String>,
|
||||
pub limit: Option<usize>,
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ObjectiveQueryItem {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
pub title: String,
|
||||
pub state: String,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub matched_fields: Vec<String>,
|
||||
pub snippet: Option<String>,
|
||||
pub linked_ticket_count: usize,
|
||||
pub linked_tickets: Vec<String>,
|
||||
pub linked_ticket_keys: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ObjectiveQueryResponse {
|
||||
pub items: Vec<ObjectiveQueryItem>,
|
||||
pub page: QueryPage,
|
||||
pub record_authority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct ObjectiveShowRequest {
|
||||
pub event_limit: Option<usize>,
|
||||
pub event_cursor: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn summarize_body(body: &str) -> String {
|
||||
let summary = body
|
||||
.lines()
|
||||
|
||||
+1653
-1226
File diff suppressed because it is too large
Load Diff
+240
-71
@@ -7,7 +7,9 @@ use std::time::{Duration, Instant};
|
||||
use chrono::Utc;
|
||||
use client::BackendApiClient;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use server_api::{ExternalWorkdirGrantCreateRequest, ExternalWorkdirGrantResponse};
|
||||
use server_api::{
|
||||
ExternalWorkdirGrantCreateRequest, ExternalWorkdirGrantResponse, ServerApiClient,
|
||||
};
|
||||
use tokio_tungstenite::connect_async_with_config;
|
||||
use tokio_tungstenite::tungstenite::{Message, protocol::WebSocketConfig};
|
||||
use workdir::external::{
|
||||
@@ -95,40 +97,57 @@ fn pin_selected_root_from(
|
||||
ExternalWorkdirRoot::pin(absolute).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn authenticated_server_api_client(
|
||||
backend_client: &BackendApiClient,
|
||||
backend_url: &str,
|
||||
) -> Result<ServerApiClient, String> {
|
||||
// BackendApiClient remains the stored-credential authority used by the
|
||||
// manual provider WebSocket. Reuse that transport's Authorization header
|
||||
// for the generated JSON client instead of loading or interpreting the
|
||||
// credential a second time here.
|
||||
let authenticated_request = backend_client
|
||||
.websocket_request("/")
|
||||
.map_err(|error| error.to_string())?;
|
||||
let authorization = authenticated_request
|
||||
.headers()
|
||||
.get(reqwest::header::AUTHORIZATION)
|
||||
.cloned()
|
||||
.ok_or_else(|| "stored Backend credential did not provide Authorization".to_string())?;
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(reqwest::header::AUTHORIZATION, authorization);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|error| format!("failed to configure Backend client: {error}"))?;
|
||||
let base_url = backend_url
|
||||
.try_into()
|
||||
.map_err(|error| format!("invalid Backend URL: {error}"))?;
|
||||
Ok(ServerApiClient::with_client(base_url, http_client))
|
||||
}
|
||||
|
||||
pub(crate) async fn run(options: WorkdirShareOptions) -> Result<(), String> {
|
||||
// Pin before creating remote authority: the Backend grant can never outlive
|
||||
// a failed local approval/open race, and subsequent path replacement cannot
|
||||
// redirect the provider.
|
||||
let pinned_root = pin_selected_root(&options.path)?;
|
||||
|
||||
let client = BackendApiClient::from_stored_token(&options.backend_url)
|
||||
let backend_client = BackendApiClient::from_stored_token(&options.backend_url)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let server_api_client = authenticated_server_api_client(&backend_client, &options.backend_url)?;
|
||||
let provider_instance_id = format!("cli-{}", uuid::Uuid::now_v7());
|
||||
let create_path = format!(
|
||||
"/api/w/{}/external-workdir-grants",
|
||||
encode_path_segment(&options.workspace_id)
|
||||
);
|
||||
let response = client
|
||||
.request(reqwest::Method::POST, &create_path)
|
||||
.map_err(|error| error.to_string())?
|
||||
.timeout(Duration::from_secs(10))
|
||||
.json(&ExternalWorkdirGrantCreateRequest {
|
||||
provider_instance_id: provider_instance_id.clone(),
|
||||
display_name: options.display_name.clone(),
|
||||
ttl_seconds: options.ttl.as_secs(),
|
||||
read_only: true,
|
||||
})
|
||||
.send()
|
||||
let grant = server_api_client
|
||||
.external_workdir_grant_create(
|
||||
options.workspace_id.clone(),
|
||||
ExternalWorkdirGrantCreateRequest {
|
||||
provider_instance_id: provider_instance_id.clone(),
|
||||
display_name: options.display_name.clone(),
|
||||
ttl_seconds: options.ttl.as_secs(),
|
||||
read_only: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| format!("External Workdir grant request failed: {error}"))?;
|
||||
let response = client
|
||||
.require_success(response)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let grant = response
|
||||
.json::<ExternalWorkdirGrantResponse>()
|
||||
.await
|
||||
.map_err(|error| format!("Backend returned an invalid External Workdir grant: {error}"))?;
|
||||
|
||||
let limits = BoundedReadLimits::EXTERNAL_DEFAULT;
|
||||
let session = LocalWorkdirSession::external_read_only_pinned(
|
||||
@@ -150,7 +169,8 @@ pub(crate) async fn run(options: WorkdirShareOptions) -> Result<(), String> {
|
||||
let mut generation = grant.generation;
|
||||
loop {
|
||||
let outcome = serve_provider_connection(
|
||||
&client,
|
||||
&backend_client,
|
||||
&server_api_client,
|
||||
&options,
|
||||
&grant,
|
||||
&provider_instance_id,
|
||||
@@ -166,11 +186,12 @@ pub(crate) async fn run(options: WorkdirShareOptions) -> Result<(), String> {
|
||||
"External Workdir grant expired after provider connection failure: {connection_error}"
|
||||
));
|
||||
}
|
||||
let refresh = fetch_grant(&client, &options.workspace_id, &grant.grant_id);
|
||||
let refresh =
|
||||
fetch_grant(&server_api_client, &options.workspace_id, &grant.grant_id);
|
||||
let refreshed = tokio::select! {
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal.map_err(|error| format!("failed to wait for Ctrl-C: {error}"))?;
|
||||
revoke(&client, &options.workspace_id, &grant.grant_id).await?;
|
||||
revoke(&server_api_client, &options.workspace_id, &grant.grant_id).await?;
|
||||
println!("External Workdir grant revoked.");
|
||||
return Ok(());
|
||||
}
|
||||
@@ -182,8 +203,12 @@ pub(crate) async fn run(options: WorkdirShareOptions) -> Result<(), String> {
|
||||
eprintln!(
|
||||
"Connection refresh failed; retrying: {connection_error}; {refresh_error}"
|
||||
);
|
||||
if wait_to_reconnect(&client, &options.workspace_id, &grant.grant_id)
|
||||
.await?
|
||||
if wait_to_reconnect(
|
||||
&server_api_client,
|
||||
&options.workspace_id,
|
||||
&grant.grant_id,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
println!("External Workdir grant revoked.");
|
||||
return Ok(());
|
||||
@@ -213,7 +238,9 @@ pub(crate) async fn run(options: WorkdirShareOptions) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
println!("Connection: reconnecting");
|
||||
if wait_to_reconnect(&client, &options.workspace_id, &grant.grant_id).await? {
|
||||
if wait_to_reconnect(&server_api_client, &options.workspace_id, &grant.grant_id)
|
||||
.await?
|
||||
{
|
||||
println!("External Workdir grant revoked.");
|
||||
return Ok(());
|
||||
}
|
||||
@@ -237,7 +264,9 @@ pub(crate) async fn run(options: WorkdirShareOptions) -> Result<(), String> {
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| "External Workdir provider generation overflow".to_string())?;
|
||||
println!("Connection: reconnecting");
|
||||
if wait_to_reconnect(&client, &options.workspace_id, &grant.grant_id).await? {
|
||||
if wait_to_reconnect(&server_api_client, &options.workspace_id, &grant.grant_id)
|
||||
.await?
|
||||
{
|
||||
println!("External Workdir grant revoked.");
|
||||
return Ok(());
|
||||
}
|
||||
@@ -247,7 +276,8 @@ pub(crate) async fn run(options: WorkdirShareOptions) -> Result<(), String> {
|
||||
}
|
||||
|
||||
async fn serve_provider_connection(
|
||||
client: &BackendApiClient,
|
||||
backend_client: &BackendApiClient,
|
||||
server_api_client: &ServerApiClient,
|
||||
options: &WorkdirShareOptions,
|
||||
grant: &ExternalWorkdirGrantResponse,
|
||||
provider_instance_id: &str,
|
||||
@@ -259,7 +289,7 @@ async fn serve_provider_connection(
|
||||
encode_path_segment(&options.workspace_id),
|
||||
encode_path_segment(&grant.grant_id)
|
||||
);
|
||||
let request = client
|
||||
let request = backend_client
|
||||
.websocket_request(&provider_path)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let websocket_config = WebSocketConfig::default()
|
||||
@@ -269,7 +299,7 @@ async fn serve_provider_connection(
|
||||
let (mut socket, _) = tokio::select! {
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal.map_err(|error| format!("failed to wait for Ctrl-C: {error}"))?;
|
||||
revoke(client, &options.workspace_id, &grant.grant_id).await?;
|
||||
revoke(server_api_client, &options.workspace_id, &grant.grant_id).await?;
|
||||
return Ok(ProviderEnd::Interrupted);
|
||||
}
|
||||
connection = connection => connection
|
||||
@@ -292,7 +322,7 @@ async fn serve_provider_connection(
|
||||
let registered = tokio::select! {
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal.map_err(|error| format!("failed to wait for Ctrl-C: {error}"))?;
|
||||
revoke(client, &options.workspace_id, &grant.grant_id).await?;
|
||||
revoke(server_api_client, &options.workspace_id, &grant.grant_id).await?;
|
||||
return Ok(ProviderEnd::Interrupted);
|
||||
}
|
||||
registered = tokio::time::timeout(Duration::from_secs(10), socket.next()) => {
|
||||
@@ -324,7 +354,7 @@ async fn serve_provider_connection(
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal.map_err(|error| format!("failed to wait for Ctrl-C: {error}"))?;
|
||||
stop_operations(&mut operations, &mut completions).await?;
|
||||
revoke(client, &options.workspace_id, &grant.grant_id).await?;
|
||||
revoke(server_api_client, &options.workspace_id, &grant.grant_id).await?;
|
||||
return Ok(ProviderEnd::Interrupted);
|
||||
}
|
||||
completion = completions.recv() => {
|
||||
@@ -558,7 +588,7 @@ where
|
||||
}
|
||||
|
||||
async fn wait_to_reconnect(
|
||||
client: &BackendApiClient,
|
||||
client: &ServerApiClient,
|
||||
workspace_id: &str,
|
||||
grant_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
@@ -573,51 +603,27 @@ async fn wait_to_reconnect(
|
||||
}
|
||||
|
||||
async fn fetch_grant(
|
||||
client: &BackendApiClient,
|
||||
client: &ServerApiClient,
|
||||
workspace_id: &str,
|
||||
grant_id: &str,
|
||||
) -> Result<ExternalWorkdirGrantResponse, String> {
|
||||
let path = format!(
|
||||
"/api/w/{}/external-workdir-grants/{}",
|
||||
encode_path_segment(workspace_id),
|
||||
encode_path_segment(grant_id)
|
||||
);
|
||||
let response = client
|
||||
.request(reqwest::Method::GET, &path)
|
||||
.map_err(|error| error.to_string())?
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("External Workdir grant refresh failed: {error}"))?;
|
||||
client
|
||||
.require_success(response)
|
||||
.external_workdir_grant_get(workspace_id.to_string(), grant_id.to_string())
|
||||
.await
|
||||
.map_err(|error| error.to_string())?
|
||||
.json::<ExternalWorkdirGrantResponse>()
|
||||
.await
|
||||
.map_err(|error| format!("Backend returned an invalid External Workdir grant: {error}"))
|
||||
.map_err(|error| format!("External Workdir grant refresh failed: {error}"))
|
||||
}
|
||||
|
||||
async fn revoke(
|
||||
client: &BackendApiClient,
|
||||
client: &ServerApiClient,
|
||||
workspace_id: &str,
|
||||
grant_id: &str,
|
||||
) -> Result<(), String> {
|
||||
let path = format!(
|
||||
"/api/w/{}/external-workdir-grants/{}",
|
||||
encode_path_segment(workspace_id),
|
||||
encode_path_segment(grant_id)
|
||||
);
|
||||
let response = client
|
||||
.request(reqwest::Method::DELETE, &path)
|
||||
.map_err(|error| error.to_string())?
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
client
|
||||
.external_workdir_grant_revoke(workspace_id.to_string(), grant_id.to_string())
|
||||
.await
|
||||
.map_err(|error| format!("External Workdir revoke request failed: {error}"))?;
|
||||
client.require_success(response).await.map_err(|error| {
|
||||
format!("External Workdir revoke was not confirmed by the Backend: {error}")
|
||||
})?;
|
||||
.map_err(|error| {
|
||||
format!("External Workdir revoke was not confirmed by the Backend: {error}")
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -636,6 +642,169 @@ fn encode_path_segment(value: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::mpsc;
|
||||
|
||||
fn test_server_api_client(base_url: &str) -> ServerApiClient {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
reqwest::header::HeaderValue::from_static("Bearer test-token"),
|
||||
);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap();
|
||||
ServerApiClient::with_client(base_url.try_into().unwrap(), http_client)
|
||||
}
|
||||
|
||||
fn read_http_request(stream: &mut TcpStream) -> String {
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
let mut request = Vec::new();
|
||||
let mut buffer = [0_u8; 4096];
|
||||
let mut expected_length = None;
|
||||
loop {
|
||||
let read = stream.read(&mut buffer).unwrap();
|
||||
assert!(read > 0, "HTTP client closed before completing request");
|
||||
request.extend_from_slice(&buffer[..read]);
|
||||
if expected_length.is_none()
|
||||
&& let Some(header_end) =
|
||||
request.windows(4).position(|window| window == b"\r\n\r\n")
|
||||
{
|
||||
let headers = String::from_utf8_lossy(&request[..header_end]);
|
||||
let content_length = headers.lines().find_map(|line| {
|
||||
let (name, value) = line.split_once(':')?;
|
||||
name.eq_ignore_ascii_case("content-length")
|
||||
.then(|| value.trim().parse::<usize>().unwrap())
|
||||
});
|
||||
expected_length = Some(header_end + 4 + content_length.unwrap_or(0));
|
||||
}
|
||||
if expected_length.is_some_and(|length| request.len() >= length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String::from_utf8(request).unwrap()
|
||||
}
|
||||
|
||||
fn spawn_json_server(
|
||||
response_body: String,
|
||||
response_count: usize,
|
||||
) -> (String, mpsc::Receiver<String>, std::thread::JoinHandle<()>) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let thread = std::thread::spawn(move || {
|
||||
for _ in 0..response_count {
|
||||
let (mut stream, _) = listener.accept().unwrap();
|
||||
let request = read_http_request(&mut stream);
|
||||
let status = if request.starts_with("POST ") {
|
||||
"201 Created"
|
||||
} else {
|
||||
"200 OK"
|
||||
};
|
||||
sender.send(request).unwrap();
|
||||
write!(
|
||||
stream,
|
||||
"HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
response_body.len(),
|
||||
response_body
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
(format!("http://{address}"), receiver, thread)
|
||||
}
|
||||
|
||||
fn grant_response_json() -> String {
|
||||
serde_json::json!({
|
||||
"grant_id": "grant/a b",
|
||||
"workspace_id": "workspace/a b",
|
||||
"working_directory_id": "workdir-a",
|
||||
"provider_instance_id": "provider-a",
|
||||
"display_name": "Shared files",
|
||||
"permissions": "read_only",
|
||||
"expires_at": "2026-09-23T03:00:00Z",
|
||||
"generation": 7,
|
||||
"status": "pending"
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generated_grant_operations_preserve_auth_and_encode_contract_paths() {
|
||||
let (base_url, requests, server) = spawn_json_server(grant_response_json(), 3);
|
||||
let client = test_server_api_client(&base_url);
|
||||
|
||||
let created = client
|
||||
.external_workdir_grant_create(
|
||||
"workspace/a b".to_string(),
|
||||
ExternalWorkdirGrantCreateRequest {
|
||||
provider_instance_id: "provider-a".to_string(),
|
||||
display_name: "Shared files".to_string(),
|
||||
ttl_seconds: 600,
|
||||
read_only: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let fetched = fetch_grant(&client, "workspace/a b", "grant/a b")
|
||||
.await
|
||||
.unwrap();
|
||||
revoke(&client, "workspace/a b", "grant/a b").await.unwrap();
|
||||
|
||||
assert_eq!(created.grant_id, "grant/a b");
|
||||
assert_eq!(fetched.generation, 7);
|
||||
let requests = (0..3)
|
||||
.map(|_| requests.recv_timeout(Duration::from_secs(5)).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
server.join().unwrap();
|
||||
assert!(
|
||||
requests[0]
|
||||
.starts_with("POST /api/w/workspace%2Fa%20b/external-workdir-grants HTTP/1.1\r\n")
|
||||
);
|
||||
assert!(requests[1].starts_with(
|
||||
"GET /api/w/workspace%2Fa%20b/external-workdir-grants/grant%2Fa%20b HTTP/1.1\r\n"
|
||||
));
|
||||
assert!(requests[2].starts_with(
|
||||
"DELETE /api/w/workspace%2Fa%20b/external-workdir-grants/grant%2Fa%20b HTTP/1.1\r\n"
|
||||
));
|
||||
for request in &requests {
|
||||
assert!(
|
||||
request
|
||||
.to_ascii_lowercase()
|
||||
.contains("\r\nauthorization: bearer test-token\r\n")
|
||||
);
|
||||
}
|
||||
assert!(requests[0].contains("\"provider_instance_id\":\"provider-a\""));
|
||||
assert!(requests[0].contains("\"ttl_seconds\":600"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generated_grant_response_is_bounded() {
|
||||
let oversized_body = format!("{{\"padding\":\"{}\"}}", "x".repeat(1_048_576));
|
||||
let (base_url, requests, server) = spawn_json_server(oversized_body, 1);
|
||||
let client = test_server_api_client(&base_url);
|
||||
|
||||
let error = fetch_grant(&client, "workspace-a", "grant-a")
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let request = requests.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
server.join().unwrap();
|
||||
assert!(
|
||||
request
|
||||
.starts_with("GET /api/w/workspace-a/external-workdir-grants/grant-a HTTP/1.1\r\n")
|
||||
);
|
||||
assert!(error.contains("External Workdir grant refresh failed"));
|
||||
assert!(
|
||||
error.contains("response body exceeded 1048576 bytes"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_share_path_is_resolved_and_pinned_before_backend_access() {
|
||||
|
||||
+335
-4173
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Input contract: 0.1.0 (sha256:6b26856793bfd6dd3d34461b3a7422adfdf0fab74c0ebab5ef6ab7fbdf034dfe)
|
||||
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/companion-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Input contract: 0.1.0 (sha256:6b26856793bfd6dd3d34461b3a7422adfdf0fab74c0ebab5ef6ab7fbdf034dfe)
|
||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Input contract: 0.1.0 (sha256:6b26856793bfd6dd3d34461b3a7422adfdf0fab74c0ebab5ef6ab7fbdf034dfe)
|
||||
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/runtime-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Input contract: 0.1.0 (sha256:6b26856793bfd6dd3d34461b3a7422adfdf0fab74c0ebab5ef6ab7fbdf034dfe)
|
||||
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/ticket-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
|
||||
|
||||
@@ -1,84 +1,20 @@
|
||||
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Input contract: 0.1.0 (sha256:6b26856793bfd6dd3d34461b3a7422adfdf0fab74c0ebab5ef6ab7fbdf034dfe)
|
||||
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/workdir-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
|
||||
|
||||
export type CommandOutput = { content: string; exit_code?: number | null; next_cursor?: number | null; output_path?: string | null; status: CommandStatus; timed_out: boolean; truncated: boolean; };
|
||||
|
||||
export type CommandOutputRequest = { cursor: number; handle: string; limit: number; wait: boolean; };
|
||||
|
||||
export type CommandRequest = { command: string; cwd?: FsPath | null; output_limit: number; spill_dir?: string | null; timeout_secs: number; tool_call_id?: string | null; };
|
||||
|
||||
export type CommandStatus = "running" | "completed" | "failed" | "timed_out" | "cancelled";
|
||||
|
||||
export type CurrentWorkerWorkdirAttachRequest = { alias: string; working_directory_id: string; };
|
||||
|
||||
export type CurrentWorkerWorkdirAttachmentResponse = { alias: string; attached: boolean; capabilities: WorkdirSessionCapabilities; working_directory_id: string; workspace_id: string; };
|
||||
|
||||
export type CurrentWorkerWorkdirOperationRequest = { operation: WorkdirSessionOperation; target_workdir: string; };
|
||||
|
||||
export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
|
||||
|
||||
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||
|
||||
export type EditRequest = { expected_hash: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]; new_string: string; old_string: string; path: FsPath; replace_all: boolean; };
|
||||
|
||||
export type EditResult = { bytes_written: number; content_hash: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]; replacements: number; };
|
||||
|
||||
export type EntryKind = "file" | "directory" | "symlink" | "other";
|
||||
|
||||
export type ExternalWorkdirGrantCreateRequest = { display_name: string; provider_instance_id: string; read_only: boolean; ttl_seconds: number; };
|
||||
|
||||
export type ExternalWorkdirGrantResponse = { display_name: string; expires_at: string; generation: number; grant_id: string; permissions: string; provider_instance_id: string; status: string; working_directory_id: string; workspace_id: string; };
|
||||
|
||||
export type FsPath = string;
|
||||
|
||||
export type GlobRequest = { limit: number; path: FsPath; pattern: string; };
|
||||
|
||||
export type GlobResult = { paths: Array<FsPath>; truncated: boolean; };
|
||||
|
||||
export type GrepOutputMode = "content" | "files_with_matches" | "count";
|
||||
|
||||
export type GrepRequest = { after_context: number; before_context: number; case_insensitive: boolean; file_type?: string | null; glob?: string | null; limit: number; multiline: boolean; offset: number; output_mode: GrepOutputMode; path: FsPath; pattern: string; };
|
||||
|
||||
export type GrepResult = { match_count: number; matched_files: number; output: string; truncated: boolean; };
|
||||
|
||||
export type ListEntry = { kind: EntryKind; path: FsPath; size: number; };
|
||||
|
||||
export type ListRequest = { limit: number; path: FsPath; };
|
||||
|
||||
export type ListResult = { entries: Array<ListEntry>; total_bytes: number; total_entries: number; truncated: boolean; };
|
||||
|
||||
export type ReadRequest = { limit: number; max_bytes: number; offset: number; path: FsPath; };
|
||||
|
||||
export type ReadResult = { bytes: Array<number>; content_hash: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]; path: FsPath; start_line: number; total_lines: number; truncated: boolean; };
|
||||
|
||||
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
|
||||
|
||||
export type StatRequest = { path: FsPath; };
|
||||
|
||||
export type StatResult = { kind: EntryKind; path: FsPath; size: number; };
|
||||
|
||||
export type WorkdirOperationApiError = { code?: WorkdirTransportErrorCode | null; diagnostics?: Array<Diagnostic> | null; error?: string | null; message: string; };
|
||||
|
||||
export type WorkdirScopeAuthorizationRequest = { path: FsPath; permission: WorkdirToolScopePermission; rules: Array<WorkdirToolScopeRule>; };
|
||||
|
||||
export type WorkdirScopeOverlapRequest = { left: WorkdirToolScopeRule; right: WorkdirToolScopeRule; };
|
||||
|
||||
export type WorkdirSessionCapabilities = { bits: number; };
|
||||
|
||||
export type WorkdirSessionOperation = { operation: "authorize_scope"; request: WorkdirScopeAuthorizationRequest; } | { operation: "scope_rules_overlap"; request: WorkdirScopeOverlapRequest; } | { operation: "stat"; request: StatRequest; } | { operation: "read"; request: ReadRequest; } | { operation: "write"; request: WriteRequest; } | { operation: "edit"; request: EditRequest; } | { operation: "list"; request: ListRequest; } | { operation: "glob"; request: GlobRequest; } | { operation: "grep"; request: GrepRequest; } | { operation: "command_start"; request: CommandRequest; } | { operation: "command_status"; request: string; } | { operation: "command_output"; request: CommandOutputRequest; } | { operation: "command_cancel"; request: string; };
|
||||
|
||||
export type WorkdirSessionOperationResult = { operation: "authorize_scope"; } | { operation: "scope_rules_overlap"; result: { overlaps: boolean; }; } | { operation: "stat"; result: StatResult; } | { operation: "read"; result: ReadResult; } | { operation: "write"; result: WriteResult; } | { operation: "edit"; result: EditResult; } | { operation: "list"; result: ListResult; } | { operation: "glob"; result: GlobResult; } | { operation: "grep"; result: GrepResult; } | { operation: "command_start"; result: string; } | { operation: "command_status"; result: CommandStatus; } | { operation: "command_output"; result: CommandOutput; } | { operation: "command_cancel"; };
|
||||
|
||||
export type WorkdirToolScopePermission = "read" | "write";
|
||||
|
||||
export type WorkdirToolScopeRule = { permission: WorkdirToolScopePermission; recursive: boolean; symlink_policy?: string; target: FsPath; };
|
||||
|
||||
export type WorkdirTransportErrorCode = "not_found" | "conflict" | "unsupported" | "invalid_request" | "denied" | "out_of_scope" | "symlink_out_of_scope" | "broken_symlink" | "symlink_target_is_directory" | "read_only" | "is_directory" | "symlink_directory_not_traversed" | "unknown_command" | "unavailable" | "io" | "transport" | "internal";
|
||||
|
||||
export type WorkingDirectoryCleanupTarget = { kind: string; repository_key: string; working_directory_id: string; };
|
||||
|
||||
export type WorkingDirectoryCreateRequest = { display_name?: string | null; operation_id?: string | null; repository_key: string; runtime_id?: string | null; selector?: string | null; };
|
||||
@@ -104,7 +40,3 @@ export type WorkingDirectorySource = { kind: "repository"; repository_key: strin
|
||||
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
|
||||
|
||||
export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; };
|
||||
|
||||
export type WriteRequest = { content: Array<number>; expected_hash?: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number] | null; path: FsPath; };
|
||||
|
||||
export type WriteResult = { bytes_written: number; created: boolean; };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:550383b600fe2a2e55630849c7de7a6a24463ad41d0c18dc5d254a3d14328f07)
|
||||
// Input contract: 0.1.0 (sha256:6b26856793bfd6dd3d34461b3a7422adfdf0fab74c0ebab5ef6ab7fbdf034dfe)
|
||||
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
|
||||
|
||||
Reference in New Issue
Block a user