1#[cfg(feature = "a2a")]
14pub mod a2a_server;
15pub mod activity; pub mod artifacts;
17pub mod audit;
18pub mod children;
19pub mod events;
20#[cfg(feature = "exec")]
21pub mod exec; pub mod goal;
23pub mod http_node;
24pub mod human; pub mod nested;
26pub mod reactor;
27pub mod reload;
28pub mod starts;
29pub mod steps;
30pub mod subagents;
31pub mod timers;
32pub mod tools;
33pub mod turns;
34pub mod waits;
35#[cfg(feature = "a2a")]
36pub mod webhooks;
37pub mod worker;
38
39pub use reactor::Runtime;
40
41use crate::config::v2::{Loaded, StoreKind};
42use crate::context::memory::Memory;
43use crate::context::{Contexts, skills, tokens};
44use crate::engine::run::StepStatus;
45use crate::governor::Governor;
46use crate::mcp::client::McpClient;
47use crate::obs::log::{Comp, Level, LogCtx, Logger};
48use crate::registry::{Registry, ServerTools};
49use crate::state::{Durable, Kind, Policy, now_ms};
50use serde_json::{Value, json};
51use std::collections::BTreeMap;
52use std::sync::Arc;
53use std::time::{Duration, Instant};
54
55pub fn run(loaded: &Loaded, args: &[String], env: &[(String, String)]) -> i32 {
57 let settings = loaded.settings.clone();
58 let instance = settings.instance_name();
59 let run_id = match settings.lifecycle.run_id.clone() {
60 Some(r) => r,
61 None => crate::state::ulid::new(),
62 };
63 let trace = crate::obs::trace::resolve(&run_id, settings.observability.traceparent.as_deref());
64 let level = settings
65 .observability
66 .log_level
67 .as_deref()
68 .and_then(Level::parse)
69 .unwrap_or(Level::Info);
70 let log = Logger::new(
71 LogCtx {
72 run_id: run_id.clone(),
73 agent_id: "sup".into(),
74 agent_path: "0".into(),
75 comp: Comp::Supervisor,
76 pid: std::process::id(),
77 trace_id: Some(trace.trace_id.clone()),
78 },
79 level,
80 )
81 .with_content(settings.observability.log_content);
82 log.info("proc.start", json!({"version": crate::VERSION, "runtime": "2.0", "instance": instance, "config_files": loaded.files.iter().map(|(p, _)| p.clone()).collect::<Vec<_>>()}));
83 for w in &loaded.warnings {
84 log.warn("config.warning", json!({"warning": w}));
85 }
86 crate::signals::install();
87 crate::supervisor::reap::set_child_subreaper();
88 let envmap = |k: &str| env.iter().find(|(n, _)| n == k).map(|(_, v)| v.clone());
89
90 #[cfg(feature = "tls")]
92 if let Some(path) = settings.security.tls_ca.as_deref()
93 && let Err(e) = std::fs::read(path).and_then(|pem| crate::net::tls::install_extra_ca(&pem))
94 {
95 log.error(
96 "proc.exit",
97 json!({"code": crate::exit::USAGE, "err": format!("security.tls_ca {path}: {e}")}),
98 );
99 return crate::exit::USAGE;
100 }
101 #[cfg(feature = "aauth")]
103 if let Some(a) = &settings.security.aauth {
104 let v1 = crate::config::AAuthSettings {
105 provider: a.provider.clone(),
106 key_file: a
107 .key_file
108 .clone()
109 .unwrap_or_else(|| "/var/lib/agentd/aauth-key".into()),
110 enrollment_token: a.enroll_token.as_ref().map(|s| s.0.clone()),
111 enroll_assertion_file: a.enroll_assertion_file.clone(),
112 person_server: a.person_server.clone(),
113 };
114 if let Err(e) = crate::aauth::setup(&v1, Duration::from_secs(30)) {
115 log.error(
116 "proc.exit",
117 json!({"code": crate::exit::USAGE, "err": format!("aauth: {e}")}),
118 );
119 return crate::exit::USAGE;
120 }
121 }
122
123 #[cfg(unix)]
128 if settings.security.cgroup.spec.is_some() {
129 let c = &settings.security.cgroup;
130 if let Some(configured) = crate::supervisor::cgroup::configure(
131 c.spec.as_deref(),
132 c.memory_max.as_deref(),
133 c.pids_max.as_deref(),
134 ) {
135 log.info("cgroup.armed", json!({"parent": configured.parent.display().to_string(), "limits_unavailable": configured.limits_unavailable}));
136 }
137 }
138
139 let intel_uri = settings.intelligence.endpoint_list().unwrap_or_default();
141 let intel_token = match resolve_intel_token(&settings, &envmap) {
142 Ok(t) => t,
143 Err(e) => {
144 log.error("proc.exit", json!({"code": crate::exit::USAGE, "err": e}));
145 return crate::exit::USAGE;
146 }
147 };
148 let model = settings.intelligence.model.clone().unwrap_or_default();
149 let intel_headers: Vec<(String, String)> = settings
152 .intelligence
153 .headers
154 .iter()
155 .filter_map(|(k, v)| {
156 crate::sec::secret::resolve(v, &envmap)
157 .ok()
158 .map(|r| (k.clone(), r))
159 })
160 .collect();
161 let intel_bearer = intel_bearer_provider(&settings);
162
163 let mut mcp: BTreeMap<String, Arc<McpClient>> = BTreeMap::new();
166 let mut mcp_specs = BTreeMap::new();
167 let mut server_tools: Vec<ServerTools> = Vec::new();
168 let mcp_timeout = settings
169 .mcp
170 .default_timeout
171 .map(|d| d.0)
172 .unwrap_or(Duration::from_secs(60));
173 for s in &settings.mcp.servers {
174 let spec = match s.to_spec() {
175 Ok(sp) => sp,
176 Err(e) => {
177 log.error("proc.exit", json!({"code": crate::exit::USAGE, "err": e}));
178 return crate::exit::USAGE;
179 }
180 };
181 let per_timeout = s.timeout.map(|d| d.0).unwrap_or(mcp_timeout);
182 match crate::mcp::from_spec(&spec, per_timeout).and_then(|mut c| c.initialize().map(|()| c))
183 {
184 Ok(mut c) => {
185 let mut meta = json!({"agent/run_id": run_id, "agent/instance": instance});
186 meta["traceparent"] =
187 crate::obs::trace::outbound_traceparent(&trace.trace_id).into();
188 c.set_tool_meta(meta);
189 let tools = c.list_tools().unwrap_or_default();
190 log.info(
191 "mcp.connect",
192 json!({"server": s.name, "tools": tools.len()}),
193 );
194 server_tools.push(ServerTools {
195 name: s.name.clone(),
196 ns: s.ns.clone(),
197 tags: spec.tags.clone(),
198 tools,
199 });
200 mcp.insert(s.name.clone(), Arc::new(c));
201 }
202 Err(e) => {
203 log.warn(
204 "mcp.connect.fail",
205 json!({"server": s.name, "err": e.to_string()}),
206 );
207 crate::obs::metrics::record_mcp_connect_failure(&s.name);
208 }
209 }
210 mcp_specs.insert(s.name.clone(), spec);
211 }
212
213 let store = match settings.store.kind {
217 StoreKind::None => {
218 log.warn(
219 "store.none",
220 json!({"note": "no durable store: state lives in this process only (job shape)"}),
221 );
222 Arc::new(crate::store::memory::MemoryStore::new()) as crate::store::SharedStore
223 }
224 _ => {
225 let mcp_ref = mcp.clone();
226 match crate::store::open(&settings.store, &|name: &str| {
227 mcp_ref
228 .get(name)
229 .map(|c| c.clone() as Arc<dyn crate::store::mcp::McpCall>)
230 }) {
231 Ok(Some(s)) => s,
232 Ok(None) => Arc::new(crate::store::memory::MemoryStore::new()),
233 Err(e) => {
234 log.error("proc.exit", json!({"code": crate::exit::MCP_REQUIRED_DOWN, "err": format!("store: {e}")}));
235 return crate::exit::MCP_REQUIRED_DOWN;
236 }
237 }
238 }
239 };
240 let durable = Durable::new(
241 store,
242 settings.store.prefix(),
243 &instance,
244 Policy::from_settings(&settings.store),
245 Some(log.clone()),
246 );
247
248 let restored = match durable.restore() {
250 Ok(r) => r,
251 Err(e) => {
252 log.error(
253 "proc.exit",
254 json!({"code": crate::exit::MCP_REQUIRED_DOWN, "err": format!("restore: {e}")}),
255 );
256 return crate::exit::MCP_REQUIRED_DOWN;
257 }
258 };
259 if settings.store.kind == StoreKind::File {
268 let root = crate::config::v2::file_store_root(&settings.store);
269 log.info(
270 "store.file",
271 json!({
272 "path": root.display().to_string(),
273 "generation": restored.manifest.as_ref().map(|m| m.generation).unwrap_or(1),
274 "defaulted": loaded.doc.pointer("/store/kind").is_none(),
277 "msg": "durable state is on the local filesystem; it survives a restart of this process but not a move to another host — use store.kind mcp|http for a fleet",
278 }),
279 );
280 }
281
282 let registry = match Registry::build(&settings, &server_tools) {
284 Ok(r) => r,
285 Err(errs) => {
286 for e in &errs {
287 log.error("config.invalid", json!({"error": e}));
288 }
289 log.error(
290 "proc.exit",
291 json!({"code": crate::exit::USAGE, "err": "tool registry"}),
292 );
293 return crate::exit::USAGE;
294 }
295 };
296 for w in ®istry.warnings {
297 log.warn("registry.warning", json!({"warning": w}));
298 }
299
300 let mut catalogue = skills::Catalogue::new(
302 settings
303 .skills
304 .reference_prefix
305 .as_deref()
306 .unwrap_or(skills::DEFAULT_PREFIX),
307 settings.skills.max_bytes.unwrap_or(32_768) as usize,
308 );
309 for src in &settings.skills.sources {
310 match mcp.get(&src.server) {
311 Some(c) => {
312 let mode = match src.discover {
313 crate::config::v2::Discover::Prompts => skills::Discover::Prompts,
314 crate::config::v2::Discover::Resources => skills::Discover::Resources,
315 crate::config::v2::Discover::Auto => skills::Discover::Auto,
316 };
317 let found = catalogue.discover(&**c, mode, src.filter.as_deref());
318 log.info(
319 "skills.discovered",
320 json!({"server": src.server, "count": found.len(), "skills": found}),
321 );
322 }
323 None => log.warn("skills.source.unavailable", json!({"server": src.server})),
324 }
325 }
326
327 let (events_tx, events_rx) = std::sync::mpsc::channel();
329 let (child_tx, child_rx) = std::sync::mpsc::channel();
330 let (reap_tx, reap_rx) = std::sync::mpsc::channel();
331 let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("agentd"));
332
333 let model_window = settings.context.model_window.unwrap_or_else(|| {
334 if model.is_empty() {
335 tokens::DEFAULT_MODEL_WINDOW
336 } else {
337 tokens::window_for_model(&model)
338 }
339 });
340 let mut rt = Runtime {
341 instance: instance.clone(),
342 run_id: run_id.clone(),
343 durable,
344 mcp,
345 mcp_specs,
346 registry,
347 contexts: Contexts::new(model_window),
348 memory: Memory::new(
349 settings.memory.max_value_bytes.unwrap_or(65_536) as usize,
350 settings.memory.list_default_limit.unwrap_or(100) as usize,
351 ),
352 artifacts: artifacts::Artifacts::new(),
353 skills: catalogue,
354 governor: Governor::new(&settings.intelligence.budget),
355 workflows: BTreeMap::new(),
356 runs: BTreeMap::new(),
357 children: children::Children::new(exe, child_tx, reap_tx),
358 timers: timers::Timers::new(),
359 events_rx,
360 events_tx,
361 child_rx,
362 reap_rx,
363 pending: Vec::new(),
364 turn_queue: Default::default(),
365 staged_turns: BTreeMap::new(),
366 inbox_queue: Default::default(),
367 subagents: BTreeMap::new(),
368 instruction: reactor::Instruction {
369 text: String::new(),
370 source: "static",
371 uri: None,
372 server: None,
373 version: 1,
374 },
375 job_shape: false,
376 exit: None,
377 draining: false,
378 paused: false,
379 drain_started: None,
380 drain_reason: String::new(),
381 idle_since: None,
382 intel_uri,
383 intel_token,
384 intel_headers,
385 intel_bearer,
386 model,
387 trace_id: Some(trace.trace_id.clone()),
388 started: Instant::now(),
389 seq: 0,
390 counters: Default::default(),
391 job_runs: Vec::new(),
392 executing: BTreeMap::new(),
393 last_manifest_flush: Instant::now(),
394 goal_judge_at: None,
395 #[cfg(feature = "a2a")]
396 tasks: BTreeMap::new(),
397 #[cfg(feature = "a2a")]
398 event_to_task: BTreeMap::new(),
399 #[cfg(feature = "a2a")]
400 #[cfg(feature = "a2a")]
401 a2a_feed: None,
402 #[cfg(feature = "a2a")]
403 a2a_pairing: None,
404 #[cfg(feature = "a2a")]
405 reserved_task_id: None,
406 #[cfg(feature = "a2a")]
407 a2a_sink: None,
408 #[cfg(feature = "a2a")]
409 a2a_listener: None,
410 activity: BTreeMap::new(),
411 last_root_reply: None,
412 #[cfg(feature = "a2a")]
413 feed_marks: BTreeMap::new(),
414 #[cfg(feature = "a2a")]
415 feed_last: Instant::now(),
416 #[cfg(feature = "a2a")]
417 webhook_callbacks: std::sync::Arc::new(std::sync::Mutex::new(
418 std::collections::HashMap::new(),
419 )),
420 #[cfg(feature = "a2a")]
421 webhook_sync: std::collections::HashMap::new(),
422 settings_doc: loaded.doc.clone(),
423 args: args.to_vec(),
424 env: env.to_vec(),
425 pinned: BTreeMap::new(),
426 recent_signals: BTreeMap::new(),
427 settings,
428 log: log.clone(),
429 };
430
431 let lost_ctx = rt.contexts.restore(restored.of(Kind::Context));
433 if !lost_ctx.is_empty() {
434 log.warn("restore.context.lost", json!({"ids": lost_ctx}));
435 }
436 rt.timers.restore(restored.timers());
437 rt.artifacts.restore(restored.of(Kind::Artifact));
438 for env in restored.of(Kind::Run) {
439 match serde_json::from_value::<crate::engine::RunState>(env.state.clone()) {
440 Ok(mut r) => {
441 r.dirty = false;
442 if !r.status.is_terminal() {
443 for (id, st) in r.steps.iter_mut() {
446 if st.status == StepStatus::Running {
447 log.info(
448 "restore.step.replay",
449 json!({"run": r.id, "step": id, "attempt": st.attempt}),
450 );
451 st.status = StepStatus::Pending;
452 st.worker = None;
453 }
454 }
455 r.status = crate::engine::RunStatus::Running;
456 r.dirty = true;
457 }
458 rt.runs.insert(r.id.clone(), r);
459 }
460 Err(e) => log.warn(
461 "restore.run.corrupt",
462 json!({"id": env.id, "err": e.to_string()}),
463 ),
464 }
465 }
466 for env in restored.of(Kind::Subagent) {
467 match serde_json::from_value::<reactor::SubagentRecord>(env.state.clone()) {
468 Ok(s) => {
469 rt.subagents.insert(s.handle.clone(), s);
470 }
471 Err(e) => log.warn(
472 "restore.subagent.corrupt",
473 json!({"id": env.id, "err": e.to_string()}),
474 ),
475 }
476 }
477 #[cfg(feature = "a2a")]
478 rt.restore_a2a_tasks(restored.of(Kind::Task));
479 if let Some(m) = &restored.manifest {
480 rt.governor.restore(&m.budget, now_ms());
481 }
482 for ev in restored.inbox_pending() {
483 rt.inbox_queue.push_back(ev);
484 }
485 if restored.manifest.is_some() {
486 log.info("restore.adopted", json!({"runs": rt.runs.len(), "contexts": rt.contexts.len(), "subagents": rt.subagents.len(), "timers": rt.timers.len(), "artifacts": rt.artifacts.len(), "inbox_pending": rt.inbox_queue.len(), "lost": restored.lost.len()}));
487 rt.audit(audit::AuditEvent {
490 action: "restore",
491 target: json!({"runs": rt.runs.len(), "subagents": rt.subagents.len(), "inbox_pending": rt.inbox_queue.len(), "lost": restored.lost.len()}),
492 outcome: if restored.lost.is_empty() { "restored" } else { "restored_with_loss" },
493 principal: Some("system"),
494 role: Some("system"),
495 request_id: None,
496 });
497 }
498
499 if let Some(text) = rt.settings.agent.instruction.clone() {
501 if crate::config::v2::looks_like_resource_uri(&text) {
502 match rt.subscribe_instruction(&text) {
503 Ok(()) => {}
504 Err(e) => {
505 log.error("proc.exit", json!({"code": crate::exit::MCP_REQUIRED_DOWN, "err": format!("agent.instruction {text}: {e}")}));
506 return crate::exit::MCP_REQUIRED_DOWN;
507 }
508 }
509 } else {
510 rt.instruction.text = text;
511 }
512 }
513
514 if let Err(errs) = rt.load_workflows() {
516 for e in &errs {
517 log.error("config.invalid", json!({"error": e}));
518 }
519 log.error(
520 "proc.exit",
521 json!({"code": crate::exit::USAGE, "err": "workflow definitions"}),
522 );
523 return crate::exit::USAGE;
524 }
525 rt.job_shape = match rt.settings.lifecycle.run_until {
528 crate::config::v2::RunUntil::Drained => false,
529 crate::config::v2::RunUntil::Idle => true,
530 crate::config::v2::RunUntil::Auto => {
531 rt.settings.a2a.listen.is_none() && !rt.workflows.values().any(|w| w.is_long_lived())
532 }
533 };
534 for r in rt.runs.values() {
536 if rt.job_shape
537 && rt
538 .workflows
539 .get(&r.workflow)
540 .and_then(|w| w.step(&r.start.node))
541 .is_some_and(|s| s.kind == "once")
542 {
543 rt.job_runs.push(r.id.clone());
544 }
545 }
546 let refs = rt.skills.references(&rt.instruction.text.clone());
548 if !refs.is_empty() {
549 let unknown = rt.preload_skills(crate::context::ROOT, &refs, None);
550 for u in unknown {
551 rt.note_root(format!(
552 "skill.unknown: {u:?} referenced by the instruction is not in the catalogue"
553 ));
554 }
555 }
556 #[cfg(all(unix, feature = "config-watch"))]
558 if rt.settings.lifecycle.watch_config {
559 for (path, _) in &loaded.files {
560 crate::config::watch::spawn_config_watcher(std::path::Path::new(path), &log);
561 }
562 }
563 rt.arm_workflows();
564 rt.arm_long_lived_starts();
565 rt.arm_goal();
566 rt.respawn_restored_subagents();
567 #[cfg(feature = "a2a")]
571 if rt.settings.a2a.listen.is_some() {
572 let resolver = match crate::a2a::Resolver::build(&rt.settings.a2a, &envmap) {
573 Ok(r) => r,
574 Err(e) => {
575 log.error(
576 "proc.exit",
577 json!({"code": crate::exit::USAGE, "err": format!("a2a principals: {e}")}),
578 );
579 return crate::exit::USAGE;
580 }
581 };
582 let write_timeout = rt.settings.lifecycle.drain_timeout();
583 match a2a_server::spawn_a2a_listener(
584 &rt.settings.a2a,
585 &rt.settings.interface,
586 rt.events_tx.clone(),
587 resolver,
588 &envmap,
589 write_timeout,
590 log.clone(),
591 ) {
592 Ok(serving) => {
593 rt.a2a_feed = serving.feed;
594 rt.a2a_pairing = serving.pairing;
595 rt.a2a_sink = Some(std::sync::Arc::clone(&serving.listener.sink));
596 rt.a2a_listener = Some(serving.listener);
599 if rt.settings.interface.enabled && rt.settings.interface.debug {
603 let cap = rt
604 .settings
605 .observability
606 .events_ring
607 .map(|n| n as usize)
608 .unwrap_or(crate::obs::log::EVENTS_RING_DEFAULT);
609 crate::obs::log::install_event_ring(cap);
610 log.info("interface.debug", json!({"events_ring": cap}));
611 }
612 for id in rt.tasks.keys().cloned().collect::<Vec<_>>() {
614 rt.task_sync(&id);
615 }
616 }
617 Err(e) => {
618 log.error(
619 "proc.exit",
620 json!({"code": crate::exit::USAGE, "err": format!("a2a listen: {e}")}),
621 );
622 return crate::exit::USAGE;
623 }
624 }
625 }
626 #[cfg(feature = "a2a")]
630 if rt.settings.webhooks.listen.is_some() {
631 let nodes: Vec<(String, String, serde_json::Map<String, serde_json::Value>)> = rt
632 .workflows
633 .values()
634 .flat_map(|wf| {
635 wf.steps
636 .values()
637 .filter(|s| s.kind == "webhook")
638 .map(|s| (wf.name.clone(), s.id.clone(), s.spec.clone()))
639 .collect::<Vec<_>>()
640 })
641 .collect();
642 let write_timeout = rt.settings.lifecycle.drain_timeout();
643 if let Err(e) = webhooks::spawn_webhook_listener(
644 &rt.settings.webhooks,
645 nodes,
646 rt.webhook_callbacks.clone(),
647 rt.events_tx.clone(),
648 &envmap,
649 write_timeout,
650 log.clone(),
651 ) {
652 log.error(
653 "proc.exit",
654 json!({"code": crate::exit::USAGE, "err": format!("webhooks listen: {e}")}),
655 );
656 return crate::exit::USAGE;
657 }
658 }
659 #[cfg(feature = "metrics")]
662 if let Some(addr) = rt.settings.observability.metrics_addr.clone()
663 && let Err(e) = crate::obs::serve::spawn(&addr, log.clone())
664 {
665 log.warn(
666 "metrics.serve.fail",
667 json!({"addr": addr, "err": e.to_string()}),
668 );
669 }
670 if let Some(path) = rt.settings.observability.health_file.clone() {
671 crate::obs::health::spawn_writer(
672 std::path::PathBuf::from(path),
673 run_id.clone(),
674 "2.0".into(),
675 std::time::Duration::from_secs(10),
676 );
677 }
678 #[cfg(feature = "otel")]
681 if rt.settings.observability.otel.logs == Some(true)
682 && let Some(ep) = rt
683 .settings
684 .observability
685 .otel
686 .endpoint
687 .clone()
688 .or_else(|| std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok())
689 {
690 crate::obs::otel::arm_logs(&ep, "agentd", crate::VERSION);
691 log.info("otel.logs.armed", json!({"endpoint": ep}));
692 }
693 if let Some(prompt) = rt.settings.agent.prompt.clone()
700 && !prompt.trim().is_empty()
701 && let Err(err) = rt.accept_event(
702 events::kinds::A2A_MESSAGE,
703 Some("operator".into()),
704 json!({"text": prompt, "context_id": crate::context::ROOT}),
705 )
706 {
707 log.warn("prompt.reject", json!({"err": err}));
708 }
709 #[cfg(any(feature = "internal-mocks", debug_assertions))]
712 if let Ok(path) = std::env::var("AGENTD_TEST_INBOX_FILE") {
713 match std::fs::read_to_string(&path).map_err(|e| e.to_string()).and_then(|t| serde_json::from_str::<Value>(&t).map_err(|e| e.to_string())) {
714 Ok(Value::Array(events)) => {
715 for e in events {
716 let kind = e["kind"].as_str().unwrap_or(events::kinds::A2A_MESSAGE).to_string();
717 let principal = e["principal"].as_str().map(str::to_string);
718 let payload = e.get("payload").cloned().unwrap_or(Value::Null);
719 if let Err(err) = rt.accept_event(&kind, principal, payload) {
720 log.warn("test.inbox.reject", json!({"err": err}));
721 }
722 }
723 let _ = std::fs::remove_file(&path);
724 }
725 other => log.warn("test.inbox.bad_file", json!({"path": path, "err": format!("{other:?}").chars().take(200).collect::<String>()})),
726 }
727 }
728 rt.checkpoint(true);
729 let code = rt.run_loop();
730 let _ = &rt.last_manifest_flush;
731 if rt.job_shape
733 && let Some(out) = rt.job_output()
734 {
735 match out {
736 Value::String(s) => println!("{s}"),
737 Value::Null => {}
738 other => println!(
739 "{}",
740 serde_json::to_string_pretty(&other).unwrap_or_default()
741 ),
742 }
743 }
744 code
745}
746
747pub fn capabilities(loaded: &Loaded) -> Value {
752 const START_KINDS: &[&str] = &[
753 "once",
754 "manual",
755 "loop",
756 "schedule",
757 "subscribe",
758 "signal",
759 "event",
760 "a2a",
761 ];
762 let s = &loaded.settings;
763 let workflows: Vec<Value> = s
764 .workflows
765 .iter()
766 .map(|w| {
767 let starts: Vec<String> = w["steps"]
768 .as_object()
769 .map(|steps| steps.values().filter_map(|st| st["kind"].as_str()).filter(|k| START_KINDS.contains(k)).map(str::to_string).collect())
770 .unwrap_or_default();
771 json!({"name": w["name"].as_str().unwrap_or(""), "description": w.get("description").and_then(Value::as_str), "start_kinds": starts, "inputs_schema": w.get("inputs").is_some()})
772 })
773 .collect();
774 let a2a = s.a2a.listen.as_ref().map(|listen| {
775 let principals: Vec<Value> = s
776 .a2a
777 .principals
778 .iter()
779 .map(|p| json!({"role": format!("{:?}", p.role).to_lowercase(), "match": principal_match_desc(&p.matcher), "grants": p.grants}))
780 .collect();
781 let mut methods = vec![
782 "SendMessage",
783 "SendStreamingMessage",
784 "GetTask",
785 "CancelTask",
786 "ListTasks",
787 "SubscribeToTask",
788 "GetAgentCard",
789 ];
790 let mut command_ops = vec![
791 "status",
792 "config",
793 "workflow.run",
794 "workflow.status",
795 "workflow.cancel",
796 "workflow.signal",
797 "subagent.send",
798 "subagent.kill",
799 "subagent.status",
800 "plan.get",
801 ];
802 if s.interface.enabled {
803 methods.push("SubscribeToEvents");
804 command_ops.push("interface.info");
805 if s.interface.debug {
806 command_ops.extend(["conversation.get", "run.get", "debug.events"]);
807 }
808 }
809 json!({
810 "listen": listen,
811 "tls": s.a2a.tls.cert.is_some(),
812 "mtls": s.a2a.tls.client_ca.is_some(),
813 "bearer": s.a2a.bearer.is_some(),
814 "methods": methods,
815 "admin": ["a2a.drain", "a2a.lameduck", "a2a.cancel", "a2a.pause", "a2a.resume"],
816 "command_ops": command_ops,
817 "principals": principals,
818 "loopback_operator": s.a2a.principals.is_empty(),
819 })
820 });
821 json!({
822 "runtime": "2.0",
823 "version": crate::VERSION,
824 "agent": {"name": s.instance_name(), "instruction": s.agent.instruction.is_some(), "preflight": format!("{:?}", s.agent.preflight).to_lowercase()},
825 "intelligence": {"model": s.intelligence.model, "endpoints": s.intelligence.endpoints.len()},
826 "mcp_servers": s.mcp.servers.iter().map(|m| m.name.clone()).collect::<Vec<_>>(),
827 "internal_tools": crate::registry::internal::names(),
828 "tools": {"overrides": s.tools.overrides.keys().cloned().collect::<Vec<_>>(), "disabled": s.tools.disabled},
829 "workflows": workflows,
830 "knowledge": {"server": s.knowledge.server},
831 "search": {"server": s.search.server},
832 "skills": {"sources": s.skills.sources.len()},
833 "a2a": a2a,
834 "interface": {"enabled": s.interface.enabled, "debug": s.interface.debug, "origins": s.interface.origins.len(), "pairing": s.interface.pairing.enabled, "display": {"top": s.interface.display.top, "bottom": s.interface.display.bottom}},
835 "store": format!("{:?}", s.store.kind).to_lowercase(),
836 "store_file": (s.store.kind == StoreKind::File).then(|| json!({
842 "path": crate::config::v2::file_store_root(&s.store).display().to_string(),
843 "defaulted": loaded.doc.pointer("/store/kind").is_none(),
844 })),
845 "lifecycle": {"run_until": format!("{:?}", s.lifecycle.run_until).to_lowercase(), "daemon": s.a2a.listen.is_some() || s.workflows.iter().any(|w| w["steps"].as_object().is_some_and(|st| st.values().any(|n| n["kind"].as_str().is_some_and(|k| matches!(k, "loop" | "schedule" | "subscribe" | "signal" | "event")))))},
846 })
847}
848
849fn principal_match_desc(m: &crate::config::v2::PrincipalMatch) -> Value {
851 if m.any {
852 json!({"any": true})
853 } else if let Some(s) = &m.san {
854 json!({"san": s})
855 } else if let Some(s) = &m.sub {
856 json!({"sub": s})
857 } else if m.bearer_ref.is_some() {
858 json!({"bearer_ref": "***"})
859 } else if let Some(a) = &m.aauth_agent {
860 json!({"aauth_agent": a})
861 } else {
862 json!({})
863 }
864}
865
866fn intel_bearer_provider(
872 settings: &crate::config::v2::Settings,
873) -> Option<std::sync::Arc<dyn Fn() -> Option<String> + Send + Sync>> {
874 #[cfg(feature = "oauth")]
875 {
876 let auth = settings.intelligence.auth.as_ref()?;
877 let spec = auth.to_spec();
878 if spec.kind == "aws" {
881 return None;
882 }
883 let signer = crate::auth::device::signer_for(
887 &spec,
888 "intelligence",
889 std::time::Duration::from_secs(30),
890 )
891 .ok()??;
892 Some(std::sync::Arc::new(move || {
893 signer
894 .sign("POST", "", "", &[])
895 .into_iter()
896 .find(|(k, _)| k.eq_ignore_ascii_case("authorization"))
897 .map(|(_, v)| v.strip_prefix("Bearer ").unwrap_or(&v).to_string())
898 }))
899 }
900 #[cfg(not(feature = "oauth"))]
901 {
902 let _ = settings;
903 None
904 }
905}
906
907impl Runtime {
908 pub(crate) fn current_intel_bearer(&self) -> Option<String> {
913 self.intel_bearer
914 .as_ref()
915 .and_then(|f| f())
916 .or_else(|| self.intel_token.clone())
917 }
918
919 pub(crate) fn intel_aws_auth(&self) -> Option<crate::config::AuthSpec> {
923 let a = self.settings.intelligence.auth.as_ref()?;
924 (a.kind == crate::config::v2::AuthKind::Aws).then(|| a.to_spec())
925 }
926
927 pub(crate) fn intel_dialect(&self) -> Option<String> {
931 self.settings.intelligence.dialect.clone()
932 }
933}
934
935fn resolve_intel_token(
937 settings: &crate::config::v2::Settings,
938 env: &dyn Fn(&str) -> Option<String>,
939) -> Result<Option<String>, String> {
940 if let Some(t) = &settings.intelligence.token {
941 let resolved = crate::sec::secret::resolve(&t.0, env)
942 .map_err(|e| format!("intelligence.token: {e}"))?;
943 return Ok(Some(resolved));
944 }
945 if let Some(p) = &settings.intelligence.token_file {
946 return crate::sec::secret::read_token_file(p)
947 .map(Some)
948 .map_err(|e| format!("intelligence.token_file: {e}"));
949 }
950 Ok(None)
952}
953
954impl Runtime {
955 pub(crate) fn subscribe_instruction(&mut self, uri: &str) -> Result<(), String> {
957 let (server, res) = match uri.strip_prefix("mcp://").and_then(|r| r.split_once('/')) {
958 Some((s, r)) => (Some(s.to_string()), r.to_string()),
959 None => (None, uri.to_string()),
960 };
961 let candidates: Vec<(String, Arc<McpClient>)> = match &server {
963 Some(s) => self
964 .mcp
965 .get(s)
966 .map(|c| vec![(s.clone(), c.clone())])
967 .unwrap_or_default(),
968 None => self
969 .mcp
970 .iter()
971 .map(|(n, c)| (n.clone(), c.clone()))
972 .collect(),
973 };
974 let mut last_err = String::from("no connected MCP server serves it");
975 for (name, c) in candidates {
976 match c.read_resource(&res) {
977 Ok(r) => {
978 let text = r.text();
979 if c.capabilities().supports_resources()
980 && let Err(e) = c.subscribe(&res)
981 {
982 self.log.warn(
983 "instruction.subscribe.fail",
984 json!({"server": name, "uri": res, "err": e.to_string()}),
985 );
986 }
987 let changed = self.instruction.text != text;
988 self.instruction = reactor::Instruction {
989 text,
990 source: "resource",
991 uri: Some(res.clone()),
992 server: Some(name.clone()),
993 version: self.instruction.version + u64::from(changed),
994 };
995 self.log.info("instruction.loaded", json!({"server": name, "uri": res, "bytes": self.instruction.text.len(), "version": self.instruction.version}));
996 return Ok(());
997 }
998 Err(e) => last_err = e.to_string(),
999 }
1000 }
1001 Err(last_err)
1002 }
1003
1004 pub(crate) fn poll_mcp_notifications(&mut self) {
1008 let mut updated_instruction = false;
1009 let mut tools_changed = Vec::new();
1010 let mut resource_updates: Vec<(String, String)> = Vec::new();
1011 for (name, c) in &self.mcp {
1012 for n in c.drain_notifications() {
1013 match n.method.as_str() {
1014 ::mcp::wire::method::NOTIFY_RESOURCES_UPDATED => {
1015 let uri = n
1016 .params
1017 .as_ref()
1018 .and_then(|p| p.get("uri"))
1019 .and_then(Value::as_str)
1020 .unwrap_or("");
1021 if self.instruction.uri.as_deref() == Some(uri)
1022 && self.instruction.server.as_deref() == Some(name.as_str())
1023 {
1024 updated_instruction = true;
1025 }
1026 resource_updates.push((name.clone(), uri.to_string()));
1027 }
1028 ::mcp::wire::method::NOTIFY_TOOLS_LIST_CHANGED => {
1029 tools_changed.push(name.clone())
1030 }
1031 _ => {}
1032 }
1033 }
1034 }
1035 if updated_instruction && let Some(uri) = self.instruction.uri.clone() {
1036 let full = match &self.instruction.server {
1037 Some(s) => format!("mcp://{s}/{uri}"),
1038 None => uri,
1039 };
1040 let before = self.instruction.version;
1041 if self.subscribe_instruction(&full).is_ok() && self.instruction.version != before {
1042 self.log.info(
1043 "instruction.updated",
1044 json!({"version": self.instruction.version}),
1045 );
1046 if self
1047 .settings
1048 .agent
1049 .wake_on()
1050 .contains(&crate::config::v2::WakeEvent::InstructionUpdated)
1051 {
1052 self.note_root("instruction.updated: the instruction resource changed; re-read it with instruction.read".into());
1053 }
1054 }
1055 }
1056 for (server, uri) in resource_updates {
1057 self.on_resource_updated(&server, &uri); self.on_subscribe_resource(&server, &uri); }
1060 for s in tools_changed {
1061 self.log.info("mcp.tools_changed", json!({"server": s, "note": "registry rebuild lands with the P5 reload choreography"}));
1062 }
1063 }
1064}