feat: deliver committed Ticket events to plugin services
This commit is contained in:
@@ -16,7 +16,9 @@ mod notification;
|
||||
mod plugin;
|
||||
pub mod plugin_data;
|
||||
pub mod plugin_host;
|
||||
pub mod plugin_service;
|
||||
pub use plugin_data::*;
|
||||
pub use plugin_service::*;
|
||||
mod plugin_types;
|
||||
pub use notification::*;
|
||||
pub use plugin::*;
|
||||
@@ -3094,6 +3096,22 @@ pub trait ServerApi {
|
||||
#[body] request: PluginDataPurgeRequest,
|
||||
) -> Result<PluginDataPurgeResponse, RepositoryApiError>;
|
||||
|
||||
#[get("/api/w/{workspace_id}/plugins/service-consumers/{consumer_id}", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,503], bearer_auth = true, browser_auth = true)]
|
||||
async fn plugin_service_diagnostics(
|
||||
&self,
|
||||
#[extension] actor: RequestActor,
|
||||
#[path] workspace_id: String,
|
||||
#[path] consumer_id: String,
|
||||
) -> Result<PluginServiceDiagnostics, RepositoryApiError>;
|
||||
|
||||
#[post("/api/w/{workspace_id}/plugins/service-consumers/recover", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,503], bearer_auth = true, browser_auth = true, normalize_body_errors = true)]
|
||||
async fn plugin_service_recover(
|
||||
&self,
|
||||
#[extension] actor: RequestActor,
|
||||
#[path] workspace_id: String,
|
||||
#[body] request: PluginServiceRecoveryRequest,
|
||||
) -> Result<PluginServiceDiagnostics, RepositoryApiError>;
|
||||
|
||||
#[get("/api/w/{workspace_id}/plugin-operations/{operation_id}", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,429,503], bearer_auth = true, browser_auth = true)]
|
||||
async fn plugin_operation(
|
||||
&self,
|
||||
|
||||
@@ -101,6 +101,7 @@ pub enum PluginGrantMetadata {
|
||||
slot: String,
|
||||
},
|
||||
TicketRead,
|
||||
TicketEvents,
|
||||
TicketDataRead {
|
||||
collection: String,
|
||||
schema_id: String,
|
||||
@@ -124,6 +125,15 @@ pub struct PluginDataSchemaMetadata {
|
||||
pub schema: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Exact artifact subscription declarations; selection and current permission are separate.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginServiceMetadata {
|
||||
pub name: String,
|
||||
pub subscriptions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -139,6 +149,8 @@ pub struct PluginArtifact {
|
||||
pub world: String,
|
||||
pub required_imports: Vec<String>,
|
||||
pub tools: Vec<PluginToolMetadata>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub services: Vec<PluginServiceMetadata>,
|
||||
pub connections: Vec<PluginConnectionMetadata>,
|
||||
pub requested_grants: Vec<PluginGrantMetadata>,
|
||||
#[serde(default)]
|
||||
@@ -160,6 +172,11 @@ impl plugin_plan::ArtifactDescriptor for PluginArtifact {
|
||||
world: self.world.clone(),
|
||||
required_imports: self.required_imports.clone(),
|
||||
tools: self.tools.iter().map(|tool| tool.name.clone()).collect(),
|
||||
services: self
|
||||
.services
|
||||
.iter()
|
||||
.map(|service| service.name.clone())
|
||||
.collect(),
|
||||
connections: self
|
||||
.connections
|
||||
.iter()
|
||||
@@ -174,6 +191,7 @@ impl plugin_plan::ArtifactDescriptor for PluginArtifact {
|
||||
plugin_plan::PluginGrant::Https { slot: slot.clone() }
|
||||
}
|
||||
PluginGrantMetadata::TicketRead => plugin_plan::PluginGrant::TicketRead,
|
||||
PluginGrantMetadata::TicketEvents => plugin_plan::PluginGrant::TicketEvents,
|
||||
PluginGrantMetadata::TicketDataRead {
|
||||
collection,
|
||||
schema_id,
|
||||
@@ -337,6 +355,7 @@ pub enum PluginDiagnosticPinState {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PluginDiagnosticSurface {
|
||||
Tool,
|
||||
Service,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -351,10 +370,20 @@ pub struct PluginWorkerDiagnostic {
|
||||
pub artifact_size: u32,
|
||||
pub surfaces: Vec<PluginDiagnosticSurface>,
|
||||
pub selected_tools: Vec<String>,
|
||||
pub selected_services: Vec<String>,
|
||||
pub service_consumers: Vec<PluginServiceConsumerDiagnostics>,
|
||||
pub admission: PluginDiagnosticAdmission,
|
||||
pub connections: Vec<PluginDiagnosticConnection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginServiceConsumerDiagnostics {
|
||||
pub service_name: String,
|
||||
pub diagnostics: crate::PluginServiceDiagnostics,
|
||||
}
|
||||
|
||||
/// A successful observation is not retained admission authority.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
|
||||
@@ -3,25 +3,44 @@ use crate::PluginHttpMethod;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum PluginHostRequest {
|
||||
/// Fresh export/restore admission only; issues no invocation or effect identity.
|
||||
Prepare {
|
||||
plan_id: String,
|
||||
installation_id: String,
|
||||
tools: Vec<String>,
|
||||
},
|
||||
Begin {
|
||||
plan_id: String,
|
||||
installation_id: String,
|
||||
consumer: PluginConsumer,
|
||||
},
|
||||
/// Irreversible cancellation of this authenticated logical invocation.
|
||||
Cancel {
|
||||
invocation_id: String,
|
||||
},
|
||||
// The same operation variants define Tool transport and the narrower leased
|
||||
// Service channel. Service cannot nest itself or issue Begin/Cancel, and the
|
||||
// wire schema remains finite rather than recursively admitting control requests.
|
||||
macro_rules! invocation_requests {
|
||||
($($variant:ident { $($field:ident: $ty:ty),* $(,)? }),* $(,)?) => {
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum PluginInvocationRequest { $($variant { $($field: $ty),* }),* }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum PluginHostRequest {
|
||||
Service { request: crate::PluginServiceRequest },
|
||||
Prepare { plan_id: String, installation_id: String, tools: Vec<String> },
|
||||
Begin { plan_id: String, installation_id: String, consumer: PluginConsumer },
|
||||
Cancel { invocation_id: String },
|
||||
$($variant { $($field: $ty),* }),*
|
||||
}
|
||||
impl TryFrom<PluginHostRequest> for PluginInvocationRequest {
|
||||
type Error = ();
|
||||
fn try_from(request: PluginHostRequest) -> Result<Self, ()> {
|
||||
match request {
|
||||
$(PluginHostRequest::$variant { $($field),* } => Ok(Self::$variant { $($field),* }),)*
|
||||
PluginHostRequest::Service { .. } | PluginHostRequest::Prepare { .. }
|
||||
| PluginHostRequest::Begin { .. } | PluginHostRequest::Cancel { .. } => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<PluginInvocationRequest> for PluginHostRequest {
|
||||
fn from(request: PluginInvocationRequest) -> Self {
|
||||
match request { $(PluginInvocationRequest::$variant { $($field),* } => Self::$variant { $($field),* }),* }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
invocation_requests! {
|
||||
Authorize {
|
||||
invocation_id: String,
|
||||
operation: PluginHostOperation,
|
||||
@@ -83,7 +102,7 @@ pub enum PluginConsumer {
|
||||
tool_call_id: String,
|
||||
tool_name: String,
|
||||
},
|
||||
/// Reserved consumer identity; this does not enable service/event delivery.
|
||||
/// Owner-issued Service delivery identity; never grants admission by itself.
|
||||
Consumer {
|
||||
consumer_id: String,
|
||||
event_id: String,
|
||||
@@ -129,21 +148,42 @@ pub struct PluginHttpResponse {
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(
|
||||
tag = "kind",
|
||||
content = "data",
|
||||
rename_all = "snake_case",
|
||||
deny_unknown_fields
|
||||
)]
|
||||
pub enum PluginHostResponse {
|
||||
Invocation {
|
||||
invocation_id: String,
|
||||
#[schemars(range(min = 0, max = 9_007_199_254_740_991_i64))]
|
||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||
expires_at_ms: i64,
|
||||
},
|
||||
macro_rules! invocation_responses {
|
||||
($($variant:ident $({ $($field:ident: $ty:ty),* $(,)? })?),* $(,)?) => {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(tag = "kind", content = "data", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum PluginInvocationResponse { $($variant $({ $($field: $ty),* })?),* }
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(tag = "kind", content = "data", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum PluginHostResponse {
|
||||
Service { response: crate::PluginServiceResponse },
|
||||
Invocation {
|
||||
invocation_id: String,
|
||||
#[schemars(range(min = 0, max = 9_007_199_254_740_991_i64))]
|
||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||
expires_at_ms: i64,
|
||||
},
|
||||
$($variant $({ $($field: $ty),* })?),*
|
||||
}
|
||||
impl TryFrom<PluginHostResponse> for PluginInvocationResponse {
|
||||
type Error = ();
|
||||
fn try_from(response: PluginHostResponse) -> Result<Self, ()> {
|
||||
match response {
|
||||
$(PluginHostResponse::$variant $({ $($field),* })? => Ok(Self::$variant $({ $($field),* })?),)*
|
||||
PluginHostResponse::Service { .. } | PluginHostResponse::Invocation { .. } => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<PluginInvocationResponse> for PluginHostResponse {
|
||||
fn from(response: PluginInvocationResponse) -> Self {
|
||||
match response { $(PluginInvocationResponse::$variant $({ $($field),* })? => Self::$variant $({ $($field),* })?),* }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
invocation_responses! {
|
||||
Authorized,
|
||||
Opened,
|
||||
Response {
|
||||
@@ -360,6 +400,24 @@ mod tests {
|
||||
assert!(serde_json::from_value::<PluginHostError>(serde_json::json!({"code":"denied","effect":"not_dispatched","diagnostic_id":"d","message":"secret"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_wire_rejects_nested_control_and_identity_claims() {
|
||||
let lease = serde_json::json!({"consumer_id":"consumer", "lease_token":"lease"});
|
||||
for nested in [
|
||||
serde_json::json!({"kind":"service", "request":{"kind":"poll", "lease":lease}}),
|
||||
serde_json::json!({"kind":"prepare", "plan_id":"p", "installation_id":"i", "tools":[]}),
|
||||
serde_json::json!({"kind":"begin", "plan_id":"p", "installation_id":"i", "consumer":{"kind":"consumer", "consumer_id":"c", "event_id":"e", "export_name":"s"}}),
|
||||
serde_json::json!({"kind":"cancel", "invocation_id":"i"}),
|
||||
serde_json::json!({"kind":"authorize", "invocation_id":"i", "operation":{"kind":"admit"}, "subject":"owner"}),
|
||||
] {
|
||||
let wire = serde_json::json!({"kind":"service", "request":{"kind":"host", "lease":lease, "request":nested}});
|
||||
assert!(serde_json::from_value::<PluginHostRequest>(wire).is_err());
|
||||
}
|
||||
let wire = serde_json::json!({"kind":"service", "request":{"kind":"host", "lease":lease, "request":{"kind":"read_ticket", "invocation_id":"invocation", "ticket_id":"ticket"}}});
|
||||
let request: PluginHostRequest = serde_json::from_value(wire.clone()).unwrap();
|
||||
assert_eq!(serde_json::to_value(request).unwrap(), wire);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_integer_wire_preserves_full_wit_range_without_json_precision_loss() {
|
||||
for scalar in [
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
//! Host-owned Ticket Service delivery. Lease proofs fence a driver, not authorize it.
|
||||
//! All requests still require the authenticated Worker and current retained plan.
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
impl api_macros::openapi::OpenApiSchema for PluginServiceRecoveryRequest {}
|
||||
impl api_macros::openapi::OpenApiSchema for PluginServiceDiagnostics {}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginServiceLease {
|
||||
pub consumer_id: String,
|
||||
pub lease_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PluginTicketEventType {
|
||||
Changed,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginTicketEventEnvelope {
|
||||
pub event_id: String,
|
||||
pub event_type: PluginTicketEventType,
|
||||
pub version: u32,
|
||||
pub occurred_at: String,
|
||||
pub ticket_id: String,
|
||||
pub domain_event_ref: String,
|
||||
pub content_digest: String,
|
||||
pub correlation_id: String,
|
||||
pub causation_id: Option<String>,
|
||||
pub attempt: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum PluginServiceEventResult {
|
||||
Ack,
|
||||
RetryAfter {
|
||||
delay_ms: u32,
|
||||
},
|
||||
Reject,
|
||||
/// Host-observed unknown effect; never automatically redelivered.
|
||||
Indeterminate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PluginServicePauseReason {
|
||||
Rejected,
|
||||
AttemptsExhausted,
|
||||
QueueFull,
|
||||
RetentionGap,
|
||||
Indeterminate,
|
||||
Authorization,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginServiceDiagnostics {
|
||||
pub consumer_id: String,
|
||||
pub pause_reason: Option<PluginServicePauseReason>,
|
||||
pub queued: u32,
|
||||
pub attempt: u32,
|
||||
pub current_event_id: Option<String>,
|
||||
pub lease_active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PluginServiceLifecycleOperation {
|
||||
Admit,
|
||||
Publish,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum PluginServiceRequest {
|
||||
/// Live preflight for a reserved or running Worker. Does not create consumer
|
||||
/// state, acquire a lease, or admit event effects.
|
||||
Prepare {
|
||||
plan_id: String,
|
||||
installation_id: String,
|
||||
service_name: String,
|
||||
operation: PluginServiceLifecycleOperation,
|
||||
},
|
||||
/// Lifecycle logging uses live policy but never an event invocation/lease.
|
||||
LifecycleLog {
|
||||
plan_id: String,
|
||||
installation_id: String,
|
||||
service_name: String,
|
||||
entry: crate::PluginLogEntry,
|
||||
},
|
||||
/// Creates a consumer at the current tail, or restores the exact retained one.
|
||||
Acquire {
|
||||
plan_id: String,
|
||||
installation_id: String,
|
||||
service_name: String,
|
||||
},
|
||||
Poll {
|
||||
lease: PluginServiceLease,
|
||||
},
|
||||
Complete {
|
||||
lease: PluginServiceLease,
|
||||
event_id: String,
|
||||
result: PluginServiceEventResult,
|
||||
},
|
||||
Release {
|
||||
lease: PluginServiceLease,
|
||||
},
|
||||
Diagnostics {
|
||||
consumer_id: String,
|
||||
},
|
||||
/// The only path for Service event Host operations. Nested Service requests,
|
||||
/// Tool admission and cancellation are not accepted here.
|
||||
Host {
|
||||
lease: PluginServiceLease,
|
||||
request: Box<crate::PluginInvocationRequest>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(
|
||||
tag = "kind",
|
||||
content = "data",
|
||||
rename_all = "snake_case",
|
||||
deny_unknown_fields
|
||||
)]
|
||||
pub enum PluginServiceResponse {
|
||||
Prepared,
|
||||
Logged,
|
||||
Acquired {
|
||||
lease: PluginServiceLease,
|
||||
},
|
||||
Idle,
|
||||
Delivery {
|
||||
envelope: PluginTicketEventEnvelope,
|
||||
invocation_id: String,
|
||||
#[schemars(range(min = 0, max = 9_007_199_254_740_991_i64))]
|
||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||
expires_at_ms: i64,
|
||||
},
|
||||
Completed,
|
||||
Released,
|
||||
Diagnostics {
|
||||
diagnostics: PluginServiceDiagnostics,
|
||||
},
|
||||
Host {
|
||||
response: Box<crate::PluginInvocationResponse>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Audited recovery is an explicit Workspace management operation, not a guest import.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum PluginServiceRecoveryAction {
|
||||
Resume,
|
||||
Skip {
|
||||
event_id: String,
|
||||
},
|
||||
/// A retention gap cannot be skipped by guessing a missing event identity.
|
||||
AcceptGap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginServiceRecoveryRequest {
|
||||
pub consumer_id: String,
|
||||
pub expected_pause: PluginServicePauseReason,
|
||||
pub action: PluginServiceRecoveryAction,
|
||||
pub operation_key: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn prepare_is_lease_free_and_has_no_consumer_or_event_identity() {
|
||||
let request = PluginServiceRequest::Prepare {
|
||||
plan_id: "plan".into(),
|
||||
installation_id: "installation".into(),
|
||||
service_name: "watch".into(),
|
||||
operation: PluginServiceLifecycleOperation::Publish,
|
||||
};
|
||||
let value = serde_json::to_value(&request).unwrap();
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({"kind":"prepare","plan_id":"plan","installation_id":"installation","service_name":"watch","operation":"publish"})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<PluginServiceRequest>(value).unwrap(),
|
||||
request
|
||||
);
|
||||
assert!(serde_json::from_value::<PluginServiceRequest>(json!({"kind":"prepare","plan_id":"plan","installation_id":"installation","service_name":"watch","lease":{"consumer_id":"consumer","lease_token":"token"}})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leased_host_wire_type_cannot_nest_service_or_admit_tool_calls() {
|
||||
for nested in [
|
||||
json!({"kind":"service","request":{"kind":"diagnostics","consumer_id":"other"}}),
|
||||
json!({"kind":"begin","plan_id":"p","installation_id":"i","consumer":{"kind":"tool_call","tool_call_id":"call","tool_name":"tool"}}),
|
||||
json!({"kind":"cancel","invocation_id":"invocation"}),
|
||||
] {
|
||||
assert!(serde_json::from_value::<PluginServiceRequest>(json!({"kind":"host","lease":{"consumer_id":"consumer","lease_token":"token"},"request":nested})).is_err());
|
||||
}
|
||||
// This must stay a finite schema for OpenAPI/TypeScript generation too.
|
||||
let schema = schemars::schema_for!(PluginServiceRequest);
|
||||
assert!(
|
||||
!serde_json::to_string(&schema)
|
||||
.unwrap()
|
||||
.contains("PluginHostRequest")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovery_is_typed_management_input_not_a_guest_service_request() {
|
||||
let recovery = json!({"consumer_id":"consumer","expected_pause":"indeterminate","action":{"kind":"skip","event_id":"event"},"operation_key":"reviewed-skip","reason":"effect independently reconciled"});
|
||||
assert!(serde_json::from_value::<PluginServiceRecoveryRequest>(recovery.clone()).is_ok());
|
||||
assert!(serde_json::from_value::<PluginServiceRequest>(recovery).is_err());
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,15 @@ pub fn plugin_host_typescript() -> String {
|
||||
use ts_rs::TS;
|
||||
let config = ts_rs::Config::default();
|
||||
let declarations = [
|
||||
PluginServiceLease::decl(&config),
|
||||
PluginTicketEventType::decl(&config),
|
||||
PluginTicketEventEnvelope::decl(&config),
|
||||
PluginServiceEventResult::decl(&config),
|
||||
PluginServicePauseReason::decl(&config),
|
||||
PluginServiceDiagnostics::decl(&config),
|
||||
PluginServiceRequest::decl(&config),
|
||||
PluginServiceResponse::decl(&config),
|
||||
PluginServiceRecoveryAction::decl(&config),
|
||||
PluginHttpMethod::decl(&config),
|
||||
PluginConsumer::decl(&config),
|
||||
PluginTicketAccess::decl(&config),
|
||||
@@ -19,6 +28,8 @@ pub fn plugin_host_typescript() -> String {
|
||||
PluginDataMutationLookup::decl(&config),
|
||||
PluginDataEntry::decl(&config),
|
||||
PluginDataPage::decl(&config),
|
||||
PluginInvocationRequest::decl(&config),
|
||||
PluginInvocationResponse::decl(&config),
|
||||
PluginHostRequest::decl(&config),
|
||||
PluginHostOperation::decl(&config),
|
||||
PluginHttpRequest::decl(&config),
|
||||
@@ -51,6 +62,11 @@ pub fn plugin_api_typescript() -> String {
|
||||
PluginHttpMethod::decl(&config),
|
||||
PluginConnectionMetadata::decl(&config),
|
||||
PluginGrantMetadata::decl(&config),
|
||||
PluginServiceMetadata::decl(&config),
|
||||
PluginServicePauseReason::decl(&config),
|
||||
PluginServiceDiagnostics::decl(&config),
|
||||
PluginServiceRecoveryAction::decl(&config),
|
||||
PluginServiceRecoveryRequest::decl(&config),
|
||||
PluginDataSchemaMetadata::decl(&config),
|
||||
PluginDataPurgeState::decl(&config),
|
||||
PluginDataPurgeRequest::decl(&config),
|
||||
@@ -69,6 +85,7 @@ pub fn plugin_api_typescript() -> String {
|
||||
PluginWorkerDiagnosticSelection::decl(&config),
|
||||
PluginDiagnosticPinState::decl(&config),
|
||||
PluginDiagnosticSurface::decl(&config),
|
||||
PluginServiceConsumerDiagnostics::decl(&config),
|
||||
PluginWorkerDiagnostic::decl(&config),
|
||||
PluginDiagnosticAdmission::decl(&config),
|
||||
PluginDiagnosticConnection::decl(&config),
|
||||
|
||||
Reference in New Issue
Block a user