Skip to main content

lean_ctx/core/ocla/adapters/
passthrough.rs

1//! Passthrough/control adapter for native A/B comparisons.
2
3use std::path::{Path, PathBuf};
4use std::sync::OnceLock;
5use std::time::Instant;
6
7use lean_ctx_protocol::CapabilityManifestV1;
8
9use super::{invoke_passthrough_text, read_context_paths};
10use crate::core::ocla::invocation::{
11    CapabilityAdapter, CapabilityInput, CapabilityInvocation, CapabilityResult,
12};
13use crate::core::ocla::{OclaError, OclaResult};
14
15/// Repository-relative manifest used by this adapter.
16pub const MANIFEST_PATH: &str =
17    "docs/contracts/ocla/capability-manifests/leanctx/passthrough-v1.json";
18const MANIFEST_JSON: &str = include_str!(concat!(
19    env!("CARGO_MANIFEST_DIR"),
20    "/assets/ocla/capability-manifests/leanctx/passthrough-v1.json"
21));
22
23fn manifest() -> &'static CapabilityManifestV1 {
24    static MANIFEST: OnceLock<CapabilityManifestV1> = OnceLock::new();
25    MANIFEST.get_or_init(|| {
26        let manifest: CapabilityManifestV1 =
27            serde_json::from_str(MANIFEST_JSON).expect("passthrough manifest is valid JSON");
28        manifest
29            .validate()
30            .expect("passthrough manifest satisfies the protocol");
31        manifest
32    })
33}
34
35/// Baseline adapter that returns the materialized input unchanged.
36pub struct PassthroughAdapter {
37    root: PathBuf,
38}
39
40impl PassthroughAdapter {
41    #[must_use]
42    pub fn new() -> Self {
43        Self {
44            root: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
45        }
46    }
47
48    #[must_use]
49    pub fn with_root(root: impl AsRef<Path>) -> Self {
50        Self {
51            root: root.as_ref().to_path_buf(),
52        }
53    }
54
55    #[must_use]
56    pub fn root(&self) -> &Path {
57        &self.root
58    }
59}
60
61impl Default for PassthroughAdapter {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl CapabilityAdapter for PassthroughAdapter {
68    fn manifest(&self) -> &CapabilityManifestV1 {
69        manifest()
70    }
71
72    fn invoke(&self, invocation: CapabilityInvocation) -> OclaResult<CapabilityResult> {
73        invocation.validate()?;
74        if invocation.capability_id != self.manifest().capability_id.as_str()
75            || invocation.capability_version != self.manifest().version
76        {
77            return Err(OclaError::InvalidRequest(
78                "invocation capability identity does not match adapter manifest".into(),
79            ));
80        }
81        let start = Instant::now();
82        match invocation.input.clone() {
83            CapabilityInput::ContextRequest { paths, .. } => {
84                let content = read_context_paths(&self.root, &paths)?;
85                invoke_passthrough_text(&invocation, &content, start)
86            }
87            CapabilityInput::ShellCommand { command, .. } => {
88                invoke_passthrough_text(&invocation, &command, start)
89            }
90            CapabilityInput::ModelRequest { prompt, .. } => {
91                invoke_passthrough_text(&invocation, &prompt, start)
92            }
93            CapabilityInput::AgentTask(_) => Err(OclaError::InvalidRequest(
94                "passthrough cannot execute agent tasks".into(),
95            )),
96        }
97    }
98
99    fn health_check(&self) -> OclaResult<bool> {
100        Ok(self.root.is_dir() && self.manifest().validate().is_ok())
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::core::ocla::invocation::{CapabilityAdapter, PolicyConstraints};
108
109    #[test]
110    fn preserves_shell_command_content_reference() {
111        let adapter = PassthroughAdapter::new();
112        let invocation = CapabilityInvocation {
113            task_id: "task-1".into(),
114            capability_id: adapter.manifest().capability_id.as_str().into(),
115            capability_version: adapter.manifest().version.clone(),
116            input: CapabilityInput::ShellCommand {
117                command: "printf unchanged".into(),
118                workdir: None,
119            },
120            policy_constraints: PolicyConstraints::default(),
121            // This test checks content identity, not machine-dependent tokenizer startup time.
122            timeout_ms: 0,
123        };
124        let result = adapter.invoke(invocation).expect("passthrough invocation");
125        assert!(result.success);
126        assert_eq!(result.observation.input_tokens, result.output_tokens);
127        assert!(result.evidence_ref.is_some());
128    }
129}