1use crate::value::VmDictExt;
2use std::cell::RefCell;
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use serde_json::Value as JsonValue;
7
8use crate::stdlib::macros::{harn_builtin, VmBuiltinDef};
9use crate::value::{values_equal, VmError, VmValue};
10use crate::vm::{AsyncBuiltinCtx, Vm};
11
12mod bridge;
13pub(crate) mod fixtured_operations;
14mod operation_registry;
15pub mod process_admission;
16pub(crate) mod process_dispatch;
17mod process_exec;
18pub mod turn_cache;
21
22use bridge::HOST_CALL_BRIDGE;
23pub use bridge::{
24 clear_host_call_bridge, dispatch_host_call_bridge, host_call_ready, install_host_call_bridge,
25 set_host_call_bridge, HostCallBridge, HostCallBridgeGuard, HostCallDispatchFuture,
26};
27
28use process_dispatch::dispatch_process_exec_with_policy;
29pub(crate) use process_dispatch::{dispatch_process_exec, dispatch_reviewed_git_push_with_lease};
30pub(crate) use process_exec::build_sandboxed_command;
31use process_exec::dispatch_process_spawn_with_policy;
32
33pub(crate) fn audited_utc_now_rfc3339(capability_id: &'static str) -> String {
37 let dt: chrono::DateTime<chrono::Utc> =
38 crate::clock_mock::leak_audit::wall_now(capability_id).into();
39 dt.to_rfc3339()
40}
41
42pub(crate) const MODULE_BUILTINS: &[&VmBuiltinDef] = &[
43 &HOST_MOCK_BUILTIN_DEF,
44 &HOST_MOCK_CLEAR_BUILTIN_DEF,
45 &HOST_MOCK_CALLS_BUILTIN_DEF,
46 &HOST_MOCK_PUSH_SCOPE_BUILTIN_DEF,
47 &HOST_MOCK_POP_SCOPE_BUILTIN_DEF,
48 &HOST_CAPABILITIES_BUILTIN_DEF,
49 &HOST_HAS_BUILTIN_DEF,
50 &HOST_CALL_BUILTIN_DEF,
51 &HOST_TOOL_LIST_BUILTIN_DEF,
52 &HOST_TOOL_CALL_BUILTIN_DEF,
53];
54
55#[derive(Clone)]
56struct HostMock {
57 capability: String,
58 operation: String,
59 params: Option<crate::value::DictMap>,
60 result: Option<VmValue>,
61 error: Option<String>,
62 unregistered_ok: bool,
63}
64
65#[derive(Clone)]
66struct HostMockCall {
67 capability: String,
68 operation: String,
69 params: crate::value::DictMap,
70}
71
72thread_local! {
73 static HOST_MOCKS: RefCell<Vec<HostMock>> = const { RefCell::new(Vec::new()) };
74 static HOST_MOCK_CALLS: RefCell<Vec<HostMockCall>> = const { RefCell::new(Vec::new()) };
75 static HOST_MOCK_SCOPES: RefCell<Vec<(Vec<HostMock>, Vec<HostMockCall>)>> =
76 const { RefCell::new(Vec::new()) };
77}
78
79pub(crate) fn reset_host_state() {
80 HOST_MOCKS.with(|mocks| mocks.borrow_mut().clear());
81 HOST_MOCK_CALLS.with(|calls| calls.borrow_mut().clear());
82 HOST_MOCK_SCOPES.with(|scopes| scopes.borrow_mut().clear());
83 turn_cache::reset_local();
87}
88
89pub(crate) fn reset_scoped_host_state() {
90 operation_registry::clear_scoped_mockable();
91}
92
93fn push_host_mock_scope() {
98 let mocks = HOST_MOCKS.with(|v| std::mem::take(&mut *v.borrow_mut()));
99 let calls = HOST_MOCK_CALLS.with(|v| std::mem::take(&mut *v.borrow_mut()));
100 HOST_MOCK_SCOPES.with(|v| v.borrow_mut().push((mocks, calls)));
101}
102
103fn pop_host_mock_scope() -> bool {
108 let entry = HOST_MOCK_SCOPES.with(|v| v.borrow_mut().pop());
109 match entry {
110 Some((mocks, calls)) => {
111 HOST_MOCKS.with(|v| *v.borrow_mut() = mocks);
112 HOST_MOCK_CALLS.with(|v| *v.borrow_mut() = calls);
113 true
114 }
115 None => false,
116 }
117}
118
119fn capability_manifest_map() -> crate::value::DictMap {
120 let mut root = crate::value::DictMap::new();
121 root.insert(
122 crate::value::intern_key("process"),
123 capability(
124 "Process execution.",
125 &[
126 op("exec", "Execute a process in argv or shell mode."),
127 op(
128 "spawn",
129 "Spawn a process non-blocking; returns a handle immediately for poll/wait/kill.",
130 ),
131 op(
132 "poll",
133 "Non-blocking snapshot of a spawned process: status, captured stdout/stderr.",
134 ),
135 op(
136 "wait",
137 "Await a spawned process to completion (optional timeout_ms); returns final result.",
138 ),
139 op(
140 "kill",
141 "Terminate a spawned process by handle and await the status transition.",
142 ),
143 op(
144 "release",
145 "Release a spawned-process handle and free its retained output.",
146 ),
147 op("list_shells", "List shells discovered by the host/session."),
148 op(
149 "get_default_shell",
150 "Return the selected default shell for this host/session.",
151 ),
152 op(
153 "set_default_shell",
154 "Select the default shell for this host/session.",
155 ),
156 op(
157 "shell_invocation",
158 "Resolve shell selection and login/interactive flags into argv.",
159 ),
160 ],
161 ),
162 );
163 root.insert(
164 crate::value::intern_key("template"),
165 capability(
166 "Template rendering.",
167 &[op("render", "Render a template file.")],
168 ),
169 );
170 root.insert(
171 crate::value::intern_key("interaction"),
172 capability(
173 "User interaction.",
174 &[op("ask", "Ask the user a question.")],
175 ),
176 );
177 root.insert(
178 crate::value::intern_key("memory"),
179 capability(
180 "Vector-aware memory: host-provided embeddings.",
181 &[op(
182 "embed",
183 "Embed text for semantic recall. Params: {text, model_hint?}. \
184 Returns {vector: list<float>, model: string, dim: int}.",
185 )],
186 ),
187 );
188 root.insert(
189 crate::value::intern_key("project"),
190 capability(
191 "Project metadata and durable project facts.",
192 &[
193 op("metadata_get", "Read project metadata."),
194 op("metadata_inspect", "Inspect project metadata provenance."),
195 op("metadata_set", "Write project metadata."),
196 op("metadata_save", "Persist pending project metadata changes."),
197 op("metadata_stale", "Check whether project metadata is stale."),
198 op(
199 "metadata_refresh_hashes",
200 "Refresh project metadata content hashes.",
201 ),
202 ],
203 ),
204 );
205 root.insert(
206 crate::value::intern_key("runtime"),
207 capability(
208 "Runtime task context and run metadata supplied by the active host.",
209 &[
210 op("task", "Read the current runtime task."),
211 op("pipeline_input", "Read the active pipeline input payload."),
212 op("prompt_content", "Read the active session prompt content."),
213 op("dry_run", "Read whether the runtime is in dry-run mode."),
214 op("approved_plan", "Read the approved plan text."),
215 op("record_run", "Record run metadata with the host."),
216 op("set_result", "Write the runtime result payload."),
217 ],
218 ),
219 );
220 root.insert(
221 crate::value::intern_key("workspace"),
222 capability(
223 "Workspace facts and file access supplied by the active host.",
224 &[
225 op("project_root", "Return the active project root."),
226 op("cwd", "Return the active current working directory."),
227 op("read_text", "Read a workspace text file."),
228 op("list", "List workspace files or directories."),
229 op("exists", "Check whether a workspace path exists."),
230 ],
231 ),
232 );
233 root.insert(
234 crate::value::intern_key("oauth_storage"),
235 capability(
236 "Host-managed OAuth token storage.",
237 &[
238 op("cloud_get", "Read a cloud-managed token set."),
239 op("cloud_set", "Write a cloud-managed token set."),
240 op("cloud_delete", "Delete a cloud-managed token set."),
241 op(
242 "cloud_acquire_refresh_lock",
243 "Acquire an OAuth refresh lock.",
244 ),
245 op(
246 "cloud_release_refresh_lock",
247 "Release an OAuth refresh lock.",
248 ),
249 ],
250 ),
251 );
252 root.insert(
253 crate::value::intern_key("mcp"),
254 capability(
255 "MCP host interactions.",
256 &[op("elicit", "Ask the connected MCP client for input.")],
257 ),
258 );
259 root.insert(
260 crate::value::intern_key("hitl"),
261 capability(
262 "Human-in-the-loop host interactions.",
263 &[
264 op(
265 "question",
266 "Ask a human a question through the active host.",
267 ),
268 op(
269 "approval",
270 "Request a human approval through the active host.",
271 ),
272 op(
273 "dual_control",
274 "Request quorum approval from multiple human reviewers.",
275 ),
276 op(
277 "escalation",
278 "Escalate a task to a human role through the active host.",
279 ),
280 ],
281 ),
282 );
283 root
284}
285
286fn mocked_operation_entry() -> VmValue {
287 op(
288 "mocked",
289 "Mocked host operation registered at runtime for tests.",
290 )
291 .1
292}
293
294fn ensure_mocked_capability(
295 root: &mut crate::value::DictMap,
296 capability_name: &str,
297 operation_name: &str,
298) {
299 let Some(existing) = root.get(capability_name).cloned() else {
300 root.insert(
301 crate::value::intern_key(capability_name),
302 capability(
303 "Mocked host capability registered at runtime for tests.",
304 &[(operation_name.to_string(), mocked_operation_entry())],
305 ),
306 );
307 return;
308 };
309
310 let Some(existing_dict) = existing.as_dict() else {
311 return;
312 };
313 let mut entry = (*existing_dict).clone();
314 let mut ops = entry
315 .get("ops")
316 .and_then(|value| match value {
317 VmValue::List(list) => Some((**list).clone()),
318 _ => None,
319 })
320 .unwrap_or_default();
321 if !ops.iter().any(|value| value.display() == operation_name) {
322 ops.push(VmValue::String(arcstr::ArcStr::from(
323 operation_name.to_string(),
324 )));
325 }
326
327 let mut operations = entry
328 .get("operations")
329 .and_then(|value| value.as_dict())
330 .map(|dict| (*dict).clone())
331 .unwrap_or_default();
332 operations
333 .entry(crate::value::intern_key(operation_name))
334 .or_insert_with(mocked_operation_entry);
335
336 entry.insert(
337 crate::value::intern_key("ops"),
338 VmValue::List(std::sync::Arc::new(ops)),
339 );
340 entry.insert(
341 crate::value::intern_key("operations"),
342 VmValue::dict(operations),
343 );
344 root.insert(
345 crate::value::intern_key(capability_name),
346 VmValue::dict(entry),
347 );
348}
349
350fn ensure_registered_operation(
351 root: &mut crate::value::DictMap,
352 capability_name: &str,
353 operation_name: &str,
354 description: &str,
355) {
356 let operation = op(operation_name, description);
357 let Some(existing) = root.get(capability_name).cloned() else {
358 root.insert(
359 crate::value::intern_key(capability_name),
360 capability(description, &[operation]),
361 );
362 return;
363 };
364
365 let Some(existing_dict) = existing.as_dict() else {
366 return;
367 };
368 let mut entry = (*existing_dict).clone();
369 let mut ops = entry
370 .get("ops")
371 .and_then(|value| match value {
372 VmValue::List(list) => Some((**list).clone()),
373 _ => None,
374 })
375 .unwrap_or_default();
376 if !ops.iter().any(|value| value.display() == operation_name) {
377 ops.push(VmValue::String(arcstr::ArcStr::from(
378 operation_name.to_string(),
379 )));
380 }
381
382 let mut operations = entry
383 .get("operations")
384 .and_then(|value| value.as_dict())
385 .map(|dict| (*dict).clone())
386 .unwrap_or_default();
387 operations
388 .entry(crate::value::intern_key(operation_name))
389 .or_insert(operation.1);
390
391 entry.insert(
392 crate::value::intern_key("ops"),
393 VmValue::List(std::sync::Arc::new(ops)),
394 );
395 entry.insert(
396 crate::value::intern_key("operations"),
397 VmValue::dict(operations),
398 );
399 root.insert(
400 crate::value::intern_key(capability_name),
401 VmValue::dict(entry),
402 );
403}
404
405pub fn register_mockable_host_operation(
406 capability_name: impl AsRef<str>,
407 operation_name: impl AsRef<str>,
408 description: impl AsRef<str>,
409) {
410 operation_registry::register_mockable(capability_name, operation_name, description);
411}
412
413pub fn register_scoped_mockable_host_operation(
415 capability_name: impl AsRef<str>,
416 operation_name: impl AsRef<str>,
417 description: impl AsRef<str>,
418) {
419 operation_registry::register_scoped_mockable(capability_name, operation_name, description);
420}
421
422pub fn register_callable_host_operation(
423 capability_name: impl AsRef<str>,
424 operation_name: impl AsRef<str>,
425 description: impl AsRef<str>,
426) {
427 operation_registry::register_callable(capability_name, operation_name, description);
428}
429
430fn apply_registered_operations(root: &mut crate::value::DictMap) {
431 operation_registry::apply_callable(root);
432}
433
434fn capability_manifest_with_mocks() -> VmValue {
435 let mut root = capability_manifest_map();
436 apply_registered_operations(&mut root);
437 HOST_MOCKS.with(|mocks| {
438 for host_mock in mocks.borrow().iter() {
439 ensure_mocked_capability(&mut root, &host_mock.capability, &host_mock.operation);
440 }
441 });
442 fixtured_operations::apply_to_manifest(&mut root, ensure_mocked_capability);
443 VmValue::dict(root)
444}
445
446fn known_host_operations() -> Vec<(String, String)> {
447 let mut root = capability_manifest_map();
448 apply_registered_operations(&mut root);
449 operation_registry::apply_mockable(&mut root);
450 root.into_iter()
451 .flat_map(|(capability_name, capability)| {
452 let capability_name = capability_name.to_string();
453 capability
454 .as_dict()
455 .and_then(|dict| dict.get("ops"))
456 .and_then(|value| match value {
457 VmValue::List(list) => Some((**list).clone()),
458 _ => None,
459 })
460 .unwrap_or_default()
461 .into_iter()
462 .map(move |operation| (capability_name.clone(), operation.display()))
463 })
464 .collect()
465}
466
467pub(crate) fn host_operation_is_registered(capability: &str, operation: &str) -> bool {
468 known_host_operations()
469 .iter()
470 .any(|(known_capability, known_operation)| {
471 known_capability == capability && known_operation == operation
472 })
473}
474
475fn closest_host_operation(capability: &str, operation: &str) -> Option<(String, String)> {
476 let requested = format!("{capability}.{operation}");
477 known_host_operations()
478 .into_iter()
479 .map(|(candidate_capability, candidate_operation)| {
480 let candidate = format!("{candidate_capability}.{candidate_operation}");
481 let distance = strsim::levenshtein(&requested, &candidate);
482 (distance, candidate_capability, candidate_operation)
483 })
484 .filter(|(distance, _, _)| *distance <= 4)
485 .min_by_key(|(distance, _, _)| *distance)
486 .map(|(_, candidate_capability, candidate_operation)| {
487 (candidate_capability, candidate_operation)
488 })
489}
490
491fn validate_host_mock_registration(host_mock: &HostMock) -> Result<(), VmError> {
492 if host_mock.unregistered_ok
493 || host_operation_is_registered(&host_mock.capability, &host_mock.operation)
494 {
495 return Ok(());
496 }
497
498 let mut message = format!(
499 "host_mock: unregistered host operation {}.{}; register the capability/operation on \
500 the host or pass {{unregistered_ok: true}} for a test-local mock",
501 host_mock.capability, host_mock.operation
502 );
503 if let Some((capability, operation)) =
504 closest_host_operation(&host_mock.capability, &host_mock.operation)
505 {
506 message.push_str(&format!(". Did you mean {capability}.{operation}?"));
507 }
508 Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
509 message,
510 ))))
511}
512
513fn op(name: &str, description: &str) -> (String, VmValue) {
514 let mut entry = crate::value::DictMap::new();
515 entry.put_str("description", description);
516 (name.to_string(), VmValue::dict(entry))
517}
518
519fn capability(description: &str, ops: &[(String, VmValue)]) -> VmValue {
520 let mut entry = crate::value::DictMap::new();
521 entry.put_str("description", description);
522 entry.insert(
523 crate::value::intern_key("ops"),
524 VmValue::List(std::sync::Arc::new(
525 ops.iter()
526 .map(|(name, _)| VmValue::String(arcstr::ArcStr::from(name.as_str())))
527 .collect(),
528 )),
529 );
530 let mut op_dict = crate::value::DictMap::new();
531 for (name, op) in ops {
532 op_dict.insert(crate::value::intern_key(name), op.clone());
533 }
534 entry.insert(
535 crate::value::intern_key("operations"),
536 VmValue::dict(op_dict),
537 );
538 VmValue::dict(entry)
539}
540
541pub(crate) fn require_param(params: &crate::value::DictMap, key: &str) -> Result<String, VmError> {
542 params
543 .get(key)
544 .map(|v| v.display())
545 .filter(|v| !v.is_empty())
546 .ok_or_else(|| {
547 VmError::Thrown(VmValue::String(arcstr::ArcStr::from(format!(
548 "host_call: missing required parameter '{key}'"
549 ))))
550 })
551}
552
553fn render_template(
554 path: &str,
555 bindings: Option<&crate::value::DictMap>,
556) -> Result<String, VmError> {
557 let asset = crate::stdlib::template::TemplateAsset::render_target(path).map_err(|msg| {
558 VmError::Thrown(VmValue::String(arcstr::ArcStr::from(format!(
559 "host_call template.render: {msg}"
560 ))))
561 })?;
562 crate::stdlib::template::render_asset_result(&asset, bindings).map_err(VmError::from)
563}
564
565fn params_match(expected: Option<&crate::value::DictMap>, actual: &crate::value::DictMap) -> bool {
566 let Some(expected) = expected else {
567 return true;
568 };
569 expected.iter().all(|(key, value)| {
570 actual
571 .get(key)
572 .is_some_and(|candidate| values_equal(candidate, value))
573 })
574}
575
576fn parse_host_mock(args: &[VmValue]) -> Result<HostMock, VmError> {
577 let capability = args
578 .first()
579 .map(|value| value.display())
580 .unwrap_or_default();
581 let operation = args.get(1).map(|value| value.display()).unwrap_or_default();
582 if capability.is_empty() || operation.is_empty() {
583 return Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
584 "host_mock: capability and operation are required",
585 ))));
586 }
587
588 let mut params = args
589 .get(3)
590 .and_then(|value| value.as_dict())
591 .map(|dict| (*dict).clone());
592 let mut result = args.get(2).cloned().or(Some(VmValue::Nil));
593 let mut error = None;
594 let mut unregistered_ok = false;
595
596 if let Some(config) = args.get(2).and_then(|value| value.as_dict()) {
597 if config.contains_key("result")
598 || config.contains_key("params")
599 || config.contains_key("error")
600 || config.contains_key("unregistered_ok")
601 {
602 params = config
603 .get("params")
604 .and_then(|value| value.as_dict())
605 .map(|dict| (*dict).clone());
606 result = config.get("result").cloned();
607 error = config
608 .get("error")
609 .map(|value| value.display())
610 .filter(|value| !value.is_empty());
611 unregistered_ok = matches!(config.get("unregistered_ok"), Some(VmValue::Bool(true)));
612 }
613 }
614
615 Ok(HostMock {
616 capability,
617 operation,
618 params,
619 result,
620 error,
621 unregistered_ok,
622 })
623}
624
625fn push_host_mock(host_mock: HostMock) {
626 HOST_MOCKS.with(|mocks| mocks.borrow_mut().push(host_mock));
627}
628
629fn mock_call_value(call: &HostMockCall) -> VmValue {
630 let mut item = crate::value::DictMap::new();
631 item.put_str("capability", call.capability.clone());
632 item.put_str("operation", call.operation.clone());
633 item.insert(
634 crate::value::intern_key("params"),
635 VmValue::dict(call.params.clone()),
636 );
637 VmValue::dict(item)
638}
639
640fn record_mock_call(capability: &str, operation: &str, params: &crate::value::DictMap) {
641 HOST_MOCK_CALLS.with(|calls| {
642 calls.borrow_mut().push(HostMockCall {
643 capability: capability.to_string(),
644 operation: operation.to_string(),
645 params: params.clone(),
646 });
647 });
648}
649
650pub(crate) fn dispatch_mock_host_call(
651 capability: &str,
652 operation: &str,
653 params: &crate::value::DictMap,
654) -> Option<Result<VmValue, VmError>> {
655 let matched = HOST_MOCKS.with(|mocks| {
656 mocks
657 .borrow()
658 .iter()
659 .rev()
660 .find(|host_mock| {
661 host_mock.capability == capability
662 && host_mock.operation == operation
663 && params_match(host_mock.params.as_ref(), params)
664 })
665 .cloned()
666 })?;
667
668 record_mock_call(capability, operation, params);
669 if let Some(error) = matched.error {
670 return Some(Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
671 error,
672 )))));
673 }
674 Some(Ok(matched.result.unwrap_or(VmValue::Nil)))
675}
676
677pub fn dispatch_mock_hostlib_call(
688 module: &str,
689 method: &str,
690 params: &crate::value::DictMap,
691) -> Option<Result<VmValue, VmError>> {
692 if let Some(mocked) = dispatch_mock_host_call(module, method, params) {
693 return Some(mocked);
694 }
695
696 if (module, method) == ("tools", "run_command") {
697 return dispatch_mock_host_call("process", "exec", params);
698 }
699
700 None
701}
702
703fn empty_tool_list_value() -> VmValue {
704 VmValue::List(std::sync::Arc::new(Vec::new()))
705}
706
707fn current_vm_host_bridge(
708 ctx: Option<&AsyncBuiltinCtx>,
709) -> Option<std::sync::Arc<crate::bridge::HostBridge>> {
710 ctx.and_then(|ctx| ctx.child_vm().bridge.clone())
711}
712
713#[cfg(test)]
714async fn dispatch_host_tool_list() -> Result<VmValue, VmError> {
715 dispatch_host_tool_list_with_ctx(None).await
716}
717
718async fn dispatch_host_tool_list_with_ctx(
719 ctx: Option<&AsyncBuiltinCtx>,
720) -> Result<VmValue, VmError> {
721 let bridge = HOST_CALL_BRIDGE.with(|b| b.borrow().clone());
722 if let Some(bridge) = bridge {
723 if let Some(value) = bridge.list_tools()? {
724 return Ok(value);
725 }
726 }
727
728 let Some(bridge) = current_vm_host_bridge(ctx) else {
729 return Ok(empty_tool_list_value());
730 };
731 let tools = bridge.list_host_tools().await?;
732 Ok(crate::bridge::json_result_to_vm_value(&JsonValue::Array(
733 tools.into_iter().collect(),
734 )))
735}
736
737pub(crate) async fn dispatch_host_tool_call(
738 name: &str,
739 args: &VmValue,
740) -> Result<VmValue, VmError> {
741 dispatch_host_tool_call_with_ctx(None, name, args).await
742}
743
744pub(crate) async fn dispatch_host_tool_call_with_ctx(
745 ctx: Option<&AsyncBuiltinCtx>,
746 name: &str,
747 args: &VmValue,
748) -> Result<VmValue, VmError> {
749 let bridge = HOST_CALL_BRIDGE.with(|b| b.borrow().clone());
750 if let Some(bridge) = bridge {
751 if let Some(value) = bridge.call_tool(name, args)? {
752 return Ok(value);
753 }
754 }
755
756 let Some(bridge) = current_vm_host_bridge(ctx) else {
757 return Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
758 "host_tool_call: no host bridge is attached",
759 ))));
760 };
761
762 let result = bridge
763 .call(
764 "builtin_call",
765 serde_json::json!({
766 "name": name,
767 "args": [crate::llm::vm_value_to_json(args)],
768 }),
769 )
770 .await?;
771 Ok(crate::bridge::json_result_to_vm_value(&result))
772}
773
774pub async fn dispatch_host_operation(
778 capability: &str,
779 operation: &str,
780 params: &crate::value::DictMap,
781) -> Result<VmValue, VmError> {
782 dispatch_host_operation_with_ctx(None, capability, operation, params).await
783}
784
785pub async fn dispatch_host_operation_with_ctx(
797 ctx: Option<&AsyncBuiltinCtx>,
798 capability: &str,
799 operation: &str,
800 params: &crate::value::DictMap,
801) -> Result<VmValue, VmError> {
802 let _invalidation = turn_cache::invalidation_scope(capability, operation);
803 if let Some(ctx) = ctx {
804 let vm = ctx.child_vm();
805 if let Some(fixtured) = vm.harness().and_then(|harness| {
806 harness
807 .inner()
808 .fixtures()
809 .dispatch_host(capability, operation, params)
810 }) {
811 return fixtured;
812 }
813 }
814 if let Some(mocked) = dispatch_mock_host_call(capability, operation, params) {
815 return mocked;
816 }
817
818 if (capability, operation) == ("process", "exec") {
819 let caller = serde_json::json!({
820 "surface": "host_call",
821 "capability": "process",
822 "operation": "exec",
823 "session_id": crate::llm::current_agent_session_id(),
824 });
825 return dispatch_process_exec_with_policy(ctx, params, caller).await;
826 }
827
828 if (capability, operation) == ("process", "spawn") {
835 let caller = serde_json::json!({
836 "surface": "host_call",
837 "capability": "process",
838 "operation": "spawn",
839 "session_id": crate::llm::current_agent_session_id(),
840 });
841 return dispatch_process_spawn_with_policy(ctx, params, caller).await;
842 }
843 if capability == "process" && matches!(operation, "poll" | "wait" | "kill" | "release") {
844 if let Some(result) = crate::stdlib::process_spawn::dispatch(
845 operation,
846 params,
847 process_exec::async_builtin_cancel_token(ctx),
848 )
849 .await
850 {
851 return result;
852 }
853 }
854
855 let bridge = HOST_CALL_BRIDGE.with(|b| b.borrow().clone());
856 if let Some(bridge) = bridge {
857 let dispatched = bridge::dispatch_cached(bridge, capability, operation, params).await?;
860 if let Some(value) = dispatched {
861 return Ok(value);
862 }
863 }
864
865 dispatch_builtin_host_operation(capability, operation, params).await
866}
867
868async fn dispatch_builtin_host_operation(
869 capability: &str,
870 operation: &str,
871 params: &crate::value::DictMap,
872) -> Result<VmValue, VmError> {
873 match (capability, operation) {
874 ("process", "list_shells") => Ok(crate::shells::list_shells_vm_value()),
875 ("process", "get_default_shell") => Ok(crate::shells::default_shell_vm_value()),
876 ("process", "set_default_shell") => crate::shells::set_default_shell_vm_value(params),
877 ("process", "shell_invocation") => crate::shells::shell_invocation_vm_value(params),
878 ("template", "render") => {
879 let path = require_param(params, "path")?;
880 let bindings = params.get("bindings").and_then(|v| v.as_dict());
881 Ok(VmValue::String(arcstr::ArcStr::from(render_template(
882 &path, bindings,
883 )?)))
884 }
885 ("interaction", "ask") => {
886 let question = require_param(params, "question")?;
887 super::io::prompt_user_value(&[VmValue::string(question)], &mut String::new())
888 }
889 ("project", "metadata_get") => crate::metadata::project_metadata_host_get(params),
890 ("project", "metadata_inspect") => crate::metadata::project_metadata_host_inspect(params),
891 ("project", "metadata_set") => crate::metadata::project_metadata_host_set(params),
892 ("project", "metadata_save") => crate::metadata::project_metadata_host_save(params),
893 ("project", "metadata_stale") => crate::metadata::project_metadata_host_stale(params),
894 ("project", "metadata_refresh_hashes") => {
895 crate::metadata::project_metadata_host_refresh_hashes(params)
896 }
897 ("runtime", "task") => Ok(VmValue::String(arcstr::ArcStr::from(
900 std::env::var("HARN_TASK").unwrap_or_default(),
901 ))),
902 ("runtime", "prompt_content") => Ok(VmValue::List(Arc::new(Vec::new()))),
903 ("runtime", "set_result") => {
904 Ok(VmValue::Nil)
907 }
908 ("workspace", "project_root") => {
909 let path = crate::stdlib::process::project_root_path()
914 .map(|root| root.display().to_string())
915 .or_else(|| std::env::var("HARN_PROJECT_ROOT").ok())
916 .unwrap_or_else(|| {
917 std::env::current_dir()
918 .map(|p| p.display().to_string())
919 .unwrap_or_default()
920 });
921 Ok(VmValue::String(arcstr::ArcStr::from(path)))
922 }
923 ("workspace", "cwd") => {
924 let path = std::env::current_dir()
925 .map(|p| p.display().to_string())
926 .unwrap_or_default();
927 Ok(VmValue::String(arcstr::ArcStr::from(path)))
928 }
929 _ => Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
930 format!("host_call: unsupported operation {capability}.{operation}"),
931 )))),
932 }
933}
934
935pub(crate) fn optional_i64(params: &crate::value::DictMap, key: &str) -> Option<i64> {
936 match params.get(key) {
937 Some(VmValue::Int(value)) => Some(*value),
938 Some(VmValue::Float(value)) if value.fract() == 0.0 => Some(*value as i64),
939 _ => None,
940 }
941}
942
943pub(crate) fn optional_string(params: &crate::value::DictMap, key: &str) -> Option<String> {
944 params.get(key).and_then(vm_string).map(ToString::to_string)
945}
946
947fn optional_string_list(params: &crate::value::DictMap, key: &str) -> Option<Vec<String>> {
948 let VmValue::List(values) = params.get(key)? else {
949 return None;
950 };
951 values
952 .iter()
953 .map(|value| vm_string(value).map(ToString::to_string))
954 .collect()
955}
956
957fn optional_string_dict(
958 params: &crate::value::DictMap,
959 key: &str,
960) -> Result<Option<BTreeMap<String, String>>, VmError> {
961 let Some(value) = params.get(key) else {
962 return Ok(None);
963 };
964 let Some(dict) = value.as_dict() else {
965 return Err(VmError::Runtime(format!(
966 "host_call process.exec {key} must be a dict"
967 )));
968 };
969 let mut out = std::collections::BTreeMap::new();
970 for (key, value) in dict.iter() {
971 let Some(value) = vm_string(value) else {
972 return Err(VmError::Runtime(format!(
973 "host_call process.exec env value for {key:?} must be a string"
974 )));
975 };
976 out.insert(key.to_string(), value.to_string());
977 }
978 Ok(Some(out))
979}
980
981fn vm_string(value: &VmValue) -> Option<&str> {
982 match value {
983 VmValue::String(value) => Some(value.as_ref()),
984 _ => None,
985 }
986}
987
988pub(crate) fn register_host_builtins(vm: &mut Vm) {
989 for def in MODULE_BUILTINS {
990 vm.register_builtin_def(def);
991 }
992}
993
994pub(crate) fn register_missing_host_builtins(vm: &mut Vm) {
995 for def in MODULE_BUILTINS {
996 if vm.builtin_metadata_for(def.sig.name).is_none() {
997 vm.register_builtin_def(def);
998 }
999 }
1000}
1001
1002#[harn_builtin(
1003 exposure = "privileged_wire",
1004 effects = ["host.mutate@arg0"],
1005 sig = "host_mock(capability: string, op: string, response_or_config?: any, params?: dict) -> nil",
1006 category = "host"
1007)]
1008pub(crate) fn host_mock_builtin(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1009 let host_mock = parse_host_mock(args)?;
1010 validate_host_mock_registration(&host_mock)?;
1011 push_host_mock(host_mock);
1012 Ok(VmValue::Nil)
1013}
1014
1015#[harn_builtin(
1016 exposure = "privileged_wire",
1017 effects = [],
1018 sig = "host_mock_clear() -> nil", category = "host"
1019)]
1020fn host_mock_clear_builtin(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1021 reset_host_state();
1022 Ok(VmValue::Nil)
1023}
1024
1025#[harn_builtin(
1026 exposure = "runtime_internal",
1027 effects = [],
1028 sig = "host_mock_calls() -> list", category = "host"
1029)]
1030fn host_mock_calls_builtin(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1031 let calls = HOST_MOCK_CALLS.with(|calls| {
1032 calls
1033 .borrow()
1034 .iter()
1035 .map(mock_call_value)
1036 .collect::<Vec<_>>()
1037 });
1038 Ok(VmValue::List(std::sync::Arc::new(calls)))
1039}
1040
1041#[harn_builtin(
1042 exposure = "runtime_internal",
1043 effects = [],
1044 sig = "host_mock_push_scope() -> nil", category = "host"
1045)]
1046fn host_mock_push_scope_builtin(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1047 push_host_mock_scope();
1048 Ok(VmValue::Nil)
1049}
1050
1051#[harn_builtin(
1052 exposure = "runtime_internal",
1053 effects = [],
1054 sig = "host_mock_pop_scope() -> nil", category = "host"
1055)]
1056fn host_mock_pop_scope_builtin(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1057 if !pop_host_mock_scope() {
1058 return Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
1059 "host_mock_pop_scope: no scope to pop",
1060 ))));
1061 }
1062 Ok(VmValue::Nil)
1063}
1064
1065#[harn_builtin(
1066 exposure = "runtime_internal",
1067 effects = [],
1068 sig = "host_capabilities() -> dict", category = "host"
1069)]
1070fn host_capabilities_builtin(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1071 Ok(capability_manifest_with_mocks())
1072}
1073
1074#[harn_builtin(
1075 exposure = "runtime_internal",
1076 effects = [],
1077 sig = "host_has(capability: string, op?: string) -> bool",
1078 category = "host"
1079)]
1080fn host_has_builtin(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1081 let capability = args.first().map(|a| a.display()).unwrap_or_default();
1082 let operation = args.get(1).map(|a| a.display());
1083 let manifest = capability_manifest_with_mocks();
1084 let has = manifest
1085 .as_dict()
1086 .and_then(|d| d.get(capability.as_str()))
1087 .and_then(|v| v.as_dict())
1088 .is_some_and(|cap| {
1089 if let Some(operation) = operation {
1090 cap.get("ops")
1091 .and_then(|v| match v {
1092 VmValue::List(list) => {
1093 Some(list.iter().any(|item| item.display() == operation))
1094 }
1095 _ => None,
1096 })
1097 .unwrap_or(false)
1098 } else {
1099 true
1100 }
1101 });
1102 Ok(VmValue::Bool(has))
1103}
1104
1105#[harn_builtin(
1106 exposure = "privileged_wire",
1107 effects = [],
1108 sig = "host_call(name: string, args?: dict) -> any",
1109 kind = "async",
1110 category = "host"
1111)]
1112async fn host_call_builtin(
1113 ctx: crate::vm::AsyncBuiltinCtx,
1114 args: Vec<VmValue>,
1115) -> Result<VmValue, VmError> {
1116 let name = args.first().map(|a| a.display()).unwrap_or_default();
1117 let params = args
1118 .get(1)
1119 .and_then(|a| a.as_dict())
1120 .cloned()
1121 .unwrap_or_default();
1122 let Some((capability, operation)) = name.split_once('.') else {
1123 return Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
1124 format!("host_call: unsupported operation name '{name}'"),
1125 ))));
1126 };
1127 dispatch_host_operation_with_ctx(Some(&ctx), capability, operation, ¶ms).await
1128}
1129
1130#[harn_builtin(
1131 exposure = "runtime_internal",
1132 effects = [],
1133 sig = "host_tool_list() -> list", kind = "async", category = "host"
1134)]
1135async fn host_tool_list_builtin(
1136 ctx: crate::vm::AsyncBuiltinCtx,
1137 _args: Vec<VmValue>,
1138) -> Result<VmValue, VmError> {
1139 dispatch_host_tool_list_with_ctx(Some(&ctx)).await
1140}
1141
1142#[harn_builtin(
1143 exposure = "runtime_internal",
1144 effects = [],
1145 sig = "host_tool_call(name: string, args?: any) -> any",
1146 kind = "async",
1147 category = "host"
1148)]
1149async fn host_tool_call_builtin(
1150 ctx: crate::vm::AsyncBuiltinCtx,
1151 args: Vec<VmValue>,
1152) -> Result<VmValue, VmError> {
1153 let name = args.first().map(|a| a.display()).unwrap_or_default();
1154 if name.is_empty() {
1155 return Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
1156 "host_tool_call: tool name is required",
1157 ))));
1158 }
1159 let call_args = args.get(1).cloned().unwrap_or(VmValue::Nil);
1160 dispatch_host_tool_call_with_ctx(Some(&ctx), &name, &call_args).await
1161}
1162
1163#[cfg(test)]
1164#[path = "host/tests.rs"]
1165mod tests;