Use live owner tool scope for autonomous routines and jobs (#1453)

* Use live owner tool scope for autonomous runs

* Address autonomous tool scope review feedback

* Normalize routine context paths again
This commit is contained in:
Henry Park
2026-03-20 10:12:32 -07:00
committed by GitHub
parent 3da9810e87
commit ee6f5cd62a
23 changed files with 944 additions and 967 deletions
+1 -4
View File
@@ -15,8 +15,7 @@ mod tests {
use uuid::Uuid;
use ironclaw::agent::routine::{
FullJobPermissionMode, Routine, RoutineAction, RoutineGuardrails, RoutineRun, RunStatus,
Trigger,
Routine, RoutineAction, RoutineGuardrails, RoutineRun, RunStatus, Trigger,
};
use ironclaw::context::{JobContext, JobState};
use ironclaw::db::Database;
@@ -46,8 +45,6 @@ mod tests {
title: "Test job".to_string(),
description: "Test description".to_string(),
max_iterations: 5,
tool_permissions: vec![],
permission_mode: FullJobPermissionMode::Explicit,
},
guardrails: RoutineGuardrails {
cooldown: std::time::Duration::from_secs(0),
+3 -20
View File
@@ -10,7 +10,7 @@ mod support;
mod tests {
use std::time::Duration;
use ironclaw::agent::routine::{FullJobPermissionMode, RoutineAction, Trigger};
use ironclaw::agent::routine::{RoutineAction, Trigger};
use crate::support::test_rig::TestRigBuilder;
use crate::support::trace_llm::LlmTrace;
@@ -356,15 +356,8 @@ mod tests {
}
match &routine.action {
RoutineAction::FullJob {
description,
tool_permissions,
permission_mode,
..
} => {
RoutineAction::FullJob { description, .. } => {
assert!(description.contains("Summarize the new issue"));
assert_eq!(tool_permissions, &vec!["shell".to_string()]);
assert_eq!(permission_mode, &FullJobPermissionMode::InheritOwner);
}
other => panic!("expected full_job action, got {other:?}"),
}
@@ -412,18 +405,8 @@ mod tests {
}
match &routine.action {
RoutineAction::FullJob {
description,
tool_permissions,
permission_mode,
..
} => {
RoutineAction::FullJob { description, .. } => {
assert!(description.contains("Prepare the morning digest"));
assert_eq!(
tool_permissions,
&vec!["message".to_string(), "http".to_string()]
);
assert_eq!(permission_mode, &FullJobPermissionMode::InheritOwner);
}
other => panic!("expected full_job action, got {other:?}"),
}
+380 -88
View File
@@ -8,27 +8,33 @@ mod support;
#[cfg(feature = "libsql")]
mod tests {
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use chrono::Utc;
use libsql::params;
use secrecy::SecretString;
use uuid::Uuid;
use ironclaw::agent::routine::{
FullJobPermissionMode, NotifyConfig, Routine, RoutineAction, RoutineGuardrails, RoutineRun,
RunStatus, Trigger,
NotifyConfig, Routine, RoutineAction, RoutineGuardrails, RoutineRun, RunStatus, Trigger,
};
use ironclaw::agent::routine_engine::RoutineEngine;
use ironclaw::agent::{HeartbeatConfig, HeartbeatRunner, SandboxReadiness, Scheduler};
use ironclaw::agent::{
HeartbeatConfig, HeartbeatRunner, SandboxReadiness, Scheduler, SchedulerDeps,
};
use ironclaw::channels::IncomingMessage;
use ironclaw::config::{AgentConfig, RoutineConfig, SafetyConfig};
use ironclaw::context::{ContextManager, JobContext};
use ironclaw::db::{Database, libsql::LibSqlBackend};
use ironclaw::extensions::ExtensionManager;
use ironclaw::hooks::HookRegistry;
use ironclaw::llm::LlmProvider;
use ironclaw::safety::SafetyLayer;
use ironclaw::secrets::{InMemorySecretsStore, SecretsCrypto, SecretsStore};
use ironclaw::tools::builtin::routine::RoutineUpdateTool;
use ironclaw::tools::mcp::{McpProcessManager, McpSessionManager};
use ironclaw::tools::{ApprovalRequirement, Tool, ToolError, ToolOutput, ToolRegistry};
use ironclaw::workspace::Workspace;
use ironclaw::workspace::hygiene::HygieneConfig;
@@ -165,11 +171,7 @@ mod tests {
}
}
fn make_full_job_routine(
name: &str,
permission_mode: FullJobPermissionMode,
tool_permissions: Vec<String>,
) -> Routine {
fn make_full_job_routine(name: &str) -> Routine {
Routine {
id: Uuid::new_v4(),
name: name.to_string(),
@@ -181,8 +183,6 @@ mod tests {
title: name.to_string(),
description: "Use the owner-gated tool when permitted.".to_string(),
max_iterations: 3,
tool_permissions,
permission_mode,
},
guardrails: RoutineGuardrails {
cooldown: Duration::from_secs(0),
@@ -234,27 +234,112 @@ mod tests {
LlmTrace::single_turn("test-owner-gate", "run owner gate", steps)
}
async fn setup_owner_gate_engine(db: Arc<dyn Database>, trace: LlmTrace) -> Arc<RoutineEngine> {
fn owner_gate_lightweight_trace() -> LlmTrace {
LlmTrace::single_turn(
"test-owner-gate-lightweight",
"run owner gate",
vec![
TraceStep {
request_hint: None,
response: TraceResponse::ToolCalls {
tool_calls: vec![TraceToolCall {
id: "call_owner_gate".to_string(),
name: "owner_gate".to_string(),
arguments: serde_json::json!({}),
}],
input_tokens: 40,
output_tokens: 10,
},
expected_tool_results: vec![],
},
TraceStep {
request_hint: None,
response: TraceResponse::Text {
content: "ROUTINE_OK".to_string(),
input_tokens: 20,
output_tokens: 5,
},
expected_tool_results: vec![],
},
],
)
}
async fn write_test_extension_wasm(tools_dir: &Path, name: &str) {
tokio::fs::create_dir_all(tools_dir)
.await
.expect("create test wasm tools dir");
tokio::fs::write(tools_dir.join(format!("{name}.wasm")), b"\0asm")
.await
.expect("write test wasm tool marker");
}
fn make_test_extension_manager(
tools: Arc<ToolRegistry>,
tools_dir: &Path,
owner_id: &str,
) -> Arc<ExtensionManager> {
let crypto = Arc::new(
SecretsCrypto::new(SecretString::from(
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
))
.expect("test crypto"),
);
let secrets: Arc<dyn SecretsStore + Send + Sync> =
Arc::new(InMemorySecretsStore::new(crypto));
Arc::new(ExtensionManager::new(
Arc::new(McpSessionManager::new()),
Arc::new(McpProcessManager::new()),
secrets,
tools,
None,
None,
tools_dir.to_path_buf(),
tools_dir.join("channels"),
None,
owner_id.to_string(),
None,
Vec::new(),
))
}
async fn setup_owner_gate_engine(
db: Arc<dyn Database>,
trace: LlmTrace,
tools_dir: &Path,
extension_owner_id: Option<&str>,
activate_owner_gate: bool,
) -> Arc<RoutineEngine> {
let ws = create_workspace(&db);
let (notify_tx, _rx) = tokio::sync::mpsc::channel(16);
let registry = Arc::new(ToolRegistry::new());
registry
.register(Arc::new(OwnerGateTool { store: db.clone() }))
.await;
if extension_owner_id.is_some() {
registry
.register(Arc::new(OwnerGateTool { store: db.clone() }))
.await;
}
if activate_owner_gate {
write_test_extension_wasm(tools_dir, "owner_gate").await;
}
let safety = Arc::new(SafetyLayer::new(&SafetyConfig {
max_output_length: 100_000,
injection_check_enabled: false,
}));
let llm: Arc<dyn LlmProvider> = Arc::new(TraceLlm::from_trace(trace));
let extension_manager = extension_owner_id
.map(|owner_id| make_test_extension_manager(registry.clone(), tools_dir, owner_id));
let scheduler = Arc::new(Scheduler::new(
AgentConfig::for_testing(),
Arc::new(ContextManager::new(5)),
llm.clone(),
safety.clone(),
registry.clone(),
Some(db.clone()),
Arc::new(HookRegistry::new()),
SchedulerDeps {
tools: registry.clone(),
extension_manager: extension_manager.clone(),
store: Some(db.clone()),
hooks: Arc::new(HookRegistry::new()),
},
));
Arc::new(RoutineEngine::new(
@@ -264,9 +349,10 @@ mod tests {
ws,
notify_tx,
Some(scheduler),
extension_manager,
registry,
safety,
SandboxReadiness::DisabledByConfig,
SandboxReadiness::Available,
))
}
@@ -303,6 +389,28 @@ mod tests {
}
}
async fn wait_for_any_run_completion(db: &Arc<dyn Database>, routine_id: Uuid) -> RoutineRun {
let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
let runs = db
.list_routine_runs(routine_id, 10)
.await
.expect("list_routine_runs");
if let Some(run) = runs
.into_iter()
.find(|run| run.status != RunStatus::Running)
{
return run;
}
assert!(
std::time::Instant::now() < deadline,
"timed out waiting for any routine run for {routine_id} to complete"
);
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
// -----------------------------------------------------------------------
// Test 1: cron_routine_fires
// -----------------------------------------------------------------------
@@ -345,6 +453,7 @@ mod tests {
ws,
notify_tx,
None,
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
@@ -423,6 +532,7 @@ mod tests {
ws,
notify_tx,
None,
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
@@ -517,6 +627,7 @@ mod tests {
ws,
notify_tx,
None,
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
@@ -625,6 +736,7 @@ mod tests {
ws,
notify_tx,
None,
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
@@ -767,6 +879,7 @@ mod tests {
ws,
notify_tx,
None,
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
@@ -953,6 +1066,7 @@ mod tests {
ws,
notify_tx,
None,
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
@@ -1083,6 +1197,7 @@ mod tests {
ws,
notify_tx,
None, // no scheduler — rejected before dispatch
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
@@ -1100,8 +1215,6 @@ mod tests {
title: "t".to_string(),
description: "d".to_string(),
max_iterations: 3,
tool_permissions: vec![],
permission_mode: ironclaw::agent::routine::FullJobPermissionMode::Explicit,
},
guardrails: RoutineGuardrails {
cooldown: Duration::from_secs(0),
@@ -1192,6 +1305,7 @@ mod tests {
ws,
notify_tx,
None,
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
@@ -1250,28 +1364,27 @@ mod tests {
}
// -----------------------------------------------------------------------
// Test: inherit_owner full_job routines can use owner-gated tools
// Test: lightweight manual routines use the owner's active extension tools
// -----------------------------------------------------------------------
#[tokio::test]
async fn full_job_inherit_owner_uses_owner_allowlist() {
let (backend, _tmp) = create_test_backend().await;
async fn lightweight_manual_routine_uses_active_owner_extension_tool() {
let (backend, tmp) = create_test_backend().await;
let db: Arc<dyn Database> = backend;
let engine = setup_owner_gate_engine(db.clone(), owner_gate_trace(true)).await;
db.set_setting(
"default",
ironclaw::agent::routine::FULL_JOB_OWNER_ALLOWED_TOOLS_SETTING_KEY,
&serde_json::json!(["owner_gate"]),
let tools_dir = tmp.path().join("wasm-tools");
let engine = setup_owner_gate_engine(
db.clone(),
owner_gate_lightweight_trace(),
tools_dir.as_path(),
Some("default"),
true,
)
.await
.expect("set owner allowlist");
.await;
let routine = make_full_job_routine(
"inherit-owner-allowed",
FullJobPermissionMode::InheritOwner,
vec![],
);
let mut routine = make_routine("manual-owner-gate", Trigger::Manual, "Use owner_gate.");
if let RoutineAction::Lightweight { use_tools, .. } = &mut routine.action {
*use_tools = true;
}
db.create_routine(&routine).await.expect("create_routine");
let run_id = engine
@@ -1285,20 +1398,141 @@ mod tests {
}
// -----------------------------------------------------------------------
// Test: inherit_owner full_job routines stay blocked without owner allowlist
// Test: full_job cron routines use the owner's active extension tools
// -----------------------------------------------------------------------
#[tokio::test]
async fn full_job_inherit_owner_blocks_without_owner_allowlist() {
let (backend, _tmp) = create_test_backend().await;
async fn full_job_cron_routine_uses_active_owner_extension_tool() {
let (backend, tmp) = create_test_backend().await;
let db: Arc<dyn Database> = backend;
let engine = setup_owner_gate_engine(db.clone(), owner_gate_trace(false)).await;
let tools_dir = tmp.path().join("wasm-tools");
let engine = setup_owner_gate_engine(
db.clone(),
owner_gate_trace(true),
tools_dir.as_path(),
Some("default"),
true,
)
.await;
let routine = make_full_job_routine(
"inherit-owner-blocked",
FullJobPermissionMode::InheritOwner,
vec![],
let mut routine = make_full_job_routine("cron-owner-gate");
routine.trigger = Trigger::Cron {
schedule: "* * * * *".to_string(),
timezone: None,
};
routine.next_fire_at = Some(Utc::now() - chrono::Duration::minutes(1));
db.create_routine(&routine).await.expect("create_routine");
engine.check_cron_triggers().await;
let run = wait_for_any_run_completion(&db, routine.id).await;
assert_eq!(run.status, RunStatus::Ok);
assert_eq!(owner_gate_count(&db).await, 1);
}
// -----------------------------------------------------------------------
// Test: lightweight event routines use the owner's active extension tools
// -----------------------------------------------------------------------
#[tokio::test]
async fn lightweight_event_routine_uses_active_owner_extension_tool() {
let (backend, tmp) = create_test_backend().await;
let db: Arc<dyn Database> = backend;
let tools_dir = tmp.path().join("wasm-tools");
let engine = setup_owner_gate_engine(
db.clone(),
owner_gate_lightweight_trace(),
tools_dir.as_path(),
Some("default"),
true,
)
.await;
let mut routine = make_routine(
"event-owner-gate",
Trigger::Event {
channel: None,
pattern: "owner-gate".to_string(),
},
"Use owner_gate.",
);
if let RoutineAction::Lightweight { use_tools, .. } = &mut routine.action {
*use_tools = true;
}
db.create_routine(&routine).await.expect("create_routine");
engine.refresh_event_cache().await;
let fired = engine
.check_event_triggers("default", "test", "owner-gate")
.await;
assert_eq!(fired, 1, "expected one matching event routine");
let run = wait_for_any_run_completion(&db, routine.id).await;
assert_eq!(run.status, RunStatus::Ok);
assert_eq!(owner_gate_count(&db).await, 1);
}
// -----------------------------------------------------------------------
// Test: full_job system-event routines use the owner's active extension tools
// -----------------------------------------------------------------------
#[tokio::test]
async fn full_job_system_event_routine_uses_active_owner_extension_tool() {
let (backend, tmp) = create_test_backend().await;
let db: Arc<dyn Database> = backend;
let tools_dir = tmp.path().join("wasm-tools");
let engine = setup_owner_gate_engine(
db.clone(),
owner_gate_trace(true),
tools_dir.as_path(),
Some("default"),
true,
)
.await;
let mut routine = make_full_job_routine("system-owner-gate");
routine.trigger = Trigger::SystemEvent {
source: "github".to_string(),
event_type: "issue.opened".to_string(),
filters: std::collections::HashMap::new(),
};
db.create_routine(&routine).await.expect("create_routine");
engine.refresh_event_cache().await;
let fired = engine
.emit_system_event(
"github",
"issue.opened",
&serde_json::json!({"issue_number": 7}),
Some("default"),
)
.await;
assert_eq!(fired, 1, "expected one matching system_event routine");
let run = wait_for_any_run_completion(&db, routine.id).await;
assert_eq!(run.status, RunStatus::Ok);
assert_eq!(owner_gate_count(&db).await, 1);
}
// -----------------------------------------------------------------------
// Test: autonomous runs fail loudly when an extension tool is inactive
// -----------------------------------------------------------------------
#[tokio::test]
async fn full_job_blocks_without_active_owner_extension_tool() {
let (backend, tmp) = create_test_backend().await;
let db: Arc<dyn Database> = backend;
let tools_dir = tmp.path().join("wasm-tools");
let engine = setup_owner_gate_engine(
db.clone(),
owner_gate_trace(false),
tools_dir.as_path(),
Some("default"),
false,
)
.await;
let routine = make_full_job_routine("inactive-owner-gate");
db.create_routine(&routine).await.expect("create_routine");
let run_id = engine
@@ -1309,27 +1543,67 @@ mod tests {
assert_eq!(run.status, RunStatus::Failed);
assert_eq!(owner_gate_count(&db).await, 0);
let failure_reason = db
.get_agent_job_failure_reason(run.job_id.expect("linked job id"))
.await
.expect("load job failure reason")
.expect("missing job failure reason");
assert!(
failure_reason.contains("owner_gate"),
"expected missing-tool failure reason, got {failure_reason}"
);
}
// -----------------------------------------------------------------------
// Test: legacy full_job routines remain explicit until updated
// Test: extension tools activated for another owner are not inherited
// -----------------------------------------------------------------------
#[tokio::test]
async fn legacy_full_job_stays_explicit_until_updated() {
let (backend, _tmp) = create_test_backend().await;
async fn full_job_blocks_when_extension_belongs_to_another_owner() {
let (backend, tmp) = create_test_backend().await;
let db: Arc<dyn Database> = backend;
let tools_dir = tmp.path().join("wasm-tools");
let engine = setup_owner_gate_engine(
db.clone(),
owner_gate_trace(false),
tools_dir.as_path(),
Some("someone-else"),
true,
)
.await;
let routine = make_full_job_routine("other-owner-gate");
db.create_routine(&routine).await.expect("create_routine");
let run_id = engine
.fire_manual(routine.id, None)
.await
.expect("fire manual");
let run = wait_for_run_completion(&db, routine.id, run_id).await;
assert_eq!(run.status, RunStatus::Failed);
assert_eq!(owner_gate_count(&db).await, 0);
let failure_reason = db
.get_agent_job_failure_reason(run.job_id.expect("linked job id"))
.await
.expect("load job failure reason")
.expect("missing job failure reason");
assert!(
failure_reason.contains("owner_gate"),
"expected owner-mismatch failure reason, got {failure_reason}"
);
}
// -----------------------------------------------------------------------
// Test: legacy permission fields are ignored on read and removed on rewrite
// -----------------------------------------------------------------------
#[tokio::test]
async fn legacy_full_job_permission_fields_are_ignored_and_removed_on_update() {
let (backend, tmp) = create_test_backend().await;
let db: Arc<dyn Database> = backend.clone();
db.set_setting(
"default",
ironclaw::agent::routine::FULL_JOB_OWNER_ALLOWED_TOOLS_SETTING_KEY,
&serde_json::json!(["owner_gate"]),
)
.await
.expect("set owner allowlist");
let legacy_routine =
make_full_job_routine("legacy-full-job", FullJobPermissionMode::Explicit, vec![]);
let legacy_routine = make_full_job_routine("legacy-full-job");
db.create_routine(&legacy_routine)
.await
.expect("create_routine");
@@ -1342,59 +1616,77 @@ mod tests {
"title": legacy_routine.name,
"description": "Use the owner-gated tool when permitted.",
"max_iterations": 3,
"tool_permissions": [],
"tool_permissions": ["owner_gate"],
"permission_mode": "inherit_owner",
})
.to_string(),
legacy_routine.id.to_string(),
],
)
.await
.expect("strip permission_mode from action_config");
.expect("inject legacy permission fields into action_config");
let blocked_engine = setup_owner_gate_engine(db.clone(), owner_gate_trace(false)).await;
let first_run_id = blocked_engine
.fire_manual(legacy_routine.id, None)
let loaded = db
.get_routine(legacy_routine.id)
.await
.expect("fire manual legacy routine");
let first_run = wait_for_run_completion(&db, legacy_routine.id, first_run_id).await;
.expect("get_routine")
.expect("routine should still exist");
assert!(matches!(
loaded.action,
RoutineAction::FullJob {
ref title,
ref description,
max_iterations,
} if title == "legacy-full-job"
&& description == "Use the owner-gated tool when permitted."
&& max_iterations == 3
));
assert_eq!(first_run.status, RunStatus::Failed);
assert_eq!(owner_gate_count(&db).await, 0);
let update_tool = RoutineUpdateTool::new(db.clone(), blocked_engine.clone());
let tools_dir = tmp.path().join("wasm-tools");
let engine = setup_owner_gate_engine(
db.clone(),
owner_gate_trace(false),
tools_dir.as_path(),
None,
false,
)
.await;
let update_tool = RoutineUpdateTool::new(db.clone(), engine);
let update_ctx = JobContext::with_user("default", "update", "update legacy routine");
update_tool
.execute(
serde_json::json!({
"name": legacy_routine.name,
"permission_mode": "inherit_owner",
"prompt": "Updated legacy description",
}),
&update_ctx,
)
.await
.expect("routine_update should succeed");
let updated = db
.get_routine(legacy_routine.id)
let mut rows = conn
.query(
"SELECT action_config FROM routines WHERE id = ?1",
params![legacy_routine.id.to_string()],
)
.await
.expect("get_routine")
.expect("routine should still exist");
assert!(matches!(
updated.action,
RoutineAction::FullJob {
permission_mode: FullJobPermissionMode::InheritOwner,
..
}
));
let allowed_engine = setup_owner_gate_engine(db.clone(), owner_gate_trace(true)).await;
let second_run_id = allowed_engine
.fire_manual(legacy_routine.id, None)
.expect("select updated action_config");
let row = rows
.next()
.await
.expect("fire manual updated routine");
let second_run = wait_for_run_completion(&db, legacy_routine.id, second_run_id).await;
.expect("next row")
.expect("updated routine row");
let action_config_raw: String = row.get(0).expect("action_config text");
let action_config: serde_json::Value =
serde_json::from_str(&action_config_raw).expect("parse updated action_config");
assert_eq!(second_run.status, RunStatus::Ok);
assert_eq!(owner_gate_count(&db).await, 1);
assert_eq!(
action_config,
serde_json::json!({
"title": "legacy-full-job",
"description": "Updated legacy description",
"max_iterations": 3,
})
);
}
}
+8 -36
View File
@@ -15,7 +15,7 @@ mod tests {
use chrono::Utc;
use ironclaw::agent::routine::{
FullJobPermissionMode, NotifyConfig, Routine, RoutineAction, RoutineGuardrails, Trigger,
NotifyConfig, Routine, RoutineAction, RoutineGuardrails, Trigger,
};
use uuid::Uuid;
@@ -266,7 +266,7 @@ mod tests {
}
#[tokio::test]
async fn routines_detail_exposes_full_job_permission_resolution() {
async fn routines_detail_omits_legacy_full_job_permission_surface() {
let mock = MockOpenAiServerBuilder::new()
.with_default_response(MockOpenAiResponse::Text("ack".to_string()))
.start()
@@ -276,25 +276,6 @@ mod tests {
GatewayWorkflowHarness::start_openai_compatible(&mock.openai_base_url(), "mock-model")
.await;
harness
.db
.set_setting(
&harness.user_id,
ironclaw::agent::routine::FULL_JOB_OWNER_ALLOWED_TOOLS_SETTING_KEY,
&serde_json::json!(["shell", "http"]),
)
.await
.expect("set owner allowlist");
harness
.db
.set_setting(
&harness.user_id,
ironclaw::agent::routine::FULL_JOB_DEFAULT_PERMISSION_MODE_SETTING_KEY,
&serde_json::json!("copy_owner"),
)
.await
.expect("set owner default mode");
let routine = Routine {
id: Uuid::new_v4(),
name: "wf-full-job-permissions".to_string(),
@@ -306,8 +287,6 @@ mod tests {
title: "permission-detail".to_string(),
description: "Check effective permission detail".to_string(),
max_iterations: 3,
tool_permissions: vec!["message".to_string()],
permission_mode: FullJobPermissionMode::InheritOwner,
},
guardrails: RoutineGuardrails {
cooldown: Duration::from_secs(0),
@@ -346,21 +325,14 @@ mod tests {
.await
.expect("invalid detail response");
assert_eq!(
detail["full_job_permissions"]["permission_mode"].as_str(),
Some("inherit_owner")
assert!(
detail.get("full_job_permissions").is_none(),
"detail response should not expose legacy permission fields: {detail}"
);
assert_eq!(detail["action"]["type"].as_str(), Some("full_job"));
assert_eq!(
detail["full_job_permissions"]["default_permission_mode"].as_str(),
Some("copy_owner")
);
assert_eq!(
detail["full_job_permissions"]["owner_allowed_tools"],
serde_json::json!(["shell", "http"])
);
assert_eq!(
detail["full_job_permissions"]["effective_tool_permissions"],
serde_json::json!(["shell", "http", "message"])
detail["action"]["description"].as_str(),
Some("Check effective permission detail")
);
harness.shutdown().await;
+1
View File
@@ -591,6 +591,7 @@ impl TestRigBuilder {
Arc::clone(ws),
notify_tx,
None,
None,
components.tools.clone(),
components.safety.clone(),
ironclaw::agent::SandboxReadiness::Available, // tests don't use real Docker