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
20 changes: 10 additions & 10 deletions scripts/file-size-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
# must be split instead. Raising an existing ceiling is allowed but is
# never silent — it lands as a visible diff here, to be justified in
# review like any other change.
1715 stella-cli/src/agent_tests.rs
2191 stella-cli/src/agent.rs
1599 stella-cli/src/candidate_ws.rs
4465 stella-cli/src/command_deck.rs
1872 stella-cli/src/agent_tests.rs
2224 stella-cli/src/agent.rs
1623 stella-cli/src/candidate_ws.rs
4592 stella-cli/src/command_deck.rs
1503 stella-cli/src/contextgraph.rs
1776 stella-cli/src/main.rs
1782 stella-cli/src/main.rs
1641 stella-cli/src/memory.rs
2224 stella-context/src/store.rs
2095 stella-core/src/bus.rs
Expand All @@ -31,10 +31,10 @@
2268 stella-store/src/lib.rs
2168 stella-store/src/tests.rs
1884 stella-store/src/usage.rs
1558 stella-tools/src/media.rs
3052 stella-tools/src/registry.rs
1556 stella-tools/src/media.rs
3014 stella-tools/src/registry.rs
1797 stella-tools/src/scripts.rs
1670 stella-tui/src/deck_render.rs
3940 stella-tui/src/deck_ui.rs
1671 stella-tui/src/deck_render.rs
3965 stella-tui/src/deck_ui.rs
1609 stella-tui/src/model.rs
1650 stella-tui/src/views/engine.rs
1652 stella-tui/src/views/engine.rs
85 changes: 59 additions & 26 deletions stella-cli/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ pub(crate) use persistence::{
persist_event, record_execution_end, spawn_renderer, warn_store_write_failed,
};
pub(crate) use prompt::*;
// `tool_policy` is a top-level module (`main.rs`); the re-export keeps every
// session driver's `agent::PolicyToolSet` reading as "the agent's tool stack".
pub(crate) use crate::tool_policy::PolicyToolSet;
pub(crate) use tools::*;

/// Construct the native tool registry without consulting optional host/user backends when the
Expand Down Expand Up @@ -316,11 +319,14 @@ async fn run_pipeline_one_shot(
Some(skills) => interactive.with_skill_registry(skills),
None => interactive,
};
// The operator's switches, applied over the complete surface — MCP and
// custom tools included — and BELOW discovery, so `tool_search` cannot
// advertise something the policy withholds.
let permitted = PolicyToolSet::new(&interactive, session_tool_policy(cfg));
// Outermost: the discovery layer (tool_search/skill_search/mcp_search)
// must see the complete advertised catalog below it.
let tools =
crate::discovery::DiscoveryToolSet::new(&interactive, cfg.workspace_root.clone())
.with_project_prompts_allowed(cfg.authority.project_prompts_allowed);
let tools = crate::discovery::DiscoveryToolSet::new(&permitted, cfg.workspace_root.clone())
.with_project_prompts_allowed(cfg.authority.project_prompts_allowed);

let ws_ports = workspace_ports(
cfg.workspace_root.clone(),
Expand Down Expand Up @@ -1555,23 +1561,29 @@ pub(crate) async fn discover_custom_tools(
/// any discovery diagnostics for broken manifests. MCP-server tools
/// (.stella/mcp.toml) are merged in at session build time and are not
/// enumerated here — connecting to the servers is out of scope for a listing.
/// Why a tool is off, phrased as the settings entry that did it — nothing is
/// "disabled (default)" any more, so the only honest answer names a key.
fn policy_reason(policy: &stella_tools::policy::ToolPolicy, name: &str) -> String {
match crate::tool_policy::disabled_by(policy, name) {
Some(key) => format!("\"tools\": {{\"{key}\": \"off\"}} in settings"),
// Unreachable for a name the caller already found denied; a plain
// sentence beats an unwrap if the two ever disagree.
None => "a settings entry".to_string(),
}
}

pub fn run_tools_listing() -> Result<(), String> {
let workspace_root =
std::env::current_dir().map_err(|e| format!("cannot determine workspace root: {e}"))?;
tui::section_header("Stella tools");

// The listing mirrors a real session's settings-driven tool switches
// (bash/web opt-ins).
// The listing mirrors a real session: the registry builds the full
// surface, and the operator's `"tools"` switches decide what survives.
let settings = crate::settings::Settings::load(&workspace_root)?;
let bash_enabled = settings.bash_tool_enabled();
let web_enabled = settings.web_tools_enabled();
let policy = settings.tool_policy();
let registry = ToolRegistry::new(
workspace_root.clone(),
stella_tools::RegistryOptions {
bash: bash_enabled,
web: web_enabled,
..Default::default()
},
stella_tools::RegistryOptions::default(),
);
println!(" {}", "built-in:".dimmed());
let mut native: Vec<String> = stella_core::ports::ToolExecutor::schemas(&registry)
Expand All @@ -1580,22 +1592,37 @@ pub fn run_tools_listing() -> Result<(), String> {
.collect();
native.sort();
for name in &native {
println!(" {} {}", "·".dimmed(), name);
if policy.allows(name) {
println!(" {} {}", "·".dimmed(), name);
} else {
println!(
" {} {}",
"·".dimmed(),
format!("{name} — off ({})", policy_reason(&policy, name)).dimmed()
);
}
}
if !bash_enabled {
// A tool the catalog declares but this environment cannot register is a
// missing prerequisite, not a switch — and a tool switched off in settings
// that also has an unmet prerequisite would otherwise vanish silently.
let mut withheld: Vec<&str> = policy
.denied_builtins()
.into_iter()
.filter(|name| !native.iter().any(|live| live == name))
.collect();
withheld.sort_unstable();
for name in withheld {
println!(
" {} {}",
"·".dimmed(),
"bash — disabled (default); enable with \"tools\": {\"bash\": \"on\"} in settings"
.dimmed()
format!("{name} — off ({})", policy_reason(&policy, name)).dimmed()
);
}
if !web_enabled {
if policy.is_default() {
println!(
" {} {}",
"·".dimmed(),
"web_search/web_fetch/web_extract_assets/web_download — disabled (default); \
enable with \"tools\": {\"web\": \"on\"} in settings"
" {}",
"every tool is on — switch one off with \"tools\": {\"<name|group|*>\": \"off\"} \
in settings"
.dimmed()
);
}
Expand Down Expand Up @@ -2024,8 +2051,14 @@ async fn run_turn(

// The scoped tool set must drop its tx clone before awaiting the renderer.
let outcome = if crate::enterprise_telemetry::process_free_authority_active() {
// Even when process-free authority strips the MCP/custom/interactive
// layers, the `"tools"` policy (operator/managed-org tool switches)
// must still be enforced above the session tool stack — mirroring
// every other driver path. Wrap the raw registry in `PolicyToolSet`
// so disabled tools cannot be invoked here either.
let permitted = PolicyToolSet::new(registry, session_tool_policy(cfg));
let engine =
Engine::with_sleeper(provider, registry, engine_config_for(cfg), &TokioSleeper)
Engine::with_sleeper(provider, &permitted, engine_config_for(cfg), &TokioSleeper)
.with_calibration(calibration);
engine.run_turn_with_sender(messages, budget, &tx).await
} else {
Expand All @@ -2043,12 +2076,12 @@ async fn run_turn(
Some(skills) => interactive.with_skill_registry(skills),
None => interactive,
};
let permitted = PolicyToolSet::new(&interactive, session_tool_policy(cfg));
// Outermost discovery layer; the session-scoped `activated` handle
// keeps lean-mode activations across the per-turn stack rebuild.
let tools =
crate::discovery::DiscoveryToolSet::new(&interactive, cfg.workspace_root.clone())
.with_project_prompts_allowed(cfg.authority.project_prompts_allowed)
.with_activation(activated.clone());
let tools = crate::discovery::DiscoveryToolSet::new(&permitted, cfg.workspace_root.clone())
.with_project_prompts_allowed(cfg.authority.project_prompts_allowed)
.with_activation(activated.clone());
let hook_runner = ShellHookRunner;
let mut engine =
Engine::with_sleeper(provider, &tools, engine_config_for(cfg), &TokioSleeper)
Expand Down
12 changes: 6 additions & 6 deletions stella-cli/src/agent/goal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,9 @@ pub(crate) async fn run_goal_turn(
);
let interactive = InteractiveToolSet::new(&customs, tx.clone(), default_ask_io(true))
.with_skill_registry(SkillRegistry::from_env(cfg.workspace_root.clone()));
let tools =
crate::discovery::DiscoveryToolSet::new(&interactive, cfg.workspace_root.clone())
.with_project_prompts_allowed(cfg.authority.project_prompts_allowed);
let permitted = PolicyToolSet::new(&interactive, session_tool_policy(cfg));
let tools = crate::discovery::DiscoveryToolSet::new(&permitted, cfg.workspace_root.clone())
.with_project_prompts_allowed(cfg.authority.project_prompts_allowed);
let hook_runner = ShellHookRunner;
let mut engine =
Engine::with_sleeper(provider, &tools, engine_config_for(cfg), &TokioSleeper)
Expand Down Expand Up @@ -583,9 +583,9 @@ async fn run_goal_pipeline_turn(
);
let interactive = InteractiveToolSet::new(&customs, tx.clone(), default_ask_io(true))
.with_skill_registry(SkillRegistry::from_env(cfg.workspace_root.clone()));
let tools =
crate::discovery::DiscoveryToolSet::new(&interactive, cfg.workspace_root.clone())
.with_project_prompts_allowed(cfg.authority.project_prompts_allowed);
let permitted = PolicyToolSet::new(&interactive, session_tool_policy(cfg));
let tools = crate::discovery::DiscoveryToolSet::new(&permitted, cfg.workspace_root.clone())
.with_project_prompts_allowed(cfg.authority.project_prompts_allowed);

let breaker = CircuitBreaker::new(Box::new(SystemClock::new()));
let router = Router::new(wiring.pins.clone(), wiring.profiles.clone(), breaker);
Expand Down
29 changes: 22 additions & 7 deletions stella-cli/src/agent/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ pub(crate) fn workspace_ports(
let mut candidate_workspaces = crate::candidate_ws::GitCandidateWorkspaces::new(
root.clone(),
registry_options,
session_tool_policy(cfg),
custom_tools,
active_rules,
);
Expand Down Expand Up @@ -555,17 +556,19 @@ fn truncate_tail(s: &str, max_bytes: usize) -> String {
s[idx..].to_string()
}

/// The registry feature switches for this session's config — the ONE
/// translation point from settings (`tools.bash`, default off) to
/// [`stella_tools::RegistryOptions`]. Every session driver (one-shot, goal,
/// interactive, deck, sub-session workers, fleet workers) builds its
/// registry through this, so no path can quietly re-enable the shell.
/// The registry's construction inputs for this session's config: host
/// attestations and media prerequisites, and nothing else.
///
/// It used to also carry `bash`/`web` booleans translated from settings —
/// operator policy applied at construction, which covered built-ins and
/// nothing else. Policy now travels as [`Config::tool_policy`] and is enforced
/// once, above the entire session tool stack, by
/// [`crate::agent::PolicyToolSet`]; see [`session_tool_policy`] for the
/// accompanying half every session driver pairs with this call.
pub(crate) fn registry_options(cfg: &Config) -> stella_tools::RegistryOptions {
let process_free = crate::enterprise_telemetry::process_free_authority_active();
let media_operation_journal = host_media_operation_journal(&cfg.workspace_root);
stella_tools::RegistryOptions {
bash: cfg.tools_bash && !process_free,
web: cfg.tools_web && !process_free,
media_requires_host_approval: cfg.authority.media_requires_host_approval,
media_operation_journal,
media_host_data_isolation: process_free
Expand All @@ -574,6 +577,18 @@ pub(crate) fn registry_options(cfg: &Config) -> stella_tools::RegistryOptions {
}
}

/// The session's tool policy — the other half of [`registry_options`].
///
/// Every session driver wraps its assembled tool stack in
/// [`crate::agent::PolicyToolSet`] with this, which is what makes a
/// `"tools"` entry cover built-ins, MCP tools, and customer-registered custom
/// tools identically. Resolved once in `Config::load_with_settings` (managed
/// ceiling already folded in), so this is a clone, not a re-derivation — there
/// is no second place that could disagree about what is switched off.
pub(crate) fn session_tool_policy(cfg: &Config) -> stella_tools::policy::ToolPolicy {
cfg.tool_policy.clone()
}

fn host_media_operation_journal(
workspace_root: &std::path::Path,
) -> Option<Arc<dyn stella_media::MediaOperationJournal>> {
Expand Down
Loading