1use std::collections::BTreeMap;
4use std::future::Future;
5use std::panic::{catch_unwind, AssertUnwindSafe};
6use std::pin::Pin;
7use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
8use std::sync::Arc;
9use std::sync::Mutex;
10use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
11
12use crate::contracts::diagnostics::{DiagnosticRecord, InvocationEvent, InvocationTrace};
13use crate::contracts::{
14 CommandPath, ErrorEnvelopeV1, ErrorPayloadV1, ExecutionPolicy, ExitCode, GlobalFlags,
15 Namespace, OutputEnvelopeMetaV1, OutputEnvelopeV1,
16};
17use crate::shared::telemetry::{truncate_chars, MAX_COMMAND_FIELD_CHARS, MAX_TEXT_FIELD_CHARS};
18use serde_json::{json, Value};
19
20static INVOCATION_COUNTER: AtomicU64 = AtomicU64::new(1);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[allow(dead_code)]
25pub enum LifecycleStage {
26 Bootstrap,
28 BuildIntent,
30 ResolvePolicy,
32 AssembleContext,
34 Dispatch,
36 Emit,
38 ExitMap,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ExecutionIntent {
45 pub command_path: Vec<String>,
47 pub global_flags: GlobalFlags,
49 pub args: Vec<String>,
51}
52
53#[derive(Debug, Clone)]
55pub struct ExecutionContext {
56 pub intent: ExecutionIntent,
58 pub policy: ExecutionPolicy,
60 pub timeout: Option<Duration>,
62 pub cancelled: Arc<AtomicBool>,
64 pub trace_mode: bool,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum OutputStream {
71 Stdout,
73 Stderr,
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub struct Emission {
80 pub stream: OutputStream,
82 pub payload: Value,
84}
85
86#[derive(Debug, Clone, PartialEq)]
88pub enum HandlerOutcome {
89 Success(Value),
91 Error(ErrorEnvelopeV1),
93}
94
95#[derive(Debug, Clone, PartialEq)]
97pub struct ExecutionResult {
98 pub exit_code: ExitCode,
100 pub emission: Option<Emission>,
102 pub trace: Option<InvocationTrace>,
104}
105
106pub trait DiagnosticsHook: Send + Sync {
108 fn record(&self, record: DiagnosticRecord);
110}
111
112pub trait LifecycleHook: Send + Sync {
114 fn on_plugin_load(&self) {}
116 fn on_repl_start(&self) {}
118 fn on_repl_shutdown(&self) {}
120}
121
122pub trait SyncHandler: Send + Sync {
124 fn execute(&self, ctx: &ExecutionContext) -> Result<Value, ErrorEnvelopeV1>;
126}
127
128pub trait AsyncHandler: Send + Sync {
130 fn execute_async(
132 &self,
133 ctx: &ExecutionContext,
134 ) -> Pin<Box<dyn Future<Output = Result<Value, ErrorEnvelopeV1>> + Send + '_>>;
135}
136
137#[cfg_attr(not(test), allow(dead_code))]
139pub enum Handler {
140 Sync(Box<dyn SyncHandler>),
142 Async(Box<dyn AsyncHandler>),
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub enum KernelError {
149 Cancelled,
151 Timeout,
153}
154
155#[must_use]
157pub fn build_intent_from_argv(argv: &[String]) -> ExecutionIntent {
158 let mut command_path = Vec::new();
161 let mut args = Vec::new();
162
163 let mut i = 1;
164 while i < argv.len() {
165 let token = &argv[i];
166 if token.starts_with('-') {
167 i += 1;
168 continue;
169 }
170
171 command_path.push(token.clone());
172 i += 1;
173 break;
174 }
175
176 while i < argv.len() {
177 args.push(argv[i].clone());
178 i += 1;
179 }
180
181 let flags = GlobalFlags {
182 output_format: None,
183 pretty_mode: None,
184 color_mode: None,
185 log_level: None,
186 quiet: argv.iter().any(|v| v == "--quiet" || v == "-q"),
187 include_runtime: argv.iter().any(|v| v == "--trace"),
188 };
189
190 ExecutionIntent { command_path, global_flags: flags, args }
191}
192
193#[must_use]
195#[allow(dead_code)]
196pub(crate) fn assemble_context(
197 intent: ExecutionIntent,
198 policy: ExecutionPolicy,
199 timeout: Option<Duration>,
200 cancelled: Arc<AtomicBool>,
201 trace_mode: bool,
202) -> ExecutionContext {
203 ExecutionContext { intent, policy, timeout, cancelled, trace_mode }
204}
205
206#[allow(dead_code)]
207fn success_meta(ctx: &ExecutionContext) -> OutputEnvelopeMetaV1 {
208 OutputEnvelopeMetaV1 {
209 version: "v1".to_string(),
210 command: CommandPath {
211 segments: ctx.intent.command_path.iter().map(|v| Namespace(v.clone())).collect(),
212 },
213 timestamp: event_timestamp(),
214 }
215}
216
217#[allow(dead_code)]
218fn map_outcome_to_emission(outcome: HandlerOutcome, quiet: bool) -> Option<Emission> {
219 if quiet {
220 return None;
221 }
222
223 match outcome {
224 HandlerOutcome::Success(payload) => {
225 Some(Emission { stream: OutputStream::Stdout, payload })
226 }
227 HandlerOutcome::Error(err) => Some(Emission {
228 stream: OutputStream::Stderr,
229 payload: serde_json::to_value(err).expect("error envelope must serialize"),
230 }),
231 }
232}
233
234#[allow(dead_code)]
235fn map_outcome_to_exit(outcome: &HandlerOutcome) -> ExitCode {
236 match outcome {
237 HandlerOutcome::Success(_) => ExitCode::Success,
238 HandlerOutcome::Error(err) => map_error_category_to_exit(&err.error.category),
239 }
240}
241
242#[allow(dead_code)]
243fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
244 if let Some(message) = payload.downcast_ref::<&str>() {
245 return (*message).to_string();
246 }
247 if let Some(message) = payload.downcast_ref::<String>() {
248 return message.clone();
249 }
250 "unknown panic payload".to_string()
251}
252
253#[allow(dead_code)]
254fn internal_kernel_error(ctx: &ExecutionContext, message: String) -> HandlerOutcome {
255 HandlerOutcome::Error(ErrorEnvelopeV1::failure(
256 ErrorPayloadV1 {
257 code: "KERNEL_HANDLER_PANIC".to_string(),
258 message: format!("kernel handler panicked: {message}"),
259 category: "internal".to_string(),
260 details: None,
261 },
262 success_meta(ctx),
263 ))
264}
265
266#[allow(dead_code)]
267fn catch_unwind_silent<T>(f: impl FnOnce() -> T) -> std::thread::Result<T> {
268 static PANIC_HOOK_LOCK: Mutex<()> = Mutex::new(());
269 let _guard = PANIC_HOOK_LOCK.lock().expect("panic hook lock poisoned");
270 let previous_hook = std::panic::take_hook();
271 std::panic::set_hook(Box::new(|_| {}));
272 let result = catch_unwind(AssertUnwindSafe(f));
273 std::panic::set_hook(previous_hook);
274 result
275}
276
277#[must_use]
279pub fn map_error_category_to_exit(category: &str) -> ExitCode {
280 match category {
281 "usage" | "validation" => ExitCode::Usage,
282 "plugin" | "internal" => ExitCode::Error,
283 _ => ExitCode::Error,
284 }
285}
286
287fn unix_timestamp_millis() -> u128 {
288 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis()
289}
290
291fn event_timestamp() -> String {
292 unix_timestamp_millis().to_string()
293}
294
295fn exit_code_kind(exit_code: ExitCode) -> &'static str {
296 match exit_code {
297 ExitCode::Success => "success",
298 ExitCode::Usage => "usage",
299 ExitCode::Encoding => "encoding",
300 ExitCode::Aborted => "aborted",
301 ExitCode::Error => "error",
302 }
303}
304
305fn next_invocation_id() -> String {
306 let seq = INVOCATION_COUNTER.fetch_add(1, Ordering::Relaxed);
307 format!("kernel-{}-{}-{seq}", std::process::id(), unix_timestamp_millis())
308}
309
310fn bounded_command(command: &str) -> (String, bool) {
311 truncate_chars(command, MAX_COMMAND_FIELD_CHARS)
312}
313
314fn bounded_message(message: &str) -> (String, bool) {
315 truncate_chars(message, MAX_TEXT_FIELD_CHARS)
316}
317
318fn bounded_optional_message(value: Option<&str>) -> (Option<String>, bool) {
319 match value {
320 Some(text) => {
321 let (bounded, truncated) = bounded_message(text);
322 (Some(bounded), truncated)
323 }
324 None => (None, false),
325 }
326}
327
328fn bounded_trace_command(path: &[String]) -> (String, bool) {
329 bounded_command(&path.join(" "))
330}
331
332#[allow(dead_code)]
333fn is_fast_path(intent: &ExecutionIntent) -> bool {
334 matches!(
335 intent.command_path.as_slice(),
336 [one] if one == "help" || one == "version" || one == "completion"
337 )
338}
339
340#[allow(dead_code)]
342pub(crate) fn execute_pipeline(
343 ctx: &ExecutionContext,
344 handler: &Handler,
345 diagnostics: &[Arc<dyn DiagnosticsHook>],
346 lifecycle: &[Arc<dyn LifecycleHook>],
347) -> Result<ExecutionResult, KernelError> {
348 let started_at = Instant::now();
349
350 let mut trace_events = Vec::<InvocationEvent>::new();
351
352 for hook in diagnostics {
353 hook.record(DiagnosticRecord {
354 id: "bootstrap".to_string(),
355 severity: "info".to_string(),
356 message: format!("stage={:?}", LifecycleStage::Bootstrap),
357 fields: BTreeMap::new(),
358 });
359 }
360
361 if ctx.cancelled.load(Ordering::SeqCst) {
362 let command_joined = ctx.intent.command_path.join(" ");
363 let (command, command_truncated) = bounded_command(&command_joined);
364 for hook in diagnostics {
365 hook.record(DiagnosticRecord {
366 id: "kernel_cancelled_before_dispatch".to_string(),
367 severity: "warning".to_string(),
368 message: "execution cancelled before dispatch".to_string(),
369 fields: BTreeMap::from([
370 ("command".to_string(), json!(command)),
371 ("command_truncated".to_string(), json!(command_truncated)),
372 ]),
373 });
374 }
375 return Err(KernelError::Cancelled);
376 }
377
378 let (trace_command, trace_command_truncated) = bounded_trace_command(&ctx.intent.command_path);
379 trace_events.push(InvocationEvent {
380 timestamp: event_timestamp(),
382 name: "bootstrap".to_string(),
383 payload: BTreeMap::from([
384 ("command".to_string(), json!(trace_command)),
385 ("command_truncated".to_string(), json!(trace_command_truncated)),
386 ]),
387 });
388
389 if is_fast_path(&ctx.intent) {
390 let payload = OutputEnvelopeV1 {
393 status: "ok".to_string(),
394 data: json!({"fast_path": true}),
395 meta: success_meta(ctx),
396 };
397 let outcome = HandlerOutcome::Success(
398 serde_json::to_value(payload).expect("success envelope must serialize"),
399 );
400 let exit_code = map_outcome_to_exit(&outcome);
401 let emission = map_outcome_to_emission(outcome, ctx.policy.quiet);
402 let emission_stream = emission.as_ref().map(|item| match item.stream {
403 OutputStream::Stdout => "stdout",
404 OutputStream::Stderr => "stderr",
405 });
406 for hook in diagnostics {
407 let command_joined = ctx.intent.command_path.join(" ");
408 let (command, command_truncated) = bounded_command(&command_joined);
409 hook.record(DiagnosticRecord {
410 id: "kernel_dispatch_fast_path_completed".to_string(),
411 severity: "info".to_string(),
412 message: "fast-path command completed".to_string(),
413 fields: BTreeMap::from([
414 ("command".to_string(), json!(command)),
415 ("command_truncated".to_string(), json!(command_truncated)),
416 ("exit_code".to_string(), json!(exit_code as i32)),
417 ("exit_kind".to_string(), json!(exit_code_kind(exit_code))),
418 ]),
419 });
420 }
421 return Ok(ExecutionResult {
422 exit_code,
423 emission,
424 trace: if ctx.trace_mode {
425 Some(InvocationTrace {
426 invocation_id: next_invocation_id(),
427 command: CommandPath {
428 segments: ctx
429 .intent
430 .command_path
431 .iter()
432 .map(|v| Namespace(v.clone()))
433 .collect(),
434 },
435 policy: ctx.policy.clone(),
436 events: vec![
437 InvocationEvent {
438 timestamp: event_timestamp(),
439 name: "dispatch.start".to_string(),
440 payload: BTreeMap::from([
441 ("command".to_string(), json!(trace_command.clone())),
442 ("command_truncated".to_string(), json!(trace_command_truncated)),
443 ("mode".to_string(), json!("fast-path")),
444 ]),
445 },
446 InvocationEvent {
447 timestamp: event_timestamp(),
448 name: "dispatch.finish".to_string(),
449 payload: BTreeMap::from([
450 ("command".to_string(), json!(trace_command)),
451 ("command_truncated".to_string(), json!(trace_command_truncated)),
452 ("mode".to_string(), json!("fast-path")),
453 ("exit_code".to_string(), json!(exit_code as i32)),
454 ("exit_kind".to_string(), json!(exit_code_kind(exit_code))),
455 ("quiet".to_string(), json!(ctx.policy.quiet)),
456 ("emission".to_string(), json!(emission_stream)),
457 (
458 "duration_ms".to_string(),
459 json!(started_at.elapsed().as_millis()),
460 ),
461 ]),
462 },
463 ],
464 })
465 } else {
466 None
467 },
468 });
469 }
470
471 if ctx.intent.command_path.first().is_some_and(|v| v == "plugins") {
472 let (trace_command, trace_command_truncated) =
473 bounded_trace_command(&ctx.intent.command_path);
474 trace_events.push(InvocationEvent {
475 timestamp: event_timestamp(),
476 name: "lifecycle.plugin.load".to_string(),
477 payload: BTreeMap::from([
478 ("command".to_string(), json!(trace_command)),
479 ("command_truncated".to_string(), json!(trace_command_truncated)),
480 ]),
481 });
482 for hook in lifecycle {
483 hook.on_plugin_load();
484 }
485 }
486 let is_repl_command = ctx.intent.command_path.first().is_some_and(|v| v == "repl");
487 if is_repl_command {
488 let (trace_command, trace_command_truncated) =
489 bounded_trace_command(&ctx.intent.command_path);
490 trace_events.push(InvocationEvent {
491 timestamp: event_timestamp(),
492 name: "lifecycle.repl.start".to_string(),
493 payload: BTreeMap::from([
494 ("command".to_string(), json!(trace_command)),
495 ("command_truncated".to_string(), json!(trace_command_truncated)),
496 ]),
497 });
498 for hook in lifecycle {
499 hook.on_repl_start();
500 }
501 }
502
503 let (trace_command, trace_command_truncated) = bounded_trace_command(&ctx.intent.command_path);
504 trace_events.push(InvocationEvent {
505 timestamp: event_timestamp(),
506 name: "dispatch.start".to_string(),
507 payload: BTreeMap::from([
508 ("command".to_string(), json!(trace_command.clone())),
509 ("command_truncated".to_string(), json!(trace_command_truncated)),
510 ("mode".to_string(), json!("standard")),
511 ]),
512 });
513
514 let outcome = match handler {
515 Handler::Sync(sync_handler) => match catch_unwind_silent(|| sync_handler.execute(ctx)) {
516 Ok(Ok(payload)) => HandlerOutcome::Success(payload),
517 Ok(Err(err)) => HandlerOutcome::Error(err),
518 Err(payload) => {
519 let panic_text = panic_message(payload);
520 let command_joined = ctx.intent.command_path.join(" ");
521 let (command, command_truncated) = bounded_command(&command_joined);
522 let (panic_message_text, panic_message_truncated) = bounded_message(&panic_text);
523 for hook in diagnostics {
524 hook.record(DiagnosticRecord {
525 id: "kernel_handler_panic".to_string(),
526 severity: "error".to_string(),
527 message: "sync handler panicked during dispatch".to_string(),
528 fields: BTreeMap::from([
529 ("command".to_string(), json!(command)),
530 ("command_truncated".to_string(), json!(command_truncated)),
531 ("panic_message".to_string(), json!(panic_message_text)),
532 ("panic_message_truncated".to_string(), json!(panic_message_truncated)),
533 ]),
534 });
535 }
536 internal_kernel_error(ctx, panic_text)
537 }
538 },
539 Handler::Async(async_handler) => {
540 match catch_unwind_silent(|| {
541 futures::executor::block_on(async_handler.execute_async(ctx))
542 }) {
543 Ok(Ok(payload)) => HandlerOutcome::Success(payload),
544 Ok(Err(err)) => HandlerOutcome::Error(err),
545 Err(payload) => {
546 let panic_text = panic_message(payload);
547 let command_joined = ctx.intent.command_path.join(" ");
548 let (command, command_truncated) = bounded_command(&command_joined);
549 let (panic_message_text, panic_message_truncated) =
550 bounded_message(&panic_text);
551 for hook in diagnostics {
552 hook.record(DiagnosticRecord {
553 id: "kernel_handler_panic".to_string(),
554 severity: "error".to_string(),
555 message: "async handler panicked during dispatch".to_string(),
556 fields: BTreeMap::from([
557 ("command".to_string(), json!(command)),
558 ("command_truncated".to_string(), json!(command_truncated)),
559 ("panic_message".to_string(), json!(panic_message_text)),
560 (
561 "panic_message_truncated".to_string(),
562 json!(panic_message_truncated),
563 ),
564 ]),
565 });
566 }
567 internal_kernel_error(ctx, panic_text)
568 }
569 }
570 }
571 };
572
573 if is_repl_command {
574 let (trace_command, trace_command_truncated) =
575 bounded_trace_command(&ctx.intent.command_path);
576 trace_events.push(InvocationEvent {
577 timestamp: event_timestamp(),
578 name: "lifecycle.repl.shutdown".to_string(),
579 payload: BTreeMap::from([
580 ("command".to_string(), json!(trace_command)),
581 ("command_truncated".to_string(), json!(trace_command_truncated)),
582 ]),
583 });
584 for hook in lifecycle {
585 hook.on_repl_shutdown();
586 }
587 }
588
589 if let Some(limit) = ctx.timeout {
590 if started_at.elapsed() > limit {
591 let command_joined = ctx.intent.command_path.join(" ");
592 let (command, command_truncated) = bounded_command(&command_joined);
593 for hook in diagnostics {
594 hook.record(DiagnosticRecord {
595 id: "kernel_timeout_after_dispatch".to_string(),
596 severity: "error".to_string(),
597 message: format!(
598 "execution exceeded timeout budget of {}ms",
599 limit.as_millis()
600 ),
601 fields: BTreeMap::from([
602 ("command".to_string(), json!(command)),
603 ("command_truncated".to_string(), json!(command_truncated)),
604 ("timeout_ms".to_string(), json!(limit.as_millis())),
605 ("elapsed_ms".to_string(), json!(started_at.elapsed().as_millis())),
606 ]),
607 });
608 }
609 return Err(KernelError::Timeout);
610 }
611 }
612
613 if ctx.cancelled.load(Ordering::SeqCst) {
614 let command_joined = ctx.intent.command_path.join(" ");
615 let (command, command_truncated) = bounded_command(&command_joined);
616 for hook in diagnostics {
617 hook.record(DiagnosticRecord {
618 id: "kernel_cancelled_after_dispatch".to_string(),
619 severity: "warning".to_string(),
620 message: "execution cancelled after dispatch".to_string(),
621 fields: BTreeMap::from([
622 ("command".to_string(), json!(command)),
623 ("command_truncated".to_string(), json!(command_truncated)),
624 ]),
625 });
626 }
627 return Err(KernelError::Cancelled);
628 }
629
630 match &outcome {
631 HandlerOutcome::Success(_) => {
632 let command_joined = ctx.intent.command_path.join(" ");
633 let (command, command_truncated) = bounded_command(&command_joined);
634 for hook in diagnostics {
635 hook.record(DiagnosticRecord {
636 id: "kernel_handler_outcome_success".to_string(),
637 severity: "info".to_string(),
638 message: "handler returned success payload".to_string(),
639 fields: BTreeMap::from([
640 ("command".to_string(), json!(command)),
641 ("command_truncated".to_string(), json!(command_truncated)),
642 ]),
643 });
644 }
645 }
646 HandlerOutcome::Error(err) => {
647 let command_joined = ctx.intent.command_path.join(" ");
648 let (command, command_truncated) = bounded_command(&command_joined);
649 let (error_code, error_code_truncated) = bounded_message(&err.error.code);
650 let (error_category, error_category_truncated) = bounded_message(&err.error.category);
651 for hook in diagnostics {
652 hook.record(DiagnosticRecord {
653 id: "kernel_handler_outcome_error".to_string(),
654 severity: "warning".to_string(),
655 message: "handler returned error payload".to_string(),
656 fields: BTreeMap::from([
657 ("command".to_string(), json!(command)),
658 ("command_truncated".to_string(), json!(command_truncated)),
659 ("error_category".to_string(), json!(error_category)),
660 ("error_category_truncated".to_string(), json!(error_category_truncated)),
661 ("error_code".to_string(), json!(error_code)),
662 ("error_code_truncated".to_string(), json!(error_code_truncated)),
663 ]),
664 });
665 }
666 }
667 }
668
669 let exit_code = map_outcome_to_exit(&outcome);
670 let (outcome_error_category, outcome_error_category_truncated) =
671 bounded_optional_message(match &outcome {
672 HandlerOutcome::Success(_) => None,
673 HandlerOutcome::Error(err) => Some(err.error.category.as_str()),
674 });
675 let (outcome_error_code, outcome_error_code_truncated) =
676 bounded_optional_message(match &outcome {
677 HandlerOutcome::Success(_) => None,
678 HandlerOutcome::Error(err) => Some(err.error.code.as_str()),
679 });
680 let emission = map_outcome_to_emission(outcome, ctx.policy.quiet);
681 let emission_stream = emission.as_ref().map(|item| match item.stream {
682 OutputStream::Stdout => "stdout",
683 OutputStream::Stderr => "stderr",
684 });
685
686 trace_events.push(InvocationEvent {
687 timestamp: event_timestamp(),
688 name: "dispatch.finish".to_string(),
689 payload: BTreeMap::from([
690 ("command".to_string(), json!(trace_command)),
691 ("command_truncated".to_string(), json!(trace_command_truncated)),
692 ("mode".to_string(), json!("standard")),
693 ("exit_code".to_string(), json!(exit_code as i32)),
694 ("exit_kind".to_string(), json!(exit_code_kind(exit_code))),
695 ("quiet".to_string(), json!(ctx.policy.quiet)),
696 ("error_category".to_string(), json!(outcome_error_category)),
697 ("error_category_truncated".to_string(), json!(outcome_error_category_truncated)),
698 ("error_code".to_string(), json!(outcome_error_code)),
699 ("error_code_truncated".to_string(), json!(outcome_error_code_truncated)),
700 ("emission".to_string(), json!(emission_stream)),
701 ("duration_ms".to_string(), json!(started_at.elapsed().as_millis())),
702 ]),
703 });
704
705 for hook in diagnostics {
706 let command_joined = ctx.intent.command_path.join(" ");
707 let (command, command_truncated) = bounded_command(&command_joined);
708 hook.record(DiagnosticRecord {
709 id: "kernel_dispatch_completed".to_string(),
710 severity: "info".to_string(),
711 message: "kernel dispatch finished".to_string(),
712 fields: BTreeMap::from([
713 ("command".to_string(), json!(command)),
714 ("command_truncated".to_string(), json!(command_truncated)),
715 ("exit_code".to_string(), json!(exit_code as i32)),
716 ("exit_kind".to_string(), json!(exit_code_kind(exit_code))),
717 ("duration_ms".to_string(), json!(started_at.elapsed().as_millis())),
718 ]),
719 });
720 }
721
722 Ok(ExecutionResult {
723 exit_code,
724 emission,
725 trace: if ctx.trace_mode {
726 Some(InvocationTrace {
727 invocation_id: next_invocation_id(),
728 command: CommandPath {
729 segments: ctx
730 .intent
731 .command_path
732 .iter()
733 .map(|v| Namespace(v.clone()))
734 .collect(),
735 },
736 policy: ctx.policy.clone(),
737 events: trace_events,
738 })
739 } else {
740 None
741 },
742 })
743}