use super::{PluginIdentity, PluginRevocationReason};
use std::{
fmt::{Display, Formatter},
num::NonZeroUsize,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PluginOperationalEvent {
inner: PluginOperationalEventInner,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum PluginOperationalEventInner {
Scoped {
identity: PluginIdentity,
kind: PluginOperationalEventKind,
},
UnscopedRuntimeFailure {
failure: PluginOperationalRuntimeFailure,
},
UnscopedQueueSaturated {
queue: PluginOperationalQueue,
limit: NonZeroUsize,
},
}
impl PluginOperationalEvent {
#[must_use]
pub fn load(identity: &PluginIdentity) -> Self {
Self::scoped(identity, PluginOperationalEventKind::Load)
}
#[must_use]
pub fn unload(identity: &PluginIdentity) -> Self {
Self::scoped(identity, PluginOperationalEventKind::Unload)
}
#[must_use]
pub fn revocation(identity: &PluginIdentity, reason: PluginRevocationReason) -> Self {
Self::scoped(identity, PluginOperationalEventKind::Revocation { reason })
}
#[must_use]
pub fn denied_import(identity: &PluginIdentity, import: PluginOperationalImport) -> Self {
Self::scoped(
identity,
PluginOperationalEventKind::DeniedImport { import },
)
}
#[must_use]
pub fn rejected_import(
identity: &PluginIdentity,
import: PluginOperationalImport,
reason: PluginOperationalImportRejection,
) -> Self {
Self::scoped(
identity,
PluginOperationalEventKind::ImportRejected { import, reason },
)
}
#[must_use]
pub fn timeout(identity: &PluginIdentity, timeout_ms: u64) -> Self {
Self::scoped(identity, PluginOperationalEventKind::Timeout { timeout_ms })
}
#[must_use]
pub fn guest_trap(identity: &PluginIdentity, export: PluginOperationalExport) -> Self {
Self::scoped(identity, PluginOperationalEventKind::GuestTrap { export })
}
#[must_use]
pub fn runtime_failure(
identity: &PluginIdentity,
failure: PluginOperationalRuntimeFailure,
) -> Self {
Self::scoped(
identity,
PluginOperationalEventKind::RuntimeFailure { failure },
)
}
#[must_use]
pub const fn unscoped_runtime_failure(failure: PluginOperationalRuntimeFailure) -> Self {
Self {
inner: PluginOperationalEventInner::UnscopedRuntimeFailure { failure },
}
}
#[must_use]
pub const fn queue_saturated(queue: PluginOperationalQueue, limit: NonZeroUsize) -> Self {
Self {
inner: PluginOperationalEventInner::UnscopedQueueSaturated { queue, limit },
}
}
#[must_use]
pub fn queue_saturated_for(
identity: &PluginIdentity,
queue: PluginOperationalQueue,
limit: NonZeroUsize,
) -> Self {
Self::scoped(
identity,
PluginOperationalEventKind::QueueSaturated { queue, limit },
)
}
#[must_use]
pub fn identity(&self) -> Option<&str> {
self.identity_proof().map(PluginIdentity::as_str)
}
#[must_use]
pub const fn identity_proof(&self) -> Option<&PluginIdentity> {
match &self.inner {
PluginOperationalEventInner::Scoped { identity, .. } => Some(identity),
PluginOperationalEventInner::UnscopedRuntimeFailure { .. }
| PluginOperationalEventInner::UnscopedQueueSaturated { .. } => None,
}
}
#[must_use]
pub const fn kind(&self) -> PluginOperationalEventKind {
match &self.inner {
PluginOperationalEventInner::Scoped { kind, .. } => *kind,
PluginOperationalEventInner::UnscopedRuntimeFailure { failure } => {
PluginOperationalEventKind::RuntimeFailure { failure: *failure }
}
PluginOperationalEventInner::UnscopedQueueSaturated { queue, limit } => {
PluginOperationalEventKind::QueueSaturated {
queue: *queue,
limit: *limit,
}
}
}
}
fn scoped(identity: &PluginIdentity, kind: PluginOperationalEventKind) -> Self {
Self {
inner: PluginOperationalEventInner::Scoped {
identity: identity.clone(),
kind,
},
}
}
}
impl Display for PluginOperationalEvent {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match &self.inner {
PluginOperationalEventInner::Scoped {
identity,
kind: PluginOperationalEventKind::Load,
} => {
write!(formatter, "plugin {:?} loaded", identity.as_str())
}
PluginOperationalEventInner::Scoped {
identity,
kind: PluginOperationalEventKind::Unload,
} => {
write!(formatter, "plugin {:?} unloaded", identity.as_str())
}
PluginOperationalEventInner::Scoped {
identity,
kind: PluginOperationalEventKind::Revocation { reason },
} => {
write!(
formatter,
"plugin {:?} revoked for {reason}",
identity.as_str()
)
}
PluginOperationalEventInner::Scoped {
identity,
kind: PluginOperationalEventKind::DeniedImport { import },
} => {
write!(
formatter,
"plugin {:?} denied import {import}",
identity.as_str()
)
}
PluginOperationalEventInner::Scoped {
identity,
kind: PluginOperationalEventKind::ImportRejected { import, reason },
} => {
write!(
formatter,
"plugin {:?} rejected import {import}: {reason}",
identity.as_str()
)
}
PluginOperationalEventInner::Scoped {
identity,
kind: PluginOperationalEventKind::Timeout { timeout_ms },
} => {
write!(
formatter,
"plugin {:?} timed out after {timeout_ms} ms",
identity.as_str()
)
}
PluginOperationalEventInner::Scoped {
identity,
kind: PluginOperationalEventKind::GuestTrap { export },
} => {
write!(
formatter,
"plugin {:?} export {export} trapped",
identity.as_str()
)
}
PluginOperationalEventInner::Scoped {
identity,
kind: PluginOperationalEventKind::RuntimeFailure { failure },
} => {
write!(
formatter,
"plugin {:?} runtime failure: {failure}",
identity.as_str()
)
}
PluginOperationalEventInner::Scoped {
identity,
kind: PluginOperationalEventKind::QueueSaturated { queue, limit },
} => {
write!(
formatter,
"plugin {:?} saturated {queue} queue at {limit}",
identity.as_str()
)
}
PluginOperationalEventInner::UnscopedRuntimeFailure { failure } => {
write!(formatter, "plugin runtime failure: {failure}")
}
PluginOperationalEventInner::UnscopedQueueSaturated { queue, limit } => {
write!(formatter, "plugin {queue} queue saturated at {limit}")
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginOperationalEventKind {
Load,
Unload,
Revocation {
reason: PluginRevocationReason,
},
DeniedImport {
import: PluginOperationalImport,
},
ImportRejected {
import: PluginOperationalImport,
reason: PluginOperationalImportRejection,
},
Timeout {
timeout_ms: u64,
},
GuestTrap {
export: PluginOperationalExport,
},
RuntimeFailure {
failure: PluginOperationalRuntimeFailure,
},
QueueSaturated {
queue: PluginOperationalQueue,
limit: NonZeroUsize,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginOperationalRuntimeFailure {
Engine,
RuntimeTimer,
Component,
Linker,
Instantiate,
Limit,
Export,
PostReturn,
Fuel,
Timer,
InstanceStateMismatch,
BatchIdentityMismatch,
}
impl PluginOperationalRuntimeFailure {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Engine => "engine",
Self::RuntimeTimer => "runtime-timer",
Self::Component => "component",
Self::Linker => "linker",
Self::Instantiate => "instantiate",
Self::Limit => "limit",
Self::Export => "export",
Self::PostReturn => "post-return",
Self::Fuel => "fuel",
Self::Timer => "timer",
Self::InstanceStateMismatch => "instance-state-mismatch",
Self::BatchIdentityMismatch => "batch-identity-mismatch",
}
}
}
impl Display for PluginOperationalRuntimeFailure {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginOperationalExport {
Load,
Update,
}
impl PluginOperationalExport {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Load => "load",
Self::Update => "update",
}
}
}
impl Display for PluginOperationalExport {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginOperationalImport {
HostImport,
UpdateResourceCleanup,
StatusPublish,
BufferObserve,
BufferProposeEdit,
WorkspaceArtifactWrite,
WorkspaceObserveStart,
WorkspaceObservePoll,
WorkspaceObserveTake,
WorkspaceObserveTaskDrop,
BufferHandleDrop,
ViewHandleDrop,
}
impl PluginOperationalImport {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::HostImport => "host-import",
Self::UpdateResourceCleanup => "update-resource-cleanup",
Self::StatusPublish => "status.publish",
Self::BufferObserve => "buffer.observe",
Self::BufferProposeEdit => "buffer.propose_edit",
Self::WorkspaceArtifactWrite => "workspace.artifact_write",
Self::WorkspaceObserveStart => "workspace.observe.start",
Self::WorkspaceObservePoll => "workspace.observe.poll",
Self::WorkspaceObserveTake => "workspace.observe.take",
Self::WorkspaceObserveTaskDrop => "workspace-observe-task.drop",
Self::BufferHandleDrop => "buffer-handle.drop",
Self::ViewHandleDrop => "view-handle.drop",
}
}
}
impl Display for PluginOperationalImport {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginOperationalQueue {
Intent,
ProposalLane,
WorkspaceIo,
WorkspaceObserveTask,
}
impl PluginOperationalQueue {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Intent => "intent",
Self::ProposalLane => "proposal-lane",
Self::WorkspaceIo => "workspace-io",
Self::WorkspaceObserveTask => "workspace-observe-task",
}
}
}
impl Display for PluginOperationalQueue {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginOperationalImportRejection {
HostStateMismatch,
Handle,
EffectQueue,
WorkspaceIoQueue,
WorkspaceObserveTaskQueue,
Observation,
Decode,
ResourceHandle,
SessionMissing,
SessionAlreadyActive,
MissingObservedRevision,
}
impl PluginOperationalImportRejection {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::HostStateMismatch => "host-state-mismatch",
Self::Handle => "handle",
Self::EffectQueue => "effect-queue",
Self::WorkspaceIoQueue => "workspace-io-queue",
Self::WorkspaceObserveTaskQueue => "workspace-observe-task-queue",
Self::Observation => "observation",
Self::Decode => "decode",
Self::ResourceHandle => "resource-handle",
Self::SessionMissing => "session-missing",
Self::SessionAlreadyActive => "session-already-active",
Self::MissingObservedRevision => "missing-observed-revision",
}
}
}
impl Display for PluginOperationalImportRejection {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::{
PluginOperationalEvent, PluginOperationalExport, PluginOperationalImport,
PluginOperationalImportRejection, PluginOperationalQueue, PluginOperationalRuntimeFailure,
};
use crate::plugin::{PluginIdentity, PluginRevocationReason};
use std::num::NonZeroUsize;
fn count_limit(value: usize) -> NonZeroUsize {
NonZeroUsize::new(value).expect("test queue limit should be non-zero")
}
#[test]
fn operational_events_are_redacted() {
let identity = identity("formatter");
let event = PluginOperationalEvent::denied_import(
&identity,
PluginOperationalImport::StatusPublish,
);
assert_eq!(event.identity(), Some("formatter"));
assert_eq!(
event.identity_proof().map(PluginIdentity::as_str),
Some("formatter")
);
assert!(!format!("{event:?}").contains("secret"));
assert!(!event.to_string().contains("secret"));
}
#[test]
fn queue_saturation_event_has_no_payload() {
let event = PluginOperationalEvent::queue_saturated(
PluginOperationalQueue::WorkspaceIo,
count_limit(1),
);
assert_eq!(event.identity(), None);
assert_eq!(
event.to_string(),
"plugin workspace-io queue saturated at 1"
);
}
#[test]
fn scoped_queue_saturation_keeps_payload_redacted() {
let identity = identity("formatter");
let event = PluginOperationalEvent::queue_saturated_for(
&identity,
PluginOperationalQueue::Intent,
count_limit(1),
);
assert_eq!(event.identity(), Some("formatter"));
assert!(!format!("{event:?}").contains("workspace bytes"));
assert_eq!(
event.to_string(),
"plugin \"formatter\" saturated intent queue at 1"
);
}
#[test]
fn import_rejection_event_has_closed_redacted_reason() {
let identity = identity("formatter");
let event = PluginOperationalEvent::rejected_import(
&identity,
PluginOperationalImport::WorkspaceArtifactWrite,
PluginOperationalImportRejection::Decode,
);
assert_eq!(event.identity(), Some("formatter"));
assert_eq!(
event.to_string(),
"plugin \"formatter\" rejected import workspace.artifact_write: decode"
);
assert!(!format!("{event:?}").contains("docs/secret.txt"));
assert!(!event.to_string().contains("docs/secret.txt"));
}
#[test]
fn revocation_event_has_closed_redacted_reason() {
let identity = identity("formatter");
let event =
PluginOperationalEvent::revocation(&identity, PluginRevocationReason::GrantsChanged);
assert_eq!(event.identity(), Some("formatter"));
assert_eq!(
event.to_string(),
"plugin \"formatter\" revoked for grants-changed"
);
assert!(!format!("{event:?}").contains("secret"));
assert!(!event.to_string().contains("secret"));
}
#[test]
fn operational_queue_classes_have_stable_redacted_text() {
for (queue, expected) in [
(PluginOperationalQueue::Intent, "intent"),
(PluginOperationalQueue::ProposalLane, "proposal-lane"),
(PluginOperationalQueue::WorkspaceIo, "workspace-io"),
(
PluginOperationalQueue::WorkspaceObserveTask,
"workspace-observe-task",
),
] {
assert_eq!(queue.as_str(), expected);
assert_eq!(queue.to_string(), expected);
}
}
#[test]
fn operational_import_classes_have_stable_redacted_text() {
for (import, expected) in [
(PluginOperationalImport::HostImport, "host-import"),
(
PluginOperationalImport::UpdateResourceCleanup,
"update-resource-cleanup",
),
(PluginOperationalImport::StatusPublish, "status.publish"),
(PluginOperationalImport::BufferObserve, "buffer.observe"),
(
PluginOperationalImport::BufferProposeEdit,
"buffer.propose_edit",
),
(
PluginOperationalImport::WorkspaceArtifactWrite,
"workspace.artifact_write",
),
(
PluginOperationalImport::WorkspaceObserveStart,
"workspace.observe.start",
),
(
PluginOperationalImport::WorkspaceObservePoll,
"workspace.observe.poll",
),
(
PluginOperationalImport::WorkspaceObserveTake,
"workspace.observe.take",
),
(
PluginOperationalImport::WorkspaceObserveTaskDrop,
"workspace-observe-task.drop",
),
(
PluginOperationalImport::BufferHandleDrop,
"buffer-handle.drop",
),
(PluginOperationalImport::ViewHandleDrop, "view-handle.drop"),
] {
assert_eq!(import.as_str(), expected);
assert_eq!(import.to_string(), expected);
}
}
#[test]
fn import_rejection_reasons_have_stable_redacted_text() {
for (reason, expected) in [
(
PluginOperationalImportRejection::HostStateMismatch,
"host-state-mismatch",
),
(PluginOperationalImportRejection::Handle, "handle"),
(
PluginOperationalImportRejection::EffectQueue,
"effect-queue",
),
(
PluginOperationalImportRejection::WorkspaceIoQueue,
"workspace-io-queue",
),
(
PluginOperationalImportRejection::WorkspaceObserveTaskQueue,
"workspace-observe-task-queue",
),
(PluginOperationalImportRejection::Observation, "observation"),
(PluginOperationalImportRejection::Decode, "decode"),
(
PluginOperationalImportRejection::ResourceHandle,
"resource-handle",
),
(
PluginOperationalImportRejection::SessionMissing,
"session-missing",
),
(
PluginOperationalImportRejection::SessionAlreadyActive,
"session-already-active",
),
(
PluginOperationalImportRejection::MissingObservedRevision,
"missing-observed-revision",
),
] {
assert_eq!(reason.as_str(), expected);
assert_eq!(reason.to_string(), expected);
}
}
#[test]
fn guest_trap_event_has_closed_redacted_export() {
let identity = identity("formatter");
let event = PluginOperationalEvent::guest_trap(&identity, PluginOperationalExport::Update);
assert_eq!(event.identity(), Some("formatter"));
assert_eq!(
event.to_string(),
"plugin \"formatter\" export update trapped"
);
assert!(!format!("{event:?}").contains("secret"));
assert!(!event.to_string().contains("secret"));
}
#[test]
fn runtime_failure_event_has_closed_redacted_reason() {
let identity = identity("formatter");
let event = PluginOperationalEvent::runtime_failure(
&identity,
PluginOperationalRuntimeFailure::Fuel,
);
assert_eq!(event.identity(), Some("formatter"));
assert_eq!(
event.to_string(),
"plugin \"formatter\" runtime failure: fuel"
);
assert_eq!(
PluginOperationalRuntimeFailure::BatchIdentityMismatch.as_str(),
"batch-identity-mismatch"
);
assert!(!format!("{event:?}").contains("secret"));
assert!(!event.to_string().contains("secret"));
}
#[test]
fn unscoped_runtime_failure_event_has_closed_redacted_reason() {
let event = PluginOperationalEvent::unscoped_runtime_failure(
PluginOperationalRuntimeFailure::Engine,
);
assert_eq!(event.identity(), None);
assert_eq!(event.to_string(), "plugin runtime failure: engine");
}
fn identity(identity: &str) -> PluginIdentity {
PluginIdentity::try_new(identity).expect("test identity should validate")
}
}