mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
feat(tui): state a child's effective posture and document inheritance
The focused worker header now names write, network, and shell from the runtime permission snapshot, never guessed. New AgentFocusPosture strings are translated in every complete locale pack. Docs and the unreleased changelog describe the session-posture gate (Ask / Auto-Review / Full Access) and the role defaults that keep network reads and planner probes without widening write-only roles.
This commit is contained in:
1 parent
317110da25
commit
43ffca8561
21 files changed
+335
-28
No files matched your search
@@ -9,6 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Children (sub-agents and Fleet workers) inherit the session's permission
|
||||
posture faithfully: Auto-Review's deterministic floor and model guardian
|
||||
decide a worker's held calls (fail closed when unavailable, never a
|
||||
prompt); under Ask a held call is raised in the parent's approval UI and
|
||||
the worker waits visibly; Full Access still fails closed on the safety
|
||||
floor. Each prompt-less decision is a one-line note in that worker's
|
||||
transcript (focus mode) and an audit-log record.
|
||||
- Worker role defaults keep what the role does not intend to withhold:
|
||||
every built-in role keeps network reads; `planner` may run read-only
|
||||
shell probes; `custom` inherits the parent's write/network/shell posture
|
||||
and is narrowed only by its explicit tool list or the spawning call.
|
||||
Read-only roles (`scout`, `reviewer`, `planner`, `verifier`,
|
||||
`consultant`) still never write the workspace. The focused worker's
|
||||
header states its effective posture from the runtime snapshot.
|
||||
- Whale Teams in the terminal: the six Signal Cut whale identities (Scout,
|
||||
Patch, Harbor, Echo, Keel, Lantern) appear as species badges on `/fleet`
|
||||
roster rows and worker rows, with an identity portrait in the roster detail
|
||||
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "No s'ha pogut lliurar a {agent}: {reason}",
|
||||
"FooterHintForAgents": "agents",
|
||||
"FooterHintToManage": "gestionar",
|
||||
"AgentRailQueuedCount": "{count} a la cua"
|
||||
"AgentRailQueuedCount": "{count} a la cua",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "escriu",
|
||||
"AgentFocusPostureReadOnly": "només lectura",
|
||||
"AgentFocusPostureNetwork": "xarxa",
|
||||
"AgentFocusPostureNoNetwork": "sense xarxa",
|
||||
"AgentFocusPostureShellFull": "shell",
|
||||
"AgentFocusPostureShellReadOnly": "shell de només lectura",
|
||||
"AgentFocusPostureShellNone": "sense shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "Zustellung an {agent} nicht möglich: {reason}",
|
||||
"FooterHintForAgents": "Agenten",
|
||||
"FooterHintToManage": "verwalten",
|
||||
"AgentRailQueuedCount": "{count} eingereiht"
|
||||
"AgentRailQueuedCount": "{count} eingereiht",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "schreibt",
|
||||
"AgentFocusPostureReadOnly": "nur lesen",
|
||||
"AgentFocusPostureNetwork": "Netzwerk",
|
||||
"AgentFocusPostureNoNetwork": "kein Netzwerk",
|
||||
"AgentFocusPostureShellFull": "Shell",
|
||||
"AgentFocusPostureShellReadOnly": "Nur-Lese-Shell",
|
||||
"AgentFocusPostureShellNone": "keine Shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "Could not deliver to {agent}: {reason}",
|
||||
"FooterHintForAgents": "for agents",
|
||||
"FooterHintToManage": "to manage",
|
||||
"AgentRailQueuedCount": "{count} queued"
|
||||
"AgentRailQueuedCount": "{count} queued",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "writes",
|
||||
"AgentFocusPostureReadOnly": "read-only",
|
||||
"AgentFocusPostureNetwork": "network",
|
||||
"AgentFocusPostureNoNetwork": "no network",
|
||||
"AgentFocusPostureShellFull": "shell",
|
||||
"AgentFocusPostureShellReadOnly": "read-only shell",
|
||||
"AgentFocusPostureShellNone": "no shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "No se pudo entregar a {agent}: {reason}",
|
||||
"FooterHintForAgents": "agentes",
|
||||
"FooterHintToManage": "gestionar",
|
||||
"AgentRailQueuedCount": "{count} en cola"
|
||||
"AgentRailQueuedCount": "{count} en cola",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "escribe",
|
||||
"AgentFocusPostureReadOnly": "solo lectura",
|
||||
"AgentFocusPostureNetwork": "red",
|
||||
"AgentFocusPostureNoNetwork": "sin red",
|
||||
"AgentFocusPostureShellFull": "shell",
|
||||
"AgentFocusPostureShellReadOnly": "shell de solo lectura",
|
||||
"AgentFocusPostureShellNone": "sin shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "Impossible de transmettre à {agent} : {reason}",
|
||||
"FooterHintForAgents": "agents",
|
||||
"FooterHintToManage": "gérer",
|
||||
"AgentRailQueuedCount": "{count} en file"
|
||||
"AgentRailQueuedCount": "{count} en file",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "écrit",
|
||||
"AgentFocusPostureReadOnly": "lecture seule",
|
||||
"AgentFocusPostureNetwork": "réseau",
|
||||
"AgentFocusPostureNoNetwork": "sans réseau",
|
||||
"AgentFocusPostureShellFull": "shell",
|
||||
"AgentFocusPostureShellReadOnly": "shell en lecture seule",
|
||||
"AgentFocusPostureShellNone": "sans shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "{agent} तक नहीं पहुँचाया जा सका: {reason}",
|
||||
"FooterHintForAgents": "एजेंट",
|
||||
"FooterHintToManage": "प्रबंधन",
|
||||
"AgentRailQueuedCount": "{count} कतार में"
|
||||
"AgentRailQueuedCount": "{count} कतार में",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "लिख सकता है",
|
||||
"AgentFocusPostureReadOnly": "केवल-पढ़ने",
|
||||
"AgentFocusPostureNetwork": "नेटवर्क",
|
||||
"AgentFocusPostureNoNetwork": "नेटवर्क नहीं",
|
||||
"AgentFocusPostureShellFull": "शेल",
|
||||
"AgentFocusPostureShellReadOnly": "केवल-पढ़ने शेल",
|
||||
"AgentFocusPostureShellNone": "शेल नहीं"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "Tidak dapat mengirim ke {agent}: {reason}",
|
||||
"FooterHintForAgents": "agen",
|
||||
"FooterHintToManage": "kelola",
|
||||
"AgentRailQueuedCount": "{count} antre"
|
||||
"AgentRailQueuedCount": "{count} antre",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "menulis",
|
||||
"AgentFocusPostureReadOnly": "hanya-baca",
|
||||
"AgentFocusPostureNetwork": "jaringan",
|
||||
"AgentFocusPostureNoNetwork": "tanpa jaringan",
|
||||
"AgentFocusPostureShellFull": "shell",
|
||||
"AgentFocusPostureShellReadOnly": "shell hanya-baca",
|
||||
"AgentFocusPostureShellNone": "tanpa shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "{agent} に届けられませんでした: {reason}",
|
||||
"FooterHintForAgents": "エージェント",
|
||||
"FooterHintToManage": "管理",
|
||||
"AgentRailQueuedCount": "{count} 件キュー"
|
||||
"AgentRailQueuedCount": "{count} 件キュー",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "書き込み可",
|
||||
"AgentFocusPostureReadOnly": "読み取り専用",
|
||||
"AgentFocusPostureNetwork": "ネットワーク可",
|
||||
"AgentFocusPostureNoNetwork": "ネットワーク不可",
|
||||
"AgentFocusPostureShellFull": "シェル可",
|
||||
"AgentFocusPostureShellReadOnly": "読み取り専用シェル",
|
||||
"AgentFocusPostureShellNone": "シェル不可"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "{agent}에게 전달할 수 없습니다: {reason}",
|
||||
"FooterHintForAgents": "에이전트",
|
||||
"FooterHintToManage": "관리",
|
||||
"AgentRailQueuedCount": "{count}개 대기"
|
||||
"AgentRailQueuedCount": "{count}개 대기",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "쓰기 가능",
|
||||
"AgentFocusPostureReadOnly": "읽기 전용",
|
||||
"AgentFocusPostureNetwork": "네트워크 가능",
|
||||
"AgentFocusPostureNoNetwork": "네트워크 없음",
|
||||
"AgentFocusPostureShellFull": "셸 가능",
|
||||
"AgentFocusPostureShellReadOnly": "읽기 전용 셸",
|
||||
"AgentFocusPostureShellNone": "셸 없음"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "Não foi possível entregar a {agent}: {reason}",
|
||||
"FooterHintForAgents": "agentes",
|
||||
"FooterHintToManage": "gerenciar",
|
||||
"AgentRailQueuedCount": "{count} na fila"
|
||||
"AgentRailQueuedCount": "{count} na fila",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "escreve",
|
||||
"AgentFocusPostureReadOnly": "somente leitura",
|
||||
"AgentFocusPostureNetwork": "rede",
|
||||
"AgentFocusPostureNoNetwork": "sem rede",
|
||||
"AgentFocusPostureShellFull": "shell",
|
||||
"AgentFocusPostureShellReadOnly": "shell somente leitura",
|
||||
"AgentFocusPostureShellNone": "sem shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "Не удалось доставить {agent}: {reason}",
|
||||
"FooterHintForAgents": "агенты",
|
||||
"FooterHintToManage": "управление",
|
||||
"AgentRailQueuedCount": "{count} в очереди"
|
||||
"AgentRailQueuedCount": "{count} в очереди",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "пишет",
|
||||
"AgentFocusPostureReadOnly": "только чтение",
|
||||
"AgentFocusPostureNetwork": "сеть",
|
||||
"AgentFocusPostureNoNetwork": "без сети",
|
||||
"AgentFocusPostureShellFull": "shell",
|
||||
"AgentFocusPostureShellReadOnly": "shell только чтение",
|
||||
"AgentFocusPostureShellNone": "без shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "Не вдалося доставити {agent}: {reason}",
|
||||
"FooterHintForAgents": "агенти",
|
||||
"FooterHintToManage": "керування",
|
||||
"AgentRailQueuedCount": "{count} у черзі"
|
||||
"AgentRailQueuedCount": "{count} у черзі",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "пише",
|
||||
"AgentFocusPostureReadOnly": "лише читання",
|
||||
"AgentFocusPostureNetwork": "мережа",
|
||||
"AgentFocusPostureNoNetwork": "без мережі",
|
||||
"AgentFocusPostureShellFull": "shell",
|
||||
"AgentFocusPostureShellReadOnly": "shell лише читання",
|
||||
"AgentFocusPostureShellNone": "без shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "Không thể gửi tới {agent}: {reason}",
|
||||
"FooterHintForAgents": "agent",
|
||||
"FooterHintToManage": "quản lý",
|
||||
"AgentRailQueuedCount": "{count} đang chờ"
|
||||
"AgentRailQueuedCount": "{count} đang chờ",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "được ghi",
|
||||
"AgentFocusPostureReadOnly": "chỉ đọc",
|
||||
"AgentFocusPostureNetwork": "có mạng",
|
||||
"AgentFocusPostureNoNetwork": "không mạng",
|
||||
"AgentFocusPostureShellFull": "shell",
|
||||
"AgentFocusPostureShellReadOnly": "shell chỉ đọc",
|
||||
"AgentFocusPostureShellNone": "không shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "无法发送给 {agent}:{reason}",
|
||||
"FooterHintForAgents": "智能体",
|
||||
"FooterHintToManage": "管理",
|
||||
"AgentRailQueuedCount": "{count} 条排队"
|
||||
"AgentRailQueuedCount": "{count} 条排队",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "可写入",
|
||||
"AgentFocusPostureReadOnly": "只读",
|
||||
"AgentFocusPostureNetwork": "可联网",
|
||||
"AgentFocusPostureNoNetwork": "无网络",
|
||||
"AgentFocusPostureShellFull": "可用 shell",
|
||||
"AgentFocusPostureShellReadOnly": "只读 shell",
|
||||
"AgentFocusPostureShellNone": "无 shell"
|
||||
}
|
||||
@@ -1440,5 +1440,13 @@
|
||||
"AgentFocusFollowUpFailed": "無法傳送給 {agent}:{reason}",
|
||||
"FooterHintForAgents": "代理",
|
||||
"FooterHintToManage": "管理",
|
||||
"AgentRailQueuedCount": "{count} 則排隊"
|
||||
"AgentRailQueuedCount": "{count} 則排隊",
|
||||
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
|
||||
"AgentFocusPostureWrites": "可寫入",
|
||||
"AgentFocusPostureReadOnly": "唯讀",
|
||||
"AgentFocusPostureNetwork": "可連網",
|
||||
"AgentFocusPostureNoNetwork": "無網路",
|
||||
"AgentFocusPostureShellFull": "可用 shell",
|
||||
"AgentFocusPostureShellReadOnly": "唯讀 shell",
|
||||
"AgentFocusPostureShellNone": "無 shell"
|
||||
}
|
||||
@@ -1618,6 +1618,14 @@ pub enum MessageId {
|
||||
AgentFocusOpened,
|
||||
AgentFocusClosed,
|
||||
AgentFocusBanner,
|
||||
AgentFocusPosture,
|
||||
AgentFocusPostureWrites,
|
||||
AgentFocusPostureReadOnly,
|
||||
AgentFocusPostureNetwork,
|
||||
AgentFocusPostureNoNetwork,
|
||||
AgentFocusPostureShellFull,
|
||||
AgentFocusPostureShellReadOnly,
|
||||
AgentFocusPostureShellNone,
|
||||
AgentFocusComposerChip,
|
||||
AgentFocusPlaceholder,
|
||||
AgentFocusNoTranscript,
|
||||
@@ -3065,6 +3073,14 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::AgentFocusOpened,
|
||||
MessageId::AgentFocusClosed,
|
||||
MessageId::AgentFocusBanner,
|
||||
MessageId::AgentFocusPosture,
|
||||
MessageId::AgentFocusPostureWrites,
|
||||
MessageId::AgentFocusPostureReadOnly,
|
||||
MessageId::AgentFocusPostureNetwork,
|
||||
MessageId::AgentFocusPostureNoNetwork,
|
||||
MessageId::AgentFocusPostureShellFull,
|
||||
MessageId::AgentFocusPostureShellReadOnly,
|
||||
MessageId::AgentFocusPostureShellNone,
|
||||
MessageId::AgentFocusComposerChip,
|
||||
MessageId::AgentFocusPlaceholder,
|
||||
MessageId::AgentFocusNoTranscript,
|
||||
|
||||
@@ -385,6 +385,40 @@ pub(crate) fn focused_status(app: &App) -> Option<(char, String)> {
|
||||
})
|
||||
}
|
||||
|
||||
/// One short line naming the focused worker's effective posture: its role,
|
||||
/// whether it may write the workspace, reach the network, and run shell —
|
||||
/// from the runtime's persisted permission snapshot, never guessed.
|
||||
pub(crate) fn focused_posture(app: &App) -> Option<String> {
|
||||
let focus = app.agent_focus.as_ref()?;
|
||||
let agent = app
|
||||
.subagent_cache
|
||||
.iter()
|
||||
.find(|agent| agent.agent_id == focus.agent_id)?;
|
||||
let permissions = agent.runtime_permissions.as_ref()?;
|
||||
let write = app.tr(if permissions.write {
|
||||
MessageId::AgentFocusPostureWrites
|
||||
} else {
|
||||
MessageId::AgentFocusPostureReadOnly
|
||||
});
|
||||
let network = app.tr(if permissions.network {
|
||||
MessageId::AgentFocusPostureNetwork
|
||||
} else {
|
||||
MessageId::AgentFocusPostureNoNetwork
|
||||
});
|
||||
let shell = app.tr(match permissions.shell.as_str() {
|
||||
"full" => MessageId::AgentFocusPostureShellFull,
|
||||
"read_only" => MessageId::AgentFocusPostureShellReadOnly,
|
||||
_ => MessageId::AgentFocusPostureShellNone,
|
||||
});
|
||||
Some(
|
||||
app.tr(MessageId::AgentFocusPosture)
|
||||
.replace("{role}", agent.agent_type.as_str())
|
||||
.replace("{write}", &write)
|
||||
.replace("{network}", &network)
|
||||
.replace("{shell}", &shell),
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether any worker exists to list, focus, or manage this session.
|
||||
pub(crate) fn agents_exist(app: &App) -> bool {
|
||||
!app.subagent_cache.is_empty() || !app.agent_progress.is_empty()
|
||||
@@ -454,7 +488,7 @@ pub(crate) fn render_focus(app: &mut App, area: Rect, buf: &mut Buffer) {
|
||||
.tr(MessageId::AgentFocusBanner)
|
||||
.replace("{agent}", &focus.label)
|
||||
.replace("{status}", &status_word);
|
||||
let banner_line = Line::from(vec![
|
||||
let mut banner_spans = vec![
|
||||
Span::styled(
|
||||
format!("{status_glyph} "),
|
||||
Style::default().fg(theme.accent_action),
|
||||
@@ -465,7 +499,16 @@ pub(crate) fn render_focus(app: &mut App, area: Rect, buf: &mut Buffer) {
|
||||
.fg(theme.accent_action)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]);
|
||||
];
|
||||
// The worker's effective posture, in the same dot chain: what it may do
|
||||
// is stated where its conversation is read, not hidden in a role name.
|
||||
if let Some(posture) = focused_posture(app) {
|
||||
banner_spans.push(Span::styled(
|
||||
format!(" · {posture}"),
|
||||
Style::default().fg(theme.text_muted),
|
||||
));
|
||||
}
|
||||
let banner_line = Line::from(banner_spans);
|
||||
let width = area.width.max(1);
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
if focus.omitted_messages > 0 {
|
||||
@@ -610,6 +653,68 @@ mod tests {
|
||||
assert!(!exit_focus(&mut app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_banner_states_the_workers_effective_posture_from_the_runtime_snapshot() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let mut app = test_app(tmp.path().to_path_buf());
|
||||
seed_resident_transcript(
|
||||
&mut app,
|
||||
"agent_scout",
|
||||
json!([{"role": "user", "content": [{"type": "text", "text": "look around", "cache_control": null}]}]),
|
||||
);
|
||||
app.subagent_cache
|
||||
.push(crate::tools::subagent::SubAgentResult {
|
||||
name: "agent_scout".to_string(),
|
||||
agent_id: "agent_scout".to_string(),
|
||||
context_mode: "fresh".to_string(),
|
||||
fork_context: false,
|
||||
workspace: None,
|
||||
git_branch: None,
|
||||
agent_type: crate::tools::subagent::FleetRole::Scout,
|
||||
assignment: crate::tools::subagent::SubAgentAssignment {
|
||||
objective: "look around".to_string(),
|
||||
role: Some("scout".to_string()),
|
||||
},
|
||||
model: "deepseek-v4-flash".to_string(),
|
||||
nickname: None,
|
||||
status: SubAgentStatus::Running,
|
||||
worker_status: None,
|
||||
runtime_permissions: Some(codewhale_protocol::fleet::FleetEffectivePermissions {
|
||||
write: false,
|
||||
network: true,
|
||||
shell: "read_only".to_string(),
|
||||
tool_scope: "inherit".to_string(),
|
||||
tools: Vec::new(),
|
||||
background: true,
|
||||
max_spawn_depth: 1,
|
||||
profile_id: None,
|
||||
profile_origin: None,
|
||||
source: "built_in".to_string(),
|
||||
}),
|
||||
parent_run_id: None,
|
||||
spawn_depth: 1,
|
||||
child_route: None,
|
||||
result: None,
|
||||
steps_taken: 0,
|
||||
checkpoint: None,
|
||||
needs_input: None,
|
||||
duration_ms: 0,
|
||||
started_at: None,
|
||||
from_prior_session: false,
|
||||
});
|
||||
focus_agent(&mut app, "agent_scout");
|
||||
let posture = focused_posture(&app).expect("posture line from the snapshot");
|
||||
assert_eq!(posture, "scout · read-only · network · read-only shell");
|
||||
let screen = render(&mut app, 100, 8);
|
||||
assert!(
|
||||
screen.contains("read-only · network · read-only shell"),
|
||||
"{screen}"
|
||||
);
|
||||
// No snapshot, no guess.
|
||||
app.subagent_cache[0].runtime_permissions = None;
|
||||
assert!(focused_posture(&app).is_none());
|
||||
}
|
||||
|
||||
fn focus_label(app: &App) -> String {
|
||||
app.agent_focus.as_ref().map(|f| f.label.clone()).unwrap()
|
||||
}
|
||||
|
||||
+8
-1
@@ -266,7 +266,14 @@ holding an `http`/`https`/`ws`/`wss`/`ftp` address. A URL appearing in file
|
||||
A member's `role` picks the worker
|
||||
posture (and system prompt) when that role already fits inside the ceiling —
|
||||
`reviewer`, `verifier`, `consultant`, `planner` — and a domain-specific role
|
||||
such as `auditor` falls back to the narrowest posture the ceiling allows. Tasks
|
||||
such as `auditor` falls back to the narrowest posture the ceiling allows.
|
||||
Built-in role defaults withhold only what the role intends: read-only roles
|
||||
never write the workspace, every role keeps network reads, `planner` may run
|
||||
read-only shell probes, and `builder`/`worker`/`custom` inherit the parent's
|
||||
effective write/network/shell posture as their ceiling (see the role table in
|
||||
`docs/SUBAGENTS.md`). The session's permission posture — Ask, Auto-Review,
|
||||
Full Access — then gates each worker call exactly as it gates the parent
|
||||
(`docs/MODES.md`, "Children"). Tasks
|
||||
cannot override any of it: `model`, `thinking`,
|
||||
`subagent_type`, `allowed_tools`, and `write_authority` are rejected on an exact
|
||||
Fleet rather than silently ignored.
|
||||
|
||||
@@ -242,6 +242,31 @@ narrowed before metadata is built and cannot invent approval authority. An
|
||||
explicit Full Access sub-agent handoff preserves the parent's standing posture
|
||||
so ordinary child work does not begin prompting again.
|
||||
|
||||
### Children (sub-agents and Fleet workers)
|
||||
|
||||
Children inherit the session posture faithfully rather than a bare
|
||||
auto-approve bit:
|
||||
|
||||
- **Auto-Review**: a worker's held call goes through the same deterministic
|
||||
policy (proven-safe calls run; publish-like and destructive background work
|
||||
is hard-blocked) and, for holds it cannot prove safe, the same one-shot
|
||||
model guardian using the child's own session client. No prompt is ever
|
||||
opened for a child; an unavailable guardian denies, fail closed.
|
||||
- **Ask**: a call the role may delegate runs. A held call is raised as an
|
||||
approval prompt in the parent's UI (`agent:<id>:approval:<n>`) when the
|
||||
host is an interactive TUI; the worker waits visibly (`waiting for user`)
|
||||
and the person's answer is routed back to it, whether the parent turn is
|
||||
idle or itself awaiting an approval. Hosts that cannot prompt deny with the
|
||||
reason.
|
||||
- **Full Access**: ordinary calls run; destructive detached work still fails
|
||||
closed, because children are background workers.
|
||||
|
||||
Role posture and the execution envelope are checked before and after this
|
||||
gate and never widen. Every decision a person did not make at a prompt is
|
||||
written to the audit log and to the child's transcript as a one-line note
|
||||
(`Auto-Review allowed 'bash' (low risk, model guardian): …`), visible when
|
||||
the worker is focused.
|
||||
|
||||
## Small-Screen Status Behavior
|
||||
|
||||
When terminal height is constrained, the status area compacts first so header/chat/composer/footer remain visible:
|
||||
|
||||
+30
-10
@@ -62,16 +62,36 @@ diff, linked issues, tests, and CI before merging, harvesting, closing, or
|
||||
deferring it. A sub-agent's result is a working set, not a substitute for
|
||||
stewardship.
|
||||
|
||||
| Role | Stance | Writes? | Shell posture | Typical use |
|
||||
|---------------|----------------------------------------|---------|---------------|----------------------------------------------|
|
||||
| `worker` | flexible; do whatever the parent says | yes | yes | the default; multi-step tasks |
|
||||
| `scout` | read-only; map the relevant code fast | no | read-only (net + bounded verify) | "find every call site of `Foo`; check the PR with gh" |
|
||||
| `planner` | analyse and produce a strategy | minimal | minimal | "design the migration; don't execute" |
|
||||
| `reviewer` | read-and-grade with severity scores | no | read-only (net + bounded verify) | "audit this PR for bugs" |
|
||||
| `builder` | land a specific change with min edit | yes | yes | "rewrite `bar.rs::Foo::bar` to do X" |
|
||||
| `verifier` | run tests / validation, report outcome | no | test-focused | "run cargo test --workspace, report" |
|
||||
| `consultant` | short-lived, high-reasoning counsel | no | none | "what are we missing in this design?" |
|
||||
| `custom` | explicit narrow tool allowlist | depends | depends | locked-down dispatch with hand-picked tools |
|
||||
| Role | Stance | Writes? | Network? | Shell posture | Typical use |
|
||||
|---------------|----------------------------------------|---------|----------|---------------|----------------------------------------------|
|
||||
| `worker` | flexible; do whatever the parent says | yes | yes | yes | the default; multi-step tasks |
|
||||
| `scout` | read-only; map the relevant code fast | no | yes | read-only (net + bounded verify) | "find every call site of `Foo`; check the PR with gh" |
|
||||
| `planner` | analyse and produce a strategy | no | yes | read-only probes | "design the migration; don't execute" |
|
||||
| `reviewer` | read-and-grade with severity scores | no | yes | read-only (net + bounded verify) | "audit this PR for bugs" |
|
||||
| `builder` | land a specific change with min edit | yes | yes | yes | "rewrite `bar.rs::Foo::bar` to do X" |
|
||||
| `verifier` | run tests / validation, report outcome | no | yes | test-focused | "run cargo test --workspace, report" |
|
||||
| `consultant` | short-lived, high-reasoning counsel | no | yes | none | "what are we missing in this design?" |
|
||||
| `custom` | explicit narrow tool allowlist | inherits | inherits | inherits | hand-picked tools on the parent's posture |
|
||||
|
||||
A role's default is what the role *intends*, and the parent's effective
|
||||
posture is always the ceiling (a child never widens beyond its parent).
|
||||
Read-only roles withhold **workspace writes** by intent; nothing else is
|
||||
taken away by default — every role keeps network reads, and `custom`
|
||||
inherits the parent's write/network/shell posture and is narrowed only by
|
||||
its explicit tool list or the spawning call. The focused worker's header
|
||||
states the effective posture (`scout · read-only · network · read-only
|
||||
shell`) from the runtime's own permission snapshot.
|
||||
|
||||
The session's **permission posture** applies inside every child exactly as
|
||||
it applies to the parent turn: under Auto-Review the same deterministic
|
||||
floor and one-shot model guardian decide a worker's held calls (never a
|
||||
prompt; an unavailable guardian denies, fail closed); under Ask a held call
|
||||
the role cannot delegate is raised as an approval prompt in the parent's
|
||||
UI and the worker waits visibly (`waiting for user`), or is denied with the
|
||||
reason on hosts that cannot prompt; Full Access still fails closed on the
|
||||
non-bypassable safety floor. Each decision nobody was prompted for is a
|
||||
one-line note in that worker's transcript (visible when it is focused) and
|
||||
an audit-log record. See `docs/MODES.md`.
|
||||
|
||||
Each role's full system prompt lives in
|
||||
`crates/tui/src/tools/subagent/mod.rs` (search for
|
||||
|
||||
Reference in new issue
Block a user