refactor(agent-contract): move fleet role and worker profile out of tui into codewhale-agent-contract

X8 of the 2026-10-24 crate-split plan. fleet/role.rs and worker_profile.rs
were the data half of the tools -> fleet edge (62 references). They now live in
the new crate codewhale-agent-contract (depends on protocol, workflow,
execpolicy and, for DEFAULT_SPAWN_DEPTH only, config); every tui caller names
codewhale_agent_contract::role / ::worker_profile directly, with no alias.

The inherent impl FleetRole in tools/subagent/mod.rs (system_prompt,
legacy_type_name) is illegal on a foreign type, so it became the free functions
role_system_prompt and role_legacy_type_name in the same file; 15 call sites
rewritten. is_posture_denial lost its cfg(test) gate (tui tests call it across
the crate boundary). One stale #[expect(dead_code)] was deleted. Items the tui
reaches are now pub (previously pub(crate)); these are the role and profile
data types and the sentinel constants, not credentials or process control.
Intra-doc links to tui items became plain code spans.

Release: codewhale-agent-contract is added to scripts/release/crates.sh after
codewhale-config (it depends on it); validate-crate-publish-order.py passes.
The package name is claimed only at publish time by a human.

Known limit: the dependency on codewhale-config only serves one constant, so
the crate's closure is about 79k lines until a config-types crate exists (X5).

Local, not CI: cargo check -p codewhale-agent-contract -p codewhale-tui
--all-targets --offline exit 0 (-D warnings); cargo test -p
codewhale-agent-contract --lib 14 passed, 0 failed; cargo test -p codewhale-tui
--lib for tools::subagent, fleet::, system_prompt, worker_profile: 1287 tests,
1287 passed in 3 of 5 runs and 1286 passed in 2, the single failure always
tools::subagent::tests::root_operate_dispatch_delegates_builtin_verification_but_not_shell
("cargo is not installed or not in PATH" from the 3 s VERSION_PROBE_TIMEOUT in
dependencies.rs under load); it passes alone and is unrelated to this change.
module_graph.py --check PASS; cargo fmt --check clean; release order validator
exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Sonnet 5.5 committed 2026-10-24 19:48:08 -07:00
1 parent 0ae2655d22
commit 8a33bc0179
53 files changed
+347 -279

No files matched your search

Generated
+13
View File
@@ -839,6 +839,18 @@ dependencies = [
"serde",
]
[[package]]
name = "codewhale-agent-contract"
version = "0.10.2"
dependencies = [
"codewhale-config",
"codewhale-execpolicy",
"codewhale-protocol",
"codewhale-workflow",
"serde",
"serde_json",
]
[[package]]
name = "codewhale-app-server"
version = "0.10.2"
@@ -1301,6 +1313,7 @@ dependencies = [
"chrono",
"clap",
"codewhale-agent",
"codewhale-agent-contract",
"codewhale-app-server",
"codewhale-build-support",
"codewhale-cloud-facts",
+1
View File
@@ -1,6 +1,7 @@
[workspace]
members = [
"crates/agent",
"crates/agent-contract",
"crates/app-server",
"crates/build-support",
"crates/cli",
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "codewhale-agent-contract"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Fleet role, child authority and worker runtime profile contract shared by the engine and its clients"
[lints]
workspace = true
[dependencies]
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.10.2" }
codewhale-protocol = { path = "../protocol", version = "0.10.2" }
codewhale-workflow = { path = "../workflow", version = "0.10.2" }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
+2
View File
@@ -0,0 +1,2 @@
pub mod role;
pub mod worker_profile;
@@ -29,7 +29,7 @@ use crate::worker_profile::{ShellPolicy, ToolScope, WorkerRuntimeProfile};
/// closed `enum` advertised on the Agent tool's `type` property. Legacy
/// aliases are accepted only at replay/deserialization boundaries
/// ([`migrate_legacy_role_token`]) and are never advertised to models.
pub(crate) const FLEET_ROLE_SCHEMA_VALUES: [&str; 8] = [
pub const FLEET_ROLE_SCHEMA_VALUES: [&str; 8] = [
"general",
"explore",
"planner",
@@ -45,7 +45,7 @@ pub(crate) const FLEET_ROLE_SCHEMA_VALUES: [&str; 8] = [
/// [`migrate_legacy_role_token`] and the `from_str` match arms; those two are
/// the only role parser (#2649 was a second table in the spawn tool drifting
/// from this set).
pub(crate) const VALID_ROLE_ALIASES: &str = "general; explore; planner; reviewer; implement; test; advisor; custom \
pub const VALID_ROLE_ALIASES: &str = "general; explore; planner; reviewer; implement; test; advisor; custom \
(legacy aliases remain accepted: worker; scout; builder; verifier; consultant; default; general-purpose; general_purpose; exploration; explorer; plan; planning; awaiter; review; code-review; code_review; implementer; implementation; verify; verification; validator; tester; oracle)";
/// Canonical Fleet role for a delegated worker, with specialized behavior
@@ -242,7 +242,7 @@ pub fn public_role_label(token: &str) -> String {
/// The `mcp*` wildcards are load-bearing: a remote MCP server's tools arrive
/// under a runtime-generated name, so they cannot be enumerated here and must be
/// matched by prefix. `is_tool_denied` supports `prefix*` globs for exactly this.
pub(crate) const NETWORK_TOOL_DENYLIST: &[&str] = &[
pub const NETWORK_TOOL_DENYLIST: &[&str] = &[
// Web search / fetch / browse, and the canonical family that fronts them.
//
// The `Web` family name itself is deliberately NOT denied. Its `search`
@@ -288,7 +288,7 @@ pub(crate) const NETWORK_TOOL_DENYLIST: &[&str] = &[
// contract was considered and rejected: `rlm_open` chooses its source from
// *input fields* (`file_path` / `content` / `url` / `session_object`), not
// from the action name, and the action-policy seam
// ([`crate::tools::canonical_action`]) resolves names, not field shapes —
// (`crate::tools::canonical_action`) resolves names, not field shapes —
// it cannot prove a source is local before execution. So this fails closed.
// A network-denied member loses `rlm` loading and evaluation entirely,
// including the purely local `file_path` form, and keeps only the bounded
@@ -315,7 +315,7 @@ pub(crate) const NETWORK_TOOL_DENYLIST: &[&str] = &[
/// network axis off the resolved grant, which folds this sentinel in at
/// resolve time — so the transport denial and the semantic answer can never
/// disagree.
pub(crate) const NETWORK_DENIAL_SENTINEL: &str = "fetch_url";
pub const NETWORK_DENIAL_SENTINEL: &str = "fetch_url";
/// Tool names that mutate the workspace directly.
///
@@ -334,7 +334,7 @@ pub(crate) const NETWORK_DENIAL_SENTINEL: &str = "fetch_url";
/// receipt said otherwise. The rest of the family — including the local
/// `file_path` load — survives a write denial, because reading a large file
/// into a kernel is exactly what a read-only member is for.
pub(crate) const MUTATING_TOOL_DENYLIST: &[&str] = &[
pub const MUTATING_TOOL_DENYLIST: &[&str] = &[
"write_file",
"edit_file",
"apply_patch",
@@ -364,9 +364,9 @@ pub(crate) const MUTATING_TOOL_DENYLIST: &[&str] = &[
/// Denying the tools outright would take the verifier's whole purpose with
/// them, so the *unbounded arguments* are refused at the execution seam
/// instead; see `reject_unbounded_verification` in
/// [`crate::tools::subagent`]. The name deny list and that guard are one
/// `crate::tools::subagent`. The name deny list and that guard are one
/// contract split across the only two places that can each see half of it.
pub(crate) const RAW_SHELL_DENYLIST: &[&str] = &[
pub const RAW_SHELL_DENYLIST: &[&str] = &[
"Bash",
"exec_shell",
"exec_shell_wait",
@@ -394,17 +394,16 @@ pub(crate) const RAW_SHELL_DENYLIST: &[&str] = &[
/// is answered by the resolved grant's `shell` axis, which distinguishes
/// "no process surface" from "bounded verification surface" by grant field
/// rather than by a sentinel name.
#[cfg_attr(not(test), expect(dead_code))]
pub(crate) const RAW_SHELL_SENTINEL: &str = "exec_shell";
pub const RAW_SHELL_SENTINEL: &str = "exec_shell";
/// The built-in verification surface: the workspace's own configured checks.
///
/// Bounded in its arguments (see [`crate::tools::execution_envelope`]) but not
/// Bounded in its arguments (see `crate::tools::execution_envelope`) but not
/// free of consequence — every entry forks a process. A member whose shell
/// ceiling is narrower than `full` holds no authority to start one, so this
/// list comes off its surface entirely. A `write = false, shell = "full"`
/// member keeps it, because running the checks is what that preset is for.
pub(crate) const VERIFICATION_SURFACE_DENYLIST: &[&str] = &["Run", "run_tests", "run_verifiers"];
pub const VERIFICATION_SURFACE_DENYLIST: &[&str] = &["Run", "run_tests", "run_verifiers"];
/// The deny-list entry that stands for "this child holds no shell authority".
///
@@ -418,7 +417,7 @@ pub(crate) const VERIFICATION_SURFACE_DENYLIST: &[&str] = &["Run", "run_tests",
/// this sentinel into its `shell` axis at resolve time: `Verify`/`Full`
/// collapse to `None`, while `Inspect` — classifier-bounded evidence reads,
/// not process-start authority — survives.
pub(crate) const SHELL_AUTHORITY_SENTINEL: &str = "run_tests";
pub const SHELL_AUTHORITY_SENTINEL: &str = "run_tests";
/// Execution primitives that are **not** spelled as shell.
///
@@ -427,7 +426,7 @@ pub(crate) const SHELL_AUTHORITY_SENTINEL: &str = "run_tests";
/// a stored automation with its own cwd and prompt, `start_mcp_server` spawns a
/// process, and `pr_attempt_*` writes durable work state. They are listed here
/// so a write-denied child never *sees* them; the authoritative refusal is
/// capability-derived and lives in [`crate::tools::execution_envelope`], which
/// capability-derived and lives in `crate::tools::execution_envelope`, which
/// also covers the ones no list can name — repository plugin tools and MCP
/// server tools registered at runtime.
///
@@ -435,7 +434,7 @@ pub(crate) const SHELL_AUTHORITY_SENTINEL: &str = "run_tests";
/// what the canonical-action seam exists for: denying `tasks` outright would
/// take `list`/`read` with it, and durable-task bookkeeping is exactly what a
/// read-only member should keep.
pub(crate) const NON_SHELL_EXECUTION_DENYLIST: &[&str] = &[
pub const NON_SHELL_EXECUTION_DENYLIST: &[&str] = &[
"task_gate_run",
"task_create",
"task_cancel",
@@ -453,9 +452,8 @@ pub(crate) const NON_SHELL_EXECUTION_DENYLIST: &[&str] = &[
/// Whether a deny rule belongs to an enforced role posture. Operator and
/// ancestor denials are also immutable; this identifies posture rules for
/// authority diagnostics and tests, not a child opt-out exception.
#[cfg(test)]
#[must_use]
pub(crate) fn is_posture_denial(rule: &str) -> bool {
pub fn is_posture_denial(rule: &str) -> bool {
[
NETWORK_TOOL_DENYLIST,
MUTATING_TOOL_DENYLIST,
@@ -471,22 +469,22 @@ pub(crate) fn is_posture_denial(rule: &str) -> bool {
/// A Runtime role policy intersected with the live parent and translated into
/// the concrete knobs a child spawn actually carries.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ChildAuthority {
pub struct ChildAuthority {
/// The clamped ceiling. Never wider than either input.
pub(crate) ceiling: PermissionCeiling,
pub ceiling: PermissionCeiling,
/// `Some(list)` narrows the child's model-visible surface to exactly
/// `list`. `Some(vec![])` — the `tools = false` case — means *no tools at
/// all*, which is what the child registry's empty-allowlist path produces.
/// `None` means full inheritance from the parent surface.
pub(crate) allowed_tools: Option<Vec<String>>,
pub allowed_tools: Option<Vec<String>>,
/// Names/globs the child must never see or call. Deny wins over allow.
pub(crate) disallowed_tools: Vec<String>,
pub disallowed_tools: Vec<String>,
/// Spawn write authority implied by the clamped ceiling.
pub(crate) write_authority: &'static str,
pub write_authority: &'static str,
/// Nested-delegation budget, clamped.
pub(crate) max_depth: u32,
pub max_depth: u32,
/// Canonical posture role that governs the child's tool posture.
pub(crate) posture_role: &'static str,
pub posture_role: &'static str,
}
impl ChildAuthority {
@@ -495,7 +493,7 @@ impl ChildAuthority {
/// Every field takes the more restrictive side, so a child can never widen
/// live authority.
#[must_use]
pub(crate) fn clamp(requested: PermissionCeiling, session: PermissionCeiling) -> Self {
pub fn clamp(requested: PermissionCeiling, session: PermissionCeiling) -> Self {
let ceiling = requested.clamp_to(session);
// `tools = false` is total: an empty allowlist leaves the child with no
@@ -602,7 +600,7 @@ impl ChildAuthority {
/// the fail-closed error, and an operator debugging a refused launch should
/// be able to see which side differs without a lookup table.
#[must_use]
pub(crate) fn fingerprint(&self) -> String {
pub fn fingerprint(&self) -> String {
let allowed = match &self.allowed_tools {
None => "inherit".to_string(),
Some(list) if list.is_empty() => "none".to_string(),
@@ -632,7 +630,7 @@ impl ChildAuthority {
/// selection. Free-form semantic roles map to Runtime `custom`; neither
/// the Fleet definition nor its legacy `permissions` key participates.
#[must_use]
pub(crate) fn from_runtime_role(role: &str, session: PermissionCeiling) -> Self {
pub fn from_runtime_role(role: &str, session: PermissionCeiling) -> Self {
let runtime_role = runtime_role_for_member(role);
let requested = runtime_permission_ceiling(&runtime_role);
let mut authority = Self::clamp(requested, session);
@@ -718,7 +716,7 @@ fn member_role_alias(token: &str) -> Option<FleetRole> {
/// raw shell, network reads and classifier-bounded `Bash` inspection. An
/// operator who genuinely wants "inherit whatever the parent has" spells that
/// `custom`, which is a declared role and resolves at step 2.
pub(crate) fn runtime_role_for_member(role: &str) -> FleetRole {
pub fn runtime_role_for_member(role: &str) -> FleetRole {
let token = role.trim().to_ascii_lowercase();
if token.is_empty() {
return FleetRole::Worker;
@@ -750,7 +748,7 @@ fn runtime_permission_ceiling(role: &FleetRole) -> PermissionCeiling {
/// Intersect a worker shell policy with the session's legacy shell opt-in.
#[must_use]
pub(crate) fn session_shell_ceiling(shell: ShellPolicy, allow_shell: bool) -> ShellCeiling {
pub fn session_shell_ceiling(shell: ShellPolicy, allow_shell: bool) -> ShellCeiling {
match shell {
ShellPolicy::None => ShellCeiling::None,
ShellPolicy::ReadOnly => ShellCeiling::ReadOnly,
@@ -762,7 +760,7 @@ pub(crate) fn session_shell_ceiling(shell: ShellPolicy, allow_shell: bool) -> Sh
/// Map a permission ceiling onto the canonical posture role that governs the
/// child's tool surface.
#[must_use]
pub(crate) fn posture_role_for(ceiling: PermissionCeiling) -> &'static str {
pub fn posture_role_for(ceiling: PermissionCeiling) -> &'static str {
if !ceiling.tools {
// No tools at all; the narrowest posture, and the allowlist is empty
// anyway.
@@ -781,7 +779,7 @@ pub(crate) fn posture_role_for(ceiling: PermissionCeiling) -> &'static str {
/// requested runtime profile says. Spawn narrows the child to a read-only
/// shell for these roles, and every receipt must report that same posture.
#[must_use]
pub(crate) fn role_requires_read_only_shell(role: &FleetRole) -> bool {
pub fn role_requires_read_only_shell(role: &FleetRole) -> bool {
matches!(
role,
FleetRole::Scout | FleetRole::Reviewer | FleetRole::Planner
@@ -792,7 +790,7 @@ pub(crate) fn role_requires_read_only_shell(role: &FleetRole) -> bool {
/// profile with the shell narrowed for read-only roles. Receipts and headers
/// derive from this, never from the requested profile alone (#5542 review).
#[must_use]
pub(crate) fn effective_runtime_profile_for_role(
pub fn effective_runtime_profile_for_role(
role: &FleetRole,
requested: &WorkerRuntimeProfile,
) -> WorkerRuntimeProfile {
@@ -825,7 +823,7 @@ fn tool_scope_label(tools: &ToolScope) -> &'static str {
/// identify a saved Fleet member when one selected the role; `None` for
/// direct role dispatches.
#[must_use]
pub(crate) fn fleet_effective_permissions(
pub fn fleet_effective_permissions(
role: &FleetRole,
requested: &WorkerRuntimeProfile,
profile_id: Option<&str>,
@@ -14,7 +14,7 @@
//! contract. Runtime enforcement of every declared field remains incremental
//! follow-up work (#3217).
use crate::fleet::role::FleetRole;
use crate::role::FleetRole;
use serde::{Deserialize, Serialize};
/// Coarse capability classes a worker may exercise, beyond read access (reads
@@ -175,8 +175,8 @@ pub enum ToolSurface {
///
/// This is deliberately a *projection*, not a second persisted record:
/// [`WorkerRuntimeProfile`] remains the wire/persistence shape and
/// [`ChildAuthority`](crate::fleet::role::ChildAuthority) /
/// [`ToolAuthorityEnvelope`](crate::tools::spec::ToolAuthorityEnvelope)
/// `ChildAuthority` /
/// `ToolAuthorityEnvelope`
/// remain the transports that carry the same authority across the durable
/// Fleet and `codewhale exec` subprocess boundaries. What this object retires
/// is the *semantic* duplication — posture sentinel entries read back off a
@@ -250,7 +250,7 @@ impl ChildGrant {
///
/// Every field takes the more restrictive side, so the result can never
/// name authority the parent lacked. This is the in-process counterpart
/// of [`crate::fleet::role::ChildAuthority::clamp`].
/// of [`crate::role::ChildAuthority::clamp`].
#[must_use]
pub fn resolve(
role: &FleetRole,
@@ -279,7 +279,7 @@ impl ChildGrant {
)
};
let resolved_shell = preset.shell.min(profile_shell);
let shell = if denied(crate::fleet::role::SHELL_AUTHORITY_SENTINEL) {
let shell = if denied(crate::role::SHELL_AUTHORITY_SENTINEL) {
match resolved_shell {
ShellGrant::Inspect => ShellGrant::Inspect,
_ => ShellGrant::None,
@@ -301,7 +301,7 @@ impl ChildGrant {
shell,
network: preset.network
&& profile.permissions.network
&& !denied(crate::fleet::role::NETWORK_DENIAL_SENTINEL),
&& !denied(crate::role::NETWORK_DENIAL_SENTINEL),
desktop: false,
surface: preset.surface,
scope,
@@ -553,10 +553,7 @@ impl WorkerRuntimeProfile {
}
/// Omission inherits; neither a child request nor a replay can widen a cap.
pub(crate) fn narrow_optional_limit<T: Ord>(
inherited: Option<T>,
requested: Option<T>,
) -> Option<T> {
pub fn narrow_optional_limit<T: Ord>(inherited: Option<T>, requested: Option<T>) -> Option<T> {
match (inherited, requested) {
(Some(inherited), Some(requested)) => Some(inherited.min(requested)),
(inherited, requested) => inherited.or(requested),
@@ -564,7 +561,7 @@ pub(crate) fn narrow_optional_limit<T: Ord>(
}
/// Zero is the operator/internal unbounded sentinel, never a widening request.
pub(crate) fn narrow_model_steps(inherited: u32, requested: u32) -> u32 {
pub fn narrow_model_steps(inherited: u32, requested: u32) -> u32 {
narrow_optional_limit(
(inherited > 0).then_some(inherited),
(requested > 0).then_some(requested),
+1
View File
@@ -90,6 +90,7 @@ codewhale-memory = { path = "../memory", version = "0.10.2" }
ahash = "0.8"
anyhow.workspace = true
codewhale-agent = { path = "../agent", version = "0.10.2" }
codewhale-agent-contract = { path = "../agent-contract", version = "0.10.2" }
codewhale-app-server = { path = "../app-server", version = "0.10.2" }
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-cloud-facts = { path = "../cloud-facts", version = "0.10.2" }
+2 -2
View File
@@ -7956,7 +7956,7 @@ impl Config {
if key == "default" {
key
} else {
crate::fleet::role::migrate_legacy_role_token(&key)
codewhale_agent_contract::role::migrate_legacy_role_token(&key)
.unwrap_or(&key)
.to_string()
}
@@ -7985,7 +7985,7 @@ impl Config {
if raw == "default" {
raw
} else {
crate::fleet::role::migrate_legacy_role_token(&raw)
codewhale_agent_contract::role::migrate_legacy_role_token(&raw)
.unwrap_or(&raw)
.to_string()
}
+1 -1
View File
@@ -16,7 +16,7 @@ use std::path::{Component, Path, PathBuf};
use crate::sandbox::SandboxPolicy;
use crate::tools::canonical_action::canonical_action_alias;
use crate::tools::spec::{ApprovalRequirement, normalize_path};
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
use codewhale_config::AppMode;
use codewhale_execpolicy::ApprovalMode;
use serde_json::Value;
+6 -3
View File
@@ -60,8 +60,8 @@ use crate::tools::todo::{SharedTodoList, new_shared_todo_list};
use crate::tools::user_input::{UserInputRequest, UserInputResponse};
use crate::tools::{ToolContext, ToolRegistryBuilder};
use crate::utils::spawn_supervised;
use crate::worker_profile::WorkerRuntimeProfile;
use crate::working_set::WorkingSet;
use codewhale_agent_contract::worker_profile::WorkerRuntimeProfile;
use codewhale_config::AppMode;
use codewhale_execpolicy::ApprovalMode;
#[cfg(test)]
@@ -6008,7 +6008,9 @@ impl Engine {
self.config.disallowed_tools.clone().unwrap_or_default();
if let Some(allowed) = allowed_tools.as_ref() {
rt.worker_profile.tools =
crate::worker_profile::ToolScope::Explicit(allowed.clone());
codewhale_agent_contract::worker_profile::ToolScope::Explicit(
allowed.clone(),
);
}
if let Some(context) = fork_context_for_runtime.clone() {
rt = rt.with_fork_context(context);
@@ -7499,7 +7501,8 @@ impl Engine {
}
rt.worker_profile.denied_tools = self.config.disallowed_tools.clone().unwrap_or_default();
if let Some(allowed) = self.config.allowed_tools.as_ref() {
rt.worker_profile.tools = crate::worker_profile::ToolScope::Explicit(allowed.clone());
rt.worker_profile.tools =
codewhale_agent_contract::worker_profile::ToolScope::Explicit(allowed.clone());
}
Some(rt)
}
+3 -3
View File
@@ -64,9 +64,9 @@ impl Engine {
&& self.config.features.enabled(Feature::ShellTool)
&& (!requested || backend.is_some());
context = context.with_shell_policy(if shell {
crate::worker_profile::ShellPolicy::Full
codewhale_agent_contract::worker_profile::ShellPolicy::Full
} else {
crate::worker_profile::ShellPolicy::None
codewhale_agent_contract::worker_profile::ShellPolicy::None
});
if let Some(backend) = backend {
context = context.with_sandbox_backend(backend);
@@ -373,7 +373,7 @@ mod tests {
assert!(build.surface.registry.get("Bash").is_none());
assert_eq!(
build.surface.registry.context().shell_policy,
crate::worker_profile::ShellPolicy::None
codewhale_agent_contract::worker_profile::ShellPolicy::None
);
}
#[test]
+14 -7
View File
@@ -220,17 +220,20 @@ async fn child_shell_ceiling_survives_every_context_policy_setter() {
let _home = crate::test_support::SealedHome::at(dir.path());
let (_engine, _handle, authority, _route) = fixture(dir.path(), None);
let ceiling = authority.grant.shell_policy();
assert_ne!(ceiling, crate::worker_profile::ShellPolicy::Full);
assert_ne!(
ceiling,
codewhale_agent_contract::worker_profile::ShellPolicy::Full
);
let mut context = authority
.context()
.with_shell_policy(crate::worker_profile::ShellPolicy::Full);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::Full);
assert_eq!(context.shell_policy, ceiling);
context.set_shell_policy(crate::worker_profile::ShellPolicy::None);
context.set_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::None);
assert_eq!(
context.shell_policy,
crate::worker_profile::ShellPolicy::None
codewhale_agent_contract::worker_profile::ShellPolicy::None
);
context.set_shell_policy(crate::worker_profile::ShellPolicy::Full);
context.set_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::Full);
assert_eq!(context.shell_policy, ceiling);
}
@@ -605,7 +608,9 @@ async fn actual_child_transport_services_approval_and_cancel_with_saturated_stee
let (_old, _old_handle, captured, _) = fixture(dir.path(), Some(vec!["bash".into()]));
let mut runtime = captured.runtime.clone();
runtime.worker_profile =
crate::worker_profile::WorkerRuntimeProfile::for_role(FleetRole::Worker);
codewhale_agent_contract::worker_profile::WorkerRuntimeProfile::for_role(
FleetRole::Worker,
);
runtime.parent_can_prompt = true;
runtime.context.auto_approve = false;
runtime.context.approval_mode = ApprovalMode::Suggest;
@@ -1355,7 +1360,9 @@ async fn detached_child_catastrophic_shell_floor_uses_captured_origin_in_every_p
let (_old, _handle, captured, _) = fixture(dir.path(), Some(vec!["bash".into()]));
let mut runtime = captured.runtime.clone();
runtime.worker_profile =
crate::worker_profile::WorkerRuntimeProfile::for_role(FleetRole::Worker);
codewhale_agent_contract::worker_profile::WorkerRuntimeProfile::for_role(
FleetRole::Worker,
);
runtime.context.live_posture = None;
runtime.context.approval_mode = approval_mode;
runtime.context.auto_approve = approval_mode == ApprovalMode::Bypass;
@@ -89,7 +89,7 @@ fn agent_tool_contexts_follow_the_published_posture() {
);
after.refresh_live_posture();
assert!(!after.auto_approve);
assert_eq!(after.shell_policy, crate::worker_profile::ShellPolicy::None);
assert_eq!(after.shell_policy, codewhale_agent_contract::worker_profile::ShellPolicy::None);
assert_eq!(
after.elevated_sandbox_policy,
Some(crate::sandbox::SandboxPolicy::read_only())
@@ -889,7 +889,7 @@ fn turn_tool_registry_builder_keeps_plan_primitive_identity() {
assert!(registry.contains("handle_read"));
assert_eq!(
registry.context().shell_policy,
crate::worker_profile::ShellPolicy::None
codewhale_agent_contract::worker_profile::ShellPolicy::None
);
}
@@ -1091,7 +1091,7 @@ fn plan_mode_registry_can_expose_agent_launcher_without_shell_tools() {
#[test]
fn mode_invariant_matrix_covers_context_catalog_subagents_and_prompt_metadata() {
use crate::sandbox::SandboxPolicy;
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
use ApprovalMode;
#[derive(Clone, Copy)]
@@ -1496,13 +1496,13 @@ fn build_tool_context_uses_typed_shell_policy_per_mode() {
// Plan mode is shell-free and exposes no shell tools.
assert_eq!(
engine.build_tool_context(AppMode::Plan, false).shell_policy,
crate::worker_profile::ShellPolicy::None
codewhale_agent_contract::worker_profile::ShellPolicy::None
);
assert_eq!(
engine
.build_tool_context(AppMode::Agent, false)
.shell_policy,
crate::worker_profile::ShellPolicy::Full
codewhale_agent_contract::worker_profile::ShellPolicy::Full
);
config.allow_shell = false;
@@ -1511,7 +1511,7 @@ fn build_tool_context_uses_typed_shell_policy_per_mode() {
engine
.build_tool_context(AppMode::Agent, false)
.shell_policy,
crate::worker_profile::ShellPolicy::None
codewhale_agent_contract::worker_profile::ShellPolicy::None
);
}
@@ -1551,7 +1551,7 @@ fn turn_tool_context_uses_planned_authority_and_route_not_installed_session() {
let context = engine.build_tool_context_for_turn(&authority, &route);
assert_eq!(
context.shell_policy,
crate::worker_profile::ShellPolicy::Full
codewhale_agent_contract::worker_profile::ShellPolicy::Full
);
assert!(context.trust_mode);
assert!(context.auto_approve);
+1 -1
View File
@@ -692,7 +692,7 @@ pub(crate) fn enforce_tool_denial(
) -> Result<(), ToolError> {
let bounded_child_read = name == "bash"
&& context.owner_agent_id.is_some()
&& context.shell_policy == crate::worker_profile::ShellPolicy::ReadOnly
&& context.shell_policy == codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly
&& crate::tools::shell::agent_readonly_bash_input(input);
if !bounded_child_read && tool_call_denied(Some(&context.disallowed_tools), name, input) {
return Err(ToolError::permission_denied(format!(
+1 -1
View File
@@ -5,7 +5,7 @@
use super::*;
use crate::core::authority::shell_policy_for_mode;
use crate::tools::AgentToolSurfaceOptions;
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
fn should_register_remember_tool(memory_enabled: bool) -> bool {
memory_enabled
+1 -1
View File
@@ -7894,7 +7894,7 @@ mod tests {
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::write(tmp.path().join("legacy.txt"), "before\n").expect("fixture");
let context = crate::tools::spec::ToolContext::new(tmp.path().to_path_buf())
.with_shell_policy(crate::worker_profile::ShellPolicy::Full);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::Full);
let registry = crate::tools::ToolRegistryBuilder::new()
.with_file_tools()
.with_foreground_shell_tools()
+10 -7
View File
@@ -46,15 +46,15 @@ use codewhale_workflow::{
router_call_plan, router_system_prompt, router_user_message,
};
use super::role::{ChildAuthority, public_role_label};
#[cfg(test)]
use super::role::{
NETWORK_DENIAL_SENTINEL, NETWORK_TOOL_DENYLIST, RAW_SHELL_SENTINEL, is_posture_denial,
session_shell_ceiling,
};
use crate::config::{Config, ProviderKind};
use crate::llm_client::LlmClient;
use crate::reasoning_preference::ReasoningEffort;
use codewhale_agent_contract::role::{ChildAuthority, public_role_label};
#[cfg(test)]
use codewhale_agent_contract::role::{
NETWORK_DENIAL_SENTINEL, NETWORK_TOOL_DENYLIST, RAW_SHELL_SENTINEL, is_posture_denial,
session_shell_ceiling,
};
use codewhale_models::Role;
/// Where exact Fleet definitions and Reasoning Router profiles are looked up,
@@ -1795,7 +1795,10 @@ mod shell_ceiling_tests {
);
assert_eq!(planner_parent_shell_off.posture_role, "planner");
assert_eq!(
session_shell_ceiling(crate::worker_profile::ShellPolicy::Full, false),
session_shell_ceiling(
codewhale_agent_contract::worker_profile::ShellPolicy::Full,
false
),
ShellCeiling::None
);
}
+2 -1
View File
@@ -1437,7 +1437,8 @@ mod tests {
assert_eq!(authority.shell, ToolShellAuthority::None);
assert_eq!(authority.verification, ToolVerificationAuthority::Bounded);
launch_spec.runtime_profile.shell = crate::worker_profile::ShellPolicy::None;
launch_spec.runtime_profile.shell =
codewhale_agent_contract::worker_profile::ShellPolicy::None;
let shellless = authority_envelope_for_worker(&launch_spec, &task).unwrap();
assert_eq!(
shellless.verification,
+1 -1
View File
@@ -15,11 +15,11 @@ use codewhale_config::{
};
use super::profile::AgentProfile;
use super::role::public_role_label;
use super::roster::{FleetRoster, ProfileOrigin};
use super::store::{
FleetFile, FleetScope, MemberCapability, load_fleet_at, resolve_selected_fleet,
};
use codewhale_agent_contract::role::public_role_label;
const MAX_IDENTITY_FIELD_CHARS: usize = 160;
+1 -1
View File
@@ -9,11 +9,11 @@
use std::path::Path;
use super::role::public_role_label;
use super::store::{
FleetFile, FleetMember, FleetScope, FleetStoreError, load_fleet_at, load_fleet_in_scope,
member_pins, provider_ids_match, resolve_selected_fleet, save_fleet, set_selected, slugify,
};
use codewhale_agent_contract::role::public_role_label;
use codewhale_localization::{Locale, MessageId, tr};
/// Default name for the personal fleet created by the first `/fleet add` or
-1
View File
@@ -14,7 +14,6 @@ pub mod manager;
pub mod members;
pub mod placement;
pub mod profile;
pub mod role;
pub mod roster;
pub mod scheduler;
pub mod scout;
+1 -1
View File
@@ -732,7 +732,7 @@ fn agent_profile_from_toml(path: &Path, parsed: AgentProfileToml) -> Result<Agen
/// its saved id. Only the semantic role is migrated; every new receipt and UI
/// label derived from it therefore uses the canonical public token.
pub(crate) fn canonical_public_role_name(role: &str) -> String {
super::role::public_role_label(role)
codewhale_agent_contract::role::public_role_label(role)
}
fn reject_permission_expansion(
+5 -3
View File
@@ -661,8 +661,10 @@ impl FleetRoster {
.filter(|issue| {
let same_id = issue.id.eq_ignore_ascii_case(requested_id)
|| member.is_some_and(|member| {
super::role::public_role_label(&issue.id)
.eq_ignore_ascii_case(&super::role::public_role_label(&member.id))
codewhale_agent_contract::role::public_role_label(&issue.id)
.eq_ignore_ascii_case(
&codewhale_agent_contract::role::public_role_label(&member.id),
)
});
same_id
&& member.is_none_or(|member| {
@@ -961,7 +963,7 @@ mod tests {
/// plain agent spawns in production (#6244).
#[test]
fn built_in_members_have_one_member_per_canonical_role() {
use crate::fleet::role::public_role_label;
use codewhale_agent_contract::role::public_role_label;
let mut seen: std::collections::BTreeMap<String, String> =
std::collections::BTreeMap::new();
for member in FleetRoster::built_in_members() {
+18 -14
View File
@@ -28,11 +28,13 @@ use super::profile::{
AgentProfile, FleetDelegationHints, FleetLoadout, FleetProfile, FleetProfilePermissions,
FleetRole as FleetProfileRole, FleetSlot, ProfileOrigin, canonical_public_role_name,
};
use super::role::runtime_role_for_member;
use crate::config::{Config, ProviderKind};
use crate::route_runtime::resolve_route_candidate;
use crate::tools::subagent::{AgentWorkerSpec, AgentWorkerToolProfile, FleetRole};
use crate::worker_profile::{ChildLaunchManifest, ModelRoute, ToolScope, WorkerRuntimeProfile};
use codewhale_agent_contract::role::runtime_role_for_member;
use codewhale_agent_contract::worker_profile::{
ChildLaunchManifest, ModelRoute, ToolScope, WorkerRuntimeProfile,
};
/// Reserved durable task metadata written after author input is validated.
///
@@ -1512,7 +1514,7 @@ pub(crate) fn fleet_effective_permissions_for_task(
let agent_profile = resolve_task_agent_profile(task_spec, agent_profiles)
.ok()
.flatten();
crate::fleet::role::fleet_effective_permissions(
codewhale_agent_contract::role::fleet_effective_permissions(
&spec.agent_type,
&spec.runtime_profile,
agent_profile.as_ref().map(|profile| profile.id.as_str()),
@@ -1602,11 +1604,13 @@ fn profile_origin_label(origin: crate::fleet::roster::ProfileOrigin) -> &'static
}
}
fn shell_policy_label(shell: crate::worker_profile::ShellPolicy) -> &'static str {
fn shell_policy_label(
shell: codewhale_agent_contract::worker_profile::ShellPolicy,
) -> &'static str {
match shell {
crate::worker_profile::ShellPolicy::None => "none",
crate::worker_profile::ShellPolicy::ReadOnly => "read_only",
crate::worker_profile::ShellPolicy::Full => "full",
codewhale_agent_contract::worker_profile::ShellPolicy::None => "none",
codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly => "read_only",
codewhale_agent_contract::worker_profile::ShellPolicy::Full => "full",
}
}
@@ -1655,9 +1659,9 @@ mod tests {
#[test]
fn read_only_roles_report_the_narrowed_shell_they_actually_run_under() {
use crate::fleet::role;
use crate::tools::subagent::FleetRole;
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::role;
use codewhale_agent_contract::worker_profile::ShellPolicy;
let mut requested = WorkerRuntimeProfile {
shell: ShellPolicy::Full,
..WorkerRuntimeProfile::default()
@@ -2291,7 +2295,7 @@ mod tests {
assert_eq!(runtime_role_for_member(role), expected, "role {role}");
// The exact driver's authority comes from the same mapper.
assert_eq!(
crate::fleet::role::ChildAuthority::from_runtime_role(role, session)
codewhale_agent_contract::role::ChildAuthority::from_runtime_role(role, session)
.write_authority,
expected_write,
"role {role} must resolve the same authority on the exact driver"
@@ -2712,7 +2716,7 @@ mod tests {
);
assert_eq!(
spec.runtime_profile.shell,
crate::worker_profile::ShellPolicy::None
codewhale_agent_contract::worker_profile::ShellPolicy::None
);
assert_eq!(
fleet_worker_launch_reasoning_effort(&task, &[]).as_deref(),
@@ -4507,7 +4511,7 @@ mod tests {
);
assert_eq!(
spec.runtime_profile.shell,
crate::worker_profile::ShellPolicy::Full
codewhale_agent_contract::worker_profile::ShellPolicy::Full
);
assert_eq!(
spec.runtime_profile.tools,
@@ -4517,7 +4521,7 @@ mod tests {
assert_eq!(spec.max_spawn_depth, 2);
assert_eq!(spec.spawn_depth, 1);
let permissions = crate::fleet::role::fleet_effective_permissions(
let permissions = codewhale_agent_contract::role::fleet_effective_permissions(
&spec.agent_type,
&spec.runtime_profile,
None,
@@ -4707,7 +4711,7 @@ mod tests {
)
.expect("worker spec with empty profiles");
let public_role = crate::fleet::role::public_role_label(role);
let public_role = codewhale_agent_contract::role::public_role_label(role);
assert_eq!(spec.role.as_deref(), Some(public_role.as_str()));
assert_eq!(spec.agent_type, expected_type, "role {role}");
assert_eq!(spec.tool_profile, expected_tools, "role {role}");
-1
View File
@@ -145,7 +145,6 @@ mod vision;
mod voice;
pub mod whats_new;
mod work_graph;
mod worker_profile;
mod working_set;
mod workspace_lease;
mod workspace_trust;
@@ -526,7 +526,7 @@
assert!(!context.trust_mode);
assert_eq!(
context.shell_policy,
crate::worker_profile::ShellPolicy::None
codewhale_agent_contract::worker_profile::ShellPolicy::None
);
assert!(matches!(
context.elevated_sandbox_policy,
+1 -1
View File
@@ -2954,7 +2954,7 @@ async fn agent_runs_runtime_api_exposes_persisted_worker_receipts() -> Result<()
AgentRunTakeoverTarget, AgentRunUsage, AgentRunVerificationSummary, AgentWorkerEvent,
AgentWorkerRecord, AgentWorkerSpec, AgentWorkerStatus, AgentWorkerToolProfile, FleetRole,
};
use crate::worker_profile::{ModelRoute, ToolScope, WorkerRuntimeProfile};
use codewhale_agent_contract::worker_profile::{ModelRoute, ToolScope, WorkerRuntimeProfile};
use std::collections::VecDeque;
let root = std::env::temp_dir().join(format!("codewhale-agent-runs-api-{}", Uuid::new_v4()));
+2 -2
View File
@@ -10010,7 +10010,7 @@ async fn worker_lifecycle_receipts_preserve_owner_outcome_and_durable_replay() -
use crate::tools::subagent::{
AgentWorkerSpec, AgentWorkerStatus, AgentWorkerToolProfile, FleetRole, SubAgentManager,
};
use crate::worker_profile::WorkerRuntimeProfile;
use codewhale_agent_contract::worker_profile::WorkerRuntimeProfile;
let runtime_dir = test_runtime_dir();
let manager = test_manager(runtime_dir.clone())?;
@@ -20792,7 +20792,7 @@ async fn notices_raise_from_engine_events_and_clear_on_settle_or_ack() -> Result
use crate::tools::subagent::{
AgentWorkerSpec, AgentWorkerToolProfile, FleetRole, SubAgentManager,
};
use crate::worker_profile::WorkerRuntimeProfile;
use codewhale_agent_contract::worker_profile::WorkerRuntimeProfile;
let manager = test_manager(test_runtime_dir())?;
let thread = manager
+1 -1
View File
@@ -728,7 +728,7 @@ mod tests {
)
.expect("automation manager");
let mut context = ToolContext::new(workspace.path());
context.shell_policy = crate::worker_profile::ShellPolicy::None;
context.shell_policy = codewhale_agent_contract::worker_profile::ShellPolicy::None;
context.runtime.automations = Some(std::sync::Arc::new(tokio::sync::Mutex::new(manager)));
let tool = AutomationTool::new("automation");
+1 -1
View File
@@ -2,7 +2,7 @@
//! **executes**, **mutates**, or **reaches the network**.
//!
//! Before this module the answer was spread across three hand-maintained name
//! lists ([`crate::fleet::role::RAW_SHELL_DENYLIST`] and its siblings) plus a
//! lists ([`codewhale_agent_contract::role::RAW_SHELL_DENYLIST`] and its siblings) plus a
//! role posture that keyed on `ShellPolicy::Full`. That shape had a structural
//! hole: a name list can only deny the execution primitives someone remembered
//! to write down, and `shell = "full"` was being read as "may run arbitrary
+9 -7
View File
@@ -664,7 +664,7 @@ pub(crate) fn enforce_tool_authority(
let canonical = super::canonical_action::canonical_action_alias(name, input);
if matches!(name, "bash" | "Bash" | "exec_shell") || canonical.starts_with("exec_shell") {
let prepared = tool.prepare(input.clone(), context)?;
if context.shell_policy == crate::worker_profile::ShellPolicy::None
if context.shell_policy == codewhale_agent_contract::worker_profile::ShellPolicy::None
|| prepared
.input
.get("action")
@@ -893,7 +893,7 @@ pub struct ToolRegistryBuilder {
/// gated behind feature flags or config state.
#[derive(Clone)]
pub struct AgentToolSurfaceOptions {
pub shell_policy: crate::worker_profile::ShellPolicy,
pub shell_policy: codewhale_agent_contract::worker_profile::ShellPolicy,
pub apply_patch_enabled: bool,
pub web_search_enabled: bool,
pub memory_tool_enabled: bool,
@@ -915,7 +915,7 @@ pub struct AgentToolSurfaceOptions {
impl AgentToolSurfaceOptions {
#[must_use]
pub fn new(shell_policy: crate::worker_profile::ShellPolicy) -> Self {
pub fn new(shell_policy: codewhale_agent_contract::worker_profile::ShellPolicy) -> Self {
Self {
shell_policy,
apply_patch_enabled: false,
@@ -1508,7 +1508,7 @@ impl ToolRegistryBuilder {
#[must_use]
pub fn with_agent_tools_policy(
self,
shell_policy: crate::worker_profile::ShellPolicy,
shell_policy: codewhale_agent_contract::worker_profile::ShellPolicy,
user_input_limits: super::user_input::UserInputLimits,
) -> Self {
let builder = self
@@ -1535,11 +1535,13 @@ impl ToolRegistryBuilder {
.with_finance_tool();
match shell_policy {
crate::worker_profile::ShellPolicy::Full => {
codewhale_agent_contract::worker_profile::ShellPolicy::Full => {
builder.with_shell_tools().with_runtime_task_shell_tools()
}
crate::worker_profile::ShellPolicy::ReadOnly => builder.with_read_only_shell_tool(),
crate::worker_profile::ShellPolicy::None => builder,
codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly => {
builder.with_read_only_shell_tool()
}
codewhale_agent_contract::worker_profile::ShellPolicy::None => builder,
}
}
+11 -11
View File
@@ -83,7 +83,7 @@ async fn shell_denial_reaches_registry_and_direct_delegation_sinks() {
#[test]
fn shell_denial_keeps_the_existing_bounded_child_read_only_exception() {
use crate::core::engine::tool_catalog::enforce_tool_denial;
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
let tmp = tempdir().unwrap();
let mut context = ToolContext::new(tmp.path()).with_shell_policy(ShellPolicy::ReadOnly);
context.disallowed_tools = vec!["Bash".into()];
@@ -669,7 +669,7 @@ fn builder_registers_speech_alias_tools() {
#[test]
fn agent_runtime_surface_skips_speech_without_a_client() {
use super::AgentToolSurfaceOptions;
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
let tmp = tempdir().expect("tempdir");
let ctx = ToolContext::new(tmp.path().to_path_buf());
let registry = ToolRegistryBuilder::new()
@@ -688,7 +688,7 @@ fn agent_runtime_surface_skips_speech_without_a_client() {
#[test]
fn model_visible_tool_descriptions_name_no_vendor() {
use super::AgentToolSurfaceOptions;
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
let tmp = tempdir().expect("tempdir");
let ctx = ToolContext::new(tmp.path().to_path_buf());
let mut options = AgentToolSurfaceOptions::new(ShellPolicy::Full);
@@ -1001,7 +1001,7 @@ fn machine_verifier_catalog_and_dispatch_add_only_bounded_run() {
let tmp = tempdir().expect("tempdir");
let registry = ToolRegistryBuilder::new()
.with_agent_tools_policy(
crate::worker_profile::ShellPolicy::None,
codewhale_agent_contract::worker_profile::ShellPolicy::None,
crate::tools::user_input::UserInputLimits::default(),
)
.with_web_tools()
@@ -1719,7 +1719,7 @@ fn with_verify_tool_registers_and_exposes_verify() {
#[test]
fn agent_runtime_surface_gates_verify_on_option() {
use super::AgentToolSurfaceOptions;
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
let build_surface = |verify_enabled: bool| {
let tmp = tempdir().expect("tempdir");
@@ -1750,7 +1750,7 @@ fn agent_runtime_surface_gates_verify_on_option() {
#[test]
fn agent_runtime_surface_gates_request_plugin_install_on_option() {
use super::AgentToolSurfaceOptions;
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
// Policy rule 11: hosts without the TUI (exec, runtime API) and sessions
// with contextual tips off get no plugin-offer tool in any mode.
@@ -1781,7 +1781,7 @@ fn test_builder_with_agent_tools_policy_includes_finance() {
let registry = ToolRegistryBuilder::new()
.with_agent_tools_policy(
crate::worker_profile::ShellPolicy::None,
codewhale_agent_contract::worker_profile::ShellPolicy::None,
crate::tools::user_input::UserInputLimits::default(),
)
.build(ctx);
@@ -1796,7 +1796,7 @@ fn agent_tools_with_shell_policy_none_excludes_shell_tools() {
let registry = ToolRegistryBuilder::new()
.with_agent_tools_policy(
crate::worker_profile::ShellPolicy::None,
codewhale_agent_contract::worker_profile::ShellPolicy::None,
crate::tools::user_input::UserInputLimits::default(),
)
.build(ctx);
@@ -1824,7 +1824,7 @@ fn agent_tools_with_shell_policy_readonly_exposes_only_run_only_bash() {
let registry = ToolRegistryBuilder::new()
.with_agent_tools_policy(
crate::worker_profile::ShellPolicy::ReadOnly,
codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly,
crate::tools::user_input::UserInputLimits::default(),
)
.build(ctx);
@@ -1881,7 +1881,7 @@ fn machine_readonly_catalog_is_exactly_the_evidence_profile() {
let tmp = tempdir().expect("tempdir");
let registry = ToolRegistryBuilder::new()
.with_agent_tools_policy(
crate::worker_profile::ShellPolicy::ReadOnly,
codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly,
crate::tools::user_input::UserInputLimits::default(),
)
.with_web_tools()
@@ -1977,7 +1977,7 @@ fn agent_tools_with_shell_policy_full_includes_shell_tools() {
let registry = ToolRegistryBuilder::new()
.with_agent_tools_policy(
crate::worker_profile::ShellPolicy::Full,
codewhale_agent_contract::worker_profile::ShellPolicy::Full,
crate::tools::user_input::UserInputLimits::default(),
)
.build(ctx);
+1 -1
View File
@@ -66,7 +66,7 @@ use crate::work_graph::{
EvidenceKind, EvidenceRef, OperationIntent, OperationOwnerSnapshot, OwnerState,
SharedWorkRuntime,
};
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
use codewhale_runtime::child_env;
use output::{
BoundedOutputAccumulator, BoundedOutputSnapshot, RAW_STREAM_SETTLED_TAIL_BYTES,
+16 -16
View File
@@ -538,7 +538,7 @@ async fn execution_receipt_keeps_spawn_cwd_when_the_command_retargets_the_worksp
async fn lowercase_bash_keeps_raw_command_under_readonly_policy() {
let workspace = tempdir().expect("workspace");
let context = ToolContext::new(workspace.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
let result = LowercaseBashTool
.execute(json!({"command": "pwd"}), &context)
.await
@@ -558,7 +558,7 @@ async fn lowercase_bash_keeps_raw_command_under_readonly_policy() {
async fn lowercase_bash_readonly_refusal_names_work_mode() {
let workspace = tempdir().expect("workspace");
let context = ToolContext::new(workspace.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
let error = LowercaseBashTool
.execute(json!({"command": "touch blocked-by-plan"}), &context)
.await
@@ -1134,7 +1134,7 @@ async fn readonly_shell_refuses_raw_string_external_backend() {
let tmp = tempdir().expect("tempdir");
let backend = std::sync::Arc::new(Backend(std::sync::atomic::AtomicBool::new(false)));
let mut context = ToolContext::new(tmp.path().to_path_buf())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
context.sandbox_backend = Some(backend.clone());
let error = BashTool::read_only("Bash")
.execute(json!({"action": "run", "command": "pwd"}), &context)
@@ -1253,8 +1253,8 @@ async fn readonly_git_reads_run_no_clean_filter_or_textconv() {
git(&["config", "diff.conv.textconv", &helper]);
std::fs::write(repo.join("c.txt"), "c2\n").expect("modify");
let ctx =
ToolContext::new(repo).with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
let ctx = ToolContext::new(repo)
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
let tool = BashTool::new("Bash");
for command in ["git diff", "git blame c.txt"] {
let result = tool
@@ -1494,7 +1494,7 @@ fn readonly_github_shell_calls_obey_the_host_network_policy_before_spawn() {
// Read-only agents: every admitted network segment is judged.
let readonly_deny = context(crate::network_policy::DecisionToml::Deny)
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
for command in ["gh pr view 1 | head", "ls && gh issue list"] {
let denied = enforce_readonly_network_reads(command, &readonly_deny)
.expect_err("a network read inside a composition is still judged")
@@ -1528,7 +1528,7 @@ fn exec_shell_interact_requires_approval() {
async fn read_only_shell_policy_blocks_non_readonly_commands() {
let tmp = tempdir().expect("tempdir");
let ctx = ToolContext::new(tmp.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
let tool = BashTool::new("Bash");
for (input, rule) in [
@@ -1572,7 +1572,7 @@ async fn read_only_refusal_names_child_alternatives_instead_of_mode_switch() {
// and only tools a read-only child actually has (#6015).
let tmp = tempdir().expect("tempdir");
let child_ctx = ToolContext::new(tmp.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly)
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly)
.with_owner_agent("agent_child", "child");
let tool = BashTool::new("Bash");
let message = tool
@@ -1595,7 +1595,7 @@ async fn read_only_refusal_names_child_alternatives_instead_of_mode_switch() {
assert!(!tmp.path().join("evil.txt").exists());
let parent_ctx = ToolContext::new(tmp.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
let message = tool
.execute(json!({"command": "cargo build"}), &parent_ctx)
.await
@@ -1642,7 +1642,7 @@ async fn read_only_shell_runs_chains_and_leading_cd() {
std::fs::write(workspace.path().join("sub").join("a.txt"), "alpha\n").expect("a");
std::fs::write(workspace.path().join("b.txt"), "beta\n").expect("b");
let ctx = ToolContext::new(workspace.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
let tool = BashTool::new("Bash");
let result = tool
.execute(json!({"command": "cd sub && ls"}), &ctx)
@@ -1683,7 +1683,7 @@ async fn read_only_shell_runs_chains_and_leading_cd() {
async fn read_only_operand_checks_apply_to_every_segment() {
let workspace = tempdir().expect("workspace");
let ctx = ToolContext::new(workspace.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
let error = BashTool::new("Bash")
.execute(json!({"command": "gh pr view 1 && cat /etc/passwd"}), &ctx)
.await
@@ -1705,7 +1705,7 @@ async fn read_only_shell_resolves_operands_from_the_effective_cwd() {
std::os::unix::fs::symlink(outside.path().join("secret"), subdir.join("secret-link"))
.expect("symlink");
let ctx = ToolContext::new(workspace.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
let error = BashTool::new("Bash")
.execute(
json!({"action": "run", "command": "cat secret-link", "cwd": "subdir"}),
@@ -1735,7 +1735,7 @@ async fn read_only_shell_skips_shell_env_hooks() {
tmp.path().to_path_buf(),
);
let mut context = ToolContext::new(tmp.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
context.runtime.hook_executor = Some(std::sync::Arc::new(executor));
let result = BashTool::read_only("Bash")
@@ -1750,7 +1750,7 @@ async fn read_only_shell_skips_shell_env_hooks() {
async fn read_only_shell_policy_allows_readonly_inspection() {
let tmp = tempdir().expect("tempdir");
let ctx = ToolContext::new(tmp.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
let result = BashTool::new("Bash")
.execute(json!({"command": "pwd"}), &ctx)
@@ -5470,7 +5470,7 @@ async fn readonly_pipeline_preserves_arguments_and_disables_git_helpers() {
std::fs::write(workspace.path().join(name), "option-shaped filename\n").unwrap();
}
let ctx = ToolContext::new(workspace.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
let tool = BashTool::new("Bash");
for command in ["sort * | cat", "sed -n 1p * | cat", "cat * | cat"] {
// #6675: a word-leading unquoted `*` is refused before anything runs
@@ -5520,7 +5520,7 @@ async fn readonly_sed_extra_options_never_mutate_files() {
let source = workspace.path().join("input.txt");
std::fs::write(&source, "first\nsecond\n").unwrap();
let ctx = ToolContext::new(workspace.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::ReadOnly);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly);
for command in [
"sed -n 1p input.txt -i",
"sed -n 1p -i.bak input.txt",
+1 -1
View File
@@ -27,7 +27,7 @@ use crate::rlm::session::{SharedRlmSessionStore, new_shared_rlm_session_store};
use crate::sandbox::backend::SandboxBackend;
use crate::tools::handle::{SharedHandleStore, new_shared_handle_store};
use crate::tools::shell::{SharedShellManager, new_shared_shell_manager};
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
use codewhale_runtime::features::Features;
#[allow(unused_imports)]
pub use codewhale_tools::{
+2 -2
View File
@@ -756,14 +756,14 @@ fn delegated_shell_is_refused_outright_when_the_session_lacks_it() {
// Leaving the flag out would fall back to the host default, which may
// allow shell: a session without full shell must store an explicit "no".
ctx.shell_policy = crate::worker_profile::ShellPolicy::None;
ctx.shell_policy = codewhale_agent_contract::worker_profile::ShellPolicy::None;
assert_eq!(ctx.cap_delegated_authority(None, None, None).0, Some(false));
assert_eq!(
ctx.cap_delegated_authority(Some(true), None, None).0,
Some(false)
);
ctx.shell_policy = crate::worker_profile::ShellPolicy::Full;
ctx.shell_policy = codewhale_agent_contract::worker_profile::ShellPolicy::Full;
assert_eq!(ctx.cap_delegated_authority(None, None, None).0, None);
assert_eq!(
ctx.cap_delegated_authority(Some(true), None, None).0,
+19 -17
View File
@@ -139,7 +139,7 @@ impl ChildAccountingProjection {
#[derive(Clone)]
pub(crate) struct ChildAuthority {
pub(crate) grant: crate::worker_profile::ChildGrant,
pub(crate) grant: codewhale_agent_contract::worker_profile::ChildGrant,
pub(super) disallowed_tools: Vec<String>,
pub(super) accept_edits: bool,
pub(crate) agent_type: FleetRole,
@@ -176,7 +176,7 @@ impl ChildAuthority {
let mut effective = runtime.worker_profile.clone();
effective.shell = effective.shell.min_with(parent_shell);
let scope = intersect_explicit_tool_scope(&effective.tools, explicit_allowed_tools);
let grant = crate::worker_profile::ChildGrant::resolve(
let grant = codewhale_agent_contract::worker_profile::ChildGrant::resolve(
&agent_type,
&effective,
scope,
@@ -364,7 +364,7 @@ impl ChildAuthority {
)));
}
let action = input.get("action").and_then(Value::as_str);
if self.grant.surface == crate::worker_profile::ToolSurface::Evidence
if self.grant.surface == codewhale_agent_contract::worker_profile::ToolSurface::Evidence
&& name == "Web"
&& !matches!(action, Some("search" | "fetch"))
{
@@ -574,7 +574,7 @@ impl ChildAuthority {
.is_some_and(|spec| match spec.approval_requirement_for(input) {
ApprovalRequirement::Auto => true,
ApprovalRequirement::Suggest => {
self.grant.files == crate::worker_profile::FileGrant::Write
self.grant.files == codewhale_agent_contract::worker_profile::FileGrant::Write
&& (self.accept_edits
|| Self::role_can_delegate_writes(&self.agent_type)
|| self.workspace_write_carve_out_permits(registry, name, input))
@@ -599,7 +599,7 @@ impl ChildAuthority {
// This is a bounded convenience for write-capable children, not an
// authority escalation: every target must resolve inside the workspace
// and still pass sensitive-path, repository-law, and claim checks.
if self.grant.files != crate::worker_profile::FileGrant::Write {
if self.grant.files != codewhale_agent_contract::worker_profile::FileGrant::Write {
return false;
}
crate::core::authority::paths_within_workspace_write_carve_out(
@@ -637,7 +637,7 @@ impl ChildAuthority {
) {
ApprovalRequirement::Auto => true,
ApprovalRequirement::Suggest => {
self.grant.files == crate::worker_profile::FileGrant::Write
self.grant.files == codewhale_agent_contract::worker_profile::FileGrant::Write
}
ApprovalRequirement::Required => {
// An outbound read needs approval because its payload can
@@ -674,7 +674,7 @@ impl ChildAuthority {
// `Verify` holds process-start authority for the bounded
// verification surface; the envelope below still refuses
// its ExecutesCode/WritesFiles calls by capability.
self.grant.shell >= crate::worker_profile::ShellGrant::Verify
self.grant.shell >= codewhale_agent_contract::worker_profile::ShellGrant::Verify
}
},
None => true,
@@ -689,7 +689,8 @@ impl ChildAuthority {
}
pub(crate) fn allows_bounded_readonly_bash(&self, name: &str) -> bool {
name == "bash" && self.grant.shell == crate::worker_profile::ShellGrant::Inspect
name == "bash"
&& self.grant.shell == codewhale_agent_contract::worker_profile::ShellGrant::Inspect
}
pub(crate) fn legacy_action_alias(family: &str, action: &str) -> Option<&'static str> {
@@ -759,7 +760,7 @@ impl ChildAuthority {
.is_some_and(|tool| crate::tools::registry::readonly_evidence_tool(tool.as_ref()));
// The Evidence surface admits only the hardened evidence tools and
// `agent` (delegation).
if self.grant.surface == crate::worker_profile::ToolSurface::Evidence
if self.grant.surface == codewhale_agent_contract::worker_profile::ToolSurface::Evidence
&& lower != "agent"
&& !evidence_tool
{
@@ -776,7 +777,7 @@ impl ChildAuthority {
)
|| lower.starts_with("terminal/");
raw_shell
&& self.grant.shell < crate::worker_profile::ShellGrant::Full
&& self.grant.shell < codewhale_agent_contract::worker_profile::ShellGrant::Full
&& !self.allows_bounded_readonly_bash(name)
}
@@ -785,11 +786,11 @@ impl ChildAuthority {
}
pub(crate) fn write_is_denied(&self) -> bool {
self.grant.files != crate::worker_profile::FileGrant::Write
self.grant.files != codewhale_agent_contract::worker_profile::FileGrant::Write
}
pub(crate) fn shell_is_denied(&self) -> bool {
self.grant.shell < crate::worker_profile::ShellGrant::Verify
self.grant.shell < codewhale_agent_contract::worker_profile::ShellGrant::Verify
}
pub(crate) fn execution_envelope(&self) -> crate::tools::execution_envelope::ExecutionEnvelope {
@@ -797,7 +798,7 @@ impl ChildAuthority {
// dispatch derive it from the same fields, never from a deny-list
// sentinel or a role re-mapping that could disagree with them.
crate::tools::execution_envelope::ExecutionEnvelope {
write: self.grant.files == crate::worker_profile::FileGrant::Write,
write: self.grant.files == codewhale_agent_contract::worker_profile::FileGrant::Write,
network: self.grant.network,
shell: !self.shell_is_denied(),
}
@@ -838,7 +839,7 @@ impl ChildAuthority {
if !matches!(action, "claim" | "release") {
return true;
}
self.grant.files == crate::worker_profile::FileGrant::Write
self.grant.files == codewhale_agent_contract::worker_profile::FileGrant::Write
&& !self.is_tool_denied("agents/coordinate")
}
@@ -846,7 +847,7 @@ impl ChildAuthority {
// Visibility and dispatch consult the same capability guard. These
// representative calls let a read-only bash schema survive catalog
// shaping without treating an empty input as arbitrary shell authority.
if self.grant.shell != crate::worker_profile::ShellGrant::Inspect {
if self.grant.shell != codewhale_agent_contract::worker_profile::ShellGrant::Inspect {
return None;
}
match name {
@@ -932,10 +933,11 @@ impl ChildAuthority {
return false;
};
let posture_allows = tool.name != "File"
|| self.grant.files == crate::worker_profile::FileGrant::Write
|| self.grant.files
== codewhale_agent_contract::worker_profile::FileGrant::Write
|| matches!(action, "read" | "list" | "search_name" | "search_content");
let evidence_action = self.grant.surface
!= crate::worker_profile::ToolSurface::Evidence
!= codewhale_agent_contract::worker_profile::ToolSurface::Evidence
|| tool.name != "Web"
|| matches!(action, "search" | "fetch");
let mut representative = self
+56 -57
View File
@@ -45,13 +45,6 @@ use crate::core::events::{AgentProgressEventMeta, Event};
#[cfg(test)]
use crate::core::session::ToolActivationCache;
use crate::dependencies::{ExternalTool, Git};
/// Compatibility re-export: the closed role set lives in
/// [`crate::fleet::role`], the lightweight role surface every spawn path
/// consumes. Existing `tools::subagent::FleetRole` paths keep resolving.
pub use crate::fleet::role::FleetRole;
use crate::fleet::role::{
FLEET_ROLE_SCHEMA_VALUES, VALID_ROLE_ALIASES, migrate_legacy_role_token, public_role_label,
};
use crate::llm_client::LlmError;
use crate::reasoning_preference::ReasoningEffort;
use crate::request_tuning::RequestTuning;
@@ -77,7 +70,14 @@ use crate::work_graph::{
EvidenceKind, EvidenceRef, OperationIntent, OperationOwnerSnapshot, OwnerState,
SharedWorkRuntime,
};
use crate::worker_profile::{
/// Compatibility re-export: the closed role set lives in
/// [`codewhale_agent_contract::role`], the lightweight role surface every spawn path
/// consumes. Existing `tools::subagent::FleetRole` paths keep resolving.
pub use codewhale_agent_contract::role::FleetRole;
use codewhale_agent_contract::role::{
FLEET_ROLE_SCHEMA_VALUES, VALID_ROLE_ALIASES, migrate_legacy_role_token, public_role_label,
};
use codewhale_agent_contract::worker_profile::{
ChildLaunchManifest, ModelRoute, ShellPolicy, ToolScope, WorkerRuntimeProfile,
narrow_model_steps, narrow_optional_limit,
};
@@ -581,46 +581,44 @@ impl SubAgentAssignment {
}
/// Role presentation: system prompts and config key lookup. The role itself
/// ([`FleetRole`], parsing, posture) lives in [`crate::fleet::role`]; this
/// impl stays on the agent tool because it renders prompt text the tool owns.
impl FleetRole {
/// Pre-Fleet model-override key (`explorer_model` / `ni_model` tables).
/// Not used for receipts or UI — only config key lookup.
#[must_use]
fn legacy_type_name(&self) -> &'static str {
match self {
Self::Worker => "general",
Self::Scout => "explore",
Self::Planner => "plan",
Self::Reviewer => "review",
Self::Builder => "implementer",
Self::Verifier => "verifier",
// Consultant is post-Fleet; it never had a pre-Fleet override table key.
Self::Consultant => "consultant",
Self::Custom => "custom",
}
/// ([`FleetRole`], parsing, posture) lives in [`codewhale_agent_contract::role`]; these
/// functions stay on the agent tool because they render prompt text the tool owns.
/// Pre-Fleet model-override key (`explorer_model` / `ni_model` tables).
/// Not used for receipts or UI — only config key lookup.
#[must_use]
fn role_legacy_type_name(role: &FleetRole) -> &'static str {
match role {
FleetRole::Worker => "general",
FleetRole::Scout => "explore",
FleetRole::Planner => "plan",
FleetRole::Reviewer => "review",
FleetRole::Builder => "implementer",
FleetRole::Verifier => "verifier",
// Consultant is post-Fleet; it never had a pre-Fleet override table key.
FleetRole::Consultant => "consultant",
FleetRole::Custom => "custom",
}
}
/// Get the system prompt for this Fleet role.
#[must_use]
pub fn system_prompt(&self) -> String {
let role_intro = match self {
Self::Worker => GENERAL_AGENT_INTRO,
Self::Scout => EXPLORE_AGENT_INTRO,
Self::Planner => PLAN_AGENT_INTRO,
Self::Reviewer => REVIEW_AGENT_INTRO,
Self::Builder => IMPLEMENTER_AGENT_INTRO,
Self::Verifier => VERIFIER_AGENT_INTRO,
Self::Consultant => CONSULTANT_AGENT_INTRO,
Self::Custom => CUSTOM_AGENT_INTRO,
};
match self {
Self::Scout => format!(
"{role_intro}{}",
crate::prompts::text::SUBAGENT_SCOUT_OUTPUT_FORMAT
),
_ => format!("{role_intro}{SUBAGENT_OUTPUT_FORMAT}"),
}
/// Get the system prompt for this Fleet role.
#[must_use]
pub fn role_system_prompt(role: &FleetRole) -> String {
let role_intro = match role {
FleetRole::Worker => GENERAL_AGENT_INTRO,
FleetRole::Scout => EXPLORE_AGENT_INTRO,
FleetRole::Planner => PLAN_AGENT_INTRO,
FleetRole::Reviewer => REVIEW_AGENT_INTRO,
FleetRole::Builder => IMPLEMENTER_AGENT_INTRO,
FleetRole::Verifier => VERIFIER_AGENT_INTRO,
FleetRole::Consultant => CONSULTANT_AGENT_INTRO,
FleetRole::Custom => CUSTOM_AGENT_INTRO,
};
match role {
FleetRole::Scout => format!(
"{role_intro}{}",
crate::prompts::text::SUBAGENT_SCOUT_OUTPUT_FORMAT
),
_ => format!("{role_intro}{SUBAGENT_OUTPUT_FORMAT}"),
}
}
@@ -9558,12 +9556,13 @@ impl SubAgentManager {
if let Some(record) = self.worker_records.get(&agent.id) {
snap.usage = Some(record.usage.clone());
snap.worker_status = Some(record.status);
snap.runtime_permissions = Some(crate::fleet::role::fleet_effective_permissions(
&record.spec.agent_type,
&record.spec.runtime_profile,
None,
None,
));
snap.runtime_permissions =
Some(codewhale_agent_contract::role::fleet_effective_permissions(
&record.spec.agent_type,
&record.spec.runtime_profile,
None,
None,
));
snap.parent_run_id = record
.parent_run_id
.clone()
@@ -13432,7 +13431,7 @@ fn verify_fleet_authority_input(expected: &str, input: &Value) -> Result<()> {
/// "You are operating in the role of `{name}`." as a final line so its
/// behavior reflects the user's choice.
fn build_subagent_system_prompt(agent_type: &FleetRole, assignment: &SubAgentAssignment) -> String {
let base = agent_type.system_prompt();
let base = role_system_prompt(&agent_type);
let mut prompt = match assignment.role.as_deref() {
Some(role) if !role.trim().is_empty() => {
format!(
@@ -16588,7 +16587,7 @@ pub(crate) fn session_permission_ceiling(
write: runtime.worker_profile.permissions.write,
network_tool: runtime.worker_profile.permissions.network
&& runtime.agent_tool_surface_options.web_search_enabled,
shell: crate::fleet::role::session_shell_ceiling(
shell: codewhale_agent_contract::role::session_shell_ceiling(
runtime.worker_profile.shell,
runtime.allow_shell,
),
@@ -17850,8 +17849,8 @@ pub(crate) fn configured_role_model_override<'a>(
push_key(normalized);
}
push_key(agent_type.as_str().to_string());
if agent_type.legacy_type_name() != agent_type.as_str() {
push_key(agent_type.legacy_type_name().to_string());
if role_legacy_type_name(&agent_type) != agent_type.as_str() {
push_key(role_legacy_type_name(&agent_type).to_string());
}
// `[subagents.models].oracle` shipped before the public role was renamed.
// Keep both historical advisory keys readable, but never expose them in
@@ -18719,7 +18718,7 @@ fn reject_network_reaching_input(name: &str, input: &Value) -> Result<()> {
/// verification at all — `run_verifiers` takes `commands`, an array of arbitrary
/// `program` + `args` pairs, and `run_tests` takes `args`, a raw cargo argv.
/// `{"program": "bash", "args": ["-lc", "rm -rf src"]}` is exactly the raw shell
/// that [`crate::fleet::role::RAW_SHELL_DENYLIST`] just removed, re-entered
/// that [`codewhale_agent_contract::role::RAW_SHELL_DENYLIST`] just removed, re-entered
/// through the one door that was left open for honest reasons.
///
/// So the tools stay and the arbitrary arguments go. The default form — the one
+1 -1
View File
@@ -507,7 +507,7 @@ pub(crate) fn subagent_role_label(
.or_else(|| non_empty(agent_type.as_str()))
.map_or_else(
|| "agent".to_string(),
|role| crate::fleet::role::public_role_label(&role),
|role| codewhale_agent_contract::role::public_role_label(&role),
)
}
+54 -45
View File
@@ -2,8 +2,8 @@ use super::*;
use crate::config::ProviderKind;
use crate::fleet::roster::FleetRoster;
use crate::tools::{AgentToolSurfaceOptions, ToolRegistryBuilder};
use crate::worker_profile::ShellPolicy;
use axum::{Json, Router, http::StatusCode, response::IntoResponse, routing::post};
use codewhale_agent_contract::worker_profile::ShellPolicy;
use std::collections::HashSet;
use std::process::Command;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
@@ -744,7 +744,7 @@ async fn explicit_read_only_general_can_inspect_git_but_cannot_mutate() {
assert!(registry.unavailable_allowed_tools().is_empty());
assert_ne!(
registry.grant.files,
crate::worker_profile::FileGrant::Write
codewhale_agent_contract::worker_profile::FileGrant::Write
);
for command in ["pwd", "git status --short", "git log --oneline -1"] {
let output = registry
@@ -3392,9 +3392,9 @@ fn test_implementer_and_verifier_have_distinct_prompts() {
// The whole point of adding the types is that they carry distinct
// posture. Defensive guard: catch the easy bug where copy-paste
// leaves two new variants with the same prompt as `Worker`.
let implementer = FleetRole::Builder.system_prompt();
let verifier = FleetRole::Verifier.system_prompt();
let general = FleetRole::Worker.system_prompt();
let implementer = role_system_prompt(&FleetRole::Builder);
let verifier = role_system_prompt(&FleetRole::Verifier);
let general = role_system_prompt(&FleetRole::Worker);
assert_ne!(
implementer, general,
"Implementer prompt must differ from General"
@@ -3434,7 +3434,7 @@ fn test_agent_type_prompts_include_shared_output_contract_once() {
(FleetRole::Verifier, "Fleet verifier"),
(FleetRole::Custom, "custom Fleet worker"),
] {
let prompt = agent_type.system_prompt();
let prompt = role_system_prompt(&agent_type);
assert!(prompt.contains(marker));
// Every role shares the parseable output-contract spine exactly once.
assert_eq!(
@@ -3467,7 +3467,7 @@ fn test_agent_type_prompts_include_shared_output_contract_once() {
#[test]
fn explore_prompt_orients_before_searching() {
let prompt = FleetRole::Scout.system_prompt();
let prompt = role_system_prompt(&FleetRole::Scout);
assert!(prompt.contains("role: `scout`"));
assert!(prompt.contains("AGENTS.md/README"));
assert!(prompt.contains("workspace/project root"));
@@ -3476,7 +3476,7 @@ fn explore_prompt_orients_before_searching() {
#[test]
fn explore_prompt_requires_evidence_and_read_only_scope() {
let prompt = FleetRole::Scout.system_prompt();
let prompt = role_system_prompt(&FleetRole::Scout);
assert!(prompt.contains("Gather enough evidence to answer the assigned question reliably"));
assert!(prompt.contains("there is no per-agent call cap to save"));
assert!(prompt.contains("strictly read-only"));
@@ -3489,14 +3489,14 @@ fn explore_prompt_requires_evidence_and_read_only_scope() {
assert!(prompt.contains("allowed read-only inspection subset"));
assert!(prompt.contains("`git log -n 5`"));
assert!(!prompt.contains("use RLM"));
let reviewer = FleetRole::Reviewer.system_prompt();
let reviewer = role_system_prompt(&FleetRole::Reviewer);
assert!(reviewer.contains("allowed read-only navigation/rg"));
assert!(reviewer.contains("shell control actions are unavailable"));
}
#[test]
fn implementer_prompt_is_not_forced_into_explorer_cap() {
let prompt = FleetRole::Builder.system_prompt();
let prompt = role_system_prompt(&FleetRole::Builder);
assert!(!prompt.contains("3-5 tool-call cap"));
assert!(prompt.contains("Checkpoint before expanding scope"));
assert!(!prompt.contains("Default to `EFFORT: quick`"));
@@ -3504,11 +3504,11 @@ fn implementer_prompt_is_not_forced_into_explorer_cap() {
#[test]
fn role_prompts_use_lowercase_primitive_contract() {
let explore = FleetRole::Scout.system_prompt();
let explore = role_system_prompt(&FleetRole::Scout);
assert!(explore.contains("Use `read` for bounded file reads"));
assert!(explore.contains("`bash` only for the allowed read-only"));
let implementer = FleetRole::Builder.system_prompt();
let implementer = role_system_prompt(&FleetRole::Builder);
assert!(implementer.contains("Use `edit` for precise unique replacements"));
assert!(implementer.contains("`write` for whole-file changes"));
assert!(implementer.contains("discover `apply_patch`"));
@@ -3525,8 +3525,8 @@ fn role_prompts_use_lowercase_primitive_contract() {
#[test]
fn review_and_verifier_prompts_stop_after_decisive_evidence() {
let review = FleetRole::Reviewer.system_prompt();
let verifier = FleetRole::Verifier.system_prompt();
let review = role_system_prompt(&FleetRole::Reviewer);
let verifier = role_system_prompt(&FleetRole::Verifier);
assert!(review.contains("stop after decisive evidence"));
assert!(review.contains("private `todo_write` list as editable working notes"));
assert!(verifier.contains("stop after decisive pass/fail evidence"));
@@ -7552,7 +7552,9 @@ fn role_grants_block_writes_and_raw_shell_for_read_only_roles() {
// never hold write authority, regardless of parent auto-approval, and the
// bounded shell grants are exactly Inspect (evidence reads) and Verify
// (the workspace's own checks).
use crate::worker_profile::{ChildGrant, FileGrant, ShellGrant, ToolSurface};
use codewhale_agent_contract::worker_profile::{
ChildGrant, FileGrant, ShellGrant, ToolSurface,
};
let full_parent = WorkerRuntimeProfile::for_role(FleetRole::Worker);
for role in [
@@ -7613,7 +7615,7 @@ fn role_grants_block_writes_and_raw_shell_for_read_only_roles() {
/// visible. Asserted against the real child registry.
#[tokio::test]
async fn issue_5633_catalog_and_dispatch_are_one_grant() {
use crate::worker_profile::{FileGrant, ShellGrant};
use codewhale_agent_contract::worker_profile::{FileGrant, ShellGrant};
for role in FleetRole::all() {
let tmp = tempdir().expect("tempdir");
let mut runtime =
@@ -8240,10 +8242,11 @@ fn an_explicit_parent_tool_scope_is_enforced_by_the_child_registry() {
let mut runtime =
stub_runtime().with_agent_tool_surface_options(enabled_agent_surface_options());
runtime.context = ToolContext::new(tmp.path().to_path_buf());
runtime.worker_profile.tools = crate::worker_profile::ToolScope::Explicit(vec![
"read_file".to_string(),
"grep_files".to_string(),
]);
runtime.worker_profile.tools =
codewhale_agent_contract::worker_profile::ToolScope::Explicit(vec![
"read_file".to_string(),
"grep_files".to_string(),
]);
let registry = ChildCoreProbe::new(
runtime,
@@ -8274,7 +8277,7 @@ fn an_explicit_parent_tool_scope_is_enforced_by_the_child_registry() {
/// outside it, however it asks.
#[test]
fn a_child_allowlist_cannot_widen_an_explicit_parent_tool_scope() {
let parent = crate::worker_profile::ToolScope::Explicit(vec![
let parent = codewhale_agent_contract::worker_profile::ToolScope::Explicit(vec![
"read_file".to_string(),
"File".to_string(),
]);
@@ -8302,13 +8305,16 @@ fn a_child_allowlist_cannot_widen_an_explicit_parent_tool_scope() {
// An unrestricted parent leaves the child's request untouched.
assert_eq!(
intersect_explicit_tool_scope(
&crate::worker_profile::ToolScope::Inherit,
&codewhale_agent_contract::worker_profile::ToolScope::Inherit,
Some(vec!["exec_shell".to_string()])
),
Some(vec!["exec_shell".to_string()]),
);
assert_eq!(
intersect_explicit_tool_scope(&crate::worker_profile::ToolScope::Inherit, None),
intersect_explicit_tool_scope(
&codewhale_agent_contract::worker_profile::ToolScope::Inherit,
None
),
None
);
}
@@ -8966,7 +8972,7 @@ fn subagent_feature_gates_match_parent_agent_surface() {
/// deny list via `allows_bounded_readonly_bash`, so seeding the rule is what
/// makes these tests exercise the carve-out rather than an absent denial.
fn seed_read_only_role_deny_list(runtime: &mut SubAgentRuntime) {
use crate::fleet::role::{MUTATING_TOOL_DENYLIST, RAW_SHELL_DENYLIST};
use codewhale_agent_contract::role::{MUTATING_TOOL_DENYLIST, RAW_SHELL_DENYLIST};
for rule in RAW_SHELL_DENYLIST
.iter()
.chain(MUTATING_TOOL_DENYLIST.iter())
@@ -21078,7 +21084,7 @@ async fn child_work_state_publishes_only_real_changes_from_its_own_list() {
#[test]
fn an_exact_member_with_tools_false_gets_no_model_tools_at_all() {
let tmp = tempdir().expect("tempdir");
let authority = crate::fleet::role::ChildAuthority::clamp(
let authority = codewhale_agent_contract::role::ChildAuthority::clamp(
codewhale_workflow::PermissionCeiling::ROUTER,
codewhale_workflow::PermissionCeiling::preset("full").expect("preset"),
);
@@ -21116,7 +21122,7 @@ fn an_exact_member_with_tools_false_gets_no_model_tools_at_all() {
#[tokio::test]
async fn an_exact_member_without_a_network_tool_really_loses_the_network_surface() {
let tmp = tempdir().expect("tempdir");
let authority = crate::fleet::role::ChildAuthority::clamp(
let authority = codewhale_agent_contract::role::ChildAuthority::clamp(
codewhale_workflow::PermissionCeiling::preset("read_write").expect("preset"),
codewhale_workflow::PermissionCeiling::preset("full").expect("preset"),
);
@@ -21304,7 +21310,8 @@ async fn a_read_only_inspection_member_gets_only_bounded_web_search() {
delegation_depth: codewhale_config::DEFAULT_SPAWN_DEPTH,
tools: true,
};
let authority = crate::fleet::role::ChildAuthority::from_runtime_role("scout", parent);
let authority =
codewhale_agent_contract::role::ChildAuthority::from_runtime_role("scout", parent);
assert_eq!(authority.posture_role, "explore");
assert!(!authority.ceiling.network_tool);
@@ -21369,7 +21376,7 @@ async fn a_read_only_inspection_member_gets_only_bounded_web_search() {
);
// A Runtime builder under a full parent keeps the whole family.
let full_authority = crate::fleet::role::ChildAuthority::from_runtime_role(
let full_authority = codewhale_agent_contract::role::ChildAuthority::from_runtime_role(
"builder",
codewhale_workflow::PermissionCeiling::preset("full").expect("preset"),
);
@@ -21412,7 +21419,7 @@ async fn a_read_only_inspection_member_gets_only_bounded_web_search() {
#[test]
fn the_unified_rlm_action_cannot_bypass_a_denied_alias() {
let tmp = tempdir().expect("tempdir");
let authority = crate::fleet::role::ChildAuthority::clamp(
let authority = codewhale_agent_contract::role::ChildAuthority::clamp(
codewhale_workflow::PermissionCeiling::preset("read_write").expect("preset"),
codewhale_workflow::PermissionCeiling::preset("full").expect("preset"),
);
@@ -21664,7 +21671,7 @@ async fn a_parent_read_only_session_narrows_a_full_exact_member_in_the_child_reg
delegation_depth: 0,
tools: true,
};
let authority = crate::fleet::role::ChildAuthority::clamp(
let authority = codewhale_agent_contract::role::ChildAuthority::clamp(
codewhale_workflow::PermissionCeiling::preset("full").expect("preset"),
session,
);
@@ -21772,7 +21779,7 @@ fn the_session_ceiling_reflects_the_live_parent_posture() {
/// The child registry a Runtime verifier under a full parent actually runs with.
fn read_only_with_shell_registry() -> (tempfile::TempDir, ChildCoreProbe) {
let tmp = tempdir().expect("tempdir");
let authority = crate::fleet::role::ChildAuthority::from_runtime_role(
let authority = codewhale_agent_contract::role::ChildAuthority::from_runtime_role(
"verifier",
codewhale_workflow::PermissionCeiling::preset("full").expect("preset"),
);
@@ -21832,7 +21839,7 @@ fn shell_denial_from_parent_is_not_a_read_only_role_exception() {
if explicit_rule.is_some() {
assert_eq!(
registry.grant.shell,
crate::worker_profile::ShellGrant::None
codewhale_agent_contract::worker_profile::ShellGrant::None
);
assert!(!registry.is_tool_allowed("bash"));
}
@@ -22072,7 +22079,7 @@ fn bounded_read_only_and_verification_paths_survive_the_ceiling() {
#[test]
fn a_write_capable_member_keeps_every_execution_gate() {
let tmp = tempdir().expect("tempdir");
let authority = crate::fleet::role::ChildAuthority::from_runtime_role(
let authority = codewhale_agent_contract::role::ChildAuthority::from_runtime_role(
"builder",
codewhale_workflow::PermissionCeiling::preset("full").expect("preset"),
);
@@ -22148,7 +22155,7 @@ fn the_durable_work_families_resolve_their_actions_through_the_policy_seam() {
/// Legacy opt-out inputs cannot relax role or operator ceilings.
#[test]
fn posture_denials_survive_a_child_that_declines_to_inherit() {
let authority = crate::fleet::role::ChildAuthority::clamp(
let authority = codewhale_agent_contract::role::ChildAuthority::clamp(
codewhale_workflow::PermissionCeiling::preset("read_write").expect("preset"),
codewhale_workflow::PermissionCeiling::preset("full").expect("preset"),
);
@@ -22183,12 +22190,12 @@ fn posture_denials_survive_a_child_that_declines_to_inherit() {
"operator rules also remain a ceiling; got {child:?}"
);
assert!(
!crate::fleet::role::is_posture_denial("some_session_preference"),
!codewhale_agent_contract::role::is_posture_denial("some_session_preference"),
"only ceiling-derived rules are sealed"
);
for name in crate::fleet::role::NON_SHELL_EXECUTION_DENYLIST {
for name in codewhale_agent_contract::role::NON_SHELL_EXECUTION_DENYLIST {
assert!(
crate::fleet::role::is_posture_denial(name),
codewhale_agent_contract::role::is_posture_denial(name),
"{name} is installed by a ceiling and must be sealed"
);
}
@@ -22203,7 +22210,8 @@ fn posture_denials_survive_a_child_that_declines_to_inherit() {
fn the_authority_fingerprint_distinguishes_every_envelope_it_names() {
let session = codewhale_workflow::PermissionCeiling::preset("full").expect("preset");
let fingerprint = |role: &str| {
crate::fleet::role::ChildAuthority::from_runtime_role(role, session).fingerprint()
codewhale_agent_contract::role::ChildAuthority::from_runtime_role(role, session)
.fingerprint()
};
let mut seen = HashSet::new();
@@ -22227,7 +22235,7 @@ fn the_authority_fingerprint_distinguishes_every_envelope_it_names() {
assert_eq!(fingerprint("verifier"), fingerprint("verifier"));
// The parent posture is part of it: the same Runtime role under a narrower
// parent is a different envelope.
let narrow = crate::fleet::role::ChildAuthority::from_runtime_role(
let narrow = codewhale_agent_contract::role::ChildAuthority::from_runtime_role(
"builder",
codewhale_workflow::PermissionCeiling::preset("read_only").expect("preset"),
);
@@ -22239,7 +22247,7 @@ fn the_authority_fingerprint_distinguishes_every_envelope_it_names() {
/// is not enforced authority.
#[test]
fn the_spawn_boundary_fails_closed_on_a_missing_or_mismatched_authority() {
let authority = crate::fleet::role::ChildAuthority::from_runtime_role(
let authority = codewhale_agent_contract::role::ChildAuthority::from_runtime_role(
"verifier",
codewhale_workflow::PermissionCeiling::preset("full").expect("preset"),
);
@@ -22301,7 +22309,7 @@ fn the_spawn_boundary_fails_closed_on_a_missing_or_mismatched_authority() {
/// and that fingerprint is what the spawn boundary accepts.
#[test]
fn the_launched_authority_is_the_one_the_spawn_boundary_accepts() {
let authority = crate::fleet::role::ChildAuthority::from_runtime_role(
let authority = codewhale_agent_contract::role::ChildAuthority::from_runtime_role(
"auditor",
codewhale_workflow::PermissionCeiling::preset("analyst").expect("preset"),
);
@@ -22323,7 +22331,7 @@ fn the_launched_authority_is_the_one_the_spawn_boundary_accepts() {
.expect("the launched envelope must satisfy its own receipt");
// A different member's envelope must not satisfy it.
let other = crate::fleet::role::ChildAuthority::from_runtime_role(
let other = codewhale_agent_contract::role::ChildAuthority::from_runtime_role(
"builder",
codewhale_workflow::PermissionCeiling::preset("full").expect("preset"),
);
@@ -26507,9 +26515,10 @@ fn agent_tool_description_names_only_schema_roles() {
// sentence must be a value the `type` enum accepts; the legacy aliases
// parse at the deserialization boundary but a strict gateway rejects
// them against the declared enum (#5940).
let schema: std::collections::HashSet<&str> = crate::fleet::role::FLEET_ROLE_SCHEMA_VALUES
.into_iter()
.collect();
let schema: std::collections::HashSet<&str> =
codewhale_agent_contract::role::FLEET_ROLE_SCHEMA_VALUES
.into_iter()
.collect();
let texts = [
super::AGENT_TOOL_DESCRIPTION.as_str(),
super::SUBAGENT_TYPE_DESCRIPTION,
@@ -760,7 +760,7 @@ async fn assert_admitted_route(
assert_eq!(manifest.profile.role, spec.agent_type);
assert_eq!(
manifest.profile.model,
crate::worker_profile::ModelRoute::Fixed(spec.model.clone())
codewhale_agent_contract::worker_profile::ModelRoute::Fixed(spec.model.clone())
);
assert_eq!(
manifest.profile.provider.as_deref(),
+3 -3
View File
@@ -681,7 +681,7 @@ impl TasksTool {
context: &ToolContext,
) -> Result<ToolResult, ToolError> {
crate::core::engine::tool_catalog::enforce_tool_denial(context, "task_gate_run", input)?;
if context.shell_policy != crate::worker_profile::ShellPolicy::Full {
if context.shell_policy != codewhale_agent_contract::worker_profile::ShellPolicy::Full {
return Err(ToolError::permission_denied(
"Gate commands require full shell permission.",
));
@@ -1804,7 +1804,7 @@ mod tests {
let mut context = ToolContext::new(workspace.path());
context.approval_mode = codewhale_execpolicy::ApprovalMode::Auto;
context.shell_policy = crate::worker_profile::ShellPolicy::None;
context.shell_policy = codewhale_agent_contract::worker_profile::ShellPolicy::None;
context.runtime.task_manager = Some(manager.clone());
TasksTool::new("tasks")
@@ -1865,7 +1865,7 @@ mod tests {
async fn timed_out_gate_run_kills_the_gate_process_tree() {
let workspace = tempfile::tempdir().expect("workspace");
let context = ToolContext::new(workspace.path())
.with_shell_policy(crate::worker_profile::ShellPolicy::Full);
.with_shell_policy(codewhale_agent_contract::worker_profile::ShellPolicy::Full);
let result = TasksTool::new("tasks")
.execute(
json!({
+1 -1
View File
@@ -986,7 +986,7 @@ fn timeout_secs(input: &serde_json::Value, key: &str) -> Result<Duration, ToolEr
fn shell_allowed(context: &ToolContext, name: &str) -> Result<(), ToolError> {
crate::core::engine::tool_catalog::enforce_tool_denial(context, name, &json!({}))?;
if matches!(name, "terminal/run" | "terminal/send" | "terminal/reset")
&& context.shell_policy != crate::worker_profile::ShellPolicy::Full
&& context.shell_policy != codewhale_agent_contract::worker_profile::ShellPolicy::Full
{
return Err(ToolError::permission_denied(
"Persistent terminal execution and input require full shell permission.",
+1 -1
View File
@@ -16,7 +16,7 @@ use super::guard::{
DnsPin, guarded_reqwest_client_builder, validate_fetch_target, validate_network_policy,
};
use crate::tools::spec::{ToolContext, ToolError};
use crate::worker_profile::ShellPolicy;
use codewhale_agent_contract::worker_profile::ShellPolicy;
use codewhale_runtime::features::Feature;
pub(crate) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
+1 -1
View File
@@ -35,7 +35,6 @@ use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::core::events::Event;
use crate::fleet::role::{FleetRole, public_role_label};
use crate::tools::spec::{
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
optional_bool, optional_str, optional_u64,
@@ -55,6 +54,7 @@ use crate::work_graph::{
CancelOutcome, EvidenceKind, EvidenceRef, OperationIntent, OperationObservation,
OperationOwnerSnapshot, OwnerState, SharedWorkRuntime,
};
use codewhale_agent_contract::role::{FleetRole, public_role_label};
/// Keep promoted artifacts compact without clipping ordinary evidence reports.
/// A 900-character cap cut six-line source receipts in half during live Fleet
@@ -433,8 +433,14 @@ async fn native_exact_fleet_builder_keeps_the_plan_read_only_ceiling() {
!profile.permissions.write,
"the authored read_only mode must remain executable policy"
);
assert_eq!(profile.shell, crate::worker_profile::ShellPolicy::ReadOnly);
assert_eq!(profile.tools, crate::worker_profile::ToolScope::Inherit);
assert_eq!(
profile.shell,
codewhale_agent_contract::worker_profile::ShellPolicy::ReadOnly
);
assert_eq!(
profile.tools,
codewhale_agent_contract::worker_profile::ToolScope::Inherit
);
assert_eq!(
records[0].spec.child_route.as_ref().unwrap().provider_id,
"openrouter"
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::tools::subagent::{
AgentRunUsage, AgentWorkerEvent, AgentWorkerRecord, AgentWorkerSpec, AgentWorkerStatus,
AgentWorkerToolProfile, FleetRole,
};
use crate::worker_profile::WorkerRuntimeProfile;
use codewhale_agent_contract::worker_profile::WorkerRuntimeProfile;
fn usage_none() -> AgentRunUsage {
AgentRunUsage {
+1 -1
View File
@@ -24,7 +24,6 @@ use ratatui::{
};
use crate::config::Config;
use crate::fleet::role::public_role_label;
use crate::fleet::store::{
FleetFile, FleetMember, FleetOperator, FleetScope, MemberCapability, load_fleet_in_scope,
save_fleet, set_selected,
@@ -34,6 +33,7 @@ use crate::tui::menu_style;
use crate::tui::views::{
ActionHint, ModalKind, ModalView, ViewAction, ViewEvent, render_modal_footer,
};
use codewhale_agent_contract::role::public_role_label;
use codewhale_localization::{Locale, MessageId, tr};
use codewhale_palette as palette;
+2 -2
View File
@@ -36,11 +36,11 @@ use ratatui::{
use crate::config::Config;
use crate::fleet::profile::AgentProfile;
use crate::fleet::role::public_role_label;
use crate::fleet::roster::{FleetRoster, ProfileLayer, ProfileOrigin, layers_from_parts};
use crate::fleet::worker_runtime::roster_member_agent_type;
use crate::tui::app::App;
use crate::tui::menu_style;
use codewhale_agent_contract::role::public_role_label;
/// Rows one PageUp/PageDown travels. Pages clamp at the ends per the shared
/// vocabulary instead of wrapping (#6290).
@@ -50,7 +50,7 @@ use crate::tui::views::{
truncate_view_text,
};
use crate::tui::whales;
use crate::worker_profile::{ShellPolicy, WorkerRuntimeProfile};
use codewhale_agent_contract::worker_profile::{ShellPolicy, WorkerRuntimeProfile};
use codewhale_localization::{Locale, MessageId, tr};
use codewhale_palette as palette;
+1 -1
View File
@@ -40,13 +40,13 @@ use ratatui::{
use crate::config::Config;
use crate::fleet::profile::FleetProfileScope;
use crate::fleet::role::public_role_label;
use crate::tui::app::App;
use crate::tui::menu_style;
use crate::tui::views::{
ActionHint, ModalKind, ModalView, ViewAction, ViewEvent, centered_modal_area,
render_modal_footer_with_gutter, render_modal_surface, truncate_view_text,
};
use codewhale_agent_contract::role::public_role_label;
use codewhale_localization::{MessageId, tr};
use codewhale_palette as palette;
+1 -1
View File
@@ -18,12 +18,12 @@ use std::time::Instant;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use crate::fleet::role::public_role_label;
use crate::todo_snapshot::{TodoCardProjection, card_omission_line, card_todo_projection};
use crate::tools::subagent::MailboxMessage;
use crate::tools::todo::TodoListSnapshot;
use crate::tui::ui_text::truncate_line_to_width;
use crate::tui::widgets::tool_card::{ToolFamily, family_glyph};
use codewhale_agent_contract::role::public_role_label;
use codewhale_palette as palette;
use unicode_width::UnicodeWidthStr;
+1
View File
@@ -16,6 +16,7 @@ release_crates=(
codewhale-hooks
codewhale-tools
codewhale-config
codewhale-agent-contract
codewhale-cloud-facts
# Path+version dependency of cli/tui — must publish before those crates.
codewhale-telemetry