mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
feat(subagent): keep permitted code mode in child catalogs (CW-7033)
Child catalogs used to drop `execute_tools` in `narrow_child_surface`, so
sub-agents could not compose tool calls in code mode even where the parent
allowed it. A child now keeps `execute_tools` when its own authority admits
it and the registry carries no tool authority override.
- Nested calls in a child's program pass the child's own gate, approvals and
write-scope admission; final dispatch skips the registry-spec lookup only
for `execute_tools` itself, never on ACP turns.
- The program runs inside the child's `run_tool_bounded`, so it stops at the
child's tool timeout (approval waits excluded) or work deadline and still
returns its receipt.
- A program in which no nested call ran reports `executed: false`, so the
Fleet denial-loop guard does not count it as progress.
- Extension tools called directly in a child's turn get their invocation
ticket as in the session's turn; comments and docs that said sub-agents had
none are corrected (EXTENSIONS.md, zh_hans/EXTENSIONS.md,
TS_EXTENSION_HOST.md, TOOL_SURFACE.md).
Known limitation, recorded in TOOL_SURFACE.md: refused nested calls inside a
child's program are not yet counted as denials by the Fleet guard.
Verification on this tree (27fb061108 plus this commit):
- cargo fmt --all -- --check: clean.
- clippy -p codewhale-tui --all-targets --locked with CI's flags: clean.
- nextest focused on codemode, approval execute_tools, subagent small_surface,
host_profile, child_host, extension_host, tools::subagent, acp_server and
tool_execution: 1091 tests run, 1091 passed (1 slow), 14502 skipped.
- npm test: 0 failures (101, 19, 413 of 420 with 7 skipped, 7, 772 passed).
npm run check:web: check-facts OK, check-docs PASS, web build OK.
- Not run: the full suite (CI owns it) and a real child-agent proof.
Refs CW-7033, #6841
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
1 parent
27fb061108
commit
6ad6b26196
11 files changed
+172
-128
No files matched your search
@@ -808,9 +808,18 @@ impl Engine {
|
||||
surface: &mut ToolSurfacePolicy,
|
||||
) {
|
||||
let discoverable = !authority.grant.scope.as_ref().is_some_and(Vec::is_empty);
|
||||
let code_mode = surface.registry.context().tool_authority.is_none()
|
||||
&& authority
|
||||
.validate(
|
||||
&surface.registry,
|
||||
tool_catalog::EXECUTE_TOOLS_TOOL_NAME,
|
||||
&serde_json::json!({}),
|
||||
)
|
||||
.is_ok();
|
||||
surface.catalog.retain(|tool| {
|
||||
surface.registry.contains(&tool.name)
|
||||
|| (discoverable && tool_catalog::is_tool_search_tool(&tool.name))
|
||||
|| (code_mode && tool.name == tool_catalog::EXECUTE_TOOLS_TOOL_NAME)
|
||||
});
|
||||
surface
|
||||
.active_names
|
||||
|
||||
@@ -468,7 +468,8 @@ impl Engine {
|
||||
let child = context_override
|
||||
.as_ref()
|
||||
.or_else(|| registry.map(|registry| registry.context()))
|
||||
.and_then(|context| context.child_host.clone());
|
||||
.and_then(|context| context.child_host.clone())
|
||||
.filter(|_| tool_name != EXECUTE_TOOLS_TOOL_NAME);
|
||||
let execution = Self::execute_tool_admitted(
|
||||
lock,
|
||||
supports_parallel,
|
||||
@@ -713,17 +714,19 @@ impl Engine {
|
||||
if let Some(context) = context
|
||||
&& (context.acp_host.is_some() || context.child_host.is_some())
|
||||
{
|
||||
let spec = registry
|
||||
.and_then(|registry| registry.get(&tool_name))
|
||||
.ok_or_else(|| {
|
||||
ToolError::not_available("ACP call has no admitted registered tool")
|
||||
})?;
|
||||
crate::tools::registry::enforce_tool_authority(
|
||||
&tool_name,
|
||||
&tool_input,
|
||||
spec.as_ref(),
|
||||
context,
|
||||
)?;
|
||||
if context.acp_host.is_some() || tool_name != EXECUTE_TOOLS_TOOL_NAME {
|
||||
let spec = registry
|
||||
.and_then(|registry| registry.get(&tool_name))
|
||||
.ok_or_else(|| {
|
||||
ToolError::not_available("ACP call has no admitted registered tool")
|
||||
})?;
|
||||
crate::tools::registry::enforce_tool_authority(
|
||||
&tool_name,
|
||||
&tool_input,
|
||||
spec.as_ref(),
|
||||
context,
|
||||
)?;
|
||||
}
|
||||
crate::extension_host::validate_caller_plugins(context.plugin_registry.as_deref())
|
||||
.map_err(ToolError::not_available)?;
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
//! **When it exists.** An invocation ticket is minted only for an extension
|
||||
//! tool call that runs under a nested-call gate the turn loop is serving *for
|
||||
//! that tool* (`NestedCallGate::extension`, set by the turn loop when the model
|
||||
//! called the tool directly). A sub-agent's call, a tool run from inside
|
||||
//! `execute_tools` (code mode hands nested specs no gate), a command, a timer,
|
||||
//! activation code and every test without a turn have none, so their
|
||||
//! `core/call`s are refused ("out of turn"). The ticket lives exactly as long
|
||||
//! as the invocation: it is revoked when the `tool/call` ends (answered,
|
||||
//! failed, timed out or dropped), when its owner is revoked and when its host
|
||||
//! exits, and each of those withdraws the invocation's pending core calls and
|
||||
//! any approval card one is waiting on.
|
||||
//! called the tool directly, in a sub-agent's turn as in the session's). A tool
|
||||
//! run from inside `execute_tools` (code mode hands nested specs no gate), a
|
||||
//! command, a timer, activation code and every test without a turn have none,
|
||||
//! so their `core/call`s are refused ("out of turn"). The ticket lives exactly
|
||||
//! as long as the invocation: it is revoked when the `tool/call` ends
|
||||
//! (answered, failed, timed out or dropped), when its owner is revoked and when
|
||||
//! its host exits, and each of those withdraws the invocation's pending core
|
||||
//! calls and any approval card one is waiting on.
|
||||
//!
|
||||
//! **What is refused outright** ([`refusal`], checked before planning on the
|
||||
//! name the host sent and again by the turn loop on the name planning resolved
|
||||
|
||||
@@ -1057,8 +1057,8 @@ pub struct ToolCallParams {
|
||||
pub workspace: Option<String>,
|
||||
/// The invocation ticket: present only when this call runs under the turn
|
||||
/// loop's permission gate, and what `core/call` must present. Absent for a
|
||||
/// call with no gate (a sub-agent, one nested in `execute_tools`, a test),
|
||||
/// whose tool then has no way to ask the core for anything.
|
||||
/// call with no gate (one nested in `execute_tools`, a test), whose tool
|
||||
/// then has no way to ask the core for anything.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ticket: Option<String>,
|
||||
/// Per-invocation identity; never cached at activation or by the host root.
|
||||
|
||||
@@ -345,7 +345,7 @@ impl ToolSpec for HostToolSpec {
|
||||
let deadline = self.manager.options.supervision.tool_call_deadline;
|
||||
// An invocation ticket (so the tool may ask the core to run tools for
|
||||
// it) exists only when the turn loop is serving a permission gate for
|
||||
// exactly this tool's call. Otherwise (a sub-agent, a call nested in
|
||||
// exactly this tool's call. Otherwise (a call nested in
|
||||
// `execute_tools`, a test) the tool has no way to ask for anything.
|
||||
let invocation: Option<InvocationGuard> = context
|
||||
.execution
|
||||
|
||||
@@ -8,21 +8,24 @@
|
||||
//! tool is engine-injected, never registered, and dispatched in
|
||||
//! `core::engine::tool_execution`.
|
||||
//!
|
||||
//! Authority stays entirely in Rust, and there is one gate. In a main-session
|
||||
//! turn the engine hands the program a [`NestedCallGate`]; every nested call —
|
||||
//! native, plugin, or MCP — is sent back to the turn loop and planned by the
|
||||
//! same `plan_tool_calls` a direct call goes through (deny/allow lists,
|
||||
//! preparation, hooks, ask-rules, Auto-Review, repo law, the worker authority
|
||||
//! envelope, the K1 Computer Use refusal), with the source tagged as code mode
|
||||
//! so a nested call never activates a deferred schema. A nested call that
|
||||
//! needs approval suspends the program: the engine raises the normal
|
||||
//! approval request (`request_tool_approval`, same receipt log, same card) and
|
||||
//! the program resumes on allow; on deny only that nested call fails, as an
|
||||
//! exception the program can catch. Approving the program itself therefore
|
||||
//! grants nothing, and `execute_tools` is prepared as auto-approved.
|
||||
//! Authority stays entirely in Rust, and there is one gate. In a session turn,
|
||||
//! a sub-agent's included, the engine hands the program a [`NestedCallGate`];
|
||||
//! every nested call — native, plugin, or MCP — is sent back to the turn loop
|
||||
//! and planned by the same `plan_tool_calls` a direct call goes through
|
||||
//! (deny/allow lists, preparation, hooks, ask-rules, Auto-Review, repo law, the
|
||||
//! worker authority envelope, a sub-agent's grant, the K1 Computer Use
|
||||
//! refusal), with the source tagged as code mode so a nested call never
|
||||
//! activates a deferred schema. A nested call that needs approval suspends the
|
||||
//! program: the engine raises the normal approval request
|
||||
//! (`request_tool_approval`, same receipt log, same card) and the program
|
||||
//! resumes on allow; on deny only that nested call fails, as an exception the
|
||||
//! program can catch. Approving the program itself therefore grants nothing,
|
||||
//! and `execute_tools` is prepared as auto-approved. A sub-agent's nested call
|
||||
//! is then admitted at execution like its direct call
|
||||
//! (`ToolRegistry::admit_child_call`: write-scope claim and live peers).
|
||||
//!
|
||||
//! Without a gate (sub-agents, direct unit calls) the program keeps the
|
||||
//! Phase-1 profile: read-only, auto-approved native calls only, no MCP.
|
||||
//! Without a gate (a direct unit call, or a dispatch with no per-call context)
|
||||
//! the program runs read-only, auto-approved native calls only, no MCP.
|
||||
//!
|
||||
//! Receipts (#6509): the host records every nested call when it starts, so a
|
||||
//! program that hits its deadline still reports what finished, what was
|
||||
@@ -52,7 +55,9 @@
|
||||
//! - Rich content blocks (images) from nested results are dropped; text and
|
||||
//! JSON payloads pass through bounded.
|
||||
//! - Hidden from Plan mode and refused under a worker authority envelope,
|
||||
//! like the other execution surfaces.
|
||||
//! like the other execution surfaces. Also hidden from a sub-agent whose
|
||||
//! grant refuses it (the evidence surface, or an explicit tool list that
|
||||
//! leaves it out).
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -83,11 +88,12 @@ const EXECUTE_TOOLS_TOOL_TYPE: &str = "execute_tools_20260918";
|
||||
|
||||
/// Maximum program source accepted, in bytes.
|
||||
const MAX_CODE_BYTES: usize = 64 * 1024;
|
||||
/// Run deadline when no engine turn serves the program (sub-agents, direct
|
||||
/// unit calls). Those callers bound the whole call themselves (the sub-agent
|
||||
/// tool timeout), so this is an hour-long backstop rather than an invented
|
||||
/// short cap. A gated run takes the remaining turn wall clock instead, which
|
||||
/// is unbounded unless `[tui].turn_wall_clock_secs` is set.
|
||||
/// Run deadline when no engine turn serves the program (a direct unit call,
|
||||
/// or a dispatch with no per-call context). This is an hour-long backstop
|
||||
/// rather than an invented short cap. A gated run takes the remaining turn
|
||||
/// wall clock instead, which is unbounded unless `[tui].turn_wall_clock_secs`
|
||||
/// is set. A sub-agent's program also stops at the sub-agent's tool timeout
|
||||
/// or work deadline, and still returns its receipt.
|
||||
const FALLBACK_RUN_DEADLINE: Duration = Duration::from_secs(3_600);
|
||||
/// How often the run watchdog re-checks while the program is paused on the
|
||||
/// gate (approval card, hook, review). Bounds the overrun after a pause.
|
||||
@@ -554,11 +560,12 @@ impl<T> Drop for AbortOnDrop<T> {
|
||||
|
||||
/// [`ToolInvoker`] over a snapshot of the parent turn's registry.
|
||||
///
|
||||
/// The snapshot (spec Arcs plus a cloned [`ToolContext`]) is taken at
|
||||
/// dispatch so the invoker is `'static` for the VM thread. Nested calls run
|
||||
/// on the engine's runtime (captured at dispatch), not the VM thread's.
|
||||
/// The snapshot (a registry of the spec Arcs under a cloned [`ToolContext`])
|
||||
/// is taken at dispatch so the invoker is `'static` for the VM thread. Nested
|
||||
/// calls run on the engine's runtime (captured at dispatch), not the VM
|
||||
/// thread's.
|
||||
pub(crate) struct CodemodeInvoker {
|
||||
specs: Vec<Arc<dyn ToolSpec>>,
|
||||
registry: Arc<ToolRegistry>,
|
||||
context: ToolContext,
|
||||
gate: Option<NestedCallGate>,
|
||||
runtime: Option<tokio::runtime::Handle>,
|
||||
@@ -639,8 +646,10 @@ impl CodemodeInvoker {
|
||||
spill_prefix: String,
|
||||
extra_refusal: Option<ExtraRefusal>,
|
||||
) -> Self {
|
||||
let mut registry = ToolRegistry::new(context.clone());
|
||||
registry.register_all(specs);
|
||||
Self {
|
||||
specs,
|
||||
registry: Arc::new(registry),
|
||||
context,
|
||||
gate,
|
||||
runtime: tokio::runtime::Handle::try_current().ok(),
|
||||
@@ -731,14 +740,14 @@ impl CodemodeInvoker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase-1 admission when no engine gate serves the program.
|
||||
/// Admission when no engine gate serves the program.
|
||||
fn ungated_admission(&self, name: &str, input: &Value) -> Result<(), String> {
|
||||
if McpPool::is_mcp_tool(name) {
|
||||
return Err(format!(
|
||||
"`{name}` is an MCP tool; this program has no session permission gate (sub-agent or host without a turn), so call it directly"
|
||||
"`{name}` is an MCP tool; no engine turn serves this program's session permission gate, so call it directly"
|
||||
));
|
||||
}
|
||||
let Some(spec) = self.specs.iter().find(|spec| spec.name() == name) else {
|
||||
let Some(spec) = self.registry.get(name) else {
|
||||
return Err(format!(
|
||||
"unknown tool `{name}`; discover names with tool_search before writing the program"
|
||||
));
|
||||
@@ -759,25 +768,13 @@ impl CodemodeInvoker {
|
||||
|
||||
/// Execute an admitted call on the engine runtime: MCP through the
|
||||
/// session pool (same dispatcher as a direct call), everything else
|
||||
/// through its registry spec under the turn's authority envelope.
|
||||
/// through the registry under the turn's authority envelope. Either way a
|
||||
/// sub-agent's call is first admitted against its grant, as its direct
|
||||
/// call is.
|
||||
async fn execute(&self, name: &str, input: Value) -> Result<RichToolResult, ToolError> {
|
||||
use crate::core::engine::tool_execution::OperationSpanGuard;
|
||||
let action_id = super::activity::action_id(name, &input);
|
||||
let activity_kind = if McpPool::is_mcp_tool(name) {
|
||||
if let Some(pool) = self.gate.as_ref().and_then(|gate| gate.mcp_pool.as_ref()) {
|
||||
pool.lock()
|
||||
.await
|
||||
.resolved_tool_servers()
|
||||
.get(name)
|
||||
.map(|server| super::activity::mcp_activity_kind(server))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
super::activity::registry_activity_kind(name, &input)
|
||||
};
|
||||
let future: std::pin::Pin<
|
||||
Box<dyn std::future::Future<Output = Result<RichToolResult, ToolError>> + Send>,
|
||||
> = if McpPool::is_mcp_tool(name) {
|
||||
let mcp = if McpPool::is_mcp_tool(name) {
|
||||
let Some(gate) = self.gate.as_ref() else {
|
||||
return Err(ToolError::not_available(format!(
|
||||
"MCP tool `{name}` needs the session MCP pool"
|
||||
@@ -788,37 +785,22 @@ impl CodemodeInvoker {
|
||||
"MCP is not connected for this turn, so `{name}` cannot run"
|
||||
)));
|
||||
};
|
||||
let tx_event = gate.tx_event.clone();
|
||||
let disallowed = self.context.disallowed_tools.clone();
|
||||
let name = name.to_string();
|
||||
Box::pin(async move {
|
||||
crate::core::engine::Engine::execute_mcp_tool_with_pool(
|
||||
pool,
|
||||
&tx_event,
|
||||
&name,
|
||||
input,
|
||||
&disallowed,
|
||||
// Calls a program makes never carry a person's decision.
|
||||
None,
|
||||
)
|
||||
.await
|
||||
})
|
||||
Some((pool, gate.tx_event.clone()))
|
||||
} else {
|
||||
let Some(spec) = self.specs.iter().find(|spec| spec.name() == name).cloned() else {
|
||||
return Err(ToolError::not_available(format!(
|
||||
"tool `{name}` is not registered"
|
||||
)));
|
||||
};
|
||||
enforce_tool_authority(name, &input, spec.as_ref(), &self.context)?;
|
||||
let context = self.context.clone();
|
||||
crate::extension_host::validate_caller_plugins(context.plugin_registry.as_deref())
|
||||
.map_err(ToolError::not_available)?;
|
||||
Box::pin(async move {
|
||||
crate::extension_host::validate_caller_plugins(context.plugin_registry.as_deref())
|
||||
.map_err(ToolError::not_available)?;
|
||||
spec.execute_rich(input, &context).await
|
||||
})
|
||||
None
|
||||
};
|
||||
let activity_kind = match mcp.as_ref() {
|
||||
Some((pool, _)) => pool
|
||||
.lock()
|
||||
.await
|
||||
.resolved_tool_servers()
|
||||
.get(name)
|
||||
.map(|server| super::activity::mcp_activity_kind(server)),
|
||||
None => super::activity::registry_activity_kind(name, &input),
|
||||
};
|
||||
let registry = Arc::clone(&self.registry);
|
||||
let context = self.context.clone();
|
||||
let name = name.to_string();
|
||||
let tx = self.gate.as_ref().map(|gate| gate.tx_event.clone());
|
||||
let cancel = self.context.cancel_token.clone();
|
||||
let call_id = self
|
||||
@@ -832,7 +814,7 @@ impl CodemodeInvoker {
|
||||
}
|
||||
let span = match (tx.as_ref(), activity_kind) {
|
||||
(Some(tx), Some(kind)) => Some(
|
||||
crate::core::engine::tool_execution::OperationSpanGuard::start(
|
||||
OperationSpanGuard::reserve(
|
||||
tx,
|
||||
&call_id,
|
||||
kind,
|
||||
@@ -846,7 +828,25 @@ impl CodemodeInvoker {
|
||||
if cancel.as_ref().is_some_and(CancellationToken::is_cancelled) {
|
||||
return Err(ToolError::cancelled("Nested dispatch admission cancelled."));
|
||||
}
|
||||
let outcome = future.await;
|
||||
let Some((pool, tx_event)) = mcp else {
|
||||
return registry
|
||||
.execute_rich_full_with_context(&name, input, Some(&context), || {
|
||||
Ok(span.map(OperationSpanGuard::activate))
|
||||
})
|
||||
.await;
|
||||
};
|
||||
let child = registry.admit_child_call(&name, &input, &context).await?;
|
||||
let span = span.map(OperationSpanGuard::activate);
|
||||
let outcome = crate::core::engine::Engine::execute_mcp_tool_with_pool(
|
||||
pool,
|
||||
&tx_event,
|
||||
&name,
|
||||
input,
|
||||
&context.disallowed_tools,
|
||||
// Calls a program makes never carry a person's decision.
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
if let Some(span) = span {
|
||||
span.complete(super::activity::operation_outcome(
|
||||
&outcome,
|
||||
@@ -854,6 +854,11 @@ impl CodemodeInvoker {
|
||||
))
|
||||
.await;
|
||||
}
|
||||
if outcome.as_ref().is_ok_and(|result| result.result.success)
|
||||
&& let Some((authority, writes)) = child
|
||||
{
|
||||
authority.record_settled_writes(writes).await;
|
||||
}
|
||||
outcome
|
||||
};
|
||||
match self.runtime.as_ref() {
|
||||
@@ -1059,7 +1064,7 @@ impl CodemodeInvoker {
|
||||
|
||||
if let Some(note) = refusal_before_gate(&tool, &input, self.gate.is_some()).or_else(|| {
|
||||
self.extra_refusal
|
||||
.and_then(|refuse| refuse(&self.specs, &tool, &input))
|
||||
.and_then(|refuse| refuse(&self.registry.all(), &tool, &input))
|
||||
}) {
|
||||
return Err(self.refused(seq, started, NestedDecision::Refused, note));
|
||||
}
|
||||
@@ -1254,6 +1259,7 @@ fn receipt_payload(
|
||||
driver: &CodemodeDriver,
|
||||
) -> ToolResult {
|
||||
let calls = invoker.drain();
|
||||
let ran = calls.iter().any(|call| call.status != CallStatus::Refused);
|
||||
let content = json!({
|
||||
"success": success,
|
||||
"body": body,
|
||||
@@ -1265,7 +1271,7 @@ fn receipt_payload(
|
||||
ToolResult {
|
||||
content,
|
||||
success,
|
||||
metadata: None,
|
||||
metadata: (!ran).then(|| json!({ "executed": false })),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1299,17 +1305,31 @@ pub async fn execute_tools_tool(
|
||||
invoker.clone(),
|
||||
cancel.clone(),
|
||||
));
|
||||
let outcome = loop {
|
||||
let Some(remaining) = invoker.remaining(deadline) else {
|
||||
break None;
|
||||
};
|
||||
tokio::select! {
|
||||
result = &mut run => break Some(result),
|
||||
() = tokio::time::sleep(remaining) => {}
|
||||
let watchdog = async {
|
||||
loop {
|
||||
let Some(remaining) = invoker.remaining(deadline) else {
|
||||
return Err(format!(
|
||||
"its {}s run deadline (time waiting on approvals is not counted)",
|
||||
deadline.as_secs()
|
||||
));
|
||||
};
|
||||
tokio::select! {
|
||||
result = &mut run => return Ok(result),
|
||||
() = tokio::time::sleep(remaining) => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
let outcome = match context.child_host.as_ref() {
|
||||
Some(child) => child
|
||||
.run_tool_bounded(async { Ok::<_, ToolError>(watchdog.await) })
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
Err("the sub-agent's tool timeout (time waiting on approvals is not counted) or work deadline".to_string())
|
||||
}),
|
||||
None => watchdog.await,
|
||||
};
|
||||
let program_result = match outcome {
|
||||
None => {
|
||||
Err(stopped_at) => {
|
||||
// Stop the VM (and with it every in-flight nested call) before
|
||||
// reading the receipt, so the listed state is final.
|
||||
cancel.cancel();
|
||||
@@ -1318,8 +1338,7 @@ pub async fn execute_tools_tool(
|
||||
false,
|
||||
json!({
|
||||
"error": format!(
|
||||
"execute_tools stopped at its {}s run deadline (time waiting on approvals is not counted). Calls with status ok/failed/refused finished; calls still in_flight were cancelled and may have partially run.",
|
||||
deadline.as_secs()
|
||||
"execute_tools stopped at {stopped_at}. Calls with status ok/failed/refused finished; calls still in_flight were cancelled and may have partially run."
|
||||
),
|
||||
"timed_out": true,
|
||||
}),
|
||||
@@ -1327,7 +1346,7 @@ pub async fn execute_tools_tool(
|
||||
&driver,
|
||||
));
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
Ok(Err(err)) => {
|
||||
return Ok(receipt_payload(
|
||||
false,
|
||||
json!({ "error": err.to_string() }),
|
||||
@@ -1335,7 +1354,7 @@ pub async fn execute_tools_tool(
|
||||
&driver,
|
||||
));
|
||||
}
|
||||
Some(Ok(value)) => value,
|
||||
Ok(Ok(value)) => value,
|
||||
};
|
||||
let spill_id = format!("{}-return", invoker.spill_prefix);
|
||||
let (bounded, truncated) = bound_json(program_result, RETURN_CAP_BYTES, &spill_id);
|
||||
|
||||
+2
-2
@@ -466,8 +466,8 @@ its tool `<tool>`") and decides whether one is shown.
|
||||
|
||||
**When `exec.core` exists.** Only when the model called your tool directly and
|
||||
the turn loop is serving its permission gate for that call. It is absent for a
|
||||
command, a timer, activation code, a sub-agent's call, and a tool run from
|
||||
inside `execute_tools` (code mode gives nested tools no gate); the core refuses
|
||||
command, a timer, activation code, and a tool run from inside
|
||||
`execute_tools` (code mode gives nested tools no gate); the core refuses
|
||||
a `core/call` from any of them. It also ends with the call: when your `execute`
|
||||
returns, fails, times out or is cancelled, or your plugin is disabled, or the
|
||||
host exits, pending core calls are cancelled and a waiting approval card is
|
||||
|
||||
+15
-2
@@ -167,8 +167,21 @@ Code mode is on by default (`[features] code_mode = true`), which makes
|
||||
stay available either way. Set `code_mode = false` (or run with
|
||||
`--disable code_mode`) to go back to deferring `execute_tools` behind
|
||||
`tool_search`. The flag is session configuration, so the prompt prefix stays
|
||||
stable within a session. Without an engine turn (sub-agents), a program keeps
|
||||
the conservative profile: read-only, auto-approved native calls only, no MCP.
|
||||
stable within a session.
|
||||
|
||||
Sub-agents get `execute_tools` on the same terms, unless their role is limited
|
||||
to the read-only evidence tools or an explicit tool list leaves it out. A
|
||||
child's nested call is planned by the child's own gate, so its grant and its
|
||||
approval path apply as for its direct calls, and it is admitted at execution
|
||||
against the child's write-scope claim and live peers. A child's program also
|
||||
stops at the child's tool timeout (time waiting on approvals is not counted)
|
||||
or its work deadline, and still returns the receipt. A program in which no
|
||||
nested call ran does not count as progress for the child's permission-denial
|
||||
guard, but the guard does not yet count a refused nested call as a denial
|
||||
either; only the child's step and time budgets end a loop of denied calls
|
||||
retried inside programs. Without an engine turn
|
||||
(a direct unit call), a program keeps the conservative profile: read-only,
|
||||
auto-approved native calls only, no MCP.
|
||||
|
||||
### Conversation toolbox cache
|
||||
|
||||
|
||||
@@ -83,10 +83,10 @@ remains outstanding; programmable pre-execute proposals do not implement it.
|
||||
checks the caller matches its own). The turn loop attaches one when a
|
||||
registry spec says `extension_caller()` (a `ToolSpec` method, default
|
||||
`None`), in `execute_tools_with_nested_gate`, the same function that serves an
|
||||
`execute_tools` program's gate. A sub-agent, a test, and a tool nested in
|
||||
`execute_tools` (whose invoker takes the gate from the nested context) have
|
||||
none, nor does any command, timer or activation: the tool gets no `exec.core`
|
||||
and a forged request finds no ticket.
|
||||
`execute_tools` program's gate, in a sub-agent's turn as in the session's. A
|
||||
test and a tool nested in `execute_tools` (whose invoker takes the gate from
|
||||
the nested context) have none, nor does any command, timer or activation: the
|
||||
tool gets no `exec.core` and a forged request finds no ticket.
|
||||
- **One gate, one executor.** The turn loop serves the request with
|
||||
`gate_nested_call`, now parameterised by who is asking
|
||||
(`ToolCallSource::Extension`), so planning is `plan_tool_calls` unchanged
|
||||
|
||||
@@ -259,7 +259,7 @@ async execute({ path }, exec) {
|
||||
|
||||
核心调用沿用 allow/deny、hooks、Auto-Review、仓库规则、worker 权限和审批检查。
|
||||
审批卡片及来源由 Rust 生成,插件不能批准或指定票据。该句柄不出现在激活、timer、
|
||||
用户命令、sub-agent 调用或 `execute_tools` 中的嵌套扩展工具执行里;调用结束、取消、
|
||||
用户命令或 `execute_tools` 中的嵌套扩展工具执行里;调用结束、取消、
|
||||
插件停用或宿主退出后失效,等待中的审批以取消结果撤回。
|
||||
|
||||
嵌套 shell 和网络调用强制要求用户审批,已记住的授权不满足它们;Ask 和
|
||||
|
||||
@@ -107,8 +107,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/TOOL_SURFACE.md",
|
||||
"sha256": "ecfb9e76e13225dabd3313b89c40c8bc904beb8be1d88e370151df72a90bc61a",
|
||||
"content": "# Tool surface\n\n> 阅读简体中文版:[zh_hans/TOOL_SURFACE.md](zh_hans/TOOL_SURFACE.md)。\n\nThis document describes the current model-facing tool contract. The v0.9.1\ncutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;\nread the workspace version from `Cargo.toml`, not from this line. The registry\nremains larger than the first-turn catalog so\nsaved transcripts can replay and uncommon capabilities can be loaded on demand.\nThe model should learn one canonical name for each common operation.\n\nImplementation sources:\n\n- `crates/tui/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.\n- `crates/tui/src/tools/registry.rs` registers canonical tools and hidden aliases.\n- `crates/tui/src/tools/{file,file_tool,shell}.rs` own the small foreground\n primitive behavior and schemas; the other native tools remain searchable.\n- `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.\n\n## Default-active contract\n\nNew turns start with twelve eager native names plus synthetic `tool_search`:\n\n1. `read`\n2. `write`\n3. `edit`\n4. `file_search`\n5. `bash`\n6. `agent`\n7. `workflow`\n8. `todo_write`\n9. `create_goal`\n10. `get_goal`\n11. `update_goal`\n12. `load_skill`\n13. `tool_search` (synthetic, always active)\n\nThe twelve native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in\n`crates/tui/src/core/engine/tool_catalog.rs`, pinned by\n`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority\nboundary may remove `agent` at the maximum child depth, but route size alone\nmust not change this core vocabulary.\n\nThe direct schemas deliberately stay small:\n\n| Tool | Input | Purpose |\n|---|---|---|\n| `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |\n| `write` | `path`, `content` | Create or replace a file. |\n| `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |\n| `bash` | `command`, optional `timeout`, optional `description` | Run one cancellable foreground shell command and return a bounded tail. The optional description is display-only. |\n| `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |\n| `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |\n| `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |\n| `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |\n| `get_goal` | none | Read the active goal and its progress. |\n| `update_goal` | terminal status | Mark the goal complete or blocked. |\n| `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |\n\nMode is an authority decision, not a synonym system. Plan, Work, and Operate\nuse the same primitive identities. Plan centrally refuses `write`, `edit`, and\n`bash`; Work and Operate still pass those calls through approval, sandbox,\ntrusted-path, repository-law, and managed-policy gates. Full Access changes\nordinary approval behavior but does not bypass hard safety or repository law.\n\n`update_plan` remains registered only for saved-artifact compatibility and is\nnot model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other\nspecialized capabilities are searchable rather than first-turn ceremony.\n\nA Bash description appears as a muted, bounded note under the command on the\nTUI shell card, including on automatic runs. It never replaces the command and\nis excluded from execution, exact approval keys and Auto-Review input. The\noriginal description remains in the recorded tool input and exports. Approval\nprompts, `/jobs`, the activity inspector, ACP and other Runtime clients keep\nshowing the command; this change adds no description row to those surfaces.\n\n## Local code execution\n\n`code_execution` (Python) and `js_execution` (Node.js) use the same\npermission-aware local launcher as workspace task gates and test runs. Ordinary\napproval retains the effective session policy. To retry a denied exact call\nwith wider permissions, supply `sandbox_permissions` (`workspace-write` or\n`danger-full-access`) and a nonempty `justification`; Ask mode requires explicit\none-shot user approval. The approved policy affects only that call.\n\nBoth tools read source from stdin, return stdout/stderr/return_code, and keep\nthe existing timeout and process-tree cleanup. They are not persistent REPLs;\ntracebacks refer to stdin rather than a temporary script path. The separate\nRLM/REPL kernel is not changed by this launcher integration.\n\nPlatform/backend limits of the shared launcher still apply: no available local\nwrapper means workspace-write remains unLine truncated
|
||||
"sha256": "17263d9a07cb5003a131a58de11e0b7f3267cc25d33ff463540484be7fc4a3be",
|
||||
"content": "# Tool surface\n\n> 阅读简体中文版:[zh_hans/TOOL_SURFACE.md](zh_hans/TOOL_SURFACE.md)。\n\nThis document describes the current model-facing tool contract. The v0.9.1\ncutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;\nread the workspace version from `Cargo.toml`, not from this line. The registry\nremains larger than the first-turn catalog so\nsaved transcripts can replay and uncommon capabilities can be loaded on demand.\nThe model should learn one canonical name for each common operation.\n\nImplementation sources:\n\n- `crates/tui/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.\n- `crates/tui/src/tools/registry.rs` registers canonical tools and hidden aliases.\n- `crates/tui/src/tools/{file,file_tool,shell}.rs` own the small foreground\n primitive behavior and schemas; the other native tools remain searchable.\n- `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.\n\n## Default-active contract\n\nNew turns start with twelve eager native names plus synthetic `tool_search`:\n\n1. `read`\n2. `write`\n3. `edit`\n4. `file_search`\n5. `bash`\n6. `agent`\n7. `workflow`\n8. `todo_write`\n9. `create_goal`\n10. `get_goal`\n11. `update_goal`\n12. `load_skill`\n13. `tool_search` (synthetic, always active)\n\nThe twelve native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in\n`crates/tui/src/core/engine/tool_catalog.rs`, pinned by\n`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority\nboundary may remove `agent` at the maximum child depth, but route size alone\nmust not change this core vocabulary.\n\nThe direct schemas deliberately stay small:\n\n| Tool | Input | Purpose |\n|---|---|---|\n| `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |\n| `write` | `path`, `content` | Create or replace a file. |\n| `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |\n| `bash` | `command`, optional `timeout`, optional `description` | Run one cancellable foreground shell command and return a bounded tail. The optional description is display-only. |\n| `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |\n| `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |\n| `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |\n| `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |\n| `get_goal` | none | Read the active goal and its progress. |\n| `update_goal` | terminal status | Mark the goal complete or blocked. |\n| `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |\n\nMode is an authority decision, not a synonym system. Plan, Work, and Operate\nuse the same primitive identities. Plan centrally refuses `write`, `edit`, and\n`bash`; Work and Operate still pass those calls through approval, sandbox,\ntrusted-path, repository-law, and managed-policy gates. Full Access changes\nordinary approval behavior but does not bypass hard safety or repository law.\n\n`update_plan` remains registered only for saved-artifact compatibility and is\nnot model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other\nspecialized capabilities are searchable rather than first-turn ceremony.\n\nA Bash description appears as a muted, bounded note under the command on the\nTUI shell card, including on automatic runs. It never replaces the command and\nis excluded from execution, exact approval keys and Auto-Review input. The\noriginal description remains in the recorded tool input and exports. Approval\nprompts, `/jobs`, the activity inspector, ACP and other Runtime clients keep\nshowing the command; this change adds no description row to those surfaces.\n\n## Local code execution\n\n`code_execution` (Python) and `js_execution` (Node.js) use the same\npermission-aware local launcher as workspace task gates and test runs. Ordinary\napproval retains the effective session policy. To retry a denied exact call\nwith wider permissions, supply `sandbox_permissions` (`workspace-write` or\n`danger-full-access`) and a nonempty `justification`; Ask mode requires explicit\none-shot user approval. The approved policy affects only that call.\n\nBoth tools read source from stdin, return stdout/stderr/return_code, and keep\nthe existing timeout and process-tree cleanup. They are not persistent REPLs;\ntracebacks refer to stdin rather than a temporary script path. The separate\nRLM/REPL kernel is not changed by this launcher integration.\n\nPlatform/backend limits of the shared launcher still apply: no available local\nwrapper means workspace-write remains unLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/TOOL_LIFECYCLE.md",
|
||||
@@ -277,8 +277,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/EXTENSIONS.md",
|
||||
"sha256": "a1c2ff519b0896aec3c42c3bf277e688392cfaa5d8c3e0380d99631d3c65fb86",
|
||||
"content": "# Writing an extension tool or command\n\nThe experimental TypeScript extension host runs reviewed plugin code in a\nshared Node process (or, as an opt-in, Bun). Rust still owns sessions, tool admission, approval and\nexecution. Extensions currently contribute tools and slash commands; they\ncannot provide an approval service, run a second agent loop or replace\nbuilt-in tools or commands.\n\n## Quick start\n\nStart with [hello-extension](examples/plugins/hello-extension/hello.mts) and its\n[plugin.json](examples/plugins/hello-extension/plugin.json). The example is\nexecuted unchanged by the host tests. It returns a greeting and does not read\nfiles or use the network.\n\n1. Explicitly enable `[features] extension_host = true` in your configuration,\n or start Codewhale with `codewhale --enable extension_host` (the flag goes\n before any subcommand).\n2. Install the example's directory using `/plugin install <local-directory>`.\n3. Run `/plugin validate hello-extension`, then `/plugin show hello-extension`.\n4. Run `/plugin enable hello-extension` to open its content/capability review.\n Read the review and personally run its exact `/plugin trust <token>` command,\n then enable the plugin. Trust alone does not execute the code.\n5. Ask Codewhale to use `hello_greet`. The tool follows the normal approval gate.\n6. Run `/hello-greet Codewhale`. The command runs when you type it, and shows\n its answer in the transcript with its origin, `extension:hello-extension`.\n\nAn authoring agent should stop after installation and validation and present\nthe review to the person. Do not trust or enable a bundle automatically.\n\n## Locations, installation and trust\n\nUser bundles live under `~/.codewhale/plugins/`; workspace bundles live under\n`<workspace>/.codewhale/plugins/`. New bundles use Agent Plugins v1.0.0\n`plugin.json`. Declare `extensions[\"net.codewhale\"].native.path` as one regular\n`.mjs`, `.js` or `.mts` file inside the bundle. Directories and other extensions\nfail validation when the host is enabled. With the flag off, native entries\nremain inventory-only.\n\nA plugin may split its code across several entries with `native.paths` (up to\n64, alone or beside `path`). They are activated in the order declared, as the\nfibers of one plugin: they share one owner, so one disable, review change or\ncrash tears all of them down together. Each entry is a separate module with its\nown `apply`, and tool and command names must be unique across them. If an entry\nfails to activate (throws, requires a service the host does not provide, or has\na registration refused), only that entry is withdrawn: entries that already\nactivated stay registered, later entries still activate, and the plugin fails\nonly when no entry activates. Listing the same file twice is one entry.\n\n## Runtime\n\n`[extension_host] runtime` selects what runs the host. Node is the default.\nBun is an opt-in: it is qualified on macOS only, and becomes the default only\nafter an explicit, recorded cutover.\n\n```toml\n[extension_host]\nruntime = \"node\" # default: Node only\n# runtime = \"bun\" # Bun only; fails rather than falling back to Node\n# runtime = \"auto\" # Bun >= 1.4.0 if one is found and starts, otherwise Node\n# bun = \"~/.bun/bin/bun\" # the only Bun tried when set\n# node = \"/opt/homebrew/bin/node\" # the only Node tried when set\n# mcp_backend = \"host\" # experimental SDK backend; default is \"rust\"\n```\n\n`mcp_backend = \"host\"` selects the pinned MCP SDK independently of\n`[features] extension_host`. That feature controls optional Native extensions;\nselecting Host MCP does not activate them. The SDK for existing stdio, HTTP and legacy SSE server connections in a separate\nbuiltin host process. Rust retains process/network authority, credential\nresolution, catalog admission, tool approval and cancellation. HTTP/SSE uses\nRust's guarded HTTP client; the SDK receives opaque session selectors and\nexact operation grants. Request IDs remain the Rust client's strings through\nan adapter bound to each grant. Explicit Host selection never falls back to\nRust or replays an uncertain operation. The default remains `\"rust\"` while\nrecorded transport parity is qualified. The Host path uses the same Rust\nGET session preflight and bounded reactive OAuth refresh as the default\nbackend. An explicit incompatible HTTP response can negotiate the real SDK\nSSE transport only with a fresh exact Rust operation grant; a stale-session\nrefusal remains a typed Rust recovery decision. Credentials, configured URLs,\nOAuth browser/login operations and Computer Use decision keys stay in Rust.\nBoth backends run against the same recorded MCP goldens. Passing that suite\nand the actual broker acceptance tests is required before any default cutover.\nThe default also requires the platform isolation and measured Phase 3 gates\nin Ops CURRENT_DECISIONS §26. The native Rust protocol adapters remain for one\nLine truncated
|
||||
"sha256": "3b7c422e6cd06a6eb5642fdc56576e3d74f6f280c183e6de8e0bf05c36bc13a8",
|
||||
"content": "# Writing an extension tool or command\n\nThe experimental TypeScript extension host runs reviewed plugin code in a\nshared Node process (or, as an opt-in, Bun). Rust still owns sessions, tool admission, approval and\nexecution. Extensions currently contribute tools and slash commands; they\ncannot provide an approval service, run a second agent loop or replace\nbuilt-in tools or commands.\n\n## Quick start\n\nStart with [hello-extension](examples/plugins/hello-extension/hello.mts) and its\n[plugin.json](examples/plugins/hello-extension/plugin.json). The example is\nexecuted unchanged by the host tests. It returns a greeting and does not read\nfiles or use the network.\n\n1. Explicitly enable `[features] extension_host = true` in your configuration,\n or start Codewhale with `codewhale --enable extension_host` (the flag goes\n before any subcommand).\n2. Install the example's directory using `/plugin install <local-directory>`.\n3. Run `/plugin validate hello-extension`, then `/plugin show hello-extension`.\n4. Run `/plugin enable hello-extension` to open its content/capability review.\n Read the review and personally run its exact `/plugin trust <token>` command,\n then enable the plugin. Trust alone does not execute the code.\n5. Ask Codewhale to use `hello_greet`. The tool follows the normal approval gate.\n6. Run `/hello-greet Codewhale`. The command runs when you type it, and shows\n its answer in the transcript with its origin, `extension:hello-extension`.\n\nAn authoring agent should stop after installation and validation and present\nthe review to the person. Do not trust or enable a bundle automatically.\n\n## Locations, installation and trust\n\nUser bundles live under `~/.codewhale/plugins/`; workspace bundles live under\n`<workspace>/.codewhale/plugins/`. New bundles use Agent Plugins v1.0.0\n`plugin.json`. Declare `extensions[\"net.codewhale\"].native.path` as one regular\n`.mjs`, `.js` or `.mts` file inside the bundle. Directories and other extensions\nfail validation when the host is enabled. With the flag off, native entries\nremain inventory-only.\n\nA plugin may split its code across several entries with `native.paths` (up to\n64, alone or beside `path`). They are activated in the order declared, as the\nfibers of one plugin: they share one owner, so one disable, review change or\ncrash tears all of them down together. Each entry is a separate module with its\nown `apply`, and tool and command names must be unique across them. If an entry\nfails to activate (throws, requires a service the host does not provide, or has\na registration refused), only that entry is withdrawn: entries that already\nactivated stay registered, later entries still activate, and the plugin fails\nonly when no entry activates. Listing the same file twice is one entry.\n\n## Runtime\n\n`[extension_host] runtime` selects what runs the host. Node is the default.\nBun is an opt-in: it is qualified on macOS only, and becomes the default only\nafter an explicit, recorded cutover.\n\n```toml\n[extension_host]\nruntime = \"node\" # default: Node only\n# runtime = \"bun\" # Bun only; fails rather than falling back to Node\n# runtime = \"auto\" # Bun >= 1.4.0 if one is found and starts, otherwise Node\n# bun = \"~/.bun/bin/bun\" # the only Bun tried when set\n# node = \"/opt/homebrew/bin/node\" # the only Node tried when set\n# mcp_backend = \"host\" # experimental SDK backend; default is \"rust\"\n```\n\n`mcp_backend = \"host\"` selects the pinned MCP SDK independently of\n`[features] extension_host`. That feature controls optional Native extensions;\nselecting Host MCP does not activate them. The SDK for existing stdio, HTTP and legacy SSE server connections in a separate\nbuiltin host process. Rust retains process/network authority, credential\nresolution, catalog admission, tool approval and cancellation. HTTP/SSE uses\nRust's guarded HTTP client; the SDK receives opaque session selectors and\nexact operation grants. Request IDs remain the Rust client's strings through\nan adapter bound to each grant. Explicit Host selection never falls back to\nRust or replays an uncertain operation. The default remains `\"rust\"` while\nrecorded transport parity is qualified. The Host path uses the same Rust\nGET session preflight and bounded reactive OAuth refresh as the default\nbackend. An explicit incompatible HTTP response can negotiate the real SDK\nSSE transport only with a fresh exact Rust operation grant; a stale-session\nrefusal remains a typed Rust recovery decision. Credentials, configured URLs,\nOAuth browser/login operations and Computer Use decision keys stay in Rust.\nBoth backends run against the same recorded MCP goldens. Passing that suite\nand the actual broker acceptance tests is required before any default cutover.\nThe default also requires the platform isolation and measured Phase 3 gates\nin Ops CURRENT_DECISIONS §26. The native Rust protocol adapters remain for one\nLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -400,8 +400,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/TOOL_SURFACE.md",
|
||||
"sha256": "ecfb9e76e13225dabd3313b89c40c8bc904beb8be1d88e370151df72a90bc61a",
|
||||
"content": "# Tool surface\n\n> 阅读简体中文版:[zh_hans/TOOL_SURFACE.md](zh_hans/TOOL_SURFACE.md)。\n\nThis document describes the current model-facing tool contract. The v0.9.1\ncutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;\nread the workspace version from `Cargo.toml`, not from this line. The registry\nremains larger than the first-turn catalog so\nsaved transcripts can replay and uncommon capabilities can be loaded on demand.\nThe model should learn one canonical name for each common operation.\n\nImplementation sources:\n\n- `crates/tui/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.\n- `crates/tui/src/tools/registry.rs` registers canonical tools and hidden aliases.\n- `crates/tui/src/tools/{file,file_tool,shell}.rs` own the small foreground\n primitive behavior and schemas; the other native tools remain searchable.\n- `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.\n\n## Default-active contract\n\nNew turns start with twelve eager native names plus synthetic `tool_search`:\n\n1. `read`\n2. `write`\n3. `edit`\n4. `file_search`\n5. `bash`\n6. `agent`\n7. `workflow`\n8. `todo_write`\n9. `create_goal`\n10. `get_goal`\n11. `update_goal`\n12. `load_skill`\n13. `tool_search` (synthetic, always active)\n\nThe twelve native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in\n`crates/tui/src/core/engine/tool_catalog.rs`, pinned by\n`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority\nboundary may remove `agent` at the maximum child depth, but route size alone\nmust not change this core vocabulary.\n\nThe direct schemas deliberately stay small:\n\n| Tool | Input | Purpose |\n|---|---|---|\n| `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |\n| `write` | `path`, `content` | Create or replace a file. |\n| `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |\n| `bash` | `command`, optional `timeout`, optional `description` | Run one cancellable foreground shell command and return a bounded tail. The optional description is display-only. |\n| `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |\n| `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |\n| `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |\n| `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |\n| `get_goal` | none | Read the active goal and its progress. |\n| `update_goal` | terminal status | Mark the goal complete or blocked. |\n| `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |\n\nMode is an authority decision, not a synonym system. Plan, Work, and Operate\nuse the same primitive identities. Plan centrally refuses `write`, `edit`, and\n`bash`; Work and Operate still pass those calls through approval, sandbox,\ntrusted-path, repository-law, and managed-policy gates. Full Access changes\nordinary approval behavior but does not bypass hard safety or repository law.\n\n`update_plan` remains registered only for saved-artifact compatibility and is\nnot model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other\nspecialized capabilities are searchable rather than first-turn ceremony.\n\nA Bash description appears as a muted, bounded note under the command on the\nTUI shell card, including on automatic runs. It never replaces the command and\nis excluded from execution, exact approval keys and Auto-Review input. The\noriginal description remains in the recorded tool input and exports. Approval\nprompts, `/jobs`, the activity inspector, ACP and other Runtime clients keep\nshowing the command; this change adds no description row to those surfaces.\n\n## Local code execution\n\n`code_execution` (Python) and `js_execution` (Node.js) use the same\npermission-aware local launcher as workspace task gates and test runs. Ordinary\napproval retains the effective session policy. To retry a denied exact call\nwith wider permissions, supply `sandbox_permissions` (`workspace-write` or\n`danger-full-access`) and a nonempty `justification`; Ask mode requires explicit\none-shot user approval. The approved policy affects only that call.\n\nBoth tools read source from stdin, return stdout/stderr/return_code, and keep\nthe existing timeout and process-tree cleanup. They are not persistent REPLs;\ntracebacks refer to stdin rather than a temporary script path. The separate\nRLM/REPL kernel is not changed by this launcher integration.\n\nPlatform/backend limits of the shared launcher still apply: no available local\nwrapper means workspace-write remains unLine truncated
|
||||
"sha256": "17263d9a07cb5003a131a58de11e0b7f3267cc25d33ff463540484be7fc4a3be",
|
||||
"content": "# Tool surface\n\n> 阅读简体中文版:[zh_hans/TOOL_SURFACE.md](zh_hans/TOOL_SURFACE.md)。\n\nThis document describes the current model-facing tool contract. The v0.9.1\ncutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;\nread the workspace version from `Cargo.toml`, not from this line. The registry\nremains larger than the first-turn catalog so\nsaved transcripts can replay and uncommon capabilities can be loaded on demand.\nThe model should learn one canonical name for each common operation.\n\nImplementation sources:\n\n- `crates/tui/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.\n- `crates/tui/src/tools/registry.rs` registers canonical tools and hidden aliases.\n- `crates/tui/src/tools/{file,file_tool,shell}.rs` own the small foreground\n primitive behavior and schemas; the other native tools remain searchable.\n- `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.\n\n## Default-active contract\n\nNew turns start with twelve eager native names plus synthetic `tool_search`:\n\n1. `read`\n2. `write`\n3. `edit`\n4. `file_search`\n5. `bash`\n6. `agent`\n7. `workflow`\n8. `todo_write`\n9. `create_goal`\n10. `get_goal`\n11. `update_goal`\n12. `load_skill`\n13. `tool_search` (synthetic, always active)\n\nThe twelve native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in\n`crates/tui/src/core/engine/tool_catalog.rs`, pinned by\n`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority\nboundary may remove `agent` at the maximum child depth, but route size alone\nmust not change this core vocabulary.\n\nThe direct schemas deliberately stay small:\n\n| Tool | Input | Purpose |\n|---|---|---|\n| `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |\n| `write` | `path`, `content` | Create or replace a file. |\n| `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |\n| `bash` | `command`, optional `timeout`, optional `description` | Run one cancellable foreground shell command and return a bounded tail. The optional description is display-only. |\n| `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |\n| `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |\n| `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |\n| `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |\n| `get_goal` | none | Read the active goal and its progress. |\n| `update_goal` | terminal status | Mark the goal complete or blocked. |\n| `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |\n\nMode is an authority decision, not a synonym system. Plan, Work, and Operate\nuse the same primitive identities. Plan centrally refuses `write`, `edit`, and\n`bash`; Work and Operate still pass those calls through approval, sandbox,\ntrusted-path, repository-law, and managed-policy gates. Full Access changes\nordinary approval behavior but does not bypass hard safety or repository law.\n\n`update_plan` remains registered only for saved-artifact compatibility and is\nnot model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other\nspecialized capabilities are searchable rather than first-turn ceremony.\n\nA Bash description appears as a muted, bounded note under the command on the\nTUI shell card, including on automatic runs. It never replaces the command and\nis excluded from execution, exact approval keys and Auto-Review input. The\noriginal description remains in the recorded tool input and exports. Approval\nprompts, `/jobs`, the activity inspector, ACP and other Runtime clients keep\nshowing the command; this change adds no description row to those surfaces.\n\n## Local code execution\n\n`code_execution` (Python) and `js_execution` (Node.js) use the same\npermission-aware local launcher as workspace task gates and test runs. Ordinary\napproval retains the effective session policy. To retry a denied exact call\nwith wider permissions, supply `sandbox_permissions` (`workspace-write` or\n`danger-full-access`) and a nonempty `justification`; Ask mode requires explicit\none-shot user approval. The approved policy affects only that call.\n\nBoth tools read source from stdin, return stdout/stderr/return_code, and keep\nthe existing timeout and process-tree cleanup. They are not persistent REPLs;\ntracebacks refer to stdin rather than a temporary script path. The separate\nRLM/REPL kernel is not changed by this launcher integration.\n\nPlatform/backend limits of the shared launcher still apply: no available local\nwrapper means workspace-write remains unLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/RUNTIME_SIMPLIFICATION_DESIGN.md",
|
||||
|
||||
Reference in new issue
Block a user