mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
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:
1 parent
0ae2655d22
commit
8a33bc0179
53 files changed
+347
-279
No files matched your search
Generated
+13
@@ -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,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/agent",
|
||||
"crates/agent-contract",
|
||||
"crates/app-server",
|
||||
"crates/build-support",
|
||||
"crates/cli",
|
||||
|
||||
@@ -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
|
||||
@@ -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),
|
||||
@@ -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" }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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!({
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user