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:
CodeWhale Bot committed 2026-08-15 20:56:04 -07:00
1 parent 317110da25
commit 43ffca8561
21 files changed
+335 -28

No files matched your search

+14
View File
@@ -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
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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": "शेल नहीं"
}
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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": "シェル不可"
}
+9 -1
View File
@@ -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": "셸 없음"
}
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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"
}
+9 -1
View File
@@ -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"
}
+16
View File
@@ -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,
+107 -2
View File
@@ -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
View File
@@ -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.
+25
View File
@@ -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
View File
@@ -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