1use std::path::PathBuf;
2use std::sync::{atomic::AtomicBool, atomic::Ordering, Arc};
3
4fn report_adapter_snapshot(
5 reporter: Option<&byteor_app_kit::agent::RuntimeReporter>,
6 stats: &byteor_adapters::AdapterLoopStats,
7 error: Option<String>,
8) {
9 let Some(reporter) = reporter else {
10 return;
11 };
12
13 if let Err(report_error) = reporter.report_snapshot_now("single_ring", || {
14 Ok(adapter_snapshot_payload(stats, error.clone()))
15 }) {
16 eprintln!("agent snapshot failed: {report_error}");
17 }
18 if let Err(report_error) = reporter.report_heartbeat_now() {
19 eprintln!("agent heartbeat failed: {report_error}");
20 }
21}
22
23fn adapter_snapshot_payload(
24 stats: &byteor_adapters::AdapterLoopStats,
25 error: Option<String>,
26) -> serde_json::Value {
27 let mut snapshot = serde_json::json!({
28 "mode": "adapter_loop",
29 "ok": error.is_none(),
30 "ingress_messages": stats.ingress_messages,
31 "egress_messages": stats.egress_messages,
32 });
33 if let Some(error) = error {
34 snapshot["error"] = serde_json::json!(error);
35 }
36 stats.merge_into_snapshot(&mut snapshot);
37 snapshot
38}
39
40fn main() {
41 let mut args = std::env::args().skip(1);
45 let cmd = args.next().unwrap_or_else(|| "help".to_string());
46
47 match cmd.as_str() {
48 "help" | "-h" | "--help" => {
49 eprintln!(
50 "byteor-actiongraph\n\nUSAGE:\n byteor-actiongraph validate --spec=/path\n byteor-actiongraph describe --spec=/path\n byteor-actiongraph dot --spec=/path\n byteor-actiongraph run --spec=/path [--profile=default|low-latency|isolated-core] [--config=/path] [--lane-path=/path] [--shm-parent=/path] [--mlockall=on|off] [--wait=spin|backoff] [--pin=none|balanced|physical] [--sched=other|fifo|rr] [--rt-prio=N] [--dry-run] [--env=dev|staging|prod] [--approval=<credential>] [--approval-key-set=/path] # long-running (Ctrl-C to stop)\n byteor-actiongraph run --sp --spec=/path [--profile=default|low-latency|isolated-core] [--config=/path] [--lane-path=/path] [--shm-parent=/path] [--mlockall=on|off] [--wait=spin|backoff] [--pin=none|balanced|physical] [--sched=other|fifo|rr] [--rt-prio=N] [--input-hex=..|--input-ascii=..] [--dry-run] [--env=dev|staging|prod] [--approval=<credential>] [--approval-key-set=/path] # one-shot bring-up\n byteor-actiongraph doctor [--profile=default|low-latency|isolated-core] [--mlockall=on|off] [--shm-parent=/path]\n byteor-actiongraph snapshot --ring-path=/path [--active-gating=N]\n byteor-actiongraph incident-bundle --out=/path --spec=/path [--config=/path] [--ring-path=/path]\n\nNOTES:\n - Action stages are resolved via `StageOpV1::ResolverKey` using `byteor-actions` (http_post:* / exec:*).\n - Side effects are gated by `byteor-policy` and may require `--approval` in staging/prod.\n"
51 );
52 std::process::exit(2);
53 }
54 "validate" => {
55 let mut spec: Option<PathBuf> = None;
56 for a in args {
57 if let Some(v) = a.strip_prefix("--spec=") {
58 spec = Some(PathBuf::from(v));
59 } else {
60 eprintln!("unknown arg: {a}");
61 std::process::exit(2);
62 }
63 }
64
65 let Some(spec) = spec else {
66 eprintln!("missing --spec");
67 std::process::exit(2);
68 };
69
70 match byteor_runtime::validate_spec_kv_v1(&spec) {
71 Ok(()) => {
72 println!("{}", serde_json::json!({"ok": true}));
73 std::process::exit(0);
74 }
75 Err(e) => {
76 eprintln!("validate failed: {e}");
77 println!("{}", serde_json::json!({"ok": false, "error": e}));
78 std::process::exit(1);
79 }
80 }
81 }
82 "describe" => {
83 let mut spec: Option<PathBuf> = None;
84 for a in args {
85 if let Some(v) = a.strip_prefix("--spec=") {
86 spec = Some(PathBuf::from(v));
87 } else {
88 eprintln!("unknown arg: {a}");
89 std::process::exit(2);
90 }
91 }
92
93 let Some(spec) = spec else {
94 eprintln!("missing --spec");
95 std::process::exit(2);
96 };
97
98 match byteor_runtime::describe_spec_kv_v1(&spec) {
99 Ok(s) => {
100 print!("{s}");
101 std::process::exit(0);
102 }
103 Err(e) => {
104 eprintln!("describe failed: {e}");
105 std::process::exit(1);
106 }
107 }
108 }
109 "dot" => {
110 let mut spec: Option<PathBuf> = None;
111 for a in args {
112 if let Some(v) = a.strip_prefix("--spec=") {
113 spec = Some(PathBuf::from(v));
114 } else {
115 eprintln!("unknown arg: {a}");
116 std::process::exit(2);
117 }
118 }
119
120 let Some(spec) = spec else {
121 eprintln!("missing --spec");
122 std::process::exit(2);
123 };
124
125 match byteor_runtime::dot_spec_kv_v1(&spec) {
126 Ok(s) => {
127 print!("{s}");
128 std::process::exit(0);
129 }
130 Err(e) => {
131 eprintln!("dot failed: {e}");
132 std::process::exit(1);
133 }
134 }
135 }
136 "run" => {
137 let mut sp = false;
138 let mut spec: Option<PathBuf> = None;
139 let mut lane_path: Option<PathBuf> = None;
140 let mut rt_prio: Option<u8> = None;
141 let mut input_hex: Option<String> = None;
142 let mut input_ascii: Option<String> = None;
143 let mut dry_run = false;
144
145 let mut env = byteor_core::config::Environment::Dev;
146 let mut approval: Option<String> = None;
147 let mut approval_key_sets: Vec<PathBuf> = Vec::new();
148 let mut config_path: Option<PathBuf> = None;
149 let mut tuning = byteor_runtime::TuningCliState::new(byteor_core::paths::shm_parent_from_env());
150
151 for a in args {
152 if a == "--sp" {
153 sp = true;
154 } else if a == "--dry-run" {
155 dry_run = true;
156 } else if let Some(v) = a.strip_prefix("--dry-run=") {
157 dry_run = matches!(v, "on" | "1" | "true" | "yes");
158 } else if let Some(v) = a.strip_prefix("--spec=") {
159 spec = Some(PathBuf::from(v));
160 } else if let Some(v) = a.strip_prefix("--lane-path=") {
161 lane_path = Some(PathBuf::from(v));
162 } else if let Some(v) = a.strip_prefix("--profile=") {
163 let profile = byteor_runtime::TuningProfile::parse(v).unwrap_or_else(|| {
164 eprintln!(
165 "unknown --profile: {v} (expected default|low-latency|isolated-core)"
166 );
167 std::process::exit(2);
168 });
169 tuning.set_profile(profile);
170 } else if let Some(v) = a.strip_prefix("--shm-parent=") {
171 tuning.set_shm_parent(PathBuf::from(v));
172 } else if let Some(v) = a.strip_prefix("--mlockall=") {
173 let preset = match v {
174 "on" | "1" | "true" | "yes" => byteor_runtime::MlockallPreset::On,
175 "off" | "0" | "false" | "no" | "none" => {
176 byteor_runtime::MlockallPreset::None
177 }
178 _ => {
179 eprintln!("unknown --mlockall: {v} (expected on|off)");
180 std::process::exit(2);
181 }
182 };
183 tuning.set_mlockall(preset);
184 } else if let Some(v) = a.strip_prefix("--sched=") {
185 let parsed_sched = match byteor_runtime::SchedPreset::parse_kind(v) {
186 Some(x) => x,
187 None => {
188 eprintln!("unknown --sched: {v} (expected other|fifo|rr)");
189 std::process::exit(2);
190 }
191 };
192 tuning.set_sched(parsed_sched);
193 } else if let Some(v) = a.strip_prefix("--rt-prio=") {
194 rt_prio = Some(v.parse().unwrap_or_else(|_| {
195 eprintln!("invalid --rt-prio: {v}");
196 std::process::exit(2);
197 }));
198 } else if let Some(v) = a.strip_prefix("--env=") {
199 env = match byteor_core::config::Environment::parse(v) {
200 Some(x) => x,
201 None => {
202 eprintln!("unknown --env: {v} (expected dev|staging|prod)");
203 std::process::exit(2);
204 }
205 };
206 } else if let Some(v) = a.strip_prefix("--approval=") {
207 approval = Some(v.to_string());
208 } else if let Some(v) = a.strip_prefix("--approval-key-set=") {
209 approval_key_sets.push(PathBuf::from(v));
210 } else if let Some(v) = a.strip_prefix("--config=") {
211 config_path = Some(PathBuf::from(v));
212 } else if let Some(v) = a.strip_prefix("--wait=") {
213 let parsed_wait = byteor_runtime::parse_wait_preset(v).unwrap_or_else(|| {
214 eprintln!("unknown --wait preset: {v}");
215 std::process::exit(2);
216 });
217 tuning.set_wait(parsed_wait);
218 } else if let Some(v) = a.strip_prefix("--pin=") {
219 let parsed_pinning = match byteor_runtime::PinningPreset::parse(v) {
220 Some(p) => p,
221 None => {
222 eprintln!(
223 "unknown --pin preset: {v} (expected none|balanced|physical)"
224 );
225 std::process::exit(2);
226 }
227 };
228 tuning.set_pinning(parsed_pinning);
229 } else if let Some(v) = a.strip_prefix("--input-hex=") {
230 input_hex = Some(v.to_string());
231 } else if let Some(v) = a.strip_prefix("--input-ascii=") {
232 input_ascii = Some(v.to_string());
233 } else {
234 eprintln!("unknown arg: {a}");
235 std::process::exit(2);
236 }
237 }
238 let Some(spec) = spec else {
239 eprintln!("missing --spec");
240 std::process::exit(2);
241 };
242
243 let mut resolved_tuning = tuning.resolve();
244 if let Some(p) = rt_prio {
245 if resolved_tuning.sched == byteor_runtime::SchedPreset::Other {
246 eprintln!("--rt-prio requires --sched=fifo|rr");
247 std::process::exit(2);
248 }
249 if !(1..=99).contains(&p) {
250 eprintln!("invalid --rt-prio: {p} (expected 1..=99)");
251 std::process::exit(2);
252 }
253 resolved_tuning.sched = resolved_tuning.sched.with_rt_prio(p);
254 }
255 let profile = resolved_tuning.profile;
256 let wait = resolved_tuning.wait;
257 let pinning = resolved_tuning.pinning;
258 let mlockall = resolved_tuning.mlockall;
259 let sched = resolved_tuning.sched;
260 let shm_parent = resolved_tuning.shm_parent.clone();
261 let tuning_report = byteor_runtime::effective_tuning_report(profile, &resolved_tuning);
262
263 let lane_path = byteor_core::paths::default_lane_path_for_run(
264 byteor_core::Product::Actiongraph,
265 lane_path,
266 &shm_parent,
267 );
268
269 if dry_run {
270 match byteor_runtime::read_spec_kv_v1(&spec) {
271 Ok(spec_v1) => {
272 let stages = spec_v1.single_ring().map(|r| r.stages.len()).unwrap_or(0);
273 let resolved_config_path = config_path
274 .clone()
275 .or_else(|| byteor_runtime::default_bundle_config_path(&spec));
276 println!(
277 "{}",
278 serde_json::json!({
279 "ok": true,
280 "dry_run": true,
281 "model": format!("{:?}", spec_v1.model()).to_lowercase(),
282 "stages": stages,
283 "lane_path": lane_path.display().to_string(),
284 "config_path": resolved_config_path
285 .as_ref()
286 .map(|path| path.display().to_string()),
287 })
288 );
289 std::process::exit(0);
290 }
291 Err(e) => {
292 eprintln!("dry-run failed: {e}");
293 println!("{}", serde_json::json!({"ok": false, "error": e}));
294 std::process::exit(1);
295 }
296 }
297 }
298
299 if let Err(e) = byteor_runtime::apply_mlockall_preset(mlockall) {
300 eprintln!(
301 "{}",
302 byteor_runtime::tuning_host_readiness_error(format!(
303 "mlockall apply failed: preset={} err={e}",
304 mlockall.as_str()
305 ))
306 );
307 std::process::exit(1);
308 }
309
310 if let Err(e) = byteor_runtime::apply_sched_preset(sched) {
311 eprintln!(
312 "{}",
313 byteor_runtime::tuning_host_readiness_error(format!(
314 "sched apply failed: sched={} prio={:?} err={e}",
315 sched.kind_str(),
316 sched.rt_prio()
317 ))
318 );
319 std::process::exit(1);
320 }
321
322 let canonical_spec_kv = match byteor_runtime::canonical_encode_spec_kv_v1(&spec) {
323 Ok(spec_kv) => spec_kv,
324 Err(e) => {
325 eprintln!("canonical spec failed: {e}");
326 println!("{}", serde_json::json!({"ok": false, "error": e}));
327 std::process::exit(1);
328 }
329 };
330
331 if approval.is_none() {
332 approval =
333 match byteor_app_kit::approval::load_approval_credential_from_bundle(&spec) {
334 Ok(credential) => credential,
335 Err(e) => {
336 eprintln!("approval credential load failed: {e}");
337 println!("{}", serde_json::json!({"ok": false, "error": e}));
338 std::process::exit(1);
339 }
340 };
341 }
342
343 if approval_key_sets.is_empty() {
344 approval_key_sets =
345 byteor_app_kit::approval::discover_trusted_approval_key_paths(&spec);
346 }
347
348 let approval_refresh_bootstrap =
349 match byteor_app_kit::approval::approval_refresh_bootstrap_from_env() {
350 Ok(bootstrap) => bootstrap,
351 Err(e) => {
352 eprintln!("approval key refresh bootstrap failed: {e}");
353 println!("{}", serde_json::json!({"ok": false, "error": e}));
354 std::process::exit(1);
355 }
356 };
357
358 let trusted_approval_cache =
359 match byteor_app_kit::approval::load_runtime_trusted_approval_key_cache(
360 &spec,
361 &approval_key_sets,
362 approval_refresh_bootstrap.as_ref(),
363 ) {
364 Ok(cache) => cache,
365 Err(e) => {
366 eprintln!("approval key set load failed: {e}");
367 println!("{}", serde_json::json!({"ok": false, "error": e}));
368 std::process::exit(1);
369 }
370 };
371
372 let approval_resolution = match byteor_app_kit::approval::resolve_runtime_approval(
373 &canonical_spec_kv,
374 env,
375 approval.as_deref(),
376 &trusted_approval_cache.keys,
377 ) {
378 Ok(resolution) => resolution,
379 Err(byteor_app_kit::approval::ApprovalVerificationError::UnknownKeyId(_))
380 if approval_refresh_bootstrap.is_some() =>
381 {
382 let refreshed_cache =
383 match byteor_app_kit::approval::refresh_runtime_trusted_approval_key_cache(
384 &spec,
385 &approval_key_sets,
386 approval_refresh_bootstrap
387 .as_ref()
388 .expect("refresh bootstrap present"),
389 ) {
390 Ok(cache) => cache,
391 Err(refresh_error) => {
392 let message = format!(
393 "approval verification failed: unknown approval signing key and refresh failed: {refresh_error}"
394 );
395 eprintln!("{message}");
396 println!("{}", serde_json::json!({"ok": false, "error": message}));
397 std::process::exit(1);
398 }
399 };
400
401 match byteor_app_kit::approval::resolve_runtime_approval(
402 &canonical_spec_kv,
403 env,
404 approval.as_deref(),
405 &refreshed_cache.keys,
406 ) {
407 Ok(resolution) => resolution,
408 Err(e) => {
409 let message = format!("approval verification failed: {e}");
410 eprintln!("{message}");
411 println!("{}", serde_json::json!({"ok": false, "error": message}));
412 std::process::exit(1);
413 }
414 }
415 }
416 Err(e) => {
417 let message = format!("approval verification failed: {e}");
418 eprintln!("{message}");
419 println!("{}", serde_json::json!({"ok": false, "error": message}));
420 std::process::exit(1);
421 }
422 };
423
424 let reporting_config = match byteor_app_kit::agent::reporting_config_from_bootstrap(
425 approval_refresh_bootstrap.as_ref(),
426 ) {
427 Ok(config) => config,
428 Err(e) => {
429 eprintln!("agent reporting bootstrap failed: {e}");
430 println!("{}", serde_json::json!({"ok": false, "error": e}));
431 std::process::exit(1);
432 }
433 };
434
435 let approval_trust = match byteor_app_kit::agent::approval_trust_status(
436 &trusted_approval_cache,
437 approval.as_deref(),
438 ) {
439 Ok(status) => Some(status),
440 Err(e) => {
441 eprintln!("agent approval trust export failed: {e}");
442 println!("{}", serde_json::json!({"ok": false, "error": e}));
443 std::process::exit(1);
444 }
445 };
446
447 let mut stage_catalog = byteor_runtime::default_stage_catalog_entries();
448 stage_catalog.extend(byteor_app_kit::actions::action_stage_catalog_entries());
449 let capability_document =
450 byteor_app_kit::capabilities::runtime_capability_document(
451 "byteor-actiongraph",
452 stage_catalog,
453 );
454
455 let mut runtime_reporter = reporting_config.map(|config| {
456 byteor_app_kit::agent::RuntimeReporter::new(
457 config,
458 env!("CARGO_PKG_VERSION").to_string(),
459 approval_resolution.spec_hash.clone(),
460 capability_document,
461 approval_trust.clone(),
462 )
463 });
464
465 let resolver = byteor_app_kit::actions::build_action_stages_default_replay(
466 env,
467 approval_resolution.approval_token_present,
468 false, false, );
471
472 let spec_v1 = match byteor_runtime::read_spec_kv_v1(&spec) {
473 Ok(spec_v1) => spec_v1,
474 Err(e) => {
475 eprintln!("read spec failed: {e}");
476 println!("{}", serde_json::json!({"ok": false, "error": e}));
477 std::process::exit(1);
478 }
479 };
480
481 if spec_v1.single_ring().is_some()
482 && !sp
483 && input_hex.is_none()
484 && input_ascii.is_none()
485 {
486 let bundle_bindings = match byteor_runtime::load_single_ring_bundle_adapter_bindings(
487 &spec,
488 config_path.as_deref(),
489 ) {
490 Ok(bindings) => bindings,
491 Err(e) => {
492 eprintln!("adapter bundle load failed: {e}");
493 println!("{}", serde_json::json!({"ok": false, "error": e}));
494 std::process::exit(1);
495 }
496 };
497
498 if let Some(bindings) = bundle_bindings {
499 let ingress_label = bindings.ingress.label();
500 let egress_label = bindings.egress.label();
501 let mut ingress = match byteor_runtime::open_ingress_endpoint(&bindings.ingress)
502 {
503 Ok(ingress) => ingress,
504 Err(e) => {
505 eprintln!("open ingress failed: {e}");
506 println!("{}", serde_json::json!({"ok": false, "error": e}));
507 std::process::exit(1);
508 }
509 };
510 let mut egress = match byteor_runtime::open_egress_endpoint(&bindings.egress) {
511 Ok(egress) => egress,
512 Err(e) => {
513 eprintln!("open egress failed: {e}");
514 println!("{}", serde_json::json!({"ok": false, "error": e}));
515 std::process::exit(1);
516 }
517 };
518
519 println!(
520 "{}",
521 serde_json::json!({
522 "ok": true,
523 "running": true,
524 "model": "single_ring",
525 "mode": "adapter_loop",
526 "config_path": bindings.config_path.display().to_string(),
527 "ingress_endpoint": bindings.ingress_name,
528 "egress_endpoint": bindings.egress_name,
529 "ingress_adapter": bindings.ingress_adapter,
530 "egress_adapter": bindings.egress_adapter,
531 "ingress": ingress_label,
532 "egress": egress_label,
533 "tuning": tuning_report.clone(),
534 })
535 );
536
537 if let Some(reporter) = runtime_reporter.as_ref() {
538 if let Err(e) = reporter.report_heartbeat_now() {
539 eprintln!("agent heartbeat failed: {e}");
540 }
541 }
542
543 match byteor_adapters::run_single_ring_adapter_loop(
544 &spec_v1,
545 &resolver,
546 &mut ingress,
547 &mut egress,
548 ) {
549 Ok(stats) => {
550 report_adapter_snapshot(runtime_reporter.as_ref(), &stats, None);
551 println!(
552 "{}",
553 serde_json::json!({
554 "ok": true,
555 "stopped": true,
556 "model": "single_ring",
557 "mode": "adapter_loop",
558 "ingress_messages": stats.ingress_messages,
559 "egress_messages": stats.egress_messages,
560 })
561 );
562 std::process::exit(0);
563 }
564 Err(e) => {
565 report_adapter_snapshot(
566 runtime_reporter.as_ref(),
567 &e.stats,
568 Some(e.to_string()),
569 );
570 eprintln!("run failed: {e}");
571 println!(
572 "{}",
573 serde_json::json!({
574 "ok": false,
575 "error": e.to_string(),
576 "model": "single_ring",
577 "mode": "adapter_loop",
578 "ingress_messages": e.stats.ingress_messages,
579 "egress_messages": e.stats.egress_messages,
580 })
581 );
582 std::process::exit(1);
583 }
584 }
585 }
586 }
587
588 if spec_v1.single_ring().is_none() {
589 if input_hex.is_some() || input_ascii.is_some() {
590 eprintln!("--input-* is not supported for lane_graph");
591 std::process::exit(2);
592 }
593
594 let stop = Arc::new(AtomicBool::new(false));
595 {
596 let stop2 = stop.clone();
597 ctrlc::set_handler(move || {
598 stop2.store(true, Ordering::Relaxed);
599 })
600 .unwrap_or_else(|e| {
601 eprintln!("failed to install ctrlc handler: {e}");
602 std::process::exit(2);
603 });
604 }
605
606 let thread_init = byteor_runtime::thread_init_hook_for_tuning(pinning, sched)
607 .unwrap_or_else(|e| {
608 eprintln!(
609 "{}",
610 byteor_runtime::tuning_host_readiness_error(format!(
611 "tuning init failed: {e}"
612 ))
613 );
614 std::process::exit(2);
615 });
616
617 let engine = byteor_engine::Engine::new(byteor_engine::EngineConfig {
618 stage_failure: byteor_engine::StageFailurePolicy::StopPipeline,
619 wait,
620 thread_init,
621 });
622
623 println!(
624 "{}",
625 serde_json::json!({
626 "ok": true,
627 "running": true,
628 "model": "lane_graph",
629 "mode": "sp",
630 "tuning": tuning_report,
631 })
632 );
633
634 let rt = engine
635 .spawn_lane_graph_kv_v1_runtime(&spec, &resolver)
636 .unwrap_or_else(|e| {
637 eprintln!("run failed: {e}");
638 println!(
639 "{}",
640 serde_json::json!({"ok": false, "error": e.to_string()})
641 );
642 std::process::exit(1);
643 });
644
645 let lane_root = byteor_runtime::default_lane_graph_shm_dir(&spec);
646
647 while !stop.load(Ordering::Relaxed) {
648 if let Some(reporter) = runtime_reporter.as_mut() {
649 if let Err(e) = reporter.report_heartbeat_if_due() {
650 eprintln!("agent heartbeat failed: {e}");
651 }
652 if let Err(e) = reporter.report_snapshot_if_due("lane_graph", || {
653 let role_activity = rt.role_activity_snapshot();
654 byteor_runtime::snapshot_lane_graph_json(
655 &spec,
656 Some(lane_root.as_path()),
657 Some(role_activity.as_slice()),
658 )
659 }) {
660 eprintln!("agent snapshot failed: {e}");
661 }
662 }
663 std::thread::sleep(std::time::Duration::from_millis(50));
664 }
665
666 if let Some(reporter) = runtime_reporter.as_ref() {
667 if let Err(e) = reporter.report_heartbeat_now() {
668 eprintln!("agent heartbeat failed: {e}");
669 }
670 if let Err(e) = reporter.report_snapshot_now("lane_graph", || {
671 let role_activity = rt.role_activity_snapshot();
672 byteor_runtime::snapshot_lane_graph_json(
673 &spec,
674 Some(lane_root.as_path()),
675 Some(role_activity.as_slice()),
676 )
677 }) {
678 eprintln!("agent snapshot failed: {e}");
679 }
680 }
681
682 if let Err(e) = rt.stop_and_join() {
683 eprintln!("stop/join failed: {e}");
684 println!(
685 "{}",
686 serde_json::json!({"ok": false, "error": e.to_string()})
687 );
688 std::process::exit(1);
689 }
690
691 println!("{}", serde_json::json!({"ok": true, "stopped": true}));
692 std::process::exit(0);
693 }
694
695 if !sp {
696 if input_hex.is_some() || input_ascii.is_some() {
697 eprintln!("--input-* is only supported for --sp");
698 std::process::exit(2);
699 }
700
701 let stop = Arc::new(AtomicBool::new(false));
702 {
703 let stop2 = stop.clone();
704 ctrlc::set_handler(move || {
705 stop2.store(true, Ordering::Relaxed);
706 })
707 .unwrap_or_else(|e| {
708 eprintln!("failed to install ctrlc handler: {e}");
709 std::process::exit(2);
710 });
711 }
712
713 println!(
714 "{}",
715 serde_json::json!({
716 "ok": true,
717 "running": true,
718 "model": "single_ring",
719 "lane_path": lane_path.display().to_string(),
720 })
721 );
722
723 match byteor_runtime::run_kv_v1_single_ring_supervised_until_stop_with_resolver_and_tuning(
724 &spec,
725 lane_path,
726 wait,
727 stop,
728 &resolver,
729 pinning,
730 sched,
731 ) {
732 Ok(()) => {
733 println!("{}", serde_json::json!({"ok": true, "stopped": true}));
734 std::process::exit(0);
735 }
736 Err(e) => {
737 eprintln!("run failed: {e}");
738 println!("{}", serde_json::json!({"ok": false, "error": e}));
739 std::process::exit(1);
740 }
741 }
742 }
743
744 let input =
745 match byteor_runtime::parse_run_input(input_hex.as_deref(), input_ascii.as_deref())
746 {
747 Ok(v) => v,
748 Err(e) => {
749 eprintln!("invalid input: {e}");
750 std::process::exit(2);
751 }
752 };
753
754 match byteor_runtime::run_sp_kv_v1_single_ring_with_resolver_and_tuning(
755 &spec, lane_path, input, wait, &resolver, pinning, sched,
756 ) {
757 Ok(v) => {
758 println!("{}", serde_json::to_string_pretty(&v).unwrap());
759 std::process::exit(0);
760 }
761 Err(e) => {
762 eprintln!("run failed: {e}");
763 println!("{}", serde_json::json!({"ok": false, "error": e}));
764 std::process::exit(1);
765 }
766 }
767 }
768 "doctor" => {
769 let mut tuning = byteor_runtime::TuningCliState::new(byteor_core::paths::shm_parent_from_env());
770
771 for a in args {
772 if let Some(v) = a.strip_prefix("--profile=") {
773 let profile = byteor_runtime::TuningProfile::parse(v).unwrap_or_else(|| {
774 eprintln!(
775 "unknown --profile: {v} (expected default|low-latency|isolated-core)"
776 );
777 std::process::exit(2);
778 });
779 tuning.set_profile(profile);
780 } else if let Some(v) = a.strip_prefix("--mlockall=") {
781 let mlockall = match v {
782 "on" | "1" | "true" | "yes" => byteor_runtime::MlockallPreset::On,
783 "off" | "0" | "false" | "no" | "none" => byteor_runtime::MlockallPreset::None,
784 _ => {
785 eprintln!("unknown --mlockall: {v} (expected on|off)");
786 std::process::exit(2);
787 }
788 };
789 tuning.set_mlockall(mlockall);
790 } else if let Some(v) = a.strip_prefix("--shm-parent=") {
791 tuning.set_shm_parent(PathBuf::from(v));
792 } else {
793 eprintln!("unknown arg: {a}");
794 std::process::exit(2);
795 }
796 }
797
798 let resolved = tuning.resolve();
799 let report = byteor_runtime::doctor_with_tuning(&resolved);
800 byteor_runtime::eprint_human(&report.checks);
801 println!("{}", serde_json::to_string_pretty(&report).unwrap());
802 std::process::exit(if report.ok { 0 } else { 1 });
803 }
804 "snapshot" => {
805 let mut ring_path: Option<PathBuf> = None;
806 let mut active_gating: usize = 4;
807
808 for a in args {
809 if let Some(v) = a.strip_prefix("--ring-path=") {
810 ring_path = Some(PathBuf::from(v));
811 } else if let Some(v) = a.strip_prefix("--active-gating=") {
812 active_gating = v.parse().unwrap_or(4);
813 } else {
814 eprintln!("unknown arg: {a}");
815 std::process::exit(2);
816 }
817 }
818
819 let Some(ring_path) = ring_path else {
820 eprintln!("missing --ring-path");
821 std::process::exit(2);
822 };
823
824 match byteor_runtime::snapshot_single_ring_json(ring_path, active_gating) {
825 Ok(v) => {
826 println!("{}", serde_json::to_string_pretty(&v).unwrap());
827 std::process::exit(0);
828 }
829 Err(e) => {
830 eprintln!("snapshot failed: {e}");
831 std::process::exit(1);
832 }
833 }
834 }
835 "incident-bundle" => {
836 let mut out: Option<PathBuf> = None;
837 let mut spec: Option<PathBuf> = None;
838 let mut config: Option<PathBuf> = None;
839 let mut ring_path: Option<PathBuf> = None;
840
841 for a in args {
842 if let Some(v) = a.strip_prefix("--out=") {
843 out = Some(PathBuf::from(v));
844 } else if let Some(v) = a.strip_prefix("--spec=") {
845 spec = Some(PathBuf::from(v));
846 } else if let Some(v) = a.strip_prefix("--config=") {
847 config = Some(PathBuf::from(v));
848 } else if let Some(v) = a.strip_prefix("--ring-path=") {
849 ring_path = Some(PathBuf::from(v));
850 } else {
851 eprintln!("unknown arg: {a}");
852 std::process::exit(2);
853 }
854 }
855
856 let Some(out) = out else {
857 eprintln!("missing --out");
858 std::process::exit(2);
859 };
860 let Some(spec) = spec else {
861 eprintln!("missing --spec");
862 std::process::exit(2);
863 };
864
865 let res = byteor_runtime::incident_bundle(
866 &out,
867 &spec,
868 config.as_deref(),
869 ring_path.as_deref(),
870 );
871
872 match res {
873 Ok(()) => {
874 println!(
875 "{}",
876 serde_json::json!({"ok": true, "out": out.display().to_string() })
877 );
878 std::process::exit(0);
879 }
880 Err(e) => {
881 eprintln!("incident-bundle failed: {e}");
882 println!("{}", serde_json::json!({"ok": false, "error": e }));
883 std::process::exit(1);
884 }
885 }
886 }
887 other => {
888 eprintln!("unknown command: {other}");
889 std::process::exit(2);
890 }
891 }
892}