Skip to content

Commit eed720a

Browse files
authored
Merge pull request #1 from rexleimo/refactor-openharness-boundaries
Refactor openharness boundaries
2 parents f33803e + a79e8f2 commit eed720a

9 files changed

Lines changed: 661 additions & 350 deletions

File tree

crates/loopforge-cli/src/dispatch/agent.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,21 @@ pub(super) async fn run(command: AgentCommand) -> anyhow::Result<()> {
1717
Some(id) => id,
1818
None => rexos::harness::resolve_session_id(&workspace)?,
1919
};
20-
if !allowed_tools.is_empty() {
21-
agent.set_session_allowed_tools(&session_id, allowed_tools)?;
22-
}
23-
if let Some(path) = mcp_config.as_ref() {
24-
let raw = std::fs::read_to_string(path)?;
25-
let json: serde_json::Value =
26-
serde_json::from_str(&raw).map_err(|err| anyhow::anyhow!("{err}"))?;
27-
agent.set_session_mcp_config(&session_id, serde_json::to_string(&json)?)?;
28-
}
20+
let allowed_tools = if allowed_tools.is_empty() {
21+
None
22+
} else {
23+
Some(allowed_tools)
24+
};
25+
let mcp_config_json = match mcp_config.as_ref() {
26+
Some(path) => {
27+
let raw = std::fs::read_to_string(path)?;
28+
let json: serde_json::Value =
29+
serde_json::from_str(&raw).map_err(|err| anyhow::anyhow!("{err}"))?;
30+
Some(serde_json::to_string(&json)?)
31+
}
32+
None => None,
33+
};
34+
agent.configure_session_tooling(&session_id, allowed_tools, mcp_config_json)?;
2935
let out = agent
3036
.run_session(
3137
workspace,

crates/rexos-runtime/src/session_runner/chat_loop.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,17 @@ impl AgentRuntime {
1919
user_prompt: &str,
2020
kind: TaskKind,
2121
) -> anyhow::Result<String> {
22-
let allowed_tools = self.load_session_allowed_tools(session_id)?;
23-
let allowed_lookup: Option<HashSet<String>> = allowed_tools
22+
let mut policy = self.load_session_policy_snapshot(session_id)?;
23+
let allowed_lookup: Option<HashSet<String>> = policy
24+
.allowed_tools
2425
.as_ref()
2526
.map(|tools| tools.iter().cloned().collect());
26-
let mcp_config = self.load_session_mcp_config(session_id)?;
27+
let allowed_tools = policy.allowed_tools.take();
2728
let tools = Toolset::new_with_allowed_tools_security_and_mcp_config(
2829
workspace_root.clone(),
2930
allowed_tools,
3031
self.security.clone(),
31-
mcp_config.as_deref(),
32+
policy.mcp_config_json.as_deref(),
3233
)
3334
.await?;
3435
let provider = self.router.provider_for(kind);

crates/rexos-runtime/src/session_skills/audit.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ impl AgentRuntime {
3131
skill_name: &str,
3232
requested_permissions: &[String],
3333
) -> anyhow::Result<()> {
34-
if let Some(allowed_skills) = self.load_session_allowed_skills(session_id)? {
34+
let session_policy = self.load_session_policy_snapshot(session_id)?;
35+
36+
if let Some(allowed_skills) = session_policy.allowed_skills.as_ref() {
3537
if !allowed_skills
3638
.iter()
3739
.any(|skill| skill.eq_ignore_ascii_case(skill_name.trim()))
@@ -52,7 +54,7 @@ impl AgentRuntime {
5254
}
5355
}
5456

55-
let policy: SessionSkillPolicy = self.load_session_skill_policy(session_id)?;
57+
let policy: SessionSkillPolicy = session_policy.skill_policy;
5658
if !policy.allowlist.is_empty()
5759
&& !policy
5860
.allowlist

crates/rexos-runtime/src/session_skills/storage.rs

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ use crate::{
88
SESSION_SKILL_POLICY_KEY_PREFIX,
99
};
1010

11+
#[derive(Debug, Clone)]
12+
pub(crate) struct SessionPolicySnapshot {
13+
pub(crate) allowed_tools: Option<Vec<String>>,
14+
pub(crate) allowed_skills: Option<Vec<String>>,
15+
pub(crate) skill_policy: SessionSkillPolicy,
16+
pub(crate) mcp_config_json: Option<String>,
17+
}
18+
1119
fn normalize_names(values: impl IntoIterator<Item = String>) -> Vec<String> {
1220
let mut cleaned = Vec::new();
1321
let mut seen = HashSet::new();
@@ -40,6 +48,33 @@ impl AgentRuntime {
4048
format!("{SESSION_SKILL_POLICY_KEY_PREFIX}{session_id}")
4149
}
4250

51+
pub(crate) fn load_session_policy_snapshot(
52+
&self,
53+
session_id: &str,
54+
) -> anyhow::Result<SessionPolicySnapshot> {
55+
Ok(SessionPolicySnapshot {
56+
allowed_tools: self.load_session_allowed_tools(session_id)?,
57+
allowed_skills: self.load_session_allowed_skills(session_id)?,
58+
skill_policy: self.load_session_skill_policy(session_id)?,
59+
mcp_config_json: self.load_session_mcp_config(session_id)?,
60+
})
61+
}
62+
63+
pub fn configure_session_tooling(
64+
&self,
65+
session_id: &str,
66+
allowed_tools: Option<Vec<String>>,
67+
mcp_config_json: Option<String>,
68+
) -> anyhow::Result<()> {
69+
if let Some(tools) = allowed_tools {
70+
self.set_session_allowed_tools(session_id, tools)?;
71+
}
72+
if let Some(config_json) = mcp_config_json {
73+
self.set_session_mcp_config(session_id, config_json)?;
74+
}
75+
Ok(())
76+
}
77+
4378
pub fn set_session_allowed_tools(
4479
&self,
4580
session_id: &str,
@@ -157,3 +192,196 @@ impl AgentRuntime {
157192
Ok(policy)
158193
}
159194
}
195+
196+
#[cfg(test)]
197+
mod tests {
198+
use std::collections::BTreeMap;
199+
200+
use rexos_kernel::config::{LlmConfig, ProviderConfig, ProviderKind, RexosConfig, RouteConfig};
201+
use rexos_kernel::paths::RexosPaths;
202+
use rexos_kernel::router::{ModelRouter, TaskKind};
203+
use rexos_kernel::security::SecurityConfig;
204+
use rexos_llm::registry::LlmRegistry;
205+
use rexos_memory::MemoryStore;
206+
207+
use crate::records::{WorkflowRunToolArgs, WorkflowStepToolArgs};
208+
use crate::AgentRuntime;
209+
210+
fn build_agent(memory: MemoryStore) -> AgentRuntime {
211+
let mut providers = BTreeMap::new();
212+
providers.insert(
213+
"ollama".to_string(),
214+
ProviderConfig {
215+
kind: ProviderKind::OpenAiCompatible,
216+
base_url: "http://127.0.0.1:11434/v1".to_string(),
217+
api_key_env: "".to_string(),
218+
default_model: "x".to_string(),
219+
aws_bedrock: None,
220+
},
221+
);
222+
223+
let security = SecurityConfig::default();
224+
let cfg = RexosConfig {
225+
llm: LlmConfig::default(),
226+
providers,
227+
router: Default::default(),
228+
security: security.clone(),
229+
};
230+
let llms = LlmRegistry::from_config(&cfg).unwrap();
231+
let router = ModelRouter::new(rexos_kernel::config::RouterConfig {
232+
planning: RouteConfig {
233+
provider: "ollama".to_string(),
234+
model: "x".to_string(),
235+
},
236+
coding: RouteConfig {
237+
provider: "ollama".to_string(),
238+
model: "x".to_string(),
239+
},
240+
summary: RouteConfig {
241+
provider: "ollama".to_string(),
242+
model: "x".to_string(),
243+
},
244+
});
245+
AgentRuntime::new_with_security_config(memory, llms, router, security)
246+
}
247+
248+
#[test]
249+
fn session_policy_snapshot_round_trips_and_normalizes() {
250+
let tmp = tempfile::tempdir().unwrap();
251+
let paths = RexosPaths {
252+
base_dir: tmp.path().join(".loopforge"),
253+
};
254+
paths.ensure_dirs().unwrap();
255+
256+
let memory = MemoryStore::open_or_create(&paths).unwrap();
257+
let agent = build_agent(memory);
258+
259+
agent
260+
.set_session_allowed_tools(
261+
"s1",
262+
vec![
263+
" fs_read ".to_string(),
264+
"".to_string(),
265+
"fs_write".to_string(),
266+
"fs_read".to_string(),
267+
],
268+
)
269+
.unwrap();
270+
agent
271+
.set_session_allowed_skills(
272+
"s1",
273+
vec![
274+
" safe-skill ".to_string(),
275+
"safe-skill".to_string(),
276+
"x".to_string(),
277+
"".to_string(),
278+
],
279+
)
280+
.unwrap();
281+
agent
282+
.set_session_skill_policy(
283+
"s1",
284+
crate::SessionSkillPolicy {
285+
allowlist: vec!["shell-helper".to_string()],
286+
require_approval: true,
287+
auto_approve_readonly: false,
288+
},
289+
)
290+
.unwrap();
291+
agent
292+
.set_session_mcp_config("s1", " {\"servers\":{}} ".to_string())
293+
.unwrap();
294+
295+
let snapshot = agent.load_session_policy_snapshot("s1").unwrap();
296+
assert_eq!(
297+
snapshot.allowed_tools,
298+
Some(vec!["fs_read".to_string(), "fs_write".to_string()])
299+
);
300+
assert_eq!(
301+
snapshot.allowed_skills,
302+
Some(vec!["safe-skill".to_string(), "x".to_string()])
303+
);
304+
assert_eq!(
305+
snapshot.mcp_config_json,
306+
Some("{\"servers\":{}}".to_string())
307+
);
308+
assert_eq!(
309+
snapshot.skill_policy.allowlist,
310+
vec!["shell-helper".to_string()]
311+
);
312+
assert!(snapshot.skill_policy.require_approval);
313+
assert!(!snapshot.skill_policy.auto_approve_readonly);
314+
}
315+
316+
#[test]
317+
fn session_policy_snapshot_defaults_when_missing_or_blank() {
318+
let tmp = tempfile::tempdir().unwrap();
319+
let paths = RexosPaths {
320+
base_dir: tmp.path().join(".loopforge"),
321+
};
322+
paths.ensure_dirs().unwrap();
323+
324+
let memory = MemoryStore::open_or_create(&paths).unwrap();
325+
let agent = build_agent(memory);
326+
327+
agent
328+
.set_session_mcp_config("s2", " ".to_string())
329+
.unwrap();
330+
let snapshot = agent.load_session_policy_snapshot("s2").unwrap();
331+
assert!(snapshot.allowed_tools.is_none());
332+
assert!(snapshot.allowed_skills.is_none());
333+
assert!(snapshot.mcp_config_json.is_none());
334+
assert!(!snapshot.skill_policy.auto_approve_readonly);
335+
assert!(!snapshot.skill_policy.require_approval);
336+
assert!(snapshot.skill_policy.allowlist.is_empty());
337+
}
338+
339+
#[tokio::test]
340+
async fn session_policy_workflow_uses_allowed_tools_snapshot() {
341+
let tmp = tempfile::tempdir().unwrap();
342+
let workspace_root = tmp.path().join("workspace");
343+
std::fs::create_dir_all(&workspace_root).unwrap();
344+
345+
let paths = RexosPaths {
346+
base_dir: tmp.path().join(".loopforge"),
347+
};
348+
paths.ensure_dirs().unwrap();
349+
let memory = MemoryStore::open_or_create(&paths).unwrap();
350+
let agent = build_agent(memory);
351+
352+
agent
353+
.set_session_allowed_tools("s3", vec!["fs_read".to_string()])
354+
.unwrap();
355+
356+
let res = agent
357+
.workflow_run(
358+
&workspace_root,
359+
"s3",
360+
TaskKind::Coding,
361+
WorkflowRunToolArgs {
362+
workflow_id: Some("wf-policy".to_string()),
363+
name: None,
364+
steps: vec![WorkflowStepToolArgs {
365+
tool: "fs_write".to_string(),
366+
arguments: serde_json::json!({
367+
"path": "x.txt",
368+
"content": "blocked",
369+
}),
370+
name: None,
371+
approval_required: None,
372+
}],
373+
continue_on_error: None,
374+
},
375+
)
376+
.await
377+
.unwrap();
378+
379+
let res: serde_json::Value = serde_json::from_str(&res).unwrap();
380+
let saved_to = res["saved_to"].as_str().unwrap();
381+
let state_raw = std::fs::read_to_string(saved_to).unwrap();
382+
let state: serde_json::Value = serde_json::from_str(&state_raw).unwrap();
383+
let err = state["steps"][0]["error"].as_str().unwrap();
384+
assert!(err.contains("workflow step 0 (fs_write)"), "got: {err}");
385+
assert!(!workspace_root.join("x.txt").exists());
386+
}
387+
}

crates/rexos-runtime/src/workflow.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@ impl AgentRuntime {
4545
let state_path = workflow_state_path(workspace_root, &workflow_id);
4646
self.write_workflow_state(&state_path, &state)?;
4747

48-
let allowed_tools = self.load_session_allowed_tools(session_id)?;
49-
let mcp_config = self.load_session_mcp_config(session_id)?;
48+
let mut policy = self.load_session_policy_snapshot(session_id)?;
49+
let allowed_tools = policy.allowed_tools.take();
5050
let tools = Toolset::new_with_allowed_tools_security_and_mcp_config(
5151
workspace_root.clone(),
5252
allowed_tools,
5353
self.security.clone(),
54-
mcp_config.as_deref(),
54+
policy.mcp_config_json.as_deref(),
5555
)
5656
.await?;
5757
let continue_on_error = args.continue_on_error.unwrap_or(false);

0 commit comments

Comments
 (0)