mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
Add owner-scoped permissions for full-job routines (#1440)
* docs: add comments explaining CLI_ENABLED=false in service templates (#990) Clarify that CLI_ENABLED=false is needed in daemon mode (launchd/systemd) to prevent blocking on stdin when running as a background service. Closes #990 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * Add owner-scoped full-job routine permissions * Address PR review feedback * Fix owner gate test timing --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c4ab382522
commit
cac6f4013c
+363
-24
@@ -19,7 +19,9 @@ use serde_json::{Map, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::agent::routine::{
|
||||
NotifyConfig, Routine, RoutineAction, RoutineGuardrails, Trigger, next_cron_fire,
|
||||
FullJobPermissionDefaultMode, FullJobPermissionMode, NotifyConfig, Routine, RoutineAction,
|
||||
RoutineGuardrails, Trigger, load_full_job_permission_settings, next_cron_fire,
|
||||
normalize_tool_names,
|
||||
};
|
||||
use crate::agent::routine_engine::RoutineEngine;
|
||||
use crate::context::JobContext;
|
||||
@@ -54,6 +56,13 @@ enum NormalizedExecutionMode {
|
||||
FullJob,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum RequestedFullJobPermissionMode {
|
||||
Explicit,
|
||||
InheritOwner,
|
||||
CopyOwner,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct NormalizedExecutionRequest {
|
||||
mode: NormalizedExecutionMode,
|
||||
@@ -61,6 +70,7 @@ struct NormalizedExecutionRequest {
|
||||
use_tools: bool,
|
||||
max_tool_rounds: u32,
|
||||
tool_permissions: Vec<String>,
|
||||
permission_mode: Option<RequestedFullJobPermissionMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -149,6 +159,11 @@ fn execution_properties() -> Value {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Only applies when execution.mode='full_job'. These tools are pre-authorized for Always-approval checks."
|
||||
},
|
||||
"permission_mode": {
|
||||
"type": "string",
|
||||
"enum": ["inherit_owner", "explicit", "copy_owner"],
|
||||
"description": "Only applies when execution.mode='full_job'. 'inherit_owner' uses the owner defaults at run time, 'explicit' uses only tool_permissions, and 'copy_owner' snapshots the current owner allowlist into tool_permissions."
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -321,7 +336,7 @@ fn lightweight_execution_variant() -> Value {
|
||||
fn full_job_execution_variant() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"description": "Full-job execution. Uses tool_permissions and ignores lightweight-only fields such as use_tools, max_tool_rounds, and context_paths.",
|
||||
"description": "Full-job execution. Uses owner-scoped permission defaults plus tool_permissions and ignores lightweight-only fields such as use_tools, max_tool_rounds, and context_paths.",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
@@ -332,6 +347,11 @@ fn full_job_execution_variant() -> Value {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Tools pre-authorized for Always-approval checks."
|
||||
},
|
||||
"permission_mode": {
|
||||
"type": "string",
|
||||
"enum": ["inherit_owner", "explicit", "copy_owner"],
|
||||
"description": "When omitted, new routines use the owner default. 'copy_owner' snapshots the current owner allowlist into this routine."
|
||||
}
|
||||
},
|
||||
"required": ["mode"]
|
||||
@@ -349,7 +369,7 @@ fn execution_discovery_schema() -> Value {
|
||||
],
|
||||
"examples": [
|
||||
{ "mode": "lightweight", "use_tools": true, "max_tool_rounds": 3 },
|
||||
{ "mode": "full_job", "tool_permissions": ["message", "http"] }
|
||||
{ "mode": "full_job", "permission_mode": "inherit_owner", "tool_permissions": ["message", "http"] }
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -399,6 +419,7 @@ fn routine_create_examples() -> Vec<Value> {
|
||||
},
|
||||
"execution": {
|
||||
"mode": "full_job",
|
||||
"permission_mode": "inherit_owner",
|
||||
"tool_permissions": ["message"]
|
||||
}
|
||||
}),
|
||||
@@ -412,7 +433,7 @@ fn routine_create_tool_summary() -> ToolDiscoverySummary {
|
||||
"request.kind='cron' requires request.schedule.".into(),
|
||||
"request.kind='message_event' requires request.pattern.".into(),
|
||||
"request.kind='system_event' requires request.source and request.event_type.".into(),
|
||||
"execution.mode='full_job' uses tool_permissions and ignores use_tools, max_tool_rounds, and context_paths.".into(),
|
||||
"execution.mode='full_job' uses permission_mode and tool_permissions, and ignores use_tools, max_tool_rounds, and context_paths.".into(),
|
||||
],
|
||||
notes: vec![
|
||||
"Omitting execution defaults to lightweight mode.".into(),
|
||||
@@ -577,6 +598,14 @@ fn routine_create_schema(include_compatibility_aliases: bool) -> Value {
|
||||
"description": "Compatibility alias for execution.tool_permissions."
|
||||
}),
|
||||
);
|
||||
properties.insert(
|
||||
"permission_mode".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "string",
|
||||
"enum": ["inherit_owner", "explicit", "copy_owner"],
|
||||
"description": "Compatibility alias for execution.permission_mode."
|
||||
}),
|
||||
);
|
||||
properties.insert(
|
||||
"notify_channel".to_string(),
|
||||
serde_json::json!({
|
||||
@@ -655,6 +684,16 @@ pub(crate) fn routine_update_parameters_schema() -> Value {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "New description"
|
||||
},
|
||||
"tool_permissions": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Updated Always-approval tool allowlist for full_job routines only."
|
||||
},
|
||||
"permission_mode": {
|
||||
"type": "string",
|
||||
"enum": ["inherit_owner", "explicit", "copy_owner"],
|
||||
"description": "Updated permission mode for full_job routines only. 'copy_owner' snapshots the current owner allowlist into the routine and persists as explicit."
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
@@ -700,6 +739,27 @@ fn u64_field(params: &Value, group: &str, field: &str, aliases: &[&str]) -> Opti
|
||||
}
|
||||
|
||||
fn string_array_field(params: &Value, group: &str, field: &str, aliases: &[&str]) -> Vec<String> {
|
||||
normalize_tool_names(
|
||||
nested_object(params, group)
|
||||
.and_then(|obj| obj.get(field))
|
||||
.and_then(Value::as_array)
|
||||
.or_else(|| {
|
||||
aliases
|
||||
.iter()
|
||||
.find_map(|alias| params.get(*alias).and_then(Value::as_array))
|
||||
})
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|value| value.as_str().map(String::from)),
|
||||
)
|
||||
}
|
||||
|
||||
fn optional_string_array_field(
|
||||
params: &Value,
|
||||
group: &str,
|
||||
field: &str,
|
||||
aliases: &[&str],
|
||||
) -> Option<Vec<String>> {
|
||||
nested_object(params, group)
|
||||
.and_then(|obj| obj.get(field))
|
||||
.and_then(Value::as_array)
|
||||
@@ -709,11 +769,11 @@ fn string_array_field(params: &Value, group: &str, field: &str, aliases: &[&str]
|
||||
.find_map(|alias| params.get(*alias).and_then(Value::as_array))
|
||||
})
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|value| value.as_str().map(String::from))
|
||||
.collect()
|
||||
normalize_tool_names(
|
||||
arr.iter()
|
||||
.filter_map(|value| value.as_str().map(String::from)),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn object_field(
|
||||
@@ -852,6 +912,20 @@ fn parse_execution_mode(value: Option<String>) -> Result<NormalizedExecutionMode
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_requested_full_job_permission_mode(
|
||||
value: Option<String>,
|
||||
) -> Result<Option<RequestedFullJobPermissionMode>, ToolError> {
|
||||
match value.as_deref() {
|
||||
None => Ok(None),
|
||||
Some("explicit") => Ok(Some(RequestedFullJobPermissionMode::Explicit)),
|
||||
Some("inherit_owner") => Ok(Some(RequestedFullJobPermissionMode::InheritOwner)),
|
||||
Some("copy_owner") => Ok(Some(RequestedFullJobPermissionMode::CopyOwner)),
|
||||
Some(other) => Err(ToolError::InvalidParameters(format!(
|
||||
"unknown full_job permission_mode: {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_routine_execution(params: &Value) -> Result<NormalizedExecutionRequest, ToolError> {
|
||||
let mode = parse_execution_mode(string_field(params, "execution", "mode", &["action_type"]))?;
|
||||
let context_paths =
|
||||
@@ -867,6 +941,12 @@ fn parse_routine_execution(params: &Value) -> Result<NormalizedExecutionRequest,
|
||||
"tool_permissions",
|
||||
&["tool_permissions"],
|
||||
);
|
||||
let permission_mode = parse_requested_full_job_permission_mode(string_field(
|
||||
params,
|
||||
"execution",
|
||||
"permission_mode",
|
||||
&["permission_mode"],
|
||||
))?;
|
||||
|
||||
Ok(NormalizedExecutionRequest {
|
||||
mode,
|
||||
@@ -874,6 +954,7 @@ fn parse_routine_execution(params: &Value) -> Result<NormalizedExecutionRequest,
|
||||
use_tools,
|
||||
max_tool_rounds,
|
||||
tool_permissions,
|
||||
permission_mode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -934,28 +1015,106 @@ fn build_routine_trigger(trigger: &NormalizedTriggerRequest) -> Trigger {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_routine_action(
|
||||
async fn build_routine_action(
|
||||
store: &dyn Database,
|
||||
user_id: &str,
|
||||
name: &str,
|
||||
prompt: &str,
|
||||
execution: &NormalizedExecutionRequest,
|
||||
) -> RoutineAction {
|
||||
) -> Result<RoutineAction, ToolError> {
|
||||
match execution.mode {
|
||||
NormalizedExecutionMode::Lightweight => RoutineAction::Lightweight {
|
||||
NormalizedExecutionMode::Lightweight => Ok(RoutineAction::Lightweight {
|
||||
prompt: prompt.to_string(),
|
||||
context_paths: execution.context_paths.clone(),
|
||||
max_tokens: 4096,
|
||||
use_tools: execution.use_tools,
|
||||
max_tool_rounds: execution.max_tool_rounds,
|
||||
},
|
||||
NormalizedExecutionMode::FullJob => RoutineAction::FullJob {
|
||||
title: name.to_string(),
|
||||
description: prompt.to_string(),
|
||||
max_iterations: 10,
|
||||
tool_permissions: execution.tool_permissions.clone(),
|
||||
},
|
||||
}),
|
||||
NormalizedExecutionMode::FullJob => {
|
||||
let mut owner_settings = None;
|
||||
let requested_mode = match execution.permission_mode {
|
||||
Some(mode) => mode,
|
||||
None => {
|
||||
let settings = load_full_job_permission_settings(store, user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ToolError::ExecutionFailed(format!(
|
||||
"failed to load routine permission settings: {e}"
|
||||
))
|
||||
})?;
|
||||
let mode = match settings.default_mode {
|
||||
FullJobPermissionDefaultMode::Explicit => {
|
||||
RequestedFullJobPermissionMode::Explicit
|
||||
}
|
||||
FullJobPermissionDefaultMode::InheritOwner => {
|
||||
RequestedFullJobPermissionMode::InheritOwner
|
||||
}
|
||||
FullJobPermissionDefaultMode::CopyOwner => {
|
||||
RequestedFullJobPermissionMode::CopyOwner
|
||||
}
|
||||
};
|
||||
owner_settings = Some(settings);
|
||||
mode
|
||||
}
|
||||
};
|
||||
let (permission_mode, tool_permissions) = match requested_mode {
|
||||
RequestedFullJobPermissionMode::Explicit => (
|
||||
FullJobPermissionMode::Explicit,
|
||||
execution.tool_permissions.clone(),
|
||||
),
|
||||
RequestedFullJobPermissionMode::InheritOwner => (
|
||||
FullJobPermissionMode::InheritOwner,
|
||||
execution.tool_permissions.clone(),
|
||||
),
|
||||
RequestedFullJobPermissionMode::CopyOwner => {
|
||||
let owner_allowed_tools = match owner_settings {
|
||||
Some(settings) => settings.owner_allowed_tools,
|
||||
None => {
|
||||
load_full_job_permission_settings(store, user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ToolError::ExecutionFailed(format!(
|
||||
"failed to load routine permission settings: {e}"
|
||||
))
|
||||
})?
|
||||
.owner_allowed_tools
|
||||
}
|
||||
};
|
||||
(
|
||||
FullJobPermissionMode::Explicit,
|
||||
normalize_tool_names(
|
||||
owner_allowed_tools
|
||||
.into_iter()
|
||||
.chain(execution.tool_permissions.iter().cloned()),
|
||||
),
|
||||
)
|
||||
}
|
||||
};
|
||||
Ok(RoutineAction::FullJob {
|
||||
title: name.to_string(),
|
||||
description: prompt.to_string(),
|
||||
max_iterations: 10,
|
||||
tool_permissions,
|
||||
permission_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn routine_requests_full_job(params: &Value) -> bool {
|
||||
matches!(
|
||||
string_field(params, "execution", "mode", &["action_type"]).as_deref(),
|
||||
Some("full_job")
|
||||
)
|
||||
}
|
||||
|
||||
fn routine_permission_fields_present(params: &Value) -> bool {
|
||||
nested_object(params, "execution").is_some_and(|execution| {
|
||||
execution.contains_key("tool_permissions") || execution.contains_key("permission_mode")
|
||||
}) || params.get("tool_permissions").is_some()
|
||||
|| params.get("permission_mode").is_some()
|
||||
}
|
||||
|
||||
fn event_emit_schema(include_source_alias: bool) -> Value {
|
||||
let mut schema = serde_json::json!({
|
||||
"type": "object",
|
||||
@@ -1054,6 +1213,14 @@ impl Tool for RoutineCreateTool {
|
||||
Use this when the user wants something to happen periodically or reactively."
|
||||
}
|
||||
|
||||
fn requires_approval(&self, params: &serde_json::Value) -> ApprovalRequirement {
|
||||
if routine_requests_full_job(params) {
|
||||
ApprovalRequirement::UnlessAutoApproved
|
||||
} else {
|
||||
ApprovalRequirement::Never
|
||||
}
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
routine_create_parameters_schema()
|
||||
}
|
||||
@@ -1074,8 +1241,14 @@ impl Tool for RoutineCreateTool {
|
||||
let start = std::time::Instant::now();
|
||||
let normalized = parse_routine_create_request(¶ms)?;
|
||||
let trigger = build_routine_trigger(&normalized.trigger);
|
||||
let action =
|
||||
build_routine_action(&normalized.name, &normalized.prompt, &normalized.execution);
|
||||
let action = build_routine_action(
|
||||
self.store.as_ref(),
|
||||
&ctx.user_id,
|
||||
&normalized.name,
|
||||
&normalized.prompt,
|
||||
&normalized.execution,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Compute next fire time for cron
|
||||
let next_fire = if let Trigger::Cron {
|
||||
@@ -1238,14 +1411,23 @@ impl Tool for RoutineUpdateTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Update an existing routine. Can change prompt, description, enabled state, or cron schedule/timezone. \
|
||||
Pass the routine name and only the fields you want to change. This does not convert trigger types."
|
||||
"Update an existing routine. Can change prompt, description, enabled state, cron schedule/timezone, \
|
||||
or full_job permission settings. Pass the routine name and only the fields you want to change. \
|
||||
This does not convert trigger types."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
routine_update_parameters_schema()
|
||||
}
|
||||
|
||||
fn requires_approval(&self, params: &serde_json::Value) -> ApprovalRequirement {
|
||||
if routine_permission_fields_present(params) {
|
||||
ApprovalRequirement::UnlessAutoApproved
|
||||
} else {
|
||||
ApprovalRequirement::Never
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
@@ -1278,6 +1460,72 @@ impl Tool for RoutineUpdateTool {
|
||||
}
|
||||
}
|
||||
|
||||
let requested_permission_mode = parse_requested_full_job_permission_mode(string_field(
|
||||
¶ms,
|
||||
"execution",
|
||||
"permission_mode",
|
||||
&["permission_mode"],
|
||||
))?;
|
||||
let requested_tool_permissions = optional_string_array_field(
|
||||
¶ms,
|
||||
"execution",
|
||||
"tool_permissions",
|
||||
&["tool_permissions"],
|
||||
);
|
||||
let updates_permissions =
|
||||
requested_permission_mode.is_some() || requested_tool_permissions.is_some();
|
||||
|
||||
if updates_permissions {
|
||||
match &mut routine.action {
|
||||
RoutineAction::FullJob {
|
||||
tool_permissions,
|
||||
permission_mode,
|
||||
..
|
||||
} => {
|
||||
let next_tool_permissions =
|
||||
requested_tool_permissions.unwrap_or_else(|| tool_permissions.clone());
|
||||
match requested_permission_mode {
|
||||
Some(RequestedFullJobPermissionMode::Explicit) => {
|
||||
*permission_mode = FullJobPermissionMode::Explicit;
|
||||
*tool_permissions = next_tool_permissions;
|
||||
}
|
||||
Some(RequestedFullJobPermissionMode::InheritOwner) => {
|
||||
*permission_mode = FullJobPermissionMode::InheritOwner;
|
||||
*tool_permissions = next_tool_permissions;
|
||||
}
|
||||
Some(RequestedFullJobPermissionMode::CopyOwner) => {
|
||||
let owner_settings = load_full_job_permission_settings(
|
||||
self.store.as_ref(),
|
||||
&ctx.user_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ToolError::ExecutionFailed(format!(
|
||||
"failed to load routine permission settings: {e}"
|
||||
))
|
||||
})?;
|
||||
*permission_mode = FullJobPermissionMode::Explicit;
|
||||
*tool_permissions = normalize_tool_names(
|
||||
owner_settings
|
||||
.owner_allowed_tools
|
||||
.into_iter()
|
||||
.chain(next_tool_permissions),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
*tool_permissions = next_tool_permissions;
|
||||
}
|
||||
}
|
||||
}
|
||||
RoutineAction::Lightweight { .. } => {
|
||||
return Err(ToolError::InvalidParameters(
|
||||
"permission_mode and tool_permissions can only be updated for full_job routines"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate timezone param if provided
|
||||
let new_timezone = params
|
||||
.get("timezone")
|
||||
@@ -1686,6 +1934,7 @@ mod tests {
|
||||
"use_tools",
|
||||
"max_tool_rounds",
|
||||
"tool_permissions",
|
||||
"permission_mode",
|
||||
"notify_channel",
|
||||
"notify_user",
|
||||
"cooldown_secs",
|
||||
@@ -1814,6 +2063,7 @@ mod tests {
|
||||
parsed.execution.tool_permissions,
|
||||
vec!["message".to_string(), "http".to_string()],
|
||||
);
|
||||
assert_eq!(parsed.execution.permission_mode, None);
|
||||
assert_eq!(parsed.delivery.channel.as_deref(), Some("telegram"));
|
||||
assert_eq!(parsed.delivery.user.as_deref(), Some("ops-team"));
|
||||
assert_eq!(parsed.cooldown_secs, 30);
|
||||
@@ -2143,8 +2393,9 @@ mod tests {
|
||||
.and_then(Value::as_object)
|
||||
.expect("full_job properties");
|
||||
assert!(
|
||||
full_job_props.contains_key("tool_permissions"),
|
||||
"full_job variant should expose tool_permissions",
|
||||
full_job_props.contains_key("tool_permissions")
|
||||
&& full_job_props.contains_key("permission_mode"),
|
||||
"full_job variant should expose permission fields",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2249,6 +2500,8 @@ mod tests {
|
||||
"schedule",
|
||||
"timezone",
|
||||
"description",
|
||||
"tool_permissions",
|
||||
"permission_mode",
|
||||
] {
|
||||
let _ = schema_property(&schema, field);
|
||||
}
|
||||
@@ -2272,6 +2525,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routine_create_detects_full_job_requests_for_approval() {
|
||||
let full_job = serde_json::json!({
|
||||
"name": "approve-me",
|
||||
"prompt": "Run autonomously",
|
||||
"request": { "kind": "manual" },
|
||||
"execution": { "mode": "full_job" }
|
||||
});
|
||||
let lightweight = serde_json::json!({
|
||||
"name": "safe",
|
||||
"prompt": "Stay lightweight",
|
||||
"request": { "kind": "manual" }
|
||||
});
|
||||
|
||||
assert!(routine_requests_full_job(&full_job));
|
||||
assert!(!routine_requests_full_job(&lightweight));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_emit_parameters_schema_prefers_canonical_event_source() {
|
||||
let schema = event_emit_parameters_schema();
|
||||
@@ -2312,4 +2583,72 @@ mod tests {
|
||||
"event_emit discovery schema should keep source alias",
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "libsql")]
|
||||
#[tokio::test]
|
||||
async fn build_full_job_action_defaults_to_inherit_owner_for_new_routines() {
|
||||
let (db, _tmp) = crate::testing::test_db().await;
|
||||
let execution = NormalizedExecutionRequest {
|
||||
mode: NormalizedExecutionMode::FullJob,
|
||||
context_paths: Vec::new(),
|
||||
use_tools: false,
|
||||
max_tool_rounds: 3,
|
||||
tool_permissions: vec!["shell".to_string()],
|
||||
permission_mode: None,
|
||||
};
|
||||
|
||||
let action =
|
||||
build_routine_action(db.as_ref(), "default", "issue-1316", "Run it", &execution)
|
||||
.await
|
||||
.expect("build action");
|
||||
|
||||
assert!(matches!(
|
||||
action,
|
||||
RoutineAction::FullJob {
|
||||
permission_mode: FullJobPermissionMode::InheritOwner,
|
||||
tool_permissions,
|
||||
..
|
||||
} if tool_permissions == vec!["shell".to_string()]
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "libsql")]
|
||||
#[tokio::test]
|
||||
async fn build_full_job_action_copy_owner_snapshots_allowlist() {
|
||||
let (db, _tmp) = crate::testing::test_db().await;
|
||||
db.set_setting(
|
||||
"default",
|
||||
crate::agent::routine::FULL_JOB_OWNER_ALLOWED_TOOLS_SETTING_KEY,
|
||||
&serde_json::json!(["http", "shell"]),
|
||||
)
|
||||
.await
|
||||
.expect("set owner allowlist");
|
||||
let execution = NormalizedExecutionRequest {
|
||||
mode: NormalizedExecutionMode::FullJob,
|
||||
context_paths: Vec::new(),
|
||||
use_tools: false,
|
||||
max_tool_rounds: 3,
|
||||
tool_permissions: vec!["message".to_string(), "shell".to_string()],
|
||||
permission_mode: Some(RequestedFullJobPermissionMode::CopyOwner),
|
||||
};
|
||||
|
||||
let action =
|
||||
build_routine_action(db.as_ref(), "default", "issue-1316", "Run it", &execution)
|
||||
.await
|
||||
.expect("build action");
|
||||
|
||||
assert!(matches!(
|
||||
action,
|
||||
RoutineAction::FullJob {
|
||||
permission_mode: FullJobPermissionMode::Explicit,
|
||||
tool_permissions,
|
||||
..
|
||||
} if tool_permissions
|
||||
== vec![
|
||||
"http".to_string(),
|
||||
"shell".to_string(),
|
||||
"message".to_string(),
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user