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 let mut replayed: Vec<(String, String)> = Vec::new();
439 for env in restored.of(Kind::Run) {
440 match serde_json::from_value::<crate::engine::RunState>(env.state.clone()) {
441 Ok(mut r) => {
442 r.dirty = false;
443 if !r.status.is_terminal() {
444 for (id, st) in r.steps.iter_mut() {
447 if st.status == StepStatus::Running {
448 log.info(
449 "restore.step.replay",
450 json!({"run": r.id, "step": id, "attempt": st.attempt}),
451 );
452 st.status = StepStatus::Pending;
453 st.worker = None;
454 replayed.push((r.id.clone(), id.clone()));
458 }
459 }
460 r.status = crate::engine::RunStatus::Running;
461 r.dirty = true;
462 }
463 rt.runs.insert(r.id.clone(), r);
464 }
465 Err(e) => log.warn(
466 "restore.run.corrupt",
467 json!({"id": env.id, "err": e.to_string()}),
468 ),
469 }
470 }
471 for env in restored.of(Kind::Subagent) {
472 match serde_json::from_value::<reactor::SubagentRecord>(env.state.clone()) {
473 Ok(s) => {
474 rt.subagents.insert(s.handle.clone(), s);
475 }
476 Err(e) => log.warn(
477 "restore.subagent.corrupt",
478 json!({"id": env.id, "err": e.to_string()}),
479 ),
480 }
481 }
482 #[cfg(feature = "a2a")]
483 rt.restore_a2a_tasks(restored.of(Kind::Task));
484 if let Some(m) = &restored.manifest {
485 rt.governor.restore(&m.budget, now_ms());
486 }
487 for ev in restored.inbox_pending() {
488 rt.inbox_queue.push_back(ev);
489 }
490 if restored.manifest.is_some() {
491 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()}));
492 rt.audit(audit::AuditEvent {
495 action: "restore",
496 target: json!({"runs": rt.runs.len(), "subagents": rt.subagents.len(), "inbox_pending": rt.inbox_queue.len(), "lost": restored.lost.len()}),
497 outcome: if restored.lost.is_empty() { "restored" } else { "restored_with_loss" },
498 principal: Some("system"),
499 role: Some("system"),
500 request_id: None,
501 });
502 }
503
504 if let Some(text) = rt.settings.agent.instruction.clone() {
506 if crate::config::v2::looks_like_resource_uri(&text) {
507 match rt.subscribe_instruction(&text) {
508 Ok(()) => {}
509 Err(e) => {
510 log.error("proc.exit", json!({"code": crate::exit::MCP_REQUIRED_DOWN, "err": format!("agent.instruction {text}: {e}")}));
511 return crate::exit::MCP_REQUIRED_DOWN;
512 }
513 }
514 } else {
515 rt.instruction.text = text;
516 }
517 }
518
519 if let Err(errs) = rt.load_workflows() {
520 for e in &errs {
521 log.error("config.invalid", json!({"error": e}));
522 }
523 log.error(
524 "proc.exit",
525 json!({"code": crate::exit::USAGE, "err": "workflow definitions"}),
526 );
527 return crate::exit::USAGE;
528 }
529 if !replayed.is_empty() {
535 let policies: Vec<(String, String, crate::engine::model::OnReplay)> = replayed
536 .iter()
537 .filter_map(|(rid, sid)| {
538 let wf_name = rt.runs.get(rid)?.workflow.clone();
539 let step = rt.workflows.get(&wf_name)?.steps.get(sid)?;
540 Some((rid.clone(), sid.clone(), step.on_replay))
541 })
542 .collect();
543 for (rid, sid, policy) in policies {
544 match policy {
545 crate::engine::model::OnReplay::Retry => {}
546 crate::engine::model::OnReplay::Skip => {
547 if let Some(r) = rt.runs.get_mut(&rid) {
548 r.end_step(&sid, StepStatus::Skipped, None, None);
549 }
550 rt.log.info(
551 "restore.step.skipped",
552 json!({"run": rid, "step": sid, "on_replay": "skip"}),
553 );
554 }
555 crate::engine::model::OnReplay::Fail => {
556 if let Some(r) = rt.runs.get_mut(&rid) {
557 r.end_step(
558 &sid,
559 StepStatus::Failed,
560 None,
561 Some(
562 "step was in flight when the process died and its \
563 on_replay policy is `fail`"
564 .into(),
565 ),
566 );
567 }
568 rt.log.warn(
569 "restore.step.failed",
570 json!({"run": rid, "step": sid, "on_replay": "fail"}),
571 );
572 }
573 }
574 }
575 }
576 rt.job_shape = match rt.settings.lifecycle.run_until {
579 crate::config::v2::RunUntil::Drained => false,
580 crate::config::v2::RunUntil::Idle => true,
581 crate::config::v2::RunUntil::Auto => {
582 rt.settings.a2a.listen.is_none() && !rt.workflows.values().any(|w| w.is_long_lived())
583 }
584 };
585 for r in rt.runs.values() {
587 if rt.job_shape
588 && rt
589 .workflows
590 .get(&r.workflow)
591 .and_then(|w| w.step(&r.start.node))
592 .is_some_and(|s| s.kind == "once")
593 {
594 rt.job_runs.push(r.id.clone());
595 }
596 }
597 let refs = rt.skills.references(&rt.instruction.text.clone());
599 if !refs.is_empty() {
600 let unknown = rt.preload_skills(crate::context::ROOT, &refs, None);
601 for u in unknown {
602 rt.note_root(format!(
603 "skill.unknown: {u:?} referenced by the instruction is not in the catalogue"
604 ));
605 }
606 }
607 #[cfg(all(unix, feature = "config-watch"))]
609 if rt.settings.lifecycle.watch_config {
610 for (path, _) in &loaded.files {
611 crate::config::watch::spawn_config_watcher(std::path::Path::new(path), &log);
612 }
613 }
614 rt.arm_workflows();
615 rt.arm_long_lived_starts();
616 rt.arm_goal();
617 rt.respawn_restored_subagents();
618 #[cfg(feature = "a2a")]
622 if rt.settings.a2a.listen.is_some() {
623 let resolver = match crate::a2a::Resolver::build(&rt.settings.a2a, &envmap) {
624 Ok(r) => r,
625 Err(e) => {
626 log.error(
627 "proc.exit",
628 json!({"code": crate::exit::USAGE, "err": format!("a2a principals: {e}")}),
629 );
630 return crate::exit::USAGE;
631 }
632 };
633 let write_timeout = rt.settings.lifecycle.drain_timeout();
634 match a2a_server::spawn_a2a_listener(
635 &rt.settings.a2a,
636 &rt.settings.interface,
637 rt.events_tx.clone(),
638 resolver,
639 &envmap,
640 write_timeout,
641 log.clone(),
642 ) {
643 Ok(serving) => {
644 rt.a2a_feed = serving.feed;
645 rt.a2a_pairing = serving.pairing;
646 rt.a2a_sink = Some(std::sync::Arc::clone(&serving.listener.sink));
647 rt.a2a_listener = Some(serving.listener);
650 if rt.settings.interface.enabled && rt.settings.interface.debug {
654 let cap = rt
655 .settings
656 .observability
657 .events_ring
658 .map(|n| n as usize)
659 .unwrap_or(crate::obs::log::EVENTS_RING_DEFAULT);
660 crate::obs::log::install_event_ring(cap);
661 log.info("interface.debug", json!({"events_ring": cap}));
662 }
663 for id in rt.tasks.keys().cloned().collect::<Vec<_>>() {
665 rt.task_sync(&id);
666 }
667 }
668 Err(e) => {
669 log.error(
670 "proc.exit",
671 json!({"code": crate::exit::USAGE, "err": format!("a2a listen: {e}")}),
672 );
673 return crate::exit::USAGE;
674 }
675 }
676 }
677 #[cfg(feature = "a2a")]
681 if rt.settings.webhooks.listen.is_some() {
682 let nodes: Vec<(String, String, serde_json::Map<String, serde_json::Value>)> = rt
683 .workflows
684 .values()
685 .flat_map(|wf| {
686 wf.steps
687 .values()
688 .filter(|s| s.kind == "webhook")
689 .map(|s| (wf.name.clone(), s.id.clone(), s.spec.clone()))
690 .collect::<Vec<_>>()
691 })
692 .collect();
693 let write_timeout = rt.settings.lifecycle.drain_timeout();
694 if let Err(e) = webhooks::spawn_webhook_listener(
695 &rt.settings.webhooks,
696 nodes,
697 rt.webhook_callbacks.clone(),
698 rt.events_tx.clone(),
699 &envmap,
700 write_timeout,
701 log.clone(),
702 ) {
703 log.error(
704 "proc.exit",
705 json!({"code": crate::exit::USAGE, "err": format!("webhooks listen: {e}")}),
706 );
707 return crate::exit::USAGE;
708 }
709 }
710 #[cfg(feature = "metrics")]
713 if let Some(addr) = rt.settings.observability.metrics_addr.clone()
714 && let Err(e) = crate::obs::serve::spawn(&addr, log.clone())
715 {
716 log.warn(
717 "metrics.serve.fail",
718 json!({"addr": addr, "err": e.to_string()}),
719 );
720 }
721 if let Some(path) = rt.settings.observability.health_file.clone() {
722 crate::obs::health::spawn_writer(
723 std::path::PathBuf::from(path),
724 run_id.clone(),
725 "2.0".into(),
726 std::time::Duration::from_secs(10),
727 );
728 }
729 #[cfg(feature = "otel")]
732 if rt.settings.observability.otel.logs == Some(true)
733 && let Some(ep) = rt
734 .settings
735 .observability
736 .otel
737 .endpoint
738 .clone()
739 .or_else(|| std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok())
740 {
741 crate::obs::otel::arm_logs(&ep, "agentd", crate::VERSION);
742 log.info("otel.logs.armed", json!({"endpoint": ep}));
743 }
744 if let Some(prompt) = rt.settings.agent.prompt.clone()
751 && !prompt.trim().is_empty()
752 && let Err(err) = rt.accept_event(
753 events::kinds::A2A_MESSAGE,
754 Some("operator".into()),
755 json!({"text": prompt, "context_id": crate::context::ROOT}),
756 )
757 {
758 log.warn("prompt.reject", json!({"err": err}));
759 }
760 #[cfg(any(feature = "internal-mocks", debug_assertions))]
763 if let Ok(path) = std::env::var("AGENTD_TEST_INBOX_FILE") {
764 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())) {
765 Ok(Value::Array(events)) => {
766 for e in events {
767 let kind = e["kind"].as_str().unwrap_or(events::kinds::A2A_MESSAGE).to_string();
768 let principal = e["principal"].as_str().map(str::to_string);
769 let payload = e.get("payload").cloned().unwrap_or(Value::Null);
770 if let Err(err) = rt.accept_event(&kind, principal, payload) {
771 log.warn("test.inbox.reject", json!({"err": err}));
772 }
773 }
774 let _ = std::fs::remove_file(&path);
775 }
776 other => log.warn("test.inbox.bad_file", json!({"path": path, "err": format!("{other:?}").chars().take(200).collect::<String>()})),
777 }
778 }
779 rt.checkpoint(true);
780 let code = rt.run_loop();
781 let _ = &rt.last_manifest_flush;
782 if rt.job_shape
784 && let Some(out) = rt.job_output()
785 {
786 match out {
787 Value::String(s) => println!("{s}"),
788 Value::Null => {}
789 other => println!(
790 "{}",
791 serde_json::to_string_pretty(&other).unwrap_or_default()
792 ),
793 }
794 }
795 code
796}
797
798pub fn capabilities(loaded: &Loaded) -> Value {
803 const START_KINDS: &[&str] = &[
804 "once",
805 "manual",
806 "loop",
807 "schedule",
808 "subscribe",
809 "signal",
810 "event",
811 "a2a",
812 ];
813 let s = &loaded.settings;
814 let workflows: Vec<Value> = s
815 .workflows
816 .iter()
817 .map(|w| {
818 let starts: Vec<String> = w["steps"]
819 .as_object()
820 .map(|steps| steps.values().filter_map(|st| st["kind"].as_str()).filter(|k| START_KINDS.contains(k)).map(str::to_string).collect())
821 .unwrap_or_default();
822 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()})
823 })
824 .collect();
825 let a2a = s.a2a.listen.as_ref().map(|listen| {
826 let principals: Vec<Value> = s
827 .a2a
828 .principals
829 .iter()
830 .map(|p| json!({"role": format!("{:?}", p.role).to_lowercase(), "match": principal_match_desc(&p.matcher), "grants": p.grants}))
831 .collect();
832 let mut methods = vec![
833 "SendMessage",
834 "SendStreamingMessage",
835 "GetTask",
836 "CancelTask",
837 "ListTasks",
838 "SubscribeToTask",
839 "GetAgentCard",
840 ];
841 let mut command_ops = vec![
842 "status",
843 "config",
844 "workflow.run",
845 "workflow.status",
846 "workflow.cancel",
847 "workflow.signal",
848 "subagent.send",
849 "subagent.kill",
850 "subagent.status",
851 "plan.get",
852 ];
853 if s.interface.enabled {
854 methods.push("SubscribeToEvents");
855 command_ops.push("interface.info");
856 if s.interface.debug {
857 command_ops.extend(["conversation.get", "run.get", "debug.events"]);
858 }
859 }
860 json!({
861 "listen": listen,
862 "tls": s.a2a.tls.cert.is_some(),
863 "mtls": s.a2a.tls.client_ca.is_some(),
864 "bearer": s.a2a.bearer.is_some(),
865 "methods": methods,
866 "admin": ["a2a.drain", "a2a.lameduck", "a2a.cancel", "a2a.pause", "a2a.resume"],
867 "command_ops": command_ops,
868 "principals": principals,
869 "loopback_operator": s.a2a.principals.is_empty(),
870 })
871 });
872 json!({
873 "runtime": "2.0",
874 "version": crate::VERSION,
875 "agent": {"name": s.instance_name(), "instruction": s.agent.instruction.is_some(), "preflight": format!("{:?}", s.agent.preflight).to_lowercase()},
876 "intelligence": {"model": s.intelligence.model, "endpoints": s.intelligence.endpoints.len()},
877 "mcp_servers": s.mcp.servers.iter().map(|m| m.name.clone()).collect::<Vec<_>>(),
878 "internal_tools": crate::registry::internal::names(),
879 "tools": {"overrides": s.tools.overrides.keys().cloned().collect::<Vec<_>>(), "disabled": s.tools.disabled},
880 "workflows": workflows,
881 "knowledge": {"server": s.knowledge.server},
882 "search": {"server": s.search.server},
883 "skills": {"sources": s.skills.sources.len()},
884 "a2a": a2a,
885 "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}},
886 "store": format!("{:?}", s.store.kind).to_lowercase(),
887 "store_file": (s.store.kind == StoreKind::File).then(|| json!({
893 "path": crate::config::v2::file_store_root(&s.store).display().to_string(),
894 "defaulted": loaded.doc.pointer("/store/kind").is_none(),
895 })),
896 "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")))))},
897 })
898}
899
900fn principal_match_desc(m: &crate::config::v2::PrincipalMatch) -> Value {
902 if m.any {
903 json!({"any": true})
904 } else if let Some(s) = &m.san {
905 json!({"san": s})
906 } else if let Some(s) = &m.sub {
907 json!({"sub": s})
908 } else if m.bearer_ref.is_some() {
909 json!({"bearer_ref": "***"})
910 } else if let Some(a) = &m.aauth_agent {
911 json!({"aauth_agent": a})
912 } else {
913 json!({})
914 }
915}
916
917fn intel_bearer_provider(
923 settings: &crate::config::v2::Settings,
924) -> Option<std::sync::Arc<dyn Fn() -> Option<String> + Send + Sync>> {
925 #[cfg(feature = "oauth")]
926 {
927 let auth = settings.intelligence.auth.as_ref()?;
928 let spec = auth.to_spec();
929 if spec.kind == "aws" {
932 return None;
933 }
934 let signer = crate::auth::device::signer_for(
938 &spec,
939 "intelligence",
940 std::time::Duration::from_secs(30),
941 )
942 .ok()??;
943 Some(std::sync::Arc::new(move || {
944 signer
945 .sign("POST", "", "", &[])
946 .into_iter()
947 .find(|(k, _)| k.eq_ignore_ascii_case("authorization"))
948 .map(|(_, v)| v.strip_prefix("Bearer ").unwrap_or(&v).to_string())
949 }))
950 }
951 #[cfg(not(feature = "oauth"))]
952 {
953 let _ = settings;
954 None
955 }
956}
957
958impl Runtime {
959 pub(crate) fn current_intel_bearer(&self) -> Option<String> {
964 self.intel_bearer
965 .as_ref()
966 .and_then(|f| f())
967 .or_else(|| self.intel_token.clone())
968 }
969
970 pub(crate) fn intel_aws_auth(&self) -> Option<crate::config::AuthSpec> {
974 let a = self.settings.intelligence.auth.as_ref()?;
975 (a.kind == crate::config::v2::AuthKind::Aws).then(|| a.to_spec())
976 }
977
978 pub(crate) fn intel_dialect(&self) -> Option<String> {
982 self.settings.intelligence.dialect.clone()
983 }
984}
985
986fn resolve_intel_token(
988 settings: &crate::config::v2::Settings,
989 env: &dyn Fn(&str) -> Option<String>,
990) -> Result<Option<String>, String> {
991 if let Some(t) = &settings.intelligence.token {
992 let resolved = crate::sec::secret::resolve(&t.0, env)
993 .map_err(|e| format!("intelligence.token: {e}"))?;
994 return Ok(Some(resolved));
995 }
996 if let Some(p) = &settings.intelligence.token_file {
997 return crate::sec::secret::read_token_file(p)
998 .map(Some)
999 .map_err(|e| format!("intelligence.token_file: {e}"));
1000 }
1001 Ok(None)
1003}
1004
1005impl Runtime {
1006 pub(crate) fn subscribe_instruction(&mut self, uri: &str) -> Result<(), String> {
1008 let (server, res) = match uri.strip_prefix("mcp://").and_then(|r| r.split_once('/')) {
1009 Some((s, r)) => (Some(s.to_string()), r.to_string()),
1010 None => (None, uri.to_string()),
1011 };
1012 let candidates: Vec<(String, Arc<McpClient>)> = match &server {
1014 Some(s) => self
1015 .mcp
1016 .get(s)
1017 .map(|c| vec![(s.clone(), c.clone())])
1018 .unwrap_or_default(),
1019 None => self
1020 .mcp
1021 .iter()
1022 .map(|(n, c)| (n.clone(), c.clone()))
1023 .collect(),
1024 };
1025 let mut last_err = String::from("no connected MCP server serves it");
1026 for (name, c) in candidates {
1027 match c.read_resource(&res) {
1028 Ok(r) => {
1029 let text = r.text();
1030 if c.capabilities().supports_resources()
1031 && let Err(e) = c.subscribe(&res)
1032 {
1033 self.log.warn(
1034 "instruction.subscribe.fail",
1035 json!({"server": name, "uri": res, "err": e.to_string()}),
1036 );
1037 }
1038 let changed = self.instruction.text != text;
1039 self.instruction = reactor::Instruction {
1040 text,
1041 source: "resource",
1042 uri: Some(res.clone()),
1043 server: Some(name.clone()),
1044 version: self.instruction.version + u64::from(changed),
1045 };
1046 self.log.info("instruction.loaded", json!({"server": name, "uri": res, "bytes": self.instruction.text.len(), "version": self.instruction.version}));
1047 return Ok(());
1048 }
1049 Err(e) => last_err = e.to_string(),
1050 }
1051 }
1052 Err(last_err)
1053 }
1054
1055 pub(crate) fn poll_mcp_notifications(&mut self) {
1059 let mut updated_instruction = false;
1060 let mut tools_changed = Vec::new();
1061 let mut resource_updates: Vec<(String, String)> = Vec::new();
1062 for (name, c) in &self.mcp {
1063 for n in c.drain_notifications() {
1064 match n.method.as_str() {
1065 ::mcp::wire::method::NOTIFY_RESOURCES_UPDATED => {
1066 let uri = n
1067 .params
1068 .as_ref()
1069 .and_then(|p| p.get("uri"))
1070 .and_then(Value::as_str)
1071 .unwrap_or("");
1072 if self.instruction.uri.as_deref() == Some(uri)
1073 && self.instruction.server.as_deref() == Some(name.as_str())
1074 {
1075 updated_instruction = true;
1076 }
1077 resource_updates.push((name.clone(), uri.to_string()));
1078 }
1079 ::mcp::wire::method::NOTIFY_TOOLS_LIST_CHANGED => {
1080 tools_changed.push(name.clone())
1081 }
1082 _ => {}
1083 }
1084 }
1085 }
1086 if updated_instruction && let Some(uri) = self.instruction.uri.clone() {
1087 let full = match &self.instruction.server {
1088 Some(s) => format!("mcp://{s}/{uri}"),
1089 None => uri,
1090 };
1091 let before = self.instruction.version;
1092 if self.subscribe_instruction(&full).is_ok() && self.instruction.version != before {
1093 self.log.info(
1094 "instruction.updated",
1095 json!({"version": self.instruction.version}),
1096 );
1097 if self
1098 .settings
1099 .agent
1100 .wake_on()
1101 .contains(&crate::config::v2::WakeEvent::InstructionUpdated)
1102 {
1103 self.note_root("instruction.updated: the instruction resource changed; re-read it with instruction.read".into());
1104 }
1105 }
1106 }
1107 for (server, uri) in resource_updates {
1108 self.on_resource_updated(&server, &uri); self.on_subscribe_resource(&server, &uri); }
1111 for s in tools_changed {
1112 self.log.info("mcp.tools_changed", json!({"server": s, "note": "registry rebuild lands with the P5 reload choreography"}));
1113 }
1114 }
1115}