Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,14 +326,16 @@ impl AppBuilder {

// Initialize tool registry with credential injection support
let credential_registry = Arc::new(SharedCredentialRegistry::new());
let tools = if let Some(ref ss) = self.secrets_store {
Arc::new(
ToolRegistry::new()
.with_credentials(Arc::clone(&credential_registry), Arc::clone(ss)),
)
let engine_version = if crate::bridge::is_engine_v2_enabled() {
crate::tools::EngineVersion::V2
} else {
Arc::new(ToolRegistry::new())
crate::tools::EngineVersion::V1
};
let mut registry = ToolRegistry::new().with_engine_version(engine_version);
if let Some(ref ss) = self.secrets_store {
registry = registry.with_credentials(Arc::clone(&credential_registry), Arc::clone(ss));
}
let tools = Arc::new(registry);
tools.register_builtin_tools();
tools.register_tool_info();

Expand Down
145 changes: 31 additions & 114 deletions src/bridge/effect_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,27 +436,20 @@ impl EffectBridgeAdapter {
});
}

if is_v1_only_tool(lookup_name) {
return Err(EngineError::Effect {
reason: format!(
"Tool '{}' is not available in engine v2. \
Tell the user to use the slash command instead (e.g. /routine, /job).",
action_name
),
});
}

if is_v1_auth_tool(lookup_name) {
return Err(EngineError::Effect {
reason: format!(
"Tool '{}' is not available in engine v2. \
Authentication is handled automatically by the kernel.",
action_name
),
});
}

if let Some((_, tool)) = self.tools.get_resolved(action_name).await {
// Defense-in-depth: reject V1Only tools even if they somehow got
// a lease (e.g. via a stale capability registry or hallucination).
if tool.engine_compatibility() == crate::tools::EngineCompatibility::V1Only {
return Err(EngineError::Effect {
reason: format!(
"Tool '{}' is v1-only and not available in engine v2. \
Use the equivalent v2 workflow (e.g. mission_create instead of \
routine_create) or the appropriate slash command.",
action_name
),
});
}
Comment thread
henrypark133 marked this conversation as resolved.

let requirement = tool.requires_approval(&parameters);
match requirement {
ApprovalRequirement::Always => {
Expand Down Expand Up @@ -755,34 +748,24 @@ impl EffectExecutor for EffectBridgeAdapter {
) -> Result<Vec<ActionDef>, EngineError> {
let tool_defs = self.tools.tool_definitions().await;

// Build action defs, excluding v1-only tools and v1 auth tools
let mut actions = Vec::with_capacity(tool_defs.len());
for td in tool_defs {
// Skip tools that can't work in engine v2
if is_v1_only_tool(&td.name) {
continue;
}

// Skip v1 auth management tools — auth is kernel-level in v2
if is_v1_auth_tool(&td.name) {
continue;
}

let python_name = td.name.replace('-', "_");

actions.push(ActionDef {
name: python_name,
description: td.description,
parameters_schema: td.parameters,
effects: vec![],
// Approval is enforced at execute-time inside this adapter so
// thread-scoped one-shot approvals and auth-aware bypasses can
// participate. Advertising approval here would cause the engine
// policy preflight to interrupt before the adapter can apply
// those runtime checks.
requires_approval: false,
});
}
let actions = tool_defs
.into_iter()
.map(|td| {
let python_name = td.name.replace('-', "_");
ActionDef {
name: python_name,
description: td.description,
parameters_schema: td.parameters,
effects: vec![],
// Approval is enforced at execute-time inside this adapter so
// thread-scoped one-shot approvals and auth-aware bypasses can
// participate. Advertising approval here would cause the engine
// policy preflight to interrupt before the adapter can apply
// those runtime checks.
requires_approval: false,
}
})
.collect();

Ok(actions)
}
Expand Down Expand Up @@ -841,31 +824,6 @@ fn extract_credential_name(error_msg: &str) -> Option<String> {
None
}

fn is_v1_only_tool(name: &str) -> bool {
matches!(
name,
"create_job"
| "create-job"
| "cancel_job"
| "cancel-job"
| "build_software"
| "build-software"
| "routine_create"
| "routine_list"
| "routine_fire"
| "routine_pause"
| "routine_resume"
| "routine_update"
| "routine_delete"
)
}

/// Auth management tools from v1 that are now kernel-internal in v2.
/// The LLM should not see or call these — auth is handled automatically.
fn is_v1_auth_tool(name: &str) -> bool {
matches!(name, "tool_auth" | "tool-auth")
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -1213,47 +1171,6 @@ mod tests {
assert_eq!(extract_credential_name(msg), None);
}

// ── is_v1_only_tool tests ──────────────────────────────────

#[test]
fn routine_tools_are_v1_only() {
assert!(is_v1_only_tool("routine_create"));
assert!(is_v1_only_tool("routine_list"));
assert!(is_v1_only_tool("routine_fire"));
assert!(is_v1_only_tool("routine_delete"));
assert!(is_v1_only_tool("routine_pause"));
assert!(is_v1_only_tool("routine_resume"));
assert!(is_v1_only_tool("routine_update"));
}

#[test]
fn mission_tools_are_not_v1_only() {
assert!(!is_v1_only_tool("mission_create"));
assert!(!is_v1_only_tool("mission_list"));
assert!(!is_v1_only_tool("mission_fire"));
assert!(!is_v1_only_tool("http"));
assert!(!is_v1_only_tool("web_search"));
}

// ── is_v1_auth_tool tests ─────────────────────────────────

#[test]
fn auth_tools_are_v1_auth() {
assert!(is_v1_auth_tool("tool_auth"));
assert!(is_v1_auth_tool("tool-auth"));
assert!(!is_v1_auth_tool("tool_activate"));
assert!(!is_v1_auth_tool("tool-activate"));
}

#[test]
fn non_auth_tools_are_not_v1_auth() {
assert!(!is_v1_auth_tool("tool_install"));
assert!(!is_v1_auth_tool("tool-install"));
assert!(!is_v1_auth_tool("http"));
assert!(!is_v1_auth_tool("tool_search"));
assert!(!is_v1_auth_tool("tool_list"));
}

// ── Pre-flight auth gate integration test ─────────────────

#[tokio::test]
Expand Down
2 changes: 1 addition & 1 deletion src/bridge/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ pub async fn init_engine(agent: &Agent) -> Result<(), Error> {
// Generate the engine workspace README
store.generate_engine_readme().await;

// Build capability registry from available tools
// Build capability registry from available tools (auto-filtered by engine version)
let mut capabilities = CapabilityRegistry::new();
let tool_defs = agent.tools().tool_definitions().await;
if !tool_defs.is_empty() {
Expand Down
7 changes: 6 additions & 1 deletion src/tools/builder/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ use crate::llm::{
ChatMessage, LlmProvider, Reasoning, ReasoningContext, RespondResult, ToolDefinition,
};
use crate::tools::tool::{
ApprovalContext, ApprovalRequirement, Tool, ToolError, ToolOutput, check_approval_in_context,
ApprovalContext, ApprovalRequirement, EngineCompatibility, Tool, ToolError, ToolOutput,
check_approval_in_context,
};
use crate::tools::{ToolRegistry, prepare_tool_params};

Expand Down Expand Up @@ -1114,6 +1115,10 @@ impl Tool for BuildSoftwareTool {
fn requires_approval(&self, _params: &serde_json::Value) -> ApprovalRequirement {
ApprovalRequirement::UnlessAutoApproved
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

#[cfg(test)]
Expand Down
16 changes: 15 additions & 1 deletion src/tools/builtin/extension_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use crate::context::JobContext;
use crate::extensions::{ExtensionKind, ExtensionManager};
use crate::tools::permissions::{TOOL_RISK_DEFAULTS, effective_permission};
use crate::tools::registry::ToolRegistry;
use crate::tools::tool::{ApprovalRequirement, Tool, ToolError, ToolOutput, require_str};
use crate::tools::tool::{
ApprovalRequirement, EngineCompatibility, Tool, ToolError, ToolOutput, require_str,
};

fn activation_error_requires_auth(err: &str) -> bool {
let err_lower = err.to_ascii_lowercase();
Expand Down Expand Up @@ -278,6 +280,10 @@ impl Tool for ToolAuthTool {
ApprovalRequirement::UnlessAutoApproved
}
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

// ── tool_activate ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -591,6 +597,10 @@ impl Tool for ToolRemoveTool {
fn requires_approval(&self, _params: &serde_json::Value) -> ApprovalRequirement {
ApprovalRequirement::Always
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

// ── tool_upgrade ─────────────────────────────────────────────────────
Expand Down Expand Up @@ -872,6 +882,10 @@ impl Tool for ToolPermissionSetTool {
});
Ok(ToolOutput::success(output, start.elapsed()))
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

#[cfg(test)]
Expand Down
12 changes: 11 additions & 1 deletion src/tools/builtin/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ use crate::history::SandboxJobRecord;
use crate::orchestrator::auth::CredentialGrant;
use crate::orchestrator::job_manager::{ContainerJobManager, JobCreationParams, JobMode};
use crate::secrets::SecretsStore;
use crate::tools::tool::{ApprovalRequirement, Tool, ToolError, ToolOutput, require_str};
use crate::tools::tool::{
ApprovalRequirement, EngineCompatibility, Tool, ToolError, ToolOutput, require_str,
};
use ironclaw_common::AppEvent;

/// Lazy scheduler reference, filled after Agent::new creates the Scheduler.
Expand Down Expand Up @@ -1064,6 +1066,10 @@ impl Tool for CreateJobTool {
fn requires_sanitization(&self) -> bool {
false
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

/// Tool for listing jobs.
Expand Down Expand Up @@ -1381,6 +1387,10 @@ impl Tool for CancelJobTool {
fn requires_sanitization(&self) -> bool {
false
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

/// Tool for reading sandbox job event logs.
Expand Down
35 changes: 34 additions & 1 deletion src/tools/builtin/routine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ use crate::agent::routine_engine::RoutineEngine;
use crate::context::JobContext;
use crate::db::Database;
use crate::tools::tool::{
ApprovalRequirement, Tool, ToolDiscoverySummary, ToolError, ToolOutput, require_str,
ApprovalRequirement, EngineCompatibility, Tool, ToolDiscoverySummary, ToolError, ToolOutput,
require_str,
};

// ==================== routine_create ====================
Expand Down Expand Up @@ -1238,6 +1239,10 @@ impl Tool for RoutineCreateTool {
fn requires_sanitization(&self) -> bool {
false
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

// ==================== routine_list ====================
Expand Down Expand Up @@ -1328,6 +1333,10 @@ impl Tool for RoutineListTool {
fn requires_sanitization(&self) -> bool {
false
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

// ==================== routine_update ====================
Expand Down Expand Up @@ -1491,6 +1500,10 @@ impl Tool for RoutineUpdateTool {
fn requires_sanitization(&self) -> bool {
false
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

// ==================== routine_delete ====================
Expand Down Expand Up @@ -1578,6 +1591,10 @@ impl Tool for RoutineDeleteTool {
fn requires_sanitization(&self) -> bool {
false
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

// ==================== routine_fire ====================
Expand Down Expand Up @@ -1659,6 +1676,10 @@ impl Tool for RoutineFireTool {
fn requires_sanitization(&self) -> bool {
false
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

// ==================== routine_history ====================
Expand Down Expand Up @@ -1798,6 +1819,10 @@ impl Tool for RoutineHistoryTool {
fn requires_sanitization(&self) -> bool {
false
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

// ==================== event_emit ====================
Expand Down Expand Up @@ -1863,6 +1888,10 @@ impl Tool for EventEmitTool {
fn requires_sanitization(&self) -> bool {
true
}

fn engine_compatibility(&self) -> EngineCompatibility {
EngineCompatibility::V1Only
}
}

#[cfg(test)]
Expand Down Expand Up @@ -2673,4 +2702,8 @@ mod tests {
&& max_iterations == 25
));
}

// Engine compatibility for routine tools is verified at the registry level
// via `tool_definitions_for_engine_excludes_v1_only_from_v2`. Each tool's
// `engine_compatibility()` returns `V1Only` — see the impl blocks above.
}
Loading
Loading